diff --git a/website/README.md b/website/README.md
new file mode 100644
index 0000000..883dce1
--- /dev/null
+++ b/website/README.md
@@ -0,0 +1,43 @@
+# OPC 培训站(website)
+
+> 📍 现位于 `code/training/website/`(业务 Web 前端,原 `code/opc-training/website/`)。课程内容见 `materials/培训计划/`;关联小程序 `code/training/miniprogram/`;**后端已并入 `code/server-core/app/training/`**(`/api/*` 由 server-core `dispatcher.py` 对外提供)。
+
+课程体系 0.1 的对外展示与内部运营站。React + Vite SPA,暗色粒子品牌语言。
+
+## 运行方式
+
+```bash
+npm install
+npm run dev # 开发(含 /api 模拟认证)
+npm run build # 构建 dist
+npm run preview # 预览构建产物(含 /api 模拟认证)
+node server/start.mjs # 独立服务:托管 dist + /api(默认 8091,生产/演示用)
+```
+
+## 路由与权限
+
+| 路由 | 内容 | 权限 |
+|------|------|------|
+| `#/` | 首页营销落地页 | 公开 |
+| `#/opc-test` | OPC 创业基因测评 | 公开 |
+| `#/pine` 及 `#/pine/*` | 内部工具(课程体系/结构/课程表/卡片/工具) | 需登录 |
+
+**内部工具登录**:访问 `#/pine/*` 时路由守卫触发登录门。账号 `pine` / 密码 `123456`(**模拟**,见下)。
+
+内部工具**不进公开导航**,仅通过 URL 直达;公开导航只含「首页 / OPC测评」。
+
+## 模拟认证与迁移
+
+- 模拟接口:`server/mock-api.mjs`(`POST /api/auth/login`、`POST /api/auth/logout`、`GET /api/auth/verify`),账号密码暂时写死 `pine/123456`。
+- 已挂载到 Vite dev / preview / 独立服务三处。
+- **迁移为真实 API**:只需改 `src/services/auth.js` 顶部的 `API_BASE` 指向真实后端,并替换 `server/` 下的 mock 实现即可;SPA 其余代码无需改动。
+
+## 目录速览
+
+```
+server/ 模拟认证 API + 独立启动服务
+src/pages/ 页面(Home/OPCTest 公开;System/Structures/Schedule/Cards/Tools 内部)
+src/data/ 内容数据(home/system/structures/schedule/cards/timetable/opcTest/site)
+src/components/ 原子/分子/组织/模板组件
+src/services/ auth 认证服务
+```
diff --git a/website/_ssr2.mjs b/website/_ssr2.mjs
new file mode 100644
index 0000000..15f7859
--- /dev/null
+++ b/website/_ssr2.mjs
@@ -0,0 +1,16 @@
+import { createServer } from 'vite';
+import React from 'react';
+import { renderToString } from 'react-dom/server';
+globalThis.window = globalThis.window || { matchMedia: () => ({ matches: false }), innerWidth: 1024, scrollTo() {} };
+globalThis.document = globalThis.document || { getElementById: () => null, querySelector: () => null };
+const server = await createServer({ server: { middlewareMode: true }, appType: 'custom', logLevel: 'error' });
+const results = [];
+try {
+ const home = await server.ssrLoadModule('/src/pages/Home.jsx');
+ const h = renderToString(React.createElement(home.default));
+ results.push(['Home(公开)渲染', h.includes('ONE PERSON') && h.includes('DESIGNED TO EVOLVE') && h.includes('滚动开班') && h.includes('先测评,再报名')]);
+ results.push(['Home 不再指向内部页', !h.includes('#/system') && !h.includes('#/tools') && !h.includes('#/schedule') && !h.includes('#/pine')]);
+} catch (e) { console.error('SSR ERROR:', e.message); process.exit(1); }
+finally { await server.close(); }
+results.forEach(([n, ok]) => console.log(ok ? '✓' : '✗', n));
+process.exit(results.every(([, ok]) => ok) ? 0 : 1);
diff --git a/website/index.html b/website/index.html
new file mode 100644
index 0000000..0108e34
--- /dev/null
+++ b/website/index.html
@@ -0,0 +1,28 @@
+
+
+
+ setOpen(false)} />}
+ {open && (
+
+ )}
+ >
+ );
+}
+
+/* ---------- 组织:SiteFooter ---------- */
+export function SiteFooter() {
+ return (
+
+ );
+}
+
+/* ---------- 组织:ParticleField(WebGL 3D 密集粒子场 · 密度×30 · 科技感 · 鼠标触摸) ---------- */
+/* WebGL2 (GLSL ES 3.00) 着色器:#version 必须为第一行 */
+const GL2_VS = `#version 300 es
+precision highp float;
+in vec3 aPos;
+in float aSize;
+in vec3 aCol;
+uniform mat3 uRot;
+uniform float uF;
+uniform float uDpr;
+uniform vec2 uHalf;
+uniform vec2 uMouse;
+uniform float uZ0;
+uniform float uZ1;
+out float vDepth;
+out vec3 vCol;
+out float vSize;
+void main(){
+ vec3 p = uRot * aPos;
+ if(p.z < 0.6){ gl_Position = vec4(2.0,2.0,2.0,1.0); vDepth = 0.0; vCol = aCol; vSize = aSize; gl_PointSize = 1.0; return; }
+ float z = p.z;
+ vec2 s = (p.xy / z) * uF;
+ vec2 dm = s - uMouse;
+ float d = length(dm);
+ float MR = 210.0 * uDpr;
+ if(d < MR && d > 0.001){
+ s += (dm / d) * (1.0 - d / MR) * 44.0 * uDpr;
+ }
+ gl_Position = vec4(s.x / uHalf.x, s.y / uHalf.y, 0.0, 1.0);
+ vDepth = clamp((uZ1 - z) / (uZ1 - uZ0), 0.0, 1.0);
+ vCol = aCol;
+ vSize = aSize;
+ gl_PointSize = max(0.8, aSize) * (0.7 + vDepth * 1.4) * uDpr;
+}`;
+const GL2_FS = `#version 300 es
+precision mediump float;
+in float vDepth;
+in vec3 vCol;
+in float vSize;
+uniform float uTime;
+out vec4 outColor;
+void main(){
+ vec2 c = gl_PointCoord * 2.0 - 1.0;
+ float dist = length(c);
+ if(dist > 1.0) discard;
+ float soft = smoothstep(1.0, 0.0, dist);
+ float core = 1.0 - smoothstep(0.0, 0.45, dist);
+ float a = (0.16 + 0.84 * soft) * (0.45 + 0.55 * core);
+ float tw = 0.8 + 0.2 * sin(uTime * 2.0 + vDepth * 22.0 + gl_FragCoord.x * 0.013);
+ float alpha = a * tw;
+ outColor = vec4(vCol * alpha, alpha);
+}`;
+
+/* WebGL1 (GLSL ES 1.00) 兜底着色器 */
+const GL1_VS = `attribute vec3 aPos;
+attribute float aSize;
+attribute vec3 aCol;
+uniform mat3 uRot;
+uniform float uF;
+uniform float uDpr;
+uniform vec2 uHalf;
+uniform vec2 uMouse;
+uniform float uZ0;
+uniform float uZ1;
+varying float vDepth;
+varying vec3 vCol;
+varying float vSize;
+void main(){
+ vec3 p = uRot * aPos;
+ if(p.z < 0.6){ gl_Position = vec4(2.0,2.0,2.0,1.0); vDepth = 0.0; vCol = aCol; vSize = aSize; gl_PointSize = 1.0; return; }
+ float z = p.z;
+ vec2 s = (p.xy / z) * uF;
+ vec2 dm = s - uMouse;
+ float d = length(dm);
+ float MR = 210.0 * uDpr;
+ if(d < MR && d > 0.001){
+ s += (dm / d) * (1.0 - d / MR) * 44.0 * uDpr;
+ }
+ gl_Position = vec4(s.x / uHalf.x, s.y / uHalf.y, 0.0, 1.0);
+ vDepth = clamp((uZ1 - z) / (uZ1 - uZ0), 0.0, 1.0);
+ vCol = aCol;
+ vSize = aSize;
+ gl_PointSize = max(0.8, aSize) * (0.7 + vDepth * 1.4) * uDpr;
+}`;
+const GL1_FS = `precision mediump float;
+varying float vDepth;
+varying vec3 vCol;
+varying float vSize;
+uniform float uTime;
+void main(){
+ vec2 c = gl_PointCoord * 2.0 - 1.0;
+ float dist = length(c);
+ if(dist > 1.0) discard;
+ float soft = smoothstep(1.0, 0.0, dist);
+ float core = 1.0 - smoothstep(0.0, 0.45, dist);
+ float a = (0.16 + 0.84 * soft) * (0.45 + 0.55 * core);
+ float tw = 0.8 + 0.2 * sin(uTime * 2.0 + vDepth * 22.0 + gl_FragCoord.x * 0.013);
+ float alpha = a * tw;
+ gl_FragColor = vec4(vCol * alpha, alpha);
+}`;
+
+/* 生成星空:星河对角带(暖核/冷臂) + 散布星点 + 光斑(bokeh)。
+ 相机在原点、盒体跨越 z 正负 → 旋转后始终有星在相机前,不会掏空一侧。 */
+function buildGalaxy(cw, ch) {
+ const B = Math.max(cw, ch) * 1.05;
+ const BX = B, BY = B, BZ = B * 0.9;
+ const total = Math.min(320000, Math.max(120000, Math.floor((cw * ch) / 8)));
+ const bokeh = 90;
+ const N = total + bokeh;
+ const arr = new Float32Array(N * 7);
+ let o = 0;
+ const push = (x, y, z, size, r, g, b) => {
+ arr[o] = x; arr[o + 1] = y; arr[o + 2] = z;
+ arr[o + 3] = size; arr[o + 4] = r; arr[o + 5] = g; arr[o + 6] = b;
+ o += 7;
+ };
+ const gauss = () => (Math.random() + Math.random() + Math.random() - 1.5);
+ // 星河:对角细带,中心暖金 / 边缘冷蓝(z 跨越正负,保证任意角度可见)
+ const gal = Math.floor(total * 0.55);
+ for (let i = 0; i < gal; i++) {
+ const x = (Math.random() * 2 - 1) * BX;
+ const y = x * 0.5 + gauss() * BY * 0.10;
+ const z = gauss() * BZ * 0.5;
+ const warm = Math.random() < 0.5;
+ push(x, y, z, 1.0 + Math.random() * 1.6,
+ warm ? 1.0 : 0.60, warm ? 0.93 : 0.82, warm ? 0.72 : 1.0);
+ }
+ // 散布星点:白 / 青 / 暖
+ const sc = total - gal;
+ for (let i = 0; i < sc; i++) {
+ const x = (Math.random() * 2 - 1) * BX, y = (Math.random() * 2 - 1) * BY, z = (Math.random() * 2 - 1) * BZ;
+ const c = Math.random();
+ push(x, y, z, 0.8 + Math.random() * 1.2,
+ c < 0.55 ? 0.95 : c < 0.8 ? 0.70 : 1.0,
+ c < 0.55 ? 0.97 : c < 0.8 ? 0.86 : 0.88,
+ c < 0.55 ? 1.0 : c < 0.8 ? 1.0 : 0.70);
+ }
+ // 光斑 bokeh:大而柔的辉光
+ for (let i = 0; i < bokeh; i++) {
+ const x = (Math.random() * 2 - 1) * BX, y = (Math.random() * 2 - 1) * BY, z = (Math.random() * 2 - 1) * BZ;
+ const c = Math.random();
+ push(x, y, z, 6 + Math.random() * 10,
+ c < 0.4 ? 0.55 : c < 0.7 ? 0.75 : 0.60,
+ c < 0.4 ? 0.80 : c < 0.7 ? 0.55 : 0.85,
+ c < 0.4 ? 1.0 : c < 0.7 ? 1.0 : 0.95);
+ }
+ return arr;
+}
+
+function compileShader(gl, type, src) {
+ const s = gl.createShader(type);
+ gl.shaderSource(s, src);
+ gl.compileShader(s);
+ if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) {
+ console.warn('shader error:', gl.getShaderInfoLog(s));
+ return null;
+ }
+ return s;
+}
+
+/* Ry(ay) 后 Rx(ax) 的组合矩阵(列主序) */
+function mat3Rot(ay, ax) {
+ const c1 = Math.cos(ay), s1 = Math.sin(ay);
+ const c2 = Math.cos(ax), s2 = Math.sin(ax);
+ // Ry
+ const Ry = [c1, 0, -s1, 0, 1, 0, s1, 0, c1]; // column-major
+ // Rx
+ const Rx = [1, 0, 0, 0, c2, s2, 0, -s2, c2]; // column-major
+ // M = Rx * Ry
+ const m = new Float32Array(9);
+ for (let c = 0; c < 3; c++) for (let r = 0; r < 3; r++) {
+ let v = 0;
+ for (let k = 0; k < 3; k++) v += Rx[k * 3 + r] * Ry[c * 3 + k];
+ m[c * 3 + r] = v;
+ }
+ return m;
+}
+
+export function ParticleField() {
+ const ref = React.useRef(null);
+
+ React.useEffect(() => {
+ const canvas = ref.current;
+ const gl = canvas.getContext('webgl2') || canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
+ if (gl) return startGL(gl, canvas);
+ return start2D(canvas);
+ }, []);
+
+ return (
+
+ );
+}
+
+function startGL(gl, canvas) {
+ const isGL2 = typeof WebGL2RenderingContext !== 'undefined' && gl instanceof WebGL2RenderingContext;
+ const vs = compileShader(gl, gl.VERTEX_SHADER, isGL2 ? GL2_VS : GL1_VS);
+ const fs = compileShader(gl, gl.FRAGMENT_SHADER, isGL2 ? GL2_FS : GL1_FS);
+ if (!vs || !fs) return start2D(canvas);
+ const prog = gl.createProgram();
+ gl.attachShader(prog, vs); gl.attachShader(prog, fs);
+ gl.linkProgram(prog);
+ if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) { console.warn('link error'); return start2D(canvas); }
+ gl.useProgram(prog);
+
+ const loc = {
+ pos: gl.getAttribLocation(prog, 'aPos'),
+ size: gl.getAttribLocation(prog, 'aSize'),
+ col: gl.getAttribLocation(prog, 'aCol'),
+ rot: gl.getUniformLocation(prog, 'uRot'),
+ F: gl.getUniformLocation(prog, 'uF'),
+ dpr: gl.getUniformLocation(prog, 'uDpr'),
+ half: gl.getUniformLocation(prog, 'uHalf'),
+ mouse: gl.getUniformLocation(prog, 'uMouse'),
+ z0: gl.getUniformLocation(prog, 'uZ0'),
+ z1: gl.getUniformLocation(prog, 'uZ1'),
+ time: gl.getUniformLocation(prog, 'uTime')
+ };
+
+ let w = 0, h = 0, N = 0, buf = null;
+ const Z0 = 60, Z1 = 900;
+ const mouse = { x: -99999, y: -99999 };
+ let t = 0, raf = 0, dpr = 1, cw = 0, ch = 0;
+
+ function build() {
+ // 关键:用 clientWidth(排除滚动条)且乘 devicePixelRatio,鼠标坐标才能精确贴合
+ dpr = window.devicePixelRatio || 1;
+ cw = canvas.clientWidth || window.innerWidth;
+ ch = canvas.clientHeight || window.innerHeight;
+ w = Math.round(cw * dpr);
+ h = Math.round(ch * dpr);
+ canvas.width = w; canvas.height = h;
+
+ const arr = buildGalaxy(cw, ch);
+ N = arr.length / 7;
+ if (buf) gl.deleteBuffer(buf);
+ buf = gl.createBuffer();
+ gl.bindBuffer(gl.ARRAY_BUFFER, buf);
+ gl.bufferData(gl.ARRAY_BUFFER, arr, gl.STATIC_DRAW);
+ const stride = 7 * 4;
+ gl.enableVertexAttribArray(loc.pos);
+ gl.vertexAttribPointer(loc.pos, 3, gl.FLOAT, false, stride, 0);
+ gl.enableVertexAttribArray(loc.size);
+ gl.vertexAttribPointer(loc.size, 1, gl.FLOAT, false, stride, 12);
+ gl.enableVertexAttribArray(loc.col);
+ gl.vertexAttribPointer(loc.col, 3, gl.FLOAT, false, stride, 16);
+ gl.viewport(0, 0, w, h);
+ gl.uniform1f(loc.F, 300 * dpr);
+ gl.uniform1f(loc.dpr, dpr);
+ gl.uniform1f(loc.z0, Z0);
+ gl.uniform1f(loc.z1, Z1);
+ gl.uniform2f(loc.half, w / 2, h / 2);
+ }
+
+ gl.enable(gl.BLEND);
+ gl.blendFunc(gl.SRC_ALPHA, gl.ONE); // 叠加 → 辉光
+ gl.clearColor(0, 0, 0, 1);
+
+ const onMove = (e) => { mouse.x = e.clientX; mouse.y = e.clientY; };
+ const onLeave = () => { mouse.x = -99999; mouse.y = -99999; };
+
+ const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
+ build();
+
+ function frame() {
+ t++;
+ const ay = Math.sin(t * 0.0005) * 0.55;
+ const ax = Math.sin(t * 0.00032) * 0.12;
+ gl.uniformMatrix3fv(loc.rot, false, mat3Rot(ay, ax));
+ // 鼠标 → 设备像素(中心原点、y 向上为正,与顶点着色器 s 坐标同一空间)
+ gl.uniform2f(loc.mouse, (mouse.x - cw / 2) * dpr, (ch / 2 - mouse.y) * dpr);
+ gl.uniform1f(loc.time, t * 0.01);
+ gl.clear(gl.COLOR_BUFFER_BIT);
+ gl.drawArrays(gl.POINTS, 0, N);
+ raf = requestAnimationFrame(frame);
+ }
+ if (reduce) { frame(); cancelAnimationFrame(raf); }
+ else frame();
+
+ window.addEventListener('resize', build);
+ window.addEventListener('pointermove', onMove);
+ document.addEventListener('mouseleave', onLeave);
+ return () => {
+ cancelAnimationFrame(raf);
+ window.removeEventListener('resize', build);
+ window.removeEventListener('pointermove', onMove);
+ document.removeEventListener('mouseleave', onLeave);
+ };
+}
+
+/* 兜底:Canvas 2D 简化 3D 粒子(WebGL 不可用时) */
+function start2D(canvas) {
+ const ctx = canvas.getContext('2d');
+ let raf, w, h, cx, cy, pts = [];
+ let t = 0;
+ const F = 300, mouse = { x: -9999, y: -9999 };
+ const build = () => {
+ w = canvas.width = window.innerWidth;
+ h = canvas.height = window.innerHeight;
+ cx = w / 2; cy = h / 2;
+ const BX = Math.max(620, (w / 2) * 2.0);
+ const BY = Math.max(400, (h / 2) * 2.0);
+ const N = Math.min(6000, Math.max(1500, Math.floor((w * h) / 400)));
+ pts = new Array(N);
+ for (let i = 0; i < N; i++) {
+ pts[i] = { bx: (Math.random() * 2 - 1) * BX, by: (Math.random() * 2 - 1) * BY, bz: 150 + Math.random() * 410, ph: Math.random() * Math.PI * 2 };
+ }
+ };
+ const draw = () => {
+ ctx.clearRect(0, 0, w, h);
+ t++;
+ const ay = Math.sin(t * 0.0005) * 0.55, ax = Math.sin(t * 0.00032) * 0.12;
+ const cy2 = Math.cos(ay), sy2 = Math.sin(ay), cx2 = Math.cos(ax), sx2 = Math.sin(ax);
+ for (let i = 0; i < pts.length; i++) {
+ const p = pts[i];
+ let x = p.bx * cy2 + p.bz * sy2, z = -p.bx * sy2 + p.bz * cy2;
+ let y = p.by * cx2 - z * sx2; z = p.by * sx2 + z * cx2;
+ if (z < 40) continue;
+ const sc = F / z;
+ let sx = cx + x * sc, sy = cy + y * sc;
+ if (sx < -24 || sx > w + 24 || sy < -24 || sy > h + 24) continue;
+ let dx = sx - mouse.x, dy = sy - mouse.y;
+ const d2 = dx * dx + dy * dy;
+ if (mouse.x > -500 && d2 < 170 * 170) {
+ const d = Math.sqrt(d2) || 1;
+ const push = (1 - Math.sqrt(d2) / 170) * 34;
+ sx += (dx / d) * push; sy += (dy / d) * push;
+ }
+ const depth = Math.max(0, Math.min(1, (560 - z) / 410));
+ const a = 0.05 + depth * 0.4;
+ ctx.fillStyle = `rgba(160,200,255,${a.toFixed(3)})`;
+ ctx.fillRect(sx, sy, 1 + depth, 1 + depth);
+ }
+ raf = requestAnimationFrame(draw);
+ };
+ const onMove = (e) => { mouse.x = e.clientX; mouse.y = e.clientY; };
+ const onLeave = () => { mouse.x = -9999; mouse.y = -9999; };
+ const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
+ build();
+ if (reduce) { draw(); cancelAnimationFrame(raf); } else draw();
+ window.addEventListener('resize', build);
+ window.addEventListener('pointermove', onMove);
+ document.addEventListener('mouseleave', onLeave);
+ return () => {
+ cancelAnimationFrame(raf);
+ window.removeEventListener('resize', build);
+ window.removeEventListener('pointermove', onMove);
+ document.removeEventListener('mouseleave', onLeave);
+ };
+}
+
+/* ---------- 组织:Reveal(滚动进场包装) ---------- */
+export function Reveal({ children, as = 'div', className = '', ...rest }) {
+ const ref = React.useRef(null);
+ React.useEffect(() => {
+ const el = ref.current;
+ if (!el) return;
+ if (!('IntersectionObserver' in window)) { el.classList.add('in'); return; }
+ const io = new IntersectionObserver((entries) => {
+ entries.forEach((en) => {
+ if (en.isIntersecting) { en.target.classList.add('in'); io.unobserve(en.target); }
+ });
+ }, { threshold: 0.12 });
+ io.observe(el);
+ return () => io.disconnect();
+ }, []);
+ const Tag = as;
+ return
{children};
+}
diff --git a/website/src/components/pineAdmin.jsx b/website/src/components/pineAdmin.jsx
new file mode 100644
index 0000000..a0fb0d3
--- /dev/null
+++ b/website/src/components/pineAdmin.jsx
@@ -0,0 +1,78 @@
+import React from 'react';
+import Skeleton from '@/components/Skeleton';
+
+/* ---------- 管理页通用 hook:拉取 + loading + 错误 + 重载 ---------- */
+export function useOps(fn) {
+ const [data, setData] = React.useState(null);
+ const [err, setErr] = React.useState('');
+ const [loading, setLoading] = React.useState(true);
+ const reload = React.useCallback(() => {
+ setLoading(true);
+ setErr('');
+ fn()
+ .then((d) => { setData(d); setErr(''); })
+ .catch((e) => { setData(null); setErr(e && e.message ? e.message : '加载失败'); })
+ .finally(() => setLoading(false));
+ }, [fn]);
+ React.useEffect(() => { reload(); }, [reload]);
+ return { data, err, loading, reload };
+}
+
+/* ---------- 预约状态徽章 ---------- */
+const STATUS_MAP = {
+ pending: { label: '待确认', cls: 's-pending' },
+ confirmed: { label: '已确认', cls: 's-confirmed' },
+ arrived: { label: '已到场', cls: 's-arrived' },
+ converted: { label: '已转化', cls: 's-converted' }
+};
+export function StatusBadge({ status }) {
+ const m = STATUS_MAP[status] || STATUS_MAP.pending;
+ return
{m.label};
+}
+export const BOOKING_STATUSES = ['pending', 'confirmed', 'arrived', 'converted'];
+
+/* ---------- 统计卡片 ---------- */
+export function StatCard({ label, value, icon, accent }) {
+ return (
+
+ );
+}
+
+/* ---------- 加载 / 错误占位 ---------- */
+export function OpsState({ loading, err, children }) {
+ if (loading) return
;
+ if (err) return
{err}
;
+ return children || null;
+}
+
+/* ---------- 时间工具 ---------- */
+export const toLocalInput = (iso) => {
+ if (!iso) return '';
+ const d = new Date(iso);
+ const p = (n) => String(n).padStart(2, '0');
+ return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}T${p(d.getHours())}:${p(d.getMinutes())}`;
+};
+export const fromLocalInput = (s) => (s ? new Date(s).toISOString() : '');
+
+export const fmtDate = (iso) => {
+ if (!iso) return '—';
+ const d = new Date(iso);
+ const p = (n) => String(n).padStart(2, '0');
+ return `${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`;
+};
+
+/* ---------- 导出 CSV ---------- */
+export function downloadCSV(filename, head, rows) {
+ const esc = (v) => `"${String(v == null ? '' : v).replace(/"/g, '""')}"`;
+ const csv = [head.map(esc).join(','), ...rows.map((r) => r.map(esc).join(','))].join('\n');
+ const blob = new Blob(['' + csv], { type: 'text/csv;charset=utf-8;' });
+ const a = document.createElement('a');
+ a.href = URL.createObjectURL(blob);
+ a.download = filename;
+ a.click();
+ URL.revokeObjectURL(a.href);
+}
diff --git a/website/src/components/templates/index.jsx b/website/src/components/templates/index.jsx
new file mode 100644
index 0000000..6aa9c16
--- /dev/null
+++ b/website/src/components/templates/index.jsx
@@ -0,0 +1,63 @@
+import React from 'react';
+import { Header, SiteFooter, Reveal } from '../organisms';
+import { TrustRow, StatItem } from '../molecules';
+import { TRUST, HERO, STATS } from '../../data/site';
+
+/* ---------- 组织:Hero(design.md 中间区) ---------- */
+export function Hero() {
+ return (
+
+
+
+ {HERO.headline.map((line, i) => (
+ {line}
+ ))}
+
+ {HERO.slogan}
+ {HERO.subhead}
+
+
+ );
+}
+
+/* ---------- 组织:StatsBar(design.md 底部 4 格) ---------- */
+export function StatsBar() {
+ return (
+
+ {STATS.map((s, i) => (
+
+ ))}
+
+ );
+}
+
+/* ---------- 模板:LandingTemplate(单视口,背景由全局粒子场景提供) ---------- */
+export function LandingTemplate({ active }) {
+ return (
+
+
+
+
+
+ );
+}
+
+/* ---------- 模板:ContentTemplate(可滚动内容页) ---------- */
+export function ContentTemplate({ active, kicker, title, desc, children }) {
+ return (
+
+
+
+
+ {kicker}
+ {title}
+ {desc}
+
+ {children}
+
+
+
+ );
+}
diff --git a/website/src/data/booking.js b/website/src/data/booking.js
new file mode 100644
index 0000000..0450ce4
--- /dev/null
+++ b/website/src/data/booking.js
@@ -0,0 +1,19 @@
+// 预约页 · 表单选项数据(口径对齐 07_公益课与沙龙课程主题表)
+
+export const BK_STATUS_OPTIONS = ['上班族', '自由职业 / 副业', '创业者 / 小老板', '大学生 / 应届', '其他'];
+
+export const BK_WANT_OPTIONS = ['公益课', '沙龙', '都可以'];
+
+// 主题意向(对齐 07 主题表栏目,多选)
+export const BK_TOPIC_OPTIONS = [
+ '副业 / 技能变现',
+ '民宿 / 文旅创业',
+ '咖啡 / 鲜花 / 农特产',
+ 'AI 工具 / 智能体实操',
+ '数字游民',
+ '政策补贴 / 园区入驻',
+ '财税 / 注册合规',
+ '还没方向,想先了解'
+];
+
+export const BK_SOURCE_OPTIONS = ['公众号', '小红书', '短视频', '朋友推荐', '园区活动', '其他'];
diff --git a/website/src/data/cards.js b/website/src/data/cards.js
new file mode 100644
index 0000000..0c51d6b
--- /dev/null
+++ b/website/src/data/cards.js
@@ -0,0 +1,35 @@
+// 卡片库页数据
+export const SELF_CARDS_A = [
+ { no: '01', name: '资源盘点卡', desc: '8 维盘点 + 证据 + 自评;认清 2 强 2 弱。', value: '建立起点自信' },
+ { no: '02', name: '价值观与动机卡', desc: '动机排序 + 内在/外在分析。', value: '判断能撑多久' },
+ { no: '03', name: '能力与经历卡', desc: '高光/失败/他人视角,提取 3 项核心能力。', value: '发现被低估的自己' },
+ { no: '04', name: '兴趣与心流卡', desc: '找到愿意免费做很久的事。', value: '兴趣是长期主义燃料' },
+ { no: '05', name: '生活方式与目标卡', desc: '理想一天 + 收入目标,倒推生意结构。', value: '目标即方向终点锚' }
+];
+
+export const SELF_CARDS_B = [
+ { no: '06', name: '现状痛点与反思卡', desc: '卡点定位 + 尝试复盘 + 痛点转机会。', value: '痛点是方向入口' },
+ { no: '07', name: '恐惧与障碍卡', desc: '恐惧清单 + 最坏情况推演 + 第一小步。', value: '把恐惧具体化' },
+ { no: '08', name: '赛道三环验证卡', desc: '需求 / 竞争 / 收益 三环自验。', value: '不靠感觉选赛道' },
+ { no: '09', name: '六维评分卡', desc: '6 维加权打分 + 匹配度诚实自评。', value: '用分数做决策' },
+ { no: '10', name: '赛道选择卡', desc: '最终选定 + 差异化理由 + 承诺。', value: '自己签字,负责执行' }
+];
+
+export const MENTOR_MODULES = [
+ { k: '基础状况', v: '身份状态 / 时间资金 / 技能基线 / 收入目标' },
+ { k: '性格与学习风格', v: '性格特质 / 想清楚再做 or 边做边学 or 需要人带' },
+ { k: '动机与价值观', v: '首要动机 / 内在 or 外在 / 坚持概率' },
+ { k: '卡点与障碍', v: '资源 / 技能 / 方向 / 行动 / 心态;恐惧与第一小步' },
+ { k: '优势与潜力', v: '3 项核心能力 / 被低估能力 / 兴趣指向' },
+ { k: '匹配赛道', v: '学员选定方向 + 六维总分 + 导师复核结论' },
+ { k: '针对性配套教学', v: '教学重点 / 节奏 / 工具配置 / 陪跑策略 / 风险预警' }
+];
+
+export const CORE_TEMPLATES = [
+ { no: 'T01', name: '资源盘点表', desc: '客户唯一要填的准入表,10 分钟。', highlight: false },
+ { no: 'T02', name: '诊断报告', desc: '现状 / 卡点 / 主攻方向,内嵌赛道选择卡。', highlight: false },
+ { no: 'T03', name: 'OPC 经营方案 6 页', desc: '定位 / 产品 / 闭环 / 工具智能体 / 财务 / 30 天表。', highlight: true },
+ { no: 'T04', name: '智能体配置清单', desc: '客服 / 获客 / 复盘智能体:系统指令 + 规则 + 交付物。', highlight: false },
+ { no: 'T05', name: '30 天落地执行表', desc: '4 周目标 + 每日流程 + 每周复盘,陪跑检查清单。', highlight: false },
+ { no: 'T06', name: '财务测算表', desc: '收入 / 成本 / 盈亏平衡 / 3 月现金流 / 定价校验。', highlight: false }
+];
diff --git a/website/src/data/forms.jsx b/website/src/data/forms.jsx
new file mode 100644
index 0000000..ece4fcf
--- /dev/null
+++ b/website/src/data/forms.jsx
@@ -0,0 +1,182 @@
+// 全部资产表单 schema —— 自我认知卡 10 张 + 核心模板 + 问卷
+
+const R = (label, key, extra = {}) => ({ type: 'range', label, key, ...extra });
+const TA = (label, key, extra = {}) => ({ type: 'textarea', label, key, ...extra });
+const T = (label, key, extra = {}) => ({ type: 'text', label, key, ...extra });
+const N = (label, key, extra = {}) => ({ type: 'number', label, key, ...extra });
+const SEL = (label, key, options) => ({ type: 'select', label, key, options });
+const RAD = (label, key, options) => ({ type: 'radio', label, key, options });
+const CHK = (label, key, options) => ({ type: 'check', label, key, options });
+
+/* ============ 自我认知卡 01–10 ============ */
+export const SELF_FORMS = [
+ {
+ no: '01', title: '资源盘点卡', desc: '8 维自评(1–10),带证据;认清 2 强 2 弱。',
+ fields: [
+ R('技能', 'skill'), R('时间', 'time'), R('资金', 'money'), R('人脉', 'network'),
+ R('资源', 'asset'), R('经验', 'exp'), R('兴趣', 'interest'), R('性格', 'persona'),
+ TA('我的 2 项最强资源', 'strong2', { rows: 2 }), TA('我的 2 项最弱', 'weak2', { rows: 2 }),
+ TA('反思:强弱如何支撑一个方向', 'reflect', { rows: 2 })
+ ]
+ },
+ {
+ no: '02', title: '价值观与动机卡', desc: '动机排序 + 内在/外在分析。',
+ fields: [
+ CHK('我的动机(可多选)', 'motive', ['财务自由/稳定收入', '时间自由', '做热爱的事', '证明自己', '逃离现状', '成长学习', '帮助他人']),
+ TA('我绝不想再过的生活', 'noWant', { rows: 2 }),
+ TA('如果这件事不赚钱,我还愿意做吗', 'inner', { rows: 2 }),
+ TA('3 年后我的一天(理想状态深描)', 'vision', { rows: 3 })
+ ]
+ },
+ {
+ no: '03', title: '能力与经历卡', desc: '从高光 / 失败里提取核心能力。',
+ fields: [
+ TA('高光时刻(我做过什么、结果如何)', 'high', { rows: 3 }),
+ TA('失败与教训(错在哪、学到什么)', 'fail', { rows: 2 }),
+ TA('他人视角(别人常夸我的)', 'others', { rows: 2 }),
+ TA('我的 3 项核心能力 + 被低估的一项', 'ability', { rows: 2 })
+ ]
+ },
+ {
+ no: '04', title: '兴趣与心流卡', desc: '找到愿意免费做很久的事。',
+ fields: [
+ TA('我做什么会忘记时间(心流)', 'flow', { rows: 2 }),
+ TA('我的素材库(常看的书/视频/博主)', 'library', { rows: 2 }),
+ TA('一直想做但没开始的领域', 'dream', { rows: 2 }),
+ TA('兴趣里最可能有人付费的方向', 'commercial', { rows: 2 })
+ ]
+ },
+ {
+ no: '05', title: '生活方式与目标卡', desc: '理想一天 + 收入目标,倒推生意结构。',
+ fields: [
+ TA('理想的一天(深描)', 'day', { rows: 3 }),
+ N('月理想收入(元)', 'income'),
+ RAD('客单价倾向', 'price', ['低价走量(9–99 元)', '中价精品(100–1000 元)', '高价定制(1000+ 元)']),
+ RAD('是否需要被动/复利部分', 'passive', ['需要', '不需要']),
+ TA('倒推:我要的是一份怎样的 OPC', 'structure', { rows: 2 })
+ ]
+ },
+ {
+ no: '06', title: '现状痛点与反思卡', desc: '卡点定位 + 痛点转机会。',
+ fields: [
+ TA('我最不满意的 3 件事', 'pain', { rows: 2 }),
+ RAD('我的主要卡点', 'blocker', ['资源型:没钱/没时间', '技能型:不会做', '方向型:不知道做什么', '行动型:不行动/拖延', '心态型:怕失败/内耗']),
+ TA('我试过什么、为什么没成', 'tried', { rows: 2 }),
+ TA('痛点转机会:我能为同样处境的人提供什么', 'opportunity', { rows: 2 })
+ ]
+ },
+ {
+ no: '07', title: '恐惧与障碍卡', desc: '把恐惧摆上台面 + 最坏情况推演。',
+ fields: [
+ CHK('我害怕的(可多选)', 'fear', ['亏钱', '失败', '丢脸', '浪费时间', '家人反对', '养不起自己']),
+ TA('我常对自己说的「我不行」', 'belief', { rows: 2 }),
+ TA('最坏能坏到哪 / 概率 / 能否承受', 'worst', { rows: 2 }),
+ TA('如果不怕失败,我最想先做的一小步', 'firstStep', { rows: 2 })
+ ]
+ },
+ {
+ no: '08', title: '赛道三环验证卡', desc: '需求 / 竞争 / 收益 三环自验。',
+ fields: [
+ TA('候选方向(1–3 个)', 'candidate', { rows: 2 }),
+ TA('需求环:谁在为这事付费 / 有案例吗', 'demand', { rows: 2 }),
+ TA('竞争环:做的人多吗 / 我的差异点', 'competition', { rows: 2 }),
+ TA('收益环:客单 × 客户数 × 频次', 'revenue', { rows: 2 }),
+ SEL('三环结论', 'conclusion', ['三环同时成立', '卡在需求环', '卡在竞争环', '卡在收益环'])
+ ]
+ },
+ {
+ no: '09', title: '六维评分卡', desc: '6 维打分(0–10),自动加权计算。',
+ fields: [
+ R('市场(权重 20%)', 'market'), R('竞争(权重 15%)', 'compet'),
+ R('收益(权重 20%)', 'revenue'), R('匹配度(权重 25%)', 'fit'),
+ R('成长性(权重 10%)', 'growth'), R('风险(权重 10%)', 'risk'),
+ TA('哪个维度最低、拖了后腿', 'weakDim', { rows: 2 })
+ ]
+ },
+ {
+ no: '10', title: '赛道选择卡', desc: '最终选定 + 理由 + 承诺。',
+ fields: [
+ TA('我的最终主攻方向(为谁做什么)', 'final', { rows: 2 }),
+ TA('为什么是「我」做(资源/兴趣/差异)', 'why', { rows: 2 }),
+ TA('我放弃的候选与原因', 'giveup', { rows: 2 }),
+ TA('我的承诺(6 个月 + 第一个月目标)', 'promise', { rows: 2 })
+ ]
+ }
+];
+
+/* ============ 核心模板 ============ */
+export const TEMPLATE_FORMS = [
+ {
+ no: 'T01', title: '资源盘点表(快速准入)', desc: '客户唯一要填的准入表,约 10 分钟。',
+ fields: [
+ T('姓名 / 微信', 'name'), SEL('现在的状态', 'status', ['全职', '副业', '待业', '自由职业']),
+ T('技能(会的)', 'skill'), T('每周可投入时间', 'time'), T('可投入资金', 'money'),
+ T('兴趣', 'interest'), T('现有资源(账号/货源/场地)', 'asset'),
+ TA('最想解决的问题', 'problem', { rows: 2 }), N('期望月收入(元)', 'income')
+ ]
+ },
+ {
+ no: 'T02', title: '诊断报告(导师填)', desc: '现状 / 卡点 / 主攻方向,内嵌赛道选择卡。',
+ fields: [
+ TA('现状画像', 'current', { rows: 2 }), TA('资源 2 强 2 弱', 'strength', { rows: 2 }),
+ TA('核心卡点', 'blocker', { rows: 2 }), TA('主攻方向(赛道选择卡)', 'direction', { rows: 3 }),
+ TA('下一步', 'next', { rows: 2 })
+ ]
+ },
+ {
+ no: 'T03', title: 'OPC 经营方案 · 6 页(导师填)', desc: '核心交付物,页页可复用。',
+ fields: [
+ TA('第 1 页 · 一句话定位', 'p1', { rows: 2 }), TA('第 2 页 · 产品金字塔', 'p2', { rows: 3 }),
+ TA('第 3 页 · 商业闭环(6 环节动作)', 'p3', { rows: 3 }), TA('第 4 页 · 工具栈 + 智能体', 'p4', { rows: 3 }),
+ TA('第 5 页 · 财务测算', 'p5', { rows: 2 }), TA('第 6 页 · 30 天落地表', 'p6', { rows: 3 })
+ ]
+ },
+ {
+ no: 'T04', title: '智能体配置清单', desc: '每个智能体:角色 + 系统指令 + 规则 + 交付物。',
+ fields: [
+ T('智能体 1 角色', 'a1role'), TA('智能体 1 系统指令', 'a1cmd', { rows: 3 }),
+ T('智能体 2 角色', 'a2role'), TA('智能体 2 系统指令', 'a2cmd', { rows: 3 }),
+ T('智能体 3 角色', 'a3role'), TA('智能体 3 系统指令', 'a3cmd', { rows: 3 })
+ ]
+ }
+];
+
+/* ============ 问卷 ============ */
+export const QUESTIONNAIRES = [
+ {
+ no: '报名问卷', title: '公益课 · 报名问卷', desc: '引导报名线上/线下课,收集意向。',
+ fields: [
+ T('姓名 / 花名', 'name'), T('微信', 'wechat'),
+ SEL('当前状态', 'status', ['全职', '副业', '待业', '自由职业', '学生']),
+ RAD('想报哪门课', 'course', ['线上标准服务(¥499–1299)', '线下深度服务(¥1980–3980)', '还在了解']),
+ TA('最想解决的一个问题', 'problem', { rows: 2 }),
+ T('从哪里听说', 'source')
+ ]
+ },
+ {
+ no: '满意度问卷', title: '结营满意度问卷', desc: '每期结营收集效果与改进建议。',
+ fields: [
+ RAD('整体满意度', 'score', ['非常满意', '满意', '一般', '不满意']),
+ RAD('是否愿意推荐给朋友', 'nps', ['愿意', '一般', '不愿意']),
+ TA('最大的收获', 'gain', { rows: 2 }),
+ TA('最想改进的一点', 'improve', { rows: 2 })
+ ]
+ }
+];
+
+/* 六维评分计算(供 CardForm compute 使用) */
+export function nicheCompute(vals) {
+ const W = { market: 0.20, compet: 0.15, revenue: 0.20, fit: 0.25, growth: 0.10, risk: 0.10 };
+ let total = 0;
+ for (const k in W) {
+ const v = Number(vals[k]);
+ if (isNaN(v)) return '请填满 6 个维度(0–10)再计算。';
+ total += v * W[k];
+ }
+ const score = total.toFixed(1);
+ const verdict = score >= 7.5 ? '≥7.5 → 优先进入' : score >= 6 ? '≥6 → 可进入' : score >= 5 ? '5–6 → 谨慎' : '<5 → 建议放弃';
+ return (
+ <>
加权总分:{score} · {verdict}
+
先看「匹配度」是否高估/低估了自己(导师确认会复核)。>
+ );
+}
diff --git a/website/src/data/home.js b/website/src/data/home.js
new file mode 100644
index 0000000..35caa6e
--- /dev/null
+++ b/website/src/data/home.js
@@ -0,0 +1,116 @@
+// 首页营销落地页 · 内容数据
+// 口径对齐:课程体系0.1/宣传与运营/00_营销总纲(对外红线:公域只邀请到公益课/沙龙)
+// 首页只展示:公益课报名 + 沙龙预约 + OPC测评;付费课价格/交付物/效果卡数字不在首页出现
+
+export const MK_HERO = {
+ badge: '一个人,也可以是一家公司',
+ headline: ['ONE PERSON', 'DESIGNED TO EVOLVE'],
+ subhead:
+ '我们落地在昆明市大学生创业园。每周有免费公益课和线下沙龙,讲透「一个人 + AI 怎么开一家公司」。先来听一场,交个朋友。',
+ ctaPrimary: { label: '预约公益课 / 沙龙', href: '#/events' },
+ ctaSecondary: { label: '先做 OPC 测评', href: '#/opc-test' }
+};
+
+export const MK_TRUST = [
+ { glyph: '①', value: 2, suffix: ' 场/周', decimals: 0, label: '公益课 · 全年滚动', delay: '0.4s' },
+ { glyph: '②', value: 36, suffix: ' 期', decimals: 0, label: '公益课主题库', delay: '0.48s' },
+ { glyph: '③', value: 4, suffix: ' 方向', decimals: 0, label: '沙龙小场深聊', delay: '0.56s' },
+ { glyph: '④', value: 200, suffix: '+', decimals: 0, label: '创业者', delay: '0.64s' }
+];
+
+export const MK_PRINCIPLE = {
+ kicker: 'Our Belief',
+ title: '别再教 OPC,直接做 OPC',
+ en: 'From teaching to delivering',
+ oldTitle: '课程逻辑(旧)',
+ oldPoints: [
+ '多周教学,学员边学边做作业',
+ '赛道卡、六维等靠学员自填自评',
+ '交付的是「知识」'
+ ],
+ newTitle: '服务逻辑(现在)',
+ newPoints: [
+ '先来公益课 / 沙龙认识我们',
+ '需要时,我们把方案做给你',
+ '交付「方案 + 智能体 + 陪跑」'
+ ],
+ quote: '课程卖的是「知识」,服务交付的是「结果」。'
+};
+
+export const MK_PRODUCTS = [
+ {
+ tag: '公益',
+ name: '公益课',
+ price: '免费',
+ period: '每周 1 场 · 主题轮换',
+ desc: '一场分享,带走一个值得做的方向——先交个朋友。',
+ deliverables: ['60–90 分钟主题认知', 'OPC典型案例分析', '扫码预约,无门槛'],
+ cta: '报名公益课',
+ scroll: null,
+ href: '#/events',
+ highlight: false
+ },
+ {
+ tag: '沙龙',
+ name: '交流沙龙',
+ price: '免费',
+ period: '每月 2–4 场 · 12–30 人',
+ desc: '小场深度聊:主题深谈 + AI 现场演示 + 云南本地人脉面对面。',
+ deliverables: ['小场深度交流', 'AI 工具 / 智能体现场演示', '本地资源对接'],
+ cta: '预约沙龙',
+ scroll: null,
+ href: '#/events',
+ highlight: false
+ },
+ {
+ tag: '测评',
+ name: 'OPC 创业基因测评',
+ price: '免费',
+ period: '5 分钟 · 随时可测',
+ desc: '测出你适不适合做 OPC、该选什么赛道、适合什么角色、怎么和 AI 协作。',
+ deliverables: ['适配指数 + 6 维分析', '推荐赛道 TOP2–3', '4 字母类型码 + 人设名'],
+ cta: '开始测评',
+ scroll: null,
+ href: '#/opc-test',
+ highlight: false
+ }
+];
+
+export const MK_CORE = {
+ kicker: 'The Core',
+ title: '我们怎么帮你:认识 → 方向 → 陪跑',
+ en: 'Know · Point · Run',
+ quote: '------',
+ items: [
+ { step: '01', title: '认识', desc: '公益课 / 沙龙里看清"一个人 + AI 开公司"是怎么回事,做一次测评找到自己的倾向。', out: '得到:一个值得做的方向' },
+ { step: '02', title: '方向', desc: '需要时,我们把你的资源、能力梳理成一条可执行的路(体系设计)。', out: '得到:一条可执行的路' },
+ { step: '03', title: '陪跑', desc: '30 天每周复盘 + 答疑,陪你一起推进,落地到结果。', out: '得到:落地进展 / 结果' }
+ ]
+};
+
+export const MK_OPENING = {
+ kicker: 'Events',
+ title: '公益课 & 沙龙 · 每周滚动',
+ en: 'Rolling events',
+ note: '免费公益课每周 1 场、沙龙每月 2–4 场,主题见公众号 / 社群每周预告。',
+ rows: [
+ { tag: '公益', item: '公益课 · 每周 1 场', desc: '每周线下公益课,每月不定期线上主题交流' },
+ { tag: '沙龙', item: '交流沙龙 · 每周 1 场', desc: '昆明 · 12–30 人小场深聊' },
+ { tag: '测评', item: 'OPC 创业测评 · 随时可测', desc: '5 分钟测出你的赛道 / 角色 / AI 协作方式' }
+ ],
+ cta: { label: '立即预约 →', href: '#/events' }
+};
+
+export const MK_ASSESS = {
+ badge: '先测一测',
+ title: '你适合做什么样的 OPC?',
+ desc: '5 分钟做一次 OPC 创业基因测评:测出适配指数、推荐赛道、适合角色与 AI 协作方式——先了解自己,再来公益课 / 沙龙深聊落地。',
+ cta: { label: '开始 OPC 测评 →', href: '#/opc-test' }
+};
+
+export const MK_SIGNUP = {
+ title: '下一步:从一次测评或一场公益课开始',
+ desc: '先花 5 分钟做 OPC 测评,或预约一场免费公益课 / 沙龙。预约方式:公众号菜单栏「报名」或现场扫码。',
+ ctaPrimary: { label: '预约公益课 / 沙龙', href: '#/events' },
+ ctaSecondary: { label: '先做 OPC 测评', href: '#/opc-test' }
+};
diff --git a/website/src/data/opcTest.js b/website/src/data/opcTest.js
new file mode 100644
index 0000000..e0e01b8
--- /dev/null
+++ b/website/src/data/opcTest.js
@@ -0,0 +1,78 @@
+// OPC 创业基因测评 · 数据与计分逻辑(单一数据源,由 OPC创业测评/scripts/*.json 生成)
+// 生成脚本见:OPC创业测评/scripts/_generate_opcTest.js(本地一次性生成)
+
+export const QUESTIONS = [{"id":1,"part":"P1","quick":true,"question":"面对一个没人给方向的任务,你通常","A":"自己定标准和节奏,直接把它做完","B":"希望有人先给个框架,心里才踏实"},{"id":2,"part":"P1","quick":false,"question":"发现一个好机会时,你更可能","A":"先拉上伙伴一起评估、一起决定","B":"先自己干出个样子,再考虑找别人"},{"id":3,"part":"P1","quick":false,"question":"关于未来收入的预期,你更能接受哪种","A":"前几个月不稳定,后期慢慢起来","B":"一开始就要有稳定的保底收入"},{"id":4,"part":"P1","quick":true,"question":"一笔可支配的钱,你更倾向","A":"尽量存着,心里更安稳","B":"拿出一部分去试错一个可能赚钱的新项目"},{"id":5,"part":"P1","quick":true,"question":"没人督促你的时候,你做事","A":"依然会按自己的节奏持续推进","B":"容易拖到有截止日才动手"},{"id":6,"part":"P1","quick":false,"question":"学一项新技能时,你更常","A":"需要有课程、有人带才学得进去","B":"自己主动查资料、边做边折腾会"},{"id":7,"part":"P1","quick":false,"question":"如果创业第一年只有你一个人,你更可能","A":"接受「什么都自己上手」并乐在其中","B":"觉得分身乏术、难以接受"},{"id":8,"part":"P1","quick":true,"question":"同时要写文案、谈客户、做交付,你更倾向","A":"觉得必须有人分工才专业","B":"一个人全扛下来,边做边学"},{"id":9,"part":"P1","quick":true,"question":"对待 AI 工具,你的态度更像","A":"愿意主动尝试用 AI 给自己提效","B":"觉得现用工具够用,学新的麻烦"},{"id":10,"part":"P1","quick":false,"question":"听说「用 AI 搭一套自动流程能省你每周 10 小时」,你会","A":"觉得太复杂,先放一放再说","B":"立刻想知道怎么搭、想马上用起来"},{"id":11,"part":"P1","quick":false,"question":"假设你接下来半年没有稳定收入,你","A":"有存款或其他收入兜底,能顶住","B":"会立刻陷入财务紧张"},{"id":12,"part":"P1","quick":true,"question":"你每周能稳定投入创业的时间","A":"只能挤零碎时间,很难保证","B":"能保证固定的一大块时间(如每天 2 小时以上)"},{"id":13,"part":"P2","quick":true,"question":"你更享受哪种状态","A":"和客户、同行、陌生人热络地聊想法,从中获得能量","B":"独自专注打磨一个作品,沉浸在自己的世界"},{"id":14,"part":"P2","quick":true,"question":"做内容时你更顺手","A":"直播、面对面,直接讲出来","B":"写长文、做深度研究,慢慢打磨"},{"id":15,"part":"P2","quick":false,"question":"做成第一单生意,最让你有成就感的瞬间是","A":"当面把客户聊动、顺利签下","B":"交付后客户给出真诚的认可"},{"id":16,"part":"P2","quick":true,"question":"面对一个还没人做过的全新品类,你会","A":"很兴奋,想第一个冲进去试试","B":"很谨慎,想先看有没有人验证过"},{"id":17,"part":"P2","quick":true,"question":"你更愿意把精力放在","A":"还没火起来的概念,可能成为下一个风口","B":"已经被验证、有稳定需求的市场"},{"id":18,"part":"P2","quick":false,"question":"选项目时你更看重","A":"成长空间大,哪怕当下需求还不明确","B":"现在就有明确的人付费,哪怕竞争激烈"},{"id":19,"part":"P2","quick":true,"question":"做生意的成就感更多来自","A":"帮一个客户真正解决了问题、赢得信任","B":"把流程做快、把规模做上去"},{"id":20,"part":"P2","quick":true,"question":"你更愿意卖","A":"有温度的一对一服务(咨询/陪跑/定制)","B":"标准化的产品或课程(做一次、可复制)"},{"id":21,"part":"P2","quick":false,"question":"客户提需求时,你更习惯","A":"深入了解他这个人,按他的情况定制","B":"提炼共性问题,做成标准方案复制"},{"id":22,"part":"P2","quick":true,"question":"你经营生意的方式更像","A":"有一套流程和标准,按计划推进","B":"跟着感觉和热点,随机应变"},{"id":23,"part":"P2","quick":true,"question":"面对临时变化,你更擅长","A":"迅速调整节奏、见招拆招","B":"坚持原计划,不喜欢被打乱"},{"id":24,"part":"P2","quick":false,"question":"你更愿意把一天过成","A":"有固定日程、按表推进","B":"看当天状态和机会,灵活安排"},{"id":25,"part":"P3","quick":true,"question":"「把云南在地文化/民宿做成体验生意」和「做一个知识类账号」,你更想做","A":"在地体验生意(民宿/活动/在地游)","B":"知识内容账号"},{"id":26,"part":"P3","quick":false,"question":"你更享受","A":"线下把人组织起来做体验、做活动","B":"线上完成咨询和交付"},{"id":27,"part":"P3","quick":false,"question":"连续做内容 3 个月没人看,你会","A":"愿意持续输出,相信有积累","B":"很快觉得没劲,想换个方式"},{"id":28,"part":"P3","quick":true,"question":"你更愿意做的方向","A":"打造个人 IP,靠内容吸引同频的人","B":"抓住信息差,做跨境资源对接"},{"id":29,"part":"P3","quick":false,"question":"你更愿意靠什么赚钱","A":"用专业知识和判断帮人解决问题(顾问/咨询)","B":"一家家上门服务本地商家"},{"id":30,"part":"P3","quick":true,"question":"面对「把一套方法论做成线上课程/付费产品」","A":"有信心做,享受这种价值沉淀","B":"更想做落地体验,不太想做课程"},{"id":31,"part":"P3","quick":false,"question":"你更擅长、更愿意投入","A":"钻研选品、货源、供应链这些硬功夫","B":"研究内容、创意这些软功夫"},{"id":32,"part":"P3","quick":true,"question":"「把普洱/咖啡/鲜花/特产卖向全国」和「做高端顾问」,你更愿意","A":"做特产电商,把货卖出去","B":"做高端顾问,卖专业判断"},{"id":33,"part":"P3","quick":true,"question":"面对东南亚/南亚的跨境机会,你","A":"觉得是巨大机会,愿意去闯","B":"觉得太麻烦,语言政策搞不定"},{"id":34,"part":"P3","quick":false,"question":"你更愿意做","A":"信息差/资源对接的生意(撮合、代运营)","B":"踏踏实实的在地体验生意"},{"id":35,"part":"P3","quick":true,"question":"你更愿意服务","A":"本地商家(帮他们做私域、代运营、企微)","B":"面向全国消费者的电商生意"},{"id":36,"part":"P3","quick":false,"question":"你更擅长","A":"上门谈、落地执行、当面服务","B":"远程交付、标准化输出"},{"id":37,"part":"P4","quick":true,"question":"你最有心流的时刻是","A":"把一个产品/交付打磨到极致","B":"写出、剪出一条自己满意的内容"},{"id":38,"part":"P4","quick":true,"question":"客户夸你「交付质量高」,你","A":"很受用,愿意继续钻研产品","B":"更想去谈更多客户、做更大的单"},{"id":39,"part":"P4","quick":false,"question":"你更愿意花时间在","A":"研究怎么做出世界级的好东西","B":"研究怎么把流程理顺、让重复的事自动化"},{"id":40,"part":"P4","quick":false,"question":"你的表达欲更多通过什么释放","A":"写作、视频、内容创作","B":"把想法做成产品、流程、方案"},{"id":41,"part":"P4","quick":true,"question":"你更擅长","A":"把复杂的东西讲清楚、让人愿意看","B":"想清楚整体方向、搭建全局框架"},{"id":42,"part":"P4","quick":false,"question":"让你连发 30 天内容,你","A":"有内容可发,乐在其中","B":"会发愁,更愿意做运营和优化"},{"id":43,"part":"P4","quick":false,"question":"连续被 10 个客户拒绝,你","A":"复盘一下,继续谈下一个","B":"会受挫,需要缓一缓再做"},{"id":44,"part":"P4","quick":true,"question":"你更享受","A":"主动出击找客户、把单谈下来","B":"把后台流程和交付安排得井井有条"},{"id":45,"part":"P4","quick":false,"question":"面对重复性工作,你更倾向","A":"做成流程/模板/自动化,一劳永逸","B":"每次现做,做的时候再想"},{"id":46,"part":"P4","quick":false,"question":"你更擅长","A":"把杂乱的事理出流程、安排得有条不紊","B":"产出有感染力的内容、抓住注意力"},{"id":47,"part":"P4","quick":false,"question":"面对整盘生意,你更习惯","A":"先想清楚全局,再分配各环节","B":"先埋头做自己最擅长的环节"},{"id":48,"part":"P4","quick":true,"question":"你更愿意","A":"拍板定方向、统筹资源、承担最终结果","B":"专注执行、把细节做好"},{"id":49,"part":"P5","quick":true,"question":"用 AI 时你更习惯","A":"像聊天一样和它一句句打磨,过程自己掌控","B":"一次性把任务交给它,它自己搞定"},{"id":50,"part":"P5","quick":true,"question":"你对 AI 的期待更多是","A":"当个得力助手,帮我把想法理得更好","B":"当条流水线,批量产出内容"},{"id":51,"part":"P5","quick":false,"question":"你更相信","A":"自己一步步亲手做出来的东西","B":"把活交出去(AI/外包)带来的效率"},{"id":52,"part":"P5","quick":false,"question":"你要持续做内容,你更倾向","A":"搭一套「选题→生成→发布」的自动流水线","B":"每一条都自己认真写"},{"id":53,"part":"P5","quick":true,"question":"对「AI 批量生成内容、人来把关选题」","A":"认可,就想这么干","B":"担心质量,想自己亲力亲为"},{"id":54,"part":"P5","quick":false,"question":"你更愿意把时间花在","A":"设置流程,让内容自动跑起来","B":"逐字打磨每一条内容"},{"id":55,"part":"P5","quick":false,"question":"你更愿意","A":"搭几个智能体(获客/客服/复盘)替自己跑链路","B":"自己亲手处理每一环"},{"id":56,"part":"P5","quick":false,"question":"对「把一个环节完全交给 AI 自动跑」","A":"放心,只要规则设好","B":"不放心,必须自己盯着"},{"id":57,"part":"P5","quick":true,"question":"你更享受","A":"设计一套系统,让生意自动运转","B":"亲手完成每件事的掌控感"},{"id":58,"part":"P5","quick":false,"question":"忙不过来时,你更倾向","A":"花钱、资源外包出去,自己抓重点","B":"自己加班硬扛"},{"id":59,"part":"P5","quick":true,"question":"对「AI 提效 + 外包人力 + 供应链合作」的组合打法","A":"认同,愿意去组这个局","B":"更想自己一竿子插到底"},{"id":60,"part":"P5","quick":false,"question":"你更愿意当","A":"一个调配资源、组团队的老板","B":"一个事事亲力亲为的匠人"}];
+
+export const RULES = [{"id":1,"A":"IND","B":null},{"id":2,"A":null,"B":"IND"},{"id":3,"A":"RISK","B":null},{"id":4,"A":null,"B":"RISK"},{"id":5,"A":"DRIVE","B":null},{"id":6,"A":null,"B":"DRIVE"},{"id":7,"A":"SOLO","B":null},{"id":8,"A":null,"B":"SOLO"},{"id":9,"A":"AI","B":null},{"id":10,"A":null,"B":"AI"},{"id":11,"A":"STABLE","B":null},{"id":12,"A":null,"B":"STABLE"},{"id":13,"A":"E","B":"I"},{"id":14,"A":"E","B":"I"},{"id":15,"A":"E","B":"I"},{"id":16,"A":"V","B":"G"},{"id":17,"A":"V","B":"G"},{"id":18,"A":"V","B":"G"},{"id":19,"A":"R","B":"T"},{"id":20,"A":"R","B":"T"},{"id":21,"A":"R","B":"T"},{"id":22,"A":"P","B":"F"},{"id":23,"A":"F","B":"P"},{"id":24,"A":"P","B":"F"},{"id":25,"A":"TOUR","B":"CONTENT"},{"id":26,"A":"TOUR","B":"DIGITAL"},{"id":27,"A":"CONTENT","B":"ECOMM"},{"id":28,"A":"CONTENT","B":"CROSS"},{"id":29,"A":"DIGITAL","B":"LOCAL"},{"id":30,"A":"DIGITAL","B":"TOUR"},{"id":31,"A":"ECOMM","B":"CONTENT"},{"id":32,"A":"ECOMM","B":"DIGITAL"},{"id":33,"A":"CROSS","B":"LOCAL"},{"id":34,"A":"CROSS","B":"TOUR"},{"id":35,"A":"LOCAL","B":"ECOMM"},{"id":36,"A":"LOCAL","B":"DIGITAL"},{"id":37,"A":"MAKER","B":"CREATOR"},{"id":38,"A":"MAKER","B":"HUNTER"},{"id":39,"A":"MAKER","B":"RUNNER"},{"id":40,"A":"CREATOR","B":"MAKER"},{"id":41,"A":"CREATOR","B":"ARCH"},{"id":42,"A":"CREATOR","B":"RUNNER"},{"id":43,"A":"HUNTER","B":"MAKER"},{"id":44,"A":"HUNTER","B":"RUNNER"},{"id":45,"A":"RUNNER","B":"HUNTER"},{"id":46,"A":"RUNNER","B":"CREATOR"},{"id":47,"A":"ARCH","B":"MAKER"},{"id":48,"A":"ARCH","B":"RUNNER"},{"id":49,"A":"DIALOG","B":"AGENT"},{"id":50,"A":"DIALOG","B":"AUTOCON"},{"id":51,"A":"DIALOG","B":"OUTSOURCE"},{"id":52,"A":"AUTOCON","B":"DIALOG"},{"id":53,"A":"AUTOCON","B":"AGENT"},{"id":54,"A":"AUTOCON","B":"OUTSOURCE"},{"id":55,"A":"AGENT","B":"AUTOCON"},{"id":56,"A":"AGENT","B":"DIALOG"},{"id":57,"A":"AGENT","B":"OUTSOURCE"},{"id":58,"A":"OUTSOURCE","B":"AGENT"},{"id":59,"A":"OUTSOURCE","B":"DIALOG"},{"id":60,"A":"OUTSOURCE","B":"AUTOCON"}];
+
+export const PROFILES = {"meta":{"name":"OPC 创业基因测评画像数据","version":"1.0","description":"赛道/角色/工具模式/适配等级/16人设/特质轴的完整描述数据。报告渲染与解读均依赖本文件。","pairs":{"EI":["E","I"],"VG":["V","G"],"RT":["R","T"],"PF":["P","F"]}},"axes":[{"code":"EI","name":"能量轴","left":"E","right":"I","leftLabel":"对外连接","rightLabel":"对内深耕","leftDesc":"从与人沟通、连接、谈单中获得能量;适合获客与连接型生意","rightDesc":"从独自专注、深度产出中获得能量;适合创作与产品型生意"},{"code":"VG","name":"视野轴","left":"V","right":"G","leftLabel":"探索新机会","rightLabel":"把握成熟机会","leftDesc":"喜欢新品类、新概念、可能的风口;适合内容IP/跨境/数字产品","rightDesc":"相信已验证的需求和稳定市场;适合特色电商/本地服务/文旅"},{"code":"RT","name":"价值轴","left":"R","right":"T","leftLabel":"关系服务","rightLabel":"效率交易","leftDesc":"重人、重信任、重定制化服务;适合数字咨询/文旅/本地服务","rightDesc":"重效率、规模、标准化复制;适合电商/内容IP/跨境"},{"code":"PF","name":"节奏轴","left":"P","right":"F","leftLabel":"计划系统","rightLabel":"灵活应变","leftDesc":"重 SOP、流程、自动化、可复制;适合本地服务/电商/数字产品","rightDesc":"重应变、热点、现场体验;适合内容IP/文旅/跨境"}],"personas":[{"code":"EVRP","name":"资源整合者","tagline":"组局的人","desc":"向外连接 + 敢想新机会 + 重人重信任 + 有系统。你能把不同的人和资源撮合到同一张牌桌上,天生适合做平台、生态和整合型生意。","openPath":"你的 OPC 打开方式:做「连接者」,把资源组织成平台或生态生意,让系统替你转。"},{"code":"EVRF","name":"风口捕手","tagline":"追新的人","desc":"向外连接 + 敢想新机会 + 重人重信任 + 应变快。你对新机会嗅觉灵敏,又擅长带动人,适合社群型、体验型、快节奏的新赛道。","openPath":"你的 OPC 打开方式:小步快跑做社群与体验,靠关系和热度滚动起来。"},{"code":"EVTP","name":"战略操盘手","tagline":"布局的人","desc":"向外连接 + 敢想新机会 + 重效率 + 有系统。你能把新机会拆成可复制的系统,适合做平台架构、自动化放大的生意。","openPath":"你的 OPC 打开方式:顶层设计 + 系统复制,用智能体团队把规模做上去。"},{"code":"EVTF","name":"机会猎头","tagline":"抢滩的人","desc":"向外连接 + 敢想新机会 + 重效率 + 应变快。你善于捕捉窗口期、快速变现,适合流量打法、新品类抢滩。","openPath":"你的 OPC 打开方式:小步快跑、流量打法,抓住窗口快速试错、快速迭代。"},{"code":"EGRP","name":"连锁店长","tagline":"扎根的人","desc":"向外连接 + 落地存量 + 重人重信任 + 有系统。你适合把一件事在地域内做深做透,用口碑和标准复制扩张。","openPath":"你的 OPC 打开方式:在本地把一家店、一个产品做成标准,再一家家复制。"},{"code":"EGRF","name":"现场运营家","tagline":"搞事的人","desc":"向外连接 + 落地存量 + 重人重信任 + 应变快。你把人气组织起来是天生强项,适合本地活动、体验、社群运营。","openPath":"你的 OPC 打开方式:把人组织起来搞事,用一场场活动沉淀口碑与复购。"},{"code":"EGTP","name":"系统经理人","tagline":"做标准的人","desc":"向外连接 + 落地存量 + 重效率 + 有系统。你能把本地生意做成可复制标准,适合代运营、连锁化、SOP 生意。","openPath":"你的 OPC 打开方式:把服务 SOP 化、标准化,用系统去服务更多本地商家。"},{"code":"EGTF","name":"渠道掮客","tagline":"周转的人","desc":"向外连接 + 落地存量 + 重效率 + 应变快。你擅长信息差、撮合、渠道周转,适合分销、代运营、对接型生意。","openPath":"你的 OPC 打开方式:做资源对接与渠道周转,快进快出、以小博大。"},{"code":"IVRP","name":"走心创作者","tagline":"治愈的人","desc":"对内深耕 + 探索 + 重人重信任 + 有系统。你能把深度洞察沉淀成有温度的内容与体系,适合知识 IP、陪伴型服务。","openPath":"你的 OPC 打开方式:以内容建立信任,把洞察沉淀成体系化产品。"},{"code":"IVRF","name":"灵感写手","tagline":"表达的人","desc":"对内深耕 + 探索 + 重人重信任 + 应变快。你的表达有感染力,适合自媒体、个人 IP、故事型内容生意。","openPath":"你的 OPC 打开方式:跟着灵感持续输出,把个人表达变成个人 IP。"},{"code":"IVTP","name":"深度架构师","tagline":"建体系的人","desc":"对内深耕 + 探索 + 重效率 + 有系统。你能把复杂专业做成可复制的产品体系,适合课程、数字产品、深度研究变现。","openPath":"你的 OPC 打开方式:把专业做成体系化产品,一次做透、无限复制。"},{"code":"IVTF","name":"数字游侠","tagline":"造工具的人","desc":"对内深耕 + 探索 + 重效率 + 应变快。你能一个人做出可用的数字产品,适合独立开发、AI 工具、SaaS 化小产品。","openPath":"你的 OPC 打开方式:一个人写代码做产品,用工具自动化放大自己。"},{"code":"IGRP","name":"匠心交付师","tagline":"把手艺做透的人","desc":"对内深耕 + 落地存量 + 重人重信任 + 有系统。你沉得住气把交付做精,适合定制服务、一对一深度交付。","openPath":"你的 OPC 打开方式:用品质和口碑换复购,做高客单的深度服务。"},{"code":"IGRF","name":"慢匠人","tagline":"做深的人","desc":"对内深耕 + 落地存量 + 重人重信任 + 应变快。你能把一件事越做越深,适合在地文化记录、深度内容与精品创作。","openPath":"你的 OPC 打开方式:把一件事做深做透,慢即是快。"},{"code":"IGTP","name":"标准品大师","tagline":"做模板的人","desc":"对内深耕 + 落地存量 + 重效率 + 有系统。你能把产品做成标准模板、规模复制,适合电商标准品、模板化产品。","openPath":"你的 OPC 打开方式:把产品标准化、规模化,用模板和供应链放大。"},{"code":"IGTF","name":"精密运营师","tagline":"精算的人","desc":"对内深耕 + 落地存量 + 重效率 + 应变快。你对数字和细节敏感,适合电商精细化运营、数据驱动的优化生意。","openPath":"你的 OPC 打开方式:精算每一笔账、持续用数据优化,把效率抠到极致。"}],"tracks":[{"code":"TOUR","name":"文旅体验","icon":"fa-solid fa-mountain-sun","desc":"民宿 / 定制游 / 在地体验 / 非遗文创。吃透云南文旅红利与在地资源。","why":"你偏「对外连接 + 落地存量」:重人、重现场、重口碑,适合把在地资源做成体验生意。","firstStep":"起步第一小步:选一个本地体验产品(如民宿的 1 条定制游 / 1 场非遗体验),先做线上获客。","courseRef":"课程模块一(云南生态)· 模块二(赛道选择)"},{"code":"CONTENT","name":"内容IP","icon":"fa-solid fa-clapperboard","desc":"自媒体 / 知识博主 / 个人 IP。靠表达与持续输出建立影响力和信任。","why":"你偏「探索 + 表达 + 自驱」:耐得住冷启动,适合用内容把个人变成 IP。","firstStep":"起步第一小步:选定 1 个垂直话题,连发 30 天内容,验证反馈。","courseRef":"课程模块四(AI 内容)· 模块六(流量获客)"},{"code":"DIGITAL","name":"数字咨询","icon":"fa-solid fa-brain","desc":"顾问 / 1v1 服务 / 知识产品。用专业判断帮人解决问题,沉淀可复制的产品。","why":"你偏「重人重信任 + 专业积累」:适合把能力打包成付费咨询或知识产品。","firstStep":"起步第一小步:把最擅长的一项能力,打包成一次可定价的付费咨询。","courseRef":"课程模块三(商业模式)· 模块二(赛道选择)"},{"code":"ECOMM","name":"特色电商","icon":"fa-solid fa-bag-shopping","desc":"普洱 / 咖啡 / 鲜花 / 农特产。钻研选品、货源、供应链与运营硬功夫。","why":"你偏「落地 + 效率 + 执行」:务实、能抠成本和细节,适合把云南特产卖向全国。","firstStep":"起步第一小步:选 1 个单品做货源验证,小批量上架测真实销量。","courseRef":"课程模块三(商业模式)· 模块六(流量获客)"},{"code":"CROSS","name":"跨境边贸","icon":"fa-solid fa-earth-asia","desc":"面向东南亚 / 南亚的代运营、资源对接与信息差生意,吃透 25 个边境县区位。","why":"你偏「探索 + 胆识 + 应变」:敢闯新市场、善做信息差,适合跨境红利。","firstStep":"起步第一小步:调研 1 个边境口岸/跨境渠道,找出一个具体可做的小切口。","courseRef":"课程模块一(政策)· 模块八(跨境专题)"},{"code":"LOCAL","name":"本地服务","icon":"fa-solid fa-shop","desc":"本地商家代运营 / 私域 / 企微。把标准化服务卖给本地商家,重落地与关系。","why":"你偏「落地 + 关系 + 系统」:适合上门谈、落地执行,把服务做成可复制标准。","firstStep":"起步第一小步:免费为 1 家本地商家做一次试点(如私域/企微搭建),跑通再收费。","courseRef":"课程模块六(流量获客)· 模块三(商业模式)"}],"roles":[{"code":"MAKER","name":"产品交付型","icon":"fa-solid fa-hammer","duty":"做产品、保质量、做交付","evidence":"你把产品/交付打磨到极致时最投入","split":"你的主战场在「交付」:把内容与获客交给 AI 智能体辅助,自己专注打磨可复制的交付物。"},{"code":"CREATOR","name":"内容表达型","icon":"fa-solid fa-pen-nib","duty":"创作内容、立人设、做流量","evidence":"你通过表达释放能量,连发内容不愁","split":"你的主战场在「流量」:用 AI 内容流水线放大产量,自己把握选题与调性,交付尽量标准化。"},{"code":"HUNTER","name":"获客商务型","icon":"fa-solid fa-handshake","duty":"谈单、成交、维护客户","evidence":"你把客户谈下来时最有成就感","split":"你的主战场在「转化」:让 AI 负责内容获客与线索,你集中精力成交与维护关键客户。"},{"code":"RUNNER","name":"系统运营型","icon":"fa-solid fa-gears","duty":"建流程、SOP、自动化、运营","evidence":"你擅长把杂事理成系统","split":"你的主战场在「系统」:用 Agent 自动化放大整个生意,让流程替人跑。"},{"code":"ARCH","name":"架构统筹型","icon":"fa-solid fa-puzzle-piece","duty":"顶层设计、拍板、整合资源、带 AI 团队","evidence":"你习惯先想全局、敢拍板","split":"你的主战场在「架构」:抓方向、配资源、做决策,让多智能体团队替你跑执行。"}],"toolModes":[{"code":"DIALOG","name":"对话协创型","icon":"fa-solid fa-comments","desc":"AI 当你的对话助手,人主导每一步。适合喜欢掌控过程、爱逐句打磨的你。","division":"你主导每一步,AI 帮你想、帮你写、帮你查。","agents":["起步智能体:1 个「内容协创智能体」——对话式帮你打磨选题与文案,人拍板。"],"fit":"你偏好亲手掌控,AI 是放大器而非替代者。"},{"code":"AUTOCON","name":"内容自动化型","icon":"fa-solid fa-wand-magic-sparkles","desc":"AI 批量产出内容资产,人做选题把关。适合需要持续输出的内容 IP 与电商。","division":"你定选题方向和调性,AI 批量生成初稿,你筛选发布。","agents":["起步智能体:①选题智能体 ②内容生产智能体(对齐 T04,人做最终把关)。"],"fit":"你要的是持续供给,AI 是内容流水线。"},{"code":"AGENT","name":"Agent 委托型","icon":"fa-solid fa-robot","desc":"搭智能体团队自动跑链路。适合有系统思维、想规模化的人。","division":"你设计规则,AI 自动完成获客、客服、复盘全链路。","agents":["起步智能体:获客智能体 + 客服智能体 + 复盘智能体(对齐 T04_智能体配置清单)。"],"fit":"你要的是自动运转,AI 是你的虚拟团队。"},{"code":"OUTSOURCE","name":"外包杠杆型","icon":"fa-solid fa-link","desc":"AI 提效 + 外包人力 + 供应链合作。适合重交付、想放大规模的人。","division":"AI 提效重复劳动,外包非核心环节,供应链合作放大产能,你抓核心。","agents":["起步智能体:①外包任务管理智能体 ②供应链数据智能体。"],"fit":"你要的是杠杆,AI 是撬动资源的那根杠杆。"}],"adaptLevels":[{"level":"high","range":[78,100],"label":"高适配","color":"#16a34a","verdict":"你的心态与条件相当适合一个人做 OPC,值得认真启动。","advice":["心态内核(独立/自驱/敢试错)与作战条件(单兵/AI/安全垫)都较齐备","可直接进入赛道/角色/工具模式深度推荐,全职启动"],"nextStep":"建议报名线下深度服务:2 天现场定稿方案 + 跑通智能体 + 资源对接,30 天陪跑到首单。"},{"level":"mid","range":[55,77],"label":"中适配","color":"#d97706","verdict":"你适合从副业 / 小规模慢启动,先补短板再放大。","advice":["报告中分数最低的维度是你的首要补齐项(见下方 6 维图)","先以副业形式小步验证,保住现有收入再切换"],"nextStep":"建议报名线上标准服务:2 周系统学习 + 30 天陪跑,边保主业边把生意跑起来。"},{"level":"low","range":[0,54],"label":"低适配","color":"#b91c1c","verdict":"现在不建议立即全职 OPC,先做 3 个月准备再决定。","advice":["先补安全垫:稳定收入 / 一笔启动储备金","用 30 天做一个最小实验(见推荐赛道的第一小步),用真实反馈检验意愿","练 1 项能独当一面的技能 + 学会用 AI 提效"],"nextStep":"建议从公益课建立认知开始,或先做 30 天小实验,再决定是否报名系统课程。"}],"personaOpen":"你的 OPC 打开方式","disclaimer":"本测评参考 MBTI 的自评逻辑开发,测量的是「偏好」而非「能力」,结果仅供参考,不构成任何职业或投资决策的硬性依据。创业没有标准答案,你的行动与迭代比类型更真实。","quickNote":"本结果来自快速版(30 题),仅供方向参考;如需更准确画像,建议完成完整版(60 题)。"};
+
+const RULE_MAP = {};
+RULES.forEach((r) => { RULE_MAP[r.id] = r; });
+
+export const ADAPT_DIMS = ['IND','RISK','DRIVE','SOLO','AI','STABLE'];
+export const ADAPT_LABELS = { IND:'独立自主', RISK:'风险承受', DRIVE:'自驱动力', SOLO:'单兵多面', AI:'AI 意愿', STABLE:'安全垫' };
+export const AXIS_PAIRS = [['E','I'],['V','G'],['R','T'],['P','F']];
+export const AXIS_NAMES = { EI:'能量', VG:'视野', RT:'价值', PF:'节奏' };
+export const SECTIONS = {
+ P1: '第一部分 · 创业内核(独立 / 风险 / 自驱 / 单兵 / AI / 安全垫)',
+ P2: '第二部分 · 特质倾向(能量 / 视野 / 价值 / 节奏)',
+ P3: '第三部分 · 赛道偏好(文旅 / 内容IP / 咨询 / 电商 / 跨境 / 本地)',
+ P4: '第四部分 · 角色偏向(产品 / 内容 / 商务 / 运营 / 架构)',
+ P5: '第五部分 · 人机协作(对话 / 自动化 / 智能体 / 外包)'
+};
+
+export function currentQuestions(version) {
+ return version === 'quick' ? QUESTIONS.filter((x) => x.quick) : QUESTIONS;
+}
+
+/** 计分:与设计文档一致 —— 适配指数(6维等权) + 类型码(4轴) + 赛道/角色/工具(share of votes) */
+export function calculate(answers, version = 'full') {
+ const ids = currentQuestions(version).map((x) => x.id);
+ const counts = {};
+ ids.forEach((id) => {
+ const ans = answers[id];
+ if (ans !== 'A' && ans !== 'B') return;
+ const rule = RULE_MAP[id];
+ if (!rule) return;
+ const code = ans === 'A' ? rule.A : rule.B;
+ if (code) counts[code] = (counts[code] || 0) + 1;
+ });
+
+ const adaptDims = [];
+ let adaptSum = 0;
+ ADAPT_DIMS.forEach((dim) => {
+ let max = 0;
+ ids.forEach((id) => { const r = RULE_MAP[id]; if ((r && r.A === dim) || (r && r.B === dim)) max++; });
+ const votes = counts[dim] || 0;
+ const score = max > 0 ? Math.round((votes / max) * 100) : 0;
+ adaptSum += score;
+ adaptDims.push({ code: dim, label: ADAPT_LABELS[dim], score, votes, max });
+ });
+ const adaptIndex = Math.round(adaptSum / ADAPT_DIMS.length);
+ let adaptLevel = null;
+ PROFILES.adaptLevels.forEach((lv) => { if (adaptIndex >= lv.range[0] && adaptIndex <= lv.range[1]) adaptLevel = lv; });
+ const weakestDims = [...adaptDims].sort((a, b) => a.score - b.score).slice(0, 2);
+
+ let typeCode = '';
+ const axesDetail = [];
+ AXIS_PAIRS.forEach(([left, right]) => {
+ const l = counts[left] || 0, r = counts[right] || 0, total = l + r;
+ const letter = l >= r ? left : right;
+ typeCode += letter;
+ axesDetail.push({ pair: left + right, left, right, leftPct: total > 0 ? Math.round((l / total) * 100) : 50, rightPct: total > 0 ? Math.round((r / total) * 100) : 50, winner: letter, lCount: l, rCount: r });
+ });
+ const persona = PROFILES.personas.find((x) => x.code === typeCode) || null;
+
+ function rank(dims) {
+ let total = 0;
+ dims.forEach((d) => { total += counts[d] || 0; });
+ return dims.map((d) => ({ code: d, votes: counts[d] || 0, pct: total > 0 ? Math.round(((counts[d] || 0) / total) * 100) : 0 }))
+ .sort((a, b) => b.votes - a.votes || a.code.localeCompare(b.code));
+ }
+ const tracks = rank(PROFILES.tracks.map((t) => t.code)).filter((t) => t.pct >= 10).slice(0, 3);
+ const roles = rank(PROFILES.roles.map((t) => t.code)).filter((t) => t.pct >= 5).slice(0, 2);
+ const tools = rank(PROFILES.toolModes.map((t) => t.code)).filter((t) => t.pct >= 5).slice(0, 2);
+
+ return { version, answeredCount: Object.keys(answers).filter((id) => ids.includes(Number(id))).length, typeCode, persona, adaptIndex, adaptLevel, adaptDims, weakestDims, axesDetail, tracks, roles, tools };
+}
diff --git a/website/src/data/schedule.js b/website/src/data/schedule.js
new file mode 100644
index 0000000..773d1db
--- /dev/null
+++ b/website/src/data/schedule.js
@@ -0,0 +1,29 @@
+// 课程表页数据
+// 公益课:每期一个独立主题(轮换)
+export const LEAD_THEMES = [
+ { time: '主题轮换', title: 'AI 让民宿主每月省 10 小时', desc: '公益分享 · 点到即止 · 引导报名线上/线下课' },
+ { time: '主题轮换', title: '用 AI 做云南咖啡品牌', desc: '公益分享 · 点到即止 · 引导报名线上/线下课' },
+ { time: '主题轮换', title: '一人公司财税避坑入门', desc: '公益分享 · 点到即止 · 引导报名线上/线下课' },
+ { time: '主题轮换', title: '如何把技能打包成产品卖出去', desc: '公益分享 · 点到即止 · 引导报名线上/线下课' }
+];
+
+export const ONLINE_PHASES = [
+ { phase: '诊断', time: 'D1–5', who: '填资源盘点表 + 10 张自我认知卡 + 30 分钟访谈', act: '出学员画像卡 + 诊断报告', mile: 'D5:主攻方向' },
+ { phase: '方向确认会', time: 'D5–6', who: '60 分钟参与', act: '讲透「为什么是它」(三环 + 六维)', mile: 'D6:方向确认' },
+ { phase: '体系设计', time: 'D7–10', who: '方案确认会 60 分钟', act: '定制 6 页《OPC 经营方案》', mile: 'D10:方案一版' },
+ { phase: '定稿交付', time: 'D11–14', who: '确认终稿', act: '智能体配置 + 交付资料包', mile: 'D14:方案终稿' },
+ { phase: '陪跑', time: 'D15–45', who: '执行 30 天表 + 周报', act: '每周复盘 + 答疑 + 首单推进', mile: '首单 / 进展周报' }
+];
+
+export const OFFLINE_CARDS = [
+ { title: 'Day 1 · 定方向(约 8h)', points: ['上午:开场破冰 + 现场填自我认知卡 + 快速访谈', '下午:体系设计工作坊,现场定稿定位 / 产品 / 闭环', '晚上:1 对 1 诊断 + 主攻方向确认'] },
+ { title: 'Day 2 · 做产品 + 交付(约 8h)', points: ['上午:智能体现场搭建跑通 + AI 内容实做', '下午:财务测算 + 方案定稿 + 3 分钟路演 + 30 天表签署 + 资源对接墙'] }
+];
+
+export const CADENCE = [
+ { step: '每周', title: '公益 3 天', desc: '常开一期,产出意向池' },
+ { step: '每 2 周', title: '线上 2 周', desc: '承接公益课,滚动开班' },
+ { step: '每月', title: '线下 2 天', desc: '每月 1–2 期,高客单' },
+ { step: '滚动', title: '30 天陪跑', desc: '持续运行,产出案例' },
+ { step: '每期', title: '效果卡', desc: '3 个数反哺招生与迭代' }
+];
diff --git a/website/src/data/site.js b/website/src/data/site.js
new file mode 100644
index 0000000..d91bfb2
--- /dev/null
+++ b/website/src/data/site.js
@@ -0,0 +1,46 @@
+// 站点级数据:导航 / 信任行 / 首页文案 / 统计
+// 公开导航(对外):首页 / 活动 / OPC测评 / 政策测评 / 启动流程 / 调研;内部工具在 /pine 下、不进公开导航
+export const NAV = [
+ { path: '', label: '首页' },
+ { path: 'events', label: '活动' },
+ { path: 'opc-test', label: 'O测评' },
+ { path: 'policy-test', label: '政策' },
+ { path: 'start-plan', label: '流程' },
+ { path: 'survey', label: '调研' }
+];
+
+// 内部工具导航(/pine 下,需登录)
+export const PINE_NAV = [
+ { path: 'pine', label: '工具' },
+ { path: 'pine/ops', label: '运营' },
+ { path: 'pine/system', label: '课程' },
+ { path: 'pine/structures', label: '结构' },
+ { path: 'pine/schedule', label: '课表' },
+ { path: 'pine/cards', label: '卡片' },
+ { path: 'pine/tools', label: '工具' }
+];
+
+export const TRUST = {
+ avatars: [
+ { icon: 'fa-brands fa-microsoft', cls: 'a1' },
+ { icon: 'fa-brands fa-amazon', cls: 'a2' },
+ { icon: 'fa-brands fa-google', cls: 'a3' }
+ ],
+ pill: '已陪伴 200+ 个体创业者'
+};
+
+export const HERO = {
+ headline: ['ONE PERSON', 'DESIGNED TO EVOLVE'],
+ slogan: '帮你用 AI 没什么了不起的,让你会 AI 才了不起',
+ subhead:
+ '把「教 OPC」,做成一门标准、可复制、能赚钱的 OPC。三阶服务:公益课 · 线上标准服务 · 线下深度服务——诊断、陪跑、OPC 体系设计,一次交付。',
+ cta: '立即了解',
+ ctaHref: '#/system'
+};
+
+export const STATS = [
+ { glyph: '<', target: 12, suffix: ' 期/年', decimals: 0, label: '公益课 · 每期新主题', delay: '0.5s' },
+ { glyph: '%', target: 85, suffix: '%', decimals: 0, label: '方案交付确认率', delay: '0.58s' },
+ { glyph: '*', target: 2, suffix: ' 档', decimals: 0, label: '线上 / 线下独立课', delay: '0.66s' },
+ { glyph: '#', target: 10, suffix: ' 张', decimals: 0, label: '自我认知卡片', delay: '0.74s' }
+];
diff --git a/website/src/data/structures.js b/website/src/data/structures.js
new file mode 100644
index 0000000..c0fbcca
--- /dev/null
+++ b/website/src/data/structures.js
@@ -0,0 +1,32 @@
+// 课程结构页数据
+export const COMPARISON = {
+ head: ['维度', '公益课', '线上标准服务', '线下深度服务'],
+ rows: [
+ ['周期', '每期一场(独立主题)', '2 周 + 30 天陪跑', '2 天 1 夜 + 30 天陪跑'],
+ ['价格', '免费(公益)', '¥499–1299', '¥1980–3980'],
+ ['核心交付', '主题认知(不深入)', '诊断报告 + OPC 经营方案 + 智能体配置', '现场定稿方案 + 跑通智能体 + 资源对接'],
+ ['三件套', '不含(免费分享)', '完整线上交付', '现场交付'],
+ ['产品关系', '入口(公益)', '独立付费', '独立付费'],
+ ['验收', '报名转化率 ≥ 5%', '交付确认率 ≥ 85%', '智能体运行率 ≥ 80%']
+ ]
+};
+
+export const TIER_STRUCTURES = [
+ { no: '02', name: '公益课 · 结构', en: 'One Theme Per Session', cards: [
+ { t: '开场钩子', p: '当期主题的一个真实案例 / 冲突,抓注意力。' },
+ { t: '主题认知分享', p: '讲清「值得做」+ 1 个要点,不展开操作细节。' },
+ { t: '报名引导', p: '公益分享结束,引导报名线上课或线下课。' }
+ ]},
+ { no: '04', name: '线下深度服务 · 结构', en: '2 Days', cards: [
+ { t: 'Day 1 · 定方向', p: '资源盘点 → 自我认知卡现场填 → 体系设计工作坊 → 1 对 1 诊断 + 方向确认。' },
+ { t: 'Day 2 · 做产品 + 交付', p: '智能体现场跑通 + AI 内容实做 → 财务测算 → 方案定稿 → 路演 → 30 天表 → 资源对接。' }
+ ]}
+];
+
+export const ONLINE_FLOW = [
+ { step: 'D1–5', title: '诊断', desc: '填卡 → 画像卡 → 诊断报告 → 主攻方向' },
+ { step: 'D5–6', title: '方向确认会', desc: '三环 + 六维,讲透「为什么是它」' },
+ { step: 'D7–10', title: '体系设计', desc: '定制 6 页方案 + 方案确认会' },
+ { step: 'D11–14', title: '定稿交付', desc: '智能体配置 + 资料包' },
+ { step: 'D15–45', title: '陪跑', desc: '每周复盘 + 首单推进' }
+];
diff --git a/website/src/data/system.js b/website/src/data/system.js
new file mode 100644
index 0000000..1fc1067
--- /dev/null
+++ b/website/src/data/system.js
@@ -0,0 +1,28 @@
+// 课程体系页数据
+export const TIERS = [
+ { tag: '公益', title: '公益课 · 每期独立主题', price: '免费', deliver: '主题认知(点到即止、不深入)', role: '公益分享,引导了解线上/线下服务', highlight: false },
+ { tag: '独立付费', title: '线上标准服务 · 2 周 + 30 天陪跑', price: '¥499–1299', deliver: '诊断报告 + OPC 经营方案 + 智能体配置 + 陪跑', role: '线上交付三件套,独立产品', highlight: true },
+ { tag: '独立付费', title: '线下深度服务 · 2 天 1 夜 + 30 天陪跑', price: '¥1980–3980', deliver: '现场定稿方案 + 跑通智能体 + 资源对接', role: '现场交付三件套,独立产品', highlight: false }
+];
+
+export const THREE_PIECE = [
+ { title: '① 诊断', points: ['客户填 10 张自我认知卡', '导师出诊断报告 + 学员画像卡'], out: '输出:主攻方向' },
+ { title: '② OPC 体系设计', points: ['服务方定制 6 页《OPC 经营方案》', '定位 / 产品 / 闭环 / 工具智能体 / 财务 / 30 天表'], out: '输出:可执行完整方案' },
+ { title: '③ 陪跑', points: ['30 天每周复盘 + 答疑 + 首单推进', '高频问题由智能体兜底'], out: '输出:首单 / 可验证进展' }
+];
+
+export const KIT = [
+ { title: '学员自填 · 10 张自我认知卡', points: ['资源盘点 / 价值观动机 / 能力经历 / 兴趣心流 / 生活方式目标', '现状痛点 / 恐惧障碍 / 三环验证 / 六维评分 / 赛道选择'], note: '带领学员深刻认知自己,找到最合适的赛道与 OPC 方法' },
+ { title: '导师侧 · 画像卡 + 模板', points: ['M01 画像卡:基础 / 学习风格 / 卡点 / 优势 / 赛道 / 配套教学', 'T01–T06:资源盘点 / 诊断报告 / 方案 6 页 / 智能体 / 30 天表 / 财务'], note: '让导师深入了解学员基础与现状,便于针对性配套教学' }
+];
+
+// 产品关系:公益课是入口;线上 / 线下是两个独立产品
+export const ENTRY = [
+ { step: '公益', title: '公益课', desc: '免费 · 每期独立主题 · 内容点到即止' },
+ { step: '引导', title: '报名入口', desc: '公益分享末尾引导报名线上课或线下课' }
+];
+
+export const INDEPENDENT = [
+ { title: '线上标准服务(独立)', price: '¥499–1299', desc: '线上交付三件套:诊断 + 陪跑 + OPC 体系设计', points: ['2 周交付 + 30 天陪跑', '可单独报名'] },
+ { title: '线下深度服务(独立)', price: '¥1980–3980', desc: '现场交付三件套:定稿方案 + 跑通智能体 + 资源对接', points: ['2 天 1 夜 + 30 天陪跑', '可单独报名'] }
+];
diff --git a/website/src/data/timetable.js b/website/src/data/timetable.js
new file mode 100644
index 0000000..952c20f
--- /dev/null
+++ b/website/src/data/timetable.js
@@ -0,0 +1,90 @@
+// 详细课时课程表:线上课 18 课时(60min/课时)、线下课 20 课时(45min/课时)
+// 重心:工具 / 智能体 / 技能 / 实操训练;商业逻辑点到即止。
+
+export const ONLINE_HOURS = {
+ label: '线上标准服务 · 18 课时(60 分钟/课时)',
+ note: '重心在工具、智能体、技能与实操:诊断 5 课时 → 工具技能 5 课时 → 智能体实操 4 课时 → 交付 1 课时 → 陪跑 4 次复盘。商业逻辑仅作点到即止的确认。',
+ sections: [
+ {
+ phase: '阶段一 · 认知与诊断(D1–D5,5 课时)',
+ head: ['课时', '时间', '主题', '内容', '产出'],
+ rows: [
+ ['1', 'D1', '开营 + 资源盘点', '课程说明、填《资源盘点表》、实操环境准备', '资源盘点表'],
+ ['2', 'D2', '自我认知 01–05', '发掘自己:资源/价值观/能力/兴趣/生活目标 引导填写', '卡 01–05'],
+ ['3', 'D3', '自我认知 06–10', '找赛道:痛点/恐惧/三环/六维/赛道选择 引导填写', '卡 06–10'],
+ ['4', 'D4', '方向确认(轻)', '三环 + 六维一句话确认主攻方向(点到即止,不深讲商业模型)', '方向确认'],
+ ['5', 'D5', '个人访谈 + 画像卡', '每人 30 分钟 1 对 1;导师汇总 M01 学员画像卡', 'M01 画像卡']
+ ]
+ },
+ {
+ phase: '阶段二 · 工具与技能(D6–D10,5 课时)',
+ head: ['课时', '时间', '主题', '内容', '产出'],
+ rows: [
+ ['6', 'D6', '对话模型 + Prompt 技能', '角色/任务/背景/约束/输出 五要素;商业 Prompt 模板实操', '3 条文案'],
+ ['7', 'D7', '生图 + 生视频实操', '海报生成、剪映口播/批量剪辑', '海报 + 脚本'],
+ ['8', 'D8', '代码与自动化', 'Claude Code / Codex 零基础脚本:数据处理、定时发布', '自动化脚本'],
+ ['9', 'D9', '内容实做', '1 文案 + 1 海报 + 1 短视频脚本(用 Prompt 库 P1–P16)', '内容三件'],
+ ['10', 'D10', '工具栈选型', '按赛道配对话/生图/生视频/代码/云算力工具栈', '工具栈清单']
+ ]
+ },
+ {
+ phase: '阶段三 · 智能体与实操(D11–D14,5 课时)',
+ head: ['课时', '时间', '主题', '内容', '产出'],
+ rows: [
+ ['11', 'D11', '智能体原理 + 获客智能体', '目标→拆解→执行→评估→迭代;配置获客智能体并跑通', '获客智能体'],
+ ['12', 'D12', '客服 / 复盘智能体', '系统指令 + 话术库 + 转人工规则;五维质检', '客服 + 复盘智能体'],
+ ['13', 'D13', '多智能体协同 + MCP', '编排图、自动化链路打通(触发→多步→产出)', '自动化链路'],
+ ['14', 'D14', '实操整合 + 交付', '内容 + 智能体 + 工具栈合稿,30 天表签署,交付资料包', '交付包 + 30 天表']
+ ]
+ },
+ {
+ phase: '阶段四 · 陪跑(D15–D45,4 次复盘课时)',
+ head: ['课时', '时间', '主题', '内容', '产出'],
+ rows: [
+ ['15', '第 1 周', '陪跑复盘 1 · 落地', '首条内容上线、智能体实际运行', '周报 1'],
+ ['16', '第 2 周', '陪跑复盘 2 · 获客', '内容数据复盘、工具调优', '周报 2'],
+ ['17', '第 3 周', '陪跑复盘 3 · 转化', '成交话术演练、促成首单', '首单 / 进展'],
+ ['18', '第 4 周', '陪跑复盘 4 · 放大', '复盘智能体跑一遍,放大与下月计划', '周报 4 + 计划']
+ ]
+ }
+ ]
+};
+
+export const OFFLINE_HOURS = {
+ label: '线下深度服务 · 20 课时(45 分钟/课时)',
+ note: '重心在动手:认知与诊断 5 课时 → 工具技能 6 课时 → 智能体与实操 6 课时 → 整合交付 3 课时。商业逻辑点到即止。',
+ sections: [
+ {
+ phase: 'Day 1 · 认知 + 诊断(10 课时)',
+ head: ['课时', '时间', '主题', '内容', '现场产出'],
+ rows: [
+ ['1', '09:00–09:45', '开场破冰', '导师介绍、学员 1 分钟自我介绍、按赛道分组', '分组完成'],
+ ['2', '09:45–10:30', '什么是 OPC(点到)', 'OPC 定义、AI 时代机遇(不展开商业理论)', '认知建立'],
+ ['3', '10:45–11:30', '现场自我认知 01–05', '填资源/价值观/能力/兴趣/生活目标卡 + 快速访谈', '卡 01–05'],
+ ['4', '11:30–12:15', '现场自我认知 06–10', '填痛点/恐惧/三环/六维/赛道选择卡', '卡 06–10'],
+ ['5', '14:00–14:45', 'AI 工具速览演示', '对话 / 生图 / 生视频 / 代码 四类模型现场演示', '工具认知'],
+ ['6', '14:45–15:30', 'Prompt 技能实操', '五要素 Prompt、现场生成 1 条文案', '文案初稿'],
+ ['7', '15:45–16:30', '方向确认(轻)', '六维一句话确认主攻方向(点到即止)', '方向确认'],
+ ['8', '16:30–17:15', '1 对 1 诊断', '导师用 M01 汇总当日信息、方向确认', '画像卡初稿'],
+ ['9', '19:30–20:15', '工具跟练 1 · 生图', '产品主图 / 海报现场生成', '海报'],
+ ['10', '20:15–21:00', '工具跟练 2 · 生视频', '短视频脚本 + 口播生成', '视频脚本']
+ ]
+ },
+ {
+ phase: 'Day 2 · 实操 + 智能体 + 交付(10 课时)',
+ head: ['课时', '时间', '主题', '内容', '现场产出'],
+ rows: [
+ ['11', '09:00–09:45', '内容实做', '1 文案 + 1 海报 + 1 短视频脚本 合体', '内容三件'],
+ ['12', '09:45–10:30', '代码与自动化实操', 'Claude Code / Codex 零基础脚本', '自动化脚本'],
+ ['13', '10:45–11:30', '智能体搭建 1', '获客智能体配置并跑通', '智能体 1'],
+ ['14', '11:30–12:15', '智能体搭建 2', '客服 / 复盘智能体配置并跑通', '智能体 2–3'],
+ ['15', '14:00–14:45', '多智能体协同 + MCP', '编排图、自动化链路打通', '链路跑通'],
+ ['16', '14:45–15:30', '实操整合', '内容 + 智能体 + 工具栈合体成交付包', '交付包'],
+ ['17', '15:45–16:30', '路演准备', '交付包整合、3 分钟路演演练', '路演稿'],
+ ['18', '16:30–17:15', '结业路演', '每人 3 分钟路演,导师 + 学员互评', '路演评审'],
+ ['19', '17:15–17:45', '30 天表签署 + 点评', '签署 30 天执行表、导师点评', '30 天表'],
+ ['20', '17:45–18:00', '结营 + 资源对接墙', '颁证、资源配对、建陪跑群', '结业']
+ ]
+ }
+ ]
+};
diff --git a/website/src/data/tools.js b/website/src/data/tools.js
new file mode 100644
index 0000000..432bdb8
--- /dev/null
+++ b/website/src/data/tools.js
@@ -0,0 +1,28 @@
+// 工具页数据:六维评分 + 财务测算 字段配置
+export const NICHE_DIMS = [
+ { key: 'market', label: '市场(需求强度/规模)', weight: 20 },
+ { key: 'compet', label: '竞争(越低越好)', weight: 15 },
+ { key: 'revenue', label: '收益(客单×复购×利润)', weight: 20 },
+ { key: 'fit', label: '匹配度(资源/兴趣/能力)', weight: 25 },
+ { key: 'growth', label: '成长性(是否持续增长)', weight: 10 },
+ { key: 'risk', label: '风险(政策/合规/淡旺季)', weight: 10 }
+];
+
+export const FINANCE_FIELDS = [
+ { key: 'price', label: '客单价(元)', placeholder: '如 800' },
+ { key: 'cost', label: '单位成本(元)', placeholder: '如 300' },
+ { key: 'fixed', label: '月固定成本(元)', placeholder: '如 3000' },
+ { key: 'income', label: '月预计收入(元)', placeholder: '如 12000' },
+ { key: 'expense', label: '月预计支出(元)', placeholder: '如 7000' }
+];
+
+export const RESOURCE_DIMS = [
+ { key: 'skill', label: '技能' },
+ { key: 'time', label: '时间' },
+ { key: 'money', label: '资金' },
+ { key: 'network', label: '人脉' },
+ { key: 'asset', label: '资源' },
+ { key: 'exp', label: '经验' },
+ { key: 'interest', label: '兴趣' },
+ { key: 'persona', label: '性格' }
+];
diff --git a/website/src/main.jsx b/website/src/main.jsx
new file mode 100644
index 0000000..0f3dc63
--- /dev/null
+++ b/website/src/main.jsx
@@ -0,0 +1,11 @@
+import React from 'react';
+import { createRoot } from 'react-dom/client';
+import App from './App';
+import './styles/tokens.css';
+import './styles/global.css';
+
+createRoot(document.getElementById('root')).render(
+
+
+
+);
diff --git a/website/src/platform/Cards.jsx b/website/src/platform/Cards.jsx
new file mode 100644
index 0000000..9fd777d
--- /dev/null
+++ b/website/src/platform/Cards.jsx
@@ -0,0 +1,64 @@
+import React from 'react';
+import { ContentTemplate } from '@/components/templates';
+import { SectionHead, Card } from '@/components/atoms';
+import { CardGrid, DataTable } from '@/components/molecules';
+import { Reveal } from '@/components/organisms';
+import { SELF_CARDS_A, SELF_CARDS_B, MENTOR_MODULES, CORE_TEMPLATES } from '@/data/cards';
+
+function SelfCard({ c }) {
+ return (
+
+ {c.desc}
价值:{c.value}
+
+ );
+}
+
+export default function Cards() {
+ return (
+
+
+
+
+ {SELF_CARDS_A.map((c, i) => )}
+
+ 每张含「设计思路 / 目的 / 逐项介绍 / 产出 / 填写价值 / 标准参考」六要素,配套文档见 自我认知工具/。
+
+
+
+
+
+
+
+ {SELF_CARDS_B.map((c, i) => )}
+
+ 08–10 可在「工具」页直接在线填写并自动加权计算,结果可打印。
+
+
+
+
+
+
+ [m.k, m.v])}
+ />
+
+
+
+
+
+ {CORE_TEMPLATES.map((t, i) => (
+
+ {t.desc}
+
+ ))}
+
+
+
+ );
+}
diff --git a/website/src/platform/Login.jsx b/website/src/platform/Login.jsx
new file mode 100644
index 0000000..3c793c9
--- /dev/null
+++ b/website/src/platform/Login.jsx
@@ -0,0 +1,50 @@
+import React from 'react';
+import { login } from '@/services/auth';
+import '@/styles/auth.css';
+
+/**
+ * 登录门(内部工具 /pine 守卫)
+ * 不进任何导航/按钮,仅由访问 /pine 路由且未登录时触发。
+ * props.target:登录成功后要回到的内部路径(如 'pine/tools')。
+ */
+export default function Login({ target = 'pine' }) {
+ const [username, setUsername] = React.useState('');
+ const [password, setPassword] = React.useState('');
+ const [error, setError] = React.useState('');
+ const [busy, setBusy] = React.useState(false);
+
+ const submit = async (e) => {
+ e.preventDefault();
+ setBusy(true);
+ setError('');
+ try {
+ await login(username.trim(), password);
+ window.location.hash = '#/' + target;
+ } catch (err) {
+ setError(err.message || '登录失败');
+ } finally {
+ setBusy(false);
+ }
+ };
+
+ return (
+
+ );
+}
diff --git a/website/src/platform/PineBookings.jsx b/website/src/platform/PineBookings.jsx
new file mode 100644
index 0000000..f14fab7
--- /dev/null
+++ b/website/src/platform/PineBookings.jsx
@@ -0,0 +1,113 @@
+import React from 'react';
+import { ContentTemplate } from '@/components/templates';
+import { SectionHead } from '@/components/atoms';
+import { Reveal } from '@/components/organisms';
+import { ops } from '@/services/ops';
+import { useOps, OpsState, StatusBadge, BOOKING_STATUSES, fmtDate, downloadCSV } from '@/components/pineAdmin';
+import '@/styles/pineAdmin.css';
+
+const FILTERS = [
+ { key: 'all', label: '全部' },
+ { key: 'pending', label: '待确认' },
+ { key: 'confirmed', label: '已确认' },
+ { key: 'arrived', label: '已到场' },
+ { key: 'converted', label: '已转化' }
+];
+const STATUS_LABEL = { pending: '待确认', confirmed: '已确认', arrived: '已到场', converted: '已转化' };
+const AUDIT_LABEL = { pending: '审核中', approved: '已通过', rejected: '未通过' };
+const AUDIT_COLOR = { pending: '#f5c451', approved: '#3fb68b', rejected: '#f07575' };
+
+export default function PineBookings() {
+ const { data, err, loading, reload } = useOps(ops.bookings);
+ const [filter, setFilter] = React.useState('all');
+ const [msg, setMsg] = React.useState('');
+
+ const list = (data && data.list) || [];
+ const shown = filter === 'all' ? list : list.filter((b) => b.status === filter);
+
+ const flash = (text) => { setMsg(text); setTimeout(() => setMsg(''), 2500); };
+
+ const changeStatus = async (b, status) => {
+ try { await ops.updateBooking(b.id, status); flash(`已将 ${b.name} 标记为「${STATUS_LABEL[status]}」`); reload(); }
+ catch (e) { flash(e.message || '更新失败'); }
+ };
+ /* 报名审核:批准 / 拒绝 */
+ const audit = async (b, auditStatus) => {
+ try { await ops.updateBookingAudit(b.id, auditStatus); flash(`已将「${b.name}」的报名${auditStatus === 'approved' ? '通过' : '拒绝'}`); reload(); }
+ catch (e) { flash(e.message || '更新失败'); }
+ };
+ const del = async (b) => {
+ if (!window.confirm(`删除 ${b.name} 的预约记录?`)) return;
+ try { await ops.deleteBooking(b.id); flash('已删除'); reload(); }
+ catch (e) { flash(e.message || '删除失败'); }
+ };
+ const exportCsv = () => {
+ downloadCSV(
+ '预约与报名.csv',
+ ['创建时间', '姓名', '联系方式', '职业', '想预约', '感兴趣主题', '想解决的问题', '来源', '状态'],
+ list.map((b) => [fmtDate(b.createdAt), b.name, b.contact, b.statusLabel, b.want, (b.topics || []).join('、'), b.question, b.source, STATUS_LABEL[b.status] || b.status])
+ );
+ };
+
+ return (
+
+ {msg && {msg}
}
+
+
+
+
+ {FILTERS.map((f) => (
+
+ ))}
+
+
+
+
+
+
+
+
+ | 姓名 | 账号 | 联系方式 | 活动 | 职业 | 来源 | 提交时间 | 报名状态 | 审核 | 操作 |
+
+
+
+ {shown.map((b) => (
+
+ | {b.name} |
+ {b.username || '—'} |
+ {b.contact} |
+ {b.eventTitle || b.want || '—'} |
+ {b.statusLabel || '—'} |
+ {b.source || '—'} |
+ {fmtDate(b.createdAt)} |
+
+
+ {' '}
+ |
+
+ {AUDIT_LABEL[b.auditStatus] || '—'}
+
+
+
+
+ |
+ |
+
+ ))}
+ {shown.length === 0 && | 暂无记录 |
}
+
+
+
+ {list.some((x) => x.question) && 备注示例:{list.find((x) => x.question)?.name} —— {list.find((x) => x.question)?.question}
}
+
+
+
+ );
+}
diff --git a/website/src/platform/PineEvents.jsx b/website/src/platform/PineEvents.jsx
new file mode 100644
index 0000000..081b6fa
--- /dev/null
+++ b/website/src/platform/PineEvents.jsx
@@ -0,0 +1,187 @@
+import React from 'react';
+import { ContentTemplate } from '@/components/templates';
+import { SectionHead } from '@/components/atoms';
+import { Reveal } from '@/components/organisms';
+import { ops, uploadImage } from '@/services/ops';
+import { useOps, OpsState, fmtDate, toLocalInput, fromLocalInput } from '@/components/pineAdmin';
+import '@/styles/pineAdmin.css';
+
+const TYPE_LABEL = { free: '公益课', salon: '沙龙' };
+const MODE_LABEL = { online: '线上', offline: '线下' };
+const STATUS_LABEL = { open: '报名中', full: '已满员', done: '已结束', pending: '待开放', invite: '邀请中' };
+const EMPTY = { type: 'free', mode: 'offline', title: '', subtitle: '', desc: '', location: '', host: '', image: '', link: '', startAt: '', endAt: '', checkinAt: '', durationMin: 90, capacity: '', status: 'open', auditMode: 'auto', showCapacity: false };
+
+export default function PineEvents() {
+ const { data, err, loading, reload } = useOps(ops.events);
+ const [form, setForm] = React.useState(EMPTY);
+ const [editingId, setEditingId] = React.useState(null);
+ const [msg, setMsg] = React.useState('');
+ const [uploading, setUploading] = React.useState(false);
+
+ const list = (data && data.list) || [];
+ const flash = (t) => { setMsg(t); setTimeout(() => setMsg(''), 2500); };
+ const set = (k) => (v) => setForm((f) => ({ ...f, [k]: v }));
+
+ /* 上传封面图 */
+ const onUpload = async (e) => {
+ const file = e.target.files && e.target.files[0];
+ if (!file) return;
+ setUploading(true);
+ try {
+ const r = await uploadImage(file);
+ if (r && r.url) { set('image')(r.url); flash('封面已上传'); }
+ } catch (ex) { flash(ex.message || '上传失败'); }
+ finally { setUploading(false); }
+ };
+
+ const startEdit = (e) => {
+ setEditingId(e.id);
+ setForm({ type: e.type, mode: e.mode || 'offline', title: e.title, subtitle: e.subtitle || '', desc: e.desc || '', location: e.location || '', host: e.host || '', image: e.image || '', link: e.link || '', startAt: toLocalInput(e.startAt), endAt: toLocalInput(e.endAt), checkinAt: toLocalInput(e.checkinAt), durationMin: e.durationMin || 90, capacity: e.capacity || 0, status: e.status || 'open', auditMode: e.auditMode || 'auto', showCapacity: !!e.showCapacity });
+ };
+ const cancelEdit = () => { setEditingId(null); setForm(EMPTY); };
+
+ const save = async () => {
+ try {
+ const payload = { ...form, startAt: fromLocalInput(form.startAt), endAt: fromLocalInput(form.endAt), checkinAt: fromLocalInput(form.checkinAt), showCapacity: form.showCapacity ? 1 : 0 };
+ if (editingId) { await ops.updateEvent(editingId, payload); flash('已更新排期'); }
+ else { await ops.createEvent(payload); flash('已新增排期,首页即时生效'); }
+ cancelEdit(); reload();
+ } catch (e) { flash(e.message || '保存失败'); }
+ };
+ const del = async (e) => {
+ if (!window.confirm(`删除「${e.title}」?`)) return;
+ try { await ops.deleteEvent(e.id); flash('已删除'); reload(); }
+ catch (ex) { flash(ex.message || '删除失败'); }
+ };
+
+ return (
+
+ {msg && {msg}
}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ set('title')(e.target.value)} placeholder="例:AI 让民宿主每月省 10 小时" />
+
+
+ set('subtitle')(e.target.value)} placeholder="例:民宿 / 文旅创业 · 主题分享" />
+
+
+ set('startAt')(e.target.value)} />
+
+
+ set('endAt')(e.target.value)} />
+
+
+ set('checkinAt')(e.target.value)} />
+
+
+ set('durationMin')(e.target.value)} />
+
+
+ set('location')(e.target.value)} placeholder="线上直播 / 昆明市大学生创业园" />
+
+
+ set('host')(e.target.value)} placeholder="例:云超服 · OPC 培训" />
+
+
+ set('capacity')(e.target.value)} placeholder="例:30" />
+
+
+
+
+
+
+ set('link')(e.target.value)} placeholder="https://… 或留空" />
+
+
+
+
+
+
+ {editingId && }
+
+
+
+
+
+
+
+
+
+
+ | 类型 | 形式 | 主题 | 时间 | 报名 | 地点 | 状态 | 操作 |
+
+
+ {list.slice().sort((a, b) => new Date(a.startAt) - new Date(b.startAt)).map((e) => (
+
+ | {TYPE_LABEL[e.type]} |
+ {MODE_LABEL[e.mode] || (e.mode === 'online' ? '线上' : '线下')} |
+ {e.title} {e.subtitle} |
+ {fmtDate(e.startAt)}
+ {e.endAt && 止 {fmtDate(e.endAt)} }
+ {e.checkinAt && 签到 {fmtDate(e.checkinAt)} }
+ |
+
+ {e.showCapacity ? (e.capacity ? `${e.enrolled} / ${e.capacity}` : `${e.enrolled} 人`) : '—'}
+ {e.auditMode === 'manual' ? '· 手动审' : ''}
+ |
+ {e.location || '—'} |
+ {STATUS_LABEL[e.status] || e.status} |
+
+ {' '}
+
+ |
+
+ ))}
+ {list.length === 0 && | 暂无排期,先在上面新增 |
}
+
+
+
+
+
+
+ );
+}
diff --git a/website/src/platform/PineHome.jsx b/website/src/platform/PineHome.jsx
new file mode 100644
index 0000000..0fda8d8
--- /dev/null
+++ b/website/src/platform/PineHome.jsx
@@ -0,0 +1,61 @@
+import React from 'react';
+import { ContentTemplate } from '@/components/templates';
+import { SectionHead } from '@/components/atoms';
+import { Reveal } from '@/components/organisms';
+import { logout } from '@/services/auth';
+import '@/styles/auth.css';
+
+const ADMIN_TOOLS = [
+ { href: '#/pine/ops', icon: 'fa-solid fa-gauge-high', title: '运营管理总览', desc: '数据概览 + 管理入口(后端管理主入口)', accent: true },
+ { href: '#/pine/bookings', icon: 'fa-solid fa-clipboard-list', title: '预约 / 报名管理', desc: '公益课报名 + 沙龙预约名单,状态流转 / 删除 / 导出' },
+ { href: '#/pine/events', icon: 'fa-solid fa-calendar-days', title: '课程 / 沙龙排期', desc: '公益课 + 沙龙排期增删改,首页当期排期即时生效' },
+ { href: '#/pine/tests', icon: 'fa-solid fa-flask', title: 'OPC 测评记录', desc: '测评结果(类型码 / 适配指数 / 赛道)汇总' }
+];
+
+const PINE_TOOLS = [
+ { href: '#/pine/system', icon: 'fa-solid fa-map', title: '课程体系', desc: '三阶标准服务产品体系(公益 / 线上 / 线下)' },
+ { href: '#/pine/structures', icon: 'fa-solid fa-cubes', title: '课程结构', desc: '模块与课时结构、课程大纲' },
+ { href: '#/pine/schedule', icon: 'fa-solid fa-calendar-days', title: '课程表', desc: '线上 18 课时 / 线下 20 课时详细排期' },
+ { href: '#/pine/cards', icon: 'fa-solid fa-layer-group', title: '卡片', desc: '10 张自我认知卡 + 导师画像卡' },
+ { href: '#/pine/tools', icon: 'fa-solid fa-toolbox', title: '工具', desc: '核心模板 T01–T06 表单 + 报名/满意度问卷' }
+];
+
+export default function PineHome() {
+ const doLogout = () => { logout(); window.location.hash = '#/'; };
+ return (
+
+
+
+
+
+
+
+
+
+
+ 使用完毕后请退出:
+ 退出登录
+
+
+
+ );
+}
diff --git a/website/src/platform/PineLogs.jsx b/website/src/platform/PineLogs.jsx
new file mode 100644
index 0000000..17fc811
--- /dev/null
+++ b/website/src/platform/PineLogs.jsx
@@ -0,0 +1,165 @@
+import React from 'react';
+import { ContentTemplate } from '@/components/templates';
+import { SectionHead } from '@/components/atoms';
+import { Reveal } from '@/components/organisms';
+import { ops, getPolicyQuestions, getPlanConfig, getSurveyQuestions } from '@/services/ops';
+import { useOps, OpsState, fmtDate, downloadCSV } from '@/components/pineAdmin';
+import '@/styles/pineAdmin.css';
+
+/* 题目/流程数据改由后端下发(数据由后端管理) */
+let PT_Q = [];
+let SP_ST = [];
+let SV_Q = [];
+
+/* 把政策测评的 value 答案映射为可读标签 */
+const ansLabel = (a) => {
+ if (!a) return '—';
+ const q = Object.fromEntries(PT_Q.map((x) => [x.id, x]));
+ const pick = (id, v) => (q[id] && q[id].options.find((o) => o.value === v)?.label) || v;
+ const parts = [];
+ if (a.status) parts.push(pick('status', a.status));
+ if (a.industry && a.industry.length) parts.push((a.industry || []).map((v) => pick('industry', v)).join('、'));
+ if (a.region) parts.push(pick('region', a.region));
+ return parts.filter(Boolean).join(' · ');
+};
+
+/* 调研答案 value → 可读标签 */
+const surveyLabel = (qid, v) => {
+ const q = SV_Q.find((x) => x.id === qid);
+ if (!q) return v;
+ if (Array.isArray(v)) return v.map((x) => q.options?.find((o) => o.value === x)?.label || x).join('、');
+ return q.options?.find((o) => o.value === v)?.label || v;
+};
+
+export default function PineLogs() {
+ const policy = useOps(ops.policyLogs);
+ const plan = useOps(ops.planLogs);
+ const survey = useOps(ops.surveyLogs);
+ const pList = (policy.data && policy.data.list) || [];
+ const lList = (plan.data && plan.data.list) || [];
+ const sList = (survey.data && survey.data.list) || [];
+
+ // 题目/流程数据由后端下发(供答案做可读标签映射)
+ React.useEffect(() => {
+ getPolicyQuestions().then((q) => { PT_Q = q; });
+ getPlanConfig().then((r) => { if (r && r.status) SP_ST = r.status; });
+ getSurveyQuestions().then((r) => { if (r && r.questions) SV_Q = r.questions; });
+ }, []);
+
+ const exportSurvey = () => downloadCSV(
+ 'OPC创业伙伴调研.csv',
+ ['时间', '来源', '状态', '行业', '真实AI赋能', '最需要政府支持', '最看重平台服务', '是否愿参加活动'],
+ sList.map((s) => {
+ const a = s.answers || {};
+ return [fmtDate(s.createdAt), s.source, surveyLabel('s1', a.s1), surveyLabel('s6', a.s6), surveyLabel('s21', a.s21), surveyLabel('s30', a.s30), surveyLabel('s36', a.s36), surveyLabel('s31', a.s31)];
+ })
+ );
+
+ const exportPolicy = () => downloadCSV(
+ '政策测评记录.csv',
+ ['时间', '账号', '选择摘要', '政策数', '补贴数', '贷款数', '摘要'],
+ pList.map((p) => [fmtDate(p.createdAt), p.username, ansLabel(p.answers), p.policiesCount, p.subsidiesCount, p.loansCount, p.summary])
+ );
+ const exportPlan = () => downloadCSV(
+ '启动流程记录.csv',
+ ['时间', '账号', '地区', '状态', '入驻园区', '社保', '步骤数'],
+ lList.map((l) => [fmtDate(l.createdAt), l.username, l.region, SP_ST.find((s) => s.value === l.status)?.label || l.status, l.needPark ? '是' : '否', l.hasStaff ? '是' : '否', l.stepsCount])
+ );
+
+ return (
+
+
+
+
+
+
+
+
+
+ | 时间 | 账号 | 选择摘要 | 政策 | 补贴 | 贷款 | 摘要 |
+
+ {pList.map((p) => (
+
+ | {fmtDate(p.createdAt)} |
+ {p.username || '—'} |
+ {ansLabel(p.answers)} |
+ {p.policiesCount} |
+ {p.subsidiesCount} |
+ {p.loansCount} |
+ {p.summary} |
+
+ ))}
+ {pList.length === 0 && | 暂无政策测评记录 |
}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | 时间 | 账号 | 地区 | 状态 | 入驻园区 | 社保 | 步骤数 |
+
+ {lList.map((l) => (
+
+ | {fmtDate(l.createdAt)} |
+ {l.username || '—'} |
+ {l.region} |
+ {SP_ST.find((s) => s.value === l.status)?.label || l.status} |
+ {l.needPark ? '是' : '否'} |
+ {l.hasStaff ? '是' : '否'} |
+ {l.stepsCount} |
+
+ ))}
+ {lList.length === 0 && | 暂无启动流程记录 |
}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | 时间 | 来源 | 状态 | 行业 | 真实AI赋能 | 最需要政府支持 | 最看重平台服务 | 愿参加 |
+
+ {sList.map((s) => {
+ const a = s.answers || {};
+ return (
+
+ | {fmtDate(s.createdAt)} |
+ {s.source || 'web'} |
+ {surveyLabel('s1', a.s1)} |
+ {surveyLabel('s6', a.s6)} |
+ {surveyLabel('s21', a.s21)} |
+ {surveyLabel('s30', a.s30)} |
+ {surveyLabel('s36', a.s36)} |
+ {surveyLabel('s31', a.s31)} |
+
+ );
+ })}
+ {sList.length === 0 && | 暂无调研记录 |
}
+
+
+
+
+
+
+ );
+}
diff --git a/website/src/platform/PineOps.jsx b/website/src/platform/PineOps.jsx
new file mode 100644
index 0000000..5fe9748
--- /dev/null
+++ b/website/src/platform/PineOps.jsx
@@ -0,0 +1,62 @@
+import React from 'react';
+import { ContentTemplate } from '@/components/templates';
+import { SectionHead } from '@/components/atoms';
+import { Reveal } from '@/components/organisms';
+import { ops } from '@/services/ops';
+import { useOps, StatCard, OpsState } from '@/components/pineAdmin';
+import '@/styles/pineAdmin.css';
+
+const ADMIN_LINKS = [
+ { href: '#/pine/bookings', icon: 'fa-solid fa-clipboard-list', title: '预约与报名管理', desc: '公益课报名 / 沙龙预约名单,改状态、删除、导出 CSV', accent: true },
+ { href: '#/pine/events', icon: 'fa-solid fa-calendar-days', title: '课程 / 沙龙排期', desc: '公益课 + 沙龙排期增删改,首页当期排期即时生效' },
+ { href: '#/pine/tests', icon: 'fa-solid fa-flask', title: 'OPC 测评记录', desc: '用户测评结果(类型码 / 适配指数 / 赛道)' },
+ { href: '#/pine/logs', icon: 'fa-solid fa-file-lines', title: '政策与流程记录', desc: '用户政策测评 / 启动流程使用记录,观察热点' },
+ { href: '#/pine/report', icon: 'fa-solid fa-chart-pie', title: '项目汇报展示', desc: '课程体系 → 商业模式 → 政策 → 数据看板(对政府 / 合作方演示)', accent: true }
+];
+
+export default function PineOps() {
+ const { data, err, loading } = useOps(ops.stats);
+ const s = (data && data.stats) || {};
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 共 {s.tests ?? '—'} 条测评记录 · {s.upcomingEvents ?? '—'} 场未来活动 · 数据落盘 server/
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/website/src/platform/PineTests.jsx b/website/src/platform/PineTests.jsx
new file mode 100644
index 0000000..64a01c4
--- /dev/null
+++ b/website/src/platform/PineTests.jsx
@@ -0,0 +1,62 @@
+import React from 'react';
+import { ContentTemplate } from '@/components/templates';
+import { SectionHead } from '@/components/atoms';
+import { Reveal } from '@/components/organisms';
+import { ops } from '@/services/ops';
+import { useOps, OpsState, fmtDate, downloadCSV } from '@/components/pineAdmin';
+import '@/styles/pineAdmin.css';
+
+const LEVEL_COLOR = (l) => (l === 'high' ? '#a8f0c8' : l === 'mid' ? '#f5d9a0' : '#f0b8b8');
+const LEVEL_LABEL = { high: '高适配', mid: '中适配', low: '低适配' };
+
+export default function PineTests() {
+ const { data, err, loading } = useOps(ops.tests);
+ const list = (data && data.list) || [];
+
+ const exportCsv = () => {
+ downloadCSV(
+ 'OPC测评记录.csv',
+ ['时间', '类型码', '人设', '适配指数', '适配档', '赛道', '版本'],
+ list.map((t) => [fmtDate(t.createdAt), t.typeCode, t.persona, t.adaptIndex, LEVEL_LABEL[t.adaptLevel] || t.adaptLevel, (t.tracks || []).join('、'), t.version])
+ );
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ | 类型码 | 人设 | 适配指数 | 适配档 | 推荐赛道 | 版本 | 时间 |
+
+
+ {list.map((t) => (
+
+ | {t.typeCode} |
+ {t.persona || '—'} |
+ {t.adaptIndex} |
+ {LEVEL_LABEL[t.adaptLevel] || t.adaptLevel} |
+ {(t.tracks || []).join('、') || '—'} |
+ {t.version === 'quick' ? '快速版' : t.version === 'full' ? '完整版' : t.version} |
+ {fmtDate(t.createdAt)} |
+
+ ))}
+ {list.length === 0 && | 暂无测评记录,用户完成测评后自动上报 |
}
+
+
+
+
+
+
+ );
+}
diff --git a/website/src/platform/Schedule.jsx b/website/src/platform/Schedule.jsx
new file mode 100644
index 0000000..d329971
--- /dev/null
+++ b/website/src/platform/Schedule.jsx
@@ -0,0 +1,41 @@
+import React from 'react';
+import { ContentTemplate } from '@/components/templates';
+import { SectionHead } from '@/components/atoms';
+import { Timeline, Flow, Timetable } from '@/components/molecules';
+import { Reveal } from '@/components/organisms';
+import { LEAD_THEMES, CADENCE } from '@/data/schedule';
+import { ONLINE_HOURS, OFFLINE_HOURS } from '@/data/timetable';
+
+export default function Schedule() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 线上课与线下课为两个独立付费产品,可单独报名;公益课为免费入口,每期独立主题。
+
+
+
+ );
+}
diff --git a/website/src/platform/Structures.jsx b/website/src/platform/Structures.jsx
new file mode 100644
index 0000000..4ccd685
--- /dev/null
+++ b/website/src/platform/Structures.jsx
@@ -0,0 +1,40 @@
+import React from 'react';
+import { ContentTemplate } from '@/components/templates';
+import { SectionHead, Card } from '@/components/atoms';
+import { DataTable, CardGrid, Flow } from '@/components/molecules';
+import { Reveal } from '@/components/organisms';
+import { COMPARISON, TIER_STRUCTURES, ONLINE_FLOW } from '@/data/structures';
+
+export default function Structures() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ {TIER_STRUCTURES.map((ts) => (
+
+
+ = 3 ? 3 : 2}>
+ {ts.cards.map((c, i) => (
+
+ {c.p}
+
+ ))}
+
+
+ ))}
+
+ );
+}
diff --git a/website/src/platform/System.jsx b/website/src/platform/System.jsx
new file mode 100644
index 0000000..51e18b7
--- /dev/null
+++ b/website/src/platform/System.jsx
@@ -0,0 +1,74 @@
+import React from 'react';
+import { ContentTemplate } from '@/components/templates';
+import { SectionHead, Card, Tag } from '@/components/atoms';
+import { CardGrid, Flow } from '@/components/molecules';
+import { Reveal } from '@/components/organisms';
+import { TIERS, THREE_PIECE, KIT, ENTRY, INDEPENDENT } from '@/data/system';
+
+export default function System() {
+ return (
+
+
+
+
+ {TIERS.map((t, i) => (
+
+ 价格:{t.price}
交付:{t.deliver}
作用:{t.role}
+
+ ))}
+
+
+
+
+
+
+ {THREE_PIECE.map((c, i) => (
+
+ {c.points.map((p, j) => - {p}
)}
+ {c.out}
+
+ ))}
+
+
+ 客户只做三件事:填一张表、开两次确认会、执行 30 天。其余全部由服务方 + AI 团队完成。
+
+
+ 三件套仅用于线上 / 线下付费课;公益课是免费入口,不提供三件套。
+
+
+
+
+
+
+ {KIT.map((k, i) => (
+
+ {k.points.map((p, j) => - {p}
)}
+ {k.note}
+
+ ))}
+
+
+
+
+
+
+
+ {INDEPENDENT.map((p, i) => (
+
+ 价格:{p.price}
{p.desc}
+ {p.points.map((x, j) => - {x}
)}
+
+ ))}
+
+
+ 线上课与线下课相互独立,可单独报名;区别在价格、深度服务与交付形式。
+
+
+
+ );
+}
diff --git a/website/src/platform/Tools.jsx b/website/src/platform/Tools.jsx
new file mode 100644
index 0000000..ed87678
--- /dev/null
+++ b/website/src/platform/Tools.jsx
@@ -0,0 +1,106 @@
+import React from 'react';
+import { ContentTemplate } from '@/components/templates';
+import { SectionHead, Card } from '@/components/atoms';
+import { CardGrid, CardForm, TextField, RangeField } from '@/components/molecules';
+import { Reveal } from '@/components/organisms';
+import { Button } from '@/components/atoms';
+import { SELF_FORMS, TEMPLATE_FORMS, QUESTIONNAIRES, nicheCompute } from '@/data/forms';
+import { FINANCE_FIELDS } from '@/data/tools';
+
+/* ---------- 财务测算(T06) ---------- */
+function FinanceCalculator() {
+ const [f, setF] = React.useState({ price: '', cost: '', fixed: '', income: '', expense: '' });
+ const [res, setRes] = React.useState(null);
+ const set = (k) => (v) => setF({ ...f, [k]: v });
+ const calc = () => {
+ const price = parseFloat(f.price), cost = parseFloat(f.cost), fixed = parseFloat(f.fixed),
+ income = parseFloat(f.income), expense = parseFloat(f.expense);
+ if ([price, cost, fixed, income, expense].some(isNaN)) { setRes('请填满 5 项再计算(均为数字)。'); return; }
+ const margin = price > 0 ? ((price - cost) / price) * 100 : 0;
+ const perUnit = price - cost;
+ const breakeven = perUnit > 0 ? Math.ceil(fixed / perUnit) : '∞(毛利 ≤ 0)';
+ const net = income - expense;
+ const note =
+ margin >= 50 ? '服务类区间(50–80%)' :
+ margin >= 30 ? '产品类区间(30–50%)' :
+ margin >= 10 ? '撮合类区间(10–30%)' : '低于建议下限,需提价或降本';
+ setRes(
+ <>
毛利率:{margin.toFixed(0)}%({note})
盈亏平衡单量:{breakeven} 单/月
月净利:{net.toFixed(0)} 元{net < 0 ? '(现金流为负,需备足 3–6 个月生活费)' : '(现金流转正)'}>
+ );
+ };
+ return (
+
+
T06 · 财务测算表
+
填数字自动计算毛利率、盈亏平衡与月净利。
+
+ {FINANCE_FIELDS.map((x) => (
+
+ ))}
+
+
+ {res &&
{res}
}
+
+ );
+}
+
+/* ---------- 30 天执行表(T05) ---------- */
+function ThirtyDayPlan() {
+ const weeks = ['落地', '获客', '转化', '复盘'];
+ const [rows, setRows] = React.useState(weeks.map(() => ({ goal: '', metric: '' })));
+ const set = (i, k) => (v) => setRows(rows.map((r, j) => (j === i ? { ...r, [k]: v } : r)));
+ return (
+
+
T05 · 30 天落地执行表
+
分周目标 + 关键指标,随陪跑每周复盘。
+ {weeks.map((w, i) => (
+
+
+
+
+ ))}
+
+ );
+}
+
+export default function Tools() {
+ return (
+
+
+
+
+ {SELF_FORMS.map((s) => (
+
+ ))}
+
+
+
+
+
+
+ {TEMPLATE_FORMS.map((t) => (
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+ {QUESTIONNAIRES.map((q) => (
+
+ ))}
+
+
+
+ );
+}
diff --git a/website/src/platform/report/ReportBiz.jsx b/website/src/platform/report/ReportBiz.jsx
new file mode 100644
index 0000000..234513e
--- /dev/null
+++ b/website/src/platform/report/ReportBiz.jsx
@@ -0,0 +1,79 @@
+import React from 'react';
+import { ContentTemplate } from '@/components/templates';
+import { SectionHead } from '@/components/atoms';
+import { Reveal } from '@/components/organisms';
+import { DataTable } from '@/components/molecules';
+
+const INCOME = { head: ['收入层', '产品', '价格', '状态'], rows: [
+ ['① 课费层', '线上 ¥499–1299 / 线下 ¥1980–3980', '—', '已运行'],
+ ['② 服务层', 'OPC 季度陪跑', '¥3000–5000 / 季', '补强'],
+ ['③ 政策层', '人社局培训补贴 / 政府采购 / 示范项目', '按项目', '变现政府资源'],
+ ['④ 生态层', '测评付费版 / 撮合 / 分销 / 内容资产', '递增', '储备']
+] };
+
+const PYRAMID = [
+ { t: '引流品', d: '公益课 / 沙龙 · 免费 · 每周滚动', note: '获客与信任' },
+ { t: '利润品', d: '线上标准服务 · ¥499–1299', note: '主力收入' },
+ { t: '高客单', d: '线下深度服务 · ¥1980–3980', note: '现场交付' },
+ { t: '复购品', d: 'OPC 季度陪跑 · ¥3000–5000/季', note: '跑通后承接' }
+];
+
+const AUDIENCE = { head: ['优先级', '人群', '画像'], rows: [
+ ['主攻', 'B 技能型副业转型者', '有技能缺商业闭环、接单不稳定'],
+ ['次攻', 'A 云南文旅轻资产创业者', '民宿主 / 咖啡 / 鲜花 / 非遗,有资源缺打法'],
+ ['次攻', 'C 数字游民 / 内容创作者', '有流量难变现,可远程交付'],
+ ['覆盖', 'D/E 传统生意 AI 化 / 上班族', '靠内容自然覆盖']
+] };
+
+const THREE = [
+ { step: '01', t: '诊断', d: '10 张自我认知卡 + 导师复核 → 主攻方向' },
+ { step: '02', t: 'OPC 体系设计', d: '6 页经营方案:定位 / 产品 / 闭环 / 智能体 / 财务 / 30 天表' },
+ { step: '03', t: '陪跑', d: '30 天每周复盘 + 答疑,推进到首单' }
+];
+
+export default function ReportBiz() {
+ return (
+
+
+
+
+
+
+
+
+
+ {PYRAMID.map((p) => (
+
+
{p.t}
+
{p.d}
+
{p.note}
+
+ ))}
+
+
+
+
+
+
+ {THREE.map((x) => (
+
+
{x.step}
+
{x.t}
+
{x.d}
+
+ ))}
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/website/src/platform/report/ReportData.jsx b/website/src/platform/report/ReportData.jsx
new file mode 100644
index 0000000..d5116c6
--- /dev/null
+++ b/website/src/platform/report/ReportData.jsx
@@ -0,0 +1,88 @@
+import React from 'react';
+import { ContentTemplate } from '@/components/templates';
+import { SectionHead } from '@/components/atoms';
+import { Reveal } from '@/components/organisms';
+import { ops } from '@/services/ops';
+import { useOps, StatCard, OpsState, fmtDate } from '@/components/pineAdmin';
+import '@/styles/pineAdmin.css';
+
+export default function ReportData() {
+ const stats = useOps(ops.stats);
+ const bookings = useOps(ops.bookings);
+ const tests = useOps(ops.tests);
+
+ const s = (stats.data && stats.data.stats) || {};
+ const bList = (bookings.data && bookings.data.list) || [];
+ const tList = (tests.data && tests.data.list) || [];
+
+ // 测评类型码 TOP 统计
+ const codeCount = {};
+ tList.forEach((t) => { if (t.typeCode) codeCount[t.typeCode] = (codeCount[t.typeCode] || 0) + 1; });
+ const topCodes = Object.entries(codeCount).sort((a, b) => b[1] - a[1]).slice(0, 8);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | 姓名 | 联系方式 | 想预约 | 主题 | 时间 |
+
+ {bList.slice(0, 12).map((b) => (
+
+ | {b.name} |
+ {b.contact} |
+ {b.want || '—'} |
+ {(b.topics || []).join('、') || '—'} |
+ {fmtDate(b.createdAt)} |
+
+ ))}
+ {bList.length === 0 && | 暂无预约 |
}
+
+
+
+
+
+
+
+
+
+
+ {topCodes.length === 0 &&
暂无测评数据
}
+ {topCodes.map(([code, n]) => {
+ const max = topCodes[0] ? topCodes[0][1] : 1;
+ return (
+
+ );
+ })}
+
+
+
+
+ );
+}
diff --git a/website/src/platform/report/ReportHome.jsx b/website/src/platform/report/ReportHome.jsx
new file mode 100644
index 0000000..8781ed3
--- /dev/null
+++ b/website/src/platform/report/ReportHome.jsx
@@ -0,0 +1,53 @@
+import React from 'react';
+import { ContentTemplate } from '@/components/templates';
+import { SectionHead } from '@/components/atoms';
+import { Reveal } from '@/components/organisms';
+import { ops } from '@/services/ops';
+import { useOps, StatCard, OpsState } from '@/components/pineAdmin';
+import '@/styles/pineAdmin.css';
+
+const REPORT_LINKS = [
+ { href: '#/pine/report/biz', icon: 'fa-solid fa-chart-pie', title: '商业模式', desc: '收入四层引擎 · 产品金字塔 · 三件套 · 人群聚焦', accent: true },
+ { href: '#/pine/report/policy', icon: 'fa-solid fa-file-contract', title: '政策红利', desc: '云南省 AI·OPC 创业扶持政策要点与申报路径' },
+ { href: '#/pine/report/data', icon: 'fa-solid fa-chart-column', title: '数据看板', desc: '预约 / 测评 / 排期经营数据' }
+];
+
+export default function ReportHome() {
+ const { data, err, loading } = useOps(ops.stats);
+ const s = (data && data.stats) || {};
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/website/src/platform/report/ReportPolicy.jsx b/website/src/platform/report/ReportPolicy.jsx
new file mode 100644
index 0000000..5fc6638
--- /dev/null
+++ b/website/src/platform/report/ReportPolicy.jsx
@@ -0,0 +1,71 @@
+import React from 'react';
+import { ContentTemplate } from '@/components/templates';
+import { SectionHead } from '@/components/atoms';
+import { Reveal } from '@/components/organisms';
+import { DataTable, Flow } from '@/components/molecules';
+
+const POLICY = {
+ name: '《关于支持服务人工智能OPC创业的若干措施》',
+ org: '云南省人力资源和社会保障厅',
+ date: '2026 年 7 月印发',
+ direction: '创业生态 · 融资渠道 · 人才评价 · 技能培训等 9 大方向'
+};
+
+const COVER = { head: ['要点', '内容'], rows: [
+ ['不强制高新背景', '作家、短视频创作者、民宿运营者、咨询顾问、独立设计师、跨境服务商等超级个体均纳入'],
+ ['地域红利', '依托文旅流量 + 25 个边境县市区位优势'],
+ ['鼓励方向', '一人一公司,用 AI 做内容产出、文旅运营、跨境服务'],
+ ['落地渠道', '省市县三级政务平台 + 本地创业载体 / 孵化器申报']
+] };
+
+const QUERY = [
+ { step: '01', t: '省人社厅官网', d: '政策法规 / 通知公告 / 就业创业栏目查原文' },
+ { step: '02', t: '省政府门户', d: 'yn.gov.cn 搜:OPC / 一人公司 / 人工智能创业 / 若干措施' },
+ { step: '03', t: '州市人社局', d: '昆明、大理、丽江、西双版纳等可能有配套落地细则' },
+ { step: '04', t: '本地载体', d: '主动联系创业载体 / 孵化器 / 人社部门,获取一手申报指导' }
+];
+
+const RED_LINE = [
+ '政策是放大器,不是起跑线——先跑通最小商业闭环再对接政策',
+ '本手册为政策线索,不替代官方原文;落地前以官方最新文件为准',
+ '保留每次查询的政策原文 / 日期 / 条款,形成自己的《政策资料库》'
+];
+
+export default function ReportPolicy() {
+ return (
+
+
+
+
+
{POLICY.name}
+
发布:{POLICY.org} · {POLICY.date}
+
核心导向:{POLICY.direction}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {RED_LINE.map((x, i) => (
+
· {x}
+ ))}
+
+
+
+ );
+}
diff --git a/website/src/services/auth.js b/website/src/services/auth.js
new file mode 100644
index 0000000..dc85080
--- /dev/null
+++ b/website/src/services/auth.js
@@ -0,0 +1,110 @@
+/**
+ * 认证服务(auth service)
+ * -------------------------------------------------------------
+ * 用户端:手机号 + 短信验证码登录 / 注册(手机号即账号,未注册自动注册)。
+ * POST /api/auth/send-code 发送验证码(演示环境响应返回 debugCode)
+ * POST /api/auth/phone-login 手机号 + 验证码 → 自动注册或登录 → token
+ * 平台端(/pine 内部管理):账号密码登录(内置 pine/123456)。
+ * POST /api/auth/login
+ * 迁移真实后端:只改 API_BASE 与保持 /api/auth/* 契约即可。
+ */
+const API_BASE = 'https://opc.pinesound.cn'; // 云超服 FastAPI 后端
+const TOKEN_KEY = 'pine_token';
+const USER_KEY = 'pine_user';
+
+export function saveSession(data) {
+ if (!data || !data.token) return;
+ localStorage.setItem(TOKEN_KEY, data.token);
+ localStorage.setItem(USER_KEY, JSON.stringify({ username: data.username, name: data.name || data.username }));
+}
+
+/* ---------------- 用户端:手机号 + 验证码 ---------------- */
+export async function sendCode(phone) {
+ let res;
+ try {
+ res = await fetch(`${API_BASE}/api/auth/send-code`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ phone })
+ });
+ } catch {
+ throw new Error('网络连接失败,请检查网络后重试');
+ }
+ const data = await res.json().catch(() => ({}));
+ if (!res.ok || !data.ok) throw new Error(data.error || '验证码发送失败,请重试');
+ return data; // { ok, sent, debugCode }
+}
+
+export async function phoneLogin(phone, code) {
+ let res;
+ try {
+ res = await fetch(`${API_BASE}/api/auth/phone-login`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ phone, code })
+ });
+ } catch {
+ throw new Error('网络连接失败,请检查网络后重试');
+ }
+ const data = await res.json().catch(() => ({}));
+ if (!res.ok || !data.token) throw new Error(data.error || '登录失败,请重试');
+ saveSession(data);
+ return data;
+}
+
+/* ---------------- 平台端:账号密码登录(内部管理) ---------------- */
+export async function login(username, password) {
+ let res;
+ try {
+ res = await fetch(`${API_BASE}/api/auth/login`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ username, password })
+ });
+ } catch {
+ throw new Error('网络连接失败,请检查网络后重试');
+ }
+ const data = await res.json().catch(() => ({}));
+ if (!res.ok || !data.token) throw new Error(data.error || '登录失败,请重试');
+ saveSession(data);
+ return data;
+}
+
+export function logout() {
+ localStorage.removeItem(TOKEN_KEY);
+ localStorage.removeItem(USER_KEY);
+ try { fetch(`${API_BASE}/api/auth/logout`, { method: 'POST' }).catch(() => {}); } catch { /* noop */ }
+}
+
+export function getToken() {
+ if (typeof localStorage === 'undefined') return null;
+ return localStorage.getItem(TOKEN_KEY);
+}
+
+/** 用完整 user 对象覆盖本地用户缓存(含 status/topics/source 等报名资料) */
+export function saveUser(user) {
+ if (!user) return;
+ localStorage.setItem(USER_KEY, JSON.stringify(user));
+}
+
+/** 更新当前用户资料(昵称/头像 + 报名资料 status/topics/source),需登录(token) */
+export async function updateProfile(data) {
+ const token = getToken();
+ const res = await fetch(`${API_BASE}/api/auth/update-profile`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}) },
+ body: JSON.stringify(data)
+ });
+ const r = await res.json().catch(() => ({}));
+ if (res.ok && r.ok && r.user) saveUser(r.user);
+ return r;
+}
+
+export function isAuthed() {
+ return !!getToken();
+}
+
+export function getUser() {
+ if (typeof localStorage === 'undefined') return null;
+ try { return JSON.parse(localStorage.getItem(USER_KEY) || 'null'); } catch { return null; }
+}
diff --git a/website/src/services/booking.js b/website/src/services/booking.js
new file mode 100644
index 0000000..8d45198
--- /dev/null
+++ b/website/src/services/booking.js
@@ -0,0 +1,50 @@
+/**
+ * 报名服务(booking service)
+ * -------------------------------------------------------------
+ * 对接后端 POST /api/bookings:需登录(Bearer token)+ 账号已绑手机号。
+ * body = { name, status, topics, question, source, eventId, eventTitle, eventStart }
+ * 后端从 token 解析账号身份,无需再传手机号/验证码。
+ */
+import { getToken } from '@/services/auth';
+import { cached } from './cache';
+const API_BASE = 'https://opc.pinesound.cn'; // 云超服 FastAPI 后端
+
+/** 活动详情 */
+export async function getEventDetail(id) {
+ return cached(`event:${id}`, 30000, async () => {
+ try {
+ const res = await fetch(`${API_BASE}/api/events/${encodeURIComponent(id)}`);
+ const data = await res.json().catch(() => ({}));
+ if (!res.ok || !data.ok) return null;
+ return data.event || null;
+ } catch {
+ return null;
+ }
+ });
+}
+
+/** 获取可报名场次(公益课/沙龙,均线下) */
+export async function getBookableEvents() {
+ return cached('events:bookable', 30000, async () => {
+ try {
+ const res = await fetch(`${API_BASE}/api/events?bookable=1`);
+ const data = await res.json().catch(() => ({}));
+ if (!res.ok || !data.ok) return [];
+ return data.list || [];
+ } catch {
+ return [];
+ }
+ });
+}
+
+export async function submitBooking(data) {
+ const token = getToken();
+ const res = await fetch(`${API_BASE}/api/bookings`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}) },
+ body: JSON.stringify(data)
+ });
+ const r = await res.json().catch(() => ({}));
+ if (!res.ok || !r.ok) throw new Error(r.detail || r.error || '报名提交失败,请重试');
+ return r; // { ok, id, createdAt, username, name, auditStatus }
+}
diff --git a/website/src/services/cache.js b/website/src/services/cache.js
new file mode 100644
index 0000000..9601f77
--- /dev/null
+++ b/website/src/services/cache.js
@@ -0,0 +1,19 @@
+/**
+ * 短缓存:内存 Map,按 key 缓存异步结果(TTL 毫秒)。
+ * 用于列表类接口(活动/题目/配置),减少重复请求与"空→内容"跳变。
+ * 仅前端缓存,不影响后端数据。
+ */
+const store = new Map();
+
+export async function cached(key, ttl, fn) {
+ const hit = store.get(key);
+ if (hit && Date.now() - hit.t < ttl) return hit.data;
+ const data = await fn();
+ store.set(key, { t: Date.now(), data });
+ return data;
+}
+
+/** 清空全部缓存(登出/主动刷新时用) */
+export function clearCache() {
+ store.clear();
+}
diff --git a/website/src/services/ops.js b/website/src/services/ops.js
new file mode 100644
index 0000000..903ebf4
--- /dev/null
+++ b/website/src/services/ops.js
@@ -0,0 +1,118 @@
+/**
+ * 运营 / 管理 API 服务(ops service)
+ * -------------------------------------------------------------
+ * 管理端点(/pine 后台)需要登录 token;公开端点(当期排期)无需。
+ * 对接 server/mock-api.mjs。
+ */
+const API_BASE = 'https://opc.pinesound.cn'; // 云超服 FastAPI 后端
+const TOKEN_KEY = 'pine_token';
+import { cached } from './cache';
+
+function authHeaders() {
+ const t = localStorage.getItem(TOKEN_KEY);
+ return { 'Content-Type': 'application/json', ...(t ? { Authorization: `Bearer ${t}` } : {}) };
+}
+
+async function api(method, path, body) {
+ const res = await fetch(`${API_BASE}${path}`, {
+ method,
+ headers: authHeaders(),
+ body: body === undefined ? undefined : JSON.stringify(body)
+ });
+ const data = await res.json().catch(() => ({}));
+ if (!res.ok || data.ok === false) throw new Error(data.error || '请求失败');
+ return data;
+}
+
+/* ---------- 后台管理 API(需登录) ---------- */
+export const ops = {
+ stats: () => api('GET', '/api/ops/stats'),
+ bookings: () => api('GET', '/api/bookings'),
+ updateBooking: (id, status) => api('PATCH', `/api/bookings/${id}`, { status }),
+ updateBookingAudit: (id, auditStatus) => api('PATCH', `/api/bookings/${id}`, { auditStatus }),
+ deleteBooking: (id) => api('DELETE', `/api/bookings/${id}`),
+ bookAudit: (audit) => api('GET', `/api/bookings?audit=${audit}`),
+ events: () => api('GET', '/api/events'),
+ createEvent: (data) => api('POST', '/api/events', data),
+ updateEvent: (id, data) => api('PUT', `/api/events/${id}`, data),
+ deleteEvent: (id) => api('DELETE', `/api/events/${id}`),
+ tests: () => api('GET', '/api/tests'),
+ policyLogs: () => api('GET', '/api/policy-logs'),
+ planLogs: () => api('GET', '/api/plan-logs'),
+ surveyLogs: () => api('GET', '/api/survey-logs')
+};
+
+/* ---------- 公开:上报政策测评 / 启动流程结果(失败不影响用户) ---------- */
+export async function reportPolicyLog(data) {
+ try { await api('POST', '/api/policy-logs', data); } catch { /* noop */ }
+}
+export async function reportPlanLog(data) {
+ try { await api('POST', '/api/plan-logs', data); } catch { /* noop */ }
+}
+/* 公开:上报 OPC 创业伙伴调研(匿名可提交;失败不影响用户) */
+export async function reportSurveyLog(data) {
+ try { await api('POST', '/api/survey-logs', data); } catch { /* noop */ }
+}
+/* 管理端:查看调研记录 */
+export async function getSurveyLogs() {
+ return api('GET', '/api/survey-logs');
+}
+
+/* ---------- 公开:当期排期(无后端时返回 null,前端降级静态排期) ---------- */
+export async function getCurrentEvents() {
+ return cached('events:current', 30000, async () => {
+ try {
+ const res = await fetch(`${API_BASE}/api/events?current=1`);
+ const data = await res.json().catch(() => ({}));
+ if (!res.ok || !data.ok) return null;
+ return data;
+ } catch {
+ return null;
+ }
+ });
+}
+
+/* ---------- 图像上传(管理端,multipart) ---------- */
+export async function uploadImage(file) {
+ const token = localStorage.getItem(TOKEN_KEY);
+ const fd = new FormData();
+ fd.append('file', file);
+ const res = await fetch(`${API_BASE}/api/upload`, {
+ method: 'POST',
+ headers: { Authorization: `Bearer ${token}` },
+ body: fd
+ });
+ const data = await res.json().catch(() => ({}));
+ if (!res.ok || !data.ok) throw new Error(data.error || '上传失败');
+ return data; // { ok, url }
+}
+
+/* ---------- 公开:政策测评(题目下发 + 结果生成,后端驱动) ---------- */
+export async function getPolicyQuestions() {
+ return cached('policy:questions', 30000, async () => {
+ try { const r = await api('GET', '/api/policy/questions'); return r.questions || []; } catch { return []; }
+ });
+}
+export async function calcPolicy(answers) {
+ try { const r = await api('POST', '/api/policy/calculate', { answers }); return r.result; } catch { return null; }
+}
+
+/* ---------- 公开:启动流程(配置 + 生成,后端驱动) ---------- */
+export async function getPlanConfig() {
+ return cached('plan:config', 30000, async () => {
+ try { const r = await api('GET', '/api/plan/config'); return r; } catch { return { regions: [], status: [] }; }
+ });
+}
+export async function genPlan(opt) {
+ try { const r = await api('POST', '/api/plan/generate', opt); return r.plan || []; } catch { return []; }
+}
+
+/* ---------- 公开:调研(题目下发 + 提交,后端驱动) ---------- */
+export async function getSurveyQuestions() {
+ return cached('survey:questions', 30000, async () => {
+ try { const r = await api('GET', '/api/survey/questions'); return r; } catch { return null; }
+ });
+}
+export async function submitSurvey(data) {
+ try { return await api('POST', '/api/survey/submit', data); } catch { return null; }
+}
diff --git a/website/src/styles/auth.css b/website/src/styles/auth.css
new file mode 100644
index 0000000..55b4568
--- /dev/null
+++ b/website/src/styles/auth.css
@@ -0,0 +1,45 @@
+/* ============================================================
+ 登录 / 内部工具区 样式(暗色令牌一致)
+ ============================================================ */
+.page-auth {
+ position: relative; z-index: 1;
+ min-height: 100vh; min-height: 100dvh;
+ display: grid; place-items: center;
+ padding: 24px;
+}
+.auth-card {
+ width: 100%; max-width: 380px;
+ background: var(--panel); border: 1px solid var(--line); border-radius: 20px;
+ padding: 34px 30px 28px; text-align: center;
+}
+.auth-logo {
+ width: 56px; height: 56px; margin: 0 auto 14px;
+ border-radius: 50%; background: #fff; color: #111;
+ display: grid; place-items: center; font-size: 24px;
+}
+.auth-card h1 { font-size: 20px; font-weight: 700; color: #fff; }
+.auth-sub { font-size: 12.5px; color: #8e8e8e; margin: 4px 0 22px; }
+.auth-card .field { text-align: left; margin-bottom: 14px; }
+.auth-card .field label { display: block; font-size: 12.5px; color: var(--muted); margin-bottom: 6px; }
+.auth-card .field input {
+ width: 100%; padding: 11px 13px; border-radius: 10px;
+ background: #161618; border: 1px solid #2a2a2d; color: #fff;
+ font-family: inherit; font-size: 14px; outline: none;
+}
+.auth-card .field input:focus { border-color: #fff; }
+.auth-err {
+ margin-top: 6px; padding: 10px 12px; border-radius: 10px;
+ background: rgba(220,60,60,.12); border: 1px solid rgba(220,60,60,.4);
+ color: #ff9d9d; font-size: 12.5px; text-align: left;
+}
+.auth-btn { width: 100%; margin-top: 6px; }
+.auth-foot { font-size: 11.5px; color: #5b5b5b; margin-top: 16px; }
+
+/* 内部工具索引 */
+.pine-intro { max-width: 680px; color: #c4c2c3; font-size: 14px; line-height: 1.8; margin-bottom: 4px; }
+.pine-intro b { color: #fff; }
+.pine-note {
+ margin-top: 18px; font-size: 12.5px; color: #6f6f6f;
+ background: var(--panel); border: 1px dashed var(--line); border-radius: 12px; padding: 13px 16px;
+}
+.pine-note .logout-link { margin-left: 8px; color: #fff; cursor: pointer; text-decoration: underline; }
diff --git a/website/src/styles/booking.css b/website/src/styles/booking.css
new file mode 100644
index 0000000..7435b64
--- /dev/null
+++ b/website/src/styles/booking.css
@@ -0,0 +1,124 @@
+/* ============================================================
+ 预约页 · 公益课报名 / 沙龙预约
+ 延续暗色令牌:面板卡片 + 白色主按钮;两栏 → 移动端单栏
+ ============================================================ */
+.bk-layout { display: grid; grid-template-columns: 340px 1fr; gap: 22px; align-items: start; }
+.bk-info { position: sticky; top: 20px; }
+.bk-info-card { background: var(--panel); border: 1px solid var(--line); border-radius: 14px; padding: 18px 20px; margin-bottom: 12px; }
+.bk-info-title { font-size: 15px; font-weight: 600; color: #fff; margin-bottom: 6px; }
+.bk-info-title i { margin-right: 8px; color: #c4c2c3; }
+.bk-info-card p { font-size: 12.5px; color: #8e8e8e; margin-bottom: 8px; }
+.bk-info-card ul li { font-size: 13px; color: #c4c2c3; line-height: 1.7; padding-left: 16px; position: relative; margin-top: 4px; }
+.bk-info-card ul li::before { content: ""; position: absolute; left: 2px; top: 8px; width: 5px; height: 5px; border-radius: 50%; background: #fff; }
+.bk-note { font-size: 12.5px; color: #6f6f6f; line-height: 1.7; margin-top: 8px; }
+.bk-form .form-card { padding: 26px; }
+
+.bk-err {
+ margin-top: 12px; padding: 10px 12px; border-radius: 10px;
+ background: rgba(220,60,60,.12); border: 1px solid rgba(220,60,60,.4);
+ color: #ff9d9d; font-size: 12.5px;
+}
+.bk-ok {
+ margin: 6px 0; padding: 8px 12px; border-radius: 8px;
+ background: rgba(120,220,160,.1); border: 1px solid rgba(120,220,160,.35);
+ color: #a8f0c8; font-size: 12.5px; line-height: 1.5;
+}
+
+.bk-success {
+ text-align: center; background: var(--panel); border: 1px solid var(--line);
+ border-radius: 20px; padding: 60px 30px;
+}
+.bk-success-icon { width: 64px; height: 64px; margin: 0 auto 18px; border-radius: 50%; background: #fff; color: #000; display: grid; place-items: center; font-size: 26px; }
+.bk-success h2 { font-size: 26px; font-weight: 700; color: #fff; }
+.bk-success p { color: #c4c2c3; font-size: 14.5px; line-height: 1.8; margin-top: 10px; }
+.bk-success p b { color: #fff; }
+.bk-success-actions { display: flex; gap: 12px; justify-content: center; flex-wrap: wrap; margin-top: 26px; }
+
+@media (max-width: 900px) {
+ .bk-layout { grid-template-columns: 1fr; }
+ .bk-info { position: static; }
+}
+@media (max-width: 720px) {
+ .bk-form .form-card { padding: 18px 16px; }
+ .bk-success { padding: 44px 20px; }
+}
+
+/* ---------- 场次选择 ---------- */
+.bk-events { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 12px; }
+.bk-ev {
+ text-align: left; cursor: pointer; background: var(--panel-2); border: 1px solid var(--line);
+ border-radius: 14px; padding: 16px 18px; transition: all .2s ease;
+}
+.bk-ev:hover { border-color: rgba(255,255,255,.4); }
+.bk-ev.sel {
+ border-color: #a8c4ff; background: rgba(160,180,255,.08);
+ box-shadow: 0 0 0 1px rgba(160,180,255,.5), 0 0 22px rgba(160,180,255,.14);
+}
+.bk-ev-top { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; }
+.bk-ev-tag { font-size: 12px; font-weight: 700; color: #000; background: #a8c4ff; border-radius: 999px; padding: 3px 12px; }
+.bk-ev-tag.alt { background: #5b8cff; color: #fff; }
+.bk-ev-check { font-size: 12.5px; color: #a8c4ff; }
+.bk-ev-title { font-size: 15px; font-weight: 700; color: #fff; line-height: 1.45; }
+.bk-ev-sub { font-size: 12.5px; color: #8e8e8e; margin-top: 4px; }
+.bk-ev-meta { font-size: 12.5px; color: #c4c2c3; margin-top: 10px; line-height: 1.5; }
+.bk-empty { font-size: 13.5px; color: #8e8e8e; padding: 12px 0; }
+
+/* ---------- 活动列表(网页) ---------- */
+.ev-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 16px; }
+.bk-ev-card {
+ background: linear-gradient(180deg, #141417, #0e0e12); border: 1px solid var(--line);
+ border-radius: 18px; padding: 22px 22px 18px; display: flex; flex-direction: column; gap: 8px;
+}
+.bk-ev-tags { display: flex; gap: 8px; }
+.bk-ev-tag { font-size: 12px; font-weight: 700; color: #000; background: #a8c4ff; border-radius: 999px; padding: 3px 12px; }
+.bk-ev-tag.alt { background: #5b8cff; color: #fff; }
+.bk-ev-tag.mode { background: rgba(255,255,255,0.12); color: #c4c2c3; }
+.bk-ev-tag.mode.online { background: rgba(140,200,255,0.16); color: #a8dcff; }
+.bk-ev-card .bk-ev-title { font-size: 18px; font-weight: 700; color: #fff; line-height: 1.45; }
+.bk-ev-card .bk-ev-sub { font-size: 13px; color: #8e8e8e; }
+.bk-ev-card .bk-ev-meta { font-size: 13px; color: #c4c2c3; line-height: 1.6; display: flex; flex-direction: column; gap: 3px; }
+.bk-ev-cta { margin-top: 10px; }
+.bk-empty { font-size: 14px; color: #8e8e8e; padding: 20px 0; }
+
+/* ---------- 活动详情(网页) ---------- */
+.ev-detail-img { border-radius: 20px; overflow: hidden; }
+.ev-detail-hero { width: 100%; max-height: 380px; object-fit: cover; border-radius: 20px; display: block; }
+.ev-detail-img.placeholder {
+ background: linear-gradient(135deg, #1b2a5e, #241b4a 55%, #3a1e5e);
+ border: 1px solid rgba(255,255,255,0.18); border-radius: 20px; padding: 40px 32px;
+ display: flex; flex-direction: column; align-items: flex-start; gap: 14px;
+}
+.ev-detail-img.placeholder h2 { font-size: 30px; color: #fff; margin: 0; }
+.ev-info { background: var(--panel); border: 1px solid var(--line); border-radius: 16px; padding: 6px 22px; }
+.ev-info-line { display: flex; gap: 22px; padding: 14px 0; border-bottom: 1px solid rgba(255,255,255,0.06); }
+.ev-info-line:last-child { border-bottom: none; }
+.ev-info-line .k { flex-shrink: 0; width: 60px; font-size: 13.5px; color: #8e8e8e; }
+.ev-info-line .v { flex: 1; font-size: 14px; color: #fff; line-height: 1.5; }
+.ev-desc { font-size: 14px; color: #c4c2c3; line-height: 1.8; margin-top: 22px; }
+
+/* 图标行(时钟/地点/主办) */
+.bk-ev-line { display: flex; align-items: center; gap: 7px; }
+.bk-ev-line .icon { color: #a8c4ff; }
+.bk-info-title { display: flex; align-items: center; gap: 8px; }
+.bk-info-title .icon { color: #a8c4ff; }
+.mk-row-arrow { display: inline-flex; align-items: center; }
+.mk-row-arrow .icon { color: #6f6f6f; }
+.btn .icon { margin-left: 2px; }
+
+/* ---------- 活动卡片:海报 + 简介 + 状态(web) ---------- */
+.bk-ev-poster { position: relative; height: 150px; border-radius: 14px; overflow: hidden; margin-bottom: 12px; }
+.bk-ev-img { width: 100%; height: 100%; object-fit: cover; display: block; }
+.bk-ev-img.ph { display: flex; align-items: flex-end; padding: 16px; box-sizing: border-box; color: #fff; font-weight: 700; background: linear-gradient(150deg, #1b2a5e, #3a1e5e); }
+.bk-ev-tags { position: absolute; top: 10px; left: 10px; display: flex; gap: 6px; }
+.bk-ev-tag.mode { background: rgba(0,0,0,0.5); color: #fff; }
+.bk-ev-body { display: flex; flex-direction: column; }
+.bk-ev-card .bk-ev-title { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
+.bk-ev-card .bk-ev-sub { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden; }
+.bk-ev-card .bk-ev-meta { display: flex; justify-content: space-between; gap: 8px; }
+.bk-ev-foot { display: flex; align-items: center; justify-content: space-between; margin-top: 10px; }
+.bk-ev-price { font-size: 14px; font-weight: 700; color: #3fb68b; }
+.bk-ev-status { font-size: 12px; padding: 3px 12px; border-radius: 999px; color: #3fb68b; background: rgba(63,182,139,0.14); border: 1px solid rgba(63,182,139,0.4); }
+.bk-ev-status.full { color: #f5c451; background: rgba(245,196,81,0.12); border-color: rgba(245,196,81,0.4); }
+.bk-ev-status.done { color: #9a9a9a; background: rgba(255,255,255,0.08); border-color: rgba(255,255,255,0.18); }
+.bk-ev-status.pending { color: #8e8e8e; background: rgba(255,255,255,0.08); border-color: rgba(255,255,255,0.18); }
+.bk-ev-status.invite { color: #a8c4ff; background: rgba(168,196,255,0.12); border-color: rgba(168,196,255,0.4); }
diff --git a/website/src/styles/global.css b/website/src/styles/global.css
new file mode 100644
index 0000000..8380f12
--- /dev/null
+++ b/website/src/styles/global.css
@@ -0,0 +1,458 @@
+/* ============================================================
+ Global / 组件样式 —— 严格参照 design.md 视觉语言
+ 原子(atom) → 分子(molecule) → 组织(organism) → 模板(template)
+ ============================================================ */
+
+/* ---------- reset ---------- */
+* { margin: 0; padding: 0; box-sizing: border-box; }
+html { scroll-behavior: smooth; }
+/* 防横向溢出:用 clip 而非 hidden——hidden 会建立滚动容器并破坏 position:sticky(吸顶页眉) */
+html, body { overflow-x: clip; }
+body {
+ background: var(--bg);
+ color: var(--text);
+ font-family: var(--font-sans);
+ -webkit-font-smoothing: antialiased;
+ text-rendering: optimizeLegibility;
+}
+a { color: inherit; text-decoration: none; }
+button { font-family: inherit; cursor: pointer; }
+img, svg { display: block; }
+ul { list-style: none; }
+::selection { background: #fff; color: #000; }
+#root { min-height: 100vh; }
+
+/* ---------- 进场动画(design.md .anim) ---------- */
+.anim {
+ opacity: 0;
+ transform: translateY(22px) scale(0.98);
+ filter: blur(6px);
+ animation: reveal 0.85s var(--ease) forwards;
+ animation-delay: var(--d, 0s);
+}
+@keyframes reveal { to { opacity: 1; transform: translateY(0) scale(1); filter: blur(0); } }
+@keyframes slideDown { from { opacity: 0; transform: translateY(-18px); } to { opacity: 1; transform: translateY(0); } }
+@keyframes headlineFade { from { opacity: 0; transform: translateY(14px); } to { opacity: 1; transform: translateY(0); } }
+@keyframes revealPulse {
+ from { opacity: 0; transform: translateY(22px) scale(0.96); filter: blur(6px); }
+ 60% { opacity: 1; transform: translateY(-2px) scale(1.02); filter: blur(0); }
+ to { opacity: 1; transform: translateY(0) scale(1); filter: blur(0); }
+}
+@keyframes overlayIn { from { opacity: 0; } to { opacity: 1; } }
+@keyframes menuIn { from { opacity: 0; transform: translateY(-14px) scale(0.98); } to { opacity: 1; transform: translateY(0) scale(1); } }
+@keyframes linkIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
+
+/* 滚动进场(内容页,原子通用) */
+.anim-scroll {
+ opacity: 0;
+ transform: translateY(22px);
+ filter: blur(6px);
+ transition: opacity 0.8s var(--ease), transform 0.8s var(--ease), filter 0.8s var(--ease);
+}
+.anim-scroll.in { opacity: 1; transform: none; filter: none; }
+
+/* ---------- 原子:Button ---------- */
+.btn {
+ display: inline-flex; align-items: center; justify-content: center;
+ border-radius: 999px; border: none; font-weight: 600; font-size: 14px;
+ padding: 11px 24px;
+ transition: transform 0.2s var(--ease), background 0.2s ease, opacity 0.2s ease;
+}
+.btn-white { background: #fff; color: #000; }
+.btn-white:hover { transform: translateY(-1px); opacity: 0.92; }
+.btn-dark { background: var(--pill-dark); color: #c8c8c8; }
+.btn-dark:hover { background: #323234; color: #fff; transform: translateY(-1px); }
+.btn-cta {
+ background: #fff; color: #000;
+ box-shadow: 0 0 0 1px rgba(255,255,255,0.15), 0 0 22px rgba(255,255,255,0.32), 0 0 44px rgba(255,255,255,0.12);
+ font-size: clamp(13.5px, 1.5vw, 14.5px);
+ padding: clamp(11px, 1.6vh, 13px) clamp(22px, 3vw, 28px);
+}
+.btn-cta:hover {
+ transform: translateY(-2px) scale(1.02);
+ box-shadow: 0 0 0 1px rgba(255,255,255,0.3), 0 0 34px rgba(255,255,255,0.5), 0 0 60px rgba(255,255,255,0.2);
+}
+
+/* ---------- 原子:Tag / Kicker / StatGlyph / SectionHead ---------- */
+.tag {
+ display: inline-block; font-size: 11px; font-weight: 600;
+ color: #000; background: #fff; border-radius: 999px; padding: 3px 10px; margin-bottom: 10px;
+}
+.tag-ghost { background: transparent; color: #8e8e8e; border: 1px solid var(--line); }
+.kicker {
+ font-family: var(--font-display);
+ font-size: clamp(13px, 1.6vw, 16px);
+ color: var(--muted); letter-spacing: 0.14em; text-transform: uppercase;
+ margin-bottom: 12px;
+}
+.stat-glyph {
+ font-family: var(--font-display);
+ font-size: clamp(22px, 3vw, 33px);
+ color: #fff; line-height: 1;
+}
+.section-head { display: flex; align-items: baseline; gap: 14px; margin-bottom: 18px; }
+.section-head .no { font-family: var(--font-display); color: var(--muted); font-size: 18px; letter-spacing: 0.05em; }
+.section-head h2 { font-size: clamp(20px, 2.6vw, 26px); font-weight: 600; letter-spacing: -0.02em; color: #fff; }
+.section-head .en { font-family: var(--font-display); color: #5b5b5b; font-size: 13px; margin-top: 3px; }
+
+/* ---------- 分子:NavPill / TrustRow / StatItem / Card ---------- */
+.nav-pill {
+ flex: 1; height: clamp(44px, 5.2vw, 48px);
+ max-width: 640px;
+ background: #fff; border-radius: 999px;
+ padding: 4px 8px; box-shadow: var(--nav-shadow);
+ display: flex; align-items: center; justify-content: center; gap: 2px;
+ overflow: hidden;
+}
+.nav-link {
+ position: relative;
+ font-weight: 500; font-size: clamp(13px, 1.4vw, 15px);
+ letter-spacing: -0.01em; color: var(--nav-text);
+ padding: 9px 12px; border-radius: 999px;
+ opacity: 0.5; transition: opacity 0.2s ease, background 0.2s ease;
+ white-space: nowrap;
+}
+.nav-link:hover { opacity: 0.75; }
+.nav-link.active { opacity: 1; }
+.nav-link.active::after {
+ content: "";
+ position: absolute; left: 50%; bottom: 5px;
+ transform: translateX(-50%);
+ width: 3px; height: 3px; border-radius: 50%; background: #000;
+ box-shadow: -5px 0 0 #000, 5px 0 0 #000;
+}
+.trust-row {
+ --trust-size: clamp(36px, 4.5vw, 42px);
+ display: inline-flex; align-items: center;
+ margin-bottom: clamp(16px, 2.5vh, 26px);
+}
+.trust-avatar {
+ width: var(--trust-size); height: var(--trust-size);
+ border-radius: 50%;
+ background: var(--trust-bg);
+ border: 1px solid var(--trust-border);
+ padding: 5px; position: relative;
+ transition: transform 0.35s var(--ease);
+}
+.trust-avatar .inner { width: 100%; height: 100%; border-radius: 50%; background: #fff; display: grid; place-items: center; }
+.trust-avatar i { font-size: calc(var(--trust-size) * 0.34); color: #111; }
+.trust-avatar.a1 { z-index: 1; }
+.trust-avatar.a2 { z-index: 2; margin-left: calc(var(--trust-size) * -0.42); }
+.trust-avatar.a3 { z-index: 4; margin-left: calc(var(--trust-size) * -0.42); }
+.trust-avatar.a1:hover { transform: translateY(-2px); }
+.trust-avatar.a2:hover { transform: translateY(-4px); }
+.trust-avatar.a3:hover { transform: translateY(-2px); }
+.trust-pill {
+ height: var(--trust-size);
+ background: var(--trust-bg);
+ border: 1px solid var(--trust-border); border-radius: 999px;
+ margin-left: calc(var(--trust-size) * -0.42);
+ padding-left: calc(var(--trust-size) * 0.58);
+ padding-right: 22px;
+ display: flex; align-items: center; white-space: nowrap;
+ font-weight: 500; font-size: clamp(12px, 1.4vw, 13.5px);
+ color: var(--trust-text);
+}
+.stat { display: flex; flex-direction: column; align-items: center; text-align: center; }
+.stat .value {
+ font-family: var(--font-sans);
+ font-size: clamp(18px, 2.2vw, 26px);
+ color: #fff; letter-spacing: -0.025em;
+ font-variant-numeric: tabular-nums; line-height: 1.2;
+}
+.stat .label { color: var(--muted); font-size: clamp(11px, 1.2vw, 12.5px); margin-top: 3px; }
+
+.card {
+ background: var(--panel);
+ border: 1px solid var(--line);
+ border-radius: var(--radius-card);
+ padding: 20px;
+ transition: transform 0.3s var(--ease), border-color 0.3s ease, background 0.3s ease;
+ position: relative; overflow: hidden;
+}
+.card:hover { transform: translateY(-3px); border-color: rgba(255,255,255,0.3); background: var(--panel-2); }
+.card .t { font-weight: 600; font-size: 17px; letter-spacing: -0.01em; margin-bottom: 8px; }
+.card p, .card li { color: #a9a9a9; font-size: 13.5px; line-height: 1.6; }
+.card li { padding-left: 16px; position: relative; margin-top: 5px; }
+.card li::before { content: ""; position: absolute; left: 0; top: 8px; width: 5px; height: 5px; border-radius: 50%; background: #fff; }
+.card b { color: #fff; font-weight: 600; }
+
+/* ---------- 分子:Flow / Timeline / DataTable / RangeField ---------- */
+.flow { display: flex; align-items: stretch; gap: 10px; flex-wrap: wrap; }
+.flow .node {
+ flex: 1 1 160px;
+ background: var(--panel); border: 1px solid var(--line); border-radius: 14px;
+ padding: 14px; position: relative;
+}
+.flow .node .step { font-family: var(--font-display); color: #6f6f6f; font-size: 13px; }
+.flow .node h4 { font-size: 14.5px; font-weight: 600; margin: 6px 0 4px; }
+.flow .node p { font-size: 12.5px; color: #8e8e8e; line-height: 1.55; }
+.flow .node:not(:last-child)::after {
+ content: "→"; position: absolute; right: -12px; top: 50%; transform: translateY(-50%);
+ color: #3f3f3f; font-size: 16px; z-index: 1;
+}
+.timeline { display: grid; gap: 12px; }
+.timeline .tl-row { display: grid; grid-template-columns: 110px 1fr; gap: 14px; align-items: start; }
+.timeline .tl-time { font-family: var(--font-display); color: #fff; font-size: 14px; padding-top: 12px; text-align: right; }
+.timeline .tl-body { background: var(--panel); border: 1px solid var(--line); border-radius: 14px; padding: 12px 16px; }
+.timeline .tl-body h4 { font-size: 14.5px; font-weight: 600; }
+.timeline .tl-body p { font-size: 13px; color: #9c9c9c; margin-top: 4px; line-height: 1.6; }
+.table-wrap { border: 1px solid var(--line); border-radius: 14px; overflow-x: auto; background: var(--panel); }
+table.tbl { width: 100%; border-collapse: collapse; min-width: 640px; font-size: 13.5px; }
+table.tbl th {
+ text-align: left; font-weight: 500; color: var(--muted);
+ padding: 12px 14px; border-bottom: 1px solid var(--line);
+ font-size: 12px; letter-spacing: 0.04em; white-space: nowrap;
+}
+table.tbl td { padding: 12px 14px; border-bottom: 1px solid rgba(255,255,255,0.06); color: #c4c2c3; vertical-align: top; line-height: 1.55; }
+table.tbl tr:last-child td { border-bottom: 0; }
+table.tbl td.hl { color: #fff; font-weight: 600; white-space: nowrap; }
+
+.field { margin-bottom: 14px; }
+.field label { display: block; font-size: 12.5px; color: var(--muted); margin-bottom: 6px; }
+.field input, .field select, .field textarea {
+ width: 100%; padding: 10px 12px; border-radius: 10px;
+ background: #161618; border: 1px solid #2a2a2d; color: #fff;
+ font-family: inherit; font-size: 14px; outline: none;
+ transition: border-color 0.2s ease;
+}
+.field input:focus, .field select:focus, .field textarea:focus { border-color: #fff; }
+.field textarea { resize: vertical; min-height: 64px; }
+.field .range { display: flex; align-items: center; gap: 10px; }
+.field .range output { font-family: var(--font-display); color: #fff; min-width: 34px; text-align: center; }
+input[type="range"] { accent-color: #fff; }
+.rows2 { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
+.check-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 8px; }
+.check-grid label { display: flex; align-items: center; gap: 8px; font-size: 13px; color: #c4c2c3; cursor: pointer; }
+.check-grid input { width: 16px; height: 16px; accent-color: #fff; }
+
+/* ---------- 组织:Header / MobileMenu / Hero / Stats / Footer ---------- */
+.site-header {
+ position: sticky; top: 0; z-index: 20;
+ width: 100%; display: flex; justify-content: center;
+ padding: clamp(12px, 2vh, 20px) 0 clamp(10px, 1.6vh, 16px);
+ background: rgba(0,0,0,0.72);
+ backdrop-filter: blur(10px);
+ -webkit-backdrop-filter: blur(10px);
+}
+.header-inner {
+ display: flex; align-items: center; justify-content: space-between;
+ width: 100%; max-width: 720px;
+ gap: clamp(18px, 2.8vw, 28px);
+ animation: slideDown 0.7s var(--ease) both;
+}
+.logo {
+ flex: 0 0 auto;
+ width: clamp(40px, 4.4vw, 46px); height: clamp(40px, 4.4vw, 46px);
+ border-radius: 50%; background: #fff;
+ box-shadow: var(--nav-shadow);
+ display: grid; place-items: center;
+ transition: transform 0.25s var(--ease);
+}
+.logo:hover { transform: scale(1.04); }
+.logo img { width: 72%; height: 72%; object-fit: contain; }
+.btn-sign {
+ flex: 0 0 auto;
+ height: clamp(44px, 5.2vw, 48px);
+ padding: 0 22px; border-radius: 999px; border: none;
+ background: var(--pill-dark); color: var(--sign-in-text);
+ font-weight: 500; font-size: clamp(13px, 1.4vw, 15px);
+ box-shadow: var(--nav-shadow);
+ display: inline-flex; align-items: center;
+ transition: background 0.2s ease, color 0.2s ease, transform 0.2s ease;
+}
+.btn-sign:hover { background: #323234; color: #fff; transform: translateY(-1px); }
+.user-nick {
+ flex: 0 0 auto;
+ font-family: var(--font-display);
+ font-size: clamp(15px, 1.8vw, 18px);
+ letter-spacing: 0.04em;
+ color: #fff;
+ border: 1px solid rgba(255,255,255,.22);
+ border-radius: 999px;
+ padding: 8px 18px;
+ background: rgba(40,40,42,.7);
+ white-space: nowrap;
+ box-shadow: var(--nav-shadow);
+}
+.burger { display: none; }
+
+.menu-overlay {
+ position: fixed; inset: 0; z-index: 50;
+ background: rgba(0,0,0,0.62); backdrop-filter: blur(6px);
+ animation: overlayIn 0.28s ease both;
+}
+.menu-sheet {
+ position: fixed; top: 74px; left: 50%; transform: translateX(-50%);
+ z-index: 51; width: min(560px, 92vw);
+ background: #fff; color: #000; border-radius: 28px;
+ padding: 22px 18px 20px;
+ box-shadow: 0 20px 60px rgba(0,0,0,0.45);
+ animation: menuIn 0.38s var(--ease) both;
+}
+.menu-sheet a {
+ display: block; padding: 14px 10px; font-weight: 500; font-size: 17px; color: #000;
+ border-radius: 12px; position: relative;
+ animation: linkIn 0.4s var(--ease) both;
+}
+.menu-sheet a:hover { background: #f2f2f2; }
+.menu-sheet a.active::after {
+ content: ""; position: absolute; left: 10px; bottom: 8px;
+ width: 3px; height: 3px; border-radius: 50%; background: #000;
+ box-shadow: -5px 0 0 #000, 5px 0 0 #000;
+}
+.menu-sheet .menu-sign { margin-top: 10px; text-align: center; border-radius: 999px; background: #000; color: #fff; padding: 14px; }
+
+.hero {
+ flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center;
+ text-align: center; max-width: 900px; width: 100%; margin: 0 auto;
+}
+.headline {
+ font-family: var(--font-display);
+ color: #fff; text-align: center;
+ font-size: clamp(28px, 6.2vw, 80px);
+ line-height: 1.12; letter-spacing: -0.04em;
+ white-space: nowrap; overflow: hidden;
+}
+.headline .hl-line {
+ display: block;
+ opacity: 0; transform: translateY(14px);
+ animation: headlineFade 0.85s var(--ease) forwards;
+}
+.headline .hl-line:nth-child(1) { animation-delay: 0.12s; }
+.headline .hl-line:nth-child(2) { animation-delay: 0.3s; }
+.slogan {
+ font-size: clamp(15px, 2vw, 19px);
+ font-weight: 600;
+ color: #fff;
+ letter-spacing: 0.01em;
+ margin-top: clamp(18px, 2.8vh, 30px);
+ opacity: 0.96;
+}
+.subhead {
+ max-width: min(500px, 92%);
+ font-size: clamp(calc(13.5px + 2pt), calc(1.55vw + 2pt), calc(16.5px + 2pt));
+ color: #d0d0d0; opacity: 0.8;
+ line-height: 1.55; font-weight: 400;
+ margin-top: clamp(16px, 2.6vh, 26px);
+}
+.cta-row { margin-top: clamp(22px, 3.6vh, 34px); }
+.stats {
+ width: 100%; max-width: 920px; margin: 0 auto;
+ display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px;
+ padding-top: clamp(12px, 2vh, 20px);
+}
+.site-footer {
+ border-top: 1px solid var(--line);
+ padding: 22px clamp(16px, 3vw, 32px);
+ color: #5b5b5b; font-size: 12.5px;
+ display: flex; justify-content: space-between; gap: 12px; flex-wrap: wrap;
+ flex-shrink: 0; margin-bottom: 0;
+}
+
+/* ---------- 模板:Landing / Content ---------- */
+/* 全站统一粒子背景(全局固定挂载,滚动不消失) */
+.bg-scene { position: fixed; inset: 0; z-index: 0; pointer-events: none; background: #000; }
+.bg-scene canvas { position: absolute; inset: 0; width: 100%; height: 100%; }
+.bg-shade {
+ position: absolute; inset: 0;
+ /* 粒子自带渐变,这里只做暗角保证文字可读,不再叠加平铺渐变 */
+ background: linear-gradient(180deg, rgba(0,0,0,0.42) 0%, rgba(0,0,0,0) 28%, rgba(0,0,0,0) 62%, rgba(0,0,0,0.62) 100%);
+}
+.page-landing {
+ position: relative; z-index: 1;
+ height: 100vh; height: 100dvh; overflow: hidden;
+ display: flex; flex-direction: column;
+ padding: clamp(16px, 2.4vh, 28px) clamp(14px, 3vw, 32px);
+}
+.page-landing .site-header, .page-landing .hero, .page-landing .stats { z-index: 1; }
+.page-content {
+ position: relative; z-index: 1;
+ min-height: 100vh; min-height: 100dvh;
+ display: flex; flex-direction: column;
+ overflow-x: clip;
+ /* 页面切换淡入:
重挂载即触发,背景恒定故只淡内容区 */
+ animation: pageIn 0.28s var(--ease) both;
+}
+@keyframes pageIn { from { opacity: 0; } to { opacity: 1; } }
+
+/* ---------- 骨架屏(Skeleton) ---------- */
+.sk { position: relative; overflow: hidden; background: #1d1d24; border-radius: 8px; }
+.sk::after { content: ''; position: absolute; inset: 0; transform: translateX(-100%);
+ background: linear-gradient(90deg, transparent, rgba(255,255,255,0.06), transparent);
+ animation: sk-shimmer 1.4s ease-in-out infinite; }
+.sk-round { border-radius: 50%; }
+@keyframes sk-shimmer { 0% { transform: translateX(-100%); } 100% { transform: translateX(100%); } }
+.sk-line { display: block; background: #1d1d24; }
+.sk-row { display: flex; gap: 10px; align-items: center; }
+.content-main { flex: 1; width: 100%; max-width: 1060px; margin: 0 auto; padding: clamp(28px, 5vh, 56px) clamp(16px, 3vw, 32px) 70px; }
+.content-main > header.page-hero { margin-bottom: clamp(26px, 4vh, 44px); }
+.page-title {
+ font-family: var(--font-display);
+ font-size: clamp(30px, 5vw, 56px);
+ line-height: 1.08; letter-spacing: -0.03em;
+ color: #fff; margin-bottom: 14px;
+}
+.page-desc { max-width: 620px; color: #c4c2c3; line-height: 1.6; font-size: 15px; }
+.section { margin-top: clamp(34px, 6vh, 60px); }
+
+/* ---------- 网格 ---------- */
+.grid { display: grid; gap: 14px; }
+.grid-c2 { grid-template-columns: repeat(2, 1fr); }
+.grid-c3 { grid-template-columns: repeat(3, 1fr); }
+.grid-c4 { grid-template-columns: repeat(4, 1fr); }
+
+/* ---------- 表单容器 ---------- */
+.form-card { background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius-card); padding: 22px; margin-top: 14px; }
+.form-card h3 { font-size: 16px; font-weight: 600; margin-bottom: 4px; }
+.form-card .hint { font-size: 12.5px; color: var(--muted); margin-bottom: 16px; }
+.form-actions { display: flex; gap: 10px; margin-top: 18px; flex-wrap: wrap; }
+.result-box {
+ margin-top: 16px; border: 1px solid rgba(255,255,255,0.25); border-radius: 12px;
+ background: #0d0d10; padding: 16px; font-size: 13.5px; color: #cfcfcf; line-height: 1.7;
+}
+.result-box b { color: #fff; }
+.result-box .big { font-family: var(--font-display); font-size: 26px; color: #fff; }
+
+/* ---------- 响应式 ---------- */
+@media (max-width: 900px) {
+ .grid-c3, .grid-c4 { grid-template-columns: repeat(2, 1fr); }
+}
+@media (max-width: 720px) {
+ .nav-pill, .btn-sign, .user-nick { display: none; }
+ .header-inner { max-width: 100%; }
+ .burger {
+ display: block;
+ width: 48px; height: 48px; border-radius: 50%;
+ background: var(--pill-dark); border: none; position: relative;
+ transition: background 0.25s ease;
+ }
+ .burger span {
+ position: absolute; left: 15px; width: 18px; height: 1.5px; background: #fff;
+ transition: transform 0.25s ease, opacity 0.25s ease;
+ }
+ .burger span:nth-child(1) { top: 20px; }
+ .burger span:nth-child(2) { top: 23.5px; }
+ .burger span:nth-child(3) { top: 27px; }
+ .burger.open { background: #fff; }
+ .burger.open span { background: #000; }
+ .burger.open span:nth-child(1) { top: 23.5px; transform: translateY(6.5px) rotate(45deg); }
+ .burger.open span:nth-child(2) { opacity: 0; }
+ .burger.open span:nth-child(3) { top: 23.5px; transform: translateY(-6.5px) rotate(-45deg); }
+ .stats { grid-template-columns: repeat(2, 1fr); }
+ .headline { letter-spacing: -0.08em; line-height: 1.05; }
+ .grid-c2, .grid-c3, .grid-c4 { grid-template-columns: 1fr; }
+ .rows2 { grid-template-columns: 1fr; }
+ .timeline .tl-row { grid-template-columns: 78px 1fr; }
+}
+@media (max-width: 420px) {
+ .headline { letter-spacing: -0.09em; line-height: 1.04; }
+ .trust-row { --trust-size: 34px; }
+ .trust-pill { padding-right: 14px; font-size: 12px; }
+}
+
+/* ---------- 无障碍 ---------- */
+@media (prefers-reduced-motion: reduce) {
+ *, *::before, *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; }
+ .anim, .anim-scroll, .headline .hl-line { opacity: 1 !important; transform: none !important; filter: none !important; }
+}
diff --git a/website/src/styles/home.css b/website/src/styles/home.css
new file mode 100644
index 0000000..650c447
--- /dev/null
+++ b/website/src/styles/home.css
@@ -0,0 +1,193 @@
+/* ============================================================
+ 首页营销落地页 · 样式
+ 延续暗色令牌 + 白色按钮 + 面板卡片的品牌语言
+ ============================================================ */
+.page-marketing {
+ position: relative; z-index: 1;
+ min-height: 100vh; min-height: 100dvh;
+ display: flex; flex-direction: column;
+ overflow-x: clip;
+}
+.marketing-main { flex: 1; width: 100%; max-width: 1060px; margin: 0 auto; padding: 0 clamp(16px, 3vw, 32px) 60px; }
+.mk-section { margin-top: clamp(40px, 6vh, 64px); }
+
+/* ---------- 1. Hero ---------- */
+.mk-hero {
+ min-height: 78vh; display: flex; flex-direction: column; align-items: center; justify-content: center;
+ text-align: center; padding: clamp(40px, 8vh, 90px) 0 clamp(28px, 5vh, 48px);
+}
+.mk-badge {
+ display: inline-flex; align-items: center; gap: 8px;
+ border: 1px solid rgba(255,255,255,.22); color: #c8c8c8;
+ background: rgba(40,40,42,.6); border-radius: 999px; padding: 7px 18px;
+ font-size: 12.5px; letter-spacing: .02em; margin-bottom: 22px;
+}
+.mk-headline {
+ font-family: var(--font-display);
+ color: #fff; font-size: clamp(34px, 7vw, 84px);
+ line-height: 1.1; letter-spacing: -.03em; white-space: nowrap;
+}
+.mk-headline span { display: block; }
+.mk-subhead {
+ max-width: 560px; margin-top: 20px;
+ font-size: 15.5px; color: #c4c2c3; line-height: 1.75;
+}
+.mk-subhead b { color: #fff; font-weight: 600; }
+.mk-cta { display: flex; gap: 12px; flex-wrap: wrap; justify-content: center; margin-top: 30px; }
+.btn-ghost {
+ display: inline-flex; align-items: center; justify-content: center;
+ border-radius: 999px; border: 1px solid rgba(255,255,255,.3); color: #e8e8e8;
+ background: transparent; font-weight: 600; font-size: 14px; padding: 12px 26px;
+ transition: border-color .2s ease, color .2s ease, transform .2s var(--ease);
+}
+.btn-ghost:hover { border-color: #fff; color: #fff; transform: translateY(-1px); }
+
+/* ---------- 2. 信任条 ---------- */
+.mk-trust {
+ max-width: 920px; margin: 0 auto;
+ display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px;
+ padding: 4px 0 8px;
+}
+.mk-trust .stat { background: var(--panel); border: 1px solid var(--line); border-radius: 16px; padding: 18px 12px; }
+
+/* ---------- 3. 痛点理念对比 ---------- */
+.mk-compare { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
+.mk-cmp {
+ background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius-card);
+ padding: 22px 24px; position: relative; overflow: hidden;
+}
+.mk-cmp.new { border-color: rgba(255,255,255,.45); background: var(--panel-2); }
+.mk-cmp h3 { font-size: 16px; font-weight: 600; margin-bottom: 14px; }
+.mk-cmp.old h3 { color: #8e8e8e; }
+.mk-cmp.new h3 { color: #fff; }
+.mk-cmp li {
+ font-size: 13.5px; color: #a9a9a9; line-height: 1.6; margin-top: 8px; padding-left: 18px; position: relative;
+}
+.mk-cmp.new li { color: #cfcfcf; }
+.mk-cmp li::before { font-family: "Font Awesome 6 Free"; font-weight: 900; content: "\f00d"; position: absolute; left: 0; color: #8e8e8e; font-size: 12px; }
+.mk-cmp.new li::before { font-family: "Font Awesome 6 Free"; font-weight: 900; content: "\f00c"; color: #fff; font-size: 12px; }
+.mk-quote {
+ margin-top: 18px; text-align: center;
+ font-size: 17px; color: #fff; font-weight: 500; letter-spacing: .01em;
+ border-top: 1px solid var(--line); padding-top: 18px;
+}
+.mk-quote b { font-weight: 700; }
+
+/* ---------- 4. 三档产品 ---------- */
+.mk-products { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; }
+.mk-product {
+ background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius-card);
+ padding: 24px; display: flex; flex-direction: column;
+ transition: transform .3s var(--ease), border-color .3s ease, background .3s ease;
+ position: relative; overflow: hidden;
+}
+.mk-product:hover { transform: translateY(-3px); border-color: rgba(255,255,255,.35); background: var(--panel-2); }
+.mk-product.hot { border-color: rgba(255,255,255,.55); box-shadow: 0 0 0 1px #fff; }
+.mk-tag { display: inline-block; font-size: 11px; font-weight: 700; color: #000; background: #fff; border-radius: 999px; padding: 3px 12px; align-self: flex-start; margin-bottom: 12px; }
+.mk-tag.alt { background: #323234; color: #c8c8c8; }
+.mk-pname { font-size: 19px; font-weight: 700; color: #fff; }
+.mk-pprice { font-size: 15px; color: #fff; font-weight: 600; margin-top: 4px; }
+.mk-pprice em { font-style: normal; font-size: 12px; color: #8e8e8e; font-weight: 500; }
+.mk-period { font-size: 12px; color: #8e8e8e; margin: 4px 0 12px; }
+.mk-pdesc { font-size: 13.5px; color: #a9a9a9; line-height: 1.65; margin-bottom: 14px; }
+.mk-pdel { list-style: none; margin-bottom: 20px; }
+.mk-pdel li { font-size: 13px; color: #c4c2c3; line-height: 1.7; padding-left: 16px; position: relative; }
+.mk-pdel li::before { content: ""; position: absolute; left: 2px; top: 8px; width: 5px; height: 5px; border-radius: 50%; background: #fff; }
+.mk-pcta { margin-top: auto; }
+
+/* ---------- 5. 三件套 ---------- */
+.mk-core { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; }
+.mk-core-item {
+ background: var(--panel); border: 1px solid var(--line); border-radius: 14px; padding: 20px;
+ position: relative;
+}
+.mk-core-step { font-family: var(--font-display); color: #6f6f6f; font-size: 14px; letter-spacing: .05em; }
+.mk-core-item h4 { font-size: 16px; font-weight: 600; margin: 8px 0 8px; }
+.mk-core-item p { font-size: 13px; color: #8e8e8e; line-height: 1.6; }
+.mk-core-item .out { font-size: 12.5px; color: #c4c2c3; margin-top: 10px; padding-top: 10px; border-top: 1px dashed var(--line); }
+.mk-core-item .out b { color: #fff; }
+
+/* ---------- 6. 效果数据 ---------- */
+.mk-metrics { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; }
+.mk-metric {
+ background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius-card);
+ padding: 26px 24px; text-align: center;
+}
+.mk-metric .num { font-family: var(--font-display); font-size: clamp(40px, 5vw, 60px); color: #fff; line-height: 1; }
+.mk-metric .lbl { font-size: 14px; color: #c4c2c3; font-weight: 600; margin-top: 10px; }
+.mk-metric .nt { font-size: 12px; color: #6f6f6f; margin-top: 6px; }
+.mk-note { text-align: center; font-size: 13px; color: #6f6f6f; margin-top: 16px; }
+
+/* ---------- 7. 当期排期 · 倒计时 ---------- */
+.mk-count {
+ background: linear-gradient(135deg, #161618, #28282a);
+ border: 1px solid rgba(255,255,255,.22); border-radius: 20px;
+ padding: 28px 24px; text-align: center; margin-bottom: 18px;
+ position: relative; overflow: hidden;
+}
+.mk-count::before { content: ""; position: absolute; inset: 0; background: radial-gradient(420px 160px at 50% 0%, rgba(255,255,255,.08), transparent 70%); }
+.mk-count-lbl { position: relative; font-size: 12px; letter-spacing: .12em; color: #8e8e8e; text-transform: uppercase; }
+.mk-count-nums { position: relative; display: flex; justify-content: center; gap: 12px; margin: 16px 0 10px; flex-wrap: wrap; }
+.mk-count-nums span { font-size: 13px; color: #c4c2c3; }
+.mk-count-nums b { font-family: var(--font-display); font-size: clamp(28px, 6vw, 44px); color: #fff; display: block; line-height: 1; margin-bottom: 4px; }
+.mk-count-name { position: relative; font-size: 15px; font-weight: 600; color: #fff; }
+.mk-note { text-align: center; font-size: 13px; color: #6f6f6f; margin-top: 16px; }
+
+/* ---------- 8. 滚动排期(降级静态) ---------- */
+.mk-opening { display: grid; gap: 10px; }
+.mk-row {
+ display: flex; align-items: center; gap: 16px;
+ background: var(--panel); border: 1px solid var(--line); border-radius: 14px; padding: 16px 20px;
+ transition: border-color .2s ease, background .2s ease;
+}
+.mk-row:hover { border-color: rgba(255,255,255,.35); background: var(--panel-2); }
+.mk-row .mk-tag { margin: 0; flex-shrink: 0; }
+.mk-row .mk-row-item { font-size: 15px; font-weight: 600; color: #fff; }
+.mk-row .mk-row-desc { font-size: 12.5px; color: #8e8e8e; margin-top: 3px; }
+.mk-row .mk-row-arrow { margin-left: auto; color: #6f6f6f; flex-shrink: 0; }
+.mk-open-cta { margin-top: 16px; }
+
+/* ---------- 8. OPC 测评入口 ---------- */
+.mk-assess {
+ background: linear-gradient(135deg, #161618, #28282a);
+ border: 1px solid rgba(255,255,255,.22); border-radius: 20px;
+ padding: clamp(28px, 5vw, 44px) clamp(24px, 4vw, 40px);
+ display: flex; align-items: center; gap: 28px; flex-wrap: wrap;
+ position: relative; overflow: hidden;
+}
+.mk-assess::before {
+ content: ""; position: absolute; inset: 0;
+ background: radial-gradient(460px 200px at 15% 0%, rgba(255,255,255,.1), transparent 70%);
+}
+.mk-assess .assess-txt { position: relative; flex: 1 1 320px; }
+.mk-assess .assess-badge { font-size: 11px; font-weight: 700; letter-spacing: .12em; color: #8e8e8e; text-transform: uppercase; }
+.mk-assess h3 { font-size: clamp(22px, 3.4vw, 30px); font-weight: 700; color: #fff; margin: 8px 0 10px; letter-spacing: -.01em; }
+.mk-assess p { font-size: 14px; color: #c4c2c3; line-height: 1.7; max-width: 520px; }
+.mk-assess .assess-cta { position: relative; flex-shrink: 0; }
+
+/* ---------- 9. 报名 CTA ---------- */
+.mk-signup { text-align: center; padding: 20px 0 6px; }
+.mk-signup h3 { font-size: clamp(22px, 3.4vw, 30px); font-weight: 700; color: #fff; letter-spacing: -.01em; }
+.mk-signup p { max-width: 520px; margin: 12px auto 0; font-size: 14.5px; color: #c4c2c3; line-height: 1.7; }
+.mk-signup .mk-cta { margin-top: 24px; }
+
+/* ---------- 响应式 ---------- */
+@media (max-width: 900px) {
+ .mk-products, .mk-core, .mk-metrics, .mk-trust { grid-template-columns: repeat(2, 1fr); }
+ .mk-compare { grid-template-columns: 1fr; }
+}
+@media (max-width: 720px) {
+ .mk-headline {
+ white-space: normal;
+ font-size: clamp(26px, 8.5vw, 34px);
+ line-height: 1.12; letter-spacing: -.04em;
+ }
+ .mk-badge { font-size: 11.5px; padding: 6px 13px; line-height: 1.5; }
+ .mk-hero { min-height: auto; }
+ .mk-trust { grid-template-columns: repeat(2, 1fr); }
+ .mk-assess { flex-direction: column; text-align: center; }
+ .mk-assess p { margin: 0 auto; }
+}
+@media (max-width: 420px) {
+ .mk-products, .mk-core, .mk-metrics { grid-template-columns: 1fr; }
+}
diff --git a/website/src/styles/opcSurvey.css b/website/src/styles/opcSurvey.css
new file mode 100644
index 0000000..bce8f36
--- /dev/null
+++ b/website/src/styles/opcSurvey.css
@@ -0,0 +1,91 @@
+/* ============================================================
+ OPC 创业伙伴调研 · 样式
+ 延续暗色令牌:面板卡片 + 选项胶囊 + 量表按钮(与政策测评同风格)
+ ============================================================ */
+
+/* ---------- 顶部提示条 ---------- */
+.sv-banner {
+ background: rgba(255, 210, 138, .08);
+ border: 1px solid rgba(255, 210, 138, .25);
+ border-radius: var(--radius-card);
+ padding: 12px 16px;
+ font-size: 13px;
+ color: #f5d9a0;
+ margin-bottom: 4px;
+}
+
+/* ---------- 问题 ---------- */
+.sv-questions { display: grid; gap: 14px; }
+.sv-q {
+ background: var(--panel);
+ border: 1px solid var(--line);
+ border-radius: var(--radius-card);
+ padding: 18px 20px;
+}
+.sv-q-label {
+ font-size: 14px; font-weight: 600; color: #fff;
+ margin-bottom: 12px; line-height: 1.55;
+}
+.sv-req { color: #f0b8b8; margin-left: 2px; }
+.sv-hint { font-size: 12px; font-weight: 400; color: #8e8e8e; margin-left: 8px; }
+
+/* 选项胶囊(单选 / 多选) */
+.sv-opts { display: flex; flex-wrap: wrap; gap: 8px; }
+.sv-opt {
+ border: 1px solid var(--line); background: var(--panel-2); color: #c4c2c3;
+ border-radius: 999px; padding: 9px 15px; font-size: 13px; cursor: pointer; transition: all .2s ease;
+}
+.sv-opt:hover { border-color: rgba(255,255,255,.4); color: #fff; }
+.sv-opt.sel { background: #fff; color: #000; border-color: #fff; font-weight: 600; }
+
+/* 量表(Likert 1–5) */
+.sv-likert { display: flex; flex-wrap: wrap; gap: 8px; }
+.sv-lk {
+ border: 1px solid var(--line); background: var(--panel-2); color: #c4c2c3;
+ border-radius: 10px; padding: 9px 14px; font-size: 13px; cursor: pointer; transition: all .2s ease;
+}
+.sv-lk:hover { border-color: rgba(255,255,255,.4); color: #fff; }
+.sv-lk.sel { background: #fff; color: #000; border-color: #fff; font-weight: 600; }
+
+/* 开放填空 */
+.sv-textarea {
+ width: 100%; box-sizing: border-box;
+ background: var(--panel-2); color: #fff;
+ border: 1px solid var(--line); border-radius: 10px;
+ padding: 10px 12px; font-size: 13px; line-height: 1.6; resize: vertical;
+ font-family: inherit;
+}
+.sv-textarea::placeholder { color: #7a7a7a; }
+.sv-textarea:focus { outline: none; border-color: rgba(255,255,255,.5); }
+
+/* ---------- 提交 ---------- */
+.sv-submit { text-align: center; padding: 8px 0 4px; }
+.sv-submit-note { font-size: 12px; color: #8e8e8e; margin-top: 10px; }
+.sv-actions { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 18px; }
+
+/* ---------- 提交成功 ---------- */
+.sv-thanks {
+ background: linear-gradient(135deg, #161618, #28282a);
+ border: 1px solid rgba(255,255,255,.22);
+ border-radius: 16px; padding: 20px 22px;
+ font-size: 14px; color: #cfcfcf; line-height: 1.8;
+}
+.sv-thanks p { margin: 0 0 8px; }
+
+/* ---------- 政策速览(手风琴) ---------- */
+.sv-accordion {
+ width: 100%; text-align: left;
+ background: var(--panel); border: 1px solid var(--line);
+ border-radius: var(--radius-card); padding: 14px 18px;
+ font-size: 13.5px; color: #fff; cursor: pointer;
+ display: flex; justify-content: space-between; align-items: center;
+}
+.sv-accordion span { color: #8e8e8e; font-size: 12px; }
+.sv-policy { margin-top: 12px; display: grid; gap: 10px; }
+.sv-policy-row {
+ background: var(--panel); border: 1px solid var(--line); border-left: 3px solid #fff;
+ border-radius: 12px; padding: 14px 16px;
+}
+.sv-policy-name { font-size: 14px; font-weight: 600; color: #fff; margin-bottom: 5px; }
+.sv-policy-note { font-size: 13px; color: #a9a9a9; line-height: 1.65; }
+.sv-policy-tip { font-size: 12.5px; color: #f5d9a0; line-height: 1.6; margin-top: 4px; }
diff --git a/website/src/styles/opcTest.css b/website/src/styles/opcTest.css
new file mode 100644
index 0000000..71499f2
--- /dev/null
+++ b/website/src/styles/opcTest.css
@@ -0,0 +1,126 @@
+/* ============================================================
+ OPC 创业基因测评 · 页面样式
+ 延续暗色主题设计令牌:--panel / --line / --pill-dark / muted 灰
+ ============================================================ */
+.ot-wrap { display: flex; flex-direction: column; gap: 18px; }
+
+/* ---------- 版本选择(欢迎) ---------- */
+.ot-intro { color: #c4c2c3; font-size: 14px; line-height: 1.8; max-width: 700px; margin-bottom: 4px; }
+.ot-intro b { color: #fff; }
+.ot-version-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
+.ot-vcard {
+ background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius-card);
+ padding: 20px; cursor: pointer; transition: border-color .25s ease, background .25s ease, transform .25s var(--ease);
+}
+.ot-vcard:hover { border-color: rgba(255,255,255,.35); background: var(--panel-2); transform: translateY(-2px); }
+.ot-vcard.sel { border-color: #fff; background: var(--panel-2); box-shadow: 0 0 0 1px #fff; }
+.ot-vname { font-size: 17px; font-weight: 600; color: #fff; }
+.ot-vmeta { font-size: 12px; color: #8e8e8e; margin: 4px 0 8px; }
+.ot-vdesc { font-size: 13px; color: #a9a9a9; line-height: 1.6; }
+.ot-features { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-top: 6px; }
+.ot-features li { font-size: 12.5px; color: #8e8e8e; line-height: 1.5; padding-left: 16px; position: relative; }
+.ot-features li::before { content: ""; position: absolute; left: 2px; top: 7px; width: 5px; height: 5px; border-radius: 50%; background: #fff; }
+
+/* ---------- 答题区 ---------- */
+.ot-quiz-head { margin-bottom: 14px; }
+.ot-quiz-top { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
+.ot-quiz-title { font-size: 15px; font-weight: 600; color: #fff; }
+.ot-quiz-tag { font-size: 11px; color: #8e8e8e; border: 1px solid var(--line); border-radius: 999px; padding: 2px 10px; margin-left: 8px; }
+.ot-progress-text { font-size: 12.5px; color: #8e8e8e; }
+.ot-progress { height: 4px; background: rgba(255,255,255,.12); border-radius: 2px; overflow: hidden; }
+.ot-progress-fill { height: 100%; background: #fff; transition: width .35s var(--ease); }
+.ot-part { margin-top: 8px; font-size: 12px; color: #8e8e8e; }
+.ot-page-info { text-align: center; font-size: 12px; color: #5b5b5b; margin-bottom: 12px; }
+.ot-qcard {
+ background: var(--panel); border: 1px solid var(--line); border-radius: 14px;
+ padding: 16px 18px; margin-bottom: 12px; transition: border-color .2s ease;
+}
+.ot-qcard.answered { border-color: rgba(255,255,255,.35); }
+.ot-qnum { font-size: 11px; font-weight: 600; letter-spacing: .04em; color: #8e8e8e; margin-bottom: 8px; }
+.ot-qnum .ot-check { color: #fff; margin-left: 4px; font-size: 10px; }
+.ot-qtext { font-size: 15px; font-weight: 600; color: #fff; line-height: 1.6; margin-bottom: 14px; }
+.ot-opts { display: flex; gap: 10px; }
+.ot-opt {
+ flex: 1; text-align: left; padding: 11px 14px; border-radius: 12px; cursor: pointer;
+ border: 1px solid rgba(255,255,255,.2); background: transparent; color: #c4c2c3;
+ font-size: 13.5px; line-height: 1.55; transition: all .2s ease;
+}
+.ot-opt:hover { border-color: rgba(255,255,255,.55); color: #fff; }
+.ot-opt.sel { background: #fff; border-color: #fff; color: #000; font-weight: 600; }
+.ot-opt .opt-key { font-weight: 700; margin-right: 6px; }
+.ot-nav { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-top: 6px; }
+.ot-dots { display: flex; gap: 5px; flex-wrap: wrap; justify-content: center; }
+.ot-dot { width: 8px; height: 8px; border-radius: 50%; background: #2e2e2e; cursor: pointer; transition: all .25s ease; }
+.ot-dot.active { background: #fff; transform: scale(1.3); }
+.ot-dot.done { background: #6f6f6f; }
+
+/* ---------- 报告 ---------- */
+.ot-result-hero {
+ background: linear-gradient(135deg, #161618, #28282a); border: 1px solid rgba(255,255,255,.18);
+ border-radius: 20px; padding: 34px 30px; display: flex; gap: 26px; align-items: center; flex-wrap: wrap;
+ position: relative; overflow: hidden;
+}
+.ot-result-hero::before { content: ""; position: absolute; inset: 0; background: radial-gradient(420px 180px at 20% 0%, rgba(255,255,255,.08), transparent 70%); }
+.ot-hero-left { position: relative; }
+.ot-code {
+ font-family: var(--font-display); font-size: clamp(56px, 10vw, 96px); line-height: 1;
+ letter-spacing: .04em; color: #fff; text-shadow: 0 6px 30px rgba(255,255,255,.18);
+}
+.ot-persona { font-size: 20px; font-weight: 600; color: #fff; margin-top: 8px; }
+.ot-tagline { font-size: 12.5px; color: #8e8e8e; margin-top: 4px; max-width: 460px; line-height: 1.6; }
+.ot-hero-right { position: relative; margin-left: auto; text-align: center; }
+.ot-ring {
+ width: 108px; height: 108px; border-radius: 50%; display: grid; place-items: center;
+ background: conic-gradient(#fff 0% var(--p), rgba(255,255,255,.14) var(--p) 100%);
+}
+.ot-ring-inner { width: 84px; height: 84px; border-radius: 50%; background: #161618; display: grid; place-items: center; }
+.ot-ring-num { font-family: var(--font-display); font-size: 30px; color: #fff; }
+.ot-ring-label { font-size: 11px; color: #8e8e8e; margin-top: 8px; letter-spacing: .06em; }
+.ot-verdict { position: relative; width: 100%; margin-top: 4px; font-size: 13.5px; color: #cfcfcf; line-height: 1.7; }
+.ot-weak { position: relative; font-size: 12.5px; color: #8e8e8e; margin-top: 4px; }
+
+.ot-section { margin-top: 22px; }
+.ot-section-title { font-size: 16px; font-weight: 600; color: #fff; margin-bottom: 14px; letter-spacing: -.01em; }
+.ot-section-title .ot-accent { color: #8e8e8e; font-weight: 500; }
+
+.ot-dim { display: flex; align-items: center; gap: 12px; margin-bottom: 10px; }
+.ot-dim-label { width: 64px; font-size: 13px; color: #8e8e8e; font-weight: 600; text-align: right; flex-shrink: 0; }
+.ot-dim-bar { flex: 1; height: 18px; background: rgba(255,255,255,.08); border-radius: 9px; overflow: hidden; }
+.ot-dim-fill { height: 100%; border-radius: 9px; display: flex; align-items: center; justify-content: flex-end; padding-right: 7px; font-size: 11px; font-weight: 700; color: #000; transition: width .8s var(--ease); min-width: 24px; }
+
+.ot-axis { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; }
+.ot-axis-name { width: 44px; font-size: 12px; color: #8e8e8e; text-align: right; flex-shrink: 0; }
+.ot-axis-win { width: 24px; height: 24px; border-radius: 50%; color: #000; background: #fff; display: grid; place-items: center; font-weight: 800; font-size: 12px; flex-shrink: 0; }
+.ot-axis-bar { flex: 1; height: 6px; background: rgba(255,255,255,.12); border-radius: 3px; overflow: hidden; }
+.ot-axis-fill { height: 100%; background: #fff; border-radius: 3px; }
+.ot-axis-pct { width: 44px; font-size: 11px; color: #6f6f6f; text-align: right; flex-shrink: 0; }
+
+.ot-card {
+ background: var(--panel); border: 1px solid var(--line); border-left: 3px solid #fff;
+ border-radius: 14px; padding: 16px 18px; margin-bottom: 12px;
+}
+.ot-card-head { font-size: 15px; font-weight: 600; color: #fff; margin-bottom: 8px; }
+.ot-badge { display: inline-block; font-size: 11px; font-weight: 600; color: #000; background: #fff; border-radius: 999px; padding: 2px 10px; margin-left: 8px; vertical-align: 2px; }
+.ot-badge.alt { background: #323234; color: #c8c8c8; }
+.ot-desc { font-size: 13px; color: #a9a9a9; line-height: 1.65; margin-bottom: 8px; }
+.ot-line { font-size: 13px; color: #c4c2c3; line-height: 1.7; margin-top: 5px; }
+.ot-line b { color: #fff; font-weight: 600; }
+.ot-line.muted { color: #6f6f6f; }
+
+.ot-steps { background: var(--panel); border: 1px solid var(--line); border-radius: 14px; padding: 18px 20px; }
+.ot-steps li { font-size: 13.5px; color: #c4c2c3; line-height: 1.9; padding-left: 16px; position: relative; }
+.ot-steps li::before { content: "→"; position: absolute; left: 0; color: #fff; }
+.ot-steps li b { color: #fff; }
+
+.ot-note { font-size: 12px; color: #6f6f6f; line-height: 1.8; background: var(--panel); border: 1px dashed var(--line); border-radius: 12px; padding: 14px 16px; margin-top: 16px; }
+
+.ot-actions { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 20px; }
+
+@media (max-width: 720px) {
+ .ot-version-grid { grid-template-columns: 1fr; }
+ .ot-features { grid-template-columns: 1fr; }
+ .ot-opts { flex-direction: column; gap: 8px; }
+ .ot-result-hero { padding: 26px 20px; }
+ .ot-hero-right { margin-left: 0; }
+ .ot-dim-label { width: 52px; font-size: 12px; }
+}
diff --git a/website/src/styles/pineAdmin.css b/website/src/styles/pineAdmin.css
new file mode 100644
index 0000000..81348f8
--- /dev/null
+++ b/website/src/styles/pineAdmin.css
@@ -0,0 +1,64 @@
+/* ============================================================
+ 后台管理(/pine · 运营管理)样式
+ 暗色令牌延续:面板卡片 + 白色主按钮 + 状态徽章
+ ============================================================ */
+
+/* ---------- 统计卡 ---------- */
+.ad-stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin-bottom: 8px; }
+.ad-stat {
+ background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius-card);
+ padding: 18px; display: flex; flex-direction: column; gap: 6px;
+}
+.ad-stat.acc { border-color: rgba(255,255,255,.45); background: var(--panel-2); }
+.ad-stat-icon { font-size: 16px; color: #6f6f6f; }
+.ad-stat-num { font-family: var(--font-display); font-size: clamp(26px, 3vw, 34px); color: #fff; line-height: 1; }
+.ad-stat-label { font-size: 12px; color: #8e8e8e; }
+
+/* ---------- 管理工具条 / 表格 ---------- */
+.ad-toolbar { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; margin-bottom: 12px; }
+.ad-toolbar .filters { display: flex; gap: 8px; flex-wrap: wrap; }
+.ad-btn {
+ border-radius: 999px; border: 1px solid var(--line); background: var(--panel); color: #c8c8c8;
+ font-size: 12.5px; font-weight: 600; padding: 8px 16px; cursor: pointer; transition: all .2s ease;
+}
+.ad-btn:hover { border-color: rgba(255,255,255,.4); color: #fff; }
+.ad-btn.active { background: #fff; color: #000; border-color: #fff; }
+.ad-btn.danger { color: #ff9d9d; }
+.ad-btn.danger:hover { border-color: rgba(255,120,120,.5); }
+
+.ad-card { background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius-card); padding: 20px; }
+.ad-table-wrap { overflow-x: auto; }
+.ad-table { width: 100%; border-collapse: collapse; min-width: 760px; font-size: 13px; }
+.ad-table th { text-align: left; font-weight: 500; color: var(--muted); padding: 10px 12px; border-bottom: 1px solid var(--line); white-space: nowrap; font-size: 12px; }
+.ad-table td { padding: 10px 12px; border-bottom: 1px solid rgba(255,255,255,.06); color: #c4c2c3; vertical-align: middle; }
+.ad-table tr:last-child td { border-bottom: 0; }
+.ad-table td.hl { color: #fff; font-weight: 600; }
+.ad-table td .sub { color: #6f6f6f; font-size: 12px; }
+.ad-table select {
+ background: #161618; border: 1px solid #2a2a2d; color: #fff; border-radius: 8px; padding: 5px 8px; font-size: 12.5px;
+}
+
+/* ---------- 状态徽章 ---------- */
+.st-badge {
+ display: inline-block; font-size: 11px; font-weight: 600; border-radius: 999px; padding: 3px 10px; white-space: nowrap;
+}
+.s-pending { background: rgba(255,200,100,.15); color: #f5d9a0; border: 1px solid rgba(255,200,100,.35); }
+.s-confirmed { background: rgba(100,180,255,.15); color: #a8d4f5; border: 1px solid rgba(100,180,255,.35); }
+.s-arrived { background: rgba(160,120,255,.15); color: #cfc0f5; border: 1px solid rgba(160,120,255,.35); }
+.s-converted { background: rgba(120,220,160,.15); color: #a8f0c8; border: 1px solid rgba(120,220,160,.35); }
+
+/* ---------- 表单 ---------- */
+.ad-form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
+.ad-form-grid .span2 { grid-column: span 2; }
+.ad-form-actions { display: flex; gap: 10px; margin-top: 14px; flex-wrap: wrap; }
+
+/* ---------- 状态提示 ---------- */
+.ad-note { color: #8e8e8e; font-size: 13px; padding: 14px 4px; }
+.ad-err { background: rgba(220,60,60,.12); border: 1px solid rgba(220,60,60,.4); color: #ff9d9d; border-radius: 12px; padding: 12px 14px; font-size: 13px; }
+.ad-ok { background: rgba(120,220,160,.12); border: 1px solid rgba(120,220,160,.4); color: #a8f0c8; border-radius: 12px; padding: 12px 14px; font-size: 13px; }
+
+@media (max-width: 720px) {
+ .ad-stats { grid-template-columns: repeat(2, 1fr); }
+ .ad-form-grid { grid-template-columns: 1fr; }
+ .ad-form-grid .span2 { grid-column: span 1; }
+}
diff --git a/website/src/styles/policyTest.css b/website/src/styles/policyTest.css
new file mode 100644
index 0000000..ae20415
--- /dev/null
+++ b/website/src/styles/policyTest.css
@@ -0,0 +1,49 @@
+/* ============================================================
+ 政策测评 + 启动流程 · 样式
+ 延续暗色令牌:面板卡片 + 白色主按钮 + 选项胶囊
+ ============================================================ */
+
+/* ---------- 问卷 ---------- */
+.pt-questions { display: grid; gap: 14px; }
+.pt-q { background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius-card); padding: 18px 20px; }
+.pt-q-label { font-size: 14px; font-weight: 600; color: #fff; margin-bottom: 12px; }
+.pt-opts { display: flex; flex-wrap: wrap; gap: 8px; }
+.pt-opt {
+ border: 1px solid var(--line); background: var(--panel-2); color: #c4c2c3;
+ border-radius: 999px; padding: 9px 16px; font-size: 13px; cursor: pointer; transition: all .2s ease;
+}
+.pt-opt:hover { border-color: rgba(255,255,255,.4); color: #fff; }
+.pt-opt.sel { background: #fff; color: #000; border-color: #fff; font-weight: 600; }
+.pt-actions { margin-top: 20px; }
+
+/* ---------- 结果 ---------- */
+.pt-summary {
+ background: linear-gradient(135deg, #161618, #28282a); border: 1px solid rgba(255,255,255,.22);
+ border-radius: 16px; padding: 18px 20px; color: #cfcfcf; font-size: 14px; line-height: 1.7;
+}
+.pt-cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 12px; }
+.pt-card { background: var(--panel); border: 1px solid var(--line); border-left: 3px solid #fff; border-radius: 14px; padding: 16px 18px; }
+.pt-card-title { font-size: 15px; font-weight: 600; color: #fff; }
+.pt-card-org { font-size: 12px; color: #8e8e8e; margin: 4px 0 8px; }
+.pt-card-amount { display: inline-block; font-size: 12.5px; font-weight: 600; color: #000; background: #fff; border-radius: 999px; padding: 3px 10px; margin-bottom: 8px; }
+.pt-card p { font-size: 13px; color: #a9a9a9; line-height: 1.65; }
+
+/* ---------- 启动流程 ---------- */
+.sp-steps { display: grid; gap: 12px; }
+.sp-step { display: flex; gap: 14px; background: var(--panel); border: 1px solid var(--line); border-radius: 14px; padding: 16px 18px; }
+.sp-no {
+ flex-shrink: 0; width: 34px; height: 34px; border-radius: 50%; background: #fff; color: #000;
+ font-family: var(--font-display); font-size: 16px; display: grid; place-items: center;
+}
+.sp-title { font-size: 16px; font-weight: 600; color: #fff; }
+.sp-desc { font-size: 13px; color: #c4c2c3; line-height: 1.65; margin-top: 6px; }
+.sp-meta { display: flex; flex-direction: column; gap: 4px; margin-top: 10px; font-size: 12.5px; color: #9c9c9c; line-height: 1.6; }
+.sp-meta b { color: #fff; }
+.sp-tips {
+ margin-top: 10px; font-size: 12.5px; color: #f5d9a0; line-height: 1.6;
+ background: rgba(255,200,100,.08); border: 1px solid rgba(255,200,100,.2); border-radius: 8px; padding: 8px 12px;
+}
+
+@media (max-width: 720px) {
+ .sp-step { flex-direction: column; }
+}
diff --git a/website/src/styles/tokens.css b/website/src/styles/tokens.css
new file mode 100644
index 0000000..c3a6403
--- /dev/null
+++ b/website/src/styles/tokens.css
@@ -0,0 +1,82 @@
+/* ============================================================
+ 设计令牌(原子层基础)—— 源自 design.md,融合 Appica UI 风格
+ (统一语义色阶 / 层级阴影 / 焦点环 / 圆角 / 字重)
+ ============================================================ */
+:root {
+ --bg: #000000;
+ --text: #ffffff;
+ --muted: #8e8e8e;
+ --nav-text: #2e2e2e;
+ --pill-dark: #28282a;
+ --sign-in-text: #c8c8c8;
+ --nav-shadow: 0 4px 14px rgba(0, 0, 0, 0.16);
+ --trust-bg: #28282a;
+ --trust-border: rgba(255, 255, 255, 0.4);
+ --trust-text: #c4c2c3;
+ --line: rgba(255, 255, 255, 0.14);
+ --panel: #0c0c0e;
+ --panel-2: #111113;
+ --font-sans: "Inter", "Segoe UI", system-ui, sans-serif;
+ --font-display: "BubbledotICG-FinePos", monospace;
+ --ease: cubic-bezier(0.22, 1, 0.36, 1);
+
+ /* ---- 语义色(Appica 风格:暗色基底 + 蓝紫科技主色) ---- */
+ --primary: #5b8cff;
+ --primary-strong: #9d6bff;
+ --primary-gradient: linear-gradient(135deg, #5b8cff 0%, #9d6bff 100%);
+ --primary-soft: rgba(120, 130, 255, 0.14);
+ --accent: #a8c4ff;
+ --accent-cyan: #7fe0ff;
+ --success: #3fb68b;
+ --success-soft: rgba(63, 182, 139, 0.14);
+ --warning: #f5c451;
+ --warning-soft: rgba(245, 196, 81, 0.14);
+ --danger: #f07575;
+ --danger-soft: rgba(240, 117, 117, 0.14);
+ --ring: rgba(168, 196, 255, 0.55);
+
+ /* ---- 层级阴影(Appica:elevation 阶梯) ---- */
+ --shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.3);
+ --shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.32);
+ --shadow-md: 0 8px 24px rgba(0, 0, 0, 0.42);
+ --shadow-lg: 0 16px 48px rgba(0, 0, 0, 0.5);
+ --shadow-glow: 0 0 0 1px rgba(168, 196, 255, 0.4), 0 0 24px rgba(120, 130, 255, 0.18);
+
+ /* ---- 圆角(Appica:统一阶梯) ---- */
+ --radius-xs: 6px;
+ --radius-sm: 10px;
+ --radius-card: 16px;
+ --radius-lg: 22px;
+ --radius-pill: 999px;
+
+ /* ---- 间距 / 字重 ---- */
+ --space-1: 4px;
+ --space-2: 8px;
+ --space-3: 12px;
+ --space-4: 20px;
+ --space-5: 32px;
+ --weight-normal: 400;
+ --weight-medium: 500;
+ --weight-semibold: 600;
+ --weight-bold: 700;
+}
+
+/* 全局可见焦点环(Appica 风格) */
+:focus-visible {
+ outline: 2px solid var(--ring);
+ outline-offset: 2px;
+ border-radius: var(--radius-sm);
+}
+
+/* 图标基础(Appica:24 尺寸、1.5 线宽、圆头) */
+.icon {
+ width: 1.35em;
+ height: 1.35em;
+ flex-shrink: 0;
+ vertical-align: -0.2em;
+ color: inherit;
+}
+.icon--xs { width: 1em; height: 1em; }
+.icon--sm { width: 1.1em; height: 1.1em; }
+.icon--lg { width: 1.7em; height: 1.7em; }
+.icon--xl { width: 2.2em; height: 2.2em; }
diff --git a/website/src/user/Booking.jsx b/website/src/user/Booking.jsx
new file mode 100644
index 0000000..4b85f2d
--- /dev/null
+++ b/website/src/user/Booking.jsx
@@ -0,0 +1,199 @@
+import React from 'react';
+import { ContentTemplate } from '@/components/templates';
+import { FieldText, FieldTextarea, FieldSelect, FieldRadio, FieldCheck, Button, SectionHead, Icon } from '@/components/atoms';
+import { submitBooking, getBookableEvents } from '@/services/booking';
+import { sendCode, saveSession, getUser } from '@/services/auth';
+import { BK_STATUS_OPTIONS, BK_TOPIC_OPTIONS, BK_SOURCE_OPTIONS } from '@/data/booking';
+import '@/styles/booking.css';
+
+const EMPTY = { phone: '', code: '', name: '', status: '', topics: [], question: '', source: '' };
+const fmtEvt = (iso) => {
+ if (!iso) return '';
+ const d = new Date(iso);
+ const p = (n) => String(n).padStart(2, '0');
+ const week = ['日', '一', '二', '三', '四', '五', '六'][d.getDay()];
+ return `${d.getMonth() + 1}月${d.getDate()}日 周${week} ${p(d.getHours())}:${p(d.getMinutes())}`;
+};
+
+export default function Booking() {
+ const [form, setForm] = React.useState(EMPTY);
+ const [events, setEvents] = React.useState([]);
+ const [eventId, setEventId] = React.useState('');
+ const [busy, setBusy] = React.useState(false);
+ const [err, setErr] = React.useState('');
+ const [debug, setDebug] = React.useState('');
+ const [cd, setCd] = React.useState(0);
+ const [done, setDone] = React.useState(null);
+
+ React.useEffect(() => { getBookableEvents().then((l) => setEvents(l)); }, []);
+
+ const set = (key) => (v) => setForm((f) => ({ ...f, [key]: v }));
+ const valid = !!eventId && /^1\d{10}$/.test(form.phone) && form.code.trim() && form.name.trim();
+ const user = getUser();
+
+ const send = async () => {
+ if (!/^1\d{10}$/.test(form.phone)) { setErr('请输入正确的 11 位手机号'); return; }
+ setErr('');
+ setBusy(true);
+ try {
+ const r = await sendCode(form.phone.trim());
+ setDebug(`验证码已发送${r.debugCode ? ',演示验证码:' + r.debugCode : ''}(5 分钟内有效)`);
+ setCd(60);
+ const t = setInterval(() => setCd((v) => { if (v <= 1) { clearInterval(t); return 0; } return v - 1; }), 1000);
+ } catch (ex) {
+ setErr(ex && ex.message ? ex.message : '发送失败,请重试');
+ } finally {
+ setBusy(false);
+ }
+ };
+
+ const submit = async (e) => {
+ e.preventDefault();
+ setErr('');
+ if (!eventId) { setErr('请先选择要报名的场次'); return; }
+ if (!valid) { setErr('请填写手机号、验证码与姓名'); return; }
+ setBusy(true);
+ const ev = events.find((x) => x.id === eventId) || {};
+ try {
+ const r = await submitBooking({
+ phone: form.phone.trim(),
+ code: form.code.trim(),
+ name: form.name.trim(),
+ status: form.status,
+ topics: form.topics,
+ question: form.question,
+ source: form.source,
+ eventId,
+ eventTitle: ev.title || '',
+ eventStart: ev.startAt || ''
+ });
+ // 报名成功即登录:后端返回 token(未注册已自动注册)
+ if (r && r.token) saveSession({ token: r.token, username: r.username, name: r.name });
+ setDone(r);
+ } catch (ex) {
+ setErr(ex && ex.message ? ex.message : '报名失败,请稍后重试');
+ } finally {
+ setBusy(false);
+ }
+ };
+
+ return (
+
+ {done ? (
+
+
+ 报名成功
+ 预约编号:{done.id}
+
+ {done.local
+ ? '当前为本地记录(站点未连接后端)。请通过公众号 / 现场与我们确认到场。'
+ : `已用手机号 ${form.phone} 报名${done.isNew ? ',账号已自动注册' : ',账号已登录'}。我们会在 48 小时内联系你确认场次。`}
+
+
+
+
+
+
+ ) : (
+
+ )}
+
+ );
+}
diff --git a/website/src/user/EventDetail.jsx b/website/src/user/EventDetail.jsx
new file mode 100644
index 0000000..e117011
--- /dev/null
+++ b/website/src/user/EventDetail.jsx
@@ -0,0 +1,135 @@
+import React from 'react';
+import { ContentTemplate } from '@/components/templates';
+import { SectionHead, FieldText, FieldTextarea, FieldSelect, FieldCheck, Button, Icon } from '@/components/atoms';
+import Skeleton from '@/components/Skeleton';
+import { submitBooking, getEventDetail } from '@/services/booking';
+import { getUser, isAuthed } from '@/services/auth';
+import AuthGate from '@/components/AuthGate';
+import { BK_STATUS_OPTIONS, BK_TOPIC_OPTIONS, BK_SOURCE_OPTIONS } from '@/data/booking';
+import '@/styles/booking.css';
+
+const EMPTY = { question: '' };
+const fmtEvt = (iso) => {
+ if (!iso) return '';
+ const d = new Date(iso);
+ const p = (n) => String(n).padStart(2, '0');
+ const week = ['日', '一', '二', '三', '四', '五', '六'][d.getDay()];
+ return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())} 周${week}`;
+};
+const getId = () => new URLSearchParams((window.location.hash.split('?')[1] || '')).get('id');
+
+export default function EventDetail() {
+ const [ev, setEv] = React.useState(null);
+ const [err, setErr] = React.useState('');
+ const [form, setForm] = React.useState(EMPTY);
+ const [busy, setBusy] = React.useState(false);
+ const [debug, setDebug] = React.useState('');
+ const [cd, setCd] = React.useState(0);
+ const [done, setDone] = React.useState(null);
+ const [authed, setAuthed] = React.useState(isAuthed());
+
+ React.useEffect(() => {
+ const id = getId();
+ if (!id) { setErr('缺少活动参数'); return; }
+ getEventDetail(id).then((e) => setEv(e));
+ }, []);
+
+ const set = (key) => (v) => setForm((f) => ({ ...f, [key]: v }));
+ const user = getUser();
+
+ const submit = async (e) => {
+ e.preventDefault();
+ setErr('');
+ setBusy(true);
+ try {
+ // 姓名/状态/主题/来源 已在「个人中心」设置好,报名仅带 eventId + 本场选填问题
+ const r = await submitBooking({
+ question: form.question, eventId: ev.id, eventTitle: ev.title, eventStart: ev.startAt
+ });
+ setDone(r);
+ } catch (ex) { setErr(ex && ex.message ? ex.message : '报名失败,请稍后重试'); }
+ finally { setBusy(false); }
+ };
+
+ if (done) {
+ return (
+
+
+
+ {ev ? ev.title : '报名成功'}
+ {ev ? `${ev.mode === 'online' ? '线上' : '线下'} · ${ev.location}` : ''}
+ 预约编号:{done.id}
+ 已用已登录账号报名成功,活动前我们会短信提醒。
+
+
+
+
+
+
+ );
+ }
+
+ if (!ev && !err) return (
+
+
+
+
+
+
+ );
+ if (!ev) return {err}
;
+
+ return (
+
+ {/* 宣传图 */}
+ {ev.image ? (
+
+
+
+ ) : (
+
+ {ev.type === 'salon' ? '沙龙' : '公益课'} · {ev.mode === 'online' ? '线上' : '线下'}
+ {ev.title}
+
+ )}
+
+ {/* 基本信息 */}
+
+
+
时间{fmtEvt(ev.startAt)}
+
{ev.mode === 'online' ? '入口' : '地点'}{ev.location}
+ {ev.host &&
主办{ev.host}
}
+ {ev.showCapacity && ev.capacity > 0 &&
人数限 {ev.capacity} 人 · 已报名 {ev.enrolled || 0} 人
}
+
+ {ev.desc && {ev.desc}
}
+
+
+ {/* 报名表单 */}
+
+
+ {!authed ? (
+ <>
+ setAuthed(true)} />
+ {err && {err}
}
+ >
+ ) : (
+
+ )}
+
+
+ );
+}
diff --git a/website/src/user/Events.jsx b/website/src/user/Events.jsx
new file mode 100644
index 0000000..fa6c68e
--- /dev/null
+++ b/website/src/user/Events.jsx
@@ -0,0 +1,80 @@
+import React from 'react';
+import { ContentTemplate } from '@/components/templates';
+import { SectionHead, Button, Icon } from '@/components/atoms';
+import { Reveal } from '@/components/organisms';
+import Skeleton from '@/components/Skeleton';
+import { getBookableEvents } from '@/services/booking';
+import '@/styles/booking.css';
+
+const fmtEvt = (iso) => {
+ if (!iso) return '';
+ const d = new Date(iso);
+ const p = (n) => String(n).padStart(2, '0');
+ const week = ['日', '一', '二', '三', '四', '五', '六'][d.getDay()];
+ return `${d.getMonth() + 1}月${d.getDate()}日 周${week} ${p(d.getHours())}:${p(d.getMinutes())}`;
+};
+
+export default function Events() {
+ const [list, setList] = React.useState([]);
+ const [loaded, setLoaded] = React.useState(false);
+ React.useEffect(() => { getBookableEvents().then((l) => { setList(l); setLoaded(true); }); }, []);
+ const SkeletonCard = () => (
+
+ );
+
+ return (
+
+
+ {list.map((e) => {
+ const STATUS_TEXT = { open: '报名中', full: '已满员', done: '已结束', pending: '待开放', invite: '邀请中' };
+ const ST_CLS = { full: ' full', done: ' done', pending: ' pending', invite: ' invite' };
+ const status = STATUS_TEXT[e.status] || '报名中';
+ const st = ST_CLS[e.status] || '';
+ return (
+
+
+ {e.image ?

:
{e.title}
}
+
+ {e.type === 'salon' ? '沙龙' : '公益课'}
+ {e.mode === 'online' ? '线上' : '线下'}
+
+
+
+
{e.title}
+
{e.subtitle || e.desc}
+
+
{fmtEvt(e.startAt)}
+
{e.location}
+ {e.showCapacity && e.capacity > 0 && (
+
名额 {e.enrolled || 0} / {e.capacity}
+ )}
+
+
+ 免费
+ {status}
+
+
+
+
+
+
+ );
+ })}
+ {!loaded && Array.from({ length: 3 }).map((_, i) =>
)}
+ {loaded && list.length === 0 &&
暂无可报名活动,请稍后再来。
}
+
+
+ );
+}
diff --git a/website/src/user/Home.jsx b/website/src/user/Home.jsx
new file mode 100644
index 0000000..f3b24e8
--- /dev/null
+++ b/website/src/user/Home.jsx
@@ -0,0 +1,246 @@
+import React from 'react';
+import { Header, SiteFooter, Reveal } from '@/components/organisms';
+import { SectionHead, Button, Icon } from '@/components/atoms';
+import { StatItem } from '@/components/molecules';
+import Skeleton from '@/components/Skeleton';
+import {
+ MK_HERO, MK_TRUST, MK_PRINCIPLE, MK_PRODUCTS,
+ MK_CORE, MK_OPENING, MK_ASSESS, MK_SIGNUP
+} from '@/data/home';
+import { getCurrentEvents } from '@/services/ops';
+import '@/styles/home.css';
+
+/* ---------- 当期排期:倒计时 + 当期公益课 / 沙龙 ---------- */
+const fmtEvt = (iso) => {
+ const d = new Date(iso);
+ const p = (n) => String(n).padStart(2, '0');
+ const week = ['日', '一', '二', '三', '四', '五', '六'][d.getDay()];
+ return `${d.getMonth() + 1}月${d.getDate()}日 周${week} ${p(d.getHours())}:${p(d.getMinutes())}`;
+};
+
+function CountdownBox({ target }) {
+ const [now, setNow] = React.useState(Date.now());
+ React.useEffect(() => {
+ const t = setInterval(() => setNow(Date.now()), 1000);
+ return () => clearInterval(t);
+ }, []);
+ const diff = Math.max(0, new Date(target).getTime() - now);
+ const d = Math.floor(diff / 86400000);
+ const h = Math.floor((diff % 86400000) / 3600000);
+ const m = Math.floor((diff % 3600000) / 60000);
+ const s = Math.floor((diff % 60000) / 1000);
+ const pad = (n) => String(n).padStart(2, '0');
+ return (
+
+ {d}天{pad(h)}时{pad(m)}分{pad(s)}秒
+
+ );
+}
+
+function EventCard({ e, tag, hot }) {
+ return (
+
+
{tag}
+
{e.title}
+
{fmtEvt(e.startAt)} {e.location}
+
{e.desc || e.subtitle}
+
+
+
+ );
+}
+
+function CurrentEvents() {
+ const [data, setData] = React.useState(null);
+ const [loaded, setLoaded] = React.useState(false);
+ React.useEffect(() => { getCurrentEvents().then((d) => { setData(d || null); setLoaded(true); }); }, []);
+ const next = data && data.next;
+
+ /* 加载中:先展示骨架,不闪静态排期 */
+ if (!loaded) {
+ return (
+
+
+
+
+ );
+ }
+
+ /* 降级:无后端 / 无排期时展示静态排期 */
+ if (!next) {
+ return (
+
+
+
+ {MK_OPENING.rows.map((r) => (
+
+ ))}
+
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
距下一场开始
+
+
{next.title}
+
+
+ {data.free &&
}
+ {data.salon &&
}
+
+
测评
+
OPC 创业基因测评
+
免费 5 分钟
+
先测一测你适合做什么,带着结果来现场聊。
+
+
+
+
+ {MK_OPENING.note}
+
+ );
+}
+
+export default function Home() {
+ const scrollTo = (id) => () => {
+ const el = document.getElementById(id);
+ if (el) el.scrollIntoView({ behavior: 'smooth' });
+ };
+ return (
+
+
+
+
+ {/* 1 · Hero */}
+
+ {MK_HERO.badge}
+
+ {MK_HERO.headline.map((l, i) => {l})}
+
+ {MK_HERO.subhead}
+
+
+
+
+
+
+ {/* 2 · 信任条 */}
+
+ {MK_TRUST.map((s, i) => )}
+
+
+ {/* 3 · 痛点理念 */}
+
+
+
+
+
{MK_PRINCIPLE.oldTitle}
+
{MK_PRINCIPLE.oldPoints.map((p, i) => - {p}
)}
+
+
+
{MK_PRINCIPLE.newTitle}
+
{MK_PRINCIPLE.newPoints.map((p, i) => - {p}
)}
+
+
+ —— {MK_PRINCIPLE.quote}
+
+
+ {/* 4 · 三档产品 */}
+
+
+
+ {MK_PRODUCTS.map((p) => (
+
+
{p.tag}
+
{p.name}
+
{p.price} {p.period}
+
{p.desc}
+
{p.deliverables.map((d, i) => - {d}
)}
+
+ {p.href
+ ?
+ : }
+
+
+ ))}
+
+
+
+ {/* 5 · 三件套 */}
+
+
+
+ {MK_CORE.items.map((it) => (
+
+
{it.step}
+
{it.title}
+
{it.desc}
+
{it.out}
+
+ ))}
+
+ —— {MK_CORE.quote}
+
+
+ {/* 6 · 当期排期(倒计时 + 当期公益课/沙龙;无后端时降级静态排期) */}
+
+
+ {/* 8 · OPC 测评入口 */}
+
+
+
+
{MK_ASSESS.badge}
+
{MK_ASSESS.title}
+
{MK_ASSESS.desc}
+
+
+
+
+
+
+
+ {/* 8.5 · OPC 创业伙伴调研入口 */}
+
+
+
+
调研
+
云南 OPC 创业伙伴调研
+
5–8 分钟匿名问卷,聊聊你在云南做的行业、遇到的难题、AI 赋能与政策诉求——用于云南 OPC 创业生态与政策研究。
+
+
+
+
+
+
+
+ {/* 9 · 报名 CTA */}
+
+
+
{MK_SIGNUP.title}
+
{MK_SIGNUP.desc}
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/website/src/user/OPCSurvey.jsx b/website/src/user/OPCSurvey.jsx
new file mode 100644
index 0000000..d3d1792
--- /dev/null
+++ b/website/src/user/OPCSurvey.jsx
@@ -0,0 +1,217 @@
+import React from 'react';
+import { ContentTemplate } from '@/components/templates';
+import { SectionHead, Button, Icon } from '@/components/atoms';
+import { Reveal } from '@/components/organisms';
+import Skeleton from '@/components/Skeleton';
+import { getSurveyQuestions, submitSurvey } from '@/services/ops';
+import { getUser } from '@/services/auth';
+import '@/styles/opcSurvey.css';
+
+/* 调研题目 / Likert 选项 / 分区(由后端 /api/survey/questions 下发填充) */
+let SV_QUESTIONS = [];
+let SV_SECTIONS = [];
+let LIKE_OPTS = [];
+
+/* ---------- 云南 OPC 政策速览(附加内容,供受访者参考;以官方为准) ---------- */
+const POLICY_ONE_PAGER = [
+ { name: '云岭创业贷', note: '试点昆明/普洱;个人信用贷最高 50 万、抵押担保最高 1000 万;政府性融资担保增信,需银行风控审批。' },
+ { name: '一次性创业补贴', note: 'OPC 经营主体同等享受 5000 元一次性创业补贴,另有社保补贴、就业见习补贴。' },
+ { name: '贷免扶补', note: '10 类重点群体个人最高 20 万、合伙最高 110 万、3 年期财政贴息、可免反担保。' },
+ { name: 'OPC 创业社区', note: '全省创业园区设 OPC 专区,免费拎包工位、补贴场地水电物业;玉溪红塔区、昆明西山庾园社区已落地。' },
+ { name: 'OPC 创业赛事', note: '"创赢未来"2026 人工智能+OPC 专项赛、春城创业荟 OPC 组,晋级进入重点创业项目库。' },
+ { name: '政策入口', note: '以云南省人社厅官网为准;落地细节各地不同,申报前可拨 12333 咨询当地公共就业服务机构。' }
+];
+
+function PolicySection({ open, setOpen }) {
+ return (
+
+
+
+ {open && (
+
+ {POLICY_ONE_PAGER.map((p, i) => (
+
+ ))}
+
+ 政策条款 / 补贴金额 / 申报入口可能动态调整,请以云南省人力资源和社会保障厅官网最新文件为准。
+
+
+ )}
+
+ );
+}
+
+function Success({ onReset, source }) {
+ const [open, setOpen] = React.useState(false);
+ return (
+ <>
+
+
+
+
你的回答已匿名记录,将用于云南 OPC 创业生态建设与政策研究,不会对外泄露个人信息。
+
我们正在组织云南 OPC 伙伴社群与调研回访,欢迎继续参与。
+
+
+
+
+
+
+
+ >
+ );
+}
+
+function Question({ q, value, onChange }) {
+ if (q.type === 'text') {
+ return (
+