deceasedSearch.vue 供奉记录页面框架

This commit is contained in:
minimaxagent1 2025-08-11 16:02:00 +08:00
parent 2a7e874ae3
commit 814d622295
8 changed files with 1060 additions and 23 deletions

18
api/memorial/memorial.js Normal file
View File

@ -0,0 +1,18 @@
import request from '@/utils/request'
// 获取牌位详情
export function getMemorialDetail(id) {
return request({
url: `/app/memorial/${id}`,
method: 'get'
})
}
// 获取供奉记录列表
export function getEnshrinedList(params) {
return request({
url: '/app/enshrined/list',
method: 'get',
params
})
}

View File

@ -6,7 +6,8 @@ export const CommonEnum = {
BASE_COLOR:"#FAF8F3", //基调颜色
SEARCH: "https://api.ccttiot.com/image-1753769500465.png", //通用搜索图标
TILE: "https://api.ccttiot.com/image-1753750309203.png", //瓦片图片
BOTTOM_TILES:"https://api.ccttiot.com/image-1754446176001.png",//底部瓦片组
BOTTOM_TILES:"https://api.ccttiot.com/image-1754446176001.png",//底部瓦片组背景淡黄
BOTTOM_TILES_2:" https://api.ccttiot.com/image-1754898426052.png",//底部瓦片组背景纯白
FILTER: "https://api.ccttiot.com/image-1753954149098.png", //筛选图标
REFRESH:"https://api.ccttiot.com/%E5%AE%B9%E5%99%A8-1754011714179.png", //刷新图标
NAV_ARROW:"https://api.ccttiot.com/image-1754127104177.png", //导航箭头
@ -17,7 +18,6 @@ export const CommonEnum = {
KongmingLantern:'https://api.ccttiot.com/image-1754376453672.png',//孔明灯
Refresh:'https://api.ccttiot.com/image-1754377032112.png',//刷新
LotusMeditation:'https://api.ccttiot.com/image-1754377169541.png',//莲坐禅心
CENTER_TILES:"https://api.ccttiot.com/image-1754897751419.png",//腰部瓦片组
};
export default CommonEnum;

View File

@ -155,6 +155,20 @@
"enablePullDownRefresh": true,
"backgroundTextStyle": "dark"
}
},
{
"path" : "pages/memorial/enshrinedList",
"style" :
{
"navigationStyle": "custom"
}
},
{
"path" : "pages/memorial/deceasedSearch",
"style" :
{
"navigationStyle": "custom"
}
}
],
"subPackages": [

134
pages/memorial/README.md Normal file
View File

@ -0,0 +1,134 @@
# 供奉记录页面使用说明
## 功能概述
`enshrinedList.vue` 是一个供奉记录展示页面通过调用两个API接口来获取和展示数据
1. **牌位详情API**: `GET /app/memorial/{id}` - 获取牌位基本信息
2. **供奉记录API**: `GET /app/enshrined/list` - 获取该牌位的供奉记录列表
## 页面特性
### 🎨 界面设计
- **牌位信息卡片**: 显示牌位名称和介绍信息
- **供奉记录表格**: 展示供奉人、时间、类型等信息
- **响应式布局**: 适配不同屏幕尺寸
- **传统风格**: 使用佛教主题色彩和设计元素
### 📱 交互功能
- **下拉刷新**: 重新加载最新数据
- **上拉加载**: 分页加载更多记录
- **姓名脱敏**: 保护用户隐私
- **错误处理**: 网络错误时显示重试选项
### 🔧 技术特性
- **并行API调用**: 同时请求两个接口提高加载速度
- **数据格式化**: 日期格式化和姓名脱敏处理
- **状态管理**: 加载状态、错误状态、空状态处理
- **分页支持**: 支持大量数据的分页展示
## API接口说明
### 1. 牌位详情接口
```javascript
GET /app/memorial/{id}
```
**响应数据结构:**
```json
{
"msg": "操作成功",
"code": 200,
"data": {
"id": "16",
"name": "牌位名字",
"code": "A01",
"introduced": "牌位介绍信息",
"extinguishTime": "2025-07-16 14:06:58",
"state": "1",
"contactName": "黄绍春",
"address": "广西壮族自治区南宁市西乡塘区"
}
}
```
### 2. 供奉记录接口
```javascript
GET /app/enshrined/list?memorialId={id}&pageNum=1&pageSize=10
```
**响应数据结构:**
```json
{
"total": 2,
"rows": [
{
"id": "1",
"memorialId": "16",
"worshiperName": "黄绍春",
"thaliName": "贡献三天",
"startDate": "2025-07-08 00:00:00",
"endDate": "2025-07-31 00:00:00",
"isShow": "1"
}
]
}
```
## 使用方法
### 1. 页面跳转
```javascript
// 跳转到供奉记录页面
uni.navigateTo({
url: '/pages/memorial/enshrinedList?id=16'
})
```
### 2. 参数说明
- `id`: 牌位ID必填参数
### 3. 测试页面
访问 `/pages/memorial/test-enshrined` 可以测试API调用和页面功能。
## 文件结构
```
pages/memorial/
├── enshrinedList.vue # 主页面
├── test-enshrined.vue # 测试页面
└── README.md # 说明文档
api/memorial/
└── memorial.js # API接口定义
```
## 样式定制
### 颜色主题
- 主色调: `#A24242` (深红棕色)
- 背景色: `#F5F0E7` (米色)
- 卡片背景: `#FFF1DD` (浅米色)
- 文字颜色: `#522510` (深棕色)
### 布局调整
可以通过修改CSS变量来调整页面样式
```scss
// 修改主色调
$primary-color: #A24242;
$background-color: #F5F0E7;
```
## 注意事项
1. **网络请求**: 确保API接口可正常访问
2. **参数验证**: 传入的牌位ID必须有效
3. **数据格式**: API返回的数据格式必须符合预期
4. **错误处理**: 网络错误时会显示重试按钮
5. **性能优化**: 大量数据时建议调整分页大小
## 更新日志
- **v1.0.0**: 初始版本,支持基本的供奉记录展示
- **v1.1.0**: 添加下拉刷新和上拉加载功能
- **v1.2.0**: 优化UI设计和错误处理

View File

@ -0,0 +1,298 @@
<template>
<view class="offering-modal" v-if="visible" @click="handleClose">
<view class="modal-overlay" @click="handleClose"></view>
<view class="modal-content" @click.stop>
<!-- 关闭按钮 -->
<view class="close-btn" @click="handleClose">
<text class="close-icon">×</text>
</view>
<!-- 标题 -->
<view class="modal-title">牌位供奉</view>
<!-- 供奉时长选择 -->
<view class="duration-section">
<view class="duration-grid">
<view
v-for="option in durationOptions"
:key="option.value"
class="duration-option"
:class="{ 'selected': selectedDuration === option.value }"
@click="selectDuration(option.value)"
>
<text class="duration-text">{{ option.label }}</text>
<text class="duration-price">¥{{ option.price }}</text>
</view>
</view>
</view>
<!-- 供奉人信息 -->
<view class="offerer-section">
<view class="section-label">供奉人</view>
<input
class="offerer-input"
placeholder="请填写供奉人姓名"
v-model="offererName"
maxlength="20"
/>
</view>
<view class="input-tip">将在供奉名单上展现供奉人的姓名此为必填</view>
<!-- 确认按钮 -->
<view class="confirm-section">
<view class="price-info">
<text class="total-price">¥{{ totalPrice }}</text>
</view>
<view class="confirm-btn" @click="handleConfirm">
<text class="btn-text">立即供奉</text>
</view>
</view>
</view>
</view>
</template>
<script>
export default {
name: 'OfferingModal',
props: {
visible: {
type: Boolean,
default: false
}
},
data() {
return {
selectedDuration: '3', // 3
offererName: '',
durationOptions: [
{ label: '供奉3天', value: '3', price: '99.9' },
{ label: '供奉1周', value: '7', price: '129.9' },
{ label: '供奉1月', value: '30', price: '299.9' },
{ label: '供奉1年', value: '365', price: '499.9' }
]
}
},
computed: {
totalPrice() {
const selectedOption = this.durationOptions.find(option => option.value === this.selectedDuration)
if (!selectedOption) return '99.9'
//
//
return selectedOption.price
}
},
methods: {
//
handleClose() {
this.$emit('close')
},
//
selectDuration(value) {
this.selectedDuration = value
},
//
handleConfirm() {
//
if (!this.offererName.trim()) {
uni.showToast({
title: '请填写供奉人姓名',
icon: 'none'
})
return
}
//
const selectedOption = this.durationOptions.find(option => option.value === this.selectedDuration)
//
this.$emit('confirm', {
duration: this.selectedDuration,
durationLabel: selectedOption.label,
price: selectedOption.price,
offererName: this.offererName.trim()
})
}
}
}
</script>
<style lang="scss" scoped>
.offering-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1000;
display: flex;
align-items: flex-end;
}
.modal-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
}
.modal-content {
position: relative;
width: 100%;
background: #FFFBF5;
border-radius: 24rpx 24rpx 0 0;
padding: 40rpx 32rpx 60rpx 32rpx;
box-sizing: border-box;
max-height: 80vh;
overflow-y: auto;
}
.close-btn {
position: absolute;
top: 20rpx;
right: 20rpx;
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
z-index: 10;
}
.close-icon {
font-size: 40rpx;
color: #999;
font-weight: bold;
}
.modal-title {
color: #695347;
text-align: left;
margin-bottom: 40rpx;
padding-top: 20rpx;
font-weight: 400;
font-size: 32rpx;
line-height: 44rpx;
}
.duration-section {
margin-bottom: 40rpx;
}
.duration-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20rpx;
}
.duration-option {
background: #FFF1DD;
border-radius: 12rpx;
padding: 24rpx 20rpx;
text-align: center;
border: 2rpx solid transparent;
transition: all 0.3s ease;
cursor: pointer;
&.selected {
background: #A24242;
border-color: #A24242;
.duration-text,
.duration-price {
color: #fff;
}
}
}
.duration-text {
display: block;
font-size: 28rpx;
color: #C7A26D;
margin-bottom: 8rpx;
font-weight: 500;
}
.duration-price {
display: block;
font-size: 32rpx;
color: #C7A26D;
font-weight: 600;
}
.offerer-section {
display: flex;
margin-bottom: 40rpx;
align-items: center;
width: 686rpx;
height: 112rpx;
border-radius: 16rpx 16rpx 16rpx 16rpx;
border: 2rpx solid #A24242;
}
.section-label {
font-size: 32rpx;
color: #522510;
font-weight: 500;
margin:34rpx 70rpx 34rpx 54rpx;
width: 196rpx;
}
.offerer-input {
width: 100%;
height: 80rpx;
border-radius: 8rpx;
font-size: 28rpx;
color: #ACACAC;
box-sizing: border-box;
margin:34rpx 0 34rpx 0;
}
.input-tip {
font-weight: 400;
font-size: 28rpx;
color: #695347;
line-height: 38rpx;
text-align: center;
margin-bottom: 30rpx;
}
.confirm-section {
display: flex;
align-items: center;
padding: 24rpx 32rpx;
width: 648rpx;
height: 90rpx;
background: #A24242;
border-radius: 45rpx 45rpx 45rpx 45rpx;
gap: 20rpx;
}
.price-info {
flex: 1;
text-align: right;
}
.total-price {
font-size: 36rpx;
color: #fff;
font-weight: 600;
}
.confirm-btn {
flex: 1;
text-align: left;
}
.btn-text {
font-size: 32rpx;
color: #fff;
font-weight: 500;
}
</style>

