更新带货视频工作流接口和页面
This commit is contained in:
@@ -284,6 +284,7 @@ import {
|
||||
getPriceTrackResultDownloadUrl,
|
||||
getPriceTrackTaskProgressBatch,
|
||||
getPriceTrackTasksBatch,
|
||||
getTaskSkipPriceAsinsPaginated,
|
||||
listPriceTrackCandidates,
|
||||
matchPriceTrackShops,
|
||||
putPriceTrackCountryPreference,
|
||||
@@ -844,17 +845,64 @@ function isRecordMissingError(error: unknown) {
|
||||
return /记录不存在|不存在|已删除|not\s*found|404/i.test(message)
|
||||
}
|
||||
|
||||
function buildSkipAsinDeletePolicy() {
|
||||
const enabled = statusModeEnabled.value && !asinModeEnabled.value
|
||||
return {
|
||||
enabled,
|
||||
mode: enabled ? 'DELETE_WHEN_PRICE_BELOW_MINIMUM' : 'NONE',
|
||||
delete_when_price_below_minimum: enabled,
|
||||
compare_field: 'price',
|
||||
threshold_field: 'minimum_price',
|
||||
target: 'skip_asin',
|
||||
source: 'frontend-price-track',
|
||||
function priceTrackModeForAppClient() {
|
||||
return statusModeEnabled.value ? 'status' : 'asin'
|
||||
}
|
||||
|
||||
function buildMinimumPriceByCountryAndAsin(rowsByCountry: Record<string, PriceTrackAsinParsedRow[]>) {
|
||||
const out: Record<string, Record<string, string>> = {}
|
||||
for (const [country, rows] of Object.entries(rowsByCountry || {})) {
|
||||
const bucket: Record<string, string> = {}
|
||||
for (const row of rows || []) {
|
||||
const asin = (row.asin || '').trim()
|
||||
if (!asin) continue
|
||||
bucket[asin] = row.minimumPrice == null ? '' : String(row.minimumPrice)
|
||||
}
|
||||
out[country] = bucket
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
function hasCountryRows(rowsByCountry: Record<string, PriceTrackAsinParsedRow[]> | undefined) {
|
||||
return Object.values(rowsByCountry || {}).some((rows) => Array.isArray(rows) && rows.length > 0)
|
||||
}
|
||||
|
||||
function hasMinimumPriceMap(map: Record<string, Record<string, string>> | undefined) {
|
||||
return Object.values(map || {}).some((rows) => rows && Object.keys(rows).length > 0)
|
||||
}
|
||||
|
||||
function buildMinimumPriceMapForAppClient(taskVo: PriceTrackCreateTaskVo, asinRowsByCountry: Record<string, PriceTrackAsinParsedRow[]>) {
|
||||
return hasMinimumPriceMap(taskVo.minimumPriceByCountryAndAsin)
|
||||
? taskVo.minimumPriceByCountryAndAsin || {}
|
||||
: buildMinimumPriceByCountryAndAsin(asinRowsByCountry)
|
||||
}
|
||||
|
||||
async function loadAsinRowsForAppClient(taskVo: PriceTrackCreateTaskVo) {
|
||||
const fromTask = taskVo.asinRowsByCountry || {}
|
||||
if (hasCountryRows(fromTask)) {
|
||||
return fromTask
|
||||
}
|
||||
if (!asinModeEnabled.value) {
|
||||
return {}
|
||||
}
|
||||
|
||||
const merged: Record<string, PriceTrackAsinParsedRow[]> = {}
|
||||
let page = 1
|
||||
let totalPages = 1
|
||||
do {
|
||||
const res = await getTaskSkipPriceAsinsPaginated(taskVo.taskId, page, 2000)
|
||||
totalPages = Math.max(1, Number(res.totalPages) || 1)
|
||||
const pageRows = (res.asin_rows_by_country || res.skipAsinDetailsByCountry || res.skip_asin_details_by_country || {}) as Record<
|
||||
string,
|
||||
PriceTrackAsinParsedRow[]
|
||||
>
|
||||
for (const [country, rows] of Object.entries(pageRows)) {
|
||||
merged[country] = [...(merged[country] || []), ...(rows || [])]
|
||||
}
|
||||
page += 1
|
||||
} while (page <= totalPages)
|
||||
|
||||
return merged
|
||||
}
|
||||
|
||||
function removeMatchedRowsLocally(rows: Array<{ shopName?: string; shopId?: number | string | null }>) {
|
||||
@@ -953,20 +1001,24 @@ async function pushToPythonQueueLegacy() {
|
||||
saveTaskSnapshotsToStorage()
|
||||
saveTaskDetailsToStorage()
|
||||
|
||||
// 2. 推送 taskId 给 Python,Python 会从 Java 拉取完整任务上下文
|
||||
const firstRow = matchedRows[0]
|
||||
const asinRowsByCountry = await loadAsinRowsForAppClient(taskVo)
|
||||
const minimumPriceByCountryAndAsin = buildMinimumPriceMapForAppClient(taskVo, asinRowsByCountry)
|
||||
|
||||
// 2. 推送 app_client 当前消费的字段
|
||||
const queuePayload = {
|
||||
type: 'price-track-run',
|
||||
ts: Date.now(),
|
||||
data: {
|
||||
task_id: taskVo.taskId,
|
||||
shop_names: matchedRows.map((i) => i.shopName).filter(Boolean),
|
||||
shop_name: (firstRow.shopName || '').trim(),
|
||||
shopName: (firstRow.shopName || '').trim(),
|
||||
companyName: firstRow.companyName || '',
|
||||
shopMallName: firstRow.shopMallName || '',
|
||||
country_codes: resolveCountryCodesForRequest(),
|
||||
use_skip_asin_check: statusModeEnabled.value && !asinModeEnabled.value,
|
||||
skip_asin_check_url: `/api/price-track/tasks/${taskVo.taskId}/skip-asin/check`,
|
||||
use_paginated_skip_asins: false,
|
||||
skip_asin_page_size: 0,
|
||||
skip_asin_delete_policy: buildSkipAsinDeletePolicy(),
|
||||
delete_skip_asin_when_price_below_minimum: statusModeEnabled.value && !asinModeEnabled.value,
|
||||
mode: priceTrackModeForAppClient(),
|
||||
asin_rows_by_country: asinRowsByCountry,
|
||||
minimum_price_by_country_and_asin: minimumPriceByCountryAndAsin,
|
||||
},
|
||||
}
|
||||
queuePayloadText.value = JSON.stringify(queuePayload, null, 2)
|
||||
@@ -990,49 +1042,24 @@ async function pushToPythonQueueLegacy() {
|
||||
}
|
||||
|
||||
// ========== 任务状态轮询 ==========
|
||||
function buildQueuePayload(taskVo: PriceTrackCreateTaskVo, row: PriceTrackShopQueueItem) {
|
||||
const taskItem = taskVo.items?.[0]
|
||||
async function buildQueuePayload(taskVo: PriceTrackCreateTaskVo, row: PriceTrackShopQueueItem) {
|
||||
const shopName = (row.shopName || '').trim()
|
||||
const skipAsinDeletePolicy = buildSkipAsinDeletePolicy()
|
||||
const skipAsinCheckPath = `/api/price-track/tasks/${taskVo.taskId}/skip-asin/check`
|
||||
const resultId = taskItem?.resultId ?? null
|
||||
const loopRunId = taskItem?.loopRunId ?? null
|
||||
const roundIndex = taskItem?.roundIndex ?? null
|
||||
const taskStatus = taskItem?.taskStatus || 'RUNNING'
|
||||
const success = taskItem?.success ?? false
|
||||
const error = taskItem?.error || null
|
||||
const outputFilename = taskItem?.outputFilename || null
|
||||
const downloadUrl = taskItem?.downloadUrl || null
|
||||
const asinRowsByCountry = await loadAsinRowsForAppClient(taskVo)
|
||||
const minimumPriceByCountryAndAsin = buildMinimumPriceMapForAppClient(taskVo, asinRowsByCountry)
|
||||
|
||||
return {
|
||||
type: 'price-track-run',
|
||||
ts: Date.now(),
|
||||
data: {
|
||||
task_id: taskVo.taskId,
|
||||
result_id: resultId,
|
||||
shop_name: shopName,
|
||||
shop_id: row.shopId ?? null,
|
||||
platform: row.platform || '',
|
||||
company_name: row.companyName || '',
|
||||
shop_mall_name: row.shopMallName || '',
|
||||
matched: !!row.matched,
|
||||
match_status: row.matchStatus || '',
|
||||
match_message: row.matchMessage || null,
|
||||
success: success,
|
||||
error: error,
|
||||
output_filename: outputFilename,
|
||||
download_url: downloadUrl,
|
||||
task_status: taskStatus,
|
||||
loop_run_id: loopRunId,
|
||||
round_index: roundIndex,
|
||||
country_codes: resolveCountryCodesForRequest(),
|
||||
mode: statusModeEnabled.value ? 'status' : 'asin',
|
||||
use_skip_asin_check: statusModeEnabled.value && !asinModeEnabled.value,
|
||||
skip_asin_check_url: skipAsinCheckPath,
|
||||
use_paginated_skip_asins: false,
|
||||
skip_asin_page_size: 0,
|
||||
skip_asin_delete_policy: skipAsinDeletePolicy,
|
||||
delete_skip_asin_when_price_below_minimum: skipAsinDeletePolicy.delete_when_price_below_minimum,
|
||||
mode: priceTrackModeForAppClient(),
|
||||
shopName,
|
||||
companyName: row.companyName || '',
|
||||
shopMallName: row.shopMallName || '',
|
||||
asin_rows_by_country: asinRowsByCountry,
|
||||
minimum_price_by_country_and_asin: minimumPriceByCountryAndAsin,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1174,7 +1201,7 @@ async function processMatchedQueue() {
|
||||
}
|
||||
saveTaskSnapshotsToStorage()
|
||||
saveTaskDetailsToStorage()
|
||||
const queuePayload = buildQueuePayload(taskVo, row)
|
||||
const queuePayload = await buildQueuePayload(taskVo, row)
|
||||
queuePayloadText.value = JSON.stringify(queuePayload, null, 2)
|
||||
const pushResult = await api.enqueue_json(queuePayload)
|
||||
if (!pushResult?.success) {
|
||||
@@ -1375,7 +1402,7 @@ async function runLoopExecution(loopId?: number) {
|
||||
}
|
||||
})()
|
||||
recordCreatedTask(taskVo)
|
||||
const queuePayload = buildQueuePayload(taskVo, row)
|
||||
const queuePayload = await buildQueuePayload(taskVo, row)
|
||||
queuePayloadText.value = JSON.stringify(queuePayload, null, 2)
|
||||
const pushResult = await api.enqueue_json(queuePayload)
|
||||
if (!pushResult?.success) {
|
||||
|
||||
@@ -58,6 +58,7 @@
|
||||
<main class="delivery-page__body">
|
||||
<DeliveryVideoWorkspace />
|
||||
</main>
|
||||
<DownloadProgressPanel />
|
||||
</div>
|
||||
</PageShell>
|
||||
</template>
|
||||
@@ -69,6 +70,7 @@ import PageShell from '@/components/layout/PageShell.vue'
|
||||
import BrandTopBar from '@/pages/brand/components/BrandTopBar.vue'
|
||||
import DeliveryVideoWorkspace from '@/pages/image-video/components/DeliveryVideoWorkspace.vue'
|
||||
import { getPywebviewApi } from '@/shared/bridges/pywebview'
|
||||
import DownloadProgressPanel from '@/shared/components/DownloadProgressPanel.vue'
|
||||
|
||||
type ViewMode = 'menu' | 'delivery'
|
||||
|
||||
@@ -389,8 +391,17 @@ async function launchDesktop() {
|
||||
background: #0f0f0f;
|
||||
}
|
||||
|
||||
:global(html),
|
||||
:global(body),
|
||||
:global(#app) {
|
||||
min-height: 100%;
|
||||
background: #0f0f0f;
|
||||
}
|
||||
|
||||
.delivery-page__body {
|
||||
min-height: calc(100vh - 64px);
|
||||
padding: 12px 20px 24px;
|
||||
background: #0f0f0f;
|
||||
}
|
||||
|
||||
.delivery-page__top-return {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -100,13 +100,35 @@ export interface UploadFileVo {
|
||||
localPath: string;
|
||||
size: number;
|
||||
relativePath?: string;
|
||||
objectKey?: string;
|
||||
url?: string;
|
||||
mediaType?: "image" | "video" | "audio" | "file" | string;
|
||||
}
|
||||
|
||||
export async function uploadTempFileToJava(file: File, relativePath?: string) {
|
||||
export interface UploadTempFileOptions {
|
||||
relativePath?: string;
|
||||
uploadToOss?: boolean;
|
||||
moduleType?: string;
|
||||
}
|
||||
|
||||
export async function uploadTempFileToJava(
|
||||
file: File,
|
||||
relativePathOrOptions?: string | UploadTempFileOptions,
|
||||
) {
|
||||
const options =
|
||||
typeof relativePathOrOptions === 'string'
|
||||
? { relativePath: relativePathOrOptions }
|
||||
: relativePathOrOptions || {}
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
if (relativePath) {
|
||||
formData.append('relativePath', relativePath)
|
||||
if (options.relativePath) {
|
||||
formData.append('relativePath', options.relativePath)
|
||||
}
|
||||
if (options.uploadToOss) {
|
||||
formData.append('uploadToOss', 'true')
|
||||
}
|
||||
if (options.moduleType) {
|
||||
formData.append('moduleType', options.moduleType)
|
||||
}
|
||||
const response = await http.post<JavaApiResponse<UploadFileVo>>(
|
||||
`${JAVA_API_PREFIX}/files/upload`,
|
||||
@@ -2437,6 +2459,11 @@ export interface SkipPriceAsinPageVo {
|
||||
totalPages: number;
|
||||
skipAsinsByCountry: Record<string, string[]>;
|
||||
skipAsinDetailsByCountry: Record<string, Array<{ asin?: string; minimumPrice?: string }>>;
|
||||
skip_asins?: Record<string, string[]>;
|
||||
skip_asins_by_country?: Record<string, string[]>;
|
||||
skip_asin_details_by_country?: Record<string, Array<{ asin?: string; minimumPrice?: string }>>;
|
||||
asin_rows_by_country?: Record<string, Array<{ asin?: string; minimumPrice?: string }>>;
|
||||
minimum_price_by_country_and_asin?: Record<string, Record<string, string>>;
|
||||
}
|
||||
|
||||
export function deletePendingPriceTrackShopResult(shopName: string) {
|
||||
@@ -2447,6 +2474,232 @@ export function deletePendingPriceTrackShopResult(shopName: string) {
|
||||
);
|
||||
}
|
||||
|
||||
// ========== 视频复刻 / 图生视频 ==========
|
||||
|
||||
export interface ImageVideoDouyinCopyVo {
|
||||
recognizedContent?: string;
|
||||
scriptDraft?: string;
|
||||
executeId?: string;
|
||||
debugUrl?: string;
|
||||
}
|
||||
|
||||
export interface ImageVideoSecretStatusVo {
|
||||
userId?: number;
|
||||
configured?: boolean;
|
||||
valid?: boolean;
|
||||
expired?: boolean;
|
||||
hasCopyApiKey?: boolean;
|
||||
hasT8Key?: boolean;
|
||||
hasVoiceApiKey?: boolean;
|
||||
hasVoiceGroupId?: boolean;
|
||||
copyApiKeyMasked?: string;
|
||||
t8KeyMasked?: string;
|
||||
voiceApiKeyMasked?: string;
|
||||
voiceGroupIdMasked?: string;
|
||||
expireDays?: number;
|
||||
expiresAt?: string;
|
||||
}
|
||||
|
||||
export interface ImageVideoSecretSavePayload {
|
||||
copyApiKey?: string;
|
||||
t8Key?: string;
|
||||
voiceApiKey?: string;
|
||||
voiceGroupId?: string;
|
||||
expireDays: number;
|
||||
}
|
||||
|
||||
export interface ImageVideoWorkflowParameters {
|
||||
api_key_info: {
|
||||
t8star_key: string;
|
||||
ai_conductor_key: string;
|
||||
};
|
||||
bg_info: {
|
||||
type: number;
|
||||
prompt: string;
|
||||
bg_image: string;
|
||||
};
|
||||
face_info: {
|
||||
type: number;
|
||||
model_figure: string;
|
||||
model_image: string[];
|
||||
};
|
||||
proc_info: {
|
||||
type: string;
|
||||
name: string;
|
||||
proc_image: string[];
|
||||
properties: string;
|
||||
};
|
||||
text_info: {
|
||||
type: number;
|
||||
language: string;
|
||||
text: string;
|
||||
file_url: string;
|
||||
};
|
||||
video_info: {
|
||||
video_url: string;
|
||||
share_url: string;
|
||||
ref_video_mode: string;
|
||||
mode: string;
|
||||
model: string;
|
||||
prompt: string;
|
||||
ratio: string;
|
||||
resolution: string;
|
||||
duration: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ImageVideoWorkflowResponse {
|
||||
code?: number;
|
||||
msg?: string;
|
||||
data?: string;
|
||||
execute_id?: string;
|
||||
debug_url?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface ImageVideoMediaUploadVo {
|
||||
url: string;
|
||||
objectKey: string;
|
||||
originalFilename: string;
|
||||
mediaType: "image" | "video" | "audio" | string;
|
||||
}
|
||||
|
||||
export interface ImageVideoVoiceWorkflowResponse {
|
||||
code?: number;
|
||||
msg?: string;
|
||||
data?: unknown;
|
||||
execute_id?: string;
|
||||
debug_url?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface ImageVideoWorkflowRunRequest {
|
||||
userId: number;
|
||||
parameters: ImageVideoWorkflowParameters;
|
||||
}
|
||||
|
||||
export interface ImageVideoWorkflowResultRequest {
|
||||
userId: number;
|
||||
executeId: string;
|
||||
}
|
||||
|
||||
export function getImageVideoSecretStatus() {
|
||||
return unwrapJavaResponse(
|
||||
get<JavaApiResponse<ImageVideoSecretStatusVo>>(
|
||||
`${JAVA_API_PREFIX}/image-video/secrets`,
|
||||
{ params: { user_id: getCurrentUserId() } },
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
export function saveImageVideoSecrets(payload: ImageVideoSecretSavePayload) {
|
||||
return unwrapJavaResponse(
|
||||
put<JavaApiResponse<ImageVideoSecretStatusVo>, ImageVideoSecretSavePayload & { userId: number }>(
|
||||
`${JAVA_API_PREFIX}/image-video/secrets`,
|
||||
{ ...payload, userId: getCurrentUserId() },
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
export function runImageVideoDouyinCopy(url: string) {
|
||||
return unwrapJavaResponse(
|
||||
post<JavaApiResponse<ImageVideoDouyinCopyVo>, { userId: number; url: string }>(
|
||||
`${JAVA_API_PREFIX}/image-video/douyin-copy`,
|
||||
{ userId: getCurrentUserId(), url },
|
||||
{ timeout: 180000 },
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
export function runImageVideoWorkflow(parameters: ImageVideoWorkflowParameters) {
|
||||
return unwrapJavaResponse(
|
||||
post<JavaApiResponse<ImageVideoWorkflowResponse>, ImageVideoWorkflowRunRequest>(
|
||||
`${JAVA_API_PREFIX}/image-video/workflow/run`,
|
||||
{ userId: getCurrentUserId(), parameters },
|
||||
{ timeout: 120000 },
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
export function getImageVideoWorkflowResult(executeId: string) {
|
||||
return unwrapJavaResponse(
|
||||
post<JavaApiResponse<ImageVideoWorkflowResponse>, ImageVideoWorkflowResultRequest>(
|
||||
`${JAVA_API_PREFIX}/image-video/workflow/result`,
|
||||
{ userId: getCurrentUserId(), executeId },
|
||||
{ timeout: 600000 },
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
export async function uploadImageVideoMedia(file: File) {
|
||||
const response = await uploadTempFileToJava(file, {
|
||||
uploadToOss: true,
|
||||
moduleType: "IMAGE_VIDEO",
|
||||
});
|
||||
if (!response.success) {
|
||||
throw new Error(response.message || "请求失败");
|
||||
}
|
||||
const data = response.data;
|
||||
if (!data?.url || !data.objectKey) {
|
||||
throw new Error("OSS 上传未返回有效 URL");
|
||||
}
|
||||
return {
|
||||
url: data.url,
|
||||
objectKey: data.objectKey,
|
||||
originalFilename: data.originalFilename || file.name,
|
||||
mediaType: data.mediaType || resolveImageVideoMediaType(file),
|
||||
} as ImageVideoMediaUploadVo;
|
||||
}
|
||||
|
||||
function resolveImageVideoMediaType(file: File) {
|
||||
const type = (file.type || "").toLowerCase();
|
||||
const name = (file.name || "").toLowerCase();
|
||||
if (type.startsWith("image/") || /\.(png|jpe?g|webp|gif|bmp|svg)$/.test(name)) return "image";
|
||||
if (type.startsWith("video/") || /\.(mp4|mov|webm|m4v|ogg|avi|mkv)$/.test(name)) return "video";
|
||||
if (type.startsWith("audio/") || /\.(mp3|wav|m4a|aac|flac)$/.test(name)) return "audio";
|
||||
return "file";
|
||||
}
|
||||
|
||||
export function listImageVideoVoices() {
|
||||
return unwrapJavaResponse(
|
||||
post<JavaApiResponse<ImageVideoVoiceWorkflowResponse>, { userId: number }>(
|
||||
`${JAVA_API_PREFIX}/image-video/voice/list`,
|
||||
{ userId: getCurrentUserId() },
|
||||
{ timeout: 180000 },
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
export function deleteImageVideoVoice(voiceId: string) {
|
||||
return unwrapJavaResponse(
|
||||
post<JavaApiResponse<ImageVideoVoiceWorkflowResponse>, { userId: number; voiceId: string }>(
|
||||
`${JAVA_API_PREFIX}/image-video/voice/delete`,
|
||||
{ userId: getCurrentUserId(), voiceId },
|
||||
{ timeout: 180000 },
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
export function cloneImageVideoVoice(payload: { audioUrl?: string; videoUrl?: string }) {
|
||||
return unwrapJavaResponse(
|
||||
post<JavaApiResponse<ImageVideoVoiceWorkflowResponse>, { userId: number; audioUrl?: string; videoUrl?: string }>(
|
||||
`${JAVA_API_PREFIX}/image-video/voice/clone`,
|
||||
{ userId: getCurrentUserId(), ...payload },
|
||||
{ timeout: 180000 },
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
export function synthesizeImageVideoVoice(payload: { text: string; voiceId: string }) {
|
||||
return unwrapJavaResponse(
|
||||
post<JavaApiResponse<ImageVideoVoiceWorkflowResponse>, { userId: number; text: string; voiceId: string }>(
|
||||
`${JAVA_API_PREFIX}/image-video/voice/synthesis`,
|
||||
{ userId: getCurrentUserId(), ...payload },
|
||||
{ timeout: 180000 },
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ========== 采集数据 ==========
|
||||
|
||||
export interface CollectDataSourceFile {
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
<template>
|
||||
<div class="ai-asset-dropzone">
|
||||
<div
|
||||
class="ai-asset-dropzone"
|
||||
:class="{
|
||||
'ai-asset-dropzone--compact': compact,
|
||||
'ai-asset-dropzone--active': Boolean(fileName || sourceUrl),
|
||||
}"
|
||||
>
|
||||
<div class="ai-asset-dropzone__head">
|
||||
<div>
|
||||
<div class="ai-asset-dropzone__title">{{ title }}</div>
|
||||
@@ -21,16 +27,31 @@
|
||||
:auto-upload="false"
|
||||
:show-file-list="false"
|
||||
:accept="accept"
|
||||
:multiple="multiple"
|
||||
:disabled="loading"
|
||||
class="ai-asset-dropzone__upload"
|
||||
@change="handleChange"
|
||||
>
|
||||
<div v-if="previewUrl || (fileName && previewKind === 'file')" class="ai-asset-dropzone__preview">
|
||||
<img
|
||||
<div
|
||||
v-if="previewUrl || (fileName && previewKind === 'file')"
|
||||
class="ai-asset-dropzone__preview"
|
||||
:class="{
|
||||
'ai-asset-dropzone__preview--clickable': previewClickable && previewKind === 'image' && previewUrl,
|
||||
}"
|
||||
>
|
||||
<button
|
||||
v-if="previewKind === 'image'"
|
||||
:src="previewUrl"
|
||||
class="ai-asset-dropzone__media"
|
||||
alt=""
|
||||
/>
|
||||
type="button"
|
||||
class="ai-asset-dropzone__image-preview-btn"
|
||||
@click.stop.prevent="emit('preview', previewUrl)"
|
||||
>
|
||||
<img
|
||||
:src="previewUrl"
|
||||
class="ai-asset-dropzone__media"
|
||||
alt=""
|
||||
/>
|
||||
<span v-if="previewClickable" class="ai-asset-dropzone__preview-tip">点击放大预览</span>
|
||||
</button>
|
||||
<video
|
||||
v-else-if="previewKind === 'video'"
|
||||
:src="previewUrl"
|
||||
@@ -42,24 +63,28 @@
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="ai-asset-dropzone__empty">
|
||||
<div class="ai-asset-dropzone__action">{{ loading ? '上传中...' : actionLabel }}</div>
|
||||
<div class="ai-asset-dropzone__placeholder">{{ placeholder }}</div>
|
||||
<div v-if="helper" class="ai-asset-dropzone__helper">{{ helper }}</div>
|
||||
</div>
|
||||
</el-upload>
|
||||
|
||||
<div v-if="fileName" class="ai-asset-dropzone__footer">
|
||||
<span class="ai-asset-dropzone__selected-label">{{ selectedLabel }}</span>
|
||||
<span class="ai-asset-dropzone__filename">{{ fileName }}</span>
|
||||
</div>
|
||||
|
||||
<el-input
|
||||
v-if="allowUrl"
|
||||
:model-value="sourceUrl"
|
||||
class="ai-asset-dropzone__url"
|
||||
clearable
|
||||
:placeholder="urlPlaceholder"
|
||||
@change="(value) => $emit('url-change', value)"
|
||||
@clear="$emit('url-change', '')"
|
||||
/>
|
||||
<div v-if="allowUrl" class="ai-asset-dropzone__url-wrap">
|
||||
<span v-if="urlLabel" class="ai-asset-dropzone__url-label">{{ urlLabel }}</span>
|
||||
<el-input
|
||||
:model-value="sourceUrl"
|
||||
class="ai-asset-dropzone__url"
|
||||
clearable
|
||||
:placeholder="urlPlaceholder"
|
||||
@change="(value) => $emit('url-change', value)"
|
||||
@clear="$emit('url-change', '')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -79,6 +104,13 @@ withDefaults(
|
||||
allowUrl?: boolean
|
||||
sourceUrl?: string
|
||||
urlPlaceholder?: string
|
||||
urlLabel?: string
|
||||
actionLabel?: string
|
||||
selectedLabel?: string
|
||||
compact?: boolean
|
||||
loading?: boolean
|
||||
multiple?: boolean
|
||||
previewClickable?: boolean
|
||||
}>(),
|
||||
{
|
||||
description: '',
|
||||
@@ -91,6 +123,12 @@ withDefaults(
|
||||
allowUrl: false,
|
||||
sourceUrl: '',
|
||||
urlPlaceholder: '也可以粘贴视频在线链接',
|
||||
urlLabel: '',
|
||||
actionLabel: '点击或拖拽上传',
|
||||
selectedLabel: '已选择',
|
||||
compact: false,
|
||||
loading: false,
|
||||
previewClickable: false,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -98,6 +136,7 @@ const emit = defineEmits<{
|
||||
(event: 'select', file: File): void
|
||||
(event: 'clear'): void
|
||||
(event: 'url-change', value: string): void
|
||||
(event: 'preview', value: string): void
|
||||
}>()
|
||||
|
||||
function handleChange(uploadFile: UploadFile, _files: UploadFiles) {
|
||||
@@ -114,6 +153,7 @@ function handleChange(uploadFile: UploadFile, _files: UploadFiles) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.ai-asset-dropzone__head {
|
||||
@@ -121,6 +161,11 @@ function handleChange(uploadFile: UploadFile, _files: UploadFiles) {
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.ai-asset-dropzone__head > div {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.ai-asset-dropzone__title {
|
||||
@@ -136,6 +181,10 @@ function handleChange(uploadFile: UploadFile, _files: UploadFiles) {
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.ai-asset-dropzone__clear {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
:deep(.ai-asset-dropzone__upload .el-upload) {
|
||||
width: 100%;
|
||||
}
|
||||
@@ -146,13 +195,19 @@ function handleChange(uploadFile: UploadFile, _files: UploadFiles) {
|
||||
padding: 12px;
|
||||
border: 1px dashed #474747;
|
||||
border-radius: 14px;
|
||||
background: #1f1f1f;
|
||||
transition: border-color 0.2s ease, background 0.2s ease;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.025), rgba(255, 255, 255, 0)),
|
||||
#1f1f1f;
|
||||
transition:
|
||||
border-color 0.2s ease,
|
||||
background 0.2s ease,
|
||||
box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
:deep(.ai-asset-dropzone__upload .el-upload-dragger:hover) {
|
||||
border-color: #8c63ff;
|
||||
background: #232323;
|
||||
box-shadow: 0 0 0 1px rgba(140, 99, 255, 0.08) inset;
|
||||
}
|
||||
|
||||
.ai-asset-dropzone__preview,
|
||||
@@ -168,6 +223,22 @@ function handleChange(uploadFile: UploadFile, _files: UploadFiles) {
|
||||
.ai-asset-dropzone__empty {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.ai-asset-dropzone__action {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 30px;
|
||||
padding: 0 12px;
|
||||
border: 1px solid rgba(140, 99, 255, 0.38);
|
||||
border-radius: 999px;
|
||||
background: rgba(140, 99, 255, 0.12);
|
||||
color: #ffffff;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.ai-asset-dropzone__placeholder {
|
||||
@@ -188,6 +259,39 @@ function handleChange(uploadFile: UploadFile, _files: UploadFiles) {
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.ai-asset-dropzone__image-preview-btn {
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.ai-asset-dropzone__preview--clickable .ai-asset-dropzone__image-preview-btn {
|
||||
cursor: zoom-in;
|
||||
}
|
||||
|
||||
.ai-asset-dropzone__preview-tip {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
bottom: 8px;
|
||||
padding: 4px 8px;
|
||||
border-radius: 999px;
|
||||
background: rgba(0, 0, 0, 0.58);
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
opacity: 0;
|
||||
transition: opacity 0.18s ease;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.ai-asset-dropzone__preview--clickable:hover .ai-asset-dropzone__preview-tip {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.ai-asset-dropzone__file {
|
||||
color: #d8d8d8;
|
||||
font-size: 13px;
|
||||
@@ -195,18 +299,42 @@ function handleChange(uploadFile: UploadFile, _files: UploadFiles) {
|
||||
}
|
||||
|
||||
.ai-asset-dropzone__footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 10px;
|
||||
background: #1f1f1f;
|
||||
border: 1px solid #383838;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.ai-asset-dropzone__selected-label {
|
||||
flex-shrink: 0;
|
||||
color: #8d8d8d;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.ai-asset-dropzone__filename {
|
||||
min-width: 0;
|
||||
color: #d8d8d8;
|
||||
font-size: 12px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.ai-asset-dropzone__url-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.ai-asset-dropzone__url-label {
|
||||
color: #9d9d9d;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.ai-asset-dropzone__url {
|
||||
--el-input-bg-color: #171717;
|
||||
--el-input-border-color: #383838;
|
||||
@@ -215,4 +343,22 @@ function handleChange(uploadFile: UploadFile, _files: UploadFiles) {
|
||||
--el-input-text-color: #f2f2f2;
|
||||
--el-input-placeholder-color: #777;
|
||||
}
|
||||
|
||||
.ai-asset-dropzone--compact {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.ai-asset-dropzone--compact :deep(.ai-asset-dropzone__upload .el-upload-dragger) {
|
||||
min-height: 138px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.ai-asset-dropzone--compact .ai-asset-dropzone__preview,
|
||||
.ai-asset-dropzone--compact .ai-asset-dropzone__empty {
|
||||
min-height: 118px;
|
||||
}
|
||||
|
||||
.ai-asset-dropzone--compact .ai-asset-dropzone__media {
|
||||
height: 118px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -58,7 +58,8 @@ export default defineConfig({
|
||||
'image-video': resolve(__dirname, 'image-video.html'),
|
||||
},
|
||||
output: {
|
||||
entryFileNames: 'assets/[name].js',
|
||||
entryFileNames: (chunkInfo) =>
|
||||
chunkInfo.name === 'image-video' ? 'assets/[name]-[hash].js' : 'assets/[name].js',
|
||||
chunkFileNames: 'assets/[name]-[hash].js',
|
||||
assetFileNames: 'assets/[name]-[hash][extname]',
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user