feat: 初始化三坛大戒H5应用项目
- 完成启动页面设计和动画效果 - 实现首页布局和功能模块 - 添加三师七证、义工、戒子等核心页面 - 集成Vue3 + Vite + Vant UI框架 - 实现页面路由和导航功能 - 添加响应式设计和交互动画
Showing
39 changed files
with
2139 additions
and
0 deletions
.env.development
0 → 100644
.env.production
0 → 100644
.eslintrc-auto-import.json
0 → 100644
| 1 | +{ | ||
| 2 | + "globals": { | ||
| 3 | + "Component": true, | ||
| 4 | + "ComponentPublicInstance": true, | ||
| 5 | + "ComputedRef": true, | ||
| 6 | + "DirectiveBinding": true, | ||
| 7 | + "EffectScope": true, | ||
| 8 | + "ExtractDefaultPropTypes": true, | ||
| 9 | + "ExtractPropTypes": true, | ||
| 10 | + "ExtractPublicPropTypes": true, | ||
| 11 | + "InjectionKey": true, | ||
| 12 | + "MaybeRef": true, | ||
| 13 | + "MaybeRefOrGetter": true, | ||
| 14 | + "PropType": true, | ||
| 15 | + "Ref": true, | ||
| 16 | + "VNode": true, | ||
| 17 | + "WritableComputedRef": true, | ||
| 18 | + "computed": true, | ||
| 19 | + "createApp": true, | ||
| 20 | + "customRef": true, | ||
| 21 | + "defineAsyncComponent": true, | ||
| 22 | + "defineComponent": true, | ||
| 23 | + "effectScope": true, | ||
| 24 | + "getCurrentInstance": true, | ||
| 25 | + "getCurrentScope": true, | ||
| 26 | + "h": true, | ||
| 27 | + "inject": true, | ||
| 28 | + "isProxy": true, | ||
| 29 | + "isReactive": true, | ||
| 30 | + "isReadonly": true, | ||
| 31 | + "isRef": true, | ||
| 32 | + "markRaw": true, | ||
| 33 | + "nextTick": true, | ||
| 34 | + "onActivated": true, | ||
| 35 | + "onBeforeMount": true, | ||
| 36 | + "onBeforeRouteLeave": true, | ||
| 37 | + "onBeforeRouteUpdate": true, | ||
| 38 | + "onBeforeUnmount": true, | ||
| 39 | + "onBeforeUpdate": true, | ||
| 40 | + "onDeactivated": true, | ||
| 41 | + "onErrorCaptured": true, | ||
| 42 | + "onMounted": true, | ||
| 43 | + "onRenderTracked": true, | ||
| 44 | + "onRenderTriggered": true, | ||
| 45 | + "onScopeDispose": true, | ||
| 46 | + "onServerPrefetch": true, | ||
| 47 | + "onUnmounted": true, | ||
| 48 | + "onUpdated": true, | ||
| 49 | + "onWatcherCleanup": true, | ||
| 50 | + "provide": true, | ||
| 51 | + "reactive": true, | ||
| 52 | + "readonly": true, | ||
| 53 | + "ref": true, | ||
| 54 | + "resolveComponent": true, | ||
| 55 | + "shallowReactive": true, | ||
| 56 | + "shallowReadonly": true, | ||
| 57 | + "shallowRef": true, | ||
| 58 | + "toRaw": true, | ||
| 59 | + "toRef": true, | ||
| 60 | + "toRefs": true, | ||
| 61 | + "toValue": true, | ||
| 62 | + "triggerRef": true, | ||
| 63 | + "unref": true, | ||
| 64 | + "useAttrs": true, | ||
| 65 | + "useCssModule": true, | ||
| 66 | + "useCssVars": true, | ||
| 67 | + "useId": true, | ||
| 68 | + "useLink": true, | ||
| 69 | + "useModel": true, | ||
| 70 | + "useRoute": true, | ||
| 71 | + "useRouter": true, | ||
| 72 | + "useSlots": true, | ||
| 73 | + "useTemplateRef": true, | ||
| 74 | + "watch": true, | ||
| 75 | + "watchEffect": true, | ||
| 76 | + "watchPostEffect": true, | ||
| 77 | + "watchSyncEffect": true | ||
| 78 | + } | ||
| 79 | +} |
.gitignore
0 → 100644
| 1 | +# Logs | ||
| 2 | +logs | ||
| 3 | +*.log | ||
| 4 | +npm-debug.log* | ||
| 5 | +yarn-debug.log* | ||
| 6 | +yarn-error.log* | ||
| 7 | +pnpm-debug.log* | ||
| 8 | +lerna-debug.log* | ||
| 9 | + | ||
| 10 | +node_modules | ||
| 11 | +dist | ||
| 12 | +dist-ssr | ||
| 13 | +*.local | ||
| 14 | + | ||
| 15 | +# Editor directories and files | ||
| 16 | +.vscode/* | ||
| 17 | +!.vscode/extensions.json | ||
| 18 | +.idea | ||
| 19 | +.DS_Store | ||
| 20 | +*.suo | ||
| 21 | +*.ntvs* | ||
| 22 | +*.njsproj | ||
| 23 | +*.sln | ||
| 24 | +*.sw? | ||
| 25 | + | ||
| 26 | +# Environment variables | ||
| 27 | +.env | ||
| 28 | +.env.local | ||
| 29 | +.env.development.local | ||
| 30 | +.env.test.local | ||
| 31 | +.env.production.local | ||
| 32 | + | ||
| 33 | +# Build outputs | ||
| 34 | +build/ | ||
| 35 | +coverage/ | ||
| 36 | + | ||
| 37 | +# Cache | ||
| 38 | +.cache/ | ||
| 39 | +.parcel-cache/ | ||
| 40 | +.eslintcache | ||
| 41 | + | ||
| 42 | +# History | ||
| 43 | +.history/ | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
README.md
0 → 100644
| 1 | +# H5 Vite Template | ||
| 2 | + | ||
| 3 | +基于 Vue 3 + Vite + Vant 4 的移动端 H5 项目模板 | ||
| 4 | + | ||
| 5 | +## 特性 | ||
| 6 | + | ||
| 7 | +- ⚡️ **Vite** - 极速的开发体验 | ||
| 8 | +- 🖖 **Vue 3** - 渐进式 JavaScript 框架 | ||
| 9 | +- 📱 **Vant 4** - 轻量、可靠的移动端组件库 | ||
| 10 | +- 🎨 **Tailwind CSS** - 原子化 CSS 框架 | ||
| 11 | +- 📦 **Pinia** - 符合直觉的 Vue.js 状态管理库 | ||
| 12 | +- 🛣️ **Vue Router** - Vue.js 官方路由 | ||
| 13 | +- 📡 **Axios** - 基于 Promise 的 HTTP 客户端 | ||
| 14 | +- 🔧 **ESLint** - 代码质量检查 | ||
| 15 | +- 📐 **PostCSS** - CSS 后处理器 | ||
| 16 | +- 📱 **移动端适配** - 基于 postcss-px-to-viewport 的移动端适配方案 | ||
| 17 | + | ||
| 18 | +## 目录结构 | ||
| 19 | + | ||
| 20 | +``` | ||
| 21 | +h5_vite_template/ | ||
| 22 | +├── public/ # 静态资源 | ||
| 23 | +├── src/ | ||
| 24 | +│ ├── api/ # API 接口 | ||
| 25 | +│ ├── assets/ # 资源文件 | ||
| 26 | +│ ├── components/ # 通用组件 | ||
| 27 | +│ ├── router/ # 路由配置 | ||
| 28 | +│ ├── stores/ # 状态管理 | ||
| 29 | +│ ├── utils/ # 工具函数 | ||
| 30 | +│ ├── views/ # 页面组件 | ||
| 31 | +│ ├── App.vue # 根组件 | ||
| 32 | +│ ├── main.js # 入口文件 | ||
| 33 | +│ └── style.css # 全局样式 | ||
| 34 | +├── .env # 环境变量 | ||
| 35 | +├── .env.development # 开发环境变量 | ||
| 36 | +├── .env.production # 生产环境变量 | ||
| 37 | +├── .gitignore # Git 忽略文件 | ||
| 38 | +├── index.html # HTML 模板 | ||
| 39 | +├── package.json # 项目配置 | ||
| 40 | +├── postcss.config.js # PostCSS 配置 | ||
| 41 | +├── tailwind.config.js # Tailwind CSS 配置 | ||
| 42 | +└── vite.config.js # Vite 配置 | ||
| 43 | +``` | ||
| 44 | + | ||
| 45 | +## 快速开始 | ||
| 46 | + | ||
| 47 | +### 安装依赖 | ||
| 48 | + | ||
| 49 | +```bash | ||
| 50 | +npm install | ||
| 51 | +# 或 | ||
| 52 | +yarn install | ||
| 53 | +# 或 | ||
| 54 | +pnpm install | ||
| 55 | +``` | ||
| 56 | + | ||
| 57 | +### 开发 | ||
| 58 | + | ||
| 59 | +```bash | ||
| 60 | +npm run dev | ||
| 61 | +# 或 | ||
| 62 | +yarn dev | ||
| 63 | +# 或 | ||
| 64 | +pnpm dev | ||
| 65 | +``` | ||
| 66 | + | ||
| 67 | +### 构建 | ||
| 68 | + | ||
| 69 | +```bash | ||
| 70 | +npm run build | ||
| 71 | +# 或 | ||
| 72 | +yarn build | ||
| 73 | +# 或 | ||
| 74 | +pnpm build | ||
| 75 | +``` | ||
| 76 | + | ||
| 77 | +### 预览 | ||
| 78 | + | ||
| 79 | +```bash | ||
| 80 | +npm run preview | ||
| 81 | +# 或 | ||
| 82 | +yarn preview | ||
| 83 | +# 或 | ||
| 84 | +pnpm preview | ||
| 85 | +``` | ||
| 86 | + | ||
| 87 | +## 配置说明 | ||
| 88 | + | ||
| 89 | +### 环境变量 | ||
| 90 | + | ||
| 91 | +项目支持多环境配置,通过 `.env` 文件进行管理: | ||
| 92 | + | ||
| 93 | +- `.env` - 所有环境的默认配置 | ||
| 94 | +- `.env.development` - 开发环境配置 | ||
| 95 | +- `.env.production` - 生产环境配置 | ||
| 96 | + | ||
| 97 | +### 移动端适配 | ||
| 98 | + | ||
| 99 | +项目使用 `postcss-px-to-viewport` 进行移动端适配,设计稿基准为 375px。 | ||
| 100 | + | ||
| 101 | +### 路由配置 | ||
| 102 | + | ||
| 103 | +路由配置位于 `src/router/index.js`,支持: | ||
| 104 | + | ||
| 105 | +- 路由懒加载 | ||
| 106 | +- 路由守卫 | ||
| 107 | +- 页面标题设置 | ||
| 108 | +- 滚动行为控制 | ||
| 109 | + | ||
| 110 | +### 状态管理 | ||
| 111 | + | ||
| 112 | +使用 Pinia 进行状态管理,store 文件位于 `src/stores/` 目录。 | ||
| 113 | + | ||
| 114 | +### API 请求 | ||
| 115 | + | ||
| 116 | +API 请求基于 Axios 封装,配置文件位于 `src/utils/request.js`,支持: | ||
| 117 | + | ||
| 118 | +- 请求/响应拦截器 | ||
| 119 | +- 自动 Loading | ||
| 120 | +- 错误处理 | ||
| 121 | +- Token 自动携带 | ||
| 122 | + | ||
| 123 | +## 组件说明 | ||
| 124 | + | ||
| 125 | +### 页面组件 | ||
| 126 | + | ||
| 127 | +- **Home** - 首页,展示轮播图、菜单网格、通知栏等 | ||
| 128 | +- **About** - 关于页面,展示项目信息和功能特性 | ||
| 129 | +- **Profile** - 个人中心,展示用户信息和功能菜单 | ||
| 130 | +- **Demo** - 组件演示页面,展示 Vant 组件使用示例 | ||
| 131 | +- **NotFound** - 404 页面 | ||
| 132 | + | ||
| 133 | +### 通用组件 | ||
| 134 | + | ||
| 135 | +- **LoadingSpinner** - 加载动画组件 | ||
| 136 | +- **EmptyState** - 空状态组件 | ||
| 137 | + | ||
| 138 | +## 开发规范 | ||
| 139 | + | ||
| 140 | +### 代码风格 | ||
| 141 | + | ||
| 142 | +项目使用 ESLint 进行代码质量检查,请遵循以下规范: | ||
| 143 | + | ||
| 144 | +- 使用 2 空格缩进 | ||
| 145 | +- 使用单引号 | ||
| 146 | +- 行末不加分号 | ||
| 147 | +- 组件名使用 PascalCase | ||
| 148 | +- 文件名使用 kebab-case | ||
| 149 | + | ||
| 150 | +### Git 提交规范 | ||
| 151 | + | ||
| 152 | +建议使用以下格式进行 Git 提交: | ||
| 153 | + | ||
| 154 | +``` | ||
| 155 | +<type>(<scope>): <subject> | ||
| 156 | + | ||
| 157 | +<body> | ||
| 158 | + | ||
| 159 | +<footer> | ||
| 160 | +``` | ||
| 161 | + | ||
| 162 | +类型说明: | ||
| 163 | +- `feat`: 新功能 | ||
| 164 | +- `fix`: 修复 bug | ||
| 165 | +- `docs`: 文档更新 | ||
| 166 | +- `style`: 代码格式调整 | ||
| 167 | +- `refactor`: 代码重构 | ||
| 168 | +- `test`: 测试相关 | ||
| 169 | +- `chore`: 构建过程或辅助工具的变动 | ||
| 170 | + | ||
| 171 | +## 部署 | ||
| 172 | + | ||
| 173 | +### 构建产物 | ||
| 174 | + | ||
| 175 | +执行 `npm run build` 后,构建产物将输出到 `dist` 目录。 | ||
| 176 | + | ||
| 177 | +### 静态部署 | ||
| 178 | + | ||
| 179 | +构建产物可以部署到任何静态文件服务器,如: | ||
| 180 | + | ||
| 181 | +- Nginx | ||
| 182 | +- Apache | ||
| 183 | +- Vercel | ||
| 184 | +- Netlify | ||
| 185 | +- GitHub Pages | ||
| 186 | + | ||
| 187 | +### 注意事项 | ||
| 188 | + | ||
| 189 | +1. 如果部署到子路径,需要在 `vite.config.js` 中配置 `base` 选项 | ||
| 190 | +2. 确保服务器支持 History 模式的路由 | ||
| 191 | +3. 生产环境需要配置正确的 API 地址 | ||
| 192 | + | ||
| 193 | +## 浏览器支持 | ||
| 194 | + | ||
| 195 | +- Chrome >= 87 | ||
| 196 | +- Firefox >= 78 | ||
| 197 | +- Safari >= 14 | ||
| 198 | +- iOS Safari >= 14.4 | ||
| 199 | +- Android Browser >= 87 | ||
| 200 | + | ||
| 201 | +## 许可证 | ||
| 202 | + | ||
| 203 | +MIT License | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
index.html
0 → 100644
| 1 | +<!DOCTYPE html> | ||
| 2 | +<html lang="zh-CN"> | ||
| 3 | + <head> | ||
| 4 | + <meta charset="UTF-8" /> | ||
| 5 | + <link rel="icon" type="image/svg+xml" href="/vite.svg" /> | ||
| 6 | + <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no" /> | ||
| 7 | + <meta name="format-detection" content="telephone=no" /> | ||
| 8 | + <meta name="apple-mobile-web-app-capable" content="yes" /> | ||
| 9 | + <meta name="apple-mobile-web-app-status-bar-style" content="black" /> | ||
| 10 | + <title>H5 Vite Template</title> | ||
| 11 | + </head> | ||
| 12 | + <body> | ||
| 13 | + <div id="app"></div> | ||
| 14 | + <script type="module" src="/src/main.js"></script> | ||
| 15 | + </body> | ||
| 16 | +</html> | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
package-lock.json
0 → 100644
This diff could not be displayed because it is too large.
package.json
0 → 100644
| 1 | +{ | ||
| 2 | + "name": "stdj-h5", | ||
| 3 | + "description": "三坛大戒", | ||
| 4 | + "version": "1.0.0", | ||
| 5 | + "type": "module", | ||
| 6 | + "scripts": { | ||
| 7 | + "dev": "vite", | ||
| 8 | + "start": "vite --host 0.0.0.0", | ||
| 9 | + "build": "vite build", | ||
| 10 | + "build-watch": "vite build --watch", | ||
| 11 | + "serve": "vite preview", | ||
| 12 | + "lint": "eslint . --ext vue,js,jsx,cjs,mjs --fix --ignore-path .gitignore" | ||
| 13 | + }, | ||
| 14 | + "dependencies": { | ||
| 15 | + "@vant/area-data": "^1.3.1", | ||
| 16 | + "@vant/touch-emulator": "^1.4.0", | ||
| 17 | + "@vueuse/core": "^10.7.2", | ||
| 18 | + "axios": "^1.6.7", | ||
| 19 | + "dayjs": "^1.11.10", | ||
| 20 | + "js-cookie": "^3.0.5", | ||
| 21 | + "lodash": "^4.17.21", | ||
| 22 | + "pinia": "^2.1.7", | ||
| 23 | + "vant": "^4.9.1", | ||
| 24 | + "vue": "^3.4.15", | ||
| 25 | + "vue-router": "^4.2.5" | ||
| 26 | + }, | ||
| 27 | + "devDependencies": { | ||
| 28 | + "@vitejs/plugin-vue": "^5.0.3", | ||
| 29 | + "autoprefixer": "^10.4.17", | ||
| 30 | + "postcss": "^8.4.35", | ||
| 31 | + "postcss-px-to-viewport": "^1.1.1", | ||
| 32 | + "tailwindcss": "^3.4.1", | ||
| 33 | + "unplugin-auto-import": "^0.17.5", | ||
| 34 | + "unplugin-vue-components": "^0.26.0", | ||
| 35 | + "vite": "^5.1.0" | ||
| 36 | + } | ||
| 37 | +} |
postcss.config.js
0 → 100644
| 1 | +export default { | ||
| 2 | + plugins: { | ||
| 3 | + tailwindcss: {}, | ||
| 4 | + autoprefixer: {}, | ||
| 5 | + 'postcss-px-to-viewport': { | ||
| 6 | + unitToConvert: 'px', | ||
| 7 | + viewportWidth: 375, | ||
| 8 | + unitPrecision: 6, | ||
| 9 | + propList: ['*'], | ||
| 10 | + viewportUnit: 'vw', | ||
| 11 | + fontViewportUnit: 'vw', | ||
| 12 | + selectorBlackList: ['ignore-'], | ||
| 13 | + minPixelValue: 1, | ||
| 14 | + mediaQuery: true, | ||
| 15 | + replace: true, | ||
| 16 | + exclude: [], | ||
| 17 | + landscape: false | ||
| 18 | + } | ||
| 19 | + } | ||
| 20 | +} | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
src/App.vue
0 → 100644
src/api/index.js
0 → 100644
| 1 | +import request from '@/utils/request' | ||
| 2 | + | ||
| 3 | +// 用户相关 API | ||
| 4 | +export const userApi = { | ||
| 5 | + // 获取用户信息 | ||
| 6 | + getUserInfo: () => request.get('/user/info'), | ||
| 7 | + | ||
| 8 | + // 更新用户信息 | ||
| 9 | + updateUserInfo: (data) => request.put('/user/info', data), | ||
| 10 | + | ||
| 11 | + // 用户登录 | ||
| 12 | + login: (data) => request.post('/user/login', data), | ||
| 13 | + | ||
| 14 | + // 用户注册 | ||
| 15 | + register: (data) => request.post('/user/register', data), | ||
| 16 | + | ||
| 17 | + // 用户登出 | ||
| 18 | + logout: () => request.post('/user/logout') | ||
| 19 | +} | ||
| 20 | + | ||
| 21 | +// 通用 API | ||
| 22 | +export const commonApi = { | ||
| 23 | + // 上传文件 | ||
| 24 | + upload: (file) => { | ||
| 25 | + const formData = new FormData() | ||
| 26 | + formData.append('file', file) | ||
| 27 | + return request.post('/upload', formData, { | ||
| 28 | + headers: { | ||
| 29 | + 'Content-Type': 'multipart/form-data' | ||
| 30 | + } | ||
| 31 | + }) | ||
| 32 | + }, | ||
| 33 | + | ||
| 34 | + // 获取配置信息 | ||
| 35 | + getConfig: () => request.get('/config'), | ||
| 36 | + | ||
| 37 | + // 发送验证码 | ||
| 38 | + sendSms: (phone) => request.post('/sms/send', { phone }) | ||
| 39 | +} | ||
| 40 | + | ||
| 41 | +// 示例 API | ||
| 42 | +export const demoApi = { | ||
| 43 | + // 获取列表数据 | ||
| 44 | + getList: (params) => request.get('/demo/list', { params }), | ||
| 45 | + | ||
| 46 | + // 获取详情 | ||
| 47 | + getDetail: (id) => request.get(`/demo/${id}`), | ||
| 48 | + | ||
| 49 | + // 创建数据 | ||
| 50 | + create: (data) => request.post('/demo', data), | ||
| 51 | + | ||
| 52 | + // 更新数据 | ||
| 53 | + update: (id, data) => request.put(`/demo/${id}`, data), | ||
| 54 | + | ||
| 55 | + // 删除数据 | ||
| 56 | + delete: (id) => request.delete(`/demo/${id}`) | ||
| 57 | +} | ||
| 58 | + | ||
| 59 | +// 导出所有 API | ||
| 60 | +export default { | ||
| 61 | + userApi, | ||
| 62 | + commonApi, | ||
| 63 | + demoApi | ||
| 64 | +} | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
src/auto-imports.d.ts
0 → 100644
| 1 | +/* eslint-disable */ | ||
| 2 | +/* prettier-ignore */ | ||
| 3 | +// @ts-nocheck | ||
| 4 | +// noinspection JSUnusedGlobalSymbols | ||
| 5 | +// Generated by unplugin-auto-import | ||
| 6 | +export {} | ||
| 7 | +declare global { | ||
| 8 | + const EffectScope: typeof import('vue')['EffectScope'] | ||
| 9 | + const computed: typeof import('vue')['computed'] | ||
| 10 | + const createApp: typeof import('vue')['createApp'] | ||
| 11 | + const customRef: typeof import('vue')['customRef'] | ||
| 12 | + const defineAsyncComponent: typeof import('vue')['defineAsyncComponent'] | ||
| 13 | + const defineComponent: typeof import('vue')['defineComponent'] | ||
| 14 | + const effectScope: typeof import('vue')['effectScope'] | ||
| 15 | + const getCurrentInstance: typeof import('vue')['getCurrentInstance'] | ||
| 16 | + const getCurrentScope: typeof import('vue')['getCurrentScope'] | ||
| 17 | + const h: typeof import('vue')['h'] | ||
| 18 | + const inject: typeof import('vue')['inject'] | ||
| 19 | + const isProxy: typeof import('vue')['isProxy'] | ||
| 20 | + const isReactive: typeof import('vue')['isReactive'] | ||
| 21 | + const isReadonly: typeof import('vue')['isReadonly'] | ||
| 22 | + const isRef: typeof import('vue')['isRef'] | ||
| 23 | + const markRaw: typeof import('vue')['markRaw'] | ||
| 24 | + const nextTick: typeof import('vue')['nextTick'] | ||
| 25 | + const onActivated: typeof import('vue')['onActivated'] | ||
| 26 | + const onBeforeMount: typeof import('vue')['onBeforeMount'] | ||
| 27 | + const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave'] | ||
| 28 | + const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate'] | ||
| 29 | + const onBeforeUnmount: typeof import('vue')['onBeforeUnmount'] | ||
| 30 | + const onBeforeUpdate: typeof import('vue')['onBeforeUpdate'] | ||
| 31 | + const onDeactivated: typeof import('vue')['onDeactivated'] | ||
| 32 | + const onErrorCaptured: typeof import('vue')['onErrorCaptured'] | ||
| 33 | + const onMounted: typeof import('vue')['onMounted'] | ||
| 34 | + const onRenderTracked: typeof import('vue')['onRenderTracked'] | ||
| 35 | + const onRenderTriggered: typeof import('vue')['onRenderTriggered'] | ||
| 36 | + const onScopeDispose: typeof import('vue')['onScopeDispose'] | ||
| 37 | + const onServerPrefetch: typeof import('vue')['onServerPrefetch'] | ||
| 38 | + const onUnmounted: typeof import('vue')['onUnmounted'] | ||
| 39 | + const onUpdated: typeof import('vue')['onUpdated'] | ||
| 40 | + const onWatcherCleanup: typeof import('vue')['onWatcherCleanup'] | ||
| 41 | + const provide: typeof import('vue')['provide'] | ||
| 42 | + const reactive: typeof import('vue')['reactive'] | ||
| 43 | + const readonly: typeof import('vue')['readonly'] | ||
| 44 | + const ref: typeof import('vue')['ref'] | ||
| 45 | + const resolveComponent: typeof import('vue')['resolveComponent'] | ||
| 46 | + const shallowReactive: typeof import('vue')['shallowReactive'] | ||
| 47 | + const shallowReadonly: typeof import('vue')['shallowReadonly'] | ||
| 48 | + const shallowRef: typeof import('vue')['shallowRef'] | ||
| 49 | + const toRaw: typeof import('vue')['toRaw'] | ||
| 50 | + const toRef: typeof import('vue')['toRef'] | ||
| 51 | + const toRefs: typeof import('vue')['toRefs'] | ||
| 52 | + const toValue: typeof import('vue')['toValue'] | ||
| 53 | + const triggerRef: typeof import('vue')['triggerRef'] | ||
| 54 | + const unref: typeof import('vue')['unref'] | ||
| 55 | + const useAttrs: typeof import('vue')['useAttrs'] | ||
| 56 | + const useCssModule: typeof import('vue')['useCssModule'] | ||
| 57 | + const useCssVars: typeof import('vue')['useCssVars'] | ||
| 58 | + const useId: typeof import('vue')['useId'] | ||
| 59 | + const useLink: typeof import('vue-router')['useLink'] | ||
| 60 | + const useModel: typeof import('vue')['useModel'] | ||
| 61 | + const useRoute: typeof import('vue-router')['useRoute'] | ||
| 62 | + const useRouter: typeof import('vue-router')['useRouter'] | ||
| 63 | + const useSlots: typeof import('vue')['useSlots'] | ||
| 64 | + const useTemplateRef: typeof import('vue')['useTemplateRef'] | ||
| 65 | + const watch: typeof import('vue')['watch'] | ||
| 66 | + const watchEffect: typeof import('vue')['watchEffect'] | ||
| 67 | + const watchPostEffect: typeof import('vue')['watchPostEffect'] | ||
| 68 | + const watchSyncEffect: typeof import('vue')['watchSyncEffect'] | ||
| 69 | +} | ||
| 70 | +// for type re-export | ||
| 71 | +declare global { | ||
| 72 | + // @ts-ignore | ||
| 73 | + export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue' | ||
| 74 | + import('vue') | ||
| 75 | +} |
src/components.d.ts
0 → 100644
| 1 | +/* eslint-disable */ | ||
| 2 | +/* prettier-ignore */ | ||
| 3 | +// @ts-nocheck | ||
| 4 | +// Generated by unplugin-vue-components | ||
| 5 | +// Read more: https://github.com/vuejs/core/pull/3399 | ||
| 6 | +export {} | ||
| 7 | + | ||
| 8 | +declare module 'vue' { | ||
| 9 | + export interface GlobalComponents { | ||
| 10 | + EmptyState: typeof import('./components/EmptyState.vue')['default'] | ||
| 11 | + LoadingSpinner: typeof import('./components/LoadingSpinner.vue')['default'] | ||
| 12 | + RouterLink: typeof import('vue-router')['RouterLink'] | ||
| 13 | + RouterView: typeof import('vue-router')['RouterView'] | ||
| 14 | + VanBadge: typeof import('vant/es')['Badge'] | ||
| 15 | + VanButton: typeof import('vant/es')['Button'] | ||
| 16 | + VanCard: typeof import('vant/es')['Card'] | ||
| 17 | + VanCell: typeof import('vant/es')['Cell'] | ||
| 18 | + VanCellGroup: typeof import('vant/es')['CellGroup'] | ||
| 19 | + VanCheckbox: typeof import('vant/es')['Checkbox'] | ||
| 20 | + VanCollapse: typeof import('vant/es')['Collapse'] | ||
| 21 | + VanCollapseItem: typeof import('vant/es')['CollapseItem'] | ||
| 22 | + VanField: typeof import('vant/es')['Field'] | ||
| 23 | + VanForm: typeof import('vant/es')['Form'] | ||
| 24 | + VanGrid: typeof import('vant/es')['Grid'] | ||
| 25 | + VanGridItem: typeof import('vant/es')['GridItem'] | ||
| 26 | + VanIcon: typeof import('vant/es')['Icon'] | ||
| 27 | + VanImage: typeof import('vant/es')['Image'] | ||
| 28 | + VanNavBar: typeof import('vant/es')['NavBar'] | ||
| 29 | + VanNoticeBar: typeof import('vant/es')['NoticeBar'] | ||
| 30 | + VanProgress: typeof import('vant/es')['Progress'] | ||
| 31 | + VanRate: typeof import('vant/es')['Rate'] | ||
| 32 | + VanSidebar: typeof import('vant/es')['Sidebar'] | ||
| 33 | + VanSidebarItem: typeof import('vant/es')['SidebarItem'] | ||
| 34 | + VanStep: typeof import('vant/es')['Step'] | ||
| 35 | + VanSteps: typeof import('vant/es')['Steps'] | ||
| 36 | + VanSwipe: typeof import('vant/es')['Swipe'] | ||
| 37 | + VanSwipeItem: typeof import('vant/es')['SwipeItem'] | ||
| 38 | + VanSwitch: typeof import('vant/es')['Switch'] | ||
| 39 | + VanTab: typeof import('vant/es')['Tab'] | ||
| 40 | + VanTabbar: typeof import('vant/es')['Tabbar'] | ||
| 41 | + VanTabbarItem: typeof import('vant/es')['TabbarItem'] | ||
| 42 | + VanTabs: typeof import('vant/es')['Tabs'] | ||
| 43 | + VanTag: typeof import('vant/es')['Tag'] | ||
| 44 | + } | ||
| 45 | +} |
src/components/EmptyState.vue
0 → 100644
| 1 | +<template> | ||
| 2 | + <div class="empty-state"> | ||
| 3 | + <div class="empty-icon"> | ||
| 4 | + <van-empty | ||
| 5 | + :image="image" | ||
| 6 | + :image-size="imageSize" | ||
| 7 | + :description="description" | ||
| 8 | + > | ||
| 9 | + <template v-if="$slots.image" #image> | ||
| 10 | + <slot name="image" /> | ||
| 11 | + </template> | ||
| 12 | + | ||
| 13 | + <template v-if="$slots.description" #description> | ||
| 14 | + <slot name="description" /> | ||
| 15 | + </template> | ||
| 16 | + | ||
| 17 | + <template v-if="showAction" #default> | ||
| 18 | + <van-button | ||
| 19 | + :type="actionType" | ||
| 20 | + :size="actionSize" | ||
| 21 | + round | ||
| 22 | + @click="handleAction" | ||
| 23 | + > | ||
| 24 | + {{ actionText }} | ||
| 25 | + </van-button> | ||
| 26 | + </template> | ||
| 27 | + </van-empty> | ||
| 28 | + </div> | ||
| 29 | + </div> | ||
| 30 | +</template> | ||
| 31 | + | ||
| 32 | +<script setup> | ||
| 33 | +import { defineEmits } from 'vue' | ||
| 34 | + | ||
| 35 | +const props = defineProps({ | ||
| 36 | + // 图片类型 | ||
| 37 | + image: { | ||
| 38 | + type: String, | ||
| 39 | + default: 'default' | ||
| 40 | + }, | ||
| 41 | + // 图片大小 | ||
| 42 | + imageSize: { | ||
| 43 | + type: [Number, String], | ||
| 44 | + default: 160 | ||
| 45 | + }, | ||
| 46 | + // 描述文字 | ||
| 47 | + description: { | ||
| 48 | + type: String, | ||
| 49 | + default: '暂无数据' | ||
| 50 | + }, | ||
| 51 | + // 是否显示操作按钮 | ||
| 52 | + showAction: { | ||
| 53 | + type: Boolean, | ||
| 54 | + default: false | ||
| 55 | + }, | ||
| 56 | + // 按钮文字 | ||
| 57 | + actionText: { | ||
| 58 | + type: String, | ||
| 59 | + default: '重新加载' | ||
| 60 | + }, | ||
| 61 | + // 按钮类型 | ||
| 62 | + actionType: { | ||
| 63 | + type: String, | ||
| 64 | + default: 'primary' | ||
| 65 | + }, | ||
| 66 | + // 按钮大小 | ||
| 67 | + actionSize: { | ||
| 68 | + type: String, | ||
| 69 | + default: 'normal' | ||
| 70 | + } | ||
| 71 | +}) | ||
| 72 | + | ||
| 73 | +const emit = defineEmits(['action']) | ||
| 74 | + | ||
| 75 | +const handleAction = () => { | ||
| 76 | + emit('action') | ||
| 77 | +} | ||
| 78 | +</script> | ||
| 79 | + | ||
| 80 | +<style scoped> | ||
| 81 | +.empty-state { | ||
| 82 | + padding: 40px 20px; | ||
| 83 | + text-align: center; | ||
| 84 | +} | ||
| 85 | +</style> | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
src/components/LoadingSpinner.vue
0 → 100644
| 1 | +<template> | ||
| 2 | + <div class="loading-spinner" :class="{ 'loading-overlay': overlay }"> | ||
| 3 | + <div class="spinner-container"> | ||
| 4 | + <div class="spinner" :style="{ width: size + 'px', height: size + 'px' }"> | ||
| 5 | + <div class="spinner-inner" :style="{ borderColor: color }"></div> | ||
| 6 | + </div> | ||
| 7 | + <p v-if="text" class="loading-text" :style="{ color: textColor }">{{ text }}</p> | ||
| 8 | + </div> | ||
| 9 | + </div> | ||
| 10 | +</template> | ||
| 11 | + | ||
| 12 | +<script setup> | ||
| 13 | +defineProps({ | ||
| 14 | + // 是否显示遮罩层 | ||
| 15 | + overlay: { | ||
| 16 | + type: Boolean, | ||
| 17 | + default: false | ||
| 18 | + }, | ||
| 19 | + // 加载器大小 | ||
| 20 | + size: { | ||
| 21 | + type: Number, | ||
| 22 | + default: 40 | ||
| 23 | + }, | ||
| 24 | + // 加载器颜色 | ||
| 25 | + color: { | ||
| 26 | + type: String, | ||
| 27 | + default: '#1989fa' | ||
| 28 | + }, | ||
| 29 | + // 加载文本 | ||
| 30 | + text: { | ||
| 31 | + type: String, | ||
| 32 | + default: '' | ||
| 33 | + }, | ||
| 34 | + // 文本颜色 | ||
| 35 | + textColor: { | ||
| 36 | + type: String, | ||
| 37 | + default: '#969799' | ||
| 38 | + } | ||
| 39 | +}) | ||
| 40 | +</script> | ||
| 41 | + | ||
| 42 | +<style scoped> | ||
| 43 | +.loading-spinner { | ||
| 44 | + display: flex; | ||
| 45 | + align-items: center; | ||
| 46 | + justify-content: center; | ||
| 47 | +} | ||
| 48 | + | ||
| 49 | +.loading-overlay { | ||
| 50 | + position: fixed; | ||
| 51 | + top: 0; | ||
| 52 | + left: 0; | ||
| 53 | + right: 0; | ||
| 54 | + bottom: 0; | ||
| 55 | + background: rgba(255, 255, 255, 0.9); | ||
| 56 | + z-index: 9999; | ||
| 57 | +} | ||
| 58 | + | ||
| 59 | +.spinner-container { | ||
| 60 | + display: flex; | ||
| 61 | + flex-direction: column; | ||
| 62 | + align-items: center; | ||
| 63 | + gap: 12px; | ||
| 64 | +} | ||
| 65 | + | ||
| 66 | +.spinner { | ||
| 67 | + position: relative; | ||
| 68 | +} | ||
| 69 | + | ||
| 70 | +.spinner-inner { | ||
| 71 | + width: 100%; | ||
| 72 | + height: 100%; | ||
| 73 | + border: 3px solid transparent; | ||
| 74 | + border-top-color: currentColor; | ||
| 75 | + border-radius: 50%; | ||
| 76 | + animation: spin 1s linear infinite; | ||
| 77 | +} | ||
| 78 | + | ||
| 79 | +.loading-text { | ||
| 80 | + font-size: 14px; | ||
| 81 | + margin: 0; | ||
| 82 | +} | ||
| 83 | + | ||
| 84 | +@keyframes spin { | ||
| 85 | + 0% { | ||
| 86 | + transform: rotate(0deg); | ||
| 87 | + } | ||
| 88 | + 100% { | ||
| 89 | + transform: rotate(360deg); | ||
| 90 | + } | ||
| 91 | +} | ||
| 92 | +</style> | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
src/main.js
0 → 100644
| 1 | +import { createApp } from 'vue' | ||
| 2 | +import { createPinia } from 'pinia' | ||
| 3 | +import router from './router' | ||
| 4 | +import App from './App.vue' | ||
| 5 | + | ||
| 6 | +// 引入样式 | ||
| 7 | +import './style.css' | ||
| 8 | +import 'vant/lib/index.css' | ||
| 9 | + | ||
| 10 | +// 引入 Vant 组件(按需引入会通过 unplugin-vue-components 自动处理) | ||
| 11 | +// 这里只需要引入一些全局配置的组件 | ||
| 12 | +import { ConfigProvider, Toast, Dialog, Notify, ImagePreview } from 'vant' | ||
| 13 | + | ||
| 14 | +const app = createApp(App) | ||
| 15 | +const pinia = createPinia() | ||
| 16 | + | ||
| 17 | +// 全局配置 | ||
| 18 | +app.config.globalProperties.$toast = Toast | ||
| 19 | +app.config.globalProperties.$dialog = Dialog | ||
| 20 | +app.config.globalProperties.$notify = Notify | ||
| 21 | +app.config.globalProperties.$imagePreview = ImagePreview | ||
| 22 | + | ||
| 23 | +// 使用插件 | ||
| 24 | +app.use(pinia) | ||
| 25 | +app.use(router) | ||
| 26 | +app.use(ConfigProvider) | ||
| 27 | + | ||
| 28 | +// 挂载应用 | ||
| 29 | +app.mount('#app') | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
src/router/index.js
0 → 100644
| 1 | +/* | ||
| 2 | + * @Date: 2025-10-30 10:29:15 | ||
| 3 | + * @LastEditors: hookehuyr hookehuyr@gmail.com | ||
| 4 | + * @LastEditTime: 2025-10-30 10:29:24 | ||
| 5 | + * @FilePath: /itomix/h5_vite_template/src/router/index.js | ||
| 6 | + * @Description: 文件描述 | ||
| 7 | + */ | ||
| 8 | +import { createRouter, createWebHistory } from 'vue-router' | ||
| 9 | +import Home from '../views/Home.vue' | ||
| 10 | + | ||
| 11 | +const routes = [ | ||
| 12 | + { | ||
| 13 | + path: '/', | ||
| 14 | + name: 'Splash', | ||
| 15 | + component: () => import('../views/Splash.vue') | ||
| 16 | + }, | ||
| 17 | + { | ||
| 18 | + path: '/home', | ||
| 19 | + name: 'Home', | ||
| 20 | + component: Home | ||
| 21 | + }, | ||
| 22 | + { | ||
| 23 | + path: '/teachers', | ||
| 24 | + name: 'Teachers', | ||
| 25 | + component: () => import('../views/Teachers.vue') | ||
| 26 | + }, | ||
| 27 | + { | ||
| 28 | + path: '/teachers/:id', | ||
| 29 | + name: 'TeacherDetail', | ||
| 30 | + component: () => import('../views/TeacherDetail.vue') | ||
| 31 | + }, | ||
| 32 | + { | ||
| 33 | + path: '/volunteers', | ||
| 34 | + name: 'Volunteers', | ||
| 35 | + component: () => import('../views/Volunteers.vue') | ||
| 36 | + }, | ||
| 37 | + { | ||
| 38 | + path: '/students', | ||
| 39 | + name: 'Students', | ||
| 40 | + component: () => import('../views/Students.vue') | ||
| 41 | + }, | ||
| 42 | + { | ||
| 43 | + path: '/students/:id', | ||
| 44 | + name: 'StudentDetail', | ||
| 45 | + component: () => import('../views/StudentDetail.vue') | ||
| 46 | + }, | ||
| 47 | + { | ||
| 48 | + path: '/news/:id', | ||
| 49 | + name: 'NewsDetail', | ||
| 50 | + component: () => import('../views/NewsDetail.vue') | ||
| 51 | + }, | ||
| 52 | + { | ||
| 53 | + path: '/:pathMatch(.*)*', | ||
| 54 | + name: 'NotFound', | ||
| 55 | + component: () => import('../views/NotFound.vue') | ||
| 56 | + } | ||
| 57 | +] | ||
| 58 | + | ||
| 59 | +const router = createRouter({ | ||
| 60 | + history: createWebHistory(), | ||
| 61 | + routes, | ||
| 62 | + scrollBehavior(to, from, savedPosition) { | ||
| 63 | + if (savedPosition) { | ||
| 64 | + return savedPosition | ||
| 65 | + } else { | ||
| 66 | + return { top: 0 } | ||
| 67 | + } | ||
| 68 | + } | ||
| 69 | +}) | ||
| 70 | + | ||
| 71 | +// 路由守卫 | ||
| 72 | +router.beforeEach((to, from, next) => { | ||
| 73 | + // 设置页面标题 | ||
| 74 | + if (to.meta.title) { | ||
| 75 | + document.title = to.meta.title | ||
| 76 | + } | ||
| 77 | + | ||
| 78 | + // 这里可以添加权限验证逻辑 | ||
| 79 | + next() | ||
| 80 | +}) | ||
| 81 | + | ||
| 82 | +router.afterEach(() => { | ||
| 83 | + // 路由跳转后的逻辑 | ||
| 84 | +}) | ||
| 85 | + | ||
| 86 | +export default router | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
src/stores/user.js
0 → 100644
| 1 | +/* | ||
| 2 | + * @Date: 2025-10-30 10:30:17 | ||
| 3 | + * @LastEditors: hookehuyr hookehuyr@gmail.com | ||
| 4 | + * @LastEditTime: 2025-10-30 10:30:26 | ||
| 5 | + * @FilePath: /itomix/h5_vite_template/src/stores/user.js | ||
| 6 | + * @Description: 文件描述 | ||
| 7 | + */ | ||
| 8 | +import { defineStore } from 'pinia' | ||
| 9 | +import { userApi } from '@/api' | ||
| 10 | + | ||
| 11 | +export const useUserStore = defineStore('user', { | ||
| 12 | + state: () => ({ | ||
| 13 | + userInfo: null, | ||
| 14 | + token: localStorage.getItem('token') || '', | ||
| 15 | + isLogin: false | ||
| 16 | + }), | ||
| 17 | + | ||
| 18 | + getters: { | ||
| 19 | + // 获取用户名 | ||
| 20 | + username: (state) => state.userInfo?.username || '', | ||
| 21 | + | ||
| 22 | + // 获取用户头像 | ||
| 23 | + avatar: (state) => state.userInfo?.avatar || '', | ||
| 24 | + | ||
| 25 | + // 是否已登录 | ||
| 26 | + hasLogin: (state) => !!state.token && !!state.userInfo | ||
| 27 | + }, | ||
| 28 | + | ||
| 29 | + actions: { | ||
| 30 | + // 设置 token | ||
| 31 | + setToken(token) { | ||
| 32 | + this.token = token | ||
| 33 | + localStorage.setItem('token', token) | ||
| 34 | + }, | ||
| 35 | + | ||
| 36 | + // 清除 token | ||
| 37 | + clearToken() { | ||
| 38 | + this.token = '' | ||
| 39 | + localStorage.removeItem('token') | ||
| 40 | + }, | ||
| 41 | + | ||
| 42 | + // 设置用户信息 | ||
| 43 | + setUserInfo(userInfo) { | ||
| 44 | + this.userInfo = userInfo | ||
| 45 | + this.isLogin = true | ||
| 46 | + }, | ||
| 47 | + | ||
| 48 | + // 清除用户信息 | ||
| 49 | + clearUserInfo() { | ||
| 50 | + this.userInfo = null | ||
| 51 | + this.isLogin = false | ||
| 52 | + }, | ||
| 53 | + | ||
| 54 | + // 登录 | ||
| 55 | + async login(loginData) { | ||
| 56 | + const response = await userApi.login(loginData) | ||
| 57 | + const { token, userInfo } = response.data | ||
| 58 | + | ||
| 59 | + this.setToken(token) | ||
| 60 | + this.setUserInfo(userInfo) | ||
| 61 | + | ||
| 62 | + return response | ||
| 63 | + }, | ||
| 64 | + | ||
| 65 | + // 获取用户信息 | ||
| 66 | + async getUserInfo() { | ||
| 67 | + try { | ||
| 68 | + const response = await userApi.getUserInfo() | ||
| 69 | + this.setUserInfo(response.data) | ||
| 70 | + return response | ||
| 71 | + } catch (error) { | ||
| 72 | + // 如果获取用户信息失败,清除本地存储 | ||
| 73 | + this.logout() | ||
| 74 | + throw error | ||
| 75 | + } | ||
| 76 | + }, | ||
| 77 | + | ||
| 78 | + // 登出 | ||
| 79 | + async logout() { | ||
| 80 | + try { | ||
| 81 | + await userApi.logout() | ||
| 82 | + } catch (error) { | ||
| 83 | + console.error('登出失败:', error) | ||
| 84 | + } finally { | ||
| 85 | + this.clearToken() | ||
| 86 | + this.clearUserInfo() | ||
| 87 | + } | ||
| 88 | + } | ||
| 89 | + } | ||
| 90 | +}) | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
src/style.css
0 → 100644
| 1 | +@tailwind base; | ||
| 2 | +@tailwind components; | ||
| 3 | +@tailwind utilities; | ||
| 4 | + | ||
| 5 | +/* 全局样式 */ | ||
| 6 | +* { | ||
| 7 | + box-sizing: border-box; | ||
| 8 | +} | ||
| 9 | + | ||
| 10 | +html, body { | ||
| 11 | + margin: 0; | ||
| 12 | + padding: 0; | ||
| 13 | + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif; | ||
| 14 | + -webkit-font-smoothing: antialiased; | ||
| 15 | + -moz-osx-font-smoothing: grayscale; | ||
| 16 | + background-color: #f7f8fa; | ||
| 17 | +} | ||
| 18 | + | ||
| 19 | +#app { | ||
| 20 | + min-height: 100vh; | ||
| 21 | +} | ||
| 22 | + | ||
| 23 | +/* 自定义工具类 */ | ||
| 24 | +.safe-area-inset-top { | ||
| 25 | + padding-top: constant(safe-area-inset-top); | ||
| 26 | + padding-top: env(safe-area-inset-top); | ||
| 27 | +} | ||
| 28 | + | ||
| 29 | +.safe-area-inset-bottom { | ||
| 30 | + padding-bottom: constant(safe-area-inset-bottom); | ||
| 31 | + padding-bottom: env(safe-area-inset-bottom); | ||
| 32 | +} | ||
| 33 | + | ||
| 34 | +/* 覆盖 Vant 样式 */ | ||
| 35 | +.van-nav-bar { | ||
| 36 | + background-color: #fff; | ||
| 37 | +} | ||
| 38 | + | ||
| 39 | +.van-nav-bar__title { | ||
| 40 | + color: #323233; | ||
| 41 | +} | ||
| 42 | + | ||
| 43 | +/* 页面容器 */ | ||
| 44 | +.page-container { | ||
| 45 | + min-height: 100vh; | ||
| 46 | + background-color: #f7f8fa; | ||
| 47 | +} | ||
| 48 | + | ||
| 49 | +.content-container { | ||
| 50 | + padding: 16px; | ||
| 51 | +} | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
src/utils/request.js
0 → 100644
| 1 | +import axios from 'axios' | ||
| 2 | +import { Toast } from 'vant' | ||
| 3 | + | ||
| 4 | +// 创建 axios 实例 | ||
| 5 | +const request = axios.create({ | ||
| 6 | + baseURL: import.meta.env.VITE_API_BASE_URL || '/api', | ||
| 7 | + timeout: 10000, | ||
| 8 | + headers: { | ||
| 9 | + 'Content-Type': 'application/json;charset=UTF-8' | ||
| 10 | + } | ||
| 11 | +}) | ||
| 12 | + | ||
| 13 | +// 请求拦截器 | ||
| 14 | +request.interceptors.request.use( | ||
| 15 | + (config) => { | ||
| 16 | + // 在发送请求之前做些什么 | ||
| 17 | + | ||
| 18 | + // 添加 token | ||
| 19 | + const token = localStorage.getItem('token') | ||
| 20 | + if (token) { | ||
| 21 | + config.headers.Authorization = `Bearer ${token}` | ||
| 22 | + } | ||
| 23 | + | ||
| 24 | + // 显示加载提示 | ||
| 25 | + Toast.loading({ | ||
| 26 | + message: '加载中...', | ||
| 27 | + forbidClick: true, | ||
| 28 | + duration: 0 | ||
| 29 | + }) | ||
| 30 | + | ||
| 31 | + return config | ||
| 32 | + }, | ||
| 33 | + (error) => { | ||
| 34 | + // 对请求错误做些什么 | ||
| 35 | + Toast.clear() | ||
| 36 | + return Promise.reject(error) | ||
| 37 | + } | ||
| 38 | +) | ||
| 39 | + | ||
| 40 | +// 响应拦截器 | ||
| 41 | +request.interceptors.response.use( | ||
| 42 | + (response) => { | ||
| 43 | + // 对响应数据做点什么 | ||
| 44 | + Toast.clear() | ||
| 45 | + | ||
| 46 | + const { data } = response | ||
| 47 | + | ||
| 48 | + // 根据后端接口规范处理响应 | ||
| 49 | + if (data.code === 200 || data.success) { | ||
| 50 | + return data | ||
| 51 | + } else { | ||
| 52 | + // 业务错误 | ||
| 53 | + Toast.fail(data.message || '请求失败') | ||
| 54 | + return Promise.reject(new Error(data.message || '请求失败')) | ||
| 55 | + } | ||
| 56 | + }, | ||
| 57 | + (error) => { | ||
| 58 | + // 对响应错误做点什么 | ||
| 59 | + Toast.clear() | ||
| 60 | + | ||
| 61 | + let message = '网络错误' | ||
| 62 | + | ||
| 63 | + if (error.response) { | ||
| 64 | + const { status, data } = error.response | ||
| 65 | + | ||
| 66 | + switch (status) { | ||
| 67 | + case 401: | ||
| 68 | + message = '未授权,请重新登录' | ||
| 69 | + // 清除 token 并跳转到登录页 | ||
| 70 | + localStorage.removeItem('token') | ||
| 71 | + // router.push('/login') | ||
| 72 | + break | ||
| 73 | + case 403: | ||
| 74 | + message = '拒绝访问' | ||
| 75 | + break | ||
| 76 | + case 404: | ||
| 77 | + message = '请求地址出错' | ||
| 78 | + break | ||
| 79 | + case 408: | ||
| 80 | + message = '请求超时' | ||
| 81 | + break | ||
| 82 | + case 500: | ||
| 83 | + message = '服务器内部错误' | ||
| 84 | + break | ||
| 85 | + case 501: | ||
| 86 | + message = '服务未实现' | ||
| 87 | + break | ||
| 88 | + case 502: | ||
| 89 | + message = '网关错误' | ||
| 90 | + break | ||
| 91 | + case 503: | ||
| 92 | + message = '服务不可用' | ||
| 93 | + break | ||
| 94 | + case 504: | ||
| 95 | + message = '网关超时' | ||
| 96 | + break | ||
| 97 | + case 505: | ||
| 98 | + message = 'HTTP版本不受支持' | ||
| 99 | + break | ||
| 100 | + default: | ||
| 101 | + message = data?.message || `连接错误${status}` | ||
| 102 | + } | ||
| 103 | + } else if (error.code === 'ECONNABORTED') { | ||
| 104 | + message = '请求超时' | ||
| 105 | + } else if (error.message) { | ||
| 106 | + message = error.message | ||
| 107 | + } | ||
| 108 | + | ||
| 109 | + Toast.fail(message) | ||
| 110 | + return Promise.reject(error) | ||
| 111 | + } | ||
| 112 | +) | ||
| 113 | + | ||
| 114 | +export default request | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
src/views/Home.vue
0 → 100644
This diff is collapsed. Click to expand it.
src/views/NewsDetail.vue
0 → 100644
This diff is collapsed. Click to expand it.
src/views/NotFound.vue
0 → 100644
| 1 | +<template> | ||
| 2 | + <div class="not-found-container"> | ||
| 3 | + <div class="not-found-content"> | ||
| 4 | + <!-- 404 图标 --> | ||
| 5 | + <div class="not-found-icon"> | ||
| 6 | + <svg viewBox="0 0 200 200" class="w-32 h-32 text-gray-300"> | ||
| 7 | + <circle cx="100" cy="100" r="90" fill="none" stroke="currentColor" stroke-width="4"/> | ||
| 8 | + <text x="100" y="120" text-anchor="middle" font-size="48" font-weight="bold" fill="currentColor">404</text> | ||
| 9 | + </svg> | ||
| 10 | + </div> | ||
| 11 | + | ||
| 12 | + <!-- 错误信息 --> | ||
| 13 | + <div class="not-found-text"> | ||
| 14 | + <h1 class="text-2xl font-bold text-gray-800 mb-2">页面不存在</h1> | ||
| 15 | + <p class="text-gray-600 mb-8">抱歉,您访问的页面不存在或已被删除</p> | ||
| 16 | + </div> | ||
| 17 | + | ||
| 18 | + <!-- 操作按钮 --> | ||
| 19 | + <div class="not-found-actions space-y-4"> | ||
| 20 | + <van-button | ||
| 21 | + type="primary" | ||
| 22 | + round | ||
| 23 | + block | ||
| 24 | + @click="goHome" | ||
| 25 | + class="mb-4" | ||
| 26 | + > | ||
| 27 | + 返回首页 | ||
| 28 | + </van-button> | ||
| 29 | + | ||
| 30 | + <van-button | ||
| 31 | + plain | ||
| 32 | + round | ||
| 33 | + block | ||
| 34 | + @click="goBack" | ||
| 35 | + > | ||
| 36 | + 返回上页 | ||
| 37 | + </van-button> | ||
| 38 | + </div> | ||
| 39 | + | ||
| 40 | + <!-- 建议链接 --> | ||
| 41 | + <div class="suggested-links mt-8"> | ||
| 42 | + <p class="text-sm text-gray-500 mb-4">您可能想要访问:</p> | ||
| 43 | + <div class="space-y-2"> | ||
| 44 | + <van-cell | ||
| 45 | + title="首页" | ||
| 46 | + is-link | ||
| 47 | + @click="$router.push('/')" | ||
| 48 | + icon="home-o" | ||
| 49 | + /> | ||
| 50 | + <van-cell | ||
| 51 | + title="组件演示" | ||
| 52 | + is-link | ||
| 53 | + @click="$router.push('/demo')" | ||
| 54 | + icon="apps-o" | ||
| 55 | + /> | ||
| 56 | + <van-cell | ||
| 57 | + title="关于我们" | ||
| 58 | + is-link | ||
| 59 | + @click="$router.push('/about')" | ||
| 60 | + icon="info-o" | ||
| 61 | + /> | ||
| 62 | + </div> | ||
| 63 | + </div> | ||
| 64 | + </div> | ||
| 65 | + </div> | ||
| 66 | +</template> | ||
| 67 | + | ||
| 68 | +<script setup> | ||
| 69 | +import { useRouter } from 'vue-router' | ||
| 70 | + | ||
| 71 | +const router = useRouter() | ||
| 72 | + | ||
| 73 | +// 返回首页 | ||
| 74 | +const goHome = () => { | ||
| 75 | + router.push('/') | ||
| 76 | +} | ||
| 77 | + | ||
| 78 | +// 返回上一页 | ||
| 79 | +const goBack = () => { | ||
| 80 | + if (window.history.length > 1) { | ||
| 81 | + router.back() | ||
| 82 | + } else { | ||
| 83 | + router.push('/') | ||
| 84 | + } | ||
| 85 | +} | ||
| 86 | +</script> | ||
| 87 | + | ||
| 88 | +<style scoped> | ||
| 89 | +.not-found-container { | ||
| 90 | + min-height: 100vh; | ||
| 91 | + display: flex; | ||
| 92 | + align-items: center; | ||
| 93 | + justify-content: center; | ||
| 94 | + padding: 20px; | ||
| 95 | + background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); | ||
| 96 | +} | ||
| 97 | + | ||
| 98 | +.not-found-content { | ||
| 99 | + text-align: center; | ||
| 100 | + max-width: 400px; | ||
| 101 | + width: 100%; | ||
| 102 | +} | ||
| 103 | + | ||
| 104 | +.not-found-icon { | ||
| 105 | + margin-bottom: 2rem; | ||
| 106 | +} | ||
| 107 | + | ||
| 108 | +.suggested-links { | ||
| 109 | + background: white; | ||
| 110 | + border-radius: 12px; | ||
| 111 | + padding: 16px; | ||
| 112 | + box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1); | ||
| 113 | +} | ||
| 114 | +</style> | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
src/views/Splash.vue
0 → 100644
| 1 | +<template> | ||
| 2 | + <div class="splash-container" :class="{ 'fade-out': isLeaving }"> | ||
| 3 | + <div class="splash-content"> | ||
| 4 | + <!-- 背景装饰 --> | ||
| 5 | + <div class="bg-decoration"> | ||
| 6 | + <div class="lotus-pattern animate-float"></div> | ||
| 7 | + <div class="cloud-pattern animate-drift"></div> | ||
| 8 | + </div> | ||
| 9 | + | ||
| 10 | + <!-- 主要内容 --> | ||
| 11 | + <div class="main-content"> | ||
| 12 | + <!-- Logo区域 --> | ||
| 13 | + <div class="logo-section animate-fade-in-up"> | ||
| 14 | + <div class="logo-circle"> | ||
| 15 | + <div class="dharma-wheel animate-rotate"> | ||
| 16 | + <div class="wheel-center"></div> | ||
| 17 | + <div class="wheel-spokes"></div> | ||
| 18 | + </div> | ||
| 19 | + </div> | ||
| 20 | + <h1 class="app-title">三坛大戒</h1> | ||
| 21 | + <p class="app-subtitle">传承千年佛法 弘扬戒律精神</p> | ||
| 22 | + </div> | ||
| 23 | + | ||
| 24 | + <!-- 加载动画 --> | ||
| 25 | + <div class="loading-section animate-fade-in-up-delay"> | ||
| 26 | + <div class="loading-dots"> | ||
| 27 | + <span></span> | ||
| 28 | + <span></span> | ||
| 29 | + <span></span> | ||
| 30 | + </div> | ||
| 31 | + <p class="loading-text">正在加载...</p> | ||
| 32 | + </div> | ||
| 33 | + </div> | ||
| 34 | + | ||
| 35 | + <!-- 底部信息 --> | ||
| 36 | + <div class="footer-info animate-fade-in"> | ||
| 37 | + <p class="version">版本 1.0.0</p> | ||
| 38 | + </div> | ||
| 39 | + </div> | ||
| 40 | + </div> | ||
| 41 | +</template> | ||
| 42 | + | ||
| 43 | +<script setup> | ||
| 44 | +import { ref, onMounted } from 'vue' | ||
| 45 | +import { useRouter } from 'vue-router' | ||
| 46 | + | ||
| 47 | +const router = useRouter() | ||
| 48 | +const isLeaving = ref(false) | ||
| 49 | + | ||
| 50 | +onMounted(() => { | ||
| 51 | + // 3秒后开始离开动画,然后跳转到首页 | ||
| 52 | + setTimeout(() => { | ||
| 53 | + isLeaving.value = true | ||
| 54 | + // 等待淡出动画完成后跳转 | ||
| 55 | + setTimeout(() => { | ||
| 56 | + router.push('/home') | ||
| 57 | + }, 500) | ||
| 58 | + }, 2500) | ||
| 59 | +}) | ||
| 60 | +</script> | ||
| 61 | + | ||
| 62 | +<style scoped> | ||
| 63 | +.splash-container { | ||
| 64 | + position: fixed; | ||
| 65 | + top: 0; | ||
| 66 | + left: 0; | ||
| 67 | + right: 0; | ||
| 68 | + bottom: 0; | ||
| 69 | + background: linear-gradient(to bottom, #fffbeb, #fed7aa); | ||
| 70 | + background-image: | ||
| 71 | + radial-gradient(circle at 20% 20%, rgba(251, 191, 36, 0.1) 0%, transparent 50%), | ||
| 72 | + radial-gradient(circle at 80% 80%, rgba(245, 158, 11, 0.1) 0%, transparent 50%); | ||
| 73 | + transition: opacity 0.5s ease-out; | ||
| 74 | +} | ||
| 75 | + | ||
| 76 | +.splash-container.fade-out { | ||
| 77 | + opacity: 0; | ||
| 78 | +} | ||
| 79 | + | ||
| 80 | +.splash-content { | ||
| 81 | + position: relative; | ||
| 82 | + height: 100%; | ||
| 83 | + display: flex; | ||
| 84 | + flex-direction: column; | ||
| 85 | + justify-content: space-between; | ||
| 86 | + align-items: center; | ||
| 87 | + padding: 3rem 2rem; | ||
| 88 | +} | ||
| 89 | + | ||
| 90 | +.bg-decoration { | ||
| 91 | + position: absolute; | ||
| 92 | + top: 0; | ||
| 93 | + left: 0; | ||
| 94 | + right: 0; | ||
| 95 | + bottom: 0; | ||
| 96 | + overflow: hidden; | ||
| 97 | +} | ||
| 98 | + | ||
| 99 | +.lotus-pattern { | ||
| 100 | + position: absolute; | ||
| 101 | + top: 2.5rem; | ||
| 102 | + right: 2.5rem; | ||
| 103 | + width: 8rem; | ||
| 104 | + height: 8rem; | ||
| 105 | + opacity: 0.1; | ||
| 106 | + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Cpath d='M50 20c-5 0-10 5-10 15s5 15 10 15 10-5 10-15-5-15-10-15z' fill='%23f59e0b'/%3E%3Cpath d='M35 35c-5-3-12-1-15 5s-1 12 5 15 12 1 15-5 1-12-5-15z' fill='%23f59e0b'/%3E%3Cpath d='M65 35c5-3 12-1 15 5s1 12-5 15-12 1-15-5-1-12 5-15z' fill='%23f59e0b'/%3E%3C/svg%3E"); | ||
| 107 | +} | ||
| 108 | + | ||
| 109 | +.cloud-pattern { | ||
| 110 | + position: absolute; | ||
| 111 | + bottom: 5rem; | ||
| 112 | + left: 2.5rem; | ||
| 113 | + width: 6rem; | ||
| 114 | + height: 4rem; | ||
| 115 | + opacity: 0.05; | ||
| 116 | + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 60'%3E%3Cpath d='M20 40c-8 0-15-7-15-15s7-15 15-15c2 0 4 0 6 1 3-6 9-10 16-10s13 4 16 10c2-1 4-1 6-1 8 0 15 7 15 15s-7 15-15 15H20z' fill='%23f59e0b'/%3E%3C/svg%3E"); | ||
| 117 | +} | ||
| 118 | + | ||
| 119 | +.main-content { | ||
| 120 | + flex: 1; | ||
| 121 | + display: flex; | ||
| 122 | + flex-direction: column; | ||
| 123 | + justify-content: center; | ||
| 124 | + align-items: center; | ||
| 125 | +} | ||
| 126 | + | ||
| 127 | +.logo-section { | ||
| 128 | + text-align: center; | ||
| 129 | + margin-bottom: 4rem; | ||
| 130 | +} | ||
| 131 | + | ||
| 132 | +.logo-circle { | ||
| 133 | + position: relative; | ||
| 134 | + width: 8rem; | ||
| 135 | + height: 8rem; | ||
| 136 | + margin: 0 auto 2rem; | ||
| 137 | + border-radius: 50%; | ||
| 138 | + background: linear-gradient(135deg, #fbbf24, #f97316); | ||
| 139 | + box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25); | ||
| 140 | + animation: float 3s ease-in-out infinite; | ||
| 141 | +} | ||
| 142 | + | ||
| 143 | +.dharma-wheel { | ||
| 144 | + position: absolute; | ||
| 145 | + top: 1rem; | ||
| 146 | + left: 1rem; | ||
| 147 | + right: 1rem; | ||
| 148 | + bottom: 1rem; | ||
| 149 | + border-radius: 50%; | ||
| 150 | + background: white; | ||
| 151 | + box-shadow: inset 0 2px 4px 0 rgba(0, 0, 0, 0.06); | ||
| 152 | + display: flex; | ||
| 153 | + align-items: center; | ||
| 154 | + justify-content: center; | ||
| 155 | +} | ||
| 156 | + | ||
| 157 | +.wheel-center { | ||
| 158 | + width: 1rem; | ||
| 159 | + height: 1rem; | ||
| 160 | + border-radius: 50%; | ||
| 161 | + background: linear-gradient(135deg, #f59e0b, #ea580c); | ||
| 162 | +} | ||
| 163 | + | ||
| 164 | +.wheel-spokes { | ||
| 165 | + position: absolute; | ||
| 166 | + top: 0; | ||
| 167 | + left: 0; | ||
| 168 | + right: 0; | ||
| 169 | + bottom: 0; | ||
| 170 | +} | ||
| 171 | + | ||
| 172 | +.wheel-spokes::before, | ||
| 173 | +.wheel-spokes::after { | ||
| 174 | + content: ''; | ||
| 175 | + position: absolute; | ||
| 176 | + top: 50%; | ||
| 177 | + left: 50%; | ||
| 178 | + width: 4rem; | ||
| 179 | + height: 0.125rem; | ||
| 180 | + background: linear-gradient(to right, #f59e0b, #ea580c); | ||
| 181 | + transform: translate(-50%, -50%); | ||
| 182 | +} | ||
| 183 | + | ||
| 184 | +.wheel-spokes::before { | ||
| 185 | + transform: translate(-50%, -50%) rotate(45deg); | ||
| 186 | +} | ||
| 187 | + | ||
| 188 | +.wheel-spokes::after { | ||
| 189 | + transform: translate(-50%, -50%) rotate(-45deg); | ||
| 190 | +} | ||
| 191 | + | ||
| 192 | +.app-title { | ||
| 193 | + font-size: 2.25rem; | ||
| 194 | + font-weight: 700; | ||
| 195 | + color: #92400e; | ||
| 196 | + margin-bottom: 0.5rem; | ||
| 197 | + font-family: 'PingFang SC', 'Hiragino Sans GB', sans-serif; | ||
| 198 | +} | ||
| 199 | + | ||
| 200 | +.app-subtitle { | ||
| 201 | + font-size: 1.125rem; | ||
| 202 | + color: #b45309; | ||
| 203 | + opacity: 0.8; | ||
| 204 | +} | ||
| 205 | + | ||
| 206 | +.loading-section { | ||
| 207 | + text-align: center; | ||
| 208 | +} | ||
| 209 | + | ||
| 210 | +.loading-dots { | ||
| 211 | + display: flex; | ||
| 212 | + justify-content: center; | ||
| 213 | + gap: 0.5rem; | ||
| 214 | + margin-bottom: 1rem; | ||
| 215 | +} | ||
| 216 | + | ||
| 217 | +.loading-dots span { | ||
| 218 | + width: 0.75rem; | ||
| 219 | + height: 0.75rem; | ||
| 220 | + border-radius: 50%; | ||
| 221 | + background-color: #f59e0b; | ||
| 222 | + animation: bounce 1.4s ease-in-out infinite both; | ||
| 223 | +} | ||
| 224 | + | ||
| 225 | +.loading-dots span:nth-child(1) { | ||
| 226 | + animation-delay: -0.32s; | ||
| 227 | +} | ||
| 228 | + | ||
| 229 | +.loading-dots span:nth-child(2) { | ||
| 230 | + animation-delay: -0.16s; | ||
| 231 | +} | ||
| 232 | + | ||
| 233 | +.loading-text { | ||
| 234 | + color: #b45309; | ||
| 235 | + font-size: 0.875rem; | ||
| 236 | +} | ||
| 237 | + | ||
| 238 | +.footer-info { | ||
| 239 | + text-align: center; | ||
| 240 | +} | ||
| 241 | + | ||
| 242 | +.version { | ||
| 243 | + color: #d97706; | ||
| 244 | + font-size: 0.75rem; | ||
| 245 | + opacity: 0.6; | ||
| 246 | +} | ||
| 247 | + | ||
| 248 | +@keyframes float { | ||
| 249 | + 0%, 100% { | ||
| 250 | + transform: translateY(0px); | ||
| 251 | + } | ||
| 252 | + 50% { | ||
| 253 | + transform: translateY(-10px); | ||
| 254 | + } | ||
| 255 | +} | ||
| 256 | + | ||
| 257 | +@keyframes bounce { | ||
| 258 | + 0%, 80%, 100% { | ||
| 259 | + transform: scale(0); | ||
| 260 | + } | ||
| 261 | + 40% { | ||
| 262 | + transform: scale(1); | ||
| 263 | + } | ||
| 264 | +} | ||
| 265 | + | ||
| 266 | +@keyframes fadeInUp { | ||
| 267 | + from { | ||
| 268 | + opacity: 0; | ||
| 269 | + transform: translateY(30px); | ||
| 270 | + } | ||
| 271 | + to { | ||
| 272 | + opacity: 1; | ||
| 273 | + transform: translateY(0); | ||
| 274 | + } | ||
| 275 | +} | ||
| 276 | + | ||
| 277 | +@keyframes rotate { | ||
| 278 | + from { | ||
| 279 | + transform: rotate(0deg); | ||
| 280 | + } | ||
| 281 | + to { | ||
| 282 | + transform: rotate(360deg); | ||
| 283 | + } | ||
| 284 | +} | ||
| 285 | + | ||
| 286 | +@keyframes drift { | ||
| 287 | + 0%, 100% { | ||
| 288 | + transform: translateX(0px); | ||
| 289 | + } | ||
| 290 | + 50% { | ||
| 291 | + transform: translateX(20px); | ||
| 292 | + } | ||
| 293 | +} | ||
| 294 | + | ||
| 295 | +.animate-fade-in-up { | ||
| 296 | + animation: fadeInUp 0.8s ease-out; | ||
| 297 | +} | ||
| 298 | + | ||
| 299 | +.animate-fade-in-up-delay { | ||
| 300 | + animation: fadeInUp 0.8s ease-out 0.3s both; | ||
| 301 | +} | ||
| 302 | + | ||
| 303 | +.animate-fade-in { | ||
| 304 | + animation: fadeInUp 0.8s ease-out 0.6s both; | ||
| 305 | +} | ||
| 306 | + | ||
| 307 | +.animate-rotate { | ||
| 308 | + animation: rotate 8s linear infinite; | ||
| 309 | +} | ||
| 310 | + | ||
| 311 | +.animate-float { | ||
| 312 | + animation: float 3s ease-in-out infinite; | ||
| 313 | +} | ||
| 314 | + | ||
| 315 | +.animate-drift { | ||
| 316 | + animation: drift 4s ease-in-out infinite; | ||
| 317 | +} | ||
| 318 | +</style> | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
src/views/StudentDetail.vue
0 → 100644
This diff is collapsed. Click to expand it.
src/views/Students.vue
0 → 100644
This diff is collapsed. Click to expand it.
src/views/TeacherDetail.vue
0 → 100644
This diff is collapsed. Click to expand it.
src/views/Teachers.vue
0 → 100644
| 1 | +<template> | ||
| 2 | + <div class="page-container"> | ||
| 3 | + <!-- 导航栏 --> | ||
| 4 | + <van-nav-bar title="三师七证" left-arrow @click-left="$router.back()" class="custom-nav"> | ||
| 5 | + <template #right> | ||
| 6 | + <van-icon name="search" size="18" /> | ||
| 7 | + </template> | ||
| 8 | + </van-nav-bar> | ||
| 9 | + | ||
| 10 | + <!-- 内容区域 --> | ||
| 11 | + <div class="content-container"> | ||
| 12 | + <!-- 顶部说明 --> | ||
| 13 | + <div class="intro-section"> | ||
| 14 | + <div class="intro-card"> | ||
| 15 | + <div class="intro-icon">📜</div> | ||
| 16 | + <div class="intro-content"> | ||
| 17 | + <h3>三师七证</h3> | ||
| 18 | + <p>三师:得戒和尚、羯磨阿阇梨、教授阿阇梨<br>七证:七位证明师</p> | ||
| 19 | + </div> | ||
| 20 | + </div> | ||
| 21 | + </div> | ||
| 22 | + | ||
| 23 | + <!-- 筛选栏 --> | ||
| 24 | + <div class="filter-section"> | ||
| 25 | + <van-tabs v-model:active="activeTab" @change="handleTabChange" class="custom-tabs"> | ||
| 26 | + <van-tab title="全部" name="all"></van-tab> | ||
| 27 | + <van-tab title="三师" name="teachers"></van-tab> | ||
| 28 | + <van-tab title="七证" name="witnesses"></van-tab> | ||
| 29 | + </van-tabs> | ||
| 30 | + </div> | ||
| 31 | + | ||
| 32 | + <!-- 法师列表 --> | ||
| 33 | + <div class="teachers-list"> | ||
| 34 | + <div | ||
| 35 | + v-for="teacher in filteredTeachers" | ||
| 36 | + :key="teacher.id" | ||
| 37 | + class="teacher-card" | ||
| 38 | + @click="handleTeacherClick(teacher)" | ||
| 39 | + > | ||
| 40 | + <div class="teacher-avatar"> | ||
| 41 | + <img v-if="teacher.avatar" :src="teacher.avatar" :alt="teacher.name" /> | ||
| 42 | + <div v-else class="avatar-placeholder"> | ||
| 43 | + <span>{{ teacher.name.charAt(0) }}</span> | ||
| 44 | + </div> | ||
| 45 | + </div> | ||
| 46 | + | ||
| 47 | + <div class="teacher-info"> | ||
| 48 | + <div class="teacher-header"> | ||
| 49 | + <h4 class="teacher-name">{{ teacher.name }}</h4> | ||
| 50 | + <div class="teacher-role" :class="getRoleClass(teacher.role)"> | ||
| 51 | + {{ teacher.role }} | ||
| 52 | + </div> | ||
| 53 | + </div> | ||
| 54 | + | ||
| 55 | + <div class="teacher-details"> | ||
| 56 | + <p class="teacher-title">{{ teacher.title }}</p> | ||
| 57 | + <p class="teacher-temple">{{ teacher.temple }}</p> | ||
| 58 | + <div class="teacher-meta"> | ||
| 59 | + <span class="ordination-year">{{ teacher.ordinationYear }}年受戒</span> | ||
| 60 | + <span class="experience">{{ teacher.experience }}年戒腊</span> | ||
| 61 | + </div> | ||
| 62 | + </div> | ||
| 63 | + </div> | ||
| 64 | + | ||
| 65 | + <div class="teacher-actions"> | ||
| 66 | + <van-icon name="arrow" /> | ||
| 67 | + </div> | ||
| 68 | + </div> | ||
| 69 | + </div> | ||
| 70 | + | ||
| 71 | + <!-- 空状态 --> | ||
| 72 | + <van-empty v-if="filteredTeachers.length === 0" description="暂无相关法师信息" /> | ||
| 73 | + </div> | ||
| 74 | + </div> | ||
| 75 | +</template> | ||
| 76 | + | ||
| 77 | +<script setup> | ||
| 78 | +import { ref, computed } from 'vue' | ||
| 79 | +import { useRouter } from 'vue-router' | ||
| 80 | + | ||
| 81 | +const router = useRouter() | ||
| 82 | +const activeTab = ref('all') | ||
| 83 | + | ||
| 84 | +// 法师数据 | ||
| 85 | +const teachers = ref([ | ||
| 86 | + { | ||
| 87 | + id: 1, | ||
| 88 | + name: '慧明法师', | ||
| 89 | + title: '方丈', | ||
| 90 | + role: '得戒和尚', | ||
| 91 | + temple: '大觉寺', | ||
| 92 | + ordinationYear: 1985, | ||
| 93 | + experience: 39, | ||
| 94 | + avatar: null, | ||
| 95 | + type: 'teacher' | ||
| 96 | + }, | ||
| 97 | + { | ||
| 98 | + id: 2, | ||
| 99 | + name: '智慧法师', | ||
| 100 | + title: '首座', | ||
| 101 | + role: '羯磨阿阇梨', | ||
| 102 | + temple: '大觉寺', | ||
| 103 | + ordinationYear: 1990, | ||
| 104 | + experience: 34, | ||
| 105 | + avatar: null, | ||
| 106 | + type: 'teacher' | ||
| 107 | + }, | ||
| 108 | + { | ||
| 109 | + id: 3, | ||
| 110 | + name: '觉悟法师', | ||
| 111 | + title: '监院', | ||
| 112 | + role: '教授阿阇梨', | ||
| 113 | + temple: '大觉寺', | ||
| 114 | + ordinationYear: 1992, | ||
| 115 | + experience: 32, | ||
| 116 | + avatar: null, | ||
| 117 | + type: 'teacher' | ||
| 118 | + }, | ||
| 119 | + { | ||
| 120 | + id: 4, | ||
| 121 | + name: '慈悲法师', | ||
| 122 | + title: '知客', | ||
| 123 | + role: '证明师', | ||
| 124 | + temple: '大觉寺', | ||
| 125 | + ordinationYear: 1995, | ||
| 126 | + experience: 29, | ||
| 127 | + avatar: null, | ||
| 128 | + type: 'witness' | ||
| 129 | + }, | ||
| 130 | + { | ||
| 131 | + id: 5, | ||
| 132 | + name: '般若法师', | ||
| 133 | + title: '维那', | ||
| 134 | + role: '证明师', | ||
| 135 | + temple: '大觉寺', | ||
| 136 | + ordinationYear: 1998, | ||
| 137 | + experience: 26, | ||
| 138 | + avatar: null, | ||
| 139 | + type: 'witness' | ||
| 140 | + }, | ||
| 141 | + { | ||
| 142 | + id: 6, | ||
| 143 | + name: '禅定法师', | ||
| 144 | + title: '典座', | ||
| 145 | + role: '证明师', | ||
| 146 | + temple: '大觉寺', | ||
| 147 | + ordinationYear: 2000, | ||
| 148 | + experience: 24, | ||
| 149 | + avatar: null, | ||
| 150 | + type: 'witness' | ||
| 151 | + }, | ||
| 152 | + { | ||
| 153 | + id: 7, | ||
| 154 | + name: '精进法师', | ||
| 155 | + title: '书记', | ||
| 156 | + role: '证明师', | ||
| 157 | + temple: '大觉寺', | ||
| 158 | + ordinationYear: 2002, | ||
| 159 | + experience: 22, | ||
| 160 | + avatar: null, | ||
| 161 | + type: 'witness' | ||
| 162 | + }, | ||
| 163 | + { | ||
| 164 | + id: 8, | ||
| 165 | + name: '持戒法师', | ||
| 166 | + title: '库头', | ||
| 167 | + role: '证明师', | ||
| 168 | + temple: '大觉寺', | ||
| 169 | + ordinationYear: 2005, | ||
| 170 | + experience: 19, | ||
| 171 | + avatar: null, | ||
| 172 | + type: 'witness' | ||
| 173 | + }, | ||
| 174 | + { | ||
| 175 | + id: 9, | ||
| 176 | + name: '忍辱法师', | ||
| 177 | + title: '僧值', | ||
| 178 | + role: '证明师', | ||
| 179 | + temple: '大觉寺', | ||
| 180 | + ordinationYear: 2008, | ||
| 181 | + experience: 16, | ||
| 182 | + avatar: null, | ||
| 183 | + type: 'witness' | ||
| 184 | + }, | ||
| 185 | + { | ||
| 186 | + id: 10, | ||
| 187 | + name: '布施法师', | ||
| 188 | + title: '衣钵', | ||
| 189 | + role: '证明师', | ||
| 190 | + temple: '大觉寺', | ||
| 191 | + ordinationYear: 2010, | ||
| 192 | + experience: 14, | ||
| 193 | + avatar: null, | ||
| 194 | + type: 'witness' | ||
| 195 | + } | ||
| 196 | +]) | ||
| 197 | + | ||
| 198 | +// 过滤后的法师列表 | ||
| 199 | +const filteredTeachers = computed(() => { | ||
| 200 | + if (activeTab.value === 'all') { | ||
| 201 | + return teachers.value | ||
| 202 | + } else if (activeTab.value === 'teachers') { | ||
| 203 | + return teachers.value.filter(teacher => teacher.type === 'teacher') | ||
| 204 | + } else if (activeTab.value === 'witnesses') { | ||
| 205 | + return teachers.value.filter(teacher => teacher.type === 'witness') | ||
| 206 | + } | ||
| 207 | + return teachers.value | ||
| 208 | +}) | ||
| 209 | + | ||
| 210 | +// 获取角色样式类 | ||
| 211 | +const getRoleClass = (role) => { | ||
| 212 | + if (role.includes('和尚') || role.includes('阿阇梨')) { | ||
| 213 | + return 'role-teacher' | ||
| 214 | + } | ||
| 215 | + return 'role-witness' | ||
| 216 | +} | ||
| 217 | + | ||
| 218 | +// 处理标签切换 | ||
| 219 | +const handleTabChange = (name) => { | ||
| 220 | + activeTab.value = name | ||
| 221 | +} | ||
| 222 | + | ||
| 223 | +// 处理法师点击 | ||
| 224 | +const handleTeacherClick = (teacher) => { | ||
| 225 | + router.push(`/teachers/${teacher.id}`) | ||
| 226 | +} | ||
| 227 | +</script> | ||
| 228 | + | ||
| 229 | +<style scoped> | ||
| 230 | +.page-container { | ||
| 231 | + min-height: 100vh; | ||
| 232 | + background: #fafafa; | ||
| 233 | +} | ||
| 234 | + | ||
| 235 | +.custom-nav { | ||
| 236 | + background: linear-gradient(135deg, #fbbf24, #f97316); | ||
| 237 | + color: white; | ||
| 238 | +} | ||
| 239 | + | ||
| 240 | +.custom-nav :deep(.van-nav-bar__title) { | ||
| 241 | + color: white; | ||
| 242 | + font-weight: 600; | ||
| 243 | +} | ||
| 244 | + | ||
| 245 | +.custom-nav :deep(.van-icon) { | ||
| 246 | + color: white; | ||
| 247 | +} | ||
| 248 | + | ||
| 249 | +.content-container { | ||
| 250 | + padding-top: 46px; | ||
| 251 | +} | ||
| 252 | + | ||
| 253 | +.intro-section { | ||
| 254 | + padding: 16px; | ||
| 255 | +} | ||
| 256 | + | ||
| 257 | +.intro-card { | ||
| 258 | + background: white; | ||
| 259 | + border-radius: 12px; | ||
| 260 | + padding: 20px; | ||
| 261 | + display: flex; | ||
| 262 | + align-items: center; | ||
| 263 | + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); | ||
| 264 | +} | ||
| 265 | + | ||
| 266 | +.intro-icon { | ||
| 267 | + font-size: 32px; | ||
| 268 | + margin-right: 16px; | ||
| 269 | +} | ||
| 270 | + | ||
| 271 | +.intro-content h3 { | ||
| 272 | + font-size: 18px; | ||
| 273 | + font-weight: 600; | ||
| 274 | + color: #333; | ||
| 275 | + margin: 0 0 8px 0; | ||
| 276 | +} | ||
| 277 | + | ||
| 278 | +.intro-content p { | ||
| 279 | + font-size: 14px; | ||
| 280 | + color: #666; | ||
| 281 | + margin: 0; | ||
| 282 | + line-height: 1.5; | ||
| 283 | +} | ||
| 284 | + | ||
| 285 | +.filter-section { | ||
| 286 | + background: white; | ||
| 287 | + border-bottom: 1px solid #eee; | ||
| 288 | +} | ||
| 289 | + | ||
| 290 | +.custom-tabs :deep(.van-tab) { | ||
| 291 | + font-weight: 500; | ||
| 292 | +} | ||
| 293 | + | ||
| 294 | +.custom-tabs :deep(.van-tab--active) { | ||
| 295 | + color: #f59e0b; | ||
| 296 | +} | ||
| 297 | + | ||
| 298 | +.custom-tabs :deep(.van-tabs__line) { | ||
| 299 | + background: #f59e0b; | ||
| 300 | +} | ||
| 301 | + | ||
| 302 | +.teachers-list { | ||
| 303 | + padding: 16px; | ||
| 304 | +} | ||
| 305 | + | ||
| 306 | +.teacher-card { | ||
| 307 | + background: white; | ||
| 308 | + border-radius: 12px; | ||
| 309 | + padding: 16px; | ||
| 310 | + margin-bottom: 12px; | ||
| 311 | + display: flex; | ||
| 312 | + align-items: center; | ||
| 313 | + gap: 16px; | ||
| 314 | + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); | ||
| 315 | + cursor: pointer; | ||
| 316 | + transition: all 0.3s ease; | ||
| 317 | +} | ||
| 318 | + | ||
| 319 | +.teacher-card:hover { | ||
| 320 | + transform: translateY(-3px); | ||
| 321 | + box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15); | ||
| 322 | +} | ||
| 323 | + | ||
| 324 | +.teacher-card:active { | ||
| 325 | + transform: translateY(-1px); | ||
| 326 | + box-shadow: 0 3px 12px rgba(0, 0, 0, 0.12); | ||
| 327 | +} | ||
| 328 | + | ||
| 329 | +.teacher-avatar { | ||
| 330 | + width: 60px; | ||
| 331 | + height: 60px; | ||
| 332 | + border-radius: 50%; | ||
| 333 | + overflow: hidden; | ||
| 334 | + margin-right: 16px; | ||
| 335 | + flex-shrink: 0; | ||
| 336 | +} | ||
| 337 | + | ||
| 338 | +.teacher-avatar img { | ||
| 339 | + width: 100%; | ||
| 340 | + height: 100%; | ||
| 341 | + object-fit: cover; | ||
| 342 | +} | ||
| 343 | + | ||
| 344 | +.avatar-placeholder { | ||
| 345 | + width: 100%; | ||
| 346 | + height: 100%; | ||
| 347 | + background: linear-gradient(135deg, #fbbf24, #f97316); | ||
| 348 | + display: flex; | ||
| 349 | + align-items: center; | ||
| 350 | + justify-content: center; | ||
| 351 | + color: white; | ||
| 352 | + font-size: 24px; | ||
| 353 | + font-weight: 600; | ||
| 354 | +} | ||
| 355 | + | ||
| 356 | +.teacher-info { | ||
| 357 | + flex: 1; | ||
| 358 | +} | ||
| 359 | + | ||
| 360 | +.teacher-header { | ||
| 361 | + display: flex; | ||
| 362 | + align-items: center; | ||
| 363 | + justify-content: space-between; | ||
| 364 | + margin-bottom: 8px; | ||
| 365 | +} | ||
| 366 | + | ||
| 367 | +.teacher-name { | ||
| 368 | + font-size: 18px; | ||
| 369 | + font-weight: 600; | ||
| 370 | + color: #333; | ||
| 371 | + margin: 0; | ||
| 372 | +} | ||
| 373 | + | ||
| 374 | +.teacher-role { | ||
| 375 | + padding: 4px 8px; | ||
| 376 | + border-radius: 12px; | ||
| 377 | + font-size: 12px; | ||
| 378 | + font-weight: 500; | ||
| 379 | +} | ||
| 380 | + | ||
| 381 | +.role-teacher { | ||
| 382 | + background: linear-gradient(135deg, #fbbf24, #f97316); | ||
| 383 | + color: white; | ||
| 384 | +} | ||
| 385 | + | ||
| 386 | +.role-witness { | ||
| 387 | + background: #f0f9ff; | ||
| 388 | + color: #0369a1; | ||
| 389 | + border: 1px solid #bae6fd; | ||
| 390 | +} | ||
| 391 | + | ||
| 392 | +.teacher-details { | ||
| 393 | + space-y: 4px; | ||
| 394 | +} | ||
| 395 | + | ||
| 396 | +.teacher-title { | ||
| 397 | + font-size: 16px; | ||
| 398 | + color: #666; | ||
| 399 | + margin: 0 0 4px 0; | ||
| 400 | +} | ||
| 401 | + | ||
| 402 | +.teacher-temple { | ||
| 403 | + font-size: 14px; | ||
| 404 | + color: #999; | ||
| 405 | + margin: 0 0 8px 0; | ||
| 406 | +} | ||
| 407 | + | ||
| 408 | +.teacher-meta { | ||
| 409 | + display: flex; | ||
| 410 | + gap: 16px; | ||
| 411 | +} | ||
| 412 | + | ||
| 413 | +.ordination-year, | ||
| 414 | +.experience { | ||
| 415 | + font-size: 12px; | ||
| 416 | + color: #999; | ||
| 417 | + background: #f5f5f5; | ||
| 418 | + padding: 2px 6px; | ||
| 419 | + border-radius: 4px; | ||
| 420 | +} | ||
| 421 | + | ||
| 422 | +.teacher-actions { | ||
| 423 | + margin-left: 12px; | ||
| 424 | + color: #ccc; | ||
| 425 | +} | ||
| 426 | +</style> | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
src/views/Volunteers.vue
0 → 100644
This diff is collapsed. Click to expand it.
tailwind.config.js
0 → 100644
| 1 | +/** @type {import('tailwindcss').Config} */ | ||
| 2 | +export default { | ||
| 3 | + content: [ | ||
| 4 | + "./index.html", | ||
| 5 | + "./src/**/*.{vue,js,ts,jsx,tsx}", | ||
| 6 | + ], | ||
| 7 | + theme: { | ||
| 8 | + extend: { | ||
| 9 | + colors: { | ||
| 10 | + primary: '#1989fa', | ||
| 11 | + success: '#07c160', | ||
| 12 | + warning: '#ff976a', | ||
| 13 | + danger: '#ee0a24', | ||
| 14 | + }, | ||
| 15 | + fontSize: { | ||
| 16 | + 'xs': '10px', | ||
| 17 | + 'sm': '12px', | ||
| 18 | + 'base': '14px', | ||
| 19 | + 'lg': '16px', | ||
| 20 | + 'xl': '18px', | ||
| 21 | + '2xl': '20px', | ||
| 22 | + '3xl': '24px', | ||
| 23 | + } | ||
| 24 | + }, | ||
| 25 | + }, | ||
| 26 | + plugins: [], | ||
| 27 | + corePlugins: { | ||
| 28 | + preflight: false, // 禁用 Tailwind 的基础样式重置,避免与 Vant 冲突 | ||
| 29 | + } | ||
| 30 | +} | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
vite.config.js
0 → 100644
| 1 | +import { defineConfig } from 'vite' | ||
| 2 | +import vue from '@vitejs/plugin-vue' | ||
| 3 | +import AutoImport from 'unplugin-auto-import/vite' | ||
| 4 | +import Components from 'unplugin-vue-components/vite' | ||
| 5 | +import { VantResolver } from 'unplugin-vue-components/resolvers' | ||
| 6 | +import path from 'path' | ||
| 7 | +import tailwindcss from 'tailwindcss' | ||
| 8 | +import autoprefixer from 'autoprefixer' | ||
| 9 | +import postcsspxtoviewport from 'postcss-px-to-viewport' | ||
| 10 | + | ||
| 11 | +// https://vitejs.dev/config/ | ||
| 12 | +export default defineConfig({ | ||
| 13 | + plugins: [ | ||
| 14 | + vue(), | ||
| 15 | + AutoImport({ | ||
| 16 | + imports: ['vue', 'vue-router'], | ||
| 17 | + dts: 'src/auto-imports.d.ts', | ||
| 18 | + eslintrc: { | ||
| 19 | + enabled: true | ||
| 20 | + } | ||
| 21 | + }), | ||
| 22 | + Components({ | ||
| 23 | + resolvers: [VantResolver()], | ||
| 24 | + dts: 'src/components.d.ts' | ||
| 25 | + }) | ||
| 26 | + ], | ||
| 27 | + resolve: { | ||
| 28 | + alias: { | ||
| 29 | + '@': path.resolve(__dirname, 'src'), | ||
| 30 | + '@components': path.resolve(__dirname, 'src/components'), | ||
| 31 | + '@utils': path.resolve(__dirname, 'src/utils'), | ||
| 32 | + '@api': path.resolve(__dirname, 'src/api'), | ||
| 33 | + '@assets': path.resolve(__dirname, 'src/assets'), | ||
| 34 | + '@views': path.resolve(__dirname, 'src/views'), | ||
| 35 | + '@stores': path.resolve(__dirname, 'src/stores') | ||
| 36 | + } | ||
| 37 | + }, | ||
| 38 | + css: { | ||
| 39 | + postcss: { | ||
| 40 | + plugins: [ | ||
| 41 | + tailwindcss, | ||
| 42 | + autoprefixer, | ||
| 43 | + postcsspxtoviewport({ | ||
| 44 | + unitToConvert: 'px', | ||
| 45 | + viewportWidth: 375, | ||
| 46 | + unitPrecision: 6, | ||
| 47 | + propList: ['*'], | ||
| 48 | + viewportUnit: 'vw', | ||
| 49 | + fontViewportUnit: 'vw', | ||
| 50 | + selectorBlackList: ['ignore-'], | ||
| 51 | + minPixelValue: 1, | ||
| 52 | + mediaQuery: true, | ||
| 53 | + replace: true, | ||
| 54 | + exclude: [], | ||
| 55 | + landscape: false | ||
| 56 | + }) | ||
| 57 | + ] | ||
| 58 | + } | ||
| 59 | + }, | ||
| 60 | + server: { | ||
| 61 | + host: '0.0.0.0', | ||
| 62 | + port: 3000, | ||
| 63 | + open: true, | ||
| 64 | + proxy: { | ||
| 65 | + '/api': { | ||
| 66 | + target: 'http://localhost:8080', | ||
| 67 | + changeOrigin: true, | ||
| 68 | + rewrite: (path) => path.replace(/^\/api/, '') | ||
| 69 | + } | ||
| 70 | + } | ||
| 71 | + }, | ||
| 72 | + build: { | ||
| 73 | + outDir: 'dist', | ||
| 74 | + assetsDir: 'static', | ||
| 75 | + rollupOptions: { | ||
| 76 | + output: { | ||
| 77 | + chunkFileNames: 'static/js/[name]-[hash].js', | ||
| 78 | + entryFileNames: 'static/js/[name]-[hash].js', | ||
| 79 | + assetFileNames: 'static/[ext]/[name]-[hash].[ext]' | ||
| 80 | + } | ||
| 81 | + } | ||
| 82 | + } | ||
| 83 | +}) | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
三坛大戒资料/设计稿/01启动页.png
0 → 100644
1.37 MB
三坛大戒资料/设计稿/02首页 .png
0 → 100644
3.08 MB
三坛大戒资料/设计稿/03三師七證.png
0 → 100644
3.72 MB
三坛大戒资料/设计稿/04三師七證– 详情.png
0 → 100644
1.38 MB
三坛大戒资料/设计稿/05义工.png
0 → 100644
1.98 MB
三坛大戒资料/设计稿/06戒子.png
0 → 100644
391 KB
三坛大戒资料/设计稿/07戒子 –详情.png
0 → 100644
858 KB
三坛大戒资料/设计稿/08新闻详情.png
0 → 100644
1.52 MB
-
Please register or login to post a comment