176 lines
8.4 KiB
HTML
176 lines
8.4 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="utf-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||
<title>游山玩水 · 技能列表</title>
|
||
<style>
|
||
/* 聚合页样式:只服务「技能列表」这一件事,不含任何业务表单/报告样式 */
|
||
:root {
|
||
--tc-bg:#f6f7f9; --tc-card:#fff; --tc-line:#e5e7eb; --tc-text:#1f2328;
|
||
--tc-sub:#6b7280; --tc-accent:#2f6fed;
|
||
}
|
||
* { box-sizing: border-box; }
|
||
body { margin:0; background:var(--tc-bg); color:var(--tc-text);
|
||
font:14px/1.6 -apple-system,"Segoe UI","Microsoft YaHei",sans-serif; }
|
||
.tc-page { max-width:1080px; margin:0 auto; padding:24px 16px 72px; }
|
||
.tc-top { display:flex; align-items:baseline; gap:10px; flex-wrap:wrap; margin-bottom:6px; }
|
||
.tc-top h1 { font-size:22px; margin:0; }
|
||
.tc-sub { font-size:12px; color:var(--tc-sub); }
|
||
.tc-meta { font-size:12px; color:var(--tc-sub); background:#fff; border:1px solid var(--tc-line);
|
||
border-radius:8px; padding:8px 12px; margin:12px 0 18px; }
|
||
|
||
.tc-note { border-radius:8px; padding:10px 12px; font-size:13px; border:1px solid; margin-bottom:12px; }
|
||
.tc-note-info { background:#f2f7ff; border-color:#cfe0ff; }
|
||
.tc-note-warn { background:#fffaf0; border-color:#f0dcae; }
|
||
.tc-note-danger { background:#fdece8; border-color:#f3c0b6; }
|
||
.tc-note b { display:block; margin-bottom:2px; }
|
||
.tc-note ol { margin:6px 0 0; padding-left:20px; }
|
||
.tc-note ol li { margin-bottom:3px; }
|
||
|
||
.tc-btn { display:inline-block; text-decoration:none; font-size:13px; border:none; cursor:pointer;
|
||
background:var(--tc-accent); color:#fff; border-radius:8px; padding:9px 18px; font-family:inherit; }
|
||
.tc-btn:hover { background:#2560d8; }
|
||
.tc-btn-ghost { background:#fff; color:var(--tc-accent); border:1px solid #cfdcff; }
|
||
.tc-btn-ghost:hover { background:#f0f4ff; }
|
||
|
||
.tc-skills { display:grid; grid-template-columns:repeat(auto-fill,minmax(300px,1fr)); gap:12px; }
|
||
.tc-skill { display:block; text-decoration:none; color:inherit; border:1px solid var(--tc-line);
|
||
border-radius:12px; padding:16px; background:#fff; transition:border-color .15s, box-shadow .15s, transform .15s; }
|
||
.tc-skill:hover { border-color:#b9cbff; box-shadow:0 8px 22px rgba(16,24,40,.08); transform:translateY(-1px); }
|
||
.tc-skill.tc-off { opacity:.62; cursor:not-allowed; border-style:dashed; }
|
||
.tc-skill.tc-off:hover { border-color:var(--tc-line); box-shadow:none; transform:none; }
|
||
.tc-skill-head { display:flex; align-items:flex-start; gap:10px; }
|
||
.tc-skill-icon { flex:none; width:34px; height:34px; display:flex; align-items:center; justify-content:center;
|
||
font-size:20px; line-height:1; background:#f2f6ff; border-radius:9px; }
|
||
.tc-skill-icon img { width:22px; height:22px; display:block; }
|
||
.tc-skill-name { font-size:15px; font-weight:600; margin:2px 0 0; }
|
||
.tc-skill-desc { font-size:12.5px; color:#374151; margin-top:8px; line-height:1.6; }
|
||
.tc-skill-foot { display:flex; flex-wrap:wrap; gap:5px; margin-top:10px; }
|
||
.tc-tag { font-size:11px; background:#eef2f7; color:#4b5563; border-radius:999px; padding:1px 8px; }
|
||
.tc-tag-go { background:#e8f0fe; color:#1d4ed8; }
|
||
.tc-tag-bad { background:#fdece8; color:#b42318; }
|
||
.tc-empty { color:var(--tc-sub); font-size:13px; padding:14px 0; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="tc-page">
|
||
<div class="tc-top">
|
||
<h1>游山玩水</h1>
|
||
<span class="tc-sub">思想实验室 · 选择一个技能进入(每个技能是完全独立的一套行程打法)</span>
|
||
</div>
|
||
<div class="tc-meta" id="tc_meta">正在读取技能列表…</div>
|
||
|
||
<div class="tc-note tc-note-danger" id="tc_auth_note" style="display:none">
|
||
<b>缺少导航通行凭证(nav_gate),接口被拒绝</b>
|
||
本工具的<b>页面</b>允许同源导航进入,但<b>接口</b>只认 nav_gate cookie,所以会出现「页面能打开、接口全是 401」。按下面做一次即可恢复:
|
||
<ol>
|
||
<li>从<b>首页九宫格</b>点「游山玩水」卡片进来(必须经过首页,它才会种 cookie)</li>
|
||
<li>全程用<b>同一个地址</b>:<code>localhost</code> 与 <code>127.0.0.1</code> 的 cookie 不通用,中途切换域名就会失联</li>
|
||
<li>nav_gate 是<b>会话级 cookie</b>,关闭浏览器后失效,需要重新从首页进一次</li>
|
||
</ol>
|
||
<div style="margin-top:10px">
|
||
<a class="tc-btn" href="/">重新从首页进入</a>
|
||
<button class="tc-btn tc-btn-ghost" id="tc_auth_retry">我已重新进入,重试</button>
|
||
</div>
|
||
<div id="tc_auth_diag"></div>
|
||
</div>
|
||
|
||
<div class="tc-skills" id="tc_skills"><div class="tc-empty">加载中…</div></div>
|
||
|
||
|
||
</div>
|
||
|
||
<script>
|
||
(function () {
|
||
'use strict'
|
||
var API = '/api/thought_lab/travel_citytour'
|
||
var $ = function (id) { return document.getElementById(id) }
|
||
|
||
function esc(s) {
|
||
return String(s === undefined || s === null ? '' : s)
|
||
.replace(/[&<>"']/g, function (c) { return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c] })
|
||
}
|
||
|
||
function isUnauthorized(err) { return /unauthorized|401/i.test(String((err && err.message) || '')) }
|
||
|
||
function showAuthNote() {
|
||
$('tc_auth_note').style.display = ''
|
||
// lab 级公开诊断接口(注册在鉴权守卫之前),只回传请求方自己已有的信息
|
||
fetch(API + '/auth/diag?t=' + Date.now(), { cache: 'no-store' })
|
||
.then(function (r) { return r.json() })
|
||
.then(function (d) {
|
||
$('tc_auth_diag').innerHTML =
|
||
'<div style="font-size:12px;font-weight:600;margin:12px 0 6px;color:#374151">当前请求诊断(公开接口,不含任何 cookie 值)</div>' +
|
||
'<pre style="background:#f7f8fa;border:1px solid #e5e7eb;border-radius:8px;padding:10px;' +
|
||
'font:12px/1.5 Consolas,Monaco,monospace;white-space:pre-wrap;word-break:break-all;margin:0;max-height:280px;overflow:auto">' +
|
||
esc(JSON.stringify(d, null, 2)) + '</pre>'
|
||
})
|
||
.catch(function () {})
|
||
}
|
||
|
||
function renderSkills(list) {
|
||
var box = $('tc_skills')
|
||
var usable = list.filter(function (s) { return !s.broken })
|
||
if (!list.length) {
|
||
box.innerHTML = '<div class="tc-empty">还没有任何技能。在后端 skills/ 与前端 skills/ 下各建同名目录即可(需含 config.json + index.js + index.html)。</div>'
|
||
return
|
||
}
|
||
box.innerHTML = ''
|
||
list.forEach(function (s) {
|
||
var broken = !!s.broken
|
||
var el = document.createElement(broken ? 'div' : 'a')
|
||
el.className = 'tc-skill' + (broken ? ' tc-off' : '')
|
||
if (!broken) el.href = s.page
|
||
el.title = s.desc || ''
|
||
var iconHtml = s.icon_url
|
||
? '<img src="' + esc(s.icon_url) + '" alt="" />'
|
||
: esc(s.icon || '🧭')
|
||
el.innerHTML =
|
||
'<div class="tc-skill-head">' +
|
||
'<div class="tc-skill-icon">' + iconHtml + '</div>' +
|
||
'<h3 class="tc-skill-name">' + esc(s.name) + '</h3>' +
|
||
'</div>' +
|
||
(s.desc ? '<div class="tc-skill-desc">' + esc(s.desc) + '</div>' : '') +
|
||
'<div class="tc-skill-foot">' +
|
||
(broken
|
||
? '<span class="tc-tag tc-tag-bad">不可用:' + esc(s.broken) + '</span>'
|
||
: '<span class="tc-tag tc-tag-go">进入 →</span>') +
|
||
'<span class="tc-tag">' + esc(s.id) + '</span>' +
|
||
'</div>'
|
||
box.appendChild(el)
|
||
})
|
||
$('tc_meta').innerHTML =
|
||
'共 <b>' + list.length + '</b> 个技能' +
|
||
(usable.length !== list.length ? '(其中 ' + (list.length - usable.length) + ' 个不可用)' : '') +
|
||
' · 每个技能在 skills/<skill_id>/ 下完全独立(前端页面 / 渲染 / 样式 / 导出 / 数据各自一份)'
|
||
}
|
||
|
||
function load() {
|
||
return fetch(API + '/skills?t=' + Date.now(), { cache: 'no-store' })
|
||
.then(function (r) {
|
||
return r.text().then(function (t) {
|
||
var j = null
|
||
try { j = JSON.parse(t) } catch (e) {}
|
||
if (!r.ok) {
|
||
var err = new Error((j && (j.error || j.hint)) || ('HTTP ' + r.status))
|
||
throw err
|
||
}
|
||
if (!j) throw new Error('返回不是合法 JSON')
|
||
return j
|
||
})
|
||
})
|
||
.then(function (j) { renderSkills(j.skills || []) })
|
||
}
|
||
|
||
$('tc_auth_retry').addEventListener('click', function () { location.reload() })
|
||
|
||
load().catch(function (e) {
|
||
if (isUnauthorized(e)) showAuthNote()
|
||
$('tc_meta').textContent = '读取失败:' + (e && e.message || '')
|
||
$('tc_skills').innerHTML = '<div class="tc-empty">技能列表加载失败</div>'
|
||
})
|
||
})()
|
||
</script>
|
||
</body>
|
||
</html> |