View File

@ -0,0 +1,161 @@
<template>
<view class="page">
<base-background />
<!-- 使用自定义导航栏组件 -->
<custom-navbar
ref="customNavbar"
title="供奉记录"/>
<view class="container">
<!-- 状态展示 -->
<status-display
v-if="loading"
type="loading"
loading-text="加载中..."
/>
<!-- 搜索框 -->
<search-box
v-model="searchName"
:width="'682rpx'"
:search-icon="CommonEnum.SEARCH"
placeholder="请输入姓名进行查找"
btn-text="搜索"
@search="handleSearch"
/>
<view class="body">
<view class="center-files">
<image :src="CommonEnum.CENTER_TILES" mode="aspectFit" class="files"></image>
</view>
</view>
</view>
<view class="bottom-files">
<image :src="CommonEnum.BOTTOM_TILES_2" mode="aspectFit" class="files"></image>
</view>
</view>
</template>
<script>
import {CommonEnum} from '@/enum/common.js'
import SearchBox from "../../components/search-box/search-box.vue"
import StatusDisplay from "../../components/status-display/status-display.vue"
import EnshrinedList from "./compositons/enshrinedList.vue"
import FloorSelector from "./compositons/floorSelector.vue"
import StatusBar from "./compositons/statusBar.vue"
import BottomButton from "../../components/bottom-button/bottom-button.vue";
export default {
components: {
BottomButton,
SearchBox,
StatusDisplay,
EnshrinedList,
FloorSelector,
StatusBar
},
data() {
return {
CommonEnum,
searchName: '',
loading: false,
memorialId: '16', // 殿ID
//
defaultFloorId: '',
defaultAreaId: '',
defaultUnitId: '',
//
currentSelection: {
floor: null,
area: null,
unit: null
},
// ID
selectedUnitId: ''
}
},
onLoad(options) {
// 殿ID
if (options.id) {
this.memorialId = options.id
}
this.initPage()
},
methods: {
//
async initPage() {
this.loading = true
try {
//
console.log('往生殿页面初始化ID:', this.memorialId)
} catch (error) {
console.error('页面初始化失败:', error)
uni.showToast({
title: '页面加载失败',
icon: 'none'
})
} finally {
this.loading = false
}
},
//
handleSearch(value) {
console.log('搜索内容:', value)
this.searchName = value
// enshrinedList
},
}
}
</script>
<style lang="scss" scoped>
.page {
width: 100%;
min-height: 100vh;
}
.container {
width: 100%;
//min-height: 100vh;
display: flex;
align-items: center;
flex-direction: column;
padding-bottom: 180rpx;
box-sizing: border-box;
.body{
position: relative;
height:1100rpx;
left: 0;
top: 18rpx;
background: #fff;
.center-files {
position: relative;
width: 100%; //
z-index: 10; //
.files {
width: 750rpx; //
height: 47rpx; //
}
}
}
}
.bottom-files {
position: fixed;
left: 0;
bottom: 0;
width: 100%; //
z-index: 10; //
background-color: #ffffff;
.files {
width: 750rpx; //
height: 90rpx; //
}
}
</style>

