/* backend-page.jsx — 后端实施手册（可浏览版） */

function BCode({ children, cap }) {
  return (
    <div style={{ margin: '10px 0' }}>
      <pre style={{
        margin: 0, background: 'var(--ink)', color: '#dbe8e6', padding: '16px 20px', borderRadius: 4,
        fontFamily: 'var(--f-mono)', fontSize: 11.5, lineHeight: 1.8, overflowX: 'auto', whiteSpace: 'pre'
      }}>{children}</pre>
      {cap && <div style={{ fontSize: 11, color: 'var(--txt-3)', marginTop: 6, fontFamily: 'var(--f-mono)' }}>{cap}</div>}
    </div>
  );
}

function BStep({ n, t, d, items, done }) {
  return (
    <div style={{ display: 'grid', gridTemplateColumns: '56px minmax(0,1fr)', gap: 20, padding: '24px 0', borderTop: '1px solid var(--line)' }}>
      <div>
        <div style={{ fontFamily: 'var(--f-serif)', fontSize: 34, lineHeight: 1, color: done ? 'var(--teal-500)' : 'var(--gold)', letterSpacing: '-.03em' }}>{n}</div>
        <div style={{ fontFamily: 'var(--f-mono)', fontSize: 9.5, letterSpacing: '.14em', color: 'var(--txt-3)', textTransform: 'uppercase', marginTop: 6 }}>{done || 'week'}</div>
      </div>
      <div>
        <h3 style={{ fontSize: 19, fontFamily: 'var(--f-sans)', fontWeight: 500 }}>{t}</h3>
        <p style={{ marginTop: 8, fontSize: 14, lineHeight: 1.8, color: 'var(--txt-2)', fontWeight: 300 }}>{d}</p>
        <ul style={{ marginTop: 12, display: 'grid', gap: 6 }}>
          {items.map((x) => (
            <li key={x} style={{ display: 'flex', gap: 9, fontSize: 13.5, color: 'var(--txt-2)', lineHeight: 1.7 }}>
              <span style={{ color: 'var(--teal-500)', flex: 'none' }}>·</span>{x}
            </li>
          ))}
        </ul>
      </div>
    </div>
  );
}

