mirror of
https://github.com/rustfs/console.git
synced 2026-11-03 06:33:31 +08:00
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:
+6
-5
@@ -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',
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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
@@ -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": {
|
||||
|
||||
Generated
+1121
-1711
File diff suppressed because it is too large
Load Diff
Vendored
-7
@@ -8,10 +8,3 @@ export interface NavItem {
|
||||
key?: string
|
||||
children?: NavItem[]
|
||||
}
|
||||
|
||||
export interface AppConfig {
|
||||
navs: NavItem[]
|
||||
name: string
|
||||
description: string
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user