140 lines
4.0 KiB
Vue
Vendored
140 lines
4.0 KiB
Vue
Vendored
<script setup lang="ts">
|
|
import { ref, onMounted } from 'vue'
|
|
import SettingsPanel from './components/SettingsPanel.vue'
|
|
import ImageGenerator from './components/ImageGenerator.vue'
|
|
import Gallery from './components/Gallery.vue'
|
|
import { probeSession, listModels, getSelf, type NewApiUser, type Auth } from './lib/api'
|
|
import { loadSettings } from './lib/settings'
|
|
|
|
const user = ref<NewApiUser | null>(null)
|
|
const auth = ref<Auth>({ kind: 'session' })
|
|
const baseUrl = ref<string>(loadSettings().baseUrl)
|
|
const models = ref<string[]>([])
|
|
const error = ref<string>('')
|
|
const settingsRef = ref<InstanceType<typeof SettingsPanel> | null>(null)
|
|
const galleryRef = ref<InstanceType<typeof Gallery> | null>(null)
|
|
const checking = ref<boolean>(true)
|
|
|
|
async function refreshSession() {
|
|
checking.value = true
|
|
try {
|
|
const u = await probeSession(baseUrl.value)
|
|
user.value = u
|
|
error.value = ''
|
|
if (u) {
|
|
try {
|
|
models.value = await listModels(baseUrl.value, { kind: 'session' })
|
|
} catch (e) {
|
|
error.value = `已登录,但拉取模型失败: ${e}`
|
|
}
|
|
} else {
|
|
models.value = []
|
|
}
|
|
} finally {
|
|
checking.value = false
|
|
}
|
|
}
|
|
|
|
onMounted(refreshSession)
|
|
// Re-probe when the tab regains focus (e.g. user came back from /sign-in).
|
|
// `visibilitychange` covers mobile / tab-switch too; `focus` covers desktop.
|
|
window.addEventListener('focus', refreshSession)
|
|
document.addEventListener('visibilitychange', () => {
|
|
if (document.visibilityState === 'visible') refreshSession()
|
|
})
|
|
|
|
async function onSessionChanged(u: NewApiUser | null) {
|
|
user.value = u
|
|
if (u) {
|
|
try {
|
|
models.value = await listModels(baseUrl.value, { kind: 'session' })
|
|
} catch (e) {
|
|
error.value = `已登录,但拉取模型失败: ${e}`
|
|
}
|
|
} else {
|
|
models.value = []
|
|
}
|
|
}
|
|
|
|
function onAuthOverridden(a: Auth) {
|
|
auth.value = a
|
|
if (a.kind === 'sk') {
|
|
// refresh user info using the new auth
|
|
getSelf(baseUrl.value, a)
|
|
.then((u) => {
|
|
user.value = u
|
|
})
|
|
.catch((e) => {
|
|
error.value = `sk-key 无效: ${e}`
|
|
})
|
|
}
|
|
}
|
|
|
|
function onGenerated(img: { prompt: string; model: string; urls: string[] }) {
|
|
galleryRef.value?.add(img)
|
|
// refresh quota after a generation (best-effort)
|
|
if (user.value) {
|
|
probeSession(baseUrl.value)
|
|
.then((u) => {
|
|
if (u) user.value = u
|
|
})
|
|
.catch(() => {
|
|
/* ignore */
|
|
})
|
|
}
|
|
}
|
|
|
|
function onError(msg: string) {
|
|
error.value = msg
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div class="min-h-full max-w-5xl mx-auto p-6 space-y-5">
|
|
<header class="flex items-baseline justify-between">
|
|
<div>
|
|
<h1 class="text-2xl font-semibold tracking-tight">newapi-image-gen</h1>
|
|
<p class="text-xs text-zinc-500 mt-0.5">
|
|
走 new-api 的 <code class="text-zinc-400">/v1/images/generations</code>,额度扣 new-api 账户
|
|
</p>
|
|
</div>
|
|
<a
|
|
v-if="user"
|
|
href="/"
|
|
class="text-xs text-zinc-500 hover:text-zinc-300"
|
|
>← 返回 new-api 主页</a>
|
|
</header>
|
|
|
|
<p v-if="error" class="rounded-md border border-rose-800 bg-rose-950/40 px-3 py-2 text-sm text-rose-300">
|
|
{{ error }}
|
|
</p>
|
|
|
|
<SettingsPanel
|
|
ref="settingsRef"
|
|
:user="user"
|
|
@session-changed="onSessionChanged"
|
|
@auth-overridden="onAuthOverridden"
|
|
/>
|
|
|
|
<div v-if="checking" class="text-sm text-zinc-500">检查登录态…</div>
|
|
|
|
<ImageGenerator
|
|
v-else-if="user"
|
|
:base-url="baseUrl"
|
|
:auth="auth"
|
|
:models="models"
|
|
@generated="onGenerated"
|
|
@error="onError"
|
|
/>
|
|
<div v-else class="rounded-xl border border-dashed border-zinc-800 p-6 text-center text-sm text-zinc-500">
|
|
先在上方登录,登录后会自动出现生图面板。
|
|
</div>
|
|
|
|
<Gallery ref="galleryRef" :base-url="baseUrl" />
|
|
|
|
<footer class="pt-4 pb-2 text-center text-xs text-zinc-600">
|
|
登录走 new-api 自己的 OAuth,会话 cookie 跟 new-api 主站共享;本前端不会碰你的密码或 token。
|
|
</footer>
|
|
</div>
|
|
</template>
|