first commit

This commit is contained in:
muzi
2025-06-16 14:42:26 +08:00
commit 6f06721506
149 changed files with 56883 additions and 0 deletions

View File

@ -0,0 +1,52 @@
<!--
* @Author: 田鑫
* @Date: 2023-03-05 18:14:16
* @LastEditors: 田鑫
* @LastEditTime: 2023-03-05 19:17:52
* @Description:
-->
<script lang="ts" setup>
import type { RouteLocationNormalized } from 'vue-router';
const router = useRouter();
const route = useRoute();
const matched = computed(() => {
if (route.matched.length === 1 && route.matched[0].path === '/') {
return [];
} else {
return route.matched.reduce((t: RouteLocationNormalized[], o) => {
const isExist = t.find((c) => c.name === o.name);
return isExist ? t : [...t, router.resolve(o)];
}, []);
}
});
</script>
<template>
<view></view>
<!-- <a-breadcrumb class="container-breadcrumb">
<a-breadcrumb-item v-for="{ meta, name } in matched" :key="name">
<router-link v-slot="{ href, navigate }" :to="{ name }" custom>
<a-link v-if="meta.needNavigate" :href="href" @click="navigate">{{
meta.locale ? meta.locale : '主页'
}}</a-link>
<a-link v-else disabled>{{ meta.locale ? meta.locale : '主页' }}</a-link>
</router-link>
</a-breadcrumb-item>
</a-breadcrumb> -->
</template>
<style scoped lang="less">
.container-breadcrumb {
margin: 16px 0;
:deep(.arco-breadcrumb-item) {
> a {
color: rgb(var(--gray-6));
}
&:last-child {
color: rgb(var(--gray-8));
}
}
}
</style>

View File

@ -0,0 +1,5 @@
export { default as Navbar } from './navbar/index.vue';
export { default as Menu } from './menu/index.vue';
export { default as TabBar } from './tab-bar/index.vue';
export { default as Breadcrumb } from './breadcrumb/index.vue';
export { default as ModalSimple } from './modal/index.vue';

View File

@ -0,0 +1,140 @@
<script lang="tsx">
import type { RouteMeta, RouteRecordRaw } from 'vue-router';
import { useAppStore } from '@/stores';
import { listenerRouteChange } from '@/utils/route-listener';
import { openWindow, regexUrl } from '@/utils';
import useMenuTree from './use-menu-tree';
export default defineComponent({
emit: ['collapse'],
setup() {
const appStore = useAppStore();
const router = useRouter();
const route = useRoute();
const { menuTree } = useMenuTree();
const collapsed = computed({
get() {
if (appStore.device === 'desktop') return appStore.menuCollapse;
return false;
},
set(value: boolean) {
appStore.updateSettings({ menuCollapse: value });
},
});
const topMenu = computed(() => appStore.topMenu);
const openKeys = ref<string[]>([]);
const selectedKey = ref<string[]>([]);
const goto = (item: RouteRecordRaw) => {
// Open external link
if (regexUrl.test(item.path)) {
openWindow(item.path);
selectedKey.value = [item.name as string];
return;
}
// Eliminate external link side effects
const { hideInMenu, activeMenu } = item.meta as RouteMeta;
if (route.name === item.name && !hideInMenu && !activeMenu) {
selectedKey.value = [item.name as string];
return;
}
// Trigger router change
router.push({
name: item.name,
});
};
const findMenuOpenKeys = (target: string) => {
const result: string[] = [];
let isFind = false;
const backtrack = (item: RouteRecordRaw, keys: string[]) => {
if (item.name === target) {
isFind = true;
result.push(...keys);
return;
}
if (item.children?.length) {
item.children.forEach((el) => {
backtrack(el, [...keys, el.name as string]);
});
}
};
menuTree.value.forEach((el: RouteRecordRaw) => {
if (isFind) return; // Performance optimization
backtrack(el, [el.name as string]);
});
return result;
};
listenerRouteChange((newRoute) => {
const { requiresAuth, activeMenu, hideInMenu } = newRoute.meta;
if (requiresAuth && (!hideInMenu || activeMenu)) {
const menuOpenKeys = findMenuOpenKeys((activeMenu || newRoute.name) as string);
const keySet = new Set([...menuOpenKeys, ...openKeys.value]);
openKeys.value = [...keySet];
selectedKey.value = [activeMenu || menuOpenKeys[menuOpenKeys.length - 1]];
}
}, true);
const setCollapse = (val: boolean) => {
if (appStore.device === 'desktop') appStore.updateSettings({ menuCollapse: val });
};
const renderSubMenu = () => {
function travel(_route: RouteRecordRaw[], nodes = []) {
if (_route) {
_route.forEach((element) => {
// This is demo, modify nodes as needed
const icon = element?.meta?.icon ? () => h(element?.meta?.icon as object) : null;
const node =
element?.children && element?.children.length !== 0 ? (
<a-sub-menu
key={element?.name}
v-slots={{
icon,
title: () => element?.meta?.locale || '',
}}
>
{travel(element?.children)}
</a-sub-menu>
) : (
<a-menu-item key={element?.name} v-slots={{ icon }} onClick={() => goto(element)}>
{element?.meta?.locale || ''}
</a-menu-item>
);
nodes.push(node as never);
});
}
return nodes;
}
return travel(menuTree.value);
};
return () => (
<a-menu
mode={topMenu.value ? 'horizontal' : 'vertical'}
v-model:collapsed={collapsed.value}
v-model:open-keys={openKeys.value}
show-collapse-button={appStore.device !== 'mobile'}
auto-open={false}
selected-keys={selectedKey.value}
auto-open-selected={true}
level-indent={34}
style="height: 100%;width:100%;"
onCollapse={setCollapse}
>
{renderSubMenu()}
</a-menu>
);
},
});
</script>
<style lang="less" scoped>
:deep(.arco-menu-inner) {
.arco-menu-inline-header {
display: flex;
align-items: center;
}
.arco-icon {
&:not(.arco-icon-down) {
font-size: 18px;
}
}
}
</style>

