feat(resource): 新增急救宣教资源管理、分类标签、审核管理模块
- 资源管理:列表筛选 + 新增/编辑弹窗 + 移动端预览 + 版本历史,已接入后端 RESTful 接口 - 分类与标签:左侧分类树(支持新增子分类/编辑/删除)+ 右侧标签云管理 - 审核管理:待审核/已通过/已驳回/审核记录四个 tab,驳回原因弹窗已接入审核接口 - 开发环境 API 地址切换为同事本地后台
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user