View File

@ -0,0 +1,387 @@
<template>
<view class="page">
<base-background />
<!-- 使用自定义导航栏组件 -->
<custom-navbar title="供奉记录" ref="customNavbar"/>
<view class="header">
<!-- 状态展示 -->
<status-display
v-if="loading"
type="loading"
loading-text="加载中..."
/>
<!-- 错误状态 -->
<status-display
v-if="error"
type="error"
:error-text="error"
@retry="fetchData"
/>
<!-- 页面内容 -->
<view v-if="!loading && !error" class="content">
<!-- 牌位信息卡片 -->
<view class="memorial-card">
<view class="memorial-info">
<view class="info-row">
<text class="label">{{ memorialData.name || '暂无' }}</text>
<text class="value">{{ memorialData.code || '暂无' }}</text>
</view>
<view class="memorial-desc" v-if="memorialData.introduced">
{{ memorialData.introduced }}
</view>
</view>
</view>
<!-- 供奉记录列表 -->
<view class="records-section">
<!-- 表头 -->
<view class="table-header">
<view class="header-cell">供奉人</view>
<view class="header-cell">供奉时间</view>
<view class="header-cell">结束时间</view>
<view class="header-cell">供奉类型</view>
</view>
<!-- 记录列表 -->
<view class="records-list">
<view
v-for="(record, index) in enshrinedList"
:key="record.id"
class="record-item"
>
<view class="record-cell">
<text class="worshiper-name">{{ formatWorshiperName(record.worshiperName) }}</text>
</view>
<view class="record-cell">
<text class="date-text">{{ formatDate(record.startDate) }}</text>
</view>
<view class="record-cell">
<text class="date-text">{{ formatDate(record.endDate) }}</text>
</view>
<view class="record-cell">
<text class="offering-type">{{ record.thaliName }}</text>
</view>
</view>
</view>
<!-- 空状态 -->
<view v-if="enshrinedList.length === 0" class="empty-state">
<text class="empty-text">暂无供奉记录</text>
</view>
<!-- 加载更多提示 -->
<view v-if="enshrinedList.length > 0 && enshrinedList.length < total" class="load-more">
<text class="load-more-text">上拉加载更多</text>
</view>
<!-- 全部加载完成提示 -->
<view v-if="enshrinedList.length > 0 && enshrinedList.length >= total" class="load-complete">
<text class="load-complete-text">已加载全部记录</text>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
import { getMemorialDetail, getEnshrinedList } from '@/api/memorial/memorial.js'
export default {
data() {
return {
loading: false,
error: '',
memorialId: '', // ID
memorialData: {}, //
enshrinedList: [], //
pageNum: 1,
pageSize: 10,
total: 0
}
},
onLoad(options) {
// ID
this.memorialId = options.id || '16'
this.fetchData()
},
//
onPullDownRefresh() {
this.pageNum = 1
this.fetchData().then(() => {
uni.stopPullDownRefresh()
})
},
//
onReachBottom() {
if (this.enshrinedList.length < this.total) {
this.pageNum++
this.loadMoreData()
}
},
methods: {
//
async fetchData() {
this.loading = true
this.error = ''
try {
// API
const [memorialRes, enshrinedRes] = await Promise.all([
this.fetchMemorialDetail(),
this.fetchEnshrinedList()
])
//
if (memorialRes.code === 200) {
this.memorialData = memorialRes.data || {}
}
//
if (enshrinedRes.code === 200) {
this.enshrinedList = enshrinedRes.rows || []
this.total = enshrinedRes.total || 0
}
} catch (error) {
console.error('获取数据失败:', error)
this.error = '网络错误,请重试'
uni.showToast({
title: '获取数据失败',
icon: 'none'
})
} finally {
this.loading = false
}
},
//
async fetchMemorialDetail() {
return await getMemorialDetail(this.memorialId)
},
//
async fetchEnshrinedList() {
const params = {
memorialId: this.memorialId,
pageNum: this.pageNum,
pageSize: this.pageSize
}
return await getEnshrinedList(params)
},
//
async loadMoreData() {
try {
const res = await this.fetchEnshrinedList()
if (res.code === 200) {
const newRecords = res.rows || []
this.enshrinedList = [...this.enshrinedList, ...newRecords]
this.total = res.total || 0
}
} catch (error) {
console.error('加载更多数据失败:', error)
this.pageNum-- // 退
uni.showToast({
title: '加载失败',
icon: 'none'
})
}
},
//
formatWorshiperName(name) {
if (!name) return '未知'
if (name.length <= 2) return name
return name.charAt(0) + '*' + name.charAt(name.length - 1)
},
//
formatDate(dateStr) {
if (!dateStr) return '未知'
const date = new Date(dateStr)
if (isNaN(date.getTime())) return '未知'
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
}
}
}
</script>
<style lang="scss" scoped>
.page {
width: 100%;
min-height: 100vh;
}
.header {
width: 100%;
min-height: 100vh;
display: flex;
align-items: flex-start;
flex-direction: column;
padding: 0 30rpx 40rpx 30rpx;
}
.content {
width: 100%;
padding-top: 20rpx;
}
//
.memorial-card {
border-radius: 16rpx;
padding: 40rpx;
margin-bottom: 40rpx;
width: 680rpx;
height: 230rpx;
background: #FFFBF5;
border: 1rpx solid #C7A26D;
}
.memorial-info {
.info-row {
display: flex;
align-items: center;
margin-bottom: 24rpx;
.label {
font-weight: 500;
font-size: 40rpx;
color: #522510;
line-height: 54rpx;
text-align: left;
margin-right: 84rpx;
}
.value {
font-weight: 500;
font-size: 40rpx;
color: #522510;
line-height: 54rpx;
}
}
.memorial-desc {
border-radius: 8rpx;
font-weight: 400;
font-size: 32rpx;
color: #4C382E;
line-height: 44rpx;
letter-spacing: 2px;
text-align: left;
}
}
//
.records-section {
padding: 46rpx 43rpx;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
background: #FFFBF5;
border-radius: 20rpx 20rpx 20rpx 20rpx;
border: 1rpx solid #C7A26D;
width: 680rpx;
}
//
.table-header {
display: flex;
border-radius: 8rpx;
margin-bottom: 30rpx;
//border:1px red solid;
align-items: center;
.header-cell {
flex: 1;
//border:1px red solid;
font-weight: 400;
font-size: 28rpx;
color: #695347;
line-height: 38rpx;
text-align: left;
}
}
//
.records-list {
//border:1px red solid;
.record-item {
//border:1px red solid;
display: flex;
margin-bottom: 30rpx;
.record-cell {
flex: 1;
font-weight: 400;
font-size: 24rpx;
color: #522510;
line-height: 32rpx;
text-align: left;
//border:1px red solid;
.worshiper-name {
color: #522510;
font-weight: 500;
}
.date-text {
color: #666;
}
}
}
}
//
.empty-state {
text-align: center;
padding: 80rpx 0;
.empty-text {
font-size: 28rpx;
color: #999;
}
}
//
.load-more {
text-align: center;
padding: 40rpx 0;
.load-more-text {
font-size: 26rpx;
color: #999;
}
}
//
.load-complete {
text-align: center;
padding: 40rpx 0;
.load-complete-text {
font-size: 26rpx;
color: #666;
}
}
</style>