View File

@ -0,0 +1,60 @@
import type { RouteRecordRaw, RouteRecordNormalized } from 'vue-router';
import { useAppStore } from '@/stores';
import appClientMenus from '@/router/app-menus';
export default function useMenuTree() {
const appStore = useAppStore();
const appRoute = computed(() => {
if (appStore.menuFromServer) {
// return appClientMenus.concat(toRaw(appStore.appAsyncMenus));
return toRaw(appStore.appAsyncMenus);
}
return appClientMenus;
});
const menuTree = computed(() => {
const copyRouter = cloneDeep(appRoute.value) as RouteRecordNormalized[];
copyRouter.sort((a: RouteRecordNormalized, b: RouteRecordNormalized) => {
return (a.meta.order || 0) - (b.meta.order || 0);
});
function travel(_routes: RouteRecordRaw[], layer: number) {
if (!_routes) return null;
const collector: any = _routes.map((element) => {
// leaf node
if (element.meta?.hideChildrenInMenu || !element.children) {
element.children = [];
return element;
}
// route filter hideInMenu true
element.children = element.children.filter((x) => x.meta?.hideInMenu !== true);
// Associated child node
const subItem = travel(element.children, layer + 1);
if (subItem.length) {
element.children = subItem;
return element;
}
// the else logic
if (layer > 1) {
element.children = subItem;
return element;
}
if (element.meta?.hideInMenu === false) {
return element;
}
return null;
});
return collector.filter(Boolean);
}
return travel(copyRouter, 0);
});
return {
menuTree,
};
}

View File

@ -0,0 +1,32 @@
<script setup lang="ts">
import type { Component, DefineComponent } from 'vue';
import IconHover from '@arco-design/web-vue/es/_components/icon-hover';
defineProps<{
title?: string;
content?: string | (() => DefineComponent | Component);
}>();
defineEmits(['close']);
</script>
<template>
<slot name="header">
<div class="flex justify-end mb7">
<slot name="close">
<icon-hover @click="$emit('close')">
<icon-close />
</icon-hover>
</slot>
</div>
</slot>
<slot>
<div class="flex flex-col text-center">
<div v-if="title" class="mb4 text-lg font-600">{{ title }}</div>
<template v-else />
<component :is="content" v-if="typeof content === 'function'" />
<div v-else>{{ content }}</div>
</div>
</slot>
</template>

View File

