refactor: components use setup (#3299)

* refactor: /@/ => @/

* refactor: table demo use script setup

* refactor: change /@/ to @/
This commit is contained in:
xingyu
2023-11-20 12:27:11 +08:00
committed by GitHub
parent cb13986a17
commit bab28af986
231 changed files with 5790 additions and 6519 deletions

52
.vscode/settings.json vendored
View File

@@ -104,38 +104,36 @@
"i18n-ally.displayLanguage": "zh-CN", "i18n-ally.displayLanguage": "zh-CN",
"i18n-ally.enabledFrameworks": ["vue", "react"], "i18n-ally.enabledFrameworks": ["vue", "react"],
"cSpell.words": [ "cSpell.words": [
"vben", "antd",
"browserslist",
"tailwindcss",
"esnext",
"antv", "antv",
"tinymce", "brotli",
"browserslist",
"codemirror",
"commitlint",
"cropperjs",
"echarts",
"esnext",
"esno",
"iconify",
"INTLIFY",
"lintstagedrc",
"logicflow",
"mockjs",
"nprogress",
"pinia",
"pnpm",
"qrcode", "qrcode",
"sider", "sider",
"pinia",
"sider",
"nprogress",
"INTLIFY",
"stylelint",
"esno",
"vitejs",
"sortablejs", "sortablejs",
"mockjs", "stylelint",
"codemirror",
"iconify",
"commitlint",
"vditor",
"echarts",
"cropperjs",
"logicflow",
"vueuse",
"zxcvbn",
"lintstagedrc",
"brotli",
"tailwindcss", "tailwindcss",
"sider", "tinymce",
"pnpm", "unref",
"antd" "vben",
"vditor",
"vitejs",
"vueuse",
"zxcvbn"
], ],
"vetur.format.scriptInitialIndent": true, "vetur.format.scriptInitialIndent": true,
"vetur.format.styleInitialIndent": true, "vetur.format.styleInitialIndent": true,

View File

@@ -1,4 +1,4 @@
import { withInstall } from '/@/utils'; import { withInstall } from '@/utils';
import appLogo from './src/AppLogo.vue'; import appLogo from './src/AppLogo.vue';
import appProvider from './src/AppProvider.vue'; import appProvider from './src/AppProvider.vue';

View File

@@ -7,12 +7,12 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, unref } from 'vue'; import { computed, unref } from 'vue';
import { SvgIcon } from '/@/components/Icon'; import { SvgIcon } from '@/components/Icon';
import { ThemeEnum } from '/@/enums/appEnum'; import { ThemeEnum } from '@/enums/appEnum';
import { useRootSetting } from '/@/hooks/setting/useRootSetting'; import { useRootSetting } from '@/hooks/setting/useRootSetting';
import { useDesign } from '/@/hooks/web/useDesign'; import { useDesign } from '@/hooks/web/useDesign';
import { updateDarkTheme } from '/@/logics/theme/dark'; import { updateDarkTheme } from '@/logics/theme/dark';
import { updateHeaderBgColor, updateSidebarBgColor } from '/@/logics/theme/updateBackground'; import { updateHeaderBgColor, updateSidebarBgColor } from '@/logics/theme/updateBackground';
const { prefixCls } = useDesign('dark-switch'); const { prefixCls } = useDesign('dark-switch');
const { getDarkMode, setDarkMode, getShowDarkModeToggle } = useRootSetting(); const { getDarkMode, setDarkMode, getShowDarkModeToggle } = useRootSetting();
@@ -61,7 +61,9 @@
z-index: 1; z-index: 1;
width: 18px; width: 18px;
height: 18px; height: 18px;
transition: transform 0.5s, background-color 0.5s; transition:
transform 0.5s,
background-color 0.5s;
border-radius: 50%; border-radius: 50%;
background-color: #fff; background-color: #fff;
will-change: transform; will-change: transform;

View File

@@ -19,12 +19,12 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import type { LocaleType } from '/#/config'; import type { LocaleType } from '/#/config';
import type { DropMenu } from '/@/components/Dropdown'; import type { DropMenu } from '@/components/Dropdown';
import { ref, watchEffect, unref, computed } from 'vue'; import { ref, watchEffect, unref, computed } from 'vue';
import { Dropdown } from '/@/components/Dropdown'; import { Dropdown } from '@/components/Dropdown';
import Icon from '@/components/Icon/Icon.vue'; import Icon from '@/components/Icon/Icon.vue';
import { useLocale } from '/@/locales/useLocale'; import { useLocale } from '@/locales/useLocale';
import { localeList } from '/@/settings/localeSetting'; import { localeList } from '@/settings/localeSetting';
const props = defineProps({ const props = defineProps({
/** /**

View File

@@ -12,12 +12,12 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, unref } from 'vue'; import { computed, unref } from 'vue';
import { useGlobSetting } from '/@/hooks/setting'; import { useGlobSetting } from '@/hooks/setting';
import { useGo } from '/@/hooks/web/usePage'; import { useGo } from '@/hooks/web/usePage';
import { useMenuSetting } from '/@/hooks/setting/useMenuSetting'; import { useMenuSetting } from '@/hooks/setting/useMenuSetting';
import { useDesign } from '/@/hooks/web/useDesign'; import { useDesign } from '@/hooks/web/useDesign';
import { PageEnum } from '/@/enums/pageEnum'; import { PageEnum } from '@/enums/pageEnum';
import { useUserStore } from '/@/store/modules/user'; import { useUserStore } from '@/store/modules/user';
const props = defineProps({ const props = defineProps({
/** /**

View File

@@ -1,10 +1,10 @@
<script lang="ts"> <script lang="ts">
import { defineComponent, toRefs, ref, unref } from 'vue'; import { defineComponent, toRefs, ref, unref } from 'vue';
import { createAppProviderContext } from './useAppContext'; import { createAppProviderContext } from './useAppContext';
import { createBreakpointListen } from '/@/hooks/event/useBreakpoint'; import { createBreakpointListen } from '@/hooks/event/useBreakpoint';
import { prefixCls } from '/@/settings/designSetting'; import { prefixCls } from '@/settings/designSetting';
import { useAppStore } from '/@/store/modules/app'; import { useAppStore } from '@/store/modules/app';
import { MenuModeEnum, MenuTypeEnum } from '/@/enums/menuEnum'; import { MenuModeEnum, MenuTypeEnum } from '@/enums/menuEnum';
const props = { const props = {
/** /**

View File

@@ -3,7 +3,7 @@
import { Tooltip } from 'ant-design-vue'; import { Tooltip } from 'ant-design-vue';
import { SearchOutlined } from '@ant-design/icons-vue'; import { SearchOutlined } from '@ant-design/icons-vue';
import AppSearchModal from './AppSearchModal.vue'; import AppSearchModal from './AppSearchModal.vue';
import { useI18n } from '/@/hooks/web/useI18n'; import { useI18n } from '@/hooks/web/useI18n';
export default defineComponent({ export default defineComponent({
name: 'AppSearch', name: 'AppSearch',

View File

@@ -12,8 +12,8 @@
<script lang="ts" setup> <script lang="ts" setup>
import AppSearchKeyItem from './AppSearchKeyItem.vue'; import AppSearchKeyItem from './AppSearchKeyItem.vue';
import { useDesign } from '/@/hooks/web/useDesign'; import { useDesign } from '@/hooks/web/useDesign';
import { useI18n } from '/@/hooks/web/useI18n'; import { useI18n } from '@/hooks/web/useI18n';
const { prefixCls } = useDesign('app-search-footer'); const { prefixCls } = useDesign('app-search-footer');
const { t } = useI18n(); const { t } = useI18n();
@@ -44,7 +44,9 @@
padding-bottom: 2px; padding-bottom: 2px;
border-radius: 2px; border-radius: 2px;
background-color: linear-gradient(-225deg, #d5dbe4, #f8f8f8); background-color: linear-gradient(-225deg, #d5dbe4, #f8f8f8);
box-shadow: inset 0 -2px 0 0 #cdcde6, inset 0 0 1px 1px #fff, box-shadow:
inset 0 -2px 0 0 #cdcde6,
inset 0 0 1px 1px #fff,
0 1px 2px 1px rgb(30 35 90 / 40%); 0 1px 2px 1px rgb(30 35 90 / 40%);
&:nth-child(2), &:nth-child(2),

View File

@@ -63,12 +63,12 @@
import AppSearchFooter from './AppSearchFooter.vue'; import AppSearchFooter from './AppSearchFooter.vue';
import Icon from '@/components/Icon/Icon.vue'; import Icon from '@/components/Icon/Icon.vue';
// @ts-ignore // @ts-ignore
import vClickOutside from '/@/directives/clickOutside'; import vClickOutside from '@/directives/clickOutside';
import { useDesign } from '/@/hooks/web/useDesign'; import { useDesign } from '@/hooks/web/useDesign';
import { useRefs } from '@vben/hooks'; import { useRefs } from '@vben/hooks';
import { useMenuSearch } from './useMenuSearch'; import { useMenuSearch } from './useMenuSearch';
import { useI18n } from '/@/hooks/web/useI18n'; import { useI18n } from '@/hooks/web/useI18n';
import { useAppInject } from '/@/hooks/web/useAppInject'; import { useAppInject } from '@/hooks/web/useAppInject';
const props = defineProps({ const props = defineProps({
visible: { type: Boolean }, visible: { type: Boolean },

View File

@@ -1,13 +1,13 @@
import { type Menu } from '/@/router/types'; import { type Menu } from '@/router/types';
import { type AnyFunction } from '@vben/types'; import { type AnyFunction } from '@vben/types';
import { ref, onBeforeMount, unref, Ref, nextTick } from 'vue'; import { ref, onBeforeMount, unref, Ref, nextTick } from 'vue';
import { getMenus } from '/@/router/menus'; import { getMenus } from '@/router/menus';
import { cloneDeep } from 'lodash-es'; import { cloneDeep } from 'lodash-es';
import { filter, forEach } from '/@/utils/helper/treeHelper'; import { filter, forEach } from '@/utils/helper/treeHelper';
import { useGo } from '/@/hooks/web/usePage'; import { useGo } from '@/hooks/web/usePage';
import { useScrollTo } from '@vben/hooks'; import { useScrollTo } from '@vben/hooks';
import { onKeyStroke, useDebounceFn } from '@vueuse/core'; import { onKeyStroke, useDebounceFn } from '@vueuse/core';
import { useI18n } from '/@/hooks/web/useI18n'; import { useI18n } from '@/hooks/web/useI18n';
export interface SearchResult { export interface SearchResult {
name: string; name: string;

View File

@@ -1,5 +1,5 @@
import { InjectionKey, Ref } from 'vue'; import { InjectionKey, Ref } from 'vue';
import { createContext, useContext } from '/@/hooks/core/useContext'; import { createContext, useContext } from '@/hooks/core/useContext';
export interface AppProviderContextProps { export interface AppProviderContextProps {
prefixCls: Ref<string>; prefixCls: Ref<string>;

View File

@@ -1,4 +1,4 @@
import { withInstall } from '/@/utils'; import { withInstall } from '@/utils';
import authority from './src/Authority.vue'; import authority from './src/Authority.vue';
export const Authority = withInstall(authority); export const Authority = withInstall(authority);

View File

@@ -4,9 +4,9 @@
<script lang="ts"> <script lang="ts">
import type { PropType } from 'vue'; import type { PropType } from 'vue';
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import { RoleEnum } from '/@/enums/roleEnum'; import { RoleEnum } from '@/enums/roleEnum';
import { usePermission } from '/@/hooks/web/usePermission'; import { usePermission } from '@/hooks/web/usePermission';
import { getSlot } from '/@/utils/helper/tsxHelper'; import { getSlot } from '@/utils/helper/tsxHelper';
export default defineComponent({ export default defineComponent({
name: 'Authority', name: 'Authority',

View File

@@ -1,4 +1,4 @@
import { withInstall } from '/@/utils'; import { withInstall } from '@/utils';
import basicArrow from './src/BasicArrow.vue'; import basicArrow from './src/BasicArrow.vue';
import basicTitle from './src/BasicTitle.vue'; import basicTitle from './src/BasicTitle.vue';
import basicHelp from './src/BasicHelp.vue'; import basicHelp from './src/BasicHelp.vue';

View File

@@ -10,7 +10,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed } from 'vue'; import { computed } from 'vue';
import Icon from '@/components/Icon/Icon.vue'; import Icon from '@/components/Icon/Icon.vue';
import { useDesign } from '/@/hooks/web/useDesign'; import { useDesign } from '@/hooks/web/useDesign';
const props = defineProps({ const props = defineProps({
/** /**

View File

@@ -3,10 +3,10 @@
import { defineComponent, computed, unref } from 'vue'; import { defineComponent, computed, unref } from 'vue';
import { Tooltip } from 'ant-design-vue'; import { Tooltip } from 'ant-design-vue';
import { InfoCircleOutlined } from '@ant-design/icons-vue'; import { InfoCircleOutlined } from '@ant-design/icons-vue';
import { getPopupContainer } from '/@/utils'; import { getPopupContainer } from '@/utils';
import { isString, isArray } from '/@/utils/is'; import { isString, isArray } from '@/utils/is';
import { getSlot } from '/@/utils/helper/tsxHelper'; import { getSlot } from '@/utils/helper/tsxHelper';
import { useDesign } from '/@/hooks/web/useDesign'; import { useDesign } from '@/hooks/web/useDesign';
const props = { const props = {
/** /**

View File

@@ -8,7 +8,7 @@
import type { PropType } from 'vue'; import type { PropType } from 'vue';
import { useSlots, computed } from 'vue'; import { useSlots, computed } from 'vue';
import BasicHelp from './BasicHelp.vue'; import BasicHelp from './BasicHelp.vue';
import { useDesign } from '/@/hooks/web/useDesign'; import { useDesign } from '@/hooks/web/useDesign';
const props = defineProps({ const props = defineProps({
/** /**

View File

@@ -1,4 +1,4 @@
import { withInstall } from '/@/utils'; import { withInstall } from '@/utils';
import type { ExtractPropTypes } from 'vue'; import type { ExtractPropTypes } from 'vue';
import button from './src/BasicButton.vue'; import button from './src/BasicButton.vue';
import popConfirmButton from './src/PopConfirmButton.vue'; import popConfirmButton from './src/PopConfirmButton.vue';

View File

@@ -2,10 +2,10 @@
import { computed, defineComponent, h, unref } from 'vue'; import { computed, defineComponent, h, unref } from 'vue';
import BasicButton from './BasicButton.vue'; import BasicButton from './BasicButton.vue';
import { Popconfirm } from 'ant-design-vue'; import { Popconfirm } from 'ant-design-vue';
import { extendSlots } from '/@/utils/helper/tsxHelper'; import { extendSlots } from '@/utils/helper/tsxHelper';
import { omit } from 'lodash-es'; import { omit } from 'lodash-es';
import { useAttrs } from '@vben/hooks'; import { useAttrs } from '@vben/hooks';
import { useI18n } from '/@/hooks/web/useI18n'; import { useI18n } from '@/hooks/web/useI18n';
const props = { const props = {
/** /**

View File

@@ -1,4 +1,4 @@
import { withInstall } from '/@/utils'; import { withInstall } from '@/utils';
import cardList from './src/CardList.vue'; import cardList from './src/CardList.vue';
export const CardList = withInstall(cardList); export const CardList = withInstall(cardList);

View File

@@ -10,22 +10,23 @@
:pagination="paginationProp" :pagination="paginationProp"
> >
<template #header> <template #header>
<div class="flex justify-end space-x-2" <div class="flex justify-end space-x-2">
><slot name="header"></slot> <slot name="header"> </slot>
<Tooltip> <Tooltip>
<template #title> <template #title>
<div class="w-50">每行显示数量</div <div class="w-50">每行显示数量</div>
><Slider <Slider
id="slider" id="slider"
v-bind="sliderProp" v-bind="sliderProp"
v-model:value="grid" v-model:value="grid"
@change="sliderChange" @change="sliderChange"
/></template> />
<Button><TableOutlined /></Button> </template>
<a-button><TableOutlined /></a-button>
</Tooltip> </Tooltip>
<Tooltip @click="fetch"> <Tooltip @click="fetch">
<template #title>刷新</template> <template #title>刷新</template>
<Button><RedoOutlined /></Button> <a-button><RedoOutlined /></a-button>
</Tooltip> </Tooltip>
</div> </div>
</template> </template>
@@ -83,11 +84,10 @@
TableOutlined, TableOutlined,
} from '@ant-design/icons-vue'; } from '@ant-design/icons-vue';
import { List, Card, Image, Typography, Tooltip, Slider, Avatar } from 'ant-design-vue'; import { List, Card, Image, Typography, Tooltip, Slider, Avatar } from 'ant-design-vue';
import { Dropdown } from '/@/components/Dropdown'; import { Dropdown } from '@/components/Dropdown';
import { BasicForm, useForm } from '/@/components/Form'; import { BasicForm, useForm } from '@/components/Form';
import { propTypes } from '/@/utils/propTypes'; import { propTypes } from '@/utils/propTypes';
import { Button } from '/@/components/Button'; import { isFunction } from '@/utils/is';
import { isFunction } from '/@/utils/is';
import { useSlider, grid } from './data'; import { useSlider, grid } from './data';
const ListItem = List.Item; const ListItem = List.Item;

View File

@@ -1,4 +1,4 @@
import { withInstall } from '/@/utils'; import { withInstall } from '@/utils';
import clickOutSide from './src/ClickOutSide.vue'; import clickOutSide from './src/ClickOutSide.vue';
export const ClickOutSide = withInstall(clickOutSide); export const ClickOutSide = withInstall(clickOutSide);

View File

@@ -1,4 +1,4 @@
import { withInstall } from '/@/utils'; import { withInstall } from '@/utils';
import codeEditor from './src/CodeEditor.vue'; import codeEditor from './src/CodeEditor.vue';
import jsonPreview from './src/json-preview/JsonPreview.vue'; import jsonPreview from './src/json-preview/JsonPreview.vue';

View File

@@ -12,7 +12,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed } from 'vue'; import { computed } from 'vue';
import CodeMirrorEditor from './codemirror/CodeMirror.vue'; import CodeMirrorEditor from './codemirror/CodeMirror.vue';
import { isString } from '/@/utils/is'; import { isString } from '@/utils/is';
import { MODE } from './typing'; import { MODE } from './typing';
const props = defineProps({ const props = defineProps({

View File

@@ -20,7 +20,7 @@
import type { Nullable } from '@vben/types'; import type { Nullable } from '@vben/types';
import { useWindowSizeFn } from '@vben/hooks'; import { useWindowSizeFn } from '@vben/hooks';
import { useDebounceFn } from '@vueuse/core'; import { useDebounceFn } from '@vueuse/core';
import { useAppStore } from '/@/store/modules/app'; import { useAppStore } from '@/store/modules/app';
import CodeMirror from 'codemirror'; import CodeMirror from 'codemirror';
import { MODE } from './../typing'; import { MODE } from './../typing';
// css // css

View File

@@ -1,4 +1,4 @@
import { withInstall } from '/@/utils'; import { withInstall } from '@/utils';
import collapseContainer from './src/collapse/CollapseContainer.vue'; import collapseContainer from './src/collapse/CollapseContainer.vue';
import scrollContainer from './src/ScrollContainer.vue'; import scrollContainer from './src/ScrollContainer.vue';

View File

@@ -9,80 +9,69 @@
</Scrollbar> </Scrollbar>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { defineComponent, ref, unref, nextTick } from 'vue'; import { ref, unref, nextTick } from 'vue';
import { Scrollbar, ScrollbarType } from '/@/components/Scrollbar'; import { Scrollbar, ScrollbarType } from '@/components/Scrollbar';
import { useScrollTo } from '@vben/hooks'; import { useScrollTo } from '@vben/hooks';
import { type Nullable } from '@vben/types'; import { type Nullable } from '@vben/types';
export default defineComponent({ defineOptions({ name: 'ScrollContainer' });
name: 'ScrollContainer',
components: { Scrollbar }, defineProps({
props: { scrollHeight: {
scrollHeight: { type: Number }, type: Number,
}, },
setup() { });
const scrollbarRef = ref<Nullable<ScrollbarType>>(null);
/** const scrollbarRef = ref<Nullable<ScrollbarType>>(null);
* Scroll to the specified position
*/ function getScrollWrap() {
function scrollTo(to: number, duration = 500) { const scrollbar = unref(scrollbarRef);
const scrollbar = unref(scrollbarRef); if (!scrollbar) {
if (!scrollbar) { return null;
return; }
} return scrollbar.wrap;
nextTick(() => { }
const wrap = unref(scrollbar.wrap);
if (!wrap) { /**
return; * Scroll to the specified position
} */
const { start } = useScrollTo({ function scrollTo(to: number, duration = 500) {
el: wrap, const wrap = unref(getScrollWrap());
to, nextTick(() => {
duration, if (!wrap) {
}); return;
start();
});
} }
const { start } = useScrollTo({
el: wrap,
to,
duration,
});
start();
});
}
function getScrollWrap() { /**
const scrollbar = unref(scrollbarRef); * Scroll to the bottom
if (!scrollbar) { */
return null; function scrollBottom() {
} const wrap = unref(getScrollWrap());
return scrollbar.wrap; nextTick(() => {
if (!wrap) {
return;
} }
const scrollHeight = wrap.scrollHeight as number;
const { start } = useScrollTo({
el: wrap,
to: scrollHeight,
});
start();
});
}
/** defineExpose({
* Scroll to the bottom scrollTo,
*/ scrollBottom,
function scrollBottom() {
const scrollbar = unref(scrollbarRef);
if (!scrollbar) {
return;
}
nextTick(() => {
const wrap = unref(scrollbar.wrap) as any;
if (!wrap) {
return;
}
const scrollHeight = wrap.scrollHeight as number;
const { start } = useScrollTo({
el: wrap,
to: scrollHeight,
});
start();
});
}
return {
scrollbarRef,
scrollTo,
scrollBottom,
getScrollWrap,
};
},
}); });
</script> </script>
<style lang="less"> <style lang="less">

View File

