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
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
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
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
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
This diff is collapsed. Click to expand it.
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