Files
supervision-test-frontend-repo/src/views/bg/xispeak/components/BgXiSpeakDetailModal.vue
T

601 lines
16 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="xispeak-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="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>
<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: 1500 }"
:loading="feedbackLoading"
:rowClassName="feedbackStatusRowClassName"
>
<template #bodyCell="{ column, record: fb }">
<template v-if="column.key === 'action'">
<a-button type="link" size="small" @click="handleFeedbackDetail(fb)">查看详情</a-button>
</template>
<template v-else-if="column.key === 'attachments'">
<SUploadFile
v-if="fb.id"
class="feedback-attachment-list"
:bussiness-id="fb.id"
:disabled="true"
:multiple="true"
/>
<span v-else>-</span>
</template>
</template>
</a-table>
</a-tab-pane>
</a-tabs>
<BgXiSpeakFeedbackDetailModal ref="feedbackDetailModalRef" />
</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 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 width = 1100;
const activeKey = ref('item');
const record = ref<Recordable>({});
const feedbackDetailModalRef = ref();
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 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 [
{
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 },
],
},
];
});
const feedbackColumns: BasicColumn[] = [
{ title: '反馈人', dataIndex: 'responPersonname', align: 'center', width: 100 },
{ title: '反馈部门', dataIndex: 'responDeptname', align: 'center', width: 140 },
{ title: '反馈措施', dataIndex: 'method', align: 'left', ellipsis: true },
{ title: '完成状态', dataIndex: 'completionStatus_dictText', align: 'center', width: 100 },
{ title: '成果形式', dataIndex: 'outcomeForm', align: 'center', width: 120 },
{ title: '检查情况(纪检)', dataIndex: 'checkStatus', align: 'left', ellipsis: true, width: 140 },
{ title: '监督意见(纪检)', dataIndex: 'inspectionAdvice', align: 'left', ellipsis: true, width: 140 },
{ title: '措施数量(纪检)', dataIndex: 'measureNum', align: 'center', width: 100 },
{ title: '附件', dataIndex: 'id', key: 'attachments', align: 'left', width: 280 },
{ title: '操作', dataIndex: 'action', key: 'action', align: 'center', width: 90, fixed: 'right' },
];
const count = computed(() => countFeedbackStatus(feedbackList.value, 'completionStatus', 'deadline'));
async function loadFeedback(mainId: string | number) {
feedbackLoading.value = true;
feedbackList.value = [];
try {
const res = await bgXiSpeakFeedbackList({
mainId,
pageNo: 1,
pageSize: 9999,
column: 'createTime',
order: 'desc',
});
feedbackList.value = Array.isArray(res) ? res : res?.records || [];
} finally {
feedbackLoading.value = false;
}
}
function handleFeedbackDetail(fb: Recordable) {
feedbackDetailModalRef.value?.open(record.value, fb);
}
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(.xispeak-detail-modal .ant-modal-title) {
font-size: 18px;
line-height: 26px;
}
: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;
}
.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 {
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;
}
@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: 14px;
font-weight: 600;
line-height: 24px;
}
.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;
}
.feedback-summary-desc {
color: #64748b;
font-size: 13px;
line-height: 22px;
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 .file-item) {
padding: 8px;
margin-bottom: 6px;
}
: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: 12px;
line-height: 20px;
}
}
</style>