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