@@ -3,10 +3,10 @@
import { isNil } from 'lodash-es'; import { isNil } from 'lodash-es';
import { Skeleton } from 'ant-design-vue'; import { Skeleton } from 'ant-design-vue';
import { useTimeoutFn } from '@vben/hooks'; import { useTimeoutFn } from '@vben/hooks';
import { CollapseTransition } from '/@/components/Transition'; import { CollapseTransition } from '@/components/Transition';
import CollapseHeader from './CollapseHeader.vue'; import CollapseHeader from './CollapseHeader.vue';
import { triggerWindowResize } from '/@/utils/event'; import { triggerWindowResize } from '@/utils/event';
import { useDesign } from '/@/hooks/web/useDesign'; import { useDesign } from '@/hooks/web/useDesign';
const collapseContainerProps = { const collapseContainerProps = {
title: { type: String, default: '' }, title: { type: String, default: '' },

View File

@@ -1,7 +1,7 @@
<script lang="tsx"> <script lang="tsx">
import { defineComponent, computed, unref, type ExtractPropTypes, PropType } from 'vue'; import { defineComponent, computed, unref, type ExtractPropTypes, PropType } from 'vue';
import { useDesign } from '/@/hooks/web/useDesign'; import { useDesign } from '@/hooks/web/useDesign';
import { BasicArrow, BasicTitle } from '/@/components/Basic'; import { BasicArrow, BasicTitle } from '@/components/Basic';
const collapseHeaderProps = { const collapseHeaderProps = {
prefixCls: String, prefixCls: String,

View File

@@ -1,5 +1,5 @@
import contextMenuVue from './ContextMenu.vue'; import contextMenuVue from './ContextMenu.vue';
import { isClient } from '/@/utils/is'; import { isClient } from '@/utils/is';
import { CreateContextOptions, ContextMenuProps } from './typing'; import { CreateContextOptions, ContextMenuProps } from './typing';
import { createVNode, render } from 'vue'; import { createVNode, render } from 'vue';

View File

@@ -1,4 +1,4 @@
import { withInstall } from '/@/utils'; import { withInstall } from '@/utils';
import countButton from './src/CountButton.vue'; import countButton from './src/CountButton.vue';
import countdownInput from './src/CountdownInput.vue'; import countdownInput from './src/CountdownInput.vue';

View File

@@ -3,60 +3,53 @@
{{ getButtonText }} {{ getButtonText }}
</Button> </Button>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { defineComponent, ref, watchEffect, computed, unref } from 'vue'; import { ref, watchEffect, computed, unref } from 'vue';
import { Button } from 'ant-design-vue'; import { Button } from 'ant-design-vue';
import { useCountdown } from './useCountdown'; import { useCountdown } from './useCountdown';
import { isFunction } from '/@/utils/is'; import { isFunction } from '@/utils/is';
import { useI18n } from '/@/hooks/web/useI18n'; import { useI18n } from '@/hooks/web/useI18n';
const props = { defineOptions({ name: 'CountButton' });
const props = defineProps({
value: { type: [Object, Number, String, Array] }, value: { type: [Object, Number, String, Array] },
count: { type: Number, default: 60 }, count: { type: Number, default: 60 },
beforeStartFunc: { beforeStartFunc: {
type: Function as PropType<() => Promise<boolean>>, type: Function as PropType<() => Promise<boolean>>,
default: null, default: null,
}, },
};
export default defineComponent({
name: 'CountButton',
components: { Button },
props,
setup(props) {
const loading = ref(false);
const { currentCount, isStart, start, reset } = useCountdown(props.count);
const { t } = useI18n();
const getButtonText = computed(() => {
return !unref(isStart)
? t('component.countdown.normalText')
: t('component.countdown.sendText', [unref(currentCount)]);
});
watchEffect(() => {
props.value === undefined && reset();
});
/**
* @description: Judge whether there is an external function before execution, and decide whether to start after execution
*/
async function handleStart() {
const { beforeStartFunc } = props;
if (beforeStartFunc && isFunction(beforeStartFunc)) {
loading.value = true;
try {
const canStart = await beforeStartFunc();
canStart && start();
} finally {
loading.value = false;
}
} else {
start();
}
}
return { handleStart, currentCount, loading, getButtonText, isStart };
},
}); });
const { t } = useI18n();
const loading = ref(false);
const { currentCount, isStart, start, reset } = useCountdown(props.count);
const getButtonText = computed(() => {
return !unref(isStart)
? t('component.countdown.normalText')
: t('component.countdown.sendText', [unref(currentCount)]);
});
watchEffect(() => {
props.value === undefined && reset();
});
/**
* @description: Judge whether there is an external function before execution, and decide whether to start after execution
*/
async function handleStart() {
const { beforeStartFunc } = props;
if (beforeStartFunc && isFunction(beforeStartFunc)) {
loading.value = true;
try {
const canStart = await beforeStartFunc();
canStart && start();
} finally {
loading.value = false;
}
} else {
start();
}
}
</script> </script>

View File

@@ -8,34 +8,26 @@
</template> </template>
</a-input> </a-input>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { defineComponent, PropType } from 'vue'; import { PropType } from 'vue';
import CountButton from './CountButton.vue'; import CountButton from './CountButton.vue';
import { useDesign } from '/@/hooks/web/useDesign'; import { useDesign } from '@/hooks/web/useDesign';
import { useRuleFormItem } from '/@/hooks/component/useFormItem'; import { useRuleFormItem } from '@/hooks/component/useFormItem';
const props = { defineOptions({ name: 'CountDownInput', inheritAttrs: false });
const props = defineProps({
value: { type: String }, value: { type: String },
size: { type: String, validator: (v) => ['default', 'large', 'small'].includes(v) }, size: { type: String, validator: (v: string) => ['default', 'large', 'small'].includes(v) },
count: { type: Number, default: 60 }, count: { type: Number, default: 60 },
sendCodeApi: { sendCodeApi: {
type: Function as PropType<() => Promise<boolean>>, type: Function as PropType<() => Promise<boolean>>,
default: null, default: null,
}, },
};
export default defineComponent({
name: 'CountDownInput',
components: { CountButton },
inheritAttrs: false,
props,
setup(props) {
const { prefixCls } = useDesign('countdown-input');
const [state] = useRuleFormItem(props);
return { prefixCls, state };
},
}); });
const { prefixCls } = useDesign('countdown-input');
const [state] = useRuleFormItem(props);
</script> </script>
<style lang="less"> <style lang="less">
@prefix-cls: ~'@{namespace}-countdown-input'; @prefix-cls: ~'@{namespace}-countdown-input';

View File

@@ -1,4 +1,4 @@
import { withInstall } from '/@/utils'; import { withInstall } from '@/utils';
import countTo from './src/CountTo.vue'; import countTo from './src/CountTo.vue';
export const CountTo = withInstall(countTo); export const CountTo = withInstall(countTo);

View File

@@ -3,12 +3,14 @@
{{ value }} {{ value }}
</span> </span>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { defineComponent, ref, computed, watchEffect, unref, onMounted, watch } from 'vue'; import { ref, computed, watchEffect, unref, onMounted, watch } from 'vue';
import { useTransition, TransitionPresets } from '@vueuse/core'; import { useTransition, TransitionPresets } from '@vueuse/core';
import { isNumber } from '/@/utils/is'; import { isNumber } from '@/utils/is';
const props = { defineOptions({ name: 'CountTo' });
const props = defineProps({
startVal: { type: Number, default: 0 }, startVal: { type: Number, default: 0 },
endVal: { type: Number, default: 2021 }, endVal: { type: Number, default: 2021 },
duration: { type: Number, default: 1500 }, duration: { type: Number, default: 1500 },
@@ -36,75 +38,70 @@
* Digital animation * Digital animation
*/ */
transition: { type: String, default: 'linear' }, transition: { type: String, default: 'linear' },
};
export default defineComponent({
name: 'CountTo',
props,
emits: ['onStarted', 'onFinished'],
setup(props, { emit }) {
const source = ref(props.startVal);
const disabled = ref(false);
let outputValue = useTransition(source);
const value = computed(() => formatNumber(unref(outputValue)));
watchEffect(() => {
source.value = props.startVal;
});
watch([() => props.startVal, () => props.endVal], () => {
if (props.autoplay) {
start();
}
});
onMounted(() => {
props.autoplay && start();
});
function start() {
run();
source.value = props.endVal;
}
function reset() {
source.value = props.startVal;
run();
}
function run() {
outputValue = useTransition(source, {
disabled,
duration: props.duration,
onFinished: () => emit('onFinished'),
onStarted: () => emit('onStarted'),
...(props.useEasing ? { transition: TransitionPresets[props.transition] } : {}),
});
}
function formatNumber(num: number | string) {
if (!num && num !== 0) {
return '';
}
const { decimals, decimal, separator, suffix, prefix } = props;
num = Number(num).toFixed(decimals);
num += '';
const x = num.split('.');
let x1 = x[0];
const x2 = x.length > 1 ? decimal + x[1] : '';
const rgx = /(\d+)(\d{3})/;
if (separator && !isNumber(separator)) {
while (rgx.test(x1)) {
x1 = x1.replace(rgx, '$1' + separator + '$2');
}
}
return prefix + x1 + x2 + suffix;
}
return { value, start, reset };
},
}); });
const emit = defineEmits(['onStarted', 'onFinished']);
const source = ref(props.startVal);
const disabled = ref(false);
let outputValue = useTransition(source);
const value = computed(() => formatNumber(unref(outputValue)));
watchEffect(() => {
source.value = props.startVal;
});
watch([() => props.startVal, () => props.endVal], () => {
if (props.autoplay) {
start();
}
});
onMounted(() => {
props.autoplay && start();
});
function start() {
run();
source.value = props.endVal;
}
function reset() {
source.value = props.startVal;
run();
}
function run() {
outputValue = useTransition(source, {
disabled,
duration: props.duration,
onFinished: () => emit('onFinished'),
onStarted: () => emit('onStarted'),
...(props.useEasing ? { transition: TransitionPresets[props.transition] } : {}),
});
}
function formatNumber(num: number | string) {
if (!num && num !== 0) {
return '';
}
const { decimals, decimal, separator, suffix, prefix } = props;
num = Number(num).toFixed(decimals);
num += '';
const x = num.split('.');
let x1 = x[0];
const x2 = x.length > 1 ? decimal + x[1] : '';
const rgx = /(\d+)(\d{3})/;
if (separator && !isNumber(separator)) {
while (rgx.test(x1)) {
x1 = x1.replace(rgx, '$1' + separator + '$2');
}
}
return prefix + x1 + x2 + suffix;
}
defineExpose({ reset });
</script> </script>

View File

@@ -1,4 +1,4 @@
import { withInstall } from '/@/utils'; import { withInstall } from '@/utils';
import cropperImage from './src/Cropper.vue'; import cropperImage from './src/Cropper.vue';
import avatarCropper from './src/CropperAvatar.vue'; import avatarCropper from './src/CropperAvatar.vue';

View File

@@ -10,13 +10,14 @@
/> />
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import type { CSSProperties } from 'vue'; import type { CSSProperties } from 'vue';
import { defineComponent, onMounted, ref, unref, computed, onUnmounted } from 'vue'; import { onMounted, ref, unref, computed, onUnmounted } from 'vue';
import Cropper from 'cropperjs'; import Cropper from 'cropperjs';
import 'cropperjs/dist/cropper.css'; import 'cropperjs/dist/cropper.css';
import { useDesign } from '/@/hooks/web/useDesign'; import { useDesign } from '@/hooks/web/useDesign';
import { useDebounceFn } from '@vueuse/shared'; import { useDebounceFn } from '@vueuse/shared';
import { useAttrs } from '@vben/hooks';
type Options = Cropper.Options; type Options = Cropper.Options;
@@ -43,7 +44,9 @@
rotatable: true, rotatable: true,
}; };
const props = { defineOptions({ name: 'CropperImage' });
const props = defineProps({
src: { type: String, required: true }, src: { type: String, required: true },
alt: { type: String }, alt: { type: String },
circled: { type: Boolean, default: false }, circled: { type: Boolean, default: false },
@@ -55,124 +58,119 @@
}, },
imageStyle: { type: Object as PropType<CSSProperties>, default: () => ({}) }, imageStyle: { type: Object as PropType<CSSProperties>, default: () => ({}) },
options: { type: Object as PropType<Options>, default: () => ({}) }, options: { type: Object as PropType<Options>, default: () => ({}) },
};
export default defineComponent({
name: 'CropperImage',
props,
emits: ['cropend', 'ready', 'cropendError'],
setup(props, { attrs, emit }) {
const imgElRef = ref<ElRef<HTMLImageElement>>();
const cropper = ref<Nullable<Cropper>>();
const isReady = ref(false);
const { prefixCls } = useDesign('cropper-image');
const debounceRealTimeCroppered = useDebounceFn(realTimeCroppered, 80);
const getImageStyle = computed((): CSSProperties => {
return {
height: props.height,
maxWidth: '100%',
...props.imageStyle,
};
});
const getClass = computed(() => {
return [
prefixCls,
attrs.class,
{
[`${prefixCls}--circled`]: props.circled,
},
];
});
const getWrapperStyle = computed((): CSSProperties => {
return { height: `${props.height}`.replace(/px/, '') + 'px' };
});
onMounted(init);
onUnmounted(() => {
cropper.value?.destroy();
});
async function init() {
const imgEl = unref(imgElRef);
if (!imgEl) {
return;
}
cropper.value = new Cropper(imgEl, {
...defaultOptions,
ready: () => {
isReady.value = true;
realTimeCroppered();
emit('ready', cropper.value);
},
crop() {
debounceRealTimeCroppered();
},
zoom() {
debounceRealTimeCroppered();
},
cropmove() {
debounceRealTimeCroppered();
},
...props.options,
});
}
// Real-time display preview
function realTimeCroppered() {
props.realTimePreview && croppered();
}
// event: return base64 and width and height information after cropping
function croppered() {
if (!cropper.value) {
return;
}
let imgInfo = cropper.value.getData();
const canvas = props.circled ? getRoundedCanvas() : cropper.value.getCroppedCanvas();
canvas.toBlob((blob) => {
if (!blob) {
return;
}
let fileReader: FileReader = new FileReader();
fileReader.readAsDataURL(blob);
fileReader.onloadend = (e) => {
emit('cropend', {
imgBase64: e.target?.result ?? '',
imgInfo,
});
};
fileReader.onerror = () => {
emit('cropendError');
};
}, 'image/png');
}
// Get a circular picture canvas
function getRoundedCanvas() {
const sourceCanvas = cropper.value!.getCroppedCanvas();
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d')!;
const width = sourceCanvas.width;
const height = sourceCanvas.height;
canvas.width = width;
canvas.height = height;
context.imageSmoothingEnabled = true;
context.drawImage(sourceCanvas, 0, 0, width, height);
context.globalCompositeOperation = 'destination-in';
context.beginPath();
context.arc(width / 2, height / 2, Math.min(width, height) / 2, 0, 2 * Math.PI, true);
context.fill();
return canvas;
}
return { getClass, imgElRef, getWrapperStyle, getImageStyle, isReady, croppered };
},
}); });
const emit = defineEmits(['cropend', 'ready', 'cropendError']);
const attrs = useAttrs();
const imgElRef = ref<ElRef<HTMLImageElement>>();
const cropper = ref<Nullable<Cropper>>();
const isReady = ref(false);
const { prefixCls } = useDesign('cropper-image');
const debounceRealTimeCroppered = useDebounceFn(realTimeCroppered, 80);
const getImageStyle = computed((): CSSProperties => {
return {
height: props.height,
maxWidth: '100%',
...props.imageStyle,
};
});
const getClass = computed(() => {
return [
prefixCls,
attrs.class,
{
[`${prefixCls}--circled`]: props.circled,
},
];
});
const getWrapperStyle = computed((): CSSProperties => {
return { height: `${props.height}`.replace(/px/, '') + 'px' };
});
onMounted(init);
onUnmounted(() => {
cropper.value?.destroy();
});
async function init() {
const imgEl = unref(imgElRef);
if (!imgEl) {
return;
}
cropper.value = new Cropper(imgEl, {
...defaultOptions,
ready: () => {
isReady.value = true;
realTimeCroppered();
emit('ready', cropper.value);
},
crop() {
debounceRealTimeCroppered();
},
zoom() {
debounceRealTimeCroppered();
},
cropmove() {
debounceRealTimeCroppered();
},
...props.options,
});
}
// Real-time display preview
function realTimeCroppered() {
props.realTimePreview && croppered();
}
// event: return base64 and width and height information after cropping
function croppered() {
if (!cropper.value) {
return;
}
let imgInfo = cropper.value.getData();
const canvas = props.circled ? getRoundedCanvas() : cropper.value.getCroppedCanvas();
canvas.toBlob((blob) => {
if (!blob) {
return;
}
let fileReader: FileReader = new FileReader();
fileReader.readAsDataURL(blob);
fileReader.onloadend = (e) => {
emit('cropend', {
imgBase64: e.target?.result ?? '',
imgInfo,
});
};
fileReader.onerror = () => {
emit('cropendError');
};
}, 'image/png');
}
// Get a circular picture canvas
function getRoundedCanvas() {
const sourceCanvas = cropper.value!.getCroppedCanvas();
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d')!;
const width = sourceCanvas.width;
const height = sourceCanvas.height;
canvas.width = width;
canvas.height = height;
context.imageSmoothingEnabled = true;
context.drawImage(sourceCanvas, 0, 0, width, height);
context.globalCompositeOperation = 'destination-in';
context.beginPath();
context.arc(width / 2, height / 2, Math.min(width, height) / 2, 0, 2 * Math.PI, true);
context.fill();
return canvas;
}
</script> </script>
<style lang="less"> <style lang="less">
@prefix-cls: ~'@{namespace}-cropper-image'; @prefix-cls: ~'@{namespace}-cropper-image';

View File

@@ -1,6 +1,6 @@
<template> <template>
<div :class="getClass" :style="getStyle"> <div :class="getClass" :style="getStyle">
<div :class="`${prefixCls}-image-wrapper`" :style="getImageWrapperStyle" @click="openModal"> <div :class="`${prefixCls}-image-wrapper`" :style="getImageWrapperStyle" @click="openModal()">
<div :class="`${prefixCls}-image-mask`" :style="getImageWrapperStyle"> <div :class="`${prefixCls}-image-mask`" :style="getImageWrapperStyle">
<Icon <Icon
icon="ant-design:cloud-upload-outlined" icon="ant-design:cloud-upload-outlined"
@@ -29,26 +29,19 @@
/> />
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { import { computed, CSSProperties, unref, ref, watchEffect, watch, PropType } from 'vue';
defineComponent,
computed,
CSSProperties,
unref,
ref,
watchEffect,
watch,
PropType,
} from 'vue';
import CropperModal from './CropperModal.vue'; import CropperModal from './CropperModal.vue';
import { useDesign } from '/@/hooks/web/useDesign'; import { useDesign } from '@/hooks/web/useDesign';
import { useModal } from '/@/components/Modal'; import { useModal } from '@/components/Modal';
import { useMessage } from '/@/hooks/web/useMessage'; import { useMessage } from '@/hooks/web/useMessage';
import { useI18n } from '/@/hooks/web/useI18n'; import { useI18n } from '@/hooks/web/useI18n';
import type { ButtonProps } from '/@/components/Button'; import type { ButtonProps } from '@/components/Button';
import Icon from '@/components/Icon/Icon.vue'; import Icon from '@/components/Icon/Icon.vue';
const props = { defineOptions({ name: 'CropperAvatar' });
const props = defineProps({
width: { type: [String, Number], default: '200px' }, width: { type: [String, Number], default: '200px' },
value: { type: String }, value: { type: String },
showBtn: { type: Boolean, default: true }, showBtn: { type: Boolean, default: true },
@@ -59,65 +52,46 @@
}, },
size: { type: Number, default: 5 }, size: { type: Number, default: 5 },
};
export default defineComponent({
name: 'CropperAvatar',
components: { CropperModal, Icon },
props,
emits: ['update:value', 'change'],
setup(props, { emit, expose }) {
const sourceValue = ref(props.value || '');
const { prefixCls } = useDesign('cropper-avatar');
const [register, { openModal, closeModal }] = useModal();
const { createMessage } = useMessage();
const { t } = useI18n();
const getClass = computed(() => [prefixCls]);
const getWidth = computed(() => `${props.width}`.replace(/px/, '') + 'px');
const getIconWidth = computed(() => parseInt(`${props.width}`.replace(/px/, '')) / 2 + 'px');
const getStyle = computed((): CSSProperties => ({ width: unref(getWidth) }));
const getImageWrapperStyle = computed(
(): CSSProperties => ({ width: unref(getWidth), height: unref(getWidth) }),
);
watchEffect(() => {
sourceValue.value = props.value || '';
});
watch(
() => sourceValue.value,
(v: string) => {
emit('update:value', v);
},
);
function handleUploadSuccess({ source, data }) {
sourceValue.value = source;
emit('change', { source, data });
createMessage.success(t('component.cropper.uploadSuccess'));
}
expose({ openModal: openModal.bind(null, true), closeModal });
return {
t,
prefixCls,
register,
openModal: openModal as any,
getIconWidth,
sourceValue,
getClass,
getImageWrapperStyle,
getStyle,
handleUploadSuccess,
};
},
}); });
const emit = defineEmits(['update:value', 'change']);
const sourceValue = ref(props.value || '');
const { prefixCls } = useDesign('cropper-avatar');
const [register, { openModal, closeModal }] = useModal();
const { createMessage } = useMessage();
const { t } = useI18n();
const getClass = computed(() => [prefixCls]);
const getWidth = computed(() => `${props.width}`.replace(/px/, '') + 'px');
const getIconWidth = computed(() => parseInt(`${props.width}`.replace(/px/, '')) / 2 + 'px');
const getStyle = computed((): CSSProperties => ({ width: unref(getWidth) }));
const getImageWrapperStyle = computed(
(): CSSProperties => ({ width: unref(getWidth), height: unref(getWidth) }),
);
watchEffect(() => {
sourceValue.value = props.value || '';
});
watch(
() => sourceValue.value,
(v: string) => {
emit('update:value', v);
},
);
function handleUploadSuccess({ source, data }) {
sourceValue.value = source;
emit('change', { source, data });
createMessage.success(t('component.cropper.uploadSuccess'));
}
defineExpose({ openModal: openModal.bind(null, true), closeModal });
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>

View File

@@ -1,289 +1,274 @@
<template> <template>
<BasicModal <BasicModal
v-bind="$attrs" v-bind="$attrs"
@register="register" @register="register"
:title="t('component.cropper.modalTitle')" :title="t('component.cropper.modalTitle')"
width="800px" width="800px"
:canFullscreen="false" :canFullscreen="false"
@ok="handleOk" @ok="handleOk"
:okText="t('component.cropper.okText')" :okText="t('component.cropper.okText')"
> >
<div :class="prefixCls"> <div :class="prefixCls">
<div :class="`${prefixCls}-left`"> <div :class="`${prefixCls}-left`">
<div :class="`${prefixCls}-cropper`"> <div :class="`${prefixCls}-cropper`">
<CropperImage <CropperImage
v-if="src" v-if="src"
:src="src" :src="src"
height="300px" height="300px"
:circled="circled" :circled="circled"
@cropend="handleCropend" @cropend="handleCropend"
@ready="handleReady" @ready="handleReady"
/> />
</div> </div>
<div :class="`${prefixCls}-toolbar`"> <div :class="`${prefixCls}-toolbar`">
<Upload :fileList="[]" accept="image/*" :beforeUpload="handleBeforeUpload"> <Upload :fileList="[]" accept="image/*" :beforeUpload="handleBeforeUpload">
<Tooltip :title="t('component.cropper.selectImage')" placement="bottom"> <Tooltip :title="t('component.cropper.selectImage')" placement="bottom">
<a-button size="small" preIcon="ant-design:upload-outlined" type="primary" /> <a-button size="small" preIcon="ant-design:upload-outlined" type="primary" />
</Tooltip> </Tooltip>
</Upload> </Upload>
<Space> <Space>
<Tooltip :title="t('component.cropper.btn_reset')" placement="bottom"> <Tooltip :title="t('component.cropper.btn_reset')" placement="bottom">
<a-button <a-button
type="primary" type="primary"
preIcon="ant-design:reload-outlined" preIcon="ant-design:reload-outlined"
size="small" size="small"
:disabled="!src" :disabled="!src"
@click="handlerToolbar('reset')" @click="handlerToolbar('reset')"
/> />
</Tooltip> </Tooltip>
<Tooltip :title="t('component.cropper.btn_rotate_left')" placement="bottom"> <Tooltip :title="t('component.cropper.btn_rotate_left')" placement="bottom">
<a-button <a-button
type="primary" type="primary"
preIcon="ant-design:rotate-left-outlined" preIcon="ant-design:rotate-left-outlined"
size="small" size="small"
:disabled="!src" :disabled="!src"
@click="handlerToolbar('rotate', -45)" @click="handlerToolbar('rotate', -45)"
/> />
</Tooltip> </Tooltip>
<Tooltip :title="t('component.cropper.btn_rotate_right')" placement="bottom"> <Tooltip :title="t('component.cropper.btn_rotate_right')" placement="bottom">
<a-button <a-button
type="primary" type="primary"
preIcon="ant-design:rotate-right-outlined" preIcon="ant-design:rotate-right-outlined"
size="small" size="small"
:disabled="!src" :disabled="!src"
@click="handlerToolbar('rotate', 45)" @click="handlerToolbar('rotate', 45)"
/> />
</Tooltip> </Tooltip>
<Tooltip :title="t('component.cropper.btn_scale_x')" placement="bottom"> <Tooltip :title="t('component.cropper.btn_scale_x')" placement="bottom">
<a-button <a-button
type="primary" type="primary"
preIcon="vaadin:arrows-long-h" preIcon="vaadin:arrows-long-h"
size="small" size="small"
:disabled="!src" :disabled="!src"
@click="handlerToolbar('scaleX')" @click="handlerToolbar('scaleX')"
/> />
</Tooltip> </Tooltip>
<Tooltip :title="t('component.cropper.btn_scale_y')" placement="bottom"> <Tooltip :title="t('component.cropper.btn_scale_y')" placement="bottom">
<a-button <a-button
type="primary" type="primary"
preIcon="vaadin:arrows-long-v" preIcon="vaadin:arrows-long-v"
size="small" size="small"
:disabled="!src" :disabled="!src"
@click="handlerToolbar('scaleY')" @click="handlerToolbar('scaleY')"
/> />
</Tooltip> </Tooltip>
<Tooltip :title="t('component.cropper.btn_zoom_in')" placement="bottom"> <Tooltip :title="t('component.cropper.btn_zoom_in')" placement="bottom">
<a-button <a-button
type="primary" type="primary"
preIcon="ant-design:zoom-in-outlined" preIcon="ant-design:zoom-in-outlined"
size="small" size="small"
:disabled="!src" :disabled="!src"
@click="handlerToolbar('zoom', 0.1)" @click="handlerToolbar('zoom', 0.1)"
/> />
</Tooltip> </Tooltip>
<Tooltip :title="t('component.cropper.btn_zoom_out')" placement="bottom"> <Tooltip :title="t('component.cropper.btn_zoom_out')" placement="bottom">
<a-button <a-button
type="primary" type="primary"
preIcon="ant-design:zoom-out-outlined" preIcon="ant-design:zoom-out-outlined"
size="small" size="small"
:disabled="!src" :disabled="!src"
@click="handlerToolbar('zoom', -0.1)" @click="handlerToolbar('zoom', -0.1)"
/> />
</Tooltip> </Tooltip>
</Space> </Space>
</div> </div>
</div> </div>
<div :class="`${prefixCls}-right`"> <div :class="`${prefixCls}-right`">
<div :class="`${prefixCls}-preview`"> <div :class="`${prefixCls}-preview`">
<img :src="previewSource" v-if="previewSource" :alt="t('component.cropper.preview')" /> <img :src="previewSource" v-if="previewSource" :alt="t('component.cropper.preview')" />
</div> </div>
<template v-if="previewSource"> <template v-if="previewSource">
<div :class="`${prefixCls}-group`"> <div :class="`${prefixCls}-group`">
<Avatar :src="previewSource" size="large" /> <Avatar :src="previewSource" size="large" />
<Avatar :src="previewSource" :size="48" /> <Avatar :src="previewSource" :size="48" />
<Avatar :src="previewSource" :size="64" /> <Avatar :src="previewSource" :size="64" />
<Avatar :src="previewSource" :size="80" /> <Avatar :src="previewSource" :size="80" />
</div> </div>
</template> </template>
</div> </div>
</div> </div>
</BasicModal> </BasicModal>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import type { CropendResult, Cropper } from './typing'; import type { CropendResult, Cropper } from './typing';
import { defineComponent, ref, PropType } from 'vue'; import { ref, PropType } from 'vue';
import CropperImage from './Cropper.vue'; import CropperImage from './Cropper.vue';
import { Space, Upload, Avatar, Tooltip } from 'ant-design-vue'; import { Space, Upload, Avatar, Tooltip } from 'ant-design-vue';
import { useDesign } from '/@/hooks/web/useDesign'; import { useDesign } from '@/hooks/web/useDesign';
import { BasicModal, useModalInner } from '/@/components/Modal'; import { BasicModal, useModalInner } from '@/components/Modal';
import { dataURLtoBlob } from '/@/utils/file/base64Conver'; import { dataURLtoBlob } from '@/utils/file/base64Conver';
import { isFunction } from '/@/utils/is'; import { isFunction } from '@/utils/is';
import { useI18n } from '/@/hooks/web/useI18n'; import { useI18n } from '@/hooks/web/useI18n';
type apiFunParams = { file: Blob; name: string; filename: string }; type apiFunParams = { file: Blob; name: string; filename: string };
const props = { defineOptions({ name: 'CropperModal' });
circled: { type: Boolean, default: true },
uploadApi: { const props = defineProps({
type: Function as PropType<(params: apiFunParams) => Promise<any>>, circled: { type: Boolean, default: true },
}, uploadApi: {
src: { type: String }, type: Function as PropType<(params: apiFunParams) => Promise<any>>,
size: { type: Number }, },
}; src: { type: String },
size: { type: Number },
export default defineComponent({ });
name: 'CropperModal',
components: { BasicModal, Space, CropperImage, Upload, Avatar, Tooltip }, const emit = defineEmits(['uploadSuccess', 'uploadError', 'register']);
props,
emits: ['uploadSuccess', 'uploadError', 'register'], let filename = '';
setup(props, { emit }) { const src = ref(props.src || '');
let filename = ''; const previewSource = ref('');
const src = ref(props.src || ''); const cropper = ref<Cropper>();
const previewSource = ref(''); let scaleX = 1;
const cropper = ref<Cropper>(); let scaleY = 1;
let scaleX = 1;
let scaleY = 1; const { prefixCls } = useDesign('cropper-am');
const [register, { closeModal, setModalProps }] = useModalInner();
const { prefixCls } = useDesign('cropper-am'); const { t } = useI18n();
const [register, { closeModal, setModalProps }] = useModalInner();
const { t } = useI18n(); // Block upload
function handleBeforeUpload(file: File) {
// Block upload if (props.size && file.size > 1024 * 1024 * props.size) {
function handleBeforeUpload(file: File) { emit('uploadError', { msg: t('component.cropper.imageTooBig') });
if (props.size && file.size > 1024 * 1024 * props.size) { return;
emit('uploadError', { msg: t('component.cropper.imageTooBig') }); }
return; const reader = new FileReader();
} reader.readAsDataURL(file);
const reader = new FileReader(); src.value = '';
reader.readAsDataURL(file); previewSource.value = '';
src.value = ''; reader.onload = function (e) {
previewSource.value = ''; src.value = (e.target?.result as string) ?? '';
reader.onload = function (e) { filename = file.name;
src.value = (e.target?.result as string) ?? ''; };
filename = file.name; return false;
}; }
return false;
} function handleCropend({ imgBase64 }: CropendResult) {
previewSource.value = imgBase64;
function handleCropend({ imgBase64 }: CropendResult) { }
previewSource.value = imgBase64;
} function handleReady(cropperInstance: Cropper) {
cropper.value = cropperInstance;
function handleReady(cropperInstance: Cropper) { }
cropper.value = cropperInstance;
} function handlerToolbar(event: string, arg?: number) {
if (event === 'scaleX') {
function handlerToolbar(event: string, arg?: number) { scaleX = arg = scaleX === -1 ? 1 : -1;
if (event === 'scaleX') { }
scaleX = arg = scaleX === -1 ? 1 : -1; if (event === 'scaleY') {
} scaleY = arg = scaleY === -1 ? 1 : -1;
if (event === 'scaleY') { }
scaleY = arg = scaleY === -1 ? 1 : -1; cropper?.value?.[event]?.(arg);
} }
cropper?.value?.[event]?.(arg);
} async function handleOk() {
const uploadApi = props.uploadApi;
async function handleOk() { if (uploadApi && isFunction(uploadApi)) {
const uploadApi = props.uploadApi; const blob = dataURLtoBlob(previewSource.value);
if (uploadApi && isFunction(uploadApi)) { try {
const blob = dataURLtoBlob(previewSource.value); setModalProps({ confirmLoading: true });
try { const result = await uploadApi({ name: 'file', file: blob, filename });
setModalProps({ confirmLoading: true }); emit('uploadSuccess', { source: previewSource.value, data: result.url });
const result = await uploadApi({ name: 'file', file: blob, filename }); closeModal();
emit('uploadSuccess', { source: previewSource.value, data: result.url }); } finally {
closeModal(); setModalProps({ confirmLoading: false });
} finally { }
setModalProps({ confirmLoading: false }); }
} }
} </script>
}
<style lang="less">
return { @prefix-cls: ~'@{namespace}-cropper-am';
t,
prefixCls, .@{prefix-cls} {
src, display: flex;
register,
previewSource, &-left,
handleBeforeUpload, &-right {
handleCropend, height: 340px;
handleReady, }
handlerToolbar,
handleOk, &-left {
}; width: 55%;
}, }
});
</script> &-right {
width: 45%;
<style lang="less"> }
@prefix-cls: ~'@{namespace}-cropper-am';
&-cropper {
.@{prefix-cls} { height: 300px;
display: flex; background: #eee;
background-image: linear-gradient(
&-left, 45deg,
&-right { rgb(0 0 0 / 25%) 25%,
height: 340px; transparent 0,
} transparent 75%,
rgb(0 0 0 / 25%) 0
&-left { ),
width: 55%; linear-gradient(
} 45deg,
rgb(0 0 0 / 25%) 25%,
&-right { transparent 0,
width: 45%; transparent 75%,
} rgb(0 0 0 / 25%) 0
);
&-cropper { background-position:
height: 300px; 0 0,
background: #eee; 12px 12px;
background-image: linear-gradient( background-size: 24px 24px;
45deg, }
rgb(0 0 0 / 25%) 25%,
transparent 0, &-toolbar {
transparent 75%, display: flex;
rgb(0 0 0 / 25%) 0 align-items: center;
), justify-content: space-between;
linear-gradient( margin-top: 10px;
45deg, }
rgb(0 0 0 / 25%) 25%,
transparent 0, &-preview {
transparent 75%, width: 220px;
rgb(0 0 0 / 25%) 0 height: 220px;
); margin: 0 auto;
background-position: 0 0, 12px 12px; overflow: hidden;
background-size: 24px 24px; border: 1px solid @border-color-base;
} border-radius: 50%;
&-toolbar { img {
display: flex; width: 100%;
align-items: center; height: 100%;
justify-content: space-between; }
margin-top: 10px; }
}
&-group {
&-preview { display: flex;
width: 220px; align-items: center;
height: 220px; justify-content: space-around;
margin: 0 auto; margin-top: 8px;
overflow: hidden; padding-top: 8px;
border: 1px solid @border-color-base; border-top: 1px solid @border-color-base;
border-radius: 50%; }
}
img { </style>
width: 100%;
height: 100%;
}
}
&-group {
display: flex;
align-items: center;
justify-content: space-around;
margin-top: 8px;
padding-top: 8px;
border-top: 1px solid @border-color-base;
}
}
</style>

