Files
supervision-test-frontend-repo/src/views/bg/bqtakepulse/components/BgTakepulseDetailModal.vue
T

690 lines
19 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<a-modal
v-model:open="visible"
:width="width"
:top="80"
:footer="null"
:maskClosable="true"
:bodyStyle="{ maxHeight: '70vh', overflowY: 'auto', padding: '4px 24px 20px' }"
wrapClassName="takepulse-detail-modal"
destroyOnClose
>
<a-tabs v-model:activeKey="activeKey">
<a-tab-pane key="item" tab="事项信息">
<div class="matter-detail">
<section class="matter-summary">
<div class="summary-heading">
<span class="summary-heading-icon">
<Icon icon="ant-design:bulb-outlined" />
</span>
<div class="summary-heading-content">
<span class="summary-kicker">把把脉事项</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.year }}</span>
</div>
<div class="summary-meta-item">
<Icon icon="ant-design:appstore-outlined" />
<span class="summary-meta-label">会议类型</span>
<span class="summary-meta-value">{{ detailSummary.taskType }}</span>
</div>
<div class="summary-tag-group">
<a-tag class="summary-tag" :color="statusTagColor">完成状态{{ detailSummary.bpmStatus }}</a-tag>
<a-tag class="summary-tag">密级{{ detailSummary.secretLevel }}</a-tag>
<a-tag class="summary-tag" :color="urgencyTagColor">是否急件{{ detailSummary.urgencyLevel }}</a-tag>
</div>
</div>
</section>
<section class="detail-section">
<div class="detail-section-header">
<span class="detail-section-icon">
<Icon icon="ant-design:profile-outlined" />
</span>
<h3>意见信息</h3>
</div>
<div class="detail-grid">
<div v-for="field in opinionFields" :key="field.key" class="detail-item">
<span class="detail-label">{{ field.label }}</span>
<span class="detail-value">{{ field.value }}</span>
</div>
</div>
</section>
<section class="detail-section">
<div class="detail-section-header">
<span class="detail-section-icon">
<Icon icon="ant-design:message-outlined" />
</span>
<h3>具体意见</h3>
</div>
<div class="long-text-content">{{ specificOpinion }}</div>
</section>
<section class="detail-section">
<div class="detail-section-header">
<span class="detail-section-icon">
<Icon icon="ant-design:team-outlined" />
</span>
<h3>责任与落实</h3>
</div>
<div class="detail-grid">
<div v-for="field in responsibilityFields" :key="field.key" class="detail-item">
<span class="detail-label">{{ field.label }}</span>
<span class="detail-value" :class="{ 'detail-value-strong': field.strong }">{{ field.value }}</span>
</div>
</div>
</section>
<section class="detail-section">
<div class="detail-section-header">
<span class="detail-section-icon">
<Icon icon="ant-design:check-circle-outlined" />
</span>
<h3>落实与成果</h3>
</div>
<div class="detail-grid">
<div v-for="field in resultFields" :key="field.key" class="detail-item" :class="{ 'detail-item-full': field.fullWidth }">
<span class="detail-label">{{ field.label }}</span>
<span class="detail-value">{{ field.value }}</span>
</div>
</div>
</section>
</div>
</a-tab-pane>
<a-tab-pane key="feedback" tab="反馈信息">
<div class="feedback-summary-header">
<div class="feedback-summary-title">
<span class="feedback-summary-icon">
<Icon icon="ant-design:message-outlined" />
</span>
<span>办理情况</span>
<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
:columns="feedbackColumns"
:data-source="feedbackList"
:pagination="false"
:scroll="{ x: 1800 }"
:loading="feedbackLoading"
:rowClassName="feedbackStatusRowClassName"
>
<template #bodyCell="{ column, record: feedbackRecord }">
<template v-if="column.key === 'attachments'">
<SUploadFile
v-if="feedbackRecord.id"
class="feedback-attachment-list"
:bussiness-id="feedbackRecord.id"
:disabled="true"
:multiple="true"
/>
<span v-else>-</span>
</template>
</template>
</a-table>
</a-tab-pane>
</a-tabs>
</a-modal>
</template>
<script lang="ts" setup>
import { ref, computed, defineExpose } from 'vue';
import Icon from '/@/components/Icon/index';
import SUploadFile from '/@/components/semri/fileComponent/SUploadFile.vue';
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 width = 1100;
const activeKey = ref('item');
const record = ref<Recordable>({});
const feedbackList = ref<Recordable[]>([]);
const feedbackLoading = ref(false);
function fmt(value: any): string {
return value === undefined || value === null || value === '' ? '-' : String(value);
}
function fmtDate(value: any): string {
const s = fmt(value);
return s.length > 10 ? s.slice(0, 10) : s;
}
function ynText(value: any): string {
const v = fmt(value);
if (v === '1') return '是';
if (v === '0') return '否';
return v;
}
function dictText(record: Recordable, key: string): string {
return fmt(record[`${key}_dictText`] || record[key]);
}
// 摘要区聚合最常用的识别与状态信息,其余字段按业务含义分组展示。
const detailSummary = computed(() => {
const r = record.value || {};
return {
title: fmt(r.title),
year: dictText(r, 'year'),
taskType: dictText(r, 'taskType'),
bpmStatus: dictText(r, 'bpmStatus'),
secretLevel: dictText(r, 'secretLevel'),
urgencyLevel: dictText(r, 'urgencyLevel'),
};
});
const opinionFields = computed(() => {
const r = record.value || {};
return [
{ key: 'number', label: '序号', value: fmt(r.number) },
{ key: 'type', label: '类别', value: fmt(r.type) },
{ key: 'proposer', label: '提出人', value: dictText(r, 'proposer') },
{ key: 'isAdopt', label: '是否采纳', value: ynText(r.isAdopt) },
];
});
const specificOpinion = computed(() => fmt(record.value?.content));
const responsibilityFields = computed(() => {
const r = record.value || {};
return [
{ key: 'manageSuoleader', label: '分管所领导', value: dictText(r, 'manageSuoleader'), strong: true },
{ key: 'suoleader', label: '责任领导', value: dictText(r, 'suoleader'), strong: true },
{ key: 'responDeptid', label: '牵头部门', value: dictText(r, 'responDeptid'), strong: true },
{ key: 'assistDeptid', label: '协办部门', value: dictText(r, 'assistDeptid'), strong: true },
{ key: 'implementDeptid', label: '落实部门', value: dictText(r, 'implementDeptid') },
{ key: 'deadline', label: '要求完成日期', value: fmtDate(r.deadline) },
{ key: 'measureNumber', label: '措施数量', value: fmt(r.measureNumber) },
{ key: 'superviseCount', label: '督办次数', value: fmt(r.superviseCount) },
{ key: 'isNeedAppro', label: '是否需要所办负责人审批', value: ynText(r.isNeedAppro) },
{ key: 'supDeptleaderid', label: '所办负责人', value: dictText(r, 'supDeptleaderid') },
];
});
const resultFields = computed(() => {
const r = record.value || {};
return [
{ key: 'actualTime', label: '实际完成时间', value: fmtDate(r.actualTime) },
{ key: 'proposerConfirm', label: '提出人确认', value: fmt(r.proposerConfirm) },
{ key: 'measure', label: '落实措施/解释说明', value: fmt(r.measure), fullWidth: true },
{ key: 'achievement', label: '成果形式', value: fmt(r.achievement), fullWidth: true },
{ key: 'remark', label: '备注', value: fmt(r.remark), fullWidth: true },
];
});
// 同时兼容字典文本和原始编码,确保状态标签在不同列表数据形态下保持一致。
const statusTagColor = computed(() => {
const rawStatus = String(record.value?.bpmStatus ?? '');
const statusText = detailSummary.value.bpmStatus;
if (rawStatus === '3' || statusText.includes('已完成')) return 'success';
if (rawStatus === '2' || statusText.includes('督办中') || statusText.includes('进行中')) return 'processing';
if (rawStatus === '1' || statusText.includes('未开始')) return 'warning';
return undefined;
});
const urgencyTagColor = computed(() => {
const rawUrgency = String(record.value?.urgencyLevel ?? '');
return rawUrgency === '1' || detailSummary.value.urgencyLevel === '是' ? 'error' : undefined;
});
const feedbackColumns: BasicColumn[] = [
{ title: '责任部门名称', dataIndex: 'responDeptname', align: 'center', width: 160 },
{ title: '反馈人姓名', dataIndex: 'responPersonname', align: 'center', width: 110 },
{ title: '计划完成目标', dataIndex: 'planExect', align: 'left', width: 260 },
{
title: '计划完成日期',
dataIndex: 'planTime',
align: 'center',
width: 120,
customRender: ({ text }) => (!text ? '' : String(text).slice(0, 10)),
},
{ title: '实际执行情况', dataIndex: 'actualExect', align: 'left', width: 280 },
{
title: '实际完成日期',
dataIndex: 'actualTime',
align: 'center',
width: 155,
customRender: ({ text }) => (!text ? '' : String(text).slice(0, 10)),
},
{ title: '调整落实措施及执行情况', dataIndex: 'actSt', align: 'left', width: 260 },
{
title: '完成状态',
dataIndex: 'bpmStatus',
align: 'center',
width: 95,
customRender: ({ record }) => record?.bpmStatus_dictText || record?.bpmStatus,
},
{
title: '附件下载',
dataIndex: 'id',
key: 'attachments',
align: 'left',
width: 340,
},
];
const count = computed(() => countFeedbackStatus(feedbackList.value, 'bpmStatus', 'planTime'));
async function loadFeedback(mainId: string | number) {
feedbackLoading.value = true;
feedbackList.value = [];
try {
const res = await bgTakepulseFeedbackList({
mainId,
pageNo: 1,
pageSize: 9999,
column: 'createTime',
order: 'desc',
});
feedbackList.value = Array.isArray(res) ? res : res?.records || res?.result?.records || [];
} finally {
feedbackLoading.value = false;
}
}
function open(currentRecord: Recordable) {
record.value = currentRecord || {};
activeKey.value = 'item';
visible.value = true;
if (currentRecord?.id !== undefined && currentRecord?.id !== null && currentRecord?.id !== '') {
loadFeedback(currentRecord.id);
}
}
defineExpose({ open });
</script>
<style lang="less" scoped>
// 弹窗内容会 Teleport 到 body,通过专属类限定公共组件的字号,避免影响其他页面。
:global(.takepulse-detail-modal .ant-modal-title) {
font-size: 18px;
line-height: 26px;
}
:global(.takepulse-detail-modal .ant-tabs-tab-btn) {
font-size: 15px;
line-height: 24px;
}
:global(.takepulse-detail-modal .ant-table) {
font-size: 15px;
}
:global(.takepulse-detail-modal .ant-table-thead > tr > th),
:global(.takepulse-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;
}
.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: 150px 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 {
color: #7b8798;
font-size: 14px;
line-height: 26px;
}
.detail-value {
min-width: 0;
color: #27364b;
font-size: 15px;
line-height: 26px;
white-space: pre-wrap;
overflow-wrap: anywhere;
}
.detail-value-strong {
color: #172033;
font-weight: 600;
}
.long-text-content {
min-height: 90px;
padding: 16px 18px 18px;
color: #27364b;
font-size: 15px;
line-height: 1.9;
white-space: pre-wrap;
overflow-wrap: anywhere;
}
@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 {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 10px;
padding: 10px 14px;
background: #f6fbff;
border: 1px solid #d6e8ff;
border-left: 4px solid #1677ff;
border-radius: 6px;
}
.feedback-summary-title {
display: inline-flex;
align-items: center;
flex-wrap: wrap;
min-width: 0;
color: #1f2937;
font-size: 15px;
font-weight: 600;
line-height: 26px;
}
.feedback-summary-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
margin-right: 8px;
color: #1677ff;
background: #e6f4ff;
border-radius: 50%;
}
.feedback-summary-tag {
margin-left: 10px;
font-size: 14px;
line-height: 22px;
}
.feedback-summary-desc {
color: #64748b;
font-size: 14px;
line-height: 24px;
white-space: nowrap;
}
.feedback-attachment-list {
min-width: 300px;
:deep(.file-list-wrapper) {
min-height: 0 !important;
padding: 4px 8px !important;
}
:deep(.ant-spin-nested-loading),
:deep(.ant-spin-container) {
min-height: 0 !important;
line-height: 20px;
}
:deep(.file-list-header) {
margin-bottom: 6px;
padding-bottom: 6px;
}
:deep(.file-list-header .file-count) {
font-size: 14px;
}
:deep(.file-list .file-item) {
padding: 8px;
margin-bottom: 6px;
}
:deep(.file-list .file-name) {
font-size: 15px;
}
:deep(.file-list .file-size) {
font-size: 13px;
}
:deep(.ant-empty) {
margin: 0 !important;
display: flex;
align-items: center;
min-height: 20px;
text-align: left;
}
:deep(.ant-empty-image) {
display: none !important;
}
:deep(.ant-empty-description) {
color: #94a3b8;
font-size: 13px;
line-height: 22px;
}
}
</style>