This commit is contained in:
dap 2025-02-18 14:08:05 +08:00
commit fbb0d641db
29 changed files with 1086 additions and 46 deletions

View File

@ -1,4 +1,8 @@
export default defineEventHandler((event) => {
event.node.res.setHeader(
'Access-Control-Allow-Origin',
event.headers.get('Origin') ?? '*',
);
if (event.method === 'OPTIONS') {
event.node.res.statusCode = 204;
event.node.res.statusMessage = 'No Content.';

View File

@ -9,7 +9,8 @@ export default defineNitroConfig({
cors: true,
headers: {
'Access-Control-Allow-Credentials': 'true',
'Access-Control-Allow-Headers': '*',
'Access-Control-Allow-Headers':
'Accept, Authorization, Content-Length, Content-Type, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, X-CSRF-TOKEN, X-Requested-With',
'Access-Control-Allow-Methods': 'GET,HEAD,PUT,PATCH,POST,DELETE',
'Access-Control-Allow-Origin': '*',
'Access-Control-Expose-Headers': '*',

View File

@ -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';
@ -49,6 +50,9 @@ async function bootstrap(namespace: string) {
// 配置路由及路由守卫
app.use(router);
// 配置Motion插件
app.use(MotionPlugin);
// 动态更新标题
watchEffect(() => {
if (preferences.app.dynamicTitle) {

View File

@ -3,7 +3,7 @@
社区交流群主要是为了方便大家交流,提问,解答问题,分享经验等。偏自助方式,如果你有问题,可以通过以下方式加入社区交流群:
- [QQ频道](https://pd.qq.com/s/16p8lvvob):推荐!!!主要提供问题解答,分享经验等。
- QQ群[大群](https://qm.qq.com/q/MEmHoCLbG0)[1群](https://qm.qq.com/q/YacMHPYAMu)、[2群](https://qm.qq.com/q/ajVKZvFICk)、[3群](https://qm.qq.com/q/36zdwThP2E)[4群](https://qm.qq.com/q/sCzSlm3504),主要使用者交流群。
- QQ群[大群](https://qm.qq.com/q/MEmHoCLbG0)[1群](https://qm.qq.com/q/YacMHPYAMu)、[2群](https://qm.qq.com/q/ajVKZvFICk)、[3群](https://qm.qq.com/q/36zdwThP2E)[4群](https://qm.qq.com/q/sCzSlm3504)[5群](https://qm.qq.com/q/ya9XrtbS6s)主要使用者交流群。
- [Discord](https://discord.com/invite/VU62jTecad): 主要提供问题解答,分享经验等。
::: tip

View File

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

View File

@ -47,6 +47,7 @@
"vue": "catalog:",
"vue-codemirror6": "1.3.4",
"vue-json-pretty": "^2.4.0",
"vue-json-viewer": "catalog:",
"vue-router": "catalog:",
"vue-tippy": "catalog:"
},

View File

@ -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>

View File

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

View File

@ -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;
}

View File

@ -2,9 +2,11 @@ export * from './api-component';
export * from './captcha';
export * from './code-mirror';
export * from './col-page';
export * from './count-to';
export * from './ellipsis-text';
export * from './icon-picker';
export * from './json-preview';
export * from './json-viewer';
export * from './markdown';
export * from './page';
export * from './resize';

View File

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

View File

@ -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>

View File

@ -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));
}
}
}
}
}

View File

@ -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;
}

View File

@ -1,9 +1,12 @@
import type { Arrayable, MaybeElementRef } from '@vueuse/core';
import type { Ref } from 'vue';
import { computed, onUnmounted, ref, unref, watch } from 'vue';
import { isFunction } from '@vben/utils';
import { useMouseInElement } from '@vueuse/core';
import { computed, onUnmounted, ref, watch } from 'vue';
import { useElementHover } from '@vueuse/core';
/**
* true false
@ -15,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);

View File

@ -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:",

View File

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

View File

@ -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];

View File

@ -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.",

View File

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

View File

@ -22,23 +22,29 @@ export namespace AuthApi {
*
*/
export async function loginApi(data: AuthApi.LoginParams) {
return requestClient.post<AuthApi.LoginResult>('/auth/login', data);
return requestClient.post<AuthApi.LoginResult>('/auth/login', data, {
withCredentials: true,
});
}
/**
* accessToken
*/
export async function refreshTokenApi() {
return baseRequestClient.post<AuthApi.RefreshTokenResult>('/auth/refresh', {
withCredentials: true,
});
return baseRequestClient.post<AuthApi.RefreshTokenResult>(
'/auth/refresh',
null,
{
withCredentials: true,
},
);
}
/**
* 退
*/
export async function logoutApi() {
return baseRequestClient.post('/auth/logout', {
return baseRequestClient.post('/auth/logout', null, {
withCredentials: true,
});
}

View File

@ -111,3 +111,9 @@ export const requestClient = createRequestClient(apiURL, {
});
export const baseRequestClient = new RequestClient({ baseURL: apiURL });
export interface PageFetchParams {
[key: string]: any;
pageNo?: number;
pageSize?: number;
}

View File

@ -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';
@ -49,6 +50,9 @@ async function bootstrap(namespace: string) {
// 配置@tanstack/vue-query
app.use(VueQueryPlugin);
// 配置Motion插件
app.use(MotionPlugin);
// 动态更新标题
watchEffect(() => {
if (preferences.app.dynamicTitle) {

View File

@ -255,6 +255,33 @@ const routes: RouteRecordRaw[] = [
title: 'Tippy',
},
},
{
name: 'JsonViewer',
path: '/examples/json-viewer',
component: () => import('#/views/examples/json-viewer/index.vue'),
meta: {
icon: 'tabler:json',
title: 'JsonViewer',
},
},
{
name: 'Motion',
path: '/examples/motion',
component: () => import('#/views/examples/motion/index.vue'),
meta: {
icon: 'mdi:animation-play',
title: 'Motion',
},
},
{
name: 'CountTo',
path: '/examples/count-to',
component: () => import('#/views/examples/count-to/index.vue'),
meta: {
icon: 'mdi:animation-play',
title: 'CountTo',
},
},
],
},
];

View File

@ -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>

View File

@ -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" ]
}
]
}
}
`);

View File

@ -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>

View File

@ -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';
// visibleVisibleOnceVisible
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>

View File

@ -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