jinx 2 месяцев назад
Родитель
Сommit
96ab6ca5e8

+ 1 - 0
.env.development

@@ -0,0 +1 @@
+VITE_API_BASE_URL=/api

+ 1 - 0
.env.production

@@ -0,0 +1 @@
+VITE_API_BASE_URL=/api

+ 24 - 0
.gitignore

@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?

+ 13 - 0
index.html

@@ -0,0 +1,13 @@
+<!doctype html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>韩山师范学院文献数字化平台</title>
+  </head>
+  <body>
+    <div id="app"></div>
+    <script type="module" src="/src/main.js"></script>
+  </body>
+</html>

+ 9 - 0
jsconfig.json

@@ -0,0 +1,9 @@
+{
+  "compilerOptions": {
+    "baseUrl": ".",
+    "paths": {
+      "@/*": ["src/*"]
+    }
+  },
+  "exclude": ["node_modules", "dist"]
+}

Разница между файлами не показана из-за своего большого размера
+ 1495 - 0
package-lock.json


+ 21 - 0
package.json

@@ -0,0 +1,21 @@
+{
+  "name": "hanshan_dpd",
+  "private": true,
+  "version": "0.0.0",
+  "type": "module",
+  "scripts": {
+    "dev": "vite",
+    "build": "vite build",
+    "preview": "vite preview"
+  },
+  "devDependencies": {
+    "@vitejs/plugin-vue": "^6.0.7",
+    "vite": "^8.0.12"
+  },
+  "dependencies": {
+    "axios": "^1.18.0",
+    "vue": "^3.5.38",
+    "vue-router": "^4.6.4",
+    "vuex": "^4.1.0"
+  }
+}

Разница между файлами не показана из-за своего большого размера
+ 1 - 0
public/favicon.svg


Разница между файлами не показана из-за своего большого размера
+ 24 - 0
public/icons.svg


+ 88 - 0
src/App.vue

@@ -0,0 +1,88 @@
+<script setup>
+import { computed } from 'vue'
+import { useRoute, useRouter } from 'vue-router'
+
+const route = useRoute()
+const router = useRouter()
+
+const showHeader = computed(() => route.name !== 'login')
+
+const routeMap = computed(() => {
+  const entries = router
+    .getRoutes()
+    .filter((item) => item.name)
+    .map((item) => [item.name, item])
+
+  return new Map(entries)
+})
+
+const buildRoutePath = (path, params) =>
+  path.replace(/:([^/]+)/g, (_, key) => params[key] ?? '')
+
+const breadcrumbs = computed(() => {
+  const currentRoute = routeMap.value.get(route.name)
+
+  if (!currentRoute || currentRoute.meta?.breadcrumb === false) {
+    return []
+  }
+
+  const chain = []
+  let cursor = currentRoute
+
+  while (cursor) {
+    if (cursor.meta?.breadcrumb !== false) {
+      chain.unshift({
+        name: cursor.name,
+        title: cursor.meta?.title || '',
+        path: buildRoutePath(cursor.path, route.params),
+      })
+    }
+
+    const parentName = cursor.meta?.breadcrumbParent
+    cursor = parentName ? routeMap.value.get(parentName) : null
+  }
+
+  return chain
+})
+
+const showBreadcrumb = computed(
+  () => showHeader.value && breadcrumbs.value.length > 0,
+)
+</script>
+
+<template>
+  <div class="app-shell">
+    <header v-if="showHeader" class="app-header">
+      <div>
+        <p class="eyebrow">Digital Reading</p>
+        <h1>业务路由示例</h1>
+      </div>
+      <nav class="nav">
+        <RouterLink to="/home">首页</RouterLink>
+        <RouterLink to="/resource">资源库</RouterLink>
+        <RouterLink to="/mine">我的</RouterLink>
+        <RouterLink to="/login">登录</RouterLink>
+      </nav>
+    </header>
+
+    <nav v-if="showBreadcrumb" class="breadcrumb" aria-label="breadcrumb">
+      <template v-for="(item, index) in breadcrumbs" :key="item.name">
+        <RouterLink
+          v-if="index < breadcrumbs.length - 1"
+          class="breadcrumb-link"
+          :to="item.path"
+        >
+          {{ item.title }}
+        </RouterLink>
+        <span v-else class="breadcrumb-current">{{ item.title }}</span>
+        <span v-if="index < breadcrumbs.length - 1" class="breadcrumb-separator">
+          /
+        </span>
+      </template>
+    </nav>
+
+    <main class="app-main">
+      <RouterView />
+    </main>
+  </div>
+</template>

