Sfoglia il codice sorgente

feat: 添加组件

DESKTOP-USV654P\pc 9 mesi fa
parent
commit
b386ad481c

+ 13 - 0
apps/web-baicai/src/api/system/cache.ts

@@ -0,0 +1,13 @@
+import { requestClient } from '#/api/request';
+
+export namespace CacheApi {
+  export const getList = () => requestClient.get<string[]>('/cache/list');
+
+  export const getDetail = (key: string) =>
+    requestClient.get<Record<string, any>>('/cache/value', {
+      params: { key },
+    });
+
+  export const deleteDetail = (key: string) =>
+    requestClient.delete('/cache', { data: { key } });
+}

+ 1 - 0
apps/web-baicai/src/api/system/index.ts

@@ -1,3 +1,4 @@
+export * from './cache';
 export * from './config';
 export * from './database';
 export * from './department';

+ 50 - 0
apps/web-baicai/src/views/system/cache/index.vue

@@ -0,0 +1,50 @@
+<script lang="ts" setup>
+import { onMounted, reactive } from 'vue';
+
+import { JsonViewer, LayoutCard, Nav, VbenScrollbar } from '@vben/common-ui';
+
+import { CacheApi } from '#/api';
+
+const props = reactive({
+  defaultLayout: [20, 80, 0],
+});
+
+const state = reactive<{ jsonValue: any; navList: any[] }>({
+  navList: [],
+  jsonValue: {},
+});
+
+const fetch = async () => {
+  const data = await CacheApi.getList();
+  state.navList = data.map((item) => {
+    return { title: item };
+  });
+  if (state.navList.length > 0) {
+    await handleChange(state.navList[0]);
+  }
+};
+
+const handleChange = async (item: Record<string, any>) => {
+  state.jsonValue = await CacheApi.getDetail(item.title);
+};
+onMounted(async () => {
+  await fetch();
+});
+</script>
+<template>
+  <LayoutCard auto-content-height v-bind="props">
+    <template #left="{ isCollapsed, expand }">
+      <VbenScrollbar class="h-full">
+        <Nav
+          :is-collapsed="isCollapsed"
+          @collapse="expand"
+          :items="state.navList"
+          @change="handleChange"
+        />
+      </VbenScrollbar>
+    </template>
+    <div class="m-4">
+      <JsonViewer :value="state.jsonValue" />
+    </div>
+  </LayoutCard>
+</template>

+ 3 - 0
packages/effects/common-ui/src/components/index.ts

@@ -5,7 +5,9 @@ export * from './count-to';
 export * from './ellipsis-text';
 export * from './icon-picker';
 export * from './json-viewer';
+export * from './layout-card';
 export * from './loading';
+export * from './nav';
 export * from './page';
 export * from './resize';
 export * from './tippy';
@@ -21,6 +23,7 @@ export {
   VbenInputPassword,
   VbenLoading,
   VbenPinInput,
+  VbenScrollbar,
   VbenSpinner,
 } from '@vben-core/shadcn-ui';
 

+ 2 - 0
packages/effects/common-ui/src/components/layout-card/index.ts

@@ -0,0 +1,2 @@
+export { default as LayoutCard } from './index.vue';
+export * from './types';

+ 135 - 0
packages/effects/common-ui/src/components/layout-card/index.vue

