commit
58aacb079f
49 changed files with 3546 additions and 0 deletions
@ -0,0 +1,11 @@ |
|||||
|
{ |
||||
|
"files.associations": { |
||||
|
"*.css": "tailwindcss" |
||||
|
}, |
||||
|
"editor.quickSuggestions": { |
||||
|
"strings": "on" |
||||
|
}, |
||||
|
"tailwindCSS.classAttributes": ["class", "ui"], |
||||
|
"tailwindCSS.classFunctions": ["defineAppConfig"] |
||||
|
} |
||||
|
|
||||
@ -0,0 +1 @@ |
|||||
|
DATABASE_URL=postgresql://postgres:xxxxxx@localhost:6666/postgres |
||||
@ -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,17 @@ |
|||||
|
|
||||
|
## 文档 |
||||
|
|
||||
|
- [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。数据库为 **postgres**(通过 `DATABASE_URL` 连接;本地可参考 `.env.example` 复制为 `.env.local`)。部署时可直接打包 `.output` 目录,在服务器环境执行迁移命令,省时省力。 |
||||
|
|
||||
|
## 计划 |
||||
|
|
||||
|
- [ ] 支持定时任务 |
||||
|
- [ ] |
||||
@ -0,0 +1,6 @@ |
|||||
|
<template> |
||||
|
<NuxtLayout> |
||||
|
<NuxtRouteAnnouncer atomic /> |
||||
|
<NuxtPage /> |
||||
|
</NuxtLayout> |
||||
|
</template> |
||||
@ -0,0 +1,2 @@ |
|||||
|
@import "tailwindcss"; |
||||
|
@import "@nuxt/ui"; |
||||
@ -0,0 +1,76 @@ |
|||||
|
<template> |
||||
|
<UApp> |
||||
|
<div class="min-h-screen bg-default text-default flex flex-col"> |
||||
|
<header class="border-b border-default"> |
||||
|
<UContainer class="h-14 flex items-center justify-between"> |
||||
|
<div class="flex items-center gap-2"> |
||||
|
<NuxtLink to="/" class="font-semibold tracking-tight"> |
||||
|
Nuxt4 Demo |
||||
|
</NuxtLink> |
||||
|
<UDropdownMenu :items="menuItems" :content="{ align: 'end' }"> |
||||
|
<UButton color="neutral" variant="ghost" label="菜单" icon="i-lucide-menu" /> |
||||
|
</UDropdownMenu> |
||||
|
</div> |
||||
|
<div> |
||||
|
<UButton color="neutral" variant="ghost" label="登录 / 注册" /> |
||||
|
</div> |
||||
|
</UContainer> |
||||
|
</header> |
||||
|
|
||||
|
<main class="flex-1"> |
||||
|
<UContainer class="py-8"> |
||||
|
<NuxtPage /> |
||||
|
</UContainer> |
||||
|
</main> |
||||
|
|
||||
|
<footer class="border-t border-default"> |
||||
|
<UContainer class="h-12 flex items-center text-sm text-muted"> |
||||
|
Built with Nuxt + Nuxt UI |
||||
|
</UContainer> |
||||
|
</footer> |
||||
|
</div> |
||||
|
</UApp> |
||||
|
</template> |
||||
|
|
||||
|
<script setup lang="ts"> |
||||
|
const menuItems = [ |
||||
|
[ |
||||
|
{ |
||||
|
label: "首页", |
||||
|
icon: "i-lucide-house", |
||||
|
to: "/", |
||||
|
}, |
||||
|
{ |
||||
|
label: "文档", |
||||
|
icon: "i-lucide-book-open", |
||||
|
children: [ |
||||
|
{ |
||||
|
label: "Nuxt", |
||||
|
to: "https://nuxt.com/docs", |
||||
|
target: "_blank", |
||||
|
}, |
||||
|
{ |
||||
|
label: "Nuxt UI", |
||||
|
to: "https://ui.nuxt.com/getting-started", |
||||
|
target: "_blank", |
||||
|
}, |
||||
|
], |
||||
|
}, |
||||
|
{ |
||||
|
label: "示例", |
||||
|
icon: "i-lucide-layout-grid", |
||||
|
children: [ |
||||
|
{ |
||||
|
label: "Hello API", |
||||
|
to: "/api/hello", |
||||
|
target: "_blank", |
||||
|
}, |
||||
|
{ |
||||
|
label: "首页页面", |
||||
|
to: "/index", |
||||
|
}, |
||||
|
], |
||||
|
}, |
||||
|
], |
||||
|
] |
||||
|
</script> |
||||
@ -0,0 +1,36 @@ |
|||||
|
<script setup lang="ts"> |
||||
|
const { data, pending, error, refresh } = await useHttpFetch('/api/hello') |
||||
|
|
||||
|
const userCount = computed(() => data.value?.users?.length ?? 0) |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
|
||||
|
<h1>Person Panel</h1> |
||||
|
<UAlert title="Heads up!" /> |
||||
|
|
||||
|
<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> |
||||
|
</template> |
||||
@ -0,0 +1,64 @@ |
|||||
|
import type { AsyncData, UseFetchOptions } from '#app' |
||||
|
import type { KeysOf, PickFrom } from '#app/composables/asyncData' |
||||
|
import type { NitroFetchRequest, TypedInternalResponse, AvailableRouterMethod } from 'nitropack/types' |
||||
|
import type { FetchError } from 'ofetch' |
||||
|
import type { Ref } from 'vue' |
||||
|
import { _useHttpFetch, _useLazyHttpFetch, type UnwrapApiResponse } from './http/factory' |
||||
|
|
||||
|
type DefaultMethod<ReqT extends NitroFetchRequest> = 'get' extends AvailableRouterMethod<ReqT> |
||||
|
? 'get' |
||||
|
: AvailableRouterMethod<ReqT> |
||||
|
|
||||
|
type HttpFetchOptions< |
||||
|
_ResT, |
||||
|
DataT, |
||||
|
PickKeys extends KeysOf<DataT>, |
||||
|
DefaultT, |
||||
|
ReqT extends NitroFetchRequest, |
||||
|
Method extends AvailableRouterMethod<ReqT>, |
||||
|
> = Omit<UseFetchOptions<_ResT, DataT, PickKeys, DefaultT, ReqT, Method>, 'transform'> |
||||
|
|
||||
|
/** |
||||
|
* 带项目默认选项的 `useFetch`,并在类型上将 `data` 视为接口 `{ code, data, message }` 中的内层 `data`。 |
||||
|
* |
||||
|
* 说明:Nuxt的 `createUseFetch` 在类型上不会把工厂里的 `transform` 的出参当作 `AsyncData['data']`, |
||||
|
* 因此这里用薄包装修正 `DataT`(见 `UnwrapApiResponse`)。 |
||||
|
*/ |
||||
|
export function useHttpFetch< |
||||
|
ResT = void, |
||||
|
ErrorT = FetchError, |
||||
|
ReqT extends NitroFetchRequest = NitroFetchRequest, |
||||
|
Method extends AvailableRouterMethod<ReqT> = ResT extends void |
||||
|
? DefaultMethod<ReqT> |
||||
|
: AvailableRouterMethod<ReqT>, |
||||
|
_ResT = ResT extends void ? TypedInternalResponse<ReqT, unknown, Lowercase<Method>> : ResT, |
||||
|
DataT = UnwrapApiResponse<_ResT>, |
||||
|
PickKeys extends KeysOf<DataT> = KeysOf<DataT>, |
||||
|
DefaultT = undefined, |
||||
|
>( |
||||
|
url: ReqT | Ref<ReqT, ReqT> | (() => ReqT), |
||||
|
opts?: HttpFetchOptions<_ResT, DataT, PickKeys, DefaultT, ReqT, Method>, |
||||
|
): AsyncData<DefaultT | PickFrom<DataT, PickKeys>, ErrorT | undefined> { |
||||
|
return _useHttpFetch(url, opts) as AsyncData<DefaultT | PickFrom<DataT, PickKeys>, ErrorT | undefined> |
||||
|
} |
||||
|
|
||||
|
export function useLazyHttpFetch< |
||||
|
ResT = void, |
||||
|
ErrorT = FetchError, |
||||
|
ReqT extends NitroFetchRequest = NitroFetchRequest, |
||||
|
Method extends AvailableRouterMethod<ReqT> = ResT extends void |
||||
|
? DefaultMethod<ReqT> |
||||
|
: AvailableRouterMethod<ReqT>, |
||||
|
_ResT = ResT extends void ? TypedInternalResponse<ReqT, unknown, Lowercase<Method>> : ResT, |
||||
|
DataT = UnwrapApiResponse<_ResT>, |
||||
|
PickKeys extends KeysOf<DataT> = KeysOf<DataT>, |
||||
|
DefaultT = undefined, |
||||
|
>( |
||||
|
url: ReqT | Ref<ReqT, ReqT> | (() => ReqT), |
||||
|
opts?: HttpFetchOptions<_ResT, DataT, PickKeys, DefaultT, ReqT, Method>, |
||||
|
): AsyncData<DefaultT | PickFrom<DataT, PickKeys>, ErrorT | undefined> { |
||||
|
return _useLazyHttpFetch(url, opts) as AsyncData< |
||||
|
DefaultT | PickFrom<DataT, PickKeys>, |
||||
|
ErrorT | undefined |
||||
|
> |
||||
|
} |
||||
@ -0,0 +1,34 @@ |
|||||
|
import { createUseFetch } from '#imports' |
||||
|
|
||||
|
/** 与 `R.success` / `R.error` 返回结构对齐 */ |
||||
|
export type ApiResponse<T = unknown> = { |
||||
|
code: number |
||||
|
message: string |
||||
|
data: T |
||||
|
} |
||||
|
|
||||
|
|
||||
|
/** 从 Nitro 推断的响应体上剥离一层 `ApiResponse`,得到 `data` 字段类型 */ |
||||
|
export type UnwrapApiResponse<T> = T extends ApiResponse<infer D> ? D : T |
||||
|
|
||||
|
export function unwrapApiBody<T>(payload: ApiResponse<T>): T { |
||||
|
if (payload.code !== 0) { |
||||
|
throw new Error(payload.message) |
||||
|
} |
||||
|
return payload.data |
||||
|
} |
||||
|
|
||||
|
|
||||
|
export const request = $fetch.create({}) |
||||
|
|
||||
|
const httpFetchDefaults = { |
||||
|
retry: 0, |
||||
|
$fetch: request, |
||||
|
transform: unwrapApiBody, |
||||
|
} |
||||
|
|
||||
|
export const _useHttpFetch = createUseFetch(httpFetchDefaults) |
||||
|
export const _useLazyHttpFetch = createUseFetch({ |
||||
|
...httpFetchDefaults, |
||||
|
lazy: true, |
||||
|
}) |
||||
@ -0,0 +1,50 @@ |
|||||
|
import { drizzle } from 'drizzle-orm/node-postgres' |
||||
|
import { migrate } from 'drizzle-orm/node-postgres/migrator' |
||||
|
import { Pool } from 'pg' |
||||
|
import path from 'node:path' |
||||
|
import { fileURLToPath } from 'node:url' |
||||
|
|
||||
|
const argv = process.argv.slice(2) |
||||
|
const migrationsFolderRelative = argv[0] |
||||
|
|
||||
|
if (!migrationsFolderRelative) { |
||||
|
throw new Error('migrations 文件夹未设置') |
||||
|
} |
||||
|
|
||||
|
|
||||
|
export async function runMigrations() { |
||||
|
const databaseUrl = process.env.DATABASE_URL |
||||
|
if (!databaseUrl) { |
||||
|
throw new Error('DATABASE_URL 未设置') |
||||
|
} |
||||
|
|
||||
|
const pool = new Pool({ connectionString: databaseUrl }) |
||||
|
const db = drizzle(pool) |
||||
|
|
||||
|
try { |
||||
|
console.log('🚀 开始执行 PostgreSQL 迁移...') |
||||
|
const migrationsFolder = path.resolve(process.cwd(), migrationsFolderRelative) |
||||
|
|
||||
|
await migrate(db, { |
||||
|
migrationsFolder, |
||||
|
}) |
||||
|
|
||||
|
console.log('✅ PostgreSQL 迁移完成!') |
||||
|
} catch (err) { |
||||
|
console.log('❌ 迁移失败:', err) |
||||
|
throw err |
||||
|
} finally { |
||||
|
await pool.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,51 @@ |
|||||
|
import { drizzle } from 'drizzle-orm/better-sqlite3' |
||||
|
import { migrate } from 'drizzle-orm/better-sqlite3/migrator' |
||||
|
import Database from 'better-sqlite3' |
||||
|
import path from 'node:path' |
||||
|
import { fileURLToPath } from 'node:url' |
||||
|
|
||||
|
const argv = process.argv.slice(2) |
||||
|
const migrationsFolderRelative = argv[0] |
||||
|
|
||||
|
if (!migrationsFolderRelative) { |
||||
|
throw new Error('migrations 文件夹未设置') |
||||
|
} |
||||
|
|
||||
|
export async function runMigrations() { |
||||
|
const dbUrl = process.env.DATABASE_URL || '' |
||||
|
const sqlitePath = dbUrl.startsWith('file:') ? dbUrl.slice(5) : dbUrl |
||||
|
|
||||
|
if (!sqlitePath) { |
||||
|
throw new Error('DATABASE_URL 未设置,且未提供有效的 SQLite 文件路径') |
||||
|
} |
||||
|
|
||||
|
const sqlite = new Database(sqlitePath) |
||||
|
const db = drizzle(sqlite) |
||||
|
|
||||
|
try { |
||||
|
console.log(`🚀 开始执行 SQLite 迁移... (${sqlitePath})`) |
||||
|
const migrationsFolder = path.resolve(process.cwd(), migrationsFolderRelative) |
||||
|
|
||||
|
await migrate(db, { |
||||
|
migrationsFolder, |
||||
|
}) |
||||
|
|
||||
|
console.log('✅ SQLite 迁移完成!') |
||||
|
} catch (err) { |
||||
|
console.log('❌ 迁移失败:', err) |
||||
|
throw err |
||||
|
} finally { |
||||
|
sqlite.close() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
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-pg.js migrations |
||||
|
node server/index.mjs |
||||
File diff suppressed because it is too large
@ -0,0 +1,25 @@ |
|||||
|
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||
|
export default defineNuxtConfig({ |
||||
|
compatibilityDate: '2025-07-15', |
||||
|
modules: ['@nuxt/ui'], |
||||
|
css: ['~/assets/css/main.css'], |
||||
|
ui: { |
||||
|
fonts: false |
||||
|
}, |
||||
|
devtools: { enabled: true }, |
||||
|
nitro: { |
||||
|
typescript: { |
||||
|
tsConfig: { |
||||
|
compilerOptions: { |
||||
|
resolvePackageJsonExports: true, |
||||
|
resolvePackageJsonImports: true, |
||||
|
baseUrl: './', |
||||
|
paths: { |
||||
|
'drizzle-pkg': ['./packages/drizzle-pkg/lib'], |
||||
|
'logger': ['./packages/logger/lib'] |
||||
|
}, |
||||
|
} |
||||
|
}, |
||||
|
} |
||||
|
}, |
||||
|
}) |
||||
@ -0,0 +1,49 @@ |
|||||
|
{ |
||||
|
"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", |
||||
|
"cp:db": "cp build-files/run.sh .output/run.sh && cp .env.example .output/.env && cp -r build-files/migrate/* .output/server/", |
||||
|
"migrate:test": "sh scripts/migrate-test.sh", |
||||
|
"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": { |
||||
|
"@nuxt/ui": "^4.6.1", |
||||
|
"better-sqlite3": "^12.9.0", |
||||
|
"dotenv": "17.4.1", |
||||
|
"drizzle-orm": "0.45.2", |
||||
|
"drizzle-pkg": "workspace:*", |
||||
|
"drizzle-seed": "0.3.1", |
||||
|
"drizzle-zod": "0.8.3", |
||||
|
"log4js": "6.9.1", |
||||
|
"logger": "workspace:*", |
||||
|
"mime": "4.1.0", |
||||
|
"multer": "2.1.1", |
||||
|
"nuxt": "4.4.2", |
||||
|
"pg": "8.20.0", |
||||
|
"tailwindcss": "^4.2.2", |
||||
|
"ufo": "1.6.3", |
||||
|
"vue": "3.5.32", |
||||
|
"vue-router": "5.0.4", |
||||
|
"zod": "4.3.6" |
||||
|
}, |
||||
|
"devDependencies": { |
||||
|
"@types/better-sqlite3": "^7.6.13", |
||||
|
"@types/multer": "2.1.0", |
||||
|
"@types/pg": "8.20.0", |
||||
|
"drizzle-kit": "0.31.10", |
||||
|
"tsx": "4.21.0", |
||||
|
"typescript": "6.0.2" |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,11 @@ |
|||||
|
|
||||
|
import { drizzle } from "drizzle-orm/node-postgres"; |
||||
|
|
||||
|
if (process.env.NODE_ENV === 'production') { |
||||
|
// 打包时需要保证migrator被引入
|
||||
|
import('drizzle-orm/node-postgres/migrator') |
||||
|
} |
||||
|
|
||||
|
const _db = drizzle(process.env.DATABASE_URL!); |
||||
|
|
||||
|
export { _db as dbGlobal } |
||||
@ -0,0 +1,16 @@ |
|||||
|
import { sql } from "drizzle-orm"; |
||||
|
import { integer, pgTable, timestamp, varchar } from "drizzle-orm/pg-core"; |
||||
|
|
||||
|
export const users = pgTable("users", { |
||||
|
id: integer().primaryKey(), |
||||
|
username: varchar().notNull().unique(), |
||||
|
email: varchar(), |
||||
|
nickname: varchar(), |
||||
|
password: varchar().notNull(), |
||||
|
avatar: varchar(), |
||||
|
createdAt: timestamp('created_at').defaultNow().notNull(), |
||||
|
updatedAt: timestamp('updated_at') |
||||
|
.defaultNow() |
||||
|
.$onUpdate(() => sql`CURRENT_TIMESTAMP`) |
||||
|
.notNull(), |
||||
|
}); |
||||
@ -0,0 +1,10 @@ |
|||||
|
import { drizzle } from 'drizzle-orm/libsql'; |
||||
|
|
||||
|
if (process.env.NODE_ENV === 'production') { |
||||
|
// 打包时需要保证migrator被引入
|
||||
|
import('drizzle-orm/better-sqlite3/migrator') |
||||
|
} |
||||
|
|
||||
|
const _db = drizzle(process.env.DATABASE_URL!); |
||||
|
|
||||
|
export { _db as dbGlobal } |
||||
@ -0,0 +1,11 @@ |
|||||
|
import './env'; |
||||
|
import { defineConfig } from 'drizzle-kit'; |
||||
|
|
||||
|
export default defineConfig({ |
||||
|
out: './migrations', |
||||
|
schema: './database/pg/schema/*', |
||||
|
dialect: 'postgresql', |
||||
|
dbCredentials: { |
||||
|
url: process.env.DATABASE_URL! |
||||
|
}, |
||||
|
}); |
||||
@ -0,0 +1,4 @@ |
|||||
|
|
||||
|
import { config } from 'dotenv'; |
||||
|
|
||||
|
config({ path: '../../.env' }); |
||||
@ -0,0 +1 @@ |
|||||
|
export { dbGlobal } from '../database/pg/db' |
||||
@ -0,0 +1 @@ |
|||||
|
export { users } from '../../database/pg/schema/auth' |
||||
@ -0,0 +1,11 @@ |
|||||
|
CREATE TABLE "users" ( |
||||
|
"id" integer PRIMARY KEY NOT NULL, |
||||
|
"username" varchar NOT NULL, |
||||
|
"email" varchar, |
||||
|
"nickname" varchar, |
||||
|
"password" varchar NOT NULL, |
||||
|
"avatar" varchar, |
||||
|
"created_at" timestamp DEFAULT now() NOT NULL, |
||||
|
"updated_at" timestamp DEFAULT now() NOT NULL, |
||||
|
CONSTRAINT "users_username_unique" UNIQUE("username") |
||||
|
); |
||||
@ -0,0 +1,90 @@ |
|||||
|
{ |
||||
|
"id": "25840823-aa2a-4e32-a6b6-70bb1e27348e", |
||||
|
"prevId": "00000000-0000-0000-0000-000000000000", |
||||
|
"version": "7", |
||||
|
"dialect": "postgresql", |
||||
|
"tables": { |
||||
|
"public.users": { |
||||
|
"name": "users", |
||||
|
"schema": "", |
||||
|
"columns": { |
||||
|
"id": { |
||||
|
"name": "id", |
||||
|
"type": "integer", |
||||
|
"primaryKey": true, |
||||
|
"notNull": true |
||||
|
}, |
||||
|
"username": { |
||||
|
"name": "username", |
||||
|
"type": "varchar", |
||||
|
"primaryKey": false, |
||||
|
"notNull": true |
||||
|
}, |
||||
|
"email": { |
||||
|
"name": "email", |
||||
|
"type": "varchar", |
||||
|
"primaryKey": false, |
||||
|
"notNull": false |
||||
|
}, |
||||
|
"nickname": { |
||||
|
"name": "nickname", |
||||
|
"type": "varchar", |
||||
|
"primaryKey": false, |
||||
|
"notNull": false |
||||
|
}, |
||||
|
"password": { |
||||
|
"name": "password", |
||||
|
"type": "varchar", |
||||
|
"primaryKey": false, |
||||
|
"notNull": true |
||||
|
}, |
||||
|
"avatar": { |
||||
|
"name": "avatar", |
||||
|
"type": "varchar", |
||||
|
"primaryKey": false, |
||||
|
"notNull": false |
||||
|
}, |
||||
|
"created_at": { |
||||
|
"name": "created_at", |
||||
|
"type": "timestamp", |
||||
|
"primaryKey": false, |
||||
|
"notNull": true, |
||||
|
"default": "now()" |
||||
|
}, |
||||
|
"updated_at": { |
||||
|
"name": "updated_at", |
||||
|
"type": "timestamp", |
||||
|
"primaryKey": false, |
||||
|
"notNull": true, |
||||
|
"default": "now()" |
||||
|
} |
||||
|
}, |
||||
|
"indexes": {}, |
||||
|
"foreignKeys": {}, |
||||
|
"compositePrimaryKeys": {}, |
||||
|
"uniqueConstraints": { |
||||
|
"users_username_unique": { |
||||
|
"name": "users_username_unique", |
||||
|
"nullsNotDistinct": false, |
||||
|
"columns": [ |
||||
|
"username" |
||||
|
] |
||||
|
} |
||||
|
}, |
||||
|
"policies": {}, |
||||
|
"checkConstraints": {}, |
||||
|
"isRLSEnabled": false |
||||
|
} |
||||
|
}, |
||||
|
"enums": {}, |
||||
|
"schemas": {}, |
||||
|
"sequences": {}, |
||||
|
"roles": {}, |
||||
|
"policies": {}, |
||||
|
"views": {}, |
||||
|
"_meta": { |
||||
|
"columns": {}, |
||||
|
"schemas": {}, |
||||
|
"tables": {} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,13 @@ |
|||||
|
{ |
||||
|
"version": "7", |
||||
|
"dialect": "postgresql", |
||||
|
"entries": [ |
||||
|
{ |
||||
|
"idx": 0, |
||||
|
"version": "7", |
||||
|
"when": 1776329125490, |
||||
|
"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/auth"; |
||||
|
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 @@ |
|||||
|
if [ -f .env ]; then |
||||
|
export $(grep -v '^#' .env | xargs) |
||||
|
fi |
||||
|
|
||||
|
echo "DATABASE_URL: $DATABASE_URL" |
||||
|
|
||||
|
node build-files/migrate/migrate-pg.js packages/drizzle-pkg/migrations |
||||
@ -0,0 +1,12 @@ |
|||||
|
import type { H3Event } from "h3"; |
||||
|
|
||||
|
const handler = eventHandler(async (event: H3Event) => { |
||||
|
event.node.res.statusCode = 404 |
||||
|
return { |
||||
|
code: 0, |
||||
|
message: "该接口不存在" |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
export type HealthCheckData = Awaited<ReturnType<typeof handler>>; |
||||
|
export default handler; |
||||
@ -0,0 +1,86 @@ |
|||||
|
import multer from 'multer'; |
||||
|
import fs from 'node:fs'; |
||||
|
import path from 'node:path'; |
||||
|
import { callNodeListener } from 'h3'; |
||||
|
|
||||
|
// 类型定义
|
||||
|
interface IFile { |
||||
|
name: string; |
||||
|
url: string; // 前端可直接访问的 URL
|
||||
|
path: string; // 服务器存储路径
|
||||
|
mimeType: string; |
||||
|
size: number; |
||||
|
} |
||||
|
|
||||
|
export default defineWrappedResponseHandler(async (event) => { |
||||
|
try { |
||||
|
// 存储目录
|
||||
|
const uploadDir = path.join(process.cwd(), 'public/assets'); |
||||
|
|
||||
|
// 自动创建目录
|
||||
|
if (!fs.existsSync(uploadDir)) { |
||||
|
fs.mkdirSync(uploadDir, { recursive: true }); |
||||
|
} |
||||
|
|
||||
|
// 配置存储
|
||||
|
const storage = multer.diskStorage({ |
||||
|
destination: uploadDir, |
||||
|
filename: (req, file, cb) => { |
||||
|
// 生成唯一文件名:时间戳 + 原始文件名(安全处理)
|
||||
|
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9); |
||||
|
// 获取文件后缀
|
||||
|
const ext = path.extname(file.originalname).toLowerCase(); |
||||
|
// 干净的文件名(只保留字母数字)
|
||||
|
const baseName = path.basename(file.originalname, ext).replace(/[^a-z0-9]/gi, '-'); |
||||
|
// 最终文件名(带后缀)
|
||||
|
const filename = `${uniqueSuffix}-${baseName}${ext}`; |
||||
|
cb(null, filename); |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
// 上传配置
|
||||
|
const upload = multer({ |
||||
|
storage, |
||||
|
limits: { |
||||
|
fileSize: 5 * 1024 * 1024, // 5MB 限制
|
||||
|
}, |
||||
|
fileFilter: (req, file, cb) => { |
||||
|
const allowedTypes = ['image/png', 'image/jpeg', 'image/jpg', 'image/webp']; |
||||
|
if (allowedTypes.includes(file.mimetype)) { |
||||
|
cb(null, true); |
||||
|
} else { |
||||
|
cb(new Error('只支持 PNG/JPG/WebP 格式图片')); |
||||
|
} |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
// 执行上传(最多 10 个文件)
|
||||
|
await callNodeListener( |
||||
|
// @ts-expect-error Nuxt 类型兼容
|
||||
|
upload.array('file', 10), |
||||
|
event.node.req, |
||||
|
event.node.res |
||||
|
); |
||||
|
|
||||
|
// 获取上传后的文件
|
||||
|
// @ts-expect-error
|
||||
|
const uploadedFiles = event.node.req.files || []; |
||||
|
|
||||
|
// 格式化返回数据
|
||||
|
const result: IFile[] = uploadedFiles.map((file: any) => ({ |
||||
|
name: file.originalname, |
||||
|
url: `/public/assets/${file.filename}`, // ✅ 前端可直接访问
|
||||
|
mimeType: file.mimetype, |
||||
|
size: file.size, |
||||
|
})); |
||||
|
|
||||
|
return result; |
||||
|
|
||||
|
} catch (err: any) { |
||||
|
console.error('上传失败:', err); |
||||
|
return createError({ |
||||
|
statusCode: 400, |
||||
|
statusMessage: err.message || '上传失败', |
||||
|
}); |
||||
|
} |
||||
|
}); |
||||
@ -0,0 +1,14 @@ |
|||||
|
|
||||
|
export default defineWrappedResponseHandler(async (event) => { |
||||
|
return R.success({ |
||||
|
hello: "aa", |
||||
|
users: [ |
||||
|
{ |
||||
|
id: 1, |
||||
|
name: "aaa", |
||||
|
email: "aaa", |
||||
|
age: 23, |
||||
|
} |
||||
|
] |
||||
|
}) |
||||
|
}) |
||||
@ -0,0 +1,56 @@ |
|||||
|
import type { H3Event } from "h3"; |
||||
|
import fs from "fs/promises"; |
||||
|
import { resolve } from "node:path"; |
||||
|
|
||||
|
const handler = eventHandler(async (event: H3Event) => { |
||||
|
const query = getQuery(event); |
||||
|
|
||||
|
if (Reflect.has(query, "auto")) { |
||||
|
try { |
||||
|
return await $fetch("https://api.miaomc.cn/image/get", { method: "get", mode: "cors" }) |
||||
|
} catch (error) {} |
||||
|
try { |
||||
|
return await sendRedirect( |
||||
|
event, |
||||
|
encodeURI("https://api.r10086.com/樱道随机图片api接口.php?图片系列=动漫综合1"), |
||||
|
302 |
||||
|
); |
||||
|
} catch (error) {} |
||||
|
return "error" |
||||
|
} |
||||
|
if (Reflect.has(query, "miaomc")) { |
||||
|
// return await $fetch("https://api.miaomc.cn/image/get", { method: "get", mode: "cors" })
|
||||
|
event.node.res.statusCode = 302; |
||||
|
event.node.res.setHeader("location", "https://api.miaomc.cn/image/get"); |
||||
|
return; |
||||
|
} |
||||
|
if (Reflect.has(query, "r10086")) { |
||||
|
// return `<!DOCTYPE html><html lang="zh"><head><meta charset="utf-8"><title>选择</title></head>
|
||||
|
// <body><script>location.href="https://api.r10086.com/樱道随机图片api接口.php?图片系列=动漫综合1"</script></body>
|
||||
|
// </html>`;
|
||||
|
return await sendRedirect( |
||||
|
event, |
||||
|
encodeURI("https://api.r10086.com/樱道随机图片api接口.php?图片系列=动漫综合1"), |
||||
|
302 |
||||
|
); |
||||
|
} |
||||
|
if (Reflect.has(query, "favicon")) { |
||||
|
const avatarPath = resolve("public", "favicon.ico") |
||||
|
event.node.res.setHeader("Content-Type", "image/jpeg"); |
||||
|
return fs.readFile(avatarPath) //fs.createReadStream(avatarPath);
|
||||
|
} |
||||
|
return `<!DOCTYPE html><html lang="zh"><head><meta charset="utf-8"><title>选择</title></head>
|
||||
|
<body> |
||||
|
<h1>选择图片路径</h1> |
||||
|
<ol> |
||||
|
<li><a href="/api/pic/random?auto">auto(同时支持以下两种方式)</a></li> |
||||
|
<li><a href="/api/pic/random?miaomc">api.miaomc.cn</a></li> |
||||
|
<li><a href="/api/pic/random?r10086">r10086</a></li> |
||||
|
<li><a href="/api/pic/random?favicon">favicon</a></li> |
||||
|
</ol> |
||||
|
</body> |
||||
|
</html>`;
|
||||
|
}); |
||||
|
|
||||
|
export type ReturnData = Awaited<ReturnType<typeof handler>>; |
||||
|
export default handler; |
||||
@ -0,0 +1,88 @@ |
|||||
|
import { resolve, join, sep, extname } from "node:path"; |
||||
|
import { promises as fsp } from "node:fs"; |
||||
|
import { |
||||
|
decodePath, |
||||
|
withLeadingSlash, |
||||
|
withoutTrailingSlash, |
||||
|
parseURL, |
||||
|
} from "ufo"; |
||||
|
import type { H3Event } from "h3"; |
||||
|
import mime from "mime"; |
||||
|
|
||||
|
const METHODS = new Set(["HEAD", "GET"]); |
||||
|
const SAFE_BASE_DIR = resolve("public"); |
||||
|
|
||||
|
// 缓存配置
|
||||
|
const CACHE_CONTROL = "public, max-age=31536000, immutable"; |
||||
|
const NOT_MODIFIED = 304; |
||||
|
const FORBIDDEN = 403; |
||||
|
const NOT_FOUND = 404; |
||||
|
const SERVER_ERROR = 500; |
||||
|
|
||||
|
export default eventHandler(async (event: H3Event) => { |
||||
|
if (!event.path.startsWith("/public")) return; |
||||
|
|
||||
|
const { req, res } = event.node; |
||||
|
const method = req.method; |
||||
|
|
||||
|
if (method && !METHODS.has(method)) return; |
||||
|
|
||||
|
try { |
||||
|
// 安全解析路径
|
||||
|
const url = event.path.replace(/^\/public/, ""); |
||||
|
const pathname = decodePath( |
||||
|
withLeadingSlash(withoutTrailingSlash(parseURL(url).pathname)) |
||||
|
); |
||||
|
|
||||
|
const targetPath = join(SAFE_BASE_DIR, pathname); |
||||
|
const resolvedPath = resolve(targetPath); |
||||
|
|
||||
|
// 安全校验
|
||||
|
if (!resolvedPath.startsWith(SAFE_BASE_DIR + sep)) { |
||||
|
res.statusCode = FORBIDDEN; |
||||
|
return "Forbidden"; |
||||
|
} |
||||
|
|
||||
|
const stat = await fsp.stat(resolvedPath); |
||||
|
if (!stat.isFile()) { |
||||
|
res.statusCode = NOT_FOUND; |
||||
|
return "Not Found"; |
||||
|
} |
||||
|
|
||||
|
// ====================== 缓存逻辑 ======================
|
||||
|
const mtime = stat.mtime.toUTCString(); |
||||
|
const etag = `"${stat.mtime.getTime().toString(16)}-${stat.size.toString(16)}"`; |
||||
|
|
||||
|
const contentType = mime.getType(resolvedPath) || "application/octet-stream"; |
||||
|
|
||||
|
// 设置缓存头
|
||||
|
res.setHeader("Cache-Control", CACHE_CONTROL); |
||||
|
res.setHeader("ETag", etag); |
||||
|
res.setHeader("Last-Modified", mtime); |
||||
|
res.setHeader("Content-Type", contentType); |
||||
|
res.setHeader("Content-Length", stat.size); |
||||
|
|
||||
|
// 禁用 keep-alive
|
||||
|
res.setHeader("Connection", "close"); |
||||
|
|
||||
|
// 304 协商缓存
|
||||
|
const ifNoneMatch = req.headers["if-none-match"]; |
||||
|
const ifModifiedSince = req.headers["if-modified-since"]; |
||||
|
if (ifNoneMatch === etag || (ifModifiedSince && ifModifiedSince === mtime)) { |
||||
|
res.statusCode = NOT_MODIFIED; |
||||
|
return ""; |
||||
|
} |
||||
|
// ======================================================
|
||||
|
|
||||
|
if (method === "HEAD") return ""; |
||||
|
return fsp.readFile(resolvedPath); |
||||
|
|
||||
|
} catch (err: any) { |
||||
|
if (err.code === "ENOENT") { |
||||
|
res.statusCode = NOT_FOUND; |
||||
|
return "Not Found"; |
||||
|
} |
||||
|
res.statusCode = SERVER_ERROR; |
||||
|
return "Server Error"; |
||||
|
} |
||||
|
}); |
||||
@ -0,0 +1,8 @@ |
|||||
|
|
||||
|
if (import.meta.dev) { |
||||
|
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}]` : "", |
||||
|
"\n", |
||||
|
error |
||||
|
); |
||||
|
}); |
||||
|
}); |
||||
@ -0,0 +1,12 @@ |
|||||
|
import { dbGlobal } from "drizzle-pkg/lib/db"; |
||||
|
import { usersTable } from "drizzle-pkg/lib/schema/schema"; |
||||
|
import { eq } from "drizzle-orm"; |
||||
|
import log4js from "logger"; |
||||
|
|
||||
|
const logger = log4js.getLogger("AUTH") |
||||
|
|
||||
|
export async function getUsers(id: number) { |
||||
|
const users = await dbGlobal.select().from(usersTable) |
||||
|
logger.info("users (formatted): %s \n", JSON.stringify(users, null, 2)); |
||||
|
return users; |
||||
|
} |
||||
@ -0,0 +1,38 @@ |
|||||
|
import log4js from "logger"; |
||||
|
|
||||
|
interface IConfig { |
||||
|
|
||||
|
} |
||||
|
|
||||
|
const defaultConfig: IConfig = { |
||||
|
|
||||
|
} |
||||
|
|
||||
|
const logger = log4js.getLogger("ERROR"); |
||||
|
|
||||
|
export const defineWrappedResponseHandler = <T extends EventHandlerRequest, D>( |
||||
|
handlerOrConfig?: EventHandler<T, D> | IConfig, |
||||
|
_handler?: EventHandler<T, D>, |
||||
|
): EventHandler<T, D> => { |
||||
|
const handler = typeof handlerOrConfig === 'function' ? handlerOrConfig : _handler; |
||||
|
if (!handler) { |
||||
|
throw new Error('handler or config is required'); |
||||
|
} |
||||
|
const config = Object.assign({ ...defaultConfig }, typeof handlerOrConfig === 'object' ? handlerOrConfig : {}); |
||||
|
|
||||
|
return defineEventHandler<T>(async (event) => { |
||||
|
try { |
||||
|
const response = await handler(event) |
||||
|
return response |
||||
|
} catch (error) { |
||||
|
logger.error( |
||||
|
event?.method ?? "", |
||||
|
event?.path ?? "(no request)", |
||||
|
`[request]`, |
||||
|
"\n", |
||||
|
error |
||||
|
); |
||||
|
return error |
||||
|
} |
||||
|
}) |
||||
|
} |
||||
@ -0,0 +1,24 @@ |
|||||
|
|
||||
|
export const R = { |
||||
|
success: <T>(data: T) => { |
||||
|
return { |
||||
|
code: 0, |
||||
|
message: 'success', |
||||
|
data: data, |
||||
|
} as const |
||||
|
}, |
||||
|
error: <T>(message: string, data: T) => { |
||||
|
return { |
||||
|
code: 1, |
||||
|
message: message, |
||||
|
data: null, |
||||
|
} as const |
||||
|
}, |
||||
|
throwError: <T>(code: number, message: string, data: T) => { |
||||
|
throw createError({ |
||||
|
statusCode: code, |
||||
|
statusMessage: message, |
||||
|
data: data, |
||||
|
}) as never |
||||
|
} |
||||
|
} as const |
||||
@ -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