feat: 封装一些函数,补充页面样式

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
2026-04-25 15:42:48 +08:00
parent aa53691dc7
commit 7d1cc2da1a
18 changed files with 394 additions and 235 deletions
+2 -2
View File
@@ -5,13 +5,13 @@ import "vue-sonner/style.css";
const userStore = useUserStore();
onMounted(() => {
onBeforeMount(() => {
userStore.getUserInfo();
});
</script>
<template>
<div class="min-h-dvh">
<div class="h-dvh">
<Toaster position="top-center" />
<NuxtLayout>
<NuxtPage />
+3
View File
@@ -1,3 +1,6 @@
:root {
--el-color-primary: #2563eb !important;
--el-border-radius-base: 12px !important;
--el-border-radius-small: 8px !important;
--el-border-radius-round: 20px !important;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

+25
View File
@@ -0,0 +1,25 @@
<script setup lang="ts">
import { useUserStore } from "~/stores";
import logo from "~/assets/logo.png";
const userStore = useUserStore();
const { isOnline } = storeToRefs(userStore);
</script>
<template>
<div class="w-full h-16 flex items-center justify-between border-b px-4">
<img
:src="logo"
alt="AI Art Studio logo"
class="h-8 select-none"
:class="{
'cursor-pointer': $route.path !== '/'
}"
@click="$router.push('/')"
/>
<div v-if="!$route.path.includes('login')">
<el-button v-if="isOnline" @click="userStore.logout()">退出</el-button>
<el-button v-else @click="$router.push('/login')">登录</el-button>
</div>
</div>
</template>
+3 -3
View File
@@ -36,13 +36,13 @@ const submit = async () => {
const valid = await formRef.value?.validate().catch(() => false);
if (!valid) return;
const response = await userStore.login({
const res = await userStore.login({
username: form.username.trim(),
password: form.password
});
if (response?.code === 0) {
emit("success", response);
if (res?.code === 0) {
emit("success", res);
}
};
+3 -3
View File
@@ -87,10 +87,10 @@ const submit = async () => {
payload.aff_code = form.aff_code.trim();
}
const response = await userStore.register(payload);
const res = await userStore.register(payload);
if (response?.code === 0) {
emit("success", response);
if (res?.code === 0) {
emit("success", res);
}
};
+11 -3
View File
@@ -1,5 +1,13 @@
<template>
<div>
<slot />
</div>
<main class="w-full h-dvh flex flex-col">
<HeaderCom />
<div class="min-h-0 flex-1 px-4 overflow-y-auto flex">
<div
class="w-full"
:class="{ 'm-auto max-w-sm': $route.path.includes('login') }"
>
<slot />
</div>
</div>
</main>
</template>
-5
View File
@@ -1,5 +0,0 @@
<template>
<main class="min-h-dvh w-full flex items-center justify-center p-6">
<slot />
</main>
</template>
+10 -1
View File
@@ -1,4 +1,13 @@
<script setup lang="ts"></script>
<script setup lang="ts">
import { useUserStore } from "~/stores";
definePageMeta({
layout: "index-view"
});
const userStore = useUserStore();
const { isOnline } = storeToRefs(userStore);
</script>
<template>
<div>123</div>
+2 -2
View File
@@ -4,7 +4,7 @@ import SignUpCom from "~/components/login/SignUpCom.vue";
import { useUserStore } from "~/stores";
definePageMeta({
layout: "login-view"
layout: "index-view"
});
const route = useRoute();
@@ -21,7 +21,7 @@ watch(isOnline, (online) => {
</script>
<template>
<div class="w-full flex justify-center">
<div class="w-full flex justify-center py-4">
<SignUpCom v-if="isSignUp" />
<LogInCom v-else />
</div>
+1
View File
@@ -19,6 +19,7 @@
"@pinia/nuxt": "0.11.3",
"@takumi-rs/core": "^1.0.16",
"nuxt": "^4.4.2",
"openai": "^6.34.0",
"pinia": "^3.0.4",
"vue": "^3.5.32",
"vue-router": "^5.0.4",
+216 -213
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 12 KiB

+3
View File
@@ -107,6 +107,7 @@ export default defineEventHandler(async (event) => {
const normalizeLoginResponse = (
response: NewApiLoginResponse | undefined
): { message: string; user: IUserLoginData | null } => {
consola.info("user data", response);
if (isUser(response)) {
return {
message: "登录成功",
@@ -152,6 +153,7 @@ const pickUserBasicData = (user: INewApiLoginUserData): IUserLoginData => {
};
};
/** 判断值是否为合法的 NewAPI 登录用户对象,所有必需字段均须类型正确 */
const isUser = (value: unknown): value is INewApiLoginUserData => {
return (
isRecord(value) &&
@@ -164,6 +166,7 @@ const isUser = (value: unknown): value is INewApiLoginUserData => {
);
};
/** 判断值是否为非 null 的普通对象,用于后续安全访问属性 */
const isRecord = (value: unknown): value is Record<string, unknown> => {
return value !== null && typeof value === "object";
};
+3 -3
View File
@@ -51,14 +51,14 @@ export const newApiFetchRaw = async <T>(
path: string,
options?: FetchOptions
): Promise<INewApiRawResponse<T>> => {
const response = await $fetch.raw<T>(path, {
const res = await $fetch.raw<T>(path, {
baseURL: BASE_URL,
...options
});
return {
_data: response._data as T | undefined,
headers: response.headers
_data: res._data as T | undefined,
headers: res.headers
};
};
+104
View File
@@ -0,0 +1,104 @@
// server/utils/openai.ts
import OpenAI from "openai";
import type { BaseOptions } from "#shared/types/openai";
/** 通用 AI 调用函数(支持文本 / 图文 / 多模态) */
export const askAI = async ({
apiKey,
model,
baseURL,
input
}: BaseOptions & {
input: OpenAI.Responses.ResponseCreateParams["input"];
}) => {
const client = new OpenAI({ apiKey, baseURL });
return await client.responses.create({
model,
input
});
};
/** 调用文本模型 */
export const askText = async (
options: BaseOptions & {
text: string;
}
) => {
const res = await askAI({
...options,
input: options.text
});
return res.output_text;
};
/**
* 调用视觉模型:输入「文本 + 图片」,输出文本
*
* 分辨率参数说明:
*
* | 值 | 含义 |
* |------|--------------------------|
* | low | 低分辨率(更快、更省成本) |
* | high | 高分辨率(更精准) |
* | auto | 自动选择 |
*
*/
export const askVision = async (
options: BaseOptions & {
text: string;
image: string;
detail?: "low" | "high" | "auto";
}
) => {
// 如果图片不是 URL,也不是 data URL,就当成 base64 处理,自动补全 data URL 前缀
const normalizeImage = (img: string) => {
if (img.startsWith("http")) return img;
if (!img.startsWith("data:")) {
return `data:image/png;base64,${img}`;
}
return img;
};
const res = await askAI({
...options,
input: [
{
role: "user",
content: [
{
type: "input_text",
text: options.text
},
{
type: "input_image",
image_url: normalizeImage(options.image),
detail: options.detail || "auto"
}
]
}
]
});
return res.output_text;
};
/** 调用流式接口 */
export const askStream = async (
options: BaseOptions & {
input: OpenAI.Responses.ResponseCreateParams["input"];
}
) => {
const client = new OpenAI({
apiKey: options.apiKey,
baseURL: options.baseURL
});
const stream = await client.responses.stream({
model: options.model,
input: options.input
});
return stream;
};
+8
View File
@@ -0,0 +1,8 @@
export type BaseOptions = {
/** OpenAI API Key */
apiKey: string;
/** 模型名称 */
model: string;
/** 自定义 baseURL */
baseURL?: string;
};