Improve manual catalog UX
This commit is contained in:
+58
-6
@@ -33,11 +33,19 @@ SYNC_LOCK = threading.Lock()
|
|||||||
SCHEDULE_LOCK = threading.Lock()
|
SCHEDULE_LOCK = threading.Lock()
|
||||||
MYSQL_CONFIG_LOCK = threading.Lock()
|
MYSQL_CONFIG_LOCK = threading.Lock()
|
||||||
INDEX_ALIAS_LOCK = threading.Lock()
|
INDEX_ALIAS_LOCK = threading.Lock()
|
||||||
|
MANUAL_REBUILD_LOCK = threading.Lock()
|
||||||
NORMALIZE_RE = re.compile(r"[^0-9a-z\u4e00-\u9fff]+")
|
NORMALIZE_RE = re.compile(r"[^0-9a-z\u4e00-\u9fff]+")
|
||||||
SCHEDULE_TIME_RE = re.compile(r"^(?:[01]?\d|2[0-3]):[0-5]\d$")
|
SCHEDULE_TIME_RE = re.compile(r"^(?:[01]?\d|2[0-3]):[0-5]\d$")
|
||||||
SCHEDULER_POLL_SECONDS = 20
|
SCHEDULER_POLL_SECONDS = 20
|
||||||
INDEX_DEVICE_NAME_ALIAS_MAP: dict[str, list[str]] | None = None
|
INDEX_DEVICE_NAME_ALIAS_MAP: dict[str, list[str]] | None = None
|
||||||
DEVICE_TYPES = {"phone", "tablet", "wear", "tv", "computer", "other"}
|
DEVICE_TYPES = {"phone", "tablet", "wear", "tv", "computer", "other"}
|
||||||
|
LAST_MANUAL_MYSQL_LOAD_STATUS: dict[str, object] = {
|
||||||
|
"running": False,
|
||||||
|
"last_started_at": None,
|
||||||
|
"last_finished_at": None,
|
||||||
|
"last_status": None,
|
||||||
|
"last_message": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def truthy_env(name: str, default: str = "0") -> bool:
|
def truthy_env(name: str, default: str = "0") -> bool:
|
||||||
@@ -471,7 +479,34 @@ def count_manual_alias_conflicts(device: dict[str, object]) -> int:
|
|||||||
return len(conflicts)
|
return len(conflicts)
|
||||||
|
|
||||||
|
|
||||||
def rebuild_generated_outputs() -> dict[str, object]:
|
def _manual_mysql_loader_task() -> None:
|
||||||
|
with MANUAL_REBUILD_LOCK:
|
||||||
|
LAST_MANUAL_MYSQL_LOAD_STATUS["running"] = True
|
||||||
|
LAST_MANUAL_MYSQL_LOAD_STATUS["last_started_at"] = local_now().isoformat(timespec="seconds")
|
||||||
|
LAST_MANUAL_MYSQL_LOAD_STATUS["last_status"] = "running"
|
||||||
|
LAST_MANUAL_MYSQL_LOAD_STATUS["last_message"] = "手动补录触发的 MySQL 刷新进行中。"
|
||||||
|
try:
|
||||||
|
load_proc = run_command(["python3", str(MYSQL_LOADER)])
|
||||||
|
message = "\n".join(part for part in [load_proc.stdout.strip(), load_proc.stderr.strip()] if part).strip() or "MySQL 已刷新。"
|
||||||
|
LAST_MANUAL_MYSQL_LOAD_STATUS["last_status"] = "success" if load_proc.returncode == 0 else "failed"
|
||||||
|
LAST_MANUAL_MYSQL_LOAD_STATUS["last_message"] = message
|
||||||
|
except Exception as err:
|
||||||
|
LAST_MANUAL_MYSQL_LOAD_STATUS["last_status"] = "failed"
|
||||||
|
LAST_MANUAL_MYSQL_LOAD_STATUS["last_message"] = str(err)
|
||||||
|
finally:
|
||||||
|
LAST_MANUAL_MYSQL_LOAD_STATUS["running"] = False
|
||||||
|
LAST_MANUAL_MYSQL_LOAD_STATUS["last_finished_at"] = local_now().isoformat(timespec="seconds")
|
||||||
|
|
||||||
|
|
||||||
|
def start_manual_mysql_loader() -> bool:
|
||||||
|
if LAST_MANUAL_MYSQL_LOAD_STATUS.get("running"):
|
||||||
|
return False
|
||||||
|
thread = threading.Thread(target=_manual_mysql_loader_task, name="manual-mysql-loader", daemon=True)
|
||||||
|
thread.start()
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def rebuild_generated_outputs(*, defer_mysql_load: bool = False) -> dict[str, object]:
|
||||||
build_proc = run_command(
|
build_proc = run_command(
|
||||||
[
|
[
|
||||||
"python3",
|
"python3",
|
||||||
@@ -504,6 +539,10 @@ def rebuild_generated_outputs() -> dict[str, object]:
|
|||||||
mysql_loaded = False
|
mysql_loaded = False
|
||||||
mysql_message = "MySQL 未刷新。"
|
mysql_message = "MySQL 未刷新。"
|
||||||
if mysql_auto_load_enabled():
|
if mysql_auto_load_enabled():
|
||||||
|
if defer_mysql_load:
|
||||||
|
started = start_manual_mysql_loader()
|
||||||
|
mysql_message = "MySQL 后台刷新中。" if started else "MySQL 后台刷新已在进行中。"
|
||||||
|
else:
|
||||||
load_proc = run_command(["python3", str(MYSQL_LOADER)])
|
load_proc = run_command(["python3", str(MYSQL_LOADER)])
|
||||||
mysql_message = "\n".join(part for part in [load_proc.stdout.strip(), load_proc.stderr.strip()] if part).strip() or "MySQL 已刷新。"
|
mysql_message = "\n".join(part for part in [load_proc.stdout.strip(), load_proc.stderr.strip()] if part).strip() or "MySQL 已刷新。"
|
||||||
if load_proc.returncode != 0:
|
if load_proc.returncode != 0:
|
||||||
@@ -513,7 +552,13 @@ def rebuild_generated_outputs() -> dict[str, object]:
|
|||||||
"index_updated": True,
|
"index_updated": True,
|
||||||
"mysql_seed_updated": True,
|
"mysql_seed_updated": True,
|
||||||
"mysql_loaded": mysql_loaded,
|
"mysql_loaded": mysql_loaded,
|
||||||
"message": "本地覆盖库已保存,索引与 MySQL seed 已刷新。" if mysql_loaded else "本地覆盖库已保存,索引与 MySQL seed 已刷新,MySQL 未自动装载。",
|
"message": (
|
||||||
|
"本地覆盖库已保存,索引与 MySQL seed 已刷新,MySQL 正在后台刷新。"
|
||||||
|
if defer_mysql_load and mysql_auto_load_enabled()
|
||||||
|
else "本地覆盖库已保存,索引与 MySQL seed 已刷新。"
|
||||||
|
if mysql_loaded
|
||||||
|
else "本地覆盖库已保存,索引与 MySQL seed 已刷新,MySQL 未自动装载。"
|
||||||
|
),
|
||||||
"build_output": build_output,
|
"build_output": build_output,
|
||||||
"mysql_seed_output": seed_output,
|
"mysql_seed_output": seed_output,
|
||||||
"mysql_message": mysql_message,
|
"mysql_message": mysql_message,
|
||||||
@@ -532,6 +577,13 @@ def manual_catalog_payload() -> dict[str, object]:
|
|||||||
"device_count": len(devices),
|
"device_count": len(devices),
|
||||||
},
|
},
|
||||||
"catalog_file": str(MANUAL_CATALOG_PATH.relative_to(PROJECT_ROOT)),
|
"catalog_file": str(MANUAL_CATALOG_PATH.relative_to(PROJECT_ROOT)),
|
||||||
|
"mysql_refresh": {
|
||||||
|
"running": bool(LAST_MANUAL_MYSQL_LOAD_STATUS.get("running")),
|
||||||
|
"last_started_at": LAST_MANUAL_MYSQL_LOAD_STATUS.get("last_started_at"),
|
||||||
|
"last_finished_at": LAST_MANUAL_MYSQL_LOAD_STATUS.get("last_finished_at"),
|
||||||
|
"last_status": LAST_MANUAL_MYSQL_LOAD_STATUS.get("last_status"),
|
||||||
|
"last_message": LAST_MANUAL_MYSQL_LOAD_STATUS.get("last_message"),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -552,7 +604,7 @@ def upsert_manual_brand(payload: dict[str, object]) -> dict[str, object]:
|
|||||||
catalog["brands"].append(incoming)
|
catalog["brands"].append(incoming)
|
||||||
validated = validate_manual_catalog(catalog)
|
validated = validate_manual_catalog(catalog)
|
||||||
write_manual_catalog(validated)
|
write_manual_catalog(validated)
|
||||||
rebuild_result = rebuild_generated_outputs()
|
rebuild_result = rebuild_generated_outputs(defer_mysql_load=True)
|
||||||
return {
|
return {
|
||||||
"saved_brand": incoming,
|
"saved_brand": incoming,
|
||||||
"catalog": manual_catalog_payload(),
|
"catalog": manual_catalog_payload(),
|
||||||
@@ -576,7 +628,7 @@ def upsert_manual_device(payload: dict[str, object]) -> dict[str, object]:
|
|||||||
catalog["devices"].append(device_payload)
|
catalog["devices"].append(device_payload)
|
||||||
validated = validate_manual_catalog(catalog)
|
validated = validate_manual_catalog(catalog)
|
||||||
write_manual_catalog(validated)
|
write_manual_catalog(validated)
|
||||||
rebuild_result = rebuild_generated_outputs()
|
rebuild_result = rebuild_generated_outputs(defer_mysql_load=True)
|
||||||
return {
|
return {
|
||||||
"saved_device": device_payload,
|
"saved_device": device_payload,
|
||||||
"alias_conflict_count": count_manual_alias_conflicts(device_payload),
|
"alias_conflict_count": count_manual_alias_conflicts(device_payload),
|
||||||
@@ -602,7 +654,7 @@ def delete_manual_brand(payload: dict[str, object]) -> dict[str, object]:
|
|||||||
if len(next_brands) == len(catalog["brands"]):
|
if len(next_brands) == len(catalog["brands"]):
|
||||||
raise RuntimeError(f"未找到品牌: {brand_name}")
|
raise RuntimeError(f"未找到品牌: {brand_name}")
|
||||||
write_manual_catalog({"brands": next_brands, "devices": catalog["devices"]})
|
write_manual_catalog({"brands": next_brands, "devices": catalog["devices"]})
|
||||||
rebuild_result = rebuild_generated_outputs()
|
rebuild_result = rebuild_generated_outputs(defer_mysql_load=True)
|
||||||
return {
|
return {
|
||||||
"deleted_brand": brand_name,
|
"deleted_brand": brand_name,
|
||||||
"catalog": manual_catalog_payload(),
|
"catalog": manual_catalog_payload(),
|
||||||
@@ -624,7 +676,7 @@ def delete_manual_device(payload: dict[str, object]) -> dict[str, object]:
|
|||||||
if len(next_devices) == len(catalog["devices"]):
|
if len(next_devices) == len(catalog["devices"]):
|
||||||
raise RuntimeError(f"未找到设备: {device_id}")
|
raise RuntimeError(f"未找到设备: {device_id}")
|
||||||
write_manual_catalog({"brands": catalog["brands"], "devices": next_devices})
|
write_manual_catalog({"brands": catalog["brands"], "devices": next_devices})
|
||||||
rebuild_result = rebuild_generated_outputs()
|
rebuild_result = rebuild_generated_outputs(defer_mysql_load=True)
|
||||||
return {
|
return {
|
||||||
"deleted_device": device_id,
|
"deleted_device": device_id,
|
||||||
"catalog": manual_catalog_payload(),
|
"catalog": manual_catalog_payload(),
|
||||||
|
|||||||
+120
-14
@@ -378,12 +378,12 @@
|
|||||||
.manual-brand-table col:nth-child(3) { width: 180px; }
|
.manual-brand-table col:nth-child(3) { width: 180px; }
|
||||||
.manual-brand-table col:nth-child(4) { width: 240px; }
|
.manual-brand-table col:nth-child(4) { width: 240px; }
|
||||||
.manual-brand-table col:nth-child(5) { width: 200px; }
|
.manual-brand-table col:nth-child(5) { width: 200px; }
|
||||||
.manual-device-table col:nth-child(1) { width: 160px; }
|
.manual-device-table col:nth-child(1) { width: 140px; }
|
||||||
.manual-device-table col:nth-child(2) { width: 240px; }
|
.manual-device-table col:nth-child(2) { width: 280px; }
|
||||||
.manual-device-table col:nth-child(3) { width: 140px; }
|
.manual-device-table col:nth-child(3) { width: 140px; }
|
||||||
.manual-device-table col:nth-child(4) { width: auto; }
|
.manual-device-table col:nth-child(4) { width: 220px; }
|
||||||
.manual-device-table col:nth-child(5) { width: 280px; }
|
.manual-device-table col:nth-child(5) { width: 220px; }
|
||||||
.manual-device-table col:nth-child(6) { width: 200px; }
|
.manual-device-table col:nth-child(6) { width: 220px; }
|
||||||
.manual-brand-table th,
|
.manual-brand-table th,
|
||||||
.manual-brand-table td,
|
.manual-brand-table td,
|
||||||
.manual-device-table th,
|
.manual-device-table th,
|
||||||
@@ -394,10 +394,30 @@
|
|||||||
.manual-device-table td {
|
.manual-device-table td {
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
|
.manual-device-table td:nth-child(5) {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
.manual-brand-table .tag,
|
.manual-brand-table .tag,
|
||||||
.manual-device-table .tag {
|
.manual-device-table .tag {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
.manual-brand-table th:last-child,
|
||||||
|
.manual-brand-table td:last-child,
|
||||||
|
.manual-device-table th:last-child,
|
||||||
|
.manual-device-table td:last-child {
|
||||||
|
position: sticky;
|
||||||
|
right: 0;
|
||||||
|
z-index: 2;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: -8px 0 12px rgba(36, 56, 89, 0.06);
|
||||||
|
}
|
||||||
|
.manual-brand-table th:last-child,
|
||||||
|
.manual-device-table th:last-child {
|
||||||
|
background: #f7f9fd;
|
||||||
|
z-index: 3;
|
||||||
|
}
|
||||||
.manual-actions-bar {
|
.manual-actions-bar {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
padding-left: 10px;
|
padding-left: 10px;
|
||||||
@@ -405,6 +425,40 @@
|
|||||||
.manual-actions-bar .btns {
|
.manual-actions-bar .btns {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
.status-row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.status-pill {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 28px;
|
||||||
|
padding: 0 12px;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.status-pill.running {
|
||||||
|
background: #eef4ff;
|
||||||
|
color: #1e59c9;
|
||||||
|
}
|
||||||
|
.status-pill.success {
|
||||||
|
background: #eefaf2;
|
||||||
|
color: #177245;
|
||||||
|
}
|
||||||
|
.status-pill.failed {
|
||||||
|
background: #fff1f0;
|
||||||
|
color: #b42318;
|
||||||
|
}
|
||||||
|
.status-pill.idle {
|
||||||
|
background: #f4f6fa;
|
||||||
|
color: #5b6679;
|
||||||
|
}
|
||||||
.collapse-card {
|
.collapse-card {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -774,6 +828,9 @@
|
|||||||
<li>保存后会重建索引和 MySQL seed;是否自动刷新 MySQL 取决于左侧同步配置中的自动装载开关。</li>
|
<li>保存后会重建索引和 MySQL seed;是否自动刷新 MySQL 取决于左侧同步配置中的自动装载开关。</li>
|
||||||
<li>本地覆盖库不会被“原始数据同步”覆盖,适合补录学习机、教育终端、定制设备。</li>
|
<li>本地覆盖库不会被“原始数据同步”覆盖,适合补录学习机、教育终端、定制设备。</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
<div id="manualRefreshBadge" class="status-row">
|
||||||
|
<span class="status-pill idle">等待刷新</span>
|
||||||
|
</div>
|
||||||
<div id="manualStatus" class="sub">正在读取本地覆盖库。</div>
|
<div id="manualStatus" class="sub">正在读取本地覆盖库。</div>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
@@ -982,6 +1039,7 @@
|
|||||||
let managedBrandConfig = null;
|
let managedBrandConfig = null;
|
||||||
let managedSourceConfig = null;
|
let managedSourceConfig = null;
|
||||||
let manualCatalog = { brands: [], devices: [] };
|
let manualCatalog = { brands: [], devices: [] };
|
||||||
|
let manualCatalogPollTimer = null;
|
||||||
let managedBrandAliasToBrand = new Map();
|
let managedBrandAliasToBrand = new Map();
|
||||||
let managedBrandToManufacturer = new Map();
|
let managedBrandToManufacturer = new Map();
|
||||||
let managedManufacturerToBrands = new Map();
|
let managedManufacturerToBrands = new Map();
|
||||||
@@ -1031,6 +1089,7 @@
|
|||||||
const indexStatusEl = document.getElementById("indexStatus");
|
const indexStatusEl = document.getElementById("indexStatus");
|
||||||
const indexSummaryEl = document.getElementById("indexSummary");
|
const indexSummaryEl = document.getElementById("indexSummary");
|
||||||
const manualStatusEl = document.getElementById("manualStatus");
|
const manualStatusEl = document.getElementById("manualStatus");
|
||||||
|
const manualRefreshBadgeEl = document.getElementById("manualRefreshBadge");
|
||||||
const manualBrandBodyEl = document.getElementById("manualBrandBody");
|
const manualBrandBodyEl = document.getElementById("manualBrandBody");
|
||||||
const manualDeviceBodyEl = document.getElementById("manualDeviceBody");
|
const manualDeviceBodyEl = document.getElementById("manualDeviceBody");
|
||||||
const addManualBrandBtnEl = document.getElementById("addManualBrandBtn");
|
const addManualBrandBtnEl = document.getElementById("addManualBrandBtn");
|
||||||
@@ -1105,6 +1164,50 @@
|
|||||||
return [...names].sort((a, b) => a.localeCompare(b));
|
return [...names].sort((a, b) => a.localeCompare(b));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildManualStatusText(payload = manualCatalog) {
|
||||||
|
const brands = Array.isArray(payload && payload.brands) ? payload.brands : [];
|
||||||
|
const devices = Array.isArray(payload && payload.devices) ? payload.devices : [];
|
||||||
|
const refresh = (payload && payload.mysql_refresh) || {};
|
||||||
|
const parts = [
|
||||||
|
`本地覆盖库: 品牌 ${brands.length} 个,设备 ${devices.length} 个。`,
|
||||||
|
"保存后会刷新索引和 MySQL seed。",
|
||||||
|
];
|
||||||
|
if (refresh.running) {
|
||||||
|
parts.push("MySQL 后台刷新中。");
|
||||||
|
} else if (refresh.last_status === "success") {
|
||||||
|
parts.push("MySQL 后台刷新已完成。");
|
||||||
|
} else if (refresh.last_status === "failed") {
|
||||||
|
parts.push(`MySQL 后台刷新失败: ${refresh.last_message || "请检查容器日志。"}`);
|
||||||
|
}
|
||||||
|
return parts.join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildManualStatusBadge(payload = manualCatalog) {
|
||||||
|
const refresh = (payload && payload.mysql_refresh) || {};
|
||||||
|
if (refresh.running) {
|
||||||
|
return '<span class="status-pill running">后台刷新中</span>';
|
||||||
|
}
|
||||||
|
if (refresh.last_status === "success") {
|
||||||
|
return '<span class="status-pill success">后台刷新完成</span>';
|
||||||
|
}
|
||||||
|
if (refresh.last_status === "failed") {
|
||||||
|
return '<span class="status-pill failed">后台刷新失败</span>';
|
||||||
|
}
|
||||||
|
return '<span class="status-pill idle">等待刷新</span>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleManualCatalogPoll() {
|
||||||
|
if (manualCatalogPollTimer) {
|
||||||
|
window.clearTimeout(manualCatalogPollTimer);
|
||||||
|
manualCatalogPollTimer = null;
|
||||||
|
}
|
||||||
|
const refresh = (manualCatalog && manualCatalog.mysql_refresh) || {};
|
||||||
|
if (!refresh.running) return;
|
||||||
|
manualCatalogPollTimer = window.setTimeout(() => {
|
||||||
|
loadManualCatalog({ silent: true });
|
||||||
|
}, 2500);
|
||||||
|
}
|
||||||
|
|
||||||
function updateSyncButtons() {
|
function updateSyncButtons() {
|
||||||
const busy = syncRunning || mysqlInitRunning;
|
const busy = syncRunning || mysqlInitRunning;
|
||||||
syncUpstreamBtnEl.disabled = busy || !syncSupported;
|
syncUpstreamBtnEl.disabled = busy || !syncSupported;
|
||||||
@@ -1992,7 +2095,8 @@
|
|||||||
function renderManualCatalog() {
|
function renderManualCatalog() {
|
||||||
const brands = Array.isArray(manualCatalog && manualCatalog.brands) ? manualCatalog.brands : [];
|
const brands = Array.isArray(manualCatalog && manualCatalog.brands) ? manualCatalog.brands : [];
|
||||||
const devices = Array.isArray(manualCatalog && manualCatalog.devices) ? manualCatalog.devices : [];
|
const devices = Array.isArray(manualCatalog && manualCatalog.devices) ? manualCatalog.devices : [];
|
||||||
manualStatusEl.textContent = `本地覆盖库: 品牌 ${brands.length} 个,设备 ${devices.length} 个。保存后会刷新索引和 MySQL seed。`;
|
manualRefreshBadgeEl.innerHTML = buildManualStatusBadge();
|
||||||
|
manualStatusEl.textContent = buildManualStatusText();
|
||||||
|
|
||||||
if (!brands.length) {
|
if (!brands.length) {
|
||||||
manualBrandBodyEl.innerHTML = `<tr><td colspan="5" class="sub">暂无手动品牌</td></tr>`;
|
manualBrandBodyEl.innerHTML = `<tr><td colspan="5" class="sub">暂无手动品牌</td></tr>`;
|
||||||
@@ -2059,15 +2163,20 @@
|
|||||||
await deleteManualDevice(deviceId);
|
await deleteManualDevice(deviceId);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
scheduleManualCatalogPoll();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadManualCatalog() {
|
async function loadManualCatalog(options = {}) {
|
||||||
|
const silent = !!options.silent;
|
||||||
|
if (!silent) {
|
||||||
manualStatusEl.textContent = "正在读取本地覆盖库。";
|
manualStatusEl.textContent = "正在读取本地覆盖库。";
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const payload = await fetchJson("/api/manual-catalog", { cache: "no-store" });
|
const payload = await fetchJson("/api/manual-catalog", { cache: "no-store" });
|
||||||
manualCatalog = {
|
manualCatalog = {
|
||||||
brands: Array.isArray(payload.brands) ? payload.brands : [],
|
brands: Array.isArray(payload.brands) ? payload.brands : [],
|
||||||
devices: Array.isArray(payload.devices) ? payload.devices : [],
|
devices: Array.isArray(payload.devices) ? payload.devices : [],
|
||||||
|
mysql_refresh: payload.mysql_refresh || null,
|
||||||
};
|
};
|
||||||
renderManualCatalog();
|
renderManualCatalog();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -2113,7 +2222,7 @@
|
|||||||
body: JSON.stringify(payload),
|
body: JSON.stringify(payload),
|
||||||
});
|
});
|
||||||
manualCatalog = result.catalog || manualCatalog;
|
manualCatalog = result.catalog || manualCatalog;
|
||||||
manualStatusEl.textContent = result.message || "品牌已保存。";
|
manualStatusEl.textContent = `${result.message || "品牌已保存。"} ${result.mysql_message || ""}`.trim();
|
||||||
renderManualCatalog();
|
renderManualCatalog();
|
||||||
await loadIndexFromPath();
|
await loadIndexFromPath();
|
||||||
await loadSyncStatus({ preserveLog: true });
|
await loadSyncStatus({ preserveLog: true });
|
||||||
@@ -2173,7 +2282,7 @@
|
|||||||
body: JSON.stringify(payload),
|
body: JSON.stringify(payload),
|
||||||
});
|
});
|
||||||
manualCatalog = result.catalog || manualCatalog;
|
manualCatalog = result.catalog || manualCatalog;
|
||||||
manualStatusEl.textContent = `${result.message || "设备已保存。"}${typeof result.alias_conflict_count === "number" ? ` 命中现有别名冲突 ${result.alias_conflict_count} 个。` : ""}`;
|
manualStatusEl.textContent = `${result.message || "设备已保存。"}${typeof result.alias_conflict_count === "number" ? ` 命中现有别名冲突 ${result.alias_conflict_count} 个。` : ""} ${result.mysql_message || ""}`.trim();
|
||||||
renderManualCatalog();
|
renderManualCatalog();
|
||||||
await loadIndexFromPath();
|
await loadIndexFromPath();
|
||||||
await loadSyncStatus({ preserveLog: true });
|
await loadSyncStatus({ preserveLog: true });
|
||||||
@@ -2188,7 +2297,7 @@
|
|||||||
body: JSON.stringify({ name }),
|
body: JSON.stringify({ name }),
|
||||||
});
|
});
|
||||||
manualCatalog = result.catalog || manualCatalog;
|
manualCatalog = result.catalog || manualCatalog;
|
||||||
manualStatusEl.textContent = result.message || `品牌 ${name} 已删除。`;
|
manualStatusEl.textContent = `${result.message || `品牌 ${name} 已删除。`} ${result.mysql_message || ""}`.trim();
|
||||||
renderManualCatalog();
|
renderManualCatalog();
|
||||||
await loadIndexFromPath();
|
await loadIndexFromPath();
|
||||||
await loadSyncStatus({ preserveLog: true });
|
await loadSyncStatus({ preserveLog: true });
|
||||||
@@ -2201,7 +2310,7 @@
|
|||||||
body: JSON.stringify({ id }),
|
body: JSON.stringify({ id }),
|
||||||
});
|
});
|
||||||
manualCatalog = result.catalog || manualCatalog;
|
manualCatalog = result.catalog || manualCatalog;
|
||||||
manualStatusEl.textContent = result.message || "设备已删除。";
|
manualStatusEl.textContent = `${result.message || "设备已删除。"} ${result.mysql_message || ""}`.trim();
|
||||||
renderManualCatalog();
|
renderManualCatalog();
|
||||||
await loadIndexFromPath();
|
await loadIndexFromPath();
|
||||||
await loadSyncStatus({ preserveLog: true });
|
await loadSyncStatus({ preserveLog: true });
|
||||||
@@ -2265,9 +2374,6 @@
|
|||||||
reloadIndexBtnEl.addEventListener("click", loadIndexFromPath);
|
reloadIndexBtnEl.addEventListener("click", loadIndexFromPath);
|
||||||
|
|
||||||
brandModalCancelBtnEl.addEventListener("click", closeBrandModal);
|
brandModalCancelBtnEl.addEventListener("click", closeBrandModal);
|
||||||
brandModalBackdropEl.addEventListener("click", (e) => {
|
|
||||||
if (e.target === brandModalBackdropEl) closeBrandModal();
|
|
||||||
});
|
|
||||||
brandModalSaveBtnEl.addEventListener("click", async () => {
|
brandModalSaveBtnEl.addEventListener("click", async () => {
|
||||||
if (!modalSaveHandler) return;
|
if (!modalSaveHandler) return;
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user