feat: sharelink 资源下载管理插件 v1.1.0

- 下载资源管理与 /download/{slug} 下载链接
- 下载统计(次数/去重人数/明细/CSV 导出)
- 按资源邮箱验证(Halo 通知中心发信,互认评论插件已验证邮箱)
- 本地附件防直链(/upload/** 404)
- 文章引用扫描
- Console 前端:Vue3 + ui-plugin-bundler-kit
This commit is contained in:
shirainbown
2026-08-03 00:25:15 +08:00
commit aa4befa0ff
62 changed files with 7838 additions and 0 deletions
+127
View File
@@ -0,0 +1,127 @@
import { axiosInstance } from '@halo-dev/api-client';
import type {
DownloadResource,
DownloadResourceList,
DownloadResourceSpec,
DownloadRecord,
ListResult,
ReferencesMap,
} from '@/types';
export const API_BASE = '/apis/console.api.sharelink.halo.run/v1alpha1';
// 后端资源 VO 为扁平结构,这里统一归一化为 Halo 风格结构供组件使用
interface ResourceVo {
name: string;
slug: string;
displayName: string;
description?: string;
attachmentName?: string;
requireEmailVerify?: boolean;
enabled?: boolean;
downloadUrl?: string;
creationTimestamp?: string;
stats?: {
downloadCount?: number;
downloaderCount?: number;
referenceCount?: number;
};
}
function normalizeResource(vo: ResourceVo): DownloadResource {
return {
metadata: { name: vo.name, creationTimestamp: vo.creationTimestamp },
spec: {
slug: vo.slug,
displayName: vo.displayName,
description: vo.description,
attachmentName: vo.attachmentName,
requireEmailVerify: vo.requireEmailVerify,
enabled: vo.enabled,
},
status: { downloadCount: vo.stats?.downloadCount ?? 0 },
stats: {
downloaderCount: vo.stats?.downloaderCount ?? 0,
referenceCount: vo.stats?.referenceCount ?? 0,
},
};
}
export async function listResources(): Promise<DownloadResourceList> {
const { data } = await axiosInstance.get<ResourceVo[]>(
`${API_BASE}/download-resources`
);
const items = (data ?? []).map(normalizeResource);
return { items, total: items.length };
}
// 后端创建/更新均接收扁平的 spec 字段(ResourceRequest
export async function createResource(
spec: DownloadResourceSpec
): Promise<DownloadResource> {
const { data } = await axiosInstance.post<ResourceVo>(
`${API_BASE}/download-resources`,
spec
);
return normalizeResource(data);
}
export async function getResource(name: string): Promise<DownloadResource> {
const { data } = await axiosInstance.get<ResourceVo>(
`${API_BASE}/download-resources/${name}`
);
return normalizeResource(data);
}
export async function updateResource(
name: string,
spec: DownloadResourceSpec
): Promise<DownloadResource> {
const { data } = await axiosInstance.put<ResourceVo>(
`${API_BASE}/download-resources/${name}`,
spec
);
return normalizeResource(data);
}
export async function deleteResource(name: string): Promise<void> {
await axiosInstance.delete(`${API_BASE}/download-resources/${name}`);
}
export async function listRecords(params: {
resourceSlug?: string;
page?: number;
size?: number;
}): Promise<ListResult<DownloadRecord>> {
const { data } = await axiosInstance.get<ListResult<DownloadRecord>>(
`${API_BASE}/download-records`,
{ params }
);
return data;
}
export async function deleteRecord(name: string): Promise<void> {
await axiosInstance.delete(`${API_BASE}/download-records/${name}`);
}
// CSV 导出走浏览器直接导航(console 端点接受 session cookie
export function recordsExportUrl(resourceSlug?: string): string {
const query = resourceSlug
? `?resourceSlug=${encodeURIComponent(resourceSlug)}`
: '';
return `${API_BASE}/download-records/-/export${query}`;
}
export async function getReferences(): Promise<ReferencesMap> {
const { data } = await axiosInstance.get<ReferencesMap>(
`${API_BASE}/references`
);
return data;
}
export async function refreshReferences(): Promise<ReferencesMap> {
const { data } = await axiosInstance.post<ReferencesMap>(
`${API_BASE}/references/-/refresh`
);
return data;
}