@@ -0,0 +1,135 @@
+<script lang="ts" setup>
+import type { LayoutCardProps } from './types';
+
+import { computed, ref, useSlots } from 'vue';
+
+import {
+  ResizableHandle,
+  ResizablePanel,
+  ResizablePanelGroup,
+  VbenScrollbar,
+} from '@vben-core/shadcn-ui';
+import { cn } from '@vben-core/shared/utils';
+
+import Page from '../page/page.vue';
+
+defineOptions({
+  name: 'LayoutCard',
+  inheritAttrs: false,
+});
+
+const props = withDefaults(defineProps<LayoutCardProps>(), {
+  defaultCollapsed: false,
+  defaultLayout: () => [20, 60, 20],
+});
+
+const slots = useSlots();
+
+const delegatedProps = computed(() => {
+  const { defaultLayout: _, ...delegated } = props;
+  return delegated;
+});
+
+const delegatedSlots = computed(() => {
+  const resultSlots: string[] = [];
+
+  for (const key of Object.keys(slots)) {
+    if (!['default', 'left', 'right'].includes(key)) {
+      resultSlots.push(key);
+    }
+  }
+  return resultSlots;
+});
+
+const leftPanelRef = ref<InstanceType<typeof ResizablePanel>>();
+
+function expandLeft() {
+  leftPanelRef.value?.expand();
+}
+
+function collapseLeft() {
+  leftPanelRef.value?.collapse();
+}
+
+const isCollapsed = ref(props.defaultCollapsed);
+const onExpand = () => {
+  isCollapsed.value = false;
+};
+
+const onCollapse = () => {
+  isCollapsed.value = true;
+};
+
+defineExpose({
+  expandLeft,
+  collapseLeft,
+});
+</script>
+<template>
+  <Page v-bind="delegatedProps">
+    <!-- 继承默认的slot -->
+    <template
+      v-for="slotName in delegatedSlots"
+      :key="slotName"
+      #[slotName]="slotProps"
+    >
+      <slot :name="slotName" v-bind="slotProps"></slot>
+    </template>
+    <div
+      class="bg-background h-full overflow-hidden rounded-[0.5rem] border shadow"
+    >
+      <ResizablePanelGroup direction="horizontal" class="h-full items-stretch">
+        <ResizablePanel
+          ref="leftPanelRef"
+          :default-size="defaultLayout[0]"
+          :collapsed-size="leftCollapsedWidth"
+          collapsible
+          :min-size="15"
+          :max-size="defaultLayout[0]"
+          :class="
+            cn(
+              isCollapsed &&
+                'min-w-[50px] transition-all duration-300 ease-in-out',
+            )
+          "
+          @expand="onExpand"
+          @collapse="onCollapse"
+          v-if="$slots.left"
+        >
+          <VbenScrollbar class="h-full">
+            <template #default="slotProps">
+              <slot
+                name="left"
+                v-bind="{
+                  ...slotProps,
+                  isCollapsed,
+                  expand: expandLeft,
+                  collapse: collapseLeft,
+                }"
+              ></slot>
+            </template>
+          </VbenScrollbar>
+        </ResizablePanel>
+        <ResizableHandle with-handle v-if="$slots.left" />
+        <ResizablePanel :default-size="defaultLayout[1]" :min-size="30">
+          <template #default>
+            <slot></slot>
+          </template>
+        </ResizablePanel>
+        <ResizableHandle with-handle v-if="$slots.right" />
+        <ResizablePanel :default-size="defaultLayout[0]" v-if="$slots.right">
+          <VbenScrollbar class="h-full">
+            <template #default="slotProps">
+              <slot
+                name="right"
+                v-bind="{
+                  ...slotProps,
+                }"
+              ></slot>
+            </template>
+          </VbenScrollbar>
+        </ResizablePanel>
+      </ResizablePanelGroup>
+    </div>
+  </Page>
+</template>

+ 7 - 0
packages/effects/common-ui/src/components/layout-card/types.ts

@@ -0,0 +1,7 @@
+import type { PageProps } from '../page/types';
+
+export interface LayoutCardProps extends PageProps {
+  defaultLayout?: number[];
+  leftCollapsedWidth?: number;
+  defaultCollapsed?: boolean;
+}

+ 2 - 0
packages/effects/common-ui/src/components/nav/index.ts

@@ -0,0 +1,2 @@
+export { default as Nav } from './index.vue';
+export * from './types';

+ 124 - 0
packages/effects/common-ui/src/components/nav/index.vue

