Compare commits

...
10 Commits
Author SHA1 Message Date
wsm 9b76e1ebde Merge remote-tracking branch 'origin/master' 2026-08-19 15:34:49 +08:00
wsm 294e683a47 !162 style(supervision): 反馈表格列名称居中
* style(supervision): 反馈表格列名称居中
* style(supervision): 详情弹窗去除标题并固定tab切换位置
* feat(xispeak): 反馈办理情况统计与脉搏一致
2026-08-19 07:34:37 +00:00
wsm 0aa00c4587 style(supervision): 反馈表格列名称居中 2026-08-19 15:32:35 +08:00
wsm e292b6aa07 style(supervision): 详情弹窗去除标题并固定tab切换位置 2026-08-19 15:32:29 +08:00
wsm ec79c4d6e7 feat(xispeak): 反馈办理情况统计与脉搏一致 2026-08-19 15:32:22 +08:00
wsmandGitee 1d1846dcb6 !161 feat(supervision): 反馈表格按状态整行着色与台账统一
Merge pull request !161 from wsm/feature/feedback_info_style
2026-08-19 06:42:48 +00:00
wsm 226a418ca6 feat(supervision): 反馈表格按状态整行着色与台账统一 2026-08-19 14:41:31 +08:00
wsm 44e12e2240 !160 style(supervision): 习讲话与巡视整改详情弹窗样式对齐党委会
* style(supervision): 习讲话与巡视整改详情弹窗样式对齐党委会
2026-08-19 06:11:55 +00:00
wsm 03d4dfebf0 feat(supervision): 各台账导入功能统一为导入弹窗 2026-08-18 13:53:33 +08:00
wsm f6005b2799 feat(bgpartymatter): 下载模板校验导入合并为导入弹窗 2026-08-18 11:16:11 +08:00
17 changed files with 1356 additions and 175 deletions
+17
View File
@@ -293,3 +293,20 @@ html[data-theme='dark'] {
margin-inline: 0;
}
// update-end--author:wangshuai---date:20240611---for:【TV360X-1070】一对多内嵌,为什么多这一块,不从头对齐
/* 反馈表格整行按状态着色(与台账颜色统一:未开始黄/进行中红/已完成绿) */
.feedback-status-row-warning td {
background: #fffbe6 !important;
}
.feedback-status-row-error td {
background: #fff1f0 !important;
}
.feedback-status-row-success td {
background: #f6ffed !important;
}
/* 反馈表格列名称(表头)居中 */
.feedback-table .ant-table-thead > tr > th,
.feedback-inner-table .ant-table-thead > tr > th {
text-align: center !important;
}
@@ -68,15 +68,7 @@
<template #tableTitle>
<a-button type="primary" v-auth="actionAuth.add" @click="handleAdd" preIcon="ant-design:plus-outlined"> 新增</a-button>
<a-button type="primary" v-auth="actionAuth.exportXls" preIcon="ant-design:export-outlined" @click="onExportXls"> 导出</a-button>
<a-button type="primary" v-auth="actionAuth.exportXls" preIcon="ant-design:download-outlined" @click="handleDownloadTemplate"
>下载模板</a-button
>
<j-upload-button type="primary" v-auth="actionAuth.importExcel" preIcon="ant-design:check-outlined" @click="handleCheckExcel"
>模板校验</j-upload-button
>
<j-upload-button type="primary" v-auth="actionAuth.importExcel" preIcon="ant-design:import-outlined" @click="handleImportByTemplate"
>模板导入</j-upload-button
>
<a-button type="primary" v-auth="actionAuth.importExcel" preIcon="ant-design:import-outlined" @click="openImportModal">导入</a-button>
<a-button
v-if="selectedRowKeys.length > 0"
v-auth="actionAuth.deleteBatch"
@@ -140,6 +132,7 @@
:loading="getFeedbackTableLoading(record)"
:pagination="false"
:scroll="{ x: 1400 }"
:rowClassName="feedbackStatusRowClassName"
>
<template #feedbackAttachments="{ record: feedbackRecord }">
<SUploadFile
@@ -164,6 +157,33 @@
<BpmPictureModal @register="registerBpmModal" />
<FlowScheduleStartModalForBG ref="flowScheduleModalRef" @confirm="handleFlowScheduleConfirm" />
<BusinessProcessListModal ref="businessProcessListModalRef" />
<!-- 导入弹窗下载模板 / 模板校验 / 模板导入 -->
<a-modal v-model:open="importModalVisible" title="导入数据" :footer="null" :width="460" centered destroyOnClose>
<div class="import-action-list">
<div v-auth="actionAuth.exportXls" class="import-action-card" @click="handleDownloadTemplateFromModal">
<Icon icon="ant-design:download-outlined" class="import-action-icon" />
<div class="import-action-body">
<div class="import-action-title">下载模板</div>
<div class="import-action-desc">下载标准导入模板按模板格式填写数据</div>
</div>
</div>
<div v-auth="actionAuth.importExcel" class="import-action-card" @click="chooseFile('check')">
<Icon icon="ant-design:check-outlined" class="import-action-icon" />
<div class="import-action-body">
<div class="import-action-title">模板校验</div>
<div class="import-action-desc">校验所选文件是否符合导入规范提前发现错误</div>
</div>
</div>
<div v-auth="actionAuth.importExcel" class="import-action-card" @click="chooseFile('import')">
<Icon icon="ant-design:import-outlined" class="import-action-icon" />
<div class="import-action-body">
<div class="import-action-title">模板导入</div>
<div class="import-action-desc">导入校验通过的模板数据到当前台账</div>
</div>
</div>
</div>
<input ref="fileInputRef" type="file" accept=".xls,.xlsx" style="display: none" @change="onFileSelected" />
</a-modal>
</div>
</template>
@@ -188,6 +208,7 @@
import SUploadFile from '/@/components/semri/fileComponent/SUploadFile.vue';
import { columns, superQuerySchema, bgPartymatterFeedbackColumns } from './BgPartymatter.data';
import { list, statusStats, deleteOne, batchDelete, getImportUrl, getExportXlsByEasyExcelUrl, getImportExcelByEasyExcelUrl, getCheckExcelByEasyExcelUrl, getExportXlsHeadersUrl, bgPartymatterFeedbackList, saveOrUpdate } from './BgPartymatter.api';
import { feedbackStatusRowClassName } from '/@/views/bg/utils/feedbackStatusRender';
import { defHttp } from '/@/utils/http/axios';
import { showImportValidationErrors } from '/@/utils/helper/importError';
import { useMessage } from '/@/hooks/web/useMessage';
@@ -723,6 +744,43 @@
sm: 20,
});
// ================= 导入弹窗(下载模板 / 模板校验 / 模板导入) =================
const importModalVisible = ref(false);
const fileInputRef = ref();
const pendingAction = ref<'check' | 'import' | null>(null);
function openImportModal() {
importModalVisible.value = true;
}
function handleDownloadTemplateFromModal() {
importModalVisible.value = false;
handleDownloadTemplate();
}
/** 触发隐藏文件选择框,记录当前动作(校验 or 导入) */
function chooseFile(action: 'check' | 'import') {
pendingAction.value = action;
fileInputRef.value?.click();
}
/** 文件选择后按动作分发到原校验/导入逻辑 */
function onFileSelected(e) {
const file = e.target.files?.[0];
e.target.value = '';
if (!file) {
return;
}
const action = pendingAction.value;
pendingAction.value = null;
importModalVisible.value = false;
if (action === 'check') {
void handleCheckExcel({ file });
} else if (action === 'import') {
void handleImportByTemplate({ file });
}
}
async function handleDownloadTemplate() {
const data = await defHttp.get({ url: getExportXlsHeadersUrl, params: { meetingType: props.meetingType }, responseType: 'blob', timeout: 60000 }, { isTransformResponse: false });
if (!data) {
@@ -961,4 +1019,48 @@
.status-row-3 td {
background: #f6ffed !important;
}
/* 导入弹窗操作卡片 */
.import-action-list {
display: flex;
flex-direction: column;
gap: 12px;
padding: 4px 8px 8px;
}
.import-action-card {
display: flex;
align-items: center;
gap: 14px;
padding: 14px 16px;
border: 1px solid #e5e6eb;
border-radius: 8px;
background: #fff;
cursor: pointer;
transition: all 0.2s ease;
}
.import-action-card:hover {
border-color: #1677ff;
background: #f0f7ff;
box-shadow: 0 2px 8px rgba(22, 119, 255, 0.15);
transform: translateY(-1px);
}
.import-action-icon {
font-size: 26px;
color: #1677ff;
flex-shrink: 0;
}
.import-action-body {
min-width: 0;
}
.import-action-title {
font-size: 15px;
font-weight: 600;
color: #1f2329;
line-height: 1.4;
}
.import-action-desc {
margin-top: 4px;
font-size: 12px;
color: #8a919f;
line-height: 1.5;
}
</style>
@@ -198,6 +198,7 @@
</a-space>
</div>
<a-table
class="feedback-table"
v-if="showFeedbackDetail"
rowKey="id"
size="small"
@@ -206,6 +207,7 @@
:data-source="feedbackList"
:pagination="false"
:scroll="{ x: feedbackTableScrollX }"
:rowClassName="feedbackStatusRowClassName"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'attachments'">
@@ -293,6 +295,7 @@
import JSelectDept from '/@/components/Form/src/jeecg/components/JSelectDept.vue';
import Icon from '/@/components/Icon/index';
import { bgPartymatterFeedbackDelete, bgPartymatterFeedbackList, saveBpmForm, setBpmVariableLocal } from '../BgPartymatter.api';
import { feedbackStatusRowClassName } from '/@/views/bg/utils/feedbackStatusRender';
import { usePermission } from '/@/hooks/web/usePermission';
import TaskHandleInnerContent from '/@/views/super/bpm/process/personalOffice/myHandleTask/content/TaskHandleInnerContent.vue';
import JDictSelectTag from '../../../../components/Form/src/jeecg/components/JDictSelectTag.vue';
@@ -1,12 +1,11 @@
<template>
<a-modal
v-model:open="visible"
:title="title"
:width="width"
centered
:top="80"
:footer="null"
:maskClosable="true"
:bodyStyle="{ maxHeight: '70vh', overflowY: 'auto', padding: '16px 24px 20px' }"
:bodyStyle="{ maxHeight: '70vh', overflowY: 'auto', padding: '4px 24px 20px' }"
wrapClassName="partymatter-detail-modal"
destroyOnClose
>
@@ -100,6 +99,7 @@
<div class="feedback-summary-desc">当前事项的责任部门办理情况记录</div>
</div>
<a-table
class="feedback-table"
rowKey="id"
size="small"
bordered
@@ -108,6 +108,7 @@
:pagination="false"
:scroll="{ x: 1600 }"
:loading="feedbackLoading"
:rowClassName="feedbackStatusRowClassName"
>
<template #bodyCell="{ column, record: feedbackRecord }">
<template v-if="column.key === 'attachments'">
@@ -132,6 +133,7 @@
import Icon from '/@/components/Icon/index';
import SUploadFile from '/@/components/semri/fileComponent/SUploadFile.vue';
import { bgPartymatterFeedbackList } from '../BgPartymatter.api';
import { feedbackStatusRowClassName } from '/@/views/bg/utils/feedbackStatusRender';
import { bgPartymatterFeedbackColumns } from '../BgPartymatter.data';
import type { BasicColumn } from '/@/components/Table';
import { countFeedbackStatus } from '/@/views/bg/utils/feedbackStatusCount';
@@ -146,7 +148,6 @@
);
const visible = ref(false);
const title = ref('详情');
const width = 1100;
const activeKey = ref('item');
const record = ref<Recordable>({});
@@ -274,7 +275,6 @@
function open(currentRecord: Recordable) {
record.value = currentRecord || {};
title.value = meetingPresentation.value.detailTitle;
activeKey.value = 'item';
visible.value = true;
if (currentRecord?.id !== undefined && currentRecord?.id !== null && currentRecord?.id !== '') {
+111 -13
View File
@@ -103,19 +103,7 @@
<a-button type="primary" v-auth="'bqtakepulse:bg_takepulse:exportXls'" preIcon="ant-design:export-outlined" @click="onExportXls">
导出</a-button
>
<a-button type="primary" v-auth="'bqtakepulse:bg_takepulse:exportXls'" preIcon="ant-design:download-outlined" @click="handleDownloadTemplate"
>下载模板</a-button
>
<j-upload-button type="primary" v-auth="'bqtakepulse:bg_takepulse:importExcel'" preIcon="ant-design:check-outlined" @click="handleCheckExcel"
>模板校验</j-upload-button
>
<j-upload-button
type="primary"
v-auth="'bqtakepulse:bg_takepulse:importExcel'"
preIcon="ant-design:import-outlined"
@click="handleImportByTemplate"
>模板导入</j-upload-button
>
<a-button type="primary" v-auth="'bqtakepulse:bg_takepulse:importExcel'" preIcon="ant-design:import-outlined" @click="openImportModal">导入</a-button>
<a-button v-if="selectedRowKeys.length > 0" v-auth="'bqtakepulse:bg_takepulse:deleteBatch'" danger @click="batchHandleDelete">
<Icon icon="ant-design:delete-outlined"></Icon>
删除
@@ -178,6 +166,7 @@
:loading="getFeedbackTableLoading(record)"
:pagination="false"
:scroll="{ x: 1780 }"
:rowClassName="feedbackStatusRowClassName"
>
<template #feedbackAttachments="{ record: feedbackRecord }">
<SUploadFile
@@ -201,6 +190,33 @@
<BpmPictureModal @register="registerBpmModal" />
<FlowScheduleStartModalForBG ref="flowScheduleModalRef" @confirm="handleFlowScheduleConfirm" />
<BusinessProcessListModal ref="businessProcessListModalRef" />
<!-- 导入弹窗下载模板 / 模板校验 / 模板导入 -->
<a-modal v-model:open="importModalVisible" title="导入数据" :footer="null" :width="460" centered destroyOnClose>
<div class="import-action-list">
<div v-auth="'bqtakepulse:bg_takepulse:exportXls'" class="import-action-card" @click="handleDownloadTemplateFromModal">
<Icon icon="ant-design:download-outlined" class="import-action-icon" />
<div class="import-action-body">
<div class="import-action-title">下载模板</div>
<div class="import-action-desc">下载标准导入模板按模板格式填写数据</div>
</div>
</div>
<div v-auth="'bqtakepulse:bg_takepulse:importExcel'" class="import-action-card" @click="chooseFile('check')">
<Icon icon="ant-design:check-outlined" class="import-action-icon" />
<div class="import-action-body">
<div class="import-action-title">模板校验</div>
<div class="import-action-desc">校验所选文件是否符合导入规范提前发现错误</div>
</div>
</div>
<div v-auth="'bqtakepulse:bg_takepulse:importExcel'" class="import-action-card" @click="chooseFile('import')">
<Icon icon="ant-design:import-outlined" class="import-action-icon" />
<div class="import-action-body">
<div class="import-action-title">模板导入</div>
<div class="import-action-desc">导入校验通过的模板数据到当前台账</div>
</div>
</div>
</div>
<input ref="fileInputRef" type="file" accept=".xls,.xlsx" style="display: none" @change="onFileSelected" />
</a-modal>
</div>
</template>
@@ -229,6 +245,7 @@
import { showImportValidationErrors } from '/@/utils/helper/importError';
import { useMessage } from '/@/hooks/web/useMessage';
import { countFeedbackStatus } from '/@/views/bg/utils/feedbackStatusCount';
import { feedbackStatusRowClassName } from '/@/views/bg/utils/feedbackStatusRender';
import { useDebounceFn } from '@vueuse/core';
import SzSelectDept from '/@/components/Form/src/jeecg/components/SzSelectDept.vue';
import JInput from '/@/components/Form/src/jeecg/components/JInput.vue';
@@ -732,6 +749,43 @@
/**
* 重置
*/
// ================= 导入弹窗(下载模板 / 模板校验 / 模板导入) =================
const importModalVisible = ref(false);
const fileInputRef = ref();
const pendingAction = ref<'check' | 'import' | null>(null);
function openImportModal() {
importModalVisible.value = true;
}
function handleDownloadTemplateFromModal() {
importModalVisible.value = false;
handleDownloadTemplate();
}
/** 触发隐藏文件选择框,记录当前动作(校验 or 导入) */
function chooseFile(action: 'check' | 'import') {
pendingAction.value = action;
fileInputRef.value?.click();
}
/** 文件选择后按动作分发到原校验/导入逻辑 */
function onFileSelected(e) {
const file = e.target.files?.[0];
e.target.value = '';
if (!file) {
return;
}
const action = pendingAction.value;
pendingAction.value = null;
importModalVisible.value = false;
if (action === 'check') {
void handleCheckExcel({ file });
} else if (action === 'import') {
void handleImportByTemplate({ file });
}
}
async function handleDownloadTemplate() {
const data = await defHttp.get({ url: getExportXlsHeadersUrl, params: {}, responseType: 'blob', timeout: 60000 }, { isTransformResponse: false });
if (!data) {
@@ -979,4 +1033,48 @@
.status-row-3 td {
background: #f6ffed !important;
}
/* 导入弹窗操作卡片 */
.import-action-list {
display: flex;
flex-direction: column;
gap: 12px;
padding: 4px 8px 8px;
}
.import-action-card {
display: flex;
align-items: center;
gap: 14px;
padding: 14px 16px;
border: 1px solid #e5e6eb;
border-radius: 8px;
background: #fff;
cursor: pointer;
transition: all 0.2s ease;
}
.import-action-card:hover {
border-color: #1677ff;
background: #f0f7ff;
box-shadow: 0 2px 8px rgba(22, 119, 255, 0.15);
transform: translateY(-1px);
}
.import-action-icon {
font-size: 26px;
color: #1677ff;
flex-shrink: 0;
}
.import-action-body {
min-width: 0;
}
.import-action-title {
font-size: 15px;
font-weight: 600;
color: #1f2329;
line-height: 1.4;
}
.import-action-desc {
margin-top: 4px;
font-size: 12px;
color: #8a919f;
line-height: 1.5;
}
</style>
@@ -223,6 +223,7 @@
</a-space>
</div>
<a-table
class="feedback-table"
v-if="showFeedbackDetail"
rowKey="id"
size="small"
@@ -231,6 +232,7 @@
:data-source="feedbackList"
:pagination="false"
:scroll="{ x: feedbackTableScrollX }"
:rowClassName="feedbackStatusRowClassName"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'attachments'">
@@ -316,6 +318,7 @@
import JSwitch from '/@/components/Form/src/jeecg/components/JSwitch.vue';
import Icon from '/@/components/Icon/index';
import { bgTakepulseFeedbackDelete, bgTakepulseFeedbackList, saveBpmForm, setBpmVariableLocal } from '../BgTakepulse.api';
import { feedbackStatusRowClassName } from '/@/views/bg/utils/feedbackStatusRender';
import { usePermission } from '/@/hooks/web/usePermission';
import TaskHandleInnerContent from '/@/views/super/bpm/process/personalOffice/myHandleTask/content/TaskHandleInnerContent.vue';
import BgTakepulseBPMFeedbackModal from './BgTakepulseBPMFeedbackModal.vue';
@@ -1,12 +1,11 @@
<template>
<a-modal
v-model:open="visible"
:title="title"
:width="width"
centered
:top="80"
:footer="null"
:maskClosable="true"
:bodyStyle="{ maxHeight: '70vh', overflowY: 'auto', padding: '16px 24px 20px' }"
:bodyStyle="{ maxHeight: '70vh', overflowY: 'auto', padding: '4px 24px 20px' }"
wrapClassName="takepulse-detail-modal"
destroyOnClose
>
@@ -115,6 +114,7 @@
<div class="feedback-summary-desc">当前事项的责任部门办理情况记录</div>
</div>
<a-table
class="feedback-table"
rowKey="id"
size="small"
bordered
@@ -123,6 +123,7 @@
:pagination="false"
:scroll="{ x: 1800 }"
:loading="feedbackLoading"
:rowClassName="feedbackStatusRowClassName"
>
<template #bodyCell="{ column, record: feedbackRecord }">
<template v-if="column.key === 'attachments'">
@@ -149,9 +150,9 @@
import { bgTakepulseFeedbackList } from '../BgTakepulse.api';
import type { BasicColumn } from '/@/components/Table';
import { countFeedbackStatus } from '/@/views/bg/utils/feedbackStatusCount';
import { feedbackStatusRowClassName } from '/@/views/bg/utils/feedbackStatusRender';
const visible = ref(false);
const title = ref('详情');
const width = 1100;
const activeKey = ref('item');
const record = ref<Recordable>({});
@@ -300,7 +301,6 @@
function open(currentRecord: Recordable) {
record.value = currentRecord || {};
title.value = '把把脉详情';
activeKey.value = 'item';
visible.value = true;
if (currentRecord?.id !== undefined && currentRecord?.id !== null && currentRecord?.id !== '') {
@@ -99,9 +99,7 @@
<template #tableTitle>
<a-button type="primary" v-auth="actionAuth.add" @click="handleAdd" preIcon="ant-design:plus-outlined"> 新增</a-button>
<a-button type="primary" v-auth="actionAuth.exportXls" preIcon="ant-design:export-outlined" @click="onExportXls"> 导出</a-button>
<a-button type="primary" v-auth="actionAuth.exportXls" preIcon="ant-design:download-outlined" @click="handleDownloadTemplate">下载模板</a-button>
<j-upload-button type="primary" v-auth="actionAuth.importExcel" preIcon="ant-design:check-outlined" @click="handleCheckExcel">模板校验</j-upload-button>
<j-upload-button type="primary" v-auth="actionAuth.importExcel" preIcon="ant-design:import-outlined" @click="handleImportByTemplate">模板导入</j-upload-button>
<a-button type="primary" v-auth="actionAuth.importExcel" preIcon="ant-design:import-outlined" @click="openImportModal">导入</a-button>
<a-button
v-if="selectedRowKeys.length > 0"
v-auth="actionAuth.deleteBatch"
@@ -174,6 +172,33 @@
<FixedContact20260730Modal ref="registerModal" @success="handleSuccess" />
<FlowScheduleStartModalForBG ref="flowScheduleModalRef" @confirm="handleFlowScheduleConfirm" />
<BusinessProcessListModal ref="businessProcessListModalRef" />
<!-- 导入弹窗下载模板 / 模板校验 / 模板导入 -->
<a-modal v-model:open="importModalVisible" title="导入数据" :footer="null" :width="460" centered destroyOnClose>
<div class="import-action-list">
<div v-auth="actionAuth.exportXls" class="import-action-card" @click="handleDownloadTemplateFromModal">
<Icon icon="ant-design:download-outlined" class="import-action-icon" />
<div class="import-action-body">
<div class="import-action-title">下载模板</div>
<div class="import-action-desc">下载标准导入模板按模板格式填写数据</div>
</div>
</div>
<div v-auth="actionAuth.importExcel" class="import-action-card" @click="chooseFile('check')">
<Icon icon="ant-design:check-outlined" class="import-action-icon" />
<div class="import-action-body">
<div class="import-action-title">模板校验</div>
<div class="import-action-desc">校验所选文件是否符合导入规范提前发现错误</div>
</div>
</div>
<div v-auth="actionAuth.importExcel" class="import-action-card" @click="chooseFile('import')">
<Icon icon="ant-design:import-outlined" class="import-action-icon" />
<div class="import-action-body">
<div class="import-action-title">模板导入</div>
<div class="import-action-desc">导入校验通过的模板数据到当前台账</div>
</div>
</div>
</div>
<input ref="fileInputRef" type="file" accept=".xls,.xlsx" style="display: none" @change="onFileSelected" />
</a-modal>
</div>
</template>
@@ -635,6 +660,43 @@ import { showImportValidationErrors } from '/@/utils/helper/importError';
sm: { span: 14 },
});
// ================= 导入弹窗(下载模板 / 模板校验 / 模板导入) =================
const importModalVisible = ref(false);
const fileInputRef = ref();
const pendingAction = ref<'check' | 'import' | null>(null);
function openImportModal() {
importModalVisible.value = true;
}
function handleDownloadTemplateFromModal() {
importModalVisible.value = false;
handleDownloadTemplate();
}
/** 触发隐藏文件选择框,记录当前动作(校验 or 导入) */
function chooseFile(action: 'check' | 'import') {
pendingAction.value = action;
fileInputRef.value?.click();
}
/** 文件选择后按动作分发到原校验/导入逻辑 */
function onFileSelected(e) {
const file = e.target.files?.[0];
e.target.value = '';
if (!file) {
return;
}
const action = pendingAction.value;
pendingAction.value = null;
importModalVisible.value = false;
if (action === 'check') {
void handleCheckExcel({ file });
} else if (action === 'import') {
void handleImportByTemplate({ file });
}
}
async function handleDownloadTemplate() {
let data;
try {
@@ -841,3 +903,50 @@ import { showImportValidationErrors } from '/@/utils/helper/importError';
background: #f6ffed !important;
}
</style>
<style lang="less">
/* 导入弹窗操作卡片 */
.import-action-list {
display: flex;
flex-direction: column;
gap: 12px;
padding: 4px 8px 8px;
}
.import-action-card {
display: flex;
align-items: center;
gap: 14px;
padding: 14px 16px;
border: 1px solid #e5e6eb;
border-radius: 8px;
background: #fff;
cursor: pointer;
transition: all 0.2s ease;
}
.import-action-card:hover {
border-color: #1677ff;
background: #f0f7ff;
box-shadow: 0 2px 8px rgba(22, 119, 255, 0.15);
transform: translateY(-1px);
}
.import-action-icon {
font-size: 26px;
color: #1677ff;
flex-shrink: 0;
}
.import-action-body {
min-width: 0;
}
.import-action-title {
font-size: 15px;
font-weight: 600;
color: #1f2329;
line-height: 1.4;
}
.import-action-desc {
margin-top: 4px;
font-size: 12px;
color: #8a919f;
line-height: 1.5;
}
</style>
@@ -0,0 +1,37 @@
/**
* 反馈状态颜色工具:与台账颜色统一
* 未开始 -> warning(黄) / 进行中·督办中·整改中 -> error(红) / 已完成 -> success(绿)
*
* 说明:反馈字典(feedback_status)的码值与台账字典(super_status)不一致
* (反馈"已完成"码值为 '2',台账为 '3'),因此按显示的字典文本判定状态,
* 不依赖状态码,避免码值差异导致颜色失效。
*/
/** 根据字典文本返回状态颜色(warning/error/success */
export function feedbackStatusColorByText(text: string | number | undefined | null): string | undefined {
const t = String(text ?? '');
if (t.includes('未开始') || t.includes('未完成')) return 'warning';
if (t.includes('进行中') || t.includes('督办中') || t.includes('整改中') || t.includes('处理中')) return 'error';
if (t.includes('已完成') || t === '完成') return 'success';
return undefined;
}
/**
* 反馈表格整行背景色 class(表格 :rowClassName 使用)
* 自动兼容 bpmStatus / completionStatus 两套字段名,按字典文本判定状态。
* 对应全局样式:feedback-status-row-warning/error/success(定义于 /@/design/index.less
*/
export function feedbackStatusRowClassName(record: Recordable | undefined | null): string {
const text = String(
record?.['bpmStatus_dictText'] ??
record?.['completionStatus_dictText'] ??
record?.['bpmStatus'] ??
record?.['completionStatus'] ??
'',
);
const color = feedbackStatusColorByText(text);
if (color === 'warning') return 'feedback-status-row-warning';
if (color === 'error') return 'feedback-status-row-error';
if (color === 'success') return 'feedback-status-row-success';
return '';
}
+112 -9
View File
@@ -72,15 +72,7 @@
<!-- <j-upload-button type="primary" v-auth="'bg.xispeak:bg_xi_speak:importExcel'" preIcon="ant-design:import-outlined" @click="onImportXls"-->
<!-- >导入</j-upload-button-->
<!-- >-->
<a-button type="primary" v-auth="'bg.xispeak:bg_xi_speak:exportXls'" preIcon="ant-design:download-outlined" @click="handleDownloadTemplate"
>下载模板</a-button
>
<j-upload-button type="primary" v-auth="'bg.xispeak:bg_xi_speak:importExcel'" preIcon="ant-design:check-outlined" @click="handleCheckExcel"
>模板校验</j-upload-button
>
<j-upload-button type="primary" v-auth="'bg.xispeak:bg_xi_speak:importExcel'" preIcon="ant-design:import-outlined" @click="handleImportByTemplate"
>模板导入</j-upload-button
>
<a-button type="primary" v-auth="'bg.xispeak:bg_xi_speak:importExcel'" preIcon="ant-design:import-outlined" @click="openImportModal">导入</a-button>
<a-button
v-if="selectedRowKeys.length > 0"
v-auth="'bg.xispeak:bg_xi_speak:deleteBatch'"
@@ -127,6 +119,33 @@
<FlowScheduleStartModal ref="flowScheduleModalRef" @confirm="handleFlowScheduleConfirm" />
<FeedbackRightNowModal ref="feedbackRightNowModalRef" @confirm="handleFeedbackRightNowConfirm" />
<BusinessProcessListModal ref="businessProcessListModalRef" />
<!-- 导入弹窗下载模板 / 模板校验 / 模板导入 -->
<a-modal v-model:open="importModalVisible" title="导入数据" :footer="null" :width="460" centered destroyOnClose>
<div class="import-action-list">
<div v-auth="'bg.xispeak:bg_xi_speak:exportXls'" class="import-action-card" @click="handleDownloadTemplateFromModal">
<Icon icon="ant-design:download-outlined" class="import-action-icon" />
<div class="import-action-body">
<div class="import-action-title">下载模板</div>
<div class="import-action-desc">下载标准导入模板按模板格式填写数据</div>
</div>
</div>
<div v-auth="'bg.xispeak:bg_xi_speak:importExcel'" class="import-action-card" @click="chooseFile('check')">
<Icon icon="ant-design:check-outlined" class="import-action-icon" />
<div class="import-action-body">
<div class="import-action-title">模板校验</div>
<div class="import-action-desc">校验所选文件是否符合导入规范提前发现错误</div>
</div>
</div>
<div v-auth="'bg.xispeak:bg_xi_speak:importExcel'" class="import-action-card" @click="chooseFile('import')">
<Icon icon="ant-design:import-outlined" class="import-action-icon" />
<div class="import-action-body">
<div class="import-action-title">模板导入</div>
<div class="import-action-desc">导入校验通过的模板数据到当前台账</div>
</div>
</div>
</div>
<input ref="fileInputRef" type="file" accept=".xls,.xlsx" style="display: none" @change="onFileSelected" />
</a-modal>
</div>
</template>
@@ -832,6 +851,43 @@ import { PARTY_COMMITTEE_DEPT_ID, SDW_LEADER_ROLE_ID } from '/@/constant/supervi
feedbackViewModalRef.value.open(record);
}
// ================= 导入弹窗(下载模板 / 模板校验 / 模板导入) =================
const importModalVisible = ref(false);
const fileInputRef = ref();
const pendingAction = ref<'check' | 'import' | null>(null);
function openImportModal() {
importModalVisible.value = true;
}
function handleDownloadTemplateFromModal() {
importModalVisible.value = false;
handleDownloadTemplate();
}
/** 触发隐藏文件选择框,记录当前动作(校验 or 导入) */
function chooseFile(action: 'check' | 'import') {
pendingAction.value = action;
fileInputRef.value?.click();
}
/** 文件选择后按动作分发到原校验/导入逻辑 */
function onFileSelected(e) {
const file = e.target.files?.[0];
e.target.value = '';
if (!file) {
return;
}
const action = pendingAction.value;
pendingAction.value = null;
importModalVisible.value = false;
if (action === 'check') {
void handleCheckExcel({ file });
} else if (action === 'import') {
void handleImportByTemplate({ file });
}
}
async function handleDownloadTemplate() {
const data = await defHttp.get(
{ url: getExportXlsHeadersUrl, params: {}, responseType: 'blob', timeout: 60000 },
@@ -994,3 +1050,50 @@ import { PARTY_COMMITTEE_DEPT_ID, SDW_LEADER_ROLE_ID } from '/@/constant/supervi
background: #f6ffed !important;
}
</style>
<style lang="less">
/* 导入弹窗操作卡片 */
.import-action-list {
display: flex;
flex-direction: column;
gap: 12px;
padding: 4px 8px 8px;
}
.import-action-card {
display: flex;
align-items: center;
gap: 14px;
padding: 14px 16px;
border: 1px solid #e5e6eb;
border-radius: 8px;
background: #fff;
cursor: pointer;
transition: all 0.2s ease;
}
.import-action-card:hover {
border-color: #1677ff;
background: #f0f7ff;
box-shadow: 0 2px 8px rgba(22, 119, 255, 0.15);
transform: translateY(-1px);
}
.import-action-icon {
font-size: 26px;
color: #1677ff;
flex-shrink: 0;
}
.import-action-body {
min-width: 0;
}
.import-action-title {
font-size: 15px;
font-weight: 600;
color: #1f2329;
line-height: 1.4;
}
.import-action-desc {
margin-top: 4px;
font-size: 12px;
color: #8a919f;
line-height: 1.5;
}
</style>
@@ -197,6 +197,7 @@
</a-space>
</div>
<a-table
class="feedback-table"
v-if="showFeedbackInfo && showFeedbackDetail"
rowKey="id"
size="small"
@@ -205,6 +206,7 @@
:data-source="feedbackList"
:pagination="false"
:scroll="{ x: 1980 }"
:rowClassName="feedbackStatusRowClassName"
>
<template #bodyCell="{ column, record, index }">
<template v-if="column.key === 'seq'">{{ index + 1 }}</template>
@@ -286,6 +288,7 @@
import JDictSelectTag from '/@/components/Form/src/jeecg/components/JDictSelectTag.vue';
import Icon from '/@/components/Icon/index';
import { bgXiSpeakFeedbackList, bgXiSpeakFeedbackDelete, saveBpmForm, saveSubApprove, getProcessVariable } from '../BgXiSpeak.api';
import { feedbackStatusRowClassName } from '/@/views/bg/utils/feedbackStatusRender';
import { useSubApproveUserResolver } from '/@/composables/useSubApproveUserResolver';
import { usePermission } from '/@/hooks/web/usePermission';
import TaskHandleInnerContent from '/@/views/super/bpm/process/personalOffice/myHandleTask/content/TaskHandleInnerContent.vue';
@@ -1,26 +1,60 @@
<template>
<a-modal
v-model:open="visible"
:title="title"
:width="width"
centered
:top="80"
:footer="null"
:maskClosable="true"
:bodyStyle="{ maxHeight: '70vh', overflowY: 'auto', padding: '16px 24px 20px' }"
:bodyStyle="{ maxHeight: '70vh', overflowY: 'auto', padding: '4px 24px 20px' }"
wrapClassName="xispeak-detail-modal"
destroyOnClose
>
<a-tabs v-model:activeKey="activeKey">
<a-tab-pane key="item" tab="事项信息">
<div class="detail-grid">
<div
v-for="field in detailFields"
:key="field.label"
class="detail-row"
:class="{ 'detail-row-full': field.fullWidth, 'detail-row-bold': field.bold }"
>
<span class="detail-label">{{ field.label }}</span>
<span class="detail-value">{{ field.value }}</span>
</div>
<div class="matter-detail">
<section class="matter-summary">
<div class="summary-heading">
<span class="summary-heading-icon">
<Icon :icon="summaryIcon" />
</span>
<div class="summary-heading-content">
<span class="summary-kicker">{{ summaryKicker }}</span>
<h3 class="summary-title">{{ detailSummary.title }}</h3>
</div>
</div>
<div class="summary-meta">
<div class="summary-meta-item">
<Icon icon="ant-design:calendar-outlined" />
<span class="summary-meta-label">时间</span>
<span class="summary-meta-value">{{ detailSummary.time }}</span>
</div>
<div class="summary-meta-item">
<Icon icon="ant-design:lock-outlined" />
<span class="summary-meta-label">密级</span>
<span class="summary-meta-value">{{ detailSummary.secretLevel }}</span>
</div>
<div class="summary-tag-group">
<a-tag class="summary-tag">截止日期{{ detailSummary.deadline }}</a-tag>
<a-tag class="summary-tag">督办次数{{ detailSummary.supervisionCount }}</a-tag>
</div>
</div>
</section>
<section v-for="group in detailGroups" :key="group.title" class="detail-section">
<div class="detail-section-header">
<span class="detail-section-icon">
<Icon :icon="group.icon" />
</span>
<h3>{{ group.title }}</h3>
</div>
<div class="detail-grid">
<div v-for="field in group.fields" :key="field.label" class="detail-item" :class="{ 'detail-item-full': field.fullWidth }">
<span class="detail-label">{{ field.label }}</span>
<span class="detail-value" :class="{ 'detail-value-strong': field.bold }">{{ field.value }}</span>
</div>
</div>
</section>
</div>
</a-tab-pane>
@@ -31,12 +65,15 @@
<Icon icon="ant-design:message-outlined" />
</span>
<span>办理情况</span>
<a-tag class="feedback-summary-tag"> {{ total }} </a-tag>
<a-tag class="feedback-summary-tag" color="success">已完成 {{ done }} </a-tag>
<a-tag class="feedback-summary-tag" color="warning" style="color: #8c6900">未开始 {{ count.notStarted }}</a-tag>
<a-tag class="feedback-summary-tag" color="error">进行中 {{ count.inProgress }}已逾期 {{ count.overdue }}</a-tag>
<a-tag class="feedback-summary-tag" color="success">已完成 {{ count.completed }}</a-tag>
<a-tag class="feedback-summary-tag"> {{ count.total }} </a-tag>
</div>
<div class="feedback-summary-desc">当前事项的落实情况反馈记录</div>
</div>
<a-table
class="feedback-table"
rowKey="id"
size="small"
bordered
@@ -45,6 +82,7 @@
:pagination="false"
:scroll="{ x: 1500 }"
:loading="feedbackLoading"
:rowClassName="feedbackStatusRowClassName"
>
<template #bodyCell="{ column, record: fb }">
<template v-if="column.key === 'action'">
@@ -74,10 +112,11 @@
import SUploadFile from '/@/components/semri/fileComponent/SUploadFile.vue';
import BgXiSpeakFeedbackDetailModal from './BgXiSpeakFeedbackDetailModal.vue';
import { bgXiSpeakFeedbackList } from '../BgXiSpeak.api';
import { feedbackStatusRowClassName } from '/@/views/bg/utils/feedbackStatusRender';
import { countFeedbackStatus } from '/@/views/bg/utils/feedbackStatusCount';
import type { BasicColumn } from '/@/components/Table';
const visible = ref(false);
const title = ref('详情');
const width = 1100;
const activeKey = ref('item');
const record = ref<Recordable>({});
@@ -103,25 +142,59 @@
return fmt(record[`${key}_dictText`] || record[key]);
}
const detailFields = computed(() => {
const summaryKicker = '习讲话事项';
const summaryIcon = 'ant-design:sound-outlined';
// 摘要区只承载最常用于快速判断事项状态的信息,详细字段放入下方分组,避免重复平铺。
const detailSummary = computed(() => {
const r = record.value || {};
return {
title: fmt(r.speakStatus),
time: fmtDate(r.time),
secretLevel: dictText(r, 'secretLevel'),
deadline: fmtDate(r.deadline),
supervisionCount: fmt(r.supervisionCount),
};
});
const detailGroups = computed(() => {
const r = record.value || {};
return [
{ label: '重要讲话指示批示', value: fmt(r.speakStatus), fullWidth: true, bold: true },
{ label: '时间', value: fmtDate(r.time), bold: true },
{ label: '企业负责同志批示情况', value: fmt(r.elmComment) },
{ label: '学习传达研究部署', value: fmt(r.status) },
{ label: '密级', value: dictText(r, 'secretLevel') },
{ label: '所党委责任领导', value: dictText(r, 'responsiblePerson'), bold: true },
{ label: '牵头部门', value: dictText(r, 'implDept'), bold: true },
{ label: '贯彻落实措施', value: fmt(r.implMeasures) },
{ label: '会议决策数', value: fmt(r.numberOfResolutions) },
{ label: '是否需要所党委领导督办', value: ynText(r.needSdwApproval) },
{ label: '所党委领导列表', value: dictText(r, 'sdwLeaderList') },
{ label: '指定落实措施条数', value: fmt(r.implCount) },
{ label: '措施已闭环数量', value: fmt(r.closedCount) },
{ label: '报告反馈情况', value: fmt(r.reportFeedbackStatus) },
{ label: '截止日期', value: fmtDate(r.deadline) },
{ label: '督办次数', value: fmt(r.supervisionCount) },
{
title: '批示与传达',
icon: 'ant-design:notification-outlined',
fields: [
{ label: '企业负责同志批示情况', value: fmt(r.elmComment) },
{ label: '学习传达研究部署', value: fmt(r.status) },
],
},
{
title: '责任与落实',
icon: 'ant-design:team-outlined',
fields: [
{ label: '所党委责任领导', value: dictText(r, 'responsiblePerson'), bold: true },
{ label: '牵头部门', value: dictText(r, 'implDept'), bold: true },
{ label: '贯彻落实措施', value: fmt(r.implMeasures), fullWidth: true },
],
},
{
title: '决策与进展',
icon: 'ant-design:fund-outlined',
fields: [
{ label: '会议决策数', value: fmt(r.numberOfResolutions) },
{ label: '指定落实措施条数', value: fmt(r.implCount) },
{ label: '措施已闭环数量', value: fmt(r.closedCount) },
{ label: '报告反馈情况', value: fmt(r.reportFeedbackStatus) },
],
},
{
title: '审批设置',
icon: 'ant-design:safety-outlined',
fields: [
{ label: '是否需要所党委领导督办', value: ynText(r.needSdwApproval) },
{ label: '所党委领导列表', value: dictText(r, 'sdwLeaderList'), fullWidth: true },
],
},
];
});
@@ -138,8 +211,7 @@
{ title: '操作', dataIndex: 'action', key: 'action', align: 'center', width: 90, fixed: 'right' },
];
const total = computed(() => feedbackList.value.length);
const done = computed(() => feedbackList.value.filter((item) => item.completionStatus_dictText === '已完成').length);
const count = computed(() => countFeedbackStatus(feedbackList.value, 'completionStatus', 'deadline'));
async function loadFeedback(mainId: string | number) {
feedbackLoading.value = true;
@@ -164,7 +236,6 @@
function open(currentRecord: Recordable) {
record.value = currentRecord || {};
title.value = '详情';
activeKey.value = 'item';
visible.value = true;
if (currentRecord?.id !== undefined && currentRecord?.id !== null && currentRecord?.id !== '') {
@@ -176,40 +247,265 @@
</script>
<style lang="less" scoped>
.detail-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px 32px;
padding: 4px 0;
// 弹窗内容会 Teleport 到 body,通过专属类限定公共组件的字号,避免影响其他页面。
:global(.xispeak-detail-modal .ant-modal-title) {
font-size: 18px;
line-height: 26px;
}
.detail-row {
:global(.xispeak-detail-modal .ant-tabs-tab-btn) {
font-size: 15px;
line-height: 24px;
}
:global(.xispeak-detail-modal .ant-table) {
font-size: 15px;
}
:global(.xispeak-detail-modal .ant-table-thead > tr > th),
:global(.xispeak-detail-modal .ant-table-tbody > tr > td) {
font-size: 15px;
line-height: 24px;
}
.matter-detail {
padding: 4px 2px 10px;
}
.matter-summary {
position: relative;
overflow: hidden;
margin-bottom: 16px;
padding: 20px 22px;
background: linear-gradient(135deg, #f2f8ff 0%, #f8fbff 48%, #ffffff 100%);
border: 1px solid #d6e8ff;
border-radius: 10px;
box-shadow: 0 4px 14px rgb(22 119 255 / 8%);
&::after {
position: absolute;
top: -46px;
right: -24px;
width: 132px;
height: 132px;
background: rgb(22 119 255 / 5%);
border-radius: 50%;
content: '';
pointer-events: none;
}
}
.summary-heading {
position: relative;
z-index: 1;
display: flex;
align-items: flex-start;
gap: 12px;
}
.summary-heading-icon {
display: inline-flex;
flex: 0 0 40px;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
color: #1677ff;
font-size: 20px;
background: #e6f4ff;
border-radius: 10px;
}
.summary-heading-content {
min-width: 0;
}
.detail-row-full {
.summary-kicker {
display: block;
margin-bottom: 2px;
color: #64748b;
font-size: 14px;
line-height: 20px;
}
.summary-title {
margin: 0;
color: #172033;
font-size: 21px;
font-weight: 600;
line-height: 30px;
overflow-wrap: anywhere;
}
.summary-meta {
position: relative;
z-index: 1;
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 10px 18px;
margin-top: 16px;
padding-top: 14px;
border-top: 1px solid #deebfa;
}
.summary-meta-item {
display: inline-flex;
align-items: center;
gap: 6px;
min-height: 24px;
color: #64748b;
> :first-child {
color: #1677ff;
font-size: 15px;
}
}
.summary-meta-label {
font-size: 14px;
}
.summary-meta-value {
color: #1f2937;
font-size: 15px;
font-weight: 600;
}
.summary-tag-group {
display: inline-flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
min-width: 0;
}
.summary-tag {
margin-inline-end: 0;
padding: 2px 10px;
font-size: 14px;
line-height: 22px;
white-space: normal;
}
.detail-section {
overflow: hidden;
margin-top: 14px;
background: #ffffff;
border: 1px solid #e7edf4;
border-radius: 8px;
}
.detail-section-header {
display: flex;
align-items: center;
gap: 8px;
min-height: 44px;
padding: 10px 16px;
background: #f8fbff;
border-bottom: 1px solid #e7edf4;
h3 {
margin: 0;
color: #27364b;
font-size: 16px;
font-weight: 600;
line-height: 24px;
}
}
.detail-section-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 26px;
height: 26px;
color: #1677ff;
background: #e6f4ff;
border-radius: 6px;
}
.detail-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 1px;
background: #edf1f5;
}
.detail-item {
display: grid;
grid-template-columns: 142px minmax(0, 1fr);
align-items: start;
gap: 12px;
min-width: 0;
min-height: 54px;
padding: 14px 16px;
background: #ffffff;
}
.detail-item-full {
grid-column: 1 / -1;
}
.detail-label {
flex: 0 0 150px;
color: #8c8c8c;
line-height: 22px;
color: #7b8798;
font-size: 14px;
line-height: 26px;
}
.detail-value {
flex: 1;
color: #1f2937;
line-height: 22px;
word-break: break-all;
min-width: 0;
color: #27364b;
font-size: 15px;
line-height: 26px;
white-space: pre-wrap;
overflow-wrap: anywhere;
}
.detail-row-bold .detail-value {
font-weight: 700;
font-size: 15px;
.detail-value-strong {
color: #172033;
font-weight: 600;
}
@media (max-width: 768px) {
.matter-summary {
padding: 16px;
}
.summary-meta {
align-items: flex-start;
}
.summary-tag-group {
flex-basis: 100%;
}
.detail-grid {
grid-template-columns: minmax(0, 1fr);
}
.detail-item-full {
grid-column: auto;
}
}
@media (max-width: 576px) {
.summary-title {
font-size: 19px;
line-height: 27px;
}
.summary-meta {
display: grid;
grid-template-columns: minmax(0, 1fr);
gap: 8px;
}
.detail-item {
grid-template-columns: minmax(0, 1fr);
gap: 2px;
padding: 11px 14px;
}
}
.feedback-summary-header {
@@ -8,6 +8,7 @@
cancelText="关闭"
>
<BasicTable
class="feedback-table"
size="small"
rowKey="id"
:canResize="false"
@@ -19,6 +20,7 @@
:columns="columns"
:dataSource="feedbackTableData"
:loading="loading"
:rowClassName="feedbackStatusRowClassName"
:pagination="false"
:bordered="false"
>
@@ -50,6 +52,7 @@
import SUploadFile from '/@/components/semri/fileComponent/SUploadFile.vue';
import BgXiSpeakFeedbackDetailModal from './BgXiSpeakFeedbackDetailModal.vue';
import { bgXiSpeakFeedbackList } from '../BgXiSpeak.api';
import { feedbackStatusRowClassName } from '/@/views/bg/utils/feedbackStatusRender';
const visible = ref(false);
const loading = ref(false);
@@ -8,6 +8,7 @@
cancelText="关闭"
>
<BasicTable
class="feedback-table"
size="small"
rowKey="id"
:canResize="false"
@@ -19,6 +20,7 @@
:dataSource="feedbackTableData"
:loading="loading"
:pagination="false"
:rowClassName="feedbackStatusRowClassName"
:bordered="false"
>
<template #bodyCell="{ column, record }">
@@ -44,6 +46,7 @@
import JModal from '/@/components/Modal/src/JModal/JModal.vue';
import SUploadFile from '/@/components/semri/fileComponent/SUploadFile.vue';
import { list } from '../DqInspectProgress.api';
import { feedbackStatusRowClassName } from '/@/views/bg/utils/feedbackStatusRender';
const visible = ref(false);
const loading = ref(false);
+113 -25
View File
@@ -59,30 +59,7 @@
<a-button type="primary" v-auth="'dqinspecttask:dq_inspect_task:exportXls'" preIcon="ant-design:export-outlined" @click="onExportXls">
导出</a-button
>
<j-upload-button type="primary" v-auth="'dqinspecttask:dq_inspect_task:importExcel'" preIcon="ant-design:import-outlined" @click="onImportXls"
>导入</j-upload-button
>
<a-button
type="primary"
v-auth="'dqinspecttask:dq_inspect_task:exportXls'"
preIcon="ant-design:download-outlined"
@click="handleDownloadTemplate"
>下载模板</a-button
>
<j-upload-button
type="primary"
v-auth="'dqinspecttask:dq_inspect_task:importExcel'"
preIcon="ant-design:check-outlined"
@click="handleCheckExcel"
>模板校验</j-upload-button
>
<j-upload-button
type="primary"
v-auth="'dqinspecttask:dq_inspect_task:importExcel'"
preIcon="ant-design:import-outlined"
@click="handleImportByTemplate"
>模板导入</j-upload-button
>
<a-button type="primary" v-auth="'dqinspecttask:dq_inspect_task:importExcel'" preIcon="ant-design:import-outlined" @click="openImportModal">导入</a-button>
<a-button
v-if="selectedRowKeys.length > 0"
@@ -126,6 +103,33 @@
<BpmPictureModal @register="registerBpmModal" />
<FlowScheduleStartModal ref="flowScheduleModalRef" @confirm="handleFlowScheduleConfirm" />
<BusinessProcessListModal ref="businessProcessListModalRef" />
<!-- 导入弹窗下载模板 / 模板校验 / 模板导入 -->
<a-modal v-model:open="importModalVisible" title="导入数据" :footer="null" :width="460" centered destroyOnClose>
<div class="import-action-list">
<div v-auth="'dqinspecttask:dq_inspect_task:exportXls'" class="import-action-card" @click="handleDownloadTemplateFromModal">
<Icon icon="ant-design:download-outlined" class="import-action-icon" />
<div class="import-action-body">
<div class="import-action-title">下载模板</div>
<div class="import-action-desc">下载标准导入模板按模板格式填写数据</div>
</div>
</div>
<div v-auth="'dqinspecttask:dq_inspect_task:importExcel'" class="import-action-card" @click="chooseFile('check')">
<Icon icon="ant-design:check-outlined" class="import-action-icon" />
<div class="import-action-body">
<div class="import-action-title">模板校验</div>
<div class="import-action-desc">校验所选文件是否符合导入规范提前发现错误</div>
</div>
</div>
<div v-auth="'dqinspecttask:dq_inspect_task:importExcel'" class="import-action-card" @click="chooseFile('import')">
<Icon icon="ant-design:import-outlined" class="import-action-icon" />
<div class="import-action-body">
<div class="import-action-title">模板导入</div>
<div class="import-action-desc">导入校验通过的模板数据到当前台账</div>
</div>
</div>
</div>
<input ref="fileInputRef" type="file" accept=".xls,.xlsx" style="display: none" @change="onFileSelected" />
</a-modal>
</div>
</template>
@@ -180,7 +184,7 @@
const userStore = useUserStore();
const { createMessage } = useMessage();
//注册table数据
const { prefixCls, tableContext, onExportXls, onImportXls } = useListPage({
const { prefixCls, tableContext, onExportXls } = useListPage({
tableProps: {
title: 'dq_inspect_task',
api: ledgerList,
@@ -295,6 +299,43 @@
(selectedRowKeys.value = []) && reload();
}
// ================= 导入弹窗(下载模板 / 模板校验 / 模板导入) =================
const importModalVisible = ref(false);
const fileInputRef = ref();
const pendingAction = ref<'check' | 'import' | null>(null);
function openImportModal() {
importModalVisible.value = true;
}
function handleDownloadTemplateFromModal() {
importModalVisible.value = false;
handleDownloadTemplate();
}
/** 触发隐藏文件选择框,记录当前动作(校验 or 导入) */
function chooseFile(action: 'check' | 'import') {
pendingAction.value = action;
fileInputRef.value?.click();
}
/** 文件选择后按动作分发到原校验/导入逻辑 */
function onFileSelected(e) {
const file = e.target.files?.[0];
e.target.value = '';
if (!file) {
return;
}
const action = pendingAction.value;
pendingAction.value = null;
importModalVisible.value = false;
if (action === 'check') {
void handleCheckExcel({ file });
} else if (action === 'import') {
void handleImportByTemplate({ file });
}
}
async function handleDownloadTemplate() {
const data = await defHttp.get({ url: getExportXlsHeadersUrl, params: {}, responseType: 'blob', timeout: 60000 }, { isTransformResponse: false });
if (!data) {
@@ -677,3 +718,50 @@
background: #f6ffed !important;
}
</style>
<style lang="less">
/* 导入弹窗操作卡片 */
.import-action-list {
display: flex;
flex-direction: column;
gap: 12px;
padding: 4px 8px 8px;
}
.import-action-card {
display: flex;
align-items: center;
gap: 14px;
padding: 14px 16px;
border: 1px solid #e5e6eb;
border-radius: 8px;
background: #fff;
cursor: pointer;
transition: all 0.2s ease;
}
.import-action-card:hover {
border-color: #1677ff;
background: #f0f7ff;
box-shadow: 0 2px 8px rgba(22, 119, 255, 0.15);
transform: translateY(-1px);
}
.import-action-icon {
font-size: 26px;
color: #1677ff;
flex-shrink: 0;
}
.import-action-body {
min-width: 0;
}
.import-action-title {
font-size: 15px;
font-weight: 600;
color: #1f2329;
line-height: 1.4;
}
.import-action-desc {
margin-top: 4px;
font-size: 12px;
color: #8a919f;
line-height: 1.5;
}
</style>
@@ -167,6 +167,7 @@
</a-space>
</div>
<a-table
class="feedback-table"
v-if="showFeedbackInfo && showFeedbackDetail"
rowKey="id"
size="small"
@@ -175,6 +176,7 @@
:data-source="feedbackList"
:pagination="false"
:scroll="{ x: 1200 }"
:rowClassName="feedbackStatusRowClassName"
>
<template #bodyCell="{ column, record, index }">
<template v-if="column.key === 'seq'">{{ index + 1 }}</template>
@@ -269,6 +271,7 @@
import Icon from '/@/components/Icon/index';
import { saveBpmForm, setProcessVariable, specificProblemOptions } from '../DqInspectTask.api';
import { list as progressList, deleteOne as progressDelete } from '../../inspectProgress/DqInspectProgress.api';
import { feedbackStatusRowClassName } from '/@/views/bg/utils/feedbackStatusRender';
import { usePermission } from '/@/hooks/web/usePermission';
import { CHARGE_LEADER_ROLE_ID } from '/@/constant/supervision';
import { useSelectNameResolver } from '/@/composables/useSelectNameResolver';
@@ -1,26 +1,65 @@
<template>
<a-modal
v-model:open="visible"
:title="title"
:width="width"
centered
:top="80"
:footer="null"
:maskClosable="true"
:bodyStyle="{ maxHeight: '70vh', overflowY: 'auto', padding: '16px 24px 20px' }"
:bodyStyle="{ maxHeight: '70vh', overflowY: 'auto', padding: '4px 24px 20px' }"
wrapClassName="inspecttask-detail-modal"
destroyOnClose
>
<a-tabs v-model:activeKey="activeKey">
<a-tab-pane key="item" tab="事项信息">
<div class="detail-grid">
<div
v-for="field in detailFields"
:key="field.label"
class="detail-row"
:class="{ 'detail-row-full': field.fullWidth, 'detail-row-bold': field.bold }"
>
<span class="detail-label">{{ field.label }}</span>
<span class="detail-value">{{ field.value }}</span>
</div>
<div class="matter-detail">
<section class="matter-summary">
<div class="summary-heading">
<span class="summary-heading-icon">
<Icon :icon="summaryIcon" />
</span>
<div class="summary-heading-content">
<span class="summary-kicker">{{ summaryKicker }}</span>
<h3 class="summary-title">{{ detailSummary.title }}</h3>
</div>
</div>
<div class="summary-meta">
<div class="summary-meta-item">
<Icon icon="ant-design:appstore-outlined" />
<span class="summary-meta-label">问题分类</span>
<span class="summary-meta-value">{{ detailSummary.categoryName }}</span>
</div>
<div class="summary-meta-item">
<Icon icon="ant-design:lock-outlined" />
<span class="summary-meta-label">密级</span>
<span class="summary-meta-value">{{ detailSummary.secretLevel }}</span>
</div>
<div class="summary-meta-item">
<Icon icon="ant-design:clock-circle-outlined" />
<span class="summary-meta-label">完成时限</span>
<span class="summary-meta-value">{{ detailSummary.deadline }}</span>
</div>
<div class="summary-tag-group">
<a-tag class="summary-tag" :color="taskStatusTagColor">整改状态{{ detailSummary.taskStatus }}</a-tag>
<a-tag class="summary-tag">督办状态{{ detailSummary.bpmStatus }}</a-tag>
</div>
</div>
</section>
<section v-for="group in detailGroups" :key="group.title" class="detail-section">
<div class="detail-section-header">
<span class="detail-section-icon">
<Icon :icon="group.icon" />
</span>
<h3>{{ group.title }}</h3>
</div>
<div class="detail-grid">
<div v-for="field in group.fields" :key="field.label" class="detail-item" :class="{ 'detail-item-full': field.fullWidth }">
<span class="detail-label">{{ field.label }}</span>
<span class="detail-value" :class="{ 'detail-value-strong': field.bold }">{{ field.value }}</span>
</div>
</div>
</section>
</div>
</a-tab-pane>
@@ -37,6 +76,7 @@
<div class="feedback-summary-desc">当前整改任务的工作进展反馈记录</div>
</div>
<a-table
class="feedback-table"
rowKey="id"
size="small"
bordered
@@ -45,6 +85,7 @@
:pagination="false"
:scroll="{ x: 1200 }"
:loading="feedbackLoading"
:rowClassName="feedbackStatusRowClassName"
>
<template #bodyCell="{ column, record: fb }">
<template v-if="column.key === 'attachments'">
@@ -69,10 +110,10 @@
import Icon from '/@/components/Icon/index';
import SUploadFile from '/@/components/semri/fileComponent/SUploadFile.vue';
import { list } from '../../inspectProgress/DqInspectProgress.api';
import { feedbackStatusRowClassName } from '/@/views/bg/utils/feedbackStatusRender';
import type { BasicColumn } from '/@/components/Table';
const visible = ref(false);
const title = ref('详情');
const width = 1100;
const activeKey = ref('item');
const record = ref<Recordable>({});
@@ -97,29 +138,77 @@
return fmt(record[`${key}_dictText`] || record[key]);
}
const detailFields = computed(() => {
const summaryKicker = '整改任务';
const summaryIcon = 'ant-design:flag-outlined';
// 摘要区只承载最常用于快速判断事项状态的信息,详细字段放入下方分组,避免重复平铺。
const detailSummary = computed(() => {
const r = record.value || {};
return {
title: fmt(r.specificName),
categoryName: fmt(r.categoryName),
secretLevel: dictText(r, 'secretLevel'),
deadline: fmtDate(r.deadline),
taskStatus: fmt(r.taskStatus),
bpmStatus: dictText(r, 'bpmStatus'),
};
});
const taskStatusTagColor = computed(() => {
const text = detailSummary.value.taskStatus;
if (text.includes('已完成')) return 'success';
if (text.includes('整改中') || text.includes('进行中')) return 'processing';
if (text.includes('未开始')) return 'warning';
return undefined;
});
const detailGroups = computed(() => {
const r = record.value || {};
return [
{ label: '问题分类', value: fmt(r.categoryName) },
{ label: '面上问题', value: fmt(r.surfaceName) },
{ label: '具体问题', value: fmt(r.specificName) },
{ label: '整改措施', value: fmt(r.improveMeasure), fullWidth: true, bold: true },
{ label: '密级', value: dictText(r, 'secretLevel') },
{ label: '完成时限', value: fmtDate(r.deadline) },
{ label: '目标节点', value: fmt(r.targetNode) },
{ label: '巡视建议', value: fmt(r.inspectAdvice) },
{ label: '检查情况', value: fmt(r.inspectionStatus) },
{ label: '监督意见', value: fmt(r.supervisoryOpinion) },
{ label: '整改状态', value: fmt(r.taskStatus) },
{ label: '措施责任部门', value: dictText(r, 'measureResDept'), bold: true },
{ label: '措施责任负责人', value: dictText(r, 'measureResLeader'), bold: true },
{ label: '分管所领导', value: dictText(r, 'chargeLeaderId'), bold: true },
{ label: '整改交付成果', value: fmt(r.rectificationEvidence) },
{ label: '成效评价标准', value: fmt(r.evaluationCriterion) },
{ label: '需党群负责人审批', value: ynText(r.isNeedAppro) },
{ label: '党群负责人', value: dictText(r, 'supDeptleaderid') },
{ label: '督办状态', value: dictText(r, 'bpmStatus') },
{ label: '督办次数', value: fmt(r.superviseCount) },
{
title: '问题定位',
icon: 'ant-design:flag-outlined',
fields: [
{ label: '面上问题', value: fmt(r.surfaceName) },
{ label: '目标节点', value: fmt(r.targetNode) },
],
},
{
title: '整改要求',
icon: 'ant-design:edit-outlined',
fields: [
{ label: '整改措施', value: fmt(r.improveMeasure), fullWidth: true, bold: true },
{ label: '巡视建议', value: fmt(r.inspectAdvice) },
],
},
{
title: '责任分工',
icon: 'ant-design:team-outlined',
fields: [
{ label: '措施责任部门', value: dictText(r, 'measureResDept'), bold: true },
{ label: '措施责任负责人', value: dictText(r, 'measureResLeader'), bold: true },
{ label: '分管所领导', value: dictText(r, 'chargeLeaderId'), bold: true },
],
},
{
title: '成效与监督',
icon: 'ant-design:audit-outlined',
fields: [
{ label: '整改交付成果', value: fmt(r.rectificationEvidence) },
{ label: '成效评价标准', value: fmt(r.evaluationCriterion) },
{ label: '检查情况', value: fmt(r.inspectionStatus) },
{ label: '监督意见', value: fmt(r.supervisoryOpinion) },
],
},
{
title: '审批与进展',
icon: 'ant-design:safety-outlined',
fields: [
{ label: '需党群负责人审批', value: ynText(r.isNeedAppro) },
{ label: '党群负责人', value: dictText(r, 'supDeptleaderid') },
{ label: '督办次数', value: fmt(r.superviseCount) },
],
},
];
});
@@ -148,7 +237,6 @@
function open(currentRecord: Recordable) {
record.value = currentRecord || {};
title.value = '详情';
activeKey.value = 'item';
visible.value = true;
if (currentRecord?.id !== undefined && currentRecord?.id !== null && currentRecord?.id !== '') {
@@ -160,40 +248,265 @@
</script>
<style lang="less" scoped>
.detail-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px 32px;
padding: 4px 0;
// 弹窗内容会 Teleport 到 body,通过专属类限定公共组件的字号,避免影响其他页面。
:global(.inspecttask-detail-modal .ant-modal-title) {
font-size: 18px;
line-height: 26px;
}
.detail-row {
:global(.inspecttask-detail-modal .ant-tabs-tab-btn) {
font-size: 15px;
line-height: 24px;
}
:global(.inspecttask-detail-modal .ant-table) {
font-size: 15px;
}
:global(.inspecttask-detail-modal .ant-table-thead > tr > th),
:global(.inspecttask-detail-modal .ant-table-tbody > tr > td) {
font-size: 15px;
line-height: 24px;
}
.matter-detail {
padding: 4px 2px 10px;
}
.matter-summary {
position: relative;
overflow: hidden;
margin-bottom: 16px;
padding: 20px 22px;
background: linear-gradient(135deg, #f2f8ff 0%, #f8fbff 48%, #ffffff 100%);
border: 1px solid #d6e8ff;
border-radius: 10px;
box-shadow: 0 4px 14px rgb(22 119 255 / 8%);
&::after {
position: absolute;
top: -46px;
right: -24px;
width: 132px;
height: 132px;
background: rgb(22 119 255 / 5%);
border-radius: 50%;
content: '';
pointer-events: none;
}
}
.summary-heading {
position: relative;
z-index: 1;
display: flex;
align-items: flex-start;
gap: 12px;
}
.summary-heading-icon {
display: inline-flex;
flex: 0 0 40px;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
color: #1677ff;
font-size: 20px;
background: #e6f4ff;
border-radius: 10px;
}
.summary-heading-content {
min-width: 0;
}
.detail-row-full {
.summary-kicker {
display: block;
margin-bottom: 2px;
color: #64748b;
font-size: 14px;
line-height: 20px;
}
.summary-title {
margin: 0;
color: #172033;
font-size: 21px;
font-weight: 600;
line-height: 30px;
overflow-wrap: anywhere;
}
.summary-meta {
position: relative;
z-index: 1;
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 10px 18px;
margin-top: 16px;
padding-top: 14px;
border-top: 1px solid #deebfa;
}
.summary-meta-item {
display: inline-flex;
align-items: center;
gap: 6px;
min-height: 24px;
color: #64748b;
> :first-child {
color: #1677ff;
font-size: 15px;
}
}
.summary-meta-label {
font-size: 14px;
}
.summary-meta-value {
color: #1f2937;
font-size: 15px;
font-weight: 600;
}
.summary-tag-group {
display: inline-flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
min-width: 0;
}
.summary-tag {
margin-inline-end: 0;
padding: 2px 10px;
font-size: 14px;
line-height: 22px;
white-space: normal;
}
.detail-section {
overflow: hidden;
margin-top: 14px;
background: #ffffff;
border: 1px solid #e7edf4;
border-radius: 8px;
}
.detail-section-header {
display: flex;
align-items: center;
gap: 8px;
min-height: 44px;
padding: 10px 16px;
background: #f8fbff;
border-bottom: 1px solid #e7edf4;
h3 {
margin: 0;
color: #27364b;
font-size: 16px;
font-weight: 600;
line-height: 24px;
}
}
.detail-section-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 26px;
height: 26px;
color: #1677ff;
background: #e6f4ff;
border-radius: 6px;
}
.detail-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 1px;
background: #edf1f5;
}
.detail-item {
display: grid;
grid-template-columns: 142px minmax(0, 1fr);
align-items: start;
gap: 12px;
min-width: 0;
min-height: 54px;
padding: 14px 16px;
background: #ffffff;
}
.detail-item-full {
grid-column: 1 / -1;
}
.detail-label {
flex: 0 0 150px;
color: #8c8c8c;
line-height: 22px;
color: #7b8798;
font-size: 14px;
line-height: 26px;
}
.detail-value {
flex: 1;
color: #1f2937;
line-height: 22px;
word-break: break-all;
min-width: 0;
color: #27364b;
font-size: 15px;
line-height: 26px;
white-space: pre-wrap;
overflow-wrap: anywhere;
}
.detail-row-bold .detail-value {
font-weight: 700;
font-size: 15px;
.detail-value-strong {
color: #172033;
font-weight: 600;
}
@media (max-width: 768px) {
.matter-summary {
padding: 16px;
}
.summary-meta {
align-items: flex-start;
}
.summary-tag-group {
flex-basis: 100%;
}
.detail-grid {
grid-template-columns: minmax(0, 1fr);
}
.detail-item-full {
grid-column: auto;
}
}
@media (max-width: 576px) {
.summary-title {
font-size: 19px;
line-height: 27px;
}
.summary-meta {
display: grid;
grid-template-columns: minmax(0, 1fr);
gap: 8px;
}
.detail-item {
grid-template-columns: minmax(0, 1fr);
gap: 2px;
padding: 11px 14px;
}
}
.feedback-summary-header {