function BackendPage() {
  const api = [
    ['GET', '/api/nav', '导航'],
    ['GET', '/api/courses', '三级课程 + 价格 + 套餐'],
    ['GET', '/api/events?state=upcoming', '即将到来'],
    ['GET', '/api/events?state=past', '往期（分页）'],
    ['GET', '/api/events/:slug/photos', '活动相册'],
    ['GET', '/api/posts?cat=', '文章列表'],
    ['GET', '/api/posts/:slug', '文章详情'],
    ['GET', '/api/voices', '17 条证言'],
    ['GET', '/api/books', '20 本书'],
    ['GET', '/api/videos', '视频片单'],
    ['POST', '/api/bookings', '预约表单提交'],
    ['POST', '/api/enrollments', '课程报名下单'],
    ['POST', '/api/admin/posts', '后台：内容编辑'],
    ['POST', '/api/admin/events/sync-scan', '后台：扫公众号'],
    ['POST', '/api/auth/qr', '带参二维码登录'],
  ];

  return (
    <>
      <V5Styles />
      <section className="sec-tight" style={{ paddingTop: 'clamp(40px,4.8vw,72px)' }}>
        <div className="wrap">
          <Eyebrow>工程 · 后端实施</Eyebrow>
          <h1 className="d1" style={{ marginTop: 18, maxWidth: '18ch' }}>前端已经就绪，<br />接下来是后端。</h1>
          <p className="lede" style={{ marginTop: 22, maxWidth: '62ch' }}>
            这套前台已经把“接口该返回什么”写死了——所有内容目前在 <code style={{ fontFamily: 'var(--f-mono)', fontSize: 14 }}>data.js</code> 与
            <code style={{ fontFamily: 'var(--f-mono)', fontSize: 14 }}> content*.js</code> 里。
            后端要做的第一步不是写业务，而是把这些 JSON 搬进数据库、用接口暴露出来。
          </p>
          <div style={{ marginTop: 26, display: 'flex', gap: 10, flexWrap: 'wrap' }}>
            <a className="btn btn-primary btn-sm" href="backend-plan.md">下载完整手册（Markdown）</a>
            <button className="btn btn-ghost btn-sm" onClick={() => window.__go('arch')}>看架构方案 →</button>
          </div>
        </div>
      </section>

      {/* 一句话结论 */}
      <section className="sec" style={{ background: 'var(--ink)', color: 'var(--on-dark)' }}>
        <div className="wrap" style={{ maxWidth: 900 }}>
          <div className="eyebrow on-dark" style={{ display: 'inline-flex' }}>结论</div>
          <p className="d3" style={{ marginTop: 18, color: '#fff', lineHeight: 1.6, fontFamily: 'var(--f-serif)' }}>
            做「一个内容接口 + 一个管理后台 + 三个外部集成」，
            <span style={{ color: 'var(--gold)' }}>不要</span>做一整套 CMS。
          </p>
          <p style={{ marginTop: 18, fontSize: 15, lineHeight: 1.9, color: 'rgba(238,244,243,.72)', fontWeight: 300 }}>
            第一周把静态前台跑成动态站；第二周让运营能不发电就改内容；第三周接预约与支付；
            之后才是公众号同步与活动抓取。每一步都能独立上线、独立回退。
          </p>
        </div>
      </section>

      {/* 接口契约 */}
      <section className="sec">
        <div className="wrap">
          <SecHead eyebrow="第一步" title="照抄现有的数据契约"
            sub="前台已经按接口的样子写好了。字段名照搬，前台改造成本最低——只需把 QD.x 换成 fetch。" />
          <div style={{ border: '1px solid var(--line)' }}>
            {api.map(([m, u, d], i) => (
              <div key={u} style={{
                display: 'grid', gridTemplateColumns: '54px minmax(0,1.2fr) minmax(0,1fr)', gap: 16, alignItems: 'center',
                padding: '12px 16px', borderTop: i ? '1px solid var(--line-soft)' : 0
              }}>
                <span style={{
                  fontFamily: 'var(--f-mono)', fontSize: 10.5, letterSpacing: '.08em', textAlign: 'center', padding: '2px 0',
                  background: m === 'GET' ? 'rgba(1,153,154,.1)' : 'rgba(192,154,94,.16)',
                  color: m === 'GET' ? 'var(--teal-700)' : 'var(--gold-deep)'
                }}>{m}</span>
                <code style={{ fontFamily: 'var(--f-mono)', fontSize: 12.5, color: 'var(--ink)', overflowWrap: 'anywhere' }}>{u}</code>
                <span style={{ fontSize: 13, color: 'var(--txt-2)' }}>{d}</span>
              </div>
            ))}
          </div>
          <p className="form-note" style={{ marginTop: 14 }}>
            价格一律用「分」的整数传输，展示文案由后端生成 —— 改价只改数据库一处，前台不允许硬编码金额。
          </p>
        </div>
      </section>

      {/* 数据模型 */}
      <section className="sec" style={{ background: 'var(--cloud-2)' }}>
        <div className="wrap">
          <SecHead eyebrow="第二步" title="最小可用的数据模型"
            sub="只建必须的表。活动与往期活动是同一张表，靠 state 区分——「到期转往期」只是一个定时任务。" />
          <div className="grid g2" style={{ alignItems: 'start' }}>
            <div>
              <BCode cap="核心内容表（节选）">{`create table courses (
  id bigserial primary key,
  slug text unique not null,
  level text not null,          -- l1 | l2 | l3
  status text default 'draft',  -- open | locked | soon
  price_cents int,              -- null = 待定
  bundle_price_cents int,       -- 职业套餐
  points jsonb default '[]',
  sort int default 0
);

create table events (
  id bigserial primary key,
  kind text not null,           -- workshop | gathering | public
  state text not null,          -- upcoming | past | archived
  start_date date not null,
  city text, venue text,
  seats int, seats_left int,
  photos jsonb default '[]',    -- 活动后可补传
  source_url text,              -- 公众号原文
  wx_article_id text,           -- 幂等键
  published_at timestamptz
);
create index on events (state, start_date desc);`}</BCode>
            </div>
            <div>
              <BCode cap="内容与运营表（节选）">{`create table posts (
  slug text unique not null,
  wx_article_id text unique,
  title text not null,
  body jsonb not null,          -- 块结构正文
  category text not null,
  status text default 'draft',  -- draft|review|published
  source_url text,
  published_at timestamptz
);

create table admins (
  email text unique not null,
  password_hash text not null,  -- argon2id
  role text default 'editor',   -- owner|admin|editor
  totp_secret text
);

create table audit_logs (        -- 一切写操作留痕
  actor_id bigint, action text,
  target_table text, target_id bigint,
  before jsonb, after jsonb, ip inet,
  created_at timestamptz default now()
);`}</BCode>
            </div>
          </div>
        </div>
      </section>

      {/* 三个集成 */}
      <section className="sec">
        <div className="wrap">
          <SecHead eyebrow="第三步" title="三个外部集成" />
          <div className="grid g3">
            {[
              ['公众号文章同步', 'webhook 验签 → 抓取 → 清洗 → 图片转存 → 入库草稿 → 后台待审。用 wx_article_id 做幂等，微信会重试推送。'],
              ['活动 AI 抽取', '命中关键词入队 → 抓正文 → LLM 输出严格 JSON（含置信度）→ 运营核对上架。低置信字段前台显示「待公布」，不要猜。'],
              ['关注即登录', '订阅号没有 OAuth。带参二维码 + subscribe 事件 + 轮询命中，二维码 5 分钟过期要能自动刷新。'],
            ].map(([t, d], i) => (
              <div key={t} className="card" style={{ height: '100%' }}>
                <div className="num">0{i + 1}</div>
                <h3 style={{ marginTop: 12, fontSize: 18 }}>{t}</h3>
                <p style={{ marginTop: 10, fontSize: 13.5, lineHeight: 1.8, color: 'var(--txt-2)', fontWeight: 300 }}>{d}</p>
              </div>
            ))}
          </div>

          <div style={{ marginTop: 26 }}>
            <BCode cap="活动抓取：从推文到上架">{`① 定时 / 手动触发 → 拉取最近推文
② 标题摘要命中 [报名|排期|工作坊|公开课|督导] → 入队
③ 抓正文 → 清洗 → 图片转存对象存储
④ LLM 输出严格 JSON（function calling / JSON Schema）：
   { title, startDate, endDate, city, venue, seats,
     feeText, deadline, points[3], confidence{} }
⑤ 写 events（state=upcoming），运营逐字段核对
⑥ 确认上架 → published_at
⑦ 定时任务：start_date 过后自动 state=past`}</BCode>
          </div>
        </div>
      </section>

      {/* 后台 */}
      <section className="sec" style={{ background: 'var(--cloud-2)' }}>
        <div className="wrap">
          <div className="split" style={{ alignItems: 'start' }}>
            <div>
              <Eyebrow>省掉两周</Eyebrow>
              <h2 className="d2" style={{ marginTop: 16 }}>后台界面<br />已经画好了。</h2>
              <p className="lede" style={{ marginTop: 18 }}>
                项目里的 <code style={{ fontFamily: 'var(--f-mono)', fontSize: 14 }}>admin.jsx</code> 就是给后端用的界面规格：
                看板、文章同步、活动抓取、内容库、课程报名、名录、会员、版本、权限。
              </p>
              <div style={{ marginTop: 22 }}><Btn kind="primary" onClick={() => {
                const b = [...document.querySelectorAll('button')].find((x) => x.textContent === '运营后台');
                if (b) b.click();
              }} arw>打开运营后台原型</Btn></div>
            </div>
            <ul className="nlist">
              {[
                ['把假数据换成 fetch', 'SYNC_JOBS / ACT_QUEUE / ENROLL 这些常量替换成接口调用。'],
                ['把 flash() 换成真实请求', '每个「已发布」提示背后要有一次真实的 PATCH。'],
                ['上传直传对象存储', '现场照片那块已经是 file input，接预签名地址即可。'],
              ].map(([a, b], i) => (
                <li key={a}><span className="k">0{i + 1}</span><div><h4>{a}</h4><p>{b}</p></div></li>
              ))}
            </ul>
          </div>
        </div>
      </section>

      {/* 路线 */}
      <section className="sec">
        <div className="wrap">
          <SecHead eyebrow="实施顺序" title="六周，每周都能上线" />
          <BStep n="01" done="第 1 周" t="只读 API：把静态站变成动态站"
            d="建库建表、写一次性 seed 脚本把现有内容导进去、实现全部 GET 接口；前台把 QD.x 换成 fetch，页面外观完全不变。"
            items={['15 场活动 / 35 篇文章 / 17 条证言 / 20 本书 / 6 支视频全部入库', '验收：断网后端，前台仍能靠构建时快照打开']} />
          <BStep n="02" done="第 2 周" t="后台登录 + 内容编辑"
            d="管理员账号、argon2id、HttpOnly Cookie、CSRF；内容 CRUD 与版本快照；图片直传与三档尺寸。"
            items={['验收：运营不发版就能改一条证言、改一个价格']} />
          <BStep n="03" done="第 3 周" t="预约与报名写库"
            d="预约表单（前台已就绪）、课程下单与支付回调、后台工单列表与状态流转。"
            items={['验收：真实提交一条预约，后台能看到并回复', '支付优先接现有课程平台，别急着自建微信支付']} />
          <BStep n="04" done="第 4 周" t="公众号文章同步"
            d="webhook 验签与幂等、抓取清洗、图片转存、入库草稿；后台「文章同步」页对接。"
            items={['验收：发一篇推文，十分钟内出现在后台待审']} />
          <BStep n="05" done="第 5 周" t="活动抓取 + AI 抽取"
            d="关键词规则命中入队、LLM 严格 JSON 抽取、后台核对上架、现场照片补传、过期自动转往期。"
            items={['验收：一篇推文 → 一条可上架活动，人工只改两个字段']} />
          <BStep n="06" done="第 6 周" t="关注即登录 + 上线加固"
            d="带参二维码 + 轮询 + subscribe 回调；限流、审计、备份；灰度先切只读再切写入。"
            items={['验收：扫码关注后，收藏与报名能关联到同一个人']} />
        </div>
      </section>

      {/* 底线 */}
      <section className="sec" style={{ background: 'var(--ink)', color: 'var(--on-dark)' }}>
        <div className="wrap">
          <div className="eyebrow on-dark" style={{ display: 'inline-flex' }}>三条底线</div>
          <div className="grid g3" style={{ marginTop: 26 }}>
            {[
              ['前台永远不白屏', '所有 GET 接口在构建时预渲染快照；运行时接口挂了回退快照。这是老站留下的教训。'],
              ['写操作全留痕', 'audit_logs 记录 who / what / before / after。内容错了能回滚，人传错了能查到。'],
              ['价格只有一处真相', '前端不允许硬编码金额。content-v3.js 里的 12800 必须删掉，改由接口下发。'],
            ].map(([t, d], i) => (
              <div key={t} style={{ paddingTop: 18, borderTop: '1px solid rgba(238,244,243,.16)' }}>
                <div style={{ fontFamily: 'var(--f-mono)', fontSize: 10, letterSpacing: '.16em', color: 'var(--gold)' }}>0{i + 1}</div>
                <h4 style={{ marginTop: 10, fontSize: 18, color: '#fff', fontFamily: 'var(--f-sans)', fontWeight: 500 }}>{t}</h4>
                <p style={{ marginTop: 8, fontSize: 13.5, lineHeight: 1.8, color: 'rgba(238,244,243,.66)', fontWeight: 300 }}>{d}</p>
              </div>
            ))}
          </div>
        </div>
      </section>

      {/* 上线前必换 */}
      <section className="sec">
        <div className="wrap">
          <SecHead eyebrow="上线前" title="必须先替换掉的占位数据"
            sub="这些留在库里会误导客户，建议一开始就做成后台可配置，而不是写死在代码里。" />
          <div style={{ border: '1px solid var(--line)' }}>
            {[
              ['3 场「即将到来」的活动', '2026 春季班 / 杭州督导日 / 线上公开课 —— 日期与人名均为示例', 'events'],
              ['客服微信 / 付款链接 / 公众号二维码', 'qhht_service、#pay 都是占位', 'site_settings'],
              ['两张 AI 生成的人物照', 'assets/portrait-teacher*.jpg 不是真实学员，对外宣传需替换为有授权照片', 'media'],
            ].map(([a, b, c], i) => (
              <div key={a} style={{ display: 'grid', gridTemplateColumns: 'minmax(0,1fr) auto', gap: 16, padding: '14px 16px', borderTop: i ? '1px solid var(--line-soft)' : 0 }}>
                <div>
                  <div style={{ fontSize: 14.5 }}>{a}</div>
                  <div style={{ fontSize: 13, color: 'var(--txt-3)', marginTop: 4, lineHeight: 1.6 }}>{b}</div>
                </div>
                <code style={{ fontFamily: 'var(--f-mono)', fontSize: 11.5, color: 'var(--gold-deep)', alignSelf: 'center' }}>{c}</code>
              </div>
            ))}
          </div>
        </div>
      </section>
    </>
  );
}

Object.assign(window, { BackendPage, BCode, BStep });
