Browse Source

feat:投诉举报添加时信件合并

master
buaixuexideshitongxue 4 weeks ago
parent
commit
653834fcc1
  1. 20
      src/api/data/complaintCollection.ts
  2. 236
      src/components/data/MergeHistoryDialog.vue
  3. 628
      src/components/data/complaintformdialog.vue
  4. 39
      src/components/negative/description.vue

20
src/api/data/complaintCollection.ts

@ -114,3 +114,23 @@ export function initialReview(body) {
}); });
} }
/**
*
*/
export function mergeComplaintCollection(body) {
return request.post({
url: `/data/complaintCollection/mergeComplaintCollection`,
body
});
}
/**
*
*/
export function getMergeHistory(body) {
return request.post({
url: `/data/complaintCollection/getMergeHistory`,
body
});
}

236
src/components/data/MergeHistoryDialog.vue

@ -0,0 +1,236 @@
<template>
<el-dialog
title="合并历史"
v-model="visibleProxy"
width="1400px"
top="8vh"
append-to-body
destroy-on-close
>
<div class="merge-history-container">
<el-empty v-if="!mergeHistory || !mergeHistory.merges || mergeHistory.merges.length === 0" description="暂无合并记录" />
<div v-else class="merge-list">
<div v-for="(item, index) in mergeHistory.merges" :key="index" class="merge-item">
<div class="merge-header">
<div class="source-tag">{{ formatSource(item) }}</div>
<div class="merge-time">合并时间{{ item.mergeTime || '-' }}</div>
</div>
<div class="merge-content">
<div class="merge-row">
<span class="label">编号</span>
<span class="value">{{ item.originId || '-' }}</span>
</div>
<div class="merge-row">
<span class="label">受理时间</span>
<span class="value">{{ formatTime(item.discoveryTime) }}</span>
</div>
<div class="merge-row">
<span class="label">业务类别</span>
<span class="value">{{ getDictLabel('businessType', item.businessTypeCode) }}</span>
</div>
<div class="merge-row">
<span class="label">被投诉机构</span>
<span class="value">{{ item.secondDepartName || '-' }}</span>
</div>
<div class="merge-row">
<span class="label">举报人</span>
<span class="value">{{ item.responderName || '-' }}</span>
</div>
<div class="merge-row">
<span class="label">身份证</span>
<span class="value">{{ item.responderIdCode || '-' }}</span>
</div>
<div class="merge-row">
<span class="label">电话</span>
<span class="value">{{ item.responderPhone || '-' }}</span>
</div>
<div class="merge-row">
<span class="label">附件</span>
<span class="value">
<template v-if="item.files && item.files.length > 0">
<span
v-for="(file, fi) in item.files"
:key="fi"
class="file-link"
@click="previewFile(file)"
>
{{ file.fileName || '附件' + (fi + 1) }}
</span>
</template>
<template v-else>-</template>
</span>
</div>
<div class="merge-row full-width">
<span class="label">内容</span>
<el-tooltip :content="item.thingDesc || '-'" placement="top" :disabled="!item.thingDesc">
<span class="value thing-desc">{{ item.thingDesc || '-' }}</span>
</el-tooltip>
</div>
</div>
</div>
</div>
</div>
</el-dialog>
</template>
<script setup>
import { computed } from 'vue'
import feedback from "@/utils/feedback";
import useCatchStore from "@/stores/modules/catch";
const catchStore = useCatchStore()
const props = defineProps({
modelValue: {
type: Boolean,
default: false
},
mergeHistory: {
type: Object,
default: null
}
})
const emit = defineEmits(['update:modelValue'])
const visibleProxy = computed({
get: () => props.modelValue,
set: (v) => emit('update:modelValue', v)
})
const dict = catchStore.getDicts(['sfssSourceTable', 'businessType'])
//
const getDictLabel = (dictType, value) => {
if (!value) return '-'
const dictList = dict[dictType] || []
const item = dictList.find(d => d.dictValue === value || d.dictValue == value)
return item ? item.dictLabel : value
}
const formatSource = (item) => {
const source = getDictLabel('sfssSourceTable', item.sourceTable)
const subOne = getDictLabel('sfssSourceTable', item.sourceTableSubOne)
if (!item.sourceTable) return '未知来源'
if (item.sourceTableSubOne) {
return `${source} / ${subOne}`
}
return source
}
const previewFile = (file) => {
if (!file.filePath) {
feedback.msgWarning('文件路径不存在')
return
}
const link = document.createElement('a')
link.href = file.filePath
link.download = file.fileName || '附件'
link.target = '_blank'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
}
const formatTime = (time) => {
if (!time) return '-'
// ISO
try {
const date = new Date(time)
const pad = n => n.toString().padStart(2, '0')
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`
} catch {
return time
}
}
</script>
<style scoped>
.merge-history-container {
min-height: 500px;
max-height: 75vh;
overflow-y: auto;
}
.merge-list {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
}
.merge-item {
border: 1px solid #e4e7ed;
border-radius: 6px;
overflow: hidden;
}
.merge-header {
background: linear-gradient(135deg, #f5f7fa 0%, #e8ecf1 100%);
padding: 12px 16px;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid #e4e7ed;
}
.source-tag {
font-weight: bold;
font-size: 13px;
color: #409eff;
}
.merge-time {
font-size: 12px;
color: #909399;
}
.merge-content {
padding: 14px 16px;
display: flex;
flex-wrap: wrap;
gap: 6px 0;
}
.merge-row {
display: flex;
width: 50%;
}
.merge-row.full-width {
width: 100%;
}
.merge-row .label {
color: #606266;
flex-shrink: 0;
min-width: 70px;
font-weight: 500;
}
.merge-row .value {
color: #303133;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: normal;
word-break: break-all;
}
.file-link {
color: #409eff;
cursor: pointer;
margin-right: 8px;
}
.file-link:hover {
text-decoration: underline;
}
.thing-desc {
color: #606266;
font-size: 13px;
white-space: pre-wrap;
word-break: break-all;
}
</style>

628
src/components/data/complaintformdialog.vue

@ -2,20 +2,29 @@
<el-dialog <el-dialog
:title="mode === 'add' ? '添加登记来件' : '修改登记来件'" :title="mode === 'add' ? '添加登记来件' : '修改登记来件'"
v-model="visibleProxy" v-model="visibleProxy"
width="70vw" width="75vw"
top="2vh" top="2vh"
style="margin-bottom: 0" style="margin-bottom: 0"
> >
<el-scrollbar max-height="76vh"> <!-- 步骤指示器仅添加模式显示 -->
<el-steps v-if="mode === 'add'" :active="currentStep" finish-status="success" align-center class="mb-20">
<el-step title="填写投诉信息" />
<el-step title="查重结果" />
<el-step title="完善信息" />
</el-steps>
<el-scrollbar max-height="72vh">
<el-form <el-form
ref="formRef" ref="formRef"
:model="formData" :model="formData"
:label-width="150" :label-width="150"
class="form-layout" class="form-layout"
style="min-height: 66vh" style="min-height: 60vh"
:rules="rules" :rules="rules"
> >
<!-- 投诉信息 -->
<!-- ==================== 步骤1投诉信息 ==================== -->
<div v-show="mode === 'add' ? currentStep === 0 : true">
<h2>投诉信息</h2> <h2>投诉信息</h2>
<div class="add-negation-container"> <div class="add-negation-container">
<!-- 来源 --> <!-- 来源 -->
@ -113,13 +122,11 @@
</el-col> </el-col>
<el-col :span="12"> <el-col :span="12">
<div style="display: flex; align-items: flex-start; gap: 8px;"> <el-form-item label="身份证号码" prop="responderIdCode">
<el-form-item label="身份证号码" prop="responderIdCode" style="flex: 1; margin-bottom: 0;">
<el-input <el-input
v-model="formData.responderIdCode" v-model="formData.responderIdCode"
:placeholder="formData.responderIdCodeSkip ? '无' : '请输入身份证号码'" :placeholder="formData.responderIdCodeSkip ? '无' : '请输入身份证号码'"
:disabled="formData.responderIdCodeSkip" :disabled="formData.responderIdCodeSkip"
@blur="onAutoCheckDuplicate"
> >
<template #append> <template #append>
<el-checkbox <el-checkbox
@ -131,26 +138,241 @@
</template> </template>
</el-input> </el-input>
</el-form-item> </el-form-item>
</el-col>
</el-row>
<el-button <!-- 联系电话 + 被投诉二级机构 -->
type="primary" <el-row>
:loading="duplicateLoading" <el-col :span="12">
@click="onCheckDuplicate" <el-form-item label="联系电话" prop="responderPhone">
<el-input
v-model="formData.responderPhone"
:placeholder="formData.responderPhoneSkip ? '无' : '请输入联系电话'"
:disabled="formData.responderPhoneSkip"
> >
查看 <template #append>
</el-button> <el-checkbox
v-model="formData.responderPhoneSkip"
@change="(v) => v && (formData.responderPhone = '')"
></el-checkbox>
</template>
</el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="被投诉二级机构" prop="secondDepartId">
<depart-tree-select
v-model="formData.secondDepartId"
:check-strictly="true"
@node-click="(row) => (formData.secondDepartName = row.shortName)"
/>
</el-form-item>
</el-col>
</el-row>
<span <!-- 来件内容 -->
v-if="duplicateHintVisible" <el-row>
style="margin-left: 8px; font-size: 12px;" <el-col :span="24">
:style="{ color: duplicateCount > 0 ? '#fa541c' : '#8c8c8c' }" <el-form-item label="来件内容" prop="thingDesc">
<el-input v-model="formData.thingDesc" type="textarea" :autosize="{ minRows: 4 }" />
</el-form-item>
</el-col>
</el-row>
<el-form-item v-if="mode === 'add'" label="附件" prop="thingFiles">
<file-upload
v-model:files="formData.thingFiles"
tips="为便于“办理单位”更全面了解问题详情,请上传相关附件,如现场督察、数字督察等相关照片、视频及其他佐证材料。"
/>
</el-form-item>
</div>
</div>
<!-- ==================== 步骤2查重结果 ==================== -->
<div v-if="mode === 'add' && currentStep === 1" class="step2-container">
<h2>查重结果</h2>
<!-- 当前填写信息摘要 -->
<div class="current-info-bar">
<el-alert type="info" :closable="false" show-icon>
<template #title>
<span class="info-label">当前填写信息</span>
<span v-if="formData.responderName">姓名: <strong>{{ formData.responderName }}</strong></span>
<span v-if="formData.responderIdCode"> | 身份证: <strong>{{ formData.responderIdCode }}</strong></span>
<span v-if="formData.responderPhone"> | 电话: <strong>{{ formData.responderPhone }}</strong></span>
</template>
</el-alert>
</div>
<!-- 重复记录列表 -->
<div class="duplicate-list" v-loading="duplicateLoading">
<el-empty v-if="duplicateList.length === 0 && !duplicateLoading" description="未发现重复记录,您可以继续添加" />
<el-result v-else-if="duplicateList.length > 0 && !selectedCardId" icon="warning" title="发现疑似重复记录">
<template #sub-title>
<p style="color: #909399;">请从下方列表选择一条记录进行合并或选择"不合并,继续添加"</p>
</template>
</el-result>
<div v-if="duplicateList.length > 0" class="card-grid">
<div
v-for="item in duplicateList"
:key="item.complaintId || item.negativeId"
class="merge-card"
:class="{ 'is-selected': selectedCardId === (item.complaintId || item.negativeId) }"
@click="handleSelectCard(item)"
> >
<template v-if="autoDuplicateLoading">查重中</template> <!-- 卡片头部 -->
<template v-else-if="duplicateCount > 0">疑似重复 {{ duplicateCount }} </template> <div class="card-header">
<template v-else>未发现重复</template> <el-tag size="small" :type="getSourceTagType(item.sourceTable)">
</span> {{ item.sourceTable || '未知来源' }}
</el-tag>
<span class="origin-id">{{ item.originId || '无编号' }}</span>
<div class="card-checkbox">
<el-checkbox
:model-value="selectedCardId === (item.complaintId || item.negativeId)"
@click.stop
@change="handleSelectCard(item)"
/>
</div>
</div>
<!-- 举报人信息 -->
<div class="card-info">
<div v-if="item.responderName" class="info-row">
<span class="label">举报人</span>
<span class="value">{{ item.responderName }}</span>
</div>
<div v-if="item.responderIdCode" class="info-row">
<span class="label">身份证</span>
<span class="value">{{ item.responderIdCode }}</span>
</div>
<div v-if="item.responderPhone" class="info-row">
<span class="label">联系电话</span>
<span class="value">{{ item.responderPhone }}</span>
</div>
</div>
<!-- 投诉内容 -->
<div class="card-content">
<div class="content-label">投诉内容</div>
<div class="content-text">{{ item.thingDesc || '无内容' }}</div>
</div> </div>
<!-- 卡片底部 -->
<div class="card-footer">
<span class="time">{{ formatTime(item.discoveryTime) }}</span>
<el-button type="primary" link @click.stop="handleViewDetail(item)">
<el-icon><View /></el-icon>
</el-button>
</div>
<!-- 选中标记 -->
<div v-if="selectedCardId === (item.complaintId || item.negativeId)" class="selected-badge">
<el-icon><Check /></el-icon>
</div>
</div>
</div>
</div>
</div>
<!-- ==================== 步骤3完善信息 ==================== -->
<div v-if="mode === 'add' && currentStep === 2 && !isMerging">
<!-- 来源 + 业务类别 -->
<el-row>
<el-col :span="12">
<el-form-item label="来源" prop="sourcePath">
<el-cascader
v-model="formData.sourcePath"
:options="dict.sourceTableAndLevel"
:props="{ emitPath: true, checkStrictly: false }"
clearable
filterable
show-all-levels
style="width: 100%"
placeholder="请选择来源(一级/二级)"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="业务类别" prop="businessTypeCode">
<el-select v-model="formData.businessTypeCode" placeholder="业务类别" clearable style="width: 100%">
<el-option
v-for="item in dict.businessType"
:key="item.id"
:label="item.dictLabel"
:value="item.dictValue"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<!-- 编号 + 登记时间 -->
<el-row>
<el-col :span="12">
<el-form-item label="编号" prop="originId">
<el-input
v-model="formData.originId"
:placeholder="formData.originIdSkip ? '无' : '请输入编号'"
:disabled="mode === 'edit' || formData.originIdSkip"
>
<template #append>
<el-checkbox
v-model="formData.originIdSkip"
@change="(v) => v && (formData.originId = '')"
:disabled="mode === 'edit'"
></el-checkbox>
</template>
</el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="登记/受理时间" prop="discoveryTime">
<el-date-picker
v-model="formData.discoveryTime"
type="datetime"
placeholder="请选择"
value-format="YYYY-MM-DDTHH:mm:ss"
format="YYYY-MM-DD HH:mm:ss"
style="width: 100%"
/>
</el-form-item>
</el-col>
</el-row>
<!-- 来件人姓名 + 身份证号码 -->
<el-row>
<el-col :span="12">
<el-form-item label="来件人姓名" prop="responderName">
<el-input
v-model="formData.responderName"
:placeholder="formData.responderNameSkip ? '无' : '请输入来件人姓名'"
:disabled="formData.responderNameSkip"
>
<template #append>
<el-checkbox
v-model="formData.responderNameSkip"
@change="(v) => v && (formData.responderName = '')"
></el-checkbox>
</template>
</el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="身份证号码" prop="responderIdCode">
<el-input
v-model="formData.responderIdCode"
:placeholder="formData.responderIdCodeSkip ? '无' : '请输入身份证号码'"
:disabled="formData.responderIdCodeSkip"
>
<template #append>
<el-checkbox
v-model="formData.responderIdCodeSkip"
@change="(v) => v && (formData.responderIdCode = '')"
></el-checkbox>
</template>
</el-input>
</el-form-item>
</el-col> </el-col>
</el-row> </el-row>
@ -162,20 +384,16 @@
v-model="formData.responderPhone" v-model="formData.responderPhone"
:placeholder="formData.responderPhoneSkip ? '无' : '请输入联系电话'" :placeholder="formData.responderPhoneSkip ? '无' : '请输入联系电话'"
:disabled="formData.responderPhoneSkip" :disabled="formData.responderPhoneSkip"
@blur="onAutoCheckDuplicate"
> >
<template #append> <template #append>
<el-checkbox <el-checkbox
v-model="formData.responderPhoneSkip" v-model="formData.responderPhoneSkip"
@change="(v) => v && (formData.responderPhone = '')" @change="(v) => v && (formData.responderPhone = '')"
> ></el-checkbox>
</el-checkbox>
</template> </template>
</el-input> </el-input>
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="12"> <el-col :span="12">
<el-form-item label="被投诉二级机构" prop="secondDepartId"> <el-form-item label="被投诉二级机构" prop="secondDepartId">
<depart-tree-select <depart-tree-select
@ -207,7 +425,6 @@
:value="item.dictValue" :value="item.dictValue"
:label="item.dictLabel" :label="item.dictLabel"
> >
<!-- 复选框展示 -->
<el-checkbox :model-value="(formData.involveProblemIdList || []).includes(item.dictValue)"> <el-checkbox :model-value="(formData.involveProblemIdList || []).includes(item.dictValue)">
{{ item.dictLabel }} {{ item.dictLabel }}
</el-checkbox> </el-checkbox>
@ -215,7 +432,6 @@
</el-select> </el-select>
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="12"> <el-col :span="12">
<el-form-item label="是否重复件" prop="repeatt"> <el-form-item label="是否重复件" prop="repeatt">
<el-radio-group v-model="formData.repeatt" clearable> <el-radio-group v-model="formData.repeatt" clearable>
@ -253,18 +469,17 @@
</el-col> </el-col>
</el-row> </el-row>
<el-form-item v-if="mode === 'add'" label="附件" prop="thingFiles"> <!-- 附件 -->
<el-form-item label="附件" prop="thingFiles">
<file-upload <file-upload
v-model:files="formData.thingFiles" v-model:files="formData.thingFiles"
tips="为便于“办理单位”更全面了解问题详情,请上传相关附件,如现场督察、数字督察等相关照片、视频及其他佐证材料。" tips="为便于「办理单位」更全面了解问题详情,请上传相关附件,如现场督察、数字督察等相关照片、视频及其他佐证材料。"
/> />
</el-form-item> </el-form-item>
</div>
<!-- 办理单位 --> <!-- 办理单位 -->
<el-divider v-if="mode === 'add'" /> <h2>办理单位</h2>
<h2 v-if="mode === 'add'">办理单位</h2> <div class="add-negation-container">
<div v-if="mode === 'add'" class="add-negation-container">
<el-form-item label="主办层级" prop="hostLevel" :rules="{ required: true, message: '请选择主办层级', trigger: ['blur'] }"> <el-form-item label="主办层级" prop="hostLevel" :rules="{ required: true, message: '请选择主办层级', trigger: ['blur'] }">
<el-select style="width: 280px" v-model="formData.hostLevel" @change="handleChangeHostLevel"> <el-select style="width: 280px" v-model="formData.hostLevel" @change="handleChangeHostLevel">
<el-option v-for="item in dict.hostLevel" :key="item.id" :label="item.dictLabel" :value="item.dictValue" /> <el-option v-for="item in dict.hostLevel" :key="item.id" :label="item.dictLabel" :value="item.dictValue" />
@ -292,23 +507,20 @@
</el-form-item> </el-form-item>
</div> </div>
<!-- 初核时限 --> <h2>初核时限</h2>
<h2 v-if="mode === 'add'">初核时限</h2> <p style="color: #909399; font-size: 14px; line-height: 1.6; padding-left: 200px;">
<p v-if="mode === 'add'" style="color: #909399; font-size: 14px; line-height: 1.6; padding-left: 200px;">
该信件的初核时限需在<span style="color: var(--el-color-danger); font-weight: 600;"> 4个工作日 </span>之内完成初核 该信件的初核时限需在<span style="color: var(--el-color-danger); font-weight: 600;"> 4个工作日 </span>之内完成初核
</p> </p>
<!-- 办理时限 --> <h2>办理时限</h2>
<h2 v-if="mode === 'add'">办理时限</h2> <div class="add-negation-container">
<div v-if="mode === 'add'" class="add-negation-container">
<el-form-item label="办理时限" prop="timeLimit" :rules="{ required: true, message: '请选择办理时限', trigger: ['blur'] }"> <el-form-item label="办理时限" prop="timeLimit" :rules="{ required: true, message: '请选择办理时限', trigger: ['blur'] }">
<time-limit-select :key="timeLimitKey" v-model="formData.timeLimit" v-model:maxSignDuration="formData.maxSignDuration" v-model:maxHandleDuration="formData.maxHandleDuration" v-model:maxExtensionDuration="formData.maxExtensionDuration" /> <time-limit-select :key="timeLimitKey" v-model="formData.timeLimit" v-model:maxSignDuration="formData.maxSignDuration" v-model:maxHandleDuration="formData.maxHandleDuration" v-model:maxExtensionDuration="formData.maxExtensionDuration" />
</el-form-item> </el-form-item>
</div> </div>
<!-- 审批流程 --> <h2>审批流程</h2>
<h2 v-if="mode === 'add'">审批流程</h2> <div class="add-negation-container">
<div v-if="mode === 'add'" class="add-negation-container">
<el-form-item label="审批流程" prop="approvalFlow" :rules="{ required: true, message: '请选择审批流程', trigger: ['blur'] }"> <el-form-item label="审批流程" prop="approvalFlow" :rules="{ required: true, message: '请选择审批流程', trigger: ['blur'] }">
<el-radio-group v-model="formData.approvalFlow" v-if="userStore.user.roleCodes.includes('admin_1')"> <el-radio-group v-model="formData.approvalFlow" v-if="userStore.user.roleCodes.includes('admin_1')">
<el-radio v-for="item in dict.approvalFlow" :key="item.dictCode" :value="item.dictValue">{{ item.dictLabel }}{{ item.remark ? `(${item.remark})` : "" }}</el-radio> <el-radio v-for="item in dict.approvalFlow" :key="item.dictCode" :value="item.dictValue">{{ item.dictLabel }}{{ item.remark ? `(${item.remark})` : "" }}</el-radio>
@ -322,15 +534,49 @@
</div> </div>
</el-form-item> </el-form-item>
</div> </div>
</div>
</el-form> </el-form>
</el-scrollbar> </el-scrollbar>
<!-- 底部按钮根据步骤显示不同按钮 -->
<template #footer> <template #footer>
<div class="dialog-footer"> <div class="dialog-footer">
<!-- 修改模式保持原有按钮 -->
<template v-if="mode === 'edit'">
<el-button @click="visibleProxy = false" size="large">取消</el-button>
<el-button type="primary" @click="onSubmit" size="large" :loading="loading || submitLoading">修改</el-button>
</template>
<!-- 添加模式步骤1 -->
<template v-else-if="currentStep === 0">
<el-button @click="visibleProxy = false" size="large">取消</el-button> <el-button @click="visibleProxy = false" size="large">取消</el-button>
<el-button type="primary" @click="onSubmit" size="large" :loading="loading || submitLoading"> <el-button type="primary" @click="handleNextStep" size="large" :loading="duplicateLoading">
{{ mode === 'add' ? '添加' : '修改' }} 下一步 查重
</el-button> </el-button>
</template>
<!-- 添加模式步骤2查重结果 -->
<template v-else-if="currentStep === 1">
<el-button @click="handlePrevStep" size="large">上一步</el-button>
<el-button type="info" @click="handleSkipMerge" size="large" :loading="submitLoading">
不合并继续添加
</el-button>
<el-button
type="primary"
@click="handleConfirmMerge"
size="large"
:disabled="!selectedCardId"
:loading="mergeLoading"
>
确认合并到已选记录
</el-button>
</template>
<!-- 添加模式步骤3完善信息 -->
<template v-else-if="currentStep === 2">
<el-button @click="handlePrevStep" size="large">上一步</el-button>
<el-button type="primary" @click="onSubmit" size="large" :loading="submitLoading">添加</el-button>
</template>
</div> </div>
</template> </template>
</el-dialog> </el-dialog>
@ -366,7 +612,7 @@
<script setup> <script setup>
import {computed, ref, watch} from "vue"; import {computed, ref, watch} from "vue";
import {maileRepeatt, getComplaintCollectionDetail, updateComplaintCollection, addComplaintCollection} from "@/api/data/complaintCollection.ts"; import {maileRepeatt, getComplaintCollectionDetail, updateComplaintCollection, addComplaintCollection, mergeComplaintCollection} from "@/api/data/complaintCollection.ts";
import dayjs from "dayjs"; import dayjs from "dayjs";
import feedback from "@/utils/feedback.ts"; import feedback from "@/utils/feedback.ts";
import DuplicateDrawerWithDetail from "@/views/data/DuplicateDrawerWithDetail.vue"; import DuplicateDrawerWithDetail from "@/views/data/DuplicateDrawerWithDetail.vue";
@ -517,6 +763,129 @@ const formRef = ref();
const submitLoading = ref(false); const submitLoading = ref(false);
const timeLimitKey = ref(0); const timeLimitKey = ref(0);
//
const currentStep = ref(0)
const isMerging = ref(false)
const selectedCardId = ref(null)
const selectedCard = ref(null)
const mergeLoading = ref(false)
//
const handleNextStep = async () => {
try {
await formRef.value.validate()
} catch {
feedback.msgWarning("请完善必填信息")
return
}
currentStep.value = 1
await fetchDuplicateList()
}
const handlePrevStep = () => {
currentStep.value--
}
const handleSkipMerge = async () => {
isMerging.value = false
formData.value.repeatt = '0' // ""
currentStep.value = 2
}
const handleConfirmMerge = async () => {
if (!selectedCardId.value) {
feedback.msgWarning("请选择一个记录进行合并")
return
}
mergeLoading.value = true
try {
await mergeComplaintCollection({
targetId: selectedCardId.value,
sourceTable: formData.value.sourceTable,
sourceTableSubOne: formData.value.sourceTableSubOne,
originId: formData.value.originId,
discoveryTime: formData.value.discoveryTime,
businessTypeCode: formData.value.businessTypeCode,
responderName: formData.value.responderName,
responderIdCode: formData.value.responderIdCode,
responderPhone: formData.value.responderPhone,
secondDepartId: formData.value.secondDepartId,
secondDepartName: formData.value.secondDepartName,
thingDesc: formData.value.thingDesc,
files: JSON.stringify(formData.value.thingFiles || [])
})
feedback.msgSuccess("合并成功")
visibleProxy.value = false
emit("updateSuccess")
} catch (e) {
console.error("合并失败", e)
feedback.notifyError("合并失败")
} finally {
mergeLoading.value = false
}
}
//
const fetchDuplicateList = async () => {
duplicateLoading.value = true
try {
const body = {
responderIdCode: formData.value.responderIdCode,
responderName: formData.value.responderName,
responderPhone: formData.value.responderPhone,
}
const res = await maileRepeatt(body)
duplicateList.value = res?.complaintCollectionRepeatDTOS || []
} catch (e) {
console.error("查重失败", e)
} finally {
duplicateLoading.value = false
}
}
//
const handleSelectCard = (item) => {
const id = item.complaintId || item.negativeId
if (selectedCardId.value === id) {
selectedCardId.value = null
selectedCard.value = null
} else {
selectedCardId.value = id
selectedCard.value = item
}
}
//
const maskIdCode = (idCode) => {
if (!idCode) return '-'
if (idCode.length >= 10) {
return idCode.substring(0, 6) + '****' + idCode.substring(idCode.length - 4)
}
return idCode
}
const maskPhone = (phone) => {
if (!phone) return '-'
if (phone.length >= 7) {
return phone.substring(0, 3) + '****' + phone.substring(phone.length - 4)
}
return phone
}
const formatTime = (time) => {
if (!time) return '-'
return dayjs(time).format('YYYY-MM-DD HH:mm')
}
const getSourceTagType = (source) => {
if (!source) return 'info'
if (source.includes('局长信箱')) return 'success'
if (source.includes('12389')) return 'warning'
return 'primary'
}
const visibleProxy = computed({ const visibleProxy = computed({
get: () => props.modelValue, get: () => props.modelValue,
set: (v) => emit("update:modelValue", v), set: (v) => emit("update:modelValue", v),
@ -526,9 +895,15 @@ watch(
() => visibleProxy.value, () => visibleProxy.value,
async (v) => { async (v) => {
if (v) { if (v) {
// //
timeLimitKey.value++ timeLimitKey.value++
formData.value = createEmptyForm(); formData.value = createEmptyForm()
//
currentStep.value = 0
isMerging.value = false
selectedCardId.value = null
selectedCard.value = null
duplicateDrawerVisible.value = false duplicateDrawerVisible.value = false
duplicateLoading.value = false duplicateLoading.value = false
@ -723,6 +1098,7 @@ async function onSubmit() {
const duplicateDrawerVisible = ref(false) const duplicateDrawerVisible = ref(false)
const duplicateList = ref([]) const duplicateList = ref([])
const duplicateLoading = ref(false) const duplicateLoading = ref(false)
const onCheckDuplicate = async () => { const onCheckDuplicate = async () => {
duplicateDrawerVisible.value = true duplicateDrawerVisible.value = true
} }
@ -784,6 +1160,11 @@ const handleDuplicateRowClick = (row) => {
} }
} }
//
const handleViewDetail = (item) => {
handleDuplicateRowClick(item)
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@ -883,4 +1264,155 @@ h2 {
font-size: 16px; font-size: 16px;
font-weight: 600; font-weight: 600;
} }
// 2
.mb-20 {
margin-bottom: 20px;
}
.step2-container {
min-height: 400px;
padding: 0 20px;
}
.current-info-bar {
margin-bottom: 20px;
padding: 0 20px;
.info-label {
font-weight: 600;
}
}
.duplicate-list {
min-height: 200px;
}
.card-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
padding: 0 20px;
}
.merge-card {
position: relative;
border: 2px solid #e8e8e8;
border-radius: 10px;
background: #fff;
transition: all 0.25s ease;
overflow: hidden;
cursor: pointer;
&:hover {
border-color: #409eff;
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.15);
}
&.is-selected {
border-color: #67c23a;
border-width: 3px;
background: linear-gradient(135deg, #f0f9eb 0%, #ffffff 100%);
box-shadow: 0 6px 20px rgba(103, 194, 58, 0.35);
transform: scale(1.02);
}
.card-header {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 14px;
border-bottom: 1px solid #f0f0f0;
background: #fafafa;
.origin-id {
font-weight: 600;
color: #303133;
flex: 1;
}
.card-checkbox {
margin-left: auto;
}
}
.card-info {
padding: 12px 14px;
background: #f9f9f9;
.info-row {
display: flex;
margin-bottom: 6px;
&:last-child {
margin-bottom: 0;
}
.label {
color: #909399;
width: 70px;
flex-shrink: 0;
}
.value {
color: #303133;
}
}
}
.card-content {
padding: 12px 14px;
.content-label {
font-size: 12px;
color: #909399;
margin-bottom: 4px;
}
.content-text {
color: #606266;
font-size: 13px;
line-height: 1.5;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
}
.card-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 14px;
border-top: 1px solid #f0f0f0;
.time {
color: #909399;
font-size: 12px;
}
}
.selected-badge {
position: absolute;
top: 0;
right: 0;
background: linear-gradient(135deg, #67c23a 0%, #5daf34 100%);
color: #fff;
font-size: 13px;
font-weight: 600;
padding: 6px 14px;
border-radius: 0 8px 0 8px;
display: flex;
align-items: center;
gap: 4px;
box-shadow: 0 2px 8px rgba(103, 194, 58, 0.4);
letter-spacing: 1px;
text-transform: uppercase;
.el-icon {
font-size: 14px;
}
}
}
</style> </style>

39
src/components/negative/description.vue

@ -60,7 +60,14 @@
</div> </div>
<div class="col col-6" v-if="negative.sourceType === '2' && negative.currentRow?.repeatt"> <div class="col col-6" v-if="negative.sourceType === '2' && negative.currentRow?.repeatt">
<label>是否重复件</label> <label>是否重复件</label>
<span>{{ getDictLable(dict.yesNo, negative.currentRow.repeatt) }}</span> <span>{{ getDictLable(dict.yesNo, negative.currentRow.repeatt) }}<el-button
v-if="negative.currentRow?.mergeHistory"
type="primary"
size="small"
link
style="margin-left: 4px; padding: 0;"
@click="showMergeHistory"
>合并历史</el-button></span>
</div> </div>
<div class="col col-6" v-if="negative.sourceType === '2' && negative.currentRow?.tag"> <div class="col col-6" v-if="negative.sourceType === '2' && negative.currentRow?.tag">
<label>标签</label> <label>标签</label>
@ -92,10 +99,40 @@
<file-list :files="negative.thingFiles" /> <file-list :files="negative.thingFiles" />
</div> </div>
</div> </div>
<!-- 合并历史弹窗 -->
<MergeHistoryDialog
v-model="mergeHistoryDialogVisible"
:mergeHistory="mergeHistoryData"
/>
</template> </template>
<script setup> <script setup>
import { ref } from 'vue'
import { getInvolveProblem ,getDictLable } from "@/utils/util"; import { getInvolveProblem ,getDictLable } from "@/utils/util";
import MergeHistoryDialog from "@/components/data/MergeHistoryDialog.vue";
import { getMergeHistory } from "@/api/data/complaintCollection.ts";
const negative = inject('negative') const negative = inject('negative')
const emit = defineEmits(['show-merge-history'])
//
const mergeHistoryDialogVisible = ref(false)
const mergeHistoryData = ref(null)
const showMergeHistory = async () => {
if (negative.value?.currentRow?.id) {
try {
const res = await getMergeHistory({ id: negative.value.currentRow.id })
const data = res?.data || res
// JSON
mergeHistoryData.value = typeof data === 'string' ? JSON.parse(data) : data
} catch (e) {
mergeHistoryData.value = null
}
mergeHistoryDialogVisible.value = true
}
}
import useCatchStore from "@/stores/modules/catch"; import useCatchStore from "@/stores/modules/catch";
const catchSotre = useCatchStore(); const catchSotre = useCatchStore();
const dict = catchSotre.getDicts([ const dict = catchSotre.getDicts([

Loading…
Cancel
Save