fxdyz
This commit is contained in:
@@ -6,18 +6,20 @@ const state = {
|
||||
kungfu: [],
|
||||
servers: [],
|
||||
events: {},
|
||||
free_event_actions: [],
|
||||
session_control: { mode: "all", entries: [] },
|
||||
legacy_bilei: [],
|
||||
token_stats: null,
|
||||
cache: {
|
||||
defaults: { api: 300, image: 300 },
|
||||
limits: { api_memory_entries: 256, image_max_mb: 512 },
|
||||
limits: { api_memory_max_mb: 16, api_max_entries: 1024, image_max_mb: 512 },
|
||||
api: [],
|
||||
images: [],
|
||||
stats: {},
|
||||
},
|
||||
};
|
||||
const editing = { bindingSession: null, controlSession: null, aliasServer: null, kungfuPzid: null };
|
||||
const editing = { bindingSession: null, controlSession: null, aliasServer: null, kungfuPzid: null, subscriptionSession: null };
|
||||
let subscriptionSaving = false;
|
||||
const restoreConfirmationTimers = new WeakMap();
|
||||
let toastTimer;
|
||||
|
||||
@@ -478,11 +480,63 @@ function renderLegacyBilei() {
|
||||
}));
|
||||
}
|
||||
|
||||
function openSubscriptionEditor(item = null) {
|
||||
if (subscriptionSaving) return;
|
||||
editing.subscriptionSession = item?.session_id ?? null;
|
||||
const form = byId("subscription-form");
|
||||
form.reset();
|
||||
byId("subscription-editor-title").textContent = item ? "编辑推送配置" : "添加推送会话";
|
||||
const sessionInput = byId("subscription-session");
|
||||
sessionInput.value = item?.session_id || "";
|
||||
sessionInput.readOnly = Boolean(item);
|
||||
byId("subscription-enabled").checked = item?.enabled ?? false;
|
||||
|
||||
const selected = new Set(item?.actions || []);
|
||||
const freeActions = new Set(state.free_event_actions);
|
||||
const groups = [
|
||||
{ title: "免费事件", free: true },
|
||||
{ title: "令牌事件", free: false },
|
||||
];
|
||||
byId("subscription-events").replaceChildren(...groups.map((group) => {
|
||||
const fieldset = document.createElement("fieldset");
|
||||
fieldset.className = "subscription-event-group";
|
||||
const legend = document.createElement("legend");
|
||||
legend.textContent = group.title;
|
||||
const grid = document.createElement("div");
|
||||
grid.className = "subscription-event-grid";
|
||||
Object.entries(state.events).forEach(([action, name]) => {
|
||||
if (freeActions.has(Number(action)) !== group.free) return;
|
||||
const label = document.createElement("label");
|
||||
label.className = "subscription-choice";
|
||||
const input = document.createElement("input");
|
||||
input.type = "checkbox";
|
||||
input.name = "subscription_action";
|
||||
input.value = action;
|
||||
input.checked = selected.has(Number(action));
|
||||
const text = document.createElement("span");
|
||||
text.textContent = `${action} ${name}`;
|
||||
label.append(input, text);
|
||||
grid.append(label);
|
||||
});
|
||||
fieldset.append(legend, grid);
|
||||
return fieldset;
|
||||
}));
|
||||
updateSubscriptionSelectionCount();
|
||||
form.hidden = false;
|
||||
form.scrollIntoView({ block: "nearest" });
|
||||
(item ? byId("subscription-enabled") : sessionInput).focus({ preventScroll: true });
|
||||
}
|
||||
|
||||
function updateSubscriptionSelectionCount() {
|
||||
const count = byId("subscription-events").querySelectorAll("input:checked").length;
|
||||
byId("subscription-selection-count").textContent = `已选择 ${count} 项`;
|
||||
}
|
||||
|
||||
function renderSubscriptions() {
|
||||
const body = byId("subscriptions-body");
|
||||
const bindings = bindingMap();
|
||||
if (!state.subscriptions.length) {
|
||||
body.replaceChildren(emptyRow(4, "暂无事件订阅会话"));
|
||||
body.replaceChildren(emptyRow(5, "暂无事件订阅会话,点击“添加推送会话”开始配置"));
|
||||
return;
|
||||
}
|
||||
body.replaceChildren(...state.subscriptions.map((item) => {
|
||||
@@ -491,10 +545,14 @@ function renderSubscriptions() {
|
||||
const server = document.createElement("td");
|
||||
const enabled = document.createElement("td");
|
||||
const actions = document.createElement("td");
|
||||
const controls = document.createElement("td");
|
||||
session.dataset.label = "会话 ID";
|
||||
server.dataset.label = "绑定区服";
|
||||
enabled.dataset.label = "总开关";
|
||||
actions.dataset.label = "已订阅事件";
|
||||
controls.dataset.label = "操作";
|
||||
controls.className = "actions";
|
||||
session.className = "subscription-session-cell";
|
||||
session.textContent = item.session_id;
|
||||
server.textContent = bindings.get(item.session_id) || "未绑定(全部区服)";
|
||||
const stateLabel = document.createElement("span");
|
||||
@@ -514,7 +572,34 @@ function renderSubscriptions() {
|
||||
tags.textContent = "无";
|
||||
}
|
||||
actions.append(tags);
|
||||
row.append(session, server, enabled, actions);
|
||||
controls.append(
|
||||
button("编辑", "", () => openSubscriptionEditor(item)),
|
||||
button("删除", "link-button--danger", async (event) => {
|
||||
if (subscriptionSaving) return;
|
||||
const control = event.currentTarget;
|
||||
control.disabled = true;
|
||||
subscriptionSaving = true;
|
||||
byId("subscription-fields").disabled = true;
|
||||
byId("add-subscription").disabled = true;
|
||||
try {
|
||||
const deleted = await mutate(
|
||||
"subscriptions/delete",
|
||||
{ session_id: item.session_id },
|
||||
"会话推送配置已删除",
|
||||
);
|
||||
if (deleted && editing.subscriptionSession === item.session_id) {
|
||||
byId("subscription-form").hidden = true;
|
||||
editing.subscriptionSession = null;
|
||||
}
|
||||
} finally {
|
||||
subscriptionSaving = false;
|
||||
control.disabled = false;
|
||||
byId("subscription-fields").disabled = false;
|
||||
byId("add-subscription").disabled = false;
|
||||
}
|
||||
}),
|
||||
);
|
||||
row.append(session, server, enabled, actions, controls);
|
||||
return row;
|
||||
}));
|
||||
}
|
||||
@@ -739,17 +824,20 @@ function renderCache() {
|
||||
const stats = cache.stats || {};
|
||||
const apiDefault = cache.defaults?.api ?? 300;
|
||||
const imageDefault = cache.defaults?.image ?? 600;
|
||||
const memoryLimit = cache.limits?.api_memory_entries ?? stats.api_memory_limit ?? 256;
|
||||
const memoryLimitMb = cache.limits?.api_memory_max_mb ?? 16;
|
||||
const apiEntryLimit = cache.limits?.api_max_entries ?? stats.api_entry_limit ?? 256;
|
||||
const imageLimitMb = cache.limits?.image_max_mb ?? 512;
|
||||
byId("api-cache-count").textContent = `${stats.api_count || 0} 条`;
|
||||
byId("api-cache-count").textContent = `${stats.api_count || 0} / ${apiEntryLimit} 条`;
|
||||
byId("api-cache-size").textContent = formatBytes(stats.api_size_bytes);
|
||||
byId("image-cache-count").textContent = `${stats.image_count || 0} 张`;
|
||||
byId("image-cache-size").textContent = `${formatBytes(stats.image_size_bytes)} / ${formatBytes(stats.image_limit_bytes)}`;
|
||||
byId("api-default-ttl").value = String(apiDefault);
|
||||
byId("image-default-ttl").value = String(imageDefault);
|
||||
byId("api-memory-limit").value = String(memoryLimit);
|
||||
byId("api-memory-size-limit").value = String(memoryLimitMb);
|
||||
byId("api-entry-limit").value = String(apiEntryLimit);
|
||||
byId("image-size-limit").value = String(imageLimitMb);
|
||||
byId("api-memory-summary").textContent = `${stats.api_memory_count || 0} / ${memoryLimit} 条`;
|
||||
byId("api-memory-summary").textContent = `${formatBytes(stats.api_memory_size_bytes)} / ${formatBytes(stats.api_memory_limit_bytes)}`;
|
||||
byId("api-memory-count").textContent = `${stats.api_memory_count || 0} 条,最久未使用优先淘汰`;
|
||||
byId("cache-default-summary").textContent = `${apiDefault} / ${imageDefault} 秒`;
|
||||
renderCacheTable("api");
|
||||
renderCacheTable("image");
|
||||
@@ -845,6 +933,52 @@ document.querySelectorAll(".tab").forEach((tab) => {
|
||||
});
|
||||
});
|
||||
|
||||
byId("add-subscription").addEventListener("click", () => openSubscriptionEditor());
|
||||
byId("cancel-subscription").addEventListener("click", () => {
|
||||
byId("subscription-form").hidden = true;
|
||||
editing.subscriptionSession = null;
|
||||
});
|
||||
byId("subscription-events").addEventListener("change", updateSubscriptionSelectionCount);
|
||||
byId("subscription-select-all").addEventListener("click", () => {
|
||||
byId("subscription-events").querySelectorAll("input").forEach((input) => { input.checked = true; });
|
||||
updateSubscriptionSelectionCount();
|
||||
});
|
||||
byId("subscription-clear-all").addEventListener("click", () => {
|
||||
byId("subscription-events").querySelectorAll("input").forEach((input) => { input.checked = false; });
|
||||
updateSubscriptionSelectionCount();
|
||||
});
|
||||
byId("subscription-form").addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
if (subscriptionSaving) return;
|
||||
const form = event.currentTarget;
|
||||
const sessionId = (editing.subscriptionSession ?? byId("subscription-session").value).trim();
|
||||
if (!sessionId) {
|
||||
showToast("会话 ID 不能为空", true);
|
||||
byId("subscription-session").focus();
|
||||
return;
|
||||
}
|
||||
const payload = {
|
||||
session_id: sessionId,
|
||||
enabled: byId("subscription-enabled").checked,
|
||||
actions: [...byId("subscription-events").querySelectorAll("input:checked")].map((input) => Number(input.value)),
|
||||
mode: editing.subscriptionSession === null ? "create" : "update",
|
||||
};
|
||||
subscriptionSaving = true;
|
||||
byId("subscription-fields").disabled = true;
|
||||
byId("add-subscription").disabled = true;
|
||||
try {
|
||||
const saved = await mutate("subscriptions/save", payload, "会话推送配置已保存");
|
||||
if (saved) {
|
||||
form.hidden = true;
|
||||
editing.subscriptionSession = null;
|
||||
}
|
||||
} finally {
|
||||
subscriptionSaving = false;
|
||||
byId("subscription-fields").disabled = false;
|
||||
byId("add-subscription").disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
byId("binding-form").addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
const saved = await mutate("bindings/save", {
|
||||
@@ -929,7 +1063,8 @@ byId("cache-limit-form").addEventListener("submit", async (event) => {
|
||||
submit.disabled = true;
|
||||
try {
|
||||
await bridge.apiPost("cache/limits/save", {
|
||||
api_memory_entries: Number(byId("api-memory-limit").value),
|
||||
api_memory_max_mb: Number(byId("api-memory-size-limit").value),
|
||||
api_max_entries: Number(byId("api-entry-limit").value),
|
||||
image_max_mb: Number(byId("image-size-limit").value),
|
||||
});
|
||||
await loadData();
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
<div class="section-intro section-intro--actions">
|
||||
<div>
|
||||
<h2>查询与图片缓存</h2>
|
||||
<p>接口数据以 JSON 保存到 SQLite,渲染图片保存到插件数据目录。图片命中时会直接发送,不再请求其上游接口;缓存时间使用秒,填写 0 可关闭对应缓存。</p>
|
||||
<p>接口数据以 JSON 保存到 SQLite,渲染图片保存到插件数据目录。图片命中时会直接发送,不再请求其上游接口;缓存时间使用秒,填写 0 可关闭对应缓存。每 60 秒自动清理过期缓存,接口请求失败时不使用过期数据。</p>
|
||||
</div>
|
||||
<div class="cache-clear-actions">
|
||||
<button class="button button--secondary" id="clear-api-cache" type="button">清空接口缓存</button>
|
||||
@@ -115,9 +115,9 @@
|
||||
</div>
|
||||
|
||||
<div class="cache-summary" aria-label="缓存统计">
|
||||
<div class="cache-stat"><span>接口缓存(SQLite)</span><strong id="api-cache-count">0 条</strong><small id="api-cache-size">0 B</small></div>
|
||||
<div class="cache-stat"><span>接口缓存(SQLite)</span><strong id="api-cache-count">0 / 256 条</strong><small id="api-cache-size">0 B</small></div>
|
||||
<div class="cache-stat"><span>图片缓存</span><strong id="image-cache-count">0 张</strong><small id="image-cache-size">0 B</small></div>
|
||||
<div class="cache-stat"><span>接口内存缓存</span><strong id="api-memory-summary">0 / 256 条</strong><small>最久未使用优先淘汰</small></div>
|
||||
<div class="cache-stat"><span>接口内存缓存</span><strong id="api-memory-summary">0 B / 16 MB</strong><small id="api-memory-count">0 条,最久未使用优先淘汰</small></div>
|
||||
<div class="cache-stat"><span>默认缓存时间</span><strong id="cache-default-summary">300 / 600 秒</strong><small>接口 / 图片</small></div>
|
||||
</div>
|
||||
|
||||
@@ -135,8 +135,12 @@
|
||||
|
||||
<form class="cache-defaults cache-limits" id="cache-limit-form">
|
||||
<label>
|
||||
<span>接口内存缓存最大条数</span>
|
||||
<input id="api-memory-limit" type="number" min="1" max="100000" step="1" required />
|
||||
<span>接口内存缓存最大容量(MB)</span>
|
||||
<input id="api-memory-size-limit" type="number" min="1" max="1024" step="1" required />
|
||||
</label>
|
||||
<label>
|
||||
<span>SQLite 接口缓存最大条数</span>
|
||||
<input id="api-entry-limit" type="number" min="1" max="100000" step="1" required />
|
||||
</label>
|
||||
<label>
|
||||
<span>图片缓存最大容量(MB)</span>
|
||||
@@ -212,12 +216,41 @@
|
||||
</section>
|
||||
|
||||
<section class="panel" id="subscriptions-panel" role="tabpanel" hidden>
|
||||
<div class="section-intro">
|
||||
<div><h2>所有会话事件状态</h2><p>总开关和已订阅事件均来自本地订阅表。</p></div>
|
||||
<div class="section-intro section-intro--actions">
|
||||
<div><h2>会话事件推送</h2><p>为会话配置推送总开关和订阅事件,保存后立即生效。推送仍受会话访问模式及绑定区服限制;删除仅移除该会话的推送配置。</p></div>
|
||||
<button class="button button--primary" id="add-subscription" type="button">添加推送会话</button>
|
||||
</div>
|
||||
<form class="subscription-editor" id="subscription-form" hidden>
|
||||
<fieldset id="subscription-fields">
|
||||
<legend id="subscription-editor-title">添加推送会话</legend>
|
||||
<div class="subscription-settings">
|
||||
<label>
|
||||
<span>会话 ID</span>
|
||||
<input id="subscription-session" name="session_id" list="session-options" maxlength="512" placeholder="选择已有会话或输入完整会话 ID" required />
|
||||
</label>
|
||||
<label class="subscription-choice">
|
||||
<input id="subscription-enabled" type="checkbox" />
|
||||
<span>开启事件推送</span>
|
||||
</label>
|
||||
</div>
|
||||
<p class="subscription-hint">关闭总开关会保留事件选择;未选择事件时不会收到推送。令牌事件需在插件配置中填写事件版令牌。</p>
|
||||
<div class="subscription-toolbar">
|
||||
<strong id="subscription-selection-count" aria-live="polite">已选择 0 项</strong>
|
||||
<div>
|
||||
<button class="link-button" id="subscription-select-all" type="button">全选</button>
|
||||
<button class="link-button" id="subscription-clear-all" type="button">清空选择</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="subscription-events"></div>
|
||||
<div class="subscription-form-actions">
|
||||
<button class="button button--primary" type="submit">保存推送配置</button>
|
||||
<button class="button button--secondary" id="cancel-subscription" type="button">取消</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead><tr><th>会话 ID</th><th>绑定区服</th><th>总开关</th><th>已订阅事件</th></tr></thead>
|
||||
<thead><tr><th>会话 ID</th><th>绑定区服</th><th>总开关</th><th>已订阅事件</th><th class="actions">操作</th></tr></thead>
|
||||
<tbody id="subscriptions-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -86,6 +86,24 @@ input:focus, select:focus { border-color: var(--focus); box-shadow: 0 0 0 3px co
|
||||
.section-intro { padding: 22px; border-bottom: 1px solid var(--border); }
|
||||
.section-intro--actions { display: flex; align-items: center; justify-content: space-between; gap: 20px; }
|
||||
.section-intro h2 { margin: 0; font-size: 18px; }
|
||||
.subscription-editor { padding: 22px; border-bottom: 1px solid var(--border); background: var(--surface-subtle); }
|
||||
.subscription-editor[hidden] { display: none; }
|
||||
.subscription-editor fieldset { min-width: 0; margin: 0; padding: 0; border: 0; }
|
||||
.subscription-editor legend { margin-bottom: 14px; font-size: 16px; font-weight: 700; }
|
||||
.subscription-settings { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: 24px; }
|
||||
.subscription-choice { display: flex; align-items: center; gap: 9px; cursor: pointer; text-align: left; }
|
||||
.subscription-choice input { width: 17px; height: 17px; padding: 0; flex: 0 0 auto; accent-color: var(--accent); }
|
||||
.subscription-choice span { overflow-wrap: anywhere; }
|
||||
.subscription-hint { margin: 14px 0; color: var(--muted); font-size: 13px; }
|
||||
.subscription-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin: 16px 0; font-size: 13px; }
|
||||
.subscription-editor .subscription-event-group { margin-top: 16px; }
|
||||
.subscription-event-group legend { margin-bottom: 10px; color: var(--muted); font-size: 13px; }
|
||||
.subscription-event-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); gap: 8px; }
|
||||
.subscription-event-grid .subscription-choice { padding: 10px; border: 1px solid var(--border); border-radius: 8px; background: var(--surface); }
|
||||
.subscription-event-grid .subscription-choice:has(input:checked) { border-color: var(--accent); background: var(--accent-soft); }
|
||||
.subscription-form-actions { display: flex; gap: 12px; margin-top: 22px; }
|
||||
.subscription-session-cell { overflow-wrap: anywhere; max-width: 300px; }
|
||||
#subscription-session[readonly] { color: var(--muted); background: var(--surface-subtle); }
|
||||
.cache-clear-actions { display: flex; flex: 0 0 auto; justify-content: flex-end; gap: 10px; }
|
||||
.cache-summary { display: grid; grid-template-columns: repeat(4, 1fr); border-bottom: 1px solid var(--border); background: var(--surface-subtle); }
|
||||
.cache-stat { display: flex; min-height: 100px; flex-direction: column; justify-content: center; padding: 18px 22px; }
|
||||
@@ -93,6 +111,7 @@ input:focus, select:focus { border-color: var(--focus); box-shadow: 0 0 0 3px co
|
||||
.cache-stat span, .cache-stat small { color: var(--muted); font-size: 12px; font-weight: 650; }
|
||||
.cache-stat strong { margin: 3px 0; font-size: 21px; font-variant-numeric: tabular-nums; }
|
||||
.cache-defaults { display: grid; grid-template-columns: minmax(240px, 1fr) minmax(240px, 1fr) auto; align-items: end; gap: 16px; padding: 22px; border-bottom: 1px solid var(--border); }
|
||||
.cache-limits { grid-template-columns: repeat(3, minmax(180px, 1fr)) auto; }
|
||||
.cache-group { border-bottom: 1px solid var(--border); }
|
||||
.cache-group:last-child { border-bottom: 0; }
|
||||
.cache-group summary { padding: 18px 22px; cursor: pointer; font-size: 16px; font-weight: 750; }
|
||||
@@ -136,6 +155,9 @@ tbody tr.is-editing { background: color-mix(in srgb, var(--accent-soft) 62%, tra
|
||||
.toast.is-error { border-left-color: var(--danger); }
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.subscription-settings { grid-template-columns: 1fr; gap: 16px; }
|
||||
.subscription-event-grid { grid-template-columns: 1fr; }
|
||||
.subscription-session-cell { max-width: none; }
|
||||
.shell { width: min(100% - 24px, 1260px); padding-top: 20px; }
|
||||
.page-header { align-items: stretch; flex-direction: column; }
|
||||
.section-intro--actions { align-items: stretch; flex-direction: column; }
|
||||
|
||||
Reference in New Issue
Block a user