View File

@@ -1,4 +1,4 @@
import { withInstall } from '/@/utils'; import { withInstall } from '@/utils';
import description from './src/Description.vue'; import description from './src/Description.vue';
export * from './src/typing'; export * from './src/typing';

View File

@@ -1,7 +1,7 @@
<script lang="tsx"> <script lang="tsx">
import type { DescriptionProps, DescInstance, DescItem } from './typing'; import type { DescriptionProps, DescInstance, DescItem } from './typing';
import type { DescriptionsProps } from 'ant-design-vue/es/descriptions/index'; import type { DescriptionsProps } from 'ant-design-vue/es/descriptions';
import type { CollapseContainerOptions } from '/@/components/Container/index'; import type { CollapseContainerOptions } from '@/components/Container';
import { import {
type CSSProperties, type CSSProperties,
type PropType, type PropType,
@@ -13,10 +13,10 @@
} from 'vue'; } from 'vue';
import { get } from 'lodash-es'; import { get } from 'lodash-es';
import { Descriptions } from 'ant-design-vue'; import { Descriptions } from 'ant-design-vue';
import { CollapseContainer } from '/@/components/Container/index'; import { CollapseContainer } from '@/components/Container';
import { useDesign } from '/@/hooks/web/useDesign'; import { useDesign } from '@/hooks/web/useDesign';
import { isFunction } from '/@/utils/is'; import { isFunction } from '@/utils/is';
import { getSlot } from '/@/utils/helper/tsxHelper'; import { getSlot } from '@/utils/helper/tsxHelper';
import { useAttrs } from '@vben/hooks'; import { useAttrs } from '@vben/hooks';
const props = { const props = {
@@ -24,7 +24,7 @@
title: { type: String, default: '' }, title: { type: String, default: '' },
size: { size: {
type: String, type: String,
validator: (v) => ['small', 'default', 'middle', undefined].includes(v), validator: (v: string) => ['small', 'default', 'middle', undefined].includes(v),
default: 'small', default: 'small',
}, },
bordered: { type: Boolean, default: true }, bordered: { type: Boolean, default: true },

View File

@@ -1,6 +1,6 @@
import type { VNode, CSSProperties } from 'vue'; import type { VNode, CSSProperties } from 'vue';
import type { CollapseContainerOptions } from '/@/components/Container/index'; import type { CollapseContainerOptions } from '@/components/Container';
import type { DescriptionsProps } from 'ant-design-vue/es/descriptions/index'; import type { DescriptionsProps } from 'ant-design-vue/es/descriptions';
export interface DescItem { export interface DescItem {
labelMinWidth?: number; labelMinWidth?: number;

View File

@@ -1,6 +1,6 @@
import type { DescriptionProps, DescInstance, UseDescReturnType } from './typing'; import type { DescriptionProps, DescInstance, UseDescReturnType } from './typing';
import { ref, getCurrentInstance, unref } from 'vue'; import { ref, getCurrentInstance, unref } from 'vue';
import { isProdMode } from '/@/utils/env'; import { isProdMode } from '@/utils/env';
export function useDescription(props?: Partial<DescriptionProps>): UseDescReturnType { export function useDescription(props?: Partial<DescriptionProps>): UseDescReturnType {
if (!getCurrentInstance()) { if (!getCurrentInstance()) {

View File

@@ -1,4 +1,4 @@
import { withInstall } from '/@/utils'; import { withInstall } from '@/utils';
import basicDrawer from './src/BasicDrawer.vue'; import basicDrawer from './src/BasicDrawer.vue';
export const BasicDrawer = withInstall(basicDrawer); export const BasicDrawer = withInstall(basicDrawer);

View File

@@ -2,7 +2,7 @@
<Drawer v-bind="getBindValues" :class="prefixCls" @close="onClose"> <Drawer v-bind="getBindValues" :class="prefixCls" @close="onClose">
<template #title v-if="!$slots.title"> <template #title v-if="!$slots.title">
<DrawerHeader <DrawerHeader
:title="getMergeProps.title" :title="mergeProps.title"
:isDetail="isDetail" :isDetail="isDetail"
:showDetailBack="showDetailBack" :showDetailBack="showDetailBack"
@close="onClose" @close="onClose"
@@ -30,166 +30,142 @@
</DrawerFooter> </DrawerFooter>
</Drawer> </Drawer>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import type { DrawerInstance, DrawerProps } from './typing'; import type { DrawerInstance, DrawerProps } from './typing';
import type { CSSProperties } from 'vue'; import type { CSSProperties } from 'vue';
import { import { ref, computed, watch, unref, nextTick, toRaw, getCurrentInstance } from 'vue';
defineComponent,
ref,
computed,
watch,
unref,
nextTick,
toRaw,
getCurrentInstance,
} from 'vue';
import { Drawer } from 'ant-design-vue'; import { Drawer } from 'ant-design-vue';
import { useI18n } from '/@/hooks/web/useI18n'; import { useI18n } from '@/hooks/web/useI18n';
import { isFunction, isNumber } from '/@/utils/is'; import { isFunction, isNumber } from '@/utils/is';
import { deepMerge } from '/@/utils'; import { deepMerge } from '@/utils';
import DrawerFooter from './components/DrawerFooter.vue'; import DrawerFooter from './components/DrawerFooter.vue';
import DrawerHeader from './components/DrawerHeader.vue'; import DrawerHeader from './components/DrawerHeader.vue';
import { ScrollContainer } from '/@/components/Container'; import { ScrollContainer } from '@/components/Container';
import { basicProps } from './props'; import { basicProps } from './props';
import { useDesign } from '/@/hooks/web/useDesign'; import { useDesign } from '@/hooks/web/useDesign';
import { useAttrs } from '@vben/hooks'; import { useAttrs } from '@vben/hooks';
export default defineComponent({ defineOptions({ inheritAttrs: false });
components: { Drawer, ScrollContainer, DrawerFooter, DrawerHeader },
inheritAttrs: false,
props: basicProps,
emits: ['open-change', 'ok', 'close', 'register'],
setup(props, { emit }) {
const openRef = ref(false);
const attrs = useAttrs();
const propsRef = ref<Partial<DrawerProps | null>>(null);
const { t } = useI18n(); const props = defineProps(basicProps);
const { prefixVar, prefixCls } = useDesign('basic-drawer');
const drawerInstance: DrawerInstance = { const emit = defineEmits(['open-change', 'ok', 'close', 'register']);
setDrawerProps: setDrawerProps as any,
emitOpen: undefined,
};
const instance = getCurrentInstance(); const openRef = ref(false);
const attrs = useAttrs();
const propsRef = ref<Partial<DrawerProps | null>>(null);
instance && emit('register', drawerInstance, instance.uid); const { t } = useI18n();
const { prefixVar, prefixCls } = useDesign('basic-drawer');
const getMergeProps = computed((): DrawerProps => { const drawerInstance: DrawerInstance = {
return deepMerge(toRaw(props), unref(propsRef)) as any; setDrawerProps: setDrawerProps as any,
}); emitOpen: undefined,
};
const getProps = computed((): DrawerProps => { const instance = getCurrentInstance();
const opt = {
placement: 'right',
...unref(attrs),
...unref(getMergeProps),
open: unref(openRef),
};
opt.title = undefined;
const { isDetail, width, wrapClassName, getContainer } = opt;
if (isDetail) {
if (!width) {
opt.width = '100%';
}
const detailCls = `${prefixCls}__detail`;
opt.rootClassName = wrapClassName ? `${wrapClassName} ${detailCls}` : detailCls;
if (!getContainer) { instance && emit('register', drawerInstance, instance.uid);
opt.getContainer = `.${prefixVar}-layout-content`;
}
}
return opt as DrawerProps;
});
const getBindValues = computed((): DrawerProps => { const getMergeProps = computed((): DrawerProps => {
return { return deepMerge(toRaw(props), unref(propsRef)) as any;
...attrs,
...unref(getProps),
};
});
// Custom implementation of the bottom button,
const getFooterHeight = computed(() => {
const { footerHeight, showFooter } = unref(getProps);
if (showFooter && footerHeight) {
return isNumber(footerHeight)
? `${footerHeight}px`
: `${footerHeight.replace('px', '')}px`;
}
return `0px`;
});
const getScrollContentStyle = computed((): CSSProperties => {
const footerHeight = unref(getFooterHeight);
return {
position: 'relative',
height: `calc(100% - ${footerHeight})`,
};
});
const getLoading = computed(() => {
return !!unref(getProps)?.loading;
});
watch(
() => props.open,
(newVal, oldVal) => {
if (newVal !== oldVal) openRef.value = newVal;
},
{ deep: true },
);
watch(
() => openRef.value,
(open) => {
nextTick(() => {
emit('open-change', open);
instance && drawerInstance.emitOpen?.(open, instance.uid);
});
},
);
// Cancel event
async function onClose(e) {
const { closeFunc } = unref(getProps);
emit('close', e);
if (closeFunc && isFunction(closeFunc)) {
const res = await closeFunc();
openRef.value = !res;
return;
}
openRef.value = false;
}
function setDrawerProps(props: Partial<DrawerProps>): void {
// Keep the last setDrawerProps
propsRef.value = deepMerge(unref(propsRef) || ({} as any), props);
if (Reflect.has(props, 'open')) {
openRef.value = !!props.open;
}
}
function handleOk() {
emit('ok');
}
return {
onClose,
t,
prefixCls,
getMergeProps: getMergeProps as any,
getScrollContentStyle,
getProps: getProps as any,
getLoading,
getBindValues,
getFooterHeight,
handleOk,
};
},
}); });
const getProps = computed((): DrawerProps => {
const opt = {
placement: 'right',
...unref(attrs),
...unref(getMergeProps),
open: unref(openRef),
};
opt.title = undefined;
const { isDetail, width, wrapClassName, getContainer } = opt;
if (isDetail) {
if (!width) {
opt.width = '100%';
}
const detailCls = `${prefixCls}__detail`;
opt.rootClassName = wrapClassName ? `${wrapClassName} ${detailCls}` : detailCls;
if (!getContainer) {
opt.getContainer = `.${prefixVar}-layout-content`;
}
}
return opt as DrawerProps;
});
const getBindValues = computed((): DrawerProps => {
return {
...attrs,
...unref(getProps),
};
});
// Custom implementation of the bottom button,
const getFooterHeight = computed(() => {
const { footerHeight, showFooter } = unref(getProps);
if (showFooter && footerHeight) {
return isNumber(footerHeight) ? `${footerHeight}px` : `${footerHeight.replace('px', '')}px`;
}
return `0px`;
});
const getScrollContentStyle = computed((): CSSProperties => {
const footerHeight = unref(getFooterHeight);
return {
position: 'relative',
height: `calc(100% - ${footerHeight})`,
};
});
const getLoading = computed(() => {
return !!unref(getProps)?.loading;
});
watch(
() => props.open,
(newVal, oldVal) => {
if (newVal !== oldVal) openRef.value = newVal;
},
{ deep: true },
);
watch(
() => openRef.value,
(open) => {
nextTick(() => {
emit('open-change', open);
instance && drawerInstance.emitOpen?.(open, instance.uid);
});
},
);
// Cancel event
async function onClose(e) {
const { closeFunc } = unref(getProps);
emit('close', e);
if (closeFunc && isFunction(closeFunc)) {
const res = await closeFunc();
openRef.value = !res;
return;
}
openRef.value = false;
}
function setDrawerProps(props: Partial<DrawerProps>): void {
// Keep the last setDrawerProps
propsRef.value = deepMerge(unref(propsRef) || ({} as any), props);
if (Reflect.has(props, 'open')) {
openRef.value = !!props.open;
}
}
function handleOk() {
emit('ok');
}
const mergeProps = getMergeProps as any;
</script> </script>
<style lang="less"> <style lang="less">
@header-height: 60px; @header-height: 60px;

View File

@@ -24,44 +24,41 @@
</template> </template>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import type { CSSProperties } from 'vue'; import type { CSSProperties } from 'vue';
import { defineComponent, computed } from 'vue'; import { computed } from 'vue';
import { useDesign } from '/@/hooks/web/useDesign'; import { useDesign } from '@/hooks/web/useDesign';
import { footerProps } from '../props'; import { footerProps } from '../props';
export default defineComponent({ defineOptions({ name: 'BasicDrawerFooter' });
name: 'BasicDrawerFooter',
props: {
...footerProps,
height: {
type: String,
default: '60px',
},
},
emits: ['ok', 'close'],
setup(props, { emit }) {
const { prefixCls } = useDesign('basic-drawer-footer');
const getStyle = computed((): CSSProperties => { const props = defineProps({
const heightStr = `${props.height}`; ...footerProps,
return { height: {
height: heightStr, type: String,
lineHeight: `calc(${heightStr} - 1px)`, default: '60px',
};
});
function handleOk() {
emit('ok');
}
function handleClose() {
emit('close');
}
return { handleOk, prefixCls, handleClose, getStyle };
}, },
}); });
const emit = defineEmits(['ok', 'close']);
const { prefixCls } = useDesign('basic-drawer-footer');
const getStyle = computed((): CSSProperties => {
const heightStr = `${props.height}`;
return {
height: heightStr,
lineHeight: `calc(${heightStr} - 1px)`,
};
});
function handleOk() {
emit('ok');
}
function handleClose() {
emit('close');
}
</script> </script>
<style lang="less"> <style lang="less">

View File

@@ -17,34 +17,27 @@
</span> </span>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { defineComponent } from 'vue'; import { BasicTitle } from '@/components/Basic';
import { BasicTitle } from '/@/components/Basic';
import { ArrowLeftOutlined } from '@ant-design/icons-vue'; import { ArrowLeftOutlined } from '@ant-design/icons-vue';
import { useDesign } from '@/hooks/web/useDesign';
import { propTypes } from '@/utils/propTypes';
import { useDesign } from '/@/hooks/web/useDesign'; defineOptions({ name: 'BasicDrawerHeader' });
import { propTypes } from '/@/utils/propTypes'; defineProps({
isDetail: propTypes.bool,
export default defineComponent({ showDetailBack: propTypes.bool,
name: 'BasicDrawerHeader', title: propTypes.string,
components: { BasicTitle, ArrowLeftOutlined },
props: {
isDetail: propTypes.bool,
showDetailBack: propTypes.bool,
title: propTypes.string,
},
emits: ['close'],
setup(_, { emit }) {
const { prefixCls } = useDesign('basic-drawer-header');
function handleClose() {
emit('close');
}
return { prefixCls, handleClose };
},
}); });
const emit = defineEmits(['close']);
const { prefixCls } = useDesign('basic-drawer-header');
function handleClose() {
emit('close');
}
</script> </script>
<style lang="less"> <style lang="less">

View File

@@ -1,6 +1,6 @@
import type { PropType } from 'vue'; import type { PropType } from 'vue';
import { useI18n } from '/@/hooks/web/useI18n'; import { useI18n } from '@/hooks/web/useI18n';
const { t } = useI18n(); const { t } = useI18n();

View File

@@ -1,6 +1,6 @@
import type { ButtonProps } from 'ant-design-vue/lib/button/buttonTypes'; import type { ButtonProps } from 'ant-design-vue/lib/button/buttonTypes';
import type { CSSProperties, VNodeChild, ComputedRef } from 'vue'; import type { CSSProperties, VNodeChild, ComputedRef } from 'vue';
import type { ScrollContainerOptions } from '/@/components/Container/index'; import type { ScrollContainerOptions } from '@/components/Container';
export interface DrawerInstance { export interface DrawerInstance {
setDrawerProps: (props: Partial<DrawerProps>) => void; setDrawerProps: (props: Partial<DrawerProps>) => void;

View File

@@ -15,11 +15,11 @@ import {
toRaw, toRaw,
computed, computed,
} from 'vue'; } from 'vue';
import { isProdMode } from '/@/utils/env'; import { isProdMode } from '@/utils/env';
import { isFunction } from '/@/utils/is'; import { isFunction } from '@/utils/is';
import { tryOnUnmounted } from '@vueuse/core'; import { tryOnUnmounted } from '@vueuse/core';
import { isEqual } from 'lodash-es'; import { isEqual } from 'lodash-es';
import { error } from '/@/utils/log'; import { error } from '@/utils/log';
const dataTransferRef = reactive<any>({}); const dataTransferRef = reactive<any>({});

View File

@@ -1,4 +1,4 @@
import { withInstall } from '/@/utils'; import { withInstall } from '@/utils';
import dropdown from './src/Dropdown.vue'; import dropdown from './src/Dropdown.vue';
export * from './src/typing'; export * from './src/typing';

View File

@@ -1,4 +1,4 @@
import { withInstall } from '/@/utils'; import { withInstall } from '@/utils';
import impExcel from './src/ImportExcel.vue'; import impExcel from './src/ImportExcel.vue';
import expExcelModal from './src/ExportExcelModal.vue'; import expExcelModal from './src/ExportExcelModal.vue';

View File

@@ -13,13 +13,12 @@
/> />
</BasicModal> </BasicModal>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import type { ExportModalResult } from './typing'; import type { ExportModalResult } from './typing';
import { defineComponent } from 'vue'; import { BasicModal, useModalInner } from '@/components/Modal';
import { BasicModal, useModalInner } from '/@/components/Modal'; import { BasicForm, FormSchema, useForm } from '@/components/Form';
import { BasicForm, FormSchema, useForm } from '/@/components/Form/index';
import { useI18n } from '/@/hooks/web/useI18n'; import { useI18n } from '@/hooks/web/useI18n';
const { t } = useI18n(); const { t } = useI18n();
@@ -62,30 +61,19 @@
}, },
}, },
]; ];
export default defineComponent({
components: { BasicModal, BasicForm },
emits: ['success', 'register'],
setup(_, { emit }) {
const [registerForm, { validate }] = useForm();
const [registerModal, { closeModal }] = useModalInner();
const handleOk = async () => { const emit = defineEmits(['success', 'register']);
const res = await validate<ExportModalResult>();
const { filename, bookType } = res;
emit('success', {
filename: `${filename.split('.').shift()}.${bookType}`,
bookType,
});
closeModal();
};
return { const [registerForm, { validate }] = useForm();
schemas, const [registerModal, { closeModal }] = useModalInner();
handleOk,
registerForm, const handleOk = async () => {
registerModal, const res = await validate<ExportModalResult>();
t, const { filename, bookType } = res;
}; emit('success', {
}, filename: `${filename.split('.').shift()}.${bookType}`,
}); bookType,
});
closeModal();
};
</script> </script>

View File

@@ -12,214 +12,210 @@
</div> </div>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { defineComponent, ref, unref } from 'vue'; import { ref, unref } from 'vue';
import * as XLSX from 'xlsx'; import * as XLSX from 'xlsx';
import { dateUtil } from '/@/utils/dateUtil'; import { dateUtil } from '@/utils/dateUtil';
import type { ExcelData } from './typing'; import type { ExcelData } from './typing';
export default defineComponent({ defineOptions({ name: 'ImportExcel' });
name: 'ImportExcel',
props: { const props = defineProps({
// 日期时间格式。如果不提供或者提供空值将返回原始Date对象 // 日期时间格式。如果不提供或者提供空值将返回原始Date对象
dateFormat: { dateFormat: {
type: String, type: String,
},
// 时区调整。实验性功能,仅为了解决读取日期时间值有偏差的问题。目前仅提供了+08:00时区的偏差修正值
// https://github.com/SheetJS/sheetjs/issues/1470#issuecomment-501108554
timeZone: {
type: Number,
default: 8,
},
// 是否直接返回选中文件
isReturnFile: {
type: Boolean,
default: false,
},
}, },
emits: ['success', 'error', 'cancel'], // 时区调整。实验性功能,仅为了解决读取日期时间值有偏差的问题。目前仅提供了+08:00时区的偏差修正值
setup(props, { emit }) { // https://github.com/SheetJS/sheetjs/issues/1470#issuecomment-501108554
const inputRef = ref<HTMLInputElement | null>(null); timeZone: {
const loadingRef = ref<Boolean>(false); type: Number,
const cancelRef = ref<Boolean>(true); default: 8,
},
function shapeWorkSheel(sheet: XLSX.WorkSheet, range: XLSX.Range) { // 是否直接返回选中文件
let str = ' ', isReturnFile: {
char = 65, type: Boolean,
customWorkSheet = { default: false,
t: 's',
v: str,
r: '<t> </t><phoneticPr fontId="1" type="noConversion"/>',
h: str,
w: str,
};
if (!sheet || !sheet['!ref']) return [];
let c = 0,
r = 1;
while (c < range.e.c + 1) {
while (r < range.e.r + 1) {
if (!sheet[String.fromCharCode(char) + r]) {
sheet[String.fromCharCode(char) + r] = customWorkSheet;
}
r++;
}
r = 1;
str += ' ';
customWorkSheet = {
t: 's',
v: str,
r: '<t> </t><phoneticPr fontId="1" type="noConversion"/>',
h: str,
w: str,
};
c++;
char++;
}
}
/**
* @description: 第一行作为头部
*/
function getHeaderRow(sheet: XLSX.WorkSheet) {
if (!sheet || !sheet['!ref']) return [];
const headers: string[] = [];
// A3:B7=>{s:{c:0, r:2}, e:{c:1, r:6}}
const range: XLSX.Range = XLSX.utils.decode_range(sheet['!ref']);
shapeWorkSheel(sheet, range);
const R = range.s.r;
/* start in the first row */
for (let C = range.s.c; C <= range.e.c; ++C) {
/* walk every column in the range */
const cell = sheet[XLSX.utils.encode_cell({ c: C, r: R })];
/* find the cell in the first row */
let hdr = 'UNKNOWN ' + C; // <-- replace with your desired default
if (cell && cell.t) hdr = XLSX.utils.format_cell(cell);
headers.push(hdr);
}
return headers;
}
/**
* @description: 获得excel数据
*/
function getExcelData(workbook: XLSX.WorkBook) {
const excelData: ExcelData[] = [];
const { dateFormat, timeZone } = props;
for (const sheetName of workbook.SheetNames) {
const worksheet = workbook.Sheets[sheetName];
const header: string[] = getHeaderRow(worksheet);
let results = XLSX.utils.sheet_to_json(worksheet, {
raw: true,
dateNF: dateFormat, //Not worked
}) as object[];
results = results.map((row: object) => {
for (let field in row) {
if (row[field] instanceof Date) {
if (timeZone === 8) {
row[field].setSeconds(row[field].getSeconds() + 43);
}
if (dateFormat) {
row[field] = dateUtil(row[field]).format(dateFormat);
}
}
}
return row;
});
excelData.push({
header,
results,
meta: {
sheetName,
},
});
}
return excelData;
}
/**
* @description: 读取excel数据
*/
function readerData(rawFile: File) {
loadingRef.value = true;
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = async (e) => {
try {
const data = e.target && e.target.result;
const workbook = XLSX.read(data, { type: 'array', cellDates: true });
// console.log(workbook);
/* DO SOMETHING WITH workbook HERE */
const excelData = getExcelData(workbook);
emit('success', excelData);
resolve('');
} catch (error) {
reject(error);
emit('error');
} finally {
loadingRef.value = false;
}
};
reader.readAsArrayBuffer(rawFile);
});
}
async function upload(rawFile: File) {
const inputRefDom = unref(inputRef);
if (inputRefDom) {
// fix can't select the same excel
inputRefDom.value = '';
}
await readerData(rawFile);
}
/**
* @description: 触发选择文件管理器
*/
function handleInputClick(e: Event) {
const target = e && (e.target as HTMLInputElement);
const files = target?.files;
const rawFile = files && files[0]; // only setting files[0]
target.value = '';
if (!rawFile) return;
cancelRef.value = false;
if (props.isReturnFile) {
emit('success', rawFile);
return;
}
upload(rawFile);
}
/**
* @description 文件选择器关闭后,判断取消状态
*/
function handleFocusChange() {
const timeId = setInterval(() => {
if (cancelRef.value === true) {
emit('cancel');
}
clearInterval(timeId);
window.removeEventListener('focus', handleFocusChange);
}, 1000);
}
/**
* @description: 点击上传按钮
*/
function handleUpload() {
const inputRefDom = unref(inputRef);
if (inputRefDom) {
cancelRef.value = true;
inputRefDom.click();
window.addEventListener('focus', handleFocusChange);
}
}
return { handleUpload, handleInputClick, inputRef };
}, },
}); });
const emit = defineEmits(['success', 'error', 'cancel']);
const inputRef = ref<HTMLInputElement | null>(null);
const loadingRef = ref<Boolean>(false);
const cancelRef = ref<Boolean>(true);
function shapeWorkSheel(sheet: XLSX.WorkSheet, range: XLSX.Range) {
let str = ' ',
char = 65,
customWorkSheet = {
t: 's',
v: str,
r: '<t> </t><phoneticPr fontId="1" type="noConversion"/>',
h: str,
w: str,
};
if (!sheet || !sheet['!ref']) return [];
let c = 0,
r = 1;
while (c < range.e.c + 1) {
while (r < range.e.r + 1) {
if (!sheet[String.fromCharCode(char) + r]) {
sheet[String.fromCharCode(char) + r] = customWorkSheet;
}
r++;
}
r = 1;
str += ' ';
customWorkSheet = {
t: 's',
v: str,
r: '<t> </t><phoneticPr fontId="1" type="noConversion"/>',
h: str,
w: str,
};
c++;
char++;
}
}
/**
* @description: 第一行作为头部
*/
function getHeaderRow(sheet: XLSX.WorkSheet) {
if (!sheet || !sheet['!ref']) return [];
const headers: string[] = [];
// A3:B7=>{s:{c:0, r:2}, e:{c:1, r:6}}
const range: XLSX.Range = XLSX.utils.decode_range(sheet['!ref']);
shapeWorkSheel(sheet, range);
const R = range.s.r;
/* start in the first row */
for (let C = range.s.c; C <= range.e.c; ++C) {
/* walk every column in the range */
const cell = sheet[XLSX.utils.encode_cell({ c: C, r: R })];
/* find the cell in the first row */
let hdr = 'UNKNOWN ' + C; // <-- replace with your desired default
if (cell && cell.t) hdr = XLSX.utils.format_cell(cell);
headers.push(hdr);
}
return headers;
}
/**
* @description: 获得excel数据
*/
function getExcelData(workbook: XLSX.WorkBook) {
const excelData: ExcelData[] = [];
const { dateFormat, timeZone } = props;
for (const sheetName of workbook.SheetNames) {
const worksheet = workbook.Sheets[sheetName];
const header: string[] = getHeaderRow(worksheet);
let results = XLSX.utils.sheet_to_json(worksheet, {
raw: true,
dateNF: dateFormat, //Not worked
}) as object[];
results = results.map((row: object) => {
for (let field in row) {
if (row[field] instanceof Date) {
if (timeZone === 8) {
row[field].setSeconds(row[field].getSeconds() + 43);
}
if (dateFormat) {
row[field] = dateUtil(row[field]).format(dateFormat);
}
}
}
return row;
});
excelData.push({
header,
results,
meta: {
sheetName,
},
});
}
return excelData;
}
/**
* @description: 读取excel数据
*/
function readerData(rawFile: File) {
loadingRef.value = true;
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = async (e) => {
try {
const data = e.target && e.target.result;
const workbook = XLSX.read(data, { type: 'array', cellDates: true });
// console.log(workbook);
/* DO SOMETHING WITH workbook HERE */
const excelData = getExcelData(workbook);
emit('success', excelData);
resolve('');
} catch (error) {
reject(error);
emit('error');
} finally {
loadingRef.value = false;
}
};
reader.readAsArrayBuffer(rawFile);
});
}
async function upload(rawFile: File) {
const inputRefDom = unref(inputRef);
if (inputRefDom) {
// fix can't select the same excel
inputRefDom.value = '';
}
await readerData(rawFile);
}
/**
* @description: 触发选择文件管理器
*/
function handleInputClick(e: Event) {
const target = e && (e.target as HTMLInputElement);
const files = target?.files;
const rawFile = files && files[0]; // only setting files[0]
target.value = '';
if (!rawFile) return;
cancelRef.value = false;
if (props.isReturnFile) {
emit('success', rawFile);
return;
}
upload(rawFile);
}
/**
* @description 文件选择器关闭后,判断取消状态
*/
function handleFocusChange() {
const timeId = setInterval(() => {
if (cancelRef.value === true) {
emit('cancel');
}
clearInterval(timeId);
window.removeEventListener('focus', handleFocusChange);
}, 1000);
}
/**
* @description: 点击上传按钮
*/
function handleUpload() {
const inputRefDom = unref(inputRef);
if (inputRefDom) {
cancelRef.value = true;
inputRefDom.click();
window.addEventListener('focus', handleFocusChange);
}
}
</script> </script>

