feat: 支持 CellData 文件夹上传处理
This commit is contained in:
@@ -5,13 +5,13 @@ from datetime import datetime
|
||||
from pathlib import Path
|
||||
from threading import Thread
|
||||
|
||||
from fastapi import APIRouter, Body, HTTPException
|
||||
from fastapi import APIRouter, Body, File, HTTPException, UploadFile
|
||||
|
||||
from app import state
|
||||
from app.api.routers.task_runtime import set_task_stage
|
||||
from app.config import AppConfig, CACHE_DIR
|
||||
from app.processor import ProcessLogger
|
||||
from app.services.cell_data import refresh_cell_data
|
||||
from app.services.cell_data import CellDataProcessor, refresh_cell_data
|
||||
|
||||
router = APIRouter(tags=["cell-data"])
|
||||
|
||||
@@ -57,6 +57,67 @@ async def start_cell_data_processing():
|
||||
return {"success": True, "message": "CellData 处理已启动", "task_id": task_id, "stage": current_stage}
|
||||
|
||||
|
||||
@router.post("/api/cell-data/process/upload")
|
||||
async def upload_and_start_cell_data_processing(files: list[UploadFile] = File(...)):
|
||||
if not files:
|
||||
raise HTTPException(status_code=400, detail="没有上传文件")
|
||||
if state.global_task_lock["locked"]:
|
||||
raise HTTPException(status_code=409, detail="已有任务在运行,请等待当前任务完成")
|
||||
|
||||
task_id = "cell_data_" + datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
work_dir = CACHE_DIR / task_id
|
||||
upload_dir = work_dir / "uploads"
|
||||
upload_dir.mkdir(parents=True, exist_ok=True)
|
||||
saved_count = 0
|
||||
for file in files:
|
||||
if not file.filename:
|
||||
continue
|
||||
target = upload_dir / file.filename.replace("\\", "/")
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
target.write_bytes(await file.read())
|
||||
saved_count += 1
|
||||
if saved_count == 0:
|
||||
raise HTTPException(status_code=400, detail="没有有效上传文件")
|
||||
|
||||
logs: list[str] = []
|
||||
current_stage = "parsing"
|
||||
|
||||
def log_callback(message: str) -> None:
|
||||
logs.append(message)
|
||||
set_task_stage(task_id, current_stage, logs)
|
||||
|
||||
def stage_callback(stage: str) -> None:
|
||||
nonlocal current_stage
|
||||
current_stage = stage
|
||||
set_task_stage(task_id, current_stage, logs)
|
||||
|
||||
logger = ProcessLogger(
|
||||
log_file=work_dir / "log.txt",
|
||||
callback=log_callback,
|
||||
stage_callback=stage_callback,
|
||||
)
|
||||
app_config = state.current_config()
|
||||
state.processing_tasks[task_id] = {"logs": [], "status": "processing", "stage": current_stage}
|
||||
state.global_task_lock.update(
|
||||
{
|
||||
"locked": True,
|
||||
"task_id": task_id,
|
||||
"stage": current_stage,
|
||||
"started_at": datetime.now().isoformat(),
|
||||
}
|
||||
)
|
||||
|
||||
thread = Thread(target=_run_uploaded_cell_data_processing, args=(task_id, upload_dir, work_dir, logger, app_config), daemon=True)
|
||||
thread.start()
|
||||
return {
|
||||
"success": True,
|
||||
"message": "CellData 处理已启动",
|
||||
"task_id": task_id,
|
||||
"stage": current_stage,
|
||||
"file_count": saved_count,
|
||||
}
|
||||
|
||||
|
||||
@router.post("/api/cell-data/process/status")
|
||||
async def get_cell_data_processing_status(task_id: str = Body(..., embed=True)):
|
||||
if task_id in state.processing_tasks:
|
||||
@@ -101,3 +162,33 @@ def _run_cell_data_processing(task_id: str, work_dir: Path, logger: ProcessLogge
|
||||
}
|
||||
finally:
|
||||
state.reset_task_lock()
|
||||
|
||||
|
||||
def _run_uploaded_cell_data_processing(task_id: str, upload_dir: Path, work_dir: Path, logger: ProcessLogger, app_config: AppConfig) -> None:
|
||||
started = time.time()
|
||||
try:
|
||||
result = CellDataProcessor(app_config, work_dir, logger).run_local(upload_dir)
|
||||
elapsed = round(time.time() - started, 2)
|
||||
state.processing_tasks[task_id] = {
|
||||
"logs": logger.get_logs(),
|
||||
"status": "completed",
|
||||
"stage": "completed",
|
||||
"elapsed_time": elapsed,
|
||||
"result": {
|
||||
"selected_files": result.selected_files,
|
||||
"parsed_rows": result.parsed_rows,
|
||||
"imported_rows": result.imported_rows,
|
||||
"skipped_rows": result.skipped_rows,
|
||||
},
|
||||
}
|
||||
except Exception as exc:
|
||||
logger.error(str(exc))
|
||||
state.processing_tasks[task_id] = {
|
||||
"logs": logger.get_logs(),
|
||||
"status": "failed",
|
||||
"stage": "failed",
|
||||
"error": str(exc),
|
||||
"elapsed_time": round(time.time() - started, 2),
|
||||
}
|
||||
finally:
|
||||
state.reset_task_lock()
|
||||
|
||||
@@ -84,6 +84,24 @@ class CellDataProcessor:
|
||||
self._log(f"CellData 导入完成,共 {result.imported_rows} 行")
|
||||
return result
|
||||
|
||||
def run_local(self, upload_root: Path) -> CellDataResult:
|
||||
self._set_stage("locating")
|
||||
local_files = self._select_local_zip_files(upload_root)
|
||||
if not local_files:
|
||||
raise RuntimeError("未找到可处理的 CellData ZIP 文件")
|
||||
self._log(f"已选择 {len(local_files)} 个 CellData ZIP 文件")
|
||||
|
||||
self._set_stage("parsing")
|
||||
result = CellDataResult(selected_files=len(local_files))
|
||||
rows = self._parse_zip_files(local_files, result)
|
||||
if not rows:
|
||||
raise RuntimeError("CellData ZIP 中未解析到有效数据")
|
||||
|
||||
self._set_stage("importing")
|
||||
result.imported_rows = self._replace_cellinfo(rows)
|
||||
self._log(f"CellData 导入完成,共 {result.imported_rows} 行")
|
||||
return result
|
||||
|
||||
def _select_latest_zip_files(self) -> list[SelectedZip]:
|
||||
selected: list[SelectedZip] = []
|
||||
for template in self.config.scan_paths:
|
||||
@@ -182,6 +200,47 @@ class CellDataProcessor:
|
||||
ssh.close()
|
||||
return local_files
|
||||
|
||||
def _select_local_zip_files(self, upload_root: Path) -> list[tuple[SelectedZip, Path]]:
|
||||
grouped: dict[str, list[Path]] = {}
|
||||
for path in upload_root.rglob("*.zip"):
|
||||
if not self.file_name_re.search(path.name):
|
||||
continue
|
||||
try:
|
||||
parent = str(path.parent.relative_to(upload_root)).replace("\\", "/")
|
||||
except ValueError:
|
||||
parent = ""
|
||||
grouped.setdefault("" if parent == "." else parent, []).append(path)
|
||||
|
||||
selected: list[tuple[SelectedZip, Path]] = []
|
||||
for parent, paths in sorted(grouped.items(), key=lambda item: item[0]):
|
||||
candidates = []
|
||||
for path in paths:
|
||||
match = self.file_time_re.search(path.name)
|
||||
if match:
|
||||
candidates.append((match.group("timestamp"), path))
|
||||
if not candidates:
|
||||
continue
|
||||
timestamp, path = max(candidates, key=lambda item: item[0])
|
||||
band = Path(parent).name if parent else ""
|
||||
selected_zip = SelectedZip(
|
||||
scan_path=str(upload_root),
|
||||
band=band,
|
||||
remote_file=RemoteFileInfo(
|
||||
path=str(path),
|
||||
relative_path=str(path.relative_to(upload_root)).replace("\\", "/"),
|
||||
parent=parent,
|
||||
name=path.name,
|
||||
size=path.stat().st_size,
|
||||
),
|
||||
timestamp=timestamp,
|
||||
)
|
||||
if not band:
|
||||
self._log(f"未从目录名识别频段: {path.name}")
|
||||
else:
|
||||
self._log(f"{band}: {path.name}")
|
||||
selected.append((selected_zip, path))
|
||||
return selected
|
||||
|
||||
@staticmethod
|
||||
def _download_result():
|
||||
from app.services.remote_download import RemoteDownloadResult
|
||||
@@ -194,15 +253,22 @@ class CellDataProcessor:
|
||||
rows_by_key: dict[str, dict[str, str]] = {}
|
||||
for selected, local_path in local_files:
|
||||
with zipfile.ZipFile(local_path) as zf:
|
||||
for source in mapping["sources"]:
|
||||
if source["band"] != selected.band:
|
||||
sources = list(mapping["sources"])
|
||||
for info in zf.infolist():
|
||||
name = Path(info.filename).name
|
||||
if not name.lower().endswith(".csv"):
|
||||
continue
|
||||
for info in zf.infolist():
|
||||
name = Path(info.filename).name
|
||||
if not name.lower().endswith(".csv"):
|
||||
continue
|
||||
if not name.startswith(source["file_prefix"]):
|
||||
continue
|
||||
matching_sources = [
|
||||
source
|
||||
for source in sources
|
||||
if name.startswith(source["file_prefix"]) and (not selected.band or source["band"] == selected.band)
|
||||
]
|
||||
if not matching_sources:
|
||||
continue
|
||||
if not selected.band and len(matching_sources) > 1:
|
||||
self._log(f"跳过无法识别频段的文件: {name}")
|
||||
continue
|
||||
for source in matching_sources:
|
||||
raw = zf.read(info.filename)
|
||||
text = self._decode_csv(raw)
|
||||
reader = csv.DictReader(text.splitlines())
|
||||
|
||||
@@ -753,3 +753,16 @@
|
||||
- 接口:新增 `app/api/routers/cell_data.py`,提供 `POST /api/cell-data/process/start` 与 `/status`,数据处理页新增独立 CellData 卡片,可只刷新 CellData,不跑容量处理。
|
||||
- 接入:本地上传、远程手动和自动调度入口都会在容量处理前调用 CellData 预处理;CellData 单独处理和容量处理共用现有全局任务锁,避免并发写库。
|
||||
- 验证:`python -m compileall -q app` 通过;`frontend` `npm run build` 通过;远程定位可从最新年份 `2026年/300表` 选出 `2.6G` 与 `700M` 各自最新 `Result_300` ZIP;用 SFTP MCP 读取的小样本 ZIP 验证解析和入库,`celldata.cellinfo` 写入 1 行且中文字段正常(样本 `CGI=460-00-12683845-1`)。
|
||||
|
||||
## 2026-06-25:CellData 卡片支持本地上传处理
|
||||
|
||||
- 数据处理页 CellData 卡片改为与主上传区一致的拖拽/点击上传样式,支持拖入或选择多个 `Result_300_*.zip`,也支持选择文件夹上传。
|
||||
- 新增 `/api/cell-data/process/upload`,上传后复用 CellData 解析入库逻辑;若 ZIP 不在 `700M/2.6G` 等目录下且无法识别频段,会跳过无法唯一匹配的 CSV。
|
||||
- CellData 卡片仍保留「远程刷新」操作,用于按系统设置中的 CellData SFTP/FTP 配置拉取处理。
|
||||
- 验证:`frontend` `npm run build` 通过;构建产物已清理。
|
||||
|
||||
## 2026-06-25:数据处理卡片补说明按钮并规范 CellData 上传
|
||||
|
||||
- 数据处理页的容量数据卡片与 CellData 卡片左上角均显示数据类型,右上角均提供说明图标按钮,点击后以小弹窗展示所需文件格式和目录结构。
|
||||
- CellData 卡片改为点击/拖拽文件夹上传,不再提供单文件选择;直接拖入单个 ZIP 会提示选择包含 `Result_300` ZIP 的文件夹。
|
||||
- 验证:`frontend` `npm run build` 通过;构建产物已清理。
|
||||
|
||||
@@ -15,6 +15,20 @@
|
||||
@dragleave="onDragLeave"
|
||||
@drop.prevent.stop="onDrop"
|
||||
>
|
||||
<span class="upload-card-label">容量数据</span>
|
||||
<n-button
|
||||
quaternary
|
||||
circle
|
||||
size="small"
|
||||
class="upload-help-button"
|
||||
aria-label="查看容量数据说明"
|
||||
title="查看说明"
|
||||
@click.stop="capacityHelpVisible = true"
|
||||
>
|
||||
<template #icon>
|
||||
<n-icon><InformationCircleOutline /></n-icon>
|
||||
</template>
|
||||
</n-button>
|
||||
<div class="upload-icon">
|
||||
<n-icon size="24"><FolderOpenOutline /></n-icon>
|
||||
</div>
|
||||
@@ -59,20 +73,66 @@
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section v-if="!taskInProgress" class="cell-data-card">
|
||||
<div>
|
||||
<h3>CellData</h3>
|
||||
<p>刷新 CellData 数据。</p>
|
||||
</div>
|
||||
<section
|
||||
v-if="!taskInProgress"
|
||||
class="cell-data-upload-zone"
|
||||
:class="{ dragover: isCellDataDragging, disabled: cellDataStarting }"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
:aria-disabled="cellDataStarting"
|
||||
@click="cellDataFolderInput?.click()"
|
||||
@keydown.enter.prevent="cellDataFolderInput?.click()"
|
||||
@keydown.space.prevent="cellDataFolderInput?.click()"
|
||||
@dragenter.prevent.stop="onCellDataDragEnter"
|
||||
@dragover.prevent.stop="onCellDataDragOver"
|
||||
@dragleave="onCellDataDragLeave"
|
||||
@drop.prevent.stop="onCellDataDrop"
|
||||
>
|
||||
<span class="upload-card-label">CellData</span>
|
||||
<n-button
|
||||
type="primary"
|
||||
secondary
|
||||
:loading="cellDataStarting"
|
||||
:disabled="working || remoteStarting || cellDataStarting"
|
||||
@click="startCellDataProcessing"
|
||||
quaternary
|
||||
circle
|
||||
size="small"
|
||||
class="upload-help-button"
|
||||
aria-label="查看 CellData 文件说明"
|
||||
title="查看说明"
|
||||
@click.stop="cellDataHelpVisible = true"
|
||||
>
|
||||
刷新 CellData
|
||||
<template #icon>
|
||||
<n-icon><InformationCircleOutline /></n-icon>
|
||||
</template>
|
||||
</n-button>
|
||||
<div class="upload-icon">
|
||||
<n-icon size="24"><FolderOpenOutline /></n-icon>
|
||||
</div>
|
||||
<div class="cell-data-upload-text">
|
||||
<h3>拖拽文件夹到这里</h3>
|
||||
<p>包含 700M、2.6G 等目录</p>
|
||||
</div>
|
||||
<div class="upload-zone-actions">
|
||||
<span class="upload-hint">或者点击选择文件夹</span>
|
||||
<n-space size="small" @click.stop @keydown.stop>
|
||||
<n-button
|
||||
type="primary"
|
||||
secondary
|
||||
:loading="cellDataStarting"
|
||||
:disabled="working || remoteStarting || cellDataStarting"
|
||||
@click="startCellDataProcessing"
|
||||
>
|
||||
<template #icon><n-icon><CloudDownloadOutline /></n-icon></template>
|
||||
远程刷新
|
||||
</n-button>
|
||||
</n-space>
|
||||
</div>
|
||||
<input
|
||||
ref="cellDataFolderInput"
|
||||
class="hidden-input"
|
||||
type="file"
|
||||
multiple
|
||||
webkitdirectory
|
||||
directory
|
||||
@change="pickCellDataFiles"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<div v-if="!taskInProgress && files.length > 0" class="file-list">
|
||||
@@ -193,6 +253,58 @@
|
||||
</div>
|
||||
</template>
|
||||
</n-modal>
|
||||
|
||||
<n-modal
|
||||
v-model:show="capacityHelpVisible"
|
||||
preset="card"
|
||||
class="cell-data-help-modal"
|
||||
title="容量数据说明"
|
||||
:style="{ width: 'min(520px, calc(100vw - 32px))' }"
|
||||
>
|
||||
<n-scrollbar class="cell-data-help-scroll">
|
||||
<section class="cell-data-help-section">
|
||||
<h4>支持格式</h4>
|
||||
<p>支持 ZIP、Excel 和 CSV 文件。</p>
|
||||
</section>
|
||||
<section class="cell-data-help-section">
|
||||
<h4>上传方式</h4>
|
||||
<p>可拖拽文件或文件夹,也可点击卡片选择文件。</p>
|
||||
</section>
|
||||
<section class="cell-data-help-section">
|
||||
<h4>远程处理</h4>
|
||||
<p>使用系统设置中的数据源配置下载并处理。</p>
|
||||
</section>
|
||||
</n-scrollbar>
|
||||
</n-modal>
|
||||
|
||||
<n-modal
|
||||
v-model:show="cellDataHelpVisible"
|
||||
preset="card"
|
||||
class="cell-data-help-modal"
|
||||
title="CellData 文件说明"
|
||||
:style="{ width: 'min(520px, calc(100vw - 32px))' }"
|
||||
>
|
||||
<n-scrollbar class="cell-data-help-scroll">
|
||||
<section class="cell-data-help-section">
|
||||
<h4>目录结构</h4>
|
||||
<p>请选择包含频段目录的文件夹,例如:</p>
|
||||
<code>300表/700M/Result_300_*.zip</code>
|
||||
<code>300表/2.6G/Result_300_*.zip</code>
|
||||
</section>
|
||||
<section class="cell-data-help-section">
|
||||
<h4>文件要求</h4>
|
||||
<p>只处理文件名以 <strong>Result_300_</strong> 开头的 ZIP。</p>
|
||||
<p>每个频段目录只取文件名末尾时间戳最新的 ZIP。</p>
|
||||
</section>
|
||||
<section class="cell-data-help-section">
|
||||
<h4>ZIP 内容</h4>
|
||||
<p>识别以下 CSV 文件名前缀:</p>
|
||||
<code>LTE_ITBBU_CellInfo</code>
|
||||
<code>LTE_SDR_CellInfo</code>
|
||||
<code>NR_CellInfo</code>
|
||||
</section>
|
||||
</n-scrollbar>
|
||||
</n-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -204,6 +316,7 @@ import {
|
||||
CloudUploadOutline,
|
||||
DocumentTextOutline,
|
||||
FolderOpenOutline,
|
||||
InformationCircleOutline,
|
||||
RefreshOutline,
|
||||
TrashOutline
|
||||
} from '@vicons/ionicons5';
|
||||
@@ -238,12 +351,16 @@ const validExtensions = new Set(['.zip', '.xlsx', '.xls', '.csv']);
|
||||
const message = useMessage();
|
||||
const fileInput = ref<HTMLInputElement | null>(null);
|
||||
const folderInput = ref<HTMLInputElement | null>(null);
|
||||
const cellDataFolderInput = ref<HTMLInputElement | null>(null);
|
||||
const files = ref<PickedFile[]>([]);
|
||||
const uploadProgress = ref(0);
|
||||
const working = ref(false);
|
||||
const remoteStarting = ref(false);
|
||||
const cellDataStarting = ref(false);
|
||||
const isDragging = ref(false);
|
||||
const isCellDataDragging = ref(false);
|
||||
const capacityHelpVisible = ref(false);
|
||||
const cellDataHelpVisible = ref(false);
|
||||
const taskStatus = ref<TaskStatus | null>(null);
|
||||
const activeTask = ref<ActiveTask | null>(null);
|
||||
const keepLatestLog = ref(false);
|
||||
@@ -320,6 +437,10 @@ onMounted(() => {
|
||||
if (folder) {
|
||||
folder.webkitdirectory = true;
|
||||
}
|
||||
const cellDataFolder = cellDataFolderInput.value as (HTMLInputElement & { webkitdirectory?: boolean }) | null;
|
||||
if (cellDataFolder) {
|
||||
cellDataFolder.webkitdirectory = true;
|
||||
}
|
||||
void checkActiveTask();
|
||||
});
|
||||
|
||||
@@ -367,6 +488,43 @@ async function onDrop(event: DragEvent) {
|
||||
addFiles(dropped);
|
||||
}
|
||||
|
||||
function onCellDataDragEnter() {
|
||||
if (!cellDataStarting.value && !taskInProgress.value) {
|
||||
isCellDataDragging.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
function onCellDataDragOver() {
|
||||
if (!cellDataStarting.value && !taskInProgress.value) {
|
||||
isCellDataDragging.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
function onCellDataDragLeave(event: DragEvent) {
|
||||
const target = event.currentTarget as HTMLElement;
|
||||
const related = event.relatedTarget as Node | null;
|
||||
if (!related || !target.contains(related)) {
|
||||
isCellDataDragging.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function onCellDataDrop(event: DragEvent) {
|
||||
isCellDataDragging.value = false;
|
||||
if (cellDataStarting.value || taskInProgress.value || !event.dataTransfer) return;
|
||||
if (!hasDroppedDirectory(event.dataTransfer)) {
|
||||
message.warning('请拖入包含 Result_300 ZIP 的文件夹');
|
||||
return;
|
||||
}
|
||||
await startCellDataUpload(await readDroppedFiles(event.dataTransfer));
|
||||
}
|
||||
|
||||
async function pickCellDataFiles(event: Event) {
|
||||
const input = event.target as HTMLInputElement;
|
||||
const selected = Array.from(input.files || []).map(toDroppedFile);
|
||||
input.value = '';
|
||||
await startCellDataUpload(selected);
|
||||
}
|
||||
|
||||
async function readDroppedFiles(dataTransfer: DataTransfer): Promise<DroppedFile[]> {
|
||||
const result: DroppedFile[] = [];
|
||||
const items = Array.from(dataTransfer.items || []);
|
||||
@@ -386,6 +544,13 @@ async function readDroppedFiles(dataTransfer: DataTransfer): Promise<DroppedFile
|
||||
return Array.from(dataTransfer.files || []).map(toDroppedFile);
|
||||
}
|
||||
|
||||
function hasDroppedDirectory(dataTransfer: DataTransfer): boolean {
|
||||
return Array.from(dataTransfer.items || []).some(item => {
|
||||
if (item.kind !== 'file' || typeof item.webkitGetAsEntry !== 'function') return false;
|
||||
return Boolean(item.webkitGetAsEntry()?.isDirectory);
|
||||
});
|
||||
}
|
||||
|
||||
async function traverseEntry(entry: FileSystemEntry, parentPath: string): Promise<DroppedFile[]> {
|
||||
if (entry.isFile) {
|
||||
const fileEntry = entry as FileSystemFileEntry;
|
||||
@@ -481,6 +646,11 @@ function isSupportedFile(path: string): boolean {
|
||||
return validExtensions.has(path.slice(dotIndex).toLowerCase());
|
||||
}
|
||||
|
||||
function isCellDataZip(path: string): boolean {
|
||||
const name = normalizePath(path).split('/').pop() || '';
|
||||
return /^Result_300_.*\.zip$/i.test(name);
|
||||
}
|
||||
|
||||
function normalizePath(path: string): string {
|
||||
return path.replace(/\\/g, '/');
|
||||
}
|
||||
@@ -589,6 +759,46 @@ async function startCellDataProcessing() {
|
||||
}
|
||||
}
|
||||
|
||||
async function startCellDataUpload(items: DroppedFile[]) {
|
||||
if (cellDataStarting.value || taskInProgress.value) return;
|
||||
const accepted = items
|
||||
.map(item => ({ ...item, path: normalizePath(item.path) }))
|
||||
.filter(item => isCellDataZip(item.path));
|
||||
if (accepted.length === 0) {
|
||||
message.warning('请选择 Result_300 ZIP 文件');
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
for (const item of accepted) {
|
||||
formData.append('files', item.file, item.path);
|
||||
}
|
||||
|
||||
cellDataStarting.value = true;
|
||||
try {
|
||||
const result = await upload<UploadResponse>('/api/cell-data/process/upload', formData);
|
||||
message.success(result.message || `已上传 ${accepted.length} 个文件`);
|
||||
activeTask.value = {
|
||||
has_active: true,
|
||||
task_id: result.task_id,
|
||||
stage: result.stage || 'parsing',
|
||||
started_at: new Date().toISOString()
|
||||
};
|
||||
taskStatus.value = {
|
||||
task_id: result.task_id,
|
||||
status: 'processing',
|
||||
stage: result.stage || 'parsing',
|
||||
logs: ['CellData 文件已上传,等待处理日志...']
|
||||
};
|
||||
taskMode.value = 'cell_data';
|
||||
startPolling(result.task_id);
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : 'CellData 上传处理失败');
|
||||
} finally {
|
||||
cellDataStarting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function startUploadedTask(taskId: string) {
|
||||
await apiPost('/api/process/start', { task_id: taskId });
|
||||
taskMode.value = 'local';
|
||||
|
||||
+69
-9
@@ -497,6 +497,21 @@ select {
|
||||
transition: all var(--td-transition);
|
||||
}
|
||||
|
||||
.upload-card-label {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
left: 16px;
|
||||
color: var(--td-text-color-primary);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.upload-help-button {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
}
|
||||
|
||||
.upload-zone:hover,
|
||||
.upload-zone.dragover {
|
||||
background: var(--td-brand-color-light);
|
||||
@@ -559,35 +574,80 @@ select {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.cell-data-card {
|
||||
.cell-data-upload-zone {
|
||||
display: flex;
|
||||
position: relative;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
margin-top: 16px;
|
||||
padding: 16px 18px;
|
||||
padding: 24px 18px;
|
||||
text-align: center;
|
||||
background: var(--td-bg-color-container);
|
||||
border: 1px solid var(--td-border-color-light);
|
||||
border: 1px dashed var(--td-border-color);
|
||||
border-radius: var(--td-radius-large);
|
||||
cursor: pointer;
|
||||
transition: all var(--td-transition);
|
||||
}
|
||||
|
||||
.cell-data-card h3,
|
||||
.cell-data-card p {
|
||||
.cell-data-upload-zone:hover,
|
||||
.cell-data-upload-zone.dragover {
|
||||
background: var(--td-brand-color-light);
|
||||
border-color: var(--td-brand-color);
|
||||
}
|
||||
|
||||
.cell-data-upload-zone.disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.72;
|
||||
}
|
||||
|
||||
.cell-data-upload-text h3,
|
||||
.cell-data-upload-text p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.cell-data-card h3 {
|
||||
.cell-data-upload-text h3 {
|
||||
color: var(--td-text-color-primary);
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.cell-data-card p {
|
||||
.cell-data-upload-text p {
|
||||
margin-top: 4px;
|
||||
color: var(--td-text-color-secondary);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.cell-data-help-scroll {
|
||||
max-height: 360px;
|
||||
}
|
||||
|
||||
.cell-data-help-section {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
padding-bottom: 14px;
|
||||
}
|
||||
|
||||
.cell-data-help-section + .cell-data-help-section {
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid var(--td-border-color-light);
|
||||
}
|
||||
|
||||
.cell-data-help-section h4,
|
||||
.cell-data-help-section p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.cell-data-help-section code {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
padding: 6px 8px;
|
||||
color: var(--td-text-color-primary);
|
||||
background: var(--td-bg-color-secondarycontainer);
|
||||
border-radius: var(--td-radius-small);
|
||||
}
|
||||
|
||||
.license-dialog-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
Reference in New Issue
Block a user