Compare commits

20 Commits

Author SHA1 Message Date
tianyongbao
b7e6c05152 feat: 新增意见反馈功能。 2026-07-13 09:27:42 +08:00
tianyongbao
9132824c61 feat: 健康档案大屏功能新增。 2026-06-20 22:10:42 +08:00
tianyongbao
90e923f490 feat: 综合分析大屏功能新增。 2026-06-18 22:49:26 +08:00
tianyongbao
0b0dd5c8e8 feat: 消费大屏功能新增。 2026-06-18 13:54:15 +08:00
tianyongbao
802d994cfe feat(auth): 新增密码复杂度校验,注册/修改密码/新建用户/重置密码统一为8-20位且须含大小写字母/数字/特殊字符 2026-06-17 05:48:36 +08:00
tianyongbao
664f49d9e0 fix: 功能修复,账号相关,新增交易明细记录。 2026-05-19 09:30:23 +08:00
tianyongbao
9fd1ebc65e fix: 慢性病管理功能优化完善。 2026-03-21 22:31:14 +08:00
tianyongbao
639c2c913d fix: 自测功能修复。 2026-03-21 10:52:56 +08:00
bot5
ffe1ed9e88 refactor(health): 前端适配后端API重构
- 新增 medicationTask.js 替代 medicationRecord.js
- 新增 medicationTask/index.vue 服药任务页面
- 更新 medicationAdherence.js 参数名 personId -> memberId
- 状态值适配:0-待服药 1-已服药 2-漏服 3-跳过
2026-03-19 21:36:51 +08:00
bot5
f090aeb9b9 feat(health): 新增依从性统计前端页面
- 新增 medicationAdherence.js:统计API接口
- 新增 medicationStatistic/index.vue:统计页面
  - 概览卡片:总任务、已服药、漏服、服药率、准时率
  - 趋势图:服药率和准时率折线图
  - 时段分布:饼图展示
  - 日历视图:月度服药日历
2026-03-19 08:35:08 +08:00
bot5
d8caa6c7e6 feat(health): 新增用药计划模块前端页面
- 新增用药计划和用药记录API接口
- 新增用药计划管理页面(列表/新增/编辑/暂停/恢复/结束)
- 新增用药记录管理页面(列表/服药/跳过)
2026-03-18 14:10:32 +08:00
bot5
febbd54134 feat(health): 新增慢性疾病档案前端页面
- 新增慢性疾病档案API接口
- 新增慢性疾病档案管理页面
- 支持列表查询、新增、编辑、查看、删除
- 支持按成员、疾病名称、状态筛选
- 支持关联成员和疾病知识库选择
- 支持状态切换开关
2026-03-17 23:32:47 +08:00
tianyongbao
cc60180990 fix: 功能优化完善。 2026-03-06 22:28:32 +08:00
tianyongbao
a88b724f2f fix: 待还款明细新增字段并优化。 2026-03-06 22:16:22 +08:00
tianyongbao
7baee5faff fix: 大屏,投资收益倒序。 2026-03-04 22:12:06 +08:00
tianyongbao
6ebe0b8b00 fix: 活动管理,费用明细逻辑修改。 2026-02-28 01:02:02 +08:00
tianyongbao
34a2b67695 fix: 新增复制功能。 2026-02-23 23:06:12 +08:00
tianyongbao
fa5872b7df fix: 增加排序 2026-02-23 18:52:43 +08:00
tianyongbao
d0b847ab68 fix: 功能优化,活动记录费用优化。 2026-02-23 17:01:45 +08:00
tianyongbao
cc89340394 fix: 修改列表宽度。 2026-02-22 22:51:01 +08:00
43 changed files with 13160 additions and 107 deletions

View File

@@ -0,0 +1,52 @@
import request from '@/utils/request'
// 查询慢性疾病档案列表
export function listChronicDisease(query) {
return request({
url: '/health/chronicDisease/list',
method: 'get',
params: query
})
}
// 根据成员ID查询慢性疾病档案列表
export function listChronicDiseaseByMember(personId) {
return request({
url: '/health/chronicDisease/listByPerson/' + personId,
method: 'get'
})
}
// 查询慢性疾病档案详细
export function getChronicDisease(id) {
return request({
url: '/health/chronicDisease/' + id,
method: 'get'
})
}
// 新增慢性疾病档案
export function addChronicDisease(data) {
return request({
url: '/health/chronicDisease',
method: 'post',
data
})
}
// 修改慢性疾病档案
export function updateChronicDisease(data) {
return request({
url: '/health/chronicDisease',
method: 'put',
data
})
}
// 删除慢性疾病档案
export function delChronicDisease(id) {
return request({
url: '/health/chronicDisease/' + id,
method: 'delete'
})
}

View File

@@ -0,0 +1,88 @@
import request from '@/utils/request'
// 查询慢病指标记录列表
export function listIndicator(query) {
return request({
url: '/health/chronicDiseaseManage/indicator/list',
method: 'get',
params: query
})
}
// 查询慢病指标记录详细
export function getIndicator(id) {
return request({
url: '/health/chronicDiseaseManage/indicator/' + id,
method: 'get'
})
}
// 新增慢病指标记录
export function addIndicator(data) {
return request({
url: '/health/chronicDiseaseManage/indicator',
method: 'post',
data
})
}
// 修改慢病指标记录
export function updateIndicator(data) {
return request({
url: '/health/chronicDiseaseManage/indicator',
method: 'put',
data
})
}
// 删除慢病指标记录
export function delIndicator(id) {
return request({
url: '/health/chronicDiseaseManage/indicator/' + id,
method: 'delete'
})
}
// 查询复诊随访记录列表
export function listFollowUp(query) {
return request({
url: '/health/chronicDiseaseManage/followUp/list',
method: 'get',
params: query
})
}
// 新增复诊随访记录
export function addFollowUp(data) {
return request({
url: '/health/chronicDiseaseManage/followUp',
method: 'post',
data
})
}
// 修改复诊随访记录
export function updateFollowUp(data) {
return request({
url: '/health/chronicDiseaseManage/followUp',
method: 'put',
data
})
}
// 删除复诊随访记录
export function delFollowUp(id) {
return request({
url: '/health/chronicDiseaseManage/followUp/' + id,
method: 'delete'
})
}
// 查询状态变更历史
export function listStatusHistory(query) {
return request({
url: '/health/chronicDiseaseManage/statusHistory/list',
method: 'get',
params: query
})
}

View File

@@ -0,0 +1,46 @@
import request from '@/utils/request'
// 获取总体依从性统计
export function getOverallAdherence(query) {
return request({
url: '/health/adherence/overall',
method: 'get',
params: query
})
}
// 获取每日依从性统计列表
export function getDailyAdherence(query) {
return request({
url: '/health/adherence/daily',
method: 'get',
params: query
})
}
// 获取日历视图数据
export function getCalendarData(memberId, yearMonth) {
return request({
url: '/health/adherence/calendar',
method: 'get',
params: { memberId, yearMonth }
})
}
// 获取各时段服药统计
export function getTimeSlotAdherence(query) {
return request({
url: '/health/adherence/timeSlot',
method: 'get',
params: query
})
}
// 获取仪表盘概览数据
export function getDashboard(memberId) {
return request({
url: '/health/adherence/dashboard',
method: 'get',
params: { memberId }
})
}

View File

@@ -0,0 +1,68 @@
import request from '@/utils/request'
// 查询用药计划列表
export function listMedicationPlan(query) {
return request({
url: '/health/medicationPlan/list',
method: 'get',
params: query
})
}
// 查询用药计划详细
export function getMedicationPlan(id) {
return request({
url: '/health/medicationPlan/' + id,
method: 'get'
})
}
// 新增用药计划
export function addMedicationPlan(data) {
return request({
url: '/health/medicationPlan',
method: 'post',
data
})
}
// 修改用药计划
export function updateMedicationPlan(data) {
return request({
url: '/health/medicationPlan',
method: 'put',
data
})
}
// 删除用药计划
export function delMedicationPlan(id) {
return request({
url: '/health/medicationPlan/' + id,
method: 'delete'
})
}
// 暂停用药计划
export function pauseMedicationPlan(id) {
return request({
url: '/health/medicationPlan/pause/' + id,
method: 'put'
})
}
// 恢复用药计划
export function resumeMedicationPlan(id) {
return request({
url: '/health/medicationPlan/resume/' + id,
method: 'put'
})
}
// 结束用药计划
export function endMedicationPlan(id) {
return request({
url: '/health/medicationPlan/end/' + id,
method: 'put'
})
}

View File

@@ -0,0 +1,53 @@
import request from '@/utils/request'
// 查询服药任务列表
export function listMedicationTask(query) {
return request({
url: '/health/medicationTask/list',
method: 'get',
params: query
})
}
// 查询服药任务详细
export function getMedicationTask(id) {
return request({
url: '/health/medicationTask/' + id,
method: 'get'
})
}
// 删除服药任务
export function delMedicationTask(id) {
return request({
url: '/health/medicationTask/' + id,
method: 'delete'
})
}
// 确认服药
export function confirmMedication(id, confirmType, confirmTime) {
return request({
url: '/health/medicationTask/confirm/' + id,
method: 'put',
params: { confirmType, confirmTime }
})
}
// 跳过服药
export function skipMedication(id, notes) {
return request({
url: '/health/medicationTask/skip/' + id,
method: 'put',
params: { notes }
})
}
// 标记漏服
export function markMissed(id, notes) {
return request({
url: '/health/medicationTask/missed/' + id,
method: 'put',
params: { notes }
})
}

View File