@ -0,0 +1,114 @@
<script lang="ts" setup>
import { useAppStore } from '@/stores';
import { IconExport, IconFile, IconCaretDown } from '@arco-design/web-vue/es/icon';
import { fetchMenusTree } from '@/api/all';
const lists = ref([]);
const getMenus = async () => {
const res = await fetchMenusTree();
lists.value = res;
};
onMounted(() => {
getMenus();
});
const appStore = useAppStore();
const { isFullscreen, toggle: toggleFullScreen } = useFullscreen();
const avatar = computed(
() => '//p3-armor.byteimg.com/tos-cn-i-49unhts6dw/dfdba5317c0c20ce20e64fac803d52bc.svg~tplv-49unhts6dw-image.image',
);
const topMenu = computed(() => appStore.topMenu && appStore.menu);
const toggleDrawerMenu = inject('toggleDrawerMenu') as () => void;
function setServerMenu() {
appStore.fetchServerMenuConfig();
console.log(appStore.serverMenu);
}
const handleSelect = (index: any) => {
console.log(index);
};
</script>
<template>
<div class="navbar">
<div class="left-side">
<a-space>
<img src="@/assets/LOGO.svg" alt="" />
</a-space>
</div>
<div class="center-side">
<div class="menu-demo">
<a-menu mode="horizontal" :default-selected-keys="['1']">
<a-menu-item :key="'1'">
<view>工作台</view>
</a-menu-item>
<a-menu-item v-for="(item, index) in lists" :key="index + 2">
<a-dropdown @select="handleSelect" :popup-max-height="false">
<a-button>{{ item.name }}<icon-caret-down /></a-button>
<template #content>
<a-doption v-for="(child, index) in item.children" :key="index">{{ child.name }}</a-doption>
</template>
</a-dropdown>
</a-menu-item>
</a-menu>
</div>
</div>
<ul class="right-side">
<li>
<a-dropdown trigger="click">
<a-avatar class="cursor-pointer" :size="32">
<img alt="avatar" :src="avatar" />
</a-avatar>
</a-dropdown>
</li>
</ul>
</div>
</template>
<style scoped lang="less">
.navbar {
display: flex;
justify-content: space-between;
height: 100%;
background-color: var(--color-bg-2);
border-bottom: 1px solid var(--color-border);
}
.left-side {
display: flex;
align-items: center;
padding-left: 20px;
}
.center-side {
flex: 1;
display: flex;
align-items: center;
margin-left: 40px;
}
.cneter-tip {
font-size: 16px;
font-weight: 400;
color: var(--color-text-1);
}
.menu-demo {
flex: 1;
}
.right-side {
display: flex;
padding-right: 20px;
list-style: none;
li {
display: flex;
align-items: center;
padding: 0 10px;
}
a {
color: var(--color-text-1);
text-decoration: none;
}
.nav-btn {
border-color: rgb(var(--gray-2));
color: rgb(var(--gray-8));
font-size: 16px;
}
}
</style>

View File

@ -0,0 +1,83 @@
<script lang="ts" setup>
import type { RouteLocationNormalized } from 'vue-router';
import { listenerRouteChange, removeRouteListener } from '@/utils/route-listener';
import { useAppStore, useTabBarStore } from '@/stores';
import TabItem from './tab-item.vue';
const appStore = useAppStore();
const tabBarStore = useTabBarStore();
const affixRef = ref();
const tagList = computed(() => {
return tabBarStore.getTabList;
});
const offsetTop = computed(() => {
return appStore.navbar ? 60 : 0;
});
watch(
() => appStore.navbar,
() => {
affixRef.value.updatePosition();
},
);
listenerRouteChange((route: RouteLocationNormalized) => {
if (!route.meta.noAffix && !tagList.value.some((tag) => tag.fullPath === route.fullPath)) {
tabBarStore.updateTabList(route);
}
}, true);
onUnmounted(() => {
removeRouteListener();
});
</script>
<template>
<div class="tab-bar-container">
<a-affix ref="affixRef" :offset-top="offsetTop">
<div class="tab-bar-box">
<div class="tab-bar-scroll">
<div class="tags-wrap">
<tab-item v-for="(tag, index) in tagList" :key="tag.fullPath" :index="index" :item-data="tag" />
</div>
</div>
<div class="tag-bar-operation"></div>
</div>
</a-affix>
</div>
</template>
<style scoped lang="less">
.tab-bar-container {
position: relative;
background-color: var(--color-bg-2);
.tab-bar-box {
display: flex;
padding: 0 0 0 20px;
background-color: var(--color-bg-2);
border-bottom: 1px solid var(--color-border);
.tab-bar-scroll {
height: 32px;
flex: 1;
overflow: hidden;
.tags-wrap {
padding: 4px 0;
height: 48px;
white-space: nowrap;
overflow-x: auto;
:deep(.arco-tag) {
display: inline-flex;
align-items: center;
margin-right: 6px;
cursor: pointer;
&:first-child {
.arco-tag-close-btn {
display: none;
}
}
}
}
}
}
.tag-bar-operation {
width: 100px;
height: 32px;
}
}
</style>

