chore: initialize monorepo tooling

This commit is contained in:
2026-09-18 15:00:48 +08:00
parent e28140a0b3
commit 55b2b091c2
42 changed files with 1858 additions and 18 deletions

12
.editorconfig Normal file
View File

@@ -0,0 +1,12 @@
root = true
[*]
charset = utf-8
end_of_line = lf
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false

11
.env.example Normal file
View File

@@ -0,0 +1,11 @@
NODE_ENV=development
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/personal_brand
API_PORT=3001
ADMIN_PORT=3000
SESSION_SECRET=replace-with-a-long-random-value
OBJECT_STORAGE_ENDPOINT=
OBJECT_STORAGE_BUCKET=
OBJECT_STORAGE_ACCESS_KEY=
OBJECT_STORAGE_SECRET_KEY=
WECHAT_APP_ID=
WECHAT_APP_SECRET=

13
.gitignore vendored Normal file
View File

@@ -0,0 +1,13 @@
node_modules/
.pnpm-store/
.turbo/
dist/
.next/
coverage/
.env
.env.*
!.env.example
*.log
.DS_Store
apps/miniapp/miniprogram_npm/
apps/miniapp/project.private.config.json

6
.prettierignore Normal file
View File

@@ -0,0 +1,6 @@
node_modules
.turbo
dist
.next
prototype
pnpm-lock.yaml

7
.prettierrc.json Normal file
View File

@@ -0,0 +1,7 @@
{
"semi": true,
"singleQuote": false,
"trailingComma": "all",
"printWidth": 100,
"tabWidth": 2
}

30
README.md Normal file
View File

@@ -0,0 +1,30 @@
# Personal Brand Miniapp
面向个体经营者的个人品牌小程序试点项目。当前首个试点为餐饮摊主与私厨服务。
## 工程结构
```text
apps/miniapp # 原生微信小程序
apps/admin # 管理后台
apps/api # API 服务
packages/types # 共享类型
packages/api-client
packages/validation
packages/config
prisma/
docs/
prototype/
```
## 环境要求
- Node.js >= 20.18
- pnpm 9.15
- PostgreSQL进入 API/数据库开发阶段时需要)
```bash
corepack enable
pnpm install
pnpm check
```

View File

@@ -24,6 +24,7 @@
6. `docs/05-pilot-case-food-stall.md`:当前首个试点的餐饮场景假设和待确认内容。 6. `docs/05-pilot-case-food-stall.md`:当前首个试点的餐饮场景假设和待确认内容。
7. `docs/06-domain-extension-strategy.md`:个人品牌核心能力与行业扩展边界。 7. `docs/06-domain-extension-strategy.md`:个人品牌核心能力与行业扩展边界。
8. `docs/07-requirements-interview-decisions.md`:首轮访谈确认的订单与客户身份规则;冲突时优先于早期文档。 8. `docs/07-requirements-interview-decisions.md`:首轮访谈确认的订单与客户身份规则;冲突时优先于早期文档。
9. `docs/08-clickable-prototype.md`:低保真原型入口、演练路径和边界。
## 已安装的成熟 Skills ## 已安装的成熟 Skills

12
apps/admin/package.json Normal file
View File

@@ -0,0 +1,12 @@
{
"name": "@personal-brand/admin",
"private": true,
"type": "module",
"scripts": {
"build": "tsc -p tsconfig.json",
"dev": "tsc -p tsconfig.json --watch",
"lint": "eslint .",
"typecheck": "tsc -p tsconfig.json --noEmit",
"clean": "rimraf dist"
}
}

1
apps/admin/src/main.ts Normal file
View File

@@ -0,0 +1 @@
export const adminAppName = "personal-brand-admin";

5
apps/admin/tsconfig.json Normal file
View File

@@ -0,0 +1,5 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": { "outDir": "dist", "rootDir": "src", "lib": ["ES2022", "DOM"] },
"include": ["src/**/*.ts"]
}

12
apps/api/package.json Normal file
View File

@@ -0,0 +1,12 @@
{
"name": "@personal-brand/api",
"private": true,
"type": "module",
"scripts": {
"build": "tsc -p tsconfig.json",
"dev": "tsc -p tsconfig.json --watch",
"lint": "eslint .",
"typecheck": "tsc -p tsconfig.json --noEmit",
"clean": "rimraf dist"
}
}

3
apps/api/src/main.ts Normal file
View File

@@ -0,0 +1,3 @@
export function healthMessage(): string {
return "personal-brand-api";
}

5
apps/api/tsconfig.json Normal file
View File

@@ -0,0 +1,5 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": { "outDir": "dist", "rootDir": "src" },
"include": ["src/**/*.ts"]
}

10
apps/miniapp/package.json Normal file
View File

@@ -0,0 +1,10 @@
{
"name": "@personal-brand/miniapp",
"private": true,
"type": "module",
"scripts": {
"lint": "eslint . --ext .ts",
"typecheck": "tsc -p tsconfig.json --noEmit",
"clean": "rimraf dist"
}
}

View File

@@ -0,0 +1 @@
export const miniappName = "personal-brand-miniapp";

View File

@@ -0,0 +1,5 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": { "noEmit": true, "lib": ["ES2022"] },
"include": ["src/**/*.ts"]
}

View File

@@ -7,7 +7,7 @@
## 2. 技术选型 ## 2. 技术选型
| 层 | 选型 | 目的 | | 层 | 选型 | 目的 |
|---|---|---| | -------- | ------------------------------------- | ------------------------------------ |
| Monorepo | pnpm workspace + Turborepo | 统一脚本、缓存构建、共享包 | | Monorepo | pnpm workspace + Turborepo | 统一脚本、缓存构建、共享包 |
| 小程序 | 原生微信小程序 + TypeScript | 直接使用微信能力,减少跨端框架层 | | 小程序 | 原生微信小程序 + TypeScript | 直接使用微信能力,减少跨端框架层 |
| 管理后台 | Next.js + TypeScript | 表单、列表、图片管理和路由生态成熟 | | 管理后台 | Next.js + TypeScript | 表单、列表、图片管理和路由生态成熟 |

View File