View File

@@ -1,4 +1,4 @@
import { withInstall } from '/@/utils'; import { withInstall } from '@/utils';
import flowChart from './src/FlowChart.vue'; import flowChart from './src/FlowChart.vue';
export const FlowChart = withInstall(flowChart); export const FlowChart = withInstall(flowChart);

View File

@@ -7,152 +7,141 @@
</BasicModal> </BasicModal>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import type { Ref } from 'vue'; import type { Ref } from 'vue';
import type { Definition } from '@logicflow/core'; import type { Definition } from '@logicflow/core';
import { defineComponent, ref, onMounted, unref, nextTick, computed, watch } from 'vue'; import { ref, onMounted, unref, nextTick, computed, watch } from 'vue';
import FlowChartToolbar from './FlowChartToolbar.vue'; import FlowChartToolbar from './FlowChartToolbar.vue';
import LogicFlow from '@logicflow/core'; import LogicFlow from '@logicflow/core';
import { Snapshot, BpmnElement, Menu, DndPanel, SelectionSelect } from '@logicflow/extension'; import { Snapshot, BpmnElement, Menu, DndPanel, SelectionSelect } from '@logicflow/extension';
import { useDesign } from '/@/hooks/web/useDesign'; import { useDesign } from '@/hooks/web/useDesign';
import { useAppStore } from '/@/store/modules/app'; import { useAppStore } from '@/store/modules/app';
import { createFlowChartContext } from './useFlowContext'; import { createFlowChartContext } from './useFlowContext';
import { toLogicFlowData } from './adpterForTurbo'; import { toLogicFlowData } from './adpterForTurbo';
import { useModal, BasicModal } from '/@/components/Modal'; import { useModal, BasicModal } from '@/components/Modal';
import { JsonPreview } from '/@/components/CodeEditor'; import { JsonPreview } from '@/components/CodeEditor';
import { configDefaultDndPanel } from './config'; import { configDefaultDndPanel } from './config';
import '@logicflow/core/dist/style/index.css'; import '@logicflow/core/dist/style/index.css';
import '@logicflow/extension/lib/style/index.css'; import '@logicflow/extension/lib/style/index.css';
export default defineComponent({ defineOptions({ name: 'FlowChart' });
name: 'FlowChart',
components: { BasicModal, FlowChartToolbar, JsonPreview },
props: {
flowOptions: {
type: Object as PropType<Definition>,
default: () => ({}),
},
data: { const props = defineProps({
type: Object as PropType<any>, flowOptions: {
default: () => ({}), type: Object as PropType<Definition>,
}, default: () => ({}),
toolbar: {
type: Boolean,
default: true,
},
patternItems: {
type: Array,
},
}, },
setup(props) {
const lfElRef = ref(null);
const graphData = ref({});
const lfInstance = ref(null) as Ref<LogicFlow | null>; data: {
type: Object as PropType<any>,
default: () => ({}),
},
const { prefixCls } = useDesign('flow-chart'); toolbar: {
const appStore = useAppStore(); type: Boolean,
const [register, { openModal }] = useModal(); default: true,
createFlowChartContext({ },
logicFlow: lfInstance as unknown as LogicFlow, patternItems: {
}); type: Array,
const getFlowOptions = computed(() => {
const { flowOptions } = props;
const defaultOptions: Partial<Definition> = {
grid: true,
background: {
color: appStore.getDarkMode === 'light' ? '#f7f9ff' : '#151515',
},
keyboard: {
enabled: true,
},
...flowOptions,
};
return defaultOptions as Definition;
});
watch(
() => props.data,
() => {
onRender();
},
);
// TODO
// watch(
// () => appStore.getDarkMode,
// () => {
// init();
// }
// );
watch(
() => unref(getFlowOptions),
(options) => {
unref(lfInstance)?.updateEditConfig(options);
},
);
// init logicFlow
async function init() {
await nextTick();
const lfEl = unref(lfElRef);
if (!lfEl) {
return;
}
LogicFlow.use(DndPanel);
// Canvas configuration
LogicFlow.use(Snapshot);
// Use the bpmn plug-in to introduce bpmn elements, which can be used after conversion in turbo
LogicFlow.use(BpmnElement);
// Start the right-click menu
LogicFlow.use(Menu);
LogicFlow.use(SelectionSelect);
lfInstance.value = new LogicFlow({
...unref(getFlowOptions),
container: lfEl,
});
const lf = unref(lfInstance)!;
lf?.setDefaultEdgeType('line');
onRender();
lf?.setPatternItems(props.patternItems || configDefaultDndPanel(lf));
}
async function onRender() {
await nextTick();
const lf = unref(lfInstance);
if (!lf) {
return;
}
const lFData = toLogicFlowData(props.data);
lf.render(lFData);
}
function handlePreview() {
const lf = unref(lfInstance);
if (!lf) {
return;
}
graphData.value = unref(lf).getGraphData();
openModal();
}
onMounted(init);
return {
register,
prefixCls,
lfElRef,
handlePreview,
graphData,
};
}, },
}); });
const lfElRef = ref(null);
const graphData = ref({});
const lfInstance = ref(null) as Ref<LogicFlow | null>;
const { prefixCls } = useDesign('flow-chart');
const appStore = useAppStore();
const [register, { openModal }] = useModal();
createFlowChartContext({
logicFlow: lfInstance as unknown as LogicFlow,
});
const getFlowOptions = computed(() => {
const { flowOptions } = props;
const defaultOptions: Partial<Definition> = {
grid: true,
background: {
color: appStore.getDarkMode === 'light' ? '#f7f9ff' : '#151515',
},
keyboard: {
enabled: true,
},
...flowOptions,
};
return defaultOptions as Definition;
});
watch(
() => props.data,
() => {
onRender();
},
);
// TODO
// watch(
// () => appStore.getDarkMode,
// () => {
// init();
// }
// );
watch(
() => unref(getFlowOptions),
(options) => {
unref(lfInstance)?.updateEditConfig(options);
},
);
// init logicFlow
async function init() {
await nextTick();
const lfEl = unref(lfElRef);
if (!lfEl) {
return;
}
LogicFlow.use(DndPanel);
// Canvas configuration
LogicFlow.use(Snapshot);
// Use the bpmn plug-in to introduce bpmn elements, which can be used after conversion in turbo
LogicFlow.use(BpmnElement);
// Start the right-click menu
LogicFlow.use(Menu);
LogicFlow.use(SelectionSelect);
lfInstance.value = new LogicFlow({
...unref(getFlowOptions),
container: lfEl,
});
const lf = unref(lfInstance)!;
lf?.setDefaultEdgeType('line');
onRender();
lf?.setPatternItems(props.patternItems || configDefaultDndPanel(lf));
}
async function onRender() {
await nextTick();
const lf = unref(lfInstance);
if (!lf) {
return;
}
const lFData = toLogicFlowData(props.data);
lf.render(lFData);
}
function handlePreview() {
const lf = unref(lfInstance);
if (!lf) {
return;
}
graphData.value = unref(lf).getGraphData();
openModal();
}
onMounted(init);
</script> </script>

View File

@@ -6,7 +6,7 @@
<span :class="`${prefixCls}-toolbar__icon`" v-if="item.icon" @click="onControl(item)"> <span :class="`${prefixCls}-toolbar__icon`" v-if="item.icon" @click="onControl(item)">
<Icon <Icon
:icon="item.icon" :icon="item.icon"
:class="item.disabled ? 'cursor-not-allowed disabeld' : 'cursor-pointer'" :class="item.disabled ? 'cursor-not-allowed disabled' : 'cursor-pointer'"
/> />
</span> </span>
</Tooltip> </Tooltip>
@@ -14,122 +14,119 @@
</template> </template>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import type { ToolbarConfig } from './types'; import type { ToolbarConfig } from './types';
import { defineComponent, ref, onUnmounted, unref, nextTick, watchEffect } from 'vue'; import { ref, onUnmounted, unref, nextTick, watchEffect } from 'vue';
import { Divider, Tooltip } from 'ant-design-vue'; import { Divider, Tooltip } from 'ant-design-vue';
import Icon from '@/components/Icon/Icon.vue'; import Icon from '@/components/Icon/Icon.vue';
import { useFlowChartContext } from './useFlowContext'; import { useFlowChartContext } from './useFlowContext';
import { ToolbarTypeEnum } from './enum'; import { ToolbarTypeEnum } from './enum';
export default defineComponent({ defineOptions({ name: 'FlowChartToolbar' });
name: 'FlowChartToolbar',
components: { Icon, Divider, Tooltip }, defineProps({
props: { prefixCls: String,
prefixCls: String, });
const emit = defineEmits(['view-data']);
const toolbarItemList = ref<ToolbarConfig[]>([
{
type: ToolbarTypeEnum.ZOOM_IN,
icon: 'codicon:zoom-out',
tooltip: '缩小',
}, },
emits: ['view-data'], {
setup(_, { emit }) { type: ToolbarTypeEnum.ZOOM_OUT,
const toolbarItemList = ref<ToolbarConfig[]>([ icon: 'codicon:zoom-in',
{ tooltip: '放大',
type: ToolbarTypeEnum.ZOOM_IN,
icon: 'codicon:zoom-out',
tooltip: '缩小',
},
{
type: ToolbarTypeEnum.ZOOM_OUT,
icon: 'codicon:zoom-in',
tooltip: '放大',
},
{
type: ToolbarTypeEnum.RESET_ZOOM,
icon: 'codicon:screen-normal',
tooltip: '重置比例',
},
{ separate: true },
{
type: ToolbarTypeEnum.UNDO,
icon: 'ion:arrow-undo-outline',
tooltip: '后退',
disabled: true,
},
{
type: ToolbarTypeEnum.REDO,
icon: 'ion:arrow-redo-outline',
tooltip: '前进',
disabled: true,
},
{ separate: true },
{
type: ToolbarTypeEnum.SNAPSHOT,
icon: 'ion:download-outline',
tooltip: '下载',
},
{
type: ToolbarTypeEnum.VIEW_DATA,
icon: 'carbon:document-view',
tooltip: '查看数据',
},
]);
const { logicFlow } = useFlowChartContext();
function onHistoryChange({ data: { undoAble, redoAble } }) {
const itemsList = unref(toolbarItemList);
const undoIndex = itemsList.findIndex((item) => item.type === ToolbarTypeEnum.UNDO);
const redoIndex = itemsList.findIndex((item) => item.type === ToolbarTypeEnum.REDO);
if (undoIndex !== -1) {
unref(toolbarItemList)[undoIndex].disabled = !undoAble;
}
if (redoIndex !== -1) {
unref(toolbarItemList)[redoIndex].disabled = !redoAble;
}
}
const onControl = (item) => {
const lf = unref(logicFlow);
if (!lf) {
return;
}
switch (item.type) {
case ToolbarTypeEnum.ZOOM_IN:
lf.zoom();
break;
case ToolbarTypeEnum.ZOOM_OUT:
lf.zoom(true);
break;
case ToolbarTypeEnum.RESET_ZOOM:
lf.resetZoom();
break;
case ToolbarTypeEnum.UNDO:
lf.undo();
break;
case ToolbarTypeEnum.REDO:
lf.redo();
break;
case ToolbarTypeEnum.SNAPSHOT:
lf.getSnapshot();
break;
case ToolbarTypeEnum.VIEW_DATA:
emit('view-data');
break;
}
};
watchEffect(async () => {
if (unref(logicFlow)) {
await nextTick();
unref(logicFlow)?.on('history:change', onHistoryChange);
}
});
onUnmounted(() => {
unref(logicFlow)?.off('history:change', onHistoryChange);
});
return { toolbarItemList, onControl };
}, },
{
type: ToolbarTypeEnum.RESET_ZOOM,
icon: 'codicon:screen-normal',
tooltip: '重置比例',
},
{ separate: true },
{
type: ToolbarTypeEnum.UNDO,
icon: 'ion:arrow-undo-outline',
tooltip: '后退',
disabled: true,
},
{
type: ToolbarTypeEnum.REDO,
icon: 'ion:arrow-redo-outline',
tooltip: '前进',
disabled: true,
},
{ separate: true },
{
type: ToolbarTypeEnum.SNAPSHOT,
icon: 'ion:download-outline',
tooltip: '下载',
},
{
type: ToolbarTypeEnum.VIEW_DATA,
icon: 'carbon:document-view',
tooltip: '查看数据',
},
]);
const { logicFlow } = useFlowChartContext();
function onHistoryChange({ data: { undoAble, redoAble } }) {
const itemsList = unref(toolbarItemList);
const undoIndex = itemsList.findIndex((item) => item.type === ToolbarTypeEnum.UNDO);
const redoIndex = itemsList.findIndex((item) => item.type === ToolbarTypeEnum.REDO);
if (undoIndex !== -1) {
unref(toolbarItemList)[undoIndex].disabled = !undoAble;
}
if (redoIndex !== -1) {
unref(toolbarItemList)[redoIndex].disabled = !redoAble;
}
}
const onControl = (item) => {
const lf = unref(logicFlow);
if (!lf) {
return;
}
switch (item.type) {
case ToolbarTypeEnum.ZOOM_IN:
lf.zoom();
break;
case ToolbarTypeEnum.ZOOM_OUT:
lf.zoom(true);
break;
case ToolbarTypeEnum.RESET_ZOOM:
lf.resetZoom();
break;
case ToolbarTypeEnum.UNDO:
lf.undo();
break;
case ToolbarTypeEnum.REDO:
lf.redo();
break;
case ToolbarTypeEnum.SNAPSHOT:
lf.getSnapshot();
break;
case ToolbarTypeEnum.VIEW_DATA:
emit('view-data');
break;
}
};
watchEffect(async () => {
if (unref(logicFlow)) {
await nextTick();
unref(logicFlow)?.on('history:change', onHistoryChange);
}
});
onUnmounted(() => {
unref(logicFlow)?.off('history:change', onHistoryChange);
}); });
</script> </script>
<style lang="less"> <style lang="less">

View File

@@ -37,286 +37,265 @@
</Row> </Row>
</Form> </Form>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import type { FormActionType, FormProps, FormSchemaInner as FormSchema } from './types/form'; import type { FormActionType, FormProps, FormSchemaInner as FormSchema } from './types/form';
import type { AdvanceState } from './types/hooks'; import type { AdvanceState } from './types/hooks';
import type { Ref } from 'vue'; import type { Ref } from 'vue';
import { defineComponent, reactive, ref, computed, unref, onMounted, watch, nextTick } from 'vue'; import { reactive, ref, computed, unref, onMounted, watch, nextTick, useAttrs } from 'vue';
import { Form, Row, type FormProps as AntFormProps } from 'ant-design-vue'; import { Form, Row, type FormProps as AntFormProps } from 'ant-design-vue';
import FormItem from './components/FormItem.vue'; import FormItem from './components/FormItem.vue';
import FormAction from './components/FormAction.vue'; import FormAction from './components/FormAction.vue';
import { dateItemType } from './helper'; import { dateItemType } from './helper';
import { dateUtil } from '/@/utils/dateUtil'; import { dateUtil } from '@/utils/dateUtil';
import { deepMerge } from '/@/utils'; import { deepMerge } from '@/utils';
import { useFormValues } from './hooks/useFormValues'; import { useFormValues } from './hooks/useFormValues';
import useAdvanced from './hooks/useAdvanced'; import useAdvanced from './hooks/useAdvanced';
import { useFormEvents } from './hooks/useFormEvents'; import { useFormEvents } from './hooks/useFormEvents';
import { createFormContext } from './hooks/useFormContext'; import { createFormContext } from './hooks/useFormContext';
import { useAutoFocus } from './hooks/useAutoFocus'; import { useAutoFocus } from './hooks/useAutoFocus';
import { useModalContext } from '/@/components/Modal'; import { useModalContext } from '@/components/Modal';
import { useDebounceFn } from '@vueuse/core'; import { useDebounceFn } from '@vueuse/core';
import { basicProps } from './props'; import { basicProps } from './props';
import { useDesign } from '/@/hooks/web/useDesign'; import { useDesign } from '@/hooks/web/useDesign';
import { cloneDeep } from 'lodash-es'; import { cloneDeep } from 'lodash-es';
export default defineComponent({ defineOptions({ name: 'BasicForm' });
name: 'BasicForm',
components: { FormItem, Form, Row, FormAction },
props: basicProps,
emits: ['advanced-change', 'reset', 'submit', 'register', 'field-value-change'],
setup(props, { emit, attrs }) {
const formModel = reactive({});
const modalFn = useModalContext();
const advanceState = reactive<AdvanceState>({ const props = defineProps(basicProps);
isAdvanced: true,
hideAdvanceBtn: false,
isLoad: false,
actionSpan: 6,
});
const defaultValueRef = ref({}); const emit = defineEmits([
const isInitedDefaultRef = ref(false); 'advanced-change',
const propsRef = ref<Partial<FormProps>>(); 'reset',
const schemaRef = ref<FormSchema[] | null>(null); 'submit',
const formElRef = ref<FormActionType | null>(null); 'register',
'field-value-change',
]);
const { prefixCls } = useDesign('basic-form'); const attrs = useAttrs();
// Get the basic configuration of the form const formModel = reactive({});
const getProps = computed(() => { const modalFn = useModalContext();
return { ...props, ...unref(propsRef) } as FormProps;
});
const getFormClass = computed(() => { const advanceState = reactive<AdvanceState>({
return [ isAdvanced: true,
prefixCls, hideAdvanceBtn: false,
{ isLoad: false,
[`${prefixCls}--compact`]: unref(getProps).compact, actionSpan: 6,
}, });
];
});
// Get uniform row style and Row configuration for the entire form const defaultValueRef = ref({});
const getRow = computed(() => { const isInitedDefaultRef = ref(false);
const { baseRowStyle = {}, rowProps } = unref(getProps); const propsRef = ref<Partial<FormProps>>();
return { const schemaRef = ref<FormSchema[] | null>(null);
style: baseRowStyle, const formElRef = ref<FormActionType | null>(null);
...rowProps,
};
});
const getBindValue = computed( const { prefixCls } = useDesign('basic-form');
() => ({ ...attrs, ...props, ...unref(getProps) }) as AntFormProps,
);
const getSchema = computed((): FormSchema[] => { // Get the basic configuration of the form
const schemas: FormSchema[] = unref(schemaRef) || (unref(getProps).schemas as any); const getProps = computed(() => {
for (const schema of schemas) { return { ...props, ...unref(propsRef) } as FormProps;
const { });
defaultValue,
component, const getFormClass = computed(() => {
componentProps, return [
isHandleDateDefaultValue = true, prefixCls,
} = schema; {
// handle date type [`${prefixCls}--compact`]: unref(getProps).compact,
if ( },
isHandleDateDefaultValue && ];
defaultValue && });
component &&
dateItemType.includes(component) // Get uniform row style and Row configuration for the entire form
) { const getRow = computed(() => {
const valueFormat = componentProps ? componentProps['valueFormat'] : null; const { baseRowStyle = {}, rowProps } = unref(getProps);
if (!Array.isArray(defaultValue)) { return {
schema.defaultValue = valueFormat style: baseRowStyle,
? dateUtil(defaultValue).format(valueFormat) ...rowProps,
: dateUtil(defaultValue); };
} else { });
const def: any[] = [];
defaultValue.forEach((item) => { const getBindValue = computed(() => ({ ...attrs, ...props, ...unref(getProps) }) as AntFormProps);
def.push(valueFormat ? dateUtil(item).format(valueFormat) : dateUtil(item));
}); const getSchema = computed((): FormSchema[] => {
schema.defaultValue = def; const schemas: FormSchema[] = unref(schemaRef) || (unref(getProps).schemas as any);
} for (const schema of schemas) {
} const { defaultValue, component, componentProps, isHandleDateDefaultValue = true } = schema;
} // handle date type
if (unref(getProps).showAdvancedButton) { if (
return cloneDeep( isHandleDateDefaultValue &&
schemas.filter((schema) => schema.component !== 'Divider') as FormSchema[], defaultValue &&
); component &&
dateItemType.includes(component)
) {
const valueFormat = componentProps ? componentProps['valueFormat'] : null;
if (!Array.isArray(defaultValue)) {
schema.defaultValue = valueFormat
? dateUtil(defaultValue).format(valueFormat)
: dateUtil(defaultValue);
} else { } else {
return cloneDeep(schemas as FormSchema[]); const def: any[] = [];
} defaultValue.forEach((item) => {
}); def.push(valueFormat ? dateUtil(item).format(valueFormat) : dateUtil(item));
const { handleToggleAdvanced, fieldsIsAdvancedMap } = useAdvanced({
advanceState,
emit,
getProps,
getSchema,
formModel,
defaultValueRef,
});
const { handleFormValues, initDefault } = useFormValues({
getProps,
defaultValueRef,
getSchema,
formModel,
});
useAutoFocus({
getSchema,
getProps,
isInitedDefault: isInitedDefaultRef,
formElRef: formElRef as Ref<FormActionType>,
});
const {
handleSubmit,
setFieldsValue,
clearValidate,
validate,
validateFields,
getFieldsValue,
updateSchema,
resetSchema,
appendSchemaByField,
removeSchemaByField,
resetFields,
scrollToField,
} = useFormEvents({
emit,
getProps,
formModel,
getSchema,
defaultValueRef,
formElRef: formElRef as Ref<FormActionType>,
schemaRef: schemaRef as Ref<FormSchema[]>,
handleFormValues,
});
createFormContext({
resetAction: resetFields,
submitAction: handleSubmit,
});
watch(
() => unref(getProps).model,
() => {
const { model } = unref(getProps);
if (!model) return;
setFieldsValue(model);
},
{
immediate: true,
},
);
watch(
() => unref(getProps).schemas,
(schemas) => {
resetSchema(schemas ?? []);
},
);
watch(
() => getSchema.value,
(schema) => {
nextTick(() => {
// Solve the problem of modal adaptive height calculation when the form is placed in the modal
modalFn?.redoModalHeight?.();
}); });
if (unref(isInitedDefaultRef)) { schema.defaultValue = def;
return;
}
if (schema?.length) {
initDefault();
isInitedDefaultRef.value = true;
}
},
);
watch(
() => formModel,
useDebounceFn(() => {
unref(getProps).submitOnChange && handleSubmit();
}, 300),
{ deep: true },
);
async function setProps(formProps: Partial<FormProps>): Promise<void> {
propsRef.value = deepMerge(unref(propsRef) || {}, formProps);
}
function setFormModel(key: string, value: any, schema: FormSchema) {
formModel[key] = value;
emit('field-value-change', key, value);
// TODO 优化验证这里如果是autoLink=false手动关联的情况下才会再次触发此函数
if (schema && schema.itemProps && !schema.itemProps.autoLink) {
validateFields([key]).catch((_) => {});
} }
} }
}
if (unref(getProps).showAdvancedButton) {
return cloneDeep(schemas.filter((schema) => schema.component !== 'Divider') as FormSchema[]);
} else {
return cloneDeep(schemas as FormSchema[]);
}
});
function handleEnterPress(e: KeyboardEvent) { const { handleToggleAdvanced, fieldsIsAdvancedMap } = useAdvanced({
const { autoSubmitOnEnter } = unref(getProps); advanceState,
if (!autoSubmitOnEnter) return; emit,
if (e.key === 'Enter' && e.target && e.target instanceof HTMLElement) { getProps,
const target: HTMLElement = e.target as HTMLElement; getSchema,
if (target && target.tagName && target.tagName.toUpperCase() == 'INPUT') { formModel,
handleSubmit(); defaultValueRef,
} });
}
}
const formActionType: Partial<FormActionType> = { const { handleFormValues, initDefault } = useFormValues({
getFieldsValue, getProps,
setFieldsValue, defaultValueRef,
resetFields, getSchema,
updateSchema, formModel,
resetSchema, });
setProps,
removeSchemaByField,
appendSchemaByField,
clearValidate,
validateFields,
validate,
submit: handleSubmit,
scrollToField: scrollToField,
};
onMounted(() => { useAutoFocus({
initDefault(); getSchema,
emit('register', formActionType); getProps,
}); isInitedDefault: isInitedDefaultRef,
formElRef: formElRef as Ref<FormActionType>,
});
return { const {
getBindValue, handleSubmit,
handleToggleAdvanced, setFieldsValue,
handleEnterPress, clearValidate,
formModel, validate,
defaultValueRef, validateFields,
advanceState, getFieldsValue,
getRow, updateSchema,
getProps, resetSchema,
formElRef, appendSchemaByField,
getSchema, removeSchemaByField,
formActionType: formActionType as any, resetFields,
setFormModel, scrollToField,
getFormClass, } = useFormEvents({
getFormActionBindProps: computed( emit,
() => getProps,
({ ...getProps.value, ...advanceState }) as InstanceType<typeof FormAction>['$props'], formModel,
), getSchema,
fieldsIsAdvancedMap, defaultValueRef,
...formActionType, formElRef: formElRef as Ref<FormActionType>,
}; schemaRef: schemaRef as Ref<FormSchema[]>,
handleFormValues,
});
createFormContext({
resetAction: resetFields,
submitAction: handleSubmit,
});
watch(
() => unref(getProps).model,
() => {
const { model } = unref(getProps);
if (!model) return;
setFieldsValue(model);
}, },
{
immediate: true,
},
);
watch(
() => unref(getProps).schemas,
(schemas) => {
resetSchema(schemas ?? []);
},
);
watch(
() => getSchema.value,
(schema) => {
nextTick(() => {
// Solve the problem of modal adaptive height calculation when the form is placed in the modal
modalFn?.redoModalHeight?.();
});
if (unref(isInitedDefaultRef)) {
return;
}
if (schema?.length) {
initDefault();
isInitedDefaultRef.value = true;
}
},
);
watch(
() => formModel,
useDebounceFn(() => {
unref(getProps).submitOnChange && handleSubmit();
}, 300),
{ deep: true },
);
async function setProps(formProps: Partial<FormProps>): Promise<void> {
propsRef.value = deepMerge(unref(propsRef) || {}, formProps);
}
function setFormModel(key: string, value: any, schema: FormSchema) {
formModel[key] = value;
emit('field-value-change', key, value);
// TODO 优化验证这里如果是autoLink=false手动关联的情况下才会再次触发此函数
if (schema && schema.itemProps && !schema.itemProps.autoLink) {
validateFields([key]).catch((_) => {});
}
}
function handleEnterPress(e: KeyboardEvent) {
const { autoSubmitOnEnter } = unref(getProps);
if (!autoSubmitOnEnter) return;
if (e.key === 'Enter' && e.target && e.target instanceof HTMLElement) {
const target: HTMLElement = e.target as HTMLElement;
if (target && target.tagName && target.tagName.toUpperCase() == 'INPUT') {
handleSubmit();
}
}
}
const formActionType: Partial<FormActionType> = {
getFieldsValue,
setFieldsValue,
resetFields,
updateSchema,
resetSchema,
setProps,
removeSchemaByField,
appendSchemaByField,
clearValidate,
validateFields,
validate,
submit: handleSubmit,
scrollToField: scrollToField,
};
const getFormActionBindProps = computed(
() => ({ ...getProps.value, ...advanceState }) as InstanceType<typeof FormAction>['$props'],
);
onMounted(() => {
initDefault();
emit('register', formActionType);
}); });
</script> </script>
<style lang="less"> <style lang="less">

View File

