Files

176 lines
8.4 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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 { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[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/&lt;skill_id&gt;/ 下完全独立(前端页面 / 渲染 / 样式 / 导出 / 数据各自一份)'
}
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>