This commit is contained in:
yinsx
2026-01-05 10:16:36 +08:00
commit f94ffe0adc
41 changed files with 4387 additions and 0 deletions

84
.drone.yml Normal file
View File

@ -0,0 +1,84 @@
kind: pipeline # 定义一个管道
type: docker # 当前管道的类型
name: test # 当前管道的名称
steps:
# 第一步:构建项目
- name: 构建项目
image: node:18-alpine
commands:
- rm -rf node_modules
- npm ci
- npm run build
# 第二步上传静态资源到腾讯云COS (使用另一个插件)
- name: 静态资源上传到cos
image: ccr.ccs.tencentyun.com/xiaoqidun/gocos:latest
settings:
secret_id:
from_secret: cos_secret_id
secret_key:
from_secret: cos_secret_key
bucket_url: https://files-1302416092.cos.ap-shanghai.myqcloud.com
source_path: dist
target_path: /studio
strip_prefix: dist
# 第三步:部署到服务器
- name: 清除服务器缓存
image: appleboy/drone-ssh
settings:
host:
from_secret: server_host
username:
from_secret: server_username
password:
from_secret: server_password
# 或者使用SSH密钥
# key:
# from_secret: server_ssh_key
port: 22
script:
- rm -rf /www/wwwroot/studio.zguiy.com/*
- mkdir -p /www/wwwroot/studio.zguiy.com/
- chmod 755 /www/wwwroot/studio.zguiy.com/
when:
branch:
- main
- master
- dev
# 第四步:上传构建文件
- name: 上传构建文件
image: appleboy/drone-scp
settings:
host:
from_secret: server_host
username:
from_secret: server_username
password:
from_secret: server_password
# 或者使用SSH密钥
# key:
# from_secret: server_ssh_key
port: 22
source: dist/*
target: /www/wwwroot/studio.zguiy.com/
strip_components: 1
when:
branch:
- main
- master
- dev
# 触发条件
trigger:
branch:
- main
- master
- dev
event:
- push
- pull_request

1
.env.development Normal file
View File

@ -0,0 +1 @@
VITE_PUBLIC = /

1
.env.production Normal file
View File

@ -0,0 +1 @@
VITE_PUBLIC = https://cdn.files.zguiy.com/studio/

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
/dist
/node_modules
/.claude

178
cursor.md Normal file
View File

@ -0,0 +1,178 @@
# AI Code Generation Prompts: Startup Website Clone
**Tech Stack:** Vite + Vue 3 (Script Setup) + Tailwind CSS + Lucide Icons
---
## 🟢 Step 0: Project Setup (Global System Prompt)
*在开始编写具体页面前,先发送这条指令,建立全局的设计规范和技术栈。*
**Prompt:**
> **Role:** Senior Frontend Developer specializing in Pixel-Perfect UI cloning.
>
> **Tech Stack Requirements:**
> - **Framework:** Vue 3 (Composition API with `<script setup>`) + typescript.
> - **Build Tool:** Vite.
> - **Styling:** Tailwind CSS (Mobile-first approach).
> - **Icons:** `lucide-vue-next` (You must use this library).
> - **Language:** TypeScript.
>
> **Design System (Strict Adherence):**
> - **Visual Style:** Minimalist, "Vercel-like" aesthetic. High contrast, clean lines.
> - **Typography:** Inter or system-ui. Headings must use `tracking-tight` (letter-spacing: -0.025em).
> - **Colors:**
> - Background: `#ffffff` (White).
> - Surface/Card: `#ffffff` (White) with `border-gray-200`.
> - Primary Text: `#0f172a` (Slate-900).
> - Muted Text: `#64748b` (Slate-500).
> - Accents: Black buttons, Light gray backgrounds (`bg-gray-50`) for secondary elements.
> - **Borders & Radius:** Delicate borders (`border`, `border-gray-100`). Card radius is `rounded-xl` or `rounded-2xl`.
> - **Shadows:** Very subtle `shadow-sm`, hover states use `shadow-md`.
>
> **Instruction:** I will provide tasks to build 3 specific pages. You must replicate the layout, spacing, and Chinese copy exactly as described.
---
## 🟡 Step 1: Pricing Page (定价页)
*复制此段生成定价页面组件。*
**Prompt:**
> **Task:** Create a **`PricingPage.vue`** component. Clone the provided "Pricing" design exactly.
>
> **1. Page Header:**
> - Layout: Centered.
> - Overline: "PRICING" (uppercase, tracking-widest, text-xs, text-gray-400).
> - Title: "产品购买页面" (text-3xl font-bold tracking-tight).
> - Subtitle: "按需套餐付费,尊享 Codex 与 Claude Code 两大产品线" (text-gray-500 mt-2).
>
> **2. Section 1: Codex Plans (2-Column Grid):**
> - **Heading:** "Codex 套餐" (Bold, text-lg, mb-4).
> - **Layout:** Grid `grid-cols-1 md:grid-cols-2 gap-6`.
> - **Card Design:** `bg-white border border-gray-200 rounded-xl p-6 relative`.
> - **Left Card (Standard):**
> - Top: Icon + "Codex" + "codex标准套餐". Badge: "工效效率" (Top Right, gray bg, text-xs).
> - Price: "¥79.9/月" (text-4xl font-bold). Subtext: "性价比首选 | 强大智能".
> - Features: List with blue bullet points (daily budget $90, etc.).
> - **Right Card (Enterprise):**
> - Price: "¥ 999". Badge: "定制高频版".
> - Features: "个人/企业 定制高险套餐", "高频需求首选".
>
> **3. Section 2: Claude Code Plans (3-Column Grid):**
> - **Heading:** "Claude Code 套餐" (Bold, text-lg, mt-10 mb-4).
> - **Layout:** Grid `grid-cols-1 md:grid-cols-3 gap-6`.
> - **Cards:**
> - Card 1: ¥228/月. Badge: "轻量性价比".
> - Card 2: ¥398/月. Badge: "适合中等规模". (Highlight: slightly darker border or shadow).
> - Card 3: ¥520/月. Badge: "深度运算".
> - **Bottom Wide Card:** A full-width card for "Claude Code 定制套餐" (¥999).
>
> **4. CRITICAL COMPONENT - Bottom Action Button:**
> - Inside EVERY card, pinned to the bottom.
> - Container: `bg-gray-50` (light gray), `rounded-lg`, `p-4`, `mt-6`.
> - Layout: Flex row, `justify-between`, `items-center`.
> - Text Left: "联系顾问" (font-medium text-sm).
> - Text Right: Small gray caption (e.g., "codex标准套餐 · ¥79.9/月").
> - Icon: Small `ChevronDown` or `ChevronRight` on the right side.
>
> **5. Copy:** Use the exact Chinese text provided in the description.
---
## 🔵 Step 2: Documentation Page (使用文档页)
*复制此段生成文档页面组件。*
**Prompt:**
> **Task:** Create a **`DocsPage.vue`** component. Clone the "Usage Guide" design exactly.
>
> **1. Header & Controls:**
> - Title: "使用说明". Subtitle: "新产品与插件安装的快速指南".
> - **Tabs (Model):** A pill-shaped toggle container. Items: [Codex | Claude Code | GLM].
> - Active State: Black background, White text.
> - Inactive State: Transparent background, Gray text.
> - **Tabs (OS):** Underline style. [Windows | macOS | Linux]. Active item has a black underline.
>
> **2. Vertical Stepper Layout (The Core Visual):**
> - **Container:** Max-width 800px, centered.
> - **Structure:** A vertical list of steps.
> - **Left Column:** A Step Indicator.
> - A black circle (`w-6 h-6 bg-black text-white rounded-full flex items-center justify-center text-xs font-bold`) with the number (1, 2, 3).
> - A thin gray vertical line (`w-px bg-gray-200`) connecting the numbers.
> - **Right Column:** The content.
>
> **3. Step Content Details:**
> - **Step 1 (Install Node.js):**
> - Title: "安装 Node.js 18+ (通用)".
> - Link: "官网下载 (推荐)" in blue.
> - Warning Box: A `bg-gray-50` rounded box containing bullet points about permissions.
> - Version Check: A dark bar `node --version`.
> - **Step 2 (Install CLI):**
> - Title: "安装 Codex CLI".
> - **Code Block:** A dark container (`bg-[#0f172a]`), `rounded-lg`, `p-4`, `shadow-inner`.
> - Text: `npm install -g @openai/codex` (White text, font-mono).
> - **Step 3 (Config):**
> - Title: "配置环境 (CodeX)".
> - **File Editor UI:** Create a mock code editor.
> - Filename: "config.toml文件" (gray text above block).
> - Code Content: Multi-line TOML config. Use specific colors for syntax highlighting (e.g., Strings in green, Keys in purple/white).
>
> **4. Component Abstraction:**
> - Please create a separate `<CodeBlock :code="code" :lang="lang" />` component for reusability.
---
## 🔴 Step 3: Features Page (功能特性页)
*复制此段生成功能着陆页组件。*
**Prompt:**
> **Task:** Create a **`FeaturesPage.vue`** component. Clone the "Features" design exactly.
>
> **1. Hero Section:**
> - Alignment: Center.
> - Headline: "更快、更智能地构建软件" (text-5xl font-bold tracking-tight mb-6).
> - Subheadline: "聚合多模型,深度理解你的代码库..." (text-gray-500 max-w-2xl mx-auto).
> - Action Buttons:
> - Primary: "开始使用" (Black bg, white text, rounded-full, px-8 py-2.5).
> - Secondary: "查看文档" (White bg, border border-gray-200, black text, rounded-full, px-8 py-2.5).
>
> **2. The Bento Grid (Critical Visual):**
> - Section Title: "覆盖从编码到交付的全流程".
> - **Grid System:** `grid-cols-1 md:grid-cols-3 gap-6`.
> - **Card Anatomy:**
> - `bg-white border border-gray-200 rounded-xl p-6 flex flex-col h-full overflow-hidden hover:shadow-md transition-all`.
> - **Icon:** Top-left. Small Lucide icon inside a square `bg-gray-50` rounded container.
> - **Text:** Title (font-bold mt-4) + Description (text-sm text-gray-500 mt-2).
> - **The Mockup Area (Must Have):** At the bottom of each card, create a visual placeholder.
> - *Implementation:* A `div` with `bg-gray-50/50 mt-6 h-32 w-full rounded border border-dashed border-gray-200`. Put a small "skeleton" UI inside (e.g., a few gray bars representing code lines) to mimic the screenshots.
>
> **3. Stats Bar:**
> - Layout: Flex row, wide container.
> - Items:
> - "2x" (交付效率提升)
> - "40%" (Bug 修复加速)
> - "60%" (重复劳动减少)
> - Style: Numbers are huge (`text-5xl font-bold tracking-tighter`). Labels are small and gray.
>
> **4. Footer CTA:**
> - Text: "准备好升级研发效率了吗?"
> - Button: "免费开始" (White button, border, rounded-full).
---
## 💡 Developer Tips (For You)
1. **Global Font Fix:** To get the crisp look from the screenshots, add this to your `index.css` or `style.css`:
```css
body {
font-family: -apple-system, BlinkMacSystemFont, "Inter", "Segoe UI", Roboto, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
```
2. **Icon Installation:** Ensure you have the icons installed:
```bash
npm install lucide-vue-next
```
3. **Refining:** If the AI output looks "too loose", tell it: *"Reduce the border-radius to 12px and make the text colors sharper (slate-900 instead of gray-800)."*

13
index.html Normal file
View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>自由职业开发 · 前端/全栈/Web3D · 交付型合作</title>
<meta name="description" content="承接企业官网、后台管理、全栈业务系统与 Web3D 互动展示Three.js / Babylon.js支持按里程碑交付与长期维护。" />
</head>
<body class="bg-white text-slate-900">
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

2579
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

31
package.json Normal file
View File

@ -0,0 +1,31 @@
{
"name": "studio",
"private": true,
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vue-tsc --noEmit && vite build",
"preview": "vite preview"
},
"dependencies": {
"@types/prismjs": "^1.26.5",
"@vueuse/core": "^14.1.0",
"lucide-vue-next": "^0.400.0",
"marked": "^17.0.1",
"prismjs": "^1.30.0",
"typeit": "^8.8.7",
"vue": "^3.4.21",
"vue-router": "^4.3.0"
},
"devDependencies": {
"@types/node": "^25.0.3",
"@vitejs/plugin-vue": "^5.0.3",
"autoprefixer": "^10.4.16",
"postcss": "^8.4.33",
"tailwindcss": "^3.4.1",
"typescript": "5.4.5",
"vite": "^5.0.10",
"vue-tsc": "^1.8.26"
}
}

6
postcss.config.js Normal file
View File

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View File

@ -0,0 +1,39 @@
# 案例模板(可直接复制改内容)
> 这是一个 Markdown 案例模板,你可以复制成 `case-xxx.md`,然后在页面里直接预览。
## 项目背景
- 行业 / 场景:
- 目标(转化/展示/提效):
- 时间周期:
- 我的角色:前端 / 全栈 / Web3D / 小程序
## 交付内容
- 核心页面:
- 核心功能:
- 技术栈:
- 部署方式:
## 难点与解决方案
1. 难点:
2. 方案:
3. 结果:
## 代码片段(示例)
```ts
export type ApiResponse<T> = {
code: number
message: string
data: T
}
```
## 复盘
- 做得好的:
- 下次会更好的:

16
public/docs/contact.md Normal file
View File

@ -0,0 +1,16 @@
# 联系方式
> 建议把这里的联系方式换成你的真实信息。
- 微信:`TyTinSx`
- 邮箱:`14154666@qq.com`
## 发需求时建议包含
1. 项目类型:官网/后台/全栈/Web3D/小程序
2. 目标:要达成什么效果或指标
3. 范围:页面/功能清单(越具体越好)
4. 参考:竞品/参考链接、设计稿、文案素材
5. 时间:期望上线时间、是否有阶段节点
6. 现状:是否已有代码、接口、服务器、域名等

22
public/docs/services.md Normal file
View File

@ -0,0 +1,22 @@
# 服务清单
## 我能做什么
- 企业官网 / 产品官网 / 落地页品牌展示、动效、响应式、SEO 基础、上线部署。
- 后台管理系统:权限、表格、筛选、导入导出、图表看板、审批流(按需)。
- 全栈业务开发:接口设计、鉴权、数据库建模、联调、部署(按需)。
- Web3D 互动展示Three.js / Babylon.js模型加载、交互热点、性能优化与业务联动。
- 3D 建模与渲染建模、UV、贴图烘焙、PBR 材质、灯光渲染与出片(静帧/视频)。
- 小程序开发:微信小程序 / uni-app按需常见业务页面与接口联调发布。
## 你需要提供什么
- 目标与范围:要解决什么问题?核心流程是什么?
- 参考与素材参考链接、设计稿Figma/蓝湖/Sketch、文案、图片、Logo 等。
- 约束条件:时间节点、预算范围、技术限制、已有接口/账号/服务器(如有)。
## 我会交付什么
- 可运行项目(源码 + 构建产物)
- 交付说明(部署/环境变量/使用说明)
- 里程碑验收清单(对照验收更省心)

63
public/docs/web3d.md Normal file
View File

@ -0,0 +1,63 @@
# Web3D 案例展示
基于 Three.js / Babylon.js 开发的 3D 互动展示项目。
---
## 小型货车 3D 配置器
![蓝擎X1货车](/案例图片/web3d/5b86f5fffbab10515fa800a435620437.png)
蓝擎X1小型货车在线配置器支持颜色切换极光蓝/珠光白)、外观/内饰/行驶模式预览,可触发车门开关、车灯等交互动画。
🔗 暂无分享链接
---
## SUV 汽车 3D 展示
![星仓SUV](/案例图片/web3d/dfcac2db610f8005a823d0dc93d8cea9.png)
星仓系列 SUV 3D 展厅,支持宝石蓝、水晶紫、冰川银、岩石灰等多种车漆颜色实时切换,外观与内饰双视角浏览。
🔗 暂无分享链接
---
## 虚拟数字展厅
![虚拟展厅](/案例图片/web3d/de96ab4609d15338c15f7eb97ec82cc1.png)
企业虚拟展馆,第三人称漫游模式,集成产品陈列、数据大屏、中国地图可视化,支持虚拟人物导览。
🔗 暂无分享链接
---
## 摩托车 3D 展示
![摩托车展示](/案例图片/web3d/xw_20251231091651.png)
高精度摩托车模型展示,展厅灯光环境,支持 360° 旋转查看细节。
🔗 暂无分享链接
---
## 运动鞋 3D 配置器
![运动鞋配置](/案例图片/web3d/xw_20251231091744.png)
运动鞋在线定制工具,支持多种配色方案实时切换,材质纹理清晰可见。
🔗 暂无分享链接
---
## 自行车 3D 展示
![自行车展示](/案例图片/web3d/xw_20251231091809.png)
山地自行车户外场景展示,支持车型配置选择,实时渲染骑行效果预览。
🔗 暂无分享链接

View File

@ -0,0 +1,33 @@
# 建模与渲染PBR 流程)
面向「产品 / 角色 / 场景」资产制作:建模 → UV → 烘焙 → PBR 材质 → 灯光渲染 → 出片(静帧/视频)。流程可按你的项目预算与时间裁剪。
## 适合的需求
- 产品展示电商主图、详情图、爆炸图、360 展示、质感渲染
- 角色/道具:游戏或宣传视觉的资产制作与渲染出片
- 场景/小片段:镜头分镜、氛围灯光、简单动画与剪辑出片(按需)
## 标准流程(可按项目裁剪)
1. 需求对齐:用途(电商/游戏/交互/视频)、风格参考、镜头与交付规格(分辨率/FPS/时长)
2. 建模:高模/低模(按需)、拓扑优化、模型命名与层级整理
3. UV拆分、展开、打包为贴图与烘焙做准备
4. 烘焙Normal / AO / Curvature / ID 等(按项目需要)
5. 材质PBRBaseColor / Metallic / Roughness / Normal / AO+ Emissive/Opacity 按需)
6. 灯光与渲染HDRI/三点布光、材质校色、渲染参数与降噪(按引擎与预算)
7. 合成与出片通道AOV合成、调色、字幕/转场、输出静帧或视频
8. 交付:源文件 + 贴图 + 成片;需要适配引擎/平台也可输出 glTF/FBX按需
## 你需要提供什么
- 参考与目标:风格图/竞品链接、用途与核心卖点
- 素材Logo/贴纸/花纹(如有)、尺寸/结构信息(如有)
- 交付规格:分辨率、画幅、背景(透明/纯色/场景)、视频时长与帧率
## 我会交付什么
- 源文件(按工具):工程文件/模型文件/材质节点(按约定)
- 贴图文件PBR 贴图与烘焙贴图(按约定打包)
- 成片静帧PNG/JPG或视频MP4可提供可复用的镜头与灯光设置按需

34
public/docs/按天.md Normal file
View File

@ -0,0 +1,34 @@
# 按天合作
适合短期支援、技术咨询或灵活用工需求。
## 合作方式
1. 确认工作内容与时间
2. 按天计费,灵活安排
3. 每日同步进度
4. 按实际工作天数结算
## 适用场景
- 项目紧急支援
- 技术难题攻关
- 代码审查与优化
- 架构设计咨询
## 日费参考
| 服务类型 | 日费 |
|---------|------|
| 前端开发 | ¥800 - ¥1,500 |
| 后端开发 | ¥1,000 - ¥2,000 |
| 全栈开发 | ¥1,200 - ¥2,500 |
| 技术咨询 | ¥1,500 - ¥3,000 |
## 工作时间
- 标准8小时/天
- 支持远程协作
- 可根据需求调整
> 长期合作可享优惠价格

27
public/docs/按项目.md Normal file
View File

@ -0,0 +1,27 @@
# 按项目合作
适合有明确需求、完整功能模块的客户。
## 合作方式
1. 需求沟通与评估
2. 报价与合同签订
3. 分阶段交付与验收
4. 项目上线与维护
## 适用场景
- 企业官网开发
- 小程序/App 开发
- 后台管理系统
- 定制化功能模块
## 报价参考
| 项目类型 | 周期 | 价格区间 |
|---------|------|---------|
| 企业官网 | 1-2周 | ¥3,000 - ¥8,000 |
| 小程序 | 2-4周 | ¥5,000 - ¥15,000 |
| 管理后台 | 3-6周 | ¥10,000 - ¥30,000 |
> 具体报价根据实际需求评估

Binary file not shown.

After

Width:  |  Height:  |  Size: 274 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 594 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 274 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 393 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

141
src/App.vue Normal file
View File

@ -0,0 +1,141 @@
<template>
<div class="flex min-h-screen flex-col bg-white text-slate-900">
<header
:class="[
'sticky top-0 z-10 border-b transition-[border-color,background,backdrop-filter] duration-[400ms] ease-[cubic-bezier(0.4,0,0.2,1)]',
isScrolled ? 'border-transparent bg-transparent [backdrop-filter:none]' : 'border-gray-200 bg-white/90 backdrop-blur-[6px]',
]"
>
<div
class="mx-auto max-w-[1200px] transition-[width] ease-[cubic-bezier(0.34,1.56,0.64,1)]"
:style="{ width: `${capsuleWidth}%`, transitionDuration: isScrolled ? '1000ms' : '1500ms' }"
>
<div
:class="[
'flex items-center justify-between transition-[height,margin-top,padding,background,border-radius,box-shadow] duration-[400ms] ease-[cubic-bezier(0.22,1,0.36,1)] max-[640px]:h-auto max-[640px]:flex-col max-[640px]:items-start max-[640px]:gap-3 max-[640px]:px-0 max-[640px]:py-3',
isScrolled
? 'mt-3 h-14 rounded-full bg-white/[0.98] px-5 shadow-[0_4px_24px_rgba(0,0,0,0.08),0_1px_2px_rgba(0,0,0,0.04)]'
: 'mt-0 h-[72px] rounded-none bg-transparent px-6 shadow-none',
]"
>
<div class="inline-flex items-center gap-2.5">
<div class="h-5 w-5 rotate-45 rounded-[4px] border-2 border-slate-900 shadow-[0_2px_6px_rgba(0,0,0,0.08)]" />
<div class="grid leading-tight">
<span class="text-sm font-semibold">{{ site.brand }}</span>
<!-- <span class="text-[11px] text-gray-500">{{ site.subtitle }}</span> -->
</div>
</div>
<nav class="flex items-center gap-[18px] max-[900px]:hidden">
<button
v-for="tab in tabs"
:key="tab.id"
:class="[
'border-b-2 border-transparent pb-1.5 text-sm text-gray-500 transition-colors hover:text-gray-900',
activeId === tab.id ? 'border-black text-black' : '',
]"
@click="handleNav(tab.path)"
>
{{ tab.label }}
</button>
</nav>
<div class="flex items-center gap-3 max-[640px]:w-full max-[640px]:justify-between">
<a
class="rounded-full border border-slate-900 bg-slate-900 px-4 py-2.5 text-sm font-semibold text-white shadow-[0_10px_24px_rgba(0,0,0,0.12)] transition-[transform,box-shadow,background,color] duration-200 hover:shadow-[0_12px_28px_rgba(0,0,0,0.16)] active:translate-y-px"
:href="`mailto:${site.contact.email}`"
>
咨询合作
</a>
</div>
</div>
</div>
</header>
<main class="mx-auto w-full max-w-[1200px] flex-1 px-6 pt-8 pb-12 max-[640px]:px-4 max-[640px]:pt-6 max-[640px]:pb-10">
<RouterView />
</main>
<footer class="border-t border-gray-200 bg-white">
<div class="mx-auto grid w-full max-w-[1200px] grid-cols-5 gap-6 px-6 pt-10 pb-6 max-[900px]:grid-cols-2 max-[640px]:grid-cols-1">
<div class="col-span-2 grid gap-3 max-[900px]:col-span-2 max-[640px]:col-span-1">
<div class="inline-flex items-center gap-2.5">
<div class="h-5 w-5 rotate-45 rounded-[4px] border-2 border-slate-900 shadow-[0_2px_6px_rgba(0,0,0,0.08)]" />
<span class="text-sm font-semibold">{{ site.brand }}</span>
</div>
<p class="m-0 leading-relaxed text-gray-600">{{ site.tagline }}</p>
<div class="grid gap-1 text-xs text-gray-500">
<div>微信{{ site.contact.wechat }}</div>
<div>邮箱{{ site.contact.email }}</div>
</div>
<p class="m-0 text-xs text-gray-400">© 2025 {{ site.brand }}. All rights reserved.</p>
</div>
<div class="grid gap-2">
<div class="font-bold text-gray-900">服务</div>
<ul class="m-0 grid list-none gap-1.5 p-0 text-gray-600">
<li>企业官网 / 落地页</li>
<li>后台管理系统</li>
<li>全栈业务开发</li>
<li>Web3D 互动展示</li>
<li>3D建模与渲染PBR</li>
</ul>
</div>
<div class="grid gap-2">
<div class="font-bold text-gray-900">技术栈</div>
<ul class="m-0 grid list-none gap-1.5 p-0 text-gray-600">
<li>Vue / React / TypeScript</li>
<li>Node.js / NestJS / Express</li>
<li>MySQL / PostgreSQL / Redis</li>
<li>Three.js / Babylon.js</li>
</ul>
</div>
<div class="grid gap-2">
<div class="font-bold text-gray-900">合作</div>
<ul class="m-0 grid list-none gap-1.5 p-0 text-gray-600">
<li>按需求评估报价</li>
<li>按里程碑验收交付</li>
<li>支持远程协作</li>
<li>可提供维护服务</li>
</ul>
</div>
</div>
<div class="border-t border-gray-200 py-3 text-center text-xs text-gray-400">合作条款 · 交付说明</div>
</footer>
</div>
</template>
<script setup lang="ts">
import { useScroll } from '@vueuse/core'
import { computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { site } from '@/config/site'
const route = useRoute()
const router = useRouter()
const tabs = [
{ id: 'features', label: '能力与服务', path: '/' },
// { id: 'pricing', label: '报价', path: '/' },
{ id: 'cooperation', label: '合作流程', path: '/cooperation' },
{ id: 'markdown', label: '案例/文章', path: '/markdown' },
] as const
const activeId = computed(() => {
if (route.name === 'features') return 'features'
if (route.name === 'cooperation') return 'cooperation'
if (route.name === 'markdown') return 'markdown'
return 'pricing'
})
const handleNav = (path: string) => {
if (route.path !== path) router.push(path)
}
const { y } = useScroll(window, { throttle: 16 })
const isScrolled = computed(() => y.value > 50)
const capsuleWidth = computed(() => (isScrolled.value ? 40 : 100))
</script>

View File

@ -0,0 +1,18 @@
<template>
<button
:class="[
'rounded-full px-5 py-3 text-sm font-semibold transition-[transform,box-shadow,background,color,border-color] duration-200 active:translate-y-px',
variant === 'secondary'
? 'border border-gray-200 bg-white text-slate-900 shadow-[0_8px_18px_rgba(0,0,0,0.08)]'
: 'border border-slate-900 bg-slate-900 text-white shadow-[0_12px_30px_rgba(0,0,0,0.16)]',
]"
>
<slot />
</button>
</template>
<script setup lang="ts">
defineProps<{
variant?: 'primary' | 'secondary'
}>()
</script>

View File

@ -0,0 +1,16 @@
<template>
<div
:class="[
'rounded-[18px] border border-gray-200 bg-white p-[22px] shadow-[0_14px_26px_rgba(0,0,0,0.06)]',
hoverable ? 'transition-[transform,box-shadow,border-color] duration-200 hover:-translate-y-1 hover:border-gray-300 hover:shadow-[0_20px_40px_rgba(0,0,0,0.1)]' : '',
]"
>
<slot />
</div>
</template>
<script setup lang="ts">
defineProps<{
hoverable?: boolean
}>()
</script>

View File

@ -0,0 +1,37 @@
<template>
<div
class="overflow-hidden rounded-xl bg-slate-900 p-3 text-gray-200 shadow-[inset_0_1px_0_rgba(255,255,255,0.05)]"
>
<div class="mb-2 flex items-center justify-between text-[11px] uppercase tracking-[0.08em] text-gray-400">
<span>{{ lang || 'text' }}</span>
<button
class="rounded-full border border-white/10 bg-white/10 px-2.5 py-1 text-[11px] text-gray-50 transition-colors hover:bg-white/15"
@click="handleCopy"
>
{{ copied ? '已复制' : '复制' }}
</button>
</div>
<pre class="m-0 whitespace-pre-wrap font-mono text-[13px] leading-relaxed"><code>{{ code }}</code></pre>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const props = defineProps<{
code: string
lang?: string
}>()
const copied = ref(false)
const handleCopy = async () => {
try {
await navigator.clipboard.writeText(props.code)
copied.value = true
setTimeout(() => (copied.value = false), 1500)
} catch (e) {
console.error(e)
}
}
</script>

View File

@ -0,0 +1,60 @@
<template>
<article
:class="[
'relative grid gap-3 rounded-2xl border border-gray-200 bg-white p-[18px] shadow-[0_14px_32px_rgba(0,0,0,0.08)] transition-[transform,box-shadow,border-color] duration-200 hover:-translate-y-1 hover:border-gray-300 hover:shadow-[0_20px_44px_rgba(0,0,0,0.12)]',
highlight ? 'border-gray-300 shadow-[0_20px_48px_rgba(0,0,0,0.14)]' : '',
]"
>
<div
v-if="badge"
class="absolute right-4 top-3 rounded-full border border-gray-200 bg-gray-100 px-2.5 py-1.5 text-xs text-gray-700"
>
{{ badge }}
</div>
<div class="mt-2 flex items-center gap-2.5 text-gray-600">
<div class="grid h-8 w-8 place-items-center rounded-full border border-gray-200 bg-gray-100 font-bold text-gray-800">
A
</div>
<div class="font-semibold text-slate-900">{{ title }}</div>
<div class="text-gray-400">· {{ subtitle }}</div>
</div>
<div class="text-3xl font-extrabold tracking-[-0.02em]">{{ price }}</div>
<div v-if="note" class="text-gray-500">{{ note }}</div>
<ul class="m-0 grid list-none gap-2 p-0">
<li v-for="feature in features" :key="feature">
<div class="grid grid-cols-[20px_1fr] items-center gap-2 text-sm text-gray-600">
<CheckCircle2 class="h-4 w-4 text-blue-600" />
<span>{{ feature }}</span>
</div>
</li>
</ul>
<a
class="mt-1.5 flex items-center justify-between rounded-xl border border-gray-200 bg-gray-50 p-3 text-gray-600 transition-colors hover:bg-gray-100"
:href="actionHref ?? `mailto:${site.contact.email}`"
>
<div class="font-semibold text-slate-900">{{ actionLabel ?? '咨询合作' }}</div>
<div class="inline-flex items-center gap-1.5 text-xs text-gray-500">
<span>{{ footerNote }}</span>
<ChevronRight class="h-4 w-4 text-gray-400" />
</div>
</a>
</article>
</template>
<script setup lang="ts">
import { CheckCircle2, ChevronRight } from 'lucide-vue-next'
import { site } from '@/config/site'
defineProps<{
title: string
subtitle: string
badge?: string
price: string
note?: string
features: string[]
footerNote: string
highlight?: boolean
actionLabel?: string
actionHref?: string
}>()
</script>

View File

@ -0,0 +1,18 @@
<template>
<div class="grid grid-cols-[32px_1fr] items-start gap-4 max-[640px]:grid-cols-1">
<div
class="grid h-8 w-8 place-items-center rounded-full bg-slate-900 text-[13px] font-bold text-white max-[640px]:h-7 max-[640px]:w-7"
>
{{ step }}
</div>
<div class="grid gap-3 rounded-2xl border border-gray-200 bg-white p-4 shadow-[0_12px_28px_rgba(0,0,0,0.08)]">
<slot />
</div>
</div>
</template>
<script setup lang="ts">
defineProps<{
step: number
}>()
</script>

View File

@ -0,0 +1,111 @@
<template>
<div ref="target" :class="rootClass" :style="rootStyle" />
</template>
<script setup lang="ts">
import TypeIt from 'typeit'
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
export type TypeItTerminalSegment = {
html: string
speed?: number
pauseBefore?: number
pauseAfter?: number
deleteAfter?: boolean
deleteSpeed?: number
}
type Props = {
segments?: TypeItTerminalSegment[]
minLines?: number
lineHeightEm?: number
align?: 'top' | 'center' | 'bottom'
startOnVisible?: boolean
startDelay?: number
cursor?: boolean
cursorChar?: string
loop?: boolean
loopDelay?: number | [number, number]
}
const props = withDefaults(defineProps<Props>(), {
minLines: 2,
lineHeightEm: 1.65,
align: 'top',
startOnVisible: true,
startDelay: 250,
cursor: true,
cursorChar: '\u258c',
loop: true,
loopDelay: () => [900, 1100],
})
const target = ref<HTMLElement | null>(null)
const segments = computed<TypeItTerminalSegment[]>(
() =>
props.segments ?? [
{
html: [
'<div><span class="text-gray-500">$</span> <span class="text-green-600">npm</span> <span class="text-blue-600">run</span> build</div>',
`<div><span class="text-gray-500">${'\u2713'}</span> <span class="text-orange-500">Compiling</span> <span class="text-gray-500">src/main.ts...</span></div>`,
].join(''),
speed: 32,
},
],
)
const rootClass = computed(() => {
const alignClass =
props.align === 'bottom' ? 'justify-end' : props.align === 'center' ? 'justify-center' : 'justify-start'
return ['flex flex-col', alignClass].join(' ')
})
const rootStyle = computed(() => {
if (!props.minLines) return undefined
return {
minHeight: `calc(${props.lineHeightEm}em * ${props.minLines})`,
}
})
let typeit: TypeIt | null = null
onMounted(() => {
if (!target.value) return
target.value.innerHTML = ''
const instance = new TypeIt(target.value, {
startDelay: props.startDelay,
lifeLike: true,
cursor: props.cursor,
cursorChar: props.cursorChar,
html: true,
waitUntilVisible: props.startOnVisible,
loop: props.loop,
loopDelay: props.loopDelay,
})
for (const segment of segments.value) {
if (segment.pauseBefore) {
instance.pause(segment.pauseBefore)
}
instance.type(segment.html, segment.speed ? { speed: segment.speed } : {})
if (segment.pauseAfter) {
instance.pause(segment.pauseAfter)
}
if (segment.deleteAfter) {
instance.delete(undefined, segment.deleteSpeed ? { deleteSpeed: segment.deleteSpeed } : {})
}
}
typeit = instance.go()
})
onBeforeUnmount(() => {
typeit?.destroy()
typeit = null
})
</script>

10
src/config/site.ts Normal file
View File

@ -0,0 +1,10 @@
export const site = {
brand: '子归云工作室',
// subtitle: '前端 · 全栈 · Web3D · 3D建模/渲染 · 官网/后台/小程序',
tagline:
'承接企业官网、后台管理、全栈业务系统、Web3D 互动展示Three.js / Babylon.js与 3D 建模渲染PBR 流程)。支持按需求评估、按里程碑交付、长期维护。',
contact: {
wechat: 'TyTinSx',
email: '1415466602@qq.com',
},
} as const

6
src/main.ts Normal file
View File

@ -0,0 +1,6 @@
import { createApp } from 'vue'
import App from './App.vue'
import { router } from './router'
import './style.css'
createApp(App).use(router).mount('#app')

16
src/router/index.ts Normal file
View File

@ -0,0 +1,16 @@
import { createRouter, createWebHistory } from 'vue-router'
import PricingPage from '@/views/PricingPage.vue'
import FeaturesPage from '@/views/FeaturesPage.vue'
import MarkdownPage from '@/views/MarkdownPage.vue'
import CooperationPage from '@/views/CooperationPage.vue'
const routes = [
{ path: '/', name: 'features', component: FeaturesPage },
{ path: '/markdown', name: 'markdown', component: MarkdownPage },
{ path: '/cooperation', name: 'cooperation', component: CooperationPage },
]
export const router = createRouter({
history: createWebHistory(),
routes,
})

9
src/style.css Normal file
View File

@ -0,0 +1,9 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
body {
@apply font-sans antialiased;
}
}

View File

@ -0,0 +1,116 @@
<template>
<section class="grid gap-6 pt-3 pb-10">
<header class="grid gap-2.5">
<h1 class="m-0 text-[28px] font-extrabold tracking-[-0.01em]">合作流程</h1>
<p class="m-0 text-gray-600">灵活的合作模式满足不同项目需求</p>
</header>
<div class="grid gap-4">
<div class="flex flex-wrap gap-2">
<button
v-for="file in mdFiles"
:key="file"
:class="[
'rounded-full px-4 py-2 text-sm font-semibold transition-colors',
currentFile === file ? 'bg-slate-900 text-white' : 'border border-gray-200 bg-white text-gray-600 hover:bg-gray-50',
]"
@click="loadFile(file)"
>
{{ file.replace('.md', '') }}
</button>
</div>
<div
v-if="htmlContent"
ref="contentRef"
class="prose prose-slate max-w-none rounded-2xl border border-gray-200 bg-white p-6 shadow-[0_14px_26px_rgba(0,0,0,0.06)]"
v-html="htmlContent"
/>
</div>
</section>
</template>
<script setup lang="ts">
import { marked } from 'marked'
import { nextTick, onMounted, ref, watch } from 'vue'
const BASE_URL = import.meta.env.BASE_URL
// 合作流程文件列表
const cooperationFiles = ['按项目.md', '按天.md']
const mdFiles = ref<string[]>([])
const currentFile = ref('')
const htmlContent = ref('')
const contentRef = ref<HTMLElement>()
onMounted(async () => {
mdFiles.value = cooperationFiles
if (mdFiles.value.length > 0) {
await loadFile(mdFiles.value[0])
}
})
const addCopyButtons = () => {
if (!contentRef.value) return
contentRef.value.querySelectorAll('pre').forEach((pre) => {
if (pre.querySelector('.copy-btn')) return
const button = document.createElement('button')
button.className = 'copy-btn'
button.textContent = '复制'
button.onclick = async () => {
const code = pre.querySelector('code')
const text = code?.textContent || ''
try {
await navigator.clipboard.writeText(text)
} catch {
const textarea = document.createElement('textarea')
textarea.value = text
textarea.style.cssText = 'position:fixed;opacity:0'
document.body.appendChild(textarea)
textarea.select()
document.execCommand('copy')
document.body.removeChild(textarea)
}
button.textContent = '已复制'
setTimeout(() => (button.textContent = '复制'), 1500)
}
pre.style.position = 'relative'
pre.appendChild(button)
})
}
watch(htmlContent, () => nextTick(addCopyButtons))
const loadFile = async (filename: string) => {
currentFile.value = filename
try {
const res = await fetch(`${BASE_URL}docs/${filename}`)
if (!res.ok) return
const content = await res.text()
htmlContent.value = marked(content) as string
} catch (e) {
console.error('加载文档失败:', e)
}
}
</script>
<style>
.prose h1 { font-size: 1.875rem; font-weight: 800; margin-bottom: 1rem; }
.prose h2 { font-size: 1.5rem; font-weight: 700; margin-top: 1.5rem; margin-bottom: 0.75rem; }
.prose h3 { font-size: 1.25rem; font-weight: 600; margin-top: 1.25rem; margin-bottom: 0.5rem; }
.prose p { margin-bottom: 1rem; line-height: 1.7; color: #4b5563; }
.prose ul, .prose ol { margin-bottom: 1rem; padding-left: 1.5rem; }
.prose li { margin-bottom: 0.25rem; color: #4b5563; }
.prose code { background: #f1f5f9; padding: 0.125rem 0.375rem; border-radius: 0.25rem; font-size: 0.875rem; }
.prose pre { background: #1e293b; color: #e2e8f0; padding: 1rem; padding-top: 2.5rem; border-radius: 0.75rem; overflow-x: auto; margin-bottom: 1rem; position: relative; }
.prose pre::before { content: ''; position: absolute; top: 12px; left: 12px; width: 12px; height: 12px; border-radius: 50%; background: #ff5f56; box-shadow: 20px 0 0 #ffbd2e, 40px 0 0 #27c93f; }
.prose pre code { background: transparent; padding: 0; }
.prose a { color: #2563eb; text-decoration: underline; }
.prose blockquote { border-left: 4px solid #e5e7eb; padding-left: 1rem; color: #6b7280; font-style: italic; }
.prose table { width: 100%; border-collapse: collapse; margin-bottom: 1rem; }
.prose th, .prose td { border: 1px solid #e5e7eb; padding: 0.5rem 1rem; text-align: left; }
.prose th { background: #f9fafb; font-weight: 600; }
.copy-btn { position: absolute; top: 0.5rem; right: 0.5rem; padding: 0.25rem 0.75rem; font-size: 0.75rem; background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.2); border-radius: 9999px; color: #e2e8f0; cursor: pointer; transition: background 0.2s; }
.copy-btn:hover { background: rgba(255,255,255,0.2); }
</style>

341
src/views/FeaturesPage.vue Normal file
View File

@ -0,0 +1,341 @@
<template>
<section class="grid gap-10 pt-3 pb-12">
<header class="grid gap-3 text-center">
<div class="text-[13px] text-gray-500">能力与服务 / 交付型合作 / 可长期维护</div>
<h1 class="m-0 text-[clamp(28px,4vw,44px)] font-extrabold tracking-[-0.02em]">把需求做成可交付的产品</h1>
<p class="m-0 mx-auto max-w-[680px] leading-[1.7] text-gray-600">
我能做前端全栈Web3DBabylon.js / Three.js3D 建模/渲染PBR官网与后台管理小程序开发等你给目标和素材我负责方案实现联调上线
</p>
<div class="inline-flex justify-center gap-3 max-[640px]:w-full max-[640px]:justify-center">
<Button @click="router.push('/cooperation')">查看报价</Button>
<Button variant="secondary" @click="router.push('/cooperation')">合作流程</Button>
</div>
</header>
<div class="grid gap-[18px]">
<div class="mt-2 text-center text-[22px] font-bold">覆盖从设计对接到上线交付的全流程</div>
<div class="grid grid-cols-3 gap-5 max-[1024px]:grid-cols-2 max-[640px]:grid-cols-1">
<Card v-for="(feature, index) in features" :key="feature.title" hoverable class="grid content-start gap-3">
<div
class="-mt-0.5 -ml-0.5 inline-flex h-12 w-12 items-center justify-center rounded-[14px] border border-gray-200 bg-gray-50 shadow-[0_8px_18px_rgba(0,0,0,0.08)]"
>
<component :is="feature.icon" class="h-6 w-6 text-gray-700" />
</div>
<h3 class="mt-2 text-[17px] font-bold">{{ feature.title }}</h3>
<p class="m-0 text-sm leading-relaxed text-gray-600">{{ feature.description }}</p>
<div
class="mt-3 grid gap-2 rounded-2xl border border-slate-100 bg-slate-50 px-3 py-2.5 shadow-[inset_0_1px_0_rgba(255,255,255,0.6),0_10px_24px_rgba(0,0,0,0.05)]"
>
<div class="flex gap-1.5">
<span class="h-2.5 w-2.5 rounded-full bg-red-400 shadow-[0_0_0_1px_rgba(0,0,0,0.06)]"></span>
<span class="h-2.5 w-2.5 rounded-full bg-amber-400 shadow-[0_0_0_1px_rgba(0,0,0,0.06)]"></span>
<span class="h-2.5 w-2.5 rounded-full bg-emerald-400 shadow-[0_0_0_1px_rgba(0,0,0,0.06)]"></span>
</div>
<TypeItTerminal
:segments="featureTerminalScripts[index]"
:align="featureTerminalAlign[index]"
:minLines="featureTerminalMinLines[index]"
class="rounded-xl bg-white/80 px-3.5 py-2.5 font-mono text-[13px] leading-relaxed whitespace-pre-wrap text-slate-900 shadow-[inset_0_1px_2px_rgba(0,0,0,0.05)]"
/>
</div>
</Card>
</div>
</div>
<div class="grid gap-[18px]">
<div class="text-center font-semibold text-gray-700">适合的项目类型</div>
<div class="grid grid-cols-2 gap-5 max-[1024px]:grid-cols-1">
<Card v-for="item in scenarios" :key="item" hoverable class="grid grid-cols-[32px_1fr] items-center gap-3">
<div
class="grid h-7 w-7 place-items-center rounded-[10px] border border-gray-200 bg-gray-50 text-base text-gray-700 shadow-[inset_0_1px_0_rgba(255,255,255,0.8),0_6px_12px_rgba(0,0,0,0.08)]"
>
</div>
<p class="m-0 leading-relaxed text-gray-600">{{ item }}</p>
</Card>
</div>
</div>
<div class="grid gap-[18px]">
<div class="text-center font-semibold text-gray-700">合作优势</div>
<div class="grid grid-cols-3 gap-5 max-[1024px]:grid-cols-2 max-[640px]:grid-cols-1">
<Card v-for="stat in stats" :key="stat.value" class="text-center">
<div class="text-[38px] font-extrabold tracking-[-0.02em]">{{ stat.value }}</div>
<div class="mt-1.5 text-sm text-gray-600">{{ stat.label }}</div>
</Card>
</div>
</div>
<Card hoverable class="text-center">
<div class="text-xl font-bold">想把需求尽快落地</div>
<p class="m-0 text-gray-500">把你的目标参考链接时间节点发我我会给出方案与排期建议</p>
<div class="mt-4 inline-flex gap-3 max-[640px]:w-full max-[640px]:justify-center">
<Button @click="router.push('/cooperation')">获取报价</Button>
<Button variant="secondary" @click="router.push('/markdown')">查看案例</Button>
</div>
</Card>
</section>
</template>
<script setup lang="ts">
import { useRouter } from 'vue-router'
import {
Boxes,
Cloud,
Code2,
Cpu,
Cuboid,
LayoutTemplate,
Layers,
Sparkles,
ShieldCheck,
} from 'lucide-vue-next'
import Button from '@/components/Button/index.vue'
import Card from '@/components/Card/index.vue'
import TypeItTerminal, { type TypeItTerminalSegment } from '@/components/TypeItTerminal.vue'
const router = useRouter()
type Feature = {
title: string
description: string
icon: any
}
const features: Feature[] = [
{ title: '前端工程化', description: 'Vue/React + TypeScript + 构建与规范,让项目可维护、可迭代。', icon: Code2 },
{ title: '后台管理', description: '权限、表格、图表、导入导出与业务流程,按习惯做出“顺手”的后台。', icon: LayoutTemplate },
{ title: '全栈开发', description: '接口设计、鉴权、数据库建模与联调,打通前后端的交付链路。', icon: Layers },
{ title: 'Web3D', description: 'Three.js/Babylon.js 场景搭建、交互、性能优化与业务 UI 联动。', icon: Cuboid },
{ title: '官网/落地页', description: '品牌展示 + 动效 + SEO 基础,支持多端适配与上线部署。', icon: Sparkles },
{ title: '小程序开发', description: '微信小程序/uni-app按需从页面到接口联调与发布。', icon: Boxes },
{ title: '性能优化', description: '首屏、图片、缓存、包体与渲染优化,提升体验与转化。', icon: Cpu },
{ title: '部署上线', description: 'Docker/服务器部署(按需),支持环境隔离与灰度发布方案。', icon: Cloud },
{ title: '质量与安全', description: '边界处理、日志与监控建议,交付稳定可靠的线上版本。', icon: ShieldCheck },
]
const featureTerminalScripts: TypeItTerminalSegment[][] = [
[
{
html: [
'<div><span class="text-gray-500">$</span> pnpm create <span class="text-emerald-600">vite</span> my-app</div>',
'<div><span class="text-gray-500">$</span> pnpm i <span class="text-amber-600">eslint</span> <span class="text-amber-600">prettier</span></div>',
'<div><span class="text-gray-500">✓</span> <span class="text-emerald-600">tsconfig</span> + <span class="text-emerald-600">lint</span> ready</div>',
].join(''),
speed: 28,
pauseAfter: 900,
deleteAfter: true,
deleteSpeed: 20,
},
{
html: [
'<div><span class="text-blue-600">export</span> <span class="text-blue-600">type</span> Use r = { id: <span class="text-blue-600">string</span>; name: <span class="text-blue-600">string</span> }</div>',
'<div><span class="text-blue-600">const</span> state = reactive&lt;{ user?: User }&gt;({})</div>',
].join(''),
speed: 18,
pauseAfter: 1200,
},
],
[
{
html: [
'<div><span class="text-gray-500">roles</span> → <span class="text-gray-500">menus</span> → <span class="text-gray-500">actions</span></div>',
'<div><span class="text-gray-500">✓</span> route guard + permission check</div>',
].join(''),
speed: 22,
pauseAfter: 900,
deleteAfter: true,
deleteSpeed: 18,
},
{
html: [
'<div><span class="text-slate-900">columns</span> = [<span class="text-amber-600">"ID"</span>, <span class="text-amber-600">"Name"</span>, <span class="text-amber-600">"Status"</span>]</div>',
'<div><span class="text-gray-500">✓</span> filter · export · batch action</div>',
].join(''),
speed: 26,
pauseAfter: 1200,
},
],
[
{
html: [
'<div><span class="text-blue-600">@Get</span>(<span class="text-amber-600">"/users"</span>)</div>',
'<div><span class="text-blue-600">async</span> list(@Query() q) { <span class="text-blue-600">return</span> svc.list(q) }</div>',
].join(''),
speed: 18,
pauseAfter: 900,
deleteAfter: true,
deleteSpeed: 16,
},
{
html: [
'<div><span class="text-gray-500">POST /auth/login</span> → <span class="text-emerald-600">JWT</span></div>',
'<div><span class="text-gray-500">DB</span>: users · roles · sessions</div>',
].join(''),
speed: 22,
pauseAfter: 1200,
},
],
[
{
html: [
'<div><span class="text-blue-600">import</span> { Engine, Scene } <span class="text-blue-600">from</span> <span class="text-amber-600">"@babylonjs/core"</span></div>',
'<div><span class="text-blue-600">const</span> engine = <span class="text-blue-600">new</span> Engine(canvas)</div>',
'<div><span class="text-blue-600">const</span> scene = <span class="text-blue-600">new</span> Scene(engine)</div>',
].join(''),
speed: 20,
pauseAfter: 900,
deleteAfter: true,
deleteSpeed: 18,
},
{
html: [
'<div><span class="text-gray-500">glTF</span> + <span class="text-gray-500">DRACO</span> + <span class="text-gray-500">LOD</span></div>',
'<div><span class="text-gray-500">✓</span> interaction · hotspot · UI sync</div>',
].join(''),
speed: 24,
pauseAfter: 1200,
},
],
[
{
html: [
'<div><span class="text-gray-500">&lt;title&gt;</span>品牌官网<span class="text-gray-500">&lt;/title&gt;</span></div>',
'<div><span class="text-gray-500">&lt;meta</span> name=<span class="text-amber-600">"description"</span> ... <span class="text-gray-500">/&gt;</span></div>',
].join(''),
speed: 26,
pauseAfter: 900,
deleteAfter: true,
deleteSpeed: 20,
},
{
html: [
'<div><span class="text-gray-500">hero</span> · <span class="text-gray-500">features</span> · <span class="text-gray-500">pricing</span> · <span class="text-gray-500">contact</span></div>',
'<div><span class="text-gray-500">✓</span> responsive · animation · deploy</div>',
].join(''),
speed: 18,
pauseAfter: 1200,
},
],
[
{
html: [
'<div><span class="text-blue-600">Page</span>({ data })</div>',
'<div>onLoad(() =&gt; fetchList())</div>',
'<div><span class="text-gray-500">✓</span> tab · list · detail</div>',
].join(''),
speed: 22,
pauseAfter: 900,
deleteAfter: true,
deleteSpeed: 18,
},
{
html: [
'<div><span class="text-gray-500">subpackages</span> · <span class="text-gray-500">request</span> · <span class="text-gray-500">upload</span></div>',
'<div><span class="text-gray-500">✓</span> publish checklist</div>',
].join(''),
speed: 18,
pauseAfter: 1200,
},
],
[
{
html: [
'<div><span class="text-gray-500">Lighthouse</span>: <span class="text-emerald-600">95+</span></div>',
'<div><span class="text-gray-500">-</span> image optimize (webp/avif)</div>',
'<div><span class="text-gray-500">-</span> code split + cache</div>',
].join(''),
speed: 16,
pauseAfter: 900,
deleteAfter: true,
deleteSpeed: 14,
},
{
html: [
'<div><span class="text-gray-500">TTFB</span> ↓ · <span class="text-gray-500">LCP</span> ↓ · <span class="text-gray-500">CLS</span> ↓</div>',
'<div><span class="text-gray-500">✓</span> perf budget applied</div>',
].join(''),
speed: 20,
pauseAfter: 1200,
},
],
[
{
html: [
'<div><span class="text-purple-600">services</span>:</div>',
'<div>&nbsp;&nbsp;<span class="text-slate-900">web</span>: <span class="text-amber-600">nginx</span></div>',
'<div>&nbsp;&nbsp;<span class="text-slate-900">api</span>: <span class="text-amber-600">node</span></div>',
'<div>&nbsp;&nbsp;<span class="text-slate-900">db</span>: <span class="text-amber-600">postgres</span></div>',
].join(''),
speed: 18,
pauseAfter: 900,
deleteAfter: true,
deleteSpeed: 16,
},
{
html: [
'<div><span class="text-gray-500">env</span>: dev · staging · prod</div>',
'<div><span class="text-gray-500">✓</span> deploy + rollback plan</div>',
].join(''),
speed: 22,
pauseAfter: 1200,
},
],
[
{
html: [
'<div><span class="text-gray-500">fix</span>: edge case for empty list</div>',
'<div><span class="text-gray-500">test</span>: add regression</div>',
'<div><span class="text-gray-500">✓</span> release notes ready</div>',
].join(''),
speed: 20,
pauseAfter: 900,
deleteAfter: true,
deleteSpeed: 16,
},
{
html: [
'<div><span class="text-gray-500">v1.2.0</span> · new feature</div>',
'<div><span class="text-gray-500">v1.2.1</span> · hotfix</div>',
].join(''),
speed: 16,
pauseAfter: 1200,
},
],
]
const featureTerminalAlign: Array<'top' | 'center' | 'bottom'> = [
'bottom',
'top',
'bottom',
'top',
'bottom',
'top',
'bottom',
'top',
'center',
]
const featureTerminalMinLines = [4, 3, 3, 4, 3, 3, 4, 4, 3]
const scenarios = [
'品牌官网/产品官网:高还原 + 动效 + SEO 基础',
'运营活动页:周期紧、改动频繁、注重转化',
'后台管理:权限/表格/导入导出/图表看板',
'全栈业务:接口 + 数据库 + 管理后台一体化交付',
'Web3D 展示:模型加载、交互热点、镜头与动效',
'3D 建模/渲染建模、材质、灯光与出片PBR 流程)',
'小程序:列表/详情/下单/登录/授权等常见场景',
'存量项目:重构、性能优化、修 Bug、继续迭代',
'上线交付:部署方案、环境配置、监控建议(按需)',
]
const stats = [
{ value: '快', label: '沟通明确后快速推进' },
{ value: '稳', label: '注重边界与线上稳定' },
{ value: '清晰', label: '按里程碑可验收交付' },
{ value: '可维护', label: '工程化与可读结构' },
{ value: '可扩展', label: '考虑后续迭代成本' },
{ value: '可协作', label: '对接设计/后端/运营' },
]
</script>

118
src/views/MarkdownPage.vue Normal file
View File

@ -0,0 +1,118 @@
<template>
<section class="grid gap-6 pt-3 pb-10">
<header class="grid gap-2.5">
<h1 class="m-0 text-[28px] font-extrabold tracking-[-0.01em]">案例 / 文章</h1>
<p class="m-0 text-gray-600"> Markdown 维护你的案例服务说明与技术文章支持代码复制</p>
</header>
<div class="grid gap-4">
<div class="flex flex-wrap gap-2">
<button
v-for="file in mdFiles"
:key="file"
:class="[
'rounded-full px-4 py-2 text-sm font-semibold transition-colors',
currentFile === file ? 'bg-slate-900 text-white' : 'border border-gray-200 bg-white text-gray-600 hover:bg-gray-50',
]"
@click="loadFile(file)"
>
{{ file.replace('.md', '') }}
</button>
</div>
<div
v-if="htmlContent"
ref="contentRef"
class="prose prose-slate max-w-none rounded-2xl border border-gray-200 bg-white p-6 shadow-[0_14px_26px_rgba(0,0,0,0.06)]"
v-html="htmlContent"
/>
<div v-else class="rounded-2xl border border-gray-200 bg-gray-50 p-6 text-center text-gray-500">
请选择一个文档查看
</div>
</div>
</section>
</template>
<script setup lang="ts">
import { marked } from 'marked'
import { nextTick, onMounted, ref, watch } from 'vue'
const BASE_URL = import.meta.env.BASE_URL
// 案例文件列表,后期可在此添加新文件
const caseFiles = ['web3d.md', '建模与渲染.md', 'services.md', 'case-template.md', 'contact.md']
const mdFiles = ref<string[]>([])
const currentFile = ref('')
const htmlContent = ref('')
const contentRef = ref<HTMLElement>()
onMounted(async () => {
mdFiles.value = caseFiles
if (mdFiles.value.length > 0) {
await loadFile(mdFiles.value[0])
}
})
const addCopyButtons = () => {
if (!contentRef.value) return
contentRef.value.querySelectorAll('pre').forEach((pre) => {
if (pre.querySelector('.copy-btn')) return
const button = document.createElement('button')
button.className = 'copy-btn'
button.textContent = '复制'
button.onclick = async () => {
const code = pre.querySelector('code')
const text = code?.textContent || ''
try {
await navigator.clipboard.writeText(text)
} catch {
const textarea = document.createElement('textarea')
textarea.value = text
textarea.style.cssText = 'position:fixed;opacity:0'
document.body.appendChild(textarea)
textarea.select()
document.execCommand('copy')
document.body.removeChild(textarea)
}
button.textContent = '已复制'
setTimeout(() => (button.textContent = '复制'), 1500)
}
pre.style.position = 'relative'
pre.appendChild(button)
})
}
watch(htmlContent, () => nextTick(addCopyButtons))
const loadFile = async (filename: string) => {
currentFile.value = filename
try {
const res = await fetch(`${BASE_URL}docs/${filename}`)
if (!res.ok) return
const content = await res.text()
htmlContent.value = marked(content) as string
} catch (e) {
console.error('加载文档失败:', e)
}
}
</script>
<style>
.prose h1 { font-size: 1.875rem; font-weight: 800; margin-bottom: 1rem; }
.prose h2 { font-size: 1.5rem; font-weight: 700; margin-top: 1.5rem; margin-bottom: 0.75rem; }
.prose h3 { font-size: 1.25rem; font-weight: 600; margin-top: 1.25rem; margin-bottom: 0.5rem; }
.prose p { margin-bottom: 1rem; line-height: 1.7; color: #4b5563; }
.prose ul, .prose ol { margin-bottom: 1rem; padding-left: 1.5rem; }
.prose li { margin-bottom: 0.25rem; color: #4b5563; }
.prose code { background: #f1f5f9; padding: 0.125rem 0.375rem; border-radius: 0.25rem; font-size: 0.875rem; }
.prose pre { background: #1e293b; color: #e2e8f0; padding: 1rem; padding-top: 2.5rem; border-radius: 0.75rem; overflow-x: auto; margin-bottom: 1rem; position: relative; }
.prose pre::before { content: ''; position: absolute; top: 12px; left: 12px; width: 12px; height: 12px; border-radius: 50%; background: #ff5f56; box-shadow: 20px 0 0 #ffbd2e, 40px 0 0 #27c93f; }
.prose pre code { background: transparent; padding: 0; }
.prose a { color: #2563eb; text-decoration: underline; }
.prose blockquote { border-left: 4px solid #e5e7eb; padding-left: 1rem; color: #6b7280; font-style: italic; }
.prose img { max-width: 100%; border-radius: 0.75rem; margin: 1rem 0; box-shadow: 0 4px 12px rgba(0,0,0,0.1); }
.prose hr { border: none; border-top: 1px solid #e5e7eb; margin: 2rem 0; }
.copy-btn { position: absolute; top: 0.5rem; right: 0.5rem; padding: 0.25rem 0.75rem; font-size: 0.75rem; background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.2); border-radius: 9999px; color: #e2e8f0; cursor: pointer; transition: background 0.2s; }
.copy-btn:hover { background: rgba(255,255,255,0.2); }
</style>

127
src/views/PricingPage.vue Normal file
View File

@ -0,0 +1,127 @@
<template>
<section class="grid gap-10 pt-3 pb-12">
<div class="grid gap-2 text-center">
<div class="inline-flex flex-wrap justify-center gap-3.5 text-xs text-gray-500">
<span>前端开发</span>
<span>全栈开发</span>
<span>后台管理</span>
<span>Web3DThree/Babylon</span>
<span>3D建模/渲染PBR</span>
<span>小程序</span>
</div>
<div class="text-[11px] tracking-[0.3em] text-gray-400">SERVICES</div>
<h1 class="m-0 text-[clamp(26px,4vw,36px)] font-extrabold tracking-[-0.02em]">服务与报价</h1>
<p class="m-0 mx-auto max-w-[680px] leading-relaxed text-gray-500">
按需求评估按里程碑交付可从 0 1 搭建也可接手存量项目重构/优化/继续迭代
</p>
</div>
<div class="grid gap-4">
<h2 class="m-0 text-lg font-bold">常见合作套餐</h2>
<div class="grid grid-cols-3 gap-[18px] max-[1024px]:grid-cols-2 max-[640px]:grid-cols-1">
<PlanCard v-for="plan in projectPlans" :key="plan.subtitle" v-bind="plan" />
</div>
</div>
<div class="grid gap-4">
<h2 class="m-0 text-lg font-bold">长期合作</h2>
<div class="grid grid-cols-2 gap-[18px] max-[1024px]:grid-cols-1">
<PlanCard v-for="plan in retainerPlans" :key="plan.subtitle" v-bind="plan" />
</div>
<div class="grid grid-cols-1 gap-[18px]">
<PlanCard v-bind="customPlan" />
</div>
</div>
</section>
</template>
<script setup lang="ts">
import PlanCard from '@/components/PlanCard/index.vue'
type Plan = {
title: string
subtitle: string
badge?: string
price: string
note?: string
features: string[]
footerNote: string
highlight?: boolean
}
const projectPlans: Plan[] = [
{
title: '企业官网 / 落地页',
subtitle: '品牌展示 · SEO 基础',
badge: '快速交付',
price: '¥ 按需求报价',
note: '适合活动页、产品官网、品牌介绍、营销落地。',
features: ['响应式适配 · 兼容主流浏览器', '高还原 UI + 动效', 'SEO/OG/站点地图(按需)', '表单/埋点/转化链路(按需)'],
footerNote: '交付:源码 + 部署说明',
},
{
title: '后台管理系统',
subtitle: '权限 · 表格 · 图表',
badge: '业务提效',
price: '¥ 按需求报价',
note: '适合数据看板、运营后台、CRM/ERP 子系统。',
features: ['账号/角色/权限模型', '列表/筛选/导出/批处理', '接口联调 + 错误处理', '可维护的组件与路由结构'],
footerNote: '交付:可部署版本 + 使用说明',
highlight: true,
},
{
title: 'Web3D 互动展示',
subtitle: 'Three.js / Babylon.js',
badge: '更强体验',
price: '¥ 按需求报价',
note: '适合产品展示、展厅导览、互动营销、3D 组件开发。',
features: ['模型加载/压缩glTF/DRACO', '交互与镜头控制', '性能优化LOD/合批/纹理)', '与页面业务联动UI/数据)'],
footerNote: '交付:可复用组件 + 性能建议',
},
{
title: '3D建模与渲染',
subtitle: '建模 · 材质 · PBR · 出片',
badge: '可出视频',
price: '¥ 按需求报价',
note: '适合产品/角色/场景资产制作,支持从建模到渲染出片(静帧/视频)。',
features: [
'高模/低模建模 + 拓扑优化(按需)',
'UV 展开与贴图烘焙Normal/AO 等)',
'PBR 材质流程Substance/Blender 等)',
'灯光/渲染/合成与视频输出(按需)',
],
footerNote: '交付:源文件 + 贴图 + 成片',
},
]
const retainerPlans: Plan[] = [
{
title: '长期迭代(按月)',
subtitle: '稳定排期 · 迭代交付',
badge: '长期合作',
price: '¥ 按月报价',
note: '适合长期运营产品、版本迭代、活动频繁的团队。',
features: ['每周同步进度与风险', '按里程碑验收与交付', '代码规范/CI/质量把控', '线上问题响应(按约定)'],
footerNote: '方式:远程协作 · 里程碑结算',
},
{
title: '全栈开发(按需求)',
subtitle: '接口 · 数据库 · 部署',
badge: '从 0 到 1',
price: '¥ 按需求报价',
note: '适合需要后端接口、数据建模、部署上线的一体化项目。',
features: ['API 设计与鉴权JWT/OAuth', '数据库建模与迁移', '日志/监控/告警(按需)', 'Docker/服务器部署(按需)'],
footerNote: '交付:接口文档 + 部署脚本(按需)',
},
]
const customPlan: Plan = {
title: '定制合作',
subtitle: '按需求评估报价',
badge: '欢迎咨询',
price: '¥ 面议',
note: '把你的需求发我:目标、时间、参考风格、已有资料/接口。我会给出排期与报价建议。',
features: ['支持接手存量项目:重构/性能优化/修 Bug', '支持小程序开发:微信小程序/uni-app按需', '支持设计对接Figma/蓝湖/Sketch', '可签 NDA/合同与发票(按需)'],
footerNote: '微信/邮箱咨询 · 先评估再开工',
}
</script>

16
tailwind.config.js Normal file
View File

@ -0,0 +1,16 @@
/** @type {import('tailwindcss').Config} */
export default {
content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
theme: {
extend: {
fontFamily: {
sans: ['Inter', '-apple-system', 'BlinkMacSystemFont', '"Segoe UI"', 'Roboto', 'sans-serif'],
mono: ['"SFMono-Regular"', 'Consolas', '"Liberation Mono"', 'Menlo', 'monospace'],
},
boxShadow: {
subtle: '0 10px 30px rgba(0,0,0,0.04)',
},
},
},
plugins: [],
}

