Files
intc-vue-h5/src/pages/health/activity/list.vue
2026-06-28 15:43:23 +08:00

769 lines
21 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="container">
<u-sticky offsetTop="0rpx" customNavHeight="0rpx">
<view class="search-view">
<u--input
v-model="queryParams.name"
border="surround"
placeholder="请输入活动名称"
placeholderStyle="color: #909399"
color="#333333"
customStyle="background: rgba(102, 126, 234, 0.08); border: 2rpx solid rgba(102, 126, 234, 0.3); border-radius: 24rpx; height: 66rpx"
class="search-input"
@blur="handleSearch"
suffixIcon="search"
suffixIconStyle="color: #667eea">
</u--input>
<view class="add-btn" @click="handleAdd()">
<uni-icons type="plusempty" size="18" color="#667eea"></uni-icons>
<text>新增</text>
</view>
</view>
</u-sticky>
<u-list @scrolltolower="loadmore" :spaceHeight="116" lowerThreshold="100">
<u-list-item v-for="(item, index) in listData" :key="index">
<view class="list-item">
<view class="item-header">
<view class="card-name-section">
<view class="card-icon">
<uni-icons type="flag-filled" size="20" color="#ffffff"></uni-icons>
</view>
<view class="place-info">
<text class="place-text">{{ item.place }}</text>
</view>
</view>
<view class="duration-section">
<text class="duration-value">{{ item.exerciseTimeStr || '--' }}</text>
</view>
<view class="name-section">
<text class="name-value">{{ item.name }}</text>
</view>
</view>
<view class="card-body">
<view class="info-row">
<view class="info-item">
<text class="info-label">活动类型</text>
<text class="info-value">{{ dictStr(item.type,typeList) || '--' }}</text>
</view>
<view class="info-item">
<text class="info-label">总费用</text>
<text class="info-value cost-value">{{ item.totalCost }} </text>
</view>
<view class="info-item">
<text class="info-label">成员</text>
<text class="info-value">{{ item.partner || '--' }}</text>
</view>
<view class="info-item">
<text class="info-label">活动量</text>
<text class="info-value">{{ dictStr(item.activityVolume,activityVolumeList) || '--' }}</text>
</view>
<view class="info-item" style="flex: 0 0 100%;">
</view>
<view class="info-item">
<text class="info-label">开始时间</text>
<text class="info-value">{{ item.startTime || '--' }}</text>
</view>
<view class="info-item">
<text class="info-label">结束时间</text>
<text class="info-value">{{ item.endTime || '--' }}</text>
</view>
<view class="info-item info-item-full" v-if="item.costDetail">
<text class="info-label">费用明细</text>
<text class="info-value">{{ item.costDetail }}</text>
</view>
<view class="info-item info-item-full" v-if="item.foods">
<text class="info-label">饮食</text>
<text class="info-value">{{ item.foods }}</text>
</view>
<view class="info-item info-item-full" v-if="item.harvest">
<text class="info-label">收获</text>
<text class="info-value">{{ item.harvest }}</text>
</view>
<view class="info-item info-item-full" v-if="item.remark">
<text class="info-label">备注</text>
<text class="info-value">{{ item.remark }}</text>
</view>
<view class="info-item info-item-full" v-if="parseAttachment(item.attachment).length">
<text class="info-label">附件</text>
<view class="attachment-list">
<image
v-for="(img, imgIdx) in parseAttachment(item.attachment)"
:key="imgIdx"
:src="img"
class="attachment-thumb"
mode="aspectFill"
@click.stop="previewAttachment(parseAttachment(item.attachment), imgIdx)"
></image>
</view>
</view>
</view>
</view>
<view class="operate" @click.stop>
<view class="btn-summary" @click="handleCostSummary(item)">
<uni-icons type="list" size="16" color="#13c2c2"></uni-icons>
<text>费用汇总</text>
</view>
<view class="btn-edit" @click="handleEdit(item)">
<uni-icons type="compose" size="16" color="#667eea"></uni-icons>
<text>修改</text>
</view>
<view class="btn-copy" @click="handleCopy(item)">
<uni-icons type="paperplane" size="16" color="#fa8c16"></uni-icons>
<text>复制</text>
</view>
<view class="btn-delete" @click="handleDelete(item)">
<uni-icons type="trash" size="16" color="#f5576c"></uni-icons>
<text>删除</text>
</view>
</view>
</view>
</u-list-item>
<view>
</view>
<u-loadmore :status="status" loadingIcon="semicircle" height="88" fontSize="32rpx" @loadmore="loadmore" />
</u-list>
<view v-if="summaryVisible" class="summary-mask" @click="closeSummary">
<view class="summary-panel" @click.stop>
<view class="summary-header">
<view class="summary-title">
<text class="summary-name">费用汇总</text>
<text class="summary-subtitle">{{ currentActivity.name || '--' }}</text>
</view>
<view class="summary-close" @click="closeSummary">
<uni-icons type="closeempty" size="24" color="#909399"></uni-icons>
</view>
</view>
<view class="summary-meta">
<view class="summary-meta-item">
<text class="meta-label">已选</text>
<text class="meta-value">{{ selectedExpenses.length }} </text>
</view>
<view class="summary-meta-item">
<text class="meta-label">合计金额</text>
<text class="meta-value amount">{{ selectedTotalAmount }} </text>
</view>
</view>
<scroll-view scroll-y class="summary-scroll">
<view v-if="summaryLoading" class="summary-empty">加载中...</view>
<view v-else-if="expenseList.length === 0" class="summary-empty">暂无可汇总支出</view>
<block v-else>
<view
v-for="expense in expenseList"
:key="expense.id"
class="expense-item"
:class="{ selected: isExpenseSelected(expense) }"
@click="toggleExpense(expense)"
>
<view class="expense-checkbox" :class="{ active: isExpenseSelected(expense) }"></view>
<view class="expense-content">
<view class="expense-main">
<text class="expense-category">{{ expense.childCategoryName || dictStr(expense.dealCategory, dealCategoryList) || '日常支出' }}</text>
<text class="expense-amount">{{ expense.amount || 0 }} </text>
</view>
<view class="expense-sub">
<text>{{ expense.accountName || '--' }}</text>
<text>{{ expense.createTime || '--' }}</text>
</view>
<text v-if="expense.remark" class="expense-remark">{{ expense.remark }}</text>
</view>
</view>
</block>
</scroll-view>
<view class="summary-footer">
<view class="summary-cancel" @click="closeSummary">取消</view>
<view class="summary-confirm" :class="{ disabled: selectedExpenses.length === 0 }" @click="confirmSummary">确认汇总</view>
</view>
</view>
</view>
</view>
<!--返回首页按钮 -->
<suspend></suspend>
</template>
<script setup>
import { listActivity, getActivity, delActivity, updateActivity } from '@/api/health/activity'
import { listAccountDealRecord } from '@/api/invest/accountDealRecord'
import { getDicts } from '@/api/system/dict/data.js'
import { timeHandler } from '@/utils/common.ts'
import {onLoad,onShow} from "@dcloudio/uni-app";
import dayjs from 'dayjs'
import config from '@/config'
// 计算属性与监听属性是在vue中而非uniap中 需要注意!!!
import {reactive ,toRefs,ref,computed }from "vue";
const pageNum = ref(1)
const listData = ref([])
const isShow = ref(false)
const status = ref('loadmore')
const typeList = ref([])
const activityVolumeList = ref([])
const dealCategoryList = ref([])
const summaryVisible = ref(false)
const summaryLoading = ref(false)
const expenseList = ref([])
const selectedExpenses = ref([])
const currentActivityId = ref(null)
const currentActivity = ref({})
const flag= ref(true)
const data = reactive({
filterPanel: false,
queryParams: {
name: null,
type: null,
nickName: null
}
})
const { filterPanel, queryParams} = toRefs(data)
const windowHeight = computed(() => {
uni.getSystemInfoSync().windowHeight - 50
})
const selectedTotalAmount = computed(() => {
return selectedExpenses.value
.reduce((sum, item) => {
return sum + (parseFloat(item.amount) || 0)
}, 0)
.toFixed(2)
})
onLoad(() => {
getList()
});
onShow(() => {
if (isShow.value) {
listData.value=[]
getList()
isShow.value = false
}
});
function handleSearch() {
pageNum.value = 1
listData.value = []
getList()
}
function dictStr(val, arr) {
let str = ''
arr.map(item => {
if (item.dictValue === val) {
str = item.dictLabel
}
})
return str
}
function loadmore() {
pageNum.value += 1
if (status.value == 'loadmore') {
getList()
}
}
function getList() {
// 类型
getDicts('activity_type').then(res => {
typeList.value = res.data
})
// 类型
getDicts('activity_exercise').then(res => {
activityVolumeList.value = res.data
})
getDicts('deal_category').then(res => {
dealCategoryList.value = res.data
})
status.value = 'loading'
listActivity({ pageSize: 10, pageNum: pageNum.value, ...queryParams.value }).then(res => {
listData.value = listData.value.concat(res.rows)
if (listData.value.length < res.total) {
status.value = 'loadmore'
} else {
status.value = 'nomore'
}
}).catch(() => {
status.value = 'nomore'
})
}
function handleEdit(item) {
uni.navigateTo({ url: `/pages/health/activity/addEdit?id=${item.id}` })
isShow.value = true
}
function handleCopy(item) {
uni.navigateTo({ url: `/pages/health/activity/addEdit?flag=copy&id=${item.id}` })
isShow.value = true
}
function handleAdd() {
uni.navigateTo({ url: `/pages/health/activity/addEdit` })
isShow.value = true
}
function handleCostSummary(item) {
currentActivityId.value = item.id
currentActivity.value = item
selectedExpenses.value = []
expenseList.value = []
summaryVisible.value = true
summaryLoading.value = true
const queryExpenseParams = {
pageNum: 1,
pageSize: 1000,
dealCategory: '1',
startTime: item.startTime ? dayjs(item.startTime).format('YYYY-MM-DD') : '',
endTime: item.endTime ? dayjs(item.endTime).format('YYYY-MM-DD') : dayjs().format('YYYY-MM-DD')
}
listAccountDealRecord(queryExpenseParams).then(res => {
expenseList.value = res.rows || []
summaryLoading.value = false
}).catch(() => {
summaryLoading.value = false
})
}
function closeSummary() {
summaryVisible.value = false
selectedExpenses.value = []
expenseList.value = []
}
function isExpenseSelected(item) {
return selectedExpenses.value.some(expense => expense.id === item.id)
}
function toggleExpense(item) {
if (isExpenseSelected(item)) {
selectedExpenses.value = selectedExpenses.value.filter(expense => expense.id !== item.id)
} else {
selectedExpenses.value.push(item)
}
}
function buildCostDetail(activity) {
const startDate = activity.startTime ? dayjs(activity.startTime).format('YYYY-MM-DD') : ''
const endDate = activity.endTime ? dayjs(activity.endTime).format('YYYY-MM-DD') : ''
const isMultiDay = startDate !== endDate
if (isMultiDay) {
const groupedByDate = {}
selectedExpenses.value.forEach(item => {
const date = dayjs(item.createTime).format('MM-DD')
if (!groupedByDate[date]) {
groupedByDate[date] = []
}
const category = item.childCategoryName || ''
groupedByDate[date].push(`${category} ${item.amount}${item.remark ? '(' + item.remark + ')' : ''}`)
})
return Object.keys(groupedByDate)
.sort()
.map(date => `${date} ${groupedByDate[date].join('、')}`)
.join('')
}
return selectedExpenses.value
.map(item => {
const category = item.childCategoryName || ''
return `${category} ${item.amount}${item.remark ? '(' + item.remark + ')' : ''}`
})
.join('')
}
function confirmSummary() {
if (selectedExpenses.value.length === 0) {
uni.showToast({
title: '请选择费用记录',
icon: 'none'
})
return
}
getActivity(currentActivityId.value).then(res => {
const activity = res.data
const totalAmount = parseFloat(selectedTotalAmount.value)
const currentTotal = parseFloat(activity.totalCost) || 0
const detailsWithDate = buildCostDetail(activity)
const updateData = {
...activity,
totalCost: (currentTotal + totalAmount).toFixed(2),
costDetail: activity.costDetail ? `${activity.costDetail}${detailsWithDate}` : detailsWithDate
}
updateActivity(updateData).then(() => {
uni.showToast({
title: '汇总成功',
icon: 'success'
})
closeSummary()
pageNum.value = 1
listData.value = []
getList()
})
})
}
function handleDelete(item) {
uni.showModal({
title: '确认删除',
content: '确定要删除这条记录吗?',
confirmText: '删除',
cancelText: '取消',
confirmColor: '#f5576c',
cancelColor: '#909399',
success: function (res) {
if (res.confirm) {
delActivity(item.id).then(() => {
uni.showToast({
title: '删除成功',
icon: 'success'
})
pageNum.value = 1
listData.value = []
getList()
})
}
}
});
}
function parseAttachment(attachment) {
if (!attachment) return []
// 附件是后端静态资源直链,需要用域名拼接,不能带网关前缀 prod-api
const origin = config.baseUrl.replace(/^(https?:\/\/[^/]+).*$/, '$1')
return attachment.split(',').filter(url => url).map(url => {
if (url.startsWith('http')) return url
return origin + url
})
}
function previewAttachment(urls, index) {
uni.previewImage({
urls: urls,
current: urls[index]
})
}
</script>
<style lang="scss" scoped>
@import '@/styles/health-common.scss';
// 筛选面板位置补充(单行模式)
.search-view .filter-panel {
top: 96rpx;
}
// 活动特有 - 卡片头部布局
.item-header {
gap: 12rpx;
/* 允许 header 内容换行,避免长地点/长名字被省略号截断 */
align-items: flex-start;
.place-info {
flex: 1;
min-width: 0;
display: flex;
align-items: flex-start;
.place-text {
color: #ffffff;
font-size: 30rpx;
font-weight: 500;
line-height: 1.3;
/* 长地点名称最多2行后省略 */
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
word-break: break-all;
}
}
.duration-section {
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
.duration-value {
color: #fa8c16;
font-size: 30rpx;
font-weight: 600;
line-height: 1.3;
white-space: nowrap;
}
}
.name-section {
display: flex;
align-items: flex-start;
flex: 0 1 auto;
min-width: 0;
max-width: 40%;
justify-content: flex-end;
.name-value {
color: #ffffff;
font-size: 30rpx;
font-weight: 700;
line-height: 1.3;
/* 长活动名最多2行后省略 */
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
word-break: break-all;
text-align: right;
letter-spacing: 0.5rpx;
}
}
}
// 活动特有 - 费用色
.info-value.cost-value {
color: #fa8c16;
font-weight: 600;
}
// 活动特有 - 汇总按钮
.operate .btn-summary {
background: rgba(19, 194, 194, 0.1);
color: #13c2c2;
border: 1rpx solid rgba(19, 194, 194, 0.3);
}
.attachment-list {
display: flex;
flex-wrap: wrap;
gap: 12rpx;
margin-top: 4rpx;
width: 100%;
.attachment-thumb {
width: 120rpx;
height: 120rpx;
border-radius: 8rpx;
background: #f5f7fa;
box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.08);
transition: transform 0.2s ease;
&:active {
transform: scale(0.95);
}
}
}
// ==================== 费用汇总面板 ====================
.summary-mask {
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
z-index: 999;
background: rgba(0, 0, 0, 0.45);
display: flex;
align-items: flex-end;
}
.summary-panel {
width: 100%;
max-height: 82vh;
background: #ffffff;
border-radius: 24rpx 24rpx 0 0;
overflow: hidden;
display: flex;
flex-direction: column;
}
.summary-header {
padding: 24rpx 28rpx;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1rpx solid #ebeef5;
}
.summary-title {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 6rpx;
}
.summary-name {
color: #2c3e50;
font-size: 32rpx;
font-weight: 700;
}
.summary-subtitle {
color: #909399;
font-size: 24rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.summary-close {
width: 56rpx;
height: 56rpx;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.summary-meta {
padding: 18rpx 28rpx;
background: #fafbfc;
display: flex;
gap: 20rpx;
}
.summary-meta-item {
flex: 1;
display: flex;
flex-direction: column;
gap: 6rpx;
min-width: 0;
}
.meta-label {
color: #909399;
font-size: 24rpx;
}
.meta-value {
color: #2c3e50;
font-size: 30rpx;
font-weight: 700;
&.amount {
color: #f5576c;
}
}
.summary-scroll {
height: 58vh;
padding: 16rpx 24rpx;
box-sizing: border-box;
}
.summary-empty {
color: #909399;
font-size: 28rpx;
text-align: center;
padding: 120rpx 0;
}
.expense-item {
display: flex;
gap: 16rpx;
padding: 18rpx;
margin-bottom: 16rpx;
border: 2rpx solid #ebeef5;
border-radius: 14rpx;
background: #ffffff;
&.selected {
border-color: rgba(102, 126, 234, 0.55);
background: rgba(102, 126, 234, 0.06);
}
}
.expense-checkbox {
width: 34rpx;
height: 34rpx;
margin-top: 4rpx;
border-radius: 50%;
border: 3rpx solid #dcdfe6;
flex-shrink: 0;
&.active {
border-color: #667eea;
background: #667eea;
box-shadow: inset 0 0 0 7rpx #ffffff;
}
}
.expense-content {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 10rpx;
}
.expense-main {
display: flex;
justify-content: space-between;
gap: 16rpx;
align-items: center;
}
.expense-category {
color: #2c3e50;
font-size: 28rpx;
font-weight: 600;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.expense-amount {
color: #f5576c;
font-size: 28rpx;
font-weight: 700;
flex-shrink: 0;
}
.expense-sub {
display: flex;
justify-content: space-between;
gap: 16rpx;
color: #909399;
font-size: 24rpx;
}
.expense-remark {
color: #606266;
font-size: 24rpx;
line-height: 1.5;
word-break: break-all;
}
.summary-footer {
padding: 18rpx 24rpx 28rpx;
display: flex;
gap: 16rpx;
border-top: 1rpx solid #ebeef5;
}
.summary-cancel,
.summary-confirm {
flex: 1;
height: 78rpx;
border-radius: 14rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 30rpx;
font-weight: 600;
}
.summary-cancel {
color: #606266;
background: #f5f7fa;
border: 2rpx solid #dcdfe6;
}
.summary-confirm {
color: #ffffff;
background: #667eea;
&.disabled {
opacity: 0.5;
}
}
</style>