<template>
|
<div class="complaint-manage">
|
<!-- 餐饮店铺信访投诉卡片 -->
|
<el-card class="mb-4">
|
<template #header>
|
<div class="card-header">
|
<span>餐饮店铺信访投诉</span>
|
<div class="filter-group">
|
<FYOptionTime
|
class="m-r-8"
|
:initValue="false"
|
type="daterange"
|
v-model:value="complaintDateRange"
|
style="width: 300px; margin-bottom: 0px"
|
:shortcuts="shortcuts"
|
></FYOptionTime>
|
<el-button type="success" icon="Plus" @click="addComplaint">新增投诉</el-button>
|
<el-button type="info" icon="Upload" @click="importComplaint">批量导入</el-button>
|
</div>
|
</div>
|
</template>
|
|
<!-- 图表展示 -->
|
<div class="chart-container">
|
<div class="chart-item">
|
<div class="chart-header">
|
<h3>投诉数趋势</h3>
|
<div class="chart-summary">投诉总数: {{ complaintStats.totalCount }}</div>
|
</div>
|
<div ref="dailyComplaintChart" class="chart"></div>
|
</div>
|
<div class="chart-item">
|
<h3>投诉来源分布</h3>
|
<div ref="sourceComplaintChart" class="chart"></div>
|
</div>
|
</div>
|
|
<!-- 投诉记录表格 -->
|
<el-table
|
:data="filteredComplaintData"
|
table-layout="fixed"
|
:show-overflow-tooltip="true"
|
height="400px"
|
border
|
>
|
<el-table-column prop="shopName" label="投诉店铺" />
|
<el-table-column prop="complaintReason" label="投诉原因" width="180" />
|
<el-table-column prop="complaintRequest" label="投诉诉求" width="180" />
|
<el-table-column prop="complaintTime" label="投诉时间" width="180" />
|
<el-table-column prop="complaintSource" label="投诉来源" width="150" />
|
<el-table-column prop="handlingDepartment" label="处理部门" width="150" />
|
<el-table-column prop="complaintResult" label="投诉结果" width="150" />
|
<el-table-column width="250">
|
<template #header>
|
<el-input v-model="complaintKeyword" placeholder="关键字搜索" style="width: 120px" />
|
</template>
|
<template #default="scope">
|
<el-button size="small" type="primary" @click="editComplaint(scope.row)"
|
>编辑</el-button
|
>
|
<el-button size="small" type="danger" @click="deleteComplaint(scope.row.id)"
|
>删除</el-button
|
>
|
</template>
|
</el-table-column>
|
</el-table>
|
|
<!-- 分页 -->
|
<div class="pagination-container">
|
<el-pagination
|
v-model:current-page="complaintPagination.currentPage"
|
v-model:page-size="complaintPagination.pageSize"
|
:page-sizes="[10, 20, 50, 100]"
|
layout="total, sizes, prev, pager, next, jumper"
|
:total="complaintPagination.total"
|
@size-change="handleComplaintSizeChange"
|
@current-change="handleComplaintCurrentChange"
|
/>
|
</div>
|
</el-card>
|
|
<!-- 新增/编辑投诉对话框 -->
|
<el-dialog v-model="complaintDialogVisible" title="投诉信息" width="600px">
|
<el-form :model="complaintForm" label-width="100px">
|
<el-form-item label="投诉店铺">
|
<el-input v-model="complaintForm.shopName" />
|
</el-form-item>
|
<el-form-item label="投诉原因">
|
<el-input v-model="complaintForm.complaintReason" />
|
</el-form-item>
|
<el-form-item label="投诉诉求">
|
<el-input v-model="complaintForm.complaintRequest" type="textarea" />
|
</el-form-item>
|
<el-form-item label="投诉时间">
|
<el-date-picker v-model="complaintForm.complaintTime" type="datetime" />
|
</el-form-item>
|
<el-form-item label="投诉来源">
|
<el-input v-model="complaintForm.complaintSource" />
|
</el-form-item>
|
<el-form-item label="处理部门">
|
<el-input v-model="complaintForm.handlingDepartment" />
|
</el-form-item>
|
<el-form-item label="投诉结果">
|
<el-input v-model="complaintForm.complaintResult" />
|
</el-form-item>
|
</el-form>
|
<template #footer>
|
<span class="dialog-footer">
|
<el-button @click="complaintDialogVisible = false">取消</el-button>
|
<el-button type="primary" @click="saveComplaint">保存</el-button>
|
</span>
|
</template>
|
</el-dialog>
|
|
<!-- 投诉批量导入对话框 -->
|
<el-dialog v-model="complaintImportDialogVisible" title="投诉批量导入" width="600px">
|
<div class="import-container">
|
<p class="import-tip">请选择要导入的Excel文件</p>
|
<el-upload
|
class="upload-demo"
|
action="#"
|
drag
|
:auto-upload="false"
|
:on-change="handleComplaintFileChange"
|
:file-list="complaintImportFileList"
|
accept=".xlsx,.xls"
|
:limit="1"
|
:on-exceed="handleExceed"
|
>
|
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
|
<div class="el-upload__text">拖动文件或<em>点击上传</em></div>
|
<template #tip>
|
<div class="el-upload__tip">只能上传Excel文件,且不超过5MB</div>
|
</template>
|
</el-upload>
|
</div>
|
<template #footer>
|
<span class="dialog-footer">
|
<el-button @click="complaintImportDialogVisible = false">取消</el-button>
|
<el-button type="primary" @click="confirmComplaintImport">导入</el-button>
|
</span>
|
</template>
|
</el-dialog>
|
</div>
|
</template>
|
|
<script setup>
|
import { ref, onMounted, onUnmounted, watch, computed } from 'vue'
|
import * as echarts from 'echarts'
|
import dayjs from 'dayjs'
|
import { ElMessage } from 'element-plus'
|
|
// 时间范围快捷选项
|
const dayStart = dayjs('2025-08-01').startOf('date')
|
const dayEnd = dayStart.endOf('date')
|
const shortcuts = [
|
{
|
text: '今天',
|
value: [dayStart.toDate(), dayEnd.toDate()],
|
},
|
{
|
text: '本周',
|
value: [dayStart.startOf('week').toDate(), dayEnd.endOf('week').toDate()],
|
},
|
{
|
text: '上周',
|
value: [dayStart.day(-7).toDate(), dayEnd.day(-1).toDate()],
|
},
|
{
|
text: '本月',
|
value: [dayStart.startOf('month').toDate(), dayEnd.endOf('month').toDate()],
|
},
|
{
|
text: '上月',
|
value: [
|
dayStart.subtract(1, 'month').startOf('month').toDate(),
|
dayEnd.subtract(1, 'month').endOf('month').toDate(),
|
],
|
},
|
{
|
text: '本季度',
|
value: [dayStart.startOf('quarter').toDate(), dayEnd.endOf('quarter').toDate()],
|
},
|
{
|
text: '上季度',
|
value: [
|
dayStart.subtract(1, 'quarter').startOf('quarter').toDate(),
|
dayEnd.subtract(1, 'quarter').endOf('quarter').toDate(),
|
],
|
},
|
{
|
text: '去年',
|
value: [
|
dayStart.subtract(1, 'year').startOf('year').toDate(),
|
dayEnd.subtract(1, 'year').endOf('year').toDate(),
|
],
|
},
|
{
|
text: '今年',
|
value: [dayStart.startOf('year').toDate(), dayEnd.endOf('year').toDate()],
|
},
|
]
|
|
// 图表引用
|
const dailyComplaintChart = ref(null)
|
const sourceComplaintChart = ref(null)
|
|
// 信访投诉数据
|
const complaintDateRange = ref([dayStart.startOf('month').toDate(), dayEnd.endOf('month').toDate()])
|
const complaintKeyword = ref('')
|
const complaintStats = ref({
|
totalCount: 85,
|
})
|
const complaintTableData = ref([])
|
const complaintPagination = ref({
|
currentPage: 1,
|
pageSize: 10,
|
total: 0,
|
})
|
|
// 过滤后的投诉数据
|
const filteredComplaintData = computed(() => {
|
if (!complaintKeyword.value) {
|
return complaintTableData.value
|
}
|
const keyword = complaintKeyword.value.toLowerCase()
|
return complaintTableData.value.filter((item) => {
|
return Object.values(item).some((value) => {
|
return String(value).toLowerCase().includes(keyword)
|
})
|
})
|
})
|
|
// 对话框状态
|
const complaintDialogVisible = ref(false)
|
const complaintImportDialogVisible = ref(false)
|
|
// 导入文件列表
|
const complaintImportFileList = ref([])
|
|
// 表单数据
|
const complaintForm = ref({
|
id: '',
|
shopName: '',
|
complaintReason: '',
|
complaintRequest: '',
|
complaintTime: '',
|
complaintSource: '',
|
handlingDepartment: '',
|
complaintResult: '',
|
})
|
|
const searchComplaint = () => {
|
// 模拟搜索投诉数据
|
console.log('搜索投诉数据', {
|
dateRange: complaintDateRange.value,
|
keyword: complaintKeyword.value,
|
})
|
|
// 计算时间范围
|
const startTime = complaintDateRange.value[0]
|
const endTime = complaintDateRange.value[1]
|
const startDate = dayjs(startTime)
|
const endDate = dayjs(endTime)
|
const daysDiff = endDate.diff(startDate, 'day')
|
const months = daysDiff / 30
|
|
// 生成新的模拟数据
|
const totalCount = Math.floor(20 * months) + Math.floor(Math.random() * 5)
|
complaintStats.value = {
|
totalCount,
|
}
|
|
// 生成新的投诉记录
|
const newData = []
|
const shopNames = [
|
'鲜味馆',
|
'烧烤达人',
|
'味美餐厅',
|
'香辣小龙虾',
|
'川菜馆',
|
'西餐厅',
|
'日料店',
|
'火锅店',
|
]
|
const complaintReasons = ['油烟扰民', '夜间噪声', '异味污染', '卫生问题']
|
const sources = ['12345热线', '居民投诉', '网络平台', '其他']
|
const departments = ['徐汇区生态环境局', '长宁区生态环境局', '静安区生态环境局', '普陀区生态环境局']
|
// const results = ['已处理', '处理中', '未处理']
|
const results = ['已处理']
|
|
for (let i = 0; i < totalCount; i++) {
|
// 生成在时间范围内的随机时间
|
const randomDays = Math.floor(Math.random() * (daysDiff + 1))
|
const randomTime = startDate
|
.add(randomDays, 'day')
|
.add(Math.floor(Math.random() * 24), 'hour')
|
.add(Math.floor(Math.random() * 60), 'minute')
|
|
newData.push({
|
id: i + 1,
|
shopName: shopNames[Math.floor(Math.random() * shopNames.length)],
|
complaintReason: complaintReasons[Math.floor(Math.random() * complaintReasons.length)],
|
complaintRequest:
|
'要求整改' + complaintReasons[Math.floor(Math.random() * complaintReasons.length)],
|
complaintTime: randomTime.format('YYYY-MM-DD HH:mm'),
|
complaintSource: sources[Math.floor(Math.random() * sources.length)],
|
handlingDepartment: departments[Math.floor(Math.random() * departments.length)],
|
complaintResult: results[Math.floor(Math.random() * results.length)],
|
})
|
}
|
|
complaintTableData.value = newData
|
complaintPagination.value.total = newData.length
|
// 重新初始化图表以更新数据
|
initCharts()
|
}
|
|
// 监听投诉日期范围变化
|
watch(
|
() => complaintDateRange.value,
|
() => {
|
searchComplaint()
|
},
|
{ deep: true },
|
)
|
|
const addComplaint = () => {
|
// 重置表单
|
complaintForm.value = {
|
id: '',
|
shopName: '',
|
complaintReason: '',
|
complaintRequest: '',
|
complaintTime: '',
|
complaintSource: '',
|
handlingDepartment: '',
|
complaintResult: '',
|
}
|
complaintDialogVisible.value = true
|
}
|
|
const editComplaint = (row) => {
|
// 填充表单
|
complaintForm.value = { ...row }
|
complaintDialogVisible.value = true
|
}
|
|
const saveComplaint = () => {
|
// 模拟保存数据
|
console.log('保存投诉数据', complaintForm.value)
|
complaintDialogVisible.value = false
|
// 这里可以添加实际的数据保存逻辑
|
}
|
|
const deleteComplaint = (id) => {
|
// 模拟删除数据
|
console.log('删除投诉数据', id)
|
// 这里可以添加实际的数据删除逻辑
|
}
|
|
const importComplaint = () => {
|
// 打开导入对话框
|
complaintImportFileList.value = []
|
complaintImportDialogVisible.value = true
|
}
|
|
const handleComplaintFileChange = (file, fileList) => {
|
complaintImportFileList.value = fileList
|
console.log('选择的投诉文件:', file)
|
}
|
|
const confirmComplaintImport = () => {
|
if (complaintImportFileList.value.length === 0) {
|
ElMessage.warning('请选择要导入的文件')
|
return
|
}
|
|
const file = complaintImportFileList.value[0]
|
console.log('开始导入投诉数据:', file.name)
|
|
// 预留导入逻辑
|
// 这里将实现实际的文件解析和数据导入
|
|
complaintImportDialogVisible.value = false
|
ElMessage.success('导入操作已触发,预留导入逻辑')
|
}
|
|
const handleExceed = (files, fileList) => {
|
ElMessage.warning('只能上传一个文件')
|
}
|
|
const handleComplaintSizeChange = (size) => {
|
complaintPagination.value.pageSize = size
|
// 这里可以添加实际的分页逻辑
|
}
|
|
const handleComplaintCurrentChange = (current) => {
|
complaintPagination.value.currentPage = current
|
// 这里可以添加实际的分页逻辑
|
}
|
|
// 初始化图表
|
const initCharts = () => {
|
// 每日投诉数量图
|
if (dailyComplaintChart.value && complaintDateRange.value.length > 0) {
|
const chart = echarts.init(dailyComplaintChart.value)
|
|
// 计算时间范围并生成x轴数据
|
const startTime = complaintDateRange.value[0]
|
const endTime = complaintDateRange.value[1]
|
const startDate = dayjs(startTime)
|
const endDate = dayjs(endTime)
|
const daysDiff = endDate.diff(startDate, 'day')
|
|
let xAxisData = []
|
let seriesData = []
|
|
// 处理投诉数据
|
const complaintData = complaintTableData.value
|
const dateFormat = daysDiff <= 30 ? 'MM/DD' : 'YYYY/MM'
|
|
// 生成日期范围
|
if (daysDiff <= 30) {
|
// 同一个月内,按日显示
|
for (let i = 0; i <= daysDiff; i++) {
|
const date = startDate.add(i, 'day')
|
xAxisData.push(date.format(dateFormat))
|
// 计算该日期的投诉数量
|
const count = complaintData.filter((item) => {
|
const itemDate = dayjs(item.complaintTime)
|
return itemDate.format(dateFormat) === date.format(dateFormat)
|
}).length
|
seriesData.push(count)
|
}
|
} else {
|
// 超过一个月,按月显示
|
const startMonth = startDate.startOf('month')
|
const endMonth = endDate.endOf('month')
|
const monthsDiff = endMonth.diff(startMonth, 'month')
|
|
for (let i = 0; i <= monthsDiff; i++) {
|
const date = startMonth.add(i, 'month')
|
xAxisData.push(date.format(dateFormat))
|
// 计算该月份的投诉数量
|
const count = complaintData.filter((item) => {
|
const itemDate = dayjs(item.complaintTime)
|
return itemDate.format(dateFormat) === date.format(dateFormat)
|
}).length
|
seriesData.push(count)
|
}
|
}
|
|
chart.setOption({
|
xAxis: {
|
type: 'category',
|
data: xAxisData,
|
},
|
yAxis: {
|
type: 'value',
|
},
|
series: [
|
{
|
data: seriesData,
|
type: 'bar',
|
},
|
],
|
})
|
}
|
|
// 投诉来源分布图
|
if (sourceComplaintChart.value) {
|
const chart = echarts.init(sourceComplaintChart.value)
|
chart.setOption({
|
series: [
|
{
|
type: 'pie',
|
data: [
|
{ value: 40, name: '12345热线' },
|
{ value: 25, name: '居民投诉' },
|
{ value: 15, name: '网络平台' },
|
{ value: 5, name: '其他' },
|
],
|
label: {
|
show: true,
|
formatter: '{b}: {d}%',
|
},
|
},
|
],
|
})
|
}
|
}
|
|
// 监听窗口大小变化
|
const handleResize = () => {
|
// 重新调整图表大小
|
if (dailyComplaintChart.value) {
|
echarts.init(dailyComplaintChart.value).resize()
|
}
|
if (sourceComplaintChart.value) {
|
echarts.init(sourceComplaintChart.value).resize()
|
}
|
}
|
|
// 生命周期
|
onMounted(() => {
|
searchComplaint()
|
initCharts()
|
window.addEventListener('resize', handleResize)
|
})
|
|
// 组件卸载时清理事件监听
|
onUnmounted(() => {
|
cleanup()
|
})
|
|
// 清理
|
const cleanup = () => {
|
window.removeEventListener('resize', handleResize)
|
}
|
</script>
|
|
<style scoped>
|
.complaint-manage {
|
padding: 20px;
|
background-color: #f5f7fa;
|
min-height: 100vh;
|
}
|
|
.card-header {
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
}
|
|
.filter-group {
|
display: flex;
|
align-items: center;
|
}
|
|
.chart-container {
|
display: flex;
|
gap: 20px;
|
margin-bottom: 30px;
|
}
|
|
.chart-item {
|
flex: 1;
|
background-color: #fff;
|
padding: 20px;
|
border-radius: 8px;
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
}
|
|
.chart-item h3 {
|
margin-bottom: 15px;
|
font-size: 16px;
|
font-weight: 600;
|
color: #303133;
|
}
|
|
.chart-header {
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
margin-bottom: 15px;
|
}
|
|
.chart-summary {
|
font-size: 14px;
|
color: #606266;
|
}
|
|
.chart {
|
height: 300px;
|
}
|
|
.pagination-container {
|
margin-top: 20px;
|
display: flex;
|
justify-content: flex-end;
|
}
|
|
.import-container {
|
padding: 20px;
|
}
|
|
.import-tip {
|
margin-bottom: 20px;
|
color: #606266;
|
}
|
|
.dialog-footer {
|
display: flex;
|
justify-content: flex-end;
|
}
|
</style>
|