Files
xj-admin/src/views/check/index.vue
T
wanghao 6bb4b97ff4 feat(emergency): 实现急救宣教推送到 IM 会话功能
- 新增 FirstAidPush 组件:分类筛选 + 搜索 + 单条/一键推送
- IM 端切换会话时 postMessage 携带 orderId 通知父页面
- 父页面监听 sessionChanged 联动推送按钮与后端调用
- 推送调后端 push/send 记录状态 + IM postMessage 推卡片
- 资源管理正文改富文本编辑器,修批量操作重复提示
- 重建 iweb 产物
2026-06-24 17:23:36 +08:00

328 lines
13 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>
<div class="check-page">
<a-tabs v-model:activeKey="activeTab" @change="onTabChange">
<a-tab-pane key="pending">
<template #tab>
待审核
<a-badge :count="pendingTotal" :number-style="{ backgroundColor: '#fa8c16' }" :overflow-count="999" />
</template>
<div class="filter-bar">
<a-input v-model:value="pendingFilter.title" placeholder="搜索标题" allow-clear style="width: 240px" @press-enter="loadPending" />
<a-button type="primary" @click="loadPending">查询</a-button>
</div>
<a-spin :spinning="pendingLoading">
<div class="review-list">
<div v-for="item in pendingList" :key="item.id" class="review-card">
<div class="review-card-top">
<div class="review-card-main">
<div class="review-title">{{ item.title }}</div>
<div class="review-meta">
提交人{{ item.createBy || '-' }} | 创建时间{{ item.createTime || '-' }} | 分类{{ item.categoryName || '-' }} | 类型{{ getTypeLabel(item.contentType) }}
</div>
</div>
<div class="review-actions">
<a-button size="small" @click="handlePreview(item)">预览</a-button>
<a-button type="primary" size="small" @click="handlePass(item)">通过</a-button>
<a-button danger size="small" @click="handleReject(item)">驳回</a-button>
</div>
</div>
</div>
<a-empty v-if="!pendingLoading && pendingList.length === 0" description="暂无待审核资源" />
</div>
</a-spin>
</a-tab-pane>
<a-tab-pane key="passed" tab="已通过">
<div class="filter-bar">
<a-input v-model:value="passedFilter.title" placeholder="搜索标题" allow-clear style="width: 240px" @press-enter="loadPassed" />
<a-button type="primary" @click="loadPassed">查询</a-button>
</div>
<a-table
:columns="passedColumns"
:data-source="passedList"
:loading="passedLoading"
:pagination="false"
row-key="id"
size="middle"
>
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'action'">
<a-button type="link" size="small" @click="handlePreview(record)">查看</a-button>
</template>
</template>
</a-table>
</a-tab-pane>
<a-tab-pane key="rejected" tab="已驳回">
<div class="filter-bar">
<a-input v-model:value="rejectedFilter.title" placeholder="搜索标题" allow-clear style="width: 240px" @press-enter="loadRejected" />
<a-button type="primary" @click="loadRejected">查询</a-button>
</div>
<a-table
:columns="rejectedColumns"
:data-source="rejectedList"
:loading="rejectedLoading"
:pagination="false"
row-key="id"
size="middle"
>
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'action'">
<a-button type="link" size="small" @click="handleReEdit(record)">重新编辑</a-button>
</template>
</template>
</a-table>
</a-tab-pane>
<a-tab-pane key="records" tab="审核记录">
<div class="filter-bar">
<a-input v-model:value="recordsFilter.title" placeholder="搜索标题" allow-clear style="width: 200px" @press-enter="loadRecords" />
<a-select v-model:value="recordsFilter.auditStatus" placeholder="审核操作" allow-clear style="width: 140px">
<a-select-option :value="10">全部</a-select-option>
<a-select-option :value="11">已通过</a-select-option>
<a-select-option :value="12">已驳回</a-select-option>
</a-select>
<a-range-picker v-model:value="recordsFilter.dateRange" style="width: 240px" />
<a-button type="primary" @click="loadRecords">查询</a-button>
</div>
<a-table
:columns="recordColumns"
:data-source="recordsList"
:loading="recordsLoading"
:pagination="false"
row-key="id"
size="middle"
>
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'action'">
<a-tag :color="record.auditStatus === 11 ? 'green' : 'red'">
{{ record.auditStatus === 11 ? '通过' : '驳回' }}
</a-tag>
</template>
</template>
</a-table>
</a-tab-pane>
</a-tabs>
<PreviewModal @register="registerPreviewModal" />
<RejectReasonModal @register="registerRejectModal" @success="loadPending" />
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue';
import { useMessage } from '/@/hooks/web/useMessage';
import { useModal } from '/@/components/Modal';
import {
pendingListApi,
passedListApi,
rejectedListApi,
recordsListApi,
approveApi,
rejectApi,
} from './check.api';
import dayjs from 'dayjs';
import { CONTENT_TYPE } from '/@/views/resource/resource.data';
import PreviewModal from '/@/views/resource/components/PreviewModal.vue';
import RejectReasonModal from './components/RejectReasonModal.vue';
const { createMessage, createConfirm } = useMessage();
const activeTab = ref('pending');
const pendingList = ref<any[]>([]);
const pendingTotal = ref(0);
const pendingLoading = ref(false);
const pendingFilter = reactive({ title: '' });
const passedList = ref<any[]>([]);
const passedLoading = ref(false);
const passedFilter = reactive({ title: '' });
const rejectedList = ref<any[]>([]);
const rejectedLoading = ref(false);
const rejectedFilter = reactive({ title: '' });
const recordsList = ref<any[]>([]);
const recordsLoading = ref(false);
const recordsFilter = reactive({
title: '',
auditStatus: undefined as number | undefined,
dateRange: undefined as any,
});
const [registerPreviewModal, { openModal: openPreviewModal }] = useModal();
const [registerRejectModal, { openModal: openRejectModal }] = useModal();
const passedColumns = [
{ title: '标题', dataIndex: 'title' },
{ title: '分类', dataIndex: 'categoryName', align: 'center', width: 120 },
{ title: '审核人', dataIndex: 'auditBy', align: 'center', width: 120 },
{ title: '审核时间', dataIndex: 'auditTime', align: 'center', width: 170 },
{ title: '审核意见', dataIndex: 'auditOpinion' },
{ title: '操作', dataIndex: 'action', align: 'center', width: 100 },
];
const rejectedColumns = [
{ title: '标题', dataIndex: 'title' },
{ title: '分类', dataIndex: 'categoryName', align: 'center', width: 120 },
{ title: '驳回人', dataIndex: 'auditBy', align: 'center', width: 120 },
{ title: '驳回时间', dataIndex: 'auditTime', align: 'center', width: 170 },
{ title: '驳回原因', dataIndex: 'auditRejectReason' },
{ title: '操作', dataIndex: 'action', align: 'center', width: 120 },
];
const recordColumns = [
{ title: '资源名称', dataIndex: 'title' },
{ title: '操作', dataIndex: 'action', align: 'center', width: 100 },
{ title: '审核人', dataIndex: 'auditBy', align: 'center', width: 120 },
{ title: '时间', dataIndex: 'auditTime', align: 'center', width: 170 },
{ title: '意见', dataIndex: 'auditOpinion' },
];
function getTypeLabel(type: string) {
return CONTENT_TYPE[type as keyof typeof CONTENT_TYPE]?.label || type || '-';
}
function handlePreview(record: any) {
openPreviewModal(true, { record });
}
function handlePass(record: any) {
createConfirm({
iconType: 'info',
title: '确认审核通过',
content: `是否通过《${record.title}》?`,
onOk: async () => {
await approveApi({ id: record.id });
createMessage.success('已审核通过');
loadPending();
},
});
}
function handleReject(record: any) {
openRejectModal(true, { record });
}
function handleReEdit(record: any) {
createMessage.info(`重新编辑:${record.title}(跳转到资源管理编辑)`);
}
async function loadPending() {
pendingLoading.value = true;
try {
const res: any = await pendingListApi({ pageNo: 1, pageSize: 100, title: pendingFilter.title || undefined });
pendingList.value = res?.records || [];
pendingTotal.value = res?.total || 0;
} finally {
pendingLoading.value = false;
}
}
async function loadPassed() {
passedLoading.value = true;
try {
const res: any = await passedListApi({ pageNo: 1, pageSize: 100, title: passedFilter.title || undefined });
passedList.value = res?.records || [];
} finally {
passedLoading.value = false;
}
}
async function loadRejected() {
rejectedLoading.value = true;
try {
const res: any = await rejectedListApi({ pageNo: 1, pageSize: 100, title: rejectedFilter.title || undefined });
rejectedList.value = res?.records || [];
} finally {
rejectedLoading.value = false;
}
}
async function loadRecords() {
recordsLoading.value = true;
try {
const params: any = {
pageNo: 1,
pageSize: 100,
title: recordsFilter.title || undefined,
auditStatus: recordsFilter.auditStatus ?? undefined,
startDate: recordsFilter.dateRange?.[0] ? dayjs(recordsFilter.dateRange[0]).format('YYYY-MM-DD') : undefined,
endDate: recordsFilter.dateRange?.[1] ? dayjs(recordsFilter.dateRange[1]).format('YYYY-MM-DD') : undefined,
};
const res: any = await recordsListApi(params);
recordsList.value = res?.records || [];
} finally {
recordsLoading.value = false;
}
}
function onTabChange(key: string) {
if (key === 'records' && recordsList.value.length === 0) {
loadRecords();
}
}
onMounted(() => {
loadPending();
loadPassed();
loadRejected();
});
</script>
<style lang="less" scoped>
.check-page {
background: #fff;
padding: 16px 20px;
}
.filter-bar {
display: flex;
gap: 8px;
margin-bottom: 16px;
}
.review-list {
display: flex;
flex-direction: column;
gap: 8px;
padding-top: 8px;
}
.review-card {
border: 1px solid #f0f0f0;
border-radius: 2px;
padding: 16px 20px;
transition: all 0.2s;
background: #fff;
&:hover {
border-color: #1890ff;
box-shadow: 0 1px 2px rgba(24, 144, 255, 0.08);
}
}
.review-card-top {
display: flex;
align-items: flex-start;
justify-content: space-between;
}
.review-card-main {
flex: 1;
}
.review-title {
font-size: 14px;
font-weight: 600;
margin-bottom: 4px;
color: rgba(0, 0, 0, 0.85);
}
.review-meta {
font-size: 12px;
color: rgba(0, 0, 0, 0.45);
}
.review-actions {
display: flex;
gap: 8px;
flex-shrink: 0;
margin-left: 16px;
}
</style>