feat: 新增意见反馈功能。

This commit is contained in:
tianyongbao
2026-07-13 09:27:42 +08:00
parent d84e81bf4c
commit 4ccfc9856c
6 changed files with 725 additions and 11 deletions

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) { function getFileName(name) {
if (name.lastIndexOf('/') > -1) { const fileName = String(name || '').split('?')[0]
return name.slice(name.lastIndexOf('/') + 1) if (!fileName) {
} else {
return '' return ''
} }
if (fileName.lastIndexOf('/') > -1) {
return decodeURIComponent(fileName.slice(fileName.lastIndexOf('/') + 1))
}
return decodeURIComponent(fileName)
} }
// 对象转成指定字符串分隔 // 对象转成指定字符串分隔

View File

@@ -50,6 +50,28 @@
<el-table-column label="活动时长" align="center" prop="exerciseTimeStr" width="150"> </el-table-column> <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="成员" align="center" prop="partner" />
<el-table-column label="总费用(元)" width="120" align="center" prop="totalCost" /> <el-table-column label="总费用(元)" width="120" align="center" prop="totalCost" />
<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"> <el-table-column label="操作" width="180" align="center" class-name="small-padding fixed-width">
<template v-slot="scope"> <template v-slot="scope">
<div class="ctrl-btn d-flex"> <div class="ctrl-btn d-flex">
@@ -115,6 +137,22 @@
<el-form-item label="收获" style="width: 792px" prop="harvest"> <el-form-item label="收获" style="width: 792px" prop="harvest">
<el-input v-model="form.harvest" type="textarea" placeholder="请输入收获" /> <el-input v-model="form.harvest" type="textarea" placeholder="请输入收获" />
</el-form-item> </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-form-item label="备注" style="width: 792px" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入内容" /> <el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
</el-form-item> </el-form-item>
@@ -190,6 +228,7 @@ const expenseList = ref([])
const selectedExpenses = ref([]) const selectedExpenses = ref([])
const currentActivityId = ref(null) const currentActivityId = ref(null)
const summaryTotal = ref(0) const summaryTotal = ref(0)
const attachmentFileTypes = ['jpg', 'jpeg', 'png', 'pdf', 'doc', 'docx', 'xls', 'xlsx']
const operateList = ref([ const operateList = ref([
{ id: 'view', icon: 'View', title: '查看', hasPermi: ['health:activity:query'] }, { id: 'view', icon: 'View', title: '查看', hasPermi: ['health:activity:query'] },
{ id: 'edit', icon: 'Edit', title: '修改', hasPermi: ['health:activity:edit'] }, { id: 'edit', icon: 'Edit', title: '修改', hasPermi: ['health:activity:edit'] },
@@ -240,6 +279,39 @@ const handleOperate = (operate, row) => {
const { queryParams, form, rules } = toRefs(data) 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() { function getList() {
loading.value = true loading.value = true
@@ -277,7 +349,8 @@ function reset() {
remark: null, remark: null,
totalCost: 0, totalCost: 0,
partner: null, partner: null,
costDetail: null costDetail: null,
attachment: null
} }
proxy.resetForm('activityRef') proxy.resetForm('activityRef')
} }
@@ -501,3 +574,23 @@ function confirmSummary() {
getList() getList()
</script> </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

@@ -63,6 +63,28 @@
<el-table-column label="就诊时间" align="center" prop="visitingTime" width="160"> </el-table-column> <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" width="120" prop="totalCost" />
<el-table-column label="诊断结果" align="center" prop="diagnosis" /> <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"> <el-table-column label="操作" align="center" width="180" class-name="small-padding fixed-width">
<template v-slot="scope"> <template v-slot="scope">
<div class="ctrl-btn d-flex"> <div class="ctrl-btn d-flex">
@@ -132,6 +154,22 @@
<el-form-item label="费用明细" style="width: 792px" prop="costDetail"> <el-form-item label="费用明细" style="width: 792px" prop="costDetail">
<el-input v-model="form.costDetail" type="textarea" placeholder="请输入费用明细" /> <el-input v-model="form.costDetail" type="textarea" placeholder="请输入费用明细" />
</el-form-item> </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-form-item label="备注" style="width: 792px" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入内容" /> <el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
</el-form-item> </el-form-item>
@@ -287,6 +325,7 @@ const total = ref(0)
const title = ref('') const title = ref('')
const personList = ref([]) const personList = ref([])
const healthRecordList = ref([]) const healthRecordList = ref([])
const attachmentFileTypes = ['jpg', 'jpeg', 'png', 'pdf', 'doc', 'docx', 'xls', 'xlsx']
const detailOpen = ref(false) const detailOpen = ref(false)
const openDetail = 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) 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) => { const handlePersonChange = (personId) => {
queryHealthRecordParams.personId = personId queryHealthRecordParams.value.personId = personId
listHealthRecord(queryHealthRecordParams).then((response) => { listHealthRecord(queryHealthRecordParams).then((response) => {
healthRecordList.value = response.rows healthRecordList.value = response.rows
if (response.rows.length > 0) { if (response.rows.length > 0) {
@@ -405,7 +477,7 @@ const handlePersonChange = (personId) => {
}) })
} }
const handleQueryPersonChange = (personId) => { const handleQueryPersonChange = (personId) => {
queryHealthRecordParams.personId = personId queryHealthRecordParams.value.personId = personId
queryParams.value.healthRecordId = null queryParams.value.healthRecordId = null
listHealthRecord(queryHealthRecordParams).then((response) => { listHealthRecord(queryHealthRecordParams).then((response) => {
healthRecordList.value = response.rows healthRecordList.value = response.rows
@@ -780,7 +852,8 @@ function reset() {
personId: null, personId: null,
totalCost: 0, totalCost: 0,
partner: null, partner: null,
costDetail: null costDetail: null,
attachment: null
} }
proxy.resetForm('doctorRecordRef') proxy.resetForm('doctorRecordRef')
} }
@@ -825,7 +898,7 @@ function handleSelectionChange(selection) {
// 查看 // 查看
const handleView = (row) => { const handleView = (row) => {
title.value = '查看就医记录' title.value = '查看就医记录'
queryHealthRecordParams.personId = row.personId queryHealthRecordParams.value.personId = row.personId
listHealthRecord(queryHealthRecordParams).then((response) => { listHealthRecord(queryHealthRecordParams).then((response) => {
healthRecordList.value = response.rows healthRecordList.value = response.rows
}) })
@@ -839,7 +912,7 @@ function handleAdd() {
form.value.visitingTime = dayjs(new Date().getTime()).format('YYYY-MM-DD HH:mm:ss') form.value.visitingTime = dayjs(new Date().getTime()).format('YYYY-MM-DD HH:mm:ss')
if (personList.value.length > 0) { if (personList.value.length > 0) {
form.value.personId = personList.value[0].id form.value.personId = personList.value[0].id
queryHealthRecordParams.personId = personList.value[0].id queryHealthRecordParams.value.personId = personList.value[0].id
listHealthRecord(queryHealthRecordParams).then((response) => { listHealthRecord(queryHealthRecordParams).then((response) => {
healthRecordList.value = response.rows healthRecordList.value = response.rows
if (response.rows.length > 0) { if (response.rows.length > 0) {
@@ -854,7 +927,7 @@ function handleAdd() {
/** 修改按钮操作 */ /** 修改按钮操作 */
function handleUpdate(row) { function handleUpdate(row) {
reset() reset()
queryHealthRecordParams.personId = row.personId queryHealthRecordParams.value.personId = row.personId
listHealthRecord(queryHealthRecordParams).then((response) => { listHealthRecord(queryHealthRecordParams).then((response) => {
healthRecordList.value = response.rows healthRecordList.value = response.rows
}) })
@@ -916,3 +989,23 @@ function handleExport() {
getList() getList()
getPersonList() getPersonList()
</script> </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

@@ -79,6 +79,28 @@
<el-table-column label="包装" align="center" prop="packaging" width="150" /> <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="brand" width="120" />
<el-table-column label="生产厂家" align="center" prop="manufacturers" /> <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"> <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template v-slot="scope"> <template v-slot="scope">
@@ -190,6 +212,22 @@
<el-form-item label="不良反应" prop="adverseReaction" style="width: 1200px"> <el-form-item label="不良反应" prop="adverseReaction" style="width: 1200px">
<el-input v-model="form.adverseReaction" type="textarea" placeholder="请输入不良反应" /> <el-input v-model="form.adverseReaction" type="textarea" placeholder="请输入不良反应" />
</el-form-item> </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-form-item label="备注" style="width: 1200px" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入内容" /> <el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
</el-form-item> </el-form-item>
@@ -229,6 +267,7 @@ const single = ref(true)
const multiple = ref(true) const multiple = ref(true)
const total = ref(0) const total = ref(0)
const title = ref('') const title = ref('')
const attachmentFileTypes = ['jpg', 'jpeg', 'png', 'pdf', 'doc', 'docx', 'xls', 'xlsx']
const operateList = ref([ const operateList = ref([
{ id: 'view', icon: 'View', title: '查看', hasPermi: ['health:medicineBasic:query'] }, { id: 'view', icon: 'View', title: '查看', hasPermi: ['health:medicineBasic:query'] },
{ id: 'edit', icon: 'Edit', title: '修改', hasPermi: ['health:medicineBasic:edit'] }, { id: 'edit', icon: 'Edit', title: '修改', hasPermi: ['health:medicineBasic:edit'] },
@@ -289,6 +328,39 @@ const handleOperate = (operate, row) => {
const { queryParams, form, rules } = toRefs(data) 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() { function getList() {
loading.value = true loading.value = true
@@ -347,7 +419,8 @@ function reset() {
contentUnit: null, contentUnit: null,
character: null, character: null,
storage: null, storage: null,
indications: null indications: null,
attachment: null
} }
proxy.resetForm('medicineBasicRef') proxy.resetForm('medicineBasicRef')
} }
@@ -463,3 +536,23 @@ function handleExport() {
getList() getList()
</script> </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,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>