fxdyz
This commit is contained in:
+353
-24
@@ -1,6 +1,15 @@
|
||||
const bridge = window.AstrBotPluginPage;
|
||||
const state = { bindings: [], subscriptions: [], aliases: [], kungfu: [], servers: [], events: {} };
|
||||
const editing = { aliasServer: null, kungfuPzid: null };
|
||||
const state = {
|
||||
bindings: [],
|
||||
subscriptions: [],
|
||||
aliases: [],
|
||||
kungfu: [],
|
||||
servers: [],
|
||||
events: {},
|
||||
session_control: { mode: "all", entries: [] },
|
||||
};
|
||||
const editing = { bindingSession: null, controlSession: null, aliasServer: null, kungfuPzid: null };
|
||||
const restoreConfirmationTimers = new WeakMap();
|
||||
let toastTimer;
|
||||
|
||||
const byId = (id) => document.getElementById(id);
|
||||
@@ -69,24 +78,226 @@ function inlineAliasEditor(aliases, label, onSave, onCancel) {
|
||||
return { input, controls: [saveButton, cancelButton] };
|
||||
}
|
||||
|
||||
function createServerSelect(selectedServer = "", label = "绑定区服") {
|
||||
const select = document.createElement("select");
|
||||
select.className = "inline-editor";
|
||||
select.required = true;
|
||||
select.setAttribute("aria-label", label);
|
||||
|
||||
const placeholder = document.createElement("option");
|
||||
placeholder.value = "";
|
||||
placeholder.textContent = "请选择标准区服";
|
||||
placeholder.disabled = true;
|
||||
placeholder.defaultSelected = true;
|
||||
select.append(placeholder);
|
||||
|
||||
state.servers.forEach((server) => {
|
||||
const option = document.createElement("option");
|
||||
option.value = server;
|
||||
option.textContent = server;
|
||||
select.append(option);
|
||||
});
|
||||
select.value = state.servers.includes(selectedServer) ? selectedServer : "";
|
||||
return select;
|
||||
}
|
||||
|
||||
function inlineServerEditor(item, onSave, onCancel) {
|
||||
const select = createServerSelect(item.server, `${item.session_id}的绑定区服`);
|
||||
const saveButton = button("保存", "", async () => {
|
||||
if (!select.reportValidity()) return;
|
||||
select.disabled = true;
|
||||
saveButton.disabled = true;
|
||||
cancelButton.disabled = true;
|
||||
const saved = await onSave(select.value);
|
||||
if (!saved) {
|
||||
select.disabled = false;
|
||||
saveButton.disabled = false;
|
||||
cancelButton.disabled = false;
|
||||
select.focus();
|
||||
}
|
||||
});
|
||||
const cancelButton = button("取消", "", onCancel);
|
||||
select.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
saveButton.click();
|
||||
} else if (event.key === "Escape") {
|
||||
event.preventDefault();
|
||||
cancelButton.click();
|
||||
}
|
||||
});
|
||||
queueMicrotask(() => select.focus());
|
||||
return { select, controls: [saveButton, cancelButton] };
|
||||
}
|
||||
|
||||
function bindingMap() {
|
||||
return new Map(state.bindings.map((item) => [item.session_id, item.server]));
|
||||
}
|
||||
|
||||
function renderSummary() {
|
||||
byId("binding-count").textContent = String(state.bindings.length);
|
||||
byId("subscription-count").textContent = String(state.subscriptions.filter((item) => item.enabled).length);
|
||||
byId("alias-count").textContent = String(state.aliases.reduce((total, item) => total + item.aliases.length, 0));
|
||||
byId("kungfu-count").textContent = String(state.kungfu.length);
|
||||
}
|
||||
|
||||
function renderServerOptions() {
|
||||
const list = byId("server-options");
|
||||
list.replaceChildren(...state.servers.map((server) => {
|
||||
const select = byId("binding-server");
|
||||
const currentValue = select.value;
|
||||
const placeholder = document.createElement("option");
|
||||
placeholder.value = "";
|
||||
placeholder.textContent = "请选择标准区服";
|
||||
placeholder.disabled = true;
|
||||
placeholder.defaultSelected = true;
|
||||
select.replaceChildren(placeholder, ...state.servers.map((server) => {
|
||||
const option = document.createElement("option");
|
||||
option.value = server;
|
||||
option.textContent = server;
|
||||
return option;
|
||||
}));
|
||||
select.value = state.servers.includes(currentValue) ? currentValue : "";
|
||||
}
|
||||
|
||||
function renderSessionOptions() {
|
||||
const sessionIds = new Set([
|
||||
...state.bindings.map((item) => item.session_id),
|
||||
...state.subscriptions.map((item) => item.session_id),
|
||||
...state.session_control.entries.map((item) => item.session_id),
|
||||
]);
|
||||
byId("session-options").replaceChildren(...[...sessionIds].sort().map((sessionId) => {
|
||||
const option = document.createElement("option");
|
||||
option.value = sessionId;
|
||||
return option;
|
||||
}));
|
||||
}
|
||||
|
||||
function controlModeCopy(mode) {
|
||||
if (mode === "whitelist") {
|
||||
return "只有白名单中的会话可以使用插件和接收事件推送;白名单为空时不放行任何会话。";
|
||||
}
|
||||
if (mode === "blacklist") {
|
||||
return "黑名单中的会话会被拦截;黑名单为空时放行全部会话。";
|
||||
}
|
||||
return "所有会话都可以使用插件并接收已订阅的事件推送;下方名单暂不生效。";
|
||||
}
|
||||
|
||||
function controlModeLabel(mode) {
|
||||
if (mode === "whitelist") return "白名单";
|
||||
if (mode === "blacklist") return "黑名单";
|
||||
return "全部会话";
|
||||
}
|
||||
|
||||
function updateModeSelection(selectedMode) {
|
||||
const activeMode = state.session_control?.mode || "all";
|
||||
document.querySelectorAll(".mode-option").forEach((option) => {
|
||||
const input = option.querySelector('input[name="control_mode"]');
|
||||
option.classList.toggle("is-selected", input?.value === selectedMode);
|
||||
option.classList.toggle("is-active-mode", input?.value === activeMode);
|
||||
});
|
||||
const saveButton = byId("control-mode-save");
|
||||
saveButton.textContent = selectedMode === activeMode
|
||||
? "当前模式已生效"
|
||||
: `切换为${controlModeLabel(selectedMode)}`;
|
||||
}
|
||||
|
||||
function renderSessionControl() {
|
||||
const control = state.session_control || { mode: "all", entries: [] };
|
||||
document.querySelectorAll('input[name="control_mode"]').forEach((input) => {
|
||||
input.checked = input.value === control.mode;
|
||||
});
|
||||
updateModeSelection(control.mode);
|
||||
byId("control-mode-label").textContent = controlModeLabel(control.mode);
|
||||
byId("control-mode-hint").textContent = controlModeCopy(control.mode);
|
||||
|
||||
const body = byId("control-entries-body");
|
||||
if (!control.entries.length) {
|
||||
body.replaceChildren(emptyRow(4, "暂无白名单或黑名单会话"));
|
||||
return;
|
||||
}
|
||||
|
||||
body.replaceChildren(...control.entries.map((item) => {
|
||||
const row = document.createElement("tr");
|
||||
const session = document.createElement("td");
|
||||
const listType = document.createElement("td");
|
||||
const remark = document.createElement("td");
|
||||
const actions = document.createElement("td");
|
||||
session.dataset.label = "会话 ID";
|
||||
listType.dataset.label = "名单类型";
|
||||
remark.dataset.label = "备注";
|
||||
actions.dataset.label = "操作";
|
||||
actions.className = "actions";
|
||||
session.textContent = item.session_id;
|
||||
|
||||
if (editing.controlSession === item.session_id) {
|
||||
row.classList.add("is-editing");
|
||||
const typeSelect = document.createElement("select");
|
||||
typeSelect.className = "inline-editor inline-editor--compact";
|
||||
typeSelect.setAttribute("aria-label", `${item.session_id}的名单类型`);
|
||||
typeSelect.append(
|
||||
new Option("白名单", "whitelist"),
|
||||
new Option("黑名单", "blacklist"),
|
||||
);
|
||||
typeSelect.value = item.list_type;
|
||||
|
||||
const remarkInput = document.createElement("input");
|
||||
remarkInput.className = "inline-editor";
|
||||
remarkInput.maxLength = 200;
|
||||
remarkInput.value = item.remark || "";
|
||||
remarkInput.placeholder = "备注(可选)";
|
||||
remarkInput.setAttribute("aria-label", `${item.session_id}的备注`);
|
||||
|
||||
const saveButton = button("保存", "", async () => {
|
||||
typeSelect.disabled = true;
|
||||
remarkInput.disabled = true;
|
||||
saveButton.disabled = true;
|
||||
cancelButton.disabled = true;
|
||||
editing.controlSession = null;
|
||||
const saved = await mutate(
|
||||
"session-control/save",
|
||||
{ session_id: item.session_id, list_type: typeSelect.value, remark: remarkInput.value },
|
||||
"会话名单已保存",
|
||||
);
|
||||
if (!saved) {
|
||||
editing.controlSession = item.session_id;
|
||||
renderSessionControl();
|
||||
}
|
||||
});
|
||||
const cancelButton = button("取消", "", () => {
|
||||
editing.controlSession = null;
|
||||
renderSessionControl();
|
||||
});
|
||||
remarkInput.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
saveButton.click();
|
||||
} else if (event.key === "Escape") {
|
||||
event.preventDefault();
|
||||
cancelButton.click();
|
||||
}
|
||||
});
|
||||
remark.append(remarkInput);
|
||||
listType.append(typeSelect);
|
||||
actions.append(saveButton, cancelButton);
|
||||
queueMicrotask(() => typeSelect.focus());
|
||||
} else {
|
||||
const badge = document.createElement("span");
|
||||
badge.className = `list-badge list-badge--${item.list_type}`;
|
||||
badge.textContent = item.list_type === "whitelist" ? "白名单" : "黑名单";
|
||||
listType.append(badge);
|
||||
remark.textContent = item.remark || "—";
|
||||
actions.append(
|
||||
button("编辑", "", () => {
|
||||
editing.controlSession = item.session_id;
|
||||
renderSessionControl();
|
||||
}),
|
||||
button("删除", "link-button--danger", async (event) => {
|
||||
const deleteButton = event.currentTarget;
|
||||
deleteButton.disabled = true;
|
||||
const deleted = await mutate(
|
||||
"session-control/delete",
|
||||
{ session_id: item.session_id },
|
||||
"会话名单已删除",
|
||||
);
|
||||
if (!deleted) deleteButton.disabled = false;
|
||||
}),
|
||||
);
|
||||
}
|
||||
row.append(session, listType, remark, actions);
|
||||
return row;
|
||||
}));
|
||||
}
|
||||
|
||||
function renderBindings() {
|
||||
@@ -104,19 +315,50 @@ function renderBindings() {
|
||||
server.dataset.label = "绑定区服";
|
||||
actions.dataset.label = "操作";
|
||||
session.textContent = item.session_id;
|
||||
server.textContent = item.server;
|
||||
actions.className = "actions";
|
||||
actions.append(
|
||||
button("编辑", "", () => {
|
||||
byId("binding-session").value = item.session_id;
|
||||
byId("binding-server").value = item.server;
|
||||
byId("binding-server").focus();
|
||||
}),
|
||||
button("解除绑定", "link-button--danger", async () => {
|
||||
if (!window.confirm(`确认解除会话 ${item.session_id} 的区服绑定?`)) return;
|
||||
await mutate("bindings/delete", { session_id: item.session_id }, "绑定已解除");
|
||||
}),
|
||||
);
|
||||
if (editing.bindingSession === item.session_id) {
|
||||
row.classList.add("is-editing");
|
||||
const editor = inlineServerEditor(
|
||||
item,
|
||||
async (selectedServer) => {
|
||||
editing.bindingSession = null;
|
||||
const saved = await mutate(
|
||||
"bindings/save",
|
||||
{ session_id: item.session_id, server: selectedServer },
|
||||
"绑定信息已保存",
|
||||
);
|
||||
if (!saved) {
|
||||
editing.bindingSession = item.session_id;
|
||||
renderBindings();
|
||||
}
|
||||
return saved;
|
||||
},
|
||||
() => {
|
||||
editing.bindingSession = null;
|
||||
renderBindings();
|
||||
},
|
||||
);
|
||||
server.append(editor.select);
|
||||
actions.append(...editor.controls);
|
||||
} else {
|
||||
server.textContent = item.server;
|
||||
actions.append(
|
||||
button("编辑", "", () => {
|
||||
editing.bindingSession = item.session_id;
|
||||
renderBindings();
|
||||
}),
|
||||
button("解除绑定", "link-button--danger", async (event) => {
|
||||
const control = event.currentTarget;
|
||||
control.disabled = true;
|
||||
const deleted = await mutate(
|
||||
"bindings/delete",
|
||||
{ session_id: item.session_id },
|
||||
"绑定已解除",
|
||||
);
|
||||
if (!deleted) control.disabled = false;
|
||||
}),
|
||||
);
|
||||
}
|
||||
row.append(session, server, actions);
|
||||
return row;
|
||||
}));
|
||||
@@ -283,8 +525,9 @@ function renderKungfu() {
|
||||
}
|
||||
|
||||
function render() {
|
||||
renderSummary();
|
||||
renderServerOptions();
|
||||
renderSessionOptions();
|
||||
renderSessionControl();
|
||||
renderBindings();
|
||||
renderSubscriptions();
|
||||
renderAliases();
|
||||
@@ -309,6 +552,50 @@ async function mutate(endpoint, payload, successMessage) {
|
||||
}
|
||||
}
|
||||
|
||||
function resetRestoreConfirmation(control) {
|
||||
const timer = restoreConfirmationTimers.get(control);
|
||||
if (timer) clearTimeout(timer);
|
||||
restoreConfirmationTimers.delete(control);
|
||||
delete control.dataset.confirming;
|
||||
control.classList.remove("button--danger");
|
||||
control.textContent = "恢复默认";
|
||||
}
|
||||
|
||||
function confirmRestoreInPage(control, confirmation) {
|
||||
if (control.dataset.confirming === "true") {
|
||||
resetRestoreConfirmation(control);
|
||||
return true;
|
||||
}
|
||||
|
||||
control.dataset.confirming = "true";
|
||||
control.classList.add("button--danger");
|
||||
control.textContent = "再次点击确认";
|
||||
showToast(confirmation);
|
||||
restoreConfirmationTimers.set(
|
||||
control,
|
||||
setTimeout(() => resetRestoreConfirmation(control), 5000),
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
async function restoreDefaults(control, endpoint, confirmation, successMessage, resetEditing) {
|
||||
if (!confirmRestoreInPage(control, confirmation)) return;
|
||||
const originalLabel = control.textContent;
|
||||
control.disabled = true;
|
||||
control.textContent = "恢复中…";
|
||||
resetEditing();
|
||||
try {
|
||||
await bridge.apiPost(endpoint, {});
|
||||
await loadData();
|
||||
showToast(successMessage);
|
||||
} catch (error) {
|
||||
showToast(error?.message || "恢复默认失败", true);
|
||||
} finally {
|
||||
control.disabled = false;
|
||||
control.textContent = originalLabel;
|
||||
}
|
||||
}
|
||||
|
||||
document.querySelectorAll(".tab").forEach((tab) => {
|
||||
tab.addEventListener("click", () => {
|
||||
document.querySelectorAll(".tab").forEach((item) => {
|
||||
@@ -333,6 +620,48 @@ byId("binding-form").addEventListener("submit", async (event) => {
|
||||
if (saved) event.currentTarget.reset();
|
||||
});
|
||||
|
||||
document.querySelectorAll('input[name="control_mode"]').forEach((input) => {
|
||||
input.addEventListener("change", () => {
|
||||
updateModeSelection(input.value);
|
||||
});
|
||||
});
|
||||
|
||||
byId("control-mode-form").addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
const selected = new FormData(event.currentTarget).get("control_mode");
|
||||
await mutate("session-control/mode", { mode: selected }, "会话控制模式已保存");
|
||||
});
|
||||
|
||||
byId("control-entry-form").addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
const saved = await mutate("session-control/save", {
|
||||
session_id: byId("control-session").value,
|
||||
list_type: byId("control-list-type").value,
|
||||
remark: byId("control-remark").value,
|
||||
}, "会话名单已保存");
|
||||
if (saved) event.currentTarget.reset();
|
||||
});
|
||||
|
||||
byId("restore-aliases").addEventListener("click", async (event) => {
|
||||
await restoreDefaults(
|
||||
event.currentTarget,
|
||||
"aliases/restore",
|
||||
"再次点击按钮,确认使用内置 JSON 覆盖当前全部区服别名",
|
||||
"区服别名已恢复默认",
|
||||
() => { editing.aliasServer = null; },
|
||||
);
|
||||
});
|
||||
|
||||
byId("restore-kungfu").addEventListener("click", async (event) => {
|
||||
await restoreDefaults(
|
||||
event.currentTarget,
|
||||
"kungfu/restore",
|
||||
"再次点击按钮,确认使用内置 JSON 覆盖当前全部心法及别名",
|
||||
"心法别名已恢复默认",
|
||||
() => { editing.kungfuPzid = null; },
|
||||
);
|
||||
});
|
||||
|
||||
byId("refresh").addEventListener("click", async (event) => {
|
||||
const control = event.currentTarget;
|
||||
control.disabled = true;
|
||||
|
||||
@@ -12,38 +12,76 @@
|
||||
<header class="page-header">
|
||||
<div>
|
||||
<h1>剑网三插件管理</h1>
|
||||
<p>管理会话区服绑定、事件订阅状态、区服别名与心法别名。</p>
|
||||
<p>管理事件推送、会话访问范围、区服绑定及别名。</p>
|
||||
</div>
|
||||
<button class="button button--secondary" id="refresh" type="button">刷新数据</button>
|
||||
</header>
|
||||
|
||||
<section class="summary" aria-label="数据概览">
|
||||
<div class="summary__item">
|
||||
<span>已绑定会话</span>
|
||||
<strong id="binding-count">—</strong>
|
||||
</div>
|
||||
<div class="summary__item">
|
||||
<span>事件推送会话</span>
|
||||
<strong id="subscription-count">—</strong>
|
||||
</div>
|
||||
<div class="summary__item">
|
||||
<span>区服别名</span>
|
||||
<strong id="alias-count">—</strong>
|
||||
</div>
|
||||
<div class="summary__item">
|
||||
<span>心法条目</span>
|
||||
<strong id="kungfu-count">—</strong>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<nav class="tabs" aria-label="管理分类" role="tablist">
|
||||
<button class="tab is-active" type="button" role="tab" aria-selected="true" data-tab="bindings">会话绑定</button>
|
||||
<button class="tab" type="button" role="tab" aria-selected="false" data-tab="subscriptions">事件订阅</button>
|
||||
<button class="tab is-active" type="button" role="tab" aria-selected="true" data-tab="session-control">会话控制</button>
|
||||
<button class="tab" type="button" role="tab" aria-selected="false" data-tab="subscriptions">事件推送</button>
|
||||
<button class="tab" type="button" role="tab" aria-selected="false" data-tab="bindings">区服绑定</button>
|
||||
<button class="tab" type="button" role="tab" aria-selected="false" data-tab="aliases">区服别名</button>
|
||||
<button class="tab" type="button" role="tab" aria-selected="false" data-tab="kungfu">心法别名</button>
|
||||
</nav>
|
||||
|
||||
<section class="panel is-active" id="bindings-panel" role="tabpanel">
|
||||
<section class="panel is-active" id="session-control-panel" role="tabpanel">
|
||||
<form class="control-mode" id="control-mode-form">
|
||||
<div class="section-intro control-mode__intro">
|
||||
<div>
|
||||
<h2>会话访问模式</h2>
|
||||
<p id="control-mode-hint">所有会话都可以使用插件并接收已订阅的事件推送。</p>
|
||||
</div>
|
||||
<div class="current-mode" aria-live="polite">
|
||||
<span>当前生效模式</span>
|
||||
<strong id="control-mode-label">全部会话</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mode-options" role="radiogroup" aria-label="会话访问模式">
|
||||
<label class="mode-option">
|
||||
<input type="radio" name="control_mode" value="all" />
|
||||
<span><strong>全部会话</strong><small>不限制任何会话,默认模式</small></span>
|
||||
</label>
|
||||
<label class="mode-option">
|
||||
<input type="radio" name="control_mode" value="whitelist" />
|
||||
<span><strong>白名单</strong><small>只允许白名单中的会话</small></span>
|
||||
</label>
|
||||
<label class="mode-option">
|
||||
<input type="radio" name="control_mode" value="blacklist" />
|
||||
<span><strong>黑名单</strong><small>只拦截黑名单中的会话</small></span>
|
||||
</label>
|
||||
<button class="button button--primary" id="control-mode-save" type="submit">保存模式</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form class="editor editor--control" id="control-entry-form">
|
||||
<label>
|
||||
<span>会话 ID</span>
|
||||
<input id="control-session" name="session_id" list="session-options" maxlength="512" placeholder="可选择已有会话或直接输入" required />
|
||||
<datalist id="session-options"></datalist>
|
||||
</label>
|
||||
<label>
|
||||
<span>名单类型</span>
|
||||
<select id="control-list-type" name="list_type" required>
|
||||
<option value="whitelist">白名单</option>
|
||||
<option value="blacklist">黑名单</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>备注(可选)</span>
|
||||
<input id="control-remark" name="remark" maxlength="200" placeholder="例如:测试会话" />
|
||||
</label>
|
||||
<button class="button button--primary" type="submit">添加会话</button>
|
||||
</form>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead><tr><th>会话 ID</th><th>名单类型</th><th>备注</th><th class="actions">操作</th></tr></thead>
|
||||
<tbody id="control-entries-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel" id="bindings-panel" role="tabpanel" hidden>
|
||||
<form class="editor" id="binding-form">
|
||||
<label>
|
||||
<span>会话 ID</span>
|
||||
@@ -51,9 +89,11 @@
|
||||
</label>
|
||||
<label>
|
||||
<span>绑定区服</span>
|
||||
<input id="binding-server" name="server" maxlength="64" list="server-options" placeholder="选择或输入标准区服名/别名" required />
|
||||
<select id="binding-server" name="server" required>
|
||||
<option value="" selected disabled>请选择标准区服</option>
|
||||
</select>
|
||||
</label>
|
||||
<button class="button button--primary" type="submit">保存绑定</button>
|
||||
<button class="button button--primary" type="submit">添加绑定</button>
|
||||
</form>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
@@ -76,8 +116,9 @@
|
||||
</section>
|
||||
|
||||
<section class="panel" id="aliases-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>标准区服名称为只读;点击对应行的“编辑”可修改别名,恢复默认会使用内置 JSON 覆盖当前配置。</p></div>
|
||||
<button class="button button--secondary" id="restore-aliases" type="button">恢复默认</button>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
@@ -88,8 +129,9 @@
|
||||
</section>
|
||||
|
||||
<section class="panel" id="kungfu-panel" role="tabpanel" hidden>
|
||||
<div class="section-intro">
|
||||
<div><h2>心法别名</h2><p>标准心法名称为只读;点击对应行的“编辑”可修改最多 5 个别名。</p></div>
|
||||
<div class="section-intro section-intro--actions">
|
||||
<div><h2>心法别名</h2><p>标准心法名称为只读;点击对应行的“编辑”可修改最多 5 个别名,恢复默认会使用内置 JSON 覆盖当前配置。</p></div>
|
||||
<button class="button button--secondary" id="restore-kungfu" type="button">恢复默认</button>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
@@ -100,7 +142,6 @@
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<datalist id="server-options"></datalist>
|
||||
<div class="toast" id="toast" role="status" aria-live="polite"></div>
|
||||
<script type="module" src="./app.js"></script>
|
||||
</body>
|
||||
|
||||
@@ -35,35 +35,48 @@
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; background: var(--bg); color: var(--text); font-size: 15px; line-height: 1.55; }
|
||||
button, input { font: inherit; }
|
||||
button, input, select { font: inherit; }
|
||||
.shell { width: min(1260px, calc(100% - 40px)); margin: 0 auto; padding: 32px 0 56px; }
|
||||
.page-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 24px; margin-bottom: 24px; }
|
||||
h1 { margin: 0; font-size: clamp(26px, 3vw, 36px); line-height: 1.2; letter-spacing: -0.025em; }
|
||||
.page-header p, .section-intro p { margin: 7px 0 0; color: var(--muted); }
|
||||
.button { min-height: 42px; padding: 9px 18px; border: 1px solid transparent; border-radius: 9px; cursor: pointer; font-weight: 650; transition: 160ms ease; white-space: nowrap; }
|
||||
.button:focus-visible, .tab:focus-visible, input:focus-visible, .link-button:focus-visible { outline: 3px solid color-mix(in srgb, var(--focus) 45%, transparent); outline-offset: 2px; }
|
||||
.button:focus-visible, .tab:focus-visible, input:focus-visible, select:focus-visible, .link-button:focus-visible { outline: 3px solid color-mix(in srgb, var(--focus) 45%, transparent); outline-offset: 2px; }
|
||||
.button:disabled { cursor: wait; opacity: .65; }
|
||||
.button--primary { color: #fff; background: var(--accent); box-shadow: 0 5px 14px color-mix(in srgb, var(--accent) 20%, transparent); }
|
||||
.button--primary:hover { background: var(--accent-hover); transform: translateY(-1px); }
|
||||
.button--secondary { color: var(--text); background: var(--surface); border-color: var(--border); }
|
||||
.button--secondary:hover { border-color: color-mix(in srgb, var(--text) 24%, var(--border)); }
|
||||
.summary { display: grid; grid-template-columns: repeat(4, 1fr); background: var(--surface); border: 1px solid var(--border); border-radius: 12px; box-shadow: var(--shadow); }
|
||||
.summary__item { display: flex; min-height: 112px; flex-direction: column; justify-content: center; padding: 22px 30px; }
|
||||
.summary__item + .summary__item { border-left: 1px solid var(--border); }
|
||||
.summary span { color: var(--muted); font-weight: 600; }
|
||||
.summary strong { margin-top: 2px; font-size: 30px; line-height: 1.2; }
|
||||
.button--danger { color: #fff; background: var(--danger); border-color: var(--danger); }
|
||||
.button--danger:hover { background: color-mix(in srgb, var(--danger) 88%, #000); border-color: transparent; }
|
||||
.tabs { display: flex; gap: 30px; margin-top: 24px; border-bottom: 1px solid var(--border); }
|
||||
.tab { position: relative; padding: 14px 4px 15px; border: 0; background: transparent; color: var(--muted); cursor: pointer; font-weight: 650; }
|
||||
.tab:hover, .tab.is-active { color: var(--accent); }
|
||||
.tab.is-active::after { position: absolute; right: 0; bottom: -1px; left: 0; height: 3px; border-radius: 3px 3px 0 0; background: var(--accent); content: ""; }
|
||||
.panel { margin-top: 18px; background: var(--surface); border: 1px solid var(--border); border-radius: 12px; box-shadow: var(--shadow); overflow: hidden; }
|
||||
.panel[hidden] { display: none; }
|
||||
.control-mode { background: var(--surface-subtle); border-bottom: 1px solid var(--border); }
|
||||
.control-mode__intro { display: flex; align-items: center; justify-content: space-between; gap: 24px; border-bottom: 0; padding-bottom: 16px; }
|
||||
.current-mode { display: grid; flex: 0 0 auto; min-width: 210px; gap: 2px; padding: 13px 17px; border: 1px solid color-mix(in srgb, var(--accent) 42%, var(--border)); border-radius: 10px; background: var(--accent-soft); box-shadow: inset 4px 0 0 var(--accent); }
|
||||
.current-mode span { color: var(--muted); font-size: 12px; font-weight: 700; }
|
||||
.current-mode strong { color: var(--accent); font-size: 20px; line-height: 1.35; }
|
||||
.mode-options { display: grid; grid-template-columns: repeat(3, minmax(180px, 1fr)) auto; align-items: stretch; gap: 12px; padding: 0 22px 22px; }
|
||||
.mode-option { position: relative; display: flex; grid-template-columns: none; align-items: flex-start; gap: 10px; min-height: 76px; padding: 14px; border: 1px solid var(--border); border-radius: 9px; background: var(--surface); cursor: pointer; font-size: 14px; }
|
||||
.mode-option:hover, .mode-option.is-selected { border-color: color-mix(in srgb, var(--accent) 58%, var(--border)); background: var(--accent-soft); }
|
||||
.mode-option.is-active-mode::after { position: absolute; top: 8px; right: 8px; padding: 2px 7px; border-radius: 999px; background: var(--accent); color: #fff; content: "当前"; font-size: 11px; font-weight: 700; }
|
||||
.mode-option input { flex: 0 0 auto; width: 18px; height: 18px; margin: 2px 0 0; accent-color: var(--accent); box-shadow: none; }
|
||||
.mode-option span { display: grid; gap: 3px; }
|
||||
.mode-option strong { color: var(--text); font-size: 14px; }
|
||||
.mode-option small { color: var(--muted); font-size: 12px; font-weight: 500; line-height: 1.4; }
|
||||
.mode-options .button { align-self: center; }
|
||||
.editor { display: grid; grid-template-columns: minmax(260px, 1.35fr) minmax(210px, 1fr) auto; align-items: end; gap: 16px; padding: 22px; background: var(--surface-subtle); border-bottom: 1px solid var(--border); }
|
||||
.editor--control { grid-template-columns: minmax(250px, 1.5fr) minmax(150px, .7fr) minmax(190px, 1fr) auto; }
|
||||
label { display: grid; gap: 7px; color: var(--text); font-size: 13px; font-weight: 650; }
|
||||
input { width: 100%; height: 42px; padding: 8px 12px; border: 1px solid var(--border); border-radius: 8px; background: var(--surface); color: var(--text); transition: border 160ms ease, box-shadow 160ms ease; }
|
||||
input, select { width: 100%; height: 42px; padding: 8px 12px; border: 1px solid var(--border); border-radius: 8px; background: var(--surface); color: var(--text); transition: border 160ms ease, box-shadow 160ms ease; }
|
||||
input::placeholder { color: color-mix(in srgb, var(--muted) 76%, transparent); }
|
||||
input:focus { border-color: var(--focus); box-shadow: 0 0 0 3px color-mix(in srgb, var(--focus) 18%, transparent); outline: none; }
|
||||
input:focus, select:focus { border-color: var(--focus); box-shadow: 0 0 0 3px color-mix(in srgb, var(--focus) 18%, transparent); outline: none; }
|
||||
.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; }
|
||||
.table-wrap { overflow-x: auto; }
|
||||
table { width: 100%; table-layout: auto; border-collapse: collapse; }
|
||||
@@ -74,12 +87,16 @@ tbody tr:hover { background: color-mix(in srgb, var(--accent-soft) 42%, transpar
|
||||
tbody tr.is-editing { background: color-mix(in srgb, var(--accent-soft) 62%, transparent); }
|
||||
.alias-cell { min-width: 320px; }
|
||||
.inline-editor { min-width: 260px; }
|
||||
.inline-editor--compact { min-width: 130px; }
|
||||
.actions { white-space: nowrap; }
|
||||
.link-button { padding: 4px 7px; border: 0; border-radius: 5px; background: transparent; color: #3568ae; cursor: pointer; font-weight: 650; }
|
||||
[data-theme="dark"] .link-button { color: #8db9f4; }
|
||||
.link-button:hover { background: color-mix(in srgb, currentColor 10%, transparent); }
|
||||
.link-button:disabled { cursor: wait; opacity: .55; }
|
||||
.link-button--danger { color: var(--danger); }
|
||||
.list-badge { display: inline-flex; padding: 3px 9px; border-radius: 999px; font-size: 12px; font-weight: 700; }
|
||||
.list-badge--whitelist { background: color-mix(in srgb, var(--success) 12%, transparent); color: var(--success); }
|
||||
.list-badge--blacklist { background: color-mix(in srgb, var(--danger) 12%, transparent); color: var(--danger); }
|
||||
.state { display: inline-flex; align-items: center; gap: 7px; font-weight: 650; }
|
||||
.state::before { width: 8px; height: 8px; border-radius: 50%; background: currentColor; content: ""; }
|
||||
.state--on { color: var(--success); }
|
||||
@@ -94,12 +111,14 @@ tbody tr.is-editing { background: color-mix(in srgb, var(--accent-soft) 62%, tra
|
||||
@media (max-width: 760px) {
|
||||
.shell { width: min(100% - 24px, 1260px); padding-top: 20px; }
|
||||
.page-header { align-items: stretch; flex-direction: column; }
|
||||
.summary { grid-template-columns: 1fr; }
|
||||
.summary__item { min-height: 86px; padding: 16px 20px; }
|
||||
.summary__item + .summary__item { border-top: 1px solid var(--border); border-left: 0; }
|
||||
.section-intro--actions { align-items: stretch; flex-direction: column; }
|
||||
.tabs { gap: 16px; overflow-x: auto; }
|
||||
.tab { flex: 0 0 auto; }
|
||||
.editor { grid-template-columns: 1fr; }
|
||||
.mode-options { grid-template-columns: 1fr; }
|
||||
.mode-options .button { width: 100%; }
|
||||
.control-mode__intro { align-items: stretch; flex-direction: column; }
|
||||
.current-mode { min-width: 0; }
|
||||
.table-wrap { overflow: visible; }
|
||||
table, tbody, tr, td { display: block; width: 100%; }
|
||||
thead { display: none; }
|
||||
|
||||
Reference in New Issue
Block a user