@@ -108,7 +108,7 @@
沿用通用状态,但在餐饮后台显示更容易理解的文案: 沿用通用状态,但在餐饮后台显示更容易理解的文案:
| 系统状态 | 页面文案 | 说明 | | 系统状态 | 页面文案 | 说明 |
|---|---|---| | --------- | -------- | -------------------------- |
| pending | 待联系 | 客户刚提交需求 | | pending | 待联系 | 客户刚提交需求 |
| contacted | 沟通中 | 已联系,正在确认菜单或细节 | | contacted | 沟通中 | 已联系,正在确认菜单或细节 |
| completed | 已完成 | 已完成本次餐饮服务 | | completed | 已完成 | 已完成本次餐饮服务 |

View File

@@ -0,0 +1,29 @@
# 可点击低保真原型
## 使用方式
直接在浏览器打开 `prototype/index.html`,无需安装依赖或启动服务。
原型顶部可以切换“客户小程序”和“经营后台”。它使用 `prototype/mock-data.js` 中的虚拟餐饮数据,不连接数据库,也不会保存刷新后的操作。
## 建议演练路径
### 普通套餐
首页 -> 订普通套餐 -> 选择数量 -> 选择自取/配送 -> 提交 -> 我的订单。
### 私厨定制
首页 -> 约私厨 -> 填写日期、人数、地点与联系信息 -> 提交 -> 我的订单。
### 后台处理
经营后台 -> 订单管理 -> 打开未读订单 -> 修改状态 -> 添加标签 -> 填写内部备忘 -> 保存。
## 原型边界
- 微信登录仅模拟,不调用微信 API。
- 图片使用公开示例资源,正式项目必须替换为经营者授权图片。
- 每日菜单维护方式仍待确认,原型只展示固定套餐。
- 原型不包含支付、库存、自动配送判断和订阅消息。
- 所有操作只保存在当前浏览器页面内存中,刷新后恢复虚拟数据。

19
eslint.config.mjs Normal file
View File

@@ -0,0 +1,19 @@
import eslint from "@eslint/js";
import tseslint from "typescript-eslint";
export default tseslint.config(
{ ignores: ["**/dist/**", "**/.next/**", "**/node_modules/**", "prototype/**"] },
eslint.configs.recommended,
...tseslint.configs.recommendedTypeChecked,
{
languageOptions: {
parserOptions: { projectService: true, tsconfigRootDir: import.meta.dirname },
},
rules: {
"@typescript-eslint/consistent-type-imports": "error",
"@typescript-eslint/no-floating-promises": "error",
"@typescript-eslint/no-misused-promises": ["error", { checksVoidReturn: false }],
"@typescript-eslint/no-explicit-any": "warn",
},
},
);

28
package.json Normal file
View File

@@ -0,0 +1,28 @@
{
"name": "personal-brand-miniapp",
"version": "0.1.0",
"private": true,
"packageManager": "pnpm@9.15.0",
"engines": {
"node": ">=20.18.0"
},
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev --parallel",
"lint": "turbo run lint",
"typecheck": "turbo run typecheck",
"format": "prettier --write .",
"format:check": "prettier --check .",
"check": "pnpm format:check && pnpm lint && pnpm typecheck",
"clean": "turbo run clean && rimraf node_modules .turbo"
},
"devDependencies": {
"@eslint/js": "^9.17.0",
"eslint": "^9.17.0",
"prettier": "^3.4.2",
"rimraf": "^6.0.1",
"turbo": "^2.3.3",
"typescript": "^5.7.2",
"typescript-eslint": "^8.18.0"
}
}

View File

@@ -0,0 +1,14 @@
{
"name": "@personal-brand/api-client",
"private": true,
"type": "module",
"scripts": {
"build": "tsc -p tsconfig.json",
"lint": "eslint .",
"typecheck": "tsc -p tsconfig.json --noEmit",
"clean": "rimraf dist"
},
"dependencies": {
"@personal-brand/types": "workspace:*"
}
}

View File

@@ -0,0 +1 @@
export const apiClientName = "personal-brand-api-client";

View File

@@ -0,0 +1,5 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": { "outDir": "dist", "rootDir": "src" },
"include": ["src/**/*.ts"]
}

View File

@@ -0,0 +1,10 @@
{
"name": "@personal-brand/config",
"private": true,
"type": "module",
"scripts": {
"lint": "eslint .",
"typecheck": "tsc -p tsconfig.json --noEmit",
"clean": "rimraf dist"
}
}

View File

@@ -0,0 +1 @@
export const configPackageName = "personal-brand-config";

View File

@@ -0,0 +1,5 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": { "noEmit": true },
"include": ["src/**/*.ts"]
}

View File

@@ -0,0 +1,17 @@
{
"name": "@personal-brand/types",
"private": true,
"type": "module",
"scripts": {
"build": "tsc -p tsconfig.json",
"lint": "eslint .",
"typecheck": "tsc -p tsconfig.json --noEmit",
"clean": "rimraf dist"
},
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
}
}

View File

@@ -0,0 +1,4 @@
export type BookingStatus = "received" | "contacting" | "confirmed" | "completed" | "cancelled";
export type BookingRequestType = "package" | "private_chef";
export type ContactPreference = "wechat" | "phone" | "none";
export type FulfillmentType = "pickup" | "delivery";

View File

@@ -0,0 +1,5 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": { "outDir": "dist", "rootDir": "src" },
"include": ["src/**/*.ts"]
}

View File

@@ -0,0 +1,11 @@
{
"name": "@personal-brand/validation",
"private": true,
"type": "module",
"scripts": {
"build": "tsc -p tsconfig.json",
"lint": "eslint .",
"typecheck": "tsc -p tsconfig.json --noEmit",
"clean": "rimraf dist"
}
}

View File

@@ -0,0 +1 @@
export const validationPackageName = "personal-brand-validation";

View File

@@ -0,0 +1,5 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": { "outDir": "dist", "rootDir": "src" },
"include": ["src/**/*.ts"]
}

1084
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

3
pnpm-workspace.yaml Normal file
View File

