feat(resource): 新增急救宣教资源管理、分类标签、审核管理模块

- 资源管理:列表筛选 + 新增/编辑弹窗 + 移动端预览 + 版本历史,已接入后端 RESTful 接口
- 分类与标签:左侧分类树(支持新增子分类/编辑/删除)+ 右侧标签云管理
- 审核管理:待审核/已通过/已驳回/审核记录四个 tab,驳回原因弹窗已接入审核接口
- 开发环境 API 地址切换为同事本地后台
This commit is contained in:
wanghao
2026-06-23 17:54:28 +08:00
parent a47c2c7400
commit a2a0e274f4
12 changed files with 2016 additions and 2 deletions
+308
View File
@@ -0,0 +1,308 @@
<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="审核记录">
<a-alert message="审核记录接口后端尚未提供独立列表,当前合并展示已通过+已驳回数据" type="info" show-icon style="margin-bottom: 12px" />
<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._action === 'pass' ? 'green' : 'red'">
{{ record._action === 'pass' ? '通过' : '驳回' }}
</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,
approveApi,
rejectApi,
} from './check.api';
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 [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 [passed, rejected]: any = await Promise.all([
passedListApi({ pageNo: 1, pageSize: 100 }),
rejectedListApi({ pageNo: 1, pageSize: 100 }),
]);
const passedRecords = (passed?.records || []).map((item: any) => ({ ...item, _action: 'pass' }));
const rejectedRecords = (rejected?.records || []).map((item: any) => ({ ...item, _action: 'reject' }));
recordsList.value = [...passedRecords, ...rejectedRecords];
} 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>