docs: 联系我们渠道二级页交互设计(Telegram/LINE 多频道群组,DB 配置)

点渠道进二级页,按 kind 分组(频道/群组/Bot)列多条链接,内容 DB 配置。
含可点原型(App 真实暗色 token)+ 单表 contact_link 数据模型 + GET /v1/contact
接口 + 交互规则(平台 >1 条链接才进二级,否则点击直达)。#24,待统一后开发。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-07-02 12:23:02 +08:00
parent 2c804d4ebf
commit 91a890428f
2 changed files with 321 additions and 0 deletions
+316
View File
@@ -0,0 +1,316 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>联系我们:渠道二级页(Telegram 频道/群组,DB 配置)· 交互设计</title>
<style>
:root{
--bg:#0f1117; --panel:#171a22; --panel2:#1d2129; --fg:#e6e8ee; --fg2:#a8afbd;
--accent:#e0884f; --accent2:#5fb0c9; --ok:#5ec27a; --bad:#e06a6a; --warn:#e0b84f;
--border:#272c36; --mono:"SF Mono",ui-monospace,Menlo,Consolas,monospace;
--sans:-apple-system,"PingFang SC","Helvetica Neue",Arial,sans-serif;
}
*{box-sizing:border-box}
body{margin:0;background:var(--bg);color:var(--fg);font-family:var(--sans);line-height:1.7;font-size:15px}
.wrap{max-width:960px;margin:0 auto;padding:48px 24px 96px}
h1{font-size:30px;line-height:1.3;margin:0 0 8px;letter-spacing:-.01em}
.sub{color:var(--fg2);font-size:15px;margin:0 0 32px}
h2{font-size:21px;margin:44px 0 14px;padding-bottom:8px;border-bottom:1px solid var(--border)}
h3{font-size:16px;margin:26px 0 8px;color:var(--accent2)}
p{margin:10px 0}
code{font-family:var(--mono);font-size:.88em;background:var(--panel2);padding:1px 6px;border-radius:5px;color:#f0d9c4}
pre{background:#0a0c11;border:1px solid var(--border);border-radius:10px;padding:14px 16px;overflow-x:auto;font-family:var(--mono);font-size:13px;line-height:1.55;color:#cdd3df}
.tag{display:inline-block;font-size:12px;font-weight:600;padding:2px 9px;border-radius:999px;vertical-align:middle}
.tag.ok{background:rgba(94,194,122,.16);color:var(--ok)}
.tag.warn{background:rgba(224,184,79,.16);color:var(--warn)}
.tag.info{background:rgba(95,176,201,.16);color:var(--accent2)}
.card{background:var(--panel);border:1px solid var(--border);border-radius:12px;padding:18px 20px;margin:16px 0}
.card.root{border-left:3px solid var(--accent)}
.card h3{margin-top:0}
table{width:100%;border-collapse:collapse;margin:16px 0;font-size:14px}
th,td{text-align:left;padding:9px 12px;border-bottom:1px solid var(--border);vertical-align:top}
th{color:var(--fg2);font-weight:600;font-size:13px}
td code{font-size:.85em}
ul,ol{padding-left:22px;margin:10px 0}
li{margin:5px 0}
.lead{background:linear-gradient(180deg,rgba(224,136,79,.10),transparent);border:1px solid var(--border);border-radius:12px;padding:18px 20px;margin:0 0 8px}
.small{color:var(--fg2);font-size:13px}
a{color:var(--accent2)}
.back{display:inline-block;margin-bottom:24px;font-size:13px}
b{color:#fff}
/* ── 交互原型:用真实 App 暗色 token(design/colors_and_type.css [data-theme=dark])──*/
.proto{
--p-bg:#14110E; --p-bg-subtle:#1F1C18; --p-surface:#221E19; --p-surface2:#2A251F;
--p-fg1:#F4EFE8; --p-fg2:#B6AC9C; --p-fg3:#897F6F; --p-fg-on-accent:#1F1C18;
--p-accent:#CC8B5C; --p-accent-hover:#D9A982; --p-accent-subtle:rgba(204,139,92,.14);
--p-accent-border:rgba(204,139,92,.30);
--p-border:rgba(242,238,231,.10); --p-border-strong:rgba(242,238,231,.18);
--p-green:#7FB07A;
background:var(--p-bg);border:1px solid var(--border);border-radius:14px;overflow:hidden;
display:flex;height:520px;margin:18px 0;font-size:14px;
}
.proto *{box-sizing:border-box}
/* sidebar */
.p-side{width:190px;flex:none;background:var(--p-bg);border-right:1px solid var(--p-border);padding:18px 14px;display:flex;flex-direction:column;gap:4px}
.p-brand{display:flex;align-items:center;gap:10px;padding:4px 8px 18px}
.p-logo{width:30px;height:30px;border-radius:8px;background:var(--p-accent-subtle);display:flex;align-items:center;justify-content:center;color:var(--p-accent);font-size:16px}
.p-brand b{color:var(--p-fg1);font-size:16px;line-height:1.1}
.p-brand span{display:block;color:var(--p-fg3);font-size:9px;letter-spacing:2px}
.p-nav{display:flex;align-items:center;gap:11px;padding:9px 12px;border-radius:9px;color:var(--p-fg2);cursor:default}
.p-nav .ic{width:17px;text-align:center;opacity:.85}
.p-nav.active{background:var(--p-accent-subtle);color:var(--p-accent);font-weight:600}
/* main */
.p-main{flex:1;display:flex;flex-direction:column;min-width:0}
.p-top{display:flex;align-items:center;gap:12px;padding:16px 22px;border-bottom:1px solid var(--p-border)}
.p-top .ttl{font-size:19px;font-weight:700;color:var(--p-fg1)}
.p-top .rt{margin-left:auto;display:flex;align-items:center;gap:14px;color:var(--p-fg2);font-size:12px}
.p-dot{width:7px;height:7px;border-radius:50%;background:var(--p-green);display:inline-block;margin-right:5px}
.p-body{padding:20px 22px;overflow-y:auto}
.p-intro{color:var(--p-fg2);font-size:13.5px;margin:0 0 14px}
/* channel / item cards */
.p-card{display:flex;align-items:center;gap:13px;padding:13px 15px;background:var(--p-surface);border:1px solid var(--p-border);border-radius:13px;margin-bottom:10px;cursor:pointer;transition:border-color .12s,background .12s}
.p-card:hover{border-color:var(--p-accent-border);background:var(--p-surface2)}
.p-ic{width:38px;height:38px;flex:none;border-radius:10px;background:var(--p-bg-subtle);display:flex;align-items:center;justify-content:center;font-size:18px;color:var(--p-fg2)}
.p-ic.accent{background:var(--p-accent-subtle);color:var(--p-accent)}
.p-tx{flex:1;min-width:0}
.p-tx .nm{color:var(--p-fg1);font-weight:600;font-size:14.5px;display:flex;align-items:center;gap:6px}
.p-tx .sb{color:var(--p-fg3);font-size:12.5px;margin-top:1px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.p-chev{color:var(--p-fg3);font-size:17px;flex:none}
.p-verify{color:var(--p-accent);font-size:12px}
.p-count{color:var(--p-fg3);font-size:11.5px;flex:none;display:flex;align-items:center;gap:4px}
.p-btn{flex:none;font-size:12.5px;font-weight:600;padding:6px 15px;border-radius:8px;border:1px solid var(--p-accent-border);color:var(--p-accent);background:transparent}
.p-btn.solid{background:var(--p-accent);color:var(--p-fg-on-accent);border-color:transparent}
.p-sechd{color:var(--p-fg3);font-size:11px;font-weight:700;letter-spacing:1.4px;margin:18px 2px 9px}
.p-back{display:inline-flex;align-items:center;gap:8px;color:var(--p-fg2);font-size:13px;cursor:pointer;padding:2px 0;margin-bottom:2px}
.p-back:hover{color:var(--p-fg1)}
.p-detail-hd{display:flex;align-items:center;gap:12px;margin:6px 0 4px}
.p-detail-hd .p-ic{width:44px;height:44px;font-size:21px}
.p-detail-hd .nm{font-size:18px;font-weight:700;color:var(--p-fg1)}
.p-detail-hd .sb{color:var(--p-fg3);font-size:12.5px}
.hide{display:none!important}
.flag{margin-left:auto;color:var(--p-fg3);font-size:12px}
.toggles{display:flex;gap:8px;margin:10px 0 2px}
.toggles button{background:var(--panel2);color:var(--fg2);border:1px solid var(--border);border-radius:8px;padding:6px 12px;font-size:13px;cursor:pointer}
.toggles button:hover{color:var(--fg)}
.capt{color:var(--fg2);font-size:12.5px;text-align:center;margin-top:-4px}
</style>
</head>
<body>
<div class="wrap">
<a class="back" href="index.html">← 返回文档索引</a>
<h1>联系我们 · 渠道二级页</h1>
<p class="sub">交互设计 · Telegram/LINE 多频道 · 群组 · 内容 DB 配置 · <span class="tag info">前端 + 后端 + 数据库</span></p>
<div class="lead">
现「联系我们」把每个渠道当<b>单一入口</b>Telegram 只挂一个 <code>@PangolinVPN_bot</code>)。
需求:点 Telegram 进入<b>二级页</b>,列出该平台下的<b>多个频道 / 群组 / Bot</b>
每项含名称、@handle、一句说明、可选成员数、认证标记与「打开 / 加入」动作,
<b>全部由数据库配置</b>(运营可随时增删改,客户端不写死)。本页给出交互原型 + 数据模型 + 接口,待你统一后再开发。
</div>
<h2>可点原型(点 Telegram 卡片进二级,← 返回)</h2>
<p class="small">下方为内嵌可交互原型,配色用 App 真实暗色 tokenclay/espresso)。点 <b>Telegram</b><b>LINE</b> 卡进详情;「邮箱客服 / 自助发卡商店」为单链接,点即直接打开(原型里仅提示)。</p>
<div class="proto" id="proto">
<!-- sidebar -->
<div class="p-side">
<div class="p-brand"><div class="p-logo">🐾</div><div><b>穿山甲</b><span>PANGOLIN</span></div></div>
<div class="p-nav"><span class="ic"></span>连接</div>
<div class="p-nav"><span class="ic">🌐</span>节点</div>
<div class="p-nav"><span class="ic">📊</span>统计</div>
<div class="p-nav"><span class="ic"></span>设置</div>
<div class="p-nav active"><span class="ic">💬</span>联系我们</div>
</div>
<!-- main -->
<div class="p-main">
<div class="p-top">
<span class="ttl" id="p-title">联系我们</span>
<span class="rt"><span><span class="p-dot"></span>US</span><span>🌙</span></span>
</div>
<!-- L1: channel list -->
<div class="p-body" id="view-l1">
<p class="p-intro">遇到问题?通过以下任一渠道联系我们,通常数分钟内回复。</p>
<div class="p-card" onclick="showTg()">
<div class="p-ic accent"></div>
<div class="p-tx"><div class="nm">Telegram</div><div class="sb">官方频道 · 交流群 · 客服 Bot</div></div>
<span class="p-chev"></span>
</div>
<div class="p-card" onclick="showLine()">
<div class="p-ic">💬</div>
<div class="p-tx"><div class="nm">LINE</div><div class="sb">官方账号 · 中文/日文群</div></div>
<span class="p-chev"></span>
</div>
<div class="p-card" title="单链接:直接打开邮件">
<div class="p-ic"></div>
<div class="p-tx"><div class="nm">邮箱客服</div><div class="sb">support@pangolin.vpn</div></div>
<span class="p-chev"></span>
</div>
<div class="p-card" title="单链接:直接打开商店">
<div class="p-ic">🛍</div>
<div class="p-tx"><div class="nm">自助发卡商店</div><div class="sb">shop.pangolin.vpn</div></div>
<span class="p-chev"></span>
</div>
</div>
<!-- L2: Telegram detail -->
<div class="p-body hide" id="view-tg">
<div class="p-back" onclick="showList()"> 返回</div>
<div class="p-detail-hd">
<div class="p-ic accent"></div>
<div><div class="nm">Telegram</div><div class="sb">加入官方频道获取更新,进群与用户互助</div></div>
</div>
<div class="p-sechd">频道 · CHANNELS</div>
<div class="p-card">
<div class="p-ic accent">📣</div>
<div class="p-tx"><div class="nm">穿山甲 · 官方频道 <span class="p-verify"></span></div><div class="sb">产品更新与公告 · @PangolinVPN</div></div>
<span class="p-count">👥 12.3k</span>
<button class="p-btn">打开</button>
</div>
<div class="p-card">
<div class="p-ic accent">📶</div>
<div class="p-tx"><div class="nm">穿山甲 · 节点状态</div><div class="sb">节点/故障实时播报 · @PangolinStatus</div></div>
<span class="p-count">👥 3.1k</span>
<button class="p-btn">打开</button>
</div>
<div class="p-sechd">群组 · GROUPS</div>
<div class="p-card">
<div class="p-ic">👥</div>
<div class="p-tx"><div class="nm">穿山甲 · 用户交流群</div><div class="sb">使用问题互助交流 · @PangolinChat</div></div>
<span class="p-count">👥 8.7k</span>
<button class="p-btn solid">加入</button>
</div>
<div class="p-card">
<div class="p-ic">🌏</div>
<div class="p-tx"><div class="nm">Pangolin · English Group</div><div class="sb">English support &amp; chat · @PangolinEN</div></div>
<span class="p-count">👥 1.2k</span>
<button class="p-btn solid">加入</button>
</div>
<div class="p-sechd">客服机器人 · BOT</div>
<div class="p-card">
<div class="p-ic accent">🤖</div>
<div class="p-tx"><div class="nm">客服机器人</div><div class="sb">自动答疑 / 提交工单 · @PangolinVPN_bot</div></div>
<button class="p-btn">打开</button>
</div>
</div>
<!-- L2: LINE detail (示意同构) -->
<div class="p-body hide" id="view-line">
<div class="p-back" onclick="showList()"> 返回</div>
<div class="p-detail-hd">
<div class="p-ic">💬</div>
<div><div class="nm">LINE</div><div class="sb">官方账号与交流群</div></div>
</div>
<div class="p-sechd">官方账号 · OFFICIAL</div>
<div class="p-card">
<div class="p-ic">💬</div>
<div class="p-tx"><div class="nm">Pangolin 官方账号 <span class="p-verify"></span></div><div class="sb">公告与客服 · @pangolinvpn</div></div>
<button class="p-btn">打开</button>
</div>
<div class="p-sechd">群组 · GROUPS</div>
<div class="p-card">
<div class="p-ic">👥</div>
<div class="p-tx"><div class="nm">中文交流群</div><div class="sb">使用互助 · openchat</div></div>
<button class="p-btn solid">加入</button>
</div>
</div>
</div>
</div>
<div class="toggles">
<button onclick="showList()">L1 渠道列表</button>
<button onclick="showTg()">L2 · Telegram</button>
<button onclick="showLine()">L2 · LINE</button>
</div>
<p class="capt">同一原型内切换三态 · 真机为路由 push(移动端整页)/ 内容区替换(桌面)</p>
<h2>交互规则</h2>
<ul>
<li><b>进入二级的条件</b>:某平台配置了 <b>&gt;1 条</b>链接 → 卡片带 <code></code>,点击进二级页(Telegram / LINE)。
<b>1 条</b>链接的平台(邮箱、发卡商店)→ 点击<b>直接执行该链接动作</b>(打开邮件 / 浏览器),不进二级。
规则统一由「该平台 link 条数」驱动,无需前端写死哪个进二级。</li>
<li><b>二级页结构</b>:顶部 <code> 返回</code> + 平台头(图标 + 名 + 一句副标题);下方按 <b>kind 分组</b>展示
——<code>频道 CHANNELS</code> / <code>群组 GROUPS</code> / <code>客服机器人 BOT</code>(分组标题仅在该组有内容时出现,顺序固定)。</li>
<li><b>每一项</b>:图标/头像 · 标题(可带 <span class="p-verify" style="color:var(--accent)"></span> 认证)· 一句说明 + <code>@handle</code> ·
可选成员数(<b>展示用缓存文本</b>,不实时拉 Telegram)· 动作按钮。</li>
<li><b>动作</b>:频道/Bot → 「打开」(描边按钮);群组 → 「加入」(实心强调按钮)。点按钮或点整行都触发。
优先 <code>tg://resolve?domain=…</code> 唤起已装 Telegram,失败回退 <code>https://t.me/…</code>(浏览器)。</li>
<li><b>桌面 vs 移动</b>:桌面在右侧内容区做<b>视图替换</b>(标题栏文案随之切到「Telegram」,← 返回回列表);
移动端为<b>整页 push 路由</b>,系统返回手势/返回键回列表。两端同一份数据与卡片组件。</li>
<li><b>空/禁用</b>:某平台所有 link <code>enabled=0</code> → L1 不展示该平台;二级页某分组为空 → 不渲染该分组标题。
全部为空的平台不出现在联系页。</li>
</ul>
<h2>数据模型(DB 配置)</h2>
<p>单表承载 L1 平台入口与 L2 子项:一行 = 一条可点链接。L1 由 <code>platform</code> 聚合而来,二级由 <code>kind</code> 分组。</p>
<pre>contact_link
─────────────────────────────────────────────────────────────
id INTEGER PK
platform TEXT -- telegram | line | email | store | ...(L1 分组键)
kind TEXT -- channel | group | bot | link(L2 分组键 & 动作样式)
title TEXT -- "穿山甲 · 官方频道"
handle TEXT -- "@PangolinVPN"(展示;可空)
url TEXT -- "https://t.me/PangolinVPN"(点击目标;App 侧优先转 tg://)
description TEXT -- 一句说明(可空)
member_hint TEXT -- "12.3k"(纯展示缓存,可空;不实时拉)
verified INTEGER -- 0/1 认证勾
sort_order INTEGER -- 组内排序
enabled INTEGER -- 0/1 下线开关
locale TEXT -- "zh"|"en"|NULL(全部) 可选按语言过滤
</pre>
<p class="small">L1 平台的<b>图标 / 显示名 / 强调色</b>由前端按 <code>platform</code> 枚举内置(Telegram=✈+强调、LINE=💬…),
不入库,避免图标资源塞数据库。库里只配<b>内容</b>。单链接平台(email/store)建议各配 1 行 <code>kind=link</code></p>
<h2>接口</h2>
<pre>GET /v1/contact # 无需登录亦可(客户端普通请求)
→ 200
{
"platforms": [
{ "platform":"telegram",
"links":[
{"kind":"channel","title":"穿山甲 · 官方频道","handle":"@PangolinVPN",
"url":"https://t.me/PangolinVPN","description":"产品更新与公告",
"member_hint":"12.3k","verified":true},
{"kind":"group","title":"穿山甲 · 用户交流群","handle":"@PangolinChat",
"url":"https://t.me/PangolinChat","description":"使用问题互助","member_hint":"8.7k"},
{"kind":"bot","title":"客服机器人","handle":"@PangolinVPN_bot",
"url":"https://t.me/PangolinVPN_bot","description":"自动答疑/提交工单"}
]},
{ "platform":"line", "links":[ ... ] },
{ "platform":"email", "links":[{"kind":"link","title":"邮箱客服","url":"mailto:support@pangolin.vpn"}] },
{ "platform":"store", "links":[{"kind":"link","title":"自助发卡商店","url":"https://shop.pangolin.vpn"}] }
]
}</pre>
<ul>
<li>后端按 <code>enabled=1</code> 过滤、按 <code>sort_order</code> 排序、按 <code>platform</code><code>kind</code> 分组返回;客户端只渲染。</li>
<li>客户端<b>缓存</b>上次结果(离线/弱网仍可展示),启动或进联系页时后台刷新。</li>
<li>迁移:<code>server/migrations/{mysql,sqlite}/</code> 各加建表 + 种子数据(把现有 4 渠道灌入,Telegram 先补真实频道/群)。</li>
</ul>
<h2>待你拍板</h2>
<ul>
<li><b>成员数</b>是否展示?(要真实数需定期抓取;建议先用 <code>member_hint</code> 手工填/留空)</li>
<li>群组动作叫「<b>加入</b>」还是统一「<b>打开</b>」?(原型里频道=打开、群组=加入)</li>
<li>LINE 是否同样做二级(配了多账号才需要),还是暂时仍单链接?</li>
<li>是否要<b>认证勾</b>与分组标题(频道/群组/Bot),还是拍平成一个列表?</li>
<li>接口走<b>独立 <code>/v1/contact</code></b> 还是并进现有配置引导(如 <code>/me</code> bootstrap)一起下发?</li>
</ul>
</div>
</body>
<script>
function _hideAll(){ ['view-l1','view-tg','view-line'].forEach(id=>document.getElementById(id).classList.add('hide')); }
function showList(){ _hideAll(); document.getElementById('view-l1').classList.remove('hide'); document.getElementById('p-title').textContent='联系我们'; }
function showTg(){ _hideAll(); document.getElementById('view-tg').classList.remove('hide'); document.getElementById('p-title').textContent='Telegram'; }
function showLine(){ _hideAll(); document.getElementById('view-line').classList.remove('hide'); document.getElementById('p-title').textContent='LINE'; }
</script>
</html>
+5
View File
@@ -44,6 +44,11 @@
</div>
<h2>设计方案 / Specs</h2>
<a class="doc" href="contact-telegram-channels-design.html">
<div class="t">联系我们 · 渠道二级页(Telegram 频道/群组,DB 配置)<span class="tag html">HTML</span></div>
<div class="d">点 Telegram/LINE 进二级页,按 kind 分组(频道/群组/Bot)列出多条链接,含 @handle/说明/成员数/认证/「打开·加入」动作,全部 DB 配置。含可点原型 + 单表 contact_link 数据模型 + GET /v1/contact 接口。规则:平台 >1 条链接才进二级,否则点击直达。</div>
<div class="path">docs/contact-telegram-channels-design.html</div>
</a>
<a class="doc" href="free-quota-ad.html">
<div class="t">免费版 10 分钟卡控 + 累加式看广告加时 <span class="tag html">HTML</span></div>
<div class="d">免费版真卡控(账户级/全设备共享):连接期倒计时 + 到点自动切断 + 耗尽按钮灰化 + 点击弹广告看完 +10 分钟(累加,每日封顶 120)。桌面硬 10 分钟不可延。服务端 ad_bonus_minutes 累加模型 + ConnectNode 按 remaining 卡控 + TTL 硬切断;占位 DevVerifier。#21。</div>