|
@@ -1,5 +1,6 @@
|
|
|
import {Events, WML, Window} from "@wailsio/runtime"; // 导入 Wails 运行时的事件系统、WML 工具与窗口控制 API
|
|
import {Events, WML, Window} from "@wailsio/runtime"; // 导入 Wails 运行时的事件系统、WML 工具与窗口控制 API
|
|
|
import {SettingsService, AppSettings} from "../bindings/TengTuToolbox/index.js"; // 设置服务与配置模型
|
|
import {SettingsService, AppSettings} from "../bindings/TengTuToolbox/index.js"; // 设置服务与配置模型
|
|
|
|
|
+import {LocalSendService} from "../bindings/TengTuToolbox/index.js"; // LocalSend 服务(发现设备/接收文件)
|
|
|
import {getCalendarInfo} from "./lunar.js"; // 农历与节日计算
|
|
import {getCalendarInfo} from "./lunar.js"; // 农历与节日计算
|
|
|
|
|
|
|
|
WML.Enable(); // 为带 data-wml-openURL 属性的链接绑定打开行为(动态内容渲染后需调 WML.Reload())
|
|
WML.Enable(); // 为带 data-wml-openURL 属性的链接绑定打开行为(动态内容渲染后需调 WML.Reload())
|
|
@@ -50,6 +51,7 @@ const FALLBACK_SETTINGS = {
|
|
|
newsCount: 30, // 新闻页显示条数
|
|
newsCount: 30, // 新闻页显示条数
|
|
|
githubRefreshMinutes: 120, // Github 自动刷新间隔(分钟)
|
|
githubRefreshMinutes: 120, // Github 自动刷新间隔(分钟)
|
|
|
fontDelta: 0, // 全局字号偏移(px),+-3,默认 0(基准字号已含 +2)
|
|
fontDelta: 0, // 全局字号偏移(px),+-3,默认 0(基准字号已含 +2)
|
|
|
|
|
+ toolOrder: ['websvc', 'translate', 'qrcode', 'ntfy', 'chat', 'poem', 'screenshot', 'shutdown', 'md5', 'sha256', 'led', 'reminder', 'localsend'], // 工具页卡片默认顺序
|
|
|
stockTicker: {enabled: false, width: 1400, fontSize: 14, color: '#ff4d4d', speed: 80, transparent: true, x: -1, y: -1},
|
|
stockTicker: {enabled: false, width: 1400, fontSize: 14, color: '#ff4d4d', speed: 80, transparent: true, x: -1, y: -1},
|
|
|
newsTicker: {enabled: false, width: 1400, fontSize: 14, color: '#3ddc84', speed: 60, transparent: true, x: -1, y: -1},
|
|
newsTicker: {enabled: false, width: 1400, fontSize: 14, color: '#3ddc84', speed: 60, transparent: true, x: -1, y: -1},
|
|
|
poem: {enabled: false, color: '#c98a3a', fontSize: 16, tts: false, speed: 18},
|
|
poem: {enabled: false, color: '#c98a3a', fontSize: 16, tts: false, speed: 18},
|
|
@@ -338,30 +340,146 @@ function applyTabOrder(order) {
|
|
|
if (settingList) {
|
|
if (settingList) {
|
|
|
const grouped = { system: [] }; // system 组 + 各 Tab 组
|
|
const grouped = { system: [] }; // system 组 + 各 Tab 组
|
|
|
settingList.querySelectorAll('.setting-row').forEach(row => {
|
|
settingList.querySelectorAll('.setting-row').forEach(row => {
|
|
|
- const g = row.dataset.tabGroup || 'system';
|
|
|
|
|
|
|
+ // 兼容旧标记:LocalSend 曾用单数「tool」,统一归入「tools」组
|
|
|
|
|
+ let g = row.dataset.tabGroup || 'system';
|
|
|
|
|
+ if (g === 'tool') g = 'tools';
|
|
|
if (!grouped[g]) grouped[g] = [];
|
|
if (!grouped[g]) grouped[g] = [];
|
|
|
grouped[g].push(row);
|
|
grouped[g].push(row);
|
|
|
});
|
|
});
|
|
|
- // 分组容器:系统在前,其后按 Tab 顺序;每组一个带边框的卡片,明显区分各页设置
|
|
|
|
|
|
|
+ // 分组容器:系统在前,其后按 Tab 顺序;未对应 Tab 的组(如 poem)追加到末尾
|
|
|
const groupOrder = ['system', ...ordered];
|
|
const groupOrder = ['system', ...ordered];
|
|
|
|
|
+ // 补全未出现在 Tab 顺序中的组(例如 poem),保持首次出现顺序
|
|
|
|
|
+ const named = new Map([['system', '系统'], ['poem', '古诗'], ['mouse', '鼠标提醒']]);
|
|
|
|
|
+ Object.keys(grouped).forEach(g => {
|
|
|
|
|
+ if (!groupOrder.includes(g)) groupOrder.push(g);
|
|
|
|
|
+ });
|
|
|
settingList.querySelectorAll('.setting-group').forEach(el => el.remove());
|
|
settingList.querySelectorAll('.setting-group').forEach(el => el.remove());
|
|
|
const frag = document.createDocumentFragment();
|
|
const frag = document.createDocumentFragment();
|
|
|
groupOrder.forEach(g => {
|
|
groupOrder.forEach(g => {
|
|
|
const gRows = grouped[g];
|
|
const gRows = grouped[g];
|
|
|
if (!gRows || !gRows.length) return; // 该组无设置项则不显示
|
|
if (!gRows || !gRows.length) return; // 该组无设置项则不显示
|
|
|
|
|
+ // 设置分组:默认收缩,点击标题展开/收起,减少滚动,专注当前设置
|
|
|
const group = document.createElement('section');
|
|
const group = document.createElement('section');
|
|
|
- group.className = 'setting-group';
|
|
|
|
|
- const title = document.createElement('div');
|
|
|
|
|
|
|
+ group.className = 'setting-group is-collapsed';
|
|
|
|
|
+ const title = document.createElement('button');
|
|
|
|
|
+ title.type = 'button';
|
|
|
title.className = 'setting-group-title';
|
|
title.className = 'setting-group-title';
|
|
|
- title.textContent = g === 'system' ? '系统' : getTabName(g); // 内置或自定义 Tab 名
|
|
|
|
|
|
|
+ title.setAttribute('aria-expanded', 'false');
|
|
|
|
|
+ // 箭头 + 组名 + 设置项数量
|
|
|
|
|
+ const arrow = document.createElement('span');
|
|
|
|
|
+ arrow.className = 'setting-group-arrow';
|
|
|
|
|
+ arrow.textContent = '▸';
|
|
|
|
|
+ const label = document.createElement('span');
|
|
|
|
|
+ label.className = 'setting-group-label';
|
|
|
|
|
+ label.textContent = named.get(g) || getTabName(g); // 内置/自定义 Tab 名,无 Tab 的组用映射名
|
|
|
|
|
+ const count = document.createElement('span');
|
|
|
|
|
+ count.className = 'setting-group-count';
|
|
|
|
|
+ count.textContent = `${gRows.length} 项`;
|
|
|
|
|
+ title.appendChild(arrow);
|
|
|
|
|
+ title.appendChild(label);
|
|
|
|
|
+ title.appendChild(count);
|
|
|
|
|
+ // 点击标题切换展开/收起
|
|
|
|
|
+ title.addEventListener('click', () => {
|
|
|
|
|
+ const collapsed = group.classList.toggle('is-collapsed');
|
|
|
|
|
+ title.setAttribute('aria-expanded', String(!collapsed));
|
|
|
|
|
+ });
|
|
|
group.appendChild(title);
|
|
group.appendChild(title);
|
|
|
- gRows.forEach(row => group.appendChild(row));
|
|
|
|
|
|
|
+ // 把设置项包进一个容器,便于统一隐藏/显示
|
|
|
|
|
+ const rowsWrap = document.createElement('div');
|
|
|
|
|
+ rowsWrap.className = 'setting-group-rows';
|
|
|
|
|
+ gRows.forEach(row => rowsWrap.appendChild(row));
|
|
|
|
|
+ group.appendChild(rowsWrap);
|
|
|
frag.appendChild(group);
|
|
frag.appendChild(group);
|
|
|
});
|
|
});
|
|
|
settingList.appendChild(frag);
|
|
settingList.appendChild(frag);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// 工具页卡片排序:按设置中的 toolOrder 重排 DOM 顺序,并刷新上移/下移按钮禁用状态。
|
|
|
|
|
+// 未在 toolOrder 中的工具(如新增工具)追加到末尾,保证全部工具都能显示。
|
|
|
|
|
+const DEFAULT_TOOL_ORDER = [
|
|
|
|
|
+ 'websvc', 'translate', 'qrcode', 'ntfy', 'chat', 'poem',
|
|
|
|
|
+ 'screenshot', 'shutdown', 'md5', 'sha256', 'led', 'reminder', 'localsend',
|
|
|
|
|
+];
|
|
|
|
|
+
|
|
|
|
|
+function applyToolOrder(order) {
|
|
|
|
|
+ const panel = document.getElementById('tab-tools');
|
|
|
|
|
+ if (!panel) return;
|
|
|
|
|
+ const cards = Array.from(panel.querySelectorAll('.tool-card'));
|
|
|
|
|
+ if (!cards.length) return;
|
|
|
|
|
+ // 合法 id:DOM 中实际存在的 data-tool
|
|
|
|
|
+ const valid = new Set(cards.map(c => c.dataset.tool));
|
|
|
|
|
+ const ordered = [...new Set((order || []).filter(id => valid.has(id)))];
|
|
|
|
|
+ cards.forEach(c => { if (!ordered.includes(c.dataset.tool)) ordered.push(c.dataset.tool); });
|
|
|
|
|
+ // 重排:按 ordered 依次 appendChild(追加即移到末尾,实现重排)
|
|
|
|
|
+ const cardMap = {};
|
|
|
|
|
+ cards.forEach(c => { cardMap[c.dataset.tool] = c; });
|
|
|
|
|
+ ordered.forEach(id => { if (cardMap[id]) panel.appendChild(cardMap[id]); });
|
|
|
|
|
+ refreshToolOrderBtns();
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 刷新每个工具卡片上移/下移按钮的可用状态(首项禁用上移,末项禁用下移)
|
|
|
|
|
+function refreshToolOrderBtns() {
|
|
|
|
|
+ const panel = document.getElementById('tab-tools');
|
|
|
|
|
+ if (!panel) return;
|
|
|
|
|
+ const cards = Array.from(panel.querySelectorAll('.tool-card'));
|
|
|
|
|
+ cards.forEach((card, i) => {
|
|
|
|
|
+ const up = card.querySelector('.tool-up');
|
|
|
|
|
+ const down = card.querySelector('.tool-down');
|
|
|
|
|
+ if (up) up.disabled = i === 0;
|
|
|
|
|
+ if (down) down.disabled = i === cards.length - 1;
|
|
|
|
|
+ });
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 读取当前工具卡片顺序(DOM 顺序),返回工具 id 数组
|
|
|
|
|
+function collectToolOrder() {
|
|
|
|
|
+ const panel = document.getElementById('tab-tools');
|
|
|
|
|
+ if (!panel) return [];
|
|
|
|
|
+ return Array.from(panel.querySelectorAll('.tool-card')).map(c => c.dataset.tool);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 上移/下移单个工具卡片,并立即保存顺序到设置
|
|
|
|
|
+function moveToolCard(card, dir) {
|
|
|
|
|
+ const panel = document.getElementById('tab-tools');
|
|
|
|
|
+ if (!panel || !card) return;
|
|
|
|
|
+ const cards = Array.from(panel.querySelectorAll('.tool-card'));
|
|
|
|
|
+ const i = cards.indexOf(card);
|
|
|
|
|
+ const j = i + dir;
|
|
|
|
|
+ if (i < 0 || j < 0 || j >= cards.length) return;
|
|
|
|
|
+ if (dir < 0) {
|
|
|
|
|
+ panel.insertBefore(card, cards[j]); // 上移:插到前一项之前
|
|
|
|
|
+ } else {
|
|
|
|
|
+ panel.insertBefore(cards[j], card); // 下移:把后一项插到当前项之前(等效当前项后移)
|
|
|
|
|
+ }
|
|
|
|
|
+ refreshToolOrderBtns();
|
|
|
|
|
+ // 持久化顺序(不阻塞 UI),更新内存配置供保存设置时提交
|
|
|
|
|
+ const order = collectToolOrder();
|
|
|
|
|
+ currentSettings.toolOrder = order;
|
|
|
|
|
+ // 直接调用后端保存 toolOrder 字段(仅该字段),避免全量表单校验副作用
|
|
|
|
|
+ if (typeof SettingsService?.SaveSettings === 'function') {
|
|
|
|
|
+ SettingsService.SaveSettings(currentSettings).then(() => {
|
|
|
|
|
+ // 保存成功,无需额外处理(currentSettings 已含最新 toolOrder)
|
|
|
|
|
+ }).catch(err => console.warn('保存工具顺序失败:', err));
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 绑定工具排序按钮点击事件(事件委托,防止点击冒泡触发折叠切换)
|
|
|
|
|
+function initToolOrder() {
|
|
|
|
|
+ // 排序按钮直接绑定在卡片标题上(捕获阶段先于折叠监听),
|
|
|
|
|
+ // 点击排序按钮既不折叠,也不冒泡触发标题折叠切换
|
|
|
|
|
+ document.querySelectorAll('.tool-card-head').forEach(head => {
|
|
|
|
|
+ head.addEventListener('click', (e) => {
|
|
|
|
|
+ const btn = e.target.closest('.tool-order-btn');
|
|
|
|
|
+ if (!btn) return;
|
|
|
|
|
+ e.preventDefault();
|
|
|
|
|
+ e.stopPropagation(); // 阻止冒泡到 .tool-card-head 的折叠切换
|
|
|
|
|
+ const card = btn.closest('.tool-card');
|
|
|
|
|
+ if (!card) return;
|
|
|
|
|
+ moveToolCard(card, btn.classList.contains('tool-up') ? -1 : 1);
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
// ------------------------------------------------------------------------
|
|
// ------------------------------------------------------------------------
|
|
|
// Tab 切换:点击按钮,仅显示对应的内容面板
|
|
// Tab 切换:点击按钮,仅显示对应的内容面板
|
|
|
// ------------------------------------------------------------------------
|
|
// ------------------------------------------------------------------------
|
|
@@ -599,9 +717,11 @@ async function syncForm(settings) {
|
|
|
syncTickerForm('news', settings.newsTicker);
|
|
syncTickerForm('news', settings.newsTicker);
|
|
|
syncPoemForm(settings.poem);
|
|
syncPoemForm(settings.poem);
|
|
|
syncMouseAlertForm(settings.mouseAlert);
|
|
syncMouseAlertForm(settings.mouseAlert);
|
|
|
|
|
+ syncLocalSendForm(settings.localSend);
|
|
|
applyTheme(settings.theme, false);
|
|
applyTheme(settings.theme, false);
|
|
|
applyFontDelta(settings.fontDelta ?? FALLBACK_SETTINGS.fontDelta); // 应用全局字号偏移(默认 +2)
|
|
applyFontDelta(settings.fontDelta ?? FALLBACK_SETTINGS.fontDelta); // 应用全局字号偏移(默认 +2)
|
|
|
applyTabOrder(settings.tabOrder); // 按设置重排 Tab 顺序
|
|
applyTabOrder(settings.tabOrder); // 按设置重排 Tab 顺序
|
|
|
|
|
+ applyToolOrder(settings.toolOrder); // 按设置重排工具卡片顺序
|
|
|
// 启动/加载设置后,默认激活 Tab 顺序的第 1 位(若当前无激活 Tab 或激活的是旧的 links)
|
|
// 启动/加载设置后,默认激活 Tab 顺序的第 1 位(若当前无激活 Tab 或激活的是旧的 links)
|
|
|
const firstId = (settings.tabOrder || []).find(id => id !== 'settings');
|
|
const firstId = (settings.tabOrder || []).find(id => id !== 'settings');
|
|
|
const activeId = document.querySelector('.tab-btn.active')?.dataset.tab;
|
|
const activeId = document.querySelector('.tab-btn.active')?.dataset.tab;
|
|
@@ -817,9 +937,41 @@ function collectForm() {
|
|
|
newsTicker: collectTicker('news'),
|
|
newsTicker: collectTicker('news'),
|
|
|
poem: collectPoem(),
|
|
poem: collectPoem(),
|
|
|
mouseAlert: collectMouseAlert(),
|
|
mouseAlert: collectMouseAlert(),
|
|
|
|
|
+ localSend: collectLocalSend(),
|
|
|
|
|
+ toolOrder: collectToolOrder(), // 工具页卡片顺序(含用户刚调整过的)
|
|
|
|
|
+ };
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 从设置页收集 LocalSend 配置(本机名/接收位置/接收模式)
|
|
|
|
|
+function collectLocalSend() {
|
|
|
|
|
+ const prev = currentSettings?.localSend || {};
|
|
|
|
|
+ const alias = (document.getElementById('localsend-alias')?.value || '').trim();
|
|
|
|
|
+ const recvDir = (document.getElementById('localsend-recvdir')?.value || '').trim();
|
|
|
|
|
+ const auto = document.getElementById('localsend-auto')?.checked ?? !!prev.autoAccept;
|
|
|
|
|
+ return {
|
|
|
|
|
+ alias,
|
|
|
|
|
+ receiveDir: recvDir,
|
|
|
|
|
+ autoAccept: auto,
|
|
|
|
|
+ enabled: !!prev.enabled,
|
|
|
|
|
+ port: prev.port || 53317,
|
|
|
|
|
+ fingerprint: prev.fingerprint || '',
|
|
|
};
|
|
};
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// 回填 LocalSend 配置到设置页表单
|
|
|
|
|
+function syncLocalSendForm(localSend) {
|
|
|
|
|
+ const ls = localSend || {};
|
|
|
|
|
+ const aliasInput = document.getElementById('localsend-alias');
|
|
|
|
|
+ const recvDirInput = document.getElementById('localsend-recvdir');
|
|
|
|
|
+ const autoRadio = document.getElementById('localsend-auto');
|
|
|
|
|
+ const manualRadio = document.getElementById('localsend-manual');
|
|
|
|
|
+ if (aliasInput) aliasInput.value = ls.alias || '';
|
|
|
|
|
+ if (recvDirInput) recvDirInput.value = ls.receiveDir || '';
|
|
|
|
|
+ if (autoRadio && manualRadio) {
|
|
|
|
|
+ (ls.autoAccept ? autoRadio : manualRadio).checked = true;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
// 从控件收集滚动条配置;位置(x/y)由拖动自动保存,表单不修改
|
|
// 从控件收集滚动条配置;位置(x/y)由拖动自动保存,表单不修改
|
|
|
function collectTicker(kind) {
|
|
function collectTicker(kind) {
|
|
|
const inputs = tickerInputs[kind];
|
|
const inputs = tickerInputs[kind];
|
|
@@ -1079,6 +1231,13 @@ async function saveSettings() {
|
|
|
loadStocks(); // 代码变化后立即刷新行情
|
|
loadStocks(); // 代码变化后立即刷新行情
|
|
|
restartAutoTimers(); // 按新设置的间隔重建各面板定时器
|
|
restartAutoTimers(); // 按新设置的间隔重建各面板定时器
|
|
|
|
|
|
|
|
|
|
+ // 把 LocalSend 配置同步到后端服务(运行中则即时生效,未运行则下次启动生效)
|
|
|
|
|
+ try {
|
|
|
|
|
+ const ls = saved.localSend || {};
|
|
|
|
|
+ await LocalSendService.SetConfig(ls.alias || '', ls.receiveDir || '', !!ls.autoAccept);
|
|
|
|
|
+ await lsRefreshDevices();
|
|
|
|
|
+ } catch (e) { console.warn('同步 LocalSend 配置失败:', e); }
|
|
|
|
|
+
|
|
|
isDirty = false; // 保存成功,清除未保存标记
|
|
isDirty = false; // 保存成功,清除未保存标记
|
|
|
saveStatus.textContent = '已保存并生效';
|
|
saveStatus.textContent = '已保存并生效';
|
|
|
saveStatus.classList.add('is-ok');
|
|
saveStatus.classList.add('is-ok');
|
|
@@ -1105,6 +1264,11 @@ document.querySelectorAll('#tab-settings .input, #tab-settings input[type="check
|
|
|
input.addEventListener('change', markDirty); // 开关切换
|
|
input.addEventListener('change', markDirty); // 开关切换
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
|
|
+// 设置页 radio(如 LocalSend 接收模式)也标记未保存
|
|
|
|
|
+document.querySelectorAll('#tab-settings input[type="radio"]').forEach(input => {
|
|
|
|
|
+ input.addEventListener('change', markDirty);
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
// 焦点移出设置面板(点击面板外区域 / 切换 tab)时自动保存
|
|
// 焦点移出设置面板(点击面板外区域 / 切换 tab)时自动保存
|
|
|
const settingsPanel = document.getElementById('tab-settings');
|
|
const settingsPanel = document.getElementById('tab-settings');
|
|
|
settingsPanel.addEventListener('focusout', (event) => {
|
|
settingsPanel.addEventListener('focusout', (event) => {
|
|
@@ -1375,22 +1539,27 @@ async function loadGithub() {
|
|
|
const releasesURL = feed.repo
|
|
const releasesURL = feed.repo
|
|
|
? 'https://github.com/' + feed.repo + '/releases'
|
|
? 'https://github.com/' + feed.repo + '/releases'
|
|
|
: (feed.url || '#');
|
|
: (feed.url || '#');
|
|
|
|
|
+ // 本地版本发布时间(鼠标提示):按本地版本号在 release 列表中匹配(作用于所有含本地版本的徽标)
|
|
|
|
|
+ const versionTip = feed.versionTime ? `(发布时间 ${escapeHtml(feed.versionTime)})` : '';
|
|
|
|
|
+ const versionTipFull = versionTip ? `,本地版本发布时间 ${escapeHtml(feed.versionTime)}` : '';
|
|
|
if (feed.latest) {
|
|
if (feed.latest) {
|
|
|
|
|
+ // 最新正式版发布时间(鼠标提示)
|
|
|
|
|
+ const latestTip = feed.latestTime ? `(发布时间 ${escapeHtml(feed.latestTime)})` : '';
|
|
|
if (feed.hasUpdate) {
|
|
if (feed.hasUpdate) {
|
|
|
// 发现新版本:高亮可点击下载 + 检视按钮
|
|
// 发现新版本:高亮可点击下载 + 检视按钮
|
|
|
const badgeText = `发现新版本 ${escapeHtml(feed.latest)}`;
|
|
const badgeText = `发现新版本 ${escapeHtml(feed.latest)}`;
|
|
|
- versionBadge = `<a class="ver-badge ver-update" href="${escapeHtml(feed.releaseURL || feed.url)}" target="_blank" data-wml-openURL="${escapeHtml(feed.releaseURL || feed.url)}" title="点击前往下载">● ${badgeText}</a>`;
|
|
|
|
|
- if (feed.version) versionBadge += `<a class="ver-local" href="${escapeHtml(releasesURL)}" target="_blank" data-wml-openURL="${escapeHtml(releasesURL)}" title="查看版本列表">本地 ${escapeHtml(feed.version)}</a>`;
|
|
|
|
|
|
|
+ versionBadge = `<a class="ver-badge ver-update" href="${escapeHtml(feed.releaseURL || feed.url)}" target="_blank" data-wml-openURL="${escapeHtml(feed.releaseURL || feed.url)}" title="点击前往下载${latestTip}">● ${badgeText}</a>`;
|
|
|
|
|
+ if (feed.version) versionBadge += `<a class="ver-local" href="${escapeHtml(releasesURL)}" target="_blank" data-wml-openURL="${escapeHtml(releasesURL)}" title="查看版本列表${versionTipFull}">本地 ${escapeHtml(feed.version)}</a>`;
|
|
|
versionBadge += `<button class="btn-ghost repo-check-btn" data-url="${escapeHtml(feed.url)}" data-latest="${escapeHtml(feed.latest)}">检视</button>`;
|
|
versionBadge += `<button class="btn-ghost repo-check-btn" data-url="${escapeHtml(feed.url)}" data-latest="${escapeHtml(feed.latest)}">检视</button>`;
|
|
|
} else if (feed.version) {
|
|
} else if (feed.version) {
|
|
|
// 本地已是最新版本:勾选本地版本号,不再显示「最新 xxx」标签
|
|
// 本地已是最新版本:勾选本地版本号,不再显示「最新 xxx」标签
|
|
|
- versionBadge = `<a class="ver-badge ver-ok" href="${escapeHtml(releasesURL)}" target="_blank" data-wml-openURL="${escapeHtml(releasesURL)}" title="查看版本列表">✓ 本地 ${escapeHtml(feed.version)}</a>`;
|
|
|
|
|
|
|
+ versionBadge = `<a class="ver-badge ver-ok" href="${escapeHtml(releasesURL)}" target="_blank" data-wml-openURL="${escapeHtml(releasesURL)}" title="查看版本列表${latestTip}${versionTipFull}">✓ 本地 ${escapeHtml(feed.version)}</a>`;
|
|
|
} else {
|
|
} else {
|
|
|
// 无本地版本可勾选时,仍显示最新版本徽标
|
|
// 无本地版本可勾选时,仍显示最新版本徽标
|
|
|
- versionBadge = `<span class="ver-badge ver-ok">✓ 最新 ${escapeHtml(feed.latest)}</span>`;
|
|
|
|
|
|
|
+ versionBadge = `<span class="ver-badge ver-ok" title="${latestTip}">✓ 最新 ${escapeHtml(feed.latest)}</span>`;
|
|
|
}
|
|
}
|
|
|
} else if (feed.version) {
|
|
} else if (feed.version) {
|
|
|
- versionBadge = `<a class="ver-badge ver-muted" href="${escapeHtml(releasesURL)}" target="_blank" data-wml-openURL="${escapeHtml(releasesURL)}" title="查看版本列表">本地 ${escapeHtml(feed.version)}</a>`;
|
|
|
|
|
|
|
+ versionBadge = `<a class="ver-badge ver-muted" href="${escapeHtml(releasesURL)}" target="_blank" data-wml-openURL="${escapeHtml(releasesURL)}" title="查看版本列表${versionTipFull}">本地 ${escapeHtml(feed.version)}</a>`;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// Latest 之后的版本(Pre-release 等):仅列出展示,不参与更新比较
|
|
// Latest 之后的版本(Pre-release 等):仅列出展示,不参与更新比较
|
|
@@ -2566,6 +2735,250 @@ Events.On('periodicReminder', (event) => {
|
|
|
showTodoReminder(event.data);
|
|
showTodoReminder(event.data);
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
|
|
+// ------------------------------------------------------------------------
|
|
|
|
|
+// 工具页:LocalSend 文件互传——组播发现设备,HTTPS 接收文件
|
|
|
|
|
+// ------------------------------------------------------------------------
|
|
|
|
|
+const lsToggleBtn = document.getElementById('localsend-toggle'); // 启动/停止乒乓按钮
|
|
|
|
|
+let lsRunning = false; // 当前 LocalSend 服务是否运行(乒乓按钮状态)
|
|
|
|
|
+const lsHintEl = document.getElementById('localsend-hint'); // 操作提示
|
|
|
|
|
+const lsStatusEl = document.getElementById('localsend-status'); // 状态徽标
|
|
|
|
|
+const lsAliasInput = document.getElementById('localsend-alias'); // 本机名称(设置页)
|
|
|
|
|
+const lsAutoRadio = document.getElementById('localsend-auto'); // 自动接收(设置页)
|
|
|
|
|
+const lsManualRadio = document.getElementById('localsend-manual'); // 手动确认(设置页)
|
|
|
|
|
+const lsRecvDirInput = document.getElementById('localsend-recvdir');// 接收位置(设置页)
|
|
|
|
|
+const lsPickDirBtn = document.getElementById('localsend-pickdir'); // 选择目录(设置页)
|
|
|
|
|
+const lsSendHintEl = document.getElementById('localsend-send-hint');// 发送状态提示
|
|
|
|
|
+const lsDevicesBox = document.getElementById('localsend-devices'); // 设备列表容器
|
|
|
|
|
+const lsDeviceList = document.getElementById('localsend-device-list'); // 设备列表 ul
|
|
|
|
|
+
|
|
|
|
|
+// 确认弹窗元素
|
|
|
|
|
+const lsOfferMask = document.getElementById('ls-offer-mask');
|
|
|
|
|
+const lsOfferSender = document.getElementById('ls-offer-sender');
|
|
|
|
|
+const lsOfferFiles = document.getElementById('ls-offer-files');
|
|
|
|
|
+const lsOfferAccept = document.getElementById('ls-offer-accept');
|
|
|
|
|
+const lsOfferReject = document.getElementById('ls-offer-reject');
|
|
|
|
|
+
|
|
|
|
|
+let lsCurrentOffer = null; // 当前待确认的 fileOffer(手动模式)
|
|
|
|
|
+
|
|
|
|
|
+// 读取当前 LocalSend 配置(浏览器预览时回退默认值)
|
|
|
|
|
+async function lsLoadConfig() {
|
|
|
|
|
+ if (!LocalSendService?.GetConfig) return null;
|
|
|
|
|
+ try {
|
|
|
|
|
+ return await LocalSendService.GetConfig();
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ console.warn('获取 LocalSend 配置失败:', err);
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 同步运行状态到 UI(配置字段已移入设置页,此处只更新状态徽标与按钮)
|
|
|
|
|
+function lsSyncStatus(cfg) {
|
|
|
|
|
+ if (!cfg) return;
|
|
|
|
|
+ lsRunning = !!cfg.enabled;
|
|
|
|
|
+ lsStatusEl.textContent = lsRunning ? '运行中' : '';
|
|
|
|
|
+ lsStatusEl.className = 'tool-status' + (lsRunning ? ' is-on' : '');
|
|
|
|
|
+ // 乒乓按钮:运行中显示「停止服务」(红),未运行显示「启动服务」(主色)
|
|
|
|
|
+ lsToggleBtn.textContent = lsRunning ? '停止服务' : '启动服务';
|
|
|
|
|
+ lsToggleBtn.classList.toggle('btn-danger', lsRunning);
|
|
|
|
|
+ lsToggleBtn.classList.toggle('btn-primary', !lsRunning);
|
|
|
|
|
+ lsToggleBtn.disabled = false;
|
|
|
|
|
+ lsHintEl.textContent = lsRunning ? '服务已启动,等待其他设备发送文件…' : '';
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 渲染设备列表
|
|
|
|
|
+function lsRenderDevices(devices) {
|
|
|
|
|
+ if (!lsDevicesBox || !lsDeviceList) return;
|
|
|
|
|
+ const list = Array.isArray(devices) ? devices.filter(Boolean) : [];
|
|
|
|
|
+ lsDevicesBox.hidden = false;
|
|
|
|
|
+ if (!list.length) {
|
|
|
|
|
+ lsDeviceList.innerHTML = '<li class="localsend-empty">未发现其他设备(确保对方已启动 LocalSend)</li>';
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ lsDeviceList.innerHTML = list.map(d => {
|
|
|
|
|
+ const online = d.available ? '在线' : '离线';
|
|
|
|
|
+ const type = d.deviceType || 'desktop';
|
|
|
|
|
+ const proto = d.protocol || 'https';
|
|
|
|
|
+ const ip = escapeHtml(d.ip || '');
|
|
|
|
|
+ return `<li data-ls-ip="${ip}">
|
|
|
|
|
+ <span class="ls-dev-name">${escapeHtml(d.alias || d.ip)}</span>
|
|
|
|
|
+ <span class="ls-dev-meta">${ip} · ${type} · ${proto} · ${escapeHtml(d.version || '')}</span>
|
|
|
|
|
+ <span class="ls-dev-online">● ${online}</span>
|
|
|
|
|
+ <button class="btn-ghost ls-dev-send" data-ls-ip="${ip}" type="button" title="发送文件到该设备">发送</button>
|
|
|
|
|
+ </li>`;
|
|
|
|
|
+ }).join('');
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 格式化文件大小
|
|
|
|
|
+function lsFormatSize(bytes) {
|
|
|
|
|
+ bytes = Number(bytes) || 0;
|
|
|
|
|
+ if (bytes >= 1024 * 1024 * 1024) return (bytes / 1024 / 1024 / 1024).toFixed(1) + ' GB';
|
|
|
|
|
+ if (bytes >= 1024 * 1024) return (bytes / 1024 / 1024).toFixed(1) + ' MB';
|
|
|
|
|
+ if (bytes >= 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
|
|
|
|
+ return bytes + ' B';
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 显示文件接收确认弹窗(手动模式)
|
|
|
|
|
+function lsShowOffer(offer) {
|
|
|
|
|
+ if (!offer) return;
|
|
|
|
|
+ lsCurrentOffer = offer;
|
|
|
|
|
+ lsOfferSender.textContent = `来自:${offer.alias || '未知设备'}`;
|
|
|
|
|
+ lsOfferFiles.innerHTML = (offer.files || []).map(f =>
|
|
|
|
|
+ `<li><span class="ls-file-name" title="${escapeHtml(f.fileName || '')}">${escapeHtml(f.fileName || '')}</span>` +
|
|
|
|
|
+ `<span class="ls-file-size">${lsFormatSize(f.size)}</span></li>`
|
|
|
|
|
+ ).join('') || '<li class="localsend-empty">(无文件)</li>';
|
|
|
|
|
+ lsOfferMask.hidden = false;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function lsHideOffer() {
|
|
|
|
|
+ lsOfferMask.hidden = true;
|
|
|
|
|
+ lsCurrentOffer = null;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 乒乓按钮:点击切换启动/停止(配置来自设置页,已随保存写入 settings.json)
|
|
|
|
|
+if (lsToggleBtn) lsToggleBtn.addEventListener('click', async () => {
|
|
|
|
|
+ if (lsRunning) {
|
|
|
|
|
+ // 当前运行中 → 停止
|
|
|
|
|
+ lsHintEl.textContent = '停止中…';
|
|
|
|
|
+ lsToggleBtn.disabled = true;
|
|
|
|
|
+ try {
|
|
|
|
|
+ await LocalSendService.Stop();
|
|
|
|
|
+ const cfg = await lsLoadConfig();
|
|
|
|
|
+ lsSyncStatus(cfg);
|
|
|
|
|
+ lsHintEl.textContent = '服务已停止';
|
|
|
|
|
+ lsDevicesBox.hidden = true;
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ lsHintEl.textContent = '停止失败:' + (err?.message || err);
|
|
|
|
|
+ lsToggleBtn.disabled = false;
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 当前未运行 → 启动
|
|
|
|
|
+ lsHintEl.textContent = '启动中…';
|
|
|
|
|
+ lsToggleBtn.disabled = true;
|
|
|
|
|
+ try {
|
|
|
|
|
+ await LocalSendService.Start();
|
|
|
|
|
+ const cfg = await lsLoadConfig();
|
|
|
|
|
+ lsSyncStatus(cfg);
|
|
|
|
|
+ lsHintEl.textContent = '服务已启动,等待其他设备发送文件…';
|
|
|
|
|
+ lsDevicesBox.hidden = false;
|
|
|
|
|
+ await lsRefreshDevices(); // 立即拉取一次设备
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ lsHintEl.textContent = '启动失败:' + (err?.message || err);
|
|
|
|
|
+ lsToggleBtn.disabled = false;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+// 选择接收目录(设置页按钮,选后标记未保存并提示)
|
|
|
|
|
+if (lsPickDirBtn) lsPickDirBtn.addEventListener('click', async () => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const dir = await SettingsService.PickWebDir(); // 复用目录选择对话框
|
|
|
|
|
+ if (!dir) return;
|
|
|
|
|
+ lsRecvDirInput.value = dir;
|
|
|
|
|
+ markDirty();
|
|
|
|
|
+ lsRecvDirInput.dispatchEvent(new Event('input'));
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ console.warn('选择 LocalSend 接收目录失败:', err);
|
|
|
|
|
+ }
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+// 点击设备项「发送」按钮(事件委托):先弹文件选择框,选中后直接发送到该设备
|
|
|
|
|
+if (lsDeviceList) lsDeviceList.addEventListener('click', async (e) => {
|
|
|
|
|
+ const btn = e.target.closest('.ls-dev-send');
|
|
|
|
|
+ if (!btn) return;
|
|
|
|
|
+ const ip = btn.getAttribute('data-ls-ip');
|
|
|
|
|
+ // 每次点击「发送」都重新选择文件(不预选,避免误发)
|
|
|
|
|
+ let files;
|
|
|
|
|
+ try {
|
|
|
|
|
+ files = await SettingsService.PickSendFiles();
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ lsSendHintEl.textContent = '选择文件失败:' + (err?.message || err);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!files || !files.length) {
|
|
|
|
|
+ lsSendHintEl.textContent = '未选择文件';
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ btn.disabled = true;
|
|
|
|
|
+ lsSendHintEl.textContent = `正在发送 ${files.length} 个文件到 ${ip}…`;
|
|
|
|
|
+ try {
|
|
|
|
|
+ await LocalSendService.SendFiles(files, ip);
|
|
|
|
|
+ lsSendHintEl.textContent = `已发送 ${files.length} 个文件到 ${ip} ✓`;
|
|
|
|
|
+ setTimeout(() => { lsSendHintEl.textContent = ''; }, 4000);
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ lsSendHintEl.textContent = '发送失败:' + (err?.message || err);
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ btn.disabled = false;
|
|
|
|
|
+ }
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+// 拉取并渲染设备列表
|
|
|
|
|
+async function lsRefreshDevices() {
|
|
|
|
|
+ if (!LocalSendService?.GetDevices) return;
|
|
|
|
|
+ try {
|
|
|
|
|
+ const devices = await LocalSendService.GetDevices();
|
|
|
|
|
+ lsRenderDevices(devices);
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ console.warn('获取 LocalSend 设备失败:', err);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 订阅设备发现事件
|
|
|
|
|
+Events.On('localsend:devices', (event) => {
|
|
|
|
|
+ lsRenderDevices(event.data);
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+// 订阅文件接收请求事件(手动模式弹出确认)
|
|
|
|
|
+Events.On('localsend:fileOffer', (event) => {
|
|
|
|
|
+ lsShowOffer(event.data);
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+// 订阅错误事件
|
|
|
|
|
+Events.On('localsend:error', (event) => {
|
|
|
|
|
+ const msg = event.data?.message || String(event.data || '未知错误');
|
|
|
|
|
+ lsHintEl.textContent = 'LocalSend 错误:' + msg;
|
|
|
|
|
+ lsStatusEl.textContent = '错误';
|
|
|
|
|
+ lsStatusEl.className = 'tool-status';
|
|
|
|
|
+ lsRunning = false;
|
|
|
|
|
+ lsToggleBtn.textContent = '启动服务';
|
|
|
|
|
+ lsToggleBtn.classList.remove('btn-danger');
|
|
|
|
|
+ lsToggleBtn.classList.add('btn-primary');
|
|
|
|
|
+ lsToggleBtn.disabled = false;
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+// 接受文件
|
|
|
|
|
+lsOfferAccept?.addEventListener('click', async () => {
|
|
|
|
|
+ if (!lsCurrentOffer) return;
|
|
|
|
|
+ try {
|
|
|
|
|
+ await LocalSendService.ConfirmTransfer(lsCurrentOffer.sessionId, true);
|
|
|
|
|
+ lsHintEl.textContent = '已接受,文件接收中…';
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ lsHintEl.textContent = '接受失败:' + (err?.message || err);
|
|
|
|
|
+ }
|
|
|
|
|
+ lsHideOffer();
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+// 拒绝文件
|
|
|
|
|
+lsOfferReject?.addEventListener('click', async () => {
|
|
|
|
|
+ if (!lsCurrentOffer) return;
|
|
|
|
|
+ try {
|
|
|
|
|
+ await LocalSendService.ConfirmTransfer(lsCurrentOffer.sessionId, false);
|
|
|
|
|
+ lsHintEl.textContent = '已拒绝该文件';
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ lsHintEl.textContent = '拒绝失败:' + (err?.message || err);
|
|
|
|
|
+ }
|
|
|
|
|
+ lsHideOffer();
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+// 页面加载时初始化 LocalSend 状态(配置表单由 loadSettings→syncForm 回填)
|
|
|
|
|
+(async function initLocalSend() {
|
|
|
|
|
+ const cfg = await lsLoadConfig();
|
|
|
|
|
+ lsSyncStatus(cfg);
|
|
|
|
|
+ if (cfg?.enabled) {
|
|
|
|
|
+ lsDevicesBox.hidden = false;
|
|
|
|
|
+ await lsRefreshDevices();
|
|
|
|
|
+ }
|
|
|
|
|
+})();
|
|
|
|
|
+
|
|
|
|
|
|
|
|
async function loadHostPanel() {
|
|
async function loadHostPanel() {
|
|
|
try {
|
|
try {
|
|
@@ -2740,7 +3153,9 @@ function initToolCollapse() {
|
|
|
document.querySelectorAll('.tool-card').forEach(card => {
|
|
document.querySelectorAll('.tool-card').forEach(card => {
|
|
|
const head = card.querySelector('.tool-card-head');
|
|
const head = card.querySelector('.tool-card-head');
|
|
|
if (!head) return;
|
|
if (!head) return;
|
|
|
- const toggle = () => {
|
|
|
|
|
|
|
+ const toggle = (e) => {
|
|
|
|
|
+ // 点击排序按钮(上移/下移)不触发折叠,交给排序逻辑处理
|
|
|
|
|
+ if (e?.target?.closest?.('.tool-order-btns')) return;
|
|
|
const collapsed = card.classList.toggle('is-collapsed');
|
|
const collapsed = card.classList.toggle('is-collapsed');
|
|
|
head.setAttribute('aria-expanded', String(!collapsed));
|
|
head.setAttribute('aria-expanded', String(!collapsed));
|
|
|
};
|
|
};
|
|
@@ -2748,12 +3163,16 @@ function initToolCollapse() {
|
|
|
head.addEventListener('keydown', e => {
|
|
head.addEventListener('keydown', e => {
|
|
|
if (e.key === 'Enter' || e.key === ' ') {
|
|
if (e.key === 'Enter' || e.key === ' ') {
|
|
|
e.preventDefault();
|
|
e.preventDefault();
|
|
|
- toggle();
|
|
|
|
|
|
|
+ toggle(e);
|
|
|
}
|
|
}
|
|
|
});
|
|
});
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
|
initToolCollapse();
|
|
initToolCollapse();
|
|
|
|
|
+initToolOrder(); // 工具排序按钮:上移/下移并持久化顺序
|
|
|
|
|
+
|
|
|
|
|
+// 初始刷新一次排序按钮禁用状态(首次渲染时 applyToolOrder 尚未执行)
|
|
|
|
|
+refreshToolOrderBtns();
|
|
|
|
|
|
|
|
// ------------------------------------------------------------------------
|
|
// ------------------------------------------------------------------------
|
|
|
// 工具页:Web 服务——选择目录,开启本地浏览服务
|
|
// 工具页:Web 服务——选择目录,开启本地浏览服务
|
|
@@ -2761,8 +3180,8 @@ initToolCollapse();
|
|
|
const websvcStatusEl = document.getElementById('websvc-status'); // 运行状态徽标
|
|
const websvcStatusEl = document.getElementById('websvc-status'); // 运行状态徽标
|
|
|
const websvcDirInput = document.getElementById('websvc-dir'); // 目录输入框
|
|
const websvcDirInput = document.getElementById('websvc-dir'); // 目录输入框
|
|
|
const websvcPickBtn = document.getElementById('websvc-pick'); // 选择目录按钮
|
|
const websvcPickBtn = document.getElementById('websvc-pick'); // 选择目录按钮
|
|
|
-const websvcStartBtn = document.getElementById('websvc-start'); // 开启服务按钮
|
|
|
|
|
-const websvcStopBtn = document.getElementById('websvc-stop'); // 停止服务按钮
|
|
|
|
|
|
|
+const websvcToggleBtn = document.getElementById('websvc-toggle'); // 开启/停止乒乓按钮
|
|
|
|
|
+let websvcRunning = false; // 当前 Web 服务是否运行(乒乓按钮状态)
|
|
|
const websvcHintEl = document.getElementById('websvc-hint'); // 提示文字
|
|
const websvcHintEl = document.getElementById('websvc-hint'); // 提示文字
|
|
|
const websvcPreview = document.getElementById('websvc-preview'); // 预览容器
|
|
const websvcPreview = document.getElementById('websvc-preview'); // 预览容器
|
|
|
const websvcFrame = document.getElementById('websvc-frame'); // 预览 iframe
|
|
const websvcFrame = document.getElementById('websvc-frame'); // 预览 iframe
|
|
@@ -2772,11 +3191,10 @@ let websvcDir = ''; // 当前选中的目录(持久化于 localStorage)
|
|
|
// 渲染 Web 服务状态(运行中 / 未运行)
|
|
// 渲染 Web 服务状态(运行中 / 未运行)
|
|
|
function renderWebSvc(info) {
|
|
function renderWebSvc(info) {
|
|
|
if (info && info.running) {
|
|
if (info && info.running) {
|
|
|
|
|
+ websvcRunning = true;
|
|
|
// URL 做成可点击链接:点击用系统浏览器打开
|
|
// URL 做成可点击链接:点击用系统浏览器打开
|
|
|
websvcStatusEl.innerHTML = `● 运行中 <a class="tool-url" href="${escapeHtml(info.url)}" target="_blank" data-wml-openURL="${escapeHtml(info.url)}" title="点击在浏览器打开">${escapeHtml(info.url)}</a>`;
|
|
websvcStatusEl.innerHTML = `● 运行中 <a class="tool-url" href="${escapeHtml(info.url)}" target="_blank" data-wml-openURL="${escapeHtml(info.url)}" title="点击在浏览器打开">${escapeHtml(info.url)}</a>`;
|
|
|
websvcStatusEl.className = 'tool-status is-on';
|
|
websvcStatusEl.className = 'tool-status is-on';
|
|
|
- websvcStartBtn.disabled = true;
|
|
|
|
|
- websvcStopBtn.disabled = false;
|
|
|
|
|
websvcHintEl.textContent = info.indexName
|
|
websvcHintEl.textContent = info.indexName
|
|
|
? `默认页 ${info.indexName}|${info.connections} 次请求`
|
|
? `默认页 ${info.indexName}|${info.connections} 次请求`
|
|
|
: `未发现 index.html 或 readme.md,显示目录列表|${info.connections} 次请求`;
|
|
: `未发现 index.html 或 readme.md,显示目录列表|${info.connections} 次请求`;
|
|
@@ -2784,14 +3202,18 @@ function renderWebSvc(info) {
|
|
|
websvcPreview.hidden = false;
|
|
websvcPreview.hidden = false;
|
|
|
websvcFrame.src = info.url;
|
|
websvcFrame.src = info.url;
|
|
|
} else {
|
|
} else {
|
|
|
|
|
+ websvcRunning = false;
|
|
|
websvcStatusEl.textContent = '未运行';
|
|
websvcStatusEl.textContent = '未运行';
|
|
|
websvcStatusEl.className = 'tool-status';
|
|
websvcStatusEl.className = 'tool-status';
|
|
|
- websvcStartBtn.disabled = false;
|
|
|
|
|
- websvcStopBtn.disabled = true;
|
|
|
|
|
- websvcHintEl.textContent = websvcDir ? '点击「开启浏览服务」开始' : '请先选择目录';
|
|
|
|
|
|
|
+ websvcHintEl.textContent = websvcDir ? '' : '请先选择目录';
|
|
|
websvcPreview.hidden = true;
|
|
websvcPreview.hidden = true;
|
|
|
websvcFrame.src = 'about:blank';
|
|
websvcFrame.src = 'about:blank';
|
|
|
}
|
|
}
|
|
|
|
|
+ // 乒乓按钮:运行中显示「停止服务」(红),未运行显示「开启浏览服务」(主色)
|
|
|
|
|
+ websvcToggleBtn.textContent = websvcRunning ? '停止服务' : '开启浏览服务';
|
|
|
|
|
+ websvcToggleBtn.classList.toggle('btn-danger', websvcRunning);
|
|
|
|
|
+ websvcToggleBtn.classList.toggle('btn-primary', !websvcRunning);
|
|
|
|
|
+ websvcToggleBtn.disabled = false;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 首次进入工具页:恢复上次目录与运行状态
|
|
// 首次进入工具页:恢复上次目录与运行状态
|
|
@@ -2827,37 +3249,40 @@ websvcPickBtn.addEventListener('click', async () => {
|
|
|
}
|
|
}
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
-// 开启浏览服务
|
|
|
|
|
-websvcStartBtn.addEventListener('click', async () => {
|
|
|
|
|
- if (!websvcDir) {
|
|
|
|
|
- websvcHintEl.textContent = '请先选择目录';
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
- websvcStartBtn.classList.add('loading');
|
|
|
|
|
- websvcStartBtn.disabled = true;
|
|
|
|
|
- try {
|
|
|
|
|
- const info = await SettingsService.StartWebService(websvcDir);
|
|
|
|
|
- renderWebSvc(info);
|
|
|
|
|
- } catch (err) {
|
|
|
|
|
- console.warn('开启服务失败:', err);
|
|
|
|
|
- websvcHintEl.textContent = '开启失败:' + (err?.message || String(err));
|
|
|
|
|
- } finally {
|
|
|
|
|
- websvcStartBtn.classList.remove('loading');
|
|
|
|
|
- }
|
|
|
|
|
-});
|
|
|
|
|
-
|
|
|
|
|
-// 停止浏览服务
|
|
|
|
|
-websvcStopBtn.addEventListener('click', async () => {
|
|
|
|
|
- websvcStopBtn.classList.add('loading');
|
|
|
|
|
- websvcStopBtn.disabled = true;
|
|
|
|
|
- try {
|
|
|
|
|
- await SettingsService.StopWebService();
|
|
|
|
|
- renderWebSvc(null);
|
|
|
|
|
- } catch (err) {
|
|
|
|
|
- console.warn('停止服务失败:', err);
|
|
|
|
|
- websvcHintEl.textContent = '停止失败:' + (err?.message || String(err));
|
|
|
|
|
- } finally {
|
|
|
|
|
- websvcStopBtn.classList.remove('loading');
|
|
|
|
|
|
|
+// 乒乓按钮:点击切换开启/停止浏览服务
|
|
|
|
|
+websvcToggleBtn.addEventListener('click', async () => {
|
|
|
|
|
+ if (websvcRunning) {
|
|
|
|
|
+ // 当前运行中 → 停止
|
|
|
|
|
+ websvcToggleBtn.classList.add('loading');
|
|
|
|
|
+ websvcToggleBtn.disabled = true;
|
|
|
|
|
+ try {
|
|
|
|
|
+ await SettingsService.StopWebService();
|
|
|
|
|
+ renderWebSvc(null);
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ console.warn('停止服务失败:', err);
|
|
|
|
|
+ websvcHintEl.textContent = '停止失败:' + (err?.message || String(err));
|
|
|
|
|
+ websvcToggleBtn.disabled = false;
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ websvcToggleBtn.classList.remove('loading');
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 当前未运行 → 开启
|
|
|
|
|
+ if (!websvcDir) {
|
|
|
|
|
+ websvcHintEl.textContent = '请先选择目录';
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ websvcToggleBtn.classList.add('loading');
|
|
|
|
|
+ websvcToggleBtn.disabled = true;
|
|
|
|
|
+ try {
|
|
|
|
|
+ const info = await SettingsService.StartWebService(websvcDir);
|
|
|
|
|
+ renderWebSvc(info);
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ console.warn('开启服务失败:', err);
|
|
|
|
|
+ websvcHintEl.textContent = '开启失败:' + (err?.message || String(err));
|
|
|
|
|
+ websvcToggleBtn.disabled = false;
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ websvcToggleBtn.classList.remove('loading');
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
});
|
|
});
|
|
|
|
|
|
|
@@ -3443,10 +3868,12 @@ async function loadStocks() {
|
|
|
const rising = q.change >= 0; // 涨跌方向
|
|
const rising = q.change >= 0; // 涨跌方向
|
|
|
const sign = rising ? '+' : ''; // 正数补加号
|
|
const sign = rising ? '+' : ''; // 正数补加号
|
|
|
const color = rising ? upColor() : downColor(); // 用设置中的涨跌颜色
|
|
const color = rising ? upColor() : downColor(); // 用设置中的涨跌颜色
|
|
|
|
|
+ // 股吧地址:东方财富股吧按市场代码(带 sh/sz/bj 前缀)进入,如 sh600519
|
|
|
|
|
+ const gubaURL = 'https://guba.eastmoney.com/list,' + q.code + '.html';
|
|
|
return `
|
|
return `
|
|
|
<div class="stock-row" data-code="${q.code}" title="点击查看分时 / 日K">
|
|
<div class="stock-row" data-code="${q.code}" title="点击查看分时 / 日K">
|
|
|
<div class="stock-main">
|
|
<div class="stock-main">
|
|
|
- <span class="stock-name">${escapeHtml(q.name)}</span>
|
|
|
|
|
|
|
+ <a class="stock-name stock-guba" href="${escapeHtml(gubaURL)}" target="_blank" data-wml-openURL="${escapeHtml(gubaURL)}" title="进入 ${escapeHtml(q.name)} 股吧">${escapeHtml(q.name)}</a>
|
|
|
<span class="stock-code">${q.code.toUpperCase()}</span>
|
|
<span class="stock-code">${q.code.toUpperCase()}</span>
|
|
|
</div>
|
|
</div>
|
|
|
<div class="stock-price" style="color:${color}">${q.price.toFixed(2)}</div>
|
|
<div class="stock-price" style="color:${color}">${q.price.toFixed(2)}</div>
|
|
@@ -3457,6 +3884,7 @@ async function loadStocks() {
|
|
|
<div class="stock-amount">成交 ${formatAmount(q.amount)}</div>
|
|
<div class="stock-amount">成交 ${formatAmount(q.amount)}</div>
|
|
|
</div>`;
|
|
</div>`;
|
|
|
}).join('');
|
|
}).join('');
|
|
|
|
|
+ WML.Reload(); // 重新绑定股票名称股吧链接的打开行为
|
|
|
showStockRefreshTime(Date.now()); // 刷新成功:更新最后刷新时间
|
|
showStockRefreshTime(Date.now()); // 刷新成功:更新最后刷新时间
|
|
|
} catch (err) {
|
|
} catch (err) {
|
|
|
console.warn('获取行情失败:', err);
|
|
console.warn('获取行情失败:', err);
|
|
@@ -3476,6 +3904,8 @@ function formatAmount(wan) {
|
|
|
|
|
|
|
|
// 点击股市行:打开该票的分时 / 日K 弹窗
|
|
// 点击股市行:打开该票的分时 / 日K 弹窗
|
|
|
stockList.addEventListener('click', (event) => {
|
|
stockList.addEventListener('click', (event) => {
|
|
|
|
|
+ // 点击股票名称(股吧链接)时:交给链接打开行为,不触发行点击(避免同时打开图表)
|
|
|
|
|
+ if (event.target.closest('a.stock-guba')) return;
|
|
|
const row = event.target.closest('.stock-row');
|
|
const row = event.target.closest('.stock-row');
|
|
|
if (!row) return;
|
|
if (!row) return;
|
|
|
const quote = lastQuotes.find(q => q.code === row.dataset.code);
|
|
const quote = lastQuotes.find(q => q.code === row.dataset.code);
|