Refine brand management modal UX

This commit is contained in:
2026-04-13 18:52:56 +08:00
parent ce80e50aec
commit 305747d4ba
+436 -122
View File
@@ -14,23 +14,34 @@
--brand: #0f6fff; --brand: #0f6fff;
--ok: #0a7f3f; --ok: #0a7f3f;
--warn: #b16a00; --warn: #b16a00;
--nav-height: 52px;
} }
* { box-sizing: border-box; } * { box-sizing: border-box; }
body { body {
margin: 0; margin: 0;
padding-top: var(--nav-height);
font-family: "PingFang SC", "Noto Sans SC", "Microsoft YaHei", sans-serif; font-family: "PingFang SC", "Noto Sans SC", "Microsoft YaHei", sans-serif;
background: radial-gradient(circle at 0 0, #eef4ff 0, var(--bg) 40%), var(--bg); background: radial-gradient(circle at 0 0, #eef4ff 0, var(--bg) 40%), var(--bg);
color: var(--text); color: var(--text);
} }
body.modal-open {
overflow: hidden;
}
.top-nav { .top-nav {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 1000;
background: linear-gradient(180deg, #1f2a3a, #1a2431); background: linear-gradient(180deg, #1f2a3a, #1a2431);
border-bottom: 1px solid rgba(255, 255, 255, 0.08); border-bottom: 1px solid rgba(255, 255, 255, 0.08);
box-shadow: 0 10px 30px rgba(14, 25, 42, 0.16);
} }
.top-nav-inner { .top-nav-inner {
max-width: 1200px; max-width: 1200px;
margin: 0 auto; margin: 0 auto;
padding: 0 16px; padding: 0 16px;
height: 52px; height: var(--nav-height);
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
@@ -66,6 +77,9 @@
margin: 24px auto; margin: 24px auto;
padding: 0 16px 32px; padding: 0 16px 32px;
} }
.page-card {
padding: 16px;
}
.card { .card {
background: var(--card); background: var(--card);
border: 1px solid var(--line); border: 1px solid var(--line);
@@ -84,6 +98,20 @@
font-size: 13px; font-size: 13px;
line-height: 1.5; line-height: 1.5;
} }
.page-sub {
margin-bottom: 0;
max-width: 720px;
}
.compact-note-list {
margin: 0;
padding-left: 18px;
color: var(--sub);
font-size: 13px;
line-height: 1.65;
}
.compact-note-list li + li {
margin-top: 4px;
}
label { label {
display: block; display: block;
margin: 10px 0 6px; margin: 10px 0 6px;
@@ -128,18 +156,39 @@
align-items: center; align-items: center;
margin-bottom: 10px; margin-bottom: 10px;
} }
.pill-btn { .brand-toolbar {
border: 1px solid #b9cae8; display: grid;
border-radius: 999px; gap: 12px;
padding: 4px 10px;
background: #f5f9ff;
color: #254575;
cursor: pointer;
font-size: 12px;
font-weight: 600;
} }
.pill-btn:hover { .brand-toolbar-row {
background: #ebf3ff; display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
gap: 10px;
}
.brand-toolbar-btn {
width: 100%;
min-height: 46px;
border: 1px solid #c9d7ee;
border-radius: 12px;
padding: 10px 14px;
background: #f3f7ff;
color: #24416d;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.72);
transition: background 0.2s ease, border-color 0.2s ease, transform 0.2s ease;
}
.brand-toolbar-btn:hover {
background: #eaf1ff;
border-color: #abc0e7;
transform: translateY(-1px);
}
.brand-toolbar-btn:active {
transform: translateY(0);
}
.brand-toolbar-btn.stat-btn {
background: #f7faff;
}
.brand-toolbar-btn.action-btn {
background: #eef3fc;
} }
.table-wrap { .table-wrap {
overflow: auto; overflow: auto;
@@ -240,7 +289,7 @@
display: grid; display: grid;
gap: 8px; gap: 8px;
position: sticky; position: sticky;
top: 12px; top: calc(var(--nav-height) + 20px);
} }
.tab-btn { .tab-btn {
width: 100%; width: 100%;
@@ -257,6 +306,58 @@
.manage-panel.hidden { .manage-panel.hidden {
display: none; display: none;
} }
.panel-stack {
display: grid;
gap: 12px;
}
.section-card {
padding: 16px;
}
.section-card .title:last-child,
.section-card .sub:last-child {
margin-bottom: 0;
}
.collapse-card {
padding: 0;
overflow: hidden;
}
.collapse-card summary {
list-style: none;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 14px 16px;
font-weight: 700;
color: #1f355a;
cursor: pointer;
}
.collapse-card summary::-webkit-details-marker {
display: none;
}
.collapse-card summary::after {
content: "展开";
flex: 0 0 auto;
border: 1px solid #c8d6ee;
border-radius: 999px;
padding: 3px 10px;
font-size: 12px;
font-weight: 600;
color: #47648f;
background: #f6f9ff;
}
.collapse-card[open] summary::after {
content: "收起";
}
.collapse-card[open] summary {
border-bottom: 1px solid var(--line);
background: #fbfcff;
}
.collapse-body {
padding: 14px 16px 16px;
display: grid;
gap: 14px;
}
.sync-log { .sync-log {
min-height: 240px; min-height: 240px;
white-space: pre-wrap; white-space: pre-wrap;
@@ -269,6 +370,9 @@
padding: 10px; padding: 10px;
margin: 0; margin: 0;
} }
.sync-log.compact {
min-height: 180px;
}
.sync-schedule-card { .sync-schedule-card {
margin: 14px 0; margin: 14px 0;
padding: 12px; padding: 12px;
@@ -304,36 +408,112 @@
inset: 0; inset: 0;
background: rgba(10, 20, 38, 0.45); background: rgba(10, 20, 38, 0.45);
display: flex; display: flex;
align-items: center; align-items: flex-start;
justify-content: center; justify-content: center;
padding: 16px; padding: calc(var(--nav-height) + 24px) 16px 20px;
z-index: 999; overflow-y: auto;
z-index: 10000;
} }
.modal-backdrop.hidden { .modal-backdrop.hidden {
display: none !important; display: none !important;
} }
.modal-card { .modal-card {
width: min(920px, 100%); width: min(920px, 100%);
max-height: 90vh; max-height: calc(100vh - var(--nav-height) - 44px);
overflow: auto; min-height: min(680px, calc(100vh - var(--nav-height) - 44px));
display: flex;
flex-direction: column;
gap: 12px;
overflow: hidden;
background: #fff; background: #fff;
border: 1px solid var(--line); border: 1px solid var(--line);
border-radius: 12px; border-radius: 12px;
padding: 14px; padding: 14px;
box-shadow: 0 12px 28px rgba(0, 0, 0, 0.24); box-shadow: 0 12px 28px rgba(0, 0, 0, 0.24);
} }
.modal-head {
display: grid;
gap: 8px;
padding-bottom: 2px;
border-bottom: 1px solid #eef2f8;
}
.modal-card .title,
.modal-card .sub {
margin-bottom: 0;
}
.modal-content {
flex: 1 1 auto;
min-height: 320px;
overflow: auto;
display: flex;
}
.modal-list {
width: 100%;
height: 100%;
overflow: auto;
border: 1px solid var(--line);
border-radius: 10px;
background: #fcfdff;
padding: 12px;
}
.modal-list-grid {
display: grid;
gap: 10px;
}
.modal-list-item {
border: 1px solid #dbe4f2;
border-radius: 10px;
background: #fff;
padding: 12px 14px;
display: grid;
gap: 8px;
}
.modal-list-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
flex-wrap: wrap;
}
.modal-list-title {
font-size: 16px;
font-weight: 700;
color: #203554;
}
.modal-list-meta {
font-size: 12px;
color: #62728a;
}
.modal-list-tags {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.modal-list-empty {
height: 100%;
display: grid;
place-items: center;
color: var(--sub);
font-size: 14px;
}
.modal-card textarea { .modal-card textarea {
width: 100%; width: 100%;
min-height: 360px; min-height: 420px;
height: auto;
resize: none;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 12px; font-size: 12px;
line-height: 1.45; line-height: 1.45;
border: 1px solid var(--line); border: 1px solid var(--line);
border-radius: 10px; border-radius: 10px;
padding: 10px; padding: 10px;
overflow: auto;
background: #fcfdff;
} }
.modal-card pre { .modal-card pre {
min-height: 240px; min-height: 100%;
height: 100%;
width: 100%;
white-space: pre-wrap; white-space: pre-wrap;
word-break: break-word; word-break: break-word;
font-size: 12px; font-size: 12px;
@@ -343,6 +523,15 @@
border-radius: 10px; border-radius: 10px;
padding: 10px; padding: 10px;
margin: 0; margin: 0;
overflow: auto;
}
.modal-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
padding-top: 10px;
border-top: 1px solid #eef2f8;
flex: 0 0 auto;
} }
@media (max-width: 1020px) { @media (max-width: 1020px) {
.manage-layout { .manage-layout {
@@ -353,6 +542,24 @@
grid-template-columns: repeat(4, minmax(0, 1fr)); grid-template-columns: repeat(4, minmax(0, 1fr));
} }
} }
@media (max-width: 720px) {
.brand-toolbar-row {
grid-template-columns: 1fr;
}
.brand-toolbar-btn,
.brand-toolbar-btn.stat-btn,
.brand-toolbar-btn.action-btn {
width: 100%;
min-width: 0;
}
.modal-backdrop {
padding: calc(var(--nav-height) + 12px) 12px 12px;
}
.modal-card {
max-height: calc(100vh - var(--nav-height) - 24px);
min-height: calc(100vh - var(--nav-height) - 24px);
}
}
</style> </style>
</head> </head>
<body> <body>
@@ -366,8 +573,9 @@
</nav> </nav>
<div class="wrap"> <div class="wrap">
<section class="card"> <section class="card page-card">
<h1 class="title">数据管理</h1> <h1 class="title">数据管理</h1>
<p class="sub page-sub">把常用操作留在前面,把低频说明收起来。</p>
<div class="manage-layout"> <div class="manage-layout">
<aside class="manage-tabs"> <aside class="manage-tabs">
@@ -379,112 +587,157 @@
<div class="manage-content"> <div class="manage-content">
<section id="brandTabPanel" class="manage-panel"> <section id="brandTabPanel" class="manage-panel">
<div class="result-head"> <div class="panel-stack">
<button id="brandCountBtn" type="button" class="pill-btn">品牌数: -</button> <article class="card section-card">
<button id="manufacturerCountBtn" type="button" class="pill-btn">厂商数: -</button> <div class="brand-toolbar">
</div> <div class="brand-toolbar-row">
<button id="brandCountBtn" type="button" class="brand-toolbar-btn stat-btn">品牌数: -</button>
<button id="manufacturerCountBtn" type="button" class="brand-toolbar-btn stat-btn">厂商数: -</button>
<button id="editBrandListBtn" type="button" class="brand-toolbar-btn action-btn">编辑品牌列表</button>
<button id="editBrandRelationsBtn" type="button" class="brand-toolbar-btn action-btn">编辑品牌-厂商关系</button>
<button id="editBrandAliasesBtn" type="button" class="brand-toolbar-btn action-btn">编辑品牌同义词</button>
</div>
</div>
<div id="brandStats" class="sub">索引未加载。</div>
</article>
<div class="btns"> <article class="card section-card">
<button id="editBrandListBtn" type="button">编辑品牌列表</button> <div class="table-wrap">
<button id="editBrandRelationsBtn" type="button">编辑品牌-厂商关系</button> <table>
<button id="editBrandAliasesBtn" type="button">编辑品牌同义词</button> <thead>
</div> <tr>
<th>品牌</th>
<div id="brandStats" class="sub">索引未加载。</div> <th>品牌同义词</th>
<th>父级厂商</th>
<div class="table-wrap"> </tr>
<table> </thead>
<thead> <tbody id="brandRelationBody">
<tr> <tr><td colspan="3" class="sub">暂无关系数据</td></tr>
<th>品牌</th> </tbody>
<th>品牌同义词</th> </table>
<th>父级厂商</th> </div>
</tr> </article>
</thead>
<tbody id="brandRelationBody">
<tr><td colspan="3" class="sub">暂无关系数据</td></tr>
</tbody>
</table>
</div> </div>
</section> </section>
<section id="sourceTabPanel" class="manage-panel hidden"> <section id="sourceTabPanel" class="manage-panel hidden">
<h3 class="title">数据来源管理(权重排序)</h3> <div class="panel-stack">
<p class="sub">拖拽调整优先级。越靠前权重越高。初始化规则:`_cn.md` 在前,非 `cn` 在后。</p> <article class="card section-card">
<div class="btns"> <h3 class="title">数据来源排序</h3>
<button id="saveSourceOrderBtn" type="button" class="primary">保存来源排序</button> <p class="sub">拖拽调整优先级,越靠前权重越高。</p>
<button id="resetSourceOrderBtn" type="button">重置来源排序</button> <div class="btns">
</div> <button id="saveSourceOrderBtn" type="button" class="primary">保存来源排序</button>
<div id="sourceOrderStats" class="sub">来源列表未加载。</div> <button id="resetSourceOrderBtn" type="button">重置来源排序</button>
<div class="source-order-wrap"> </div>
<ul id="sourceOrderList" class="source-order-list"> <div id="sourceOrderStats" class="sub">来源列表未加载。</div>
<li class="sub">暂无来源数据</li> </article>
</ul>
<article class="card section-card">
<div class="source-order-wrap">
<ul id="sourceOrderList" class="source-order-list">
<li class="sub">暂无来源数据</li>
</ul>
</div>
</article>
</div> </div>
</section> </section>
<section id="syncTabPanel" class="manage-panel hidden"> <section id="syncTabPanel" class="manage-panel hidden">
<h3 class="title">原始数据同步</h3> <div class="panel-stack">
<p class="sub">从上游 `KHwang9883/MobileModels` 拉取原始 markdown 数据,并重建 `dist/device_index.json`。如已开启 MySQL 自动装载,也会同步刷新 MySQL。请先启动完整服务。</p> <article class="card section-card">
<div class="sync-schedule-card"> <h3 class="title">原始数据同步</h3>
<h4 class="title">MySQL 自动装载</h4> <ul class="compact-note-list">
<p class="sub">控制同步任务和容器后续启动时是否自动导入 schema 与 seed。保持关闭更安全;开启后,启动容器和“开始同步原始数据”都可能刷新 MySQL 数据</p> <li>从上游拉取原始 markdown,并重建 <code>dist/device_index.json</code></li>
<div class="sync-schedule-grid"> <li>如果已开启 MySQL 自动装载,同步时也会刷新 MySQL。</li>
<label class="check-row"> <li>开始前请确认完整服务已经启动。</li>
<input id="mysqlAutoLoadEnabled" type="checkbox" /> </ul>
<span>启用 MySQL 自动装载</span> </article>
</label>
</div> <details class="card collapse-card">
<div class="btns"> <summary>同步配置</summary>
<button id="saveMysqlSettingsBtn" type="button">保存 MySQL 设置</button> <div class="collapse-body">
</div> <div class="sync-schedule-card">
<div id="mysqlSettingsStatus" class="sub">正在读取 MySQL 自动装载设置。</div> <h4 class="title">MySQL 自动装载</h4>
</div> <p class="sub">控制同步任务和容器后续启动时是否自动导入 schema 与 seed。</p>
<div class="sync-schedule-card"> <div class="sync-schedule-grid">
<h4 class="title">外部 MySQL 初始化</h4> <label class="check-row">
<p class="sub">面向关闭自动装载的外部 MySQL。点击后会执行 schema 与 seed 导入,自动创建数据库,并重建 `mobilemodels` 相关表与视图。请确认连接参数与账号权限无误后再执行。</p> <input id="mysqlAutoLoadEnabled" type="checkbox" />
<div class="btns"> <span>启用 MySQL 自动装载</span>
<button id="initMysqlBtn" type="button">初始化外部 MySQL</button> </label>
</div> </div>
</div> <div class="btns">
<div class="sync-schedule-card"> <button id="saveMysqlSettingsBtn" type="button">保存 MySQL 设置</button>
<h4 class="title">每日自动同步</h4> </div>
<p class="sub">在项目容器内按固定时间自动拉取上游原始数据,并重建索引与 MySQL Seed。时间按容器时区执行,设置会持久化到运行期数据目录</p> <div id="mysqlSettingsStatus" class="sub">正在读取 MySQL 自动装载设置</div>
<div class="sync-schedule-grid"> </div>
<label class="check-row">
<input id="scheduleEnabled" type="checkbox" /> <div class="sync-schedule-card">
<span>启用每日自动同步</span> <h4 class="title">外部 MySQL 初始化</h4>
</label> <p class="sub">面向关闭自动装载的外部 MySQL,需要时再执行。</p>
<div> <div class="btns">
<label for="scheduleTimeInput">每日同步时间</label> <button id="initMysqlBtn" type="button">初始化外部 MySQL</button>
<input id="scheduleTimeInput" type="time" step="60" value="03:00" /> </div>
</div>
<div class="sync-schedule-card">
<h4 class="title">每日自动同步</h4>
<p class="sub">按固定时间自动拉取上游并重建索引与 MySQL Seed。</p>
<div class="sync-schedule-grid">
<label class="check-row">
<input id="scheduleEnabled" type="checkbox" />
<span>启用每日自动同步</span>
</label>
<div>
<label for="scheduleTimeInput">每日同步时间</label>
<input id="scheduleTimeInput" type="time" step="60" value="03:00" />
</div>
<div class="full-row">
<label for="githubProxyPrefixInput">GitHub 加速前缀</label>
<input id="githubProxyPrefixInput" type="text" placeholder="例如 https://ghfast.top/" />
</div>
</div>
<div class="btns">
<button id="saveSyncScheduleBtn" type="button" class="primary">保存同步设置</button>
</div>
<div id="scheduleStatus" class="sub">正在读取自动同步设置。</div>
</div>
</div> </div>
<div class="full-row"> </details>
<label for="githubProxyPrefixInput">GitHub 加速前缀</label>
<input id="githubProxyPrefixInput" type="text" placeholder="例如 https://ghfast.top/" /> <article class="card section-card">
<div class="btns">
<button id="syncUpstreamBtn" type="button" class="primary">开始同步原始数据</button>
<button id="refreshSyncStatusBtn" type="button">刷新同步状态</button>
</div> </div>
</div> <div id="syncStatus" class="sub">正在检测同步能力。</div>
<div class="btns"> </article>
<button id="saveSyncScheduleBtn" type="button" class="primary">保存同步设置</button>
</div> <details class="card collapse-card">
<div id="scheduleStatus" class="sub">正在读取自动同步设置。</div> <summary>任务日志</summary>
<div class="collapse-body">
<pre id="syncLog" class="sync-log compact mono">暂无同步记录</pre>
</div>
</details>
</div> </div>
<div class="btns">
<button id="syncUpstreamBtn" type="button" class="primary">开始同步原始数据</button>
<button id="refreshSyncStatusBtn" type="button">刷新同步状态</button>
</div>
<div id="syncStatus" class="sub">正在检测同步能力。</div>
<pre id="syncLog" class="sync-log mono">暂无同步记录</pre>
</section> </section>
<section id="indexTabPanel" class="manage-panel hidden"> <section id="indexTabPanel" class="manage-panel hidden">
<h3 class="title">索引数据</h3> <div class="panel-stack">
<p class="sub">这里集中显示 `dist/device_index.json` 的加载状态与基础统计,并提供手动重新加载入口。</p> <article class="card section-card">
<div class="btns"> <h3 class="title">索引数据</h3>
<button id="reloadIndexBtn" type="button" class="primary">重新加载索引</button> <p class="sub">查看当前索引加载状态,并在需要时手动刷新。</p>
<div class="btns">
<button id="reloadIndexBtn" type="button" class="primary">重新加载索引</button>
</div>
<div id="indexStatus" class="sub">索引尚未加载。</div>
</article>
<details class="card collapse-card">
<summary>索引详情</summary>
<div class="collapse-body">
<pre id="indexSummary" class="sync-log compact mono">暂无索引信息</pre>
</div>
</details>
</div> </div>
<div id="indexStatus" class="sub">索引尚未加载。</div>
<pre id="indexSummary" class="sync-log mono">暂无索引信息</pre>
</section> </section>
</div> </div>
</div> </div>
@@ -493,11 +746,16 @@
<div id="brandModalBackdrop" class="modal-backdrop hidden"> <div id="brandModalBackdrop" class="modal-backdrop hidden">
<div class="modal-card"> <div class="modal-card">
<h3 id="brandModalTitle" class="title">数据管理</h3> <div class="modal-head">
<p id="brandModalHint" class="sub"></p> <h3 id="brandModalTitle" class="title">数据管理</h3>
<textarea id="brandModalTextarea" class="hidden"></textarea> <p id="brandModalHint" class="sub"></p>
<pre id="brandModalPre" class="hidden"></pre> </div>
<div class="btns"> <div class="modal-content">
<textarea id="brandModalTextarea" class="hidden"></textarea>
<pre id="brandModalPre" class="hidden"></pre>
<div id="brandModalList" class="modal-list hidden"></div>
</div>
<div class="modal-actions">
<button id="brandModalCancelBtn" type="button">关闭</button> <button id="brandModalCancelBtn" type="button">关闭</button>
<button id="brandModalSaveBtn" type="button" class="primary">保存</button> <button id="brandModalSaveBtn" type="button" class="primary">保存</button>
</div> </div>
@@ -526,6 +784,7 @@
const brandModalHintEl = document.getElementById("brandModalHint"); const brandModalHintEl = document.getElementById("brandModalHint");
const brandModalTextareaEl = document.getElementById("brandModalTextarea"); const brandModalTextareaEl = document.getElementById("brandModalTextarea");
const brandModalPreEl = document.getElementById("brandModalPre"); const brandModalPreEl = document.getElementById("brandModalPre");
const brandModalListEl = document.getElementById("brandModalList");
const brandModalSaveBtnEl = document.getElementById("brandModalSaveBtn"); const brandModalSaveBtnEl = document.getElementById("brandModalSaveBtn");
const brandModalCancelBtnEl = document.getElementById("brandModalCancelBtn"); const brandModalCancelBtnEl = document.getElementById("brandModalCancelBtn");
const sourceOrderStatsEl = document.getElementById("sourceOrderStats"); const sourceOrderStatsEl = document.getElementById("sourceOrderStats");
@@ -734,7 +993,7 @@
async function runMysqlInit() { async function runMysqlInit() {
if (syncRunning || mysqlInitRunning) return; if (syncRunning || mysqlInitRunning) return;
const confirmed = window.confirm( const confirmed = window.confirm(
"初始化外部 MySQL 会创建数据库,并重建 mobilemodels 相关表、视图和 seed 数据。是否继续?" "初始化外部 MySQL 会创建数据库,并重建 mobilemodels 主表、相关索引和 seed 数据,同时清理历史兼容对象。是否继续?"
); );
if (!confirmed) return; if (!confirmed) return;
@@ -1183,7 +1442,38 @@
renderSourceOrder(); renderSourceOrder();
} }
function openBrandModal({ title, hint, text, editable, onSave }) { function renderModalList(items = []) {
if (!items.length) {
brandModalListEl.innerHTML = `<div class="modal-list-empty">暂无数据</div>`;
return;
}
brandModalListEl.innerHTML = `
<div class="modal-list-grid">
${items.map((item) => `
<section class="modal-list-item">
<div class="modal-list-head">
<div class="modal-list-title">${escapeHtml(item.title || "-")}</div>
${item.meta ? `<div class="modal-list-meta">${escapeHtml(item.meta)}</div>` : ""}
</div>
${item.tags && item.tags.length ? `
<div class="modal-list-tags">
${item.tags.map((tag) => `<span class="tag">${escapeHtml(tag)}</span>`).join("")}
</div>
` : ""}
</section>
`).join("")}
</div>
`;
}
function resizeBrandModalTextarea() {
if (brandModalTextareaEl.classList.contains("hidden")) return;
brandModalTextareaEl.style.height = "auto";
brandModalTextareaEl.style.height = `${Math.max(420, brandModalTextareaEl.scrollHeight + 24)}px`;
}
function openBrandModal({ title, hint, text, editable, onSave, listItems }) {
modalSaveHandler = onSave || null; modalSaveHandler = onSave || null;
brandModalTitleEl.textContent = title || "数据管理"; brandModalTitleEl.textContent = title || "数据管理";
brandModalHintEl.textContent = hint || ""; brandModalHintEl.textContent = hint || "";
@@ -1192,20 +1482,31 @@
brandModalTextareaEl.classList.remove("hidden"); brandModalTextareaEl.classList.remove("hidden");
brandModalTextareaEl.value = text || ""; brandModalTextareaEl.value = text || "";
brandModalPreEl.classList.add("hidden"); brandModalPreEl.classList.add("hidden");
brandModalListEl.classList.add("hidden");
brandModalSaveBtnEl.classList.remove("hidden"); brandModalSaveBtnEl.classList.remove("hidden");
requestAnimationFrame(resizeBrandModalTextarea);
} else if (listItems) {
brandModalListEl.classList.remove("hidden");
renderModalList(listItems);
brandModalPreEl.classList.add("hidden");
brandModalTextareaEl.classList.add("hidden");
brandModalSaveBtnEl.classList.add("hidden");
} else { } else {
brandModalPreEl.classList.remove("hidden"); brandModalPreEl.classList.remove("hidden");
brandModalPreEl.textContent = text || ""; brandModalPreEl.textContent = text || "";
brandModalTextareaEl.classList.add("hidden"); brandModalTextareaEl.classList.add("hidden");
brandModalListEl.classList.add("hidden");
brandModalSaveBtnEl.classList.add("hidden"); brandModalSaveBtnEl.classList.add("hidden");
} }
brandModalBackdropEl.classList.remove("hidden"); brandModalBackdropEl.classList.remove("hidden");
document.body.classList.add("modal-open");
} }
function closeBrandModal() { function closeBrandModal() {
modalSaveHandler = null; modalSaveHandler = null;
brandModalBackdropEl.classList.add("hidden"); brandModalBackdropEl.classList.add("hidden");
document.body.classList.remove("modal-open");
} }
function applyManagedBrandConfigUpdate(newConfig) { function applyManagedBrandConfigUpdate(newConfig) {
@@ -1218,8 +1519,14 @@
function openBrandListModal() { function openBrandListModal() {
openBrandModal({ openBrandModal({
title: "品牌列表", title: "品牌列表",
hint: "当前独立维护的品牌列表(含同义词与父级厂商。", hint: "当前独立维护的品牌列表,按品牌查看同义词与所属厂商。",
text: JSON.stringify(managedBrandConfig.brands, null, 2), listItems: [...managedBrandConfig.brands]
.sort((a, b) => a.name.localeCompare(b.name))
.map((brand) => ({
title: brand.name,
meta: `所属厂商:${managedBrandToManufacturer.get(brand.name) || brand.manufacturer || "-"}`,
tags: normalizeAliasList(brand.name, brand.aliases || []),
})),
editable: false, editable: false,
}); });
} }
@@ -1231,8 +1538,14 @@
})); }));
openBrandModal({ openBrandModal({
title: "厂商列表", title: "厂商列表",
hint: "当前独立维护的厂商列表(含所属品牌。", hint: "当前独立维护的厂商列表,按厂商查看归属品牌。",
text: JSON.stringify(rows, null, 2), listItems: rows
.sort((a, b) => a.manufacturer.localeCompare(b.manufacturer))
.map((row) => ({
title: row.manufacturer,
meta: `品牌数:${row.brands.length}`,
tags: row.brands,
})),
editable: false, editable: false,
}); });
} }
@@ -1469,6 +1782,7 @@
brandStatsEl.textContent = `保存失败: ${err.message}`; brandStatsEl.textContent = `保存失败: ${err.message}`;
} }
}); });
brandModalTextareaEl.addEventListener("input", resizeBrandModalTextarea);
function switchManageTab(tab) { function switchManageTab(tab) {
const isBrand = tab === "brand"; const isBrand = tab === "brand";