refactor(ui): 前端图标统一为自绘线性 SVG 图标,替换原有 emoji 图标 (#473)

* refactor(ui): 界面图标统一为自绘线性 SVG 图标,替换原有 emoji 图标

界面中充当图标的 emoji 除底部彩色的 TG 链接外,全部替换为统一的线性图标。新增
ui/js/icons.js 图标库(32 个图标,24×24 网格、2px 圆头描边),以 SVG sprite 内联于
页面并通过 <use> 引用;图标尺寸随所在文字字号缩放,描边取 currentColor,颜色随容器、
八种强调色主题与明暗模式适配。

- 按钮、配置分类导航、搜索分组标题、状态行、徽标与映射行的图标全部替换
- 图标与文字并列处改用 flex 容器居中对齐
- 「重新部署」使用云与上行箭头,「弹幕配置」使用评论气泡
- 合并模式徽标切换改为只更新标签文本
- 为 .confirm-merge-btn:disabled 设置文字颜色

* refactor(ui): 配置分类导航统一渲染并补齐配置界面细节

- 映射表编辑界面(TITLE_MAPPING_TABLE、AUTO_MATCH_MAPPING_TABLE)增加「查看最近数据」按钮,仅查看缓存、不参与自动填充
- 配置预览与环境变量配置的分类按钮、搜索分组标题统一为同一渲染函数,消除两份平行实现
- 页头「当前版本」「最新版本」「API 端点」徽标补齐图标并对齐风格,当前版本为主视觉、最新版本为次要
- 各样式表头注释中「ACG 设计系统」描述改为「柔和圆角设计风格」

* feat(ui): 映射表查看数据入口调整与移动端头部、徽标动效优化

- 映射表编辑界面「查看最近数据」按钮移到「添加映射项」右侧并右对齐
- 移动端主标题「LogVar弹幕API」放大,突出主标题层级
- 「最新版本」徽标改为持续呼吸缩放动效,引导点击而不盖过「当前版本」

* feat(ui): 移动端标题微调、系统配置按钮缩小并补充弹幕词库贡献者

- 移动端主标题字号调整为 21px
- 移动端系统配置的导出/导入/清理缓存/重新部署按钮缩小
- 弹幕词库新增贡献者 @xlmc、@mz289、@sugarbliss
This commit is contained in:
Elegy233 authored and GitHub committed 2026-09-12 07:51:27 +08:00
1 parent c81f63993c
commit 96602d6a7e
9 files changed
+262 -151

No files matched your search

+13 -41
View File
@@ -1,6 +1,6 @@
// language=CSS
export const baseCssContent = /* css */ `
/* 基础布局样式 — 参照 Bangumi-syncer 设计系统 */
/* 基础布局样式 — 参照 Bangumi-syncer 柔和圆角设计风格 */
/* ============ 全局重置 ============ */
* {
@@ -206,8 +206,8 @@ body::before {
}
.version-badge {
background: var(--theme-accent-soft);
color: var(--theme-accent);
background: var(--theme-accent);
color: #ffffff;
padding: 3px 12px;
border-radius: 999px;
font-weight: 600;
@@ -219,16 +219,16 @@ body::before {
}
.update-badge {
background: var(--theme-accent);
color: #ffffff;
background: var(--theme-accent-soft);
color: var(--theme-accent);
padding: 3px 12px;
border-radius: 999px;
font-weight: 600;
font-size: 11px;
animation: pulse 2s infinite;
cursor: pointer;
text-decoration: none;
transition: transform 0.2s ease, box-shadow 0.2s ease;
animation: badge-breathe 2.4s ease-in-out infinite;
transition: box-shadow 0.2s ease;
position: relative;
z-index: 1;
-webkit-backdrop-filter: blur(8px);
@@ -236,10 +236,14 @@ body::before {
}
.update-badge:hover {
transform: scale(1.04);
box-shadow: var(--app-shadow-sm);
}
@keyframes badge-breathe {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.05); }
}
.api-endpoint-badge {
background: rgba(107, 138, 255, 0.10);
color: #6B8AFF;
@@ -254,11 +258,6 @@ body::before {
backdrop-filter: blur(8px);
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.6; }
}
/* ============ 内容区 ============ */
.content {
padding: 24px 28px;
@@ -376,32 +375,5 @@ body::before {
}
}
@media (max-width: 768px) {
body {
padding: 0;
}
.container {
border-radius: 0;
box-shadow: none;
}
.header {
padding: 12px 14px;
}
.content {
padding: 16px;
}
.logo {
width: 36px;
height: 36px;
font-size: 20px;
}
.header h1 {
font-size: 17px;
}
}
`;
+35 -4
View File
@@ -1,6 +1,30 @@
// language=CSS
export const componentsCssContent = /* css */ `
/* 组件样式 — 参照 Bangumi-syncer ACG 设计系统 */
/* 组件样式 — 参照 Bangumi-syncer 柔和圆角设计风格 */
/* ============ 图标 ============ */
/* 统一线性图标:尺寸随字号(1em),描边取 currentColor */
.ui-icon-sprite {
position: absolute;
width: 0;
height: 0;
overflow: hidden;
}
.ui-icon {
display: inline-block;
flex: none;
width: 1em;
height: 1em;
vertical-align: -0.125em;
}
/* 图标与文字成对出现时的对齐容器(flex 居中) */
.ui-icon-label {
display: inline-flex;
align-items: center;
gap: 0.4em;
}
/* ============ 标签导航 ============ */
.nav-buttons {
@@ -152,7 +176,10 @@ export const componentsCssContent = /* css */ `
font-size: 13px;
font-weight: 500;
transition: all 0.22s var(--app-ease-smooth);
display: inline-block;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
text-align: center;
line-height: 1.4;
}
@@ -705,7 +732,7 @@ body[data-theme] .favorite-schedule-btn:disabled:hover {
display: grid;
place-items: center;
color: var(--theme-muted);
font-size: 24px;
font-size: 16px;
line-height: 1;
}
@@ -716,7 +743,7 @@ body[data-theme] .favorite-schedule-btn:disabled:hover {
.preview-group-heading {
min-height: 32px;
display: flex;
align-items: baseline;
align-items: center;
justify-content: space-between;
gap: 10px;
padding: 0 2px 8px;
@@ -826,6 +853,9 @@ body[data-theme] .favorite-schedule-btn:disabled:hover {
.preview-copy-btn {
padding: 0;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 16px;
}
@@ -1737,6 +1767,7 @@ body[data-theme] input[type="checkbox"].app-checkbox:focus-visible {
.confirm-merge-btn:disabled {
background: var(--theme-panel-strong);
color: var(--theme-muted);
cursor: not-allowed;
box-shadow: none;
}
+2 -2
View File
@@ -49,7 +49,7 @@ export const responsiveCssContent = /* css */ `
}
.header h1 {
font-size: 19px;
font-size: 21px;
margin: 0;
}
@@ -214,7 +214,7 @@ export const responsiveCssContent = /* css */ `
/* ============ 小屏手机 (<= 480px) ============ */
@media (max-width: 480px) {
.header h1 {
font-size: 18px;
font-size: 21px;
}
.nav-btn {
+2 -3
View File
@@ -1,6 +1,6 @@
// language=CSS
export const themesCssContent = /* css */ `
/* 设计令牌与 ACG 七色主题 + 独立明暗切换 — 参照 Bangumi-syncer 设计系统 */
/* 设计令牌与七色主题 + 独立明暗切换 — 参照 Bangumi-syncer 柔和圆角设计风格 */
/* ============ 全局设计令牌 ============ */
:root {
@@ -216,7 +216,6 @@ body[data-theme] .theme-option:focus-visible, body[data-theme] .btn:focus-visibl
.theme-option-label { font-size: 11px; font-weight: 600; white-space: nowrap; }
.config-transfer-btn { display: inline-flex; align-items: center; gap: 7px; }
.env-toolbar-actions .btn { align-items: center; display: inline-flex; justify-content: center; gap: 7px; line-height: 1.2; min-height: 38px; white-space: nowrap; }
.config-transfer-icon { font-size: 20px; font-weight: 700; line-height: 1; }
/* ============ 自定义滚动条 ============ */
::-webkit-scrollbar { width: 8px; height: 8px; }
@@ -235,7 +234,7 @@ body[data-color-scheme="dark"] #bg-danmaku-layer span { opacity: 0.08 !important
.env-section-header { align-items: stretch !important; }
.env-section-header > div:first-child { min-width: 0; width: 100%; }
.env-toolbar-actions { display: grid !important; grid-template-columns: repeat(2, minmax(0, 1fr)); width: 100%; }
.env-toolbar-actions .btn { flex: none; min-height: 44px; padding: 10px 8px; width: 100%; }
.env-toolbar-actions .btn { flex: none; min-height: 34px; padding: 6px 10px; width: 100%; }
.theme-settings { align-items: stretch; flex-direction: column; }
.theme-options { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
+107
View File
@@ -0,0 +1,107 @@
// language=JavaScript
// 界面图标库:集中定义界面使用的线性图标,统一 24×24 网格、2px 圆头描边。
// UI_ICON_PATHS —— 图标名到 SVG 图形的映射;
// iconsSpriteContent —— 内联 SVG sprite,供 <use> 引用;
// renderIcon / iconJsContent —— 分别供服务端模板与浏览器动态内容生成 <use> 引用。
// 图标颜色与尺寸统一由 components.css.js 的 .ui-icon 控制。
// 图标几何数据:名称 -> symbol 内部图形
const UI_ICON_PATHS = {
// ===== 明暗模式切换 =====
// 太阳:明暗切换按钮在暗色模式下显示
sun: '<circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.42 1.42M17.65 17.65l1.42 1.42M2 12h2M20 12h2M6.35 17.65l-1.42 1.42M19.07 4.93l-1.42 1.42"/>',
// 月亮:明暗切换按钮在亮色模式下显示
moon: '<path d="M21 12.79A9 9 0 1 1 11.21 3a7 7 0 0 0 9.79 9.79z"/>',
// ===== 状态提示 =====
// 星光:页头「最新版本」徽标
sparkles: '<path d="M12 3l1.8 5.2L19 10l-5.2 1.8L12 17l-1.8-5.2L5 10l5.2-1.8z"/><path d="M18.4 15.3l.7 2 2 .7-2 .7-.7 2-.7-2-2-.7 2-.7z"/>',
// 标签:页头「当前版本」徽标
tag: '<path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"/><path d="M7 7h.01"/>',
// 警告三角:配置获取失败、Cookie/API Key 未配置、检测失败
'alert-triangle': '<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><path d="M12 9v4M12 17h.01"/>',
// 成功圆:已登录、扫码登录成功、连通性测试通过
'check-circle': '<circle cx="12" cy="12" r="9"/><path d="M8.4 12.4l2.5 2.5 4.7-4.9"/>',
// 失败圆:Cookie 失效、二维码过期、连通性测试失败
'x-circle': '<circle cx="12" cy="12" r="9"/><path d="M15 9l-6 6M9 9l6 6"/>',
// 时钟:等待扫码、正在生成二维码
clock: '<circle cx="12" cy="12" r="9"/><path d="M12 7v5.2l3.2 1.9"/>',
// ===== 通用操作 =====
// 刷新:刷新日志、刷新请求记录
'refresh-cw': '<path d="M21 3v5h-5"/><path d="M3 21v-5h5"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L21 8"/><path d="M20.49 15a9 9 0 0 1-14.85 3.36L3 16"/>',
// 垃圾桶:清空日志、清理缓存
'trash-2': '<path d="M3 6h18"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><path d="M10 11v6M14 11v6"/>',
// 上传(托盘 + 向上箭头):导出配置文件
upload: '<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="M17 8l-5-5-5 5"/><path d="M12 3v12"/>',
// 下载(托盘 + 向下箭头):导入配置文件
download: '<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="M7 10l5 5 5-5"/><path d="M12 15V3"/>',
// 云 + 上行箭头:重新部署系统
'cloud-up': '<path d="M17.5 11H6.5a3 3 0 0 1-.5-5A6 6 0 0 1 17 6a3 3 0 0 1 .5 5z"/><path d="M12 21v-6.6"/><path d="M8.7 17.7l3.3-3.3 3.3 3.3"/>',
// 复制:复制配置值
copy: '<rect x="9" y="9" width="12" height="12" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>',
// 软盘:Bilibili Cookie 待保存提示
save: '<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/><path d="M17 21v-8H7v8"/><path d="M7 3v5h8"/>',
// ===== 勾选与方向 =====
// 对勾:复制成功、合并确认、映射「匹配」状态
check: '<path d="M20 6L9 17l-5-5"/>',
// 叉:映射「落单」状态
x: '<path d="M18 6L6 18M6 6l12 12"/>',
// 向上箭头:数字步进器增大
'chevron-up': '<path d="M18 15l-6-6-6 6"/>',
// 向下箭头:数字步进器减小
'chevron-down': '<path d="M6 9l6 6 6-6"/>',
// 向右箭头:总览卡片进入指示
'chevron-right': '<path d="M9 18l6-6-6-6"/>',
// ===== 导航与分类 =====
// 链接:API 配置分类、开启合并模式、被合并源数量徽标
link: '<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>',
// 断开链接:关闭合并模式
unlink: '<path d="M18.84 12.25l1.72-1.71a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M5.17 11.75l-1.71 1.71a5 5 0 0 0 7.07 7.07l1.71-1.71"/><path d="M8 2v3M2 8h3M16 22v-3M22 16h-3"/>',
// 叠层:源配置分类
layers: '<path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/>',
// 放大镜:匹配配置分类、检测中状态
search: '<circle cx="11" cy="11" r="7"/><path d="M20.5 20.5l-4.4-4.4"/>',
// 评论气泡(方形 + 文本行):弹幕配置分类
comment: '<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/><path d="M8 8h8M8 12h5"/>',
// 数据库(圆柱):缓存配置分类
database: '<ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M3 5v14c0 1.66 4.03 3 9 3s9-1.34 9-3V5"/><path d="M3 12c0 1.66 4.03 3 9 3s9-1.34 9-3"/>',
// 齿轮:系统配置分类
settings: '<circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.6a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/>',
// 四宫格:配置总览按钮
'layout-grid': '<rect x="3" y="3" width="7" height="7" rx="1.5"/><rect x="14" y="3" width="7" height="7" rx="1.5"/><rect x="14" y="14" width="7" height="7" rx="1.5"/><rect x="3" y="14" width="7" height="7" rx="1.5"/>',
// 列表:展开 / 收起映射详情
list: '<path d="M8 6h13M8 12h13M8 18h13"/><path d="M3.5 6h.01M3.5 12h.01M3.5 18h.01"/>',
// 柱状图:查看最近数据
'bar-chart': '<path d="M3 21h18"/><path d="M6 21v-9M12 21V5M18 21v-6"/>',
// 胶片:剧集数量徽标
film: '<rect x="2" y="3" width="20" height="18" rx="2"/><path d="M7 3v18M17 3v18M2 9h5M2 15h5M17 9h5M17 15h5"/>',
// 二维码:扫码登录
'qr-code': '<rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><path d="M14 14h3v3h-3z"/><path d="M21 14v3M21 21h.01M14 21h3"/>',
// 锥形瓶:AI API Key 连通性测试
flask: '<path d="M10 2v7.3L4.6 18.9A1.5 1.5 0 0 0 5.9 21h12.2a1.5 1.5 0 0 0 1.3-2.1L14 9.3V2"/><path d="M8.5 2h7"/><path d="M7.2 14h9.6"/>',
};
// 内联 SVG sprite:集中所有 <symbol> 定义,供 <use href="#icon-..."> 引用
export const iconsSpriteContent = `<svg class="ui-icon-sprite" aria-hidden="true" focusable="false">${Object.entries(UI_ICON_PATHS).map(([name, paths]) => `<symbol id="icon-${name}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${paths}</symbol>`).join('')}</svg>`;
// 服务端模板用(template.js 在 Node 中拼接 HTML 时调用):生成引用 sprite 的图标
export function renderIcon(name, className = 'ui-icon') {
return `<svg class="${className}" aria-hidden="true" focusable="false"><use href="#icon-${name}"></use></svg>`;
}
// 浏览器动态内容用(注入页面 <script>):与 renderIcon 保持同一份标记结构
export const iconJsContent = /* javascript */ `
function uiIcon(name, className = 'ui-icon') {
return '<svg class="' + className + '" aria-hidden="true" focusable="false"><use href="#icon-' + name + '"></use></svg>';
}
// 以图标 + 纯文本渲染状态行,图标与文字由 flex 容器居中,文本用 textContent 写入
function uiSetStatus(el, name, text) {
if (!el) return;
el.innerHTML = '<span class="ui-icon-label">' + uiIcon(name) + '<span class="ui-status-text"></span></span>';
el.querySelector('.ui-status-text').textContent = text;
}
`;
+2 -1
View File
@@ -692,7 +692,7 @@ function updateColorSchemeToggle() {
const btn = document.getElementById('theme-corner-toggle');
if (!btn) return;
const scheme = document.body.dataset.colorScheme || 'light';
btn.textContent = scheme === 'dark' ? '☀' : '🌙';
btn.innerHTML = uiIcon(scheme === 'dark' ? 'sun' : 'moon');
btn.title = scheme === 'dark' ? '切换浅色模式' : '切换暗色模式';
}
@@ -719,6 +719,7 @@ const DANMAKU_DICT = [
'@Celestials316', '@dyphire', '@piaoyizy', '@xiaoQQya', '@liixing',
'@goodcommunication', '@Mr-Quin', '@chason-zhao', '@DemoJameson',
'@rinnein', '@Lampon', '@zcw199604', 'Mashiro', '@wade6716',
'@xlmc', '@mz289', '@sugarbliss',
'请合理使用', '公益服务请适当调高缓存避免滥用',
'有弹幕才有氛围~', '弹幕陪你看', 'LogVar可能会倒闭但绝对不会变质',
];
+35 -33
View File
@@ -3,14 +3,34 @@ export const previewJsContent = /* javascript */ `
const previewCategoryOrder = ['api', 'source', 'match', 'danmu', 'cache', 'system'];
const previewCategoryMeta = {
api: { label: '🔗 API 配置', description: '访问凭证与请求控制' },
source: { label: '📜 源配置', description: '弹幕源、VOD 服务与平台凭证' },
match: { label: '🔍 匹配配置', description: '标题处理、匹配策略与 AI 服务' },
danmu: { label: '🔣 弹幕配置', description: '过滤、转换、输出与时间调整' },
cache: { label: '💾 缓存配置', description: '缓存时效、容量与 Redis 服务' },
system: { label: '⚙️ 系统配置', description: '界面、网络、部署与安全设置' }
api: { icon: 'link', label: 'API 配置', description: '访问凭证与请求控制' },
source: { icon: 'layers', label: '源配置', description: '弹幕源、VOD 服务与平台凭证' },
match: { icon: 'search', label: '匹配配置', description: '标题处理、匹配策略与 AI 服务' },
danmu: { icon: 'comment', label: '弹幕配置', description: '过滤、转换、输出与时间调整' },
cache: { icon: 'database', label: '缓存配置', description: '缓存时效、容量与 Redis 服务' },
system: { icon: 'settings', label: '系统配置', description: '界面、网络、部署与安全设置' }
};
// 分类按钮(图标 + 名称 + 计数):配置预览与环境变量配置两个导航共用
function renderCategoryNavButton(category, count, isActive, onClickExpr) {
return \`
<button
type="button"
class="preview-category-btn\${isActive ? ' active' : ''}"
onclick="\${onClickExpr}"
aria-pressed="\${isActive}"
>
<span class="ui-icon-label">\${uiIcon(previewCategoryMeta[category].icon)}\${previewCategoryMeta[category].label}</span>
<span class="preview-category-count">\${count}</span>
</button>
\`;
}
// 分类分组标题(图标 + 名称):搜索结果分组标题共用
function renderCategoryHeading(category) {
return \`<h3 class="ui-icon-label">\${uiIcon(previewCategoryMeta[category].icon)} \${previewCategoryMeta[category].label}</h3>\`;
}
const previewGroupDefinitions = {
api: [
{ name: '访问认证', keys: ['TOKEN', 'ADMIN_TOKEN', 'FAVORITE_REQUIRE_ADMIN'] },
@@ -123,32 +143,14 @@ function renderPreviewNavigation() {
aria-pressed="\${inOverview}"
\${inOverview ? '' : 'title="返回总览"'}
>
<span>🗂\uFE0E 总览</span>
<span class="ui-icon-label">\${uiIcon('layout-grid')}总览</span>
<span class="preview-category-count">\${totalCount}</span>
</button>
\`;
const categories = [
...previewCategoryOrder.map(category => ({
key: category,
label: previewCategoryMeta[category].label,
count: (previewState.categorizedVars[category] || []).length
}))
];
navigation.innerHTML = overviewBtn + categories.map(category => {
const isActive = !previewState.query && previewState.activeCategory === category.key;
return \`
<button
type="button"
class="preview-category-btn\${isActive ? ' active' : ''}"
onclick="selectPreviewCategory('\${category.key}')"
aria-pressed="\${isActive}"
>
<span>\${category.label}</span>
<span class="preview-category-count">\${category.count}</span>
</button>
\`;
navigation.innerHTML = overviewBtn + previewCategoryOrder.map(category => {
const isActive = !previewState.query && previewState.activeCategory === category;
return renderCategoryNavButton(category, (previewState.categorizedVars[category] || []).length, isActive, "selectPreviewCategory('" + category + "')");
}).join('');
}
@@ -182,11 +184,11 @@ function renderPreviewOverview() {
const count = (previewState.categorizedVars[category] || []).length;
return \`
<button type="button" class="preview-summary" onclick="selectPreviewCategory('\${category}')">
<span class="preview-summary-title">\${meta.label}</span>
<span class="preview-summary-title ui-icon-label">\${uiIcon(meta.icon)}\${meta.label}</span>
<span class="preview-summary-description">\${meta.description}</span>
<span class="preview-summary-side">
<span class="preview-summary-count">\${count}</span>
<span class="preview-summary-arrow" aria-hidden="true">&rsaquo;</span>
<span class="preview-summary-arrow" aria-hidden="true">\${uiIcon('chevron-right')}</span>
</span>
</button>
\`;
@@ -248,7 +250,7 @@ function renderPreviewSearchResults(query) {
html += \`
<section class="preview-group preview-search-group">
<div class="preview-group-heading">
<h3>\${previewCategoryMeta[category].label}</h3>
\${renderCategoryHeading(category)}
<span>\${matches.length} 项</span>
</div>
<div class="preview-list">
@@ -277,7 +279,7 @@ function renderPreviewItem(item, category, index) {
<code class="preview-value\${isLong ? ' is-collapsed' : ''}" id="\${valueId}">\${escapeHtml(value)}</code>
<div class="preview-value-actions">
\${isLong ? \`<button type="button" class="preview-action-btn preview-expand-btn" onclick="togglePreviewValue(this)" aria-controls="\${valueId}" aria-expanded="false">展开</button>\` : ''}
<button type="button" class="preview-action-btn preview-copy-btn" onclick="copyPreviewValue('\${category}', \${index}, this)" title="复制配置值" aria-label="复制 \${escapeHtml(item.key)} 的值"><span aria-hidden="true">⧉</span></button>
<button type="button" class="preview-action-btn preview-copy-btn" onclick="copyPreviewValue('\${category}', \${index}, this)" title="复制配置值" aria-label="复制 \${escapeHtml(item.key)} 的值">\${uiIcon('copy')}</button>
</div>
</div>
</div>
@@ -381,7 +383,7 @@ async function copyPreviewValue(category, index, button) {
}
const originalContent = button.innerHTML;
button.textContent = '✓';
button.innerHTML = uiIcon('check');
button.classList.add('is-copied');
button.setAttribute('title', '已复制');
setTimeout(() => {
+51 -55
View File
@@ -765,8 +765,8 @@ function renderValueInput(item) {
<label>值 (\${min}-\${max})</label>
<div class="number-picker">
<div class="number-controls">
<button type="button" class="number-btn" onclick="adjustNumber(1)">▲</button>
<button type="button" class="number-btn" onclick="adjustNumber(-1)">▼</button>
<button type="button" class="number-btn" onclick="adjustNumber(1)">\${uiIcon('chevron-up')}</button>
<button type="button" class="number-btn" onclick="adjustNumber(-1)">\${uiIcon('chevron-down')}</button>
</div>
<div class="number-display" id="num-value">\${currentValue}</div>
</div>
@@ -841,7 +841,7 @@ function renderValueInput(item) {
\${shouldShowMergeMode ? \`
<div class="merge-mode-controls">
<div class="merge-mode-btn" id="merge-mode-toggle" onclick="toggleMergeMode()">
<span class="icon">🔗️</span> 开启合并模式
\${uiIcon('link')} 开启合并模式
</div>
<div class="form-help" style="margin: 0; margin-left: 10px;">
开启后点击下方选项将添加到暂存区,组合后点击 √ 确认
@@ -849,7 +849,7 @@ function renderValueInput(item) {
</div>
<div class="staging-area" id="staging-area">
<button type="button" class="confirm-merge-btn" onclick="confirmMergeGroup()" title="确认添加该组">✓</button>
<button type="button" class="confirm-merge-btn" onclick="confirmMergeGroup()" title="确认添加该组">\${uiIcon('check')}</button>
</div>
\` : ''}
@@ -869,7 +869,7 @@ function renderValueInput(item) {
\${currentKey === 'MERGE_SOURCE_PAIRS' ? \`
<div style="margin-top: 15px; margin-bottom: 8px;">
<button type="button" class="btn btn-primary btn-sm" onclick="fetchAndShowRecentData()">
📊 查看最近数据
\${uiIcon('bar-chart')} 查看最近数据
</button>
</div>
<div id="recent-data-panel" class="recent-data-panel">
@@ -914,7 +914,15 @@ function renderValueInput(item) {
<button type="button" class="btn btn-danger map-remove-btn" onclick="removeMapItem(this)">删除</button>
</div>
</div>
<button type="button" class="btn btn-primary" onclick="addMapItem()">添加映射项</button>
<div style="display: flex; align-items: center; justify-content: space-between;">
<button type="button" class="btn btn-primary" onclick="addMapItem()">添加映射项</button>
<button type="button" class="btn btn-primary btn-sm" onclick="fetchAndShowRecentData()">
\${uiIcon('bar-chart')} 查看最近数据
</button>
</div>
<div id="recent-data-panel" class="recent-data-panel">
<div id="recent-data-list"></div>
</div>
\`;
bindMapInputSync();
@@ -977,7 +985,7 @@ function renderValueInput(item) {
添加规则
</button>
<button type="button" class="btn btn-primary btn-sm" onclick="fetchAndShowRecentData()">
📊 查看最近数据
\${uiIcon('bar-chart')} 查看最近数据
</button>
</div>
<div id="recent-data-panel" class="recent-data-panel">
@@ -1037,12 +1045,12 @@ function renderValueInput(item) {
<div class="form-help">支持 OpenAI 兼容的 API,需配合 AI_BASE_URL 和 AI_MODEL 配置使用</div>
<div class="ai-apikey-status" id="ai-apikey-status">
<span class="ai-status-icon">🔍</span>
<span class="ai-status-icon">\${uiIcon('search')}</span>
<span class="ai-status-text">点击下方按钮测试连通性</span>
</div>
<div class="ai-apikey-actions" style="margin-bottom: 15px;">
<button type="button" class="btn btn-primary btn-sm" id="ai-verify-btn" onclick="verifyAiConnection()">
🧪 测试连通性
\${uiIcon('flask')} 测试连通性
</button>
</div>
</div>
@@ -1053,13 +1061,13 @@ function renderValueInput(item) {
container.innerHTML = \`
<div class="bili-cookie-editor">
<div class="bili-cookie-status" id="bili-cookie-status">
<span class="bili-status-icon">🔍</span>
<span class="bili-status-icon">\${uiIcon('search')}</span>
<span class="bili-status-text">检测中...</span>
</div>
<div class="bili-cookie-actions">
<button type="button" class="btn btn-primary btn-sm" onclick="startBilibiliQRLogin()">
📱 扫码登录
\${uiIcon('qr-code')} 扫码登录
</button>
</div>
@@ -1095,7 +1103,7 @@ function renderValueInput(item) {
添加规则
</button>
<button type="button" class="btn btn-primary btn-sm" onclick="fetchAndShowRecentData()">
📊 查看最近数据
\${uiIcon('bar-chart')} 查看最近数据
</button>
</div>
<div id="recent-data-panel" class="recent-data-panel">
@@ -1747,12 +1755,12 @@ function toggleMergeMode() {
if (isMergeMode) {
btn.classList.add('active');
btn.innerHTML = '<span class="icon">⛓‍💥</span> 合并模式已开启,点击关闭';
btn.innerHTML = uiIcon('unlink') + ' 合并模式已开启,点击关闭';
stagingArea.classList.add('active');
renderStagingArea();
} else {
btn.classList.remove('active');
btn.innerHTML = '<span class="icon">🔗️</span> 点击开启合并模式';
btn.innerHTML = uiIcon('link') + ' 点击开启合并模式';
stagingArea.classList.remove('active');
stagingTags = [];
}
@@ -2380,18 +2388,7 @@ function renderEnvNavigation() {
navigation.innerHTML = previewCategoryOrder.map(category => {
const isActive = !envSearchQuery && currentCategory === category;
const count = (envVariables[category] || []).length;
return \`
<button
type="button"
class="preview-category-btn\${isActive ? ' active' : ''}"
onclick="switchCategory('\${category}')"
aria-pressed="\${isActive}"
>
<span>\${previewCategoryMeta[category].label}</span>
<span class="preview-category-count">\${count}</span>
</button>
\`;
return renderCategoryNavButton(category, (envVariables[category] || []).length, isActive, "switchCategory('" + category + "')");
}).join('');
}
@@ -2486,7 +2483,7 @@ function renderEnvList() {
html += \`
<section class="preview-group env-search-group">
<div class="preview-group-heading">
<h3>\${previewCategoryMeta[category].label}</h3>
\${renderCategoryHeading(category)}
<span>\${regularMatches.length} 项</span>
</div>
<div>
@@ -2782,7 +2779,7 @@ async function startBilibiliQRLogin() {
<div class="modal" id="bili-qr-modal">
<div class="modal-content" style="max-width: 400px;">
<div class="modal-header">
<h3>📱 扫码登录 Bilibili</h3>
<h3 class="ui-icon-label">\${uiIcon('qr-code')} 扫码登录 Bilibili</h3>
<button class="close-btn" onclick="closeBiliQRModal()">×</button>
</div>
<div class="modal-body" style="text-align: center;">
@@ -2835,7 +2832,7 @@ async function startBilibiliQRLogin() {
}
} catch (error) {
qrLoading.style.display = 'none';
qrStatus.textContent = '❌ ' + error.message;
uiSetStatus(qrStatus, 'x-circle', error.message);
}
}
@@ -2859,17 +2856,17 @@ function startBiliQRCheck() {
switch (code) {
case 86101:
qrStatus.textContent = '⏳ 等待扫码...';
uiSetStatus(qrStatus, 'clock', '等待扫码...');
break;
case 86090:
qrStatus.textContent = '📱 已扫码,请确认';
uiSetStatus(qrStatus, 'qr-code', '已扫码,请确认');
break;
case 86038:
qrStatus.textContent = '❌ 二维码已过期';
uiSetStatus(qrStatus, 'x-circle', '二维码已过期');
clearInterval(biliQRCheckInterval);
break;
case 0:
qrStatus.textContent = '✅ 登录成功!';
uiSetStatus(qrStatus, 'check-circle', '登录成功!');
clearInterval(biliQRCheckInterval);
if (result.data.cookie) {
@@ -2924,11 +2921,11 @@ async function autoCheckBilibiliCookieStatus() {
// 如果输入框为空,提示未配置
if (!cookie) {
statusEl.innerHTML = '<span class="bili-status-icon">⚠️</span><span class="bili-status-text">未配置</span>';
statusEl.innerHTML = \`<span class="bili-status-icon">\${uiIcon('alert-triangle')}</span><span class="bili-status-text">未配置</span>\`;
return;
}
statusEl.innerHTML = '<span class="bili-status-icon">🔍</span><span class="bili-status-text">检测中...</span>';
statusEl.innerHTML = \`<span class="bili-status-icon">\${uiIcon('search')}</span><span class="bili-status-text">检测中...</span>\`;
// 脱敏后的 *...* 无法直接校验,后端会自动改为校验“已保存”的 Cookie
const isMasked = /^[*]+$/.test(cookie);
@@ -2957,20 +2954,20 @@ async function autoCheckBilibiliCookieStatus() {
// 用户手动输入/扫码填入的 Cookie → 提示保存
if (!isMasked) {
statusEl.innerHTML = \`<span class="bili-status-icon">✅</span><span class="bili-status-text">\${uname}\${leftText} · 请点击保存按钮(Vercel等平台需重新部署后生效)</span>\`;
statusEl.innerHTML = \`<span class="bili-status-icon">\${uiIcon('check-circle')}</span><span class="bili-status-text">\${uname}\${leftText} · 请点击保存按钮(Vercel等平台需重新部署后生效)</span>\`;
} else {
// 脱敏显示时只展示当前已保存 Cookie 的状态
statusEl.innerHTML = \`<span class="bili-status-icon">✅</span><span class="bili-status-text">\${uname}\${leftText}</span>\`;
statusEl.innerHTML = \`<span class="bili-status-icon">\${uiIcon('check-circle')}</span><span class="bili-status-text">\${uname}\${leftText}</span>\`;
}
} else {
const err = result.data.error || 'Cookie无效或已失效';
statusEl.innerHTML = \`<span class="bili-status-icon">❌</span><span class="bili-status-text">\${err},请重新扫码登录并保存</span>\`;
statusEl.innerHTML = \`<span class="bili-status-icon">\${uiIcon('x-circle')}</span><span class="bili-status-text">\${err},请重新扫码登录并保存</span>\`;
}
} else {
statusEl.innerHTML = '<span class="bili-status-icon">⚠️</span><span class="bili-status-text">检测失败</span>';
statusEl.innerHTML = \`<span class="bili-status-icon">\${uiIcon('alert-triangle')}</span><span class="bili-status-text">检测失败</span>\`;
}
} catch (error) {
statusEl.innerHTML = '<span class="bili-status-icon">⚠️</span><span class="bili-status-text">检测失败</span>';
statusEl.innerHTML = \`<span class="bili-status-icon">\${uiIcon('alert-triangle')}</span><span class="bili-status-text">检测失败</span>\`;
}
}
// 显示 Bilibili Cookie 保存提示
@@ -2979,7 +2976,7 @@ function showBilibiliCookieSaveHint(text) {
if (!statusEl) return;
const msg = text || '请点击保存按钮,Vercel等平台需重新部署后生效';
statusEl.innerHTML = \`<span class="bili-status-icon">💾</span><span class="bili-status-text">\${msg}</span>\`;
statusEl.innerHTML = \`<span class="bili-status-icon">\${uiIcon('save')}</span><span class="bili-status-text">\${msg}</span>\`;
}
/* ========================================
@@ -2996,7 +2993,7 @@ async function verifyAiConnection() {
// 如果输入框为空,提示未配置
if (!apiKey) {
statusEl.innerHTML = '<span class="ai-status-icon">⚠️</span><span class="ai-status-text">请先输入 API Key</span>';
statusEl.innerHTML = \`<span class="ai-status-icon">\${uiIcon('alert-triangle')}</span><span class="ai-status-text">请先输入 API Key</span>\`;
return;
}
@@ -3005,7 +3002,7 @@ async function verifyAiConnection() {
btn.innerHTML = '<span class="loading-spinner-small"></span>';
btn.disabled = true;
statusEl.innerHTML = '<span class="ai-status-icon">🔍</span><span class="ai-status-text">正在测试连通性...</span>';
statusEl.innerHTML = \`<span class="ai-status-icon">\${uiIcon('search')}</span><span class="ai-status-text">正在测试连通性...</span>\`;
// 检查是否为脱敏后的 *...*
const isMasked = /^[*]+$/.test(apiKey);
@@ -3020,14 +3017,14 @@ async function verifyAiConnection() {
const result = await response.json();
if (result.ok) {
statusEl.innerHTML = '<span class="ai-status-icon">✅</span><span class="ai-status-text">' + (result.message || 'AI 服务连通性测试成功') + '</span>';
statusEl.innerHTML = \`<span class="ai-status-icon">\${uiIcon('check-circle')}</span><span class="ai-status-text">\${result.message || 'AI 服务连通性测试成功'}</span>\`;
statusEl.style.color = 'var(--success-color, #28a745)';
} else {
statusEl.innerHTML = '<span class="ai-status-icon">❌</span><span class="ai-status-text">' + (result.message || '连通性测试失败') + '</span>';
statusEl.innerHTML = \`<span class="ai-status-icon">\${uiIcon('x-circle')}</span><span class="ai-status-text">\${result.message || '连通性测试失败'}</span>\`;
statusEl.style.color = 'var(--danger-color, #dc3545)';
}
} catch (error) {
statusEl.innerHTML = '<span class="ai-status-icon">⚠️</span><span class="ai-status-text">测试请求失败: ' + error.message + '</span>';
statusEl.innerHTML = \`<span class="ai-status-icon">\${uiIcon('alert-triangle')}</span><span class="ai-status-text">测试请求失败: \${error.message}</span>\`;
statusEl.style.color = 'var(--warning-color, #ffc107)';
} finally {
// 恢复按钮状态
@@ -3049,14 +3046,13 @@ function toggleCardSection(btnEl, targetSelector, openText, closeText) {
if (!container) return;
const isHidden = window.getComputedStyle(container).display === 'none';
btnEl.querySelector('.cache-badge-label').textContent = isHidden ? closeText : openText;
if (isHidden) {
container.style.display = 'flex';
btnEl.innerHTML = closeText;
btnEl.classList.add('active');
} else {
container.style.display = 'none';
btnEl.innerHTML = openText;
btnEl.classList.remove('active');
}
}
@@ -3072,11 +3068,11 @@ function toggleMapping(btnEl) {
const isHidden = window.getComputedStyle(container).display === 'none';
if (isHidden) {
container.style.display = 'flex';
btnEl.innerHTML = '📊 收起映射详情';
btnEl.innerHTML = '<span class="ui-icon-label">' + uiIcon('list') + ' 收起映射详情</span>';
btnEl.classList.add('active');
} else {
container.style.display = 'none';
btnEl.innerHTML = '📊 展开映射详情';
btnEl.innerHTML = '<span class="ui-icon-label">' + uiIcon('list') + ' 展开映射详情</span>';
btnEl.classList.remove('active');
}
}
@@ -3213,9 +3209,9 @@ function renderAnimeCachePanel(data, listContainer) {
let rowHtml = '';
if (hasMainMatch && hasChildMatch) {
rowHtml = \`<div class="mapping-row"><span class="mapping-status success">✓ 匹配</span> <span class="mapping-text">\${mainSide} ↔ \${childSide}</span></div>\`;
rowHtml = \`<div class="mapping-row"><span class="mapping-status success ui-icon-label">\${uiIcon('check')} 匹配</span> <span class="mapping-text">\${mainSide} ↔ \${childSide}</span></div>\`;
} else {
rowHtml = \`<div class="mapping-row"><span class="mapping-status warning">✗ 落单</span> <span class="mapping-text">\${mainSide} ↔ \${childSide}</span></div>\`;
rowHtml = \`<div class="mapping-row"><span class="mapping-status warning ui-icon-label">\${uiIcon('x')} 落单</span> <span class="mapping-text">\${mainSide} ↔ \${childSide}</span></div>\`;
}
parsedRows.push({
@@ -3240,7 +3236,7 @@ function renderAnimeCachePanel(data, listContainer) {
if (mappingRowsHtml) {
mappingHtml = \`
<div class="child-mapping-toggle" onclick="toggleMapping(this)">📊 展开映射详情</div>
<div class="child-mapping-toggle" onclick="toggleMapping(this)"><span class="ui-icon-label">\${uiIcon('list')} 展开映射详情</span></div>
<div class="child-mapping-container">
\${mappingRowsHtml}
</div>
@@ -3302,10 +3298,10 @@ function renderAnimeCachePanel(data, listContainer) {
if (childrenCount > 0 || episodesCount > 0) {
let badges = '';
if (episodesCount > 0) {
badges += \`<div class="cache-badge badge-episodes" onclick="toggleCardSection(this, '.episodes-list-container', '📺 \${episodesCount} 个剧集', '📺 收起剧集')">📺 \${episodesCount} 个剧集</div>\`;
badges += \`<div class="cache-badge badge-episodes" onclick="toggleCardSection(this, '.episodes-list-container', '\${episodesCount} 个剧集', '收起剧集')">\${uiIcon('film')}<span class="cache-badge-label">\${episodesCount} 个剧集</span></div>\`;
}
if (childrenCount > 0) {
badges += \`<div class="cache-badge badge-sources" onclick="toggleCardSection(this, '.merged-children-container', '🔗 \${childrenCount} 个被合并源', '🔗 收起被合并源')">🔗 \${childrenCount} 个被合并源</div>\`;
badges += \`<div class="cache-badge badge-sources" onclick="toggleCardSection(this, '.merged-children-container', '\${childrenCount} 个被合并源', '收起被合并源')">\${uiIcon('link')}<span class="cache-badge-label">\${childrenCount} 个被合并源</span></div>\`;
}
footerHtml = \`<div class="anime-cache-footer">\${badges}</div>\`;
}
+15 -12
View File
@@ -4,6 +4,7 @@ import { componentsCssContent } from "./css/components.css.js";
import { formsCssContent } from "./css/forms.css.js";
import { responsiveCssContent } from "./css/responsive.css.js";
import { themesCssContent } from "./css/themes.css.js";
import { iconJsContent, iconsSpriteContent, renderIcon } from "./js/icons.js";
import { mainJsContent } from "./js/main.js";
import { previewJsContent } from "./js/preview.js";
import { logviewJsContent } from "./js/logview.js";
@@ -39,6 +40,7 @@ export const HTML_TEMPLATE = /* html */ `
</head>
<body data-theme="globals.uiTheme">
${iconsSpriteContent}
<script>
try {
var storedTheme = localStorage.getItem('logvar_ui_theme');
@@ -54,7 +56,7 @@ export const HTML_TEMPLATE = /* html */ `
</script>
<div class="container">
<div class="corner-fold"></div>
<button class="theme-corner-toggle" id="theme-corner-toggle" onclick="toggleColorScheme()" title="切换明暗模式" aria-label="切换明暗模式">🌙</button>
<button class="theme-corner-toggle" id="theme-corner-toggle" onclick="toggleColorScheme()" title="切换明暗模式" aria-label="切换明暗模式">${renderIcon('moon')}</button>
<!-- 进度条 -->
<div class="progress-container" id="progress-container">
<div class="progress-bar" id="progress-bar"></div>
@@ -67,12 +69,12 @@ export const HTML_TEMPLATE = /* html */ `
<h1>LogVar弹幕API</h1>
</div>
<div class="version-info">
<span class="version-badge">当前版本: <span id="current-version">v${globals.version}</span></span>
<span class="version-badge">${renderIcon('tag')} 当前版本: <span id="current-version">v${globals.version}</span></span>
<a class="update-badge" id="update-badge" href="https://t.me/s/logvar_danmu_channel" target="_blank" rel="noopener" title="查看更新通知">
🎉 最新版本: <span id="latest-version">加载中...</span>
${renderIcon('sparkles')} 最新版本: <span id="latest-version">加载中...</span>
</a>
<span class="api-endpoint-badge" onclick="copyApiEndpoint()" title="点击复制API端点" style="cursor: pointer;">
API端点: <span id="api-endpoint" style="color: #4CAF50; font-weight: bold;">加载中...</span>
${renderIcon('link')} API端点: <span id="api-endpoint" style="color: #4CAF50; font-weight: bold;">加载中...</span>
</span>
</div>
</div>
@@ -93,7 +95,7 @@ export const HTML_TEMPLATE = /* html */ `
<h2>配置预览</h2>
<div id="proxy-config-container" class="error-config-banner" style="display: none;">
<h3 class="error-config-title">⚠️ 获取配置失败</h3>
<h3 class="error-config-title ui-icon-label">${renderIcon('alert-triangle')} 获取配置失败</h3>
<p class="error-config-text">
检测到无法获取配置。如果您使用了复杂的反向代理:例如将 <code>http://{ip}:9321/</code> 代理到了 <code>http://{ip}:9321/danmu_api/</code>,请在此处手动输入完整的反代后链接(不包含TOKEN和ADMIN_TOKEN的)
</p>
@@ -138,8 +140,8 @@ export const HTML_TEMPLATE = /* html */ `
<h2>日志查看</h2>
<div class="log-controls">
<div>
<button class="btn btn-primary" onclick="refreshLogs()">🔄 刷新日志</button>
<button class="btn btn-danger" onclick="clearLogs()">🗑️ 清空日志</button>
<button class="btn btn-primary" onclick="refreshLogs()">${renderIcon('refresh-cw')} 刷新日志</button>
<button class="btn btn-danger" onclick="clearLogs()">${renderIcon('trash-2')} 清空日志</button>
</div>
<span style="color: #666;">实时日志监控</span>
</div>
@@ -291,7 +293,7 @@ export const HTML_TEMPLATE = /* html */ `
<h2>请求记录</h2>
<div class="log-controls">
<div>
<button class="btn btn-primary" id="refresh-request-records">🔄 刷新记录</button>
<button class="btn btn-primary" id="refresh-request-records">${renderIcon('refresh-cw')} 刷新记录</button>
<span id="total-requests-today" style="color: #ff5722; margin-left: 15px; vertical-align: middle; font-size: 1.2em; font-weight: bold;"></span>
</div>
<span style="color: #666;">云服务部署需要配置redis</span>
@@ -343,17 +345,17 @@ export const HTML_TEMPLATE = /* html */ `
</div>
<div class="env-toolbar-actions" style="display: flex; gap: 10px; flex-wrap: wrap;">
<button class="btn btn-primary config-transfer-btn" onclick="exportSystemConfig()" title="下载当前环境变量配置文件">
<span class="config-transfer-icon" aria-hidden="true">📤</span> 导出配置
${renderIcon('upload')} 导出配置
</button>
<button class="btn btn-primary config-transfer-btn" onclick="triggerConfigImport()" title="上传 JSON 文件并导入环境变量配置">
<span class="config-transfer-icon" aria-hidden="true">📥</span> 导入配置
${renderIcon('download')} 导入配置
</button>
<input type="file" id="config-import-file" accept=".json,application/json" style="display: none;" onchange="importSystemConfigFile(this.files[0])">
<button class="btn btn-danger" onclick="showClearCacheModal()" title="清理系统缓存">
🗑️ 清理缓存
${renderIcon('trash-2')} 清理缓存
</button>
<button class="btn btn-success" onclick="showDeploySystemModal()" title="重新部署系统">
🚀 重新部署
${renderIcon('cloud-up')} 重新部署
</button>
</div>
@@ -547,6 +549,7 @@ export const HTML_TEMPLATE = /* html */ `
</nav>
<script>
${iconJsContent}
${mainJsContent}
${previewJsContent}
${logviewJsContent}