+ 5 - 0
src/api/modules/example.js

@@ -0,0 +1,5 @@
+import request, { get } from '@/utils/request'
+
+export const getExampleList = (params) => get('/example/list', params)
+
+export const createExample = (data) => request.post('/example', data)

+ 7 - 0
src/main.js

@@ -0,0 +1,7 @@
+import { createApp } from 'vue'
+import App from './App.vue'
+import router from './router'
+import store from './store'
+import './style.css'
+
+createApp(App).use(store).use(router).mount('#app')

+ 72 - 0
src/router/index.js

@@ -0,0 +1,72 @@
+import { createRouter, createWebHistory } from 'vue-router'
+
+const routes = [
+  {
+    path: '/',
+    redirect: '/home',
+  },
+  {
+    path: '/login',
+    name: 'login',
+    component: () => import('@/views/LoginView.vue'),
+    meta: {
+      title: '登录页',
+    },
+  },
+  {
+    path: '/home',
+    name: 'home',
+    component: () => import('@/views/HomeView.vue'),
+    meta: {
+      title: '首页',
+      breadcrumb: false,
+    },
+  },
+  {
+    path: '/resource',
+    name: 'resource-library',
+    component: () => import('@/views/ResourceLibraryView.vue'),
+    meta: {
+      title: '资源库页',
+      breadcrumbParent: 'home',
+    },
+  },
+  {
+    path: '/resource/:id',
+    name: 'resource-detail',
+    component: () => import('@/views/ResourceDetailView.vue'),
+    meta: {
+      title: '资源详情页',
+      breadcrumbParent: 'resource-library',
+    },
+  },
+  {
+    path: '/reading/:id',
+    name: 'reading-detail',
+    component: () => import('@/views/ReadingDetailView.vue'),
+    meta: {
+      title: '阅读详情页',
+      breadcrumb: false,
+    },
+  },
+  {
+    path: '/mine',
+    name: 'mine',
+    component: () => import('@/views/MineView.vue'),
+    meta: {
+      title: '我的页',
+      breadcrumb: false,
+    },
+  },
+]
+
+const router = createRouter({
+  history: createWebHistory(),
+  routes,
+})
+
+router.afterEach((to) => {
+  document.title = to.meta.title || 'hanshan_dpd'
+})
+
+export default router

+ 32 - 0
src/store/index.js

@@ -0,0 +1,32 @@
+import { createStore } from 'vuex'
+
+const store = createStore({
+  state() {
+    return {
+      count: 0,
+    }
+  },
+  getters: {
+    doubleCount: (state) => state.count * 2,
+  },
+  mutations: {
+    increment(state) {
+      state.count += 1
+    },
+    reset(state) {
+      state.count = 0
+    },
+  },
+  actions: {
+    incrementAsync({ commit }) {
+      return new Promise((resolve) => {
+        setTimeout(() => {
+          commit('increment')
+          resolve()
+        }, 300)
+      })
+    },
+  },
+})
+
+export default store

+ 287 - 0
src/style.css

