hookehuyr

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

- 完成启动页面设计和动画效果
- 实现首页布局和功能模块
- 添加三师七证、义工、戒子等核心页面
- 集成Vue3 + Vite + Vant UI框架
- 实现页面路由和导航功能
- 添加响应式设计和交互动画
# 开发环境配置
# API 基础地址
VITE_API_BASE_URL=http://localhost:3000/api
# 是否开启 Mock
VITE_USE_MOCK=true
# 开发服务器端口
VITE_PORT=5173
# 是否自动打开浏览器
VITE_OPEN=true
\ No newline at end of file
# 生产环境配置
# API 基础地址
VITE_API_BASE_URL=https://api.yourdomain.com
# 是否开启 Mock
VITE_USE_MOCK=false
# 构建输出目录
VITE_OUTPUT_DIR=dist
# 静态资源目录
VITE_ASSETS_DIR=assets
\ No newline at end of file
{
"globals": {
"Component": true,
"ComponentPublicInstance": true,
"ComputedRef": true,
"DirectiveBinding": true,
"EffectScope": true,
"ExtractDefaultPropTypes": true,
"ExtractPropTypes": true,
"ExtractPublicPropTypes": true,
"InjectionKey": true,
"MaybeRef": true,
"MaybeRefOrGetter": true,
"PropType": true,
"Ref": true,
"VNode": true,
"WritableComputedRef": true,
"computed": true,
"createApp": true,
"customRef": true,
"defineAsyncComponent": true,
"defineComponent": true,
"effectScope": true,
"getCurrentInstance": true,
"getCurrentScope": true,
"h": true,
"inject": true,
"isProxy": true,
"isReactive": true,
"isReadonly": true,
"isRef": true,
"markRaw": true,
"nextTick": true,
"onActivated": true,
"onBeforeMount": true,
"onBeforeRouteLeave": true,
"onBeforeRouteUpdate": true,
"onBeforeUnmount": true,
"onBeforeUpdate": true,
"onDeactivated": true,
"onErrorCaptured": true,
"onMounted": true,
"onRenderTracked": true,
"onRenderTriggered": true,
"onScopeDispose": true,
"onServerPrefetch": true,
"onUnmounted": true,
"onUpdated": true,
"onWatcherCleanup": true,
"provide": true,
"reactive": true,
"readonly": true,
"ref": true,
"resolveComponent": true,
"shallowReactive": true,
"shallowReadonly": true,
"shallowRef": true,
"toRaw": true,
"toRef": true,
"toRefs": true,
"toValue": true,
"triggerRef": true,
"unref": true,
"useAttrs": true,
"useCssModule": true,
"useCssVars": true,
"useId": true,
"useLink": true,
"useModel": true,
"useRoute": true,
"useRouter": true,
"useSlots": true,
"useTemplateRef": true,
"watch": true,
"watchEffect": true,
"watchPostEffect": true,
"watchSyncEffect": true
}
}
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# Environment variables
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# Build outputs
build/
coverage/
# Cache
.cache/
.parcel-cache/
.eslintcache
# History
.history/
\ No newline at end of file
# H5 Vite Template
基于 Vue 3 + Vite + Vant 4 的移动端 H5 项目模板
## 特性
- ⚡️ **Vite** - 极速的开发体验
- 🖖 **Vue 3** - 渐进式 JavaScript 框架
- 📱 **Vant 4** - 轻量、可靠的移动端组件库
- 🎨 **Tailwind CSS** - 原子化 CSS 框架
- 📦 **Pinia** - 符合直觉的 Vue.js 状态管理库
- 🛣️ **Vue Router** - Vue.js 官方路由
- 📡 **Axios** - 基于 Promise 的 HTTP 客户端
- 🔧 **ESLint** - 代码质量检查
- 📐 **PostCSS** - CSS 后处理器
- 📱 **移动端适配** - 基于 postcss-px-to-viewport 的移动端适配方案
## 目录结构
```
h5_vite_template/
├── public/ # 静态资源
├── src/
│ ├── api/ # API 接口
│ ├── assets/ # 资源文件
│ ├── components/ # 通用组件
│ ├── router/ # 路由配置
│ ├── stores/ # 状态管理
│ ├── utils/ # 工具函数
│ ├── views/ # 页面组件
│ ├── App.vue # 根组件
│ ├── main.js # 入口文件
│ └── style.css # 全局样式
├── .env # 环境变量
├── .env.development # 开发环境变量
├── .env.production # 生产环境变量
├── .gitignore # Git 忽略文件
├── index.html # HTML 模板
├── package.json # 项目配置
├── postcss.config.js # PostCSS 配置
├── tailwind.config.js # Tailwind CSS 配置
└── vite.config.js # Vite 配置
```
## 快速开始
### 安装依赖
```bash
npm install
# 或
yarn install
# 或
pnpm install
```
### 开发
```bash
npm run dev
# 或
yarn dev
# 或
pnpm dev
```
### 构建
```bash
npm run build
# 或
yarn build
# 或
pnpm build
```
### 预览
```bash
npm run preview
# 或
yarn preview
# 或
pnpm preview
```
## 配置说明
### 环境变量
项目支持多环境配置,通过 `.env` 文件进行管理:
- `.env` - 所有环境的默认配置
- `.env.development` - 开发环境配置
- `.env.production` - 生产环境配置
### 移动端适配
项目使用 `postcss-px-to-viewport` 进行移动端适配,设计稿基准为 375px。
### 路由配置
路由配置位于 `src/router/index.js`,支持:
- 路由懒加载
- 路由守卫
- 页面标题设置
- 滚动行为控制
### 状态管理
使用 Pinia 进行状态管理,store 文件位于 `src/stores/` 目录。
### API 请求
API 请求基于 Axios 封装,配置文件位于 `src/utils/request.js`,支持:
- 请求/响应拦截器
- 自动 Loading
- 错误处理
- Token 自动携带
## 组件说明
### 页面组件
- **Home** - 首页,展示轮播图、菜单网格、通知栏等
- **About** - 关于页面,展示项目信息和功能特性
- **Profile** - 个人中心,展示用户信息和功能菜单
- **Demo** - 组件演示页面,展示 Vant 组件使用示例
- **NotFound** - 404 页面
### 通用组件
- **LoadingSpinner** - 加载动画组件
- **EmptyState** - 空状态组件
## 开发规范
### 代码风格
项目使用 ESLint 进行代码质量检查,请遵循以下规范:
- 使用 2 空格缩进
- 使用单引号
- 行末不加分号
- 组件名使用 PascalCase
- 文件名使用 kebab-case
### Git 提交规范
建议使用以下格式进行 Git 提交:
```
<type>(<scope>): <subject>
<body>
<footer>
```
类型说明:
- `feat`: 新功能
- `fix`: 修复 bug
- `docs`: 文档更新
- `style`: 代码格式调整
- `refactor`: 代码重构
- `test`: 测试相关
- `chore`: 构建过程或辅助工具的变动
## 部署
### 构建产物
执行 `npm run build` 后,构建产物将输出到 `dist` 目录。
### 静态部署
构建产物可以部署到任何静态文件服务器,如:
- Nginx
- Apache
- Vercel
- Netlify
- GitHub Pages
### 注意事项
1. 如果部署到子路径,需要在 `vite.config.js` 中配置 `base` 选项
2. 确保服务器支持 History 模式的路由
3. 生产环境需要配置正确的 API 地址
## 浏览器支持
- Chrome >= 87
- Firefox >= 78
- Safari >= 14
- iOS Safari >= 14.4
- Android Browser >= 87
## 许可证
MIT License
\ No newline at end of file
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no" />
<meta name="format-detection" content="telephone=no" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
<title>H5 Vite Template</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
\ No newline at end of file
This diff could not be displayed because it is too large.
{
"name": "stdj-h5",
"description": "三坛大戒",
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"start": "vite --host 0.0.0.0",
"build": "vite build",
"build-watch": "vite build --watch",
"serve": "vite preview",
"lint": "eslint . --ext vue,js,jsx,cjs,mjs --fix --ignore-path .gitignore"
},
"dependencies": {
"@vant/area-data": "^1.3.1",
"@vant/touch-emulator": "^1.4.0",
"@vueuse/core": "^10.7.2",
"axios": "^1.6.7",
"dayjs": "^1.11.10",
"js-cookie": "^3.0.5",
"lodash": "^4.17.21",
"pinia": "^2.1.7",
"vant": "^4.9.1",
"vue": "^3.4.15",
"vue-router": "^4.2.5"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.0.3",
"autoprefixer": "^10.4.17",
"postcss": "^8.4.35",
"postcss-px-to-viewport": "^1.1.1",
"tailwindcss": "^3.4.1",
"unplugin-auto-import": "^0.17.5",
"unplugin-vue-components": "^0.26.0",
"vite": "^5.1.0"
}
}
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
'postcss-px-to-viewport': {
unitToConvert: 'px',
viewportWidth: 375,
unitPrecision: 6,
propList: ['*'],
viewportUnit: 'vw',
fontViewportUnit: 'vw',
selectorBlackList: ['ignore-'],
minPixelValue: 1,
mediaQuery: true,
replace: true,
exclude: [],
landscape: false
}
}
}
\ No newline at end of file
<template>
<div id="app">
<router-view />
</div>
</template>
<script setup>
// 这里可以添加全局逻辑
</script>
<style>
/* 全局样式已在 style.css 中定义 */
</style>
\ No newline at end of file
import request from '@/utils/request'
// 用户相关 API
export const userApi = {
// 获取用户信息
getUserInfo: () => request.get('/user/info'),
// 更新用户信息
updateUserInfo: (data) => request.put('/user/info', data),
// 用户登录
login: (data) => request.post('/user/login', data),
// 用户注册
register: (data) => request.post('/user/register', data),
// 用户登出
logout: () => request.post('/user/logout')
}
// 通用 API
export const commonApi = {
// 上传文件
upload: (file) => {
const formData = new FormData()
formData.append('file', file)
return request.post('/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
})
},
// 获取配置信息
getConfig: () => request.get('/config'),
// 发送验证码
sendSms: (phone) => request.post('/sms/send', { phone })
}
// 示例 API
export const demoApi = {
// 获取列表数据
getList: (params) => request.get('/demo/list', { params }),
// 获取详情
getDetail: (id) => request.get(`/demo/${id}`),
// 创建数据
create: (data) => request.post('/demo', data),
// 更新数据
update: (id, data) => request.put(`/demo/${id}`, data),
// 删除数据
delete: (id) => request.delete(`/demo/${id}`)
}
// 导出所有 API
export default {
userApi,
commonApi,
demoApi
}
\ No newline at end of file
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
export {}
declare global {
const EffectScope: typeof import('vue')['EffectScope']
const computed: typeof import('vue')['computed']
const createApp: typeof import('vue')['createApp']
const customRef: typeof import('vue')['customRef']
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
const defineComponent: typeof import('vue')['defineComponent']
const effectScope: typeof import('vue')['effectScope']
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
const getCurrentScope: typeof import('vue')['getCurrentScope']
const h: typeof import('vue')['h']
const inject: typeof import('vue')['inject']
const isProxy: typeof import('vue')['isProxy']
const isReactive: typeof import('vue')['isReactive']
const isReadonly: typeof import('vue')['isReadonly']
const isRef: typeof import('vue')['isRef']
const markRaw: typeof import('vue')['markRaw']
const nextTick: typeof import('vue')['nextTick']
const onActivated: typeof import('vue')['onActivated']
const onBeforeMount: typeof import('vue')['onBeforeMount']
const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave']
const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate']
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
const onDeactivated: typeof import('vue')['onDeactivated']
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
const onMounted: typeof import('vue')['onMounted']
const onRenderTracked: typeof import('vue')['onRenderTracked']
const onRenderTriggered: typeof import('vue')['onRenderTriggered']
const onScopeDispose: typeof import('vue')['onScopeDispose']
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
const onUnmounted: typeof import('vue')['onUnmounted']
const onUpdated: typeof import('vue')['onUpdated']
const onWatcherCleanup: typeof import('vue')['onWatcherCleanup']
const provide: typeof import('vue')['provide']
const reactive: typeof import('vue')['reactive']
const readonly: typeof import('vue')['readonly']
const ref: typeof import('vue')['ref']
const resolveComponent: typeof import('vue')['resolveComponent']
const shallowReactive: typeof import('vue')['shallowReactive']
const shallowReadonly: typeof import('vue')['shallowReadonly']
const shallowRef: typeof import('vue')['shallowRef']
const toRaw: typeof import('vue')['toRaw']
const toRef: typeof import('vue')['toRef']
const toRefs: typeof import('vue')['toRefs']
const toValue: typeof import('vue')['toValue']
const triggerRef: typeof import('vue')['triggerRef']
const unref: typeof import('vue')['unref']
const useAttrs: typeof import('vue')['useAttrs']
const useCssModule: typeof import('vue')['useCssModule']
const useCssVars: typeof import('vue')['useCssVars']
const useId: typeof import('vue')['useId']
const useLink: typeof import('vue-router')['useLink']
const useModel: typeof import('vue')['useModel']
const useRoute: typeof import('vue-router')['useRoute']
const useRouter: typeof import('vue-router')['useRouter']
const useSlots: typeof import('vue')['useSlots']
const useTemplateRef: typeof import('vue')['useTemplateRef']
const watch: typeof import('vue')['watch']
const watchEffect: typeof import('vue')['watchEffect']
const watchPostEffect: typeof import('vue')['watchPostEffect']
const watchSyncEffect: typeof import('vue')['watchSyncEffect']
}
// for type re-export
declare global {
// @ts-ignore
export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
import('vue')
}
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
export {}
declare module 'vue' {
export interface GlobalComponents {
EmptyState: typeof import('./components/EmptyState.vue')['default']
LoadingSpinner: typeof import('./components/LoadingSpinner.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
VanBadge: typeof import('vant/es')['Badge']
VanButton: typeof import('vant/es')['Button']
VanCard: typeof import('vant/es')['Card']
VanCell: typeof import('vant/es')['Cell']
VanCellGroup: typeof import('vant/es')['CellGroup']
VanCheckbox: typeof import('vant/es')['Checkbox']
VanCollapse: typeof import('vant/es')['Collapse']
VanCollapseItem: typeof import('vant/es')['CollapseItem']
VanField: typeof import('vant/es')['Field']
VanForm: typeof import('vant/es')['Form']
VanGrid: typeof import('vant/es')['Grid']
VanGridItem: typeof import('vant/es')['GridItem']
VanIcon: typeof import('vant/es')['Icon']
VanImage: typeof import('vant/es')['Image']
VanNavBar: typeof import('vant/es')['NavBar']
VanNoticeBar: typeof import('vant/es')['NoticeBar']
VanProgress: typeof import('vant/es')['Progress']
VanRate: typeof import('vant/es')['Rate']
VanSidebar: typeof import('vant/es')['Sidebar']
VanSidebarItem: typeof import('vant/es')['SidebarItem']
VanStep: typeof import('vant/es')['Step']
VanSteps: typeof import('vant/es')['Steps']
VanSwipe: typeof import('vant/es')['Swipe']
VanSwipeItem: typeof import('vant/es')['SwipeItem']
VanSwitch: typeof import('vant/es')['Switch']
VanTab: typeof import('vant/es')['Tab']
VanTabbar: typeof import('vant/es')['Tabbar']
VanTabbarItem: typeof import('vant/es')['TabbarItem']
VanTabs: typeof import('vant/es')['Tabs']
VanTag: typeof import('vant/es')['Tag']
}
}
<template>
<div class="empty-state">
<div class="empty-icon">
<van-empty
:image="image"
:image-size="imageSize"
:description="description"
>
<template v-if="$slots.image" #image>
<slot name="image" />
</template>
<template v-if="$slots.description" #description>
<slot name="description" />
</template>
<template v-if="showAction" #default>
<van-button
:type="actionType"
:size="actionSize"
round
@click="handleAction"
>
{{ actionText }}
</van-button>
</template>
</van-empty>
</div>
</div>
</template>
<script setup>
import { defineEmits } from 'vue'
const props = defineProps({
// 图片类型
image: {
type: String,
default: 'default'
},
// 图片大小
imageSize: {
type: [Number, String],
default: 160
},
// 描述文字
description: {
type: String,
default: '暂无数据'
},
// 是否显示操作按钮
showAction: {
type: Boolean,
default: false
},
// 按钮文字
actionText: {
type: String,
default: '重新加载'
},
// 按钮类型
actionType: {
type: String,
default: 'primary'
},
// 按钮大小
actionSize: {
type: String,
default: 'normal'
}
})
const emit = defineEmits(['action'])
const handleAction = () => {
emit('action')
}
</script>
<style scoped>
.empty-state {
padding: 40px 20px;
text-align: center;
}
</style>
\ No newline at end of file
<template>
<div class="loading-spinner" :class="{ 'loading-overlay': overlay }">
<div class="spinner-container">
<div class="spinner" :style="{ width: size + 'px', height: size + 'px' }">
<div class="spinner-inner" :style="{ borderColor: color }"></div>
</div>
<p v-if="text" class="loading-text" :style="{ color: textColor }">{{ text }}</p>
</div>
</div>
</template>
<script setup>
defineProps({
// 是否显示遮罩层
overlay: {
type: Boolean,
default: false
},
// 加载器大小
size: {
type: Number,
default: 40
},
// 加载器颜色
color: {
type: String,
default: '#1989fa'
},
// 加载文本
text: {
type: String,
default: ''
},
// 文本颜色
textColor: {
type: String,
default: '#969799'
}
})
</script>
<style scoped>
.loading-spinner {
display: flex;
align-items: center;
justify-content: center;
}
.loading-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.9);
z-index: 9999;
}
.spinner-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
}
.spinner {
position: relative;
}
.spinner-inner {
width: 100%;
height: 100%;
border: 3px solid transparent;
border-top-color: currentColor;
border-radius: 50%;
animation: spin 1s linear infinite;
}
.loading-text {
font-size: 14px;
margin: 0;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>
\ No newline at end of file
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import router from './router'
import App from './App.vue'
// 引入样式
import './style.css'
import 'vant/lib/index.css'
// 引入 Vant 组件(按需引入会通过 unplugin-vue-components 自动处理)
// 这里只需要引入一些全局配置的组件
import { ConfigProvider, Toast, Dialog, Notify, ImagePreview } from 'vant'
const app = createApp(App)
const pinia = createPinia()
// 全局配置
app.config.globalProperties.$toast = Toast
app.config.globalProperties.$dialog = Dialog
app.config.globalProperties.$notify = Notify
app.config.globalProperties.$imagePreview = ImagePreview
// 使用插件
app.use(pinia)
app.use(router)
app.use(ConfigProvider)
// 挂载应用
app.mount('#app')
\ No newline at end of file
/*
* @Date: 2025-10-30 10:29:15
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-10-30 10:29:24
* @FilePath: /itomix/h5_vite_template/src/router/index.js
* @Description: 文件描述
*/
import { createRouter, createWebHistory } from 'vue-router'
import Home from '../views/Home.vue'
const routes = [
{
path: '/',
name: 'Splash',
component: () => import('../views/Splash.vue')
},
{
path: '/home',
name: 'Home',
component: Home
},
{
path: '/teachers',
name: 'Teachers',
component: () => import('../views/Teachers.vue')
},
{
path: '/teachers/:id',
name: 'TeacherDetail',
component: () => import('../views/TeacherDetail.vue')
},
{
path: '/volunteers',
name: 'Volunteers',
component: () => import('../views/Volunteers.vue')
},
{
path: '/students',
name: 'Students',
component: () => import('../views/Students.vue')
},
{
path: '/students/:id',
name: 'StudentDetail',
component: () => import('../views/StudentDetail.vue')
},
{
path: '/news/:id',
name: 'NewsDetail',
component: () => import('../views/NewsDetail.vue')
},
{
path: '/:pathMatch(.*)*',
name: 'NotFound',
component: () => import('../views/NotFound.vue')
}
]
const router = createRouter({
history: createWebHistory(),
routes,
scrollBehavior(to, from, savedPosition) {
if (savedPosition) {
return savedPosition
} else {
return { top: 0 }
}
}
})
// 路由守卫
router.beforeEach((to, from, next) => {
// 设置页面标题
if (to.meta.title) {
document.title = to.meta.title
}
// 这里可以添加权限验证逻辑
next()
})
router.afterEach(() => {
// 路由跳转后的逻辑
})
export default router
\ No newline at end of file
/*
* @Date: 2025-10-30 10:30:17
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-10-30 10:30:26
* @FilePath: /itomix/h5_vite_template/src/stores/user.js
* @Description: 文件描述
*/
import { defineStore } from 'pinia'
import { userApi } from '@/api'
export const useUserStore = defineStore('user', {
state: () => ({
userInfo: null,
token: localStorage.getItem('token') || '',
isLogin: false
}),
getters: {
// 获取用户名
username: (state) => state.userInfo?.username || '',
// 获取用户头像
avatar: (state) => state.userInfo?.avatar || '',
// 是否已登录
hasLogin: (state) => !!state.token && !!state.userInfo
},
actions: {
// 设置 token
setToken(token) {
this.token = token
localStorage.setItem('token', token)
},
// 清除 token
clearToken() {
this.token = ''
localStorage.removeItem('token')
},
// 设置用户信息
setUserInfo(userInfo) {
this.userInfo = userInfo
this.isLogin = true
},
// 清除用户信息
clearUserInfo() {
this.userInfo = null
this.isLogin = false
},
// 登录
async login(loginData) {
const response = await userApi.login(loginData)
const { token, userInfo } = response.data
this.setToken(token)
this.setUserInfo(userInfo)
return response
},
// 获取用户信息
async getUserInfo() {
try {
const response = await userApi.getUserInfo()
this.setUserInfo(response.data)
return response
} catch (error) {
// 如果获取用户信息失败,清除本地存储
this.logout()
throw error
}
},
// 登出
async logout() {
try {
await userApi.logout()
} catch (error) {
console.error('登出失败:', error)
} finally {
this.clearToken()
this.clearUserInfo()
}
}
}
})
\ No newline at end of file
@tailwind base;
@tailwind components;
@tailwind utilities;
/* 全局样式 */
* {
box-sizing: border-box;
}
html, body {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background-color: #f7f8fa;
}
#app {
min-height: 100vh;
}
/* 自定义工具类 */
.safe-area-inset-top {
padding-top: constant(safe-area-inset-top);
padding-top: env(safe-area-inset-top);
}
.safe-area-inset-bottom {
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
}
/* 覆盖 Vant 样式 */
.van-nav-bar {
background-color: #fff;
}
.van-nav-bar__title {
color: #323233;
}
/* 页面容器 */
.page-container {
min-height: 100vh;
background-color: #f7f8fa;
}
.content-container {
padding: 16px;
}
\ No newline at end of file
import axios from 'axios'
import { Toast } from 'vant'
// 创建 axios 实例
const request = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
timeout: 10000,
headers: {
'Content-Type': 'application/json;charset=UTF-8'
}
})
// 请求拦截器
request.interceptors.request.use(
(config) => {
// 在发送请求之前做些什么
// 添加 token
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
// 显示加载提示
Toast.loading({
message: '加载中...',
forbidClick: true,
duration: 0
})
return config
},
(error) => {
// 对请求错误做些什么
Toast.clear()
return Promise.reject(error)
}
)
// 响应拦截器
request.interceptors.response.use(
(response) => {
// 对响应数据做点什么
Toast.clear()
const { data } = response
// 根据后端接口规范处理响应
if (data.code === 200 || data.success) {
return data
} else {
// 业务错误
Toast.fail(data.message || '请求失败')
return Promise.reject(new Error(data.message || '请求失败'))
}
},
(error) => {
// 对响应错误做点什么
Toast.clear()
let message = '网络错误'
if (error.response) {
const { status, data } = error.response
switch (status) {
case 401:
message = '未授权,请重新登录'
// 清除 token 并跳转到登录页
localStorage.removeItem('token')
// router.push('/login')
break
case 403:
message = '拒绝访问'
break
case 404:
message = '请求地址出错'
break
case 408:
message = '请求超时'
break
case 500:
message = '服务器内部错误'
break
case 501:
message = '服务未实现'
break
case 502:
message = '网关错误'
break
case 503:
message = '服务不可用'
break
case 504:
message = '网关超时'
break
case 505:
message = 'HTTP版本不受支持'
break
default:
message = data?.message || `连接错误${status}`
}
} else if (error.code === 'ECONNABORTED') {
message = '请求超时'
} else if (error.message) {
message = error.message
}
Toast.fail(message)
return Promise.reject(error)
}
)
export default request
\ No newline at end of file
<template>
<div class="page-container">
<!-- 顶部导航栏 -->
<van-nav-bar title="三坛大戒" fixed class="custom-nav">
<template #left>
<div class="nav-logo">
<div class="dharma-symbol">☸</div>
</div>
</template>
<template #right>
<van-icon name="search" size="18" />
</template>
</van-nav-bar>
<!-- 内容区域 -->
<div class="content-container">
<!-- 轮播图 -->
<van-swipe class="hero-swipe" :autoplay="4000" indicator-color="#f59e0b">
<van-swipe-item v-for="(banner, index) in banners" :key="index">
<div class="swipe-item" :style="{ backgroundImage: `linear-gradient(135deg, ${banner.gradient})` }">
<div class="banner-content">
<h3 class="banner-title">{{ banner.title }}</h3>
<p class="banner-desc">{{ banner.desc }}</p>
<div class="banner-decoration">
<div class="lotus-icon">🪷</div>
</div>
</div>
</div>
</van-swipe-item>
</van-swipe>
<!-- 功能菜单 -->
<div class="menu-section">
<h2 class="section-title">戒律修学</h2>
<van-grid :column-num="2" :gutter="16" class="main-menu">
<van-grid-item
v-for="item in mainMenuItems"
:key="item.id"
@click="handleMenuClick(item)"
class="menu-item animate-on-scroll"
>
<div class="menu-content">
<div class="menu-icon" :style="{ background: item.gradient }">
<span class="icon-text">{{ item.icon }}</span>
</div>
<div class="menu-text">
<h4>{{ item.title }}</h4>
<p>{{ item.desc }}</p>
</div>
</div>
</van-grid-item>
</van-grid>
</div>
<!-- 通知公告 -->
<div class="notice-section">
<van-notice-bar
left-icon="volume-o"
:text="noticeText"
color="#b45309"
background="#fffbeb"
class="custom-notice"
/>
</div>
<!-- 最新资讯 -->
<div class="news-section">
<div class="section-header">
<h2 class="section-title">最新资讯</h2>
<van-button type="primary" size="mini" plain @click="$router.push('/news')">
更多
</van-button>
</div>
<div class="news-list">
<div
v-for="news in newsList"
:key="news.id"
class="news-item animate-on-scroll"
@click="handleNewsClick(news)"
>
<div class="news-content">
<h4 class="news-title">{{ news.title }}</h4>
<p class="news-summary">{{ news.summary }}</p>
<div class="news-meta">
<span class="news-date">{{ news.date }}</span>
<span class="news-views">{{ news.views }}人阅读</span>
</div>
</div>
<div class="news-image" v-if="news.image">
<img :src="news.image" :alt="news.title" />
</div>
</div>
</div>
</div>
</div>
<!-- 底部导航 -->
<van-tabbar v-model="activeTab" fixed class="custom-tabbar">
<van-tabbar-item icon="home-o" to="/">
首页
</van-tabbar-item>
<van-tabbar-item icon="certificate" to="/teachers">
三师七证
</van-tabbar-item>
<van-tabbar-item icon="friends-o" to="/volunteers">
义工
</van-tabbar-item>
<van-tabbar-item icon="user-o" to="/disciples">
戒子
</van-tabbar-item>
</van-tabbar>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import { useRouter } from 'vue-router'
import { Toast } from 'vant'
const router = useRouter()
const activeTab = ref(0)
// 轮播图数据
const banners = ref([
{
title: '三坛大戒',
desc: '传承千年戒律 弘扬佛法精神',
gradient: '#fbbf24, #f97316'
},
{
title: '戒律修学',
desc: '严持戒律 清净身心',
gradient: '#f59e0b, #ea580c'
},
{
title: '法师开示',
desc: '聆听法音 增长智慧',
gradient: '#d97706, #c2410c'
}
])
// 主要菜单项
const mainMenuItems = ref([
{
id: 1,
icon: '👨‍🏫',
title: '三师七证',
desc: '查看法师资质',
gradient: 'linear-gradient(135deg, #fbbf24, #f97316)',
path: '/teachers'
},
{
id: 2,
icon: '🙏',
title: '义工服务',
desc: '参与义工活动',
gradient: 'linear-gradient(135deg, #f59e0b, #ea580c)',
path: '/volunteers'
},
{
id: 3,
icon: '👤',
title: '戒子信息',
desc: '戒子档案管理',
gradient: 'linear-gradient(135deg, #d97706, #c2410c)',
path: '/disciples'
},
{
id: 4,
icon: '📰',
title: '最新资讯',
desc: '佛教新闻动态',
gradient: 'linear-gradient(135deg, #b45309, #9a3412)',
path: '/news'
}
])
// 通知文本
const noticeText = ref('欢迎参加三坛大戒法会!请各位戒子严格遵守戒律,精进修学。')
// 新闻列表
const newsList = ref([
{
id: 1,
title: '三坛大戒法会圆满举行',
summary: '本次法会共有200余位戒子参加,法师们为戒子们传授了沙弥戒、比丘戒等重要戒律...',
date: '2024-01-15',
views: 1250,
image: null
},
{
id: 2,
title: '戒律学习心得分享',
summary: '戒子们分享了在戒律学习过程中的心得体会,互相交流修学经验...',
date: '2024-01-12',
views: 890,
image: null
},
{
id: 3,
title: '义工服务活动通知',
summary: '寺院将于本周末举行义工服务活动,欢迎各位善信踊跃参与...',
date: '2024-01-10',
views: 650,
image: null
}
])
// 处理菜单点击
const handleMenuClick = (item) => {
if (item.path) {
router.push(item.path)
} else {
Toast('功能开发中...')
}
}
// 处理新闻点击
const handleNewsClick = (news) => {
router.push(`/news/${news.id}`)
}
// 滚动动画观察器
let observer = null
const initScrollAnimation = () => {
observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add('animate-visible')
}
})
}, {
threshold: 0.1,
rootMargin: '0px 0px -50px 0px'
})
// 观察所有需要动画的元素
const animateElements = document.querySelectorAll('.animate-on-scroll')
animateElements.forEach((el) => {
observer.observe(el)
})
}
onMounted(() => {
// 延迟初始化滚动动画,确保DOM已渲染
setTimeout(() => {
initScrollAnimation()
}, 100)
})
onUnmounted(() => {
if (observer) {
observer.disconnect()
}
})
</script>
<style scoped>
.page-container {
min-height: 100vh;
background: #fafafa;
}
.custom-nav {
background: linear-gradient(135deg, #fbbf24, #f97316);
color: white;
}
.custom-nav :deep(.van-nav-bar__title) {
color: white;
font-weight: 600;
}
.nav-logo {
display: flex;
align-items: center;
}
.dharma-symbol {
font-size: 20px;
color: white;
}
.content-container {
padding-top: 46px;
padding-bottom: 50px;
}
.hero-swipe {
height: 180px;
margin: 16px;
border-radius: 12px;
overflow: hidden;
}
.swipe-item {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
position: relative;
color: white;
}
.banner-content {
text-align: center;
z-index: 2;
}
.banner-title {
font-size: 24px;
font-weight: 700;
margin-bottom: 8px;
}
.banner-desc {
font-size: 14px;
opacity: 0.9;
}
.banner-decoration {
position: absolute;
top: 20px;
right: 20px;
opacity: 0.3;
}
.lotus-icon {
font-size: 32px;
}
.menu-section {
padding: 16px;
}
.section-title {
font-size: 18px;
font-weight: 600;
color: #333;
margin-bottom: 16px;
display: flex;
align-items: center;
}
.section-title::before {
content: '';
width: 4px;
height: 18px;
background: linear-gradient(135deg, #fbbf24, #f97316);
margin-right: 8px;
border-radius: 2px;
}
.main-menu :deep(.van-grid-item__content) {
padding: 0;
background: transparent;
}
.menu-item {
background: white;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
cursor: pointer;
transition: all 0.3s ease;
opacity: 0;
transform: translateY(30px);
}
.menu-item:hover {
transform: translateY(-5px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
}
.menu-item:active {
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.12);
}
.menu-item.animate-visible {
opacity: 1;
transform: translateY(0);
}
.menu-item:nth-child(1).animate-visible {
transition-delay: 0.1s;
}
.menu-item:nth-child(2).animate-visible {
transition-delay: 0.2s;
}
.menu-item:nth-child(3).animate-visible {
transition-delay: 0.3s;
}
.menu-item:nth-child(4).animate-visible {
transition-delay: 0.4s;
}
.menu-content {
padding: 20px 16px;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
}
.menu-icon {
width: 48px;
height: 48px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 12px;
}
.icon-text {
font-size: 24px;
}
.menu-text h4 {
font-size: 16px;
font-weight: 600;
color: #333;
margin: 0 0 4px 0;
}
.menu-text p {
font-size: 12px;
color: #666;
margin: 0;
}
.notice-section {
padding: 0 16px 16px;
}
.custom-notice {
border-radius: 8px;
border: 1px solid #f59e0b;
}
.news-section {
padding: 0 16px 16px;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.news-list {
background: white;
border-radius: 12px;
overflow: hidden;
}
.news-item {
display: flex;
padding: 16px;
border-bottom: 1px solid #f5f5f5;
cursor: pointer;
transition: all 0.3s ease;
opacity: 0;
transform: translateX(-30px);
}
.news-item:last-child {
border-bottom: none;
}
.news-item:hover {
transform: translateX(0) translateY(-2px);
background-color: #f9f9f9;
}
.news-item:active {
transform: translateX(0) translateY(0);
background-color: #f5f5f5;
}
.news-item.animate-visible {
opacity: 1;
transform: translateX(0);
}
.news-item:nth-child(1).animate-visible {
transition-delay: 0.1s;
}
.news-item:nth-child(2).animate-visible {
transition-delay: 0.2s;
}
.news-item:nth-child(3).animate-visible {
transition-delay: 0.3s;
}
.news-content {
flex: 1;
}
.news-title {
font-size: 16px;
font-weight: 600;
color: #333;
margin: 0 0 8px 0;
line-height: 1.4;
}
.news-summary {
font-size: 14px;
color: #666;
margin: 0 0 8px 0;
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.news-meta {
display: flex;
gap: 16px;
}
.news-date,
.news-views {
font-size: 12px;
color: #999;
}
.news-image {
width: 80px;
height: 60px;
margin-left: 12px;
border-radius: 6px;
overflow: hidden;
}
.news-image img {
width: 100%;
height: 100%;
object-fit: cover;
}
.custom-tabbar {
background: white;
border-top: 1px solid #eee;
}
.custom-tabbar :deep(.van-tabbar-item--active) {
color: #f59e0b;
}
</style>
\ No newline at end of file
<template>
<div class="page-container">
<!-- 导航栏 -->
<van-nav-bar title="新闻详情" left-arrow @click-left="$router.back()" class="custom-nav">
<template #right>
<van-icon name="share-o" size="18" @click="handleShare" />
</template>
</van-nav-bar>
<!-- 内容区域 -->
<div class="content-container">
<!-- 文章头部 -->
<div class="article-header">
<h1 class="article-title">{{ article.title }}</h1>
<div class="article-meta">
<div class="meta-info">
<span class="publish-date">{{ article.publishDate }}</span>
<span class="author">{{ article.author }}</span>
</div>
<div class="article-stats">
<span class="view-count">
<van-icon name="eye-o" />
{{ article.viewCount }}
</span>
<span class="like-count" @click="handleLike">
<van-icon :name="article.isLiked ? 'like' : 'like-o'" :color="article.isLiked ? '#ff6b6b' : '#999'" />
{{ article.likeCount }}
</span>
</div>
</div>
<!-- 标签 -->
<div class="article-tags" v-if="article.tags && article.tags.length">
<van-tag
v-for="tag in article.tags"
:key="tag"
type="primary"
size="small"
class="article-tag"
>
{{ tag }}
</van-tag>
</div>
</div>
<!-- 文章封面 -->
<div class="article-cover" v-if="article.coverImage">
<img :src="article.coverImage" :alt="article.title" />
</div>
<!-- 文章内容 -->
<div class="article-content">
<div class="content-text" v-html="article.content"></div>
</div>
<!-- 文章底部 -->
<div class="article-footer">
<div class="footer-actions">
<div class="action-item" @click="handleLike">
<van-icon :name="article.isLiked ? 'like' : 'like-o'" :color="article.isLiked ? '#ff6b6b' : '#666'" size="20" />
<span>{{ article.isLiked ? '已赞' : '点赞' }}</span>
</div>
<div class="action-item" @click="handleCollect">
<van-icon :name="article.isCollected ? 'star' : 'star-o'" :color="article.isCollected ? '#fbbf24' : '#666'" size="20" />
<span>{{ article.isCollected ? '已收藏' : '收藏' }}</span>
</div>
<div class="action-item" @click="handleShare">
<van-icon name="share-o" color="#666" size="20" />
<span>分享</span>
</div>
<div class="action-item" @click="handleComment">
<van-icon name="chat-o" color="#666" size="20" />
<span>评论</span>
</div>
</div>
</div>
<!-- 相关文章 -->
<div class="related-articles" v-if="relatedArticles.length">
<h3 class="section-title">相关文章</h3>
<div class="related-list">
<div
v-for="related in relatedArticles"
:key="related.id"
class="related-item"
@click="handleRelatedClick(related)"
>
<div class="related-cover">
<img v-if="related.coverImage" :src="related.coverImage" :alt="related.title" />
<div v-else class="cover-placeholder">
<van-icon name="photo-o" size="24" color="#ccc" />
</div>
</div>
<div class="related-info">
<h4 class="related-title">{{ related.title }}</h4>
<div class="related-meta">
<span class="related-date">{{ related.publishDate }}</span>
<span class="related-views">{{ related.viewCount }}阅读</span>
</div>
</div>
</div>
</div>
</div>
<!-- 评论区 -->
<div class="comments-section">
<h3 class="section-title">评论 ({{ comments.length }})</h3>
<!-- 评论输入 -->
<div class="comment-input">
<van-field
v-model="commentText"
type="textarea"
placeholder="写下你的评论..."
rows="3"
maxlength="500"
show-word-limit
/>
<van-button
type="primary"
size="small"
@click="handleSubmitComment"
:disabled="!commentText.trim()"
class="submit-btn"
>
发表
</van-button>
</div>
<!-- 评论列表 -->
<div class="comments-list">
<div
v-for="comment in comments"
:key="comment.id"
class="comment-item"
>
<div class="comment-avatar">
<img v-if="comment.avatar" :src="comment.avatar" :alt="comment.author" />
<div v-else class="avatar-placeholder">
<span>{{ comment.author.charAt(0) }}</span>
</div>
</div>
<div class="comment-content">
<div class="comment-header">
<span class="comment-author">{{ comment.author }}</span>
<span class="comment-date">{{ comment.date }}</span>
</div>
<p class="comment-text">{{ comment.content }}</p>
<div class="comment-actions">
<span class="comment-like" @click="handleCommentLike(comment)">
<van-icon :name="comment.isLiked ? 'like' : 'like-o'" :color="comment.isLiked ? '#ff6b6b' : '#999'" size="14" />
{{ comment.likeCount || '' }}
</span>
<span class="comment-reply" @click="handleCommentReply(comment)">回复</span>
</div>
</div>
</div>
</div>
<!-- 空状态 -->
<van-empty v-if="comments.length === 0" description="暂无评论,快来抢沙发吧~" />
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { Toast } from 'vant'
const route = useRoute()
const router = useRouter()
const commentText = ref('')
// 文章数据
const article = ref({
id: 1,
title: '三坛大戒传戒法会圆满举行',
content: `
<p>2024年春季三坛大戒传戒法会于今日在本寺圆满举行。此次法会历时21天,共有来自全国各地的68位戒子参加,在诸位戒师的慈悲教导下,圆满受持三坛大戒。</p>
<p>三坛大戒是佛教出家众必须受持的重要戒律,包括沙弥戒、比丘戒和菩萨戒三个层次。通过严格的戒律学习和实践,戒子们不仅在戒律知识上有了深入的理解,更在修行品格上得到了显著提升。</p>
<p>在传戒期间,戒子们每日早起晚睡,精进修学。从戒律理论的学习到实际生活中的践行,从个人修持到集体共修,每一个环节都体现了佛教戒律的庄严与神圣。</p>
<p>本次传戒法会得到了十方善信的大力护持,义工菩萨们日夜辛劳,为法会的顺利进行提供了有力保障。在此向所有护持法会的善信表示衷心感谢。</p>
<p>愿诸位新戒比丘能够严持净戒,精进修行,早证菩提,广度众生。愿佛法久住,法轮常转,众生离苦得乐。</p>
`,
author: '释智慧法师',
publishDate: '2024-01-15',
viewCount: 1256,
likeCount: 89,
isLiked: false,
isCollected: false,
coverImage: null,
tags: ['三坛大戒', '传戒法会', '戒律', '修行']
})
// 相关文章
const relatedArticles = ref([
{
id: 2,
title: '戒律学习的重要性与方法',
publishDate: '2024-01-10',
viewCount: 856,
coverImage: null
},
{
id: 3,
title: '沙弥戒的基本要求与实践',
publishDate: '2024-01-08',
viewCount: 642,
coverImage: null
},
{
id: 4,
title: '比丘戒的深层含义解析',
publishDate: '2024-01-05',
viewCount: 789,
coverImage: null
}
])
// 评论数据
const comments = ref([
{
id: 1,
author: '慧心居士',
content: '随喜赞叹!三坛大戒的传承是佛教的重要传统,愿新戒比丘们都能严持净戒,精进修行。',
date: '2024-01-15 14:30',
likeCount: 12,
isLiked: false,
avatar: null
},
{
id: 2,
author: '觉悟行者',
content: '感恩诸位法师的慈悲教导,戒律是修行的基础,希望能有更多这样的法会。',
date: '2024-01-15 15:45',
likeCount: 8,
isLiked: false,
avatar: null
},
{
id: 3,
author: '清净莲花',
content: '阿弥陀佛!看到这样的法会真是法喜充满,愿佛法久住世间。',
date: '2024-01-15 16:20',
likeCount: 5,
isLiked: false,
avatar: null
}
])
// 处理点赞
const handleLike = () => {
article.value.isLiked = !article.value.isLiked
if (article.value.isLiked) {
article.value.likeCount++
Toast('点赞成功')
} else {
article.value.likeCount--
Toast('取消点赞')
}
}
// 处理收藏
const handleCollect = () => {
article.value.isCollected = !article.value.isCollected
if (article.value.isCollected) {
Toast('收藏成功')
} else {
Toast('取消收藏')
}
}
// 处理分享
const handleShare = () => {
Toast('分享功能开发中...')
}
// 处理评论
const handleComment = () => {
document.querySelector('.comment-input').scrollIntoView({ behavior: 'smooth' })
}
// 处理相关文章点击
const handleRelatedClick = (related) => {
router.push(`/news/${related.id}`)
}
// 提交评论
const handleSubmitComment = () => {
if (!commentText.value.trim()) {
Toast('请输入评论内容')
return
}
const newComment = {
id: Date.now(),
author: '当前用户',
content: commentText.value.trim(),
date: new Date().toLocaleString('zh-CN'),
likeCount: 0,
isLiked: false,
avatar: null
}
comments.value.unshift(newComment)
commentText.value = ''
Toast('评论发表成功')
}
// 处理评论点赞
const handleCommentLike = (comment) => {
comment.isLiked = !comment.isLiked
if (comment.isLiked) {
comment.likeCount = (comment.likeCount || 0) + 1
} else {
comment.likeCount = Math.max(0, (comment.likeCount || 0) - 1)
}
}
// 处理评论回复
const handleCommentReply = (comment) => {
Toast('回复功能开发中...')
}
// 组件挂载时加载数据
onMounted(() => {
const articleId = route.params.id
console.log('Loading article data for ID:', articleId)
// 这里可以根据ID加载具体的文章数据
})
</script>
<style scoped>
.page-container {
min-height: 100vh;
background: #fafafa;
}
.custom-nav {
background: linear-gradient(135deg, #3b82f6, #1d4ed8);
color: white;
}
.custom-nav :deep(.van-nav-bar__title) {
color: white;
font-weight: 600;
}
.custom-nav :deep(.van-icon) {
color: white;
}
.content-container {
padding-top: 46px;
}
.article-header {
background: white;
padding: 20px;
margin-bottom: 12px;
}
.article-title {
font-size: 24px;
font-weight: 700;
color: #333;
line-height: 1.4;
margin: 0 0 16px 0;
}
.article-meta {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.meta-info {
display: flex;
gap: 16px;
}
.publish-date,
.author {
font-size: 14px;
color: #666;
}
.article-stats {
display: flex;
gap: 16px;
}
.view-count,
.like-count {
display: flex;
align-items: center;
gap: 4px;
font-size: 14px;
color: #666;
cursor: pointer;
}
.article-tags {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.article-tag {
background: #e0f2fe !important;
color: #0277bd !important;
border: 1px solid #81d4fa !important;
}
.article-cover {
background: white;
padding: 0 20px 20px;
margin-bottom: 12px;
}
.article-cover img {
width: 100%;
border-radius: 8px;
}
.article-content {
background: white;
padding: 20px;
margin-bottom: 12px;
}
.content-text {
font-size: 16px;
line-height: 1.8;
color: #333;
}
.content-text :deep(p) {
margin: 0 0 16px 0;
text-indent: 2em;
}
.content-text :deep(p:last-child) {
margin-bottom: 0;
}
.article-footer {
background: white;
padding: 20px;
margin-bottom: 12px;
border-top: 1px solid #eee;
}
.footer-actions {
display: flex;
justify-content: space-around;
}
.action-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
cursor: pointer;
padding: 12px;
border-radius: 8px;
transition: background-color 0.2s;
}
.action-item:active {
background: #f5f5f5;
}
.action-item span {
font-size: 12px;
color: #666;
}
.related-articles {
background: white;
padding: 20px;
margin-bottom: 12px;
}
.section-title {
font-size: 18px;
font-weight: 600;
color: #333;
margin: 0 0 16px 0;
padding-left: 4px;
border-left: 4px solid #3b82f6;
}
.related-list {
space-y: 12px;
}
.related-item {
display: flex;
gap: 12px;
padding: 12px;
border-radius: 8px;
cursor: pointer;
transition: background-color 0.2s;
margin-bottom: 12px;
}
.related-item:active {
background: #f5f5f5;
}
.related-cover {
width: 80px;
height: 60px;
border-radius: 6px;
overflow: hidden;
flex-shrink: 0;
background: #f5f5f5;
}
.related-cover img {
width: 100%;
height: 100%;
object-fit: cover;
}
.cover-placeholder {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: #f5f5f5;
}
.related-info {
flex: 1;
}
.related-title {
font-size: 16px;
font-weight: 500;
color: #333;
margin: 0 0 8px 0;
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.related-meta {
display: flex;
gap: 12px;
}
.related-date,
.related-views {
font-size: 12px;
color: #999;
}
.comments-section {
background: white;
padding: 20px;
margin-bottom: 20px;
}
.comment-input {
margin-bottom: 20px;
position: relative;
}
.submit-btn {
position: absolute;
bottom: 12px;
right: 12px;
background: #3b82f6 !important;
border-color: #3b82f6 !important;
}
.comments-list {
space-y: 16px;
}
.comment-item {
display: flex;
gap: 12px;
padding: 16px 0;
border-bottom: 1px solid #f0f0f0;
}
.comment-item:last-child {
border-bottom: none;
}
.comment-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
overflow: hidden;
flex-shrink: 0;
}
.comment-avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
.avatar-placeholder {
width: 100%;
height: 100%;
background: linear-gradient(135deg, #3b82f6, #1d4ed8);
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 16px;
font-weight: 600;
}
.comment-content {
flex: 1;
}
.comment-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.comment-author {
font-size: 14px;
font-weight: 500;
color: #333;
}
.comment-date {
font-size: 12px;
color: #999;
}
.comment-text {
font-size: 14px;
color: #666;
line-height: 1.6;
margin: 0 0 12px 0;
}
.comment-actions {
display: flex;
gap: 16px;
}
.comment-like,
.comment-reply {
display: flex;
align-items: center;
gap: 4px;
font-size: 12px;
color: #999;
cursor: pointer;
}
.comment-reply:hover {
color: #3b82f6;
}
</style>
\ No newline at end of file
<template>
<div class="not-found-container">
<div class="not-found-content">
<!-- 404 图标 -->
<div class="not-found-icon">
<svg viewBox="0 0 200 200" class="w-32 h-32 text-gray-300">
<circle cx="100" cy="100" r="90" fill="none" stroke="currentColor" stroke-width="4"/>
<text x="100" y="120" text-anchor="middle" font-size="48" font-weight="bold" fill="currentColor">404</text>
</svg>
</div>
<!-- 错误信息 -->
<div class="not-found-text">
<h1 class="text-2xl font-bold text-gray-800 mb-2">页面不存在</h1>
<p class="text-gray-600 mb-8">抱歉,您访问的页面不存在或已被删除</p>
</div>
<!-- 操作按钮 -->
<div class="not-found-actions space-y-4">
<van-button
type="primary"
round
block
@click="goHome"
class="mb-4"
>
返回首页
</van-button>
<van-button
plain
round
block
@click="goBack"
>
返回上页
</van-button>
</div>
<!-- 建议链接 -->
<div class="suggested-links mt-8">
<p class="text-sm text-gray-500 mb-4">您可能想要访问:</p>
<div class="space-y-2">
<van-cell
title="首页"
is-link
@click="$router.push('/')"
icon="home-o"
/>
<van-cell
title="组件演示"
is-link
@click="$router.push('/demo')"
icon="apps-o"
/>
<van-cell
title="关于我们"
is-link
@click="$router.push('/about')"
icon="info-o"
/>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { useRouter } from 'vue-router'
const router = useRouter()
// 返回首页
const goHome = () => {
router.push('/')
}
// 返回上一页
const goBack = () => {
if (window.history.length > 1) {
router.back()
} else {
router.push('/')
}
}
</script>
<style scoped>
.not-found-container {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
}
.not-found-content {
text-align: center;
max-width: 400px;
width: 100%;
}
.not-found-icon {
margin-bottom: 2rem;
}
.suggested-links {
background: white;
border-radius: 12px;
padding: 16px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
}
</style>
\ No newline at end of file
<template>
<div class="splash-container" :class="{ 'fade-out': isLeaving }">
<div class="splash-content">
<!-- 背景装饰 -->
<div class="bg-decoration">
<div class="lotus-pattern animate-float"></div>
<div class="cloud-pattern animate-drift"></div>
</div>
<!-- 主要内容 -->
<div class="main-content">
<!-- Logo区域 -->
<div class="logo-section animate-fade-in-up">
<div class="logo-circle">
<div class="dharma-wheel animate-rotate">
<div class="wheel-center"></div>
<div class="wheel-spokes"></div>
</div>
</div>
<h1 class="app-title">三坛大戒</h1>
<p class="app-subtitle">传承千年佛法 弘扬戒律精神</p>
</div>
<!-- 加载动画 -->
<div class="loading-section animate-fade-in-up-delay">
<div class="loading-dots">
<span></span>
<span></span>
<span></span>
</div>
<p class="loading-text">正在加载...</p>
</div>
</div>
<!-- 底部信息 -->
<div class="footer-info animate-fade-in">
<p class="version">版本 1.0.0</p>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
const isLeaving = ref(false)
onMounted(() => {
// 3秒后开始离开动画,然后跳转到首页
setTimeout(() => {
isLeaving.value = true
// 等待淡出动画完成后跳转
setTimeout(() => {
router.push('/home')
}, 500)
}, 2500)
})
</script>
<style scoped>
.splash-container {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(to bottom, #fffbeb, #fed7aa);
background-image:
radial-gradient(circle at 20% 20%, rgba(251, 191, 36, 0.1) 0%, transparent 50%),
radial-gradient(circle at 80% 80%, rgba(245, 158, 11, 0.1) 0%, transparent 50%);
transition: opacity 0.5s ease-out;
}
.splash-container.fade-out {
opacity: 0;
}
.splash-content {
position: relative;
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
padding: 3rem 2rem;
}
.bg-decoration {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
overflow: hidden;
}
.lotus-pattern {
position: absolute;
top: 2.5rem;
right: 2.5rem;
width: 8rem;
height: 8rem;
opacity: 0.1;
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");
}
.cloud-pattern {
position: absolute;
bottom: 5rem;
left: 2.5rem;
width: 6rem;
height: 4rem;
opacity: 0.05;
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");
}
.main-content {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.logo-section {
text-align: center;
margin-bottom: 4rem;
}
.logo-circle {
position: relative;
width: 8rem;
height: 8rem;
margin: 0 auto 2rem;
border-radius: 50%;
background: linear-gradient(135deg, #fbbf24, #f97316);
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
animation: float 3s ease-in-out infinite;
}
.dharma-wheel {
position: absolute;
top: 1rem;
left: 1rem;
right: 1rem;
bottom: 1rem;
border-radius: 50%;
background: white;
box-shadow: inset 0 2px 4px 0 rgba(0, 0, 0, 0.06);
display: flex;
align-items: center;
justify-content: center;
}
.wheel-center {
width: 1rem;
height: 1rem;
border-radius: 50%;
background: linear-gradient(135deg, #f59e0b, #ea580c);
}
.wheel-spokes {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
.wheel-spokes::before,
.wheel-spokes::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 4rem;
height: 0.125rem;
background: linear-gradient(to right, #f59e0b, #ea580c);
transform: translate(-50%, -50%);
}
.wheel-spokes::before {
transform: translate(-50%, -50%) rotate(45deg);
}
.wheel-spokes::after {
transform: translate(-50%, -50%) rotate(-45deg);
}
.app-title {
font-size: 2.25rem;
font-weight: 700;
color: #92400e;
margin-bottom: 0.5rem;
font-family: 'PingFang SC', 'Hiragino Sans GB', sans-serif;
}
.app-subtitle {
font-size: 1.125rem;
color: #b45309;
opacity: 0.8;
}
.loading-section {
text-align: center;
}
.loading-dots {
display: flex;
justify-content: center;
gap: 0.5rem;
margin-bottom: 1rem;
}
.loading-dots span {
width: 0.75rem;
height: 0.75rem;
border-radius: 50%;
background-color: #f59e0b;
animation: bounce 1.4s ease-in-out infinite both;
}
.loading-dots span:nth-child(1) {
animation-delay: -0.32s;
}
.loading-dots span:nth-child(2) {
animation-delay: -0.16s;
}
.loading-text {
color: #b45309;
font-size: 0.875rem;
}
.footer-info {
text-align: center;
}
.version {
color: #d97706;
font-size: 0.75rem;
opacity: 0.6;
}
@keyframes float {
0%, 100% {
transform: translateY(0px);
}
50% {
transform: translateY(-10px);
}
}
@keyframes bounce {
0%, 80%, 100% {
transform: scale(0);
}
40% {
transform: scale(1);
}
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@keyframes drift {
0%, 100% {
transform: translateX(0px);
}
50% {
transform: translateX(20px);
}
}
.animate-fade-in-up {
animation: fadeInUp 0.8s ease-out;
}
.animate-fade-in-up-delay {
animation: fadeInUp 0.8s ease-out 0.3s both;
}
.animate-fade-in {
animation: fadeInUp 0.8s ease-out 0.6s both;
}
.animate-rotate {
animation: rotate 8s linear infinite;
}
.animate-float {
animation: float 3s ease-in-out infinite;
}
.animate-drift {
animation: drift 4s ease-in-out infinite;
}
</style>
\ No newline at end of file
<template>
<div class="page-container">
<!-- 导航栏 -->
<van-nav-bar title="戒子详情" left-arrow @click-left="$router.back()" class="custom-nav">
<template #right>
<van-icon name="edit" size="18" @click="handleEdit" />
</template>
</van-nav-bar>
<!-- 内容区域 -->
<div class="content-container">
<!-- 戒子基本信息 -->
<div class="profile-section">
<div class="profile-header">
<div class="avatar-container">
<img v-if="student.avatar" :src="student.avatar" :alt="student.name" class="avatar" />
<div v-else class="avatar-placeholder">
<span>{{ student.name.charAt(0) }}</span>
</div>
<div class="precept-badge" :class="getPreceptClass(student.preceptType)">
{{ getPreceptText(student.preceptType) }}
</div>
</div>
<div class="profile-info">
<h2 class="student-name">{{ student.name }}</h2>
<p class="student-dharma-name">法名:{{ student.dharmaName }}</p>
<p class="student-temple">{{ student.temple }}</p>
<div class="status-badge" :class="getStatusClass(student.status)">
{{ getStatusText(student.status) }}
</div>
</div>
</div>
<!-- 统计信息 -->
<div class="stats-grid">
<div class="stat-item">
<div class="stat-value">{{ student.studyDays }}</div>
<div class="stat-label">学习天数</div>
</div>
<div class="stat-item">
<div class="stat-value">{{ student.progress }}%</div>
<div class="stat-label">学习进度</div>
</div>
<div class="stat-item">
<div class="stat-value">{{ student.completedCourses }}</div>
<div class="stat-label">完成课程</div>
</div>
<div class="stat-item">
<div class="stat-value">{{ student.merits }}</div>
<div class="stat-label">功德积分</div>
</div>
</div>
</div>
<!-- 详细信息 -->
<div class="details-section">
<!-- 基本信息 -->
<van-cell-group title="基本信息" class="info-group">
<van-cell title="戒师" :value="student.teacher" />
<van-cell title="入学时间" :value="student.entryDate" />
<van-cell title="预计毕业" :value="student.expectedGraduation" />
<van-cell title="籍贯" :value="student.hometown" />
<van-cell title="年龄" :value="student.age + '岁'" />
</van-cell-group>
<!-- 学习进度 -->
<div class="progress-section">
<h3 class="section-title">学习进度</h3>
<div class="progress-card">
<div class="progress-header">
<span class="progress-text">总体进度</span>
<span class="progress-percent">{{ student.progress }}%</span>
</div>
<van-progress :percentage="student.progress" color="#8b5cf6" />
</div>
<div class="course-list">
<div
v-for="course in student.courses"
:key="course.id"
class="course-item"
>
<div class="course-info">
<h4 class="course-name">{{ course.name }}</h4>
<p class="course-teacher">授课法师:{{ course.teacher }}</p>
</div>
<div class="course-progress">
<div class="course-status" :class="getCourseStatusClass(course.status)">
{{ getCourseStatusText(course.status) }}
</div>
<div class="course-score" v-if="course.score">
{{ course.score }}分
</div>
</div>
</div>
</div>
</div>
<!-- 戒律修学记录 -->
<div class="records-section">
<h3 class="section-title">修学记录</h3>
<van-timeline>
<van-timeline-item
v-for="record in student.studyRecords"
:key="record.id"
:time="record.date"
>
<div class="record-content">
<h4 class="record-title">{{ record.title }}</h4>
<p class="record-description">{{ record.description }}</p>
<div class="record-tags">
<van-tag
v-for="tag in record.tags"
:key="tag"
type="primary"
size="small"
class="record-tag"
>
{{ tag }}
</van-tag>
</div>
</div>
</van-timeline-item>
</van-timeline>
</div>
<!-- 联系方式 -->
<van-cell-group title="联系方式" class="info-group">
<van-cell title="手机号码" :value="student.phone" />
<van-cell title="紧急联系人" :value="student.emergencyContact" />
<van-cell title="紧急联系电话" :value="student.emergencyPhone" />
</van-cell-group>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { Toast } from 'vant'
const route = useRoute()
const router = useRouter()
// 戒子详细信息
const student = ref({
id: 1,
name: '释慧明',
dharmaName: '慧明',
temple: '大雄宝殿',
teacher: '释智慧法师',
preceptType: 'bhiksu',
status: 'studying',
entryDate: '2023-03-15',
expectedGraduation: '2024-03-15',
hometown: '江苏南京',
age: 28,
studyDays: 285,
progress: 75,
completedCourses: 12,
merits: 1580,
phone: '138****8888',
emergencyContact: '张居士',
emergencyPhone: '139****9999',
avatar: null,
courses: [
{
id: 1,
name: '沙弥律仪',
teacher: '释智慧法师',
status: 'completed',
score: 95
},
{
id: 2,
name: '比丘戒本',
teacher: '释慈悲法师',
status: 'studying',
score: null
},
{
id: 3,
name: '戒律学纲要',
teacher: '释般若法师',
status: 'completed',
score: 88
},
{
id: 4,
name: '四分律删繁补阙行事钞',
teacher: '释智慧法师',
status: 'pending',
score: null
}
],
studyRecords: [
{
id: 1,
date: '2024-01-15',
title: '完成沙弥律仪考试',
description: '以优异成绩通过沙弥律仪课程考试,获得95分',
tags: ['考试', '沙弥律仪', '优秀']
},
{
id: 2,
date: '2024-01-10',
title: '参加戒律研讨会',
description: '积极参与戒律研讨,发表见解,获得法师好评',
tags: ['研讨会', '戒律', '积极参与']
},
{
id: 3,
date: '2024-01-05',
title: '开始比丘戒本学习',
description: '正式开始比丘戒本课程的学习,制定详细学习计划',
tags: ['比丘戒', '学习计划']
},
{
id: 4,
date: '2023-12-20',
title: '戒律学纲要结业',
description: '完成戒律学纲要课程,掌握戒律基本理论',
tags: ['结业', '戒律学', '理论']
}
]
})
// 获取戒律类型样式类
const getPreceptClass = (type) => {
const classes = {
novice: 'precept-novice',
bhiksu: 'precept-bhiksu',
bodhisattva: 'precept-bodhisattva'
}
return classes[type] || 'precept-novice'
}
// 获取戒律类型文本
const getPreceptText = (type) => {
const texts = {
novice: '沙弥戒',
bhiksu: '比丘戒',
bodhisattva: '菩萨戒'
}
return texts[type] || '沙弥戒'
}
// 获取状态样式类
const getStatusClass = (status) => {
const classes = {
studying: 'status-studying',
graduated: 'status-graduated',
suspended: 'status-suspended'
}
return classes[status] || 'status-studying'
}
// 获取状态文本
const getStatusText = (status) => {
const texts = {
studying: '在学',
graduated: '毕业',
suspended: '暂停'
}
return texts[status] || '在学'
}
// 获取课程状态样式类
const getCourseStatusClass = (status) => {
const classes = {
completed: 'course-completed',
studying: 'course-studying',
pending: 'course-pending'
}
return classes[status] || 'course-pending'
}
// 获取课程状态文本
const getCourseStatusText = (status) => {
const texts = {
completed: '已完成',
studying: '学习中',
pending: '未开始'
}
return texts[status] || '未开始'
}
// 处理编辑
const handleEdit = () => {
Toast('编辑功能开发中...')
}
// 组件挂载时加载数据
onMounted(() => {
// 这里可以根据路由参数加载具体的戒子数据
const studentId = route.params.id
console.log('Loading student data for ID:', studentId)
})
</script>
<style scoped>
.page-container {
min-height: 100vh;
background: #fafafa;
}
.custom-nav {
background: linear-gradient(135deg, #8b5cf6, #7c3aed);
color: white;
}
.custom-nav :deep(.van-nav-bar__title) {
color: white;
font-weight: 600;
}
.custom-nav :deep(.van-icon) {
color: white;
}
.content-container {
padding-top: 46px;
}
.profile-section {
background: white;
padding: 20px;
margin-bottom: 12px;
}
.profile-header {
display: flex;
align-items: center;
margin-bottom: 20px;
}
.avatar-container {
position: relative;
width: 80px;
height: 80px;
border-radius: 50%;
overflow: hidden;
margin-right: 20px;
flex-shrink: 0;
}
.avatar {
width: 100%;
height: 100%;
object-fit: cover;
}
.avatar-placeholder {
width: 100%;
height: 100%;
background: linear-gradient(135deg, #8b5cf6, #7c3aed);
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 32px;
font-weight: 600;
}
.precept-badge {
position: absolute;
bottom: -2px;
right: -2px;
padding: 4px 8px;
border-radius: 12px;
font-size: 10px;
font-weight: 500;
border: 2px solid white;
}
.precept-novice {
background: #10b981;
color: white;
}
.precept-bhiksu {
background: #3b82f6;
color: white;
}
.precept-bodhisattva {
background: #f59e0b;
color: white;
}
.profile-info {
flex: 1;
}
.student-name {
font-size: 24px;
font-weight: 700;
color: #333;
margin: 0 0 8px 0;
}
.student-dharma-name {
font-size: 16px;
color: #666;
margin: 0 0 4px 0;
}
.student-temple {
font-size: 16px;
color: #666;
margin: 0 0 12px 0;
}
.status-badge {
display: inline-block;
padding: 6px 12px;
border-radius: 16px;
font-size: 12px;
font-weight: 500;
}
.status-studying {
background: #dbeafe;
color: #1d4ed8;
border: 1px solid #93c5fd;
}
.status-graduated {
background: #dcfce7;
color: #166534;
border: 1px solid #86efac;
}
.status-suspended {
background: #fef3c7;
color: #92400e;
border: 1px solid #fcd34d;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
}
.stat-item {
text-align: center;
}
.stat-value {
font-size: 20px;
font-weight: 700;
color: #8b5cf6;
margin-bottom: 4px;
}
.stat-label {
font-size: 12px;
color: #666;
}
.details-section {
padding: 0 16px 20px;
}
.info-group {
margin-bottom: 16px;
}
.info-group :deep(.van-cell-group__title) {
color: #333;
font-weight: 600;
}
.section-title {
font-size: 18px;
font-weight: 600;
color: #333;
margin: 20px 0 16px 0;
padding-left: 4px;
border-left: 4px solid #8b5cf6;
}
.progress-section {
background: white;
border-radius: 12px;
padding: 20px;
margin-bottom: 16px;
}
.progress-card {
background: #f8fafc;
border-radius: 8px;
padding: 16px;
margin-bottom: 20px;
}
.progress-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.progress-text {
font-size: 16px;
font-weight: 500;
color: #333;
}
.progress-percent {
font-size: 18px;
font-weight: 700;
color: #8b5cf6;
}
.course-list {
space-y: 12px;
}
.course-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
background: #f8fafc;
border-radius: 8px;
margin-bottom: 12px;
}
.course-info {
flex: 1;
}
.course-name {
font-size: 16px;
font-weight: 600;
color: #333;
margin: 0 0 4px 0;
}
.course-teacher {
font-size: 14px;
color: #666;
margin: 0;
}
.course-progress {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 4px;
}
.course-status {
padding: 4px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
}
.course-completed {
background: #dcfce7;
color: #166534;
}
.course-studying {
background: #dbeafe;
color: #1d4ed8;
}
.course-pending {
background: #f3f4f6;
color: #6b7280;
}
.course-score {
font-size: 14px;
font-weight: 600;
color: #8b5cf6;
}
.records-section {
background: white;
border-radius: 12px;
padding: 20px;
margin-bottom: 16px;
}
.record-content {
padding-left: 16px;
}
.record-title {
font-size: 16px;
font-weight: 600;
color: #333;
margin: 0 0 8px 0;
}
.record-description {
font-size: 14px;
color: #666;
margin: 0 0 12px 0;
line-height: 1.5;
}
.record-tags {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.record-tag {
background: #f3e8ff !important;
color: #8b5cf6 !important;
border: 1px solid #c4b5fd !important;
}
</style>
\ No newline at end of file
<template>
<div class="page-container">
<!-- 导航栏 -->
<van-nav-bar title="戒子管理" left-arrow @click-left="$router.back()" class="custom-nav">
<template #right>
<van-icon name="search" size="18" @click="handleSearch" />
</template>
</van-nav-bar>
<!-- 内容区域 -->
<div class="content-container">
<!-- 顶部统计 -->
<div class="stats-section">
<div class="stat-card">
<div class="stat-icon">🙏</div>
<div class="stat-info">
<div class="stat-number">{{ totalStudents }}</div>
<div class="stat-label">总戒子数</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon">📚</div>
<div class="stat-info">
<div class="stat-number">{{ studyingStudents }}</div>
<div class="stat-label">在学戒子</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon">🎓</div>
<div class="stat-info">
<div class="stat-number">{{ graduatedStudents }}</div>
<div class="stat-label">已毕业</div>
</div>
</div>
</div>
<!-- 筛选栏 -->
<div class="filter-section">
<van-tabs v-model:active="activeTab" @change="handleTabChange" class="custom-tabs">
<van-tab title="全部" name="all"></van-tab>
<van-tab title="沙弥戒" name="novice"></van-tab>
<van-tab title="比丘戒" name="bhiksu"></van-tab>
<van-tab title="菩萨戒" name="bodhisattva"></van-tab>
</van-tabs>
</div>
<!-- 戒子列表 -->
<div class="students-list">
<div
v-for="student in filteredStudents"
:key="student.id"
class="student-card"
@click="handleStudentClick(student)"
>
<div class="student-avatar">
<img v-if="student.avatar" :src="student.avatar" :alt="student.name" />
<div v-else class="avatar-placeholder">
<span>{{ student.name.charAt(0) }}</span>
</div>
<div class="precept-badge" :class="getPreceptClass(student.preceptType)">
{{ getPreceptText(student.preceptType) }}
</div>
</div>
<div class="student-info">
<div class="student-header">
<h4 class="student-name">{{ student.name }}</h4>
<div class="student-status" :class="getStatusClass(student.status)">
{{ getStatusText(student.status) }}
</div>
</div>
<div class="student-details">
<p class="student-temple">{{ student.temple }}</p>
<p class="student-teacher">戒师:{{ student.teacher }}</p>
<div class="student-meta">
<span class="entry-date">{{ student.entryDate }}入学</span>
<span class="progress">进度:{{ student.progress }}%</span>
</div>
</div>
</div>
<div class="student-actions">
<van-icon name="arrow" />
</div>
</div>
</div>
<!-- 空状态 -->
<van-empty v-if="filteredStudents.length === 0" description="暂无戒子信息" />
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import { useRouter } from 'vue-router'
import { Toast } from 'vant'
const router = useRouter()
const activeTab = ref('all')
// 统计数据
const totalStudents = ref(68)
const studyingStudents = ref(52)
const graduatedStudents = ref(16)
// 戒子数据
const students = ref([
{
id: 1,
name: '释慧明',
temple: '大雄宝殿',
teacher: '释智慧法师',
preceptType: 'bhiksu',
status: 'studying',
entryDate: '2023-03-15',
progress: 75,
avatar: null
},
{
id: 2,
name: '释德行',
temple: '观音殿',
teacher: '释慈悲法师',
preceptType: 'novice',
status: 'studying',
entryDate: '2023-06-20',
progress: 45,
avatar: null
},
{
id: 3,
name: '释觉悟',
temple: '文殊殿',
teacher: '释般若法师',
preceptType: 'bodhisattva',
status: 'graduated',
entryDate: '2022-09-10',
progress: 100,
avatar: null
},
{
id: 4,
name: '释持戒',
temple: '地藏殿',
teacher: '释智慧法师',
preceptType: 'bhiksu',
status: 'studying',
entryDate: '2023-01-05',
progress: 85,
avatar: null
},
{
id: 5,
name: '释精进',
temple: '药师殿',
teacher: '释慈悲法师',
preceptType: 'novice',
status: 'studying',
entryDate: '2023-08-12',
progress: 30,
avatar: null
},
{
id: 6,
name: '释忍辱',
temple: '弥勒殿',
teacher: '释般若法师',
preceptType: 'bodhisattva',
status: 'studying',
entryDate: '2023-04-18',
progress: 60,
avatar: null
},
{
id: 7,
name: '释禅定',
temple: '韦陀殿',
teacher: '释智慧法师',
preceptType: 'bhiksu',
status: 'graduated',
entryDate: '2022-11-30',
progress: 100,
avatar: null
},
{
id: 8,
name: '释智慧',
temple: '伽蓝殿',
teacher: '释慈悲法师',
preceptType: 'novice',
status: 'studying',
entryDate: '2023-07-08',
progress: 40,
avatar: null
}
])
// 过滤后的戒子列表
const filteredStudents = computed(() => {
if (activeTab.value === 'all') {
return students.value
}
return students.value.filter(student => student.preceptType === activeTab.value)
})
// 获取戒律类型样式类
const getPreceptClass = (type) => {
const classes = {
novice: 'precept-novice',
bhiksu: 'precept-bhiksu',
bodhisattva: 'precept-bodhisattva'
}
return classes[type] || 'precept-novice'
}
// 获取戒律类型文本
const getPreceptText = (type) => {
const texts = {
novice: '沙弥',
bhiksu: '比丘',
bodhisattva: '菩萨'
}
return texts[type] || '沙弥'
}
// 获取状态样式类
const getStatusClass = (status) => {
const classes = {
studying: 'status-studying',
graduated: 'status-graduated',
suspended: 'status-suspended'
}
return classes[status] || 'status-studying'
}
// 获取状态文本
const getStatusText = (status) => {
const texts = {
studying: '在学',
graduated: '毕业',
suspended: '暂停'
}
return texts[status] || '在学'
}
// 处理标签切换
const handleTabChange = (name) => {
activeTab.value = name
}
// 处理戒子点击
const handleStudentClick = (student) => {
router.push(`/students/${student.id}`)
}
// 处理搜索
const handleSearch = () => {
Toast('搜索功能开发中...')
}
</script>
<style scoped>
.page-container {
min-height: 100vh;
background: #fafafa;
}
.custom-nav {
background: linear-gradient(135deg, #8b5cf6, #7c3aed);
color: white;
}
.custom-nav :deep(.van-nav-bar__title) {
color: white;
font-weight: 600;
}
.custom-nav :deep(.van-icon) {
color: white;
}
.content-container {
padding-top: 46px;
}
.stats-section {
display: flex;
gap: 12px;
padding: 16px;
}
.stat-card {
flex: 1;
background: white;
border-radius: 12px;
padding: 16px;
display: flex;
align-items: center;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.stat-icon {
font-size: 24px;
margin-right: 12px;
}
.stat-info {
flex: 1;
}
.stat-number {
font-size: 20px;
font-weight: 700;
color: #8b5cf6;
margin-bottom: 2px;
}
.stat-label {
font-size: 12px;
color: #666;
}
.filter-section {
background: white;
border-bottom: 1px solid #eee;
}
.custom-tabs :deep(.van-tab) {
font-weight: 500;
}
.custom-tabs :deep(.van-tab--active) {
color: #8b5cf6;
}
.custom-tabs :deep(.van-tabs__line) {
background: #8b5cf6;
}
.students-list {
padding: 16px;
}
.student-card {
background: white;
border-radius: 12px;
padding: 16px;
margin-bottom: 12px;
display: flex;
align-items: center;
gap: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
cursor: pointer;
transition: all 0.3s ease;
}
.student-card:hover {
transform: translateY(-3px);
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15);
}
.student-card:active {
transform: translateY(-1px);
box-shadow: 0 3px 12px rgba(0, 0, 0, 0.12);
}
.student-avatar {
position: relative;
width: 60px;
height: 60px;
border-radius: 50%;
overflow: hidden;
margin-right: 16px;
flex-shrink: 0;
}
.student-avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
.avatar-placeholder {
width: 100%;
height: 100%;
background: linear-gradient(135deg, #8b5cf6, #7c3aed);
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 24px;
font-weight: 600;
}
.precept-badge {
position: absolute;
bottom: -2px;
right: -2px;
padding: 2px 6px;
border-radius: 8px;
font-size: 10px;
font-weight: 500;
border: 2px solid white;
}
.precept-novice {
background: #10b981;
color: white;
}
.precept-bhiksu {
background: #3b82f6;
color: white;
}
.precept-bodhisattva {
background: #f59e0b;
color: white;
}
.student-info {
flex: 1;
}
.student-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
}
.student-name {
font-size: 18px;
font-weight: 600;
color: #333;
margin: 0;
}
.student-status {
padding: 4px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
}
.status-studying {
background: #dbeafe;
color: #1d4ed8;
border: 1px solid #93c5fd;
}
.status-graduated {
background: #dcfce7;
color: #166534;
border: 1px solid #86efac;
}
.status-suspended {
background: #fef3c7;
color: #92400e;
border: 1px solid #fcd34d;
}
.student-details {
space-y: 4px;
}
.student-temple {
font-size: 16px;
color: #666;
margin: 0 0 4px 0;
}
.student-teacher {
font-size: 14px;
color: #999;
margin: 0 0 8px 0;
}
.student-meta {
display: flex;
gap: 16px;
}
.entry-date,
.progress {
font-size: 12px;
color: #999;
background: #f5f5f5;
padding: 2px 6px;
border-radius: 4px;
}
.student-actions {
margin-left: 12px;
color: #ccc;
}
</style>
\ No newline at end of file
<template>
<div class="page-container">
<!-- 导航栏 -->
<van-nav-bar title="法师详情" left-arrow @click-left="$router.back()" class="custom-nav">
<template #right>
<van-icon name="share" size="18" />
</template>
</van-nav-bar>
<!-- 内容区域 -->
<div class="content-container">
<!-- 法师基本信息 -->
<div class="teacher-profile">
<div class="profile-header">
<div class="teacher-avatar">
<img v-if="teacher.avatar" :src="teacher.avatar" :alt="teacher.name" />
<div v-else class="avatar-placeholder">
<span>{{ teacher.name.charAt(0) }}</span>
</div>
</div>
<div class="profile-info">
<h2 class="teacher-name">{{ teacher.name }}</h2>
<div class="teacher-role" :class="getRoleClass(teacher.role)">
{{ teacher.role }}
</div>
<p class="teacher-title">{{ teacher.title }} · {{ teacher.temple }}</p>
</div>
</div>
<!-- 统计信息 -->
<div class="stats-section">
<div class="stat-item">
<div class="stat-number">{{ teacher.experience }}</div>
<div class="stat-label">戒腊年数</div>
</div>
<div class="stat-divider"></div>
<div class="stat-item">
<div class="stat-number">{{ teacher.ordinationYear }}</div>
<div class="stat-label">受戒年份</div>
</div>
<div class="stat-divider"></div>
<div class="stat-item">
<div class="stat-number">{{ teacher.disciples || 0 }}</div>
<div class="stat-label">弟子人数</div>
</div>
</div>
</div>
<!-- 详细信息 -->
<div class="detail-sections">
<!-- 个人简介 -->
<div class="detail-section">
<h3 class="section-title">
<van-icon name="user-o" />
个人简介
</h3>
<div class="section-content">
<p>{{ teacher.biography || '暂无个人简介信息' }}</p>
</div>
</div>
<!-- 修学经历 -->
<div class="detail-section">
<h3 class="section-title">
<van-icon name="certificate" />
修学经历
</h3>
<div class="section-content">
<div class="timeline">
<div v-for="(experience, index) in teacher.experiences" :key="index" class="timeline-item">
<div class="timeline-dot"></div>
<div class="timeline-content">
<div class="timeline-year">{{ experience.year }}</div>
<div class="timeline-event">{{ experience.event }}</div>
<div class="timeline-location">{{ experience.location }}</div>
</div>
</div>
</div>
</div>
</div>
<!-- 弘法活动 -->
<div class="detail-section">
<h3 class="section-title">
<van-icon name="fire-o" />
弘法活动
</h3>
<div class="section-content">
<div v-if="teacher.activities && teacher.activities.length > 0" class="activities-list">
<div v-for="activity in teacher.activities" :key="activity.id" class="activity-item">
<div class="activity-date">{{ activity.date }}</div>
<div class="activity-title">{{ activity.title }}</div>
<div class="activity-location">{{ activity.location }}</div>
</div>
</div>
<p v-else class="no-data">暂无弘法活动记录</p>
</div>
</div>
<!-- 联系方式 -->
<div class="detail-section">
<h3 class="section-title">
<van-icon name="phone-o" />
联系方式
</h3>
<div class="section-content">
<div class="contact-info">
<div class="contact-item">
<span class="contact-label">所在寺院:</span>
<span class="contact-value">{{ teacher.temple }}</span>
</div>
<div class="contact-item" v-if="teacher.phone">
<span class="contact-label">联系电话:</span>
<span class="contact-value">{{ teacher.phone }}</span>
</div>
<div class="contact-item" v-if="teacher.email">
<span class="contact-label">电子邮箱:</span>
<span class="contact-value">{{ teacher.email }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
const route = useRoute()
const router = useRouter()
// 法师详细信息
const teacher = ref({
id: 1,
name: '慧明法师',
title: '方丈',
role: '得戒和尚',
temple: '大觉寺',
ordinationYear: 1985,
experience: 39,
disciples: 156,
avatar: null,
biography: '慧明法师,俗姓李,1960年生于江苏南京。1985年在大觉寺依止上慧下觉老和尚剃度出家,同年在宝华山隆昌寺受具足戒。法师戒行清净,学修并重,深得四众弟子敬仰。现任大觉寺方丈,致力于佛法弘扬和寺院建设。',
experiences: [
{
year: '1985年',
event: '在大觉寺剃度出家',
location: '大觉寺'
},
{
year: '1985年',
event: '在宝华山隆昌寺受具足戒',
location: '宝华山隆昌寺'
},
{
year: '1990年',
event: '任大觉寺知客',
location: '大觉寺'
},
{
year: '1995年',
event: '任大觉寺监院',
location: '大觉寺'
},
{
year: '2000年',
event: '升座为大觉寺方丈',
location: '大觉寺'
}
],
activities: [
{
id: 1,
date: '2024-01-15',
title: '三坛大戒传戒法会',
location: '大觉寺'
},
{
id: 2,
date: '2023-12-08',
title: '佛成道日法会',
location: '大觉寺'
},
{
id: 3,
date: '2023-11-20',
title: '佛学讲座:戒律的现代意义',
location: '大觉寺讲堂'
}
],
phone: '025-12345678',
email: 'huiming@dajuesi.org'
})
// 获取角色样式类
const getRoleClass = (role) => {
if (role.includes('和尚') || role.includes('阿阇梨')) {
return 'role-teacher'
}
return 'role-witness'
}
// 加载法师详情
const loadTeacherDetail = async () => {
const teacherId = route.params.id
// 这里应该根据 teacherId 从 API 获取法师详情
// 现在使用模拟数据
console.log('Loading teacher detail for ID:', teacherId)
}
onMounted(() => {
loadTeacherDetail()
})
</script>
<style scoped>
.page-container {
min-height: 100vh;
background: #fafafa;
}
.custom-nav {
background: linear-gradient(135deg, #fbbf24, #f97316);
color: white;
}
.custom-nav :deep(.van-nav-bar__title) {
color: white;
font-weight: 600;
}
.custom-nav :deep(.van-icon) {
color: white;
}
.content-container {
padding-top: 46px;
}
.teacher-profile {
background: white;
margin: 16px;
border-radius: 12px;
padding: 24px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.profile-header {
display: flex;
align-items: center;
margin-bottom: 24px;
}
.teacher-avatar {
width: 80px;
height: 80px;
border-radius: 50%;
overflow: hidden;
margin-right: 20px;
flex-shrink: 0;
}
.teacher-avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
.avatar-placeholder {
width: 100%;
height: 100%;
background: linear-gradient(135deg, #fbbf24, #f97316);
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 32px;
font-weight: 600;
}
.profile-info {
flex: 1;
}
.teacher-name {
font-size: 24px;
font-weight: 700;
color: #333;
margin: 0 0 8px 0;
}
.teacher-role {
display: inline-block;
padding: 6px 12px;
border-radius: 16px;
font-size: 14px;
font-weight: 500;
margin-bottom: 8px;
}
.role-teacher {
background: linear-gradient(135deg, #fbbf24, #f97316);
color: white;
}
.role-witness {
background: #f0f9ff;
color: #0369a1;
border: 1px solid #bae6fd;
}
.teacher-title {
font-size: 16px;
color: #666;
margin: 0;
}
.stats-section {
display: flex;
align-items: center;
justify-content: space-around;
padding-top: 24px;
border-top: 1px solid #f0f0f0;
}
.stat-item {
text-align: center;
}
.stat-number {
font-size: 24px;
font-weight: 700;
color: #f59e0b;
margin-bottom: 4px;
}
.stat-label {
font-size: 12px;
color: #999;
}
.stat-divider {
width: 1px;
height: 40px;
background: #f0f0f0;
}
.detail-sections {
padding: 0 16px 16px;
}
.detail-section {
background: white;
border-radius: 12px;
margin-bottom: 16px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.section-title {
display: flex;
align-items: center;
padding: 16px 20px;
margin: 0;
font-size: 16px;
font-weight: 600;
color: #333;
background: #fafafa;
border-bottom: 1px solid #f0f0f0;
}
.section-title .van-icon {
margin-right: 8px;
color: #f59e0b;
}
.section-content {
padding: 20px;
}
.section-content p {
font-size: 14px;
line-height: 1.6;
color: #666;
margin: 0;
}
.timeline {
position: relative;
}
.timeline::before {
content: '';
position: absolute;
left: 8px;
top: 0;
bottom: 0;
width: 2px;
background: #f0f0f0;
}
.timeline-item {
position: relative;
padding-left: 32px;
margin-bottom: 20px;
}
.timeline-item:last-child {
margin-bottom: 0;
}
.timeline-dot {
position: absolute;
left: 0;
top: 4px;
width: 16px;
height: 16px;
border-radius: 50%;
background: #f59e0b;
border: 3px solid white;
box-shadow: 0 0 0 2px #f59e0b;
}
.timeline-year {
font-size: 14px;
font-weight: 600;
color: #f59e0b;
margin-bottom: 4px;
}
.timeline-event {
font-size: 16px;
font-weight: 500;
color: #333;
margin-bottom: 4px;
}
.timeline-location {
font-size: 14px;
color: #999;
}
.activities-list {
space-y: 12px;
}
.activity-item {
padding: 16px;
background: #fafafa;
border-radius: 8px;
margin-bottom: 12px;
}
.activity-date {
font-size: 12px;
color: #f59e0b;
font-weight: 500;
margin-bottom: 4px;
}
.activity-title {
font-size: 16px;
font-weight: 500;
color: #333;
margin-bottom: 4px;
}
.activity-location {
font-size: 14px;
color: #666;
}
.contact-info {
space-y: 12px;
}
.contact-item {
display: flex;
align-items: center;
margin-bottom: 12px;
}
.contact-label {
font-size: 14px;
color: #666;
width: 80px;
flex-shrink: 0;
}
.contact-value {
font-size: 14px;
color: #333;
flex: 1;
}
.no-data {
text-align: center;
color: #999;
font-size: 14px;
margin: 0;
}
</style>
\ No newline at end of file
<template>
<div class="page-container">
<!-- 导航栏 -->
<van-nav-bar title="三师七证" left-arrow @click-left="$router.back()" class="custom-nav">
<template #right>
<van-icon name="search" size="18" />
</template>
</van-nav-bar>
<!-- 内容区域 -->
<div class="content-container">
<!-- 顶部说明 -->
<div class="intro-section">
<div class="intro-card">
<div class="intro-icon">📜</div>
<div class="intro-content">
<h3>三师七证</h3>
<p>三师:得戒和尚、羯磨阿阇梨、教授阿阇梨<br>七证:七位证明师</p>
</div>
</div>
</div>
<!-- 筛选栏 -->
<div class="filter-section">
<van-tabs v-model:active="activeTab" @change="handleTabChange" class="custom-tabs">
<van-tab title="全部" name="all"></van-tab>
<van-tab title="三师" name="teachers"></van-tab>
<van-tab title="七证" name="witnesses"></van-tab>
</van-tabs>
</div>
<!-- 法师列表 -->
<div class="teachers-list">
<div
v-for="teacher in filteredTeachers"
:key="teacher.id"
class="teacher-card"
@click="handleTeacherClick(teacher)"
>
<div class="teacher-avatar">
<img v-if="teacher.avatar" :src="teacher.avatar" :alt="teacher.name" />
<div v-else class="avatar-placeholder">
<span>{{ teacher.name.charAt(0) }}</span>
</div>
</div>
<div class="teacher-info">
<div class="teacher-header">
<h4 class="teacher-name">{{ teacher.name }}</h4>
<div class="teacher-role" :class="getRoleClass(teacher.role)">
{{ teacher.role }}
</div>
</div>
<div class="teacher-details">
<p class="teacher-title">{{ teacher.title }}</p>
<p class="teacher-temple">{{ teacher.temple }}</p>
<div class="teacher-meta">
<span class="ordination-year">{{ teacher.ordinationYear }}年受戒</span>
<span class="experience">{{ teacher.experience }}年戒腊</span>
</div>
</div>
</div>
<div class="teacher-actions">
<van-icon name="arrow" />
</div>
</div>
</div>
<!-- 空状态 -->
<van-empty v-if="filteredTeachers.length === 0" description="暂无相关法师信息" />
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
const activeTab = ref('all')
// 法师数据
const teachers = ref([
{
id: 1,
name: '慧明法师',
title: '方丈',
role: '得戒和尚',
temple: '大觉寺',
ordinationYear: 1985,
experience: 39,
avatar: null,
type: 'teacher'
},
{
id: 2,
name: '智慧法师',
title: '首座',
role: '羯磨阿阇梨',
temple: '大觉寺',
ordinationYear: 1990,
experience: 34,
avatar: null,
type: 'teacher'
},
{
id: 3,
name: '觉悟法师',
title: '监院',
role: '教授阿阇梨',
temple: '大觉寺',
ordinationYear: 1992,
experience: 32,
avatar: null,
type: 'teacher'
},
{
id: 4,
name: '慈悲法师',
title: '知客',
role: '证明师',
temple: '大觉寺',
ordinationYear: 1995,
experience: 29,
avatar: null,
type: 'witness'
},
{
id: 5,
name: '般若法师',
title: '维那',
role: '证明师',
temple: '大觉寺',
ordinationYear: 1998,
experience: 26,
avatar: null,
type: 'witness'
},
{
id: 6,
name: '禅定法师',
title: '典座',
role: '证明师',
temple: '大觉寺',
ordinationYear: 2000,
experience: 24,
avatar: null,
type: 'witness'
},
{
id: 7,
name: '精进法师',
title: '书记',
role: '证明师',
temple: '大觉寺',
ordinationYear: 2002,
experience: 22,
avatar: null,
type: 'witness'
},
{
id: 8,
name: '持戒法师',
title: '库头',
role: '证明师',
temple: '大觉寺',
ordinationYear: 2005,
experience: 19,
avatar: null,
type: 'witness'
},
{
id: 9,
name: '忍辱法师',
title: '僧值',
role: '证明师',
temple: '大觉寺',
ordinationYear: 2008,
experience: 16,
avatar: null,
type: 'witness'
},
{
id: 10,
name: '布施法师',
title: '衣钵',
role: '证明师',
temple: '大觉寺',
ordinationYear: 2010,
experience: 14,
avatar: null,
type: 'witness'
}
])
// 过滤后的法师列表
const filteredTeachers = computed(() => {
if (activeTab.value === 'all') {
return teachers.value
} else if (activeTab.value === 'teachers') {
return teachers.value.filter(teacher => teacher.type === 'teacher')
} else if (activeTab.value === 'witnesses') {
return teachers.value.filter(teacher => teacher.type === 'witness')
}
return teachers.value
})
// 获取角色样式类
const getRoleClass = (role) => {
if (role.includes('和尚') || role.includes('阿阇梨')) {
return 'role-teacher'
}
return 'role-witness'
}
// 处理标签切换
const handleTabChange = (name) => {
activeTab.value = name
}
// 处理法师点击
const handleTeacherClick = (teacher) => {
router.push(`/teachers/${teacher.id}`)
}
</script>
<style scoped>
.page-container {
min-height: 100vh;
background: #fafafa;
}
.custom-nav {
background: linear-gradient(135deg, #fbbf24, #f97316);
color: white;
}
.custom-nav :deep(.van-nav-bar__title) {
color: white;
font-weight: 600;
}
.custom-nav :deep(.van-icon) {
color: white;
}
.content-container {
padding-top: 46px;
}
.intro-section {
padding: 16px;
}
.intro-card {
background: white;
border-radius: 12px;
padding: 20px;
display: flex;
align-items: center;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.intro-icon {
font-size: 32px;
margin-right: 16px;
}
.intro-content h3 {
font-size: 18px;
font-weight: 600;
color: #333;
margin: 0 0 8px 0;
}
.intro-content p {
font-size: 14px;
color: #666;
margin: 0;
line-height: 1.5;
}
.filter-section {
background: white;
border-bottom: 1px solid #eee;
}
.custom-tabs :deep(.van-tab) {
font-weight: 500;
}
.custom-tabs :deep(.van-tab--active) {
color: #f59e0b;
}
.custom-tabs :deep(.van-tabs__line) {
background: #f59e0b;
}
.teachers-list {
padding: 16px;
}
.teacher-card {
background: white;
border-radius: 12px;
padding: 16px;
margin-bottom: 12px;
display: flex;
align-items: center;
gap: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
cursor: pointer;
transition: all 0.3s ease;
}
.teacher-card:hover {
transform: translateY(-3px);
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15);
}
.teacher-card:active {
transform: translateY(-1px);
box-shadow: 0 3px 12px rgba(0, 0, 0, 0.12);
}
.teacher-avatar {
width: 60px;
height: 60px;
border-radius: 50%;
overflow: hidden;
margin-right: 16px;
flex-shrink: 0;
}
.teacher-avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
.avatar-placeholder {
width: 100%;
height: 100%;
background: linear-gradient(135deg, #fbbf24, #f97316);
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 24px;
font-weight: 600;
}
.teacher-info {
flex: 1;
}
.teacher-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
}
.teacher-name {
font-size: 18px;
font-weight: 600;
color: #333;
margin: 0;
}
.teacher-role {
padding: 4px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
}
.role-teacher {
background: linear-gradient(135deg, #fbbf24, #f97316);
color: white;
}
.role-witness {
background: #f0f9ff;
color: #0369a1;
border: 1px solid #bae6fd;
}
.teacher-details {
space-y: 4px;
}
.teacher-title {
font-size: 16px;
color: #666;
margin: 0 0 4px 0;
}
.teacher-temple {
font-size: 14px;
color: #999;
margin: 0 0 8px 0;
}
.teacher-meta {
display: flex;
gap: 16px;
}
.ordination-year,
.experience {
font-size: 12px;
color: #999;
background: #f5f5f5;
padding: 2px 6px;
border-radius: 4px;
}
.teacher-actions {
margin-left: 12px;
color: #ccc;
}
</style>
\ No newline at end of file
<template>
<div class="page-container">
<!-- 导航栏 -->
<van-nav-bar title="义工服务" left-arrow @click-left="$router.back()" class="custom-nav">
<template #right>
<van-icon name="plus" size="18" @click="handleAddVolunteer" />
</template>
</van-nav-bar>
<!-- 内容区域 -->
<div class="content-container">
<!-- 顶部统计 -->
<div class="stats-section">
<div class="stat-card">
<div class="stat-icon">👥</div>
<div class="stat-info">
<div class="stat-number">{{ totalVolunteers }}</div>
<div class="stat-label">总义工数</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon">✅</div>
<div class="stat-info">
<div class="stat-number">{{ activeVolunteers }}</div>
<div class="stat-label">在岗义工</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon">📅</div>
<div class="stat-info">
<div class="stat-number">{{ todayTasks }}</div>
<div class="stat-label">今日任务</div>
</div>
</div>
</div>
<!-- 筛选栏 -->
<div class="filter-section">
<van-tabs v-model:active="activeTab" @change="handleTabChange" class="custom-tabs">
<van-tab title="全部" name="all"></van-tab>
<van-tab title="在岗" name="active"></van-tab>
<van-tab title="休息" name="rest"></van-tab>
<van-tab title="请假" name="leave"></van-tab>
</van-tabs>
</div>
<!-- 义工列表 -->
<div class="volunteers-list">
<div
v-for="volunteer in filteredVolunteers"
:key="volunteer.id"
class="volunteer-card"
@click="handleVolunteerClick(volunteer)"
>
<div class="volunteer-avatar">
<img v-if="volunteer.avatar" :src="volunteer.avatar" :alt="volunteer.name" />
<div v-else class="avatar-placeholder">
<span>{{ volunteer.name.charAt(0) }}</span>
</div>
<div class="status-badge" :class="getStatusClass(volunteer.status)">
{{ getStatusText(volunteer.status) }}
</div>
</div>
<div class="volunteer-info">
<div class="volunteer-header">
<h4 class="volunteer-name">{{ volunteer.name }}</h4>
<div class="volunteer-level" :class="getLevelClass(volunteer.level)">
{{ volunteer.level }}
</div>
</div>
<div class="volunteer-details">
<p class="volunteer-department">{{ volunteer.department }}</p>
<p class="volunteer-task">当前任务:{{ volunteer.currentTask || '暂无' }}</p>
<div class="volunteer-meta">
<span class="join-date">{{ volunteer.joinDate }}加入</span>
<span class="service-hours">{{ volunteer.serviceHours }}小时</span>
</div>
</div>
</div>
<div class="volunteer-actions">
<van-icon name="arrow" />
</div>
</div>
</div>
<!-- 空状态 -->
<van-empty v-if="filteredVolunteers.length === 0" description="暂无义工信息" />
</div>
<!-- 浮动按钮 -->
<van-floating-bubble
axis="xy"
icon="plus"
@click="handleAddVolunteer"
class="add-button"
/>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import { useRouter } from 'vue-router'
import { Toast } from 'vant'
const router = useRouter()
const activeTab = ref('all')
// 统计数据
const totalVolunteers = ref(45)
const activeVolunteers = ref(32)
const todayTasks = ref(18)
// 义工数据
const volunteers = ref([
{
id: 1,
name: '张慧敏',
department: '客堂组',
level: '资深义工',
status: 'active',
currentTask: '接待来访信众',
joinDate: '2022-03-15',
serviceHours: 520,
avatar: null
},
{
id: 2,
name: '李明德',
department: '维护组',
level: '普通义工',
status: 'active',
currentTask: '大殿清洁维护',
joinDate: '2023-01-20',
serviceHours: 280,
avatar: null
},
{
id: 3,
name: '王慈悲',
department: '斋堂组',
level: '组长',
status: 'active',
currentTask: '午斋准备工作',
joinDate: '2021-08-10',
serviceHours: 750,
avatar: null
},
{
id: 4,
name: '陈智慧',
department: '法务组',
level: '资深义工',
status: 'rest',
currentTask: null,
joinDate: '2022-06-05',
serviceHours: 420,
avatar: null
},
{
id: 5,
name: '刘精进',
department: '安保组',
level: '普通义工',
status: 'leave',
currentTask: null,
joinDate: '2023-04-12',
serviceHours: 150,
avatar: null
},
{
id: 6,
name: '赵般若',
department: '文宣组',
level: '资深义工',
status: 'active',
currentTask: '活动摄影记录',
joinDate: '2022-11-30',
serviceHours: 380,
avatar: null
},
{
id: 7,
name: '孙持戒',
department: '客堂组',
level: '普通义工',
status: 'active',
currentTask: '登记来访信息',
joinDate: '2023-07-08',
serviceHours: 95,
avatar: null
},
{
id: 8,
name: '周忍辱',
department: '斋堂组',
level: '普通义工',
status: 'rest',
currentTask: null,
joinDate: '2023-02-14',
serviceHours: 220,
avatar: null
}
])
// 过滤后的义工列表
const filteredVolunteers = computed(() => {
if (activeTab.value === 'all') {
return volunteers.value
}
return volunteers.value.filter(volunteer => volunteer.status === activeTab.value)
})
// 获取状态样式类
const getStatusClass = (status) => {
const classes = {
active: 'status-active',
rest: 'status-rest',
leave: 'status-leave'
}
return classes[status] || 'status-rest'
}
// 获取状态文本
const getStatusText = (status) => {
const texts = {
active: '在岗',
rest: '休息',
leave: '请假'
}
return texts[status] || '休息'
}
// 获取等级样式类
const getLevelClass = (level) => {
if (level === '组长') return 'level-leader'
if (level === '资深义工') return 'level-senior'
return 'level-normal'
}
// 处理标签切换
const handleTabChange = (name) => {
activeTab.value = name
}
// 处理义工点击
const handleVolunteerClick = (volunteer) => {
router.push(`/volunteers/${volunteer.id}`)
}
// 处理添加义工
const handleAddVolunteer = () => {
Toast('添加义工功能开发中...')
}
</script>
<style scoped>
.page-container {
min-height: 100vh;
background: #fafafa;
}
.custom-nav {
background: linear-gradient(135deg, #fbbf24, #f97316);
color: white;
}
.custom-nav :deep(.van-nav-bar__title) {
color: white;
font-weight: 600;
}
.custom-nav :deep(.van-icon) {
color: white;
}
.content-container {
padding-top: 46px;
}
.stats-section {
display: flex;
gap: 12px;
padding: 16px;
}
.stat-card {
flex: 1;
background: white;
border-radius: 12px;
padding: 16px;
display: flex;
align-items: center;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.stat-icon {
font-size: 24px;
margin-right: 12px;
}
.stat-info {
flex: 1;
}
.stat-number {
font-size: 20px;
font-weight: 700;
color: #f59e0b;
margin-bottom: 2px;
}
.stat-label {
font-size: 12px;
color: #666;
}
.filter-section {
background: white;
border-bottom: 1px solid #eee;
}
.custom-tabs :deep(.van-tab) {
font-weight: 500;
}
.custom-tabs :deep(.van-tab--active) {
color: #f59e0b;
}
.custom-tabs :deep(.van-tabs__line) {
background: #f59e0b;
}
.volunteers-list {
padding: 16px;
}
.volunteer-card {
background: white;
border-radius: 12px;
padding: 16px;
margin-bottom: 12px;
display: flex;
align-items: center;
gap: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
cursor: pointer;
transition: all 0.3s ease;
}
.volunteer-card:hover {
transform: translateY(-3px);
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15);
}
.volunteer-card:active {
transform: translateY(-1px);
box-shadow: 0 3px 12px rgba(0, 0, 0, 0.12);
}
.volunteer-avatar {
position: relative;
width: 60px;
height: 60px;
border-radius: 50%;
overflow: hidden;
margin-right: 16px;
flex-shrink: 0;
}
.volunteer-avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
.avatar-placeholder {
width: 100%;
height: 100%;
background: linear-gradient(135deg, #fbbf24, #f97316);
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 24px;
font-weight: 600;
}
.status-badge {
position: absolute;
bottom: -2px;
right: -2px;
padding: 2px 6px;
border-radius: 8px;
font-size: 10px;
font-weight: 500;
border: 2px solid white;
}
.status-active {
background: #10b981;
color: white;
}
.status-rest {
background: #6b7280;
color: white;
}
.status-leave {
background: #ef4444;
color: white;
}
.volunteer-info {
flex: 1;
}
.volunteer-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
}
.volunteer-name {
font-size: 18px;
font-weight: 600;
color: #333;
margin: 0;
}
.volunteer-level {
padding: 4px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
}
.level-leader {
background: linear-gradient(135deg, #fbbf24, #f97316);
color: white;
}
.level-senior {
background: #dbeafe;
color: #1d4ed8;
border: 1px solid #93c5fd;
}
.level-normal {
background: #f3f4f6;
color: #6b7280;
border: 1px solid #d1d5db;
}
.volunteer-details {
space-y: 4px;
}
.volunteer-department {
font-size: 16px;
color: #666;
margin: 0 0 4px 0;
}
.volunteer-task {
font-size: 14px;
color: #999;
margin: 0 0 8px 0;
}
.volunteer-meta {
display: flex;
gap: 16px;
}
.join-date,
.service-hours {
font-size: 12px;
color: #999;
background: #f5f5f5;
padding: 2px 6px;
border-radius: 4px;
}
.volunteer-actions {
margin-left: 12px;
color: #ccc;
}
.add-button {
background: linear-gradient(135deg, #fbbf24, #f97316) !important;
transition: all 0.3s ease;
animation: float 3s ease-in-out infinite;
}
.add-button:hover {
transform: translateY(-3px) scale(1.1);
box-shadow: 0 8px 25px rgba(251, 191, 36, 0.6);
}
.add-button:active {
transform: translateY(-1px) scale(1.05);
box-shadow: 0 4px 16px rgba(251, 191, 36, 0.4);
}
.add-button :deep(.van-floating-bubble__icon) {
color: white;
}
@keyframes float {
0%, 100% {
transform: translateY(0px);
}
50% {
transform: translateY(-10px);
}
}
</style>
\ No newline at end of file
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{vue,js,ts,jsx,tsx}",
],
theme: {
extend: {
colors: {
primary: '#1989fa',
success: '#07c160',
warning: '#ff976a',
danger: '#ee0a24',
},
fontSize: {
'xs': '10px',
'sm': '12px',
'base': '14px',
'lg': '16px',
'xl': '18px',
'2xl': '20px',
'3xl': '24px',
}
},
},
plugins: [],
corePlugins: {
preflight: false, // 禁用 Tailwind 的基础样式重置,避免与 Vant 冲突
}
}
\ No newline at end of file
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { VantResolver } from 'unplugin-vue-components/resolvers'
import path from 'path'
import tailwindcss from 'tailwindcss'
import autoprefixer from 'autoprefixer'
import postcsspxtoviewport from 'postcss-px-to-viewport'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
AutoImport({
imports: ['vue', 'vue-router'],
dts: 'src/auto-imports.d.ts',
eslintrc: {
enabled: true
}
}),
Components({
resolvers: [VantResolver()],
dts: 'src/components.d.ts'
})
],
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
'@components': path.resolve(__dirname, 'src/components'),
'@utils': path.resolve(__dirname, 'src/utils'),
'@api': path.resolve(__dirname, 'src/api'),
'@assets': path.resolve(__dirname, 'src/assets'),
'@views': path.resolve(__dirname, 'src/views'),
'@stores': path.resolve(__dirname, 'src/stores')
}
},
css: {
postcss: {
plugins: [
tailwindcss,
autoprefixer,
postcsspxtoviewport({
unitToConvert: 'px',
viewportWidth: 375,
unitPrecision: 6,
propList: ['*'],
viewportUnit: 'vw',
fontViewportUnit: 'vw',
selectorBlackList: ['ignore-'],
minPixelValue: 1,
mediaQuery: true,
replace: true,
exclude: [],
landscape: false
})
]
}
},
server: {
host: '0.0.0.0',
port: 3000,
open: true,
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
},
build: {
outDir: 'dist',
assetsDir: 'static',
rollupOptions: {
output: {
chunkFileNames: 'static/js/[name]-[hash].js',
entryFileNames: 'static/js/[name]-[hash].js',
assetFileNames: 'static/[ext]/[name]-[hash].[ext]'
}
}
}
})
\ No newline at end of file