@@ -1,5 +1,5 @@
import type { Component } from 'vue'; import type { Component } from 'vue';
import type { ComponentType } from './types/index'; import type { ComponentType } from './types';
/** /**
* Component list, register here to setting it in the form * Component list, register here to setting it in the form
@@ -27,10 +27,10 @@ import ApiTree from './components/ApiTree.vue';
import ApiTreeSelect from './components/ApiTreeSelect.vue'; import ApiTreeSelect from './components/ApiTreeSelect.vue';
import ApiCascader from './components/ApiCascader.vue'; import ApiCascader from './components/ApiCascader.vue';
import ApiTransfer from './components/ApiTransfer.vue'; import ApiTransfer from './components/ApiTransfer.vue';
import { BasicUpload, ImageUpload } from '/@/components/Upload'; import { BasicUpload, ImageUpload } from '@/components/Upload';
import { StrengthMeter } from '/@/components/StrengthMeter'; import { StrengthMeter } from '@/components/StrengthMeter';
import { IconPicker } from '/@/components/Icon'; import { IconPicker } from '@/components/Icon';
import { CountdownInput } from '/@/components/CountDown'; import { CountdownInput } from '@/components/CountDown';
const componentMap = new Map<ComponentType, Component>(); const componentMap = new Map<ComponentType, Component>();

View File

@@ -1,5 +1,5 @@
<template> <template>
<a-cascader <Cascader
v-model:value="state" v-model:value="state"
:options="options" :options="options"
:load-data="loadData" :load-data="loadData"
@@ -16,18 +16,18 @@
{{ t('component.form.apiSelectNotFound') }} {{ t('component.form.apiSelectNotFound') }}
</span> </span>
</template> </template>
</a-cascader> </Cascader>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { type Recordable } from '@vben/types'; import { type Recordable } from '@vben/types';
import { defineComponent, PropType, ref, unref, watch, watchEffect } from 'vue'; import { PropType, ref, unref, watch, watchEffect } from 'vue';
import { Cascader } from 'ant-design-vue'; import { Cascader } from 'ant-design-vue';
import { propTypes } from '/@/utils/propTypes'; import { propTypes } from '@/utils/propTypes';
import { isFunction } from '/@/utils/is'; import { isFunction } from '@/utils/is';
import { get, omit } from 'lodash-es'; import { get, omit } from 'lodash-es';
import { useRuleFormItem } from '/@/hooks/component/useFormItem'; import { useRuleFormItem } from '@/hooks/component/useFormItem';
import { LoadingOutlined } from '@ant-design/icons-vue'; import { LoadingOutlined } from '@ant-design/icons-vue';
import { useI18n } from '/@/hooks/web/useI18n'; import { useI18n } from '@/hooks/web/useI18n';
interface Option { interface Option {
value: string; value: string;
@@ -36,165 +36,151 @@
isLeaf?: boolean; isLeaf?: boolean;
children?: Option[]; children?: Option[];
} }
export default defineComponent({
name: 'ApiCascader', defineOptions({ name: 'ApiCascader' });
components: {
LoadingOutlined, const props = defineProps({
[Cascader.name]: Cascader, value: {
type: Array,
}, },
props: { api: {
value: { type: Function as PropType<(arg?: Recordable<any>) => Promise<Option[]>>,
type: Array, default: null,
},
api: {
type: Function as PropType<(arg?: Recordable<any>) => Promise<Option[]>>,
default: null,
},
numberToString: propTypes.bool,
resultField: propTypes.string.def(''),
labelField: propTypes.string.def('label'),
valueField: propTypes.string.def('value'),
childrenField: propTypes.string.def('children'),
apiParamKey: propTypes.string.def('parentCode'),
immediate: propTypes.bool.def(true),
// init fetch params
initFetchParams: {
type: Object as PropType<Recordable<any>>,
default: () => ({}),
},
// 是否有下级,默认是
isLeaf: {
type: Function as PropType<(arg: Recordable<any>) => boolean>,
default: null,
},
displayRenderArray: {
type: Array,
},
}, },
emits: ['change', 'defaultChange'], numberToString: propTypes.bool,
setup(props, { emit }) { resultField: propTypes.string.def(''),
const apiData = ref<any[]>([]); labelField: propTypes.string.def('label'),
const options = ref<Option[]>([]); valueField: propTypes.string.def('value'),
const loading = ref<boolean>(false); childrenField: propTypes.string.def('children'),
const emitData = ref<any[]>([]); apiParamKey: propTypes.string.def('parentCode'),
const isFirstLoad = ref(true); immediate: propTypes.bool.def(true),
const { t } = useI18n(); // init fetch params
// Embedded in the form, just use the hook binding to perform form verification initFetchParams: {
const [state] = useRuleFormItem(props, 'value', 'change', emitData); type: Object as PropType<Recordable<any>>,
default: () => ({}),
watch( },
apiData, // 是否有下级,默认是
(data) => { isLeaf: {
const opts = generatorOptions(data); type: Function as PropType<(arg: Recordable<any>) => boolean>,
options.value = opts; default: null,
}, },
{ deep: true }, displayRenderArray: {
); type: Array,
function generatorOptions(options: any[]): Option[] {
const { labelField, valueField, numberToString, childrenField, isLeaf } = props;
return options.reduce((prev, next: Recordable<any>) => {
if (next) {
const value = next[valueField];
const item = {
...omit(next, [labelField, valueField]),
label: next[labelField],
value: numberToString ? `${value}` : value,
isLeaf: isLeaf && typeof isLeaf === 'function' ? isLeaf(next) : false,
};
const children = Reflect.get(next, childrenField);
if (children) {
Reflect.set(item, childrenField, generatorOptions(children));
}
prev.push(item);
}
return prev;
}, [] as Option[]);
}
async function initialFetch() {
const api = props.api;
if (!api || !isFunction(api)) return;
apiData.value = [];
loading.value = true;
try {
const res = await api(props.initFetchParams);
if (Array.isArray(res)) {
apiData.value = res;
return;
}
if (props.resultField) {
apiData.value = get(res, props.resultField) || [];
}
} catch (error) {
console.warn(error);
} finally {
loading.value = false;
}
}
async function loadData(selectedOptions: Option[]) {
const targetOption = selectedOptions[selectedOptions.length - 1];
targetOption.loading = true;
const api = props.api;
if (!api || !isFunction(api)) return;
try {
const res = await api({
[props.apiParamKey]: Reflect.get(targetOption, 'value'),
});
if (Array.isArray(res)) {
const children = generatorOptions(res);
targetOption.children = children;
return;
}
if (props.resultField) {
const children = generatorOptions(get(res, props.resultField) || []);
targetOption.children = children;
}
} catch (e) {
console.error(e);
} finally {
targetOption.loading = false;
}
}
watchEffect(() => {
props.immediate && initialFetch();
});
watch(
() => props.initFetchParams,
() => {
!unref(isFirstLoad) && initialFetch();
},
{ deep: true },
);
function handleChange(keys, args) {
emitData.value = args;
emit('defaultChange', keys, args);
}
function handleRenderDisplay({ labels, selectedOptions }) {
if (unref(emitData).length === selectedOptions.length) {
return labels.join(' / ');
}
if (props.displayRenderArray) {
return props.displayRenderArray.join(' / ');
}
return '';
}
return {
state,
options,
loading,
t,
handleChange,
loadData,
handleRenderDisplay,
};
}, },
}); });
const emit = defineEmits(['change', 'defaultChange']);
const apiData = ref<any[]>([]);
const options = ref<Option[]>([]);
const loading = ref<boolean>(false);
const emitData = ref<any[]>([]);
const isFirstLoad = ref(true);
const { t } = useI18n();
// Embedded in the form, just use the hook binding to perform form verification
const [state]: any = useRuleFormItem(props, 'value', 'change', emitData);
watch(
apiData,
(data) => {
const opts = generatorOptions(data);
options.value = opts;
},
{ deep: true },
);
function generatorOptions(options: any[]): Option[] {
const { labelField, valueField, numberToString, childrenField, isLeaf } = props;
return options.reduce((prev, next: Recordable<any>) => {
if (next) {
const value = next[valueField];
const item = {
...omit(next, [labelField, valueField]),
label: next[labelField],
value: numberToString ? `${value}` : value,
isLeaf: isLeaf && typeof isLeaf === 'function' ? isLeaf(next) : false,
};
const children = Reflect.get(next, childrenField);
if (children) {
Reflect.set(item, childrenField, generatorOptions(children));
}
prev.push(item);
}
return prev;
}, [] as Option[]);
}
async function initialFetch() {
const api = props.api;
if (!api || !isFunction(api)) return;
apiData.value = [];
loading.value = true;
try {
const res = await api(props.initFetchParams);
if (Array.isArray(res)) {
apiData.value = res;
return;
}
if (props.resultField) {
apiData.value = get(res, props.resultField) || [];
}
} catch (error) {
console.warn(error);
} finally {
loading.value = false;
}
}
async function loadData(selectedOptions: Option[]) {
const targetOption = selectedOptions[selectedOptions.length - 1];
targetOption.loading = true;
const api = props.api;
if (!api || !isFunction(api)) return;
try {
const res = await api({
[props.apiParamKey]: Reflect.get(targetOption, 'value'),
});
if (Array.isArray(res)) {
const children = generatorOptions(res);
targetOption.children = children;
return;
}
if (props.resultField) {
const children = generatorOptions(get(res, props.resultField) || []);
targetOption.children = children;
}
} catch (e) {
console.error(e);
} finally {
targetOption.loading = false;
}
}
watchEffect(() => {
props.immediate && initialFetch();
});
watch(
() => props.initFetchParams,
() => {
!unref(isFirstLoad) && initialFetch();
},
{ deep: true },
);
function handleChange(keys, args) {
emitData.value = args;
emit('defaultChange', keys, args);
}
function handleRenderDisplay({ labels, selectedOptions }) {
if (unref(emitData).length === selectedOptions.length) {
return labels.join(' / ');
}
if (props.displayRenderArray) {
return props.displayRenderArray.join(' / ');
}
return '';
}
</script> </script>

View File

@@ -2,135 +2,125 @@
* @Description:It is troublesome to implement radio button group in the form. So it is extracted independently as a separate component * @Description:It is troublesome to implement radio button group in the form. So it is extracted independently as a separate component
--> -->
<template> <template>
<RadioGroup v-bind="attrs" v-model:value="state" button-style="solid"> <Radio.Group v-bind="attrs" v-model:value="state" button-style="solid">
<template v-for="item in getOptions" :key="`${item.value}`"> <template v-for="item in getOptions" :key="`${item.value}`">
<RadioButton <Radio.Button
v-if="props.isBtn" v-if="props.isBtn"
:value="item.value" :value="item.value"
:disabled="item.disabled" :disabled="item.disabled"
@click="handleClick(item)" @click="handleClick(item)"
> >
{{ item.label }} {{ item.label }}
</RadioButton> </Radio.Button>
<Radio v-else :value="item.value" :disabled="item.disabled" @click="handleClick(item)"> <Radio v-else :value="item.value" :disabled="item.disabled" @click="handleClick(item)">
{{ item.label }} {{ item.label }}
</Radio> </Radio>
</template> </template>
</RadioGroup> </Radio.Group>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { defineComponent, type PropType, ref, watchEffect, computed, unref, watch } from 'vue'; import { type PropType, ref, watchEffect, computed, unref, watch } from 'vue';
import { Radio } from 'ant-design-vue'; import { Radio } from 'ant-design-vue';
import { isFunction } from '/@/utils/is'; import { isFunction } from '@/utils/is';
import { useRuleFormItem } from '/@/hooks/component/useFormItem'; import { useRuleFormItem } from '@/hooks/component/useFormItem';
import { useAttrs } from '@vben/hooks'; import { useAttrs } from '@vben/hooks';
import { propTypes } from '/@/utils/propTypes'; import { propTypes } from '@/utils/propTypes';
import { get, omit } from 'lodash-es'; import { get, omit } from 'lodash-es';
import { useI18n } from '/@/hooks/web/useI18n';
type OptionsItem = { label: string; value: string | number | boolean; disabled?: boolean }; type OptionsItem = { label: string; value: string | number | boolean; disabled?: boolean };
export default defineComponent({ defineOptions({ name: 'ApiRadioGroup' });
name: 'ApiRadioGroup',
components: { const props = defineProps({
RadioGroup: Radio.Group, api: {
RadioButton: Radio.Button, type: Function as PropType<(arg?: any | string) => Promise<OptionsItem[]>>,
Radio, default: null,
}, },
props: { params: {
api: { type: [Object, String] as PropType<any | string>,
type: Function as PropType<(arg?: any | string) => Promise<OptionsItem[]>>, default: () => ({}),
default: null,
},
params: {
type: [Object, String] as PropType<any | string>,
default: () => ({}),
},
value: {
type: [String, Number, Boolean] as PropType<string | number | boolean>,
},
isBtn: {
type: [Boolean] as PropType<boolean>,
default: false,
},
numberToString: propTypes.bool,
resultField: propTypes.string.def(''),
labelField: propTypes.string.def('label'),
valueField: propTypes.string.def('value'),
immediate: propTypes.bool.def(true),
}, },
emits: ['options-change', 'change'], value: {
setup(props, { emit }) { type: [String, Number, Boolean] as PropType<string | number | boolean>,
const options = ref<OptionsItem[]>([]);
const loading = ref(false);
const isFirstLoad = ref(true);
const emitData = ref<any[]>([]);
const attrs = useAttrs();
const { t } = useI18n();
// Embedded in the form, just use the hook binding to perform form verification
const [state] = useRuleFormItem(props, 'value', 'change', emitData);
// Processing options value
const getOptions = computed(() => {
const { labelField, valueField, numberToString } = props;
return unref(options).reduce((prev, next: any) => {
if (next) {
const value = next[valueField];
prev.push({
label: next[labelField],
value: numberToString ? `${value}` : value,
...omit(next, [labelField, valueField]),
});
}
return prev;
}, [] as OptionsItem[]);
});
watchEffect(() => {
props.immediate && fetch();
});
watch(
() => props.params,
() => {
!unref(isFirstLoad) && fetch();
},
{ deep: true },
);
async function fetch() {
const api = props.api;
if (!api || !isFunction(api)) return;
options.value = [];
try {
loading.value = true;
const res = await api(props.params);
if (Array.isArray(res)) {
options.value = res;
emitChange();
return;
}
if (props.resultField) {
options.value = get(res, props.resultField) || [];
}
emitChange();
} catch (error) {
console.warn(error);
} finally {
loading.value = false;
}
}
function emitChange() {
emit('options-change', unref(getOptions));
}
function handleClick(...args) {
emitData.value = args;
}
return { state, getOptions, attrs, loading, t, handleClick, props };
}, },
isBtn: {
type: [Boolean] as PropType<boolean>,
default: false,
},
numberToString: propTypes.bool,
resultField: propTypes.string.def(''),
labelField: propTypes.string.def('label'),
valueField: propTypes.string.def('value'),
immediate: propTypes.bool.def(true),
}); });
const emit = defineEmits(['options-change', 'change']);
const options = ref<OptionsItem[]>([]);
const loading = ref(false);
const isFirstLoad = ref(true);
const emitData = ref<any[]>([]);
const attrs = useAttrs();
// Embedded in the form, just use the hook binding to perform form verification
const [state] = useRuleFormItem(props, 'value', 'change', emitData);
// Processing options value
const getOptions = computed(() => {
const { labelField, valueField, numberToString } = props;
return unref(options).reduce((prev, next: any) => {
if (next) {
const value = next[valueField];
prev.push({
label: next[labelField],
value: numberToString ? `${value}` : value,
...omit(next, [labelField, valueField]),
});
}
return prev;
}, [] as OptionsItem[]);
});
watchEffect(() => {
props.immediate && fetch();
});
watch(
() => props.params,
() => {
!unref(isFirstLoad) && fetch();
},
{ deep: true },
);
async function fetch() {
const api = props.api;
if (!api || !isFunction(api)) return;
options.value = [];
try {
loading.value = true;
const res = await api(props.params);
if (Array.isArray(res)) {
options.value = res;
emitChange();
return;
}
if (props.resultField) {
options.value = get(res, props.resultField) || [];
}
emitChange();
} catch (error) {
console.warn(error);
} finally {
loading.value = false;
}
}
function emitChange() {
emit('options-change', unref(getOptions));
}
function handleClick(...args) {
emitData.value = args;
}
</script> </script>

View File

@@ -20,137 +20,128 @@
</template> </template>
</Select> </Select>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { defineComponent, PropType, ref, computed, unref, watch } from 'vue'; import { PropType, ref, computed, unref, watch } from 'vue';
import { Select } from 'ant-design-vue'; import { Select } from 'ant-design-vue';
import type { SelectValue } from 'ant-design-vue/es/select'; import type { SelectValue } from 'ant-design-vue/es/select';
import { isFunction } from '/@/utils/is'; import { isFunction } from '@/utils/is';
import { useRuleFormItem } from '/@/hooks/component/useFormItem'; import { useRuleFormItem } from '@/hooks/component/useFormItem';
import { useAttrs } from '@vben/hooks';
import { get, omit } from 'lodash-es'; import { get, omit } from 'lodash-es';
import { LoadingOutlined } from '@ant-design/icons-vue'; import { LoadingOutlined } from '@ant-design/icons-vue';
import { useI18n } from '/@/hooks/web/useI18n'; import { useI18n } from '@/hooks/web/useI18n';
import { propTypes } from '/@/utils/propTypes'; import { propTypes } from '@/utils/propTypes';
type OptionsItem = { label?: string; value?: string; disabled?: boolean; [name: string]: any }; type OptionsItem = { label?: string; value?: string; disabled?: boolean; [name: string]: any };
export default defineComponent({ defineOptions({ name: 'ApiSelect', inheritAttrs: false });
name: 'ApiSelect',
components: { const props = defineProps({
Select, value: { type: [Array, Object, String, Number] as PropType<SelectValue> },
LoadingOutlined, numberToString: propTypes.bool,
api: {
type: Function as PropType<(arg?: any) => Promise<OptionsItem[]>>,
default: null,
}, },
inheritAttrs: false, // api params
props: { params: propTypes.any.def({}),
value: { type: [Array, Object, String, Number] as PropType<SelectValue> }, // support xxx.xxx.xx
numberToString: propTypes.bool, resultField: propTypes.string.def(''),
api: { labelField: propTypes.string.def('label'),
type: Function as PropType<(arg?: any) => Promise<OptionsItem[]>>, valueField: propTypes.string.def('value'),
default: null, immediate: propTypes.bool.def(true),
}, alwaysLoad: propTypes.bool.def(false),
// api params options: {
params: propTypes.any.def({}), type: Array<OptionsItem>,
// support xxx.xxx.xx default: [],
resultField: propTypes.string.def(''),
labelField: propTypes.string.def('label'),
valueField: propTypes.string.def('value'),
immediate: propTypes.bool.def(true),
alwaysLoad: propTypes.bool.def(false),
options: {
type: Array<OptionsItem>,
default: [],
},
},
emits: ['options-change', 'change', 'update:value'],
setup(props, { emit }) {
const options = ref<OptionsItem[]>([]);
const loading = ref(false);
// 首次是否加载过了
const isFirstLoaded = ref(false);
const emitData = ref<OptionsItem[]>([]);
const attrs = useAttrs();
const { t } = useI18n();
// Embedded in the form, just use the hook binding to perform form verification
const [state] = useRuleFormItem(props, 'value', 'change', emitData);
const getOptions = computed(() => {
const { labelField, valueField, numberToString } = props;
let data = unref(options).reduce((prev, next: any) => {
if (next) {
const value = get(next, valueField);
prev.push({
...omit(next, [labelField, valueField]),
label: get(next, labelField),
value: numberToString ? `${value}` : value,
});
}
return prev;
}, [] as OptionsItem[]);
return data.length > 0 ? data : props.options;
});
watch(
() => state.value,
(v) => {
emit('update:value', v);
},
);
watch(
() => props.params,
() => {
!unref(isFirstLoaded) && fetch();
},
{ deep: true, immediate: props.immediate },
);
async function fetch() {
const api = props.api;
if (!api || !isFunction(api) || loading.value) return;
options.value = [];
try {
loading.value = true;
const res = await api(props.params);
isFirstLoaded.value = true;
if (Array.isArray(res)) {
options.value = res;
emitChange();
return;
}
if (props.resultField) {
options.value = get(res, props.resultField) || [];
}
emitChange();
} catch (error) {
console.warn(error);
} finally {
loading.value = false;
// reset status
isFirstLoaded.value = false;
}
}
async function handleFetch(visible: boolean) {
if (visible) {
if (props.alwaysLoad) {
await fetch();
} else if (!props.immediate && !unref(isFirstLoaded)) {
await fetch();
}
}
}
function emitChange() {
emit('options-change', unref(getOptions));
}
function handleChange(_, ...args) {
emitData.value = args;
}
return { state, attrs, getOptions, loading, t, handleFetch, handleChange };
}, },
}); });
const emit = defineEmits(['options-change', 'change', 'update:value']);
const optionsRef = ref<OptionsItem[]>([]);
const loading = ref(false);
// 首次是否加载过了
const isFirstLoaded = ref(false);
const emitData = ref<OptionsItem[]>([]);
const { t } = useI18n();
// Embedded in the form, just use the hook binding to perform form verification
const [state] = useRuleFormItem(props, 'value', 'change', emitData);
const getOptions = computed(() => {
const { labelField, valueField, numberToString } = props;
let data = unref(optionsRef).reduce((prev, next: any) => {
if (next) {
const value = get(next, valueField);
prev.push({
...omit(next, [labelField, valueField]),
label: get(next, labelField),
value: numberToString ? `${value}` : value,
});
}
return prev;
}, [] as OptionsItem[]);
return data.length > 0 ? data : props.options;
});
watch(
() => state.value,
(v) => {
emit('update:value', v);
},
);
watch(
() => props.params,
() => {
!unref(isFirstLoaded) && fetch();
},
{ deep: true, immediate: props.immediate },
);
async function fetch() {
const api = props.api;
if (!api || !isFunction(api) || loading.value) return;
optionsRef.value = [];
try {
loading.value = true;
const res = await api(props.params);
isFirstLoaded.value = true;
if (Array.isArray(res)) {
optionsRef.value = res;
emitChange();
return;
}
if (props.resultField) {
optionsRef.value = get(res, props.resultField) || [];
}
emitChange();
} catch (error) {
console.warn(error);
} finally {
loading.value = false;
// reset status
isFirstLoaded.value = false;
}
}
async function handleFetch(visible: boolean) {
if (visible) {
if (props.alwaysLoad) {
await fetch();
} else if (!props.immediate && !unref(isFirstLoaded)) {
await fetch();
}
}
}
function emitChange() {
emit('options-change', unref(getOptions));
}
function handleChange(_, ...args) {
emitData.value = args;
}
</script> </script>

View File

@@ -12,127 +12,116 @@
/> />
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { computed, defineComponent, watch, ref, unref, watchEffect, PropType } from 'vue'; import { computed, watch, ref, unref, watchEffect, PropType } from 'vue';
import { Transfer } from 'ant-design-vue'; import { Transfer } from 'ant-design-vue';
import { isFunction } from '/@/utils/is'; import { isFunction } from '@/utils/is';
import { get, omit } from 'lodash-es'; import { get, omit } from 'lodash-es';
import { propTypes } from '/@/utils/propTypes'; import { propTypes } from '@/utils/propTypes';
import { useI18n } from '/@/hooks/web/useI18n';
import { TransferDirection, TransferItem } from 'ant-design-vue/lib/transfer'; import { TransferDirection, TransferItem } from 'ant-design-vue/lib/transfer';
export default defineComponent({ defineOptions({ name: 'ApiTransfer' });
name: 'ApiTransfer',
components: { Transfer }, const props = defineProps({
props: { value: { type: Array as PropType<Array<string>> },
value: { type: Array as PropType<Array<string>> }, api: {
api: { type: Function as PropType<(arg) => Promise<TransferItem[]>>,
type: Function as PropType<(arg) => Promise<TransferItem[]>>, default: null,
default: null,
},
params: { type: Object },
dataSource: { type: Array as PropType<Array<TransferItem>> },
immediate: propTypes.bool.def(true),
alwaysLoad: propTypes.bool.def(false),
afterFetch: { type: Function },
resultField: propTypes.string.def(''),
labelField: propTypes.string.def('title'),
valueField: propTypes.string.def('key'),
showSearch: { type: Boolean, default: false },
disabled: { type: Boolean, default: false },
filterOption: {
type: Function as PropType<(inputValue: string, item: TransferItem) => boolean>,
},
selectedKeys: { type: Array as PropType<Array<string>> },
showSelectAll: { type: Boolean, default: false },
targetKeys: { type: Array as PropType<Array<string>> },
}, },
emits: ['options-change', 'change'], params: { type: Object },
setup(props, { attrs, emit }) { dataSource: { type: Array as PropType<Array<TransferItem>> },
const _dataSource = ref<TransferItem[]>([]); immediate: propTypes.bool.def(true),
const _targetKeys = ref<string[]>([]); alwaysLoad: propTypes.bool.def(false),
const { t } = useI18n(); afterFetch: { type: Function },
resultField: propTypes.string.def(''),
labelField: propTypes.string.def('title'),
valueField: propTypes.string.def('key'),
showSearch: { type: Boolean, default: false },
disabled: { type: Boolean, default: false },
filterOption: {
type: Function as PropType<(inputValue: string, item: TransferItem) => boolean>,
},
selectedKeys: { type: Array as PropType<Array<string>> },
showSelectAll: { type: Boolean, default: false },
targetKeys: { type: Array as PropType<Array<string>> },
});
const getAttrs = computed(() => { const emit = defineEmits(['options-change', 'change']);
return {
...(!props.api ? { dataSource: unref(_dataSource) } : {}),
...attrs,
};
});
const getdataSource = computed(() => {
const { labelField, valueField } = props;
return unref(_dataSource).reduce((prev, next) => { const _dataSource = ref<TransferItem[]>([]);
if (next) { const _targetKeys = ref<string[]>([]);
prev.push({
...omit(next, [labelField, valueField]), const getdataSource = computed(() => {
title: next[labelField], const { labelField, valueField } = props;
key: next[valueField],
}); return unref(_dataSource).reduce((prev, next) => {
} if (next) {
return prev; prev.push({
}, [] as TransferItem[]); ...omit(next, [labelField, valueField]),
}); title: next[labelField],
const getTargetKeys = computed<string[]>(() => { key: next[valueField],
/* if (unref(_targetKeys).length > 0) { });
}
return prev;
}, [] as TransferItem[]);
});
const getTargetKeys = computed<string[]>(() => {
/* if (unref(_targetKeys).length > 0) {
return unref(_targetKeys); return unref(_targetKeys);
} */ } */
if (Array.isArray(props.value)) { if (Array.isArray(props.value)) {
return props.value; return props.value;
} }
if (Array.isArray(props.targetKeys)) { if (Array.isArray(props.targetKeys)) {
return props.targetKeys; return props.targetKeys;
} }
return []; return [];
});
function handleChange(keys: string[], direction: TransferDirection, moveKeys: string[]) {
_targetKeys.value = keys;
console.log(direction);
console.log(moveKeys);
emit('change', keys);
}
watchEffect(() => {
props.immediate && !props.alwaysLoad && fetch();
});
watch(
() => props.params,
() => {
fetch();
},
{ deep: true },
);
async function fetch() {
const api = props.api;
if (!api || !isFunction(api)) {
if (Array.isArray(props.dataSource)) {
_dataSource.value = props.dataSource;
}
return;
}
_dataSource.value = [];
try {
const res = await api(props.params);
if (Array.isArray(res)) {
_dataSource.value = res;
emitChange();
return;
}
if (props.resultField) {
_dataSource.value = get(res, props.resultField) || [];
}
emitChange();
} catch (error) {
console.warn(error);
}
}
function emitChange() {
emit('options-change', unref(getdataSource));
}
return { getTargetKeys, getdataSource, t, getAttrs, handleChange };
},
}); });
function handleChange(keys: string[], direction: TransferDirection, moveKeys: string[]) {
_targetKeys.value = keys;
console.log(direction);
console.log(moveKeys);
emit('change', keys);
}
watchEffect(() => {
props.immediate && !props.alwaysLoad && fetch();
});
watch(
() => props.params,
() => {
fetch();
},
{ deep: true },
);
async function fetch() {
const api = props.api;
if (!api || !isFunction(api)) {
if (Array.isArray(props.dataSource)) {
_dataSource.value = props.dataSource;
}
return;
}
_dataSource.value = [];
try {
const res = await api(props.params);
if (Array.isArray(res)) {
_dataSource.value = res;
emitChange();
return;
}
if (props.resultField) {
_dataSource.value = get(res, props.resultField) || [];
}
emitChange();
} catch (error) {
console.warn(error);
}
}
function emitChange() {
emit('options-change', unref(getdataSource));
}
</script> </script>

