ui: 基本框架
This commit is contained in:
@@ -1,6 +1,27 @@
|
|||||||
export default defineAppConfig({
|
export default defineAppConfig({
|
||||||
ui: {
|
ui: {
|
||||||
primary: 'green',
|
primary: 'green',
|
||||||
gray: 'neutral'
|
gray: 'neutral',
|
||||||
|
strategy: 'merge',
|
||||||
|
button: {
|
||||||
|
icon: {
|
||||||
|
loading: 'animate-none',
|
||||||
|
},
|
||||||
|
default: {
|
||||||
|
loadingIcon: 'i-svg-spinners-180-ring-with-bg'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
notifications: {
|
||||||
|
position: 'top-0 bottom-auto'
|
||||||
|
},
|
||||||
|
horizontalNavigation: {
|
||||||
|
container: 'gap-2',
|
||||||
|
base: 'px-3 py-4',
|
||||||
|
after: 'after:hidden',
|
||||||
|
active: 'before:bg-neutral-600 hover:before:bg-neutral-600 dark:before:bg-neutral-800 dark:hover:before:bg-neutral-800 text-neutral-50 dark:text-neutral-300',
|
||||||
|
icon: {
|
||||||
|
active: 'text-neutral-50 dark:text-neutral-300',
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
3
app.vue
3
app.vue
@@ -3,5 +3,8 @@
|
|||||||
<NuxtLayout>
|
<NuxtLayout>
|
||||||
<NuxtPage/>
|
<NuxtPage/>
|
||||||
</NuxtLayout>
|
</NuxtLayout>
|
||||||
|
|
||||||
|
<UModals/>
|
||||||
|
<UNotifications />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
3
assets/background-pattern-dark.svg
Normal file
3
assets/background-pattern-dark.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 32 KiB |
3
assets/background-pattern.svg
Normal file
3
assets/background-pattern.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 32 KiB |
153
components/ModalAuthentication.vue
Normal file
153
components/ModalAuthentication.vue
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {Label, PinInputInput, PinInputRoot} from 'radix-vue'
|
||||||
|
|
||||||
|
const toast = useToast()
|
||||||
|
|
||||||
|
const sms_triggered = ref(false)
|
||||||
|
const sms_sending = ref(false)
|
||||||
|
const sms_counting_down = ref(0)
|
||||||
|
const final_loading = ref(false)
|
||||||
|
const handleComplete = (e: string[]) => toast.add({title: `提交验证码 ${e.join('')}`, color: 'indigo', icon: 'i-tabler-circle-check'})
|
||||||
|
|
||||||
|
const items = [
|
||||||
|
{
|
||||||
|
key: 'sms',
|
||||||
|
label: '短信登录',
|
||||||
|
icon: 'i-tabler-message-2',
|
||||||
|
description: '使用短信验证码登录,未注册的账号将自动注册'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'account',
|
||||||
|
label: '密码登录',
|
||||||
|
icon: 'i-tabler-key',
|
||||||
|
description: '使用已有账号和密码登录'
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const accountForm = reactive({username: '', password: ''})
|
||||||
|
const smsForm = reactive({mobile: '', sms_code: []})
|
||||||
|
|
||||||
|
function onSubmit(form: any) {
|
||||||
|
console.log('Submitted form:', form)
|
||||||
|
}
|
||||||
|
|
||||||
|
const obtainSmsCode = () => {
|
||||||
|
smsForm.sms_code = []
|
||||||
|
sms_sending.value = true
|
||||||
|
setTimeout(() => {
|
||||||
|
sms_triggered.value = true
|
||||||
|
sms_sending.value = false
|
||||||
|
sms_counting_down.value = 15 // TODO: modify this to change the countdown time
|
||||||
|
toast.add({title: '短信验证码已发送', color: 'indigo', icon: 'i-tabler-circle-check'})
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
sms_counting_down.value--
|
||||||
|
if (sms_counting_down.value <= 0) {
|
||||||
|
clearInterval(interval)
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
}, 2000)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UModal>
|
||||||
|
<UCard>
|
||||||
|
|
||||||
|
<UTabs :items="items" class="w-full">
|
||||||
|
<template #default="{ item, index, selected }">
|
||||||
|
<div class="flex items-center gap-2 relative truncate">
|
||||||
|
<UIcon :name="item.icon" class="w-4 h-4 flex-shrink-0"/>
|
||||||
|
<span class="truncate">{{ item.label }}</span>
|
||||||
|
<span v-if="selected" class="absolute -right-4 w-2 h-2 rounded-full bg-primary-500 dark:bg-primary-400"/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #item="{ item }">
|
||||||
|
<UCard @submit.prevent="() => onSubmit(item.key === 'account' ? accountForm : smsForm)">
|
||||||
|
<template #header>
|
||||||
|
<p class="text-base font-semibold leading-6 text-gray-900 dark:text-white">
|
||||||
|
{{ item.label }}
|
||||||
|
</p>
|
||||||
|
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
{{ item.description }}
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div v-if="item.key === 'account'" class="space-y-3">
|
||||||
|
<UFormGroup label="用户名" name="username" required>
|
||||||
|
<UInput v-model="accountForm.username" :disabled="final_loading" required/>
|
||||||
|
</UFormGroup>
|
||||||
|
<UFormGroup label="密码" name="password" required>
|
||||||
|
<UInput v-model="accountForm.password" :disabled="final_loading" type="password" required/>
|
||||||
|
</UFormGroup>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="item.key === 'sms'" class="space-y-3">
|
||||||
|
<UFormGroup label="手机号" name="mobile" required>
|
||||||
|
<UButtonGroup class="w-full">
|
||||||
|
<UInput v-model="smsForm.mobile" type="sms" class="w-full" required>
|
||||||
|
<template #leading>
|
||||||
|
<span class="text-gray-500 dark:text-gray-400 text-xs">+86</span>
|
||||||
|
</template>
|
||||||
|
</UInput>
|
||||||
|
<UButton :label="sms_counting_down ? `${sms_counting_down}秒后重发` : '获取验证码'"
|
||||||
|
@click="obtainSmsCode"
|
||||||
|
:loading="sms_sending" :disabled="!!sms_counting_down"
|
||||||
|
class="text-xs font-bold" color="gray"/>
|
||||||
|
</UButtonGroup>
|
||||||
|
</UFormGroup>
|
||||||
|
<Transition name="pin-root">
|
||||||
|
<div v-if="sms_triggered">
|
||||||
|
<Label for="pin-input" class="pin-label">验证码</Label>
|
||||||
|
<PinInputRoot
|
||||||
|
id="sms-input"
|
||||||
|
v-model="smsForm.sms_code"
|
||||||
|
:disabled="sms_sending"
|
||||||
|
placeholder="○"
|
||||||
|
class="w-full flex gap-2 justify-between items-center mt-1"
|
||||||
|
@complete="handleComplete"
|
||||||
|
type="number" otp required
|
||||||
|
>
|
||||||
|
<PinInputInput
|
||||||
|
v-for="(id, index) in 6"
|
||||||
|
:key="id"
|
||||||
|
:index="index"
|
||||||
|
class="pin-input"
|
||||||
|
/>
|
||||||
|
</PinInputRoot>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer v-if="item.key !== 'sms'">
|
||||||
|
<UButton type="submit" color="black" :disabled="final_loading">
|
||||||
|
登录
|
||||||
|
</UButton>
|
||||||
|
</template>
|
||||||
|
</UCard>
|
||||||
|
</template>
|
||||||
|
</UTabs>
|
||||||
|
|
||||||
|
</UCard>
|
||||||
|
</UModal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.pin-root-enter-active,
|
||||||
|
.pin-root-leave-active {
|
||||||
|
@apply transition duration-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pin-root-enter-from,
|
||||||
|
.pin-root-leave-to {
|
||||||
|
@apply opacity-0 -translate-y-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pin-input {
|
||||||
|
@apply w-full aspect-square rounded text-center shadow caret-transparent;
|
||||||
|
@apply outline-0 ring-indigo-500 focus:ring font-bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pin-label {
|
||||||
|
@apply block text-sm font-medium text-gray-700 dark:text-gray-200 after:content-['*'] after:ms-0.5 after:text-red-500 dark:after:text-red-400;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,13 +1,96 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import ModalAuthentication from "~/components/ModalAuthentication.vue";
|
||||||
|
|
||||||
|
const colorMode = useColorMode()
|
||||||
|
const dayjs = useDayjs()
|
||||||
|
const modal = useModal()
|
||||||
|
|
||||||
|
const isDark = computed({
|
||||||
|
get() {
|
||||||
|
return colorMode.value === 'dark'
|
||||||
|
},
|
||||||
|
set() {
|
||||||
|
colorMode.preference = colorMode.value === 'dark' ? 'light' : 'dark'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const links = [
|
||||||
|
{
|
||||||
|
label: '绘画',
|
||||||
|
icon: 'i-tabler-brush',
|
||||||
|
to: '/aigc/drawing'
|
||||||
|
}, {
|
||||||
|
label: '聊天',
|
||||||
|
icon: 'i-tabler-message-2',
|
||||||
|
to: '/aigc/chat'
|
||||||
|
}, {
|
||||||
|
label: 'PPT',
|
||||||
|
icon: 'i-tabler-file-type-ppt',
|
||||||
|
to: '/aigc/ppt-course-gen'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const open_login_modal = () => {
|
||||||
|
modal.open(ModalAuthentication, {})
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="w-full h-screen">
|
||||||
|
<header>
|
||||||
|
<h1 class="inline-flex flex-col">
|
||||||
|
<span class="text-lg text-neutral-600 dark:text-neutral-300 font-bold">眩生花 AI 助手</span>
|
||||||
|
<span class="text-xs text-neutral-600 dark:text-neutral-300">这里可以有一个副标题</span>
|
||||||
|
</h1>
|
||||||
|
<div class="hidden md:block">
|
||||||
|
<UHorizontalNavigation :links="links" class="select-none"/>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row items-center gap-4">
|
||||||
|
<ClientOnly>
|
||||||
|
<UButton
|
||||||
|
:icon="isDark ? 'i-line-md-sunny-outline-to-moon-alt-loop-transition' : 'i-line-md-moon-alt-to-sunny-outline-loop-transition'"
|
||||||
|
color="gray"
|
||||||
|
variant="ghost"
|
||||||
|
aria-label="Theme"
|
||||||
|
@click="isDark = !isDark"
|
||||||
|
/>
|
||||||
|
<UButton label="登录或注册" size="xs" class="font-bold" color="indigo" @click="open_login_modal"/>
|
||||||
|
</ClientOnly>
|
||||||
|
<UAvatar :src="void 0" icon="i-tabler-user" size="md"/>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
<slot/>
|
<slot/>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<p class="text-sm text-neutral-500">© {{ dayjs().year() }} 重庆眩生花科技有限公司</p>
|
||||||
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style>
|
||||||
|
body {
|
||||||
|
@apply bg-neutral-50 dark:bg-neutral-950 bg-fixed;
|
||||||
|
@apply bg-[url('~/assets/background-pattern.svg')] dark:bg-[url('~/assets/background-pattern-dark.svg')];
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
header {
|
||||||
|
@apply fixed inset-x-0 h-16 bg-white border-b;
|
||||||
|
@apply dark:bg-neutral-900 dark:border-neutral-800;
|
||||||
|
@apply flex flex-row items-center justify-between px-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
@apply min-h-full pt-16;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
@apply h-16 bg-white border-t;
|
||||||
|
@apply dark:bg-neutral-900 dark:border-neutral-800;
|
||||||
|
@apply flex flex-row items-center justify-between px-4;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -3,9 +3,22 @@ export default defineNuxtConfig({
|
|||||||
devtools: {enabled: true},
|
devtools: {enabled: true},
|
||||||
modules: [
|
modules: [
|
||||||
'@nuxt/ui',
|
'@nuxt/ui',
|
||||||
'radix-vue/nuxt'
|
'radix-vue/nuxt',
|
||||||
|
'dayjs-nuxt',
|
||||||
|
"@pinia/nuxt",
|
||||||
|
"@pinia-plugin-persistedstate/nuxt",
|
||||||
|
"@vite-pwa/nuxt"
|
||||||
],
|
],
|
||||||
|
ui: {
|
||||||
|
icons: ['tabler', 'solar', 'line-md', 'svg-spinners']
|
||||||
|
},
|
||||||
colorMode: {
|
colorMode: {
|
||||||
preference: 'dark'
|
preference: 'dark'
|
||||||
|
},
|
||||||
|
dayjs: {
|
||||||
|
locales: ['zh', 'en'],
|
||||||
|
plugins: ['relativeTime', 'utc', 'timezone'],
|
||||||
|
defaultLocale: 'zh',
|
||||||
|
defaultTimezone: 'Asia/Shanghai',
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
10
package.json
10
package.json
@@ -10,10 +10,20 @@
|
|||||||
"postinstall": "nuxt prepare"
|
"postinstall": "nuxt prepare"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@iconify-json/line-md": "^1.1.36",
|
||||||
|
"@iconify-json/solar": "^1.1.9",
|
||||||
|
"@iconify-json/svg-spinners": "^1.1.2",
|
||||||
|
"@iconify-json/tabler": "^1.1.105",
|
||||||
"@nuxt/ui": "^2.14.1",
|
"@nuxt/ui": "^2.14.1",
|
||||||
"nuxt": "^3.10.3",
|
"nuxt": "^3.10.3",
|
||||||
"radix-vue": "^1.4.9",
|
"radix-vue": "^1.4.9",
|
||||||
"vue": "^3.4.19",
|
"vue": "^3.4.19",
|
||||||
"vue-router": "^4.3.0"
|
"vue-router": "^4.3.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@pinia-plugin-persistedstate/nuxt": "^1.2.0",
|
||||||
|
"@pinia/nuxt": "^0.5.1",
|
||||||
|
"@vite-pwa/nuxt": "^0.5.0",
|
||||||
|
"dayjs-nuxt": "^2.1.9"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
13
pages/aigc/chat.vue
Normal file
13
pages/aigc/chat.vue
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
useHead({
|
||||||
|
title: '聊天 | XSH AI'
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
ChatGPT
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
15
pages/aigc/drawing.vue
Normal file
15
pages/aigc/drawing.vue
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
useHead({
|
||||||
|
title: '绘画 | XSH AI'
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<h1>drawing page</h1>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
13
pages/aigc/ppt-course-gen.vue
Normal file
13
pages/aigc/ppt-course-gen.vue
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
useHead({
|
||||||
|
title: 'PPT 生成视频 | XSH AI'
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
PPT Course
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user