chore: initial commit
This commit is contained in:
154
web/api-config.html
Normal file
154
web/api-config.html
Normal file
@@ -0,0 +1,154 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>SGClaw Skill Converter - API 配置</title>
|
||||
<link rel="stylesheet" href="/style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<main class="page">
|
||||
<header class="hero">
|
||||
<h1>API 配置</h1>
|
||||
<p>这里填写 AI 调用参数,转换页面会作为实际生效配置使用。</p>
|
||||
<p class="subnav">
|
||||
<a href="/">返回转换页</a>
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<section class="grid">
|
||||
<section class="card">
|
||||
<label>
|
||||
API Base URL
|
||||
<input id="aiBaseUrl" type="text" placeholder="https://open.bigmodel.cn/api/paas/v4" />
|
||||
</label>
|
||||
<label>
|
||||
API Key
|
||||
<input id="aiApiKey" type="password" placeholder="sk-..." />
|
||||
</label>
|
||||
<label>
|
||||
模型
|
||||
<input id="aiModel" type="text" placeholder="glm-5.1" />
|
||||
</label>
|
||||
<label>
|
||||
超时(秒)
|
||||
<input id="aiTimeout" type="number" min="1" placeholder="30" />
|
||||
</label>
|
||||
|
||||
<div class="actions">
|
||||
<button type="button" id="saveApiConfig">保存</button>
|
||||
<button type="button" id="clearApiConfig">清空</button>
|
||||
</div>
|
||||
<p id="apiStatus" class="muted">未保存</p>
|
||||
<p id="serverCfgHint" class="muted">服务端: 读取中...</p>
|
||||
<p id="effectiveCfgHint" class="muted">实际生效: 未加载</p>
|
||||
</section>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
const API_CONFIG_KEY = "sgclaw.converter.apiConfig";
|
||||
const aiBaseUrl = document.getElementById("aiBaseUrl");
|
||||
const aiApiKey = document.getElementById("aiApiKey");
|
||||
const aiModel = document.getElementById("aiModel");
|
||||
const aiTimeout = document.getElementById("aiTimeout");
|
||||
const apiStatus = document.getElementById("apiStatus");
|
||||
const serverCfgHint = document.getElementById("serverCfgHint");
|
||||
const effectiveCfgHint = document.getElementById("effectiveCfgHint");
|
||||
|
||||
function trimText(v) {
|
||||
return typeof v === "string" ? v.trim() : "";
|
||||
}
|
||||
|
||||
function parseTimeout(v) {
|
||||
const n = Number.parseInt(trimText(v), 10);
|
||||
return Number.isFinite(n) && n > 0 ? n : "";
|
||||
}
|
||||
|
||||
function formatCfgLine(cfg) {
|
||||
const timeout = cfg.timeout ? `${cfg.timeout}s` : "未设置";
|
||||
const keyStatus = cfg.api_key ? "已配置" : "未配置";
|
||||
return `base_url=${cfg.base_url || "未设置"} | model=${cfg.model || "未设置"} | timeout=${timeout} | api_key=${keyStatus}`;
|
||||
}
|
||||
|
||||
function loadServerCfg() {
|
||||
return fetch("/api/config")
|
||||
.then((resp) => (resp.ok ? resp.json() : Promise.reject(new Error("config api unavailable"))))
|
||||
.then((cfg) => {
|
||||
serverCfgHint.textContent = `服务端: ${formatCfgLine(cfg)}`;
|
||||
return cfg;
|
||||
})
|
||||
.catch(() => {
|
||||
serverCfgHint.textContent = "服务端: 读取失败";
|
||||
return {};
|
||||
});
|
||||
}
|
||||
|
||||
function refreshEffectiveCfg(serverCfg) {
|
||||
const local = loadLocalCfg();
|
||||
const effective = {
|
||||
base_url: trimText(local.base_url) || trimText(serverCfg.base_url),
|
||||
model: trimText(local.model) || trimText(serverCfg.model),
|
||||
timeout: parseTimeout(trimText(local.timeout || serverCfg.timeout)),
|
||||
api_key: trimText(local.api_key),
|
||||
};
|
||||
effectiveCfgHint.textContent = `实际生效: ${formatCfgLine(effective)}(${effective.api_key ? "本地覆盖" : "服务端兜底"})`;
|
||||
}
|
||||
|
||||
function loadLocalCfg() {
|
||||
try {
|
||||
const raw = localStorage.getItem(API_CONFIG_KEY);
|
||||
if (!raw) {
|
||||
return {};
|
||||
}
|
||||
return JSON.parse(raw);
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function syncFromStorage() {
|
||||
const cfg = loadLocalCfg();
|
||||
aiBaseUrl.value = cfg.base_url || "";
|
||||
aiApiKey.value = cfg.api_key || "";
|
||||
aiModel.value = cfg.model || "";
|
||||
aiTimeout.value = cfg.timeout ? String(cfg.timeout) : "";
|
||||
apiStatus.textContent = cfg ? "本地配置已加载" : "未保存";
|
||||
}
|
||||
|
||||
function saveConfig() {
|
||||
const cfg = {
|
||||
base_url: trimText(aiBaseUrl.value),
|
||||
api_key: trimText(aiApiKey.value),
|
||||
model: trimText(aiModel.value),
|
||||
};
|
||||
const timeout = parseTimeout(aiTimeout.value);
|
||||
if (timeout) {
|
||||
cfg.timeout = timeout;
|
||||
}
|
||||
localStorage.setItem(API_CONFIG_KEY, JSON.stringify(cfg));
|
||||
apiStatus.textContent = "已保存(仅浏览器本地)";
|
||||
loadServerCfg().then(refreshEffectiveCfg);
|
||||
}
|
||||
|
||||
function clearConfig() {
|
||||
localStorage.removeItem(API_CONFIG_KEY);
|
||||
aiBaseUrl.value = "";
|
||||
aiApiKey.value = "";
|
||||
aiModel.value = "";
|
||||
aiTimeout.value = "";
|
||||
apiStatus.textContent = "已清空";
|
||||
loadServerCfg().then(refreshEffectiveCfg);
|
||||
}
|
||||
|
||||
document.getElementById("saveApiConfig").addEventListener("click", saveConfig);
|
||||
document.getElementById("clearApiConfig").addEventListener("click", clearConfig);
|
||||
|
||||
(async () => {
|
||||
const srv = await loadServerCfg();
|
||||
syncFromStorage();
|
||||
refreshEffectiveCfg(srv);
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
352
web/app.js
Normal file
352
web/app.js
Normal file
@@ -0,0 +1,352 @@
|
||||
const form = document.getElementById("converterForm");
|
||||
const jsCode = document.getElementById("jsCode");
|
||||
const scriptNotes = document.getElementById("scriptNotes");
|
||||
const scriptFile = document.getElementById("scriptFile");
|
||||
const selectedFile = document.getElementById("selectedFile");
|
||||
const skillRoot = document.getElementById("skillRoot");
|
||||
const aiStatus = document.getElementById("aiStatus");
|
||||
const aiServerCfg = document.getElementById("aiServerCfg");
|
||||
const aiEffectiveCfg = document.getElementById("aiEffectiveCfg");
|
||||
const useAi = document.getElementById("useAi");
|
||||
const statusBox = document.getElementById("status");
|
||||
const output = document.getElementById("output");
|
||||
const resultList = document.getElementById("resultList");
|
||||
const clearBtn = document.getElementById("clear");
|
||||
|
||||
const API_CONFIG_KEY = "sgclaw.converter.apiConfig";
|
||||
let serverCfg = null;
|
||||
|
||||
function setStatus(text, ok = true) {
|
||||
statusBox.textContent = text;
|
||||
statusBox.style.color = ok ? "#20a76b" : "#c62828";
|
||||
}
|
||||
|
||||
function fileToText(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => resolve(String(e.target.result || ""));
|
||||
reader.onerror = () => reject(new Error("读取文件失败"));
|
||||
reader.readAsText(file);
|
||||
});
|
||||
}
|
||||
|
||||
function readApiConfigFromStorage() {
|
||||
try {
|
||||
const raw = localStorage.getItem(API_CONFIG_KEY);
|
||||
if (!raw) return null;
|
||||
return JSON.parse(raw);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function trimText(v) {
|
||||
if (typeof v === "number" && Number.isFinite(v)) {
|
||||
return String(v);
|
||||
}
|
||||
return typeof v === "string" ? v.trim() : "";
|
||||
}
|
||||
|
||||
function parseTimeout(v) {
|
||||
const n = Number.parseInt(trimText(v), 10);
|
||||
return Number.isFinite(n) && n > 0 ? n : "";
|
||||
}
|
||||
|
||||
function maskApiKey(v) {
|
||||
const value = trimText(v);
|
||||
if (!value) {
|
||||
return "未配置";
|
||||
}
|
||||
if (value.length <= 6) {
|
||||
return `${value.slice(0, 1)}***${value.slice(-1)}`;
|
||||
}
|
||||
return `${value.slice(0, 3)}***${value.slice(-3)}`;
|
||||
}
|
||||
|
||||
function resolveAiConfig() {
|
||||
const localCfg = readApiConfigFromStorage() || {};
|
||||
const effective = {
|
||||
base_url: trimText(localCfg.base_url) || trimText(serverCfg?.base_url),
|
||||
model: trimText(localCfg.model) || trimText(serverCfg?.model),
|
||||
timeout: parseTimeout(trimText(localCfg.timeout || serverCfg?.timeout)),
|
||||
api_key: trimText(localCfg.api_key),
|
||||
api_key_masked: localCfg.api_key ? maskApiKey(localCfg.api_key) : trimText(serverCfg?.api_key_masked),
|
||||
key_source: localCfg.api_key ? "local" : "server_env",
|
||||
};
|
||||
return { local: localCfg, effective };
|
||||
}
|
||||
|
||||
function formatCfgLine(cfg) {
|
||||
const timeout = cfg.timeout ? `${cfg.timeout}s` : "未设置";
|
||||
return `base_url=${cfg.base_url || "未设置"} | model=${cfg.model || "未设置"} | timeout=${timeout} | api_key=${cfg.api_key_masked || "未配置"}`;
|
||||
}
|
||||
|
||||
function updateAiStatusSummary() {
|
||||
const resolved = resolveAiConfig();
|
||||
aiEffectiveCfg.textContent = `实际生效: ${formatCfgLine(resolved.effective)}(${resolved.effective.key_source})`;
|
||||
}
|
||||
|
||||
function formatAiState(result, requestedUseAi) {
|
||||
if (!requestedUseAi) {
|
||||
return "已关闭";
|
||||
}
|
||||
if (result.ai_used) {
|
||||
return "已使用模型";
|
||||
}
|
||||
if (result.ai_attempted) {
|
||||
return `已回退(${result.ai_fallback_reason || "模型调用失败"})`;
|
||||
}
|
||||
return `未调用模型(${result.ai_fallback_reason || "AI配置不可用"})`;
|
||||
}
|
||||
|
||||
function formatElapsed(ms) {
|
||||
if (!Number.isFinite(ms) || ms < 0) {
|
||||
return "-";
|
||||
}
|
||||
if (ms < 1000) {
|
||||
return `${Math.round(ms)} ms`;
|
||||
}
|
||||
return `${(ms / 1000).toFixed(2)} s`;
|
||||
}
|
||||
|
||||
function formatSourceName(fileName, index, total, isManual, note) {
|
||||
const tag = note ? ` (${note})` : "";
|
||||
if (!isManual) {
|
||||
return `${fileName}${tag}`;
|
||||
}
|
||||
return total > 1 ? `manual-${index + 1}${tag}` : `manual${tag}`;
|
||||
}
|
||||
|
||||
scriptFile.addEventListener("change", () => {
|
||||
const files = scriptFile.files || [];
|
||||
if (!files.length) {
|
||||
selectedFile.textContent = "未选择文件";
|
||||
return;
|
||||
}
|
||||
selectedFile.textContent = Array.from(files).map((f) => f.name).join(",");
|
||||
});
|
||||
|
||||
function renderResults(rows) {
|
||||
if (!rows.length) {
|
||||
resultList.innerHTML = "";
|
||||
return;
|
||||
}
|
||||
|
||||
resultList.innerHTML = rows
|
||||
.map((item) => {
|
||||
const statusClass = item.ok ? "result-ok" : "result-err";
|
||||
const openBtn = item.ok && item.target_dir
|
||||
? `<button type="button" class="open-dir-btn" data-target="${encodeURIComponent(item.target_dir)}">打开目录</button>`
|
||||
: "";
|
||||
return `
|
||||
<div class="result-item ${statusClass}">
|
||||
<div class="result-head">
|
||||
<strong>${item.source}</strong>
|
||||
${openBtn}
|
||||
</div>
|
||||
<div class="result-meta">技能名:${item.skill_name || "-"}</div>
|
||||
<div class="result-meta">目录:${item.target_dir || "-"}</div>
|
||||
<div class="result-meta">耗时:${formatElapsed(item.elapsed_ms)}</div>
|
||||
<div class="result-meta">实际AI配置:${item.ai_config_line || "-"}</div>
|
||||
<div class="result-meta">AI状态:${item.ai_state || "-"}</div>
|
||||
${item.ok ? "" : `<div class="result-meta err">${item.error || "处理失败"}</div>`}
|
||||
</div>
|
||||
`;
|
||||
})
|
||||
.join("");
|
||||
}
|
||||
|
||||
resultList.addEventListener("click", async (e) => {
|
||||
const targetBtn = e.target.closest(".open-dir-btn");
|
||||
if (!targetBtn) {
|
||||
return;
|
||||
}
|
||||
|
||||
const target = decodeURIComponent(targetBtn.dataset.target || "");
|
||||
if (!target) {
|
||||
setStatus("没有可打开的目录", false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const resp = await fetch(`/api/open-dir?path=${encodeURIComponent(target)}`);
|
||||
const result = await resp.json().catch(() => ({}));
|
||||
if (!resp.ok) {
|
||||
setStatus(`打开目录失败:${result.error || "server error"}`, false);
|
||||
return;
|
||||
}
|
||||
setStatus(`已打开:${result.target_dir}`);
|
||||
} catch (err) {
|
||||
setStatus(`打开目录失败:${err.message}`, false);
|
||||
}
|
||||
});
|
||||
|
||||
form.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
setStatus("正在生成...");
|
||||
const totalStart = performance.now();
|
||||
|
||||
const files = Array.from(scriptFile.files || []);
|
||||
const manualCode = jsCode.value.trim();
|
||||
const note = scriptNotes.value.trim();
|
||||
|
||||
const tasks = [];
|
||||
|
||||
for (const f of files) {
|
||||
tasks.push({
|
||||
source: f.name,
|
||||
isManual: false,
|
||||
name: `${f.name.replace(/\.[^/.]+$/, "")}`,
|
||||
promise: fileToText(f).then((code) => ({ code, error: null })).catch((err) => ({ code: "", error: err.message || "读取文件失败" })),
|
||||
});
|
||||
}
|
||||
|
||||
if (manualCode) {
|
||||
tasks.push({
|
||||
source: "manual-script",
|
||||
isManual: true,
|
||||
name: "manual-script",
|
||||
promise: Promise.resolve({ code: manualCode, error: null }),
|
||||
});
|
||||
}
|
||||
|
||||
if (!tasks.length) {
|
||||
setStatus("请上传脚本文件或粘贴脚本内容", false);
|
||||
return;
|
||||
}
|
||||
|
||||
const reads = await Promise.all(tasks.map((t) => t.promise));
|
||||
const { effective } = resolveAiConfig();
|
||||
const aiConfig = {
|
||||
base_url: effective.base_url,
|
||||
model: effective.model,
|
||||
timeout: effective.timeout || undefined,
|
||||
api_key: effective.api_key,
|
||||
};
|
||||
|
||||
const jobs = tasks.map((task, idx) => {
|
||||
const payload = reads[idx] || {};
|
||||
const code = payload.code || "";
|
||||
const readError = payload.error;
|
||||
|
||||
if (readError || !code.trim()) {
|
||||
const sourceName = formatSourceName(task.source, idx, tasks.length, task.isManual, note);
|
||||
return Promise.resolve({
|
||||
ok: false,
|
||||
source: sourceName,
|
||||
skill_name: task.name,
|
||||
target_dir: "",
|
||||
elapsed_ms: 0,
|
||||
ai_config_line: formatCfgLine(effective),
|
||||
ai_state: useAi.checked ? "未执行(输入读取失败)" : "已关闭",
|
||||
error: readError || "脚本内容为空",
|
||||
});
|
||||
}
|
||||
|
||||
const displaySource = formatSourceName(task.source, idx, tasks.length, task.isManual, note);
|
||||
|
||||
return (async () => {
|
||||
const itemStart = performance.now();
|
||||
const req = {
|
||||
js_script_code: code,
|
||||
write: true,
|
||||
use_ai: useAi.checked,
|
||||
skill_root: skillRoot.value,
|
||||
skill_name: task.name,
|
||||
skill_use_case: note,
|
||||
ai_config: aiConfig,
|
||||
};
|
||||
|
||||
try {
|
||||
const resp = await fetch("/api/convert", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json; charset=utf-8" },
|
||||
body: JSON.stringify(req),
|
||||
});
|
||||
const result = await resp.json();
|
||||
if (!resp.ok) {
|
||||
return {
|
||||
ok: false,
|
||||
source: displaySource,
|
||||
skill_name: task.name,
|
||||
target_dir: "",
|
||||
elapsed_ms: performance.now() - itemStart,
|
||||
ai_config_line: formatCfgLine(effective),
|
||||
ai_state: useAi.checked ? "请求失败" : "已关闭",
|
||||
error: result.error || "server error",
|
||||
};
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
source: displaySource,
|
||||
skill_name: result.skill_name,
|
||||
target_dir: result.target_dir,
|
||||
elapsed_ms: performance.now() - itemStart,
|
||||
ai_config_line: formatCfgLine(effective),
|
||||
ai_state: formatAiState(result, useAi.checked),
|
||||
raw: result,
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
ok: false,
|
||||
source: displaySource,
|
||||
skill_name: task.name,
|
||||
target_dir: "",
|
||||
elapsed_ms: performance.now() - itemStart,
|
||||
ai_config_line: formatCfgLine(effective),
|
||||
ai_state: useAi.checked ? "请求失败" : "已关闭",
|
||||
error: err.message || "请求失败",
|
||||
};
|
||||
}
|
||||
})();
|
||||
});
|
||||
|
||||
const results = await Promise.all(jobs);
|
||||
const success = results.filter((r) => r.ok).length;
|
||||
const fail = results.length - success;
|
||||
const totalElapsed = performance.now() - totalStart;
|
||||
setStatus(`完成:成功 ${success} / ${results.length},失败 ${fail},总耗时 ${formatElapsed(totalElapsed)}`, fail === 0);
|
||||
|
||||
renderResults(results);
|
||||
output.textContent = JSON.stringify(results, null, 2);
|
||||
});
|
||||
|
||||
clearBtn.addEventListener("click", () => {
|
||||
jsCode.value = "";
|
||||
scriptNotes.value = "";
|
||||
resultList.innerHTML = "";
|
||||
output.textContent = "";
|
||||
setStatus("");
|
||||
scriptFile.value = "";
|
||||
selectedFile.textContent = "未选择文件";
|
||||
});
|
||||
|
||||
async function loadConfig() {
|
||||
try {
|
||||
const resp = await fetch("/api/config");
|
||||
const cfg = await resp.json();
|
||||
serverCfg = cfg;
|
||||
aiStatus.textContent = `AI: ${cfg.ai_enabled ? "已启用" : "未启用"}`;
|
||||
aiServerCfg.textContent = `服务端: ${formatCfgLine(cfg)}`;
|
||||
updateAiStatusSummary();
|
||||
|
||||
const root = cfg.skill_root || "skills";
|
||||
const hasOption = Array.from(skillRoot.options).some((o) => o.value === root);
|
||||
if (!hasOption) {
|
||||
const option = document.createElement("option");
|
||||
option.value = root;
|
||||
option.textContent = root;
|
||||
skillRoot.appendChild(option);
|
||||
}
|
||||
skillRoot.value = root;
|
||||
} catch {
|
||||
serverCfg = {};
|
||||
aiStatus.textContent = "AI: 未读取到配置";
|
||||
aiServerCfg.textContent = "服务端: 读取失败";
|
||||
updateAiStatusSummary();
|
||||
skillRoot.value = "skills";
|
||||
}
|
||||
}
|
||||
|
||||
loadConfig();
|
||||
setInterval(updateAiStatusSummary, 2000);
|
||||
84
web/index.html
Normal file
84
web/index.html
Normal file
@@ -0,0 +1,84 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>SGClaw Skill Converter</title>
|
||||
<link rel="stylesheet" href="/style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<main class="page">
|
||||
<header class="hero">
|
||||
<h1>SGClaw Skill Converter</h1>
|
||||
<p>上传脚本、或粘贴脚本内容并填写说明,一键生成可直接打开目录的 SGClaw skill。</p>
|
||||
<p class="subnav">
|
||||
<a href="/api-config.html">API 配置页</a>
|
||||
<span>·</span>
|
||||
<a href="/">转换页</a>
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<section class="grid">
|
||||
<form id="converterForm" class="card">
|
||||
<label>
|
||||
上传脚本文件(支持多选)
|
||||
<input id="scriptFile" type="file" accept=".js,.txt" multiple />
|
||||
<small id="selectedFile" class="muted">未选择文件</small>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
脚本内容(可手填)
|
||||
<textarea id="jsCode" placeholder="粘贴 JS 脚本内容"></textarea>
|
||||
</label>
|
||||
<label>
|
||||
脚本说明(可选)
|
||||
<textarea id="scriptNotes" placeholder="例如:抓取知乎热榜,输出 title / heat / rank"></textarea>
|
||||
</label>
|
||||
|
||||
<div class="actions">
|
||||
<button type="submit">生成 Skill</button>
|
||||
<button type="button" id="clear">清空</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<details class="settings-sheet card" open>
|
||||
<summary>设置</summary>
|
||||
<div class="settings-inner">
|
||||
<h4>导出设置</h4>
|
||||
<p id="aiStatus" class="muted">AI: 加载中...</p>
|
||||
<p id="aiServerCfg" class="muted">服务端: 未加载</p>
|
||||
<p id="aiEffectiveCfg" class="muted">实际生效: 未加载</p>
|
||||
<label>
|
||||
导出目录(可选)
|
||||
<select id="skillRoot">
|
||||
<option value="skills">skills</option>
|
||||
<option value="output">output</option>
|
||||
<option value="dist">dist</option>
|
||||
<option value="tmp">tmp</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="inline-checkbox">
|
||||
<input id="useAi" type="checkbox" checked />
|
||||
使用 AI 生成元数据
|
||||
</label>
|
||||
<p class="muted">AI 配置读取于本地配置文件页(<a href="/api-config.html">去设置</a>)。</p>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<section class="card">
|
||||
<div class="outhead">
|
||||
<h3>输出</h3>
|
||||
<span id="status" class="muted"></span>
|
||||
</div>
|
||||
<div id="resultList" class="result-list"></div>
|
||||
<details class="raw-wrap">
|
||||
<summary>查看原始 JSON</summary>
|
||||
<pre id="output" class="output"></pre>
|
||||
</details>
|
||||
</section>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script src="/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
223
web/style.css
Normal file
223
web/style.css
Normal file
@@ -0,0 +1,223 @@
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
background: #f4f6fb;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.page {
|
||||
max-width: 980px;
|
||||
margin: 24px auto;
|
||||
padding: 0 16px 24px;
|
||||
}
|
||||
|
||||
.hero h1 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.hero p {
|
||||
margin: 8px 0 0;
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.subnav {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.subnav a {
|
||||
color: #2563eb;
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.subnav a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.muted {
|
||||
color: #6b7280;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.grid {
|
||||
margin-top: 16px;
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #fff;
|
||||
border: 1px solid #dbe3f0;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 12px;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
textarea,
|
||||
input,
|
||||
select,
|
||||
button {
|
||||
width: 100%;
|
||||
margin-top: 6px;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
font-size: 14px;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
textarea {
|
||||
min-height: 120px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.actions,
|
||||
.actions-line {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.actions-line {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
button {
|
||||
border: 0;
|
||||
border-radius: 8px;
|
||||
padding: 10px 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
background: #2563eb;
|
||||
color: #fff;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: #1d4ed8;
|
||||
}
|
||||
|
||||
.outhead {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.raw-wrap {
|
||||
margin-top: 12px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.output {
|
||||
margin: 10px 0 0;
|
||||
background: #0b1220;
|
||||
color: #dbeafe;
|
||||
min-height: 220px;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
overflow: auto;
|
||||
max-height: 320px;
|
||||
max-width: 100%;
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.result-list {
|
||||
margin-top: 8px;
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.result-item {
|
||||
border: 1px solid #dbe3f0;
|
||||
border-left-width: 4px;
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.result-ok {
|
||||
border-left-color: #16a34a;
|
||||
background: #f5fff9;
|
||||
}
|
||||
|
||||
.result-err {
|
||||
border-left-color: #dc2626;
|
||||
background: #fff7f7;
|
||||
}
|
||||
|
||||
.result-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.result-head strong {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.result-meta {
|
||||
color: #4b5563;
|
||||
margin-top: 4px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.result-meta.err {
|
||||
color: #b91c1c;
|
||||
}
|
||||
|
||||
.open-dir-btn {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.settings-sheet {
|
||||
padding: 12px 14px 14px;
|
||||
}
|
||||
|
||||
.settings-sheet h4 {
|
||||
margin: 12px 0 6px;
|
||||
}
|
||||
|
||||
.settings-sheet summary {
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.settings-inner {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.inline-checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 4px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.inline-checkbox input {
|
||||
width: auto;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
small {
|
||||
display: block;
|
||||
margin-top: 6px;
|
||||
}
|
||||
Reference in New Issue
Block a user