This commit is contained in:
overtrue
2024-12-08 14:18:52 +08:00
parent efbbec2ee1
commit b86e3f1591
43 changed files with 13827 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
APP_NAME=RustFS
APP_DESCRIPTION=RustFS is a distributed file system written in Rust.
API_BASE_URL=http://localhost:9000/api
+24
View File
@@ -0,0 +1,24 @@
# Nuxt dev/build outputs
.output
.data
.nuxt
.nitro
.cache
dist
# Node dependencies
node_modules
# Logs
logs
*.log
# Misc
.DS_Store
.fleet
.idea
# Local env files
.env
.env.*
!.env.example
+75
View File
@@ -0,0 +1,75 @@
# RustFS Console
Look at the [Nuxt documentation](https://nuxt.com/docs/getting-started/introduction) to learn more.
## Setup
Make sure to install dependencies:
```bash
# npm
npm install
# pnpm
pnpm install
# yarn
yarn install
# bun
bun install
```
## Development Server
Start the development server on `http://localhost:3000`:
```bash
# npm
npm run dev
# pnpm
pnpm dev
# yarn
yarn dev
# bun
bun run dev
```
## Production
Build the application for production:
```bash
# npm
npm run build
# pnpm
pnpm build
# yarn
yarn build
# bun
bun run build
```
Locally preview production build:
```bash
# npm
npm run preview
# pnpm
pnpm preview
# yarn
yarn preview
# bun
bun run preview
```
Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.
+7
View File
@@ -0,0 +1,7 @@
import navs from '~/config/navs'
export default defineAppConfig({
navs: navs,
name: process.env.APP_NAME || 'RustFS',
description: process.env.APP_DESCRIPTION || 'RustFS is a distributed file system written in Rust.',
})
+17
View File
@@ -0,0 +1,17 @@
<template>
<div :class="theme.name === darkTheme.name ? 'dark' : ''">
<n-config-provider :theme="theme">
<n-message-provider>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</n-message-provider>
</n-config-provider>
</div>
</template>
<script lang="ts" setup>
import { darkTheme, type GlobalTheme } from 'naive-ui'
import { ref } from 'vue'
const theme = ref<GlobalTheme | null>(darkTheme)
</script>
+34
View File
@@ -0,0 +1,34 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1920 1080">
<g transform="
rotate(0 960 540)
translate(-0 -0)
scale(1)
">
<rect width="1920" height="1080" fill="rgb(184, 171, 255)"></rect>
<g transform="translate(0, 0)">
<path fill="rgb(152, 137, 233)" fill-opacity="1"
d="M0,211.766L40,210.045C80,208.324,160,204.882,240,204.349C320,203.815,400,206.19,480,206.457C560,206.724,640,204.883,720,181.91C800,158.936,880,114.83,960,111.105C1040,107.38,1120,144.036,1200,132.802C1280,121.567,1360,62.441,1440,59.088C1520,55.735,1600,108.155,1680,121.211C1760,134.268,1840,107.962,1880,94.809L1920,81.656L1920,1080L1880,1080C1840,1080,1760,1080,1680,1080C1600,1080,1520,1080,1440,1080C1360,1080,1280,1080,1200,1080C1120,1080,1040,1080,960,1080C880,1080,800,1080,720,1080C640,1080,560,1080,480,1080C400,1080,320,1080,240,1080C160,1080,80,1080,40,1080L0,1080Z">
</path>
</g>
<g transform="translate(0, 216)">
<path fill="rgb(121, 103, 210)" fill-opacity="1"
d="M0,14.482L40,37.236C80,59.99,160,105.497,240,113.703C320,121.909,400,92.812,480,78.248C560,63.685,640,63.654,720,84.765C800,105.875,880,148.126,960,170.818C1040,193.51,1120,196.643,1200,170.805C1280,144.967,1360,90.158,1440,67.574C1520,44.99,1600,54.631,1680,75.764C1760,96.896,1840,129.519,1880,145.831L1920,162.143L1920,864L1880,864C1840,864,1760,864,1680,864C1600,864,1520,864,1440,864C1360,864,1280,864,1200,864C1120,864,1040,864,960,864C880,864,800,864,720,864C640,864,560,864,480,864C400,864,320,864,240,864C160,864,80,864,40,864L0,864Z">
</path>
</g>
<g transform="translate(0, 432)">
<path fill="rgb(89, 68, 188)" fill-opacity="1"
d="M0,202.744L40,187.075C80,171.407,160,140.071,240,108.188C320,76.304,400,43.874,480,26.019C560,8.164,640,4.885,720,24.647C800,44.41,880,87.215,960,100.669C1040,114.124,1120,98.228,1200,81.706C1280,65.185,1360,48.037,1440,43.353C1520,38.669,1600,46.447,1680,51.965C1760,57.483,1840,60.739,1880,62.367L1920,63.996L1920,648L1880,648C1840,648,1760,648,1680,648C1600,648,1520,648,1440,648C1360,648,1280,648,1200,648C1120,648,1040,648,960,648C880,648,800,648,720,648C640,648,560,648,480,648C400,648,320,648,240,648C160,648,80,648,40,648L0,648Z">
</path>
</g>
<g transform="translate(0, 648)">
<path fill="rgb(58, 34, 165)" fill-opacity="1"
d="M0,168.793L40,175.116C80,181.439,160,194.085,240,201.671C320,209.256,400,211.781,480,197.174C560,182.567,640,150.829,720,125.577C800,100.325,880,81.56,960,65.182C1040,48.804,1120,34.813,1200,55.484C1280,76.155,1360,131.487,1440,163.3C1520,195.113,1600,203.408,1680,207.849C1760,212.29,1840,212.879,1880,213.173L1920,213.467L1920,432L1880,432C1840,432,1760,432,1680,432C1600,432,1520,432,1440,432C1360,432,1280,432,1200,432C1120,432,1040,432,960,432C880,432,800,432,720,432C640,432,560,432,480,432C400,432,320,432,240,432C160,432,80,432,40,432L0,432Z">
</path>
</g>
<g transform="translate(0, 864)">
<path fill="rgb(26, 0, 143)" fill-opacity="1"
d="M0,137.843L40,134.706C80,131.569,160,125.295,240,112.475C320,99.655,400,80.29,480,95.586C560,110.882,640,160.839,720,181.672C800,202.506,880,194.215,960,191.858C1040,189.501,1120,193.078,1200,188.928C1280,184.778,1360,172.902,1440,143.273C1520,113.645,1600,66.265,1680,71.939C1760,77.613,1840,136.34,1880,165.704L1920,195.068L1920,216L1880,216C1840,216,1760,216,1680,216C1600,216,1520,216,1440,216C1360,216,1280,216,1200,216C1120,216,1040,216,960,216C880,216,800,216,720,216C640,216,560,216,480,216C400,216,320,216,240,216C160,216,80,216,40,216L0,216Z">
</path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

+3
View File
@@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
+13
View File
@@ -0,0 +1,13 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
// biome-ignore lint: disable
export {}
declare global {
const useDialog: typeof import('naive-ui')['useDialog']
const useLoadingBar: typeof import('naive-ui')['useLoadingBar']
const useMessage: typeof import('naive-ui')['useMessage']
const useNotification: typeof import('naive-ui')['useNotification']
}
+27
View File
@@ -0,0 +1,27 @@
/* eslint-disable */
// @ts-nocheck
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
NAvatar: typeof import('naive-ui')['NAvatar']
NButton: typeof import('naive-ui')['NButton']
NConfigProvider: typeof import('naive-ui')['NConfigProvider']
NDropdown: typeof import('naive-ui')['NDropdown']
NInput: typeof import('naive-ui')['NInput']
NLayout: typeof import('naive-ui')['NLayout']
NLayoutFooter: typeof import('naive-ui')['NLayoutFooter']
NLayoutHeader: typeof import('naive-ui')['NLayoutHeader']
NLayoutSider: typeof import('naive-ui')['NLayoutSider']
NMenu: typeof import('naive-ui')['NMenu']
NMessageProvider: typeof import('naive-ui')['NMessageProvider']
NSelect: typeof import('naive-ui')['NSelect']
NSpace: typeof import('naive-ui')['NSpace']
NSwitch: typeof import('naive-ui')['NSwitch']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
}
}
+5
View File
@@ -0,0 +1,5 @@
<template>
<div>
FOOTER
</div>
</template>
+5
View File
@@ -0,0 +1,5 @@
<template>
<div>
HEADER
</div>
</template>
+60
View File
@@ -0,0 +1,60 @@
<script lang="ts" setup>
import { RouterLink } from 'vue-router'
import { useSidebarStore } from '~/store/sidebar'
const appConfig = useAppConfig()
const sidebarStore = useSidebarStore();
const isCollapsed = computed(() => sidebarStore.isCollapsed);
const toggleSidebar = () => {
sidebarStore.toggleSidebar();
};
const options = computed(() => {
return appConfig.navs.map((nav) => {
let item: { key: string; label: () => string | VNode; icon?: () => VNode; children?: any[] } = {
key: nav.label,
label: () => nav.to ? h(RouterLink, { to: nav.to }, { default: () => nav.label }) : nav.label,
icon: nav.icon ? iconRender(nav.icon) : undefined
}
if (nav['children']) {
item.children = nav['children'].map((child) => {
return {
key: child.label,
type: 'item',
label: () => h(RouterLink, { to: child.to }, { default: () => child.label }),
icon: child.icon ? iconRender(child.icon) : undefined
}
})
}
return item
})
})
</script>
<template>
<n-layout-sider bordered collapse-mode="width" :collapsed-width="64" :width="240" :native-scrollbar="false" :collapsed="isCollapsed">
<div class="flex flex-col min-h-screen gap-4">
<div class="border-b dark:border-gray-600 flex flex-wrap items-center py-2" :class="isCollapsed ? 'justify-center' : 'justify-between'">
<div class="py-2 px-4">
<n-avatar v-if="isCollapsed" class="text-center text-2xl">{{ appConfig.name.substring(0, 1) }}</n-avatar>
<h2 v-else class="text-center text-2xl">{{ appConfig.name }}</h2>
</div>
<div v-if="!isCollapsed" class="px-4 flex items-center">
<Icon name="ri:menu-fold-fill" class="cursor-pointer text-xl" @click="toggleSidebar" />
</div>
</div>
<n-menu :inverted="isCollapsed" :indent="26" :root-indent="12" :collapsed-width="64" :collapsed-icon-size="22" :options="options" default-expand-all class="flex-1" />
<div v-if="isCollapsed" class="w-full flex items-center justify-center py-4">
<Icon name="ri:menu-unfold-fill" class="cursor-pointer text-xl" @click="toggleSidebar" />
</div>
<div class="p-4">
<UserDropdown :isCollapsed="isCollapsed" />
</div>
</div>
</n-layout-sider>
</template>
+35
View File
@@ -0,0 +1,35 @@
<template>
<n-dropdown :options="options" placement="right-end">
<div class="flex items-center border-t dark:border-gray-600 py-4">
<n-avatar round size="small" src="https://07akioni.oss-cn-beijing.aliyuncs.com/07akioni.jpeg" />
<template v-if="!isCollapsed">
<span class="px-2">RustFS</span>
<Icon name="ri:more-2-line" class="ml-auto text-xl" />
</template>
</div>
</n-dropdown>
</template>
<script lang="ts" setup>
import { Icon } from '#components'
import { defineProps, withDefaults } from 'vue'
const props = withDefaults(defineProps<{
isCollapsed?: boolean
}>(), {
isCollapsed: false
})
const options = [
{
label: '用户资料',
key: 'profile',
icon: () => icon('ri:account-box-line')
},
{
label: '退出登录',
key: 'logout',
icon: () => icon('ri:logout-box-r-line')
},
]
</script>
+11
View File
@@ -0,0 +1,11 @@
import type { UseFetchOptions } from 'nuxt/app'
export function useAPI<T>(
url: string | (() => string),
options?: UseFetchOptions<T>,
) {
return useFetch(url, {
...options,
$fetch: useNuxtApp().$apiFetch as typeof $fetch
})
}
+77
View File
@@ -0,0 +1,77 @@
// icons: https://icones.js.org/collection/ri
export default [
{
label: 'Home',
to: '/',
icon: 'ri:home-2-line',
},
{
label: 'Data',
icon: 'ri:file-user-line',
children: [
{
label: 'Object Browser',
to: '/browser',
icon: 'ri:box-3-line',
},
{
label: 'Buckets',
to: '/buckets',
icon: 'ri:archive-drawer-line',
},
],
},
{
label: 'Authentication',
icon: 'ri:door-lock-box-line',
children: [
{
label: 'Access keys',
to: '/access-keys',
icon: 'ri:door-lock-line',
},
{
label: 'Policies',
to: '/policies',
icon: 'ri:shield-check-fill',
},
{
label: 'Users',
to: '/users',
icon: 'ri:file-user-line',
},
{
label: 'Events',
to: '/events',
icon: 'ri:broadcast-line',
},
{
label: 'Configuration',
to: '/configuration',
icon: 'ri:settings-2-line',
},
]
},
{
label: 'Info',
icon: 'ri:settings-2-line',
children: [
{
label: 'Performance',
to: '/performance',
icon: 'ri:bar-chart-box-line'
},
{
label: 'License',
to: '/license',
icon: 'ri:copyright-line'
},
{
label: 'Documentation',
to: '/docs',
icon: 'ri:file-list-3-line'
}
]
}
]
+12
View File
@@ -0,0 +1,12 @@
import en from '~/locales/en-US.json'
import zh from '~/locales/zh-CN.json'
export default defineI18nConfig(() => ({
legacy: false,
locale: 'en',
locales: [
{ code: 'en', language: 'en-US' },
{ code: 'zh', language: 'zh-CN' }
],
defaultLocale: 'en',
messages: { en, zh }
}))
+16
View File
@@ -0,0 +1,16 @@
<template>
<n-space vertical>
<n-layout class="h-full">
<n-layout has-sider>
<Sidebar />
<div class="min-h-screen flex flex-col">
<Header />
<div class="flex-1">
<slot />
</div>
<!-- <Footer /> -->
</div>
</n-layout>
</n-layout>
</n-space>
</template>
+5
View File
@@ -0,0 +1,5 @@
<template>
<div class="min-h-screen">
<slot />
</div>
</template>
+56
View File
@@ -0,0 +1,56 @@
class ApiClient {
private instance: any
constructor(baseURL: string, token?: string, options?: any) {
this.instance = $fetch.create({
baseURL,
onRequest({ request, options }) {
if (token) {
options.headers.set('Authorization', `Bearer ${token}`)
}
},
onResponse({ response }) {
console.log('[API] response', response)
},
// ...options,
})
}
getInstance() {
return this.instance
}
async get(url: string, options?: any) {
return this.instance(url, { method: 'GET', ...options })
}
async post(url: string, body: any, options?: any) {
return this.instance(url, { method: 'POST', body, ...options })
}
async delete(url: string, options?: any) {
return this.instance(url, { method: 'DELETE', ...options })
}
async put(url: string, body: any, options?: any) {
return this.instance(url, { method: 'PUT', body, ...options })
}
async patch(url: string, body: any, options?: any) {
return this.instance(url, { method: 'PATCH', body, ...options })
}
async head(url: string, options?: any) {
return this.instance(url, { method: 'HEAD', ...options })
}
async options(url: string, options?: any) {
return this.instance(url, { method: 'OPTIONS', ...options })
}
async trace(url: string, options?: any) {
return this.instance(url, { method: 'TRACE', ...options })
}
}
export default ApiClient
+3
View File
@@ -0,0 +1,3 @@
{
"hello": "Hello"
}
+3
View File
@@ -0,0 +1,3 @@
{
"hello": "您好"
}
+3
View File
@@ -0,0 +1,3 @@
export default defineNuxtRouteMiddleware((to, from) => {
})
+3
View File
@@ -0,0 +1,3 @@
export default defineNuxtRouteMiddleware((to, from) => {
})
+53
View File
@@ -0,0 +1,53 @@
import AutoImport from 'unplugin-auto-import/vite'
import { NaiveUiResolver } from 'unplugin-vue-components/resolvers'
import Components from 'unplugin-vue-components/vite'
const isDev = process.env.NODE_ENV === 'development'
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
ssr: false,
css: ['~/assets/css/tailwind.css'],
compatibilityDate: '2024-11-01',
devtools: { enabled: true },
modules: [
'@nuxtjs/tailwindcss',
'@nuxtjs/i18n',
'@pinia/nuxt',
'@nuxt/icon',
'nuxtjs-naive-ui',
'@vueuse/nuxt'
],
plugins: ['~/plugins/api.ts'],
runtimeConfig: {
public: {
api: {
baseURL: process.env.API_BASE_URL || ''
}
}
},
i18n: {
vueI18n: './i18n.config.ts'
},
typescript: {
typeCheck: true
},
vite: {
plugins: [
AutoImport({
imports: [
{
'naive-ui': [
'useDialog',
'useMessage',
'useNotification',
'useLoadingBar'
]
}
]
}),
Components({
resolvers: [NaiveUiResolver()]
})
]
}
})
+12898
View File
File diff suppressed because it is too large Load Diff
+34
View File
@@ -0,0 +1,34 @@
{
"name": "nuxt-app",
"private": true,
"type": "module",
"scripts": {
"build": "nuxt build",
"dev": "nuxt dev",
"generate": "nuxt generate",
"preview": "nuxt preview",
"postinstall": "nuxt prepare"
},
"dependencies": {
"@nuxt/icon": "^1.9.1",
"@nuxtjs/i18n": "^9.1.1",
"@nuxtjs/tailwindcss": "^6.12.2",
"@pinia/nuxt": "^0.9.0",
"@vueuse/nuxt": "^12.0.0",
"nuxt": "^3.14.1592",
"nuxtjs-naive-ui": "^1.0.2",
"pinia": "^2.3.0",
"ufo": "^1.5.4",
"vue": "latest",
"vue-router": "latest"
},
"devDependencies": {
"@iconify-json/ri": "^1.2.3",
"naive-ui": "^2.40.3",
"typescript": "^5.7.2",
"unplugin-auto-import": "^0.18.6",
"unplugin-vue-components": "^0.27.5",
"vfonts": "^0.0.3",
"vue-tsc": "^2.1.10"
}
}
+98
View File
@@ -0,0 +1,98 @@
<script lang="ts" setup>
setPageLayout('plain')
const { $api } = useNuxtApp()
import { ref } from 'vue'
const method = ref('credentials')
const credentials = ref({
accessKey: '',
secretKey: '',
})
const sts = ref({
accessKey: '',
secretKey: '',
sessionToken: '',
})
const options = ref([
{ label: 'Login with credentials', value: 'credentials' },
{ label: 'Login with STS', value: 'sts' },
])
const message = useMessage()
const handleLogin = async () => {
console.log('login', method.value, credentials.value, sts.value)
const body = method.value === 'credentials' ? credentials.value : sts.value
try {
await $api.post('login', body)
message.success('Login success')
window.location.href = '/'
} catch (error) {
message.error('Login failed')
}
}
</script>
<template>
<div class="flex flex-col items-center justify-center min-h-screen bg-gray-100 relative">
<img src="~/assets/backgrounds/gradient-waves.svg" class="absolute z-0 top-0 left-0 w-full h-full object-cover" />
<div class="mt-7 w-full max-w-sm z-10 bg-white shadow-sm dark:bg-neutral-900 dark:border-neutral-700">
<div class="p-4 sm:p-7">
<div class="text-center">
<h1 class="block text-2xl font-bold text-gray-800 dark:text-white">Sign in</h1>
<p class="mt-2 text-sm text-gray-600 dark:text-neutral-400">
Welcome back! <br>
please sign in to your account
</p>
</div>
<div class="mt-5 space-y-4">
<n-select v-model:value="method" :options="options" />
<!-- Form -->
<form @submit.prevent="handleLogin">
<div class="grid gap-y-6">
<template v-if="method == 'credentials'">
<div>
<label for="accessKey" class="block text-sm mb-2 dark:text-white">Access Key</label>
<n-input v-model:value="credentials.accessKey" type="text" placeholder="Please input you access key" />
</div>
<div>
<div class="flex justify-between items-center">
<label for="secretKey" class="block text-sm mb-2 dark:text-white">Access Secret</label>
</div>
<n-input v-model:value="credentials.secretKey" type="password" placeholder="Please input you access secret" />
</div>
</template>
<template v-else>
<div>
<label for="accessKey" class="block text-sm mb-2 dark:text-white">STS Access Key</label>
<n-input v-model:value="sts.accessKey" type="text" placeholder="Please input you STS access key" />
</div>
<div>
<label for="sts.secretKey" class="block text-sm mb-2 dark:text-white">STS Access Secret</label>
<n-input v-model:value="sts.secretKey" type="password" placeholder="Please input you STS access key" />
</div>
<div>
<label for="sessionToken" class="block text-sm mb-2 dark:text-white">STS sessionToken</label>
<n-input v-model:value="sts.sessionToken" type="text" placeholder="Please input you STS access key" />
</div>
</template>
<button type="submit"
class="w-full py-3 px-4 inline-flex justify-center items-center gap-x-2 text-sm font-medium rounded-lg border border-transparent bg-blue-600 text-white hover:bg-blue-700 focus:outline-none focus:bg-blue-700 disabled:opacity-50 disabled:pointer-events-none">Sign
in</button>
</div>
</form>
<!-- End Form -->
</div>
</div>
</div>
</div>
</template>
+5
View File
@@ -0,0 +1,5 @@
<template>
<div>
key
</div>
</template>
+16
View File
@@ -0,0 +1,16 @@
<template>
<div>
buckets
<div>
Error: {{ error }}
Status: {{ status }}
Buckets: {{ buckets }}
</div>
</div>
</template>
<script lang="ts" setup>
const { data: buckets, status, error } = await useAPI('/buckets')
console.log(buckets, status, error);
</script>
+3
View File
@@ -0,0 +1,3 @@
<template>
<h1>hello</h1>
</template>
+25
View File
@@ -0,0 +1,25 @@
import ApiClient from "~/lib/api-client"
export default defineNuxtPlugin((nuxtApp) => {
const token = useCookie('token')
const api = new ApiClient(
'/api',
token.value as string | undefined,
{
async onResponseError({ response }: { response: { status: number } }) {
if (response.status === 401) {
await nuxtApp.runWithContext(() => navigateTo('/auth/login'))
}
}
}
)
// Expose to useNuxtApp().$api, useNuxtApp().$apiFetch
return {
provide: {
api,
apiFetch: api.getInstance()
}
}
})
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