@@ -0,0 +1,3 @@
packages:
- apps/*
- packages/*

190
prototype/app.js vendored Normal file
View File

@@ -0,0 +1,190 @@
const data = window.PROTOTYPE_DATA;
const state = {
miniRoute: "home",
adminRoute: "dashboard",
fulfillment: "pickup",
contactPreference: "none",
quantities: Object.fromEntries(data.packages.map((item) => [item.id, 0])),
orders: structuredClone(data.orders),
selectedOrderId: null,
orderFilter: "all"
};
const statusLabels = { received: "已收到", contacting: "沟通中", confirmed: "已确认", completed: "已完成", cancelled: "已取消" };
function escapeHtml(value = "") {
return String(value).replace(/[&<>'"]/g, (char) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", "'": "&#39;", '"': "&quot;" })[char]);
}
function showToast(message) {
const toast = document.querySelector("#toast");
toast.textContent = message;
toast.classList.add("show");
setTimeout(() => toast.classList.remove("show"), 1800);
}
function setMiniRoute(route) {
state.miniRoute = route;
document.querySelectorAll("[data-route]").forEach((button) => button.classList.toggle("active", button.dataset.route === route));
renderMiniapp();
}
function pageHeader(title) {
return `<header class="page-header"><button class="back-button" data-action="back-home" aria-label="返回首页">←</button><h1>${title}</h1></header>`;
}
function renderHome() {
return `
<section class="hero">
<span class="hero-badge">现做 · 家常 · 可定制</span>
<h1>${data.brand.name}</h1>
<p>${data.brand.tagline}</p>
<div class="button-row">
<button class="primary-button" data-action="package-order">订普通套餐</button>
<button class="secondary-button" data-action="chef-request">约私厨</button>
</div>
</section>
<section class="block">
<div class="block-heading"><h2>常见套餐</h2><button class="text-button" data-action="package-order">查看全部</button></div>
<div class="service-list">${data.packages.slice(0, 2).map((item) => `
<article class="service-card"><img src="${item.image}" alt="${item.name}"><div><h3>${item.name}</h3><p>${item.description}</p><span class="price">¥${item.price}</span></div></article>`).join("")}
</div>
</section>
<section class="block">
<div class="block-heading"><h2>私厨案例</h2><button class="text-button" data-action="brand">了解更多</button></div>
<div class="portfolio-grid">${data.portfolio.slice(0, 2).map((item) => `<article class="portfolio-item"><img src="${item.image}" alt="${item.title}"><div><strong>${item.title}</strong><span>${item.category}</span></div></article>`).join("")}</div>
</section>
<section class="block">
<div class="block-heading"><h2>食客反馈</h2></div>
${data.feedback.map((item) => `<div class="quote"><p>${item.content}</p><span>${item.name}</span></div>`).join("")}
</section>`;
}
function renderPackageOrder() {
const total = data.packages.reduce((sum, item) => sum + item.price * state.quantities[item.id], 0);
return `${pageHeader("预订普通套餐")}
<form class="form-page" id="package-form">
<section class="form-section"><h2>选择套餐和数量</h2>
${data.packages.map((item) => `<div class="qty-row"><div><strong>${item.name}</strong><div class="helper">¥${item.price} / 份</div></div><div class="stepper"><button type="button" data-qty="${item.id}" data-delta="-1" aria-label="减少${item.name}"></button><span>${state.quantities[item.id]}</span><button type="button" data-qty="${item.id}" data-delta="1" aria-label="增加${item.name}">+</button></div></div>`).join("")}
<div class="summary-box">预计总价 <strong>¥${total}</strong><div class="helper">最终以经营者确认和线下结算为准</div></div>
</section>
<section class="form-section"><h2>取餐方式</h2><div class="segmented"><button type="button" data-fulfillment="pickup" class="${state.fulfillment === "pickup" ? "active" : ""}">到摊位自取</button><button type="button" data-fulfillment="delivery" class="${state.fulfillment === "delivery" ? "active" : ""}">配送到宿舍</button></div></section>
${state.fulfillment === "delivery" ? `<section class="form-section"><div class="field"><label>宿舍楼或具体位置 *</label><input name="address" placeholder="例如3号男生宿舍楼门口" required></div><div class="field"><label>期望送达时间 *</label><input name="time" type="time" required></div><p class="helper">${data.brand.deliveryNote}</p></section>` : `<section class="form-section"><div class="field"><label>期望取餐时间 *</label><input name="time" type="time" required></div></section>`}
<section class="form-section"><h2>联系信息</h2><div class="field"><label>称呼 *</label><input name="name" value="陈同学" required></div><div class="field"><label>手机号或微信号 *</label><input name="contact" value="wx_chen2026" required></div><div class="field"><label>联系偏好</label><select name="contactPreference"><option value="none">无需联系,无法满足时再联系</option><option value="wechat">优先微信联系</option><option value="phone">优先电话联系</option></select></div><div class="field"><label>备注</label><textarea name="note" placeholder="口味、餐具等要求"></textarea></div></section>
<button class="primary-button" style="width:100%;margin-top:16px" type="submit">提交订单请求</button>
</form>`;
}
function renderChefRequest() {
return `${pageHeader("提交私厨需求")}
<form class="form-page" id="chef-form">
<div class="summary-box">提交后由经营者评估并联系报价,不产生费用,也不代表已经确认。</div>
<section class="form-section"><h2>用餐安排</h2><div class="field"><label>期望日期 *</label><input name="date" type="date" required></div><div class="field"><label>用餐人数 *</label><input name="people" type="number" min="1" placeholder="例如8" required></div><div class="field"><label>用餐地点或区域 *</label><input name="location" placeholder="填写大致区域即可" required></div></section>
<section class="form-section"><h2>需求偏好</h2><div class="field"><label>预算范围</label><select><option>暂不确定</option><option>500元以内</option><option>500-800元</option><option>800-1200元</option><option>1200元以上</option></select></div><div class="field"><label>口味偏好</label><input placeholder="例如:家常、清淡、微辣"></div><div class="field"><label>忌口或过敏信息</label><textarea placeholder="例如:花生过敏、不吃香菜"></textarea></div><div class="field"><label>想吃的菜或其他说明</label><textarea placeholder="选填,不需要现在确定完整菜单"></textarea></div></section>
<section class="form-section"><h2>怎么联系你</h2><div class="field"><label>称呼 *</label><input name="name" required></div><div class="field"><label>手机号或微信号 *</label><input name="contact" required></div><div class="field"><label>联系偏好</label><select><option>微信联系</option><option>电话联系</option></select></div></section>
<button class="primary-button" style="width:100%;margin-top:16px" type="submit">提交私厨需求</button>
</form>`;
}
function renderOrders() {
return `${pageHeader("我的订单")}<section class="block">${state.orders.map((order) => `<article class="order-card"><div class="order-card-head"><span class="status ${order.status}">${statusLabels[order.status]}</span><span class="helper">${order.createdAt}</span></div><h3>${order.summary}</h3><p>${order.fulfillment}</p><p>订单号 ${order.id}</p>${order.status === "received" ? `<button class="secondary-button" data-cancel-order="${order.id}">取消订单</button>` : ""}</article>`).join("") || `<div class="empty-state">还没有订单</div>`}</section>`;
}
function renderBrand() {
return `${pageHeader("关于阿成")}<section class="block"><img src="${data.portfolio[0].image}" alt="家常菜制作" style="width:100%;aspect-ratio:16/9;object-fit:cover;border-radius:8px"><h2>${data.brand.name}</h2><p>${data.brand.description}</p><div class="quote"><strong>营业时间</strong><p>${data.brand.hours}</p><strong>摊位位置</strong><p>${data.brand.address}</p><strong>联系电话</strong><p>${data.brand.phone}</p></div></section>`;
}
function renderServices() {
return `${pageHeader("服务")}
<section class="block"><div class="block-heading"><h2>普通套餐</h2></div><div class="service-list">${data.packages.map((item) => `<article class="service-card"><img src="${item.image}" alt="${item.name}"><div><h3>${item.name}</h3><p>${item.description}</p><span class="price">¥${item.price}</span></div></article>`).join("")}</div><button class="primary-button" data-action="package-order" style="width:100%;margin-top:12px">选择套餐</button></section>
<section class="block"><div class="block-heading"><h2>私厨定制</h2></div><div class="quote"><p>适合家庭聚餐、朋友小聚和小型活动。根据人数、地点和口味人工沟通报价。</p></div><button class="secondary-button" data-action="chef-request" style="width:100%">提交私厨需求</button></section>`;
}
function renderMiniapp() {
const views = { home: renderHome, package: renderPackageOrder, chef: renderChefRequest, orders: renderOrders, brand: renderBrand, services: renderServices };
document.querySelector("#miniapp-content").innerHTML = (views[state.miniRoute] || renderHome)();
}
function orderRows(orders) {
return orders.map((order) => `<tr data-order-id="${order.id}"><td>${order.unread ? '<i class="unread-dot"></i>' : ""}${order.id}</td><td><strong>${order.customer}</strong><br><span class="helper">${order.type === "package" ? "普通套餐" : "私厨需求"}</span></td><td>${order.summary}</td><td><span class="status ${order.status}">${statusLabels[order.status]}</span></td><td>${order.tags.map((tag) => `<span class="tag">${tag}</span>`).join("")}</td><td>${order.createdAt}</td></tr>`).join("");
}
function renderDashboard() {
return `<div class="admin-page"><div class="admin-page-header"><div><h1>经营概览</h1><span class="helper">今天也先把每一份订单处理好</span></div></div><div class="metric-grid"><div class="metric"><span>未读订单</span><strong>${state.orders.filter((o) => o.unread).length}</strong></div><div class="metric"><span>待处理</span><strong>${state.orders.filter((o) => o.status === "received").length}</strong></div><div class="metric"><span>沟通中的私厨</span><strong>${state.orders.filter((o) => o.type === "private_chef" && o.status === "contacting").length}</strong></div></div><div class="panel"><div class="panel-header"><h2>最近订单</h2><button class="text-button" data-admin-action="all-orders">查看全部</button></div><table class="order-table"><thead><tr><th>订单</th><th>客户</th><th>内容</th><th>状态</th><th>标签</th><th>提交时间</th></tr></thead><tbody>${orderRows(state.orders)}</tbody></table></div></div>`;
}
function renderAdminOrders() {
const visible = state.orderFilter === "all" ? state.orders : state.orders.filter((item) => item.type === state.orderFilter);
return `<div class="admin-page"><div class="admin-page-header"><div><h1>订单管理</h1><span class="helper">普通套餐和私厨需求统一进入这里</span></div><button class="secondary-button" data-admin-action="create-tag">+ 新建标签</button></div><div class="panel"><div class="filter-row"><button class="${state.orderFilter === "all" ? "active" : ""}" data-order-filter="all">全部</button><button class="${state.orderFilter === "package" ? "active" : ""}" data-order-filter="package">普通套餐</button><button class="${state.orderFilter === "private_chef" ? "active" : ""}" data-order-filter="private_chef">私厨需求</button></div><table class="order-table"><thead><tr><th>订单</th><th>客户</th><th>内容</th><th>状态</th><th>标签</th><th>提交时间</th></tr></thead><tbody>${orderRows(visible)}</tbody></table></div></div>`;
}
function renderOrderDetail(order) {
const allTags = ["普通套餐", "私厨", "配送", "自取", "熟客", "重点跟进"];
return `<div class="admin-page"><div class="admin-page-header"><div><button class="text-button" data-admin-action="all-orders">← 返回订单</button><h1>${order.type === "package" ? "普通套餐订单" : "私厨需求"}</h1><span class="helper">${order.id} · ${order.createdAt}</span></div><select id="admin-status"><option value="received" ${order.status === "received" ? "selected" : ""}>已收到</option><option value="contacting" ${order.status === "contacting" ? "selected" : ""}>沟通中</option><option value="confirmed" ${order.status === "confirmed" ? "selected" : ""}>已确认</option><option value="completed" ${order.status === "completed" ? "selected" : ""}>已完成</option><option value="cancelled" ${order.status === "cancelled" ? "selected" : ""}>已取消</option></select></div><div class="detail-grid"><div class="panel"><div class="detail-section"><h3>订单内容</h3><div class="key-value"><span>客户</span><strong>${order.customer}</strong></div><div class="key-value"><span>内容</span><strong>${order.summary}</strong></div><div class="key-value"><span>履约</span><strong>${order.fulfillment}</strong></div><div class="key-value"><span>预计金额</span><strong>${order.amount === null ? "人工沟通报价" : `¥${order.amount}`}</strong></div><div class="key-value"><span>联系偏好</span><strong>${order.contact}</strong></div></div><div class="detail-section"><h3>内部备忘</h3><textarea id="admin-note" style="width:100%;min-height:100px;padding:10px" placeholder="仅后台可见">${escapeHtml(order.note)}</textarea><button class="primary-button" data-admin-action="save-order" style="margin-top:10px">保存处理结果</button></div></div><aside class="panel"><div class="detail-section"><h3>订单标签</h3><div class="tag-picker">${allTags.map((tag) => `<button class="${order.tags.includes(tag) ? "active" : ""}" data-toggle-tag="${tag}">${tag}</button>`).join("")}</div></div><div class="detail-section"><h3>客户可见范围</h3><p class="helper">客户只会看到订单内容和粗粒度状态。标签、内部备忘、报价过程不会展示。</p></div></aside></div></div>`;
}
function renderPlaceholder(title, body) {
return `<div class="admin-page"><div class="admin-page-header"><h1>${title}</h1></div><div class="panel"><div class="empty-state">${body}</div></div></div>`;
}
function renderAdmin() {
let html;
if (state.adminRoute === "dashboard") html = renderDashboard();
else if (state.adminRoute === "orders") html = renderAdminOrders();
else if (state.adminRoute === "order-detail") html = renderOrderDetail(state.orders.find((item) => item.id === state.selectedOrderId));
else if (state.adminRoute === "services") html = renderPlaceholder("套餐与服务", "每日菜单维护方式尚待朋友确认。当前原型只保留固定套餐和启用状态。");
else if (state.adminRoute === "brand") html = renderPlaceholder("品牌资料", "编辑品牌名称、介绍、营业时间、摊位位置和配送说明。");
else if (state.adminRoute === "portfolio") html = renderPlaceholder("作品集", "管理招牌菜、日常套餐和私厨案例图片。");
else html = renderPlaceholder("客户反馈", "审核后才会公开展示客户反馈。");
document.querySelector("#admin-content").innerHTML = html;
}
document.addEventListener("click", (event) => {
const mode = event.target.closest("[data-mode]");
if (mode) {
document.querySelectorAll("[data-mode]").forEach((button) => button.classList.toggle("active", button === mode));
document.querySelectorAll(".mode-panel").forEach((panel) => panel.classList.toggle("active", panel.id === `${mode.dataset.mode}-shell`));
}
const route = event.target.closest("[data-route]");
if (route) setMiniRoute(route.dataset.route);
const action = event.target.closest("[data-action]")?.dataset.action;
if (action === "package-order") { state.miniRoute = "package"; renderMiniapp(); }
if (action === "chef-request") { state.miniRoute = "chef"; renderMiniapp(); }
if (action === "brand") setMiniRoute("brand");
if (action === "back-home") setMiniRoute("home");
const qty = event.target.closest("[data-qty]");
if (qty) { state.quantities[qty.dataset.qty] = Math.max(0, state.quantities[qty.dataset.qty] + Number(qty.dataset.delta)); renderMiniapp(); }
const fulfillment = event.target.closest("[data-fulfillment]");
if (fulfillment) { state.fulfillment = fulfillment.dataset.fulfillment; renderMiniapp(); }
const cancel = event.target.closest("[data-cancel-order]");
if (cancel) { const order = state.orders.find((item) => item.id === cancel.dataset.cancelOrder); order.status = "cancelled"; renderMiniapp(); showToast("订单已取消"); }
const adminRoute = event.target.closest("[data-admin-route]");
if (adminRoute) { state.adminRoute = adminRoute.dataset.adminRoute; document.querySelectorAll("[data-admin-route]").forEach((button) => button.classList.toggle("active", button === adminRoute)); renderAdmin(); }
const orderRow = event.target.closest("[data-order-id]");
if (orderRow) { state.selectedOrderId = orderRow.dataset.orderId; const order = state.orders.find((item) => item.id === state.selectedOrderId); order.unread = false; state.adminRoute = "order-detail"; renderAdmin(); }
const filter = event.target.closest("[data-order-filter]");
if (filter) { state.orderFilter = filter.dataset.orderFilter; renderAdmin(); }
const adminAction = event.target.closest("[data-admin-action]")?.dataset.adminAction;
if (adminAction === "all-orders") { state.adminRoute = "orders"; renderAdmin(); }
if (adminAction === "create-tag") showToast("原型:这里打开新建标签弹窗");
const toggleTag = event.target.closest("[data-toggle-tag]");
if (toggleTag) { const order = state.orders.find((item) => item.id === state.selectedOrderId); const tag = toggleTag.dataset.toggleTag; order.tags = order.tags.includes(tag) ? order.tags.filter((item) => item !== tag) : [...order.tags, tag]; renderAdmin(); }
if (adminAction === "save-order") { const order = state.orders.find((item) => item.id === state.selectedOrderId); order.status = document.querySelector("#admin-status").value; order.note = document.querySelector("#admin-note").value; renderAdmin(); showToast("处理结果已保存"); }
});
document.addEventListener("submit", (event) => {
event.preventDefault();
if (event.target.id === "package-form") {
const total = data.packages.reduce((sum, item) => sum + item.price * state.quantities[item.id], 0);
if (!total) { showToast("请至少选择一份套餐"); return; }
state.orders.unshift({ id: `AC${Date.now().toString().slice(-11)}`, type: "package", customer: "陈同学", summary: "新提交的套餐订单", amount: total, fulfillment: state.fulfillment === "pickup" ? "自取" : "配送", contact: "按客户偏好", status: "received", unread: true, tags: ["普通套餐", state.fulfillment === "pickup" ? "自取" : "配送"], note: "", createdAt: "刚刚" });
} else {
state.orders.unshift({ id: `AC${Date.now().toString().slice(-11)}`, type: "private_chef", customer: "新客户", summary: "新提交的私厨需求", amount: null, fulfillment: "待评估", contact: "微信联系", status: "received", unread: true, tags: ["私厨"], note: "", createdAt: "刚刚" });
}
state.miniRoute = "orders";
renderMiniapp();
showToast("提交成功,已进入我的订单");
});
renderMiniapp();
renderAdmin();

60
prototype/index.html Normal file
View File

@@ -0,0 +1,60 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>个人品牌小程序 · 低保真原型</title>
<link rel="stylesheet" href="./styles.css" />
</head>
<body>
<header class="prototype-bar">
<div>
<strong>餐饮个人品牌 · 交互原型</strong>
<span id="prototype-note">使用虚拟数据,仅验证流程</span>
</div>
<div class="mode-switch" role="group" aria-label="切换原型端">
<button class="mode-button active" data-mode="miniapp">客户小程序</button>
<button class="mode-button" data-mode="admin">经营后台</button>
</div>
</header>
<main>
<section id="miniapp-shell" class="mode-panel active" aria-label="客户小程序原型">
<div class="phone-shell">
<div class="phone-status"><span>9:41</span><span>微信小程序</span></div>
<div id="miniapp-content" class="phone-content"></div>
<nav class="tabbar" aria-label="小程序导航">
<button data-route="home" class="active"><span></span>首页</button>
<button data-route="services"><span></span>服务</button>
<button data-route="orders"><span></span>订单</button>
<button data-route="brand"><span></span>品牌</button>
</nav>
</div>
</section>
<section id="admin-shell" class="mode-panel" aria-label="经营后台原型">
<div class="admin-app">
<aside class="sidebar">
<div class="admin-brand"><span></span><strong>阿成家常菜</strong></div>
<nav aria-label="后台导航">
<button class="active" data-admin-route="dashboard">概览</button>
<button data-admin-route="orders">订单管理 <b>1</b></button>
<button data-admin-route="services">套餐与服务</button>
<button data-admin-route="brand">品牌资料</button>
<button data-admin-route="portfolio">作品集</button>
<button data-admin-route="feedback">客户反馈</button>
</nav>
</aside>
<section class="admin-main">
<header class="admin-topbar"><span>试点环境</span><button class="quiet-button">管理员 · 阿成</button></header>
<div id="admin-content"></div>
</section>
</div>
</section>
</main>
<div id="toast" class="toast" role="status" aria-live="polite"></div>
<script src="./mock-data.js"></script>
<script src="./app.js"></script>
</body>
</html>

30
prototype/mock-data.js vendored Normal file
View File

@@ -0,0 +1,30 @@
window.PROTOTYPE_DATA = {
brand: {
name: "阿成家常菜",
tagline: "现做家常套餐,也为小型聚餐定制一桌热饭菜",
description: "每天认真做好一顿家常饭。日常供应现做套餐,也接受家庭聚餐、朋友小聚和小型活动的私厨定制。",
hours: "周一至周六 10:30-20:00",
address: "大学城生活区东门附近",
deliveryNote: "目前支持部分学生宿舍楼配送,无法送达时会主动联系。",
phone: "138 0000 6628"
},
packages: [
{ id: "pkg-1", name: "红烧肉双拼套餐", price: 28, description: "红烧肉、时蔬、米饭,可选搭配卤蛋", image: "https://images.unsplash.com/photo-1547592180-85f173990554?auto=format&fit=crop&w=900&q=80" },
{ id: "pkg-2", name: "小炒鸡套餐", price: 25, description: "现炒鸡块、时蔬、米饭,默认微辣", image: "https://images.unsplash.com/photo-1601050690597-df0568f70950?auto=format&fit=crop&w=900&q=80" },
{ id: "pkg-3", name: "番茄牛腩套餐", price: 32, description: "番茄牛腩、时蔬、米饭", image: "https://images.unsplash.com/photo-1604908176997-125f25cc6f3d?auto=format&fit=crop&w=900&q=80" }
],
portfolio: [
{ title: "六人家庭聚餐", category: "家庭聚餐", image: "https://images.unsplash.com/photo-1515003197210-e0cd71810b5f?auto=format&fit=crop&w=900&q=80" },
{ title: "春季家常菜单", category: "季节菜单", image: "https://images.unsplash.com/photo-1543353071-873f17a7a088?auto=format&fit=crop&w=900&q=80" },
{ title: "朋友小聚八菜一汤", category: "私厨案例", image: "https://images.unsplash.com/photo-1504674900247-0877df9cc836?auto=format&fit=crop&w=900&q=80" }
],
feedback: [
{ name: "周同学", content: "分量很足,提前说了少辣,口味刚好。" },
{ name: "林女士", content: "家庭聚餐菜单沟通得很细,老人孩子都吃得惯。" }
],
orders: [
{ id: "AC20260918001", type: "package", customer: "陈同学", summary: "红烧肉双拼套餐 x2", amount: 56, fulfillment: "配送 · 3号男生宿舍楼", contact: "无需联系", status: "received", unread: true, tags: ["普通套餐", "配送"], note: "不要香菜", createdAt: "今天 09:42" },
{ id: "AC20260917003", type: "private_chef", customer: "王女士", summary: "8人家庭聚餐 · 预算 800-1000", amount: null, fulfillment: "滨江区9月21日晚餐", contact: "微信联系", status: "contacting", unread: false, tags: ["私厨", "重点跟进"], note: "有两位老人,口味清淡;一人花生过敏", createdAt: "昨天 18:10" },
{ id: "AC20260916008", type: "package", customer: "刘同学", summary: "番茄牛腩套餐 x1", amount: 32, fulfillment: "自取 · 18:20", contact: "电话联系", status: "confirmed", unread: false, tags: ["普通套餐", "自取", "熟客"], note: "", createdAt: "9月16日 16:32" }
]
};

151
prototype/styles.css Normal file
View File

@@ -0,0 +1,151 @@
:root {
--ink: #202522;
--muted: #667069;
--line: #d9dedb;
--surface: #ffffff;
--canvas: #f2f4f2;
--brand: #b2382f;
--brand-dark: #84271f;
--green: #2f684a;
--yellow: #f1c75b;
--blue: #315f78;
--radius: 8px;
}
* { box-sizing: border-box; }
body { margin: 0; color: var(--ink); background: var(--canvas); font: 14px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; letter-spacing: 0; }
button, input, select, textarea { font: inherit; letter-spacing: 0; }
button { cursor: pointer; }
.prototype-bar { height: 64px; padding: 0 24px; display: flex; align-items: center; justify-content: space-between; color: white; background: #202522; }
.prototype-bar strong { display: block; font-size: 15px; }
.prototype-bar span { color: #b8c1bb; font-size: 12px; }
.mode-switch { display: flex; border: 1px solid #56605a; border-radius: 6px; overflow: hidden; }
.mode-button { border: 0; padding: 8px 14px; color: #dce1de; background: transparent; }
.mode-button.active { color: #202522; background: white; }
.mode-panel { display: none; min-height: calc(100vh - 64px); }
.mode-panel.active { display: grid; place-items: center; }
.phone-shell { width: min(390px, 100vw); height: min(780px, calc(100vh - 88px)); min-height: 620px; position: relative; overflow: hidden; background: white; border: 1px solid #bcc4bf; border-radius: 24px; box-shadow: 0 12px 36px rgba(31, 40, 34, .12); }
.phone-status { height: 30px; padding: 0 16px; display: flex; align-items: center; justify-content: space-between; font-size: 11px; border-bottom: 1px solid #edf0ee; }
.phone-content { height: calc(100% - 88px); overflow-y: auto; padding-bottom: 24px; }
.tabbar { height: 58px; display: grid; grid-template-columns: repeat(4, 1fr); border-top: 1px solid var(--line); background: white; }
.tabbar button { border: 0; color: var(--muted); background: white; font-size: 11px; }
.tabbar button span { display: block; height: 24px; font-size: 19px; line-height: 22px; }
.tabbar button.active { color: var(--brand); font-weight: 700; }
.page-header { position: sticky; top: 0; z-index: 4; min-height: 50px; display: flex; align-items: center; gap: 10px; padding: 10px 16px; background: rgba(255,255,255,.96); border-bottom: 1px solid #edf0ee; }
.page-header h1 { margin: 0; font-size: 17px; }
.back-button { width: 30px; height: 30px; border: 0; border-radius: 50%; background: #edf0ee; }
.hero { min-height: 260px; padding: 24px 20px; color: white; display: flex; flex-direction: column; justify-content: flex-end; background: linear-gradient(0deg, rgba(20,25,22,.78), rgba(20,25,22,.08)), url('https://images.unsplash.com/photo-1556911220-bff31c812dba?auto=format&fit=crop&w=1000&q=80') center/cover; }
.hero-badge { width: max-content; padding: 4px 8px; margin-bottom: 8px; color: #222; background: var(--yellow); border-radius: 4px; font-size: 12px; font-weight: 700; }
.hero h1 { margin: 0; font-size: 30px; line-height: 1.18; }
.hero p { margin: 8px 0 18px; color: #f1f3f1; }
.button-row { display: flex; gap: 8px; }
.primary-button, .secondary-button { min-height: 42px; padding: 0 16px; border-radius: 6px; font-weight: 700; }
.primary-button { border: 1px solid var(--brand); color: white; background: var(--brand); }
.primary-button:hover { background: var(--brand-dark); }
.secondary-button { border: 1px solid var(--line); color: var(--ink); background: white; }
.block { padding: 20px 16px 0; }
.block-heading { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; }
.block-heading h2 { margin: 0; font-size: 18px; }
.text-button { border: 0; color: var(--brand); background: transparent; }
.service-list { display: grid; gap: 10px; }
.service-card { display: grid; grid-template-columns: 92px 1fr; overflow: hidden; border: 1px solid var(--line); border-radius: var(--radius); background: white; }
.service-card img { width: 92px; height: 100%; min-height: 92px; object-fit: cover; }
.service-card div { padding: 11px; }
.service-card h3 { margin: 0 0 4px; font-size: 15px; }
.service-card p { margin: 0 0 8px; color: var(--muted); font-size: 12px; }
.price { color: var(--brand); font-size: 16px; font-weight: 800; }
.portfolio-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px; }
.portfolio-item { overflow: hidden; border-radius: var(--radius); background: #e8ece9; }
.portfolio-item img { width: 100%; aspect-ratio: 4/3; display: block; object-fit: cover; }
.portfolio-item div { padding: 8px; }
.portfolio-item strong, .portfolio-item span { display: block; }
.portfolio-item span { color: var(--muted); font-size: 11px; }
.quote { padding: 12px; margin-bottom: 8px; border-left: 3px solid var(--green); background: #f4f7f5; }
.quote p { margin: 0 0 5px; }
.quote span { color: var(--muted); font-size: 12px; }
.form-page { padding: 0 16px 28px; }
.form-section { padding: 16px 0; border-bottom: 1px solid #edf0ee; }
.form-section h2 { margin: 0 0 12px; font-size: 15px; }
.field { display: grid; gap: 5px; margin-bottom: 12px; }
.field label { font-size: 12px; font-weight: 700; }
.field input, .field select, .field textarea { width: 100%; min-height: 42px; padding: 9px 10px; border: 1px solid #c8cfca; border-radius: 6px; background: white; }
.field textarea { min-height: 76px; resize: vertical; }
.segmented { display: grid; grid-template-columns: repeat(2, 1fr); gap: 6px; }
.segmented button { min-height: 40px; border: 1px solid var(--line); border-radius: 6px; background: white; }
.segmented button.active { color: var(--brand); border-color: var(--brand); background: #fff5f3; }
.qty-row { display: grid; grid-template-columns: 1fr 92px; align-items: center; gap: 8px; padding: 10px 0; border-bottom: 1px solid #edf0ee; }
.stepper { display: grid; grid-template-columns: 28px 32px 28px; align-items: center; text-align: center; }
.stepper button { width: 28px; height: 28px; border: 1px solid var(--line); background: white; }
.summary-box { padding: 12px; margin: 14px 0; background: #f7f3e8; border: 1px solid #eadcad; border-radius: 6px; }
.summary-box strong { float: right; color: var(--brand); font-size: 18px; }
.helper { color: var(--muted); font-size: 12px; }
.order-card { padding: 14px; margin-bottom: 10px; border: 1px solid var(--line); border-radius: var(--radius); background: white; }
.order-card-head { display: flex; justify-content: space-between; gap: 10px; }
.order-card h3 { margin: 5px 0; font-size: 15px; }
.order-card p { margin: 4px 0; color: var(--muted); font-size: 12px; }
.status { width: max-content; padding: 3px 7px; border-radius: 4px; color: #614900; background: #fff1b7; font-size: 11px; font-weight: 700; }
.status.contacting { color: #254d64; background: #e6f2f8; }
.status.confirmed { color: #24513a; background: #e4f3e9; }
.status.completed { color: #4c5350; background: #e9eeeb; }
.status.cancelled { color: #7c2b27; background: #f8e8e6; }
.empty-state { padding: 36px 20px; text-align: center; color: var(--muted); }
.admin-app { width: min(1400px, calc(100vw - 40px)); height: calc(100vh - 104px); min-height: 650px; display: grid; grid-template-columns: 220px 1fr; overflow: hidden; border: 1px solid #cbd2ce; background: white; }
.sidebar { color: #e9edea; background: #26312b; }
.admin-brand { height: 68px; display: flex; align-items: center; gap: 10px; padding: 0 18px; border-bottom: 1px solid #3c4841; }
.admin-brand span { width: 32px; height: 32px; display: grid; place-items: center; color: #222; background: var(--yellow); border-radius: 6px; font-weight: 800; }
.sidebar nav { padding: 12px 8px; }
.sidebar nav button { width: 100%; min-height: 42px; display: flex; align-items: center; justify-content: space-between; padding: 0 12px; border: 0; border-radius: 5px; color: #cdd5d0; background: transparent; text-align: left; }
.sidebar nav button.active { color: white; background: #3b4941; }
.sidebar nav b { min-width: 20px; padding: 1px 6px; color: white; background: var(--brand); border-radius: 10px; text-align: center; font-size: 11px; }
.admin-main { min-width: 0; overflow: auto; background: #f5f7f5; }
.admin-topbar { height: 68px; display: flex; align-items: center; justify-content: space-between; padding: 0 24px; border-bottom: 1px solid var(--line); background: white; }
.quiet-button { border: 0; color: var(--muted); background: transparent; }
.admin-page { padding: 24px; }
.admin-page-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 20px; }
.admin-page-header h1 { margin: 0; font-size: 24px; }
.metric-grid { display: grid; grid-template-columns: repeat(3, minmax(160px, 1fr)); gap: 12px; margin-bottom: 20px; }
.metric { padding: 18px; border: 1px solid var(--line); border-radius: var(--radius); background: white; }
.metric span { color: var(--muted); }
.metric strong { display: block; margin-top: 7px; font-size: 28px; }
.panel { border: 1px solid var(--line); border-radius: var(--radius); background: white; }
.panel-header { padding: 14px 16px; display: flex; align-items: center; justify-content: space-between; border-bottom: 1px solid var(--line); }
.panel-header h2 { margin: 0; font-size: 16px; }
.filter-row { display: flex; gap: 8px; padding: 12px 16px; border-bottom: 1px solid var(--line); }
.filter-row button { min-height: 32px; padding: 0 10px; border: 1px solid var(--line); border-radius: 5px; background: white; }
.filter-row button.active { color: white; border-color: var(--green); background: var(--green); }
.order-table { width: 100%; border-collapse: collapse; }
.order-table th, .order-table td { padding: 12px 14px; border-bottom: 1px solid #edf0ee; text-align: left; vertical-align: top; }
.order-table th { color: var(--muted); background: #fafbfa; font-size: 12px; }
.order-table tbody tr { cursor: pointer; }
.order-table tbody tr:hover { background: #fafcfb; }
.unread-dot { width: 7px; height: 7px; display: inline-block; margin-right: 6px; border-radius: 50%; background: var(--brand); }
.tag { display: inline-block; padding: 2px 6px; margin: 2px; color: #34423a; background: #e8eeea; border-radius: 4px; font-size: 11px; }
.detail-grid { display: grid; grid-template-columns: minmax(0, 1.5fr) minmax(260px, .8fr); gap: 16px; }
.detail-section { padding: 16px; border-bottom: 1px solid var(--line); }
.detail-section:last-child { border-bottom: 0; }
.detail-section h3 { margin: 0 0 12px; font-size: 15px; }
.key-value { display: grid; grid-template-columns: 110px 1fr; gap: 8px; margin: 7px 0; }
.key-value span { color: var(--muted); }
.tag-picker { display: flex; flex-wrap: wrap; gap: 6px; }
.tag-picker button { padding: 5px 8px; border: 1px solid var(--line); border-radius: 4px; background: white; }
.tag-picker button.active { color: #24513a; border-color: #86aa93; background: #e4f3e9; }
.toast { position: fixed; left: 50%; bottom: 24px; z-index: 20; padding: 10px 14px; color: white; background: #202522; border-radius: 6px; opacity: 0; pointer-events: none; transform: translate(-50%, 10px); transition: .2s; }
.toast.show { opacity: 1; transform: translate(-50%, 0); }
.hidden { display: none !important; }
@media (max-width: 800px) {
.prototype-bar { height: auto; min-height: 72px; padding: 10px 12px; gap: 8px; }
.prototype-bar span { display: none; }
.mode-button { padding: 7px 9px; }
.mode-panel { min-height: calc(100vh - 72px); }
.phone-shell { height: calc(100vh - 72px); min-height: 560px; border: 0; border-radius: 0; }
.admin-app { width: 100vw; height: calc(100vh - 72px); min-height: 560px; grid-template-columns: 76px 1fr; border: 0; }
.admin-brand { justify-content: center; padding: 0; }
.admin-brand strong { display: none; }
.sidebar nav button { justify-content: center; padding: 8px 4px; font-size: 11px; text-align: center; }
.sidebar nav b { display: none; }
.admin-page { padding: 14px; }
.metric-grid { grid-template-columns: 1fr; }
.detail-grid { grid-template-columns: 1fr; }
.order-table th:nth-child(3), .order-table td:nth-child(3), .order-table th:nth-child(5), .order-table td:nth-child(5) { display: none; }
}

23
tsconfig.base.json Normal file
View File

@@ -0,0 +1,23 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022"],
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noImplicitOverride": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"isolatedModules": true,
"verbatimModuleSyntax": true,
"skipLibCheck": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true
}
}

10
turbo.json Normal file
View File

@@ -0,0 +1,10 @@
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"build": { "dependsOn": ["^build"], "outputs": ["dist/**", ".next/**"] },
"dev": { "cache": false, "persistent": true },
"lint": { "dependsOn": ["^lint"], "outputs": [] },
"typecheck": { "dependsOn": ["^typecheck"], "outputs": [] },
"clean": { "cache": false }
}
}