初始代码

This commit is contained in:
wangmingwei
2026-04-21 17:48:26 +08:00
parent d3631949e9
commit 7f9e424a5c
1822 changed files with 288292 additions and 0 deletions

View 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);

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

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

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

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

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

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

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

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

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

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

View File

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

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

View 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);

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

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

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

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

View 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>>;

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

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

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

View 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 },
};

View File

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

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

View 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,
};
};

View File

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

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

View File

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

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

View 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';

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

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

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

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

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

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

View File

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

View File

@@ -0,0 +1,5 @@
export enum MODE {
JSON = 'application/json',
HTML = 'htmlmixed',
JS = 'javascript',
}

View File

@@ -0,0 +1,3 @@
import BasicColumnDesign from './src/BasicColumnDesign.vue';
export { BasicColumnDesign };

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

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

View File

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

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

View File

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

View File

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

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

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

File diff suppressed because it is too large Load Diff

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

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

View 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,
};

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

View 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);

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

View File

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

View File

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

View File

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

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

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

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

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

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

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

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

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

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

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

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

View File

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

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

View 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';

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

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

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

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

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

View File

@@ -0,0 +1,3 @@
export { createContextMenu, destroyContextMenu } from './src/createContextMenu';
export * from './src/typing';

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

View 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 = [];
}
};

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

View 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);

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

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

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

View File

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

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

View 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);

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

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

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

View File

@@ -0,0 +1,8 @@
import type Cropper from 'cropperjs';
export interface CropendResult {
imgBase64: string;
imgInfo: Cropper.Data;
}
export type { Cropper };

View 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);

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

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

View 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];
}

View 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';

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

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

View 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