test
shenhan 2025-01-17 17:28:08 +08:00
commit 44463a89e0
56 changed files with 14057 additions and 0 deletions

8
.gitignore vendored 100644
View File

@ -0,0 +1,8 @@
node_modules
*.log
dist
.output
.nuxt
.env
.idea/
public/assets/fonts

4
.npmrc 100644
View File

@ -0,0 +1,4 @@
shamefully-hoist=true
strict-peer-dependencies=false
shell-emulator=true
auto-install-peers=false

4
.stackblitzrc 100644
View File

@ -0,0 +1,4 @@
{
"installDependencies": true,
"startCommand": "npm run dev"
}

10
.vscode/extensions.json vendored 100644
View File

@ -0,0 +1,10 @@
{
"recommendations": [
"antfu.iconify",
"antfu.unocss",
"antfu.goto-alias",
"csstools.postcss",
"dbaeumer.vscode-eslint",
"vue.volar"
]
}

45
.vscode/settings.json vendored 100644
View File

@ -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
}

21
LICENSE 100644
View File

@ -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.

80
README.md 100644
View File

@ -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
```

21
app/api/common.ts 100644
View File

@ -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')
}
}

4
app/api/index.ts 100644
View File

@ -0,0 +1,4 @@
// api/index.ts
export * from './user'
export * from './common'
// 导出其他 API 模块

36
app/api/user.ts 100644
View File

@ -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)
}
}

47
app/app.vue 100644
View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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,
}
}

79
app/config/pwa.ts 100644
View File

@ -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,
},
}

View File

@ -0,0 +1,2 @@
export const appName = '魔创未来'
export const appDescription = '魔创未来'

View File

@ -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

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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')
}
})

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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 {}
})
}
})
})

29
app/stores/menu.ts 100644
View File

@ -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,
}
})

View File

@ -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
}
})

39
app/stores/user.ts 100644
View File

@ -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
}
})

18
app/types/api.ts 100644
View File

@ -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
}

View File

@ -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
}

36
app/utils/error.ts 100644
View File

@ -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}`)
}
}

View File

@ -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

12
eslint.config.js 100644
View File

@ -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,
},
),
)

11
netlify.toml 100755
View File

@ -0,0 +1,11 @@
[build]
publish = "dist"
command = "pnpm run build"
[build.environment]
NODE_VERSION = "20"
[[redirects]]
from = "/*"
to = "/index.html"
status = 200

109
nuxt.config.ts 100644
View File

@ -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,
})

55
package.json 100644
View File

@ -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"
}
}

11683
pnpm-lock.yaml 100644

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
public/favicon.ico 100644

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

3
public/nuxt.svg 100644
View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

View File

@ -0,0 +1,2 @@
User-agent: *
Allow: /

BIN
public/vite.png 100644

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@ -0,0 +1,7 @@
const startAt = Date.now()
let count = 0
export default defineEventHandler(() => ({
pageview: count++,
startAt,
}))

View File

@ -0,0 +1,3 @@
{
"extends": "../.nuxt/tsconfig.server.json"
}

45
tailwind.config.js 100644
View File

@ -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,
},
}

8
tsconfig.json 100644
View File

@ -0,0 +1,8 @@
{
"extends": "./.nuxt/tsconfig.json",
"compilerOptions": {
"types": [
"naive-ui/volar"
]
}
}

View File

@ -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]}}