View File

@@ -1,99 +1,97 @@
<template> <template>
<a-tree v-bind="getAttrs" v-model:selectedKeys="state"> <Tree v-bind="getAttrs" v-model:selectedKeys="state">
<template #[item]="data" v-for="item in Object.keys($slots)"> <template #[item]="data" v-for="item in Object.keys($slots)">
<slot :name="item" v-bind="data || {}"></slot> <slot :name="item" v-bind="data || {}"></slot>
</template> </template>
</a-tree> </Tree>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { type Recordable, type AnyFunction } from '@vben/types'; import { type Recordable, type AnyFunction } from '@vben/types';
import { type PropType, computed, defineComponent, watch, ref, onMounted, unref } from 'vue'; import { type PropType, computed, watch, ref, onMounted, unref, useAttrs } from 'vue';
import { Tree, TreeProps } from 'ant-design-vue'; import { Tree, TreeProps } from 'ant-design-vue';
import { isArray, isFunction } from '/@/utils/is'; import { isArray, isFunction } from '@/utils/is';
import { get } from 'lodash-es'; import { get } from 'lodash-es';
import { propTypes } from '/@/utils/propTypes';
import { DataNode } from 'ant-design-vue/es/tree'; import { DataNode } from 'ant-design-vue/es/tree';
import { useRuleFormItem } from '/@/hooks/component/useFormItem'; import { useRuleFormItem } from '@/hooks/component/useFormItem';
export default defineComponent({ defineOptions({ name: 'ApiTree' });
name: 'ApiTree',
components: { ATree: Tree },
props: {
api: { type: Function as PropType<(arg?: Recordable<any>) => Promise<Recordable<any>>> },
params: { type: Object },
immediate: { type: Boolean, default: true },
resultField: propTypes.string.def(''),
afterFetch: { type: Function as PropType<AnyFunction> },
value: {
type: Array as PropType<TreeProps['selectedKeys']>,
},
},
emits: ['options-change', 'change', 'update:value'],
setup(props, { attrs, emit }) {
const treeData = ref<DataNode[]>([]);
const isFirstLoaded = ref<Boolean>(false);
const loading = ref(false);
const emitData = ref<any[]>([]);
const [state] = useRuleFormItem(props, 'value', 'change', emitData); const props = defineProps({
const getAttrs = computed(() => { api: { type: Function as PropType<(arg?: Recordable<any>) => Promise<Recordable<any>>> },
return { params: { type: Object },
...(props.api ? { treeData: unref(treeData) } : {}), immediate: { type: Boolean, default: true },
...attrs, resultField: { type: String, default: '' },
}; afterFetch: { type: Function as PropType<AnyFunction> },
}); value: {
type: Array as PropType<TreeProps['selectedKeys']>,
watch(
() => state.value,
(v) => {
emit('update:value', v);
},
);
watch(
() => props.params,
() => {
!unref(isFirstLoaded) && fetch();
},
{ deep: true },
);
watch(
() => props.immediate,
(v) => {
v && !isFirstLoaded.value && fetch();
},
);
onMounted(() => {
props.immediate && fetch();
});
async function fetch() {
const { api, afterFetch } = props;
if (!api || !isFunction(api)) return;
loading.value = true;
treeData.value = [];
let result;
try {
result = await api(props.params);
} catch (e) {
console.error(e);
}
if (afterFetch && isFunction(afterFetch)) {
result = afterFetch(result);
}
loading.value = false;
if (!result) return;
if (!isArray(result)) {
result = get(result, props.resultField);
}
treeData.value = (result as (Recordable & { key: string | number })[]) || [];
isFirstLoaded.value = true;
emit('options-change', treeData.value);
}
return { getAttrs, loading, state };
}, },
}); });
const emit = defineEmits(['options-change', 'change', 'update:value']);
const attrs = useAttrs();
const treeData = ref<DataNode[]>([]);
const isFirstLoaded = ref<Boolean>(false);
const loading = ref(false);
const emitData = ref<any[]>([]);
const [state] = useRuleFormItem(props, 'value', 'change', emitData);
const getAttrs = computed(() => {
return {
...(props.api ? { treeData: unref(treeData) } : {}),
...attrs,
};
});
watch(
() => state.value,
(v) => {
emit('update:value', v);
},
);
watch(
() => props.params,
() => {
!unref(isFirstLoaded) && fetch();
},
{ deep: true },
);
watch(
() => props.immediate,
(v) => {
v && !isFirstLoaded.value && fetch();
},
);
onMounted(() => {
props.immediate && fetch();
});
async function fetch() {
const { api, afterFetch } = props;
if (!api || !isFunction(api)) return;
loading.value = true;
treeData.value = [];
let result;
try {
result = await api(props.params);
} catch (e) {
console.error(e);
}
if (afterFetch && isFunction(afterFetch)) {
result = afterFetch(result);
}
loading.value = false;
if (!result) return;
if (!isArray(result)) {
result = get(result, props.resultField);
}
treeData.value = (result as (Recordable & { key: string | number })[]) || [];
isFirstLoaded.value = true;
emit('options-change', treeData.value);
}
</script> </script>

View File

@@ -1,5 +1,5 @@
<template> <template>
<a-tree-select <TreeSelect
v-bind="getAttrs" v-bind="getAttrs"
@change="handleChange" @change="handleChange"
:field-names="fieldNames" :field-names="fieldNames"
@@ -11,102 +11,100 @@
<template #suffixIcon v-if="loading"> <template #suffixIcon v-if="loading">
<LoadingOutlined spin /> <LoadingOutlined spin />
</template> </template>
</a-tree-select> </TreeSelect>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { type Recordable } from '@vben/types'; import { type Recordable } from '@vben/types';
import { type PropType, computed, defineComponent, watch, ref, onMounted, unref } from 'vue'; import { type PropType, computed, watch, ref, onMounted, unref, useAttrs } from 'vue';
import { TreeSelect } from 'ant-design-vue'; import { TreeSelect } from 'ant-design-vue';
import { isArray, isFunction } from '/@/utils/is'; import { isArray, isFunction } from '@/utils/is';
import { get } from 'lodash-es'; import { get } from 'lodash-es';
import { propTypes } from '/@/utils/propTypes'; import { propTypes } from '@/utils/propTypes';
import { LoadingOutlined } from '@ant-design/icons-vue'; import { LoadingOutlined } from '@ant-design/icons-vue';
export default defineComponent({ defineOptions({ name: 'ApiTreeSelect' });
name: 'ApiTreeSelect',
components: { ATreeSelect: TreeSelect, LoadingOutlined },
props: {
api: { type: Function as PropType<(arg?: Recordable<any>) => Promise<Recordable<any>>> },
params: { type: Object },
immediate: { type: Boolean, default: true },
async: { type: Boolean, default: false },
resultField: propTypes.string.def(''),
labelField: propTypes.string.def('title'),
valueField: propTypes.string.def('value'),
childrenField: propTypes.string.def('children'),
},
emits: ['options-change', 'change', 'load-data'],
setup(props, { attrs, emit }) {
const treeData = ref<Recordable<any>[]>([]);
const isFirstLoaded = ref<Boolean>(false);
const loading = ref(false);
const getAttrs = computed(() => {
return {
...(props.api ? { treeData: unref(treeData) } : {}),
...attrs,
};
});
const fieldNames = {
children: props.childrenField,
value: props.valueField,
label: props.labelField,
};
function handleChange(...args) { const props = defineProps({
emit('change', ...args); api: { type: Function as PropType<(arg?: Recordable<any>) => Promise<Recordable<any>>> },
} params: { type: Object },
immediate: { type: Boolean, default: true },
watch( async: { type: Boolean, default: false },
() => props.params, resultField: propTypes.string.def(''),
() => { labelField: propTypes.string.def('title'),
!unref(isFirstLoaded) && fetch(); valueField: propTypes.string.def('value'),
}, childrenField: propTypes.string.def('children'),
{ deep: true },
);
watch(
() => props.immediate,
(v) => {
v && !isFirstLoaded.value && fetch();
},
);
onMounted(() => {
props.immediate && fetch();
});
function onLoadData(treeNode) {
return new Promise((resolve: (value?: unknown) => void) => {
if (isArray(treeNode.children) && treeNode.children.length > 0) {
resolve();
return;
}
emit('load-data', { treeData, treeNode, resolve });
});
}
async function fetch() {
const { api } = props;
if (!api || !isFunction(api) || loading.value) return;
loading.value = true;
treeData.value = [];
let result;
try {
result = await api(props.params);
} catch (e) {
console.error(e);
}
loading.value = false;
if (!result) return;
if (!isArray(result)) {
result = get(result, props.resultField);
}
treeData.value = (result as Recordable<any>[]) || [];
isFirstLoaded.value = true;
emit('options-change', treeData.value);
}
return { getAttrs, loading, handleChange, fieldNames, onLoadData };
},
}); });
const emit = defineEmits(['options-change', 'change', 'load-data']);
const attrs = useAttrs();
const treeData = ref<Recordable<any>[]>([]);
const isFirstLoaded = ref<Boolean>(false);
const loading = ref(false);
const getAttrs = computed(() => {
return {
...(props.api ? { treeData: unref(treeData) } : {}),
...attrs,
};
});
const fieldNames = {
children: props.childrenField,
value: props.valueField,
label: props.labelField,
};
function handleChange(...args) {
emit('change', ...args);
}
watch(
() => props.params,
() => {
!unref(isFirstLoaded) && fetch();
},
{ deep: true },
);
watch(
() => props.immediate,
(v) => {
v && !isFirstLoaded.value && fetch();
},
);
onMounted(() => {
props.immediate && fetch();
});
function onLoadData(treeNode) {
return new Promise((resolve: (value?: unknown) => void) => {
if (isArray(treeNode.children) && treeNode.children.length > 0) {
resolve();
return;
}
emit('load-data', { treeData, treeNode, resolve });
});
}
async function fetch() {
const { api } = props;
if (!api || !isFunction(api) || loading.value) return;
loading.value = true;
treeData.value = [];
let result;
try {
result = await api(props.params);
} catch (e) {
console.error(e);
}
loading.value = false;
if (!result) return;
if (!isArray(result)) {
result = get(result, props.resultField);
}
treeData.value = (result as Recordable<any>[]) || [];
isFirstLoaded.value = true;
emit('options-change', treeData.value);
}
</script> </script>

View File

@@ -1,7 +1,7 @@
<template> <template>
<a-col v-bind="actionColOpt" v-if="showActionButtonGroup"> <Col v-bind="actionColOpt" v-if="showActionButtonGroup">
<div style="width: 100%" :style="{ textAlign: actionColOpt.style.textAlign }"> <div style="width: 100%" :style="{ textAlign: actionColOpt.style.textAlign }">
<FormItem> <Form.Item>
<slot name="resetBefore"></slot> <slot name="resetBefore"></slot>
<Button <Button
type="default" type="default"
@@ -35,100 +35,83 @@
<BasicArrow class="ml-1" :expand="!isAdvanced" up /> <BasicArrow class="ml-1" :expand="!isAdvanced" up />
</Button> </Button>
<slot name="advanceAfter"></slot> <slot name="advanceAfter"></slot>
</FormItem> </Form.Item>
</div> </div>
</a-col> </Col>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import type { ColEx } from '../types/index'; import type { ColEx } from '../types';
import { defineComponent, computed, PropType } from 'vue'; import { computed, PropType } from 'vue';
import { Form, Col } from 'ant-design-vue'; import { Form, Col } from 'ant-design-vue';
import { Button, ButtonProps } from '/@/components/Button'; import { Button, ButtonProps } from '@/components/Button';
import { BasicArrow } from '/@/components/Basic'; import { BasicArrow } from '@/components/Basic';
import { useFormContext } from '../hooks/useFormContext'; import { useFormContext } from '../hooks/useFormContext';
import { useI18n } from '/@/hooks/web/useI18n'; import { useI18n } from '@/hooks/web/useI18n';
import { propTypes } from '/@/utils/propTypes'; import { propTypes } from '@/utils/propTypes';
type ButtonOptions = Partial<ButtonProps> & { text: string }; type ButtonOptions = Partial<ButtonProps> & { text: string };
export default defineComponent({ defineOptions({ name: 'BasicFormAction' });
name: 'BasicFormAction',
components: { const props = defineProps({
FormItem: Form.Item, showActionButtonGroup: propTypes.bool.def(true),
Button, showResetButton: propTypes.bool.def(true),
BasicArrow, showSubmitButton: propTypes.bool.def(true),
[Col.name]: Col, showAdvancedButton: propTypes.bool.def(true),
resetButtonOptions: {
type: Object as PropType<ButtonOptions>,
default: () => ({}),
}, },
props: { submitButtonOptions: {
showActionButtonGroup: propTypes.bool.def(true), type: Object as PropType<ButtonOptions>,
showResetButton: propTypes.bool.def(true), default: () => ({}),
showSubmitButton: propTypes.bool.def(true),
showAdvancedButton: propTypes.bool.def(true),
resetButtonOptions: {
type: Object as PropType<ButtonOptions>,
default: () => ({}),
},
submitButtonOptions: {
type: Object as PropType<ButtonOptions>,
default: () => ({}),
},
actionColOptions: {
type: Object as PropType<Partial<ColEx>>,
default: () => ({}),
},
actionSpan: propTypes.number.def(6),
isAdvanced: propTypes.bool,
hideAdvanceBtn: propTypes.bool,
}, },
emits: ['toggle-advanced'], actionColOptions: {
setup(props, { emit }) { type: Object as PropType<Partial<ColEx>>,
const { t } = useI18n(); default: () => ({}),
const actionColOpt = computed(() => {
const { showAdvancedButton, actionSpan: span, actionColOptions } = props;
const actionSpan = 24 - span;
const advancedSpanObj = showAdvancedButton
? { span: actionSpan < 6 ? 24 : actionSpan }
: {};
const actionColOpt: Partial<ColEx> = {
style: { textAlign: 'right' },
span: showAdvancedButton ? 6 : 4,
...advancedSpanObj,
...actionColOptions,
};
return actionColOpt;
});
const getResetBtnOptions = computed((): ButtonOptions => {
return Object.assign(
{
text: t('common.resetText'),
},
props.resetButtonOptions,
);
});
const getSubmitBtnOptions = computed(() => {
return Object.assign(
{
text: t('common.queryText'),
},
props.submitButtonOptions,
);
});
function toggleAdvanced() {
emit('toggle-advanced');
}
return {
t,
actionColOpt,
getResetBtnOptions,
getSubmitBtnOptions,
toggleAdvanced,
...useFormContext(),
};
}, },
actionSpan: propTypes.number.def(6),
isAdvanced: propTypes.bool,
hideAdvanceBtn: propTypes.bool,
}); });
const emit = defineEmits(['toggle-advanced']);
const { t } = useI18n();
const { resetAction, submitAction } = useFormContext();
const actionColOpt = computed(() => {
const { showAdvancedButton, actionSpan: span, actionColOptions } = props;
const actionSpan = 24 - span;
const advancedSpanObj = showAdvancedButton ? { span: actionSpan < 6 ? 24 : actionSpan } : {};
const actionColOpt: Partial<ColEx> = {
style: { textAlign: 'right' },
span: showAdvancedButton ? 6 : 4,
...advancedSpanObj,
...actionColOptions,
};
return actionColOpt;
});
const getResetBtnOptions = computed((): ButtonOptions => {
return Object.assign(
{
text: t('common.resetText'),
},
props.resetButtonOptions,
);
});
const getSubmitBtnOptions = computed(() => {
return Object.assign(
{
text: t('common.queryText'),
},
props.submitButtonOptions,
);
});
function toggleAdvanced() {
emit('toggle-advanced');
}
</script> </script>

View File

