chore(frontend): introduce prettier + eslint and prune unused code
Backend CI / Backend (push) Has been cancelled
Frontend CI / Frontend (push) Failing after 1m39s

- Add Prettier 3 with prettier-plugin-organize-imports (sorts/removes unused imports)
- Add ESLint 10 flat config with typescript-eslint + eslint-plugin-vue + eslint-config-prettier
- Add root scripts: format, format:check, lint, lint:fix
- Reformat 257 files across admin-web, ops-web, desktop-client, packages
- Remove unused locals/exports flagged by --noUnusedLocals/--noUnusedParameters
- Fix duplicate localTabLabel key, surrogate-pair regex u-flag, NBSP literal in regex
- Skip server/ (Go) and apps/browser-extension/ (deprecated per ADR)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-01 20:39:09 +08:00
parent 21c7892ee4
commit 162abdc97c
258 changed files with 42261 additions and 37556 deletions
+300 -276
View File
@@ -1,104 +1,114 @@
<script setup lang="ts">
import { CloseOutlined, CloudUploadOutlined, PlusOutlined } from "@ant-design/icons-vue";
import { message } from "ant-design-vue";
import { computed, onBeforeUnmount, ref, watch } from "vue";
import { useI18n } from "vue-i18n";
import { CloseOutlined, CloudUploadOutlined, PlusOutlined } from '@ant-design/icons-vue'
import { message } from 'ant-design-vue'
import { computed, onBeforeUnmount, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import ImageLibraryPicker from "@/components/ImageLibraryPicker.vue";
import { articlesApi } from "@/lib/api";
import ImageLibraryPicker from '@/components/ImageLibraryPicker.vue'
import { articlesApi } from '@/lib/api'
import {
deriveCoverFileName,
getPrimaryCoverRequirement,
type PlatformCoverRequirement,
} from "@/lib/cover-requirements";
import { formatError } from "@/lib/errors";
import { IMAGE_UPLOAD_ACCEPT, useImageUploadProgress, validateImageUploadFile } from "@/lib/image-webp";
import type { ImageAssetItem } from "@geo/shared-types";
} from '@/lib/cover-requirements'
import { formatError } from '@/lib/errors'
import {
IMAGE_UPLOAD_ACCEPT,
useImageUploadProgress,
validateImageUploadFile,
} from '@/lib/image-webp'
import type { ImageAssetItem } from '@geo/shared-types'
type PickerUploadResult = {
url: string;
fileName?: string | null;
assetId?: number | null;
};
url: string
fileName?: string | null
assetId?: number | null
}
const props = defineProps<{
open: boolean;
articleId: number | null;
platformIds: string[];
currentUrl?: string | null;
currentFileName?: string | null;
currentAssetId?: number | null;
title?: string;
localTabLabel?: string;
emptyUploadTitle?: string;
emptyUploadHint?: string;
uploadHandler?: (file: File) => Promise<PickerUploadResult>;
}>();
open: boolean
articleId: number | null
platformIds: string[]
currentUrl?: string | null
currentFileName?: string | null
currentAssetId?: number | null
title?: string
localTabLabel?: string
emptyUploadTitle?: string
emptyUploadHint?: string
uploadHandler?: (file: File) => Promise<PickerUploadResult>
}>()
const emit = defineEmits<{
"update:open": [value: boolean];
confirmed: [payload: { url: string; fileName: string; assetId?: number | null }];
}>();
'update:open': [value: boolean]
confirmed: [payload: { url: string; fileName: string; assetId?: number | null }]
}>()
const { t } = useI18n();
const imageUploadProgress = useImageUploadProgress();
const { t } = useI18n()
const imageUploadProgress = useImageUploadProgress()
const fileInput = ref<HTMLInputElement | null>(null);
const activeTab = ref("local");
const workingUrl = ref("");
const workingFileName = ref("");
const sourceKind = ref<"remote" | "local" | null>(null);
const naturalWidth = ref(0);
const naturalHeight = ref(0);
const zoom = ref(1);
const offsetX = ref(0);
const offsetY = ref(0);
const confirmLoading = ref(false);
const transformDirty = ref(false);
const localObjectUrl = ref("");
const localFile = ref<File | null>(null);
const selectedLibraryImageId = ref<number | null>(null);
const fileInput = ref<HTMLInputElement | null>(null)
const activeTab = ref('local')
const workingUrl = ref('')
const workingFileName = ref('')
const sourceKind = ref<'remote' | 'local' | null>(null)
const naturalWidth = ref(0)
const naturalHeight = ref(0)
const zoom = ref(1)
const offsetX = ref(0)
const offsetY = ref(0)
const confirmLoading = ref(false)
const transformDirty = ref(false)
const localObjectUrl = ref('')
const localFile = ref<File | null>(null)
const selectedLibraryImageId = ref<number | null>(null)
const dragging = ref(false);
const dragStartX = ref(0);
const dragStartY = ref(0);
const dragOriginX = ref(0);
const dragOriginY = ref(0);
const imageLoadToken = ref(0);
const dragging = ref(false)
const dragStartX = ref(0)
const dragStartY = ref(0)
const dragOriginX = ref(0)
const dragOriginY = ref(0)
const imageLoadToken = ref(0)
const primaryRequirement = computed(() => getPrimaryCoverRequirement(props.platformIds));
const aspectRatioLocked = computed(() => Boolean(primaryRequirement.value.aspectRatio));
const modalTitle = computed(() => props.title?.trim() || t("coverPicker.title"));
const resolvedLocalTabLabel = computed(() => props.localTabLabel?.trim() || t("coverPicker.tabs.local"));
const resolvedEmptyUploadTitle = computed(() => props.emptyUploadTitle?.trim() || t("coverPicker.dropzoneTitle"));
const resolvedEmptyUploadHint = computed(() => props.emptyUploadHint?.trim() || t("coverPicker.dropzoneHint"));
const localTabLabel = computed(() =>
const primaryRequirement = computed(() => getPrimaryCoverRequirement(props.platformIds))
const aspectRatioLocked = computed(() => Boolean(primaryRequirement.value.aspectRatio))
const modalTitle = computed(() => props.title?.trim() || t('coverPicker.title'))
const resolvedLocalTabLabel = computed(
() => props.localTabLabel?.trim() || t('coverPicker.tabs.local'),
)
const resolvedEmptyUploadTitle = computed(
() => props.emptyUploadTitle?.trim() || t('coverPicker.dropzoneTitle'),
)
const resolvedEmptyUploadHint = computed(
() => props.emptyUploadHint?.trim() || t('coverPicker.dropzoneHint'),
)
const localTabLabelDisplay = computed(() =>
workingUrl.value ? `${resolvedLocalTabLabel.value} (1)` : resolvedLocalTabLabel.value,
);
const hasImage = computed(() => Boolean(workingUrl.value));
const previewHint = computed(() =>
aspectRatioLocked.value ? t("coverPicker.previewHintLocked") : t("coverPicker.previewHintFree"),
);
const imageUploadBusy = computed(() => imageUploadProgress.visible);
)
const hasImage = computed(() => Boolean(workingUrl.value))
const imageUploadBusy = computed(() => imageUploadProgress.visible)
const stageSize = computed(() => {
const ratio = primaryRequirement.value.aspectRatio
|| (naturalWidth.value > 0 && naturalHeight.value > 0 ? naturalWidth.value / naturalHeight.value : 4 / 3);
const maxWidth = 720;
const maxHeight = 420;
const ratio =
primaryRequirement.value.aspectRatio ||
(naturalWidth.value > 0 && naturalHeight.value > 0
? naturalWidth.value / naturalHeight.value
: 4 / 3)
const maxWidth = 720
const maxHeight = 420
let width = maxWidth;
let height = width / ratio;
let width = maxWidth
let height = width / ratio
if (height > maxHeight) {
height = maxHeight;
width = height * ratio;
height = maxHeight
width = height * ratio
}
return {
width: Math.round(width),
height: Math.round(height),
};
});
}
})
const displayMetrics = computed(() => {
if (!naturalWidth.value || !naturalHeight.value) {
@@ -106,415 +116,430 @@ const displayMetrics = computed(() => {
width: 0,
height: 0,
baseScale: 1,
};
}
}
const baseScale = Math.max(
stageSize.value.width / naturalWidth.value,
stageSize.value.height / naturalHeight.value,
);
const scaled = baseScale * zoom.value;
)
const scaled = baseScale * zoom.value
return {
width: naturalWidth.value * scaled,
height: naturalHeight.value * scaled,
baseScale,
};
});
}
})
const stageImageStyle = computed(() => ({
width: `${displayMetrics.value.width}px`,
height: `${displayMetrics.value.height}px`,
transform: `translate(calc(-50% + ${offsetX.value}px), calc(-50% + ${offsetY.value}px))`,
}));
}))
const stageFrameStyle = computed(() => ({
width: `${stageSize.value.width}px`,
height: `${stageSize.value.height}px`,
}));
}))
watch(
() => props.open,
(open) => {
if (open) {
hydrateFromProps();
return;
hydrateFromProps()
return
}
stopDragging();
stopDragging()
},
{ immediate: true },
);
)
watch(
() => [props.currentUrl, props.currentFileName],
() => {
if (props.open && sourceKind.value !== "local") {
hydrateFromProps();
if (props.open && sourceKind.value !== 'local') {
hydrateFromProps()
}
},
);
)
watch(
() => props.platformIds.join(","),
() => props.platformIds.join(','),
() => {
if (!props.open || !hasImage.value) {
return;
return
}
resetTransform();
resetTransform()
},
);
)
watch(
() => [zoom.value, naturalWidth.value, naturalHeight.value, stageSize.value.width, stageSize.value.height],
() => [
zoom.value,
naturalWidth.value,
naturalHeight.value,
stageSize.value.width,
stageSize.value.height,
],
() => {
clampOffsets();
clampOffsets()
},
);
)
onBeforeUnmount(() => {
stopDragging();
revokeLocalObjectUrl();
});
stopDragging()
revokeLocalObjectUrl()
})
function hydrateFromProps(): void {
activeTab.value = "local";
transformDirty.value = false;
const nextUrl = String(props.currentUrl ?? "").trim();
const nextFileName = String(props.currentFileName ?? "").trim() || deriveCoverFileName(nextUrl);
activeTab.value = 'local'
transformDirty.value = false
const nextUrl = String(props.currentUrl ?? '').trim()
const nextFileName = String(props.currentFileName ?? '').trim() || deriveCoverFileName(nextUrl)
workingUrl.value = nextUrl;
workingFileName.value = nextFileName;
sourceKind.value = nextUrl ? "remote" : null;
localFile.value = null;
selectedLibraryImageId.value = props.currentAssetId ?? null;
workingUrl.value = nextUrl
workingFileName.value = nextFileName
sourceKind.value = nextUrl ? 'remote' : null
localFile.value = null
selectedLibraryImageId.value = props.currentAssetId ?? null
if (!nextUrl) {
resetStage();
return;
resetStage()
return
}
void loadImageMeta(nextUrl);
void loadImageMeta(nextUrl)
}
function resetStage(): void {
naturalWidth.value = 0;
naturalHeight.value = 0;
resetTransform();
naturalWidth.value = 0
naturalHeight.value = 0
resetTransform()
}
function resetTransform(): void {
zoom.value = 1;
offsetX.value = 0;
offsetY.value = 0;
transformDirty.value = false;
zoom.value = 1
offsetX.value = 0
offsetY.value = 0
transformDirty.value = false
}
function clampOffsets(): void {
const maxX = Math.max(0, (displayMetrics.value.width - stageSize.value.width) / 2);
const maxY = Math.max(0, (displayMetrics.value.height - stageSize.value.height) / 2);
const maxX = Math.max(0, (displayMetrics.value.width - stageSize.value.width) / 2)
const maxY = Math.max(0, (displayMetrics.value.height - stageSize.value.height) / 2)
offsetX.value = clamp(offsetX.value, -maxX, maxX);
offsetY.value = clamp(offsetY.value, -maxY, maxY);
offsetX.value = clamp(offsetX.value, -maxX, maxX)
offsetY.value = clamp(offsetY.value, -maxY, maxY)
}
function markTransformDirty(): void {
transformDirty.value = zoom.value !== 1 || offsetX.value !== 0 || offsetY.value !== 0;
transformDirty.value = zoom.value !== 1 || offsetX.value !== 0 || offsetY.value !== 0
}
function clamp(value: number, min: number, max: number): number {
return Math.min(Math.max(value, min), max);
return Math.min(Math.max(value, min), max)
}
async function loadImageMeta(url: string): Promise<void> {
const token = imageLoadToken.value + 1;
imageLoadToken.value = token;
const token = imageLoadToken.value + 1
imageLoadToken.value = token
try {
const image = await loadImageElement(url);
const image = await loadImageElement(url)
if (token !== imageLoadToken.value) {
return;
return
}
naturalWidth.value = image.naturalWidth;
naturalHeight.value = image.naturalHeight;
resetTransform();
naturalWidth.value = image.naturalWidth
naturalHeight.value = image.naturalHeight
resetTransform()
} catch {
if (token !== imageLoadToken.value) {
return;
return
}
resetStage();
message.warning(t("coverPicker.messages.imageLoadFailed"));
resetStage()
message.warning(t('coverPicker.messages.imageLoadFailed'))
}
}
function openFileDialog(): void {
if (imageUploadBusy.value) {
return;
return
}
fileInput.value?.click();
fileInput.value?.click()
}
function handleFileChange(event: Event): void {
if (imageUploadBusy.value) {
(event.target as HTMLInputElement).value = "";
return;
;(event.target as HTMLInputElement).value = ''
return
}
const input = event.target as HTMLInputElement;
const file = input.files?.[0];
input.value = "";
const input = event.target as HTMLInputElement
const file = input.files?.[0]
input.value = ''
if (!file) {
return;
return
}
try {
validateImageUploadFile(file);
validateImageUploadFile(file)
} catch (error) {
message.warning(formatError(error));
return;
message.warning(formatError(error))
return
}
revokeLocalObjectUrl();
const objectUrl = URL.createObjectURL(file);
localObjectUrl.value = objectUrl;
workingUrl.value = objectUrl;
workingFileName.value = file.name;
sourceKind.value = "local";
localFile.value = file;
selectedLibraryImageId.value = null;
transformDirty.value = false;
void loadImageMeta(objectUrl);
revokeLocalObjectUrl()
const objectUrl = URL.createObjectURL(file)
localObjectUrl.value = objectUrl
workingUrl.value = objectUrl
workingFileName.value = file.name
sourceKind.value = 'local'
localFile.value = file
selectedLibraryImageId.value = null
transformDirty.value = false
void loadImageMeta(objectUrl)
}
function revokeLocalObjectUrl(): void {
if (!localObjectUrl.value) {
return;
return
}
URL.revokeObjectURL(localObjectUrl.value);
localObjectUrl.value = "";
URL.revokeObjectURL(localObjectUrl.value)
localObjectUrl.value = ''
}
function handleStageMouseDown(event: MouseEvent): void {
if (!hasImage.value || confirmLoading.value) {
return;
return
}
dragging.value = true;
dragStartX.value = event.clientX;
dragStartY.value = event.clientY;
dragOriginX.value = offsetX.value;
dragOriginY.value = offsetY.value;
window.addEventListener("mousemove", handleWindowMouseMove);
window.addEventListener("mouseup", handleWindowMouseUp);
dragging.value = true
dragStartX.value = event.clientX
dragStartY.value = event.clientY
dragOriginX.value = offsetX.value
dragOriginY.value = offsetY.value
window.addEventListener('mousemove', handleWindowMouseMove)
window.addEventListener('mouseup', handleWindowMouseUp)
}
function handleWindowMouseMove(event: MouseEvent): void {
if (!dragging.value) {
return;
return
}
offsetX.value = dragOriginX.value + event.clientX - dragStartX.value;
offsetY.value = dragOriginY.value + event.clientY - dragStartY.value;
clampOffsets();
markTransformDirty();
offsetX.value = dragOriginX.value + event.clientX - dragStartX.value
offsetY.value = dragOriginY.value + event.clientY - dragStartY.value
clampOffsets()
markTransformDirty()
}
function handleWindowMouseUp(): void {
stopDragging();
stopDragging()
}
function stopDragging(): void {
if (!dragging.value) {
return;
return
}
dragging.value = false;
window.removeEventListener("mousemove", handleWindowMouseMove);
window.removeEventListener("mouseup", handleWindowMouseUp);
dragging.value = false
window.removeEventListener('mousemove', handleWindowMouseMove)
window.removeEventListener('mouseup', handleWindowMouseUp)
}
function handleZoomChange(value: number | [number, number]): void {
const nextValue = Array.isArray(value) ? value[0] : value;
zoom.value = clamp(nextValue, 1, 2.4);
clampOffsets();
markTransformDirty();
const nextValue = Array.isArray(value) ? value[0] : value
zoom.value = clamp(nextValue, 1, 2.4)
clampOffsets()
markTransformDirty()
}
function handleClear(): void {
if (confirmLoading.value) {
return;
return
}
workingUrl.value = "";
workingFileName.value = "";
sourceKind.value = null;
localFile.value = null;
selectedLibraryImageId.value = null;
transformDirty.value = false;
resetStage();
revokeLocalObjectUrl();
workingUrl.value = ''
workingFileName.value = ''
sourceKind.value = null
localFile.value = null
selectedLibraryImageId.value = null
transformDirty.value = false
resetStage()
revokeLocalObjectUrl()
}
function handleLibrarySelect(image: ImageAssetItem): void {
activeTab.value = "local";
workingUrl.value = image.url;
workingFileName.value = image.name;
sourceKind.value = "remote";
selectedLibraryImageId.value = image.id;
localFile.value = null;
transformDirty.value = false;
void loadImageMeta(image.url);
activeTab.value = 'local'
workingUrl.value = image.url
workingFileName.value = image.name
sourceKind.value = 'remote'
selectedLibraryImageId.value = image.id
localFile.value = null
transformDirty.value = false
void loadImageMeta(image.url)
}
async function handleConfirm(): Promise<void> {
if (!workingUrl.value) {
message.warning(t("coverPicker.messages.missingImage"));
return;
message.warning(t('coverPicker.messages.missingImage'))
return
}
if (!props.uploadHandler && !props.articleId) {
message.error(t("coverPicker.messages.missingArticle"));
return;
message.error(t('coverPicker.messages.missingArticle'))
return
}
if (sourceKind.value !== "local" && !transformDirty.value && !aspectRatioLocked.value) {
emit("confirmed", {
if (sourceKind.value !== 'local' && !transformDirty.value && !aspectRatioLocked.value) {
emit('confirmed', {
url: workingUrl.value,
fileName: workingFileName.value || deriveCoverFileName(workingUrl.value) || "cover.png",
fileName: workingFileName.value || deriveCoverFileName(workingUrl.value) || 'cover.png',
assetId: selectedLibraryImageId.value,
});
emit("update:open", false);
return;
})
emit('update:open', false)
return
}
confirmLoading.value = true;
confirmLoading.value = true
try {
if (sourceKind.value === "local" && !transformDirty.value && !aspectRatioLocked.value && localFile.value) {
const fallbackFileName = localFile.value.name || deriveCoverFileName(workingUrl.value) || "cover.webp";
const uploaded = await uploadPickedFile(localFile.value, fallbackFileName);
emit("confirmed", {
if (
sourceKind.value === 'local' &&
!transformDirty.value &&
!aspectRatioLocked.value &&
localFile.value
) {
const fallbackFileName =
localFile.value.name || deriveCoverFileName(workingUrl.value) || 'cover.webp'
const uploaded = await uploadPickedFile(localFile.value, fallbackFileName)
emit('confirmed', {
url: uploaded.url,
fileName: uploaded.fileName,
assetId: uploaded.assetId,
});
emit("update:open", false);
return;
})
emit('update:open', false)
return
}
const blob = await renderCroppedBlob(primaryRequirement.value);
const blob = await renderCroppedBlob(primaryRequirement.value)
const fileName = ensureImageExtension(
workingFileName.value || deriveCoverFileName(workingUrl.value) || "cover.png",
workingFileName.value || deriveCoverFileName(workingUrl.value) || 'cover.png',
blob.type,
);
const file = new File([blob], fileName, { type: blob.type });
const uploaded = await uploadPickedFile(file, fileName);
)
const file = new File([blob], fileName, { type: blob.type })
const uploaded = await uploadPickedFile(file, fileName)
emit("confirmed", {
emit('confirmed', {
url: uploaded.url,
fileName: uploaded.fileName,
assetId: uploaded.assetId,
});
emit("update:open", false);
})
emit('update:open', false)
} catch (error) {
message.error(formatError(error));
message.error(formatError(error))
} finally {
confirmLoading.value = false;
confirmLoading.value = false
}
}
async function uploadPickedFile(file: File, fallbackFileName: string): Promise<{ url: string; fileName: string; assetId?: number | null }> {
async function uploadPickedFile(
file: File,
fallbackFileName: string,
): Promise<{ url: string; fileName: string; assetId?: number | null }> {
if (props.uploadHandler) {
const uploaded = await props.uploadHandler(file);
const uploaded = await props.uploadHandler(file)
return {
url: uploaded.url,
fileName: String(uploaded.fileName ?? "").trim() || fallbackFileName,
fileName: String(uploaded.fileName ?? '').trim() || fallbackFileName,
assetId: uploaded.assetId ?? null,
};
}
}
if (!props.articleId) {
throw new Error(t("coverPicker.messages.missingArticle"));
throw new Error(t('coverPicker.messages.missingArticle'))
}
const uploaded = await articlesApi.uploadImage(props.articleId, file);
const uploaded = await articlesApi.uploadImage(props.articleId, file)
return {
url: uploaded.url,
fileName: fallbackFileName,
assetId: null,
};
}
}
async function renderCroppedBlob(requirement: PlatformCoverRequirement): Promise<Blob> {
const image = await loadImageElement(workingUrl.value);
const canvas = document.createElement("canvas");
canvas.width = requirement.outputWidth ?? Math.max(Math.round(stageSize.value.width * 2), 1);
canvas.height = requirement.outputHeight ?? Math.max(Math.round(stageSize.value.height * 2), 1);
const image = await loadImageElement(workingUrl.value)
const canvas = document.createElement('canvas')
canvas.width = requirement.outputWidth ?? Math.max(Math.round(stageSize.value.width * 2), 1)
canvas.height = requirement.outputHeight ?? Math.max(Math.round(stageSize.value.height * 2), 1)
const context = canvas.getContext("2d");
const context = canvas.getContext('2d')
if (!context) {
throw new Error("article_image_upload_failed");
throw new Error('article_image_upload_failed')
}
const baseScale = Math.max(
stageSize.value.width / image.naturalWidth,
stageSize.value.height / image.naturalHeight,
);
const renderedWidth = image.naturalWidth * baseScale * zoom.value;
const renderedHeight = image.naturalHeight * baseScale * zoom.value;
const outputScale = canvas.width / stageSize.value.width;
)
const renderedWidth = image.naturalWidth * baseScale * zoom.value
const renderedHeight = image.naturalHeight * baseScale * zoom.value
const outputScale = canvas.width / stageSize.value.width
context.fillStyle = "#ffffff";
context.fillRect(0, 0, canvas.width, canvas.height);
context.fillStyle = '#ffffff'
context.fillRect(0, 0, canvas.width, canvas.height)
context.drawImage(
image,
(stageSize.value.width / 2 - renderedWidth / 2 + offsetX.value) * outputScale,
(stageSize.value.height / 2 - renderedHeight / 2 + offsetY.value) * outputScale,
renderedWidth * outputScale,
renderedHeight * outputScale,
);
)
const blob = await new Promise<Blob | null>((resolve) => {
canvas.toBlob(resolve, "image/png", 0.92);
});
canvas.toBlob(resolve, 'image/png', 0.92)
})
if (!blob) {
throw new Error("article_image_upload_failed");
throw new Error('article_image_upload_failed')
}
return blob;
return blob
}
function ensureImageExtension(fileName: string, mimeType: string): string {
const trimmed = fileName.trim();
const normalizedMime = mimeType.toLowerCase();
const extension = normalizedMime.includes("webp")
? "webp"
: normalizedMime.includes("gif")
? "gif"
: normalizedMime.includes("jpeg")
? "jpg"
: "png";
const trimmed = fileName.trim()
const normalizedMime = mimeType.toLowerCase()
const extension = normalizedMime.includes('webp')
? 'webp'
: normalizedMime.includes('gif')
? 'gif'
: normalizedMime.includes('jpeg')
? 'jpg'
: 'png'
if (/\.(png|jpe?g|gif|webp)$/i.test(trimmed)) {
return trimmed;
return trimmed
}
return `${trimmed || "cover"}.${extension}`;
return `${trimmed || 'cover'}.${extension}`
}
function closeModal(): void {
emit("update:open", false);
emit('update:open', false)
}
function loadImageElement(url: string): Promise<HTMLImageElement> {
return new Promise((resolve, reject) => {
const image = new Image();
image.onload = () => resolve(image);
image.onerror = () => reject(new Error("article_image_upload_failed"));
image.src = url;
});
const image = new Image()
image.onload = () => resolve(image)
image.onerror = () => reject(new Error('article_image_upload_failed'))
image.src = url
})
}
</script>
@@ -540,7 +565,7 @@ function loadImageElement(url: string): Promise<HTMLImageElement> {
/>
<a-tabs v-model:activeKey="activeTab" class="cover-picker__tabs">
<a-tab-pane key="local" :tab="localTabLabel">
<a-tab-pane key="local" :tab="localTabLabelDisplay">
<div class="cover-picker__body">
<section class="cover-picker__workspace">
<div class="cover-picker__stage-panel">
@@ -578,7 +603,7 @@ function loadImageElement(url: string): Promise<HTMLImageElement> {
</div>
<div v-if="hasImage" class="cover-picker__controls">
<span>{{ t("coverPicker.zoom") }}</span>
<span>{{ t('coverPicker.zoom') }}</span>
<a-slider
class="cover-picker__slider"
:min="1"
@@ -589,8 +614,8 @@ function loadImageElement(url: string): Promise<HTMLImageElement> {
@change="handleZoomChange"
/>
<strong>{{ Math.round(zoom * 100) }}%</strong>
<a-button size="small" @click="resetTransform" style="margin-left: 16px;">
{{ t("coverPicker.actions.reset") }}
<a-button size="small" style="margin-left: 16px" @click="resetTransform">
{{ t('coverPicker.actions.reset') }}
</a-button>
</div>
@@ -604,13 +629,10 @@ function loadImageElement(url: string): Promise<HTMLImageElement> {
<span class="cover-picker__upload-tile-icon">
<PlusOutlined />
</span>
<span>{{ t("coverPicker.actions.upload") }}</span>
<span>{{ t('coverPicker.actions.upload') }}</span>
</button>
<div
v-if="hasImage"
class="cover-picker__thumb cover-picker__thumb--active"
>
<div v-if="hasImage" class="cover-picker__thumb cover-picker__thumb--active">
<img :src="workingUrl" alt="cover thumbnail" />
<button type="button" class="cover-picker__thumb-close" @click="handleClear">
<CloseOutlined />
@@ -837,7 +859,9 @@ function loadImageElement(url: string): Promise<HTMLImageElement> {
cursor: pointer;
font-size: 11px;
backdrop-filter: blur(4px);
transition: background-color 0.2s, transform 0.2s;
transition:
background-color 0.2s,
transform 0.2s;
z-index: 10;
}