feat: 版本号管理、构建日志持久化、App_Store 构建号自动递增
- 新增版本号配置 API(GET/PUT /api/config/versions)和前端管理界面
- 构建日志收集与保存:任务完成/取消/失败时写入 build.log
- 新增任务日志查询接口 GET /api/tasks/{task_id}/log
- App_Store 打包时自动递增 Build_Ver 第四位版本号
- 从 Provisioning Profile 自动提取 TEAM_ID 和 PROVISIONING_NAME
- 前端完成后支持查看完整构建日志
This commit is contained in:
parent
898ec01560
commit
75363ee276
@ -298,6 +298,57 @@ async def update_build_settings(settings: BuildConfigUpdate, db: Session = Depen
|
|||||||
return {"message": "打包设置已更新"}
|
return {"message": "打包设置已更新"}
|
||||||
|
|
||||||
|
|
||||||
|
# 版本号配置 API
|
||||||
|
@router.get("/versions")
|
||||||
|
async def get_versions():
|
||||||
|
"""获取 App_Ver 和 Build_Ver"""
|
||||||
|
from ..config import AUTOPACKING_DIR
|
||||||
|
|
||||||
|
start_build_path = AUTOPACKING_DIR / "start_build_app.py"
|
||||||
|
app_ver = "2.180.0"
|
||||||
|
build_ver = "2.180.0.0"
|
||||||
|
|
||||||
|
if start_build_path.exists():
|
||||||
|
content = start_build_path.read_text(encoding="utf-8")
|
||||||
|
for line in content.split("\n"):
|
||||||
|
if line.startswith("App_Ver") and '"' in line:
|
||||||
|
app_ver = line.split('"')[1]
|
||||||
|
elif line.startswith("Build_Ver") and '"' in line:
|
||||||
|
build_ver = line.split('"')[1]
|
||||||
|
|
||||||
|
return {"app_ver": app_ver, "build_ver": build_ver}
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/versions")
|
||||||
|
async def update_versions(data: dict):
|
||||||
|
"""更新 App_Ver 和 Build_Ver"""
|
||||||
|
from ..config import AUTOPACKING_DIR
|
||||||
|
|
||||||
|
app_ver = data.get("app_ver", "").strip()
|
||||||
|
build_ver = data.get("build_ver", "").strip()
|
||||||
|
|
||||||
|
if not app_ver or not build_ver:
|
||||||
|
raise HTTPException(status_code=400, detail="版本号不能为空")
|
||||||
|
|
||||||
|
start_build_path = AUTOPACKING_DIR / "start_build_app.py"
|
||||||
|
if not start_build_path.exists():
|
||||||
|
raise HTTPException(status_code=404, detail="start_build_app.py 不存在")
|
||||||
|
|
||||||
|
async with _config_lock:
|
||||||
|
content = start_build_path.read_text(encoding="utf-8")
|
||||||
|
new_lines = []
|
||||||
|
for line in content.split("\n"):
|
||||||
|
if line.startswith("App_Ver") and '"' in line:
|
||||||
|
new_lines.append(f'App_Ver = "{app_ver}"')
|
||||||
|
elif line.startswith("Build_Ver") and '"' in line:
|
||||||
|
new_lines.append(f'Build_Ver = "{build_ver}"')
|
||||||
|
else:
|
||||||
|
new_lines.append(line)
|
||||||
|
start_build_path.write_text("\n".join(new_lines), encoding="utf-8")
|
||||||
|
|
||||||
|
return {"message": "版本号已更新", "app_ver": app_ver, "build_ver": build_ver}
|
||||||
|
|
||||||
|
|
||||||
# 上传配置 API
|
# 上传配置 API
|
||||||
@router.get("/upload")
|
@router.get("/upload")
|
||||||
async def get_upload_config():
|
async def get_upload_config():
|
||||||
|
|||||||
@ -101,6 +101,36 @@ async def cancel_task(task_id: str, db: Session = Depends(get_db)):
|
|||||||
return {"message": "任务已取消"}
|
return {"message": "任务已取消"}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{task_id}/log")
|
||||||
|
async def get_build_log(task_id: str, db: Session = Depends(get_db)):
|
||||||
|
"""获取打包日志"""
|
||||||
|
task = db.query(Task).filter(Task.id == task_id).first()
|
||||||
|
if not task:
|
||||||
|
raise HTTPException(status_code=404, detail="任务不存在")
|
||||||
|
|
||||||
|
from ..services.log_streamer import log_streamer
|
||||||
|
|
||||||
|
# 先从内存中获取(任务运行中)
|
||||||
|
lines = log_streamer._log_lines.get(task_id)
|
||||||
|
if lines:
|
||||||
|
content = ""
|
||||||
|
for entry in lines:
|
||||||
|
ts = entry["timestamp"]
|
||||||
|
level = entry["level"].upper()
|
||||||
|
msg = entry["message"]
|
||||||
|
content += f"[{ts}] [{level}] {msg}\n"
|
||||||
|
return {"log": content}
|
||||||
|
|
||||||
|
# 从文件获取(任务已完成)
|
||||||
|
if task.build_dir:
|
||||||
|
from pathlib import Path
|
||||||
|
log_path = Path(task.build_dir) / "build.log"
|
||||||
|
if log_path.exists():
|
||||||
|
return {"log": log_path.read_text(encoding="utf-8")}
|
||||||
|
|
||||||
|
return {"log": ""}
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{task_id}/dsym")
|
@router.get("/{task_id}/dsym")
|
||||||
async def download_dsym(task_id: str, db: Session = Depends(get_db)):
|
async def download_dsym(task_id: str, db: Session = Depends(get_db)):
|
||||||
"""下载 dSYM 文件"""
|
"""下载 dSYM 文件"""
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import os
|
|||||||
import shutil
|
import shutil
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import List
|
||||||
|
|
||||||
from ..config import (
|
from ..config import (
|
||||||
PROJECT_ROOT,
|
PROJECT_ROOT,
|
||||||
@ -242,11 +243,11 @@ async def run_build_task(task_id: str):
|
|||||||
# 4. 代码混淆(可选)
|
# 4. 代码混淆(可选)
|
||||||
if task.obfuscation:
|
if task.obfuscation:
|
||||||
await asyncio.to_thread(_db_update, db, task, current_step="obfuscation")
|
await asyncio.to_thread(_db_update, db, task, current_step="obfuscation")
|
||||||
await run_obfuscation(task_id, task, build_dir, source_dir)
|
await run_obfuscation(task_id, task, config_data, build_dir, source_dir)
|
||||||
|
|
||||||
# 5. 构建项目
|
# 5. 构建项目
|
||||||
await asyncio.to_thread(_db_update, db, task, current_step="build")
|
await asyncio.to_thread(_db_update, db, task, current_step="build")
|
||||||
ipa_path = await build_project(task_id, task, build_dir)
|
ipa_path = await build_project(task_id, task, config_data, build_dir)
|
||||||
|
|
||||||
# 查找 dSYM
|
# 查找 dSYM
|
||||||
dsym_path = await find_dsym(task_id, build_dir)
|
dsym_path = await find_dsym(task_id, build_dir)
|
||||||
@ -283,6 +284,10 @@ async def run_build_task(task_id: str):
|
|||||||
if task.oss_url:
|
if task.oss_url:
|
||||||
await log_streamer.emit(task_id, f"下载链接: {task.oss_url}")
|
await log_streamer.emit(task_id, f"下载链接: {task.oss_url}")
|
||||||
|
|
||||||
|
# 保存打包日志
|
||||||
|
if build_dir and build_dir.exists():
|
||||||
|
await asyncio.to_thread(log_streamer.save_log, task_id, build_dir)
|
||||||
|
|
||||||
# 清理打包目录(保留产物文件,删除源码拷贝)
|
# 清理打包目录(保留产物文件,删除源码拷贝)
|
||||||
await log_streamer.emit(task_id, "清理临时文件...")
|
await log_streamer.emit(task_id, "清理临时文件...")
|
||||||
await asyncio.to_thread(_cleanup_build_dir, build_dir)
|
await asyncio.to_thread(_cleanup_build_dir, build_dir)
|
||||||
@ -292,6 +297,7 @@ async def run_build_task(task_id: str):
|
|||||||
status="cancelled", completed_at=datetime.utcnow())
|
status="cancelled", completed_at=datetime.utcnow())
|
||||||
await log_streamer.emit(task_id, "任务已取消")
|
await log_streamer.emit(task_id, "任务已取消")
|
||||||
if build_dir and build_dir.exists():
|
if build_dir and build_dir.exists():
|
||||||
|
await asyncio.to_thread(log_streamer.save_log, task_id, build_dir)
|
||||||
await asyncio.to_thread(_cleanup_build_dir, build_dir)
|
await asyncio.to_thread(_cleanup_build_dir, build_dir)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -315,7 +321,8 @@ async def run_build_task(task_id: str):
|
|||||||
error_message=error_msg, error_category=category)
|
error_message=error_msg, error_category=category)
|
||||||
await log_streamer.emit_error(task_id, f"打包失败: {error_msg}")
|
await log_streamer.emit_error(task_id, f"打包失败: {error_msg}")
|
||||||
if build_dir and build_dir.exists():
|
if build_dir and build_dir.exists():
|
||||||
await asyncio.to_thread(_cleanup_build_dir, build_dir)
|
await log_streamer.emit(task_id, f"临时文件保留在: {build_dir}")
|
||||||
|
await asyncio.to_thread(log_streamer.save_log, task_id, build_dir)
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
log_streamer.complete(task_id)
|
log_streamer.complete(task_id)
|
||||||
@ -351,7 +358,7 @@ async def generate_config(task_id: str, task, build_dir: Path) -> dict:
|
|||||||
"""生成打包配置"""
|
"""生成打包配置"""
|
||||||
await log_streamer.emit_step(task_id, "生成配置")
|
await log_streamer.emit_step(task_id, "生成配置")
|
||||||
|
|
||||||
from ..routers.config import load_config
|
from ..routers.config import load_config, _config_lock
|
||||||
|
|
||||||
config = load_config()
|
config = load_config()
|
||||||
apps = config.get("apps", {})
|
apps = config.get("apps", {})
|
||||||
@ -373,6 +380,26 @@ async def generate_config(task_id: str, task, build_dir: Path) -> dict:
|
|||||||
elif line.startswith("Build_Ver"):
|
elif line.startswith("Build_Ver"):
|
||||||
build_ver = line.split('"')[1] if '"' in line else build_ver
|
build_ver = line.split('"')[1] if '"' in line else build_ver
|
||||||
|
|
||||||
|
# App_Store 打包时自动递增 Build_Ver 第四位版本号
|
||||||
|
if task.build_type == "App_Store" and start_build_path.exists():
|
||||||
|
async with _config_lock:
|
||||||
|
content = start_build_path.read_text(encoding="utf-8")
|
||||||
|
new_lines = []
|
||||||
|
for line in content.split("\n"):
|
||||||
|
if line.startswith("Build_Ver") and '"' in line:
|
||||||
|
old_ver = line.split('"')[1]
|
||||||
|
parts = old_ver.split(".")
|
||||||
|
if len(parts) >= 4:
|
||||||
|
parts[3] = str(int(parts[3]) + 1)
|
||||||
|
else:
|
||||||
|
parts.append("1")
|
||||||
|
build_ver = ".".join(parts)
|
||||||
|
new_lines.append(f'Build_Ver = "{build_ver}"')
|
||||||
|
else:
|
||||||
|
new_lines.append(line)
|
||||||
|
start_build_path.write_text("\n".join(new_lines), encoding="utf-8")
|
||||||
|
await log_streamer.emit(task_id, f"Build_Ver 已递增: {build_ver}")
|
||||||
|
|
||||||
# 构建配置
|
# 构建配置
|
||||||
config_data = {
|
config_data = {
|
||||||
"VERSION": app_ver,
|
"VERSION": app_ver,
|
||||||
@ -397,8 +424,26 @@ async def generate_config(task_id: str, task, build_dir: Path) -> dict:
|
|||||||
config_data["BUNDLE_ID"] = cert.get("name", "")
|
config_data["BUNDLE_ID"] = cert.get("name", "")
|
||||||
config_data["THEME"] = cert.get("theme", "")
|
config_data["THEME"] = cert.get("theme", "")
|
||||||
|
|
||||||
# 其他配置
|
# 从 Provisioning Profile 提取 TEAM_ID 和 PROVISIONING_NAME
|
||||||
for key in ["weixinlogin", "weixinpay", "tencent", "AssDom", "UniversalLink", "AlivcLicenseKey"]:
|
provisioning_profile = config_data.get("PROVISIONING_PROFILE", "")
|
||||||
|
if provisioning_profile and os.path.exists(provisioning_profile):
|
||||||
|
try:
|
||||||
|
import subprocess as _sp
|
||||||
|
plist_xml = _sp.check_output(
|
||||||
|
["security", "cms", "-D", "-i", provisioning_profile],
|
||||||
|
stderr=_sp.DEVNULL,
|
||||||
|
)
|
||||||
|
import plistlib as _pl
|
||||||
|
plist = _pl.loads(plist_xml)
|
||||||
|
team_ids = plist.get("TeamIdentifier", [])
|
||||||
|
config_data["TEAM_ID"] = team_ids[0] if team_ids else ""
|
||||||
|
config_data["PROVISIONING_NAME"] = plist.get("Name", "")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 关联域名等配置
|
||||||
|
config_data["ASSOCIATED_DOMAINS"] = app.get("AssDom", "")
|
||||||
|
for key in ["weixinlogin", "weixinpay", "tencent", "UniversalLink", "AlivcLicenseKey"]:
|
||||||
if key in app:
|
if key in app:
|
||||||
config_data[key] = app[key]
|
config_data[key] = app[key]
|
||||||
|
|
||||||
@ -439,25 +484,32 @@ async def patch_project(task_id: str, task, config_data: dict, build_dir: Path):
|
|||||||
stderr=asyncio.subprocess.STDOUT,
|
stderr=asyncio.subprocess.STDOUT,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
patch_lines = []
|
||||||
async for line in process.stdout:
|
async for line in process.stdout:
|
||||||
decoded = line.decode("utf-8", errors="replace").strip()
|
decoded = line.decode("utf-8", errors="replace").strip()
|
||||||
if decoded:
|
if decoded:
|
||||||
|
patch_lines.append(decoded)
|
||||||
await log_streamer.emit(task_id, decoded)
|
await log_streamer.emit(task_id, decoded)
|
||||||
|
|
||||||
await process.wait()
|
await process.wait()
|
||||||
|
|
||||||
|
# 保存替换日志
|
||||||
|
patch_log_path = build_dir / "patch_result.log"
|
||||||
|
with open(patch_log_path, "w", encoding="utf-8") as f:
|
||||||
|
f.write("\n".join(patch_lines))
|
||||||
|
|
||||||
if process.returncode != 0:
|
if process.returncode != 0:
|
||||||
raise Exception("项目配置替换失败")
|
raise Exception("项目配置替换失败")
|
||||||
|
|
||||||
await log_streamer.emit(task_id, "项目配置替换完成")
|
await log_streamer.emit(task_id, "项目配置替换完成")
|
||||||
|
|
||||||
|
|
||||||
async def run_obfuscation(task_id: str, task, build_dir: Path, source_root: Path):
|
async def run_obfuscation(task_id: str, task, config_data: dict, build_dir: Path, source_root: Path):
|
||||||
"""执行代码混淆"""
|
"""执行代码混淆"""
|
||||||
await log_streamer.emit_step(task_id, "代码混淆")
|
await log_streamer.emit_step(task_id, "代码混淆")
|
||||||
|
|
||||||
env = os.environ.copy()
|
env = os.environ.copy()
|
||||||
env["TARGET_NAME"] = task.scheme_name
|
env["TARGET_NAME"] = config_data.get("SCHEME", "")
|
||||||
env["SKIP_OBF_PHASE"] = "1"
|
env["SKIP_OBF_PHASE"] = "1"
|
||||||
|
|
||||||
# 自检
|
# 自检
|
||||||
@ -505,11 +557,11 @@ async def run_obfuscation(task_id: str, task, build_dir: Path, source_root: Path
|
|||||||
await log_streamer.emit(task_id, "代码混淆完成")
|
await log_streamer.emit(task_id, "代码混淆完成")
|
||||||
|
|
||||||
|
|
||||||
async def build_project(task_id: str, task, build_dir: Path) -> Path:
|
async def build_project(task_id: str, task, config_data: dict, build_dir: Path) -> Path:
|
||||||
"""构建项目"""
|
"""构建项目"""
|
||||||
await log_streamer.emit_step(task_id, "构建项目")
|
await log_streamer.emit_step(task_id, "构建项目")
|
||||||
|
|
||||||
scheme = task.scheme_name
|
scheme = config_data.get("SCHEME", "")
|
||||||
export_path = build_dir / "build"
|
export_path = build_dir / "build"
|
||||||
archive_path = export_path / f"{scheme}.xcarchive"
|
archive_path = export_path / f"{scheme}.xcarchive"
|
||||||
workspace_path = build_dir / "readoor.xcworkspace"
|
workspace_path = build_dir / "readoor.xcworkspace"
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
from typing import Dict, AsyncGenerator
|
from typing import Dict, AsyncGenerator
|
||||||
|
|
||||||
|
|
||||||
@ -11,24 +12,30 @@ class LogStreamer:
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._queues: Dict[str, asyncio.Queue] = {}
|
self._queues: Dict[str, asyncio.Queue] = {}
|
||||||
self._subscribers: Dict[str, list] = {}
|
self._subscribers: Dict[str, list] = {}
|
||||||
|
self._log_lines: Dict[str, list] = {}
|
||||||
|
|
||||||
def create_queue(self, task_id: str) -> asyncio.Queue:
|
def create_queue(self, task_id: str) -> asyncio.Queue:
|
||||||
"""创建任务的日志队列"""
|
"""创建任务的日志队列"""
|
||||||
queue = asyncio.Queue(maxsize=1000)
|
queue = asyncio.Queue(maxsize=1000)
|
||||||
self._queues[task_id] = queue
|
self._queues[task_id] = queue
|
||||||
|
self._log_lines[task_id] = []
|
||||||
return queue
|
return queue
|
||||||
|
|
||||||
async def emit(self, task_id: str, message: str, level: str = "info"):
|
async def emit(self, task_id: str, message: str, level: str = "info"):
|
||||||
"""发送日志消息"""
|
"""发送日志消息"""
|
||||||
if task_id not in self._queues:
|
|
||||||
return
|
|
||||||
|
|
||||||
log_entry = {
|
log_entry = {
|
||||||
"timestamp": datetime.now().isoformat(),
|
"timestamp": datetime.now().isoformat(),
|
||||||
"level": level,
|
"level": level,
|
||||||
"message": message,
|
"message": message,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# 收集日志
|
||||||
|
if task_id in self._log_lines:
|
||||||
|
self._log_lines[task_id].append(log_entry)
|
||||||
|
|
||||||
|
if task_id not in self._queues:
|
||||||
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self._queues[task_id].put_nowait(log_entry)
|
self._queues[task_id].put_nowait(log_entry)
|
||||||
except asyncio.QueueFull:
|
except asyncio.QueueFull:
|
||||||
@ -81,6 +88,27 @@ class LogStreamer:
|
|||||||
"""清理任务日志队列"""
|
"""清理任务日志队列"""
|
||||||
self._queues.pop(task_id, None)
|
self._queues.pop(task_id, None)
|
||||||
|
|
||||||
|
def save_log(self, task_id: str, build_dir: Path):
|
||||||
|
"""将收集的日志保存到打包目录"""
|
||||||
|
lines = self._log_lines.pop(task_id, None)
|
||||||
|
if not lines:
|
||||||
|
return
|
||||||
|
|
||||||
|
log_path = build_dir / "build.log"
|
||||||
|
with open(log_path, "w", encoding="utf-8") as f:
|
||||||
|
for entry in lines:
|
||||||
|
ts = entry["timestamp"]
|
||||||
|
level = entry["level"].upper()
|
||||||
|
msg = entry["message"]
|
||||||
|
f.write(f"[{ts}] [{level}] {msg}\n")
|
||||||
|
|
||||||
|
def get_log(self, build_dir: Path) -> str:
|
||||||
|
"""读取已保存的打包日志"""
|
||||||
|
log_path = build_dir / "build.log"
|
||||||
|
if log_path.exists():
|
||||||
|
return log_path.read_text(encoding="utf-8")
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
# 全局单例
|
# 全局单例
|
||||||
log_streamer = LogStreamer()
|
log_streamer = LogStreamer()
|
||||||
|
|||||||
@ -173,6 +173,27 @@ const fetchTaskDetail = async (taskId) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fetchBuildLog = async (taskId) => {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/tasks/${taskId}/log`)
|
||||||
|
if (res.ok) {
|
||||||
|
const data = await res.json()
|
||||||
|
if (data.log) {
|
||||||
|
const lines = data.log.split('\n').filter(Boolean)
|
||||||
|
logs.value = lines.map(line => {
|
||||||
|
const m = line.match(/^\[(.+?)\]\s+\[(.+?)\]\s+(.*)$/)
|
||||||
|
if (m) {
|
||||||
|
return { timestamp: m[1], level: m[2].toLowerCase(), message: m[3] }
|
||||||
|
}
|
||||||
|
return { timestamp: '', level: 'info', message: line }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const connectWs = (taskId) => {
|
const connectWs = (taskId) => {
|
||||||
if (activeWs) {
|
if (activeWs) {
|
||||||
activeWs.close()
|
activeWs.close()
|
||||||
@ -215,6 +236,12 @@ const connectWs = (taskId) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ws.onclose = () => {
|
||||||
|
// WebSocket 关闭后,尝试从服务器获取完整日志
|
||||||
|
if (logs.value.length === 0) {
|
||||||
|
fetchBuildLog(taskId)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(() => currentTaskId.value, (newId) => {
|
watch(() => currentTaskId.value, (newId) => {
|
||||||
@ -248,7 +275,10 @@ onMounted(async () => {
|
|||||||
// 已完成/失败的任务,直接显示结果
|
// 已完成/失败的任务,直接显示结果
|
||||||
if (task.status === 'completed' || task.status === 'failed' || task.status === 'cancelled') {
|
if (task.status === 'completed' || task.status === 'failed' || task.status === 'cancelled') {
|
||||||
completedTask.value = task
|
completedTask.value = task
|
||||||
logs.value = [{ timestamp: task.completed_at || task.created_at, level: task.status === 'failed' ? 'error' : 'step', message: task.status === 'failed' ? `打包失败: ${task.error_message || ''}` : '打包完成' }]
|
await fetchBuildLog(queryTaskId)
|
||||||
|
if (!logs.value.length) {
|
||||||
|
logs.value = [{ timestamp: task.completed_at || task.created_at, level: task.status === 'failed' ? 'error' : 'step', message: task.status === 'failed' ? `打包失败: ${task.error_message || ''}` : '打包完成' }]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// 设置 currentTaskId(触发 watch → connectWs,用 flag 跳过)
|
// 设置 currentTaskId(触发 watch → connectWs,用 flag 跳过)
|
||||||
skipNextConnect = true
|
skipNextConnect = true
|
||||||
|
|||||||
@ -230,6 +230,20 @@
|
|||||||
<div v-if="tab === 'build'">
|
<div v-if="tab === 'build'">
|
||||||
<h2>打包设置</h2>
|
<h2>打包设置</h2>
|
||||||
<div style="max-width: 500px;">
|
<div style="max-width: 500px;">
|
||||||
|
<h4 class="section-title">应用版本号</h4>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>App_Ver(应用版本)</label>
|
||||||
|
<input type="text" v-model="versions.app_ver" placeholder="2.180.0">
|
||||||
|
<div style="font-size: 12px; color: #999; margin-top: 4px;">格式:主版本.次版本.修订号</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Build_Ver(构建版本)</label>
|
||||||
|
<input type="text" v-model="versions.build_ver" placeholder="2.180.0.0">
|
||||||
|
<div style="font-size: 12px; color: #999; margin-top: 4px;">格式:主版本.次版本.修订号.构建号(App_Store 打包时构建号自动 +1)</div>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-primary" style="width: auto; padding: 10px 32px; margin-bottom: 24px;" @click="saveVersions">保存版本号</button>
|
||||||
|
|
||||||
|
<h4 class="section-title">打包参数</h4>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>最大并行打包数</label>
|
<label>最大并行打包数</label>
|
||||||
<input type="number" v-model.number="buildSettings.max_concurrent_builds" min="1" max="4">
|
<input type="number" v-model.number="buildSettings.max_concurrent_builds" min="1" max="4">
|
||||||
@ -439,6 +453,7 @@ const servers = ref({})
|
|||||||
const branches = ref([])
|
const branches = ref([])
|
||||||
const newBranch = ref('')
|
const newBranch = ref('')
|
||||||
const buildSettings = ref({})
|
const buildSettings = ref({})
|
||||||
|
const versions = ref({ app_ver: '', build_ver: '' })
|
||||||
const uploadConfig = ref({ mode: 'oss', oss: {}, webdav: {}, dingtalk: {} })
|
const uploadConfig = ref({ mode: 'oss', oss: {}, webdav: {}, dingtalk: {} })
|
||||||
const jsonContent = ref('{}')
|
const jsonContent = ref('{}')
|
||||||
|
|
||||||
@ -459,7 +474,7 @@ onMounted(async () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
const [appsRes, schemesRes, serversRes, branchesRes, buildRes, uploadRes, configRes] = await Promise.all([
|
const [appsRes, schemesRes, serversRes, branchesRes, buildRes, uploadRes, configRes, versionsRes] = await Promise.all([
|
||||||
fetch('/api/config/apps'),
|
fetch('/api/config/apps'),
|
||||||
fetch('/api/config/schemes'),
|
fetch('/api/config/schemes'),
|
||||||
fetch('/api/config/servers'),
|
fetch('/api/config/servers'),
|
||||||
@ -467,6 +482,7 @@ const loadData = async () => {
|
|||||||
fetch('/api/config/build'),
|
fetch('/api/config/build'),
|
||||||
fetch('/api/config/upload'),
|
fetch('/api/config/upload'),
|
||||||
fetch('/api/config'),
|
fetch('/api/config'),
|
||||||
|
fetch('/api/config/versions'),
|
||||||
])
|
])
|
||||||
apps.value = await appsRes.json()
|
apps.value = await appsRes.json()
|
||||||
schemes.value = await schemesRes.json()
|
schemes.value = await schemesRes.json()
|
||||||
@ -475,6 +491,7 @@ const loadData = async () => {
|
|||||||
buildSettings.value = await buildRes.json()
|
buildSettings.value = await buildRes.json()
|
||||||
uploadConfig.value = await uploadRes.json()
|
uploadConfig.value = await uploadRes.json()
|
||||||
jsonContent.value = JSON.stringify(await configRes.json(), null, 2)
|
jsonContent.value = JSON.stringify(await configRes.json(), null, 2)
|
||||||
|
versions.value = await versionsRes.json()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 服务器环境管理
|
// 服务器环境管理
|
||||||
@ -724,6 +741,26 @@ const saveBuildSettings = async () => {
|
|||||||
alert('设置已保存')
|
alert('设置已保存')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const saveVersions = async () => {
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/config/versions', {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(versions.value),
|
||||||
|
})
|
||||||
|
if (res.ok) {
|
||||||
|
const data = await res.json()
|
||||||
|
versions.value = { app_ver: data.app_ver, build_ver: data.build_ver }
|
||||||
|
alert('版本号已保存')
|
||||||
|
} else {
|
||||||
|
const err = await res.json()
|
||||||
|
alert(err.detail || '保存失败')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
alert('保存失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const saveUploadConfig = async () => {
|
const saveUploadConfig = async () => {
|
||||||
try {
|
try {
|
||||||
await fetch('/api/config/upload', {
|
await fetch('/api/config/upload', {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user