@@ -9,12 +9,12 @@
type FormSchemaInner as FormSchema, type FormSchemaInner as FormSchema,
} from '../types/form'; } from '../types/form';
import type { Rule as ValidationRule } from 'ant-design-vue/lib/form/interface'; import type { Rule as ValidationRule } from 'ant-design-vue/lib/form/interface';
import type { TableActionType } from '/@/components/Table'; import type { TableActionType } from '@/components/Table';
import { Col, Divider, Form } from 'ant-design-vue'; import { Col, Divider, Form } from 'ant-design-vue';
import { componentMap } from '../componentMap'; import { componentMap } from '../componentMap';
import { BasicHelp } from '/@/components/Basic'; import { BasicHelp } from '@/components/Basic';
import { isBoolean, isFunction, isNull } from '/@/utils/is'; import { isBoolean, isFunction, isNull } from '@/utils/is';
import { getSlot } from '/@/utils/helper/tsxHelper'; import { getSlot } from '@/utils/helper/tsxHelper';
import { import {
createPlaceholderMessage, createPlaceholderMessage,
NO_AUTO_LINK_COMPONENTS, NO_AUTO_LINK_COMPONENTS,
@@ -22,7 +22,7 @@
} from '../helper'; } from '../helper';
import { cloneDeep, upperFirst } from 'lodash-es'; import { cloneDeep, upperFirst } from 'lodash-es';
import { useItemLabelWidth } from '../hooks/useLabelWidth'; import { useItemLabelWidth } from '../hooks/useLabelWidth';
import { useI18n } from '/@/hooks/web/useI18n'; import { useI18n } from '@/hooks/web/useI18n';
export default defineComponent({ export default defineComponent({
name: 'BasicFormItem', name: 'BasicFormItem',

View File

@@ -2,62 +2,55 @@
* @Description:It is troublesome to implement radio button group in the form. So it is extracted independently as a separate component * @Description:It is troublesome to implement radio button group in the form. So it is extracted independently as a separate component
--> -->
<template> <template>
<RadioGroup v-bind="attrs" v-model:value="state" button-style="solid"> <Radio.Group v-bind="attrs" v-model:value="state" button-style="solid">
<template v-for="item in getOptions" :key="`${item.value}`"> <template v-for="item in getOptions" :key="`${item.value}`">
<RadioButton :value="item.value" :disabled="item.disabled" @click="handleClick(item)"> <Radio.Button :value="item.value" :disabled="item.disabled" @click="handleClick(item)">
{{ item.label }} {{ item.label }}
</RadioButton> </Radio.Button>
</template> </template>
</RadioGroup> </Radio.Group>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { defineComponent, PropType, computed, ref } from 'vue'; import { PropType, computed, ref } from 'vue';
import { Radio } from 'ant-design-vue'; import { Radio } from 'ant-design-vue';
import { isString } from '/@/utils/is'; import { isString } from '@/utils/is';
import { useRuleFormItem } from '/@/hooks/component/useFormItem'; import { useRuleFormItem } from '@/hooks/component/useFormItem';
import { useAttrs } from '@vben/hooks'; import { useAttrs } from '@vben/hooks';
type OptionsItem = { label: string; value: string | number | boolean; disabled?: boolean }; type OptionsItem = { label: string; value: string | number | boolean; disabled?: boolean };
type RadioItem = string | OptionsItem; type RadioItem = string | OptionsItem;
export default defineComponent({ defineOptions({ name: 'RadioButtonGroup' });
name: 'RadioButtonGroup',
components: { const props = defineProps({
RadioGroup: Radio.Group, value: {
RadioButton: Radio.Button, type: [String, Number, Boolean] as PropType<string | number | boolean>,
}, },
props: { options: {
value: { type: Array as PropType<RadioItem[]>,
type: [String, Number, Boolean] as PropType<string | number | boolean>, default: () => [],
},
options: {
type: Array as PropType<RadioItem[]>,
default: () => [],
},
},
emits: ['change'],
setup(props) {
const attrs = useAttrs();
const emitData = ref<any[]>([]);
// Embedded in the form, just use the hook binding to perform form verification
const [state] = useRuleFormItem(props, 'value', 'change', emitData);
// Processing options value
const getOptions = computed((): OptionsItem[] => {
const { options } = props;
if (!options || options?.length === 0) return [];
const isStringArr = options.some((item) => isString(item));
if (!isStringArr) return options as OptionsItem[];
return options.map((item) => ({ label: item, value: item })) as OptionsItem[];
});
function handleClick(...args) {
emitData.value = args;
}
return { state, getOptions, attrs, handleClick };
}, },
}); });
// const emit = defineEmits(['change']);
const attrs = useAttrs();
const emitData = ref<any[]>([]);
// Embedded in the form, just use the hook binding to perform form verification
const [state] = useRuleFormItem(props, 'value', 'change', emitData);
// Processing options value
const getOptions = computed((): OptionsItem[] => {
const { options } = props;
if (!options || options?.length === 0) return [];
const isStringArr = options.some((item) => isString(item));
if (!isStringArr) return options as OptionsItem[];
return options.map((item) => ({ label: item, value: item })) as OptionsItem[];
});
function handleClick(...args) {
emitData.value = args;
}
</script> </script>

View File

@@ -1,8 +1,8 @@
import type { Rule as ValidationRule } from 'ant-design-vue/lib/form/interface'; import type { Rule as ValidationRule } from 'ant-design-vue/lib/form/interface';
import type { ComponentType } from './types/index'; import type { ComponentType } from './types';
import { useI18n } from '/@/hooks/web/useI18n'; import { useI18n } from '@/hooks/web/useI18n';
import { dateUtil } from '/@/utils/dateUtil'; import { dateUtil } from '@/utils/dateUtil';
import { isNumber, isObject } from '/@/utils/is'; import { isNumber, isObject } from '@/utils/is';
const { t } = useI18n(); const { t } = useI18n();

View File

@@ -2,8 +2,8 @@ import type { ColEx } from '../types';
import type { AdvanceState } from '../types/hooks'; import type { AdvanceState } from '../types/hooks';
import { ComputedRef, getCurrentInstance, Ref, shallowReactive, computed, unref, watch } from 'vue'; import { ComputedRef, getCurrentInstance, Ref, shallowReactive, computed, unref, watch } from 'vue';
import type { FormProps, FormSchemaInner as FormSchema } from '../types/form'; import type { FormProps, FormSchemaInner as FormSchema } from '../types/form';
import { isBoolean, isFunction, isNumber, isObject } from '/@/utils/is'; import { isBoolean, isFunction, isNumber, isObject } from '@/utils/is';
import { useBreakpoint } from '/@/hooks/event/useBreakpoint'; import { useBreakpoint } from '@/hooks/event/useBreakpoint';
import { useDebounceFn } from '@vueuse/core'; import { useDebounceFn } from '@vueuse/core';
const BASIC_COL_LEN = 24; const BASIC_COL_LEN = 24;

View File

@@ -1,4 +1,4 @@
import type { ComponentType } from '../types/index'; import type { ComponentType } from '../types';
import { tryOnUnmounted } from '@vueuse/core'; import { tryOnUnmounted } from '@vueuse/core';
import { add, del } from '../componentMap'; import { add, del } from '../componentMap';
import type { Component } from 'vue'; import type { Component } from 'vue';

View File

@@ -7,9 +7,9 @@ import type {
import type { NamePath } from 'ant-design-vue/lib/form/interface'; import type { NamePath } from 'ant-design-vue/lib/form/interface';
import type { DynamicProps } from '/#/utils'; import type { DynamicProps } from '/#/utils';
import { ref, onUnmounted, unref, nextTick, watch } from 'vue'; import { ref, onUnmounted, unref, nextTick, watch } from 'vue';
import { isProdMode } from '/@/utils/env'; import { isProdMode } from '@/utils/env';
import { error } from '/@/utils/log'; import { error } from '@/utils/log';
import { getDynamicProps } from '/@/utils'; import { getDynamicProps } from '@/utils';
export declare type ValidateFields = (nameList?: NamePath[]) => Promise<Recordable>; export declare type ValidateFields = (nameList?: NamePath[]) => Promise<Recordable>;

View File

@@ -1,5 +1,5 @@
import type { InjectionKey } from 'vue'; import type { InjectionKey } from 'vue';
import { createContext, useContext } from '/@/hooks/core/useContext'; import { createContext, useContext } from '@/hooks/core/useContext';
export interface FormContextProps { export interface FormContextProps {
resetAction: () => Promise<void>; resetAction: () => Promise<void>;

View File

@@ -2,12 +2,12 @@ import type { ComputedRef, Ref } from 'vue';
import type { FormProps, FormSchemaInner as FormSchema, FormActionType } from '../types/form'; import type { FormProps, FormSchemaInner as FormSchema, FormActionType } from '../types/form';
import type { NamePath } from 'ant-design-vue/lib/form/interface'; import type { NamePath } from 'ant-design-vue/lib/form/interface';
import { unref, toRaw, nextTick } from 'vue'; import { unref, toRaw, nextTick } from 'vue';
import { isArray, isFunction, isObject, isString, isDef, isNil } from '/@/utils/is'; import { isArray, isFunction, isObject, isString, isDef, isNil } from '@/utils/is';
import { deepMerge } from '/@/utils'; import { deepMerge } from '@/utils';
import { dateItemType, handleInputNumberValue, defaultValueComponents } from '../helper'; import { dateItemType, handleInputNumberValue, defaultValueComponents } from '../helper';
import { dateUtil } from '/@/utils/dateUtil'; import { dateUtil } from '@/utils/dateUtil';
import { cloneDeep, set, uniqBy, get } from 'lodash-es'; import { cloneDeep, set, uniqBy, get } from 'lodash-es';
import { error } from '/@/utils/log'; import { error } from '@/utils/log';
interface UseFormActionContext { interface UseFormActionContext {
emit: EmitType; emit: EmitType;

View File

@@ -1,5 +1,5 @@
import { isArray, isFunction, isEmpty, isObject, isString, isNil } from '/@/utils/is'; import { isArray, isFunction, isEmpty, isObject, isString, isNil } from '@/utils/is';
import { dateUtil } from '/@/utils/dateUtil'; import { dateUtil } from '@/utils/dateUtil';
import { unref } from 'vue'; import { unref } from 'vue';
import type { Ref, ComputedRef } from 'vue'; import type { Ref, ComputedRef } from 'vue';
import type { FormProps, FormSchemaInner as FormSchema } from '../types/form'; import type { FormProps, FormSchemaInner as FormSchema } from '../types/form';

View File

@@ -1,7 +1,7 @@
import type { Ref } from 'vue'; import type { Ref } from 'vue';
import { computed, unref } from 'vue'; import { computed, unref } from 'vue';
import type { FormProps, FormSchemaInner as FormSchema } from '../types/form'; import type { FormProps, FormSchemaInner as FormSchema } from '../types/form';
import { isNumber } from '/@/utils/is'; import { isNumber } from '@/utils/is';
export function useItemLabelWidth(schemaItemRef: Ref<FormSchema>, propsRef: Ref<FormProps>) { export function useItemLabelWidth(schemaItemRef: Ref<FormSchema>, propsRef: Ref<FormProps>) {
return computed(() => { return computed(() => {

View File

@@ -1,10 +1,10 @@
import type { FieldMapToTime, FormSchema } from './types/form'; import type { FieldMapToTime, FormSchema } from './types/form';
import type { CSSProperties, PropType } from 'vue'; import type { CSSProperties, PropType } from 'vue';
import type { ColEx } from './types'; import type { ColEx } from './types';
import type { TableActionType } from '/@/components/Table'; import type { TableActionType } from '@/components/Table';
import type { ButtonProps } from 'ant-design-vue/es/button/buttonTypes'; import type { ButtonProps } from 'ant-design-vue/es/button/buttonTypes';
import type { RowProps } from 'ant-design-vue/lib/grid/Row'; import type { RowProps } from 'ant-design-vue/lib/grid/Row';
import { propTypes } from '/@/utils/propTypes'; import { propTypes } from '@/utils/propTypes';
export const basicProps = { export const basicProps = {
model: { model: {

View File

@@ -1,9 +1,9 @@
import type { NamePath, RuleObject } from 'ant-design-vue/lib/form/interface'; import type { NamePath, RuleObject } from 'ant-design-vue/lib/form/interface';
import type { VNode, CSSProperties } from 'vue'; import type { VNode, CSSProperties } from 'vue';
import type { ButtonProps as AntdButtonProps } from '/@/components/Button'; import type { ButtonProps as AntdButtonProps } from '@/components/Button';
import type { FormItem } from './formItem'; import type { FormItem } from './formItem';
import type { ColEx, ComponentType } from './index'; import type { ColEx, ComponentType } from './';
import type { TableActionType } from '/@/components/Table/src/types/table'; import type { TableActionType } from '@/components/Table/src/types/table';
import type { RowProps } from 'ant-design-vue/lib/grid/Row'; import type { RowProps } from 'ant-design-vue/lib/grid/Row';
export type FieldMapToTime = [string, [string, string], (string | [string, string])?][]; export type FieldMapToTime = [string, [string, string], (string | [string, string])?][];

View File

@@ -13,91 +13,78 @@
:style="getWrapStyle" :style="getWrapStyle"
></span> ></span>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import type { PropType } from 'vue'; import type { PropType } from 'vue';
import { import { ref, watch, onMounted, nextTick, unref, computed, CSSProperties } from 'vue';
defineComponent,
ref,
watch,
onMounted,
nextTick,
unref,
computed,
CSSProperties,
} from 'vue';
import SvgIcon from './src/SvgIcon.vue'; import SvgIcon from './src/SvgIcon.vue';
import Iconify from '@purge-icons/generated'; import Iconify from '@purge-icons/generated';
import { isString } from '/@/utils/is'; import { isString } from '@/utils/is';
import { propTypes } from '/@/utils/propTypes'; import { propTypes } from '@/utils/propTypes';
const SVG_END_WITH_FLAG = '|svg'; const SVG_END_WITH_FLAG = '|svg';
export default defineComponent({
name: 'Icon', defineOptions({ name: 'Icon' });
components: { SvgIcon },
props: { const props = defineProps({
// icon name // icon name
icon: propTypes.string, icon: propTypes.string,
// icon color // icon color
color: propTypes.string, color: propTypes.string,
// icon size // icon size
size: { size: {
type: [String, Number] as PropType<string | number>, type: [String, Number] as PropType<string | number>,
default: 16, default: 16,
},
spin: propTypes.bool.def(false),
prefix: propTypes.string.def(''),
},
setup(props) {
const elRef = ref(null);
const isSvgIcon = computed(() => props.icon?.endsWith(SVG_END_WITH_FLAG));
const getSvgIcon = computed(() => props.icon.replace(SVG_END_WITH_FLAG, ''));
const getIconRef = computed(() => `${props.prefix ? props.prefix + ':' : ''}${props.icon}`);
const update = async () => {
if (unref(isSvgIcon)) return;
const el: any = unref(elRef);
if (!el) return;
await nextTick();
const icon = unref(getIconRef);
if (!icon) return;
const svg = Iconify.renderSVG(icon, {});
if (svg) {
el.textContent = '';
el.appendChild(svg);
} else {
const span = document.createElement('span');
span.className = 'iconify';
span.dataset.icon = icon;
el.textContent = '';
el.appendChild(span);
}
};
const getWrapStyle = computed((): CSSProperties => {
const { size, color } = props;
let fs = size;
if (isString(size)) {
fs = parseInt(size, 10);
}
return {
fontSize: `${fs}px`,
color: color,
display: 'inline-flex',
};
});
watch(() => props.icon, update, { flush: 'post' });
onMounted(update);
return { elRef, getWrapStyle, isSvgIcon, getSvgIcon };
}, },
spin: propTypes.bool.def(false),
prefix: propTypes.string.def(''),
}); });
const elRef = ref(null);
const isSvgIcon = computed(() => props.icon?.endsWith(SVG_END_WITH_FLAG));
const getSvgIcon = computed(() => props.icon.replace(SVG_END_WITH_FLAG, ''));
const getIconRef = computed(() => `${props.prefix ? props.prefix + ':' : ''}${props.icon}`);
const update = async () => {
if (unref(isSvgIcon)) return;
const el: any = unref(elRef);
if (!el) return;
await nextTick();
const icon = unref(getIconRef);
if (!icon) return;
const svg = Iconify.renderSVG(icon, {});
if (svg) {
el.textContent = '';
el.appendChild(svg);
} else {
const span = document.createElement('span');
span.className = 'iconify';
span.dataset.icon = icon;
el.textContent = '';
el.appendChild(span);
}
};
const getWrapStyle = computed((): CSSProperties => {
const { size, color } = props;
let fs = size;
if (isString(size)) {
fs = parseInt(size, 10);
}
return {
fontSize: `${fs}px`,
color: color,
display: 'inline-flex',
};
});
watch(() => props.icon, update, { flush: 'post' });
onMounted(update);
</script> </script>
<style lang="less"> <style lang="less">
.app-iconify { .app-iconify {

View File

@@ -1,5 +1,5 @@
<template> <template>
<a-input <Input
readonly readonly
:style="{ width }" :style="{ width }"
:placeholder="t('component.icon.placeholder')" :placeholder="t('component.icon.placeholder')"
@@ -8,7 +8,7 @@
@click="triggerPopover" @click="triggerPopover"
> >
<template #addonAfter> <template #addonAfter>
<a-popover <Popover
placement="bottomLeft" placement="bottomLeft"
trigger="click" trigger="click"
v-model="visible" v-model="visible"
@@ -16,7 +16,7 @@
> >
<template #title> <template #title>
<div class="flex justify-between"> <div class="flex justify-between">
<a-input <Input
:placeholder="t('component.icon.search')" :placeholder="t('component.icon.search')"
@change="debounceHandleSearchChange" @change="debounceHandleSearchChange"
allowClear allowClear
@@ -36,14 +36,13 @@
@click="handleClick(icon)" @click="handleClick(icon)"
:title="icon" :title="icon"
> >
<!-- <Icon :icon="icon" :prefix="prefix" /> -->
<SvgIcon v-if="isSvgMode" :name="icon" /> <SvgIcon v-if="isSvgMode" :name="icon" />
<Icon :icon="icon" v-else /> <Icon :icon="icon" v-else />
</li> </li>
</ul> </ul>
</ScrollContainer> </ScrollContainer>
<div class="flex py-2 items-center justify-center" v-if="getTotal >= pageSize"> <div class="flex py-2 items-center justify-center" v-if="getTotal >= pageSize">
<a-pagination <Pagination
showLessItems showLessItems
size="small" size="small"
:pageSize="pageSize" :pageSize="pageSize"
@@ -52,8 +51,8 @@
/> />
</div> </div>
</div> </div>
<template v-else <template v-else>
><div class="p-5"><a-empty /></div> <div class="p-5"><Empty /> </div>
</template> </template>
</template> </template>
@@ -70,30 +69,24 @@
v-else v-else
/> />
</div> </div>
</a-popover> </Popover>
</template> </template>
</a-input> </Input>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, watchEffect, watch } from 'vue'; import { ref, watchEffect, watch } from 'vue';
import { useDesign } from '/@/hooks/web/useDesign'; import { useDesign } from '@/hooks/web/useDesign';
import { ScrollContainer } from '/@/components/Container'; import { ScrollContainer } from '@/components/Container';
import { Input, Popover, Pagination, Empty } from 'ant-design-vue'; import { Input, Popover, Pagination, Empty } from 'ant-design-vue';
import Icon from '../Icon.vue'; import Icon from '../Icon.vue';
import SvgIcon from './SvgIcon.vue'; import SvgIcon from './SvgIcon.vue';
import iconsData from '../data/icons.data'; import iconsData from '../data/icons.data';
import { usePagination } from '/@/hooks/web/usePagination'; import { usePagination } from '@/hooks/web/usePagination';
import { useDebounceFn } from '@vueuse/core'; import { useDebounceFn } from '@vueuse/core';
import { useI18n } from '/@/hooks/web/useI18n'; import { useI18n } from '@/hooks/web/useI18n';
import svgIcons from 'virtual:svg-icons-names'; import svgIcons from 'virtual:svg-icons-names';
import { copyText } from '/@/utils/copyTextToClipboard'; import { copyText } from '@/utils/copyTextToClipboard';
// 没有使用别名引入是因为WebStorm当前版本还不能正确识别会报unused警告
const AInput = Input;
const APopover = Popover;
const APagination = Pagination;
const AEmpty = Empty;
function getIcons() { function getIcons() {
const prefix = iconsData.prefix; const prefix = iconsData.prefix;

View File

@@ -7,46 +7,43 @@
<use :xlink:href="symbolId" /> <use :xlink:href="symbolId" />
</svg> </svg>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import type { CSSProperties } from 'vue'; import type { CSSProperties } from 'vue';
import { defineComponent, computed } from 'vue'; import { computed } from 'vue';
import { useDesign } from '/@/hooks/web/useDesign'; import { useDesign } from '@/hooks/web/useDesign';
export default defineComponent({ defineOptions({ name: 'SvgIcon' });
name: 'SvgIcon',
props: {
prefix: {
type: String,
default: 'icon',
},
name: {
type: String,
required: true,
},
size: {
type: [Number, String],
default: 16,
},
spin: {
type: Boolean,
default: false,
},
},
setup(props) {
const { prefixCls } = useDesign('svg-icon');
const symbolId = computed(() => `#${props.prefix}-${props.name}`);
const getStyle = computed((): CSSProperties => { const props = defineProps({
const { size } = props; prefix: {
let s = `${size}`; type: String,
s = `${s.replace('px', '')}px`; default: 'icon',
return {
width: s,
height: s,
};
});
return { symbolId, prefixCls, getStyle };
}, },
name: {
type: String,
required: true,
},
size: {
type: [Number, String],
default: 16,
},
spin: {
type: Boolean,
default: false,
},
});
const { prefixCls } = useDesign('svg-icon');
const symbolId = computed(() => `#${props.prefix}-${props.name}`);
const getStyle = computed((): CSSProperties => {
const { size } = props;
let s = `${size}`;
s = `${s.replace('px', '')}px`;
return {
width: s,
height: s,
};
}); });
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>

View File

@@ -8,41 +8,39 @@
<Spin v-bind="$attrs" :tip="tip" :size="size" :spinning="loading" /> <Spin v-bind="$attrs" :tip="tip" :size="size" :spinning="loading" />
</section> </section>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { PropType, defineComponent } from 'vue'; import type { PropType } from 'vue';
import { Spin } from 'ant-design-vue'; import { Spin } from 'ant-design-vue';
import { SizeEnum } from '/@/enums/sizeEnum'; import { SizeEnum } from '@/enums/sizeEnum';
export default defineComponent({ defineOptions({ name: 'Loading' });
name: 'Loading',
components: { Spin }, defineProps({
props: { tip: {
tip: { type: String as PropType<string>,
type: String as PropType<string>, default: '',
default: '', },
}, size: {
size: { type: String as PropType<SizeEnum>,
type: String as PropType<SizeEnum>, default: SizeEnum.LARGE,
default: SizeEnum.LARGE, validator: (v: SizeEnum): boolean => {
validator: (v: SizeEnum): boolean => { return [SizeEnum.DEFAULT, SizeEnum.SMALL, SizeEnum.LARGE].includes(v);
return [SizeEnum.DEFAULT, SizeEnum.SMALL, SizeEnum.LARGE].includes(v);
},
},
absolute: {
type: Boolean as PropType<boolean>,
default: false,
},
loading: {
type: Boolean as PropType<boolean>,
default: false,
},
background: {
type: String as PropType<string>,
},
theme: {
type: String as PropType<'dark' | 'light'>,
}, },
}, },
absolute: {
type: Boolean as PropType<boolean>,
default: false,
},
loading: {
type: Boolean as PropType<boolean>,
default: false,
},
background: {
type: String as PropType<string>,
},
theme: {
type: String as PropType<'dark' | 'light'>,
},
}); });
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>

View File

@@ -1,4 +1,4 @@
import { SizeEnum } from '/@/enums/sizeEnum'; import { SizeEnum } from '@/enums/sizeEnum';
export interface LoadingProps { export interface LoadingProps {
tip: string; tip: string;

View File

@@ -1,4 +1,4 @@
import { withInstall } from '/@/utils'; import { withInstall } from '@/utils';
import markDown from './src/Markdown.vue'; import markDown from './src/Markdown.vue';
import markDownViewer from './src/MarkdownViewer.vue'; import markDownViewer from './src/MarkdownViewer.vue';

View File

@@ -1,10 +1,9 @@
<template> <template>
<div ref="wrapRef"></div> <div ref="wrapRef"></div>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import type { Ref } from 'vue'; import type { Ref } from 'vue';
import { import {
defineComponent,
ref, ref,
unref, unref,
nextTick, nextTick,
@@ -12,149 +11,147 @@
watch, watch,
onBeforeUnmount, onBeforeUnmount,
onDeactivated, onDeactivated,
useAttrs,
} from 'vue'; } from 'vue';
import Vditor from 'vditor'; import Vditor from 'vditor';
import 'vditor/dist/index.css'; import 'vditor/dist/index.css';
import { useLocale } from '/@/locales/useLocale'; import { useLocale } from '@/locales/useLocale';
import { useModalContext } from '../../Modal'; import { useModalContext } from '../../Modal';
import { useRootSetting } from '/@/hooks/setting/useRootSetting'; import { useRootSetting } from '@/hooks/setting/useRootSetting';
import { onMountedOrActivated } from '@vben/hooks'; import { onMountedOrActivated } from '@vben/hooks';
import { getTheme } from './getTheme'; import { getTheme } from './getTheme';
type Lang = 'zh_CN' | 'en_US' | 'ja_JP' | 'ko_KR' | undefined; type Lang = 'zh_CN' | 'en_US' | 'ja_JP' | 'ko_KR' | undefined;
export default defineComponent({ defineOptions({ inheritAttrs: false });
inheritAttrs: false,
props: {
height: { type: Number, default: 360 },
value: { type: String, default: '' },
},
emits: ['change', 'get', 'update:value'],
setup(props, { attrs, emit }) {
const wrapRef = ref(null);
const vditorRef = ref(null) as Ref<Vditor | null>;
const initedRef = ref(false);
const modalFn = useModalContext(); const props = defineProps({
height: { type: Number, default: 360 },
const { getLocale } = useLocale(); value: { type: String, default: '' },
const { getDarkMode } = useRootSetting();
const valueRef = ref(props.value || '');
watch(
[() => getDarkMode.value, () => initedRef.value],
([val, inited]) => {
if (!inited) {
return;
}
instance
.getVditor()
?.setTheme(getTheme(val) as any, getTheme(val, 'content'), getTheme(val, 'code'));
},
{
immediate: true,
flush: 'post',
},
);
watch(
() => props.value,
(v) => {
if (v !== valueRef.value) {
instance.getVditor()?.setValue(v);
}
valueRef.value = v;
},
);
const getCurrentLang = computed((): 'zh_CN' | 'en_US' | 'ja_JP' | 'ko_KR' => {
let lang: Lang;
switch (unref(getLocale)) {
case 'en':
lang = 'en_US';
break;
case 'ja':
lang = 'ja_JP';
break;
case 'ko':
lang = 'ko_KR';
break;
default:
lang = 'zh_CN';
}
return lang;
});
function init() {
const wrapEl = unref(wrapRef);
if (!wrapEl) return;
const bindValue = { ...attrs, ...props };
const insEditor = new Vditor(wrapEl, {
// 设置外观主题
theme: getTheme(getDarkMode.value) as any,
lang: unref(getCurrentLang),
mode: 'sv',
fullscreen: {
index: 520,
},
preview: {
theme: {
// 设置内容主题
current: getTheme(getDarkMode.value, 'content'),
},
hljs: {
// 设置代码块主题
style: getTheme(getDarkMode.value, 'code'),
},
actions: [],
},
input: (v) => {
valueRef.value = v;
emit('update:value', v);
emit('change', v);
},
after: () => {
nextTick(() => {
modalFn?.redoModalHeight?.();
insEditor.setValue(valueRef.value);
vditorRef.value = insEditor;
initedRef.value = true;
emit('get', instance);
});
},
blur: () => {
//unref(vditorRef)?.setValue(props.value);
},
...bindValue,
cache: {
enable: false,
},
});
}
const instance = {
getVditor: (): Vditor => vditorRef.value!,
};
function destroy() {
const vditorInstance = unref(vditorRef);
if (!vditorInstance) return;
try {
vditorInstance?.destroy?.();
} catch (error) {
//
}
vditorRef.value = null;
initedRef.value = false;
}
onMountedOrActivated(init);
onBeforeUnmount(destroy);
onDeactivated(destroy);
return {
wrapRef,
...instance,
};
},
}); });
const emit = defineEmits(['change', 'get', 'update:value']);
const attrs = useAttrs();
const wrapRef = ref(null);
const vditorRef = ref(null) as Ref<Vditor | null>;
const initedRef = ref(false);
const modalFn = useModalContext();
const { getLocale } = useLocale();
const { getDarkMode } = useRootSetting();
const valueRef = ref(props.value || '');
watch(
[() => getDarkMode.value, () => initedRef.value],
([val, inited]) => {
if (!inited) {
return;
}
instance
.getVditor()
?.setTheme(getTheme(val) as any, getTheme(val, 'content'), getTheme(val, 'code'));
},
{
immediate: true,
flush: 'post',
},
);
watch(
() => props.value,
(v) => {
if (v !== valueRef.value) {
instance.getVditor()?.setValue(v);
}
valueRef.value = v;
},
);
const getCurrentLang = computed((): 'zh_CN' | 'en_US' | 'ja_JP' | 'ko_KR' => {
let lang: Lang;
switch (unref(getLocale)) {
case 'en':
lang = 'en_US';
break;
case 'ja':
lang = 'ja_JP';
break;
case 'ko':
lang = 'ko_KR';
break;
default:
lang = 'zh_CN';
}
return lang;
});
function init() {
const wrapEl = unref(wrapRef);
if (!wrapEl) return;
const bindValue = { ...attrs, ...props };
const insEditor = new Vditor(wrapEl, {
// 设置外观主题
theme: getTheme(getDarkMode.value) as any,
lang: unref(getCurrentLang),
mode: 'sv',
fullscreen: {
index: 520,
},
preview: {
theme: {
// 设置内容主题
current: getTheme(getDarkMode.value, 'content'),
},
hljs: {
// 设置代码块主题
style: getTheme(getDarkMode.value, 'code'),
},
actions: [],
},
input: (v) => {
valueRef.value = v;
emit('update:value', v);
emit('change', v);
},
after: () => {
nextTick(() => {
modalFn?.redoModalHeight?.();
insEditor.setValue(valueRef.value);
vditorRef.value = insEditor;
initedRef.value = true;
emit('get', instance);
});
},
blur: () => {
//unref(vditorRef)?.setValue(props.value);
},
...bindValue,
cache: {
enable: false,
},
});
}
const instance = {
getVditor: (): Vditor => vditorRef.value!,
};
function destroy() {
const vditorInstance = unref(vditorRef);
if (!vditorInstance) return;
try {
vditorInstance?.destroy?.();
} catch (error) {
//
}
vditorRef.value = null;
initedRef.value = false;
}
onMountedOrActivated(init);
onBeforeUnmount(destroy);
onDeactivated(destroy);
</script> </script>

View File

@@ -6,7 +6,7 @@
import { onBeforeUnmount, onDeactivated, Ref, ref, unref, watch } from 'vue'; import { onBeforeUnmount, onDeactivated, Ref, ref, unref, watch } from 'vue';
import VditorPreview from 'vditor/dist/method.min'; import VditorPreview from 'vditor/dist/method.min';
import { onMountedOrActivated } from '@vben/hooks'; import { onMountedOrActivated } from '@vben/hooks';
import { useRootSetting } from '/@/hooks/setting/useRootSetting'; import { useRootSetting } from '@/hooks/setting/useRootSetting';
import { getTheme } from './getTheme'; import { getTheme } from './getTheme';
const props = defineProps({ const props = defineProps({

View File

@@ -1,7 +1,7 @@
<template> <template>
<Menu <Menu
:selectedKeys="selectedKeys" :selectedKeys="menuState.selectedKeys"
:defaultSelectedKeys="defaultSelectedKeys" :defaultSelectedKeys="menuState.defaultSelectedKeys"
:mode="mode" :mode="mode"
:openKeys="getOpenKeys" :openKeys="getOpenKeys"
:inlineIndent="inlineIndent" :inlineIndent="inlineIndent"
@@ -17,147 +17,132 @@
</template> </template>
</Menu> </Menu>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import type { MenuState } from './types'; import type { MenuState } from './types';
import { computed, defineComponent, unref, reactive, watch, toRefs, ref } from 'vue'; import { computed, unref, reactive, watch, toRefs, ref } from 'vue';
import { Menu, MenuProps } from 'ant-design-vue'; import { Menu, MenuProps } from 'ant-design-vue';
import BasicSubMenuItem from './components/BasicSubMenuItem.vue'; import BasicSubMenuItem from './components/BasicSubMenuItem.vue';
import { MenuModeEnum, MenuTypeEnum } from '/@/enums/menuEnum'; import { MenuModeEnum, MenuTypeEnum } from '@/enums/menuEnum';
import { useOpenKeys } from './useOpenKeys'; import { useOpenKeys } from './useOpenKeys';
import { RouteLocationNormalizedLoaded, useRouter } from 'vue-router'; import { RouteLocationNormalizedLoaded, useRouter } from 'vue-router';
import { isFunction } from '/@/utils/is'; import { isFunction } from '@/utils/is';
import { basicProps } from './props'; import { basicProps } from './props';
import { useMenuSetting } from '/@/hooks/setting/useMenuSetting'; import { useMenuSetting } from '@/hooks/setting/useMenuSetting';
import { REDIRECT_NAME } from '/@/router/constant'; import { REDIRECT_NAME } from '@/router/constant';
import { useDesign } from '/@/hooks/web/useDesign'; import { useDesign } from '@/hooks/web/useDesign';
import { getCurrentParentPath } from '/@/router/menus'; import { getCurrentParentPath } from '@/router/menus';
import { listenerRouteChange } from '/@/logics/mitt/routeChange'; import { listenerRouteChange } from '@/logics/mitt/routeChange';
import { getAllParentPath } from '/@/router/helper/menuHelper'; import { getAllParentPath } from '@/router/helper/menuHelper';
export default defineComponent({ defineOptions({ name: 'BasicMenu' });
name: 'BasicMenu',
components: {
Menu,
BasicSubMenuItem,
},
props: basicProps,
emits: ['menuClick'],
setup(props, { emit }) {
const isClickGo = ref(false);
const currentActiveMenu = ref('');
const menuState = reactive<MenuState>({ const props = defineProps(basicProps);
defaultSelectedKeys: [],
openKeys: [],
selectedKeys: [],
collapsedOpenKeys: [],
});
const { prefixCls } = useDesign('basic-menu'); const emit = defineEmits(['menuClick']);
const { items, mode, accordion } = toRefs(props);
const { getCollapsed, getTopMenuAlign, getSplit } = useMenuSetting(); const isClickGo = ref(false);
const currentActiveMenu = ref('');
const { currentRoute } = useRouter(); const menuState = reactive<MenuState>({
defaultSelectedKeys: [],
const { handleOpenChange, setOpenKeys, getOpenKeys } = useOpenKeys( openKeys: [],
menuState, selectedKeys: [],
items, collapsedOpenKeys: [],
mode as any,
accordion,
);
const getIsTopMenu = computed(() => {
const { type, mode } = props;
return (
(type === MenuTypeEnum.TOP_MENU && mode === MenuModeEnum.HORIZONTAL) ||
(props.isHorizontal && unref(getSplit))
);
});
const getMenuClass = computed(() => {
const align = props.isHorizontal && unref(getSplit) ? 'start' : unref(getTopMenuAlign);
return [
prefixCls,
`justify-${align}`,
{
[`${prefixCls}__second`]: !props.isHorizontal && unref(getSplit),
[`${prefixCls}__sidebar-hor`]: unref(getIsTopMenu),
},
];
});
const getInlineCollapseOptions = computed(() => {
const isInline = props.mode === MenuModeEnum.INLINE;
const inlineCollapseOptions: { inlineCollapsed?: boolean } = {};
if (isInline) {
inlineCollapseOptions.inlineCollapsed = props.mixSider ? false : unref(getCollapsed);
}
return inlineCollapseOptions;
});
listenerRouteChange((route) => {
if (route.name === REDIRECT_NAME) return;
handleMenuChange(route);
currentActiveMenu.value = route.meta?.currentActiveMenu as string;
if (unref(currentActiveMenu)) {
menuState.selectedKeys = [unref(currentActiveMenu)];
setOpenKeys(unref(currentActiveMenu));
}
});
!props.mixSider &&
watch(
() => props.items,
() => {
handleMenuChange();
},
);
const handleMenuClick: MenuProps['onClick'] = async ({ key }) => {
const { beforeClickFn } = props;
if (beforeClickFn && isFunction(beforeClickFn)) {
const flag = await beforeClickFn(key);
if (!flag) return;
}
emit('menuClick', key);
isClickGo.value = true;
menuState.selectedKeys = [key];
};
async function handleMenuChange(route?: RouteLocationNormalizedLoaded) {
if (unref(isClickGo)) {
isClickGo.value = false;
return;
}
const path =
(route || unref(currentRoute)).meta?.currentActiveMenu ||
(route || unref(currentRoute)).path;
setOpenKeys(path);
if (unref(currentActiveMenu)) return;
if (props.isHorizontal && unref(getSplit)) {
const parentPath = await getCurrentParentPath(path);
menuState.selectedKeys = [parentPath];
} else {
const parentPaths = await getAllParentPath(props.items, path);
menuState.selectedKeys = parentPaths;
}
}
return {
handleMenuClick,
getInlineCollapseOptions,
getMenuClass,
handleOpenChange,
getOpenKeys,
...toRefs(menuState),
};
},
}); });
const { prefixCls } = useDesign('basic-menu');
const { items, mode, accordion } = toRefs(props);
const { getCollapsed, getTopMenuAlign, getSplit } = useMenuSetting();
const { currentRoute } = useRouter();
const { handleOpenChange, setOpenKeys, getOpenKeys } = useOpenKeys(
menuState,
items,
mode as any,
accordion,
);
const getIsTopMenu = computed(() => {
const { type, mode } = props;
return (
(type === MenuTypeEnum.TOP_MENU && mode === MenuModeEnum.HORIZONTAL) ||
(props.isHorizontal && unref(getSplit))
);
});
const getMenuClass = computed(() => {
const align = props.isHorizontal && unref(getSplit) ? 'start' : unref(getTopMenuAlign);
return [
prefixCls,
`justify-${align}`,
{
[`${prefixCls}__second`]: !props.isHorizontal && unref(getSplit),
[`${prefixCls}__sidebar-hor`]: unref(getIsTopMenu),
},
];
});
const getInlineCollapseOptions = computed(() => {
const isInline = props.mode === MenuModeEnum.INLINE;
const inlineCollapseOptions: { inlineCollapsed?: boolean } = {};
if (isInline) {
inlineCollapseOptions.inlineCollapsed = props.mixSider ? false : unref(getCollapsed);
}
return inlineCollapseOptions;
});
listenerRouteChange((route) => {
if (route.name === REDIRECT_NAME) return;
handleMenuChange(route);
currentActiveMenu.value = route.meta?.currentActiveMenu as string;
if (unref(currentActiveMenu)) {
menuState.selectedKeys = [unref(currentActiveMenu)];
setOpenKeys(unref(currentActiveMenu));
}
});
!props.mixSider &&
watch(
() => props.items,
() => {
handleMenuChange();
},
);
const handleMenuClick: MenuProps['onClick'] = async ({ key }) => {
const { beforeClickFn } = props;
if (beforeClickFn && isFunction(beforeClickFn)) {
const flag = await beforeClickFn(key);
if (!flag) return;
}
emit('menuClick', key);
isClickGo.value = true;
menuState.selectedKeys = [key];
};
async function handleMenuChange(route?: RouteLocationNormalizedLoaded) {
if (unref(isClickGo)) {
isClickGo.value = false;
return;
}
const path =
(route || unref(currentRoute)).meta?.currentActiveMenu || (route || unref(currentRoute)).path;
setOpenKeys(path);
if (unref(currentActiveMenu)) return;
if (props.isHorizontal && unref(getSplit)) {
const parentPath = await getCurrentParentPath(path);
menuState.selectedKeys = [parentPath];
} else {
const parentPaths = await getAllParentPath(props.items, path);
menuState.selectedKeys = parentPaths;
}
}
</script> </script>
<style lang="less"> <style lang="less">
@import url('./index.less'); @import url('./index.less');

View File

@@ -1,21 +1,14 @@
<template> <template>
<MenuItem :key="item.path"> <Menu.Item :key="item.path">
<MenuItemContent v-bind="$props" :item="item" /> <MenuItemContent v-bind="$props" :item="item" />
</MenuItem> </Menu.Item>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { defineComponent } from 'vue';
import { Menu } from 'ant-design-vue'; import { Menu } from 'ant-design-vue';
import { itemProps } from '../props'; import { itemProps } from '../props';
import MenuItemContent from './MenuItemContent.vue'; import MenuItemContent from './MenuItemContent.vue';
export default defineComponent({ defineOptions({ name: 'BasicMenuItem' });
name: 'BasicMenuItem',
components: { MenuItem: Menu.Item, MenuItemContent }, defineProps(itemProps);
props: itemProps,
setup() {
return {};
},
});
</script> </script>

View File

@@ -1,6 +1,6 @@
<template> <template>
<BasicMenuItem v-if="!menuHasChildren(item) && getShowMenu" v-bind="$props" /> <BasicMenuItem v-if="!menuHasChildren(item) && getShowMenu" v-bind="$props" />
<SubMenu <Menu.SubMenu
v-if="menuHasChildren(item) && getShowMenu" v-if="menuHasChildren(item) && getShowMenu"
:class="[theme]" :class="[theme]"
:key="`submenu-${item.path}`" :key="`submenu-${item.path}`"
@@ -13,43 +13,27 @@
<template v-for="childrenItem in item.children || []" :key="childrenItem.path"> <template v-for="childrenItem in item.children || []" :key="childrenItem.path">
<BasicSubMenuItem v-bind="$props" :item="childrenItem" /> <BasicSubMenuItem v-bind="$props" :item="childrenItem" />
</template> </template>
</SubMenu> </Menu.SubMenu>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import type { Menu as MenuType } from '/@/router/types'; import type { Menu as MenuType } from '@/router/types';
import { defineComponent, computed } from 'vue'; import { computed } from 'vue';
import { Menu } from 'ant-design-vue'; import { Menu } from 'ant-design-vue';
import { useDesign } from '/@/hooks/web/useDesign';
import { itemProps } from '../props'; import { itemProps } from '../props';
import BasicMenuItem from './BasicMenuItem.vue'; import BasicMenuItem from './BasicMenuItem.vue';
import MenuItemContent from './MenuItemContent.vue'; import MenuItemContent from './MenuItemContent.vue';
export default defineComponent({ defineOptions({ name: 'BasicSubMenuItem', isSubMenu: true });
name: 'BasicSubMenuItem',
isSubMenu: true,
components: {
BasicMenuItem,
SubMenu: Menu.SubMenu,
MenuItemContent,
},
props: itemProps,
setup(props) {
const { prefixCls } = useDesign('basic-menu-item');
const getShowMenu = computed(() => !props.item.meta?.hideMenu); const props = defineProps(itemProps);
function menuHasChildren(menuTreeItem: MenuType): boolean {
return ( const getShowMenu = computed(() => !props.item.meta?.hideMenu);
!menuTreeItem.meta?.hideChildrenInMenu && function menuHasChildren(menuTreeItem: MenuType): boolean {
Reflect.has(menuTreeItem, 'children') && return (
!!menuTreeItem.children && !menuTreeItem.meta?.hideChildrenInMenu &&
menuTreeItem.children.length > 0 Reflect.has(menuTreeItem, 'children') &&
); !!menuTreeItem.children &&
} menuTreeItem.children.length > 0
return { );
prefixCls, }
menuHasChildren,
getShowMenu,
};
},
});
</script> </script>

View File

@@ -5,33 +5,21 @@
{{ getI18nName }} {{ getI18nName }}
</span> </span>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { computed, defineComponent } from 'vue'; import { computed } from 'vue';
import Icon from '@/components/Icon/Icon.vue'; import Icon from '@/components/Icon/Icon.vue';
import { useI18n } from '/@/hooks/web/useI18n'; import { useI18n } from '@/hooks/web/useI18n';
import { useDesign } from '/@/hooks/web/useDesign'; import { useDesign } from '@/hooks/web/useDesign';
import { contentProps } from '../props'; import { contentProps } from '../props';
defineOptions({ name: 'MenuItemContent' });
const props = defineProps(contentProps);
const { t } = useI18n(); const { t } = useI18n();
const { prefixCls } = useDesign('basic-menu-item-content');
export default defineComponent({ const getI18nName = computed(() => t(props.item?.name));
name: 'MenuItemContent', const getIcon = computed(() => (props.item?.img ? undefined : props.item?.icon));
components: { const getImg = computed(() => props.item?.img);
Icon,
},
props: contentProps,
setup(props) {
const { prefixCls } = useDesign('basic-menu-item-content');
const getI18nName = computed(() => t(props.item?.name));
const getIcon = computed(() => (props.item?.img ? undefined : props.item?.icon));
const getImg = computed(() => props.item?.img);
return {
prefixCls,
getI18nName,
getIcon,
getImg,
};
},
});
</script> </script>

View File

@@ -1,9 +1,9 @@
import type { Menu } from '/@/router/types'; import type { Menu } from '@/router/types';
import type { PropType } from 'vue'; import type { PropType } from 'vue';
import { MenuModeEnum, MenuTypeEnum } from '/@/enums/menuEnum'; import { MenuModeEnum, MenuTypeEnum } from '@/enums/menuEnum';
import { ThemeEnum } from '/@/enums/appEnum'; import { ThemeEnum } from '@/enums/appEnum';
import { propTypes } from '/@/utils/propTypes'; import { propTypes } from '@/utils/propTypes';
import type { Key } from './types'; import type { Key } from './types';
import type { MenuTheme } from 'ant-design-vue'; import type { MenuTheme } from 'ant-design-vue';
import type { MenuMode } from 'ant-design-vue/lib/menu/src/interface'; import type { MenuMode } from 'ant-design-vue/lib/menu/src/interface';

View File

@@ -1,11 +1,11 @@
import { MenuModeEnum } from '/@/enums/menuEnum'; import { MenuModeEnum } from '@/enums/menuEnum';
import type { Menu as MenuType } from '/@/router/types'; import type { Menu as MenuType } from '/@/router/types';
import type { MenuState, Key } from './types'; import type { MenuState, Key } from './types';
import { computed, Ref, toRaw, unref } from 'vue'; import { computed, Ref, toRaw, unref } from 'vue';
import { useTimeoutFn } from '@vben/hooks'; import { useTimeoutFn } from '@vben/hooks';
import { uniq } from 'lodash-es'; import { uniq } from 'lodash-es';
import { useMenuSetting } from '/@/hooks/setting/useMenuSetting'; import { useMenuSetting } from '@/hooks/setting/useMenuSetting';
import { getAllParentPath } from '/@/router/helper/menuHelper'; import { getAllParentPath } from '@/router/helper/menuHelper';
export function useOpenKeys( export function useOpenKeys(
menuState: MenuState, menuState: MenuState,

View File

@@ -1,4 +1,4 @@
import { withInstall } from '/@/utils'; import { withInstall } from '@/utils';
import './src/index.less'; import './src/index.less';
import basicModal from './src/BasicModal.vue'; import basicModal from './src/BasicModal.vue';

View File

@@ -48,11 +48,9 @@
</template> </template>
</Modal> </Modal>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import type { ModalProps, ModalMethods } from './typing'; import type { ModalProps, ModalMethods } from './typing';
import { import {
defineComponent,
computed, computed,
ref, ref,
watch, watch,
@@ -61,183 +59,172 @@
toRef, toRef,
getCurrentInstance, getCurrentInstance,
nextTick, nextTick,
useAttrs,
} from 'vue'; } from 'vue';
import Modal from './components/Modal'; import Modal from './components/Modal';
import ModalWrapper from './components/ModalWrapper.vue'; import ModalWrapper from './components/ModalWrapper.vue';
import ModalClose from './components/ModalClose.vue'; import ModalClose from './components/ModalClose.vue';
import ModalFooter from './components/ModalFooter.vue'; import ModalFooter from './components/ModalFooter.vue';
import ModalHeader from './components/ModalHeader.vue'; import ModalHeader from './components/ModalHeader.vue';
import { isFunction } from '/@/utils/is'; import { isFunction } from '@/utils/is';
import { deepMerge } from '/@/utils'; import { deepMerge } from '@/utils';
import { basicProps } from './props'; import { basicProps } from './props';
import { useFullScreen } from './hooks/useModalFullScreen'; import { useFullScreen } from './hooks/useModalFullScreen';
import { omit } from 'lodash-es'; import { omit } from 'lodash-es';
import { useDesign } from '/@/hooks/web/useDesign'; import { useDesign } from '@/hooks/web/useDesign';
export default defineComponent({ defineOptions({ name: 'BasicModal', inheritAttrs: false });
name: 'BasicModal',
components: { Modal, ModalWrapper, ModalClose, ModalFooter, ModalHeader },
inheritAttrs: false,
props: basicProps,
emits: ['open-change', 'height-change', 'cancel', 'ok', 'register', 'update:open'],
setup(props, { emit, attrs }) {
const openRef = ref(false);
const propsRef = ref<Partial<ModalProps> | null>(null);
const modalWrapperRef = ref<any>(null);
const { prefixCls } = useDesign('basic-modal');
// modal Bottom and top height const props = defineProps(basicProps);
const extHeightRef = ref(0);
const modalMethods: ModalMethods = {
setModalProps,
emitOpen: undefined,
redoModalHeight: () => {
nextTick(() => {
if (unref(modalWrapperRef)) {
(unref(modalWrapperRef) as any).setModalHeight();
}
});
},
};
const instance = getCurrentInstance(); const emit = defineEmits([
if (instance) { 'open-change',
emit('register', modalMethods, instance.uid); 'height-change',
} 'cancel',
'ok',
'register',
'update:open',
]);
// Custom title component: get title const attrs = useAttrs();
const getMergeProps = computed((): Recordable => { const openRef = ref(false);
return { const propsRef = ref<Partial<ModalProps> | null>(null);
...props, const modalWrapperRef = ref<any>(null);
...(unref(propsRef) as any), const { prefixCls } = useDesign('basic-modal');
};
});
const { handleFullScreen, getWrapClassName, fullScreenRef } = useFullScreen({ // modal Bottom and top height
modalWrapperRef, const extHeightRef = ref(0);
extHeightRef, const modalMethods: ModalMethods = {
wrapClassName: toRef(getMergeProps.value, 'wrapClassName'), setModalProps,
}); emitOpen: undefined,
redoModalHeight: () => {
// modal component does not need title and origin buttons nextTick(() => {
const getProps = computed((): Recordable => { if (unref(modalWrapperRef)) {
const opt = { (unref(modalWrapperRef) as any).setModalHeight();
...unref(getMergeProps),
open: unref(openRef),
okButtonProps: undefined,
cancelButtonProps: undefined,
title: undefined,
};
return {
...opt,
wrapClassName: unref(getWrapClassName),
};
});
const getBindValue = computed((): Recordable => {
const attr = {
...attrs,
...unref(getMergeProps),
open: unref(openRef),
};
attr['wrapClassName'] =
`${attr?.['wrapClassName'] || ''} ${unref(getWrapClassName)}` + 'vben-basic-modal-wrap';
if (unref(fullScreenRef)) {
return omit(attr, ['height', 'title']);
} }
return omit(attr, 'title');
}); });
const getWrapperHeight = computed(() => {
if (unref(fullScreenRef)) return undefined;
return unref(getProps).height;
});
watchEffect(() => {
openRef.value = !!props.open;
fullScreenRef.value = !!props.defaultFullscreen;
});
watch(
() => unref(openRef),
(v) => {
emit('open-change', v);
emit('update:open', v);
instance && modalMethods.emitOpen?.(v, instance.uid);
nextTick(() => {
if (props.scrollTop && v && unref(modalWrapperRef)) {
(unref(modalWrapperRef) as any).scrollTop();
}
});
},
{
immediate: false,
},
);
// 取消事件
async function handleCancel(e: Event) {
e?.stopPropagation();
// 过滤自定义关闭按钮的空白区域
if ((e.target as HTMLElement)?.classList?.contains(prefixCls + '-close--custom')) return;
if (props.closeFunc && isFunction(props.closeFunc)) {
const isClose: boolean = await props.closeFunc();
openRef.value = !isClose;
return;
}
openRef.value = false;
emit('cancel', e);
}
/**
* @description: 设置modal参数
*/
function setModalProps(props: Partial<ModalProps>): void {
// Keep the last setModalProps
propsRef.value = deepMerge(unref(propsRef) || ({} as any), props);
if (Reflect.has(props, 'open')) {
openRef.value = !!props.open;
}
if (Reflect.has(props, 'defaultFullscreen')) {
fullScreenRef.value = !!props.defaultFullscreen;
}
}
function handleOk(e: Event) {
emit('ok', e);
}
function handleHeightChange(height: string) {
emit('height-change', height);
}
function handleExtHeight(height: number) {
extHeightRef.value = height;
}
function handleTitleDbClick(e) {
if (!props.canFullscreen) return;
e.stopPropagation();
handleFullScreen(e);
}
return {
handleCancel,
getBindValue,
getProps,
handleFullScreen,
fullScreenRef,
getMergeProps,
handleOk,
openRef,
omit,
modalWrapperRef,
handleExtHeight,
handleHeightChange,
handleTitleDbClick,
getWrapperHeight,
};
}, },
};
const instance = getCurrentInstance();
if (instance) {
emit('register', modalMethods, instance.uid);
}
// Custom title component: get title
const getMergeProps = computed((): Recordable => {
return {
...props,
...(unref(propsRef) as any),
};
}); });
const { handleFullScreen, getWrapClassName, fullScreenRef } = useFullScreen({
modalWrapperRef,
extHeightRef,
wrapClassName: toRef(getMergeProps.value, 'wrapClassName'),
});
// modal component does not need title and origin buttons
const getProps = computed((): Recordable => {
const opt = {
...unref(getMergeProps),
open: unref(openRef),
okButtonProps: undefined,
cancelButtonProps: undefined,
title: undefined,
};
return {
...opt,
wrapClassName: unref(getWrapClassName),
};
});
const getBindValue = computed((): Recordable => {
const attr = {
...attrs,
...unref(getMergeProps),
open: unref(openRef),
};
attr['wrapClassName'] =
`${attr?.['wrapClassName'] || ''} ${unref(getWrapClassName)}` + 'vben-basic-modal-wrap';
if (unref(fullScreenRef)) {
return omit(attr, ['height', 'title']);
}
return omit(attr, 'title');
});
const getWrapperHeight = computed(() => {
if (unref(fullScreenRef)) return undefined;
return unref(getProps).height;
});
watchEffect(() => {
openRef.value = !!props.open;
fullScreenRef.value = !!props.defaultFullscreen;
});
watch(
() => unref(openRef),
(v) => {
emit('open-change', v);
emit('update:open', v);
instance && modalMethods.emitOpen?.(v, instance.uid);
nextTick(() => {
if (props.scrollTop && v && unref(modalWrapperRef)) {
(unref(modalWrapperRef) as any).scrollTop();
}
});
},
{
immediate: false,
},
);
// 取消事件
async function handleCancel(e: Event) {
e?.stopPropagation();
// 过滤自定义关闭按钮的空白区域
if ((e.target as HTMLElement)?.classList?.contains(prefixCls + '-close--custom')) return;
if (props.closeFunc && isFunction(props.closeFunc)) {
const isClose: boolean = await props.closeFunc();
openRef.value = !isClose;
return;
}
openRef.value = false;
emit('cancel', e);
}
/**
* @description: 设置modal参数
*/
function setModalProps(props: Partial<ModalProps>): void {
// Keep the last setModalProps
propsRef.value = deepMerge(unref(propsRef) || ({} as any), props);
if (Reflect.has(props, 'open')) {
openRef.value = !!props.open;
}
if (Reflect.has(props, 'defaultFullscreen')) {
fullScreenRef.value = !!props.defaultFullscreen;
}
}
function handleOk(e: Event) {
emit('ok', e);
}
function handleHeightChange(height: string) {
emit('height-change', height);
}
function handleExtHeight(height: number) {
extHeightRef.value = height;
}
function handleTitleDbClick(e) {
if (!props.canFullscreen) return;
e.stopPropagation();
handleFullScreen(e);
}
</script> </script>

View File

@@ -3,7 +3,7 @@ import { defineComponent, toRefs, unref } from 'vue';
import { basicProps } from '../props'; import { basicProps } from '../props';
import { useModalDragMove } from '../hooks/useModalDrag'; import { useModalDragMove } from '../hooks/useModalDrag';
import { useAttrs } from '@vben/hooks'; import { useAttrs } from '@vben/hooks';
import { extendSlots } from '/@/utils/helper/tsxHelper'; import { extendSlots } from '@/utils/helper/tsxHelper';
export default defineComponent({ export default defineComponent({
name: 'Modal', name: 'Modal',

View File

@@ -13,54 +13,44 @@
</Tooltip> </Tooltip>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { defineComponent, computed } from 'vue'; import { computed } from 'vue';
import { FullscreenExitOutlined, FullscreenOutlined, CloseOutlined } from '@ant-design/icons-vue'; import { FullscreenExitOutlined, FullscreenOutlined, CloseOutlined } from '@ant-design/icons-vue';
import { useDesign } from '/@/hooks/web/useDesign';
import { Tooltip } from 'ant-design-vue'; import { Tooltip } from 'ant-design-vue';
import { useI18n } from '/@/hooks/web/useI18n'; import { useDesign } from '@/hooks/web/useDesign';
import { useI18n } from '@/hooks/web/useI18n';
export default defineComponent({ defineOptions({ name: 'ModalClose' });
name: 'ModalClose',
components: { Tooltip, FullscreenExitOutlined, FullscreenOutlined, CloseOutlined },
props: {
canFullscreen: { type: Boolean, default: true },
fullScreen: { type: Boolean },
},
emits: ['cancel', 'fullscreen'],
setup(props, { emit }) {
const { prefixCls } = useDesign('basic-modal-close');
const { t } = useI18n();
const getClass = computed(() => { const props = defineProps({
return [ canFullscreen: { type: Boolean, default: true },
prefixCls, fullScreen: { type: Boolean },
`${prefixCls}--custom`,
{
[`${prefixCls}--can-full`]: props.canFullscreen,
},
];
});
function handleCancel(e: Event) {
emit('cancel', e);
}
function handleFullScreen(e: Event) {
e?.stopPropagation();
e?.preventDefault();
emit('fullscreen');
}
return {
t,
getClass,
prefixCls,
handleCancel,
handleFullScreen,
};
},
}); });
const emit = defineEmits(['cancel', 'fullscreen']);
const { prefixCls } = useDesign('basic-modal-close');
const { t } = useI18n();
const getClass = computed(() => {
return [
prefixCls,
`${prefixCls}--custom`,
{
[`${prefixCls}--can-full`]: props.canFullscreen,
},
];
});
function handleCancel(e: Event) {
emit('cancel', e);
}
function handleFullScreen(e: Event) {
e?.stopPropagation();
e?.preventDefault();
emit('fullscreen');
}
</script> </script>
<style lang="less"> <style lang="less">
@prefix-cls: ~'@{namespace}-basic-modal-close'; @prefix-cls: ~'@{namespace}-basic-modal-close';

View File

@@ -17,25 +17,20 @@
<slot name="appendFooter"></slot> <slot name="appendFooter"></slot>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { defineComponent } from 'vue';
import { basicProps } from '../props'; import { basicProps } from '../props';
export default defineComponent({ defineOptions({ name: 'BasicModalFooter' });
name: 'BasicModalFooter',
props: basicProps,
emits: ['ok', 'cancel'],
setup(_, { emit }) {
function handleOk(e: Event) {
emit('ok', e);
}
function handleCancel(e: Event) { defineProps(basicProps);
emit('cancel', e);
}
return { handleOk, handleCancel }; const emit = defineEmits(['ok', 'cancel']);
},
}); function handleOk(e: Event) {
emit('ok', e);
}
function handleCancel(e: Event) {
emit('cancel', e);
}
</script> </script>

View File

@@ -3,19 +3,16 @@
{{ title }} {{ title }}
</BasicTitle> </BasicTitle>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import type { PropType } from 'vue'; import type { PropType } from 'vue';
import { defineComponent } from 'vue'; import { BasicTitle } from '@/components/Basic';
import { BasicTitle } from '/@/components/Basic';
export default defineComponent({ defineOptions({ name: 'BasicModalHeader' });
name: 'BasicModalHeader',
components: { BasicTitle }, defineProps({
props: { helpMessage: {
helpMessage: { type: [String, Array] as PropType<string | string[]>,
type: [String, Array] as PropType<string | string[]>,
},
title: { type: String },
}, },
title: { type: String },
}); });
</script> </script>

View File

@@ -5,26 +5,18 @@
</div> </div>
</ScrollContainer> </ScrollContainer>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import type { CSSProperties } from 'vue'; import type { CSSProperties } from 'vue';
import { import { computed, ref, watchEffect, unref, watch, onMounted, nextTick, onUnmounted } from 'vue';
defineComponent,
computed,
ref,
watchEffect,
unref,
watch,
onMounted,
nextTick,
onUnmounted,
} from 'vue';
import { useWindowSizeFn } from '@vben/hooks'; import { useWindowSizeFn } from '@vben/hooks';
import { type AnyFunction } from '@vben/types'; import { type AnyFunction } from '@vben/types';
import { ScrollContainer } from '/@/components/Container'; import { ScrollContainer } from '@/components/Container';
import { createModalContext } from '../hooks/useModalContext'; import { createModalContext } from '../hooks/useModalContext';
import { useMutationObserver } from '@vueuse/core'; import { useMutationObserver } from '@vueuse/core';
const props = { defineOptions({ name: 'ModalWrapper', inheritAttrs: false });
const props = defineProps({
loading: { type: Boolean }, loading: { type: Boolean },
useWrapper: { type: Boolean, default: true }, useWrapper: { type: Boolean, default: true },
modalHeaderHeight: { type: Number, default: 57 }, modalHeaderHeight: { type: Number, default: 57 },
@@ -35,136 +27,128 @@
open: { type: Boolean }, open: { type: Boolean },
fullScreen: { type: Boolean }, fullScreen: { type: Boolean },
loadingTip: { type: String }, loadingTip: { type: String },
};
export default defineComponent({
name: 'ModalWrapper',
components: { ScrollContainer },
inheritAttrs: false,
props,
emits: ['height-change', 'ext-height'],
setup(props, { emit }) {
const wrapperRef = ref(null);
const spinRef = ref(null);
const realHeightRef = ref(0);
const minRealHeightRef = ref(0);
const realHeight = ref(0);
let stopElResizeFn: AnyFunction = () => {};
useWindowSizeFn(setModalHeight.bind(null));
useMutationObserver(
spinRef,
() => {
setModalHeight();
},
{
attributes: true,
subtree: true,
},
);
createModalContext({
redoModalHeight: setModalHeight,
});
const spinStyle = computed((): CSSProperties => {
return {
minHeight: `${props.minHeight}px`,
[props.fullScreen ? 'height' : 'maxHeight']: `${unref(realHeightRef)}px`,
};
});
watchEffect(() => {
props.useWrapper && setModalHeight();
});
watch(
() => props.fullScreen,
(v) => {
setModalHeight();
if (!v) {
realHeightRef.value = minRealHeightRef.value;
} else {
minRealHeightRef.value = realHeightRef.value;
}
},
);
onMounted(() => {
const { modalHeaderHeight, modalFooterHeight } = props;
emit('ext-height', modalHeaderHeight + modalFooterHeight);
});
onUnmounted(() => {
stopElResizeFn && stopElResizeFn();
});
async function scrollTop() {
nextTick(() => {
const wrapperRefDom = unref(wrapperRef);
if (!wrapperRefDom) return;
(wrapperRefDom as any)?.scrollTo?.(0);
});
}
async function setModalHeight() {
// 解决在弹窗关闭的时候监听还存在,导致再次打开弹窗没有高度
// 加上这个,就必须在使用的时候传递父级的open
if (!props.open) return;
const wrapperRefDom = unref(wrapperRef);
if (!wrapperRefDom) return;
const bodyDom = (wrapperRefDom as any).$el.parentElement;
if (!bodyDom) return;
bodyDom.style.padding = '0';
await nextTick();
try {
const modalDom = bodyDom.parentElement && bodyDom.parentElement.parentElement;
if (!modalDom) return;
const modalRect = getComputedStyle(modalDom as Element).top;
const modalTop = Number.parseInt(modalRect);
let maxHeight =
window.innerHeight -
modalTop * 2 +
(props.footerOffset! || 0) -
props.modalFooterHeight -
props.modalHeaderHeight;
// 距离顶部过进会出现滚动条
if (modalTop < 40) {
maxHeight -= 26;
}
await nextTick();
const spinEl: any = unref(spinRef);
if (!spinEl) return;
await nextTick();
// if (!realHeight) {
realHeight.value = spinEl.scrollHeight;
// }
if (props.fullScreen) {
realHeightRef.value =
window.innerHeight - props.modalFooterHeight - props.modalHeaderHeight - 28;
} else {
realHeightRef.value = props.height
? props.height
: realHeight.value > maxHeight
? maxHeight
: realHeight.value;
}
emit('height-change', unref(realHeightRef));
} catch (error) {
console.log(error);
}
}
return { wrapperRef, spinRef, spinStyle, scrollTop, setModalHeight, realHeight };
},
}); });
const emit = defineEmits(['height-change', 'ext-height']);
const wrapperRef = ref(null);
const spinRef = ref(null);
const realHeightRef = ref(0);
const minRealHeightRef = ref(0);
const realHeight = ref(0);
let stopElResizeFn: AnyFunction = () => {};
useWindowSizeFn(setModalHeight.bind(null));
useMutationObserver(
spinRef,
() => {
setModalHeight();
},
{
attributes: true,
subtree: true,
},
);
createModalContext({
redoModalHeight: setModalHeight,
});
const spinStyle = computed((): CSSProperties => {
return {
minHeight: `${props.minHeight}px`,
[props.fullScreen ? 'height' : 'maxHeight']: `${unref(realHeightRef)}px`,
};
});
watchEffect(() => {
props.useWrapper && setModalHeight();
});
watch(
() => props.fullScreen,
(v) => {
setModalHeight();
if (!v) {
realHeightRef.value = minRealHeightRef.value;
} else {
minRealHeightRef.value = realHeightRef.value;
}
},
);
onMounted(() => {
const { modalHeaderHeight, modalFooterHeight } = props;
emit('ext-height', modalHeaderHeight + modalFooterHeight);
});
onUnmounted(() => {
stopElResizeFn && stopElResizeFn();
});
async function scrollTop() {
nextTick(() => {
const wrapperRefDom = unref(wrapperRef);
if (!wrapperRefDom) return;
(wrapperRefDom as any)?.scrollTo?.(0);
});
}
async function setModalHeight() {
// 解决在弹窗关闭的时候监听还存在,导致再次打开弹窗没有高度
// 加上这个,就必须在使用的时候传递父级的open
if (!props.open) return;
const wrapperRefDom = unref(wrapperRef);
if (!wrapperRefDom) return;
const bodyDom = (wrapperRefDom as any).$el.parentElement;
if (!bodyDom) return;
bodyDom.style.padding = '0';
await nextTick();
try {
const modalDom = bodyDom.parentElement && bodyDom.parentElement.parentElement;
if (!modalDom) return;
const modalRect = getComputedStyle(modalDom as Element).top;
const modalTop = Number.parseInt(modalRect);
let maxHeight =
window.innerHeight -
modalTop * 2 +
(props.footerOffset! || 0) -
props.modalFooterHeight -
props.modalHeaderHeight;
// 距离顶部过进会出现滚动条
if (modalTop < 40) {
maxHeight -= 26;
}
await nextTick();
const spinEl: any = unref(spinRef);
if (!spinEl) return;
await nextTick();
// if (!realHeight) {
realHeight.value = spinEl.scrollHeight;
// }
if (props.fullScreen) {
realHeightRef.value =
window.innerHeight - props.modalFooterHeight - props.modalHeaderHeight - 28;
} else {
realHeightRef.value = props.height
? props.height
: realHeight.value > maxHeight
? maxHeight
: realHeight.value;
}
emit('height-change', unref(realHeightRef));
} catch (error) {
console.log(error);
}
}
defineExpose({ scrollTop });
</script> </script>

View File

@@ -16,11 +16,11 @@ import {
toRaw, toRaw,
computed, computed,
} from 'vue'; } from 'vue';
import { isProdMode } from '/@/utils/env'; import { isProdMode } from '@/utils/env';
import { isFunction } from '/@/utils/is'; import { isFunction } from '@/utils/is';
import { isEqual } from 'lodash-es'; import { isEqual } from 'lodash-es';
import { tryOnUnmounted } from '@vueuse/core'; import { tryOnUnmounted } from '@vueuse/core';
import { error } from '/@/utils/log'; import { error } from '@/utils/log';
const dataTransfer = reactive<any>({}); const dataTransfer = reactive<any>({});

Some files were not shown because too many files have changed in this diff Show More