Compare commits
10 Commits
2eb1e392d8
...
fb67355a15
| Author | SHA1 | Date | |
|---|---|---|---|
| fb67355a15 | |||
|
|
cc0cb01d28 | ||
|
|
ccfd268682 | ||
|
|
40b3ee147f | ||
|
|
8cc04b7f59 | ||
|
|
31a4103f9b | ||
|
|
78bc2c34a0 | ||
|
|
87731a6379 | ||
|
|
113ebabb94 | ||
|
|
5c8baf14c3 |
1
.env.example
Normal file
@@ -0,0 +1 @@
|
|||||||
|
NUXT_PUBLIC_WHATSAPP_NUMBER=+60123456789
|
||||||
8
.vscode/settings.json
vendored
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"i18n-ally.localesPaths": ["i18n/locales"],
|
"i18n-ally.localesPaths": ["i18n/locales"],
|
||||||
"i18n-ally.enabledFrameworks": [
|
"i18n-ally.enabledFrameworks": ["vue"],
|
||||||
"vue"
|
"i18n-ally.sourceLanguage": "en-US",
|
||||||
],
|
"i18n-ally.displayLanguage": "en-US",
|
||||||
"i18n-ally.keystyle": "nested"
|
"i18n-ally.keystyle": "nested"
|
||||||
}
|
}
|
||||||
|
|||||||
84
AGENTS.md
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
当然可以,下面是完整的中文翻译版,保持了原文的结构与语气,适合直接放在项目的 `AGENTS.zh-CN.md` 中使用👇
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 仓库指南(Repository Guidelines)
|
||||||
|
|
||||||
|
本仓库托管了 **Tootaio Studio** 网站项目,基于 **Nuxt 4**、`@nuxt/content`、`@nuxt/ui` 和 i18n 构建。
|
||||||
|
项目使用 **pnpm** 进行全部工作流程。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 项目结构与模块组织
|
||||||
|
|
||||||
|
* `app/` — Nuxt 应用源码目录,包含:`pages/`、`layouts/`、`assets/`、`composables/`、`utils/` 等(例如:`app/pages/index.vue`)。
|
||||||
|
* `content/` — 基于 YAML 的多语言内容文件(如:`content/en-US`、`content/zh-CN`)。内容结构定义在 `content.config.ts` 中。
|
||||||
|
* `i18n/` — UI 翻译文件(JSON 格式),位于 `i18n/locales/<locale>/`。
|
||||||
|
* `public/` — 静态资源目录,内容将原样提供给客户端。
|
||||||
|
* 根级配置文件包括:`nuxt.config.ts`、`eslint.config.mjs`、`pnpm-workspace.yaml`。项目文档存放于 `docs/`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 构建、测试与开发命令
|
||||||
|
|
||||||
|
* **安装依赖**:`pnpm i`(会自动执行 `nuxt prepare`)
|
||||||
|
* **开发服务器**:`pnpm dev` — 启动后访问 [http://localhost:3000](http://localhost:3000),支持 HMR。
|
||||||
|
* **生产构建**:`pnpm build` — 生成 `.output/` 目录。
|
||||||
|
* **预览构建**:`pnpm preview` — 以生产模式运行构建产物。
|
||||||
|
* **静态生成**:`pnpm generate` — 执行 SSG(静态站点生成)。
|
||||||
|
* **代码检查**:`pnpm exec eslint .`(手动执行时使用;CI 流程中会自动运行)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 代码风格与命名规范
|
||||||
|
|
||||||
|
* 使用 **TypeScript** 与 Vue 3 `<script setup lang="ts">`;配置文件采用 **ESM 模块** 格式。
|
||||||
|
* 缩进统一为 **2 空格**;保持与现有代码库的引号与风格一致。
|
||||||
|
* Vue 单文件组件(SFC)位于 `pages/` 下,文件名简洁(例如:`webDev.vue`);
|
||||||
|
自定义组合式函数(composables)使用 **PascalCase**(如:`LocalizedCollection.ts`)。
|
||||||
|
* ESLint 通过 `@nuxt/eslint` 配置,依赖编辑器集成与 CI/构建阶段反馈。
|
||||||
|
* i18n 多语言 key 使用 **点号嵌套命名法**,例如:`$t('index.featuredProjects.viewDemo')`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 测试指南
|
||||||
|
|
||||||
|
* 当前尚未配置自动化测试。
|
||||||
|
推荐在未来使用 **Vitest** 进行单元测试,**Playwright** 进行端到端(e2e)测试。
|
||||||
|
* 在此之前,请进行本地验证:
|
||||||
|
|
||||||
|
* 运行 `pnpm dev`;
|
||||||
|
* 切换多语言;
|
||||||
|
* 检查 `content/*.yml` 是否符合 `content.config.ts` 中的 schema;
|
||||||
|
* 确认 `pnpm build` 与 `pnpm preview` 能成功执行。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 提交与 Pull Request 规范
|
||||||
|
|
||||||
|
* 遵循 **Conventional Commits** 规范,并带有 scope(可参考提交历史):
|
||||||
|
|
||||||
|
* 示例:`feat(content): …`、`refactor(ui): …`、`feat(pages): …`
|
||||||
|
* 分支命名格式:
|
||||||
|
|
||||||
|
* `feat/<简短描述>`、`fix/<简短描述>`
|
||||||
|
* Pull Request 要包含以下信息:
|
||||||
|
|
||||||
|
* **改动目的**
|
||||||
|
* **关联的 Issue(如有)**
|
||||||
|
* **UI 改动截图**
|
||||||
|
* **测试步骤**
|
||||||
|
* 若涉及内容文件,请确保 **中英文版本同步更新**。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 内容与多语言(i18n)建议
|
||||||
|
|
||||||
|
* 在 `content/en-US/` 与 `content/zh-CN/` 下维护平行的内容文件。
|
||||||
|
若新增页面类型,请同时扩展 `content.config.ts` 的 schema。
|
||||||
|
* 使用 `useLocalizedCollection('index')` 或类似函数来获取内容,以获得类型支持与语言回退(fallback)。
|
||||||
|
* 保持多语言翻译文件在各个 locale 目录下同步更新:`i18n/locales/<locale>/`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
是否希望我帮你把这份翻译排版成一个更正式、带 Front Matter 的文档版本(例如带 `title`, `lastUpdated`, `description` 的 YAML 头部),以便直接放进 Nuxt Content 或 docs 目录?
|
||||||
214
README.md
@@ -1,75 +1,191 @@
|
|||||||
# Nuxt Minimal Starter
|
# Tootaio Studio 网站(Nuxt 4)
|
||||||
|
|
||||||
Look at the [Nuxt documentation](https://nuxt.com/docs/getting-started/introduction) to learn more.
|
Tootaio Studio 的官方网站代码仓库,基于 Nuxt 4、@nuxt/content、@nuxt/ui 与 i18n 构建,采用 TypeScript 与 ESM 配置。项目默认使用 pnpm 进行依赖与脚本管理。
|
||||||
|
|
||||||
## Setup
|
- 在线地址:`https://tootaio.com`
|
||||||
|
- 技术要点:内容与 UI 分离(YAML 内容 + JSON 翻译)、类型安全的内容加载、内置 SEO 与 CSP 头部策略
|
||||||
|
|
||||||
Make sure to install dependencies:
|
---
|
||||||
|
|
||||||
|
## 特性概览
|
||||||
|
|
||||||
|
- Nuxt 4 与 Vue 3 `<script setup lang="ts">`,全量 TypeScript 化
|
||||||
|
- @nuxt/content 管理结构化 YAML 内容,zod 校验 schema,类型安全
|
||||||
|
- @nuxt/ui 负责 UI 组件与样式,支持暗色模式与主题定制
|
||||||
|
- @nuxtjs/i18n 多语言(英文、简中),消息分包按需加载
|
||||||
|
- @nuxtjs/seo 统一 SEO 元信息配置(OG/Twitter/keywords)
|
||||||
|
- Nitro routeRules 内置严格 CSP、缓存与安全响应头
|
||||||
|
- 运行时配置集成 WhatsApp 咨询(公开变量)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
### 先决条件
|
||||||
|
|
||||||
|
- Node.js 18.20+ 或 20+
|
||||||
|
- pnpm 8+
|
||||||
|
|
||||||
|
### 安装依赖
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# npm
|
pnpm i
|
||||||
npm install
|
|
||||||
|
|
||||||
# pnpm
|
|
||||||
pnpm install
|
|
||||||
|
|
||||||
# yarn
|
|
||||||
yarn install
|
|
||||||
|
|
||||||
# bun
|
|
||||||
bun install
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Development Server
|
安装后会自动执行 `nuxt prepare`(见 `package.json:postinstall`)。
|
||||||
|
|
||||||
Start the development server on `http://localhost:3000`:
|
### 配置环境变量
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# npm
|
cp .env.example .env
|
||||||
npm run dev
|
```
|
||||||
|
|
||||||
# pnpm
|
必须设置:
|
||||||
|
|
||||||
|
- `NUXT_PUBLIC_WHATSAPP_NUMBER`:用于页面内“立刻咨询”按钮,走 WhatsApp H5 链接。
|
||||||
|
|
||||||
|
注:`nuxt.config.ts` 提供默认占位值,生产环境应以 `.env` 覆盖。
|
||||||
|
|
||||||
|
### 本地开发
|
||||||
|
|
||||||
|
```bash
|
||||||
pnpm dev
|
pnpm dev
|
||||||
|
|
||||||
# yarn
|
|
||||||
yarn dev
|
|
||||||
|
|
||||||
# bun
|
|
||||||
bun run dev
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Production
|
访问 `http://localhost:3000`,支持热更新(HMR)与暗色模式切换。
|
||||||
|
|
||||||
Build the application for production:
|
---
|
||||||
|
|
||||||
```bash
|
## 常用脚本
|
||||||
# npm
|
|
||||||
npm run build
|
|
||||||
|
|
||||||
# pnpm
|
- 开发:`pnpm dev`
|
||||||
pnpm build
|
- 构建:`pnpm build`(产物在 `.output/`)
|
||||||
|
- 预览:`pnpm preview`(以生产模式本地运行)
|
||||||
|
- 静态生成:`pnpm generate`(SSG 场景)
|
||||||
|
- 代码检查:`pnpm exec eslint .`
|
||||||
|
|
||||||
# yarn
|
---
|
||||||
yarn build
|
|
||||||
|
|
||||||
# bun
|
## 目录结构
|
||||||
bun run build
|
|
||||||
|
- `app/`:Nuxt 应用源码
|
||||||
|
- `pages/`:页面组件(如 `app/pages/index.vue`, `app/pages/webDev.vue`)
|
||||||
|
- `layouts/`:布局(默认头部含语言与主题切换)
|
||||||
|
- `assets/css/main.css`:主题与样式(集成 `@nuxt/ui` 与 `tailwindcss`)
|
||||||
|
- `composables/`:组合式函数(如 `LocalizedCollection.ts`, `NavLinks.ts`)
|
||||||
|
- `schemas/`:zod 定义(如 `pricingPlanSchema.ts`, `buttonSchema.ts`)
|
||||||
|
- `content/`:YAML 内容(按语言区分 `en-US/`, `zh-CN/`)
|
||||||
|
- `i18n/locales/<locale>/`:UI 文案 JSON 包(按页面拆分,如 `common.json`, `index.json`)
|
||||||
|
- `public/`:静态资源(如 `favicon.ico`, `og-image-1.png`, `_robots.txt`)
|
||||||
|
- 根配置:`nuxt.config.ts`, `content.config.ts`, `eslint.config.mjs`
|
||||||
|
- 文档:`docs/`(工程审计、路线等)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 内容与多语言
|
||||||
|
|
||||||
|
### 内容模型(@nuxt/content)
|
||||||
|
|
||||||
|
项目在 `content.config.ts` 中用 zod 定义了页面内容的 schema,并建立集合与源文件的对应关系:
|
||||||
|
|
||||||
|
- `index_en` ↔ `content/en-US/index.yml`
|
||||||
|
- `index_zh` ↔ `content/zh-CN/index.yml`
|
||||||
|
- `webDev_en` ↔ `content/en-US/webDev.yml`
|
||||||
|
- `webDev_zh` ↔ `content/zh-CN/webDev.yml`
|
||||||
|
|
||||||
|
在页面中通过类型安全的加载器获取内容:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// 示例:在页面/组件中获取本地化内容
|
||||||
|
const { data: page } = await useLocalizedCollection('index')
|
||||||
```
|
```
|
||||||
|
|
||||||
Locally preview production build:
|
上述加载器会根据当前语言自动选择集合,并在缺失时回退到英文。
|
||||||
|
|
||||||
```bash
|
### UI 文案(@nuxtjs/i18n)
|
||||||
# npm
|
|
||||||
npm run preview
|
|
||||||
|
|
||||||
# pnpm
|
- 语言:`en`(en-US)、`zh-CN`
|
||||||
pnpm preview
|
- 加载策略:`strategy: "no_prefix"`
|
||||||
|
- 文案文件:`i18n/locales/<locale>/*.json`
|
||||||
|
|
||||||
# yarn
|
示例 key:`index.featuredProjects.viewDemo`, `common.button.submit`。
|
||||||
yarn preview
|
|
||||||
|
|
||||||
# bun
|
新增页面或模块时,请同步维护中英文版本的 YAML 与 JSON,保持 key 一致。
|
||||||
bun run preview
|
|
||||||
```
|
|
||||||
|
|
||||||
Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.
|
---
|
||||||
|
|
||||||
|
## UI 与主题
|
||||||
|
|
||||||
|
- 组件库:`@nuxt/ui`(`UPage*`, `UHeader`, `UButton`, `UTabs`, `UModal` 等)
|
||||||
|
- 主题:`app/assets/css/main.css` 中可定制字体与主色(项目默认霓虹红 `--color-primary`)
|
||||||
|
- 暗色模式:内置 `UColorModeButton`,图标与样式随模式自适应
|
||||||
|
- 图标:`@iconify-json/mdi` 与 `skill-icons` 系列(通过 `UIcon` 使用)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## SEO 与安全
|
||||||
|
|
||||||
|
- SEO:`@nuxtjs/seo` 统一设置标题、描述、OG、Twitter 等元信息(见 `nuxt.config.ts` 中 `DEFAULT_SEO`)
|
||||||
|
- 站点地址:`site.url = https://tootaio.com`
|
||||||
|
- 安全头:在 `routeRules` 配置了 CSP、`X-Content-Type-Options`, `Referrer-Policy` 等;静态资源(`/_nuxt/**`)设置了长期缓存
|
||||||
|
- 图片白名单:`img-src` 允许 `self` 与 `https://img.tootaio.com` 与 `data:`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 运行时配置与外部集成
|
||||||
|
|
||||||
|
- 环境变量:`NUXT_PUBLIC_WHATSAPP_NUMBER`(公开变量,用于发起 WhatsApp 咨询)
|
||||||
|
- 组合式函数:`useWhatsAppMsgSender()` 会根据该号码构造 WhatsApp H5 链接并 `window.open`
|
||||||
|
- 示例入口:`app/components/webDev/ContactSalesModal.vue` 在提交后拼接多语言消息并发起咨询
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 代码规范与校验
|
||||||
|
|
||||||
|
- 风格:TypeScript、2 空格缩进、ESM 配置、组合式函数使用 PascalCase
|
||||||
|
- ESLint:`@nuxt/eslint` 预设,配置见 `eslint.config.mjs`
|
||||||
|
- 手动检查:`pnpm exec eslint .`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 测试(当前状态)
|
||||||
|
|
||||||
|
- 暂未配置自动化测试
|
||||||
|
- 建议:单测使用 Vitest,E2E 使用 Playwright
|
||||||
|
- 在此之前请进行本地验证:
|
||||||
|
- `pnpm dev` 运行并切换语言
|
||||||
|
- 校验 `content/*.yml` 是否符合 `content.config.ts` 中的 schema
|
||||||
|
- `pnpm build` 与 `pnpm preview` 能否成功
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 提交与协作
|
||||||
|
|
||||||
|
- 提交规范:Conventional Commits(含 scope),如:`feat(content): ...`, `refactor(ui): ...`
|
||||||
|
- 分支命名:`feat/<desc>`、`fix/<desc>`
|
||||||
|
- PR 说明:改动目的 / 关联 Issue / UI 截图 / 测试步骤
|
||||||
|
- 内容改动请确保中英文同步更新(YAML 与 i18n JSON)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 部署建议
|
||||||
|
|
||||||
|
- 服务器渲染(默认):`pnpm build` 生成 `.output/` 后按 Nuxt/Nitro 的 Node 运行方式部署
|
||||||
|
- 静态站点:`pnpm generate` 进行 SSG(请根据实际路由策略评估可行性)
|
||||||
|
- 常见平台:Vercel / Netlify / 任意 Node 主机(注意保留 `routeRules` 与响应头设置)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 参考文件
|
||||||
|
|
||||||
|
- 配置:`nuxt.config.ts:1`、`content.config.ts:1`
|
||||||
|
- 页面:`app/pages/index.vue:1`、`app/pages/webDev.vue:1`
|
||||||
|
- 主题:`app/assets/css/main.css:1`
|
||||||
|
- 多语言:`i18n/locales/en-US/common.json:1`、`i18n/locales/zh-CN/common.json:1`
|
||||||
|
- 环境:`.env.example:1`(复制为 `.env` 并填写)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 许可
|
||||||
|
|
||||||
|
如无单独说明,本仓库内容归 Tootaio Studio 所有。
|
||||||
|
|||||||
206
app/components/webDev/ContactSalesModal.vue
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
<template>
|
||||||
|
<UModal
|
||||||
|
v-model:open="open"
|
||||||
|
:title="
|
||||||
|
$t('webDev.know_more_title', {
|
||||||
|
plan: pkg?.planTitle || '—',
|
||||||
|
})
|
||||||
|
"
|
||||||
|
:description="
|
||||||
|
$t('webDev.know_more_description', {
|
||||||
|
plan: pkg?.planTitle || '—',
|
||||||
|
})
|
||||||
|
"
|
||||||
|
aria-label="Contact Sales Modal"
|
||||||
|
>
|
||||||
|
<template #body>
|
||||||
|
<div v-if="!pkg" class="py-6">
|
||||||
|
<!-- 占位 / loading -->
|
||||||
|
<div class="text-center text-sm text-muted">
|
||||||
|
{{ $t("webDev.loading_plan", "Loading plan...") }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<UForm
|
||||||
|
v-else
|
||||||
|
ref="form"
|
||||||
|
:schema="schema"
|
||||||
|
:state="state"
|
||||||
|
class="space-y-4"
|
||||||
|
@submit="onSubmit"
|
||||||
|
>
|
||||||
|
<p class="text-lg font-medium">
|
||||||
|
{{
|
||||||
|
$t("webDev.contact_intro", {
|
||||||
|
service: pkg.serviceTitle,
|
||||||
|
plan: pkg.planTitle,
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p class="text-lg font-medium">
|
||||||
|
{{ $t("webDev.which_provides", "Which provides:") }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ul class="space-y-1">
|
||||||
|
<li v-for="(feature, idx) in pkg.features" :key="idx">
|
||||||
|
<UIcon
|
||||||
|
size="16"
|
||||||
|
name="mdi:check-circle-outline"
|
||||||
|
class="text-primary-500 mr-2"
|
||||||
|
/>{{ feature }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p class="text-lg font-medium">
|
||||||
|
{{
|
||||||
|
$t("webDev.extra_remarks_title", "Besides that, I'd like to add:")
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<UFormField name="remarks">
|
||||||
|
<UTextarea
|
||||||
|
v-model="state.remarks"
|
||||||
|
:placeholder="
|
||||||
|
$t(
|
||||||
|
'webDev.remarks_placeholder',
|
||||||
|
'Enter your remarks or requirements, one per line'
|
||||||
|
)
|
||||||
|
"
|
||||||
|
class="w-full"
|
||||||
|
:rows="4"
|
||||||
|
/>
|
||||||
|
</UFormField>
|
||||||
|
</UForm>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="w-full flex gap-4">
|
||||||
|
<UButton
|
||||||
|
:label="$t('common.button.cancel')"
|
||||||
|
variant="subtle"
|
||||||
|
color="neutral"
|
||||||
|
class="flex-1"
|
||||||
|
@click="closeModal"
|
||||||
|
:disabled="isSubmitting"
|
||||||
|
/>
|
||||||
|
<UButton
|
||||||
|
:label="
|
||||||
|
isSubmitting
|
||||||
|
? $t('common.button.saving', 'Saving...')
|
||||||
|
: $t('common.button.submit')
|
||||||
|
"
|
||||||
|
variant="solid"
|
||||||
|
color="success"
|
||||||
|
class="flex-1"
|
||||||
|
@click="handleSubmit"
|
||||||
|
:loading="isSubmitting"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</UModal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import * as z from "zod";
|
||||||
|
import type { FormSubmitEvent } from "@nuxt/ui";
|
||||||
|
|
||||||
|
/* ----- types ----- */
|
||||||
|
type ContactSalesModalProps = {
|
||||||
|
serviceTitle: string;
|
||||||
|
planTitle: string;
|
||||||
|
startingPrice?: string;
|
||||||
|
features: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ----- reactive state ----- */
|
||||||
|
const pkg = ref<ContactSalesModalProps | null>(null);
|
||||||
|
const open = ref<boolean>(false);
|
||||||
|
const isSubmitting = ref(false);
|
||||||
|
|
||||||
|
/* form ref(保持和你原来用法一致) */
|
||||||
|
const form = useTemplateRef("form");
|
||||||
|
|
||||||
|
/* ----- zod schema ----- */
|
||||||
|
/* 注意:zod 的提示文本这里使用英文/固定字符串,若要用 i18n 需要在 validate 时把错误替换为 t(...) */
|
||||||
|
const schema = z.object({
|
||||||
|
remarks: z.string().optional(),
|
||||||
|
});
|
||||||
|
type Schema = z.infer<typeof schema>;
|
||||||
|
|
||||||
|
/* ----- 表单状态,初始化为空(openModal 时会填充) ----- */
|
||||||
|
const state = reactive<Partial<Schema>>({
|
||||||
|
remarks: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ----- 外部调用:打开 modal 并填充数据 ----- */
|
||||||
|
const openModal = (pricingPlan: ContactSalesModalProps) => {
|
||||||
|
pkg.value = { ...pricingPlan };
|
||||||
|
state.remarks = "";
|
||||||
|
open.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ----- 关闭并重置 ----- */
|
||||||
|
const closeModal = () => {
|
||||||
|
open.value = false;
|
||||||
|
// 延迟清理以防动画或确认逻辑中读取到空
|
||||||
|
setTimeout(() => {
|
||||||
|
pkg.value = null;
|
||||||
|
state.remarks = "";
|
||||||
|
// 如果需要也可以重置表单验证状态: form.value?.reset()
|
||||||
|
}, 200);
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ----- 表单提交处理 ----- */
|
||||||
|
async function onSubmit(event: FormSubmitEvent<Schema>) {
|
||||||
|
// event.data 已通过 schema 验证
|
||||||
|
try {
|
||||||
|
isSubmitting.value = true;
|
||||||
|
const payload = {
|
||||||
|
planTitle: pkg.value?.planTitle,
|
||||||
|
features: pkg.value?.features,
|
||||||
|
remarks: (event.data.remarks || "")
|
||||||
|
.split("\n")
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.filter(Boolean),
|
||||||
|
// 可以加上更多 metadata,比如 timestamp / user id / source page
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: 在此发送到后端 API,例如:
|
||||||
|
// await $fetch('/api/contact-sales', { method: 'POST', body: payload });
|
||||||
|
|
||||||
|
// 临时方案,发送到 WhatsApp 去
|
||||||
|
const wa_msg = $t("webDev.whatsapp_message", {
|
||||||
|
service: pkg.value?.serviceTitle,
|
||||||
|
plan: pkg.value?.planTitle,
|
||||||
|
price: pkg.value?.startingPrice,
|
||||||
|
featureList: pkg.value?.features.map((f) => ` ✅ ${f}`).join("\n"),
|
||||||
|
remarkList: (event.data.remarks || "")
|
||||||
|
.split("\n")
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((r) => `- ${r}`)
|
||||||
|
.join("\n"),
|
||||||
|
});
|
||||||
|
useWhatsAppMsgSender().sendMessage(wa_msg);
|
||||||
|
|
||||||
|
// 成功提示(如果你有全局 toast/notification): e.g. useToast().success(...)
|
||||||
|
// 关闭并清理
|
||||||
|
closeModal();
|
||||||
|
} catch (err) {
|
||||||
|
// 处理错误(显示错误 toast / 控制台)
|
||||||
|
console.error("submit failed", err);
|
||||||
|
// 这里可以显示友好的错误信息,例如: useToast().error(t('webDev.submit_failed'))
|
||||||
|
} finally {
|
||||||
|
isSubmitting.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* footer 按钮触发的提交(触发表单验证) */
|
||||||
|
async function handleSubmit() {
|
||||||
|
await form.value?.submit();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 暴露给父组件的 API */
|
||||||
|
defineExpose({ openModal, closeModal });
|
||||||
|
</script>
|
||||||
66
app/composables/LocalizedCollection.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
// /composables/useLocalizedCollection.ts
|
||||||
|
import type { Collections } from "@nuxt/content";
|
||||||
|
|
||||||
|
export type UseLocalizedOptions = {
|
||||||
|
/** 默认 locale -> suffix 映射 */
|
||||||
|
localeMap?: Record<string, string>;
|
||||||
|
/** 回退 locale 的 suffix(例如 'en') */
|
||||||
|
fallbackSuffix?: string;
|
||||||
|
/** 当找不到内容时是否抛错,默认 true */
|
||||||
|
throwOnMissing?: boolean;
|
||||||
|
/** useAsyncData 的 key 前缀(默认等于 baseName) */
|
||||||
|
keyPrefix?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function asCollectionKey(key: string) {
|
||||||
|
return key as keyof Collections;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 带类型安全的多语言内容加载器
|
||||||
|
* @example
|
||||||
|
* const { data: page } = await useLocalizedCollection('index')
|
||||||
|
*/
|
||||||
|
export function useLocalizedCollection<
|
||||||
|
B extends string, // 基础名称
|
||||||
|
>(baseName: B, opts: UseLocalizedOptions = {}) {
|
||||||
|
const { locale } = useI18n();
|
||||||
|
|
||||||
|
const localeMap = opts.localeMap ?? { en: "en", "zh-CN": "zh" };
|
||||||
|
const fallbackSuffix = opts.fallbackSuffix ?? "en";
|
||||||
|
const keyPrefix = opts.keyPrefix ?? baseName;
|
||||||
|
const throwOnMissing = opts.throwOnMissing ?? true;
|
||||||
|
|
||||||
|
// 🔥 自动推断对应集合类型
|
||||||
|
type LocalizedKey = keyof {
|
||||||
|
[K in keyof Collections as K extends `${B}_${string}` ? K : never]: any;
|
||||||
|
};
|
||||||
|
type Schema = Collections[LocalizedKey];
|
||||||
|
|
||||||
|
return useAsyncData<Schema | null>(
|
||||||
|
`${keyPrefix}-${locale.value}`,
|
||||||
|
async () => {
|
||||||
|
const suffix =
|
||||||
|
localeMap[locale.value] ?? locale.value.split("-")[0] ?? "en";
|
||||||
|
const key = asCollectionKey(`${baseName}_${suffix}`);
|
||||||
|
let content = (await queryCollection(key).first()) as Schema | null;
|
||||||
|
|
||||||
|
if (!content && suffix !== fallbackSuffix) {
|
||||||
|
const fallbackKey = asCollectionKey(`${baseName}_${fallbackSuffix}`);
|
||||||
|
content = (await queryCollection(fallbackKey).first()) as Schema | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return content;
|
||||||
|
},
|
||||||
|
{ watch: [locale] }
|
||||||
|
).then((res) => {
|
||||||
|
if (throwOnMissing && res && !res.data?.value) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 404,
|
||||||
|
statusMessage: `Page not found: ${baseName} for locale ${locale.value}`,
|
||||||
|
fatal: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
});
|
||||||
|
}
|
||||||
86
app/composables/NavLinks.ts
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
// composables/useNavLinks.ts
|
||||||
|
import type { NavigationMenuItem } from "@nuxt/ui";
|
||||||
|
|
||||||
|
export const useNavLinks = () => {
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const navLinks = computed<NavigationMenuItem[]>(() => [
|
||||||
|
{
|
||||||
|
label: t("common.header.services.label"),
|
||||||
|
icon: "mdi:briefcase-outline",
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
label: t("common.header.services.children.webDev.label"),
|
||||||
|
description: t("common.header.services.children.webDev.description"),
|
||||||
|
icon: "mdi:web",
|
||||||
|
to: "/webDev"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("common.header.services.children.softwareDev.label"),
|
||||||
|
description: t(
|
||||||
|
"common.header.services.children.softwareDev.description"
|
||||||
|
),
|
||||||
|
icon: "mdi:tools",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("common.header.services.children.eventVisual.label"),
|
||||||
|
description: t(
|
||||||
|
"common.header.services.children.eventVisual.description"
|
||||||
|
),
|
||||||
|
icon: "mdi:monitor-dashboard",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("common.header.services.children.lab.label"),
|
||||||
|
description: t("common.header.services.children.lab.description"),
|
||||||
|
icon: "mdi:test-tube-off",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("common.header.projects.label"),
|
||||||
|
icon: "mdi:lightbulb-group-outline",
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
label: t("common.header.projects.children.commercialWebsite.label"),
|
||||||
|
description: t(
|
||||||
|
"common.header.projects.children.commercialWebsite.description"
|
||||||
|
),
|
||||||
|
icon: "mdi:web",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("common.header.projects.children.gameDev.label"),
|
||||||
|
description: t("common.header.projects.children.gameDev.description"),
|
||||||
|
icon: "mdi:gamepad-variant-outline",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("common.header.projects.children.tools.label"),
|
||||||
|
description: t("common.header.projects.children.tools.description"),
|
||||||
|
icon: "mdi:tools",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("common.header.projects.children.special.label"),
|
||||||
|
description: t("common.header.projects.children.special.description"),
|
||||||
|
icon: "mdi:star",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("common.header.insights.label"),
|
||||||
|
icon: "mdi:brain",
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
label: t("common.header.insights.children.xiaomaiBlog.label"),
|
||||||
|
description: t(
|
||||||
|
"common.header.insights.children.xiaomaiBlog.description"
|
||||||
|
),
|
||||||
|
icon: "mdi:pencil-outline",
|
||||||
|
to: "https://xiaomai.tootaio.com/",
|
||||||
|
type: "link",
|
||||||
|
target: "_blank",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
return navLinks;
|
||||||
|
};
|
||||||
21
app/composables/WhatsAppMsgSender.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { createSharedComposable } from "@vueuse/core";
|
||||||
|
|
||||||
|
const _useWhatsAppMsgSender = () => {
|
||||||
|
const config = useRuntimeConfig();
|
||||||
|
const phone = config.public.whatsappNumber;
|
||||||
|
|
||||||
|
// --- WhatsApp 自动消息逻辑 ---
|
||||||
|
const sendMessage = (message: string) => {
|
||||||
|
const text = encodeURIComponent(message);
|
||||||
|
const url = `https://api.whatsapp.com/send?phone=${phone}&text=${text}`;
|
||||||
|
window.open(url, "_blank", "noopener,noreferrer");
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
sendMessage,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useWhatsAppMsgSender = createSharedComposable(
|
||||||
|
_useWhatsAppMsgSender
|
||||||
|
);
|
||||||
@@ -1,13 +1,58 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<UPage>
|
||||||
|
<UHeader
|
||||||
|
:ui="{
|
||||||
|
left: 'flex items-center gap-1.5',
|
||||||
|
center: 'hidden lg:flex lg:flex-4',
|
||||||
|
right: 'flex items-center justify-end gap-1.5',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<template #title> Tootaio Studio </template>
|
||||||
|
<template #default>
|
||||||
|
<UNavigationMenu
|
||||||
|
:items="navLinks"
|
||||||
|
variant="link"
|
||||||
|
class="w-full justify-center"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #body>
|
||||||
|
<UNavigationMenu :items="navLinks" orientation="vertical" />
|
||||||
|
</template>
|
||||||
|
<template #right>
|
||||||
|
<UColorModeButton />
|
||||||
|
<UButton
|
||||||
|
icon="twemoji:flag-china"
|
||||||
|
:variant="$i18n.locale == 'zh-CN' ? 'outline' : 'ghost'"
|
||||||
|
class="cursor-pointer"
|
||||||
|
color="neutral"
|
||||||
|
:disabled="$i18n.locale == 'zh-CN'"
|
||||||
|
@click="setLocale('zh-CN')"
|
||||||
|
/>
|
||||||
|
<UButton
|
||||||
|
icon="twemoji:flag-united-states"
|
||||||
|
:variant="$i18n.locale == 'en' ? 'outline' : 'ghost'"
|
||||||
|
class="cursor-pointer"
|
||||||
|
color="neutral"
|
||||||
|
:disabled="$i18n.locale == 'en'"
|
||||||
|
@click="setLocale('en')"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</UHeader>
|
||||||
|
<UMain>
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</UMain>
|
||||||
|
<UFooter>
|
||||||
|
<template #left>
|
||||||
|
© {{ new Date().getFullYear() }} Tootaio Studio. All rights
|
||||||
|
reserved.
|
||||||
|
</template>
|
||||||
|
</UFooter>
|
||||||
|
</UPage>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
const { setLocale } = useI18n();
|
||||||
|
const navLinks = useNavLinks();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style></style>
|
||||||
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -1,44 +1,177 @@
|
|||||||
<template>
|
<template>
|
||||||
<UPage>
|
<div>
|
||||||
<UHeader>
|
<!-- 全幅 Hero - 在 Page 布局之外 -->
|
||||||
<template #title> Tootaio Studio </template>
|
<UPageHero
|
||||||
<template #right>
|
:title="page?.title"
|
||||||
<ULocaleSelect
|
:description="page?.description"
|
||||||
:model-value="locale"
|
:ui="{
|
||||||
:locales="[en, zh_cn]"
|
root: 'relative before:absolute before:inset-0 before:bg-[image:var(--bg-image)] before:bg-cover before:bg-center before:-z-10 before:opacity-40',
|
||||||
@update:model-value="(v) => setLocale(v as 'en' | 'zh-CN')"
|
}"
|
||||||
|
:style="{
|
||||||
|
'--bg-image': `url('${currentBgImage}')`,
|
||||||
|
}"
|
||||||
/>
|
/>
|
||||||
</template>
|
|
||||||
</UHeader>
|
|
||||||
<UPageSection
|
<UPageSection
|
||||||
title="Trusted by"
|
:title="page?.capabilities.title"
|
||||||
:description="$t('index.trustedBy', { count: 10000 })"
|
:features="page?.capabilities.features"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<UPageSection :title="page?.featuredProjects.title">
|
||||||
|
<UCarousel
|
||||||
|
v-slot="{ item }"
|
||||||
|
:items="page?.featuredProjects.projects"
|
||||||
|
:ui="{ item: 'basis-full sm:basis-1/2 lg:basis-1/3' }"
|
||||||
>
|
>
|
||||||
|
<UPageCard
|
||||||
|
class="my-2"
|
||||||
|
:title="item.title"
|
||||||
|
:description="item.description"
|
||||||
|
:highlight="item.highlight"
|
||||||
|
:spotlight="item.spotlight"
|
||||||
|
>
|
||||||
|
<img :src="item.image" :alt="item.title" />
|
||||||
|
<UButton
|
||||||
|
v-if="item.demoLink"
|
||||||
|
:href="item.demoLink"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
{{ $t("index.featuredProjects.viewDemo") }}
|
||||||
|
</UButton>
|
||||||
|
</UPageCard>
|
||||||
|
</UCarousel>
|
||||||
|
</UPageSection>
|
||||||
|
|
||||||
|
<UPageSection :title="page?.techStack.title">
|
||||||
<UMarquee>
|
<UMarquee>
|
||||||
<img
|
<UIcon
|
||||||
v-for="logo in trustedBy"
|
v-for="icon in techIcons"
|
||||||
:key="logo.src"
|
:key="icon"
|
||||||
:src="logo.src"
|
:name="icon"
|
||||||
:alt="logo.alt"
|
class="size-16"
|
||||||
class="h-12 mx-8 grayscale opacity-60"
|
/>
|
||||||
|
</UMarquee>
|
||||||
|
<UMarquee reverse>
|
||||||
|
<UIcon
|
||||||
|
v-for="icon in toolsIcons"
|
||||||
|
:key="icon"
|
||||||
|
:name="icon"
|
||||||
|
class="size-16"
|
||||||
/>
|
/>
|
||||||
</UMarquee>
|
</UMarquee>
|
||||||
</UPageSection>
|
</UPageSection>
|
||||||
</UPage>
|
|
||||||
|
<UPageSection
|
||||||
|
:title="page?.whyChooseUs.title"
|
||||||
|
:features="page?.whyChooseUs.features"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { en, zh_cn } from "@nuxt/ui/locale";
|
const { data: page } = await useLocalizedCollection("index");
|
||||||
const { locale, setLocale } = useI18n();
|
|
||||||
|
|
||||||
const trustedBy = ref([
|
useSeoMeta({
|
||||||
{ src: "/index/trusted-by/logoipsum-284.svg", alt: "Logo Ipsum" },
|
title: page.value?.seo.title,
|
||||||
{ src: "/index/trusted-by/logoipsum-338.svg", alt: "Logo Ipsum" },
|
});
|
||||||
{ src: "/index/trusted-by/logoipsum-353.svg", alt: "Logo Ipsum" },
|
|
||||||
{ src: "/index/trusted-by/logoipsum-378.svg", alt: "Logo Ipsum" },
|
const colorMode = useColorMode();
|
||||||
{ src: "/index/trusted-by/logoipsum-392.svg", alt: "Logo Ipsum" },
|
|
||||||
{ src: "/index/trusted-by/logoipsum-403.svg", alt: "Logo Ipsum" },
|
const backgroundImages = [
|
||||||
{ src: "/index/trusted-by/logoipsum-409.svg", alt: "Logo Ipsum" },
|
"https://img.tootaio.com/i/2025/11/05/avc5ld.png",
|
||||||
|
"https://img.tootaio.com/i/2025/11/05/avcaff.png",
|
||||||
|
"https://img.tootaio.com/i/2025/11/05/avcjbw.png",
|
||||||
|
"https://img.tootaio.com/i/2025/11/05/avcp16.png",
|
||||||
|
"https://img.tootaio.com/i/2025/11/05/avcv1q.png",
|
||||||
|
"https://img.tootaio.com/i/2025/11/05/avd47a.png",
|
||||||
|
"https://img.tootaio.com/i/2025/11/05/avdx6a.png",
|
||||||
|
"https://img.tootaio.com/i/2025/11/05/avegxy.png",
|
||||||
|
"https://img.tootaio.com/i/2025/11/05/avemgn.png",
|
||||||
|
"https://img.tootaio.com/i/2025/11/05/avf3wl.png",
|
||||||
|
];
|
||||||
|
|
||||||
|
const currentBgImage = ref<string | undefined>("");
|
||||||
|
|
||||||
|
// 随机选择背景
|
||||||
|
const randomBg = () => {
|
||||||
|
const randomIndex = Math.floor(Math.random() * backgroundImages.length);
|
||||||
|
currentBgImage.value = backgroundImages[randomIndex];
|
||||||
|
};
|
||||||
|
|
||||||
|
// 轮播背景
|
||||||
|
onMounted(() => {
|
||||||
|
randomBg();
|
||||||
|
});
|
||||||
|
|
||||||
|
const techIcons = computed(() => [
|
||||||
|
"skill-icons:html",
|
||||||
|
"skill-icons:css",
|
||||||
|
"skill-icons:javascript",
|
||||||
|
"skill-icons:typescript",
|
||||||
|
"skill-icons:docker",
|
||||||
|
colorMode.value === "dark"
|
||||||
|
? "skill-icons:vuejs-dark"
|
||||||
|
: "skill-icons:vuejs-light",
|
||||||
|
colorMode.value === "dark"
|
||||||
|
? "skill-icons:nuxtjs-dark"
|
||||||
|
: "skill-icons:nuxtjs-light",
|
||||||
|
colorMode.value === "dark"
|
||||||
|
? "skill-icons:tailwindcss-dark"
|
||||||
|
: "skill-icons:tailwindcss-light",
|
||||||
|
colorMode.value === "dark"
|
||||||
|
? "skill-icons:nodejs-dark"
|
||||||
|
: "skill-icons:nodejs-light",
|
||||||
|
"skill-icons:cs",
|
||||||
|
colorMode.value === "dark"
|
||||||
|
? "skill-icons:python-dark"
|
||||||
|
: "skill-icons:python-light",
|
||||||
|
]);
|
||||||
|
|
||||||
|
const toolsIcons = ref([
|
||||||
|
"skill-icons:photoshop",
|
||||||
|
"skill-icons:illustrator",
|
||||||
|
"skill-icons:git",
|
||||||
|
colorMode.value === "dark"
|
||||||
|
? "skill-icons:vscode-dark"
|
||||||
|
: "skill-icons:vscode-light",
|
||||||
|
colorMode.value === "dark"
|
||||||
|
? "skill-icons:visualstudio-dark"
|
||||||
|
: "skill-icons:visualstudio-light",
|
||||||
|
colorMode.value === "dark"
|
||||||
|
? "skill-icons:github-dark"
|
||||||
|
: "skill-icons:github-light",
|
||||||
|
colorMode.value === "dark"
|
||||||
|
? "skill-icons:godot-dark"
|
||||||
|
: "skill-icons:godot-light",
|
||||||
|
colorMode.value === "dark"
|
||||||
|
? "skill-icons:unity-dark"
|
||||||
|
: "skill-icons:unity-light",
|
||||||
|
colorMode.value === "dark"
|
||||||
|
? "skill-icons:blender-dark"
|
||||||
|
: "skill-icons:blender-light",
|
||||||
|
colorMode.value === "dark"
|
||||||
|
? "skill-icons:androidstudio-dark"
|
||||||
|
: "skill-icons:androidstudio-light",
|
||||||
|
colorMode.value === "dark"
|
||||||
|
? "skill-icons:windows-dark"
|
||||||
|
: "skill-icons:windows-light",
|
||||||
|
colorMode.value === "dark"
|
||||||
|
? "skill-icons:linux-dark"
|
||||||
|
: "skill-icons:linux-light",
|
||||||
|
colorMode.value === "dark"
|
||||||
|
? "skill-icons:apple-dark"
|
||||||
|
: "skill-icons:apple-light",
|
||||||
|
colorMode.value === "dark"
|
||||||
|
? "skill-icons:idea-dark"
|
||||||
|
: "skill-icons:idea-light",
|
||||||
|
colorMode.value === "dark"
|
||||||
|
? "skill-icons:pycharm-dark"
|
||||||
|
: "skill-icons:pycharm-light",
|
||||||
|
colorMode.value === "dark"
|
||||||
|
? "skill-icons:rider-dark"
|
||||||
|
: "skill-icons:rider-light",
|
||||||
]);
|
]);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
47
app/pages/webDev.vue
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<template>
|
||||||
|
<UContainer>
|
||||||
|
<UPageHero :title="page?.title" :description="page?.description" />
|
||||||
|
|
||||||
|
<p class="text-muted py-4 text-center">{{ page?.remarks }}</p>
|
||||||
|
|
||||||
|
<UTabs :items="serviceTabs">
|
||||||
|
<template v-for="service in serviceTabs" :key="service.id" #[service.id]>
|
||||||
|
<UPricingPlans :plans="service.plans" />
|
||||||
|
</template>
|
||||||
|
</UTabs>
|
||||||
|
<WebDevContactSalesModal ref="webDevContactSalesModal" />
|
||||||
|
</UContainer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import WebDevContactSalesModal from "~/components/webDev/ContactSalesModal.vue";
|
||||||
|
|
||||||
|
const { data: page } = await useLocalizedCollection("webDev");
|
||||||
|
|
||||||
|
const webDevContactSalesModal = ref<InstanceType<
|
||||||
|
typeof WebDevContactSalesModal
|
||||||
|
> | null>(null);
|
||||||
|
|
||||||
|
const serviceTabs = computed(() =>
|
||||||
|
page.value?.services.map((srv) => ({
|
||||||
|
...srv,
|
||||||
|
plans: srv.plans.map((pln) => ({
|
||||||
|
...pln,
|
||||||
|
button: {
|
||||||
|
label: "立刻咨询", // TODO: i18n 适配
|
||||||
|
onClick: () => {
|
||||||
|
webDevContactSalesModal.value?.openModal({
|
||||||
|
serviceTitle: srv.label,
|
||||||
|
planTitle: pln.title,
|
||||||
|
startingPrice: pln.price,
|
||||||
|
features: pln.features ?? [],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
slot: srv.id,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style></style>
|
||||||
37
app/schemas/buttonSchema.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
// buttonSchema.ts
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const ButtonSize = z.enum(["2xs", "xs", "sm", "md", "lg", "xl"]);
|
||||||
|
const ButtonColor = z.enum([
|
||||||
|
"error",
|
||||||
|
"primary",
|
||||||
|
"secondary",
|
||||||
|
"success",
|
||||||
|
"info",
|
||||||
|
"warning",
|
||||||
|
"neutral",
|
||||||
|
]);
|
||||||
|
const ButtonVariant = z.enum(["solid", "outline", "soft", "ghost", "link"]);
|
||||||
|
|
||||||
|
export const ButtonPropsSchema = z.object({
|
||||||
|
size: ButtonSize.optional().default("sm"),
|
||||||
|
type: z.enum(["button", "submit", "reset"]).optional().default("button"),
|
||||||
|
label: z.string().optional().default(""),
|
||||||
|
color: ButtonColor.optional().default("primary"),
|
||||||
|
variant: ButtonVariant.optional().default("solid"),
|
||||||
|
icon: z.string().optional().default(""),
|
||||||
|
leading: z.boolean().optional().default(false),
|
||||||
|
trailing: z.boolean().optional().default(false),
|
||||||
|
disabled: z.boolean().optional().default(false),
|
||||||
|
loading: z.boolean().optional().default(false),
|
||||||
|
loadingIcon: z.string().optional().default("i-heroicons-arrow-path-20-solid"),
|
||||||
|
block: z.boolean().optional().default(false),
|
||||||
|
to: z.string().optional().default(""),
|
||||||
|
target: z.string().optional().default(""),
|
||||||
|
padded: z.boolean().optional().default(true),
|
||||||
|
square: z.boolean().optional().default(false),
|
||||||
|
truncate: z.boolean().optional().default(false),
|
||||||
|
attrs: z.record(z.any()).optional().default({}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ButtonProps = z.infer<typeof ButtonPropsSchema>;
|
||||||
57
app/schemas/pricingPlanSchema.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
import { ButtonPropsSchema } from "./buttonSchema";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zod schema for UPricingPlan component (basic props only)
|
||||||
|
* Reference: https://ui.nuxt.com/docs/components/pricing-plan#props
|
||||||
|
*/
|
||||||
|
export const PricingPlanPropsSchema = z.object({
|
||||||
|
/** The title of the pricing plan. */
|
||||||
|
title: z.string(),
|
||||||
|
/** The description text shown under the title. */
|
||||||
|
description: z.string().optional(),
|
||||||
|
/** The current price of the plan. */
|
||||||
|
price: z.string().optional(),
|
||||||
|
/**
|
||||||
|
* Discounted price.
|
||||||
|
* When set, the main price will appear with a strikethrough.
|
||||||
|
*/
|
||||||
|
discount: z.string().optional(),
|
||||||
|
/** The unit period (e.g. /month) displayed next to price. */
|
||||||
|
billingCycle: z.string().optional(),
|
||||||
|
/** Additional billing text above the billing cycle. */
|
||||||
|
billingPeriod: z.string().optional(),
|
||||||
|
/**
|
||||||
|
* List of plan features.
|
||||||
|
* Can be an array of strings or array of objects (feature items).
|
||||||
|
*/
|
||||||
|
features: z.array(z.string()).optional().default([]),
|
||||||
|
/** The button displayed at the bottom (ButtonProps). */
|
||||||
|
button: ButtonPropsSchema.optional(),
|
||||||
|
/**
|
||||||
|
* Visual variant of the pricing plan.
|
||||||
|
* @default "outline"
|
||||||
|
*/
|
||||||
|
variant: z.enum(["soft", "solid", "outline", "subtle"]).optional().default("outline"),
|
||||||
|
/**
|
||||||
|
* Layout orientation of the component.
|
||||||
|
* @default "vertical"
|
||||||
|
*/
|
||||||
|
orientation: z.enum(["vertical", "horizontal"]).optional().default("vertical"),
|
||||||
|
/** Optional tagline text displayed above price. */
|
||||||
|
tagline: z.string().optional(),
|
||||||
|
/** Terms or disclaimer text displayed below features. */
|
||||||
|
terms: z.string().optional(),
|
||||||
|
/**
|
||||||
|
* Highlights the pricing plan visually (adds a ring around it).
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
highlight: z.boolean().optional().default(false),
|
||||||
|
/**
|
||||||
|
* Enlarges the plan card slightly for emphasis.
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
scale: z.boolean().optional().default(false),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type PricingPlanProps = z.infer<typeof PricingPlanPropsSchema>;
|
||||||
86
content.config.ts
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import { defineContentConfig, defineCollection, z } from "@nuxt/content";
|
||||||
|
import { PricingPlanPropsSchema } from "./app/schemas/pricingPlanSchema";
|
||||||
|
|
||||||
|
const defineIndexSchema = () =>
|
||||||
|
z.object({
|
||||||
|
capabilities: z.object({
|
||||||
|
title: z.string(),
|
||||||
|
features: z.array(
|
||||||
|
z.object({
|
||||||
|
title: z.string(),
|
||||||
|
description: z.string(),
|
||||||
|
icon: z.string(),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
featuredProjects: z.object({
|
||||||
|
title: z.string(),
|
||||||
|
projects: z.array(
|
||||||
|
z.object({
|
||||||
|
title: z.string(),
|
||||||
|
description: z.string(),
|
||||||
|
image: z.string(),
|
||||||
|
demoLink: z.string(),
|
||||||
|
highlight: z.boolean(),
|
||||||
|
spotlight: z.boolean(),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
techStack: z.object({
|
||||||
|
title: z.string(),
|
||||||
|
}),
|
||||||
|
whyChooseUs: z.object({
|
||||||
|
title: z.string(),
|
||||||
|
features: z.array(
|
||||||
|
z.object({
|
||||||
|
title: z.string(),
|
||||||
|
description: z.string(),
|
||||||
|
icon: z.string(),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const defineWebDevSchema = () =>
|
||||||
|
z.object({
|
||||||
|
remarks: z.string(),
|
||||||
|
services: z.array(
|
||||||
|
z.object({
|
||||||
|
id: z.string().min(1),
|
||||||
|
label: z.string().min(1),
|
||||||
|
icon: z.string().optional(), // 比如 "lucide:mouse-pointer-click"
|
||||||
|
// 你原结构里通过 createService 包装,但最终是一个对象
|
||||||
|
plans: z
|
||||||
|
.array(PricingPlanPropsSchema)
|
||||||
|
.min(1),
|
||||||
|
// 预留扩展字段(例如:category、tags、hidden 等)
|
||||||
|
category: z.string().optional(),
|
||||||
|
tags: z.array(z.string()).optional(),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default defineContentConfig({
|
||||||
|
collections: {
|
||||||
|
index_en: defineCollection({
|
||||||
|
type: "page",
|
||||||
|
source: "en-US/index.yml",
|
||||||
|
schema: defineIndexSchema(),
|
||||||
|
}),
|
||||||
|
index_zh: defineCollection({
|
||||||
|
type: "page",
|
||||||
|
source: "zh-CN/index.yml",
|
||||||
|
schema: defineIndexSchema(),
|
||||||
|
}),
|
||||||
|
webDev_en: defineCollection({
|
||||||
|
type: "page",
|
||||||
|
source: "en-US/webDev.yml",
|
||||||
|
schema: defineWebDevSchema(),
|
||||||
|
}),
|
||||||
|
webDev_zh: defineCollection({
|
||||||
|
type: "page",
|
||||||
|
source: "zh-CN/webDev.yml",
|
||||||
|
schema: defineWebDevSchema(),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
});
|
||||||
52
content/en-US/index.yml
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
seo:
|
||||||
|
title: "Homepage"
|
||||||
|
title: "Tootaio Studio"
|
||||||
|
description: "Technology Meets Imagination."
|
||||||
|
capabilities:
|
||||||
|
title: "Our Capabilities"
|
||||||
|
features:
|
||||||
|
- title: "Custom Website Development"
|
||||||
|
description: "Tailored websites and backend systems built with modern frameworks like Nuxt and Next, delivering performance, scalability, and visual impact."
|
||||||
|
- title: "Software & Tool Engineering"
|
||||||
|
description: "Custom-built applications, internal dashboards, and automation tools that streamline workflows and enhance productivity."
|
||||||
|
- title: "Game Design & Development"
|
||||||
|
description: "From Game Jam prototypes to commercial releases — we design and develop immersive, creative, and technically robust gaming experiences."
|
||||||
|
- title: "Interactive Media & Event Systems"
|
||||||
|
description: "Creating dynamic, large-screen visuals and real-time interactive systems for events, exhibitions, and banquets."
|
||||||
|
- title: "Tech Exploration & Evaluation"
|
||||||
|
description: "Experimenting with emerging technologies and evaluating software and hardware to stay ahead of the innovation curve."
|
||||||
|
- title: "Creative Consulting & Digital Strategy"
|
||||||
|
description: "Providing expert guidance on product architecture, digital transformation, and long-term technology strategy for brands and startups."
|
||||||
|
featuredProjects:
|
||||||
|
title: "Featured Projects"
|
||||||
|
projects:
|
||||||
|
- title: "YPHS Alumni Official Website"
|
||||||
|
description: "Website Design and Development worth RM28,000. Donate to YPHS Alumni by founder of Tootaio."
|
||||||
|
image: "http://img.tootaio.com/i/2025/11/05/d9kurl.png"
|
||||||
|
demoLink: "https://yphsalumni.org"
|
||||||
|
- title: "Malaysia students return to China guide"
|
||||||
|
description: "An interactive guide which help more than 5000 students back to China Mainland to continue their study during pandemic COVID-19 in year 2022. This project is supported by The Ministry of Foreign Affairs (MFA) in Malaysia"
|
||||||
|
image: "http://img.tootaio.com/i/2025/11/05/d9kcma.png"
|
||||||
|
demoLink: "https://tootaio.github.io"
|
||||||
|
spotlight: true
|
||||||
|
- title: "Light Chasing"
|
||||||
|
description: "This is an entry from the 2023 G-bits University GameJam Challenge, based on the Godot engine."
|
||||||
|
image: "https://img.tootaio.com/i/2025/09/26/j2swgq.png"
|
||||||
|
techStack:
|
||||||
|
title: "Tech Stacks"
|
||||||
|
whyChooseUs:
|
||||||
|
title: "Why Choose Us"
|
||||||
|
description: "We don’t just build code — we craft digital experiences."
|
||||||
|
features:
|
||||||
|
- title: "Fully Custom-Built"
|
||||||
|
description: "Every website, system, and game is built from scratch to match your unique brand identity and performance needs."
|
||||||
|
- title: "Tech-Driven, Not Just Design"
|
||||||
|
description: "Our engineers lead every project, optimizing architecture, performance, and security at every level."
|
||||||
|
- title: "Cross-Domain Expertise"
|
||||||
|
description: "From web systems to games and interactive tools — we merge creativity and engineering to deliver seamless experiences."
|
||||||
|
- title: "End-to-End Service"
|
||||||
|
description: "From concept, prototyping, and frontend to deployment and long-term support — we handle everything in-house."
|
||||||
|
- title: "Proven Project Value"
|
||||||
|
description: "We’ve delivered projects for educational institutions, brands, and game developers — with real impact and measurable value."
|
||||||
|
- title: "Future-Oriented"
|
||||||
|
description: "Beyond client work, we’re building our own products and experiments, pushing the boundaries of what’s possible."
|
||||||
95
content/en-US/webDev.yml
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
seo:
|
||||||
|
title: "Web / Website Custom Development"
|
||||||
|
description: "Create a tailor-made online portal for your brand to boost visibility and conversion rates."
|
||||||
|
title: "Web / Website Custom Development"
|
||||||
|
description: "Create a tailor-made online portal for your brand to boost visibility and conversion rates."
|
||||||
|
|
||||||
|
remarks: "All plans include basic domain and server deployment for the first year. If server load becomes too high, we will assist with migration to a more stable third-party environment."
|
||||||
|
|
||||||
|
services:
|
||||||
|
- id: landing-page
|
||||||
|
label: "Landing Page"
|
||||||
|
icon: "mdi:cursor-default-click-outline"
|
||||||
|
plans:
|
||||||
|
- title: "Basic"
|
||||||
|
description: "Ideal for individuals or small businesses to launch a one-page showcase website quickly."
|
||||||
|
price: "From RM899"
|
||||||
|
tagline: "Includes domain & hosting"
|
||||||
|
features:
|
||||||
|
- "Single-page structure (1–3 sections)"
|
||||||
|
- "Responsive design (mobile / tablet / desktop)"
|
||||||
|
- "Basic text and image layout"
|
||||||
|
- "Contact form or WhatsApp button"
|
||||||
|
- "Google Analytics integration"
|
||||||
|
- "1 free revision"
|
||||||
|
- "Delivery within 7 days"
|
||||||
|
button:
|
||||||
|
label: "Contact Now"
|
||||||
|
|
||||||
|
- title: "Standard"
|
||||||
|
description: "Designed for brands and startups to create high-conversion pages."
|
||||||
|
price: "From RM1,599"
|
||||||
|
tagline: "Includes domain & hosting"
|
||||||
|
highlight: true
|
||||||
|
features:
|
||||||
|
- "Multi-section layout (4–6 sections)"
|
||||||
|
- "Custom brand styling and color scheme"
|
||||||
|
- "Lightweight animations and motion effects"
|
||||||
|
- "SEO optimization + performance tuning"
|
||||||
|
- "Tracking integration (GA / Pixel)"
|
||||||
|
- "2 free revisions"
|
||||||
|
- "Delivery within 14 days"
|
||||||
|
button:
|
||||||
|
label: "Request Quote"
|
||||||
|
|
||||||
|
- title: "Premium Custom"
|
||||||
|
description: "Comprehensive visual and marketing upgrade for established brands."
|
||||||
|
price: "From RM2,999"
|
||||||
|
features:
|
||||||
|
- "Exclusive visual design and interaction experience"
|
||||||
|
- "Complete brand style system"
|
||||||
|
- "A/B testing and conversion optimization"
|
||||||
|
- "Marketing tool integration (email, analytics, CRM)"
|
||||||
|
- "Multi-language / dynamic content support"
|
||||||
|
button:
|
||||||
|
label: "Book a Custom Plan"
|
||||||
|
- id: official-web
|
||||||
|
label: "Official Website"
|
||||||
|
icon: "lucide:globe"
|
||||||
|
plans:
|
||||||
|
- title: "Basic Website"
|
||||||
|
description: "Build a professional online presence for small and medium-sized businesses."
|
||||||
|
price: "From RM3,999"
|
||||||
|
tagline: "Includes domain & hosting"
|
||||||
|
features:
|
||||||
|
- "Up to 5 pages (Home, About, Services, Contact, etc.)"
|
||||||
|
- "Responsive design (desktop / tablet / mobile)"
|
||||||
|
- "Basic SEO setup"
|
||||||
|
- "Contact form + map + social media links"
|
||||||
|
button:
|
||||||
|
label: "Contact Now"
|
||||||
|
|
||||||
|
- title: "Standard Website"
|
||||||
|
description: "Ideal for brand upgrades and content-rich businesses."
|
||||||
|
price: "From RM6,999"
|
||||||
|
# discount: "From RM4,999"
|
||||||
|
tagline: "Includes domain & hosting"
|
||||||
|
highlight: true
|
||||||
|
features:
|
||||||
|
- "Around 8–12 pages (Case Studies, Blog, Team, etc.)"
|
||||||
|
- "Custom brand styling + UI/UX optimization"
|
||||||
|
- "Lightweight CMS for content management"
|
||||||
|
- "Advanced SEO optimization and performance acceleration"
|
||||||
|
button:
|
||||||
|
label: "Book Standard Plan"
|
||||||
|
|
||||||
|
- title: "Enterprise Custom"
|
||||||
|
description: "Fully tailored visual, functional, and interactive experience."
|
||||||
|
price: "From RM15,000"
|
||||||
|
features:
|
||||||
|
- "Fully custom UI / animation design"
|
||||||
|
- "Multi-language support / client login module"
|
||||||
|
- "API / third-party system integrations"
|
||||||
|
- "Enhanced security and automated backup mechanism"
|
||||||
|
button:
|
||||||
|
label: "Book Enterprise Plan"
|
||||||
64
content/zh-CN/index.yml
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
seo:
|
||||||
|
title: "首页"
|
||||||
|
title: "Tootaio Studio"
|
||||||
|
description: "专为想要独特体验的品牌打造量身定制的数字产品。"
|
||||||
|
capabilities:
|
||||||
|
title: "我们的核心能力"
|
||||||
|
features:
|
||||||
|
- title: "网站定制开发"
|
||||||
|
description: "基于 Nuxt / Next 等现代框架,为企业打造高性能、可扩展且视觉出众的专属网站与后台系统。"
|
||||||
|
icon: mdi:web
|
||||||
|
- title: "软件与工具工程"
|
||||||
|
description: "为企业定制自动化工具、数据面板与业务流程系统,提高工作效率与可靠性。"
|
||||||
|
icon: mdi:cog-outline
|
||||||
|
- title: "游戏设计与开发"
|
||||||
|
description: "从 Game Jam 原型到商业发行,打造富有创意与技术深度的互动体验。"
|
||||||
|
icon: mdi:gamepad-variant-outline
|
||||||
|
- title: "互动媒体与宴会系统"
|
||||||
|
description: "为展会、活动与宴会定制实时交互内容与大型屏幕视觉展示。"
|
||||||
|
icon: mdi:monitor-dashboard
|
||||||
|
- title: "技术探索与评测"
|
||||||
|
description: "研究与评估前沿软硬件技术,保持创新优势与研发热情。"
|
||||||
|
icon: mdi:flask-outline
|
||||||
|
- title: "创意咨询与数字策略"
|
||||||
|
description: "为品牌与团队提供产品架构、数字化转型与长期技术规划咨询。"
|
||||||
|
icon: mdi:lightbulb-outline
|
||||||
|
featuredProjects:
|
||||||
|
title: "特色项目"
|
||||||
|
projects:
|
||||||
|
- title: "永中校友会官方网站"
|
||||||
|
description: "永平中学校友会官方网站的设计与开发项目,整体价值 RM28,000,由本工作室创办人无偿捐赠予校友会永久使用。"
|
||||||
|
image: "http://img.tootaio.com/i/2025/11/05/d9kurl.png"
|
||||||
|
demoLink: "https://yphsalumni.org"
|
||||||
|
- title: "留华生来华资料汇总"
|
||||||
|
description: "2022 年疫情期间,为马来西亚留学生开发的返校攻略网站。帮助 5000+ 名留学生顺利返校。并获得马来西亚外交部推荐。"
|
||||||
|
image: "http://img.tootaio.com/i/2025/11/05/d9kcma.png"
|
||||||
|
demoLink: "https://tootaio.github.io"
|
||||||
|
spotlight: true
|
||||||
|
- title: "光追"
|
||||||
|
description: "基于 Godot 引擎的 2023 年吉比特高校挑战赛参赛作品。"
|
||||||
|
image: "https://img.tootaio.com/i/2025/09/26/j2swgq.png"
|
||||||
|
techStack:
|
||||||
|
title: "Tech Stacks"
|
||||||
|
whyChooseUs:
|
||||||
|
title: "为什么选择我们"
|
||||||
|
description: "我们不仅编写代码——我们打造数字体验。"
|
||||||
|
features:
|
||||||
|
- title: "完全定制开发"
|
||||||
|
description: "我们从不使用模板。每一个网站、系统、游戏都从零设计与开发,确保风格、性能与体验完全符合品牌个性。"
|
||||||
|
icon: mdi:brush-variant
|
||||||
|
- title: "技术驱动,而非仅仅是设计"
|
||||||
|
description: "作为开发导向的团队,我们理解底层逻辑。从架构、性能、安全到交互动画,所有细节都由工程师主导优化。"
|
||||||
|
icon: mdi:cog-sync-outline
|
||||||
|
- title: "跨领域专长"
|
||||||
|
description: "我们横跨网站、游戏、工具与交互内容开发,让每个项目都能获得更广阔的技术整合思路。"
|
||||||
|
icon: mdi:gamepad-variant-outline
|
||||||
|
- title: "端到端一站式服务"
|
||||||
|
description: "从概念、原型、前端到部署与长期维护,我们全程负责,让客户专注业务,而非技术问题。"
|
||||||
|
icon: mdi:rocket-launch-outline
|
||||||
|
- title: "经验证的项目价值"
|
||||||
|
description: "我们曾为教育机构、品牌活动、独立游戏等开发高价值系统,实力可见。"
|
||||||
|
icon: mdi:chart-timeline-variant
|
||||||
|
- title: "面向未来"
|
||||||
|
description: "我们在研发自己的产品与工具,不止接案,也在打造未来生态。这代表我们具备持续创新与自我进化的能力。"
|
||||||
|
icon: mdi:lightbulb-on-outline
|
||||||
95
content/zh-CN/webDev.yml
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
seo:
|
||||||
|
title: "网页 / 网站定制开发"
|
||||||
|
description: "为您的品牌量身定做一套线上门户,增加产品曝光率与转化率。"
|
||||||
|
title: "网页 / 网站定制开发"
|
||||||
|
description: "为您的品牌量身定做一套线上门户,增加产品曝光率与转化率。"
|
||||||
|
|
||||||
|
remarks: "所有方案均含基础域名与服务器部署(首年)。若服务器负载过高,将协助迁移至更稳定的第三方环境。"
|
||||||
|
|
||||||
|
services:
|
||||||
|
- id: landing-page
|
||||||
|
label: "Landing Page"
|
||||||
|
icon: "mdi:cursor-default-click-outline"
|
||||||
|
plans:
|
||||||
|
- title: "基础版"
|
||||||
|
description: "适合个人、小型商家,快速上线单页展示网站。"
|
||||||
|
price: "RM899 起"
|
||||||
|
tagline: "含域名与服务器"
|
||||||
|
features:
|
||||||
|
- "单页面结构(1-3 屏)"
|
||||||
|
- "响应式设计(手机 / 平板 / 桌面)"
|
||||||
|
- "基本图文排版"
|
||||||
|
- "联系表单或 WhatsApp 按钮"
|
||||||
|
- "Google Analytics 整合"
|
||||||
|
- "1 次免费修改"
|
||||||
|
- "7 日内交付"
|
||||||
|
button:
|
||||||
|
label: "立即咨询"
|
||||||
|
|
||||||
|
- title: "标准版"
|
||||||
|
description: "为品牌与创业项目打造高转化页面。"
|
||||||
|
price: "RM1,599 起"
|
||||||
|
tagline: "含域名与服务器"
|
||||||
|
highlight: true
|
||||||
|
features:
|
||||||
|
- "多区块结构(4-6 屏)"
|
||||||
|
- "品牌定制风格与配色"
|
||||||
|
- "轻量动画与动效展示"
|
||||||
|
- "SEO 优化 + 加载性能优化"
|
||||||
|
- "整合追踪代码(GA / Pixel)"
|
||||||
|
- "2 次免费修改"
|
||||||
|
- "14 日内交付"
|
||||||
|
button:
|
||||||
|
label: "预约报价"
|
||||||
|
|
||||||
|
- title: "高级定制"
|
||||||
|
description: "为成熟品牌提供全面视觉与营销升级方案。"
|
||||||
|
price: "RM2,999 起"
|
||||||
|
features:
|
||||||
|
- "专属视觉设计与交互体验"
|
||||||
|
- "完整品牌风格系统"
|
||||||
|
- "A/B 测试与转化优化"
|
||||||
|
- "营销工具整合(邮件、统计、CRM)"
|
||||||
|
- "多语言 / 动态内容支持"
|
||||||
|
button:
|
||||||
|
label: "预约定制方案"
|
||||||
|
- id: official-web
|
||||||
|
label: "Official Website"
|
||||||
|
icon: "lucide:globe"
|
||||||
|
plans:
|
||||||
|
- title: "基础官网"
|
||||||
|
description: "为中小型企业建立专业在线形象。"
|
||||||
|
price: "RM3,999 起"
|
||||||
|
tagline: "含域名与服务器"
|
||||||
|
features:
|
||||||
|
- "最多 5 个页面(首页、关于、服务、联系等)"
|
||||||
|
- "响应式设计(桌面 / 平板 / 手机)"
|
||||||
|
- "基础 SEO 设置"
|
||||||
|
- "联系表单 + 地图 + 社交媒体链接"
|
||||||
|
button:
|
||||||
|
label: "立即咨询"
|
||||||
|
|
||||||
|
- title: "标准官网"
|
||||||
|
description: "适合品牌升级与内容扩展型企业。"
|
||||||
|
price: "RM6,999 起"
|
||||||
|
# discount: "RM4,999 起"
|
||||||
|
tagline: "含域名与服务器"
|
||||||
|
highlight: true
|
||||||
|
features:
|
||||||
|
- "约 8-12 个页面(案例、博客、团队等)"
|
||||||
|
- "品牌定制风格 + UI/UX 优化"
|
||||||
|
- "轻量 CMS 后台管理系统"
|
||||||
|
- "进阶 SEO 优化与性能加速"
|
||||||
|
button:
|
||||||
|
label: "预约标准方案"
|
||||||
|
|
||||||
|
- title: "企业定制"
|
||||||
|
description: "专属视觉、功能与交互体验整合。"
|
||||||
|
price: "RM15,000 起"
|
||||||
|
features:
|
||||||
|
- "完全定制 UI / 动效设计"
|
||||||
|
- "多语言支持 / 客户登录模块"
|
||||||
|
- "API / 第三方系统整合"
|
||||||
|
- "增强安全与自动备份机制"
|
||||||
|
button:
|
||||||
|
label: "预约企业方案"
|
||||||
28
docs/20251106/README.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# Tootaio Studio 工程与架构审计(2025-11-06)
|
||||||
|
|
||||||
|
本报告对当前基于 Nuxt 4.2.0 的网站项目进行一次性全方位工程化审计,涵盖架构、代码质量、性能、CI/CD、安全与可观测性,并提供可直接落地的改进方案与补丁。
|
||||||
|
|
||||||
|
## 摘要
|
||||||
|
- 架构总体清晰,采用 Nuxt Content + i18n + @nuxt/ui,模块职责明确(Low)。
|
||||||
|
- 发现内容 Schema 与实际 YAML 不一致(Critical),导致构建期校验风险与类型漂移。
|
||||||
|
- 缺少统一工程工具链(lint/typecheck/CI)(High)。
|
||||||
|
- 未配置 Route Rules/SWR 与静态资源缓存(Medium)。
|
||||||
|
- 运行时配置、监控与安全头未体系化(Medium)。
|
||||||
|
|
||||||
|
## 关键输出
|
||||||
|
- 补丁:
|
||||||
|
- 修正 `content.config.ts` Schema 与内容对齐(0001)。
|
||||||
|
- 增加 `package.json` 脚本(lint/typecheck/check)(0002)。
|
||||||
|
- 提供 GitHub Actions 工作流(lint → typecheck → build)(0003)。
|
||||||
|
- 可选:为首页与静态资源增加 `routeRules`(0004)。
|
||||||
|
- 提供 8 份专题文档:架构、质量、性能、CI/CD、安全、可观测性与路线图。
|
||||||
|
|
||||||
|
## 执行指引
|
||||||
|
1) 审阅 `docs/20251106/` 文档;
|
||||||
|
2) 依次应用补丁(位于 `docs/20251106/patches/`):
|
||||||
|
- `git apply docs/20251106/patches/0001-content-config-align.patch`
|
||||||
|
- `git apply docs/20251106/patches/0002-package-scripts.patch`
|
||||||
|
- `git apply docs/20251106/patches/0003-github-actions-ci.patch`
|
||||||
|
- (可选)`git apply docs/20251106/patches/0004-nuxt-route-rules.patch`
|
||||||
|
3) 本地验证:`pnpm i && pnpm check && pnpm preview`。
|
||||||
|
|
||||||
52
docs/20251106/architecture.md
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
# 架构结构与改进方案
|
||||||
|
|
||||||
|
本文审计 Nuxt 4.2.0 架构与模块使用,指出结构异味并提供可落地改进。
|
||||||
|
|
||||||
|
## 现状评估
|
||||||
|
- 模块与目录(Low)
|
||||||
|
- `app/`(pages/layouts/composables/utils/assets)职责清晰。
|
||||||
|
- `content/` 与 `content.config.ts` 使用 zod 定义 Schema,利于类型驱动内容。
|
||||||
|
- `i18n/` 采用 `locales/<locale>` 结构;`no_prefix` 策略与当前站点定位一致。
|
||||||
|
- Nuxt 4 特性使用(Medium)
|
||||||
|
- 自动导入:已使用(如 `useLocalizedCollection`)。建议将文件命名统一为 `useXxx.ts` 便于识别。
|
||||||
|
- Runtime Config:未使用。建议引入 `runtimeConfig` 管理外部域名、监控 DSN 等。
|
||||||
|
- Nitro Route Rules/缓存:未配置。建议为首页与静态资源设置 `prerender`+`swr` 与长缓存。
|
||||||
|
- 设计风险(Critical)
|
||||||
|
- `content.config.ts` 的 Index Schema 与 YAML 不一致:缺少 `title/description/seo` 字段;`icon/highlight/spotlight` 约束与内容不符。
|
||||||
|
|
||||||
|
## 改进策略
|
||||||
|
1) 内容 Schema 对齐(Critical)
|
||||||
|
- 为 Index 增加 `title/description/seo`;将 `icon` 设为可选,将 `highlight/spotlight` 设为可选并默认 `false`。
|
||||||
|
- 参见补丁:`0001-content-config-align.patch`。
|
||||||
|
2) 运行时配置(Medium)
|
||||||
|
- 在 `nuxt.config.ts` 中:
|
||||||
|
```ts
|
||||||
|
export default defineNuxtConfig({
|
||||||
|
runtimeConfig: {
|
||||||
|
sentryDsn: '',
|
||||||
|
public: { assetBase: 'https://img.tootaio.com' },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
- 在代码中读取 `useRuntimeConfig()`,避免硬编码外链与密钥。
|
||||||
|
3) 路由规则与缓存(Medium)
|
||||||
|
- 为首页预渲染并设置 SWR,为构建产物设置 immutable 缓存:
|
||||||
|
```ts
|
||||||
|
export default defineNuxtConfig({
|
||||||
|
routeRules: {
|
||||||
|
'/': { prerender: true, swr: 3600 },
|
||||||
|
'/_nuxt/**': {
|
||||||
|
headers: { 'cache-control': 'public, max-age=31536000, immutable' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
- 参见补丁:`0004-nuxt-route-rules.patch`。
|
||||||
|
4) 组合式命名与分层(Low)
|
||||||
|
- 将 `app/composables/LocalizedCollection.ts` 重命名为 `useLocalizedCollection.ts`。
|
||||||
|
- 保持 composable 只做“取数+组装”,复杂 UI/状态拆到组件/Pinia(若引入)。
|
||||||
|
|
||||||
|
## 与 Nuxt 4.2.0 兼容性
|
||||||
|
- 现有 `compatibilityDate: '2025-07-15'` 符合要求。
|
||||||
|
- 上述配置属于稳定 API:`routeRules/runtimeConfig/auto-import`,安全可用。
|
||||||
|
|
||||||
40
docs/20251106/ci-cd.md
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# CI/CD 设计与部署方案
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
提供从代码质量到产物构建的自动化流程:lint → typecheck → build。
|
||||||
|
|
||||||
|
## GitHub Actions(推荐)
|
||||||
|
- 工作流文件参见补丁 `0003-github-actions-ci.patch`,关键步骤:
|
||||||
|
- 使用 `actions/setup-node@v4` + `corepack enable`;
|
||||||
|
- 安装依赖:`pnpm i --frozen-lockfile`;
|
||||||
|
- 执行 `pnpm lint && pnpm typecheck && pnpm build`。
|
||||||
|
|
||||||
|
## 部署对比
|
||||||
|
- Vercel(推荐)
|
||||||
|
- 优点:零配置、内置 CDN、适配 Nuxt/Nitro;Preview 环境完善。
|
||||||
|
- 缺点:免费额度受限;对长时间 SSR 任务需商用套餐。
|
||||||
|
- Docker(自管)
|
||||||
|
- 优点:环境一致性高;更灵活接入内网服务。
|
||||||
|
- 缺点:需自建 CI/CD 与监控;维护成本更高。
|
||||||
|
- 自建 Node + Nginx
|
||||||
|
- 优点:成本可控;传统可见性强。
|
||||||
|
- 缺点:手工配置多;需要额外缓存/CDN 配合。
|
||||||
|
|
||||||
|
## Dockerfile(示例)
|
||||||
|
```dockerfile
|
||||||
|
FROM node:20-alpine AS build
|
||||||
|
WORKDIR /app
|
||||||
|
COPY . .
|
||||||
|
RUN corepack enable && pnpm i --frozen-lockfile && pnpm build
|
||||||
|
|
||||||
|
FROM node:20-alpine
|
||||||
|
WORKDIR /app
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
COPY --from=build /app/.output ./.output
|
||||||
|
EXPOSE 3000
|
||||||
|
CMD ["node","./.output/server/index.mjs"]
|
||||||
|
```
|
||||||
|
|
||||||
|
## 环境变量与密钥
|
||||||
|
- 在 CI 中设置 `NITRO_PRESET`、`SENTRY_DSN` 等;Nuxt 通过 `runtimeConfig` 注入,避免硬编码。
|
||||||
|
|
||||||
41
docs/20251106/code-quality.md
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# 代码规范与质量分析
|
||||||
|
|
||||||
|
## 主要发现
|
||||||
|
- 类型定义(High)
|
||||||
|
- `content.config.ts` 缺少 Index 页的 `title/description/seo`,导致 `page?.title` 等缺少类型提示;特定字段(`icon/highlight/spotlight`)与 YAML 不一致。
|
||||||
|
- 组合式命名(Medium)
|
||||||
|
- `LocalizedCollection.ts` 建议改为 `useLocalizedCollection.ts`,与 Nuxt 习惯保持一致,便于检索与自动导入心智统一。
|
||||||
|
|
||||||
|
## 统一约定
|
||||||
|
- 文件与目录
|
||||||
|
- composables:`useXxx.ts`(示例:`useLocalizedCollection.ts`)。
|
||||||
|
- 页面:`pages/webDev.vue`(帕斯卡命名不用于路由页)。
|
||||||
|
- 工具:`utils/some-helper.ts`,避免与 composables 混用。
|
||||||
|
- 代码风格
|
||||||
|
- TypeScript、ESM、Vue SFC `<script setup lang="ts">`。
|
||||||
|
- 缩进 2 空格;组件/组合式函数采用明确的返回类型。
|
||||||
|
- i18n 使用嵌套点式键:`$t('index.featuredProjects.viewDemo')`。
|
||||||
|
- 提交规范
|
||||||
|
- 参考历史:`feat(content): ...`、`refactor(ui): ...`、`feat(pages): ...`。
|
||||||
|
- 分支:`feat/<desc>`、`fix/<desc>`。
|
||||||
|
|
||||||
|
## 重构建议(示例)
|
||||||
|
1) 内容类型对齐(Critical)
|
||||||
|
- 见补丁 `0001-content-config-align.patch`。
|
||||||
|
2) 工程脚本完善(High)
|
||||||
|
- 新增脚本:`lint/typecheck/check`,见补丁 `0002-package-scripts.patch`。
|
||||||
|
3) 组合式命名(Medium)
|
||||||
|
- 将 `LocalizedCollection.ts` 重命名为 `useLocalizedCollection.ts`(建议)。
|
||||||
|
|
||||||
|
## 示例:更严格的返回类型
|
||||||
|
```ts
|
||||||
|
export function useLocalizedCollection<B extends string>(baseName: B) {
|
||||||
|
const { locale } = useI18n()
|
||||||
|
type LocalizedKey = keyof { [K in keyof Collections as K extends `${B}_${string}` ? K : never]: any }
|
||||||
|
type Schema = Collections[LocalizedKey]
|
||||||
|
return useAsyncData<Schema | null>(`${baseName}-${locale.value}`, async () => {
|
||||||
|
// ...
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
40
docs/20251106/observability.md
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# 监控、日志与可观测性
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
构建可追踪错误、性能瓶颈与用户关键路径的最小监控闭环。
|
||||||
|
|
||||||
|
## Sentry(推荐)
|
||||||
|
1) 配置 `runtimeConfig.sentryDsn`;
|
||||||
|
2) 插件(示例,待引入依赖后启用):
|
||||||
|
```ts
|
||||||
|
// plugins/sentry.client.ts
|
||||||
|
export default defineNuxtPlugin(() => {
|
||||||
|
const { public: pub, sentryDsn } = useRuntimeConfig()
|
||||||
|
if (!sentryDsn) return
|
||||||
|
// @ts-ignore: add SDK in deps later
|
||||||
|
const Sentry = (window as any).Sentry
|
||||||
|
if (Sentry) {
|
||||||
|
Sentry.init({ dsn: sentryDsn, tracesSampleRate: 0.1 })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## 日志与错误边界
|
||||||
|
- 服务器:使用 Nitro 默认日志(生产应接入集中式日志,如 CloudWatch/ELK)。
|
||||||
|
- 客户端:全局错误捕获(`app:error` 事件)上报到 Sentry/自建网关。
|
||||||
|
|
||||||
|
## Web Vitals(可选)
|
||||||
|
收集 LCP/CLS/INP 等指标(示例):
|
||||||
|
```ts
|
||||||
|
// plugins/web-vitals.client.ts
|
||||||
|
import { onCLS, onINP, onLCP } from 'web-vitals'
|
||||||
|
export default defineNuxtPlugin(() => {
|
||||||
|
const send = (metric: any) => navigator.sendBeacon('/vitals', JSON.stringify(metric))
|
||||||
|
onLCP(send); onCLS(send); onINP(send)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dashboard 路线
|
||||||
|
- 第一步:仅错误计数与关键事务。
|
||||||
|
- 第二步:接口慢查询、资源错误分布、版本追踪(commit SHA)。
|
||||||
|
|
||||||
89
docs/20251106/patches/0001-content-config-align.patch
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
diff --git a/content.config.ts b/content.config.ts
|
||||||
|
--- a/content.config.ts
|
||||||
|
+++ b/content.config.ts
|
||||||
|
@@
|
||||||
|
const defineIndexSchema = () =>
|
||||||
|
- z.object({
|
||||||
|
- capabilities: z.object({
|
||||||
|
- title: z.string(),
|
||||||
|
- features: z.array(
|
||||||
|
- z.object({
|
||||||
|
- title: z.string(),
|
||||||
|
- description: z.string(),
|
||||||
|
- icon: z.string(),
|
||||||
|
- })
|
||||||
|
- ),
|
||||||
|
- }),
|
||||||
|
- featuredProjects: z.object({
|
||||||
|
- title: z.string(),
|
||||||
|
- projects: z.array(
|
||||||
|
- z.object({
|
||||||
|
- title: z.string(),
|
||||||
|
- description: z.string(),
|
||||||
|
- image: z.string(),
|
||||||
|
- demoLink: z.string(),
|
||||||
|
- highlight: z.boolean(),
|
||||||
|
- spotlight: z.boolean(),
|
||||||
|
- })
|
||||||
|
- ),
|
||||||
|
- }),
|
||||||
|
- techStack: z.object({
|
||||||
|
- title: z.string(),
|
||||||
|
- }),
|
||||||
|
- whyChooseUs: z.object({
|
||||||
|
- title: z.string(),
|
||||||
|
- features: z.array(
|
||||||
|
- z.object({
|
||||||
|
- title: z.string(),
|
||||||
|
- description: z.string(),
|
||||||
|
- icon: z.string(),
|
||||||
|
- })
|
||||||
|
- ),
|
||||||
|
- }),
|
||||||
|
- });
|
||||||
|
+ z.object({
|
||||||
|
+ title: z.string(),
|
||||||
|
+ description: z.string(),
|
||||||
|
+ seo: z
|
||||||
|
+ .object({
|
||||||
|
+ title: z.string().optional(),
|
||||||
|
+ description: z.string().optional(),
|
||||||
|
+ })
|
||||||
|
+ .optional(),
|
||||||
|
+ capabilities: z.object({
|
||||||
|
+ title: z.string(),
|
||||||
|
+ features: z.array(
|
||||||
|
+ z.object({
|
||||||
|
+ title: z.string(),
|
||||||
|
+ description: z.string(),
|
||||||
|
+ icon: z.string().optional(),
|
||||||
|
+ })
|
||||||
|
+ ),
|
||||||
|
+ }),
|
||||||
|
+ featuredProjects: z.object({
|
||||||
|
+ title: z.string(),
|
||||||
|
+ projects: z.array(
|
||||||
|
+ z.object({
|
||||||
|
+ title: z.string(),
|
||||||
|
+ description: z.string(),
|
||||||
|
+ image: z.string(),
|
||||||
|
+ demoLink: z.string().url().optional(),
|
||||||
|
+ highlight: z.boolean().optional().default(false),
|
||||||
|
+ spotlight: z.boolean().optional().default(false),
|
||||||
|
+ })
|
||||||
|
+ ),
|
||||||
|
+ }),
|
||||||
|
+ techStack: z.object({
|
||||||
|
+ title: z.string(),
|
||||||
|
+ }),
|
||||||
|
+ whyChooseUs: z.object({
|
||||||
|
+ title: z.string(),
|
||||||
|
+ features: z.array(
|
||||||
|
+ z.object({
|
||||||
|
+ title: z.string(),
|
||||||
|
+ description: z.string(),
|
||||||
|
+ icon: z.string().optional(),
|
||||||
|
+ })
|
||||||
|
+ ),
|
||||||
|
+ }),
|
||||||
|
+ });
|
||||||
39
docs/20251106/patches/0002-package-scripts.patch
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
diff --git a/package.json b/package.json
|
||||||
|
--- a/package.json
|
||||||
|
+++ b/package.json
|
||||||
|
@@
|
||||||
|
"scripts": {
|
||||||
|
- "build": "nuxt build",
|
||||||
|
- "dev": "nuxt dev",
|
||||||
|
- "generate": "nuxt generate",
|
||||||
|
- "preview": "nuxt preview",
|
||||||
|
- "postinstall": "nuxt prepare"
|
||||||
|
+ "build": "nuxt build",
|
||||||
|
+ "dev": "nuxt dev",
|
||||||
|
+ "generate": "nuxt generate",
|
||||||
|
+ "preview": "nuxt preview",
|
||||||
|
+ "postinstall": "nuxt prepare",
|
||||||
|
+ "typecheck": "nuxt typecheck",
|
||||||
|
+ "lint": "eslint --ext .ts,.js,.mjs,.vue --ignore-path .gitignore .",
|
||||||
|
+ "lint:fix": "pnpm lint -- --fix",
|
||||||
|
+ "check": "pnpm lint && pnpm typecheck && pnpm build"
|
||||||
|
},
|
||||||
|
@@
|
||||||
|
"dependencies": {
|
||||||
|
"@iconify-json/mdi": "^1.2.3",
|
||||||
|
"@nuxt/content": "3.8.0",
|
||||||
|
"@nuxt/eslint": "1.10.0",
|
||||||
|
"@nuxt/ui": "4.1.0",
|
||||||
|
"@nuxtjs/i18n": "10.2.0",
|
||||||
|
"@nuxtjs/seo": "3.2.2",
|
||||||
|
"better-sqlite3": "^12.4.1",
|
||||||
|
"eslint": "^9.39.0",
|
||||||
|
"nuxt": "^4.2.0",
|
||||||
|
"typescript": "^5.9.3",
|
||||||
|
"vue": "^3.5.22",
|
||||||
|
"vue-router": "^4.6.3"
|
||||||
|
- }
|
||||||
|
+ },
|
||||||
|
+ "engines": {
|
||||||
|
+ "node": ">=18.20.0"
|
||||||
|
+ }
|
||||||
34
docs/20251106/patches/0003-github-actions-ci.patch
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
|
||||||
|
new file mode 100644
|
||||||
|
--- /dev/null
|
||||||
|
+++ b/.github/workflows/ci.yml
|
||||||
|
@@
|
||||||
|
name: CI
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ main ]
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
cache: 'pnpm'
|
||||||
|
- name: Enable Corepack
|
||||||
|
run: corepack enable
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm i --frozen-lockfile
|
||||||
|
- name: Lint
|
||||||
|
run: pnpm lint
|
||||||
|
- name: Typecheck
|
||||||
|
run: pnpm typecheck
|
||||||
|
- name: Build
|
||||||
|
run: pnpm build
|
||||||
50
docs/20251106/patches/0004-nuxt-route-rules.patch
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
diff --git a/nuxt.config.ts b/nuxt.config.ts
|
||||||
|
--- a/nuxt.config.ts
|
||||||
|
+++ b/nuxt.config.ts
|
||||||
|
@@
|
||||||
|
export default defineNuxtConfig({
|
||||||
|
compatibilityDate: "2025-07-15",
|
||||||
|
devtools: { enabled: true },
|
||||||
|
modules: [
|
||||||
|
"@nuxt/content",
|
||||||
|
"@nuxt/ui",
|
||||||
|
"@nuxt/eslint",
|
||||||
|
"@nuxtjs/i18n",
|
||||||
|
"@nuxtjs/seo",
|
||||||
|
],
|
||||||
|
css: ["@/assets/css/main.css"],
|
||||||
|
app: {
|
||||||
|
head: {
|
||||||
|
titleTemplate: "%s - Tootaio Studio",
|
||||||
|
meta: [
|
||||||
|
{ name: "viewport", content: "width=device-width, initial-scale=1" },
|
||||||
|
{ charset: "utf-8" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
@@
|
||||||
|
seo: {
|
||||||
|
meta: {
|
||||||
|
title: DEFAULT_SEO.title,
|
||||||
|
description: DEFAULT_SEO.description,
|
||||||
|
keywords: DEFAULT_SEO.keywords,
|
||||||
|
ogTitle: DEFAULT_SEO.title,
|
||||||
|
ogDescription: DEFAULT_SEO.description,
|
||||||
|
ogImage: DEFAULT_SEO.image,
|
||||||
|
ogUrl: DEFAULT_SEO.url,
|
||||||
|
twitterCard: "summary_large_image",
|
||||||
|
twitterTitle: DEFAULT_SEO.title,
|
||||||
|
twitterDescription: DEFAULT_SEO.description,
|
||||||
|
twitterImage: DEFAULT_SEO.image,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
site: {
|
||||||
|
url: "https://tootaio.com",
|
||||||
|
},
|
||||||
|
+ routeRules: {
|
||||||
|
+ "/": { prerender: true, swr: 3600 },
|
||||||
|
+ "/_nuxt/**": {
|
||||||
|
+ headers: { "cache-control": "public, max-age=31536000, immutable" },
|
||||||
|
+ },
|
||||||
|
+ },
|
||||||
|
});
|
||||||
33
docs/20251106/performance.md
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# 性能诊断与优化建议
|
||||||
|
|
||||||
|
## 现状与影响
|
||||||
|
- SSR/CSR(Medium):默认 SSR,未针对首页静态化或增量缓存配置,变化不频繁的内容可降本提速。
|
||||||
|
- 资源缓存(Medium):未设置构建产物(`/_nuxt/**`)的长期缓存与 immutable,重复访问浪费带宽。
|
||||||
|
- 图片与外链(Low):OG 图与背景图使用外部域名,建议统一通过 CDN 域名与缓存规则管理。
|
||||||
|
|
||||||
|
## Nuxt 4 定向优化
|
||||||
|
1) Route Rules 与 SWR(Medium)
|
||||||
|
- 预渲染首页并启用 SWR:降低首屏 TTFB。
|
||||||
|
- 长缓存构建产物:
|
||||||
|
```ts
|
||||||
|
export default defineNuxtConfig({
|
||||||
|
routeRules: {
|
||||||
|
'/': { prerender: true, swr: 3600 },
|
||||||
|
'/_nuxt/**': { headers: { 'cache-control': 'public, max-age=31536000, immutable' } },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
- 参见补丁:`0004-nuxt-route-rules.patch`。
|
||||||
|
2) 代码分割与懒加载(Low)
|
||||||
|
- 仅在首屏需要时挂载大型组件(如轮播);通过 `v-if` 配合 `onMounted` 或 `client-only` 降低 SSR 负载。
|
||||||
|
3) 构建与依赖(Low)
|
||||||
|
- 保持 `pnpm-lock.yaml` 锁定;CI 使用 `pnpm i --frozen-lockfile` 保证可重复构建。
|
||||||
|
4) 图片与静态资源(Medium)
|
||||||
|
- 将站点图片统一迁到自有 CDN(如 Cloudflare/Vercel Assets),配置缓存与压缩。
|
||||||
|
- 如未来引入 `@nuxt/image`,优先使用自动格式与尺寸。
|
||||||
|
|
||||||
|
## 部署建议
|
||||||
|
- Node/Nitro(通用):适合 SSR + SWR 与渐进更新。
|
||||||
|
- Edge(部份路由):静态资源 + 首页可下放至边缘,注意依赖兼容性。
|
||||||
|
- Static/Generate:若大部分页面静态,可考虑 `pnpm generate` 并配合 CDN。
|
||||||
|
|
||||||
34
docs/20251106/roadmap.md
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# 分阶段工程化改进路线图
|
||||||
|
|
||||||
|
## 阶段一:基础规范化(1–2 天)
|
||||||
|
- 目标:消除 Critical/High 风险,建立最小工程基线。
|
||||||
|
- 动作:
|
||||||
|
- 应用补丁 0001/0002;
|
||||||
|
- 启用 `pnpm check` 在本地与 CI;
|
||||||
|
- 修复内容与 Schema 校验。
|
||||||
|
- 验证:`pnpm check` 全绿;手测多语言与内容渲染。
|
||||||
|
|
||||||
|
## 阶段二:模块解耦(2–3 天)
|
||||||
|
- 目标:清晰分层,降低耦合与心智成本。
|
||||||
|
- 动作:
|
||||||
|
- 统一 composables 命名为 `useXxx.ts`;
|
||||||
|
- 将硬编码外链迁到 `runtimeConfig.public.assetBase`;
|
||||||
|
- 视需要引入 Pinia 拆分跨页状态。
|
||||||
|
- 验证:路由与数据流稳定;影响面与回归点有测试用例(增量引入 Vitest)。
|
||||||
|
|
||||||
|
## 阶段三:自动化部署(1–2 天)
|
||||||
|
- 目标:PR 即可验证、主干可随时发布。
|
||||||
|
- 动作:
|
||||||
|
- 应用补丁 0003(GitHub Actions);
|
||||||
|
- 配置 Vercel/自建 Docker 部署;
|
||||||
|
- 产出构建产物与环境变量清单。
|
||||||
|
- 验证:PR 触发 CI,产物可预览。
|
||||||
|
|
||||||
|
## 阶段四:性能与监控(2–4 天)
|
||||||
|
- 目标:稳定响应、端到端可观测。
|
||||||
|
- 动作:
|
||||||
|
- 应用补丁 0004(Route Rules/SWR);
|
||||||
|
- 引入 Sentry(DSN + 插件);
|
||||||
|
- 图片与静态资源走 CDN 与长缓存。
|
||||||
|
- 验证:关键页面首屏数据、错误率与 Web Vitals。
|
||||||
|
|
||||||
33
docs/20251106/security.md
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# 安全审计与防护建议
|
||||||
|
|
||||||
|
## 发现与等级
|
||||||
|
- 外链与配置(Medium)
|
||||||
|
- 外部图片/链接硬编码在源码中;建议迁移到 `runtimeConfig.public.assetBase` 并集中管理。
|
||||||
|
- 内容与校验(High)
|
||||||
|
- Content Schema 与数据不一致会在构建期暴露为错误或导致运行时空值处理不当。
|
||||||
|
- 依赖与脚本(Low)
|
||||||
|
- 缺少 `typecheck`/`lint` 脚本,易让问题晚发现。
|
||||||
|
|
||||||
|
## 防护清单
|
||||||
|
1) 运行时配置(Medium)
|
||||||
|
```ts
|
||||||
|
export default defineNuxtConfig({
|
||||||
|
runtimeConfig: {
|
||||||
|
// 仅服务器可见
|
||||||
|
sentryDsn: '',
|
||||||
|
// 客户端可见
|
||||||
|
public: {
|
||||||
|
assetBase: 'https://img.tootaio.com',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
2) 安全头(建议由网关/Nginx 配置)(Low)
|
||||||
|
- `Content-Security-Policy`(允许必要的域名);
|
||||||
|
- `X-Content-Type-Options: nosniff`、`Referrer-Policy: strict-origin-when-cross-origin`;
|
||||||
|
- 静态资源 `Cache-Control: immutable`。
|
||||||
|
3) 敏感信息检查(Medium)
|
||||||
|
- 使用 `.gitignore` 与 CI 检查(如 `trufflehog`/`gitleaks`,后续可引入)。
|
||||||
|
4) 依赖与锁定(Low)
|
||||||
|
- 全量使用 `pnpm-lock.yaml`,CI 中启用 `--frozen-lockfile` 保证可复现构建。
|
||||||
|
|
||||||
188
docs/20251107/security-audit.md
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
---
|
||||||
|
title: 安全审计报告(Tootaio Studio 网站)
|
||||||
|
description: 开源发布前的安全评估与加固建议与清单
|
||||||
|
lastUpdated: 2025-11-07
|
||||||
|
---
|
||||||
|
|
||||||
|
# 概览(Summary)
|
||||||
|
|
||||||
|
- 架构为 Nuxt 4 + @nuxt/content 的前端站点,无自建服务端 API/上传功能,攻击面小。若开源,整体安全风险低。
|
||||||
|
- 主要关注点:外部资源使用 HTTP、窗口打开的 `opener` 风险、生产环境 DevTools 启用、缺少统一安全响应头、依赖冗余与未来内容来源治理。
|
||||||
|
|
||||||
|
## 审计范围
|
||||||
|
|
||||||
|
- 配置与依赖:`nuxt.config.ts`、`package.json`、`.env*`、`.gitignore`。
|
||||||
|
- 前端页面与可执行逻辑:`app/` 下的页面、布局、可组合函数(composables)。
|
||||||
|
- 内容与多语言:`content/**.yml` 与 `i18n/**.json`。
|
||||||
|
- 文档与脚本:`docs/` 内建议信息。
|
||||||
|
|
||||||
|
# 结论与优先级
|
||||||
|
|
||||||
|
- 风险等级:低-中(Low–Medium)。不存在明显的密钥泄露或远程代码执行面。
|
||||||
|
- 高价值、低成本修复项(建议开源前完成):
|
||||||
|
1) 将所有外部资源统一为 HTTPS。
|
||||||
|
2) 生产环境禁用 DevTools。
|
||||||
|
3) `window.open` 显式使用 `noopener,noreferrer`。
|
||||||
|
4) 为外链/图片增加基础协议白名单校验(http/https)。
|
||||||
|
5) 通过网关或 Nitro `routeRules` 添加基础安全响应头与静态资源缓存策略。
|
||||||
|
6) 精简未使用依赖(如未使用 `better-sqlite3`)。
|
||||||
|
|
||||||
|
# 关键发现与证据(Evidence)
|
||||||
|
|
||||||
|
- 公开运行时配置(非敏感):
|
||||||
|
- `nuxt.config.ts:22` 暴露 `runtimeConfig.public.whatsappNumber`(按设计公开,无敏感性)。
|
||||||
|
|
||||||
|
- DevTools 在生产可能启用:
|
||||||
|
- `nuxt.config.ts:14` `devtools: { enabled: true }`,建议生产禁用。
|
||||||
|
|
||||||
|
- 外链/图片使用了 HTTP(混合内容与篡改风险):
|
||||||
|
- `app/pages/index.vue:83-92` 背景图 `http://img.tootaio.com/...`
|
||||||
|
- `content/en-US/index.yml:25,29` 与 `content/zh-CN/index.yml:31,35` 项目图片使用 `http://`。
|
||||||
|
|
||||||
|
- 外链打开策略:
|
||||||
|
- `app/pages/index.vue:37-38` 使用 `target="_blank"` 已含 `rel="noopener"`,建议补充 `noreferrer`。
|
||||||
|
- `app/composables/WhatsAppMsgSender.ts:11` 的 `window.open` 未显式 `noopener,noreferrer`。
|
||||||
|
|
||||||
|
- 统一安全头缺失:
|
||||||
|
- 未在 `nuxt.config.ts` 配置 `routeRules` 的安全头(可在网关/Nginx 层或 Nitro 层补充)。
|
||||||
|
|
||||||
|
- 依赖冗余:
|
||||||
|
- `package.json:19` 引入了 `better-sqlite3`,当前项目未使用,建议移除以降低供应链与构建复杂度。
|
||||||
|
|
||||||
|
- 非安全但会影响构建的细节:
|
||||||
|
- `content.config.ts:2` 大小写引用与实际文件名不一致(Linux 下可能报错):应从 `./app/schemas/PricingPlanSchema` 调整为 `./app/schemas/pricingPlanSchema`。
|
||||||
|
|
||||||
|
- 环境文件治理:
|
||||||
|
- `.env` 已被 `.gitignore` 忽略(`.gitignore:22`),且内容仅有公开号码(`.env:1`)。建议继续保持从未提交到历史。
|
||||||
|
|
||||||
|
# 修复与加固建议(Actionable)
|
||||||
|
|
||||||
|
## 开源前必须(High Priority)
|
||||||
|
|
||||||
|
- 统一使用 HTTPS 资源
|
||||||
|
- 将 `http://img.tootaio.com/...` 统一替换为 `https://img.tootaio.com/...`。
|
||||||
|
- 建议将资源基址抽离到 `runtimeConfig.public.assetBase` 并集中管理,减少散落硬编码。
|
||||||
|
|
||||||
|
- 生产禁用 DevTools
|
||||||
|
- 将 `devtools` 改为:`devtools: { enabled: process.env.NODE_ENV !== 'production' }`。
|
||||||
|
|
||||||
|
- `window.open` 加固
|
||||||
|
- 修改为:`window.open(url, '_blank', 'noopener,noreferrer')` 或在新窗口上设置 `opener = null`。
|
||||||
|
|
||||||
|
- 外链与图片的协议白名单
|
||||||
|
- 在渲染外链/图片前校验 URL 协议,只允许 `http:` 与 `https:`,避免 `javascript:`、`data:` 等危险 scheme。
|
||||||
|
|
||||||
|
- 安全响应头(建议由网关/Nginx 配置,或用 Nitro routeRules)
|
||||||
|
- 最小集:
|
||||||
|
- `Content-Security-Policy`(仅放行必要域名,样例见下)。
|
||||||
|
- `X-Content-Type-Options: nosniff`
|
||||||
|
- `Referrer-Policy: strict-origin-when-cross-origin`
|
||||||
|
- `Permissions-Policy`(按需收紧)
|
||||||
|
- 静态资源缓存:`/_nuxt/**` 设置 `Cache-Control: public, max-age=31536000, immutable`。
|
||||||
|
|
||||||
|
## 建议完成(Medium Priority)
|
||||||
|
|
||||||
|
- 依赖精简
|
||||||
|
- 若未用到 `better-sqlite3`,从 `package.json` 移除并更新锁文件。
|
||||||
|
|
||||||
|
- `.gitignore` 更严格忽略 env 变体
|
||||||
|
- 将当前注释掉的 `.env.*` 忽略规则启用,并保留示例白名单:
|
||||||
|
- 忽略:`.env`、`.env.*`
|
||||||
|
- 白名单:`!.env.sample`
|
||||||
|
|
||||||
|
- 文件名大小写一致性
|
||||||
|
- `content.config.ts` 引用改为实际文件名大小写以避免跨平台问题。
|
||||||
|
|
||||||
|
## 可选增强(Nice-to-have)
|
||||||
|
|
||||||
|
- 在 CI 中启用:
|
||||||
|
- `pnpm install --frozen-lockfile`
|
||||||
|
- 依赖与漏洞审计(Dependabot / `pnpm audit --prod`)
|
||||||
|
- Secrets 扫描(`gitleaks`/`trufflehog`)
|
||||||
|
|
||||||
|
- HSTS 与全站 HTTPS
|
||||||
|
- 前置网关开启 HSTS,并确保所有外链与资源均可通过 HTTPS 访问。
|
||||||
|
|
||||||
|
# 附录:建议配置片段
|
||||||
|
|
||||||
|
## 1) Nitro routeRules(示例)
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// nuxt.config.ts
|
||||||
|
export default defineNuxtConfig({
|
||||||
|
// ...
|
||||||
|
routeRules: {
|
||||||
|
'/**': {
|
||||||
|
headers: {
|
||||||
|
'Content-Security-Policy': [
|
||||||
|
"default-src 'self'",
|
||||||
|
"script-src 'self'",
|
||||||
|
"style-src 'self' 'unsafe-inline'",
|
||||||
|
"img-src 'self' https://img.tootaio.com data:",
|
||||||
|
"connect-src 'self'",
|
||||||
|
"frame-ancestors 'self'",
|
||||||
|
'upgrade-insecure-requests',
|
||||||
|
].join('; '),
|
||||||
|
'X-Content-Type-Options': 'nosniff',
|
||||||
|
'Referrer-Policy': 'strict-origin-when-cross-origin',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'/_nuxt/**': {
|
||||||
|
headers: { 'cache-control': 'public, max-age=31536000, immutable' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
请按实际依赖域名精简 CSP,特别是 `img-src`、`connect-src`。
|
||||||
|
|
||||||
|
## 2) `window.open` 安全用法(示例)
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const win = window.open(url, '_blank', 'noopener,noreferrer')
|
||||||
|
// 或者:
|
||||||
|
const win = window.open(url, '_blank')
|
||||||
|
if (win) win.opener = null
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3) 外链协议白名单(示意)
|
||||||
|
|
||||||
|
```ts
|
||||||
|
function isSafeHttpUrl(href: string) {
|
||||||
|
try {
|
||||||
|
const u = new URL(href)
|
||||||
|
return u.protocol === 'http:' || u.protocol === 'https:'
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用时:
|
||||||
|
// <UButton v-if="item.demoLink && isSafeHttpUrl(item.demoLink)" :href="item.demoLink" ... />
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4) DevTools 生产禁用
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export default defineNuxtConfig({
|
||||||
|
devtools: { enabled: process.env.NODE_ENV !== 'production' },
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5) `.gitignore` 建议
|
||||||
|
|
||||||
|
```gitignore
|
||||||
|
# Local env files
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.sample
|
||||||
|
```
|
||||||
|
|
||||||
|
# 附注(Non-security 但建议修复)
|
||||||
|
|
||||||
|
- `content.config.ts:2` 的大小写引用问题:应改为 `./app/schemas/pricingPlanSchema`(与实际文件名一致),避免在大小写敏感的文件系统上构建失败。
|
||||||
|
|
||||||
|
# 下一步
|
||||||
|
|
||||||
|
- 如需,我可以基于本报告直接提交最小化补丁(HTTPS 资源替换、`devtools` 切换、`window.open` 加固、`routeRules` 安全头、`.gitignore` 调整、文件名大小写修复),并附上验证与回退说明。
|
||||||
|
|
||||||
147
docs/CODEX-PROMPT.md
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
你现在是 **Tootaio Studio 项目的高级工程化顾问兼代码审计师**。
|
||||||
|
你的职责是对这个基于 **Nuxt 4.2.0** 的项目进行一次**全方位工程级分析与优化规划**,
|
||||||
|
以生成一份可以直接落地的**架构与工程化报告**。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、项目背景
|
||||||
|
|
||||||
|
本仓库托管的是 **Tootaio Studio 网站**,基于以下技术栈:
|
||||||
|
|
||||||
|
* **Nuxt 4.2.0**
|
||||||
|
* **@nuxt/content**(内容驱动)
|
||||||
|
* **@nuxt/ui**
|
||||||
|
* **i18n**
|
||||||
|
* 包管理器:**pnpm**
|
||||||
|
* 参考规范见 `AGENTS.md`(已提供)。
|
||||||
|
|
||||||
|
项目结构(符合最新 Nuxt 4.2.0):
|
||||||
|
|
||||||
|
```
|
||||||
|
app/
|
||||||
|
├─ assets/
|
||||||
|
├─ composables/
|
||||||
|
├─ layouts/
|
||||||
|
├─ pages/
|
||||||
|
├─ app.vue
|
||||||
|
content/
|
||||||
|
i18n/
|
||||||
|
public/
|
||||||
|
docs/
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、分析目标
|
||||||
|
|
||||||
|
请你以拥有 10 年以上经验的 **Nuxt / 前端架构师** 的身份,
|
||||||
|
对整个项目进行一次性全面分析与输出,目标包括但不限于:
|
||||||
|
|
||||||
|
1. **架构审计**
|
||||||
|
|
||||||
|
* 分析模块划分、Nuxt 4 特性使用(Nitro routes、auto-import、runtime config 等)。
|
||||||
|
* 指出结构异味与潜在设计风险,并提供优化策略。
|
||||||
|
* 判断 app/ 层结构是否遵循最新 Nuxt 4.2.0 官方建议。
|
||||||
|
|
||||||
|
2. **代码质量与维护性**
|
||||||
|
|
||||||
|
* 检查类型定义、composables 命名规范、逻辑分层是否清晰。
|
||||||
|
* 判断是否存在可重构点(如 utils / composables / layouts 中重复逻辑)。
|
||||||
|
* 给出统一命名与约定规范建议(含具体命名示例与代码片段)。
|
||||||
|
|
||||||
|
3. **性能与构建流程**
|
||||||
|
|
||||||
|
* 检查 SSR / CSR 渲染策略是否合理;有无未按需加载的模块。
|
||||||
|
* 给出针对 Nuxt 4 的性能优化建议(build options, code-splitting, lazy loading, runtime config 优化等)。
|
||||||
|
* 分析缓存策略、图片优化、CDN 使用、Nitro 部署模式(Node / Edge / Static)。
|
||||||
|
|
||||||
|
4. **开发体验与工程体系(DX)**
|
||||||
|
|
||||||
|
* 审查 eslint、prettier、tsconfig、vitest、commitlint、husky 等工具配置。
|
||||||
|
* 给出一份推荐的现代化工程工具链(pnpm workspace + lint + test + CI/CD)配置清单。
|
||||||
|
* 输出改进示例(配置文件、命令、package.json scripts)。
|
||||||
|
|
||||||
|
5. **CI/CD 与部署**
|
||||||
|
|
||||||
|
* 检查是否存在 GitHub Actions / CI 流程;如无,请生成完整示例:
|
||||||
|
包含 lint → type-check → test → build → deploy 的 workflow 文件。
|
||||||
|
* 同时给出 Vercel / Docker / 自建部署方案的对比说明。
|
||||||
|
|
||||||
|
6. **安全与可观测性**
|
||||||
|
|
||||||
|
* 检查敏感信息暴露、dotenv 安全、runtimeConfig 泄露风险。
|
||||||
|
* 建议增加 Sentry/LogRocket/Datadog 等监控与日志方案,并提供示例代码。
|
||||||
|
|
||||||
|
7. **内容与多语言结构**
|
||||||
|
|
||||||
|
* 确认 `content/` 与 `i18n/` 结构是否合理、是否符合 `AGENTS.md` 规范。
|
||||||
|
* 检查 YAML schema 与 JSON locale 对齐情况,建议自动化同步方案。
|
||||||
|
|
||||||
|
8. **改进路线图(Roadmap)**
|
||||||
|
|
||||||
|
* 生成一个可执行的工程化升级路线图:
|
||||||
|
分为四个阶段(基础规范化 → 模块解耦 → 自动化部署 → 性能与监控)。
|
||||||
|
* 每个阶段列出要点、执行命令和预期产出。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、输出要求(一次性全部完成)
|
||||||
|
|
||||||
|
请将你的分析结果写入 `docs/20251106/` 目录中,包含以下文件(全部在本次回复中完整输出):
|
||||||
|
|
||||||
|
| 文件路径 | 内容说明 |
|
||||||
|
| -------------------------------- | ---------------------------------------- |
|
||||||
|
| `docs/20251106/README.md` | 分析概要与执行总结 |
|
||||||
|
| `docs/20251106/architecture.md` | 架构结构与改进方案 |
|
||||||
|
| `docs/20251106/code-quality.md` | 代码规范与质量分析 |
|
||||||
|
| `docs/20251106/performance.md` | 性能诊断与优化建议 |
|
||||||
|
| `docs/20251106/ci-cd.md` | CI/CD 设计与部署方案 |
|
||||||
|
| `docs/20251106/security.md` | 安全审计与防护建议 |
|
||||||
|
| `docs/20251106/observability.md` | 监控、日志与运行时可观测性 |
|
||||||
|
| `docs/20251106/roadmap.md` | 分阶段工程化改进计划 |
|
||||||
|
| `docs/20251106/patches/` | 含至少 3 个可应用的 `.patch` 文件(unified diff 格式) |
|
||||||
|
|
||||||
|
每个 patch 文件需附带:
|
||||||
|
|
||||||
|
* 修改目标与理由;
|
||||||
|
* 应用命令(`git apply patches/xxx.patch`);
|
||||||
|
* 改动前后对比(`--- a/... +++ b/...`);
|
||||||
|
* 若涉及配置文件,请保持类型安全与 Nuxt 4.2.0 兼容。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、格式要求
|
||||||
|
|
||||||
|
* 所有输出以 Markdown 格式展示;
|
||||||
|
* 按文件分节,使用代码块展示 `.patch` 内容;
|
||||||
|
* 每份文档前加上清晰标题与简短摘要;
|
||||||
|
* 禁止模糊性推迟或要求我继续上传文件;
|
||||||
|
* 如果无法直接读取仓库,请列出你需要的关键文件清单(按优先级),但仍需生成基于假设的分析报告与目录结构模板。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、风格与深度要求
|
||||||
|
|
||||||
|
* 用语风格:专业、实操、能落地;
|
||||||
|
* 所有建议应附示例代码、命令、配置;
|
||||||
|
* 明确指出每个问题的严重程度(Critical / High / Medium / Low);
|
||||||
|
* 推荐方案必须与 Nuxt 4.2.0 当前版本特性兼容;
|
||||||
|
* 输出中请表现出架构师的思考过程(但不需解释 Prompt)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、触发执行
|
||||||
|
|
||||||
|
准备好后,请开始分析项目并输出完整报告。
|
||||||
|
触发关键词:
|
||||||
|
|
||||||
|
`ANALYZE_AND_OUTPUT_NOW`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔚 Prompt 结束
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
是否希望我现在帮你生成这个 Prompt 的 **可直接存放在仓库根目录的 `codex_prompt.md` 版本**(带标题、描述和 front-matter)?
|
||||||
|
那样你只需放进仓库、Codex 扫描就能自动理解执行。
|
||||||
24
docs/Company Overview.md
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
我的工作室侧重于:定制开发,科技公司
|
||||||
|
|
||||||
|
涉及业务包括但不限于:
|
||||||
|
|
||||||
|
1. 网站 / 网页开发(高级定制,包括客制化后台系统等)
|
||||||
|
2. 程序开发
|
||||||
|
3. 游戏开发(曾经有一两款单机小游戏,目前暂停,未来会重新启动)
|
||||||
|
4. 工具开发
|
||||||
|
5. 软硬件测评(未来加入)
|
||||||
|
6. 额外服务:宴会大屏幕内容定制
|
||||||
|
7. 小网页开发(普通定制,用于简单展示)
|
||||||
|
|
||||||
|
目前线上可访问(大学做的,基于 Nuxt3)游戏介绍为主。现在想把涉猎面广一些
|
||||||
|
|
||||||
|
目前我们已落地的产品:
|
||||||
|
|
||||||
|
- GameJam 游戏一个(地牢探险生存类)
|
||||||
|
- 业余开发的手机游戏一个(速降类)
|
||||||
|
- 永平中学校友会官方网站与管理系统(价值 RM32,000)
|
||||||
|
- 宴会大屏竞标系统(价值:RM 500 / 次)
|
||||||
|
|
||||||
|
正在开发的产品:
|
||||||
|
|
||||||
|
- 室内设计公司高定官网(价值 RM 8,000)
|
||||||
63
docs/Index Our Capabilities.md
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
## 🧩 Our Capabilities(建议结构)
|
||||||
|
|
||||||
|
### 1️⃣ Custom Web Development
|
||||||
|
|
||||||
|
> 从企业官网到复杂后台系统,提供端到端的定制化开发。
|
||||||
|
> **关键词**:Nuxt / Next / Tailwind / CMS / API Integration
|
||||||
|
|
||||||
|
可加一句扩展:
|
||||||
|
|
||||||
|
> 我们专注于速度、可维护性与美学并重的前端体验。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2️⃣ Software & Tool Engineering
|
||||||
|
|
||||||
|
> 为企业或个人定制内部工具与业务流程系统。
|
||||||
|
> **关键词**:Automation / Dashboard / Data Visualization / Desktop Apps
|
||||||
|
|
||||||
|
> 你可以展示如「宴会大屏幕竞标系统」或「管理系统」作为例子。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3️⃣ Game Design & Development
|
||||||
|
|
||||||
|
> 将创意化为可交互的世界。独立游戏、原型、游戏化体验一站式实现。
|
||||||
|
> **关键词**:Godot / Unity / WebGL / Procedural Design
|
||||||
|
|
||||||
|
> 可以提及 Game Jam 作品、速降手游、未来的重启计划。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4️⃣ Interactive Media & Event Systems
|
||||||
|
|
||||||
|
> 为活动、展会、宴会提供沉浸式视觉与互动内容。
|
||||||
|
> **关键词**:Large Screen Content / Real-time Animation / Custom Interaction
|
||||||
|
|
||||||
|
> 这块是你独有的“宴会大屏幕定制服务”,建议配视频或动效展示。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5️⃣ Tech Exploration & Evaluation (Lab)
|
||||||
|
|
||||||
|
> 研究前沿软硬件,测试并分享最新技术成果。
|
||||||
|
> **关键词**:Experimental / Hardware Review / Prototype / Innovation
|
||||||
|
|
||||||
|
> 可作为未来内容板块,让网站保持“持续成长感”。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 6️⃣ Creative Consulting & Digital Strategy *(可选)*
|
||||||
|
|
||||||
|
> 为品牌或团队提供技术方向规划与产品实现建议。
|
||||||
|
> **关键词**:Digital Transformation / UX Consulting / Architecture Design
|
||||||
|
|
||||||
|
> 如果你常为客户做「网站+系统整体方案」,就适合加上这项。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✨ 展示形式建议
|
||||||
|
|
||||||
|
* 六宫格布局,每项配一个动效图标(iconify /lucide)
|
||||||
|
* hover 时显示一句“我们的价值主张”
|
||||||
|
* 用对比色或霓虹描边营造科技氛围
|
||||||
122
docs/design-doc-v1-en.md
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
# 🧭 1. Brand Positioning
|
||||||
|
|
||||||
|
### 🔹 Core Brand Statement
|
||||||
|
|
||||||
|
> “A creative technology studio offering custom development services, specializing in full‑stack solutions for websites, software, games, and interactive experiences.”
|
||||||
|
|
||||||
|
**A creative technology studio offering *custom development services*, specializing in full‑stack solutions for websites, software, games, and interactive experiences.**
|
||||||
|
|
||||||
|
### 🔹 Brand Keywords
|
||||||
|
|
||||||
|
Tech‑forward, Professional, Creative, Reliable, Cross‑disciplinary.
|
||||||
|
|
||||||
|
### 🔹 Suggested Slogans
|
||||||
|
|
||||||
|
* “Crafted Code, Custom Experience.”
|
||||||
|
* “Technology Meets Imagination.”
|
||||||
|
* (Chinese original translated) “Tailored digital products for brands that want unique experiences.”
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 🧩 2. Site Map (Information Architecture)
|
||||||
|
|
||||||
|
Because your business covers a wide range of services, we should present them in a structured way while keeping clear pathways to featured projects.
|
||||||
|
|
||||||
|
**Recommended structure:**
|
||||||
|
|
||||||
|
```
|
||||||
|
Home
|
||||||
|
├── About (brand story + tech stack + team / contact)
|
||||||
|
├── Services
|
||||||
|
│ ├── Website Development (highly customized + CMS)
|
||||||
|
│ ├── Software & Tools (automation / utilities / desktop apps)
|
||||||
|
│ ├── Game Development (indie titles + future roadmap)
|
||||||
|
│ ├── Event Displays (custom banquet/event screen systems)
|
||||||
|
│ └── Lab & Testing (experiments / future features)
|
||||||
|
├── Projects
|
||||||
|
│ ├── Corporate Sites
|
||||||
|
│ ├── Games
|
||||||
|
│ ├── Tools & Systems
|
||||||
|
│ └── Special Projects (e.g., event systems)
|
||||||
|
├── Insights / Blog
|
||||||
|
│ ├── Technical Articles
|
||||||
|
│ ├── Dev Logs
|
||||||
|
│ ├── Game Dev Journal
|
||||||
|
│ └── Test Reports (future)
|
||||||
|
└── Contact
|
||||||
|
```
|
||||||
|
|
||||||
|
✅ Use a **tagging system** (e.g., website / game / tool / event) to let visitors quickly filter projects.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 🧱 3. Visual & Interaction Style Recommendations
|
||||||
|
|
||||||
|
### 🔹 Overall Aesthetic
|
||||||
|
|
||||||
|
| Element | Recommendation |
|
||||||
|
| ---------- | ------------------------------------------------------------------------------------------- |
|
||||||
|
| Tone | Monochrome base (black & white) + accent color (neon blue / electric purple / amber orange) |
|
||||||
|
| Typography | Inter / JetBrains Mono / Noto Sans |
|
||||||
|
| Motion | Scroll‑reveal, micro‑interactions, concise GSAP transitions |
|
||||||
|
| Layout | Generous white space + modular components (each text block paired with a visual panel) |
|
||||||
|
|
||||||
|
### 🔹 Homepage Layout Suggestions
|
||||||
|
|
||||||
|
1. **Hero Section** — One‑line mission statement + dynamic logo / particle animation demo.
|
||||||
|
2. **Our Capabilities** — Display 3–5 core services with subtle motion.
|
||||||
|
3. **Featured Works** — Select 3–6 highlighted projects.
|
||||||
|
4. **Tech Stack / Tools** — Visual badges for frameworks, languages, engines.
|
||||||
|
5. **Why Choose Us** — Differentiators (customization, speed, delivery standards).
|
||||||
|
6. **Call to Action** — Contact / Collaboration / Proposal button.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# ⚙️ 4. Technology Stack Recommendations (Nuxt 3 rebuild)
|
||||||
|
|
||||||
|
| Module | Recommended Implementation |
|
||||||
|
| ------------ | --------------------------------------------------- |
|
||||||
|
| UI Framework | Tailwind CSS + shadcn/ui |
|
||||||
|
| Animation | GSAP / Framer Motion (Vue compatible) |
|
||||||
|
| Content Mgmt | `@nuxt/content` (manage projects/blog via Markdown) |
|
||||||
|
| Image Optim | Nuxt Image Module |
|
||||||
|
| Deployment | Vercel / Cloudflare Pages |
|
||||||
|
| SEO | Nuxt SEO module + automatic sitemap generation |
|
||||||
|
| Analytics | Umami (lightweight, self‑hosted) |
|
||||||
|
|
||||||
|
> 💡 To showcase technical capability, consider exposing an API surface, visualizing project metadata, or integrating a CMS backend (Directus or a custom Nuxt Admin module).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 🎨 5. Content Strategy
|
||||||
|
|
||||||
|
| Section | Content Direction |
|
||||||
|
| --------------- | -------------------------------------------------------------------------- |
|
||||||
|
| Home | Who we are + representative works + what we can deliver |
|
||||||
|
| Services | Detailed explanation for each service + links to real examples |
|
||||||
|
| Projects | Story‑driven case studies (Client Problem → Solution → Outcome) |
|
||||||
|
| Blog / Insights | More than technical posts: behind‑the‑scenes, lessons learned, dev stories |
|
||||||
|
| About | Emphasize team philosophy and future direction (mention game revival plan) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 📈 6. Phased Rebuild Roadmap (Execution Plan)
|
||||||
|
|
||||||
|
| Phase | Duration | Tasks |
|
||||||
|
| ----------------- | --------- | ---------------------------------------------- |
|
||||||
|
| 1️⃣ Brand Refresh | 3–5 days | Copywriting, visual direction, finalize slogan |
|
||||||
|
| 2️⃣ Architecture | 5–7 days | Sitemap, wireframes, content structure |
|
||||||
|
| 3️⃣ Development | 2–3 weeks | Nuxt 3 + Tailwind skeleton, build pages |
|
||||||
|
| 4️⃣ Content Import | 1 week | Integrate Markdown / CMS, upload projects |
|
||||||
|
| 5️⃣ Motion & Polish | 3–5 days | Animations, SEO tuning, performance testing |
|
||||||
|
| 6️⃣ Deploy & Launch | 1 day | One‑click deploy to Vercel / Cloudflare Pages |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 🔮 7. Future Expansion Opportunities
|
||||||
|
|
||||||
|
* Add a **Lab** section to showcase experimental tools and prototypes.
|
||||||
|
* Build a **custom CMS** (a lightweight Nuxt Admin is feasible).
|
||||||
|
* Add **multilingual support (i18n)** to reach international clients.
|
||||||
|
* For game projects, enable **WebGL / Godot Web Export** for in‑browser demos.
|
||||||
|
* Modularize project data as JSON / Markdown for easier maintenance.
|
||||||
@@ -120,13 +120,3 @@
|
|||||||
* 集成 **多语言支持(i18n)**,面向海外客户。
|
* 集成 **多语言支持(i18n)**,面向海外客户。
|
||||||
* 游戏项目可接入 **WebGL / Godot Web Export**,直接可试玩。
|
* 游戏项目可接入 **WebGL / Godot Web Export**,直接可试玩。
|
||||||
* 将案例模块化为 JSON / Markdown 文件,可持续维护。
|
* 将案例模块化为 JSON / Markdown 文件,可持续维护。
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
要不我直接帮你整理成一个完整的:
|
|
||||||
|
|
||||||
> 🧾《工作室网站重构计划书(v1)》——包括结构图、推荐技术栈、视觉方向与页面草稿说明。
|
|
||||||
|
|
||||||
格式我可以生成成 Word / PDF,你可以直接交给团队或自己用来开发。
|
|
||||||
|
|
||||||
是否要我现在帮你生成?(如果要,请告诉我希望输出哪种格式:**Word** 还是 **PDF**)
|
|
||||||
|
|||||||
62
i18n/locales/en-US/common.json
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
{
|
||||||
|
"common": {
|
||||||
|
"header": {
|
||||||
|
"insights": {
|
||||||
|
"label": "Insights",
|
||||||
|
"children": {
|
||||||
|
"xiaomaiBlog": {
|
||||||
|
"label": "Founder's Blog",
|
||||||
|
"description": "The blog sites of our studio founder - Xiaomai."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"services": {
|
||||||
|
"label": "Services",
|
||||||
|
"children": {
|
||||||
|
"softwareDev": {
|
||||||
|
"label": "Software Development",
|
||||||
|
"description": "Software and Tools / Automation Development"
|
||||||
|
},
|
||||||
|
"eventVisual": {
|
||||||
|
"label": "Event Big Screen Visualization",
|
||||||
|
"description": "Display the content whichever you want on the big screen of your events!"
|
||||||
|
},
|
||||||
|
"lab": {
|
||||||
|
"label": "Labs and Tests",
|
||||||
|
"description": "Unavailable yet..."
|
||||||
|
},
|
||||||
|
"webDev": {
|
||||||
|
"label": "Web Development",
|
||||||
|
"description": "Customise Web design + CMS"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"projects": {
|
||||||
|
"label": "Projects",
|
||||||
|
"children": {
|
||||||
|
"commercialWebsite": {
|
||||||
|
"label": "Commercial Website",
|
||||||
|
"description": "Fully customize website showcases."
|
||||||
|
},
|
||||||
|
"tools": {
|
||||||
|
"label": "Tools and Softwares",
|
||||||
|
"description": "Some QoL tools we developed"
|
||||||
|
},
|
||||||
|
"special": {
|
||||||
|
"label": "Special Projects",
|
||||||
|
"description": "Special projects to meet customer needs"
|
||||||
|
},
|
||||||
|
"gameDev": {
|
||||||
|
"label": "Indie Games",
|
||||||
|
"description": "Have a look at our indie games!"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"button": {
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"submit": "Submit",
|
||||||
|
"saving": "Saving..."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,18 @@
|
|||||||
{
|
{
|
||||||
"index": {
|
"index": {
|
||||||
"trustedBy": "Trusted by over {count} users worldwide"
|
"trustedBy": "Trusted by over {count} users worldwide",
|
||||||
|
"featuredProjects": {
|
||||||
|
"viewDemo": "Visit Site"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"webDev": {
|
||||||
|
"know_more_description": "I need to know more about the pricing and service about {plan} plan.",
|
||||||
|
"know_more_title": "Know more {plan} plan",
|
||||||
|
"contact_intro": "Hi — I'd like to learn more about {service} {plan} plan.",
|
||||||
|
"which_provides": "Which provides:",
|
||||||
|
"extra_remarks_title": "Besides that, I'd like to add:",
|
||||||
|
"remarks_placeholder": "Enter your remarks or requirements, one per line",
|
||||||
|
"loading_plan": "Loading plan...",
|
||||||
|
"whatsapp_message": "Hello! I’m interested in the **{plan}** plan under your **{service}** service.\n\nHere are the plan details:\n💰 Price: {price}\n✨ Key Features:\n{featureList}\nAdditionally, I would like to request:\n{remarkList}\nPlease provide more detailed information. Thank you!"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
62
i18n/locales/zh-CN/common.json
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
{
|
||||||
|
"common": {
|
||||||
|
"header": {
|
||||||
|
"services": {
|
||||||
|
"label": "服务",
|
||||||
|
"children": {
|
||||||
|
"softwareDev": {
|
||||||
|
"label": "软件开发",
|
||||||
|
"description": "软件和工具/自动化开发"
|
||||||
|
},
|
||||||
|
"eventVisual": {
|
||||||
|
"label": "活动大屏可视化",
|
||||||
|
"description": "在您的活动大屏幕上显示您想要的内容!"
|
||||||
|
},
|
||||||
|
"lab": {
|
||||||
|
"label": "实验室和测试",
|
||||||
|
"description": "还无法使用..."
|
||||||
|
},
|
||||||
|
"webDev": {
|
||||||
|
"label": "网站开发",
|
||||||
|
"description": "定制网页设计 + CMS"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"projects": {
|
||||||
|
"label": "案例",
|
||||||
|
"children": {
|
||||||
|
"commercialWebsite": {
|
||||||
|
"label": "商业网站",
|
||||||
|
"description": "完全定制网站展示。"
|
||||||
|
},
|
||||||
|
"tools": {
|
||||||
|
"label": "工具和软件",
|
||||||
|
"description": "我们开发的一些生活质量工具"
|
||||||
|
},
|
||||||
|
"special": {
|
||||||
|
"label": "特别项目",
|
||||||
|
"description": "特殊项目满足客户需求"
|
||||||
|
},
|
||||||
|
"gameDev": {
|
||||||
|
"label": "独立游戏",
|
||||||
|
"description": "看看我们的独立游戏!"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"insights": {
|
||||||
|
"label": "博客",
|
||||||
|
"children": {
|
||||||
|
"xiaomaiBlog": {
|
||||||
|
"label": "创始人博客",
|
||||||
|
"description": "我们工作室创始人小麦的博客网站。"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"button": {
|
||||||
|
"cancel": "取消",
|
||||||
|
"submit": "提交",
|
||||||
|
"saving": "保存..."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,18 @@
|
|||||||
{
|
{
|
||||||
"index": {
|
"index": {
|
||||||
"trustedBy": "全球有超过 {count} 用户信赖"
|
"trustedBy": "全球有超过 {count} 用户信赖",
|
||||||
|
"featuredProjects": {
|
||||||
|
"viewDemo": "访问页面"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"webDev": {
|
||||||
|
"know_more_description": "我需要了解有关 {plan} 的定价和服务的更多信息。",
|
||||||
|
"know_more_title": "了解更多{plan}",
|
||||||
|
"contact_intro": "你好——我想了解更多关于 {service} {plan}",
|
||||||
|
"which_provides": "其中包含:",
|
||||||
|
"extra_remarks_title": "除此之外,我还想补充:",
|
||||||
|
"remarks_placeholder": "输入您的备注或要求,每行一个",
|
||||||
|
"loading_plan": "配套加载中。。。",
|
||||||
|
"whatsapp_message": "您好!我对您的【{service}】服务中的【{plan}】方案感兴趣。\n\n方案详情:\n💰 价格:{price}\n✨ 主要功能:\n{featureList}\n另外我还需要:\n{remarkList}\n请提供更多详细信息,谢谢!"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ const DEFAULT_SEO = {
|
|||||||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||||
export default defineNuxtConfig({
|
export default defineNuxtConfig({
|
||||||
compatibilityDate: "2025-07-15",
|
compatibilityDate: "2025-07-15",
|
||||||
devtools: { enabled: true },
|
devtools: { enabled: process.env.NODE_ENV !== "production" },
|
||||||
modules: [
|
modules: [
|
||||||
"@nuxt/content",
|
"@nuxt/content",
|
||||||
"@nuxt/ui",
|
"@nuxt/ui",
|
||||||
@@ -19,7 +19,41 @@ export default defineNuxtConfig({
|
|||||||
"@nuxtjs/i18n",
|
"@nuxtjs/i18n",
|
||||||
"@nuxtjs/seo",
|
"@nuxtjs/seo",
|
||||||
],
|
],
|
||||||
|
runtimeConfig: {
|
||||||
|
public: {
|
||||||
|
whatsappNumber: "+601234567890",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
routeRules: {
|
||||||
|
"/**": {
|
||||||
|
headers: {
|
||||||
|
"Content-Security-Policy": [
|
||||||
|
"default-src 'self'",
|
||||||
|
"script-src 'self'",
|
||||||
|
"style-src 'self' 'unsafe-inline'",
|
||||||
|
"img-src 'self' https://img.tootaio.com data:",
|
||||||
|
"connect-src 'self'",
|
||||||
|
"frame-ancestors 'self'",
|
||||||
|
"upgrade-insecure-requests",
|
||||||
|
].join("; "),
|
||||||
|
"X-Content-Type-Options": "nosniff",
|
||||||
|
"Referrer-Policy": "strict-origin-when-cross-origin",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"/_nuxt/**": {
|
||||||
|
headers: { "cache-control": "public, max-age=31536000, immutable" },
|
||||||
|
},
|
||||||
|
},
|
||||||
css: ["@/assets/css/main.css"],
|
css: ["@/assets/css/main.css"],
|
||||||
|
app: {
|
||||||
|
head: {
|
||||||
|
titleTemplate: "%s - Tootaio Studio",
|
||||||
|
meta: [
|
||||||
|
{ name: "viewport", content: "width=device-width, initial-scale=1" },
|
||||||
|
{ charset: "utf-8" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
i18n: {
|
i18n: {
|
||||||
defaultLocale: "en",
|
defaultLocale: "en",
|
||||||
locales: [
|
locales: [
|
||||||
@@ -27,15 +61,16 @@ export default defineNuxtConfig({
|
|||||||
code: "en",
|
code: "en",
|
||||||
iso: "en-US",
|
iso: "en-US",
|
||||||
name: "English",
|
name: "English",
|
||||||
files: ["en-US/index.json"],
|
files: ["en-US/common.json", "en-US/index.json"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
code: "zh-CN",
|
code: "zh-CN",
|
||||||
iso: "zh-CN",
|
iso: "zh-CN",
|
||||||
name: "简体中文",
|
name: "简体中文",
|
||||||
files: ["zh-CN/index.json"],
|
files: ["zh-CN/common.json", "zh-CN/index.json"],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
strategy: "no_prefix",
|
||||||
},
|
},
|
||||||
seo: {
|
seo: {
|
||||||
meta: {
|
meta: {
|
||||||
@@ -52,4 +87,7 @@ export default defineNuxtConfig({
|
|||||||
twitterImage: DEFAULT_SEO.image,
|
twitterImage: DEFAULT_SEO.image,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
site: {
|
||||||
|
url: "https://tootaio.com",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
"postinstall": "nuxt prepare"
|
"postinstall": "nuxt prepare"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@iconify-json/mdi": "^1.2.3",
|
||||||
"@nuxt/content": "3.8.0",
|
"@nuxt/content": "3.8.0",
|
||||||
"@nuxt/eslint": "1.10.0",
|
"@nuxt/eslint": "1.10.0",
|
||||||
"@nuxt/ui": "4.1.0",
|
"@nuxt/ui": "4.1.0",
|
||||||
|
|||||||
2197
pnpm-lock.yaml
generated
|
Before Width: | Height: | Size: 17 KiB |
@@ -1,3 +0,0 @@
|
|||||||
<svg width="67" height="41" viewBox="0 0 67 41" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M45.0353 4.66312C45.8331 3.77669 46.7195 3.04539 47.6281 2.46921C49.2236 1.47198 50.9079 0.940125 52.6364 0.940125V15.411C51.3732 11.0232 48.6475 7.25591 45.0353 4.66312ZM66.5533 40.9401H15.2957C6.87461 40.9401 0.0712891 34.1146 0.0712891 25.7157C0.0712891 17.6714 6.3206 11.0675 14.232 10.5135V0.940125C16.0048 0.940125 17.7555 1.44982 19.3954 2.46921C20.304 3.02323 21.1904 3.75453 21.9882 4.59663C25.2458 2.31409 29.1904 0.984446 33.4674 0.984446C33.4674 10.2254 30.1433 20.9734 19.3289 20.9955H33.3566C32.9577 19.2005 31.3178 17.8709 29.3677 17.8487H37.5228C35.5727 17.8487 33.9328 19.2005 33.5339 21.0177H46.6087C49.2236 21.0177 51.8164 21.5274 54.2541 22.5468C56.6696 23.544 58.8857 25.0288 60.725 26.8681C62.5865 28.7296 64.0491 30.9235 65.0464 33.339C66.0436 35.7324 66.5533 38.3252 66.5533 40.9401ZM22.8525 10.7795C23.1849 11.6437 24.0713 12.6188 25.3123 13.3279C26.5533 14.0371 27.8386 14.3252 28.7472 14.1922C28.4148 13.3279 27.5284 12.3529 26.2874 11.6437C25.0464 10.9346 23.761 10.6465 22.8525 10.7795ZM41.5117 13.3279C40.2707 14.0371 38.9854 14.3252 38.0768 14.1922C38.4092 13.3279 39.2957 12.3529 40.5367 11.6437C41.7777 10.9346 43.063 10.6465 43.9716 10.7795C43.6613 11.6437 42.7527 12.6188 41.5117 13.3279Z" fill="#283841"></path>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.3 KiB |
@@ -1,3 +0,0 @@
|
|||||||
<svg width="35" height="40" viewBox="0 0 35 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M29.4554 2.43478V0H35V22.4348C35 32.1358 27.165 40 17.5 40C8.24271 40 0.664262 32.7853 0.0413736 23.6522H0V0H5.54455V2.43478L14.901 2.43478V0H20.4455V2.43478L29.4554 2.43478ZM29.4554 22.4348V19.0202C28.8318 19.6656 28.1633 20.2785 27.4539 20.8558C25.1121 22.7615 22.3612 24.2503 19.369 25.2589C16.3764 26.2677 13.1833 26.7826 9.96797 26.7826H6.35343C8.08848 31.2608 12.425 34.4348 17.5 34.4348C24.1028 34.4348 29.4554 29.0622 29.4554 22.4348ZM15.4269 18.2435C14.3706 19.3674 13.18 20.3419 11.8852 21.1425C13.8545 20.9882 15.7827 20.5971 17.6038 19.9833C20.013 19.1712 22.1698 17.9913 23.9621 16.5329C25.7535 15.075 27.136 13.3757 28.0645 11.5515C28.6507 10.3998 29.0518 9.20727 29.2674 8H20.2671C20.0641 9.47968 19.6891 10.9319 19.1475 12.3231C18.2893 14.5274 17.0275 16.5405 15.4269 18.2435ZM5.54455 17.8146V8H14.6483C14.4948 8.78546 14.2724 9.55482 13.9832 10.2975C13.3786 11.8506 12.4962 13.2517 11.3938 14.4246C10.2918 15.5971 8.99228 16.518 7.57404 17.143C6.91535 17.4333 6.23601 17.6576 5.54455 17.8146Z" fill="#1D3AA7"></path>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 25 KiB |
@@ -1,4 +0,0 @@
|
|||||||
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M0 20C0 12.5231 0 8.78461 1.60769 6C2.66091 4.17577 4.17577 2.66091 6 1.60769C8.78461 0 12.5231 0 20 0C27.4769 0 31.2154 0 34 1.60769C35.8242 2.66091 37.3391 4.17577 38.3923 6C40 8.78461 40 12.5231 40 20C40 27.4769 40 31.2154 38.3923 34C37.3391 35.8242 35.8242 37.3391 34 38.3923C31.2154 40 27.4769 40 20 40C12.5231 40 8.78461 40 6 38.3923C4.17577 37.3391 2.66091 35.8242 1.60769 34C0 31.2154 0 27.4769 0 20Z" fill="#00DC33"></path>
|
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M28.0441 7.60927C28.8868 6.80331 30.2152 6.79965 31.0622 7.58229L31.1425 7.66005L31.4164 7.94729C34.1911 10.9318 35.2251 14.4098 34.9599 17.8065C34.6908 21.2511 33.1012 24.4994 30.8836 27.0664C28.6673 29.6316 25.7084 31.6519 22.51 32.5287C19.2714 33.4164 15.7294 33.1334 12.6547 30.9629C10.0469 29.1218 9.05406 26.1465 8.98661 23.2561C7.52323 22.5384 5.98346 21.6463 4.36789 20.5615L3.941 20.2716L3.85006 20.206C2.93285 19.5053 2.72313 18.2084 3.39161 17.2564C4.06029 16.3043 5.36233 16.046 6.34665 16.6512L6.44134 16.7126L6.83024 16.9771C7.79805 17.6269 8.72153 18.1903 9.59966 18.6767C10.1661 16.6889 11.1047 14.7802 12.3413 13.207C14.1938 10.8501 16.9713 8.96525 20.374 9.24647C23.439 9.49995 25.7036 11.081 26.8725 13.3122C28.0044 15.4728 28.0211 18.0719 27.0319 20.307C26.0234 22.5857 23.976 24.484 21.0309 25.2662C18.9114 25.8291 16.4284 25.7905 13.6267 25.0367V25.0377C12.5115 24.7375 11.3427 24.323 10.1212 23.7846C9.8472 23.6638 9.60873 23.8483 10.1212 24.1686C11.5636 25.1924 13.5956 26.0505 14.1836 26.3385C14.4615 26.788 14.8061 27.1568 15.2011 27.4356C17.0188 28.7188 19.1451 28.9539 21.3396 28.3523C23.5743 27.7397 25.8141 26.2625 27.5514 24.2516C29.2873 22.2423 30.4065 19.8348 30.5909 17.4727C30.765 15.2439 30.1218 12.9543 28.1842 10.8736L27.9927 10.6731L27.9162 10.5906C27.1538 9.72748 27.2018 8.41516 28.0441 7.60927ZM20.0092 13.5651C18.6033 13.4489 17.1196 14.189 15.8013 15.8662C14.7973 17.1436 14.0376 18.8033 13.6503 20.5112C16.4093 21.4544 18.4655 21.4608 19.8942 21.0814C21.5481 20.6422 22.5399 19.6477 23.0172 18.5693C23.5137 17.4472 23.4628 16.2245 22.9813 15.3055C22.5369 14.4571 21.6422 13.7002 20.0092 13.5651Z" fill="#ffffff"></path>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 2.2 KiB |
@@ -1,4 +0,0 @@
|
|||||||
<svg width="55" height="41" viewBox="0 0 55 41" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M35.5 0.5C45.9934 0.5 54.5 9.00659 54.5 19.5V40.5C50.0817 40.5 46.5 36.9183 46.5 32.5V19.5C46.5 13.4249 41.5751 8.5 35.5 8.5H30.2988C27.6467 8.50004 25.1029 9.55339 23.2275 11.4287L9.67188 24.9854C8.92179 25.7354 8.50006 26.7527 8.5 27.8135V28.5C8.5 30.7091 10.2909 32.5 12.5 32.5H30.5C30.5 36.7801 27.1389 40.2748 22.9121 40.4893L22.5 40.5H12.5C5.87259 40.5 0.5 35.1274 0.5 28.5V27.8135C0.500062 24.631 1.76427 21.5785 4.01465 19.3281L17.5713 5.77246C20.9469 2.39685 25.525 0.500044 30.2988 0.5H35.5Z" fill="#FF500B"></path>
|
|
||||||
<path d="M37.5 12.5C40.2614 12.5 42.5 14.7386 42.5 17.5V40.5C38.0817 40.5 34.5 36.9183 34.5 32.5V20.5H31.1562L24.6207 27.0355C23.683 27.9732 22.4113 28.5 21.0852 28.5H12.9775C12.5588 28.5 12.3491 27.9937 12.6452 27.6976L26.3789 13.9648C27.3165 13.0272 28.588 12.5 29.9141 12.5H37.5Z" fill="#FF500B"></path>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 944 B |
@@ -1,4 +0,0 @@
|
|||||||
<svg width="100%" height="100%" viewBox="0 0 56 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M43 0C50.1797 6.44277e-07 56 5.8203 56 13C56 20.1797 50.1797 26 43 26H34.4844L48.4844 40H31.5156L15.7578 24.2422C14.672 23.1564 14 21.6569 14 20C14 16.6863 16.6863 14 20 14H43C43.5523 14 44 13.5523 44 13C44 12.4477 43.5523 12 43 12H20C15.5817 12 12 15.5817 12 20C12 22.3901 13.0482 24.5347 14.71 26H14.6875L28.6875 40H20C8.95431 40 0 31.0457 0 20C0 8.95431 8.9543 0 20 0H43Z" fill="#3902FF"></path>
|
|
||||||
<path d="M56 28V40H51.3125L39.3125 28H56Z" fill="#3902FF"></path>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 580 B |