@@ -24,6 +24,22 @@ export function getHealthAnalysis(query) {
})
}
export function getActivityAnalysis(query) {
return request({
url: '/health/analysis/activityAnalysis',
method: 'get',
params: query
})
}
export function getHealthScreen(query) {
return request({
url: '/health/analysis/healthScreen',
method: 'get',
params: query
})
}
export function getRecordAnalysis(query) {
return request({
url: '/health/analysis/recordAnalysis',
@@ -32,6 +48,14 @@ export function getRecordAnalysis(query) {
})
}
export function getRecordScreen(query) {
return request({
url: '/health/analysis/recordScreen',
method: 'get',
params: query
})
}
export function getDoctorAnalysis(query) {
return request({
url: '/health/analysis/doctorAnalysis',

View File

@@ -121,3 +121,30 @@ export function getBankCardStatistics() {
method: 'get'
})
}
// 查询交易地点消费地图概览
export function getAccountDealLocationOverview(query) {
return request({
url: '/invest/analysis/accountDealLocationOverview',
method: 'get',
params: query
})
}
// 查询交易地点消费地图聚合点位
export function getAccountDealLocationPoints(query) {
return request({
url: '/invest/analysis/accountDealLocationPoints',
method: 'get',
params: query
})
}
// 查询交易地点消费明细
export function getAccountDealLocationRecords(query) {
return request({
url: '/invest/analysis/accountDealLocationRecords',
method: 'get',
params: query
})
}

View File

@@ -0,0 +1,53 @@
import request from '@/utils/request'
// 查询意见反馈列表
export function listFeedback(query) {
return request({
url: '/system/feedback/list',
method: 'get',
params: query
})
}
// 查询意见反馈详细
export function getFeedback(id) {
return request({
url: '/system/feedback/' + id,
method: 'get'
})
}
// 新增意见反馈
export function addFeedback(data) {
return request({
url: '/system/feedback',
method: 'post',
data
})
}
// 修改意见反馈
export function updateFeedback(data) {
return request({
url: '/system/feedback',
method: 'put',
data
})
}
// 回复意见反馈
export function replyFeedback(data) {
return request({
url: '/system/feedback/reply',
method: 'put',
data
})
}
// 删除意见反馈
export function delFeedback(id) {
return request({
url: '/system/feedback/' + id,
method: 'delete'
})
}

View File

@@ -158,11 +158,14 @@ function uploadedSuccessfully() {
// 获取文件名称
function getFileName(name) {
if (name.lastIndexOf('/') > -1) {
return name.slice(name.lastIndexOf('/') + 1)
} else {
const fileName = String(name || '').split('?')[0]
if (!fileName) {
return ''
}
if (fileName.lastIndexOf('/') > -1) {
return decodeURIComponent(fileName.slice(fileName.lastIndexOf('/') + 1))
}
return decodeURIComponent(fileName)
}
// 对象转成指定字符串分隔

View File

@@ -65,28 +65,19 @@ export const constantRoutes = [
{
path: '',
component: Layout,
redirect: '/accountCalendar'
// children: [
// {
// path: '/index',
// component: () => import('@/views/index'),
// name: 'Index',
// meta: { title: '首页', icon: 'dashboard', affix: true }
// }
// ]
redirect: '/index'
},
{
path: '/index',
component: Layout,
redirect: '/statisticanalysis/accountAnalysis'
// children: [
// {
// path: '/index',
// component: () => import('@/views/index'),
// name: 'Index',
// meta: { title: '首页', icon: 'dashboard', affix: true }
// }
// ]
children: [
{
path: '',
component: () => import('@/views/index'),
name: 'Index',
meta: { title: '首页', icon: 'dashboard', affix: true }
}
]
},
{
path: '/user',

View File

@@ -1,9 +1,9 @@
/**
* 判断url是否是http或https
* 判断url是否是http或https
* @param {string} path
* @returns {Boolean}
*/
export function isHttp(url) {
export function isHttp(url) {
return url.indexOf('http://') !== -1 || url.indexOf('https://') !== -1
}
@@ -12,7 +12,7 @@
* @param {string} path
* @returns {Boolean}
*/
export function isExternal(path) {
export function isExternal(path) {
return /^(https?:|mailto:|tel:)/.test(path)
}
@@ -30,7 +30,8 @@ export function validUsername(str) {
* @returns {Boolean}
*/
export function validURL(url) {
const reg = /^(https?|ftp):\/\/([a-zA-Z0-9.-]+(:[a-zA-Z0-9.&%$-]+)*@)*((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.(com|edu|gov|int|mil|net|org|biz|arpa|info|name|pro|aero|coop|museum|[a-zA-Z]{2}))(:[0-9]+)*(\/($|[a-zA-Z0-9.,?'\\+&%$#=~_-]+))*$/
const reg =
/^(https?|ftp):\/\/([a-zA-Z0-9.-]+(:[a-zA-Z0-9.&%$-]+)*@)*((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.(com|edu|gov|int|mil|net|org|biz|arpa|info|name|pro|aero|coop|museum|[a-zA-Z]{2}))(:[0-9]+)*(\/($|[a-zA-Z0-9.,?'\\+&%$#=~_-]+))*$/
return reg.test(url)
}
@@ -66,7 +67,8 @@ export function validAlphabets(str) {
* @returns {Boolean}
*/
export function validEmail(email) {
const reg = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
const reg =
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
return reg.test(email)
}
@@ -91,3 +93,65 @@ export function isArray(arg) {
}
return Array.isArray(arg)
}
// ============ 密码复杂度校验 ============
// 规则(与 H5 端保持一致):
// 1. 长度 8-20 个字符
// 2. 不能包含空格
// 3. 必须同时包含:大写字母、小写字母、数字、特殊字符 四类
export const PASSWORD_MIN_LENGTH = 8
export const PASSWORD_MAX_LENGTH = 20
const REG_UPPER = /[A-Z]/
const REG_LOWER = /[a-z]/
const REG_NUMBER = /[0-9]/
// 特殊字符:非字母、非数字、非空白(即标点符号等)
const REG_SPECIAL = /[^A-Za-z0-9\s]/
/**
* 校验密码复杂度,返回精确的失败原因
* @param {string} password
* @returns {{ valid: boolean, message: string }}
*/
export function validatePassword(password) {
if (password === undefined || password === null || password === '') {
return { valid: false, message: '密码不能为空' }
}
const value = String(password)
if (value.length < PASSWORD_MIN_LENGTH || value.length > PASSWORD_MAX_LENGTH) {
return { valid: false, message: `密码长度在 ${PASSWORD_MIN_LENGTH}${PASSWORD_MAX_LENGTH} 个字符` }
}
if (/\s/.test(value)) {
return { valid: false, message: '密码不能包含空格' }
}
if (!REG_UPPER.test(value)) {
return { valid: false, message: '密码必须包含大写字母' }
}
if (!REG_LOWER.test(value)) {
return { valid: false, message: '密码必须包含小写字母' }
}
if (!REG_NUMBER.test(value)) {
return { valid: false, message: '密码必须包含数字' }
}
if (!REG_SPECIAL.test(value)) {
return { valid: false, message: '密码必须包含特殊字符' }
}
return { valid: true, message: '' }
}
/**
* Element Plus (async-validator) 自定义校验函数
* 用法:{ validator: passwordValidator, trigger: 'blur' }
* @param {object} rule
* @param {string} value
* @param {Function} callback
*/
export function passwordValidator(rule, value, callback) {
const { valid, message } = validatePassword(value)
if (valid) {
callback()
} else {
callback(new Error(message))
}
}

View File

@@ -12,11 +12,11 @@
<el-form-item label="发行年份" prop="issueYear">
<el-date-picker v-model="queryParams.issueYear" type="year" placeholder="请选择发行年份" value-format="YYYY" clearable />
</el-form-item>
<el-form-item label="系列" prop="series">
<!-- <el-form-item label="系列" prop="series">
<el-select v-model="queryParams.series" placeholder="请选择系列" clearable>
<el-option v-for="dict in commemorative_banknote_series" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
</el-form-item> -->
</el-form>
<div class="search-btn-con">
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
@@ -35,7 +35,7 @@
<div class="content-con" v-loading="loading">
<el-table v-loading="loading" :data="CommemorativeBanknoteList" @selection-change="handleSelectionChange" height="calc(100% - 0.62rem)">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="缩略图" align="center" prop="collectFileList" width="150">
<el-table-column label="缩略图" align="center" prop="collectFileList" width="180">
<template #default="scope">
<el-image
v-if="scope.row.collectFileList && scope.row.collectFileList.length > 0"
@@ -58,13 +58,13 @@
<dict-tag :options="commemorative_banknote_composition" :value="scope.row.composition" />
</template>
</el-table-column>
<el-table-column label="系列" align="center" prop="series">
<!-- <el-table-column label="系列" align="center" prop="series">
<template #default="scope">
<dict-tag :options="commemorative_banknote_series" :value="scope.row.series" />
</template>
</el-table-column>
</el-table-column> -->
<!-- <el-table-column label="铸币单位" align="center" prop="coinageUnit" /> -->
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<el-table-column label="操作" width="220" align="center" class-name="small-padding fixed-width">
<template v-slot="scope">
<div class="ctrl-btn d-flex">
<el-tooltip v-for="item in operateList" :key="item.id" class="item" effect="dark" :content="item.title" placement="top">
@@ -196,6 +196,7 @@ const previewImageList = ref([])
const operateList = ref([
{ id: 'view', icon: 'View', title: '查看', hasPermi: ['collect:CommemorativeBanknote:query'] },
{ id: 'edit', icon: 'Edit', title: '修改', hasPermi: ['collect:CommemorativeBanknote:edit'] },
{ id: 'copy', icon: 'DocumentCopy', title: '复制', hasPermi: ['collect:CommemorativeBanknote:add'] },
{ id: 'delete', icon: 'Delete', title: '删除', hasPermi: ['collect:CommemorativeBanknote:remove'] }
])
const data = reactive({
@@ -232,6 +233,9 @@ const handleOperate = (operate, row) => {
case 'edit':
handleUpdate(row)
break
case 'copy':
handleCopy(row)
break
case 'delete':
handleDelete(row)
break
@@ -365,6 +369,19 @@ function handleUpdate(row) {
})
}
/** 复制按钮操作 */
function handleCopy(row) {
reset()
const _id = row.id || ids.value
getCommemorativeBanknote(_id).then((response) => {
form.value = response.data
form.value.id = null // 清除ID作为新增处理
form.value.collectFileList = [] // 清除图片列表
open.value = true
title.value = '复制纪念钞'
})
}
/** 提交按钮 */
function submitForm() {
proxy.$refs.CommemorativeBanknoteRef.validate((valid) => {

View File

@@ -62,22 +62,23 @@
<dict-tag :options="commemorative_coin_type" :value="scope.row.type" />
</template>
</el-table-column> -->
<el-table-column label="全称" align="center" prop="fullName" width="320" />
<el-table-column label="简称" align="center" prop="shortName" width="320" />
<el-table-column label="面值" align="center" prop="faceValue" />
<el-table-column label="发行量" align="center" prop="mintage" />
<el-table-column label="全称" align="center" prop="fullName" width="280" />
<el-table-column label="简称" align="center" prop="shortName" width="280" />
<el-table-column label="面值" align="center" prop="faceValue" width="120" />
<el-table-column label="发行量" align="center" prop="mintage" width="120" />
<el-table-column label="材质" align="center" prop="composition">
<template #default="scope">
<dict-tag :options="commemorative_coin_composition" :value="scope.row.composition" />
</template>
</el-table-column>
<el-table-column label="发行时间" align="center" prop="issueDate" />
<el-table-column label="发行时间" align="center" prop="issueDate" width="120" />
<el-table-column label="排序" align="center" prop="setNumber" width="100" />
<el-table-column label="系列" align="center" prop="series">
<template #default="scope">
<dict-tag :options="commemorative_coin_series" :value="scope.row.series" />
</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<el-table-column label="操作" width="200" align="center" class-name="small-padding fixed-width">
<template v-slot="scope">
<div class="ctrl-btn d-flex">
<el-tooltip v-for="item in operateList" :key="item.id" class="item" effect="dark" :content="item.title" placement="top">
@@ -232,6 +233,7 @@ const title = ref('')
const operateList = ref([
{ id: 'view', icon: 'View', title: '查看', hasPermi: ['collect:commemorativeCoin:query'] },
{ id: 'edit', icon: 'Edit', title: '修改', hasPermi: ['collect:commemorativeCoin:edit'] },
{ id: 'copy', icon: 'DocumentCopy', title: '复制', hasPermi: ['collect:commemorativeCoin:add'] },
{ id: 'delete', icon: 'Delete', title: '删除', hasPermi: ['collect:commemorativeCoin:remove'] }
])
const data = reactive({
@@ -281,6 +283,9 @@ const handleOperate = (operate, row) => {
case 'edit':
handleUpdate(row)
break
case 'copy':
handleCopy(row)
break
case 'delete':
handleDelete(row)
break
@@ -413,6 +418,19 @@ function handleUpdate(row) {
})
}
/** 复制按钮操作 */
function handleCopy(row) {
reset()
const _id = row.id || ids.value
getCommemorativeCoin(_id).then((response) => {
form.value = response.data
form.value.id = null // 清除ID作为新增处理
form.value.collectFileList = [] // 清除图片列表
open.value = true
title.value = '复制流通纪念币'
})
}
/** 提交按钮 */
function submitForm() {
proxy.$refs.commemorativeCoinRef.validate((valid) => {

View File

@@ -12,16 +12,16 @@
<el-form-item label="发行年份" prop="issueYear">
<el-date-picker v-model="queryParams.issueYear" type="year" placeholder="请选择发行年份" value-format="YYYY" clearable />
</el-form-item>
<el-form-item label="系列" prop="series">
<el-select v-model="queryParams.series" placeholder="请选择系列" clearable>
<el-option v-for="dict in precious_metal_coin_series" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
<el-form-item label="项目题材" prop="blockType">
<el-select v-model="queryParams.blockType" placeholder="请选择项目题材" clearable>
<el-option v-for="dict in precious_metal_block_type" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
<el-form-item label="系列" prop="series">
<el-select v-model="queryParams.series" placeholder="请选择系列" clearable>
<el-option v-for="dict in precious_metal_coin_series" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
</el-form>
<div class="search-btn-con">
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
@@ -53,17 +53,17 @@
</template>
</el-table-column>
<el-table-column label="全称" align="center" prop="fullName" width="320" />
<el-table-column label="简称" align="center" prop="shortName" width="320" />
<el-table-column label="面额" align="center" prop="faceValue" />
<el-table-column label="发行量" align="center" prop="mintage" />
<el-table-column label="发行时间" align="center" prop="issueDate" />
<el-table-column label="简称" align="center" prop="shortName" width="200" />
<el-table-column label="面额" align="center" prop="faceValue" width="120" />
<el-table-column label="发行量" align="center" prop="mintage" width="120" />
<el-table-column label="发行时间" align="center" prop="issueDate" width="120" />
<el-table-column label="系列" align="center" prop="series">
<template #default="scope">
<dict-tag :options="precious_metal_coin_series" :value="scope.row.series" />
</template>
</el-table-column>
<el-table-column label="铸币单位" align="center" prop="coinageUnit" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<el-table-column label="操作" width="220" align="center" class-name="small-padding fixed-width">
<template v-slot="scope">
<div class="ctrl-btn d-flex">
<el-tooltip v-for="item in operateList" :key="item.id" class="item" effect="dark" :content="item.title" placement="top">
@@ -223,6 +223,7 @@ const previewImageList = ref([])
const operateList = ref([
{ id: 'view', icon: 'View', title: '查看', hasPermi: ['collect:preciousMetalCoin:query'] },
{ id: 'edit', icon: 'Edit', title: '修改', hasPermi: ['collect:preciousMetalCoin:edit'] },
{ id: 'copy', icon: 'DocumentCopy', title: '复制', hasPermi: ['collect:preciousMetalCoin:add'] },
{ id: 'delete', icon: 'Delete', title: '删除', hasPermi: ['collect:preciousMetalCoin:remove'] }
])
const data = reactive({
@@ -261,6 +262,9 @@ const handleOperate = (operate, row) => {
case 'edit':
handleUpdate(row)
break
case 'copy':
handleCopy(row)
break
case 'delete':
handleDelete(row)
break
@@ -404,6 +408,19 @@ function handleUpdate(row) {
})
}
/** 复制按钮操作 */
function handleCopy(row) {
reset()
const _id = row.id || ids.value
getPreciousMetalCoin(_id).then((response) => {
form.value = response.data
form.value.id = null // 清除ID作为新增处理
form.value.collectFileList = [] // 清除图片列表
open.value = true
title.value = '复制贵金属纪念币'
})
}
/** 提交按钮 */
function submitForm() {
proxy.$refs.preciousMetalCoinRef.validate((valid) => {

View File

@@ -50,7 +50,29 @@
<el-table-column label="活动时长" align="center" prop="exerciseTimeStr" width="150"> </el-table-column>
<el-table-column label="成员" align="center" prop="partner" />
<el-table-column label="总费用(元)" width="120" align="center" prop="totalCost" />
<el-table-column label="操作" width="140" align="center" class-name="small-padding fixed-width">
<el-table-column label="附件" align="center" prop="attachment" width="120">
<template #default="scope">
<el-popover v-if="parseAttachments(scope.row.attachment).length" placement="left" width="280" trigger="click">
<template #reference>
<el-button link type="primary">查看{{ parseAttachments(scope.row.attachment).length }}</el-button>
</template>
<div class="attachment-popover-list">
<el-link
v-for="(file, index) in parseAttachments(scope.row.attachment)"
:key="file"
type="primary"
:href="getAttachmentUrl(file)"
target="_blank"
:underline="false"
>
{{ getAttachmentName(file, index) }}
</el-link>
</div>
</el-popover>
<span v-else class="attachment-empty"></span>
</template>
</el-table-column>
<el-table-column label="操作" width="180" align="center" class-name="small-padding fixed-width">
<template v-slot="scope">
<div class="ctrl-btn d-flex">
<el-tooltip v-for="item in operateList" :key="item.id" class="item" effect="dark" :content="item.title" placement="top">
@@ -115,6 +137,22 @@
<el-form-item label="收获" style="width: 792px" prop="harvest">
<el-input v-model="form.harvest" type="textarea" placeholder="请输入收获" />
</el-form-item>
<el-form-item label="附件" style="width: 792px" prop="attachment">
<div v-if="title === '查看活动记录'" class="attachment-readonly-list attachment-form-list">
<el-link
v-for="(file, index) in parseAttachments(form.attachment)"
:key="file"
type="primary"
:href="getAttachmentUrl(file)"
target="_blank"
:underline="false"
>
{{ getAttachmentName(file, index) }}
</el-link>
<span v-if="!parseAttachments(form.attachment).length" class="attachment-empty">暂无附件</span>
</div>
<file-upload v-else v-model="form.attachment" :limit="9" :file-size="10" :file-type="attachmentFileTypes" />
</el-form-item>
<el-form-item label="备注" style="width: 792px" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
</el-form-item>
@@ -126,16 +164,54 @@
</div>
</template>
</el-dialog>
<!-- 费用汇总对话框 -->
<el-dialog title="费用汇总" v-model="summaryDialogVisible" width="1200px" append-to-body>
<div v-loading="summaryLoading">
<el-table :data="expenseList" @selection-change="handleExpenseSelectionChange" max-height="500">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="账户名称" align="center" width="180" prop="accountName" />
<el-table-column label="交易时间" align="center" width="160" prop="createTime" />
<el-table-column label="交易金额" align="center" width="100" prop="amount" />
<el-table-column label="交易类别" align="center" width="140" prop="dealCategory">
<template #default="scope">
<dict-tag :options="deal_category" :value="scope.row.dealCategory" />
</template>
</el-table-column>
<el-table-column label="交易子类别" align="center" width="140" prop="childCategoryName" />
<el-table-column label="备注" align="center" prop="remark" show-overflow-tooltip />
</el-table>
<div style="margin-top: 16px; text-align: right; color: #606266; font-size: 14px">
已选 <span style="color: #409eff; font-weight: bold">{{ selectedExpenses.length }}</span> 合计金额<span
style="color: #f56c6c; font-weight: bold; font-size: 16px"
>{{ calculateTotalAmount() }}</span
>
</div>
</div>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="confirmSummary" :disabled="selectedExpenses.length === 0">确认汇总</el-button>
<el-button @click="summaryDialogVisible = false"> </el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup name="Activity">
import { listActivity, getActivity, delActivity, addActivity, updateActivity } from '@/api/health/activity'
import { listAccountDealRecord } from '@/api/invest/accountDealRecord'
// eslint-disable-next-line no-unused-vars
import dayjs from 'dayjs'
import { require } from '@/utils/require'
const { proxy } = getCurrentInstance()
const { activity_type, activity_exercise } = proxy.useDict('activity_type', 'activity_exercise')
const { activity_type, activity_exercise, deal_category, daily_expenses } = proxy.useDict(
'activity_type',
'activity_exercise',
'deal_category',
'daily_expenses'
)
const activityList = ref([])
const open = ref(false)
@@ -146,9 +222,17 @@ const single = ref(true)
const multiple = ref(true)
const total = ref(0)
const title = ref('')
const summaryDialogVisible = ref(false)
const summaryLoading = ref(false)
const expenseList = ref([])
const selectedExpenses = ref([])
const currentActivityId = ref(null)
const summaryTotal = ref(0)
const attachmentFileTypes = ['jpg', 'jpeg', 'png', 'pdf', 'doc', 'docx', 'xls', 'xlsx']
const operateList = ref([
{ id: 'view', icon: 'View', title: '查看', hasPermi: ['health:activity:query'] },
{ id: 'edit', icon: 'Edit', title: '修改', hasPermi: ['health:activity:edit'] },
{ id: 'summary', icon: 'Money', title: '费用汇总', hasPermi: ['health:activity:edit'] },
{ id: 'delete', icon: 'Delete', title: '删除', hasPermi: ['health:activity:remove'] }
])
const data = reactive({
@@ -182,6 +266,9 @@ const handleOperate = (operate, row) => {
case 'edit':
handleUpdate(row)
break
case 'summary':
handleCostSummary(row)
break
case 'delete':
handleDelete(row)
break
@@ -192,6 +279,39 @@ const handleOperate = (operate, row) => {
const { queryParams, form, rules } = toRefs(data)
function parseAttachments(attachment) {
if (!attachment) {
return []
}
return String(attachment)
.split(',')
.map((item) => item.trim())
.filter(Boolean)
}
function getAttachmentUrl(file) {
if (!file) {
return ''
}
if (/^(https?:)?\/\//.test(file) || file.startsWith('/') || file.startsWith('blob:') || file.startsWith('data:')) {
return file
}
return `/${file}`
}
function getAttachmentName(file, index) {
const purePath = String(file || '').split('?')[0]
const name = purePath.slice(purePath.lastIndexOf('/') + 1)
if (!name) {
return `附件${index + 1}`
}
try {
return decodeURIComponent(name)
} catch {
return name
}
}
/** 查询活动记录列表 */
function getList() {
loading.value = true
@@ -229,7 +349,8 @@ function reset() {
remark: null,
totalCost: 0,
partner: null,
costDetail: null
costDetail: null,
attachment: null
}
proxy.resetForm('activityRef')
}
@@ -345,5 +466,131 @@ function handleExport() {
)
}
// 费用汇总按钮操作
function handleCostSummary(row) {
currentActivityId.value = row.id
summaryDialogVisible.value = true
summaryLoading.value = true
// 构建查询参数
const queryExpenseParams = {
pageNum: 1,
pageSize: 1000,
dealCategory: '1', // 日常支出
startTime: row.startTime ? dayjs(row.startTime).format('YYYY-MM-DD') : '',
endTime: row.endTime ? dayjs(row.endTime).format('YYYY-MM-DD') : dayjs().format('YYYY-MM-DD')
}
// 查询日常支出列表
listAccountDealRecord(queryExpenseParams)
.then((response) => {
expenseList.value = response.rows || []
summaryLoading.value = false
})
.catch(() => {
summaryLoading.value = false
})
}
// 多选框选中数据(费用汇总)
function handleExpenseSelectionChange(selection) {
selectedExpenses.value = selection
}
// 计算选中费用总额
function calculateTotalAmount() {
return selectedExpenses.value
.reduce((sum, item) => {
return sum + (parseFloat(item.amount) || 0)
}, 0)
.toFixed(2)
}
// 确认汇总
function confirmSummary() {
if (selectedExpenses.value.length === 0) {
proxy.$modal.msgWarning('请至少选择一条费用记录')
return
}
// 获取活动详情
getActivity(currentActivityId.value).then((response) => {
const activity = response.data
// 计算总金额
const totalAmount = parseFloat(calculateTotalAmount())
const currentTotal = parseFloat(activity.totalCost) || 0
// 判断活动是否跨天
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
// 构建费用明细
let detailsWithDate = ''
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 + ')' : ''}`)
})
// 按日期组装明细
detailsWithDate = Object.keys(groupedByDate)
.sort()
.map((date) => `${date} ${groupedByDate[date].join('、')}`)
.join('')
} else {
// 单天活动:不带日期
detailsWithDate = selectedExpenses.value
.map((item) => {
const category = item.childCategoryName || ''
return `${category} ${item.amount}${item.remark ? '(' + item.remark + ')' : ''}`
})
.join('')
}
// 更新活动费用
const updateData = {
...activity,
totalCost: (currentTotal + totalAmount).toFixed(2),
costDetail: activity.costDetail ? `${activity.costDetail}${detailsWithDate}` : detailsWithDate
}
updateActivity(updateData).then(() => {
proxy.$modal.msgSuccess('费用汇总成功')
summaryDialogVisible.value = false
selectedExpenses.value = []
getList()
})
})
}
getList()
</script>
<style scoped lang="scss">
.attachment-popover-list,
.attachment-readonly-list {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 8px;
line-height: 1.4;
}
.attachment-form-list {
min-height: 32px;
padding: 4px 0;
}
.attachment-empty {
color: #909399;
}
</style>

View File

@@ -0,0 +1,582 @@
<template>
<div class="app-container">
<div class="search-con">
<div class="title">查询条件</div>
<el-form :model="queryParams" ref="queryRef" :inline="true" :rules="rules" label-width="100px">
<el-form-item label="活动名称" prop="name">
<el-input v-model="queryParams.name" placeholder="请输入活动名称" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="活动类型" prop="activityType">
<el-select v-model="queryParams.activityType" placeholder="请选择活动类型" clearable>
<el-option v-for="dict in activity_type" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
<el-form-item label="活动量" prop="activityVolume">
<el-select v-model="queryParams.activityVolume" placeholder="请选择活动量" clearable>
<el-option v-for="dict in activity_exercise" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
<el-form-item label="活动地点" prop="place">
<el-input v-model="queryParams.place" placeholder="请输入活动地点" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="统计粒度" prop="type">
<el-radio-group v-model="queryParams.type" @change="handleTimeChange">
<el-radio-button :label="1"></el-radio-button>
<el-radio-button :label="2"></el-radio-button>
<el-radio-button :label="3"></el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item label="活动日期" prop="time" v-if="queryParams.type === 1">
<el-date-picker
v-model="queryParams.time"
type="daterange"
range-separator="~"
start-placeholder="开始时间"
end-placeholder="结束时间"
format="YYYY-MM-DD"
@calendar-change="calendarChange"
/>
</el-form-item>
<el-form-item label="活动月份" prop="time" v-if="queryParams.type === 2">
<el-date-picker
v-model="queryParams.time"
type="monthrange"
range-separator="~"
format="YYYY-MM"
start-placeholder="开始时间"
end-placeholder="结束时间"
:disabled-date="disabledDateFun"
@calendar-change="calendarChange"
/>
</el-form-item>
<el-form-item label="活动年份" prop="time" v-if="queryParams.type === 3">
<yearPicker
v-model="queryParams.time"
ref="statisticPicker"
labelText="选择年份"
:initYear="dateValue"
:showYear="showYearValue"
:maxLength="5"
sp="~"
@updateTimeRange="updateStatisticYear"
/>
</el-form-item>
</el-form>
<div class="search-btn-con">
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button type="info" icon="Refresh" @click="resetQuery">重置</el-button>
</div>
</div>
<div class="main-con activity-stat-main" style="height: calc(100% - 1.45rem)">
<div class="summary-con" style="height: 115px">
<div class="right-con">
<div class="icon-box">
<el-icon :size="40" color="#409EFF"><Histogram /></el-icon>
</div>
<div class="item-wrap">
<div class="title">活动次数</div>
<div>
<span class="num">{{ activity.activityCount || 0 }}</span
><span class="unit"></span>
</div>
</div>
</div>
<div class="right-con">
<div class="icon-box">
<el-icon :size="40" color="#67C23A"><Calendar /></el-icon>
</div>
<div class="item-wrap">
<div class="title">活动天数</div>
<div>
<span class="num">{{ activity.activityDays || 0 }}</span
><span class="unit"></span>
</div>
</div>
</div>
<div class="right-con">
<div class="icon-box">
<el-icon :size="40" color="#E6A23C"><Timer /></el-icon>
</div>
<div class="item-wrap">
<div class="title">总时长</div>
<div>
<span class="num">{{ activity.totalHours || 0 }}</span
><span class="unit">小时</span>
</div>
</div>
</div>
<div class="right-con">
<div class="icon-box">
<el-icon :size="40" color="#F56C6C"><Money /></el-icon>
</div>
<div class="item-wrap">
<div class="title">活动费用</div>
<div>
<span class="num">{{ activity.totalCost || 0 }}</span
><span class="unit"></span>
</div>
</div>
</div>
<div class="right-con">
<div class="icon-box">
<el-icon :size="40" color="#67C23A"><DataLine /></el-icon>
</div>
<div class="item-wrap">
<div class="title">平均时长</div>
<div>
<span class="num">{{ activity.avgHours || 0 }}</span
><span class="unit">小时/</span>
</div>
</div>
</div>
</div>
<div class="summary-con" style="height: 115px">
<div class="right-con">
<div class="icon-box">
<el-icon :size="40" color="#E6A23C"><Coin /></el-icon>
</div>
<div class="item-wrap">
<div class="title">平均费用</div>
<div>
<span class="num">{{ activity.avgCost || 0 }}</span
><span class="unit">/</span>
</div>
</div>
</div>
<div class="right-con">
<div class="icon-box">
<el-icon :size="40" color="#F56C6C"><Medal /></el-icon>
</div>
<div class="item-wrap highlight-item">
<div class="title">最高费用活动</div>
<div class="summary-detail">
<span class="num text-name">{{ activity.maxCostName || '--' }}</span>
<span class="unit">{{ activity.maxCost || 0 }}</span>
</div>
</div>
</div>
<div class="right-con">
<div class="icon-box">
<el-icon :size="40" color="#409EFF"><Stopwatch /></el-icon>
</div>
<div class="item-wrap highlight-item">
<div class="title">最长活动</div>
<div class="summary-detail">
<span class="num text-name">{{ activity.longestName || '--' }}</span>
<span class="unit">{{ activity.longestHours || 0 }}小时</span>
</div>
</div>
</div>
<div class="right-con">
<div class="icon-box">
<el-icon :size="40" color="#67C23A"><Location /></el-icon>
</div>
<div class="item-wrap highlight-item">
<div class="title">常去地点</div>
<div class="summary-detail">
<span class="num text-name">{{ topPlace.name }}</span>
<span class="unit">{{ topPlace.count }}</span>
</div>
</div>
</div>
<div class="right-con">
<div class="icon-box">
<el-icon :size="40" color="#409EFF"><User /></el-icon>
</div>
<div class="item-wrap highlight-item">
<div class="title">常参与成员</div>
<div class="summary-detail">
<span class="num text-name">{{ topPartner.name }}</span>
<span class="unit">{{ topPartner.count }}</span>
</div>
</div>
</div>
</div>
<div class="title-con" style="margin-top: 18px">
<div class="title">活动统计</div>
<div class="operate-btn-con">
<el-radio-group v-model="radioVal" @change="handleRadioChange">
<el-radio-button label="活动明细" />
<el-radio-button label="次数趋势" />
<el-radio-button label="费用趋势" />
<el-radio-button label="时长趋势" />
<el-radio-button label="类型分布" />
<el-radio-button label="地点排行" />
<el-radio-button label="成员排行" />
</el-radio-group>
</div>
</div>
<div class="content-con">
<div v-show="radioVal === '次数趋势'" class="chart" id="activityCountChart" style="height: calc(100% - 245px); margin-top: -10px"></div>
<div v-show="radioVal === '费用趋势'" class="chart" id="activityCostChart" style="height: calc(100% - 245px); margin-top: -10px"></div>
<div v-show="radioVal === '时长趋势'" class="chart" id="activityDurationChart" style="height: calc(100% - 245px); margin-top: -10px"></div>
<div v-show="radioVal === '类型分布'" class="chart-grid" style="height: calc(100% - 245px)">
<div class="chart" id="activityTypeChart"></div>
<div class="chart" id="activityVolumeChart"></div>
</div>
<el-table v-show="radioVal === '地点排行'" v-loading="loading" :data="activity.placeList || []" height="calc(100% - 245px)" class="wrap-table">
<el-table-column label="序号" width="70" type="index" align="center" />
<el-table-column label="活动地点" align="center" prop="name" min-width="220" class-name="wrap-cell" />
<el-table-column label="活动次数" align="center" prop="count" />
<el-table-column label="累计时长(小时)" align="center" prop="hours" />
<el-table-column label="累计费用(元)" align="center" prop="cost" />
</el-table>
<el-table v-show="radioVal === '成员排行'" v-loading="loading" :data="activity.partnerList || []" height="calc(100% - 245px)" class="wrap-table">
<el-table-column label="序号" width="70" type="index" align="center" />
<el-table-column label="成员" align="center" prop="name" min-width="220" class-name="wrap-cell" />
<el-table-column label="参与次数" align="center" prop="count" />
<el-table-column label="累计时长(小时)" align="center" prop="hours" />
<el-table-column label="关联费用(元)" align="center" prop="cost" />
</el-table>
<el-table v-show="radioVal === '活动明细'" v-loading="loading" :data="activity.tableList || []" height="calc(100% - 245px)" class="wrap-table">
<el-table-column label="序号" width="60" type="index" align="center" />
<el-table-column label="活动名称" align="center" prop="name" min-width="180" class-name="wrap-cell" />
<el-table-column label="活动类型" align="center" prop="typeName" width="120" />
<el-table-column label="活动量" align="center" prop="activityVolumeName" width="110" />
<el-table-column label="活动地点" align="center" prop="place" min-width="160" class-name="wrap-cell" />
<el-table-column label="成员" align="center" prop="partner" min-width="180" class-name="wrap-cell" />
<el-table-column label="开始时间" align="center" prop="startTime" width="170" />
<el-table-column label="结束时间" align="center" prop="endTime" width="170" />
<el-table-column label="活动时长" align="center" prop="duration" width="130" />
<el-table-column label="费用(元)" align="center" prop="totalCost" width="110" />
<el-table-column label="饮食" align="center" prop="foods" min-width="220" class-name="wrap-cell" />
<el-table-column label="收获" align="center" prop="harvest" min-width="220" class-name="wrap-cell" />
<el-table-column label="备注" align="center" prop="remark" min-width="220" class-name="wrap-cell" />
</el-table>
</div>
</div>
</div>
</template>
<script setup name="ActivityStatistic">
import dayjs from 'dayjs'
import * as echarts from 'echarts'
import { ElMessage } from 'element-plus'
import { Calendar, Coin, DataLine, Histogram, Location, Medal, Money, Stopwatch, Timer, User } from '@element-plus/icons-vue'
import yearPicker from '@/components/YearPicker/index.vue'
import { getActivityAnalysis } from '@/api/health/statisticAnalysis'
const { proxy } = getCurrentInstance()
const { activity_type, activity_exercise } = proxy.useDict('activity_type', 'activity_exercise')
const loading = ref(false)
const radioVal = ref('活动明细')
const currentType = ref('活动明细')
const firstChooseDate = ref('')
const dateValue = ref({ startYear: 2000, endYear: new Date().getFullYear() })
const showYearValue = ref({ startShowYear: '', endShowYear: '' })
const activity = ref({})
const chartData = ref({
name: [],
count: [],
cost: [],
duration: []
})
const dateValidate = (rules, value, callback) => {
const dateType = rules.dateType || 'months'
const num = rules.num || 23
const message = rules.message || `时间跨度不能超过${num}个月`
if (value && value.length === 2) {
const start = value[0]
const end = value[1]
if (dayjs(end).diff(dayjs(start), dateType) > num) {
queryParams.value.time = [start, dayjs(start).add(num, dateType)]
ElMessage.warning(message)
}
}
}
const data = reactive({
queryParams: {
type: 1,
time: null,
name: null,
activityType: null,
activityVolume: null,
place: null
},
rules: {
time: [{ validator: dateValidate, dateType: 'months', num: 23, message: '时间跨度不能大于24个月默认选择开始日期的24个月' }]
}
})
const { queryParams, rules } = toRefs(data)
const topPlace = computed(() => {
const item = (activity.value.placeList || [])[0] || {}
return { name: item.name || '--', count: item.count || 0 }
})
const topPartner = computed(() => {
const item = (activity.value.partnerList || [])[0] || {}
return { name: item.name || '--', count: item.count || 0 }
})
const updateStatisticYear = (startYear, endYear) => {
queryParams.value.time = [new Date(startYear, 0, 1), new Date(endYear, 0, 1)]
if (endYear - startYear > 5) {
ElMessage.warning('时间跨度不能大于5年默认选择开始日期的5年')
queryParams.value.time = [new Date(startYear, 0, 1), new Date(startYear + 5, 0, 1)]
}
}
const disabledDateFun = (time) => {
const arr = [0, 30, 365, 365 * 5]
const days = arr[queryParams.value.type]
if (firstChooseDate.value) {
const one = days * 24 * 3600 * 1000
const minTime = firstChooseDate.value - one
const maxTime = firstChooseDate.value + one
const endTime = maxTime > Date.now() ? Date.now() : maxTime
return time.getTime() > endTime || time.getTime() < minTime
}
return time.getTime() > Date.now()
}
const calendarChange = (val) => {
firstChooseDate.value = val[0].getTime()
if (val[1]) firstChooseDate.value = ''
}
function handleTimeChange(type) {
queryParams.value.time = null
if (type === 1) {
const end = dayjs().format('YYYY-MM-DD')
queryParams.value.time = [dayjs(end).add(-60, 'day'), end]
} else if (type === 2) {
const end = dayjs().format('YYYY-MM')
queryParams.value.time = [dayjs(end).add(-11, 'months'), end]
} else if (type === 3) {
const endYear = new Date().getFullYear()
queryParams.value.time = [new Date(endYear - 2, 0, 1), new Date(endYear, 0, 1)]
showYearValue.value = { startShowYear: endYear - 2, endShowYear: endYear }
}
getList()
}
function getList() {
loading.value = true
chartData.value = { name: [], count: [], cost: [], duration: [] }
const { type, time, name, activityType, activityVolume, place } = queryParams.value
let formatValue = 'YYYY-MM-DD'
if (type === 2) formatValue = 'YYYY-MM'
if (type === 3) formatValue = 'YYYY'
const params = {
type,
startTime: time && time.length > 0 ? dayjs(time[0]).format(formatValue) : '',
endTime: time && time.length > 0 ? dayjs(time[1]).format(formatValue) : '',
name,
activityType,
activityVolume,
place
}
getActivityAnalysis(params)
.then((response) => {
activity.value = response.data || {}
const countMap = new Map((activity.value.countTrend || []).map((item) => [item.time, Number(item.value || 0)]))
const costMap = new Map((activity.value.costTrend || []).map((item) => [item.time, Number(item.value || 0)]))
const durationMap = new Map((activity.value.durationTrend || []).map((item) => [item.time, Number(item.value || 0)]))
const names = [
...new Set(
[...(activity.value.countTrend || []), ...(activity.value.costTrend || []), ...(activity.value.durationTrend || [])].map((item) => item.time)
)
].sort()
chartData.value = {
name: names,
count: names.map((time) => countMap.get(time) || 0),
cost: names.map((time) => costMap.get(time) || 0),
duration: names.map((time) => durationMap.get(time) || 0)
}
handleRadioChange(currentType.value)
})
.finally(() => {
loading.value = false
})
}
function handleQuery() {
getList()
}
function resetQuery() {
proxy.resetForm('queryRef')
queryParams.value.name = null
queryParams.value.activityType = null
queryParams.value.activityVolume = null
queryParams.value.place = null
handleTimeChange(1)
}
function chartBaseOption(data, seriesName, unit, color) {
return {
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' },
formatter: (params) => `${params[0].name}<br/>${seriesName}${params[0].value}${unit}`
},
grid: { left: '5%', right: '5%', bottom: '5%', containLabel: true },
xAxis: {
type: 'category',
data: data.name,
axisTick: { alignWithLabel: true },
axisLine: { lineStyle: { width: 1, color: '#999' } }
},
yAxis: {
type: 'value',
axisTick: { show: true },
axisLine: { show: true, lineStyle: { width: 1, color: '#999' } }
},
series: [
{
name: seriesName,
data: data.value,
type: data.type,
barWidth: 12,
smooth: true,
lineStyle: { color },
itemStyle: { color },
areaStyle:
data.type === 'line'
? {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: `${color}99` },
{ offset: 1, color: `${color}16` }
])
}
: undefined
}
]
}
}
function renderTrendChart(id, value, name, unit, color, type = 'bar') {
if (document.getElementById(id) === null) return
echarts.dispose(document.getElementById(id))
const myChart = echarts.init(document.getElementById(id))
myChart.setOption(chartBaseOption({ name: chartData.value.name, value, type }, name, unit, color))
setTimeout(() => myChart.resize(), 100)
}
function renderPieChart(id, data, title) {
if (document.getElementById(id) === null) return
echarts.dispose(document.getElementById(id))
const myChart = echarts.init(document.getElementById(id))
myChart.setOption({
title: { text: title, left: 'center', top: 16 },
tooltip: { trigger: 'item' },
legend: { bottom: 10 },
series: [
{
type: 'pie',
radius: ['42%', '70%'],
center: ['50%', '48%'],
data,
label: { formatter: '{b}\n{c}次' }
}
]
})
setTimeout(() => myChart.resize(), 100)
}
function handleRadioChange(type) {
currentType.value = type
nextTick(() => {
switch (type) {
case '次数趋势':
renderTrendChart('activityCountChart', chartData.value.count, '活动次数', '次', '#409EFF', 'bar')
break
case '费用趋势':
renderTrendChart('activityCostChart', chartData.value.cost, '活动费用', '元', '#E6A23C', 'line')
break
case '时长趋势':
renderTrendChart('activityDurationChart', chartData.value.duration, '活动时长', '小时', '#67C23A', 'line')
break
case '类型分布':
renderPieChart('activityTypeChart', activity.value.typeList || [], '活动类型分布')
renderPieChart('activityVolumeChart', activity.value.volumeList || [], '活动量分布')
break
default:
break
}
})
}
handleTimeChange(1)
</script>
<style lang="scss" scoped>
.activity-stat-main {
.summary-con {
.right-con,
.icon-box,
.item-wrap,
.num,
.title {
list-style: none !important;
&::before,
&::after {
display: none !important;
content: none !important;
}
}
}
}
.text-name {
display: block;
max-width: 100%;
overflow: visible;
font-size: 22px !important;
line-height: 1.2;
text-overflow: initial;
white-space: normal;
word-break: break-all;
}
.highlight-item {
min-width: 0;
}
.summary-detail {
display: flex;
min-width: 0;
flex-direction: column;
align-items: flex-start;
gap: 4px;
.unit {
display: block;
margin-left: 0;
line-height: 1.2;
}
}
.wrap-table {
:deep(.el-table__cell) {
vertical-align: top;
}
:deep(.wrap-cell .cell) {
overflow: visible;
line-height: 20px;
text-overflow: initial;
white-space: normal;
word-break: break-all;
}
}
.chart-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16px;
}
.chart-grid .chart {
min-height: 320px;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -63,6 +63,28 @@
<el-table-column label="就诊时间" align="center" prop="visitingTime" width="160"> </el-table-column>
<el-table-column label="总费用(元)" align="center" width="120" prop="totalCost" />
<el-table-column label="诊断结果" align="center" prop="diagnosis" />
<el-table-column label="附件" align="center" prop="attachment" width="120">
<template #default="scope">
<el-popover v-if="parseAttachments(scope.row.attachment).length" placement="left" width="280" trigger="click">
<template #reference>
<el-button link type="primary">查看{{ parseAttachments(scope.row.attachment).length }}</el-button>
</template>
<div class="attachment-popover-list">
<el-link
v-for="(file, index) in parseAttachments(scope.row.attachment)"
:key="file"
type="primary"
:href="getAttachmentUrl(file)"
target="_blank"
:underline="false"
>
{{ getAttachmentName(file, index) }}
</el-link>
</div>
</el-popover>
<span v-else class="attachment-empty"></span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="180" class-name="small-padding fixed-width">
<template v-slot="scope">
<div class="ctrl-btn d-flex">
@@ -132,6 +154,22 @@
<el-form-item label="费用明细" style="width: 792px" prop="costDetail">
<el-input v-model="form.costDetail" type="textarea" placeholder="请输入费用明细" />
</el-form-item>
<el-form-item label="附件" style="width: 792px" prop="attachment">
<div v-if="title === '查看就医记录'" class="attachment-readonly-list attachment-form-list">
<el-link
v-for="(file, index) in parseAttachments(form.attachment)"
:key="file"
type="primary"
:href="getAttachmentUrl(file)"
target="_blank"
:underline="false"
>
{{ getAttachmentName(file, index) }}
</el-link>
<span v-if="!parseAttachments(form.attachment).length" class="attachment-empty">暂无附件</span>
</div>
<file-upload v-else v-model="form.attachment" :limit="9" :file-size="10" :file-type="attachmentFileTypes" />
</el-form-item>
<el-form-item label="备注" style="width: 792px" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
</el-form-item>
@@ -287,6 +325,7 @@ const total = ref(0)
const title = ref('')
const personList = ref([])
const healthRecordList = ref([])
const attachmentFileTypes = ['jpg', 'jpeg', 'png', 'pdf', 'doc', 'docx', 'xls', 'xlsx']
const detailOpen = ref(false)
const openDetail = ref(false)
@@ -395,8 +434,41 @@ const handleOperate = (operate, row) => {
const { queryParams, form, formDetail, rules, queryPersonParams, queryMedicineParams, queryHealthRecordParams, rulesDetail } = toRefs(data)
function parseAttachments(attachment) {
if (!attachment) {
return []
}
return String(attachment)
.split(',')
.map((item) => item.trim())
.filter(Boolean)
}
function getAttachmentUrl(file) {
if (!file) {
return ''
}
if (/^(https?:)?\/\//.test(file) || file.startsWith('/') || file.startsWith('blob:') || file.startsWith('data:')) {
return file
}
return `/${file}`
}
function getAttachmentName(file, index) {
const purePath = String(file || '').split('?')[0]
const name = purePath.slice(purePath.lastIndexOf('/') + 1)
if (!name) {
return `附件${index + 1}`
}
try {
return decodeURIComponent(name)
} catch {
return name
}
}
const handlePersonChange = (personId) => {
queryHealthRecordParams.personId = personId
queryHealthRecordParams.value.personId = personId
listHealthRecord(queryHealthRecordParams).then((response) => {
healthRecordList.value = response.rows
if (response.rows.length > 0) {
@@ -405,7 +477,7 @@ const handlePersonChange = (personId) => {
})
}
const handleQueryPersonChange = (personId) => {
queryHealthRecordParams.personId = personId
queryHealthRecordParams.value.personId = personId
queryParams.value.healthRecordId = null
listHealthRecord(queryHealthRecordParams).then((response) => {
healthRecordList.value = response.rows
@@ -780,7 +852,8 @@ function reset() {
personId: null,
totalCost: 0,
partner: null,
costDetail: null
costDetail: null,
attachment: null
}
proxy.resetForm('doctorRecordRef')
}
@@ -825,7 +898,7 @@ function handleSelectionChange(selection) {
// 查看
const handleView = (row) => {
title.value = '查看就医记录'
queryHealthRecordParams.personId = row.personId
queryHealthRecordParams.value.personId = row.personId
listHealthRecord(queryHealthRecordParams).then((response) => {
healthRecordList.value = response.rows
})
@@ -839,7 +912,7 @@ function handleAdd() {
form.value.visitingTime = dayjs(new Date().getTime()).format('YYYY-MM-DD HH:mm:ss')
if (personList.value.length > 0) {
form.value.personId = personList.value[0].id
queryHealthRecordParams.personId = personList.value[0].id
queryHealthRecordParams.value.personId = personList.value[0].id
listHealthRecord(queryHealthRecordParams).then((response) => {
healthRecordList.value = response.rows
if (response.rows.length > 0) {
@@ -854,7 +927,7 @@ function handleAdd() {
/** 修改按钮操作 */
function handleUpdate(row) {
reset()
queryHealthRecordParams.personId = row.personId
queryHealthRecordParams.value.personId = row.personId
listHealthRecord(queryHealthRecordParams).then((response) => {
healthRecordList.value = response.rows
})
@@ -916,3 +989,23 @@ function handleExport() {
getList()
getPersonList()
</script>
<style scoped lang="scss">
.attachment-popover-list,
.attachment-readonly-list {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 8px;
line-height: 1.4;
}
.attachment-form-list {
min-height: 32px;
padding: 4px 0;
}
.attachment-empty {
color: #909399;
}
</style>

View File

@@ -41,6 +41,7 @@
<div class="operate-btn-con">
<el-button @click="handleAdd" icon="Plus" v-hasPermi="['health:healthRecord:add']">新增</el-button>
<el-button :disabled="multiple" icon="Delete" @click="handleDelete" v-hasPermi="['health:healthRecord:remove']">删除</el-button>
<el-button :disabled="single" icon="Download" @click="handleFullExport()" v-hasPermi="['health:healthRecord:export']">完整导出</el-button>
<!-- <el-button @click="handleExport" icon="Download" v-hasPermi="['health:healthRecord:export']">导出</el-button> -->
</div>
</div>
@@ -323,6 +324,7 @@ const processRecordList = ref([])
const operateList = ref([
{ id: 'view', icon: 'View', title: '查看', hasPermi: ['health:healthRecord:query'] },
{ id: 'fullExport', icon: 'Download', title: '完整导出', hasPermi: ['health:healthRecord:export'] },
{ id: 'edit', icon: 'Edit', title: '修改', hasPermi: ['health:healthRecord:edit'] },
{ id: 'delete', icon: 'Delete', title: '删除', hasPermi: ['health:healthRecord:remove'] }
])
@@ -381,6 +383,9 @@ const handleOperate = (operate, row) => {
case 'edit':
handleUpdate(row)
break
case 'fullExport':
handleFullExport(row)
break
case 'delete':
handleDelete(row)
break
@@ -623,6 +628,22 @@ function handleExport() {
)
}
function handleFullExport(row) {
const recordId = row && row.id ? row.id : ids.value[0]
const recordName = row && row.name ? row.name : '健康档案'
if (!recordId) {
proxy.$modal.msgWarning('请选择要导出的健康档案')
return
}
proxy.download(
'health/analysis/recordFullExport',
{
recordId
},
`${recordName}_完整信息_${new Date().getTime()}.xlsx`
)
}
getList()
getPersonList()
</script>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,705 @@
<template>
<div class="app-container">
<div class="search-con">
<div class="title">查询条件</div>
<el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="100px">
<el-form-item label="计划名称" prop="planName">
<el-input v-model="queryParams.planName" placeholder="请输入计划名称" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="人员" prop="personId">
<el-select v-model="queryParams.personId" placeholder="请选择人员" clearable filterable>
<el-option v-for="item in personList" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item label="频率类型" prop="frequencyType">
<el-select v-model="queryParams.frequencyType" placeholder="请选择频率类型" clearable>
<el-option v-for="dict in medication_frequency_type" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="请选择状态" clearable>
<el-option v-for="dict in medication_plan_status" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
<el-form-item label="关键字" prop="keys">
<el-input v-model="queryParams.keys" placeholder="计划名称/药品名称" clearable @keyup.enter="handleQuery" />
</el-form-item>
</el-form>
<div class="search-btn-con">
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button type="info" icon="Refresh" @click="resetQuery">重置</el-button>
</div>
</div>
<div class="main-con">
<div class="title-con">
<div class="title">用药计划</div>
<div class="operate-btn-con">
<el-button @click="handleAdd" icon="Plus" v-hasPermi="['health:medicationPlan:add']">新增</el-button>
<el-button :disabled="multiple" icon="Delete" @click="handleDelete" v-hasPermi="['health:medicationPlan:remove']">删除</el-button>
</div>
</div>
<div class="content-con" v-loading="loading" height="calc(100% - 0.65rem)">
<el-table v-loading="loading" :data="planList" @selection-change="handleSelectionChange" height="calc(100% - 0.65rem)">
<el-table-column type="selection" width="55" align="center" />
<el-table-column type="index" label="序号" :index="indexMethod" width="50"></el-table-column>
<el-table-column label="计划名称" align="center" prop="planName" width="150" />
<el-table-column label="人员" align="center" prop="personName" width="100" />
<el-table-column label="药品" align="center" prop="medicineName" width="200" />
<el-table-column label="剂量" align="center" width="100">
<template #default="scope">
<span style="display: inline-flex; align-items: center; gap: 4px">
{{ scope.row.dosagePerTime }}
<dict-tag :options="medical_unit" :value="scope.row.dosageUnit" style="display: inline-flex" />
</span>
</template>
</el-table-column>
<el-table-column label="频次类型" align="center" prop="frequencyType" width="100">
<template #default="scope">
<dict-tag :options="medication_frequency_type" :value="scope.row.frequencyType" />
</template>
</el-table-column>
<el-table-column label="服药时间" align="center" width="180">
<template #default="scope">
<div v-if="scope.row.timeSlots" class="time-slots-list">
<el-tag v-for="(time, idx) in parseTimeSlots(scope.row.timeSlots)" :key="idx" size="small" type="info" class="time-tag">
{{ time }}
</el-tag>
</div>
<span v-else>--</span>
</template>
</el-table-column>
<el-table-column label="开始日期" align="center" prop="startDate" width="110">
<template #default="scope">
{{ formatDate(scope.row.startDate) }}
</template>
</el-table-column>
<el-table-column label="结束日期" align="center" prop="endDate" width="110">
<template #default="scope">
{{ scope.row.endDate ? formatDate(scope.row.endDate) : '长期' }}
</template>
</el-table-column>
<el-table-column label="今日进度" align="center" width="100">
<template #default="scope">
<span v-if="scope.row.status === 1"> {{ scope.row.todayCompleted || 0 }}/{{ scope.row.todayTotal || 0 }} </span>
<span v-else>--</span>
</template>
</el-table-column>
<el-table-column label="提醒" align="center" prop="remindEnabled" width="80">
<template #default="scope">
<el-tag v-if="scope.row.remindEnabled === 1" type="success"></el-tag>
<el-tag v-else type="info"></el-tag>
</template>
</el-table-column>
<el-table-column label="状态" align="center" prop="status" width="90">
<template #default="scope">
<dict-tag :options="medication_plan_status" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding">
<template v-slot="scope">
<div class="ctrl-btn d-flex">
<el-tooltip class="item" effect="dark" content="查看" placement="top">
<el-button icon="View" v-hasPermi="['health:medicationPlan:query']" circle @click="handleView(scope.row)"></el-button>
</el-tooltip>
<el-tooltip v-if="scope.row.status === 1" class="item" effect="dark" content="暂停" placement="top">
<el-button icon="VideoPause" v-hasPermi="['health:medicationPlan:edit']" circle @click="handlePause(scope.row)"></el-button>
</el-tooltip>
<el-tooltip v-if="scope.row.status === 2" class="item" effect="dark" content="恢复" placement="top">
<el-button icon="VideoPlay" v-hasPermi="['health:medicationPlan:edit']" circle @click="handleResume(scope.row)"></el-button>
</el-tooltip>
<el-tooltip v-if="scope.row.status !== 3" class="item" effect="dark" content="结束" placement="top">
<el-button icon="Finished" v-hasPermi="['health:medicationPlan:edit']" circle @click="handleEnd(scope.row)"></el-button>
</el-tooltip>
<el-tooltip class="item" effect="dark" content="修改" placement="top">
<el-button icon="Edit" v-hasPermi="['health:medicationPlan:edit']" circle @click="handleUpdate(scope.row)"></el-button>
</el-tooltip>
<el-tooltip class="item" effect="dark" content="删除" placement="top">
<el-button icon="Delete" v-hasPermi="['health:medicationPlan:remove']" circle @click="handleDelete(scope.row)"></el-button>
</el-tooltip>
</div>
</template>
</el-table-column>
</el-table>
<el-pagination
small
background
layout="total,sizes, prev, pager, next"
:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</div>
<!-- 添加或修改用药计划对话框 -->
<el-dialog :title="title" v-model="open" width="900px" append-to-body>
<el-form ref="planRef" :model="form" :inline="true" :rules="rules" label-width="120px">
<el-form-item label="计划名称" prop="planName">
<el-input v-model="form.planName" placeholder="请输入计划名称(可选)" />
</el-form-item>
<el-form-item label="人员" prop="personId">
<el-select v-model="form.personId" placeholder="请选择人员" filterable @change="handlePersonChange">
<el-option v-for="item in personList" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item label="慢性疾病" prop="chronicDiseaseId">
<el-select v-model="form.chronicDiseaseId" placeholder="请选择慢性疾病(可选)" filterable clearable>
<el-option v-for="item in chronicDiseaseList" :key="item.id" :label="item.diseaseName" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item label="药品" prop="medicineId">
<el-select v-model="form.medicineId" placeholder="请选择药品" filterable @change="handleMedicineChange">
<el-option v-for="item in medicineList" :key="item.id" :label="item.shortName + '-' + item.brand" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item label="每次剂量" prop="dosagePerTime">
<el-input-number v-model="form.dosagePerTime" :min="0" :precision="2" style="width: 150px" />
<el-select v-model="form.dosageUnit" placeholder="单位" style="width: 100px; margin-left: 10px">
<el-option v-for="dict in medical_unit" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
<el-form-item label="频次类型" prop="frequencyType">
<el-select v-model="form.frequencyType" placeholder="请选择频次类型">
<el-option label="每天" :value="1" />
<el-option label="每周" :value="2" />
<el-option label="隔天" :value="3" />
<el-option label="自定义" :value="4" />
</el-select>
</el-form-item>
<el-form-item label="服药时间点" prop="timeSlots" style="width: 100%">
<div class="time-slots-wrapper">
<div v-for="(time, index) in form.timePointList" :key="index" class="time-slot-item">
<span class="time-text">{{ time }}</span>
<el-icon class="close-icon" @click="removeTimePoint(index)"><Close /></el-icon>
</div>
<el-time-picker
v-model="newTimePoint"
placeholder="添加时间"
format="HH:mm"
value-format="HH:mm"
@change="addTimePoint"
style="width: 110px"
size="small"
/>
</div>
</el-form-item>
<el-form-item label="开始日期" prop="startDate">
<el-date-picker v-model="form.startDate" type="date" placeholder="选择开始日期" value-format="YYYY-MM-DD" />
</el-form-item>
<el-form-item label="结束日期" prop="endDate">
<el-date-picker v-model="form.endDate" type="date" placeholder="选择结束日期(可选)" value-format="YYYY-MM-DD" />
</el-form-item>
<el-form-item label="启用提醒" prop="remindEnabled">
<el-switch v-model="form.remindEnabled" :active-value="1" :inactive-value="0" />
</el-form-item>
<el-form-item v-if="form.remindEnabled === 1" label="提前提醒" prop="remindAdvanceMin">
<el-input-number v-model="form.remindAdvanceMin" :min="0" :max="60" />
<span style="margin-left: 10px">分钟</span>
</el-form-item>
<el-form-item label="通知家属" prop="notifyFamily">
<el-switch v-model="form.notifyFamily" :active-value="1" :inactive-value="0" />
</el-form-item>
<el-form-item v-if="form.notifyFamily === 1" label="家属成员" prop="familyMemberIds">
<el-select v-model="form.familyMemberIdList" multiple placeholder="请选择家属成员" filterable style="width: 300px">
<el-option v-for="item in personList" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item label="备注" style="width: 100%">
<el-input v-model="form.remark" type="textarea" placeholder="请输入备注" :rows="3" />
</el-form-item>
</el-form>
<template v-if="title !== '查看用药计划'" #footer>
<div class="dialog-footer">
<el-button type="primary" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup name="MedicationPlan">
import {
listMedicationPlan,
getMedicationPlan,
delMedicationPlan,
addMedicationPlan,
updateMedicationPlan,
pauseMedicationPlan,
resumeMedicationPlan,
endMedicationPlan
} from '@/api/health/medicationPlan'
import { listMedicineBasic } from '@/api/health/medicineBasic'
import { listPerson } from '@/api/health/person'
import { listChronicDiseaseByMember } from '@/api/health/chronicDisease'
import { Close } from '@element-plus/icons-vue'
const { proxy } = getCurrentInstance()
const route = useRoute()
const { medication_frequency_type, medication_plan_status, medical_unit } = proxy.useDict('medication_frequency_type', 'medication_plan_status', 'medical_unit')
const planList = ref([])
const personList = ref([])
const medicineList = ref([])
const chronicDiseaseList = ref([])
const open = ref(false)
const loading = ref(true)
const showSearch = ref(true)
const ids = ref([])
const single = ref(true)
const multiple = ref(true)
const total = ref(0)
const title = ref('')
const newTimePoint = ref(null)
const data = reactive({
form: {},
queryParams: {
pageNum: 1,
pageSize: 10,
planName: null,
personId: null,
chronicDiseaseId: null,
frequencyType: null,
status: null,
keys: null,
keyword: null
},
rules: {
personId: [{ required: true, message: '请选择人员', trigger: 'change' }],
medicineId: [{ required: true, message: '请选择药品', trigger: 'change' }],
dosagePerTime: [{ required: true, message: '请输入剂量', trigger: 'blur' }],
startDate: [{ required: true, message: '请选择开始日期', trigger: 'change' }]
}
})
const { queryParams, form, rules } = toRefs(data)
/** 查询用药计划列表 */
function getList() {
loading.value = true
const params = { ...queryParams.value, keyword: queryParams.value.keys }
listMedicationPlan(params).then((response) => {
planList.value = response.rows
total.value = response.total
loading.value = false
})
}
/** 查询人员列表 */
function getPersonList() {
listPerson({ pageNum: 1, pageSize: 100 }).then((response) => {
personList.value = response.rows || []
})
}
/** 查询药品列表 */
function getMedicineList() {
listMedicineBasic({ pageNum: 1, pageSize: 1000 }).then((response) => {
medicineList.value = response.rows || []
})
}
// 取消按钮
function cancel() {
open.value = false
reset()
}
// 表单重置
function reset() {
form.value = {
id: null,
personId: null,
chronicDiseaseId: null,
medicineId: null,
planName: null,
medicineName: null,
specification: null,
dosagePerTime: 1,
dosageUnit: '片',
frequencyType: 1,
frequencyConfig: null,
timeSlots: null,
timePointList: ['08:00', '12:00', '18:00'],
startDate: null,
endDate: null,
remindEnabled: 0,
remindAdvanceMin: 15,
notifyFamily: 0,
familyMemberIds: null,
familyMemberIdList: [],
status: 1,
remark: null
}
chronicDiseaseList.value = []
proxy.resetForm('planRef')
}
/** 搜索按钮操作 */
function handleQuery() {
queryParams.value.pageNum = 1
getList()
}
/** 重置按钮操作 */
function resetQuery() {
proxy.resetForm('queryRef')
queryParams.value.chronicDiseaseId = null
handleQuery()
}
// 分页
const handleCurrentChange = (val) => {
queryParams.value.pageNum = val
getList()
}
const handleSizeChange = (val) => {
queryParams.value.pageSize = val
getList()
}
// 序号翻页递增
const indexMethod = (index) => {
const nowPage = queryParams.value.pageNum
const nowLimit = queryParams.value.pageSize
return index + 1 + (nowPage - 1) * nowLimit
}
// 多选框选中数据
function handleSelectionChange(selection) {
ids.value = selection.map((item) => item.id)
single.value = selection.length !== 1
multiple.value = !selection.length
}
// 格式化日期
function formatDate(date) {
if (!date) return ''
// 处理数组格式 [2026, 3, 21]
if (Array.isArray(date)) {
return `${date[0]}-${String(date[1]).padStart(2, '0')}-${String(date[2]).padStart(2, '0')}`
}
// 处理字符串格式
if (typeof date === 'string') {
return date.substring(0, 10)
}
return ''
}
// 解析服药时间点 JSON
function parseTimeSlots(timeSlots) {
if (!timeSlots) return []
try {
return typeof timeSlots === 'string' ? JSON.parse(timeSlots) : timeSlots
} catch (e) {
return []
}
}
// 将数组日期转换为字符串日期
function convertArrayDateToString(dateArray) {
if (!dateArray) return null
if (Array.isArray(dateArray)) {
return `${dateArray[0]}-${String(dateArray[1]).padStart(2, '0')}-${String(dateArray[2]).padStart(2, '0')}`
}
if (typeof dateArray === 'string') {
return dateArray.substring(0, 10)
}
return null
}
// 查看
const handleView = (row) => {
getMedicationPlan(row.id).then((response) => {
form.value = response.data
// 转换日期格式
form.value.startDate = convertArrayDateToString(form.value.startDate)
form.value.endDate = convertArrayDateToString(form.value.endDate)
// 解析 timeSlots JSON
if (form.value.timeSlots) {
try {
form.value.timePointList = typeof form.value.timeSlots === 'string' ? JSON.parse(form.value.timeSlots) : form.value.timeSlots
} catch (e) {
form.value.timePointList = []
}
} else {
form.value.timePointList = []
}
// 解析 familyMemberIds JSON
if (form.value.familyMemberIds) {
try {
form.value.familyMemberIdList = typeof form.value.familyMemberIds === 'string' ? JSON.parse(form.value.familyMemberIds) : form.value.familyMemberIds
} catch (e) {
form.value.familyMemberIdList = []
}
} else {
form.value.familyMemberIdList = []
}
title.value = '查看用药计划'
open.value = true
// 加载该人员的慢性疾病列表
if (form.value.personId) {
listChronicDiseaseByMember(form.value.personId).then((res) => {
chronicDiseaseList.value = res.data || []
})
}
})
}
/** 新增按钮操作 */
function handleAdd() {
reset()
open.value = true
title.value = '添加用药计划'
}
function handleAddFromChronicDisease() {
const personId = route.query.personId ? Number(route.query.personId) : null
const chronicDiseaseId = route.query.chronicDiseaseId ? Number(route.query.chronicDiseaseId) : null
if (!personId || !chronicDiseaseId) {
return
}
reset()
form.value.personId = personId
form.value.chronicDiseaseId = chronicDiseaseId
form.value.planName = route.query.diseaseName ? `${route.query.diseaseName}用药计划` : null
queryParams.value.personId = personId
queryParams.value.chronicDiseaseId = chronicDiseaseId
listChronicDiseaseByMember(personId).then((res) => {
chronicDiseaseList.value = res.data || []
})
open.value = true
title.value = '添加用药计划'
getList()
}
/** 修改按钮操作 */
function handleUpdate(row) {
reset()
const _id = row.id || ids.value
getMedicationPlan(_id).then((response) => {
form.value = response.data
// 转换日期格式
form.value.startDate = convertArrayDateToString(form.value.startDate)
form.value.endDate = convertArrayDateToString(form.value.endDate)
// 解析 timeSlots JSON
if (form.value.timeSlots) {
try {
form.value.timePointList = typeof form.value.timeSlots === 'string' ? JSON.parse(form.value.timeSlots) : form.value.timeSlots
} catch (e) {
form.value.timePointList = []
}
} else {
form.value.timePointList = []
}
// 解析 familyMemberIds JSON
if (form.value.familyMemberIds) {
try {
form.value.familyMemberIdList = typeof form.value.familyMemberIds === 'string' ? JSON.parse(form.value.familyMemberIds) : form.value.familyMemberIds
} catch (e) {
form.value.familyMemberIdList = []
}
} else {
form.value.familyMemberIdList = []
}
open.value = true
title.value = '修改用药计划'
// 加载该人员的慢性疾病列表
if (form.value.personId) {
listChronicDiseaseByMember(form.value.personId).then((res) => {
chronicDiseaseList.value = res.data || []
})
}
})
}
/** 暂停按钮操作 */
function handlePause(row) {
proxy.$modal
.confirm('确认暂停该用药计划?')
.then(() => {
return pauseMedicationPlan(row.id)
})
.then((response) => {
getList()
proxy.$modal.msgSuccess('暂停成功')
})
.catch(() => {})
}
/** 恢复按钮操作 */
function handleResume(row) {
resumeMedicationPlan(row.id).then(() => {
getList()
proxy.$modal.msgSuccess('恢复成功')
})
}
/** 结束按钮操作 */
function handleEnd(row) {
proxy.$modal
.confirm('确认结束该用药计划?结束后将不再生成用药记录。')
.then(() => {
return endMedicationPlan(row.id)
})
.then(() => {
getList()
proxy.$modal.msgSuccess('已结束')
})
.catch(() => {})
}
/** 提交按钮 */
function submitForm() {
proxy.$refs.planRef.validate((valid) => {
if (valid) {
// 构建提交数据,排除后端自动生成的字段
const submitData = { ...form.value }
// 将时间点列表转为 JSON 字符串
submitData.timeSlots = JSON.stringify(form.value.timePointList)
// 将家属成员ID列表转为 JSON 字符串
if (form.value.familyMemberIdList && form.value.familyMemberIdList.length > 0) {
submitData.familyMemberIds = JSON.stringify(form.value.familyMemberIdList)
} else {
submitData.familyMemberIds = null
}
// 根据药品ID获取药品名称和规格
const medicine = medicineList.value.find((m) => m.id === submitData.medicineId)
if (medicine) {
submitData.medicineName = medicine.shortName + '-' + medicine.brand
submitData.specification = medicine.specification || ''
}
// 移除后端自动管理的字段
delete submitData.createTime
delete submitData.updateTime
delete submitData.familyMemberIdList
if (submitData.id != null) {
updateMedicationPlan(submitData).then((response) => {
proxy.$modal.msgSuccess('修改成功')
open.value = false
getList()
})
} else {
addMedicationPlan(submitData).then((response) => {
proxy.$modal.msgSuccess('新增成功')
open.value = false
getList()
})
}
}
})
}
/** 删除按钮操作 */
function handleDelete(row) {
const _ids = row.id || ids.value
proxy.$modal
.confirm('是否确认删除选中的数据项?')
.then(function () {
return delMedicationPlan(_ids)
})
.then(() => {
getList()
proxy.$modal.msgSuccess('删除成功')
})
.catch(() => {})
}
/** 添加服药时间点 */
function addTimePoint(val) {
if (val && !form.value.timePointList.includes(val)) {
form.value.timePointList.push(val)
}
newTimePoint.value = null
}
/** 移除服药时间点 */
function removeTimePoint(index) {
form.value.timePointList.splice(index, 1)
}
/** 药品选择变化 */
function handleMedicineChange(medicineId) {
const medicine = medicineList.value.find((m) => m.id === medicineId)
if (medicine) {
form.value.dosageUnit = medicine.unit || '片'
}
}
/** 人员选择变化 */
function handlePersonChange(personId) {
form.value.chronicDiseaseId = null
chronicDiseaseList.value = []
if (personId) {
listChronicDiseaseByMember(personId).then((response) => {
chronicDiseaseList.value = response.data || []
})
}
}
getPersonList()
getMedicineList()
getList()
handleAddFromChronicDisease()
</script>
<style scoped>
.time-slots-wrapper {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
padding: 10px 12px;
background: #fafbfc;
border: 1px solid #e4e7ed;
border-radius: 6px;
min-height: 44px;
}
.time-slot-item {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 4px 10px;
background: #fff;
border: 1px solid #dcdfe6;
border-radius: 4px;
color: #606266;
font-size: 13px;
transition: all 0.2s ease;
}
.time-slot-item:hover {
border-color: #409eff;
color: #409eff;
}
.time-text {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
font-weight: 500;
}
.close-icon {
cursor: pointer;
font-size: 12px;
color: #c0c4cc;
transition: all 0.2s;
margin-left: 2px;
}
.close-icon:hover {
color: #f56c6c;
}
.time-slots-list {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 4px;
}
.time-tag {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
}
</style>

View File

@@ -0,0 +1,555 @@
<template>
<div class="app-container">
<!-- 查询条件 -->
<div class="search-con">
<div class="title">查询条件</div>
<el-form :model="queryParams" ref="queryRef" :inline="true" label-width="80px">
<el-form-item label="人员" prop="personId">
<el-select v-model="queryParams.personId" placeholder="请选择人员" clearable filterable>
<el-option v-for="item in personList" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item label="统计周期">
<el-date-picker
v-model="dateRange"
type="daterange"
range-separator="-"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
style="width: 240px"
/>
</el-form-item>
</el-form>
<div class="search-btn-con">
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button type="info" icon="Refresh" @click="resetQuery">重置</el-button>
</div>
</div>
<!-- 概览卡片 -->
<div class="overview-cards" v-loading="loading">
<div class="stat-card">
<div class="stat-icon" style="background: #409eff">
<el-icon><Calendar /></el-icon>
</div>
<div class="stat-content">
<div class="stat-value">{{ overview.totalTasks || 0 }}</div>
<div class="stat-label">总任务数</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon" style="background: #67c23a">
<el-icon><Check /></el-icon>
</div>
<div class="stat-content">
<div class="stat-value">{{ overview.completedTasks || 0 }}</div>
<div class="stat-label">已服药</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon" style="background: #f56c6c">
<el-icon><Close /></el-icon>
</div>
<div class="stat-content">
<div class="stat-value">{{ overview.missedTasks || 0 }}</div>
<div class="stat-label">漏服</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon" style="background: #e6a23c">
<el-icon><Timer /></el-icon>
</div>
<div class="stat-content">
<div class="stat-value">{{ overview.adherenceRate || 0 }}%</div>
<div class="stat-label">服药率</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon" style="background: #909399">
<el-icon><Clock /></el-icon>
</div>
<div class="stat-content">
<div class="stat-value">{{ overview.ontimeRate || 0 }}%</div>
<div class="stat-label">准时率</div>
</div>
</div>
</div>
<!-- 图表区域 -->
<div class="charts-row">
<!-- 趋势图 -->
<div class="chart-card">
<div class="chart-title">服药趋势最近7天</div>
<div ref="trendChartRef" class="chart-container"></div>
</div>
<!-- 时段统计 -->
<div class="chart-card">
<div class="chart-title">时段分布</div>
<div ref="timeSlotChartRef" class="chart-container"></div>
</div>
</div>
<!-- 日历视图 -->
<div class="calendar-section">
<div class="calendar-header">
<span class="calendar-title">日历视图</span>
<div class="calendar-nav">
<el-button-group>
<el-button @click="changeMonth(-1)" icon="ArrowLeft">上月</el-button>
<el-button>{{ currentMonth }}</el-button>
<el-button @click="changeMonth(1)"
>下月<el-icon class="el-icon--right"><ArrowRight /></el-icon
></el-button>
</el-button-group>
</div>
</div>
<div class="calendar-grid" v-loading="calendarLoading">
<div class="calendar-weekdays">
<div v-for="day in weekdays" :key="day" class="weekday">{{ day }}</div>
</div>
<div class="calendar-days">
<div v-for="(day, index) in calendarDays" :key="index" class="calendar-day" :class="{ 'other-month': day.otherMonth, today: day.isToday }">
<div class="day-number">{{ day.day }}</div>
<div class="day-stats" v-if="day.data && day.data.totalTasks > 0">
<span class="stat-dot completed" :title="`已服药: ${day.data.completedTasks}`"></span>
<span class="stat-dot missed" v-if="day.data.missedTasks > 0" :title="`漏服: ${day.data.missedTasks}`"></span>
<span class="stat-rate">{{ day.data.adherenceRate }}%</span>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup name="MedicationStatistic">
import { ref, reactive, onMounted, watch, computed, nextTick } from 'vue'
import { getOverallAdherence, getDailyAdherence, getCalendarData, getTimeSlotAdherence } from '@/api/health/medicationAdherence'
import { listPerson } from '@/api/health/person'
import * as echarts from 'echarts'
const { proxy } = getCurrentInstance()
const loading = ref(false)
const calendarLoading = ref(false)
const personList = ref([])
const queryParams = reactive({
personId: null
})
const dateRange = ref([])
const overview = ref({})
// 日历相关
const currentMonth = ref('')
const currentYearMonth = ref('')
const calendarData = ref([])
const weekdays = ['一', '二', '三', '四', '五', '六', '日']
// 图表
const trendChartRef = ref(null)
const timeSlotChartRef = ref(null)
let trendChart = null
let timeSlotChart = null
// 计算日历天数
const calendarDays = computed(() => {
const days = []
if (!currentYearMonth.value) return days
const [year, month] = currentYearMonth.value.split('-').map(Number)
const firstDay = new Date(year, month - 1, 1)
const lastDay = new Date(year, month, 0)
const startWeekday = (firstDay.getDay() + 6) % 7 // 周一为0
// 上月填充
const prevMonthLastDay = new Date(year, month - 1, 0).getDate()
for (let i = startWeekday - 1; i >= 0; i--) {
days.push({
day: prevMonthLastDay - i,
otherMonth: true,
isToday: false,
data: null
})
}
// 当月
const today = new Date()
const todayStr = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`
const dataMap = {}
calendarData.value.forEach((d) => {
dataMap[d.date] = d
})
for (let i = 1; i <= lastDay.getDate(); i++) {
const dateStr = `${year}-${String(month).padStart(2, '0')}-${String(i).padStart(2, '0')}`
days.push({
day: i,
otherMonth: false,
isToday: dateStr === todayStr,
data: dataMap[dateStr] || null
})
}
// 下月填充
const remaining = 42 - days.length
for (let i = 1; i <= remaining; i++) {
days.push({
day: i,
otherMonth: true,
isToday: false,
data: null
})
}
return days
})
// 获取人员列表
const getPersonList = async () => {
try {
const res = await listPerson({})
personList.value = res.rows || []
} catch (e) {
console.error(e)
}
}
// 查询统计数据
const getStatistics = async () => {
loading.value = true
try {
const params = { ...queryParams }
if (dateRange.value && dateRange.value.length === 2) {
params.startDate = dateRange.value[0]
params.endDate = dateRange.value[1]
} else {
// 默认最近7天
const today = new Date()
const weekAgo = new Date(today.getTime() - 7 * 24 * 60 * 60 * 1000)
params.startDate = weekAgo.toISOString().split('T')[0]
params.endDate = today.toISOString().split('T')[0]
}
// 获取总体统计
const overviewRes = await getOverallAdherence(params)
overview.value = overviewRes.data || {}
// 获取每日趋势
const dailyRes = await getDailyAdherence(params)
renderTrendChart(dailyRes.data || [])
// 获取时段统计
const timeSlotRes = await getTimeSlotAdherence(params)
renderTimeSlotChart(timeSlotRes.data || {})
} catch (e) {
console.error(e)
} finally {
loading.value = false
}
}
// 渲染趋势图
const renderTrendChart = (data) => {
nextTick(() => {
if (!trendChartRef.value) return
if (!trendChart) {
trendChart = echarts.init(trendChartRef.value)
}
const option = {
tooltip: {
trigger: 'axis'
},
legend: {
data: ['服药率', '准时率']
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'category',
data: data.map((d) => d.date?.substring(5) || '')
},
yAxis: {
type: 'value',
max: 100
},
series: [
{
name: '服药率',
type: 'line',
data: data.map((d) => d.adherenceRate || 0),
smooth: true,
itemStyle: { color: '#409EFF' }
},
{
name: '准时率',
type: 'line',
data: data.map((d) => d.ontimeRate || 0),
smooth: true,
itemStyle: { color: '#67C23A' }
}
]
}
trendChart.setOption(option)
})
}
// 渲染时段统计图
const renderTimeSlotChart = (data) => {
nextTick(() => {
if (!timeSlotChartRef.value) return
if (!timeSlotChart) {
timeSlotChart = echarts.init(timeSlotChartRef.value)
}
const slotNames = {
morning: '上午(6-12)',
afternoon: '下午(12-18)',
evening: '晚间(18-24)',
night: '凌晨(0-6)'
}
const option = {
tooltip: {
trigger: 'item',
formatter: '{b}: {c}%'
},
series: [
{
type: 'pie',
radius: ['40%', '70%'],
data: Object.entries(data).map(([key, value]) => ({
name: slotNames[key] || key,
value: value.adherenceRate || 0
})),
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
}
]
}
timeSlotChart.setOption(option)
})
}
// 获取日历数据
const getCalendar = async () => {
calendarLoading.value = true
try {
const res = await getCalendarData(queryParams.personId, currentYearMonth.value)
calendarData.value = res.data || []
} catch (e) {
console.error(e)
} finally {
calendarLoading.value = false
}
}
// 切换月份
const changeMonth = (delta) => {
const [year, month] = currentYearMonth.value.split('-').map(Number)
const newDate = new Date(year, month - 1 + delta, 1)
currentYearMonth.value = `${newDate.getFullYear()}-${String(newDate.getMonth() + 1).padStart(2, '0')}`
currentMonth.value = `${newDate.getFullYear()}${newDate.getMonth() + 1}`
getCalendar()
}
const handleQuery = () => {
getStatistics()
getCalendar()
}
const resetQuery = () => {
queryParams.personId = null
dateRange.value = []
handleQuery()
}
onMounted(() => {
// 初始化日期
const today = new Date()
currentYearMonth.value = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}`
currentMonth.value = `${today.getFullYear()}${today.getMonth() + 1}`
getPersonList()
getStatistics()
getCalendar()
})
</script>
<style scoped lang="scss">
.app-container {
height: calc(100vh - 84px);
overflow-y: auto;
}
.overview-cards {
display: flex;
gap: 16px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.stat-card {
display: flex;
align-items: center;
background: #fff;
border-radius: 8px;
padding: 20px;
min-width: 180px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
.stat-icon {
width: 50px;
height: 50px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
color: #fff;
margin-right: 16px;
}
.stat-content {
.stat-value {
font-size: 28px;
font-weight: bold;
color: #303133;
}
.stat-label {
font-size: 14px;
color: #909399;
margin-top: 4px;
}
}
}
.charts-row {
display: flex;
gap: 16px;
margin-bottom: 20px;
}
.chart-card {
flex: 1;
background: #fff;
border-radius: 8px;
padding: 16px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
.chart-title {
font-size: 16px;
font-weight: bold;
margin-bottom: 12px;
color: #303133;
}
.chart-container {
height: 300px;
}
}
.calendar-section {
background: #fff;
border-radius: 8px;
padding: 16px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
.calendar-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
.calendar-title {
font-size: 16px;
font-weight: bold;
color: #303133;
}
}
.calendar-grid {
.calendar-weekdays {
display: grid;
grid-template-columns: repeat(7, 1fr);
text-align: center;
margin-bottom: 8px;
.weekday {
padding: 8px;
color: #909399;
font-weight: bold;
}
}
.calendar-days {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 4px;
.calendar-day {
min-height: 80px;
border: 1px solid #ebeef5;
border-radius: 4px;
padding: 4px;
&.other-month {
background: #f5f7fa;
.day-number {
color: #c0c4cc;
}
}
&.today {
border-color: #409eff;
.day-number {
color: #409eff;
font-weight: bold;
}
}
.day-number {
font-size: 14px;
color: #303133;
}
.day-stats {
margin-top: 4px;
display: flex;
align-items: center;
gap: 4px;
.stat-dot {
width: 8px;
height: 8px;
border-radius: 50%;
&.completed {
background: #67c23a;
}
&.missed {
background: #f56c6c;
}
}
.stat-rate {
font-size: 12px;
color: #606266;
}
}
}
}
}
}
</style>

View File

@@ -0,0 +1,298 @@
<template>
<div class="app-container">
<div class="search-con">
<div class="title">查询条件</div>
<el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="100px">
<el-form-item label="人员" prop="memberId">
<el-select v-model="queryParams.memberId" placeholder="请选择人员" clearable filterable>
<el-option v-for="item in memberList" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item label="计划" prop="planId">
<el-select v-model="queryParams.planId" placeholder="请选择计划" clearable filterable style="width: 250px">
<el-option v-for="item in planList" :key="item.id" :label="item.planName" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="请选择状态" clearable>
<el-option v-for="dict in medication_task_status" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
<el-form-item label="计划日期">
<el-date-picker
v-model="dateRange"
type="daterange"
range-separator="-"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
style="width: 240px"
/>
</el-form-item>
</el-form>
<div class="search-btn-con">
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button type="info" icon="Refresh" @click="resetQuery">重置</el-button>
</div>
</div>
<div class="main-con">
<div class="title-con">
<div class="title">服药任务</div>
<div class="operate-btn-con">
<el-button :disabled="multiple" icon="Delete" @click="handleDelete" v-hasPermi="['health:medicationTask:remove']">删除</el-button>
</div>
</div>
<div class="content-con" v-loading="loading">
<el-table v-loading="loading" :data="taskList" @selection-change="handleSelectionChange" height="calc(100% - 0.65rem)">
<el-table-column type="selection" width="55" align="center" />
<el-table-column type="index" label="序号" :index="indexMethod" width="50"></el-table-column>
<el-table-column label="计划名称" align="center" prop="planName" width="150" />
<el-table-column label="人员" align="center" prop="personName" width="80" />
<el-table-column label="药品" align="center" prop="medicineName" width="180" />
<el-table-column label="计划日期" align="center" prop="plannedDate" width="110">
<template #default="scope">
{{ formatDate(scope.row.plannedDate) }}
</template>
</el-table-column>
<el-table-column label="计划时间" align="center" prop="plannedTime" width="100">
<template #default="scope">
{{ formatTime(scope.row.plannedTime) }}
</template>
</el-table-column>
<el-table-column label="实际时间" align="center" width="160">
<template #default="scope">
{{ scope.row.actualTime ? formatDateTime(scope.row.actualTime) : '--' }}
</template>
</el-table-column>
<el-table-column label="剂量" align="center" width="100">
<template #default="scope">
{{ scope.row.plannedDosage }}
</template>
</el-table-column>
<el-table-column label="状态" align="center" prop="status" width="90">
<template #default="scope">
<dict-tag :options="medication_task_status" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column label="备注" align="center" prop="notes" show-overflow-tooltip />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template v-slot="scope">
<div class="ctrl-btn d-flex">
<el-tooltip v-if="scope.row.status === 0" class="item" effect="dark" content="确认服药" placement="top">
<el-button type="success" icon="Check" v-hasPermi="['health:medicationTask:edit']" circle @click="handleConfirm(scope.row)"></el-button>
</el-tooltip>
<el-tooltip v-if="scope.row.status === 0" class="item" effect="dark" content="跳过" placement="top">
<el-button type="warning" icon="Close" v-hasPermi="['health:medicationTask:edit']" circle @click="handleSkip(scope.row)"></el-button>
</el-tooltip>
<el-tooltip class="item" effect="dark" content="删除" placement="top">
<el-button icon="Delete" v-hasPermi="['health:medicationTask:remove']" circle @click="handleDelete(scope.row)"></el-button>
</el-tooltip>
</div>
</template>
</el-table-column>
</el-table>
<el-pagination
small
background
layout="total,sizes, prev, pager, next"
:page-sizes="[10, 20, 50]"
:total="total"
v-model:current-page="queryParams.pageNum"
v-model:page-size="queryParams.pageSize"
@size-change="getList"
@current-change="getList"
/>
</div>
</div>
<!-- 确认服药对话框 -->
<el-dialog title="确认服药" v-model="confirmDialogVisible" width="400px">
<el-form>
<el-form-item label="实际服药时间">
<el-time-picker v-model="actualTime" placeholder="选择实际服药时间" format="HH:mm" value-format="HH:mm" style="width: 100%" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="confirmDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitConfirm">确定</el-button>
</template>
</el-dialog>
<!-- 跳过对话框 -->
<el-dialog title="跳过服药" v-model="skipDialogVisible" width="400px">
<el-form>
<el-form-item label="跳过原因">
<el-input v-model="skipNotes" type="textarea" placeholder="请输入跳过原因(选填)" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="skipDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitSkip">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup name="MedicationTask">
import { listMedicationTask, delMedicationTask, confirmMedication, skipMedication } from '@/api/health/medicationTask'
import { listMedicationPlan } from '@/api/health/medicationPlan'
import { listPerson } from '@/api/health/person'
const { proxy } = getCurrentInstance()
const { medication_task_status } = proxy.useDict('medication_task_status')
const taskList = ref([])
const planList = ref([])
const memberList = ref([])
const loading = ref(false)
const multiple = ref(true)
const total = ref(0)
const dateRange = ref([])
const ids = ref([])
const queryParams = reactive({
pageNum: 1,
pageSize: 10,
memberId: null,
planId: null,
status: null,
plannedDateBegin: null,
plannedDateEnd: null
})
const confirmDialogVisible = ref(false)
const actualTime = ref('')
const skipDialogVisible = ref(false)
const skipNotes = ref('')
const currentRow = ref(null)
const getList = async () => {
loading.value = true
if (dateRange.value && dateRange.value.length === 2) {
queryParams.plannedDateBegin = dateRange.value[0]
queryParams.plannedDateEnd = dateRange.value[1]
} else {
queryParams.plannedDateBegin = null
queryParams.plannedDateEnd = null
}
try {
const res = await listMedicationTask(queryParams)
taskList.value = res.rows || []
total.value = res.total || 0
} finally {
loading.value = false
}
}
const getPlanList = async () => {
const res = await listMedicationPlan({})
planList.value = res.rows || []
}
const getMemberList = async () => {
const res = await listPerson({})
memberList.value = res.rows || []
}
const handleQuery = () => {
queryParams.pageNum = 1
getList()
}
const resetQuery = () => {
dateRange.value = []
queryParams.memberId = null
queryParams.planId = null
queryParams.status = null
handleQuery()
}
const handleSelectionChange = (selection) => {
ids.value = selection.map((item) => item.id)
multiple.value = !selection.length
}
const handleConfirm = (row) => {
currentRow.value = row
// 默认当前时间
const now = new Date()
actualTime.value = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`
confirmDialogVisible.value = true
}
const submitConfirm = async () => {
if (!actualTime.value) {
proxy.$modal.msgError('请选择实际服药时间')
return
}
// confirmType=1 表示确认服药confirmTime 传递时间字符串
await confirmMedication(currentRow.value.id, 1, actualTime.value)
proxy.$modal.msgSuccess('操作成功')
confirmDialogVisible.value = false
getList()
}
const handleSkip = (row) => {
currentRow.value = row
skipNotes.value = ''
skipDialogVisible.value = true
}
const submitSkip = async () => {
await skipMedication(currentRow.value.id, skipNotes.value)
proxy.$modal.msgSuccess('操作成功')
skipDialogVisible.value = false
getList()
}
const handleDelete = async (row) => {
const idList = row.id ? [row.id] : ids.value
await proxy.$modal.confirm('确认删除选中的记录?')
await delMedicationTask(idList.join(','))
proxy.$modal.msgSuccess('删除成功')
getList()
}
const indexMethod = (index) => {
return (queryParams.pageNum - 1) * queryParams.pageSize + index + 1
}
const formatDate = (date) => {
if (!date) return '--'
// 处理数组格式 [2026, 3, 21]
if (Array.isArray(date)) {
return `${date[0]}-${String(date[1]).padStart(2, '0')}-${String(date[2]).padStart(2, '0')}`
}
// 处理字符串格式
if (typeof date === 'string') {
return date.substring(0, 10)
}
return '--'
}
const formatTime = (time) => {
if (!time) return '--'
// 处理数组格式 [8, 0] 或 [14, 30]
if (Array.isArray(time)) {
return `${String(time[0]).padStart(2, '0')}:${String(time[1]).padStart(2, '0')}`
}
// 处理字符串格式
if (typeof time === 'string') {
return time.substring(0, 5)
}
return '--'
}
const formatDateTime = (datetime) => {
if (!datetime) return '--'
return datetime.replace('T', ' ').substring(0, 16)
}
onMounted(() => {
getList()
getPlanList()
getMemberList()
})
</script>
<style scoped lang="scss"></style>

View File

@@ -79,6 +79,28 @@
<el-table-column label="包装" align="center" prop="packaging" width="150" />
<el-table-column label="品牌" align="center" prop="brand" width="120" />
<el-table-column label="生产厂家" align="center" prop="manufacturers" />
<el-table-column label="附件" align="center" prop="attachment" width="120">
<template #default="scope">
<el-popover v-if="parseAttachments(scope.row.attachment).length" placement="left" width="280" trigger="click">
<template #reference>
<el-button link type="primary">查看{{ parseAttachments(scope.row.attachment).length }}</el-button>
</template>
<div class="attachment-popover-list">
<el-link
v-for="(file, index) in parseAttachments(scope.row.attachment)"
:key="file"
type="primary"
:href="getAttachmentUrl(file)"
target="_blank"
:underline="false"
>
{{ getAttachmentName(file, index) }}
</el-link>
</div>
</el-popover>
<span v-else class="attachment-empty"></span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template v-slot="scope">
@@ -190,6 +212,22 @@
<el-form-item label="不良反应" prop="adverseReaction" style="width: 1200px">
<el-input v-model="form.adverseReaction" type="textarea" placeholder="请输入不良反应" />
</el-form-item>
<el-form-item label="附件" style="width: 1200px" prop="attachment">
<div v-if="title === '查看药品基础信息'" class="attachment-readonly-list attachment-form-list">
<el-link
v-for="(file, index) in parseAttachments(form.attachment)"
:key="file"
type="primary"
:href="getAttachmentUrl(file)"
target="_blank"
:underline="false"
>
{{ getAttachmentName(file, index) }}
</el-link>
<span v-if="!parseAttachments(form.attachment).length" class="attachment-empty">暂无附件</span>
</div>
<file-upload v-else v-model="form.attachment" :limit="9" :file-size="10" :file-type="attachmentFileTypes" />
</el-form-item>
<el-form-item label="备注" style="width: 1200px" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
</el-form-item>
@@ -229,6 +267,7 @@ const single = ref(true)
const multiple = ref(true)
const total = ref(0)
const title = ref('')
const attachmentFileTypes = ['jpg', 'jpeg', 'png', 'pdf', 'doc', 'docx', 'xls', 'xlsx']
const operateList = ref([
{ id: 'view', icon: 'View', title: '查看', hasPermi: ['health:medicineBasic:query'] },
{ id: 'edit', icon: 'Edit', title: '修改', hasPermi: ['health:medicineBasic:edit'] },
@@ -289,6 +328,39 @@ const handleOperate = (operate, row) => {
const { queryParams, form, rules } = toRefs(data)
function parseAttachments(attachment) {
if (!attachment) {
return []
}
return String(attachment)
.split(',')
.map((item) => item.trim())
.filter(Boolean)
}
function getAttachmentUrl(file) {
if (!file) {
return ''
}
if (/^(https?:)?\/\//.test(file) || file.startsWith('/') || file.startsWith('blob:') || file.startsWith('data:')) {
return file
}
return `/${file}`
}
function getAttachmentName(file, index) {
const purePath = String(file || '').split('?')[0]
const name = purePath.slice(purePath.lastIndexOf('/') + 1)
if (!name) {
return `附件${index + 1}`
}
try {
return decodeURIComponent(name)
} catch {
return name
}
}
/** 查询药品基础信息列表 */
function getList() {
loading.value = true
@@ -347,7 +419,8 @@ function reset() {
contentUnit: null,
character: null,
storage: null,
indications: null
indications: null,
attachment: null
}
proxy.resetForm('medicineBasicRef')
}
@@ -463,3 +536,23 @@ function handleExport() {
getList()
</script>
<style scoped lang="scss">
.attachment-popover-list,
.attachment-readonly-list {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 8px;
line-height: 1.4;
}
.attachment-form-list {
min-height: 32px;
padding: 4px 0;
}
.attachment-empty {
color: #909399;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,121 @@
<template>
<h1>首页</h1>
<section class="welcome-page">
<div class="welcome-content">
<p class="eyebrow">WELCOME</p>
<h1>欢迎使用智聪综合管理平台</h1>
<p class="subtitle">让数据管理更清晰让日常协作更高效</p>
<div class="welcome-notes">
<span>统一入口</span>
<span>高效管理</span>
<span>稳定运行</span>
</div>
</div>
</section>
</template>
<script setup name="Index"></script>
<style scoped lang="scss"></style>
<style scoped lang="scss">
.welcome-page {
position: relative;
min-height: calc(100vh - 84px);
padding: 64px;
overflow: hidden;
background: linear-gradient(90deg, rgba(8, 24, 54, 0.94), rgba(10, 47, 92, 0.82) 46%, rgba(15, 82, 128, 0.54)),
linear-gradient(135deg, rgba(41, 171, 226, 0.28) 0 1px, transparent 1px 42px), linear-gradient(45deg, rgba(255, 255, 255, 0.08) 0 1px, transparent 1px 58px),
radial-gradient(circle at 82% 18%, rgba(81, 198, 255, 0.42), transparent 28%), radial-gradient(circle at 72% 72%, rgba(76, 220, 178, 0.22), transparent 34%),
#071832;
color: #fff;
}
.welcome-page::before {
content: '';
position: absolute;
right: 8%;
top: 18%;
width: 420px;
height: 420px;
border: 1px solid rgba(255, 255, 255, 0.16);
border-radius: 50%;
box-shadow: inset 0 0 0 42px rgba(255, 255, 255, 0.03), inset 0 0 0 108px rgba(255, 255, 255, 0.025), 0 0 48px rgba(44, 180, 255, 0.18);
}
.welcome-page::after {
content: '';
position: absolute;
right: 12%;
bottom: 14%;
width: 34%;
max-width: 520px;
height: 2px;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.52), transparent);
transform: rotate(-18deg);
}
.welcome-content {
position: relative;
z-index: 1;
max-width: 720px;
padding-top: 9vh;
}
.eyebrow {
margin: 0 0 18px;
color: rgba(255, 255, 255, 0.72);
font-size: 14px;
font-weight: 700;
letter-spacing: 0.18em;
}
h1 {
margin: 0;
font-size: 44px;
font-weight: 800;
line-height: 1.22;
letter-spacing: 0;
}
.subtitle {
max-width: 520px;
margin: 22px 0 0;
color: rgba(255, 255, 255, 0.78);
font-size: 18px;
line-height: 1.8;
}
.welcome-notes {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-top: 34px;
span {
padding: 10px 18px;
color: rgba(255, 255, 255, 0.88);
background: rgba(255, 255, 255, 0.12);
border: 1px solid rgba(255, 255, 255, 0.22);
border-radius: 6px;
backdrop-filter: blur(8px);
font-size: 14px;
}
}
@media (max-width: 768px) {
.welcome-page {
min-height: calc(100vh - 50px);
padding: 36px 22px;
}
.welcome-content {
padding-top: 7vh;
}
h1 {
font-size: 30px;
}
.subtitle {
font-size: 15px;
}
}
</style>

View File

@@ -1722,12 +1722,15 @@ const loadAllData = async () => {
weeklyExpenseData.value = weekDates.map((date) => ({ date, value: 0 }))
}
// 处理近一周投资收益数据(使用 tableAccountsList 字段)
// 处理近一周投资收益数据(使用 tableAccountsList 字段,倒序后按时间正序展示
if (weeklyInvestRes && weeklyInvestRes.data && weeklyInvestRes.data.tableAccountsList) {
weeklyInvestIncomeData.value = weeklyInvestRes.data.tableAccountsList.map((item) => ({
date: item.time.substring(5), // 只显示 MM-DD
value: Number(item.value) || 0
}))
weeklyInvestIncomeData.value = weeklyInvestRes.data.tableAccountsList
.slice()
.reverse()
.map((item) => ({
date: item.time.substring(5), // 只显示 MM-DD
value: Number(item.value) || 0
}))
} else {
weeklyInvestIncomeData.value = weekDates.map((date) => ({ date, value: 0 }))
}
@@ -1784,12 +1787,15 @@ const loadAllData = async () => {
monthlyExpenseData.value = []
}
// 处理近一月投资收益数据(使用 tableAccountsList 字段)
// 处理近一月投资收益数据(使用 tableAccountsList 字段,倒序后按时间正序展示
if (monthlyInvestRes && monthlyInvestRes.data && monthlyInvestRes.data.tableAccountsList) {
monthlyInvestIncomeData.value = monthlyInvestRes.data.tableAccountsList.map((item) => ({
date: item.time.substring(5), // 只显示 MM-DD
value: Number(item.value) || 0
}))
monthlyInvestIncomeData.value = monthlyInvestRes.data.tableAccountsList
.slice()
.reverse()
.map((item) => ({
date: item.time.substring(5), // 只显示 MM-DD
value: Number(item.value) || 0
}))
} else {
monthlyInvestIncomeData.value = []
}

File diff suppressed because it is too large Load Diff

View File

@@ -149,20 +149,21 @@
</el-form-item>
</el-form>
<el-table v-loading="loadingDealRecord" :data="tableDealRecordData">
<el-table-column label="交易时间" align="center" prop="createTime" />
<el-table-column label="交易时间" align="center" width="180" prop="createTime" />
<el-table-column label="交易金额" align="center" prop="amount" />
<el-table-column label="当前余额" align="center" prop="currentBalance" />
<el-table-column label="交易类型" align="center" prop="dealType">
<el-table-column label="交易类型" align="center" width="120" prop="dealType">
<template #default="scope">
<dict-tag :options="deal_type" :value="scope.row.dealType" />
</template>
</el-table-column>
<el-table-column label="交易类别" align="center" prop="dealCategory">
<el-table-column label="交易类别" align="center" width="150" prop="dealCategory">
<template #default="scope">
<dict-tag :options="deal_category" :value="scope.row.dealCategory" />
</template>
</el-table-column>
<el-table-column label="备注" align="center" width="380" prop="remark" />
<el-table-column label="交易子类别" align="center" width="150" prop="childCategoryName" />
<el-table-column label="备注" align="center" width="300" prop="remark" />
</el-table>
<el-pagination small background layout="total, prev, pager, next" :total="dealRecordTotal" @current-change="handleCurrentDealRecordChange" />
</el-dialog>
@@ -266,7 +267,7 @@ const operateList = ref([
{ id: 'view', icon: 'View', title: '查看', hasPermi: ['invest:accounts:query'] },
{ id: 'edit', icon: 'Edit', title: '修改', hasPermi: ['invest:accounts:edit'] },
// { id: 'delete', icon: 'Delete', title: '删除', hasPermi: ['invest:accounts:remove'] },
{ id: 'dealRecord', icon: 'Tickets', title: '交易记录', hasPermi: ['invest:accounts:query'] },
{ id: 'dealRecord', icon: 'Tickets', title: '交易明细', hasPermi: ['invest:accounts:query'] },
{ id: 'transDeal', icon: 'Cellphone', title: '转账记录', hasPermi: ['invest:accounts:query'] }
])
const data = reactive({
@@ -465,13 +466,13 @@ function handleExport() {
)
}
/** 历史数据按钮操作 */
/** 交易明细按钮操作 */
function handleDealRecord(row) {
const _id = row.id || ids.value
queryDealRecordParams.value.time = ''
queryDealRecordParams.value.dealType = ''
queryDealRecordParams.value.dealCategory = ''
titleDealRecord.value = row.nameCode + '历史交易记录'
titleDealRecord.value = `${row.name}${row.code ? '【' + row.code + '】' : ''}交易明细`
currentAccountId.value = _id
getDealRecordList(1)
openDealRecord.value = true

View File

@@ -73,7 +73,7 @@
<dict-tag :options="account_status" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column label="操作" width="165" align="center" class-name="small-padding fixed-width">
<el-table-column label="操作" width="210" align="center" class-name="small-padding fixed-width">
<template v-slot="scope">
<div class="ctrl-btn d-flex">
<el-tooltip v-for="item in operateList" :key="item.id" class="item" effect="dark" :content="item.title" placement="top">
@@ -265,6 +265,46 @@
</div>
<template #footer> </template>
</el-dialog>
<el-dialog :title="titleDealRecord" v-model="openDealRecord" width="1200px" append-to-body destroy-on-close>
<el-form :model="queryDealRecordParams" ref="queryDealRecordRef" inline>
<el-form-item label="交易时间" prop="time">
<el-date-picker v-model="queryDealRecordParams.time" type="daterange" range-separator="至" start-placeholder="开始时间" end-placeholder="结束时间" />
</el-form-item>
<el-form-item label="交易类型" style="width: 220px" prop="dealType">
<el-select v-model="queryDealRecordParams.dealType" placeholder="请选择交易类型" clearable>
<el-option v-for="dict in deal_type" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
<el-form-item label="交易类别" style="width: 220px" prop="dealCategory">
<el-select v-model="queryDealRecordParams.dealCategory" placeholder="请选择交易类别" clearable>
<el-option v-for="dict in deal_category" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
<el-form-item label="">
<el-button type="primary" icon="Search" @click="handleDealRecordQuery">搜索</el-button>
<el-button type="info" icon="Refresh" @click="resetDealRecordQuery">重置</el-button>
</el-form-item>
</el-form>
<el-table v-loading="loadingDealRecord" :data="tableDealRecordData">
<el-table-column label="交易时间" align="center" prop="createTime" width="180" />
<el-table-column label="交易金额" align="center" prop="amount" />
<el-table-column label="当前余额" align="center" prop="currentBalance" />
<el-table-column label="交易类型" width="120" align="center" prop="dealType">
<template #default="scope">
<dict-tag :options="deal_type" :value="scope.row.dealType" />
</template>
</el-table-column>
<el-table-column label="交易类别" width="150" align="center" prop="dealCategory">
<template #default="scope">
<dict-tag :options="deal_category" :value="scope.row.dealCategory" />
</template>
</el-table-column>
<el-table-column label="交易子类别" width="150" align="center" prop="childCategoryName" />
<el-table-column label="备注" align="center" width="300" prop="remark" />
</el-table>
<el-pagination small background layout="total, prev, pager, next" :total="dealRecordTotal" @current-change="handleCurrentDealRecordChange" />
</el-dialog>
</div>
</template>
@@ -272,11 +312,14 @@
import { listBankcardLend, getBankcardLend, delBankcardLend, addBankcardLend, updateBankcardLend } from '@/api/invest/bankcardlend'
import { listLimitHistory, getLimitHistory, delLimitHistory, addLimitHistory, updateLimitHistory } from '@/api/invest/limitHistory'
import { listBankBaseInfo } from '@/api/invest/bankBaseInfo'
import { listAccountDealRecord } from '@/api/invest/accountDealRecord'
import dayjs from 'dayjs'
// eslint-disable-next-line no-unused-vars
import { require } from '@/utils/require'
const { proxy } = getCurrentInstance()
const { is_zero_bill, is_next_bill_date, account_status, card_tier } = proxy.useDict('is_zero_bill', 'is_next_bill_date', 'account_status', 'card_tier')
const { limit_history_type, adjust_type } = proxy.useDict('limit_history_type', 'adjust_type')
const { deal_type, deal_category } = proxy.useDict('deal_type', 'deal_category')
const bankcardList = ref([])
const open = ref(false)
const loading = ref(true)
@@ -286,6 +329,19 @@ const single = ref(true)
const multiple = ref(true)
const total = ref(0)
const title = ref('')
const titleDealRecord = ref('')
const openDealRecord = ref(false)
const currentAccountId = ref('')
const loadingDealRecord = ref(false)
const tableDealRecordData = ref([])
const dealRecordTotal = ref(0)
const queryDealRecordParams = ref({
time: '',
dealType: null,
dealCategory: null,
pageNum: 1,
pageSize: 10
})
const detailOpen = ref(false)
const openDetail = ref(false)
@@ -297,6 +353,7 @@ const bankList = ref([])
const operateList = ref([
{ id: 'view', icon: 'View', title: '查看', hasPermi: ['invest:bankcard:query'] },
{ id: 'dealRecord', icon: 'Tickets', title: '交易明细', hasPermi: ['invest:bankcard:query'] },
{ id: 'limit', icon: 'Grid', title: '调额记录', hasPermi: ['invest:bankcard:edit'] },
{ id: 'edit', icon: 'Edit', title: '修改', hasPermi: ['invest:bankcard:edit'] },
{ id: 'delete', icon: 'Delete', title: '删除', hasPermi: ['invest:bankcard:remove'] }
@@ -339,6 +396,9 @@ const data = reactive({
const handleOperate = (operate, row) => {
switch (operate) {
case 'dealRecord':
handleDealRecord(row)
break
case 'limit':
handleDetail(row)
break
@@ -474,6 +534,54 @@ function handleDetail(row) {
})
}
/** 交易明细按钮操作 */
function handleDealRecord(row) {
queryDealRecordParams.value.time = ''
queryDealRecordParams.value.dealType = ''
queryDealRecordParams.value.dealCategory = ''
titleDealRecord.value = `${row.name}${row.code ? '【' + row.code + '】' : ''}交易明细`
currentAccountId.value = row.id || ids.value
getDealRecordList(1)
openDealRecord.value = true
}
const handleCurrentDealRecordChange = (num) => {
getDealRecordList(num)
}
const handleDealRecordQuery = () => {
getDealRecordList(1)
}
const resetDealRecordQuery = () => {
proxy.resetForm('queryDealRecordRef')
queryDealRecordParams.value.time = ''
queryDealRecordParams.value.dealType = ''
queryDealRecordParams.value.dealCategory = ''
dealRecordTotal.value = 0
getDealRecordList(1)
}
const getDealRecordList = (num) => {
loadingDealRecord.value = true
const timeRange = queryDealRecordParams.value.time
let st = ''
let et = ''
if (timeRange && timeRange.length === 2) {
st = dayjs(timeRange[0]).format('YYYY-MM-DD')
et = dayjs(timeRange[1]).format('YYYY-MM-DD')
}
queryDealRecordParams.value.startTime = st
queryDealRecordParams.value.endTime = et
queryDealRecordParams.value.pageNum = num
queryDealRecordParams.value.accountId = currentAccountId.value
listAccountDealRecord(queryDealRecordParams.value).then((response) => {
loadingDealRecord.value = false
tableDealRecordData.value = response.rows
dealRecordTotal.value = response.total
})
}
/** 查询信用卡信息列表 */
function getList() {
loading.value = true

View File

@@ -72,7 +72,7 @@
</el-table-column>
<el-table-column label="卡面" align="center" width="100" prop="cardFace" />
<el-table-column label="备注" align="center" prop="remark" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<el-table-column label="操作" width="210" align="center" class-name="small-padding fixed-width">
<template v-slot="scope">
<div class="ctrl-btn d-flex">
<el-tooltip v-for="item in operateList" :key="item.id" class="item" effect="dark" :content="item.title" placement="top">
@@ -224,6 +224,46 @@
</div>
<template #footer> </template>
</el-dialog>
<el-dialog :title="titleDealRecord" v-model="openDealRecord" width="1200px" append-to-body destroy-on-close>
<el-form :model="queryDealRecordParams" ref="queryDealRecordRef" inline>
<el-form-item label="交易时间" prop="time">
<el-date-picker v-model="queryDealRecordParams.time" type="daterange" range-separator="至" start-placeholder="开始时间" end-placeholder="结束时间" />
</el-form-item>
<el-form-item label="交易类型" style="width: 220px" prop="dealType">
<el-select v-model="queryDealRecordParams.dealType" placeholder="请选择交易类型" clearable>
<el-option v-for="dict in deal_type" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
<el-form-item label="交易类别" style="width: 220px" prop="dealCategory">
<el-select v-model="queryDealRecordParams.dealCategory" placeholder="请选择交易类别" clearable>
<el-option v-for="dict in deal_category" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
<el-form-item label="">
<el-button type="primary" icon="Search" @click="handleDealRecordQuery">搜索</el-button>
<el-button type="info" icon="Refresh" @click="resetDealRecordQuery">重置</el-button>
</el-form-item>
</el-form>
<el-table v-loading="loadingDealRecord" :data="tableDealRecordData">
<el-table-column label="交易时间" align="center" prop="createTime" width="180" />
<el-table-column label="交易金额" align="center" prop="amount" />
<el-table-column label="当前余额" align="center" prop="currentBalance" />
<el-table-column label="交易类型" align="center" width="120" prop="dealType">
<template #default="scope">
<dict-tag :options="deal_type" :value="scope.row.dealType" />
</template>
</el-table-column>
<el-table-column label="交易类别" align="center" width="120" prop="dealCategory">
<template #default="scope">
<dict-tag :options="deal_category" :value="scope.row.dealCategory" />
</template>
</el-table-column>
<el-table-column label="交易子类别" align="center" width="150" prop="childCategoryName" />
<el-table-column label="备注" align="center" width="300" prop="remark" />
</el-table>
<el-pagination small background layout="total, prev, pager, next" :total="dealRecordTotal" @current-change="handleCurrentDealRecordChange" />
</el-dialog>
</div>
</template>
@@ -231,11 +271,14 @@
import { listBankcardLend, getBankcardLend, delBankcardLend, addBankcardLend, updateBankcardLend } from '@/api/invest/bankcardlend'
import { listLimitHistory, getLimitHistory, delLimitHistory, addLimitHistory, updateLimitHistory } from '@/api/invest/limitHistory'
import { listBankBaseInfo } from '@/api/invest/bankBaseInfo'
import { listAccountDealRecord } from '@/api/invest/accountDealRecord'
import dayjs from 'dayjs'
// eslint-disable-next-line no-unused-vars
import { require } from '@/utils/require'
const { proxy } = getCurrentInstance()
const { bank_card_type, debit_type, account_status, card_tier } = proxy.useDict('bank_card_type', 'debit_type', 'account_status', 'card_tier')
const { limit_history_type, adjust_type } = proxy.useDict('limit_history_type', 'adjust_type')
const { deal_type, deal_category } = proxy.useDict('deal_type', 'deal_category')
const bankcardList = ref([])
const open = ref(false)
const loading = ref(true)
@@ -245,6 +288,19 @@ const single = ref(true)
const multiple = ref(true)
const total = ref(0)
const title = ref('')
const titleDealRecord = ref('')
const openDealRecord = ref(false)
const currentAccountId = ref('')
const loadingDealRecord = ref(false)
const tableDealRecordData = ref([])
const dealRecordTotal = ref(0)
const queryDealRecordParams = ref({
time: '',
dealType: null,
dealCategory: null,
pageNum: 1,
pageSize: 10
})
const detailOpen = ref(false)
const openDetail = ref(false)
@@ -256,6 +312,7 @@ const bankList = ref([])
const operateList = ref([
{ id: 'view', icon: 'View', title: '查看', hasPermi: ['invest:bankcard:query'] },
{ id: 'dealRecord', icon: 'Tickets', title: '交易明细', hasPermi: ['invest:bankcard:query'] },
{ id: 'limit', icon: 'Grid', title: '非柜面限额记录', hasPermi: ['invest:bankcard:edit'] },
{ id: 'edit', icon: 'Edit', title: '修改', hasPermi: ['invest:bankcard:edit'] },
{ id: 'delete', icon: 'Delete', title: '删除', hasPermi: ['invest:bankcard:remove'] }
@@ -295,6 +352,9 @@ const data = reactive({
const handleOperate = (operate, row) => {
switch (operate) {
case 'dealRecord':
handleDealRecord(row)
break
case 'limit':
handleDetail(row)
break
@@ -429,6 +489,54 @@ function handleDetail(row) {
limitHistoryList.value = response.rows
})
}
/** 交易明细按钮操作 */
function handleDealRecord(row) {
queryDealRecordParams.value.time = ''
queryDealRecordParams.value.dealType = ''
queryDealRecordParams.value.dealCategory = ''
titleDealRecord.value = `${row.name}${row.code ? '【' + row.code + '】' : ''}交易明细`
currentAccountId.value = row.id || ids.value
getDealRecordList(1)
openDealRecord.value = true
}
const handleCurrentDealRecordChange = (num) => {
getDealRecordList(num)
}
const handleDealRecordQuery = () => {
getDealRecordList(1)
}
const resetDealRecordQuery = () => {
proxy.resetForm('queryDealRecordRef')
queryDealRecordParams.value.time = ''
queryDealRecordParams.value.dealType = ''
queryDealRecordParams.value.dealCategory = ''
dealRecordTotal.value = 0
getDealRecordList(1)
}
const getDealRecordList = (num) => {
loadingDealRecord.value = true
const timeRange = queryDealRecordParams.value.time
let st = ''
let et = ''
if (timeRange && timeRange.length === 2) {
st = dayjs(timeRange[0]).format('YYYY-MM-DD')
et = dayjs(timeRange[1]).format('YYYY-MM-DD')
}
queryDealRecordParams.value.startTime = st
queryDealRecordParams.value.endTime = et
queryDealRecordParams.value.pageNum = num
queryDealRecordParams.value.accountId = currentAccountId.value
listAccountDealRecord(queryDealRecordParams.value).then((response) => {
loadingDealRecord.value = false
tableDealRecordData.value = response.rows
dealRecordTotal.value = response.total
})
}
/** 查询储蓄账户信息列表 */
function getList() {
loading.value = true

View File

@@ -46,7 +46,7 @@
</template>
</el-table-column>
<el-table-column label="备注" align="center" prop="remark" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<el-table-column label="操作" width="210" align="center" class-name="small-padding fixed-width">
<template v-slot="scope">
<div class="ctrl-btn d-flex">
<el-tooltip v-for="item in operateList" :key="item.id" class="item" effect="dark" :content="item.title" placement="top">
@@ -111,16 +111,58 @@
</div>
</template>
</el-dialog>
<el-dialog :title="titleDealRecord" v-model="openDealRecord" width="1200px" append-to-body destroy-on-close>
<el-form :model="queryDealRecordParams" ref="queryDealRecordRef" inline>
<el-form-item label="交易时间" prop="time">
<el-date-picker v-model="queryDealRecordParams.time" type="daterange" range-separator="至" start-placeholder="开始时间" end-placeholder="结束时间" />
</el-form-item>
<el-form-item label="交易类型" style="width: 220px" prop="dealType">
<el-select v-model="queryDealRecordParams.dealType" placeholder="请选择交易类型" clearable>
<el-option v-for="dict in deal_type" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
<el-form-item label="交易类别" style="width: 220px" prop="dealCategory">
<el-select v-model="queryDealRecordParams.dealCategory" placeholder="请选择交易类别" clearable>
<el-option v-for="dict in deal_category" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
<el-form-item label="">
<el-button type="primary" icon="Search" @click="handleDealRecordQuery">搜索</el-button>
<el-button type="info" icon="Refresh" @click="resetDealRecordQuery">重置</el-button>
</el-form-item>
</el-form>
<el-table v-loading="loadingDealRecord" :data="tableDealRecordData">
<el-table-column label="交易时间" align="center" prop="createTime" width="180" />
<el-table-column label="交易金额" align="center" prop="amount" />
<el-table-column label="当前余额" align="center" prop="currentBalance" />
<el-table-column label="交易类型" align="center" width="120" prop="dealType">
<template #default="scope">
<dict-tag :options="deal_type" :value="scope.row.dealType" />
</template>
</el-table-column>
<el-table-column label="交易类别" align="center" width="150" prop="dealCategory">
<template #default="scope">
<dict-tag :options="deal_category" :value="scope.row.dealCategory" />
</template>
</el-table-column>
<el-table-column label="交易子类别" align="center" width="150" prop="childCategoryName" />
<el-table-column label="备注" align="center" width="300" prop="remark" />
</el-table>
<el-pagination small background layout="total, prev, pager, next" :total="dealRecordTotal" @current-change="handleCurrentDealRecordChange" />
</el-dialog>
</div>
</template>
<script setup name="FutureStocks">
import { listFutureStocks, getFutureStocks, delFutureStocks, addFutureStocks, updateFutureStocks } from '@/api/invest/futureStocks'
import { listAccountDealRecord } from '@/api/invest/accountDealRecord'
import { listBankcardLend } from '@/api/invest/bankcardlend'
// eslint-disable-next-line no-unused-vars
import { require } from '@/utils/require'
const { proxy } = getCurrentInstance()
const { future_stock_type, account_status } = proxy.useDict('future_stock_type', 'account_status')
const { deal_type, deal_category } = proxy.useDict('deal_type', 'deal_category')
const futureStocksList = ref([])
const open = ref(false)
@@ -131,9 +173,23 @@ const single = ref(true)
const multiple = ref(true)
const total = ref(0)
const title = ref('')
const titleDealRecord = ref('')
const openDealRecord = ref(false)
const currentAccountId = ref('')
const loadingDealRecord = ref(false)
const tableDealRecordData = ref([])
const dealRecordTotal = ref(0)
const queryDealRecordParams = ref({
time: '',
dealType: null,
dealCategory: null,
pageNum: 1,
pageSize: 10
})
const debitCardList = ref([])
const operateList = ref([
{ id: 'view', icon: 'View', title: '查看', hasPermi: ['invest:futureStocks:query'] },
{ id: 'dealRecord', icon: 'Tickets', title: '交易明细', hasPermi: ['invest:futureStocks:query'] },
{ id: 'edit', icon: 'Edit', title: '修改', hasPermi: ['invest:futureStocks:edit'] },
{ id: 'delete', icon: 'Delete', title: '删除', hasPermi: ['invest:futureStocks:remove'] }
])
@@ -172,6 +228,9 @@ const handleOperate = (operate, row) => {
case 'view':
handleView(row)
break
case 'dealRecord':
handleDealRecord(row)
break
case 'edit':
handleUpdate(row)
break
@@ -298,6 +357,54 @@ function handleUpdate(row) {
})
}
/** 交易明细按钮操作 */
function handleDealRecord(row) {
queryDealRecordParams.value.time = ''
queryDealRecordParams.value.dealType = ''
queryDealRecordParams.value.dealCategory = ''
titleDealRecord.value = `${row.name}${row.code ? '【' + row.code + '】' : ''}交易明细`
currentAccountId.value = row.id || ids.value
getDealRecordList(1)
openDealRecord.value = true
}
const handleCurrentDealRecordChange = (num) => {
getDealRecordList(num)
}
const handleDealRecordQuery = () => {
getDealRecordList(1)
}
const resetDealRecordQuery = () => {
proxy.resetForm('queryDealRecordRef')
queryDealRecordParams.value.time = ''
queryDealRecordParams.value.dealType = ''
queryDealRecordParams.value.dealCategory = ''
dealRecordTotal.value = 0
getDealRecordList(1)
}
const getDealRecordList = (num) => {
loadingDealRecord.value = true
const timeRange = queryDealRecordParams.value.time
let st = ''
let et = ''
if (timeRange && timeRange.length > 0) {
st = timeRange[0]
et = timeRange[1]
}
queryDealRecordParams.value.startTime = st
queryDealRecordParams.value.endTime = et
queryDealRecordParams.value.pageNum = num
queryDealRecordParams.value.accountId = currentAccountId.value
listAccountDealRecord(queryDealRecordParams.value).then((response) => {
loadingDealRecord.value = false
tableDealRecordData.value = response.rows
dealRecordTotal.value = response.total
})
}
/** 提交按钮 */
function submitForm() {
proxy.$refs.futureStocksRef.validate((valid) => {

View File

@@ -44,7 +44,7 @@
<el-table-column label="关联储蓄卡" align="center" prop="bankNameCode" />
<el-table-column label="开户日期" align="center" prop="activationDate" width="120"> </el-table-column>
<el-table-column label="备注" align="center" prop="remark" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<el-table-column label="操作" width="210" align="center" class-name="small-padding fixed-width">
<template v-slot="scope">
<div class="ctrl-btn d-flex">
<el-tooltip v-for="item in operateList" :key="item.id" class="item" effect="dark" :content="item.title" placement="top">
@@ -121,16 +121,58 @@
</div>
</template>
</el-dialog>
<el-dialog :title="titleDealRecord" v-model="openDealRecord" width="1200px" append-to-body destroy-on-close>
<el-form :model="queryDealRecordParams" ref="queryDealRecordRef" inline>
<el-form-item label="交易时间" prop="time">
<el-date-picker v-model="queryDealRecordParams.time" type="daterange" range-separator="至" start-placeholder="开始时间" end-placeholder="结束时间" />
</el-form-item>
<el-form-item label="交易类型" style="width: 220px" prop="dealType">
<el-select v-model="queryDealRecordParams.dealType" placeholder="请选择交易类型" clearable>
<el-option v-for="dict in deal_type" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
<el-form-item label="交易类别" style="width: 220px" prop="dealCategory">
<el-select v-model="queryDealRecordParams.dealCategory" placeholder="请选择交易类别" clearable>
<el-option v-for="dict in deal_category" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
<el-form-item label="">
<el-button type="primary" icon="Search" @click="handleDealRecordQuery">搜索</el-button>
<el-button type="info" icon="Refresh" @click="resetDealRecordQuery">重置</el-button>
</el-form-item>
</el-form>
<el-table v-loading="loadingDealRecord" :data="tableDealRecordData">
<el-table-column label="交易时间" align="center" prop="createTime" width="180" />
<el-table-column label="交易金额" align="center" prop="amount" />
<el-table-column label="当前余额" align="center" prop="currentBalance" />
<el-table-column label="交易类型" align="center" width="120" prop="dealType">
<template #default="scope">
<dict-tag :options="deal_type" :value="scope.row.dealType" />
</template>
</el-table-column>
<el-table-column label="交易类别" align="center" width="150" prop="dealCategory">
<template #default="scope">
<dict-tag :options="deal_category" :value="scope.row.dealCategory" />
</template>
</el-table-column>
<el-table-column label="交易子类别" align="center" width="150" prop="childCategoryName" />
<el-table-column label="备注" align="center" width="300" prop="remark" />
</el-table>
<el-pagination small background layout="total, prev, pager, next" :total="dealRecordTotal" @current-change="handleCurrentDealRecordChange" />
</el-dialog>
</div>
</template>
<script setup name="FutureStocks">
import { listFutureStocks, getFutureStocks, delFutureStocks, addFutureStocks, updateFutureStocks } from '@/api/invest/futureStocks'
import { listAccountDealRecord } from '@/api/invest/accountDealRecord'
import { listBankcardLend } from '@/api/invest/bankcardlend'
// eslint-disable-next-line no-unused-vars
import { require } from '@/utils/require'
const { proxy } = getCurrentInstance()
const { future_stock_type, account_status } = proxy.useDict('future_stock_type', 'account_status')
const { deal_type, deal_category } = proxy.useDict('deal_type', 'deal_category')
const futureStocksList = ref([])
const open = ref(false)
@@ -141,9 +183,23 @@ const single = ref(true)
const multiple = ref(true)
const total = ref(0)
const title = ref('')
const titleDealRecord = ref('')
const openDealRecord = ref(false)
const currentAccountId = ref('')
const loadingDealRecord = ref(false)
const tableDealRecordData = ref([])
const dealRecordTotal = ref(0)
const queryDealRecordParams = ref({
time: '',
dealType: null,
dealCategory: null,
pageNum: 1,
pageSize: 10
})
const debitCardList = ref([])
const operateList = ref([
{ id: 'view', icon: 'View', title: '查看', hasPermi: ['invest:futureStocks:query'] },
{ id: 'dealRecord', icon: 'Tickets', title: '交易明细', hasPermi: ['invest:futureStocks:query'] },
{ id: 'edit', icon: 'Edit', title: '修改', hasPermi: ['invest:futureStocks:edit'] },
{ id: 'delete', icon: 'Delete', title: '删除', hasPermi: ['invest:futureStocks:remove'] }
])
@@ -182,6 +238,9 @@ const handleOperate = (operate, row) => {
case 'view':
handleView(row)
break
case 'dealRecord':
handleDealRecord(row)
break
case 'edit':
handleUpdate(row)
break
@@ -308,6 +367,54 @@ function handleUpdate(row) {
})
}
/** 交易明细按钮操作 */
function handleDealRecord(row) {
queryDealRecordParams.value.time = ''
queryDealRecordParams.value.dealType = ''
queryDealRecordParams.value.dealCategory = ''
titleDealRecord.value = `${row.name}${row.code ? '【' + row.code + '】' : ''}交易明细`
currentAccountId.value = row.id || ids.value
getDealRecordList(1)
openDealRecord.value = true
}
const handleCurrentDealRecordChange = (num) => {
getDealRecordList(num)
}
const handleDealRecordQuery = () => {
getDealRecordList(1)
}
const resetDealRecordQuery = () => {
proxy.resetForm('queryDealRecordRef')
queryDealRecordParams.value.time = ''
queryDealRecordParams.value.dealType = ''
queryDealRecordParams.value.dealCategory = ''
dealRecordTotal.value = 0
getDealRecordList(1)
}
const getDealRecordList = (num) => {
loadingDealRecord.value = true
const timeRange = queryDealRecordParams.value.time
let st = ''
let et = ''
if (timeRange && timeRange.length > 0) {
st = timeRange[0]
et = timeRange[1]
}
queryDealRecordParams.value.startTime = st
queryDealRecordParams.value.endTime = et
queryDealRecordParams.value.pageNum = num
queryDealRecordParams.value.accountId = currentAccountId.value
listAccountDealRecord(queryDealRecordParams.value).then((response) => {
loadingDealRecord.value = false
tableDealRecordData.value = response.rows
dealRecordTotal.value = response.total
})
}
/** 提交按钮 */
function submitForm() {
proxy.$refs.futureStocksRef.validate((valid) => {

View File

@@ -128,16 +128,58 @@
</div>
</template>
</el-dialog>
<el-dialog :title="titleDealRecord" v-model="openDealRecord" width="1200px" append-to-body destroy-on-close>
<el-form :model="queryDealRecordParams" ref="queryDealRecordRef" inline>
<el-form-item label="交易时间" prop="time">
<el-date-picker v-model="queryDealRecordParams.time" type="daterange" range-separator="至" start-placeholder="开始时间" end-placeholder="结束时间" />
</el-form-item>
<el-form-item label="交易类型" style="width: 220px" prop="dealType">
<el-select v-model="queryDealRecordParams.dealType" placeholder="请选择交易类型" clearable>
<el-option v-for="dict in deal_type" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
<el-form-item label="交易类别" style="width: 220px" prop="dealCategory">
<el-select v-model="queryDealRecordParams.dealCategory" placeholder="请选择交易类别" clearable>
<el-option v-for="dict in deal_category" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
<el-form-item label="">
<el-button type="primary" icon="Search" @click="handleDealRecordQuery">搜索</el-button>
<el-button type="info" icon="Refresh" @click="resetDealRecordQuery">重置</el-button>
</el-form-item>
</el-form>
<el-table v-loading="loadingDealRecord" :data="tableDealRecordData">
<el-table-column label="交易时间" align="center" prop="createTime" width="180" />
<el-table-column label="交易金额" align="center" prop="amount" />
<el-table-column label="当前余额" align="center" prop="currentBalance" />
<el-table-column label="交易类型" align="center" width="120" prop="dealType">
<template #default="scope">
<dict-tag :options="deal_type" :value="scope.row.dealType" />
</template>
</el-table-column>
<el-table-column label="交易类别" align="center" width="150" prop="dealCategory">
<template #default="scope">
<dict-tag :options="deal_category" :value="scope.row.dealCategory" />
</template>
</el-table-column>
<el-table-column label="交易子类别" align="center" width="150" prop="childCategoryName" />
<el-table-column label="备注" align="center" width="300" prop="remark" />
</el-table>
<el-pagination small background layout="total, prev, pager, next" :total="dealRecordTotal" @current-change="handleCurrentDealRecordChange" />
</el-dialog>
</div>
</template>
<script setup name="BankcardLend">
import { listBankcardLend, getBankcardLend, delBankcardLend, addBankcardLend, updateBankcardLend } from '@/api/invest/bankcardlend'
import { listAccountDealRecord } from '@/api/invest/accountDealRecord'
import { listBankBaseInfo } from '@/api/invest/bankBaseInfo'
// eslint-disable-next-line no-unused-vars
import { require } from '@/utils/require'
const { proxy } = getCurrentInstance()
const { bank_card_type, lend_type, account_status } = proxy.useDict('bank_card_type', 'lend_type', 'account_status')
const { deal_type, deal_category } = proxy.useDict('deal_type', 'deal_category')
const bankcardList = ref([])
const open = ref(false)
@@ -148,9 +190,23 @@ const single = ref(true)
const multiple = ref(true)
const total = ref(0)
const title = ref('')
const titleDealRecord = ref('')
const openDealRecord = ref(false)
const currentAccountId = ref('')
const loadingDealRecord = ref(false)
const tableDealRecordData = ref([])
const dealRecordTotal = ref(0)
const queryDealRecordParams = ref({
time: '',
dealType: null,
dealCategory: null,
pageNum: 1,
pageSize: 10
})
const bankList = ref([])
const operateList = ref([
{ id: 'view', icon: 'View', title: '查看', hasPermi: ['invest:bankcard:query'] },
{ id: 'dealRecord', icon: 'Tickets', title: '交易明细', hasPermi: ['invest:bankcard:query'] },
{ id: 'edit', icon: 'Edit', title: '修改', hasPermi: ['invest:bankcard:edit'] },
{ id: 'delete', icon: 'Delete', title: '删除', hasPermi: ['invest:bankcard:remove'] }
])
@@ -180,6 +236,9 @@ const handleOperate = (operate, row) => {
case 'view':
handleView(row)
break
case 'dealRecord':
handleDealRecord(row)
break
case 'edit':
handleUpdate(row)
break
@@ -312,6 +371,54 @@ function handleUpdate(row) {
})
}
/** 交易明细按钮操作 */
function handleDealRecord(row) {
queryDealRecordParams.value.time = ''
queryDealRecordParams.value.dealType = ''
queryDealRecordParams.value.dealCategory = ''
titleDealRecord.value = `${row.name}${row.code ? '【' + row.code + '】' : ''}交易明细`
currentAccountId.value = row.id || ids.value
getDealRecordList(1)
openDealRecord.value = true
}
const handleCurrentDealRecordChange = (num) => {
getDealRecordList(num)
}
const handleDealRecordQuery = () => {
getDealRecordList(1)
}
const resetDealRecordQuery = () => {
proxy.resetForm('queryDealRecordRef')
queryDealRecordParams.value.time = ''
queryDealRecordParams.value.dealType = ''
queryDealRecordParams.value.dealCategory = ''
dealRecordTotal.value = 0
getDealRecordList(1)
}
const getDealRecordList = (num) => {
loadingDealRecord.value = true
const timeRange = queryDealRecordParams.value.time
let st = ''
let et = ''
if (timeRange && timeRange.length > 0) {
st = timeRange[0]
et = timeRange[1]
}
queryDealRecordParams.value.startTime = st
queryDealRecordParams.value.endTime = et
queryDealRecordParams.value.pageNum = num
queryDealRecordParams.value.accountId = currentAccountId.value
listAccountDealRecord(queryDealRecordParams.value).then((response) => {
loadingDealRecord.value = false
tableDealRecordData.value = response.rows
dealRecordTotal.value = response.total
})
}
/** 提交按钮 */
function submitForm() {
proxy.$refs.bankcardRef.validate((valid) => {

View File

@@ -160,22 +160,34 @@
<template #default="scope">
<div v-if="scope.row.detailList && scope.row.detailList.length > 0">
<div
v-for="(item, index) in scope.row.detailList"
:key="index"
style="
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 8px;
margin-bottom: 4px;
background: #f5f7fa;
border-radius: 4px;
"
v-for="(group, groupIndex) in groupByRepaymentAccount(scope.row.detailList)"
:key="groupIndex"
style="padding: 6px 8px; margin-bottom: 4px; background: #f5f7fa; border-radius: 4px"
>
<span style="flex: 1"
>{{ item.bankCardLendName }}(本金:{{ item.principal }},利息:{{ item.interest }},合计:{{ item.currentAmount }})</span
<!-- 该账户下的所有明细 -->
<div
v-for="(item, itemIndex) in group.items"
:key="itemIndex"
:style="{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '4px 0',
borderBottom: itemIndex < group.items.length - 1 ? '1px dashed #dcdfe6' : 'none'
}"
>
<el-button size="small" type="primary" icon="Coin" @click="handleRepayment(item)">还款</el-button>
<span style="flex: 1"
>{{ item.bankCardLendName }}(本金:{{ item.principal }},利息:{{ item.interest }},合计:{{ item.currentAmount }})</span
>
<el-button size="small" type="primary" icon="Coin" @click="handleRepayment(item)">还款</el-button>
</div>
<!-- 还款账户信息同一账户只显示一次放在明细后面 -->
<div
v-if="group.repaymentAccountName"
style="margin-top: 4px; padding-top: 4px; color: #409eff; font-weight: 500; border-top: 1px solid #e4e7ed"
>
还款账户:{{ group.repaymentAccountName }}(余额:{{ group.repaymentAccountBalance }})
</div>
</div>
</div>
<span v-else v-html="formatMultiLineData(scope.row.detail)"></span>
@@ -392,6 +404,26 @@ const reversedTableData = computed(() => {
return []
})
// 按还款账户分组处理明细数据
const groupByRepaymentAccount = (detailList) => {
if (!detailList || detailList.length === 0) return []
const groups = {}
detailList.forEach((item) => {
const key = item.repaymentAccountName || '未指定账户'
if (!groups[key]) {
groups[key] = {
repaymentAccountName: item.repaymentAccountName,
repaymentAccountBalance: item.repaymentAccountBalance,
items: []
}
}
groups[key].items.push(item)
})
return Object.values(groups)
}
function getList() {
loading.value = true
chartData.value = { name: [], value1: [] }

View File

@@ -62,7 +62,7 @@
<el-table-column label="供货商" align="center" width="100" prop="manufacture" />
<!--
<el-table-column label="商户码" align="center" prop="merchantCode" /> -->
<el-table-column label="操作" align="center" width="150" class-name="small-padding fixed-width">
<el-table-column label="操作" align="center" width="200" class-name="small-padding fixed-width">
<template v-slot="scope">
<div class="ctrl-btn d-flex">
<el-tooltip v-for="item in operateList" :key="item.id" class="item" effect="dark" :content="item.title" placement="top">
@@ -150,12 +150,44 @@
</div>
</template>
</el-dialog>
<el-dialog :title="titleSwipeRecord" v-model="openSwipeRecord" width="1200px" append-to-body destroy-on-close>
<el-form :model="querySwipeRecordParams" ref="querySwipeRecordRef" inline>
<el-form-item label="信用卡" style="width: 420px" prop="outAccountId">
<el-select v-model="querySwipeRecordParams.outAccountId" placeholder="请选择信用卡" clearable>
<el-option v-for="creditCard in creditCardList" :key="creditCard.id" :label="creditCard.nameCodeAvailableLimit" :value="creditCard.id" />
</el-select>
</el-form-item>
<el-form-item label="刷卡时间" prop="time">
<el-date-picker v-model="querySwipeRecordParams.time" type="daterange" range-separator="至" start-placeholder="开始时间" end-placeholder="结束时间" />
</el-form-item>
<el-form-item label="">
<el-button type="primary" icon="Search" @click="handleSwipeRecordQuery">搜索</el-button>
<el-button type="info" icon="Refresh" @click="resetSwipeRecordQuery">重置</el-button>
</el-form-item>
</el-form>
<el-table v-loading="loadingSwipeRecord" :data="tableSwipeRecordData">
<el-table-column type="index" label="序号" :index="swipeRecordIndexMethod" width="60" />
<el-table-column label="POS机名称" align="center" prop="posName" />
<el-table-column label="刷卡商户" align="center" prop="merchantName" />
<el-table-column label="信用卡" align="center" prop="outAccountName" />
<el-table-column label="刷卡时间" align="center" width="180" prop="createTime" />
<el-table-column label="刷卡金额" align="center" prop="amount" />
<el-table-column label="手续费" align="center" width="100" prop="commission" />
<el-table-column label="储蓄卡" align="center" prop="inAccountName" />
<el-table-column label="入账金额" align="center" width="120" prop="actualAmount" />
</el-table>
<el-pagination small background layout="total, prev, pager, next" :total="swipeRecordTotal" @current-change="handleCurrentSwipeRecordChange" />
</el-dialog>
</div>
</template>
<script setup name="Posmachine">
import { listPosmachine, getPosmachine, delPosmachine, addPosmachine, updatePosmachine } from '@/api/invest/posmachine'
import { listBankcardLend } from '@/api/invest/bankcardlend'
import { listAccounts } from '@/api/invest/accounts'
import { listAccountsTransferRecord } from '@/api/invest/accountsTransferRecord'
import dayjs from 'dayjs'
// eslint-disable-next-line no-unused-vars
import { require } from '@/utils/require'
const { proxy } = getCurrentInstance()
@@ -171,8 +203,16 @@ const multiple = ref(true)
const total = ref(0)
const title = ref('')
const debitCardList = ref([])
const creditCardList = ref([])
const titleSwipeRecord = ref('')
const openSwipeRecord = ref(false)
const currentPosId = ref('')
const loadingSwipeRecord = ref(false)
const tableSwipeRecordData = ref([])
const swipeRecordTotal = ref(0)
const operateList = ref([
{ id: 'view', icon: 'View', title: '查看', hasPermi: ['invest:posmachine:query'] },
{ id: 'swipeRecord', icon: 'Tickets', title: '刷卡明细', hasPermi: ['invest:accountsTransferRecord:query'] },
{ id: 'edit', icon: 'Edit', title: '修改', hasPermi: ['invest:posmachine:edit'] },
{ id: 'delete', icon: 'Delete', title: '删除', hasPermi: ['invest:posmachine:remove'] }
])
@@ -193,6 +233,12 @@ const data = reactive({
status: '1',
pageSize: 1000
},
queryCreditCardParams: {
pageNum: 1,
type: '2',
status: '1',
pageSize: 1000
},
rules: {
name: [{ required: true, message: 'pos机名称不能为空', trigger: 'blur' }],
status: [{ required: true, message: '账户状态不能为空', trigger: 'blur' }],
@@ -208,12 +254,23 @@ const data = reactive({
// merchantType: [{ required: true, message: '商户类型不能为空', trigger: 'change' }]
}
})
const querySwipeRecordParams = ref({
pageNum: 1,
pageSize: 10,
type: '1',
time: '',
posId: null,
outAccountId: null
})
const handleOperate = (operate, row) => {
switch (operate) {
case 'view':
handleView(row)
break
case 'swipeRecord':
handleSwipeRecord(row)
break
case 'edit':
handleUpdate(row)
break
@@ -225,7 +282,7 @@ const handleOperate = (operate, row) => {
}
}
const { queryParams, queryDebitCardParams, form, rules } = toRefs(data)
const { queryParams, queryDebitCardParams, queryCreditCardParams, form, rules } = toRefs(data)
/** 查询储蓄卡管理列表 */
function getDebitCardList() {
@@ -234,6 +291,13 @@ function getDebitCardList() {
})
}
/** 查询信用卡管理列表 */
function getCreditCardList() {
listAccounts(queryCreditCardParams.value).then((response) => {
creditCardList.value = response.rows
})
}
/** 查询pos机信息列表 */
function getList() {
loading.value = true
@@ -340,6 +404,59 @@ function handleUpdate(row) {
})
}
/** 刷卡明细按钮操作 */
function handleSwipeRecord(row) {
querySwipeRecordParams.value.time = ''
querySwipeRecordParams.value.outAccountId = null
titleSwipeRecord.value = `${row.name}${row.merchantName ? '【' + row.merchantName + '】' : ''}刷卡明细`
currentPosId.value = row.id || ids.value
getSwipeRecordList(1)
openSwipeRecord.value = true
}
const handleCurrentSwipeRecordChange = (num) => {
getSwipeRecordList(num)
}
const handleSwipeRecordQuery = () => {
getSwipeRecordList(1)
}
const resetSwipeRecordQuery = () => {
proxy.resetForm('querySwipeRecordRef')
querySwipeRecordParams.value.time = ''
querySwipeRecordParams.value.outAccountId = null
swipeRecordTotal.value = 0
getSwipeRecordList(1)
}
const getSwipeRecordList = (num) => {
loadingSwipeRecord.value = true
const timeRange = querySwipeRecordParams.value.time
let st = ''
let et = ''
if (timeRange && timeRange.length === 2) {
st = dayjs(timeRange[0]).format('YYYY-MM-DD')
et = dayjs(timeRange[1]).format('YYYY-MM-DD')
}
querySwipeRecordParams.value.startTime = st
querySwipeRecordParams.value.endTime = et
querySwipeRecordParams.value.pageNum = num
querySwipeRecordParams.value.posId = currentPosId.value
querySwipeRecordParams.value.type = '1'
listAccountsTransferRecord(querySwipeRecordParams.value).then((response) => {
loadingSwipeRecord.value = false
tableSwipeRecordData.value = response.rows
swipeRecordTotal.value = response.total
})
}
const swipeRecordIndexMethod = (index) => {
const nowPage = querySwipeRecordParams.value.pageNum
const nowLimit = querySwipeRecordParams.value.pageSize
return index + 1 + (nowPage - 1) * nowLimit
}
/** 提交按钮 */
function submitForm() {
proxy.$refs.posmachineRef.validate((valid) => {
@@ -386,6 +503,7 @@ function handleExport() {
`posmachine_${new Date().getTime()}.xlsx`
)
}
getCreditCardList()
getDebitCardList()
getList()
</script>

View File

@@ -46,7 +46,7 @@
</template>
</el-table-column>
<el-table-column label="备注" align="center" prop="remark" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<el-table-column label="操作" width="210" align="center" class-name="small-padding fixed-width">
<template v-slot="scope">
<div class="ctrl-btn d-flex">
<el-tooltip v-for="item in operateList" :key="item.id" class="item" effect="dark" :content="item.title" placement="top">
@@ -111,16 +111,58 @@
</div>
</template>
</el-dialog>
<el-dialog :title="titleDealRecord" v-model="openDealRecord" width="1200px" append-to-body destroy-on-close>
<el-form :model="queryDealRecordParams" ref="queryDealRecordRef" inline>
<el-form-item label="交易时间" prop="time">
<el-date-picker v-model="queryDealRecordParams.time" type="daterange" range-separator="至" start-placeholder="开始时间" end-placeholder="结束时间" />
</el-form-item>
<el-form-item label="交易类型" style="width: 220px" prop="dealType">
<el-select v-model="queryDealRecordParams.dealType" placeholder="请选择交易类型" clearable>
<el-option v-for="dict in deal_type" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
<el-form-item label="交易类别" style="width: 220px" prop="dealCategory">
<el-select v-model="queryDealRecordParams.dealCategory" placeholder="请选择交易类别" clearable>
<el-option v-for="dict in deal_category" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
<el-form-item label="">
<el-button type="primary" icon="Search" @click="handleDealRecordQuery">搜索</el-button>
<el-button type="info" icon="Refresh" @click="resetDealRecordQuery">重置</el-button>
</el-form-item>
</el-form>
<el-table v-loading="loadingDealRecord" :data="tableDealRecordData">
<el-table-column label="交易时间" align="center" prop="createTime" width="180" />
<el-table-column label="交易金额" align="center" prop="amount" />
<el-table-column label="当前余额" align="center" prop="currentBalance" />
<el-table-column label="交易类型" align="center" width="120" prop="dealType">
<template #default="scope">
<dict-tag :options="deal_type" :value="scope.row.dealType" />
</template>
</el-table-column>
<el-table-column label="交易类别" align="center" width="150" prop="dealCategory">
<template #default="scope">
<dict-tag :options="deal_category" :value="scope.row.dealCategory" />
</template>
</el-table-column>
<el-table-column label="交易子类别" align="center" width="150" prop="childCategoryName" />
<el-table-column label="备注" align="center" width="300" prop="remark" />
</el-table>
<el-pagination small background layout="total, prev, pager, next" :total="dealRecordTotal" @current-change="handleCurrentDealRecordChange" />
</el-dialog>
</div>
</template>
<script setup name="FutureStocks">
import { listFutureStocks, getFutureStocks, delFutureStocks, addFutureStocks, updateFutureStocks } from '@/api/invest/futureStocks'
import { listAccountDealRecord } from '@/api/invest/accountDealRecord'
import { listBankcardLend } from '@/api/invest/bankcardlend'
// eslint-disable-next-line no-unused-vars
import { require } from '@/utils/require'
const { proxy } = getCurrentInstance()
const { future_stock_type, account_status } = proxy.useDict('future_stock_type', 'account_status')
const { deal_type, deal_category } = proxy.useDict('deal_type', 'deal_category')
const futureStocksList = ref([])
const open = ref(false)
@@ -131,9 +173,23 @@ const single = ref(true)
const multiple = ref(true)
const total = ref(0)
const title = ref('')
const titleDealRecord = ref('')
const openDealRecord = ref(false)
const currentAccountId = ref('')
const loadingDealRecord = ref(false)
const tableDealRecordData = ref([])
const dealRecordTotal = ref(0)
const queryDealRecordParams = ref({
time: '',
dealType: null,
dealCategory: null,
pageNum: 1,
pageSize: 10
})
const debitCardList = ref([])
const operateList = ref([
{ id: 'view', icon: 'View', title: '查看', hasPermi: ['invest:futureStocks:query'] },
{ id: 'dealRecord', icon: 'Tickets', title: '交易明细', hasPermi: ['invest:futureStocks:query'] },
{ id: 'edit', icon: 'Edit', title: '修改', hasPermi: ['invest:futureStocks:edit'] },
{ id: 'delete', icon: 'Delete', title: '删除', hasPermi: ['invest:futureStocks:remove'] }
])
@@ -172,6 +228,9 @@ const handleOperate = (operate, row) => {
case 'view':
handleView(row)
break
case 'dealRecord':
handleDealRecord(row)
break
case 'edit':
handleUpdate(row)
break
@@ -298,6 +357,54 @@ function handleUpdate(row) {
})
}
/** 交易明细按钮操作 */
function handleDealRecord(row) {
queryDealRecordParams.value.time = ''
queryDealRecordParams.value.dealType = ''
queryDealRecordParams.value.dealCategory = ''
titleDealRecord.value = `${row.name}${row.code ? '【' + row.code + '】' : ''}交易明细`
currentAccountId.value = row.id || ids.value
getDealRecordList(1)
openDealRecord.value = true
}
const handleCurrentDealRecordChange = (num) => {
getDealRecordList(num)
}
const handleDealRecordQuery = () => {
getDealRecordList(1)
}
const resetDealRecordQuery = () => {
proxy.resetForm('queryDealRecordRef')
queryDealRecordParams.value.time = ''
queryDealRecordParams.value.dealType = ''
queryDealRecordParams.value.dealCategory = ''
dealRecordTotal.value = 0
getDealRecordList(1)
}
const getDealRecordList = (num) => {
loadingDealRecord.value = true
const timeRange = queryDealRecordParams.value.time
let st = ''
let et = ''
if (timeRange && timeRange.length > 0) {
st = timeRange[0]
et = timeRange[1]
}
queryDealRecordParams.value.startTime = st
queryDealRecordParams.value.endTime = et
queryDealRecordParams.value.pageNum = num
queryDealRecordParams.value.accountId = currentAccountId.value
listAccountDealRecord(queryDealRecordParams.value).then((response) => {
loadingDealRecord.value = false
tableDealRecordData.value = response.rows
dealRecordTotal.value = response.total
})
}
/** 提交按钮 */
function submitForm() {
proxy.$refs.futureStocksRef.validate((valid) => {

File diff suppressed because it is too large Load Diff

View File

@@ -67,6 +67,7 @@
<script setup>
import { ElMessageBox } from 'element-plus'
import { getCodeImg, register } from '@/api/login'
import { passwordValidator } from '@/utils/validate'
const router = useRouter()
const { proxy } = getCurrentInstance()
@@ -95,7 +96,7 @@ const registerRules = {
],
password: [
{ required: true, trigger: 'blur', message: '请输入您的密码' },
{ min: 5, max: 20, message: '用户密码长度必须介于 5 和 20 之间', trigger: 'blur' }
{ validator: passwordValidator, trigger: 'blur' }
],
confirmPassword: [
{ required: true, trigger: 'blur', message: '请再次输入您的密码' },
@@ -127,7 +128,7 @@ function handleRegister() {
})
.catch(() => {
loading.value = false
if (captchaEnabled) {
if (captchaEnabled.value) {
getCode()
}
})

View File

@@ -0,0 +1,379 @@
<template>
<div class="app-container">
<div class="search-con" ref="searchHeightRef">
<div class="title">查询条件</div>
<el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="80px">
<el-form-item label="反馈类型" prop="feedbackType">
<el-select v-model="queryParams.feedbackType" placeholder="请选择反馈类型" clearable style="width: 200px">
<el-option v-for="dict in feedbackTypeOptions" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
<el-form-item label="处理状态" prop="status">
<el-select v-model="queryParams.status" placeholder="请选择处理状态" clearable style="width: 200px">
<el-option v-for="dict in feedbackStatusOptions" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
<el-form-item label="反馈来源" prop="source">
<el-select v-model="queryParams.source" placeholder="请选择反馈来源" clearable style="width: 200px">
<el-option v-for="item in sourceOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="提交用户" prop="createBy">
<el-input v-model="queryParams.createBy" placeholder="请输入提交用户" clearable style="width: 200px" @keyup.enter="handleQuery" />
</el-form-item>
</el-form>
<div class="search-btn-con">
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button type="info" icon="Refresh" @click="resetQuery">重置</el-button>
</div>
</div>
<div class="main-con" :style="{ height: mainStyleHeight }">
<div class="title-con">
<div class="title">反馈列表</div>
<div class="operate-btn-con">
<el-button icon="Check" :disabled="single" @click="handleProcess" v-hasPermi="['system:feedback:edit']">标记已处理</el-button>
<el-button icon="Delete" :disabled="multiple" @click="handleDelete" v-hasPermi="['system:feedback:remove']">删除</el-button>
<el-button icon="Download" @click="handleExport" v-hasPermi="['system:feedback:export']">导出</el-button>
</div>
</div>
<div class="content-con" v-loading="loading">
<el-table :data="feedbackList" @selection-change="handleSelectionChange" stripe height="calc(100% - 0.62rem)">
<el-table-column type="selection" width="50" align="center" />
<el-table-column type="index" label="序号" :index="indexMethod" width="60" align="center" />
<el-table-column label="反馈类型" align="center" prop="feedbackType" width="110" />
<el-table-column label="反馈内容" prop="content" min-width="240" :show-overflow-tooltip="true">
<template #default="scope">
<span class="content-text">{{ scope.row.content }}</span>
</template>
</el-table-column>
<el-table-column label="联系方式" align="center" prop="contact" width="140" :show-overflow-tooltip="true" />
<el-table-column label="截图" align="center" width="100">
<template #default="scope">
<el-image
v-if="getImageList(scope.row).length"
class="feedback-thumb"
:src="getImageList(scope.row)[0]"
:preview-src-list="getImageList(scope.row)"
fit="cover"
/>
<span v-else class="empty-text"></span>
</template>
</el-table-column>
<el-table-column label="来源" align="center" prop="source" width="90" />
<el-table-column label="状态" align="center" prop="status" width="100">
<template #default="scope">
<el-tag :type="statusTagType(scope.row.status)">{{ statusText(scope.row.status) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="提交用户" align="center" prop="createBy" width="120" :show-overflow-tooltip="true" />
<el-table-column label="提交时间" align="center" prop="createTime" width="160">
<template #default="scope">
<span>{{ parseTime(scope.row.createTime) }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" fixed="right" width="180" class-name="small-padding fixed-width">
<template #default="scope">
<el-tooltip content="详情" placement="top">
<el-button circle icon="View" @click="handleView(scope.row)" v-hasPermi="['system:feedback:query']"></el-button>
</el-tooltip>
<el-tooltip content="回复" placement="top">
<el-button circle icon="ChatDotRound" @click="handleReply(scope.row)" v-hasPermi="['system:feedback:reply']"></el-button>
</el-tooltip>
<el-tooltip content="删除" placement="top">
<el-button circle icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['system:feedback:remove']"></el-button>
</el-tooltip>
</template>
</el-table-column>
</el-table>
<el-pagination
small
background
layout="total, prev, pager, next"
:total="total"
v-model:current-page="currentPage"
@current-change="handleCurrentChange"
/>
</div>
</div>
<el-dialog title="反馈详情" v-model="detailOpen" width="760px" append-to-body>
<el-descriptions :column="2" border>
<el-descriptions-item label="反馈类型">{{ detailForm.feedbackType }}</el-descriptions-item>
<el-descriptions-item label="处理状态">
<el-tag :type="statusTagType(detailForm.status)">{{ statusText(detailForm.status) }}</el-tag>
</el-descriptions-item>
<el-descriptions-item label="联系方式">{{ detailForm.contact || '未填写' }}</el-descriptions-item>
<el-descriptions-item label="反馈来源">{{ detailForm.source || '-' }}</el-descriptions-item>
<el-descriptions-item label="提交用户">{{ detailForm.createBy || '-' }}</el-descriptions-item>
<el-descriptions-item label="提交时间">{{ parseTime(detailForm.createTime) }}</el-descriptions-item>
<el-descriptions-item label="反馈内容" :span="2">
<div class="detail-content">{{ detailForm.content }}</div>
</el-descriptions-item>
<el-descriptions-item label="截图" :span="2">
<div v-if="getImageList(detailForm).length" class="image-list">
<el-image
v-for="image in getImageList(detailForm)"
:key="image"
class="detail-image"
:src="image"
:preview-src-list="getImageList(detailForm)"
fit="cover"
/>
</div>
<span v-else></span>
</el-descriptions-item>
<el-descriptions-item label="回复内容" :span="2">{{ detailForm.replyContent || '暂无回复' }}</el-descriptions-item>
<el-descriptions-item label="回复人">{{ detailForm.replyBy || '-' }}</el-descriptions-item>
<el-descriptions-item label="回复时间">{{ parseTime(detailForm.replyTime) || '-' }}</el-descriptions-item>
<el-descriptions-item label="备注" :span="2">{{ detailForm.remark || '-' }}</el-descriptions-item>
</el-descriptions>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="handleReply(detailForm)" v-hasPermi="['system:feedback:reply']">回复</el-button>
<el-button @click="detailOpen = false"> </el-button>
</div>
</template>
</el-dialog>
<el-dialog title="回复反馈" v-model="replyOpen" width="600px" append-to-body>
<el-form ref="replyRef" :model="replyForm" :rules="replyRules" label-width="80px">
<el-form-item label="回复内容" prop="replyContent">
<el-input v-model="replyForm.replyContent" type="textarea" :rows="5" maxlength="1000" show-word-limit placeholder="请输入回复内容" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="submitReply"> </el-button>
<el-button @click="replyOpen = false"> </el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup name="Feedback">
import { listFeedback, getFeedback, updateFeedback, replyFeedback, delFeedback } from '@/api/system/feedback'
const { proxy } = getCurrentInstance()
const { feedback_type, feedback_status } = proxy.useDict('feedback_type', 'feedback_status')
const defaultFeedbackTypeOptions = [
{ label: '功能建议', value: '功能建议' },
{ label: '问题反馈', value: '问题反馈' },
{ label: '数据异常', value: '数据异常' },
{ label: '其他', value: '其他' }
]
const defaultFeedbackStatusOptions = [
{ label: '待处理', value: '0' },
{ label: '已处理', value: '1' },
{ label: '已回复', value: '2' }
]
const sourceOptions = [
{ label: '小程序', value: '小程序' },
{ label: 'H5', value: 'H5' },
{ label: 'App', value: 'App' }
]
const feedbackTypeOptions = computed(() => (feedback_type.value && feedback_type.value.length ? feedback_type.value : defaultFeedbackTypeOptions))
const feedbackStatusOptions = computed(() => (feedback_status.value && feedback_status.value.length ? feedback_status.value : defaultFeedbackStatusOptions))
const feedbackList = ref([])
const loading = ref(true)
const showSearch = ref(true)
const ids = ref([])
const single = ref(true)
const multiple = ref(true)
const total = ref(0)
const currentPage = ref(1)
const mainStyleHeight = ref(0)
const searchHeightRef = ref(null)
const detailOpen = ref(false)
const replyOpen = ref(false)
const detailForm = ref({})
const replyForm = ref({})
const data = reactive({
queryParams: {
pageNum: 1,
pageSize: 10,
feedbackType: undefined,
status: undefined,
source: undefined,
createBy: undefined
},
replyRules: {
replyContent: [{ required: true, message: '回复内容不能为空', trigger: 'blur' }]
}
})
const { queryParams, replyRules } = toRefs(data)
function getList() {
loading.value = true
listFeedback(queryParams.value).then((response) => {
feedbackList.value = response.rows
total.value = response.total
loading.value = false
})
}
function handleQuery() {
queryParams.value.pageNum = 1
currentPage.value = 1
getList()
}
function resetQuery() {
proxy.resetForm('queryRef')
handleQuery()
}
function handleSelectionChange(selection) {
ids.value = selection.map((item) => item.id)
single.value = selection.length !== 1
multiple.value = !selection.length
}
function handleView(row) {
getFeedback(row.id).then((response) => {
detailForm.value = response.data || {}
detailOpen.value = true
})
}
function handleReply(row) {
const id = row.id
getFeedback(id).then((response) => {
const data = response.data || row
replyForm.value = {
id: data.id,
replyContent: data.replyContent || ''
}
replyOpen.value = true
})
}
function submitReply() {
proxy.$refs.replyRef.validate((valid) => {
if (valid) {
replyFeedback(replyForm.value).then(() => {
proxy.$modal.msgSuccess('回复成功')
replyOpen.value = false
detailOpen.value = false
getList()
})
}
})
}
function handleProcess(row) {
const id = row.id || ids.value[0]
if (!id) return
proxy.$modal
.confirm('是否确认将该反馈标记为已处理?')
.then(() => updateFeedback({ id, status: '1' }))
.then(() => {
proxy.$modal.msgSuccess('操作成功')
getList()
})
.catch(() => {})
}
function handleDelete(row) {
const feedbackIds = row.id || ids.value
proxy.$modal
.confirm('是否确认删除意见反馈编号为"' + feedbackIds + '"的数据项?')
.then(() => delFeedback(feedbackIds))
.then(() => {
getList()
proxy.$modal.msgSuccess('删除成功')
})
.catch(() => {})
}
function handleExport() {
proxy.download(
'system/feedback/export',
{
...queryParams.value
},
`feedback_${new Date().getTime()}.xlsx`
)
}
const handleCurrentChange = (val) => {
queryParams.value.pageNum = val
getList()
}
const indexMethod = (index) => {
const nowPage = queryParams.value.pageNum
const nowLimit = queryParams.value.pageSize
return index + 1 + (nowPage - 1) * nowLimit
}
function getImageList(row) {
if (!row) return []
if (Array.isArray(row.imageList) && row.imageList.length) {
return row.imageList.filter(Boolean)
}
if (row.images) {
return String(row.images)
.split(',')
.map((item) => item.trim())
.filter(Boolean)
}
return []
}
function statusText(status) {
const text = proxy.selectDictLabel(feedbackStatusOptions.value, status)
return text || '-'
}
function statusTagType(status) {
const map = {
0: 'warning',
1: 'info',
2: 'success'
}
return map[status] || 'info'
}
onMounted(() => {
if (searchHeightRef.value) {
mainStyleHeight.value = `calc(100% - ${(searchHeightRef.value.clientHeight + 10) / 100}rem)`
}
})
getList()
</script>
<style scoped>
.content-text {
display: inline-block;
max-width: 100%;
}
.empty-text {
color: #909399;
}
.feedback-thumb {
width: 48px;
height: 48px;
border-radius: 4px;
}
.detail-content {
line-height: 24px;
white-space: pre-wrap;
}
.image-list {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.detail-image {
width: 96px;
height: 96px;
border-radius: 4px;
}
</style>

View File

@@ -238,6 +238,7 @@
<script setup name="User">
import { getToken } from '@/utils/auth'
import { changeUserStatus, listUser, resetUserPwd, delUser, getUser, updateUser, addUser, deptTreeSelect } from '@/api/system/user'
import { passwordValidator } from '@/utils/validate'
import { onMounted } from 'vue'
const router = useRouter()
const { proxy } = getCurrentInstance()
@@ -304,7 +305,7 @@ const data = reactive({
nickName: [{ required: true, message: '用户昵称不能为空', trigger: 'blur' }],
password: [
{ required: true, message: '用户密码不能为空', trigger: 'blur' },
{ min: 5, max: 20, message: '用户密码长度必须介于 5 和 20 之间', trigger: 'blur' }
{ validator: passwordValidator, trigger: 'blur' }
],
email: [{ type: 'email', message: '请输入正确的邮箱地址', trigger: ['blur', 'change'] }],
phonenumber: [{ pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/, message: '请输入正确的手机号码', trigger: 'blur' }]
@@ -418,8 +419,8 @@ function handleResetPwd(row) {
confirmButtonText: '确定',
cancelButtonText: '取消',
closeOnClickModal: false,
inputPattern: /^.{5,20}$/,
inputErrorMessage: '用户密码长度必须介于 5 和 20 之间'
inputPattern: /^(?=.*[A-Z])(?=.*[a-z])(?=.*[0-9])(?=.*[^A-Za-z0-9\s])\S{8,20}$/,
inputErrorMessage: '密码需 8-20 位,且须同时含大小写字母、数字和特殊字符(不能含空格)'
})
.then(({ value }) => {
resetUserPwd(row.userId, value).then((response) => {
@@ -507,7 +508,7 @@ function handleUpdate(row) {
form.value.roleIds = response.roleIds
open.value = true
title.value = '修改用户'
form.password = ''
form.value.password = ''
})
}
/** 提交按钮 */

View File

@@ -18,6 +18,7 @@
<script setup>
import { updateUserPwd } from '@/api/system/user'
import { passwordValidator } from '@/utils/validate'
const { proxy } = getCurrentInstance()
@@ -38,7 +39,7 @@ const rules = ref({
oldPassword: [{ required: true, message: '旧密码不能为空', trigger: 'blur' }],
newPassword: [
{ required: true, message: '新密码不能为空', trigger: 'blur' },
{ min: 6, max: 20, message: '长度在 6 到 20 个字符', trigger: 'blur' }
{ validator: passwordValidator, trigger: 'blur' }
],
confirmPassword: [
{ required: true, message: '确认密码不能为空', trigger: 'blur' },

View File

@@ -42,8 +42,8 @@ export default defineConfig(({ mode, command }) => {
proxy: {
// https://cn.vitejs.dev/config/#server-proxy
'/dev-api': {
target: 'https://www.qdintc.com/prod-api/',
// target: 'http://127.0.0.1:8080',
// target: 'https://www.qdintc.com/prod-api/',
target: 'http://127.0.0.1:8080',
changeOrigin: true,
rewrite: (p) => p.replace(/^\/dev-api/, '')
},