初始代码
This commit is contained in:
15
src/components/Application/index.ts
Normal file
15
src/components/Application/index.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { withInstall } from '@/utils';
|
||||
|
||||
import appLogo from './src/AppLogo.vue';
|
||||
import appProvider from './src/AppProvider.vue';
|
||||
import appSearch from './src/search/AppSearch.vue';
|
||||
import appLocalePicker from './src/AppLocalePicker.vue';
|
||||
import appDarkModeToggle from './src/AppDarkModeToggle.vue';
|
||||
|
||||
export { useAppProviderContext } from './src/useAppContext';
|
||||
|
||||
export const AppLogo = withInstall(appLogo);
|
||||
export const AppProvider = withInstall(appProvider);
|
||||
export const AppSearch = withInstall(appSearch);
|
||||
export const AppLocalePicker = withInstall(appLocalePicker);
|
||||
export const AppDarkModeToggle = withInstall(appDarkModeToggle);
|
||||
80
src/components/Application/src/AppDarkModeToggle.vue
Normal file
80
src/components/Application/src/AppDarkModeToggle.vue
Normal file
@@ -0,0 +1,80 @@
|
||||
<template>
|
||||
<div v-if="getShowDarkModeToggle" :class="getClass" @click="toggleDarkMode">
|
||||
<div :class="`${prefixCls}-inner`"></div>
|
||||
<i class="icon-ym icon-ym-light"></i>
|
||||
<i class="icon-ym icon-ym-dark"></i>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { computed, unref } from 'vue';
|
||||
import { useDesign } from '@/hooks/web/useDesign';
|
||||
import { useRootSetting } from '@/hooks/setting/useRootSetting';
|
||||
import { updateHeaderBgColor, updateSidebarBgColor } from '@/logics/theme/updateBackground';
|
||||
import { updateDarkTheme } from '@/logics/theme/dark';
|
||||
import { ThemeEnum } from '@/enums/appEnum';
|
||||
|
||||
const { prefixCls } = useDesign('dark-switch');
|
||||
const { getDarkMode, setDarkMode, getShowDarkModeToggle } = useRootSetting();
|
||||
|
||||
const isDark = computed(() => getDarkMode.value === ThemeEnum.DARK);
|
||||
|
||||
const getClass = computed(() => [
|
||||
prefixCls,
|
||||
{
|
||||
[`${prefixCls}--dark`]: unref(isDark),
|
||||
},
|
||||
]);
|
||||
|
||||
function toggleDarkMode() {
|
||||
const darkMode = getDarkMode.value === ThemeEnum.DARK ? ThemeEnum.LIGHT : ThemeEnum.DARK;
|
||||
setDarkMode(darkMode);
|
||||
updateDarkTheme(darkMode);
|
||||
updateHeaderBgColor();
|
||||
updateSidebarBgColor();
|
||||
}
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
@prefix-cls: ~'@{namespace}-dark-switch';
|
||||
|
||||
html[data-theme='dark'] {
|
||||
.@{prefix-cls} {
|
||||
background-color: #151515;
|
||||
border: 1px solid rgb(196 188 188);
|
||||
}
|
||||
}
|
||||
|
||||
.@{prefix-cls} {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 56px;
|
||||
height: 28px;
|
||||
padding: 0 6px;
|
||||
cursor: pointer;
|
||||
background-color: @primary-color;
|
||||
border-radius: 30px;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
&-inner {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
background-color: #fff;
|
||||
border-radius: 50%;
|
||||
transition: transform 0.5s, background-color 0.5s;
|
||||
will-change: transform;
|
||||
}
|
||||
.icon-ym {
|
||||
font-size: 16px;
|
||||
line-height: 28px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&--dark {
|
||||
.@{prefix-cls}-inner {
|
||||
transform: translateX(calc(100% + 6px));
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
81
src/components/Application/src/AppLocalePicker.vue
Normal file
81
src/components/Application/src/AppLocalePicker.vue
Normal file
@@ -0,0 +1,81 @@
|
||||
<template>
|
||||
<Dropdown
|
||||
placement="bottom"
|
||||
:trigger="['click']"
|
||||
:dropMenuList="localeList"
|
||||
:selectedKeys="selectedKeys"
|
||||
@menu-event="handleMenuEvent"
|
||||
overlayClassName="app-locale-picker-overlay">
|
||||
<span class="cursor-pointer flex items-center">
|
||||
<i class="icon-ym icon-ym-header-lang"></i>
|
||||
<span v-if="showText" class="ml-1">{{ getLocaleText }}</span>
|
||||
</span>
|
||||
</Dropdown>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import type { LocaleType } from '#/config';
|
||||
import type { DropMenu } from '@/components/Dropdown';
|
||||
import { ref, watchEffect, unref, computed, onMounted } from 'vue';
|
||||
import { Dropdown } from '@/components/Dropdown';
|
||||
import { useLocale } from '@/locales/useLocale';
|
||||
import { updateLanguage } from '@/api/permission/userSetting';
|
||||
import { useMessage } from '@/hooks/web/useMessage';
|
||||
import { useBaseStore } from '@/store/modules/base';
|
||||
|
||||
const props = defineProps({
|
||||
/**
|
||||
* Whether to display text
|
||||
*/
|
||||
showText: { type: Boolean, default: true },
|
||||
/**
|
||||
* Whether to refresh the interface when changing
|
||||
*/
|
||||
reload: { type: Boolean, default: true },
|
||||
});
|
||||
|
||||
const selectedKeys = ref<string[]>([]);
|
||||
const localeList = ref<DropMenu[]>([]);
|
||||
|
||||
const { changeLocale, getLocale } = useLocale();
|
||||
const { createMessage } = useMessage();
|
||||
const baseStore = useBaseStore();
|
||||
|
||||
const getLocaleText = computed(() => {
|
||||
const key = selectedKeys.value[0];
|
||||
if (!key) {
|
||||
return '';
|
||||
}
|
||||
return localeList.value.find(item => item.event === key)?.text;
|
||||
});
|
||||
|
||||
watchEffect(() => {
|
||||
selectedKeys.value = [unref(getLocale)];
|
||||
});
|
||||
|
||||
async function toggleLocale(lang: LocaleType | string) {
|
||||
await changeLocale(lang as LocaleType);
|
||||
selectedKeys.value = [lang as string];
|
||||
props.reload && location.reload();
|
||||
}
|
||||
function handleMenuEvent(menu: DropMenu) {
|
||||
if (unref(getLocale) === menu.event) return;
|
||||
updateLanguage({ language: menu.event }).then(() => {
|
||||
let msg = '切换成功';
|
||||
if (menu.event === 'en_US') msg = 'Switch Language Success';
|
||||
if (menu.event === 'zh_TW') msg = '切換成功';
|
||||
createMessage.success(msg);
|
||||
setTimeout(() => toggleLocale(menu.event as string), 500);
|
||||
});
|
||||
}
|
||||
async function getLocaleList() {
|
||||
const list = (await baseStore.getDictionaryData('Language')) as any[];
|
||||
localeList.value = list.map((item: any) => ({
|
||||
event: item.enCode.replace('-', '_'),
|
||||
text: item.fullName,
|
||||
}));
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getLocaleList();
|
||||
});
|
||||
</script>
|
||||
101
src/components/Application/src/AppLogo.vue
Normal file
101
src/components/Application/src/AppLogo.vue
Normal file
@@ -0,0 +1,101 @@
|
||||
<template>
|
||||
<div class="anticon" :class="getAppLogoClass" @click="goHome">
|
||||
<template v-if="showTitle">
|
||||
<a-image
|
||||
class="login-logo"
|
||||
:src="apiUrl + getSysConfig.navigationIcon"
|
||||
:fallback="logoImg"
|
||||
:preview="false"
|
||||
v-if="getSysConfig && getSysConfig.navigationIcon" />
|
||||
<img class="login-logo" :src="logoImg" v-else />
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-image
|
||||
class="login-logo"
|
||||
:src="apiUrl + getSysConfig.workLogoIcon"
|
||||
:fallback="yunzhupaasImg"
|
||||
:preview="false"
|
||||
v-if="getSysConfig && getSysConfig.workLogoIcon" />
|
||||
<img :src="yunzhupaasImg" v-else />
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { computed, unref, ref } from 'vue';
|
||||
import { Image as AImage } from 'ant-design-vue';
|
||||
import { useGo } from '@/hooks/web/usePage';
|
||||
import { useMenuSetting } from '@/hooks/setting/useMenuSetting';
|
||||
import { useDesign } from '@/hooks/web/useDesign';
|
||||
import { PageEnum } from '@/enums/pageEnum';
|
||||
import { useAppStore } from '@/store/modules/app';
|
||||
import { useGlobSetting } from '@/hooks/setting';
|
||||
import logoImg from '@/assets/images/yunzhu.png';
|
||||
import yunzhupaasImg from '@/assets/images/yunzhupaas.png';
|
||||
|
||||
const props = defineProps({
|
||||
/**
|
||||
* The theme of the current parent component
|
||||
*/
|
||||
theme: { type: String, validator: (v: string) => ['light', 'dark'].includes(v) },
|
||||
/**
|
||||
* Whether to show title
|
||||
*/
|
||||
showTitle: { type: Boolean, default: true },
|
||||
/**
|
||||
* The title is also displayed when the menu is collapsed
|
||||
*/
|
||||
alwaysShowTitle: { type: Boolean },
|
||||
});
|
||||
|
||||
const { prefixCls } = useDesign('app-logo');
|
||||
const { getCollapsedShowTitle } = useMenuSetting();
|
||||
const appStore = useAppStore();
|
||||
const globSetting = useGlobSetting();
|
||||
const apiUrl = ref(globSetting.apiUrl);
|
||||
const go = useGo();
|
||||
|
||||
const getAppLogoClass = computed(() => [prefixCls, props.theme, { 'collapsed-show-title': unref(getCollapsedShowTitle) }]);
|
||||
const getSysConfig = computed(() => appStore.getSysConfigInfo);
|
||||
|
||||
function goHome() {
|
||||
go(PageEnum.BASE_HOME);
|
||||
}
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
@prefix-cls: ~'@{namespace}-app-logo';
|
||||
|
||||
.@{prefix-cls} {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&.collapsed-show-title {
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
&.light &__title {
|
||||
color: @primary-color;
|
||||
}
|
||||
|
||||
&.dark &__title {
|
||||
color: @white;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
transition: all 0.5s;
|
||||
line-height: normal;
|
||||
}
|
||||
:deep(.ant-image),
|
||||
.login-logo {
|
||||
width: auto;
|
||||
height: 100%;
|
||||
.login-logo {
|
||||
width: auto;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
77
src/components/Application/src/AppProvider.vue
Normal file
77
src/components/Application/src/AppProvider.vue
Normal file
@@ -0,0 +1,77 @@
|
||||
<script lang="ts">
|
||||
import { defineComponent, toRefs, ref, unref } from 'vue';
|
||||
import { createAppProviderContext } from './useAppContext';
|
||||
import { createBreakpointListen } from '@/hooks/event/useBreakpoint';
|
||||
import { prefixCls } from '@/settings/designSetting';
|
||||
import { useAppStore } from '@/store/modules/app';
|
||||
import { MenuModeEnum, MenuTypeEnum } from '@/enums/menuEnum';
|
||||
|
||||
const props = {
|
||||
/**
|
||||
* class style prefix
|
||||
*/
|
||||
prefixCls: { type: String, default: prefixCls },
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
name: 'AppProvider',
|
||||
inheritAttrs: false,
|
||||
props,
|
||||
setup(props, { slots }) {
|
||||
const isMobile = ref(false);
|
||||
const isSetState = ref(false);
|
||||
|
||||
const appStore = useAppStore();
|
||||
|
||||
// Monitor screen breakpoint information changes
|
||||
createBreakpointListen(({ screenMap, sizeEnum, width }) => {
|
||||
const lgWidth = screenMap.get(sizeEnum.LG);
|
||||
if (lgWidth) {
|
||||
isMobile.value = width.value - 1 < lgWidth;
|
||||
}
|
||||
handleRestoreState();
|
||||
});
|
||||
|
||||
const { prefixCls } = toRefs(props);
|
||||
|
||||
// Inject variables into the global
|
||||
createAppProviderContext({ prefixCls, isMobile });
|
||||
|
||||
/**
|
||||
* Used to maintain the state before the window changes
|
||||
*/
|
||||
function handleRestoreState() {
|
||||
if (unref(isMobile)) {
|
||||
if (!unref(isSetState)) {
|
||||
isSetState.value = true;
|
||||
const {
|
||||
menuSetting: { type: menuType, mode: menuMode, collapsed: menuCollapsed, split: menuSplit },
|
||||
} = appStore.getProjectConfig;
|
||||
appStore.setProjectConfig({
|
||||
menuSetting: {
|
||||
type: MenuTypeEnum.SIDEBAR,
|
||||
mode: MenuModeEnum.INLINE,
|
||||
split: false,
|
||||
},
|
||||
});
|
||||
appStore.setBeforeMiniInfo({ menuMode, menuCollapsed, menuType, menuSplit });
|
||||
}
|
||||
} else {
|
||||
if (unref(isSetState)) {
|
||||
isSetState.value = false;
|
||||
const { menuMode, menuCollapsed, menuType, menuSplit } = appStore.getBeforeMiniInfo;
|
||||
appStore.setProjectConfig({
|
||||
menuSetting: {
|
||||
type: menuType,
|
||||
mode: menuMode,
|
||||
collapsed: menuCollapsed,
|
||||
split: menuSplit,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return () => slots.default?.();
|
||||
},
|
||||
});
|
||||
</script>
|
||||
22
src/components/Application/src/search/AppSearch.vue
Normal file
22
src/components/Application/src/search/AppSearch.vue
Normal file
@@ -0,0 +1,22 @@
|
||||
<template>
|
||||
<Tooltip :title="t('common.searchText')" :mouseEnterDelay="0.5">
|
||||
<div @click="changeModal(true)">
|
||||
<i class="icon-ym icon-ym-header-search"></i>
|
||||
<AppSearchModal @Close="changeModal(false)" :visible="showModal" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { Tooltip } from 'ant-design-vue';
|
||||
import AppSearchModal from './AppSearchModal.vue';
|
||||
import { useI18n } from '@/hooks/web/useI18n';
|
||||
|
||||
defineOptions({ name: 'AppSearch' });
|
||||
const showModal = ref(false);
|
||||
const { t } = useI18n();
|
||||
|
||||
function changeModal(show: boolean) {
|
||||
showModal.value = show;
|
||||
}
|
||||
</script>
|
||||
60
src/components/Application/src/search/AppSearchFooter.vue
Normal file
60
src/components/Application/src/search/AppSearchFooter.vue
Normal file
@@ -0,0 +1,60 @@
|
||||
<template>
|
||||
<div :class="`${prefixCls}`">
|
||||
<AppSearchKeyItem :class="`${prefixCls}-item`" icon="ant-design:enter-outlined" />
|
||||
<span>{{ t('component.app.toSearch') }}</span>
|
||||
<AppSearchKeyItem :class="`${prefixCls}-item`" icon="ion:arrow-up-outline" />
|
||||
<AppSearchKeyItem :class="`${prefixCls}-item`" icon="ion:arrow-down-outline" />
|
||||
<span>{{ t('component.app.toNavigate') }}</span>
|
||||
<AppSearchKeyItem :class="`${prefixCls}-item`" icon="mdi:keyboard-esc" />
|
||||
<span>{{ t('common.closeText') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import AppSearchKeyItem from './AppSearchKeyItem.vue';
|
||||
import { useDesign } from '@/hooks/web/useDesign';
|
||||
import { useI18n } from '@/hooks/web/useI18n';
|
||||
const { prefixCls } = useDesign('app-search-footer');
|
||||
const { t } = useI18n();
|
||||
</script>
|
||||
<style lang="less">
|
||||
@prefix-cls: ~'@{namespace}-app-search-footer';
|
||||
.@{prefix-cls} {
|
||||
background-color: @component-background;
|
||||
}
|
||||
</style>
|
||||
<style lang="less" scoped>
|
||||
@prefix-cls: ~'@{namespace}-app-search-footer';
|
||||
|
||||
.@{prefix-cls} {
|
||||
position: relative;
|
||||
display: flex;
|
||||
height: 44px;
|
||||
padding: 0 16px;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
border-top: 1px solid @border-color-base;
|
||||
border-radius: 0 0 16px 16px;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
|
||||
&-item {
|
||||
display: flex;
|
||||
width: 20px;
|
||||
height: 18px;
|
||||
padding-bottom: 2px;
|
||||
margin-right: 0.4em;
|
||||
background-color: linear-gradient(-225deg, #d5dbe4, #f8f8f8);
|
||||
border-radius: 2px;
|
||||
box-shadow: inset 0 -2px 0 0 #cdcde6, inset 0 0 1px 1px #fff, 0 1px 2px 1px rgb(30 35 90 / 40%);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&:nth-child(2),
|
||||
&:nth-child(3),
|
||||
&:nth-child(6) {
|
||||
margin-left: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
11
src/components/Application/src/search/AppSearchKeyItem.vue
Normal file
11
src/components/Application/src/search/AppSearchKeyItem.vue
Normal file
@@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<span :class="$attrs.class">
|
||||
<Icon :icon="icon" />
|
||||
</span>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { Icon } from '@/components/Icon';
|
||||
defineProps({
|
||||
icon: String,
|
||||
});
|
||||
</script>
|
||||
269
src/components/Application/src/search/AppSearchModal.vue
Normal file
269
src/components/Application/src/search/AppSearchModal.vue
Normal file
@@ -0,0 +1,269 @@
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<transition name="zoom-fade" mode="out-in">
|
||||
<div :class="getClass" @click.stop v-if="visible">
|
||||
<div :class="`${prefixCls}-content`" v-click-outside="handleClose">
|
||||
<div :class="`${prefixCls}-input__wrapper`">
|
||||
<a-input :class="`${prefixCls}-input`" :placeholder="t('common.searchText')" ref="inputRef" allow-clear @change="handleSearch">
|
||||
<template #prefix>
|
||||
<i class="icon-ym icon-ym-header-search"></i>
|
||||
</template>
|
||||
</a-input>
|
||||
<span :class="`${prefixCls}-cancel`" @click="handleClose">
|
||||
{{ t('common.cancelText') }}
|
||||
</span>
|
||||
</div>
|
||||
<div :class="`${prefixCls}-not-data`" v-show="getIsNotData">
|
||||
{{ t('component.app.searchNotData') }}
|
||||
</div>
|
||||
<ul :class="`${prefixCls}-list`" v-show="!getIsNotData" ref="scrollWrap">
|
||||
<li
|
||||
:ref="setRefs(index)"
|
||||
v-for="(item, index) in searchResult"
|
||||
:key="item.path"
|
||||
:data-index="index"
|
||||
@mouseenter="handleMouseenter"
|
||||
@click="handleEnter"
|
||||
:class="[
|
||||
`${prefixCls}-list__item`,
|
||||
{
|
||||
[`${prefixCls}-list__item--active`]: activeIndex === index,
|
||||
},
|
||||
]">
|
||||
<div :class="`${prefixCls}-list__item-icon`">
|
||||
<i :class="item.icon"></i>
|
||||
</div>
|
||||
<div :class="`${prefixCls}-list__item-text`">
|
||||
{{ item.name }}
|
||||
</div>
|
||||
<div :class="`${prefixCls}-list__item-enter`">
|
||||
<Icon icon="ant-design:enter-outlined" :size="20" />
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<AppSearchFooter />
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, unref, ref, watch, nextTick } from 'vue';
|
||||
import AppSearchFooter from './AppSearchFooter.vue';
|
||||
import Icon from '@/components/Icon';
|
||||
// @ts-ignore
|
||||
import vClickOutside from '@/directives/clickOutside';
|
||||
import { useDesign } from '@/hooks/web/useDesign';
|
||||
import { useRefs } from '@/hooks/core/useRefs';
|
||||
import { useMenuSearch } from './useMenuSearch';
|
||||
import { useI18n } from '@/hooks/web/useI18n';
|
||||
import { useAppInject } from '@/hooks/web/useAppInject';
|
||||
|
||||
const props = defineProps({
|
||||
visible: { type: Boolean },
|
||||
});
|
||||
|
||||
const emit = defineEmits(['close']);
|
||||
|
||||
const scrollWrap = ref(null);
|
||||
const inputRef = ref<Nullable<HTMLElement>>(null);
|
||||
|
||||
const { t } = useI18n();
|
||||
const { prefixCls } = useDesign('app-search-modal');
|
||||
const [refs, setRefs] = useRefs();
|
||||
const { getIsMobile } = useAppInject();
|
||||
|
||||
const { handleSearch, searchResult, keyword, activeIndex, handleEnter, handleMouseenter } = useMenuSearch(refs, scrollWrap, emit);
|
||||
|
||||
const getIsNotData = computed(() => !keyword || unref(searchResult).length === 0);
|
||||
|
||||
const getClass = computed(() => {
|
||||
return [
|
||||
prefixCls,
|
||||
{
|
||||
[`${prefixCls}--mobile`]: unref(getIsMobile),
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(visible: boolean) => {
|
||||
visible &&
|
||||
nextTick(() => {
|
||||
unref(inputRef)?.focus();
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
function handleClose() {
|
||||
searchResult.value = [];
|
||||
emit('close');
|
||||
}
|
||||
</script>
|
||||
<style lang="less">
|
||||
@prefix-cls: ~'@{namespace}-app-search-modal';
|
||||
.@{prefix-cls} {
|
||||
&-content {
|
||||
background-color: @component-background;
|
||||
}
|
||||
&-list {
|
||||
&__item {
|
||||
background-color: @component-background;
|
||||
&--active {
|
||||
background-color: @primary-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style lang="less" scoped>
|
||||
@prefix-cls: ~'@{namespace}-app-search-modal';
|
||||
@footer-prefix-cls: ~'@{namespace}-app-search-footer';
|
||||
.@{prefix-cls} {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 8000;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding-top: 50px;
|
||||
background-color: rgb(0 0 0 / 25%);
|
||||
justify-content: center;
|
||||
|
||||
&--mobile {
|
||||
padding: 0;
|
||||
|
||||
> div {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.@{prefix-cls}-input {
|
||||
width: calc(100% - 38px);
|
||||
}
|
||||
|
||||
.@{prefix-cls}-cancel {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.@{prefix-cls}-content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.@{footer-prefix-cls} {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.@{prefix-cls}-list {
|
||||
height: calc(100% - 80px);
|
||||
max-height: unset;
|
||||
|
||||
&__item {
|
||||
&-enter {
|
||||
opacity: 0% !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-content {
|
||||
position: relative;
|
||||
width: 632px;
|
||||
margin: 0 auto auto;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 25px 50px -12px rgb(0 0 0 / 25%);
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&-input__wrapper {
|
||||
display: flex;
|
||||
padding: 14px 14px 0;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&-input {
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
font-size: 1em;
|
||||
color: #1c1e21;
|
||||
border-radius: 6px;
|
||||
|
||||
.icon-ym,
|
||||
span[role='img'] {
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
&-cancel {
|
||||
display: none;
|
||||
font-size: 1em;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
&-not-data {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
font-size: 0.9;
|
||||
color: rgb(150 159 175);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&-list {
|
||||
max-height: 472px;
|
||||
padding: 0 14px;
|
||||
padding-bottom: 20px;
|
||||
margin: 0 auto;
|
||||
margin-top: 14px;
|
||||
overflow: auto;
|
||||
|
||||
&__item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 56px;
|
||||
padding-bottom: 4px;
|
||||
padding-left: 14px;
|
||||
margin-top: 8px;
|
||||
font-size: 14px;
|
||||
color: @text-color-base;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 1px 3px 0 #d4d9e1;
|
||||
align-items: center;
|
||||
|
||||
> div:first-child,
|
||||
> div:last-child {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&--active {
|
||||
color: #fff;
|
||||
.@{prefix-cls}-list__item-enter {
|
||||
opacity: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&-icon {
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
&-text {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&-enter {
|
||||
width: 30px;
|
||||
opacity: 0%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
183
src/components/Application/src/search/useMenuSearch.ts
Normal file
183
src/components/Application/src/search/useMenuSearch.ts
Normal file
@@ -0,0 +1,183 @@
|
||||
import type { Menu } from '@/router/types';
|
||||
import { ref, onBeforeMount, unref, Ref, nextTick } from 'vue';
|
||||
import { getMenus } from '@/router/menus';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import { filter, forEach } from '@/utils/helper/treeHelper';
|
||||
import { useGo } from '@/hooks/web/usePage';
|
||||
import { useScrollTo } from '@/hooks/event/useScrollTo';
|
||||
import { onKeyStroke, useDebounceFn } from '@vueuse/core';
|
||||
import { useI18n } from '@/hooks/web/useI18n';
|
||||
|
||||
export interface SearchResult {
|
||||
name: string;
|
||||
path: string;
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
// Translate special characters
|
||||
function transform(c: string) {
|
||||
const code: string[] = ['$', '(', ')', '*', '+', '.', '[', ']', '?', '\\', '^', '{', '}', '|'];
|
||||
return code.includes(c) ? `\\${c}` : c;
|
||||
}
|
||||
|
||||
function createSearchReg(key: string) {
|
||||
const keys = [...key].map(item => transform(item));
|
||||
const str = ['', ...keys, ''].join('.*');
|
||||
return new RegExp(str);
|
||||
}
|
||||
|
||||
export function useMenuSearch(refs: Ref<HTMLElement[]>, scrollWrap: Ref<ElRef>, emit: EmitType) {
|
||||
const searchResult = ref<SearchResult[]>([]);
|
||||
const keyword = ref('');
|
||||
const activeIndex = ref(-1);
|
||||
|
||||
let menuList: Menu[] = [];
|
||||
|
||||
const { t } = useI18n();
|
||||
const go = useGo();
|
||||
const handleSearch = useDebounceFn(search, 200);
|
||||
|
||||
onBeforeMount(async () => {
|
||||
const list = await getMenus();
|
||||
menuList = cloneDeep(list);
|
||||
forEach(menuList, item => {
|
||||
item.fullName = t('routes.' + item.enCode.replace(/\./g, '-'), item.fullName);
|
||||
});
|
||||
});
|
||||
|
||||
function search(e: ChangeEvent) {
|
||||
e?.stopPropagation();
|
||||
const key = e.target.value;
|
||||
keyword.value = key.trim();
|
||||
if (!key) {
|
||||
searchResult.value = [];
|
||||
return;
|
||||
}
|
||||
const reg = createSearchReg(unref(keyword).toLocaleLowerCase());
|
||||
const filterMenu = filter(menuList, item => {
|
||||
const name = t('routes.' + item.enCode.replace(/\./g, '-'), item.fullName).toLocaleLowerCase();
|
||||
return reg.test(name);
|
||||
});
|
||||
searchResult.value = handlerSearchResult(filterMenu, reg);
|
||||
|
||||
activeIndex.value = 0;
|
||||
}
|
||||
|
||||
function handlerSearchResult(filterMenu: Menu[], reg: RegExp, parent?: Menu) {
|
||||
const ret: SearchResult[] = [];
|
||||
filterMenu.forEach(item => {
|
||||
const { fullName, enCode, path, icon, children, type } = item;
|
||||
const name = t('routes.' + enCode.replace(/\./g, '-'), fullName);
|
||||
|
||||
if (reg.test(name.toLocaleLowerCase()) && !children?.length && type != 1) {
|
||||
ret.push({
|
||||
name: parent?.fullName ? `${parent.fullName} > ${name}` : name,
|
||||
path,
|
||||
icon,
|
||||
});
|
||||
}
|
||||
if (Array.isArray(children) && children.length) {
|
||||
ret.push(...handlerSearchResult(children, reg, item));
|
||||
}
|
||||
});
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Activate when the mouse moves to a certain line
|
||||
function handleMouseenter(e: any) {
|
||||
const index = e.target.dataset.index;
|
||||
activeIndex.value = Number(index);
|
||||
}
|
||||
|
||||
// Arrow key up
|
||||
function handleUp() {
|
||||
if (!searchResult.value.length) return;
|
||||
activeIndex.value--;
|
||||
if (activeIndex.value < 0) {
|
||||
activeIndex.value = searchResult.value.length - 1;
|
||||
}
|
||||
handleScroll();
|
||||
}
|
||||
|
||||
// Arrow key down
|
||||
function handleDown() {
|
||||
if (!searchResult.value.length) return;
|
||||
activeIndex.value++;
|
||||
if (activeIndex.value > searchResult.value.length - 1) {
|
||||
activeIndex.value = 0;
|
||||
}
|
||||
handleScroll();
|
||||
}
|
||||
|
||||
// When the keyboard up and down keys move to an invisible place
|
||||
// the scroll bar needs to scroll automatically
|
||||
function handleScroll() {
|
||||
const refList = unref(refs);
|
||||
if (!refList || !Array.isArray(refList) || refList.length === 0 || !unref(scrollWrap)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const index = unref(activeIndex);
|
||||
const currentRef = refList[index];
|
||||
if (!currentRef) {
|
||||
return;
|
||||
}
|
||||
const wrapEl = unref(scrollWrap);
|
||||
if (!wrapEl) {
|
||||
return;
|
||||
}
|
||||
const scrollHeight = currentRef.offsetTop + currentRef.offsetHeight;
|
||||
const wrapHeight = wrapEl.offsetHeight;
|
||||
const { start } = useScrollTo({
|
||||
el: wrapEl,
|
||||
duration: 100,
|
||||
to: scrollHeight - wrapHeight,
|
||||
});
|
||||
start();
|
||||
}
|
||||
|
||||
function isHttpUrl(url?: string): boolean {
|
||||
if (!url) {
|
||||
return false;
|
||||
}
|
||||
// 使用正则表达式测试URL是否以http:// 或 https:// 开头
|
||||
const httpRegex = /^https?:\/\/.*$/;
|
||||
return httpRegex.test(url);
|
||||
}
|
||||
|
||||
// enter keyboard event
|
||||
async function handleEnter() {
|
||||
if (!searchResult.value.length) {
|
||||
return;
|
||||
}
|
||||
const result = unref(searchResult);
|
||||
const index = unref(activeIndex);
|
||||
if (result.length === 0 || index < 0) {
|
||||
return;
|
||||
}
|
||||
const to = result[index];
|
||||
handleClose();
|
||||
await nextTick();
|
||||
if (isHttpUrl(to.path)) {
|
||||
window.open(to.path, '_blank');
|
||||
} else {
|
||||
go(to.path);
|
||||
}
|
||||
}
|
||||
|
||||
// close search modal
|
||||
function handleClose() {
|
||||
searchResult.value = [];
|
||||
emit('close');
|
||||
}
|
||||
|
||||
// enter search
|
||||
onKeyStroke('Enter', handleEnter);
|
||||
// Monitor keyboard arrow keys
|
||||
onKeyStroke('ArrowUp', handleUp);
|
||||
onKeyStroke('ArrowDown', handleDown);
|
||||
// esc close
|
||||
onKeyStroke('Escape', handleClose);
|
||||
|
||||
return { handleSearch, searchResult, keyword, activeIndex, handleMouseenter, handleEnter };
|
||||
}
|
||||
17
src/components/Application/src/useAppContext.ts
Normal file
17
src/components/Application/src/useAppContext.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { InjectionKey, Ref } from 'vue';
|
||||
import { createContext, useContext } from '@/hooks/core/useContext';
|
||||
|
||||
export interface AppProviderContextProps {
|
||||
prefixCls: Ref<string>;
|
||||
isMobile: Ref<boolean>;
|
||||
}
|
||||
|
||||
const key: InjectionKey<AppProviderContextProps> = Symbol();
|
||||
|
||||
export function createAppProviderContext(context: AppProviderContextProps) {
|
||||
return createContext<AppProviderContextProps>(context, key);
|
||||
}
|
||||
|
||||
export function useAppProviderContext() {
|
||||
return useContext<AppProviderContextProps>(key);
|
||||
}
|
||||
4
src/components/Authority/index.ts
Normal file
4
src/components/Authority/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { withInstall } from '@/utils';
|
||||
import authority from './src/Authority.vue';
|
||||
|
||||
export const Authority = withInstall(authority);
|
||||
44
src/components/Authority/src/Authority.vue
Normal file
44
src/components/Authority/src/Authority.vue
Normal file
@@ -0,0 +1,44 @@
|
||||
<!--
|
||||
Access control component for fine-grained access control.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import type { PropType } from 'vue';
|
||||
import { defineComponent } from 'vue';
|
||||
import { usePermission } from '@/hooks/web/usePermission';
|
||||
import { getSlot } from '@/utils/helper/tsxHelper';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Authority',
|
||||
props: {
|
||||
/**
|
||||
* Specified role is visible
|
||||
* When the permission mode is the role mode, the value value can pass the role value.
|
||||
* When the permission mode is background, the value value can pass the code permission value
|
||||
* @default ''
|
||||
*/
|
||||
value: {
|
||||
type: [Number, Array, String] as PropType<string>,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
setup(props, { slots }) {
|
||||
const { hasColumnP } = usePermission();
|
||||
|
||||
/**
|
||||
* Render role button
|
||||
*/
|
||||
function renderAuth() {
|
||||
const { value } = props;
|
||||
if (!value) {
|
||||
return getSlot(slots);
|
||||
}
|
||||
return hasColumnP(value) ? getSlot(slots) : null;
|
||||
}
|
||||
|
||||
return () => {
|
||||
// Role-based value control
|
||||
return renderAuth();
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
10
src/components/Basic/index.ts
Normal file
10
src/components/Basic/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { withInstall } from '@/utils';
|
||||
import basicArrow from './src/BasicArrow.vue';
|
||||
import basicTitle from './src/BasicTitle.vue';
|
||||
import basicCaption from './src/BasicCaption.vue';
|
||||
import basicHelp from './src/BasicHelp.vue';
|
||||
|
||||
export const BasicArrow = withInstall(basicArrow);
|
||||
export const BasicTitle = withInstall(basicTitle);
|
||||
export const BasicCaption = withInstall(basicCaption);
|
||||
export const BasicHelp = withInstall(basicHelp);
|
||||
82
src/components/Basic/src/BasicArrow.vue
Normal file
82
src/components/Basic/src/BasicArrow.vue
Normal file
@@ -0,0 +1,82 @@
|
||||
<template>
|
||||
<span :class="getClass">
|
||||
<DownOutlined :style="getStyle" />
|
||||
</span>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { computed, unref } from 'vue';
|
||||
import { DownOutlined } from '@ant-design/icons-vue';
|
||||
import { useDesign } from '@/hooks/web/useDesign';
|
||||
import { useAttrs } from '@/hooks/core/useAttrs';
|
||||
|
||||
const props = defineProps({
|
||||
/**
|
||||
* Arrow expand state
|
||||
*/
|
||||
expand: { type: Boolean },
|
||||
/**
|
||||
* Arrow up by default
|
||||
*/
|
||||
up: { type: Boolean },
|
||||
/**
|
||||
* Arrow down by default
|
||||
*/
|
||||
down: { type: Boolean },
|
||||
/**
|
||||
* Cancel padding/margin for inline
|
||||
*/
|
||||
inset: { type: Boolean },
|
||||
});
|
||||
const attrs: any = useAttrs({ excludeDefaultKeys: false });
|
||||
const { prefixCls } = useDesign('basic-arrow');
|
||||
|
||||
// get component class
|
||||
const getClass = computed(() => {
|
||||
const { expand, up, down, inset } = props;
|
||||
return [
|
||||
prefixCls,
|
||||
{
|
||||
[`${prefixCls}--active`]: expand,
|
||||
up,
|
||||
inset,
|
||||
down,
|
||||
},
|
||||
];
|
||||
});
|
||||
const getStyle = computed(() => unref(attrs)?.iconStyle || {});
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
@prefix-cls: ~'@{namespace}-basic-arrow';
|
||||
|
||||
.@{prefix-cls} {
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
transform: rotate(0deg);
|
||||
transition: all 0.3s ease 0.1s;
|
||||
transform-origin: center center;
|
||||
|
||||
&--active {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
&.inset {
|
||||
line-height: 0px;
|
||||
}
|
||||
|
||||
&.up {
|
||||
transform: rotate(-180deg);
|
||||
}
|
||||
|
||||
&.down {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
&.up.@{prefix-cls}--active {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
&.down.@{prefix-cls}--active {
|
||||
transform: rotate(-180deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
71
src/components/Basic/src/BasicCaption.vue
Normal file
71
src/components/Basic/src/BasicCaption.vue
Normal file
@@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<div :class="getClass">
|
||||
<div :class="`${prefixCls}-content`" :style="{ 'justify-content': getContentPosition }">
|
||||
<slot name="content" v-if="slots.content"></slot>
|
||||
<BasicTitle :helpMessage="helpMessage" v-if="!slots.content && content">
|
||||
{{ content }}
|
||||
</BasicTitle>
|
||||
</div>
|
||||
<div :class="`${prefixCls}__action`" v-if="slots.action">
|
||||
<slot name="action" v-if="slots.action"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { computed, useSlots } from 'vue';
|
||||
import BasicTitle from './BasicTitle.vue';
|
||||
import { useDesign } from '@/hooks/web/useDesign';
|
||||
|
||||
const props = defineProps({
|
||||
helpMessage: {
|
||||
type: [String, Array] as PropType<string | string[]>,
|
||||
default: '',
|
||||
},
|
||||
content: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
contentPosition: {
|
||||
type: String as PropType<'left' | 'center' | 'right'>,
|
||||
default: 'left',
|
||||
},
|
||||
bordered: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
const slots = useSlots();
|
||||
const { prefixCls } = useDesign('basic-caption');
|
||||
const getClass = computed(() => [prefixCls, { [`${prefixCls}-border`]: props.bordered }]);
|
||||
const getContentPosition = computed(() => {
|
||||
if (props.contentPosition === 'left') return 'flex-start';
|
||||
if (props.contentPosition === 'right') return 'flex-end';
|
||||
return props.contentPosition;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@prefix-cls: ~'@{namespace}-basic-caption';
|
||||
|
||||
.@{prefix-cls} {
|
||||
min-height: 50px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
box-sizing: border-box;
|
||||
word-break: break-all;
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
&-border {
|
||||
border-bottom: 1px solid @border-color-base;
|
||||
}
|
||||
&-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
}
|
||||
&__action {
|
||||
margin-left: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
111
src/components/Basic/src/BasicHelp.vue
Normal file
111
src/components/Basic/src/BasicHelp.vue
Normal file
@@ -0,0 +1,111 @@
|
||||
<script lang="tsx">
|
||||
import type { CSSProperties, PropType } from 'vue';
|
||||
import { defineComponent, computed, unref } from 'vue';
|
||||
import { Tooltip } from 'ant-design-vue';
|
||||
import { QuestionCircleFilled } from '@ant-design/icons-vue';
|
||||
import { getPopupContainer } from '@/utils';
|
||||
import { isString, isArray } from '@/utils/is';
|
||||
import { getSlot } from '@/utils/helper/tsxHelper';
|
||||
import { useDesign } from '@/hooks/web/useDesign';
|
||||
|
||||
const props = {
|
||||
/**
|
||||
* Help text max-width
|
||||
* @default: 600px
|
||||
*/
|
||||
maxWidth: { type: String, default: '600px' },
|
||||
/**
|
||||
* Whether to display the serial number
|
||||
* @default: false
|
||||
*/
|
||||
showIndex: { type: Boolean },
|
||||
/**
|
||||
* Help text font color
|
||||
* @default: #ffffff
|
||||
*/
|
||||
color: { type: String, default: '#ffffff' },
|
||||
/**
|
||||
* Help text font size
|
||||
* @default: 14px
|
||||
*/
|
||||
fontSize: { type: String, default: '14px' },
|
||||
/**
|
||||
* Help text list
|
||||
*/
|
||||
placement: { type: String, default: 'top' },
|
||||
/**
|
||||
* Help text list
|
||||
*/
|
||||
text: { type: [Array, String] as PropType<string[] | string> },
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
name: 'BasicHelp',
|
||||
components: { Tooltip },
|
||||
props,
|
||||
setup(props, { slots }) {
|
||||
const { prefixCls } = useDesign('basic-help');
|
||||
|
||||
const getTooltipStyle = computed((): CSSProperties => ({ color: props.color, fontSize: props.fontSize }));
|
||||
|
||||
const getOverlayStyle = computed((): CSSProperties => ({ maxWidth: props.maxWidth }));
|
||||
|
||||
function renderTitle() {
|
||||
const textList = props.text;
|
||||
|
||||
if (isString(textList)) {
|
||||
return <p>{textList}</p>;
|
||||
}
|
||||
|
||||
if (isArray(textList)) {
|
||||
return textList.map((text, index) => {
|
||||
return (
|
||||
<p key={text}>
|
||||
<>
|
||||
{props.showIndex ? `${index + 1}. ` : ''}
|
||||
{text}
|
||||
</>
|
||||
</p>
|
||||
);
|
||||
});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
return () => {
|
||||
return (
|
||||
<Tooltip
|
||||
overlayClassName={`${prefixCls}__wrap`}
|
||||
title={<div style={unref(getTooltipStyle)}>{renderTitle()}</div>}
|
||||
autoAdjustOverflow={true}
|
||||
overlayStyle={unref(getOverlayStyle)}
|
||||
placement={props.placement as 'right'}
|
||||
getPopupContainer={() => getPopupContainer()}>
|
||||
<span class={prefixCls}>{getSlot(slots) || <QuestionCircleFilled />}</span>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="less">
|
||||
@prefix-cls: ~'@{namespace}-basic-help';
|
||||
|
||||
.@{prefix-cls} {
|
||||
display: inline-block;
|
||||
margin-left: 4px;
|
||||
font-size: 14px;
|
||||
color: @text-color-secondary;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: @primary-color;
|
||||
}
|
||||
|
||||
&__wrap {
|
||||
p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
75
src/components/Basic/src/BasicTitle.vue
Normal file
75
src/components/Basic/src/BasicTitle.vue
Normal file
@@ -0,0 +1,75 @@
|
||||
<template>
|
||||
<span :class="getClass">
|
||||
<slot></slot>
|
||||
<BasicHelp :class="`${prefixCls}-help`" v-if="helpMessage" :text="helpMessage" />
|
||||
</span>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import type { PropType } from 'vue';
|
||||
import { useSlots, computed } from 'vue';
|
||||
import BasicHelp from './BasicHelp.vue';
|
||||
import { useDesign } from '@/hooks/web/useDesign';
|
||||
|
||||
const props = defineProps({
|
||||
/**
|
||||
* Help text list or string
|
||||
* @default: ''
|
||||
*/
|
||||
helpMessage: {
|
||||
type: [String, Array] as PropType<string | string[]>,
|
||||
default: '',
|
||||
},
|
||||
/**
|
||||
* Whether the color block on the left side of the title
|
||||
* @default: false
|
||||
*/
|
||||
span: { type: Boolean },
|
||||
/**
|
||||
* Whether to default the text, that is, not bold
|
||||
* @default: false
|
||||
*/
|
||||
normal: { type: Boolean },
|
||||
});
|
||||
|
||||
const { prefixCls } = useDesign('basic-title');
|
||||
const slots = useSlots();
|
||||
const getClass = computed(() => [prefixCls, { [`${prefixCls}-show-span`]: props.span && slots.default }, { [`${prefixCls}-normal`]: props.normal }]);
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
@prefix-cls: ~'@{namespace}-basic-title';
|
||||
|
||||
.@{prefix-cls} {
|
||||
position: relative;
|
||||
display: flex;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
line-height: 24px;
|
||||
color: @text-color-base;
|
||||
user-select: none;
|
||||
align-items: center;
|
||||
|
||||
&-normal {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&-show-span {
|
||||
padding-left: 7px;
|
||||
&::before {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
left: 0;
|
||||
width: 3px;
|
||||
height: 16px;
|
||||
margin-right: 4px;
|
||||
background-color: @primary-color;
|
||||
content: '';
|
||||
}
|
||||
}
|
||||
|
||||
:deep(&-help) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
11
src/components/Button/index.ts
Normal file
11
src/components/Button/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { withInstall } from '@/utils';
|
||||
import type { ExtractPropTypes } from 'vue';
|
||||
import button from './src/BasicButton.vue';
|
||||
import popConfirmButton from './src/PopConfirmButton.vue';
|
||||
import modelConfirmButton from './src/ModelConfirmButton.vue';
|
||||
import { buttonProps } from './src/props';
|
||||
|
||||
export const Button = withInstall(button);
|
||||
export const PopConfirmButton = withInstall(popConfirmButton);
|
||||
export const ModelConfirmButton = withInstall(modelConfirmButton);
|
||||
export declare type ButtonProps = Partial<ExtractPropTypes<typeof buttonProps>>;
|
||||
62
src/components/Button/src/BasicButton.vue
Normal file
62
src/components/Button/src/BasicButton.vue
Normal file
@@ -0,0 +1,62 @@
|
||||
<template>
|
||||
<Button v-bind="getBindValue" :class="getButtonClass" @click="onClick">
|
||||
<template #icon v-if="$slots.icon || preIcon">
|
||||
<slot name="icon">
|
||||
<i :class="[preIcon, 'button-preIcon']"></i>
|
||||
</slot>
|
||||
</template>
|
||||
<template #default="data">
|
||||
<slot v-bind="data || {}"></slot>
|
||||
<i :class="[postIcon, 'button-postIcon']" v-if="postIcon"></i>
|
||||
</template>
|
||||
</Button>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import { Button } from 'ant-design-vue';
|
||||
export default defineComponent({
|
||||
name: 'AButton',
|
||||
inheritAttrs: false,
|
||||
extends: Button,
|
||||
});
|
||||
</script>
|
||||
<script lang="ts" setup>
|
||||
import { computed, unref } from 'vue';
|
||||
|
||||
import { buttonProps } from './props';
|
||||
import { useAttrs } from '@/hooks/core/useAttrs';
|
||||
|
||||
const props: any = defineProps(buttonProps);
|
||||
// get component class
|
||||
const attrs = useAttrs({ excludeDefaultKeys: false });
|
||||
const getButtonClass = computed(() => {
|
||||
const { color, disabled, type } = props;
|
||||
return [
|
||||
{
|
||||
[`ant-btn-${color}`]: !!color,
|
||||
[`ant-btn-${type}`]: type && ['warning', 'error'].includes(type),
|
||||
[`is-disabled`]: disabled,
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
// get inherit binding value
|
||||
const getBindValue = computed(() => ({ ...unref(attrs), ...props, type: !props.type || ['warning', 'error'].includes(props.type) ? 'default' : props.type }));
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.ant-btn {
|
||||
.button-preIcon,
|
||||
.button-postIcon,
|
||||
i {
|
||||
font-size: 14px;
|
||||
}
|
||||
:deep(.button-preIcon + span),
|
||||
:deep(i + span) {
|
||||
margin-left: 5px;
|
||||
}
|
||||
.button-postIcon {
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
38
src/components/Button/src/ModelConfirmButton.vue
Normal file
38
src/components/Button/src/ModelConfirmButton.vue
Normal file
@@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<a-button v-bind="getBindValue" @click="onClick">
|
||||
<template #default="data">
|
||||
<slot v-bind="data || {}"></slot>
|
||||
</template>
|
||||
</a-button>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
export default defineComponent({
|
||||
inheritAttrs: false,
|
||||
});
|
||||
</script>
|
||||
<script lang="ts" setup>
|
||||
import { computed, unref } from 'vue';
|
||||
import { omit } from 'lodash-es';
|
||||
import { useAttrs } from '@/hooks/core/useAttrs';
|
||||
import { useMessage } from '@/hooks/web/useMessage';
|
||||
import { useI18n } from '@/hooks/web/useI18n';
|
||||
|
||||
const props = defineProps({
|
||||
modelConfirm: { type: Object },
|
||||
});
|
||||
const attrs = useAttrs({ excludeDefaultKeys: false });
|
||||
const { createConfirm } = useMessage();
|
||||
const { t } = useI18n();
|
||||
const { modelConfirm } = props;
|
||||
function onClick() {
|
||||
createConfirm({
|
||||
iconType: modelConfirm?.iconType || 'warning',
|
||||
title: modelConfirm?.title || t('common.tipTitle'),
|
||||
content: modelConfirm?.content || t('common.delTip'),
|
||||
onOk: modelConfirm?.onOk,
|
||||
});
|
||||
}
|
||||
const getBindValue = computed(() => omit({ ...unref(attrs) }, ['enable', 'getPopupContainer', 'label', 'onClick', 'icon']));
|
||||
</script>
|
||||
54
src/components/Button/src/PopConfirmButton.vue
Normal file
54
src/components/Button/src/PopConfirmButton.vue
Normal file
@@ -0,0 +1,54 @@
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, h, unref } from 'vue';
|
||||
import BasicButton from './BasicButton.vue';
|
||||
import { Popconfirm } from 'ant-design-vue';
|
||||
import { extendSlots } from '@/utils/helper/tsxHelper';
|
||||
import { omit } from 'lodash-es';
|
||||
import { useAttrs } from '@/hooks/core/useAttrs';
|
||||
import { useI18n } from '@/hooks/web/useI18n';
|
||||
|
||||
const props = {
|
||||
/**
|
||||
* Whether to enable the drop-down menu
|
||||
* @default: true
|
||||
*/
|
||||
enable: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
name: 'PopButton',
|
||||
inheritAttrs: false,
|
||||
props,
|
||||
setup(props, { slots }) {
|
||||
const { t } = useI18n();
|
||||
const attrs = useAttrs();
|
||||
|
||||
// get inherit binding value
|
||||
const getBindValues = computed(() => {
|
||||
return Object.assign(
|
||||
{
|
||||
okText: t('common.okText'),
|
||||
cancelText: t('common.cancelText'),
|
||||
},
|
||||
{ ...props, ...unref(attrs) },
|
||||
);
|
||||
});
|
||||
|
||||
return () => {
|
||||
const bindValues = omit(unref(getBindValues), 'icon');
|
||||
const btnBind = omit(bindValues, 'title') as Recordable;
|
||||
if (btnBind.disabled) btnBind.color = '';
|
||||
const Button = h(BasicButton, btnBind, extendSlots(slots));
|
||||
|
||||
// If it is not enabled, it is a normal button
|
||||
if (!props.enable) {
|
||||
return Button;
|
||||
}
|
||||
return h(Popconfirm, bindValues, { default: () => Button });
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
26
src/components/Button/src/props.ts
Normal file
26
src/components/Button/src/props.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
const validColors = ['primary', 'error', 'warning', 'success', 'info', ''] as const;
|
||||
type ButtonColorType = (typeof validColors)[number];
|
||||
|
||||
export const buttonProps = {
|
||||
color: {
|
||||
type: String as PropType<ButtonColorType>,
|
||||
validator: v => validColors.includes(v),
|
||||
default: '',
|
||||
},
|
||||
loading: { type: Boolean },
|
||||
disabled: { type: Boolean },
|
||||
/**
|
||||
* Text before icon.
|
||||
*/
|
||||
preIcon: { type: String },
|
||||
/**
|
||||
* Text after icon.
|
||||
*/
|
||||
postIcon: { type: String },
|
||||
/**
|
||||
* preIcon and postIcon icon size.
|
||||
* @default: 14
|
||||
*/
|
||||
// iconSize: { type: Number, default: 14 },
|
||||
onClick: { type: Function as PropType<(...args) => any>, default: null },
|
||||
};
|
||||
4
src/components/CardList/index.ts
Normal file
4
src/components/CardList/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { withInstall } from '@/utils';
|
||||
import cardList from './src/CardList.vue';
|
||||
|
||||
export const CardList = withInstall(cardList);
|
||||
162
src/components/CardList/src/CardList.vue
Normal file
162
src/components/CardList/src/CardList.vue
Normal file
@@ -0,0 +1,162 @@
|
||||
<template>
|
||||
<div class="p-2">
|
||||
<div class="p-4 mb-2 bg-white">
|
||||
<BasicForm @register="registerForm" />
|
||||
</div>
|
||||
<div class="p-2 bg-white">
|
||||
<List :grid="{ gutter: 5, xs: 1, sm: 2, md: 4, lg: 4, xl: 6, xxl: grid }" :data-source="data" :pagination="paginationProp">
|
||||
<template #header>
|
||||
<div class="flex justify-end space-x-2"
|
||||
><slot name="header"></slot>
|
||||
<Tooltip>
|
||||
<template #title>
|
||||
<div class="w-50">每行显示数量</div><Slider id="slider" v-bind="sliderProp" v-model:value="grid" @change="sliderChange"
|
||||
/></template>
|
||||
<Button><TableOutlined /></Button>
|
||||
</Tooltip>
|
||||
<Tooltip @click="fetch">
|
||||
<template #title>刷新</template>
|
||||
<Button><RedoOutlined /></Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<template #renderItem="{ item }">
|
||||
<ListItem>
|
||||
<Card>
|
||||
<template #title></template>
|
||||
<template #cover>
|
||||
<div :class="height">
|
||||
<Image :src="item.imgs[0]" />
|
||||
</div>
|
||||
</template>
|
||||
<template #actions>
|
||||
<!-- <SettingOutlined key="setting" />-->
|
||||
<EditOutlined key="edit" />
|
||||
<Dropdown
|
||||
:trigger="['hover']"
|
||||
:dropMenuList="[
|
||||
{
|
||||
text: '删除',
|
||||
event: '1',
|
||||
popConfirm: {
|
||||
title: '是否确认删除',
|
||||
confirm: handleDelete.bind(null, item.id),
|
||||
},
|
||||
},
|
||||
]"
|
||||
popconfirm>
|
||||
<EllipsisOutlined key="ellipsis" />
|
||||
</Dropdown>
|
||||
</template>
|
||||
|
||||
<CardMeta>
|
||||
<template #title>
|
||||
<TypographyText :content="item.name" :ellipsis="{ tooltip: item.address }" />
|
||||
</template>
|
||||
<template #avatar>
|
||||
<Avatar :src="item.avatar" />
|
||||
</template>
|
||||
<template #description>{{ item.time }}</template>
|
||||
</CardMeta>
|
||||
</Card>
|
||||
</ListItem>
|
||||
</template>
|
||||
</List>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { EditOutlined, EllipsisOutlined, RedoOutlined, TableOutlined } from '@ant-design/icons-vue';
|
||||
import { List, Card, Image, Typography, Tooltip, Slider, Avatar } from 'ant-design-vue';
|
||||
import { Dropdown } from '@/components/Dropdown';
|
||||
import { BasicForm, useForm } from '@/components/Form';
|
||||
import { propTypes } from '@/utils/propTypes';
|
||||
import { Button } from '@/components/Button';
|
||||
import { isFunction } from '@/utils/is';
|
||||
import { useSlider, grid } from './data';
|
||||
const ListItem = List.Item;
|
||||
const CardMeta = Card.Meta;
|
||||
const TypographyText = Typography.Text;
|
||||
// 获取slider属性
|
||||
const sliderProp = computed(() => useSlider(4));
|
||||
// 组件接收参数
|
||||
const props = defineProps({
|
||||
// 请求API的参数
|
||||
params: propTypes.object.def({}),
|
||||
//api
|
||||
api: propTypes.func,
|
||||
});
|
||||
//暴露内部方法
|
||||
const emit = defineEmits(['getMethod', 'delete']);
|
||||
//数据
|
||||
const data = ref([]);
|
||||
// 切换每行个数
|
||||
// cover图片自适应高度
|
||||
//修改pageSize并重新请求数据
|
||||
|
||||
const height = computed(() => {
|
||||
return `h-${120 - grid.value * 6}`;
|
||||
});
|
||||
//表单
|
||||
const [registerForm, { validate }] = useForm({
|
||||
schemas: [{ field: 'type', component: 'Input', label: '类型' }],
|
||||
labelWidth: 80,
|
||||
baseColProps: { span: 6 },
|
||||
actionColOptions: { span: 24 },
|
||||
autoSubmitOnEnter: true,
|
||||
submitFunc: handleSubmit,
|
||||
});
|
||||
//表单提交
|
||||
async function handleSubmit() {
|
||||
const data = await validate();
|
||||
await fetch(data);
|
||||
}
|
||||
function sliderChange(n) {
|
||||
pageSize.value = n * 4;
|
||||
fetch();
|
||||
}
|
||||
|
||||
// 自动请求并暴露内部方法
|
||||
onMounted(() => {
|
||||
fetch();
|
||||
emit('getMethod', fetch);
|
||||
});
|
||||
|
||||
async function fetch(p = {}) {
|
||||
const { api, params } = props;
|
||||
if (api && isFunction(api)) {
|
||||
const res = await api({ ...params, page: page.value, pageSize: pageSize.value, ...p });
|
||||
data.value = res.items;
|
||||
total.value = res.total;
|
||||
}
|
||||
}
|
||||
//分页相关
|
||||
const page = ref(1);
|
||||
const pageSize = ref(36);
|
||||
const total = ref(0);
|
||||
const paginationProp = ref({
|
||||
showSizeChanger: false,
|
||||
showQuickJumper: true,
|
||||
pageSize,
|
||||
current: page,
|
||||
total,
|
||||
showTotal: total => `总 ${total} 条`,
|
||||
onChange: pageChange,
|
||||
onShowSizeChange: pageSizeChange,
|
||||
});
|
||||
|
||||
function pageChange(p, pz) {
|
||||
page.value = p;
|
||||
pageSize.value = pz;
|
||||
fetch();
|
||||
}
|
||||
function pageSizeChange(_current, size) {
|
||||
pageSize.value = size;
|
||||
fetch();
|
||||
}
|
||||
|
||||
async function handleDelete(id) {
|
||||
emit('delete', id);
|
||||
}
|
||||
</script>
|
||||
25
src/components/CardList/src/data.ts
Normal file
25
src/components/CardList/src/data.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { ref } from 'vue';
|
||||
// 每行个数
|
||||
export const grid = ref(12);
|
||||
// slider属性
|
||||
export const useSlider = (min = 6, max = 12) => {
|
||||
// 每行显示个数滑动条
|
||||
const getMarks = () => {
|
||||
const l = {};
|
||||
for (let i = min; i < max + 1; i++) {
|
||||
l[i] = {
|
||||
style: {
|
||||
color: '#fff',
|
||||
},
|
||||
label: i,
|
||||
};
|
||||
}
|
||||
return l;
|
||||
};
|
||||
return {
|
||||
min,
|
||||
max,
|
||||
marks: getMarks(),
|
||||
step: 1,
|
||||
};
|
||||
};
|
||||
4
src/components/Chart/index.ts
Normal file
4
src/components/Chart/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { withInstall } from '@/utils';
|
||||
import chart from './src/Chart.vue';
|
||||
|
||||
export const Chart = withInstall(chart);
|
||||
38
src/components/Chart/src/Chart.vue
Normal file
38
src/components/Chart/src/Chart.vue
Normal file
@@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<div :loading="loading">
|
||||
<div ref="chartRef" :style="{ width, height }"></div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { Ref, ref, watch } from 'vue';
|
||||
import { useECharts } from '@/hooks/web/useECharts';
|
||||
|
||||
const props = defineProps({
|
||||
loading: {
|
||||
type: Boolean as PropType<boolean>,
|
||||
default: false,
|
||||
},
|
||||
width: {
|
||||
type: String as PropType<string>,
|
||||
default: '100%',
|
||||
},
|
||||
height: {
|
||||
type: String as PropType<string>,
|
||||
default: '300px',
|
||||
},
|
||||
options: {
|
||||
type: Object as PropType<any>,
|
||||
default: {},
|
||||
},
|
||||
});
|
||||
const chartRef = ref<HTMLDivElement | null>(null);
|
||||
const { setOptions } = useECharts(chartRef as Ref<HTMLDivElement>);
|
||||
|
||||
watch(
|
||||
() => props.options,
|
||||
() => {
|
||||
setOptions(props.options, false);
|
||||
},
|
||||
{ immediate: true, deep: true },
|
||||
);
|
||||
</script>
|
||||
4
src/components/ClickOutSide/index.ts
Normal file
4
src/components/ClickOutSide/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { withInstall } from '@/utils';
|
||||
import clickOutSide from './src/ClickOutSide.vue';
|
||||
|
||||
export const ClickOutSide = withInstall(clickOutSide);
|
||||
19
src/components/ClickOutSide/src/ClickOutSide.vue
Normal file
19
src/components/ClickOutSide/src/ClickOutSide.vue
Normal file
@@ -0,0 +1,19 @@
|
||||
<template>
|
||||
<div ref="wrap">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { onClickOutside } from '@vueuse/core';
|
||||
const emit = defineEmits(['mounted', 'clickOutside']);
|
||||
const wrap = ref<ElRef>(null);
|
||||
|
||||
onClickOutside(wrap, () => {
|
||||
emit('clickOutside');
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
emit('mounted');
|
||||
});
|
||||
</script>
|
||||
10
src/components/CodeEditor/index.ts
Normal file
10
src/components/CodeEditor/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { withInstall } from '@/utils';
|
||||
import codeEditor from './src/CodeEditor.vue';
|
||||
import monacoEditor from './src/monacoEditor/MonacoEditor.vue';
|
||||
import jsonPreview from './src/json-preview/JsonPreview.vue';
|
||||
|
||||
export const CodeEditor = withInstall(codeEditor);
|
||||
export const MonacoEditor = withInstall(monacoEditor);
|
||||
export const JsonPreview = withInstall(jsonPreview);
|
||||
|
||||
export * from './src/typing';
|
||||
53
src/components/CodeEditor/src/CodeEditor.vue
Normal file
53
src/components/CodeEditor/src/CodeEditor.vue
Normal file
@@ -0,0 +1,53 @@
|
||||
<template>
|
||||
<div class="h-full">
|
||||
<CodeMirrorEditor :value="getValue" @change="handleValueChange" :mode="mode" :readonly="readonly" ref="editorRef" />
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import CodeMirrorEditor from './codemirror/CodeMirror.vue';
|
||||
import { isString } from '@/utils/is';
|
||||
import { MODE } from './typing';
|
||||
|
||||
const props = defineProps({
|
||||
value: { type: [Object, String] as PropType<Record<string, any> | string> },
|
||||
mode: {
|
||||
type: String as PropType<MODE>,
|
||||
default: MODE.JSON,
|
||||
validator(value: any) {
|
||||
// 这个值必须匹配下列字符串中的一个
|
||||
return Object.values(MODE).includes(value);
|
||||
},
|
||||
},
|
||||
readonly: { type: Boolean },
|
||||
autoFormat: { type: Boolean, default: true },
|
||||
});
|
||||
defineExpose({ insert });
|
||||
const emit = defineEmits(['change', 'update:value', 'format-error']);
|
||||
const editorRef = ref(null);
|
||||
|
||||
const getValue = computed(() => {
|
||||
const { value, mode, autoFormat } = props;
|
||||
if (!autoFormat || mode !== MODE.JSON) {
|
||||
return value as string;
|
||||
}
|
||||
let result = value;
|
||||
if (isString(value)) {
|
||||
try {
|
||||
result = JSON.parse(value);
|
||||
} catch (e) {
|
||||
emit('format-error', value);
|
||||
return value as string;
|
||||
}
|
||||
}
|
||||
return JSON.stringify(result, null, 2);
|
||||
});
|
||||
|
||||
function handleValueChange(v) {
|
||||
emit('update:value', v);
|
||||
emit('change', v);
|
||||
}
|
||||
function insert(val, hasBrackets = false) {
|
||||
(editorRef.value as any)?.insert(val, hasBrackets);
|
||||
}
|
||||
</script>
|
||||
121
src/components/CodeEditor/src/codemirror/CodeMirror.vue
Normal file
121
src/components/CodeEditor/src/codemirror/CodeMirror.vue
Normal file
@@ -0,0 +1,121 @@
|
||||
<template>
|
||||
<div class="relative !h-full w-full overflow-hidden" ref="el"></div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted, onUnmounted, watchEffect, watch, unref, nextTick } from 'vue';
|
||||
import { useDebounceFn } from '@vueuse/core';
|
||||
import { useAppStore } from '@/store/modules/app';
|
||||
import { useWindowSizeFn } from '@/hooks/event/useWindowSizeFn';
|
||||
import CodeMirror from 'codemirror';
|
||||
import { MODE } from './../typing';
|
||||
// css
|
||||
import './codemirror.css';
|
||||
import 'codemirror/theme/idea.css';
|
||||
import 'codemirror/theme/material-palenight.css';
|
||||
// modes
|
||||
import 'codemirror/mode/javascript/javascript';
|
||||
import 'codemirror/mode/css/css';
|
||||
import 'codemirror/mode/htmlmixed/htmlmixed';
|
||||
|
||||
const props = defineProps({
|
||||
mode: {
|
||||
type: String as PropType<MODE>,
|
||||
default: MODE.JSON,
|
||||
validator(value: any) {
|
||||
// 这个值必须匹配下列字符串中的一个
|
||||
return Object.values(MODE).includes(value);
|
||||
},
|
||||
},
|
||||
value: { type: String, default: '' },
|
||||
readonly: { type: Boolean, default: false },
|
||||
});
|
||||
defineExpose({ insert });
|
||||
const emit = defineEmits(['change']);
|
||||
|
||||
const el = ref();
|
||||
let editor: Nullable<CodeMirror.Editor>;
|
||||
|
||||
const debounceRefresh = useDebounceFn(refresh, 100);
|
||||
const appStore = useAppStore();
|
||||
|
||||
watch(
|
||||
() => props.value,
|
||||
async value => {
|
||||
await nextTick();
|
||||
const oldValue = editor?.getValue();
|
||||
if (value !== oldValue) {
|
||||
editor?.setValue(value ? value : '');
|
||||
}
|
||||
},
|
||||
{ flush: 'post' },
|
||||
);
|
||||
|
||||
watchEffect(() => {
|
||||
editor?.setOption('mode', props.mode);
|
||||
});
|
||||
|
||||
watch(
|
||||
() => appStore.getDarkMode,
|
||||
async () => {
|
||||
setTheme();
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
function setTheme() {
|
||||
unref(editor)?.setOption('theme', appStore.getDarkMode === 'light' ? 'idea' : 'material-palenight');
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
editor?.refresh();
|
||||
}
|
||||
function insert(val, hasBrackets = false) {
|
||||
editor?.replaceSelection(val);
|
||||
editor?.focus();
|
||||
let pos1: any = editor?.getCursor();
|
||||
let pos2: any = {};
|
||||
pos2.line = pos1.line;
|
||||
pos2.ch = hasBrackets ? pos1.ch - 1 : pos1.ch;
|
||||
editor?.setCursor(pos2);
|
||||
}
|
||||
async function init() {
|
||||
const addonOptions = {
|
||||
autoCloseBrackets: true,
|
||||
autoCloseTags: true,
|
||||
foldGutter: true,
|
||||
gutters: ['CodeMirror-linenumbers'],
|
||||
matchBrackets: true,
|
||||
styleActiveLine: true,
|
||||
hintOptions: {
|
||||
keywords: ['SUM', 'SUBTRACT', 'PRODUCT', 'DIVIDE', 'COUNT'],
|
||||
},
|
||||
};
|
||||
|
||||
editor = CodeMirror(el.value!, {
|
||||
value: '',
|
||||
mode: props.mode,
|
||||
readOnly: props.readonly,
|
||||
tabSize: 2,
|
||||
theme: 'material-palenight',
|
||||
lineWrapping: true,
|
||||
lineNumbers: true,
|
||||
...addonOptions,
|
||||
});
|
||||
editor?.setValue(props.value);
|
||||
setTheme();
|
||||
editor?.on('change', () => {
|
||||
emit('change', editor?.getValue());
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await nextTick();
|
||||
init();
|
||||
useWindowSizeFn(debounceRefresh);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
editor = null;
|
||||
});
|
||||
</script>
|
||||
21
src/components/CodeEditor/src/codemirror/codeMirror.ts
Normal file
21
src/components/CodeEditor/src/codemirror/codeMirror.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import CodeMirror from 'codemirror';
|
||||
import './codemirror.css';
|
||||
import 'codemirror/theme/idea.css';
|
||||
import 'codemirror/theme/material-palenight.css';
|
||||
// import 'codemirror/addon/lint/lint.css';
|
||||
|
||||
// modes
|
||||
import 'codemirror/mode/javascript/javascript';
|
||||
import 'codemirror/mode/css/css';
|
||||
import 'codemirror/mode/htmlmixed/htmlmixed';
|
||||
// addons
|
||||
// import 'codemirror/addon/edit/closebrackets';
|
||||
// import 'codemirror/addon/edit/closetag';
|
||||
// import 'codemirror/addon/comment/comment';
|
||||
// import 'codemirror/addon/fold/foldcode';
|
||||
// import 'codemirror/addon/fold/foldgutter';
|
||||
// import 'codemirror/addon/fold/brace-fold';
|
||||
// import 'codemirror/addon/fold/indent-fold';
|
||||
// import 'codemirror/addon/lint/json-lint';
|
||||
// import 'codemirror/addon/fold/comment-fold';
|
||||
export { CodeMirror };
|
||||
525
src/components/CodeEditor/src/codemirror/codemirror.css
Normal file
525
src/components/CodeEditor/src/codemirror/codemirror.css
Normal file
@@ -0,0 +1,525 @@
|
||||
/* BASICS */
|
||||
|
||||
.CodeMirror {
|
||||
--base: #545281;
|
||||
--comment: hsl(210deg 25% 60%);
|
||||
--keyword: #af4ab1;
|
||||
--variable: #0055d1;
|
||||
--function: #c25205;
|
||||
--string: #2ba46d;
|
||||
--number: #c25205;
|
||||
--tags: #d00;
|
||||
--qualifier: #ff6032;
|
||||
--important: var(--string);
|
||||
|
||||
position: relative;
|
||||
height: auto;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
font-family: var(--font-code);
|
||||
background: white;
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
/* PADDING */
|
||||
|
||||
.CodeMirror-lines {
|
||||
min-height: 1px; /* prevents collapsing before first draw */
|
||||
padding: 4px 0; /* Vertical padding around content */
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.CodeMirror-scrollbar-filler,
|
||||
.CodeMirror-gutter-filler {
|
||||
background-color: white; /* The little square between H and V scrollbars */
|
||||
}
|
||||
|
||||
/* GUTTER */
|
||||
|
||||
.CodeMirror-gutters {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 3;
|
||||
min-height: 100%;
|
||||
white-space: nowrap;
|
||||
background-color: transparent;
|
||||
border-right: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.CodeMirror-linenumber {
|
||||
min-width: 20px;
|
||||
padding: 0 3px 0 5px;
|
||||
color: var(--comment);
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.CodeMirror-guttermarker {
|
||||
color: black;
|
||||
}
|
||||
|
||||
.CodeMirror-guttermarker-subtle {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* FOLD GUTTER */
|
||||
|
||||
.CodeMirror-foldmarker {
|
||||
font-family: arial;
|
||||
line-height: 0.3;
|
||||
color: #414141;
|
||||
text-shadow: #f96 1px 1px 2px, #f96 -1px -1px 2px, #f96 1px -1px 2px, #f96 -1px 1px 2px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.CodeMirror-foldgutter {
|
||||
width: 0.7em;
|
||||
}
|
||||
|
||||
.CodeMirror-foldgutter-open,
|
||||
.CodeMirror-foldgutter-folded {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.CodeMirror-foldgutter-open::after,
|
||||
.CodeMirror-foldgutter-folded::after {
|
||||
position: relative;
|
||||
top: -0.1em;
|
||||
display: inline-block;
|
||||
font-size: 0.8em;
|
||||
content: '>';
|
||||
opacity: 0.8;
|
||||
transform: rotate(90deg);
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.CodeMirror-foldgutter-folded::after {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
/* CURSOR */
|
||||
|
||||
.CodeMirror-cursor {
|
||||
position: absolute;
|
||||
width: 0;
|
||||
pointer-events: none;
|
||||
border-right: none;
|
||||
border-left: 1px solid black;
|
||||
}
|
||||
|
||||
/* Shown when moving in bi-directional text */
|
||||
.CodeMirror div.CodeMirror-secondarycursor {
|
||||
border-left: 1px solid silver;
|
||||
}
|
||||
|
||||
.cm-fat-cursor .CodeMirror-cursor {
|
||||
width: auto;
|
||||
background: #7e7;
|
||||
border: 0 !important;
|
||||
}
|
||||
|
||||
.cm-fat-cursor div.CodeMirror-cursors {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.cm-fat-cursor-mark {
|
||||
background-color: rgb(20 255 20 / 50%);
|
||||
animation: blink 1.06s steps(1) infinite;
|
||||
}
|
||||
|
||||
.cm-animate-fat-cursor {
|
||||
width: auto;
|
||||
background-color: #7e7;
|
||||
border: 0;
|
||||
animation: blink 1.06s steps(1) infinite;
|
||||
}
|
||||
@keyframes blink {
|
||||
50% {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
@keyframes blink {
|
||||
50% {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
@keyframes blink {
|
||||
50% {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.cm-tab {
|
||||
display: inline-block;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
|
||||
.CodeMirror-rulers {
|
||||
position: absolute;
|
||||
top: -50px;
|
||||
right: 0;
|
||||
bottom: -20px;
|
||||
left: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.CodeMirror-ruler {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
border-left: 1px solid #ccc;
|
||||
}
|
||||
|
||||
/* DEFAULT THEME */
|
||||
.cm-s-default.CodeMirror {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.cm-s-default .cm-header {
|
||||
color: blue;
|
||||
}
|
||||
|
||||
.cm-s-default .cm-quote {
|
||||
color: #090;
|
||||
}
|
||||
|
||||
.cm-negative {
|
||||
color: #d44;
|
||||
}
|
||||
|
||||
.cm-positive {
|
||||
color: #292;
|
||||
}
|
||||
|
||||
.cm-header,
|
||||
.cm-strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.cm-em {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.cm-link {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.cm-strikethrough {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.cm-s-default .cm-atom,
|
||||
.cm-s-default .cm-def,
|
||||
.cm-s-default .cm-property,
|
||||
.cm-s-default .cm-variable-2,
|
||||
.cm-s-default .cm-variable-3,
|
||||
.cm-s-default .cm-punctuation {
|
||||
color: var(--base);
|
||||
}
|
||||
|
||||
.cm-s-default .cm-hr,
|
||||
.cm-s-default .cm-comment {
|
||||
color: var(--comment);
|
||||
}
|
||||
|
||||
.cm-s-default .cm-attribute,
|
||||
.cm-s-default .cm-keyword {
|
||||
color: var(--keyword);
|
||||
}
|
||||
|
||||
.cm-s-default .cm-variable {
|
||||
color: var(--variable);
|
||||
}
|
||||
|
||||
.cm-s-default .cm-bracket,
|
||||
.cm-s-default .cm-tag {
|
||||
color: var(--tags);
|
||||
}
|
||||
|
||||
.cm-s-default .cm-number {
|
||||
color: var(--number);
|
||||
}
|
||||
|
||||
.cm-s-default .cm-string,
|
||||
.cm-s-default .cm-string-2 {
|
||||
color: var(--string);
|
||||
}
|
||||
|
||||
.cm-s-default .cm-type {
|
||||
color: #085;
|
||||
}
|
||||
|
||||
.cm-s-default .cm-meta {
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.cm-s-default .cm-qualifier {
|
||||
color: var(--qualifier);
|
||||
}
|
||||
|
||||
.cm-s-default .cm-builtin {
|
||||
color: #7539ff;
|
||||
}
|
||||
|
||||
.cm-s-default .cm-link {
|
||||
color: var(--flash);
|
||||
}
|
||||
|
||||
.cm-s-default .cm-error {
|
||||
color: #ff008c;
|
||||
}
|
||||
|
||||
.cm-invalidchar {
|
||||
color: #ff008c;
|
||||
}
|
||||
|
||||
.CodeMirror-composing {
|
||||
border-bottom: 2px solid;
|
||||
}
|
||||
|
||||
/* Default styles for common addons */
|
||||
|
||||
div.CodeMirror span.CodeMirror-matchingbracket {
|
||||
color: #0b0;
|
||||
}
|
||||
|
||||
div.CodeMirror span.CodeMirror-nonmatchingbracket {
|
||||
color: #a22;
|
||||
}
|
||||
|
||||
.CodeMirror-matchingtag {
|
||||
background: rgb(255 150 0 / 30%);
|
||||
}
|
||||
|
||||
.CodeMirror-activeline-background {
|
||||
background: #e8f2ff;
|
||||
}
|
||||
|
||||
/* STOP */
|
||||
|
||||
/* The rest of this file contains styles related to the mechanics of
|
||||
the editor. You probably shouldn't touch them. */
|
||||
|
||||
.CodeMirror-scroll {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
padding-bottom: 30px;
|
||||
margin-right: -30px;
|
||||
|
||||
/* 30px is the magic margin used to hide the element's real scrollbars */
|
||||
|
||||
/* See overflow: hidden in .CodeMirror */
|
||||
margin-bottom: -30px;
|
||||
overflow: scroll !important; /* Things will break if this is overridden */
|
||||
outline: none; /* Prevent dragging from highlighting the element */
|
||||
}
|
||||
|
||||
.CodeMirror-sizer {
|
||||
position: relative;
|
||||
margin-bottom: 20px !important;
|
||||
border-right: 30px solid transparent;
|
||||
}
|
||||
|
||||
/* The fake, visible scrollbars. Used to force redraw during scrolling
|
||||
before actual scrolling happens, thus preventing shaking and
|
||||
flickering artifacts. */
|
||||
.CodeMirror-vscrollbar,
|
||||
.CodeMirror-hscrollbar,
|
||||
.CodeMirror-scrollbar-filler,
|
||||
.CodeMirror-gutter-filler {
|
||||
position: absolute;
|
||||
z-index: 6;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.CodeMirror-vscrollbar {
|
||||
top: 0;
|
||||
right: 0;
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.CodeMirror-hscrollbar {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
overflow-x: scroll;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.CodeMirror-scrollbar-filler {
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.CodeMirror-gutter-filler {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.CodeMirror-gutter {
|
||||
display: inline-block;
|
||||
height: 100%;
|
||||
margin-bottom: -30px;
|
||||
white-space: normal;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.CodeMirror-gutter-wrapper {
|
||||
position: absolute;
|
||||
z-index: 4;
|
||||
background: none !important;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.CodeMirror-gutter-background {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
.CodeMirror-gutter-elt {
|
||||
position: absolute;
|
||||
z-index: 4;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.CodeMirror-gutter-wrapper ::selection {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.CodeMirrorwrapper ::selection {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.CodeMirror pre {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
padding: 0 4px; /* Horizontal padding of content */
|
||||
margin: 0;
|
||||
overflow: visible;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
color: inherit;
|
||||
word-wrap: normal;
|
||||
white-space: pre;
|
||||
background: transparent;
|
||||
border-width: 0;
|
||||
|
||||
/* Reset some styles that the rest of the page might have set */
|
||||
border-radius: 0;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
font-variant-ligatures: contextual;
|
||||
}
|
||||
|
||||
.CodeMirror-wrap pre {
|
||||
word-break: normal;
|
||||
word-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.CodeMirror-linebackground {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.CodeMirror-linewidget {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
padding: 0.1px; /* Force widget margins to stay inside of the container */
|
||||
}
|
||||
|
||||
.CodeMirror-rtl pre {
|
||||
direction: rtl;
|
||||
}
|
||||
|
||||
.CodeMirror-code {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* Force content-box sizing for the elements where we expect it */
|
||||
.CodeMirror-scroll,
|
||||
.CodeMirror-sizer,
|
||||
.CodeMirror-gutter,
|
||||
.CodeMirror-gutters,
|
||||
.CodeMirror-linenumber {
|
||||
box-sizing: content-box;
|
||||
}
|
||||
|
||||
.CodeMirror-measure {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.CodeMirror-measure pre {
|
||||
position: static;
|
||||
}
|
||||
|
||||
div.CodeMirror-cursors {
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
div.CodeMirror-dragcursors {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.CodeMirror-focused div.CodeMirror-cursors {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.CodeMirror-selected {
|
||||
background: #d9d9d9;
|
||||
}
|
||||
|
||||
.CodeMirror-focused .CodeMirror-selected {
|
||||
background: #d7d4f0;
|
||||
}
|
||||
|
||||
.CodeMirror-crosshair {
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
.CodeMirror-line::selection,
|
||||
.CodeMirror-line > span::selection,
|
||||
.CodeMirror-line > span > span::selection {
|
||||
background: #d7d4f0;
|
||||
}
|
||||
|
||||
.cm-searching {
|
||||
background-color: #ffa;
|
||||
background-color: rgb(255 255 0 / 40%);
|
||||
}
|
||||
|
||||
/* Used to force a border model for a node */
|
||||
.cm-force-border {
|
||||
padding-right: 0.1px;
|
||||
}
|
||||
|
||||
@media print {
|
||||
/* Hide the cursor when printing */
|
||||
.CodeMirror div.CodeMirror-cursors {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
/* See issue #2901 */
|
||||
.cm-tab-wrap-hack::after {
|
||||
content: '';
|
||||
}
|
||||
|
||||
/* Help users use markselection to safely style text background */
|
||||
span.CodeMirror-selectedtext {
|
||||
background: none;
|
||||
}
|
||||
12
src/components/CodeEditor/src/json-preview/JsonPreview.vue
Normal file
12
src/components/CodeEditor/src/json-preview/JsonPreview.vue
Normal file
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<vue-json-pretty :path="'res'" :deep="3" :showLength="true" :data="data" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import VueJsonPretty from 'vue-json-pretty';
|
||||
import 'vue-json-pretty/lib/styles.css';
|
||||
|
||||
defineProps({
|
||||
data: Object,
|
||||
});
|
||||
</script>
|
||||
139
src/components/CodeEditor/src/monacoEditor/MonacoEditor.vue
Normal file
139
src/components/CodeEditor/src/monacoEditor/MonacoEditor.vue
Normal file
@@ -0,0 +1,139 @@
|
||||
<template>
|
||||
<div ref="codeEditBox" class="codeEditBox"></div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||
import { editorProps, defaultOptions } from './monacoEditorType';
|
||||
import jsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker';
|
||||
import cssWorker from 'monaco-editor/esm/vs/language/css/css.worker?worker';
|
||||
import htmlWorker from 'monaco-editor/esm/vs/language/html/html.worker?worker';
|
||||
import tsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker';
|
||||
import EditorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker';
|
||||
import * as monaco from 'monaco-editor';
|
||||
import { useAppStore } from '@/store/modules/app';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'monacoEditor',
|
||||
props: editorProps,
|
||||
emits: ['update:modelValue', 'change', 'editor-mounted'],
|
||||
setup(props, { emit, expose }) {
|
||||
const appStore = useAppStore();
|
||||
self.MonacoEnvironment = {
|
||||
getWorker(_: string, label: string) {
|
||||
if (label === 'json') {
|
||||
return new jsonWorker();
|
||||
}
|
||||
if (['css', 'scss', 'less'].includes(label)) {
|
||||
return new cssWorker();
|
||||
}
|
||||
if (['html', 'handlebars', 'razor'].includes(label)) {
|
||||
return new htmlWorker();
|
||||
}
|
||||
if (['typescript', 'javascript'].includes(label)) {
|
||||
return new tsWorker();
|
||||
}
|
||||
return new EditorWorker();
|
||||
},
|
||||
};
|
||||
let editor: monaco.editor.IStandaloneCodeEditor;
|
||||
const codeEditBox = ref();
|
||||
|
||||
const init = () => {
|
||||
monaco.languages.typescript.javascriptDefaults.setDiagnosticsOptions({
|
||||
noSemanticValidation: true,
|
||||
noSyntaxValidation: false,
|
||||
});
|
||||
monaco.languages.typescript.javascriptDefaults.setCompilerOptions({
|
||||
target: monaco.languages.typescript.ScriptTarget.ES2020,
|
||||
allowNonTsExtensions: true,
|
||||
});
|
||||
|
||||
editor = monaco.editor.create(codeEditBox.value, {
|
||||
value: props.modelValue,
|
||||
language: props.language,
|
||||
theme: appStore.getDarkMode === 'light' ? 'vs' : 'vs-dark',
|
||||
...{ ...defaultOptions, ...props.options },
|
||||
});
|
||||
|
||||
// 监听值的变化
|
||||
editor.onDidChangeModelContent(() => {
|
||||
const value = editor.getValue(); //给父组件实时返回最新文本
|
||||
emit('update:modelValue', value);
|
||||
emit('change', value);
|
||||
});
|
||||
|
||||
emit('editor-mounted', editor);
|
||||
};
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
newValue => {
|
||||
if (editor) {
|
||||
const value = editor.getValue();
|
||||
if (newValue !== value) {
|
||||
editor.setValue(newValue);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.options,
|
||||
newValue => {
|
||||
editor.updateOptions({ ...defaultOptions, ...newValue });
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.language,
|
||||
newValue => {
|
||||
monaco.editor.setModelLanguage(editor.getModel()!, newValue);
|
||||
},
|
||||
);
|
||||
watch(
|
||||
() => appStore.getDarkMode,
|
||||
async () => {
|
||||
monaco.editor.setTheme(appStore.getDarkMode === 'light' ? 'vs' : 'vs-dark');
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
},
|
||||
);
|
||||
|
||||
function insert(text) {
|
||||
text = text || '';
|
||||
const position = editor.getPosition();
|
||||
editor.executeEdits('', [
|
||||
{
|
||||
range: {
|
||||
startLineNumber: position?.lineNumber as number,
|
||||
startColumn: position?.column as number,
|
||||
endLineNumber: position?.lineNumber as number,
|
||||
endColumn: position?.column as number,
|
||||
},
|
||||
text: text,
|
||||
},
|
||||
]);
|
||||
}
|
||||
expose({ insert });
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
editor.dispose();
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
init();
|
||||
});
|
||||
|
||||
return { codeEditBox };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.codeEditBox {
|
||||
width: v-bind(width);
|
||||
height: v-bind(height);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,69 @@
|
||||
import { PropType } from 'vue';
|
||||
|
||||
export type Theme = 'vs' | 'hc-black' | 'vs-dark';
|
||||
export type FoldingStrategy = 'auto' | 'indentation';
|
||||
export type RenderLineHighlight = 'all' | 'line' | 'none' | 'gutter';
|
||||
export type LineNumbersType = 'on' | 'off' | 'relative' | 'interval' | ((lineNumber: number) => string);
|
||||
export type WordWrapType = 'off' | 'on' | 'wordWrapColumn' | 'bounded';
|
||||
export interface Options {
|
||||
automaticLayout: boolean; // 自适应布局
|
||||
foldingStrategy: FoldingStrategy; // 折叠方式 auto | indentation
|
||||
renderLineHighlight: RenderLineHighlight; // 行亮
|
||||
lineNumbers: LineNumbersType; // 显示行号
|
||||
wordWrap: WordWrapType;
|
||||
selectOnLineNumbers: boolean;
|
||||
minimap: {
|
||||
// 关闭小地图
|
||||
enabled: boolean;
|
||||
};
|
||||
readOnly: boolean; // 只读
|
||||
fontSize: number; // 字体大小
|
||||
scrollBeyondLastLine: boolean; // 取消代码后面一大段空白
|
||||
overviewRulerBorder: boolean; // 不要滚动条的边框
|
||||
}
|
||||
|
||||
export const defaultOptions: Options = {
|
||||
automaticLayout: true,
|
||||
foldingStrategy: 'indentation',
|
||||
renderLineHighlight: 'all',
|
||||
lineNumbers: 'on',
|
||||
wordWrap: 'on',
|
||||
selectOnLineNumbers: true,
|
||||
minimap: {
|
||||
enabled: false,
|
||||
},
|
||||
readOnly: false,
|
||||
fontSize: 14,
|
||||
scrollBeyondLastLine: false,
|
||||
overviewRulerBorder: false,
|
||||
};
|
||||
|
||||
export const editorProps = {
|
||||
modelValue: {
|
||||
type: String as PropType<string>,
|
||||
default: null,
|
||||
},
|
||||
width: {
|
||||
type: [String, Number] as PropType<string | number>,
|
||||
default: '100%',
|
||||
},
|
||||
height: {
|
||||
type: [String, Number] as PropType<string | number>,
|
||||
default: '100%',
|
||||
},
|
||||
language: {
|
||||
type: String as PropType<string>,
|
||||
default: 'javascript',
|
||||
},
|
||||
theme: {
|
||||
type: String as PropType<Theme>,
|
||||
validator(value: string): boolean {
|
||||
return ['vs', 'hc-black', 'vs-dark'].includes(value);
|
||||
},
|
||||
default: 'vs',
|
||||
},
|
||||
options: {
|
||||
type: Object as PropType<Options>,
|
||||
default: () => defaultOptions,
|
||||
},
|
||||
};
|
||||
5
src/components/CodeEditor/src/typing.ts
Normal file
5
src/components/CodeEditor/src/typing.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export enum MODE {
|
||||
JSON = 'application/json',
|
||||
HTML = 'htmlmixed',
|
||||
JS = 'javascript',
|
||||
}
|
||||
3
src/components/ColumnDesign/index.ts
Normal file
3
src/components/ColumnDesign/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import BasicColumnDesign from './src/BasicColumnDesign.vue';
|
||||
|
||||
export { BasicColumnDesign };
|
||||
60
src/components/ColumnDesign/src/BasicColumnDesign.vue
Normal file
60
src/components/ColumnDesign/src/BasicColumnDesign.vue
Normal file
@@ -0,0 +1,60 @@
|
||||
<template>
|
||||
<div :class="`${prefixCls}`">
|
||||
<div class="h-full" v-show="formInfo.webType == 2 || formInfo.webType == 4">
|
||||
<column-main ref="columnMain" v-bind="getBindValue" v-show="showType === 'pc'" />
|
||||
<column-main-app ref="columnMainApp" v-bind="getAppBindValue" v-show="showType === 'app'" />
|
||||
<div class="head-tabs">
|
||||
<a-button pre-icon="icon-ym icon-ym-pc" type="link" :class="{ 'unActive-btn': showType != 'pc' }" @click="showType = 'pc'">桌面端</a-button>
|
||||
<a-button pre-icon="icon-ym icon-ym-mobile" type="link" :class="{ 'unActive-btn': showType != 'app' }" @click="showType = 'app'">移动端</a-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column-empty-box" v-show="formInfo.webType == 1">
|
||||
<img :src="getEmptyImg" class="empty-img" />
|
||||
<p>开启后,表单带有数据列表</p>
|
||||
<a-button type="primary" class="w-150px" size="large" @click="toggleWebType">开启列表</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref, unref, computed } from 'vue';
|
||||
import { useDesign } from '@/hooks/web/useDesign';
|
||||
import ColumnMain from './components/Main.vue';
|
||||
import ColumnMainApp from './components/MainApp.vue';
|
||||
import emptyImg from '@/assets/images/generator/columnType1.png';
|
||||
import emptyImgDark from '@/assets/images/generator/columnType1-dark.png';
|
||||
import { omit } from 'lodash-es';
|
||||
import { useAppStore } from '@/store/modules/app';
|
||||
|
||||
const props = defineProps(['columnData', 'appColumnData', 'formInfo', 'viewFields', 'interfaceParam', 'interfaceHasPage']);
|
||||
const emit = defineEmits(['toggleWebType']);
|
||||
defineExpose({ getData });
|
||||
const { prefixCls } = useDesign('basic-column-design');
|
||||
|
||||
const columnMain = ref<any>(null);
|
||||
const columnMainApp = ref<any>(null);
|
||||
const showType = ref('pc');
|
||||
const appStore = useAppStore();
|
||||
|
||||
const getBindValue = computed(() => ({ ...omit(props, ['columnData', 'appColumnData']), conf: props.columnData }));
|
||||
const getAppBindValue = computed(() => ({ ...omit(props, ['columnData', 'appColumnData']), conf: props.appColumnData }));
|
||||
const getEmptyImg = computed(() => (appStore.getDarkMode === 'light' ? emptyImg : emptyImgDark));
|
||||
|
||||
// 供父组件使用 获取表单JSON
|
||||
function getData() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const columnData = unref(columnMain).getData();
|
||||
if (!columnData) reject({ msg: '', target: 2 });
|
||||
const appColumnData = unref(columnMainApp).getData();
|
||||
if (!appColumnData.columnList || !appColumnData.columnList.length) {
|
||||
appColumnData.columnList = columnData.columnList;
|
||||
}
|
||||
resolve({ columnData: columnData, appColumnData: appColumnData, target: 2 });
|
||||
});
|
||||
}
|
||||
function toggleWebType() {
|
||||
emit('toggleWebType', 2);
|
||||
}
|
||||
</script>
|
||||
<style lang="less">
|
||||
@import '../style/index.less';
|
||||
</style>
|
||||
431
src/components/ColumnDesign/src/components/BtnEvent.vue
Normal file
431
src/components/ColumnDesign/src/components/BtnEvent.vue
Normal file
@@ -0,0 +1,431 @@
|
||||
<template>
|
||||
<BasicModal
|
||||
v-bind="$attrs"
|
||||
@register="registerModal"
|
||||
title="按钮事件配置"
|
||||
helpMessage="小程序不支持在线JS脚本"
|
||||
:width="800"
|
||||
@ok="handleSubmit"
|
||||
destroyOnClose
|
||||
class="btn-event-modal">
|
||||
<a-form :colon="false" labelAlign="left" :labelCol="{ style: { width: '90px' } }" :model="dataForm" :rules="formRules" ref="formElRef" hideRequiredMark>
|
||||
<a-form-item label="按钮位置" name="position">
|
||||
<a-radio-group v-model:value="dataForm.position" @change="onPositionChange">
|
||||
<a-radio :value="1">列表中</a-radio>
|
||||
<a-radio :value="2">列表头部<BasicHelp text="树形表格自定义按钮不支持列表头部" /></a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<a-form-item label="按钮图标" v-show="dataForm.position === 2">
|
||||
<yunzhupaas-icon-picker v-model:value="dataForm.btnIcon" placeholder="请选择" />
|
||||
</a-form-item>
|
||||
<a-form-item label="必选提示" v-show="dataForm.position === 2">
|
||||
<a-switch v-model:checked="dataForm.dataRequired" />
|
||||
</a-form-item>
|
||||
<a-form-item label="启用规则" v-show="dataForm.position !== 2">
|
||||
<a-button block @click="handleEnableRule">启用规则配置</a-button>
|
||||
</a-form-item>
|
||||
<yunzhupaas-group-title content="动作设置" :bordered="false" />
|
||||
<a-form-item label="类型选择" name="btnType">
|
||||
<a-radio-group v-model:value="dataForm.btnType" @change="onBtnTypeChange">
|
||||
<a-radio :value="1" v-if="dataForm.position != 2">弹窗配置</a-radio>
|
||||
<a-radio :value="2">JS脚本</a-radio>
|
||||
<a-radio :value="3">数据接口</a-radio>
|
||||
<a-radio :value="4">发起审批</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<template v-if="dataForm.btnType == 1">
|
||||
<a-form-item label="选择表单" name="modelId">
|
||||
<yunzhupaas-tree-select v-model:value="dataForm.modelId" :options="treeData" placeholder="请选择" lastLevel allowClear @change="onModeIdChange" />
|
||||
</a-form-item>
|
||||
<a-form-item label="弹窗标题" name="popupTitle">
|
||||
<a-input v-model:value="dataForm.popupTitle" placeholder="请输入" allowClear />
|
||||
</a-form-item>
|
||||
<a-form-item label="弹窗类型" name="popupType" v-if="showType === 'pc'">
|
||||
<yunzhupaas-select v-model:value="dataForm.popupType" placeholder="请选择" :options="popupTypeOptions" />
|
||||
</a-form-item>
|
||||
<a-form-item label="弹窗宽度" name="popupWidth" v-if="showType === 'pc'">
|
||||
<a-select v-model:value="dataForm.popupWidth">
|
||||
<a-select-option v-for="item in popupWidthOptions" :key="item" :value="item">{{ item }}</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="赋值规则" style="margin-bottom: 0"></a-form-item>
|
||||
<a-table :data-source="dataForm.formOptions" :columns="formOptionsColumns" size="small" :pagination="false">
|
||||
<template #bodyCell="{ column, record, index }">
|
||||
<template v-if="column.key === 'currentField'">
|
||||
<yunzhupaas-select
|
||||
v-model:value="record.currentField"
|
||||
placeholder="请选择"
|
||||
:options="formFieldsOptions1"
|
||||
:fieldNames="{ options: 'options1' }"
|
||||
allowClear
|
||||
showSearch />
|
||||
</template>
|
||||
<template v-if="column.key === 'type'">赋值给</template>
|
||||
<template v-if="column.key === 'field'">
|
||||
<yunzhupaas-select v-model:value="record.field" placeholder="请选择" :options="fieldOptions" showSearch @dropdownVisibleChange="visibleChange" />
|
||||
</template>
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-button class="action-btn" type="link" color="error" @click="handleDelItem(index)" size="small">删除</a-button>
|
||||
</template>
|
||||
</template>
|
||||
<template #emptyText>
|
||||
<p class="leading-60px">暂无数据</p>
|
||||
</template>
|
||||
</a-table>
|
||||
<div class="table-add-action mb-20px" @click="handleAddFormOptions()">
|
||||
<a-button type="link" preIcon="icon-ym icon-ym-btn-add">新增</a-button>
|
||||
</div>
|
||||
<a-form-item label="自定义按钮事件" :labelCol="{ style: { width: '110px' } }">
|
||||
<a-switch v-model:checked="dataForm.customBtn" />
|
||||
<span class="tip">(开启后,弹窗中按钮事件失效,调用接口事件。)</span>
|
||||
</a-form-item>
|
||||
<template v-if="dataForm.customBtn">
|
||||
<a-form-item label="数据接口" name="interfaceId">
|
||||
<interface-modal :value="dataForm.interfaceId" :title="dataForm.interfaceName" :sourceType="3" @change="onInterfaceChange" />
|
||||
</a-form-item>
|
||||
<a-form-item label="参数设置" style="margin-bottom: 0"></a-form-item>
|
||||
<a-table :data-source="dataForm.templateJson" :columns="templateJsonColumns" size="small" :pagination="false" class="mb-20px">
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'field'">
|
||||
<span class="required-sign">{{ record.required ? '*' : '' }}</span>
|
||||
{{ record.field }}{{ record.fieldName ? '(' + record.fieldName + ')' : '' }}
|
||||
</template>
|
||||
<template v-if="column.key === 'sourceType'">
|
||||
<yunzhupaas-select
|
||||
v-model:value="record.sourceType"
|
||||
placeholder="请选择"
|
||||
:options="getSourceTypeOptions(record.required)"
|
||||
class="!w-100px"
|
||||
@change="onSourceTypeChange(record)" />
|
||||
</template>
|
||||
<template v-if="column.key === 'relationField'">
|
||||
<yunzhupaas-select
|
||||
v-model:value="record.relationField"
|
||||
placeholder="请选择"
|
||||
:options="fieldOptions"
|
||||
allowClear
|
||||
showSearch
|
||||
@dropdownVisibleChange="visibleChange"
|
||||
v-if="record.sourceType === 1" />
|
||||
<template v-else-if="record.sourceType == 2">
|
||||
<yunzhupaas-input-number v-if="['int', 'decimal'].includes(record.dataType)" v-model:value="record.relationField" placeholder="请输入" allowClear />
|
||||
<yunzhupaas-date-picker
|
||||
v-else-if="record.dataType == 'datetime'"
|
||||
class="!w-full"
|
||||
v-model:value="record.relationField"
|
||||
placeholder="请选择"
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
allowClear />
|
||||
<a-input v-else v-model:value="record.relationField" placeholder="请输入" allowClear />
|
||||
</template>
|
||||
<yunzhupaas-select
|
||||
v-model:value="record.relationField"
|
||||
placeholder="请选择"
|
||||
:options="interfaceSystemOptions"
|
||||
:fieldNames="{ options: 'options1' }"
|
||||
allowClear
|
||||
class="!w-204px"
|
||||
:disabled="record.disabled"
|
||||
v-else-if="record.sourceType === 4" />
|
||||
</template>
|
||||
</template>
|
||||
<template #emptyText>
|
||||
<p class="leading-60px">暂无数据</p>
|
||||
</template>
|
||||
</a-table>
|
||||
</template>
|
||||
<a-form-item label="刷新数据列表" :labelCol="{ style: { width: '110px' } }">
|
||||
<a-switch v-model:checked="dataForm.isRefresh" />
|
||||
</a-form-item>
|
||||
</template>
|
||||
<template v-if="dataForm.btnType == 2">
|
||||
<div class="form-script-editor">
|
||||
<MonacoEditor v-model="dataForm.func" />
|
||||
</div>
|
||||
<div class="form-script-tips">
|
||||
<p>支持JavaScript的脚本,<ScriptDemo type="btnEvent" /></p>
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="dataForm.btnType == 3">
|
||||
<a-form-item label="数据接口" name="interfaceId">
|
||||
<interface-modal :value="dataForm.interfaceId" :title="dataForm.interfaceName" :sourceType="3" @change="onInterfaceChange" />
|
||||
</a-form-item>
|
||||
<a-form-item label="参数设置" style="margin-bottom: 0"></a-form-item>
|
||||
<a-table :data-source="dataForm.templateJson" :columns="templateJsonColumns" size="small" :pagination="false" class="mb-20px">
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'field'">
|
||||
<span class="required-sign">{{ record.required ? '*' : '' }}</span>
|
||||
{{ record.field }}{{ record.fieldName ? '(' + record.fieldName + ')' : '' }}
|
||||
</template>
|
||||
<template v-if="column.key === 'sourceType'">
|
||||
<yunzhupaas-select
|
||||
v-model:value="record.sourceType"
|
||||
placeholder="请选择"
|
||||
:options="getSourceTypeOptions(record.required)"
|
||||
class="!w-100px"
|
||||
@change="onSourceTypeChange(record)" />
|
||||
</template>
|
||||
<template v-if="column.key === 'relationField'">
|
||||
<yunzhupaas-select
|
||||
v-model:value="record.relationField"
|
||||
placeholder="请选择"
|
||||
:options="formFieldsOptions"
|
||||
:fieldNames="{ options: 'options1' }"
|
||||
allowClear
|
||||
showSearch
|
||||
v-if="record.sourceType === 1" />
|
||||
<template v-else-if="record.sourceType == 2">
|
||||
<yunzhupaas-input-number v-if="['int', 'decimal'].includes(record.dataType)" v-model:value="record.relationField" placeholder="请输入" allowClear />
|
||||
<yunzhupaas-date-picker
|
||||
v-else-if="record.dataType == 'datetime'"
|
||||
class="!w-full"
|
||||
v-model:value="record.relationField"
|
||||
placeholder="请选择"
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
allowClear />
|
||||
<a-input v-else v-model:value="record.relationField" placeholder="请输入" allowClear />
|
||||
</template>
|
||||
<yunzhupaas-select
|
||||
v-model:value="record.relationField"
|
||||
placeholder="请选择"
|
||||
:options="interfaceSystemOptions"
|
||||
:fieldNames="{ options: 'options1' }"
|
||||
allowClear
|
||||
class="!w-204px"
|
||||
v-else-if="record.sourceType === 4" />
|
||||
</template>
|
||||
</template>
|
||||
<template #emptyText>
|
||||
<p class="leading-60px">暂无数据</p>
|
||||
</template>
|
||||
</a-table>
|
||||
<a-form-item label="启用确认框">
|
||||
<a-switch v-model:checked="dataForm.useConfirm" />
|
||||
</a-form-item>
|
||||
<a-form-item name="confirmTitle" v-if="dataForm.useConfirm">
|
||||
<a-input v-model:value="dataForm.confirmTitle" placeholder="请输入" allowClear />
|
||||
</a-form-item>
|
||||
<a-form-item label="刷新数据列表">
|
||||
<a-switch v-model:checked="dataForm.isRefresh" />
|
||||
</a-form-item>
|
||||
</template>
|
||||
<template v-if="dataForm.btnType == 4">
|
||||
<LaunchFlowCfg :dataForm="dataForm" :allFormFieldsOptions="allFormFieldsOptions" />
|
||||
</template>
|
||||
</a-form>
|
||||
</BasicModal>
|
||||
<FormScript @register="registerScriptModal" @confirm="updateScript" />
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref, toRefs } from 'vue';
|
||||
import { BasicModal, useModalInner, useModal } from '@/components/Modal';
|
||||
import { MonacoEditor } from '@/components/CodeEditor';
|
||||
import { getVisualDevSelector, getFormDataFields } from '@/api/onlineDev/visualDev';
|
||||
import { useMessage } from '@/hooks/web/useMessage';
|
||||
import { InterfaceModal } from '@/components/CommonModal';
|
||||
import type { FormInstance } from 'ant-design-vue';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import ScriptDemo from '@/components/FormGenerator/src/components/ScriptDemo.vue';
|
||||
import { noAllowSelectList } from '@/components/FormGenerator/src/helper/config';
|
||||
import { sourceTypeOptions, interfaceSystemOptions, templateJsonColumns } from '@/components/FlowProcess/src/helper/define';
|
||||
import FormScript from './FormScript.vue';
|
||||
import { defaultBtnEnableFunc } from '../helper/config';
|
||||
import LaunchFlowCfg from '@/components/FormGenerator/src/components/LaunchFlowCfg.vue';
|
||||
|
||||
const emit = defineEmits(['register', 'confirm']);
|
||||
const { createMessage } = useMessage();
|
||||
const [registerModal, { closeModal }] = useModalInner(init);
|
||||
const [registerScriptModal, { openModal: openScriptModal }] = useModal();
|
||||
const popupTypeOptions = [
|
||||
{ id: 'dialog', fullName: '居中弹窗' },
|
||||
{ id: 'drawer', fullName: '右侧弹窗' },
|
||||
];
|
||||
const popupWidthOptions = ['600px', '800px', '1000px', '40%', '50%', '60%', '70%', '80%'];
|
||||
const formOptionsColumns = [
|
||||
{ width: 50, title: '序号', align: 'center', customRender: ({ index }) => index + 1 },
|
||||
{ title: '当前表单值', dataIndex: 'currentField', key: 'currentField', width: 250 },
|
||||
{ title: '赋值给', dataIndex: 'type', key: 'type', align: 'center' },
|
||||
{ title: '弹窗表单值', dataIndex: 'field', key: 'field', width: 250 },
|
||||
{ title: '操作', dataIndex: 'action', key: 'action', width: 50 },
|
||||
];
|
||||
const defaultData = {
|
||||
btnType: 1,
|
||||
modelId: '',
|
||||
popupTitle: '自定义操作',
|
||||
popupType: 'dialog',
|
||||
popupWidth: '800px',
|
||||
formOptions: [],
|
||||
customBtn: false,
|
||||
func: '({ data, index, refresh, onlineUtils }) => {\r\n \r\n}',
|
||||
enableFunc: defaultBtnEnableFunc,
|
||||
interfaceId: '',
|
||||
interfaceName: '',
|
||||
templateJson: [],
|
||||
useConfirm: false,
|
||||
confirmTitle: '此操作将通过接口处理',
|
||||
isRefresh: false,
|
||||
position: 1,
|
||||
btnShowType: '',
|
||||
btnIcon: '',
|
||||
launchFlow: {
|
||||
flowId: '',
|
||||
flowName: '',
|
||||
currentUser: 1,
|
||||
customUser: 0,
|
||||
initiator: [],
|
||||
transferList: [],
|
||||
formFieldList: [],
|
||||
},
|
||||
dataRequired: true,
|
||||
};
|
||||
const showType = ref('pc');
|
||||
const state = reactive({
|
||||
dataForm: cloneDeep(defaultData),
|
||||
formRules: {
|
||||
btnType: [{ required: true, message: '必填', trigger: 'change' }],
|
||||
modelId: [{ required: true, message: '必填', trigger: 'change' }],
|
||||
popupTitle: [{ required: true, message: '必填', trigger: 'change' }],
|
||||
interfaceId: [{ required: true, message: '必填', trigger: 'change' }],
|
||||
confirmTitle: [{ required: true, message: '必填', trigger: 'change' }],
|
||||
},
|
||||
});
|
||||
const { dataForm, formRules } = toRefs(state);
|
||||
const treeData = ref([]);
|
||||
const fieldOptions = ref<any[]>([]);
|
||||
const formFieldsOptions = ref<any[]>([]);
|
||||
const formFieldsOptions1 = ref<any[]>([]);
|
||||
const allFormFieldsOptions = ref<any[]>([]);
|
||||
const formElRef = ref<FormInstance>();
|
||||
const noAllowFormFieldsList = noAllowSelectList.filter(o => o != 'billRule');
|
||||
|
||||
function init(data) {
|
||||
showType.value = data.showType || 'pc';
|
||||
state.dataForm = { ...cloneDeep(defaultData), ...cloneDeep(data.dataForm) };
|
||||
formFieldsOptions.value = cloneDeep(data.formFieldsOptions)
|
||||
.filter(o => !noAllowFormFieldsList.includes(o.__config__.yunzhupaasKey) && o.id.indexOf('-') < 0)
|
||||
.map(o => ({ ...o, disabled: false }));
|
||||
formFieldsOptions1.value = [{ fullName: '@表单ID', id: '@formId' }, ...formFieldsOptions.value];
|
||||
allFormFieldsOptions.value = [{ fullName: '@表单ID', id: '@formId' }, ...cloneDeep(data.formFieldsOptions).map(o => ({ ...o, disabled: false }))];
|
||||
getFeatureList();
|
||||
getFieldOptions();
|
||||
}
|
||||
function getFeatureList() {
|
||||
getVisualDevSelector({ type: 1, webType: '1,2', enableFlow: 0, isRelease: 1 }).then(res => {
|
||||
treeData.value = res.data.list;
|
||||
});
|
||||
}
|
||||
function onPositionChange() {
|
||||
if (state.dataForm.position !== 2 || state.dataForm.btnType !== 1) return;
|
||||
state.dataForm.btnType = 2;
|
||||
onBtnTypeChange();
|
||||
}
|
||||
function onBtnTypeChange() {
|
||||
const data: any = {
|
||||
modelId: '',
|
||||
popupTitle: '自定义操作',
|
||||
popupType: 'dialog',
|
||||
popupWidth: '800px',
|
||||
formOptions: [],
|
||||
customBtn: false,
|
||||
func: '({ data, index, refresh, onlineUtils }) => {\r\n \r\n}',
|
||||
interfaceId: '',
|
||||
interfaceName: '',
|
||||
templateJson: [],
|
||||
useConfirm: false,
|
||||
confirmTitle: '此操作将通过接口处理',
|
||||
isRefresh: false,
|
||||
};
|
||||
state.dataForm = { ...state.dataForm, ...data };
|
||||
state.dataForm.formOptions = [];
|
||||
state.dataForm.templateJson = [];
|
||||
fieldOptions.value = [];
|
||||
formElRef.value?.clearValidate();
|
||||
}
|
||||
function onModeIdChange(val) {
|
||||
clearField();
|
||||
if (!val) {
|
||||
fieldOptions.value = [];
|
||||
return;
|
||||
}
|
||||
getFieldOptions();
|
||||
}
|
||||
function clearField() {
|
||||
state.dataForm.formOptions = (state.dataForm.formOptions as any[]).map(o => ({ ...o, field: '' })) as any;
|
||||
state.dataForm.templateJson = (state.dataForm.templateJson as any[]).map(o => ({ ...o, relationField: o.sourceType === 1 ? '' : o.relationField })) as any;
|
||||
}
|
||||
function getFieldOptions() {
|
||||
if (!state.dataForm.modelId) return;
|
||||
getFormDataFields(state.dataForm.modelId, 1).then(res => {
|
||||
fieldOptions.value = res.data.list.map(o => ({
|
||||
...o,
|
||||
id: o.vmodel,
|
||||
fullName: o.label,
|
||||
}));
|
||||
});
|
||||
}
|
||||
function handleAddFormOptions() {
|
||||
(state.dataForm.formOptions as any).push({
|
||||
currentField: '',
|
||||
field: '',
|
||||
});
|
||||
}
|
||||
function handleDelItem(index) {
|
||||
state.dataForm.formOptions.splice(index, 1);
|
||||
}
|
||||
function visibleChange(val) {
|
||||
if (!val) return;
|
||||
if (!state.dataForm.modelId) createMessage.warning('请先选择关联功能');
|
||||
}
|
||||
function onInterfaceChange(id, row) {
|
||||
if (!id) {
|
||||
state.dataForm.interfaceId = '';
|
||||
state.dataForm.interfaceName = '';
|
||||
state.dataForm.templateJson = [];
|
||||
return;
|
||||
}
|
||||
if (state.dataForm.interfaceId === id) return;
|
||||
state.dataForm.interfaceId = id;
|
||||
state.dataForm.interfaceName = row.fullName;
|
||||
state.dataForm.templateJson = row.templateJson ? row.templateJson.map(o => ({ ...o, relationField: '', sourceType: 1 })) : [];
|
||||
}
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
const values = await formElRef.value?.validate();
|
||||
if (!values) return;
|
||||
if (state.dataForm.btnType == 1) {
|
||||
if (!state.dataForm.formOptions.length) return createMessage.warning('赋值规则不能为空');
|
||||
if (state.dataForm.formOptions.length) {
|
||||
for (let i = 0; i < state.dataForm.formOptions.length; i++) {
|
||||
const e: any = state.dataForm.formOptions[i];
|
||||
if (!e.currentField) return createMessage.warning(`赋值规则第${i + 1}行当前表单值不能为空`);
|
||||
if (!e.field) return createMessage.warning(`赋值规则第${i + 1}行弹窗表单值不能为空`);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (state.dataForm.btnType == 4) {
|
||||
const launchFlow = state.dataForm.launchFlow;
|
||||
if (!launchFlow.transferList.length) return createMessage.warning('请至少设置一个字段');
|
||||
for (let i = 0; i < launchFlow.transferList.length; i++) {
|
||||
const e: any = launchFlow.transferList[i];
|
||||
if (!e.targetField) return createMessage.warning(`第${i + 1}行目标表单值不能为空`);
|
||||
if (!e.sourceValue) return createMessage.warning(`第${i + 1}行值不能为空`);
|
||||
}
|
||||
if (launchFlow.customUser && !launchFlow.initiator?.length) return createMessage.warning('请选择自定义发起人');
|
||||
}
|
||||
emit('confirm', state.dataForm);
|
||||
closeModal();
|
||||
} catch (_) {}
|
||||
}
|
||||
function onSourceTypeChange(record) {
|
||||
record.relationField = record.sourceType == 4 ? interfaceSystemOptions[0]?.id : '';
|
||||
}
|
||||
function getSourceTypeOptions(isRequired) {
|
||||
return isRequired ? sourceTypeOptions.filter(o => o.id != 3) : sourceTypeOptions;
|
||||
}
|
||||
function handleEnableRule() {
|
||||
openScriptModal(true, { text: state.dataForm.enableFunc, funcName: 'btnEnableRule' });
|
||||
}
|
||||
function updateScript(data) {
|
||||
state.dataForm.enableFunc = data;
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,102 @@
|
||||
<template>
|
||||
<BasicModal v-bind="$attrs" width="1000px" class="YUNZHUPAAS-complex-header-Modal" @register="registerModal" title="复杂表头配置" @ok="handleSubmit" destroyOnClose>
|
||||
<a-table size="small" rowKey="id" class="complex-header-table" :data-source="list" :columns="columns" :pagination="false">
|
||||
<template #bodyCell="{ column, record, index }">
|
||||
<template v-if="column.key === 'fullName'">
|
||||
<yunzhupaas-i18n-input v-model:value="record.fullName" v-model:i18n="record.fullNameI18nCode" placeholder="请输入" allowClear :maxlength="50" />
|
||||
</template>
|
||||
<template v-if="column.key === 'childColumns'">
|
||||
<YunzhupaasSelect
|
||||
v-model:value="record.childColumns"
|
||||
:fieldNames="{ options: 'options1' }"
|
||||
placeholder="请选择"
|
||||
multiple
|
||||
showSearch
|
||||
allowClear
|
||||
:options="getChildColumnsList(index)" />
|
||||
</template>
|
||||
<template v-if="column.key === 'align'">
|
||||
<YunzhupaasSelect v-model:value="record.align" placeholder="请选择" :options="alignList" />
|
||||
</template>
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-button class="action-btn" type="link" color="error" @click="handleDel(index)" size="small">删除</a-button>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
<div class="table-add-action" @click="handleAdd()">
|
||||
<a-button type="link" preIcon="icon-ym icon-ym-btn-add">添加</a-button>
|
||||
</div>
|
||||
</BasicModal>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { nextTick, reactive, toRefs } from 'vue';
|
||||
import { BasicModal, useModalInner } from '@/components/Modal';
|
||||
import { buildBitUUID } from '@/utils/uuid';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
|
||||
interface State {
|
||||
list: any[];
|
||||
childColumnsList: any[];
|
||||
}
|
||||
|
||||
const state = reactive<State>({
|
||||
list: [],
|
||||
childColumnsList: [],
|
||||
});
|
||||
const { list } = toRefs(state);
|
||||
const alignList = [
|
||||
{ fullName: '左对齐', id: 'left' },
|
||||
{ fullName: '居中对齐', id: 'center' },
|
||||
{ fullName: '右对齐', id: 'right' },
|
||||
];
|
||||
const emit = defineEmits(['register', 'confirm']);
|
||||
const [registerModal, { closeModal }] = useModalInner(init);
|
||||
const columns = [
|
||||
{ title: '表头列名', dataIndex: 'fullName', key: 'fullName', width: 200 },
|
||||
{ title: '子列', dataIndex: 'childColumns', key: 'childColumns', width: 330 },
|
||||
{ title: '对齐方式', dataIndex: 'align', key: 'align', width: 150 },
|
||||
{ title: '操作', dataIndex: 'action', key: 'action', width: 50 },
|
||||
];
|
||||
|
||||
function init(data) {
|
||||
state.list = cloneDeep(data.list);
|
||||
state.childColumnsList = cloneDeep(data.columnOptions).map(o => ({ ...o, disabled: false }));
|
||||
}
|
||||
function handleAdd() {
|
||||
const id = 'complex' + buildBitUUID();
|
||||
const item = { fullName: '表头列名' + id, fullNameI18nCode: '', childColumns: [], align: 'center', id };
|
||||
state.list.push(item);
|
||||
}
|
||||
function handleDel(index) {
|
||||
state.list.splice(index, 1);
|
||||
}
|
||||
function getChildColumnsList(index) {
|
||||
let options: any[] = [];
|
||||
for (let i = 0; i < state.list.length; i++) {
|
||||
const e = state.list[i];
|
||||
if (e.childColumns?.length && index !== i) options.push(...e.childColumns);
|
||||
}
|
||||
let list: any[] = state.childColumnsList.filter(o => !options.includes(o.id));
|
||||
if (list.length) list = list.map(o => ({ ...o, align: 'left' }));
|
||||
return list;
|
||||
}
|
||||
function handleSubmit() {
|
||||
emit('confirm', state.list);
|
||||
nextTick(() => closeModal());
|
||||
}
|
||||
</script>
|
||||
<style lang="less">
|
||||
.YUNZHUPAAS-complex-header-Modal {
|
||||
.ant-modal-body {
|
||||
height: 60vh;
|
||||
& > .scrollbar {
|
||||
padding: 0;
|
||||
.scrollbar__view {
|
||||
.table-add-action {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
576
src/components/ColumnDesign/src/components/ConditionMain.vue
Normal file
576
src/components/ColumnDesign/src/components/ConditionMain.vue
Normal file
@@ -0,0 +1,576 @@
|
||||
<template>
|
||||
<div class="condition-main" :class="{ 'condition-main-bordered': bordered, 'condition-main-compact': compact }">
|
||||
<div class="mb-10px" v-if="conditionList.length">
|
||||
<yunzhupaas-radio v-model:value="matchLogic" :options="logicOptions" optionType="button" button-style="solid" />
|
||||
</div>
|
||||
<div class="condition-item" v-for="(item, index) in conditionList" :key="index">
|
||||
<div class="condition-item-title">
|
||||
<div>条件组</div>
|
||||
<i class="icon-ym icon-ym-nav-close" @click="delGroup(index)"></i>
|
||||
</div>
|
||||
<div class="condition-item-content" :class="{ '!px-5px': compact }">
|
||||
<div class="condition-item-cap">
|
||||
以下条件全部执行:
|
||||
<yunzhupaas-radio v-model:value="item.logic" :options="logicOptions" optionType="button" button-style="solid" size="small" />
|
||||
</div>
|
||||
<a-row :gutter="compact ? 2 : 8" v-for="(child, childIndex) in item.groups" :key="index + childIndex" class="mb-10px">
|
||||
<a-col :span="6">
|
||||
<yunzhupaas-select
|
||||
v-model:value="child.field"
|
||||
:options="fieldOptions"
|
||||
showSearch
|
||||
allowClear
|
||||
:fieldNames="{ options: 'options1' }"
|
||||
@change="(val, data) => onFieldChange(val, data, child, index, childIndex)" />
|
||||
</a-col>
|
||||
<a-col :span="5">
|
||||
<yunzhupaas-select
|
||||
v-model:value="child.symbol"
|
||||
placeholder="运算符号"
|
||||
:options="getSymbolOptions(child.yunzhupaasKey)"
|
||||
:dropdownMatchSelectWidth="false"
|
||||
@change="(val, data) => onSymbolChange(val, data, child)" />
|
||||
</a-col>
|
||||
<a-col :span="4" v-if="showFieldValueType">
|
||||
<yunzhupaas-select
|
||||
v-model:value="child.fieldValueType"
|
||||
:options="isCustomFieldValueType ? getSourceTypeOptions(child) : sourceTypeOptions"
|
||||
:disabled="child.disabled"
|
||||
@change="child.fieldValue = undefined" />
|
||||
</a-col>
|
||||
<a-col :span="8" v-if="child.fieldValueType === 1">
|
||||
<yunzhupaas-select
|
||||
v-model:value="child.fieldValue"
|
||||
:options="valueFieldOptions"
|
||||
showSearch
|
||||
allowClear
|
||||
:fieldNames="{ options: 'children' }"
|
||||
:disabled="child.disabled" />
|
||||
</a-col>
|
||||
<a-col :span="showFieldValueType ? 8 : 12" v-if="child.fieldValueType == 2">
|
||||
<template v-if="child.yunzhupaasKey === 'inputNumber'">
|
||||
<yunzhupaas-number-range v-model:value="child.fieldValue" :precision="child.precision" :disabled="child.disabled" v-if="child.symbol == 'between'" />
|
||||
<yunzhupaas-input-number v-model:value="child.fieldValue" :precision="child.precision" :disabled="child.disabled" placeholder="请输入" v-else />
|
||||
</template>
|
||||
<template v-else-if="child.yunzhupaasKey === 'calculate'">
|
||||
<yunzhupaas-number-range
|
||||
v-model:value="child.fieldValue"
|
||||
:precision="child.precision || 0"
|
||||
:disabled="child.disabled"
|
||||
v-if="child.symbol == 'between'" />
|
||||
<yunzhupaas-input-number v-model:value="child.fieldValue" :precision="child.precision || 0" :disabled="child.disabled" placeholder="请输入" v-else />
|
||||
</template>
|
||||
<template v-else-if="['rate', 'slider'].includes(child.yunzhupaasKey)">
|
||||
<yunzhupaas-number-range
|
||||
v-model:value="child.fieldValue"
|
||||
:precision="child.yunzhupaasKey == 'rate' && child.allowHalf ? 1 : 0"
|
||||
:disabled="child.disabled"
|
||||
v-if="child.symbol == 'between'" />
|
||||
<yunzhupaas-input-number
|
||||
v-model:value="child.fieldValue"
|
||||
:precision="child.yunzhupaasKey == 'rate' && child.allowHalf ? 1 : 0"
|
||||
:disabled="child.disabled"
|
||||
placeholder="请输入"
|
||||
v-else />
|
||||
</template>
|
||||
<div class="pt-3px" v-else-if="child.yunzhupaasKey === 'switch'">
|
||||
<yunzhupaas-switch v-model:value="child.fieldValue" :disabled="child.disabled" />
|
||||
</div>
|
||||
<template v-else-if="child.yunzhupaasKey === 'colorPicker'">
|
||||
<yunzhupaas-color-picker v-model:value="child.fieldValue" size="small" :disabled="child.disabled" />
|
||||
</template>
|
||||
<template v-else-if="child.yunzhupaasKey === 'timePicker'">
|
||||
<yunzhupaas-time-range v-model:value="child.fieldValue" :format="child.format" allowClear :disabled="child.disabled" v-if="child.symbol == 'between'" />
|
||||
<yunzhupaas-time-picker v-model:value="child.fieldValue" :format="child.format" allowClear :disabled="child.disabled" v-else />
|
||||
</template>
|
||||
<template v-else-if="['datePicker', 'createTime', 'modifyTime'].includes(child.yunzhupaasKey)">
|
||||
<yunzhupaas-date-range
|
||||
v-model:value="child.fieldValue"
|
||||
:format="child.format || 'YYYY-MM-DD HH:mm:ss'"
|
||||
allowClear
|
||||
:disabled="child.disabled"
|
||||
v-if="child.symbol == 'between'" />
|
||||
<yunzhupaas-date-picker v-model:value="child.fieldValue" :format="child.format || 'YYYY-MM-DD HH:mm:ss'" allowClear :disabled="child.disabled" v-else />
|
||||
</template>
|
||||
<template v-else-if="['organizeSelect', 'currOrganize'].includes(child.yunzhupaasKey)">
|
||||
<yunzhupaas-organize-select
|
||||
v-model:value="child.fieldValue"
|
||||
allowClear
|
||||
:selectType="child.selectType"
|
||||
:ableIds="child.ableIds"
|
||||
:multiple="child.multiple"
|
||||
:disabled="child.disabled" />
|
||||
</template>
|
||||
<template v-else-if="['depSelect'].includes(child.yunzhupaasKey)">
|
||||
<yunzhupaas-dep-select
|
||||
v-model:value="child.fieldValue"
|
||||
allowClear
|
||||
:selectType="child.selectType"
|
||||
:ableIds="child.ableIds"
|
||||
:multiple="child.multiple"
|
||||
:disabled="child.disabled" />
|
||||
</template>
|
||||
<template v-else-if="child.yunzhupaasKey === 'roleSelect'">
|
||||
<yunzhupaas-role-select
|
||||
v-model:value="child.fieldValue"
|
||||
allowClear
|
||||
:selectType="child.selectType"
|
||||
:ableIds="child.ableIds"
|
||||
:multiple="child.multiple"
|
||||
:disabled="child.disabled" />
|
||||
</template>
|
||||
<template v-else-if="child.yunzhupaasKey === 'groupSelect'">
|
||||
<yunzhupaas-group-select
|
||||
v-model:value="child.fieldValue"
|
||||
allowClear
|
||||
:selectType="child.selectType"
|
||||
:ableIds="child.ableIds"
|
||||
:multiple="child.multiple"
|
||||
:disabled="child.disabled" />
|
||||
</template>
|
||||
<template v-else-if="child.yunzhupaasKey === 'posSelect'">
|
||||
<yunzhupaas-pos-select
|
||||
v-model:value="child.fieldValue"
|
||||
allowClear
|
||||
:selectType="child.selectType"
|
||||
:ableIds="child.ableIds"
|
||||
:multiple="child.multiple"
|
||||
:disabled="child.disabled" />
|
||||
</template>
|
||||
<template v-else-if="child.yunzhupaasKey === 'currPosition'">
|
||||
<yunzhupaas-pos-select v-model:value="child.fieldValue" allowClear :multiple="child.multiple" :disabled="child.disabled" />
|
||||
</template>
|
||||
<template v-else-if="['createUser', 'modifyUser'].includes(child.yunzhupaasKey)">
|
||||
<yunzhupaas-user-select v-model:value="child.fieldValue" allowClear :multiple="child.multiple" :disabled="child.disabled" />
|
||||
</template>
|
||||
<template v-else-if="['userSelect'].includes(child.yunzhupaasKey)">
|
||||
<yunzhupaas-user-select
|
||||
v-model:value="child.fieldValue"
|
||||
allowClear
|
||||
:selectType="child.selectType != 'all' && child.selectType != 'custom' ? 'all' : child.selectType"
|
||||
:ableIds="child.ableIds"
|
||||
:multiple="child.multiple"
|
||||
:disabled="child.disabled" />
|
||||
</template>
|
||||
<template v-else-if="['usersSelect'].includes(child.yunzhupaasKey)">
|
||||
<yunzhupaas-users-select
|
||||
v-model:value="child.fieldValue"
|
||||
allowClear
|
||||
:selectType="child.selectType"
|
||||
:ableIds="child.ableIds"
|
||||
:multiple="child.multiple"
|
||||
:disabled="child.disabled" />
|
||||
</template>
|
||||
<template v-else-if="child.yunzhupaasKey === 'areaSelect'">
|
||||
<yunzhupaas-area-select
|
||||
v-model:value="child.fieldValue"
|
||||
:level="child.level"
|
||||
allowClear
|
||||
:multiple="child.multiple"
|
||||
:disabled="child.disabled"
|
||||
:key="item.cellKey" />
|
||||
</template>
|
||||
<template v-else-if="['select', 'radio', 'checkbox'].includes(child.yunzhupaasKey)">
|
||||
<yunzhupaas-select
|
||||
v-model:value="child.fieldValue"
|
||||
showSearch
|
||||
allowClear
|
||||
:options="child.options"
|
||||
:fieldNames="child.props"
|
||||
:multiple="child.multiple || child.yunzhupaasKey === 'checkbox'"
|
||||
:disabled="child.disabled" />
|
||||
</template>
|
||||
<template v-else-if="child.yunzhupaasKey === 'cascader'">
|
||||
<yunzhupaas-cascader
|
||||
v-model:value="child.fieldValue"
|
||||
:options="child.options"
|
||||
:fieldNames="child.props"
|
||||
:showAllLevels="child.showAllLevels"
|
||||
showSearch
|
||||
allowClear
|
||||
placeholder="请选择"
|
||||
:multiple="child.multiple"
|
||||
:disabled="child.disabled" />
|
||||
</template>
|
||||
<template v-else-if="child.yunzhupaasKey === 'treeSelect'">
|
||||
<yunzhupaas-tree-select
|
||||
v-model:value="child.fieldValue"
|
||||
:options="child.options"
|
||||
:fieldNames="child.props"
|
||||
showSearch
|
||||
allowClear
|
||||
placeholder="请选择"
|
||||
:multiple="child.multiple"
|
||||
:disabled="child.disabled" />
|
||||
</template>
|
||||
<template v-else-if="child.yunzhupaasKey === 'relationForm'">
|
||||
<yunzhupaas-relation-form
|
||||
v-model:value="child.fieldValue"
|
||||
placeholder="请选择"
|
||||
:modelId="child.modelId"
|
||||
allowClear
|
||||
:columnOptions="child.columnOptions"
|
||||
:relationField="child.relationField"
|
||||
:hasPage="child.hasPage"
|
||||
:pageSize="child.pageSize"
|
||||
:popupType="child.popupType"
|
||||
:popupTitle="child.popupTitle"
|
||||
:popupWidth="child.popupWidth"
|
||||
:disabled="child.disabled"
|
||||
:queryType="child.queryType"
|
||||
:propsValue="child.propsValue" />
|
||||
</template>
|
||||
<template v-else-if="child.yunzhupaasKey === 'popupSelect' || child.yunzhupaasKey === 'popupTableSelect'">
|
||||
<yunzhupaas-popup-select
|
||||
v-model:value="child.fieldValue"
|
||||
placeholder="请选择"
|
||||
:interfaceId="child.interfaceId"
|
||||
allowClear
|
||||
:multiple="child.multiple"
|
||||
:columnOptions="child.columnOptions"
|
||||
:propsValue="child.propsValue"
|
||||
:templateJson="child.templateJson"
|
||||
:relationField="child.relationField"
|
||||
:hasPage="child.hasPage"
|
||||
:pageSize="child.pageSize"
|
||||
:popupType="child.popupType"
|
||||
:popupTitle="child.popupTitle"
|
||||
:popupWidth="child.popupWidth"
|
||||
:disabled="child.disabled" />
|
||||
</template>
|
||||
<template v-else-if="child.yunzhupaasKey === 'autoComplete'">
|
||||
<yunzhupaas-auto-complete
|
||||
v-model:value="child.fieldValue"
|
||||
placeholder="请输入"
|
||||
allowClear
|
||||
:interfaceId="child.interfaceId"
|
||||
:relationField="child.relationField"
|
||||
:templateJson="child.templateJson"
|
||||
:total="child.total"
|
||||
:disabled="child.disabled" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-input v-model:value="child.fieldValue" placeholder="请输入" allowClear :disabled="child.disabled" />
|
||||
</template>
|
||||
</a-col>
|
||||
<a-col :span="8" v-if="child.fieldValueType === 4">
|
||||
<yunzhupaas-select v-model:value="child.fieldValue" :options="getParameterList(child.yunzhupaasKey)" allowClear />
|
||||
</a-col>
|
||||
<a-col :span="1" class="text-center">
|
||||
<i class="icon-ym icon-ym-btn-clearn" @click="delItem(index, childIndex)" />
|
||||
</a-col>
|
||||
</a-row>
|
||||
<span class="link-text inline-block" @click="addItem(index)"><i class="icon-ym icon-ym-btn-add text-14px mr-4px"></i>添加条件</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="query-noData" v-show="!conditionList.length && isSuperQuery">
|
||||
<img src="../../../../assets/images/query-noData.png" class="noData-img" />
|
||||
<div class="noData-txt">
|
||||
<span>没有任何查询条件</span>
|
||||
<a-divider type="vertical"></a-divider>
|
||||
<span class="link-text" @click="addGroup">点击新增</span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="link-text inline-block" @click="addGroup()" v-show="conditionList.length || !isSuperQuery">
|
||||
<i class="icon-ym icon-ym-btn-add text-14px mr-4px"></i>添加条件组
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, toRefs, watch, computed } from 'vue';
|
||||
import { getDictionaryDataSelector } from '@/api/systemData/dictionary';
|
||||
import { getDataInterfaceRes } from '@/api/systemData/dataInterface';
|
||||
import { useMessage } from '@/hooks/web/useMessage';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import { dyOptionsList } from '@/components/FormGenerator/src/helper/config';
|
||||
import { YunzhupaasRelationForm } from '@/components/Yunzhupaas';
|
||||
import { isEmpty } from '@/utils/is';
|
||||
import { getParamList } from '@/utils/yunzhupaas';
|
||||
import { useDebounceFn } from '@vueuse/core';
|
||||
|
||||
interface State {
|
||||
conditionList: any;
|
||||
fieldOptions: any[];
|
||||
matchLogic: any;
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
bordered: { type: Boolean, default: false },
|
||||
isSuperQuery: { type: Boolean, default: false },
|
||||
defaultAddEmpty: { type: Boolean, default: false },
|
||||
showFieldValueType: { type: Boolean, default: false },
|
||||
valueFieldOptions: { type: Array, default: () => [] },
|
||||
compact: { type: Boolean, default: false },
|
||||
isCustomFieldValueType: { type: Boolean, default: false },
|
||||
});
|
||||
defineExpose({
|
||||
init,
|
||||
confirm,
|
||||
updateConditionList,
|
||||
});
|
||||
|
||||
const { createMessage } = useMessage();
|
||||
const notSupportList = [
|
||||
'relationFormAttr',
|
||||
'popupAttr',
|
||||
'uploadFile',
|
||||
'uploadImg',
|
||||
'colorPicker',
|
||||
'editor',
|
||||
'link',
|
||||
'button',
|
||||
'text',
|
||||
'alert',
|
||||
'table',
|
||||
'sign',
|
||||
'signature',
|
||||
];
|
||||
const emptyChildItem = {
|
||||
field: '',
|
||||
symbol: '',
|
||||
yunzhupaasKey: '',
|
||||
fieldValueType: !props.showFieldValueType || props.isCustomFieldValueType ? 2 : 1,
|
||||
fieldValue: undefined,
|
||||
fieldValueYunzhupaasKey: '',
|
||||
cellKey: +new Date(),
|
||||
};
|
||||
const emptyItem = { logic: 'and', groups: [emptyChildItem] };
|
||||
const sourceTypeOptions = [
|
||||
{ id: 1, fullName: '字段' },
|
||||
{ id: 2, fullName: '自定义' },
|
||||
];
|
||||
const logicOptions = [
|
||||
{ id: 'and', fullName: '且' },
|
||||
{ id: 'or', fullName: '或' },
|
||||
];
|
||||
const baseSymbolOptions = [
|
||||
{ id: '==', fullName: '等于' },
|
||||
{ id: '<>', fullName: '不等于' },
|
||||
{ id: 'like', fullName: '包含' },
|
||||
{ id: 'notLike', fullName: '不包含' },
|
||||
{ id: 'null', fullName: '为空' },
|
||||
{ id: 'notNull', fullName: '不为空' },
|
||||
];
|
||||
const rangeSymbolOptions = [
|
||||
{ id: '>=', fullName: '大于等于' },
|
||||
{ id: '>', fullName: '大于' },
|
||||
{ id: '==', fullName: '等于' },
|
||||
{ id: '<=', fullName: '小于等于' },
|
||||
{ id: '<', fullName: '小于' },
|
||||
{ id: '<>', fullName: '不等于' },
|
||||
{ id: 'between', fullName: '介于' },
|
||||
{ id: 'null', fullName: '为空' },
|
||||
{ id: 'notNull', fullName: '不为空' },
|
||||
];
|
||||
const selectSymbolOptions = [
|
||||
{ id: '==', fullName: '等于' },
|
||||
{ id: '<>', fullName: '不等于' },
|
||||
{ id: 'in', fullName: '包含任意一个' },
|
||||
{ id: 'notIn', fullName: '不包含任意一个' },
|
||||
{ id: 'null', fullName: '为空' },
|
||||
{ id: 'notNull', fullName: '不为空' },
|
||||
];
|
||||
const switchSymbolOptions = [
|
||||
{ id: '==', fullName: '等于' },
|
||||
{ id: '<>', fullName: '不等于' },
|
||||
];
|
||||
const locationSymbolOptions = [
|
||||
{ id: 'like', fullName: '包含' },
|
||||
{ id: 'notLike', fullName: '不包含' },
|
||||
{ id: 'null', fullName: '为空' },
|
||||
{ id: 'notNull', fullName: '不为空' },
|
||||
];
|
||||
const relationFormSymbolOptions = [...switchSymbolOptions, { id: 'null', fullName: '为空' }, { id: 'notNull', fullName: '不为空' }];
|
||||
const useRangeSymbolList = ['calculate', 'inputNumber', 'rate', 'slider', 'datePicker', 'timePicker', 'createTime', 'modifyTime'];
|
||||
const useSelectSymbolList = [
|
||||
'radio',
|
||||
'checkbox',
|
||||
'select',
|
||||
'treeSelect',
|
||||
'cascader',
|
||||
'areaSelect',
|
||||
'organizeSelect',
|
||||
'depSelect',
|
||||
'posSelect',
|
||||
'userSelect',
|
||||
'usersSelect',
|
||||
'roleSelect',
|
||||
'groupSelect',
|
||||
'createUser',
|
||||
'modifyUser',
|
||||
'currOrganize',
|
||||
'currPosition',
|
||||
'popupTableSelect',
|
||||
];
|
||||
const dateList = ['datePicker', 'createTime', 'modifyTime'];
|
||||
const organizeList = ['organizeSelect', 'currOrganize'];
|
||||
const depList = ['depSelect'];
|
||||
const positionList = ['posSelect', 'currPosition'];
|
||||
const userList = ['userSelect', 'createUser', 'modifyUser'];
|
||||
const useSwitchSymbolList = ['switch'];
|
||||
const useRelationFormSymbolList = ['relationForm', 'popupSelect'];
|
||||
const state = reactive<State>({
|
||||
conditionList: [],
|
||||
fieldOptions: [],
|
||||
matchLogic: 'and',
|
||||
});
|
||||
const { conditionList, fieldOptions, matchLogic } = toRefs(state);
|
||||
const emit = defineEmits(['confirm']);
|
||||
const onConfirm = useDebounceFn(data => {
|
||||
emit('confirm', data);
|
||||
}, 300);
|
||||
|
||||
watch(
|
||||
[() => state.conditionList, () => state.matchLogic],
|
||||
() => {
|
||||
props.compact && onConfirm({ conditionList: state.conditionList, matchLogic: state.matchLogic });
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
function init(data) {
|
||||
updateConditionList(data);
|
||||
const fieldOptions = data.fieldOptions.filter(o => !notSupportList.includes(o.__config__.yunzhupaasKey));
|
||||
state.fieldOptions = buildOptions(fieldOptions);
|
||||
if (!state.conditionList.length && props.defaultAddEmpty) addGroup();
|
||||
}
|
||||
function updateConditionList(data) {
|
||||
state.conditionList = cloneDeep(data.conditionList || []);
|
||||
state.matchLogic = data.matchLogic || 'and';
|
||||
}
|
||||
function buildOptions(componentList) {
|
||||
componentList.forEach(cur => {
|
||||
cur.disabled = false;
|
||||
const config = cur.__config__;
|
||||
if (dyOptionsList.includes(config.yunzhupaasKey)) {
|
||||
if (config.dataType === 'dictionary' && config.dictionaryType) {
|
||||
cur.options = [];
|
||||
getDictionaryDataSelector(config.dictionaryType).then(res => {
|
||||
cur.options = res.data.list;
|
||||
});
|
||||
}
|
||||
if (config.dataType === 'dynamic' && config.propsUrl) {
|
||||
cur.options = [];
|
||||
const query = { paramList: getParamList(config.templateJson) };
|
||||
getDataInterfaceRes(config.propsUrl, query).then(res => {
|
||||
cur.options = Array.isArray(res.data) ? res.data : [];
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
return componentList;
|
||||
}
|
||||
function onFieldChange(val, data, item, index, childIndex) {
|
||||
item.cellKey = +new Date();
|
||||
if (item.fieldValueType != 1) {
|
||||
item.fieldValue = undefined;
|
||||
item.fieldValueYunzhupaasKey = '';
|
||||
}
|
||||
const newItem = cloneDeep(emptyChildItem);
|
||||
for (let key of Object.keys(newItem)) {
|
||||
newItem[key] = item[key];
|
||||
}
|
||||
if (!val) {
|
||||
item.yunzhupaasKey = '';
|
||||
item.symbol = undefined;
|
||||
item.disabled = false;
|
||||
return;
|
||||
}
|
||||
item = { ...newItem, ...data };
|
||||
const config = data.__config__;
|
||||
if (item.yunzhupaasKey != config.yunzhupaasKey) item.symbol = undefined;
|
||||
item.yunzhupaasKey = data.__config__?.yunzhupaasKey || '';
|
||||
item.disabled = ['null', 'notNull'].includes(item.symbol);
|
||||
item.multiple = ['in', 'notIn'].includes(item.symbol);
|
||||
state.conditionList[index].groups[childIndex] = item;
|
||||
}
|
||||
function onSymbolChange(val, _data, item) {
|
||||
item.fieldValue = undefined;
|
||||
item.disabled = ['null', 'notNull'].includes(val);
|
||||
item.multiple = ['in', 'notIn'].includes(val);
|
||||
if (props.showFieldValueType && (['null', 'notNull'].includes(val) || (val == 'between' && dateList.includes(item.yunzhupaasKey)))) {
|
||||
item.fieldValueType = !props.showFieldValueType || props.isCustomFieldValueType ? 2 : 1;
|
||||
item.fieldValueYunzhupaasKey = '';
|
||||
}
|
||||
}
|
||||
function addItem(index) {
|
||||
state.conditionList[index].groups.push(cloneDeep(emptyChildItem));
|
||||
}
|
||||
function delItem(index, childIndex) {
|
||||
state.conditionList[index].groups.splice(childIndex, 1);
|
||||
if (!state.conditionList[index].groups.length) delGroup(index);
|
||||
}
|
||||
function addGroup() {
|
||||
state.conditionList.push(cloneDeep(emptyItem));
|
||||
}
|
||||
function delGroup(index) {
|
||||
state.conditionList.splice(index, 1);
|
||||
}
|
||||
function getSymbolOptions(yunzhupaasKey) {
|
||||
if (useSwitchSymbolList.includes(yunzhupaasKey)) return switchSymbolOptions;
|
||||
if (useRelationFormSymbolList.includes(yunzhupaasKey)) return relationFormSymbolOptions;
|
||||
if (useRangeSymbolList.includes(yunzhupaasKey)) return rangeSymbolOptions;
|
||||
if (useSelectSymbolList.includes(yunzhupaasKey)) return selectSymbolOptions;
|
||||
if (yunzhupaasKey == 'location') return locationSymbolOptions;
|
||||
return baseSymbolOptions;
|
||||
}
|
||||
function exist() {
|
||||
let isOk = true;
|
||||
for (let i = 0; i < state.conditionList.length; i++) {
|
||||
const e = state.conditionList[i];
|
||||
for (let j = 0; j < e.groups.length; j++) {
|
||||
const child = e.groups[j];
|
||||
if (!child.field) {
|
||||
createMessage.warning('条件字段不能为空');
|
||||
isOk = false;
|
||||
return;
|
||||
}
|
||||
if (!child.symbol) {
|
||||
createMessage.warning('条件符号不能为空');
|
||||
isOk = false;
|
||||
return;
|
||||
}
|
||||
if (!['null', 'notNull'].includes(child.symbol) && ((!child.fieldValue && child.fieldValue !== 0) || isEmpty(child.fieldValue))) {
|
||||
createMessage.warning('数据值不能为空');
|
||||
isOk = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
return isOk;
|
||||
}
|
||||
function confirm() {
|
||||
if (!exist()) return false;
|
||||
return {
|
||||
matchLogic: state.matchLogic,
|
||||
conditionList: cloneDeep(state.conditionList),
|
||||
};
|
||||
}
|
||||
function getSourceTypeOptions({ symbol, yunzhupaasKey }) {
|
||||
const list = [...organizeList, ...depList, ...positionList, ...userList];
|
||||
const options = [{ id: 2, fullName: '自定义' }];
|
||||
return list.includes(yunzhupaasKey) || (symbol != 'between' && dateList.includes(yunzhupaasKey)) ? [...options, { id: 4, fullName: '系统变量' }] : options;
|
||||
}
|
||||
function getParameterList(yunzhupaasKey) {
|
||||
if (dateList.includes(yunzhupaasKey)) return [{ id: '@currentTime', fullName: '当前时间' }];
|
||||
if (positionList.includes(yunzhupaasKey)) return [{ id: '@positionId', fullName: '当前岗位' }];
|
||||
if (organizeList.includes(yunzhupaasKey))
|
||||
return [
|
||||
{ id: '@organizeId', fullName: '当前组织' },
|
||||
{ id: '@organizationAndSuborganization', fullName: '当前组织及子组织' },
|
||||
{ id: '@branchManageOrganize', fullName: '当前分管组织' },
|
||||
];
|
||||
if (depList.includes(yunzhupaasKey))
|
||||
return [
|
||||
{ id: '@depId', fullName: '当前部门' },
|
||||
{ id: '@depAndSubordinates', fullName: '当前部门及下级部门' },
|
||||
];
|
||||
if (userList.includes(yunzhupaasKey))
|
||||
return [
|
||||
{ id: '@userId', fullName: '当前用户' },
|
||||
{ id: '@userAndSubordinates', fullName: '当前用户及下属' },
|
||||
];
|
||||
return [];
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,28 @@
|
||||
<template>
|
||||
<BasicModal v-bind="$attrs" @register="registerModal" title="过滤规则配置" :width="700" @ok="handleSubmit" destroyOnClose class="yunzhupaas-condition-modal">
|
||||
<ConditionMain ref="conditionMainRef" defaultAddEmpty showFieldValueType isCustomFieldValueType />
|
||||
</BasicModal>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref, nextTick } from 'vue';
|
||||
import { BasicModal, useModalInner } from '@/components/Modal';
|
||||
import ConditionMain from './ConditionMain.vue';
|
||||
|
||||
const emit = defineEmits(['register', 'confirm']);
|
||||
const [registerModal, { closeModal, changeLoading }] = useModalInner(init);
|
||||
const conditionMainRef = ref();
|
||||
|
||||
function init(data) {
|
||||
changeLoading(true);
|
||||
nextTick(() => {
|
||||
conditionMainRef.value?.init(data);
|
||||
changeLoading(false);
|
||||
});
|
||||
}
|
||||
function handleSubmit() {
|
||||
const values = conditionMainRef.value?.confirm();
|
||||
if (!values) return;
|
||||
emit('confirm', values);
|
||||
closeModal();
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,109 @@
|
||||
<template>
|
||||
<BasicModal v-bind="$attrs" class="YUNZHUPAAS-complex-header-Modal" @register="registerModal" title="默认排序配置" @ok="handleSubmit" destroyOnClose>
|
||||
<a-table size="small" rowKey="id" class="sort-table" :data-source="list" :columns="columns" :pagination="false">
|
||||
<template #bodyCell="{ column, record, index }">
|
||||
<template v-if="column.key === 'drag'">
|
||||
<i class="drag-handler icon-ym icon-ym-darg" title="点击拖动" />
|
||||
</template>
|
||||
<template v-if="column.key === 'field'">
|
||||
<YunzhupaasSelect
|
||||
v-model:value="record.field"
|
||||
:fieldNames="{ options: 'options1' }"
|
||||
placeholder="请选择"
|
||||
showSearch
|
||||
allowClear
|
||||
:options="getOptions(index)" />
|
||||
</template>
|
||||
<template v-if="column.key === 'sort'">
|
||||
<YunzhupaasRadio v-model:value="record.sort" :options="sortTypeOptions" />
|
||||
</template>
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-button class="action-btn" type="link" color="error" @click="handleDel(index)" size="small">删除</a-button>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
<div class="table-add-action" @click="handleAdd()">
|
||||
<a-button type="link" preIcon="icon-ym icon-ym-btn-add">添加</a-button>
|
||||
</div>
|
||||
</BasicModal>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { nextTick, reactive, toRefs } from 'vue';
|
||||
import { BasicModal, useModalInner } from '@/components/Modal';
|
||||
import Sortablejs from 'sortablejs';
|
||||
import { buildBitUUID } from '@/utils/uuid';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import { useMessage } from '@/hooks/web/useMessage';
|
||||
|
||||
interface State {
|
||||
list: any[];
|
||||
columnOptions: any[];
|
||||
}
|
||||
|
||||
const { createMessage } = useMessage();
|
||||
const state = reactive<State>({
|
||||
list: [],
|
||||
columnOptions: [],
|
||||
});
|
||||
const { list } = toRefs(state);
|
||||
const sortTypeOptions = [
|
||||
{ id: 'asc', fullName: '升序' },
|
||||
{ id: 'desc', fullName: '降序' },
|
||||
];
|
||||
const emit = defineEmits(['register', 'confirm']);
|
||||
const [registerModal, { closeModal }] = useModalInner(init);
|
||||
const columns = [
|
||||
{ title: '拖动', dataIndex: 'drag', key: 'drag', align: 'center', width: 50 },
|
||||
{ title: '字段', dataIndex: 'field', key: 'field', width: 300 },
|
||||
{ title: '类型', dataIndex: 'sort', key: 'sort', width: 200 },
|
||||
{ title: '操作', dataIndex: 'action', key: 'action', width: 50 },
|
||||
];
|
||||
|
||||
function init(data) {
|
||||
state.list = cloneDeep(data.list);
|
||||
state.columnOptions = cloneDeep(data.columnOptions).map(o => ({ ...o, disabled: false }));
|
||||
nextTick(() => initSort());
|
||||
}
|
||||
function initSort() {
|
||||
const searchTable: any = document.querySelector(`.sort-table .ant-table-tbody`);
|
||||
Sortablejs.create(searchTable, {
|
||||
handle: '.drag-handler',
|
||||
animation: 150,
|
||||
easing: 'cubic-bezier(1, 0, 0, 1)',
|
||||
onStart: () => {},
|
||||
onEnd: ({ newIndex, oldIndex }: any) => {
|
||||
const currRow = state.list.splice(oldIndex, 1)[0];
|
||||
state.list.splice(newIndex, 0, currRow);
|
||||
},
|
||||
});
|
||||
}
|
||||
function handleAdd() {
|
||||
const id = 'sort' + buildBitUUID();
|
||||
const item = { field: '', sort: 'desc', id };
|
||||
state.list.push(item);
|
||||
}
|
||||
function handleDel(index) {
|
||||
state.list.splice(index, 1);
|
||||
}
|
||||
function handleSubmit() {
|
||||
let isOk = true;
|
||||
for (let i = 0; i < state.list.length; i++) {
|
||||
if (!state.list[i].field) {
|
||||
createMessage.warning(`字段不能为空`);
|
||||
isOk = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!isOk) return;
|
||||
emit('confirm', state.list);
|
||||
nextTick(() => closeModal());
|
||||
}
|
||||
function getOptions(index) {
|
||||
let options: any[] = state.columnOptions;
|
||||
for (let i = 0; i < state.list.length; i++) {
|
||||
const e = state.list[i];
|
||||
if (e.field && index !== i) options = options.filter(o => o.id !== e.field);
|
||||
}
|
||||
return options;
|
||||
}
|
||||
</script>
|
||||
281
src/components/ColumnDesign/src/components/ExtraConfigModal.vue
Normal file
281
src/components/ColumnDesign/src/components/ExtraConfigModal.vue
Normal file
@@ -0,0 +1,281 @@
|
||||
<template>
|
||||
<BasicModal v-bind="$attrs" @register="registerModal" title="组件属性配置" @ok="handleSubmit" destroyOnClose class="extra-config-modal">
|
||||
<div class="extra-config-modal-body" :style="{ 'min-height': activeData.yunzhupaasKey === 'select' ? '300px' : '150px' }">
|
||||
<a-form :colon="false" labelAlign="left" :labelCol="{ style: { width: '90px' } }" class="right-board-form">
|
||||
<template v-if="activeData.yunzhupaasKey === 'select'">
|
||||
<a-form-item label="数据来源">
|
||||
<yunzhupaas-radio
|
||||
v-model:value="activeData.__config__.dataType"
|
||||
:options="dataTypeOptions"
|
||||
optionType="button"
|
||||
buttonStyle="solid"
|
||||
@change="onDataTypeChange" />
|
||||
</a-form-item>
|
||||
<div class="options-list" v-if="activeData.__config__.dataType === 'static'">
|
||||
<draggable v-model="activeData.options" :animation="300" group="selectItem" handle=".option-drag" itemKey="uuid">
|
||||
<template #item="{ element, index }">
|
||||
<div class="select-item">
|
||||
<div class="select-line-icon option-drag">
|
||||
<i class="icon-ym icon-ym-darg" />
|
||||
</div>
|
||||
<a-input v-model:value="element.fullName" placeholder="选项名" />
|
||||
<a-input v-model:value="element.id" placeholder="选项值" />
|
||||
<div class="close-btn select-line-icon" @click="activeData.options.splice(index, 1)">
|
||||
<i class="icon-ym icon-ym-btn-clearn" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</draggable>
|
||||
</div>
|
||||
<div class="add-btn" v-if="activeData.__config__.dataType === 'static'">
|
||||
<a-button type="link" preIcon="icon-ym icon-ym-btn-add" @click="addSelectItem" class="!px-0">添加选项</a-button>
|
||||
<a-divider type="vertical"></a-divider>
|
||||
<a-button type="link" @click="openModal(true, { options: activeData.options })" class="!px-0">批量编辑</a-button>
|
||||
</div>
|
||||
<div v-if="activeData.__config__.dataType === 'dictionary'">
|
||||
<a-form-item label="数据字典">
|
||||
<yunzhupaas-tree-select
|
||||
:options="dicOptions"
|
||||
v-model:value="activeData.__config__.dictionaryType"
|
||||
placeholder="请选择"
|
||||
lastLevel
|
||||
allowClear
|
||||
@change="onDictionaryTypeChange" />
|
||||
</a-form-item>
|
||||
<a-form-item label="存储字段">
|
||||
<yunzhupaas-select v-model:value="activeData.props.value" placeholder="请选择" :options="valueOptions" />
|
||||
</a-form-item>
|
||||
</div>
|
||||
<div v-if="activeData.__config__.dataType === 'dynamic'">
|
||||
<a-form-item label="数据接口">
|
||||
<interface-modal
|
||||
:value="activeData.__config__.propsUrl"
|
||||
:title="activeData.__config__.propsName"
|
||||
popupTitle="数据接口"
|
||||
@change="onPropsUrlChange" />
|
||||
</a-form-item>
|
||||
<a-form-item label="存储字段">
|
||||
<a-auto-complete
|
||||
v-model:value="activeData.props.value"
|
||||
placeholder="请输入"
|
||||
:options="options"
|
||||
@focus="onFocus(activeData.props.value)"
|
||||
@search="debounceOnSearch(activeData.props.value)" />
|
||||
</a-form-item>
|
||||
<a-form-item label="回显字段">
|
||||
<a-auto-complete
|
||||
v-model:value="activeData.props.label"
|
||||
placeholder="请输入"
|
||||
:options="options"
|
||||
@focus="onFocus(activeData.props.label)"
|
||||
@search="debounceOnSearch(activeData.props.label)" />
|
||||
</a-form-item>
|
||||
</div>
|
||||
</template>
|
||||
<a-form-item label="日期类型" v-if="activeData.yunzhupaasKey === 'datePicker'">
|
||||
<yunzhupaas-select v-model:value="activeData.format" placeholder="请选择" :options="dateFormatOptions" />
|
||||
</a-form-item>
|
||||
<a-form-item label="时间类型" v-if="activeData.yunzhupaasKey === 'timePicker'">
|
||||
<yunzhupaas-select v-model:value="activeData.format" placeholder="请选择" :options="timeFormatOptions" />
|
||||
</a-form-item>
|
||||
<a-form-item label="" v-if="['organizeSelect', 'depSelect', 'userSelect'].includes(activeData.yunzhupaasKey)">
|
||||
<a-checkbox v-model:checked="activeData.isIncludeSubordinate">
|
||||
查询当前{{ activeData.yunzhupaasKey === 'organizeSelect' ? '组织及子组织' : activeData.yunzhupaasKey === 'depSelect' ? '部门及子部门' : '用户及下属' }}
|
||||
</a-checkbox>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<BatchOperate @register="registerBatchOperate" @confirm="onBatchOperateConfirm" />
|
||||
</div>
|
||||
</BasicModal>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { getDictionaryTypeSelector } from '@/api/systemData/dictionary';
|
||||
import { getDataInterfaceInfo } from '@/api/systemData/dataInterface';
|
||||
import { reactive, toRefs } from 'vue';
|
||||
import { BasicModal, useModal, useModalInner } from '@/components/Modal';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import draggable from 'vuedraggable';
|
||||
import { buildBitUUID } from '@/utils/uuid';
|
||||
import { InterfaceModal } from '@/components/CommonModal';
|
||||
import BatchOperate from '@/components/FormGenerator/src/rightComponents/components/BatchOperate.vue';
|
||||
import { useDebounceFn } from '@vueuse/core';
|
||||
|
||||
interface State {
|
||||
activeData: any;
|
||||
dicOptions: any[];
|
||||
options: any[];
|
||||
allOptions: any[];
|
||||
}
|
||||
|
||||
const emit = defineEmits(['register', 'confirm']);
|
||||
const dataTypeOptions = [
|
||||
{ id: 'static', fullName: '静态数据' },
|
||||
{ id: 'dictionary', fullName: '数据字典' },
|
||||
{ id: 'dynamic', fullName: '数据接口' },
|
||||
];
|
||||
const valueOptions = [
|
||||
{ id: 'id', fullName: 'id' },
|
||||
{ id: 'enCode', fullName: 'enCode' },
|
||||
];
|
||||
const dateFormatOptions = [
|
||||
{ id: 'yyyy', fullName: 'yyyy' },
|
||||
{ id: 'yyyy-MM', fullName: 'yyyy-MM' },
|
||||
{ id: 'yyyy-MM-dd', fullName: 'yyyy-MM-dd' },
|
||||
{ id: 'yyyy-MM-dd HH:mm', fullName: 'yyyy-MM-dd HH:mm' },
|
||||
{ id: 'yyyy-MM-dd HH:mm:ss', fullName: 'yyyy-MM-dd HH:mm:ss' },
|
||||
];
|
||||
const timeFormatOptions = [
|
||||
{ id: 'HH:mm:ss', fullName: 'HH:mm:ss' },
|
||||
{ id: 'HH:mm', fullName: 'HH:mm' },
|
||||
];
|
||||
const state = reactive<State>({
|
||||
activeData: {
|
||||
__config__: {},
|
||||
},
|
||||
dicOptions: [],
|
||||
options: [],
|
||||
allOptions: [],
|
||||
});
|
||||
const { activeData, dicOptions, options } = toRefs(state);
|
||||
const [registerModal, { closeModal }] = useModalInner(init);
|
||||
const [registerBatchOperate, { openModal }] = useModal();
|
||||
|
||||
const onDataTypeChange = () => {
|
||||
state.activeData.options = [];
|
||||
state.activeData.props.value = 'id';
|
||||
state.activeData.props.label = 'fullName';
|
||||
if (Reflect.has(state.activeData.props, 'children')) state.activeData.props.children = 'children';
|
||||
state.activeData.__config__.dictionaryType = '';
|
||||
state.activeData.__config__.propsUrl = '';
|
||||
state.activeData.__config__.propsName = '';
|
||||
state.activeData.__config__.templateJson = [];
|
||||
};
|
||||
const onDictionaryTypeChange = () => {
|
||||
state.activeData.options = [];
|
||||
};
|
||||
const onPropsUrlChange = (val, row) => {
|
||||
state.activeData.options = [];
|
||||
if (!val) {
|
||||
state.activeData.__config__.propsUrl = '';
|
||||
state.activeData.__config__.propsName = '';
|
||||
state.activeData.__config__.templateJson = [];
|
||||
state.options = [];
|
||||
state.allOptions = [];
|
||||
return;
|
||||
}
|
||||
if (state.activeData.__config__.propsUrl === val) return;
|
||||
const list = row.parameterJson ? JSON.parse(row.parameterJson) : [];
|
||||
state.activeData.__config__.propsUrl = val;
|
||||
state.activeData.__config__.propsName = row.fullName;
|
||||
state.activeData.__config__.templateJson = list.map(o => ({ ...o, relationField: '' }));
|
||||
initFieldData();
|
||||
};
|
||||
const addSelectItem = () => {
|
||||
const uuid = buildBitUUID();
|
||||
state.activeData.options.push({
|
||||
fullName: '选项' + uuid,
|
||||
id: uuid,
|
||||
});
|
||||
};
|
||||
const onBatchOperateConfirm = options => {
|
||||
state.activeData.options = options;
|
||||
};
|
||||
const debounceOnSearch = useDebounceFn(onSearch, 300);
|
||||
|
||||
function onSearch(searchText: string) {
|
||||
state.options = state.allOptions.filter(o => o.value.toLowerCase().indexOf(searchText.toLowerCase()) !== -1);
|
||||
}
|
||||
function onFocus(searchText) {
|
||||
onSearch(searchText);
|
||||
}
|
||||
function initFieldData() {
|
||||
if (state.activeData.__config__.dataType !== 'dynamic' || !state.activeData.__config__.propsUrl) return (state.allOptions = []);
|
||||
getDataInterfaceInfo(state.activeData.__config__.propsUrl).then(res => {
|
||||
const data = res.data;
|
||||
let list = data.fieldJson ? JSON.parse(data.fieldJson) : [];
|
||||
state.allOptions = list.map(o => ({ ...o, value: o.defaultValue }));
|
||||
});
|
||||
}
|
||||
function init(data) {
|
||||
state.activeData = cloneDeep(data);
|
||||
if (data.yunzhupaasKey === 'select') {
|
||||
getDicOptions();
|
||||
initFieldData();
|
||||
}
|
||||
}
|
||||
function getDicOptions() {
|
||||
getDictionaryTypeSelector().then(res => {
|
||||
state.dicOptions = res.data.list;
|
||||
});
|
||||
}
|
||||
function handleSubmit() {
|
||||
emit('confirm', state.activeData);
|
||||
closeModal();
|
||||
}
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.extra-config-modal {
|
||||
.extra-config-modal-body {
|
||||
min-height: 150px;
|
||||
padding-bottom: 20px;
|
||||
|
||||
.options-list {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
margin-bottom: -10px;
|
||||
.scrollbar__wrap {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
.select-item {
|
||||
display: flex;
|
||||
border: 1px dashed @component-background;
|
||||
box-sizing: border-box;
|
||||
& .ant-input + .ant-input {
|
||||
margin-left: 4px;
|
||||
}
|
||||
.ant-select {
|
||||
width: 100%;
|
||||
}
|
||||
& + .select-item {
|
||||
margin-top: 4px;
|
||||
}
|
||||
&.sortable-chosen {
|
||||
border: 1px dashed @primary-color;
|
||||
}
|
||||
.select-line-icon {
|
||||
line-height: 31px;
|
||||
font-size: 22px;
|
||||
padding: 0 4px;
|
||||
color: #606266;
|
||||
.icon-ym-darg {
|
||||
font-size: 20px;
|
||||
line-height: 31px;
|
||||
display: inline-block;
|
||||
}
|
||||
.icon-ym-btn-clearn {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
.close-btn {
|
||||
cursor: pointer;
|
||||
color: @error-color;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.option-drag {
|
||||
cursor: move;
|
||||
}
|
||||
}
|
||||
|
||||
.yunzhupaas-tree__name {
|
||||
width: calc(100% - 60px);
|
||||
}
|
||||
}
|
||||
.add-btn {
|
||||
padding-left: 27px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
65
src/components/ColumnDesign/src/components/FormScript.vue
Normal file
65
src/components/ColumnDesign/src/components/FormScript.vue
Normal file
@@ -0,0 +1,65 @@
|
||||
<template>
|
||||
<BasicModal
|
||||
v-bind="$attrs"
|
||||
@register="registerModal"
|
||||
:title="title"
|
||||
helpMessage="小程序不支持在线JS脚本"
|
||||
:width="1000"
|
||||
@ok="handleSubmit"
|
||||
destroyOnClose
|
||||
class="form-script-modal">
|
||||
<div class="form-script-modal-body">
|
||||
<div class="main-board">
|
||||
<div class="main-board-editor">
|
||||
<MonacoEditor ref="editorRef" v-model="text" />
|
||||
</div>
|
||||
<div class="main-board-tips">
|
||||
<p>支持JavaScript的脚本,<ScriptDemo :type="funcName" /></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BasicModal>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { BasicModal, useModalInner } from '@/components/Modal';
|
||||
import { MonacoEditor } from '@/components/CodeEditor';
|
||||
import ScriptDemo from '@/components/FormGenerator/src/components/ScriptDemo.vue';
|
||||
|
||||
const emit = defineEmits(['register', 'confirm']);
|
||||
const [registerModal, { closeModal }] = useModalInner(init);
|
||||
const editorRef = ref(null);
|
||||
const text = ref('');
|
||||
const funcName = ref('');
|
||||
const title = ref('');
|
||||
function init(data) {
|
||||
text.value = data.text;
|
||||
funcName.value = data.funcName;
|
||||
title.value = getFuncText(data.funcName);
|
||||
}
|
||||
function getFuncText(key) {
|
||||
let text = '';
|
||||
switch (key) {
|
||||
case 'afterOnload':
|
||||
text = '表格事件';
|
||||
break;
|
||||
case 'rowStyle':
|
||||
text = '表格行样式';
|
||||
break;
|
||||
case 'cellStyle':
|
||||
text = '单元格样式';
|
||||
break;
|
||||
case 'btnEnableRule':
|
||||
text = '启用规则配置';
|
||||
break;
|
||||
default:
|
||||
text = '';
|
||||
break;
|
||||
}
|
||||
return text;
|
||||
}
|
||||
function handleSubmit() {
|
||||
emit('confirm', text.value, funcName.value);
|
||||
closeModal();
|
||||
}
|
||||
</script>
|
||||
1325
src/components/ColumnDesign/src/components/Main.vue
Normal file
1325
src/components/ColumnDesign/src/components/Main.vue
Normal file
File diff suppressed because it is too large
Load Diff
839
src/components/ColumnDesign/src/components/MainApp.vue
Normal file
839
src/components/ColumnDesign/src/components/MainApp.vue
Normal file
@@ -0,0 +1,839 @@
|
||||
<template>
|
||||
<div class="column-design-container">
|
||||
<div class="main-board">
|
||||
<yunzhupaas-group-title content="查询字段" :bordered="false" />
|
||||
<a-table :data-source="columnData.searchList" :columns="searchColumns" size="small" :pagination="false" rowKey="id" class="search-table-app">
|
||||
<template #bodyCell="{ column, record, index }">
|
||||
<template v-if="column.key === 'drag'">
|
||||
<i class="drag-handler icon-ym icon-ym-darg" title="点击拖动" />
|
||||
</template>
|
||||
<template v-if="column.key === 'label'">
|
||||
<yunzhupaas-i18n-input v-model:value="record.label" v-model:i18n="record.labelI18nCode" placeholder="请输入" allowClear />
|
||||
</template>
|
||||
<template v-if="column.key === 'yunzhupaasKey'">
|
||||
<yunzhupaas-select
|
||||
v-model:value="record.yunzhupaasKey"
|
||||
:options="viewYunzhupaasKeyOptions"
|
||||
:disabled="record.__config__.isFromParam"
|
||||
class="!w-160px"
|
||||
@change="onYunzhupaasKeyChange($event, record, index)" />
|
||||
<template v-if="canSetAttrs.includes(record.yunzhupaasKey) && !record.__config__.isFromParam">
|
||||
<i class="icon-ym icon-ym-shezhi cursor-pointer ml-8px leading-30px" title="组件属性设置" @click="openExtraConfig(record, index)" />
|
||||
</template>
|
||||
</template>
|
||||
<template v-if="column.key === 'searchType'">
|
||||
<yunzhupaas-select
|
||||
v-model:value="record.searchType"
|
||||
:options="searchTypeOptions"
|
||||
:disabled="!['input', 'textarea'].includes(record.yunzhupaasKey)"
|
||||
v-if="!record.__config__.isFromParam" />
|
||||
<span v-else></span>
|
||||
</template>
|
||||
<template v-if="column.key === 'value'">
|
||||
<template v-if="showInputList.includes(record.yunzhupaasKey)">
|
||||
<a-input v-model:value="record.value" placeholder="请输入" allowClear />
|
||||
</template>
|
||||
<template v-if="record.yunzhupaasKey === 'inputNumber'">
|
||||
<yunzhupaas-number-range v-model:value="record.value" :precision="record.precision" :disabled="record.disabled" />
|
||||
</template>
|
||||
<template v-if="['rate', 'slider'].includes(record.yunzhupaasKey)">
|
||||
<yunzhupaas-number-range v-model:value="record.value" :precision="record.allowHalf ? 1 : 0" :disabled="record.disabled" />
|
||||
</template>
|
||||
<template v-if="showSelectList.includes(record.yunzhupaasKey)">
|
||||
<yunzhupaas-select
|
||||
v-model:value="record.value"
|
||||
:options="record.options"
|
||||
:fieldNames="record.props"
|
||||
:multiple="record.searchMultiple"
|
||||
showSearch
|
||||
allowClear />
|
||||
</template>
|
||||
<template v-if="record.yunzhupaasKey === 'datePicker'">
|
||||
<yunzhupaas-date-range v-model:value="record.value" :format="record.format" allowClear />
|
||||
</template>
|
||||
<template v-if="record.yunzhupaasKey === 'timePicker'">
|
||||
<yunzhupaas-time-range v-model:value="record.value" :format="record.format" allowClear />
|
||||
</template>
|
||||
<template v-if="record.yunzhupaasKey === 'roleSelect'">
|
||||
<yunzhupaas-role-select
|
||||
v-model:value="record.value"
|
||||
:selectType="record.selectType"
|
||||
:ableIds="record.ableIds"
|
||||
:multiple="record.searchMultiple"
|
||||
allowClear />
|
||||
</template>
|
||||
<template v-if="record.yunzhupaasKey === 'groupSelect'">
|
||||
<yunzhupaas-group-select
|
||||
v-model:value="record.value"
|
||||
:selectType="record.selectType"
|
||||
:ableIds="record.ableIds"
|
||||
:multiple="record.searchMultiple"
|
||||
allowClear />
|
||||
</template>
|
||||
<template v-if="record.yunzhupaasKey === 'areaSelect'">
|
||||
<yunzhupaas-area-select v-model:value="record.value" :level="record.level" :multiple="record.searchMultiple" :key="record.cellKey" allowClear />
|
||||
</template>
|
||||
<template v-if="record.yunzhupaasKey === 'organizeSelect'">
|
||||
<yunzhupaas-organize-select
|
||||
v-model:value="record.value"
|
||||
:selectType="record.selectType"
|
||||
:ableIds="record.ableIds"
|
||||
:multiple="record.searchMultiple"
|
||||
allowClear />
|
||||
</template>
|
||||
<template v-if="record.yunzhupaasKey === 'cascader'">
|
||||
<yunzhupaas-cascader
|
||||
v-model:value="record.value"
|
||||
placeholder="请选择"
|
||||
:options="record.options"
|
||||
:fieldNames="record.props"
|
||||
:showAllLevels="record.showAllLevels"
|
||||
:multiple="record.searchMultiple"
|
||||
showSearch
|
||||
allowClear />
|
||||
</template>
|
||||
<template v-if="record.yunzhupaasKey === 'depSelect'">
|
||||
<yunzhupaas-dep-select
|
||||
v-model:value="record.value"
|
||||
:selectType="record.selectType"
|
||||
:ableIds="record.ableIds"
|
||||
:multiple="record.searchMultiple"
|
||||
allowClear />
|
||||
</template>
|
||||
<template v-if="record.yunzhupaasKey === 'posSelect'">
|
||||
<yunzhupaas-pos-select
|
||||
v-model:value="record.value"
|
||||
:selectType="record.selectType"
|
||||
:ableIds="record.ableIds"
|
||||
:multiple="record.searchMultiple"
|
||||
allowClear />
|
||||
</template>
|
||||
<template v-if="record.yunzhupaasKey === 'userSelect'">
|
||||
<yunzhupaas-user-select
|
||||
v-model:value="record.value"
|
||||
:selectType="record.selectType != 'all' && record.selectType != 'custom' ? 'all' : record.selectType"
|
||||
:ableIds="record.ableIds"
|
||||
:multiple="record.searchMultiple"
|
||||
allowClear />
|
||||
</template>
|
||||
<template v-if="record.yunzhupaasKey === 'usersSelect'">
|
||||
<yunzhupaas-users-select
|
||||
v-model:value="record.value"
|
||||
:selectType="record.selectType"
|
||||
:ableIds="record.ableIds"
|
||||
:multiple="record.searchMultiple"
|
||||
allowClear />
|
||||
</template>
|
||||
<template v-if="record.yunzhupaasKey === 'autoComplete'">
|
||||
<yunzhupaas-auto-complete
|
||||
v-model:value="record.value"
|
||||
:placeholder="record.placeholder"
|
||||
:allowClear="record.clearable"
|
||||
:disabled="record.disabled"
|
||||
:interfaceId="record.interfaceId"
|
||||
:relationField="record.relationField"
|
||||
:templateJson="record.templateJson"
|
||||
:total="record.total" />
|
||||
</template>
|
||||
</template>
|
||||
<template v-if="column.key === 'isKeyword'">
|
||||
<a-checkbox v-model:checked="record.isKeyword" :disabled="!canSetKeyword.includes(record.yunzhupaasKey) || (getIsKeywordDisabled && !record.isKeyword)" />
|
||||
</template>
|
||||
<template v-if="column.key === 'searchMultiple'">
|
||||
<a-checkbox
|
||||
v-model:checked="record.searchMultiple"
|
||||
:disabled="!multipleList.includes(record.yunzhupaasKey)"
|
||||
@change="onSearchMultipleChange(record, index)" />
|
||||
</template>
|
||||
<template v-if="column.key === 'noShow'">
|
||||
<a-checkbox v-model:checked="record.noShow" />
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
<yunzhupaas-group-title content="列表字段" :bordered="false" class="mt-20px" />
|
||||
<a-table :data-source="columnData.columnList" :columns="columnColumns" size="small" :pagination="false" rowKey="id" class="column-table-app">
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'drag'">
|
||||
<i class="drag-handler icon-ym icon-ym-darg" title="点击拖动" />
|
||||
</template>
|
||||
<template v-if="column.key === 'label' && webType == 4">
|
||||
<yunzhupaas-i18n-input v-model:value="record.label" v-model:i18n="record.labelI18nCode" placeholder="请输入" allowClear />
|
||||
</template>
|
||||
<template v-if="column.key === 'sortable'">
|
||||
<a-checkbox
|
||||
v-model:checked="record.sortable"
|
||||
:disabled="
|
||||
record.id.indexOf('_yunzhupaas_') > 0 || (record.__config__ && record.__config__.isSubTable) || noGroupList.includes(record.__config__.yunzhupaasKey)
|
||||
" />
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
<div class="right-board">
|
||||
<a-tabs v-model:activeKey="activeKey" :tabBarGutter="11" class="average-tabs">
|
||||
<a-tab-pane key="search" tab="查询字段"></a-tab-pane>
|
||||
<a-tab-pane key="field" tab="列表字段"></a-tab-pane>
|
||||
<a-tab-pane key="column" tab="列表属性"></a-tab-pane>
|
||||
</a-tabs>
|
||||
<div class="right-main">
|
||||
<div class="h-full" v-show="activeKey === 'search'">
|
||||
<a-table
|
||||
:data-source="getSearchOptions"
|
||||
:columns="rightColumns"
|
||||
size="small"
|
||||
:pagination="false"
|
||||
:scroll="{ y: 'calc(100vh - 161px)' }"
|
||||
rowKey="id"
|
||||
:row-selection="{ columnWidth: 50, selectedRowKeys: searchSelectedRowKeys, onChange: onSearchSelectChange }">
|
||||
<template #headerCell>查询字段</template>
|
||||
</a-table>
|
||||
</div>
|
||||
<div class="h-full" v-show="activeKey === 'field'">
|
||||
<a-table
|
||||
:data-source="state.columnOptions"
|
||||
:columns="rightColumns"
|
||||
size="small"
|
||||
:pagination="false"
|
||||
:scroll="{ y: 'calc(100vh - 161px)' }"
|
||||
rowKey="id"
|
||||
:row-selection="{ columnWidth: 50, selectedRowKeys: columnSelectedRowKeys, onChange: onColumnSelectChange }">
|
||||
<template #headerCell>列表字段</template>
|
||||
</a-table>
|
||||
</div>
|
||||
<ScrollContainer v-show="activeKey === 'column'">
|
||||
<a-form :colon="false" layout="vertical" class="right-board-form !-mt-10px">
|
||||
<a-divider>表格配置</a-divider>
|
||||
<a-form-item label="数据过滤" v-if="webType != 4">
|
||||
<a-button block @click="editRuleList">{{ getRuleBtnText }}</a-button>
|
||||
</a-form-item>
|
||||
<a-form-item label="默认排序">
|
||||
<a-button block @click="editDefaultSortConfig">默认排序配置</a-button>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="webType == 4">
|
||||
<template #label>视图主键<BasicHelp text="视图主键用于当前选中的数据导出,设置的主键必须是唯一值才能确保数据正常导出" /></template>
|
||||
<yunzhupaas-select v-model:value="columnData.viewKey" :options="state.columnOptions" />
|
||||
</a-form-item>
|
||||
<a-form-item label="分页设置">
|
||||
<a-switch v-model:checked="columnData.hasPage" :disabled="!!interfaceHasPage" />
|
||||
</a-form-item>
|
||||
<a-form-item label="分页条数" v-if="columnData.hasPage">
|
||||
<yunzhupaas-radio v-model:value="columnData.pageSize" :options="pageSizeOptions" optionType="button" button-style="solid" class="right-radio" />
|
||||
</a-form-item>
|
||||
<template v-if="webType != 4 && [1, 4].includes(columnData.type)">
|
||||
<a-form-item label="标签面板">
|
||||
<a-switch v-model:checked="columnData.tabConfig.on" @change="onTabOnChange" />
|
||||
</a-form-item>
|
||||
<template v-if="columnData?.tabConfig?.on">
|
||||
<a-form-item label="筛选字段">
|
||||
<yunzhupaas-select
|
||||
v-model:value="columnData.tabConfig.relationField"
|
||||
:options="state.tabRelationFieldOptions"
|
||||
:fieldNames="{ options: 'options1' }"
|
||||
showSearch
|
||||
@change="onRelationChange" />
|
||||
</a-form-item>
|
||||
<a-form-item label="全部标签">
|
||||
<a-switch v-model:checked="columnData.tabConfig.hasAllTab" />
|
||||
</a-form-item>
|
||||
</template>
|
||||
</template>
|
||||
<a-divider>按钮配置</a-divider>
|
||||
<div class="btnsList" v-if="webType != 4">
|
||||
<div v-for="item in columnData.btnsList" :key="item.value" class="btnsList-cell">
|
||||
<a-checkbox v-model:checked="item.show">{{ getBtnText(item.value) }}</a-checkbox>
|
||||
<yunzhupaas-i18n-input v-model:value="item.label" v-model:i18n="item.labelI18nCode" placeholder="按钮名称" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="btnsList" v-if="webType != 4">
|
||||
<div v-for="(item, index) in columnData.columnBtnsList" :key="item.value" class="btnsList-cell">
|
||||
<a-checkbox v-model:checked="item.show">{{ getBtnText(item.value) }}</a-checkbox>
|
||||
<yunzhupaas-i18n-input v-model:value="item.label" v-model:i18n="item.labelI18nCode" placeholder="按钮名称">
|
||||
<template #addonBefore>
|
||||
<span class="cursor-pointer px-11px" @click="handleEnableRule(item, index)">启用规则<BasicHelp text="不支持代码生成" /></span>
|
||||
</template>
|
||||
</yunzhupaas-i18n-input>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p class="btn-cap">自定义按钮区<BasicHelp text="不支持代码生成" /></p>
|
||||
<div class="custom-btns-list">
|
||||
<draggable v-model="columnData.customBtnsList" :animation="300" group="selectItem" handle=".option-drag" itemKey="value">
|
||||
<template #item="{ element, index }">
|
||||
<div class="custom-item">
|
||||
<div class="custom-line-icon option-drag">
|
||||
<i class="icon-ym icon-ym-darg" />
|
||||
</div>
|
||||
<p class="custom-line-value">{{ element.value }}</p>
|
||||
<yunzhupaas-i18n-input v-model:value="element.label" v-model:i18n="element.labelI18nCode" placeholder="按钮名称">
|
||||
<template #addonBefore>
|
||||
<span class="cursor-pointer" @click="editBtnEvent(element, index)">事件</span>
|
||||
</template>
|
||||
</yunzhupaas-i18n-input>
|
||||
<div class="close-btn custom-line-icon" @click="columnData.customBtnsList.splice(index, 1)">
|
||||
<i class="icon-ym icon-ym-btn-clearn" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</draggable>
|
||||
<div class="add-btn">
|
||||
<a-button type="link" preIcon="icon-ym icon-ym-btn-add" @click="addCustomBtn">添加按钮</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="webType != 4">
|
||||
<a-divider>权限设置</a-divider>
|
||||
<a-form-item label="按钮权限">
|
||||
<a-switch v-model:checked="columnData.useBtnPermission" />
|
||||
</a-form-item>
|
||||
<a-form-item label="列表权限">
|
||||
<a-switch v-model:checked="columnData.useColumnPermission" />
|
||||
</a-form-item>
|
||||
<a-form-item label="表单权限">
|
||||
<a-switch v-model:checked="columnData.useFormPermission" />
|
||||
</a-form-item>
|
||||
<a-form-item label="数据权限">
|
||||
<a-switch v-model:checked="columnData.useDataPermission" />
|
||||
</a-form-item>
|
||||
</div>
|
||||
<a-divider>脚本事件<BasicHelp text="不支持代码生成" /></a-divider>
|
||||
<a-form-item :label="getFuncText(key)" v-for="(_value, key) in columnData.funcs" :key="key">
|
||||
<a-button block @click="editFunc(key)">脚本编写</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</ScrollContainer>
|
||||
</div>
|
||||
</div>
|
||||
<FormScript @register="registerScriptModal" @confirm="updateScript" />
|
||||
<BtnEvent @register="registerBtnEventModal" @confirm="updateBtnEvent" />
|
||||
<ConditionModal @register="registerConditionModal" @confirm="updateRuleList" />
|
||||
<DefaultSortConfigModal @register="registerDefaultSortConfigModal" @confirm="updateDefaultSortConfig" />
|
||||
<ExtraConfigModal @register="registerExtraConfigModal" @confirm="updateSearchRow" />
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, onMounted, computed, toRefs, unref, nextTick } from 'vue';
|
||||
import { ScrollContainer } from '@/components/Container';
|
||||
import {
|
||||
noColumnShowList,
|
||||
noSearchList,
|
||||
noGroupList,
|
||||
getSearchType,
|
||||
getDefaultValue,
|
||||
getSearchMultiple,
|
||||
defaultFuncsData,
|
||||
defaultAppColumnData,
|
||||
defaultBtnEnableFunc,
|
||||
defaultAppBtnsList,
|
||||
defaultColumnBtnsList,
|
||||
} from '../helper/config';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import draggable from 'vuedraggable';
|
||||
import { buildBitUUID } from '@/utils/uuid';
|
||||
import { useModal } from '@/components/Modal';
|
||||
import FormScript from './FormScript.vue';
|
||||
import BtnEvent from './BtnEvent.vue';
|
||||
import ConditionModal from './ConditionModal.vue';
|
||||
import DefaultSortConfigModal from './DefaultSortConfigModal.vue';
|
||||
import ExtraConfigModal from './ExtraConfigModal.vue';
|
||||
import Sortablejs from 'sortablejs';
|
||||
import { dyOptionsList } from '@/components/FormGenerator/src/helper/config';
|
||||
import { getDictionaryDataSelector } from '@/api/systemData/dictionary';
|
||||
import { getDataInterfaceRes } from '@/api/systemData/dataInterface';
|
||||
import { getParamList } from '@/utils/yunzhupaas';
|
||||
|
||||
interface State {
|
||||
columnData: any;
|
||||
groupFieldOptions: any[];
|
||||
tabRelationFieldOptions: any[];
|
||||
columnOptions: any[];
|
||||
searchOptions: any[];
|
||||
defaultBtnsList: any[];
|
||||
defaultColumnBtnsList: any[];
|
||||
activeFunc: string;
|
||||
activeBtn: string;
|
||||
searchSelectedRowKeys: string[];
|
||||
columnSelectedRowKeys: string[];
|
||||
activeSearchRowIndex: number;
|
||||
}
|
||||
|
||||
const props = defineProps(['conf', 'formInfo', 'viewFields', 'interfaceParam', 'interfaceHasPage']);
|
||||
defineExpose({ getData });
|
||||
|
||||
const pageSizeOptions = [
|
||||
{ id: 20, fullName: '20条' },
|
||||
{ id: 50, fullName: '50条' },
|
||||
{ id: 80, fullName: '80条' },
|
||||
{ id: 100, fullName: '100条' },
|
||||
];
|
||||
const rightColumns = [{ title: '字段', dataIndex: 'fullName', key: 'fullName' }];
|
||||
const columnColumns = [
|
||||
{ title: '拖动', dataIndex: 'drag', key: 'drag', align: 'center', width: 50 },
|
||||
{ title: '列名', dataIndex: 'label', key: 'label', width: '40%' },
|
||||
{ title: '字段', dataIndex: 'prop', key: 'prop' },
|
||||
{ title: '排序', dataIndex: 'sortable', key: 'sortable', width: 60, align: 'center' },
|
||||
];
|
||||
const multipleList = ['select', 'depSelect', 'roleSelect', 'userSelect', 'usersSelect', 'organizeSelect', 'posSelect', 'groupSelect'];
|
||||
const canSetKeyword = ['input', 'textarea', 'autoComplete'];
|
||||
const canSetAttrs = ['select', 'datePicker', 'timePicker', 'organizeSelect', 'depSelect', 'userSelect'];
|
||||
const searchTypeOptions = [
|
||||
{ id: 1, fullName: '等于查询' },
|
||||
{ id: 2, fullName: '模糊查询' },
|
||||
{ id: 3, fullName: '范围查询' },
|
||||
];
|
||||
const viewYunzhupaasKeyOptions = [
|
||||
{ id: 'input', fullName: '单行输入' },
|
||||
{ id: 'inputNumber', fullName: '数字输入' },
|
||||
{ id: 'select', fullName: '下拉选择' },
|
||||
{ id: 'datePicker', fullName: '日期选择' },
|
||||
{ id: 'timePicker', fullName: '时间选择' },
|
||||
{ id: 'organizeSelect', fullName: '组织选择' },
|
||||
{ id: 'depSelect', fullName: '部门选择' },
|
||||
{ id: 'roleSelect', fullName: '角色选择' },
|
||||
{ id: 'posSelect', fullName: '岗位选择' },
|
||||
{ id: 'groupSelect', fullName: '分组选择' },
|
||||
{ id: 'userSelect', fullName: '用户选择' },
|
||||
];
|
||||
const showInputList = ['input', 'billRule'];
|
||||
const showSelectList = ['checkbox', 'radio', 'select'];
|
||||
|
||||
const activeKey = ref('column');
|
||||
const state = reactive<State>({
|
||||
columnData: cloneDeep(defaultAppColumnData),
|
||||
groupFieldOptions: [],
|
||||
tabRelationFieldOptions: [],
|
||||
columnOptions: [],
|
||||
searchOptions: [],
|
||||
defaultBtnsList: [],
|
||||
defaultColumnBtnsList: [],
|
||||
activeFunc: '',
|
||||
activeBtn: '',
|
||||
searchSelectedRowKeys: [],
|
||||
columnSelectedRowKeys: [],
|
||||
activeSearchRowIndex: 0,
|
||||
});
|
||||
const { columnData, searchSelectedRowKeys, columnSelectedRowKeys } = toRefs(state);
|
||||
const [registerScriptModal, { openModal: openScriptModal }] = useModal();
|
||||
const [registerBtnEventModal, { openModal: openBtnEventModal }] = useModal();
|
||||
const [registerConditionModal, { openModal: openConditionModal }] = useModal();
|
||||
const [registerDefaultSortConfigModal, { openModal: openDefaultSortConfigModal }] = useModal();
|
||||
const [registerExtraConfigModal, { openModal: openExtraConfigModal }] = useModal();
|
||||
|
||||
const webType = computed(() => props.formInfo?.webType);
|
||||
const getDrawingList = computed(() => {
|
||||
if (!props.formInfo || !props.formInfo.formData) return [];
|
||||
const formData = props.formInfo?.formData && JSON.parse(props.formInfo.formData);
|
||||
return formData.fields || [];
|
||||
});
|
||||
const getRuleBtnText = computed(() => (state.columnData?.ruleListApp?.conditionList?.length ? '编辑过滤条件' : '添加过滤条件'));
|
||||
const formFieldsOptions = computed(() => {
|
||||
let list: any[] = [];
|
||||
const loop = (data, parent?) => {
|
||||
if (!data) return;
|
||||
if (data.__config__ && data.__config__.children && Array.isArray(data.__config__.children)) {
|
||||
loop(data.__config__.children, data);
|
||||
}
|
||||
if (Array.isArray(data)) data.forEach(d => loop(d, parent));
|
||||
if (data.__config__ && data.__config__.yunzhupaasKey) {
|
||||
const visibility = !data.__config__.visibility || (Array.isArray(data.__config__.visibility) && data.__config__.visibility.includes('app'));
|
||||
if (data.__config__.layout === 'colFormItem' && data.__vModel__ && visibility) {
|
||||
const isTableChild = parent && parent.__config__ && parent.__config__.yunzhupaasKey === 'table';
|
||||
list.push({
|
||||
id: isTableChild ? parent.__vModel__ + '-' + data.__vModel__ : data.__vModel__,
|
||||
fullName: isTableChild ? parent.__config__.label + '-' + data.__config__.label : data.__config__.label,
|
||||
fullNameI18nCode: isTableChild
|
||||
? [parent.__config__.labelI18nCode || '', data.__config__.labelI18nCode || '']
|
||||
: [data.__config__.labelI18nCode || ''],
|
||||
...data,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
loop(unref(getDrawingList));
|
||||
return list;
|
||||
});
|
||||
const viewFieldOptions = computed(() => {
|
||||
if (!props.viewFields) return [];
|
||||
return props.viewFields.map(o => ({ id: o.defaultValue, fullName: o.field, __vModel__: o.defaultValue, __config__: { yunzhupaasKey: 'input' } }));
|
||||
});
|
||||
const searchColumns = computed(() => {
|
||||
let list = [
|
||||
{ title: '拖动', dataIndex: 'drag', key: 'drag', align: 'center', width: 50 },
|
||||
{ title: '列名', dataIndex: 'label', key: 'label', width: 200 },
|
||||
{ title: '字段', dataIndex: 'prop', key: 'prop' },
|
||||
{ title: '输入类型', dataIndex: 'yunzhupaasKey', key: 'yunzhupaasKey', width: 200 },
|
||||
{ title: '条件类型', dataIndex: 'searchType', key: 'searchType', width: 200 },
|
||||
{ title: '默认值', dataIndex: 'value', key: 'value', width: 200 },
|
||||
{ title: '关键词', dataIndex: 'isKeyword', key: 'isKeyword', width: 70, align: 'center' },
|
||||
{ title: '是否多选', dataIndex: 'searchMultiple', key: 'searchMultiple', width: 80, align: 'center' },
|
||||
{ title: '是否隐藏', dataIndex: 'noShow', key: 'noShow', width: 80, align: 'center' },
|
||||
];
|
||||
if (unref(webType) == 4) {
|
||||
list = list.filter(o => o.dataIndex != 'value' && o.dataIndex != 'isKeyword');
|
||||
} else {
|
||||
list = list.filter(o => o.dataIndex != 'yunzhupaasKey');
|
||||
}
|
||||
return list;
|
||||
});
|
||||
const getSearchOptions = computed(() =>
|
||||
state.columnData.tabConfig.relationField && state.columnData.tabConfig.on
|
||||
? state.searchOptions.filter(o => o.id !== state.columnData.tabConfig.relationField)
|
||||
: state.searchOptions,
|
||||
);
|
||||
const getIsKeywordDisabled = computed(() => state.columnData.searchList.filter(o => o.isKeyword).length >= 3);
|
||||
|
||||
// 供父组件使用 获取表单JSON
|
||||
function getData() {
|
||||
state.columnData.defaultColumnList = state.columnOptions.map(o => ({
|
||||
...o,
|
||||
checked: state.columnData.columnList.some(i => i.prop === o.prop),
|
||||
}));
|
||||
return state.columnData;
|
||||
}
|
||||
function getBtnText(key) {
|
||||
let text = '';
|
||||
switch (key) {
|
||||
case 'download':
|
||||
text = '导出';
|
||||
break;
|
||||
case 'batchRemove':
|
||||
text = '批量删除';
|
||||
break;
|
||||
case 'edit':
|
||||
text = '编辑';
|
||||
break;
|
||||
case 'remove':
|
||||
text = '删除';
|
||||
break;
|
||||
case 'detail':
|
||||
text = '详情';
|
||||
break;
|
||||
case 'upload':
|
||||
text = '导入';
|
||||
break;
|
||||
default:
|
||||
text = '新增';
|
||||
break;
|
||||
}
|
||||
return text;
|
||||
}
|
||||
function getFuncText(key) {
|
||||
let text = '';
|
||||
switch (key) {
|
||||
case 'afterOnload':
|
||||
text = '表格事件';
|
||||
break;
|
||||
case 'rowStyle':
|
||||
text = '表格行样式';
|
||||
break;
|
||||
case 'cellStyle':
|
||||
text = '单元格样式';
|
||||
break;
|
||||
default:
|
||||
text = '';
|
||||
break;
|
||||
}
|
||||
return text;
|
||||
}
|
||||
function addCustomBtn() {
|
||||
const id = buildBitUUID();
|
||||
state.columnData.customBtnsList.push({
|
||||
show: true,
|
||||
value: 'btn_' + id,
|
||||
label: '按钮' + id,
|
||||
labelI18nCode: '',
|
||||
event: {},
|
||||
});
|
||||
}
|
||||
function editBtnEvent(item, index) {
|
||||
state.activeBtn = index;
|
||||
openBtnEventModal(true, {
|
||||
showType: 'app',
|
||||
formFieldsOptions: unref(webType) == 4 ? state.columnOptions : unref(formFieldsOptions),
|
||||
dataForm: item.event,
|
||||
});
|
||||
}
|
||||
function updateBtnEvent(data) {
|
||||
state.columnData.customBtnsList[state.activeBtn].event = data;
|
||||
}
|
||||
function editRuleList() {
|
||||
if (!state.columnData.ruleListApp || !state.columnData.ruleListApp.matchLogic) state.columnData.ruleListApp = { matchLogic: 'and', conditionList: [] };
|
||||
openConditionModal(true, {
|
||||
...state.columnData.ruleListApp,
|
||||
fieldOptions: unref(webType) == 4 ? state.columnOptions : unref(formFieldsOptions),
|
||||
});
|
||||
}
|
||||
function updateRuleList(data) {
|
||||
state.columnData.ruleListApp = data;
|
||||
}
|
||||
function editFunc(funcName) {
|
||||
state.activeFunc = funcName;
|
||||
if (!state.columnData.funcs[state.activeFunc]) state.columnData.funcs[state.activeFunc] = defaultFuncsData[state.activeFunc];
|
||||
openScriptModal(true, { text: state.columnData.funcs[state.activeFunc], funcName });
|
||||
}
|
||||
function handleEnableRule(item, index) {
|
||||
state.activeBtn = index;
|
||||
if (!item?.event?.enableFunc) item.event = { enableFunc: defaultBtnEnableFunc };
|
||||
openScriptModal(true, { text: item.event.enableFunc, funcName: 'btnEnableRule' });
|
||||
}
|
||||
function updateScript(data, funcName) {
|
||||
if (funcName == 'btnEnableRule') {
|
||||
state.columnData.columnBtnsList[state.activeBtn].event.enableFunc = data;
|
||||
} else {
|
||||
state.columnData.funcs[state.activeFunc] = data;
|
||||
}
|
||||
}
|
||||
function editDefaultSortConfig() {
|
||||
openDefaultSortConfigModal(true, {
|
||||
list: state.columnData.defaultSortConfig,
|
||||
columnOptions: state.groupFieldOptions.filter(o => o.id.indexOf('_yunzhupaas_') < 0),
|
||||
});
|
||||
}
|
||||
function updateDefaultSortConfig(data) {
|
||||
state.columnData.defaultSortConfig = data;
|
||||
}
|
||||
function openExtraConfig(record, index) {
|
||||
state.activeSearchRowIndex = index;
|
||||
openExtraConfigModal(true, { ...record });
|
||||
}
|
||||
function updateSearchRow(data) {
|
||||
state.columnData.searchList[state.activeSearchRowIndex] = data;
|
||||
}
|
||||
function onYunzhupaasKeyChange(val, record, i) {
|
||||
record.__config__.yunzhupaasKey = val;
|
||||
let defaultItem: any = {
|
||||
id: record.id,
|
||||
fullName: record.fullName,
|
||||
label: record.label,
|
||||
prop: record.prop,
|
||||
yunzhupaasKey: record.yunzhupaasKey,
|
||||
value: getDefaultValue(record),
|
||||
searchType: getSearchType(record),
|
||||
__vModel__: record.__vModel__,
|
||||
searchMultiple: getSearchMultiple(val),
|
||||
isKeyword: false,
|
||||
__config__: {
|
||||
label: record.label,
|
||||
yunzhupaasKey: val,
|
||||
},
|
||||
};
|
||||
if (val === 'datePicker') defaultItem.format = 'yyyy-MM-dd';
|
||||
if (val === 'timePicker') defaultItem.format = 'HH:mm:ss';
|
||||
if (val === 'select') {
|
||||
defaultItem.options = [];
|
||||
defaultItem.props = { label: 'fullName', value: 'id' };
|
||||
defaultItem.__config__ = {
|
||||
...defaultItem.__config__,
|
||||
dataType: 'static',
|
||||
propsUrl: '',
|
||||
propsName: '',
|
||||
templateJson: [],
|
||||
dictionaryType: '',
|
||||
};
|
||||
}
|
||||
if (['organizeSelect', 'depSelect', 'userSelect'].includes(val)) {
|
||||
defaultItem.isIncludeSubordinate = false;
|
||||
}
|
||||
state.columnData.searchList[i] = cloneDeep(defaultItem);
|
||||
}
|
||||
function setBtnValue(data, defaultData) {
|
||||
outer: for (let i = 0; i < defaultData.length; i++) {
|
||||
inter: for (let ii = 0; ii < data.length; ii++) {
|
||||
if (defaultData[i].value === data[ii].value) {
|
||||
defaultData[i] = data[ii];
|
||||
if (!Reflect.has(defaultData[i], 'show')) defaultData[i].show = true;
|
||||
break inter;
|
||||
}
|
||||
}
|
||||
}
|
||||
return defaultData;
|
||||
}
|
||||
function setListValue(data, defaultData, type) {
|
||||
data = data.filter(o => defaultData.some(e => o.prop == e.prop));
|
||||
outer: for (let i = 0; i < data.length; i++) {
|
||||
inter: for (let ii = 0; ii < defaultData.length; ii++) {
|
||||
if (data[i].prop === defaultData[ii].prop) {
|
||||
if (type === 'column') {
|
||||
defaultData[ii].fixed = data[i].fixed;
|
||||
defaultData[ii].align = data[i].align;
|
||||
defaultData[ii].width = data[i].width;
|
||||
defaultData[ii].sortable = data[i].sortable;
|
||||
if (unref(webType) == 4) {
|
||||
defaultData[ii].label = data[i].label;
|
||||
defaultData[ii].labelI18nCode = data[i].labelI18nCode;
|
||||
}
|
||||
}
|
||||
if (type === 'search') {
|
||||
if (data[i].yunzhupaasKey === defaultData[ii].yunzhupaasKey) {
|
||||
defaultData[ii].searchType = data[i].searchType;
|
||||
defaultData[ii].searchMultiple = data[i].searchMultiple;
|
||||
defaultData[ii].noShow = data[i].noShow;
|
||||
defaultData[ii].value = data[i].value;
|
||||
}
|
||||
defaultData[ii].label = data[i].label;
|
||||
defaultData[ii].labelI18nCode = data[i].labelI18nCode;
|
||||
defaultData[ii].isKeyword = data[i].isKeyword;
|
||||
if (unref(webType) == 4) defaultData[ii] = data[i];
|
||||
}
|
||||
data[i] = defaultData[ii];
|
||||
break inter;
|
||||
}
|
||||
}
|
||||
}
|
||||
state[type + 'SelectedRowKeys'] = data.map(o => o.prop);
|
||||
return data;
|
||||
}
|
||||
function updateListValue(selectedRowKeys, selectedRows, type) {
|
||||
state[type + 'SelectedRowKeys'] = selectedRowKeys;
|
||||
if (!selectedRowKeys.length) return (state.columnData[type + 'List'] = []);
|
||||
state.columnData[type + 'List'] = state.columnData[type + 'List'].filter(o => selectedRowKeys.some(e => o.prop == e));
|
||||
for (let i = 0; i < selectedRows.length; i++) {
|
||||
if (!state.columnData[type + 'List'].some(o => o.prop === selectedRows[i].prop)) {
|
||||
state.columnData[type + 'List'].push(cloneDeep(selectedRows[i]));
|
||||
if (type == 'search') buildOptions([selectedRows[i]]);
|
||||
}
|
||||
}
|
||||
}
|
||||
function onSearchSelectChange(selectedRowKeys, selectedRows) {
|
||||
updateListValue(selectedRowKeys, selectedRows, 'search');
|
||||
}
|
||||
function onColumnSelectChange(selectedRowKeys, selectedRows) {
|
||||
updateListValue(selectedRowKeys, selectedRows, 'column');
|
||||
}
|
||||
function initSort() {
|
||||
const searchTable: any = document.querySelector(`.search-table-app .ant-table-tbody`);
|
||||
Sortablejs.create(searchTable, {
|
||||
handle: '.drag-handler',
|
||||
animation: 150,
|
||||
easing: 'cubic-bezier(1, 0, 0, 1)',
|
||||
onStart: () => {},
|
||||
onEnd: ({ newIndex, oldIndex }: any) => {
|
||||
const currRow = state.columnData.searchList.splice(oldIndex, 1)[0];
|
||||
state.columnData.searchList.splice(newIndex, 0, currRow);
|
||||
},
|
||||
});
|
||||
const columnTable: any = document.querySelector(`.column-table-app .ant-table-tbody`);
|
||||
Sortablejs.create(columnTable, {
|
||||
handle: '.drag-handler',
|
||||
animation: 150,
|
||||
easing: 'cubic-bezier(1, 0, 0, 1)',
|
||||
onStart: () => {},
|
||||
onEnd: ({ newIndex, oldIndex }: any) => {
|
||||
const currRow = state.columnData.columnList.splice(oldIndex, 1)[0];
|
||||
state.columnData.columnList.splice(newIndex, 0, currRow);
|
||||
},
|
||||
});
|
||||
}
|
||||
function init(list) {
|
||||
list = list.map(o => {
|
||||
if (o.__config__ && dyOptionsList.includes(o.__config__.yunzhupaasKey) && o.__config__.dataType !== 'static') o.options = [];
|
||||
return o;
|
||||
});
|
||||
const columnOptions = list.filter(o => !noColumnShowList.includes(o.__config__.yunzhupaasKey) || o.isStorage);
|
||||
let searchOptions = list.filter(o => !noSearchList.includes(o.__config__.yunzhupaasKey));
|
||||
if (unref(webType) == 4) {
|
||||
const interfaceParam = (props.interfaceParam || [])
|
||||
.filter(o => o.useSearch)
|
||||
.map(o => {
|
||||
let yunzhupaasKey = 'input';
|
||||
if (o.dataType === 'int' || o.dataType === 'decimal') yunzhupaasKey = 'inputNumber';
|
||||
if (o.dataType === 'datetime') yunzhupaasKey = 'datePicker';
|
||||
return {
|
||||
id: o.field,
|
||||
fullName: o.fieldName || o.field,
|
||||
__vModel__: o.field,
|
||||
__config__: { isFromParam: true, yunzhupaasKey },
|
||||
};
|
||||
});
|
||||
if (props.interfaceHasPage) {
|
||||
searchOptions = interfaceParam;
|
||||
} else {
|
||||
searchOptions = searchOptions.filter(o => !interfaceParam.some(e => e.id === o.id));
|
||||
searchOptions = [...interfaceParam, ...searchOptions];
|
||||
}
|
||||
}
|
||||
state.groupFieldOptions = list.filter(o => o.id.indexOf('-') < 0 && !noGroupList.includes(o.__config__.yunzhupaasKey)).map(o => ({ ...o, disabled: false }));
|
||||
state.tabRelationFieldOptions = list.filter(
|
||||
o => o.id.indexOf('-') < 0 && ['radio', 'select'].includes(o.__config__.yunzhupaasKey) && o.__config__.dataType != 'dynamic' && !o.multiple,
|
||||
);
|
||||
state.columnOptions = columnOptions.map(o => ({
|
||||
label: o.fullName,
|
||||
labelI18nCode: o.__config__.labelI18nCode || '',
|
||||
prop: o.id,
|
||||
fixed: 'none',
|
||||
align: 'left',
|
||||
yunzhupaasKey: o.__config__.yunzhupaasKey,
|
||||
sortable: false,
|
||||
resizable: true,
|
||||
width: null,
|
||||
...o,
|
||||
}));
|
||||
state.searchOptions = searchOptions.map(o => ({
|
||||
label: o.fullName,
|
||||
labelI18nCode: o.__config__.labelI18nCode || '',
|
||||
prop: o.id,
|
||||
yunzhupaasKey: o.__config__.yunzhupaasKey,
|
||||
value: getDefaultValue(o),
|
||||
searchType: getSearchType(o),
|
||||
searchMultiple: getSearchMultiple(o.__config__.yunzhupaasKey),
|
||||
isKeyword: false,
|
||||
...o,
|
||||
}));
|
||||
state.columnData.columnOptions = columnOptions;
|
||||
if (!state.columnOptions.length) state.columnData.columnList = [];
|
||||
if (!state.searchOptions.length) state.columnData.searchList = [];
|
||||
// 处理旧数据兼容问题
|
||||
state.columnData.btnsList = setBtnValue(state.columnData.btnsList, cloneDeep(state.defaultBtnsList));
|
||||
state.columnData.columnBtnsList = setBtnValue(state.columnData.columnBtnsList, cloneDeep(state.defaultColumnBtnsList));
|
||||
nextTick(() => {
|
||||
state.columnData.searchList = setListValue(state.columnData.searchList, cloneDeep(state.searchOptions), 'search');
|
||||
state.columnData.columnList = setListValue(state.columnData.columnList, cloneDeep(state.columnOptions), 'column');
|
||||
initSort();
|
||||
buildOptions(state.columnData.searchList);
|
||||
});
|
||||
}
|
||||
function onSearchMultipleChange(record, index) {
|
||||
state.columnData.searchList[index].value = getDefaultValue(record);
|
||||
}
|
||||
function buildOptions(componentList) {
|
||||
if (unref(webType) == 4) return;
|
||||
componentList.forEach(cur => {
|
||||
const config = cur.__config__;
|
||||
if (dyOptionsList.includes(config.yunzhupaasKey)) {
|
||||
if (config.dataType === 'dictionary' && config.dictionaryType) {
|
||||
cur.options = [];
|
||||
getDictionaryDataSelector(config.dictionaryType).then(res => {
|
||||
cur.options = res.data.list;
|
||||
});
|
||||
}
|
||||
if (config.dataType === 'dynamic' && config.propsUrl) {
|
||||
cur.options = [];
|
||||
const query = { paramList: getParamList(config.templateJson) };
|
||||
getDataInterfaceRes(config.propsUrl, query).then(res => {
|
||||
cur.options = Array.isArray(res.data) ? res.data : [];
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
function onRelationChange(val) {
|
||||
if (!val) return;
|
||||
state.columnData.searchList = state.columnData.searchList.filter(o => o.id !== val);
|
||||
state.columnData.searchList = setListValue(state.columnData.searchList, cloneDeep(state.searchOptions), 'search');
|
||||
}
|
||||
function onTabOnChange(val) {
|
||||
if (!val) state.columnData.tabConfig.relationField = '';
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (typeof props.conf === 'object' && props.conf !== null) {
|
||||
state.columnData = cloneDeep(Object.assign({}, defaultAppColumnData, props.conf));
|
||||
}
|
||||
state.defaultBtnsList = defaultAppBtnsList;
|
||||
state.defaultColumnBtnsList = defaultColumnBtnsList;
|
||||
if (unref(webType) != 4) {
|
||||
init(unref(formFieldsOptions));
|
||||
} else {
|
||||
init(unref(viewFieldOptions));
|
||||
}
|
||||
});
|
||||
</script>
|
||||
79
src/components/ColumnDesign/src/components/UpLoadTpl.vue
Normal file
79
src/components/ColumnDesign/src/components/UpLoadTpl.vue
Normal file
@@ -0,0 +1,79 @@
|
||||
<template>
|
||||
<BasicModal v-bind="$attrs" @register="registerModal" title="模板设置" :width="600" @ok="handleSubmit" destroyOnClose class="export-modal">
|
||||
<a-form :colon="false" labelAlign="left" :labelCol="{ style: { width: '90px' } }">
|
||||
<a-form-item label="导入模式">
|
||||
<a-radio-group v-model:value="dataType">
|
||||
<a-radio value="1">仅新增数据<BasicHelp text="导入数据只能进行新增,同一条数据无法重复导入" /></a-radio>
|
||||
<a-radio value="2">更新和新增数据<BasicHelp text="允许新增数据的同时支持导入数据更新" /></a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<div class="export-line">
|
||||
<p class="export-label">表单数据<span>请选择要导入的字段</span></p>
|
||||
</div>
|
||||
<a-checkbox :indeterminate="isIndeterminate" v-model:checked="checkAll" @change="handleCheckAllChange">全选</a-checkbox>
|
||||
<a-checkbox-group v-model:value="checkedList" class="options-list" @change="handleCheckedChange">
|
||||
<a-checkbox v-for="item in columnList" :key="item.id" :value="item.id" :disabled="item.disabled" class="options-item">
|
||||
{{ item.fullName }}
|
||||
</a-checkbox>
|
||||
</a-checkbox-group>
|
||||
</a-form>
|
||||
</BasicModal>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { BasicModal, useModalInner } from '@/components/Modal';
|
||||
import { systemComponentsList, noUploadList } from '../helper/config';
|
||||
import { useMessage } from '@/hooks/web/useMessage';
|
||||
|
||||
const emit = defineEmits(['register', 'confirm']);
|
||||
const [registerModal, { closeModal }] = useModalInner(init);
|
||||
const { createMessage } = useMessage();
|
||||
const dataType = ref('1');
|
||||
const isIndeterminate = ref(false);
|
||||
const checkAll = ref(false);
|
||||
const columnList = ref<any[]>([]);
|
||||
const checkedList = ref<string[]>([]);
|
||||
const defaultCheckedList = ref<string[]>([]);
|
||||
|
||||
function init(data) {
|
||||
dataType.value = data.dataType || '1';
|
||||
columnList.value = [];
|
||||
checkedList.value = [];
|
||||
isIndeterminate.value = false;
|
||||
checkAll.value = false;
|
||||
defaultCheckedList.value = [];
|
||||
for (let i = 0; i < data.fieldsOptions.length; i++) {
|
||||
const e = data.fieldsOptions[i];
|
||||
const required = e.__config__.required;
|
||||
const yunzhupaasKey = e.__config__.yunzhupaasKey;
|
||||
if (![...noUploadList, ...systemComponentsList].includes(yunzhupaasKey)) {
|
||||
columnList.value.push({ id: e.id, fullName: e.fullName, disabled: required });
|
||||
if (required) {
|
||||
checkedList.value.push(e.id);
|
||||
defaultCheckedList.value.push(e.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (data.selectKey?.length) {
|
||||
checkedList.value = [...checkedList.value, ...data.selectKey];
|
||||
checkedList.value = Array.from(new Set(checkedList.value));
|
||||
}
|
||||
if (!checkedList.value.length) return;
|
||||
handleCheckedChange(checkedList.value);
|
||||
}
|
||||
function handleCheckAllChange(e) {
|
||||
const val = e.target.checked;
|
||||
checkedList.value = val ? columnList.value.map(o => o.id) : defaultCheckedList.value;
|
||||
isIndeterminate.value = val ? false : !!defaultCheckedList.value.length;
|
||||
}
|
||||
function handleCheckedChange(val) {
|
||||
const checkedCount = val.length;
|
||||
checkAll.value = checkedCount === columnList.value.length;
|
||||
isIndeterminate.value = !!checkedCount && checkedCount < columnList.value.length;
|
||||
}
|
||||
function handleSubmit() {
|
||||
if (!checkedList.value.length) return createMessage.warning('请至少选择一个导入字段');
|
||||
emit('confirm', { dataType: dataType.value, selectKey: checkedList.value });
|
||||
closeModal();
|
||||
}
|
||||
</script>
|
||||
180
src/components/ColumnDesign/src/helper/config.ts
Normal file
180
src/components/ColumnDesign/src/helper/config.ts
Normal file
@@ -0,0 +1,180 @@
|
||||
import {
|
||||
useInputList,
|
||||
useDateList,
|
||||
noVModelList,
|
||||
noColumnShowList,
|
||||
noSearchList,
|
||||
systemComponentsList,
|
||||
noGroupList,
|
||||
} from '@/components/FormGenerator/src/helper/config';
|
||||
|
||||
const noUploadList = [
|
||||
...noVModelList,
|
||||
'uploadFile',
|
||||
'uploadImg',
|
||||
'colorPicker',
|
||||
'popupTableSelect',
|
||||
'relationForm',
|
||||
'popupSelect',
|
||||
'calculate',
|
||||
'sign',
|
||||
'signature',
|
||||
'location',
|
||||
];
|
||||
|
||||
const getSearchType = item => {
|
||||
const yunzhupaasKey = item.__config__.yunzhupaasKey;
|
||||
// 等于-1 模糊-2 范围-3
|
||||
const fuzzyList = [...useInputList];
|
||||
const RangeList = [...useDateList, 'timePicker', 'datePicker', 'inputNumber', 'calculate', 'rate', 'slider'];
|
||||
if (RangeList.includes(yunzhupaasKey)) return 3;
|
||||
if (fuzzyList.includes(yunzhupaasKey)) return 2;
|
||||
return 1;
|
||||
};
|
||||
const getSearchMultiple = yunzhupaasKey => {
|
||||
const searchMultipleList = ['select', 'depSelect', 'roleSelect', 'userSelect', 'usersSelect', 'organizeSelect', 'posSelect', 'groupSelect'];
|
||||
return searchMultipleList.includes(yunzhupaasKey);
|
||||
};
|
||||
const getDefaultValue = item => {
|
||||
if (item.__config__.isFromParam) return undefined;
|
||||
const yunzhupaasKey = item.__config__.yunzhupaasKey;
|
||||
const list = ['areaSelect', 'timePicker', 'datePicker', 'inputNumber', 'organizeSelect', 'calculate'];
|
||||
return list.includes(yunzhupaasKey) || item.multiple ? [] : undefined;
|
||||
};
|
||||
const defaultBtnEnableFunc = '({ row, rowIndex, onlineUtils }) => {\r\n\r\n return true \r\n}';
|
||||
const defaultFuncsData = {
|
||||
afterOnload: '({ data, tableRef, onlineUtils }) => {\r\n \r\n}',
|
||||
rowStyle: '({ row, rowIndex }) => {\r\n \r\n}',
|
||||
cellStyle: '({ row, column, rowIndex, columnIndex }) => {\r\n \r\n}',
|
||||
};
|
||||
const defaultBtnsList = [
|
||||
{ show: true, value: 'add', icon: 'icon-ym icon-ym-btn-add', label: '新增', labelI18nCode: 'common.add2Text' },
|
||||
{ show: false, value: 'download', icon: 'icon-ym icon-ym-btn-download', label: '导出', labelI18nCode: 'common.exportText' },
|
||||
{ show: false, value: 'upload', icon: 'icon-ym icon-ym-btn-upload', label: '导入', labelI18nCode: 'common.importText' },
|
||||
{ show: false, value: 'batchRemove', icon: 'icon-ym icon-ym-btn-clearn', label: '批量删除', labelI18nCode: 'common.batchDelText' },
|
||||
{ show: false, value: 'batchPrint', icon: 'icon-ym icon-ym-report-icon-preview-printPreview', label: '批量打印', labelI18nCode: 'common.batchPrintText' },
|
||||
];
|
||||
const defaultAppBtnsList = defaultBtnsList.filter(o => ['add', 'batchRemove'].includes(o.value));
|
||||
const defaultColumnBtnsList = [
|
||||
{
|
||||
show: true,
|
||||
value: 'edit',
|
||||
icon: 'icon-ym icon-ym-btn-edit',
|
||||
label: '编辑',
|
||||
labelI18nCode: 'common.editText',
|
||||
event: { enableFunc: defaultBtnEnableFunc },
|
||||
},
|
||||
{
|
||||
show: true,
|
||||
value: 'remove',
|
||||
icon: 'icon-ym icon-ym-btn-clearn',
|
||||
label: '删除',
|
||||
labelI18nCode: 'common.delText',
|
||||
event: { enableFunc: defaultBtnEnableFunc },
|
||||
},
|
||||
{
|
||||
show: true,
|
||||
value: 'detail',
|
||||
icon: 'icon-ym icon-ym-generator-menu',
|
||||
label: '详情',
|
||||
labelI18nCode: 'common.detailText',
|
||||
event: { enableFunc: defaultBtnEnableFunc },
|
||||
},
|
||||
];
|
||||
|
||||
const defaultColumnData = {
|
||||
ruleList: { matchLogic: 'and', conditionList: [] }, // 过滤规则
|
||||
searchList: [], // 查询字段
|
||||
hasSuperQuery: true, // 高级查询
|
||||
showOverflow: true, // 溢出省略
|
||||
childTableStyle: 1, // 子表样式
|
||||
showSummary: false, // 合计配置
|
||||
summaryField: [], // 合计字段
|
||||
columnList: [], // 字段列表
|
||||
columnOptions: [], // 字段列表
|
||||
defaultColumnList: [], // 所有可选择字段列表
|
||||
type: 1, //列表类型
|
||||
defaultSortConfig: [], // 默认排序配置
|
||||
viewKey: '', //视图主键
|
||||
hasPage: true, // 列表分页
|
||||
pageSize: 20, // 分页条数
|
||||
hasTreeQuery: false, //左侧树查询
|
||||
treeTitle: '左侧标题', // 树形标题
|
||||
treeTitleI18nCode: '', // 树形标题多语言标识
|
||||
treeDataSource: 'dictionary', // 树形数据来源
|
||||
treeDictionary: '', //数据字典
|
||||
treeRelation: '', // 关联字段
|
||||
treeRelationFieldSelectType: 'all', // 关联字段选择类型
|
||||
treeRelationFieldAbleIds: [], // 关联字段范围
|
||||
treeSyncType: 0, //数据加载 同步、异步
|
||||
treeSyncInterfaceId: '',
|
||||
treeSyncInterfaceName: '',
|
||||
treeSyncTemplateJson: [],
|
||||
treePropsUrl: '', // 数据选择id
|
||||
treePropsName: '', // 数据选择名称
|
||||
treeTemplateJson: [],
|
||||
treePropsValue: 'id', // 主键字段
|
||||
treePropsChildren: 'children', // 子级字段
|
||||
treePropsLabel: 'fullName', // 回显字段
|
||||
groupField: '', // 分组字段
|
||||
parentField: '', // 父级字段
|
||||
printIds: [],
|
||||
useColumnPermission: false,
|
||||
useFormPermission: false,
|
||||
useBtnPermission: false,
|
||||
useDataPermission: false,
|
||||
customBtnsList: [],
|
||||
btnsList: defaultBtnsList, // 按钮
|
||||
columnBtnsList: defaultColumnBtnsList, // 列按钮
|
||||
funcs: {
|
||||
afterOnload: '({ data, tableRef, onlineUtils }) => {\r\n \r\n}',
|
||||
rowStyle: '({ row, rowIndex }) => {\r\n \r\n}',
|
||||
cellStyle: '({ row, column, rowIndex, columnIndex }) => {\r\n \r\n}',
|
||||
},
|
||||
uploaderTemplateJson: {},
|
||||
complexHeaderList: [],
|
||||
tabConfig: { on: false, relationField: '', hasAllTab: true }, //标签面板
|
||||
};
|
||||
const defaultAppColumnData = {
|
||||
ruleListApp: { matchLogic: 'and', conditionList: [] }, // 过滤规则
|
||||
searchList: [], // 查询字段
|
||||
hasSuperQuery: false, // 高级查询
|
||||
showOverflow: true, // 溢出省略
|
||||
columnList: [], // 字段列表
|
||||
columnOptions: [],
|
||||
defaultColumnList: [], // 所有可选择字段列表
|
||||
type: 1, //列表类型
|
||||
defaultSortConfig: [], // 默认排序配置
|
||||
viewKey: '', //视图主键
|
||||
hasPage: true, // 列表分页
|
||||
pageSize: 20, // 分页条数
|
||||
useColumnPermission: false,
|
||||
useFormPermission: false,
|
||||
useBtnPermission: false,
|
||||
useDataPermission: false,
|
||||
customBtnsList: [],
|
||||
btnsList: defaultAppBtnsList, // 按钮
|
||||
columnBtnsList: defaultColumnBtnsList, // 列按钮
|
||||
funcs: {
|
||||
afterOnload: '({ data, tableRef, onlineUtils }) => {\r\n \r\n}',
|
||||
},
|
||||
tabConfig: { on: false, relationField: '', hasAllTab: true }, //标签面板
|
||||
};
|
||||
|
||||
export {
|
||||
noColumnShowList,
|
||||
noSearchList,
|
||||
systemComponentsList,
|
||||
noGroupList,
|
||||
noUploadList,
|
||||
getSearchType,
|
||||
getSearchMultiple,
|
||||
getDefaultValue,
|
||||
defaultBtnEnableFunc,
|
||||
defaultFuncsData,
|
||||
defaultColumnData,
|
||||
defaultAppColumnData,
|
||||
defaultBtnsList,
|
||||
defaultAppBtnsList,
|
||||
defaultColumnBtnsList,
|
||||
};
|
||||
238
src/components/ColumnDesign/style/index.less
Normal file
238
src/components/ColumnDesign/style/index.less
Normal file
@@ -0,0 +1,238 @@
|
||||
@prefix-cls: ~'@{namespace}-basic-column-design';
|
||||
|
||||
.@{prefix-cls} {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
.head-tabs {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: calc(100% - 350px);
|
||||
height: 42px;
|
||||
border-bottom: 1px solid @border-color-base1;
|
||||
background: @component-background;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding: 0 10px;
|
||||
z-index: 100;
|
||||
border-radius: 8px 8px 0 0;
|
||||
.ant-btn {
|
||||
padding: 0;
|
||||
margin-left: 15px;
|
||||
}
|
||||
.unActive-btn {
|
||||
color: @text-color !important;
|
||||
&:hover {
|
||||
color: @primary-color !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
.column-empty-box {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: @component-background;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
.empty-img {
|
||||
width: 180px;
|
||||
height: 120px;
|
||||
}
|
||||
p {
|
||||
padding: 15px 0;
|
||||
}
|
||||
}
|
||||
.column-design-container {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
padding-top: 42px;
|
||||
.main-board {
|
||||
height: 100%;
|
||||
width: auto;
|
||||
margin: 0 350px 0 0;
|
||||
padding: 10px;
|
||||
overflow: auto;
|
||||
overflow-x: hidden;
|
||||
background-color: @component-background;
|
||||
border-radius: 0 0 8px 8px;
|
||||
}
|
||||
.right-board {
|
||||
width: 340px;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
margin-left: 10px;
|
||||
background-color: @component-background;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
.right-main {
|
||||
position: relative;
|
||||
height: calc(100% - 42px);
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
.scrollbar__view {
|
||||
padding: 10px;
|
||||
}
|
||||
.right-board-form {
|
||||
.ant-form-item {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
.typeList {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
.item:nth-child(3n + 3) {
|
||||
margin-right: 0;
|
||||
}
|
||||
.item {
|
||||
width: 100px;
|
||||
margin-bottom: 15px;
|
||||
margin-right: 10px;
|
||||
border-bottom: unset !important;
|
||||
&.view-item {
|
||||
width: 150px;
|
||||
.item-img {
|
||||
height: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
.item-img {
|
||||
width: 100%;
|
||||
height: 70px;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
border: 1px solid transparent;
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: -1;
|
||||
}
|
||||
&.checked {
|
||||
border: 1px solid @primary-color;
|
||||
}
|
||||
.icon-checked {
|
||||
display: block;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border: 12px solid @primary-color;
|
||||
border-left: 12px solid transparent !important;
|
||||
border-top: 12px solid transparent !important;
|
||||
border-bottom-right-radius: 4px;
|
||||
position: absolute;
|
||||
right: -1px;
|
||||
bottom: -1px;
|
||||
.anticon-check {
|
||||
position: absolute;
|
||||
top: -1px;
|
||||
left: -1px;
|
||||
font-size: 12px;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
.item-name {
|
||||
font-size: 12px;
|
||||
color: @text-color-secondary;
|
||||
margin-top: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
.right-radio {
|
||||
.ant-radio-button-wrapper {
|
||||
padding: 0 11px;
|
||||
}
|
||||
}
|
||||
.btnsList {
|
||||
width: 100%;
|
||||
.btnsList-cell {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 10px;
|
||||
width: 100%;
|
||||
}
|
||||
.ant-checkbox-wrapper {
|
||||
width: 90px;
|
||||
flex-shrink: 0;
|
||||
line-height: 32px;
|
||||
}
|
||||
.btn-upload {
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
.btn-cap {
|
||||
margin-bottom: 10px;
|
||||
color: @text-color-secondary;
|
||||
}
|
||||
.custom-btns-list {
|
||||
.custom-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 1px dashed @component-background;
|
||||
box-sizing: border-box;
|
||||
& + .custom-item {
|
||||
margin-top: 4px;
|
||||
}
|
||||
&.sortable-chosen {
|
||||
border: 1px dashed @primary-color;
|
||||
}
|
||||
.ant-input + .ant-input {
|
||||
margin-left: 4px;
|
||||
}
|
||||
.ant-input-group-addon {
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
span {
|
||||
display: inline-block;
|
||||
line-height: 30px;
|
||||
padding: 0 11px;
|
||||
}
|
||||
}
|
||||
.custom-line-icon {
|
||||
line-height: 32px;
|
||||
font-size: 22px;
|
||||
padding: 0 4px;
|
||||
color: #606266;
|
||||
.icon-ym-btn-clearn {
|
||||
font-size: 18px;
|
||||
}
|
||||
.icon-ym-darg {
|
||||
font-size: 20px;
|
||||
line-height: 31px;
|
||||
display: inline-block;
|
||||
cursor: move;
|
||||
}
|
||||
&.option-drag {
|
||||
padding-left: 0;
|
||||
}
|
||||
&.close-btn {
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
.custom-line-value {
|
||||
width: 90px;
|
||||
flex-shrink: 0;
|
||||
line-height: 32px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.close-btn {
|
||||
cursor: pointer;
|
||||
color: @error-color;
|
||||
}
|
||||
}
|
||||
.add-btn .ant-btn {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
24
src/components/CommonModal/index.ts
Normal file
24
src/components/CommonModal/index.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { withInstall } from '@/utils';
|
||||
import interfaceModal from './src/InterfaceModal.vue';
|
||||
import billRuleModal from './src/BillRuleModal.vue';
|
||||
import selectModal from './src/SelectModal.vue';
|
||||
import previewModal from './src/PreviewModal.vue';
|
||||
import exportModal from './src/ExportModal.vue';
|
||||
import importModal from './src/ImportModal.vue';
|
||||
import superQueryModal from './src/SuperQueryModal.vue';
|
||||
import selectFlowModal from './src/SelectFlowModal.vue';
|
||||
import dataSetModal from './src/DataSetModal/index.vue';
|
||||
import userSelect from './src/UserSelect.vue';
|
||||
import validatePopover from './src/ValidatePopover.vue';
|
||||
|
||||
export const InterfaceModal = withInstall(interfaceModal);
|
||||
export const BillRuleModal = withInstall(billRuleModal);
|
||||
export const SelectModal = withInstall(selectModal);
|
||||
export const PreviewModal = withInstall(previewModal);
|
||||
export const ExportModal = withInstall(exportModal);
|
||||
export const ImportModal = withInstall(importModal);
|
||||
export const SuperQueryModal = withInstall(superQueryModal);
|
||||
export const SelectFlowModal = withInstall(selectFlowModal);
|
||||
export const DataSetModal = withInstall(dataSetModal);
|
||||
export const UserSelect = withInstall(userSelect);
|
||||
export const ValidatePopover = withInstall(validatePopover);
|
||||
160
src/components/CommonModal/src/BillRuleModal.vue
Normal file
160
src/components/CommonModal/src/BillRuleModal.vue
Normal file
@@ -0,0 +1,160 @@
|
||||
<template>
|
||||
<div class="common-container">
|
||||
<a-select v-model:value="innerValue" v-bind="getSelectBindValue" :options="options" @change="onChange" @click="openSelectModal" />
|
||||
<a-modal
|
||||
v-model:open="visible"
|
||||
:title="popupTitle"
|
||||
:width="1000"
|
||||
class="common-container-modal"
|
||||
@ok="handleSubmit"
|
||||
@cancel="handleCancel"
|
||||
:maskClosable="false">
|
||||
<template #closeIcon>
|
||||
<ModalClose :canFullscreen="false" @cancel="handleCancel" />
|
||||
</template>
|
||||
<div class="yunzhupaas-content-wrapper">
|
||||
<div class="yunzhupaas-content-wrapper-left">
|
||||
<BasicLeftTree ref="leftTreeRef" :showSearch="false" :treeData="treeData" :loading="treeLoading" @select="handleTreeSelect" />
|
||||
</div>
|
||||
<div class="yunzhupaas-content-wrapper-center">
|
||||
<div class="yunzhupaas-content-wrapper-content">
|
||||
<BasicTable @register="registerTable" :searchInfo="searchInfo" class="yunzhupaas-sub-table"></BasicTable>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { getBillRuleSelector } from '@/api/system/billRule';
|
||||
import { Form, Modal as AModal } from 'ant-design-vue';
|
||||
import { reactive, ref, unref, watch, computed, nextTick } from 'vue';
|
||||
import ModalClose from '@/components/Modal/src/components/ModalClose.vue';
|
||||
import { BasicLeftTree, TreeActionType } from '@/components/Tree';
|
||||
import { BasicTable, useTable, BasicColumn } from '@/components/Table';
|
||||
import { useI18n } from '@/hooks/web/useI18n';
|
||||
import { useBaseStore } from '@/store/modules/base';
|
||||
import { pick } from 'lodash-es';
|
||||
|
||||
defineOptions({ inheritAttrs: false });
|
||||
const props = defineProps({
|
||||
value: { default: '' },
|
||||
title: { type: String, default: '' },
|
||||
popupTitle: { type: String, default: '模板' },
|
||||
disabled: { type: Boolean, default: false },
|
||||
allowClear: { type: Boolean, default: true },
|
||||
size: { type: String, default: 'default' },
|
||||
});
|
||||
const emit = defineEmits(['update:value', 'change']);
|
||||
const formItemContext = Form.useInjectFormItemContext();
|
||||
const { t } = useI18n();
|
||||
const baseStore = useBaseStore();
|
||||
const innerValue = ref(undefined);
|
||||
const visible = ref(false);
|
||||
const options = ref<any[]>([]);
|
||||
|
||||
const columns: BasicColumn[] = [
|
||||
{ title: '业务名称', dataIndex: 'fullName' },
|
||||
{ title: '业务编码', dataIndex: 'enCode' },
|
||||
];
|
||||
const searchInfo = reactive({
|
||||
categoryId: '',
|
||||
});
|
||||
const leftTreeRef = ref<Nullable<TreeActionType>>(null);
|
||||
const treeLoading = ref(false);
|
||||
const treeData = ref<any[]>([]);
|
||||
const [registerTable, { getForm, getSelectRows, setSelectedRowKeys, getSelectRowKeys }] = useTable({
|
||||
api: getBillRuleSelector,
|
||||
columns,
|
||||
immediate: false,
|
||||
useSearchForm: true,
|
||||
formConfig: {
|
||||
baseColProps: { span: 8 },
|
||||
schemas: [
|
||||
{
|
||||
field: 'keyword',
|
||||
label: t('common.keyword'),
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: t('common.enterKeyword'),
|
||||
submitOnPressEnter: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
rowKey: 'enCode',
|
||||
tableSetting: { size: false, setting: false },
|
||||
isCanResizeParent: true,
|
||||
resizeHeightOffset: -74,
|
||||
rowSelection: { type: 'radio' },
|
||||
});
|
||||
|
||||
const getSelectBindValue = computed(() => {
|
||||
return {
|
||||
...pick(props, ['disabled', 'size', 'allowClear']),
|
||||
fieldNames: { label: 'fullName', value: 'id' },
|
||||
placeholder: '请选择',
|
||||
open: false,
|
||||
showSearch: false,
|
||||
showArrow: true,
|
||||
};
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.value,
|
||||
val => {
|
||||
setValue(val);
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
function setValue(value) {
|
||||
innerValue.value = value || undefined;
|
||||
options.value = [{ id: innerValue.value, fullName: props.title }];
|
||||
}
|
||||
function onChange() {
|
||||
options.value = [];
|
||||
emit('change', '', {});
|
||||
}
|
||||
async function openSelectModal() {
|
||||
if (props.disabled) return;
|
||||
visible.value = true;
|
||||
treeLoading.value = true;
|
||||
const res = (await baseStore.getDictionaryData('businessType')) as any[];
|
||||
treeData.value = [{ id: '0', fullName: '业务分类', children: res }];
|
||||
searchInfo.categoryId = '';
|
||||
nextTick(() => {
|
||||
const leftTree = unref(leftTreeRef);
|
||||
leftTree?.setSelectedKeys(['0']);
|
||||
treeLoading.value = false;
|
||||
getForm().resetFields();
|
||||
setSelectedRowKeys(innerValue.value ? [innerValue.value] : []);
|
||||
});
|
||||
}
|
||||
function handleTreeSelect(id) {
|
||||
if (!id || searchInfo.categoryId === id) return;
|
||||
searchInfo.categoryId = id === '0' ? '' : id;
|
||||
getForm().resetFields();
|
||||
}
|
||||
function handleCancel() {
|
||||
visible.value = false;
|
||||
}
|
||||
function handleSubmit() {
|
||||
if (!getSelectRowKeys().length && !getSelectRows().length) return;
|
||||
if (!getSelectRows().length) {
|
||||
emit('update:value', innerValue.value);
|
||||
emit('change', innerValue.value, options.value[0]);
|
||||
formItemContext.onFieldChange();
|
||||
handleCancel();
|
||||
return;
|
||||
}
|
||||
const selectRow = getSelectRows()[0];
|
||||
options.value = getSelectRows();
|
||||
innerValue.value = selectRow.enCode;
|
||||
emit('update:value', selectRow.enCode);
|
||||
emit('change', selectRow.enCode, selectRow);
|
||||
formItemContext.onFieldChange();
|
||||
handleCancel();
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,270 @@
|
||||
<template>
|
||||
<BasicDrawer
|
||||
v-bind="$attrs"
|
||||
width="500px"
|
||||
@register="registerDrawer"
|
||||
title="筛选设置"
|
||||
class="dataSet-table-config-drawer"
|
||||
showFooter
|
||||
:maskClosable="false"
|
||||
@ok="handleSubmit"
|
||||
destroyOnClose>
|
||||
<div class="p-20px">
|
||||
<div class="condition-main overflow-auto">
|
||||
<div class="mb-10px" v-if="dataForm.ruleList?.length">
|
||||
<yunzhupaas-radio v-model:value="dataForm.matchLogic" :options="logicOptions" optionType="button" button-style="solid" />
|
||||
</div>
|
||||
<div class="condition-item" v-for="(item, index) in dataForm.ruleList" :key="index">
|
||||
<div class="condition-item-title">
|
||||
<div>条件组</div>
|
||||
<i class="icon-ym icon-ym-nav-close" @click="delRuleGroup(index)"></i>
|
||||
</div>
|
||||
<div class="condition-item-content">
|
||||
<div class="condition-item-cap">
|
||||
以下条件全部执行:
|
||||
<yunzhupaas-radio v-model:value="item.logic" :options="logicOptions" optionType="button" button-style="solid" size="small" />
|
||||
</div>
|
||||
<a-row :gutter="8" v-for="(child, childIndex) in item.groups" :key="index + childIndex" wrap class="mb-10px">
|
||||
<a-col :span="18" class="!flex items-center">
|
||||
<yunzhupaas-select v-model:value="child.field" :options="fieldList" placeholder="请选择字段" allowClear showSearch />
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<yunzhupaas-select class="w-full" v-model:value="child.dataType" :options="dataTypeOptions" @change="onDataTypeChange(child)" />
|
||||
</a-col>
|
||||
<a-col :span="6" class="mt-10px">
|
||||
<yunzhupaas-select
|
||||
class="w-full"
|
||||
v-model:value="child.symbol"
|
||||
:options="child.dataType === 'text' ? baseSymbolOptions : rangeSymbolOptions"
|
||||
@change="onSymbolChange(child)" />
|
||||
</a-col>
|
||||
<a-col :span="16" class="mt-10px" v-if="['double', 'bigint', 'date', 'time'].includes(child.dataType)">
|
||||
<template v-if="['double', 'bigint'].includes(child.dataType)">
|
||||
<yunzhupaas-number-range v-model:value="child.fieldValue" v-if="child.symbol == 'between'" />
|
||||
<yunzhupaas-input-number v-model:value="child.fieldValue" placeholder="请输入" :disabled="['null', 'notNull'].includes(child.symbol)" v-else />
|
||||
</template>
|
||||
<template v-else-if="['date', 'time'].includes(child.dataType)">
|
||||
<yunzhupaas-date-range
|
||||
v-model:value="child.fieldValue"
|
||||
:format="child.dataType === 'time' ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD'"
|
||||
allowClear
|
||||
v-if="child.symbol == 'between'" />
|
||||
<yunzhupaas-date-picker
|
||||
v-model:value="child.fieldValue"
|
||||
:format="child.dataType === 'time' ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD'"
|
||||
allowClear
|
||||
:disabled="['null', 'notNull'].includes(child.symbol)"
|
||||
v-else />
|
||||
</template>
|
||||
</a-col>
|
||||
<template v-else>
|
||||
<a-col :span="6" class="mt-10px">
|
||||
<yunzhupaas-select
|
||||
v-model:value="child.fieldValueType"
|
||||
:options="fieldValueTypeOptions"
|
||||
@change="onFieldValueTypeChange(child)"
|
||||
:disabled="['null', 'notNull'].includes(child.symbol)" />
|
||||
</a-col>
|
||||
<a-col :span="10" class="mt-10px">
|
||||
<a-input
|
||||
v-model:value="child.fieldValue"
|
||||
placeholder="请输入"
|
||||
allowClear
|
||||
:disabled="['null', 'notNull'].includes(child.symbol)"
|
||||
v-if="child.fieldValueType === 1" />
|
||||
<yunzhupaas-select v-model:value="child.fieldValue" :options="sysVariableList" allowClear showSearch placeholder="请选择系统参数" v-else />
|
||||
</a-col>
|
||||
</template>
|
||||
<a-col :span="2" class="text-center mt-10px">
|
||||
<i class="icon-ym icon-ym-btn-clearn" @click="delRuleItem(index, childIndex)" />
|
||||
</a-col>
|
||||
</a-row>
|
||||
<span class="link-text inline-block" @click="addRuleItem(index)"><i class="icon-ym icon-ym-btn-add text-14px mr-4px"></i>添加条件</span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="link-text inline-block" @click="addRuleGroup()"><i class="icon-ym icon-ym-btn-add text-14px mr-4px"></i>添加条件组</span>
|
||||
</div>
|
||||
</div>
|
||||
</BasicDrawer>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { reactive, toRefs } from 'vue';
|
||||
import { BasicDrawer, useDrawerInner } from '@/components/Drawer';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import { useMessage } from '@/hooks/web/useMessage';
|
||||
import { isEmpty } from '@/utils/is';
|
||||
import { treeToList } from '@/utils/helper/treeHelper';
|
||||
|
||||
interface State {
|
||||
dataForm: any;
|
||||
fieldList: any[];
|
||||
}
|
||||
|
||||
defineProps({
|
||||
sysVariableList: { type: Array, default: () => [] },
|
||||
});
|
||||
|
||||
const emit = defineEmits(['register', 'confirm']);
|
||||
const logicOptions = [
|
||||
{ id: 'and', fullName: '且' },
|
||||
{ id: 'or', fullName: '或' },
|
||||
];
|
||||
const dataTypeOptions = [
|
||||
{ id: 'text', fullName: 'text' },
|
||||
{ id: 'double', fullName: 'double' },
|
||||
{ id: 'bigint', fullName: 'bigint' },
|
||||
{ id: 'date', fullName: 'date' },
|
||||
{ id: 'time', fullName: 'time' },
|
||||
];
|
||||
const baseSymbolOptions = [
|
||||
{ id: '==', fullName: '等于' },
|
||||
{ id: '<>', fullName: '不等于' },
|
||||
{ id: 'like', fullName: '包含' },
|
||||
{ id: 'notLike', fullName: '不包含' },
|
||||
{ id: 'null', fullName: '为空' },
|
||||
{ id: 'notNull', fullName: '不为空' },
|
||||
];
|
||||
const rangeSymbolOptions = [
|
||||
{ id: '>=', fullName: '大于等于' },
|
||||
{ id: '>', fullName: '大于' },
|
||||
{ id: '==', fullName: '等于' },
|
||||
{ id: '<=', fullName: '小于等于' },
|
||||
{ id: '<', fullName: '小于' },
|
||||
{ id: '<>', fullName: '不等于' },
|
||||
{ id: 'between', fullName: '介于' },
|
||||
{ id: 'null', fullName: '为空' },
|
||||
{ id: 'notNull', fullName: '不为空' },
|
||||
];
|
||||
const fieldValueTypeOptions = [
|
||||
{ id: 1, fullName: '固定值' },
|
||||
{ id: 2, fullName: '系统参数' },
|
||||
];
|
||||
const emptyChildItem = {
|
||||
field: '',
|
||||
dataType: 'text',
|
||||
symbol: '==',
|
||||
fieldValue: '',
|
||||
fieldValueType: 1,
|
||||
};
|
||||
const emptyItem = { logic: 'and', groups: [emptyChildItem] };
|
||||
|
||||
const state = reactive<State>({
|
||||
dataForm: {
|
||||
ruleList: [],
|
||||
},
|
||||
fieldList: [],
|
||||
});
|
||||
const { dataForm, fieldList } = toRefs(state);
|
||||
const [registerDrawer, { closeDrawer }] = useDrawerInner(init);
|
||||
const { createMessage } = useMessage();
|
||||
|
||||
function init(data) {
|
||||
getTableFieldList(data.visualConfigJson);
|
||||
state.dataForm = cloneDeep(data.data);
|
||||
}
|
||||
|
||||
function getTableFieldList(treeList = []) {
|
||||
const list = treeToList(treeList, { id: 'table' });
|
||||
state.fieldList = list.map(o => ({
|
||||
id: o.table,
|
||||
fullName: o.tableName,
|
||||
options: o.fieldList.map(c => ({ id: o.table + '-' + c.field, fullName: c.fieldName, dataType: c.dataType })),
|
||||
}));
|
||||
}
|
||||
function addRuleItem(index) {
|
||||
state.dataForm.ruleList[index].groups.push(cloneDeep(emptyChildItem));
|
||||
}
|
||||
function delRuleItem(index, childIndex) {
|
||||
state.dataForm.ruleList[index].groups.splice(childIndex, 1);
|
||||
if (!state.dataForm.ruleList[index].groups.length) delRuleGroup(index);
|
||||
}
|
||||
function addRuleGroup() {
|
||||
state.dataForm.ruleList.push(cloneDeep(emptyItem));
|
||||
}
|
||||
function delRuleGroup(index) {
|
||||
state.dataForm.ruleList.splice(index, 1);
|
||||
}
|
||||
function onDataTypeChange(item) {
|
||||
item.fieldValueType = 1;
|
||||
if (item.dataType === 'text') {
|
||||
if (!baseSymbolOptions.some(o => o.id === item.symbol)) {
|
||||
item.symbol = '==';
|
||||
}
|
||||
item.fieldValue = '';
|
||||
} else {
|
||||
if (!rangeSymbolOptions.some(o => o.id === item.symbol)) {
|
||||
item.symbol = '==';
|
||||
}
|
||||
item.fieldValue = undefined;
|
||||
}
|
||||
}
|
||||
function onSymbolChange(item) {
|
||||
if (item.dataType === 'text') {
|
||||
if (['null', 'notNull'].includes(item.symbol)) {
|
||||
item.fieldValueType = 1;
|
||||
item.fieldValue = '';
|
||||
}
|
||||
} else {
|
||||
if (['null', 'notNull'].includes(item.symbol)) {
|
||||
item.fieldValue = undefined;
|
||||
} else if (item.symbol === 'between') {
|
||||
!Array.isArray(item.fieldValue) && (item.fieldValue = []);
|
||||
} else {
|
||||
Array.isArray(item.fieldValue) && (item.fieldValue = undefined);
|
||||
}
|
||||
}
|
||||
}
|
||||
function onFieldValueTypeChange(item) {
|
||||
item.fieldValue = '';
|
||||
}
|
||||
function conditionExist() {
|
||||
const list = state.dataForm.ruleList;
|
||||
let isOk = true;
|
||||
outer: for (let i = 0; i < list.length; i++) {
|
||||
const e = list[i];
|
||||
for (let j = 0; j < e.groups.length; j++) {
|
||||
const child = e.groups[j];
|
||||
if (!child.field) {
|
||||
createMessage.warning(`字段不能为空`);
|
||||
isOk = false;
|
||||
break outer;
|
||||
}
|
||||
if (child.fieldValueType === 2 && !child.fieldValue) {
|
||||
createMessage.warning(`系统参数不能为空`);
|
||||
isOk = false;
|
||||
break outer;
|
||||
}
|
||||
if (
|
||||
child.fieldValueType === 1 &&
|
||||
!['null', 'notNull'].includes(child.symbol) &&
|
||||
((!child.fieldValue && child.fieldValue !== 0) || isEmpty(child.fieldValue))
|
||||
) {
|
||||
createMessage.warning('数据值不能为空');
|
||||
isOk = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
return isOk;
|
||||
}
|
||||
function handleSubmit() {
|
||||
if (!conditionExist()) return;
|
||||
emit('confirm', state.dataForm);
|
||||
closeDrawer();
|
||||
}
|
||||
</script>
|
||||
<style lang="less">
|
||||
html[data-theme='dark'] {
|
||||
.dataSet-table-config-drawer {
|
||||
.common-cap {
|
||||
.title {
|
||||
color: #fff !important;
|
||||
}
|
||||
}
|
||||
.condition-item-title,
|
||||
.condition-item-cap {
|
||||
color: #fff !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,345 @@
|
||||
<template>
|
||||
<BasicDrawer
|
||||
v-bind="$attrs"
|
||||
width="500px"
|
||||
@register="registerDrawer"
|
||||
title="数据连接"
|
||||
class="dataSet-table-config-drawer"
|
||||
showFooter
|
||||
:maskClosable="false"
|
||||
@ok="handleSubmit"
|
||||
destroyOnClose>
|
||||
<div class="px-20px pb-20px">
|
||||
<div class="common-cap">
|
||||
<span class="title">关联关系</span>
|
||||
</div>
|
||||
<a-radio-group v-model:value="dataForm.type" button-style="solid">
|
||||
<a-radio-button :value="item.id" v-for="item in typeList" :key="item.id">
|
||||
<i class="mr-10px" :class="item.icon"></i>{{ item.fullName }}
|
||||
</a-radio-button>
|
||||
</a-radio-group>
|
||||
<div class="common-cap">
|
||||
<span class="title">关联字段</span>
|
||||
</div>
|
||||
<div class="condition-main">
|
||||
<a-row :gutter="8" v-for="(item, index) in dataForm.relationList" :key="index" class="mb-10px">
|
||||
<a-col :span="10">
|
||||
<yunzhupaas-select v-model:value="item.pField" :options="getParentTableFieldList" showSearch allowClear />
|
||||
</a-col>
|
||||
<a-col :span="2" class="leading-32px symbol-text">等于</a-col>
|
||||
<a-col :span="10">
|
||||
<yunzhupaas-select v-model:value="item.field" :options="getCurrTableFieldList" showSearch allowClear />
|
||||
</a-col>
|
||||
<a-col :span="2" class="text-center">
|
||||
<i class="icon-ym icon-ym-btn-clearn" @click="delRelationItem(index)" />
|
||||
</a-col>
|
||||
</a-row>
|
||||
<span class="link-text inline-block" @click="addRelationItem()"><i class="icon-ym icon-ym-btn-add text-14px mr-4px"></i>新增关联字段</span>
|
||||
</div>
|
||||
<div class="common-cap">
|
||||
<span class="title">条件筛选</span>
|
||||
</div>
|
||||
<div class="condition-main overflow-auto">
|
||||
<div class="mb-10px" v-if="dataForm.ruleList?.length">
|
||||
<yunzhupaas-radio v-model:value="dataForm.matchLogic" :options="logicOptions" optionType="button" button-style="solid" />
|
||||
</div>
|
||||
<div class="condition-item" v-for="(item, index) in dataForm.ruleList" :key="index">
|
||||
<div class="condition-item-title">
|
||||
<div>条件组</div>
|
||||
<i class="icon-ym icon-ym-nav-close" @click="delRuleGroup(index)"></i>
|
||||
</div>
|
||||
<div class="condition-item-content">
|
||||
<div class="condition-item-cap">
|
||||
以下条件全部执行:
|
||||
<yunzhupaas-radio v-model:value="item.logic" :options="logicOptions" optionType="button" button-style="solid" size="small" />
|
||||
</div>
|
||||
<a-row :gutter="8" v-for="(child, childIndex) in item.groups" :key="index + childIndex" wrap class="mb-10px">
|
||||
<a-col :span="18" class="!flex items-center">
|
||||
<yunzhupaas-select v-model:value="child.field" :options="getFieldList" placeholder="请选择字段" allowClear showSearch />
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<yunzhupaas-select class="w-full" v-model:value="child.dataType" :options="dataTypeOptions" @change="onDataTypeChange(child)" />
|
||||
</a-col>
|
||||
<a-col :span="6" class="mt-10px">
|
||||
<yunzhupaas-select
|
||||
class="w-full"
|
||||
v-model:value="child.symbol"
|
||||
:options="child.dataType === 'text' ? baseSymbolOptions : rangeSymbolOptions"
|
||||
@change="onSymbolChange(child)" />
|
||||
</a-col>
|
||||
<a-col :span="16" class="mt-10px" v-if="['double', 'bigint', 'date', 'time'].includes(child.dataType)">
|
||||
<template v-if="['double', 'bigint'].includes(child.dataType)">
|
||||
<yunzhupaas-number-range v-model:value="child.fieldValue" v-if="child.symbol == 'between'" />
|
||||
<yunzhupaas-input-number v-model:value="child.fieldValue" placeholder="请输入" :disabled="['null', 'notNull'].includes(child.symbol)" v-else />
|
||||
</template>
|
||||
<template v-else-if="['date', 'time'].includes(child.dataType)">
|
||||
<yunzhupaas-date-range
|
||||
v-model:value="child.fieldValue"
|
||||
:format="child.dataType === 'time' ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD'"
|
||||
allowClear
|
||||
v-if="child.symbol == 'between'" />
|
||||
<yunzhupaas-date-picker
|
||||
v-model:value="child.fieldValue"
|
||||
:format="child.dataType === 'time' ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD'"
|
||||
allowClear
|
||||
:disabled="['null', 'notNull'].includes(child.symbol)"
|
||||
v-else />
|
||||
</template>
|
||||
</a-col>
|
||||
<template v-else>
|
||||
<a-col :span="6" class="mt-10px">
|
||||
<yunzhupaas-select
|
||||
v-model:value="child.fieldValueType"
|
||||
:options="fieldValueTypeOptions"
|
||||
@change="onFieldValueTypeChange(child)"
|
||||
:disabled="['null', 'notNull'].includes(child.symbol)" />
|
||||
</a-col>
|
||||
<a-col :span="10" class="mt-10px">
|
||||
<a-input
|
||||
v-model:value="child.fieldValue"
|
||||
placeholder="请输入"
|
||||
allowClear
|
||||
:disabled="['null', 'notNull'].includes(child.symbol)"
|
||||
v-if="child.fieldValueType === 1" />
|
||||
<yunzhupaas-select v-model:value="child.fieldValue" :options="sysVariableList" allowClear showSearch placeholder="请选择系统参数" v-else />
|
||||
</a-col>
|
||||
</template>
|
||||
<a-col :span="2" class="text-center mt-10px">
|
||||
<i class="icon-ym icon-ym-btn-clearn" @click="delRuleItem(index, childIndex)" />
|
||||
</a-col>
|
||||
</a-row>
|
||||
<span class="link-text inline-block" @click="addRuleItem(index)"><i class="icon-ym icon-ym-btn-add text-14px mr-4px"></i>添加条件</span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="link-text inline-block" @click="addRuleGroup()"><i class="icon-ym icon-ym-btn-add text-14px mr-4px"></i>添加条件组</span>
|
||||
</div>
|
||||
</div>
|
||||
</BasicDrawer>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { reactive, toRefs, computed } from 'vue';
|
||||
import { BasicDrawer, useDrawerInner } from '@/components/Drawer';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import { useMessage } from '@/hooks/web/useMessage';
|
||||
import { isEmpty } from '@/utils/is';
|
||||
|
||||
interface State {
|
||||
currTable: any;
|
||||
parentTable: any;
|
||||
dataForm: any;
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
linkId: { type: String, default: '0' },
|
||||
sysVariableList: { type: Array, default: () => [] },
|
||||
getTableConfigTree: { type: Function },
|
||||
});
|
||||
|
||||
const emit = defineEmits(['register', 'confirm']);
|
||||
const logicOptions = [
|
||||
{ id: 'and', fullName: '且' },
|
||||
{ id: 'or', fullName: '或' },
|
||||
];
|
||||
const dataTypeOptions = [
|
||||
{ id: 'text', fullName: 'text' },
|
||||
{ id: 'double', fullName: 'double' },
|
||||
{ id: 'bigint', fullName: 'bigint' },
|
||||
{ id: 'date', fullName: 'date' },
|
||||
{ id: 'time', fullName: 'time' },
|
||||
];
|
||||
const baseSymbolOptions = [
|
||||
{ id: '==', fullName: '等于' },
|
||||
{ id: '<>', fullName: '不等于' },
|
||||
{ id: 'like', fullName: '包含' },
|
||||
{ id: 'notLike', fullName: '不包含' },
|
||||
{ id: 'null', fullName: '为空' },
|
||||
{ id: 'notNull', fullName: '不为空' },
|
||||
];
|
||||
const rangeSymbolOptions = [
|
||||
{ id: '>=', fullName: '大于等于' },
|
||||
{ id: '>', fullName: '大于' },
|
||||
{ id: '==', fullName: '等于' },
|
||||
{ id: '<=', fullName: '小于等于' },
|
||||
{ id: '<', fullName: '小于' },
|
||||
{ id: '<>', fullName: '不等于' },
|
||||
{ id: 'between', fullName: '介于' },
|
||||
{ id: 'null', fullName: '为空' },
|
||||
{ id: 'notNull', fullName: '不为空' },
|
||||
];
|
||||
const fieldValueTypeOptions = [
|
||||
{ id: 1, fullName: '固定值' },
|
||||
{ id: 2, fullName: '系统参数' },
|
||||
];
|
||||
const typeList = [
|
||||
{ id: 1, fullName: '左连接', icon: 'icon-ym icon-ym-left-join' },
|
||||
{ id: 2, fullName: '右连接', icon: 'icon-ym icon-ym-right-join' },
|
||||
{ id: 3, fullName: '内连接', icon: 'icon-ym icon-ym-inner-join' },
|
||||
{ id: 4, fullName: '全连接', icon: 'icon-ym icon-ym-full-join' },
|
||||
];
|
||||
const emptyChildItem = {
|
||||
field: '',
|
||||
dataType: 'text',
|
||||
symbol: '==',
|
||||
fieldValue: '',
|
||||
fieldValueType: 1,
|
||||
};
|
||||
const emptyItem = { logic: 'and', groups: [emptyChildItem] };
|
||||
const state = reactive<State>({
|
||||
currTable: {},
|
||||
parentTable: {},
|
||||
dataForm: {},
|
||||
});
|
||||
const { dataForm } = toRefs(state);
|
||||
const [registerDrawer, { closeDrawer }] = useDrawerInner(init);
|
||||
const { createMessage } = useMessage();
|
||||
|
||||
const getParentTableFieldList = computed(() => {
|
||||
const item = {
|
||||
id: state.parentTable.table,
|
||||
fullName: state.parentTable.tableName,
|
||||
options: state.parentTable.fieldList.map(c => ({ id: c.field, fullName: c.fieldName, dataType: c.dataType })),
|
||||
};
|
||||
return [item];
|
||||
});
|
||||
const getCurrTableFieldList = computed(() => {
|
||||
const item = {
|
||||
id: state.currTable.table,
|
||||
fullName: state.currTable.tableName,
|
||||
options: state.currTable.fieldList.map(c => ({ id: c.field, fullName: c.fieldName, dataType: c.dataType })),
|
||||
};
|
||||
return [item];
|
||||
});
|
||||
const getFieldList = computed(() => {
|
||||
const parentTableObj = {
|
||||
id: state.parentTable.table,
|
||||
fullName: state.parentTable.tableName,
|
||||
options: state.parentTable.fieldList.map(c => ({ id: state.parentTable.table + '-' + c.field, fullName: c.fieldName, dataType: c.dataType })),
|
||||
};
|
||||
const currTableObj = {
|
||||
id: state.currTable.table,
|
||||
fullName: state.currTable.tableName,
|
||||
options: state.currTable.fieldList.map(c => ({ id: state.currTable.table + '-' + c.field, fullName: c.fieldName, dataType: c.dataType })),
|
||||
};
|
||||
return [parentTableObj, currTableObj];
|
||||
});
|
||||
|
||||
function init(data) {
|
||||
state.currTable = data;
|
||||
state.parentTable = (props.getTableConfigTree as any)().getSelectedNode(data.parentTable);
|
||||
state.dataForm = cloneDeep(data.relationConfig);
|
||||
}
|
||||
function addRuleItem(index) {
|
||||
state.dataForm.ruleList[index].groups.push(cloneDeep(emptyChildItem));
|
||||
}
|
||||
function delRuleItem(index, childIndex) {
|
||||
state.dataForm.ruleList[index].groups.splice(childIndex, 1);
|
||||
if (!state.dataForm.ruleList[index].groups.length) delRuleGroup(index);
|
||||
}
|
||||
function addRuleGroup() {
|
||||
state.dataForm.ruleList.push(cloneDeep(emptyItem));
|
||||
}
|
||||
function delRuleGroup(index) {
|
||||
state.dataForm.ruleList.splice(index, 1);
|
||||
}
|
||||
function onDataTypeChange(item) {
|
||||
item.fieldValueType = 1;
|
||||
if (item.dataType === 'text') {
|
||||
if (!baseSymbolOptions.some(o => o.id === item.symbol)) {
|
||||
item.symbol = '==';
|
||||
}
|
||||
item.fieldValue = '';
|
||||
} else {
|
||||
if (!rangeSymbolOptions.some(o => o.id === item.symbol)) {
|
||||
item.symbol = '==';
|
||||
}
|
||||
item.fieldValue = undefined;
|
||||
}
|
||||
}
|
||||
function onSymbolChange(item) {
|
||||
if (item.dataType === 'text') {
|
||||
if (['null', 'notNull'].includes(item.symbol)) {
|
||||
item.fieldValueType = 1;
|
||||
item.fieldValue = '';
|
||||
}
|
||||
} else {
|
||||
if (['null', 'notNull'].includes(item.symbol)) {
|
||||
item.fieldValue = undefined;
|
||||
} else if (item.symbol === 'between') {
|
||||
!Array.isArray(item.fieldValue) && (item.fieldValue = []);
|
||||
} else {
|
||||
Array.isArray(item.fieldValue) && (item.fieldValue = undefined);
|
||||
}
|
||||
}
|
||||
}
|
||||
function onFieldValueTypeChange(item) {
|
||||
item.fieldValue = '';
|
||||
}
|
||||
function conditionExist() {
|
||||
const list = state.dataForm.ruleList;
|
||||
let isOk = true;
|
||||
outer: for (let i = 0; i < list.length; i++) {
|
||||
const e = list[i];
|
||||
for (let j = 0; j < e.groups.length; j++) {
|
||||
const child = e.groups[j];
|
||||
if (!child.field) {
|
||||
createMessage.warning(`字段不能为空`);
|
||||
isOk = false;
|
||||
break outer;
|
||||
}
|
||||
if (child.fieldValueType === 2 && !child.fieldValue) {
|
||||
createMessage.warning(`系统参数不能为空`);
|
||||
isOk = false;
|
||||
break outer;
|
||||
}
|
||||
if (
|
||||
child.fieldValueType === 1 &&
|
||||
!['null', 'notNull'].includes(child.symbol) &&
|
||||
((!child.fieldValue && child.fieldValue !== 0) || isEmpty(child.fieldValue))
|
||||
) {
|
||||
createMessage.warning('数据值不能为空');
|
||||
isOk = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
return isOk;
|
||||
}
|
||||
function addRelationItem() {
|
||||
state.dataForm.relationList.push({ pField: '', field: '' });
|
||||
}
|
||||
function delRelationItem(index) {
|
||||
state.dataForm.relationList.splice(index, 1);
|
||||
}
|
||||
function relationExist() {
|
||||
const list = state.dataForm.relationList;
|
||||
let isOk = true;
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
if (!list[i].pField) {
|
||||
createMessage.warning(`关联字段中父表字段不能为空`);
|
||||
isOk = false;
|
||||
break;
|
||||
}
|
||||
if (!list[i].field) {
|
||||
createMessage.warning(`关联字段中当前表字段不能为空`);
|
||||
isOk = false;
|
||||
}
|
||||
}
|
||||
return isOk;
|
||||
}
|
||||
function handleSubmit() {
|
||||
if (!state.dataForm.relationList.length) return createMessage.warning('请至少配置一组字段关联');
|
||||
if (!relationExist()) return;
|
||||
if (!conditionExist()) return;
|
||||
emit('confirm', state.dataForm);
|
||||
closeDrawer();
|
||||
}
|
||||
</script>
|
||||
<style lang="less">
|
||||
html[data-theme='dark'] {
|
||||
.dataSet-table-config-drawer {
|
||||
.symbol-text {
|
||||
color: #fff !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,420 @@
|
||||
<template>
|
||||
<BasicDrawer
|
||||
v-bind="$attrs"
|
||||
width="500px"
|
||||
title="数据库表设置"
|
||||
@register="registerDrawer"
|
||||
class="dataSet-table-config-drawer"
|
||||
showFooter
|
||||
:maskClosable="false"
|
||||
@ok="handleSubmit"
|
||||
:closeFunc="handleClose"
|
||||
destroyOnClose>
|
||||
<div class="p-20px">
|
||||
<div class="common-cap !mt-0">
|
||||
<span class="title w-80px flex-shrink-0">数据库表</span>
|
||||
<div class="flex-1">
|
||||
<TableSelect :value="dataForm.table" :title="dataForm.tableName" :linkId="linkId" @change="onTableChange" />
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="dataForm.table">
|
||||
<div class="common-cap">
|
||||
<span class="title">选择 {{ dataForm?.fieldList?.length || 0 }}/{{ allFieldList.length }}</span>
|
||||
</div>
|
||||
<a-table
|
||||
:data-source="allFieldList"
|
||||
:columns="fieldColumns"
|
||||
size="small"
|
||||
:pagination="false"
|
||||
:scroll="{ y: '300px' }"
|
||||
rowKey="field"
|
||||
:row-selection="{ columnWidth: 50, selectedRowKeys: selectedRowKeys, onChange: onSelectedChange }" />
|
||||
<div class="common-cap">
|
||||
<span class="title">条件筛选</span>
|
||||
</div>
|
||||
<div class="condition-main overflow-auto">
|
||||
<div class="mb-10px" v-if="dataForm.ruleList?.length">
|
||||
<yunzhupaas-radio v-model:value="dataForm.matchLogic" :options="logicOptions" optionType="button" button-style="solid" />
|
||||
</div>
|
||||
<div class="condition-item" v-for="(item, index) in dataForm.ruleList" :key="index">
|
||||
<div class="condition-item-title">
|
||||
<div>条件组</div>
|
||||
<i class="icon-ym icon-ym-nav-close" @click="delRuleGroup(index)"></i>
|
||||
</div>
|
||||
<div class="condition-item-content">
|
||||
<div class="condition-item-cap">
|
||||
以下条件全部执行:
|
||||
<yunzhupaas-radio v-model:value="item.logic" :options="logicOptions" optionType="button" button-style="solid" size="small" />
|
||||
</div>
|
||||
<a-row :gutter="8" v-for="(child, childIndex) in item.groups" :key="index + childIndex" wrap class="mb-10px">
|
||||
<a-col :span="18" class="!flex items-center">
|
||||
<yunzhupaas-select
|
||||
v-model:value="child.field"
|
||||
:options="dataForm.fieldList"
|
||||
placeholder="请选择字段"
|
||||
allowClear
|
||||
showSearch
|
||||
:fieldNames="{ label: 'fieldName', value: 'field' }" />
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<yunzhupaas-select class="w-full" v-model:value="child.dataType" :options="dataTypeOptions" @change="onDataTypeChange(child)" />
|
||||
</a-col>
|
||||
<a-col :span="6" class="mt-10px">
|
||||
<yunzhupaas-select
|
||||
class="w-full"
|
||||
v-model:value="child.symbol"
|
||||
:options="child.dataType === 'text' ? baseSymbolOptions : rangeSymbolOptions"
|
||||
@change="onSymbolChange(child)" />
|
||||
</a-col>
|
||||
<a-col :span="16" class="mt-10px" v-if="['double', 'bigint', 'date', 'time'].includes(child.dataType)">
|
||||
<template v-if="['double', 'bigint'].includes(child.dataType)">
|
||||
<yunzhupaas-number-range v-model:value="child.fieldValue" v-if="child.symbol == 'between'" />
|
||||
<yunzhupaas-input-number v-model:value="child.fieldValue" placeholder="请输入" :disabled="['null', 'notNull'].includes(child.symbol)" v-else />
|
||||
</template>
|
||||
<template v-else-if="['date', 'time'].includes(child.dataType)">
|
||||
<yunzhupaas-date-range
|
||||
v-model:value="child.fieldValue"
|
||||
:format="child.dataType === 'time' ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD'"
|
||||
allowClear
|
||||
v-if="child.symbol == 'between'" />
|
||||
<yunzhupaas-date-picker
|
||||
v-model:value="child.fieldValue"
|
||||
:format="child.dataType === 'time' ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD'"
|
||||
allowClear
|
||||
:disabled="['null', 'notNull'].includes(child.symbol)"
|
||||
v-else />
|
||||
</template>
|
||||
</a-col>
|
||||
<template v-else>
|
||||
<a-col :span="6" class="mt-10px">
|
||||
<yunzhupaas-select
|
||||
v-model:value="child.fieldValueType"
|
||||
:options="fieldValueTypeOptions"
|
||||
@change="onFieldValueTypeChange(child)"
|
||||
:disabled="['null', 'notNull'].includes(child.symbol)" />
|
||||
</a-col>
|
||||
<a-col :span="10" class="mt-10px">
|
||||
<a-input
|
||||
v-model:value="child.fieldValue"
|
||||
placeholder="请输入"
|
||||
allowClear
|
||||
:disabled="['null', 'notNull'].includes(child.symbol)"
|
||||
v-if="child.fieldValueType === 1" />
|
||||
<yunzhupaas-select v-model:value="child.fieldValue" :options="sysVariableList" allowClear showSearch placeholder="请选择系统参数" v-else />
|
||||
</a-col>
|
||||
</template>
|
||||
<a-col :span="2" class="text-center mt-10px">
|
||||
<i class="icon-ym icon-ym-btn-clearn" @click="delRuleItem(index, childIndex)" />
|
||||
</a-col>
|
||||
</a-row>
|
||||
<span class="link-text inline-block" @click="addRuleItem(index)"><i class="icon-ym icon-ym-btn-add text-14px mr-4px"></i>添加条件</span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="link-text inline-block" @click="addRuleGroup()"><i class="icon-ym icon-ym-btn-add text-14px mr-4px"></i>添加条件组</span>
|
||||
</div>
|
||||
<template v-if="!!dataForm.parentTable">
|
||||
<div class="common-cap">
|
||||
<span class="title">数据连接</span>
|
||||
</div>
|
||||
<a-button block @click="openRelationConfig()">配置数据连接</a-button>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
<RelationConfigDrawer v-bind="getConfigBind" @register="registerRelationConfigDrawer" @confirm="onRelationConfigConfirm" />
|
||||
</BasicDrawer>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { reactive, toRefs, computed } from 'vue';
|
||||
import { getDataModelFieldList } from '@/api/systemData/dataModel';
|
||||
import { BasicDrawer, useDrawer, useDrawerInner } from '@/components/Drawer';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import TableSelect from './TableSelect.vue';
|
||||
import { useMessage } from '@/hooks/web/useMessage';
|
||||
import { useI18n } from '@/hooks/web/useI18n';
|
||||
import { isEmpty } from '@/utils/is';
|
||||
import RelationConfigDrawer from './RelationConfigDrawer.vue';
|
||||
|
||||
interface State {
|
||||
dataForm: any;
|
||||
isEdit: boolean;
|
||||
allFieldList: any[];
|
||||
selectedRowKeys: string[];
|
||||
oldTable: string;
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
linkId: { type: String, default: '0' },
|
||||
sysVariableList: { type: Array, default: () => [] },
|
||||
getTableConfigTree: { type: Function },
|
||||
});
|
||||
|
||||
const emit = defineEmits(['register', 'confirm', 'close']);
|
||||
const fieldColumns = [
|
||||
{ title: '字段名', dataIndex: 'field', key: 'field', width: 200 },
|
||||
{ title: '字段描述', dataIndex: 'fieldName', key: 'fieldName' },
|
||||
];
|
||||
const logicOptions = [
|
||||
{ id: 'and', fullName: '且' },
|
||||
{ id: 'or', fullName: '或' },
|
||||
];
|
||||
const dataTypeOptions = [
|
||||
{ id: 'text', fullName: 'text' },
|
||||
{ id: 'double', fullName: 'double' },
|
||||
{ id: 'bigint', fullName: 'bigint' },
|
||||
{ id: 'date', fullName: 'date' },
|
||||
{ id: 'time', fullName: 'time' },
|
||||
];
|
||||
const baseSymbolOptions = [
|
||||
{ id: '==', fullName: '等于' },
|
||||
{ id: '<>', fullName: '不等于' },
|
||||
{ id: 'like', fullName: '包含' },
|
||||
{ id: 'notLike', fullName: '不包含' },
|
||||
{ id: 'null', fullName: '为空' },
|
||||
{ id: 'notNull', fullName: '不为空' },
|
||||
];
|
||||
const rangeSymbolOptions = [
|
||||
{ id: '>=', fullName: '大于等于' },
|
||||
{ id: '>', fullName: '大于' },
|
||||
{ id: '==', fullName: '等于' },
|
||||
{ id: '<=', fullName: '小于等于' },
|
||||
{ id: '<', fullName: '小于' },
|
||||
{ id: '<>', fullName: '不等于' },
|
||||
{ id: 'between', fullName: '介于' },
|
||||
{ id: 'null', fullName: '为空' },
|
||||
{ id: 'notNull', fullName: '不为空' },
|
||||
];
|
||||
const fieldValueTypeOptions = [
|
||||
{ id: 1, fullName: '固定值' },
|
||||
{ id: 2, fullName: '系统参数' },
|
||||
];
|
||||
const emptyChildItem = {
|
||||
field: '',
|
||||
dataType: 'text',
|
||||
symbol: '==',
|
||||
fieldValue: '',
|
||||
fieldValueType: 1,
|
||||
};
|
||||
const emptyItem = { logic: 'and', groups: [emptyChildItem] };
|
||||
const defaultRelationConfig = {
|
||||
ruleList: [],
|
||||
matchLogic: 'and',
|
||||
type: 1,
|
||||
relationList: [],
|
||||
};
|
||||
const defaultDataForm = {
|
||||
parentTable: '',
|
||||
table: '',
|
||||
tableName: '',
|
||||
fieldList: [],
|
||||
ruleList: [],
|
||||
matchLogic: 'and',
|
||||
relationConfig: defaultRelationConfig,
|
||||
children: [],
|
||||
};
|
||||
const state = reactive<State>({
|
||||
dataForm: {},
|
||||
isEdit: false,
|
||||
allFieldList: [],
|
||||
selectedRowKeys: [],
|
||||
oldTable: '',
|
||||
});
|
||||
const { dataForm, allFieldList, selectedRowKeys } = toRefs(state);
|
||||
const [registerDrawer, { closeDrawer }] = useDrawerInner(init);
|
||||
const [registerRelationConfigDrawer, { openDrawer: openConfigDrawer }] = useDrawer();
|
||||
const { t } = useI18n();
|
||||
const { createMessage, createConfirm } = useMessage();
|
||||
|
||||
const getConfigBind = computed(() => ({ ...props }));
|
||||
|
||||
function init(data) {
|
||||
state.selectedRowKeys = [];
|
||||
state.allFieldList = [];
|
||||
state.isEdit = !!data.data;
|
||||
const dataForm = data.data ? cloneDeep(data.data) : cloneDeep(defaultDataForm);
|
||||
if (!state.isEdit && data.parentTable) dataForm.parentTable = data.parentTable;
|
||||
if (state.isEdit) getTableFieldList(dataForm.table, true);
|
||||
state.oldTable = dataForm.table;
|
||||
state.dataForm = dataForm;
|
||||
}
|
||||
function onTableChange(table, item) {
|
||||
if (!table) return handleNull();
|
||||
if (state.dataForm.table !== table) handleNull();
|
||||
getTableFieldList(table, state.dataForm.table === table);
|
||||
state.dataForm.table = table;
|
||||
state.dataForm.tableName = table + (item.tableName ? `(${item.tableName})` : '');
|
||||
}
|
||||
function handleNull() {
|
||||
state.dataForm.table = '';
|
||||
state.dataForm.tableName = '';
|
||||
state.dataForm.fieldList = [];
|
||||
state.dataForm.ruleList = [];
|
||||
}
|
||||
function getTableFieldList(table, isInit = false) {
|
||||
getDataModelFieldList(props.linkId, table).then(res => {
|
||||
state.allFieldList = res.data.list;
|
||||
if (!isInit) {
|
||||
state.selectedRowKeys = state.allFieldList.map(o => o.field);
|
||||
const fieldList = state.allFieldList.map(o => ({
|
||||
field: o.field,
|
||||
fieldName: o.field + (o.fieldName ? `(${o.fieldName})` : ''),
|
||||
dataType: o.dataType,
|
||||
}));
|
||||
state.dataForm.fieldList = fieldList;
|
||||
} else {
|
||||
const fieldList = state.dataForm.fieldList.filter(o => state.allFieldList.some(e => o.field === e.field));
|
||||
state.dataForm.fieldList = fieldList;
|
||||
state.selectedRowKeys = state.dataForm.fieldList.map(o => o.field);
|
||||
}
|
||||
});
|
||||
}
|
||||
function onSelectedChange(selectedRowKeys, selectedRows) {
|
||||
state.selectedRowKeys = selectedRowKeys;
|
||||
state.dataForm.fieldList = selectedRows.map(o => ({
|
||||
field: o.field,
|
||||
fieldName: o.field + (o.fieldName ? `(${o.fieldName})` : ''),
|
||||
dataType: o.dataType,
|
||||
}));
|
||||
}
|
||||
function addRuleItem(index) {
|
||||
state.dataForm.ruleList[index].groups.push(cloneDeep(emptyChildItem));
|
||||
}
|
||||
function delRuleItem(index, childIndex) {
|
||||
state.dataForm.ruleList[index].groups.splice(childIndex, 1);
|
||||
if (!state.dataForm.ruleList[index].groups.length) delRuleGroup(index);
|
||||
}
|
||||
function addRuleGroup() {
|
||||
state.dataForm.ruleList.push(cloneDeep(emptyItem));
|
||||
}
|
||||
function delRuleGroup(index) {
|
||||
state.dataForm.ruleList.splice(index, 1);
|
||||
}
|
||||
function onDataTypeChange(item) {
|
||||
item.fieldValueType = 1;
|
||||
if (item.dataType === 'text') {
|
||||
if (!baseSymbolOptions.some(o => o.id === item.symbol)) {
|
||||
item.symbol = '==';
|
||||
}
|
||||
item.fieldValue = '';
|
||||
} else {
|
||||
if (!rangeSymbolOptions.some(o => o.id === item.symbol)) {
|
||||
item.symbol = '==';
|
||||
}
|
||||
item.fieldValue = undefined;
|
||||
}
|
||||
}
|
||||
function onSymbolChange(item) {
|
||||
if (item.dataType === 'text') {
|
||||
if (['null', 'notNull'].includes(item.symbol)) {
|
||||
item.fieldValueType = 1;
|
||||
item.fieldValue = '';
|
||||
}
|
||||
} else {
|
||||
if (['null', 'notNull'].includes(item.symbol)) {
|
||||
item.fieldValue = undefined;
|
||||
} else if (item.symbol === 'between') {
|
||||
!Array.isArray(item.fieldValue) && (item.fieldValue = []);
|
||||
} else {
|
||||
Array.isArray(item.fieldValue) && (item.fieldValue = undefined);
|
||||
}
|
||||
}
|
||||
}
|
||||
function onFieldValueTypeChange(item) {
|
||||
item.fieldValue = '';
|
||||
}
|
||||
function openRelationConfig() {
|
||||
if (!state.dataForm.fieldList.length) return createMessage.warning('请至少选择一个字段');
|
||||
openConfigDrawer(true, { ...state.dataForm });
|
||||
}
|
||||
function onRelationConfigConfirm(data) {
|
||||
state.dataForm.relationConfig = data;
|
||||
}
|
||||
function conditionExist() {
|
||||
const list = state.dataForm.ruleList;
|
||||
let isOk = true;
|
||||
outer: for (let i = 0; i < list.length; i++) {
|
||||
const e = list[i];
|
||||
for (let j = 0; j < e.groups.length; j++) {
|
||||
const child = e.groups[j];
|
||||
if (!child.field) {
|
||||
createMessage.warning(`字段不能为空`);
|
||||
isOk = false;
|
||||
break outer;
|
||||
}
|
||||
if (child.fieldValueType === 2 && !child.fieldValue) {
|
||||
createMessage.warning(`系统参数不能为空`);
|
||||
isOk = false;
|
||||
break outer;
|
||||
}
|
||||
if (
|
||||
child.fieldValueType === 1 &&
|
||||
!['null', 'notNull'].includes(child.symbol) &&
|
||||
((!child.fieldValue && child.fieldValue !== 0) || isEmpty(child.fieldValue))
|
||||
) {
|
||||
createMessage.warning('数据值不能为空');
|
||||
isOk = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
return isOk;
|
||||
}
|
||||
function handleSubmit() {
|
||||
if (!state.dataForm.table) return createMessage.warning('请选择数据库表');
|
||||
if (state.dataForm.parentTable) {
|
||||
let allKeys = (props.getTableConfigTree as any)().getAllKeys();
|
||||
if (state.isEdit) allKeys = allKeys.filter(o => o != state.oldTable);
|
||||
if (allKeys.includes(state.dataForm.table)) return createMessage.warning('数据库表不能重复,请重新选择');
|
||||
}
|
||||
if (!state.dataForm.fieldList.length) return createMessage.warning('请至少选择一个字段');
|
||||
if (!conditionExist()) return;
|
||||
if (state.dataForm.parentTable && !state.dataForm?.relationConfig?.relationList?.length) return createMessage.warning('请至少配置一组字段关联');
|
||||
if (state.oldTable && state.dataForm.table != state.oldTable) {
|
||||
createConfirm({
|
||||
iconType: 'warning',
|
||||
title: t('common.tipTitle'),
|
||||
content: `切换数据库表将清空关联子表,确定要继续?`,
|
||||
onOk: () => {
|
||||
emit('confirm', state.dataForm, state.isEdit, true);
|
||||
closeDrawer();
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
emit('confirm', state.dataForm, state.isEdit);
|
||||
closeDrawer();
|
||||
}
|
||||
function handleClose() {
|
||||
emit('close');
|
||||
return true;
|
||||
}
|
||||
</script>
|
||||
<style lang="less">
|
||||
.dataSet-table-config-drawer {
|
||||
.common-cap {
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin: 15px 0;
|
||||
.title {
|
||||
font-size: 16px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
html[data-theme='dark'] {
|
||||
.dataSet-table-config-drawer {
|
||||
.common-cap {
|
||||
.title {
|
||||
color: #fff !important;
|
||||
}
|
||||
}
|
||||
.condition-item-title,
|
||||
.condition-item-cap {
|
||||
color: #fff !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
141
src/components/CommonModal/src/DataSetModal/TableSelect.vue
Normal file
141
src/components/CommonModal/src/DataSetModal/TableSelect.vue
Normal file
@@ -0,0 +1,141 @@
|
||||
<template>
|
||||
<div class="common-container">
|
||||
<a-select v-model:value="innerValue" v-bind="getSelectBindValue" :options="options" @change="onChange" @click="openSelectModal" />
|
||||
<a-modal
|
||||
v-model:open="visible"
|
||||
title="数据选择"
|
||||
:width="800"
|
||||
class="common-container-modal"
|
||||
@ok="handleSubmit"
|
||||
@cancel="handleCancel"
|
||||
:maskClosable="false">
|
||||
<template #closeIcon>
|
||||
<ModalClose :canFullscreen="false" @cancel="handleCancel" />
|
||||
</template>
|
||||
<div class="yunzhupaas-content-wrapper">
|
||||
<div class="yunzhupaas-content-wrapper-center">
|
||||
<div class="yunzhupaas-content-wrapper-content">
|
||||
<BasicTable @register="registerTable" :searchInfo="getSearchInfo" class="yunzhupaas-sub-table" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { getDataModelList } from '@/api/systemData/dataModel';
|
||||
import { Form, Modal as AModal } from 'ant-design-vue';
|
||||
import { ref, watch, computed, nextTick, unref } from 'vue';
|
||||
import ModalClose from '@/components/Modal/src/components/ModalClose.vue';
|
||||
import { BasicTable, useTable } from '@/components/Table';
|
||||
import { useI18n } from '@/hooks/web/useI18n';
|
||||
import { useAttrs } from '@/hooks/core/useAttrs';
|
||||
import { pick } from 'lodash-es';
|
||||
|
||||
defineOptions({ inheritAttrs: false });
|
||||
const props = defineProps({
|
||||
value: { default: '' },
|
||||
title: { type: String, default: '' },
|
||||
placeholder: { type: String, default: '请选择' },
|
||||
disabled: { type: Boolean, default: false },
|
||||
allowClear: { type: Boolean, default: true },
|
||||
size: { type: String, default: 'default' },
|
||||
linkId: { type: String, default: '0' },
|
||||
});
|
||||
const emit = defineEmits(['update:value', 'change']);
|
||||
const attrs: any = useAttrs({ excludeDefaultKeys: false });
|
||||
const formItemContext = Form.useInjectFormItemContext();
|
||||
const { t } = useI18n();
|
||||
const innerValue = ref(undefined);
|
||||
const visible = ref(false);
|
||||
const options = ref<any[]>([]);
|
||||
const columns = [
|
||||
{ title: '表名', dataIndex: 'table' },
|
||||
{ title: '说明', dataIndex: 'tableName' },
|
||||
];
|
||||
const [registerTable, { getForm, getSelectRows, setSelectedRowKeys, getSelectRowKeys }] = useTable({
|
||||
api: getDataModelList,
|
||||
columns,
|
||||
rowKey: 'table',
|
||||
immediate: false,
|
||||
useSearchForm: true,
|
||||
formConfig: {
|
||||
baseColProps: { span: 8 },
|
||||
schemas: [
|
||||
{
|
||||
field: 'keyword',
|
||||
label: t('common.keyword'),
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: t('common.enterKeyword'),
|
||||
submitOnPressEnter: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
tableSetting: { size: false, setting: false },
|
||||
isCanResizeParent: true,
|
||||
resizeHeightOffset: -73,
|
||||
rowSelection: { type: 'radio', columnWidth: 50 },
|
||||
});
|
||||
|
||||
const getSearchInfo = computed(() => ({
|
||||
linkId: props.linkId,
|
||||
}));
|
||||
const getSelectBindValue = computed(() => {
|
||||
return {
|
||||
...pick(props, ['disabled', 'size', 'allowClear', 'placeholder']),
|
||||
fieldNames: { label: 'tableName', value: 'table' },
|
||||
open: false,
|
||||
showSearch: false,
|
||||
showArrow: true,
|
||||
class: unref(attrs).class ? 'w-full ' + unref(attrs).class : 'w-full',
|
||||
};
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.value,
|
||||
val => {
|
||||
setValue(val);
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
function setValue(value) {
|
||||
innerValue.value = value || undefined;
|
||||
options.value = [{ table: innerValue.value, tableName: props.title }];
|
||||
}
|
||||
function onChange() {
|
||||
options.value = [];
|
||||
emit('change', '', {});
|
||||
}
|
||||
async function openSelectModal() {
|
||||
if (props.disabled) return;
|
||||
visible.value = true;
|
||||
nextTick(() => {
|
||||
getForm().resetFields();
|
||||
setSelectedRowKeys(innerValue.value ? [innerValue.value] : []);
|
||||
});
|
||||
}
|
||||
function handleCancel() {
|
||||
visible.value = false;
|
||||
}
|
||||
function handleSubmit() {
|
||||
if (!getSelectRowKeys().length && !getSelectRows().length) return;
|
||||
if (!getSelectRows().length) {
|
||||
emit('update:value', innerValue.value);
|
||||
emit('change', innerValue.value, options.value[0]);
|
||||
formItemContext.onFieldChange();
|
||||
handleCancel();
|
||||
return;
|
||||
}
|
||||
const selectRow = getSelectRows()[0];
|
||||
options.value = getSelectRows();
|
||||
innerValue.value = selectRow.table;
|
||||
emit('update:value', selectRow.table);
|
||||
emit('change', selectRow.table, selectRow);
|
||||
formItemContext.onFieldChange();
|
||||
handleCancel();
|
||||
}
|
||||
</script>
|
||||
870
src/components/CommonModal/src/DataSetModal/index.vue
Normal file
870
src/components/CommonModal/src/DataSetModal/index.vue
Normal file
@@ -0,0 +1,870 @@
|
||||
<template>
|
||||
<BasicModal
|
||||
v-bind="$attrs"
|
||||
@register="registerModal"
|
||||
defaultFullscreen
|
||||
:footer="null"
|
||||
:closable="false"
|
||||
:keyboard="false"
|
||||
destroyOnClose
|
||||
class="yunzhupaas-full-modal full-modal designer-modal dataSet-modal">
|
||||
<template #title>
|
||||
<div class="yunzhupaas-full-modal-header">
|
||||
<div class="header-title">数据集名称:<a-input v-model:value="dataForm.fullName" placeholder="请输入" class="!w-200px"></a-input></div>
|
||||
<a-steps v-model:current="dataForm.type" :initial="1" type="navigation" size="small" @change="handleStep" class="header-steps tab-steps">
|
||||
<a-step title="SQL语句" />
|
||||
<a-step title="配置式" />
|
||||
<a-step title="数据接口" />
|
||||
</a-steps>
|
||||
<a-space class="options" :size="10">
|
||||
<a-button type="primary" @click="handleSubmit()" :disabled="btnLoading">{{ t('common.okText') }}</a-button>
|
||||
<a-button @click="closeModal" :disabled="btnLoading">{{ t('common.cancelText') }}</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
</template>
|
||||
<div class="dataSet-modal-container" v-if="dataForm.type == 2">
|
||||
<span class="label">数据连接</span>
|
||||
<yunzhupaas-select
|
||||
class="dataSet-modal-select"
|
||||
v-model:value="dataForm.dbLinkId"
|
||||
showSearch
|
||||
:options="dbOptions"
|
||||
placeholder="选择数据库"
|
||||
@change="onDbLinkChange"
|
||||
:fieldNames="{ options: 'children' }" />
|
||||
</div>
|
||||
<div class="dataSet-modal-main">
|
||||
<div class="left-pane" v-if="dataForm.type == 1">
|
||||
<yunzhupaas-select
|
||||
class="!w-full"
|
||||
v-model:value="dataForm.dbLinkId"
|
||||
showSearch
|
||||
:options="dbOptions"
|
||||
placeholder="选择数据库"
|
||||
@change="onDbLinkChange"
|
||||
:fieldNames="{ options: 'children' }" />
|
||||
<div class="left-pane-box">
|
||||
<InputSearch class="search-box" :placeholder="t('common.enterKeyword')" allowClear v-model:value="keyword" @search="handleSearchTable" />
|
||||
<ScrollContainer ref="infiniteBody">
|
||||
<BasicTree class="tree-box remove-active-tree" ref="leftTreeRef" v-bind="getTreeBindValue" />
|
||||
</ScrollContainer>
|
||||
</div>
|
||||
</div>
|
||||
<div class="middle-pane">
|
||||
<a-tabs class="yunzhupaas-content-wrapper-tabs">
|
||||
<template #rightExtra v-if="dataForm.type == 1">
|
||||
<a-dropdown>
|
||||
<a-button type="link">系统变量<DownOutlined /></a-button>
|
||||
<template #overlay>
|
||||
<a-menu>
|
||||
<a-menu-item disabled>当前系统变量仅支持内部接口引用</a-menu-item>
|
||||
<a-menu-divider />
|
||||
<a-menu-item v-for="(item, index) in getSysVariableList" :key="index" @click="handleSysNodeClick(item.id)">
|
||||
<span>{{ item.id }}</span>
|
||||
<span style="float: right; color: #8492a6; padding-left: 10px">{{ item.fullName }}</span>
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</template>
|
||||
</a-tabs>
|
||||
<template v-if="dataForm.type == 1">
|
||||
<MonacoEditor class="h-full" ref="sqlEditorRef" language="sql" v-model="dataForm.dataConfigJson" />
|
||||
</template>
|
||||
<template v-else-if="dataForm.type == 2">
|
||||
<div class="config-tree-box">
|
||||
<div class="add-btn-box" v-if="!visualConfigJson.length">
|
||||
<a-button size="large" preIcon="icon-ym icon-ym-btn-add" @click="handleAdd()">新增数据库表</a-button>
|
||||
</div>
|
||||
<BasicTree
|
||||
class="tree-box remove-active-tree"
|
||||
ref="tableConfigTreeRef"
|
||||
:treeData="visualConfigJson"
|
||||
:fieldNames="fieldNames"
|
||||
v-model:selectedKeys="selectedKeys"
|
||||
defaultExpandAll
|
||||
v-else>
|
||||
<template #title="item">
|
||||
<div class="yunzhupaas-tree__title" @click="handleEdit(item.table)">
|
||||
<i class="mr-6px" :class="getIconClassName(item)" :title="getIconTitle(item)"></i>
|
||||
{{ item.tableName }}
|
||||
<span class="yunzhupaas-tree__action">
|
||||
<PlusOutlined class="ml-10px" title="添加" @click.stop="handleAdd(item.table)" v-if="getAddBtnShow(item.parentTable)" />
|
||||
<DeleteOutlined class="ml-6px" title="删除" @click.stop="handleDelete(item.table)" />
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</BasicTree>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="dataForm.type == 3">
|
||||
<div class="interface-content">
|
||||
<a-form-item label="数据接口" :colon="false">
|
||||
<interface-modal
|
||||
class="interface-modal"
|
||||
:value="dataForm.interfaceId"
|
||||
:title="dataForm.treePropsName"
|
||||
popupTitle="数据接口"
|
||||
:sourceType="1"
|
||||
@change="onPropsUrlChange" />
|
||||
</a-form-item>
|
||||
<div class="interface-box">
|
||||
<a-table
|
||||
:data-source="dataForm.parameterJson"
|
||||
:columns="templateJsonColumns"
|
||||
size="small"
|
||||
:pagination="false"
|
||||
v-if="dataForm.parameterJson?.length">
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'field'">
|
||||
<span class="required-sign">{{ record.required ? '*' : '' }}</span>
|
||||
{{ record.field }}{{ record.fieldName ? '(' + record.fieldName + ')' : '' }}
|
||||
</template>
|
||||
<template v-if="column.key === 'sourceType'">
|
||||
<yunzhupaas-select
|
||||
class="!w-100%"
|
||||
v-model:value="record.sourceType"
|
||||
:options="getSourceTypeOptions(record.required)"
|
||||
@change="onSourceTypeChange(record)" />
|
||||
</template>
|
||||
<template v-if="column.key === 'relationField'">
|
||||
<a-input v-if="record.sourceType == 2" v-model:value="record.relationField" placeholder="请输入" class="!w-100%" />
|
||||
<yunzhupaas-select v-else-if="record.sourceType == 4" class="!w-100%" v-model:value="record.relationField" :options="getSysVariableList" />
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="preview-box" v-if="dataForm.type != 1">
|
||||
<a-tabs v-model:activeKey="previewActiveKey" class="yunzhupaas-content-wrapper-tabs">
|
||||
<a-tab-pane :key="1" tab="数据预览"></a-tab-pane>
|
||||
<a-tab-pane :key="2" tab="SQL预览" v-if="dataForm.type == 2"></a-tab-pane>
|
||||
<template #rightExtra>
|
||||
<a-button v-if="dataForm.type == 2" type="link" preIcon="icon-ym icon-ym-filter" @click="openFilterConfig">筛选设置</a-button>
|
||||
<a-button type="link" preIcon="icon-ym icon-ym-Refresh" @click="handleGetPreviewData">刷新数据</a-button>
|
||||
</template>
|
||||
</a-tabs>
|
||||
<div class="preview-content" v-loading="previewLoading" v-show="previewActiveKey === 1">
|
||||
<div class="preview-tip">预览数据仅显示20条数据</div>
|
||||
<BasicTable
|
||||
:data-source="state.previewData"
|
||||
:columns="state.previewColumns"
|
||||
size="small"
|
||||
:pagination="false"
|
||||
:showTableSetting="false"
|
||||
v-if="!!state.previewColumns.length" />
|
||||
</div>
|
||||
<div class="preview-content" v-loading="previewLoading" v-show="previewActiveKey === 2">
|
||||
<a-textarea v-model:value="state.previewSqlText" readonly class="preview-content-sql" />
|
||||
<a-tooltip placement="bottom" :title="t('common.copyText')">
|
||||
<CopyOutlined class="copy-btn" @click="handleCopySql" v-if="state.previewSqlText" />
|
||||
</a-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<TableConfigDrawer v-bind="getTableConfigBind" @register="registerTableConfigDrawer" @confirm="onTreeNodeConfirm" @close="onTreeNodeClose" />
|
||||
<FilterConfigDrawer :sysVariableList="getSysVariableList" @register="registerFilterConfigDrawer" @confirm="onFilterConfigConfirm" />
|
||||
</BasicModal>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { getDataSourceSelector } from '@/api/systemData/dataSource';
|
||||
import { getDataModelList, getDataModelFieldList } from '@/api/systemData/dataModel';
|
||||
import { getFields, getPreviewData } from '@/api/system/dataSet';
|
||||
import { reactive, toRefs, ref, unref, computed, nextTick } from 'vue';
|
||||
import { PlusOutlined, DeleteOutlined, DownOutlined, CopyOutlined } from '@ant-design/icons-vue';
|
||||
import { BasicModal, useModalInner } from '@/components/Modal';
|
||||
import { useDrawer } from '@/components/Drawer';
|
||||
import { useMessage } from '@/hooks/web/useMessage';
|
||||
import { useI18n } from '@/hooks/web/useI18n';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import { BasicTree, TreeActionType } from '@/components/Tree';
|
||||
import { MonacoEditor } from '@/components/CodeEditor';
|
||||
import { InputSearch } from 'ant-design-vue';
|
||||
import { buildBitUUID } from '@/utils/uuid';
|
||||
import TableConfigDrawer from './TableConfigDrawer.vue';
|
||||
import FilterConfigDrawer from './FilterConfigDrawer.vue';
|
||||
import { useCopyToClipboard } from '@/hooks/web/useCopyToClipboard';
|
||||
import { BasicTable } from '@/components/Table';
|
||||
import { InterfaceModal } from '@/components/CommonModal';
|
||||
import { ScrollContainer, ScrollActionType } from '@/components/Container';
|
||||
|
||||
interface State {
|
||||
dataForm: any;
|
||||
dataSetList: any[];
|
||||
parameterJson: any[];
|
||||
fieldJson: any[];
|
||||
dbOptions: any[];
|
||||
treeLoading: boolean;
|
||||
btnLoading: boolean;
|
||||
keyword: string;
|
||||
treeData: any[];
|
||||
isEdit: boolean;
|
||||
visualConfigJson: any[];
|
||||
filterConfigJson: any;
|
||||
previewActiveKey: number;
|
||||
currentNode: string;
|
||||
previewSqlText: string;
|
||||
previewData: any[];
|
||||
previewColumns: any[];
|
||||
previewLoading: boolean;
|
||||
allOptions: any[];
|
||||
selectedKeys: any[];
|
||||
pagination: any;
|
||||
finish: boolean;
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
type: { type: String, default: '' },
|
||||
});
|
||||
|
||||
const relationFieldOptions = [
|
||||
{ fullName: '自定义', id: 2 },
|
||||
{ fullName: '系统参数', id: 4 },
|
||||
{ fullName: '为空', id: 3 },
|
||||
];
|
||||
|
||||
const templateJsonColumns = [
|
||||
{ width: 50, title: '序号', align: 'center', customRender: ({ index }) => index + 1 },
|
||||
{ title: '参数名称', dataIndex: 'field', key: 'field', width: 135 },
|
||||
{ title: '参数来源', dataIndex: 'sourceType', key: 'sourceType', width: 220 },
|
||||
{ title: '参数值', dataIndex: 'relationField', key: 'relationField', width: 220 },
|
||||
];
|
||||
const fieldNames = { key: 'table' };
|
||||
const defaultDataForm = {
|
||||
yunzhupaasId: '',
|
||||
id: '',
|
||||
fullName: '',
|
||||
dbLinkId: '0',
|
||||
dataConfigJson: '',
|
||||
visualConfigJson: '',
|
||||
filterConfigJson: '',
|
||||
parameterJson: '',
|
||||
fieldJson: '',
|
||||
objectType: props.type === 'print' ? 'printVersion' : '',
|
||||
type: 1,
|
||||
};
|
||||
const defaultFilterConfigJson = {
|
||||
ruleList: [],
|
||||
matchLogic: 'and',
|
||||
};
|
||||
const state = reactive<State>({
|
||||
dataForm: {
|
||||
yunzhupaasId: '',
|
||||
id: '',
|
||||
fullName: '',
|
||||
dbLinkId: '0',
|
||||
dataConfigJson: '',
|
||||
visualConfigJson: '',
|
||||
filterConfigJson: '',
|
||||
parameterJson: '',
|
||||
fieldJson: '',
|
||||
objectType: '',
|
||||
type: 1,
|
||||
interfaceId: '',
|
||||
treePropsName: '',
|
||||
templateJson: [],
|
||||
},
|
||||
dataSetList: [],
|
||||
parameterJson: [],
|
||||
fieldJson: [],
|
||||
dbOptions: [],
|
||||
treeLoading: true,
|
||||
btnLoading: false,
|
||||
keyword: '',
|
||||
treeData: [],
|
||||
isEdit: false,
|
||||
visualConfigJson: [],
|
||||
filterConfigJson: {},
|
||||
previewActiveKey: 1,
|
||||
currentNode: '',
|
||||
previewSqlText: '',
|
||||
previewData: [],
|
||||
previewColumns: [],
|
||||
previewLoading: false,
|
||||
allOptions: [],
|
||||
selectedKeys: [],
|
||||
pagination: {
|
||||
currentPage: 1,
|
||||
pageSize: 30,
|
||||
},
|
||||
finish: false,
|
||||
});
|
||||
const { dataForm, parameterJson, dbOptions, keyword, treeData, btnLoading, visualConfigJson, previewActiveKey, previewLoading, selectedKeys } = toRefs(state);
|
||||
const { t } = useI18n();
|
||||
const { createMessage, createConfirm } = useMessage();
|
||||
const emit = defineEmits(['register', 'confirm']);
|
||||
const sqlEditorRef = ref(null);
|
||||
const leftTreeRef = ref(null);
|
||||
const tableConfigTreeRef = ref<Nullable<TreeActionType>>(null);
|
||||
const [registerModal, { closeModal, changeLoading }] = useModalInner(init);
|
||||
const [registerTableConfigDrawer, { openDrawer: openTableConfigDrawer }] = useDrawer();
|
||||
const [registerFilterConfigDrawer, { openDrawer: openFilterConfigDrawer }] = useDrawer();
|
||||
const infiniteBody = ref<Nullable<ScrollActionType>>(null);
|
||||
|
||||
const getSysVariableList = computed(() => {
|
||||
let list = [
|
||||
{ id: '@userId', fullName: '当前用户' },
|
||||
{ id: '@userAndSubordinates', fullName: '当前用户及下属' },
|
||||
{ id: '@organizeId', fullName: '当前组织' },
|
||||
{ id: '@organizationAndSuborganization', fullName: '当前组织及子组织' },
|
||||
{ id: '@branchManageOrganize', fullName: '当前分管组织' },
|
||||
];
|
||||
if (props.type == 'print') list.unshift({ id: '@formId', fullName: '当前表单ID' });
|
||||
if (state.dataForm.type === 1) {
|
||||
const extraList = [
|
||||
{ id: '@lotSnowID', fullName: '批量生成不同雪花ID' },
|
||||
{ id: '@snowFlakeID', fullName: '系统生成雪花ID' },
|
||||
];
|
||||
list.unshift(...extraList);
|
||||
}
|
||||
return list;
|
||||
});
|
||||
const getTreeBindValue = computed(() => {
|
||||
const key = +new Date();
|
||||
const data: any = {
|
||||
defaultExpandAll: false,
|
||||
treeData: state.treeData,
|
||||
loading: state.treeLoading,
|
||||
loadData: onLoadData,
|
||||
onSelect: handleTreeSelect,
|
||||
key,
|
||||
};
|
||||
return data;
|
||||
});
|
||||
const getTableConfigBind = computed(() => ({
|
||||
linkId: state.dataForm.dbLinkId,
|
||||
sysVariableList: unref(getSysVariableList),
|
||||
getTableConfigTree: getTableConfigTree,
|
||||
}));
|
||||
|
||||
function init(data) {
|
||||
state.btnLoading = false;
|
||||
state.previewActiveKey = 1;
|
||||
handleClearPreviewData();
|
||||
state.keyword = '';
|
||||
state.isEdit = !!data.data;
|
||||
state.dataSetList = cloneDeep(data.list);
|
||||
const dataForm = data.data ? cloneDeep(data.data) : cloneDeep(defaultDataForm);
|
||||
state.dataForm = dataForm;
|
||||
if (!state.isEdit) {
|
||||
const id = 'DataSet' + buildBitUUID();
|
||||
state.dataForm.yunzhupaasId = id;
|
||||
state.dataForm.fullName = id;
|
||||
}
|
||||
state.dataForm.parameterJson = dataForm.parameterJson ? JSON.parse(dataForm.parameterJson) : [];
|
||||
state.fieldJson = dataForm.fieldJson ? JSON.parse(dataForm.fieldJson) : [];
|
||||
state.visualConfigJson = dataForm.visualConfigJson ? JSON.parse(dataForm.visualConfigJson) : [];
|
||||
state.filterConfigJson = dataForm.filterConfigJson ? JSON.parse(dataForm.filterConfigJson) : cloneDeep(defaultFilterConfigJson);
|
||||
changeLoading(true);
|
||||
getDataSourceSelector().then(res => {
|
||||
let list = res.data.list || [];
|
||||
list = list.filter(o => o.children && o.children.length);
|
||||
if (list[0]?.children?.length) list[0] = list[0].children[0];
|
||||
delete list[0].children;
|
||||
state.dbOptions = list;
|
||||
changeLoading(false);
|
||||
state.finish = false;
|
||||
state.treeData = [];
|
||||
state.pagination.currentPage = 1;
|
||||
getTableList();
|
||||
nextTick(() => {
|
||||
bindScroll();
|
||||
});
|
||||
});
|
||||
}
|
||||
function getSourceTypeOptions(isRequired) {
|
||||
return isRequired ? relationFieldOptions.filter(o => o.id != 3) : relationFieldOptions;
|
||||
}
|
||||
function bindScroll() {
|
||||
const bodyRef = infiniteBody.value;
|
||||
const vBody = bodyRef?.getScrollWrap();
|
||||
vBody?.addEventListener('scroll', () => {
|
||||
if (vBody.scrollHeight - vBody.clientHeight - vBody.scrollTop <= 200 && !state.treeLoading && !state.finish) {
|
||||
state.pagination.currentPage += 1;
|
||||
getTableList();
|
||||
}
|
||||
});
|
||||
}
|
||||
function onPropsUrlChange(val, row) {
|
||||
if (!val) {
|
||||
state.dataForm.interfaceId = '';
|
||||
state.dataForm.treePropsName = '';
|
||||
state.dataForm.parameterJson = [];
|
||||
return;
|
||||
}
|
||||
if (state.dataForm.interfaceId === val) return;
|
||||
state.dataForm.interfaceId = val;
|
||||
state.dataForm.treePropsName = row.fullName;
|
||||
state.dataForm.parameterJson = row.parameterJson ? JSON.parse(row.parameterJson).map(o => ({ ...o, relationField: '', sourceType: 2 })) : [];
|
||||
}
|
||||
function onSourceTypeChange(record) {
|
||||
record.relationField = record.sourceType == 4 ? unref(getSysVariableList)[0]?.id : '';
|
||||
}
|
||||
function onDbLinkChange() {
|
||||
treeData.value = [];
|
||||
state.pagination.currentPage = 1;
|
||||
state.finish = false;
|
||||
getTableList(true);
|
||||
}
|
||||
function getTableList(isClean = false) {
|
||||
state.treeLoading = true;
|
||||
const query = {
|
||||
linkId: state.dataForm.dbLinkId,
|
||||
keyword: state.keyword,
|
||||
pageSize: state.pagination.pageSize,
|
||||
currentPage: state.pagination.currentPage,
|
||||
};
|
||||
getDataModelList(query)
|
||||
.then(res => {
|
||||
state.treeLoading = false;
|
||||
if (res.data.list.length < state.pagination.pageSize) state.finish = true;
|
||||
state.treeData = [...state.treeData, ...res.data.list];
|
||||
state.treeData = state.treeData.map(o => ({
|
||||
...o,
|
||||
fullName: o.tableName ? o.table + '(' + o.tableName + ')' : o.table,
|
||||
isLeaf: false,
|
||||
id: o.table,
|
||||
icon: o.type == 1 ? 'icon-ym icon-ym-view' : 'icon-ym icon-ym-generator-tableGrid',
|
||||
}));
|
||||
if (isClean) {
|
||||
state.dataForm.dataConfigJson = '';
|
||||
state.visualConfigJson = [];
|
||||
state.filterConfigJson = cloneDeep(defaultFilterConfigJson);
|
||||
handleClearPreviewData();
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
state.treeLoading = false;
|
||||
state.treeData = [];
|
||||
});
|
||||
}
|
||||
function onLoadData(node) {
|
||||
return new Promise((resolve: (value?: unknown) => void) => {
|
||||
getDataModelFieldList(state.dataForm.dbLinkId, node.dataRef.table).then(res => {
|
||||
const data = res.data.list.map(o => ({
|
||||
...o,
|
||||
isLeaf: true,
|
||||
fullName: o.fieldName ? o.field + '(' + o.fieldName + ')' : o.field,
|
||||
id: node.dataRef.table + '-' + o.field,
|
||||
}));
|
||||
getTree().updateNodeByKey(node.eventKey, { children: data, isLeaf: !data.length });
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
function handleSearchTable() {
|
||||
state.pagination.currentPage = 1;
|
||||
getTableList();
|
||||
treeData.value = [];
|
||||
handleClearPreviewData();
|
||||
}
|
||||
function handleSysNodeClick(data) {
|
||||
(sqlEditorRef.value as any)?.insert(data);
|
||||
}
|
||||
function handleItemClick(item) {
|
||||
item.field && (sqlEditorRef.value as any)?.insert('{' + item.field + '}');
|
||||
}
|
||||
function handleTreeSelect(keys) {
|
||||
const selectedNode: any = getTree()?.getSelectedNode(keys[0]);
|
||||
const content = selectedNode.isLeaf ? selectedNode.field : selectedNode.table;
|
||||
(sqlEditorRef.value as any)?.insert(content);
|
||||
}
|
||||
function getTree() {
|
||||
const tree = unref(leftTreeRef);
|
||||
if (!tree) {
|
||||
throw new Error('tree is null!');
|
||||
}
|
||||
return tree as any;
|
||||
}
|
||||
// 更新字段
|
||||
function updateFieldList(data) {
|
||||
state.btnLoading = true;
|
||||
changeLoading(true);
|
||||
getFields(data)
|
||||
.then(res => {
|
||||
data.children = res.data.map(o => ({ ...o, yunzhupaasId: data.fullName + '.' + o.id }));
|
||||
changeLoading(false);
|
||||
state.btnLoading = false;
|
||||
updateDataSetList(data);
|
||||
})
|
||||
.catch(() => {
|
||||
changeLoading(false);
|
||||
state.btnLoading = false;
|
||||
});
|
||||
}
|
||||
function getTableConfigTree() {
|
||||
const tree = unref(tableConfigTreeRef);
|
||||
if (!tree) {
|
||||
throw new Error('tree is null!');
|
||||
}
|
||||
return tree;
|
||||
}
|
||||
// 删除数据库表
|
||||
function handleDelete(table) {
|
||||
createConfirm({
|
||||
iconType: 'warning',
|
||||
title: t('common.tipTitle'),
|
||||
content: `确定删除数据库表?`,
|
||||
onOk: () => {
|
||||
getTableConfigTree().deleteNodeByKey(table);
|
||||
handleClearPreviewData();
|
||||
},
|
||||
});
|
||||
}
|
||||
function getAddBtnShow(parentTable) {
|
||||
if (!parentTable) return true;
|
||||
return state.visualConfigJson.some(o => o.table === parentTable);
|
||||
}
|
||||
function getIconClassName(item) {
|
||||
if (!item.parentTable) return 'icon-ym icon-ym-generator-tableGrid';
|
||||
if (item.relationConfig.type === 1) return 'icon-ym icon-ym-left-join';
|
||||
if (item.relationConfig.type === 2) return 'icon-ym icon-ym-right-join';
|
||||
if (item.relationConfig.type === 3) return 'icon-ym icon-ym-inner-join';
|
||||
if (item.relationConfig.type === 4) return 'icon-ym icon-ym-full-join';
|
||||
return 'icon-ym icon-ym-generator-tableGrid';
|
||||
}
|
||||
function getIconTitle(item) {
|
||||
if (!item.parentTable) return '';
|
||||
if (item.relationConfig.type === 1) return '左连接';
|
||||
if (item.relationConfig.type === 2) return '右连接';
|
||||
if (item.relationConfig.type === 3) return '内连接';
|
||||
if (item.relationConfig.type === 4) return '全连接';
|
||||
return '';
|
||||
}
|
||||
// 新增数据库表
|
||||
function handleAdd(table = '') {
|
||||
state.currentNode = table;
|
||||
if (state.currentNode) {
|
||||
const node: any = getTableConfigTree().getSelectedNode(state.currentNode);
|
||||
if (node?.children?.length >= 2) {
|
||||
createMessage.warning('最多只能添加两个子表');
|
||||
return;
|
||||
}
|
||||
}
|
||||
openTableConfigDrawer(true, { parentTable: table });
|
||||
}
|
||||
function handleEdit(table) {
|
||||
state.currentNode = table;
|
||||
const node: any = getTableConfigTree().getSelectedNode(state.currentNode);
|
||||
openTableConfigDrawer(true, { parentTable: node.parentTable, data: node });
|
||||
}
|
||||
function onTreeNodeClose() {
|
||||
handleClearPreviewData();
|
||||
}
|
||||
function onTreeNodeConfirm(data, isEdit, isDelChild) {
|
||||
if (!isEdit) {
|
||||
if (!state.currentNode) return state.visualConfigJson.push(data);
|
||||
const node = getTableConfigTree().getSelectedNode(state.currentNode);
|
||||
node?.children?.push(data);
|
||||
handleClearPreviewData();
|
||||
return;
|
||||
}
|
||||
if (isDelChild) data.children = [];
|
||||
getTableConfigTree().updateNodeByKey(state.currentNode, data);
|
||||
handleClearPreviewData();
|
||||
}
|
||||
function openFilterConfig() {
|
||||
if (!state.visualConfigJson.length) return createMessage.error('请先配置数据库表');
|
||||
openFilterConfigDrawer(true, { data: state.filterConfigJson, visualConfigJson: state.visualConfigJson });
|
||||
}
|
||||
function onFilterConfigConfirm(data) {
|
||||
state.filterConfigJson = data;
|
||||
handleClearPreviewData();
|
||||
}
|
||||
// 获取预览数据
|
||||
function handleGetPreviewData() {
|
||||
if (!state.visualConfigJson.length && state.dataForm.type == 1) return createMessage.error('请先配置数据库表');
|
||||
if (!state.dataForm.interfaceId && state.dataForm.type == 3) return createMessage.error('请选择数据接口');
|
||||
state.previewLoading = true;
|
||||
const data = {
|
||||
...state.dataForm,
|
||||
parameterJson: state.dataForm.parameterJson.length ? JSON.stringify(state.dataForm.parameterJson) : '',
|
||||
fieldJson: state.fieldJson.length ? JSON.stringify(state.fieldJson) : '',
|
||||
visualConfigJson: state.visualConfigJson.length ? JSON.stringify(state.visualConfigJson) : '',
|
||||
filterConfigJson: state.filterConfigJson ? JSON.stringify(state.filterConfigJson) : '',
|
||||
};
|
||||
getPreviewData(data)
|
||||
.then(res => {
|
||||
state.previewData = res.data.previewData;
|
||||
let previewColumns = res.data.previewColumns.map(o => ({ title: o.title, dataIndex: o.title, key: o.title }));
|
||||
state.previewColumns = [...previewColumns];
|
||||
state.previewSqlText = res.data.previewSqlText;
|
||||
state.previewLoading = false;
|
||||
})
|
||||
.catch(() => {
|
||||
handleClearPreviewData();
|
||||
});
|
||||
}
|
||||
// 清空预览数据
|
||||
function handleClearPreviewData() {
|
||||
state.previewLoading = false;
|
||||
state.previewData = [];
|
||||
state.previewColumns = [];
|
||||
state.previewSqlText = '';
|
||||
state.dataForm.interfaceId = '';
|
||||
state.dataForm.propsName = '';
|
||||
state.dataForm.parameterJson = [];
|
||||
state.selectedKeys = [];
|
||||
}
|
||||
function handleStep() {
|
||||
treeData.value = [];
|
||||
state.visualConfigJson = [];
|
||||
state.pagination.currentPage = 1;
|
||||
state.finish = false;
|
||||
handleClearPreviewData();
|
||||
if (state.dataForm.type == 1) {
|
||||
getTableList();
|
||||
nextTick(() => {
|
||||
bindScroll();
|
||||
});
|
||||
}
|
||||
}
|
||||
function handleCopySql() {
|
||||
if (!state.previewSqlText) return;
|
||||
const { isSuccessRef } = useCopyToClipboard(state.previewSqlText);
|
||||
unref(isSuccessRef) && createMessage.success('复制成功');
|
||||
}
|
||||
// 更新数据集列表
|
||||
function updateDataSetList(data) {
|
||||
if (!state.isEdit) {
|
||||
state.dataSetList.push(data);
|
||||
} else {
|
||||
for (let i = 0; i < state.dataSetList.length; i++) {
|
||||
if (state.dataSetList[i].yunzhupaasId === data.yunzhupaasId) {
|
||||
state.dataSetList[i] = data;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
emit('confirm', state.dataSetList);
|
||||
closeModal();
|
||||
}
|
||||
function handleSubmit() {
|
||||
if (!state.dataForm.fullName) {
|
||||
createMessage.error('请输入数据集名称');
|
||||
return;
|
||||
}
|
||||
const reg = /(^_([A-Za-z0-9]_?)*$)|(^[A-Za-z](_?[A-Za-z0-9])*_?$)/;
|
||||
if (!reg.test(state.dataForm.fullName)) {
|
||||
createMessage.error('数据集名称只能包含字母、数字、下划线,并且以字母开头');
|
||||
return;
|
||||
}
|
||||
let boo = false;
|
||||
for (let i = 0; i < state.dataSetList.length; i++) {
|
||||
if (state.dataForm.yunzhupaasId !== state.dataSetList[i].yunzhupaasId && state.dataForm.fullName === state.dataSetList[i].fullName) {
|
||||
boo = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (boo) return createMessage.error('数据集名称已存在');
|
||||
if (state.dataForm.type === 1 && !state.dataForm.dataConfigJson) return createMessage.error('请输入SQL语句');
|
||||
if (state.dataForm.type === 2 && !state.visualConfigJson.length) return createMessage.error('请配置数据库表');
|
||||
if (!state.dataForm.interfaceId && state.dataForm.type == 3) return createMessage.error('请选择数据接口');
|
||||
if (state.dataForm.type == 3 && !exist()) return;
|
||||
const data = {
|
||||
...state.dataForm,
|
||||
parameterJson: state.dataForm.parameterJson.length ? JSON.stringify(state.dataForm.parameterJson) : '',
|
||||
fieldJson: state.fieldJson.length ? JSON.stringify(state.fieldJson) : '',
|
||||
visualConfigJson: state.visualConfigJson.length ? JSON.stringify(state.visualConfigJson) : '',
|
||||
filterConfigJson: state.filterConfigJson ? JSON.stringify(state.filterConfigJson) : '',
|
||||
};
|
||||
updateFieldList(data);
|
||||
}
|
||||
function exist() {
|
||||
let isOk = true;
|
||||
for (let i = 0; i < state.dataForm.parameterJson?.length; i++) {
|
||||
const e = state.dataForm.parameterJson[i];
|
||||
if (e.sourceType == 2 && !e.relationField) {
|
||||
createMessage.error(`参数${e.field}的参数值不能为空`);
|
||||
isOk = false;
|
||||
break;
|
||||
}
|
||||
if (!!e.required && e.sourceType == 2 && !e.relationField) {
|
||||
createMessage.error(`参数${e.field}的参数值不能为空`);
|
||||
isOk = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return isOk;
|
||||
}
|
||||
</script>
|
||||
<style lang="less">
|
||||
.dataSet-modal {
|
||||
.dataSet-modal-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
border-radius: 4px;
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
border: 1px solid @border-color-base;
|
||||
background-color: @component-background;
|
||||
.label {
|
||||
margin-left: 10px;
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
width: 70px;
|
||||
}
|
||||
.dataSet-modal-select {
|
||||
width: 400px;
|
||||
border-radius: 3px;
|
||||
margin: 8px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.dataSet-modal-main {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
overflow: hidden;
|
||||
.left-pane {
|
||||
flex-shrink: 0;
|
||||
width: 350px;
|
||||
margin-right: 10px;
|
||||
.left-pane-box {
|
||||
margin-top: 8px;
|
||||
border-radius: 4px;
|
||||
height: calc(100% - 40px);
|
||||
border: 1px solid @border-color-base;
|
||||
background-color: @component-background;
|
||||
overflow: hidden;
|
||||
.search-box {
|
||||
padding: 10px;
|
||||
}
|
||||
& > .scroll-container {
|
||||
height: calc(100% - 52px) !important;
|
||||
}
|
||||
.tree-box {
|
||||
overflow: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
.middle-pane {
|
||||
background-color: @component-background;
|
||||
border: 1px solid @border-color-base;
|
||||
border-radius: 4px;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
.system-text {
|
||||
text-align: end;
|
||||
}
|
||||
.title {
|
||||
border-top: 1px solid @border-color-base;
|
||||
}
|
||||
.title-box {
|
||||
height: 36px;
|
||||
line-height: 36px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
color: @text-color-label;
|
||||
font-size: 14px;
|
||||
padding: 0 10px;
|
||||
flex-shrink: 0;
|
||||
border-bottom: 1px solid @border-color-base;
|
||||
}
|
||||
.tabs-box {
|
||||
overflow: unset;
|
||||
:deep(.ant-tabs-tab:first-child) {
|
||||
margin-left: 20px;
|
||||
}
|
||||
}
|
||||
.table-actions {
|
||||
flex-shrink: 0;
|
||||
border-top: 1px dashed @border-color-base;
|
||||
text-align: center;
|
||||
}
|
||||
.top-box {
|
||||
display: flex;
|
||||
.main-box {
|
||||
flex: 1;
|
||||
margin-right: 18px;
|
||||
}
|
||||
}
|
||||
.yunzhupaas-content-wrapper-tabs {
|
||||
flex-shrink: 0;
|
||||
.ant-btn-link {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
margin-left: 20px;
|
||||
}
|
||||
}
|
||||
.config-tree-box {
|
||||
flex: 1;
|
||||
min-height: 200px;
|
||||
.yunzhupaas-tree .ant-tree .ant-tree-node-content-wrapper,
|
||||
.yunzhupaas-tree .ant-tree .ant-tree-switcher,
|
||||
.yunzhupaas-tree__title {
|
||||
line-height: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
.add-btn-box {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
.preview-box {
|
||||
flex-shrink: 0;
|
||||
border-top: 1px solid @border-color-base1;
|
||||
.preview-content {
|
||||
position: relative;
|
||||
height: 380px;
|
||||
.preview-tip {
|
||||
color: @text-color-label;
|
||||
text-align: right;
|
||||
line-height: 40px;
|
||||
padding: 0 10px;
|
||||
}
|
||||
.copy-btn {
|
||||
position: absolute;
|
||||
right: 15px;
|
||||
top: 15px;
|
||||
color: @text-color-label;
|
||||
cursor: pointer;
|
||||
font-size: 18px;
|
||||
}
|
||||
.preview-content-sql {
|
||||
height: 100%;
|
||||
border-radius: 0;
|
||||
border: unset !important;
|
||||
box-shadow: unset !important;
|
||||
padding: 10px 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.interface-content {
|
||||
padding: 20px;
|
||||
.ant-select.ant-select-in-form-item {
|
||||
width: 400px;
|
||||
}
|
||||
.interface-box {
|
||||
flex: 1;
|
||||
min-height: 310px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.right-pane {
|
||||
width: 350px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100% + 9px);
|
||||
overflow: hidden;
|
||||
margin-left: 10px;
|
||||
.right-pane-btn {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.field-table-box {
|
||||
background-color: @component-background;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
78
src/components/CommonModal/src/ExportModal.vue
Normal file
78
src/components/CommonModal/src/ExportModal.vue
Normal file
@@ -0,0 +1,78 @@
|
||||
<template>
|
||||
<BasicModal
|
||||
v-bind="$attrs"
|
||||
@register="registerModal"
|
||||
:title="t('common.exportText')"
|
||||
:okText="t('common.exportText')"
|
||||
@ok="handleSubmit"
|
||||
destroyOnClose
|
||||
class="export-modal">
|
||||
<a-form :colon="false" labelAlign="left" :labelCol="{ style: { width: '90px' } }">
|
||||
<div class="export-line">
|
||||
<p class="export-label">导出方式</p>
|
||||
</div>
|
||||
<a-form-item>
|
||||
<a-radio-group v-model:value="dataType">
|
||||
<a-radio :value="0">当前页面数据</a-radio>
|
||||
<a-radio :value="1">全部页面数据</a-radio>
|
||||
<a-radio :value="2" :disabled="!selectIds || !selectIds.length" v-if="showExportSelected">当前选择数据</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<div class="export-line">
|
||||
<p class="export-label">导出数据<span>请选择导出字段</span></p>
|
||||
</div>
|
||||
<a-checkbox :indeterminate="isIndeterminate" v-model:checked="checkAll" @change="handleCheckAllChange">全选</a-checkbox>
|
||||
<a-checkbox-group v-model:value="checkedList" class="options-list" @change="handleCheckedChange">
|
||||
<a-checkbox v-for="item in columnList" :key="item.id" :value="item.id" class="options-item">
|
||||
{{ item.fullName }}
|
||||
</a-checkbox>
|
||||
</a-checkbox-group>
|
||||
</a-form>
|
||||
</BasicModal>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { BasicModal, useModalInner } from '@/components/Modal';
|
||||
import { useMessage } from '@/hooks/web/useMessage';
|
||||
import { useI18n } from '@/hooks/web/useI18n';
|
||||
import { noGroupList } from '@/components/FormGenerator/src/helper/config';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
|
||||
const emit = defineEmits(['register', 'download']);
|
||||
const [registerModal, { changeOkLoading }] = useModalInner(init);
|
||||
const { createMessage } = useMessage();
|
||||
const { t } = useI18n();
|
||||
const dataType = ref(0);
|
||||
const showExportSelected = ref(false);
|
||||
const selectIds = ref([]);
|
||||
const isIndeterminate = ref(false);
|
||||
const checkAll = ref(false);
|
||||
const columnList = ref<any[]>([]);
|
||||
const checkedList = ref<string[]>([]);
|
||||
const defaultCheckedList = ref<string[]>([]);
|
||||
|
||||
function init(data) {
|
||||
showExportSelected.value = data.showExportSelected || data.showExportSelected == false ? data.showExportSelected : true;
|
||||
columnList.value = cloneDeep(data.columnList).filter(o => !noGroupList.includes(o.__config__.yunzhupaasKey));
|
||||
selectIds.value = data.selectIds || [];
|
||||
dataType.value = 0;
|
||||
checkedList.value = columnList.value.map(o => o.id);
|
||||
handleCheckedChange(checkedList.value);
|
||||
}
|
||||
function handleCheckAllChange(e) {
|
||||
const val = e.target.checked;
|
||||
checkedList.value = val ? columnList.value.map(o => o.id) : defaultCheckedList.value;
|
||||
isIndeterminate.value = val ? false : !!defaultCheckedList.value.length;
|
||||
}
|
||||
function handleCheckedChange(val) {
|
||||
const checkedCount = val.length;
|
||||
checkAll.value = checkedCount === columnList.value.length;
|
||||
isIndeterminate.value = !!checkedCount && checkedCount < columnList.value.length;
|
||||
}
|
||||
function handleSubmit() {
|
||||
if (!checkedList.value.length) return createMessage.warning('请至少选择一个导出字段');
|
||||
changeOkLoading(true);
|
||||
const data = { dataType: dataType.value, selectKey: checkedList.value, selectIds: selectIds.value };
|
||||
emit('download', data);
|
||||
}
|
||||
</script>
|
||||
384
src/components/CommonModal/src/ImportModal.vue
Normal file
384
src/components/CommonModal/src/ImportModal.vue
Normal file
@@ -0,0 +1,384 @@
|
||||
<template>
|
||||
<BasicModal
|
||||
v-bind="$attrs"
|
||||
@register="registerModal"
|
||||
title="批量导入"
|
||||
:width="1000"
|
||||
:show-cancel-btn="false"
|
||||
:show-ok-btn="false"
|
||||
destroyOnClose
|
||||
class="yunzhupaas-import-modal">
|
||||
<div class="header-steps">
|
||||
<a-steps v-model:current="activeStep" type="navigation" size="small">
|
||||
<a-step title="上传文件" disabled />
|
||||
<a-step title="数据预览" disabled />
|
||||
<a-step title="导入数据" disabled />
|
||||
</a-steps>
|
||||
</div>
|
||||
<div class="import-main" v-show="activeStep == 0">
|
||||
<div class="upload">
|
||||
<div class="up_left">
|
||||
<img src="../../../assets/images/upload.png" />
|
||||
</div>
|
||||
<div class="up_right">
|
||||
<p class="title">上传填好的数据表</p>
|
||||
<p class="tip">文件后缀名必须是xls或xlsx,文件大小不超过500KB,最多支持导入1000条数据</p>
|
||||
<a-upload
|
||||
v-model:file-list="fileList"
|
||||
class="upload-area"
|
||||
accept=".xls,.xlsx"
|
||||
:max-count="1"
|
||||
:action="getAction"
|
||||
:headers="getHeaders"
|
||||
:before-upload="beforeUpload"
|
||||
@remove="handleFileRemove"
|
||||
@change="handleFileChange">
|
||||
<a-button type="link">上传文件</a-button>
|
||||
</a-upload>
|
||||
</div>
|
||||
</div>
|
||||
<div class="upload">
|
||||
<div class="up_left">
|
||||
<img src="../../../assets/images/import.png" />
|
||||
</div>
|
||||
<div class="up_right">
|
||||
<p class="title">填写导入数据信息</p>
|
||||
<p class="tip">请按照数据模板的格式准备导入数据,模板中的表头名称不可更改,表头行不能删除</p>
|
||||
<a-button type="link" @click="handleTemplateDownload()">下载模板</a-button>
|
||||
</div>
|
||||
</div>
|
||||
<a-form-item :colon="false" label="跳过数据预览">
|
||||
<a-switch v-model:checked="skipPreview" />
|
||||
</a-form-item>
|
||||
</div>
|
||||
<div class="import-main" v-show="activeStep == 1">
|
||||
<a-table
|
||||
:data-source="list"
|
||||
:columns="columns"
|
||||
size="small"
|
||||
bordered
|
||||
:pagination="false"
|
||||
:scroll="{ x: 'max-content', y: '420px' }"
|
||||
class="import-preview-table">
|
||||
<template #bodyCell="{ column, record, index }">
|
||||
<template v-if="column.dataIndex === 'index'">{{ index + 1 }}</template>
|
||||
<template v-else-if="column.dataIndex === 'action'">
|
||||
<a-button class="action-btn" type="link" color="error" @click="handleDelItem(index)" size="small">删除</a-button>
|
||||
</template>
|
||||
<template v-else-if="column.isChild">
|
||||
<template v-for="item in state.columnsChildList">
|
||||
<template v-if="item.children && item.children[0] && column.dataIndex === item.children[0]?.dataIndex && column.isChild">
|
||||
<div class="child-table-column">
|
||||
<tr v-for="(cItem, cIndex) in record[item.dataIndex]" class="child-table__row" :key="cIndex">
|
||||
<td v-for="(headItem, i) in item.children" :key="i" :style="{ width: `${headItem.width}px` }">
|
||||
<div class="cell" v-if="headItem.dataIndex === 'delete'">
|
||||
<a-button class="action-btn" type="link" color="error" @click="handleDelChildItem(record[item.dataIndex], cIndex)" size="small">
|
||||
删除
|
||||
</a-button>
|
||||
</div>
|
||||
<div class="cell" v-else>
|
||||
<a-input v-model:value="cItem[headItem.dataIndex]" />
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-input v-model:value="record[column.dataIndex]" />
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
<div class="import-main" v-show="activeStep == 2">
|
||||
<div class="success" v-if="!result.resultType">
|
||||
<img src="../../../assets/images/success.png" />
|
||||
<p class="success-title">批量导入成功</p>
|
||||
<p class="success-tip">您已成功导入{{ result.snum }}条数据</p>
|
||||
</div>
|
||||
<div class="unsuccess" v-if="result.resultType">
|
||||
<div class="upload error-show">
|
||||
<div class="up_left">
|
||||
<img src="../../../assets/images/tip.png" />
|
||||
</div>
|
||||
<div class="up_right">
|
||||
<p class="tip success-tip">
|
||||
正常数据条数:<span>{{ result.snum }}条</span>
|
||||
</p>
|
||||
<p class="tip error-tip">
|
||||
异常数据条数:<span>{{ result.fnum }}条</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="conTips">
|
||||
<p>以下文件数据为导入异常数据</p>
|
||||
<a-button type="link" preIcon="icon-ym icon-ym-btn-download" @click="handleExportExceptionData()">导出异常数据</a-button>
|
||||
</div>
|
||||
<a-table :data-source="resultList" :columns="resultColumns" size="small" bordered :pagination="false" :scroll="{ x: 'max-content', y: '205px' }">
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-for="item in state.resultColumnsChildList">
|
||||
<template v-if="item.children && item.children[0] && column.dataIndex === item.children[0]?.dataIndex && column.isChild">
|
||||
<div class="child-table-column">
|
||||
<tr v-for="(cItem, cIndex) in record[item.dataIndex]" class="child-table__row" :key="cIndex">
|
||||
<td v-for="(headItem, i) in item.children" :key="i" :style="{ width: `${headItem.width}px` }">
|
||||
<div class="cell" :title="cItem[headItem.dataIndex]">{{ cItem[headItem.dataIndex] }}</div>
|
||||
</td>
|
||||
</tr>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
</div>
|
||||
<template #insertFooter>
|
||||
<a-button @click="handleClose()" v-if="activeStep == 0">{{ t('common.cancelText') }}</a-button>
|
||||
<a-button @click="handlePrev" v-if="activeStep === 1">{{ t('common.prev') }}</a-button>
|
||||
<a-button type="primary" @click="handleNext" :loading="btnLoading" v-if="activeStep < 2" :disabled="activeStep === 0 && !fileName">
|
||||
{{ t('common.next') }}
|
||||
</a-button>
|
||||
<a-button type="primary" @click="handleClose(true)" v-else>关闭</a-button>
|
||||
</template>
|
||||
</BasicModal>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { reactive, toRefs, computed } from 'vue';
|
||||
import { BasicModal, useModalInner } from '@/components/Modal';
|
||||
import { getTemplateDownload, getImportPreview, importData, getImportExceptionData } from '@/api/basic/common';
|
||||
import { useMessage } from '@/hooks/web/useMessage';
|
||||
import { useI18n } from '@/hooks/web/useI18n';
|
||||
import { downloadByUrl } from '@/utils/file/download';
|
||||
import { useGlobSetting } from '@/hooks/setting';
|
||||
import { getToken } from '@/utils/auth';
|
||||
import { Upload as AUpload } from 'ant-design-vue';
|
||||
import type { UploadChangeParam, UploadFile } from 'ant-design-vue';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
|
||||
interface State {
|
||||
activeStep: number;
|
||||
fileName: string;
|
||||
fileList: UploadFile[];
|
||||
btnLoading: boolean;
|
||||
list: any[];
|
||||
result: any;
|
||||
resultList: any[];
|
||||
actionUrl: string;
|
||||
apiUrl: string;
|
||||
columns: any[];
|
||||
resultColumns: any[];
|
||||
columnsChildList: any[];
|
||||
resultColumnsChildList: any[];
|
||||
flowId: string;
|
||||
menuId: string;
|
||||
skipPreview: boolean;
|
||||
}
|
||||
const emit = defineEmits(['register', 'reload']);
|
||||
const [registerModal, { closeModal }] = useModalInner(init);
|
||||
const { createMessage, createConfirm } = useMessage();
|
||||
const { t } = useI18n();
|
||||
const noColumn = { title: '序号', dataIndex: 'index', align: 'center', width: 50, customRender: ({ index }) => index + 1 };
|
||||
const actionColumn = { title: '操作', dataIndex: 'action', align: 'center', width: 50, fixed: 'right' };
|
||||
const errorColumn = { title: '异常原因', dataIndex: 'errorsInfo', width: 150, fixed: 'right' };
|
||||
|
||||
const globSetting = useGlobSetting();
|
||||
const state = reactive<State>({
|
||||
activeStep: 0,
|
||||
fileName: '',
|
||||
fileList: [],
|
||||
btnLoading: false,
|
||||
list: [],
|
||||
result: {},
|
||||
resultList: [],
|
||||
actionUrl: '',
|
||||
apiUrl: '',
|
||||
columns: [],
|
||||
resultColumns: [],
|
||||
columnsChildList: [],
|
||||
resultColumnsChildList: [],
|
||||
flowId: '',
|
||||
menuId: '',
|
||||
skipPreview: false,
|
||||
});
|
||||
const { activeStep, fileName, fileList, btnLoading, list, result, resultList, columns, resultColumns, skipPreview } = toRefs(state);
|
||||
|
||||
const getAction = computed(() => globSetting.apiUrl + state.actionUrl);
|
||||
const getHeaders = computed(() => ({ Authorization: getToken() as string }));
|
||||
|
||||
function init(data) {
|
||||
state.actionUrl = `/api/${data.url || 'visualdev/OnlineDev'}/Uploader`;
|
||||
state.apiUrl = data.url || `visualdev/OnlineDev/${data.modelId}`;
|
||||
state.menuId = data.menuId;
|
||||
state.flowId = data.flowId || '';
|
||||
state.activeStep = 0;
|
||||
state.fileName = '';
|
||||
state.fileList = [];
|
||||
state.btnLoading = false;
|
||||
state.skipPreview = false;
|
||||
}
|
||||
function handlePrev() {
|
||||
if (state.activeStep == 0) return;
|
||||
state.activeStep -= 1;
|
||||
}
|
||||
function handleNext() {
|
||||
if (state.activeStep == 0) {
|
||||
if (!state.fileList.length || !state.fileName) return createMessage.warning('请先上传文件');
|
||||
if (state.skipPreview) return handleSubmit();
|
||||
state.btnLoading = true;
|
||||
getImportPreview(state.apiUrl, { fileName: state.fileName })
|
||||
.then(res => {
|
||||
state.list = res.data.dataRow || [];
|
||||
const headerList = res.data.headerRow || [];
|
||||
state.resultColumns = getHeaderColumn(headerList);
|
||||
getMergeList(state.resultColumns);
|
||||
state.resultColumnsChildList = state.resultColumns.filter(o => o.yunzhupaasKey === 'table' && o.children && o.children.length);
|
||||
const preHeaderList = cloneDeep(headerList);
|
||||
for (let i = 0; i < preHeaderList.length; i++) {
|
||||
if (preHeaderList[i].children?.length && preHeaderList[i]?.yunzhupaasKey == 'table') {
|
||||
preHeaderList[i].children.push({
|
||||
title: '操作',
|
||||
dataIndex: 'delete',
|
||||
width: 50,
|
||||
isChild: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
state.columns = [noColumn, ...preHeaderList, actionColumn];
|
||||
getMergeList(state.columns);
|
||||
state.columnsChildList = state.columns.filter(o => o.yunzhupaasKey === 'table' && o.children && o.children.length);
|
||||
state.btnLoading = false;
|
||||
state.activeStep += 1;
|
||||
})
|
||||
.catch(() => {
|
||||
state.btnLoading = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (state.activeStep == 1) {
|
||||
if (!state.list.length) return createMessage.warning('导入数据为空');
|
||||
handleSubmit();
|
||||
}
|
||||
}
|
||||
function handleSubmit() {
|
||||
state.btnLoading = true;
|
||||
let query: any = { list: state.list || [] };
|
||||
if (state.flowId) query.flowId = state.flowId;
|
||||
if (state.skipPreview) query = { ...query, type: state.skipPreview, fileName: state.fileName };
|
||||
importData(state.apiUrl, query)
|
||||
.then(res => {
|
||||
state.result = res.data;
|
||||
state.resultList = res.data.failResult;
|
||||
state.activeStep += 1;
|
||||
if (state.skipPreview) {
|
||||
state.activeStep = 2;
|
||||
const headerList = res.data.headerRow || [];
|
||||
state.resultColumns = getHeaderColumn(headerList);
|
||||
getMergeList(state.resultColumns);
|
||||
state.resultColumnsChildList = state.resultColumns.filter(o => o.yunzhupaasKey === 'table' && o.children && o.children.length);
|
||||
}
|
||||
state.btnLoading = false;
|
||||
})
|
||||
.catch(() => {
|
||||
state.btnLoading = false;
|
||||
});
|
||||
}
|
||||
function getHeaderColumn(headerList) {
|
||||
const loop = list => {
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
list[i] = {
|
||||
...list[i],
|
||||
title: list[i].fullName,
|
||||
dataIndex: list[i].id,
|
||||
width: 150,
|
||||
};
|
||||
if (list[i].children?.length) loop(list[i].children);
|
||||
}
|
||||
};
|
||||
loop(headerList);
|
||||
return [noColumn, ...headerList, errorColumn];
|
||||
}
|
||||
function getMergeList(list) {
|
||||
list.forEach(item => {
|
||||
if (item.children && item.children.length && item?.yunzhupaasKey == 'table') {
|
||||
item.children.forEach((child, index) => {
|
||||
child.isChild = true;
|
||||
if (index == 0) {
|
||||
child.customCell = () => ({
|
||||
rowspan: 1,
|
||||
colspan: item.children.length,
|
||||
class: 'child-table-box',
|
||||
});
|
||||
} else {
|
||||
child.customCell = () => ({
|
||||
rowspan: 0,
|
||||
colspan: 0,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
function handleFileChange({ file }: UploadChangeParam) {
|
||||
if (file.status === 'error') {
|
||||
createMessage.error('上传失败');
|
||||
return;
|
||||
}
|
||||
if (file.status === 'done') {
|
||||
if (file.response.code === 200) {
|
||||
state.fileName = file.response.data.name;
|
||||
} else {
|
||||
createMessage.error(file.response.msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
function beforeUpload(file) {
|
||||
const fileType = file.name.replace(/.+\./, '');
|
||||
const isAccept = ['xls', 'xlsx'].indexOf(fileType.toLowerCase()) !== -1;
|
||||
if (!isAccept) {
|
||||
createMessage.error('文件格式不正确');
|
||||
return AUpload.LIST_IGNORE;
|
||||
}
|
||||
const isRightSize = file.size / 1024 < 500;
|
||||
if (!isRightSize) {
|
||||
createMessage.error('文件大小超过500KB');
|
||||
return AUpload.LIST_IGNORE;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
function handleFileRemove(file) {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
createConfirm({
|
||||
iconType: 'warning',
|
||||
title: t('common.tipTitle'),
|
||||
content: `确定移除${file.name}?`,
|
||||
onOk: () => {
|
||||
state.fileName = '';
|
||||
resolve();
|
||||
},
|
||||
onCancel: () => {
|
||||
reject();
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
function handleTemplateDownload() {
|
||||
getTemplateDownload(state.apiUrl, { menuId: state.menuId }).then(res => {
|
||||
downloadByUrl({ url: res.data?.url });
|
||||
});
|
||||
}
|
||||
function handleExportExceptionData() {
|
||||
getImportExceptionData(state.apiUrl, { list: state.resultList, menuId: state.menuId }).then(res => {
|
||||
downloadByUrl({ url: res.data?.url });
|
||||
});
|
||||
}
|
||||
function handleDelItem(index) {
|
||||
state.list.splice(index, 1);
|
||||
}
|
||||
function handleDelChildItem(data, index) {
|
||||
data.splice(index, 1);
|
||||
}
|
||||
function handleClose(reload = false) {
|
||||
closeModal();
|
||||
if (reload) emit('reload');
|
||||
}
|
||||
</script>
|
||||
235
src/components/CommonModal/src/InterfaceModal.vue
Normal file
235
src/components/CommonModal/src/InterfaceModal.vue
Normal file
@@ -0,0 +1,235 @@
|
||||
<template>
|
||||
<div class="common-container">
|
||||
<a-select v-model:value="innerValue" v-bind="getSelectBindValue" :options="options" @change="onChange" @click="openSelectModal" />
|
||||
<a-modal
|
||||
v-model:open="visible"
|
||||
:title="popupTitle"
|
||||
:width="1000"
|
||||
class="common-container-modal"
|
||||
@ok="handleSubmit"
|
||||
@cancel="handleCancel"
|
||||
:maskClosable="false">
|
||||
<template #closeIcon>
|
||||
<ModalClose :canFullscreen="false" @cancel="handleCancel" />
|
||||
</template>
|
||||
<div class="yunzhupaas-content-wrapper">
|
||||
<div class="yunzhupaas-content-wrapper-left">
|
||||
<BasicLeftTree ref="leftTreeRef" :showSearch="false" :treeData="treeData" :loading="treeLoading" @select="handleTreeSelect" />
|
||||
</div>
|
||||
<div class="yunzhupaas-content-wrapper-center">
|
||||
<div class="yunzhupaas-content-wrapper-content">
|
||||
<BasicTable @register="registerTable" :searchInfo="searchInfo" class="yunzhupaas-sub-table">
|
||||
<template #expandedRowRender="{ record }">
|
||||
<BasicTable @register="registerSubTable" :data-source="record.templateJson">
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'field'">
|
||||
<span class="required-sign">{{ record.required ? '*' : '' }}</span>
|
||||
{{ record.field }}{{ record.fieldName ? '(' + record.fieldName + ')' : '' }}
|
||||
</template>
|
||||
<template v-if="column.key === 'dataType'">
|
||||
{{ getTypeText(record.dataType) }}
|
||||
</template>
|
||||
</template>
|
||||
</BasicTable>
|
||||
</template>
|
||||
</BasicTable>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { getDataInterfaceSelectorList } from '@/api/systemData/dataInterface';
|
||||
import { Form, Modal as AModal } from 'ant-design-vue';
|
||||
import { reactive, ref, unref, watch, computed } from 'vue';
|
||||
import ModalClose from '@/components/Modal/src/components/ModalClose.vue';
|
||||
import { BasicLeftTree, TreeActionType } from '@/components/Tree';
|
||||
import { BasicTable, useTable, BasicColumn } from '@/components/Table';
|
||||
import { useI18n } from '@/hooks/web/useI18n';
|
||||
import { useBaseStore } from '@/store/modules/base';
|
||||
import { pick } from 'lodash-es';
|
||||
|
||||
defineOptions({ inheritAttrs: false });
|
||||
const props = defineProps({
|
||||
value: { default: '' },
|
||||
title: { type: String, default: '' },
|
||||
popupTitle: { type: String, default: '数据接口' },
|
||||
dataType: { type: String, default: '' },
|
||||
disabled: { type: Boolean, default: false },
|
||||
allowClear: { type: Boolean, default: true },
|
||||
size: { type: String, default: 'default' },
|
||||
/**
|
||||
* sourceType
|
||||
* 1 - 过滤掉:鉴权、真分页、SQL的增加、修改、删除类型
|
||||
* 2 - 过滤掉:鉴权、SQL的增加、修改、删除类型
|
||||
* 3 - 过滤掉:鉴权、真分页、SQL的查询类型
|
||||
*/
|
||||
sourceType: { type: Number, default: 1 },
|
||||
});
|
||||
const emit = defineEmits(['update:value', 'change']);
|
||||
const formItemContext = Form.useInjectFormItemContext();
|
||||
const { t } = useI18n();
|
||||
const baseStore = useBaseStore();
|
||||
const innerValue = ref(undefined);
|
||||
const visible = ref(false);
|
||||
const options = ref<any[]>([]);
|
||||
|
||||
const columns: BasicColumn[] = [
|
||||
{ title: '名称', dataIndex: 'fullName' },
|
||||
{ title: '编码', dataIndex: 'enCode' },
|
||||
{ title: '类型', dataIndex: 'type', width: 100 },
|
||||
];
|
||||
const searchInfo = reactive({
|
||||
category: '',
|
||||
sourceType: props.sourceType,
|
||||
});
|
||||
const leftTreeRef = ref<Nullable<TreeActionType>>(null);
|
||||
const treeLoading = ref(false);
|
||||
const treeData = ref<any[]>([]);
|
||||
const [registerTable, { getForm, getSelectRows, setSelectedRowKeys, getSelectRowKeys }] = useTable({
|
||||
api: getDataInterfaceSelectorList,
|
||||
columns,
|
||||
immediate: false,
|
||||
useSearchForm: true,
|
||||
formConfig: {
|
||||
baseColProps: { span: 8 },
|
||||
schemas: [
|
||||
{
|
||||
field: 'keyword',
|
||||
label: t('common.keyword'),
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: t('common.enterKeyword'),
|
||||
submitOnPressEnter: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'type',
|
||||
label: '类型',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: [
|
||||
{ id: '2', fullName: '静态数据' },
|
||||
{ id: '1', fullName: 'SQL操作' },
|
||||
{ id: '3', fullName: 'API操作' },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
tableSetting: { size: false, setting: false },
|
||||
isCanResizeParent: true,
|
||||
resizeHeightOffset: -74,
|
||||
rowSelection: { type: 'radio' },
|
||||
afterFetch: data => {
|
||||
const list = data.map(o => {
|
||||
let templateJson = o.parameterJson ? JSON.parse(o.parameterJson) : [];
|
||||
if (!templateJson) templateJson = [];
|
||||
return {
|
||||
...o,
|
||||
templateJson,
|
||||
};
|
||||
});
|
||||
return list;
|
||||
},
|
||||
});
|
||||
const [registerSubTable] = useTable({
|
||||
columns: [
|
||||
{ title: '参数名称', dataIndex: 'field', key: 'field' },
|
||||
{ title: '参数类型', dataIndex: 'dataType', key: 'dataType' },
|
||||
{ title: '默认值', dataIndex: 'defaultValue', key: 'defaultValue', ellipsis: false },
|
||||
],
|
||||
pagination: false,
|
||||
showTableSetting: false,
|
||||
canResize: false,
|
||||
scroll: { x: undefined },
|
||||
});
|
||||
|
||||
const typeOptions: any[] = [
|
||||
{ fullName: '字符串', id: 'varchar' },
|
||||
{ fullName: '整型', id: 'int' },
|
||||
{ fullName: '日期时间', id: 'datetime' },
|
||||
{ fullName: '浮点', id: 'decimal' },
|
||||
{ fullName: '长整型', id: 'bigint' },
|
||||
{ fullName: '文本', id: 'text' },
|
||||
];
|
||||
|
||||
const getSelectBindValue = computed(() => {
|
||||
return {
|
||||
...pick(props, ['disabled', 'size', 'allowClear']),
|
||||
fieldNames: { label: 'fullName', value: 'id' },
|
||||
placeholder: '请选择',
|
||||
open: false,
|
||||
showSearch: false,
|
||||
showArrow: true,
|
||||
};
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.value,
|
||||
val => {
|
||||
setValue(val);
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
watch(
|
||||
() => props.title,
|
||||
() => {
|
||||
setValue(props.value);
|
||||
},
|
||||
);
|
||||
|
||||
function getTypeText(type) {
|
||||
let item = typeOptions.filter(o => o.id == type)[0];
|
||||
return item && item.fullName ? item.fullName : '';
|
||||
}
|
||||
function setValue(value) {
|
||||
innerValue.value = value || undefined;
|
||||
options.value = [{ id: innerValue.value, fullName: props.title }];
|
||||
}
|
||||
function onChange() {
|
||||
options.value = [];
|
||||
emit('change', '', {});
|
||||
}
|
||||
async function openSelectModal() {
|
||||
if (props.disabled) return;
|
||||
visible.value = true;
|
||||
treeLoading.value = true;
|
||||
treeData.value = (await baseStore.getDictionaryData('DataInterfaceType')) as any[];
|
||||
if (!treeData.value.length) return (treeLoading.value = false);
|
||||
searchInfo.category = treeData.value[0].id;
|
||||
const leftTree = unref(leftTreeRef);
|
||||
leftTree?.setSelectedKeys([searchInfo.category]);
|
||||
treeLoading.value = false;
|
||||
getForm().resetFields();
|
||||
setSelectedRowKeys(innerValue.value ? [innerValue.value] : []);
|
||||
}
|
||||
function handleTreeSelect(id) {
|
||||
if (!id || searchInfo.category === id) return;
|
||||
searchInfo.category = id;
|
||||
searchInfo.sourceType = props.sourceType;
|
||||
getForm().resetFields();
|
||||
}
|
||||
function handleCancel() {
|
||||
visible.value = false;
|
||||
}
|
||||
function handleSubmit() {
|
||||
if (!getSelectRowKeys().length && !getSelectRows().length) return;
|
||||
if (!getSelectRows().length) {
|
||||
emit('update:value', innerValue.value);
|
||||
emit('change', innerValue.value, options.value[0]);
|
||||
formItemContext.onFieldChange();
|
||||
handleCancel();
|
||||
return;
|
||||
}
|
||||
const selectRow = getSelectRows()[0];
|
||||
options.value = getSelectRows();
|
||||
innerValue.value = selectRow.id;
|
||||
emit('update:value', selectRow.id);
|
||||
emit('change', selectRow.id, selectRow);
|
||||
formItemContext.onFieldChange();
|
||||
handleCancel();
|
||||
}
|
||||
</script>
|
||||
96
src/components/CommonModal/src/PreviewModal.vue
Normal file
96
src/components/CommonModal/src/PreviewModal.vue
Normal file
@@ -0,0 +1,96 @@
|
||||
<template>
|
||||
<BasicModal v-bind="$attrs" @register="registerModal" title="预览" :footer="null" :width="600" class="yunzhupaas-add-modal yunzhupaas-preview-modal">
|
||||
<a-tabs v-model:activeKey="state.previewType" size="small" v-if="state.type === 'webDesign' || state.type === 'flow'">
|
||||
<a-tab-pane :key="0" tab="设计中" />
|
||||
<a-tab-pane :key="1" tab="已发布" v-if="state.isRelease" />
|
||||
</a-tabs>
|
||||
<div class="add-main">
|
||||
<div class="add-item add-item-left" @click="previewPc()">
|
||||
<i class="add-icon icon-ym icon-ym-pc"></i>
|
||||
<div class="add-txt">
|
||||
<p class="add-title">Web预览</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="add-item" @click="previewApp()">
|
||||
<i class="add-icon icon-ym icon-ym-mobile"></i>
|
||||
<div class="add-txt">
|
||||
<p class="add-title">App预览</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<BasicModal v-bind="$attrs" @register="registerQrModal" title="预览" :footer="null" :width="400" class="yunzhupaas-qrcode-modal">
|
||||
<yunzhupaas-qrcode :staticText="qrCodeText" :width="280" />
|
||||
<p class="tip">打开手机APP扫码预览</p>
|
||||
</BasicModal>
|
||||
</BasicModal>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive } from 'vue';
|
||||
import { useGo } from '@/hooks/web/usePage';
|
||||
import { BasicModal, useModal, useModalInner } from '@/components/Modal';
|
||||
|
||||
interface State {
|
||||
type: string;
|
||||
id: string;
|
||||
fullName: string;
|
||||
previewType: number;
|
||||
isRelease: number;
|
||||
}
|
||||
|
||||
const emit = defineEmits(['register', 'previewPc']);
|
||||
const [registerModal, { closeModal }] = useModalInner(init);
|
||||
const [registerQrModal, { openModal: openQrModal }] = useModal();
|
||||
const go = useGo();
|
||||
const qrCodeText = ref('');
|
||||
const state = reactive<State>({
|
||||
type: '',
|
||||
id: '',
|
||||
fullName: '',
|
||||
previewType: 0,
|
||||
isRelease: 0,
|
||||
});
|
||||
|
||||
function init(data) {
|
||||
state.type = data.type || '';
|
||||
state.id = data.id;
|
||||
state.fullName = data.fullName || '';
|
||||
state.isRelease = data.isRelease || 0;
|
||||
state.previewType = 0;
|
||||
}
|
||||
function previewPc() {
|
||||
closeModal();
|
||||
if (state.type === 'webDesign') {
|
||||
if (!state.id) return;
|
||||
go(`/previewModel?isPreview=1&id=${state.id}&previewType=${state.previewType}`);
|
||||
return;
|
||||
}
|
||||
setTimeout(() => {
|
||||
emit('previewPc', { id: state.id, previewType: state.previewType });
|
||||
}, 300);
|
||||
}
|
||||
function previewApp() {
|
||||
let data: any = {
|
||||
t: state.type === 'flow' ? 'WFP' : state.type === 'webDesign' ? 'ADP' : state.type,
|
||||
id: state.id,
|
||||
};
|
||||
if (state.type === 'report') data.fullName = state.fullName;
|
||||
if (state.type == 'webDesign' || state.type === 'flow') data.previewType = state.previewType;
|
||||
qrCodeText.value = JSON.stringify(data);
|
||||
closeModal();
|
||||
openQrModal(true);
|
||||
}
|
||||
</script>
|
||||
<style lang="less">
|
||||
.yunzhupaas-qrcode-modal {
|
||||
.yunzhupaas-qrcode {
|
||||
padding: 10px;
|
||||
}
|
||||
.tip {
|
||||
text-align: center;
|
||||
font-size: 18px;
|
||||
margin-top: 10px;
|
||||
padding-bottom: 20px;
|
||||
color: @text-color-secondary;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
45
src/components/CommonModal/src/SelectFlowModal.vue
Normal file
45
src/components/CommonModal/src/SelectFlowModal.vue
Normal file
@@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<BasicModal v-bind="$attrs" @register="registerModal" title="请选择流程" width="400px" :footer="null" destroyOnClose class="yunzhupaas-flow-list-modal">
|
||||
<div class="template-search">
|
||||
<a-input-search :placeholder="t('common.enterKeyword')" allowClear v-model:value="state.keyword" />
|
||||
</div>
|
||||
<div class="template-list">
|
||||
<ScrollContainer>
|
||||
<div class="template-item" v-for="item in getFlowList" :key="item.id" @click="selectFlow(item)">
|
||||
{{ item.fullName }}
|
||||
</div>
|
||||
<yunzhupaas-empty v-if="!getFlowList.length" />
|
||||
</ScrollContainer>
|
||||
</div>
|
||||
</BasicModal>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { reactive, computed } from 'vue';
|
||||
import { BasicModal, useModalInner } from '@/components/Modal';
|
||||
import { ScrollContainer } from '@/components/Container';
|
||||
import { useI18n } from '@/hooks/web/useI18n';
|
||||
|
||||
interface State {
|
||||
flowList: any[];
|
||||
keyword: string;
|
||||
}
|
||||
|
||||
const state = reactive<State>({
|
||||
flowList: [],
|
||||
keyword: '',
|
||||
});
|
||||
const emit = defineEmits(['register', 'change']);
|
||||
const [registerModal, { closeModal }] = useModalInner(init);
|
||||
const { t } = useI18n();
|
||||
|
||||
const getFlowList = computed(() => (state.keyword ? state.flowList.filter(o => o.fullName.indexOf(state.keyword) !== -1) : state.flowList));
|
||||
|
||||
function init(data) {
|
||||
state.keyword = '';
|
||||
state.flowList = data.flowList || [];
|
||||
}
|
||||
function selectFlow(item) {
|
||||
emit('change', item);
|
||||
closeModal();
|
||||
}
|
||||
</script>
|
||||
302
src/components/CommonModal/src/SelectModal.vue
Normal file
302
src/components/CommonModal/src/SelectModal.vue
Normal file
@@ -0,0 +1,302 @@
|
||||
<template>
|
||||
<div class="common-container">
|
||||
<template v-if="config.popupType === 'dialog'">
|
||||
<a-modal
|
||||
v-model:open="visible"
|
||||
:title="config.popupTitle"
|
||||
:width="config.popupWidth"
|
||||
class="common-container-modal"
|
||||
@ok="handleSubmit"
|
||||
@cancel="handleCancel"
|
||||
:maskClosable="false">
|
||||
<template #closeIcon>
|
||||
<ModalClose :canFullscreen="false" @cancel="handleCancel" />
|
||||
</template>
|
||||
<div class="yunzhupaas-common-search-box yunzhupaas-common-search-box-modal">
|
||||
<a-form :colon="false" labelAlign="right" :model="listQuery" ref="formElRef" :class="getFormClass">
|
||||
<a-row :gutter="10">
|
||||
<a-col :span="8">
|
||||
<a-form-item :label="t('common.keyword')" name="keyword">
|
||||
<a-input v-model:value="listQuery.keyword" :placeholder="t('common.enterKeyword')" allowClear @pressEnter="handleSearch" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item label=" ">
|
||||
<a-button type="primary" class="mr-2" @click="handleSearch">{{ t('common.queryText') }}</a-button>
|
||||
<a-button @click="handleReset">{{ t('common.resetText') }}</a-button>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form>
|
||||
<div class="yunzhupaas-common-search-box-right">
|
||||
<a-tooltip placement="top">
|
||||
<template #title>
|
||||
<span>{{ t('common.redo') }}</span>
|
||||
</template>
|
||||
<RedoOutlined class="yunzhupaas-common-search-box-right-icon" @click="initData" />
|
||||
</a-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<a-table :data-source="list" v-bind="getTableBindValues" @change="handleTableChange" ref="tableElRef">
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.dataIndex !== 'index'">{{ record[column.dataIndex] }}</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-modal>
|
||||
</template>
|
||||
<template v-if="config.popupType === 'drawer'">
|
||||
<a-drawer :title="config.popupTitle" :width="config.popupWidth" v-model:open="visible" :class="drawerPrefixCls + ' common-container-drawer'">
|
||||
<div class="common-container-drawer-body">
|
||||
<div class="yunzhupaas-common-search-box yunzhupaas-common-search-box-modal">
|
||||
<a-form :colon="false" labelAlign="right" :model="listQuery" ref="formElRef" :class="getFormClass">
|
||||
<a-row :gutter="10">
|
||||
<a-col :span="8">
|
||||
<a-form-item :label="t('common.keyword')" name="keyword">
|
||||
<a-input v-model:value="listQuery.keyword" :placeholder="t('common.enterKeyword')" allowClear @pressEnter="handleSearch" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item label=" ">
|
||||
<a-button type="primary" class="mr-2" @click="handleSearch">{{ t('common.queryText') }}</a-button>
|
||||
<a-button @click="handleReset">{{ t('common.resetText') }}</a-button>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form>
|
||||
<div class="yunzhupaas-common-search-box-right">
|
||||
<a-tooltip placement="top">
|
||||
<template #title>
|
||||
<span>{{ t('common.redo') }}</span>
|
||||
</template>
|
||||
<RedoOutlined class="yunzhupaas-common-search-box-right-icon" @click="initData" />
|
||||
</a-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<a-table :data-source="list" v-bind="getTableBindValues" @change="handleTableChange" ref="tableElRef">
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.dataIndex !== 'index'">{{ record[column.dataIndex] }}</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
<DrawerFooter showFooter @close="handleCancel" @ok="handleSubmit"></DrawerFooter>
|
||||
</a-drawer>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { getDataInterfaceDataSelect } from '@/api/systemData/dataInterface';
|
||||
import { getFieldDataSelect } from '@/api/onlineDev/visualDev';
|
||||
import { Modal as AModal, Drawer as ADrawer } from 'ant-design-vue';
|
||||
import { ref, unref, computed, nextTick, reactive, toRefs } from 'vue';
|
||||
import ModalClose from '@/components/Modal/src/components/ModalClose.vue';
|
||||
import { useI18n } from '@/hooks/web/useI18n';
|
||||
import { useDesign } from '@/hooks/web/useDesign';
|
||||
import DrawerFooter from '@/components/Drawer/src/components/DrawerFooter.vue';
|
||||
import { RedoOutlined } from '@ant-design/icons-vue';
|
||||
import type { FormInstance } from 'ant-design-vue';
|
||||
import { useUserStore } from '@/store/modules/user';
|
||||
|
||||
interface State {
|
||||
list: any[];
|
||||
listQuery: any;
|
||||
loading: boolean;
|
||||
total: number;
|
||||
selectedRowKeys: any[];
|
||||
selectedRows: any[];
|
||||
}
|
||||
|
||||
defineOptions({ name: 'SelectModal', inheritAttrs: false });
|
||||
const props = defineProps({
|
||||
config: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
formData: Object,
|
||||
});
|
||||
const emit = defineEmits(['select']);
|
||||
const { t } = useI18n();
|
||||
const { prefixCls: drawerPrefixCls } = useDesign('basic-drawer');
|
||||
const { prefixCls: formPrefixCls } = useDesign('basic-form');
|
||||
const { prefixCls: tablePrefixCls } = useDesign('basic-table');
|
||||
|
||||
const selectRow = ref<any>(null);
|
||||
const visible = ref(false);
|
||||
const userStore = useUserStore();
|
||||
const formElRef = ref<FormInstance>();
|
||||
const tableElRef = ref<any>(null);
|
||||
const indexColumn = { width: 50, title: '序号', dataIndex: 'index', key: 'index', align: 'center', customRender: ({ index }) => index + 1 };
|
||||
const state = reactive<State>({
|
||||
list: [],
|
||||
listQuery: {
|
||||
keyword: '',
|
||||
currentPage: 1,
|
||||
pageSize: 20,
|
||||
},
|
||||
loading: false,
|
||||
total: 0,
|
||||
selectedRowKeys: [],
|
||||
selectedRows: [],
|
||||
});
|
||||
const { listQuery, list } = toRefs(state);
|
||||
|
||||
const getUserInfo = computed(() => userStore.getUserInfo || {});
|
||||
const getIsDynamic = computed(() => props.config.dataSource == 'dynamic');
|
||||
const getFormClass = computed(() => {
|
||||
return [formPrefixCls, `${formPrefixCls}--compact`, 'search-form'];
|
||||
});
|
||||
const getColumns = computed<any[]>(() => {
|
||||
const columns = (props.config.columnOptions as any)
|
||||
.filter(o => o.ifShow || o.ifShow === undefined)
|
||||
.map(o => ({ title: o.label, dataIndex: o.value, ellipsis: true, width: o.width || 100 }));
|
||||
return [indexColumn, ...columns];
|
||||
});
|
||||
const searchInfo = computed(() => {
|
||||
const paramList = getParamList();
|
||||
const columnOptions = (props.config.columnOptions as any).map(o => o.value).join(',');
|
||||
const info: any = {
|
||||
interfaceId: props.config.interfaceId,
|
||||
columnOptions,
|
||||
paramList,
|
||||
};
|
||||
const data = {
|
||||
modelId: props.config.modelId,
|
||||
relationField: props.config.relationField,
|
||||
columnOptions,
|
||||
};
|
||||
return unref(getIsDynamic) ? info : data;
|
||||
});
|
||||
const getPagination = computed<any>(() => {
|
||||
return {
|
||||
current: state.listQuery.currentPage,
|
||||
pageSize: state.listQuery.pageSize,
|
||||
size: 'small',
|
||||
defaultPageSize: 20,
|
||||
showTotal: total => t('component.table.total', { total }),
|
||||
showSizeChanger: true,
|
||||
pageSizeOptions: ['20', '50', '80', '100'],
|
||||
showQuickJumper: true,
|
||||
total: state.total,
|
||||
};
|
||||
});
|
||||
const getRowSelection = computed<any>(() => ({
|
||||
type: 'checkbox',
|
||||
onChange: setSelectedRowKeys,
|
||||
}));
|
||||
const getScrollY = computed(() => {
|
||||
let height = props.config.popupType === 'drawer' ? window.innerHeight - 120 - 52 - 38 : window.innerHeight * 0.7 - 52 - 38;
|
||||
height -= 44;
|
||||
return height;
|
||||
});
|
||||
const getTableBindValues = computed(() => {
|
||||
return {
|
||||
columns: unref(getColumns),
|
||||
pagination: unref(getPagination),
|
||||
rowSelection: unref(getRowSelection),
|
||||
size: 'small',
|
||||
loading: state.loading,
|
||||
rowKey: record => record,
|
||||
scroll: {
|
||||
y: unref(getScrollY),
|
||||
},
|
||||
class: unref(tablePrefixCls),
|
||||
};
|
||||
});
|
||||
|
||||
defineExpose({ openSelectModal });
|
||||
|
||||
function getForm() {
|
||||
const form = unref(formElRef);
|
||||
if (!form) {
|
||||
throw new Error('form is null!');
|
||||
}
|
||||
return form;
|
||||
}
|
||||
function openSelectModal() {
|
||||
visible.value = true;
|
||||
setTimeout(() => {
|
||||
nextTick(() => {
|
||||
handleReset();
|
||||
state.selectedRowKeys = [];
|
||||
state.selectedRows = [];
|
||||
state.list = [];
|
||||
state.total = 0;
|
||||
const tableEl = tableElRef.value?.$el;
|
||||
let bodyEl = tableEl.querySelector('.ant-table-body');
|
||||
bodyEl!.style.height = `${unref(getScrollY)}px`;
|
||||
});
|
||||
}, 50);
|
||||
}
|
||||
function handleCancel() {
|
||||
visible.value = false;
|
||||
}
|
||||
function handleSubmit() {
|
||||
if (!state.selectedRowKeys.length && !state.selectedRows.length) return;
|
||||
selectRow.value = state.selectedRows;
|
||||
let checked: any[] = [];
|
||||
for (let i = 0; i < unref(selectRow).length; i++) {
|
||||
const e = unref(selectRow)[i];
|
||||
let item = {};
|
||||
for (let j = 0; j < props.config.relationOptions.length; j++) {
|
||||
const row = props.config.relationOptions[j];
|
||||
item[row.field] = row.type === 1 ? e[!unref(getIsDynamic) ? row.value + '_yunzhupaasId' : row.value] : row.value;
|
||||
}
|
||||
checked.push(item);
|
||||
}
|
||||
emit('select', checked);
|
||||
handleCancel();
|
||||
}
|
||||
function getParamList() {
|
||||
let templateJson: any[] = props.config.templateJson;
|
||||
if (!props.formData) return templateJson;
|
||||
for (let i = 0; i < templateJson.length; i++) {
|
||||
const e = templateJson[i];
|
||||
const data = props.formData;
|
||||
if (e.sourceType == 1) {
|
||||
e.defaultValue = data[e.relationField] || data[e.relationField] == 0 || data[e.relationField] == false ? data[e.relationField] : '';
|
||||
}
|
||||
if (e.yunzhupaasKey === 'createUser') e.defaultValue = unref(getUserInfo).userId;
|
||||
if (e.yunzhupaasKey === 'createTime') e.defaultValue = new Date().getTime();
|
||||
if (e.yunzhupaasKey === 'currOrganize') e.defaultValue = unref(getUserInfo).organizeId;
|
||||
if (e.yunzhupaasKey === 'currPosition' && unref(getUserInfo).positionIds?.length) e.defaultValue = unref(getUserInfo).positionIds[0].id;
|
||||
}
|
||||
return templateJson;
|
||||
}
|
||||
function handleSearch() {
|
||||
state.listQuery.currentPage = 1;
|
||||
state.listQuery.pageSize = 20;
|
||||
initData();
|
||||
}
|
||||
function handleReset() {
|
||||
getForm().resetFields();
|
||||
state.listQuery.keyword = '';
|
||||
handleSearch();
|
||||
}
|
||||
function initData() {
|
||||
if (unref(getIsDynamic) && !props.config.interfaceId) return;
|
||||
if (!unref(getIsDynamic) && !props.config.modelId) return;
|
||||
state.loading = true;
|
||||
const query = {
|
||||
...state.listQuery,
|
||||
...unref(searchInfo),
|
||||
};
|
||||
const method = unref(getIsDynamic) ? getDataInterfaceDataSelect : getFieldDataSelect;
|
||||
method(query)
|
||||
.then(res => {
|
||||
state.list = res.data.list;
|
||||
state.total = res.data.pagination.total;
|
||||
state.loading = false;
|
||||
})
|
||||
.catch(() => {
|
||||
state.loading = false;
|
||||
});
|
||||
}
|
||||
function handleTableChange(pagination) {
|
||||
state.listQuery.currentPage = pagination.current;
|
||||
state.listQuery.pageSize = pagination.pageSize;
|
||||
initData();
|
||||
}
|
||||
function setSelectedRowKeys(_selectedRowKeys, selectedRows) {
|
||||
state.selectedRows = selectedRows;
|
||||
}
|
||||
</script>
|
||||
177
src/components/CommonModal/src/SuperQueryModal.vue
Normal file
177
src/components/CommonModal/src/SuperQueryModal.vue
Normal file
@@ -0,0 +1,177 @@
|
||||
<template>
|
||||
<BasicModal
|
||||
v-bind="$attrs"
|
||||
@register="registerModal"
|
||||
:title="t('common.superQuery')"
|
||||
:okText="t('common.queryText')"
|
||||
width="700px"
|
||||
@ok="handleSubmit"
|
||||
destroyOnClose
|
||||
class="yunzhupaas-super-query-modal yunzhupaas-condition-modal">
|
||||
<template #insertFooter v-if="!hidePlan">
|
||||
<a-space :size="10" class="float-left">
|
||||
<a-button @click="addPlan">保存方案</a-button>
|
||||
<a-popover placement="bottom" trigger="click" overlayClassName="plan-popover" v-model:open="popoverVisible">
|
||||
<a-button>方案选择<DownOutlined /></a-button>
|
||||
<template #content>
|
||||
<div class="plan-list" v-if="planList.length">
|
||||
<div class="plan-list-item" v-for="(item, i) in planList" :key="i" @click="selectPlan(item)">
|
||||
<p class="plan-list-name">{{ item.fullName }} </p>
|
||||
<i class="icon-ym icon-ym-nav-close" @click.stop="delPlan(item.id, i)"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="noData-txt" v-else>暂无数据</div>
|
||||
</template>
|
||||
</a-popover>
|
||||
</a-space>
|
||||
</template>
|
||||
<ConditionMain ref="conditionMainRef" isSuperQuery showFieldValueType isCustomFieldValueType class="super-query-main" />
|
||||
<BasicModal v-bind="$attrs" @register="registerFormModal" title="保存方案" @ok="handleFormSubmit">
|
||||
<BasicForm @register="registerForm" />
|
||||
</BasicModal>
|
||||
</BasicModal>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { reactive, toRefs, computed, unref, nextTick, ref } from 'vue';
|
||||
import { BasicModal, useModal, useModalInner } from '@/components/Modal';
|
||||
import { BasicForm, useForm } from '@/components/Form';
|
||||
import { getAdvancedQueryList, delAdvancedQuery, create, update } from '@/api/system/advancedQuery';
|
||||
import { useMessage } from '@/hooks/web/useMessage';
|
||||
import { useI18n } from '@/hooks/web/useI18n';
|
||||
import { DownOutlined } from '@ant-design/icons-vue';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import { useRoute } from 'vue-router';
|
||||
import ConditionMain from '@/components/ColumnDesign/src/components/ConditionMain.vue';
|
||||
|
||||
interface State {
|
||||
planList: any[];
|
||||
conditionList: any[];
|
||||
popoverVisible: boolean;
|
||||
fieldOptions: any[];
|
||||
matchLogic: string;
|
||||
hidePlan: boolean;
|
||||
}
|
||||
const emit = defineEmits(['register', 'superQuery']);
|
||||
const [registerModal, { closeModal, changeLoading }] = useModalInner(init);
|
||||
const [registerFormModal, { openModal: openFormModal, closeModal: closeFormModal, setModalProps: setFormModalProps }] = useModal();
|
||||
const [registerForm, { resetFields, validate }] = useForm({
|
||||
labelWidth: 80,
|
||||
schemas: [
|
||||
{
|
||||
field: 'fullName',
|
||||
label: '方案名称',
|
||||
component: 'Input',
|
||||
componentProps: { placeholder: '请输入', maxlength: 50 },
|
||||
rules: [{ required: true, trigger: 'blur', message: '必填' }],
|
||||
},
|
||||
],
|
||||
});
|
||||
const { createMessage, createConfirm } = useMessage();
|
||||
const { t } = useI18n();
|
||||
const route = useRoute();
|
||||
const conditionMainRef = ref();
|
||||
|
||||
const state = reactive<State>({
|
||||
planList: [],
|
||||
conditionList: [
|
||||
{ logic: 'and', groups: [{ field: '', symbol: '', yunzhupaasKey: '', fieldValueType: 2, fieldValue: undefined, fieldValueYunzhupaasKey: '', cellKey: +new Date() }] },
|
||||
],
|
||||
popoverVisible: false,
|
||||
fieldOptions: [],
|
||||
matchLogic: 'and',
|
||||
hidePlan: false,
|
||||
});
|
||||
const { planList, popoverVisible, hidePlan } = toRefs(state);
|
||||
|
||||
const getCurrMenuId = computed(() => (route.meta.modelId as string) || '');
|
||||
|
||||
function init(data) {
|
||||
state.popoverVisible = false;
|
||||
state.hidePlan = data.hidePlan || false;
|
||||
changeLoading(true);
|
||||
state.fieldOptions = cloneDeep(data.columnOptions);
|
||||
conditionMainRef.value?.init({ conditionList: state.conditionList, matchLogic: state.matchLogic, fieldOptions: state.fieldOptions });
|
||||
getPlanList();
|
||||
}
|
||||
function getPlanList() {
|
||||
if (!unref(getCurrMenuId)) return changeLoading(false);
|
||||
getAdvancedQueryList(unref(getCurrMenuId)).then(res => {
|
||||
state.planList = res.data.list;
|
||||
changeLoading(false);
|
||||
});
|
||||
}
|
||||
function addPlan() {
|
||||
const values = conditionMainRef.value?.confirm();
|
||||
if (!values) return;
|
||||
state.conditionList = values.conditionList || [];
|
||||
state.matchLogic = values.matchLogic;
|
||||
if (!state.conditionList.length) return createMessage.error('请添加条件');
|
||||
openFormModal();
|
||||
nextTick(() => {
|
||||
resetFields();
|
||||
});
|
||||
}
|
||||
function delPlan(id, i) {
|
||||
delAdvancedQuery(id).then(res => {
|
||||
createMessage.success(res.msg);
|
||||
state.planList.splice(i, 1);
|
||||
});
|
||||
}
|
||||
function selectPlan(item) {
|
||||
state.matchLogic = item.matchLogic;
|
||||
state.conditionList = item.conditionJson ? JSON.parse(item.conditionJson) : [];
|
||||
conditionMainRef.value?.updateConditionList({ conditionList: state.conditionList, matchLogic: state.matchLogic });
|
||||
state.popoverVisible = false;
|
||||
}
|
||||
async function handleFormSubmit() {
|
||||
const values = await validate();
|
||||
if (!values) return;
|
||||
const fullName = values.fullName;
|
||||
const boo = state.planList.some(o => o.fullName === fullName);
|
||||
if (!boo) return savePlan(fullName);
|
||||
const list = state.planList.filter(o => o.fullName === fullName);
|
||||
createConfirm({
|
||||
iconType: 'warning',
|
||||
title: t('common.tipTitle'),
|
||||
content: `${list[0].fullName}已存在, 是否覆盖方案?`,
|
||||
onOk: () => {
|
||||
savePlan(fullName, list[0].id);
|
||||
},
|
||||
});
|
||||
}
|
||||
function savePlan(fullName, id = '') {
|
||||
setFormModalProps({ confirmLoading: true });
|
||||
const query = {
|
||||
id,
|
||||
fullName,
|
||||
moduleId: unref(getCurrMenuId),
|
||||
matchLogic: state.matchLogic,
|
||||
conditionJson: JSON.stringify(state.conditionList),
|
||||
};
|
||||
const formMethod = query.id ? update : create;
|
||||
formMethod(query)
|
||||
.then(res => {
|
||||
closeFormModal();
|
||||
setFormModalProps({ confirmLoading: false });
|
||||
createMessage.success(res.msg);
|
||||
getPlanList();
|
||||
})
|
||||
.catch(() => {
|
||||
setFormModalProps({ confirmLoading: false });
|
||||
});
|
||||
}
|
||||
function handleSubmit() {
|
||||
const values = conditionMainRef.value?.confirm();
|
||||
if (!values) return;
|
||||
state.conditionList = values.conditionList || [];
|
||||
state.matchLogic = values.matchLogic;
|
||||
const query = {
|
||||
matchLogic: state.matchLogic,
|
||||
conditionList: state.conditionList,
|
||||
};
|
||||
let str = JSON.stringify(query);
|
||||
if (!state.conditionList.length) str = '';
|
||||
emit('superQuery', str);
|
||||
closeModal();
|
||||
}
|
||||
</script>
|
||||
228
src/components/CommonModal/src/UserSelect.vue
Normal file
228
src/components/CommonModal/src/UserSelect.vue
Normal file
@@ -0,0 +1,228 @@
|
||||
<template>
|
||||
<div :class="[$attrs.class, 'select-tag-list']" v-if="buttonType === 'button'">
|
||||
<a-button preIcon="icon-ym icon-ym-btn-add" @click="openSelectModal">{{ modalTitle }}</a-button>
|
||||
<div class="tags">
|
||||
<a-tag class="!mt-10px" :closable="!disabled" v-for="(item, i) in options" :key="item.id" @close="onTagClose(i)">{{ item.fullName }}</a-tag>
|
||||
</div>
|
||||
</div>
|
||||
<a-select v-bind="getSelectBindValue" v-model:value="innerValue" :options="options" @change="onChange" @click="openSelectModal" v-else />
|
||||
<a-modal
|
||||
v-model:open="visible"
|
||||
:title="modalTitle"
|
||||
:width="800"
|
||||
class="transfer-modal"
|
||||
@ok="handleSubmit"
|
||||
centered
|
||||
destroyOnClose
|
||||
:maskClosable="false"
|
||||
:keyboard="false">
|
||||
<template #closeIcon>
|
||||
<ModalClose :canFullscreen="false" @cancel="handleCancel" />
|
||||
</template>
|
||||
<div class="transfer__body">
|
||||
<div class="transfer-pane left-pane">
|
||||
<div class="transfer-pane__tool">
|
||||
<a-input-search :placeholder="t('common.enterKeyword')" allowClear v-model:value="pagination.keyword" @search="handleSearch" />
|
||||
</div>
|
||||
<div class="transfer-pane__body transfer-pane__body-tab">
|
||||
<div class="custom-title">全部数据</div>
|
||||
<ScrollContainer v-loading="loading && pagination.currentPage === 1" ref="infiniteBody">
|
||||
<div v-for="item in ableList" :key="item.id" class="selected-item selected-item-user" @click="handleNodeClick(item)">
|
||||
<div class="selected-item-main">
|
||||
<a-avatar :size="36" :src="apiUrl + item.headIcon" class="selected-item-headIcon" />
|
||||
<div class="selected-item-text">
|
||||
<p class="name">{{ item.fullName }}</p>
|
||||
<p class="organize" :title="item.organize">{{ item.organize }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<yunzhupaas-empty v-if="!ableList.length" />
|
||||
</ScrollContainer>
|
||||
</div>
|
||||
</div>
|
||||
<div class="transfer-pane right-pane">
|
||||
<div class="transfer-pane__tool">
|
||||
<span>{{ t('component.yunzhupaas.common.selected') }}</span>
|
||||
<span class="remove-all-btn" @click="removeAll">清空列表</span>
|
||||
</div>
|
||||
<div class="transfer-pane__body">
|
||||
<ScrollContainer>
|
||||
<div v-for="(item, i) in selectedData" :key="i" class="selected-item selected-item-user">
|
||||
<div class="selected-item-main">
|
||||
<a-avatar :size="36" :src="apiUrl + item.headIcon" class="selected-item-headIcon" />
|
||||
<div class="selected-item-text">
|
||||
<p class="name">{{ item.fullName }}</p>
|
||||
<p class="organize" :title="item.organize">{{ item.organize }}</p>
|
||||
</div>
|
||||
<delete-outlined class="delete-btn" @click="removeData(i)" />
|
||||
</div>
|
||||
</div>
|
||||
<yunzhupaas-empty v-if="!selectedData.length" />
|
||||
</ScrollContainer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { getUserInfoList } from '@/api/permission/user';
|
||||
import { Form, Modal as AModal } from 'ant-design-vue';
|
||||
import { DeleteOutlined } from '@ant-design/icons-vue';
|
||||
import { computed, ref, unref, watch, reactive, nextTick } from 'vue';
|
||||
import { ScrollContainer, ScrollActionType } from '@/components/Container';
|
||||
import ModalClose from '@/components/Modal/src/components/ModalClose.vue';
|
||||
import { useGlobSetting } from '@/hooks/setting';
|
||||
import { useI18n } from '@/hooks/web/useI18n';
|
||||
import { useAttrs } from '@/hooks/core/useAttrs';
|
||||
import { cloneDeep, pick } from 'lodash-es';
|
||||
|
||||
defineOptions({ name: 'CandidateUserSelect', inheritAttrs: false });
|
||||
const props = defineProps({
|
||||
value: { type: [String, Array] as PropType<String | string[]> },
|
||||
multiple: { type: Boolean, default: false },
|
||||
placeholder: { type: String, default: '请选择' },
|
||||
disabled: { type: Boolean, default: false },
|
||||
allowClear: { type: Boolean, default: true },
|
||||
size: String,
|
||||
buttonType: { type: String as PropType<'' | 'select' | 'button' | undefined>, default: 'select' },
|
||||
modalTitle: { type: String, default: '选择用户' },
|
||||
api: { type: Function },
|
||||
query: { type: Object, default: () => ({}) },
|
||||
});
|
||||
const emit = defineEmits(['update:value', 'change', 'labelChange']);
|
||||
const attrs: any = useAttrs({ excludeDefaultKeys: false });
|
||||
const { t } = useI18n();
|
||||
const globSetting = useGlobSetting();
|
||||
const apiUrl = ref(globSetting.apiUrl);
|
||||
const visible = ref(false);
|
||||
const innerValue = ref<string | any[] | undefined>([]);
|
||||
const pagination = reactive({
|
||||
keyword: '',
|
||||
currentPage: 1,
|
||||
pageSize: 20,
|
||||
});
|
||||
const finish = ref<boolean>(false);
|
||||
const infiniteBody = ref<Nullable<ScrollActionType>>(null);
|
||||
const ableList = ref<any[]>([]);
|
||||
const options = ref<any[]>([]);
|
||||
const loading = ref(false);
|
||||
const selectedData = ref<any[]>([]);
|
||||
const formItemContext = Form.useInjectFormItemContext();
|
||||
|
||||
const getSelectBindValue = computed(() => ({
|
||||
...pick(props, ['placeholder', 'disabled', 'size', 'allowClear']),
|
||||
fieldNames: { label: 'fullName', value: 'id' },
|
||||
open: false,
|
||||
mode: props.multiple ? 'multiple' : '',
|
||||
showSearch: false,
|
||||
showArrow: true,
|
||||
class: unref(attrs).class ? 'w-full ' + unref(attrs).class : 'w-full',
|
||||
}));
|
||||
|
||||
watch(
|
||||
() => props.value,
|
||||
() => {
|
||||
setValue();
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
function setValue() {
|
||||
if (!props.value || !props.value.length) {
|
||||
innerValue.value = props.multiple ? [] : undefined;
|
||||
options.value = [];
|
||||
selectedData.value = [];
|
||||
emit('labelChange', '');
|
||||
return;
|
||||
}
|
||||
const ids = props.multiple ? (props.value as any[]) : [props.value];
|
||||
getUserInfoList(unref(ids)).then(res => {
|
||||
const selectedList: any[] = res.data.list;
|
||||
const innerIds = selectedList.map(o => o.id);
|
||||
innerValue.value = props.multiple ? innerIds : innerIds[0];
|
||||
options.value = cloneDeep(selectedList);
|
||||
selectedData.value = cloneDeep(selectedList);
|
||||
const labels = selectedData.value.map(o => o.fullName).join();
|
||||
emit('labelChange', labels);
|
||||
});
|
||||
}
|
||||
function onChange(_val, option) {
|
||||
selectedData.value = option ?? [];
|
||||
handleSubmit();
|
||||
}
|
||||
function onTagClose(i) {
|
||||
selectedData.value.splice(i, 1);
|
||||
handleSubmit();
|
||||
}
|
||||
function openSelectModal() {
|
||||
if (props.disabled) return;
|
||||
visible.value = true;
|
||||
pagination.keyword = '';
|
||||
pagination.currentPage = 1;
|
||||
ableList.value = [];
|
||||
finish.value = false;
|
||||
nextTick(() => {
|
||||
bindScroll();
|
||||
handleSearch();
|
||||
setValue();
|
||||
});
|
||||
}
|
||||
function handleCancel() {
|
||||
visible.value = false;
|
||||
}
|
||||
function handleSearch() {
|
||||
ableList.value = [];
|
||||
pagination.currentPage = 1;
|
||||
finish.value = false;
|
||||
getAbleList();
|
||||
}
|
||||
function bindScroll() {
|
||||
const bodyRef = infiniteBody.value;
|
||||
const vBody = bodyRef?.getScrollWrap();
|
||||
vBody?.addEventListener('scroll', () => {
|
||||
if (vBody.scrollHeight - vBody.clientHeight - vBody.scrollTop <= 200 && !loading.value && !finish.value) {
|
||||
pagination.currentPage += 1;
|
||||
getAbleList();
|
||||
}
|
||||
});
|
||||
}
|
||||
function handleNodeClick(data) {
|
||||
const boo = selectedData.value.some(o => o.id === data.id);
|
||||
if (boo) return;
|
||||
props.multiple ? selectedData.value.push(data) : (selectedData.value = [data]);
|
||||
}
|
||||
function removeAll() {
|
||||
selectedData.value = [];
|
||||
}
|
||||
function removeData(index: number) {
|
||||
selectedData.value.splice(index, 1);
|
||||
}
|
||||
function handleSubmit() {
|
||||
const ids = unref(selectedData).map(o => o.id);
|
||||
options.value = unref(selectedData);
|
||||
innerValue.value = props.multiple ? ids : ids[0];
|
||||
if (props.multiple) {
|
||||
emit('update:value', ids);
|
||||
emit('change', ids, unref(options));
|
||||
} else {
|
||||
emit('update:value', ids[0]);
|
||||
emit('change', ids[0], unref(options)[0]);
|
||||
}
|
||||
formItemContext.onFieldChange();
|
||||
handleCancel();
|
||||
}
|
||||
function getAbleList() {
|
||||
if (!props.api) return;
|
||||
loading.value = true;
|
||||
const query = {
|
||||
...pagination,
|
||||
...props.query,
|
||||
};
|
||||
props.api(query).then(res => {
|
||||
if (res.data.list.length < pagination.pageSize) finish.value = true;
|
||||
ableList.value = [...ableList.value, ...res.data.list];
|
||||
loading.value = false;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
150
src/components/CommonModal/src/ValidatePopover.vue
Normal file
150
src/components/CommonModal/src/ValidatePopover.vue
Normal file
@@ -0,0 +1,150 @@
|
||||
<template>
|
||||
<a-popover
|
||||
v-model:open="visible"
|
||||
trigger="click"
|
||||
placement="bottom"
|
||||
overlayClassName="error-contain yunzhupaas-flow-common-popover"
|
||||
:bordered="false"
|
||||
arrow-point-at-center
|
||||
v-if="getErrorList.length">
|
||||
<template #content>
|
||||
<div class="w-355px">
|
||||
<div class="error-title"><exclamation-circle-filled class="error-icon" />内容不完善,校验失败!</div>
|
||||
<div class="error-content">
|
||||
<div class="error-item" v-for="item in getErrorList">
|
||||
<div class="error-sub-item error-top">
|
||||
<div class="title" :title="item.title">{{ item.title }}</div>
|
||||
<div>
|
||||
<span>{{ item.children.length }}</span>
|
||||
项
|
||||
</div>
|
||||
</div>
|
||||
<div class="error-sub-item error-bottom" v-for="child in item.children">
|
||||
<div class="title" :title="child.errorInfo">{{ child.errorInfo }}</div>
|
||||
<div class="write" v-if="item?.id" @click="handleSelect(item.id)">去填写</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<a-button shape="round" class="error-tips-button">
|
||||
{{ getErrorList.length || 0 }}项不完善<i class="icon-ym icon-ym-unfold ml-5px font text-10px" />
|
||||
</a-button>
|
||||
</a-popover>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import { ExclamationCircleFilled } from '@ant-design/icons-vue';
|
||||
|
||||
const visible = ref<boolean>(false);
|
||||
const emit = defineEmits(['select']);
|
||||
const props = defineProps({
|
||||
errorList: { type: Array as PropType<any[]>, default: () => [] },
|
||||
});
|
||||
|
||||
defineExpose({ setVisible });
|
||||
|
||||
const getErrorList = computed(() => convertArrayToTree(props.errorList));
|
||||
|
||||
/**
|
||||
* 将errorList转成树
|
||||
* @param list errorList
|
||||
*/
|
||||
function convertArrayToTree(list) {
|
||||
let newList: any[] = [];
|
||||
list.map(item => {
|
||||
const index = newList.findIndex(o => o.id === item.id);
|
||||
if (index !== -1) {
|
||||
newList[index].children.push({ errorInfo: item.errorInfo });
|
||||
} else {
|
||||
newList.push({ title: item.title, id: item.id, children: [{ errorInfo: item.errorInfo }] });
|
||||
}
|
||||
});
|
||||
return newList;
|
||||
}
|
||||
function handleSelect(id) {
|
||||
setVisible(false);
|
||||
emit('select', id);
|
||||
}
|
||||
function setVisible(data) {
|
||||
visible.value = !!data;
|
||||
}
|
||||
</script>
|
||||
<style lang="less">
|
||||
.error-tips-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
&:hover i {
|
||||
color: @primary-color;
|
||||
}
|
||||
i {
|
||||
color: @text-color-label;
|
||||
}
|
||||
}
|
||||
.error-contain {
|
||||
.ant-popover-inner {
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
.ant-popover-inner-content {
|
||||
padding: unset !important;
|
||||
}
|
||||
}
|
||||
.ant-popover-arrow::before {
|
||||
background: #fdc6c6 !important;
|
||||
}
|
||||
.error-title {
|
||||
background: linear-gradient(26deg, #fceaea 0%, #fdc6c6 100%);
|
||||
height: 46px;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: @text-color-base;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 26px;
|
||||
.error-icon {
|
||||
margin-right: 4px;
|
||||
color: #e85959;
|
||||
}
|
||||
}
|
||||
.error-content {
|
||||
padding: 8px 26px;
|
||||
max-height: 300px;
|
||||
overflow: auto;
|
||||
.error-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.error-sub-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
line-height: 35px;
|
||||
.title {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
.error-top {
|
||||
.title {
|
||||
font-weight: bold;
|
||||
}
|
||||
span {
|
||||
color: @error-color;
|
||||
}
|
||||
}
|
||||
.error-bottom {
|
||||
.title {
|
||||
color: @text-color-label;
|
||||
}
|
||||
.write {
|
||||
color: @primary-color;
|
||||
flex-shrink: 0;
|
||||
margin-left: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
4
src/components/CommonPopover/index.ts
Normal file
4
src/components/CommonPopover/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { withInstall } from '@/utils';
|
||||
import History from './src/History.vue';
|
||||
|
||||
export const HistoryPopover = withInstall(History);
|
||||
41
src/components/CommonPopover/src/History.vue
Normal file
41
src/components/CommonPopover/src/History.vue
Normal file
@@ -0,0 +1,41 @@
|
||||
<template>
|
||||
<a-popover trigger="click" placement="bottom" overlayClassName="yunzhupaas-common-history-popover" arrow-point-at-center destroyTooltipOnHide>
|
||||
<template #content>
|
||||
<div class="history-popover-content">
|
||||
<div class="title"><i class="icon-ym icon-ym-flow-history" />历史记录</div>
|
||||
<ScrollContainer class="contain">
|
||||
<div
|
||||
v-if="recordList.length"
|
||||
class="item"
|
||||
v-for="(item, index) in recordList"
|
||||
:key="index"
|
||||
@click="handleJump(item)"
|
||||
:class="{ 'current-item': activeIndex == item.id, 'past-item': activeIndex < item.id }">
|
||||
<i :class="item.icon" v-if="item.icon" />
|
||||
{{ item.fullName }}
|
||||
</div>
|
||||
<yunzhupaas-empty class="my-85px" v-else />
|
||||
</ScrollContainer>
|
||||
</div>
|
||||
</template>
|
||||
<a-tooltip placement="top" title="历史记录">
|
||||
<a-button type="text" class="yunzhupaas-history-btn">
|
||||
<i class="icon-ym icon-ym-flow-history" />
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
</a-popover>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ScrollContainer } from '@/components/Container';
|
||||
|
||||
defineProps({
|
||||
activeIndex: { type: Number, default: 0 },
|
||||
recordList: { type: Array as PropType<any[]>, default: () => [] },
|
||||
});
|
||||
const emit = defineEmits(['jump']);
|
||||
|
||||
function handleJump(item) {
|
||||
emit('jump', item.id);
|
||||
}
|
||||
</script>
|
||||
10
src/components/Container/index.ts
Normal file
10
src/components/Container/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { withInstall } from '@/utils';
|
||||
import collapseContainer from './src/collapse/CollapseContainer.vue';
|
||||
import scrollContainer from './src/ScrollContainer.vue';
|
||||
import lazyContainer from './src/LazyContainer.vue';
|
||||
|
||||
export const CollapseContainer = withInstall(collapseContainer);
|
||||
export const ScrollContainer = withInstall(scrollContainer);
|
||||
export const LazyContainer = withInstall(lazyContainer);
|
||||
|
||||
export * from './src/typing';
|
||||
138
src/components/Container/src/LazyContainer.vue
Normal file
138
src/components/Container/src/LazyContainer.vue
Normal file
@@ -0,0 +1,138 @@
|
||||
<template>
|
||||
<transition-group class="h-full w-full" v-bind="$attrs" ref="elRef" :name="transitionName" :tag="tag" mode="out-in">
|
||||
<div key="component" v-if="isInit">
|
||||
<slot :loading="loading"></slot>
|
||||
</div>
|
||||
<div key="skeleton" v-else>
|
||||
<slot name="skeleton" v-if="$slots.skeleton"></slot>
|
||||
<Skeleton v-else />
|
||||
</div>
|
||||
</transition-group>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import type { PropType } from 'vue';
|
||||
import { defineComponent, reactive, onMounted, ref, toRef, toRefs } from 'vue';
|
||||
import { Skeleton } from 'ant-design-vue';
|
||||
import { useTimeoutFn } from '@/hooks/core/useTimeout';
|
||||
import { useIntersectionObserver } from '@/hooks/event/useIntersectionObserver';
|
||||
|
||||
interface State {
|
||||
isInit: boolean;
|
||||
loading: boolean;
|
||||
intersectionObserverInstance: IntersectionObserver | null;
|
||||
}
|
||||
|
||||
const props = {
|
||||
/**
|
||||
* Waiting time, if the time is specified, whether visible or not, it will be automatically loaded after the specified time
|
||||
*/
|
||||
timeout: { type: Number },
|
||||
/**
|
||||
* The viewport where the component is located.
|
||||
* If the component is scrolling in the page container, the viewport is the container
|
||||
*/
|
||||
viewport: {
|
||||
type: (typeof window !== 'undefined' ? window.HTMLElement : Object) as PropType<HTMLElement>,
|
||||
default: () => null,
|
||||
},
|
||||
/**
|
||||
* Preload threshold, css unit
|
||||
*/
|
||||
threshold: { type: String, default: '0px' },
|
||||
/**
|
||||
* The scroll direction of the viewport, vertical represents the vertical direction, horizontal represents the horizontal direction
|
||||
*/
|
||||
direction: {
|
||||
type: String,
|
||||
default: 'vertical',
|
||||
validator: v => ['vertical', 'horizontal'].includes(v),
|
||||
},
|
||||
/**
|
||||
* The label name of the outer container that wraps the component
|
||||
*/
|
||||
tag: { type: String, default: 'div' },
|
||||
maxWaitingTime: { type: Number, default: 80 },
|
||||
/**
|
||||
* transition name
|
||||
*/
|
||||
transitionName: { type: String, default: 'lazy-container' },
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
name: 'LazyContainer',
|
||||
components: { Skeleton },
|
||||
inheritAttrs: false,
|
||||
props,
|
||||
emits: ['init'],
|
||||
setup(props, { emit }) {
|
||||
const elRef = ref();
|
||||
const state = reactive<State>({
|
||||
isInit: false,
|
||||
loading: false,
|
||||
intersectionObserverInstance: null,
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
immediateInit();
|
||||
initIntersectionObserver();
|
||||
});
|
||||
|
||||
// If there is a set delay time, it will be executed immediately
|
||||
function immediateInit() {
|
||||
const { timeout } = props;
|
||||
timeout &&
|
||||
useTimeoutFn(() => {
|
||||
init();
|
||||
}, timeout);
|
||||
}
|
||||
|
||||
function init() {
|
||||
state.loading = true;
|
||||
|
||||
useTimeoutFn(() => {
|
||||
if (state.isInit) return;
|
||||
state.isInit = true;
|
||||
emit('init');
|
||||
}, props.maxWaitingTime || 80);
|
||||
}
|
||||
|
||||
function initIntersectionObserver() {
|
||||
const { timeout, direction, threshold } = props;
|
||||
if (timeout) return;
|
||||
// According to the scrolling direction to construct the viewport margin, used to load in advance
|
||||
let rootMargin = '0px';
|
||||
switch (direction) {
|
||||
case 'vertical':
|
||||
rootMargin = `${threshold} 0px`;
|
||||
break;
|
||||
case 'horizontal':
|
||||
rootMargin = `0px ${threshold}`;
|
||||
break;
|
||||
}
|
||||
|
||||
try {
|
||||
const { stop, observer } = useIntersectionObserver({
|
||||
rootMargin,
|
||||
target: toRef(elRef.value, '$el'),
|
||||
onIntersect: (entries: any[]) => {
|
||||
const isIntersecting = entries[0].isIntersecting || entries[0].intersectionRatio;
|
||||
if (isIntersecting) {
|
||||
init();
|
||||
if (observer) {
|
||||
stop();
|
||||
}
|
||||
}
|
||||
},
|
||||
root: toRef(props, 'viewport'),
|
||||
});
|
||||
} catch (e) {
|
||||
init();
|
||||
}
|
||||
}
|
||||
return {
|
||||
elRef,
|
||||
...toRefs(state),
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
93
src/components/Container/src/ScrollContainer.vue
Normal file
93
src/components/Container/src/ScrollContainer.vue
Normal file
@@ -0,0 +1,93 @@
|
||||
<template>
|
||||
<Scrollbar ref="scrollbarRef" class="scroll-container" v-bind="$attrs">
|
||||
<slot></slot>
|
||||
</Scrollbar>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, unref, nextTick } from 'vue';
|
||||
import { Scrollbar, ScrollbarType } from '@/components/Scrollbar';
|
||||
import { useScrollTo } from '@/hooks/event/useScrollTo';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ScrollContainer',
|
||||
components: { Scrollbar },
|
||||
setup() {
|
||||
const scrollbarRef = ref<Nullable<ScrollbarType>>(null);
|
||||
|
||||
/**
|
||||
* Scroll to the specified position
|
||||
*/
|
||||
function scrollTo(to: number, duration = 500) {
|
||||
const scrollbar = unref(scrollbarRef);
|
||||
if (!scrollbar) {
|
||||
return;
|
||||
}
|
||||
nextTick(() => {
|
||||
const wrap = unref(scrollbar.wrap);
|
||||
if (!wrap) {
|
||||
return;
|
||||
}
|
||||
const { start } = useScrollTo({
|
||||
el: wrap,
|
||||
to,
|
||||
duration,
|
||||
});
|
||||
start();
|
||||
});
|
||||
}
|
||||
|
||||
function getScrollWrap() {
|
||||
const scrollbar = unref(scrollbarRef);
|
||||
if (!scrollbar) {
|
||||
return null;
|
||||
}
|
||||
return scrollbar.wrap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scroll to the bottom
|
||||
*/
|
||||
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>
|
||||
<style lang="less">
|
||||
.scroll-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.scrollbar__wrap {
|
||||
margin-bottom: 18px !important;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.scrollbar__view {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
118
src/components/Container/src/collapse/CollapseContainer.vue
Normal file
118
src/components/Container/src/collapse/CollapseContainer.vue
Normal file
@@ -0,0 +1,118 @@
|
||||
<script lang="tsx">
|
||||
import { ref, unref, defineComponent, type PropType, type ExtractPropTypes } from 'vue';
|
||||
import { isNil } from 'lodash-es';
|
||||
import { Skeleton } from 'ant-design-vue';
|
||||
import { CollapseTransition } from '@/components/Transition';
|
||||
import CollapseHeader from './CollapseHeader.vue';
|
||||
import { triggerWindowResize } from '@/utils/event';
|
||||
import { useTimeoutFn } from '@/hooks/core/useTimeout';
|
||||
import { useDesign } from '@/hooks/web/useDesign';
|
||||
|
||||
const collapseContainerProps = {
|
||||
title: { type: String, default: '' },
|
||||
loading: { type: Boolean },
|
||||
/**
|
||||
* Can it be expanded
|
||||
*/
|
||||
canExpan: { type: Boolean, default: true },
|
||||
/**
|
||||
* Warm reminder on the right side of the title
|
||||
*/
|
||||
helpMessage: {
|
||||
type: [Array, String] as PropType<string[] | string>,
|
||||
default: '',
|
||||
},
|
||||
/**
|
||||
* Whether to trigger window.resize when expanding and contracting,
|
||||
* Can adapt to tables and forms, when the form shrinks, the form triggers resize to adapt to the height
|
||||
*/
|
||||
triggerWindowResize: { type: Boolean },
|
||||
/**
|
||||
* Delayed loading time
|
||||
*/
|
||||
lazyTime: { type: Number, default: 0 },
|
||||
};
|
||||
|
||||
export type CollapseContainerProps = ExtractPropTypes<typeof collapseContainerProps>;
|
||||
|
||||
export default defineComponent({
|
||||
name: 'CollapseContainer',
|
||||
|
||||
props: collapseContainerProps,
|
||||
|
||||
setup(props, { expose, slots }) {
|
||||
const { prefixCls } = useDesign('collapse-container');
|
||||
|
||||
const show = ref(true);
|
||||
|
||||
const handleExpand = (val: boolean) => {
|
||||
show.value = isNil(val) ? !show.value : val;
|
||||
if (props.triggerWindowResize) {
|
||||
// 200 milliseconds here is because the expansion has animation,
|
||||
useTimeoutFn(triggerWindowResize, 200);
|
||||
}
|
||||
};
|
||||
|
||||
expose({ handleExpand });
|
||||
|
||||
return () => (
|
||||
<div class={unref(prefixCls)}>
|
||||
<CollapseHeader
|
||||
{...props}
|
||||
prefixCls={unref(prefixCls)}
|
||||
onExpand={handleExpand}
|
||||
show={show.value}
|
||||
v-slots={{
|
||||
title: slots.title,
|
||||
action: slots.action,
|
||||
}}
|
||||
/>
|
||||
|
||||
<div class="p-2">
|
||||
<CollapseTransition enable={props.canExpan}>
|
||||
{props.loading ? (
|
||||
<Skeleton active={props.loading} />
|
||||
) : (
|
||||
<div class={`${prefixCls}__body`} v-show={show.value}>
|
||||
{slots.default?.()}
|
||||
</div>
|
||||
)}
|
||||
</CollapseTransition>
|
||||
</div>
|
||||
|
||||
{slots.footer && <div class={`${prefixCls}__footer`}>{slots.footer()}</div>}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
@prefix-cls: ~'@{namespace}-collapse-container';
|
||||
|
||||
.@{prefix-cls} {
|
||||
background-color: @component-background;
|
||||
border-radius: 2px;
|
||||
transition: all 0.3s ease-in-out;
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
height: 32px;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid @border-color-light;
|
||||
}
|
||||
|
||||
&__footer {
|
||||
border-top: 1px solid @border-color-light;
|
||||
}
|
||||
|
||||
&__action {
|
||||
display: flex;
|
||||
text-align: right;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
41
src/components/Container/src/collapse/CollapseHeader.vue
Normal file
41
src/components/Container/src/collapse/CollapseHeader.vue
Normal file
@@ -0,0 +1,41 @@
|
||||
<script lang="tsx">
|
||||
import { defineComponent, computed, unref, type ExtractPropTypes } from 'vue';
|
||||
import { useDesign } from '@/hooks/web/useDesign';
|
||||
import { BasicArrow, BasicTitle } from '@/components/Basic';
|
||||
|
||||
const collapseHeaderProps = {
|
||||
prefixCls: String,
|
||||
title: String,
|
||||
show: Boolean,
|
||||
canExpan: Boolean,
|
||||
helpMessage: {
|
||||
type: [Array, String] as PropType<string[] | string>,
|
||||
default: '',
|
||||
},
|
||||
};
|
||||
|
||||
export type CollapseHeaderProps = ExtractPropTypes<typeof collapseHeaderProps>;
|
||||
|
||||
export default defineComponent({
|
||||
name: 'CollapseHeader',
|
||||
inheritAttrs: false,
|
||||
props: collapseHeaderProps,
|
||||
emits: ['expand'],
|
||||
setup(props, { slots, attrs, emit }) {
|
||||
const { prefixCls } = useDesign('collapse-container');
|
||||
const _prefixCls = computed(() => props.prefixCls || unref(prefixCls));
|
||||
return () => (
|
||||
<div class={[`${unref(_prefixCls)}__header px-2 py-5`, attrs.class]}>
|
||||
<BasicTitle helpMessage={props.helpMessage} normal>
|
||||
{slots.title?.() || props.title}
|
||||
</BasicTitle>
|
||||
<div class={`${unref(_prefixCls)}__action`}>
|
||||
{slots.action
|
||||
? slots.action({ expand: props.show, onClick: () => emit('expand') })
|
||||
: props.canExpan && <BasicArrow up expand={props.show} onClick={() => emit('expand')} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
</script>
|
||||
17
src/components/Container/src/typing.ts
Normal file
17
src/components/Container/src/typing.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
export type ScrollType = 'default' | 'main';
|
||||
|
||||
export interface CollapseContainerOptions {
|
||||
canExpand?: boolean;
|
||||
title?: string;
|
||||
helpMessage?: Array<any> | string;
|
||||
}
|
||||
export interface ScrollContainerOptions {
|
||||
enableScroll?: boolean;
|
||||
type?: ScrollType;
|
||||
}
|
||||
|
||||
export type ScrollActionType = RefType<{
|
||||
scrollBottom: () => void;
|
||||
getScrollWrap: () => Nullable<HTMLElement>;
|
||||
scrollTo: (top: number) => void;
|
||||
}>;
|
||||
3
src/components/ContextMenu/index.ts
Normal file
3
src/components/ContextMenu/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { createContextMenu, destroyContextMenu } from './src/createContextMenu';
|
||||
|
||||
export * from './src/typing';
|
||||
208
src/components/ContextMenu/src/ContextMenu.vue
Normal file
208
src/components/ContextMenu/src/ContextMenu.vue
Normal file
@@ -0,0 +1,208 @@
|
||||
<script lang="tsx">
|
||||
import type { ContextMenuItem, ItemContentProps, Axis } from './typing';
|
||||
import type { FunctionalComponent, CSSProperties, PropType } from 'vue';
|
||||
import { defineComponent, nextTick, onMounted, computed, ref, unref, onUnmounted } from 'vue';
|
||||
import Icon from '@/components/Icon';
|
||||
import { Menu, Divider } from 'ant-design-vue';
|
||||
|
||||
const prefixCls = 'context-menu';
|
||||
|
||||
const props = {
|
||||
width: { type: Number, default: 120 },
|
||||
customEvent: { type: Object as PropType<Event>, default: null },
|
||||
styles: { type: Object as PropType<CSSProperties> },
|
||||
showIcon: { type: Boolean, default: true },
|
||||
axis: {
|
||||
// The position of the right mouse button click
|
||||
type: Object as PropType<Axis>,
|
||||
default() {
|
||||
return { x: 0, y: 0 };
|
||||
},
|
||||
},
|
||||
items: {
|
||||
// The most important list, if not, will not be displayed
|
||||
type: Array as PropType<ContextMenuItem[]>,
|
||||
default() {
|
||||
return [];
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const ItemContent: FunctionalComponent<ItemContentProps> = props => {
|
||||
const { item } = props;
|
||||
return (
|
||||
<span style="display: inline-block; width: 100%; " class="px-4" onClick={props.handler.bind(null, item)}>
|
||||
{props.showIcon && item.icon && <Icon class="mr-2" icon={item.icon} />}
|
||||
<span>{item.label}</span>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ContextMenu',
|
||||
props,
|
||||
setup(props) {
|
||||
const wrapRef = ref(null);
|
||||
const showRef = ref(false);
|
||||
|
||||
const getStyle = computed((): CSSProperties => {
|
||||
const { axis, items, styles, width } = props;
|
||||
const { x, y } = axis || { x: 0, y: 0 };
|
||||
const menuHeight = (items || []).length * 40;
|
||||
const menuWidth = width;
|
||||
const body = document.body;
|
||||
|
||||
const left = body.clientWidth < x + menuWidth ? x - menuWidth : x;
|
||||
const top = body.clientHeight < y + menuHeight ? y - menuHeight : y;
|
||||
return {
|
||||
...styles,
|
||||
position: 'absolute',
|
||||
width: `${width}px`,
|
||||
left: `${left + 1}px`,
|
||||
top: `${top + 1}px`,
|
||||
zIndex: 9999,
|
||||
};
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => (showRef.value = true));
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
const el = unref(wrapRef);
|
||||
el && document.body.removeChild(el);
|
||||
});
|
||||
|
||||
function handleAction(item: ContextMenuItem, e: MouseEvent) {
|
||||
const { handler, disabled } = item;
|
||||
if (disabled) {
|
||||
return;
|
||||
}
|
||||
showRef.value = false;
|
||||
e?.stopPropagation();
|
||||
e?.preventDefault();
|
||||
handler?.();
|
||||
}
|
||||
|
||||
function renderMenuItem(items: ContextMenuItem[]) {
|
||||
const visibleItems = items.filter(item => !item.hidden);
|
||||
return visibleItems.map(item => {
|
||||
const { disabled, label, children, divider = false } = item;
|
||||
|
||||
const contentProps = {
|
||||
item,
|
||||
handler: handleAction,
|
||||
showIcon: props.showIcon,
|
||||
};
|
||||
|
||||
if (!children || children.length === 0) {
|
||||
return (
|
||||
<>
|
||||
<Menu.Item disabled={disabled} class={`${prefixCls}__item`} key={label}>
|
||||
<ItemContent {...contentProps} />
|
||||
</Menu.Item>
|
||||
{divider ? <Divider key={`d-${label}`} /> : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
if (!unref(showRef)) return null;
|
||||
|
||||
return (
|
||||
<Menu.SubMenu key={label} disabled={disabled} popupClassName={`${prefixCls}__popup`}>
|
||||
{{
|
||||
title: () => <ItemContent {...contentProps} />,
|
||||
default: () => renderMenuItem(children),
|
||||
}}
|
||||
</Menu.SubMenu>
|
||||
);
|
||||
});
|
||||
}
|
||||
return () => {
|
||||
if (!unref(showRef)) {
|
||||
return null;
|
||||
}
|
||||
const { items } = props;
|
||||
return (
|
||||
<div class={prefixCls}>
|
||||
<Menu inlineIndent={12} mode="vertical" ref={wrapRef} style={unref(getStyle)} class="context-menu-main">
|
||||
{renderMenuItem(items)}
|
||||
</Menu>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="less">
|
||||
@default-height: 30px !important;
|
||||
|
||||
@small-height: 30px !important;
|
||||
|
||||
@large-height: 30px !important;
|
||||
|
||||
.item-style() {
|
||||
li {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
height: @default-height;
|
||||
margin: 0 !important;
|
||||
line-height: @default-height;
|
||||
|
||||
span {
|
||||
line-height: @default-height;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
> div {
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
&:not(.ant-menu-item-disabled):hover {
|
||||
color: @text-color-base;
|
||||
background-color: @item-hover-bg;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.context-menu {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1001;
|
||||
display: block;
|
||||
width: 120px;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
background-color: @component-background;
|
||||
border: 1px solid rgb(0 0 0 / 8%);
|
||||
border-radius: 0.25rem;
|
||||
box-shadow: 0 2px 2px 0 rgb(0 0 0 / 14%), 0 3px 1px -2px rgb(0 0 0 / 10%), 0 1px 5px 0 rgb(0 0 0 / 6%);
|
||||
background-clip: padding-box;
|
||||
user-select: none;
|
||||
|
||||
&__item {
|
||||
margin: 0 !important;
|
||||
}
|
||||
.item-style();
|
||||
|
||||
.ant-divider {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&__popup {
|
||||
.ant-divider {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.item-style();
|
||||
}
|
||||
|
||||
.ant-menu-submenu-title,
|
||||
.ant-menu-item {
|
||||
padding: 0 !important;
|
||||
}
|
||||
.context-menu-main {
|
||||
box-shadow: 0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 9px 28px 8px rgba(0, 0, 0, 0.05) !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
75
src/components/ContextMenu/src/createContextMenu.ts
Normal file
75
src/components/ContextMenu/src/createContextMenu.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import contextMenuVue from './ContextMenu.vue';
|
||||
import { isClient } from '@/utils/is';
|
||||
import { CreateContextOptions, ContextMenuProps } from './typing';
|
||||
import { createVNode, render } from 'vue';
|
||||
|
||||
const menuManager: {
|
||||
domList: Element[];
|
||||
resolve: Fn;
|
||||
} = {
|
||||
domList: [],
|
||||
resolve: () => {},
|
||||
};
|
||||
|
||||
export const createContextMenu = function (options: CreateContextOptions) {
|
||||
const { event } = options || {};
|
||||
|
||||
event && event?.preventDefault();
|
||||
|
||||
if (!isClient) {
|
||||
return;
|
||||
}
|
||||
return new Promise(resolve => {
|
||||
const body = document.body;
|
||||
|
||||
const container = document.createElement('div');
|
||||
const propsData: Partial<ContextMenuProps> = {};
|
||||
if (options.styles) {
|
||||
propsData.styles = options.styles;
|
||||
}
|
||||
|
||||
if (options.items) {
|
||||
propsData.items = options.items;
|
||||
}
|
||||
|
||||
if (options.event) {
|
||||
propsData.customEvent = event;
|
||||
propsData.axis = { x: event.clientX, y: event.clientY };
|
||||
}
|
||||
|
||||
const vm = createVNode(contextMenuVue, propsData);
|
||||
render(vm, container);
|
||||
|
||||
const handleClick = function () {
|
||||
menuManager.resolve('');
|
||||
};
|
||||
|
||||
menuManager.domList.push(container);
|
||||
|
||||
const remove = function () {
|
||||
menuManager.domList.forEach((dom: Element) => {
|
||||
try {
|
||||
dom && body.removeChild(dom);
|
||||
} catch (error) {}
|
||||
});
|
||||
body.removeEventListener('click', handleClick);
|
||||
body.removeEventListener('scroll', handleClick);
|
||||
};
|
||||
|
||||
menuManager.resolve = function (arg) {
|
||||
remove();
|
||||
resolve(arg);
|
||||
};
|
||||
remove();
|
||||
body.appendChild(container);
|
||||
body.addEventListener('click', handleClick);
|
||||
body.addEventListener('scroll', handleClick);
|
||||
});
|
||||
};
|
||||
|
||||
export const destroyContextMenu = function () {
|
||||
if (menuManager) {
|
||||
menuManager.resolve('');
|
||||
menuManager.domList = [];
|
||||
}
|
||||
};
|
||||
36
src/components/ContextMenu/src/typing.ts
Normal file
36
src/components/ContextMenu/src/typing.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
export interface Axis {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export interface ContextMenuItem {
|
||||
label: string;
|
||||
icon?: string;
|
||||
hidden?: boolean;
|
||||
disabled?: boolean;
|
||||
handler?: Fn;
|
||||
divider?: boolean;
|
||||
children?: ContextMenuItem[];
|
||||
}
|
||||
export interface CreateContextOptions {
|
||||
event: MouseEvent;
|
||||
icon?: string;
|
||||
styles?: any;
|
||||
items?: ContextMenuItem[];
|
||||
}
|
||||
|
||||
export interface ContextMenuProps {
|
||||
event?: MouseEvent;
|
||||
styles?: any;
|
||||
items: ContextMenuItem[];
|
||||
customEvent?: MouseEvent;
|
||||
axis?: Axis;
|
||||
width?: number;
|
||||
showIcon?: boolean;
|
||||
}
|
||||
|
||||
export interface ItemContentProps {
|
||||
showIcon: boolean | undefined;
|
||||
item: ContextMenuItem;
|
||||
handler: Fn;
|
||||
}
|
||||
6
src/components/CountDown/index.ts
Normal file
6
src/components/CountDown/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { withInstall } from '@/utils';
|
||||
import countButton from './src/CountButton.vue';
|
||||
import countdownInput from './src/CountdownInput.vue';
|
||||
|
||||
export const CountdownInput = withInstall(countdownInput);
|
||||
export const CountButton = withInstall(countButton);
|
||||
60
src/components/CountDown/src/CountButton.vue
Normal file
60
src/components/CountDown/src/CountButton.vue
Normal file
@@ -0,0 +1,60 @@
|
||||
<template>
|
||||
<Button v-bind="$attrs" :disabled="isStart" @click="handleStart" :loading="loading">
|
||||
{{ getButtonText }}
|
||||
</Button>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, watchEffect, computed, unref } from 'vue';
|
||||
import { Button } from 'ant-design-vue';
|
||||
import { useCountdown } from './useCountdown';
|
||||
import { isFunction } from '@/utils/is';
|
||||
import { useI18n } from '@/hooks/web/useI18n';
|
||||
|
||||
const props = {
|
||||
value: { type: [Object, Number, String, Array] },
|
||||
count: { type: Number, default: 60 },
|
||||
beforeStartFunc: {
|
||||
type: Function as PropType<() => Promise<boolean>>,
|
||||
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 };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
54
src/components/CountDown/src/CountdownInput.vue
Normal file
54
src/components/CountDown/src/CountdownInput.vue
Normal file
@@ -0,0 +1,54 @@
|
||||
<template>
|
||||
<a-input v-bind="$attrs" :class="prefixCls" :size="size" :value="state">
|
||||
<template #addonAfter>
|
||||
<CountButton :size="size" :count="count" :value="state" :beforeStartFunc="sendCodeApi" />
|
||||
</template>
|
||||
<template #[item]="data" v-for="item in Object.keys($slots).filter(k => k !== 'addonAfter')">
|
||||
<slot :name="item" v-bind="data || {}"></slot>
|
||||
</template>
|
||||
</a-input>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType } from 'vue';
|
||||
import CountButton from './CountButton.vue';
|
||||
import { useDesign } from '@/hooks/web/useDesign';
|
||||
import { useRuleFormItem } from '@/hooks/component/useFormItem';
|
||||
|
||||
const props = {
|
||||
value: { type: String },
|
||||
size: { type: String, validator: v => ['default', 'large', 'small'].includes(v) },
|
||||
count: { type: Number, default: 60 },
|
||||
sendCodeApi: {
|
||||
type: Function as PropType<() => Promise<boolean>>,
|
||||
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 };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="less">
|
||||
@prefix-cls: ~'@{namespace}-countdown-input';
|
||||
|
||||
.@{prefix-cls} {
|
||||
.ant-input-group-addon {
|
||||
padding-right: 0;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
|
||||
button {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
51
src/components/CountDown/src/useCountdown.ts
Normal file
51
src/components/CountDown/src/useCountdown.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { ref, unref } from 'vue';
|
||||
import { tryOnUnmounted } from '@vueuse/core';
|
||||
|
||||
export function useCountdown(count: number) {
|
||||
const currentCount = ref(count);
|
||||
|
||||
const isStart = ref(false);
|
||||
|
||||
let timerId: ReturnType<typeof setInterval> | null;
|
||||
|
||||
function clear() {
|
||||
timerId && window.clearInterval(timerId);
|
||||
}
|
||||
|
||||
function stop() {
|
||||
isStart.value = false;
|
||||
clear();
|
||||
timerId = null;
|
||||
}
|
||||
|
||||
function start() {
|
||||
if (unref(isStart) || !!timerId) {
|
||||
return;
|
||||
}
|
||||
isStart.value = true;
|
||||
timerId = setInterval(() => {
|
||||
if (unref(currentCount) === 1) {
|
||||
stop();
|
||||
currentCount.value = count;
|
||||
} else {
|
||||
currentCount.value -= 1;
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function reset() {
|
||||
currentCount.value = count;
|
||||
stop();
|
||||
}
|
||||
|
||||
function restart() {
|
||||
reset();
|
||||
start();
|
||||
}
|
||||
|
||||
tryOnUnmounted(() => {
|
||||
reset();
|
||||
});
|
||||
|
||||
return { start, reset, restart, clear, stop, currentCount, isStart };
|
||||
}
|
||||
4
src/components/CountTo/index.ts
Normal file
4
src/components/CountTo/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { withInstall } from '@/utils';
|
||||
import countTo from './src/CountTo.vue';
|
||||
|
||||
export const CountTo = withInstall(countTo);
|
||||
110
src/components/CountTo/src/CountTo.vue
Normal file
110
src/components/CountTo/src/CountTo.vue
Normal file
@@ -0,0 +1,110 @@
|
||||
<template>
|
||||
<span :style="{ color }">
|
||||
{{ value }}
|
||||
</span>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, computed, watchEffect, unref, onMounted, watch } from 'vue';
|
||||
import { useTransition, TransitionPresets } from '@vueuse/core';
|
||||
import { isNumber } from '@/utils/is';
|
||||
|
||||
const props = {
|
||||
startVal: { type: Number, default: 0 },
|
||||
endVal: { type: Number, default: 2021 },
|
||||
duration: { type: Number, default: 1500 },
|
||||
autoplay: { type: Boolean, default: true },
|
||||
decimals: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
validator(value: number) {
|
||||
return value >= 0;
|
||||
},
|
||||
},
|
||||
prefix: { type: String, default: '' },
|
||||
suffix: { type: String, default: '' },
|
||||
separator: { type: String, default: ',' },
|
||||
decimal: { type: String, default: '.' },
|
||||
/**
|
||||
* font color
|
||||
*/
|
||||
color: { type: String },
|
||||
/**
|
||||
* Turn on digital animation
|
||||
*/
|
||||
useEasing: { type: Boolean, default: true },
|
||||
/**
|
||||
* Digital animation
|
||||
*/
|
||||
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 };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
7
src/components/Cropper/index.ts
Normal file
7
src/components/Cropper/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { withInstall } from '@/utils';
|
||||
import cropperImage from './src/Cropper.vue';
|
||||
import avatarCropper from './src/CropperAvatar.vue';
|
||||
|
||||
export * from './src/typing';
|
||||
export const CropperImage = withInstall(cropperImage);
|
||||
export const CropperAvatar = withInstall(avatarCropper);
|
||||
221
src/components/Cropper/src/CopperModal.vue
Normal file
221
src/components/Cropper/src/CopperModal.vue
Normal file
@@ -0,0 +1,221 @@
|
||||
<template>
|
||||
<BasicModal
|
||||
v-bind="$attrs"
|
||||
@register="register"
|
||||
:title="t('component.cropper.modalTitle')"
|
||||
width="800px"
|
||||
:canFullscreen="false"
|
||||
@ok="handleOk"
|
||||
:okText="t('component.cropper.okText')">
|
||||
<div :class="prefixCls">
|
||||
<div :class="`${prefixCls}-left`">
|
||||
<div :class="`${prefixCls}-cropper`">
|
||||
<CropperImage v-if="src" :src="src" height="300px" :circled="circled" @cropend="handleCropend" @ready="handleReady" />
|
||||
</div>
|
||||
<div :class="`${prefixCls}-toolbar`">
|
||||
<Upload :fileList="[]" accept="image/*" :beforeUpload="handleBeforeUpload">
|
||||
<Tooltip :title="t('component.cropper.selectImage')" placement="bottom">
|
||||
<a-button size="small" preIcon="ant-design:upload-outlined" type="primary" />
|
||||
</Tooltip>
|
||||
</Upload>
|
||||
<Space>
|
||||
<Tooltip :title="t('component.cropper.btn_reset')" placement="bottom">
|
||||
<a-button type="primary" preIcon="ant-design:reload-outlined" size="small" :disabled="!src" @click="handlerToolbar('reset')" />
|
||||
</Tooltip>
|
||||
<Tooltip :title="t('component.cropper.btn_rotate_left')" placement="bottom">
|
||||
<a-button type="primary" preIcon="ant-design:rotate-left-outlined" size="small" :disabled="!src" @click="handlerToolbar('rotate', -45)" />
|
||||
</Tooltip>
|
||||
<Tooltip :title="t('component.cropper.btn_rotate_right')" placement="bottom">
|
||||
<a-button type="primary" preIcon="ant-design:rotate-right-outlined" size="small" :disabled="!src" @click="handlerToolbar('rotate', 45)" />
|
||||
</Tooltip>
|
||||
<Tooltip :title="t('component.cropper.btn_scale_x')" placement="bottom">
|
||||
<a-button type="primary" preIcon="vaadin:arrows-long-h" size="small" :disabled="!src" @click="handlerToolbar('scaleX')" />
|
||||
</Tooltip>
|
||||
<Tooltip :title="t('component.cropper.btn_scale_y')" placement="bottom">
|
||||
<a-button type="primary" preIcon="vaadin:arrows-long-v" size="small" :disabled="!src" @click="handlerToolbar('scaleY')" />
|
||||
</Tooltip>
|
||||
<Tooltip :title="t('component.cropper.btn_zoom_in')" placement="bottom">
|
||||
<a-button type="primary" preIcon="ant-design:zoom-in-outlined" size="small" :disabled="!src" @click="handlerToolbar('zoom', 0.1)" />
|
||||
</Tooltip>
|
||||
<Tooltip :title="t('component.cropper.btn_zoom_out')" placement="bottom">
|
||||
<a-button type="primary" preIcon="ant-design:zoom-out-outlined" size="small" :disabled="!src" @click="handlerToolbar('zoom', -0.1)" />
|
||||
</Tooltip>
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="`${prefixCls}-right`">
|
||||
<div :class="`${prefixCls}-preview`">
|
||||
<img :src="previewSource" v-if="previewSource" :alt="t('component.cropper.preview')" />
|
||||
</div>
|
||||
<template v-if="previewSource">
|
||||
<div :class="`${prefixCls}-group`">
|
||||
<Avatar :src="previewSource" size="large" />
|
||||
<Avatar :src="previewSource" :size="48" />
|
||||
<Avatar :src="previewSource" :size="64" />
|
||||
<Avatar :src="previewSource" :size="80" />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</BasicModal>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import type { CropendResult, Cropper } from './typing';
|
||||
|
||||
import { defineComponent, ref } from 'vue';
|
||||
import CropperImage from './Cropper.vue';
|
||||
import { Space, Upload, Avatar, Tooltip } from 'ant-design-vue';
|
||||
import { useDesign } from '@/hooks/web/useDesign';
|
||||
import { BasicModal, useModalInner } from '@/components/Modal';
|
||||
import { dataURLtoBlob } from '@/utils/file/base64Conver';
|
||||
import { isFunction } from '@/utils/is';
|
||||
import { useI18n } from '@/hooks/web/useI18n';
|
||||
|
||||
type apiFunParams = { file: Blob; name: string; filename: string };
|
||||
|
||||
const props = {
|
||||
circled: { type: Boolean, default: true },
|
||||
uploadApi: {
|
||||
type: Function as PropType<(params: apiFunParams) => Promise<any>>,
|
||||
},
|
||||
src: { type: String },
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
name: 'CropperModal',
|
||||
components: { BasicModal, Space, CropperImage, Upload, Avatar, Tooltip },
|
||||
props,
|
||||
emits: ['uploadSuccess', 'register'],
|
||||
setup(props, { emit }) {
|
||||
let filename = '';
|
||||
const src = ref(props.src || '');
|
||||
const previewSource = ref('');
|
||||
const cropper = ref<Cropper>();
|
||||
let scaleX = 1;
|
||||
let scaleY = 1;
|
||||
|
||||
const { prefixCls } = useDesign('cropper-am');
|
||||
const [register, { closeModal, setModalProps }] = useModalInner();
|
||||
const { t } = useI18n();
|
||||
|
||||
// Block upload
|
||||
function handleBeforeUpload(file: File) {
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(file);
|
||||
src.value = '';
|
||||
previewSource.value = '';
|
||||
reader.onload = function (e) {
|
||||
src.value = (e.target?.result as string) ?? '';
|
||||
filename = file.name;
|
||||
};
|
||||
return false;
|
||||
}
|
||||
|
||||
function handleCropend({ imgBase64 }: CropendResult) {
|
||||
previewSource.value = imgBase64;
|
||||
}
|
||||
|
||||
function handleReady(cropperInstance: Cropper) {
|
||||
cropper.value = cropperInstance;
|
||||
}
|
||||
|
||||
function handlerToolbar(event: string, arg?: number) {
|
||||
if (event === 'scaleX') {
|
||||
scaleX = arg = scaleX === -1 ? 1 : -1;
|
||||
}
|
||||
if (event === 'scaleY') {
|
||||
scaleY = arg = scaleY === -1 ? 1 : -1;
|
||||
}
|
||||
cropper?.value?.[event]?.(arg);
|
||||
}
|
||||
|
||||
async function handleOk() {
|
||||
const uploadApi = props.uploadApi;
|
||||
if (uploadApi && isFunction(uploadApi)) {
|
||||
const blob = dataURLtoBlob(previewSource.value);
|
||||
try {
|
||||
setModalProps({ confirmLoading: true });
|
||||
const result = await uploadApi({ name: 'file', file: blob, filename });
|
||||
emit('uploadSuccess', { source: previewSource.value, data: result.url });
|
||||
closeModal();
|
||||
} finally {
|
||||
setModalProps({ confirmLoading: false });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
t,
|
||||
prefixCls,
|
||||
src,
|
||||
register,
|
||||
previewSource,
|
||||
handleBeforeUpload,
|
||||
handleCropend,
|
||||
handleReady,
|
||||
handlerToolbar,
|
||||
handleOk,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
@prefix-cls: ~'@{namespace}-cropper-am';
|
||||
|
||||
.@{prefix-cls} {
|
||||
display: flex;
|
||||
|
||||
&-left,
|
||||
&-right {
|
||||
height: 340px;
|
||||
}
|
||||
|
||||
&-left {
|
||||
width: 55%;
|
||||
}
|
||||
|
||||
&-right {
|
||||
width: 45%;
|
||||
}
|
||||
|
||||
&-cropper {
|
||||
height: 300px;
|
||||
background: #eee;
|
||||
background-image: linear-gradient(45deg, rgb(0 0 0 / 25%) 25%, transparent 0, transparent 75%, rgb(0 0 0 / 25%) 0),
|
||||
linear-gradient(45deg, rgb(0 0 0 / 25%) 25%, transparent 0, transparent 75%, rgb(0 0 0 / 25%) 0);
|
||||
background-position: 0 0, 12px 12px;
|
||||
background-size: 24px 24px;
|
||||
}
|
||||
|
||||
&-toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
&-preview {
|
||||
width: 220px;
|
||||
height: 220px;
|
||||
margin: 0 auto;
|
||||
overflow: hidden;
|
||||
border: 1px solid @border-color-base;
|
||||
border-radius: 50%;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&-group {
|
||||
display: flex;
|
||||
padding-top: 8px;
|
||||
margin-top: 8px;
|
||||
border-top: 1px solid @border-color-base;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
181
src/components/Cropper/src/Cropper.vue
Normal file
181
src/components/Cropper/src/Cropper.vue
Normal file
@@ -0,0 +1,181 @@
|
||||
<template>
|
||||
<div :class="getClass" :style="getWrapperStyle">
|
||||
<img v-show="isReady" ref="imgElRef" :src="src" :alt="alt" :crossorigin="crossorigin" :style="getImageStyle" />
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import type { CSSProperties } from 'vue';
|
||||
import { defineComponent, onMounted, ref, unref, computed, onUnmounted } from 'vue';
|
||||
import Cropper from 'cropperjs';
|
||||
import 'cropperjs/dist/cropper.css';
|
||||
import { useDesign } from '@/hooks/web/useDesign';
|
||||
import { useDebounceFn } from '@vueuse/shared';
|
||||
|
||||
type Options = Cropper.Options;
|
||||
|
||||
const defaultOptions: Options = {
|
||||
aspectRatio: 1,
|
||||
zoomable: true,
|
||||
zoomOnTouch: true,
|
||||
zoomOnWheel: true,
|
||||
cropBoxMovable: true,
|
||||
cropBoxResizable: true,
|
||||
toggleDragModeOnDblclick: true,
|
||||
autoCrop: true,
|
||||
background: true,
|
||||
highlight: true,
|
||||
center: true,
|
||||
responsive: true,
|
||||
restore: true,
|
||||
checkCrossOrigin: true,
|
||||
checkOrientation: true,
|
||||
scalable: true,
|
||||
modal: true,
|
||||
guides: true,
|
||||
movable: true,
|
||||
rotatable: true,
|
||||
};
|
||||
|
||||
const props = {
|
||||
src: { type: String, required: true },
|
||||
alt: { type: String },
|
||||
circled: { type: Boolean, default: false },
|
||||
realTimePreview: { type: Boolean, default: true },
|
||||
height: { type: [String, Number], default: '360px' },
|
||||
crossorigin: {
|
||||
type: String as PropType<'' | 'anonymous' | 'use-credentials' | undefined>,
|
||||
default: undefined,
|
||||
},
|
||||
imageStyle: { type: Object as PropType<CSSProperties>, 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 };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="less">
|
||||
@prefix-cls: ~'@{namespace}-cropper-image';
|
||||
|
||||
.@{prefix-cls} {
|
||||
&--circled {
|
||||
.cropper-view-box,
|
||||
.cropper-face {
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
135
src/components/Cropper/src/CropperAvatar.vue
Normal file
135
src/components/Cropper/src/CropperAvatar.vue
Normal file
@@ -0,0 +1,135 @@
|
||||
<template>
|
||||
<div :class="getClass" :style="getStyle">
|
||||
<div :class="`${prefixCls}-image-wrapper`" :style="getImageWrapperStyle" @click="openModal">
|
||||
<div :class="`${prefixCls}-image-mask`" :style="getImageWrapperStyle">
|
||||
<Icon icon="ant-design:cloud-upload-outlined" :size="getIconWidth" :style="getImageWrapperStyle" color="#d6d6d6" />
|
||||
</div>
|
||||
<img :src="sourceValue" v-if="sourceValue" alt="avatar" />
|
||||
</div>
|
||||
<a-button :class="`${prefixCls}-upload-btn`" @click="openModal" v-if="showBtn" v-bind="btnProps">
|
||||
{{ btnText ? btnText : t('component.cropper.selectImage') }}
|
||||
</a-button>
|
||||
|
||||
<CopperModal @register="register" @upload-success="handleUploadSuccess" :uploadApi="uploadApi" :src="sourceValue" />
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed, CSSProperties, unref, ref, watchEffect, watch, PropType } from 'vue';
|
||||
import CopperModal from './CopperModal.vue';
|
||||
import { useDesign } from '@/hooks/web/useDesign';
|
||||
import { useModal } from '@/components/Modal';
|
||||
import { useMessage } from '@/hooks/web/useMessage';
|
||||
import { useI18n } from '@/hooks/web/useI18n';
|
||||
import type { ButtonProps } from '@/components/Button';
|
||||
import Icon from '@/components/Icon';
|
||||
|
||||
const props = {
|
||||
width: { type: [String, Number], default: '200px' },
|
||||
value: { type: String },
|
||||
showBtn: { type: Boolean, default: true },
|
||||
btnProps: { type: Object as PropType<ButtonProps> },
|
||||
btnText: { type: String, default: '' },
|
||||
uploadApi: { type: Function as PropType<({ file: Blob, name: string }) => Promise<void>> },
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
name: 'CropperAvatar',
|
||||
components: { CopperModal, 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,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@prefix-cls: ~'@{namespace}-cropper-avatar';
|
||||
|
||||
.@{prefix-cls} {
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
|
||||
&-image-wrapper {
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
background: @component-background;
|
||||
border: 1px solid @border-color-base;
|
||||
border-radius: 50%;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&-image-mask {
|
||||
opacity: 0%;
|
||||
position: absolute;
|
||||
width: inherit;
|
||||
height: inherit;
|
||||
border-radius: inherit;
|
||||
border: inherit;
|
||||
background: rgb(0 0 0 / 40%);
|
||||
cursor: pointer;
|
||||
transition: opacity 0.4s;
|
||||
|
||||
:deep(svg) {
|
||||
margin: auto;
|
||||
}
|
||||
}
|
||||
|
||||
&-image-mask:hover {
|
||||
opacity: 4000%;
|
||||
}
|
||||
|
||||
&-upload-btn {
|
||||
margin: 10px auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
8
src/components/Cropper/src/typing.ts
Normal file
8
src/components/Cropper/src/typing.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import type Cropper from 'cropperjs';
|
||||
|
||||
export interface CropendResult {
|
||||
imgBase64: string;
|
||||
imgInfo: Cropper.Data;
|
||||
}
|
||||
|
||||
export type { Cropper };
|
||||
6
src/components/Description/index.ts
Normal file
6
src/components/Description/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { withInstall } from '@/utils';
|
||||
import description from './src/Description.vue';
|
||||
|
||||
export * from './src/typing';
|
||||
export { useDescription } from './src/useDescription';
|
||||
export const Description = withInstall(description);
|
||||
184
src/components/Description/src/Description.vue
Normal file
184
src/components/Description/src/Description.vue
Normal file
@@ -0,0 +1,184 @@
|
||||
<script lang="tsx">
|
||||
import type { DescriptionProps, DescInstance, DescItem } from './typing';
|
||||
import type { DescriptionsProps } from 'ant-design-vue/es/descriptions/index';
|
||||
import type { CSSProperties } from 'vue';
|
||||
import type { CollapseContainerOptions } from '@/components/Container/index';
|
||||
import { defineComponent, computed, ref, unref, toRefs } from 'vue';
|
||||
import { get } from 'lodash-es';
|
||||
import { Descriptions } from 'ant-design-vue';
|
||||
import { CollapseContainer } from '@/components/Container/index';
|
||||
import { useDesign } from '@/hooks/web/useDesign';
|
||||
import { isFunction } from '@/utils/is';
|
||||
import { getSlot } from '@/utils/helper/tsxHelper';
|
||||
import { useAttrs } from '@/hooks/core/useAttrs';
|
||||
|
||||
const props = {
|
||||
useCollapse: { type: Boolean, default: true },
|
||||
title: { type: String, default: '' },
|
||||
size: {
|
||||
type: String,
|
||||
validator: v => ['small', 'default', 'middle', undefined].includes(v),
|
||||
default: 'small',
|
||||
},
|
||||
bordered: { type: Boolean, default: true },
|
||||
column: {
|
||||
type: [Number, Object] as PropType<number | Recordable>,
|
||||
default: () => {
|
||||
return { xxl: 4, xl: 3, lg: 3, md: 3, sm: 2, xs: 1 };
|
||||
},
|
||||
},
|
||||
collapseOptions: {
|
||||
type: Object as PropType<CollapseContainerOptions>,
|
||||
default: null,
|
||||
},
|
||||
schema: {
|
||||
type: Array as PropType<DescItem[]>,
|
||||
default: () => [],
|
||||
},
|
||||
data: { type: Object },
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Description',
|
||||
props,
|
||||
emits: ['register'],
|
||||
setup(props, { slots, emit }) {
|
||||
const propsRef = ref<Partial<DescriptionProps> | null>(null);
|
||||
|
||||
const { prefixCls } = useDesign('description');
|
||||
const attrs = useAttrs();
|
||||
|
||||
// Custom title component: get title
|
||||
const getMergeProps = computed(() => {
|
||||
return {
|
||||
...props,
|
||||
...(unref(propsRef) as Recordable),
|
||||
} as DescriptionProps;
|
||||
});
|
||||
|
||||
const getProps = computed(() => {
|
||||
const opt = {
|
||||
...unref(getMergeProps),
|
||||
title: undefined,
|
||||
};
|
||||
return opt as DescriptionProps;
|
||||
});
|
||||
|
||||
/**
|
||||
* @description: Whether to setting title
|
||||
*/
|
||||
const useWrapper = computed(() => !!unref(getMergeProps).title);
|
||||
|
||||
/**
|
||||
* @description: Get configuration Collapse
|
||||
*/
|
||||
const getCollapseOptions = computed((): CollapseContainerOptions => {
|
||||
return {
|
||||
// Cannot be expanded by default
|
||||
canExpand: false,
|
||||
...unref(getProps).collapseOptions,
|
||||
};
|
||||
});
|
||||
|
||||
const getDescriptionsProps = computed(() => {
|
||||
return { ...unref(attrs), ...unref(getProps) } as DescriptionsProps;
|
||||
});
|
||||
|
||||
/**
|
||||
* @description:设置desc
|
||||
*/
|
||||
function setDescProps(descProps: Partial<DescriptionProps>): void {
|
||||
// Keep the last setDrawerProps
|
||||
propsRef.value = { ...(unref(propsRef) as Recordable), ...descProps } as Recordable;
|
||||
}
|
||||
|
||||
// Prevent line breaks
|
||||
function renderLabel({ label, labelMinWidth, labelStyle }: DescItem) {
|
||||
if (!labelStyle && !labelMinWidth) {
|
||||
return label;
|
||||
}
|
||||
|
||||
const labelStyles: CSSProperties = {
|
||||
...labelStyle,
|
||||
minWidth: `${labelMinWidth}px `,
|
||||
};
|
||||
return <div style={labelStyles}>{label}</div>;
|
||||
}
|
||||
|
||||
function renderItem() {
|
||||
const { schema, data } = unref(getProps);
|
||||
return unref(schema)
|
||||
.map(item => {
|
||||
const { render, field, span, show, contentMinWidth } = item;
|
||||
|
||||
if (show && isFunction(show) && !show(data)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const getContent = () => {
|
||||
const _data = unref(getProps)?.data;
|
||||
if (!_data) {
|
||||
return null;
|
||||
}
|
||||
const getField = get(_data, field);
|
||||
if (getField && !toRefs(_data).hasOwnProperty(field)) {
|
||||
return isFunction(render) ? render('', _data) : '';
|
||||
}
|
||||
return isFunction(render) ? render(getField, _data) : getField ?? '';
|
||||
};
|
||||
|
||||
const width = contentMinWidth;
|
||||
return (
|
||||
<Descriptions.Item label={renderLabel(item)} key={field} span={span}>
|
||||
{() => {
|
||||
if (!contentMinWidth) {
|
||||
return getContent();
|
||||
}
|
||||
const style: CSSProperties = {
|
||||
minWidth: `${width}px`,
|
||||
};
|
||||
return <div style={style}>{getContent()}</div>;
|
||||
}}
|
||||
</Descriptions.Item>
|
||||
);
|
||||
})
|
||||
.filter(item => !!item);
|
||||
}
|
||||
|
||||
const renderDesc = () => {
|
||||
return (
|
||||
<Descriptions class={`${prefixCls}`} {...(unref(getDescriptionsProps) as any)}>
|
||||
{renderItem()}
|
||||
</Descriptions>
|
||||
);
|
||||
};
|
||||
|
||||
const renderContainer = () => {
|
||||
const content = props.useCollapse ? renderDesc() : <div>{renderDesc()}</div>;
|
||||
// Reduce the dom level
|
||||
if (!props.useCollapse) {
|
||||
return content;
|
||||
}
|
||||
|
||||
const { canExpand, helpMessage } = unref(getCollapseOptions);
|
||||
const { title } = unref(getMergeProps);
|
||||
|
||||
return (
|
||||
<CollapseContainer title={title} canExpan={canExpand} helpMessage={helpMessage}>
|
||||
{{
|
||||
default: () => content,
|
||||
action: () => getSlot(slots, 'action'),
|
||||
}}
|
||||
</CollapseContainer>
|
||||
);
|
||||
};
|
||||
|
||||
const methods: DescInstance = {
|
||||
setDescProps,
|
||||
};
|
||||
|
||||
emit('register', methods);
|
||||
return () => (unref(useWrapper) ? renderContainer() : renderDesc());
|
||||
},
|
||||
});
|
||||
</script>
|
||||
47
src/components/Description/src/typing.ts
Normal file
47
src/components/Description/src/typing.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import type { VNode, CSSProperties } from 'vue';
|
||||
import type { CollapseContainerOptions } from '@/components/Container/index';
|
||||
import type { DescriptionsProps } from 'ant-design-vue/es/descriptions/index';
|
||||
|
||||
export interface DescItem {
|
||||
labelMinWidth?: number;
|
||||
contentMinWidth?: number;
|
||||
labelStyle?: CSSProperties;
|
||||
field: string;
|
||||
label: string | VNode | JSX.Element;
|
||||
// Merge column
|
||||
span?: number;
|
||||
show?: (...arg: any) => boolean;
|
||||
// render
|
||||
render?: (val: any, data: Recordable) => VNode | undefined | JSX.Element | Element | string | number;
|
||||
}
|
||||
|
||||
export interface DescriptionProps extends DescriptionsProps {
|
||||
// Whether to include the collapse component
|
||||
useCollapse?: boolean;
|
||||
/**
|
||||
* item configuration
|
||||
* @type DescItem
|
||||
*/
|
||||
schema: DescItem[];
|
||||
/**
|
||||
* 数据
|
||||
* @type object
|
||||
*/
|
||||
data: Recordable;
|
||||
/**
|
||||
* Built-in CollapseContainer component configuration
|
||||
* @type CollapseContainerOptions
|
||||
*/
|
||||
collapseOptions?: CollapseContainerOptions;
|
||||
}
|
||||
|
||||
export interface DescInstance {
|
||||
setDescProps(descProps: Partial<DescriptionProps>): void;
|
||||
}
|
||||
|
||||
export type Register = (descInstance: DescInstance) => void;
|
||||
|
||||
/**
|
||||
* @description:
|
||||
*/
|
||||
export type UseDescReturnType = [Register, DescInstance];
|
||||
28
src/components/Description/src/useDescription.ts
Normal file
28
src/components/Description/src/useDescription.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import type { DescriptionProps, DescInstance, UseDescReturnType } from './typing';
|
||||
import { ref, getCurrentInstance, unref } from 'vue';
|
||||
import { isProdMode } from '@/utils/env';
|
||||
|
||||
export function useDescription(props?: Partial<DescriptionProps>): UseDescReturnType {
|
||||
if (!getCurrentInstance()) {
|
||||
throw new Error('useDescription() can only be used inside setup() or functional components!');
|
||||
}
|
||||
const desc = ref<Nullable<DescInstance>>(null);
|
||||
const loaded = ref(false);
|
||||
|
||||
function register(instance: DescInstance) {
|
||||
if (unref(loaded) && isProdMode()) {
|
||||
return;
|
||||
}
|
||||
desc.value = instance;
|
||||
props && instance.setDescProps(props);
|
||||
loaded.value = true;
|
||||
}
|
||||
|
||||
const methods: DescInstance = {
|
||||
setDescProps: (descProps: Partial<DescriptionProps>): void => {
|
||||
unref(desc)?.setDescProps(descProps);
|
||||
},
|
||||
};
|
||||
|
||||
return [register, methods];
|
||||
}
|
||||
6
src/components/Drawer/index.ts
Normal file
6
src/components/Drawer/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { withInstall } from '@/utils';
|
||||
import basicDrawer from './src/BasicDrawer.vue';
|
||||
|
||||
export const BasicDrawer = withInstall(basicDrawer);
|
||||
export * from './src/typing';
|
||||
export { useDrawer, useDrawerInner } from './src/useDrawer';
|
||||
259
src/components/Drawer/src/BasicDrawer.vue
Normal file
259
src/components/Drawer/src/BasicDrawer.vue
Normal file
@@ -0,0 +1,259 @@
|
||||
<template>
|
||||
<Drawer :rootClassName="prefixCls" @close="onClose" v-bind="getBindValues">
|
||||
<template #title v-if="!$slots.title">
|
||||
<DrawerHeader :title="getMergeProps.title" :isDetail="isDetail" :showDetailBack="showDetailBack" @close="onClose">
|
||||
<template #titleToolbar>
|
||||
<slot name="titleToolbar"></slot>
|
||||
</template>
|
||||
</DrawerHeader>
|
||||
</template>
|
||||
<template v-else #title>
|
||||
<slot name="title"></slot>
|
||||
</template>
|
||||
<ScrollContainer :style="getScrollContentStyle" v-loading="getLoading" :loading-tip="loadingText || t('common.loadingText')">
|
||||
<slot></slot>
|
||||
</ScrollContainer>
|
||||
<DrawerFooter v-bind="getProps" @close="onClose" @ok="handleOk" :height="getFooterHeight">
|
||||
<template #[item]="data" v-for="item in Object.keys($slots)">
|
||||
<slot :name="item" v-bind="data || {}"></slot>
|
||||
</template>
|
||||
</DrawerFooter>
|
||||
</Drawer>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import type { DrawerInstance, DrawerProps } from './typing';
|
||||
import type { CSSProperties } from 'vue';
|
||||
import { defineComponent, ref, computed, watch, unref, nextTick, getCurrentInstance } from 'vue';
|
||||
import { Drawer } from 'ant-design-vue';
|
||||
import { useI18n } from '@/hooks/web/useI18n';
|
||||
import { isFunction, isNumber } from '@/utils/is';
|
||||
import { deepMerge } from '@/utils';
|
||||
import DrawerFooter from './components/DrawerFooter.vue';
|
||||
import DrawerHeader from './components/DrawerHeader.vue';
|
||||
import { ScrollContainer } from '@/components/Container';
|
||||
import { basicProps } from './props';
|
||||
import { useDesign } from '@/hooks/web/useDesign';
|
||||
import { useAttrs } from '@/hooks/core/useAttrs';
|
||||
|
||||
export default defineComponent({
|
||||
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({ excludeDefaultKeys: false });
|
||||
const propsRef = ref<Partial<Nullable<DrawerProps>>>(null);
|
||||
|
||||
const { t } = useI18n();
|
||||
const { prefixVar, prefixCls } = useDesign('basic-drawer');
|
||||
|
||||
const drawerInstance: DrawerInstance = {
|
||||
setDrawerProps: setDrawerProps,
|
||||
emitOpen: undefined,
|
||||
};
|
||||
|
||||
const instance = getCurrentInstance();
|
||||
|
||||
instance && emit('register', drawerInstance, instance.uid);
|
||||
|
||||
const getMergeProps = computed((): DrawerProps => {
|
||||
return deepMerge(props, unref(propsRef)) as any;
|
||||
});
|
||||
|
||||
const getProps = computed(() => {
|
||||
const opt: Partial<DrawerProps> = {
|
||||
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` as any;
|
||||
}
|
||||
}
|
||||
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: Recordable) {
|
||||
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,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="less">
|
||||
@drawer-header-height: 60px;
|
||||
@detail-header-height: 40px;
|
||||
@prefix-cls: ~'@{namespace}-basic-drawer';
|
||||
@prefix-cls-detail: ~'@{namespace}-basic-drawer__detail';
|
||||
|
||||
.@{prefix-cls}.ant-drawer {
|
||||
.full-drawer {
|
||||
.ant-drawer-body {
|
||||
& > .scrollbar {
|
||||
& > .scrollbar__bar {
|
||||
display: none !important;
|
||||
}
|
||||
& > .scrollbar__wrap > .scrollbar__view {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.ant-drawer-wrapper-body {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ant-drawer-close {
|
||||
&:hover {
|
||||
color: @error-color;
|
||||
}
|
||||
}
|
||||
.ant-drawer-header {
|
||||
height: @drawer-header-height;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
flex: unset;
|
||||
|
||||
.ant-drawer-title,
|
||||
.yunzhupaas-basic-title {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-drawer-body {
|
||||
height: calc(100% - @drawer-header-height);
|
||||
padding: 0;
|
||||
background-color: @component-background;
|
||||
|
||||
.scrollbar__wrap {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
> .scrollbar > .scrollbar__bar.is-horizontal {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.@{prefix-cls-detail} {
|
||||
position: absolute;
|
||||
|
||||
.ant-drawer-header {
|
||||
width: 100%;
|
||||
height: @detail-header-height;
|
||||
padding: 0;
|
||||
border-top: 1px solid @border-color-base;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.ant-drawer-title {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.ant-drawer-close {
|
||||
height: @detail-header-height;
|
||||
line-height: @detail-header-height;
|
||||
}
|
||||
|
||||
.scrollbar__wrap {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.ant-drawer-body {
|
||||
height: calc(100% - @detail-header-height);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
93
src/components/Drawer/src/components/DrawerFooter.vue
Normal file
93
src/components/Drawer/src/components/DrawerFooter.vue
Normal file
@@ -0,0 +1,93 @@
|
||||
<template>
|
||||
<div :class="prefixCls" :style="getStyle" v-if="showFooter || $slots.footer">
|
||||
<template v-if="!$slots.footer">
|
||||
<slot name="insertFooter"></slot>
|
||||
<a-button
|
||||
:type="continueType"
|
||||
@click="handleContinue"
|
||||
:loading="continueLoading"
|
||||
:disabled="confirmLoading"
|
||||
class="mr-10px"
|
||||
v-bind="continueButtonProps"
|
||||
v-if="showContinueBtn">
|
||||
{{ continueText }}
|
||||
</a-button>
|
||||
<a-button v-bind="cancelButtonProps" @click="handleClose" class="mr-10px" v-if="showCancelBtn">
|
||||
{{ cancelText }}
|
||||
</a-button>
|
||||
<slot name="centerFooter"></slot>
|
||||
<a-button
|
||||
:type="okType"
|
||||
@click="handleOk"
|
||||
v-bind="okButtonProps"
|
||||
class="mr-10px"
|
||||
:loading="confirmLoading"
|
||||
:disabled="okButtonProps?.disabled || continueLoading"
|
||||
v-if="showOkBtn">
|
||||
{{ okText }}
|
||||
</a-button>
|
||||
<slot name="appendFooter"></slot>
|
||||
</template>
|
||||
<template v-else>
|
||||
<slot name="footer"></slot>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import type { CSSProperties } from 'vue';
|
||||
import { defineComponent, computed } from 'vue';
|
||||
import { useDesign } from '@/hooks/web/useDesign';
|
||||
|
||||
import { footerProps } from '../props';
|
||||
export default defineComponent({
|
||||
name: 'BasicDrawerFooter',
|
||||
props: {
|
||||
...footerProps,
|
||||
height: {
|
||||
type: String,
|
||||
default: '60px',
|
||||
},
|
||||
},
|
||||
emits: ['close', 'ok', 'continue'],
|
||||
setup(props, { emit }) {
|
||||
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 handleContinue(e: Event) {
|
||||
emit('continue', e);
|
||||
}
|
||||
function handleClose() {
|
||||
emit('close');
|
||||
}
|
||||
return { handleOk, handleContinue, prefixCls, handleClose, getStyle };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
@prefix-cls: ~'@{namespace}-basic-drawer-footer';
|
||||
@footer-height: 60px;
|
||||
.@{prefix-cls} {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
padding: 0 12px 0 20px;
|
||||
text-align: right;
|
||||
background-color: @component-background;
|
||||
border-top: 1px solid @border-color-base;
|
||||
|
||||
> * {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
74
src/components/Drawer/src/components/DrawerHeader.vue
Normal file
74
src/components/Drawer/src/components/DrawerHeader.vue
Normal file
@@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<BasicTitle v-if="!isDetail" :class="prefixCls">
|
||||
<slot name="title"></slot>
|
||||
{{ !$slots.title ? title : '' }}
|
||||
</BasicTitle>
|
||||
|
||||
<div :class="[prefixCls, `${prefixCls}--detail`]" v-else>
|
||||
<span :class="`${prefixCls}__twrap`">
|
||||
<span @click="handleClose" v-if="showDetailBack">
|
||||
<ArrowLeftOutlined :class="`${prefixCls}__back`" />
|
||||
</span>
|
||||
<span v-if="title">{{ title }}</span>
|
||||
</span>
|
||||
|
||||
<span :class="`${prefixCls}__toolbar`">
|
||||
<slot name="titleToolbar"></slot>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import { BasicTitle } from '@/components/Basic';
|
||||
import { ArrowLeftOutlined } from '@ant-design/icons-vue';
|
||||
|
||||
import { useDesign } from '@/hooks/web/useDesign';
|
||||
|
||||
import { propTypes } from '@/utils/propTypes';
|
||||
export default defineComponent({
|
||||
name: 'BasicDrawerHeader',
|
||||
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 };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
@prefix-cls: ~'@{namespace}-basic-drawer-header';
|
||||
@footer-height: 60px;
|
||||
.@{prefix-cls} {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
|
||||
&__back {
|
||||
padding: 0 12px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: @primary-color;
|
||||
}
|
||||
}
|
||||
|
||||
&__twrap {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&__toolbar {
|
||||
padding-right: 50px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user