feat: CellData 脚本编辑与跨库表复制

This commit is contained in:
2026-06-25 16:57:16 +08:00
parent df017e4c68
commit c9851336d5
9 changed files with 294 additions and 32 deletions
+4 -2
View File
@@ -9,9 +9,9 @@ from fastapi import APIRouter, Body, File, HTTPException, UploadFile
from app import state from app import state
from app.api.routers.task_runtime import set_task_stage from app.api.routers.task_runtime import set_task_stage
from app.config import AppConfig, CACHE_DIR from app.config import AppConfig, CACHE_DIR, CELLDATA_SCRIPT
from app.processor import ProcessLogger from app.processor import ProcessLogger
from app.services.cell_data import CellDataProcessor, refresh_cell_data from app.services.cell_data import CellDataProcessor, execute_celldata_script, refresh_cell_data
from app.utils.files import safe_relative_path from app.utils.files import safe_relative_path
router = APIRouter(tags=["cell-data"]) router = APIRouter(tags=["cell-data"])
@@ -139,6 +139,7 @@ def _run_cell_data_processing(task_id: str, work_dir: Path, logger: ProcessLogge
started = time.time() started = time.time()
try: try:
result = refresh_cell_data(app_config, work_dir, logger) result = refresh_cell_data(app_config, work_dir, logger)
execute_celldata_script(CELLDATA_SCRIPT, app_config, logger)
elapsed = round(time.time() - started, 2) elapsed = round(time.time() - started, 2)
state.processing_tasks[task_id] = { state.processing_tasks[task_id] = {
"logs": logger.get_logs(), "logs": logger.get_logs(),
@@ -169,6 +170,7 @@ def _run_uploaded_cell_data_processing(task_id: str, upload_dir: Path, work_dir:
started = time.time() started = time.time()
try: try:
result = CellDataProcessor(app_config, work_dir, logger).run_local(upload_dir) result = CellDataProcessor(app_config, work_dir, logger).run_local(upload_dir)
execute_celldata_script(CELLDATA_SCRIPT, app_config, logger)
elapsed = round(time.time() - started, 2) elapsed = round(time.time() - started, 2)
state.processing_tasks[task_id] = { state.processing_tasks[task_id] = {
"logs": logger.get_logs(), "logs": logger.get_logs(),
+4 -1
View File
@@ -14,7 +14,8 @@ from app.api.routers.task_runtime import (
) )
from app.config import AppConfig, CACHE_DIR, RemoteDataConfig from app.config import AppConfig, CACHE_DIR, RemoteDataConfig
from app.processor import DataProcessor, ProcessLogger from app.processor import DataProcessor, ProcessLogger
from app.services.cell_data import refresh_cell_data from app.config import CELLDATA_SCRIPT
from app.services.cell_data import copy_celldata_tables_to_capacity, execute_celldata_script, refresh_cell_data
from app.services.license import LicenseError, check_processing_allowed from app.services.license import LicenseError, check_processing_allowed
from app.services.platform import PlatformStorageDownloader, make_source_downloader from app.services.platform import PlatformStorageDownloader, make_source_downloader
from app.services.pipeline import RESULT_TABLES, run_import_and_report from app.services.pipeline import RESULT_TABLES, run_import_and_report
@@ -221,6 +222,8 @@ def _try_refresh_cell_data(app_config: AppConfig, work_dir: Path, logger: Proces
f"CellData 更新完成:{result.imported_rows} 行" f"CellData 更新完成:{result.imported_rows} 行"
f"(解析 {result.parsed_rows},跳过 {result.skipped_rows})" f"(解析 {result.parsed_rows},跳过 {result.skipped_rows})"
) )
execute_celldata_script(CELLDATA_SCRIPT, app_config, logger)
copy_celldata_tables_to_capacity(app_config, logger)
except Exception as exc: except Exception as exc:
logger.warning(f"CellData 更新失败,继续容量处理: {exc}") logger.warning(f"CellData 更新失败,继续容量处理: {exc}")
+68 -22
View File
@@ -4,41 +4,62 @@ from datetime import datetime
from pathlib import Path from pathlib import Path
from threading import Thread from threading import Thread
from fastapi import APIRouter, Body, HTTPException from fastapi import APIRouter, Body, HTTPException, Query
from app import state from app import state
from app.api.routers.task_runtime import set_task_stage from app.api.routers.task_runtime import set_task_stage
from app.config import AppConfig, CACHE_DIR, SQL_SCRIPT from app.config import AppConfig, CACHE_DIR, CELLDATA_SCRIPT, SQL_SCRIPT
from app.processor import DataProcessor, ProcessLogger from app.processor import DataProcessor, ProcessLogger
router = APIRouter(tags=["script"]) router = APIRouter(tags=["script"])
SCRIPT_PATHS = {
"report": SQL_SCRIPT,
"celldata": CELLDATA_SCRIPT,
}
SCRIPT_LABELS = {
"report": "报表脚本",
"celldata": "CellData 脚本",
}
def _resolve_script_path(script_type: str) -> Path:
path = SCRIPT_PATHS.get(script_type)
if path is None:
raise HTTPException(status_code=400, detail=f"不支持的脚本类型: {script_type}")
return path
@router.get("/api/script/content") @router.get("/api/script/content")
async def get_script_content(): async def get_script_content(type: str = Query("report")):
script_path = _resolve_script_path(type)
label = SCRIPT_LABELS.get(type, type)
try: try:
if not SQL_SCRIPT.exists(): if not script_path.exists():
return { return {
"success": True, "success": True,
"content": "# SQL 脚本文件不存在,请在此编写脚本\n", "content": f"# {label}文件不存在,请在此编写脚本\n",
"modified": None, "modified": None,
"path": str(SQL_SCRIPT), "path": str(script_path),
"script_type": type,
} }
modified = datetime.fromtimestamp(SQL_SCRIPT.stat().st_mtime).strftime("%Y-%m-%d %H:%M:%S") modified = datetime.fromtimestamp(script_path.stat().st_mtime).strftime("%Y-%m-%d %H:%M:%S")
return { return {
"success": True, "success": True,
"content": SQL_SCRIPT.read_text(encoding="utf-8"), "content": script_path.read_text(encoding="utf-8"),
"modified": modified, "modified": modified,
"path": str(SQL_SCRIPT), "path": str(script_path),
"script_type": type,
} }
except Exception as exc: except Exception as exc:
return {"success": False, "error": str(exc)} return {"success": False, "error": str(exc)}
@router.post("/api/script/execute") @router.post("/api/script/execute")
async def execute_script(): async def execute_script(script_type: str = Body("report", embed=True)):
script_path = _resolve_script_path(script_type)
if state.global_task_lock["locked"]: if state.global_task_lock["locked"]:
raise HTTPException(status_code=409, detail="已有任务在运行,请等待完成") raise HTTPException(status_code=409, detail="已有任务在运行,请等待完成")
@@ -62,29 +83,48 @@ async def execute_script():
set_task_stage(task_id, "processing", logs) set_task_stage(task_id, "processing", logs)
app_config = state.current_config() app_config = state.current_config()
thread = Thread(target=_run_script, args=(task_id, logger, logs, app_config), daemon=True) thread = Thread(
target=_run_script,
args=(task_id, logger, logs, app_config, script_path, script_type),
daemon=True,
)
thread.start() thread.start()
return {"success": True, "message": "脚本执行任务已启动", "task_id": task_id} label = SCRIPT_LABELS.get(script_type, script_type)
return {"success": True, "message": f"{label}执行任务已启动", "task_id": task_id}
@router.post("/api/script/save") @router.post("/api/script/save")
async def save_script_content(content: str = Body(..., embed=True)): async def save_script_content(
content: str = Body(...),
script_type: str = Body("report"),
):
script_path = _resolve_script_path(script_type)
try: try:
if SQL_SCRIPT.exists(): if script_path.exists():
shutil.copy(SQL_SCRIPT, SQL_SCRIPT.with_suffix(".sql.bak")) shutil.copy(script_path, script_path.with_suffix(".sql.bak"))
SQL_SCRIPT.write_text(content, encoding="utf-8") script_path.write_text(content, encoding="utf-8")
modified = datetime.fromtimestamp(SQL_SCRIPT.stat().st_mtime).strftime("%Y-%m-%d %H:%M:%S") modified = datetime.fromtimestamp(script_path.stat().st_mtime).strftime("%Y-%m-%d %H:%M:%S")
return {"success": True, "message": "脚本保存成功", "modified": modified} return {"success": True, "message": "脚本保存成功", "modified": modified}
except Exception as exc: except Exception as exc:
return {"success": False, "error": str(exc)} return {"success": False, "error": str(exc)}
def _run_script(task_id: str, logger: ProcessLogger, logs: list[str], app_config: AppConfig) -> None: def _run_script(
task_id: str,
logger: ProcessLogger,
logs: list[str],
app_config: AppConfig,
script_path: Path,
script_type: str,
) -> None:
temp_work_dir: Path | None = None temp_work_dir: Path | None = None
label = SCRIPT_LABELS.get(script_type, script_type)
try: try:
logger.info("开始执行 SQL 脚本...") logger.info(f"开始执行{label}...")
if app_config.warehouse_type == "metrix": if script_type == "celldata":
_run_celldata_script(script_path, app_config, logger)
elif app_config.warehouse_type == "metrix":
from app.services.pipeline import run_report_sql from app.services.pipeline import run_report_sql
run_report_sql(app_config, logger) run_report_sql(app_config, logger)
@@ -93,12 +133,18 @@ def _run_script(task_id: str, logger: ProcessLogger, logs: list[str], app_config
temp_work_dir.mkdir(parents=True, exist_ok=True) temp_work_dir.mkdir(parents=True, exist_ok=True)
processor = DataProcessor(app_config, temp_work_dir, logger) processor = DataProcessor(app_config, temp_work_dir, logger)
processor._execute_sql_script() processor._execute_sql_script()
logger.success("SQL 脚本执行完成") logger.success(f"{label}执行完成")
set_task_stage(task_id, "completed", logs, status="completed") set_task_stage(task_id, "completed", logs, status="completed")
except Exception as exc: except Exception as exc:
logger.error(f"SQL 脚本执行失败: {exc}") logger.error(f"{label}执行失败: {exc}")
set_task_stage(task_id, "failed", logs, status="failed") set_task_stage(task_id, "failed", logs, status="failed")
finally: finally:
if temp_work_dir and temp_work_dir.exists(): if temp_work_dir and temp_work_dir.exists():
shutil.rmtree(temp_work_dir, ignore_errors=True) shutil.rmtree(temp_work_dir, ignore_errors=True)
state.reset_task_lock() state.reset_task_lock()
def _run_celldata_script(script_path: Path, app_config: AppConfig, logger: ProcessLogger) -> None:
from app.services.cell_data import execute_celldata_script
execute_celldata_script(script_path, app_config, logger)
+4 -1
View File
@@ -13,7 +13,8 @@ from app.api.routers.task_runtime import (
) )
from app.config import AppConfig from app.config import AppConfig
from app.processor import DataProcessor, ProcessLogger from app.processor import DataProcessor, ProcessLogger
from app.services.cell_data import refresh_cell_data from app.config import CELLDATA_SCRIPT
from app.services.cell_data import copy_celldata_tables_to_capacity, execute_celldata_script, refresh_cell_data
from app.services.license import LicenseError, check_processing_allowed from app.services.license import LicenseError, check_processing_allowed
@@ -227,5 +228,7 @@ def _try_refresh_cell_data(app_config: AppConfig, work_dir: Path, logger: Proces
f"CellData 更新完成:{result.imported_rows} 行" f"CellData 更新完成:{result.imported_rows} 行"
f"(解析 {result.parsed_rows},跳过 {result.skipped_rows})" f"(解析 {result.parsed_rows},跳过 {result.skipped_rows})"
) )
execute_celldata_script(CELLDATA_SCRIPT, app_config, logger)
copy_celldata_tables_to_capacity(app_config, logger)
except Exception as exc: except Exception as exc:
logger.warning(f"CellData 更新失败,继续容量处理: {exc}") logger.warning(f"CellData 更新失败,继续容量处理: {exc}")
+1
View File
@@ -24,6 +24,7 @@ BASE_DIR = _resolve_base_dir()
CACHE_DIR = BASE_DIR / "cache" CACHE_DIR = BASE_DIR / "cache"
CONFIG_FILE = BASE_DIR / "Configure.json" CONFIG_FILE = BASE_DIR / "Configure.json"
SQL_SCRIPT = BASE_DIR / "ReportScript.sql" SQL_SCRIPT = BASE_DIR / "ReportScript.sql"
CELLDATA_SCRIPT = BASE_DIR / "CellDataScript.sql"
@dataclass @dataclass
+150
View File
@@ -428,3 +428,153 @@ class CellDataProcessor:
def refresh_cell_data(app_config: AppConfig, work_dir: Path, logger=None) -> CellDataResult: def refresh_cell_data(app_config: AppConfig, work_dir: Path, logger=None) -> CellDataResult:
return CellDataProcessor(app_config, work_dir, logger).run() return CellDataProcessor(app_config, work_dir, logger).run()
def execute_celldata_script(script_path: Path, app_config: AppConfig, logger=None) -> None:
if not script_path.exists():
if logger:
logger.info("CellData 脚本文件不存在,跳过")
return
sql_text = script_path.read_text(encoding="utf-8").strip()
if not sql_text:
if logger:
logger.info("CellData 脚本为空,跳过")
return
from app.processor import DataProcessor
statements = DataProcessor.parse_sql_script(sql_text)
if not statements:
if logger:
logger.info("CellData 脚本中没有有效语句,跳过")
return
mysql = app_config.cell_data.mysql.normalized()
conn = pymysql.connect(
host=mysql.host,
port=mysql.port,
user=mysql.user,
password=mysql.passwd,
database=mysql.dbname,
charset="utf8mb4",
cursorclass=pymysql.cursors.DictCursor,
autocommit=False,
)
try:
with conn.cursor() as cursor:
for i, sql in enumerate(statements, 1):
preview = sql[:80].replace("\n", " ")
if logger:
logger.info(f"CellData SQL ({i}/{len(statements)}): {preview}...")
cursor.execute(sql)
affected = cursor.rowcount if cursor.rowcount >= 0 else 0
if affected > 0 and logger:
logger.info(f"完成,影响 {affected} 行")
conn.commit()
if logger:
logger.success(f"CellData 脚本执行完成,共 {len(statements)} 条语句")
except Exception:
conn.rollback()
raise
finally:
conn.close()
def copy_celldata_tables_to_capacity(app_config: AppConfig, logger=None) -> int:
cd_mysql = app_config.cell_data.mysql.normalized()
cap_mysql = app_config.mysql.normalized()
if (
cd_mysql.host == cap_mysql.host
and cd_mysql.port == cap_mysql.port
and cd_mysql.dbname == cap_mysql.dbname
):
if logger:
logger.info("CellData 与容量数据库相同,跳过表复制")
return 0
cd_conn = pymysql.connect(
host=cd_mysql.host, port=cd_mysql.port,
user=cd_mysql.user, password=cd_mysql.passwd,
database=cd_mysql.dbname, charset="utf8mb4",
cursorclass=pymysql.cursors.DictCursor,
)
cap_conn = pymysql.connect(
host=cap_mysql.host, port=cap_mysql.port,
user=cap_mysql.user, password=cap_mysql.passwd,
database=cap_mysql.dbname, charset="utf8mb4",
cursorclass=pymysql.cursors.DictCursor,
local_infile=True, autocommit=False,
)
try:
tables = _list_tables(cd_conn)
if not tables:
if logger:
logger.info("CellData 数据库中没有表,跳过复制")
return 0
copied = 0
for table in tables:
rows = _copy_one_table(cd_conn, cap_conn, table, logger)
if rows >= 0:
copied += 1
cap_conn.commit()
if logger:
logger.success(f"已将 {copied} 张表从 CellData 复制到容量数据库")
return copied
except Exception:
cap_conn.rollback()
raise
finally:
cd_conn.close()
cap_conn.close()
def _list_tables(conn) -> list[str]:
with conn.cursor() as cur:
cur.execute("SHOW TABLES")
return [list(row.values())[0] for row in cur.fetchall()]
def _copy_one_table(src_conn, dst_conn, table: str, logger=None) -> int:
with src_conn.cursor() as cur:
cur.execute(f"SHOW CREATE TABLE `{table}`")
row = cur.fetchone()
create_sql = row.get("Create Table") or list(row.values())[1]
with src_conn.cursor() as cur:
cur.execute(f"SELECT COUNT(*) AS `cnt` FROM `{table}`")
count = cur.fetchone()["cnt"]
with dst_conn.cursor() as cur:
cur.execute(f"DROP TABLE IF EXISTS `{table}`")
cur.execute(create_sql)
if count == 0:
if logger:
logger.info(f"复制表 {table}: 0 行(空表)")
return 0
with src_conn.cursor() as src_cur:
src_cur.execute(f"SELECT * FROM `{table}`")
columns = [desc[0] for desc in src_cur.description]
col_list = ", ".join(f"`{c}`" for c in columns)
placeholders = ", ".join(["%s"] * len(columns))
insert_sql = f"INSERT INTO `{table}` ({col_list}) VALUES ({placeholders})"
batch: list[tuple] = []
inserted = 0
with dst_conn.cursor() as dst_cur:
for row in src_cur:
batch.append(tuple(row.values()))
if len(batch) >= 5000:
dst_cur.executemany(insert_sql, batch)
inserted += len(batch)
batch.clear()
if batch:
dst_cur.executemany(insert_sql, batch)
inserted += len(batch)
if logger:
logger.info(f"复制表 {table}: {inserted} 行")
return inserted
+11
View File
@@ -868,3 +868,14 @@
- 修复:`processor.py::_convert_datetime_column` 在 ISO8601 解析前先用正则剥离时区后缀(`+08:00`、`-05:30`、`Z`)和 `T` 分隔符,再按朴素日期时间解析,保留原始壁钟时间。 - 修复:`processor.py::_convert_datetime_column` 在 ISO8601 解析前先用正则剥离时区后缀(`+08:00`、`-05:30`、`Z`)和 `T` 分隔符,再按朴素日期时间解析,保留原始壁钟时间。
- SQL 清理:`ReportScript.sql` 删除 4G/5G 的 `DATE_ADD(INTERVAL 8 HOUR)` 补偿语句,因为数据导入阶段已正确保留本地时间,不再需要 SQL 层面修正。 - SQL 清理:`ReportScript.sql` 删除 4G/5G 的 `DATE_ADD(INTERVAL 8 HOUR)` 补偿语句,因为数据导入阶段已正确保留本地时间,不再需要 SQL 层面修正。
- 验证:`python -m compileall -q app` 通过。 - 验证:`python -m compileall -q app` 通过。
## 2026-06-25:CellData 脚本编辑与跨库表复制
- 新增 `CellDataScript.sql` 脚本路径(`config.py::CELLDATA_SCRIPT = BASE_DIR / "CellDataScript.sql"`),与 `ReportScript.sql` 并列,用于 CellData 数据导入后执行的 SQL。
- 脚本 API(`script.py`)三个接口(读取/保存/执行)新增 `script_type` 参数(`report` / `celldata`),默认 `report` 保持向后兼容;CellData 脚本执行在 CellData 数据库上下文中运行。
- 新增 `cell_data.py::execute_celldata_script()`:在 CellData MySQL 库上执行 CellDataScript.sql,复用 `DataProcessor.parse_sql_script` 解析。
- 新增 `cell_data.py::copy_celldata_tables_to_capacity()`:列出 CellData 库所有表,逐表 `SHOW CREATE TABLE` → 目标库建表 → 分批 `SELECT * / INSERT INTO` 复制数据;两库相同时跳过。
- CellData 独立处理(远程 / 本地上传)完成后自动执行 CellData 脚本(不复制表)。
- 容量处理集成(`tasks.py` / `remote.py` 的 `_try_refresh_cell_data`):CellData 数据更新 → 执行 CellData 脚本 → 复制表到容量库 → 后续 ReportScript.sql 可直接引用 CellData 表。
- 前端脚本编辑页(`ScriptPanel.vue`)编辑器上方新增 Naive UI segment tabs 切换「报表脚本」和「CellData 脚本」;切换时检查未保存状态、加载对应脚本内容;保存和运行按钮跟随当前脚本类型。
- 验证:`python -m compileall -q app` 通过;`frontend` `npm run build`(vue-tsc)通过;构建产物已清理。
+51 -6
View File
@@ -1,6 +1,17 @@
<template> <template>
<div class="script-page-body"> <div class="script-page-body">
<div class="script-editor-container"> <div class="script-editor-container">
<div class="script-type-bar">
<n-tabs
:value="scriptType"
type="segment"
size="small"
@update:value="switchScript"
>
<n-tab-pane name="report" tab="报表脚本" :disabled="switching" />
<n-tab-pane name="celldata" tab="CellData 脚本" :disabled="switching" />
</n-tabs>
</div>
<div ref="editorHost" class="script-editor" /> <div ref="editorHost" class="script-editor" />
</div> </div>
@@ -63,6 +74,11 @@ type MonacoGlobal = typeof globalThis & {
getWorker: () => new editorWorker() getWorker: () => new editorWorker()
}; };
const SCRIPT_LABELS: Record<string, string> = {
report: '报表脚本',
celldata: 'CellData 脚本',
};
const message = useMessage(); const message = useMessage();
const editorHost = ref<HTMLDivElement | null>(null); const editorHost = ref<HTMLDivElement | null>(null);
const editor = shallowRef<monaco.editor.IStandaloneCodeEditor | null>(null); const editor = shallowRef<monaco.editor.IStandaloneCodeEditor | null>(null);
@@ -77,6 +93,8 @@ const isModified = ref(false);
const loading = ref(false); const loading = ref(false);
const saving = ref(false); const saving = ref(false);
const executing = ref(false); const executing = ref(false);
const scriptType = ref('report');
const switching = ref(false);
let timer: number | undefined; let timer: number | undefined;
let themeObserver: MutationObserver | undefined; let themeObserver: MutationObserver | undefined;
let applyingRemoteContent = false; let applyingRemoteContent = false;
@@ -108,13 +126,15 @@ const statusText = computed(() => {
const editorStatus = computed(() => { const editorStatus = computed(() => {
if (loading.value) return '加载中'; if (loading.value) return '加载中';
if (saving.value) return '保存中'; if (saving.value) return '保存中';
if (switching.value) return '切换中';
return isModified.value ? '未保存' : '就绪'; return isModified.value ? '未保存' : '就绪';
}); });
const modifiedText = computed(() => (modified.value ? `最后修改: ${modified.value}` : '最后修改: -')); const modifiedText = computed(() => (modified.value ? `最后修改: ${modified.value}` : '最后修改: -'));
const currentLabel = computed(() => SCRIPT_LABELS[scriptType.value] || scriptType.value);
onMounted(async () => { onMounted(async () => {
setPageHeader({ setPageHeader({
subtitle: computed(() => scriptPath.value || 'ReportScript.sql'), subtitle: computed(() => currentLabel.value + (scriptPath.value ? ` (${scriptPath.value})` : '')),
actions: [ actions: [
{ key: 'script-modified', kind: 'text', label: modifiedText }, { key: 'script-modified', kind: 'text', label: modifiedText },
{ {
@@ -129,7 +149,7 @@ onMounted(async () => {
{ key: 'format-script', label: '格式化', icon: BrushOutline, onClick: formatScript }, { key: 'format-script', label: '格式化', icon: BrushOutline, onClick: formatScript },
{ {
key: 'run-script', key: 'run-script',
label: '运行脚本', label: computed(() => `运行${currentLabel.value}`),
icon: PlayOutline, icon: PlayOutline,
type: 'success', type: 'success',
variant: 'solid', variant: 'solid',
@@ -162,7 +182,7 @@ function createEditor() {
if (!editorHost.value || editor.value) return; if (!editorHost.value || editor.value) return;
editor.value = monaco.editor.create(editorHost.value, { editor.value = monaco.editor.create(editorHost.value, {
value: '-- 正在加载 ReportScript.sql', value: '-- 正在加载脚本...',
language: 'sql', language: 'sql',
theme: currentEditorTheme(), theme: currentEditorTheme(),
fontSize: 14, fontSize: 14,
@@ -204,10 +224,22 @@ function createEditor() {
updateCursor(); updateCursor();
} }
async function switchScript(type: string) {
if (type === scriptType.value || switching.value) return;
if (isModified.value) {
const confirm = window.confirm(`当前${currentLabel.value}有未保存的修改,是否放弃?`);
if (!confirm) return;
}
switching.value = true;
scriptType.value = type;
await loadScript();
switching.value = false;
}
async function loadScript() { async function loadScript() {
loading.value = true; loading.value = true;
try { try {
const result = await apiGet<ScriptContent>('/api/script/content'); const result = await apiGet<ScriptContent>(`/api/script/content?type=${scriptType.value}`);
if (!result.success) { if (!result.success) {
throw new Error(result.error || '加载脚本失败'); throw new Error(result.error || '加载脚本失败');
} }
@@ -226,7 +258,8 @@ async function saveScript() {
try { try {
const value = getEditorContent(); const value = getEditorContent();
const result = await apiPost<ApiMessage & { modified?: string }>('/api/script/save', { const result = await apiPost<ApiMessage & { modified?: string }>('/api/script/save', {
content: value content: value,
script_type: scriptType.value
}); });
if (!result.success) { if (!result.success) {
throw new Error(result.error || '保存失败'); throw new Error(result.error || '保存失败');
@@ -261,7 +294,9 @@ async function executeScript() {
if (isModified.value) return; if (isModified.value) return;
} }
const result = await apiPost<ApiMessage>('/api/script/execute'); const result = await apiPost<ApiMessage>('/api/script/execute', {
script_type: scriptType.value
});
if (!result.task_id) { if (!result.task_id) {
throw new Error(result.message || '脚本任务启动失败'); throw new Error(result.message || '脚本任务启动失败');
} }
@@ -335,3 +370,13 @@ async function refreshStatus() {
} }
} }
</script> </script>
<style scoped>
.script-type-bar {
padding: 6px 12px;
border-bottom: 1px solid var(--n-border-color, #e0e0e6);
}
.script-type-bar :deep(.n-tabs .n-tabs-tab) {
padding: 4px 16px;
}
</style>
+1
View File
@@ -0,0 +1 @@
feat: CellData 脚本编辑与跨库表复制