Browse Source

init

tags/pure-sql-nuxt4
npmrun 7 days ago
commit
8d8af17223
  1. 1
      .env.example
  2. 24
      .gitignore
  3. 12
      README.md
  4. 37
      app/app.vue
  5. 53
      build-files/migrate.js
  6. 8
      build-files/run.sh
  7. 1779
      bun.lock
  8. 5
      nuxt.config.ts
  9. 37
      package.json
  10. 11
      packages/drizzle-pkg/drizzle.config.ts
  11. 4
      packages/drizzle-pkg/env.ts
  12. 6
      packages/drizzle-pkg/lib/db.ts
  13. 8
      packages/drizzle-pkg/lib/schema/schema.ts
  14. 8
      packages/drizzle-pkg/migrations/0000_init.sql
  15. 70
      packages/drizzle-pkg/migrations/meta/0000_snapshot.json
  16. 13
      packages/drizzle-pkg/migrations/meta/_journal.json
  17. 9
      packages/drizzle-pkg/package.json
  18. 7
      packages/drizzle-pkg/scripts/mv.sh
  19. 25
      packages/drizzle-pkg/seed.ts
  20. 13
      packages/drizzle-pkg/tsconfig.json
  21. 38
      packages/logger/index.ts
  22. 4
      packages/logger/package.json
  23. BIN
      public/favicon.ico
  24. 2
      public/robots.txt
  25. 7
      server/api/health.get.ts
  26. 15
      server/api/hello.ts
  27. 11
      server/plugins/00.global.ts
  28. 36
      server/plugins/01.req-time.ts
  29. 14
      server/plugins/02.well-known-ignore.ts
  30. 43
      server/plugins/03.error-logger.ts
  31. 0
      server/types/index.d.ts
  32. 18
      tsconfig.json

1
.env.example

@ -0,0 +1 @@
DATABASE_URL=mysql://root:xxxxxxxx@localhost:3306/nuxt-db

24
.gitignore

@ -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

12
README.md

@ -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` 目录,在服务器环境执行迁移命令,省时省力。

37
app/app.vue

@ -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>

53
build-files/migrate.js

@ -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)
})
}

8
build-files/run.sh

@ -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

1779
bun.lock

File diff suppressed because it is too large

5
nuxt.config.ts

@ -0,0 +1,5 @@
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
compatibilityDate: '2025-07-15',
devtools: { enabled: true },
})

37
package.json

@ -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"
}
}

11
packages/drizzle-pkg/drizzle.config.ts

@ -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!
},
});

4
packages/drizzle-pkg/env.ts

@ -0,0 +1,4 @@
import { config } from 'dotenv';
config({ path: '../../.env.local' });

6
packages/drizzle-pkg/lib/db.ts

@ -0,0 +1,6 @@
import { drizzle } from "drizzle-orm/mysql2";
const _db = drizzle({ connection:{ uri: process.env.DATABASE_URL! }});
export { _db as dbGlobal }

8
packages/drizzle-pkg/lib/schema/schema.ts

@ -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(),
});

8
packages/drizzle-pkg/migrations/0000_init.sql

@ -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`)
);

70
packages/drizzle-pkg/migrations/meta/0000_snapshot.json

@ -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": {}
}
}

13
packages/drizzle-pkg/migrations/meta/_journal.json

@ -0,0 +1,13 @@
{
"version": "7",
"dialect": "mysql",
"entries": [
{
"idx": 0,
"version": "5",
"when": 1775843596838,
"tag": "0000_init",
"breakpoints": true
}
]
}

9
packages/drizzle-pkg/package.json

@ -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"
}
}

7
packages/drizzle-pkg/scripts/mv.sh

@ -0,0 +1,7 @@
if [ -d "./migrations" ]; then
cp -r ./migrations ../../.output/
else
echo "migrations directory not found"
exit 1
fi

25
packages/drizzle-pkg/seed.ts

@ -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);
});

13
packages/drizzle-pkg/tsconfig.json

@ -0,0 +1,13 @@
{
"compilerOptions": {
"forceConsistentCasingInFileNames": true,
"strict": true,
"noEmit": true,
"skipLibCheck": true,
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Bundler",
"resolveJsonModule": true,
"allowSyntheticDefaultImports": true,
}
}

38
packages/logger/index.ts

@ -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();

4
packages/logger/package.json

@ -0,0 +1,4 @@
{
"name": "logger",
"sideEffects": true
}

BIN
public/favicon.ico

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

2
public/robots.txt

@ -0,0 +1,2 @@
User-Agent: *
Disallow:

7
server/api/health.get.ts

@ -0,0 +1,7 @@
export default defineEventHandler(() => {
return {
status: 'ok',
timestamp: new Date().toISOString(),
uptime: Math.floor(process.uptime()),
}
})

15
server/api/hello.ts

@ -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,
}
})

11
server/plugins/00.global.ts

@ -0,0 +1,11 @@
if (!import.meta.dev) {
// 打包时需要保证migrator被引入
import('drizzle-orm/mysql2/migrator')
} else {
console.log("plugin: 00.global");
}
export default defineNitroPlugin(async () => {
})

36
server/plugins/01.req-time.ts

@ -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");
})
})

14
server/plugins/02.well-known-ignore.ts

@ -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);
};
});

43
server/plugins/03.error-logger.ts

@ -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
server/types/index.d.ts

18
tsconfig.json

@ -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…
Cancel
Save