+2
-2
@@ -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 />
|
||||
|
||||
@@ -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 |
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
<template>
|
||||
<main class="min-h-dvh w-full flex items-center justify-center p-6">
|
||||
<slot />
|
||||
</main>
|
||||
</template>
|
||||
+10
-1
@@ -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
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
|
||||
Generated
+216
-213
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 |
@@ -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";
|
||||
};
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
export type BaseOptions = {
|
||||
/** OpenAI API Key */
|
||||
apiKey: string;
|
||||
/** 模型名称 */
|
||||
model: string;
|
||||
/** 自定义 baseURL */
|
||||
baseURL?: string;
|
||||
};
|
||||
Reference in New Issue
Block a user