feat: import fluid portfolio snapshot
This commit is contained in:
@@ -0,0 +1,236 @@
|
||||
import gsap from "gsap";
|
||||
import { ScrollTrigger } from "gsap/ScrollTrigger";
|
||||
import Lenis from "lenis";
|
||||
import { rounds } from "./dialogue";
|
||||
|
||||
gsap.registerPlugin(ScrollTrigger);
|
||||
|
||||
/* Lenis 平滑滚动 + ScrollTrigger 集成 */
|
||||
export function initSmoothScroll() {
|
||||
const lenis = new Lenis({ lerp: 0.1 });
|
||||
lenis.on("scroll", ScrollTrigger.update);
|
||||
gsap.ticker.add((time) => lenis.raf(time * 1000));
|
||||
gsap.ticker.lagSmoothing(0);
|
||||
return lenis;
|
||||
}
|
||||
|
||||
/* 打字机:scrub 时间轴上的离散字符 reveal */
|
||||
function addTypewriter(tl, textEl, caretEl, fullText, start, duration) {
|
||||
const proxy = { p: 0 };
|
||||
tl.to(
|
||||
proxy,
|
||||
{
|
||||
p: 1,
|
||||
duration,
|
||||
ease: `steps(${fullText.length})`,
|
||||
onUpdate() {
|
||||
const n = Math.round(proxy.p * fullText.length);
|
||||
textEl.textContent = fullText.slice(0, n);
|
||||
if (caretEl) caretEl.style.opacity = proxy.p >= 1 ? "0" : "1";
|
||||
},
|
||||
},
|
||||
start
|
||||
);
|
||||
}
|
||||
|
||||
/* 开场(时间驱动,非滚动)*/
|
||||
export function playBoot(mind) {
|
||||
const boot = document.querySelector("#r-boot");
|
||||
const typedLine = boot.querySelector(".typed-line");
|
||||
const items = boot.querySelectorAll(".gen");
|
||||
gsap.set(boot.querySelector(".answer"), { opacity: 1 });
|
||||
gsap.set(items, { y: 26, autoAlpha: 0 });
|
||||
|
||||
const text = "这是我的数字思维体,向它提问即可。";
|
||||
const proxy = { p: 0 };
|
||||
const tl = gsap.timeline({ delay: 0.3 });
|
||||
|
||||
tl.call(() => mind.setState("thinking"))
|
||||
.to(proxy, {
|
||||
p: 1,
|
||||
duration: 1.4,
|
||||
ease: `steps(${text.length})`,
|
||||
onUpdate: () => {
|
||||
typedLine.textContent = text.slice(0, Math.round(proxy.p * text.length));
|
||||
},
|
||||
}, 0.5)
|
||||
.call(() => mind.setState("answering"), null, 1.9)
|
||||
.to(items, {
|
||||
y: 0,
|
||||
autoAlpha: 1,
|
||||
duration: 0.7,
|
||||
ease: "expo.out",
|
||||
stagger: { each: 0.09, from: "start" },
|
||||
}, 2.0)
|
||||
.call(() => mind.setState("idle"), null, 3.4);
|
||||
return tl;
|
||||
}
|
||||
|
||||
/* 轮次编排:短轮 pin + scrub;长轮(flow)流式 reveal */
|
||||
export function initChoreography(mind, setActive = () => {}) {
|
||||
rounds
|
||||
.filter((r) => r.question)
|
||||
.forEach((round) => {
|
||||
const section = document.getElementById(`r-${round.id}`);
|
||||
const inner = section.querySelector(".round-inner");
|
||||
const qText = section.querySelector(".q-text");
|
||||
const qCaret = section.querySelector(".q-caret");
|
||||
const thinking = section.querySelector(".thinking");
|
||||
const answer = section.querySelector(".answer");
|
||||
const items = section.querySelectorAll(".gen");
|
||||
|
||||
gsap.set(items, { y: 34, autoAlpha: 0 });
|
||||
|
||||
if (round.flow) {
|
||||
buildFlowRound({ round, section, qText, qCaret, thinking, answer, items, mind, setActive });
|
||||
return;
|
||||
}
|
||||
|
||||
const { qEnd, thinkEnd, staggerEach } = round;
|
||||
const burstEnd = 0.8;
|
||||
|
||||
const tl = gsap.timeline({
|
||||
defaults: { ease: "none" },
|
||||
scrollTrigger: {
|
||||
trigger: section,
|
||||
start: "top top",
|
||||
end: round.pinLength,
|
||||
scrub: 0.65,
|
||||
pin: true,
|
||||
anticipatePin: 1,
|
||||
onToggle(self) {
|
||||
if (self.isActive) setActive(round.id);
|
||||
},
|
||||
onEnter: () => {
|
||||
mind.setSide(round.side);
|
||||
mind.setPalette(round.palette);
|
||||
},
|
||||
onEnterBack: () => {
|
||||
mind.setSide(round.side);
|
||||
mind.setPalette(round.palette);
|
||||
},
|
||||
onLeave: () => mind.setState("idle"),
|
||||
onLeaveBack: () => mind.setState("idle"),
|
||||
onUpdate(self) {
|
||||
const p = self.progress;
|
||||
if (p < qEnd) mind.setState("idle");
|
||||
else if (p < thinkEnd) mind.setState("thinking");
|
||||
else if (p < burstEnd) mind.setState("answering");
|
||||
else mind.setState("idle");
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// 1. 提问打字
|
||||
addTypewriter(tl, qText, qCaret, round.question, 0, qEnd);
|
||||
// 2. 思考 shimmer(刻意停顿 = 非线性蓄力)
|
||||
tl.to(thinking, { opacity: 1, duration: 0.03 }, qEnd)
|
||||
.to(thinking, { opacity: 0, duration: 0.04 }, thinkEnd - 0.04);
|
||||
// 3. 爆发生成:不等间隔 stagger 模拟 token 到达
|
||||
tl.to(answer, { opacity: 1, duration: 0.02 }, thinkEnd);
|
||||
tl.to(
|
||||
items,
|
||||
{
|
||||
y: 0,
|
||||
autoAlpha: 1,
|
||||
duration: 0.1,
|
||||
ease: "expo.out",
|
||||
stagger: { each: staggerEach, from: "start" },
|
||||
},
|
||||
thinkEnd + 0.02
|
||||
);
|
||||
// 4. 余韵:内容微视差上浮
|
||||
tl.to(inner, { y: -26, duration: 0.2, ease: "power1.in" }, burstEnd);
|
||||
// 时间轴总长归一
|
||||
tl.to({}, { duration: 0.001 }, 1);
|
||||
});
|
||||
}
|
||||
|
||||
/* 长内容轮:提问/思考用入场 scrub,内容块各自进入视口时「生成」 */
|
||||
function buildFlowRound({ round, section, qText, qCaret, thinking, answer, items, mind, setActive }) {
|
||||
gsap.set(answer, { opacity: 1 });
|
||||
|
||||
const qTl = gsap.timeline({
|
||||
defaults: { ease: "none" },
|
||||
scrollTrigger: {
|
||||
trigger: section,
|
||||
start: "top 78%",
|
||||
end: "top 18%",
|
||||
scrub: 0.5,
|
||||
},
|
||||
});
|
||||
addTypewriter(qTl, qText, qCaret, round.question, 0, 0.55);
|
||||
qTl.to(thinking, { opacity: 1, duration: 0.08 }, 0.6)
|
||||
.to(thinking, { opacity: 0, duration: 0.1 }, 0.9);
|
||||
|
||||
items.forEach((item, i) => {
|
||||
gsap.to(item, {
|
||||
y: 0,
|
||||
autoAlpha: 1,
|
||||
duration: 0.8,
|
||||
delay: (i % 3) * 0.09, // 不等间隔,模拟 token 到达
|
||||
ease: "expo.out",
|
||||
scrollTrigger: { trigger: item, start: "top 88%" },
|
||||
});
|
||||
});
|
||||
|
||||
ScrollTrigger.create({
|
||||
trigger: section,
|
||||
start: "top 60%",
|
||||
end: "bottom 55%",
|
||||
onToggle(self) {
|
||||
if (self.isActive) {
|
||||
setActive(round.id);
|
||||
mind.setSide(round.side);
|
||||
mind.setPalette(round.palette);
|
||||
} else {
|
||||
mind.setState("idle");
|
||||
}
|
||||
},
|
||||
onUpdate(self) {
|
||||
mind.setState(self.progress < 0.1 ? "thinking" : "answering");
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/* 进度轨:高亮由编排回调驱动,这里只负责节点与跳转 */
|
||||
export function initRail(lenis) {
|
||||
const setActive = (id) => {
|
||||
document.querySelectorAll(".rail-node").forEach((n) => {
|
||||
n.classList.toggle("active", n.dataset.round === id);
|
||||
});
|
||||
};
|
||||
|
||||
ScrollTrigger.create({
|
||||
trigger: "#r-boot",
|
||||
start: "top top",
|
||||
end: "bottom 40%",
|
||||
onToggle(self) {
|
||||
if (self.isActive) setActive("boot");
|
||||
},
|
||||
});
|
||||
|
||||
const jump = (id) => {
|
||||
const target = document.getElementById(`r-${id}`);
|
||||
if (!target) return;
|
||||
if (lenis) lenis.scrollTo(target, { offset: 2, duration: 1.2 });
|
||||
else target.scrollIntoView({ behavior: "smooth" });
|
||||
};
|
||||
|
||||
return { jump, setActive };
|
||||
}
|
||||
|
||||
/* 降级:reduced motion 直接展示全部内容 */
|
||||
export function applyReducedMotion() {
|
||||
rounds
|
||||
.filter((r) => r.question)
|
||||
.forEach((round) => {
|
||||
const section = document.getElementById(`r-${round.id}`);
|
||||
section.querySelector(".q-text").textContent = round.question;
|
||||
const answer = section.querySelector(".answer");
|
||||
answer.style.opacity = "1";
|
||||
});
|
||||
const boot = document.querySelector("#r-boot");
|
||||
boot.querySelector(".typed-line").textContent = "这是我的数字思维体。";
|
||||
document.querySelectorAll(".gen").forEach((n) => (n.style.opacity = "1"));
|
||||
}
|
||||
@@ -1,481 +0,0 @@
|
||||
export const resume = {
|
||||
name: "王元有",
|
||||
alias: "湛兮",
|
||||
title: "前端开发工程师",
|
||||
intent: "求职意向:前端开发工程师",
|
||||
profile:
|
||||
"7 年前端与跨端开发经验,长期负责 Web、App、H5、小程序与管理后台的架构设计、工程化建设和核心业务交付。熟悉 AI 产品工程、SSE 流式通信、Next.js/Vue/React Native 跨端体系、微前端和 Monorepo 协作模式。",
|
||||
basics: [
|
||||
{ label: "工作经验", value: "7 年" },
|
||||
{ label: "技术管理", value: "2+ 年" },
|
||||
{ label: "项目用户规模", value: "千万级 MAU" },
|
||||
{ label: "教育背景", value: "电子科技大学 本科" },
|
||||
],
|
||||
contact: {
|
||||
phone: "19980439383",
|
||||
email: "419021733@qq.com",
|
||||
emailAlt: "wmagmgema521@gmail.com",
|
||||
meta: "男 | 29 岁",
|
||||
},
|
||||
highlights: [
|
||||
"主导多款企业级产品从 0 到 1 搭建,覆盖 AI 模型服务、AI 资讯对话、供应链、门店数字化、工业互联网平台等场景。",
|
||||
"掌握 Vue3、React、Next.js、React Native、Expo、UniApp 等技术栈,能独立完成 Web、移动端、App 与管理后台方案落地。",
|
||||
"推动 Micro-Frontend、SSR、pnpm Monorepo、Git Flow、代码审查、CI/CD、监控体系等工程化实践。",
|
||||
"有明确的性能与效率结果:构建体积优化 30%、首屏白屏时间缩短 25%、组件库工程化提升开发效率 50%、重复开发成本减少约 40%。",
|
||||
],
|
||||
metrics: [
|
||||
{ value: "-30%", label: "构建体积优化" },
|
||||
{ value: "-25%", label: "首屏白屏时间" },
|
||||
{ value: "+50%", label: "组件库效率提升" },
|
||||
{ value: "+40%", label: "团队人效提升" },
|
||||
{ value: "40%", label: "重复开发成本降低" },
|
||||
{ value: "30%", label: "消息渲染性能提升" },
|
||||
],
|
||||
skills: [
|
||||
{
|
||||
group: "前端框架",
|
||||
items: [
|
||||
"Next.js 16",
|
||||
"React 19",
|
||||
"Vue 3",
|
||||
"TypeScript 5",
|
||||
"Tailwind CSS 4",
|
||||
"Nuxt/SSR",
|
||||
],
|
||||
},
|
||||
{
|
||||
group: "跨端开发",
|
||||
items: [
|
||||
"React Native 0.79",
|
||||
"Expo 53",
|
||||
"UniApp",
|
||||
"H5",
|
||||
"微信/飞书小程序",
|
||||
],
|
||||
},
|
||||
{
|
||||
group: "AI 与通信",
|
||||
items: [
|
||||
"Vercel AI SDK v6",
|
||||
"SSE 流式通信",
|
||||
"多 Agent 协作",
|
||||
"Markdown/LaTeX 渲染",
|
||||
"AI 对话",
|
||||
"AI 生图/生视频",
|
||||
],
|
||||
},
|
||||
{
|
||||
group: "工程化",
|
||||
items: [
|
||||
"pnpm Monorepo",
|
||||
"Git Submodules",
|
||||
"Qiankun",
|
||||
"Lerna",
|
||||
"Git Flow",
|
||||
"Playwright",
|
||||
],
|
||||
},
|
||||
{
|
||||
group: "状态与 UI",
|
||||
items: [
|
||||
"Zustand",
|
||||
"MobX",
|
||||
"Pinia",
|
||||
"Radix UI",
|
||||
"Ant Design",
|
||||
"Element Plus",
|
||||
"ProComponents",
|
||||
],
|
||||
},
|
||||
{
|
||||
group: "基础设施",
|
||||
items: [
|
||||
"Alova",
|
||||
"next-intl",
|
||||
"i18next",
|
||||
"ARMS 监控",
|
||||
"CDN 优化",
|
||||
"Adyen 支付",
|
||||
],
|
||||
},
|
||||
],
|
||||
experiences: [
|
||||
{
|
||||
company: "成都海艺互娱科技有限公司",
|
||||
role: "React Native 开发工程师",
|
||||
period: "2025.05 - 至今",
|
||||
points: [
|
||||
"参与多款 AI 产品从 0 到 1 搭建与架构设计,覆盖前端性能、交互体验、国际化体系与多端组件复用。",
|
||||
"在 AI 对话、生图、生视频与多 Agent 协作领域沉淀工程经验,负责流式通信与前端渲染体验优化。",
|
||||
"支撑千万级海外用户产品,推动多端组件库统一建设,减少重复开发成本约 40%。",
|
||||
],
|
||||
},
|
||||
{
|
||||
company: "四川茶姬企业管理有限公司",
|
||||
role: "高级 Web 前端开发",
|
||||
period: "2024.03 - 2025.03",
|
||||
points: [
|
||||
"主导供应链管理系统前端开发,实现采购、仓储、配送等核心模块。",
|
||||
"独立负责门店报损与食安管理核心功能搭建及迭代,覆盖国内、东南亚与北美业务逻辑。",
|
||||
"引入阿里云 ARMS 前端监控体系,落地 CDN 静态资源优化方案,并制定代码审查与 Git 操作规范。",
|
||||
],
|
||||
},
|
||||
{
|
||||
company: "中钧科技有限公司四川分公司",
|
||||
role: "前端开发组长",
|
||||
period: "2022.04 - 2024.03",
|
||||
points: [
|
||||
"负责经营帮 PC 端微前端、门户和商管核心模块开发,完成 UI 还原、接口联调及性能优化。",
|
||||
"从零搭建新 E 畅行小程序基础框架,完成技术方案制定与全流程开发。",
|
||||
"主导经营帮小程序、H5、Admin 后台迭代开发与跨端技术方案设计,建立 Git Flow、代码审查和新人培训机制。",
|
||||
],
|
||||
},
|
||||
],
|
||||
projects: [
|
||||
{
|
||||
id: "vtrix",
|
||||
name: "SeaCloud / Vtrix",
|
||||
subtitle: "AI 模型服务平台(Web 端)",
|
||||
period: "2025.09 - 至今",
|
||||
tech: [
|
||||
"Next.js 16",
|
||||
"React 19",
|
||||
"TypeScript",
|
||||
"Tailwind CSS",
|
||||
"Zustand",
|
||||
"Alova",
|
||||
"Vercel AI SDK",
|
||||
"Radix UI",
|
||||
"next-intl",
|
||||
],
|
||||
summary:
|
||||
"面向全球用户的 AI 模型聚合服务平台,聚合 LLM、图像、视频、音频、3D 等多模态模型能力,覆盖 C 端调用与 B 端组织/分销管理。",
|
||||
modules: [
|
||||
"分销商客户邀请、折扣模板、额度分配、销售配置与利润率计算。",
|
||||
"多币种与汇率体系,封装 useCurrency Hook 并贯穿 Pricing、Billing、API Keys 等模块。",
|
||||
"组织成员、角色权限守卫、配额设置、支出限额、账单报表、交易筛选与 Excel 导出。",
|
||||
"i18n Submodule 架构迁移、企业微信同步工作流、通用表格/筛选器/分页组件增强。",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "seabuzz",
|
||||
name: "SeaBuzz",
|
||||
subtitle: "AI 智能资讯与对话平台",
|
||||
period: "2025.05 - 至今",
|
||||
tech: [
|
||||
"React Native",
|
||||
"Expo 53",
|
||||
"Expo Router",
|
||||
"Zustand",
|
||||
"NativeWind",
|
||||
"SSE",
|
||||
"i18next",
|
||||
"Adyen",
|
||||
"Lerna",
|
||||
],
|
||||
summary:
|
||||
"海艺 AI 旗下 AI 新闻聚合、智能搜索与多模态对话平台,基于 Expo 实现 iOS、Android、Web 三端统一开发。",
|
||||
modules: [
|
||||
"AI Agent 对话完整链路:SSE 流式聊天、打字机渲染、Markdown/LaTeX、思考动画、来源引用与历史同步。",
|
||||
"Discover 发现页与新闻详情,支持瀑布流、大/小卡动态布局、骨架屏与 Smart Image。",
|
||||
"Google、Facebook、Discord、Email 登录与 SeaArt Auth SDK 对接。",
|
||||
"Monorepo 公共包体系、API 层、数据模型、UI 组件、状态管理、OTA 热更新与代码签名机制。",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "bawang",
|
||||
name: "霸王功夫",
|
||||
subtitle: "门店数字化与供应链管理平台",
|
||||
period: "2024.03 - 2025.03",
|
||||
tech: [
|
||||
"React 18",
|
||||
"Vue 3",
|
||||
"UniApp",
|
||||
"MobX",
|
||||
"Pinia",
|
||||
"Element Plus",
|
||||
"Ant Design",
|
||||
"ProComponents",
|
||||
],
|
||||
summary:
|
||||
"服务全球 6000+ 门店及运营伙伴的数字化管理平台,覆盖门店运营、食品安全、供应链协同等核心业务。",
|
||||
modules: [
|
||||
"小程序报损模块:摄像头扫码、在线报损登记,兼容微信小程序与飞书 H5。",
|
||||
"食安管理模块:低频蓝牙连接 TSPL 指令集打印机,支持国内、东南亚、北美三套业务逻辑。",
|
||||
"经营信息模块:多门店经营状态移动端看板,按区域、时间、指标筛选。",
|
||||
"供应链模块:采购系统、供应商合同管理、供应商结算系统,对接云厉、费控等外部系统。",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "jingyingbang",
|
||||
name: "经营帮平台 + 经营帮拉新",
|
||||
subtitle: "工业互联网与微前端平台",
|
||||
period: "2022.04 - 2024.03",
|
||||
tech: [
|
||||
"Vue",
|
||||
"Qiankun",
|
||||
"Element UI",
|
||||
"华为云 OBS",
|
||||
"百度地图",
|
||||
"高德地图",
|
||||
"IM",
|
||||
"UniApp",
|
||||
],
|
||||
summary:
|
||||
"基于信息化设计理念和区块链技术的工业互联网平台,为企业和个人提供数字化运营服务。",
|
||||
modules: [
|
||||
"参与单体前端到 Qiankun 微前端拆分,拆分出 12 个基础项目。",
|
||||
"负责商管、门户、经营帮系列小程序/H5/Admin 后台核心业务交付。",
|
||||
"引入华为云 OBS 直传减轻请求链路性能浪费。",
|
||||
"开发内部 Chrome 插件 zjkj-decryption,提升数据解析效率。",
|
||||
],
|
||||
},
|
||||
],
|
||||
education: {
|
||||
school: "电子科技大学",
|
||||
degree: "本科",
|
||||
major: "信息管理与信息系统",
|
||||
period: "2023 - 2025",
|
||||
},
|
||||
};
|
||||
|
||||
export const resumeEn = {
|
||||
name: "Wang Yuanyou",
|
||||
alias: "mrZhan",
|
||||
title: "Frontend Engineer",
|
||||
intent: "Target Role: Frontend Engineer",
|
||||
profile:
|
||||
"Frontend and cross-platform engineer with 7 years of experience building Web, App, H5, mini-program and admin systems. Strong in AI product engineering, SSE streaming, Next.js/Vue/React Native ecosystems, micro-frontends and Monorepo collaboration.",
|
||||
basics: [
|
||||
{ label: "Experience", value: "7 years" },
|
||||
{ label: "Tech Leadership", value: "2+ years" },
|
||||
{ label: "Product Scale", value: "10M+ MAU" },
|
||||
{ label: "Education", value: "UESTC Bachelor" },
|
||||
],
|
||||
contact: {
|
||||
phone: "19980439383",
|
||||
email: "419021733@qq.com",
|
||||
emailAlt: "wmagmgema521@gmail.com",
|
||||
meta: "Male | 29",
|
||||
},
|
||||
highlights: [
|
||||
"Led multiple enterprise products from 0 to 1 across AI model services, AI news and chat, supply chain, store digitization and industrial internet platforms.",
|
||||
"Hands-on with Vue3, React, Next.js, React Native, Expo and UniApp, capable of delivering Web, mobile, App and admin products end to end.",
|
||||
"Drove engineering practices including Micro-Frontend, SSR, pnpm Monorepo, Git Flow, code review, CI/CD and frontend observability.",
|
||||
"Delivered measurable outcomes: 30% smaller bundles, 25% faster first screen, 50% faster component-driven delivery and around 40% less duplicated work.",
|
||||
],
|
||||
metrics: [
|
||||
{ value: "-30%", label: "Bundle Size" },
|
||||
{ value: "-25%", label: "First Screen Blank Time" },
|
||||
{ value: "+50%", label: "Component Delivery Efficiency" },
|
||||
{ value: "+40%", label: "Team Productivity" },
|
||||
{ value: "40%", label: "Duplicated Work Reduced" },
|
||||
{ value: "30%", label: "Message Rendering Performance" },
|
||||
],
|
||||
skills: [
|
||||
{
|
||||
group: "Frontend Frameworks",
|
||||
items: [
|
||||
"Next.js 16",
|
||||
"React 19",
|
||||
"Vue 3",
|
||||
"TypeScript 5",
|
||||
"Tailwind CSS 4",
|
||||
"Nuxt/SSR",
|
||||
],
|
||||
},
|
||||
{
|
||||
group: "Cross-platform",
|
||||
items: [
|
||||
"React Native 0.79",
|
||||
"Expo 53",
|
||||
"UniApp",
|
||||
"H5",
|
||||
"WeChat/Lark Mini Programs",
|
||||
],
|
||||
},
|
||||
{
|
||||
group: "AI & Streaming",
|
||||
items: [
|
||||
"Vercel AI SDK v6",
|
||||
"SSE Streaming",
|
||||
"Multi-Agent Collaboration",
|
||||
"Markdown/LaTeX",
|
||||
"AI Chat",
|
||||
"AI Image/Video",
|
||||
],
|
||||
},
|
||||
{
|
||||
group: "Engineering",
|
||||
items: [
|
||||
"pnpm Monorepo",
|
||||
"Git Submodules",
|
||||
"Qiankun",
|
||||
"Lerna",
|
||||
"Git Flow",
|
||||
"Playwright",
|
||||
],
|
||||
},
|
||||
{
|
||||
group: "State & UI",
|
||||
items: [
|
||||
"Zustand",
|
||||
"MobX",
|
||||
"Pinia",
|
||||
"Radix UI",
|
||||
"Ant Design",
|
||||
"Element Plus",
|
||||
"ProComponents",
|
||||
],
|
||||
},
|
||||
{
|
||||
group: "Infrastructure",
|
||||
items: [
|
||||
"Alova",
|
||||
"next-intl",
|
||||
"i18next",
|
||||
"ARMS Monitoring",
|
||||
"CDN Optimization",
|
||||
"Adyen Payments",
|
||||
],
|
||||
},
|
||||
],
|
||||
experiences: [
|
||||
{
|
||||
company: "Chengdu Haiyi Interactive Entertainment Technology Co., Ltd.",
|
||||
role: "React Native Engineer",
|
||||
period: "2025.05 - Present",
|
||||
points: [
|
||||
"Contributed to architecture and 0-to-1 delivery of multiple AI products, covering performance, UX, i18n and multi-platform component reuse.",
|
||||
"Built engineering experience in AI chat, image/video generation and multi-agent collaboration, with focus on streaming and frontend rendering performance.",
|
||||
"Supported products serving 10M+ overseas users and promoted unified multi-platform component libraries, reducing duplicate work by around 40%.",
|
||||
],
|
||||
},
|
||||
{
|
||||
company: "Sichuan Chaji Enterprise Management Co., Ltd.",
|
||||
role: "Senior Web Frontend Developer",
|
||||
period: "2024.03 - 2025.03",
|
||||
points: [
|
||||
"Led frontend development of supply-chain management systems, including procurement, warehousing and delivery modules.",
|
||||
"Owned store loss-reporting and food-safety features across China, Southeast Asia and North America business rules.",
|
||||
"Introduced Alibaba Cloud ARMS frontend monitoring, delivered CDN optimization and established code review and Git workflow standards.",
|
||||
],
|
||||
},
|
||||
{
|
||||
company: "Zhongjun Technology Sichuan Branch",
|
||||
role: "Frontend Team Lead",
|
||||
period: "2022.04 - 2024.03",
|
||||
points: [
|
||||
"Delivered core modules for the Jingyingbang PC micro-frontend platform, portal and business management systems, including UI implementation, API integration and performance optimization.",
|
||||
"Built the New E Travel mini-program foundation from scratch and owned technical planning through delivery.",
|
||||
"Led mini-program, H5 and admin iterations, designed cross-platform solutions, and established Git Flow, code review and onboarding practices.",
|
||||
],
|
||||
},
|
||||
],
|
||||
projects: [
|
||||
{
|
||||
id: "vtrix",
|
||||
name: "SeaCloud / Vtrix",
|
||||
subtitle: "AI Model Service Platform (Web)",
|
||||
period: "2025.09 - Present",
|
||||
tech: [
|
||||
"Next.js 16",
|
||||
"React 19",
|
||||
"TypeScript",
|
||||
"Tailwind CSS",
|
||||
"Zustand",
|
||||
"Alova",
|
||||
"Vercel AI SDK",
|
||||
"Radix UI",
|
||||
"next-intl",
|
||||
],
|
||||
summary:
|
||||
"A global AI model aggregation platform covering LLM, image, video, audio and 3D model capabilities for developers, organizations and distributors.",
|
||||
modules: [
|
||||
"Built distributor invitation, discount templates, credit allocation, sales configuration and profit margin workflows.",
|
||||
"Implemented global currency switching and exchange-rate conversion via a reusable useCurrency hook across Pricing, Billing and API Keys.",
|
||||
"Delivered organization roles, quota controls, spending limits, billing reports, transaction filtering and Excel export flows.",
|
||||
"Supported i18n submodule migration, WeCom translation sync and shared table/filter/pagination component enhancements.",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "seabuzz",
|
||||
name: "SeaBuzz",
|
||||
subtitle: "AI News and Conversation Platform",
|
||||
period: "2025.05 - Present",
|
||||
tech: [
|
||||
"React Native",
|
||||
"Expo 53",
|
||||
"Expo Router",
|
||||
"Zustand",
|
||||
"NativeWind",
|
||||
"SSE",
|
||||
"i18next",
|
||||
"Adyen",
|
||||
"Lerna",
|
||||
],
|
||||
summary:
|
||||
"An AI-powered news aggregation, smart search and multimodal conversation platform under SeaArt AI, built with Expo for iOS, Android and Web.",
|
||||
modules: [
|
||||
"Built the AI Agent chat flow with SSE streaming, typewriter rendering, Markdown/LaTeX, thinking animation, citations and history sync.",
|
||||
"Delivered Discover feed and news detail pages with masonry layout, large/small dynamic cards, skeleton loading and Smart Image.",
|
||||
"Integrated Google, Facebook, Discord and Email login with SeaArt Auth SDK.",
|
||||
"Built Monorepo shared packages for API, data models, UI components, state, OTA updates and code signing.",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "bawang",
|
||||
name: "Bawang Kungfu",
|
||||
subtitle: "Store Digitization and Supply Chain Platform",
|
||||
period: "2024.03 - 2025.03",
|
||||
tech: [
|
||||
"React 18",
|
||||
"Vue 3",
|
||||
"UniApp",
|
||||
"MobX",
|
||||
"Pinia",
|
||||
"Element Plus",
|
||||
"Ant Design",
|
||||
"ProComponents",
|
||||
],
|
||||
summary:
|
||||
"A digital operations platform serving 6,000+ stores and partners, covering store operations, food safety and supply-chain collaboration.",
|
||||
modules: [
|
||||
"Built mini-program loss reporting with camera scanning and online registration, compatible with WeChat mini-program and Lark H5.",
|
||||
"Implemented food safety flows with Bluetooth TSPL printers and separate China, Southeast Asia and North America business rules.",
|
||||
"Delivered mobile dashboards for multi-store operation metrics with region, time and KPI filtering.",
|
||||
"Built procurement, supplier contract and settlement modules, integrating multiple external systems.",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "jingyingbang",
|
||||
name: "Jingyingbang Platform",
|
||||
subtitle: "Industrial Internet and Micro-frontend Platform",
|
||||
period: "2022.04 - 2024.03",
|
||||
tech: [
|
||||
"Vue",
|
||||
"Qiankun",
|
||||
"Element UI",
|
||||
"Huawei Cloud OBS",
|
||||
"Baidu Map",
|
||||
"Amap",
|
||||
"IM",
|
||||
"UniApp",
|
||||
],
|
||||
summary:
|
||||
"An industrial internet platform based on informatization and blockchain concepts, providing digital operation services for companies and individuals.",
|
||||
modules: [
|
||||
"Participated in splitting a large frontend monolith into 12 Qiankun-based micro-frontend projects.",
|
||||
"Delivered core business features for portals, business management, mini-program, H5 and admin systems.",
|
||||
"Introduced Huawei Cloud OBS direct upload to reduce request-chain overhead.",
|
||||
"Built an internal Chrome extension, zjkj-decryption, to improve data parsing efficiency.",
|
||||
],
|
||||
},
|
||||
],
|
||||
education: {
|
||||
school: "University of Electronic Science and Technology of China",
|
||||
degree: "Bachelor",
|
||||
major: "Information Management and Information Systems",
|
||||
period: "2023 - 2025",
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,90 @@
|
||||
// 7 轮问答的文案与节奏配置 —— 每轮节奏参数刻意不同,避免线性重复
|
||||
export const contact = {
|
||||
emails: ["419021733@qq.com", "wmagmgema521@gmail.com"],
|
||||
phone: "19980439383",
|
||||
github: "https://github.com/zhanBoss",
|
||||
identity: "男 · 29 岁 · 电子科技大学 信息管理与信息系统 本科",
|
||||
};
|
||||
|
||||
export const rounds = [
|
||||
{
|
||||
id: "boot",
|
||||
label: "BOOT",
|
||||
type: "boot",
|
||||
question: null,
|
||||
side: 1,
|
||||
palette: ["#22d3ee", "#a78bfa"],
|
||||
},
|
||||
{
|
||||
id: "intro",
|
||||
label: "WHO",
|
||||
type: "signals",
|
||||
question: "先介绍一下你自己?",
|
||||
side: -1,
|
||||
palette: ["#2dd4bf", "#a78bfa"],
|
||||
pinLength: "+=160%",
|
||||
qEnd: 0.14,
|
||||
thinkEnd: 0.3,
|
||||
staggerEach: 0.055,
|
||||
},
|
||||
{
|
||||
id: "focus",
|
||||
label: "NOW",
|
||||
type: "focus",
|
||||
question: "最近一年具体在做什么?",
|
||||
side: 1,
|
||||
palette: ["#22d3ee", "#c084fc"],
|
||||
pinLength: "+=200%",
|
||||
qEnd: 0.12,
|
||||
thinkEnd: 0.34,
|
||||
staggerEach: 0.085,
|
||||
},
|
||||
{
|
||||
id: "projects",
|
||||
label: "PROOF",
|
||||
type: "projects",
|
||||
question: "有实际项目证明吗?",
|
||||
side: -1,
|
||||
palette: ["#38bdf8", "#a78bfa"],
|
||||
flow: true, // 内容超过一屏:不 pin,逐块流式生成
|
||||
qEnd: 0.07,
|
||||
thinkEnd: 0.16,
|
||||
staggerEach: 0.045,
|
||||
},
|
||||
{
|
||||
id: "skills",
|
||||
label: "STACK",
|
||||
type: "skills",
|
||||
question: "技能栈展开讲讲?",
|
||||
side: 1,
|
||||
palette: ["#67e8f9", "#8b5cf6"],
|
||||
pinLength: "+=240%",
|
||||
qEnd: 0.1,
|
||||
thinkEnd: 0.26,
|
||||
staggerEach: 0.02,
|
||||
},
|
||||
{
|
||||
id: "experience",
|
||||
label: "PATH",
|
||||
type: "experience",
|
||||
question: "之前的团队经历?",
|
||||
side: -1,
|
||||
palette: ["#5eead4", "#a78bfa"],
|
||||
pinLength: "+=220%",
|
||||
qEnd: 0.13,
|
||||
thinkEnd: 0.32,
|
||||
staggerEach: 0.1,
|
||||
},
|
||||
{
|
||||
id: "contact",
|
||||
label: "PING",
|
||||
type: "contact",
|
||||
question: "怎么联系你?",
|
||||
side: 0,
|
||||
palette: ["#22d3ee", "#f0abfc"],
|
||||
pinLength: "+=150%",
|
||||
qEnd: 0.16,
|
||||
thinkEnd: 0.34,
|
||||
staggerEach: 0.09,
|
||||
},
|
||||
];
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
import gsap from "gsap";
|
||||
import { renderDialogue, renderRail } from "./render";
|
||||
import { createMind } from "./mind/mind";
|
||||
import {
|
||||
applyReducedMotion,
|
||||
initChoreography,
|
||||
initRail,
|
||||
initSmoothScroll,
|
||||
playBoot,
|
||||
} from "./choreography";
|
||||
|
||||
const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
||||
|
||||
/* 1. 由简历数据生成对话 DOM */
|
||||
renderDialogue(document.getElementById("dialogue"));
|
||||
|
||||
/* 2. 思维流体 */
|
||||
const mind = createMind(document.getElementById("mind-canvas"), { reduced });
|
||||
window.__mind = mind;
|
||||
gsap.ticker.add(() => mind.tick());
|
||||
|
||||
/* 3. 进度轨 + 滚动编排 */
|
||||
let lenis = null;
|
||||
if (!reduced) lenis = initSmoothScroll();
|
||||
|
||||
const rail = initRail(lenis);
|
||||
renderRail(document.getElementById("session-rail"), rail.jump);
|
||||
|
||||
if (!reduced) {
|
||||
initChoreography(mind, rail.setActive);
|
||||
playBoot(mind);
|
||||
} else {
|
||||
applyReducedMotion();
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
import * as THREE from "three";
|
||||
import { blobFragment, blobVertex, particleFragment, particleVertex } from "./shaders";
|
||||
|
||||
// 三状态目标值 —— 每帧向目标插值,无硬切
|
||||
const STATES = {
|
||||
idle: { freq: 1.15, amp: 0.24, speed: 0.22, heat: 0.0, scale: 1.0, attract: 0.12 },
|
||||
thinking: { freq: 2.6, amp: 0.27, speed: 0.95, heat: 0.7, scale: 0.86, attract: 1.0 },
|
||||
answering: { freq: 1.9, amp: 0.46, speed: 0.5, heat: 0.22, scale: 1.09, attract: 0.0 },
|
||||
};
|
||||
|
||||
const lerp = (a, b, t) => a + (b - a) * t;
|
||||
|
||||
export function createMind(canvas, { reduced = false } = {}) {
|
||||
const isMobile = window.innerWidth < 880;
|
||||
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true });
|
||||
renderer.setPixelRatio(Math.min(window.devicePixelRatio, isMobile ? 1.5 : 2));
|
||||
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||
|
||||
const scene = new THREE.Scene();
|
||||
const camera = new THREE.PerspectiveCamera(42, window.innerWidth / window.innerHeight, 0.1, 30);
|
||||
camera.position.z = 6;
|
||||
|
||||
const group = new THREE.Group();
|
||||
scene.add(group);
|
||||
|
||||
/* ---- 流体球 ---- */
|
||||
const blobUniforms = {
|
||||
uTime: { value: 0 },
|
||||
uFreq: { value: STATES.idle.freq },
|
||||
uAmp: { value: STATES.idle.amp },
|
||||
uSpeed: { value: STATES.idle.speed },
|
||||
uHeat: { value: 0 },
|
||||
uOpacity: { value: 1 },
|
||||
uColorA: { value: new THREE.Color("#22d3ee") },
|
||||
uColorB: { value: new THREE.Color("#a78bfa") },
|
||||
};
|
||||
const blob = new THREE.Mesh(
|
||||
new THREE.IcosahedronGeometry(1.35, isMobile ? 48 : 96),
|
||||
new THREE.ShaderMaterial({
|
||||
vertexShader: blobVertex,
|
||||
fragmentShader: blobFragment,
|
||||
uniforms: blobUniforms,
|
||||
transparent: true,
|
||||
blending: THREE.AdditiveBlending,
|
||||
depthWrite: false,
|
||||
})
|
||||
);
|
||||
group.add(blob);
|
||||
|
||||
/* ---- 伴生粒子 ---- */
|
||||
const COUNT = isMobile ? 900 : 2000;
|
||||
const seeds = new Float32Array(COUNT * 3);
|
||||
const radii = new Float32Array(COUNT);
|
||||
const speeds = new Float32Array(COUNT);
|
||||
for (let i = 0; i < COUNT; i++) {
|
||||
seeds[i * 3] = Math.random();
|
||||
seeds[i * 3 + 1] = Math.random();
|
||||
seeds[i * 3 + 2] = Math.random();
|
||||
radii[i] = 1.7 + Math.random() * 1.5;
|
||||
speeds[i] = 0.25 + Math.random() * 0.6;
|
||||
}
|
||||
const pGeo = new THREE.BufferGeometry();
|
||||
pGeo.setAttribute("position", new THREE.BufferAttribute(new Float32Array(COUNT * 3), 3));
|
||||
pGeo.setAttribute("aSeed", new THREE.BufferAttribute(seeds, 3));
|
||||
pGeo.setAttribute("aRadius", new THREE.BufferAttribute(radii, 1));
|
||||
pGeo.setAttribute("aSpeed", new THREE.BufferAttribute(speeds, 1));
|
||||
|
||||
const particleUniforms = {
|
||||
uTime: { value: 0 },
|
||||
uAttract: { value: STATES.idle.attract },
|
||||
uBurst: { value: 0 },
|
||||
uDir: { value: -1 },
|
||||
uSize: { value: isMobile ? 9 : 12 },
|
||||
uMouse: { value: new THREE.Vector3(999, 999, 999) },
|
||||
uMouseForce: { value: 0 },
|
||||
uGrab: { value: 0 },
|
||||
uHeat: { value: 0 },
|
||||
uOpacity: { value: 1 },
|
||||
uColorA: blobUniforms.uColorA,
|
||||
uColorB: blobUniforms.uColorB,
|
||||
};
|
||||
const particles = new THREE.Points(
|
||||
pGeo,
|
||||
new THREE.ShaderMaterial({
|
||||
vertexShader: particleVertex,
|
||||
fragmentShader: particleFragment,
|
||||
uniforms: particleUniforms,
|
||||
transparent: true,
|
||||
blending: THREE.AdditiveBlending,
|
||||
depthWrite: false,
|
||||
})
|
||||
);
|
||||
group.add(particles);
|
||||
|
||||
/* ---- 状态机 ---- */
|
||||
let current = { ...STATES.idle };
|
||||
let target = STATES.idle;
|
||||
let stateName = "idle";
|
||||
let burst = 0;
|
||||
|
||||
const colorA = blobUniforms.uColorA.value;
|
||||
const colorB = blobUniforms.uColorB.value;
|
||||
const targetColorA = colorA.clone();
|
||||
const targetColorB = colorB.clone();
|
||||
|
||||
// 停靠位置:side -1 内容在右 → 流体去左;1 → 右;0 → 居中偏后
|
||||
const dock = new THREE.Vector3();
|
||||
const dockTarget = new THREE.Vector3();
|
||||
const pointer = { x: 0, y: 0 };
|
||||
|
||||
function dockFor(side) {
|
||||
if (window.innerWidth < 880) {
|
||||
// 移动端:居中上移、退后、缩小,让内容可读
|
||||
return new THREE.Vector3(side * 0.3, 1.25, -1.6);
|
||||
}
|
||||
if (side === 0) return new THREE.Vector3(0, 0.95, -2.8);
|
||||
return new THREE.Vector3(side * 2.05, 0.1, -0.7);
|
||||
}
|
||||
|
||||
let side = 1;
|
||||
let opacityTarget = 1;
|
||||
let hasPointer = false;
|
||||
let mouseSnapped = false;
|
||||
// 按压牵引弹簧(欠阻尼 → 过冲回弹的阻尼感)
|
||||
let grab = 0;
|
||||
let grabVel = 0;
|
||||
let grabTarget = 0;
|
||||
const mouseRay = new THREE.Vector3();
|
||||
const mouseLocal = new THREE.Vector3();
|
||||
|
||||
const api = {
|
||||
setState(name) {
|
||||
if (name === stateName || reduced) return;
|
||||
stateName = name;
|
||||
target = STATES[name];
|
||||
if (name === "answering") burst = 1; // 喷发脉冲,随帧衰减
|
||||
},
|
||||
setSide(s) {
|
||||
side = s;
|
||||
dockTarget.copy(dockFor(s));
|
||||
particleUniforms.uDir.value = s === 0 ? 0 : -s; // 粒子喷向内容区
|
||||
opacityTarget = s === 0 ? 0.42 : 1; // 居中停靠时减淡,保证内容可读
|
||||
},
|
||||
setPalette([a, b]) {
|
||||
targetColorA.set(a);
|
||||
targetColorB.set(b);
|
||||
},
|
||||
get state() {
|
||||
return stateName;
|
||||
},
|
||||
debug() {
|
||||
return { stateName, side, dock: dock.toArray(), dockTarget: dockTarget.toArray(), group: group.position.toArray() };
|
||||
},
|
||||
};
|
||||
|
||||
api.setSide(1);
|
||||
dock.copy(dockTarget);
|
||||
group.position.copy(dock);
|
||||
|
||||
window.addEventListener("pointermove", (e) => {
|
||||
pointer.x = (e.clientX / window.innerWidth) * 2 - 1;
|
||||
pointer.y = (e.clientY / window.innerHeight) * 2 - 1;
|
||||
hasPointer = true;
|
||||
});
|
||||
|
||||
window.addEventListener("pointerdown", (e) => {
|
||||
pointer.x = (e.clientX / window.innerWidth) * 2 - 1;
|
||||
pointer.y = (e.clientY / window.innerHeight) * 2 - 1;
|
||||
hasPointer = true;
|
||||
grabTarget = 1;
|
||||
});
|
||||
const release = () => { grabTarget = 0; };
|
||||
window.addEventListener("pointerup", release);
|
||||
window.addEventListener("pointercancel", release);
|
||||
window.addEventListener("blur", release);
|
||||
|
||||
window.addEventListener("resize", () => {
|
||||
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||
camera.aspect = window.innerWidth / window.innerHeight;
|
||||
camera.updateProjectionMatrix();
|
||||
api.setSide(side);
|
||||
});
|
||||
|
||||
/* ---- 帧循环(由外部 ticker 驱动) ---- */
|
||||
const clock = new THREE.Clock();
|
||||
function tick() {
|
||||
const dt = Math.min(clock.getDelta(), 0.05);
|
||||
const t = clock.elapsedTime;
|
||||
const k = 1 - Math.pow(0.0014, dt); // 帧率无关的平滑系数
|
||||
|
||||
current.freq = lerp(current.freq, target.freq, k);
|
||||
current.amp = lerp(current.amp, target.amp, k);
|
||||
current.speed = lerp(current.speed, target.speed, k);
|
||||
current.heat = lerp(current.heat, target.heat, k);
|
||||
current.scale = lerp(current.scale, target.scale, k);
|
||||
current.attract = lerp(current.attract, target.attract, k);
|
||||
burst = lerp(burst, 0, 1 - Math.pow(0.25, dt)); // 脉冲衰减
|
||||
|
||||
blobUniforms.uTime.value = reduced ? t * 0.25 : t;
|
||||
blobUniforms.uFreq.value = current.freq;
|
||||
blobUniforms.uAmp.value = current.amp;
|
||||
blobUniforms.uSpeed.value = current.speed;
|
||||
blobUniforms.uHeat.value = current.heat;
|
||||
|
||||
particleUniforms.uTime.value = reduced ? t * 0.25 : t;
|
||||
particleUniforms.uAttract.value = current.attract;
|
||||
particleUniforms.uBurst.value = burst;
|
||||
particleUniforms.uHeat.value = current.heat;
|
||||
|
||||
colorA.lerp(targetColorA, k);
|
||||
colorB.lerp(targetColorB, k);
|
||||
|
||||
const op = lerp(blobUniforms.uOpacity.value, opacityTarget, k);
|
||||
blobUniforms.uOpacity.value = op;
|
||||
particleUniforms.uOpacity.value = op;
|
||||
|
||||
const breathe = 1 + Math.sin(t * 1.4) * 0.018;
|
||||
group.scale.setScalar(current.scale * breathe);
|
||||
|
||||
dock.lerp(dockTarget, 1 - Math.pow(0.02, dt));
|
||||
group.position.set(
|
||||
dock.x + pointer.x * 0.14,
|
||||
dock.y - pointer.y * 0.1,
|
||||
dock.z
|
||||
);
|
||||
group.rotation.y += dt * 0.12;
|
||||
group.rotation.x = pointer.y * 0.08;
|
||||
|
||||
// 鼠标牵引:把指针投影到流体所在深度平面,转到 group 局部空间后平滑跟随
|
||||
if (hasPointer && !reduced) {
|
||||
mouseRay.set(pointer.x, -pointer.y, 0.5).unproject(camera).sub(camera.position).normalize();
|
||||
const planeDist = (group.position.z - camera.position.z) / mouseRay.z;
|
||||
mouseLocal.copy(camera.position).addScaledVector(mouseRay, planeDist);
|
||||
group.updateMatrixWorld();
|
||||
group.worldToLocal(mouseLocal);
|
||||
if (!mouseSnapped) {
|
||||
particleUniforms.uMouse.value.copy(mouseLocal);
|
||||
mouseSnapped = true;
|
||||
} else {
|
||||
particleUniforms.uMouse.value.lerp(mouseLocal, 1 - Math.pow(0.01, dt));
|
||||
}
|
||||
particleUniforms.uMouseForce.value = lerp(particleUniforms.uMouseForce.value, 0.85, k);
|
||||
|
||||
// 按压牵引:欠阻尼弹簧积分 —— 按下缓冲吸入,松开过冲散开再归位
|
||||
grabVel += (grabTarget - grab) * 26 * dt;
|
||||
grabVel *= Math.exp(-3.2 * dt);
|
||||
grab += grabVel * dt;
|
||||
particleUniforms.uGrab.value = grab;
|
||||
}
|
||||
|
||||
renderer.render(scene, camera);
|
||||
}
|
||||
|
||||
return { ...api, tick };
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
// 思维流体 GLSL —— Ashima simplex noise + 3 octave FBM + Fresnel
|
||||
const SIMPLEX = /* glsl */ `
|
||||
vec3 mod289(vec3 x){ return x - floor(x * (1.0/289.0)) * 289.0; }
|
||||
vec4 mod289(vec4 x){ return x - floor(x * (1.0/289.0)) * 289.0; }
|
||||
vec4 permute(vec4 x){ return mod289(((x*34.0)+1.0)*x); }
|
||||
vec4 taylorInvSqrt(vec4 r){ return 1.79284291400159 - 0.85373472095314 * r; }
|
||||
|
||||
float snoise(vec3 v){
|
||||
const vec2 C = vec2(1.0/6.0, 1.0/3.0);
|
||||
const vec4 D = vec4(0.0, 0.5, 1.0, 2.0);
|
||||
|
||||
vec3 i = floor(v + dot(v, C.yyy));
|
||||
vec3 x0 = v - i + dot(i, C.xxx);
|
||||
|
||||
vec3 g = step(x0.yzx, x0.xyz);
|
||||
vec3 l = 1.0 - g;
|
||||
vec3 i1 = min(g.xyz, l.zxy);
|
||||
vec3 i2 = max(g.xyz, l.zxy);
|
||||
|
||||
vec3 x1 = x0 - i1 + C.xxx;
|
||||
vec3 x2 = x0 - i2 + C.yyy;
|
||||
vec3 x3 = x0 - D.yyy;
|
||||
|
||||
i = mod289(i);
|
||||
vec4 p = permute(permute(permute(
|
||||
i.z + vec4(0.0, i1.z, i2.z, 1.0))
|
||||
+ i.y + vec4(0.0, i1.y, i2.y, 1.0))
|
||||
+ i.x + vec4(0.0, i1.x, i2.x, 1.0));
|
||||
|
||||
float n_ = 0.142857142857;
|
||||
vec3 ns = n_ * D.wyz - D.xzx;
|
||||
|
||||
vec4 j = p - 49.0 * floor(p * ns.z * ns.z);
|
||||
|
||||
vec4 x_ = floor(j * ns.z);
|
||||
vec4 y_ = floor(j - 7.0 * x_);
|
||||
|
||||
vec4 x = x_ * ns.x + ns.yyyy;
|
||||
vec4 y = y_ * ns.x + ns.yyyy;
|
||||
vec4 h = 1.0 - abs(x) - abs(y);
|
||||
|
||||
vec4 b0 = vec4(x.xy, y.xy);
|
||||
vec4 b1 = vec4(x.zw, y.zw);
|
||||
|
||||
vec4 s0 = floor(b0) * 2.0 + 1.0;
|
||||
vec4 s1 = floor(b1) * 2.0 + 1.0;
|
||||
vec4 sh = -step(h, vec4(0.0));
|
||||
|
||||
vec4 a0 = b0.xzyw + s0.xzyw * sh.xxyy;
|
||||
vec4 a1 = b1.xzyw + s1.xzyw * sh.zzww;
|
||||
|
||||
vec3 p0 = vec3(a0.xy, h.x);
|
||||
vec3 p1 = vec3(a0.zw, h.y);
|
||||
vec3 p2 = vec3(a1.xy, h.z);
|
||||
vec3 p3 = vec3(a1.zw, h.w);
|
||||
|
||||
vec4 norm = taylorInvSqrt(vec4(dot(p0,p0), dot(p1,p1), dot(p2,p2), dot(p3,p3)));
|
||||
p0 *= norm.x; p1 *= norm.y; p2 *= norm.z; p3 *= norm.w;
|
||||
|
||||
vec4 m = max(0.6 - vec4(dot(x0,x0), dot(x1,x1), dot(x2,x2), dot(x3,x3)), 0.0);
|
||||
m = m * m;
|
||||
return 42.0 * dot(m*m, vec4(dot(p0,x0), dot(p1,x1), dot(p2,x2), dot(p3,x3)));
|
||||
}
|
||||
|
||||
float fbm(vec3 p){
|
||||
float sum = 0.0;
|
||||
float amp = 0.5;
|
||||
for (int i = 0; i < 3; i++) {
|
||||
sum += amp * snoise(p);
|
||||
p *= 2.1;
|
||||
amp *= 0.5;
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
`;
|
||||
|
||||
export const blobVertex = /* glsl */ `
|
||||
uniform float uTime;
|
||||
uniform float uFreq;
|
||||
uniform float uAmp;
|
||||
uniform float uSpeed;
|
||||
|
||||
varying float vDisp;
|
||||
varying vec3 vNormal;
|
||||
varying vec3 vViewDir;
|
||||
|
||||
${SIMPLEX}
|
||||
|
||||
void main(){
|
||||
float t = uTime * uSpeed;
|
||||
float d = fbm(normal * uFreq + vec3(t, t * 0.7, -t * 0.4));
|
||||
vDisp = d;
|
||||
vec3 displaced = position + normal * d * uAmp;
|
||||
|
||||
vec4 mvPosition = modelViewMatrix * vec4(displaced, 1.0);
|
||||
vNormal = normalize(normalMatrix * normal);
|
||||
vViewDir = normalize(-mvPosition.xyz);
|
||||
gl_Position = projectionMatrix * mvPosition;
|
||||
}
|
||||
`;
|
||||
|
||||
export const blobFragment = /* glsl */ `
|
||||
uniform vec3 uColorA;
|
||||
uniform vec3 uColorB;
|
||||
uniform float uHeat;
|
||||
uniform float uOpacity;
|
||||
|
||||
varying float vDisp;
|
||||
varying vec3 vNormal;
|
||||
varying vec3 vViewDir;
|
||||
|
||||
void main(){
|
||||
float fresnel = pow(1.0 - max(dot(vNormal, vViewDir), 0.0), 2.4);
|
||||
vec3 base = mix(uColorA, uColorB, vDisp * 0.5 + 0.5);
|
||||
base = mix(base, vec3(1.0, 0.62, 0.38), uHeat * 0.4);
|
||||
vec3 color = base * (0.32 + fresnel * 1.6) + base * smoothstep(0.35, 0.9, vDisp) * 0.45;
|
||||
float alpha = (0.22 + fresnel * 0.85) * uOpacity;
|
||||
gl_FragColor = vec4(color, alpha);
|
||||
}
|
||||
`;
|
||||
|
||||
export const particleVertex = /* glsl */ `
|
||||
uniform float uTime;
|
||||
uniform float uAttract; // 1 = 吸入核心 (thinking)
|
||||
uniform float uBurst; // 1 = 向外喷发 (answering)
|
||||
uniform float uDir; // 喷发的水平方向(指向内容区)
|
||||
uniform float uSize;
|
||||
uniform vec3 uMouse; // 指针在粒子局部空间的位置
|
||||
uniform float uMouseForce;
|
||||
uniform float uGrab; // 按压牵引强度(弹簧驱动,可为负 → 回弹散开)
|
||||
|
||||
attribute vec3 aSeed; // 每粒子随机种子
|
||||
attribute float aRadius;
|
||||
attribute float aSpeed;
|
||||
|
||||
varying float vFade;
|
||||
|
||||
void main(){
|
||||
float t = uTime * aSpeed;
|
||||
// 进动轨道:两组相位叠加,避免规则圆轨
|
||||
float theta = aSeed.x * 6.2831 + t;
|
||||
float phi = aSeed.y * 3.1415 + sin(t * 0.6 + aSeed.z * 6.2831) * 0.7;
|
||||
|
||||
float r = aRadius * mix(1.0, 0.3, uAttract);
|
||||
r += uBurst * (0.8 + aSeed.z * 2.6);
|
||||
|
||||
vec3 pos = vec3(
|
||||
cos(theta) * sin(phi) * r,
|
||||
cos(phi) * r * 0.85,
|
||||
sin(theta) * sin(phi) * r
|
||||
);
|
||||
// 喷发时整体偏向内容区一侧
|
||||
pos.x += uBurst * uDir * (0.6 + aSeed.y * 1.8);
|
||||
|
||||
// 鼠标牵引:靠近指针的粒子被吸过去,绕指针形成小漩涡
|
||||
float md = distance(pos, uMouse);
|
||||
float pull = smoothstep(2.8, 0.2, md) * uMouseForce * (0.35 + aSeed.z * 0.65);
|
||||
vec3 swirl = vec3(
|
||||
sin(t * 2.2 + aSeed.x * 6.2831),
|
||||
cos(t * 1.7 + aSeed.y * 6.2831),
|
||||
sin(t * 1.3 + aSeed.z * 6.2831)
|
||||
) * (0.12 + aSeed.y * 0.3);
|
||||
pos = mix(pos, uMouse + swirl, pull);
|
||||
|
||||
// 按住鼠标:全体粒子被牵向按压点,逐粒错相往复脉动;
|
||||
// uGrab 为负(松开回弹)时 mix 外推 → 粒子向外散开再被拉回
|
||||
float osc = 0.55 + 0.45 * sin(uTime * (1.2 + aSeed.x * 1.6) + aSeed.y * 6.2831);
|
||||
float gpull = clamp(uGrab, -0.35, 1.25) * osc * (0.5 + aSeed.z * 0.5);
|
||||
gpull = clamp(gpull, -0.4, 0.96);
|
||||
vec3 gswirl = vec3(
|
||||
sin(t * 1.8 + aSeed.y * 6.2831),
|
||||
cos(t * 1.4 + aSeed.z * 6.2831),
|
||||
sin(t * 1.1 + aSeed.x * 6.2831)
|
||||
) * (0.18 + aSeed.x * 0.5);
|
||||
pos = mix(pos, uMouse + gswirl, gpull);
|
||||
|
||||
vFade = 1.0 - uBurst * aSeed.z * 0.9;
|
||||
vFade = min(vFade + pull * 0.5 + max(gpull, 0.0) * 0.35, 1.2);
|
||||
|
||||
vec4 mvPosition = modelViewMatrix * vec4(pos, 1.0);
|
||||
gl_PointSize = uSize * (0.6 + aSeed.z) * (1.0 + pull * 0.5 + max(gpull, 0.0) * 0.3) * (3.4 / -mvPosition.z);
|
||||
gl_Position = projectionMatrix * mvPosition;
|
||||
}
|
||||
`;
|
||||
|
||||
export const particleFragment = /* glsl */ `
|
||||
uniform vec3 uColorA;
|
||||
uniform vec3 uColorB;
|
||||
uniform float uHeat;
|
||||
uniform float uOpacity;
|
||||
|
||||
varying float vFade;
|
||||
|
||||
void main(){
|
||||
vec2 uv = gl_PointCoord - 0.5;
|
||||
float d = length(uv);
|
||||
float mask = smoothstep(0.5, 0.05, d);
|
||||
vec3 color = mix(uColorA, uColorB, vFade);
|
||||
color = mix(color, vec3(1.0, 0.62, 0.38), uHeat * 0.35);
|
||||
gl_FragColor = vec4(color, mask * vFade * 0.75 * uOpacity);
|
||||
}
|
||||
`;
|
||||
File diff suppressed because it is too large
Load Diff
+209
@@ -0,0 +1,209 @@
|
||||
import {
|
||||
experiences,
|
||||
focusAreas,
|
||||
metrics,
|
||||
projects,
|
||||
resumeSignals,
|
||||
skills,
|
||||
} from "./resume-data";
|
||||
import { contact, rounds } from "./dialogue";
|
||||
|
||||
const el = (tag, className, html) => {
|
||||
const node = document.createElement(tag);
|
||||
if (className) node.className = className;
|
||||
if (html != null) node.innerHTML = html;
|
||||
return node;
|
||||
};
|
||||
|
||||
/* ---------- 各轮回答内容 ---------- */
|
||||
|
||||
function bootContent(answer) {
|
||||
answer.append(
|
||||
el(
|
||||
"h1",
|
||||
"boot-greeting",
|
||||
`你好,我是<span class="grad">湛兮(花名)</span>。<br /><span class="typed-line"></span>`,
|
||||
),
|
||||
el(
|
||||
"p",
|
||||
"boot-identity gen",
|
||||
`7 年 Web 与跨端前端 · 最近一年在 <em>AI 产品团队</em> 做对话链路与控制台`,
|
||||
),
|
||||
);
|
||||
const stream = el("div", "metric-stream");
|
||||
metrics.forEach((m) => {
|
||||
const token = el("div", "metric-token gen");
|
||||
token.append(el("span", "v", m.value), el("span", "l", m.label));
|
||||
stream.append(token);
|
||||
});
|
||||
answer.append(stream, el("div", "boot-hint gen", "SCROLL TO ASK"));
|
||||
}
|
||||
|
||||
function signalsContent(answer) {
|
||||
answer.append(el("div", "a-title gen", "Self Introduction"));
|
||||
resumeSignals.forEach((line, i) => {
|
||||
const row = el("div", "signal-line gen");
|
||||
row.append(
|
||||
el("span", "idx", String(i + 1).padStart(2, "0")),
|
||||
el("p", null, line),
|
||||
);
|
||||
answer.append(row);
|
||||
});
|
||||
}
|
||||
|
||||
function focusContent(answer) {
|
||||
answer.append(el("div", "a-title gen", "Last 12 Months"));
|
||||
const grid = el("div", "focus-grid");
|
||||
answer.append(grid);
|
||||
focusAreas.forEach((f) => {
|
||||
const card = el("div", "focus-card gen");
|
||||
card.append(el("h3", null, f.title), el("p", null, f.summary));
|
||||
const tags = el("div", "tag-row");
|
||||
f.points.forEach((p) => tags.append(el("span", "tag", p)));
|
||||
card.append(tags);
|
||||
grid.append(card);
|
||||
});
|
||||
}
|
||||
|
||||
function projectsContent(answer) {
|
||||
answer.append(el("div", "a-title gen", "Selected Proof"));
|
||||
projects.forEach((p) => {
|
||||
const block = el("article", "project-block gen");
|
||||
const head = el("div", "project-head");
|
||||
head.append(
|
||||
Object.assign(el("img"), { src: p.logo, alt: p.name, loading: "lazy" }),
|
||||
el("span", "name", p.name),
|
||||
el("span", "period", p.period),
|
||||
);
|
||||
block.append(
|
||||
head,
|
||||
el("div", "project-sub", p.subtitle),
|
||||
el("p", "project-summary", p.summary),
|
||||
);
|
||||
const ul = el("ul", "project-modules");
|
||||
p.modules.forEach((m) => ul.append(el("li", "gen-sub", m)));
|
||||
block.append(ul);
|
||||
const tags = el("div", "tag-row");
|
||||
p.tech.forEach((t) => tags.append(el("span", "tag", t)));
|
||||
block.append(tags);
|
||||
answer.append(block);
|
||||
});
|
||||
}
|
||||
|
||||
function skillsContent(answer) {
|
||||
answer.append(el("div", "a-title gen", "Token Stream / Skills"));
|
||||
const HOT = new Set([
|
||||
"SSE 流式通信",
|
||||
"Next.js",
|
||||
"React",
|
||||
"React Native",
|
||||
"TypeScript",
|
||||
"Qiankun",
|
||||
]);
|
||||
skills.forEach((group) => {
|
||||
const wrap = el("div", "skill-group");
|
||||
wrap.append(el("div", "g-name", group.group));
|
||||
const row = el("div", "tag-row");
|
||||
group.items.forEach((item) => {
|
||||
row.append(
|
||||
el("span", `skill-token gen${HOT.has(item) ? " hot" : ""}`, item),
|
||||
);
|
||||
});
|
||||
wrap.append(row);
|
||||
answer.append(wrap);
|
||||
});
|
||||
}
|
||||
|
||||
function experienceContent(answer) {
|
||||
answer.append(el("div", "a-title gen", "Career Path"));
|
||||
experiences.forEach((exp) => {
|
||||
const item = el("div", "exp-item gen");
|
||||
const head = el("div", "exp-head");
|
||||
head.append(
|
||||
Object.assign(el("img"), {
|
||||
src: exp.logo,
|
||||
alt: exp.company,
|
||||
loading: "lazy",
|
||||
}),
|
||||
el("span", "co", exp.company),
|
||||
el("span", "period", exp.period),
|
||||
);
|
||||
item.append(head, el("div", "exp-role", exp.role));
|
||||
const ul = el("ul", "exp-points");
|
||||
exp.points.forEach((pt) => ul.append(el("li", null, pt)));
|
||||
item.append(ul);
|
||||
answer.append(item);
|
||||
});
|
||||
}
|
||||
|
||||
function contactContent(answer) {
|
||||
answer.append(
|
||||
el("h2", "contact-lead gen", "随时可以开启下一轮对话。"),
|
||||
el("p", "contact-id gen", contact.identity),
|
||||
);
|
||||
const actions = el("div", "contact-actions gen");
|
||||
const mail = el("a", "contact-btn primary", "发送邮件");
|
||||
mail.href = `mailto:${contact.emails[0]}`;
|
||||
const mail2 = el("a", "contact-btn", contact.emails[1]);
|
||||
mail2.href = `mailto:${contact.emails[1]}`;
|
||||
const gh = el("a", "contact-btn", "GitHub / zhanBoss");
|
||||
gh.href = contact.github;
|
||||
gh.target = "_blank";
|
||||
gh.rel = "noreferrer";
|
||||
const tel = el("a", "contact-btn", contact.phone);
|
||||
tel.href = `tel:${contact.phone}`;
|
||||
actions.append(mail, mail2, gh, tel);
|
||||
answer.append(actions, el("div", "session-end gen", "SESSION SAVED · 2026"));
|
||||
}
|
||||
|
||||
const CONTENT = {
|
||||
boot: bootContent,
|
||||
signals: signalsContent,
|
||||
focus: focusContent,
|
||||
projects: projectsContent,
|
||||
skills: skillsContent,
|
||||
experience: experienceContent,
|
||||
contact: contactContent,
|
||||
};
|
||||
|
||||
/* ---------- 装配 ---------- */
|
||||
|
||||
export function renderDialogue(mount) {
|
||||
rounds.forEach((round) => {
|
||||
const section = el("section", "round");
|
||||
section.id = `r-${round.id}`;
|
||||
section.dataset.side = String(round.side);
|
||||
const inner = el("div", "round-inner");
|
||||
|
||||
if (round.question) {
|
||||
const qRow = el("div", "q-row");
|
||||
const bubble = el("div", "q-bubble");
|
||||
bubble.append(el("span", "q-text"), el("span", "q-caret"));
|
||||
qRow.append(bubble);
|
||||
const thinking = el("div", "thinking");
|
||||
const dots = el("span", "dots");
|
||||
dots.append(el("span", "dot"), el("span", "dot"), el("span", "dot"));
|
||||
thinking.append(dots, el("span", null, "THINKING"));
|
||||
inner.append(qRow, thinking);
|
||||
}
|
||||
|
||||
const answer = el("div", "answer");
|
||||
CONTENT[round.type](answer);
|
||||
if (round.type !== "boot") answer.append(el("span", "a-caret", "▋"));
|
||||
inner.append(answer);
|
||||
section.append(inner);
|
||||
mount.append(section);
|
||||
});
|
||||
}
|
||||
|
||||
export function renderRail(mount, onJump) {
|
||||
rounds.forEach((round) => {
|
||||
const node = el("button", "rail-node");
|
||||
node.type = "button";
|
||||
node.dataset.round = round.id;
|
||||
node.setAttribute("aria-label", `跳转到 ${round.label}`);
|
||||
node.append(el("span", "rail-label", round.label));
|
||||
node.addEventListener("click", () => onJump(round.id));
|
||||
mount.append(node);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
export const metrics = [
|
||||
{ value: "7 年", label: "Web 与跨端前端经验" },
|
||||
{ value: "2 条", label: "近一年参与的产品线" },
|
||||
{ value: "SSE", label: "对话流、思考态、消息重试" },
|
||||
{ value: "12 个", label: "微前端拆分基础项目" },
|
||||
{ value: "30%", label: "消息渲染链路优化结果" },
|
||||
{ value: "6000+", label: "门店业务系统服务规模" },
|
||||
];
|
||||
|
||||
export const focusAreas = [
|
||||
{
|
||||
title: "对话链路",
|
||||
summary: "做过流式消息、思考态、来源引用、历史同步和失败重试,重点是让对话过程稳定、可恢复。",
|
||||
points: ["SSE", "Markdown/LaTeX", "来源引用", "消息重试"],
|
||||
},
|
||||
{
|
||||
title: "控制台业务",
|
||||
summary: "参与过 API Key、账单、组织权限、额度、交易筛选和导出,知道复杂后台最怕状态不清和边界不稳。",
|
||||
points: ["API Key", "Billing", "RBAC", "Excel 导出"],
|
||||
},
|
||||
{
|
||||
title: "跨端内容流",
|
||||
summary: "在 Expo / React Native 项目里做过新闻流、详情页、瀑布流、图片组件和游客转登录数据处理。",
|
||||
points: ["Expo", "瀑布流", "Smart Image", "游客绑定"],
|
||||
},
|
||||
{
|
||||
title: "工程习惯",
|
||||
summary: "经历过微前端拆分、跨项目公共包、i18n 协作、监控接入和代码审查,能把模块交付和团队维护放在一起考虑。",
|
||||
points: ["Qiankun", "Monorepo", "i18n", "Code Review"],
|
||||
},
|
||||
];
|
||||
|
||||
export const resumeSignals = [
|
||||
"前端经验覆盖 PC 管理后台、小程序、H5、React Native 和微前端,最近一年主要在 AI 产品团队做业务前端。",
|
||||
"SeaBuzz 侧更偏用户体验:对话、内容流、新闻详情、游客数据、分享和反馈链路。",
|
||||
"SeaCloud / Vtrix 侧更偏控制台:Pricing、Billing、API Keys、组织权限、交易筛选和分销配置。",
|
||||
"技术栈以 React / Next.js / React Native / TypeScript 为主,也有 Vue、UniApp 和 Qiankun 项目经验。",
|
||||
];
|
||||
|
||||
export const skills = [
|
||||
{
|
||||
group: "对话与内容",
|
||||
items: ["SSE 流式通信", "打字机响应", "Markdown/LaTeX", "来源引用", "对话历史", "新闻详情"],
|
||||
},
|
||||
{
|
||||
group: "控制台",
|
||||
items: ["Pricing", "Billing", "API Keys", "组织权限", "额度限制", "交易筛选", "Excel 导出"],
|
||||
},
|
||||
{
|
||||
group: "跨端体验",
|
||||
items: ["React Native", "Expo Router", "NativeWind", "游客转登录", "只读分享", "Smart Image"],
|
||||
},
|
||||
{
|
||||
group: "Web 主栈",
|
||||
items: ["Next.js", "React", "Vue 3", "TypeScript", "Tailwind CSS", "Zustand", "Alova"],
|
||||
},
|
||||
{
|
||||
group: "小程序与 H5",
|
||||
items: ["UniApp", "微信小程序", "飞书小程序", "H5", "摄像头扫码", "低功耗蓝牙"],
|
||||
},
|
||||
{
|
||||
group: "工程协作",
|
||||
items: ["pnpm Monorepo", "Git Submodules", "Qiankun", "Lerna", "i18n 同步", "Code Review"],
|
||||
},
|
||||
{
|
||||
group: "工程化与质量",
|
||||
items: ["ARMS 监控", "CDN 优化", "Playwright", "Git Flow", "分支规范", "新人培训"],
|
||||
},
|
||||
];
|
||||
|
||||
export const projects = [
|
||||
{
|
||||
id: "vtrix",
|
||||
name: "SeaCloud / Vtrix",
|
||||
logo: "/logos/vtrix.png",
|
||||
period: "2025.09 - 至今",
|
||||
subtitle: "模型服务平台控制台",
|
||||
summary:
|
||||
"参与模型服务平台的前端建设,主要处理 Pricing、Billing、API Keys、组织权限、额度和分销相关页面。项目复杂度不在单个页面,而在状态、权限和账务边界。",
|
||||
modules: [
|
||||
"建设分销控制台:客户邀请、折扣模板、额度分配、销售配置与利润率计算。",
|
||||
"封装 useCurrency Hook,贯穿 Pricing、Billing、API Keys 等模型调用和费用模块。",
|
||||
"落地组织角色权限、成员配额、支出限额、账单报表、交易筛选与 Excel 导出。",
|
||||
"参与 i18n Submodule 架构迁移、企业微信翻译同步和通用表格/筛选器/分页组件增强。",
|
||||
],
|
||||
tech: ["Next.js 16", "React 19", "TypeScript", "Tailwind CSS", "Zustand", "Alova", "Radix UI", "next-intl"],
|
||||
},
|
||||
{
|
||||
id: "seabuzz",
|
||||
name: "SeaBuzz",
|
||||
logo: "/logos/seabuzz.webp",
|
||||
period: "2025.05 - 至今",
|
||||
subtitle: "资讯、搜索与对话应用",
|
||||
summary:
|
||||
"基于 Expo 的跨端应用,覆盖 iOS、Android、Web。我的工作集中在对话链路、内容流、登录绑定、分享和公共包沉淀。",
|
||||
modules: [
|
||||
"实现对话链路:SSE 流式聊天、打字机渲染、Markdown/LaTeX、思考动画、来源引用、消息重写与历史同步。",
|
||||
"处理游客聊天记录本地存储、登录后自动绑定、分享链接只读模式和反馈机制。",
|
||||
"交付 Discover 发现页、新闻详情、瀑布流动态布局、骨架屏、Smart Image 与富文本章节渲染。",
|
||||
"集成 Google、Facebook、Discord、Email 登录与 SeaArt Auth SDK。",
|
||||
"封装 Monorepo 公共包体系,覆盖 API、数据模型、UI 组件、状态、OTA 热更新与代码签名。",
|
||||
],
|
||||
tech: ["React Native", "Expo 53", "Expo Router", "Zustand", "NativeWind", "SSE", "i18next", "Adyen"],
|
||||
},
|
||||
{
|
||||
id: "bawang",
|
||||
name: "霸王功夫",
|
||||
logo: "/logos/chagee.png",
|
||||
period: "2024.03 - 2025.03",
|
||||
subtitle: "门店、食安与供应链系统",
|
||||
summary:
|
||||
"服务全球 6000+ 门店及运营伙伴,覆盖门店运营、食品安全、供应链协同等核心业务。",
|
||||
modules: [
|
||||
"搭建小程序报损模块,调用摄像头扫码并兼容微信小程序与飞书 H5。",
|
||||
"实现食安管理模块,通过低频蓝牙连接 TSPL 指令集打印机,覆盖国内、东南亚、北美三套业务逻辑。",
|
||||
"交付多门店经营状态移动看板,支持区域、时间、指标筛选。",
|
||||
"负责采购、供应商合同、供应商结算等供应链系统,对接云厉、费控等外部系统。",
|
||||
],
|
||||
tech: ["React 18", "Vue 3", "UniApp", "MobX", "Pinia", "Element Plus", "Ant Design"],
|
||||
},
|
||||
{
|
||||
id: "jingyingbang",
|
||||
name: "经营帮平台 + 经营帮拉新",
|
||||
logo: "/logos/jingyingbang.png",
|
||||
period: "2022.04 - 2024.03",
|
||||
subtitle: "微前端平台与小程序",
|
||||
summary:
|
||||
"基于信息化设计理念和区块链技术的工业互联网平台,为企业和个人提供数字化运营服务。",
|
||||
modules: [
|
||||
"参与单体前端到 Qiankun 微前端拆分,共拆分出 12 个基础项目。",
|
||||
"负责商管、门户、经营帮小程序、H5、Admin 后台核心业务交付。",
|
||||
"引入华为云 OBS 直传,降低请求链路性能浪费。",
|
||||
"开发内部 Chrome 插件 zjkj-decryption,提升数据解析效率。",
|
||||
],
|
||||
tech: ["Vue", "Qiankun", "Element UI", "华为云 OBS", "百度地图", "高德地图", "UniApp"],
|
||||
},
|
||||
];
|
||||
|
||||
export const experiences = [
|
||||
{
|
||||
company: "成都海艺互娱科技有限公司",
|
||||
logo: "/logos/seaart.webp",
|
||||
role: "React Native 开发工程师",
|
||||
period: "2025.05 - 至今",
|
||||
points: [
|
||||
"参与 SeaBuzz、SeaCloud / Vtrix 等项目,主要负责 React Native 跨端页面和 Web 控制台模块。",
|
||||
"负责对话流、内容流、分享、游客数据绑定、账单、API Keys、组织权限等业务页面开发。",
|
||||
"优化消息渲染链路和跨端公共包复用,消息渲染性能提升约 30%。",
|
||||
],
|
||||
},
|
||||
{
|
||||
company: "四川茶姬企业管理有限公司",
|
||||
logo: "/logos/chagee.png",
|
||||
role: "高级 Web 前端开发",
|
||||
period: "2024.03 - 2025.03",
|
||||
points: [
|
||||
"主导供应链管理系统前端开发,实现采购、仓储、配送等核心模块功能。",
|
||||
"独立负责门店报损与食安核心功能搭建及迭代。",
|
||||
"完成前端监控体系引入、CDN 静态资源优化、代码审查机制与 Git 操作规范建设。",
|
||||
],
|
||||
},
|
||||
{
|
||||
company: "中钧科技有限公司四川分公司",
|
||||
logo: "/logos/zhongjun.png",
|
||||
role: "前端开发组长",
|
||||
period: "2022.04 - 2024.03",
|
||||
points: [
|
||||
"负责经营帮 PC 端微前端、门户和商管核心模块开发,完成 UI 还原、接口联调与性能优化。",
|
||||
"从零搭建新 E 畅行小程序基础框架,制定技术方案并完成全流程开发。",
|
||||
"设计 Git Flow、代码审查与分支管理机制,主导新人培训与项目分工协调。",
|
||||
],
|
||||
},
|
||||
];
|
||||
+745
@@ -0,0 +1,745 @@
|
||||
/* ============ 深色 AI 实验室 ============ */
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
--bg: #070b14;
|
||||
--bg-soft: #0b1120;
|
||||
--ink: #e6edf7;
|
||||
--ink-dim: #8b97ad;
|
||||
--ink-faint: #4d586e;
|
||||
--cyan: #22d3ee;
|
||||
--violet: #a78bfa;
|
||||
--amber: #fbbf24;
|
||||
--line: rgba(139, 151, 173, 0.16);
|
||||
--mono: "JetBrains Mono", "SFMono-Regular", ui-monospace, Menlo, monospace;
|
||||
--sans: "Inter", "PingFang SC", "Noto Sans SC", "Microsoft YaHei", sans-serif;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: auto;
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--bg);
|
||||
color: var(--ink);
|
||||
font-family: var(--sans);
|
||||
font-size: 16px;
|
||||
line-height: 1.7;
|
||||
overflow-x: hidden;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
::selection {
|
||||
background: rgba(34, 211, 238, 0.35);
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--cyan);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* ============ 底层画布与氛围 ============ */
|
||||
#mind-canvas {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.grid-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
background-image: radial-gradient(rgba(139, 151, 173, 0.13) 1px, transparent 1px);
|
||||
background-size: 34px 34px;
|
||||
mask-image: radial-gradient(ellipse 90% 80% at 50% 45%, black 30%, transparent 100%);
|
||||
-webkit-mask-image: radial-gradient(ellipse 90% 80% at 50% 45%, black 30%, transparent 100%);
|
||||
}
|
||||
|
||||
/* ============ HUD ============ */
|
||||
.hud {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 30;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 18px 28px;
|
||||
font-family: var(--mono);
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.14em;
|
||||
background: linear-gradient(to bottom, rgba(7, 11, 20, 0.85), transparent);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.hud-id {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.hud-name {
|
||||
color: var(--ink);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.hud-sub {
|
||||
color: var(--ink-faint);
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.hud-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: var(--cyan);
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
background: var(--cyan);
|
||||
box-shadow: 0 0 10px var(--cyan);
|
||||
animation: pulse 2.2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.35; }
|
||||
}
|
||||
|
||||
/* ============ 会话进度轨 ============ */
|
||||
#session-rail {
|
||||
position: fixed;
|
||||
right: 22px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
z-index: 30;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.rail-node {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-direction: row-reverse;
|
||||
background: none;
|
||||
border: 0;
|
||||
padding: 6px 0;
|
||||
cursor: pointer;
|
||||
font-family: var(--mono);
|
||||
font-size: 9px;
|
||||
letter-spacing: 0.18em;
|
||||
color: var(--ink-faint);
|
||||
transition: color 0.3s;
|
||||
}
|
||||
|
||||
.rail-node::after {
|
||||
content: "";
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid var(--ink-faint);
|
||||
transition: all 0.3s;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.rail-node .rail-label {
|
||||
opacity: 0;
|
||||
transform: translateX(6px);
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.rail-node:hover .rail-label {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.rail-node.active {
|
||||
color: var(--cyan);
|
||||
}
|
||||
|
||||
.rail-node.active::after {
|
||||
background: var(--cyan);
|
||||
border-color: var(--cyan);
|
||||
box-shadow: 0 0 12px var(--cyan);
|
||||
}
|
||||
|
||||
.rail-node.active .rail-label {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
/* ============ 对话轮次 ============ */
|
||||
main {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.round {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 96px 7vw;
|
||||
}
|
||||
|
||||
.round-inner {
|
||||
width: min(640px, 100%);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.round[data-side="-1"] .round-inner { margin-left: auto; }
|
||||
.round[data-side="1"] .round-inner { margin-right: auto; }
|
||||
.round[data-side="0"] .round-inner { margin: 0 auto; }
|
||||
|
||||
/* 提问气泡(访客) */
|
||||
.q-bubble {
|
||||
display: inline-flex;
|
||||
align-items: baseline;
|
||||
margin-left: auto;
|
||||
margin-bottom: 34px;
|
||||
padding: 12px 20px;
|
||||
border: 1px solid rgba(34, 211, 238, 0.45);
|
||||
border-radius: 18px 18px 4px 18px;
|
||||
font-family: var(--mono);
|
||||
font-size: 15px;
|
||||
color: var(--cyan);
|
||||
background: rgba(34, 211, 238, 0.05);
|
||||
min-height: 46px;
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
.q-row {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.q-caret {
|
||||
display: inline-block;
|
||||
width: 8px;
|
||||
height: 1.1em;
|
||||
margin-left: 4px;
|
||||
background: var(--cyan);
|
||||
transform: translateY(2px);
|
||||
animation: blink 0.9s steps(1) infinite;
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
50% { opacity: 0; }
|
||||
}
|
||||
|
||||
/* 思考态 */
|
||||
.thinking {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 30px;
|
||||
font-family: var(--mono);
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.2em;
|
||||
color: var(--violet);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.thinking .dots {
|
||||
display: inline-flex;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.thinking .dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: var(--violet);
|
||||
animation: think 1.1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.thinking .dot:nth-child(2) { animation-delay: 0.18s; }
|
||||
.thinking .dot:nth-child(3) { animation-delay: 0.36s; }
|
||||
|
||||
@keyframes think {
|
||||
0%, 100% { transform: translateY(0); opacity: 0.4; }
|
||||
50% { transform: translateY(-5px); opacity: 1; }
|
||||
}
|
||||
|
||||
/* 回答块 */
|
||||
.answer {
|
||||
position: relative;
|
||||
padding-left: 26px;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.answer::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 4px;
|
||||
bottom: 4px;
|
||||
width: 2px;
|
||||
border-radius: 2px;
|
||||
background: linear-gradient(to bottom, var(--cyan), var(--violet));
|
||||
box-shadow: 0 0 14px rgba(167, 139, 250, 0.5);
|
||||
}
|
||||
|
||||
.a-caret {
|
||||
display: inline-block;
|
||||
margin-top: 14px;
|
||||
color: var(--violet);
|
||||
font-family: var(--mono);
|
||||
animation: blink 1s steps(1) infinite;
|
||||
}
|
||||
|
||||
.a-title {
|
||||
font-family: var(--mono);
|
||||
font-size: 13px;
|
||||
letter-spacing: 0.22em;
|
||||
color: var(--violet);
|
||||
margin-bottom: 18px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
/* ============ Round 0: BOOT ============ */
|
||||
#r-boot .round-inner {
|
||||
width: min(760px, 100%);
|
||||
}
|
||||
|
||||
.boot-greeting {
|
||||
font-size: clamp(34px, 5.4vw, 58px);
|
||||
font-weight: 700;
|
||||
line-height: 1.18;
|
||||
letter-spacing: -0.01em;
|
||||
min-height: 1.2em;
|
||||
}
|
||||
|
||||
.boot-greeting .grad {
|
||||
background: linear-gradient(100deg, var(--cyan), var(--violet));
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.boot-identity {
|
||||
margin-top: 20px;
|
||||
font-family: var(--mono);
|
||||
font-size: 14px;
|
||||
color: var(--ink-dim);
|
||||
}
|
||||
|
||||
.boot-identity em {
|
||||
font-style: normal;
|
||||
color: var(--cyan);
|
||||
}
|
||||
|
||||
.metric-stream {
|
||||
margin-top: 44px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.metric-token {
|
||||
font-family: var(--mono);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 10px;
|
||||
padding: 10px 16px;
|
||||
background: rgba(11, 17, 32, 0.7);
|
||||
backdrop-filter: blur(6px);
|
||||
}
|
||||
|
||||
.metric-token .v {
|
||||
color: var(--amber);
|
||||
font-weight: 700;
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
.metric-token .l {
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
color: var(--ink-dim);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.boot-hint {
|
||||
margin-top: 56px;
|
||||
font-family: var(--mono);
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.24em;
|
||||
color: var(--ink-faint);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.boot-hint::after {
|
||||
content: "↓";
|
||||
animation: bob 1.6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes bob {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(6px); }
|
||||
}
|
||||
|
||||
/* ============ Round 1: 概述 ============ */
|
||||
.signal-line {
|
||||
display: flex;
|
||||
gap: 14px;
|
||||
margin-bottom: 18px;
|
||||
font-size: 16px;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.signal-line .idx {
|
||||
font-family: var(--mono);
|
||||
font-size: 12px;
|
||||
color: var(--ink-faint);
|
||||
padding-top: 5px;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
/* ============ Round 2: 焦点卡 ============ */
|
||||
.focus-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.focus-card {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 14px;
|
||||
padding: 20px;
|
||||
background: rgba(11, 17, 32, 0.72);
|
||||
backdrop-filter: blur(8px);
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
|
||||
.focus-card:hover {
|
||||
border-color: rgba(34, 211, 238, 0.4);
|
||||
}
|
||||
|
||||
.focus-card h3 {
|
||||
font-size: 16px;
|
||||
margin-bottom: 8px;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.focus-card p {
|
||||
font-size: 13px;
|
||||
color: var(--ink-dim);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.tag-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.tag {
|
||||
font-family: var(--mono);
|
||||
font-size: 10.5px;
|
||||
padding: 3px 9px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid rgba(167, 139, 250, 0.35);
|
||||
color: var(--violet);
|
||||
}
|
||||
|
||||
/* ============ Round 3: 项目 ============ */
|
||||
.project-block {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 16px;
|
||||
padding: 24px;
|
||||
margin-bottom: 18px;
|
||||
background: rgba(11, 17, 32, 0.78);
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
.project-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.project-head img {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
border-radius: 10px;
|
||||
object-fit: cover;
|
||||
background: var(--bg-soft);
|
||||
}
|
||||
|
||||
.project-head .name {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.project-head .period {
|
||||
margin-left: auto;
|
||||
font-family: var(--mono);
|
||||
font-size: 11px;
|
||||
color: var(--ink-faint);
|
||||
}
|
||||
|
||||
.project-sub {
|
||||
font-family: var(--mono);
|
||||
font-size: 12px;
|
||||
color: var(--cyan);
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.project-summary {
|
||||
font-size: 13.5px;
|
||||
color: var(--ink-dim);
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.project-modules {
|
||||
list-style: none;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.project-modules li {
|
||||
position: relative;
|
||||
padding-left: 18px;
|
||||
font-size: 13px;
|
||||
color: var(--ink);
|
||||
margin-bottom: 7px;
|
||||
}
|
||||
|
||||
.project-modules li::before {
|
||||
content: "▸";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
color: var(--violet);
|
||||
}
|
||||
|
||||
/* ============ Round 4: 技能 ============ */
|
||||
.skill-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.skill-group .g-name {
|
||||
font-family: var(--mono);
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.2em;
|
||||
color: var(--ink-faint);
|
||||
margin-bottom: 8px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.skill-token {
|
||||
font-family: var(--mono);
|
||||
font-size: 12px;
|
||||
padding: 6px 12px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--line);
|
||||
background: rgba(11, 17, 32, 0.7);
|
||||
color: var(--ink);
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.skill-token.hot {
|
||||
border-color: rgba(34, 211, 238, 0.5);
|
||||
color: var(--cyan);
|
||||
}
|
||||
|
||||
/* ============ Round 5: 经历 ============ */
|
||||
.exp-item {
|
||||
position: relative;
|
||||
padding: 0 0 30px 30px;
|
||||
}
|
||||
|
||||
.exp-item::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 6px;
|
||||
top: 8px;
|
||||
bottom: -4px;
|
||||
width: 1px;
|
||||
background: var(--line);
|
||||
}
|
||||
|
||||
.exp-item:last-child::before { bottom: auto; height: calc(100% - 8px); }
|
||||
|
||||
.exp-item::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 2px;
|
||||
top: 8px;
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
border-radius: 50%;
|
||||
background: var(--bg);
|
||||
border: 2px solid var(--cyan);
|
||||
box-shadow: 0 0 10px rgba(34, 211, 238, 0.6);
|
||||
}
|
||||
|
||||
.exp-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.exp-head img {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border-radius: 7px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.exp-head .co {
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.exp-head .period {
|
||||
font-family: var(--mono);
|
||||
font-size: 11px;
|
||||
color: var(--ink-faint);
|
||||
}
|
||||
|
||||
.exp-role {
|
||||
font-family: var(--mono);
|
||||
font-size: 12px;
|
||||
color: var(--violet);
|
||||
margin: 4px 0 8px;
|
||||
}
|
||||
|
||||
.exp-points {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.exp-points li {
|
||||
font-size: 13px;
|
||||
color: var(--ink-dim);
|
||||
margin-bottom: 5px;
|
||||
padding-left: 16px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.exp-points li::before {
|
||||
content: "—";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
color: var(--ink-faint);
|
||||
}
|
||||
|
||||
/* ============ Round 6: 联系 ============ */
|
||||
.contact-lead {
|
||||
font-size: clamp(24px, 3.4vw, 36px);
|
||||
font-weight: 700;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.contact-id {
|
||||
font-family: var(--mono);
|
||||
font-size: 12.5px;
|
||||
color: var(--ink-dim);
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
|
||||
.contact-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
margin-bottom: 36px;
|
||||
}
|
||||
|
||||
.contact-btn {
|
||||
font-family: var(--mono);
|
||||
font-size: 13px;
|
||||
padding: 12px 22px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--line);
|
||||
color: var(--ink);
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.contact-btn.primary {
|
||||
border-color: var(--cyan);
|
||||
color: #06222b;
|
||||
background: var(--cyan);
|
||||
box-shadow: 0 0 24px rgba(34, 211, 238, 0.35);
|
||||
}
|
||||
|
||||
.contact-btn:not(.primary):hover {
|
||||
border-color: var(--violet);
|
||||
color: var(--violet);
|
||||
}
|
||||
|
||||
.session-end {
|
||||
font-family: var(--mono);
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.24em;
|
||||
color: var(--ink-faint);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.session-end::before,
|
||||
.session-end::after {
|
||||
content: "";
|
||||
height: 1px;
|
||||
flex: 1;
|
||||
background: var(--line);
|
||||
}
|
||||
|
||||
/* ============ 响应式 ============ */
|
||||
@media (max-width: 880px) {
|
||||
.round {
|
||||
padding: 80px 6vw;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.round-inner {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.focus-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
#session-rail {
|
||||
right: auto;
|
||||
top: 54px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
flex-direction: row;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.rail-node .rail-label { display: none; }
|
||||
|
||||
.hud { padding: 14px 18px; }
|
||||
|
||||
.project-head .period {
|
||||
margin-left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* ============ 降级 ============ */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.q-caret, .a-caret, .status-dot, .thinking .dot, .boot-hint::after {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.thinking { display: none; }
|
||||
.answer { opacity: 1; }
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user