@@ -0,0 +1,287 @@
+:root {
+  font-family: "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
+  color: #1f2937;
+  background: linear-gradient(135deg, #f5f7fb 0%, #e8eef9 100%);
+  line-height: 1.5;
+  font-weight: 400;
+}
+
+* {
+  box-sizing: border-box;
+}
+
+body {
+  margin: 0;
+  min-width: 320px;
+  min-height: 100vh;
+}
+
+a {
+  color: inherit;
+  text-decoration: none;
+}
+
+button {
+  font: inherit;
+}
+
+#app {
+  min-height: 100vh;
+}
+
+.app-shell {
+  max-width: 1080px;
+  margin: 0 auto;
+  padding: 32px 20px 48px;
+}
+
+.app-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  gap: 24px;
+  margin-bottom: 28px;
+}
+
+.eyebrow {
+  margin: 0 0 8px;
+  font-size: 13px;
+  font-weight: 700;
+  letter-spacing: 0.08em;
+  text-transform: uppercase;
+  color: #2563eb;
+}
+
+.app-header h1 {
+  margin: 0;
+  font-size: clamp(28px, 5vw, 40px);
+}
+
+.nav {
+  display: flex;
+  gap: 12px;
+  padding: 8px;
+  border-radius: 999px;
+  background: rgba(255, 255, 255, 0.7);
+  backdrop-filter: blur(12px);
+}
+
+.nav a {
+  padding: 10px 16px;
+  border-radius: 999px;
+  color: #4b5563;
+  transition: all 0.2s ease;
+}
+
+.nav a.router-link-active {
+  background: #2563eb;
+  color: #fff;
+}
+
+.app-main {
+  display: grid;
+}
+
+.breadcrumb {
+  display: flex;
+  align-items: center;
+  flex-wrap: wrap;
+  gap: 8px;
+  margin-bottom: 20px;
+  padding: 12px 16px;
+  border-radius: 16px;
+  background: rgba(255, 255, 255, 0.72);
+  color: #64748b;
+}
+
+.breadcrumb-link {
+  color: #2563eb;
+  font-weight: 600;
+}
+
+.breadcrumb-current {
+  color: #0f172a;
+  font-weight: 600;
+}
+
+.breadcrumb-separator {
+  color: #94a3b8;
+}
+
+.panel {
+  padding: 28px;
+  border-radius: 24px;
+  background: rgba(255, 255, 255, 0.82);
+  box-shadow: 0 16px 40px rgba(15, 23, 42, 0.08);
+}
+
+.panel h2 {
+  margin-top: 0;
+  margin-bottom: 12px;
+  font-size: 28px;
+}
+
+.panel p {
+  margin: 0 0 16px;
+  color: #4b5563;
+}
+
+.actions {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 12px;
+  margin-top: 24px;
+}
+
+.button {
+  border: 0;
+  border-radius: 12px;
+  padding: 12px 18px;
+  background: #2563eb;
+  color: #fff;
+  cursor: pointer;
+  transition: transform 0.2s ease, box-shadow 0.2s ease;
+}
+
+.button:hover {
+  transform: translateY(-1px);
+  box-shadow: 0 10px 24px rgba(37, 99, 235, 0.2);
+}
+
+.button.secondary {
+  background: #e5edff;
+  color: #1d4ed8;
+}
+
+.card-grid {
+  display: grid;
+  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
+  gap: 16px;
+  margin-top: 24px;
+}
+
+.card {
+  padding: 20px;
+  border-radius: 18px;
+  background: #f8fafc;
+}
+
+.link-card {
+  display: block;
+  transition: transform 0.2s ease, box-shadow 0.2s ease;
+}
+
+.link-card:hover {
+  transform: translateY(-2px);
+  box-shadow: 0 12px 24px rgba(15, 23, 42, 0.08);
+}
+
+.card strong {
+  display: block;
+  margin-bottom: 8px;
+  font-size: 15px;
+}
+
+.list-block {
+  display: grid;
+  gap: 14px;
+  margin-top: 24px;
+}
+
+.list-item {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  gap: 20px;
+  padding: 20px;
+  border-radius: 18px;
+  background: #f8fafc;
+  transition: transform 0.2s ease, box-shadow 0.2s ease;
+}
+
+.list-item:hover {
+  transform: translateY(-2px);
+  box-shadow: 0 12px 24px rgba(15, 23, 42, 0.08);
+}
+
+.list-item p {
+  margin: 8px 0 0;
+}
+
+.code {
+  padding: 2px 8px;
+  border-radius: 999px;
+  background: #dbeafe;
+  color: #1d4ed8;
+}
+
+.text-link {
+  color: #2563eb;
+  font-weight: 600;
+}
+
+.login-page {
+  min-height: calc(100vh - 64px);
+  display: grid;
+  place-items: center;
+}
+
+.login-card {
+  width: min(100%, 440px);
+  padding: 36px 32px;
+  border-radius: 28px;
+  background: rgba(255, 255, 255, 0.88);
+  box-shadow: 0 20px 48px rgba(15, 23, 42, 0.12);
+}
+
+.login-card h2 {
+  margin: 0 0 12px;
+  font-size: 32px;
+}
+
+.login-copy {
+  margin: 0 0 20px;
+  color: #4b5563;
+}
+
+.login-form {
+  display: grid;
+  gap: 14px;
+  margin-bottom: 16px;
+}
+
+.input {
+  width: 100%;
+  border: 1px solid #dbe3f1;
+  border-radius: 14px;
+  padding: 14px 16px;
+  background: #fff;
+  color: #111827;
+  outline: none;
+}
+
+.input:focus {
+  border-color: #2563eb;
+  box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.12);
+}
+
+@media (max-width: 720px) {
+  .app-header {
+    flex-direction: column;
+    align-items: flex-start;
+  }
+
+  .nav {
+    width: 100%;
+    overflow-x: auto;
+  }
+
+  .list-item {
+    align-items: flex-start;
+    flex-direction: column;
+  }
+
+  .login-card {
+    padding: 28px 20px;
+  }
+}

