commit
8d8af17223
32 changed files with 2318 additions and 0 deletions
@ -0,0 +1 @@ |
|||||
|
DATABASE_URL=mysql://root:xxxxxxxx@localhost:3306/nuxt-db |
||||
@ -0,0 +1,24 @@ |
|||||
|
# Nuxt dev/build outputs |
||||
|
.output |
||||
|
.data |
||||
|
.nuxt |
||||
|
.nitro |
||||
|
.cache |
||||
|
dist |
||||
|
|
||||
|
# Node dependencies |
||||
|
node_modules |
||||
|
|
||||
|
# Logs |
||||
|
logs |
||||
|
*.log |
||||
|
|
||||
|
# Misc |
||||
|
.DS_Store |
||||
|
.fleet |
||||
|
.idea |
||||
|
|
||||
|
# Local env files |
||||
|
.env |
||||
|
.env.* |
||||
|
!.env.example |
||||
@ -0,0 +1,12 @@ |
|||||
|
|
||||
|
## 文档 |
||||
|
|
||||
|
- [nuxt4目录结构](https://nuxt.com/docs/4.x/directory-structure/app/layouts) |
||||
|
- [nuxt4 API](https://nuxt.com/docs/4.x/api/nuxt-config#modulesdir) |
||||
|
- [nitro 文档](https://nitro.build/docs/plugins) |
||||
|
- [drizzle 文档](https://orm.drizzle.org.cn/docs/select) |
||||
|
|
||||
|
|
||||
|
## 开发与部署 |
||||
|
|
||||
|
用 Linux 开发与部署,包管理器采用 bun@1.3.11。数据库为 **MySQL**(通过 `DATABASE_URL` 连接;本地可参考 `.env.example` 复制为 `.env.local`)。部署时可直接打包 `.output` 目录,在服务器环境执行迁移命令,省时省力。 |
||||
@ -0,0 +1,37 @@ |
|||||
|
<script setup lang="ts"> |
||||
|
const { data, pending, error, refresh } = await useFetch('/api/hello') |
||||
|
const userCount = computed(() => data.value?.users?.length ?? 0) |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<main style="padding: 1rem; font-family: sans-serif"> |
||||
|
<NuxtRouteAnnouncer atomic /> |
||||
|
|
||||
|
<h1>Person Panel</h1> |
||||
|
|
||||
|
<p v-if="pending">加载中...</p> |
||||
|
|
||||
|
<div v-else-if="error"> |
||||
|
<p>接口请求失败:{{ error.message }}</p> |
||||
|
<button type="button" @click="refresh()">重试</button> |
||||
|
</div> |
||||
|
|
||||
|
<section v-else> |
||||
|
<p>hello: {{ data?.hello }}</p> |
||||
|
<p>users count: {{ userCount }}</p> |
||||
|
<button type="button" @click="refresh()">刷新数据</button> |
||||
|
<div v-if="Array.isArray(data?.users)"> |
||||
|
<ul> |
||||
|
<li v-for="user in data.users" :key="user.id"> |
||||
|
<span v-if="user.name">姓名:{{ user.name }}</span> |
||||
|
<span v-if="user.email" style="margin-left: 1em;">邮箱:{{ user.email }}</span> |
||||
|
<span v-if="user.age !== undefined" style="margin-left: 1em;">年龄:{{ user.age }}</span> |
||||
|
</li> |
||||
|
</ul> |
||||
|
</div> |
||||
|
<div v-else> |
||||
|
<p>暂无用户信息</p> |
||||
|
</div> |
||||
|
</section> |
||||
|
</main> |
||||
|
</template> |
||||
@ -0,0 +1,53 @@ |
|||||
|
import { drizzle } from 'drizzle-orm/mysql2' |
||||
|
import { migrate } from 'drizzle-orm/mysql2/migrator' |
||||
|
import { createConnection } from 'mysql2/promise' |
||||
|
import path from 'node:path' |
||||
|
import { fileURLToPath } from 'node:url' |
||||
|
|
||||
|
/** |
||||
|
* 考虑做成一个脚本,不是在这里执行,暂时无法保证一定在其它插件前最先运行 |
||||
|
*/ |
||||
|
|
||||
|
/** |
||||
|
* 自动执行 MySQL 数据库迁移 |
||||
|
* 等同于命令:drizzle-kit migrate |
||||
|
*/ |
||||
|
export async function runMigrations() { |
||||
|
const databaseUrl = process.env.DATABASE_URL |
||||
|
if (!databaseUrl) { |
||||
|
throw new Error('DATABASE_URL 未设置') |
||||
|
} |
||||
|
|
||||
|
const connection = await createConnection({ |
||||
|
uri: databaseUrl, |
||||
|
}) |
||||
|
|
||||
|
const db = drizzle(connection) |
||||
|
|
||||
|
try { |
||||
|
console.log('🚀 开始执行 MySQL 迁移...') |
||||
|
const migrationsFolder = path.resolve(process.cwd(), 'migrations') |
||||
|
|
||||
|
await migrate(db, { |
||||
|
migrationsFolder, |
||||
|
}) |
||||
|
|
||||
|
console.log('✅ MySQL 迁移完成!') |
||||
|
} catch (err) { |
||||
|
console.log('❌ 迁移失败:', err) |
||||
|
throw err |
||||
|
} finally { |
||||
|
await connection.end() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const isMain = |
||||
|
process.argv[1] && |
||||
|
path.resolve(process.argv[1]) === fileURLToPath(import.meta.url) |
||||
|
|
||||
|
if (isMain) { |
||||
|
runMigrations().catch((err) => { |
||||
|
console.error(err) |
||||
|
process.exit(1) |
||||
|
}) |
||||
|
} |
||||
@ -0,0 +1,8 @@ |
|||||
|
cd "$(dirname "$0")" |
||||
|
|
||||
|
if [ -f .env ]; then |
||||
|
export $(grep -v '^#' .env | xargs) |
||||
|
fi |
||||
|
|
||||
|
node server/migrate.js |
||||
|
node server/index.mjs |
||||
File diff suppressed because it is too large
@ -0,0 +1,5 @@ |
|||||
|
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||
|
export default defineNuxtConfig({ |
||||
|
compatibilityDate: '2025-07-15', |
||||
|
devtools: { enabled: true }, |
||||
|
}) |
||||
@ -0,0 +1,37 @@ |
|||||
|
{ |
||||
|
"name": "person-panel", |
||||
|
"type": "module", |
||||
|
"packageManager": "bun@1.3.11", |
||||
|
"workspaces": [ |
||||
|
"packages/*" |
||||
|
], |
||||
|
"private": true, |
||||
|
"scripts": { |
||||
|
"build": "nuxt build && bun run cp:db && bun --elide-lines=0 --filter drizzle-pkg build", |
||||
|
"dev": "nuxt dev --dotenv .env.local", |
||||
|
"cp:db": "cp build-files/run.sh .output/run.sh && cp .env.example .output/.env && cp build-files/migrate.js .output/server/migrate.js", |
||||
|
"db:migrate": "bun --elide-lines=0 --filter drizzle-pkg migrate", |
||||
|
"db:generate": "bun --elide-lines=0 --filter drizzle-pkg generate --name", |
||||
|
"db:seed": "bun --elide-lines=0 --filter drizzle-pkg seed", |
||||
|
"generate": "nuxt generate", |
||||
|
"preview": "nuxt preview", |
||||
|
"postinstall": "nuxt prepare" |
||||
|
}, |
||||
|
"dependencies": { |
||||
|
"dotenv": "17.4.1", |
||||
|
"drizzle-pkg": "workspace:*", |
||||
|
"drizzle-orm": "0.45.2", |
||||
|
"drizzle-seed": "0.3.1", |
||||
|
"log4js": "^6.9.1", |
||||
|
"logger": "workspace:*", |
||||
|
"mysql2": "3.22.0", |
||||
|
"nuxt": "4.4.2", |
||||
|
"vue": "3.5.32", |
||||
|
"vue-router": "5.0.4" |
||||
|
}, |
||||
|
"devDependencies": { |
||||
|
"drizzle-kit": "0.31.10", |
||||
|
"tsx": "4.21.0", |
||||
|
"typescript": "6.0.2" |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,11 @@ |
|||||
|
import './env'; |
||||
|
import { defineConfig } from 'drizzle-kit'; |
||||
|
|
||||
|
export default defineConfig({ |
||||
|
out: './migrations', |
||||
|
schema: './lib/schema/*', |
||||
|
dialect: 'mysql', |
||||
|
dbCredentials: { |
||||
|
url: process.env.DATABASE_URL! |
||||
|
}, |
||||
|
}); |
||||
@ -0,0 +1,4 @@ |
|||||
|
|
||||
|
import { config } from 'dotenv'; |
||||
|
|
||||
|
config({ path: '../../.env.local' }); |
||||
@ -0,0 +1,6 @@ |
|||||
|
|
||||
|
import { drizzle } from "drizzle-orm/mysql2"; |
||||
|
|
||||
|
const _db = drizzle({ connection:{ uri: process.env.DATABASE_URL! }}); |
||||
|
|
||||
|
export { _db as dbGlobal } |
||||
@ -0,0 +1,8 @@ |
|||||
|
import { int, mysqlTable, varchar } from "drizzle-orm/mysql-core"; |
||||
|
|
||||
|
export const usersTable = mysqlTable("users_table", { |
||||
|
id: int().primaryKey().autoincrement(), |
||||
|
name: varchar("name", { length: 255 }).notNull(), |
||||
|
age: int().notNull(), |
||||
|
email: varchar("email", { length: 255 }).notNull().unique(), |
||||
|
}); |
||||
@ -0,0 +1,8 @@ |
|||||
|
CREATE TABLE `users_table` ( |
||||
|
`id` int AUTO_INCREMENT NOT NULL, |
||||
|
`name` varchar(255) NOT NULL, |
||||
|
`age` int NOT NULL, |
||||
|
`email` varchar(255) NOT NULL, |
||||
|
CONSTRAINT `users_table_id` PRIMARY KEY(`id`), |
||||
|
CONSTRAINT `users_table_email_unique` UNIQUE(`email`) |
||||
|
); |
||||
@ -0,0 +1,70 @@ |
|||||
|
{ |
||||
|
"version": "5", |
||||
|
"dialect": "mysql", |
||||
|
"id": "dc6e882d-3e66-48c7-8d83-de136ee1cd98", |
||||
|
"prevId": "00000000-0000-0000-0000-000000000000", |
||||
|
"tables": { |
||||
|
"users_table": { |
||||
|
"name": "users_table", |
||||
|
"columns": { |
||||
|
"id": { |
||||
|
"name": "id", |
||||
|
"type": "int", |
||||
|
"primaryKey": false, |
||||
|
"notNull": true, |
||||
|
"autoincrement": true |
||||
|
}, |
||||
|
"name": { |
||||
|
"name": "name", |
||||
|
"type": "varchar(255)", |
||||
|
"primaryKey": false, |
||||
|
"notNull": true, |
||||
|
"autoincrement": false |
||||
|
}, |
||||
|
"age": { |
||||
|
"name": "age", |
||||
|
"type": "int", |
||||
|
"primaryKey": false, |
||||
|
"notNull": true, |
||||
|
"autoincrement": false |
||||
|
}, |
||||
|
"email": { |
||||
|
"name": "email", |
||||
|
"type": "varchar(255)", |
||||
|
"primaryKey": false, |
||||
|
"notNull": true, |
||||
|
"autoincrement": false |
||||
|
} |
||||
|
}, |
||||
|
"indexes": {}, |
||||
|
"foreignKeys": {}, |
||||
|
"compositePrimaryKeys": { |
||||
|
"users_table_id": { |
||||
|
"name": "users_table_id", |
||||
|
"columns": [ |
||||
|
"id" |
||||
|
] |
||||
|
} |
||||
|
}, |
||||
|
"uniqueConstraints": { |
||||
|
"users_table_email_unique": { |
||||
|
"name": "users_table_email_unique", |
||||
|
"columns": [ |
||||
|
"email" |
||||
|
] |
||||
|
} |
||||
|
}, |
||||
|
"checkConstraint": {} |
||||
|
} |
||||
|
}, |
||||
|
"views": {}, |
||||
|
"_meta": { |
||||
|
"schemas": {}, |
||||
|
"tables": {}, |
||||
|
"columns": {} |
||||
|
}, |
||||
|
"internal": { |
||||
|
"tables": {}, |
||||
|
"indexes": {} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,13 @@ |
|||||
|
{ |
||||
|
"version": "7", |
||||
|
"dialect": "mysql", |
||||
|
"entries": [ |
||||
|
{ |
||||
|
"idx": 0, |
||||
|
"version": "5", |
||||
|
"when": 1775843596838, |
||||
|
"tag": "0000_init", |
||||
|
"breakpoints": true |
||||
|
} |
||||
|
] |
||||
|
} |
||||
@ -0,0 +1,9 @@ |
|||||
|
{ |
||||
|
"name": "drizzle-pkg", |
||||
|
"scripts": { |
||||
|
"migrate": "drizzle-kit migrate", |
||||
|
"generate": "drizzle-kit generate", |
||||
|
"build": "sh scripts/mv.sh", |
||||
|
"seed": "bun run seed.ts" |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,7 @@ |
|||||
|
|
||||
|
if [ -d "./migrations" ]; then |
||||
|
cp -r ./migrations ../../.output/ |
||||
|
else |
||||
|
echo "migrations directory not found" |
||||
|
exit 1 |
||||
|
fi |
||||
@ -0,0 +1,25 @@ |
|||||
|
import './env'; |
||||
|
import { seed } from "drizzle-seed"; |
||||
|
import { usersTable } from "./lib/schema/schema"; |
||||
|
import { dbGlobal } from "./lib/db"; |
||||
|
|
||||
|
async function main() { |
||||
|
await seed(dbGlobal, { usersTable }).refine((f) => ({ |
||||
|
usersTable: { |
||||
|
columns: { |
||||
|
name: f.fullName(), |
||||
|
age: f.int({ minValue: 18, maxValue: 60 }), |
||||
|
email: f.email(), |
||||
|
}, |
||||
|
count: 10, |
||||
|
}, |
||||
|
})); |
||||
|
console.log('Seed complete!'); |
||||
|
process.exit(0); |
||||
|
} |
||||
|
|
||||
|
main().catch(e => { |
||||
|
console.error(e); |
||||
|
process.exit(1); |
||||
|
}); |
||||
|
|
||||
@ -0,0 +1,13 @@ |
|||||
|
{ |
||||
|
"compilerOptions": { |
||||
|
"forceConsistentCasingInFileNames": true, |
||||
|
"strict": true, |
||||
|
"noEmit": true, |
||||
|
"skipLibCheck": true, |
||||
|
"target": "ESNext", |
||||
|
"module": "ESNext", |
||||
|
"moduleResolution": "Bundler", |
||||
|
"resolveJsonModule": true, |
||||
|
"allowSyntheticDefaultImports": true, |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,38 @@ |
|||||
|
import path from "node:path"; |
||||
|
import log4js from "log4js"; |
||||
|
import fs from "node:fs"; |
||||
|
|
||||
|
const logDir = path.resolve(process.cwd(), "logs"); |
||||
|
const pathLog = path.resolve(logDir, "running.log"); |
||||
|
|
||||
|
if (!fs.existsSync(logDir)) { |
||||
|
fs.mkdirSync(logDir, { recursive: true }); |
||||
|
} |
||||
|
|
||||
|
const configureLogger = () => { |
||||
|
const log4jsConfig = function () { |
||||
|
return { |
||||
|
appenders: { |
||||
|
file: { |
||||
|
type: "file", |
||||
|
filename: pathLog, |
||||
|
}, |
||||
|
console: { |
||||
|
type: "console", |
||||
|
}, |
||||
|
}, |
||||
|
categories: { |
||||
|
default: { |
||||
|
appenders: ["file", "console"], |
||||
|
level: "all", |
||||
|
} |
||||
|
}, |
||||
|
}; |
||||
|
}; |
||||
|
|
||||
|
log4js.configure(log4jsConfig()); |
||||
|
|
||||
|
return log4js; |
||||
|
} |
||||
|
|
||||
|
export default configureLogger(); |
||||
@ -0,0 +1,4 @@ |
|||||
|
{ |
||||
|
"name": "logger", |
||||
|
"sideEffects": true |
||||
|
} |
||||
|
After Width: | Height: | Size: 4.2 KiB |
@ -0,0 +1,2 @@ |
|||||
|
User-Agent: * |
||||
|
Disallow: |
||||
@ -0,0 +1,7 @@ |
|||||
|
export default defineEventHandler(() => { |
||||
|
return { |
||||
|
status: 'ok', |
||||
|
timestamp: new Date().toISOString(), |
||||
|
uptime: Math.floor(process.uptime()), |
||||
|
} |
||||
|
}) |
||||
@ -0,0 +1,15 @@ |
|||||
|
import { usersTable } from "drizzle-pkg/lib/schema/schema"; |
||||
|
import { dbGlobal } from "drizzle-pkg/lib/db"; |
||||
|
import log4js from "logger"; |
||||
|
|
||||
|
const logger = log4js.getLogger("APP") |
||||
|
|
||||
|
export default defineEventHandler(async (event) => { |
||||
|
logger.info("hello: world"); |
||||
|
const users = await dbGlobal.select().from(usersTable); |
||||
|
logger.info("users (formatted): %s \n", JSON.stringify(users, null, 2)); |
||||
|
return { |
||||
|
hello: 'world', |
||||
|
users: users, |
||||
|
} |
||||
|
}) |
||||
@ -0,0 +1,11 @@ |
|||||
|
|
||||
|
if (!import.meta.dev) { |
||||
|
// 打包时需要保证migrator被引入
|
||||
|
import('drizzle-orm/mysql2/migrator') |
||||
|
} else { |
||||
|
console.log("plugin: 00.global"); |
||||
|
} |
||||
|
|
||||
|
export default defineNitroPlugin(async () => { |
||||
|
|
||||
|
}) |
||||
@ -0,0 +1,36 @@ |
|||||
|
import log4js from "logger"; |
||||
|
import { randomUUID } from "crypto"; |
||||
|
|
||||
|
const logger = log4js.getLogger("APP") |
||||
|
|
||||
|
if (import.meta.dev) { |
||||
|
console.log("plugin: 1.error-handler"); |
||||
|
} |
||||
|
|
||||
|
declare module "http" { |
||||
|
interface IncomingMessage { |
||||
|
$beginTime: number; |
||||
|
$requestId: string; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export default defineNitroPlugin((nitroApp) => { |
||||
|
nitroApp.hooks.hook('request', async (event) => { |
||||
|
const incoming = event.node.req.headers["x-request-id"]; |
||||
|
const requestId = |
||||
|
typeof incoming === "string" && incoming.trim().length > 0 |
||||
|
? incoming.trim() |
||||
|
: randomUUID(); |
||||
|
|
||||
|
event.node.req.$requestId = requestId; |
||||
|
event.node.req.$beginTime = new Date().getTime(); |
||||
|
event.node.res.setHeader("X-Request-Id", requestId); |
||||
|
logger.info(`[${requestId}]`, `[${event.method}-${event.path}]`, "开始请求"); |
||||
|
}) |
||||
|
nitroApp.hooks.hook('afterResponse', async (event) => { |
||||
|
const requestId = event.node.req.$requestId ?? "(no-request-id)"; |
||||
|
let curTime = new Date().getTime() |
||||
|
let offsetTime = curTime - event.node.req.$beginTime |
||||
|
logger.info(`[${requestId}]`, `[${event.method}-${event.path}]`, "请求结束,花费了", offsetTime, "ms"); |
||||
|
}) |
||||
|
}) |
||||
@ -0,0 +1,14 @@ |
|||||
|
if (import.meta.dev) { |
||||
|
console.log("plugin: 01.well-known-ignore"); |
||||
|
} |
||||
|
|
||||
|
export default defineNitroPlugin(() => { |
||||
|
const originalWarn = console.warn; |
||||
|
console.warn = (...args) => { |
||||
|
const msg = args.join(' '); |
||||
|
if (msg.includes('/.well-known/appspecific/com.chrome.devtools.json')) { |
||||
|
return; |
||||
|
} |
||||
|
originalWarn(...args); |
||||
|
}; |
||||
|
}); |
||||
@ -0,0 +1,43 @@ |
|||||
|
import log4js from "logger"; |
||||
|
|
||||
|
const logger = log4js.getLogger("ERROR"); |
||||
|
|
||||
|
const processHandlersKey = "__personPanelErrorLoggerProcessHandlers"; |
||||
|
|
||||
|
function installProcessErrorLogging() { |
||||
|
const g = globalThis as typeof globalThis & { [processHandlersKey]?: boolean }; |
||||
|
if (g[processHandlersKey]) return; |
||||
|
g[processHandlersKey] = true; |
||||
|
|
||||
|
process.on("unhandledRejection", (reason) => { |
||||
|
if (reason instanceof Error) { |
||||
|
logger.error("[unhandledRejection]", reason.message, reason.stack ?? ""); |
||||
|
} else { |
||||
|
logger.error("[unhandledRejection]", String(reason)); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
process.on("uncaughtException", (err) => { |
||||
|
logger.error("[uncaughtException]", err.message, err.stack ?? ""); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
if (import.meta.dev) { |
||||
|
console.log("plugin: 03.error-logger"); |
||||
|
} |
||||
|
|
||||
|
export default defineNitroPlugin((nitroApp) => { |
||||
|
installProcessErrorLogging(); |
||||
|
|
||||
|
nitroApp.hooks.hook("error", (error, context) => { |
||||
|
const event = context.event; |
||||
|
const tags = Array.isArray(context.tags) ? (context.tags as string[]).join(",") : ""; |
||||
|
logger.error( |
||||
|
event?.method ?? "", |
||||
|
event?.path ?? "(no request)", |
||||
|
tags ? `[${tags}]` : "", |
||||
|
error.message, |
||||
|
error.stack ?? "" |
||||
|
); |
||||
|
}); |
||||
|
}); |
||||
@ -0,0 +1,18 @@ |
|||||
|
{ |
||||
|
// https://nuxt.com/docs/guide/concepts/typescript |
||||
|
"files": [], |
||||
|
"references": [ |
||||
|
{ |
||||
|
"path": "./.nuxt/tsconfig.app.json" |
||||
|
}, |
||||
|
{ |
||||
|
"path": "./.nuxt/tsconfig.server.json" |
||||
|
}, |
||||
|
{ |
||||
|
"path": "./.nuxt/tsconfig.shared.json" |
||||
|
}, |
||||
|
{ |
||||
|
"path": "./.nuxt/tsconfig.node.json" |
||||
|
} |
||||
|
] |
||||
|
} |
||||
Loading…
Reference in new issue