hookehuyr

feat: 初始化三坛大戒H5应用项目

- 完成启动页面设计和动画效果
- 实现首页布局和功能模块
- 添加三师七证、义工、戒子等核心页面
- 集成Vue3 + Vite + Vant UI框架
- 实现页面路由和导航功能
- 添加响应式设计和交互动画
1 +# 开发环境配置
2 +
3 +# API 基础地址
4 +VITE_API_BASE_URL=http://localhost:3000/api
5 +
6 +# 是否开启 Mock
7 +VITE_USE_MOCK=true
8 +
9 +# 开发服务器端口
10 +VITE_PORT=5173
11 +
12 +# 是否自动打开浏览器
13 +VITE_OPEN=true
...\ No newline at end of file ...\ No newline at end of file
1 +# 生产环境配置
2 +
3 +# API 基础地址
4 +VITE_API_BASE_URL=https://api.yourdomain.com
5 +
6 +# 是否开启 Mock
7 +VITE_USE_MOCK=false
8 +
9 +# 构建输出目录
10 +VITE_OUTPUT_DIR=dist
11 +
12 +# 静态资源目录
13 +VITE_ASSETS_DIR=assets
...\ No newline at end of file ...\ No newline at end of file
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 +}
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
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
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
This diff could not be displayed because it is too large.
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 +}
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
1 +<template>
2 + <div id="app">
3 + <router-view />
4 + </div>
5 +</template>
6 +
7 +<script setup>
8 +// 这里可以添加全局逻辑
9 +</script>
10 +
11 +<style>
12 +/* 全局样式已在 style.css 中定义 */
13 +</style>
...\ No newline at end of file ...\ No newline at end of file
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
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 +}
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 +}
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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