写接口

This commit is contained in:
expressgy 2025-04-25 11:33:24 +08:00
parent 5c9b297760
commit 54c35687c9
26 changed files with 340 additions and 49 deletions

View File

@ -1,10 +1,43 @@
/* 淡入淡出动画 */
.fade-enter-active,
.fade-leave-active {
.indexFade-enter-active,
.indexFade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
.indexFade-enter-from,
.indexFade-leave-to {
opacity: 0;
}
/* 淡入淡出动画 */
.slide-enter-active,
.slide-leave-active {
transition: all 0.5s ease;
}
.slide-enter-from,
.slide-leave-to {
opacity: 0;
transform: translate(100%, 0);
}
.slide-left-enter-active,
.slide-left-leave-active,
.slide-right-enter-active,
.slide-right-leave-active {
transition: all 0.2s;
}
.slide-left-enter-from {
opacity: 0;
transform: translate(50px, 0);
}
.slide-left-leave-to {
opacity: 0;
transform: translate(-50px, 0);
}
.slide-right-enter-from {
opacity: 0;
transform: translate(-50px, 0);
}
.slide-right-leave-to {
opacity: 0;
transform: translate(50px, 0);
}

111
docs/06-Nuxt生命周期.md Normal file
View File

@ -0,0 +1,111 @@
# Nuxt HTTP 请求生命周期
当一个 HTTP 请求发送到 Nuxt 应用时,它会经过一系列步骤,下面是完整的请求生命周期:
## 服务器端处理
### 1. Nitro 服务器初始化(仅一次)
- Nitro 服务器启动并初始化 `/server/plugins` 目录下的插件
- 这些插件只在服务器启动时执行一次
- 在无服务器环境中,每个请求都会启动服务器,但插件不会被等待执行完成
[Nuxt 生命周期](https://nuxt.com/docs/guide/concepts/nuxt-lifecycle#step-1-setup-nitro-server-and-nitro-plugins-once)
### 2. Nitro 服务器中间件
- 对于每个请求,执行 `server/middleware/` 下的中间件
- 这些中间件可用于身份验证、日志记录或请求转换
- 如果中间件返回值,请求将终止并将返回值作为响应
[Nuxt 生命周期](https://nuxt.com/docs/guide/concepts/nuxt-lifecycle#step-2-nitro-server-middleware)
### 3. 请求路由分发
根据请求路径Nitro 将请求分发到不同的处理程序:
- 如果请求路径匹配 `/api/*`,则路由到 API 处理程序(`server/api/` 目录)
- 如果请求路径匹配 `server/routes/` 中定义的路由,则路由到相应的处理程序
- 如果是页面请求,则继续 Nuxt 应用的初始化
[服务器目录结构](https://nuxt.com/docs/guide/directory-structure/server)
### 4. API 处理(如果是 API 请求)
- 如果请求匹配 `server/api/` 目录中的路由,相应的处理程序将被执行
- 处理程序可以直接返回 JSON 数据、Promise 或使用 `event.node.res.end()` 发送响应
- API 处理完成后,请求结束,不会继续到 Vue 应用
[服务器 API](https://nuxt.com/docs/guide/directory-structure/server)
### 5. Nuxt 应用初始化(如果是页面请求)
- 创建 Vue 和 Nuxt 实例
- 执行 Nuxt 服务器插件,包括内置插件和 `plugins/` 目录中的自定义插件
- 调用 `app:created` 钩子
[Nuxt 生命周期](https://nuxt.com/docs/guide/concepts/nuxt-lifecycle#step-3-initialize-nuxt-and-execute-nuxt-app-plugins)
### 6. 路由验证
- 如果在 `definePageMeta` 中定义了 `validate` 方法,则调用该方法验证动态路由参数
- 如果验证失败,可能会终止请求或重定向
[Nuxt 生命周期](https://nuxt.com/docs/guide/concepts/nuxt-lifecycle#step-4-route-validation)
### 7. 执行 Nuxt 应用中间件
- 执行全局路由中间件、命名路由中间件和匿名路由中间件
- 中间件可以执行重定向,这会导致浏览器收到 `Location:` 头并发起新请求
[Nuxt 生命周期](https://nuxt.com/docs/guide/concepts/nuxt-lifecycle#step-5-execute-nuxt-app-middleware)
### 8. 页面和组件设置
- 初始化页面和组件
- 使用 `useFetch``useAsyncData` 获取所需数据
- 服务器端不执行 Vue 生命周期钩子如 `onBeforeMount`、`onMounted` 等
[Nuxt 生命周期](https://nuxt.com/docs/guide/concepts/nuxt-lifecycle#step-6-setup-page-and-components)
### 9. 渲染和生成 HTML 输出
- 将组件与 `unhead` 设置结合生成完整的 HTML 文档
- 调用 `app:rendered` 钩子
- 调用 `render:html` 钩子,允许操作生成的 HTML
- 将 HTML 和相关数据发送回客户端
[Nuxt 生命周期](https://nuxt.com/docs/guide/concepts/nuxt-lifecycle#step-7-render-and-generate-html-output)
## 客户端处理
### 1. 执行 Nuxt 应用中间件
- 中间件在服务器端和客户端都会运行
- 可以使用 `import.meta.client``import.meta.server` 区分环境
[Nuxt 生命周期](https://nuxt.com/docs/guide/concepts/nuxt-lifecycle#step-3-execute-nuxt-app-middleware)
### 2. 挂载 Vue 应用和水合
- 调用 `app:beforeMount` 钩子
- 调用 `app.mount('#__nuxt')` 将 Vue 应用挂载到 DOM
- Vue 执行水合步骤,使客户端应用程序具有交互性
- 调用 `app:mounted` 钩子
[Nuxt 生命周期](https://nuxt.com/docs/guide/concepts/nuxt-lifecycle#step-4-mount-vue-application-and-hydration)
### 3. Vue 生命周期
- 浏览器执行完整的 Vue 生命周期
- 包括 `onBeforeMount`、`onMounted` 等钩子
[Nuxt 生命周期](https://nuxt.com/docs/guide/concepts/nuxt-lifecycle#step-5-vue-lifecycle)
## 重要说明
- 服务器端重定向会导致浏览器收到 `Location:` 头并发起新请求,所有应用状态将重置
- 中间件在服务器端渲染和客户端水合期间都会执行,可能导致代码执行两次
- 对于 API 请求,建议使用 `useAsyncData`、`useFetch` 等 SSR 友好的组合式函数,确保服务器端获取的数据在水合期间被重用
[通用渲染](https://nuxt.com/docs/guide/concepts/rendering#universal-rendering)

View File

@ -1,10 +1,10 @@
import { mysqlTable, index, primaryKey, unique, varchar, int, tinyint, datetime } from "drizzle-orm/mysql-core"
import { sql } from "drizzle-orm"
import { bigintString } from './customType.js';
import { bigintString } from '~/drizzle/customType';
const bigint = bigintString;
export const blogMenu = mysqlTable("blog_menu", {
id: bigint({ mode: "number" }).notNull(),
pid: bigint({ mode: "number" }).notNull(),
pid: bigint({ mode: "number" }).default('0').notNull(),
name: varchar({ length: 255 }).notNull(),
desc: varchar({ length: 255 }),
icon: varchar({ length: 255 }).default("").notNull(),

View File

@ -5,11 +5,9 @@
<div class="homeLayout">
<HomeHeader class="homeHeader"/>
<div class="homeMain">
<div class="homeMainLeft" name="left"></div>
<main class="homeMainContent">
<slot class="homeMainContent"/>
<slot name="default" class="homeMainContent"/>
</main>
<div class="homeMainRight" name="right"></div>
</div>
</div>
</template>
@ -38,7 +36,7 @@
overflow-y: auto;
}
& > div.homeMainLeft {
& > .homeMainLeft {
flex: 1;
}
@ -48,7 +46,7 @@
background-color: var(--bg-color-be);
}
& > div.homeMainRight {
& > .homeMainRight {
flex: 1;
}

18
modules/my-imports.ts Normal file
View File

@ -0,0 +1,18 @@
import { defineNuxtModule, addImportsSources } from '@nuxt/kit'
export default defineNuxtModule({
meta: {
name: 'my-imports'
},
setup() {
addImportsSources({
from: 'consola',
imports: [
// 列出您需要从consola导入的函数
'createConsola',
'consola'
// 其他需要的函数...
],
})
}
})

View File

@ -13,6 +13,9 @@ export default defineNuxtConfig({
host: '0.0.0.0',
port: 3000,
},
features: {
devLogs: 'silent' // 这是默认值,确保它没有被设置为 true日志区分server和client false为不在client打印
},
css: [
'~/assets/css/style.css',
'~/assets/css/font.css',
@ -40,12 +43,12 @@ export default defineNuxtConfig({
]
},
layoutTransition:{
name: 'fade',
name: 'indexFade',
mode: 'out-in',
type: 'transition', // 明确指定动画类型
duration: {
enter: 200,
leave: 500
enter: 300,
leave: 300
},
appear: true
},
@ -92,6 +95,11 @@ export default defineNuxtConfig({
'/docs/json'
],
},
defaultToken: {
username: process.env.USERNAME || 'expressgy',
nickname: process.env.PASSWORD || 'Nie',
userId: '1'
}
},
})

View File

@ -1,13 +1,17 @@
<script setup lang="ts">
definePageMeta({
layout: 'home',
pageTransition: {
name: 'slide',
mode: 'out-in',
},
})
const {data} = useFetch('/api/blog/blogMenu')
</script>
<template>
<div>Blog</div>
<div>内容SS{{data}}</div>
</template>
<style scoped>
</style>

View File

@ -1,6 +1,10 @@
<script setup lang="ts">
definePageMeta({
layout: 'home',
pageTransition: {
name: 'slide',
mode: 'out-in',
},
})
</script>

View File

@ -1,6 +1,10 @@
<script setup lang="ts">
definePageMeta({
layout: 'home',
pageTransition: {
name: 'slide',
mode: 'out-in',
},
})
</script>

View File

@ -1,6 +1,10 @@
<script setup lang="ts">
definePageMeta({
layout: 'home',
pageTransition: {
name: 'slide',
mode: 'out-in',
},
})
</script>

View File

@ -1,7 +1,10 @@
<script setup lang="ts">
import consola from "consola";
definePageMeta({
layout: 'home',
pageTransition: {
name: 'slide',
mode: 'out-in',
},
})
const {data} = await useFetch('/api/hello')
consola.info(toValue(data))
@ -9,19 +12,31 @@ consola.info(toValue(data))
<template>
<div class="homeIndex">
<div>redisData: {{data}}</div>
<div class="f1">汇文明朝体 ""| 星撰你好啊你叫什么名字,ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890</div>
<div class="f2">落霞孤鹜 ""| 星撰你好啊你叫什么名字,ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890</div>
<div class="f3">nice ""| 星撰你好啊你叫什么名字,ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890</div>
<div class="f4">TechnicLite ""| 星撰你好啊你叫什么名字,ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890</div>
<div>redisData: {{ data }}</div>
<div class="f1">汇文明朝体 ""|
星撰你好啊你叫什么名字,ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890
</div>
<div class="f2">落霞孤鹜 ""|
星撰你好啊你叫什么名字,ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890
</div>
<div class="f3">nice ""|
星撰你好啊你叫什么名字,ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890
</div>
<div class="f4">TechnicLite ""|
星撰你好啊你叫什么名字,ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890
</div>
<div class="f5">星撰</div>
<div class="f6">6sans-serif ""| 星撰你好啊你叫什么名字,ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890</div>
<div class="f7">default ""| 星撰你好啊你叫什么名字,ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890</div>
<div class="f6">6sans-serif ""|
星撰你好啊你叫什么名字,ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890
</div>
<div class="f7">default ""|
星撰你好啊你叫什么名字,ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890
</div>
</div>
</template>
<style scoped>
.homeIndex{
.homeIndex {
/* 定义英文字体 */
@font-face {
font-family: 'MyEnglishFont';
@ -35,19 +50,24 @@ consola.info(toValue(data))
src: local('LXGW WenKai');
unicode-range: U+201c, U+201d; /* 中文范围 */
}
div.f1{
div.f1 {
font-family: AA !important;
}
div.f2{
div.f2 {
font-family: 'LXGW WenKai';
}
div.f3{
div.f3 {
font-family: 'nice';
}
div.f4{
div.f4 {
font-family: 'TechnicLite';
}
div.f5{
div.f5 {
font-family: 'KingHwa_OldSong' !important;
}
}

View File

@ -0,0 +1,17 @@
<script setup lang="ts">
definePageMeta({
layout: 'home',
pageTransition: {
name: 'slide',
mode: 'out-in',
},
})
</script>
<template>
<div>tools</div>
</template>
<style scoped>
</style>

View File

@ -8,9 +8,10 @@ const leaveTime = ref(false)
//
onBeforeRouteLeave((to, from, next) => {
leaveTime.value = true
consola.error('leave')
setTimeout(() => {
next(true)
}, 1300)
}, 1000)
})
</script>

View File

@ -1,3 +1,3 @@
export default defineEventHandler(event => {
return 'Hello blog/blogMenu.[blogId].delete' + event.context.params.blogId;
return 'Hello blog/blogMenu.[blogId].delete' + event.context.params?.blogId;
})

View File

@ -1,3 +1,3 @@
export default defineEventHandler(event => {
return 'Hello blog/blogMenu.[blogId].patch' + event.context.params.blogId;
return 'Hello blog/blogMenu.[blogId].patch' + event.context.params?.blogId;
})

View File

@ -1,3 +1,3 @@
export default defineEventHandler(event => {
return 'Hello blog/blogMenu.[blogId].update' + event.context.params.blogId;
return 'Hello blog/blogMenu.[blogId].update' + event.context.params?.blogId;
})

View File

@ -1,3 +1,12 @@
export default defineEventHandler(event => {
return 'Hello blog/blogMenu.get'
import {BlogMenuDB} from "~/server/services/blog/blogMenu";
import consola from "consola";
export default defineEventHandler(async event => {
// 获取登录信息
const headerAuth = event.context.auth
// 初始化DB
const blogMenuDB = new BlogMenuDB(event)
const resd = await blogMenuDB.getBlogMenu(headerAuth)
consola.info(resd)
return resd
})

View File

@ -1,3 +1,11 @@
export default defineEventHandler(event => {
export default defineEventHandler(async event => {
const requAuth = event.context.auth
const body = await readBody(event)
if(!requAuth.isTrue) {
// 判断为正常登录
throw createError({
})
}
return 'Hello blog/blogMenu.post'
})

View File

@ -3,7 +3,6 @@ import consola from "consola";
export default defineEventHandler(async event => {
// console.log(await event.context.redis.get('SI HI'))
consola.info('API')
await new Promise(resolve => setTimeout(resolve, 1000));
consola.info('API TimeOut')
const result = await event.context.redis.get('SI HI');
return {

View File

@ -2,10 +2,12 @@ import consola from "consola";
export default defineEventHandler(event => {
// 跳过无需验证的路由
if (event.path?.startsWith('/api/auth')) return
const {defaultToken} = useRuntimeConfig()
const token = getHeader(event, 'Authorization')?.split('Bearer ')[1];
const t = generateAccessToken({
name: event.path,
...defaultToken,
isTrue: false
})
consola.info(`Token ${t}`);

View File

@ -44,7 +44,8 @@ export default defineNitroPlugin(async nitroApp => {
const db = drizzle(pool,{
logger: {
logQuery: (query, params) => {
consola.debug(`SQL: ${query} - Params: ${JSON.stringify(params)}`); // 打印日志,包括查询和参数
console.log(`SQL: ${query} - Params: ${JSON.stringify(params, (_, v) =>
typeof v === 'bigint' ? v.toString() : v)}`); // 打印日志,包括查询和参数
}
}
});

View File

@ -0,0 +1,40 @@
import type {EventHandlerRequest, H3Event} from "h3";
import type {MySql2Database, MySqlRawQueryResult} from "drizzle-orm/mysql2";
import type {RedisClient} from "ioredis/built/connectors/SentinelConnector/types";
import type {Pool} from "mysql2/promise";
import type {HeaderAuth} from "~/types/nuxt.config";
import {blogMenu} from "~/drizzle/schema";
import {and, asc, desc, eq} from "drizzle-orm";
type InsertBlogMenu = typeof blogMenu.$inferInsert;
export class BlogMenuDB {
private readonly db:MySql2Database<Record<string, never>>& { $client: Pool }
private readonly redis: RedisClient
constructor(event: H3Event<EventHandlerRequest>) {
this.db = event.context.mysql
this.redis = event.context.redis
}
// 插入目录
async insertBlogMenu(insertBlogMenu: InsertBlogMenu, headerAuth: HeaderAuth): Promise<MySqlRawQueryResult> {
const result = await this.db.insert(blogMenu).values({
...insertBlogMenu,
createdBy: headerAuth.userId,
})
return result;
}
// 查询目录
async getBlogMenu(headerAuth: HeaderAuth){
// 指定用户的
// 判断未删除的
// 如果是登陆的,可以查看未公开的,如果不是,只能查看公开的
return this.db
.select()
.from(blogMenu)
.where(
and(eq(blogMenu.createdBy, headerAuth.userId), eq(blogMenu.deleted, 0), headerAuth.isTrue ? undefined : eq(blogMenu.public, 1))
).orderBy(asc(blogMenu.sort), desc(blogMenu.createdAt));
}
}

View File

@ -1,4 +1,4 @@
import * as schema from "../../drizzle/schema";
import * as schema from "~/drizzle/schema";
export function entity() {
return schema
}

View File

@ -1,8 +1,6 @@
import jwt from 'jsonwebtoken'
import type {StringValue} from "ms";
const {sign, verify} = jwt
console.log(jwt)
// export function jwt() {}
export const generateAccessToken = (payload: object) => {

View File

@ -1,4 +1,9 @@
{
// https://nuxt.com/docs/guide/concepts/typescript
"extends": "./.nuxt/tsconfig.json"
"extends": "./.nuxt/tsconfig.json",
"compilerOptions": {
"paths": {
// "~/*": ["./*"]
}
}
}

View File

@ -9,3 +9,10 @@
// },
// }
// }
export type HeaderAuth = {
username: string;
nickname: string;
userId: string;
isTrue: boolean;
}