View File

@ -0,0 +1,177 @@
<script lang="ts" setup>
import type { PropType } from 'vue';
import type { TagProps } from '@/stores/modules/tab-bar/types';
import { useTabBarStore } from '@/stores';
import { DEFAULT_ROUTE_NAME, REDIRECT_ROUTE_NAME } from '@/router/constants';
const props = defineProps({
itemData: {
type: Object as PropType<TagProps>,
default() {
return [];
},
},
index: {
type: Number,
default: 0,
},
});
// eslint-disable-next-line no-shadow
enum Eaction {
reload = 'reload',
current = 'current',
left = 'left',
right = 'right',
others = 'others',
all = 'all',
}
const router = useRouter();
const route = useRoute();
const tabBarStore = useTabBarStore();
const goto = (tag: TagProps) => {
router.push({ ...tag });
};
const tagList = computed(() => {
return tabBarStore.getTabList;
});
const disabledReload = computed(() => {
return props.itemData.fullPath !== route.fullPath;
});
const disabledCurrent = computed(() => {
return props.index === 0;
});
const disabledLeft = computed(() => {
return [0, 1].includes(props.index);
});
const disabledRight = computed(() => {
return props.index === tagList.value.length - 1;
});
const tagClose = (tag: TagProps, idx: number) => {
tabBarStore.deleteTag(idx, tag);
if (props.itemData.fullPath === route.fullPath) {
const latest = tagList.value[idx - 1]; // 获取队列的前一个tab
router.push({ name: latest.name });
}
};
const findCurrentRouteIndex = () => {
return tagList.value.findIndex((el) => el.fullPath === route.fullPath);
};
const actionSelect = async (value: any) => {
const { itemData, index } = props;
const copyTagList = [...tagList.value];
if (value === Eaction.current) {
tagClose(itemData, index);
} else if (value === Eaction.left) {
const currentRouteIdx = findCurrentRouteIndex();
copyTagList.splice(1, props.index - 1);
tabBarStore.freshTabList(copyTagList);
if (currentRouteIdx < index) {
router.push({ name: itemData.name });
}
} else if (value === Eaction.right) {
const currentRouteIdx = findCurrentRouteIndex();
copyTagList.splice(props.index + 1);
tabBarStore.freshTabList(copyTagList);
if (currentRouteIdx > index) {
router.push({ name: itemData.name });
}
} else if (value === Eaction.others) {
const filterList = tagList.value.filter((el, idx) => {
return idx === 0 || idx === props.index;
});
tabBarStore.freshTabList(filterList);
router.push({ name: itemData.name });
} else if (value === Eaction.reload) {
tabBarStore.deleteCache(itemData);
await router.push({
name: REDIRECT_ROUTE_NAME,
params: {
path: route.fullPath,
},
});
tabBarStore.addCache(itemData.name);
} else {
tabBarStore.resetTabList();
router.push({ name: DEFAULT_ROUTE_NAME });
}
};
</script>
<template>
<a-dropdown trigger="contextMenu" :popup-max-height="false" @select="actionSelect">
<span
:class="[
'arco-tag arco-tag-size-medium arco-tag-checked',
{ 'link-activated': itemData.fullPath === $route.fullPath },
]"
@click="goto(itemData)"
>
<span class="tag-link">{{ itemData.title }}</span>
<span
class="arco-icon-hover arco-tag-icon-hover arco-icon-hover-size-medium arco-tag-close-btn"
@click.stop="tagClose(itemData, index)"
>
<icon-close />
</span>
</span>
<template #content>
<a-doption :disabled="disabledReload" :value="Eaction.reload">
<icon-refresh />
<span>重新加载</span>
</a-doption>
<a-doption class="sperate-line" :disabled="disabledCurrent" :value="Eaction.current">
<icon-close />
<span>关闭当前标签页</span>
</a-doption>
<a-doption :disabled="disabledLeft" :value="Eaction.left">
<icon-to-left />
<span>关闭左侧标签页</span>
</a-doption>
<a-doption class="sperate-line" :disabled="disabledRight" :value="Eaction.right">
<icon-to-right />
<span>关闭右侧标签页</span>
</a-doption>
<a-doption :value="Eaction.others">
<icon-swap />
<span>关闭其它标签页</span>
</a-doption>
<a-doption :value="Eaction.all">
<icon-folder-delete />
<span>关闭全部标签页</span>
</a-doption>
</template>
</a-dropdown>
</template>
<style scoped lang="less">
.tag-link {
color: var(--color-text-2);
text-decoration: none;
}
.link-activated {
color: rgb(var(--link-6));
.tag-link {
color: rgb(var(--link-6));
}
& + .arco-tag-close-btn {
color: rgb(var(--link-6));
}
}
:deep(.arco-dropdown-option-content) {
span {
margin-left: 10px;
}
}
.arco-dropdown-open {
.tag-link {
color: rgb(var(--danger-6));
}
.arco-tag-close-btn {
color: rgb(var(--danger-6));
}
}
.sperate-line {
border-bottom: 1px solid var(--color-neutral-3);
}
</style>