+1
View File
@@ -0,0 +1 @@
+11
View File
@@ -0,0 +1,11 @@
import { joinURL } from "ufo";
export default defineEventHandler(async (event) => {
const proxyUrl = useRuntimeConfig().public.api.baseURL;
const target = joinURL(proxyUrl, event.path.replace('/api', ''));
console.log('Proxying request to:', target);
return proxyRequest(event, target);
})
+3
View File
@@ -0,0 +1,3 @@
{
"extends": "../.nuxt/tsconfig.server.json"
}
+23
View File
@@ -0,0 +1,23 @@
import { defineStore } from "pinia";
import { ref, watch } from "vue";
export const useSidebarStore = defineStore(
"sidebar",
() => {
const isCollapsed = ref(localStorage.getItem("sidebarCollapsed") === "true");
const toggleSidebar = () => {
isCollapsed.value = !isCollapsed.value;
};
const setSidebarState = (collapsed: boolean) => {
isCollapsed.value = collapsed;
};
watch(isCollapsed, (newValue) => {
localStorage.setItem("sidebarCollapsed", newValue.toString());
});
return { isCollapsed, toggleSidebar, setSidebarState };
}
);
+24
View File
@@ -0,0 +1,24 @@
import { defineStore } from "pinia";
export const useThemeStore = defineStore(
"theme",
() => {
const appTheme = ref();
const setTheme = (theme: "dark" | "light") => {
appTheme.value = theme;
};
const isDarkTheme = () => {
return appTheme.value === "dark";
};
watch(appTheme, (value, oldValue) => {
document.body.setAttribute("data-theme", value);
document.body.classList.remove(oldValue);
document.body.classList.add(value);
});
return { appTheme, setTheme, isDarkTheme };
}
);
+17
View File
@@ -0,0 +1,17 @@
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./components/**/*.{js,vue,ts}",
"./layouts/**/*.vue",
"./pages/**/*.vue",
"./plugins/**/*.{js,ts}",
"./nuxt.config.{js,ts}",
"./app.vue",
],
darkMode: 'class', // or 'media' or 'class'
theme: {
extend: {},
},
plugins: [],
}
+4
View File
@@ -0,0 +1,4 @@
{
// https://nuxt.com/docs/guide/concepts/typescript
"extends": "./.nuxt/tsconfig.json"
}
+37
View File
@@ -0,0 +1,37 @@
/**
* 将给定的数组转换为树形结构。
* @param arr - 原始数组,其中每个元素包含id和pid属性,pid表示父级id。
* @returns 返回转换后的树形结构数组。
*/
export function arrayToTree(arr: any[]) {
// 初始化结果数组
const res: any = []
// 使用Map存储数组元素,以id为键,元素本身为值
const map = new Map()
// 遍历数组,将每个元素以id为键存储到Map中
arr.forEach((item) => {
map.set(item.id, item)
})
// 再次遍历数组,根据pid将元素组织成树形结构
arr.forEach((item) => {
// 获取当前元素的父级元素
const parent = item.pid && map.get(item.pid)
// 如果有父级元素
if (parent) {
// 如果父级元素已有子元素,则将当前元素追加到子元素数组中
if (parent?.children)
parent.children.push(item)
// 如果父级元素没有子元素,则创建子元素数组,并将当前元素作为第一个元素
else
parent.children = [item]
}
// 如果没有父级元素,则将当前元素直接添加到结果数组中
else {
res.push(item)
}
})
// 返回组织好的树形结构数组
return res
}
+60
View File
@@ -0,0 +1,60 @@
// 定义单位
const UNITS = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']
const K8S_UNITS = ['B', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi', 'Yi']
/**
* 将字节数转换为可读格式
* @param {string} x - 字节数的字符串表示
* @param {boolean} isK8sUnits - 是否使用 K8s 单位
* @returns {string} - 可读格式的字节数
*/
export function niceBytes(x: string, isK8sUnits: boolean = false): string {
const n = Number.parseInt(x, 10) || 0
return formatBytes(n, isK8sUnits)
}
/**
* 将整数字节数转换为可读格式
* @param {number} n - 字节数
* @param {boolean} isK8sUnits - 是否使用 K8s 单位
* @returns {string} - 可读格式的字节数
*/
export function formatBytes(n: number, isK8sUnits: boolean = false): string {
const l = Math.floor(Math.log(n) / Math.log(1024)) || 0
if (l < 0) {
return '0 B'
}
const value = n / 1024 ** l
return `${value.toFixed(1)} ${isK8sUnits ? K8S_UNITS[l] : UNITS[l]}`
}
/**
* 将值和单位转换为字节数的字符串表示
* @param {string} value - 数值的字符串表示
* @param {string} unit - 单位
* @param {boolean} fromK8s - 是否从 K8s 单位转换
* @returns {string} - 字节数的字符串表示
*/
export function getBytes(value: string, unit: string, fromK8s: boolean = false): string {
return convertToBytes(value, unit, fromK8s).toString(10)
}
/**
* 将值和单位转换为字节数
* @param {string} value - 数值的字符串表示
* @param {string} unit - 单位
* @param {boolean} fromK8s - 是否从 K8s 单位转换
* @returns {number} - 字节数
*/
function convertToBytes(value: string, unit: string, fromK8s: boolean = false): number {
const vl: number = Number.parseFloat(value)
const unitsTake = fromK8s ? K8S_UNITS : UNITS
const powFactor = unitsTake.findIndex(element => element === unit)
if (powFactor === -1) {
return 0
}
return vl * 1024 ** powFactor
}
+3
View File
@@ -0,0 +1,3 @@
export * from './array'
export * from './functions'
+13
View File
@@ -0,0 +1,13 @@
import { Icon } from "#components";
export function icon(name: string) {
if (!name.includes(':')) {
name = 'ri:' + name;
}
return h(Icon, { name });
}
export function iconRender(name: string) {
return () => icon(name);
}