Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,16 @@ public/chunk-manifest.json
public/service.worker.js

#db
db/generated/
db/generated/

# Environment variables
.env.local

# IDE
.cursor/

# OS
.DS_Store

# Logs
*.log
30 changes: 0 additions & 30 deletions db/Untitled-1

This file was deleted.

10 changes: 9 additions & 1 deletion db/schema/enums.ts
Original file line number Diff line number Diff line change
Expand Up @@ -426,4 +426,12 @@ export const REGISLET_TYPE = [
"SmashEnhance",
"SonicWaveEnhance",
] as const;
export type RegisletType = (typeof REGISLET_TYPE)[number];
export type RegisletType = (typeof REGISLET_TYPE)[number];

// 料理审核状态
export const DISH_REVIEW_STATUS = ["Pending", "Approved", "Rejected"] as const;
export type DishReviewStatus = (typeof DISH_REVIEW_STATUS)[number];

// 料理来源
export const DISH_SOURCE = ["Web", "QQBot"] as const;
export type DishSource = (typeof DISH_SOURCE)[number];
42 changes: 42 additions & 0 deletions db/schema/models/data.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -837,3 +837,45 @@ model member {
belongToTeam team @relation(fields: [belongToTeamId], references: [id], onDelete: Cascade)
belongToTeamId String
}

// 料理表
model dish {
id String @id

name String // 料理名字
level Int // 料理等级 (1-10)
playerId String // 门牌号(玩家ID)
source String // Enum DISH_SOURCE 来源:Web或QQBot
status String // Enum DISH_REVIEW_STATUS 审核状态:Pending/Approved/Rejected
qqNumber String? // QQ号(如果是QQ机器人提交的)
remark String? // 备注/审核意见

createdAt DateTime
updatedAt DateTime @updatedAt

reviewedAt DateTime? // 审核时间
reviewedById String? // 审核人ID
reviewedBy account? @relation("dish_reviewed_by", fields: [reviewedById], references: [id], onDelete: SetNull)

submittedById String // 提交人ID
submittedBy account @relation("dish_submitted_by", fields: [submittedById], references: [id], onDelete: Cascade)

@@index([status])
@@index([level])
@@index([playerId])
}

// 料理配置表
model dish_config {
id String @id

key String @unique // 配置键名
value String // 配置值
remark String? // 备注

createdAt DateTime
updatedAt DateTime @updatedAt

updatedById String? // 更新人ID
updatedBy account? @relation("dish_config_updated_by", fields: [updatedById], references: [id], onDelete: SetNull)
}
5 changes: 5 additions & 0 deletions db/schema/models/user.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ model account {
create account_create_data?
update account_update_data?

// 料理相关
submittedDishes dish[] @relation("dish_submitted_by")
reviewedDishes dish[] @relation("dish_reviewed_by")
dishConfigs dish_config[] @relation("dish_config_updated_by")

@@unique([provider, providerAccountId])
}

Expand Down
104 changes: 104 additions & 0 deletions db/scripts/migrate-dish.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/**
* @file migrate-dish.ts
* @description 手动迁移脚本:创建 dish 和 dish_config 表
*/

import path from "node:path";
import { fileURLToPath } from "node:url";
import dotenv from "dotenv";
import dotenvExpand from "dotenv-expand";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

// 加载环境变量
dotenvExpand.expand(dotenv.config({ path: path.join(__dirname, "../.env") }));

const PG_HOST = process.env.PG_HOST?.replace("${VITE_SERVER_HOST}", "localhost") || "localhost";
const PG_PORT = parseInt(process.env.PG_PORT || "5432");
const PG_USERNAME = process.env.PG_USERNAME || "postgres";
const PG_PASSWORD = process.env.PG_PASSWORD || "123456";
const PG_DBNAME = process.env.PG_DBNAME || "postgres";

const createTablesSQL = `
-- 创建 dish 表
CREATE TABLE IF NOT EXISTS "dish" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"level" INTEGER NOT NULL,
"playerId" TEXT NOT NULL,
"source" TEXT NOT NULL,
"status" TEXT NOT NULL,
"qqNumber" TEXT,
"remark" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL,
"updatedAt" TIMESTAMP(3) NOT NULL,
"reviewedAt" TIMESTAMP(3),
"reviewedById" TEXT,
"submittedById" TEXT NOT NULL,
CONSTRAINT "dish_pkey" PRIMARY KEY ("id")
);

-- 创建 dish_config 表
CREATE TABLE IF NOT EXISTS "dish_config" (
"id" TEXT NOT NULL,
"key" TEXT NOT NULL,
"value" TEXT NOT NULL,
"remark" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL,
"updatedAt" TIMESTAMP(3) NOT NULL,
"updatedById" TEXT,
CONSTRAINT "dish_config_pkey" PRIMARY KEY ("id")
);

-- 创建唯一索引
CREATE UNIQUE INDEX IF NOT EXISTS "dish_config_key_key" ON "dish_config"("key");

-- 创建索引
CREATE INDEX IF NOT EXISTS "dish_status_idx" ON "dish"("status");
CREATE INDEX IF NOT EXISTS "dish_level_idx" ON "dish"("level");
CREATE INDEX IF NOT EXISTS "dish_playerId_idx" ON "dish"("playerId");

-- 添加外键约束
ALTER TABLE "dish" ADD CONSTRAINT "dish_reviewedById_fkey"
FOREIGN KEY ("reviewedById") REFERENCES "account"("id") ON DELETE SET NULL ON UPDATE CASCADE;
ALTER TABLE "dish" ADD CONSTRAINT "dish_submittedById_fkey"
FOREIGN KEY ("submittedById") REFERENCES "account"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "dish_config" ADD CONSTRAINT "dish_config_updatedById_fkey"
FOREIGN KEY ("updatedById") REFERENCES "account"("id") ON DELETE SET NULL ON UPDATE CASCADE;
`;

