feat: 初始化三坛大戒H5应用项目
- 完成启动页面设计和动画效果 - 实现首页布局和功能模块 - 添加三师七证、义工、戒子等核心页面 - 集成Vue3 + Vite + Vant UI框架 - 实现页面路由和导航功能 - 添加响应式设计和交互动画
Showing
39 changed files
with
5527 additions
and
0 deletions
.env.development
0 → 100644
.env.production
0 → 100644
.eslintrc-auto-import.json
0 → 100644
| 1 | +{ | ||
| 2 | + "globals": { | ||
| 3 | + "Component": true, | ||
| 4 | + "ComponentPublicInstance": true, | ||
| 5 | + "ComputedRef": true, | ||
| 6 | + "DirectiveBinding": true, | ||
| 7 | + "EffectScope": true, | ||
| 8 | + "ExtractDefaultPropTypes": true, | ||
| 9 | + "ExtractPropTypes": true, | ||
| 10 | + "ExtractPublicPropTypes": true, | ||
| 11 | + "InjectionKey": true, | ||
| 12 | + "MaybeRef": true, | ||
| 13 | + "MaybeRefOrGetter": true, | ||
| 14 | + "PropType": true, | ||
| 15 | + "Ref": true, | ||
| 16 | + "VNode": true, | ||
| 17 | + "WritableComputedRef": true, | ||
| 18 | + "computed": true, | ||
| 19 | + "createApp": true, | ||
| 20 | + "customRef": true, | ||
| 21 | + "defineAsyncComponent": true, | ||
| 22 | + "defineComponent": true, | ||
| 23 | + "effectScope": true, | ||
| 24 | + "getCurrentInstance": true, | ||
| 25 | + "getCurrentScope": true, | ||
| 26 | + "h": true, | ||
| 27 | + "inject": true, | ||
| 28 | + "isProxy": true, | ||
| 29 | + "isReactive": true, | ||
| 30 | + "isReadonly": true, | ||
| 31 | + "isRef": true, | ||
| 32 | + "markRaw": true, | ||
| 33 | + "nextTick": true, | ||
| 34 | + "onActivated": true, | ||
| 35 | + "onBeforeMount": true, | ||
| 36 | + "onBeforeRouteLeave": true, | ||
| 37 | + "onBeforeRouteUpdate": true, | ||
| 38 | + "onBeforeUnmount": true, | ||
| 39 | + "onBeforeUpdate": true, | ||
| 40 | + "onDeactivated": true, | ||
| 41 | + "onErrorCaptured": true, | ||
| 42 | + "onMounted": true, | ||
| 43 | + "onRenderTracked": true, | ||
| 44 | + "onRenderTriggered": true, | ||
| 45 | + "onScopeDispose": true, | ||
| 46 | + "onServerPrefetch": true, | ||
| 47 | + "onUnmounted": true, | ||
| 48 | + "onUpdated": true, | ||
| 49 | + "onWatcherCleanup": true, | ||
| 50 | + "provide": true, | ||
| 51 | + "reactive": true, | ||
| 52 | + "readonly": true, | ||
| 53 | + "ref": true, | ||
| 54 | + "resolveComponent": true, | ||
| 55 | + "shallowReactive": true, | ||
| 56 | + "shallowReadonly": true, | ||
| 57 | + "shallowRef": true, | ||
| 58 | + "toRaw": true, | ||
| 59 | + "toRef": true, | ||
| 60 | + "toRefs": true, | ||
| 61 | + "toValue": true, | ||
| 62 | + "triggerRef": true, | ||
| 63 | + "unref": true, | ||
| 64 | + "useAttrs": true, | ||
| 65 | + "useCssModule": true, | ||
| 66 | + "useCssVars": true, | ||
| 67 | + "useId": true, | ||
| 68 | + "useLink": true, | ||
| 69 | + "useModel": true, | ||
| 70 | + "useRoute": true, | ||
| 71 | + "useRouter": true, | ||
| 72 | + "useSlots": true, | ||
| 73 | + "useTemplateRef": true, | ||
| 74 | + "watch": true, | ||
| 75 | + "watchEffect": true, | ||
| 76 | + "watchPostEffect": true, | ||
| 77 | + "watchSyncEffect": true | ||
| 78 | + } | ||
| 79 | +} |
.gitignore
0 → 100644
| 1 | +# Logs | ||
| 2 | +logs | ||
| 3 | +*.log | ||
| 4 | +npm-debug.log* | ||
| 5 | +yarn-debug.log* | ||
| 6 | +yarn-error.log* | ||
| 7 | +pnpm-debug.log* | ||
| 8 | +lerna-debug.log* | ||
| 9 | + | ||
| 10 | +node_modules | ||
| 11 | +dist | ||
| 12 | +dist-ssr | ||
| 13 | +*.local | ||
| 14 | + | ||
| 15 | +# Editor directories and files | ||
| 16 | +.vscode/* | ||
| 17 | +!.vscode/extensions.json | ||
| 18 | +.idea | ||
| 19 | +.DS_Store | ||
| 20 | +*.suo | ||
| 21 | +*.ntvs* | ||
| 22 | +*.njsproj | ||
| 23 | +*.sln | ||
| 24 | +*.sw? | ||
| 25 | + | ||
| 26 | +# Environment variables | ||
| 27 | +.env | ||
| 28 | +.env.local | ||
| 29 | +.env.development.local | ||
| 30 | +.env.test.local | ||
| 31 | +.env.production.local | ||
| 32 | + | ||
| 33 | +# Build outputs | ||
| 34 | +build/ | ||
| 35 | +coverage/ | ||
| 36 | + | ||
| 37 | +# Cache | ||
| 38 | +.cache/ | ||
| 39 | +.parcel-cache/ | ||
| 40 | +.eslintcache | ||
| 41 | + | ||
| 42 | +# History | ||
| 43 | +.history/ | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
README.md
0 → 100644
| 1 | +# H5 Vite Template | ||
| 2 | + | ||
| 3 | +基于 Vue 3 + Vite + Vant 4 的移动端 H5 项目模板 | ||
| 4 | + | ||
| 5 | +## 特性 | ||
| 6 | + | ||
| 7 | +- ⚡️ **Vite** - 极速的开发体验 | ||
| 8 | +- 🖖 **Vue 3** - 渐进式 JavaScript 框架 | ||
| 9 | +- 📱 **Vant 4** - 轻量、可靠的移动端组件库 | ||
| 10 | +- 🎨 **Tailwind CSS** - 原子化 CSS 框架 | ||
| 11 | +- 📦 **Pinia** - 符合直觉的 Vue.js 状态管理库 | ||
| 12 | +- 🛣️ **Vue Router** - Vue.js 官方路由 | ||
| 13 | +- 📡 **Axios** - 基于 Promise 的 HTTP 客户端 | ||
| 14 | +- 🔧 **ESLint** - 代码质量检查 | ||
| 15 | +- 📐 **PostCSS** - CSS 后处理器 | ||
| 16 | +- 📱 **移动端适配** - 基于 postcss-px-to-viewport 的移动端适配方案 | ||
| 17 | + | ||
| 18 | +## 目录结构 | ||
| 19 | + | ||
| 20 | +``` | ||
| 21 | +h5_vite_template/ | ||
| 22 | +├── public/ # 静态资源 | ||
| 23 | +├── src/ | ||
| 24 | +│ ├── api/ # API 接口 | ||
| 25 | +│ ├── assets/ # 资源文件 | ||
| 26 | +│ ├── components/ # 通用组件 | ||
| 27 | +│ ├── router/ # 路由配置 | ||
| 28 | +│ ├── stores/ # 状态管理 | ||
| 29 | +│ ├── utils/ # 工具函数 | ||
| 30 | +│ ├── views/ # 页面组件 | ||
| 31 | +│ ├── App.vue # 根组件 | ||
| 32 | +│ ├── main.js # 入口文件 | ||
| 33 | +│ └── style.css # 全局样式 | ||
| 34 | +├── .env # 环境变量 | ||
| 35 | +├── .env.development # 开发环境变量 | ||
| 36 | +├── .env.production # 生产环境变量 | ||
| 37 | +├── .gitignore # Git 忽略文件 | ||
| 38 | +├── index.html # HTML 模板 | ||
| 39 | +├── package.json # 项目配置 | ||
| 40 | +├── postcss.config.js # PostCSS 配置 | ||
| 41 | +├── tailwind.config.js # Tailwind CSS 配置 | ||
| 42 | +└── vite.config.js # Vite 配置 | ||
| 43 | +``` | ||
| 44 | + | ||
| 45 | +## 快速开始 | ||
| 46 | + | ||
| 47 | +### 安装依赖 | ||
| 48 | + | ||
| 49 | +```bash | ||
| 50 | +npm install | ||
| 51 | +# 或 | ||
| 52 | +yarn install | ||
| 53 | +# 或 | ||
| 54 | +pnpm install | ||
| 55 | +``` | ||
| 56 | + | ||
| 57 | +### 开发 | ||
| 58 | + | ||
| 59 | +```bash | ||
| 60 | +npm run dev | ||
| 61 | +# 或 | ||
| 62 | +yarn dev | ||
| 63 | +# 或 | ||
| 64 | +pnpm dev | ||
| 65 | +``` | ||
| 66 | + | ||
| 67 | +### 构建 | ||
| 68 | + | ||
| 69 | +```bash | ||
| 70 | +npm run build | ||
| 71 | +# 或 | ||
| 72 | +yarn build | ||
| 73 | +# 或 | ||
| 74 | +pnpm build | ||
| 75 | +``` | ||
| 76 | + | ||
| 77 | +### 预览 | ||
| 78 | + | ||
| 79 | +```bash | ||
| 80 | +npm run preview | ||
| 81 | +# 或 | ||
| 82 | +yarn preview | ||
| 83 | +# 或 | ||
| 84 | +pnpm preview | ||
| 85 | +``` | ||
| 86 | + | ||
| 87 | +## 配置说明 | ||
| 88 | + | ||
| 89 | +### 环境变量 | ||
| 90 | + | ||
| 91 | +项目支持多环境配置,通过 `.env` 文件进行管理: | ||
| 92 | + | ||
| 93 | +- `.env` - 所有环境的默认配置 | ||
| 94 | +- `.env.development` - 开发环境配置 | ||
| 95 | +- `.env.production` - 生产环境配置 | ||
| 96 | + | ||
| 97 | +### 移动端适配 | ||
| 98 | + | ||
| 99 | +项目使用 `postcss-px-to-viewport` 进行移动端适配,设计稿基准为 375px。 | ||
| 100 | + | ||
| 101 | +### 路由配置 | ||
| 102 | + | ||
| 103 | +路由配置位于 `src/router/index.js`,支持: | ||
| 104 | + | ||
| 105 | +- 路由懒加载 | ||
| 106 | +- 路由守卫 | ||
| 107 | +- 页面标题设置 | ||
| 108 | +- 滚动行为控制 | ||
| 109 | + | ||
| 110 | +### 状态管理 | ||
| 111 | + | ||
| 112 | +使用 Pinia 进行状态管理,store 文件位于 `src/stores/` 目录。 | ||
| 113 | + | ||
| 114 | +### API 请求 | ||
| 115 | + | ||
| 116 | +API 请求基于 Axios 封装,配置文件位于 `src/utils/request.js`,支持: | ||
| 117 | + | ||
| 118 | +- 请求/响应拦截器 | ||
| 119 | +- 自动 Loading | ||
| 120 | +- 错误处理 | ||
| 121 | +- Token 自动携带 | ||
| 122 | + | ||
| 123 | +## 组件说明 | ||
| 124 | + | ||
| 125 | +### 页面组件 | ||
| 126 | + | ||
| 127 | +- **Home** - 首页,展示轮播图、菜单网格、通知栏等 | ||
| 128 | +- **About** - 关于页面,展示项目信息和功能特性 | ||
| 129 | +- **Profile** - 个人中心,展示用户信息和功能菜单 | ||
| 130 | +- **Demo** - 组件演示页面,展示 Vant 组件使用示例 | ||
| 131 | +- **NotFound** - 404 页面 | ||
| 132 | + | ||
| 133 | +### 通用组件 | ||
| 134 | + | ||
| 135 | +- **LoadingSpinner** - 加载动画组件 | ||
| 136 | +- **EmptyState** - 空状态组件 | ||
| 137 | + | ||
| 138 | +## 开发规范 | ||
| 139 | + | ||
| 140 | +### 代码风格 | ||
| 141 | + | ||
| 142 | +项目使用 ESLint 进行代码质量检查,请遵循以下规范: | ||
| 143 | + | ||
| 144 | +- 使用 2 空格缩进 | ||
| 145 | +- 使用单引号 | ||
| 146 | +- 行末不加分号 | ||
| 147 | +- 组件名使用 PascalCase | ||
| 148 | +- 文件名使用 kebab-case | ||
| 149 | + | ||
| 150 | +### Git 提交规范 | ||
| 151 | + | ||
| 152 | +建议使用以下格式进行 Git 提交: | ||
| 153 | + | ||
| 154 | +``` | ||
| 155 | +<type>(<scope>): <subject> | ||
| 156 | + | ||
| 157 | +<body> | ||
| 158 | + | ||
| 159 | +<footer> | ||
| 160 | +``` | ||
| 161 | + | ||
| 162 | +类型说明: | ||
| 163 | +- `feat`: 新功能 | ||
| 164 | +- `fix`: 修复 bug | ||
| 165 | +- `docs`: 文档更新 | ||
| 166 | +- `style`: 代码格式调整 | ||
| 167 | +- `refactor`: 代码重构 | ||
| 168 | +- `test`: 测试相关 | ||
| 169 | +- `chore`: 构建过程或辅助工具的变动 | ||
| 170 | + | ||
| 171 | +## 部署 | ||
| 172 | + | ||
| 173 | +### 构建产物 | ||
| 174 | + | ||
| 175 | +执行 `npm run build` 后,构建产物将输出到 `dist` 目录。 | ||
| 176 | + | ||
| 177 | +### 静态部署 | ||
| 178 | + | ||
| 179 | +构建产物可以部署到任何静态文件服务器,如: | ||
| 180 | + | ||
| 181 | +- Nginx | ||
| 182 | +- Apache | ||
| 183 | +- Vercel | ||
| 184 | +- Netlify | ||
| 185 | +- GitHub Pages | ||
| 186 | + | ||
| 187 | +### 注意事项 | ||
| 188 | + | ||
| 189 | +1. 如果部署到子路径,需要在 `vite.config.js` 中配置 `base` 选项 | ||
| 190 | +2. 确保服务器支持 History 模式的路由 | ||
| 191 | +3. 生产环境需要配置正确的 API 地址 | ||
| 192 | + | ||
| 193 | +## 浏览器支持 | ||
| 194 | + | ||
| 195 | +- Chrome >= 87 | ||
| 196 | +- Firefox >= 78 | ||
| 197 | +- Safari >= 14 | ||
| 198 | +- iOS Safari >= 14.4 | ||
| 199 | +- Android Browser >= 87 | ||
| 200 | + | ||
| 201 | +## 许可证 | ||
| 202 | + | ||
| 203 | +MIT License | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
index.html
0 → 100644
| 1 | +<!DOCTYPE html> | ||
| 2 | +<html lang="zh-CN"> | ||
| 3 | + <head> | ||
| 4 | + <meta charset="UTF-8" /> | ||
| 5 | + <link rel="icon" type="image/svg+xml" href="/vite.svg" /> | ||
| 6 | + <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no" /> | ||
| 7 | + <meta name="format-detection" content="telephone=no" /> | ||
| 8 | + <meta name="apple-mobile-web-app-capable" content="yes" /> | ||
| 9 | + <meta name="apple-mobile-web-app-status-bar-style" content="black" /> | ||
| 10 | + <title>H5 Vite Template</title> | ||
| 11 | + </head> | ||
| 12 | + <body> | ||
| 13 | + <div id="app"></div> | ||
| 14 | + <script type="module" src="/src/main.js"></script> | ||
| 15 | + </body> | ||
| 16 | +</html> | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
package-lock.json
0 → 100644
This diff could not be displayed because it is too large.
package.json
0 → 100644
| 1 | +{ | ||
| 2 | + "name": "stdj-h5", | ||
| 3 | + "description": "三坛大戒", | ||
| 4 | + "version": "1.0.0", | ||
| 5 | + "type": "module", | ||
| 6 | + "scripts": { | ||
| 7 | + "dev": "vite", | ||
| 8 | + "start": "vite --host 0.0.0.0", | ||
| 9 | + "build": "vite build", | ||
| 10 | + "build-watch": "vite build --watch", | ||
| 11 | + "serve": "vite preview", | ||
| 12 | + "lint": "eslint . --ext vue,js,jsx,cjs,mjs --fix --ignore-path .gitignore" | ||
| 13 | + }, | ||
| 14 | + "dependencies": { | ||
| 15 | + "@vant/area-data": "^1.3.1", | ||
| 16 | + "@vant/touch-emulator": "^1.4.0", | ||
| 17 | + "@vueuse/core": "^10.7.2", | ||
| 18 | + "axios": "^1.6.7", | ||
| 19 | + "dayjs": "^1.11.10", | ||
| 20 | + "js-cookie": "^3.0.5", | ||
| 21 | + "lodash": "^4.17.21", | ||
| 22 | + "pinia": "^2.1.7", | ||
| 23 | + "vant": "^4.9.1", | ||
| 24 | + "vue": "^3.4.15", | ||
| 25 | + "vue-router": "^4.2.5" | ||
| 26 | + }, | ||
| 27 | + "devDependencies": { | ||
| 28 | + "@vitejs/plugin-vue": "^5.0.3", | ||
| 29 | + "autoprefixer": "^10.4.17", | ||
| 30 | + "postcss": "^8.4.35", | ||
| 31 | + "postcss-px-to-viewport": "^1.1.1", | ||
| 32 | + "tailwindcss": "^3.4.1", | ||
| 33 | + "unplugin-auto-import": "^0.17.5", | ||
| 34 | + "unplugin-vue-components": "^0.26.0", | ||
| 35 | + "vite": "^5.1.0" | ||
| 36 | + } | ||
| 37 | +} |
postcss.config.js
0 → 100644
| 1 | +export default { | ||
| 2 | + plugins: { | ||
| 3 | + tailwindcss: {}, | ||
| 4 | + autoprefixer: {}, | ||
| 5 | + 'postcss-px-to-viewport': { | ||
| 6 | + unitToConvert: 'px', | ||
| 7 | + viewportWidth: 375, | ||
| 8 | + unitPrecision: 6, | ||
| 9 | + propList: ['*'], | ||
| 10 | + viewportUnit: 'vw', | ||
| 11 | + fontViewportUnit: 'vw', | ||
| 12 | + selectorBlackList: ['ignore-'], | ||
| 13 | + minPixelValue: 1, | ||
| 14 | + mediaQuery: true, | ||
| 15 | + replace: true, | ||
| 16 | + exclude: [], | ||
| 17 | + landscape: false | ||
| 18 | + } | ||
| 19 | + } | ||
| 20 | +} | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
src/App.vue
0 → 100644
src/api/index.js
0 → 100644
| 1 | +import request from '@/utils/request' | ||
| 2 | + | ||
| 3 | +// 用户相关 API | ||
| 4 | +export const userApi = { | ||
| 5 | + // 获取用户信息 | ||
| 6 | + getUserInfo: () => request.get('/user/info'), | ||
| 7 | + | ||
| 8 | + // 更新用户信息 | ||
| 9 | + updateUserInfo: (data) => request.put('/user/info', data), | ||
| 10 | + | ||
| 11 | + // 用户登录 | ||
| 12 | + login: (data) => request.post('/user/login', data), | ||
| 13 | + | ||
| 14 | + // 用户注册 | ||
| 15 | + register: (data) => request.post('/user/register', data), | ||
| 16 | + | ||
| 17 | + // 用户登出 | ||
| 18 | + logout: () => request.post('/user/logout') | ||
| 19 | +} | ||
| 20 | + | ||
| 21 | +// 通用 API | ||
| 22 | +export const commonApi = { | ||
| 23 | + // 上传文件 | ||
| 24 | + upload: (file) => { | ||
| 25 | + const formData = new FormData() | ||
| 26 | + formData.append('file', file) | ||
| 27 | + return request.post('/upload', formData, { | ||
| 28 | + headers: { | ||
| 29 | + 'Content-Type': 'multipart/form-data' | ||
| 30 | + } | ||
| 31 | + }) | ||
| 32 | + }, | ||
| 33 | + | ||
| 34 | + // 获取配置信息 | ||
| 35 | + getConfig: () => request.get('/config'), | ||
| 36 | + | ||
| 37 | + // 发送验证码 | ||
| 38 | + sendSms: (phone) => request.post('/sms/send', { phone }) | ||
| 39 | +} | ||
| 40 | + | ||
| 41 | +// 示例 API | ||
| 42 | +export const demoApi = { | ||
| 43 | + // 获取列表数据 | ||
| 44 | + getList: (params) => request.get('/demo/list', { params }), | ||
| 45 | + | ||
| 46 | + // 获取详情 | ||
| 47 | + getDetail: (id) => request.get(`/demo/${id}`), | ||
| 48 | + | ||
| 49 | + // 创建数据 | ||
| 50 | + create: (data) => request.post('/demo', data), | ||
| 51 | + | ||
| 52 | + // 更新数据 | ||
| 53 | + update: (id, data) => request.put(`/demo/${id}`, data), | ||
| 54 | + | ||
| 55 | + // 删除数据 | ||
| 56 | + delete: (id) => request.delete(`/demo/${id}`) | ||
| 57 | +} | ||
| 58 | + | ||
| 59 | +// 导出所有 API | ||
| 60 | +export default { | ||
| 61 | + userApi, | ||
| 62 | + commonApi, | ||
| 63 | + demoApi | ||
| 64 | +} | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
src/auto-imports.d.ts
0 → 100644
| 1 | +/* eslint-disable */ | ||
| 2 | +/* prettier-ignore */ | ||
| 3 | +// @ts-nocheck | ||
| 4 | +// noinspection JSUnusedGlobalSymbols | ||
| 5 | +// Generated by unplugin-auto-import | ||
| 6 | +export {} | ||
| 7 | +declare global { | ||
| 8 | + const EffectScope: typeof import('vue')['EffectScope'] | ||
| 9 | + const computed: typeof import('vue')['computed'] | ||
| 10 | + const createApp: typeof import('vue')['createApp'] | ||
| 11 | + const customRef: typeof import('vue')['customRef'] | ||
| 12 | + const defineAsyncComponent: typeof import('vue')['defineAsyncComponent'] | ||
| 13 | + const defineComponent: typeof import('vue')['defineComponent'] | ||
| 14 | + const effectScope: typeof import('vue')['effectScope'] | ||
| 15 | + const getCurrentInstance: typeof import('vue')['getCurrentInstance'] | ||
| 16 | + const getCurrentScope: typeof import('vue')['getCurrentScope'] | ||
| 17 | + const h: typeof import('vue')['h'] | ||
| 18 | + const inject: typeof import('vue')['inject'] | ||
| 19 | + const isProxy: typeof import('vue')['isProxy'] | ||
| 20 | + const isReactive: typeof import('vue')['isReactive'] | ||
| 21 | + const isReadonly: typeof import('vue')['isReadonly'] | ||
| 22 | + const isRef: typeof import('vue')['isRef'] | ||
| 23 | + const markRaw: typeof import('vue')['markRaw'] | ||
| 24 | + const nextTick: typeof import('vue')['nextTick'] | ||
| 25 | + const onActivated: typeof import('vue')['onActivated'] | ||
| 26 | + const onBeforeMount: typeof import('vue')['onBeforeMount'] | ||
| 27 | + const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave'] | ||
| 28 | + const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate'] | ||
| 29 | + const onBeforeUnmount: typeof import('vue')['onBeforeUnmount'] | ||
| 30 | + const onBeforeUpdate: typeof import('vue')['onBeforeUpdate'] | ||
| 31 | + const onDeactivated: typeof import('vue')['onDeactivated'] | ||
| 32 | + const onErrorCaptured: typeof import('vue')['onErrorCaptured'] | ||
| 33 | + const onMounted: typeof import('vue')['onMounted'] | ||
| 34 | + const onRenderTracked: typeof import('vue')['onRenderTracked'] | ||
| 35 | + const onRenderTriggered: typeof import('vue')['onRenderTriggered'] | ||
| 36 | + const onScopeDispose: typeof import('vue')['onScopeDispose'] | ||
| 37 | + const onServerPrefetch: typeof import('vue')['onServerPrefetch'] | ||
| 38 | + const onUnmounted: typeof import('vue')['onUnmounted'] | ||
| 39 | + const onUpdated: typeof import('vue')['onUpdated'] | ||
| 40 | + const onWatcherCleanup: typeof import('vue')['onWatcherCleanup'] | ||
| 41 | + const provide: typeof import('vue')['provide'] | ||
| 42 | + const reactive: typeof import('vue')['reactive'] | ||
| 43 | + const readonly: typeof import('vue')['readonly'] | ||
| 44 | + const ref: typeof import('vue')['ref'] | ||
| 45 | + const resolveComponent: typeof import('vue')['resolveComponent'] | ||
| 46 | + const shallowReactive: typeof import('vue')['shallowReactive'] | ||
| 47 | + const shallowReadonly: typeof import('vue')['shallowReadonly'] | ||
| 48 | + const shallowRef: typeof import('vue')['shallowRef'] | ||
| 49 | + const toRaw: typeof import('vue')['toRaw'] | ||
| 50 | + const toRef: typeof import('vue')['toRef'] | ||
| 51 | + const toRefs: typeof import('vue')['toRefs'] | ||
| 52 | + const toValue: typeof import('vue')['toValue'] | ||
| 53 | + const triggerRef: typeof import('vue')['triggerRef'] | ||
| 54 | + const unref: typeof import('vue')['unref'] | ||
| 55 | + const useAttrs: typeof import('vue')['useAttrs'] | ||
| 56 | + const useCssModule: typeof import('vue')['useCssModule'] | ||
| 57 | + const useCssVars: typeof import('vue')['useCssVars'] | ||
| 58 | + const useId: typeof import('vue')['useId'] | ||
| 59 | + const useLink: typeof import('vue-router')['useLink'] | ||
| 60 | + const useModel: typeof import('vue')['useModel'] | ||
| 61 | + const useRoute: typeof import('vue-router')['useRoute'] | ||
| 62 | + const useRouter: typeof import('vue-router')['useRouter'] | ||
| 63 | + const useSlots: typeof import('vue')['useSlots'] | ||
| 64 | + const useTemplateRef: typeof import('vue')['useTemplateRef'] | ||
| 65 | + const watch: typeof import('vue')['watch'] | ||
| 66 | + const watchEffect: typeof import('vue')['watchEffect'] | ||
| 67 | + const watchPostEffect: typeof import('vue')['watchPostEffect'] | ||
| 68 | + const watchSyncEffect: typeof import('vue')['watchSyncEffect'] | ||
| 69 | +} | ||
| 70 | +// for type re-export | ||
| 71 | +declare global { | ||
| 72 | + // @ts-ignore | ||
| 73 | + export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue' | ||
| 74 | + import('vue') | ||
| 75 | +} |
src/components.d.ts
0 → 100644
| 1 | +/* eslint-disable */ | ||
| 2 | +/* prettier-ignore */ | ||
| 3 | +// @ts-nocheck | ||
| 4 | +// Generated by unplugin-vue-components | ||
| 5 | +// Read more: https://github.com/vuejs/core/pull/3399 | ||
| 6 | +export {} | ||
| 7 | + | ||
| 8 | +declare module 'vue' { | ||
| 9 | + export interface GlobalComponents { | ||
| 10 | + EmptyState: typeof import('./components/EmptyState.vue')['default'] | ||
| 11 | + LoadingSpinner: typeof import('./components/LoadingSpinner.vue')['default'] | ||
| 12 | + RouterLink: typeof import('vue-router')['RouterLink'] | ||
| 13 | + RouterView: typeof import('vue-router')['RouterView'] | ||
| 14 | + VanBadge: typeof import('vant/es')['Badge'] | ||
| 15 | + VanButton: typeof import('vant/es')['Button'] | ||
| 16 | + VanCard: typeof import('vant/es')['Card'] | ||
| 17 | + VanCell: typeof import('vant/es')['Cell'] | ||
| 18 | + VanCellGroup: typeof import('vant/es')['CellGroup'] | ||
| 19 | + VanCheckbox: typeof import('vant/es')['Checkbox'] | ||
| 20 | + VanCollapse: typeof import('vant/es')['Collapse'] | ||
| 21 | + VanCollapseItem: typeof import('vant/es')['CollapseItem'] | ||
| 22 | + VanField: typeof import('vant/es')['Field'] | ||
| 23 | + VanForm: typeof import('vant/es')['Form'] | ||
| 24 | + VanGrid: typeof import('vant/es')['Grid'] | ||
| 25 | + VanGridItem: typeof import('vant/es')['GridItem'] | ||
| 26 | + VanIcon: typeof import('vant/es')['Icon'] | ||
| 27 | + VanImage: typeof import('vant/es')['Image'] | ||
| 28 | + VanNavBar: typeof import('vant/es')['NavBar'] | ||
| 29 | + VanNoticeBar: typeof import('vant/es')['NoticeBar'] | ||
| 30 | + VanProgress: typeof import('vant/es')['Progress'] | ||
| 31 | + VanRate: typeof import('vant/es')['Rate'] | ||
| 32 | + VanSidebar: typeof import('vant/es')['Sidebar'] | ||
| 33 | + VanSidebarItem: typeof import('vant/es')['SidebarItem'] | ||
| 34 | + VanStep: typeof import('vant/es')['Step'] | ||
| 35 | + VanSteps: typeof import('vant/es')['Steps'] | ||
| 36 | + VanSwipe: typeof import('vant/es')['Swipe'] | ||
| 37 | + VanSwipeItem: typeof import('vant/es')['SwipeItem'] | ||
| 38 | + VanSwitch: typeof import('vant/es')['Switch'] | ||
| 39 | + VanTab: typeof import('vant/es')['Tab'] | ||
| 40 | + VanTabbar: typeof import('vant/es')['Tabbar'] | ||
| 41 | + VanTabbarItem: typeof import('vant/es')['TabbarItem'] | ||
| 42 | + VanTabs: typeof import('vant/es')['Tabs'] | ||
| 43 | + VanTag: typeof import('vant/es')['Tag'] | ||
| 44 | + } | ||
| 45 | +} |
src/components/EmptyState.vue
0 → 100644
| 1 | +<template> | ||
| 2 | + <div class="empty-state"> | ||
| 3 | + <div class="empty-icon"> | ||
| 4 | + <van-empty | ||
| 5 | + :image="image" | ||
| 6 | + :image-size="imageSize" | ||
| 7 | + :description="description" | ||
| 8 | + > | ||
| 9 | + <template v-if="$slots.image" #image> | ||
| 10 | + <slot name="image" /> | ||
| 11 | + </template> | ||
| 12 | + | ||
| 13 | + <template v-if="$slots.description" #description> | ||
| 14 | + <slot name="description" /> | ||
| 15 | + </template> | ||
| 16 | + | ||
| 17 | + <template v-if="showAction" #default> | ||
| 18 | + <van-button | ||
| 19 | + :type="actionType" | ||
| 20 | + :size="actionSize" | ||
| 21 | + round | ||
| 22 | + @click="handleAction" | ||
| 23 | + > | ||
| 24 | + {{ actionText }} | ||
| 25 | + </van-button> | ||
| 26 | + </template> | ||
| 27 | + </van-empty> | ||
| 28 | + </div> | ||
| 29 | + </div> | ||
| 30 | +</template> | ||
| 31 | + | ||
| 32 | +<script setup> | ||
| 33 | +import { defineEmits } from 'vue' | ||
| 34 | + | ||
| 35 | +const props = defineProps({ | ||
| 36 | + // 图片类型 | ||
| 37 | + image: { | ||
| 38 | + type: String, | ||
| 39 | + default: 'default' | ||
| 40 | + }, | ||
| 41 | + // 图片大小 | ||
| 42 | + imageSize: { | ||
| 43 | + type: [Number, String], | ||
| 44 | + default: 160 | ||
| 45 | + }, | ||
| 46 | + // 描述文字 | ||
| 47 | + description: { | ||
| 48 | + type: String, | ||
| 49 | + default: '暂无数据' | ||
| 50 | + }, | ||
| 51 | + // 是否显示操作按钮 | ||
| 52 | + showAction: { | ||
| 53 | + type: Boolean, | ||
| 54 | + default: false | ||
| 55 | + }, | ||
| 56 | + // 按钮文字 | ||
| 57 | + actionText: { | ||
| 58 | + type: String, | ||
| 59 | + default: '重新加载' | ||
| 60 | + }, | ||
| 61 | + // 按钮类型 | ||
| 62 | + actionType: { | ||
| 63 | + type: String, | ||
| 64 | + default: 'primary' | ||
| 65 | + }, | ||
| 66 | + // 按钮大小 | ||
| 67 | + actionSize: { | ||
| 68 | + type: String, | ||
| 69 | + default: 'normal' | ||
| 70 | + } | ||
| 71 | +}) | ||
| 72 | + | ||
| 73 | +const emit = defineEmits(['action']) | ||
| 74 | + | ||
| 75 | +const handleAction = () => { | ||
| 76 | + emit('action') | ||
| 77 | +} | ||
| 78 | +</script> | ||
| 79 | + | ||
| 80 | +<style scoped> | ||
| 81 | +.empty-state { | ||
| 82 | + padding: 40px 20px; | ||
| 83 | + text-align: center; | ||
| 84 | +} | ||
| 85 | +</style> | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
src/components/LoadingSpinner.vue
0 → 100644
| 1 | +<template> | ||
| 2 | + <div class="loading-spinner" :class="{ 'loading-overlay': overlay }"> | ||
| 3 | + <div class="spinner-container"> | ||
| 4 | + <div class="spinner" :style="{ width: size + 'px', height: size + 'px' }"> | ||
| 5 | + <div class="spinner-inner" :style="{ borderColor: color }"></div> | ||
| 6 | + </div> | ||
| 7 | + <p v-if="text" class="loading-text" :style="{ color: textColor }">{{ text }}</p> | ||
| 8 | + </div> | ||
| 9 | + </div> | ||
| 10 | +</template> | ||
| 11 | + | ||
| 12 | +<script setup> | ||
| 13 | +defineProps({ | ||
| 14 | + // 是否显示遮罩层 | ||
| 15 | + overlay: { | ||
| 16 | + type: Boolean, | ||
| 17 | + default: false | ||
| 18 | + }, | ||
| 19 | + // 加载器大小 | ||
| 20 | + size: { | ||
| 21 | + type: Number, | ||
| 22 | + default: 40 | ||
| 23 | + }, | ||
| 24 | + // 加载器颜色 | ||
| 25 | + color: { | ||
| 26 | + type: String, | ||
| 27 | + default: '#1989fa' | ||
| 28 | + }, | ||
| 29 | + // 加载文本 | ||
| 30 | + text: { | ||
| 31 | + type: String, | ||
| 32 | + default: '' | ||
| 33 | + }, | ||
| 34 | + // 文本颜色 | ||
| 35 | + textColor: { | ||
| 36 | + type: String, | ||
| 37 | + default: '#969799' | ||
| 38 | + } | ||
| 39 | +}) | ||
| 40 | +</script> | ||
| 41 | + | ||
| 42 | +<style scoped> | ||
| 43 | +.loading-spinner { | ||
| 44 | + display: flex; | ||
| 45 | + align-items: center; | ||
| 46 | + justify-content: center; | ||
| 47 | +} | ||
| 48 | + | ||
| 49 | +.loading-overlay { | ||
| 50 | + position: fixed; | ||
| 51 | + top: 0; | ||
| 52 | + left: 0; | ||
| 53 | + right: 0; | ||
| 54 | + bottom: 0; | ||
| 55 | + background: rgba(255, 255, 255, 0.9); | ||
| 56 | + z-index: 9999; | ||
| 57 | +} | ||
| 58 | + | ||
| 59 | +.spinner-container { | ||
| 60 | + display: flex; | ||
| 61 | + flex-direction: column; | ||
| 62 | + align-items: center; | ||
| 63 | + gap: 12px; | ||
| 64 | +} | ||
| 65 | + | ||
| 66 | +.spinner { | ||
| 67 | + position: relative; | ||
| 68 | +} | ||
| 69 | + | ||
| 70 | +.spinner-inner { | ||
| 71 | + width: 100%; | ||
| 72 | + height: 100%; | ||
| 73 | + border: 3px solid transparent; | ||
| 74 | + border-top-color: currentColor; | ||
| 75 | + border-radius: 50%; | ||
| 76 | + animation: spin 1s linear infinite; | ||
| 77 | +} | ||
| 78 | + | ||
| 79 | +.loading-text { | ||
| 80 | + font-size: 14px; | ||
| 81 | + margin: 0; | ||
| 82 | +} | ||
| 83 | + | ||
| 84 | +@keyframes spin { | ||
| 85 | + 0% { | ||
| 86 | + transform: rotate(0deg); | ||
| 87 | + } | ||
| 88 | + 100% { | ||
| 89 | + transform: rotate(360deg); | ||
| 90 | + } | ||
| 91 | +} | ||
| 92 | +</style> | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
src/main.js
0 → 100644
| 1 | +import { createApp } from 'vue' | ||
| 2 | +import { createPinia } from 'pinia' | ||
| 3 | +import router from './router' | ||
| 4 | +import App from './App.vue' | ||
| 5 | + | ||
| 6 | +// 引入样式 | ||
| 7 | +import './style.css' | ||
| 8 | +import 'vant/lib/index.css' | ||
| 9 | + | ||
| 10 | +// 引入 Vant 组件(按需引入会通过 unplugin-vue-components 自动处理) | ||
| 11 | +// 这里只需要引入一些全局配置的组件 | ||
| 12 | +import { ConfigProvider, Toast, Dialog, Notify, ImagePreview } from 'vant' | ||
| 13 | + | ||
| 14 | +const app = createApp(App) | ||
| 15 | +const pinia = createPinia() | ||
| 16 | + | ||
| 17 | +// 全局配置 | ||
| 18 | +app.config.globalProperties.$toast = Toast | ||
| 19 | +app.config.globalProperties.$dialog = Dialog | ||
| 20 | +app.config.globalProperties.$notify = Notify | ||
| 21 | +app.config.globalProperties.$imagePreview = ImagePreview | ||
| 22 | + | ||
| 23 | +// 使用插件 | ||
| 24 | +app.use(pinia) | ||
| 25 | +app.use(router) | ||
| 26 | +app.use(ConfigProvider) | ||
| 27 | + | ||
| 28 | +// 挂载应用 | ||
| 29 | +app.mount('#app') | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
src/router/index.js
0 → 100644
| 1 | +/* | ||
| 2 | + * @Date: 2025-10-30 10:29:15 | ||
| 3 | + * @LastEditors: hookehuyr hookehuyr@gmail.com | ||
| 4 | + * @LastEditTime: 2025-10-30 10:29:24 | ||
| 5 | + * @FilePath: /itomix/h5_vite_template/src/router/index.js | ||
| 6 | + * @Description: 文件描述 | ||
| 7 | + */ | ||
| 8 | +import { createRouter, createWebHistory } from 'vue-router' | ||
| 9 | +import Home from '../views/Home.vue' | ||
| 10 | + | ||
| 11 | +const routes = [ | ||
| 12 | + { | ||
| 13 | + path: '/', | ||
| 14 | + name: 'Splash', | ||
| 15 | + component: () => import('../views/Splash.vue') | ||
| 16 | + }, | ||
| 17 | + { | ||
| 18 | + path: '/home', | ||
| 19 | + name: 'Home', | ||
| 20 | + component: Home | ||
| 21 | + }, | ||
| 22 | + { | ||
| 23 | + path: '/teachers', | ||
| 24 | + name: 'Teachers', | ||
| 25 | + component: () => import('../views/Teachers.vue') | ||
| 26 | + }, | ||
| 27 | + { | ||
| 28 | + path: '/teachers/:id', | ||
| 29 | + name: 'TeacherDetail', | ||
| 30 | + component: () => import('../views/TeacherDetail.vue') | ||
| 31 | + }, | ||
| 32 | + { | ||
| 33 | + path: '/volunteers', | ||
| 34 | + name: 'Volunteers', | ||
| 35 | + component: () => import('../views/Volunteers.vue') | ||
| 36 | + }, | ||
| 37 | + { | ||
| 38 | + path: '/students', | ||
| 39 | + name: 'Students', | ||
| 40 | + component: () => import('../views/Students.vue') | ||
| 41 | + }, | ||
| 42 | + { | ||
| 43 | + path: '/students/:id', | ||
| 44 | + name: 'StudentDetail', | ||
| 45 | + component: () => import('../views/StudentDetail.vue') | ||
| 46 | + }, | ||
| 47 | + { | ||
| 48 | + path: '/news/:id', | ||
| 49 | + name: 'NewsDetail', | ||
| 50 | + component: () => import('../views/NewsDetail.vue') | ||
| 51 | + }, | ||
| 52 | + { | ||
| 53 | + path: '/:pathMatch(.*)*', | ||
| 54 | + name: 'NotFound', | ||
| 55 | + component: () => import('../views/NotFound.vue') | ||
| 56 | + } | ||
| 57 | +] | ||
| 58 | + | ||
| 59 | +const router = createRouter({ | ||
| 60 | + history: createWebHistory(), | ||
| 61 | + routes, | ||
| 62 | + scrollBehavior(to, from, savedPosition) { | ||
| 63 | + if (savedPosition) { | ||
| 64 | + return savedPosition | ||
| 65 | + } else { | ||
| 66 | + return { top: 0 } | ||
| 67 | + } | ||
| 68 | + } | ||
| 69 | +}) | ||
| 70 | + | ||
| 71 | +// 路由守卫 | ||
| 72 | +router.beforeEach((to, from, next) => { | ||
| 73 | + // 设置页面标题 | ||
| 74 | + if (to.meta.title) { | ||
| 75 | + document.title = to.meta.title | ||
| 76 | + } | ||
| 77 | + | ||
| 78 | + // 这里可以添加权限验证逻辑 | ||
| 79 | + next() | ||
| 80 | +}) | ||
| 81 | + | ||
| 82 | +router.afterEach(() => { | ||
| 83 | + // 路由跳转后的逻辑 | ||
| 84 | +}) | ||
| 85 | + | ||
| 86 | +export default router | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
src/stores/user.js
0 → 100644
| 1 | +/* | ||
| 2 | + * @Date: 2025-10-30 10:30:17 | ||
| 3 | + * @LastEditors: hookehuyr hookehuyr@gmail.com | ||
| 4 | + * @LastEditTime: 2025-10-30 10:30:26 | ||
| 5 | + * @FilePath: /itomix/h5_vite_template/src/stores/user.js | ||
| 6 | + * @Description: 文件描述 | ||
| 7 | + */ | ||
| 8 | +import { defineStore } from 'pinia' | ||
| 9 | +import { userApi } from '@/api' | ||
| 10 | + | ||
| 11 | +export const useUserStore = defineStore('user', { | ||
| 12 | + state: () => ({ | ||
| 13 | + userInfo: null, | ||
| 14 | + token: localStorage.getItem('token') || '', | ||
| 15 | + isLogin: false | ||
| 16 | + }), | ||
| 17 | + | ||
| 18 | + getters: { | ||
| 19 | + // 获取用户名 | ||
| 20 | + username: (state) => state.userInfo?.username || '', | ||
| 21 | + | ||
| 22 | + // 获取用户头像 | ||
| 23 | + avatar: (state) => state.userInfo?.avatar || '', | ||
| 24 | + | ||
| 25 | + // 是否已登录 | ||
| 26 | + hasLogin: (state) => !!state.token && !!state.userInfo | ||
| 27 | + }, | ||
| 28 | + | ||
| 29 | + actions: { | ||
| 30 | + // 设置 token | ||
| 31 | + setToken(token) { | ||
| 32 | + this.token = token | ||
| 33 | + localStorage.setItem('token', token) | ||
| 34 | + }, | ||
| 35 | + | ||
| 36 | + // 清除 token | ||
| 37 | + clearToken() { | ||
| 38 | + this.token = '' | ||
| 39 | + localStorage.removeItem('token') | ||
| 40 | + }, | ||
| 41 | + | ||
| 42 | + // 设置用户信息 | ||
| 43 | + setUserInfo(userInfo) { | ||
| 44 | + this.userInfo = userInfo | ||
| 45 | + this.isLogin = true | ||
| 46 | + }, | ||
| 47 | + | ||
| 48 | + // 清除用户信息 | ||
| 49 | + clearUserInfo() { | ||
| 50 | + this.userInfo = null | ||
| 51 | + this.isLogin = false | ||
| 52 | + }, | ||
| 53 | + | ||
| 54 | + // 登录 | ||
| 55 | + async login(loginData) { | ||
| 56 | + const response = await userApi.login(loginData) | ||
| 57 | + const { token, userInfo } = response.data | ||
| 58 | + | ||
| 59 | + this.setToken(token) | ||
| 60 | + this.setUserInfo(userInfo) | ||
| 61 | + | ||
| 62 | + return response | ||
| 63 | + }, | ||
| 64 | + | ||
| 65 | + // 获取用户信息 | ||
| 66 | + async getUserInfo() { | ||
| 67 | + try { | ||
| 68 | + const response = await userApi.getUserInfo() | ||
| 69 | + this.setUserInfo(response.data) | ||
| 70 | + return response | ||
| 71 | + } catch (error) { | ||
| 72 | + // 如果获取用户信息失败,清除本地存储 | ||
| 73 | + this.logout() | ||
| 74 | + throw error | ||
| 75 | + } | ||
| 76 | + }, | ||
| 77 | + | ||
| 78 | + // 登出 | ||
| 79 | + async logout() { | ||
| 80 | + try { | ||
| 81 | + await userApi.logout() | ||
| 82 | + } catch (error) { | ||
| 83 | + console.error('登出失败:', error) | ||
| 84 | + } finally { | ||
| 85 | + this.clearToken() | ||
| 86 | + this.clearUserInfo() | ||
| 87 | + } | ||
| 88 | + } | ||
| 89 | + } | ||
| 90 | +}) | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
src/style.css
0 → 100644
| 1 | +@tailwind base; | ||
| 2 | +@tailwind components; | ||
| 3 | +@tailwind utilities; | ||
| 4 | + | ||
| 5 | +/* 全局样式 */ | ||
| 6 | +* { | ||
| 7 | + box-sizing: border-box; | ||
| 8 | +} | ||
| 9 | + | ||
| 10 | +html, body { | ||
| 11 | + margin: 0; | ||
| 12 | + padding: 0; | ||
| 13 | + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif; | ||
| 14 | + -webkit-font-smoothing: antialiased; | ||
| 15 | + -moz-osx-font-smoothing: grayscale; | ||
| 16 | + background-color: #f7f8fa; | ||
| 17 | +} | ||
| 18 | + | ||
| 19 | +#app { | ||
| 20 | + min-height: 100vh; | ||
| 21 | +} | ||
| 22 | + | ||
| 23 | +/* 自定义工具类 */ | ||
| 24 | +.safe-area-inset-top { | ||
| 25 | + padding-top: constant(safe-area-inset-top); | ||
| 26 | + padding-top: env(safe-area-inset-top); | ||
| 27 | +} | ||
| 28 | + | ||
| 29 | +.safe-area-inset-bottom { | ||
| 30 | + padding-bottom: constant(safe-area-inset-bottom); | ||
| 31 | + padding-bottom: env(safe-area-inset-bottom); | ||
| 32 | +} | ||
| 33 | + | ||
| 34 | +/* 覆盖 Vant 样式 */ | ||
| 35 | +.van-nav-bar { | ||
| 36 | + background-color: #fff; | ||
| 37 | +} | ||
| 38 | + | ||
| 39 | +.van-nav-bar__title { | ||
| 40 | + color: #323233; | ||
| 41 | +} | ||
| 42 | + | ||
| 43 | +/* 页面容器 */ | ||
| 44 | +.page-container { | ||
| 45 | + min-height: 100vh; | ||
| 46 | + background-color: #f7f8fa; | ||
| 47 | +} | ||
| 48 | + | ||
| 49 | +.content-container { | ||
| 50 | + padding: 16px; | ||
| 51 | +} | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
src/utils/request.js
0 → 100644
| 1 | +import axios from 'axios' | ||
| 2 | +import { Toast } from 'vant' | ||
| 3 | + | ||
| 4 | +// 创建 axios 实例 | ||
| 5 | +const request = axios.create({ | ||
| 6 | + baseURL: import.meta.env.VITE_API_BASE_URL || '/api', | ||
| 7 | + timeout: 10000, | ||
| 8 | + headers: { | ||
| 9 | + 'Content-Type': 'application/json;charset=UTF-8' | ||
| 10 | + } | ||
| 11 | +}) | ||
| 12 | + | ||
| 13 | +// 请求拦截器 | ||
| 14 | +request.interceptors.request.use( | ||
| 15 | + (config) => { | ||
| 16 | + // 在发送请求之前做些什么 | ||
| 17 | + | ||
| 18 | + // 添加 token | ||
| 19 | + const token = localStorage.getItem('token') | ||
| 20 | + if (token) { | ||
| 21 | + config.headers.Authorization = `Bearer ${token}` | ||
| 22 | + } | ||
| 23 | + | ||
| 24 | + // 显示加载提示 | ||
| 25 | + Toast.loading({ | ||
| 26 | + message: '加载中...', | ||
| 27 | + forbidClick: true, | ||
| 28 | + duration: 0 | ||
| 29 | + }) | ||
| 30 | + | ||
| 31 | + return config | ||
| 32 | + }, | ||
| 33 | + (error) => { | ||
| 34 | + // 对请求错误做些什么 | ||
| 35 | + Toast.clear() | ||
| 36 | + return Promise.reject(error) | ||
| 37 | + } | ||
| 38 | +) | ||
| 39 | + | ||
| 40 | +// 响应拦截器 | ||
| 41 | +request.interceptors.response.use( | ||
| 42 | + (response) => { | ||
| 43 | + // 对响应数据做点什么 | ||
| 44 | + Toast.clear() | ||
| 45 | + | ||
| 46 | + const { data } = response | ||
| 47 | + | ||
| 48 | + // 根据后端接口规范处理响应 | ||
| 49 | + if (data.code === 200 || data.success) { | ||
| 50 | + return data | ||
| 51 | + } else { | ||
| 52 | + // 业务错误 | ||
| 53 | + Toast.fail(data.message || '请求失败') | ||
| 54 | + return Promise.reject(new Error(data.message || '请求失败')) | ||
| 55 | + } | ||
| 56 | + }, | ||
| 57 | + (error) => { | ||
| 58 | + // 对响应错误做点什么 | ||
| 59 | + Toast.clear() | ||
| 60 | + | ||
| 61 | + let message = '网络错误' | ||
| 62 | + | ||
| 63 | + if (error.response) { | ||
| 64 | + const { status, data } = error.response | ||
| 65 | + | ||
| 66 | + switch (status) { | ||
| 67 | + case 401: | ||
| 68 | + message = '未授权,请重新登录' | ||
| 69 | + // 清除 token 并跳转到登录页 | ||
| 70 | + localStorage.removeItem('token') | ||
| 71 | + // router.push('/login') | ||
| 72 | + break | ||
| 73 | + case 403: | ||
| 74 | + message = '拒绝访问' | ||
| 75 | + break | ||
| 76 | + case 404: | ||
| 77 | + message = '请求地址出错' | ||
| 78 | + break | ||
| 79 | + case 408: | ||
| 80 | + message = '请求超时' | ||
| 81 | + break | ||
| 82 | + case 500: | ||
| 83 | + message = '服务器内部错误' | ||
| 84 | + break | ||
| 85 | + case 501: | ||
| 86 | + message = '服务未实现' | ||
| 87 | + break | ||
| 88 | + case 502: | ||
| 89 | + message = '网关错误' | ||
| 90 | + break | ||
| 91 | + case 503: | ||
| 92 | + message = '服务不可用' | ||
| 93 | + break | ||
| 94 | + case 504: | ||
| 95 | + message = '网关超时' | ||
| 96 | + break | ||
| 97 | + case 505: | ||
| 98 | + message = 'HTTP版本不受支持' | ||
| 99 | + break | ||
| 100 | + default: | ||
| 101 | + message = data?.message || `连接错误${status}` | ||
| 102 | + } | ||
| 103 | + } else if (error.code === 'ECONNABORTED') { | ||
| 104 | + message = '请求超时' | ||
| 105 | + } else if (error.message) { | ||
| 106 | + message = error.message | ||
| 107 | + } | ||
| 108 | + | ||
| 109 | + Toast.fail(message) | ||
| 110 | + return Promise.reject(error) | ||
| 111 | + } | ||
| 112 | +) | ||
| 113 | + | ||
| 114 | +export default request | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
src/views/Home.vue
0 → 100644
| 1 | +<template> | ||
| 2 | + <div class="page-container"> | ||
| 3 | + <!-- 顶部导航栏 --> | ||
| 4 | + <van-nav-bar title="三坛大戒" fixed class="custom-nav"> | ||
| 5 | + <template #left> | ||
| 6 | + <div class="nav-logo"> | ||
| 7 | + <div class="dharma-symbol">☸</div> | ||
| 8 | + </div> | ||
| 9 | + </template> | ||
| 10 | + <template #right> | ||
| 11 | + <van-icon name="search" size="18" /> | ||
| 12 | + </template> | ||
| 13 | + </van-nav-bar> | ||
| 14 | + | ||
| 15 | + <!-- 内容区域 --> | ||
| 16 | + <div class="content-container"> | ||
| 17 | + <!-- 轮播图 --> | ||
| 18 | + <van-swipe class="hero-swipe" :autoplay="4000" indicator-color="#f59e0b"> | ||
| 19 | + <van-swipe-item v-for="(banner, index) in banners" :key="index"> | ||
| 20 | + <div class="swipe-item" :style="{ backgroundImage: `linear-gradient(135deg, ${banner.gradient})` }"> | ||
| 21 | + <div class="banner-content"> | ||
| 22 | + <h3 class="banner-title">{{ banner.title }}</h3> | ||
| 23 | + <p class="banner-desc">{{ banner.desc }}</p> | ||
| 24 | + <div class="banner-decoration"> | ||
| 25 | + <div class="lotus-icon">🪷</div> | ||
| 26 | + </div> | ||
| 27 | + </div> | ||
| 28 | + </div> | ||
| 29 | + </van-swipe-item> | ||
| 30 | + </van-swipe> | ||
| 31 | + | ||
| 32 | + <!-- 功能菜单 --> | ||
| 33 | + <div class="menu-section"> | ||
| 34 | + <h2 class="section-title">戒律修学</h2> | ||
| 35 | + <van-grid :column-num="2" :gutter="16" class="main-menu"> | ||
| 36 | + <van-grid-item | ||
| 37 | + v-for="item in mainMenuItems" | ||
| 38 | + :key="item.id" | ||
| 39 | + @click="handleMenuClick(item)" | ||
| 40 | + class="menu-item animate-on-scroll" | ||
| 41 | + > | ||
| 42 | + <div class="menu-content"> | ||
| 43 | + <div class="menu-icon" :style="{ background: item.gradient }"> | ||
| 44 | + <span class="icon-text">{{ item.icon }}</span> | ||
| 45 | + </div> | ||
| 46 | + <div class="menu-text"> | ||
| 47 | + <h4>{{ item.title }}</h4> | ||
| 48 | + <p>{{ item.desc }}</p> | ||
| 49 | + </div> | ||
| 50 | + </div> | ||
| 51 | + </van-grid-item> | ||
| 52 | + </van-grid> | ||
| 53 | + </div> | ||
| 54 | + | ||
| 55 | + <!-- 通知公告 --> | ||
| 56 | + <div class="notice-section"> | ||
| 57 | + <van-notice-bar | ||
| 58 | + left-icon="volume-o" | ||
| 59 | + :text="noticeText" | ||
| 60 | + color="#b45309" | ||
| 61 | + background="#fffbeb" | ||
| 62 | + class="custom-notice" | ||
| 63 | + /> | ||
| 64 | + </div> | ||
| 65 | + | ||
| 66 | + <!-- 最新资讯 --> | ||
| 67 | + <div class="news-section"> | ||
| 68 | + <div class="section-header"> | ||
| 69 | + <h2 class="section-title">最新资讯</h2> | ||
| 70 | + <van-button type="primary" size="mini" plain @click="$router.push('/news')"> | ||
| 71 | + 更多 | ||
| 72 | + </van-button> | ||
| 73 | + </div> | ||
| 74 | + <div class="news-list"> | ||
| 75 | + <div | ||
| 76 | + v-for="news in newsList" | ||
| 77 | + :key="news.id" | ||
| 78 | + class="news-item animate-on-scroll" | ||
| 79 | + @click="handleNewsClick(news)" | ||
| 80 | + > | ||
| 81 | + <div class="news-content"> | ||
| 82 | + <h4 class="news-title">{{ news.title }}</h4> | ||
| 83 | + <p class="news-summary">{{ news.summary }}</p> | ||
| 84 | + <div class="news-meta"> | ||
| 85 | + <span class="news-date">{{ news.date }}</span> | ||
| 86 | + <span class="news-views">{{ news.views }}人阅读</span> | ||
| 87 | + </div> | ||
| 88 | + </div> | ||
| 89 | + <div class="news-image" v-if="news.image"> | ||
| 90 | + <img :src="news.image" :alt="news.title" /> | ||
| 91 | + </div> | ||
| 92 | + </div> | ||
| 93 | + </div> | ||
| 94 | + </div> | ||
| 95 | + </div> | ||
| 96 | + | ||
| 97 | + <!-- 底部导航 --> | ||
| 98 | + <van-tabbar v-model="activeTab" fixed class="custom-tabbar"> | ||
| 99 | + <van-tabbar-item icon="home-o" to="/"> | ||
| 100 | + 首页 | ||
| 101 | + </van-tabbar-item> | ||
| 102 | + <van-tabbar-item icon="certificate" to="/teachers"> | ||
| 103 | + 三师七证 | ||
| 104 | + </van-tabbar-item> | ||
| 105 | + <van-tabbar-item icon="friends-o" to="/volunteers"> | ||
| 106 | + 义工 | ||
| 107 | + </van-tabbar-item> | ||
| 108 | + <van-tabbar-item icon="user-o" to="/disciples"> | ||
| 109 | + 戒子 | ||
| 110 | + </van-tabbar-item> | ||
| 111 | + </van-tabbar> | ||
| 112 | + </div> | ||
| 113 | +</template> | ||
| 114 | + | ||
| 115 | +<script setup> | ||
| 116 | +import { ref, onMounted, onUnmounted } from 'vue' | ||
| 117 | +import { useRouter } from 'vue-router' | ||
| 118 | +import { Toast } from 'vant' | ||
| 119 | + | ||
| 120 | +const router = useRouter() | ||
| 121 | +const activeTab = ref(0) | ||
| 122 | + | ||
| 123 | +// 轮播图数据 | ||
| 124 | +const banners = ref([ | ||
| 125 | + { | ||
| 126 | + title: '三坛大戒', | ||
| 127 | + desc: '传承千年戒律 弘扬佛法精神', | ||
| 128 | + gradient: '#fbbf24, #f97316' | ||
| 129 | + }, | ||
| 130 | + { | ||
| 131 | + title: '戒律修学', | ||
| 132 | + desc: '严持戒律 清净身心', | ||
| 133 | + gradient: '#f59e0b, #ea580c' | ||
| 134 | + }, | ||
| 135 | + { | ||
| 136 | + title: '法师开示', | ||
| 137 | + desc: '聆听法音 增长智慧', | ||
| 138 | + gradient: '#d97706, #c2410c' | ||
| 139 | + } | ||
| 140 | +]) | ||
| 141 | + | ||
| 142 | +// 主要菜单项 | ||
| 143 | +const mainMenuItems = ref([ | ||
| 144 | + { | ||
| 145 | + id: 1, | ||
| 146 | + icon: '👨🏫', | ||
| 147 | + title: '三师七证', | ||
| 148 | + desc: '查看法师资质', | ||
| 149 | + gradient: 'linear-gradient(135deg, #fbbf24, #f97316)', | ||
| 150 | + path: '/teachers' | ||
| 151 | + }, | ||
| 152 | + { | ||
| 153 | + id: 2, | ||
| 154 | + icon: '🙏', | ||
| 155 | + title: '义工服务', | ||
| 156 | + desc: '参与义工活动', | ||
| 157 | + gradient: 'linear-gradient(135deg, #f59e0b, #ea580c)', | ||
| 158 | + path: '/volunteers' | ||
| 159 | + }, | ||
| 160 | + { | ||
| 161 | + id: 3, | ||
| 162 | + icon: '👤', | ||
| 163 | + title: '戒子信息', | ||
| 164 | + desc: '戒子档案管理', | ||
| 165 | + gradient: 'linear-gradient(135deg, #d97706, #c2410c)', | ||
| 166 | + path: '/disciples' | ||
| 167 | + }, | ||
| 168 | + { | ||
| 169 | + id: 4, | ||
| 170 | + icon: '📰', | ||
| 171 | + title: '最新资讯', | ||
| 172 | + desc: '佛教新闻动态', | ||
| 173 | + gradient: 'linear-gradient(135deg, #b45309, #9a3412)', | ||
| 174 | + path: '/news' | ||
| 175 | + } | ||
| 176 | +]) | ||
| 177 | + | ||
| 178 | +// 通知文本 | ||
| 179 | +const noticeText = ref('欢迎参加三坛大戒法会!请各位戒子严格遵守戒律,精进修学。') | ||
| 180 | + | ||
| 181 | +// 新闻列表 | ||
| 182 | +const newsList = ref([ | ||
| 183 | + { | ||
| 184 | + id: 1, | ||
| 185 | + title: '三坛大戒法会圆满举行', | ||
| 186 | + summary: '本次法会共有200余位戒子参加,法师们为戒子们传授了沙弥戒、比丘戒等重要戒律...', | ||
| 187 | + date: '2024-01-15', | ||
| 188 | + views: 1250, | ||
| 189 | + image: null | ||
| 190 | + }, | ||
| 191 | + { | ||
| 192 | + id: 2, | ||
| 193 | + title: '戒律学习心得分享', | ||
| 194 | + summary: '戒子们分享了在戒律学习过程中的心得体会,互相交流修学经验...', | ||
| 195 | + date: '2024-01-12', | ||
| 196 | + views: 890, | ||
| 197 | + image: null | ||
| 198 | + }, | ||
| 199 | + { | ||
| 200 | + id: 3, | ||
| 201 | + title: '义工服务活动通知', | ||
| 202 | + summary: '寺院将于本周末举行义工服务活动,欢迎各位善信踊跃参与...', | ||
| 203 | + date: '2024-01-10', | ||
| 204 | + views: 650, | ||
| 205 | + image: null | ||
| 206 | + } | ||
| 207 | +]) | ||
| 208 | + | ||
| 209 | +// 处理菜单点击 | ||
| 210 | +const handleMenuClick = (item) => { | ||
| 211 | + if (item.path) { | ||
| 212 | + router.push(item.path) | ||
| 213 | + } else { | ||
| 214 | + Toast('功能开发中...') | ||
| 215 | + } | ||
| 216 | +} | ||
| 217 | + | ||
| 218 | +// 处理新闻点击 | ||
| 219 | +const handleNewsClick = (news) => { | ||
| 220 | + router.push(`/news/${news.id}`) | ||
| 221 | +} | ||
| 222 | + | ||
| 223 | +// 滚动动画观察器 | ||
| 224 | +let observer = null | ||
| 225 | + | ||
| 226 | +const initScrollAnimation = () => { | ||
| 227 | + observer = new IntersectionObserver((entries) => { | ||
| 228 | + entries.forEach((entry) => { | ||
| 229 | + if (entry.isIntersecting) { | ||
| 230 | + entry.target.classList.add('animate-visible') | ||
| 231 | + } | ||
| 232 | + }) | ||
| 233 | + }, { | ||
| 234 | + threshold: 0.1, | ||
| 235 | + rootMargin: '0px 0px -50px 0px' | ||
| 236 | + }) | ||
| 237 | + | ||
| 238 | + // 观察所有需要动画的元素 | ||
| 239 | + const animateElements = document.querySelectorAll('.animate-on-scroll') | ||
| 240 | + animateElements.forEach((el) => { | ||
| 241 | + observer.observe(el) | ||
| 242 | + }) | ||
| 243 | +} | ||
| 244 | + | ||
| 245 | +onMounted(() => { | ||
| 246 | + // 延迟初始化滚动动画,确保DOM已渲染 | ||
| 247 | + setTimeout(() => { | ||
| 248 | + initScrollAnimation() | ||
| 249 | + }, 100) | ||
| 250 | +}) | ||
| 251 | + | ||
| 252 | +onUnmounted(() => { | ||
| 253 | + if (observer) { | ||
| 254 | + observer.disconnect() | ||
| 255 | + } | ||
| 256 | +}) | ||
| 257 | +</script> | ||
| 258 | + | ||
| 259 | +<style scoped> | ||
| 260 | +.page-container { | ||
| 261 | + min-height: 100vh; | ||
| 262 | + background: #fafafa; | ||
| 263 | +} | ||
| 264 | + | ||
| 265 | +.custom-nav { | ||
| 266 | + background: linear-gradient(135deg, #fbbf24, #f97316); | ||
| 267 | + color: white; | ||
| 268 | +} | ||
| 269 | + | ||
| 270 | +.custom-nav :deep(.van-nav-bar__title) { | ||
| 271 | + color: white; | ||
| 272 | + font-weight: 600; | ||
| 273 | +} | ||
| 274 | + | ||
| 275 | +.nav-logo { | ||
| 276 | + display: flex; | ||
| 277 | + align-items: center; | ||
| 278 | +} | ||
| 279 | + | ||
| 280 | +.dharma-symbol { | ||
| 281 | + font-size: 20px; | ||
| 282 | + color: white; | ||
| 283 | +} | ||
| 284 | + | ||
| 285 | +.content-container { | ||
| 286 | + padding-top: 46px; | ||
| 287 | + padding-bottom: 50px; | ||
| 288 | +} | ||
| 289 | + | ||
| 290 | +.hero-swipe { | ||
| 291 | + height: 180px; | ||
| 292 | + margin: 16px; | ||
| 293 | + border-radius: 12px; | ||
| 294 | + overflow: hidden; | ||
| 295 | +} | ||
| 296 | + | ||
| 297 | +.swipe-item { | ||
| 298 | + height: 100%; | ||
| 299 | + display: flex; | ||
| 300 | + align-items: center; | ||
| 301 | + justify-content: center; | ||
| 302 | + position: relative; | ||
| 303 | + color: white; | ||
| 304 | +} | ||
| 305 | + | ||
| 306 | +.banner-content { | ||
| 307 | + text-align: center; | ||
| 308 | + z-index: 2; | ||
| 309 | +} | ||
| 310 | + | ||
| 311 | +.banner-title { | ||
| 312 | + font-size: 24px; | ||
| 313 | + font-weight: 700; | ||
| 314 | + margin-bottom: 8px; | ||
| 315 | +} | ||
| 316 | + | ||
| 317 | +.banner-desc { | ||
| 318 | + font-size: 14px; | ||
| 319 | + opacity: 0.9; | ||
| 320 | +} | ||
| 321 | + | ||
| 322 | +.banner-decoration { | ||
| 323 | + position: absolute; | ||
| 324 | + top: 20px; | ||
| 325 | + right: 20px; | ||
| 326 | + opacity: 0.3; | ||
| 327 | +} | ||
| 328 | + | ||
| 329 | +.lotus-icon { | ||
| 330 | + font-size: 32px; | ||
| 331 | +} | ||
| 332 | + | ||
| 333 | +.menu-section { | ||
| 334 | + padding: 16px; | ||
| 335 | +} | ||
| 336 | + | ||
| 337 | +.section-title { | ||
| 338 | + font-size: 18px; | ||
| 339 | + font-weight: 600; | ||
| 340 | + color: #333; | ||
| 341 | + margin-bottom: 16px; | ||
| 342 | + display: flex; | ||
| 343 | + align-items: center; | ||
| 344 | +} | ||
| 345 | + | ||
| 346 | +.section-title::before { | ||
| 347 | + content: ''; | ||
| 348 | + width: 4px; | ||
| 349 | + height: 18px; | ||
| 350 | + background: linear-gradient(135deg, #fbbf24, #f97316); | ||
| 351 | + margin-right: 8px; | ||
| 352 | + border-radius: 2px; | ||
| 353 | +} | ||
| 354 | + | ||
| 355 | +.main-menu :deep(.van-grid-item__content) { | ||
| 356 | + padding: 0; | ||
| 357 | + background: transparent; | ||
| 358 | +} | ||
| 359 | + | ||
| 360 | +.menu-item { | ||
| 361 | + background: white; | ||
| 362 | + border-radius: 12px; | ||
| 363 | + overflow: hidden; | ||
| 364 | + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); | ||
| 365 | + cursor: pointer; | ||
| 366 | + transition: all 0.3s ease; | ||
| 367 | + opacity: 0; | ||
| 368 | + transform: translateY(30px); | ||
| 369 | +} | ||
| 370 | + | ||
| 371 | +.menu-item:hover { | ||
| 372 | + transform: translateY(-5px); | ||
| 373 | + box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15); | ||
| 374 | +} | ||
| 375 | + | ||
| 376 | +.menu-item:active { | ||
| 377 | + transform: translateY(-2px); | ||
| 378 | + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.12); | ||
| 379 | +} | ||
| 380 | + | ||
| 381 | +.menu-item.animate-visible { | ||
| 382 | + opacity: 1; | ||
| 383 | + transform: translateY(0); | ||
| 384 | +} | ||
| 385 | + | ||
| 386 | +.menu-item:nth-child(1).animate-visible { | ||
| 387 | + transition-delay: 0.1s; | ||
| 388 | +} | ||
| 389 | + | ||
| 390 | +.menu-item:nth-child(2).animate-visible { | ||
| 391 | + transition-delay: 0.2s; | ||
| 392 | +} | ||
| 393 | + | ||
| 394 | +.menu-item:nth-child(3).animate-visible { | ||
| 395 | + transition-delay: 0.3s; | ||
| 396 | +} | ||
| 397 | + | ||
| 398 | +.menu-item:nth-child(4).animate-visible { | ||
| 399 | + transition-delay: 0.4s; | ||
| 400 | +} | ||
| 401 | + | ||
| 402 | +.menu-content { | ||
| 403 | + padding: 20px 16px; | ||
| 404 | + display: flex; | ||
| 405 | + flex-direction: column; | ||
| 406 | + align-items: center; | ||
| 407 | + text-align: center; | ||
| 408 | +} | ||
| 409 | + | ||
| 410 | +.menu-icon { | ||
| 411 | + width: 48px; | ||
| 412 | + height: 48px; | ||
| 413 | + border-radius: 12px; | ||
| 414 | + display: flex; | ||
| 415 | + align-items: center; | ||
| 416 | + justify-content: center; | ||
| 417 | + margin-bottom: 12px; | ||
| 418 | +} | ||
| 419 | + | ||
| 420 | +.icon-text { | ||
| 421 | + font-size: 24px; | ||
| 422 | +} | ||
| 423 | + | ||
| 424 | +.menu-text h4 { | ||
| 425 | + font-size: 16px; | ||
| 426 | + font-weight: 600; | ||
| 427 | + color: #333; | ||
| 428 | + margin: 0 0 4px 0; | ||
| 429 | +} | ||
| 430 | + | ||
| 431 | +.menu-text p { | ||
| 432 | + font-size: 12px; | ||
| 433 | + color: #666; | ||
| 434 | + margin: 0; | ||
| 435 | +} | ||
| 436 | + | ||
| 437 | +.notice-section { | ||
| 438 | + padding: 0 16px 16px; | ||
| 439 | +} | ||
| 440 | + | ||
| 441 | +.custom-notice { | ||
| 442 | + border-radius: 8px; | ||
| 443 | + border: 1px solid #f59e0b; | ||
| 444 | +} | ||
| 445 | + | ||
| 446 | +.news-section { | ||
| 447 | + padding: 0 16px 16px; | ||
| 448 | +} | ||
| 449 | + | ||
| 450 | +.section-header { | ||
| 451 | + display: flex; | ||
| 452 | + justify-content: space-between; | ||
| 453 | + align-items: center; | ||
| 454 | + margin-bottom: 16px; | ||
| 455 | +} | ||
| 456 | + | ||
| 457 | +.news-list { | ||
| 458 | + background: white; | ||
| 459 | + border-radius: 12px; | ||
| 460 | + overflow: hidden; | ||
| 461 | +} | ||
| 462 | + | ||
| 463 | +.news-item { | ||
| 464 | + display: flex; | ||
| 465 | + padding: 16px; | ||
| 466 | + border-bottom: 1px solid #f5f5f5; | ||
| 467 | + cursor: pointer; | ||
| 468 | + transition: all 0.3s ease; | ||
| 469 | + opacity: 0; | ||
| 470 | + transform: translateX(-30px); | ||
| 471 | +} | ||
| 472 | + | ||
| 473 | +.news-item:last-child { | ||
| 474 | + border-bottom: none; | ||
| 475 | +} | ||
| 476 | + | ||
| 477 | +.news-item:hover { | ||
| 478 | + transform: translateX(0) translateY(-2px); | ||
| 479 | + background-color: #f9f9f9; | ||
| 480 | +} | ||
| 481 | + | ||
| 482 | +.news-item:active { | ||
| 483 | + transform: translateX(0) translateY(0); | ||
| 484 | + background-color: #f5f5f5; | ||
| 485 | +} | ||
| 486 | + | ||
| 487 | +.news-item.animate-visible { | ||
| 488 | + opacity: 1; | ||
| 489 | + transform: translateX(0); | ||
| 490 | +} | ||
| 491 | + | ||
| 492 | +.news-item:nth-child(1).animate-visible { | ||
| 493 | + transition-delay: 0.1s; | ||
| 494 | +} | ||
| 495 | + | ||
| 496 | +.news-item:nth-child(2).animate-visible { | ||
| 497 | + transition-delay: 0.2s; | ||
| 498 | +} | ||
| 499 | + | ||
| 500 | +.news-item:nth-child(3).animate-visible { | ||
| 501 | + transition-delay: 0.3s; | ||
| 502 | +} | ||
| 503 | + | ||
| 504 | +.news-content { | ||
| 505 | + flex: 1; | ||
| 506 | +} | ||
| 507 | + | ||
| 508 | +.news-title { | ||
| 509 | + font-size: 16px; | ||
| 510 | + font-weight: 600; | ||
| 511 | + color: #333; | ||
| 512 | + margin: 0 0 8px 0; | ||
| 513 | + line-height: 1.4; | ||
| 514 | +} | ||
| 515 | + | ||
| 516 | +.news-summary { | ||
| 517 | + font-size: 14px; | ||
| 518 | + color: #666; | ||
| 519 | + margin: 0 0 8px 0; | ||
| 520 | + line-height: 1.4; | ||
| 521 | + display: -webkit-box; | ||
| 522 | + -webkit-line-clamp: 2; | ||
| 523 | + -webkit-box-orient: vertical; | ||
| 524 | + overflow: hidden; | ||
| 525 | +} | ||
| 526 | + | ||
| 527 | +.news-meta { | ||
| 528 | + display: flex; | ||
| 529 | + gap: 16px; | ||
| 530 | +} | ||
| 531 | + | ||
| 532 | +.news-date, | ||
| 533 | +.news-views { | ||
| 534 | + font-size: 12px; | ||
| 535 | + color: #999; | ||
| 536 | +} | ||
| 537 | + | ||
| 538 | +.news-image { | ||
| 539 | + width: 80px; | ||
| 540 | + height: 60px; | ||
| 541 | + margin-left: 12px; | ||
| 542 | + border-radius: 6px; | ||
| 543 | + overflow: hidden; | ||
| 544 | +} | ||
| 545 | + | ||
| 546 | +.news-image img { | ||
| 547 | + width: 100%; | ||
| 548 | + height: 100%; | ||
| 549 | + object-fit: cover; | ||
| 550 | +} | ||
| 551 | + | ||
| 552 | +.custom-tabbar { | ||
| 553 | + background: white; | ||
| 554 | + border-top: 1px solid #eee; | ||
| 555 | +} | ||
| 556 | + | ||
| 557 | +.custom-tabbar :deep(.van-tabbar-item--active) { | ||
| 558 | + color: #f59e0b; | ||
| 559 | +} | ||
| 560 | +</style> | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
src/views/NewsDetail.vue
0 → 100644
| 1 | +<template> | ||
| 2 | + <div class="page-container"> | ||
| 3 | + <!-- 导航栏 --> | ||
| 4 | + <van-nav-bar title="新闻详情" left-arrow @click-left="$router.back()" class="custom-nav"> | ||
| 5 | + <template #right> | ||
| 6 | + <van-icon name="share-o" size="18" @click="handleShare" /> | ||
| 7 | + </template> | ||
| 8 | + </van-nav-bar> | ||
| 9 | + | ||
| 10 | + <!-- 内容区域 --> | ||
| 11 | + <div class="content-container"> | ||
| 12 | + <!-- 文章头部 --> | ||
| 13 | + <div class="article-header"> | ||
| 14 | + <h1 class="article-title">{{ article.title }}</h1> | ||
| 15 | + <div class="article-meta"> | ||
| 16 | + <div class="meta-info"> | ||
| 17 | + <span class="publish-date">{{ article.publishDate }}</span> | ||
| 18 | + <span class="author">{{ article.author }}</span> | ||
| 19 | + </div> | ||
| 20 | + <div class="article-stats"> | ||
| 21 | + <span class="view-count"> | ||
| 22 | + <van-icon name="eye-o" /> | ||
| 23 | + {{ article.viewCount }} | ||
| 24 | + </span> | ||
| 25 | + <span class="like-count" @click="handleLike"> | ||
| 26 | + <van-icon :name="article.isLiked ? 'like' : 'like-o'" :color="article.isLiked ? '#ff6b6b' : '#999'" /> | ||
| 27 | + {{ article.likeCount }} | ||
| 28 | + </span> | ||
| 29 | + </div> | ||
| 30 | + </div> | ||
| 31 | + | ||
| 32 | + <!-- 标签 --> | ||
| 33 | + <div class="article-tags" v-if="article.tags && article.tags.length"> | ||
| 34 | + <van-tag | ||
| 35 | + v-for="tag in article.tags" | ||
| 36 | + :key="tag" | ||
| 37 | + type="primary" | ||
| 38 | + size="small" | ||
| 39 | + class="article-tag" | ||
| 40 | + > | ||
| 41 | + {{ tag }} | ||
| 42 | + </van-tag> | ||
| 43 | + </div> | ||
| 44 | + </div> | ||
| 45 | + | ||
| 46 | + <!-- 文章封面 --> | ||
| 47 | + <div class="article-cover" v-if="article.coverImage"> | ||
| 48 | + <img :src="article.coverImage" :alt="article.title" /> | ||
| 49 | + </div> | ||
| 50 | + | ||
| 51 | + <!-- 文章内容 --> | ||
| 52 | + <div class="article-content"> | ||
| 53 | + <div class="content-text" v-html="article.content"></div> | ||
| 54 | + </div> | ||
| 55 | + | ||
| 56 | + <!-- 文章底部 --> | ||
| 57 | + <div class="article-footer"> | ||
| 58 | + <div class="footer-actions"> | ||
| 59 | + <div class="action-item" @click="handleLike"> | ||
| 60 | + <van-icon :name="article.isLiked ? 'like' : 'like-o'" :color="article.isLiked ? '#ff6b6b' : '#666'" size="20" /> | ||
| 61 | + <span>{{ article.isLiked ? '已赞' : '点赞' }}</span> | ||
| 62 | + </div> | ||
| 63 | + <div class="action-item" @click="handleCollect"> | ||
| 64 | + <van-icon :name="article.isCollected ? 'star' : 'star-o'" :color="article.isCollected ? '#fbbf24' : '#666'" size="20" /> | ||
| 65 | + <span>{{ article.isCollected ? '已收藏' : '收藏' }}</span> | ||
| 66 | + </div> | ||
| 67 | + <div class="action-item" @click="handleShare"> | ||
| 68 | + <van-icon name="share-o" color="#666" size="20" /> | ||
| 69 | + <span>分享</span> | ||
| 70 | + </div> | ||
| 71 | + <div class="action-item" @click="handleComment"> | ||
| 72 | + <van-icon name="chat-o" color="#666" size="20" /> | ||
| 73 | + <span>评论</span> | ||
| 74 | + </div> | ||
| 75 | + </div> | ||
| 76 | + </div> | ||
| 77 | + | ||
| 78 | + <!-- 相关文章 --> | ||
| 79 | + <div class="related-articles" v-if="relatedArticles.length"> | ||
| 80 | + <h3 class="section-title">相关文章</h3> | ||
| 81 | + <div class="related-list"> | ||
| 82 | + <div | ||
| 83 | + v-for="related in relatedArticles" | ||
| 84 | + :key="related.id" | ||
| 85 | + class="related-item" | ||
| 86 | + @click="handleRelatedClick(related)" | ||
| 87 | + > | ||
| 88 | + <div class="related-cover"> | ||
| 89 | + <img v-if="related.coverImage" :src="related.coverImage" :alt="related.title" /> | ||
| 90 | + <div v-else class="cover-placeholder"> | ||
| 91 | + <van-icon name="photo-o" size="24" color="#ccc" /> | ||
| 92 | + </div> | ||
| 93 | + </div> | ||
| 94 | + <div class="related-info"> | ||
| 95 | + <h4 class="related-title">{{ related.title }}</h4> | ||
| 96 | + <div class="related-meta"> | ||
| 97 | + <span class="related-date">{{ related.publishDate }}</span> | ||
| 98 | + <span class="related-views">{{ related.viewCount }}阅读</span> | ||
| 99 | + </div> | ||
| 100 | + </div> | ||
| 101 | + </div> | ||
| 102 | + </div> | ||
| 103 | + </div> | ||
| 104 | + | ||
| 105 | + <!-- 评论区 --> | ||
| 106 | + <div class="comments-section"> | ||
| 107 | + <h3 class="section-title">评论 ({{ comments.length }})</h3> | ||
| 108 | + | ||
| 109 | + <!-- 评论输入 --> | ||
| 110 | + <div class="comment-input"> | ||
| 111 | + <van-field | ||
| 112 | + v-model="commentText" | ||
| 113 | + type="textarea" | ||
| 114 | + placeholder="写下你的评论..." | ||
| 115 | + rows="3" | ||
| 116 | + maxlength="500" | ||
| 117 | + show-word-limit | ||
| 118 | + /> | ||
| 119 | + <van-button | ||
| 120 | + type="primary" | ||
| 121 | + size="small" | ||
| 122 | + @click="handleSubmitComment" | ||
| 123 | + :disabled="!commentText.trim()" | ||
| 124 | + class="submit-btn" | ||
| 125 | + > | ||
| 126 | + 发表 | ||
| 127 | + </van-button> | ||
| 128 | + </div> | ||
| 129 | + | ||
| 130 | + <!-- 评论列表 --> | ||
| 131 | + <div class="comments-list"> | ||
| 132 | + <div | ||
| 133 | + v-for="comment in comments" | ||
| 134 | + :key="comment.id" | ||
| 135 | + class="comment-item" | ||
| 136 | + > | ||
| 137 | + <div class="comment-avatar"> | ||
| 138 | + <img v-if="comment.avatar" :src="comment.avatar" :alt="comment.author" /> | ||
| 139 | + <div v-else class="avatar-placeholder"> | ||
| 140 | + <span>{{ comment.author.charAt(0) }}</span> | ||
| 141 | + </div> | ||
| 142 | + </div> | ||
| 143 | + <div class="comment-content"> | ||
| 144 | + <div class="comment-header"> | ||
| 145 | + <span class="comment-author">{{ comment.author }}</span> | ||
| 146 | + <span class="comment-date">{{ comment.date }}</span> | ||
| 147 | + </div> | ||
| 148 | + <p class="comment-text">{{ comment.content }}</p> | ||
| 149 | + <div class="comment-actions"> | ||
| 150 | + <span class="comment-like" @click="handleCommentLike(comment)"> | ||
| 151 | + <van-icon :name="comment.isLiked ? 'like' : 'like-o'" :color="comment.isLiked ? '#ff6b6b' : '#999'" size="14" /> | ||
| 152 | + {{ comment.likeCount || '' }} | ||
| 153 | + </span> | ||
| 154 | + <span class="comment-reply" @click="handleCommentReply(comment)">回复</span> | ||
| 155 | + </div> | ||
| 156 | + </div> | ||
| 157 | + </div> | ||
| 158 | + </div> | ||
| 159 | + | ||
| 160 | + <!-- 空状态 --> | ||
| 161 | + <van-empty v-if="comments.length === 0" description="暂无评论,快来抢沙发吧~" /> | ||
| 162 | + </div> | ||
| 163 | + </div> | ||
| 164 | + </div> | ||
| 165 | +</template> | ||
| 166 | + | ||
| 167 | +<script setup> | ||
| 168 | +import { ref, onMounted } from 'vue' | ||
| 169 | +import { useRoute, useRouter } from 'vue-router' | ||
| 170 | +import { Toast } from 'vant' | ||
| 171 | + | ||
| 172 | +const route = useRoute() | ||
| 173 | +const router = useRouter() | ||
| 174 | + | ||
| 175 | +const commentText = ref('') | ||
| 176 | + | ||
| 177 | +// 文章数据 | ||
| 178 | +const article = ref({ | ||
| 179 | + id: 1, | ||
| 180 | + title: '三坛大戒传戒法会圆满举行', | ||
| 181 | + content: ` | ||
| 182 | + <p>2024年春季三坛大戒传戒法会于今日在本寺圆满举行。此次法会历时21天,共有来自全国各地的68位戒子参加,在诸位戒师的慈悲教导下,圆满受持三坛大戒。</p> | ||
| 183 | + | ||
| 184 | + <p>三坛大戒是佛教出家众必须受持的重要戒律,包括沙弥戒、比丘戒和菩萨戒三个层次。通过严格的戒律学习和实践,戒子们不仅在戒律知识上有了深入的理解,更在修行品格上得到了显著提升。</p> | ||
| 185 | + | ||
| 186 | + <p>在传戒期间,戒子们每日早起晚睡,精进修学。从戒律理论的学习到实际生活中的践行,从个人修持到集体共修,每一个环节都体现了佛教戒律的庄严与神圣。</p> | ||
| 187 | + | ||
| 188 | + <p>本次传戒法会得到了十方善信的大力护持,义工菩萨们日夜辛劳,为法会的顺利进行提供了有力保障。在此向所有护持法会的善信表示衷心感谢。</p> | ||
| 189 | + | ||
| 190 | + <p>愿诸位新戒比丘能够严持净戒,精进修行,早证菩提,广度众生。愿佛法久住,法轮常转,众生离苦得乐。</p> | ||
| 191 | + `, | ||
| 192 | + author: '释智慧法师', | ||
| 193 | + publishDate: '2024-01-15', | ||
| 194 | + viewCount: 1256, | ||
| 195 | + likeCount: 89, | ||
| 196 | + isLiked: false, | ||
| 197 | + isCollected: false, | ||
| 198 | + coverImage: null, | ||
| 199 | + tags: ['三坛大戒', '传戒法会', '戒律', '修行'] | ||
| 200 | +}) | ||
| 201 | + | ||
| 202 | +// 相关文章 | ||
| 203 | +const relatedArticles = ref([ | ||
| 204 | + { | ||
| 205 | + id: 2, | ||
| 206 | + title: '戒律学习的重要性与方法', | ||
| 207 | + publishDate: '2024-01-10', | ||
| 208 | + viewCount: 856, | ||
| 209 | + coverImage: null | ||
| 210 | + }, | ||
| 211 | + { | ||
| 212 | + id: 3, | ||
| 213 | + title: '沙弥戒的基本要求与实践', | ||
| 214 | + publishDate: '2024-01-08', | ||
| 215 | + viewCount: 642, | ||
| 216 | + coverImage: null | ||
| 217 | + }, | ||
| 218 | + { | ||
| 219 | + id: 4, | ||
| 220 | + title: '比丘戒的深层含义解析', | ||
| 221 | + publishDate: '2024-01-05', | ||
| 222 | + viewCount: 789, | ||
| 223 | + coverImage: null | ||
| 224 | + } | ||
| 225 | +]) | ||
| 226 | + | ||
| 227 | +// 评论数据 | ||
| 228 | +const comments = ref([ | ||
| 229 | + { | ||
| 230 | + id: 1, | ||
| 231 | + author: '慧心居士', | ||
| 232 | + content: '随喜赞叹!三坛大戒的传承是佛教的重要传统,愿新戒比丘们都能严持净戒,精进修行。', | ||
| 233 | + date: '2024-01-15 14:30', | ||
| 234 | + likeCount: 12, | ||
| 235 | + isLiked: false, | ||
| 236 | + avatar: null | ||
| 237 | + }, | ||
| 238 | + { | ||
| 239 | + id: 2, | ||
| 240 | + author: '觉悟行者', | ||
| 241 | + content: '感恩诸位法师的慈悲教导,戒律是修行的基础,希望能有更多这样的法会。', | ||
| 242 | + date: '2024-01-15 15:45', | ||
| 243 | + likeCount: 8, | ||
| 244 | + isLiked: false, | ||
| 245 | + avatar: null | ||
| 246 | + }, | ||
| 247 | + { | ||
| 248 | + id: 3, | ||
| 249 | + author: '清净莲花', | ||
| 250 | + content: '阿弥陀佛!看到这样的法会真是法喜充满,愿佛法久住世间。', | ||
| 251 | + date: '2024-01-15 16:20', | ||
| 252 | + likeCount: 5, | ||
| 253 | + isLiked: false, | ||
| 254 | + avatar: null | ||
| 255 | + } | ||
| 256 | +]) | ||
| 257 | + | ||
| 258 | +// 处理点赞 | ||
| 259 | +const handleLike = () => { | ||
| 260 | + article.value.isLiked = !article.value.isLiked | ||
| 261 | + if (article.value.isLiked) { | ||
| 262 | + article.value.likeCount++ | ||
| 263 | + Toast('点赞成功') | ||
| 264 | + } else { | ||
| 265 | + article.value.likeCount-- | ||
| 266 | + Toast('取消点赞') | ||
| 267 | + } | ||
| 268 | +} | ||
| 269 | + | ||
| 270 | +// 处理收藏 | ||
| 271 | +const handleCollect = () => { | ||
| 272 | + article.value.isCollected = !article.value.isCollected | ||
| 273 | + if (article.value.isCollected) { | ||
| 274 | + Toast('收藏成功') | ||
| 275 | + } else { | ||
| 276 | + Toast('取消收藏') | ||
| 277 | + } | ||
| 278 | +} | ||
| 279 | + | ||
| 280 | +// 处理分享 | ||
| 281 | +const handleShare = () => { | ||
| 282 | + Toast('分享功能开发中...') | ||
| 283 | +} | ||
| 284 | + | ||
| 285 | +// 处理评论 | ||
| 286 | +const handleComment = () => { | ||
| 287 | + document.querySelector('.comment-input').scrollIntoView({ behavior: 'smooth' }) | ||
| 288 | +} | ||
| 289 | + | ||
| 290 | +// 处理相关文章点击 | ||
| 291 | +const handleRelatedClick = (related) => { | ||
| 292 | + router.push(`/news/${related.id}`) | ||
| 293 | +} | ||
| 294 | + | ||
| 295 | +// 提交评论 | ||
| 296 | +const handleSubmitComment = () => { | ||
| 297 | + if (!commentText.value.trim()) { | ||
| 298 | + Toast('请输入评论内容') | ||
| 299 | + return | ||
| 300 | + } | ||
| 301 | + | ||
| 302 | + const newComment = { | ||
| 303 | + id: Date.now(), | ||
| 304 | + author: '当前用户', | ||
| 305 | + content: commentText.value.trim(), | ||
| 306 | + date: new Date().toLocaleString('zh-CN'), | ||
| 307 | + likeCount: 0, | ||
| 308 | + isLiked: false, | ||
| 309 | + avatar: null | ||
| 310 | + } | ||
| 311 | + | ||
| 312 | + comments.value.unshift(newComment) | ||
| 313 | + commentText.value = '' | ||
| 314 | + Toast('评论发表成功') | ||
| 315 | +} | ||
| 316 | + | ||
| 317 | +// 处理评论点赞 | ||
| 318 | +const handleCommentLike = (comment) => { | ||
| 319 | + comment.isLiked = !comment.isLiked | ||
| 320 | + if (comment.isLiked) { | ||
| 321 | + comment.likeCount = (comment.likeCount || 0) + 1 | ||
| 322 | + } else { | ||
| 323 | + comment.likeCount = Math.max(0, (comment.likeCount || 0) - 1) | ||
| 324 | + } | ||
| 325 | +} | ||
| 326 | + | ||
| 327 | +// 处理评论回复 | ||
| 328 | +const handleCommentReply = (comment) => { | ||
| 329 | + Toast('回复功能开发中...') | ||
| 330 | +} | ||
| 331 | + | ||
| 332 | +// 组件挂载时加载数据 | ||
| 333 | +onMounted(() => { | ||
| 334 | + const articleId = route.params.id | ||
| 335 | + console.log('Loading article data for ID:', articleId) | ||
| 336 | + // 这里可以根据ID加载具体的文章数据 | ||
| 337 | +}) | ||
| 338 | +</script> | ||
| 339 | + | ||
| 340 | +<style scoped> | ||
| 341 | +.page-container { | ||
| 342 | + min-height: 100vh; | ||
| 343 | + background: #fafafa; | ||
| 344 | +} | ||
| 345 | + | ||
| 346 | +.custom-nav { | ||
| 347 | + background: linear-gradient(135deg, #3b82f6, #1d4ed8); | ||
| 348 | + color: white; | ||
| 349 | +} | ||
| 350 | + | ||
| 351 | +.custom-nav :deep(.van-nav-bar__title) { | ||
| 352 | + color: white; | ||
| 353 | + font-weight: 600; | ||
| 354 | +} | ||
| 355 | + | ||
| 356 | +.custom-nav :deep(.van-icon) { | ||
| 357 | + color: white; | ||
| 358 | +} | ||
| 359 | + | ||
| 360 | +.content-container { | ||
| 361 | + padding-top: 46px; | ||
| 362 | +} | ||
| 363 | + | ||
| 364 | +.article-header { | ||
| 365 | + background: white; | ||
| 366 | + padding: 20px; | ||
| 367 | + margin-bottom: 12px; | ||
| 368 | +} | ||
| 369 | + | ||
| 370 | +.article-title { | ||
| 371 | + font-size: 24px; | ||
| 372 | + font-weight: 700; | ||
| 373 | + color: #333; | ||
| 374 | + line-height: 1.4; | ||
| 375 | + margin: 0 0 16px 0; | ||
| 376 | +} | ||
| 377 | + | ||
| 378 | +.article-meta { | ||
| 379 | + display: flex; | ||
| 380 | + justify-content: space-between; | ||
| 381 | + align-items: center; | ||
| 382 | + margin-bottom: 16px; | ||
| 383 | +} | ||
| 384 | + | ||
| 385 | +.meta-info { | ||
| 386 | + display: flex; | ||
| 387 | + gap: 16px; | ||
| 388 | +} | ||
| 389 | + | ||
| 390 | +.publish-date, | ||
| 391 | +.author { | ||
| 392 | + font-size: 14px; | ||
| 393 | + color: #666; | ||
| 394 | +} | ||
| 395 | + | ||
| 396 | +.article-stats { | ||
| 397 | + display: flex; | ||
| 398 | + gap: 16px; | ||
| 399 | +} | ||
| 400 | + | ||
| 401 | +.view-count, | ||
| 402 | +.like-count { | ||
| 403 | + display: flex; | ||
| 404 | + align-items: center; | ||
| 405 | + gap: 4px; | ||
| 406 | + font-size: 14px; | ||
| 407 | + color: #666; | ||
| 408 | + cursor: pointer; | ||
| 409 | +} | ||
| 410 | + | ||
| 411 | +.article-tags { | ||
| 412 | + display: flex; | ||
| 413 | + gap: 8px; | ||
| 414 | + flex-wrap: wrap; | ||
| 415 | +} | ||
| 416 | + | ||
| 417 | +.article-tag { | ||
| 418 | + background: #e0f2fe !important; | ||
| 419 | + color: #0277bd !important; | ||
| 420 | + border: 1px solid #81d4fa !important; | ||
| 421 | +} | ||
| 422 | + | ||
| 423 | +.article-cover { | ||
| 424 | + background: white; | ||
| 425 | + padding: 0 20px 20px; | ||
| 426 | + margin-bottom: 12px; | ||
| 427 | +} | ||
| 428 | + | ||
| 429 | +.article-cover img { | ||
| 430 | + width: 100%; | ||
| 431 | + border-radius: 8px; | ||
| 432 | +} | ||
| 433 | + | ||
| 434 | +.article-content { | ||
| 435 | + background: white; | ||
| 436 | + padding: 20px; | ||
| 437 | + margin-bottom: 12px; | ||
| 438 | +} | ||
| 439 | + | ||
| 440 | +.content-text { | ||
| 441 | + font-size: 16px; | ||
| 442 | + line-height: 1.8; | ||
| 443 | + color: #333; | ||
| 444 | +} | ||
| 445 | + | ||
| 446 | +.content-text :deep(p) { | ||
| 447 | + margin: 0 0 16px 0; | ||
| 448 | + text-indent: 2em; | ||
| 449 | +} | ||
| 450 | + | ||
| 451 | +.content-text :deep(p:last-child) { | ||
| 452 | + margin-bottom: 0; | ||
| 453 | +} | ||
| 454 | + | ||
| 455 | +.article-footer { | ||
| 456 | + background: white; | ||
| 457 | + padding: 20px; | ||
| 458 | + margin-bottom: 12px; | ||
| 459 | + border-top: 1px solid #eee; | ||
| 460 | +} | ||
| 461 | + | ||
| 462 | +.footer-actions { | ||
| 463 | + display: flex; | ||
| 464 | + justify-content: space-around; | ||
| 465 | +} | ||
| 466 | + | ||
| 467 | +.action-item { | ||
| 468 | + display: flex; | ||
| 469 | + flex-direction: column; | ||
| 470 | + align-items: center; | ||
| 471 | + gap: 8px; | ||
| 472 | + cursor: pointer; | ||
| 473 | + padding: 12px; | ||
| 474 | + border-radius: 8px; | ||
| 475 | + transition: background-color 0.2s; | ||
| 476 | +} | ||
| 477 | + | ||
| 478 | +.action-item:active { | ||
| 479 | + background: #f5f5f5; | ||
| 480 | +} | ||
| 481 | + | ||
| 482 | +.action-item span { | ||
| 483 | + font-size: 12px; | ||
| 484 | + color: #666; | ||
| 485 | +} | ||
| 486 | + | ||
| 487 | +.related-articles { | ||
| 488 | + background: white; | ||
| 489 | + padding: 20px; | ||
| 490 | + margin-bottom: 12px; | ||
| 491 | +} | ||
| 492 | + | ||
| 493 | +.section-title { | ||
| 494 | + font-size: 18px; | ||
| 495 | + font-weight: 600; | ||
| 496 | + color: #333; | ||
| 497 | + margin: 0 0 16px 0; | ||
| 498 | + padding-left: 4px; | ||
| 499 | + border-left: 4px solid #3b82f6; | ||
| 500 | +} | ||
| 501 | + | ||
| 502 | +.related-list { | ||
| 503 | + space-y: 12px; | ||
| 504 | +} | ||
| 505 | + | ||
| 506 | +.related-item { | ||
| 507 | + display: flex; | ||
| 508 | + gap: 12px; | ||
| 509 | + padding: 12px; | ||
| 510 | + border-radius: 8px; | ||
| 511 | + cursor: pointer; | ||
| 512 | + transition: background-color 0.2s; | ||
| 513 | + margin-bottom: 12px; | ||
| 514 | +} | ||
| 515 | + | ||
| 516 | +.related-item:active { | ||
| 517 | + background: #f5f5f5; | ||
| 518 | +} | ||
| 519 | + | ||
| 520 | +.related-cover { | ||
| 521 | + width: 80px; | ||
| 522 | + height: 60px; | ||
| 523 | + border-radius: 6px; | ||
| 524 | + overflow: hidden; | ||
| 525 | + flex-shrink: 0; | ||
| 526 | + background: #f5f5f5; | ||
| 527 | +} | ||
| 528 | + | ||
| 529 | +.related-cover img { | ||
| 530 | + width: 100%; | ||
| 531 | + height: 100%; | ||
| 532 | + object-fit: cover; | ||
| 533 | +} | ||
| 534 | + | ||
| 535 | +.cover-placeholder { | ||
| 536 | + width: 100%; | ||
| 537 | + height: 100%; | ||
| 538 | + display: flex; | ||
| 539 | + align-items: center; | ||
| 540 | + justify-content: center; | ||
| 541 | + background: #f5f5f5; | ||
| 542 | +} | ||
| 543 | + | ||
| 544 | +.related-info { | ||
| 545 | + flex: 1; | ||
| 546 | +} | ||
| 547 | + | ||
| 548 | +.related-title { | ||
| 549 | + font-size: 16px; | ||
| 550 | + font-weight: 500; | ||
| 551 | + color: #333; | ||
| 552 | + margin: 0 0 8px 0; | ||
| 553 | + line-height: 1.4; | ||
| 554 | + display: -webkit-box; | ||
| 555 | + -webkit-line-clamp: 2; | ||
| 556 | + -webkit-box-orient: vertical; | ||
| 557 | + overflow: hidden; | ||
| 558 | +} | ||
| 559 | + | ||
| 560 | +.related-meta { | ||
| 561 | + display: flex; | ||
| 562 | + gap: 12px; | ||
| 563 | +} | ||
| 564 | + | ||
| 565 | +.related-date, | ||
| 566 | +.related-views { | ||
| 567 | + font-size: 12px; | ||
| 568 | + color: #999; | ||
| 569 | +} | ||
| 570 | + | ||
| 571 | +.comments-section { | ||
| 572 | + background: white; | ||
| 573 | + padding: 20px; | ||
| 574 | + margin-bottom: 20px; | ||
| 575 | +} | ||
| 576 | + | ||
| 577 | +.comment-input { | ||
| 578 | + margin-bottom: 20px; | ||
| 579 | + position: relative; | ||
| 580 | +} | ||
| 581 | + | ||
| 582 | +.submit-btn { | ||
| 583 | + position: absolute; | ||
| 584 | + bottom: 12px; | ||
| 585 | + right: 12px; | ||
| 586 | + background: #3b82f6 !important; | ||
| 587 | + border-color: #3b82f6 !important; | ||
| 588 | +} | ||
| 589 | + | ||
| 590 | +.comments-list { | ||
| 591 | + space-y: 16px; | ||
| 592 | +} | ||
| 593 | + | ||
| 594 | +.comment-item { | ||
| 595 | + display: flex; | ||
| 596 | + gap: 12px; | ||
| 597 | + padding: 16px 0; | ||
| 598 | + border-bottom: 1px solid #f0f0f0; | ||
| 599 | +} | ||
| 600 | + | ||
| 601 | +.comment-item:last-child { | ||
| 602 | + border-bottom: none; | ||
| 603 | +} | ||
| 604 | + | ||
| 605 | +.comment-avatar { | ||
| 606 | + width: 40px; | ||
| 607 | + height: 40px; | ||
| 608 | + border-radius: 50%; | ||
| 609 | + overflow: hidden; | ||
| 610 | + flex-shrink: 0; | ||
| 611 | +} | ||
| 612 | + | ||
| 613 | +.comment-avatar img { | ||
| 614 | + width: 100%; | ||
| 615 | + height: 100%; | ||
| 616 | + object-fit: cover; | ||
| 617 | +} | ||
| 618 | + | ||
| 619 | +.avatar-placeholder { | ||
| 620 | + width: 100%; | ||
| 621 | + height: 100%; | ||
| 622 | + background: linear-gradient(135deg, #3b82f6, #1d4ed8); | ||
| 623 | + display: flex; | ||
| 624 | + align-items: center; | ||
| 625 | + justify-content: center; | ||
| 626 | + color: white; | ||
| 627 | + font-size: 16px; | ||
| 628 | + font-weight: 600; | ||
| 629 | +} | ||
| 630 | + | ||
| 631 | +.comment-content { | ||
| 632 | + flex: 1; | ||
| 633 | +} | ||
| 634 | + | ||
| 635 | +.comment-header { | ||
| 636 | + display: flex; | ||
| 637 | + justify-content: space-between; | ||
| 638 | + align-items: center; | ||
| 639 | + margin-bottom: 8px; | ||
| 640 | +} | ||
| 641 | + | ||
| 642 | +.comment-author { | ||
| 643 | + font-size: 14px; | ||
| 644 | + font-weight: 500; | ||
| 645 | + color: #333; | ||
| 646 | +} | ||
| 647 | + | ||
| 648 | +.comment-date { | ||
| 649 | + font-size: 12px; | ||
| 650 | + color: #999; | ||
| 651 | +} | ||
| 652 | + | ||
| 653 | +.comment-text { | ||
| 654 | + font-size: 14px; | ||
| 655 | + color: #666; | ||
| 656 | + line-height: 1.6; | ||
| 657 | + margin: 0 0 12px 0; | ||
| 658 | +} | ||
| 659 | + | ||
| 660 | +.comment-actions { | ||
| 661 | + display: flex; | ||
| 662 | + gap: 16px; | ||
| 663 | +} | ||
| 664 | + | ||
| 665 | +.comment-like, | ||
| 666 | +.comment-reply { | ||
| 667 | + display: flex; | ||
| 668 | + align-items: center; | ||
| 669 | + gap: 4px; | ||
| 670 | + font-size: 12px; | ||
| 671 | + color: #999; | ||
| 672 | + cursor: pointer; | ||
| 673 | +} | ||
| 674 | + | ||
| 675 | +.comment-reply:hover { | ||
| 676 | + color: #3b82f6; | ||
| 677 | +} | ||
| 678 | +</style> | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
src/views/NotFound.vue
0 → 100644
| 1 | +<template> | ||
| 2 | + <div class="not-found-container"> | ||
| 3 | + <div class="not-found-content"> | ||
| 4 | + <!-- 404 图标 --> | ||
| 5 | + <div class="not-found-icon"> | ||
| 6 | + <svg viewBox="0 0 200 200" class="w-32 h-32 text-gray-300"> | ||
| 7 | + <circle cx="100" cy="100" r="90" fill="none" stroke="currentColor" stroke-width="4"/> | ||
| 8 | + <text x="100" y="120" text-anchor="middle" font-size="48" font-weight="bold" fill="currentColor">404</text> | ||
| 9 | + </svg> | ||
| 10 | + </div> | ||
| 11 | + | ||
| 12 | + <!-- 错误信息 --> | ||
| 13 | + <div class="not-found-text"> | ||
| 14 | + <h1 class="text-2xl font-bold text-gray-800 mb-2">页面不存在</h1> | ||
| 15 | + <p class="text-gray-600 mb-8">抱歉,您访问的页面不存在或已被删除</p> | ||
| 16 | + </div> | ||
| 17 | + | ||
| 18 | + <!-- 操作按钮 --> | ||
| 19 | + <div class="not-found-actions space-y-4"> | ||
| 20 | + <van-button | ||
| 21 | + type="primary" | ||
| 22 | + round | ||
| 23 | + block | ||
| 24 | + @click="goHome" | ||
| 25 | + class="mb-4" | ||
| 26 | + > | ||
| 27 | + 返回首页 | ||
| 28 | + </van-button> | ||
| 29 | + | ||
| 30 | + <van-button | ||
| 31 | + plain | ||
| 32 | + round | ||
| 33 | + block | ||
| 34 | + @click="goBack" | ||
| 35 | + > | ||
| 36 | + 返回上页 | ||
| 37 | + </van-button> | ||
| 38 | + </div> | ||
| 39 | + | ||
| 40 | + <!-- 建议链接 --> | ||
| 41 | + <div class="suggested-links mt-8"> | ||
| 42 | + <p class="text-sm text-gray-500 mb-4">您可能想要访问:</p> | ||
| 43 | + <div class="space-y-2"> | ||
| 44 | + <van-cell | ||
| 45 | + title="首页" | ||
| 46 | + is-link | ||
| 47 | + @click="$router.push('/')" | ||
| 48 | + icon="home-o" | ||
| 49 | + /> | ||
| 50 | + <van-cell | ||
| 51 | + title="组件演示" | ||
| 52 | + is-link | ||
| 53 | + @click="$router.push('/demo')" | ||
| 54 | + icon="apps-o" | ||
| 55 | + /> | ||
| 56 | + <van-cell | ||
| 57 | + title="关于我们" | ||
| 58 | + is-link | ||
| 59 | + @click="$router.push('/about')" | ||
| 60 | + icon="info-o" | ||
| 61 | + /> | ||
| 62 | + </div> | ||
| 63 | + </div> | ||
| 64 | + </div> | ||
| 65 | + </div> | ||
| 66 | +</template> | ||
| 67 | + | ||
| 68 | +<script setup> | ||
| 69 | +import { useRouter } from 'vue-router' | ||
| 70 | + | ||
| 71 | +const router = useRouter() | ||
| 72 | + | ||
| 73 | +// 返回首页 | ||
| 74 | +const goHome = () => { | ||
| 75 | + router.push('/') | ||
| 76 | +} | ||
| 77 | + | ||
| 78 | +// 返回上一页 | ||
| 79 | +const goBack = () => { | ||
| 80 | + if (window.history.length > 1) { | ||
| 81 | + router.back() | ||
| 82 | + } else { | ||
| 83 | + router.push('/') | ||
| 84 | + } | ||
| 85 | +} | ||
| 86 | +</script> | ||
| 87 | + | ||
| 88 | +<style scoped> | ||
| 89 | +.not-found-container { | ||
| 90 | + min-height: 100vh; | ||
| 91 | + display: flex; | ||
| 92 | + align-items: center; | ||
| 93 | + justify-content: center; | ||
| 94 | + padding: 20px; | ||
| 95 | + background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); | ||
| 96 | +} | ||
| 97 | + | ||
| 98 | +.not-found-content { | ||
| 99 | + text-align: center; | ||
| 100 | + max-width: 400px; | ||
| 101 | + width: 100%; | ||
| 102 | +} | ||
| 103 | + | ||
| 104 | +.not-found-icon { | ||
| 105 | + margin-bottom: 2rem; | ||
| 106 | +} | ||
| 107 | + | ||
| 108 | +.suggested-links { | ||
| 109 | + background: white; | ||
| 110 | + border-radius: 12px; | ||
| 111 | + padding: 16px; | ||
| 112 | + box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1); | ||
| 113 | +} | ||
| 114 | +</style> | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
src/views/Splash.vue
0 → 100644
| 1 | +<template> | ||
| 2 | + <div class="splash-container" :class="{ 'fade-out': isLeaving }"> | ||
| 3 | + <div class="splash-content"> | ||
| 4 | + <!-- 背景装饰 --> | ||
| 5 | + <div class="bg-decoration"> | ||
| 6 | + <div class="lotus-pattern animate-float"></div> | ||
| 7 | + <div class="cloud-pattern animate-drift"></div> | ||
| 8 | + </div> | ||
| 9 | + | ||
| 10 | + <!-- 主要内容 --> | ||
| 11 | + <div class="main-content"> | ||
| 12 | + <!-- Logo区域 --> | ||
| 13 | + <div class="logo-section animate-fade-in-up"> | ||
| 14 | + <div class="logo-circle"> | ||
| 15 | + <div class="dharma-wheel animate-rotate"> | ||
| 16 | + <div class="wheel-center"></div> | ||
| 17 | + <div class="wheel-spokes"></div> | ||
| 18 | + </div> | ||
| 19 | + </div> | ||
| 20 | + <h1 class="app-title">三坛大戒</h1> | ||
| 21 | + <p class="app-subtitle">传承千年佛法 弘扬戒律精神</p> | ||
| 22 | + </div> | ||
| 23 | + | ||
| 24 | + <!-- 加载动画 --> | ||
| 25 | + <div class="loading-section animate-fade-in-up-delay"> | ||
| 26 | + <div class="loading-dots"> | ||
| 27 | + <span></span> | ||
| 28 | + <span></span> | ||
| 29 | + <span></span> | ||
| 30 | + </div> | ||
| 31 | + <p class="loading-text">正在加载...</p> | ||
| 32 | + </div> | ||
| 33 | + </div> | ||
| 34 | + | ||
| 35 | + <!-- 底部信息 --> | ||
| 36 | + <div class="footer-info animate-fade-in"> | ||
| 37 | + <p class="version">版本 1.0.0</p> | ||
| 38 | + </div> | ||
| 39 | + </div> | ||
| 40 | + </div> | ||
| 41 | +</template> | ||
| 42 | + | ||
| 43 | +<script setup> | ||
| 44 | +import { ref, onMounted } from 'vue' | ||
| 45 | +import { useRouter } from 'vue-router' | ||
| 46 | + | ||
| 47 | +const router = useRouter() | ||
| 48 | +const isLeaving = ref(false) | ||
| 49 | + | ||
| 50 | +onMounted(() => { | ||
| 51 | + // 3秒后开始离开动画,然后跳转到首页 | ||
| 52 | + setTimeout(() => { | ||
| 53 | + isLeaving.value = true | ||
| 54 | + // 等待淡出动画完成后跳转 | ||
| 55 | + setTimeout(() => { | ||
| 56 | + router.push('/home') | ||
| 57 | + }, 500) | ||
| 58 | + }, 2500) | ||
| 59 | +}) | ||
| 60 | +</script> | ||
| 61 | + | ||
| 62 | +<style scoped> | ||
| 63 | +.splash-container { | ||
| 64 | + position: fixed; | ||
| 65 | + top: 0; | ||
| 66 | + left: 0; | ||
| 67 | + right: 0; | ||
| 68 | + bottom: 0; | ||
| 69 | + background: linear-gradient(to bottom, #fffbeb, #fed7aa); | ||
| 70 | + background-image: | ||
| 71 | + radial-gradient(circle at 20% 20%, rgba(251, 191, 36, 0.1) 0%, transparent 50%), | ||
| 72 | + radial-gradient(circle at 80% 80%, rgba(245, 158, 11, 0.1) 0%, transparent 50%); | ||
| 73 | + transition: opacity 0.5s ease-out; | ||
| 74 | +} | ||
| 75 | + | ||
| 76 | +.splash-container.fade-out { | ||
| 77 | + opacity: 0; | ||
| 78 | +} | ||
| 79 | + | ||
| 80 | +.splash-content { | ||
| 81 | + position: relative; | ||
| 82 | + height: 100%; | ||
| 83 | + display: flex; | ||
| 84 | + flex-direction: column; | ||
| 85 | + justify-content: space-between; | ||
| 86 | + align-items: center; | ||
| 87 | + padding: 3rem 2rem; | ||
| 88 | +} | ||
| 89 | + | ||
| 90 | +.bg-decoration { | ||
| 91 | + position: absolute; | ||
| 92 | + top: 0; | ||
| 93 | + left: 0; | ||
| 94 | + right: 0; | ||
| 95 | + bottom: 0; | ||
| 96 | + overflow: hidden; | ||
| 97 | +} | ||
| 98 | + | ||
| 99 | +.lotus-pattern { | ||
| 100 | + position: absolute; | ||
| 101 | + top: 2.5rem; | ||
| 102 | + right: 2.5rem; | ||
| 103 | + width: 8rem; | ||
| 104 | + height: 8rem; | ||
| 105 | + opacity: 0.1; | ||
| 106 | + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Cpath d='M50 20c-5 0-10 5-10 15s5 15 10 15 10-5 10-15-5-15-10-15z' fill='%23f59e0b'/%3E%3Cpath d='M35 35c-5-3-12-1-15 5s-1 12 5 15 12 1 15-5 1-12-5-15z' fill='%23f59e0b'/%3E%3Cpath d='M65 35c5-3 12-1 15 5s1 12-5 15-12 1-15-5-1-12 5-15z' fill='%23f59e0b'/%3E%3C/svg%3E"); | ||
| 107 | +} | ||
| 108 | + | ||
| 109 | +.cloud-pattern { | ||
| 110 | + position: absolute; | ||
| 111 | + bottom: 5rem; | ||
| 112 | + left: 2.5rem; | ||
| 113 | + width: 6rem; | ||
| 114 | + height: 4rem; | ||
| 115 | + opacity: 0.05; | ||
| 116 | + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 60'%3E%3Cpath d='M20 40c-8 0-15-7-15-15s7-15 15-15c2 0 4 0 6 1 3-6 9-10 16-10s13 4 16 10c2-1 4-1 6-1 8 0 15 7 15 15s-7 15-15 15H20z' fill='%23f59e0b'/%3E%3C/svg%3E"); | ||
| 117 | +} | ||
| 118 | + | ||
| 119 | +.main-content { | ||
| 120 | + flex: 1; | ||
| 121 | + display: flex; | ||
| 122 | + flex-direction: column; | ||
| 123 | + justify-content: center; | ||
| 124 | + align-items: center; | ||
| 125 | +} | ||
| 126 | + | ||
| 127 | +.logo-section { | ||
| 128 | + text-align: center; | ||
| 129 | + margin-bottom: 4rem; | ||
| 130 | +} | ||
| 131 | + | ||
| 132 | +.logo-circle { | ||
| 133 | + position: relative; | ||
| 134 | + width: 8rem; | ||
| 135 | + height: 8rem; | ||
| 136 | + margin: 0 auto 2rem; | ||
| 137 | + border-radius: 50%; | ||
| 138 | + background: linear-gradient(135deg, #fbbf24, #f97316); | ||
| 139 | + box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25); | ||
| 140 | + animation: float 3s ease-in-out infinite; | ||
| 141 | +} | ||
| 142 | + | ||
| 143 | +.dharma-wheel { | ||
| 144 | + position: absolute; | ||
| 145 | + top: 1rem; | ||
| 146 | + left: 1rem; | ||
| 147 | + right: 1rem; | ||
| 148 | + bottom: 1rem; | ||
| 149 | + border-radius: 50%; | ||
| 150 | + background: white; | ||
| 151 | + box-shadow: inset 0 2px 4px 0 rgba(0, 0, 0, 0.06); | ||
| 152 | + display: flex; | ||
| 153 | + align-items: center; | ||
| 154 | + justify-content: center; | ||
| 155 | +} | ||
| 156 | + | ||
| 157 | +.wheel-center { | ||
| 158 | + width: 1rem; | ||
| 159 | + height: 1rem; | ||
| 160 | + border-radius: 50%; | ||
| 161 | + background: linear-gradient(135deg, #f59e0b, #ea580c); | ||
| 162 | +} | ||
| 163 | + | ||
| 164 | +.wheel-spokes { | ||
| 165 | + position: absolute; | ||
| 166 | + top: 0; | ||
| 167 | + left: 0; | ||
| 168 | + right: 0; | ||
| 169 | + bottom: 0; | ||
| 170 | +} | ||
| 171 | + | ||
| 172 | +.wheel-spokes::before, | ||
| 173 | +.wheel-spokes::after { | ||
| 174 | + content: ''; | ||
| 175 | + position: absolute; | ||
| 176 | + top: 50%; | ||
| 177 | + left: 50%; | ||
| 178 | + width: 4rem; | ||
| 179 | + height: 0.125rem; | ||
| 180 | + background: linear-gradient(to right, #f59e0b, #ea580c); | ||
| 181 | + transform: translate(-50%, -50%); | ||
| 182 | +} | ||
| 183 | + | ||
| 184 | +.wheel-spokes::before { | ||
| 185 | + transform: translate(-50%, -50%) rotate(45deg); | ||
| 186 | +} | ||
| 187 | + | ||
| 188 | +.wheel-spokes::after { | ||
| 189 | + transform: translate(-50%, -50%) rotate(-45deg); | ||
| 190 | +} | ||
| 191 | + | ||
| 192 | +.app-title { | ||
| 193 | + font-size: 2.25rem; | ||
| 194 | + font-weight: 700; | ||
| 195 | + color: #92400e; | ||
| 196 | + margin-bottom: 0.5rem; | ||
| 197 | + font-family: 'PingFang SC', 'Hiragino Sans GB', sans-serif; | ||
| 198 | +} | ||
| 199 | + | ||
| 200 | +.app-subtitle { | ||
| 201 | + font-size: 1.125rem; | ||
| 202 | + color: #b45309; | ||
| 203 | + opacity: 0.8; | ||
| 204 | +} | ||
| 205 | + | ||
| 206 | +.loading-section { | ||
| 207 | + text-align: center; | ||
| 208 | +} | ||
| 209 | + | ||
| 210 | +.loading-dots { | ||
| 211 | + display: flex; | ||
| 212 | + justify-content: center; | ||
| 213 | + gap: 0.5rem; | ||
| 214 | + margin-bottom: 1rem; | ||
| 215 | +} | ||
| 216 | + | ||
| 217 | +.loading-dots span { | ||
| 218 | + width: 0.75rem; | ||
| 219 | + height: 0.75rem; | ||
| 220 | + border-radius: 50%; | ||
| 221 | + background-color: #f59e0b; | ||
| 222 | + animation: bounce 1.4s ease-in-out infinite both; | ||
| 223 | +} | ||
| 224 | + | ||
| 225 | +.loading-dots span:nth-child(1) { | ||
| 226 | + animation-delay: -0.32s; | ||
| 227 | +} | ||
| 228 | + | ||
| 229 | +.loading-dots span:nth-child(2) { | ||
| 230 | + animation-delay: -0.16s; | ||
| 231 | +} | ||
| 232 | + | ||
| 233 | +.loading-text { | ||
| 234 | + color: #b45309; | ||
| 235 | + font-size: 0.875rem; | ||
| 236 | +} | ||
| 237 | + | ||
| 238 | +.footer-info { | ||
| 239 | + text-align: center; | ||
| 240 | +} | ||
| 241 | + | ||
| 242 | +.version { | ||
| 243 | + color: #d97706; | ||
| 244 | + font-size: 0.75rem; | ||
| 245 | + opacity: 0.6; | ||
| 246 | +} | ||
| 247 | + | ||
| 248 | +@keyframes float { | ||
| 249 | + 0%, 100% { | ||
| 250 | + transform: translateY(0px); | ||
| 251 | + } | ||
| 252 | + 50% { | ||
| 253 | + transform: translateY(-10px); | ||
| 254 | + } | ||
| 255 | +} | ||
| 256 | + | ||
| 257 | +@keyframes bounce { | ||
| 258 | + 0%, 80%, 100% { | ||
| 259 | + transform: scale(0); | ||
| 260 | + } | ||
| 261 | + 40% { | ||
| 262 | + transform: scale(1); | ||
| 263 | + } | ||
| 264 | +} | ||
| 265 | + | ||
| 266 | +@keyframes fadeInUp { | ||
| 267 | + from { | ||
| 268 | + opacity: 0; | ||
| 269 | + transform: translateY(30px); | ||
| 270 | + } | ||
| 271 | + to { | ||
| 272 | + opacity: 1; | ||
| 273 | + transform: translateY(0); | ||
| 274 | + } | ||
| 275 | +} | ||
| 276 | + | ||
| 277 | +@keyframes rotate { | ||
| 278 | + from { | ||
| 279 | + transform: rotate(0deg); | ||
| 280 | + } | ||
| 281 | + to { | ||
| 282 | + transform: rotate(360deg); | ||
| 283 | + } | ||
| 284 | +} | ||
| 285 | + | ||
| 286 | +@keyframes drift { | ||
| 287 | + 0%, 100% { | ||
| 288 | + transform: translateX(0px); | ||
| 289 | + } | ||
| 290 | + 50% { | ||
| 291 | + transform: translateX(20px); | ||
| 292 | + } | ||
| 293 | +} | ||
| 294 | + | ||
| 295 | +.animate-fade-in-up { | ||
| 296 | + animation: fadeInUp 0.8s ease-out; | ||
| 297 | +} | ||
| 298 | + | ||
| 299 | +.animate-fade-in-up-delay { | ||
| 300 | + animation: fadeInUp 0.8s ease-out 0.3s both; | ||
| 301 | +} | ||
| 302 | + | ||
| 303 | +.animate-fade-in { | ||
| 304 | + animation: fadeInUp 0.8s ease-out 0.6s both; | ||
| 305 | +} | ||
| 306 | + | ||
| 307 | +.animate-rotate { | ||
| 308 | + animation: rotate 8s linear infinite; | ||
| 309 | +} | ||
| 310 | + | ||
| 311 | +.animate-float { | ||
| 312 | + animation: float 3s ease-in-out infinite; | ||
| 313 | +} | ||
| 314 | + | ||
| 315 | +.animate-drift { | ||
| 316 | + animation: drift 4s ease-in-out infinite; | ||
| 317 | +} | ||
| 318 | +</style> | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
src/views/StudentDetail.vue
0 → 100644
| 1 | +<template> | ||
| 2 | + <div class="page-container"> | ||
| 3 | + <!-- 导航栏 --> | ||
| 4 | + <van-nav-bar title="戒子详情" left-arrow @click-left="$router.back()" class="custom-nav"> | ||
| 5 | + <template #right> | ||
| 6 | + <van-icon name="edit" size="18" @click="handleEdit" /> | ||
| 7 | + </template> | ||
| 8 | + </van-nav-bar> | ||
| 9 | + | ||
| 10 | + <!-- 内容区域 --> | ||
| 11 | + <div class="content-container"> | ||
| 12 | + <!-- 戒子基本信息 --> | ||
| 13 | + <div class="profile-section"> | ||
| 14 | + <div class="profile-header"> | ||
| 15 | + <div class="avatar-container"> | ||
| 16 | + <img v-if="student.avatar" :src="student.avatar" :alt="student.name" class="avatar" /> | ||
| 17 | + <div v-else class="avatar-placeholder"> | ||
| 18 | + <span>{{ student.name.charAt(0) }}</span> | ||
| 19 | + </div> | ||
| 20 | + <div class="precept-badge" :class="getPreceptClass(student.preceptType)"> | ||
| 21 | + {{ getPreceptText(student.preceptType) }} | ||
| 22 | + </div> | ||
| 23 | + </div> | ||
| 24 | + | ||
| 25 | + <div class="profile-info"> | ||
| 26 | + <h2 class="student-name">{{ student.name }}</h2> | ||
| 27 | + <p class="student-dharma-name">法名:{{ student.dharmaName }}</p> | ||
| 28 | + <p class="student-temple">{{ student.temple }}</p> | ||
| 29 | + <div class="status-badge" :class="getStatusClass(student.status)"> | ||
| 30 | + {{ getStatusText(student.status) }} | ||
| 31 | + </div> | ||
| 32 | + </div> | ||
| 33 | + </div> | ||
| 34 | + | ||
| 35 | + <!-- 统计信息 --> | ||
| 36 | + <div class="stats-grid"> | ||
| 37 | + <div class="stat-item"> | ||
| 38 | + <div class="stat-value">{{ student.studyDays }}</div> | ||
| 39 | + <div class="stat-label">学习天数</div> | ||
| 40 | + </div> | ||
| 41 | + <div class="stat-item"> | ||
| 42 | + <div class="stat-value">{{ student.progress }}%</div> | ||
| 43 | + <div class="stat-label">学习进度</div> | ||
| 44 | + </div> | ||
| 45 | + <div class="stat-item"> | ||
| 46 | + <div class="stat-value">{{ student.completedCourses }}</div> | ||
| 47 | + <div class="stat-label">完成课程</div> | ||
| 48 | + </div> | ||
| 49 | + <div class="stat-item"> | ||
| 50 | + <div class="stat-value">{{ student.merits }}</div> | ||
| 51 | + <div class="stat-label">功德积分</div> | ||
| 52 | + </div> | ||
| 53 | + </div> | ||
| 54 | + </div> | ||
| 55 | + | ||
| 56 | + <!-- 详细信息 --> | ||
| 57 | + <div class="details-section"> | ||
| 58 | + <!-- 基本信息 --> | ||
| 59 | + <van-cell-group title="基本信息" class="info-group"> | ||
| 60 | + <van-cell title="戒师" :value="student.teacher" /> | ||
| 61 | + <van-cell title="入学时间" :value="student.entryDate" /> | ||
| 62 | + <van-cell title="预计毕业" :value="student.expectedGraduation" /> | ||
| 63 | + <van-cell title="籍贯" :value="student.hometown" /> | ||
| 64 | + <van-cell title="年龄" :value="student.age + '岁'" /> | ||
| 65 | + </van-cell-group> | ||
| 66 | + | ||
| 67 | + <!-- 学习进度 --> | ||
| 68 | + <div class="progress-section"> | ||
| 69 | + <h3 class="section-title">学习进度</h3> | ||
| 70 | + <div class="progress-card"> | ||
| 71 | + <div class="progress-header"> | ||
| 72 | + <span class="progress-text">总体进度</span> | ||
| 73 | + <span class="progress-percent">{{ student.progress }}%</span> | ||
| 74 | + </div> | ||
| 75 | + <van-progress :percentage="student.progress" color="#8b5cf6" /> | ||
| 76 | + </div> | ||
| 77 | + | ||
| 78 | + <div class="course-list"> | ||
| 79 | + <div | ||
| 80 | + v-for="course in student.courses" | ||
| 81 | + :key="course.id" | ||
| 82 | + class="course-item" | ||
| 83 | + > | ||
| 84 | + <div class="course-info"> | ||
| 85 | + <h4 class="course-name">{{ course.name }}</h4> | ||
| 86 | + <p class="course-teacher">授课法师:{{ course.teacher }}</p> | ||
| 87 | + </div> | ||
| 88 | + <div class="course-progress"> | ||
| 89 | + <div class="course-status" :class="getCourseStatusClass(course.status)"> | ||
| 90 | + {{ getCourseStatusText(course.status) }} | ||
| 91 | + </div> | ||
| 92 | + <div class="course-score" v-if="course.score"> | ||
| 93 | + {{ course.score }}分 | ||
| 94 | + </div> | ||
| 95 | + </div> | ||
| 96 | + </div> | ||
| 97 | + </div> | ||
| 98 | + </div> | ||
| 99 | + | ||
| 100 | + <!-- 戒律修学记录 --> | ||
| 101 | + <div class="records-section"> | ||
| 102 | + <h3 class="section-title">修学记录</h3> | ||
| 103 | + <van-timeline> | ||
| 104 | + <van-timeline-item | ||
| 105 | + v-for="record in student.studyRecords" | ||
| 106 | + :key="record.id" | ||
| 107 | + :time="record.date" | ||
| 108 | + > | ||
| 109 | + <div class="record-content"> | ||
| 110 | + <h4 class="record-title">{{ record.title }}</h4> | ||
| 111 | + <p class="record-description">{{ record.description }}</p> | ||
| 112 | + <div class="record-tags"> | ||
| 113 | + <van-tag | ||
| 114 | + v-for="tag in record.tags" | ||
| 115 | + :key="tag" | ||
| 116 | + type="primary" | ||
| 117 | + size="small" | ||
| 118 | + class="record-tag" | ||
| 119 | + > | ||
| 120 | + {{ tag }} | ||
| 121 | + </van-tag> | ||
| 122 | + </div> | ||
| 123 | + </div> | ||
| 124 | + </van-timeline-item> | ||
| 125 | + </van-timeline> | ||
| 126 | + </div> | ||
| 127 | + | ||
| 128 | + <!-- 联系方式 --> | ||
| 129 | + <van-cell-group title="联系方式" class="info-group"> | ||
| 130 | + <van-cell title="手机号码" :value="student.phone" /> | ||
| 131 | + <van-cell title="紧急联系人" :value="student.emergencyContact" /> | ||
| 132 | + <van-cell title="紧急联系电话" :value="student.emergencyPhone" /> | ||
| 133 | + </van-cell-group> | ||
| 134 | + </div> | ||
| 135 | + </div> | ||
| 136 | + </div> | ||
| 137 | +</template> | ||
| 138 | + | ||
| 139 | +<script setup> | ||
| 140 | +import { ref, onMounted } from 'vue' | ||
| 141 | +import { useRoute, useRouter } from 'vue-router' | ||
| 142 | +import { Toast } from 'vant' | ||
| 143 | + | ||
| 144 | +const route = useRoute() | ||
| 145 | +const router = useRouter() | ||
| 146 | + | ||
| 147 | +// 戒子详细信息 | ||
| 148 | +const student = ref({ | ||
| 149 | + id: 1, | ||
| 150 | + name: '释慧明', | ||
| 151 | + dharmaName: '慧明', | ||
| 152 | + temple: '大雄宝殿', | ||
| 153 | + teacher: '释智慧法师', | ||
| 154 | + preceptType: 'bhiksu', | ||
| 155 | + status: 'studying', | ||
| 156 | + entryDate: '2023-03-15', | ||
| 157 | + expectedGraduation: '2024-03-15', | ||
| 158 | + hometown: '江苏南京', | ||
| 159 | + age: 28, | ||
| 160 | + studyDays: 285, | ||
| 161 | + progress: 75, | ||
| 162 | + completedCourses: 12, | ||
| 163 | + merits: 1580, | ||
| 164 | + phone: '138****8888', | ||
| 165 | + emergencyContact: '张居士', | ||
| 166 | + emergencyPhone: '139****9999', | ||
| 167 | + avatar: null, | ||
| 168 | + courses: [ | ||
| 169 | + { | ||
| 170 | + id: 1, | ||
| 171 | + name: '沙弥律仪', | ||
| 172 | + teacher: '释智慧法师', | ||
| 173 | + status: 'completed', | ||
| 174 | + score: 95 | ||
| 175 | + }, | ||
| 176 | + { | ||
| 177 | + id: 2, | ||
| 178 | + name: '比丘戒本', | ||
| 179 | + teacher: '释慈悲法师', | ||
| 180 | + status: 'studying', | ||
| 181 | + score: null | ||
| 182 | + }, | ||
| 183 | + { | ||
| 184 | + id: 3, | ||
| 185 | + name: '戒律学纲要', | ||
| 186 | + teacher: '释般若法师', | ||
| 187 | + status: 'completed', | ||
| 188 | + score: 88 | ||
| 189 | + }, | ||
| 190 | + { | ||
| 191 | + id: 4, | ||
| 192 | + name: '四分律删繁补阙行事钞', | ||
| 193 | + teacher: '释智慧法师', | ||
| 194 | + status: 'pending', | ||
| 195 | + score: null | ||
| 196 | + } | ||
| 197 | + ], | ||
| 198 | + studyRecords: [ | ||
| 199 | + { | ||
| 200 | + id: 1, | ||
| 201 | + date: '2024-01-15', | ||
| 202 | + title: '完成沙弥律仪考试', | ||
| 203 | + description: '以优异成绩通过沙弥律仪课程考试,获得95分', | ||
| 204 | + tags: ['考试', '沙弥律仪', '优秀'] | ||
| 205 | + }, | ||
| 206 | + { | ||
| 207 | + id: 2, | ||
| 208 | + date: '2024-01-10', | ||
| 209 | + title: '参加戒律研讨会', | ||
| 210 | + description: '积极参与戒律研讨,发表见解,获得法师好评', | ||
| 211 | + tags: ['研讨会', '戒律', '积极参与'] | ||
| 212 | + }, | ||
| 213 | + { | ||
| 214 | + id: 3, | ||
| 215 | + date: '2024-01-05', | ||
| 216 | + title: '开始比丘戒本学习', | ||
| 217 | + description: '正式开始比丘戒本课程的学习,制定详细学习计划', | ||
| 218 | + tags: ['比丘戒', '学习计划'] | ||
| 219 | + }, | ||
| 220 | + { | ||
| 221 | + id: 4, | ||
| 222 | + date: '2023-12-20', | ||
| 223 | + title: '戒律学纲要结业', | ||
| 224 | + description: '完成戒律学纲要课程,掌握戒律基本理论', | ||
| 225 | + tags: ['结业', '戒律学', '理论'] | ||
| 226 | + } | ||
| 227 | + ] | ||
| 228 | +}) | ||
| 229 | + | ||
| 230 | +// 获取戒律类型样式类 | ||
| 231 | +const getPreceptClass = (type) => { | ||
| 232 | + const classes = { | ||
| 233 | + novice: 'precept-novice', | ||
| 234 | + bhiksu: 'precept-bhiksu', | ||
| 235 | + bodhisattva: 'precept-bodhisattva' | ||
| 236 | + } | ||
| 237 | + return classes[type] || 'precept-novice' | ||
| 238 | +} | ||
| 239 | + | ||
| 240 | +// 获取戒律类型文本 | ||
| 241 | +const getPreceptText = (type) => { | ||
| 242 | + const texts = { | ||
| 243 | + novice: '沙弥戒', | ||
| 244 | + bhiksu: '比丘戒', | ||
| 245 | + bodhisattva: '菩萨戒' | ||
| 246 | + } | ||
| 247 | + return texts[type] || '沙弥戒' | ||
| 248 | +} | ||
| 249 | + | ||
| 250 | +// 获取状态样式类 | ||
| 251 | +const getStatusClass = (status) => { | ||
| 252 | + const classes = { | ||
| 253 | + studying: 'status-studying', | ||
| 254 | + graduated: 'status-graduated', | ||
| 255 | + suspended: 'status-suspended' | ||
| 256 | + } | ||
| 257 | + return classes[status] || 'status-studying' | ||
| 258 | +} | ||
| 259 | + | ||
| 260 | +// 获取状态文本 | ||
| 261 | +const getStatusText = (status) => { | ||
| 262 | + const texts = { | ||
| 263 | + studying: '在学', | ||
| 264 | + graduated: '毕业', | ||
| 265 | + suspended: '暂停' | ||
| 266 | + } | ||
| 267 | + return texts[status] || '在学' | ||
| 268 | +} | ||
| 269 | + | ||
| 270 | +// 获取课程状态样式类 | ||
| 271 | +const getCourseStatusClass = (status) => { | ||
| 272 | + const classes = { | ||
| 273 | + completed: 'course-completed', | ||
| 274 | + studying: 'course-studying', | ||
| 275 | + pending: 'course-pending' | ||
| 276 | + } | ||
| 277 | + return classes[status] || 'course-pending' | ||
| 278 | +} | ||
| 279 | + | ||
| 280 | +// 获取课程状态文本 | ||
| 281 | +const getCourseStatusText = (status) => { | ||
| 282 | + const texts = { | ||
| 283 | + completed: '已完成', | ||
| 284 | + studying: '学习中', | ||
| 285 | + pending: '未开始' | ||
| 286 | + } | ||
| 287 | + return texts[status] || '未开始' | ||
| 288 | +} | ||
| 289 | + | ||
| 290 | +// 处理编辑 | ||
| 291 | +const handleEdit = () => { | ||
| 292 | + Toast('编辑功能开发中...') | ||
| 293 | +} | ||
| 294 | + | ||
| 295 | +// 组件挂载时加载数据 | ||
| 296 | +onMounted(() => { | ||
| 297 | + // 这里可以根据路由参数加载具体的戒子数据 | ||
| 298 | + const studentId = route.params.id | ||
| 299 | + console.log('Loading student data for ID:', studentId) | ||
| 300 | +}) | ||
| 301 | +</script> | ||
| 302 | + | ||
| 303 | +<style scoped> | ||
| 304 | +.page-container { | ||
| 305 | + min-height: 100vh; | ||
| 306 | + background: #fafafa; | ||
| 307 | +} | ||
| 308 | + | ||
| 309 | +.custom-nav { | ||
| 310 | + background: linear-gradient(135deg, #8b5cf6, #7c3aed); | ||
| 311 | + color: white; | ||
| 312 | +} | ||
| 313 | + | ||
| 314 | +.custom-nav :deep(.van-nav-bar__title) { | ||
| 315 | + color: white; | ||
| 316 | + font-weight: 600; | ||
| 317 | +} | ||
| 318 | + | ||
| 319 | +.custom-nav :deep(.van-icon) { | ||
| 320 | + color: white; | ||
| 321 | +} | ||
| 322 | + | ||
| 323 | +.content-container { | ||
| 324 | + padding-top: 46px; | ||
| 325 | +} | ||
| 326 | + | ||
| 327 | +.profile-section { | ||
| 328 | + background: white; | ||
| 329 | + padding: 20px; | ||
| 330 | + margin-bottom: 12px; | ||
| 331 | +} | ||
| 332 | + | ||
| 333 | +.profile-header { | ||
| 334 | + display: flex; | ||
| 335 | + align-items: center; | ||
| 336 | + margin-bottom: 20px; | ||
| 337 | +} | ||
| 338 | + | ||
| 339 | +.avatar-container { | ||
| 340 | + position: relative; | ||
| 341 | + width: 80px; | ||
| 342 | + height: 80px; | ||
| 343 | + border-radius: 50%; | ||
| 344 | + overflow: hidden; | ||
| 345 | + margin-right: 20px; | ||
| 346 | + flex-shrink: 0; | ||
| 347 | +} | ||
| 348 | + | ||
| 349 | +.avatar { | ||
| 350 | + width: 100%; | ||
| 351 | + height: 100%; | ||
| 352 | + object-fit: cover; | ||
| 353 | +} | ||
| 354 | + | ||
| 355 | +.avatar-placeholder { | ||
| 356 | + width: 100%; | ||
| 357 | + height: 100%; | ||
| 358 | + background: linear-gradient(135deg, #8b5cf6, #7c3aed); | ||
| 359 | + display: flex; | ||
| 360 | + align-items: center; | ||
| 361 | + justify-content: center; | ||
| 362 | + color: white; | ||
| 363 | + font-size: 32px; | ||
| 364 | + font-weight: 600; | ||
| 365 | +} | ||
| 366 | + | ||
| 367 | +.precept-badge { | ||
| 368 | + position: absolute; | ||
| 369 | + bottom: -2px; | ||
| 370 | + right: -2px; | ||
| 371 | + padding: 4px 8px; | ||
| 372 | + border-radius: 12px; | ||
| 373 | + font-size: 10px; | ||
| 374 | + font-weight: 500; | ||
| 375 | + border: 2px solid white; | ||
| 376 | +} | ||
| 377 | + | ||
| 378 | +.precept-novice { | ||
| 379 | + background: #10b981; | ||
| 380 | + color: white; | ||
| 381 | +} | ||
| 382 | + | ||
| 383 | +.precept-bhiksu { | ||
| 384 | + background: #3b82f6; | ||
| 385 | + color: white; | ||
| 386 | +} | ||
| 387 | + | ||
| 388 | +.precept-bodhisattva { | ||
| 389 | + background: #f59e0b; | ||
| 390 | + color: white; | ||
| 391 | +} | ||
| 392 | + | ||
| 393 | +.profile-info { | ||
| 394 | + flex: 1; | ||
| 395 | +} | ||
| 396 | + | ||
| 397 | +.student-name { | ||
| 398 | + font-size: 24px; | ||
| 399 | + font-weight: 700; | ||
| 400 | + color: #333; | ||
| 401 | + margin: 0 0 8px 0; | ||
| 402 | +} | ||
| 403 | + | ||
| 404 | +.student-dharma-name { | ||
| 405 | + font-size: 16px; | ||
| 406 | + color: #666; | ||
| 407 | + margin: 0 0 4px 0; | ||
| 408 | +} | ||
| 409 | + | ||
| 410 | +.student-temple { | ||
| 411 | + font-size: 16px; | ||
| 412 | + color: #666; | ||
| 413 | + margin: 0 0 12px 0; | ||
| 414 | +} | ||
| 415 | + | ||
| 416 | +.status-badge { | ||
| 417 | + display: inline-block; | ||
| 418 | + padding: 6px 12px; | ||
| 419 | + border-radius: 16px; | ||
| 420 | + font-size: 12px; | ||
| 421 | + font-weight: 500; | ||
| 422 | +} | ||
| 423 | + | ||
| 424 | +.status-studying { | ||
| 425 | + background: #dbeafe; | ||
| 426 | + color: #1d4ed8; | ||
| 427 | + border: 1px solid #93c5fd; | ||
| 428 | +} | ||
| 429 | + | ||
| 430 | +.status-graduated { | ||
| 431 | + background: #dcfce7; | ||
| 432 | + color: #166534; | ||
| 433 | + border: 1px solid #86efac; | ||
| 434 | +} | ||
| 435 | + | ||
| 436 | +.status-suspended { | ||
| 437 | + background: #fef3c7; | ||
| 438 | + color: #92400e; | ||
| 439 | + border: 1px solid #fcd34d; | ||
| 440 | +} | ||
| 441 | + | ||
| 442 | +.stats-grid { | ||
| 443 | + display: grid; | ||
| 444 | + grid-template-columns: repeat(4, 1fr); | ||
| 445 | + gap: 16px; | ||
| 446 | +} | ||
| 447 | + | ||
| 448 | +.stat-item { | ||
| 449 | + text-align: center; | ||
| 450 | +} | ||
| 451 | + | ||
| 452 | +.stat-value { | ||
| 453 | + font-size: 20px; | ||
| 454 | + font-weight: 700; | ||
| 455 | + color: #8b5cf6; | ||
| 456 | + margin-bottom: 4px; | ||
| 457 | +} | ||
| 458 | + | ||
| 459 | +.stat-label { | ||
| 460 | + font-size: 12px; | ||
| 461 | + color: #666; | ||
| 462 | +} | ||
| 463 | + | ||
| 464 | +.details-section { | ||
| 465 | + padding: 0 16px 20px; | ||
| 466 | +} | ||
| 467 | + | ||
| 468 | +.info-group { | ||
| 469 | + margin-bottom: 16px; | ||
| 470 | +} | ||
| 471 | + | ||
| 472 | +.info-group :deep(.van-cell-group__title) { | ||
| 473 | + color: #333; | ||
| 474 | + font-weight: 600; | ||
| 475 | +} | ||
| 476 | + | ||
| 477 | +.section-title { | ||
| 478 | + font-size: 18px; | ||
| 479 | + font-weight: 600; | ||
| 480 | + color: #333; | ||
| 481 | + margin: 20px 0 16px 0; | ||
| 482 | + padding-left: 4px; | ||
| 483 | + border-left: 4px solid #8b5cf6; | ||
| 484 | +} | ||
| 485 | + | ||
| 486 | +.progress-section { | ||
| 487 | + background: white; | ||
| 488 | + border-radius: 12px; | ||
| 489 | + padding: 20px; | ||
| 490 | + margin-bottom: 16px; | ||
| 491 | +} | ||
| 492 | + | ||
| 493 | +.progress-card { | ||
| 494 | + background: #f8fafc; | ||
| 495 | + border-radius: 8px; | ||
| 496 | + padding: 16px; | ||
| 497 | + margin-bottom: 20px; | ||
| 498 | +} | ||
| 499 | + | ||
| 500 | +.progress-header { | ||
| 501 | + display: flex; | ||
| 502 | + justify-content: space-between; | ||
| 503 | + align-items: center; | ||
| 504 | + margin-bottom: 12px; | ||
| 505 | +} | ||
| 506 | + | ||
| 507 | +.progress-text { | ||
| 508 | + font-size: 16px; | ||
| 509 | + font-weight: 500; | ||
| 510 | + color: #333; | ||
| 511 | +} | ||
| 512 | + | ||
| 513 | +.progress-percent { | ||
| 514 | + font-size: 18px; | ||
| 515 | + font-weight: 700; | ||
| 516 | + color: #8b5cf6; | ||
| 517 | +} | ||
| 518 | + | ||
| 519 | +.course-list { | ||
| 520 | + space-y: 12px; | ||
| 521 | +} | ||
| 522 | + | ||
| 523 | +.course-item { | ||
| 524 | + display: flex; | ||
| 525 | + justify-content: space-between; | ||
| 526 | + align-items: center; | ||
| 527 | + padding: 16px; | ||
| 528 | + background: #f8fafc; | ||
| 529 | + border-radius: 8px; | ||
| 530 | + margin-bottom: 12px; | ||
| 531 | +} | ||
| 532 | + | ||
| 533 | +.course-info { | ||
| 534 | + flex: 1; | ||
| 535 | +} | ||
| 536 | + | ||
| 537 | +.course-name { | ||
| 538 | + font-size: 16px; | ||
| 539 | + font-weight: 600; | ||
| 540 | + color: #333; | ||
| 541 | + margin: 0 0 4px 0; | ||
| 542 | +} | ||
| 543 | + | ||
| 544 | +.course-teacher { | ||
| 545 | + font-size: 14px; | ||
| 546 | + color: #666; | ||
| 547 | + margin: 0; | ||
| 548 | +} | ||
| 549 | + | ||
| 550 | +.course-progress { | ||
| 551 | + display: flex; | ||
| 552 | + flex-direction: column; | ||
| 553 | + align-items: flex-end; | ||
| 554 | + gap: 4px; | ||
| 555 | +} | ||
| 556 | + | ||
| 557 | +.course-status { | ||
| 558 | + padding: 4px 8px; | ||
| 559 | + border-radius: 12px; | ||
| 560 | + font-size: 12px; | ||
| 561 | + font-weight: 500; | ||
| 562 | +} | ||
| 563 | + | ||
| 564 | +.course-completed { | ||
| 565 | + background: #dcfce7; | ||
| 566 | + color: #166534; | ||
| 567 | +} | ||
| 568 | + | ||
| 569 | +.course-studying { | ||
| 570 | + background: #dbeafe; | ||
| 571 | + color: #1d4ed8; | ||
| 572 | +} | ||
| 573 | + | ||
| 574 | +.course-pending { | ||
| 575 | + background: #f3f4f6; | ||
| 576 | + color: #6b7280; | ||
| 577 | +} | ||
| 578 | + | ||
| 579 | +.course-score { | ||
| 580 | + font-size: 14px; | ||
| 581 | + font-weight: 600; | ||
| 582 | + color: #8b5cf6; | ||
| 583 | +} | ||
| 584 | + | ||
| 585 | +.records-section { | ||
| 586 | + background: white; | ||
| 587 | + border-radius: 12px; | ||
| 588 | + padding: 20px; | ||
| 589 | + margin-bottom: 16px; | ||
| 590 | +} | ||
| 591 | + | ||
| 592 | +.record-content { | ||
| 593 | + padding-left: 16px; | ||
| 594 | +} | ||
| 595 | + | ||
| 596 | +.record-title { | ||
| 597 | + font-size: 16px; | ||
| 598 | + font-weight: 600; | ||
| 599 | + color: #333; | ||
| 600 | + margin: 0 0 8px 0; | ||
| 601 | +} | ||
| 602 | + | ||
| 603 | +.record-description { | ||
| 604 | + font-size: 14px; | ||
| 605 | + color: #666; | ||
| 606 | + margin: 0 0 12px 0; | ||
| 607 | + line-height: 1.5; | ||
| 608 | +} | ||
| 609 | + | ||
| 610 | +.record-tags { | ||
| 611 | + display: flex; | ||
| 612 | + gap: 8px; | ||
| 613 | + flex-wrap: wrap; | ||
| 614 | +} | ||
| 615 | + | ||
| 616 | +.record-tag { | ||
| 617 | + background: #f3e8ff !important; | ||
| 618 | + color: #8b5cf6 !important; | ||
| 619 | + border: 1px solid #c4b5fd !important; | ||
| 620 | +} | ||
| 621 | +</style> | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
src/views/Students.vue
0 → 100644
| 1 | +<template> | ||
| 2 | + <div class="page-container"> | ||
| 3 | + <!-- 导航栏 --> | ||
| 4 | + <van-nav-bar title="戒子管理" left-arrow @click-left="$router.back()" class="custom-nav"> | ||
| 5 | + <template #right> | ||
| 6 | + <van-icon name="search" size="18" @click="handleSearch" /> | ||
| 7 | + </template> | ||
| 8 | + </van-nav-bar> | ||
| 9 | + | ||
| 10 | + <!-- 内容区域 --> | ||
| 11 | + <div class="content-container"> | ||
| 12 | + <!-- 顶部统计 --> | ||
| 13 | + <div class="stats-section"> | ||
| 14 | + <div class="stat-card"> | ||
| 15 | + <div class="stat-icon">🙏</div> | ||
| 16 | + <div class="stat-info"> | ||
| 17 | + <div class="stat-number">{{ totalStudents }}</div> | ||
| 18 | + <div class="stat-label">总戒子数</div> | ||
| 19 | + </div> | ||
| 20 | + </div> | ||
| 21 | + <div class="stat-card"> | ||
| 22 | + <div class="stat-icon">📚</div> | ||
| 23 | + <div class="stat-info"> | ||
| 24 | + <div class="stat-number">{{ studyingStudents }}</div> | ||
| 25 | + <div class="stat-label">在学戒子</div> | ||
| 26 | + </div> | ||
| 27 | + </div> | ||
| 28 | + <div class="stat-card"> | ||
| 29 | + <div class="stat-icon">🎓</div> | ||
| 30 | + <div class="stat-info"> | ||
| 31 | + <div class="stat-number">{{ graduatedStudents }}</div> | ||
| 32 | + <div class="stat-label">已毕业</div> | ||
| 33 | + </div> | ||
| 34 | + </div> | ||
| 35 | + </div> | ||
| 36 | + | ||
| 37 | + <!-- 筛选栏 --> | ||
| 38 | + <div class="filter-section"> | ||
| 39 | + <van-tabs v-model:active="activeTab" @change="handleTabChange" class="custom-tabs"> | ||
| 40 | + <van-tab title="全部" name="all"></van-tab> | ||
| 41 | + <van-tab title="沙弥戒" name="novice"></van-tab> | ||
| 42 | + <van-tab title="比丘戒" name="bhiksu"></van-tab> | ||
| 43 | + <van-tab title="菩萨戒" name="bodhisattva"></van-tab> | ||
| 44 | + </van-tabs> | ||
| 45 | + </div> | ||
| 46 | + | ||
| 47 | + <!-- 戒子列表 --> | ||
| 48 | + <div class="students-list"> | ||
| 49 | + <div | ||
| 50 | + v-for="student in filteredStudents" | ||
| 51 | + :key="student.id" | ||
| 52 | + class="student-card" | ||
| 53 | + @click="handleStudentClick(student)" | ||
| 54 | + > | ||
| 55 | + <div class="student-avatar"> | ||
| 56 | + <img v-if="student.avatar" :src="student.avatar" :alt="student.name" /> | ||
| 57 | + <div v-else class="avatar-placeholder"> | ||
| 58 | + <span>{{ student.name.charAt(0) }}</span> | ||
| 59 | + </div> | ||
| 60 | + <div class="precept-badge" :class="getPreceptClass(student.preceptType)"> | ||
| 61 | + {{ getPreceptText(student.preceptType) }} | ||
| 62 | + </div> | ||
| 63 | + </div> | ||
| 64 | + | ||
| 65 | + <div class="student-info"> | ||
| 66 | + <div class="student-header"> | ||
| 67 | + <h4 class="student-name">{{ student.name }}</h4> | ||
| 68 | + <div class="student-status" :class="getStatusClass(student.status)"> | ||
| 69 | + {{ getStatusText(student.status) }} | ||
| 70 | + </div> | ||
| 71 | + </div> | ||
| 72 | + | ||
| 73 | + <div class="student-details"> | ||
| 74 | + <p class="student-temple">{{ student.temple }}</p> | ||
| 75 | + <p class="student-teacher">戒师:{{ student.teacher }}</p> | ||
| 76 | + <div class="student-meta"> | ||
| 77 | + <span class="entry-date">{{ student.entryDate }}入学</span> | ||
| 78 | + <span class="progress">进度:{{ student.progress }}%</span> | ||
| 79 | + </div> | ||
| 80 | + </div> | ||
| 81 | + </div> | ||
| 82 | + | ||
| 83 | + <div class="student-actions"> | ||
| 84 | + <van-icon name="arrow" /> | ||
| 85 | + </div> | ||
| 86 | + </div> | ||
| 87 | + </div> | ||
| 88 | + | ||
| 89 | + <!-- 空状态 --> | ||
| 90 | + <van-empty v-if="filteredStudents.length === 0" description="暂无戒子信息" /> | ||
| 91 | + </div> | ||
| 92 | + </div> | ||
| 93 | +</template> | ||
| 94 | + | ||
| 95 | +<script setup> | ||
| 96 | +import { ref, computed } from 'vue' | ||
| 97 | +import { useRouter } from 'vue-router' | ||
| 98 | +import { Toast } from 'vant' | ||
| 99 | + | ||
| 100 | +const router = useRouter() | ||
| 101 | +const activeTab = ref('all') | ||
| 102 | + | ||
| 103 | +// 统计数据 | ||
| 104 | +const totalStudents = ref(68) | ||
| 105 | +const studyingStudents = ref(52) | ||
| 106 | +const graduatedStudents = ref(16) | ||
| 107 | + | ||
| 108 | +// 戒子数据 | ||
| 109 | +const students = ref([ | ||
| 110 | + { | ||
| 111 | + id: 1, | ||
| 112 | + name: '释慧明', | ||
| 113 | + temple: '大雄宝殿', | ||
| 114 | + teacher: '释智慧法师', | ||
| 115 | + preceptType: 'bhiksu', | ||
| 116 | + status: 'studying', | ||
| 117 | + entryDate: '2023-03-15', | ||
| 118 | + progress: 75, | ||
| 119 | + avatar: null | ||
| 120 | + }, | ||
| 121 | + { | ||
| 122 | + id: 2, | ||
| 123 | + name: '释德行', | ||
| 124 | + temple: '观音殿', | ||
| 125 | + teacher: '释慈悲法师', | ||
| 126 | + preceptType: 'novice', | ||
| 127 | + status: 'studying', | ||
| 128 | + entryDate: '2023-06-20', | ||
| 129 | + progress: 45, | ||
| 130 | + avatar: null | ||
| 131 | + }, | ||
| 132 | + { | ||
| 133 | + id: 3, | ||
| 134 | + name: '释觉悟', | ||
| 135 | + temple: '文殊殿', | ||
| 136 | + teacher: '释般若法师', | ||
| 137 | + preceptType: 'bodhisattva', | ||
| 138 | + status: 'graduated', | ||
| 139 | + entryDate: '2022-09-10', | ||
| 140 | + progress: 100, | ||
| 141 | + avatar: null | ||
| 142 | + }, | ||
| 143 | + { | ||
| 144 | + id: 4, | ||
| 145 | + name: '释持戒', | ||
| 146 | + temple: '地藏殿', | ||
| 147 | + teacher: '释智慧法师', | ||
| 148 | + preceptType: 'bhiksu', | ||
| 149 | + status: 'studying', | ||
| 150 | + entryDate: '2023-01-05', | ||
| 151 | + progress: 85, | ||
| 152 | + avatar: null | ||
| 153 | + }, | ||
| 154 | + { | ||
| 155 | + id: 5, | ||
| 156 | + name: '释精进', | ||
| 157 | + temple: '药师殿', | ||
| 158 | + teacher: '释慈悲法师', | ||
| 159 | + preceptType: 'novice', | ||
| 160 | + status: 'studying', | ||
| 161 | + entryDate: '2023-08-12', | ||
| 162 | + progress: 30, | ||
| 163 | + avatar: null | ||
| 164 | + }, | ||
| 165 | + { | ||
| 166 | + id: 6, | ||
| 167 | + name: '释忍辱', | ||
| 168 | + temple: '弥勒殿', | ||
| 169 | + teacher: '释般若法师', | ||
| 170 | + preceptType: 'bodhisattva', | ||
| 171 | + status: 'studying', | ||
| 172 | + entryDate: '2023-04-18', | ||
| 173 | + progress: 60, | ||
| 174 | + avatar: null | ||
| 175 | + }, | ||
| 176 | + { | ||
| 177 | + id: 7, | ||
| 178 | + name: '释禅定', | ||
| 179 | + temple: '韦陀殿', | ||
| 180 | + teacher: '释智慧法师', | ||
| 181 | + preceptType: 'bhiksu', | ||
| 182 | + status: 'graduated', | ||
| 183 | + entryDate: '2022-11-30', | ||
| 184 | + progress: 100, | ||
| 185 | + avatar: null | ||
| 186 | + }, | ||
| 187 | + { | ||
| 188 | + id: 8, | ||
| 189 | + name: '释智慧', | ||
| 190 | + temple: '伽蓝殿', | ||
| 191 | + teacher: '释慈悲法师', | ||
| 192 | + preceptType: 'novice', | ||
| 193 | + status: 'studying', | ||
| 194 | + entryDate: '2023-07-08', | ||
| 195 | + progress: 40, | ||
| 196 | + avatar: null | ||
| 197 | + } | ||
| 198 | +]) | ||
| 199 | + | ||
| 200 | +// 过滤后的戒子列表 | ||
| 201 | +const filteredStudents = computed(() => { | ||
| 202 | + if (activeTab.value === 'all') { | ||
| 203 | + return students.value | ||
| 204 | + } | ||
| 205 | + return students.value.filter(student => student.preceptType === activeTab.value) | ||
| 206 | +}) | ||
| 207 | + | ||
| 208 | +// 获取戒律类型样式类 | ||
| 209 | +const getPreceptClass = (type) => { | ||
| 210 | + const classes = { | ||
| 211 | + novice: 'precept-novice', | ||
| 212 | + bhiksu: 'precept-bhiksu', | ||
| 213 | + bodhisattva: 'precept-bodhisattva' | ||
| 214 | + } | ||
| 215 | + return classes[type] || 'precept-novice' | ||
| 216 | +} | ||
| 217 | + | ||
| 218 | +// 获取戒律类型文本 | ||
| 219 | +const getPreceptText = (type) => { | ||
| 220 | + const texts = { | ||
| 221 | + novice: '沙弥', | ||
| 222 | + bhiksu: '比丘', | ||
| 223 | + bodhisattva: '菩萨' | ||
| 224 | + } | ||
| 225 | + return texts[type] || '沙弥' | ||
| 226 | +} | ||
| 227 | + | ||
| 228 | +// 获取状态样式类 | ||
| 229 | +const getStatusClass = (status) => { | ||
| 230 | + const classes = { | ||
| 231 | + studying: 'status-studying', | ||
| 232 | + graduated: 'status-graduated', | ||
| 233 | + suspended: 'status-suspended' | ||
| 234 | + } | ||
| 235 | + return classes[status] || 'status-studying' | ||
| 236 | +} | ||
| 237 | + | ||
| 238 | +// 获取状态文本 | ||
| 239 | +const getStatusText = (status) => { | ||
| 240 | + const texts = { | ||
| 241 | + studying: '在学', | ||
| 242 | + graduated: '毕业', | ||
| 243 | + suspended: '暂停' | ||
| 244 | + } | ||
| 245 | + return texts[status] || '在学' | ||
| 246 | +} | ||
| 247 | + | ||
| 248 | +// 处理标签切换 | ||
| 249 | +const handleTabChange = (name) => { | ||
| 250 | + activeTab.value = name | ||
| 251 | +} | ||
| 252 | + | ||
| 253 | +// 处理戒子点击 | ||
| 254 | +const handleStudentClick = (student) => { | ||
| 255 | + router.push(`/students/${student.id}`) | ||
| 256 | +} | ||
| 257 | + | ||
| 258 | +// 处理搜索 | ||
| 259 | +const handleSearch = () => { | ||
| 260 | + Toast('搜索功能开发中...') | ||
| 261 | +} | ||
| 262 | +</script> | ||
| 263 | + | ||
| 264 | +<style scoped> | ||
| 265 | +.page-container { | ||
| 266 | + min-height: 100vh; | ||
| 267 | + background: #fafafa; | ||
| 268 | +} | ||
| 269 | + | ||
| 270 | +.custom-nav { | ||
| 271 | + background: linear-gradient(135deg, #8b5cf6, #7c3aed); | ||
| 272 | + color: white; | ||
| 273 | +} | ||
| 274 | + | ||
| 275 | +.custom-nav :deep(.van-nav-bar__title) { | ||
| 276 | + color: white; | ||
| 277 | + font-weight: 600; | ||
| 278 | +} | ||
| 279 | + | ||
| 280 | +.custom-nav :deep(.van-icon) { | ||
| 281 | + color: white; | ||
| 282 | +} | ||
| 283 | + | ||
| 284 | +.content-container { | ||
| 285 | + padding-top: 46px; | ||
| 286 | +} | ||
| 287 | + | ||
| 288 | +.stats-section { | ||
| 289 | + display: flex; | ||
| 290 | + gap: 12px; | ||
| 291 | + padding: 16px; | ||
| 292 | +} | ||
| 293 | + | ||
| 294 | +.stat-card { | ||
| 295 | + flex: 1; | ||
| 296 | + background: white; | ||
| 297 | + border-radius: 12px; | ||
| 298 | + padding: 16px; | ||
| 299 | + display: flex; | ||
| 300 | + align-items: center; | ||
| 301 | + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); | ||
| 302 | +} | ||
| 303 | + | ||
| 304 | +.stat-icon { | ||
| 305 | + font-size: 24px; | ||
| 306 | + margin-right: 12px; | ||
| 307 | +} | ||
| 308 | + | ||
| 309 | +.stat-info { | ||
| 310 | + flex: 1; | ||
| 311 | +} | ||
| 312 | + | ||
| 313 | +.stat-number { | ||
| 314 | + font-size: 20px; | ||
| 315 | + font-weight: 700; | ||
| 316 | + color: #8b5cf6; | ||
| 317 | + margin-bottom: 2px; | ||
| 318 | +} | ||
| 319 | + | ||
| 320 | +.stat-label { | ||
| 321 | + font-size: 12px; | ||
| 322 | + color: #666; | ||
| 323 | +} | ||
| 324 | + | ||
| 325 | +.filter-section { | ||
| 326 | + background: white; | ||
| 327 | + border-bottom: 1px solid #eee; | ||
| 328 | +} | ||
| 329 | + | ||
| 330 | +.custom-tabs :deep(.van-tab) { | ||
| 331 | + font-weight: 500; | ||
| 332 | +} | ||
| 333 | + | ||
| 334 | +.custom-tabs :deep(.van-tab--active) { | ||
| 335 | + color: #8b5cf6; | ||
| 336 | +} | ||
| 337 | + | ||
| 338 | +.custom-tabs :deep(.van-tabs__line) { | ||
| 339 | + background: #8b5cf6; | ||
| 340 | +} | ||
| 341 | + | ||
| 342 | +.students-list { | ||
| 343 | + padding: 16px; | ||
| 344 | +} | ||
| 345 | + | ||
| 346 | +.student-card { | ||
| 347 | + background: white; | ||
| 348 | + border-radius: 12px; | ||
| 349 | + padding: 16px; | ||
| 350 | + margin-bottom: 12px; | ||
| 351 | + display: flex; | ||
| 352 | + align-items: center; | ||
| 353 | + gap: 16px; | ||
| 354 | + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); | ||
| 355 | + cursor: pointer; | ||
| 356 | + transition: all 0.3s ease; | ||
| 357 | +} | ||
| 358 | + | ||
| 359 | +.student-card:hover { | ||
| 360 | + transform: translateY(-3px); | ||
| 361 | + box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15); | ||
| 362 | +} | ||
| 363 | + | ||
| 364 | +.student-card:active { | ||
| 365 | + transform: translateY(-1px); | ||
| 366 | + box-shadow: 0 3px 12px rgba(0, 0, 0, 0.12); | ||
| 367 | +} | ||
| 368 | + | ||
| 369 | +.student-avatar { | ||
| 370 | + position: relative; | ||
| 371 | + width: 60px; | ||
| 372 | + height: 60px; | ||
| 373 | + border-radius: 50%; | ||
| 374 | + overflow: hidden; | ||
| 375 | + margin-right: 16px; | ||
| 376 | + flex-shrink: 0; | ||
| 377 | +} | ||
| 378 | + | ||
| 379 | +.student-avatar img { | ||
| 380 | + width: 100%; | ||
| 381 | + height: 100%; | ||
| 382 | + object-fit: cover; | ||
| 383 | +} | ||
| 384 | + | ||
| 385 | +.avatar-placeholder { | ||
| 386 | + width: 100%; | ||
| 387 | + height: 100%; | ||
| 388 | + background: linear-gradient(135deg, #8b5cf6, #7c3aed); | ||
| 389 | + display: flex; | ||
| 390 | + align-items: center; | ||
| 391 | + justify-content: center; | ||
| 392 | + color: white; | ||
| 393 | + font-size: 24px; | ||
| 394 | + font-weight: 600; | ||
| 395 | +} | ||
| 396 | + | ||
| 397 | +.precept-badge { | ||
| 398 | + position: absolute; | ||
| 399 | + bottom: -2px; | ||
| 400 | + right: -2px; | ||
| 401 | + padding: 2px 6px; | ||
| 402 | + border-radius: 8px; | ||
| 403 | + font-size: 10px; | ||
| 404 | + font-weight: 500; | ||
| 405 | + border: 2px solid white; | ||
| 406 | +} | ||
| 407 | + | ||
| 408 | +.precept-novice { | ||
| 409 | + background: #10b981; | ||
| 410 | + color: white; | ||
| 411 | +} | ||
| 412 | + | ||
| 413 | +.precept-bhiksu { | ||
| 414 | + background: #3b82f6; | ||
| 415 | + color: white; | ||
| 416 | +} | ||
| 417 | + | ||
| 418 | +.precept-bodhisattva { | ||
| 419 | + background: #f59e0b; | ||
| 420 | + color: white; | ||
| 421 | +} | ||
| 422 | + | ||
| 423 | +.student-info { | ||
| 424 | + flex: 1; | ||
| 425 | +} | ||
| 426 | + | ||
| 427 | +.student-header { | ||
| 428 | + display: flex; | ||
| 429 | + align-items: center; | ||
| 430 | + justify-content: space-between; | ||
| 431 | + margin-bottom: 8px; | ||
| 432 | +} | ||
| 433 | + | ||
| 434 | +.student-name { | ||
| 435 | + font-size: 18px; | ||
| 436 | + font-weight: 600; | ||
| 437 | + color: #333; | ||
| 438 | + margin: 0; | ||
| 439 | +} | ||
| 440 | + | ||
| 441 | +.student-status { | ||
| 442 | + padding: 4px 8px; | ||
| 443 | + border-radius: 12px; | ||
| 444 | + font-size: 12px; | ||
| 445 | + font-weight: 500; | ||
| 446 | +} | ||
| 447 | + | ||
| 448 | +.status-studying { | ||
| 449 | + background: #dbeafe; | ||
| 450 | + color: #1d4ed8; | ||
| 451 | + border: 1px solid #93c5fd; | ||
| 452 | +} | ||
| 453 | + | ||
| 454 | +.status-graduated { | ||
| 455 | + background: #dcfce7; | ||
| 456 | + color: #166534; | ||
| 457 | + border: 1px solid #86efac; | ||
| 458 | +} | ||
| 459 | + | ||
| 460 | +.status-suspended { | ||
| 461 | + background: #fef3c7; | ||
| 462 | + color: #92400e; | ||
| 463 | + border: 1px solid #fcd34d; | ||
| 464 | +} | ||
| 465 | + | ||
| 466 | +.student-details { | ||
| 467 | + space-y: 4px; | ||
| 468 | +} | ||
| 469 | + | ||
| 470 | +.student-temple { | ||
| 471 | + font-size: 16px; | ||
| 472 | + color: #666; | ||
| 473 | + margin: 0 0 4px 0; | ||
| 474 | +} | ||
| 475 | + | ||
| 476 | +.student-teacher { | ||
| 477 | + font-size: 14px; | ||
| 478 | + color: #999; | ||
| 479 | + margin: 0 0 8px 0; | ||
| 480 | +} | ||
| 481 | + | ||
| 482 | +.student-meta { | ||
| 483 | + display: flex; | ||
| 484 | + gap: 16px; | ||
| 485 | +} | ||
| 486 | + | ||
| 487 | +.entry-date, | ||
| 488 | +.progress { | ||
| 489 | + font-size: 12px; | ||
| 490 | + color: #999; | ||
| 491 | + background: #f5f5f5; | ||
| 492 | + padding: 2px 6px; | ||
| 493 | + border-radius: 4px; | ||
| 494 | +} | ||
| 495 | + | ||
| 496 | +.student-actions { | ||
| 497 | + margin-left: 12px; | ||
| 498 | + color: #ccc; | ||
| 499 | +} | ||
| 500 | +</style> | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
src/views/TeacherDetail.vue
0 → 100644
| 1 | +<template> | ||
| 2 | + <div class="page-container"> | ||
| 3 | + <!-- 导航栏 --> | ||
| 4 | + <van-nav-bar title="法师详情" left-arrow @click-left="$router.back()" class="custom-nav"> | ||
| 5 | + <template #right> | ||
| 6 | + <van-icon name="share" size="18" /> | ||
| 7 | + </template> | ||
| 8 | + </van-nav-bar> | ||
| 9 | + | ||
| 10 | + <!-- 内容区域 --> | ||
| 11 | + <div class="content-container"> | ||
| 12 | + <!-- 法师基本信息 --> | ||
| 13 | + <div class="teacher-profile"> | ||
| 14 | + <div class="profile-header"> | ||
| 15 | + <div class="teacher-avatar"> | ||
| 16 | + <img v-if="teacher.avatar" :src="teacher.avatar" :alt="teacher.name" /> | ||
| 17 | + <div v-else class="avatar-placeholder"> | ||
| 18 | + <span>{{ teacher.name.charAt(0) }}</span> | ||
| 19 | + </div> | ||
| 20 | + </div> | ||
| 21 | + | ||
| 22 | + <div class="profile-info"> | ||
| 23 | + <h2 class="teacher-name">{{ teacher.name }}</h2> | ||
| 24 | + <div class="teacher-role" :class="getRoleClass(teacher.role)"> | ||
| 25 | + {{ teacher.role }} | ||
| 26 | + </div> | ||
| 27 | + <p class="teacher-title">{{ teacher.title }} · {{ teacher.temple }}</p> | ||
| 28 | + </div> | ||
| 29 | + </div> | ||
| 30 | + | ||
| 31 | + <!-- 统计信息 --> | ||
| 32 | + <div class="stats-section"> | ||
| 33 | + <div class="stat-item"> | ||
| 34 | + <div class="stat-number">{{ teacher.experience }}</div> | ||
| 35 | + <div class="stat-label">戒腊年数</div> | ||
| 36 | + </div> | ||
| 37 | + <div class="stat-divider"></div> | ||
| 38 | + <div class="stat-item"> | ||
| 39 | + <div class="stat-number">{{ teacher.ordinationYear }}</div> | ||
| 40 | + <div class="stat-label">受戒年份</div> | ||
| 41 | + </div> | ||
| 42 | + <div class="stat-divider"></div> | ||
| 43 | + <div class="stat-item"> | ||
| 44 | + <div class="stat-number">{{ teacher.disciples || 0 }}</div> | ||
| 45 | + <div class="stat-label">弟子人数</div> | ||
| 46 | + </div> | ||
| 47 | + </div> | ||
| 48 | + </div> | ||
| 49 | + | ||
| 50 | + <!-- 详细信息 --> | ||
| 51 | + <div class="detail-sections"> | ||
| 52 | + <!-- 个人简介 --> | ||
| 53 | + <div class="detail-section"> | ||
| 54 | + <h3 class="section-title"> | ||
| 55 | + <van-icon name="user-o" /> | ||
| 56 | + 个人简介 | ||
| 57 | + </h3> | ||
| 58 | + <div class="section-content"> | ||
| 59 | + <p>{{ teacher.biography || '暂无个人简介信息' }}</p> | ||
| 60 | + </div> | ||
| 61 | + </div> | ||
| 62 | + | ||
| 63 | + <!-- 修学经历 --> | ||
| 64 | + <div class="detail-section"> | ||
| 65 | + <h3 class="section-title"> | ||
| 66 | + <van-icon name="certificate" /> | ||
| 67 | + 修学经历 | ||
| 68 | + </h3> | ||
| 69 | + <div class="section-content"> | ||
| 70 | + <div class="timeline"> | ||
| 71 | + <div v-for="(experience, index) in teacher.experiences" :key="index" class="timeline-item"> | ||
| 72 | + <div class="timeline-dot"></div> | ||
| 73 | + <div class="timeline-content"> | ||
| 74 | + <div class="timeline-year">{{ experience.year }}</div> | ||
| 75 | + <div class="timeline-event">{{ experience.event }}</div> | ||
| 76 | + <div class="timeline-location">{{ experience.location }}</div> | ||
| 77 | + </div> | ||
| 78 | + </div> | ||
| 79 | + </div> | ||
| 80 | + </div> | ||
| 81 | + </div> | ||
| 82 | + | ||
| 83 | + <!-- 弘法活动 --> | ||
| 84 | + <div class="detail-section"> | ||
| 85 | + <h3 class="section-title"> | ||
| 86 | + <van-icon name="fire-o" /> | ||
| 87 | + 弘法活动 | ||
| 88 | + </h3> | ||
| 89 | + <div class="section-content"> | ||
| 90 | + <div v-if="teacher.activities && teacher.activities.length > 0" class="activities-list"> | ||
| 91 | + <div v-for="activity in teacher.activities" :key="activity.id" class="activity-item"> | ||
| 92 | + <div class="activity-date">{{ activity.date }}</div> | ||
| 93 | + <div class="activity-title">{{ activity.title }}</div> | ||
| 94 | + <div class="activity-location">{{ activity.location }}</div> | ||
| 95 | + </div> | ||
| 96 | + </div> | ||
| 97 | + <p v-else class="no-data">暂无弘法活动记录</p> | ||
| 98 | + </div> | ||
| 99 | + </div> | ||
| 100 | + | ||
| 101 | + <!-- 联系方式 --> | ||
| 102 | + <div class="detail-section"> | ||
| 103 | + <h3 class="section-title"> | ||
| 104 | + <van-icon name="phone-o" /> | ||
| 105 | + 联系方式 | ||
| 106 | + </h3> | ||
| 107 | + <div class="section-content"> | ||
| 108 | + <div class="contact-info"> | ||
| 109 | + <div class="contact-item"> | ||
| 110 | + <span class="contact-label">所在寺院:</span> | ||
| 111 | + <span class="contact-value">{{ teacher.temple }}</span> | ||
| 112 | + </div> | ||
| 113 | + <div class="contact-item" v-if="teacher.phone"> | ||
| 114 | + <span class="contact-label">联系电话:</span> | ||
| 115 | + <span class="contact-value">{{ teacher.phone }}</span> | ||
| 116 | + </div> | ||
| 117 | + <div class="contact-item" v-if="teacher.email"> | ||
| 118 | + <span class="contact-label">电子邮箱:</span> | ||
| 119 | + <span class="contact-value">{{ teacher.email }}</span> | ||
| 120 | + </div> | ||
| 121 | + </div> | ||
| 122 | + </div> | ||
| 123 | + </div> | ||
| 124 | + </div> | ||
| 125 | + </div> | ||
| 126 | + </div> | ||
| 127 | +</template> | ||
| 128 | + | ||
| 129 | +<script setup> | ||
| 130 | +import { ref, onMounted } from 'vue' | ||
| 131 | +import { useRoute, useRouter } from 'vue-router' | ||
| 132 | + | ||
| 133 | +const route = useRoute() | ||
| 134 | +const router = useRouter() | ||
| 135 | + | ||
| 136 | +// 法师详细信息 | ||
| 137 | +const teacher = ref({ | ||
| 138 | + id: 1, | ||
| 139 | + name: '慧明法师', | ||
| 140 | + title: '方丈', | ||
| 141 | + role: '得戒和尚', | ||
| 142 | + temple: '大觉寺', | ||
| 143 | + ordinationYear: 1985, | ||
| 144 | + experience: 39, | ||
| 145 | + disciples: 156, | ||
| 146 | + avatar: null, | ||
| 147 | + biography: '慧明法师,俗姓李,1960年生于江苏南京。1985年在大觉寺依止上慧下觉老和尚剃度出家,同年在宝华山隆昌寺受具足戒。法师戒行清净,学修并重,深得四众弟子敬仰。现任大觉寺方丈,致力于佛法弘扬和寺院建设。', | ||
| 148 | + experiences: [ | ||
| 149 | + { | ||
| 150 | + year: '1985年', | ||
| 151 | + event: '在大觉寺剃度出家', | ||
| 152 | + location: '大觉寺' | ||
| 153 | + }, | ||
| 154 | + { | ||
| 155 | + year: '1985年', | ||
| 156 | + event: '在宝华山隆昌寺受具足戒', | ||
| 157 | + location: '宝华山隆昌寺' | ||
| 158 | + }, | ||
| 159 | + { | ||
| 160 | + year: '1990年', | ||
| 161 | + event: '任大觉寺知客', | ||
| 162 | + location: '大觉寺' | ||
| 163 | + }, | ||
| 164 | + { | ||
| 165 | + year: '1995年', | ||
| 166 | + event: '任大觉寺监院', | ||
| 167 | + location: '大觉寺' | ||
| 168 | + }, | ||
| 169 | + { | ||
| 170 | + year: '2000年', | ||
| 171 | + event: '升座为大觉寺方丈', | ||
| 172 | + location: '大觉寺' | ||
| 173 | + } | ||
| 174 | + ], | ||
| 175 | + activities: [ | ||
| 176 | + { | ||
| 177 | + id: 1, | ||
| 178 | + date: '2024-01-15', | ||
| 179 | + title: '三坛大戒传戒法会', | ||
| 180 | + location: '大觉寺' | ||
| 181 | + }, | ||
| 182 | + { | ||
| 183 | + id: 2, | ||
| 184 | + date: '2023-12-08', | ||
| 185 | + title: '佛成道日法会', | ||
| 186 | + location: '大觉寺' | ||
| 187 | + }, | ||
| 188 | + { | ||
| 189 | + id: 3, | ||
| 190 | + date: '2023-11-20', | ||
| 191 | + title: '佛学讲座:戒律的现代意义', | ||
| 192 | + location: '大觉寺讲堂' | ||
| 193 | + } | ||
| 194 | + ], | ||
| 195 | + phone: '025-12345678', | ||
| 196 | + email: 'huiming@dajuesi.org' | ||
| 197 | +}) | ||
| 198 | + | ||
| 199 | +// 获取角色样式类 | ||
| 200 | +const getRoleClass = (role) => { | ||
| 201 | + if (role.includes('和尚') || role.includes('阿阇梨')) { | ||
| 202 | + return 'role-teacher' | ||
| 203 | + } | ||
| 204 | + return 'role-witness' | ||
| 205 | +} | ||
| 206 | + | ||
| 207 | +// 加载法师详情 | ||
| 208 | +const loadTeacherDetail = async () => { | ||
| 209 | + const teacherId = route.params.id | ||
| 210 | + // 这里应该根据 teacherId 从 API 获取法师详情 | ||
| 211 | + // 现在使用模拟数据 | ||
| 212 | + console.log('Loading teacher detail for ID:', teacherId) | ||
| 213 | +} | ||
| 214 | + | ||
| 215 | +onMounted(() => { | ||
| 216 | + loadTeacherDetail() | ||
| 217 | +}) | ||
| 218 | +</script> | ||
| 219 | + | ||
| 220 | +<style scoped> | ||
| 221 | +.page-container { | ||
| 222 | + min-height: 100vh; | ||
| 223 | + background: #fafafa; | ||
| 224 | +} | ||
| 225 | + | ||
| 226 | +.custom-nav { | ||
| 227 | + background: linear-gradient(135deg, #fbbf24, #f97316); | ||
| 228 | + color: white; | ||
| 229 | +} | ||
| 230 | + | ||
| 231 | +.custom-nav :deep(.van-nav-bar__title) { | ||
| 232 | + color: white; | ||
| 233 | + font-weight: 600; | ||
| 234 | +} | ||
| 235 | + | ||
| 236 | +.custom-nav :deep(.van-icon) { | ||
| 237 | + color: white; | ||
| 238 | +} | ||
| 239 | + | ||
| 240 | +.content-container { | ||
| 241 | + padding-top: 46px; | ||
| 242 | +} | ||
| 243 | + | ||
| 244 | +.teacher-profile { | ||
| 245 | + background: white; | ||
| 246 | + margin: 16px; | ||
| 247 | + border-radius: 12px; | ||
| 248 | + padding: 24px; | ||
| 249 | + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); | ||
| 250 | +} | ||
| 251 | + | ||
| 252 | +.profile-header { | ||
| 253 | + display: flex; | ||
| 254 | + align-items: center; | ||
| 255 | + margin-bottom: 24px; | ||
| 256 | +} | ||
| 257 | + | ||
| 258 | +.teacher-avatar { | ||
| 259 | + width: 80px; | ||
| 260 | + height: 80px; | ||
| 261 | + border-radius: 50%; | ||
| 262 | + overflow: hidden; | ||
| 263 | + margin-right: 20px; | ||
| 264 | + flex-shrink: 0; | ||
| 265 | +} | ||
| 266 | + | ||
| 267 | +.teacher-avatar img { | ||
| 268 | + width: 100%; | ||
| 269 | + height: 100%; | ||
| 270 | + object-fit: cover; | ||
| 271 | +} | ||
| 272 | + | ||
| 273 | +.avatar-placeholder { | ||
| 274 | + width: 100%; | ||
| 275 | + height: 100%; | ||
| 276 | + background: linear-gradient(135deg, #fbbf24, #f97316); | ||
| 277 | + display: flex; | ||
| 278 | + align-items: center; | ||
| 279 | + justify-content: center; | ||
| 280 | + color: white; | ||
| 281 | + font-size: 32px; | ||
| 282 | + font-weight: 600; | ||
| 283 | +} | ||
| 284 | + | ||
| 285 | +.profile-info { | ||
| 286 | + flex: 1; | ||
| 287 | +} | ||
| 288 | + | ||
| 289 | +.teacher-name { | ||
| 290 | + font-size: 24px; | ||
| 291 | + font-weight: 700; | ||
| 292 | + color: #333; | ||
| 293 | + margin: 0 0 8px 0; | ||
| 294 | +} | ||
| 295 | + | ||
| 296 | +.teacher-role { | ||
| 297 | + display: inline-block; | ||
| 298 | + padding: 6px 12px; | ||
| 299 | + border-radius: 16px; | ||
| 300 | + font-size: 14px; | ||
| 301 | + font-weight: 500; | ||
| 302 | + margin-bottom: 8px; | ||
| 303 | +} | ||
| 304 | + | ||
| 305 | +.role-teacher { | ||
| 306 | + background: linear-gradient(135deg, #fbbf24, #f97316); | ||
| 307 | + color: white; | ||
| 308 | +} | ||
| 309 | + | ||
| 310 | +.role-witness { | ||
| 311 | + background: #f0f9ff; | ||
| 312 | + color: #0369a1; | ||
| 313 | + border: 1px solid #bae6fd; | ||
| 314 | +} | ||
| 315 | + | ||
| 316 | +.teacher-title { | ||
| 317 | + font-size: 16px; | ||
| 318 | + color: #666; | ||
| 319 | + margin: 0; | ||
| 320 | +} | ||
| 321 | + | ||
| 322 | +.stats-section { | ||
| 323 | + display: flex; | ||
| 324 | + align-items: center; | ||
| 325 | + justify-content: space-around; | ||
| 326 | + padding-top: 24px; | ||
| 327 | + border-top: 1px solid #f0f0f0; | ||
| 328 | +} | ||
| 329 | + | ||
| 330 | +.stat-item { | ||
| 331 | + text-align: center; | ||
| 332 | +} | ||
| 333 | + | ||
| 334 | +.stat-number { | ||
| 335 | + font-size: 24px; | ||
| 336 | + font-weight: 700; | ||
| 337 | + color: #f59e0b; | ||
| 338 | + margin-bottom: 4px; | ||
| 339 | +} | ||
| 340 | + | ||
| 341 | +.stat-label { | ||
| 342 | + font-size: 12px; | ||
| 343 | + color: #999; | ||
| 344 | +} | ||
| 345 | + | ||
| 346 | +.stat-divider { | ||
| 347 | + width: 1px; | ||
| 348 | + height: 40px; | ||
| 349 | + background: #f0f0f0; | ||
| 350 | +} | ||
| 351 | + | ||
| 352 | +.detail-sections { | ||
| 353 | + padding: 0 16px 16px; | ||
| 354 | +} | ||
| 355 | + | ||
| 356 | +.detail-section { | ||
| 357 | + background: white; | ||
| 358 | + border-radius: 12px; | ||
| 359 | + margin-bottom: 16px; | ||
| 360 | + overflow: hidden; | ||
| 361 | + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); | ||
| 362 | +} | ||
| 363 | + | ||
| 364 | +.section-title { | ||
| 365 | + display: flex; | ||
| 366 | + align-items: center; | ||
| 367 | + padding: 16px 20px; | ||
| 368 | + margin: 0; | ||
| 369 | + font-size: 16px; | ||
| 370 | + font-weight: 600; | ||
| 371 | + color: #333; | ||
| 372 | + background: #fafafa; | ||
| 373 | + border-bottom: 1px solid #f0f0f0; | ||
| 374 | +} | ||
| 375 | + | ||
| 376 | +.section-title .van-icon { | ||
| 377 | + margin-right: 8px; | ||
| 378 | + color: #f59e0b; | ||
| 379 | +} | ||
| 380 | + | ||
| 381 | +.section-content { | ||
| 382 | + padding: 20px; | ||
| 383 | +} | ||
| 384 | + | ||
| 385 | +.section-content p { | ||
| 386 | + font-size: 14px; | ||
| 387 | + line-height: 1.6; | ||
| 388 | + color: #666; | ||
| 389 | + margin: 0; | ||
| 390 | +} | ||
| 391 | + | ||
| 392 | +.timeline { | ||
| 393 | + position: relative; | ||
| 394 | +} | ||
| 395 | + | ||
| 396 | +.timeline::before { | ||
| 397 | + content: ''; | ||
| 398 | + position: absolute; | ||
| 399 | + left: 8px; | ||
| 400 | + top: 0; | ||
| 401 | + bottom: 0; | ||
| 402 | + width: 2px; | ||
| 403 | + background: #f0f0f0; | ||
| 404 | +} | ||
| 405 | + | ||
| 406 | +.timeline-item { | ||
| 407 | + position: relative; | ||
| 408 | + padding-left: 32px; | ||
| 409 | + margin-bottom: 20px; | ||
| 410 | +} | ||
| 411 | + | ||
| 412 | +.timeline-item:last-child { | ||
| 413 | + margin-bottom: 0; | ||
| 414 | +} | ||
| 415 | + | ||
| 416 | +.timeline-dot { | ||
| 417 | + position: absolute; | ||
| 418 | + left: 0; | ||
| 419 | + top: 4px; | ||
| 420 | + width: 16px; | ||
| 421 | + height: 16px; | ||
| 422 | + border-radius: 50%; | ||
| 423 | + background: #f59e0b; | ||
| 424 | + border: 3px solid white; | ||
| 425 | + box-shadow: 0 0 0 2px #f59e0b; | ||
| 426 | +} | ||
| 427 | + | ||
| 428 | +.timeline-year { | ||
| 429 | + font-size: 14px; | ||
| 430 | + font-weight: 600; | ||
| 431 | + color: #f59e0b; | ||
| 432 | + margin-bottom: 4px; | ||
| 433 | +} | ||
| 434 | + | ||
| 435 | +.timeline-event { | ||
| 436 | + font-size: 16px; | ||
| 437 | + font-weight: 500; | ||
| 438 | + color: #333; | ||
| 439 | + margin-bottom: 4px; | ||
| 440 | +} | ||
| 441 | + | ||
| 442 | +.timeline-location { | ||
| 443 | + font-size: 14px; | ||
| 444 | + color: #999; | ||
| 445 | +} | ||
| 446 | + | ||
| 447 | +.activities-list { | ||
| 448 | + space-y: 12px; | ||
| 449 | +} | ||
| 450 | + | ||
| 451 | +.activity-item { | ||
| 452 | + padding: 16px; | ||
| 453 | + background: #fafafa; | ||
| 454 | + border-radius: 8px; | ||
| 455 | + margin-bottom: 12px; | ||
| 456 | +} | ||
| 457 | + | ||
| 458 | +.activity-date { | ||
| 459 | + font-size: 12px; | ||
| 460 | + color: #f59e0b; | ||
| 461 | + font-weight: 500; | ||
| 462 | + margin-bottom: 4px; | ||
| 463 | +} | ||
| 464 | + | ||
| 465 | +.activity-title { | ||
| 466 | + font-size: 16px; | ||
| 467 | + font-weight: 500; | ||
| 468 | + color: #333; | ||
| 469 | + margin-bottom: 4px; | ||
| 470 | +} | ||
| 471 | + | ||
| 472 | +.activity-location { | ||
| 473 | + font-size: 14px; | ||
| 474 | + color: #666; | ||
| 475 | +} | ||
| 476 | + | ||
| 477 | +.contact-info { | ||
| 478 | + space-y: 12px; | ||
| 479 | +} | ||
| 480 | + | ||
| 481 | +.contact-item { | ||
| 482 | + display: flex; | ||
| 483 | + align-items: center; | ||
| 484 | + margin-bottom: 12px; | ||
| 485 | +} | ||
| 486 | + | ||
| 487 | +.contact-label { | ||
| 488 | + font-size: 14px; | ||
| 489 | + color: #666; | ||
| 490 | + width: 80px; | ||
| 491 | + flex-shrink: 0; | ||
| 492 | +} | ||
| 493 | + | ||
| 494 | +.contact-value { | ||
| 495 | + font-size: 14px; | ||
| 496 | + color: #333; | ||
| 497 | + flex: 1; | ||
| 498 | +} | ||
| 499 | + | ||
| 500 | +.no-data { | ||
| 501 | + text-align: center; | ||
| 502 | + color: #999; | ||
| 503 | + font-size: 14px; | ||
| 504 | + margin: 0; | ||
| 505 | +} | ||
| 506 | +</style> | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
src/views/Teachers.vue
0 → 100644
| 1 | +<template> | ||
| 2 | + <div class="page-container"> | ||
| 3 | + <!-- 导航栏 --> | ||
| 4 | + <van-nav-bar title="三师七证" left-arrow @click-left="$router.back()" class="custom-nav"> | ||
| 5 | + <template #right> | ||
| 6 | + <van-icon name="search" size="18" /> | ||
| 7 | + </template> | ||
| 8 | + </van-nav-bar> | ||
| 9 | + | ||
| 10 | + <!-- 内容区域 --> | ||
| 11 | + <div class="content-container"> | ||
| 12 | + <!-- 顶部说明 --> | ||
| 13 | + <div class="intro-section"> | ||
| 14 | + <div class="intro-card"> | ||
| 15 | + <div class="intro-icon">📜</div> | ||
| 16 | + <div class="intro-content"> | ||
| 17 | + <h3>三师七证</h3> | ||
| 18 | + <p>三师:得戒和尚、羯磨阿阇梨、教授阿阇梨<br>七证:七位证明师</p> | ||
| 19 | + </div> | ||
| 20 | + </div> | ||
| 21 | + </div> | ||
| 22 | + | ||
| 23 | + <!-- 筛选栏 --> | ||
| 24 | + <div class="filter-section"> | ||
| 25 | + <van-tabs v-model:active="activeTab" @change="handleTabChange" class="custom-tabs"> | ||
| 26 | + <van-tab title="全部" name="all"></van-tab> | ||
| 27 | + <van-tab title="三师" name="teachers"></van-tab> | ||
| 28 | + <van-tab title="七证" name="witnesses"></van-tab> | ||
| 29 | + </van-tabs> | ||
| 30 | + </div> | ||
| 31 | + | ||
| 32 | + <!-- 法师列表 --> | ||
| 33 | + <div class="teachers-list"> | ||
| 34 | + <div | ||
| 35 | + v-for="teacher in filteredTeachers" | ||
| 36 | + :key="teacher.id" | ||
| 37 | + class="teacher-card" | ||
| 38 | + @click="handleTeacherClick(teacher)" | ||
| 39 | + > | ||
| 40 | + <div class="teacher-avatar"> | ||
| 41 | + <img v-if="teacher.avatar" :src="teacher.avatar" :alt="teacher.name" /> | ||
| 42 | + <div v-else class="avatar-placeholder"> | ||
| 43 | + <span>{{ teacher.name.charAt(0) }}</span> | ||
| 44 | + </div> | ||
| 45 | + </div> | ||
| 46 | + | ||
| 47 | + <div class="teacher-info"> | ||
| 48 | + <div class="teacher-header"> | ||
| 49 | + <h4 class="teacher-name">{{ teacher.name }}</h4> | ||
| 50 | + <div class="teacher-role" :class="getRoleClass(teacher.role)"> | ||
| 51 | + {{ teacher.role }} | ||
| 52 | + </div> | ||
| 53 | + </div> | ||
| 54 | + | ||
| 55 | + <div class="teacher-details"> | ||
| 56 | + <p class="teacher-title">{{ teacher.title }}</p> | ||
| 57 | + <p class="teacher-temple">{{ teacher.temple }}</p> | ||
| 58 | + <div class="teacher-meta"> | ||
| 59 | + <span class="ordination-year">{{ teacher.ordinationYear }}年受戒</span> | ||
| 60 | + <span class="experience">{{ teacher.experience }}年戒腊</span> | ||
| 61 | + </div> | ||
| 62 | + </div> | ||
| 63 | + </div> | ||
| 64 | + | ||
| 65 | + <div class="teacher-actions"> | ||
| 66 | + <van-icon name="arrow" /> | ||
| 67 | + </div> | ||
| 68 | + </div> | ||
| 69 | + </div> | ||
| 70 | + | ||
| 71 | + <!-- 空状态 --> | ||
| 72 | + <van-empty v-if="filteredTeachers.length === 0" description="暂无相关法师信息" /> | ||
| 73 | + </div> | ||
| 74 | + </div> | ||
| 75 | +</template> | ||
| 76 | + | ||
| 77 | +<script setup> | ||
| 78 | +import { ref, computed } from 'vue' | ||
| 79 | +import { useRouter } from 'vue-router' | ||
| 80 | + | ||
| 81 | +const router = useRouter() | ||
| 82 | +const activeTab = ref('all') | ||
| 83 | + | ||
| 84 | +// 法师数据 | ||
| 85 | +const teachers = ref([ | ||
| 86 | + { | ||
| 87 | + id: 1, | ||
| 88 | + name: '慧明法师', | ||
| 89 | + title: '方丈', | ||
| 90 | + role: '得戒和尚', | ||
| 91 | + temple: '大觉寺', | ||
| 92 | + ordinationYear: 1985, | ||
| 93 | + experience: 39, | ||
| 94 | + avatar: null, | ||
| 95 | + type: 'teacher' | ||
| 96 | + }, | ||
| 97 | + { | ||
| 98 | + id: 2, | ||
| 99 | + name: '智慧法师', | ||
| 100 | + title: '首座', | ||
| 101 | + role: '羯磨阿阇梨', | ||
| 102 | + temple: '大觉寺', | ||
| 103 | + ordinationYear: 1990, | ||
| 104 | + experience: 34, | ||
| 105 | + avatar: null, | ||
| 106 | + type: 'teacher' | ||
| 107 | + }, | ||
| 108 | + { | ||
| 109 | + id: 3, | ||
| 110 | + name: '觉悟法师', | ||
| 111 | + title: '监院', | ||
| 112 | + role: '教授阿阇梨', | ||
| 113 | + temple: '大觉寺', | ||
| 114 | + ordinationYear: 1992, | ||
| 115 | + experience: 32, | ||
| 116 | + avatar: null, | ||
| 117 | + type: 'teacher' | ||
| 118 | + }, | ||
| 119 | + { | ||
| 120 | + id: 4, | ||
| 121 | + name: '慈悲法师', | ||
| 122 | + title: '知客', | ||
| 123 | + role: '证明师', | ||
| 124 | + temple: '大觉寺', | ||
| 125 | + ordinationYear: 1995, | ||
| 126 | + experience: 29, | ||
| 127 | + avatar: null, | ||
| 128 | + type: 'witness' | ||
| 129 | + }, | ||
| 130 | + { | ||
| 131 | + id: 5, | ||
| 132 | + name: '般若法师', | ||
| 133 | + title: '维那', | ||
| 134 | + role: '证明师', | ||
| 135 | + temple: '大觉寺', | ||
| 136 | + ordinationYear: 1998, | ||
| 137 | + experience: 26, | ||
| 138 | + avatar: null, | ||
| 139 | + type: 'witness' | ||
| 140 | + }, | ||
| 141 | + { | ||
| 142 | + id: 6, | ||
| 143 | + name: '禅定法师', | ||
| 144 | + title: '典座', | ||
| 145 | + role: '证明师', | ||
| 146 | + temple: '大觉寺', | ||
| 147 | + ordinationYear: 2000, | ||
| 148 | + experience: 24, | ||
| 149 | + avatar: null, | ||
| 150 | + type: 'witness' | ||
| 151 | + }, | ||
| 152 | + { | ||
| 153 | + id: 7, | ||
| 154 | + name: '精进法师', | ||
| 155 | + title: '书记', | ||
| 156 | + role: '证明师', | ||
| 157 | + temple: '大觉寺', | ||
| 158 | + ordinationYear: 2002, | ||
| 159 | + experience: 22, | ||
| 160 | + avatar: null, | ||
| 161 | + type: 'witness' | ||
| 162 | + }, | ||
| 163 | + { | ||
| 164 | + id: 8, | ||
| 165 | + name: '持戒法师', | ||
| 166 | + title: '库头', | ||
| 167 | + role: '证明师', | ||
| 168 | + temple: '大觉寺', | ||
| 169 | + ordinationYear: 2005, | ||
| 170 | + experience: 19, | ||
| 171 | + avatar: null, | ||
| 172 | + type: 'witness' | ||
| 173 | + }, | ||
| 174 | + { | ||
| 175 | + id: 9, | ||
| 176 | + name: '忍辱法师', | ||
| 177 | + title: '僧值', | ||
| 178 | + role: '证明师', | ||
| 179 | + temple: '大觉寺', | ||
| 180 | + ordinationYear: 2008, | ||
| 181 | + experience: 16, | ||
| 182 | + avatar: null, | ||
| 183 | + type: 'witness' | ||
| 184 | + }, | ||
| 185 | + { | ||
| 186 | + id: 10, | ||
| 187 | + name: '布施法师', | ||
| 188 | + title: '衣钵', | ||
| 189 | + role: '证明师', | ||
| 190 | + temple: '大觉寺', | ||
| 191 | + ordinationYear: 2010, | ||
| 192 | + experience: 14, | ||
| 193 | + avatar: null, | ||
| 194 | + type: 'witness' | ||
| 195 | + } | ||
| 196 | +]) | ||
| 197 | + | ||
| 198 | +// 过滤后的法师列表 | ||
| 199 | +const filteredTeachers = computed(() => { | ||
| 200 | + if (activeTab.value === 'all') { | ||
| 201 | + return teachers.value | ||
| 202 | + } else if (activeTab.value === 'teachers') { | ||
| 203 | + return teachers.value.filter(teacher => teacher.type === 'teacher') | ||
| 204 | + } else if (activeTab.value === 'witnesses') { | ||
| 205 | + return teachers.value.filter(teacher => teacher.type === 'witness') | ||
| 206 | + } | ||
| 207 | + return teachers.value | ||
| 208 | +}) | ||
| 209 | + | ||
| 210 | +// 获取角色样式类 | ||
| 211 | +const getRoleClass = (role) => { | ||
| 212 | + if (role.includes('和尚') || role.includes('阿阇梨')) { | ||
| 213 | + return 'role-teacher' | ||
| 214 | + } | ||
| 215 | + return 'role-witness' | ||
| 216 | +} | ||
| 217 | + | ||
| 218 | +// 处理标签切换 | ||
| 219 | +const handleTabChange = (name) => { | ||
| 220 | + activeTab.value = name | ||
| 221 | +} | ||
| 222 | + | ||
| 223 | +// 处理法师点击 | ||
| 224 | +const handleTeacherClick = (teacher) => { | ||
| 225 | + router.push(`/teachers/${teacher.id}`) | ||
| 226 | +} | ||
| 227 | +</script> | ||
| 228 | + | ||
| 229 | +<style scoped> | ||
| 230 | +.page-container { | ||
| 231 | + min-height: 100vh; | ||
| 232 | + background: #fafafa; | ||
| 233 | +} | ||
| 234 | + | ||
| 235 | +.custom-nav { | ||
| 236 | + background: linear-gradient(135deg, #fbbf24, #f97316); | ||
| 237 | + color: white; | ||
| 238 | +} | ||
| 239 | + | ||
| 240 | +.custom-nav :deep(.van-nav-bar__title) { | ||
| 241 | + color: white; | ||
| 242 | + font-weight: 600; | ||
| 243 | +} | ||
| 244 | + | ||
| 245 | +.custom-nav :deep(.van-icon) { | ||
| 246 | + color: white; | ||
| 247 | +} | ||
| 248 | + | ||
| 249 | +.content-container { | ||
| 250 | + padding-top: 46px; | ||
| 251 | +} | ||
| 252 | + | ||
| 253 | +.intro-section { | ||
| 254 | + padding: 16px; | ||
| 255 | +} | ||
| 256 | + | ||
| 257 | +.intro-card { | ||
| 258 | + background: white; | ||
| 259 | + border-radius: 12px; | ||
| 260 | + padding: 20px; | ||
| 261 | + display: flex; | ||
| 262 | + align-items: center; | ||
| 263 | + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); | ||
| 264 | +} | ||
| 265 | + | ||
| 266 | +.intro-icon { | ||
| 267 | + font-size: 32px; | ||
| 268 | + margin-right: 16px; | ||
| 269 | +} | ||
| 270 | + | ||
| 271 | +.intro-content h3 { | ||
| 272 | + font-size: 18px; | ||
| 273 | + font-weight: 600; | ||
| 274 | + color: #333; | ||
| 275 | + margin: 0 0 8px 0; | ||
| 276 | +} | ||
| 277 | + | ||
| 278 | +.intro-content p { | ||
| 279 | + font-size: 14px; | ||
| 280 | + color: #666; | ||
| 281 | + margin: 0; | ||
| 282 | + line-height: 1.5; | ||
| 283 | +} | ||
| 284 | + | ||
| 285 | +.filter-section { | ||
| 286 | + background: white; | ||
| 287 | + border-bottom: 1px solid #eee; | ||
| 288 | +} | ||
| 289 | + | ||
| 290 | +.custom-tabs :deep(.van-tab) { | ||
| 291 | + font-weight: 500; | ||
| 292 | +} | ||
| 293 | + | ||
| 294 | +.custom-tabs :deep(.van-tab--active) { | ||
| 295 | + color: #f59e0b; | ||
| 296 | +} | ||
| 297 | + | ||
| 298 | +.custom-tabs :deep(.van-tabs__line) { | ||
| 299 | + background: #f59e0b; | ||
| 300 | +} | ||
| 301 | + | ||
| 302 | +.teachers-list { | ||
| 303 | + padding: 16px; | ||
| 304 | +} | ||
| 305 | + | ||
| 306 | +.teacher-card { | ||
| 307 | + background: white; | ||
| 308 | + border-radius: 12px; | ||
| 309 | + padding: 16px; | ||
| 310 | + margin-bottom: 12px; | ||
| 311 | + display: flex; | ||
| 312 | + align-items: center; | ||
| 313 | + gap: 16px; | ||
| 314 | + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); | ||
| 315 | + cursor: pointer; | ||
| 316 | + transition: all 0.3s ease; | ||
| 317 | +} | ||
| 318 | + | ||
| 319 | +.teacher-card:hover { | ||
| 320 | + transform: translateY(-3px); | ||
| 321 | + box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15); | ||
| 322 | +} | ||
| 323 | + | ||
| 324 | +.teacher-card:active { | ||
| 325 | + transform: translateY(-1px); | ||
| 326 | + box-shadow: 0 3px 12px rgba(0, 0, 0, 0.12); | ||
| 327 | +} | ||
| 328 | + | ||
| 329 | +.teacher-avatar { | ||
| 330 | + width: 60px; | ||
| 331 | + height: 60px; | ||
| 332 | + border-radius: 50%; | ||
| 333 | + overflow: hidden; | ||
| 334 | + margin-right: 16px; | ||
| 335 | + flex-shrink: 0; | ||
| 336 | +} | ||
| 337 | + | ||
| 338 | +.teacher-avatar img { | ||
| 339 | + width: 100%; | ||
| 340 | + height: 100%; | ||
| 341 | + object-fit: cover; | ||
| 342 | +} | ||
| 343 | + | ||
| 344 | +.avatar-placeholder { | ||
| 345 | + width: 100%; | ||
| 346 | + height: 100%; | ||
| 347 | + background: linear-gradient(135deg, #fbbf24, #f97316); | ||
| 348 | + display: flex; | ||
| 349 | + align-items: center; | ||
| 350 | + justify-content: center; | ||
| 351 | + color: white; | ||
| 352 | + font-size: 24px; | ||
| 353 | + font-weight: 600; | ||
| 354 | +} | ||
| 355 | + | ||
| 356 | +.teacher-info { | ||
| 357 | + flex: 1; | ||
| 358 | +} | ||
| 359 | + | ||
| 360 | +.teacher-header { | ||
| 361 | + display: flex; | ||
| 362 | + align-items: center; | ||
| 363 | + justify-content: space-between; | ||
| 364 | + margin-bottom: 8px; | ||
| 365 | +} | ||
| 366 | + | ||
| 367 | +.teacher-name { | ||
| 368 | + font-size: 18px; | ||
| 369 | + font-weight: 600; | ||
| 370 | + color: #333; | ||
| 371 | + margin: 0; | ||
| 372 | +} | ||
| 373 | + | ||
| 374 | +.teacher-role { | ||
| 375 | + padding: 4px 8px; | ||
| 376 | + border-radius: 12px; | ||
| 377 | + font-size: 12px; | ||
| 378 | + font-weight: 500; | ||
| 379 | +} | ||
| 380 | + | ||
| 381 | +.role-teacher { | ||
| 382 | + background: linear-gradient(135deg, #fbbf24, #f97316); | ||
| 383 | + color: white; | ||
| 384 | +} | ||
| 385 | + | ||
| 386 | +.role-witness { | ||
| 387 | + background: #f0f9ff; | ||
| 388 | + color: #0369a1; | ||
| 389 | + border: 1px solid #bae6fd; | ||
| 390 | +} | ||
| 391 | + | ||
| 392 | +.teacher-details { | ||
| 393 | + space-y: 4px; | ||
| 394 | +} | ||
| 395 | + | ||
| 396 | +.teacher-title { | ||
| 397 | + font-size: 16px; | ||
| 398 | + color: #666; | ||
| 399 | + margin: 0 0 4px 0; | ||
| 400 | +} | ||
| 401 | + | ||
| 402 | +.teacher-temple { | ||
| 403 | + font-size: 14px; | ||
| 404 | + color: #999; | ||
| 405 | + margin: 0 0 8px 0; | ||
| 406 | +} | ||
| 407 | + | ||
| 408 | +.teacher-meta { | ||
| 409 | + display: flex; | ||
| 410 | + gap: 16px; | ||
| 411 | +} | ||
| 412 | + | ||
| 413 | +.ordination-year, | ||
| 414 | +.experience { | ||
| 415 | + font-size: 12px; | ||
| 416 | + color: #999; | ||
| 417 | + background: #f5f5f5; | ||
| 418 | + padding: 2px 6px; | ||
| 419 | + border-radius: 4px; | ||
| 420 | +} | ||
| 421 | + | ||
| 422 | +.teacher-actions { | ||
| 423 | + margin-left: 12px; | ||
| 424 | + color: #ccc; | ||
| 425 | +} | ||
| 426 | +</style> | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
src/views/Volunteers.vue
0 → 100644
| 1 | +<template> | ||
| 2 | + <div class="page-container"> | ||
| 3 | + <!-- 导航栏 --> | ||
| 4 | + <van-nav-bar title="义工服务" left-arrow @click-left="$router.back()" class="custom-nav"> | ||
| 5 | + <template #right> | ||
| 6 | + <van-icon name="plus" size="18" @click="handleAddVolunteer" /> | ||
| 7 | + </template> | ||
| 8 | + </van-nav-bar> | ||
| 9 | + | ||
| 10 | + <!-- 内容区域 --> | ||
| 11 | + <div class="content-container"> | ||
| 12 | + <!-- 顶部统计 --> | ||
| 13 | + <div class="stats-section"> | ||
| 14 | + <div class="stat-card"> | ||
| 15 | + <div class="stat-icon">👥</div> | ||
| 16 | + <div class="stat-info"> | ||
| 17 | + <div class="stat-number">{{ totalVolunteers }}</div> | ||
| 18 | + <div class="stat-label">总义工数</div> | ||
| 19 | + </div> | ||
| 20 | + </div> | ||
| 21 | + <div class="stat-card"> | ||
| 22 | + <div class="stat-icon">✅</div> | ||
| 23 | + <div class="stat-info"> | ||
| 24 | + <div class="stat-number">{{ activeVolunteers }}</div> | ||
| 25 | + <div class="stat-label">在岗义工</div> | ||
| 26 | + </div> | ||
| 27 | + </div> | ||
| 28 | + <div class="stat-card"> | ||
| 29 | + <div class="stat-icon">📅</div> | ||
| 30 | + <div class="stat-info"> | ||
| 31 | + <div class="stat-number">{{ todayTasks }}</div> | ||
| 32 | + <div class="stat-label">今日任务</div> | ||
| 33 | + </div> | ||
| 34 | + </div> | ||
| 35 | + </div> | ||
| 36 | + | ||
| 37 | + <!-- 筛选栏 --> | ||
| 38 | + <div class="filter-section"> | ||
| 39 | + <van-tabs v-model:active="activeTab" @change="handleTabChange" class="custom-tabs"> | ||
| 40 | + <van-tab title="全部" name="all"></van-tab> | ||
| 41 | + <van-tab title="在岗" name="active"></van-tab> | ||
| 42 | + <van-tab title="休息" name="rest"></van-tab> | ||
| 43 | + <van-tab title="请假" name="leave"></van-tab> | ||
| 44 | + </van-tabs> | ||
| 45 | + </div> | ||
| 46 | + | ||
| 47 | + <!-- 义工列表 --> | ||
| 48 | + <div class="volunteers-list"> | ||
| 49 | + <div | ||
| 50 | + v-for="volunteer in filteredVolunteers" | ||
| 51 | + :key="volunteer.id" | ||
| 52 | + class="volunteer-card" | ||
| 53 | + @click="handleVolunteerClick(volunteer)" | ||
| 54 | + > | ||
| 55 | + <div class="volunteer-avatar"> | ||
| 56 | + <img v-if="volunteer.avatar" :src="volunteer.avatar" :alt="volunteer.name" /> | ||
| 57 | + <div v-else class="avatar-placeholder"> | ||
| 58 | + <span>{{ volunteer.name.charAt(0) }}</span> | ||
| 59 | + </div> | ||
| 60 | + <div class="status-badge" :class="getStatusClass(volunteer.status)"> | ||
| 61 | + {{ getStatusText(volunteer.status) }} | ||
| 62 | + </div> | ||
| 63 | + </div> | ||
| 64 | + | ||
| 65 | + <div class="volunteer-info"> | ||
| 66 | + <div class="volunteer-header"> | ||
| 67 | + <h4 class="volunteer-name">{{ volunteer.name }}</h4> | ||
| 68 | + <div class="volunteer-level" :class="getLevelClass(volunteer.level)"> | ||
| 69 | + {{ volunteer.level }} | ||
| 70 | + </div> | ||
| 71 | + </div> | ||
| 72 | + | ||
| 73 | + <div class="volunteer-details"> | ||
| 74 | + <p class="volunteer-department">{{ volunteer.department }}</p> | ||
| 75 | + <p class="volunteer-task">当前任务:{{ volunteer.currentTask || '暂无' }}</p> | ||
| 76 | + <div class="volunteer-meta"> | ||
| 77 | + <span class="join-date">{{ volunteer.joinDate }}加入</span> | ||
| 78 | + <span class="service-hours">{{ volunteer.serviceHours }}小时</span> | ||
| 79 | + </div> | ||
| 80 | + </div> | ||
| 81 | + </div> | ||
| 82 | + | ||
| 83 | + <div class="volunteer-actions"> | ||
| 84 | + <van-icon name="arrow" /> | ||
| 85 | + </div> | ||
| 86 | + </div> | ||
| 87 | + </div> | ||
| 88 | + | ||
| 89 | + <!-- 空状态 --> | ||
| 90 | + <van-empty v-if="filteredVolunteers.length === 0" description="暂无义工信息" /> | ||
| 91 | + </div> | ||
| 92 | + | ||
| 93 | + <!-- 浮动按钮 --> | ||
| 94 | + <van-floating-bubble | ||
| 95 | + axis="xy" | ||
| 96 | + icon="plus" | ||
| 97 | + @click="handleAddVolunteer" | ||
| 98 | + class="add-button" | ||
| 99 | + /> | ||
| 100 | + </div> | ||
| 101 | +</template> | ||
| 102 | + | ||
| 103 | +<script setup> | ||
| 104 | +import { ref, computed } from 'vue' | ||
| 105 | +import { useRouter } from 'vue-router' | ||
| 106 | +import { Toast } from 'vant' | ||
| 107 | + | ||
| 108 | +const router = useRouter() | ||
| 109 | +const activeTab = ref('all') | ||
| 110 | + | ||
| 111 | +// 统计数据 | ||
| 112 | +const totalVolunteers = ref(45) | ||
| 113 | +const activeVolunteers = ref(32) | ||
| 114 | +const todayTasks = ref(18) | ||
| 115 | + | ||
| 116 | +// 义工数据 | ||
| 117 | +const volunteers = ref([ | ||
| 118 | + { | ||
| 119 | + id: 1, | ||
| 120 | + name: '张慧敏', | ||
| 121 | + department: '客堂组', | ||
| 122 | + level: '资深义工', | ||
| 123 | + status: 'active', | ||
| 124 | + currentTask: '接待来访信众', | ||
| 125 | + joinDate: '2022-03-15', | ||
| 126 | + serviceHours: 520, | ||
| 127 | + avatar: null | ||
| 128 | + }, | ||
| 129 | + { | ||
| 130 | + id: 2, | ||
| 131 | + name: '李明德', | ||
| 132 | + department: '维护组', | ||
| 133 | + level: '普通义工', | ||
| 134 | + status: 'active', | ||
| 135 | + currentTask: '大殿清洁维护', | ||
| 136 | + joinDate: '2023-01-20', | ||
| 137 | + serviceHours: 280, | ||
| 138 | + avatar: null | ||
| 139 | + }, | ||
| 140 | + { | ||
| 141 | + id: 3, | ||
| 142 | + name: '王慈悲', | ||
| 143 | + department: '斋堂组', | ||
| 144 | + level: '组长', | ||
| 145 | + status: 'active', | ||
| 146 | + currentTask: '午斋准备工作', | ||
| 147 | + joinDate: '2021-08-10', | ||
| 148 | + serviceHours: 750, | ||
| 149 | + avatar: null | ||
| 150 | + }, | ||
| 151 | + { | ||
| 152 | + id: 4, | ||
| 153 | + name: '陈智慧', | ||
| 154 | + department: '法务组', | ||
| 155 | + level: '资深义工', | ||
| 156 | + status: 'rest', | ||
| 157 | + currentTask: null, | ||
| 158 | + joinDate: '2022-06-05', | ||
| 159 | + serviceHours: 420, | ||
| 160 | + avatar: null | ||
| 161 | + }, | ||
| 162 | + { | ||
| 163 | + id: 5, | ||
| 164 | + name: '刘精进', | ||
| 165 | + department: '安保组', | ||
| 166 | + level: '普通义工', | ||
| 167 | + status: 'leave', | ||
| 168 | + currentTask: null, | ||
| 169 | + joinDate: '2023-04-12', | ||
| 170 | + serviceHours: 150, | ||
| 171 | + avatar: null | ||
| 172 | + }, | ||
| 173 | + { | ||
| 174 | + id: 6, | ||
| 175 | + name: '赵般若', | ||
| 176 | + department: '文宣组', | ||
| 177 | + level: '资深义工', | ||
| 178 | + status: 'active', | ||
| 179 | + currentTask: '活动摄影记录', | ||
| 180 | + joinDate: '2022-11-30', | ||
| 181 | + serviceHours: 380, | ||
| 182 | + avatar: null | ||
| 183 | + }, | ||
| 184 | + { | ||
| 185 | + id: 7, | ||
| 186 | + name: '孙持戒', | ||
| 187 | + department: '客堂组', | ||
| 188 | + level: '普通义工', | ||
| 189 | + status: 'active', | ||
| 190 | + currentTask: '登记来访信息', | ||
| 191 | + joinDate: '2023-07-08', | ||
| 192 | + serviceHours: 95, | ||
| 193 | + avatar: null | ||
| 194 | + }, | ||
| 195 | + { | ||
| 196 | + id: 8, | ||
| 197 | + name: '周忍辱', | ||
| 198 | + department: '斋堂组', | ||
| 199 | + level: '普通义工', | ||
| 200 | + status: 'rest', | ||
| 201 | + currentTask: null, | ||
| 202 | + joinDate: '2023-02-14', | ||
| 203 | + serviceHours: 220, | ||
| 204 | + avatar: null | ||
| 205 | + } | ||
| 206 | +]) | ||
| 207 | + | ||
| 208 | +// 过滤后的义工列表 | ||
| 209 | +const filteredVolunteers = computed(() => { | ||
| 210 | + if (activeTab.value === 'all') { | ||
| 211 | + return volunteers.value | ||
| 212 | + } | ||
| 213 | + return volunteers.value.filter(volunteer => volunteer.status === activeTab.value) | ||
| 214 | +}) | ||
| 215 | + | ||
| 216 | +// 获取状态样式类 | ||
| 217 | +const getStatusClass = (status) => { | ||
| 218 | + const classes = { | ||
| 219 | + active: 'status-active', | ||
| 220 | + rest: 'status-rest', | ||
| 221 | + leave: 'status-leave' | ||
| 222 | + } | ||
| 223 | + return classes[status] || 'status-rest' | ||
| 224 | +} | ||
| 225 | + | ||
| 226 | +// 获取状态文本 | ||
| 227 | +const getStatusText = (status) => { | ||
| 228 | + const texts = { | ||
| 229 | + active: '在岗', | ||
| 230 | + rest: '休息', | ||
| 231 | + leave: '请假' | ||
| 232 | + } | ||
| 233 | + return texts[status] || '休息' | ||
| 234 | +} | ||
| 235 | + | ||
| 236 | +// 获取等级样式类 | ||
| 237 | +const getLevelClass = (level) => { | ||
| 238 | + if (level === '组长') return 'level-leader' | ||
| 239 | + if (level === '资深义工') return 'level-senior' | ||
| 240 | + return 'level-normal' | ||
| 241 | +} | ||
| 242 | + | ||
| 243 | +// 处理标签切换 | ||
| 244 | +const handleTabChange = (name) => { | ||
| 245 | + activeTab.value = name | ||
| 246 | +} | ||
| 247 | + | ||
| 248 | +// 处理义工点击 | ||
| 249 | +const handleVolunteerClick = (volunteer) => { | ||
| 250 | + router.push(`/volunteers/${volunteer.id}`) | ||
| 251 | +} | ||
| 252 | + | ||
| 253 | +// 处理添加义工 | ||
| 254 | +const handleAddVolunteer = () => { | ||
| 255 | + Toast('添加义工功能开发中...') | ||
| 256 | +} | ||
| 257 | +</script> | ||
| 258 | + | ||
| 259 | +<style scoped> | ||
| 260 | +.page-container { | ||
| 261 | + min-height: 100vh; | ||
| 262 | + background: #fafafa; | ||
| 263 | +} | ||
| 264 | + | ||
| 265 | +.custom-nav { | ||
| 266 | + background: linear-gradient(135deg, #fbbf24, #f97316); | ||
| 267 | + color: white; | ||
| 268 | +} | ||
| 269 | + | ||
| 270 | +.custom-nav :deep(.van-nav-bar__title) { | ||
| 271 | + color: white; | ||
| 272 | + font-weight: 600; | ||
| 273 | +} | ||
| 274 | + | ||
| 275 | +.custom-nav :deep(.van-icon) { | ||
| 276 | + color: white; | ||
| 277 | +} | ||
| 278 | + | ||
| 279 | +.content-container { | ||
| 280 | + padding-top: 46px; | ||
| 281 | +} | ||
| 282 | + | ||
| 283 | +.stats-section { | ||
| 284 | + display: flex; | ||
| 285 | + gap: 12px; | ||
| 286 | + padding: 16px; | ||
| 287 | +} | ||
| 288 | + | ||
| 289 | +.stat-card { | ||
| 290 | + flex: 1; | ||
| 291 | + background: white; | ||
| 292 | + border-radius: 12px; | ||
| 293 | + padding: 16px; | ||
| 294 | + display: flex; | ||
| 295 | + align-items: center; | ||
| 296 | + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); | ||
| 297 | +} | ||
| 298 | + | ||
| 299 | +.stat-icon { | ||
| 300 | + font-size: 24px; | ||
| 301 | + margin-right: 12px; | ||
| 302 | +} | ||
| 303 | + | ||
| 304 | +.stat-info { | ||
| 305 | + flex: 1; | ||
| 306 | +} | ||
| 307 | + | ||
| 308 | +.stat-number { | ||
| 309 | + font-size: 20px; | ||
| 310 | + font-weight: 700; | ||
| 311 | + color: #f59e0b; | ||
| 312 | + margin-bottom: 2px; | ||
| 313 | +} | ||
| 314 | + | ||
| 315 | +.stat-label { | ||
| 316 | + font-size: 12px; | ||
| 317 | + color: #666; | ||
| 318 | +} | ||
| 319 | + | ||
| 320 | +.filter-section { | ||
| 321 | + background: white; | ||
| 322 | + border-bottom: 1px solid #eee; | ||
| 323 | +} | ||
| 324 | + | ||
| 325 | +.custom-tabs :deep(.van-tab) { | ||
| 326 | + font-weight: 500; | ||
| 327 | +} | ||
| 328 | + | ||
| 329 | +.custom-tabs :deep(.van-tab--active) { | ||
| 330 | + color: #f59e0b; | ||
| 331 | +} | ||
| 332 | + | ||
| 333 | +.custom-tabs :deep(.van-tabs__line) { | ||
| 334 | + background: #f59e0b; | ||
| 335 | +} | ||
| 336 | + | ||
| 337 | +.volunteers-list { | ||
| 338 | + padding: 16px; | ||
| 339 | +} | ||
| 340 | + | ||
| 341 | +.volunteer-card { | ||
| 342 | + background: white; | ||
| 343 | + border-radius: 12px; | ||
| 344 | + padding: 16px; | ||
| 345 | + margin-bottom: 12px; | ||
| 346 | + display: flex; | ||
| 347 | + align-items: center; | ||
| 348 | + gap: 16px; | ||
| 349 | + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); | ||
| 350 | + cursor: pointer; | ||
| 351 | + transition: all 0.3s ease; | ||
| 352 | +} | ||
| 353 | + | ||
| 354 | +.volunteer-card:hover { | ||
| 355 | + transform: translateY(-3px); | ||
| 356 | + box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15); | ||
| 357 | +} | ||
| 358 | + | ||
| 359 | +.volunteer-card:active { | ||
| 360 | + transform: translateY(-1px); | ||
| 361 | + box-shadow: 0 3px 12px rgba(0, 0, 0, 0.12); | ||
| 362 | +} | ||
| 363 | + | ||
| 364 | +.volunteer-avatar { | ||
| 365 | + position: relative; | ||
| 366 | + width: 60px; | ||
| 367 | + height: 60px; | ||
| 368 | + border-radius: 50%; | ||
| 369 | + overflow: hidden; | ||
| 370 | + margin-right: 16px; | ||
| 371 | + flex-shrink: 0; | ||
| 372 | +} | ||
| 373 | + | ||
| 374 | +.volunteer-avatar img { | ||
| 375 | + width: 100%; | ||
| 376 | + height: 100%; | ||
| 377 | + object-fit: cover; | ||
| 378 | +} | ||
| 379 | + | ||
| 380 | +.avatar-placeholder { | ||
| 381 | + width: 100%; | ||
| 382 | + height: 100%; | ||
| 383 | + background: linear-gradient(135deg, #fbbf24, #f97316); | ||
| 384 | + display: flex; | ||
| 385 | + align-items: center; | ||
| 386 | + justify-content: center; | ||
| 387 | + color: white; | ||
| 388 | + font-size: 24px; | ||
| 389 | + font-weight: 600; | ||
| 390 | +} | ||
| 391 | + | ||
| 392 | +.status-badge { | ||
| 393 | + position: absolute; | ||
| 394 | + bottom: -2px; | ||
| 395 | + right: -2px; | ||
| 396 | + padding: 2px 6px; | ||
| 397 | + border-radius: 8px; | ||
| 398 | + font-size: 10px; | ||
| 399 | + font-weight: 500; | ||
| 400 | + border: 2px solid white; | ||
| 401 | +} | ||
| 402 | + | ||
| 403 | +.status-active { | ||
| 404 | + background: #10b981; | ||
| 405 | + color: white; | ||
| 406 | +} | ||
| 407 | + | ||
| 408 | +.status-rest { | ||
| 409 | + background: #6b7280; | ||
| 410 | + color: white; | ||
| 411 | +} | ||
| 412 | + | ||
| 413 | +.status-leave { | ||
| 414 | + background: #ef4444; | ||
| 415 | + color: white; | ||
| 416 | +} | ||
| 417 | + | ||
| 418 | +.volunteer-info { | ||
| 419 | + flex: 1; | ||
| 420 | +} | ||
| 421 | + | ||
| 422 | +.volunteer-header { | ||
| 423 | + display: flex; | ||
| 424 | + align-items: center; | ||
| 425 | + justify-content: space-between; | ||
| 426 | + margin-bottom: 8px; | ||
| 427 | +} | ||
| 428 | + | ||
| 429 | +.volunteer-name { | ||
| 430 | + font-size: 18px; | ||
| 431 | + font-weight: 600; | ||
| 432 | + color: #333; | ||
| 433 | + margin: 0; | ||
| 434 | +} | ||
| 435 | + | ||
| 436 | +.volunteer-level { | ||
| 437 | + padding: 4px 8px; | ||
| 438 | + border-radius: 12px; | ||
| 439 | + font-size: 12px; | ||
| 440 | + font-weight: 500; | ||
| 441 | +} | ||
| 442 | + | ||
| 443 | +.level-leader { | ||
| 444 | + background: linear-gradient(135deg, #fbbf24, #f97316); | ||
| 445 | + color: white; | ||
| 446 | +} | ||
| 447 | + | ||
| 448 | +.level-senior { | ||
| 449 | + background: #dbeafe; | ||
| 450 | + color: #1d4ed8; | ||
| 451 | + border: 1px solid #93c5fd; | ||
| 452 | +} | ||
| 453 | + | ||
| 454 | +.level-normal { | ||
| 455 | + background: #f3f4f6; | ||
| 456 | + color: #6b7280; | ||
| 457 | + border: 1px solid #d1d5db; | ||
| 458 | +} | ||
| 459 | + | ||
| 460 | +.volunteer-details { | ||
| 461 | + space-y: 4px; | ||
| 462 | +} | ||
| 463 | + | ||
| 464 | +.volunteer-department { | ||
| 465 | + font-size: 16px; | ||
| 466 | + color: #666; | ||
| 467 | + margin: 0 0 4px 0; | ||
| 468 | +} | ||
| 469 | + | ||
| 470 | +.volunteer-task { | ||
| 471 | + font-size: 14px; | ||
| 472 | + color: #999; | ||
| 473 | + margin: 0 0 8px 0; | ||
| 474 | +} | ||
| 475 | + | ||
| 476 | +.volunteer-meta { | ||
| 477 | + display: flex; | ||
| 478 | + gap: 16px; | ||
| 479 | +} | ||
| 480 | + | ||
| 481 | +.join-date, | ||
| 482 | +.service-hours { | ||
| 483 | + font-size: 12px; | ||
| 484 | + color: #999; | ||
| 485 | + background: #f5f5f5; | ||
| 486 | + padding: 2px 6px; | ||
| 487 | + border-radius: 4px; | ||
| 488 | +} | ||
| 489 | + | ||
| 490 | +.volunteer-actions { | ||
| 491 | + margin-left: 12px; | ||
| 492 | + color: #ccc; | ||
| 493 | +} | ||
| 494 | + | ||
| 495 | +.add-button { | ||
| 496 | + background: linear-gradient(135deg, #fbbf24, #f97316) !important; | ||
| 497 | + transition: all 0.3s ease; | ||
| 498 | + animation: float 3s ease-in-out infinite; | ||
| 499 | +} | ||
| 500 | + | ||
| 501 | +.add-button:hover { | ||
| 502 | + transform: translateY(-3px) scale(1.1); | ||
| 503 | + box-shadow: 0 8px 25px rgba(251, 191, 36, 0.6); | ||
| 504 | +} | ||
| 505 | + | ||
| 506 | +.add-button:active { | ||
| 507 | + transform: translateY(-1px) scale(1.05); | ||
| 508 | + box-shadow: 0 4px 16px rgba(251, 191, 36, 0.4); | ||
| 509 | +} | ||
| 510 | + | ||
| 511 | +.add-button :deep(.van-floating-bubble__icon) { | ||
| 512 | + color: white; | ||
| 513 | +} | ||
| 514 | + | ||
| 515 | +@keyframes float { | ||
| 516 | + 0%, 100% { | ||
| 517 | + transform: translateY(0px); | ||
| 518 | + } | ||
| 519 | + 50% { | ||
| 520 | + transform: translateY(-10px); | ||
| 521 | + } | ||
| 522 | +} | ||
| 523 | +</style> | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
tailwind.config.js
0 → 100644
| 1 | +/** @type {import('tailwindcss').Config} */ | ||
| 2 | +export default { | ||
| 3 | + content: [ | ||
| 4 | + "./index.html", | ||
| 5 | + "./src/**/*.{vue,js,ts,jsx,tsx}", | ||
| 6 | + ], | ||
| 7 | + theme: { | ||
| 8 | + extend: { | ||
| 9 | + colors: { | ||
| 10 | + primary: '#1989fa', | ||
| 11 | + success: '#07c160', | ||
| 12 | + warning: '#ff976a', | ||
| 13 | + danger: '#ee0a24', | ||
| 14 | + }, | ||
| 15 | + fontSize: { | ||
| 16 | + 'xs': '10px', | ||
| 17 | + 'sm': '12px', | ||
| 18 | + 'base': '14px', | ||
| 19 | + 'lg': '16px', | ||
| 20 | + 'xl': '18px', | ||
| 21 | + '2xl': '20px', | ||
| 22 | + '3xl': '24px', | ||
| 23 | + } | ||
| 24 | + }, | ||
| 25 | + }, | ||
| 26 | + plugins: [], | ||
| 27 | + corePlugins: { | ||
| 28 | + preflight: false, // 禁用 Tailwind 的基础样式重置,避免与 Vant 冲突 | ||
| 29 | + } | ||
| 30 | +} | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
vite.config.js
0 → 100644
| 1 | +import { defineConfig } from 'vite' | ||
| 2 | +import vue from '@vitejs/plugin-vue' | ||
| 3 | +import AutoImport from 'unplugin-auto-import/vite' | ||
| 4 | +import Components from 'unplugin-vue-components/vite' | ||
| 5 | +import { VantResolver } from 'unplugin-vue-components/resolvers' | ||
| 6 | +import path from 'path' | ||
| 7 | +import tailwindcss from 'tailwindcss' | ||
| 8 | +import autoprefixer from 'autoprefixer' | ||
| 9 | +import postcsspxtoviewport from 'postcss-px-to-viewport' | ||
| 10 | + | ||
| 11 | +// https://vitejs.dev/config/ | ||
| 12 | +export default defineConfig({ | ||
| 13 | + plugins: [ | ||
| 14 | + vue(), | ||
| 15 | + AutoImport({ | ||
| 16 | + imports: ['vue', 'vue-router'], | ||
| 17 | + dts: 'src/auto-imports.d.ts', | ||
| 18 | + eslintrc: { | ||
| 19 | + enabled: true | ||
| 20 | + } | ||
| 21 | + }), | ||
| 22 | + Components({ | ||
| 23 | + resolvers: [VantResolver()], | ||
| 24 | + dts: 'src/components.d.ts' | ||
| 25 | + }) | ||
| 26 | + ], | ||
| 27 | + resolve: { | ||
| 28 | + alias: { | ||
| 29 | + '@': path.resolve(__dirname, 'src'), | ||
| 30 | + '@components': path.resolve(__dirname, 'src/components'), | ||
| 31 | + '@utils': path.resolve(__dirname, 'src/utils'), | ||
| 32 | + '@api': path.resolve(__dirname, 'src/api'), | ||
| 33 | + '@assets': path.resolve(__dirname, 'src/assets'), | ||
| 34 | + '@views': path.resolve(__dirname, 'src/views'), | ||
| 35 | + '@stores': path.resolve(__dirname, 'src/stores') | ||
| 36 | + } | ||
| 37 | + }, | ||
| 38 | + css: { | ||
| 39 | + postcss: { | ||
| 40 | + plugins: [ | ||
| 41 | + tailwindcss, | ||
| 42 | + autoprefixer, | ||
| 43 | + postcsspxtoviewport({ | ||
| 44 | + unitToConvert: 'px', | ||
| 45 | + viewportWidth: 375, | ||
| 46 | + unitPrecision: 6, | ||
| 47 | + propList: ['*'], | ||
| 48 | + viewportUnit: 'vw', | ||
| 49 | + fontViewportUnit: 'vw', | ||
| 50 | + selectorBlackList: ['ignore-'], | ||
| 51 | + minPixelValue: 1, | ||
| 52 | + mediaQuery: true, | ||
| 53 | + replace: true, | ||
| 54 | + exclude: [], | ||
| 55 | + landscape: false | ||
| 56 | + }) | ||
| 57 | + ] | ||
| 58 | + } | ||
| 59 | + }, | ||
| 60 | + server: { | ||
| 61 | + host: '0.0.0.0', | ||
| 62 | + port: 3000, | ||
| 63 | + open: true, | ||
| 64 | + proxy: { | ||
| 65 | + '/api': { | ||
| 66 | + target: 'http://localhost:8080', | ||
| 67 | + changeOrigin: true, | ||
| 68 | + rewrite: (path) => path.replace(/^\/api/, '') | ||
| 69 | + } | ||
| 70 | + } | ||
| 71 | + }, | ||
| 72 | + build: { | ||
| 73 | + outDir: 'dist', | ||
| 74 | + assetsDir: 'static', | ||
| 75 | + rollupOptions: { | ||
| 76 | + output: { | ||
| 77 | + chunkFileNames: 'static/js/[name]-[hash].js', | ||
| 78 | + entryFileNames: 'static/js/[name]-[hash].js', | ||
| 79 | + assetFileNames: 'static/[ext]/[name]-[hash].[ext]' | ||
| 80 | + } | ||
| 81 | + } | ||
| 82 | + } | ||
| 83 | +}) | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
三坛大戒资料/设计稿/01启动页.png
0 → 100644
1.37 MB
三坛大戒资料/设计稿/02首页 .png
0 → 100644
3.08 MB
三坛大戒资料/设计稿/03三師七證.png
0 → 100644
3.72 MB
三坛大戒资料/设计稿/04三師七證– 详情.png
0 → 100644
1.38 MB
三坛大戒资料/设计稿/05义工.png
0 → 100644
1.98 MB
三坛大戒资料/设计稿/06戒子.png
0 → 100644
391 KB
三坛大戒资料/设计稿/07戒子 –详情.png
0 → 100644
858 KB
三坛大戒资料/设计稿/08新闻详情.png
0 → 100644
1.52 MB
-
Please register or login to post a comment