feat: add image management functionality

- Implemented image folder repository with CRUD operations.
- Added image reference repository for managing image references to articles.
- Created image repository for handling image assets, including listing, inserting, updating, and deleting images.
- Introduced image usage repository to track storage usage and quotas for tenants.
- Added SQL queries for image assets, folders, references, and usage.
- Developed image handler for HTTP endpoints to manage images and folders.
- Created database migration scripts for image-related tables and structures.
This commit is contained in:
2026-04-16 20:40:41 +08:00
parent 0a3558fc51
commit 27389164b0
57 changed files with 8063 additions and 153 deletions
+29 -2
View File
@@ -36,8 +36,10 @@ const publishPlatforms = ref<string[]>([]);
const initialPublishPlatforms = ref<string[]>([]);
const coverEnabled = ref(false);
const coverAssetUrl = ref("");
const coverImageAssetId = ref<number | null>(null);
const coverFileName = ref("");
const initialCoverAssetUrl = ref("");
const initialCoverImageAssetId = ref<number | null>(null);
const coverPickerOpen = ref(false);
const leaveModalOpen = ref(false);
const publishModalOpen = ref(false);
@@ -83,8 +85,10 @@ watch(
publishPlatforms.value = normalizePublishPlatformIds(detail.platforms ?? []);
initialPublishPlatforms.value = [...publishPlatforms.value];
coverAssetUrl.value = resolveApiURL(detail.cover_asset_url);
coverImageAssetId.value = detail.cover_image_asset_id ?? null;
coverFileName.value = deriveCoverFileName(coverAssetUrl.value);
initialCoverAssetUrl.value = coverAssetUrl.value;
initialCoverImageAssetId.value = coverImageAssetId.value;
coverEnabled.value = Boolean(coverAssetUrl.value);
},
{ immediate: true },
@@ -99,12 +103,14 @@ const normalizedCoverValue = computed(() => {
}
return coverAssetUrl.value.trim();
});
const referencedImageAssetIds = computed(() => extractReferencedImageAssetIds(markdown.value));
const hasChanges = computed(
() =>
title.value.trim() !== initialTitle.value.trim() ||
markdown.value !== initialMarkdown.value ||
serializePlatformSelection(publishPlatforms.value) !== serializePlatformSelection(initialPublishPlatforms.value) ||
normalizedCoverValue.value !== initialCoverAssetUrl.value.trim(),
normalizedCoverValue.value !== initialCoverAssetUrl.value.trim() ||
coverImageAssetId.value !== initialCoverImageAssetId.value,
);
watch(
@@ -124,6 +130,8 @@ const saveMutation = useMutation({
markdown_content: markdown.value,
platforms: normalizePublishPlatformIds(publishPlatforms.value),
cover_asset_url: normalizedCoverValue.value,
referenced_image_asset_ids: referencedImageAssetIds.value,
cover_image_asset_id: coverImageAssetId.value,
}),
onSuccess: async (data) => {
message.success(t("article.editor.messages.saved"));
@@ -134,8 +142,10 @@ const saveMutation = useMutation({
publishPlatforms.value = normalizePublishPlatformIds(data.platforms ?? []);
initialPublishPlatforms.value = [...publishPlatforms.value];
coverAssetUrl.value = resolveApiURL(data.cover_asset_url);
coverImageAssetId.value = data.cover_image_asset_id ?? null;
coverFileName.value = deriveCoverFileName(coverAssetUrl.value);
initialCoverAssetUrl.value = coverAssetUrl.value;
initialCoverImageAssetId.value = coverImageAssetId.value;
coverEnabled.value = coverRequired.value || Boolean(coverAssetUrl.value);
await Promise.all([
queryClient.invalidateQueries({ queryKey: ["articles"] }),
@@ -199,14 +209,16 @@ async function uploadEditorImage(file: File): Promise<string> {
return result.url;
}
function handleCoverPicked(payload: { url: string; fileName: string }): void {
function handleCoverPicked(payload: { url: string; fileName: string; assetId?: number | null }): void {
coverAssetUrl.value = payload.url;
coverImageAssetId.value = payload.assetId ?? null;
coverFileName.value = payload.fileName;
coverEnabled.value = true;
}
function handleRemoveCover(): void {
coverAssetUrl.value = "";
coverImageAssetId.value = null;
coverFileName.value = "";
}
@@ -314,6 +326,20 @@ function stripLeadingTitleHeading(titleValue: string, markdownValue: string): st
return normalizedMarkdown.slice(match[0].length).replace(/^\s*\r?\n/, "");
}
function extractReferencedImageAssetIds(markdownValue: string): number[] {
const ids = new Set<number>();
const matcher = /data-asset-id=(?:"|')(\d+)(?:"|')/g;
for (const match of markdownValue.matchAll(matcher)) {
const id = Number.parseInt(match[1] ?? "", 10);
if (Number.isInteger(id) && id > 0) {
ids.add(id);
}
}
return [...ids];
}
function serializePlatformSelection(platformIds: string[]): string {
return normalizePublishPlatformIds(platformIds)
.sort()
@@ -467,6 +493,7 @@ function serializePlatformSelection(platformIds: string[]): string {
:platform-ids="publishPlatforms"
:current-url="coverAssetUrl"
:current-file-name="coverFileName"
:current-asset-id="coverImageAssetId"
@confirmed="handleCoverPicked"
/>
</div>