feat(resource): 对接急救宣教资源管理 V3 接口

- 审批与上架分离:已通过状态显示「已通过」+「上架」操作
- 分类扁平化:分类表单去掉父级分类字段
- 标签从分类 tags 字段联动取,不再调独立标签接口
- 标签表单加所属分类必填,支持从分类「+标签」按钮预设锁定
- 审核记录字段对齐后端:resourceTitle/auditRejectReason
- 资源表单 footer 隐藏默认按钮,写操作 API 关全局提示避免重复弹
This commit is contained in:
wanghao
2026-06-25 10:30:22 +08:00
parent c01fd0741d
commit 9c6680146f
8 changed files with 99 additions and 69 deletions
+11 -2
View File
@@ -50,6 +50,7 @@
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'action'">
<a-button type="link" size="small" @click="handlePreview(record)">查看</a-button>
<a-button type="link" size="small" @click="handlePublish(record)">上架</a-button>
</template>
</template>
</a-table>
@@ -123,6 +124,7 @@
approveApi,
rejectApi,
} from './check.api';
import { publishApi } from '/@/views/resource/resource.api';
import dayjs from 'dayjs';
import { CONTENT_TYPE } from '/@/views/resource/resource.data';
import PreviewModal from '/@/views/resource/components/PreviewModal.vue';
@@ -174,11 +176,11 @@
];
const recordColumns = [
{ title: '资源名称', dataIndex: 'title' },
{ title: '资源名称', dataIndex: 'resourceTitle' },
{ title: '操作', dataIndex: 'action', align: 'center', width: 100 },
{ title: '审核人', dataIndex: 'auditBy', align: 'center', width: 120 },
{ title: '时间', dataIndex: 'auditTime', align: 'center', width: 170 },
{ title: '意见', dataIndex: 'auditOpinion' },
{ title: '意见', dataIndex: 'auditRejectReason' },
];
function getTypeLabel(type: string) {
@@ -210,6 +212,13 @@
createMessage.info(`重新编辑:${record.title}(跳转到资源管理编辑)`);
}
// V3:已通过的资源需手动上架
async function handlePublish(record: any) {
await publishApi({ id: record.id });
createMessage.success('已上架');
loadPassed();
}
async function loadPending() {
pendingLoading.value = true;
try {
@@ -4,17 +4,6 @@
<a-form-item label="分类名称" required>
<a-input v-model:value="formState.name" placeholder="请输入分类名称" :maxlength="50" />
</a-form-item>
<a-form-item label="父级分类">
<a-tree-select
v-model:value="formState.parentId"
:tree-data="categoryTreeData"
:field-names="{ label: 'name', value: 'id', children: 'children' }"
placeholder="顶级分类(不选)"
allow-clear
tree-default-expand-all
:disabled="!!formState.id"
/>
</a-form-item>
<a-form-item label="排序号">
<a-input-number v-model:value="formState.sortNo" :min="0" style="width: 100%" />
</a-form-item>
@@ -26,20 +15,19 @@
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue';
import { ref, reactive } from 'vue';
import { BasicModal, useModalInner } from '/@/components/Modal';
import { useMessage } from '/@/hooks/web/useMessage';
import { categoryListAllApi, categoryAddApi, categoryEditApi, buildTree } from '../label.api';
import { categoryAddApi, categoryEditApi } from '../label.api';
const emit = defineEmits(['success', 'register']);
const { createMessage } = useMessage();
const title = ref('新增分类');
const categoryTreeData = ref<any[]>([]);
// V3:分类扁平化,仅一级,不再有 parentId
const formState = reactive({
id: '',
name: '',
parentId: undefined as string | undefined,
sortNo: 1,
description: '',
});
@@ -49,21 +37,10 @@
title.value = isUpdate ? '编辑分类' : '新增分类';
formState.id = data?.record?.id || '';
formState.name = data?.record?.name || '';
formState.parentId = data?.record?.parentId || (data?.parentId || undefined);
formState.sortNo = data?.record?.sortNo ?? 1;
formState.description = data?.record?.description || '';
// 编辑时父级分类禁用(避免循环引用)
if (isUpdate) {
formState.parentId = data?.record?.parentId || undefined;
}
await loadCategoryTree();
});
async function loadCategoryTree() {
const list = (await categoryListAllApi()) || [];
categoryTreeData.value = buildTree(list);
}
function validate(): boolean {
if (!formState.name) {
createMessage.warning('请输入分类名称');
@@ -79,7 +56,6 @@
const params = {
id: formState.id || undefined,
name: formState.name,
parentId: formState.parentId || undefined,
sortNo: formState.sortNo,
description: formState.description,
};
@@ -96,8 +72,4 @@
setModalProps({ confirmLoading: false });
}
}
onMounted(() => {
loadCategoryTree();
});
</script>
+33 -1
View File
@@ -1,6 +1,16 @@
<template>
<BasicModal v-bind="$attrs" @register="registerModal" :title="title" :width="480" :minHeight="260" @ok="handleSubmit">
<a-form layout="vertical">
<a-form-item label="所属分类" required>
<a-select
v-model:value="formState.parentId"
:options="categoryOptions"
:field-names="{ label: 'name', value: 'id' }"
placeholder="请选择所属分类"
:disabled="!!lockParentId"
allow-clear
/>
</a-form-item>
<a-form-item label="标签名称" required>
<a-input v-model:value="formState.name" placeholder="请输入标签名称" :maxlength="20" />
</a-form-item>
@@ -15,18 +25,23 @@
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue';
import { ref, reactive, onMounted } from 'vue';
import { BasicModal, useModalInner } from '/@/components/Modal';
import { useMessage } from '/@/hooks/web/useMessage';
import { tagAddApi, tagEditApi } from '../label.api';
import { categoryListAllApi } from '/@/views/resource/resource.api';
const emit = defineEmits(['success', 'register']);
const { createMessage } = useMessage();
const title = ref('新增标签');
const categoryOptions = ref<any[]>([]);
// 从分类的「+标签」按钮进来时锁定所属分类
const lockParentId = ref('');
const formState = reactive({
id: '',
name: '',
parentId: '' as string,
sortNo: 1,
description: '',
});
@@ -36,11 +51,19 @@
title.value = isUpdate ? '编辑标签' : '新增标签';
formState.id = data?.record?.id || '';
formState.name = data?.record?.name || '';
// V3:支持从分类「+标签」按钮进来时预设 parentId 并锁定
const presetParentId = data?.parentId || data?.record?.parentId || '';
formState.parentId = presetParentId;
lockParentId.value = data?.parentId ? presetParentId : '';
formState.sortNo = data?.record?.sortNo ?? 1;
formState.description = data?.record?.description || '';
});
function validate(): boolean {
if (!formState.parentId) {
createMessage.warning('请选择所属分类');
return false;
}
if (!formState.name) {
createMessage.warning('请输入标签名称');
return false;
@@ -55,6 +78,7 @@
const params = {
id: formState.id || undefined,
name: formState.name,
parentId: formState.parentId,
sortNo: formState.sortNo,
description: formState.description,
};
@@ -71,4 +95,12 @@
setModalProps({ confirmLoading: false });
}
}
onMounted(async () => {
try {
categoryOptions.value = (await categoryListAllApi()) || [];
} catch {
categoryOptions.value = [];
}
});
</script>
+25 -17
View File
@@ -29,7 +29,7 @@
<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" @click.stop="handleAddTagUnderCategory(id)">+标签</a-button>
<a-button type="link" size="small" danger @click.stop="handleDeleteCategory({ id, name })"></a-button>
</span>
</div>
@@ -93,7 +93,7 @@
<!-- 分类新增/编辑弹窗 -->
<CategoryFormModal @register="registerCategoryModal" @success="loadCategoryTree" />
<!-- 标签新增/编辑弹窗 -->
<TagFormModal @register="registerTagModal" @success="loadTags" />
<TagFormModal @register="registerTagModal" @success="loadCategoryTree" />
</div>
</template>
@@ -105,7 +105,6 @@
import {
categoryListAllApi,
categoryDeleteApi,
tagListAllApi,
tagDeleteApi,
buildTree,
} from './label.api';
@@ -144,9 +143,13 @@
const categoryTreeSelectData = computed(() => categoryTree.value);
function onSelectCategory(keys: string[], info: any) {
function onSelectCategory(keys: string[]) {
// V3:标签数据从分类的 tags 字段取,不再单独调接口
if (keys.length > 0) {
createMessage.info(`已选中分类:${info.node.name}`);
const category = categoryList.value.find((c) => c.id === keys[0]);
tagList.value = category?.tags || [];
} else {
tagList.value = [];
}
}
@@ -155,9 +158,15 @@
}
function handleAddSubCategory(parentId: string) {
// V3:分类扁平化,此函数已废弃,保留以防其他地方调用
openCategoryModal(true, { isUpdate: false, parentId });
}
// V3:分类项后面的「+标签」按钮,新增该分类下的标签
function handleAddTagUnderCategory(parentId: string) {
openTagModal(true, { isUpdate: false, parentId });
}
function handleEditCategory(record: any) {
openCategoryModal(true, { isUpdate: true, record });
}
@@ -195,7 +204,7 @@
function handleDeleteTag(tag: any) {
tagDeleteApi({ id: tag.id }, () => {
createMessage.success('已删除标签');
loadTags();
loadCategoryTree();
});
}
@@ -255,24 +264,23 @@
if (expandedKeys.value.length === 0 && categoryTree.value.length > 0) {
expandedKeys.value = [categoryTree.value[0].id];
}
// V3:默认选中第一个分类,标签从分类的 tags 字段取
if (selectedKeys.value.length === 0 && categoryList.value.length > 0) {
const first = categoryList.value[0];
selectedKeys.value = [first.id];
tagList.value = first?.tags || [];
} else if (selectedKeys.value.length > 0) {
// 已选中分类,重新拉数据后刷新该分类的标签(新增/编辑/删除标签后回显最新数据)
const selected = categoryList.value.find((c) => c.id === selectedKeys.value[0]);
tagList.value = selected?.tags || [];
}
} 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>
+8 -8
View File
@@ -30,8 +30,8 @@ export const categoryListAllApi = (params?: any) => defHttp.get({ url: Api.categ
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 categoryAddApi = (params: any) => defHttp.post({ url: Api.categoryAdd, params }, { successNeedMessage: false });
export const categoryEditApi = (params: any) => defHttp.post({ url: Api.categoryEdit, params }, { successNeedMessage: false });
export const categoryDeleteApi = (params: any, handleSuccess: any) => {
createConfirm({
@@ -39,7 +39,7 @@ export const categoryDeleteApi = (params: any, handleSuccess: any) => {
title: '确认删除',
content: '是否删除该分类?',
onOk: async () => {
await defHttp.post({ url: Api.categoryDelete, params });
await defHttp.post({ url: Api.categoryDelete, params }, { successNeedMessage: false });
handleSuccess();
},
});
@@ -51,7 +51,7 @@ export const categoryDeleteBatchApi = (params: any, handleSuccess: any) => {
title: '批量删除',
content: `是否删除选中的 ${params?.length || 0} 个分类?`,
onOk: async () => {
await defHttp.post({ url: Api.categoryDeleteBatch, params });
await defHttp.post({ url: Api.categoryDeleteBatch, params }, { successNeedMessage: false });
handleSuccess();
},
});
@@ -61,8 +61,8 @@ export const categoryDeleteBatchApi = (params: any, handleSuccess: any) => {
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 tagAddApi = (params: any) => defHttp.post({ url: Api.tagAdd, params }, { successNeedMessage: false });
export const tagEditApi = (params: any) => defHttp.post({ url: Api.tagEdit, params }, { successNeedMessage: false });
export const tagDeleteApi = (params: any, handleSuccess: any) => {
createConfirm({
@@ -70,7 +70,7 @@ export const tagDeleteApi = (params: any, handleSuccess: any) => {
title: '确认删除',
content: '是否删除该标签?',
onOk: async () => {
await defHttp.post({ url: Api.tagDelete, params });
await defHttp.post({ url: Api.tagDelete, params }, { successNeedMessage: false });
handleSuccess();
},
});
@@ -82,7 +82,7 @@ export const tagDeleteBatchApi = (params: any, handleSuccess: any) => {
title: '批量删除',
content: `是否删除选中的 ${params?.length || 0} 个标签?`,
onOk: async () => {
await defHttp.post({ url: Api.tagDeleteBatch, params });
await defHttp.post({ url: Api.tagDeleteBatch, params }, { successNeedMessage: false });
handleSuccess();
},
});
@@ -1,5 +1,5 @@
<template>
<BasicModal v-bind="$attrs" @register="registerModal" :title="title" :width="900" :minHeight="500" :useDefaultFooter="false" @ok="handleSubmit">
<BasicModal v-bind="$attrs" @register="registerModal" :title="title" :width="900" :minHeight="500" :useDefaultFooter="false" :showCancelBtn="false" :showOkBtn="false">
<div class="resource-form">
<a-form layout="vertical" :model="formState">
<a-row :gutter="16">
@@ -33,9 +33,10 @@
<a-select
v-model:value="formState.tagIdList"
mode="multiple"
:options="tagOptions"
placeholder="请选择标签"
:options="currentCategoryTags"
placeholder="请选择分类"
:field-names="{ label: 'name', value: 'id' }"
:disabled="!formState.categoryId"
allow-clear
/>
</a-form-item>
@@ -88,11 +89,11 @@
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue';
import { ref, reactive, computed, 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';
import { addApi, editApi, submitForReviewApi, categoryListAllApi } from '../resource.api';
import JImageUpload from '/@/components/Form/src/jeecg/components/JImageUpload.vue';
import JUpload from '/@/components/Form/src/jeecg/components/JUpload/JUpload.vue';
import JEditor from '/@/components/Form/src/jeecg/components/JEditor.vue';
@@ -101,10 +102,16 @@
const { createMessage } = useMessage();
const title = ref('新增急救宣教资源');
const categoryOptions = ref<any[]>([]);
const tagOptions = ref<any[]>([]);
// 附件列表(JUpload 用 array 绑定,提交时 JSON.stringify 为后端 fileUrl 字段)
const fileList = ref<any[]>([]);
// V3:标签从分类的 tags 字段取,选择分类后联动
const currentCategoryTags = computed(() => {
if (!formState.categoryId) return [];
const category = categoryOptions.value.find((c) => c.id === formState.categoryId);
return category?.tags || [];
});
const formState = reactive({
id: '',
title: '',
@@ -233,8 +240,8 @@
}
onMounted(async () => {
// V3:标签从分类的 tags 字段取,不再单独调接口
categoryOptions.value = (await categoryListAllApi()) || [];
tagOptions.value = (await tagListAllApi()) || [];
});
</script>
+3 -3
View File
@@ -200,10 +200,10 @@
},
});
} else if (record.status === STATUS.DRAFT && record.auditStatus === AUDIT_STATUS.PASSED) {
// 已通过的草稿状态(编辑后自动退回草稿),可重新提交
// V3:已通过待上架状态,操作为「上架」
actions.push({
label: '提交审核',
onClick: () => handleSubmitReview(record),
label: '上架',
onClick: () => handlePublish(record),
});
}
return actions;
+2
View File
@@ -33,10 +33,12 @@ export const CONTENT_TYPE = {
};
// 根据发布状态+审核状态返回展示信息
// V3:审核通过但未上架 (auditStatus=11 && status=0) 显示为「已通过」
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.PASSED) return { text: '已通过', color: 'processing' };
if (auditStatus === AUDIT_STATUS.REJECTED) return { text: '已驳回', color: 'error' };
return { text: '草稿', color: 'default' };
}