视频任务管理更新

This commit is contained in:
super
2026-07-19 22:23:26 +08:00
parent 69784b8d32
commit cbde22ec79
62 changed files with 2164 additions and 280 deletions

Binary file not shown.

View File

@@ -50,6 +50,8 @@
</label>
</div>
<ZiniaoVersionSetting v-model="ziniaoVersion" />
<div class="run-row">
<button type="button" class="btn-run" :disabled="running" @click="submitRun">
{{ running ? '解析中...' : '开始解析' }}
@@ -211,6 +213,7 @@
import { computed, onMounted, onUnmounted, ref } from 'vue'
import { ElMessage } from 'element-plus'
import BrandTopBar from './BrandTopBar.vue'
import ZiniaoVersionSetting from '@/shared/components/ZiniaoVersionSetting.vue'
import { expandBrandFolderRecursive, type BrandExpandFolderItem } from '@/shared/api/brand'
import {
deleteDeleteBrandHistory,
@@ -229,12 +232,15 @@ import { getPywebviewApi, type UploadedJavaFile } from '@/shared/bridges/pywebvi
import { getTaskPollIntervalMs } from '@/shared/task-progress-config'
import { createCategorizedTimers } from '@/shared/utils/categorized-timers'
import { saveUrlWithProgress } from '@/shared/utils/download-progress'
import { useZiniaoVersion } from '@/shared/utils/ziniao-version'
interface SessionDeleteBrandItem extends DeleteBrandResultItem {
_pushed?: boolean
_completed?: boolean
}
const ziniaoVersion = useZiniaoVersion()
interface StoredCurrentTask {
taskId: number
items: SessionDeleteBrandItem[]
@@ -1307,6 +1313,7 @@ async function runItem(item: DeleteBrandResultItem, options?: { auto?: boolean }
ts: Date.now(),
data: {
taskId: taskId,
ziniao_version: ziniaoVersion.value,
items: [item]
},
}

View File

@@ -113,6 +113,8 @@
</ul>
</div>
<ZiniaoVersionSetting v-model="ziniaoVersion" />
<div class="run-row">
<button
type="button"
@@ -393,9 +395,12 @@ import { getPywebviewApi } from "@/shared/bridges/pywebview";
import { getTaskPollIntervalMs } from "@/shared/task-progress-config";
import { createCategorizedTimers } from "@/shared/utils/categorized-timers";
import { saveUrlWithProgress } from "@/shared/utils/download-progress";
import ZiniaoVersionSetting from "@/shared/components/ZiniaoVersionSetting.vue";
import { useZiniaoVersion } from "@/shared/utils/ziniao-version";
const COUNTRY_TEMPLATE = ["德国", "英国", "法国", "意大利", "西班牙"] as const;
const MAX_TRANSIENT_ERRORS = 30;
const ziniaoVersion = useZiniaoVersion();
const shopInput = ref("");
const conditionInput = ref("");
@@ -999,6 +1004,7 @@ function buildQueuePayload(taskId: number, items: PatrolDeleteHistoryItem[]) {
ts: Date.now(),
data: {
taskId,
ziniao_version: ziniaoVersion.value,
user_id: Number(uidForStorage()) || 0,
source: "frontend-vue-patrol-delete",
delete_conditions: deleteConditionsForTask,

View File

@@ -114,6 +114,8 @@
<div v-if="countryPrefSaving" class="country-pref-status">保存中</div>
</template>
<ZiniaoVersionSetting v-model="ziniaoVersion" />
<!-- 状态指示 -->
<div v-if="statusModeEnabled" class="mode-status-bar">
<span class="mode-status-dot active"></span>
@@ -261,12 +263,14 @@
import { computed, onMounted, onUnmounted, ref } from 'vue'
import { ElMessage } from 'element-plus'
import BrandTopBar from '@/pages/brand/components/BrandTopBar.vue'
import ZiniaoVersionSetting from '@/shared/components/ZiniaoVersionSetting.vue'
import { LISTING_FILTER_OPTIONS, type ListingFilterValue } from '@/pages/brand/components/listingFilters'
import { expandBrandFolderRecursive } from '@/shared/api/brand'
import { getPywebviewApi, type UploadedJavaFile } from '@/shared/bridges/pywebview'
import { getPywebviewApi, type PywebviewApi, type UploadedJavaFile } from '@/shared/bridges/pywebview'
import { getTaskPollIntervalMs } from '@/shared/task-progress-config'
import { createCategorizedTimers } from '@/shared/utils/categorized-timers'
import { saveUrlWithProgress } from '@/shared/utils/download-progress'
import { useZiniaoVersion } from '@/shared/utils/ziniao-version'
import {
addPriceTrackCandidate,
completePriceTrackLoopChild,
@@ -287,6 +291,7 @@ import {
getTaskSkipPriceAsinsPaginated,
listPriceTrackCandidates,
matchPriceTrackShops,
markPriceTrackDispatchFailed,
putPriceTrackCountryPreference,
stopPriceTrackLoopRun,
type PriceTrackAsinParsedRow,
@@ -300,6 +305,8 @@ import {
type PriceTrackTaskDetailVo,
} from '@/shared/api/java-modules'
const ziniaoVersion = useZiniaoVersion()
// ========== 类型定义 ==========
const COUNTRY_OPTIONS = [
{ code: 'DE', label: '德国' },
@@ -1011,6 +1018,7 @@ async function pushToPythonQueueLegacy() {
ts: Date.now(),
data: {
task_id: taskVo.taskId,
ziniao_version: ziniaoVersion.value,
shop_name: (firstRow.shopName || '').trim(),
shopName: (firstRow.shopName || '').trim(),
companyName: firstRow.companyName || '',
@@ -1023,16 +1031,11 @@ async function pushToPythonQueueLegacy() {
}
queuePayloadText.value = JSON.stringify(queuePayload, null, 2)
const pushResult = await api.enqueue_json(queuePayload)
if (pushResult?.success) {
queuePushResult.value = `任务 ${taskVo.taskId} 已入队,等待执行完成...`
addPollingTask(taskVo.taskId)
scheduleNextPoll(true)
ElMessage.success('已推送到 Python 队列')
} else {
queuePushResult.value = `推送失败:${pushResult?.error || '未知错误'}`
ElMessage.error(queuePushResult.value)
}
await enqueueCreatedTask(api, taskVo.taskId, queuePayload)
queuePushResult.value = `任务 ${taskVo.taskId} 已入队,等待执行完成...`
addPollingTask(taskVo.taskId)
scheduleNextPoll(true)
ElMessage.success('已推送到 Python 队列')
} catch (e) {
queuePushResult.value = e instanceof Error ? e.message : '推送异常'
ElMessage.error(queuePushResult.value)
@@ -1052,6 +1055,7 @@ async function buildQueuePayload(taskVo: PriceTrackCreateTaskVo, row: PriceTrack
ts: Date.now(),
data: {
task_id: taskVo.taskId,
ziniao_version: ziniaoVersion.value,
shop_name: shopName,
country_codes: resolveCountryCodesForRequest(),
mode: priceTrackModeForAppClient(),
@@ -1064,6 +1068,42 @@ async function buildQueuePayload(taskVo: PriceTrackCreateTaskVo, row: PriceTrack
}
}
async function enqueueCreatedTask(api: PywebviewApi, taskId: number, queuePayload: unknown) {
if (!api.enqueue_json) throw new Error('当前环境未启用 pywebview enqueue_json')
let pushResult: Awaited<ReturnType<NonNullable<PywebviewApi['enqueue_json']>>>
try {
pushResult = await api.enqueue_json(queuePayload)
} catch (error) {
const reason = `Python 队列调用异常:${error instanceof Error ? error.message : String(error || '未知错误')}`
await compensateDispatchFailure(taskId, reason)
throw new Error(reason)
}
if (!pushResult?.success) {
const reason = pushResult?.error || 'Python 队列拒绝接收任务'
await compensateDispatchFailure(taskId, reason)
throw new Error(reason)
}
return pushResult
}
async function compensateDispatchFailure(taskId: number, reason: string) {
try {
await markPriceTrackDispatchFailed(taskId, reason)
removePollingTask(taskId)
taskDetails.value = { ...taskDetails.value, [taskId]: 'FAILED' }
saveTaskDetailsToStorage()
await loadHistory()
await loadDashboard()
syncPollingIdsWithHistory()
if (activeLoopRunId.value) await syncActiveLoopRun()
} catch (error) {
addPollingTask(taskId)
scheduleNextPoll(true)
const syncError = error instanceof Error ? error.message : String(error || '未知错误')
throw new Error(`${reason};后端失败状态同步失败:${syncError}`)
}
}
async function waitForTaskTerminal(taskId: number) {
let transientErrorCount = 0
const maxTransientErrors = 30
@@ -1203,14 +1243,7 @@ async function processMatchedQueue() {
saveTaskDetailsToStorage()
const queuePayload = await buildQueuePayload(taskVo, row)
queuePayloadText.value = JSON.stringify(queuePayload, null, 2)
const pushResult = await api.enqueue_json(queuePayload)
if (!pushResult?.success) {
failedCount += 1
queuePushResult.value = '任务 ' + taskVo.taskId + ' 推送失败,已自动继续下一条:' + (pushResult?.error || '未知错误')
ElMessage.error(queuePushResult.value)
removeMatchedRowsLocally([row])
continue
}
await enqueueCreatedTask(api, taskVo.taskId, queuePayload)
addPollingTask(taskVo.taskId)
scheduleNextPoll(true)
removeMatchedRowsLocally([row])
@@ -1404,10 +1437,7 @@ async function runLoopExecution(loopId?: number) {
recordCreatedTask(taskVo)
const queuePayload = await buildQueuePayload(taskVo, row)
queuePayloadText.value = JSON.stringify(queuePayload, null, 2)
const pushResult = await api.enqueue_json(queuePayload)
if (!pushResult?.success) {
throw new Error(pushResult?.error || `任务 ${taskVo.taskId} 推送失败`)
}
await enqueueCreatedTask(api, taskVo.taskId, queuePayload)
addPollingTask(taskVo.taskId)
scheduleNextPoll(true)
removeMatchedRowsLocally([row])

View File

@@ -64,6 +64,8 @@
</el-select>
</div>
<ZiniaoVersionSetting v-model="ziniaoVersion" />
<div class="run-row">
<button type="button" class="btn-run" :disabled="matching" @click="runMatch">
{{ matching ? '匹配中' : '匹配店铺' }}
@@ -222,6 +224,8 @@ import { getPywebviewApi } from '@/shared/bridges/pywebview'
import { getTaskPollIntervalMs } from '@/shared/task-progress-config'
import { createCategorizedTimers } from '@/shared/utils/categorized-timers'
import { saveUrlWithProgress } from '@/shared/utils/download-progress'
import ZiniaoVersionSetting from '@/shared/components/ZiniaoVersionSetting.vue'
import { useZiniaoVersion } from '@/shared/utils/ziniao-version'
const shopInput = ref('')
const candidates = ref<ProductRiskCandidateVo[]>([])
@@ -234,6 +238,7 @@ const queueWorkerRunning = ref(false)
const autoQueueEnabled = ref(false)
const queuePushResult = ref('')
const queuePayloadText = ref('')
const ziniaoVersion = useZiniaoVersion()
const PRODUCT_RISK_LISTING_FILTER_OPTIONS = [
...LISTING_FILTER_OPTIONS,
@@ -1078,6 +1083,7 @@ async function processMatchedBatchQueue(toPush: ProductRiskShopQueueItem[]) {
ts: Date.now(),
data: {
taskId,
ziniao_version: ziniaoVersion.value,
items: toPush,
country_codes: [...orderedCountryCodes.value],
risk_listing_filter: productRiskListingFilter.value,
@@ -1155,6 +1161,7 @@ async function processMatchedQueue() {
ts: Date.now(),
data: {
taskId,
ziniao_version: ziniaoVersion.value,
items: [item],
country_codes: [...orderedCountryCodes.value],
risk_listing_filter: productRiskListingFilter.value,

View File

@@ -60,6 +60,8 @@
</el-table>
</div>
<ZiniaoVersionSetting v-model="ziniaoVersion" />
<div class="run-row">
<button
type="button"
@@ -341,8 +343,11 @@ import { getPywebviewApi } from "@/shared/bridges/pywebview";
import { getTaskPollIntervalMs } from "@/shared/task-progress-config";
import { createCategorizedTimers } from "@/shared/utils/categorized-timers";
import { saveUrlWithProgress } from "@/shared/utils/download-progress";
import ZiniaoVersionSetting from "@/shared/components/ZiniaoVersionSetting.vue";
import { useZiniaoVersion } from "@/shared/utils/ziniao-version";
const MAX_TRANSIENT_ERRORS = 30;
const ziniaoVersion = useZiniaoVersion();
const shopInput = ref("");
const candidates = ref<QueryAsinCandidateVo[]>([]);
@@ -937,6 +942,7 @@ function buildQueuePayload(taskId: number, item: QueryAsinHistoryItem) {
ts: Date.now(),
data: {
taskId,
ziniao_version: ziniaoVersion.value,
user_id: Number(uidForStorage()) || 0,
source: "frontend-vue-query-asin",
submissionId,

View File

@@ -55,6 +55,8 @@
<button type="button" class="schedule-add" :disabled="pushing" @click="addScheduleValue">添加时间点</button>
<p class="hint schedule-hint">默认开启定时支持按 `MM-DD HH:mm` 配置多个时间点前端不再限制选择更早时间实际是否可执行以后端校验为准</p>
</div>
<ZiniaoVersionSetting v-model="ziniaoVersion" />
<div class="run-row">
<button type="button" class="btn-run" :disabled="matching" @click="runMatch">{{ matching ? '匹配中...' : '匹配店铺' }}</button>
<button type="button" class="btn-run btn-queue" :disabled="pushing || !matchedItems.length" @click="pushToPythonQueue">{{ pushing ? '推送中...' : scheduleEnabled ? '创建定时任务' : '推送到 Python 队列' }}</button>
@@ -107,12 +109,14 @@
import { computed, onMounted, onUnmounted, ref } from 'vue'
import { ElMessage } from 'element-plus'
import BrandTopBar from '@/pages/brand/components/BrandTopBar.vue'
import ZiniaoVersionSetting from '@/shared/components/ZiniaoVersionSetting.vue'
import { LISTING_FILTER_OPTIONS, type ListingFilterValue } from '@/pages/brand/components/listingFilters'
import { activateShopMatchTask, addShopMatchCandidate, createShopMatchTask, deleteShopMatchCandidate, deleteShopMatchHistory, deleteShopMatchTask, getShopMatchCountryPreference, getShopMatchDashboard, getShopMatchHistory, getShopMatchResultDownloadUrl, getShopMatchTaskProgressBatch, getShopMatchTasksBatch, listShopMatchCandidates, matchShopMatchShops, putShopMatchCountryPreference, type ShopMatchCandidateVo, type ShopMatchCreateTaskItem, type ShopMatchDashboardVo, type ShopMatchHistoryItem, type ShopMatchShopQueueItem, type ShopMatchTaskDetailVo } from '@/shared/api/java-modules'
import { getPywebviewApi } from '@/shared/bridges/pywebview'
import { getTaskPollIntervalMs } from '@/shared/task-progress-config'
import { createCategorizedTimers } from '@/shared/utils/categorized-timers'
import { saveUrlWithProgress } from '@/shared/utils/download-progress'
import { useZiniaoVersion } from '@/shared/utils/ziniao-version'
const COUNTRY_OPTIONS = [{ code: 'DE', label: '德国' }, { code: 'UK', label: '英国' }, { code: 'FR', label: '法国' }, { code: 'IT', label: '意大利' }, { code: 'ES', label: '西班牙' }] as const
const shopInput = ref('')
@@ -127,6 +131,7 @@ const queueWorkerRunning = ref(false)
const autoQueueEnabled = ref(false)
const queuePushResult = ref('')
const queuePayloadText = ref('')
const ziniaoVersion = useZiniaoVersion()
const orderedCountryCodes = ref<string[]>(['DE', 'UK', 'FR', 'IT', 'ES'])
const dragCountryIndex = ref<number | null>(null)
const countryPrefSaving = ref(false)
@@ -325,6 +330,7 @@ function buildQueuePayload(taskId: number, item: Pick<ShopMatchHistoryItem, 'sho
ts: Date.now(),
data: {
taskId,
ziniao_version: ziniaoVersion.value,
user_id: currentUserId(),
items: [{
shopName: item.shopName,

View File

@@ -71,6 +71,8 @@
</el-table>
</div>
<ZiniaoVersionSetting v-model="ziniaoVersion" />
<div class="run-row">
<button
type="button"
@@ -354,8 +356,11 @@ import { getPywebviewApi } from "@/shared/bridges/pywebview";
import { getTaskPollIntervalMs } from "@/shared/task-progress-config";
import { createCategorizedTimers } from "@/shared/utils/categorized-timers";
import { saveUrlWithProgress } from "@/shared/utils/download-progress";
import ZiniaoVersionSetting from "@/shared/components/ZiniaoVersionSetting.vue";
import { useZiniaoVersion } from "@/shared/utils/ziniao-version";
const MAX_TRANSIENT_ERRORS = 30;
const ziniaoVersion = useZiniaoVersion();
type WithdrawTaskBatchQueueItem = {
batchId: string;
@@ -1048,6 +1053,7 @@ function buildQueuePayload(taskId: number, batch: WithdrawTaskBatchQueueItem) {
ts: Date.now(),
data: {
task_id: taskId,
ziniao_version: ziniaoVersion.value,
user_id: Number(uidForStorage()) || 0,
source: "frontend-vue-withdraw",
reserved_amount: batch.reservedAmount,

View File

@@ -2501,6 +2501,15 @@ export function getTaskSkipPriceAsinsPaginated(
);
}
export function markPriceTrackDispatchFailed(taskId: number, errorMessage: string) {
return unwrapJavaResponse(
post<JavaApiResponse<null>, { errorMessage: string }>(
`${JAVA_API_PREFIX}/price-track/tasks/${taskId}/dispatch-failed?user_id=${encodeURIComponent(String(getCurrentUserId()))}`,
{ errorMessage },
),
);
}
export function checkTaskSkipPriceAsin(taskId: number, country: string, asin: string) {
return unwrapJavaResponse(
get<JavaApiResponse<SkipPriceAsinCheckVo>>(

View File

@@ -0,0 +1,35 @@
<template>
<section class="ziniao-version-setting">
<div class="setting-title">紫鸟版本</div>
<div class="version-options" role="radiogroup" aria-label="紫鸟版本">
<label v-for="option in options" :key="option.value" class="version-option"
:class="{ active: modelValue === option.value }">
<input :checked="modelValue === option.value" type="radio" :name="radioName" :value="option.value"
@change="emit('update:modelValue', option.value)" />
<span>{{ option.label }}</span>
</label>
</div>
</section>
</template>
<script setup lang="ts">
import type { ZiniaoVersion } from '@/shared/utils/ziniao-version'
defineProps<{ modelValue: ZiniaoVersion }>()
const emit = defineEmits<{ 'update:modelValue': [value: ZiniaoVersion] }>()
const radioName = `ziniao-version-${Math.random().toString(36).slice(2)}`
const options: Array<{ label: string; value: ZiniaoVersion }> = [
{ label: '新版', value: 'new' },
{ label: '旧版', value: 'old' },
]
</script>
<style scoped>
.ziniao-version-setting { margin: 16px 0; }
.setting-title { margin-bottom: 10px; color: #bbb; font-size: 13px; }
.version-options { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); width: 100%; min-height: 40px; padding: 3px; border: 1px solid #3a3a3a; border-radius: 6px; background: #202020; }
.version-option { display: flex; min-width: 0; align-items: center; justify-content: center; border-radius: 4px; color: #aaa; cursor: pointer; font-size: 13px; transition: background-color .15s ease, color .15s ease; }
.version-option:hover { color: #eee; }
.version-option.active { background: #409eff; color: #fff; }
.version-option input { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; }
</style>

View File

@@ -0,0 +1,20 @@
import { ref, watch } from 'vue'
export type ZiniaoVersion = 'new' | 'old'
const STORAGE_KEY = 'ziniao:version'
function loadZiniaoVersion(): ZiniaoVersion {
if (typeof window === 'undefined') return 'new'
return window.localStorage.getItem(STORAGE_KEY) === 'old' ? 'old' : 'new'
}
export function useZiniaoVersion() {
const ziniaoVersion = ref<ZiniaoVersion>(loadZiniaoVersion())
watch(ziniaoVersion, (value) => {
if (typeof window !== 'undefined') window.localStorage.setItem(STORAGE_KEY, value)
})
return ziniaoVersion
}

View File

@@ -39,6 +39,70 @@ html {
.page-shell {
min-height: 100vh;
min-height: 100dvh;
}
/*
* The desktop client uses a resizable WebView. Brand modules have a dynamic
* navigation header, so their content must consume the remaining space
* instead of subtracting the old fixed 56px header height.
*/
@media (min-width: 1101px) {
html:has(.module-page),
body:has(.module-page) {
height: 100%;
overflow: hidden;
background: #1a1a1a;
}
#app:has(> .module-page) {
height: 100vh;
height: 100dvh;
min-height: 0;
overflow: hidden;
background: #1a1a1a;
}
.module-page {
display: flex;
flex-direction: column;
height: 100vh;
height: 100dvh;
min-height: 0 !important;
overflow: hidden;
}
.module-page > .top-bar {
flex: 0 0 auto;
}
.module-page > .main-content {
flex: 1 1 auto;
height: auto !important;
min-height: 0 !important;
overflow: hidden;
}
.module-page > .main-content > .left-panel,
.module-page > .main-content > .right-panel,
.module-page .task-list-wrap {
min-height: 0;
}
.module-page > .main-content > .left-panel,
.module-page .task-list-wrap {
overscroll-behavior: contain;
scrollbar-gutter: stable;
}
}
@media (max-width: 1100px) {
body:has(.module-page),
#app:has(> .module-page) {
min-height: 100vh;
min-height: 100dvh;
background: #1a1a1a;
}
}
.theme-light {