View File

@ -0,0 +1,70 @@
<!--
* @Author: 田鑫
* @Date: 2023-02-16 11:58:01
* @LastEditors: 田鑫
* @LastEditTime: 2023-02-16 16:56:27
* @Description: 二次确认框
-->
<template>
<div>
<a-popconfirm
:content="content"
:position="position"
:ok-text="okText"
:cancel-text="cancelText"
:type="popupType"
@ok="handleConfirm"
@cancel="handleCancel"
>
<slot></slot>
</a-popconfirm>
</div>
</template>
<script setup lang="ts">
import type { PropType } from 'vue-demi';
type Position = 'top' | 'tl' | 'tr' | 'bottom' | 'bl' | 'br' | 'left' | 'lt' | 'lb' | 'right' | 'rt' | 'rb';
type PopupType = 'info' | 'success' | 'warning' | 'error';
const props = defineProps({
content: {
type: String,
default: '是否确认?',
},
position: {
type: String as PropType<Position>,
default: 'top',
},
okText: {
type: String,
default: '确定',
},
cancelText: {
type: String,
default: '取消',
},
popupType: {
type: String as PropType<PopupType>,
default: 'info',
},
});
const emit = defineEmits(['confirmEmit', 'cancelEmit']);
/**
* 确定事件
*/
function handleConfirm() {
emit('confirmEmit');
}
/**
* 确定事件
*/
function handleCancel() {
emit('cancelEmit');
}
</script>
<style lang="less" scoped></style>

View File

@ -0,0 +1,5 @@
# 动态配置form表单
示例见 views/components/form
参数 fieldList配置项包括arco.design Form.Item和Input、Select以及自定义属性component等
参数 model: 传默认值
通过ref获取实例调用子组件实例updateFieldsList方法更新配置项调用setModel方法更新数据调用setForm方法更新Form属性自定义事件change处理逻辑

View File

@ -0,0 +1,40 @@
// form.item的属性名称集合
export const formItemKeys = [
'field',
'label',
'tooltip',
'showColon',
'noStyle',
'disabled',
'help',
'extra',
'required',
'asteriskPosition',
'rules',
'validateStatus',
'validateTrigger',
'wrapperColProps',
'hideLabel',
'hideAsterisk',
'labelColStyle',
'wrapperColStyle',
'rowProps',
'rowClass',
'contentClass',
'contentFlex',
'labelColFlex',
'feedback',
'labelComponent',
'labelAttrs',
];
// 自定义属性名称集合
export const customKeys = ['component', 'lists'];
// 响应式栅格默认配置
export const COL_PROPS = {
xs: 12,
sm: 12,
md: 8,
lg: 8,
xl: 6,
xxl: 6,
};

View File

