hookehuyr

feat: 添加三师七证详情页和瀑布流展示功能

重构路由配置,新增MastersDetail页面
实现戒子页面的瀑布流图片展示功能
优化新闻详情页的样式和布局
移除不再使用的Teachers相关组件
添加Vant组件类型声明和mock数据
......@@ -19,14 +19,17 @@ declare module 'vue' {
VanCheckbox: typeof import('vant/es')['Checkbox']
VanCollapse: typeof import('vant/es')['Collapse']
VanCollapseItem: typeof import('vant/es')['CollapseItem']
VanEmpty: typeof import('vant/es')['Empty']
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']
VanList: typeof import('vant/es')['List']
VanNavBar: typeof import('vant/es')['NavBar']
VanNoticeBar: typeof import('vant/es')['NoticeBar']
VanOverlay: typeof import('vant/es')['Overlay']
VanProgress: typeof import('vant/es')['Progress']
VanRate: typeof import('vant/es')['Rate']
VanSidebar: typeof import('vant/es')['Sidebar']
......@@ -42,5 +45,6 @@ declare module 'vue' {
VanTabs: typeof import('vant/es')['Tabs']
VanTag: typeof import('vant/es')['Tag']
VideoPlayer: typeof import('./components/VideoPlayer.vue')['default']
WaterfallGallery: typeof import('./components/WaterfallGallery.vue')['default']
}
}
......
/*
* @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
* @LastEditTime: 2025-10-30 20:53:14
* @FilePath: /stdj_h5/src/router/index.js
* @Description: 文件描述
*/
import { createRouter, createWebHistory } from 'vue-router'
......@@ -20,34 +20,29 @@ const routes = [
component: Home
},
{
path: '/teachers',
name: 'Teachers',
component: () => import('../views/Teachers.vue')
path: '/masters',
name: '三師七證',
component: () => import('../views/Masters.vue')
},
{
path: '/teachers/:id',
name: 'TeacherDetail',
component: () => import('../views/TeacherDetail.vue')
path: '/masters/:id',
name: '三師七證详情',
component: () => import('../views/MastersDetail.vue')
},
{
path: '/volunteers',
name: 'Volunteers',
component: () => import('../views/Volunteers.vue')
path: '/news/:id',
name: 'NewsDetail',
component: () => import('../views/NewsDetail.vue')
},
{
path: '/students',
name: 'Students',
name: '戒子',
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: '/volunteers',
name: '义工',
component: () => import('../views/Volunteers.vue')
},
{
path: '/:pathMatch(.*)*',
......@@ -74,7 +69,7 @@ router.beforeEach((to, from, next) => {
if (to.meta.title) {
document.title = to.meta.title
}
// 这里可以添加权限验证逻辑
next()
})
......@@ -83,4 +78,4 @@ router.afterEach(() => {
// 路由跳转后的逻辑
})
export default router
\ No newline at end of file
export default router
......
......@@ -240,3 +240,87 @@ export const communityPosts = [
createdAt: '2023-03-14T15:45:00Z'
}
];
// 瀑布流图片数据 - 用于义工和戒子页面
export const waterfallImages = [
{
id: 1,
url: 'https://cdn.ipadbiz.cn/mlaj/images/zMRLZh40kms.jpg',
height: 300,
title: '义工活动'
},
{
id: 2,
url: 'https://cdn.ipadbiz.cn/mlaj/images/27kCu7bXGEI.jpg',
height: 400,
title: '学习交流'
},
{
id: 3,
url: 'https://cdn.ipadbiz.cn/mlaj/images/jbwr0qZvpD4.jpg',
height: 350,
title: '禅修体验'
},
{
id: 4,
url: 'https://cdn.ipadbiz.cn/mlaj/images/GGCP6vshpPY.jpg',
height: 280,
title: '传统文化'
},
{
id: 5,
url: 'https://cdn.ipadbiz.cn/mlaj/images/2JIvboGLeho.jpg',
height: 320,
title: '法会现场'
},
{
id: 6,
url: 'https://cdn.ipadbiz.cn/mlaj/images/_6HzPU9Hyfg (2).jpg',
height: 380,
title: '诵经学习'
},
{
id: 7,
url: 'https://cdn.ipadbiz.cn/mlaj/images/2Juj2cXWB7U.jpg',
height: 290,
title: '义工服务'
},
{
id: 8,
url: 'https://cdn.ipadbiz.cn/mlaj/images/Y17FE9Fuw4Y.jpg',
height: 360,
title: '戒子生活'
},
{
id: 9,
url: 'https://cdn.ipadbiz.cn/mlaj/images/-G3rw6Y02D0.jpg',
height: 310,
title: '修行日常'
},
{
id: 10,
url: 'https://cdn.ipadbiz.cn/mlaj/images/Oalh2MojUuk.jpg',
height: 340,
title: '集体活动'
}
];
// 生成更多瀑布流数据的函数
export const generateWaterfallData = (page = 1, pageSize = 10) => {
const baseImages = waterfallImages;
const startIndex = (page - 1) * pageSize;
const data = [];
for (let i = 0; i < pageSize; i++) {
const baseIndex = i % baseImages.length;
const baseImage = baseImages[baseIndex];
data.push({
...baseImage,
id: startIndex + i + 1,
height: Math.floor(Math.random() * 200) + 250, // 随机高度 250-450px
title: `${baseImage.title} ${Math.floor((startIndex + i) / baseImages.length) + 1}`
});
}
return data;
};
......
......@@ -187,6 +187,9 @@
import { ref, computed, nextTick, onMounted, watch } from 'vue'
import VideoPlayer from '@/components/VideoPlayer.vue'
import { useTitle } from '@vueuse/core';
import { useRouter } from 'vue-router'
const router = useRouter()
// 页面标题
useTitle('西园戒幢律寺三坛大戒法会');
......@@ -322,14 +325,17 @@ const viewMore = (type) => {
case 'masters':
console.log('跳转到三师七证页面')
// 这里可以添加跳转到三师七证页面的逻辑
router.push('/masters')
break
case 'students':
console.log('跳转到戒子页面')
// 这里可以添加跳转到戒子页面的逻辑
router.push('/students')
break
case 'volunteers':
console.log('跳转到义工页面')
// 这里可以添加跳转到义工页面的逻辑
router.push('/volunteers')
break
}
} else {
......@@ -467,6 +473,7 @@ const attachTransitionEndOnce = (looping) => {
const handleNewsClick = (item) => {
console.log('点击新闻:', item)
// 这里可以添加跳转到新闻详情页面的逻辑
router.push({ name: 'NewsDetail', params: { id: item.date } })
}
</script>
......
<template>
<div class="masters-container">
<!-- 一行一个 Item(单列) -->
<section class="single-list">
<div
class="item-card"
v-for="(item, i) in singleItems"
:key="`single-${i}`"
@click="goDetail(item)"
>
<div class="item-image">
<img :src="item.image" :alt="item.name" />
</div>
<div class="item-caption">
<div class="item-role">{{ item.role }}</div>
<div class="item-name">{{ item.name }}</div>
</div>
</div>
</section>
<!-- 一行两个 Item(双列) -->
<section class="grid-two">
<div
class="item-card small"
v-for="(item, i) in gridItems"
:key="`grid-${i}`"
@click="goDetail(item)"
>
<div class="item-image">
<img :src="item.image" :alt="item.name" />
</div>
<div class="item-caption">
<div class="item-role">{{ item.role }}</div>
<div class="item-name">{{ item.name }}</div>
</div>
</div>
</section>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { useTitle } from '@vueuse/core';
const router = useRouter()
useTitle('三師七證')
// 单列数据(示例)
const singleItems = ref([
{
id: 101,
role: '中国·戒幢律寺',
name: '上明仁传师',
image: '/src/assets/images/02 西园戒幢律寺三坛大戒法会/截图/全家福3_副本.jpg'
},
{
id: 102,
role: '中国·戒幢律寺',
name: '上明仁传师',
image: '/src/assets/images/02 西园戒幢律寺三坛大戒法会/截图/全家福3_副本.jpg'
},
{
id: 103,
role: '中国·戒幢律寺',
name: '上明仁传师',
image: '/src/assets/images/02 西园戒幢律寺三坛大戒法会/截图/全家福3_副本.jpg'
}
])
// 双列数据(示例)
const gridItems = ref([
{
id: 201,
role: '传戒师',
name: '上智和法师',
image: '/src/assets/images/02 西园戒幢律寺三坛大戒法会/截图/全家福3_副本.jpg'
},
{
id: 202,
role: '教授师',
name: '上德弘法师',
image: '/src/assets/images/02 西园戒幢律寺三坛大戒法会/截图/全家福3_副本.jpg'
},
{
id: 203,
role: '羯磨师',
name: '上寂明法师',
image: '/src/assets/images/02 西园戒幢律寺三坛大戒法会/截图/全家福3_副本.jpg'
},
{
id: 204,
role: '得戒和尚',
name: '上慧觉法师',
image: '/src/assets/images/02 西园戒幢律寺三坛大戒法会/截图/全家福3_副本.jpg'
},
{
id: 205,
role: '引礼师',
name: '上圆道法师',
image: '/src/assets/images/02 西园戒幢律寺三坛大戒法会/截图/全家福3_副本.jpg'
},
{
id: 206,
role: '开导师',
name: '上演真法师',
image: '/src/assets/images/02 西园戒幢律寺三坛大戒法会/截图/全家福3_副本.jpg'
}
])
const goDetail = (item) => {
router.push(`/masters/${item.id}`)
}
</script>
<style scoped>
/* 页面容器 */
.masters-container {
padding: 1.5rem;
background: #F2EBDB;
}
/* 单列列表区 */
.single-list {
display: flex;
flex-direction: column;
gap: 1rem;
margin-bottom: 1rem;
}
/* 双列网格区 */
.grid-two {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.75rem;
}
/* 卡片 */
.item-card {
position: relative;
background: #fff;
border: 2px solid #6B4102;
overflow: hidden;
transition: transform 0.2s ease;
padding: 0.5rem;
}
.item-card:hover {
transform: translateY(-0.125rem);
}
.item-card.small {
/* 双列卡片视觉上更紧凑 */
border: 1px solid rgba(107, 65, 2, 0.8);
}
/* 图片区域 - 采用固定纵向比例 */
.item-image {
width: 100%;
aspect-ratio: 3 / 4;
background: #f5f5f5;
}
.item-image img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
/* 底部说明条 */
.item-caption {
position: absolute;
left: 0;
right: 0;
bottom: 0;
background: rgba(107, 65, 2, 0.8);
color: #fff;
padding: 0.75rem;
text-align: center;
}
.item-role {
font-size: 0.75rem;
opacity: 0.95;
}
.item-name {
font-size: 0.875rem;
font-weight: 600;
margin-top: 0.25rem;
}
/* 响应式调整 */
@media (max-width: 48rem) {
.item-caption {
padding: 0.625rem;
margin: 0.5rem;
}
}
@media (max-width: 30rem) {
.masters-container {
padding: 0.75rem;
}
.item-caption {
padding: 0.5rem;
margin: 0.5rem;
}
.item-name {
font-size: 0.8125rem;
}
}
@media (max-width: 20rem) {
.grid-two {
gap: 0.5rem;
}
.item-caption {
padding: 0.375rem;
margin: 0.5rem;
}
.item-name {
font-size: 0.75rem;
}
}
</style>
<!--
* @Date: 2025-10-30 20:00:25
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-10-30 20:50:36
* @FilePath: /stdj_h5/src/views/MastersDetail.vue
* @Description: 文件描述
-->
<template>
<div class="masters-detail-container">
<section class="single-list">
<div class="item-card">
</div>
</section>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useTitle } from '@vueuse/core';
useTitle('三師七證 - 詳情')
</script>
<style scoped>
.masters-detail-container {
padding: 1.5rem;
background: #F2EBDB;
min-height: 100vh; /* 背景至少覆盖整个视口高度 */
width: 100%;
box-sizing: border-box;
}
.single-list {
display: flex;
flex-direction: column;
gap: 1rem;
margin-bottom: 1rem;
}
.item-card {
position: relative;
background: #fff;
border: 2px solid #6B4102;
overflow: hidden;
transition: transform 0.2s ease;
padding: 0.5rem;
}
</style>
<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">
<!-- 文章头部 -->
......@@ -14,152 +7,21 @@
<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>
<van-icon name="clock-o" /><span class="publish-date">{{ article.publishDate }}</span>
<van-icon name="manager-o" /><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>
......@@ -177,158 +39,26 @@ const commentText = ref('')
// 文章数据
const article = ref({
id: 1,
title: '三坛大戒传戒法会圆满举行',
title: '三坛大戒 | 护戒胜福田 成就最上因',
content: `
<p>2024年春季三坛大戒传戒法会于今日在本寺圆满举行。此次法会历时21天,共有来自全国各地的68位戒子参加,在诸位戒师的慈悲教导下,圆满受持三坛大戒。</p>
<p>三坛大戒是佛教出家众必须受持的重要戒律,包括沙弥戒、比丘戒和菩萨戒三个层次。通过严格的戒律学习和实践,戒子们不仅在戒律知识上有了深入的理解,更在修行品格上得到了显著提升。</p>
<p>在传戒期间,戒子们每日早起晚睡,精进修学。从戒律理论的学习到实际生活中的践行,从个人修持到集体共修,每一个环节都体现了佛教戒律的庄严与神圣。</p>
<p>本次传戒法会得到了十方善信的大力护持,义工菩萨们日夜辛劳,为法会的顺利进行提供了有力保障。在此向所有护持法会的善信表示衷心感谢。</p>
<p>愿诸位新戒比丘能够严持净戒,精进修行,早证菩提,广度众生。愿佛法久住,法轮常转,众生离苦得乐。</p>
<p>为弘承世尊遗教,光大如来戒法,绍隆佛种,续佛慧命,经研究并报中国佛教协会批复同意(中佛会〔2025〕35号),由江苏省佛教协会主办,苏州市佛教协会、苏州市姑苏区佛教协会、苏州工业园区佛教协会协办,苏州西园戒幢律寺、苏州工业园区积善寺承办的传戒三坛大戒法会定于2025年10月15日至11月15日举办。</p>
<h3>天增上三学,以戒为首</h3>
<h3>佛灭度后,以戒为师</h3>
<p>《梵网经》云:"众生受佛戒,即入诸佛位。位同大觉已,真是诸佛子。"传戒法会,具称"千佛三坛大戒法会",三坛大戒是成就僧人最重要的增上缘仪式,谓授千佛共同所制戒。戒场分非同一般之法会场所,庄妙标有,无过之者。</p>
<p>今苏州西园戒幢律寺作为主戒场传戒三坛大戒,正值因时,其事庄严,此意成就道场通达。</p>
`,
author: '释智慧法师',
publishDate: '2024-01-15',
viewCount: 1256,
likeCount: 89,
author: '戒幢律寺编辑部',
publishDate: '2025-10-1',
viewCount: 2156,
likeCount: 156,
isLiked: false,
isCollected: false,
coverImage: null,
tags: ['三坛大戒', '传戒法会', '戒律', '修行']
coverImage: 'https://images.unsplash.com/photo-1545558014-8692077e9b5c?w=800&h=600&fit=crop',
})
// 相关文章
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
......@@ -340,339 +70,89 @@ onMounted(() => {
<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;
background: #f5f5f5;
}
.content-container {
padding-top: 46px;
background-color: #F2EBDB;
}
.article-header {
background: white;
padding: 20px;
margin-bottom: 12px;
padding: 1rem;
}
.article-title {
font-size: 24px;
font-size: 1.25rem;
font-weight: 700;
color: #333;
color: #8b4513;
line-height: 1.4;
margin: 0 0 16px 0;
margin: 0 0 1rem 0;
text-align: center;
}
.article-meta {
display: flex;
justify-content: space-between;
justify-content: center;
align-items: center;
margin-bottom: 16px;
margin-bottom: 1rem;
gap: 1rem;
}
.meta-info {
display: flex;
gap: 16px;
gap: 1rem;
align-items: center;
}
.publish-date,
.author {
font-size: 14px;
color: #666;
}
.article-stats {
display: flex;
gap: 16px;
}
.view-count,
.like-count {
font-size: 0.875rem;
color: #8b4513;
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;
gap: 0.25rem;
}
.article-cover {
background: white;
padding: 0 20px 20px;
margin-bottom: 12px;
margin: 1rem 0;
}
.article-cover img {
width: 100%;
border-radius: 8px;
border-radius: 0.5rem;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.article-content {
background: white;
padding: 20px;
margin-bottom: 12px;
background: #f9f7f4;
padding: 1.5rem;
margin: 1rem 0;
border-radius: 1rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
border: 1px solid #e8e3dc;
}
.content-text {
font-size: 16px;
font-size: 1rem;
line-height: 1.8;
color: #333;
color: #5d4e37;
}
.content-text :deep(p) {
margin: 0 0 16px 0;
margin: 0 0 1rem 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;
.content-text :deep(h3) {
font-size: 1.125rem;
font-weight: 600;
color: #333;
margin: 0 0 16px 0;
padding-left: 4px;
border-left: 4px solid #3b82f6;
color: #8b4513;
text-align: center;
margin: 1.5rem 0 1rem 0;
text-indent: 0;
}
.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;
.content-text :deep(p:last-child) {
margin-bottom: 0;
}
</style>
\ No newline at end of file
</style>
......
<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
<!--
* @Date: 2025-10-30 20:52:19
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-10-30 21:22:42
* @FilePath: /stdj_h5/src/views/Students.vue
* @Description: 戒子页面 - 图片瀑布流展示
-->
<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 class="students-container">
<!-- 瀑布流内容 -->
<div class="waterfall-content">
<van-list
v-model:loading="loading"
:finished="finished"
finished-text="没有更多了"
@load="onLoad"
>
<div class="waterfall-container">
<div class="waterfall-column" v-for="(column, index) in columns" :key="index">
<div
class="waterfall-item"
v-for="item in column"
:key="item.id"
@click="onImageClick(item)"
>
<div class="image-wrapper">
<img
:src="item.url"
:alt="item.title"
:style="{ height: item.height + 'px' }"
@load="onImageLoad"
@error="onImageError"
/>
<div class="image-overlay">
<span class="image-title">{{ item.title }}</span>
</div>
</div>
</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>
</van-list>
</div>
<!-- 遮罩层弹窗 -->
<van-overlay :show="showOverlay" @click="closeOverlay">
<div class="overlay-content" @click.stop>
<!-- 关闭按钮 -->
<div class="close-btn" @click="closeOverlay">
<van-icon name="cross" size="1.5rem" color="#fff" />
</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 class="overlay-image-wrapper">
<img
:src="selectedImage?.url"
:alt="selectedImage?.title"
class="overlay-image"
/>
</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 class="overlay-description">
<h3 class="overlay-title">{{ selectedImage?.title }}</h3>
<p class="overlay-text">{{ selectedImage?.description }}</p>
</div>
</div>
<!-- 空状态 -->
<van-empty v-if="filteredStudents.length === 0" description="暂无戒子信息" />
</div>
</van-overlay>
</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'
import { ref, reactive, onMounted } from 'vue'
import { generateWaterfallData } from '@/utils/mockData'
import { useTitle } from '@vueuse/core';
useTitle('同戒录')
// 响应式数据
const loading = ref(false)
const finished = ref(false)
const currentPage = ref(1)
const pageSize = 10
const allImages = ref([])
const columns = reactive([[], []])
// 遮罩层相关状态
const showOverlay = ref(false)
const selectedImage = ref(null)
// 加载数据
const onLoad = async () => {
loading.value = true
// 模拟网络延迟
await new Promise(resolve => setTimeout(resolve, 1000))
try {
const newData = generateWaterfallData(currentPage.value, pageSize)
if (newData.length === 0) {
finished.value = true
} else {
allImages.value.push(...newData)
distributeImages(newData)
currentPage.value++
}
} catch (error) {
console.error('加载数据失败:', error)
} finally {
loading.value = false
}
return classes[type] || 'precept-novice'
}
// 获取戒律类型文本
const getPreceptText = (type) => {
const texts = {
novice: '沙弥',
bhiksu: '比丘',
bodhisattva: '菩萨'
}
return texts[type] || '沙弥'
}
// 分配图片到两列
const distributeImages = (images) => {
images.forEach(image => {
// 计算两列的当前高度
const leftHeight = columns[0].reduce((sum, item) => sum + item.height + 20, 0)
const rightHeight = columns[1].reduce((sum, item) => sum + item.height + 20, 0)
// 获取状态样式类
const getStatusClass = (status) => {
const classes = {
studying: 'status-studying',
graduated: 'status-graduated',
suspended: 'status-suspended'
}
return classes[status] || 'status-studying'
// 将图片添加到高度较小的列
if (leftHeight <= rightHeight) {
columns[0].push(image)
} else {
columns[1].push(image)
}
})
}
// 获取状态文本
const getStatusText = (status) => {
const texts = {
studying: '在学',
graduated: '毕业',
suspended: '暂停'
}
return texts[status] || '在学'
// 图片点击事件
const onImageClick = (item) => {
console.log('点击图片:', item)
selectedImage.value = item
showOverlay.value = true
}
// 处理标签切换
const handleTabChange = (name) => {
activeTab.value = name
// 关闭遮罩层
const closeOverlay = () => {
showOverlay.value = false
selectedImage.value = null
}
// 处理戒子点击
const handleStudentClick = (student) => {
router.push(`/students/${student.id}`)
// 图片加载成功
const onImageLoad = (event) => {
console.log('图片加载成功:', event.target.src)
}
// 处理搜索
const handleSearch = () => {
Toast('搜索功能开发中...')
// 图片加载失败
const onImageError = (event) => {
console.error('图片加载失败:', event.target.src)
// 可以设置默认图片
event.target.src = 'https://via.placeholder.com/300x400?text=加载失败'
}
// 组件挂载时初始化
onMounted(() => {
// 初始加载第一页数据
onLoad()
})
</script>
<style scoped>
.page-container {
.students-container {
background-color: #F2EBDB;
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;
.header {
position: sticky;
top: 0;
z-index: 100;
background-color: #fff;
}
.content-container {
padding-top: 46px;
.waterfall-content {
padding: 1rem;
}
.stats-section {
.waterfall-container {
display: flex;
gap: 12px;
padding: 16px;
gap: 0.75rem;
align-items: flex-start;
}
.stat-card {
.waterfall-column {
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);
flex-direction: column;
gap: 0.75rem;
}
.stat-icon {
font-size: 24px;
margin-right: 12px;
.waterfall-item {
background-color: #fff;
border-radius: 0.5rem;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: transform 0.2s ease, box-shadow 0.2s ease;
cursor: pointer;
}
.stat-info {
flex: 1;
.waterfall-item:hover {
transform: translateY(-2px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
}
.stat-number {
font-size: 20px;
font-weight: 700;
color: #8b5cf6;
margin-bottom: 2px;
.image-wrapper {
position: relative;
overflow: hidden;
}
.stat-label {
font-size: 12px;
color: #666;
.image-wrapper img {
width: 100%;
display: block;
object-fit: cover;
transition: transform 0.3s ease;
}
.filter-section {
background: white;
border-bottom: 1px solid #eee;
.waterfall-item:hover .image-wrapper img {
transform: scale(1.05);
}
.custom-tabs :deep(.van-tab) {
font-weight: 500;
.image-overlay {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: linear-gradient(transparent, rgba(0, 0, 0, 0.7));
padding: 1rem 0.75rem 0.75rem;
transform: translateY(100%);
transition: transform 0.3s ease;
}
.custom-tabs :deep(.van-tab--active) {
color: #8b5cf6;
.waterfall-item:hover .image-overlay {
transform: translateY(0);
}
.custom-tabs :deep(.van-tabs__line) {
background: #8b5cf6;
.image-title {
color: #fff;
font-size: 0.875rem;
font-weight: 500;
line-height: 1.4;
}
.students-list {
padding: 16px;
/* 加载状态样式 */
:deep(.van-list__loading) {
padding: 1rem;
text-align: center;
color: #969799;
}
.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;
:deep(.van-list__finished-text) {
padding: 1rem;
text-align: center;
color: #969799;
font-size: 0.875rem;
}
.student-card:hover {
transform: translateY(-3px);
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15);
}
/* 响应式设计 */
@media (max-width: 480px) {
.waterfall-content {
padding: 0.75rem;
}
.student-card:active {
transform: translateY(-1px);
box-shadow: 0 3px 12px rgba(0, 0, 0, 0.12);
.waterfall-container {
gap: 0.5rem;
}
.waterfall-column {
gap: 0.5rem;
}
.image-title {
font-size: 0.8125rem;
}
}
.student-avatar {
position: relative;
width: 60px;
height: 60px;
border-radius: 50%;
overflow: hidden;
margin-right: 16px;
flex-shrink: 0;
/* 骨架屏效果 */
.waterfall-item.loading {
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: loading 1.5s infinite;
}
.student-avatar img {
width: 100%;
height: 100%;
object-fit: cover;
@keyframes loading {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
.avatar-placeholder {
width: 100%;
height: 100%;
background: linear-gradient(135deg, #8b5cf6, #7c3aed);
/* 遮罩层样式 */
:deep(.van-overlay) {
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 24px;
font-weight: 600;
padding: 1rem;
}
.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;
.overlay-content {
background-color: #fff;
border-radius: 1rem;
max-width: 90vw;
max-height: 80vh;
overflow: hidden;
position: relative;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
}
.student-header {
.close-btn {
position: absolute;
top: 1rem;
right: 1rem;
width: 2.5rem;
height: 2.5rem;
background-color: rgba(0, 0, 0, 0.5);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
}
.student-name {
font-size: 18px;
font-weight: 600;
color: #333;
margin: 0;
justify-content: center;
cursor: pointer;
z-index: 10;
transition: background-color 0.2s ease;
}
.student-status {
padding: 4px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
.close-btn:hover {
background-color: rgba(0, 0, 0, 0.7);
}
.status-studying {
background: #dbeafe;
color: #1d4ed8;
border: 1px solid #93c5fd;
.overlay-image-wrapper {
width: 100%;
max-height: 60vh;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
background-color: #f5f5f5;
}
.status-graduated {
background: #dcfce7;
color: #166534;
border: 1px solid #86efac;
.overlay-image {
width: 100%;
height: auto;
max-height: 60vh;
object-fit: contain;
display: block;
}
.status-suspended {
background: #fef3c7;
color: #92400e;
border: 1px solid #fcd34d;
.overlay-description {
padding: 1.5rem;
background-color: #fff;
}
.student-details {
space-y: 4px;
.overlay-title {
font-size: 1.25rem;
font-weight: 600;
color: #333;
margin: 0 0 1rem 0;
line-height: 1.4;
}
.student-temple {
font-size: 16px;
.overlay-text {
font-size: 1rem;
color: #666;
margin: 0 0 4px 0;
line-height: 1.6;
margin: 0;
white-space: pre-wrap;
}
.student-teacher {
font-size: 14px;
color: #999;
margin: 0 0 8px 0;
}
/* 移动端适配 */
@media (max-width: 480px) {
.overlay-content {
max-width: 95vw;
max-height: 85vh;
border-radius: 0.75rem;
}
.student-meta {
display: flex;
gap: 16px;
}
.close-btn {
top: 0.75rem;
right: 0.75rem;
width: 2rem;
height: 2rem;
}
.entry-date,
.progress {
font-size: 12px;
color: #999;
background: #f5f5f5;
padding: 2px 6px;
border-radius: 4px;
}
.overlay-description {
padding: 1rem;
}
.student-actions {
margin-left: 12px;
color: #ccc;
.overlay-title {
font-size: 1.125rem;
}
.overlay-text {
font-size: 0.875rem;
}
}
</style>
\ No newline at end of file
</style>
......
<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>
<!--
* @Date: 2025-01-01 15:20:00
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-10-30 21:15:28
* @FilePath: /stdj_h5/src/views/Volunteers.vue
* @Description: 义工页面 - 图片瀑布流展示
-->
<!-- 筛选栏 -->
<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>
<template>
<div class="volunteers-container">
<!-- 瀑布流内容 -->
<div class="waterfall-content">
<van-list
v-model:loading="loading"
:finished="finished"
finished-text="没有更多了"
@load="onLoad"
>
<div class="waterfall-container">
<div class="waterfall-column" v-for="(column, index) in columns" :key="index">
<div
class="waterfall-item"
v-for="item in column"
:key="item.id"
@click="onImageClick(item)"
>
<div class="image-wrapper">
<img
:src="item.url"
:alt="item.title"
:style="{ height: item.height + 'px' }"
@load="onImageLoad"
@error="onImageError"
/>
<div class="image-overlay">
<span class="image-title">{{ item.title }}</span>
</div>
</div>
</div>
</div>
<div class="volunteer-actions">
<van-icon name="arrow" />
</div>
</div>
</div>
<!-- 空状态 -->
<van-empty v-if="filteredVolunteers.length === 0" description="暂无义工信息" />
</van-list>
</div>
<!-- 浮动按钮 -->
<van-floating-bubble
axis="xy"
icon="plus"
@click="handleAddVolunteer"
class="add-button"
/>
<!-- 遮罩层弹窗 -->
<van-overlay :show="showOverlay" @click="closeOverlay">
<div class="overlay-content" @click.stop>
<!-- 关闭按钮 -->
<div class="close-btn" @click="closeOverlay">
<van-icon name="cross" size="1.5rem" color="#fff" />
</div>
<!-- 图片展示 -->
<div class="overlay-image-wrapper">
<img
:src="selectedImage?.url"
:alt="selectedImage?.title"
class="overlay-image"
/>
</div>
<!-- 描述内容 -->
<div class="overlay-description">
<h3 class="overlay-title">{{ selectedImage?.title }}</h3>
<p class="overlay-text">{{ selectedImage?.description }}</p>
</div>
</div>
</van-overlay>
</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
import { ref, reactive, onMounted } from 'vue'
import { generateWaterfallData } from '@/utils/mockData'
import { useTitle } from '@vueuse/core';
useTitle('义工')
// 响应式数据
const loading = ref(false)
const finished = ref(false)
const currentPage = ref(1)
const pageSize = 10
const allImages = ref([])
const columns = reactive([[], []])
// 遮罩层相关状态
const showOverlay = ref(false)
const selectedImage = ref(null)
// 加载数据
const onLoad = async () => {
loading.value = true
// 模拟网络延迟
await new Promise(resolve => setTimeout(resolve, 1000))
try {
const newData = generateWaterfallData(currentPage.value, pageSize)
if (newData.length === 0) {
finished.value = true
} else {
allImages.value.push(...newData)
distributeImages(newData)
currentPage.value++
}
} catch (error) {
console.error('加载数据失败:', error)
} finally {
loading.value = false
}
])
}
// 过滤后的义工列表
const filteredVolunteers = computed(() => {
if (activeTab.value === 'all') {
return volunteers.value
}
return volunteers.value.filter(volunteer => volunteer.status === activeTab.value)
})
// 分配图片到两列
const distributeImages = (images) => {
images.forEach(image => {
// 计算两列的当前高度
const leftHeight = columns[0].reduce((sum, item) => sum + item.height + 20, 0)
const rightHeight = columns[1].reduce((sum, item) => sum + item.height + 20, 0)
// 获取状态样式类
const getStatusClass = (status) => {
const classes = {
active: 'status-active',
rest: 'status-rest',
leave: 'status-leave'
}
return classes[status] || 'status-rest'
// 将图片添加到高度较小的列
if (leftHeight <= rightHeight) {
columns[0].push(image)
} else {
columns[1].push(image)
}
})
}
// 获取状态文本
const getStatusText = (status) => {
const texts = {
active: '在岗',
rest: '休息',
leave: '请假'
}
return texts[status] || '休息'
// 图片点击事件
const onImageClick = (item) => {
console.log('点击图片:', item)
selectedImage.value = item
showOverlay.value = true
}
// 获取等级样式类
const getLevelClass = (level) => {
if (level === '组长') return 'level-leader'
if (level === '资深义工') return 'level-senior'
return 'level-normal'
// 关闭遮罩层
const closeOverlay = () => {
showOverlay.value = false
selectedImage.value = null
}
// 处理标签切换
const handleTabChange = (name) => {
activeTab.value = name
// 图片加载成功
const onImageLoad = (event) => {
console.log('图片加载成功:', event.target.src)
}
// 处理义工点击
const handleVolunteerClick = (volunteer) => {
router.push(`/volunteers/${volunteer.id}`)
// 图片加载失败
const onImageError = (event) => {
console.error('图片加载失败:', event.target.src)
// 可以设置默认图片
event.target.src = 'https://via.placeholder.com/300x400?text=加载失败'
}
// 处理添加义工
const handleAddVolunteer = () => {
Toast('添加义工功能开发中...')
}
// 组件挂载时初始化
onMounted(() => {
// 初始加载第一页数据
onLoad()
})
</script>
<style scoped>
.page-container {
.volunteers-container {
background-color: #F2EBDB;
min-height: 100vh;
background: #fafafa;
background-color: #f5f5f5;
}
.custom-nav {
background: linear-gradient(135deg, #fbbf24, #f97316);
color: white;
.header {
position: sticky;
top: 0;
z-index: 100;
background-color: #fff;
}
.custom-nav :deep(.van-nav-bar__title) {
color: white;
font-weight: 600;
}
.custom-nav :deep(.van-icon) {
color: white;
.waterfall-content {
padding: 1rem;
}
.content-container {
padding-top: 46px;
}
.stats-section {
.waterfall-container {
display: flex;
gap: 12px;
padding: 16px;
gap: 0.75rem;
align-items: flex-start;
}
.stat-card {
.waterfall-column {
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;
flex-direction: column;
gap: 0.75rem;
}
.volunteer-card {
background: white;
border-radius: 12px;
padding: 16px;
margin-bottom: 12px;
display: flex;
align-items: center;
gap: 16px;
.waterfall-item {
background-color: #fff;
border-radius: 0.5rem;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: transform 0.2s ease, box-shadow 0.2s ease;
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);
.waterfall-item:hover {
transform: translateY(-2px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
}
.volunteer-avatar {
.image-wrapper {
position: relative;
width: 60px;
height: 60px;
border-radius: 50%;
overflow: hidden;
margin-right: 16px;
flex-shrink: 0;
}
.volunteer-avatar img {
.image-wrapper img {
width: 100%;
height: 100%;
display: block;
object-fit: cover;
transition: transform 0.3s ease;
}
.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;
.waterfall-item:hover .image-wrapper img {
transform: scale(1.05);
}
.status-badge {
.image-overlay {
position: absolute;
bottom: -2px;
right: -2px;
padding: 2px 6px;
border-radius: 8px;
font-size: 10px;
font-weight: 500;
border: 2px solid white;
bottom: 0;
left: 0;
right: 0;
background: linear-gradient(transparent, rgba(0, 0, 0, 0.7));
padding: 1rem 0.75rem 0.75rem;
transform: translateY(100%);
transition: transform 0.3s ease;
}
.status-active {
background: #10b981;
color: white;
.waterfall-item:hover .image-overlay {
transform: translateY(0);
}
.status-rest {
background: #6b7280;
color: white;
.image-title {
color: #fff;
font-size: 0.875rem;
font-weight: 500;
line-height: 1.4;
}
.status-leave {
background: #ef4444;
color: white;
/* 加载状态样式 */
:deep(.van-list__loading) {
padding: 1rem;
text-align: center;
color: #969799;
}
.volunteer-info {
flex: 1;
:deep(.van-list__finished-text) {
padding: 1rem;
text-align: center;
color: #969799;
font-size: 0.875rem;
}
.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;
}
/* 响应式设计 */
@media (max-width: 480px) {
.waterfall-content {
padding: 0.75rem;
}
.volunteer-level {
padding: 4px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
}
.waterfall-container {
gap: 0.5rem;
}
.level-leader {
background: linear-gradient(135deg, #fbbf24, #f97316);
color: white;
}
.waterfall-column {
gap: 0.5rem;
}
.level-senior {
background: #dbeafe;
color: #1d4ed8;
border: 1px solid #93c5fd;
.image-title {
font-size: 0.8125rem;
}
}
.level-normal {
background: #f3f4f6;
color: #6b7280;
border: 1px solid #d1d5db;
/* 骨架屏效果 */
.waterfall-item.loading {
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: loading 1.5s infinite;
}
.volunteer-details {
space-y: 4px;
@keyframes loading {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
.volunteer-department {
font-size: 16px;
color: #666;
margin: 0 0 4px 0;
/* 遮罩层样式 */
:deep(.van-overlay) {
display: flex;
align-items: center;
justify-content: center;
padding: 1rem;
}
.volunteer-task {
font-size: 14px;
color: #999;
margin: 0 0 8px 0;
.overlay-content {
background-color: #fff;
border-radius: 1rem;
max-width: 90vw;
max-height: 80vh;
overflow: hidden;
position: relative;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
}
.volunteer-meta {
.close-btn {
position: absolute;
top: 1rem;
right: 1rem;
width: 2.5rem;
height: 2.5rem;
background-color: rgba(0, 0, 0, 0.5);
border-radius: 50%;
display: flex;
gap: 16px;
align-items: center;
justify-content: center;
cursor: pointer;
z-index: 10;
transition: background-color 0.2s ease;
}
.join-date,
.service-hours {
font-size: 12px;
color: #999;
background: #f5f5f5;
padding: 2px 6px;
border-radius: 4px;
.close-btn:hover {
background-color: rgba(0, 0, 0, 0.7);
}
.volunteer-actions {
margin-left: 12px;
color: #ccc;
.overlay-image-wrapper {
width: 100%;
max-height: 60vh;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
background-color: #f5f5f5;
}
.add-button {
background: linear-gradient(135deg, #fbbf24, #f97316) !important;
transition: all 0.3s ease;
animation: float 3s ease-in-out infinite;
.overlay-image {
width: 100%;
height: auto;
max-height: 60vh;
object-fit: contain;
display: block;
}
.add-button:hover {
transform: translateY(-3px) scale(1.1);
box-shadow: 0 8px 25px rgba(251, 191, 36, 0.6);
.overlay-description {
padding: 1.5rem;
background-color: #fff;
}
.add-button:active {
transform: translateY(-1px) scale(1.05);
box-shadow: 0 4px 16px rgba(251, 191, 36, 0.4);
.overlay-title {
font-size: 1.25rem;
font-weight: 600;
color: #333;
margin: 0 0 1rem 0;
line-height: 1.4;
}
.add-button :deep(.van-floating-bubble__icon) {
color: white;
.overlay-text {
font-size: 1rem;
color: #666;
line-height: 1.6;
margin: 0;
white-space: pre-wrap;
}
@keyframes float {
0%, 100% {
transform: translateY(0px);
/* 移动端适配 */
@media (max-width: 480px) {
.overlay-content {
max-width: 95vw;
max-height: 85vh;
border-radius: 0.75rem;
}
.close-btn {
top: 0.75rem;
right: 0.75rem;
width: 2rem;
height: 2rem;
}
.overlay-description {
padding: 1rem;
}
.overlay-title {
font-size: 1.125rem;
}
50% {
transform: translateY(-10px);
.overlay-text {
font-size: 0.875rem;
}
}
</style>
\ No newline at end of file
</style>
......