diff --git a/apps/web-antd/src/adapter/component/index.ts b/apps/web-antd/src/adapter/component/index.ts index 79b9bf6f8..80d990b76 100644 --- a/apps/web-antd/src/adapter/component/index.ts +++ b/apps/web-antd/src/adapter/component/index.ts @@ -3,6 +3,8 @@ * 可用于 vben-form、vben-modal、vben-drawer 等组件使用, */ +/* eslint-disable vue/one-component-per-file */ + import type { UploadChangeParam, UploadFile, @@ -15,6 +17,7 @@ import type { BaseFormComponentType } from '@vben/common-ui'; import type { Recordable } from '@vben/types'; import { + computed, defineAsyncComponent, defineComponent, h, @@ -24,12 +27,17 @@ import { watch, } from 'vue'; -import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui'; +import { + ApiComponent, + globalShareState, + IconPicker, + VCropper, +} from '@vben/common-ui'; import { IconifyIcon } from '@vben/icons'; import { $t } from '@vben/locales'; import { isEmpty } from '@vben/utils'; -import { message, notification } from 'ant-design-vue'; +import { message, Modal, notification } from 'ant-design-vue'; const AutoComplete = defineAsyncComponent( () => import('ant-design-vue/es/auto-complete'), @@ -119,6 +127,33 @@ const withDefaultPlaceholder = ( }; const withPreviewUpload = () => { + // 检查是否为图片文件的辅助函数 + const isImageFile = (file: UploadFile): boolean => { + const imageExtensions = new Set([ + 'bmp', + 'gif', + 'jpeg', + 'jpg', + 'png', + 'svg', + 'webp', + ]); + if (file.url) { + try { + const pathname = new URL(file.url, 'http://localhost').pathname; + const ext = pathname.split('.').pop()?.toLowerCase(); + return ext ? imageExtensions.has(ext) : false; + } catch { + const ext = file.url?.split('.').pop()?.toLowerCase(); + return ext ? imageExtensions.has(ext) : false; + } + } + if (!file.type) { + const ext = file.name?.split('.').pop()?.toLowerCase(); + return ext ? imageExtensions.has(ext) : false; + } + return file.type.startsWith('image/'); + }; // 创建默认的上传按钮插槽 const createDefaultSlotsWithUpload = ( listType: string, @@ -153,27 +188,6 @@ const withPreviewUpload = () => { visible: Ref, fileList: Ref, ) => { - // 检查是否为图片文件的辅助函数 - const isImageFile = (file: UploadFile): boolean => { - const imageExtensions = new Set([ - 'bmp', - 'gif', - 'jpeg', - 'jpg', - 'png', - 'webp', - ]); - if (file.url) { - const ext = file.url?.split('.').pop()?.toLowerCase(); - return ext ? imageExtensions.has(ext) : false; - } - if (!file.type) { - const ext = file.name?.split('.').pop()?.toLowerCase(); - return ext ? imageExtensions.has(ext) : false; - } - return file.type.startsWith('image/'); - }; - // 如果当前文件不是图片,直接打开 if (!isImageFile(file)) { if (file.url) { @@ -259,6 +273,100 @@ const withPreviewUpload = () => { render(h(PreviewWrapper), container); }; + + // 图片裁剪操作 + const cropImage = (file: File, aspectRatio: string | undefined) => { + return new Promise((resolve, reject) => { + const container: HTMLElement | null = document.createElement('div'); + document.body.append(container); + + // 用于追踪组件是否已卸载 + let isUnmounted = false; + let objectUrl: null | string = null; + + const open = ref(true); + const cropperRef = ref | null>(null); + + const closeModal = () => { + open.value = false; + // 延迟清理,确保动画完成 + setTimeout(() => { + if (!isUnmounted && container) { + if (objectUrl) { + URL.revokeObjectURL(objectUrl); + } + isUnmounted = true; + render(null, container); + container.remove(); + } + }, 300); + }; + + const CropperWrapper = { + setup() { + return () => { + if (isUnmounted) return null; + if (!objectUrl) { + objectUrl = URL.createObjectURL(file); + } + return h( + Modal, + { + open: open.value, + title: h('div', {}, [ + $t('ui.crop.title'), + h( + 'span', + { + class: `${aspectRatio ? '' : 'hidden'} ml-2 text-sm text-gray-400 font-normal`, + }, + $t('ui.crop.titleTip', [aspectRatio]), + ), + ]), + centered: true, + width: 548, + keyboard: false, + maskClosable: false, + closable: false, + cancelText: $t('common.cancel'), + okText: $t('ui.crop.confirm'), + destroyOnClose: true, + onOk: async () => { + const cropper = cropperRef.value; + if (!cropper) { + reject(new Error('Cropper not found')); + closeModal(); + return; + } + try { + const dataUrl = await cropper.getCropImage(); + resolve(dataUrl); + } catch { + reject(new Error($t('ui.crop.errorTip'))); + } finally { + closeModal(); + } + }, + onCancel() { + resolve(''); + closeModal(); + }, + }, + () => + h(VCropper, { + ref: (ref: any) => (cropperRef.value = ref), + img: objectUrl as string, + aspectRatio, + }), + ); + }; + }, + }; + + render(h(CropperWrapper), container); + }); + }; + return defineComponent({ name: Upload.name, emits: ['update:modelValue'], @@ -276,16 +384,51 @@ const withPreviewUpload = () => { attrs?.fileList || attrs?.['file-list'] || [], ); - const handleBeforeUpload = (file: UploadFile) => { - if (attrs.maxSize && (file.size || 0) / 1024 / 1024 > attrs.maxSize) { - message.error($t('ui.formRules.sizeLimit', [attrs.maxSize])); + const maxSize = computed(() => attrs?.maxSize ?? attrs?.['max-size']); + const aspectRatio = computed( + () => attrs?.aspectRatio ?? attrs?.['aspect-ratio'], + ); + + const handleBeforeUpload = async ( + file: UploadFile, + originFileList: Array, + ) => { + if (maxSize.value && (file.size || 0) / 1024 / 1024 > maxSize.value) { + message.error($t('ui.formRules.sizeLimit', [maxSize.value])); file.status = 'removed'; return false; } + // 多选或者非图片不唤起裁剪框 + if ( + attrs.crop && + !attrs.multiple && + originFileList[0] && + isImageFile(file) + ) { + file.status = 'removed'; + // antd Upload组件问题 file参数获取的是UploadFile类型对象无法取到File类型 所以通过originFileList[0]获取 + const blob = await cropImage(originFileList[0], aspectRatio.value); + return new Promise((resolve, reject) => { + if (!blob) { + return reject(new Error($t('ui.crop.errorTip'))); + } + resolve(blob); + }); + } + return attrs.beforeUpload?.(file) ?? true; }; - const handleChange = async (event: UploadChangeParam) => { + const handleChange = (event: UploadChangeParam) => { + try { + // 行内写法 handleChange: (event) => {} + attrs.handleChange?.(event); + // template写法 @handle-change="(event) => {}" + attrs.onHandleChange?.(event); + } catch (error) { + // Avoid breaking internal v-model sync on user handler errors + console.error(error); + } fileList.value = event.fileList.filter( (file) => file.status !== 'removed', ); @@ -375,6 +518,7 @@ async function initComponentAdapter() { // 如果你的组件体积比较大,可以使用异步加载 // Button: () => // import('xxx').then((res) => res.Button), + ApiCascader: withDefaultPlaceholder(ApiComponent, 'select', { component: Cascader, fieldNames: { label: 'label', value: 'value', children: 'children' }, @@ -382,34 +526,20 @@ async function initComponentAdapter() { modelPropName: 'value', visibleEvent: 'onVisibleChange', }), - ApiSelect: withDefaultPlaceholder( - { - ...ApiComponent, - name: 'ApiSelect', - }, - 'select', - { - component: Select, - loadingSlot: 'suffixIcon', - visibleEvent: 'onDropdownVisibleChange', - modelPropName: 'value', - }, - ), - ApiTreeSelect: withDefaultPlaceholder( - { - ...ApiComponent, - name: 'ApiTreeSelect', - }, - 'select', - { - component: TreeSelect, - fieldNames: { label: 'label', value: 'value', children: 'children' }, - loadingSlot: 'suffixIcon', - modelPropName: 'value', - optionsPropName: 'treeData', - visibleEvent: 'onVisibleChange', - }, - ), + ApiSelect: withDefaultPlaceholder(ApiComponent, 'select', { + component: Select, + loadingSlot: 'suffixIcon', + modelPropName: 'value', + visibleEvent: 'onVisibleChange', + }), + ApiTreeSelect: withDefaultPlaceholder(ApiComponent, 'select', { + component: TreeSelect, + fieldNames: { label: 'label', value: 'value', children: 'children' }, + loadingSlot: 'suffixIcon', + modelPropName: 'value', + optionsPropName: 'treeData', + visibleEvent: 'onVisibleChange', + }), AutoComplete, Cascader, Checkbox, diff --git a/apps/web-tdesign/src/app.vue b/apps/web-tdesign/src/app.vue index f37688b67..f2fa88408 100644 --- a/apps/web-tdesign/src/app.vue +++ b/apps/web-tdesign/src/app.vue @@ -1,7 +1,7 @@ + + + + diff --git a/packages/effects/common-ui/src/components/cropper/index.ts b/packages/effects/common-ui/src/components/cropper/index.ts new file mode 100644 index 000000000..58a8f0ab6 --- /dev/null +++ b/packages/effects/common-ui/src/components/cropper/index.ts @@ -0,0 +1 @@ +export { default as VCropper } from './cropper.vue'; diff --git a/packages/effects/common-ui/src/components/index.ts b/packages/effects/common-ui/src/components/index.ts index 9ff0d5ee1..33f32bd04 100644 --- a/packages/effects/common-ui/src/components/index.ts +++ b/packages/effects/common-ui/src/components/index.ts @@ -2,6 +2,7 @@ export * from './api-component'; export * from './captcha'; export * from './col-page'; export * from './count-to'; +export * from './cropper'; export * from './ellipsis-text'; export * from './icon-picker'; export * from './json-viewer'; diff --git a/packages/effects/layouts/src/basic/layout.vue b/packages/effects/layouts/src/basic/layout.vue index 6263fbdfc..45b978334 100644 --- a/packages/effects/layouts/src/basic/layout.vue +++ b/packages/effects/layouts/src/basic/layout.vue @@ -14,7 +14,7 @@ import { updatePreferences, usePreferences, } from '@vben/preferences'; -import { useAccessStore } from '@vben/stores'; +import { useAccessStore, useTabbarStore, useTimezoneStore } from '@vben/stores'; import { cloneDeep, mapTree } from '@vben/utils'; import { VbenAdminLayout } from '@vben-core/layout-ui'; @@ -52,6 +52,7 @@ const { theme, } = usePreferences(); const accessStore = useAccessStore(); +const timezoneStore = useTimezoneStore(); const { refresh } = useRefresh(); const sidebarTheme = computed(() => { @@ -187,9 +188,19 @@ watch( }, ); +const tabbarStore = useTabbarStore(); + +function refreshAll() { + tabbarStore.cachedTabs.clear(); + refresh(); +} + // 语言更新后,刷新页面 // i18n.global.locale会在preference.app.locale变更之后才会更新,因此watchpreference.app.locale是不合适的,刷新页面时可能语言配置尚未完全加载完成 -watch(i18n.global.locale, refresh, { flush: 'post' }); +watch(i18n.global.locale, refreshAll, { flush: 'post' }); + +// 时区更新后,刷新页面 +watch(() => timezoneStore.timezone, refreshAll, { flush: 'post' }); const slots: SetupContext['slots'] = useSlots(); const headerSlots = computed(() => { diff --git a/packages/effects/plugins/src/echarts/use-echarts.ts b/packages/effects/plugins/src/echarts/use-echarts.ts index 1a28fb125..92aea5f3b 100644 --- a/packages/effects/plugins/src/echarts/use-echarts.ts +++ b/packages/effects/plugins/src/echarts/use-echarts.ts @@ -92,7 +92,8 @@ function useEcharts(chartRef: Ref) { return; } useTimeoutFn(() => { - if (!chartInstance) { + if (!chartInstance || chartInstance?.getDom() !== el) { + chartInstance?.dispose(); const instance = initCharts(); if (!instance) return; } @@ -104,6 +105,36 @@ function useEcharts(chartRef: Ref) { }); }; + const updateDate = ( + option: EChartsOption, + notMerge = false, // false = 合并(保留动画),true = 完全替换 + lazyUpdate = false, // true 时不立即重绘,适合短时间内多次调用 + ): Promise => { + return new Promise((resolve) => { + nextTick(() => { + if (!chartInstance) { + // 还没初始化 → 当作首次渲染 + renderEcharts(option).then(resolve); + return; + } + + // 合并你原有的全局配置(比如 backgroundColor) + const finalOption = { + ...option, + ...getOptions.value, + }; + + chartInstance.setOption(finalOption, { + notMerge, + lazyUpdate, + // silent: true, // 如果追求极致性能可开启(关闭所有事件) + }); + + resolve(chartInstance); + }); + }); + }; + function resize() { const el = getChartEl(); if (isElHidden(el)) { @@ -139,6 +170,7 @@ function useEcharts(chartRef: Ref) { return { renderEcharts, resize, + updateDate, getChartInstance: () => chartInstance, }; } diff --git a/packages/locales/src/langs/en-US/ui.json b/packages/locales/src/langs/en-US/ui.json index f5166cdfa..5e1c8557b 100644 --- a/packages/locales/src/langs/en-US/ui.json +++ b/packages/locales/src/langs/en-US/ui.json @@ -54,6 +54,13 @@ "copy": "Copy", "copied": "Copied" }, + "crop": { + "title": "Image Cropping", + "titleTip": "Cropping Ratio {0}", + "confirm": "Crop", + "cancel": "Cancel cropping", + "errorTip": "Cropping error" + }, "fallback": { "pageNotFound": "Oops! Page Not Found", "pageNotFoundDesc": "Sorry, we couldn't find the page you were looking for.", diff --git a/packages/locales/src/langs/zh-CN/ui.json b/packages/locales/src/langs/zh-CN/ui.json index 460931898..12306b424 100644 --- a/packages/locales/src/langs/zh-CN/ui.json +++ b/packages/locales/src/langs/zh-CN/ui.json @@ -54,6 +54,13 @@ "copy": "复制", "copied": "已复制" }, + "crop": { + "title": "图片裁剪", + "titleTip": "裁剪比例 {0}", + "confirm": "裁剪", + "cancel": "取消裁剪", + "errorTip": "裁剪错误" + }, "fallback": { "pageNotFound": "哎呀!未找到页面", "pageNotFoundDesc": "抱歉,我们无法找到您要找的页面。", diff --git a/playground/src/adapter/component/index.ts b/playground/src/adapter/component/index.ts index b0e43c9ac..80d990b76 100644 --- a/playground/src/adapter/component/index.ts +++ b/playground/src/adapter/component/index.ts @@ -3,6 +3,8 @@ * 可用于 vben-form、vben-modal、vben-drawer 等组件使用, */ +/* eslint-disable vue/one-component-per-file */ + import type { UploadChangeParam, UploadFile, @@ -15,6 +17,7 @@ import type { BaseFormComponentType } from '@vben/common-ui'; import type { Recordable } from '@vben/types'; import { + computed, defineAsyncComponent, defineComponent, h, @@ -24,12 +27,17 @@ import { watch, } from 'vue'; -import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui'; +import { + ApiComponent, + globalShareState, + IconPicker, + VCropper, +} from '@vben/common-ui'; import { IconifyIcon } from '@vben/icons'; import { $t } from '@vben/locales'; import { isEmpty } from '@vben/utils'; -import { message, notification } from 'ant-design-vue'; +import { message, Modal, notification } from 'ant-design-vue'; const AutoComplete = defineAsyncComponent( () => import('ant-design-vue/es/auto-complete'), @@ -99,7 +107,6 @@ const withDefaultPlaceholder = ( $t(`ui.placeholder.${type}`); // 透传组件暴露的方法 const innerRef = ref(); - // const publicApi: Recordable = {}; expose( new Proxy( {}, @@ -109,14 +116,6 @@ const withDefaultPlaceholder = ( }, ), ); - // const instance = getCurrentInstance(); - // instance?.proxy?.$nextTick(() => { - // for (const key in innerRef.value) { - // if (typeof innerRef.value[key] === 'function') { - // publicApi[key] = innerRef.value[key]; - // } - // } - // }); return () => h( component, @@ -128,6 +127,33 @@ const withDefaultPlaceholder = ( }; const withPreviewUpload = () => { + // 检查是否为图片文件的辅助函数 + const isImageFile = (file: UploadFile): boolean => { + const imageExtensions = new Set([ + 'bmp', + 'gif', + 'jpeg', + 'jpg', + 'png', + 'svg', + 'webp', + ]); + if (file.url) { + try { + const pathname = new URL(file.url, 'http://localhost').pathname; + const ext = pathname.split('.').pop()?.toLowerCase(); + return ext ? imageExtensions.has(ext) : false; + } catch { + const ext = file.url?.split('.').pop()?.toLowerCase(); + return ext ? imageExtensions.has(ext) : false; + } + } + if (!file.type) { + const ext = file.name?.split('.').pop()?.toLowerCase(); + return ext ? imageExtensions.has(ext) : false; + } + return file.type.startsWith('image/'); + }; // 创建默认的上传按钮插槽 const createDefaultSlotsWithUpload = ( listType: string, @@ -162,27 +188,6 @@ const withPreviewUpload = () => { visible: Ref, fileList: Ref, ) => { - // 检查是否为图片文件的辅助函数 - const isImageFile = (file: UploadFile): boolean => { - const imageExtensions = new Set([ - 'bmp', - 'gif', - 'jpeg', - 'jpg', - 'png', - 'webp', - ]); - if (file.url) { - const ext = file.url?.split('.').pop()?.toLowerCase(); - return ext ? imageExtensions.has(ext) : false; - } - if (!file.type) { - const ext = file.name?.split('.').pop()?.toLowerCase(); - return ext ? imageExtensions.has(ext) : false; - } - return file.type.startsWith('image/'); - }; - // 如果当前文件不是图片,直接打开 if (!isImageFile(file)) { if (file.url) { @@ -268,6 +273,100 @@ const withPreviewUpload = () => { render(h(PreviewWrapper), container); }; + + // 图片裁剪操作 + const cropImage = (file: File, aspectRatio: string | undefined) => { + return new Promise((resolve, reject) => { + const container: HTMLElement | null = document.createElement('div'); + document.body.append(container); + + // 用于追踪组件是否已卸载 + let isUnmounted = false; + let objectUrl: null | string = null; + + const open = ref(true); + const cropperRef = ref | null>(null); + + const closeModal = () => { + open.value = false; + // 延迟清理,确保动画完成 + setTimeout(() => { + if (!isUnmounted && container) { + if (objectUrl) { + URL.revokeObjectURL(objectUrl); + } + isUnmounted = true; + render(null, container); + container.remove(); + } + }, 300); + }; + + const CropperWrapper = { + setup() { + return () => { + if (isUnmounted) return null; + if (!objectUrl) { + objectUrl = URL.createObjectURL(file); + } + return h( + Modal, + { + open: open.value, + title: h('div', {}, [ + $t('ui.crop.title'), + h( + 'span', + { + class: `${aspectRatio ? '' : 'hidden'} ml-2 text-sm text-gray-400 font-normal`, + }, + $t('ui.crop.titleTip', [aspectRatio]), + ), + ]), + centered: true, + width: 548, + keyboard: false, + maskClosable: false, + closable: false, + cancelText: $t('common.cancel'), + okText: $t('ui.crop.confirm'), + destroyOnClose: true, + onOk: async () => { + const cropper = cropperRef.value; + if (!cropper) { + reject(new Error('Cropper not found')); + closeModal(); + return; + } + try { + const dataUrl = await cropper.getCropImage(); + resolve(dataUrl); + } catch { + reject(new Error($t('ui.crop.errorTip'))); + } finally { + closeModal(); + } + }, + onCancel() { + resolve(''); + closeModal(); + }, + }, + () => + h(VCropper, { + ref: (ref: any) => (cropperRef.value = ref), + img: objectUrl as string, + aspectRatio, + }), + ); + }; + }, + }; + + render(h(CropperWrapper), container); + }); + }; + return defineComponent({ name: Upload.name, emits: ['update:modelValue'], @@ -285,16 +384,51 @@ const withPreviewUpload = () => { attrs?.fileList || attrs?.['file-list'] || [], ); - const handleBeforeUpload = (file: UploadFile) => { - if (attrs.maxSize && (file.size || 0) / 1024 / 1024 > attrs.maxSize) { - message.error($t('ui.formRules.sizeLimit', [attrs.maxSize])); + const maxSize = computed(() => attrs?.maxSize ?? attrs?.['max-size']); + const aspectRatio = computed( + () => attrs?.aspectRatio ?? attrs?.['aspect-ratio'], + ); + + const handleBeforeUpload = async ( + file: UploadFile, + originFileList: Array, + ) => { + if (maxSize.value && (file.size || 0) / 1024 / 1024 > maxSize.value) { + message.error($t('ui.formRules.sizeLimit', [maxSize.value])); file.status = 'removed'; return false; } + // 多选或者非图片不唤起裁剪框 + if ( + attrs.crop && + !attrs.multiple && + originFileList[0] && + isImageFile(file) + ) { + file.status = 'removed'; + // antd Upload组件问题 file参数获取的是UploadFile类型对象无法取到File类型 所以通过originFileList[0]获取 + const blob = await cropImage(originFileList[0], aspectRatio.value); + return new Promise((resolve, reject) => { + if (!blob) { + return reject(new Error($t('ui.crop.errorTip'))); + } + resolve(blob); + }); + } + return attrs.beforeUpload?.(file) ?? true; }; - const handleChange = async (event: UploadChangeParam) => { + const handleChange = (event: UploadChangeParam) => { + try { + // 行内写法 handleChange: (event) => {} + attrs.handleChange?.(event); + // template写法 @handle-change="(event) => {}" + attrs.onHandleChange?.(event); + } catch (error) { + // Avoid breaking internal v-model sync on user handler errors + console.error(error); + } fileList.value = event.fileList.filter( (file) => file.status !== 'removed', ); diff --git a/playground/src/locales/langs/en-US/examples.json b/playground/src/locales/langs/en-US/examples.json index 42a42d54e..05035487f 100644 --- a/playground/src/locales/langs/en-US/examples.json +++ b/playground/src/locales/langs/en-US/examples.json @@ -23,6 +23,7 @@ "upload-error": "Partial file upload failed", "upload-urls": "Urls after file upload", "file": "file", + "crop-image": "Crop image", "upload-image": "Click to upload image" }, "vxeTable": { @@ -75,5 +76,8 @@ }, "function": { "contentMenu": "Content Menu" + }, + "cropper": { + "title": "Cropper" } } diff --git a/playground/src/locales/langs/zh-CN/examples.json b/playground/src/locales/langs/zh-CN/examples.json index 8808b5a9a..3b0d934cf 100644 --- a/playground/src/locales/langs/zh-CN/examples.json +++ b/playground/src/locales/langs/zh-CN/examples.json @@ -26,6 +26,7 @@ "upload-error": "部分文件上传失败", "upload-urls": "文件上传后的网址", "file": "文件", + "crop-image": "裁剪图片", "upload-image": "点击上传图片" }, "vxeTable": { @@ -75,5 +76,8 @@ }, "function": { "contentMenu": "上下文菜单" + }, + "cropper": { + "title": "图片裁剪" } } diff --git a/playground/src/router/guard.ts b/playground/src/router/guard.ts index 514bd2eb6..167a84d10 100644 --- a/playground/src/router/guard.ts +++ b/playground/src/router/guard.ts @@ -108,9 +108,9 @@ function setupAccessGuard(router: Router) { let redirectPath: string; if (from.query.redirect) { redirectPath = from.query.redirect as string; - } else if (to.path === preferences.app.defaultHomePath) { + } else if (to.fullPath === preferences.app.defaultHomePath) { redirectPath = preferences.app.defaultHomePath; - } else if (userInfo.homePath && to.path === userInfo.homePath) { + } else if (userInfo.homePath && to.fullPath === userInfo.homePath) { redirectPath = userInfo.homePath; } else { redirectPath = to.fullPath; diff --git a/playground/src/router/routes/modules/demos.ts b/playground/src/router/routes/modules/demos.ts index 3df4f0ad6..b8d27ef5a 100644 --- a/playground/src/router/routes/modules/demos.ts +++ b/playground/src/router/routes/modules/demos.ts @@ -157,9 +157,7 @@ const routes: RouteRecordRaw[] = [ name: 'HideChildrenInMenuDemo', path: '', component: () => - import( - '#/views/demos/features/hide-menu-children/parent.vue' - ), + import('#/views/demos/features/hide-menu-children/parent.vue'), meta: { // hideInMenu: true, title: $t('demos.features.hideChildrenInMenu'), @@ -169,9 +167,7 @@ const routes: RouteRecordRaw[] = [ name: 'HideChildrenInMenuChildrenDemo', path: '/demos/features/hide-menu-children/children', component: () => - import( - '#/views/demos/features/hide-menu-children/children.vue' - ), + import('#/views/demos/features/hide-menu-children/children.vue'), meta: { activePath: '/demos/features/hide-menu-children', title: $t('demos.features.hideChildrenInMenu'), @@ -247,9 +243,7 @@ const routes: RouteRecordRaw[] = [ name: 'RequestParamsSerializerDemo', path: '/demos/features/request-params-serializer', component: () => - import( - '#/views/demos/features/request-params-serializer/index.vue' - ), + import('#/views/demos/features/request-params-serializer/index.vue'), meta: { icon: 'lucide:git-pull-request-arrow', title: $t('demos.features.requestParamsSerializer'), diff --git a/playground/src/router/routes/modules/examples.ts b/playground/src/router/routes/modules/examples.ts index 5361fcc8f..017b2c22f 100644 --- a/playground/src/router/routes/modules/examples.ts +++ b/playground/src/router/routes/modules/examples.ts @@ -337,6 +337,15 @@ const routes: RouteRecordRaw[] = [ title: $t('examples.function.contentMenu'), }, }, + { + name: 'CropperDemo', + path: '/examples/cropper', + component: () => import('#/views/examples/cropper/index.vue'), + meta: { + icon: 'mdi:crop', + title: $t('examples.cropper.title'), + }, + }, ], }, ]; diff --git a/playground/src/views/examples/context-menu/index.vue b/playground/src/views/examples/context-menu/index.vue index 4688015a6..711b4c6d4 100644 --- a/playground/src/views/examples/context-menu/index.vue +++ b/playground/src/views/examples/context-menu/index.vue @@ -43,15 +43,14 @@ const contextMenus = () => { }, ]; }; -