- 新增 FirstAidPush 组件:分类筛选 + 搜索 + 单条/一键推送 - IM 端切换会话时 postMessage 携带 orderId 通知父页面 - 父页面监听 sessionChanged 联动推送按钮与后端调用 - 推送调后端 push/send 记录状态 + IM postMessage 推卡片 - 资源管理正文改富文本编辑器,修批量操作重复提示 - 重建 iweb 产物
328 lines
13 KiB
Vue
328 lines
13 KiB
Vue
<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>
|