Forráskód Böngészése

chore: 同步github

赖奇 1 éve
szülő
commit
dbb3458aeb

+ 4 - 0
apps/web-ele/src/bootstrap.ts

@@ -2,6 +2,7 @@ import { createApp, watchEffect } from 'vue';
 
 import { registerAccessDirective } from '@vben/access';
 import { initTippy } from '@vben/common-ui';
+import { MotionPlugin } from '@vben/plugins/motion';
 import { preferences } from '@vben/preferences';
 import { initStores } from '@vben/stores';
 import '@vben/styles';
@@ -47,6 +48,9 @@ async function bootstrap(namespace: string) {
   // 配置路由及路由守卫
   app.use(router);
 
+  // 配置Motion插件
+  app.use(MotionPlugin);
+
   // 动态更新标题
   watchEffect(() => {
     if (preferences.app.dynamicTitle) {

+ 1 - 1
packages/@core/ui-kit/form-ui/src/form-render/form-field.vue

@@ -336,7 +336,7 @@ function autofocus() {
               >
                 <VbenRenderContent
                   :content="customContentRender[name]"
-                  v-bind="{ ...renderSlotProps, $formContext: slotProps }"
+                  v-bind="{ ...renderSlotProps, formContext: slotProps }"
                 />
               </template>
               <!-- <slot></slot> -->

+ 1 - 0
packages/effects/common-ui/package.json

@@ -39,6 +39,7 @@
     "qrcode": "catalog:",
     "tippy.js": "catalog:",
     "vue": "catalog:",
+    "vue-json-viewer": "catalog:",
     "vue-router": "catalog:",
     "vue-tippy": "catalog:"
   },

+ 123 - 0
packages/effects/common-ui/src/components/count-to/count-to.vue

@@ -0,0 +1,123 @@
+<script lang="ts" setup>
+import type { CountToProps } from './types';
+
+import { computed, onMounted, ref, watch } from 'vue';
+
+import { isString } from '@vben-core/shared/utils';
+
+import { TransitionPresets, useTransition } from '@vueuse/core';
+
+const props = withDefaults(defineProps<CountToProps>(), {
+  startVal: 0,
+  duration: 2000,
+  separator: ',',
+  decimal: '.',
+  decimals: 0,
+  delay: 0,
+  transition: () => TransitionPresets.easeOutExpo,
+});
+
+const emit = defineEmits(['started', 'finished']);
+
+const lastValue = ref(props.startVal);
+
+onMounted(() => {
+  lastValue.value = props.endVal;
+});
+
+watch(
+  () => props.endVal,
+  (val) => {
+    lastValue.value = val;
+  },
+);
+
+const currentValue = useTransition(lastValue, {
+  delay: computed(() => props.delay),
+  duration: computed(() => props.duration),
+  disabled: computed(() => props.disabled),
+  transition: computed(() => {
+    return isString(props.transition)
+      ? TransitionPresets[props.transition]
+      : props.transition;
+  }),
+  onStarted() {
+    emit('started');
+  },
+  onFinished() {
+    emit('finished');
+  },
+});
+
+const numMain = computed(() => {
+  const result = currentValue.value
+    .toFixed(props.decimals)
+    .split('.')[0]
+    ?.replaceAll(/\B(?=(\d{3})+(?!\d))/g, ',');
+  return result;
+});
+
+const numDec = computed(() => {
+  return (
+    props.decimal + currentValue.value.toFixed(props.decimals).split('.')[1]
+  );
+});
+</script>
+<template>
+  <div class="count-to" v-bind="$attrs">
+    <slot name="prefix">
+      <div
+        class="count-to-prefix"
+        :style="prefixStyle"
+        :class="prefixClass"
+        v-if="prefix"
+      >
+        {{ prefix }}
+      </div>
+    </slot>
+    <div class="count-to-main" :class="mainClass" :style="mainStyle">
+      <span>{{ numMain }}</span>
+      <span
+        class="count-to-main-decimal"
+        v-if="decimals > 0"
+        :class="decimalClass"
+        :style="decimalStyle"
+      >
+        {{ numDec }}
+      </span>
+    </div>
+    <slot name="suffix">
+      <div
+        class="count-to-suffix"
+        :style="suffixStyle"
+        :class="suffixClass"
+        v-if="suffix"
+      >
+        {{ suffix }}
+      </div>
+    </slot>
+  </div>
+</template>
+<style lang="scss" scoped>
+.count-to {
+  display: flex;
+  align-items: baseline;
+
+  &-prefix {
+    // font-size: 1rem;
+  }
+
+  &-suffix {
+    // font-size: 1rem;
+  }
+
+  &-main {
+    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
+    // font-size: 1.5rem;
+
+    &-decimal {
+      // font-size: 0.8rem;
+    }
+  }
+}
+</style>

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

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

+ 53 - 0
packages/effects/common-ui/src/components/count-to/types.ts

@@ -0,0 +1,53 @@
+import type { CubicBezierPoints, EasingFunction } from '@vueuse/core';
+
+import type { StyleValue } from 'vue';
+
+import { TransitionPresets as TransitionPresetsData } from '@vueuse/core';
+
+export type TransitionPresets = keyof typeof TransitionPresetsData;
+
+export const TransitionPresetsKeys = Object.keys(
+  TransitionPresetsData,
+) as TransitionPresets[];
+
+export interface CountToProps {
+  /** 初始值 */
+  startVal?: number;
+  /** 当前值 */
+  endVal: number;
+  /** 是否禁用动画 */
+  disabled?: boolean;
+  /** 延迟动画开始的时间 */
+  delay?: number;
+  /** 持续时间  */
+  duration?: number;
+  /** 小数位数  */
+  decimals?: number;
+  /** 小数点  */
+  decimal?: string;
+  /** 分隔符  */
+  separator?: string;
+  /** 前缀  */
+  prefix?: string;
+  /** 后缀  */
+  suffix?: string;
+  /** 过渡效果  */
+  transition?: CubicBezierPoints | EasingFunction | TransitionPresets;
+  /** 整数部分的类名 */
+  mainClass?: string;
+  /** 小数部分的类名 */
+  decimalClass?: string;
+  /** 前缀部分的类名 */
+  prefixClass?: string;
+  /** 后缀部分的类名 */
+  suffixClass?: string;
+
+  /** 整数部分的样式 */
+  mainStyle?: StyleValue;
+  /** 小数部分的样式 */
+  decimalStyle?: StyleValue;
+  /** 前缀部分的样式 */
+  prefixStyle?: StyleValue;
+  /** 后缀部分的样式 */
+  suffixStyle?: StyleValue;
+}

+ 6 - 2
packages/effects/common-ui/src/components/ellipsis-text/ellipsis-text.vue

@@ -5,6 +5,8 @@ import { computed, ref, watchEffect } from 'vue';
 
 import { VbenTooltip } from '@vben-core/shadcn-ui';
 
+import { useElementSize } from '@vueuse/core';
+
 interface Props {
   /**
    * 是否启用点击文本展开全部
@@ -78,11 +80,13 @@ const ellipsis = ref();
 const isExpand = ref(false);
 const defaultTooltipMaxWidth = ref();
 
+const { width: eleWidth } = useElementSize(ellipsis);
+
 watchEffect(
   () => {
-    if (props.tooltip && ellipsis.value) {
+    if (props.tooltip && eleWidth.value) {
       defaultTooltipMaxWidth.value =
-        props.tooltipMaxWidth ?? ellipsis.value.offsetWidth + 24;
+        props.tooltipMaxWidth ?? eleWidth.value + 24;
     }
   },
   { flush: 'post' },

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

@@ -1,8 +1,10 @@
 export * from './api-component';
 export * from './captcha';
 export * from './col-page';
+export * from './count-to';
 export * from './ellipsis-text';
 export * from './icon-picker';
+export * from './json-viewer';
 export * from './page';
 export * from './resize';
 export * from './tippy';

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

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

+ 98 - 0
packages/effects/common-ui/src/components/json-viewer/index.vue

@@ -0,0 +1,98 @@
+<script lang="ts" setup>
+import type { SetupContext } from 'vue';
+
+import type { Recordable } from '@vben/types';
+
+import type {
+  JsonViewerAction,
+  JsonViewerProps,
+  JsonViewerToggle,
+  JsonViewerValue,
+} from './types';
+
+import { computed, useAttrs } from 'vue';
+// @ts-ignore
+import VueJsonViewer from 'vue-json-viewer';
+
+import { $t } from '@vben/locales';
+
+import { isBoolean } from '@vben-core/shared/utils';
+
+defineOptions({ name: 'JsonViewer' });
+
+const props = withDefaults(defineProps<JsonViewerProps>(), {
+  expandDepth: 1,
+  copyable: false,
+  sort: false,
+  boxed: false,
+  theme: 'default-json-theme',
+  expanded: false,
+  previewMode: false,
+  showArrayIndex: true,
+  showDoubleQuotes: false,
+});
+
+const emit = defineEmits<{
+  click: [event: MouseEvent];
+  copied: [event: JsonViewerAction];
+  keyClick: [key: string];
+  toggle: [param: JsonViewerToggle];
+  valueClick: [value: JsonViewerValue];
+}>();
+
+const attrs: SetupContext['attrs'] = useAttrs();
+
+function handleClick(event: MouseEvent) {
+  if (
+    event.target instanceof HTMLElement &&
+    event.target.classList.contains('jv-item')
+  ) {
+    const pathNode = event.target.closest('.jv-push');
+    if (!pathNode || !pathNode.hasAttribute('path')) {
+      return;
+    }
+    const param: JsonViewerValue = {
+      path: '',
+      value: '',
+      depth: 0,
+      el: event.target,
+    };
+
+    param.path = pathNode.getAttribute('path') || '';
+    param.depth = Number(pathNode.getAttribute('depth')) || 0;
+
+    param.value = event.target.textContent || undefined;
+    param.value = JSON.parse(param.value);
+    emit('valueClick', param);
+  }
+  emit('click', event);
+}
+
+const bindProps = computed<Recordable<any>>(() => {
+  const copyable = {
+    copyText: $t('ui.jsonViewer.copy'),
+    copiedText: $t('ui.jsonViewer.copied'),
+    timeout: 2000,
+    ...(isBoolean(props.copyable) ? {} : props.copyable),
+  };
+
+  return {
+    ...props,
+    ...attrs,
+    onCopied: (event: JsonViewerAction) => emit('copied', event),
+    onKeyclick: (key: string) => emit('keyClick', key),
+    onClick: (event: MouseEvent) => handleClick(event),
+    copyable: props.copyable ? copyable : false,
+  };
+});
+</script>
+<template>
+  <VueJsonViewer v-bind="bindProps">
+    <template #copy="slotProps">
+      <slot name="copy" v-bind="slotProps"></slot>
+    </template>
+  </VueJsonViewer>
+</template>
+<style lang="scss">
+@use './style.scss';
+</style>

+ 98 - 0
packages/effects/common-ui/src/components/json-viewer/style.scss

@@ -0,0 +1,98 @@
+.default-json-theme {
+  font-family: Consolas, Menlo, Courier, monospace;
+  font-size: 14px;
+  color: hsl(var(--foreground));
+  white-space: nowrap;
+  background: hsl(var(--background));
+
+  &.jv-container.boxed {
+    border: 1px solid hsl(var(--border));
+  }
+
+  .jv-ellipsis {
+    display: inline-block;
+    padding: 0 4px 2px;
+    font-size: 0.9em;
+    line-height: 0.9;
+    color: hsl(var(--secondary-foreground));
+    vertical-align: 2px;
+    cursor: pointer;
+    user-select: none;
+    background-color: hsl(var(--secondary));
+    border-radius: 3px;
+  }
+
+  .jv-button {
+    color: hsl(var(--primary));
+  }
+
+  .jv-key {
+    color: hsl(var(--heavy-foreground));
+  }
+
+  .jv-item {
+    &.jv-array {
+      color: hsl(var(--heavy-foreground));
+    }
+
+    &.jv-boolean {
+      color: hsl(var(--red-400));
+    }
+
+    &.jv-function {
+      color: hsl(var(--destructive-foreground));
+    }
+
+    &.jv-number {
+      color: hsl(var(--info-foreground));
+    }
+
+    &.jv-number-float {
+      color: hsl(var(--info-foreground));
+    }
+
+    &.jv-number-integer {
+      color: hsl(var(--info-foreground));
+    }
+
+    &.jv-object {
+      color: hsl(var(--accent-darker));
+    }
+
+    &.jv-undefined {
+      color: hsl(var(--secondary-foreground));
+    }
+
+    &.jv-string {
+      color: hsl(var(--primary));
+      word-break: break-word;
+      white-space: normal;
+    }
+  }
+
+  &.jv-container .jv-code {
+    padding: 10px;
+
+    &.boxed:not(.open) {
+      padding-bottom: 20px;
+      margin-bottom: 10px;
+    }
+
+    &.open {
+      padding-bottom: 10px;
+    }
+
+    .jv-toggle {
+      &::before {
+        padding: 0 2px;
+        border-radius: 2px;
+      }
+
+      &:hover {
+        &::before {
+          background: hsl(var(--accent-foreground));
+        }
+      }
+    }
+  }
+}

+ 44 - 0
packages/effects/common-ui/src/components/json-viewer/types.ts

@@ -0,0 +1,44 @@
+export interface JsonViewerProps {
+  /** 要展示的结构数据 */
+  value: any;
+  /** 展开深度 */
+  expandDepth?: number;
+  /** 是否可复制 */
+  copyable?: boolean;
+  /** 是否排序 */
+  sort?: boolean;
+  /** 显示边框 */
+  boxed?: boolean;
+  /** 主题 */
+  theme?: string;
+  /** 是否展开 */
+  expanded?: boolean;
+  /** 时间格式化函数 */
+  timeformat?: (time: Date | number | string) => string;
+  /** 预览模式 */
+  previewMode?: boolean;
+  /** 显示数组索引 */
+  showArrayIndex?: boolean;
+  /** 显示双引号 */
+  showDoubleQuotes?: boolean;
+}
+
+export interface JsonViewerAction {
+  action: string;
+  text: string;
+  trigger: HTMLElement;
+}
+
+export interface JsonViewerValue {
+  value: any;
+  path: string;
+  depth: number;
+  el: HTMLElement;
+}
+
+export interface JsonViewerToggle {
+  /** 鼠标事件 */
+  event: MouseEvent;
+  /** 当前展开状态 */
+  open: boolean;
+}

+ 10 - 6
packages/effects/hooks/src/use-hover-toggle.ts

@@ -2,11 +2,11 @@ import type { Arrayable, MaybeElementRef } from '@vueuse/core';
 
 import type { Ref } from 'vue';
 
-import { computed, onUnmounted, ref, watch } from 'vue';
+import { computed, onUnmounted, ref, unref, watch } from 'vue';
 
 import { isFunction } from '@vben/utils';
 
-import { useMouseInElement } from '@vueuse/core';
+import { useElementHover } from '@vueuse/core';
 
 /**
  * 监测鼠标是否在元素内部,如果在元素内部则返回 true,否则返回 false
@@ -18,15 +18,19 @@ export function useHoverToggle(
   refElement: Arrayable<MaybeElementRef>,
   delay: (() => number) | number = 500,
 ) {
-  const isOutsides: Array<Ref<boolean>> = [];
+  const isHovers: Array<Ref<boolean>> = [];
   const value = ref(false);
   const timer = ref<ReturnType<typeof setTimeout> | undefined>();
   const refs = Array.isArray(refElement) ? refElement : [refElement];
   refs.forEach((refEle) => {
-    const listener = useMouseInElement(refEle, { handleOutside: true });
-    isOutsides.push(listener.isOutside);
+    const eleRef = computed(() => {
+      const ele = unref(refEle);
+      return ele instanceof Element ? ele : (ele?.$el as Element);
+    });
+    const isHover = useElementHover(eleRef);
+    isHovers.push(isHover);
   });
-  const isOutsideAll = computed(() => isOutsides.every((v) => v.value));
+  const isOutsideAll = computed(() => isHovers.every((v) => !v.value));
 
   function setValueDelay(val: boolean) {
     timer.value && clearTimeout(timer.value);

+ 5 - 0
packages/effects/plugins/package.json

@@ -21,6 +21,10 @@
     "./vxe-table": {
       "types": "./src/vxe-table/index.ts",
       "default": "./src/vxe-table/index.ts"
+    },
+    "./motion": {
+      "types": "./src/motion/index.ts",
+      "default": "./src/motion/index.ts"
     }
   },
   "dependencies": {
@@ -34,6 +38,7 @@
     "@vben/types": "workspace:*",
     "@vben/utils": "workspace:*",
     "@vueuse/core": "catalog:",
+    "@vueuse/motion": "catalog:",
     "echarts": "catalog:",
     "vue": "catalog:",
     "vxe-pc-ui": "catalog:",

+ 8 - 0
packages/effects/plugins/src/motion/index.ts

@@ -0,0 +1,8 @@
+export * from './types';
+
+export {
+  MotionComponent as Motion,
+  MotionDirective,
+  MotionGroupComponent as MotionGroup,
+  MotionPlugin,
+} from '@vueuse/motion';

+ 26 - 0
packages/effects/plugins/src/motion/types.ts

@@ -0,0 +1,26 @@
+export const MotionPresets = [
+  'fade',
+  'fadeVisible',
+  'fadeVisibleOnce',
+  'rollBottom',
+  'rollLeft',
+  'rollRight',
+  'rollTop',
+  'rollVisibleBottom',
+  'rollVisibleLeft',
+  'rollVisibleRight',
+  'rollVisibleTop',
+  'pop',
+  'popVisible',
+  'popVisibleOnce',
+  'slideBottom',
+  'slideLeft',
+  'slideRight',
+  'slideTop',
+  'slideVisibleBottom',
+  'slideVisibleLeft',
+  'slideVisibleRight',
+  'slideVisibleTop',
+] as const;
+
+export type MotionPreset = (typeof MotionPresets)[number];

+ 4 - 0
packages/locales/src/langs/en-US/ui.json

@@ -25,6 +25,10 @@
     "placeholder": "Select an icon",
     "search": "Search icon..."
   },
+  "jsonViewer": {
+    "copy": "Copy",
+    "copied": "Copied"
+  },
   "fallback": {
     "pageNotFound": "Oops! Page Not Found",
     "pageNotFoundDesc": "Sorry, we couldn't find the page you were looking for.",

+ 4 - 0
packages/locales/src/langs/zh-CN/ui.json

@@ -25,6 +25,10 @@
     "placeholder": "选择一个图标",
     "search": "搜索图标..."
   },
+  "jsonViewer": {
+    "copy": "复制",
+    "copied": "已复制"
+  },
   "fallback": {
     "pageNotFound": "哎呀!未找到页面",
     "pageNotFoundDesc": "抱歉,我们无法找到您要找的页面。",

+ 178 - 0
playground/src/views/examples/count-to/index.vue

@@ -0,0 +1,178 @@
+<script lang="ts" setup>
+import type { CountToProps, TransitionPresets } from '@vben/common-ui';
+
+import { reactive } from 'vue';
+
+import { CountTo, Page, TransitionPresetsKeys } from '@vben/common-ui';
+import { IconifyIcon } from '@vben/icons';
+
+import {
+  Button,
+  Card,
+  Col,
+  Form,
+  FormItem,
+  Input,
+  InputNumber,
+  message,
+  Row,
+  Select,
+  Switch,
+} from 'ant-design-vue';
+
+const props = reactive<CountToProps & { transition: TransitionPresets }>({
+  decimal: '.',
+  decimals: 2,
+  decimalStyle: {
+    fontSize: 'small',
+    fontStyle: 'italic',
+  },
+  delay: 0,
+  disabled: false,
+  duration: 2000,
+  endVal: 100_000,
+  mainStyle: {
+    color: 'hsl(var(--primary))',
+    fontSize: 'xx-large',
+    fontWeight: 'bold',
+  },
+  prefix: '¥',
+  prefixStyle: {
+    paddingRight: '0.5rem',
+  },
+  separator: ',',
+  startVal: 0,
+  suffix: '元',
+  suffixStyle: {
+    paddingLeft: '0.5rem',
+  },
+  transition: 'easeOutQuart',
+});
+
+function changeNumber() {
+  props.endVal =
+    Math.floor(Math.random() * 100_000_000) / 10 ** (props.decimals || 0);
+}
+
+function openDocumentation() {
+  window.open('https://vueuse.org/core/useTransition/', '_blank');
+}
+
+function onStarted() {
+  message.loading({
+    content: '动画已开始',
+    duration: 0,
+    key: 'animator-info',
+  });
+}
+
+function onFinished() {
+  message.success({
+    content: '动画已结束',
+    duration: 2,
+    key: 'animator-info',
+  });
+}
+</script>
+<template>
+  <Page title="CountTo" description="数字滚动动画组件。使用">
+    <template #description>
+      <span>
+        使用useTransition封装的数字滚动动画组件,每次改变当前值都会产生过渡动画。
+      </span>
+      <Button type="link" @click="openDocumentation">
+        查看useTransition文档
+      </Button>
+    </template>
+    <Card title="基本用法">
+      <div class="flex w-full items-center justify-center pb-4">
+        <CountTo v-bind="props" @started="onStarted" @finished="onFinished" />
+      </div>
+      <Form :model="props">
+        <Row :gutter="20">
+          <Col :span="8">
+            <FormItem label="初始值" name="startVal">
+              <InputNumber v-model:value="props.startVal" />
+            </FormItem>
+          </Col>
+          <Col :span="8">
+            <FormItem label="当前值" name="endVal">
+              <InputNumber
+                v-model:value="props.endVal"
+                class="w-full"
+                :precision="props.decimals"
+              >
+                <template #addonAfter>
+                  <IconifyIcon
+                    v-tippy="`设置一个随机值`"
+                    class="size-5 cursor-pointer outline-none"
+                    icon="ix:random-filled"
+                    @click="changeNumber"
+                  />
+                </template>
+              </InputNumber>
+            </FormItem>
+          </Col>
+          <Col :span="8">
+            <FormItem label="禁用动画" name="disabled">
+              <Switch v-model:checked="props.disabled" />
+            </FormItem>
+          </Col>
+          <Col :span="8">
+            <FormItem label="延迟动画" name="delay">
+              <InputNumber v-model:value="props.delay" :min="0" />
+            </FormItem>
+          </Col>
+          <Col :span="8">
+            <FormItem label="持续时间" name="duration">
+              <InputNumber v-model:value="props.duration" :min="0" />
+            </FormItem>
+          </Col>
+
+          <Col :span="8">
+            <FormItem label="小数位数" name="decimals">
+              <InputNumber
+                v-model:value="props.decimals"
+                :min="0"
+                :precision="0"
+              />
+            </FormItem>
+          </Col>
+          <Col :span="8">
+            <FormItem label="分隔符" name="separator">
+              <Input v-model:value="props.separator" />
+            </FormItem>
+          </Col>
+          <Col :span="8">
+            <FormItem label="小数点" name="decimal">
+              <Input v-model:value="props.decimal" />
+            </FormItem>
+          </Col>
+          <Col :span="8">
+            <FormItem label="动画" name="transition">
+              <Select v-model:value="props.transition">
+                <Select.Option
+                  v-for="preset in TransitionPresetsKeys"
+                  :key="preset"
+                  :value="preset"
+                >
+                  {{ preset }}
+                </Select.Option>
+              </Select>
+            </FormItem>
+          </Col>
+          <Col :span="8">
+            <FormItem label="前缀" name="prefix">
+              <Input v-model:value="props.prefix" />
+            </FormItem>
+          </Col>
+          <Col :span="8">
+            <FormItem label="后缀" name="suffix">
+              <Input v-model:value="props.suffix" />
+            </FormItem>
+          </Col>
+        </Row>
+      </Form>
+    </Card>
+  </Page>
+</template>

+ 66 - 0
playground/src/views/examples/json-viewer/data.ts

@@ -0,0 +1,66 @@
+export const json1 = {
+  additionalInfo: {
+    author: 'Your Name',
+    debug: true,
+    version: '1.3.10',
+    versionCode: 132,
+  },
+  additionalNotes: 'This JSON is used for demonstration purposes',
+  tools: [
+    {
+      description: 'Description of Tool 1',
+      name: 'Tool 1',
+    },
+    {
+      description: 'Description of Tool 2',
+      name: 'Tool 2',
+    },
+    {
+      description: 'Description of Tool 3',
+      name: 'Tool 3',
+    },
+    {
+      description: 'Description of Tool 4',
+      name: 'Tool 4',
+    },
+  ],
+};
+
+export const json2 = JSON.parse(`
+  {
+	"id": "chatcmpl-123",
+	"object": "chat.completion",
+	"created": 1677652288,
+	"model": "gpt-3.5-turbo-0613",
+	"system_fingerprint": "fp_44709d6fcb",
+	"choices": [{
+		"index": 0,
+		"message": {
+			"role": "assistant",
+			"content": "Hello there, how may I assist you today?"
+		},
+		"finish_reason": "stop"
+	}],
+	"usage": {
+		"prompt_tokens": 9,
+		"completion_tokens": 12,
+		"total_tokens": 21,
+    "debug_mode": true
+	},
+  "debug": {
+    "startAt": "2021-08-01T00:00:00Z",
+    "logs": [
+      {
+        "timestamp": "2021-08-01T00:00:00Z",
+        "message": "This is a debug message",
+        "extra":[ "extra1", "extra2" ]
+      },
+      {
+        "timestamp": "2021-08-01T00:00:01Z",
+        "message": "This is another debug message",
+        "extra":[ "extra3", "extra4" ]
+      }
+    ]
+  }
+}
+  `);

+ 51 - 0
playground/src/views/examples/json-viewer/index.vue

@@ -0,0 +1,51 @@
+<script lang="ts" setup>
+import type { JsonViewerAction, JsonViewerValue } from '@vben/common-ui';
+
+import { JsonViewer, Page } from '@vben/common-ui';
+
+import { Card, message } from 'ant-design-vue';
+
+import { json1, json2 } from './data';
+
+function handleKeyClick(key: string) {
+  message.info(`点击了Key ${key}`);
+}
+
+function handleValueClick(value: JsonViewerValue) {
+  message.info(`点击了Value ${JSON.stringify(value)}`);
+}
+
+function handleCopied(_event: JsonViewerAction) {
+  message.success('已复制JSON');
+}
+</script>
+<template>
+  <Page
+    title="Json Viewer"
+    description="一个渲染 JSON 结构数据的组件,支持复制、展开等,简单易用"
+  >
+    <Card title="默认配置">
+      <JsonViewer :value="json1" />
+    </Card>
+    <Card title="可复制、默认展开3层、显示边框、事件处理" class="mt-4">
+      <JsonViewer
+        :value="json2"
+        :expand-depth="3"
+        copyable
+        :sort="false"
+        @key-click="handleKeyClick"
+        @value-click="handleValueClick"
+        @copied="handleCopied"
+        boxed
+      />
+    </Card>
+    <Card title="预览模式" class="mt-4">
+      <JsonViewer
+        :value="json2"
+        copyable
+        preview-mode
+        :show-array-index="false"
+      />
+    </Card>
+  </Page>
+</template>

+ 213 - 0
playground/src/views/examples/motion/index.vue

@@ -0,0 +1,213 @@
+<script lang="ts" setup>
+import { reactive } from 'vue';
+
+import { Page } from '@vben/common-ui';
+import { Motion, MotionGroup, MotionPresets } from '@vben/plugins/motion';
+
+import { refAutoReset, watchDebounced } from '@vueuse/core';
+import {
+  Button,
+  Card,
+  Col,
+  Form,
+  FormItem,
+  InputNumber,
+  Row,
+  Select,
+} from 'ant-design-vue';
+// 本例子用不到visible类型的动画。带有VisibleOnce和Visible的类型会在组件进入视口被显示时执行动画,
+const presets = MotionPresets.filter((v) => !v.includes('Visible'));
+const showCard1 = refAutoReset(true, 100);
+const showCard2 = refAutoReset(true, 100);
+const showCard3 = refAutoReset(true, 100);
+const motionProps = reactive({
+  delay: 0,
+  duration: 300,
+  enter: { scale: 1 },
+  hovered: { scale: 1.1, transition: { delay: 0, duration: 50 } },
+  preset: 'fade',
+  tapped: { scale: 0.9, transition: { delay: 0, duration: 50 } },
+});
+
+const motionGroupProps = reactive({
+  delay: 0,
+  duration: 300,
+  enter: { scale: 1 },
+  hovered: { scale: 1.1, transition: { delay: 0, duration: 50 } },
+  preset: 'fade',
+  tapped: { scale: 0.9, transition: { delay: 0, duration: 50 } },
+});
+
+watchDebounced(
+  motionProps,
+  () => {
+    showCard2.value = false;
+  },
+  { debounce: 200, deep: true },
+);
+
+watchDebounced(
+  motionGroupProps,
+  () => {
+    showCard3.value = false;
+  },
+  { debounce: 200, deep: true },
+);
+
+function openDocPage() {
+  window.open('https://motion.vueuse.org/', '_blank');
+}
+</script>
+<template>
+  <Page title="Motion">
+    <template #description>
+      <span>一个易于使用的为其它组件赋予动画效果的组件。</span>
+      <Button type="link" @click="openDocPage">查看文档</Button>
+    </template>
+    <Card title="使用指令" :body-style="{ minHeight: '5rem' }">
+      <template #extra>
+        <Button type="primary" @click="showCard1 = false">重载</Button>
+      </template>
+      <div>
+        <div class="relative flex gap-2 overflow-hidden" v-if="showCard1">
+          <Button v-motion-fade-visible>fade</Button>
+          <Button v-motion-pop-visible :duration="500">pop</Button>
+          <Button v-motion-slide-left>slide-left</Button>
+          <Button v-motion-slide-right>slide-right</Button>
+          <Button v-motion-slide-bottom>slide-bottom</Button>
+          <Button v-motion-slide-top>slide-top</Button>
+        </div>
+      </div>
+    </Card>
+    <Card
+      class="mt-2"
+      title="使用组件(将内部作为一个整体添加动画)"
+      :body-style="{ padding: 0 }"
+    >
+      <div
+        class="relative flex min-h-32 items-center justify-center gap-2 overflow-hidden"
+      >
+        <Motion
+          v-bind="motionProps"
+          v-if="showCard2"
+          class="flex items-center gap-2"
+        >
+          <Button size="large">这个按钮在显示时会有动画效果</Button>
+          <span>附属组件,会作为整体处理动画</span>
+        </Motion>
+      </div>
+      <div
+        class="relative flex min-h-32 items-center justify-center gap-2 overflow-hidden"
+      >
+        <div v-if="showCard2" class="flex items-center gap-2">
+          <span>顺序延迟</span>
+          <Motion
+            v-bind="{
+              ...motionProps,
+              delay: motionProps.delay + 100 * i,
+            }"
+            v-for="i in 5"
+            :key="i"
+          >
+            <Button size="large">按钮{{ i }}</Button>
+          </Motion>
+        </div>
+      </div>
+      <div>
+        <Form :model="motionProps" :label-col="{ span: 10 }">
+          <Row>
+            <Col :span="8">
+              <FormItem prop="preset" label="动画效果">
+                <Select v-model:value="motionProps.preset">
+                  <Select.Option
+                    :value="preset"
+                    v-for="preset in presets"
+                    :key="preset"
+                  >
+                    {{ preset }}
+                  </Select.Option>
+                </Select>
+              </FormItem>
+            </Col>
+            <Col :span="8">
+              <FormItem prop="duration" label="持续时间">
+                <InputNumber v-model:value="motionProps.duration" />
+              </FormItem>
+            </Col>
+            <Col :span="8">
+              <FormItem prop="delay" label="延迟动画">
+                <InputNumber v-model:value="motionProps.delay" />
+              </FormItem>
+            </Col>
+            <Col :span="8">
+              <FormItem prop="hovered.scale" label="Hover缩放">
+                <InputNumber v-model:value="motionProps.hovered.scale" />
+              </FormItem>
+            </Col>
+            <Col :span="8">
+              <FormItem prop="hovered.tapped" label="按下时缩放">
+                <InputNumber v-model:value="motionProps.tapped.scale" />
+              </FormItem>
+            </Col>
+          </Row>
+        </Form>
+      </div>
+    </Card>
+    <Card
+      class="mt-2"
+      title="分组动画(每个子元素都会应用相同的独立动画)"
+      :body-style="{ padding: 0 }"
+    >
+      <div
+        class="relative flex min-h-32 items-center justify-center gap-2 overflow-hidden"
+      >
+        <MotionGroup v-bind="motionGroupProps" v-if="showCard3">
+          <Button size="large">按钮1</Button>
+          <Button size="large">按钮2</Button>
+          <Button size="large">按钮3</Button>
+          <Button size="large">按钮4</Button>
+          <Button size="large">按钮5</Button>
+        </MotionGroup>
+      </div>
+      <div>
+        <Form :model="motionGroupProps" :label-col="{ span: 10 }">
+          <Row>
+            <Col :span="8">
+              <FormItem prop="preset" label="动画效果">
+                <Select v-model:value="motionGroupProps.preset">
+                  <Select.Option
+                    :value="preset"
+                    v-for="preset in presets"
+                    :key="preset"
+                  >
+                    {{ preset }}
+                  </Select.Option>
+                </Select>
+              </FormItem>
+            </Col>
+            <Col :span="8">
+              <FormItem prop="duration" label="持续时间">
+                <InputNumber v-model:value="motionGroupProps.duration" />
+              </FormItem>
+            </Col>
+            <Col :span="8">
+              <FormItem prop="delay" label="延迟动画">
+                <InputNumber v-model:value="motionGroupProps.delay" />
+              </FormItem>
+            </Col>
+            <Col :span="8">
+              <FormItem prop="hovered.scale" label="Hover缩放">
+                <InputNumber v-model:value="motionGroupProps.hovered.scale" />
+              </FormItem>
+            </Col>
+            <Col :span="8">
+              <FormItem prop="hovered.tapped" label="按下时缩放">
+                <InputNumber v-model:value="motionGroupProps.tapped.scale" />
+              </FormItem>
+            </Col>
+          </Row>
+        </Form>
+      </div>
+    </Card>
+  </Page>
+</template>

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 355 - 147
pnpm-lock.yaml


+ 34 - 32
pnpm-workspace.yaml

@@ -21,22 +21,22 @@ catalog:
   '@commitlint/cli': ^19.7.1
   '@commitlint/config-conventional': ^19.7.1
   '@ctrl/tinycolor': ^4.1.0
-  '@eslint/js': ^9.19.0
-  '@faker-js/faker': ^9.4.0
-  '@iconify/json': ^2.2.302
+  '@eslint/js': ^9.20.0
+  '@faker-js/faker': ^9.5.0
+  '@iconify/json': ^2.2.307
   '@iconify/tailwind': ^1.2.0
   '@iconify/vue': ^4.3.0
-  '@intlify/core-base': ^11.1.0
+  '@intlify/core-base': ^11.1.1
   '@intlify/unplugin-vue-i18n': ^6.0.3
-  '@jspm/generator': ^2.4.2
+  '@jspm/generator': ^2.5.0
   '@manypkg/get-packages': ^2.2.2
-  '@nolebase/vitepress-plugin-git-changelog': ^2.12.1
+  '@nolebase/vitepress-plugin-git-changelog': ^2.14.0
   '@playwright/test': ^1.50.1
   '@pnpm/workspace.read-manifest': ^1000.0.2
   '@stylistic/stylelint-plugin': ^3.1.1
   '@tailwindcss/nesting': 0.0.0-insiders.565cd3e
   '@tailwindcss/typography': ^0.5.16
-  '@tanstack/vue-query': ^5.65.0
+  '@tanstack/vue-query': ^5.66.3
   '@tanstack/vue-store': ^0.7.0
   '@types/archiver': ^6.0.3
   '@types/eslint': ^9.6.1
@@ -45,13 +45,13 @@ catalog:
   '@types/lodash.clonedeep': ^4.5.9
   '@types/lodash.get': ^4.4.9
   '@types/lodash.isequal': ^4.5.8
-  '@types/node': ^22.13.1
+  '@types/node': ^22.13.4
   '@types/nprogress': ^0.2.3
   '@types/postcss-import': ^14.0.3
   '@types/qrcode': ^1.5.5
   '@types/sortablejs': ^1.15.8
-  '@typescript-eslint/eslint-plugin': ^8.23.0
-  '@typescript-eslint/parser': ^8.23.0
+  '@typescript-eslint/eslint-plugin': ^8.24.0
+  '@typescript-eslint/parser': ^8.24.0
   '@vee-validate/zod': ^4.15.0
   '@vite-pwa/vitepress': ^0.5.3
   '@vitejs/plugin-vue': ^5.2.1
@@ -59,8 +59,9 @@ catalog:
   '@vue/reactivity': ^3.5.13
   '@vue/shared': ^3.5.13
   '@vue/test-utils': ^2.4.6
-  '@vueuse/core': ^12.5.0
-  '@vueuse/integrations': ^12.5.0
+  '@vueuse/core': ^12.7.0
+  '@vueuse/motion': ^2.2.6
+  '@vueuse/integrations': ^12.7.0
   ant-design-vue: ^4.2.6
   archiver: ^7.0.1
   autoprefixer: ^10.4.20
@@ -84,9 +85,9 @@ catalog:
   depcheck: ^1.4.7
   dotenv: ^16.4.7
   echarts: ^5.6.0
-  element-plus: ^2.9.3
-  eslint: ^9.19.0
-  eslint-config-turbo: ^2.4.0
+  element-plus: ^2.9.4
+  eslint: ^9.20.1
+  eslint-config-turbo: ^2.4.2
   eslint-plugin-command: ^0.2.7
   eslint-plugin-eslint-comments: ^3.2.0
   eslint-plugin-import-x: ^4.6.1
@@ -94,7 +95,7 @@ catalog:
   eslint-plugin-jsonc: ^2.19.1
   eslint-plugin-n: ^17.15.1
   eslint-plugin-no-only-tests: ^3.3.0
-  eslint-plugin-perfectionist: ^4.8.0
+  eslint-plugin-perfectionist: ^4.9.0
   eslint-plugin-prettier: ^5.2.3
   eslint-plugin-regexp: ^2.7.0
   eslint-plugin-unicorn: ^56.0.1
@@ -104,8 +105,8 @@ catalog:
   execa: ^9.5.2
   find-up: ^7.0.0
   get-port: ^7.1.0
-  globals: ^15.14.0
-  h3: ^1.14.0
+  globals: ^15.15.0
+  h3: ^1.15.0
   happy-dom: ^16.8.1
   html-minifier-terser: ^7.2.0
   husky: ^9.1.7
@@ -126,22 +127,22 @@ catalog:
   pinia-plugin-persistedstate: ^4.2.0
   pkg-types: ^1.3.1
   playwright: ^1.50.1
-  postcss: ^8.5.1
+  postcss: ^8.5.2
   postcss-antd-fixes: ^0.2.0
   postcss-html: ^1.8.0
   postcss-import: ^16.1.0
-  postcss-preset-env: ^10.1.3
+  postcss-preset-env: ^10.1.4
   postcss-scss: ^4.0.9
-  prettier: ^3.4.2
+  prettier: ^3.5.1
   prettier-plugin-tailwindcss: ^0.6.11
   publint: ^0.2.12
   qrcode: ^1.5.4
-  radix-vue: ^1.9.13
+  radix-vue: ^1.9.14
   resolve.exports: ^2.0.3
   rimraf: ^6.0.1
-  rollup: ^4.34.2
+  rollup: ^4.34.7
   rollup-plugin-visualizer: ^5.14.0
-  sass: ^1.83.4
+  sass: ^1.85.0
   sortablejs: ^1.15.6
   stylelint: ^16.14.1
   stylelint-config-recess-order: ^5.1.1
@@ -157,29 +158,30 @@ catalog:
   tailwindcss-animate: ^1.0.7
   theme-colors: ^0.1.0
   tippy.js: ^6.2.5
-  turbo: ^2.4.0
+  turbo: ^2.4.2
   typescript: ^5.7.3
   unbuild: ^3.3.1
-  unplugin-element-plus: ^0.9.0
+  unplugin-element-plus: ^0.9.1
   vee-validate: ^4.15.0
-  vite: ^6.0.11
+  vite: ^6.1.0
   vite-plugin-compression: ^0.5.1
   vite-plugin-dts: ^4.5.0
   vite-plugin-html: ^3.2.2
   vite-plugin-lazy-import: ^1.0.7
   vite-plugin-pwa: ^0.21.1
-  vite-plugin-vue-devtools: ^7.7.1
+  vite-plugin-vue-devtools: ^7.7.2
   vitepress: ^1.6.3
   vitepress-plugin-group-icons: ^1.3.5
   vitest: ^2.1.9
   vue: ^3.5.13
   vue-eslint-parser: ^9.4.3
-  vue-i18n: ^11.1.0
+  vue-i18n: ^11.1.1
+  vue-json-viewer: ^3.0.4
   vue-router: ^4.5.0
   vue-tippy: ^6.6.0
   vue-tsc: 2.1.10
-  vxe-pc-ui: ^4.3.79
+  vxe-pc-ui: ^4.3.87
   vxe-table: 4.10.0
-  watermark-js-plus: ^1.5.7
-  zod: ^3.24.1
+  watermark-js-plus: ^1.5.8
+  zod: ^3.24.2
   zod-defaults: ^0.1.3

Nem az összes módosított fájl került megjelenítésre, mert túl sok fájl változott