数字督察一体化平台-前端
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

911 lines
27 KiB

<template>
<div class="container">
<header>
<el-form :label-width="114">
<el-row>
<el-col :span="6">
<el-form-item label="问题来源">
<el-cascader
v-model="query.sourcePath"
:options="dict.sourceTableAndLevel"
:props="{ emitPath: true, checkStrictly: true , multiple: true }"
clearable
filterable
show-all-levels
placeholder="请选择来源(一级/二级)"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="信件编号">
<el-input
placeholder="请输入编号"
v-model="query.originId"
clearable
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="登记/受理时间">
<date-time-range-picker-ext
v-model="query.discoveryTimeList"
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="来件人">
<el-input
placeholder="来件人姓名、身份证号码、联系电话"
v-model="query.personInfo"
clearable
/>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="6">
<el-form-item label="涉及单位" prop="involveDepartId">
<depart-tree-select
v-model="query.involveDepartId"
:check-strictly="true"
placeholder="请选择涉及单位"
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="办理单位" prop="handleDepartId">
<depart-tree-select
v-model="query.handleDepartId"
placeholder="办理单位"
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="办理方式">
<el-select
v-model="query.handleMethod"
placeholder="全部"
clearable
>
<el-option value="0" label="自办"/>
<el-option value="1" label="下发"/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="办理状态">
<el-select
v-model="query.processingStatus"
clearable
multiple
collapse-tags
placeholder="办理状态"
>
<el-option
v-for="item in dict.processingStatus"
:key="item.id"
:label="item.dictLabel"
:value="item.dictValue"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="6">
<el-form-item label="来件内容">
<el-input
placeholder="请输入来件内容"
v-model="query.thingDesc"
clearable
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="是否重复件">
<el-select
v-model="query.repeatt"
placeholder="全部"
clearable
>
<el-option value="0" label="否"/>
<el-option value="1" label="是"/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="核查结论">
<el-select v-model="query.checkStatusList" clearable multiple
style="width: 280px">
<el-option v-for="item in dict.checkStatus" :value="item.dictValue" :label="item.dictLabel"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="初核材料">
<el-select
v-model="query.initialReviewFileList"
placeholder="全部"
clearable
multiple
>
<el-option value="0" label="未上传"/>
<el-option value="1" label="已上传"/>
<el-option value="2" label="超时上传"/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="涉嫌问题" prop="involveProblemIdList">
<el-select
v-model="query.involveProblemIdList"
multiple
clearable
collapse-tags
style="width: 100%"
placeholder="请选择涉嫌问题"
>
<el-option
v-for="item in dict.suspectProblem"
:key="item.dictValue"
:value="item.dictValue"
:label="item.dictLabel"
>
<!-- 复选框展示(和你弹窗里一致) -->
<el-checkbox :model-value="(query.involveProblemIdList || []).includes(item.dictValue)">
{{ item.dictLabel }}
</el-checkbox>
</el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="标签">
<el-select
placeholder="全部"
clearable
v-model="query.tags"
multiple
collapse-tags
>
<el-option
v-for="item in dict.sfssTags"
:key="item.id"
:value="item.dictValue"
:label="item.dictLabel"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="录入时间">
<date-time-range-picker-ext
v-model="query.createTimeList"
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="处理结果">
<el-select
v-model="query.handleResultCode"
placeholder="处理结果"
multiple
clearable
collapse-tags
style="width: 100%"
>
<el-option
v-for="item in dict.handleResult"
:key="item.dictCode"
:value="item.dictValue"
:label="item.dictLabel"
/>
</el-select>
</el-form-item>
</el-col>
<!-- 局长信箱专属搜索框 - 注释 by Claude
<el-col :span="6"
v-if=" query.sourceTableList?.length === 1 && String(query.sourceTableList[0]) === '23'">
<el-form-item label="信件状态">
<el-select
v-model="query.processingStatus"
placeholder="全部"
clearable
style="width: 100%"
>
<el-option value="processing" label="办理中"/>
<el-option value="completion" label="已办结"/>
<el-option value="delayed" label="已延期"/>
<el-option value="terminated" label="已终止"/>
</el-select>
</el-form-item>
</el-col>
-->
</el-row>
</el-form>
<div class="mb-25 flex between">
<div>
<el-button type="primary" :disabled="!canAdd" @click="add()">添加</el-button>
<el-button type="primary" @click="handleExport">数据导出</el-button>
</div>
<div>
<el-button type="primary" @click="getList">
<template #icon>
<icon name="el-icon-Search"/>
</template>
查询
</el-button>
<el-button @click="reset">重置</el-button>
</div>
</div>
</header>
<div class="table-container" v-loading="loading">
<el-table :data="list" @click="onHeaderDblClick">
<el-table-column type="expand" v-if="false">
<template #default="{ row }">
<div class="row mt-10">
<div class="col col-6">
<label>编号</label>
<span>{{ row.originId }}</span>
</div>
<div class="col col-6">
<label>问题发现时间</label>
<span>{{ row.discoveryTime }}</span>
</div>
</div>
</template>
</el-table-column>
<el-table-column label="信件编号" width="130" prop="originId" show-overflow-tooltip/>
<el-table-column label="来源" width="100" show-overflow-tooltip v-if="false">
<template #default="{ row }">
{{ getDictLabel(dict.sfssSourceTable, row.sourceTable) }}
</template>
</el-table-column>
<el-table-column label="来源" width="100" show-overflow-tooltip>
<template #default="{ row }">
{{ row.sourceTablePath }}
</template>
</el-table-column>
<el-table-column
label="登记/受理时间"
width="170"
prop="discoveryTime"
:formatter="(_, __, v) => v ? timeFormat(v, 'yyyy-mm-dd hh:MM:ss') : '/'"
show-overflow-tooltip
/>
<el-table-column label="来件人姓名" width="100" prop="responderName"
:formatter="row => row.responderName ? row.responderName : '匿名'"/>
<el-table-column label="身份证号" width="100" prop="responderIdCode"
:formatter="row => row.responderIdCode ? row.responderIdCode : '无'" show-overflow-tooltip/>
<el-table-column label="联系电话" width="100" prop="responderPhone"
:formatter="row => row.responderPhone ? row.responderPhone : '无'" show-overflow-tooltip/>
<el-table-column label="被投诉机构" width="130" show-overflow-tooltip>
<template #default="{ row }">
<span>{{ row.secondDepartName }}</span>
<span v-if="row.thirdDepartName"> {{ row.thirdDepartName }}</span>
</template>
</el-table-column>
<el-table-column label="办理单位" width="160" show-overflow-tooltip>
<template #default="{ row }">
{{ row.handleSecondDepartName || '' }}{{ row.handleThreeDepartName ? '/' + row.handleThreeDepartName : '' }}
</template>
</el-table-column>
<el-table-column label="来信内容" width="100" prop="thingDesc" show-overflow-tooltip/>
<el-table-column label="涉嫌问题" width="100" prop="involveProblemStr" show-overflow-tooltip/>
<el-table-column label="是否重复件" width="100">
<template #default="{ row }">
{{ getDictLabel(dict.yesNo, row.repeatt) }}
</template>
</el-table-column>
<el-table-column label="初核限时" width="150" align="center">
<template #default="{ row }">
<!-- 有状态:显示状态文案 -->
<el-tag v-if="row.gwf3>=1" :type="row.gwf3 === '2' ? 'danger' : 'success'">
{{ row.gwf3 === '2' ? '超时完成初核' : '按时完成初核' }}
</el-tag>
<!-- 无状态:显示倒计时 -->
<countdown v-else :time="Number(row.remainingDuration || 0)"/>
</template>
</el-table-column>
<el-table-column
label="录入时间"
width="170"
prop="createTime"
:formatter="(_, __, v) => v ? timeFormat(v, 'yyyy-mm-dd hh:MM:ss') : '/'"
show-overflow-tooltip
/>
<el-table-column label="是否领导批示" width="130" v-if="false">
<template #default="{ row }">
{{ getDictLabel(dict.yesNo, row.leadApproval) }}
</template>
</el-table-column>
<el-table-column label="标签" width="100" show-overflow-tooltip>
<template #default="{ row }">
{{ getDictLabel(dict.sfssTags, row.tag) }}
</template>
</el-table-column>
<el-table-column label="办结情况" width="100" prop="completionStatus">
<template #default="{ row }">
{{
row.completionStatus === '1' ? '程序办结' :
row.completionStatus === '2' ? '已解决合理诉求' :
'/'
}}
</template>
</el-table-column>
<el-table-column label="群众认可" width="100" prop="publicRecognition">
<template #default="{ row }">
{{
row.publicRecognition === '1' ? '认可' :
row.publicRecognition === '2' ? '不认可' :
row.publicRecognition === '3' ? '不接电话' :
'/'
}}
</template>
</el-table-column>
<el-table-column label="办理方式" width="100">
<template #default="{ row }">
{{ getDictLabel(dict.handleMethodType, row.handleMethod) }}
</template>
</el-table-column>
<el-table-column label="办理情况" width="100" prop="xxx" v-if="false"/>
<el-table-column label="业务类别" width="100" prop="businessTypeName"/>
<el-table-column label="业务类别Code" width="100" prop="businessTypeCode" v-if="false"/>
<el-table-column label="核查结论" width="140" prop="checkStatus" show-overflow-tooltip>
<template #default="{ row }">
{{ getDictLabel(dict.checkStatus, row.checkStatusCode) }}
</template>
</el-table-column>
<el-table-column label="办理状态" prop="processingStatus" width="100">
<template #default="{ row }">
<el-tag
:type="row.processingStatus === 'completed' ? 'success' : 'primary'"
v-if="row.processingStatus"
>
{{ getDictLabel(dict.processingStatus, row.processingStatus) }}
</el-tag>
<span v-else>-</span>
</template>
</el-table-column>
<!-- <el-table-column label="信件状态(局)" width="120" prop="processingStatus">-->
<!-- <template #default="{ row }">-->
<!-- <el-tag-->
<!-- :type="row.processingStatus === 'completed' ? 'success' : 'primary'"-->
<!-- v-if="row.processingStatus"-->
<!-- >-->
<!-- {{ getDictLabel(dict.processingStatus, row.processingStatus) }}-->
<!-- </el-tag>-->
<!-- <span v-else>/</span>-->
<!-- </template>-->
<!-- </el-table-column>-->
<el-table-column label="操作" width="240" fixed="right">
<template #default="{ row }">
<!-- <el-button type="primary" v-if="row.status === '0'" link @click="handleAction(row)">办理</el-button>-->
<el-button
type="primary"
link
@click="handleActionDetail(row)"
>办理详情</el-button
>
<el-button type="primary" link @click="handleUpdate(row)">修改</el-button>
<el-button type="danger" v-if="row.processingStatus !== 'completed'" link @click="handleDel(row)">删除</el-button>
<!-- <el-button type="primary" v-if="row.status === '1' || row.status === '2'" link @click="handleWatchDetail(row)">查看详情</el-button>-->
</template>
</el-table-column>
</el-table>
</div>
<div class="flex end mt-8">
<el-pagination
@size-change="getList"
@current-change="getList"
:page-sizes="[10, 20, 50]"
v-model:page-size="query.size"
v-model:current-page="query.current"
layout="total, sizes, prev, pager, next"
:total="total"
>
</el-pagination>
</div>
</div>
<!-- 添加 -->
<data-complaintformdialog
v-model="addShow"
mode="add"
@updateSuccess="getList"
/>
<!-- 修改 -->
<data-complaintformdialog
v-model="updateShow"
mode="edit"
:id="updateId"
:negativeId="activeNegativeId"
:loading="updateLoading"
@updateSuccess="getList"
/>
<negative-dialog
v-model="show"
:id="activeNegativeId"
@close="show = false"
ref="negativeDialogRef"
/>
<negative-mailbox-dialog
v-model="mailboxShow"
:id="activeNegativeId"
@close="mailboxShow = false"
/>
</template>
<script setup>
import {timeFormat} from "@/utils/util";
import feedback from "@/utils/feedback";
import useCatchStore from "@/stores/modules/catch";
import {
addComplaintCollection,
delComplaintCollection, exportData,
getComplaintCollectionPage, saveInvolveJson,
updateComplaintCollection
} from "@/api/data/complaintCollection.ts";
import useUserStore from "@/stores/modules/user.ts";
const route = useRoute()
const catchStore = useCatchStore();
const show = ref(false);
const userStore = useUserStore();
// region 列表
const query = ref({
size: 10,
current: 1,
sourcePath: [],
sourceTableList: [],
sourceTableSubOneList: [],
involveDepartId: null,
handleDepartId: null,
handleResultCode: [],
});
watch(
() => query.value.sourcePath,
(paths = []) => {
const tableSet = new Set()
const subSet = new Set()
paths.forEach(path => {
if (!Array.isArray(path)) return
if (path[0]) tableSet.add(path[0])
if (path[1]) subSet.add(path[1])
})
query.value.sourceTableList = Array.from(tableSet)
query.value.sourceTableSubOneList = Array.from(subSet)
},
{deep: true}
)
const list = ref([]);
const total = ref(0);
const loading = ref(false)
const getList = async () => {
hasPermission()
console.log("===============xxx==================")
console.log(dict.value.sfssSourceTable)
loading.value = true;
const params = {
...query.value,
}
delete params.sourcePath
let res = await getComplaintCollectionPage(params);
debugger
list.value = res.records;
total.value = res.total;
loading.value = false;
}
// 重置
function reset() {
query.value = {
size: 10,
current: 1,
sourcePath: [],
sourceTableList: [],
sourceTableSubOneList: [],
involveDepartId: null,
handleDepartId: null,
};
getList();
}
/**
* 页面初始化
*/
onMounted(() => {
getList()
})
// endregion
// region 添加相关
const addShow = ref(false);
const add = () => {
addShow.value = true;
};
// 添加按钮权限判断
function hasPermission() {
const roles = userStore.user?.roleCodes || []
const allow = new Set(['admin_1', 'admin_1_12337'])
return roles.some(r => allow.has(r))
}
const canAdd = computed(() => hasPermission())
// endregion
// region 修改相关
const updateShow = ref(false);
const updateId = ref("");
const updateLoading = ref(false);
// 修改按钮点击事件
const handleUpdate = (row) => {
updateId.value = row.id || '';
activeNegativeId.value = row.negativeId || '';
updateShow.value = true;
}
// endregion
// region 删除相关
const handleDel = async (row) => {
console.log(row)
await feedback.confirm(`确定删除该数据?`);
const body = {
id: row.id
}
await delComplaintCollection(body)
feedback.msgSuccess("操作成功");
getList();
}
// endregion
// region 枚举相关
const storeDict = computed(() =>
catchStore.getDicts([
"businessType",
"suspectProblem",
"policeType",
"hostLevel",
"timeLimit",
"approvalFlow",
"specialSupervision",
"checkStatus",
"sfssSourceTable",
"sfssTags",
"accountabilityTarget",
"processingStatus",
"handleMethodType",
"yesNo",
"handleResult"
]) || {}
);
// 组装数据
const sourceTableAndLevel = computed(() => {
const list = storeDict.value.sfssSourceTable || []
// 一级:remark 为空
const parents = list
.filter(d => !d.remark)
.sort((a, b) => (a.dictSort ?? 0) - (b.dictSort ?? 0))
// 二级:remark 有值(remark=父级dictValue)
const children = list
.filter(d => d.remark)
.sort((a, b) => (a.dictSort ?? 0) - (b.dictSort ?? 0))
return parents.map(p => ({
label: p.dictLabel,
value: String(p.dictValue),
children: children
.filter(c => String(c.remark) === String(p.dictValue))
.map(c => ({
label: c.dictLabel,
value: String(c.dictValue),
})),
}))
})
// ② 页面私有字典
const localDict = {
sourceTable: [
{id: 1, dictLabel: "局长信箱", dictValue: "data_mailbox"},
{id: 2, dictLabel: "公安部信访", dictValue: "data_petition_complaint_21"},
{id: 3, dictLabel: "国家信访", dictValue: "data_petition_complaint_22"},
{id: 4, dictLabel: "12389投诉", dictValue: "data_case_verif"},
{id: 5, dictLabel: "领导交办", dictValue: "leader_explain"},
],
yesNo: [
{id: 1, dictLabel: "是", dictValue: "1"},
{id: 2, dictLabel: "否", dictValue: "0"},
],
handleMethodType: [
{id: 1, dictLabel: "自办", dictValue: "0"},
{id: 2, dictLabel: "下发", dictValue: "1"},
],
leadResponsibilityType: [
{dictCode: "1", dictValue: "1", dictLabel: "领导责任"},
],
};
// ③ 最终使用的 dict(合并)
const dict = computed(() => ({
...storeDict.value,
...localDict,
sourceTableAndLevel: sourceTableAndLevel.value,
}));
function getDictLabel(list, value) {
if (!Array.isArray(list)) return '/'
if (value === null || value === undefined || value === '') return '/'
let values = []
// 1 value 是数组
if (Array.isArray(value)) {
values = value
}
// 2 value 是 "1,2,3" 这种字符串
else if (typeof value === 'string' && value.includes(',')) {
values = value.split(',').map(v => v.trim())
}
// 3 单值(string / number)
else {
values = [value]
}
// 4 映射成字典中文
const labels = values
.map(v => {
const item = list.find(d => d.dictValue == v)
return item?.dictLabel
})
.filter(Boolean)
return labels.length ? labels.join(',') : '/'
}
// endregion
// region 办理相关
const negativeVerifySfssDailog = ref(false)
const submitLoading = ref(false)
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const negativeVerifySfssRef = ref();
// 办理提交
const handleSubmit = async () => {
if (submitLoading.value) return
submitLoading.value = true
await nextTick();
try {
await negativeVerifySfssRef.value.validate();
const formData = negativeVerifySfssRef.value.getData()
console.log("提交前面==================")
console.log(formData)
let res = await addComplaintCollectionBlame(formData);
feedback.msgSuccess("提交成功");
negativeVerifySfssDailog.value = false;
getList();
} catch (err) {
feedback.notifyError("提交失败")
} finally {
submitLoading.value = false
}
}
// 办理保存
const saveLoading = ref(false)
const handleSaveInvolve = async () => {
if (saveLoading.value) return
saveLoading.value = true
try {
// ✅ 关键:让 el-input / el-select / 自定义组件把最后一次变更刷进 model
await nextTick()
// ✅ 再取一次最新数据
const formData = negativeVerifySfssRef.value.getData()
// ✅ 关键:深拷贝 + 去代理,避免 Proxy/引用带来的“看起来旧值”
const plain = JSON.parse(JSON.stringify(toRaw(formData)))
const payload = {
blames: plain.blames || [],
blameLeaders: plain.blameLeaders || [],
files: plain.files || [],
}
await saveInvolveJson({
complaintId: plain.complaintId,
// 表字段
checkStatusCode: plain.checkStatusCode,
checkStatusName: plain.checkStatusName,
checkStatusDesc: plain.checkStatusDesc,
accountabilityTarget: plain.accountabilityTarget,
involveDepartId: plain.involveDepartId,
involveDepartName: plain.involveDepartName,
completionStatus: plain.completionStatus,
publicRecognition: plain.publicRecognition,
caseNumber: plain.caseNumber,
// JSON
involveJson: JSON.stringify(payload),
files: plain.files
})
feedback.msgSuccess("操作成功")
negativeVerifySfssDailog.value = false
getList()
} catch (e) {
console.error(e)
feedback.notifyError("操作失败")
} finally {
saveLoading.value = false
}
}
const negativeSfss = ref({
currentRow: {},
// problemSourcesCode: ProblemSources.GJXFPT,
// 涉及人员
blames: [],
// 涉及领导
blameLeaders: [],
// 经办人
handlePolices: [{}],
});
provide('negative', negativeSfss)
watch(negativeVerifySfssDailog, (open) => {
if (!open) {
negativeSfss.value = {
currentRow: {},
blames: [],
blameLeaders: [],
handlePolices: [{}],
files: [],
}
}
})
// endregion
// region 强制终结
const forceTerminationLoading = ref(false);
const forceTerminationFun = async () => {
if (forceTerminationLoading.value) return
forceTerminationLoading.value = true
try {
await nextTick()
// ✅ 再取一次最新数据
const formData = negativeVerifySfssRef.value.getData()
// ✅ 关键:深拷贝 + 去代理,避免 Proxy/引用带来的“看起来旧值”
const plain = JSON.parse(JSON.stringify(toRaw(formData)))
await forceTermination({
complaintId: plain.complaintId,
})
feedback.msgSuccess("操作成功")
negativeVerifySfssDailog.value = false
getList()
} catch (e) {
console.error(e)
feedback.notifyError("操作失败")
} finally {
forceTerminationLoading.value = false
}
}
// endregion
// region 查看详情相关
const detailShow = ref(false)
const activeId = ref("")
const handleWatchDetail = async (row) => {
activeId.value = row.id
detailShow.value = true
}
// endregion
const mailboxShow = ref(false)
// region 办理详情
// const show = ref(false);
const activeNegativeId = ref("");
function handleActionDetail(row) {
console.log('办理详情', row)
if (row.problemSources === '局长信箱') {
mailboxShow.value = true;
activeNegativeId.value = row.originId;
} else {
show.value = true;
activeNegativeId.value = row.negativeId;
}
}
// endregion
// region 自己排查相关
const onHeaderDblClick = () => {
const rows = JSON.parse(JSON.stringify(toRaw(list.value || [])))
console.groupCollapsed(`[DEBUG] list 全量(${rows.length}条)`)
console.log('raw:', rows)
console.groupEnd()
}
// endregion
// region 导出相关
const handleExport = async () => {
let body = {
...query.value
}
await exportData(body);
}
// endregion
</script>
<style lang="scss" scoped>
.el-form-item .el-form-item {
margin-bottom: 18px;
}
:deep() {
.el-form-item--label-right .el-form-item__label {
text-align: right;
line-height: 32px;
margin-bottom: 0;
}
}
p {
margin: 0;
line-height: 1.4;
}
.radio-group {
display: flex;
flex-wrap: wrap;
gap: 8px 20px;
}
.radio-group :deep(.el-radio) {
margin-right: 0;
}
.dialog-footer {
display: flex;
justify-content: flex-end; /* 右下角 */
padding: 0 24px 16px; /* 👈 关键:不贴边 */
}
</style>