feat(B2): 附件管理改"未使用"语义+用途列;banner/link 上传带 scene

- AttachmentManage: orphanCount→unusedCount(零残留),用途列(文章/轮播/友链/站点配置/未使用)
- BannerManage/LinkManage 上传 action 传 scene;ArticleList 删除提示文案同步
- types AttachmentVo 增 refType;vitest 24/24+build 绿
This commit is contained in:
chenxing
2026-09-08 15:14:07 +08:00
parent 3990811ed5
commit bcd639cfc2
6 changed files with 24 additions and 20 deletions
+1 -1
View File
@@ -166,7 +166,7 @@ class ArticleApi {
class AttachmentApi { class AttachmentApi {
page(siteId: number, orphanOnly: boolean, page: number, pageSize: number) { page(siteId: number, orphanOnly: boolean, page: number, pageSize: number) {
return get<PageResult<AttachmentVo> & { orphanCount: number }>(`${ATTACHMENT}/page`, { return get<PageResult<AttachmentVo> & { unusedCount: number }>(`${ATTACHMENT}/page`, {
siteId, siteId,
orphanOnly, orphanOnly,
page, page,
+3
View File
@@ -173,11 +173,14 @@ export interface AttachmentVo {
id: number; id: number;
siteId: number; siteId: number;
articleId?: number; articleId?: number;
/** 场景引用类型(B2):banner/link/site;空=无场景引用 */
refType?: string;
fileName: string; fileName: string;
url: string; url: string;
fileSize?: number; fileSize?: number;
mimeType?: string; mimeType?: string;
createTime?: string; createTime?: string;
/** 未使用(B2 语义:无文章引用且无场景引用) */
orphan: boolean; orphan: boolean;
} }
+1 -1
View File
@@ -174,7 +174,7 @@ function doDelete(row: ArticlePageItem) {
MyConfirm({ MyConfirm({
title: "删除文章", title: "删除文章",
msg: `确定删除「${row.title}」吗?`, msg: `确定删除「${row.title}」吗?`,
info: "删除后正文图片将转为孤儿附件,可在附件管理中清理", info: "删除后正文图片将转为未使用附件,可在附件管理中清理",
async onOk() { async onOk() {
await articleApi.delete({ id: row.id, siteId: store.siteId! }).send(); await articleApi.delete({ id: row.id, siteId: store.siteId! }).send();
Message.success("操作成功!"); Message.success("操作成功!");
+17 -16
View File
@@ -11,7 +11,7 @@ const store = useCmsStore();
const rows = ref<AttachmentVo[]>([]); const rows = ref<AttachmentVo[]>([]);
const total = ref(0); const total = ref(0);
const orphanCount = ref(0); const unusedCount = ref(0);
const loading = ref(false); const loading = ref(false);
const pageNum = ref(1); const pageNum = ref(1);
const pageSize = ref(parseInt(localStorage.getItem("customPageCount") || "10")); const pageSize = ref(parseInt(localStorage.getItem("customPageCount") || "10"));
@@ -32,16 +32,17 @@ const columns = [
}, },
{ title: "大小", key: "fileSize", width: 100, render: (h: any, p: any) => h("span", Utils.getFileSize(p.row.fileSize || 0)) }, { title: "大小", key: "fileSize", width: 100, render: (h: any, p: any) => h("span", Utils.getFileSize(p.row.fileSize || 0)) },
{ {
title: "关联文章", title: "用途",
key: "articleId", key: "usage",
width: 100, width: 110,
align: "center", align: "center",
render: (h: any, p: any) => render: (h: any, p: any) => {
h( // B2 引用模型:文章(articleId)优先,其次场景引用(banner/link/site),均无=未使用
Tag, const usage = p.row.articleId
{ color: p.row.orphan ? "warning" : "default" }, ? "文章"
() => (p.row.orphan ? "孤儿" : p.row.articleId) : { banner: "轮播", link: "友链", site: "站点配置" }[p.row.refType as string] || "未使用";
), return h(Tag, { color: usage === "未使用" ? "warning" : "default" }, () => usage);
},
}, },
{ title: "上传时间", key: "createTime", width: 170 }, { title: "上传时间", key: "createTime", width: 170 },
{ {
@@ -68,7 +69,7 @@ async function run() {
.send(); .send();
rows.value = page?.list || []; rows.value = page?.list || [];
total.value = page?.total || 0; total.value = page?.total || 0;
orphanCount.value = (page as any)?.orphanCount || 0; unusedCount.value = (page as any)?.unusedCount || 0;
} finally { } finally {
loading.value = false; loading.value = false;
} }
@@ -95,9 +96,9 @@ function doDelete(row: AttachmentVo) {
function cleanOrphans() { function cleanOrphans() {
MyConfirm({ MyConfirm({
title: "清理孤儿附件", title: "查看未使用附件",
msg: `当前共有 ${orphanCount.value} 条孤儿附件记录(关联文章已删除),仅清理列表页中勾选的记录。`, msg: `当前共有 ${unusedCount.value} 条未使用附件记录(无文章引用且无场景引用),点击确定切换到未使用列表。`,
info: "为避免误删被封面/轮播引用的文件,MVP 不提供一键全清", info: "为避免误删被引用的文件,MVP 不提供一键全清;旧数据在重新保存对应内容前显示为未使用属正常现象",
onOk() { onOk() {
filter.orphanOnly = true; filter.orphanOnly = true;
pageNum.value = 1; pageNum.value = 1;
@@ -140,11 +141,11 @@ watch(() => store.siteId, () => {
<Button type="primary" size="large" icon="ios-cloud-upload-outline">上传附件</Button> <Button type="primary" size="large" icon="ios-cloud-upload-outline">上传附件</Button>
</Upload> </Upload>
<Button class="fl" style="margin-left: 8px" size="large" @click="cleanOrphans"> <Button class="fl" style="margin-left: 8px" size="large" @click="cleanOrphans">
孤儿附件({{ orphanCount }}) 未使用附件({{ unusedCount }})
</Button> </Button>
</div> </div>
<div class="search-wrap"> <div class="search-wrap">
<Checkbox v-model="filter.orphanOnly" @on-change="pageNum = 1; run()">仅看孤儿附件</Checkbox> <Checkbox v-model="filter.orphanOnly" @on-change="pageNum = 1; run()">仅看未使用附件</Checkbox>
</div> </div>
<Table <Table
:columns="columns" :columns="columns"
+1 -1
View File
@@ -207,7 +207,7 @@ watch(() => store.siteId, () => run());
</FormItem> </FormItem>
<FormItem label="轮播图片" required> <FormItem label="轮播图片" required>
<Upload <Upload
:action="uploadUrl() + '?siteId=' + store.siteId" :action="uploadUrl() + '?siteId=' + store.siteId + '&scene=banner'"
with-credentials with-credentials
:show-upload-list="false" :show-upload-list="false"
:format="['jpg', 'jpeg', 'png', 'gif', 'webp']" :format="['jpg', 'jpeg', 'png', 'gif', 'webp']"
+1 -1
View File
@@ -198,7 +198,7 @@ watch(() => store.siteId, () => run());
</FormItem> </FormItem>
<FormItem label="Logo(可选)"> <FormItem label="Logo(可选)">
<Upload <Upload
:action="uploadUrl() + '?siteId=' + store.siteId" :action="uploadUrl() + '?siteId=' + store.siteId + '&scene=link'"
with-credentials with-credentials
:show-upload-list="false" :show-upload-list="false"
:format="['jpg', 'jpeg', 'png', 'gif', 'webp']" :format="['jpg', 'jpeg', 'png', 'gif', 'webp']"