feat(resource): 新增急救宣教资源管理、分类标签、审核管理模块
- 资源管理:列表筛选 + 新增/编辑弹窗 + 移动端预览 + 版本历史,已接入后端 RESTful 接口 - 分类与标签:左侧分类树(支持新增子分类/编辑/删除)+ 右侧标签云管理 - 审核管理:待审核/已通过/已驳回/审核记录四个 tab,驳回原因弹窗已接入审核接口 - 开发环境 API 地址切换为同事本地后台
This commit is contained in:
+4
-2
@@ -11,12 +11,14 @@ VITE_PUBLIC_PATH = /
|
|||||||
VITE_DROP_CONSOLE = false
|
VITE_DROP_CONSOLE = false
|
||||||
|
|
||||||
#后台接口父地址(必填)
|
#后台接口父地址(必填)
|
||||||
VITE_GLOB_API_URL=https://xj-api.mcrm.vip:8888
|
VITE_GLOB_API_URL=http://10.10.20.18
|
||||||
|
#VITE_GLOB_API_URL=https://xj-api.mcrm.vip:8888
|
||||||
#VITE_GLOB_API_URL=http://192.168.1.223
|
#VITE_GLOB_API_URL=http://192.168.1.223
|
||||||
#VITE_GLOB_API_URL=http://cqyt.test.yg.dt.io
|
#VITE_GLOB_API_URL=http://cqyt.test.yg.dt.io
|
||||||
|
|
||||||
#后台接口全路径地址(必填)
|
#后台接口全路径地址(必填)
|
||||||
VITE_GLOB_DOMAIN_URL=https://xj-api.mcrm.vip:8888
|
VITE_GLOB_DOMAIN_URL=http://10.10.20.18
|
||||||
|
#VITE_GLOB_DOMAIN_URL=https://xj-api.mcrm.vip:8888
|
||||||
#VITE_GLOB_DOMAIN_URL=http://192.168.1.223
|
#VITE_GLOB_DOMAIN_URL=http://192.168.1.223
|
||||||
#VITE_GLOB_DOMAIN_URL=http://cqyt.test.yg.dt.io
|
#VITE_GLOB_DOMAIN_URL=http://cqyt.test.yg.dt.io
|
||||||
# 接口前缀
|
# 接口前缀
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { defHttp } from '/@/utils/http/axios';
|
||||||
|
|
||||||
|
enum Api {
|
||||||
|
pendingList = '/health-emergency/emergency/firstAid/audit/pending',
|
||||||
|
passedList = '/health-emergency/emergency/firstAid/audit/passed',
|
||||||
|
rejectedList = '/health-emergency/emergency/firstAid/audit/rejected',
|
||||||
|
approve = '/health-emergency/emergency/firstAid/audit/approve',
|
||||||
|
reject = '/health-emergency/emergency/firstAid/audit/reject',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const pendingListApi = (params: any) => defHttp.get({ url: Api.pendingList, params });
|
||||||
|
export const passedListApi = (params: any) => defHttp.get({ url: Api.passedList, params });
|
||||||
|
export const rejectedListApi = (params: any) => defHttp.get({ url: Api.rejectedList, params });
|
||||||
|
export const approveApi = (params: any) => defHttp.post({ url: Api.approve, params });
|
||||||
|
export const rejectApi = (params: any) => defHttp.post({ url: Api.reject, params });
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<template>
|
||||||
|
<BasicModal v-bind="$attrs" @register="registerModal" title="填写驳回原因" :width="520" :minHeight="300" @ok="handleSubmit">
|
||||||
|
<a-form layout="vertical">
|
||||||
|
<a-form-item label="驳回原因" required>
|
||||||
|
<a-textarea v-model:value="reason" :rows="5" placeholder="请填写驳回原因,将通知内容编辑人员修改..." />
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
</BasicModal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { BasicModal, useModalInner } from '/@/components/Modal';
|
||||||
|
import { useMessage } from '/@/hooks/web/useMessage';
|
||||||
|
import { rejectApi } from '../check.api';
|
||||||
|
|
||||||
|
const emit = defineEmits(['success', 'register']);
|
||||||
|
const { createMessage } = useMessage();
|
||||||
|
const reason = ref('');
|
||||||
|
const record = ref<any>({});
|
||||||
|
const submitting = ref(false);
|
||||||
|
|
||||||
|
const [registerModal, { closeModal, setModalProps }] = useModalInner(async (data) => {
|
||||||
|
record.value = data?.record || {};
|
||||||
|
reason.value = '';
|
||||||
|
});
|
||||||
|
|
||||||
|
async function handleSubmit() {
|
||||||
|
if (!reason.value.trim()) {
|
||||||
|
createMessage.warning('请填写驳回原因');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setModalProps({ confirmLoading: true });
|
||||||
|
try {
|
||||||
|
await rejectApi({ id: record.value.id, reason: reason.value });
|
||||||
|
createMessage.success('已驳回,通知已发送');
|
||||||
|
emit('success');
|
||||||
|
closeModal();
|
||||||
|
} finally {
|
||||||
|
setModalProps({ confirmLoading: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -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>
|
||||||
@@ -0,0 +1,422 @@
|
|||||||
|
<template>
|
||||||
|
<div class="label-page">
|
||||||
|
<a-row :gutter="16">
|
||||||
|
<!-- 左侧:分类树 -->
|
||||||
|
<a-col :span="7">
|
||||||
|
<a-card title="分类管理" :bordered="false" class="card-section">
|
||||||
|
<template #extra>
|
||||||
|
<a-button type="primary" size="small" @click="handleAddCategory()">
|
||||||
|
<template #icon><PlusOutlined /></template>
|
||||||
|
新增
|
||||||
|
</a-button>
|
||||||
|
</template>
|
||||||
|
<a-input-search v-model:value="categorySearch" placeholder="搜索分类" style="margin-bottom: 12px" allow-clear />
|
||||||
|
<a-spin :spinning="categoryLoading">
|
||||||
|
<div class="tree-wrap">
|
||||||
|
<a-tree
|
||||||
|
v-if="filteredTreeData.length > 0"
|
||||||
|
v-model:expandedKeys="expandedKeys"
|
||||||
|
v-model:selectedKeys="selectedKeys"
|
||||||
|
:tree-data="filteredTreeData"
|
||||||
|
:field-names="{ title: 'name', key: 'id', children: 'children' }"
|
||||||
|
block-node
|
||||||
|
@select="onSelectCategory"
|
||||||
|
>
|
||||||
|
<template #title="{ name, id, status }">
|
||||||
|
<div class="tree-node-content">
|
||||||
|
<span class="tree-name">{{ name }}</span>
|
||||||
|
<a-tag v-if="status === 2" color="red" style="margin-left: 4px">冻结</a-tag>
|
||||||
|
<span class="tree-actions">
|
||||||
|
<a-button type="link" size="small" @click.stop="handleEditCategory({ id, name })">编</a-button>
|
||||||
|
<a-button type="link" size="small" @click.stop="handleAddSubCategory(id)">+</a-button>
|
||||||
|
<a-button type="link" size="small" danger @click.stop="handleDeleteCategory({ id, name })">删</a-button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</a-tree>
|
||||||
|
<a-empty v-else description="暂无分类" />
|
||||||
|
</div>
|
||||||
|
</a-spin>
|
||||||
|
</a-card>
|
||||||
|
</a-col>
|
||||||
|
|
||||||
|
<!-- 右侧:标签云 + 权限矩阵 -->
|
||||||
|
<a-col :span="17">
|
||||||
|
<a-card title="标签管理" :bordered="false" class="card-section">
|
||||||
|
<template #extra>
|
||||||
|
<a-button type="primary" size="small" @click="handleAddTag">
|
||||||
|
<template #icon><PlusOutlined /></template>
|
||||||
|
新增标签
|
||||||
|
</a-button>
|
||||||
|
</template>
|
||||||
|
<a-input-search v-model:value="tagSearch" placeholder="搜索标签" style="width: 240px; margin-bottom: 12px" allow-clear />
|
||||||
|
<a-spin :spinning="tagLoading">
|
||||||
|
<div class="tag-cloud">
|
||||||
|
<a-tag
|
||||||
|
v-for="tag in filteredTags"
|
||||||
|
:key="tag.id"
|
||||||
|
:color="getTagColor(tag.id)"
|
||||||
|
class="tag-item-cloud"
|
||||||
|
closable
|
||||||
|
@close.prevent="handleDeleteTag(tag)"
|
||||||
|
>
|
||||||
|
{{ tag.name }}
|
||||||
|
</a-tag>
|
||||||
|
<a-empty v-if="filteredTags.length === 0 && !tagLoading" description="暂无标签" />
|
||||||
|
</div>
|
||||||
|
</a-spin>
|
||||||
|
</a-card>
|
||||||
|
|
||||||
|
<a-card title="分类权限配置" :bordered="false" class="card-section">
|
||||||
|
<a-alert message="权限矩阵接口后端尚未提供,当前为占位展示" type="info" show-icon style="margin-bottom: 12px" />
|
||||||
|
<a-table
|
||||||
|
:columns="permissionColumns"
|
||||||
|
:data-source="permissionList"
|
||||||
|
:pagination="false"
|
||||||
|
size="middle"
|
||||||
|
bordered
|
||||||
|
>
|
||||||
|
<template #bodyCell="{ column, record }">
|
||||||
|
<template v-if="column.dataIndex !== 'role'">
|
||||||
|
<a-tag :color="getPermissionColor(record.permissions[column.idx])">
|
||||||
|
{{ getPermissionText(record.permissions[column.idx]) }}
|
||||||
|
</a-tag>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</a-table>
|
||||||
|
</a-card>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
|
||||||
|
<!-- 分类新增/编辑弹窗 -->
|
||||||
|
<a-modal
|
||||||
|
v-model:open="categoryModalVisible"
|
||||||
|
:title="categoryModalTitle"
|
||||||
|
@ok="handleCategorySubmit"
|
||||||
|
:width="480"
|
||||||
|
:confirm-loading="categorySubmitting"
|
||||||
|
>
|
||||||
|
<a-form layout="vertical">
|
||||||
|
<a-form-item label="分类名称" required>
|
||||||
|
<a-input v-model:value="categoryForm.name" placeholder="请输入分类名称" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="父级分类">
|
||||||
|
<a-tree-select
|
||||||
|
v-model:value="categoryForm.parentId"
|
||||||
|
:tree-data="categoryTreeSelectData"
|
||||||
|
:field-names="{ label: 'name', value: 'id', children: 'children' }"
|
||||||
|
placeholder="顶级分类(不选)"
|
||||||
|
allow-clear
|
||||||
|
tree-default-expand-all
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="排序号">
|
||||||
|
<a-input-number v-model:value="categoryForm.sortNo" :min="0" style="width: 100%" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="描述">
|
||||||
|
<a-textarea v-model:value="categoryForm.description" :rows="2" placeholder="可选" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
</a-modal>
|
||||||
|
|
||||||
|
<!-- 标签新增弹窗 -->
|
||||||
|
<a-modal v-model:open="tagModalVisible" title="新增标签" @ok="handleTagSubmit" :width="480" :confirm-loading="tagSubmitting">
|
||||||
|
<a-form layout="vertical">
|
||||||
|
<a-form-item label="标签名称" required>
|
||||||
|
<a-input v-model:value="tagForm.name" placeholder="请输入标签名称" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="排序号">
|
||||||
|
<a-input-number v-model:value="tagForm.sortNo" :min="0" style="width: 100%" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="描述">
|
||||||
|
<a-textarea v-model:value="tagForm.description" :rows="2" placeholder="可选" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
</a-modal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive, computed, onMounted } from 'vue';
|
||||||
|
import { PlusOutlined } from '@ant-design/icons-vue';
|
||||||
|
import { useMessage } from '/@/hooks/web/useMessage';
|
||||||
|
import {
|
||||||
|
categoryListAllApi,
|
||||||
|
categoryQueryByIdApi,
|
||||||
|
categoryAddApi,
|
||||||
|
categoryEditApi,
|
||||||
|
categoryDeleteApi,
|
||||||
|
tagListAllApi,
|
||||||
|
tagAddApi,
|
||||||
|
tagDeleteApi,
|
||||||
|
buildTree,
|
||||||
|
} from './label.api';
|
||||||
|
|
||||||
|
const { createMessage } = useMessage();
|
||||||
|
|
||||||
|
// ============ 分类树 ============
|
||||||
|
const categoryList = ref<any[]>([]);
|
||||||
|
const categoryLoading = ref(false);
|
||||||
|
const categorySearch = ref('');
|
||||||
|
const expandedKeys = ref<string[]>([]);
|
||||||
|
const selectedKeys = ref<string[]>([]);
|
||||||
|
|
||||||
|
const categoryTree = computed(() => buildTree(categoryList.value));
|
||||||
|
|
||||||
|
const filteredTreeData = computed(() => {
|
||||||
|
if (!categorySearch.value) return categoryTree.value;
|
||||||
|
const kw = categorySearch.value.toLowerCase();
|
||||||
|
const filter = (list: any[]): any[] =>
|
||||||
|
list
|
||||||
|
.map((item) => {
|
||||||
|
const children = item.children ? filter(item.children) : undefined;
|
||||||
|
if (item.name.toLowerCase().includes(kw) || (children && children.length > 0)) {
|
||||||
|
return { ...item, children };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
})
|
||||||
|
.filter(Boolean);
|
||||||
|
return filter(categoryTree.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
const categoryTreeSelectData = computed(() => categoryTree.value);
|
||||||
|
|
||||||
|
function onSelectCategory(keys: string[], info: any) {
|
||||||
|
if (keys.length > 0) {
|
||||||
|
createMessage.info(`已选中分类:${info.node.name}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleAddCategory(parentId?: string) {
|
||||||
|
categoryModalTitle.value = '新增分类';
|
||||||
|
categoryForm.id = '';
|
||||||
|
categoryForm.name = '';
|
||||||
|
categoryForm.parentId = parentId || undefined;
|
||||||
|
categoryForm.sortNo = 1;
|
||||||
|
categoryForm.description = '';
|
||||||
|
categoryModalVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleAddSubCategory(parentId: string) {
|
||||||
|
handleAddCategory(parentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleEditCategory(record: any) {
|
||||||
|
// 拉详情获取完整字段
|
||||||
|
const detail = await categoryQueryByIdApi({ id: record.id }).catch(() => null);
|
||||||
|
categoryModalTitle.value = '编辑分类';
|
||||||
|
categoryForm.id = detail?.id || record.id;
|
||||||
|
categoryForm.name = detail?.name || record.name;
|
||||||
|
categoryForm.parentId = detail?.parentId || undefined;
|
||||||
|
categoryForm.sortNo = detail?.sortNo ?? 1;
|
||||||
|
categoryForm.description = detail?.description || '';
|
||||||
|
categoryModalVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDeleteCategory(record: any) {
|
||||||
|
categoryDeleteApi({ id: record.id }, () => {
|
||||||
|
createMessage.success('已删除分类');
|
||||||
|
loadCategoryTree();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleCategorySubmit() {
|
||||||
|
if (!categoryForm.name) {
|
||||||
|
createMessage.warning('请输入分类名称');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
categorySubmitting.value = true;
|
||||||
|
try {
|
||||||
|
const payload = {
|
||||||
|
id: categoryForm.id || undefined,
|
||||||
|
name: categoryForm.name,
|
||||||
|
parentId: categoryForm.parentId,
|
||||||
|
sortNo: categoryForm.sortNo,
|
||||||
|
description: categoryForm.description,
|
||||||
|
status: 1,
|
||||||
|
};
|
||||||
|
if (categoryForm.id) {
|
||||||
|
await categoryEditApi(payload);
|
||||||
|
createMessage.success('已更新分类');
|
||||||
|
} else {
|
||||||
|
await categoryAddApi(payload);
|
||||||
|
createMessage.success('已新增分类');
|
||||||
|
}
|
||||||
|
categoryModalVisible.value = false;
|
||||||
|
loadCategoryTree();
|
||||||
|
} finally {
|
||||||
|
categorySubmitting.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============ 标签 ============
|
||||||
|
const tagList = ref<any[]>([]);
|
||||||
|
const tagLoading = ref(false);
|
||||||
|
const tagSearch = ref('');
|
||||||
|
|
||||||
|
const filteredTags = computed(() => {
|
||||||
|
if (!tagSearch.value) return tagList.value;
|
||||||
|
return tagList.value.filter((t) => t.name.includes(tagSearch.value));
|
||||||
|
});
|
||||||
|
|
||||||
|
const tagColorPool = ['blue', 'green', 'orange', 'purple', 'red', 'default'];
|
||||||
|
function getTagColor(id: string) {
|
||||||
|
let hash = 0;
|
||||||
|
for (let i = 0; i < id.length; i++) {
|
||||||
|
hash = id.charCodeAt(i) + ((hash << 5) - hash);
|
||||||
|
}
|
||||||
|
return tagColorPool[Math.abs(hash) % tagColorPool.length];
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleAddTag() {
|
||||||
|
tagForm.name = '';
|
||||||
|
tagForm.sortNo = 1;
|
||||||
|
tagForm.description = '';
|
||||||
|
tagModalVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDeleteTag(tag: any) {
|
||||||
|
tagDeleteApi({ id: tag.id }, () => {
|
||||||
|
createMessage.success('已删除标签');
|
||||||
|
loadTags();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleTagSubmit() {
|
||||||
|
if (!tagForm.name) {
|
||||||
|
createMessage.warning('请输入标签名称');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
tagSubmitting.value = true;
|
||||||
|
try {
|
||||||
|
await tagAddApi({
|
||||||
|
name: tagForm.name,
|
||||||
|
sortNo: tagForm.sortNo,
|
||||||
|
description: tagForm.description,
|
||||||
|
status: 1,
|
||||||
|
});
|
||||||
|
createMessage.success('已新增标签');
|
||||||
|
tagModalVisible.value = false;
|
||||||
|
loadTags();
|
||||||
|
} finally {
|
||||||
|
tagSubmitting.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============ 权限矩阵(mock,后端未提供接口) ============
|
||||||
|
const permissionList = ref<any[]>([
|
||||||
|
{ role: '系统管理员', permissions: ['rw', 'rw', 'rw', 'rw', 'rw'] },
|
||||||
|
{ role: '内容编辑', permissions: ['r', 'rw', 'rw', 'r', 'rw'] },
|
||||||
|
{ role: '审核员', permissions: ['r', 'r', 'r', 'r', 'r'] },
|
||||||
|
{ role: '普通用户', permissions: ['n', 'n', 'n', 'n', 'n'] },
|
||||||
|
]);
|
||||||
|
const permissionColumns = [
|
||||||
|
{ title: '角色', dataIndex: 'role', align: 'center', width: 120 },
|
||||||
|
{ title: '心肺复苏', dataIndex: 'p0', align: 'center', idx: 0 },
|
||||||
|
{ title: '创伤处理', dataIndex: 'p1', align: 'center', idx: 1 },
|
||||||
|
{ title: '中暑急救', dataIndex: 'p2', align: 'center', idx: 2 },
|
||||||
|
{ title: '骨折固定', dataIndex: 'p3', align: 'center', idx: 3 },
|
||||||
|
{ title: '烧烫伤', dataIndex: 'p4', align: 'center', idx: 4 },
|
||||||
|
];
|
||||||
|
|
||||||
|
function getPermissionColor(code: string) {
|
||||||
|
if (code === 'rw') return 'blue';
|
||||||
|
if (code === 'r') return 'default';
|
||||||
|
if (code === 'n') return 'red';
|
||||||
|
return 'default';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPermissionText(code: string) {
|
||||||
|
if (code === 'rw') return '读写';
|
||||||
|
if (code === 'r') return '只读';
|
||||||
|
if (code === 'n') return '无权限';
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============ 弹窗状态 ============
|
||||||
|
const categoryModalVisible = ref(false);
|
||||||
|
const categoryModalTitle = ref('新增分类');
|
||||||
|
const categorySubmitting = ref(false);
|
||||||
|
const categoryForm = reactive({
|
||||||
|
id: '',
|
||||||
|
name: '',
|
||||||
|
parentId: undefined as string | undefined,
|
||||||
|
sortNo: 1,
|
||||||
|
description: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const tagModalVisible = ref(false);
|
||||||
|
const tagSubmitting = ref(false);
|
||||||
|
const tagForm = reactive({ name: '', sortNo: 1, description: '' });
|
||||||
|
|
||||||
|
// ============ 加载 ============
|
||||||
|
async function loadCategoryTree() {
|
||||||
|
categoryLoading.value = true;
|
||||||
|
try {
|
||||||
|
const res = await categoryListAllApi();
|
||||||
|
categoryList.value = res || [];
|
||||||
|
// 默认展开第一层
|
||||||
|
if (expandedKeys.value.length === 0 && categoryTree.value.length > 0) {
|
||||||
|
expandedKeys.value = [categoryTree.value[0].id];
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
categoryLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadTags() {
|
||||||
|
tagLoading.value = true;
|
||||||
|
try {
|
||||||
|
const res = await tagListAllApi();
|
||||||
|
tagList.value = res || [];
|
||||||
|
} finally {
|
||||||
|
tagLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadCategoryTree();
|
||||||
|
loadTags();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.label-page {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.card-section {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
.tree-wrap {
|
||||||
|
min-height: 200px;
|
||||||
|
}
|
||||||
|
.tree-node-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.tree-name {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.tree-actions {
|
||||||
|
display: none;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
:deep(.ant-tree-node-content-wrapper:hover) .tree-actions,
|
||||||
|
:deep(.ant-tree-treenode-selected) .tree-actions {
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
|
.tag-cloud {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
min-height: 60px;
|
||||||
|
}
|
||||||
|
.tag-item-cloud {
|
||||||
|
font-size: 13px;
|
||||||
|
padding: 4px 14px;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
import { defHttp } from '/@/utils/http/axios';
|
||||||
|
import { useMessage } from '/@/hooks/web/useMessage';
|
||||||
|
|
||||||
|
const { createConfirm } = useMessage();
|
||||||
|
|
||||||
|
enum Api {
|
||||||
|
// 分类
|
||||||
|
categoryList = '/health-emergency/emergency/firstAid/category/list',
|
||||||
|
categoryListAll = '/health-emergency/emergency/firstAid/category/listAll',
|
||||||
|
categoryListRoot = '/health-emergency/emergency/firstAid/category/listRoot',
|
||||||
|
categoryListChildren = '/health-emergency/emergency/firstAid/category/listChildren',
|
||||||
|
categoryQueryById = '/health-emergency/emergency/firstAid/category/queryById',
|
||||||
|
categoryAdd = '/health-emergency/emergency/firstAid/category/add',
|
||||||
|
categoryEdit = '/health-emergency/emergency/firstAid/category/edit',
|
||||||
|
categoryDelete = '/health-emergency/emergency/firstAid/category/delete',
|
||||||
|
categoryDeleteBatch = '/health-emergency/emergency/firstAid/category/deleteBatch',
|
||||||
|
// 标签
|
||||||
|
tagList = '/health-emergency/emergency/firstAid/tag/list',
|
||||||
|
tagListAll = '/health-emergency/emergency/firstAid/tag/listAll',
|
||||||
|
tagQueryById = '/health-emergency/emergency/firstAid/tag/queryById',
|
||||||
|
tagAdd = '/health-emergency/emergency/firstAid/tag/add',
|
||||||
|
tagEdit = '/health-emergency/emergency/firstAid/tag/edit',
|
||||||
|
tagDelete = '/health-emergency/emergency/firstAid/tag/delete',
|
||||||
|
tagDeleteBatch = '/health-emergency/emergency/firstAid/tag/deleteBatch',
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============ 分类 ============
|
||||||
|
export const categoryListApi = (params: any) => defHttp.get({ url: Api.categoryList, params });
|
||||||
|
export const categoryListAllApi = (params?: any) => defHttp.get({ url: Api.categoryListAll, params });
|
||||||
|
export const categoryListRootApi = (params?: any) => defHttp.get({ url: Api.categoryListRoot, params });
|
||||||
|
export const categoryListChildrenApi = (params: any) => defHttp.get({ url: Api.categoryListChildren, params });
|
||||||
|
export const categoryQueryByIdApi = (params: any) => defHttp.get({ url: Api.categoryQueryById, params });
|
||||||
|
export const categoryAddApi = (params: any) => defHttp.post({ url: Api.categoryAdd, params });
|
||||||
|
export const categoryEditApi = (params: any) => defHttp.post({ url: Api.categoryEdit, params });
|
||||||
|
|
||||||
|
export const categoryDeleteApi = (params: any, handleSuccess: any) => {
|
||||||
|
createConfirm({
|
||||||
|
iconType: 'warning',
|
||||||
|
title: '确认删除',
|
||||||
|
content: '是否删除该分类?',
|
||||||
|
onOk: async () => {
|
||||||
|
await defHttp.post({ url: Api.categoryDelete, params });
|
||||||
|
handleSuccess();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const categoryDeleteBatchApi = (params: any, handleSuccess: any) => {
|
||||||
|
createConfirm({
|
||||||
|
iconType: 'warning',
|
||||||
|
title: '批量删除',
|
||||||
|
content: `是否删除选中的 ${params?.length || 0} 个分类?`,
|
||||||
|
onOk: async () => {
|
||||||
|
await defHttp.post({ url: Api.categoryDeleteBatch, params });
|
||||||
|
handleSuccess();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============ 标签 ============
|
||||||
|
export const tagListApi = (params: any) => defHttp.get({ url: Api.tagList, params });
|
||||||
|
export const tagListAllApi = (params?: any) => defHttp.get({ url: Api.tagListAll, params });
|
||||||
|
export const tagQueryByIdApi = (params: any) => defHttp.get({ url: Api.tagQueryById, params });
|
||||||
|
export const tagAddApi = (params: any) => defHttp.post({ url: Api.tagAdd, params });
|
||||||
|
export const tagEditApi = (params: any) => defHttp.post({ url: Api.tagEdit, params });
|
||||||
|
|
||||||
|
export const tagDeleteApi = (params: any, handleSuccess: any) => {
|
||||||
|
createConfirm({
|
||||||
|
iconType: 'warning',
|
||||||
|
title: '确认删除',
|
||||||
|
content: '是否删除该标签?',
|
||||||
|
onOk: async () => {
|
||||||
|
await defHttp.post({ url: Api.tagDelete, params });
|
||||||
|
handleSuccess();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const tagDeleteBatchApi = (params: any, handleSuccess: any) => {
|
||||||
|
createConfirm({
|
||||||
|
iconType: 'warning',
|
||||||
|
title: '批量删除',
|
||||||
|
content: `是否删除选中的 ${params?.length || 0} 个标签?`,
|
||||||
|
onOk: async () => {
|
||||||
|
await defHttp.post({ url: Api.tagDeleteBatch, params });
|
||||||
|
handleSuccess();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============ 工具:扁平列表转树 ============
|
||||||
|
export function buildTree(list: any[]): any[] {
|
||||||
|
const map = new Map<string, any>();
|
||||||
|
const roots: any[] = [];
|
||||||
|
list.forEach((item) => {
|
||||||
|
map.set(item.id, { ...item, children: [] });
|
||||||
|
});
|
||||||
|
list.forEach((item) => {
|
||||||
|
const node = map.get(item.id);
|
||||||
|
if (item.parentId && map.has(item.parentId)) {
|
||||||
|
map.get(item.parentId).children.push(node);
|
||||||
|
} else {
|
||||||
|
roots.push(node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// 移除空 children
|
||||||
|
const clean = (nodes: any[]) => {
|
||||||
|
nodes.forEach((n) => {
|
||||||
|
if (n.children.length === 0) {
|
||||||
|
delete n.children;
|
||||||
|
} else {
|
||||||
|
clean(n.children);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return nodes;
|
||||||
|
};
|
||||||
|
return clean(roots);
|
||||||
|
}
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
<template>
|
||||||
|
<BasicModal v-bind="$attrs" @register="registerModal" title="移动端预览" :width="420" :minHeight="500" :showOkBtn="false" cancelText="关闭">
|
||||||
|
<div class="preview-wrap">
|
||||||
|
<div class="preview-phone">
|
||||||
|
<div class="phone-bar">
|
||||||
|
<span class="phone-status">急救宣教资源库</span>
|
||||||
|
<span class="phone-time">9:41</span>
|
||||||
|
</div>
|
||||||
|
<div class="phone-content">
|
||||||
|
<div class="phone-cover">
|
||||||
|
<img v-if="record.coverImage" :src="record.coverImage" class="cover-img" />
|
||||||
|
<span v-else>{{ typeIcon }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="phone-tags">
|
||||||
|
<a-tag v-if="record.categoryName" color="blue">{{ record.categoryName }}</a-tag>
|
||||||
|
<a-tag v-if="record.contentType">{{ typeLabel }}</a-tag>
|
||||||
|
</div>
|
||||||
|
<div class="phone-title">{{ record.title }}</div>
|
||||||
|
<div class="phone-meta">
|
||||||
|
<span>🕐 {{ record.createTime || '-' }}</span>
|
||||||
|
<span style="margin-left: 8px">👁️ {{ record.viewCount || 0 }}次阅读</span>
|
||||||
|
</div>
|
||||||
|
<div class="phone-body">
|
||||||
|
{{ record.summary || '暂无摘要' }}
|
||||||
|
</div>
|
||||||
|
<div class="phone-action">
|
||||||
|
<a-button type="primary" block>▶ 播放视频</a-button>
|
||||||
|
</div>
|
||||||
|
<div class="phone-footer">
|
||||||
|
<span>👍 {{ Math.floor((record.viewCount || 0) / 6) }}</span>
|
||||||
|
<span>⭐ {{ record.favoriteCount || 0 }}</span>
|
||||||
|
<span>🔗 分享</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</BasicModal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed } from 'vue';
|
||||||
|
import { BasicModal, useModalInner } from '/@/components/Modal';
|
||||||
|
import { CONTENT_TYPE } from '../resource.data';
|
||||||
|
|
||||||
|
const record = ref<any>({});
|
||||||
|
|
||||||
|
const typeIcon = computed(() => {
|
||||||
|
return CONTENT_TYPE[record.value.contentType as keyof typeof CONTENT_TYPE]?.icon || '📄';
|
||||||
|
});
|
||||||
|
|
||||||
|
const typeLabel = computed(() => {
|
||||||
|
return CONTENT_TYPE[record.value.contentType as keyof typeof CONTENT_TYPE]?.label || record.value.contentType;
|
||||||
|
});
|
||||||
|
|
||||||
|
const [registerModal] = useModalInner(async (data) => {
|
||||||
|
record.value = data?.record || {};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.preview-wrap {
|
||||||
|
background: #f5f5f5;
|
||||||
|
padding: 24px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.preview-phone {
|
||||||
|
width: 300px;
|
||||||
|
border: 10px solid #001529;
|
||||||
|
border-radius: 32px;
|
||||||
|
background: #fff;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 6px 16px rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
.phone-bar {
|
||||||
|
background: #001529;
|
||||||
|
height: 40px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 16px;
|
||||||
|
}
|
||||||
|
.phone-status {
|
||||||
|
color: rgba(255, 255, 255, 0.65);
|
||||||
|
font-size: 12px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.phone-time {
|
||||||
|
color: #fff;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.phone-content {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
.phone-cover {
|
||||||
|
width: 100%;
|
||||||
|
height: 140px;
|
||||||
|
background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%);
|
||||||
|
border-radius: 2px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 32px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.cover-img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
.phone-tags {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.phone-title {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: rgba(0, 0, 0, 0.85);
|
||||||
|
}
|
||||||
|
.phone-meta {
|
||||||
|
font-size: 11px;
|
||||||
|
color: rgba(0, 0, 0, 0.45);
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.phone-body {
|
||||||
|
font-size: 13px;
|
||||||
|
color: rgba(0, 0, 0, 0.65);
|
||||||
|
line-height: 1.8;
|
||||||
|
}
|
||||||
|
.phone-action {
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
.phone-footer {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
margin-top: 14px;
|
||||||
|
padding-top: 10px;
|
||||||
|
border-top: 1px solid #f0f0f0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(0, 0, 0, 0.45);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,258 @@
|
|||||||
|
<template>
|
||||||
|
<BasicModal v-bind="$attrs" @register="registerModal" :title="title" :width="900" :minHeight="500" :useDefaultFooter="false" @ok="handleSubmit">
|
||||||
|
<div class="resource-form">
|
||||||
|
<a-form layout="vertical" :model="formState">
|
||||||
|
<a-row :gutter="16">
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item label="标题" required>
|
||||||
|
<a-input v-model:value="formState.title" placeholder="请输入资源标题" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item label="内容类型" required>
|
||||||
|
<a-select v-model:value="formState.contentType" :options="typeOptions" placeholder="请选择内容类型" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
<a-form-item label="摘要">
|
||||||
|
<a-input v-model:value="formState.summary" placeholder="请输入资源摘要(一句话简介)" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-row :gutter="16">
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item label="所属分类" required>
|
||||||
|
<a-select v-model:value="formState.categoryId" :options="categoryOptions" placeholder="请选择分类" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item label="适用场景">
|
||||||
|
<a-select v-model:value="formState.applicableScenario" :options="sceneOptions" placeholder="请选择适用场景" allow-clear />
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
<a-form-item label="标签">
|
||||||
|
<a-select
|
||||||
|
v-model:value="formState.tagIdList"
|
||||||
|
mode="multiple"
|
||||||
|
:options="tagOptions"
|
||||||
|
placeholder="请选择标签"
|
||||||
|
:field-names="{ label: 'name', value: 'id' }"
|
||||||
|
allow-clear
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="正文内容" required>
|
||||||
|
<a-textarea v-model:value="formState.content" :rows="5" placeholder="请输入急救知识内容,支持富文本编辑..." />
|
||||||
|
</a-form-item>
|
||||||
|
<a-row :gutter="16">
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item label="封面图">
|
||||||
|
<div class="upload-area">
|
||||||
|
<div class="upload-icon">🖼️</div>
|
||||||
|
<div class="upload-text">点击或拖拽上传封面图</div>
|
||||||
|
<div class="upload-hint">支持 JPG/PNG,建议尺寸 750×420px</div>
|
||||||
|
</div>
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item label="附件/媒体文件">
|
||||||
|
<div class="upload-area">
|
||||||
|
<div class="upload-icon">📎</div>
|
||||||
|
<div class="upload-text">点击或拖拽上传文件</div>
|
||||||
|
<div class="upload-hint">支持 MP4/MP3/PDF,单文件最大 500MB</div>
|
||||||
|
</div>
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
<a-row :gutter="16">
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item label="生效时间">
|
||||||
|
<a-date-picker v-model:value="formState.effectiveTime" showTime valueFormat="YYYY-MM-DD HH:mm:ss" style="width: 100%" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item label="失效时间">
|
||||||
|
<a-date-picker v-model:value="formState.expiryTime" showTime valueFormat="YYYY-MM-DD HH:mm:ss" style="width: 100%" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
</a-form>
|
||||||
|
</div>
|
||||||
|
<template #appendFooter>
|
||||||
|
<a-button @click="closeModal">取消</a-button>
|
||||||
|
<a-button @click="handleSaveDraft">保存草稿</a-button>
|
||||||
|
<a-button type="primary" @click="handleSubmit">提交审核</a-button>
|
||||||
|
</template>
|
||||||
|
</BasicModal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive, onMounted } from 'vue';
|
||||||
|
import { BasicModal, useModalInner } from '/@/components/Modal';
|
||||||
|
import { useMessage } from '/@/hooks/web/useMessage';
|
||||||
|
import { TYPE_OPTIONS, SCENE_OPTIONS } from '../resource.data';
|
||||||
|
import { addApi, editApi, submitForReviewApi, categoryListAllApi, tagListAllApi } from '../resource.api';
|
||||||
|
|
||||||
|
const emit = defineEmits(['success', 'register']);
|
||||||
|
const { createMessage } = useMessage();
|
||||||
|
const title = ref('新增急救宣教资源');
|
||||||
|
const categoryOptions = ref<any[]>([]);
|
||||||
|
const tagOptions = ref<any[]>([]);
|
||||||
|
|
||||||
|
const formState = reactive({
|
||||||
|
id: '',
|
||||||
|
title: '',
|
||||||
|
summary: '',
|
||||||
|
contentType: 'IMAGE_TEXT',
|
||||||
|
categoryId: '' as string,
|
||||||
|
applicableScenario: '' as string,
|
||||||
|
tagIdList: [] as string[],
|
||||||
|
content: '',
|
||||||
|
coverImage: '',
|
||||||
|
fileUrl: '',
|
||||||
|
effectiveTime: null as any,
|
||||||
|
expiryTime: null as any,
|
||||||
|
});
|
||||||
|
|
||||||
|
const typeOptions = TYPE_OPTIONS;
|
||||||
|
const sceneOptions = SCENE_OPTIONS;
|
||||||
|
|
||||||
|
const [registerModal, { closeModal, setModalProps }] = useModalInner(async (data) => {
|
||||||
|
if (data?.isUpdate) {
|
||||||
|
title.value = '编辑急救宣教资源';
|
||||||
|
Object.assign(formState, data.record);
|
||||||
|
// 后端 tags 是逗号分隔字符串,前端需要拆成数组
|
||||||
|
if (formState.tagIdList && typeof (formState as any).tags === 'string') {
|
||||||
|
formState.tagIdList = ((formState as any).tags || '').split(',').filter(Boolean);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
title.value = '新增急救宣教资源';
|
||||||
|
resetForm();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function resetForm() {
|
||||||
|
formState.id = '';
|
||||||
|
formState.title = '';
|
||||||
|
formState.summary = '';
|
||||||
|
formState.contentType = 'IMAGE_TEXT';
|
||||||
|
formState.categoryId = '';
|
||||||
|
formState.applicableScenario = '';
|
||||||
|
formState.tagIdList = [];
|
||||||
|
formState.content = '';
|
||||||
|
formState.coverImage = '';
|
||||||
|
formState.fileUrl = '';
|
||||||
|
formState.effectiveTime = null;
|
||||||
|
formState.expiryTime = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildPayload(): any {
|
||||||
|
return {
|
||||||
|
id: formState.id || undefined,
|
||||||
|
title: formState.title,
|
||||||
|
summary: formState.summary,
|
||||||
|
contentType: formState.contentType,
|
||||||
|
categoryId: formState.categoryId,
|
||||||
|
applicableScenario: formState.applicableScenario,
|
||||||
|
tags: formState.tagIdList.join(','),
|
||||||
|
content: formState.content,
|
||||||
|
coverImage: formState.coverImage,
|
||||||
|
fileUrl: formState.fileUrl,
|
||||||
|
effectiveTime: formState.effectiveTime,
|
||||||
|
expiryTime: formState.expiryTime,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function validate(): boolean {
|
||||||
|
if (!formState.title) {
|
||||||
|
createMessage.warning('请输入资源标题');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!formState.categoryId) {
|
||||||
|
createMessage.warning('请选择所属分类');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!formState.content) {
|
||||||
|
createMessage.warning('请输入正文内容');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSaveDraft() {
|
||||||
|
if (!formState.title) {
|
||||||
|
createMessage.warning('请输入资源标题');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setModalProps({ confirmLoading: true });
|
||||||
|
try {
|
||||||
|
if (formState.id) {
|
||||||
|
await editApi(buildPayload());
|
||||||
|
} else {
|
||||||
|
await addApi(buildPayload());
|
||||||
|
}
|
||||||
|
createMessage.success('已保存为草稿');
|
||||||
|
closeModal();
|
||||||
|
emit('success');
|
||||||
|
} finally {
|
||||||
|
setModalProps({ confirmLoading: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSubmit() {
|
||||||
|
if (!validate()) return;
|
||||||
|
setModalProps({ confirmLoading: true });
|
||||||
|
try {
|
||||||
|
const id = formState.id;
|
||||||
|
if (id) {
|
||||||
|
await editApi(buildPayload());
|
||||||
|
await submitForReviewApi({ id });
|
||||||
|
} else {
|
||||||
|
const res: any = await addApi(buildPayload());
|
||||||
|
await submitForReviewApi({ id: res?.id || res });
|
||||||
|
}
|
||||||
|
createMessage.success('已提交审核');
|
||||||
|
closeModal();
|
||||||
|
emit('success');
|
||||||
|
} finally {
|
||||||
|
setModalProps({ confirmLoading: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
categoryOptions.value = (await categoryListAllApi()) || [];
|
||||||
|
tagOptions.value = (await tagListAllApi()) || [];
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.resource-form {
|
||||||
|
padding: 4px 4px 0;
|
||||||
|
}
|
||||||
|
.upload-area {
|
||||||
|
border: 1px dashed #d9d9d9;
|
||||||
|
border-radius: 2px;
|
||||||
|
padding: 24px;
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
background: #fafafa;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: #1890ff;
|
||||||
|
background: #e6f7ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.upload-icon {
|
||||||
|
font-size: 32px;
|
||||||
|
color: rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
.upload-text {
|
||||||
|
font-size: 14px;
|
||||||
|
color: rgba(0, 0, 0, 0.65);
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
.upload-hint {
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(0, 0, 0, 0.45);
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
<template>
|
||||||
|
<BasicModal v-bind="$attrs" @register="registerModal" title="版本历史" :width="560" :minHeight="400" :showOkBtn="false" cancelText="关闭">
|
||||||
|
<a-spin :spinning="loading">
|
||||||
|
<div class="version-list">
|
||||||
|
<div v-for="(item, idx) in versions" :key="item.id" class="version-item">
|
||||||
|
<div class="version-num" :class="{ 'version-old': idx > 0 }">v{{ item.versionNo }}</div>
|
||||||
|
<div class="version-main">
|
||||||
|
<div class="version-op">{{ item.changeDescription || item.title }}</div>
|
||||||
|
<div class="version-meta">
|
||||||
|
{{ item.createBy }} · {{ item.createTime }}
|
||||||
|
<a @click="handleDiff(item)">查看差异</a>
|
||||||
|
<a v-if="idx > 0" @click="handleRollback(item)">回滚</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a-empty v-if="!loading && versions.length === 0" description="暂无版本记录" />
|
||||||
|
</div>
|
||||||
|
</a-spin>
|
||||||
|
</BasicModal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { BasicModal, useModalInner } from '/@/components/Modal';
|
||||||
|
import { useMessage } from '/@/hooks/web/useMessage';
|
||||||
|
import { versionListApi, versionRollbackApi } from '../resource.api';
|
||||||
|
|
||||||
|
const { createMessage, createConfirm } = useMessage();
|
||||||
|
const record = ref<any>({});
|
||||||
|
const versions = ref<any[]>([]);
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
|
const [registerModal] = useModalInner(async (data) => {
|
||||||
|
record.value = data?.record || {};
|
||||||
|
await loadVersions();
|
||||||
|
});
|
||||||
|
|
||||||
|
async function loadVersions() {
|
||||||
|
if (!record.value.id) return;
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const res = await versionListApi({ resourceId: record.value.id });
|
||||||
|
versions.value = res || [];
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDiff(item: any) {
|
||||||
|
createMessage.info(`查看差异:v${item.versionNo}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRollback(item: any) {
|
||||||
|
createConfirm({
|
||||||
|
iconType: 'warning',
|
||||||
|
title: '确认回滚',
|
||||||
|
content: `是否回滚至 v${item.versionNo}?回滚前会自动保存当前状态为新版本。`,
|
||||||
|
onOk: async () => {
|
||||||
|
await versionRollbackApi({ versionId: item.id });
|
||||||
|
createMessage.success('已回滚');
|
||||||
|
await loadVersions();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.version-list {
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
|
.version-item {
|
||||||
|
display: flex;
|
||||||
|
gap: 14px;
|
||||||
|
padding: 12px 0;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.version-num {
|
||||||
|
font-size: 12px;
|
||||||
|
background: #e6f7ff;
|
||||||
|
color: #1890ff;
|
||||||
|
padding: 0 7px;
|
||||||
|
border-radius: 2px;
|
||||||
|
white-space: nowrap;
|
||||||
|
border: 1px solid #91d5ff;
|
||||||
|
line-height: 20px;
|
||||||
|
height: fit-content;
|
||||||
|
}
|
||||||
|
.version-old {
|
||||||
|
background: #fafafa;
|
||||||
|
color: rgba(0, 0, 0, 0.45);
|
||||||
|
border-color: #d9d9d9;
|
||||||
|
}
|
||||||
|
.version-main {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.version-op {
|
||||||
|
font-size: 14px;
|
||||||
|
color: rgba(0, 0, 0, 0.85);
|
||||||
|
}
|
||||||
|
.version-meta {
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(0, 0, 0, 0.45);
|
||||||
|
margin-top: 3px;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #1890ff;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-left: 8px;
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,348 @@
|
|||||||
|
<template>
|
||||||
|
<BasicTable @register="registerTable" :rowSelection="{ type: 'checkbox', onChange: onSelectChange }">
|
||||||
|
<template #tableTitle>
|
||||||
|
<a-button type="primary" preIcon="ant-design:plus-outlined" @click="handleAdd"> 新增资源 </a-button>
|
||||||
|
<a-button type="primary" preIcon="ant-design:export-outlined" @click="handleExport"> 导出 </a-button>
|
||||||
|
<template v-if="selectedRowKeys.length > 0">
|
||||||
|
<a-button type="primary" preIcon="ant-design:cloud-upload-outlined" @click="handleBatchPublish"> 批量发布 </a-button>
|
||||||
|
<a-button preIcon="ant-design:cloud-download-outlined" @click="handleBatchOffline"> 批量下架 </a-button>
|
||||||
|
<a-button danger preIcon="ant-design:delete-outlined" @click="handleBatchDelete"> 批量删除 </a-button>
|
||||||
|
<span class="batch-tip">已选中 {{ selectedRowKeys.length }} 条</span>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
<template #bodyCell="{ column, record }">
|
||||||
|
<template v-if="column.dataIndex === 'coverImage'">
|
||||||
|
<div class="cover-box" :style="coverStyle(record)">
|
||||||
|
<img v-if="record.coverImage" :src="record.coverImage" class="cover-img" />
|
||||||
|
<span v-else>{{ typeIcon(record.contentType) }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-if="column.dataIndex === 'title'">
|
||||||
|
<div class="title-cell">
|
||||||
|
<div class="title-main">{{ record.title }}</div>
|
||||||
|
<div class="title-sub">{{ record.summary }}</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-if="column.dataIndex === 'categoryName'">
|
||||||
|
<a-tag v-if="record.categoryName" color="blue">{{ record.categoryName }}</a-tag>
|
||||||
|
</template>
|
||||||
|
<template v-if="column.dataIndex === 'contentType'">
|
||||||
|
<a-tag :color="typeColor(record.contentType)">{{ typeIcon(record.contentType) }} {{ typeLabel(record.contentType) }}</a-tag>
|
||||||
|
</template>
|
||||||
|
<template v-if="column.dataIndex === 'tagNameList'">
|
||||||
|
<a-tag v-for="tag in (record.tagNameList || [])" :key="tag" color="default">{{ tag }}</a-tag>
|
||||||
|
</template>
|
||||||
|
<template v-if="column.dataIndex === 'statusDisplay'">
|
||||||
|
<a-badge :status="getStatusInfo(record).color" :text="getStatusInfo(record).text" />
|
||||||
|
</template>
|
||||||
|
<template v-if="column.dataIndex === 'viewFavorite'">
|
||||||
|
<span>{{ record.viewCount || 0 }} / {{ record.favoriteCount || 0 }}</span>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
<template #action="{ record }">
|
||||||
|
<TableAction :actions="getTableAction(record)" :dropDownActions="getDropDownAction(record)" />
|
||||||
|
</template>
|
||||||
|
</BasicTable>
|
||||||
|
|
||||||
|
<ResourceFormModal @register="registerFormModal" @success="reload" />
|
||||||
|
<PreviewModal @register="registerPreviewModal" />
|
||||||
|
<VersionHistoryModal @register="registerVersionModal" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import BasicTable from '/@/components/Table/src/BasicTable.vue';
|
||||||
|
import { TableAction } from '/@/components/Table';
|
||||||
|
import { useListPage } from '/@/hooks/system/useListPage';
|
||||||
|
import { useMessage } from '/@/hooks/web/useMessage';
|
||||||
|
import { useModal } from '/@/components/Modal';
|
||||||
|
import {
|
||||||
|
columns,
|
||||||
|
searchSchema,
|
||||||
|
CONTENT_TYPE,
|
||||||
|
STATUS,
|
||||||
|
AUDIT_STATUS,
|
||||||
|
getResourceDisplayStatus,
|
||||||
|
} from './resource.data';
|
||||||
|
import {
|
||||||
|
listApi,
|
||||||
|
deleteApi,
|
||||||
|
batchDeleteApi,
|
||||||
|
submitForReviewApi,
|
||||||
|
takeDownApi,
|
||||||
|
publishApi,
|
||||||
|
copyApi,
|
||||||
|
batchPublishApi,
|
||||||
|
batchTakeDownApi,
|
||||||
|
} from './resource.api';
|
||||||
|
import ResourceFormModal from './components/ResourceFormModal.vue';
|
||||||
|
import PreviewModal from './components/PreviewModal.vue';
|
||||||
|
import VersionHistoryModal from './components/VersionHistoryModal.vue';
|
||||||
|
|
||||||
|
const { createMessage, createConfirm } = useMessage();
|
||||||
|
const selectedRowKeys = ref<string[]>([]);
|
||||||
|
|
||||||
|
const [registerFormModal, { openModal: openFormModal }] = useModal();
|
||||||
|
const [registerPreviewModal, { openModal: openPreviewModal }] = useModal();
|
||||||
|
const [registerVersionModal, { openModal: openVersionModal }] = useModal();
|
||||||
|
|
||||||
|
const { tableContext } = useListPage({
|
||||||
|
tableProps: {
|
||||||
|
title: '急救宣教资源列表',
|
||||||
|
api: listApi,
|
||||||
|
columns,
|
||||||
|
canResize: false,
|
||||||
|
showIndexColumn: false,
|
||||||
|
rowKey: 'id',
|
||||||
|
formConfig: {
|
||||||
|
schemas: searchSchema,
|
||||||
|
autoSubmitOnEnter: true,
|
||||||
|
showAdvancedButton: false,
|
||||||
|
},
|
||||||
|
actionColumn: {
|
||||||
|
width: 280,
|
||||||
|
fixed: 'right',
|
||||||
|
},
|
||||||
|
afterFetch: (data) => {
|
||||||
|
data.forEach((item: any) => {
|
||||||
|
const display = getResourceDisplayStatus(item.status, item.auditStatus);
|
||||||
|
item.statusDisplay = display;
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const [registerTable, { reload }] = tableContext;
|
||||||
|
|
||||||
|
function onSelectChange(keys: string[]) {
|
||||||
|
selectedRowKeys.value = keys;
|
||||||
|
}
|
||||||
|
|
||||||
|
function coverStyle(record: Recordable) {
|
||||||
|
const t = CONTENT_TYPE[record.contentType as keyof typeof CONTENT_TYPE];
|
||||||
|
const bgMap: Record<string, string> = {
|
||||||
|
IMAGE_TEXT: '#fa8c16',
|
||||||
|
VIDEO: '#001529',
|
||||||
|
AUDIO: '#8c8c8c',
|
||||||
|
PDF: '#434343',
|
||||||
|
};
|
||||||
|
return { background: bgMap[record.contentType] || '#001529' };
|
||||||
|
}
|
||||||
|
|
||||||
|
function typeColor(type: string) {
|
||||||
|
return CONTENT_TYPE[type as keyof typeof CONTENT_TYPE]?.color || 'default';
|
||||||
|
}
|
||||||
|
|
||||||
|
function typeIcon(type: string) {
|
||||||
|
return CONTENT_TYPE[type as keyof typeof CONTENT_TYPE]?.icon || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function typeLabel(type: string) {
|
||||||
|
return CONTENT_TYPE[type as keyof typeof CONTENT_TYPE]?.label || type;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStatusInfo(record: Recordable) {
|
||||||
|
return getResourceDisplayStatus(record.status, record.auditStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 状态判断辅助
|
||||||
|
const isPublished = (r: Recordable) => r.status === STATUS.PUBLISHED;
|
||||||
|
const isOffline = (r: Recordable) => r.status === STATUS.OFFLINE;
|
||||||
|
const isPending = (r: Recordable) => r.auditStatus === AUDIT_STATUS.PENDING;
|
||||||
|
const isRejected = (r: Recordable) => r.auditStatus === AUDIT_STATUS.REJECTED;
|
||||||
|
const isDraft = (r: Recordable) => r.status === STATUS.DRAFT && r.auditStatus == null;
|
||||||
|
|
||||||
|
function getTableAction(record: Recordable) {
|
||||||
|
const actions: any[] = [
|
||||||
|
{
|
||||||
|
label: '预览',
|
||||||
|
onClick: () => openPreviewModal(true, { record }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '编辑',
|
||||||
|
onClick: () => openFormModal(true, { isUpdate: true, record }),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
if (isPublished(record)) {
|
||||||
|
actions.push({
|
||||||
|
label: '下架',
|
||||||
|
color: 'error',
|
||||||
|
popConfirm: {
|
||||||
|
title: `是否下架《${record.title}》?`,
|
||||||
|
confirm: () => handleOffline(record),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else if (isOffline(record)) {
|
||||||
|
actions.push({
|
||||||
|
label: '上架',
|
||||||
|
onClick: () => handlePublish(record),
|
||||||
|
});
|
||||||
|
} else if (isPending(record)) {
|
||||||
|
// 待审核状态不显示额外按钮
|
||||||
|
} else if (isRejected(record)) {
|
||||||
|
actions.push({
|
||||||
|
label: '提交审核',
|
||||||
|
onClick: () => handleSubmitReview(record),
|
||||||
|
});
|
||||||
|
} else if (isDraft(record)) {
|
||||||
|
actions.push({
|
||||||
|
label: '提交审核',
|
||||||
|
onClick: () => handleSubmitReview(record),
|
||||||
|
});
|
||||||
|
actions.push({
|
||||||
|
label: '删除',
|
||||||
|
color: 'error',
|
||||||
|
popConfirm: {
|
||||||
|
title: `是否删除《${record.title}》?`,
|
||||||
|
confirm: () => handleDelete(record),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else if (record.status === STATUS.DRAFT && record.auditStatus === AUDIT_STATUS.PASSED) {
|
||||||
|
// 已通过的草稿状态(编辑后自动退回草稿),可重新提交
|
||||||
|
actions.push({
|
||||||
|
label: '提交审核',
|
||||||
|
onClick: () => handleSubmitReview(record),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return actions;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDropDownAction(record: Recordable) {
|
||||||
|
const dropdown: any[] = [
|
||||||
|
{
|
||||||
|
label: '版本历史',
|
||||||
|
onClick: () => openVersionModal(true, { record }),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
// 非草稿状态可复制
|
||||||
|
if (!isDraft(record)) {
|
||||||
|
dropdown.push({
|
||||||
|
label: '复制',
|
||||||
|
onClick: () => handleCopy(record),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// 已驳回或已下架可删除
|
||||||
|
if (isRejected(record) || isOffline(record)) {
|
||||||
|
dropdown.push({
|
||||||
|
label: '删除',
|
||||||
|
color: 'error',
|
||||||
|
popConfirm: {
|
||||||
|
title: `是否删除《${record.title}》?`,
|
||||||
|
confirm: () => handleDelete(record),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return dropdown;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleAdd() {
|
||||||
|
openFormModal(true, { isUpdate: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleExport() {
|
||||||
|
createMessage.info('导出功能待后端提供接口');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleOffline(record: Recordable) {
|
||||||
|
await takeDownApi({ id: record.id });
|
||||||
|
createMessage.success('已下架');
|
||||||
|
reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handlePublish(record: Recordable) {
|
||||||
|
await publishApi({ id: record.id });
|
||||||
|
createMessage.success('已上架');
|
||||||
|
reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSubmitReview(record: Recordable) {
|
||||||
|
await submitForReviewApi({ id: record.id });
|
||||||
|
createMessage.success('已提交审核');
|
||||||
|
reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleCopy(record: Recordable) {
|
||||||
|
await copyApi({ id: record.id });
|
||||||
|
createMessage.success('已复制为草稿');
|
||||||
|
reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDelete(record: Recordable) {
|
||||||
|
deleteApi({ id: record.id }, () => {
|
||||||
|
createMessage.success('已删除');
|
||||||
|
reload();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleBatchPublish() {
|
||||||
|
createConfirm({
|
||||||
|
iconType: 'info',
|
||||||
|
title: '批量发布',
|
||||||
|
content: `是否发布选中的 ${selectedRowKeys.value.length} 条资源?`,
|
||||||
|
onOk: async () => {
|
||||||
|
await batchPublishApi(selectedRowKeys.value);
|
||||||
|
createMessage.success('批量发布成功');
|
||||||
|
selectedRowKeys.value = [];
|
||||||
|
reload();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleBatchOffline() {
|
||||||
|
createConfirm({
|
||||||
|
iconType: 'warning',
|
||||||
|
title: '批量下架',
|
||||||
|
content: `是否下架选中的 ${selectedRowKeys.value.length} 条资源?`,
|
||||||
|
onOk: async () => {
|
||||||
|
await batchTakeDownApi(selectedRowKeys.value);
|
||||||
|
createMessage.success('批量下架成功');
|
||||||
|
selectedRowKeys.value = [];
|
||||||
|
reload();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleBatchDelete() {
|
||||||
|
batchDeleteApi(selectedRowKeys.value, () => {
|
||||||
|
createMessage.success('已批量删除');
|
||||||
|
selectedRowKeys.value = [];
|
||||||
|
reload();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.batch-tip {
|
||||||
|
margin-left: 8px;
|
||||||
|
color: #1890ff;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.cover-box {
|
||||||
|
width: 50px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 2px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 18px;
|
||||||
|
margin: 0 auto;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.cover-img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
.title-cell {
|
||||||
|
.title-main {
|
||||||
|
font-weight: 600;
|
||||||
|
color: rgba(0, 0, 0, 0.85);
|
||||||
|
}
|
||||||
|
.title-sub {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #999;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import { defHttp } from '/@/utils/http/axios';
|
||||||
|
import { useMessage } from '/@/hooks/web/useMessage';
|
||||||
|
|
||||||
|
const { createConfirm } = useMessage();
|
||||||
|
|
||||||
|
enum Api {
|
||||||
|
// 资源 CRUD
|
||||||
|
list = '/health-emergency/emergency/firstAid/resource/list',
|
||||||
|
queryById = '/health-emergency/emergency/firstAid/resource/queryById',
|
||||||
|
add = '/health-emergency/emergency/firstAid/resource/add',
|
||||||
|
edit = '/health-emergency/emergency/firstAid/resource/edit',
|
||||||
|
delete = '/health-emergency/emergency/firstAid/resource/delete',
|
||||||
|
deleteBatch = '/health-emergency/emergency/firstAid/resource/deleteBatch',
|
||||||
|
submitForReview = '/health-emergency/emergency/firstAid/resource/submitForReview',
|
||||||
|
takeDown = '/health-emergency/emergency/firstAid/resource/takeDown',
|
||||||
|
publish = '/health-emergency/emergency/firstAid/resource/publish',
|
||||||
|
batchTakeDown = '/health-emergency/emergency/firstAid/resource/batchTakeDown',
|
||||||
|
batchPublish = '/health-emergency/emergency/firstAid/resource/batchPublish',
|
||||||
|
incrementView = '/health-emergency/emergency/firstAid/resource/incrementView',
|
||||||
|
copy = '/health-emergency/emergency/firstAid/resource/copy',
|
||||||
|
// 版本
|
||||||
|
versionList = '/health-emergency/emergency/firstAid/version/list',
|
||||||
|
versionRollback = '/health-emergency/emergency/firstAid/version/rollback',
|
||||||
|
// 分类 & 标签(下拉用)
|
||||||
|
categoryListAll = '/health-emergency/emergency/firstAid/category/listAll',
|
||||||
|
tagListAll = '/health-emergency/emergency/firstAid/tag/listAll',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const listApi = (params: any) => defHttp.get({ url: Api.list, params });
|
||||||
|
export const queryByIdApi = (params: any) => defHttp.get({ url: Api.queryById, params });
|
||||||
|
export const addApi = (params: any) => defHttp.post({ url: Api.add, params });
|
||||||
|
export const editApi = (params: any) => defHttp.post({ url: Api.edit, params });
|
||||||
|
export const submitForReviewApi = (params: any) => defHttp.post({ url: Api.submitForReview, params });
|
||||||
|
export const takeDownApi = (params: any) => defHttp.post({ url: Api.takeDown, params });
|
||||||
|
export const publishApi = (params: any) => defHttp.post({ url: Api.publish, params });
|
||||||
|
export const batchTakeDownApi = (params: any) => defHttp.post({ url: Api.batchTakeDown, params });
|
||||||
|
export const batchPublishApi = (params: any) => defHttp.post({ url: Api.batchPublish, params });
|
||||||
|
export const incrementViewApi = (params: any) => defHttp.post({ url: Api.incrementView, params });
|
||||||
|
export const copyApi = (params: any) => defHttp.post({ url: Api.copy, params });
|
||||||
|
export const versionListApi = (params: any) => defHttp.get({ url: Api.versionList, params });
|
||||||
|
export const versionRollbackApi = (params: any) => defHttp.post({ url: Api.versionRollback, params });
|
||||||
|
export const categoryListAllApi = (params?: any) => defHttp.get({ url: Api.categoryListAll, params });
|
||||||
|
export const tagListAllApi = (params?: any) => defHttp.get({ url: Api.tagListAll, params });
|
||||||
|
|
||||||
|
export const deleteApi = (params: any, handleSuccess: any) => {
|
||||||
|
createConfirm({
|
||||||
|
iconType: 'warning',
|
||||||
|
title: '确认删除',
|
||||||
|
content: '是否将选中资源移入回收站?',
|
||||||
|
okText: '确认',
|
||||||
|
cancelText: '取消',
|
||||||
|
onOk: async () => {
|
||||||
|
await defHttp.post({ url: Api.delete, params });
|
||||||
|
handleSuccess();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const batchDeleteApi = (params: any, handleSuccess: any) => {
|
||||||
|
createConfirm({
|
||||||
|
iconType: 'warning',
|
||||||
|
title: '批量删除',
|
||||||
|
content: `是否将选中的 ${params?.length || 0} 条资源移入回收站?`,
|
||||||
|
okText: '确认',
|
||||||
|
cancelText: '取消',
|
||||||
|
onOk: async () => {
|
||||||
|
await defHttp.post({ url: Api.deleteBatch, params });
|
||||||
|
handleSuccess();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
import { BasicColumn, FormSchema } from '/@/components/Table';
|
||||||
|
|
||||||
|
// 发布状态
|
||||||
|
export const STATUS = {
|
||||||
|
DRAFT: 0,
|
||||||
|
PUBLISHED: 1,
|
||||||
|
OFFLINE: 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 审核状态
|
||||||
|
export const AUDIT_STATUS = {
|
||||||
|
PENDING: 10,
|
||||||
|
PASSED: 11,
|
||||||
|
REJECTED: 12,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 复合状态(搜索用)
|
||||||
|
export const COMBINED_STATUS = {
|
||||||
|
DRAFT: '0',
|
||||||
|
PUBLISHED: '1',
|
||||||
|
OFFLINE: '2',
|
||||||
|
PENDING: '10',
|
||||||
|
PASSED: '11',
|
||||||
|
REJECTED: '12',
|
||||||
|
};
|
||||||
|
|
||||||
|
// 内容类型
|
||||||
|
export const CONTENT_TYPE = {
|
||||||
|
IMAGE_TEXT: { label: '图文', color: 'orange', icon: '📄' },
|
||||||
|
VIDEO: { label: '视频', color: 'purple', icon: '🎬' },
|
||||||
|
AUDIO: { label: '音频', color: 'green', icon: '🎧' },
|
||||||
|
PDF: { label: 'PDF', color: 'red', icon: '📑' },
|
||||||
|
};
|
||||||
|
|
||||||
|
// 根据发布状态+审核状态返回展示信息
|
||||||
|
export function getResourceDisplayStatus(status: number, auditStatus: number | null) {
|
||||||
|
if (status === STATUS.PUBLISHED) return { text: '已发布', color: 'success' };
|
||||||
|
if (status === STATUS.OFFLINE) return { text: '已下架', color: 'error' };
|
||||||
|
if (auditStatus === AUDIT_STATUS.PENDING) return { text: '待审核', color: 'warning' };
|
||||||
|
if (auditStatus === AUDIT_STATUS.REJECTED) return { text: '已驳回', color: 'error' };
|
||||||
|
return { text: '草稿', color: 'default' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 内容类型选项
|
||||||
|
export const TYPE_OPTIONS = [
|
||||||
|
{ label: '图文', value: 'IMAGE_TEXT' },
|
||||||
|
{ label: '视频', value: 'VIDEO' },
|
||||||
|
{ label: '音频', value: 'AUDIO' },
|
||||||
|
{ label: 'PDF', value: 'PDF' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// 复合状态选项
|
||||||
|
export const STATUS_OPTIONS = [
|
||||||
|
{ label: '草稿', value: '0' },
|
||||||
|
{ label: '已发布', value: '1' },
|
||||||
|
{ label: '已下架', value: '2' },
|
||||||
|
{ label: '待审核', value: '10' },
|
||||||
|
{ label: '已通过', value: '11' },
|
||||||
|
{ label: '已驳回', value: '12' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// 适用场景
|
||||||
|
export const SCENE_OPTIONS = [
|
||||||
|
{ label: '油田现场', value: '油田现场' },
|
||||||
|
{ label: '日常生活', value: '日常生活' },
|
||||||
|
{ label: '高原环境', value: '高原环境' },
|
||||||
|
{ label: '高温环境', value: '高温环境' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// 表格列定义
|
||||||
|
export const columns: BasicColumn[] = [
|
||||||
|
{
|
||||||
|
title: '封面',
|
||||||
|
dataIndex: 'coverImage',
|
||||||
|
width: 80,
|
||||||
|
align: 'center',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '标题',
|
||||||
|
dataIndex: 'title',
|
||||||
|
align: 'left',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '分类',
|
||||||
|
dataIndex: 'categoryName',
|
||||||
|
width: 100,
|
||||||
|
align: 'center',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '类型',
|
||||||
|
dataIndex: 'contentType',
|
||||||
|
width: 90,
|
||||||
|
align: 'center',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '标签',
|
||||||
|
dataIndex: 'tagNameList',
|
||||||
|
width: 160,
|
||||||
|
align: 'left',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '状态',
|
||||||
|
dataIndex: 'statusDisplay',
|
||||||
|
width: 100,
|
||||||
|
align: 'center',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '阅读/收藏',
|
||||||
|
dataIndex: 'viewFavorite',
|
||||||
|
width: 110,
|
||||||
|
align: 'center',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '创建时间',
|
||||||
|
dataIndex: 'createTime',
|
||||||
|
width: 170,
|
||||||
|
align: 'center',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// 搜索表单
|
||||||
|
export const searchSchema: FormSchema[] = [
|
||||||
|
{
|
||||||
|
field: 'title',
|
||||||
|
label: '标题',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '搜索标题',
|
||||||
|
},
|
||||||
|
colProps: { span: 6 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'categoryId',
|
||||||
|
label: '分类',
|
||||||
|
component: 'ApiSelect',
|
||||||
|
componentProps: {
|
||||||
|
api: () => import('./resource.api').then((m) => m.categoryListAllApi()),
|
||||||
|
labelField: 'name',
|
||||||
|
valueField: 'id',
|
||||||
|
placeholder: '全部分类',
|
||||||
|
},
|
||||||
|
colProps: { span: 6 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'combinedStatus',
|
||||||
|
label: '状态',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
options: STATUS_OPTIONS,
|
||||||
|
placeholder: '全部状态',
|
||||||
|
allowClear: true,
|
||||||
|
},
|
||||||
|
colProps: { span: 6 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'contentType',
|
||||||
|
label: '类型',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
options: TYPE_OPTIONS,
|
||||||
|
placeholder: '全部类型',
|
||||||
|
allowClear: true,
|
||||||
|
},
|
||||||
|
colProps: { span: 6 },
|
||||||
|
},
|
||||||
|
];
|
||||||
Reference in New Issue
Block a user