Refine brand management modal UX
This commit is contained in:
+436
-122
@@ -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";
|
||||||
|
|||||||
Reference in New Issue
Block a user