feat: login
This commit is contained in:
10
app.vue
10
app.vue
@@ -1,3 +1,11 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
const loginState = useLoginState()
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loginState.checkSession()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<NuxtLayout>
|
<NuxtLayout>
|
||||||
@@ -5,6 +13,6 @@
|
|||||||
</NuxtLayout>
|
</NuxtLayout>
|
||||||
|
|
||||||
<UModals/>
|
<UModals/>
|
||||||
<UNotifications />
|
<UNotifications/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
26
components/LoginNeededContent.vue
Normal file
26
components/LoginNeededContent.vue
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
const loginState = useLoginState()
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
contentClass: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="!loginState.is_logged_in"
|
||||||
|
class="w-full flex flex-col justify-center items-center gap-2 bg-neutral-100 dark:bg-neutral-900">
|
||||||
|
<Icon name="i-tabler-user-circle" class="text-7xl text-neutral-300 dark:text-neutral-700"/>
|
||||||
|
<p class="text-sm text-neutral-500 dark:text-neutral-400">请登录后使用</p>
|
||||||
|
<UButton class="mt-2 font-bold" to="/login" color="black" variant="solid" size="xs">登录</UButton>
|
||||||
|
</div>
|
||||||
|
<div :class="contentClass" v-else>
|
||||||
|
<slot/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -1,14 +1,15 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {Label, PinInputInput, PinInputRoot} from 'radix-vue'
|
import {Label, PinInputInput, PinInputRoot} from 'radix-vue'
|
||||||
|
import {useFetchWrapped} from "~/composables/useFetchWrapped";
|
||||||
|
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const modal = useModal()
|
const modal = useModal()
|
||||||
|
const loginState = useLoginState()
|
||||||
|
|
||||||
const sms_triggered = ref(false)
|
const sms_triggered = ref(false)
|
||||||
const sms_sending = ref(false)
|
const sms_sending = ref(false)
|
||||||
const sms_counting_down = ref(0)
|
const sms_counting_down = ref(0)
|
||||||
const final_loading = ref(false)
|
const final_loading = ref(false)
|
||||||
const handleComplete = (e: string[]) => toast.add({title: `提交验证码 ${e.join('')}`, color: 'indigo', icon: 'i-tabler-circle-check'})
|
|
||||||
|
|
||||||
const items = [
|
const items = [
|
||||||
{
|
{
|
||||||
@@ -25,20 +26,85 @@ const items = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const accountForm = reactive({username: '', password: ''})
|
const accountForm = reactive<req.user.Login>({username: '', password: ''})
|
||||||
const smsForm = reactive({mobile: '', sms_code: []})
|
const smsForm = reactive({mobile: '', sms_code: []})
|
||||||
|
|
||||||
function onSubmit(form: any) {
|
function onSubmit(form: req.user.Login) {
|
||||||
console.log('Submitted form:', form)
|
console.log('Submitted form:', form)
|
||||||
|
final_loading.value = true
|
||||||
|
useFetchWrapped<req.user.Login, BaseResponse<resp.user.Login>>('App.User_User.Login', {
|
||||||
|
username: form.username,
|
||||||
|
password: form.password
|
||||||
|
}).then(res => {
|
||||||
|
final_loading.value = false
|
||||||
|
if (res.ret !== 200) {
|
||||||
|
toast.add({
|
||||||
|
title: '登录失败',
|
||||||
|
description: res.msg || '未知错误',
|
||||||
|
color: 'red',
|
||||||
|
icon: 'i-tabler-circle-x'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!res.data.is_login) {
|
||||||
|
toast.add({
|
||||||
|
title: '登录失败',
|
||||||
|
description: res.msg || '账号或密码错误',
|
||||||
|
color: 'red',
|
||||||
|
icon: 'i-tabler-circle-x'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!res.data.token || !res.data.user_id) {
|
||||||
|
toast.add({
|
||||||
|
title: '登录失败',
|
||||||
|
description: res.msg || '无法获取登录状态',
|
||||||
|
color: 'red',
|
||||||
|
icon: 'i-tabler-circle-x'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
loginState.token = res.data.token
|
||||||
|
loginState.user.id = res.data.user_id
|
||||||
|
loginState.updateProfile().then(() => {
|
||||||
|
loginState.checkSession()
|
||||||
|
modal.close()
|
||||||
|
toast.add({
|
||||||
|
title: '登录成功',
|
||||||
|
description: `${loginState.user.username}, 欢迎回来`,
|
||||||
|
color: 'primary',
|
||||||
|
icon: 'i-tabler-login-2'
|
||||||
|
})
|
||||||
|
}).catch(err => {
|
||||||
|
toast.add({
|
||||||
|
title: '登录失败',
|
||||||
|
description: err.msg || '网络错误',
|
||||||
|
color: 'red',
|
||||||
|
icon: 'i-tabler-circle-x'
|
||||||
|
})
|
||||||
|
}).finally(() => {
|
||||||
|
final_loading.value = false
|
||||||
|
})
|
||||||
|
}).catch(err => {
|
||||||
|
toast.add({
|
||||||
|
title: '登录失败',
|
||||||
|
description: err.msg || '网络错误',
|
||||||
|
color: 'red',
|
||||||
|
icon: 'i-tabler-circle-x'
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const obtainSmsCode = () => {
|
const obtainSmsCode = () => {
|
||||||
smsForm.sms_code = []
|
smsForm.sms_code = []
|
||||||
sms_sending.value = true
|
sms_sending.value = true
|
||||||
setTimeout(() => {
|
|
||||||
|
useFetchWrapped<req.user.SmsLogin, BaseResponse<resp.user.SmsLogin>>('App.User_User.MobileLogin', {
|
||||||
|
mobile: smsForm.mobile
|
||||||
|
}).then(res => {
|
||||||
sms_triggered.value = true
|
sms_triggered.value = true
|
||||||
sms_sending.value = false
|
sms_sending.value = false
|
||||||
sms_counting_down.value = 15 // TODO: modify this to change the countdown time
|
sms_counting_down.value = 60 // TODO: modify this to change the countdown time
|
||||||
toast.add({title: '短信验证码已发送', color: 'indigo', icon: 'i-tabler-circle-check'})
|
toast.add({title: '短信验证码已发送', color: 'indigo', icon: 'i-tabler-circle-check'})
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
sms_counting_down.value--
|
sms_counting_down.value--
|
||||||
@@ -46,7 +112,64 @@ const obtainSmsCode = () => {
|
|||||||
clearInterval(interval)
|
clearInterval(interval)
|
||||||
}
|
}
|
||||||
}, 1000)
|
}, 1000)
|
||||||
}, 2000)
|
}).catch(err => {
|
||||||
|
toast.add({
|
||||||
|
title: '验证码发送失败',
|
||||||
|
description: err.msg || '网络错误',
|
||||||
|
color: 'red',
|
||||||
|
icon: 'i-tabler-circle-x'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handle_sms_verify = (e: string[]) => {
|
||||||
|
final_loading.value = true
|
||||||
|
useFetchWrapped<req.user.SmsLoginVerify, BaseResponse<resp.user.SmsLoginVerify>>('App.User_User.MobileLoginVerify', {
|
||||||
|
mobile: smsForm.mobile,
|
||||||
|
sms_code: e.join('')
|
||||||
|
}).then(res => {
|
||||||
|
if (res.ret !== 200) {
|
||||||
|
smsForm.sms_code = []
|
||||||
|
toast.add({
|
||||||
|
title: '登录失败',
|
||||||
|
description: res.msg || '未知错误',
|
||||||
|
color: 'red',
|
||||||
|
icon: 'i-tabler-circle-x'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!res.data.token || !res.data.person_id) {
|
||||||
|
smsForm.sms_code = []
|
||||||
|
toast.add({
|
||||||
|
title: '登录失败',
|
||||||
|
description: res.msg || '无法获取登录状态',
|
||||||
|
color: 'red',
|
||||||
|
icon: 'i-tabler-circle-x'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
loginState.token = res.data.token
|
||||||
|
loginState.user.id = res.data.person_id
|
||||||
|
loginState.updateProfile().then(() => {
|
||||||
|
loginState.checkSession()
|
||||||
|
modal.close()
|
||||||
|
toast.add({
|
||||||
|
title: '登录成功',
|
||||||
|
description: `${loginState.user.username}, 欢迎回来`,
|
||||||
|
color: 'primary',
|
||||||
|
icon: 'i-tabler-login-2'
|
||||||
|
})
|
||||||
|
}).catch(err => {
|
||||||
|
toast.add({
|
||||||
|
title: '登录失败',
|
||||||
|
description: err.msg || '网络错误',
|
||||||
|
color: 'red',
|
||||||
|
icon: 'i-tabler-circle-x'
|
||||||
|
})
|
||||||
|
}).finally(() => {
|
||||||
|
final_loading.value = false
|
||||||
|
})
|
||||||
|
}).finally(() => final_loading.value = false)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -58,7 +181,8 @@ const obtainSmsCode = () => {
|
|||||||
<h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white">
|
<h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white">
|
||||||
登录眩生花 AI 助手
|
登录眩生花 AI 助手
|
||||||
</h3>
|
</h3>
|
||||||
<UButton color="gray" variant="ghost" icon="i-heroicons-x-mark-20-solid" class="-my-1" @click="modal.close()"/>
|
<UButton color="gray" variant="ghost" icon="i-heroicons-x-mark-20-solid" class="-my-1"
|
||||||
|
@click="modal.close()"/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -71,7 +195,7 @@ const obtainSmsCode = () => {
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #item="{ item }">
|
<template #item="{ item }">
|
||||||
<UCard @submit.prevent="() => onSubmit(item.key === 'account' ? accountForm : smsForm)">
|
<UCard @submit.prevent="() => onSubmit(accountForm)">
|
||||||
<template #header>
|
<template #header>
|
||||||
<p class="text-base font-semibold leading-6 text-gray-900 dark:text-white">
|
<p class="text-base font-semibold leading-6 text-gray-900 dark:text-white">
|
||||||
{{ item.label }}
|
{{ item.label }}
|
||||||
@@ -93,14 +217,14 @@ const obtainSmsCode = () => {
|
|||||||
<div v-else-if="item.key === 'sms'" class="space-y-3">
|
<div v-else-if="item.key === 'sms'" class="space-y-3">
|
||||||
<UFormGroup label="手机号" name="mobile" required>
|
<UFormGroup label="手机号" name="mobile" required>
|
||||||
<UButtonGroup class="w-full">
|
<UButtonGroup class="w-full">
|
||||||
<UInput v-model="smsForm.mobile" type="sms" class="w-full" required>
|
<UInput v-model="smsForm.mobile" :disabled="final_loading" type="sms" class="w-full" required>
|
||||||
<template #leading>
|
<template #leading>
|
||||||
<span class="text-gray-500 dark:text-gray-400 text-xs">+86</span>
|
<span class="text-gray-500 dark:text-gray-400 text-xs">+86</span>
|
||||||
</template>
|
</template>
|
||||||
</UInput>
|
</UInput>
|
||||||
<UButton :label="sms_counting_down ? `${sms_counting_down}秒后重发` : '获取验证码'"
|
<UButton :label="sms_counting_down ? `${sms_counting_down}秒后重发` : '获取验证码'"
|
||||||
@click="obtainSmsCode"
|
@click="obtainSmsCode"
|
||||||
:loading="sms_sending" :disabled="!!sms_counting_down"
|
:loading="sms_sending" :disabled="!!sms_counting_down || final_loading"
|
||||||
class="text-xs font-bold" color="gray"/>
|
class="text-xs font-bold" color="gray"/>
|
||||||
</UButtonGroup>
|
</UButtonGroup>
|
||||||
</UFormGroup>
|
</UFormGroup>
|
||||||
@@ -110,17 +234,18 @@ const obtainSmsCode = () => {
|
|||||||
<PinInputRoot
|
<PinInputRoot
|
||||||
id="sms-input"
|
id="sms-input"
|
||||||
v-model="smsForm.sms_code"
|
v-model="smsForm.sms_code"
|
||||||
:disabled="sms_sending"
|
:disabled="sms_sending || final_loading"
|
||||||
placeholder="○"
|
placeholder="○"
|
||||||
class="w-full flex gap-2 justify-between items-center mt-1"
|
class="w-full flex gap-2 justify-between md:justify-start items-center mt-1"
|
||||||
@complete="handleComplete"
|
@complete="handle_sms_verify"
|
||||||
type="number" otp required
|
type="number" otp required
|
||||||
>
|
>
|
||||||
<PinInputInput
|
<PinInputInput
|
||||||
v-for="(id, index) in 6"
|
v-for="(id, index) in 4"
|
||||||
:key="id"
|
:key="id"
|
||||||
:index="index"
|
:index="index"
|
||||||
class="pin-input"
|
class="pin-input"
|
||||||
|
:autofocus="index === 0"
|
||||||
/>
|
/>
|
||||||
</PinInputRoot>
|
</PinInputRoot>
|
||||||
</div>
|
</div>
|
||||||
@@ -128,7 +253,7 @@ const obtainSmsCode = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template #footer v-if="item.key !== 'sms'">
|
<template #footer v-if="item.key !== 'sms'">
|
||||||
<UButton type="submit" color="black" :disabled="final_loading">
|
<UButton type="submit" color="black" :loading="final_loading">
|
||||||
登录
|
登录
|
||||||
</UButton>
|
</UButton>
|
||||||
</template>
|
</template>
|
||||||
@@ -152,7 +277,7 @@ const obtainSmsCode = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.pin-input {
|
.pin-input {
|
||||||
@apply w-full aspect-square rounded text-center shadow caret-transparent;
|
@apply w-full md:w-16 aspect-square rounded text-center shadow caret-transparent;
|
||||||
@apply outline-0 ring-indigo-500 focus:ring font-bold;
|
@apply outline-0 ring-indigo-500 focus:ring font-bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
22
composables/useDefer.ts
Normal file
22
composables/useDefer.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
export const useDefer = (maxFrame: number = 1000) => {
|
||||||
|
const frame = ref(1)
|
||||||
|
let rafId: number
|
||||||
|
|
||||||
|
function updateFrame() {
|
||||||
|
rafId = requestAnimationFrame(() => {
|
||||||
|
frame.value++
|
||||||
|
if (frame.value > maxFrame) return
|
||||||
|
updateFrame()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
updateFrame()
|
||||||
|
})
|
||||||
|
onUnmounted(() => {
|
||||||
|
cancelAnimationFrame(rafId)
|
||||||
|
})
|
||||||
|
return (n: number) => {
|
||||||
|
return frame.value >= n
|
||||||
|
}
|
||||||
|
}
|
||||||
21
composables/useFetchWrapped.ts
Normal file
21
composables/useFetchWrapped.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import {useFormPayload} from "~/composables/useFormPayload";
|
||||||
|
|
||||||
|
export const useFetchWrapped = <TypeReq, TypeResp>(
|
||||||
|
action: string,
|
||||||
|
payload?: TypeReq,
|
||||||
|
options?: {
|
||||||
|
method?: 'GET' | 'POST'
|
||||||
|
headers?: Record<string, string>
|
||||||
|
baseURL?: string
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
const runtimeConfig = useRuntimeConfig()
|
||||||
|
return $fetch<TypeResp>('/', {
|
||||||
|
baseURL: options?.baseURL || runtimeConfig.public.API_BASE,
|
||||||
|
method: options?.method || 'POST',
|
||||||
|
query: {
|
||||||
|
s: action
|
||||||
|
},
|
||||||
|
body: useFormPayload(payload as object)
|
||||||
|
})
|
||||||
|
}
|
||||||
10
composables/useFormPayload.ts
Normal file
10
composables/useFormPayload.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
export const useFormPayload = (payload: object) => {
|
||||||
|
const formData = new FormData()
|
||||||
|
for (const dataKey in payload) {
|
||||||
|
if (payload.hasOwnProperty(dataKey)) {
|
||||||
|
// @ts-ignore
|
||||||
|
formData.append(dataKey, payload[dataKey])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return formData
|
||||||
|
}
|
||||||
64
composables/useLoginState.ts
Normal file
64
composables/useLoginState.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import {useFetchWrapped} from "~/composables/useFetchWrapped";
|
||||||
|
|
||||||
|
export const useLoginState = defineStore('loginState', () => {
|
||||||
|
const is_logged_in = ref(false)
|
||||||
|
const token = ref<string | null>(null)
|
||||||
|
const user = ref<UserSchema>({} as UserSchema)
|
||||||
|
|
||||||
|
const checkSession = () => {
|
||||||
|
return new Promise<boolean>(resolve => {
|
||||||
|
if (!token.value) return resolve(false)
|
||||||
|
useFetchWrapped<AuthedRequest, BaseResponse<resp.user.CheckSession>>('App.User_User.CheckSession', {
|
||||||
|
token: token.value,
|
||||||
|
user_id: user.value.id
|
||||||
|
}).then(res => {
|
||||||
|
if (res.ret !== 200) {
|
||||||
|
resolve(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resolve(res.data.is_login)
|
||||||
|
// update global state
|
||||||
|
is_logged_in.value = res.data.is_login
|
||||||
|
}).catch(err => resolve(false))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateProfile = () => {
|
||||||
|
return new Promise<UserSchema>((resolve, reject) => {
|
||||||
|
if (!token.value) return reject('token is empty')
|
||||||
|
useFetchWrapped<AuthedRequest, BaseResponse<resp.user.Profile>>('App.User_User.Profile', {
|
||||||
|
token: token.value,
|
||||||
|
user_id: user.value.id
|
||||||
|
}).then(res => {
|
||||||
|
if (res.ret !== 200) {
|
||||||
|
reject(res.msg || '未知错误')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user.value = res.data.profile
|
||||||
|
resolve(res.data.profile)
|
||||||
|
}).catch(err => reject(err || '未知错误'))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const logout = () => new Promise<void>(resolve => {
|
||||||
|
token.value = null
|
||||||
|
user.value = {} as UserSchema
|
||||||
|
is_logged_in.value = false
|
||||||
|
resolve()
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
is_logged_in,
|
||||||
|
token,
|
||||||
|
user,
|
||||||
|
checkSession,
|
||||||
|
updateProfile,
|
||||||
|
logout
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
persist: {
|
||||||
|
key: 'xsh_assistant_persisted_state',
|
||||||
|
storage: persistedState.localStorage,
|
||||||
|
paths: ['token', 'user']
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -4,6 +4,8 @@ import ModalAuthentication from "~/components/ModalAuthentication.vue";
|
|||||||
const colorMode = useColorMode()
|
const colorMode = useColorMode()
|
||||||
const dayjs = useDayjs()
|
const dayjs = useDayjs()
|
||||||
const modal = useModal()
|
const modal = useModal()
|
||||||
|
const toast = useToast()
|
||||||
|
const loginState = useLoginState()
|
||||||
|
|
||||||
const isDark = computed({
|
const isDark = computed({
|
||||||
get() {
|
get() {
|
||||||
@@ -40,7 +42,13 @@ const items = [
|
|||||||
icon: 'i-tabler-user-circle'
|
icon: 'i-tabler-user-circle'
|
||||||
}], [{
|
}], [{
|
||||||
label: '注销登录',
|
label: '注销登录',
|
||||||
icon: 'i-tabler-logout'
|
icon: 'i-tabler-logout',
|
||||||
|
click: () => loginState.logout().then(() => toast.add({
|
||||||
|
title: '退出登录',
|
||||||
|
description: `您已成功退出登录账号`,
|
||||||
|
color: 'indigo',
|
||||||
|
icon: 'i-tabler-logout-2'
|
||||||
|
}))
|
||||||
}]
|
}]
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -68,9 +76,10 @@ const open_login_modal = () => {
|
|||||||
aria-label="Theme"
|
aria-label="Theme"
|
||||||
@click="isDark = !isDark"
|
@click="isDark = !isDark"
|
||||||
/>
|
/>
|
||||||
<UButton label="登录或注册" size="xs" class="font-bold" color="indigo" @click="open_login_modal"/>
|
<UButton v-if="!loginState.is_logged_in" label="登录或注册" size="xs" class="font-bold" color="indigo"
|
||||||
|
@click="open_login_modal"/>
|
||||||
</ClientOnly>
|
</ClientOnly>
|
||||||
<UDropdown :items="items" :popper="{ placement: 'bottom-start' }"
|
<UDropdown v-if="loginState.is_logged_in" :items="items" :popper="{ placement: 'bottom-start' }"
|
||||||
:ui="{ item: { disabled: 'cursor-text select-text' } }">
|
:ui="{ item: { disabled: 'cursor-text select-text' } }">
|
||||||
<UAvatar :src="void 0" icon="i-tabler-user" size="md"/>
|
<UAvatar :src="void 0" icon="i-tabler-user" size="md"/>
|
||||||
|
|
||||||
@@ -78,10 +87,12 @@ const open_login_modal = () => {
|
|||||||
<div class="text-left">
|
<div class="text-left">
|
||||||
<p class="flex items-center gap-1">
|
<p class="flex items-center gap-1">
|
||||||
已登录为
|
已登录为
|
||||||
<UBadge color="amber" size="xs" variant="subtle">OP</UBadge>
|
<UBadge v-if="loginState.user.auth_code === 2" color="amber" size="xs" variant="subtle">
|
||||||
|
OP
|
||||||
|
</UBadge>
|
||||||
</p>
|
</p>
|
||||||
<p class="truncate whitespace-nowrap max-w-40 font-medium text-gray-900 dark:text-white">
|
<p class="truncate whitespace-nowrap max-w-40 font-medium text-gray-900 dark:text-white">
|
||||||
{{ item.label }}
|
{{ loginState.user?.username }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||||
export default defineNuxtConfig({
|
export default defineNuxtConfig({
|
||||||
devtools: {enabled: true},
|
devtools: {enabled: true},
|
||||||
|
runtimeConfig: {
|
||||||
|
public: {
|
||||||
|
API_BASE: 'https://service1.fenshenzhike.com/'
|
||||||
|
}
|
||||||
|
},
|
||||||
modules: [
|
modules: [
|
||||||
'@nuxt/ui',
|
'@nuxt/ui',
|
||||||
'radix-vue/nuxt',
|
'radix-vue/nuxt',
|
||||||
|
|||||||
@@ -24,11 +24,11 @@ const props = defineProps({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const expand_prompt = ref(false)
|
const expand_prompt = ref(false)
|
||||||
const show_meta = ref(false)
|
const show_meta = ref(true)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="">
|
<div class="w-full">
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
<UIcon :name="icon"/>
|
<UIcon :name="icon"/>
|
||||||
<h1 class="text-sm font-semibold">
|
<h1 class="text-sm font-semibold">
|
||||||
|
|||||||
@@ -4,11 +4,16 @@ import image2 from '~/assets/example/2.jpg';
|
|||||||
import image3 from '~/assets/example/3.jpg';
|
import image3 from '~/assets/example/3.jpg';
|
||||||
import OptionBlock from "~/pages/aigc/drawing/components/OptionBlock.vue";
|
import OptionBlock from "~/pages/aigc/drawing/components/OptionBlock.vue";
|
||||||
import ResultBlock from "~/pages/aigc/drawing/components/ResultBlock.vue";
|
import ResultBlock from "~/pages/aigc/drawing/components/ResultBlock.vue";
|
||||||
|
import {useLoginState} from "~/composables/useLoginState";
|
||||||
|
import ModalAuthentication from "~/components/ModalAuthentication.vue";
|
||||||
|
|
||||||
useHead({
|
useHead({
|
||||||
title: '绘画 | XSH AI'
|
title: '绘画 | XSH AI'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const modal = useModal()
|
||||||
|
const loginState = useLoginState()
|
||||||
|
|
||||||
const leftSection = ref<HTMLElement | null>(null)
|
const leftSection = ref<HTMLElement | null>(null)
|
||||||
const leftHandler = ref<HTMLElement | null>(null)
|
const leftHandler = ref<HTMLElement | null>(null)
|
||||||
|
|
||||||
@@ -54,7 +59,7 @@ const images2 = [
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full flex">
|
<div class="w-full flex">
|
||||||
<div ref="leftSection"
|
<div ref="leftSection"
|
||||||
class="hidden md:block relative h-[calc(100vh-4rem)] overflow-hidden bg-neutral-200 dark:bg-neutral-800 transition-all"
|
class="sticky hidden md:block h-[calc(100vh-4rem)] overflow-hidden bg-neutral-200 dark:bg-neutral-800 transition-all"
|
||||||
style="width: 320px">
|
style="width: 320px">
|
||||||
<div ref="leftHandler"
|
<div ref="leftHandler"
|
||||||
class="absolute inset-0 left-auto hidden xl:flex flex-col justify-center items-center cursor-ew-resize px-1 group"
|
class="absolute inset-0 left-auto hidden xl:flex flex-col justify-center items-center cursor-ew-resize px-1 group"
|
||||||
@@ -77,28 +82,29 @@ const images2 = [
|
|||||||
</OptionBlock>
|
</OptionBlock>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1 h-screen flex flex-col gap-4 bg-neutral-100 dark:bg-neutral-900 p-4 overflow-y-auto">
|
<div class="flex-1 h-screen flex flex-col gap-4 bg-neutral-100 dark:bg-neutral-900 p-4 pb-20 overflow-y-auto">
|
||||||
<ResultBlock :images="images"
|
<div v-if="!loginState.is_logged_in"
|
||||||
|
class="w-full h-full flex flex-col justify-center items-center gap-2 bg-neutral-100 dark:bg-neutral-900">
|
||||||
|
<Icon name="i-tabler-user-circle" class="text-7xl text-neutral-300 dark:text-neutral-700"/>
|
||||||
|
<p class="text-sm text-neutral-500 dark:text-neutral-400">请登录后使用</p>
|
||||||
|
<UButton class="mt-2 font-bold" @click="modal.open(ModalAuthentication)" color="black" variant="solid"
|
||||||
|
size="xs">登录
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
<ResultBlock v-else :images="images" v-for="i in 12" :key="i"
|
||||||
title="XX大模型 · 文生图" :meta="{
|
title="XX大模型 · 文生图" :meta="{
|
||||||
id: 'd166429411dfc6722e54c032cdba97a2',
|
id: 'd166429411dfc6722e54c032cdba97a2',
|
||||||
aspect: '9:16',
|
aspect: '9:16',
|
||||||
cost: '1500',
|
cost: '1500',
|
||||||
modal: '混元大模型',
|
modal: '混元大模型',
|
||||||
ratio: '16:9',
|
ratio: '16:9',
|
||||||
datetime: 1709106270
|
datetime: 1709106270
|
||||||
}"
|
}"
|
||||||
prompt="这是, 一组, 测试用的, 提示词, 很长, 很长很长, 很长, 很长, 很长, 很长, 很长, 很长, 很长, 很长, 很长, 很长, 很长, 很长, 很长, 很长, 很长, 很长, 很长, 很长, 很长, 很长">
|
prompt="这是, 一组, 测试用的, 提示词, 很长, 很长很长, 很长, 很长, 很长, 很长, 很长, 很长, 很长, 很长, 很长, 很长, 很长, 很长, 很长, 很长, 很长, 很长, 很长, 很长, 很长, 很长">
|
||||||
<template #header-right>
|
<template #header-right>
|
||||||
<UButton color="gray" size="xs" icon="i-tabler-trash" variant="ghost"></UButton>
|
<UButton color="gray" size="xs" icon="i-tabler-trash" variant="ghost"></UButton>
|
||||||
</template>
|
</template>
|
||||||
</ResultBlock>
|
</ResultBlock>
|
||||||
<ResultBlock :images="images2"
|
|
||||||
title="XX大模型 · 图生图"
|
|
||||||
prompt="这是, 一组, 测试用的, 提示词">
|
|
||||||
<template #header-right>
|
|
||||||
<UButton color="gray" size="xs" icon="i-tabler-trash" variant="ghost"></UButton>
|
|
||||||
</template>
|
|
||||||
</ResultBlock>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
70
typings/schema.d.ts
vendored
Normal file
70
typings/schema.d.ts
vendored
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
interface AuthedRequest {
|
||||||
|
token?: string
|
||||||
|
user_id?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BaseResponse<T> {
|
||||||
|
ret: number
|
||||||
|
msg: string
|
||||||
|
data: T
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UserSchema {
|
||||||
|
id: number
|
||||||
|
username: string
|
||||||
|
nickname: string
|
||||||
|
avatar: string
|
||||||
|
sex: 0 | 1 | 2 // 0: 未知, 1: 男, 2: 女
|
||||||
|
email: string
|
||||||
|
mobile: string
|
||||||
|
auth_code: 0 | 1 | 2 // 0: Banned, 1: User, 2: Operator
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace req {
|
||||||
|
namespace user {
|
||||||
|
/**
|
||||||
|
* @description 用户登录
|
||||||
|
* @param username 用户名或手机号
|
||||||
|
* @param password 密码
|
||||||
|
*/
|
||||||
|
interface Login {
|
||||||
|
username: string
|
||||||
|
password: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SmsLogin {
|
||||||
|
mobile: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SmsLoginVerify {
|
||||||
|
mobile: string
|
||||||
|
sms_code: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace resp {
|
||||||
|
namespace user {
|
||||||
|
interface CheckSession {
|
||||||
|
is_login: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Login {
|
||||||
|
is_login: boolean
|
||||||
|
token?: string
|
||||||
|
user_id?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Profile {
|
||||||
|
profile: UserSchema
|
||||||
|
}
|
||||||
|
interface SmsLogin {
|
||||||
|
message: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SmsLoginVerify {
|
||||||
|
token: string
|
||||||
|
person_id: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user