38
tsconfig.json Normal file
View File

@ -0,0 +1,38 @@
{
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"module": "ESNext",
"moduleResolution": "Node",
"strict": true,
"skipLibCheck": true,
"jsx": "preserve",
"resolveJsonModule": true,
"isolatedModules": true,
"esModuleInterop": true,
"lib": [
"ESNext",
"DOM"
],
"types": [
"vite/client"
],
"baseUrl": ".",
"paths": {
"@/*": [
"./src/*"
]
}
},
"include": [
"src/**/*.ts",
"src/**/*.d.ts",
"src/**/*.tsx",
"src/**/*.vue"
],
"references": [
{
"path": "./tsconfig.node.json"
}
]
}

12
tsconfig.node.json Normal file
View File

@ -0,0 +1,12 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "Node",
"allowSyntheticDefaultImports": true
},
"include": [
"vite.config.ts"
]
}

47
vite.config.ts Normal file
View File

@ -0,0 +1,47 @@
import { defineConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd());
return {
base: env.VITE_PUBLIC || './', //使用环境变量设置资源路径,默认为相对路径
server: {
host: true,
port: 8080, //vite项目启动时自定义端口
open: true,
proxy: {
// 正则表达式写法
'^/api': {
target: 'http://192.168.3.13:3000/api', // 后端服务实际地址
changeOrigin: true, //开启代理
rewrite: (path) => path.replace(/^\/api/, '')
}
}
},
plugins: [
vue(),
],
resolve: {
//别名
alias: {
'@': resolve(__dirname, './src'),
components: resolve(__dirname, './src/components'),
script: resolve(__dirname, './src/script'),
utils: resolve(__dirname, './src/utils'),
stores: resolve(__dirname, './src/stores')
}
},
build: {
assetsDir: 'static', //打包后的公共文件夹名
target: 'es2015',
cssTarget: ['chrome61'],
chunkSizeWarningLimit: 5000
}
}
})