更新带货视频工作流接口和页面

This commit is contained in:
super
2026-07-09 00:27:49 +08:00
parent 0ecf5cd45a
commit aab4ce942c
51 changed files with 4777 additions and 820 deletions

View File

@@ -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 给 PythonPython 会从 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) {

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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>

View File

@@ -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]',
},