@ -0,0 +1,271 @@
<template>
<slot name="header"></slot>
<a-form v-bind="_options" ref="formRef" :model="model" @submit.prevent>
<a-row v-bind="rowProps" :gutter="20">
<template
v-for="{ field, component, formItemProps, componentProps, lists, colProps } in newFieldList"
:key="field"
>
<!-- 单选框 -->
<a-col v-if="component === 'radio'" v-bind="colProps">
<a-form-item v-bind="formItemProps">
<a-radio-group v-bind="componentProps" v-model="model[field]">
<a-radio v-for="val in lists" :key="val['value']" :label="val['value']" size="large">
{{ val['label'] }}
</a-radio>
</a-radio-group>
</a-form-item>
</a-col>
<!-- 复选框 -->
<a-col v-if="component === 'checkbox'" v-bind="colProps">
<a-form-item v-bind="formItemProps">
<a-checkbox-group v-bind="componentProps" v-model="model[field]">
<a-checkbox v-for="c in lists" :key="c['value']" :label="c['value']">{{ c['label'] }}</a-checkbox>
</a-checkbox-group>
</a-form-item>
</a-col>
<!-- 下拉框 -->
<a-col v-if="component === 'select'" v-bind="colProps">
<a-form-item v-bind="formItemProps">
<a-select v-bind="componentProps" v-model="model[field]">
<a-option v-for="s in lists" :key="s['value']" :label="s['label']" :value="s['value']" />
</a-select>
</a-form-item>
</a-col>
<!-- 文本域 -->
<a-col v-if="component === 'textarea'" v-bind="colProps">
<a-form-item v-bind="formItemProps">
<a-textarea v-bind="componentProps" v-model="model[field]" />
</a-form-item>
</a-col>
<!-- 时间选择器 -->
<a-col v-if="component === 'time'" v-bind="colProps">
<a-form-item v-bind="formItemProps">
<a-time-picker v-bind="componentProps" v-model="model[field]" />
</a-form-item>
</a-col>
<!-- 日期选择器 -->
<a-col v-if="component === 'date'" v-bind="colProps">
<a-form-item v-bind="formItemProps">
<a-date-picker v-bind="componentProps" v-model="model[field]" />
</a-form-item>
</a-col>
<!-- 日期范围选择器 -->
<a-col v-if="component === 'rangeDate'" v-bind="colProps">
<a-form-item v-bind="formItemProps">
<a-range-picker v-bind="componentProps" v-model="model[field]" />
</a-form-item>
</a-col>
<!-- 级联选择器 -->
<a-col v-if="component === 'cascader'" v-bind="colProps">
<a-form-item v-bind="formItemProps">
<a-cascader v-bind="componentProps" v-model="model[field]" />
</a-form-item>
</a-col>
<!-- 数字输入框 -->
<a-col v-if="component === 'inputNumber'" v-bind="colProps">
<a-form-item v-bind="formItemProps">
<a-input-number v-bind="componentProps" v-model="model[field]" />
</a-form-item>
</a-col>
<!-- 输入框 -->
<a-col v-if="component === 'input'" v-bind="colProps">
<a-form-item v-bind="formItemProps">
<a-input v-bind="componentProps" v-model="model[field]" />
</a-form-item>
</a-col>
<!-- 标题模块 -->
<a-col v-if="component === 'title'" :span="24">
<div class="title">
<div class="bar"></div>
<h4 class="text">{{ formItemProps.label }}</h4>
</div>
</a-col>
<!-- 自定义插槽slot -->
<a-col v-if="component === 'slot'" :span="24">
<slot :name="field"></slot>
</a-col>
</template>
<a-col :span="24">
<a-form-item>
<slot name="buttons" :model="model" :formRef="formRef">
<a-space>
<a-button type="primary" @click="onSubmit(formRef)">{{ _options.submitButtonText }}</a-button>
<a-button v-if="_options.showResetButton" @click="resetForm(formRef)">
{{ _options.resetButtonText }}
</a-button>
<a-button v-if="_options.showCancelButton" @click="emit('cancel')">
{{ _options.cancelButtonText }}
</a-button>
</a-space>
</slot>
</a-form-item>
</a-col>
</a-row>
</a-form>
<slot name="footer"></slot>
</template>
<script lang="ts" setup>
import type { FormInstance, RowProps, ValidatedError } from '@arco-design/web-vue';
import type { ComputedRef } from 'vue';
import type { Form } from './interface';
import { formItemKeys, customKeys, COL_PROPS } from './constants';
import { changeFormList } from './utils';
import type { FieldData } from '@arco-design/web-vue/es/form/interface';
// 父组件传递的值
interface Props {
fieldList: Form.FieldItem[];
model?: Record<string, any>;
options?: Form.Options;
rowProps?: RowProps;
}
interface EmitEvent {
(e: 'submit' | 'change', params: any): void;
(e: 'reset' | 'cancel'): void;
}
const props = defineProps<Props>();
const emit = defineEmits<EmitEvent>();
// 表单的数据
let model = ref<Record<string, any>>({});
const formRef = ref<FormInstance>();
// 初始化处理Form组件属性options
const _options = ref<Record<string, any>>({});
const initOptions = () => {
const option = {
layout: 'vertical',
disabled: false,
submitButtonText: '提交',
resetButtonText: '重置',
cancelButtonText: '取消',
showResetButton: true,
};
Object.assign(option, props?.options);
_options.value = option;
};
initOptions();
// 初始化处理model
const initFormModel = () => {
props.fieldList.forEach((item: Form.FieldItem) => {
// 如果类型为checkbox默认值需要设置一个空数组
const value = item.component === 'checkbox' ? [] : '';
const { field, component } = item;
if (component !== 'slot' && component !== 'title') {
model.value[item.field] = props?.model?.[field] || value;
}
});
};
initFormModel();
// 初始化处理fieldList
const newFieldList: any = ref(null);
const initFieldList = () => {
const list = props?.fieldList.map((item: Form.FieldItem) => {
const customProps = pick(item, customKeys);
const formItemProps = pick(item, formItemKeys);
const componentProps = omit(item, [...formItemKeys, ...customKeys, 'field', 'colProps']);
const { colProps = {}, field, placeholder, component = 'input', label } = item;
componentProps.onChange = (val: any) => onChange(field, val);
const newColProps = {
...colProps,
...COL_PROPS,
};
const obj = {
field,
colProps: newColProps,
...customProps,
formItemProps,
componentProps,
};
if ((component === 'input' || component === 'textarea') && !placeholder) {
componentProps.placeholder = `请输入${label}`;
}
if (component === 'select' && !placeholder) {
componentProps.placeholder = `请选择${label}`;
}
if (component === 'rangeDate') {
componentProps.value = [null, null];
}
return obj;
});
newFieldList.value = list;
return list;
};
initFieldList();
// 提交
const onSubmit = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
let flag = false;
await formEl.validate((errors: undefined | Record<string, ValidatedError>) => {
if (!errors) {
emit('submit', model.value);
flag = true;
} else {
return false;
}
});
return (flag && model.value) || null;
};
// 提交--父组件调用
const submit = () => {
return onSubmit(formRef.value);
};
// 重置
const resetForm = (formEl: FormInstance | undefined) => {
if (!formEl) return;
formEl.resetFields();
};
// 表单变化
const onChange = (key: string, val: any) => {
emit('change', { key, val });
};
// 设置
const setModel = (data: Record<string, FieldData>) => {
const newData = {
...model.value,
...data,
};
model.value = newData;
};
// 设置Form
const setForm = (data: Form.Options) => {
const options = {
..._options.value,
...data,
};
_options.value = options;
};
// 更新配置项
const updateFieldsList = (updateList: any[]) => {
const list = changeFormList(newFieldList.value, updateList);
newFieldList.value = list;
};
defineExpose({
submit,
setModel,
setForm,
updateFieldsList,
});
</script>
<style lang="less" scoped>
:deep(.arco-picker) {
width: 100%;
}
.title {
display: flex;
align-items: center;
.bar {
width: 4px;
height: 14px;
border-radius: 1px;
margin-right: 8px;
background-color: rgb(var(--primary-6));
}
.text {
font-size: 16px;
color: var(--color-text-1);
}
}
</style>

