Fix PageHeader collision with Sidebar, update dependencies, and clean up code (#44)

* fix: PageHeader overlaps Sidebar on small screens

* refactor: move appName and appDescription to defineNuxtConfig

* chore: replace clipboard with vueuse useClipboard

* chore(deps): remove unused deps

* chore: bump deps

Add type conversion `... as Shape` to AutoForm and AutoFormFiledObject for compatibility with vue-tsc v3.

Add `import ‘vue-sonner/style.css’` for compatibility with vue-sonner v2.

Move @aws-sdk/types to dependencies.

Format code.
This commit is contained in:
nakrovati
2026-01-25 09:23:27 +08:00
committed by GitHub
parent bd3ea4b0d1
commit 94fcbd9e57
13 changed files with 1190 additions and 1830 deletions
+6 -5
View File
@@ -1,8 +1,9 @@
import navs from '~/config/navs'
import type { AppConfig } from '~/types/app-config'
export default defineAppConfig<AppConfig>({
navs: navs,
name: process.env.APP_NAME || 'RustFS',
description: process.env.APP_DESCRIPTION || 'RustFS is a distributed file system written in Rust.',
export default defineAppConfig({
navs,
icon: {
mode: 'css',
cssLayer: 'base',
},
})
+8 -7
View File
@@ -22,15 +22,16 @@ import {
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRoute } from 'vue-router'
import type { AppConfig, NavItem } from '~/types/app-config'
import type { NavItem } from '~/types/app-config'
const appConfig = useAppConfig() as unknown as AppConfig
const appConfig = useAppConfig()
const config = useRuntimeConfig()
const route = useRoute()
const { t } = useI18n()
const { state } = useSidebar()
const { isAdmin, canAccessPath } = usePermissions()
const isCollapsed = computed(() => state.value === 'collapsed')
const brandInitial = computed(() => appConfig.name?.charAt(0)?.toUpperCase() ?? 'R')
const brandInitial = computed(() => config.public.appName.charAt(0).toUpperCase() ?? 'R')
const navGroups = computed(() => {
const groups: NavItem[][] = []
@@ -42,11 +43,11 @@ const navGroups = computed(() => {
if (nav.children?.length) {
visibleChildren = nav.children.filter(child => {
if (child.to && !canAccessPath(child.to)) return false
// If child is admin only and user is not admin, but has permission (checked above), allow it.
// If child has no path (unlikely for leaf) and is admin only, hide it.
if (child.isAdminOnly && !isAdmin.value && !child.to) return false
return true
})
@@ -59,10 +60,10 @@ const navGroups = computed(() => {
if (nav.to && !canAccessPath(nav.to)) {
continue
}
// If no path and no children (e.g. divider), check isAdminOnly
if (!nav.to && nav.isAdminOnly && !isAdmin.value) {
continue
continue
}
}
+19 -30
View File
@@ -4,8 +4,6 @@ import { Input } from '@/components/ui/input'
import { Icon } from '#components'
import { useMessage } from '@/lib/ui/message'
import ClipboardJS from 'clipboard'
import { onMounted, onUnmounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
@@ -31,50 +29,41 @@ const props = defineProps({
const model = defineModel<string>()
const message = useMessage()
const clipboard = ref<ClipboardJS | null>(null)
onMounted(() => {
clipboard.value = new ClipboardJS(props.copyIcon ? `#${props.id}-btn-icon` : `#${props.id}-btn`)
const { copy } = useClipboard()
async function handleCopyGeneratedUrl() {
try {
if (!model.value) throw new Error('No value to copy')
await copy(model.value)
clipboard.value.on('success', function (e) {
message.success(t('Copy Success'))
e.clearSelection()
})
clipboard.value.on('error', async function () {
// If copy fails, use navigator.clipboard.writeText as fallback
try {
if (navigator.clipboard && model.value) {
await navigator.clipboard.writeText(model.value)
message.success(t('Copy Success'))
} else {
message.error(t('Copy Failed'))
}
} catch {
message.error(t('Copy Failed'))
}
})
})
onUnmounted(() => {
clipboard.value?.destroy()
clipboard.value = null
})
} catch (error) {
message.error(t('Copy Failed'))
}
}
</script>
<template>
<div class="flex h-full items-center gap-2">
<Input v-model="model" :readonly="props.readonly" :id="props.id" class="flex-1" />
<Button v-if="!props.copyIcon" :id="`${props.id}-btn`" :data-clipboard-target="`#${props.id}`" variant="default">
<Button
v-if="!props.copyIcon"
@click="handleCopyGeneratedUrl"
:id="`${props.id}-btn`"
:data-clipboard-target="`#${props.id}`"
variant="default"
>
{{ t('Copy') }}
</Button>
<Button
v-else
@click="handleCopyGeneratedUrl"
:id="`${props.id}-btn-icon`"
variant="ghost"
size="sm"
class="shrink-0 outline"
:data-clipboard-target="`#${props.id}`"
:title="t('Copy')"
>
<Icon :size="18" name="ri:file-copy-line" />
+4 -36
View File
@@ -13,11 +13,10 @@
</div>
<Button
v-if="props.objectKey"
:id="`copy-path-btn-${buttonId}`"
variant="ghost"
size="sm"
class="shrink-0"
:data-clipboard-text="fullPath"
@click="copy(fullPath)"
:title="t('Copy Path')"
>
<Icon v-if="!copied" :size="16" name="ri:file-copy-line" />
@@ -30,8 +29,7 @@
<script lang="ts" setup>
import { Button } from '@/components/ui/button'
import { Icon } from '#components'
import ClipboardJS from 'clipboard'
import { onMounted, onUnmounted, ref, computed } from 'vue'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
@@ -42,10 +40,6 @@ const props = defineProps<{
onClick: (path: string) => any
}>()
const clipboard = ref<ClipboardJS | null>(null)
const buttonId = ref(`${Math.random().toString(36).substring(2, 15)}`)
const copied = ref(false)
const fullPath = computed(() => {
if (props.bucketName) {
return `${props.bucketName}/${props.objectKey}`
@@ -65,6 +59,8 @@ const segments = computed(() =>
})
)
const { copy, copied } = useClipboard({ copiedDuring: 3000 })
const displaySegments = computed(() => {
if (segments.value.length <= 6) {
return segments.value
@@ -84,32 +80,4 @@ const handleOnClick = (segment: { value: string; index: number }) => {
.join('/')
props.onClick(path)
}
const showCopiedState = () => {
copied.value = true
setTimeout(() => {
copied.value = false
}, 3000)
}
onMounted(() => {
clipboard.value = new ClipboardJS(`#copy-path-btn-${buttonId.value}`)
clipboard.value.on('success', function (e) {
showCopiedState()
e.clearSelection()
})
clipboard.value.on('error', async function () {
if (navigator.clipboard && fullPath.value) {
await navigator.clipboard.writeText(fullPath.value)
showCopiedState()
}
})
})
onUnmounted(() => {
clipboard.value?.destroy()
clipboard.value = null
})
</script>
+1
View File
@@ -12,6 +12,7 @@ import { Toaster } from '@/components/ui/sonner'
import { createDialogController, dialogControllerKey } from '@/lib/ui/dialog'
import { createMessageApi, messageInjectionKey } from '@/lib/ui/message'
import { provide } from 'vue'
import 'vue-sonner/style.css'
const dialogController = createDialogController()
const messageApi = createMessageApi()
+2 -2
View File
@@ -80,7 +80,7 @@ const formComponentProps = computed(() => {
<template>
<component :is="formComponent" v-bind="formComponentProps">
<slot name="customAutoForm" :fields="fields">
<template v-for="(shape, key) of shapes" :key="key">
<template v-for="(shape, key) in shapes" :key="key">
<slot
:shape="shape"
:name="key.toString() as keyof z.infer<T>"
@@ -90,7 +90,7 @@ const formComponentProps = computed(() => {
<AutoFormField
:config="fieldConfig?.[key as keyof typeof fieldConfig] as ConfigItem"
:field-name="key.toString()"
:shape="shape"
:shape="shape as Shape"
/>
</slot>
</template>
@@ -62,7 +62,7 @@ provide(FieldContextKey, fieldContext)
</AccordionTrigger>
<AccordionContent>
<template v-for="(field, index) of fields" :key="field.key">
<template v-for="(field, index) in fields" :key="field.key">
<div class="mb-4 p-1">
<AutoFormField
:field-name="`${fieldName}[${index}]`"
@@ -63,7 +63,7 @@ provide(FieldContextKey, fieldContext)
:config="config?.[key as keyof typeof config] as ConfigItem"
:field-name="`${fieldName}.${key.toString()}`"
:label="key.toString()"
:shape="shape"
:shape="shape as Shape"
/>
</template>
</AccordionContent>
+1 -1
View File
@@ -66,7 +66,7 @@ const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
<div
:class="
cn(
'duration-200 fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] ease-linear md:flex',
'duration-200 fixed inset-y-0 z-20 hidden h-svh w-(--sidebar-width) transition-[left,right,width] ease-linear md:flex',
side === 'left'
? 'left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]'
: 'right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]',
+4 -1
View File
@@ -1,7 +1,8 @@
import tailwindcss from '@tailwindcss/vite'
const appName = process.env.APP_NAME || 'RustFS'
const baseUrl = (process.env.BASE_URL || '/rustfs/console/').replace(/\/$/, '')
const appDescription = process.env.APP_DESCRIPTION || 'RustFS is a distributed file system written in Rust.'
const baseUrl = (process.env.BASE_URL || '/rustfs/console/').replace(/\/$/, '')
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
@@ -32,6 +33,8 @@ export default defineNuxtConfig({
plugins: [],
runtimeConfig: {
public: {
appName,
appDescription,
session: {
// 临时凭证有效期
durationSeconds: Number(process.env.SESSION_DURATION_SECONDS) || 3600 * 12,
+22 -28
View File
@@ -25,71 +25,65 @@
"@aws-sdk/client-s3": "^3.844.0",
"@aws-sdk/client-sts": "^3.844.0",
"@aws-sdk/s3-request-presigner": "^3.844.0",
"@iconify-json/ri": "^1.2.5",
"@aws-sdk/types": "^3.914.0",
"@iconify-json/ri": "^1.2.7",
"@iconify/vue": "^5.0.0",
"@internationalized/date": "^3.10.0",
"@nuxt/icon": "^2.2.0",
"@internationalized/date": "^3.10.1",
"@nuxt/icon": "^2.2.1",
"@nuxt/image": "^2.0.0",
"@nuxtjs/i18n": "^10.2.1",
"@pinia/nuxt": "^0.11.3",
"@smithy/protocol-http": "^5.1.2",
"@smithy/protocol-http": "^5.3.8",
"@tanstack/vue-table": "^8.21.3",
"@unovis/ts": "^1.6.1",
"@unovis/vue": "^1.6.1",
"@unovis/ts": "^1.6.2",
"@unovis/vue": "^1.6.2",
"@vee-validate/zod": "^4.15.1",
"@vueuse/core": "^14.1.0",
"@vueuse/integrations": "^14.1.0",
"@vueuse/nuxt": "^14.1.0",
"axios": "^1.10.0",
"class-variance-authority": "^0.7.1",
"clipboard": "^2.0.11",
"clsx": "^2.1.1",
"dayjs": "^1.11.13",
"dayjs": "^1.11.19",
"dotted-map": "^2.2.3",
"echarts": "^5.6.0",
"echarts": "^6.0.0",
"embla-carousel-vue": "^8.6.0",
"file-saver": "^2.0.5",
"json-editor-vue": "^0.18.1",
"jszip": "^3.10.1",
"lucide-vue-next": "^0.487.0",
"motion-v": "^1.5.0",
"node-forge": "^1.3.2",
"nuxt": "^4.2.2",
"lucide-vue-next": "^0.563.0",
"motion-v": "^1.9.0",
"node-forge": "^1.3.3",
"nuxt": "^4.3.0",
"pinia": "^3.0.4",
"postprocessing": "^6.37.6",
"reka-ui": "^2.6.0",
"strip-json-comments": "^5.0.2",
"reka-ui": "^2.7.0",
"tailwind-merge": "^3.4.0",
"three": "^0.175.0",
"three-globe": "^2.43.0",
"ufo": "^1.6.1",
"ufo": "^1.6.3",
"vaul-vue": "^0.4.1",
"vee-validate": "^4.15.1",
"vue": "^3.5.27",
"vue-router": "^4.6.4",
"vue-sonner": "^1.3.2",
"vue-sonner": "^2.0.9",
"zod": "^3.25.76"
},
"devDependencies": {
"@aws-sdk/types": "^3.840.0",
"@oxc-minify/binding-darwin-arm64": "^0.95.0",
"@oxc-transform/binding-darwin-arm64": "^0.95.0",
"@tailwindcss/vite": "^4.1.18",
"@types/file-saver": "^2.0.7",
"@types/node-forge": "^1.3.12",
"@types/three": "^0.182.0",
"@vitalets/google-translate-api": "^9.2.1",
"@vitest/coverage-v8": "^3.2.4",
"@vitest/ui": "^3.2.4",
"@vitest/coverage-v8": "^4.0.18",
"@vitest/ui": "^4.0.18",
"c8": "^10.1.3",
"jsdom": "^27.0.0",
"prettier": "^3.2.5",
"jsdom": "^27.4.0",
"prettier": "^3.8.1",
"shadcn-nuxt": "^2.4.3",
"tailwindcss": "^4.1.18",
"tw-animate-css": "^1.4.0",
"typescript": "^5.8.3",
"vitest": "^3.2.4",
"vue-tsc": "^2.2.12"
"vitest": "^4.0.18",
"vue-tsc": "^3.2.3"
},
"packageManager": "pnpm@10.27.0",
"engines": {
+1121 -1711
View File
File diff suppressed because it is too large Load Diff
-7
View File
@@ -8,10 +8,3 @@ export interface NavItem {
key?: string
children?: NavItem[]
}
export interface AppConfig {
navs: NavItem[]
name: string
description: string
[key: string]: any
}