View File

@ -108,6 +108,13 @@
/>
</view>
<!-- 供奉弹窗 -->
<OfferingModal
:visible="showOfferingModal"
@close="closeOfferingModal"
@confirm="handleOfferingConfirm"
/>
</view>
</template>
@ -117,9 +124,9 @@ import { getDeceasedList, getMemorialDetail } from '@/api/memorial/index.js'
import SearchBox from "../../components/search-box/search-box.vue"
import StatusDisplay from "../../components/status-display/status-display.vue"
import EnshrinedList from "./compositons/enshrinedList.vue"
import FloorSelector from "./compositons/floorSelector.vue"
import StatusBar from "./compositons/statusBar.vue"
import BottomButton from "../../components/bottom-button/bottom-button.vue";
import BottomButton from "../../components/bottom-button/bottom-button.vue"
import OfferingModal from "./compositons/offeringModal.vue"
export default {
components: {
@ -127,24 +134,14 @@ export default {
SearchBox,
StatusDisplay,
EnshrinedList,
FloorSelector,
StatusBar
StatusBar,
OfferingModal
},
data() {
return {
CommonEnum,
searchName: '',
loading: false,
//
defaultFloorId: '',
defaultAreaId: '',
defaultUnitId: '',
//
currentSelection: {
floor: null,
area: null,
unit: null
},
// ID
selectedUnitId: '',
//
@ -157,7 +154,9 @@ export default {
// 殿
memorialDetail: null,
//
scrollTop: 0
scrollTop: 0,
//
showOfferingModal: false
}
},
onLoad(options) {
@ -274,8 +273,37 @@ export default {
//
submitPrayer() {
console.log('提交供奉')
//
console.log('显示供奉弹窗')
this.showOfferingModal = true
},
//
closeOfferingModal() {
this.showOfferingModal = false
},
//
handleOfferingConfirm(offeringData) {
console.log('确认供奉:', offeringData)
// API
uni.showLoading({
title: '正在提交供奉...'
})
// API
setTimeout(() => {
uni.hideLoading()
uni.showToast({
title: '供奉成功!',
icon: 'success'
})
//
this.closeOfferingModal()
//
}, 2000)
},
//
@ -479,9 +507,6 @@ export default {
}
//
:deep(.status-bar) {
width: 100%;