+ 45 - 0
src/utils/request.js

@@ -0,0 +1,45 @@
+import axios from 'axios'
+
+const service = axios.create({
+  baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
+  timeout: 10000,
+  headers: {
+    'Content-Type': 'application/json',
+  },
+})
+
+service.interceptors.request.use(
+  (config) => {
+    const token = localStorage.getItem('token')
+
+    if (token) {
+      config.headers.Authorization = `Bearer ${token}`
+    }
+
+    return config
+  },
+  (error) => Promise.reject(error),
+)
+
+service.interceptors.response.use(
+  (response) => response.data,
+  (error) => {
+    const message =
+      error.response?.data?.message || error.message || 'Request Error'
+
+    return Promise.reject(new Error(message))
+  },
+)
+
+export const get = (url, params = {}, config = {}) =>
+  service.get(url, { params, ...config })
+
+export const post = (url, data = {}, config = {}) =>
+  service.post(url, data, config)
+
+export const put = (url, data = {}, config = {}) =>
+  service.put(url, data, config)
+
+export const del = (url, config = {}) => service.delete(url, config)
+
+export default service

+ 25 - 0
src/views/HomeView.vue

@@ -0,0 +1,25 @@
+<template>
+  <section class="panel">
+    <h2>首页</h2>
+    <p>这里作为业务首页入口,统一放常用页面跳转和内容概览。</p>
+
+    <div class="card-grid">
+      <RouterLink class="card link-card" to="/resource">
+        <strong>资源库页</strong>
+        <span>查看资源列表与分类内容</span>
+      </RouterLink>
+      <RouterLink class="card link-card" to="/resource/1001">
+        <strong>资源详情页</strong>
+        <span>示例参数:resource id = 1001</span>
+      </RouterLink>
+      <RouterLink class="card link-card" to="/reading/9001">
+        <strong>阅读详情页</strong>
+        <span>示例参数:reading id = 9001</span>
+      </RouterLink>
+      <RouterLink class="card link-card" to="/mine">
+        <strong>我的页</strong>
+        <span>查看账号信息、阅读记录和收藏</span>
+      </RouterLink>
+    </div>
+  </section>
+</template>

+ 17 - 0
src/views/LoginView.vue

@@ -0,0 +1,17 @@
+<template>
+  <section class="login-page">
+    <div class="login-card">
+      <p class="eyebrow">Account Access</p>
+      <h2>登录页</h2>
+      <p class="login-copy">这里预留账号登录表单入口,后续可以直接接登录接口。</p>
+
+      <form class="login-form">
+        <input class="input" type="text" placeholder="请输入账号" />
+        <input class="input" type="password" placeholder="请输入密码" />
+        <button class="button" type="button">登录</button>
+      </form>
+
+      <RouterLink class="text-link" to="/home">跳过登录,进入首页</RouterLink>
+    </div>
+  </section>
+</template>

+ 25 - 0
src/views/MineView.vue