@@ -0,0 +1,124 @@
+<script lang="ts" setup>
+import type { NavItem, NavProps } from './types';
+
+import { reactive } from 'vue';
+
+import { IconifyIcon } from '@vben/icons';
+
+import {
+  buttonVariants,
+  VbenScrollbar,
+  VbenTooltip,
+} from '@vben-core/shadcn-ui';
+import { cn } from '@vben-core/shared/utils';
+
+withDefaults(defineProps<NavProps>(), {
+  isCollapsed: false,
+});
+
+const emit = defineEmits(['change', 'collapse']);
+const state = reactive({
+  currentIndex: 0,
+});
+
+const handleClick = (index: number, item: NavItem) => {
+  state.currentIndex = index;
+  emit('change', item);
+};
+
+const handleCollapse = () => {
+  emit('collapse');
+};
+</script>
+
+<template>
+  <div :data-collapsed="isCollapsed" class="group flex h-full flex-col gap-4">
+    <div
+      class="flex h-[45px] items-center border-b px-4 py-2 shadow group-[[data-collapsed=true]]:px-2"
+    >
+      <IconifyIcon
+        v-if="isCollapsed"
+        icon="bxs:right-arrow-square"
+        class="size-8"
+        @click="handleCollapse"
+      />
+      <h1 v-else class="text-xl font-bold">缓存列表</h1>
+    </div>
+    <VbenScrollbar class="h-[calc(100% - 45px)]">
+      <nav
+        class="grid gap-1 px-2 group-[[data-collapsed=true]]:justify-center group-[[data-collapsed=true]]:px-2"
+      >
+        <template v-for="(link, index) of items">
+          <VbenTooltip v-if="isCollapsed" :key="`1-${index}`">
+            <template #trigger>
+              <a
+                href="#"
+                :class="
+                  cn(
+                    buttonVariants({
+                      variant:
+                        state.currentIndex === index ? 'default' : 'ghost',
+                      size: 'icon',
+                    }),
+                    'h-9 w-9',
+                    state.currentIndex === index &&
+                      'dark:bg-muted dark:text-muted-foreground dark:hover:bg-muted dark:hover:text-white',
+                  )
+                "
+              >
+                <IconifyIcon
+                  :icon="link.icon || 'ic:baseline-more-horiz'"
+                  class="size-4"
+                />
+                <span class="sr-only">{{ link.title }}</span>
+              </a>
+            </template>
+            <div class="flex items-center gap-4">
+              {{ link.title }}
+              <span v-if="link.label" class="text-muted-foreground ml-auto">
+                {{ link.label }}
+              </span>
+            </div>
+          </VbenTooltip>
+
+          <a
+            v-else
+            :key="`2-${index}`"
+            href="#"
+            @click="handleClick(index, link)"
+            :class="
+              cn(
+                buttonVariants({
+                  variant: state.currentIndex === index ? 'default' : 'ghost',
+                  size: 'sm',
+                }),
+                state.currentIndex === index &&
+                  'dark:bg-muted dark:hover:bg-muted dark:text-white dark:hover:text-white',
+                'justify-start',
+              )
+            "
+          >
+            <IconifyIcon
+              v-if="link.icon"
+              :icon="link.icon"
+              class="mr-2 size-4"
+            />
+            {{ link.title }}
+            <span
+              v-if="link.label"
+              :class="
+                cn(
+                  'ml-auto',
+                  state.currentIndex === index &&
+                    'text-background dark:text-white',
+                )
+              "
+            >
+              {{ link.label }}
+            </span>
+          </a>
+        </template>
+      </nav>
+    </VbenScrollbar>
+  </div>
+</template>

+ 11 - 0
packages/effects/common-ui/src/components/nav/types.ts

@@ -0,0 +1,11 @@
+export interface NavItem {
+  title: string;
+  label?: string;
+  icon?: string;
+  [key: string]: any;
+}
+
+export interface NavProps {
+  isCollapsed?: boolean;
+  items: NavItem[];
+}