95
src/components/wyg-form/interface.d.ts vendored Normal file
View File

@ -0,0 +1,95 @@
import type { FieldRule } from '@arco-design/web-vue/es/form/interface';
import { Size, ColProps, CascaderOption, InputProps } from '@arco-design/web-vue';
console.log(InputProps, 'InputProps=====');
export namespace Form {
/** 表单项自身Props */
interface FormItem<T = string> {
field: string;
label: string;
tooltip?: string;
showColon?: boolean;
noStyle?: boolean;
disabled?: boolean;
help?: string;
extra?: string;
required?: boolean;
asteriskPosition?: 'start' | 'end';
rules?: FieldRule | FieldRule[];
validateStatus?: 'success' | 'warning' | 'error' | 'validating';
validateTrigger?: 'change' | 'input' | 'focus' | 'blur';
labelColProps?: object;
wrapperColProps?: object;
hideLabel?: boolean;
hideAsterisk?: boolean;
labelColStyle?: object;
wrapperColStyle?: object;
rowProps?: object;
rowClass?: string | Array<T> | object;
contentClass?: string | Array<T> | object;
contentFlex?: boolean;
labelColFlex?: number | string;
feedback?: boolean;
labelComponent?: string;
labelAttrs?: object;
}
// 当前 fieldItem 的类型 默认值'input'
type ComponentType =
| 'input'
| 'textarea'
| 'radio'
| 'checkbox'
| 'select'
| 'time'
| 'date'
| 'rangeDate'
| 'inputNumber'
| 'cascader'
| 'title'
| 'slot';
/** 自定义Props */
interface CustomProps {
component?: ComponentType;
lists?: object; // 如果 type='checkbox' / 'radio' / 'select'时需传入此配置项。格式参考FieldItemOptions配置项
}
/** Input、Select组件等的Props */
interface ComponentProps {
placeholder?: string; // 输入框占位文本
readonly?: boolean; // 是否只读 false
allowClear?: boolean; // 是否可清空 false
onChange?: Function;
options?: CascaderOption[];
}
/** 每一项配置项的属性 */
interface FieldItem extends FormItem, CustomProps, ComponentProps {
colProps?: ColProps;
}
/** 处理后的配置项属性 */
interface NewFieldItem extends CustomProps {
formItemProps: FormItem;
componentProps: ComponentProps;
field: string;
colProps?: ColProps;
}
interface FieldItemOptions {
label: string | number;
value: string | number;
}
/** 表单Form自身Props */
interface Options {
layout?: 'horizontal' | 'vertical' | 'inline'; // 表单的布局方式,包括水平、垂直、多列
size?: Size; // 用于控制该表单内组件的尺寸
labelColProps?: object; // 标签元素布局选项。参数同 <col> 组件一致默认值span: 5, offset: 0
wrapperColProps?: object; // 表单控件布局选项。参数同 <col> 组件一致默认值span: 19, offset: 0
labelAlign?: 'left' | 'right'; // 标签的对齐方向,默认值'right'
disabled?: boolean; // 是否禁用表单
rules?: Record<string, FieldRule | FieldRule[]>; // 表单项校验规则
autoLabelWidth?: boolean; // 是否开启自动标签宽度,仅在 layout="horizontal" 下生效。默认值false
showResetButton?: boolean; // 是否展示重置按钮
showCancelButton?: boolean; // 是否展示取消按钮
submitButtonText?: string;
resetButtonText?: string;
cancelButtonText?: string;
}
}