// 主函数
async function main() {
console.log("开始创建 dish 和 dish_config 表...");
console.log(`连接到: ${PG_HOST}:${PG_PORT}/${PG_DBNAME}`);

// 动态导入 pg
const pg = await import("pg");
const client = new pg.Client({
host: PG_HOST,
port: PG_PORT,
user: PG_USERNAME,
password: PG_PASSWORD,
database: PG_DBNAME,
});

try {
await client.connect();
console.log("✅ 数据库连接成功");

await client.query(createTablesSQL);
console.log("✅ 表创建成功!");

} catch (error) {
console.error("❌ 执行失败:", (error as Error).message);
throw error;
} finally {
await client.end();
}
}

main().catch((error) => {
console.error("迁移失败:", error);
process.exit(1);
});
11 changes: 4 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,26 @@
"type": "module",
"scripts": {
"setup": "pnpm generate && pnpm infra:reset",

"dev": "vite dev",
"start": "PORT=3001 node .output/server/index.mjs",

"generate": "pnpm generate:inject && pnpm generate:schema && pnpm generate:colorSystem",
"generate:inject": "tsx db/generator/injectEnums.ts",
"generate:schema": "prisma generate --schema=db/generated/schema.prisma",
"generate:colorSystem": "tsx src/styles/colorSystem/generator/generator.ts",

"infra:up": "docker compose --env-file .env -f ./backend/docker-compose.yaml up -d",
"infra:stop": "docker compose --env-file .env -f ./backend/docker-compose.yaml stop",
"infra:down": "docker compose --env-file .env -f ./backend/docker-compose.yaml down --volumes",
"infra:reset": "pnpm infra:down && pnpm infra:up && pnpm db:restore",

"db:studio": "prisma studio --config=db/studio.config.ts",
"db:backup": "tsx db/scripts/backup.ts",
"db:restore": "tsx db/scripts/restore.ts",

"build": "node src/worker/sw/build.mjs && NODE_OPTIONS=--max-old-space-size=4096 vite build",

"clean": "pnpm clean:build && pnpm clean:generated",
"clean:generated": "rm -rf db/generated",
"clean:build": "rm -rf dist .output .nitro bundle.tar.gz public/service.worker.js public/chunk-manifest.json",

"package": "tar -czf bundle.tar.gz .output/"
"package": "tar -czf bundle.tar.gz .output/",
"prepare": "husky"
},
"devDependencies": {
"@babylonjs/inspector": "8.53.0",
Expand All @@ -37,6 +32,7 @@
"@types/js-cookie": "^3.0.6",
"@types/node": "^22.7.9",
"@types/pg": "^8.11.11",
"@types/ws": "^8.18.1",
"esbuild": "^0.25.6",
"prisma": "7.2.0",
"tsx": "^4.21.0",
Expand Down Expand Up @@ -93,6 +89,7 @@
"solid-motionone": "^1.0.4",
"tailwindcss": "latest",
"vite": "^7.0.0",
"ws": "^8.20.0",
"xstate": "^5.19.2",
"zod": "^4.1.12"
},
Expand Down
12 changes: 9 additions & 3 deletions src/components/controls/input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,14 @@ interface InputProps extends JSX.InputHTMLAttributes<HTMLInputElement> {
state?: InputStateType;
validationMessage?: string;
inputWidth?: number;
setValue?: (value: string) => void;
}

export const Input = (props: InputProps) => {
const hasChildren = "children" in props;
const id = `input-${createId()}`;
// 从 props 中排除 setValue、value、type,避免传递给 DOM 元素时冲突
const { setValue, value, type, class: classProp, ...restProps } = props;

const getSizeClass = () => {
const sizeMap = {
Expand Down Expand Up @@ -78,32 +81,35 @@ export const Input = (props: InputProps) => {
<Switch fallback={<div>未知类型的输入框</div>}>
<Match when={props.type === "text"}>
<input
{...props}
{...restProps}
id={id}
class={`text-accent-color bg-area-color w-full rounded p-3 ${getDisableClass()} ${getStateClass()}`}
style={{
width: `${props.inputWidth}px`,
}}
onInput={(e) => setValue?.(e.currentTarget.value)}
/>
</Match>
<Match when={props.type === "password"}>
<input
{...props}
{...restProps}
id={id}
class={`text-accent-color bg-area-color w-full rounded p-3 ${getDisableClass()} ${getStateClass()}`}
style={{
width: `${props.inputWidth}px`,
}}
onInput={(e) => setValue?.(e.currentTarget.value)}
/>
</Match>
<Match when={props.type === "number"}>
<input
{...props}
{...restProps}
id={id}
class={`text-accent-color bg-area-color w-full rounded p-3 ${getDisableClass()} ${getStateClass()}`}
style={{
width: `${props.inputWidth}px`,
}}
onInput={(e) => setValue?.(e.currentTarget.value)}
/>
</Match>
<Match when={props.type === "boolean"}>
Expand Down
Loading