@@ -0,0 +1,25 @@
+<template>
+  <section class="panel">
+    <h2>我的页</h2>
+    <p>这里作为个人中心页,后续可以扩展用户资料、阅读记录、收藏和设置。</p>
+
+    <div class="card-grid">
+      <article class="card">
+        <strong>个人信息</strong>
+        <span>昵称、头像、账号绑定信息</span>
+      </article>
+      <article class="card">
+        <strong>阅读记录</strong>
+        <span>最近阅读内容与阅读进度</span>
+      </article>
+      <article class="card">
+        <strong>我的收藏</strong>
+        <span>收藏的资源和文章列表</span>
+      </article>
+      <article class="card">
+        <strong>系统设置</strong>
+        <span>通知、主题、清理缓存等设置项</span>
+      </article>
+    </div>
+  </section>
+</template>

+ 31 - 0
src/views/ReadingDetailView.vue

@@ -0,0 +1,31 @@
+<script setup>
+import { computed } from 'vue'
+import { useRoute } from 'vue-router'
+
+const route = useRoute()
+
+const readingId = computed(() => route.params.id)
+</script>
+
+<template>
+  <section class="panel">
+    <h2>阅读详情页</h2>
+    <p>当前阅读内容 ID:<span class="code">{{ readingId }}</span></p>
+
+    <div class="card-grid">
+      <article class="card">
+        <strong>正文区域</strong>
+        <span>后续可接正文、目录、批注和章节切换功能</span>
+      </article>
+      <article class="card">
+        <strong>阅读状态</strong>
+        <span>可扩展进度、书签、收藏和最近阅读记录</span>
+      </article>
+    </div>
+
+    <div class="actions">
+      <RouterLink class="button secondary" to="/resource">返回资源库</RouterLink>
+      <RouterLink class="button" to="/mine">查看我的</RouterLink>
+    </div>
+  </section>
+</template>

+ 31 - 0
src/views/ResourceDetailView.vue

@@ -0,0 +1,31 @@
+<script setup>
+import { computed } from 'vue'
+import { useRoute } from 'vue-router'
+
+const route = useRoute()
+
+const resourceId = computed(() => route.params.id)
+</script>
+
+<template>
+  <section class="panel">
+    <h2>资源详情页</h2>
+    <p>当前资源 ID:<span class="code">{{ resourceId }}</span></p>
+
+    <div class="card-grid">
+      <article class="card">
+        <strong>资源标题</strong>
+        <span>这里接入资源详情接口返回的标题字段</span>
+      </article>
+      <article class="card">
+        <strong>资源简介</strong>
+        <span>这里接入摘要、作者、来源、分类等信息</span>
+      </article>
+    </div>
+
+    <div class="actions">
+      <RouterLink class="button" :to="`/reading/${resourceId}`">进入阅读详情</RouterLink>
+      <RouterLink class="button secondary" to="/resource">返回资源库</RouterLink>
+    </div>
+  </section>
+</template>

+ 29 - 0
src/views/ResourceLibraryView.vue

@@ -0,0 +1,29 @@
+<script setup>
+const resources = [
+  { id: 1001, title: '寒山文集', desc: '古籍整理与数字化阅读资源' },
+  { id: 1002, title: '地方志库', desc: '按地域归档的志书与参考资料' },
+  { id: 1003, title: '专题影像', desc: '图像、手稿与注释资料集合' },
+]
+</script>
+
+<template>
+  <section class="panel">
+    <h2>资源库页</h2>
+    <p>这里展示资源列表,后续可以接分类筛选、分页和搜索。</p>
+
+    <div class="list-block">
+      <RouterLink
+        v-for="item in resources"
+        :key="item.id"
+        class="list-item"
+        :to="`/resource/${item.id}`"
+      >
+        <div>
+          <strong>{{ item.title }}</strong>
+          <p>{{ item.desc }}</p>
+        </div>
+        <span class="text-link">查看详情</span>
+      </RouterLink>
+    </div>
+  </section>
+</template>

+ 12 - 0
vite.config.js

@@ -0,0 +1,12 @@
+import { defineConfig } from 'vite'
+import vue from '@vitejs/plugin-vue'
+import { fileURLToPath, URL } from 'node:url'
+
+export default defineConfig({
+  plugins: [vue()],
+  resolve: {
+    alias: {
+      '@': fileURLToPath(new URL('./src', import.meta.url)),
+    },
+  },
+})