View File

@ -0,0 +1,25 @@
import { formItemKeys, customKeys, COL_PROPS } from './constants';
import type { Form } from './interface';
// fieldList更新
export const changeFormList = (formList: Form.NewFieldItem[], updateList: Form.FieldItem[]): Form.NewFieldItem[] => {
let list: any = formList;
list.forEach((item: any, index: string | number) => {
updateList.forEach((ele: any) => {
if (item.field === ele.field) {
list[index] = { ...item, ...ele };
const keys: string[] = Object.keys(ele);
keys.forEach((key: string) => {
const val = ele[key];
if (formItemKeys.includes(key)) {
list[index].formItemProps[key] = val;
} else if (customKeys.includes(key) || key === 'colProps') {
list[index][key] = val;
} else {
list[index].componentProps[key] = val;
}
});
}
});
});
return list;
};

View File

@ -0,0 +1,73 @@
<!--
* @Author: 田鑫
* @Date: 2023-02-16 14:40:38
* @LastEditors: 田鑫
* @LastEditTime: 2023-02-16 16:37:52
* @Description: table公用封装
-->
<template>
<div>
<a-table
:loading="loading"
:size="size"
:data="tableData"
:bordered="{ cell: borderCell }"
:pagination="setPagination"
page-position="br"
v-bind="propsRes"
v-on="propsEvent"
>
<template #columns>
<slot></slot>
</template>
</a-table>
</div>
</template>
<script setup lang="ts">
import type { PaginationProps, TableData } from '@arco-design/web-vue';
import type { PropType } from 'vue-demi';
import Components from 'unplugin-vue-components/vite';
type Size = 'mini' | 'small' | 'medium' | 'large';
const props = defineProps({
size: {
type: String as PropType<Size>,
default: 'large',
},
tableData: {
type: Array as PropType<TableData[]>,
default: () => [],
},
borderCell: {
type: Boolean,
default: true,
},
pagination: {
type: Object as PropType<PaginationProps>,
default: () => {},
},
});
const loading = ref(false);
const setPagination = computed(() => {
const defaultPagination: PaginationProps = {
showPageSize: true,
showTotal: true,
showMore: true,
size: 'large',
};
return Object.assign(defaultPagination, props.pagination);
});
watchEffect(() => {
if (props.tableData.length === 0) {
loading.value = true;
} else {
loading.value = false;
}
});
</script>
<style lang="less" scoped></style>