update
|
@ -0,0 +1,8 @@
|
||||||
|
node_modules
|
||||||
|
*.log
|
||||||
|
dist
|
||||||
|
.output
|
||||||
|
.nuxt
|
||||||
|
.env
|
||||||
|
.idea/
|
||||||
|
public/assets/fonts
|
|
@ -0,0 +1,4 @@
|
||||||
|
shamefully-hoist=true
|
||||||
|
strict-peer-dependencies=false
|
||||||
|
shell-emulator=true
|
||||||
|
auto-install-peers=false
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"installDependencies": true,
|
||||||
|
"startCommand": "npm run dev"
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"antfu.iconify",
|
||||||
|
"antfu.unocss",
|
||||||
|
"antfu.goto-alias",
|
||||||
|
"csstools.postcss",
|
||||||
|
"dbaeumer.vscode-eslint",
|
||||||
|
"vue.volar"
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
{
|
||||||
|
"files.associations": {
|
||||||
|
"*.css": "postcss"
|
||||||
|
},
|
||||||
|
|
||||||
|
// Disable the default formatter, use eslint instead
|
||||||
|
"prettier.enable": false,
|
||||||
|
"editor.formatOnSave": false,
|
||||||
|
|
||||||
|
// Auto fix
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.fixAll.eslint": "explicit",
|
||||||
|
"source.organizeImports": "never"
|
||||||
|
},
|
||||||
|
|
||||||
|
// Silent the stylistic rules in you IDE, but still auto fix them
|
||||||
|
"eslint.rules.customizations": [
|
||||||
|
{ "rule": "style/*", "severity": "off" },
|
||||||
|
{ "rule": "*-indent", "severity": "off" },
|
||||||
|
{ "rule": "*-spacing", "severity": "off" },
|
||||||
|
{ "rule": "*-spaces", "severity": "off" },
|
||||||
|
{ "rule": "*-order", "severity": "off" },
|
||||||
|
{ "rule": "*-dangle", "severity": "off" },
|
||||||
|
{ "rule": "*-newline", "severity": "off" },
|
||||||
|
{ "rule": "*quotes", "severity": "off" },
|
||||||
|
{ "rule": "*semi", "severity": "off" }
|
||||||
|
],
|
||||||
|
|
||||||
|
// Enable eslint for all supported languages
|
||||||
|
"eslint.validate": [
|
||||||
|
"javascript",
|
||||||
|
"javascriptreact",
|
||||||
|
"typescript",
|
||||||
|
"typescriptreact",
|
||||||
|
"vue",
|
||||||
|
"html",
|
||||||
|
"markdown",
|
||||||
|
"json",
|
||||||
|
"jsonc",
|
||||||
|
"yaml"
|
||||||
|
],
|
||||||
|
"interline-translate.knownPopularWordCount": 6000,
|
||||||
|
"iconify.annotations": true,
|
||||||
|
"iconify.inplace": true
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2021-PRESENT Anthony Fu<https://github.com/antfu>
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
|
@ -0,0 +1,80 @@
|
||||||
|
<p align="center">
|
||||||
|
<img src="https://user-images.githubusercontent.com/11247099/140462375-7b7ac4db-35b7-453c-8a05-13d8d20282c4.png" width="600"/>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2 align="center">
|
||||||
|
<a href="https://github.com/antfu/vitesse">Vitesse</a> for Nuxt 3
|
||||||
|
</h2><br>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<br>
|
||||||
|
<a href="https://vitesse-nuxt3.netlify.app/">🖥 Online Preview</a>
|
||||||
|
<br><br>
|
||||||
|
<a href="https://stackblitz.com/github/antfu/vitesse-nuxt"><img src="https://developer.stackblitz.com/img/open_in_stackblitz.svg" alt=""></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- 💚 [Nuxt 3](https://nuxt.com/) - SSR, ESR, File-based routing, components auto importing, modules, etc.
|
||||||
|
|
||||||
|
- ⚡️ Vite - Instant HMR.
|
||||||
|
|
||||||
|
- 🎨 [UnoCSS](https://github.com/unocss/unocss) - The instant on-demand atomic CSS engine.
|
||||||
|
|
||||||
|
- 😃 Use icons from any icon sets in Pure CSS, powered by [UnoCSS](https://github.com/unocss/unocss).
|
||||||
|
|
||||||
|
- 🔥 The `<script setup>` syntax.
|
||||||
|
|
||||||
|
- 🍍 [State Management via Pinia](https://github.com/vuejs/pinia), see [./app/composables/user.ts](./app/composables/user.ts).
|
||||||
|
|
||||||
|
- 📑 [Layout system](./app/layouts).
|
||||||
|
|
||||||
|
- 📥 APIs auto importing - for Composition API, VueUse and custom composables.
|
||||||
|
|
||||||
|
- 🏎 Zero-config cloud functions and deploy.
|
||||||
|
|
||||||
|
- 🦾 TypeScript, of course.
|
||||||
|
|
||||||
|
- 📲 [PWA](https://github.com/vite-pwa/nuxt) with offline support and auto-update behavior.
|
||||||
|
|
||||||
|
## Plugins
|
||||||
|
|
||||||
|
### Nuxt Modules
|
||||||
|
|
||||||
|
- [VueUse](https://github.com/vueuse/vueuse) - collection of useful composition APIs.
|
||||||
|
- [ColorMode](https://github.com/nuxt-modules/color-mode) - dark and Light mode with auto detection made easy with Nuxt.
|
||||||
|
- [UnoCSS](https://github.com/unocss/unocss) - the instant on-demand atomic CSS engine.
|
||||||
|
- [Pinia](https://github.com/vuejs/pinia) - intuitive, type safe, light and flexible Store for Vue.
|
||||||
|
- [VitePWA](https://github.com/vite-pwa/nuxt) - zero-config PWA Plugin for Nuxt 3.
|
||||||
|
- [DevTools](https://github.com/nuxt/devtools) - unleash Nuxt Developer Experience.
|
||||||
|
|
||||||
|
## IDE
|
||||||
|
|
||||||
|
We recommend using [VS Code](https://code.visualstudio.com/) with [Volar](https://github.com/johnsoncodehk/volar) to get the best experience (You might want to disable [Vetur](https://vuejs.github.io/vetur/) if you have it).
|
||||||
|
|
||||||
|
## Variations
|
||||||
|
|
||||||
|
- [vitesse](https://github.com/antfu/vitesse) - Opinionated Vite Starter Template
|
||||||
|
- [vitesse-lite](https://github.com/antfu/vitesse-lite) - Lightweight version of Vitesse
|
||||||
|
- [vitesse-nuxt-bridge](https://github.com/antfu/vitesse-nuxt-bridge) - Vitesse for Nuxt 2 with Bridge
|
||||||
|
- [vitesse-webext](https://github.com/antfu/vitesse-webext) - WebExtension Vite starter template
|
||||||
|
|
||||||
|
## Try it now!
|
||||||
|
|
||||||
|
### Online
|
||||||
|
|
||||||
|
<a href="https://stackblitz.com/github/antfu/vitesse-nuxt"><img src="https://developer.stackblitz.com/img/open_in_stackblitz.svg" alt=""></a>
|
||||||
|
|
||||||
|
### GitHub Template
|
||||||
|
|
||||||
|
[Create a repo from this template on GitHub](https://github.com/antfu/vitesse-nuxt/generate).
|
||||||
|
|
||||||
|
### Clone to local
|
||||||
|
|
||||||
|
If you prefer to do it manually with the cleaner git history
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx degit antfu/vitesse-nuxt my-nuxt-app
|
||||||
|
cd my-nuxt-app
|
||||||
|
pnpm i # If you don't have pnpm installed, run: npm install -g pnpm
|
||||||
|
```
|
|
@ -0,0 +1,21 @@
|
||||||
|
// api/common.ts
|
||||||
|
import request from '~/utils/request'
|
||||||
|
import type { ApiResponse, PaginationParams, PaginationResponse } from '~/types/api'
|
||||||
|
|
||||||
|
export const commonApi = {
|
||||||
|
// 上传文件
|
||||||
|
uploadFile(file: File) {
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('file', file)
|
||||||
|
return request.post<ApiResponse<{ url: string }>>('/upload', formData, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取配置信息
|
||||||
|
getConfig() {
|
||||||
|
return request.get<ApiResponse<Record<string, any>>>('/config')
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
// api/index.ts
|
||||||
|
export * from './user'
|
||||||
|
export * from './common'
|
||||||
|
// 导出其他 API 模块
|
|
@ -0,0 +1,36 @@
|
||||||
|
// api/user.ts
|
||||||
|
import request from '~/utils/request'
|
||||||
|
|
||||||
|
export interface LoginParams {
|
||||||
|
username: string
|
||||||
|
password: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserInfo {
|
||||||
|
id: number
|
||||||
|
username: string
|
||||||
|
email: string
|
||||||
|
avatar?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const userApi = {
|
||||||
|
// 登录
|
||||||
|
login(data: LoginParams) {
|
||||||
|
return request.post<{ token: string }>('/auth/login', data, { loading: true })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取用户信息
|
||||||
|
getUserInfo() {
|
||||||
|
return request.get<UserInfo>('/user/info')
|
||||||
|
},
|
||||||
|
|
||||||
|
// 更新用户信息
|
||||||
|
updateUserInfo(data: Partial<UserInfo>) {
|
||||||
|
return request.put<UserInfo>('/user/info', data, { loading: true })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取用户列表
|
||||||
|
getUserList(params: any) {
|
||||||
|
return request.getPageList<UserInfo[]>('/user/list', params)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { appName } from '~/constants'
|
||||||
|
|
||||||
|
import LoginModal from '~/components/LoginModal.vue'
|
||||||
|
import { useModalStore } from '~/stores/modal'
|
||||||
|
const loginModalRef = ref<InstanceType<typeof LoginModal> | null>(null)
|
||||||
|
const modalStore = useModalStore()
|
||||||
|
|
||||||
|
// 监听 ref 变化
|
||||||
|
watch(loginModalRef, (newRef) => {
|
||||||
|
if (newRef) {
|
||||||
|
modalStore.setLoginModal(newRef)
|
||||||
|
}
|
||||||
|
}, { immediate: true })
|
||||||
|
|
||||||
|
useHead({
|
||||||
|
title: appName,
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<VitePwaManifest />
|
||||||
|
<NuxtLayout>
|
||||||
|
<GlobalConfig>
|
||||||
|
<NuxtPage />
|
||||||
|
</GlobalConfig>
|
||||||
|
</NuxtLayout>
|
||||||
|
|
||||||
|
<LoginModal ref="loginModalRef" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
html,
|
||||||
|
body,
|
||||||
|
#__nuxt {
|
||||||
|
height: 100vh;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark {
|
||||||
|
background: #222;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
</style>
|
|
@ -0,0 +1,21 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
const color = useColorMode()
|
||||||
|
|
||||||
|
useHead({
|
||||||
|
meta: [{
|
||||||
|
id: 'theme-color',
|
||||||
|
name: 'theme-color',
|
||||||
|
content: () => color.value === 'dark' ? '#222222' : '#ffffff',
|
||||||
|
}],
|
||||||
|
})
|
||||||
|
|
||||||
|
function toggleDark() {
|
||||||
|
color.preference = color.value === 'dark' ? 'light' : 'dark'
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<button class="!outline-none" @click="toggleDark">
|
||||||
|
<div class="i-carbon-sun dark:i-carbon-moon" />
|
||||||
|
</button>
|
||||||
|
</template>
|
|
@ -0,0 +1,7 @@
|
||||||
|
<template>
|
||||||
|
<div text="xl gray4" m-5 flex="~ gap3" justify-center>
|
||||||
|
<!-- <NuxtLink i-carbon-campsite to="/" />
|
||||||
|
<a i-carbon-logo-github href="https://github.com/antfu/vitesse-nuxt3" target="_blank" /> -->
|
||||||
|
<DarkToggle />
|
||||||
|
</div>
|
||||||
|
</template>
|
|
@ -0,0 +1,18 @@
|
||||||
|
<template>
|
||||||
|
<n-config-provider :theme="theme" :theme-overrides="themeOverrides">
|
||||||
|
<n-message-provider>
|
||||||
|
<slot></slot>
|
||||||
|
</n-message-provider>
|
||||||
|
</n-config-provider>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { darkTheme, NConfigProvider, NMessageProvider } from 'naive-ui';
|
||||||
|
const theme = darkTheme
|
||||||
|
// 可以根据需要配置主题
|
||||||
|
const themeOverrides = {
|
||||||
|
common: {
|
||||||
|
primaryColor: '#18a058'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -0,0 +1,125 @@
|
||||||
|
<!-- components/LoginModal.vue -->
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import {
|
||||||
|
NModal,
|
||||||
|
NCard,
|
||||||
|
NForm,
|
||||||
|
NFormItem,
|
||||||
|
NInput,
|
||||||
|
NButton,
|
||||||
|
createDiscreteApi
|
||||||
|
} from 'naive-ui'
|
||||||
|
|
||||||
|
// 使用 createDiscreteApi 替代 useMessage
|
||||||
|
const { message } = createDiscreteApi(['message'])
|
||||||
|
const userStore = useUserStore()
|
||||||
|
const visible = ref(false)
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
|
||||||
|
const formRef = ref(null)
|
||||||
|
const formModel = ref({
|
||||||
|
username: '',
|
||||||
|
password: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const rules = {
|
||||||
|
username: {
|
||||||
|
required: true,
|
||||||
|
message: '请输入用户名',
|
||||||
|
trigger: 'blur'
|
||||||
|
},
|
||||||
|
password: {
|
||||||
|
required: true,
|
||||||
|
message: '请输入密码',
|
||||||
|
trigger: 'blur'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打开登录框
|
||||||
|
function showModal() {
|
||||||
|
visible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭登录框
|
||||||
|
function hideModal() {
|
||||||
|
visible.value = false
|
||||||
|
formModel.value = {
|
||||||
|
username: '',
|
||||||
|
password: ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理登录
|
||||||
|
async function handleLogin() {
|
||||||
|
if (!formRef.value) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
await formRef.value.validate()
|
||||||
|
|
||||||
|
// 这里替换为你的实际登录 API 调用
|
||||||
|
// const res = await login(formModel.value)
|
||||||
|
|
||||||
|
// 模拟登录成功
|
||||||
|
userStore.login('fake-token')
|
||||||
|
message.success('登录成功')
|
||||||
|
hideModal()
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 暴露方法给父组件
|
||||||
|
defineExpose({
|
||||||
|
showModal
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<NModal
|
||||||
|
v-model:show="visible"
|
||||||
|
preset="card"
|
||||||
|
style="width: 400px"
|
||||||
|
:maskClosable="false"
|
||||||
|
title="登录"
|
||||||
|
>
|
||||||
|
<NForm
|
||||||
|
ref="formRef"
|
||||||
|
:model="formModel"
|
||||||
|
:rules="rules"
|
||||||
|
>
|
||||||
|
<NFormItem label="用户名" path="username">
|
||||||
|
<NInput
|
||||||
|
v-model:value="formModel.username"
|
||||||
|
placeholder="请输入用户名"
|
||||||
|
/>
|
||||||
|
</NFormItem>
|
||||||
|
<NFormItem label="密码" path="password">
|
||||||
|
<NInput
|
||||||
|
v-model:value="formModel.password"
|
||||||
|
type="password"
|
||||||
|
placeholder="请输入密码"
|
||||||
|
@keyup.enter="handleLogin"
|
||||||
|
/>
|
||||||
|
</NFormItem>
|
||||||
|
</NForm>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="flex justify-end gap-2">
|
||||||
|
<NButton @click="hideModal">取消</NButton>
|
||||||
|
<NButton
|
||||||
|
type="primary"
|
||||||
|
:loading="loading"
|
||||||
|
@click="handleLogin"
|
||||||
|
>
|
||||||
|
登录
|
||||||
|
</NButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</NModal>
|
||||||
|
</template>
|
|
@ -0,0 +1,16 @@
|
||||||
|
export function useCount() {
|
||||||
|
const count = useState('count', () => Math.round(Math.random() * 20))
|
||||||
|
|
||||||
|
function inc() {
|
||||||
|
count.value += 1
|
||||||
|
}
|
||||||
|
function dec() {
|
||||||
|
count.value -= 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
count,
|
||||||
|
inc,
|
||||||
|
dec,
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,79 @@
|
||||||
|
import type { ModuleOptions } from '@vite-pwa/nuxt'
|
||||||
|
import process from 'node:process'
|
||||||
|
import { appDescription, appName } from '../constants/index'
|
||||||
|
|
||||||
|
const scope = '/'
|
||||||
|
|
||||||
|
export const pwa: ModuleOptions = {
|
||||||
|
registerType: 'autoUpdate',
|
||||||
|
scope,
|
||||||
|
base: scope,
|
||||||
|
manifest: {
|
||||||
|
id: scope,
|
||||||
|
scope,
|
||||||
|
name: appName,
|
||||||
|
short_name: appName,
|
||||||
|
description: appDescription,
|
||||||
|
theme_color: '#ffffff',
|
||||||
|
icons: [
|
||||||
|
{
|
||||||
|
src: 'pwa-192x192.png',
|
||||||
|
sizes: '192x192',
|
||||||
|
type: 'image/png',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: 'pwa-512x512.png',
|
||||||
|
sizes: '512x512',
|
||||||
|
type: 'image/png',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: 'maskable-icon.png',
|
||||||
|
sizes: '512x512',
|
||||||
|
type: 'image/png',
|
||||||
|
purpose: 'any maskable',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
workbox: {
|
||||||
|
globPatterns: ['**/*.{js,css,html,txt,png,ico,svg}'],
|
||||||
|
navigateFallbackDenylist: [/^\/api\//],
|
||||||
|
navigateFallback: '/',
|
||||||
|
cleanupOutdatedCaches: true,
|
||||||
|
runtimeCaching: [
|
||||||
|
{
|
||||||
|
urlPattern: /^https:\/\/fonts.googleapis.com\/.*/i,
|
||||||
|
handler: 'CacheFirst',
|
||||||
|
options: {
|
||||||
|
cacheName: 'google-fonts-cache',
|
||||||
|
expiration: {
|
||||||
|
maxEntries: 10,
|
||||||
|
maxAgeSeconds: 60 * 60 * 24 * 365, // <== 365 days
|
||||||
|
},
|
||||||
|
cacheableResponse: {
|
||||||
|
statuses: [0, 200],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
urlPattern: /^https:\/\/fonts.gstatic.com\/.*/i,
|
||||||
|
handler: 'CacheFirst',
|
||||||
|
options: {
|
||||||
|
cacheName: 'gstatic-fonts-cache',
|
||||||
|
expiration: {
|
||||||
|
maxEntries: 10,
|
||||||
|
maxAgeSeconds: 60 * 60 * 24 * 365, // <== 365 days
|
||||||
|
},
|
||||||
|
cacheableResponse: {
|
||||||
|
statuses: [0, 200],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
registerWebManifestInRouteRules: true,
|
||||||
|
writePlugin: true,
|
||||||
|
devOptions: {
|
||||||
|
enabled: process.env.VITE_PLUGIN_PWA === 'true',
|
||||||
|
navigateFallback: scope,
|
||||||
|
},
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
export const appName = '魔创未来'
|
||||||
|
export const appDescription = '魔创未来'
|
|
@ -0,0 +1,15 @@
|
||||||
|
## Layouts
|
||||||
|
|
||||||
|
Vue components in this dir are used as layouts.
|
||||||
|
|
||||||
|
By default, `default.vue` will be used unless an alternative is specified in the route meta.
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<script setup lang="ts">
|
||||||
|
definePageMeta({
|
||||||
|
layout: 'home',
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
Learn more on https://nuxt.com/docs/guide/directory-structure/layouts
|
|
@ -0,0 +1,245 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {
|
||||||
|
Bell, Binary, Code2, Crown, GraduationCap, Image, LayoutGrid,
|
||||||
|
Lightbulb, Maximize, Monitor, Plus, Search, User, Workflow
|
||||||
|
} from 'lucide-vue-next';
|
||||||
|
import {
|
||||||
|
NAvatar,
|
||||||
|
NBadge, NButton, NDropdown, NInput, NSpace
|
||||||
|
} from 'naive-ui';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const userStore = useUserStore()
|
||||||
|
userStore.checkLoginStatus()
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 路径到图标的映射
|
||||||
|
const iconMap: any = {
|
||||||
|
'/model-square': LayoutGrid,
|
||||||
|
'/works-inspire': Lightbulb,
|
||||||
|
'/workspace': Lightbulb,
|
||||||
|
'/web-ui': Image,
|
||||||
|
'/comfy-ui': Workflow,
|
||||||
|
'/training-lora': Binary,
|
||||||
|
'/high-availability': Maximize,
|
||||||
|
'/api-platform': Code2,
|
||||||
|
'/creator-center': Code2,
|
||||||
|
'/personal-center': User,
|
||||||
|
'/member-center': Crown
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
import { useMenuStore } from '~/stores/menu';
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
const menuStore = useMenuStore()
|
||||||
|
|
||||||
|
// 监听路由变化
|
||||||
|
watch(
|
||||||
|
() => route.path,
|
||||||
|
(path) => {
|
||||||
|
menuStore.setActiveMenu(path)
|
||||||
|
},
|
||||||
|
{ immediate: true } // 这样一进入页面就会执行一次
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 更新菜单项中的图标
|
||||||
|
menuStore.menuItems = menuStore.menuItems.map(item => ({
|
||||||
|
...item,
|
||||||
|
LucideIcon: iconMap[item.path] // 添加 Lucide 图标组件
|
||||||
|
}))
|
||||||
|
|
||||||
|
// 消息通知下拉选项
|
||||||
|
const notificationOptions = [
|
||||||
|
{
|
||||||
|
label: '系统通知',
|
||||||
|
key: 'system'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '互动消息',
|
||||||
|
key: 'interaction'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
// 用户下拉选项
|
||||||
|
const userOptions = [
|
||||||
|
{
|
||||||
|
label: '个人中心',
|
||||||
|
key: 'profile'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '创作中心',
|
||||||
|
key: 'creator'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'divider',
|
||||||
|
key: 'd1'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '退出登录',
|
||||||
|
key: 'logout'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex h-screen flex-col bg-white dark:bg-dark-800">
|
||||||
|
<!-- Header -->
|
||||||
|
<header class="sticky top-0 z-50 flex h-14 items-center justify-between border-b border-gray-100 bg-white/80 px-6 backdrop-blur dark:border-dark-700 dark:bg-dark-800/80">
|
||||||
|
<div class="flex items-center gap-6">
|
||||||
|
<!-- Logo 区域调整 -->
|
||||||
|
<div class="flex min-w-[220px] items-center gap-3 pr-4">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<!-- <img src="/vite.png" alt="Logo" class="h-9 w-9" /> -->
|
||||||
|
<span class="text-xl font-semibold tracking-tight">魔创未来</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Search -->
|
||||||
|
<NInput
|
||||||
|
round
|
||||||
|
clearable
|
||||||
|
placeholder="搜索模型/图片/创作者寻找灵感"
|
||||||
|
class="w-[480px]"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<Search class="h-4 w-4 text-gray-400" />
|
||||||
|
</template>
|
||||||
|
</NInput>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Right Actions -->
|
||||||
|
<NSpace align="center" :size="24">
|
||||||
|
<!-- PC Client -->
|
||||||
|
<NButton text class="flex items-center gap-1.5">
|
||||||
|
<Monitor class="h-4 w-4" />
|
||||||
|
<span>PC客户端</span>
|
||||||
|
</NButton>
|
||||||
|
|
||||||
|
<!-- Tutorials -->
|
||||||
|
<NButton text class="flex items-center gap-1.5">
|
||||||
|
<GraduationCap class="h-4 w-4" />
|
||||||
|
<span>教程专区</span>
|
||||||
|
</NButton>
|
||||||
|
|
||||||
|
<!-- VIP -->
|
||||||
|
<NuxtLink to="/member-center" target="_blank" class="inline-block">
|
||||||
|
<NButton text type="warning" class="flex items-center gap-1.5">
|
||||||
|
<Crown class="h-4 w-4" />
|
||||||
|
<span>会员中心</span>
|
||||||
|
</NButton>
|
||||||
|
</NuxtLink>
|
||||||
|
|
||||||
|
<!-- Create -->
|
||||||
|
<NButton type="primary" round class="flex items-center gap-1.5">
|
||||||
|
<Plus class="h-4 w-4" />
|
||||||
|
<span>发布</span>
|
||||||
|
</NButton>
|
||||||
|
|
||||||
|
<!-- Notifications -->
|
||||||
|
<NDropdown :options="notificationOptions" trigger="click">
|
||||||
|
<NBadge :value="5" :max="99" processing>
|
||||||
|
<NButton text circle>
|
||||||
|
<Bell class="h-5 w-5" />
|
||||||
|
</NButton>
|
||||||
|
</NBadge>
|
||||||
|
</NDropdown>
|
||||||
|
|
||||||
|
<!-- User -->
|
||||||
|
<NDropdown :options="userOptions" trigger="click">
|
||||||
|
<NAvatar
|
||||||
|
round
|
||||||
|
size="small"
|
||||||
|
src="https://07akioni.oss-cn-beijing.aliyuncs.com/07akioni.jpeg"
|
||||||
|
/>
|
||||||
|
</NDropdown>
|
||||||
|
</NSpace>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<div class="flex flex-1 overflow-hidden">
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<nav class="w-[240px] border-r border-gray-100 bg-gray-50/50 py-2 dark:border-dark-700 dark:bg-dark-800/50">
|
||||||
|
<div class="space-y-1 px-2">
|
||||||
|
<NuxtLink
|
||||||
|
v-for="item in menuStore.menuItems"
|
||||||
|
:key="item.path"
|
||||||
|
:to="item.path"
|
||||||
|
class="flex items-center gap-3 rounded-lg px-4 py-2.5 text-[15px] font-medium no-underline transition-colors"
|
||||||
|
:class="[
|
||||||
|
menuStore.activeMenu === item.path
|
||||||
|
? 'bg-blue-500/8 text-blue-600 dark:bg-blue-500/10 dark:text-blue-400'
|
||||||
|
: 'text-gray-600 hover:bg-gray-500/5 dark:text-gray-300 dark:hover:bg-dark-700/50'
|
||||||
|
]"
|
||||||
|
@click="menuStore.setActiveMenu(item.path)"
|
||||||
|
>
|
||||||
|
<!-- 只显示 Lucide 图标 -->
|
||||||
|
<component
|
||||||
|
:is="item.LucideIcon"
|
||||||
|
class="h-[18px] w-[18px]"
|
||||||
|
:class="menuStore.activeMenu === item.path
|
||||||
|
? 'text-blue-600 dark:text-blue-400'
|
||||||
|
: 'text-gray-500 dark:text-gray-400'"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<span>{{ item.label }}</span>
|
||||||
|
</NuxtLink>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- Page Content -->
|
||||||
|
<main class="flex-1 overflow-auto p-6">
|
||||||
|
<slot />
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- 登录框组件 -->
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--primary: rgb(59, 130, 246);
|
||||||
|
--primary-hover: rgb(37, 99, 235);
|
||||||
|
}
|
||||||
|
|
||||||
|
::selection {
|
||||||
|
background: var(--primary);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 自定义滚动条 */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: #e5e7eb;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #d1d5db;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark ::-webkit-scrollbar-thumb {
|
||||||
|
background: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark ::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #4b5563;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,232 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {
|
||||||
|
Bell, Binary, Code2, Crown, GraduationCap, Image, LayoutGrid,
|
||||||
|
Lightbulb, Maximize, Monitor, Plus, Search, User, Workflow
|
||||||
|
} from 'lucide-vue-next';
|
||||||
|
import {
|
||||||
|
NAvatar,
|
||||||
|
NBadge, NButton, NDropdown, NInput, NSpace
|
||||||
|
} from 'naive-ui';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const userStore = useUserStore()
|
||||||
|
userStore.checkLoginStatus()
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 路径到图标的映射
|
||||||
|
const iconMap: any = {
|
||||||
|
'/model-square': LayoutGrid,
|
||||||
|
'/works-inspire': Lightbulb,
|
||||||
|
'/workspace': Lightbulb,
|
||||||
|
'/web-ui': Image,
|
||||||
|
'/comfy-ui': Workflow,
|
||||||
|
'/training-lora': Binary,
|
||||||
|
'/high-availability': Maximize,
|
||||||
|
'/api-platform': Code2,
|
||||||
|
'/creator-center': Code2,
|
||||||
|
'/personal-center': User,
|
||||||
|
'/member-center': Crown
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
import { useMenuStore } from '~/stores/menu';
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
const menuStore = useMenuStore()
|
||||||
|
|
||||||
|
// 监听路由变化
|
||||||
|
watch(
|
||||||
|
() => route.path,
|
||||||
|
(path) => {
|
||||||
|
menuStore.setActiveMenu(path)
|
||||||
|
},
|
||||||
|
{ immediate: true } // 这样一进入页面就会执行一次
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 更新菜单项中的图标
|
||||||
|
menuStore.menuItems = menuStore.menuItems.map(item => ({
|
||||||
|
...item,
|
||||||
|
LucideIcon: iconMap[item.path] // 添加 Lucide 图标组件
|
||||||
|
}))
|
||||||
|
|
||||||
|
// 消息通知下拉选项
|
||||||
|
const notificationOptions = [
|
||||||
|
{
|
||||||
|
label: '系统通知',
|
||||||
|
key: 'system'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '互动消息',
|
||||||
|
key: 'interaction'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
const publisOptions = [
|
||||||
|
{
|
||||||
|
label: '模型',
|
||||||
|
key: 'model'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '图片',
|
||||||
|
key: 'picture'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label:'工作流',
|
||||||
|
key:'workFlow'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
// 用户下拉选项
|
||||||
|
const userOptions = [
|
||||||
|
{
|
||||||
|
label: '个人中心',
|
||||||
|
key: 'profile'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '创作中心',
|
||||||
|
key: 'creator'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'divider',
|
||||||
|
key: 'd1'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '退出登录',
|
||||||
|
key: 'logout'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex h-screen flex-col bg-white dark:bg-dark-800">
|
||||||
|
<!-- Header -->
|
||||||
|
<header class="sticky top-0 z-50 flex h-14 items-center justify-between border-b border-gray-100 bg-white/80 px-6 backdrop-blur dark:border-dark-700 dark:bg-dark-800/80">
|
||||||
|
<div class="flex items-center gap-6">
|
||||||
|
<!-- Logo 区域调整 -->
|
||||||
|
<div class="flex min-w-[220px] items-center gap-3 pr-4">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<img src="/vite.png" alt="Logo" class="h-9 w-9" />
|
||||||
|
<span class="text-xl font-semibold tracking-tight">LibLib AI</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Search -->
|
||||||
|
<NInput
|
||||||
|
round
|
||||||
|
clearable
|
||||||
|
placeholder="搜索模型/图片/创作者寻找灵感"
|
||||||
|
class="w-[480px]"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<Search class="h-4 w-4 text-gray-400" />
|
||||||
|
</template>
|
||||||
|
</NInput>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Right Actions -->
|
||||||
|
<NSpace align="center" :size="24">
|
||||||
|
<!-- PC Client -->
|
||||||
|
<NButton text class="flex items-center gap-1.5">
|
||||||
|
<Monitor class="h-4 w-4" />
|
||||||
|
<span>PC客户端</span>
|
||||||
|
</NButton>
|
||||||
|
|
||||||
|
<!-- Tutorials -->
|
||||||
|
<NButton text class="flex items-center gap-1.5">
|
||||||
|
<GraduationCap class="h-4 w-4" />
|
||||||
|
<span>教程专区</span>
|
||||||
|
</NButton>
|
||||||
|
|
||||||
|
<!-- VIP -->
|
||||||
|
<NuxtLink to="/member-center" target="_blank" class="inline-block">
|
||||||
|
<NButton text type="warning" class="flex items-center gap-1.5">
|
||||||
|
<Crown class="h-4 w-4" />
|
||||||
|
<span>会员中心</span>
|
||||||
|
</NButton>
|
||||||
|
</NuxtLink>
|
||||||
|
|
||||||
|
<!-- Create -->
|
||||||
|
<NDropdown :options="publisOptions" trigger="click">
|
||||||
|
<NButton class="flex items-center gap-1.5">
|
||||||
|
<Plus class="h-4 w-4" />
|
||||||
|
<span>发布</span>
|
||||||
|
</NButton>
|
||||||
|
</NDropdown>
|
||||||
|
<!-- Notifications -->
|
||||||
|
<NDropdown :options="notificationOptions" trigger="click">
|
||||||
|
<NBadge :value="5" :max="99" processing>
|
||||||
|
<NButton text circle>
|
||||||
|
<Bell class="h-5 w-5" />
|
||||||
|
</NButton>
|
||||||
|
</NBadge>
|
||||||
|
</NDropdown>
|
||||||
|
|
||||||
|
<!-- User -->
|
||||||
|
<NDropdown :options="userOptions" trigger="click">
|
||||||
|
<NAvatar
|
||||||
|
round
|
||||||
|
size="small"
|
||||||
|
src="https://07akioni.oss-cn-beijing.aliyuncs.com/07akioni.jpeg"
|
||||||
|
/>
|
||||||
|
</NDropdown>
|
||||||
|
</NSpace>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<div class="flex flex-1 overflow-hidden">
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Page Content -->
|
||||||
|
<main class="flex-1 overflow-auto p-6">
|
||||||
|
<slot />
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- 登录框组件 -->
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--primary: rgb(59, 130, 246);
|
||||||
|
--primary-hover: rgb(37, 99, 235);
|
||||||
|
}
|
||||||
|
|
||||||
|
::selection {
|
||||||
|
background: var(--primary);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 自定义滚动条 */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: #e5e7eb;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #d1d5db;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark ::-webkit-scrollbar-thumb {
|
||||||
|
background: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark ::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #4b5563;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,137 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {
|
||||||
|
NInput,
|
||||||
|
NButton,
|
||||||
|
NSpace,
|
||||||
|
NDropdown,
|
||||||
|
NAvatar,
|
||||||
|
NBadge
|
||||||
|
} from 'naive-ui'
|
||||||
|
import {
|
||||||
|
Search,
|
||||||
|
Monitor,
|
||||||
|
GraduationCap,
|
||||||
|
Crown,
|
||||||
|
Plus,
|
||||||
|
Bell,
|
||||||
|
LayoutGrid,
|
||||||
|
Lightbulb,
|
||||||
|
Image,
|
||||||
|
Workflow,
|
||||||
|
Binary,
|
||||||
|
Maximize,
|
||||||
|
Code2,
|
||||||
|
PenTool,
|
||||||
|
User,
|
||||||
|
CreditCard
|
||||||
|
} from 'lucide-vue-next'
|
||||||
|
|
||||||
|
// 路径到图标的映射
|
||||||
|
const iconMap: any = {
|
||||||
|
'/model-square': LayoutGrid,
|
||||||
|
'/works-inspire': Lightbulb,
|
||||||
|
'/workspace': Lightbulb,
|
||||||
|
'/web-ui': Image,
|
||||||
|
'/comfy-ui': Workflow,
|
||||||
|
'/training-lora': Binary,
|
||||||
|
'/high-availability': Maximize,
|
||||||
|
'/api-platform': Code2,
|
||||||
|
'/creator-center': Code2,
|
||||||
|
'/personal-center': User,
|
||||||
|
'/member-center': Crown
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
import { useMenuStore } from '~/stores/menu'
|
||||||
|
|
||||||
|
const menuStore = useMenuStore()
|
||||||
|
|
||||||
|
// 更新菜单项中的图标
|
||||||
|
menuStore.menuItems = menuStore.menuItems.map(item => ({
|
||||||
|
...item,
|
||||||
|
LucideIcon: iconMap[item.path] // 添加 Lucide 图标组件
|
||||||
|
}))
|
||||||
|
|
||||||
|
// 消息通知下拉选项
|
||||||
|
const notificationOptions = [
|
||||||
|
{
|
||||||
|
label: '系统通知',
|
||||||
|
key: 'system'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '互动消息',
|
||||||
|
key: 'interaction'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
// 用户下拉选项
|
||||||
|
const userOptions = [
|
||||||
|
{
|
||||||
|
label: '个人中心',
|
||||||
|
key: 'profile'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '创作中心',
|
||||||
|
key: 'creator'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'divider',
|
||||||
|
key: 'd1'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '退出登录',
|
||||||
|
key: 'logout'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex h-screen flex-col bg-white dark:bg-dark-800">
|
||||||
|
|
||||||
|
demo
|
||||||
|
<main class="flex-1 overflow-auto p-6">
|
||||||
|
<slot />
|
||||||
|
</main>
|
||||||
|
demo
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--primary: rgb(59, 130, 246);
|
||||||
|
--primary-hover: rgb(37, 99, 235);
|
||||||
|
}
|
||||||
|
|
||||||
|
::selection {
|
||||||
|
background: var(--primary);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 自定义滚动条 */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: #e5e7eb;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #d1d5db;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark ::-webkit-scrollbar-thumb {
|
||||||
|
background: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark ::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #4b5563;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,28 @@
|
||||||
|
// middleware/auth.ts
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 全局认证中间件
|
||||||
|
* 用于处理需要登录才能访问的路由
|
||||||
|
* 如果用户未登录,将显示登录框并重定向到模型广场
|
||||||
|
*
|
||||||
|
* @param to - 目标路由对象
|
||||||
|
* @returns void | NavigationResult - 如果需要重定向则返回导航结果
|
||||||
|
*/
|
||||||
|
export default defineNuxtRouteMiddleware((to) => {
|
||||||
|
const userStore = useUserStore()
|
||||||
|
const modalStore = useModalStore()
|
||||||
|
|
||||||
|
// 需要登录权限的路由列表
|
||||||
|
const authRoutes = [
|
||||||
|
'/high-availability',
|
||||||
|
// 可以继续添加其他需要登录的路由
|
||||||
|
]
|
||||||
|
|
||||||
|
// 如果是需要登录的路由,且用户未登录
|
||||||
|
if (authRoutes.includes(to.path) && !userStore.isLoggedIn) {
|
||||||
|
// 显示登录模态框
|
||||||
|
modalStore.showLoginModal()
|
||||||
|
// 重定向到模型广场页面
|
||||||
|
return navigateTo('/model-square')
|
||||||
|
}
|
||||||
|
})
|
|
@ -0,0 +1,156 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { NButton, NInput, NDataTable, useMessage } from 'naive-ui'
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import request from '~/utils/request'
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
layout: 'home',
|
||||||
|
})
|
||||||
|
|
||||||
|
const message = useMessage()
|
||||||
|
const online = useOnline()
|
||||||
|
|
||||||
|
// 定义数据接口
|
||||||
|
interface UserData {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
email: string
|
||||||
|
status: number
|
||||||
|
}
|
||||||
|
|
||||||
|
// 定义响应接口
|
||||||
|
interface ApiResponse<T> {
|
||||||
|
code: number
|
||||||
|
data: T
|
||||||
|
message: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 状态变量
|
||||||
|
const loading = ref(false)
|
||||||
|
const searchText = ref('')
|
||||||
|
const userData = ref<UserData[]>([])
|
||||||
|
|
||||||
|
// 表格列定义
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: 'ID',
|
||||||
|
key: 'id'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '姓名',
|
||||||
|
key: 'name'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '邮箱',
|
||||||
|
key: 'email'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '状态',
|
||||||
|
key: 'status',
|
||||||
|
render: (row: UserData) => {
|
||||||
|
return row.status === 1 ? '激活' : '禁用'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
// 获取用户列表
|
||||||
|
async function fetchUserList() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const response = await request.get<ApiResponse<UserData[]>>('/api/users', {
|
||||||
|
params: {
|
||||||
|
keyword: searchText.value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.code === 200) {
|
||||||
|
// userData.value = response.data
|
||||||
|
message.success('数据加载成功')
|
||||||
|
} else {
|
||||||
|
message.error(response.message || '获取数据失败')
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
message.error(error.message || '请求出错')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索用户
|
||||||
|
function handleSearch() {
|
||||||
|
fetchUserList()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加用户
|
||||||
|
async function handleAddUser() {
|
||||||
|
try {
|
||||||
|
const response = await request.post<ApiResponse<UserData>>('/api/users', {
|
||||||
|
name: '测试用户',
|
||||||
|
email: 'test@example.com',
|
||||||
|
status: 1
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.code === 200) {
|
||||||
|
message.success('添加成功')
|
||||||
|
fetchUserList() // 刷新列表
|
||||||
|
} else {
|
||||||
|
message.error(response.message || '添加失败')
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
message.error(error.message || '请求出错')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 组件挂载时获取数据
|
||||||
|
onMounted(() => {
|
||||||
|
fetchUserList()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="p-4">
|
||||||
|
<div class="mb-4 flex gap-4 items-center">
|
||||||
|
<n-input
|
||||||
|
v-model:value="searchText"
|
||||||
|
placeholder="请输入搜索关键词"
|
||||||
|
class="w-64"
|
||||||
|
@keyup.enter="handleSearch"
|
||||||
|
/>
|
||||||
|
<n-button type="primary" :loading="loading" @click="handleSearch">
|
||||||
|
搜索
|
||||||
|
</n-button>
|
||||||
|
<n-button type="info" @click="handleAddUser">
|
||||||
|
添加用户
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<n-data-table
|
||||||
|
:loading="loading"
|
||||||
|
:columns="columns"
|
||||||
|
:data="userData"
|
||||||
|
:bordered="false"
|
||||||
|
striped
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.p-4 {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
.mb-4 {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
.flex {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.gap-4 {
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
.items-center {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.w-64 {
|
||||||
|
width: 16rem;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,17 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
const router = useRouter()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<main p="x4 y10" text="center teal-700 dark:gray-200">
|
||||||
|
<div text-4xl>
|
||||||
|
<div i-carbon-warning inline-block />
|
||||||
|
</div>
|
||||||
|
<div>Not found</div>
|
||||||
|
<div>
|
||||||
|
<button text-sm btn m="3 t8" @click="router.back()">
|
||||||
|
Back
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</template>
|
|
@ -0,0 +1,42 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { NButton, NInput } from 'naive-ui'
|
||||||
|
// definePageMeta({
|
||||||
|
// layout: 'home',
|
||||||
|
// })
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
layout: 'default',
|
||||||
|
})
|
||||||
|
|
||||||
|
const online = useOnline()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<!-- <ClientOnly>
|
||||||
|
<Suspense>
|
||||||
|
<PageView v-if="online" />
|
||||||
|
<div v-else text-gray:80>
|
||||||
|
You're offline
|
||||||
|
</div>
|
||||||
|
<template #fallback>
|
||||||
|
<div italic op50>
|
||||||
|
<span animate-pulse>Loading...</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Suspense>
|
||||||
|
<template #fallback>
|
||||||
|
<div op50>
|
||||||
|
<span animate-pulse>...</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</ClientOnly> -->
|
||||||
|
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<n-button type="info">测试按钮-3-3</n-button>
|
||||||
|
<n-input placeholder="请输入" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
|
@ -0,0 +1,55 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useMessage } from 'naive-ui';
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
layout: 'default',
|
||||||
|
})
|
||||||
|
|
||||||
|
const message = useMessage()
|
||||||
|
|
||||||
|
// 定义数据接口
|
||||||
|
interface UserData {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
email: string
|
||||||
|
status: number
|
||||||
|
}
|
||||||
|
|
||||||
|
// 定义响应接口
|
||||||
|
interface ApiResponse<T> {
|
||||||
|
code: number
|
||||||
|
data: T
|
||||||
|
message: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 状态变量
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="p-4">
|
||||||
|
83475982345897234957420435365
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.p-4 {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
.mb-4 {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
.flex {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.gap-4 {
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
.items-center {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.w-64 {
|
||||||
|
width: 16rem;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,12 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
definePageMeta({
|
||||||
|
layout: 'default',
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="p-6">
|
||||||
|
<h1 class="text-2xl font-bold">模型广场</h1>
|
||||||
|
<!-- 这里添加模型广场的具体内容 -->
|
||||||
|
</div>
|
||||||
|
</template>
|
|
@ -0,0 +1,48 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
const route = useRoute<'publishDetails-id'>()
|
||||||
|
const user = useUserStore()
|
||||||
|
const name = route.params.id
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
user.setNewName(route.params.id as string)
|
||||||
|
})
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
layout: 'home',
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div i-twemoji:waving-hand inline-block animate-shake-x animate-duration-5000 text-4xl />
|
||||||
|
<h3 text-2xl font-500>
|
||||||
|
Hi,
|
||||||
|
</h3>
|
||||||
|
<div text-xl>
|
||||||
|
{{ name }}!
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-if="user.otherNames.length">
|
||||||
|
<div my-4 text-sm>
|
||||||
|
<span op-50>Also as known as:</span>
|
||||||
|
<ul>
|
||||||
|
<li v-for="otherName in user.otherNames" :key="otherName">
|
||||||
|
<router-link :to="`/hi/${otherName}`" replace>
|
||||||
|
{{ otherName }}
|
||||||
|
</router-link>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<NuxtLink
|
||||||
|
class="m-3 text-sm btn"
|
||||||
|
to="/"
|
||||||
|
>
|
||||||
|
Back
|
||||||
|
</NuxtLink>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
|
@ -0,0 +1,215 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {
|
||||||
|
NCard,
|
||||||
|
NTabs,
|
||||||
|
NTabPane,
|
||||||
|
NSelect,
|
||||||
|
NInput,
|
||||||
|
NInputNumber,
|
||||||
|
NButton,
|
||||||
|
NSpace,
|
||||||
|
NGrid,
|
||||||
|
NGridItem,
|
||||||
|
NImage,
|
||||||
|
NTag
|
||||||
|
} from 'naive-ui'
|
||||||
|
|
||||||
|
// 模型列表
|
||||||
|
const models = ref([
|
||||||
|
{ label: 'Stable Diffusion v1.5', value: 'sd-v1.5' },
|
||||||
|
{ label: 'Stable Diffusion v2.1', value: 'sd-v2.1' },
|
||||||
|
{ label: 'Stable Diffusion XL', value: 'sd-xl' },
|
||||||
|
])
|
||||||
|
|
||||||
|
// 采样器选项
|
||||||
|
const samplers = ref([
|
||||||
|
{ label: 'Euler a', value: 'euler-a' },
|
||||||
|
{ label: 'DPM++ 2M Karras', value: 'dpm-2m-karras' },
|
||||||
|
{ label: 'DPM++ SDE Karras', value: 'dpm-sde-karras' },
|
||||||
|
])
|
||||||
|
|
||||||
|
// 表单数据
|
||||||
|
const formState = ref({
|
||||||
|
model: 'sd-v1.5',
|
||||||
|
prompt: '',
|
||||||
|
negativePrompt: '',
|
||||||
|
sampler: 'euler-a',
|
||||||
|
steps: 20,
|
||||||
|
cfgScale: 7,
|
||||||
|
width: 512,
|
||||||
|
height: 512,
|
||||||
|
seed: -1,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 生成的图片列表
|
||||||
|
const generatedImages = ref([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
url: 'https://placeholder.co/512x512',
|
||||||
|
prompt: 'a beautiful landscape',
|
||||||
|
params: { steps: 20, cfg: 7, sampler: 'Euler a' }
|
||||||
|
},
|
||||||
|
// ... 更多图片
|
||||||
|
])
|
||||||
|
|
||||||
|
// 生成图片方法
|
||||||
|
const generateImage = () => {
|
||||||
|
// TODO: 实现图片生成逻辑
|
||||||
|
console.log('Generating image with params:', formState.value)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="min-h-full p-4">
|
||||||
|
<NCard title="在线生图" class="mb-4">
|
||||||
|
<NTabs type="segment" animated>
|
||||||
|
<!-- 文生图 -->
|
||||||
|
<NTabPane name="text2img" tab="文生图">
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<!-- 左侧参数面板 -->
|
||||||
|
<div class="md:col-span-2 space-y-4">
|
||||||
|
<!-- 提示词输入 -->
|
||||||
|
<NInput
|
||||||
|
v-model:value="formState.prompt"
|
||||||
|
type="textarea"
|
||||||
|
placeholder="请输入图像提示词(英文)"
|
||||||
|
:autosize="{ minRows: 3, maxRows: 6 }"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 反向提示词 -->
|
||||||
|
<NInput
|
||||||
|
v-model:value="formState.negativePrompt"
|
||||||
|
type="textarea"
|
||||||
|
placeholder="请输入反向提示词(英文)"
|
||||||
|
:autosize="{ minRows: 2, maxRows: 4 }"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 基础参数 -->
|
||||||
|
<NGrid :cols="2" :x-gap="12" :y-gap="8">
|
||||||
|
<NGridItem>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<span class="text-sm">基础模型</span>
|
||||||
|
<NSelect
|
||||||
|
v-model:value="formState.model"
|
||||||
|
:options="models"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</NGridItem>
|
||||||
|
<NGridItem>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<span class="text-sm">采样器</span>
|
||||||
|
<NSelect
|
||||||
|
v-model:value="formState.sampler"
|
||||||
|
:options="samplers"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</NGridItem>
|
||||||
|
<NGridItem>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<span class="text-sm">采样步数</span>
|
||||||
|
<NInputNumber
|
||||||
|
v-model:value="formState.steps"
|
||||||
|
:min="1"
|
||||||
|
:max="150"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</NGridItem>
|
||||||
|
<NGridItem>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<span class="text-sm">CFG Scale</span>
|
||||||
|
<NInputNumber
|
||||||
|
v-model:value="formState.cfgScale"
|
||||||
|
:min="1"
|
||||||
|
:max="30"
|
||||||
|
:step="0.5"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</NGridItem>
|
||||||
|
<NGridItem>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<span class="text-sm">宽度</span>
|
||||||
|
<NInputNumber
|
||||||
|
v-model:value="formState.width"
|
||||||
|
:min="64"
|
||||||
|
:max="2048"
|
||||||
|
:step="64"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</NGridItem>
|
||||||
|
<NGridItem>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<span class="text-sm">高度</span>
|
||||||
|
<NInputNumber
|
||||||
|
v-model:value="formState.height"
|
||||||
|
:min="64"
|
||||||
|
:max="2048"
|
||||||
|
:step="64"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</NGridItem>
|
||||||
|
</NGrid>
|
||||||
|
|
||||||
|
<!-- 生成按钮 -->
|
||||||
|
<div class="flex justify-end">
|
||||||
|
<NButton type="primary" size="large" @click="generateImage">
|
||||||
|
开始生成
|
||||||
|
</NButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧预览面板 -->
|
||||||
|
<div class="space-y-4">
|
||||||
|
<NCard title="实时预览" class="text-center">
|
||||||
|
<div class="aspect-square bg-gray-100 rounded-lg flex items-center justify-center">
|
||||||
|
<span class="text-gray-400">等待生成...</span>
|
||||||
|
</div>
|
||||||
|
</NCard>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</NTabPane>
|
||||||
|
|
||||||
|
<!-- 图生图 -->
|
||||||
|
<NTabPane name="img2img" tab="图生图">
|
||||||
|
<div class="flex items-center justify-center h-64">
|
||||||
|
<span class="text-gray-400">图生图功能开发中...</span>
|
||||||
|
</div>
|
||||||
|
</NTabPane>
|
||||||
|
</NTabs>
|
||||||
|
</NCard>
|
||||||
|
|
||||||
|
<!-- 历史记录 -->
|
||||||
|
<NCard title="生成历史" class="mb-4">
|
||||||
|
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||||
|
<div v-for="image in generatedImages" :key="image.id" class="space-y-2">
|
||||||
|
<NImage
|
||||||
|
:src="image.url"
|
||||||
|
class="rounded-lg w-full aspect-square object-cover"
|
||||||
|
:preview-src="image.url"
|
||||||
|
/>
|
||||||
|
<div class="space-y-1">
|
||||||
|
<p class="text-sm text-gray-600 truncate">{{ image.prompt }}</p>
|
||||||
|
<div class="flex flex-wrap gap-1">
|
||||||
|
<NTag size="small" v-for="(value, key) in image.params" :key="key">
|
||||||
|
{{ key }}: {{ value }}
|
||||||
|
</NTag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</NCard>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.n-input {
|
||||||
|
min-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.n-card-header) {
|
||||||
|
padding: 12px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.n-tabs-tab) {
|
||||||
|
padding: 8px 16px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,40 @@
|
||||||
|
import { setup } from '@css-render/vue3-ssr'
|
||||||
|
import { defineNuxtPlugin } from '#app'
|
||||||
|
|
||||||
|
export default defineNuxtPlugin((nuxtApp) => {
|
||||||
|
if (process.server) {
|
||||||
|
const { collect } = setup(nuxtApp.vueApp)
|
||||||
|
const originalRenderMeta = nuxtApp.ssrContext?.renderMeta
|
||||||
|
nuxtApp.ssrContext!.renderMeta = () => {
|
||||||
|
if (!originalRenderMeta) {
|
||||||
|
return {
|
||||||
|
headTags: collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const originalMeta = originalRenderMeta()
|
||||||
|
if ('headTags' in originalMeta) {
|
||||||
|
originalMeta.headTags += collect()
|
||||||
|
} else {
|
||||||
|
originalMeta.headTags = collect()
|
||||||
|
}
|
||||||
|
return originalMeta
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { collect } = setup(nuxtApp.vueApp)
|
||||||
|
useServerHead({
|
||||||
|
style: () => {
|
||||||
|
const stylesString = collect()
|
||||||
|
const stylesArray = stylesString.split(/<\/style>/g).filter(style => style)
|
||||||
|
return stylesArray.map((styleString: string) => {
|
||||||
|
const match = styleString.match(/<style cssr-id="([^"]*)">([\s\S]*)/)
|
||||||
|
if (match) {
|
||||||
|
const id = match[1]
|
||||||
|
return { 'cssr-id': id, children: match[2] }
|
||||||
|
}
|
||||||
|
return {}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
|
||||||
|
export const useMenuStore = defineStore('menu', () => {
|
||||||
|
const activeMenu = ref('/model-square')
|
||||||
|
|
||||||
|
const menuItems = [
|
||||||
|
{ path: '/model-square', icon: 'i-carbon-gallery', label: '模型广场' },
|
||||||
|
// { path: '/works-inspire', icon: 'i-carbon-light-filled', label: '作品灵感' },
|
||||||
|
// { path: '/workspace', icon: 'i-carbon-workspace', label: '工作台' },
|
||||||
|
// { path: '/web-ui', icon: 'i-carbon-image-search', label: '在线生图' },
|
||||||
|
// { path: '/comfy-ui', icon: 'i-carbon-image-search', label: '在线工作流' },
|
||||||
|
// { path: '/training-lora', icon: 'i-carbon-machine-learning', label: '训练LoRA' },
|
||||||
|
// { path: '/high-availability', icon: 'i-carbon-image', label: '高级预生图' },
|
||||||
|
// { path: '/api-platform', icon: 'i-carbon-api', label: 'API开放平台' },
|
||||||
|
// { path: '/creator-center', icon: 'i-carbon-user-admin', label: '创作中心' },
|
||||||
|
{ path: '/personal-center', icon: 'i-carbon-user', label: '个人中心' },
|
||||||
|
{ path: '/member-center', icon: 'i-carbon-user-profile', label: '会员中心' },
|
||||||
|
]
|
||||||
|
|
||||||
|
function setActiveMenu(path: string) {
|
||||||
|
activeMenu.value = path
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
activeMenu,
|
||||||
|
menuItems,
|
||||||
|
setActiveMenu,
|
||||||
|
}
|
||||||
|
})
|
|
@ -0,0 +1,38 @@
|
||||||
|
// stores/modal.ts
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
export const useModalStore = defineStore('modal', () => {
|
||||||
|
const loginModalRef = ref<any>(null)
|
||||||
|
const isModalVisible = ref(false)
|
||||||
|
|
||||||
|
function setLoginModal(modalRef: any) {
|
||||||
|
loginModalRef.value = modalRef
|
||||||
|
console.log('Modal ref set:', modalRef)
|
||||||
|
}
|
||||||
|
|
||||||
|
function showLoginModal() {
|
||||||
|
console.log('Showing login modal, ref:', loginModalRef.value)
|
||||||
|
if (loginModalRef.value?.showModal) {
|
||||||
|
loginModalRef.value.showModal()
|
||||||
|
isModalVisible.value = true
|
||||||
|
} else {
|
||||||
|
console.warn('Login modal not initialized')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideLoginModal() {
|
||||||
|
if (loginModalRef.value?.hideModal) {
|
||||||
|
loginModalRef.value.hideModal()
|
||||||
|
isModalVisible.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
loginModalRef,
|
||||||
|
isModalVisible,
|
||||||
|
setLoginModal,
|
||||||
|
showLoginModal,
|
||||||
|
hideLoginModal
|
||||||
|
}
|
||||||
|
})
|
|
@ -0,0 +1,39 @@
|
||||||
|
// stores/user.ts
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
|
||||||
|
export const useUserStore = defineStore('user', () => {
|
||||||
|
const isLoggedIn = ref(false)
|
||||||
|
const token = ref('')
|
||||||
|
|
||||||
|
// 模拟登录
|
||||||
|
function login(userToken: string) {
|
||||||
|
isLoggedIn.value = true
|
||||||
|
token.value = userToken
|
||||||
|
// 可以存储到 localStorage
|
||||||
|
localStorage.setItem('token', userToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 登出
|
||||||
|
function logout() {
|
||||||
|
isLoggedIn.value = false
|
||||||
|
token.value = ''
|
||||||
|
localStorage.removeItem('token')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查登录状态
|
||||||
|
function checkLoginStatus() {
|
||||||
|
const savedToken = localStorage.getItem('token')
|
||||||
|
if (savedToken) {
|
||||||
|
isLoggedIn.value = true
|
||||||
|
token.value = savedToken
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isLoggedIn,
|
||||||
|
token,
|
||||||
|
login,
|
||||||
|
logout,
|
||||||
|
checkLoginStatus
|
||||||
|
}
|
||||||
|
})
|
|
@ -0,0 +1,18 @@
|
||||||
|
// types/api.ts
|
||||||
|
export interface ApiResponse<T = any> {
|
||||||
|
code: number
|
||||||
|
data: T
|
||||||
|
message: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PaginationParams {
|
||||||
|
page: number
|
||||||
|
pageSize: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PaginationResponse<T> {
|
||||||
|
list: T[]
|
||||||
|
total: number
|
||||||
|
page: number
|
||||||
|
pageSize: number
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
// types/request.ts
|
||||||
|
export interface RequestOptions extends Record<string, any> {
|
||||||
|
loading?: boolean // 是否显示加载状态
|
||||||
|
timeout?: number // 超时时间
|
||||||
|
headers?: Record<string, string> // 自定义请求头
|
||||||
|
ignoreToken?: boolean // 是否忽略 token
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PaginationParams {
|
||||||
|
page: number
|
||||||
|
pageSize: number
|
||||||
|
[key: string]: any
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiResponse<T = any> {
|
||||||
|
code: number
|
||||||
|
data: T
|
||||||
|
message: string
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
// utils/error.ts
|
||||||
|
import { createDiscreteApi } from 'naive-ui'
|
||||||
|
|
||||||
|
const { message } = createDiscreteApi(['message'])
|
||||||
|
|
||||||
|
export function handleError(error: any) {
|
||||||
|
if (error.response) {
|
||||||
|
// 服务器返回错误状态码
|
||||||
|
const status = error.response.status
|
||||||
|
switch (status) {
|
||||||
|
case 400:
|
||||||
|
message.error('请求参数错误')
|
||||||
|
break
|
||||||
|
case 401:
|
||||||
|
message.error('未授权,请登录')
|
||||||
|
break
|
||||||
|
case 403:
|
||||||
|
message.error('拒绝访问')
|
||||||
|
break
|
||||||
|
case 404:
|
||||||
|
message.error('请求地址不存在')
|
||||||
|
break
|
||||||
|
case 500:
|
||||||
|
message.error('服务器内部错误')
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
message.error(`请求失败: ${error.message}`)
|
||||||
|
}
|
||||||
|
} else if (error.request) {
|
||||||
|
// 请求发出但没有收到响应
|
||||||
|
message.error('网络错误,请检查您的网络连接')
|
||||||
|
} else {
|
||||||
|
// 请求配置出错
|
||||||
|
message.error(`请求错误: ${error.message}`)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,149 @@
|
||||||
|
// utils/request.ts
|
||||||
|
import axios from 'axios'
|
||||||
|
import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
|
||||||
|
import { createDiscreteApi } from 'naive-ui'
|
||||||
|
|
||||||
|
const { message, loadingBar } = createDiscreteApi(['message', 'loadingBar'])
|
||||||
|
|
||||||
|
// 定义响应数据接口
|
||||||
|
interface ApiResponse<T = any> {
|
||||||
|
code: number
|
||||||
|
data: T
|
||||||
|
message: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 定义请求配置接口
|
||||||
|
interface RequestOptions extends AxiosRequestConfig {
|
||||||
|
loading?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
class RequestHttp {
|
||||||
|
private instance: AxiosInstance
|
||||||
|
|
||||||
|
constructor(config: AxiosRequestConfig) {
|
||||||
|
this.instance = axios.create(config)
|
||||||
|
|
||||||
|
// 请求拦截器
|
||||||
|
this.instance.interceptors.request.use(
|
||||||
|
(config) => {
|
||||||
|
// 开启 loading
|
||||||
|
if (config.loading) {
|
||||||
|
loadingBar.start()
|
||||||
|
}
|
||||||
|
return config
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// 响应拦截器
|
||||||
|
this.instance.interceptors.response.use(
|
||||||
|
(response: AxiosResponse) => {
|
||||||
|
const { data, config } = response
|
||||||
|
|
||||||
|
// 关闭 loading
|
||||||
|
if (config.loading) {
|
||||||
|
loadingBar.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理业务状态码
|
||||||
|
if (data.code !== 200) {
|
||||||
|
message.error(data.message || '请求失败')
|
||||||
|
return Promise.reject(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
return data
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
// 关闭 loading
|
||||||
|
loadingBar.error()
|
||||||
|
|
||||||
|
// 处理错误
|
||||||
|
if (error.response) {
|
||||||
|
this.handleError(error.response.status)
|
||||||
|
} else {
|
||||||
|
message.error('网络连接异常')
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理错误状态码
|
||||||
|
private handleError(status: number): void {
|
||||||
|
switch (status) {
|
||||||
|
case 400:
|
||||||
|
message.error('请求参数错误')
|
||||||
|
break
|
||||||
|
case 401:
|
||||||
|
message.error('未登录或登录已过期')
|
||||||
|
break
|
||||||
|
case 403:
|
||||||
|
message.error('没有权限')
|
||||||
|
break
|
||||||
|
case 404:
|
||||||
|
message.error('请求的资源不存在')
|
||||||
|
break
|
||||||
|
case 500:
|
||||||
|
message.error('服务器错误')
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
message.error('未知错误')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET 请求
|
||||||
|
// GET 请求
|
||||||
|
public get<T = any>(
|
||||||
|
url: string,
|
||||||
|
data?: Record<string, any>,
|
||||||
|
options: RequestOptions = {}
|
||||||
|
): Promise<ApiResponse<T>> {
|
||||||
|
// 如果 data 中包含 params,则使用 params 中的值
|
||||||
|
const params = data?.params || data
|
||||||
|
|
||||||
|
return this.instance.get(url, {
|
||||||
|
...options,
|
||||||
|
params // 使用解构后的参数
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST 请求
|
||||||
|
public post<T = any>(
|
||||||
|
url: string,
|
||||||
|
data?: Record<string, any>,
|
||||||
|
options: RequestOptions = {}
|
||||||
|
): Promise<ApiResponse<T>> {
|
||||||
|
return this.instance.post(url, data, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PUT 请求
|
||||||
|
public put<T = any>(
|
||||||
|
url: string,
|
||||||
|
data?: Record<string, any>,
|
||||||
|
options: RequestOptions = {}
|
||||||
|
): Promise<ApiResponse<T>> {
|
||||||
|
return this.instance.put(url, data, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DELETE 请求
|
||||||
|
public delete<T = any>(
|
||||||
|
url: string,
|
||||||
|
params?: Record<string, any>,
|
||||||
|
options: RequestOptions = {}
|
||||||
|
): Promise<ApiResponse<T>> {
|
||||||
|
return this.instance.delete(url, { params, ...options })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const request = new RequestHttp({
|
||||||
|
baseURL: process.env.NUXT_API_BASE_URL || '/api',
|
||||||
|
timeout: 15000,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export default request
|
|
@ -0,0 +1,12 @@
|
||||||
|
// @ts-check
|
||||||
|
import antfu from '@antfu/eslint-config'
|
||||||
|
import nuxt from './.nuxt/eslint.config.mjs'
|
||||||
|
|
||||||
|
export default nuxt(
|
||||||
|
antfu(
|
||||||
|
{
|
||||||
|
unocss: true,
|
||||||
|
formatters: true,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
|
@ -0,0 +1,11 @@
|
||||||
|
[build]
|
||||||
|
publish = "dist"
|
||||||
|
command = "pnpm run build"
|
||||||
|
|
||||||
|
[build.environment]
|
||||||
|
NODE_VERSION = "20"
|
||||||
|
|
||||||
|
[[redirects]]
|
||||||
|
from = "/*"
|
||||||
|
to = "/index.html"
|
||||||
|
status = 200
|
|
@ -0,0 +1,109 @@
|
||||||
|
import { pwa } from './app/config/pwa'
|
||||||
|
import { appDescription } from './app/constants/index'
|
||||||
|
|
||||||
|
export default defineNuxtConfig({
|
||||||
|
ssr: true,
|
||||||
|
modules: [
|
||||||
|
'@vueuse/nuxt',
|
||||||
|
'@unocss/nuxt',
|
||||||
|
'@pinia/nuxt',
|
||||||
|
'@nuxtjs/color-mode',
|
||||||
|
'@vite-pwa/nuxt',
|
||||||
|
'@nuxt/eslint',
|
||||||
|
],
|
||||||
|
routeRules: {
|
||||||
|
'/': { redirect: '/model-square' }
|
||||||
|
},
|
||||||
|
nitro: {
|
||||||
|
devProxy: {
|
||||||
|
'/api': {
|
||||||
|
target: 'http://example.com',
|
||||||
|
changeOrigin: true,
|
||||||
|
prependPath: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
devtools: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
build: {
|
||||||
|
transpile:
|
||||||
|
process.env.NODE_ENV === 'production'
|
||||||
|
? ['naive-ui', 'vueuc', '@css-render/vue3-ssr', '@juggle/resize-observer']
|
||||||
|
: ['@juggle/resize-observer']
|
||||||
|
},
|
||||||
|
vite: {
|
||||||
|
define: {
|
||||||
|
'process.env.DEBUG': false,
|
||||||
|
},
|
||||||
|
// 避免 vite 热更新时出现警告
|
||||||
|
// optimizeDeps: {
|
||||||
|
// include: ['date-fns-tz/esm/formatInTimeZone']
|
||||||
|
// }
|
||||||
|
},
|
||||||
|
|
||||||
|
app: {
|
||||||
|
head: {
|
||||||
|
viewport: 'width=device-width,initial-scale=1',
|
||||||
|
link: [
|
||||||
|
{ rel: 'icon', href: '/favicon.ico', sizes: 'any' },
|
||||||
|
{ rel: 'icon', type: 'image/svg+xml', href: '/nuxt.svg' },
|
||||||
|
{ rel: 'apple-touch-icon', href: '/apple-touch-icon.png' },
|
||||||
|
],
|
||||||
|
meta: [
|
||||||
|
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
|
||||||
|
{ name: 'description', content: appDescription },
|
||||||
|
{ name: 'apple-mobile-web-app-status-bar-style', content: 'black-translucent' },
|
||||||
|
{ name: 'theme-color', media: '(prefers-color-scheme: light)', content: 'white' },
|
||||||
|
{ name: 'theme-color', media: '(prefers-color-scheme: dark)', content: '#222222' },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// css: [
|
||||||
|
// '@unocss/reset/tailwind.css',
|
||||||
|
// ],
|
||||||
|
|
||||||
|
colorMode: {
|
||||||
|
classSuffix: '',
|
||||||
|
},
|
||||||
|
|
||||||
|
future: {
|
||||||
|
compatibilityVersion: 4,
|
||||||
|
},
|
||||||
|
|
||||||
|
experimental: {
|
||||||
|
// when using generate, payload js assets included in sw precache manifest
|
||||||
|
// but missing on offline, disabling extraction it until fixed
|
||||||
|
payloadExtraction: false,
|
||||||
|
renderJsonPayloads: true,
|
||||||
|
typedPages: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
compatibilityDate: '2024-08-14',
|
||||||
|
|
||||||
|
nitro: {
|
||||||
|
esbuild: {
|
||||||
|
options: {
|
||||||
|
target: 'esnext',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
prerender: {
|
||||||
|
crawlLinks: false,
|
||||||
|
routes: ['/'],
|
||||||
|
ignore: ['/hi'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
eslint: {
|
||||||
|
config: {
|
||||||
|
standalone: false,
|
||||||
|
nuxt: {
|
||||||
|
sortConfigKeys: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
pwa,
|
||||||
|
})
|
|
@ -0,0 +1,55 @@
|
||||||
|
{
|
||||||
|
"type": "module",
|
||||||
|
"private": true,
|
||||||
|
"packageManager": "pnpm@9.15.1",
|
||||||
|
"scripts": {
|
||||||
|
"build": "nuxi build",
|
||||||
|
"dev:pwa": "VITE_PLUGIN_PWA=true nuxi dev",
|
||||||
|
"dev": "nuxi dev",
|
||||||
|
"generate": "nuxi generate",
|
||||||
|
"prepare": "nuxi prepare",
|
||||||
|
"start": "node .output/server/index.mjs",
|
||||||
|
"start:generate": "npx serve .output/public",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"typecheck": "vue-tsc --noEmit"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@antfu/eslint-config": "^3.12.1",
|
||||||
|
"@css-render/vue3-ssr": "^0.15.14",
|
||||||
|
"@iconify-json/carbon": "^1.2.5",
|
||||||
|
"@iconify-json/twemoji": "^1.2.2",
|
||||||
|
"@nuxt/devtools": "^1.7.0",
|
||||||
|
"@nuxt/eslint": "^0.7.4",
|
||||||
|
"@nuxtjs/color-mode": "^3.5.2",
|
||||||
|
"@nuxtjs/tailwindcss": "^6.13.1",
|
||||||
|
"@pinia/nuxt": "^0.9.0",
|
||||||
|
"@types/node": "^22.10.6",
|
||||||
|
"@unocss/eslint-config": "^0.65.3",
|
||||||
|
"@unocss/nuxt": "^0.65.3",
|
||||||
|
"@vite-pwa/nuxt": "^0.10.6",
|
||||||
|
"@vueuse/nuxt": "^12.2.0",
|
||||||
|
"autoprefixer": "^10.4.20",
|
||||||
|
"consola": "^3.3.1",
|
||||||
|
"eslint": "^9.17.0",
|
||||||
|
"eslint-plugin-format": "^0.1.3",
|
||||||
|
"nuxt": "^3.15.0",
|
||||||
|
"pinia": "^2.3.0",
|
||||||
|
"postcss": "^8.5.1",
|
||||||
|
"tailwindcss": "^3.4.17",
|
||||||
|
"typescript": "^5.7.2",
|
||||||
|
"vue-tsc": "^2.2.0",
|
||||||
|
"vueuc": "^0.4.64"
|
||||||
|
},
|
||||||
|
"resolutions": {
|
||||||
|
"unplugin": "^2.1.0",
|
||||||
|
"vite": "^6.0.6",
|
||||||
|
"vite-plugin-inspect": "^0.10.6"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@unocss/reset": "^65.4.0",
|
||||||
|
"axios": "^1.7.9",
|
||||||
|
"date-fns-tz": "^3.2.0",
|
||||||
|
"lucide-vue-next": "^0.471.0",
|
||||||
|
"naive-ui": "^2.41.0"
|
||||||
|
}
|
||||||
|
}
|
After Width: | Height: | Size: 3.0 KiB |
After Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 4.5 KiB |
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="900" height="900" viewBox="0 0 900 900" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M504.908 750H839.476C850.103 750.001 860.542 747.229 869.745 741.963C878.948 736.696 886.589 729.121 891.9 719.999C897.211 710.876 900.005 700.529 900 689.997C899.995 679.465 897.193 669.12 891.873 660.002L667.187 274.289C661.876 265.169 654.237 257.595 645.036 252.329C635.835 247.064 625.398 244.291 614.773 244.291C604.149 244.291 593.711 247.064 584.511 252.329C575.31 257.595 567.67 265.169 562.36 274.289L504.908 372.979L392.581 179.993C387.266 170.874 379.623 163.301 370.42 158.036C361.216 152.772 350.777 150 340.151 150C329.525 150 319.086 152.772 309.883 158.036C300.679 163.301 293.036 170.874 287.721 179.993L8.12649 660.002C2.80743 669.12 0.00462935 679.465 5.72978e-06 689.997C-0.00461789 700.529 2.78909 710.876 8.10015 719.999C13.4112 729.121 21.0523 736.696 30.255 741.963C39.4576 747.229 49.8973 750.001 60.524 750H270.538C353.748 750 415.112 713.775 457.336 643.101L559.849 467.145L614.757 372.979L779.547 655.834H559.849L504.908 750ZM267.114 655.737L120.551 655.704L340.249 278.586L449.87 467.145L376.474 593.175C348.433 639.03 316.577 655.737 267.114 655.737Z" fill="#00DC82"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 3.2 KiB |
After Width: | Height: | Size: 7.0 KiB |
|
@ -0,0 +1,2 @@
|
||||||
|
User-agent: *
|
||||||
|
Allow: /
|
After Width: | Height: | Size: 3.4 KiB |
|
@ -0,0 +1,7 @@
|
||||||
|
const startAt = Date.now()
|
||||||
|
let count = 0
|
||||||
|
|
||||||
|
export default defineEventHandler(() => ({
|
||||||
|
pageview: count++,
|
||||||
|
startAt,
|
||||||
|
}))
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"extends": "../.nuxt/tsconfig.server.json"
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
// tailwind.config.js
|
||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
content: [
|
||||||
|
"./components/**/*.{js,vue,ts}",
|
||||||
|
"./layouts/**/*.vue",
|
||||||
|
"./pages/**/*.vue",
|
||||||
|
"./plugins/**/*.{js,ts}",
|
||||||
|
"./app.vue",
|
||||||
|
"./error.vue",
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
// 自定义颜色
|
||||||
|
colors: {
|
||||||
|
primary: {
|
||||||
|
50: '#f0f9ff',
|
||||||
|
100: '#e0f2fe',
|
||||||
|
200: '#bae6fd',
|
||||||
|
300: '#7dd3fc',
|
||||||
|
400: '#38bdf8',
|
||||||
|
500: '#0ea5e9',
|
||||||
|
600: '#0284c7',
|
||||||
|
700: '#0369a1',
|
||||||
|
800: '#075985',
|
||||||
|
900: '#0c4a6e',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// 自定义字体
|
||||||
|
fontFamily: {
|
||||||
|
sans: ['Inter var', 'sans-serif'],
|
||||||
|
},
|
||||||
|
// 自定义断点
|
||||||
|
screens: {
|
||||||
|
'xs': '475px',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
// 禁用预加载(可选)
|
||||||
|
future: {
|
||||||
|
removeDeprecatedGapUtilities: true,
|
||||||
|
purgeLayersByDefault: true,
|
||||||
|
},
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"extends": "./.nuxt/tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"types": [
|
||||||
|
"naive-ui/volar"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
{"code":"import{createLocalFontProcessor}from\"@unocss/preset-web-fonts/local\";import{defineConfig,presetAttributify,presetIcons,presetTypography,presetUno,presetWebFonts,transformerDirectives,transformerVariantGroup}from\"unocss\";var uno_config_default=defineConfig({shortcuts:[[\"btn\",\"px-4 py-1 rounded inline-block bg-teal-600 text-white cursor-pointer hover:bg-teal-700 disabled:cursor-default disabled:bg-gray-600 disabled:opacity-50\"],[\"icon-btn\",\"inline-block cursor-pointer select-none opacity-75 transition duration-200 ease-in-out hover:opacity-100 hover:text-teal-600\"]],presets:[presetUno({preflight:false}),presetAttributify(),presetIcons({scale:1.2}),presetTypography(),presetWebFonts({fonts:{sans:\"DM Sans\",serif:\"DM Serif Display\",mono:\"DM Mono\"},processors:createLocalFontProcessor()})],transformers:[transformerDirectives(),transformerVariantGroup()]});export{uno_config_default as default};\n","warnings":[],"map":{"version":3,"mappings":"AAAA,OAAS,6BAAgC,iCACzC,OACE,aACA,kBACA,YACA,iBACA,UACA,eACA,sBACA,4BACK,SAEP,IAAO,mBAAQ,aAAa,CAC1B,UAAW,CACT,CAAC,MAAO,yJAAyJ,EACjK,CAAC,WAAY,8HAA8H,CAC7I,EACA,QAAS,CACP,UAAU,CACR,UAAW,KACb,CAAC,EACD,kBAAkB,EAClB,YAAY,CACV,MAAO,GACT,CAAC,EACD,iBAAiB,EACjB,eAAe,CACb,MAAO,CACL,KAAM,UACN,MAAO,mBACP,KAAM,SACR,EACA,WAAY,yBAAyB,CACvC,CAAC,CACH,EACA,aAAc,CACZ,sBAAsB,EACtB,wBAAwB,CAC1B,CACF,CAAC","names":[],"ignoreList":[],"sources":["/Users/shenhan/Desktop/liblib_web_pro/uno.config.ts"],"sourcesContent":[null]}}
|