处理这个python打开紫鸟
This commit is contained in:
18
app/main.py
18
app/main.py
@@ -16,6 +16,7 @@ import webview
|
|||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
import requests
|
import requests
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
|
||||||
with open("version.txt","w",encoding="utf-8") as file:
|
with open("version.txt","w",encoding="utf-8") as file:
|
||||||
@@ -234,6 +235,21 @@ class WindowAPI:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return {'success': False, 'error': str(e)}
|
return {'success': False, 'error': str(e)}
|
||||||
|
|
||||||
|
def open_external_url(self, url):
|
||||||
|
if not url or not str(url).strip():
|
||||||
|
return {'success': False, 'error': '链接为空'}
|
||||||
|
target = str(url).strip()
|
||||||
|
try:
|
||||||
|
if sys.platform.startswith('win'):
|
||||||
|
os.startfile(target)
|
||||||
|
elif sys.platform == 'darwin':
|
||||||
|
subprocess.Popen(['open', target])
|
||||||
|
else:
|
||||||
|
subprocess.Popen(['xdg-open', target])
|
||||||
|
return {'success': True}
|
||||||
|
except Exception as e:
|
||||||
|
return {'success': False, 'error': str(e)}
|
||||||
|
|
||||||
def enqueue_json(self, data):
|
def enqueue_json(self, data):
|
||||||
"""保存任务到队列"""
|
"""保存任务到队列"""
|
||||||
try:
|
try:
|
||||||
@@ -350,7 +366,7 @@ def main():
|
|||||||
)
|
)
|
||||||
api = WindowAPI(window)
|
api = WindowAPI(window)
|
||||||
window.expose(api.close, api.minimize, api.maximize, api.toggle_maximize, generate_images, api.save_image, api.save_image_to_folder, api.select_folder, api.select_brand_xlsx_files, api.select_brand_folder,
|
window.expose(api.close, api.minimize, api.maximize, api.toggle_maximize, generate_images, api.save_image, api.save_image_to_folder, api.select_folder, api.select_brand_xlsx_files, api.select_brand_folder,
|
||||||
api.save_file_from_url, api.save_template_xlsx,api.save_template_zip,api.upload_file_to_java,api.enqueue_json)
|
api.save_file_from_url, api.save_template_xlsx,api.save_template_zip,api.upload_file_to_java,api.open_external_url,api.enqueue_json)
|
||||||
webview.start(
|
webview.start(
|
||||||
debug=True,
|
debug=True,
|
||||||
storage_path=cache_path,
|
storage_path=cache_path,
|
||||||
|
|||||||
@@ -99,27 +99,27 @@
|
|||||||
<div v-if="currentSectionItems.length" class="result-subsection">
|
<div v-if="currentSectionItems.length" class="result-subsection">
|
||||||
<div class="result-subsection-title">当前任务</div>
|
<div class="result-subsection-title">当前任务</div>
|
||||||
<ul class="task-list clean-result-list">
|
<ul class="task-list clean-result-list">
|
||||||
<li
|
<li v-for="item in currentSectionItems"
|
||||||
v-for="item in currentSectionItems"
|
|
||||||
:key="`current-${item.taskId || item.resultId || item.sourceFilename}`"
|
:key="`current-${item.taskId || item.resultId || item.sourceFilename}`"
|
||||||
|
class="task-item split-result-item">
|
||||||
class="task-item split-result-item"
|
|
||||||
>
|
|
||||||
<div class="left split-result-main">
|
<div class="left split-result-main">
|
||||||
<span class="id" :title="item.sourceFilename">{{ item.sourceFilename || '-' }}</span>
|
<span class="id" :title="item.sourceFilename">{{ item.sourceFilename || '-' }}</span>
|
||||||
<div class="files">店铺名:{{ item.shopName || '-' }}</div>
|
<div class="files">店铺名:{{ item.shopName || '-' }}</div>
|
||||||
<div class="files">匹配结果:{{ item.matched ? `已匹配 ${item.shopId || ''}` : '未匹配到紫鸟店铺' }}</div>
|
<div class="files">匹配结果:{{ item.matched ? `已匹配 ${item.shopId || ''}` : '未匹配到紫鸟店铺' }}</div>
|
||||||
<div v-if="item.platform" class="files">平台:{{ item.platform }}</div>
|
<div v-if="item.platform" class="files">平台:{{ item.platform }}</div>
|
||||||
<div v-if="getQueueStatus(item)" class="files">队列状态:{{ getQueueStatus(item) }}</div>
|
<div v-if="getQueueStatus(item)" class="files">队列状态:{{ getQueueStatus(item) }}</div>
|
||||||
<div v-if="item.countryCount !== undefined && item.matched" class="files">国家数:{{ item.countryCount }}</div>
|
<div v-if="item.countryCount !== undefined && item.matched" class="files">国家数:{{ item.countryCount
|
||||||
<div v-if="item.totalRows !== undefined && item.matched" class="time">去重后 {{ item.totalRows }} 条</div>
|
}}</div>
|
||||||
|
<div v-if="item.totalRows !== undefined && item.matched" class="time">去重后 {{ item.totalRows }} 条
|
||||||
|
</div>
|
||||||
<div v-if="shouldShowProgress(item)" class="delete-brand-progress-block">
|
<div v-if="shouldShowProgress(item)" class="delete-brand-progress-block">
|
||||||
<div class="delete-brand-progress-header">
|
<div class="delete-brand-progress-header">
|
||||||
<span>任务进度</span>
|
<span>任务进度</span>
|
||||||
<span>{{ formatProgressPercent(item.taskId!) }}%</span>
|
<span>{{ formatProgressPercent(item.taskId!) }}%</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="delete-brand-progress-bar">
|
<div class="delete-brand-progress-bar">
|
||||||
<div class="delete-brand-progress-bar-fill" :style="{ width: `${formatProgressPercent(item.taskId!)}%` }"></div>
|
<div class="delete-brand-progress-bar-fill"
|
||||||
|
:style="{ width: `${formatProgressPercent(item.taskId!)}%` }"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="files delete-brand-progress">{{ formatProgress(item.taskId!) }}</div>
|
<div class="files delete-brand-progress">{{ formatProgress(item.taskId!) }}</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -133,28 +133,15 @@
|
|||||||
<span class="status" :class="getTaskStatusInfo(item).className">
|
<span class="status" :class="getTaskStatusInfo(item).className">
|
||||||
{{ getTaskStatusInfo(item).text }}
|
{{ getTaskStatusInfo(item).text }}
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button v-if="item.openStoreUrl" type="button" class="download" @click="triggerItemRun(item)">
|
||||||
v-if="item.openStoreUrl"
|
|
||||||
type="button"
|
|
||||||
class="download"
|
|
||||||
@click="triggerItemRun(item)"
|
|
||||||
>
|
|
||||||
打开紫鸟
|
打开紫鸟
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button v-if="canDownloadTaskResult(item)" type="button" class="download"
|
||||||
v-if="canDownloadTaskResult(item)"
|
@click="downloadTaskResult(item)">
|
||||||
type="button"
|
|
||||||
class="download"
|
|
||||||
@click="downloadTaskResult(item)"
|
|
||||||
>
|
|
||||||
下载
|
下载
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button v-if="item.resultId" type="button" class="btn-delete"
|
||||||
v-if="item.resultId"
|
@click="deleteHistoryRecord(item.resultId)">
|
||||||
type="button"
|
|
||||||
class="btn-delete"
|
|
||||||
@click="deleteHistoryRecord(item.resultId)"
|
|
||||||
>
|
|
||||||
删除
|
删除
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -165,26 +152,26 @@
|
|||||||
<div v-if="historySectionItems.length" class="result-subsection">
|
<div v-if="historySectionItems.length" class="result-subsection">
|
||||||
<div class="result-subsection-title">历史记录</div>
|
<div class="result-subsection-title">历史记录</div>
|
||||||
<ul class="task-list clean-result-list">
|
<ul class="task-list clean-result-list">
|
||||||
<li
|
<li v-for="item in historySectionItems"
|
||||||
v-for="item in historySectionItems"
|
|
||||||
:key="`history-${item.taskId || item.resultId || item.sourceFilename}`"
|
:key="`history-${item.taskId || item.resultId || item.sourceFilename}`"
|
||||||
|
class="task-item split-result-item">
|
||||||
class="task-item split-result-item"
|
|
||||||
>
|
|
||||||
<div class="left split-result-main">
|
<div class="left split-result-main">
|
||||||
<span class="id" :title="item.sourceFilename">{{ item.sourceFilename || '-' }}</span>
|
<span class="id" :title="item.sourceFilename">{{ item.sourceFilename || '-' }}</span>
|
||||||
<div class="files">店铺名:{{ item.shopName || '-' }}</div>
|
<div class="files">店铺名:{{ item.shopName || '-' }}</div>
|
||||||
<div class="files">匹配结果:{{ item.matched ? `已匹配 ${item.shopId || ''}` : '未匹配到紫鸟店铺' }}</div>
|
<div class="files">匹配结果:{{ item.matched ? `已匹配 ${item.shopId || ''}` : '未匹配到紫鸟店铺' }}</div>
|
||||||
<div v-if="item.platform" class="files">平台:{{ item.platform }}</div>
|
<div v-if="item.platform" class="files">平台:{{ item.platform }}</div>
|
||||||
<div v-if="item.countryCount !== undefined && item.matched" class="files">国家数:{{ item.countryCount }}</div>
|
<div v-if="item.countryCount !== undefined && item.matched" class="files">国家数:{{ item.countryCount
|
||||||
<div v-if="item.totalRows !== undefined && item.matched" class="time">去重后 {{ item.totalRows }} 条</div>
|
}}</div>
|
||||||
|
<div v-if="item.totalRows !== undefined && item.matched" class="time">去重后 {{ item.totalRows }} 条
|
||||||
|
</div>
|
||||||
<div v-if="shouldShowProgress(item)" class="delete-brand-progress-block">
|
<div v-if="shouldShowProgress(item)" class="delete-brand-progress-block">
|
||||||
<div class="delete-brand-progress-header">
|
<div class="delete-brand-progress-header">
|
||||||
<span>任务进度</span>
|
<span>任务进度</span>
|
||||||
<span>{{ formatProgressPercent(item.taskId!) }}%</span>
|
<span>{{ formatProgressPercent(item.taskId!) }}%</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="delete-brand-progress-bar">
|
<div class="delete-brand-progress-bar">
|
||||||
<div class="delete-brand-progress-bar-fill" :style="{ width: `${formatProgressPercent(item.taskId!)}%` }"></div>
|
<div class="delete-brand-progress-bar-fill"
|
||||||
|
:style="{ width: `${formatProgressPercent(item.taskId!)}%` }"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="files delete-brand-progress">{{ formatProgress(item.taskId!) }}</div>
|
<div class="files delete-brand-progress">{{ formatProgress(item.taskId!) }}</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -199,20 +186,12 @@
|
|||||||
{{ getTaskStatusInfo(item).text }}
|
{{ getTaskStatusInfo(item).text }}
|
||||||
</span>
|
</span>
|
||||||
<!-- 历史记录区不再显示打开紫鸟按钮 -->
|
<!-- 历史记录区不再显示打开紫鸟按钮 -->
|
||||||
<button
|
<button v-if="canDownloadTaskResult(item)" type="button" class="download"
|
||||||
v-if="canDownloadTaskResult(item)"
|
@click="downloadTaskResult(item)">
|
||||||
type="button"
|
|
||||||
class="download"
|
|
||||||
@click="downloadTaskResult(item)"
|
|
||||||
>
|
|
||||||
下载
|
下载
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button v-if="item.resultId" type="button" class="btn-delete"
|
||||||
v-if="item.resultId"
|
@click="deleteHistoryRecord(item.resultId)">
|
||||||
type="button"
|
|
||||||
class="btn-delete"
|
|
||||||
@click="deleteHistoryRecord(item.resultId)"
|
|
||||||
>
|
|
||||||
删除
|
删除
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -303,9 +282,9 @@ function saveSessionTask(taskId: number, items: DeleteBrandResultItem[], pushRes
|
|||||||
const validItems = items.filter(i => i.matched !== false && i.success !== false)
|
const validItems = items.filter(i => i.matched !== false && i.success !== false)
|
||||||
if (validItems.length === 0) return
|
if (validItems.length === 0) return
|
||||||
|
|
||||||
const newTask: StoredCurrentTask = {
|
const newTask: StoredCurrentTask = {
|
||||||
taskId,
|
taskId,
|
||||||
items: validItems,
|
items: validItems,
|
||||||
createdAt: Date.now(),
|
createdAt: Date.now(),
|
||||||
queuePushResult: pushResult,
|
queuePushResult: pushResult,
|
||||||
queuePayloadText: payloadText
|
queuePayloadText: payloadText
|
||||||
@@ -454,7 +433,7 @@ function getQueueStatus(item: DeleteBrandResultItem) {
|
|||||||
if (!taskId) return ''
|
if (!taskId) return ''
|
||||||
const sessionTask = sessionTasks.value.find(t => t.taskId === taskId)
|
const sessionTask = sessionTasks.value.find(t => t.taskId === taskId)
|
||||||
if (!sessionTask) return ''
|
if (!sessionTask) return ''
|
||||||
|
|
||||||
const status = getTaskStatus(taskId)
|
const status = getTaskStatus(taskId)
|
||||||
if (status === 'SUCCESS' || status === 'FAILED' || item.taskStatus === 'SUCCESS' || item.taskStatus === 'FAILED') {
|
if (status === 'SUCCESS' || status === 'FAILED' || item.taskStatus === 'SUCCESS' || item.taskStatus === 'FAILED') {
|
||||||
return '已被全局判定为终态'
|
return '已被全局判定为终态'
|
||||||
@@ -463,7 +442,7 @@ function getQueueStatus(item: DeleteBrandResultItem) {
|
|||||||
const sessionItem = sessionTask.items.find(i => i.resultId === item.resultId || i.sourceFilename === item.sourceFilename)
|
const sessionItem = sessionTask.items.find(i => i.resultId === item.resultId || i.sourceFilename === item.sourceFilename)
|
||||||
|
|
||||||
const info = taskDetails.value[taskId]?.line_progress
|
const info = taskDetails.value[taskId]?.line_progress
|
||||||
|
|
||||||
// 此时这个特定文件是否正好被后端报了进度名
|
// 此时这个特定文件是否正好被后端报了进度名
|
||||||
if (status === 'RUNNING' && info?.has_progress && info?.info?.file_name === item.sourceFilename) {
|
if (status === 'RUNNING' && info?.has_progress && info?.info?.file_name === item.sourceFilename) {
|
||||||
return '处理中'
|
return '处理中'
|
||||||
@@ -489,7 +468,7 @@ function getQueueStatus(item: DeleteBrandResultItem) {
|
|||||||
function canDownloadTaskResult(item: DeleteBrandResultItem) {
|
function canDownloadTaskResult(item: DeleteBrandResultItem) {
|
||||||
// 核心修复:只要单个项目已经成功(后端返回了下载地址或标记为成功),就允许下载,哪怕整包任务还在“执行中”
|
// 核心修复:只要单个项目已经成功(后端返回了下载地址或标记为成功),就允许下载,哪怕整包任务还在“执行中”
|
||||||
if (item.downloadUrl || item.success) return true
|
if (item.downloadUrl || item.success) return true
|
||||||
|
|
||||||
const taskId = item.taskId
|
const taskId = item.taskId
|
||||||
if (!taskId) return false
|
if (!taskId) return false
|
||||||
const status = taskDetails.value[taskId]?.task?.status
|
const status = taskDetails.value[taskId]?.task?.status
|
||||||
@@ -523,7 +502,7 @@ function scheduleNextPoll(immediate = false) {
|
|||||||
clearTimeout(pollTimer.value)
|
clearTimeout(pollTimer.value)
|
||||||
pollTimer.value = null
|
pollTimer.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
const executePoll = async () => {
|
const executePoll = async () => {
|
||||||
pollTimer.value = null
|
pollTimer.value = null
|
||||||
if (pollingInFlight.value) {
|
if (pollingInFlight.value) {
|
||||||
@@ -723,7 +702,7 @@ async function loadHistory() {
|
|||||||
const response = await getDeleteBrandHistory()
|
const response = await getDeleteBrandHistory()
|
||||||
historyItems.value = normalizeDeleteBrandItems(response.items || [])
|
historyItems.value = normalizeDeleteBrandItems(response.items || [])
|
||||||
syncResultState()
|
syncResultState()
|
||||||
|
|
||||||
// 关键:页面加载历史后,立刻拉取详情触发状态机
|
// 关键:页面加载历史后,立刻拉取详情触发状态机
|
||||||
await refreshTaskDetails()
|
await refreshTaskDetails()
|
||||||
ensurePolling(true)
|
ensurePolling(true)
|
||||||
@@ -793,8 +772,17 @@ async function triggerItemRun(item: DeleteBrandResultItem) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const api = getPywebviewApi()
|
||||||
if (item.openStoreUrl) {
|
if (item.openStoreUrl) {
|
||||||
window.open(item.openStoreUrl, '_blank')
|
if (api?.open_external_url) {
|
||||||
|
const openResult = await api.open_external_url(item.openStoreUrl)
|
||||||
|
if (!openResult?.success) {
|
||||||
|
ElMessage.error(openResult?.error || '打开紫鸟失败')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
window.open(item.openStoreUrl, '_self')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const taskId = item.taskId
|
const taskId = item.taskId
|
||||||
@@ -803,7 +791,6 @@ async function triggerItemRun(item: DeleteBrandResultItem) {
|
|||||||
const sessionTask = sessionTasks.value.find(t => t.taskId === taskId)
|
const sessionTask = sessionTasks.value.find(t => t.taskId === taskId)
|
||||||
if (!sessionTask) return
|
if (!sessionTask) return
|
||||||
|
|
||||||
const api = getPywebviewApi()
|
|
||||||
if (!api?.enqueue_json) {
|
if (!api?.enqueue_json) {
|
||||||
ElMessage.error('当前环境未启用 pywebview enqueue_json(浏览器环境不会推送)')
|
ElMessage.error('当前环境未启用 pywebview enqueue_json(浏览器环境不会推送)')
|
||||||
return
|
return
|
||||||
@@ -812,7 +799,7 @@ async function triggerItemRun(item: DeleteBrandResultItem) {
|
|||||||
const payload = {
|
const payload = {
|
||||||
type: 'delete-brand-run',
|
type: 'delete-brand-run',
|
||||||
ts: Date.now(),
|
ts: Date.now(),
|
||||||
data: {
|
data: {
|
||||||
taskId: taskId,
|
taskId: taskId,
|
||||||
items: [item]
|
items: [item]
|
||||||
},
|
},
|
||||||
@@ -823,14 +810,14 @@ async function triggerItemRun(item: DeleteBrandResultItem) {
|
|||||||
let qpr = ''
|
let qpr = ''
|
||||||
if (pushResult?.success) {
|
if (pushResult?.success) {
|
||||||
qpr = `已单独推送文件 ${item.sourceFilename},当前队列长度:${pushResult.queue_size ?? '-'}`
|
qpr = `已单独推送文件 ${item.sourceFilename},当前队列长度:${pushResult.queue_size ?? '-'}`
|
||||||
|
|
||||||
// Mark the item as pushed
|
// Mark the item as pushed
|
||||||
const sessionItem = sessionTask.items.find(i => i.resultId === item.resultId || i.sourceFilename === item.sourceFilename)
|
const sessionItem = sessionTask.items.find(i => i.resultId === item.resultId || i.sourceFilename === item.sourceFilename)
|
||||||
if (sessionItem) {
|
if (sessionItem) {
|
||||||
(sessionItem as any)._pushed = true
|
(sessionItem as any)._pushed = true
|
||||||
}
|
}
|
||||||
saveSessionTask(taskId, sessionTask.items, qpr, queuePayloadText.value)
|
saveSessionTask(taskId, sessionTask.items, qpr, queuePayloadText.value)
|
||||||
|
|
||||||
ensurePolling(true)
|
ensurePolling(true)
|
||||||
} else {
|
} else {
|
||||||
qpr = `推送到 Python 队列失败:${pushResult?.error || '未知错误'}`
|
qpr = `推送到 Python 队列失败:${pushResult?.error || '未知错误'}`
|
||||||
@@ -872,71 +859,481 @@ onUnmounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.queue-debug-card { margin-top: 18px; padding: 14px; border: 1px solid #2a2a2a; border-radius: 10px; background: #202020; }
|
.queue-debug-card {
|
||||||
.queue-debug-title { margin-bottom: 8px; }
|
margin-top: 18px;
|
||||||
.queue-debug-line { color: #b8c1cc; font-size: 12px; line-height: 1.6; margin-bottom: 8px; }
|
padding: 14px;
|
||||||
.queue-debug-payload { margin: 0; max-height: 220px; overflow: auto; padding: 10px; border-radius: 8px; background: #141414; color: #8fd3ff; font-size: 12px; line-height: 1.5; white-space: pre-wrap; word-break: break-all; }
|
border: 1px solid #2a2a2a;
|
||||||
.module-page { min-height: 100vh; background: #1a1a1a; }
|
border-radius: 10px;
|
||||||
.main-content { display: flex; min-height: calc(100vh - 56px); height: calc(100vh - 56px); }
|
background: #202020;
|
||||||
.left-panel { width: 380px; background: #1e1e1e; padding: 20px; overflow-y: auto; border-right: 1px solid #2a2a2a; }
|
}
|
||||||
.right-panel { flex: 1; display: flex; flex-direction: column; min-width: 0; background: #1a1a1a; }
|
|
||||||
.section-title { font-size: 13px; color: #bbb; margin-bottom: 10px; }
|
.queue-debug-title {
|
||||||
.upload-zone { border: 1px dashed #3a3a3a; border-radius: 10px; padding: 24px; text-align: center; background: #252525; margin-bottom: 20px; transition: all 0.2s ease; }
|
margin-bottom: 8px;
|
||||||
.upload-zone:hover { border-color: #3498db; background: #2a2a2a; }
|
}
|
||||||
.hint { color: #888; font-size: 13px; margin-bottom: 12px; line-height: 1.5; }
|
|
||||||
.btns { display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; }
|
.queue-debug-line {
|
||||||
.opt-btn, .btn-run, .download, .btn-delete { border: none; cursor: pointer; transition: all 0.2s ease; }
|
color: #b8c1cc;
|
||||||
.opt-btn { padding: 8px 16px; font-size: 13px; color: #ccc; background: #2a2a2a; border: 1px solid #3a3a3a; border-radius: 6px; }
|
font-size: 12px;
|
||||||
.opt-btn:hover { color: #3498db; background: #333; border-color: #3498db; }
|
line-height: 1.6;
|
||||||
.selected-files { margin-top: 14px; font-size: 12px; color: #888; max-height: 112px; overflow-y: auto; text-align: left; }
|
margin-bottom: 8px;
|
||||||
.selected-files span { display: block; margin: 4px 0; word-break: break-all; }
|
}
|
||||||
.more-line { color: #b8c1cc; }
|
|
||||||
.split-selected-files { max-height: 120px; min-height: 72px; padding-right: 4px; }
|
.queue-debug-payload {
|
||||||
.option-group { margin-bottom: 20px; }
|
margin: 0;
|
||||||
.radio-item { display: flex; align-items: flex-start; gap: 10px; cursor: pointer; padding: 10px 12px; border-radius: 8px; background: #252525; border: 1px solid #2a2a2a; margin-bottom: 8px; transition: all 0.2s ease; }
|
max-height: 220px;
|
||||||
.radio-item:hover, .radio-item.active { background: #2a2a2a; border-color: #3a3a3a; }
|
overflow: auto;
|
||||||
.radio-item input { margin-top: 3px; }
|
padding: 10px;
|
||||||
.label { font-weight: 500; color: #e0e0e0; font-size: 13px; }
|
border-radius: 8px;
|
||||||
.desc { font-size: 12px; color: #888; margin-top: 2px; line-height: 1.5; font-weight: 400; }
|
background: #141414;
|
||||||
.run-row { display: flex; align-items: center; flex-wrap: wrap; gap: 10px 12px; margin-top: 16px; }
|
color: #8fd3ff;
|
||||||
.btn-run { padding: 10px 22px; background: #3498db; color: #fff; border-radius: 8px; font-size: 14px; }
|
font-size: 12px;
|
||||||
.btn-run:hover { background: #2980b9; }
|
line-height: 1.5;
|
||||||
.btn-run:disabled { background: #555; color: #999; cursor: not-allowed; }
|
white-space: pre-wrap;
|
||||||
.loading-msg { color: #3498db; font-size: 13px; }
|
word-break: break-all;
|
||||||
.panel-header { padding: 16px 20px; border-bottom: 1px solid #2a2a2a; font-size: 15px; font-weight: 600; color: #ddd; }
|
}
|
||||||
.task-list-wrap { flex: 1; overflow-y: auto; padding: 16px; }
|
|
||||||
.task-list { list-style: none; margin: 0; padding: 0; }
|
.module-page {
|
||||||
.task-item { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 14px 16px; border: 1px solid #2a2a2a; border-radius: 8px; margin-bottom: 10px; background: #1e1e1e; }
|
min-height: 100vh;
|
||||||
.split-result-item { align-items: flex-start; }
|
background: #1a1a1a;
|
||||||
.left { flex: 1; min-width: 0; }
|
}
|
||||||
.split-result-main { display: flex; flex-direction: column; gap: 4px; }
|
|
||||||
.id { display: inline-block; font-weight: 600; color: #e0e0e0; font-size: 13px; }
|
.main-content {
|
||||||
.files { font-size: 12px; color: #888; margin-top: 4px; }
|
display: flex;
|
||||||
.time { font-size: 12px; color: #666; margin-top: 2px; }
|
min-height: calc(100vh - 56px);
|
||||||
.split-entry-list { line-height: 1.6; word-break: break-all; max-width: 100%; }
|
height: calc(100vh - 56px);
|
||||||
.delete-brand-preview { white-space: normal; }
|
}
|
||||||
.delete-brand-progress-block { margin-top: 8px; }
|
|
||||||
.delete-brand-progress-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 6px; font-size: 12px; color: #8fbfff; }
|
.left-panel {
|
||||||
.delete-brand-progress-bar { width: 100%; height: 8px; border-radius: 999px; background: rgba(255, 255, 255, 0.08); overflow: hidden; }
|
width: 380px;
|
||||||
.delete-brand-progress-bar-fill { height: 100%; border-radius: inherit; background: linear-gradient(90deg, #2d8cf0 0%, #4db3ff 100%); transition: width 0.3s ease; }
|
background: #1e1e1e;
|
||||||
.delete-brand-progress { color: #69b6ff; line-height: 1.6; white-space: normal; margin-top: 6px; }
|
padding: 20px;
|
||||||
.task-right { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; justify-content: flex-end; }
|
overflow-y: auto;
|
||||||
.split-result-actions { flex-shrink: 0; min-width: 180px; justify-content: flex-end; align-self: center; }
|
border-right: 1px solid #2a2a2a;
|
||||||
.status { padding: 4px 10px; border-radius: 6px; font-size: 12px; white-space: nowrap; }
|
}
|
||||||
.status.success { background: rgba(46, 204, 113, 0.18); color: #2ecc71; }
|
|
||||||
.status.failed { background: rgba(231, 76, 60, 0.18); color: #ff6b6b; }
|
.right-panel {
|
||||||
.status.running { background: rgba(52, 152, 219, 0.18); color: #3498db; }
|
flex: 1;
|
||||||
.download { padding: 6px 10px; border-radius: 6px; font-size: 12px; background: rgba(52, 152, 219, 0.18); color: #69b6ff; }
|
display: flex;
|
||||||
.download:hover { background: rgba(52, 152, 219, 0.28); }
|
flex-direction: column;
|
||||||
.btn-delete { padding: 6px 10px; border-radius: 6px; font-size: 12px; background: rgba(231, 76, 60, 0.12); color: #ff8f8f; }
|
min-width: 0;
|
||||||
.btn-delete:hover { background: rgba(231, 76, 60, 0.22); }
|
background: #1a1a1a;
|
||||||
.empty-tasks { color: #666; font-size: 13px; padding: 24px 8px; }
|
}
|
||||||
.clean-placeholder { max-height: 112px; }
|
|
||||||
.clean-result-summary { display: grid; grid-template-columns: repeat(3, minmax(0, 220px)); gap: 16px; margin-bottom: 18px; }
|
.section-title {
|
||||||
.summary-card { padding: 16px 18px; border: 1px solid #2a2a2a; border-radius: 10px; background: #1e1e1e; }
|
font-size: 13px;
|
||||||
.summary-card strong { display: block; margin-top: 8px; font-size: 24px; color: #eaf4ff; }
|
color: #bbb;
|
||||||
.summary-label { font-size: 12px; color: #8d8d8d; }
|
margin-bottom: 10px;
|
||||||
.result-list-wrap { border: 1px solid #2a2a2a; border-radius: 10px; background: #1e1e1e; min-height: 260px; }
|
}
|
||||||
.result-list-header { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 14px 16px; border-bottom: 1px solid #2a2a2a; font-size: 14px; color: #ddd; }
|
|
||||||
@media (max-width: 1100px) { .main-content { flex-direction: column; height: auto; } .left-panel { width: 100%; border-right: none; border-bottom: 1px solid #2a2a2a; } .right-panel { min-height: 420px; } .task-item { flex-direction: column; align-items: flex-start; } .task-right { width: 100%; justify-content: flex-start; } .split-result-actions { min-width: 0; align-self: flex-start; } .clean-result-summary { grid-template-columns: 1fr; } }
|
.upload-zone {
|
||||||
|
border: 1px dashed #3a3a3a;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 24px;
|
||||||
|
text-align: center;
|
||||||
|
background: #252525;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-zone:hover {
|
||||||
|
border-color: #3498db;
|
||||||
|
background: #2a2a2a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint {
|
||||||
|
color: #888;
|
||||||
|
font-size: 13px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btns {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opt-btn,
|
||||||
|
.btn-run,
|
||||||
|
.download,
|
||||||
|
.btn-delete {
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opt-btn {
|
||||||
|
padding: 8px 16px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #ccc;
|
||||||
|
background: #2a2a2a;
|
||||||
|
border: 1px solid #3a3a3a;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opt-btn:hover {
|
||||||
|
color: #3498db;
|
||||||
|
background: #333;
|
||||||
|
border-color: #3498db;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-files {
|
||||||
|
margin-top: 14px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #888;
|
||||||
|
max-height: 112px;
|
||||||
|
overflow-y: auto;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-files span {
|
||||||
|
display: block;
|
||||||
|
margin: 4px 0;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.more-line {
|
||||||
|
color: #b8c1cc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.split-selected-files {
|
||||||
|
max-height: 120px;
|
||||||
|
min-height: 72px;
|
||||||
|
padding-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-group {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #252525;
|
||||||
|
border: 1px solid #2a2a2a;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-item:hover,
|
||||||
|
.radio-item.active {
|
||||||
|
background: #2a2a2a;
|
||||||
|
border-color: #3a3a3a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-item input {
|
||||||
|
margin-top: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #e0e0e0;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desc {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #888;
|
||||||
|
margin-top: 2px;
|
||||||
|
line-height: 1.5;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.run-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px 12px;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-run {
|
||||||
|
padding: 10px 22px;
|
||||||
|
background: #3498db;
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-run:hover {
|
||||||
|
background: #2980b9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-run:disabled {
|
||||||
|
background: #555;
|
||||||
|
color: #999;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-msg {
|
||||||
|
color: #3498db;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-header {
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-bottom: 1px solid #2a2a2a;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-list-wrap {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-list {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
border: 1px solid #2a2a2a;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
background: #1e1e1e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.split-result-item {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.split-result-main {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.id {
|
||||||
|
display: inline-block;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #e0e0e0;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.files {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #888;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.split-entry-list {
|
||||||
|
line-height: 1.6;
|
||||||
|
word-break: break-all;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-brand-preview {
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-brand-progress-block {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-brand-progress-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #8fbfff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-brand-progress-bar {
|
||||||
|
width: 100%;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-brand-progress-bar-fill {
|
||||||
|
height: 100%;
|
||||||
|
border-radius: inherit;
|
||||||
|
background: linear-gradient(90deg, #2d8cf0 0%, #4db3ff 100%);
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-brand-progress {
|
||||||
|
color: #69b6ff;
|
||||||
|
line-height: 1.6;
|
||||||
|
white-space: normal;
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.split-result-actions {
|
||||||
|
flex-shrink: 0;
|
||||||
|
min-width: 180px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status.success {
|
||||||
|
background: rgba(46, 204, 113, 0.18);
|
||||||
|
color: #2ecc71;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status.failed {
|
||||||
|
background: rgba(231, 76, 60, 0.18);
|
||||||
|
color: #ff6b6b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status.running {
|
||||||
|
background: rgba(52, 152, 219, 0.18);
|
||||||
|
color: #3498db;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download {
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
background: rgba(52, 152, 219, 0.18);
|
||||||
|
color: #69b6ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download:hover {
|
||||||
|
background: rgba(52, 152, 219, 0.28);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-delete {
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
background: rgba(231, 76, 60, 0.12);
|
||||||
|
color: #ff8f8f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-delete:hover {
|
||||||
|
background: rgba(231, 76, 60, 0.22);
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-tasks {
|
||||||
|
color: #666;
|
||||||
|
font-size: 13px;
|
||||||
|
padding: 24px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clean-placeholder {
|
||||||
|
max-height: 112px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clean-result-summary {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 220px));
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-card {
|
||||||
|
padding: 16px 18px;
|
||||||
|
border: 1px solid #2a2a2a;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #1e1e1e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-card strong {
|
||||||
|
display: block;
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 24px;
|
||||||
|
color: #eaf4ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #8d8d8d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-list-wrap {
|
||||||
|
border: 1px solid #2a2a2a;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #1e1e1e;
|
||||||
|
min-height: 260px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-list-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
border-bottom: 1px solid #2a2a2a;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1100px) {
|
||||||
|
.main-content {
|
||||||
|
flex-direction: column;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-panel {
|
||||||
|
width: 100%;
|
||||||
|
border-right: none;
|
||||||
|
border-bottom: 1px solid #2a2a2a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-panel {
|
||||||
|
min-height: 420px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-item {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-right {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.split-result-actions {
|
||||||
|
min-width: 0;
|
||||||
|
align-self: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clean-result-summary {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export interface PywebviewApi {
|
|||||||
save_template_xlsx?: () => Promise<{ success: boolean; path?: string; error?: string }>
|
save_template_xlsx?: () => Promise<{ success: boolean; path?: string; error?: string }>
|
||||||
save_template_zip?: () => Promise<{ success: boolean; path?: string; error?: string }>
|
save_template_zip?: () => Promise<{ success: boolean; path?: string; error?: string }>
|
||||||
enqueue_json?: (data: unknown) => Promise<{ success: boolean; queue_size?: number; error?: string }>
|
enqueue_json?: (data: unknown) => Promise<{ success: boolean; queue_size?: number; error?: string }>
|
||||||
|
open_external_url?: (url: string) => Promise<{ success: boolean; error?: string }>
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
|||||||
Reference in New Issue
Block a user