+2
-2
@@ -5,13 +5,13 @@ import "vue-sonner/style.css";
|
|||||||
|
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
|
|
||||||
onMounted(() => {
|
onBeforeMount(() => {
|
||||||
userStore.getUserInfo();
|
userStore.getUserInfo();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="min-h-dvh">
|
<div class="h-dvh">
|
||||||
<Toaster position="top-center" />
|
<Toaster position="top-center" />
|
||||||
<NuxtLayout>
|
<NuxtLayout>
|
||||||
<NuxtPage />
|
<NuxtPage />
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
:root {
|
:root {
|
||||||
--el-color-primary: #2563eb !important;
|
--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);
|
const valid = await formRef.value?.validate().catch(() => false);
|
||||||
if (!valid) return;
|
if (!valid) return;
|
||||||
|
|
||||||
const response = await userStore.login({
|
const res = await userStore.login({
|
||||||
username: form.username.trim(),
|
username: form.username.trim(),
|
||||||
password: form.password
|
password: form.password
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response?.code === 0) {
|
if (res?.code === 0) {
|
||||||
emit("success", response);
|
emit("success", res);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -87,10 +87,10 @@ const submit = async () => {
|
|||||||
payload.aff_code = form.aff_code.trim();
|
payload.aff_code = form.aff_code.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await userStore.register(payload);
|
const res = await userStore.register(payload);
|
||||||
|
|
||||||
if (response?.code === 0) {
|
if (res?.code === 0) {
|
||||||
emit("success", response);
|
emit("success", res);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<main class="w-full h-dvh flex flex-col">
|
||||||
<slot />
|
<HeaderCom />
|
||||||
</div>
|
<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>
|
</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>
|
<template>
|
||||||
<div>123</div>
|
<div>123</div>
|
||||||
|
|||||||
+2
-2
@@ -4,7 +4,7 @@ import SignUpCom from "~/components/login/SignUpCom.vue";
|
|||||||
import { useUserStore } from "~/stores";
|
import { useUserStore } from "~/stores";
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
layout: "login-view"
|
layout: "index-view"
|
||||||
});
|
});
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
@@ -21,7 +21,7 @@ watch(isOnline, (online) => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="w-full flex justify-center">
|
<div class="w-full flex justify-center py-4">
|
||||||
<SignUpCom v-if="isSignUp" />
|
<SignUpCom v-if="isSignUp" />
|
||||||
<LogInCom v-else />
|
<LogInCom v-else />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
"@pinia/nuxt": "0.11.3",
|
"@pinia/nuxt": "0.11.3",
|
||||||
"@takumi-rs/core": "^1.0.16",
|
"@takumi-rs/core": "^1.0.16",
|
||||||
"nuxt": "^4.4.2",
|
"nuxt": "^4.4.2",
|
||||||
|
"openai": "^6.34.0",
|
||||||
"pinia": "^3.0.4",
|
"pinia": "^3.0.4",
|
||||||
"vue": "^3.5.32",
|
"vue": "^3.5.32",
|
||||||
"vue-router": "^5.0.4",
|
"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 = (
|
const normalizeLoginResponse = (
|
||||||
response: NewApiLoginResponse | undefined
|
response: NewApiLoginResponse | undefined
|
||||||
): { message: string; user: IUserLoginData | null } => {
|
): { message: string; user: IUserLoginData | null } => {
|
||||||
|
consola.info("user data", response);
|
||||||
if (isUser(response)) {
|
if (isUser(response)) {
|
||||||
return {
|
return {
|
||||||
message: "登录成功",
|
message: "登录成功",
|
||||||
@@ -152,6 +153,7 @@ const pickUserBasicData = (user: INewApiLoginUserData): IUserLoginData => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** 判断值是否为合法的 NewAPI 登录用户对象,所有必需字段均须类型正确 */
|
||||||
const isUser = (value: unknown): value is INewApiLoginUserData => {
|
const isUser = (value: unknown): value is INewApiLoginUserData => {
|
||||||
return (
|
return (
|
||||||
isRecord(value) &&
|
isRecord(value) &&
|
||||||
@@ -164,6 +166,7 @@ const isUser = (value: unknown): value is INewApiLoginUserData => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** 判断值是否为非 null 的普通对象,用于后续安全访问属性 */
|
||||||
const isRecord = (value: unknown): value is Record<string, unknown> => {
|
const isRecord = (value: unknown): value is Record<string, unknown> => {
|
||||||
return value !== null && typeof value === "object";
|
return value !== null && typeof value === "object";
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -51,14 +51,14 @@ export const newApiFetchRaw = async <T>(
|
|||||||
path: string,
|
path: string,
|
||||||
options?: FetchOptions
|
options?: FetchOptions
|
||||||
): Promise<INewApiRawResponse<T>> => {
|
): Promise<INewApiRawResponse<T>> => {
|
||||||
const response = await $fetch.raw<T>(path, {
|
const res = await $fetch.raw<T>(path, {
|
||||||
baseURL: BASE_URL,
|
baseURL: BASE_URL,
|
||||||
...options
|
...options
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
_data: response._data as T | undefined,
|
_data: res._data as T | undefined,
|
||||||
headers: response.headers
|
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