Files
pangolin/docs/contact-telegram-channels-design.html
T
wangjia 91a890428f 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>
2026-07-02 12:23:02 +08:00

317 lines
19 KiB
HTML
Raw 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.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>