🆕 项目初始化,页面布局待修改完善

This commit is contained in:
my_ong
2024-12-01 19:02:48 +08:00
commit 6ebf253598
229 changed files with 32894 additions and 0 deletions

79
src/App.vue Normal file
View File

@@ -0,0 +1,79 @@
<template>
<a-config-provider :theme="themeV" :locale="locale">
<div
:style="{
'--ant-primary-color': useAppStore().theme.primaryColor,
'--vxe-primary-color': useAppStore().theme.primaryColor,
'--vxe-loading-color': useAppStore().theme.primaryColor,
}"
>
<a-watermark :content="config.layout.waterMark">
<router-view />
</a-watermark>
</div>
</a-config-provider>
</template>
<script lang="ts" setup>
import { useAppStore } from '@/stores/app'
import { theme } from 'ant-design-vue'
import { config } from './settings/application'
import { storeToRefs } from 'pinia'
import { useI18n } from 'vue-i18n'
import 'dayjs/locale/zh-cn'
import { messages, setI18nLanguage } from './locales'
const locale = computed(() => {
return useAppStore().language === 'zh_CN' ? messages.zh_CN : messages.en_US
})
setI18nLanguage(useAppStore().language as 'zh_CN' | 'en_US')
useI18n().locale.value = useAppStore().language
const themeV = computed(() => {
return {
algorithm: app.layout.value.navTheme === 'real-dark' ? theme.darkAlgorithm : theme.defaultAlgorithm,
token: {
colorPrimary: useAppStore().theme.primaryColor,
borderRadius: 2,
},
}
})
const app = storeToRefs(useAppStore())
useTitle(app.isCN.value ? config.title : config.name)
const colorWeak = computed(() => {
return app.layout.value.colorWeak
})
const gray = computed(() => {
return app.layout.value.gray
})
watch(
colorWeak,
(newVal) => {
const body = document.body
if (newVal) {
app.layout.value.gray = false
body.style.filter = 'invert(80%)'
} else {
body.style.removeProperty('filter')
}
},
{ immediate: true },
)
watch(
gray,
(newVal) => {
const body = document.body
if (newVal) {
app.layout.value.colorWeak = false
body.style.filter = 'grayscale(0.95)'
} else {
body.style.removeProperty('filter')
}
},
{ immediate: true },
)
</script>
<style lang="css">
:deep(.ant-btn-link) {
color: var(--ant-primary-color) !important;
}
</style>

198
src/api/acl/api.d.ts vendored Normal file
View File

@@ -0,0 +1,198 @@
declare namespace acl {
/**
* 权限
*/
export interface Permission {
/** createdTime */
createdTime?: string;
/** 权限描述 */
description?: string;
/** id */
id?: number;
/** 权限key,英文 */
key?: string;
/** 权限keyPath,用来做业务,(父级keyPath.key) */
keyPath?: string;
/** 权限名称,中文用来做标识 */
name?: string;
/** 父权限key */
parentKey?: string;
/** 权限类型 */
type?: 'MENU' | 'BUTTON' | 'OTHER';
/** typeInfo */
typeInfo?: acl.Codebook;
/** updatedTime */
updatedTime?: string;
}
/**
* 权限信息,包含是否选中标识
*/
export interface PermissionInfo {
/** createdTime */
createdTime?: string;
/** 权限描述 */
description?: string;
/** id */
id?: number;
/** 权限key,英文 */
key?: string;
/** 权限keyPath,用来做业务,(父级keyPath.key) */
keyPath?: string;
/** 权限名称,中文用来做标识 */
name?: string;
/** 父权限key */
parentKey?: string;
/** 权限是否选中标识 */
selected: boolean;
/** 权限类型 */
type?: 'MENU' | 'BUTTON' | 'OTHER';
/** typeInfo */
typeInfo?: acl.Codebook;
/** updatedTime */
updatedTime?: string;
}
/**
* 角色
*/
export interface Role {
/** createdTime */
createdTime?: string;
/** 角色描述 */
description?: string;
/** id */
id?: number;
/** 角色key,英文,用来做业务 */
key: string;
/** 角色名称,中文用来做标识 */
name?: string;
/** updatedTime */
updatedTime?: string;
}
/**
* 角色信息,包含是否选中标识
*/
export interface RoleInfo {
/** createdTime */
createdTime?: string;
/** 角色描述 */
description?: string;
/** id */
id?: number;
/** 角色key,英文,用来做业务 */
key: string;
/** 角色名称,中文用来做标识 */
name?: string;
/** 角色是否选中标识 */
selected: boolean;
/** updatedTime */
updatedTime?: string;
}
/**
* 树
*/
export interface TreeString {
/** 下级列表 */
children?: Array<acl.TreeString>;
/** 描述 */
description?: string;
/** ID */
key: string;
/** 名称 */
name: string;
/** originData */
originData: acl.TreeableString;
/** 父级 ID */
parentKey?: string;
}
/**
* 原始数据
*/
export interface TreeableString {
/** description */
description?: string;
/** key */
key?: string;
/** name */
name?: string;
/** parentKey */
parentKey?: string;
}
/**
* 用户
*/
export interface User {
/** createdTime */
createdTime?: string;
/** 邮箱 */
email?: string;
/** 真实姓名 */
fullName?: string;
/** id */
id?: number;
/** 手机号 */
mobile?: string;
/** 用户名 */
name: string;
/** 密码 */
password?: string;
/** 性别 */
sex?: 'MALE' | 'FEMALE';
/** sexInfo */
sexInfo?: acl.Codebook;
/** updatedTime */
updatedTime?: string;
}
}

11
src/api/acl/mods/index.ts Normal file
View File

@@ -0,0 +1,11 @@
import permission from './permission';
import role from './role';
import user from './user';
export default {
permission,
role,
user,
};

View File

@@ -0,0 +1,22 @@
/**
* @desc 增加权限
*/
import { defaultSuccess, defaultError, http } from '@/plugins/axios';
import type { AxiosResponse } from 'axios';
export default async function (
/** 请求体 */
requestBody: acl.Permission,
success: (data: acl.Permission) => void = defaultSuccess,
fail: (error: { code: string; error?: string }) => void = defaultError,
): Promise<void> {
return http({
method: 'post',
url: `/permission`,
data: requestBody,
})
.then((data: AxiosResponse<acl.Permission, unknown>) => {
success(data.data);
})
.catch((error: { code: string; error?: string }) => fail(error));
}

View File

@@ -0,0 +1,22 @@
/**
* @desc 批量新增权限
*/
import { defaultSuccess, defaultError, http } from '@/plugins/axios';
import type { AxiosResponse } from 'axios';
export default async function (
/** 请求体 */
requestBody: Array<acl.Permission>,
success: (data: void) => void = defaultSuccess,
fail: (error: { code: string; error?: string }) => void = defaultError,
): Promise<void> {
return http({
method: 'post',
url: `/batch-init-permissions`,
data: requestBody,
})
.then((data: AxiosResponse<void, unknown>) => {
success(data.data);
})
.catch((error: { code: string; error?: string }) => fail(error));
}

View File

@@ -0,0 +1,22 @@
/**
* @desc 增量新增权限
*/
import { defaultSuccess, defaultError, http } from '@/plugins/axios';
import type { AxiosResponse } from 'axios';
export default async function (
/** 请求体 */
requestBody: Array<acl.Permission>,
success: (data: void) => void = defaultSuccess,
fail: (error: { code: string; error?: string }) => void = defaultError,
): Promise<void> {
return http({
method: 'patch',
url: `/batch-sync-permissions`,
data: requestBody,
})
.then((data: AxiosResponse<void, unknown>) => {
success(data.data);
})
.catch((error: { code: string; error?: string }) => fail(error));
}

View File

@@ -0,0 +1,21 @@
/**
* @desc 删除权限
*/
import { defaultSuccess, defaultError, http } from '@/plugins/axios';
import type { AxiosResponse } from 'axios';
export default async function (
/** 权限key */
key: string,
success: (data: void) => void = defaultSuccess,
fail: (error: { code: string; error?: string }) => void = defaultError,
): Promise<void> {
return http({
method: 'delete',
url: `/permission/${key}`,
})
.then((data: AxiosResponse<void, unknown>) => {
success(data.data);
})
.catch((error: { code: string; error?: string }) => fail(error));
}

View File

@@ -0,0 +1,21 @@
/**
* @desc 权限详情
*/
import { defaultSuccess, defaultError, http } from '@/plugins/axios';
import type { AxiosResponse } from 'axios';
export default async function (
/** 权限id */
id: number,
success: (data: acl.Permission) => void = defaultSuccess,
fail: (error: { code: string; error?: string }) => void = defaultError,
): Promise<void> {
return http({
method: 'get',
url: `/permission/${id}`,
})
.then((data: AxiosResponse<acl.Permission, unknown>) => {
success(data.data);
})
.catch((error: { code: string; error?: string }) => fail(error));
}

View File

@@ -0,0 +1,25 @@
/**
* @description 权限
*
*/
import batchInitPermissions from './batchInitPermissions';
import batchSyncPermissions from './batchSyncPermissions';
import add from './add';
import update from './update';
import types from './types';
import detail from './detail';
import deletePermission from './deletePermission';
import permissions from './permissions';
import permissionTree from './permissionTree';
export default {
batchInitPermissions,
batchSyncPermissions,
add,
update,
types,
detail,
deletePermission,
permissions,
permissionTree,
};

View File

@@ -0,0 +1,26 @@
/**
* @desc 权限树
*/
import { defaultSuccess, defaultError, http } from '@/plugins/axios';
import type { AxiosResponse } from 'axios';
export interface Params {
/** 强制拉取 */
force?: boolean;
}
export default async function (
params: Params,
success: (data: Array<acl.TreeString>) => void = defaultSuccess,
fail: (error: { code: string; error?: string }) => void = defaultError,
): Promise<void> {
return http({
method: 'get',
url: `/permissions/tree`,
params,
})
.then((data: AxiosResponse<Array<acl.TreeString>, unknown>) => {
success(data.data);
})
.catch((error: { code: string; error?: string }) => fail(error));
}

View File

@@ -0,0 +1,26 @@
/**
* @desc 权限列表
*/
import { defaultSuccess, defaultError, http } from '@/plugins/axios';
import type { AxiosResponse } from 'axios';
export interface Params {
/** 强制拉取 */
force?: boolean;
}
export default async function (
params: Params,
success: (data: Array<acl.Permission>) => void = defaultSuccess,
fail: (error: { code: string; error?: string }) => void = defaultError,
): Promise<void> {
return http({
method: 'get',
url: `/permissions`,
params,
})
.then((data: AxiosResponse<Array<acl.Permission>, unknown>) => {
success(data.data);
})
.catch((error: { code: string; error?: string }) => fail(error));
}

View File

@@ -0,0 +1,20 @@
/**
* @desc 权限类型
*/
import { defaultSuccess, defaultError, http } from '@/plugins/axios';
import type { AxiosResponse } from 'axios';
import type { Codebook } from '@/api/api';
export default async function (
success: (data: Array<Codebook>) => void = defaultSuccess,
fail: (error: { code: string; error?: string }) => void = defaultError,
): Promise<void> {
return http({
method: 'get',
url: `/permission/types`,
})
.then((data: AxiosResponse<Array<Codebook>, unknown>) => {
success(data.data);
})
.catch((error: { code: string; error?: string }) => fail(error));
}

View File

@@ -0,0 +1,22 @@
/**
* @desc 更新权限
*/
import { defaultSuccess, defaultError, http } from '@/plugins/axios';
import type { AxiosResponse } from 'axios';
export default async function (
/** 请求体 */
requestBody: acl.Permission,
success: (data: acl.Permission) => void = defaultSuccess,
fail: (error: { code: string; error?: string }) => void = defaultError,
): Promise<void> {
return http({
method: 'patch',
url: `/permission`,
data: requestBody,
})
.then((data: AxiosResponse<acl.Permission, unknown>) => {
success(data.data);
})
.catch((error: { code: string; error?: string }) => fail(error));
}

View File

@@ -0,0 +1,21 @@
/**
* @desc 删除角色
*/
import { defaultSuccess, defaultError, http } from '@/plugins/axios';
import type { AxiosResponse } from 'axios';
export default async function (
/** 角色key */
key: string,
success: (data: void) => void = defaultSuccess,
fail: (error: { code: string; error?: string }) => void = defaultError,
): Promise<void> {
return http({
method: 'delete',
url: `/role/${key}`,
})
.then((data: AxiosResponse<void, unknown>) => {
success(data.data);
})
.catch((error: { code: string; error?: string }) => fail(error));
}

View File

@@ -0,0 +1,21 @@
/**
* @desc 角色详情
*/
import { defaultSuccess, defaultError, http } from '@/plugins/axios';
import type { AxiosResponse } from 'axios';
export default async function (
/** 角色id */
id: number,
success: (data: acl.Role) => void = defaultSuccess,
fail: (error: { code: string; error?: string }) => void = defaultError,
): Promise<void> {
return http({
method: 'get',
url: `/role/${id}`,
})
.then((data: AxiosResponse<acl.Role, unknown>) => {
success(data.data);
})
.catch((error: { code: string; error?: string }) => fail(error));
}

View File

@@ -0,0 +1,25 @@
/**
* @desc 为指定角色授权
*/
import { defaultSuccess, defaultError, http } from '@/plugins/axios';
import type { AxiosResponse } from 'axios';
export default async function (
/** 角色key */
key: string,
/** 请求体 */
requestBody: Array<string>,
success: (data: void) => void = defaultSuccess,
fail: (error: { code: string; error?: string }) => void = defaultError,
): Promise<void> {
return http({
method: 'post',
url: `/role/${key}/permissions`,
data: requestBody,
})
.then((data: AxiosResponse<void, unknown>) => {
success(data.data);
})
.catch((error: { code: string; error?: string }) => fail(error));
}

View File

@@ -0,0 +1,21 @@
/**
* @description 角色
*
*/
import saveOrUpdateRole from './saveOrUpdateRole';
import detail from './detail';
import deleteRole from './deleteRole';
import permissionInfos from './permissionInfos';
import permissions from './permissions';
import grant from './grant';
import roles from './roles';
export default {
saveOrUpdateRole,
detail,
deleteRole,
permissionInfos,
permissions,
grant,
roles,
};

View File

@@ -0,0 +1,21 @@
/**
* @desc 查询用于授权的权限信息
*/
import { defaultSuccess, defaultError, http } from '@/plugins/axios';
import type { AxiosResponse } from 'axios';
export default async function (
/** 角色key */
key: string,
success: (data: Array<acl.PermissionInfo>) => void = defaultSuccess,
fail: (error: { code: string; error?: string }) => void = defaultError,
): Promise<void> {
return http({
method: 'get',
url: `/role/${key}/permission-infos`,
})
.then((data: AxiosResponse<Array<acl.PermissionInfo>, unknown>) => {
success(data.data);
})
.catch((error: { code: string; error?: string }) => fail(error));
}

View File

@@ -0,0 +1,21 @@
/**
* @desc 查询角色权限
*/
import { defaultSuccess, defaultError, http } from '@/plugins/axios';
import type { AxiosResponse } from 'axios';
export default async function (
/** 角色key */
key: string,
success: (data: Array<acl.Permission>) => void = defaultSuccess,
fail: (error: { code: string; error?: string }) => void = defaultError,
): Promise<void> {
return http({
method: 'get',
url: `/role/${key}/permissions`,
})
.then((data: AxiosResponse<Array<acl.Permission>, unknown>) => {
success(data.data);
})
.catch((error: { code: string; error?: string }) => fail(error));
}

View File

@@ -0,0 +1,31 @@
/**
* @desc 分页查询角色
*/
import { defaultSuccess, defaultError, http } from '@/plugins/axios';
import type { AxiosResponse } from 'axios';
import type { IPage } from '@/api/api';
export interface Params {
/** 页码 */
page?: number;
/** 页面大小 */
size?: number;
/** 搜索关键词 */
key?: string;
}
export default async function (
params: Params,
success: (data: IPage<acl.Role>) => void = defaultSuccess,
fail: (error: { code: string; error?: string }) => void = defaultError,
): Promise<void> {
return http({
method: 'get',
url: `/roles`,
params,
})
.then((data: AxiosResponse<IPage<acl.Role>, unknown>) => {
success(data.data);
})
.catch((error: { code: string; error?: string }) => fail(error));
}

View File

@@ -0,0 +1,22 @@
/**
* @desc 增加/编辑角色
*/
import { defaultSuccess, defaultError, http } from '@/plugins/axios';
import type { AxiosResponse } from 'axios';
export default async function (
/** 请求体 */
requestBody: acl.Role,
success: (data: acl.Role) => void = defaultSuccess,
fail: (error: { code: string; error?: string }) => void = defaultError,
): Promise<void> {
return http({
method: 'put',
url: `/role`,
data: requestBody,
})
.then((data: AxiosResponse<acl.Role, unknown>) => {
success(data.data);
})
.catch((error: { code: string; error?: string }) => fail(error));
}

View File

@@ -0,0 +1,21 @@
/**
* @desc 删除用户
*/
import { defaultSuccess, defaultError, http } from '@/plugins/axios';
import type { AxiosResponse } from 'axios';
export default async function (
/** 用户id */
id: number,
success: (data: void) => void = defaultSuccess,
fail: (error: { code: string; error?: string }) => void = defaultError,
): Promise<void> {
return http({
method: 'delete',
url: `/user/${id}`,
})
.then((data: AxiosResponse<void, unknown>) => {
success(data.data);
})
.catch((error: { code: string; error?: string }) => fail(error));
}

View File

@@ -0,0 +1,21 @@
/**
* @desc 用户详情
*/
import { defaultSuccess, defaultError, http } from '@/plugins/axios';
import type { AxiosResponse } from 'axios';
export default async function (
/** 用户id */
id: number,
success: (data: acl.User) => void = defaultSuccess,
fail: (error: { code: string; error?: string }) => void = defaultError,
): Promise<void> {
return http({
method: 'get',
url: `/user/${id}`,
})
.then((data: AxiosResponse<acl.User, unknown>) => {
success(data.data);
})
.catch((error: { code: string; error?: string }) => fail(error));
}

View File

@@ -0,0 +1,25 @@
/**
* @desc 为指定用户授权
*/
import { defaultSuccess, defaultError, http } from '@/plugins/axios';
import type { AxiosResponse } from 'axios';
export default async function (
/** 用户名 */
name: string,
/** 请求体 */
requestBody: Array<string>,
success: (data: void) => void = defaultSuccess,
fail: (error: { code: string; error?: string }) => void = defaultError,
): Promise<void> {
return http({
method: 'post',
url: `/user/${name}/permissions`,
data: requestBody,
})
.then((data: AxiosResponse<void, unknown>) => {
success(data.data);
})
.catch((error: { code: string; error?: string }) => fail(error));
}

View File

@@ -0,0 +1,25 @@
/**
* @desc 为指定用户设置角色
*/
import { defaultSuccess, defaultError, http } from '@/plugins/axios';
import type { AxiosResponse } from 'axios';
export default async function (
/** 用户名 */
name: string,
/** 请求体 */
requestBody: Array<string>,
success: (data: void) => void = defaultSuccess,
fail: (error: { code: string; error?: string }) => void = defaultError,
): Promise<void> {
return http({
method: 'post',
url: `/user/${name}/roles`,
data: requestBody,
})
.then((data: AxiosResponse<void, unknown>) => {
success(data.data);
})
.catch((error: { code: string; error?: string }) => fail(error));
}

View File

@@ -0,0 +1,29 @@
/**
* @description 用户
*
*/
import saveOrUpdateUser from './saveOrUpdateUser';
import sexes from './sexes';
import detail from './detail';
import deleteUser from './deleteUser';
import resetPassword from './resetPassword';
import permissionInfos from './permissionInfos';
import permissions from './permissions';
import grant from './grant';
import roleInfos from './roleInfos';
import grantRole from './grantRole';
import users from './users';
export default {
saveOrUpdateUser,
sexes,
detail,
deleteUser,
resetPassword,
permissionInfos,
permissions,
grant,
roleInfos,
grantRole,
users,
};

View File

@@ -0,0 +1,21 @@
/**
* @desc 查询用于授权的权限信息
*/
import { defaultSuccess, defaultError, http } from '@/plugins/axios';
import type { AxiosResponse } from 'axios';
export default async function (
/** 用户名 */
name: string,
success: (data: Array<acl.PermissionInfo>) => void = defaultSuccess,
fail: (error: { code: string; error?: string }) => void = defaultError,
): Promise<void> {
return http({
method: 'get',
url: `/user/${name}/permission-infos`,
})
.then((data: AxiosResponse<Array<acl.PermissionInfo>, unknown>) => {
success(data.data);
})
.catch((error: { code: string; error?: string }) => fail(error));
}

View File

@@ -0,0 +1,21 @@
/**
* @desc 查询用户权限
*/
import { defaultSuccess, defaultError, http } from '@/plugins/axios';
import type { AxiosResponse } from 'axios';
export default async function (
/** 用户名 */
name: string,
success: (data: Array<acl.Permission>) => void = defaultSuccess,
fail: (error: { code: string; error?: string }) => void = defaultError,
): Promise<void> {
return http({
method: 'get',
url: `/user/${name}/permissions`,
})
.then((data: AxiosResponse<Array<acl.Permission>, unknown>) => {
success(data.data);
})
.catch((error: { code: string; error?: string }) => fail(error));
}

View File

@@ -0,0 +1,21 @@
/**
* @desc 重置密码
*/
import { defaultSuccess, defaultError, http } from '@/plugins/axios';
import type { AxiosResponse } from 'axios';
export default async function (
/** 用户名 */
name: string,
success: (data: string) => void = defaultSuccess,
fail: (error: { code: string; error?: string }) => void = defaultError,
): Promise<void> {
return http({
method: 'patch',
url: `/user/${name}/password`,
})
.then((data: AxiosResponse<string, unknown>) => {
success(data.data);
})
.catch((error: { code: string; error?: string }) => fail(error));
}

View File

@@ -0,0 +1,21 @@
/**
* @desc 查询用户权限
*/
import { defaultSuccess, defaultError, http } from '@/plugins/axios';
import type { AxiosResponse } from 'axios';
export default async function (
/** 用户名 */
name: string,
success: (data: Array<acl.RoleInfo>) => void = defaultSuccess,
fail: (error: { code: string; error?: string }) => void = defaultError,
): Promise<void> {
return http({
method: 'get',
url: `/user/${name}/role-infos`,
})
.then((data: AxiosResponse<Array<acl.RoleInfo>, unknown>) => {
success(data.data);
})
.catch((error: { code: string; error?: string }) => fail(error));
}

View File

@@ -0,0 +1,22 @@
/**
* @desc 增加/编辑用户
*/
import { defaultSuccess, defaultError, http } from '@/plugins/axios';
import type { AxiosResponse } from 'axios';
export default async function (
/** 请求体 */
requestBody: acl.User,
success: (data: acl.User) => void = defaultSuccess,
fail: (error: { code: string; error?: string }) => void = defaultError,
): Promise<void> {
return http({
method: 'put',
url: `/user`,
data: requestBody,
})
.then((data: AxiosResponse<acl.User, unknown>) => {
success(data.data);
})
.catch((error: { code: string; error?: string }) => fail(error));
}

View File

@@ -0,0 +1,20 @@
/**
* @desc 用户性别
*/
import { defaultSuccess, defaultError, http } from '@/plugins/axios';
import type { AxiosResponse } from 'axios';
import type { Codebook } from '@/api/api';
export default async function (
success: (data: Array<Codebook>) => void = defaultSuccess,
fail: (error: { code: string; error?: string }) => void = defaultError,
): Promise<void> {
return http({
method: 'get',
url: `/user/sexes`,
})
.then((data: AxiosResponse<Array<Codebook>, unknown>) => {
success(data.data);
})
.catch((error: { code: string; error?: string }) => fail(error));
}

View File

@@ -0,0 +1,33 @@
/**
* @desc 分页查询用户
*/
import { defaultSuccess, defaultError, http } from '@/plugins/axios';
import type { AxiosResponse } from 'axios';
import type { IPage } from '@/api/api';
export interface Params {
/** 页码 */
page?: number;
/** 页面大小 */
size?: number;
/** 性别 */
sex?: 'MALE' | 'FEMALE';
/** 搜索关键词 */
key?: string;
}
export default async function (
params: Params,
success: (data: IPage<acl.User>) => void = defaultSuccess,
fail: (error: { code: string; error?: string }) => void = defaultError,
): Promise<void> {
return http({
method: 'get',
url: `/users`,
params,
})
.then((data: AxiosResponse<IPage<acl.User>, unknown>) => {
success(data.data);
})
.catch((error: { code: string; error?: string }) => fail(error));
}

4329
src/api/api-lock.json Normal file

File diff suppressed because it is too large Load Diff

118
src/api/api.d.ts vendored Normal file
View File

@@ -0,0 +1,118 @@
/**
* mybatis-plus 分页对象
*/
export interface IPage<T> {
/**
* 当前页面
*/
current: number;
/**
* 总页数
*/
pages?: number;
/**
* 数据记录
*/
records?: Array<T>;
/**
* 分页大小
*/
size: number;
/**
* 数据总条数
*/
total?: number;
}
/**
* Nutz 分页对象
*/
export interface Pagination<T> {
/**
* 数据记录
*/
dataList: Array<T>;
/**
* 是否首页
*/
first?: boolean;
/**
* 是否尾页
*/
last?: boolean;
/**
* 偏移量
*/
offset?: number;
/**
* 总页数
*/
pageCount: number;
/**
* 当前页码
*/
pageNumber: number;
/**
* 页面大小
*/
pageSize: number;
/**
* sql参数
*/
paras: Record<string, unknown>;
/**
* 数据记录总数
*/
recordCount?: number;
}
/**
* VXE 数据结构
*/
export interface VXETableSaveDTO<T> {
/**
* 新增记录数据
*/
insertRecords: Array<T>;
/**
* 删除记录数据
*/
removeRecords: Array<T>;
/**
* 更新记录数据
*/
updateRecords: Array<T>;
}
/**
* 枚举码本
*/
export interface Codebook {
/** 码本编码 */
code: string;
/** 码本名称 */
description: string;
/** 码本值 */
name: string;
}
/**
* 全局错误
*/
export interface GlobalError {
/** 错误码 */
code?: number;
/** 错误信息 */
message?: string;
}
/**
* NutMap
*/
type NutMap<Key extends string, Value = unknown> = {
[key in Key]: Value;
};
// 导入其他模块
/// <reference path="./auth/api.d.ts" />
/// <reference path="./acl/api.d.ts" />
/// <reference path="./dictionary/api.d.ts" />
/// <reference path="./material/api.d.ts" />

71
src/api/auth/api.d.ts vendored Normal file
View File

@@ -0,0 +1,71 @@
declare namespace auth {
/**
* AuthUser
*/
export interface AuthUser {
/** anonymous */
anonymous?: boolean;
/** 用户头像 */
avatar?: string;
/** 邮箱 */
email?: string;
/** extInfo */
extInfo?: auth.NutMap;
/** 姓名 */
fullName: string;
/** 电话 */
mobile?: string;
/** 密码 */
password: string;
/** 权限列表 */
permissions: Array<string>;
/** jwt refreshToken */
refreshToken: string;
/** 角色列表 */
roles: Array<string>;
/** 性别 */
sex?: 'MALE' | 'FEMALE';
/** sexInfo */
sexInfo?: auth.Codebook;
/** jwt Token */
token: string;
/** 用户名 */
userName: string;
}
/**
* 登录实体
*/
export interface LoginDTO {
/** 验证码 */
captcha?: string;
/** 手机号 */
mobile?: string;
/** 密码 */
password?: string;
/** 登录类型 */
type: 'ACCOUNT' | 'WECHAT';
/** 用户名 */
userName?: string;
/** uuid */
uuid?: string;
}
}

View File

@@ -0,0 +1,18 @@
/**
* @desc 当前用户
*/
import { defaultSuccess, defaultError, http } from '@/plugins/axios';
import type { AxiosResponse } from 'axios';
export default async function (
success: (data: auth.AuthUser) => void = defaultSuccess,
fail: (error: { code: string; error?: string }) => void = defaultError,
): Promise<void> {
return http({
method: 'get',
url: `/auth/current-user`,
})
.then((data: AxiosResponse<auth.AuthUser, unknown>) => {
success(data.data);
})
.catch((error: { code: string; error?: string }) => fail(error));
}

View File

@@ -0,0 +1,13 @@
/**
* @description 登录
*
*/
import currentUser from './currentUser';
import login from './login';
import logout from './logout';
export default {
currentUser,
login,
logout,
};

View File

@@ -0,0 +1,22 @@
/**
* @desc 登录
*/
import { defaultSuccess, defaultError, http } from '@/plugins/axios';
import type { AxiosResponse } from 'axios';
export default async function (
/** 请求体 */
requestBody: auth.LoginDTO,
success: (data: auth.AuthUser) => void = defaultSuccess,
fail: (error: { code: string; error?: string }) => void = defaultError,
): Promise<void> {
return http({
method: 'post',
url: `/auth/login`,
data: requestBody,
})
.then((data: AxiosResponse<auth.AuthUser, unknown>) => {
success(data.data);
})
.catch((error: { code: string; error?: string }) => fail(error));
}

View File

@@ -0,0 +1,18 @@
/**
* @desc 登出
*/
import { defaultSuccess, defaultError, http } from '@/plugins/axios';
import type { AxiosResponse } from 'axios';
export default async function (
success: (data: void) => void = defaultSuccess,
fail: (error: { code: string; error?: string }) => void = defaultError,
): Promise<void> {
return http({
method: 'post',
url: `/auth/logout`,
})
.then((data: AxiosResponse<void, unknown>) => {
success(data.data);
})
.catch((error: { code: string; error?: string }) => fail(error));
}

View File

@@ -0,0 +1,5 @@
import auth from './auth';
export default {
auth,
};

62
src/api/dictionary/api.d.ts vendored Normal file
View File

@@ -0,0 +1,62 @@
declare namespace dictionary {
/**
* 码本数据
*/
export interface Dictionary {
/** createdTime */
createdTime?: string;
/** 描述 */
description?: string;
/** 禁用标识 */
disabled?: boolean;
/** 分组Key */
groupKey?: string;
/** id */
id?: number;
/** 序号 */
index?: number;
/** key */
key?: string;
/** 上级Key */
parentKey?: string;
/** updatedTime */
updatedTime?: string;
/** value */
value?: string;
}
/**
* 码本分组
*/
export interface Group {
/** createdTime */
createdTime?: string;
/** 分组描述 */
description?: string;
/** 禁用标识 */
disabled?: boolean;
/** id */
id?: number;
/** 分组唯一键 */
key: string;
/** 分组名称 */
name?: string;
/** updatedTime */
updatedTime?: string;
}
}

View File

@@ -0,0 +1,23 @@
/**
* @desc 删除码本数据
*/
import { defaultSuccess, defaultError, http } from '@/plugins/axios';
import type { AxiosResponse } from 'axios';
export default async function (
/** 码本分组key */
group: string,
/** 码本数据key */
key: string,
success: (data: void) => void = defaultSuccess,
fail: (error: { code: string; error?: string }) => void = defaultError,
): Promise<void> {
return http({
method: 'delete',
url: `/group/${group}/dictionary/${key}`,
})
.then((data: AxiosResponse<void, unknown>) => {
success(data.data);
})
.catch((error: { code: string; error?: string }) => fail(error));
}

View File

@@ -0,0 +1,21 @@
/**
* @desc 删除码本分组
*/
import { defaultSuccess, defaultError, http } from '@/plugins/axios';
import type { AxiosResponse } from 'axios';
export default async function (
/** 码本分组key */
key: string,
success: (data: void) => void = defaultSuccess,
fail: (error: { code: string; error?: string }) => void = defaultError,
): Promise<void> {
return http({
method: 'delete',
url: `/group/${key}`,
})
.then((data: AxiosResponse<void, unknown>) => {
success(data.data);
})
.catch((error: { code: string; error?: string }) => fail(error));
}

View File

@@ -0,0 +1,21 @@
/**
* @desc 分组下的数据字典列表
*/
import { defaultSuccess, defaultError, http } from '@/plugins/axios';
import type { AxiosResponse } from 'axios';
export default async function (
/** 码本分组key */
group: string,
success: (data: Array<dictionary.Dictionary>) => void = defaultSuccess,
fail: (error: { code: string; error?: string }) => void = defaultError,
): Promise<void> {
return http({
method: 'get',
url: `/group/${group}/dictionaries`,
})
.then((data: AxiosResponse<Array<dictionary.Dictionary>, unknown>) => {
success(data.data);
})
.catch((error: { code: string; error?: string }) => fail(error));
}

View File

@@ -0,0 +1,21 @@
/**
* @desc 码本分组详情
*/
import { defaultSuccess, defaultError, http } from '@/plugins/axios';
import type { AxiosResponse } from 'axios';
export default async function (
/** 码本分组id */
id: number,
success: (data: dictionary.Group) => void = defaultSuccess,
fail: (error: { code: string; error?: string }) => void = defaultError,
): Promise<void> {
return http({
method: 'get',
url: `/group/${id}`,
})
.then((data: AxiosResponse<dictionary.Group, unknown>) => {
success(data.data);
})
.catch((error: { code: string; error?: string }) => fail(error));
}

View File

@@ -0,0 +1,31 @@
/**
* @desc 分页查询码本分组
*/
import { defaultSuccess, defaultError, http } from '@/plugins/axios';
import type { AxiosResponse } from 'axios';
import type { IPage } from '@/api/api';
export interface Params {
/** 页码 */
page?: number;
/** 页面大小 */
size?: number;
/** 搜索关键词 */
key?: string;
}
export default async function (
params: Params,
success: (data: IPage<dictionary.Group>) => void = defaultSuccess,
fail: (error: { code: string; error?: string }) => void = defaultError,
): Promise<void> {
return http({
method: 'get',
url: `/groups`,
params,
})
.then((data: AxiosResponse<IPage<dictionary.Group>, unknown>) => {
success(data.data);
})
.catch((error: { code: string; error?: string }) => fail(error));
}

View File

@@ -0,0 +1,21 @@
/**
* @description 码本数据
*
*/
import saveOrUpdateGroup from './saveOrUpdateGroup';
import dictionaries from './dictionaries';
import saveOrUpdateDictionary from './saveOrUpdateDictionary';
import deleteDictionary from './deleteDictionary';
import groupDetail from './groupDetail';
import deleteGroup from './deleteGroup';
import groups from './groups';
export default {
saveOrUpdateGroup,
dictionaries,
saveOrUpdateDictionary,
deleteDictionary,
groupDetail,
deleteGroup,
groups,
};

View File

@@ -0,0 +1,25 @@
/**
* @desc 增加/编辑码本数据
*/
import { defaultSuccess, defaultError, http } from '@/plugins/axios';
import type { AxiosResponse } from 'axios';
export default async function (
/** 码本分组key */
group: string,
/** 请求体 */
requestBody: dictionary.Dictionary,
success: (data: dictionary.Dictionary) => void = defaultSuccess,
fail: (error: { code: string; error?: string }) => void = defaultError,
): Promise<void> {
return http({
method: 'put',
url: `/group/${group}/dictionary`,
data: requestBody,
})
.then((data: AxiosResponse<dictionary.Dictionary, unknown>) => {
success(data.data);
})
.catch((error: { code: string; error?: string }) => fail(error));
}

View File

@@ -0,0 +1,22 @@
/**
* @desc 增加/编辑码本分组
*/
import { defaultSuccess, defaultError, http } from '@/plugins/axios';
import type { AxiosResponse } from 'axios';
export default async function (
/** 请求体 */
requestBody: dictionary.Group,
success: (data: dictionary.Group) => void = defaultSuccess,
fail: (error: { code: string; error?: string }) => void = defaultError,
): Promise<void> {
return http({
method: 'put',
url: `/group`,
data: requestBody,
})
.then((data: AxiosResponse<dictionary.Group, unknown>) => {
success(data.data);
})
.catch((error: { code: string; error?: string }) => fail(error));
}

View File

@@ -0,0 +1,5 @@
import dictionary from './dictionary';
export default {
dictionary,
};

19
src/api/index.ts Normal file
View File

@@ -0,0 +1,19 @@
import type { App } from 'vue';
import authApi from './auth/mods';
import aclApi from './acl/mods';
import dictionaryApi from './dictionary/mods';
import materialApi from './material/mods';
export const api = {
authApi,
aclApi,
dictionaryApi,
materialApi,
install: (app: App) => {
app.config.globalProperties.$api = api;
},
};
export default api;

105
src/api/material/api.d.ts vendored Normal file
View File

@@ -0,0 +1,105 @@
declare namespace material {
/**
* 申请单明细
*/
export interface ApplyDetail {
/** 申请单ID */
applyId?: number;
/** 确认数量 */
confirmQuantity?: number;
/** createdTime */
createdTime?: string;
/** 异常情况说明 */
exceptionRemark?: string;
/** id */
id?: number;
/** 物料Id */
materialId?: number;
/** 申请数量 */
quantity?: number;
/** updatedTime */
updatedTime?: string;
}
/**
* 入库/出库 申请单
*/
export interface ApplyForm {
/** 申请人 */
applicant?: string;
/** 申请日期 */
applyDate?: string;
/** createdTime */
createdTime?: string;
/** 异常说明 */
exceptionExplain?: string;
/** id */
id?: number;
/** 是否确认 */
isConfirm?: boolean;
/** 类型 */
type?: number;
/** updatedTime */
updatedTime?: string;
}
/**
* 申请单
*/
export interface ApplyInfo {
/** applyDetails */
applyDetails?: Array<material.ApplyDetail>;
/** applyForm */
applyForm?: material.ApplyForm;
}
/**
* 物料信息
*/
export interface Material {
/** 赋码规则 */
assignRule?: number;
/** 编码 */
code?: string;
/** createdTime */
createdTime?: string;
/** 备注 */
description?: string;
/** id */
id?: number;
/** 名称 */
name?: string;
/** 规格 */
spec?: string;
/** 库存数量 */
stock?: number;
/** 类型 */
type?: string;
/** updatedTime */
updatedTime?: string;
}
}

View File

@@ -0,0 +1,33 @@
/**
* @desc 分页查询申请单列表
*/
import { defaultSuccess, defaultError, http } from '@/plugins/axios';
import type { AxiosResponse } from 'axios';
import type { IPage } from '@/api/api';
export interface Params {
/** 类型 */
type: number;
/** 页码 */
page?: number;
/** 页面大小 */
size?: number;
/** 搜索关键词 */
key?: string;
}
export default async function (
params: Params,
success: (data: IPage<material.ApplyForm>) => void = defaultSuccess,
fail: (error: { code: string; error?: string }) => void = defaultError,
): Promise<void> {
return http({
method: 'get',
url: `/apply`,
params,
})
.then((data: AxiosResponse<IPage<material.ApplyForm>, unknown>) => {
success(data.data);
})
.catch((error: { code: string; error?: string }) => fail(error));
}

View File

@@ -0,0 +1,21 @@
/**
* @desc 查询申请单详情
*/
import { defaultSuccess, defaultError, http } from '@/plugins/axios';
import type { AxiosResponse } from 'axios';
export default async function (
/** 类型 */
applyId: number,
success: (data: material.ApplyInfo) => void = defaultSuccess,
fail: (error: { code: string; error?: string }) => void = defaultError,
): Promise<void> {
return http({
method: 'get',
url: `/apply/${applyId}`,
})
.then((data: AxiosResponse<material.ApplyInfo, unknown>) => {
success(data.data);
})
.catch((error: { code: string; error?: string }) => fail(error));
}

View File

@@ -0,0 +1,13 @@
/**
* @description 入库/出库 申请单管理
*
*/
import applies from './applies';
import saveOrUpdateApply from './saveOrUpdateApply';
import detail from './detail';
export default {
applies,
saveOrUpdateApply,
detail,
};

View File

@@ -0,0 +1,34 @@
/**
* @desc 分页查询申请单列表
*/
import { defaultSuccess, defaultError, http } from '@/plugins/axios';
import type { AxiosResponse } from 'axios';
import type { IPage } from '@/api/api';
export interface Params {
/** 页码 */
page?: number;
/** 页面大小 */
size?: number;
/** 搜索关键词 */
key?: string;
}
export default async function (
/** 类型 */
type: number,
params: Params,
success: (data: IPage<material.ApplyForm>) => void = defaultSuccess,
fail: (error: { code: string; error?: string }) => void = defaultError,
): Promise<void> {
return http({
method: 'get',
url: `/apply/${type}`,
params,
})
.then((data: AxiosResponse<IPage<material.ApplyForm>, unknown>) => {
success(data.data);
})
.catch((error: { code: string; error?: string }) => fail(error));
}

View File

@@ -0,0 +1,22 @@
/**
* @desc 增加/编辑申请单
*/
import { defaultSuccess, defaultError, http } from '@/plugins/axios';
import type { AxiosResponse } from 'axios';
export default async function (
/** 请求体 */
requestBody: material.ApplyInfo,
success: (data: void) => void = defaultSuccess,
fail: (error: { code: string; error?: string }) => void = defaultError,
): Promise<void> {
return http({
method: 'put',
url: `/apply`,
data: requestBody,
})
.then((data: AxiosResponse<void, unknown>) => {
success(data.data);
})
.catch((error: { code: string; error?: string }) => fail(error));
}

View File

@@ -0,0 +1,8 @@
import apply from './apply';
import material from './material';
export default {
apply,
material,
};

View File

@@ -0,0 +1,18 @@
/**
* @desc 查询所有物料列表
*/
import { defaultSuccess, defaultError, http } from '@/plugins/axios';
import type { AxiosResponse } from 'axios';
export default async function (
success: (data: Array<material.Material>) => void = defaultSuccess,
fail: (error: { code: string; error?: string }) => void = defaultError,
): Promise<void> {
return http({
method: 'get',
url: `/material/list`,
})
.then((data: AxiosResponse<Array<material.Material>, unknown>) => {
success(data.data);
})
.catch((error: { code: string; error?: string }) => fail(error));
}

View File

@@ -0,0 +1,21 @@
/**
* @desc 删除物料
*/
import { defaultSuccess, defaultError, http } from '@/plugins/axios';
import type { AxiosResponse } from 'axios';
export default async function (
/** 物料id */
id: number,
success: (data: void) => void = defaultSuccess,
fail: (error: { code: string; error?: string }) => void = defaultError,
): Promise<void> {
return http({
method: 'delete',
url: `/material/${id}`,
})
.then((data: AxiosResponse<void, unknown>) => {
success(data.data);
})
.catch((error: { code: string; error?: string }) => fail(error));
}

View File

@@ -0,0 +1,21 @@
/**
* @desc 删除物料
*/
import { defaultSuccess, defaultError, http } from '@/plugins/axios';
import type { AxiosResponse } from 'axios';
export default async function (
/** 物料id */
id: number,
success: (data: void) => void = defaultSuccess,
fail: (error: { code: string; error?: string }) => void = defaultError,
): Promise<void> {
return http({
method: 'delete',
url: `/material/${id}`,
})
.then((data: AxiosResponse<void, unknown>) => {
success(data.data);
})
.catch((error: { code: string; error?: string }) => fail(error));
}

View File

@@ -0,0 +1,21 @@
/**
* @desc 物料详情
*/
import { defaultSuccess, defaultError, http } from '@/plugins/axios';
import type { AxiosResponse } from 'axios';
export default async function (
/** 物料id */
id: number,
success: (data: material.Material) => void = defaultSuccess,
fail: (error: { code: string; error?: string }) => void = defaultError,
): Promise<void> {
return http({
method: 'get',
url: `/material/${id}`,
})
.then((data: AxiosResponse<material.Material, unknown>) => {
success(data.data);
})
.catch((error: { code: string; error?: string }) => fail(error));
}

View File

@@ -0,0 +1,17 @@
/**
* @description 物料管理
*
*/
import saveOrUpdateMaterial from './saveOrUpdateMaterial';
import all from './all';
import detail from './detail';
import deleteMaterial from './deleteMaterial';
import materials from './materials';
export default {
saveOrUpdateMaterial,
all,
detail,
deleteMaterial,
materials,
};

View File

@@ -0,0 +1,33 @@
/**
* @desc 分页查询物料列表
*/
import { defaultSuccess, defaultError, http } from '@/plugins/axios';
import type { AxiosResponse } from 'axios';
import type { IPage } from '@/api/api';
export interface Params {
/** 页码 */
page?: number;
/** 页面大小 */
size?: number;
/** 类型 */
type?: number;
/** 搜索关键词 */
key?: string;
}
export default async function (
params: Params,
success: (data: IPage<material.Material>) => void = defaultSuccess,
fail: (error: { code: string; error?: string }) => void = defaultError,
): Promise<void> {
return http({
method: 'get',
url: `/materials`,
params,
})
.then((data: AxiosResponse<IPage<material.Material>, unknown>) => {
success(data.data);
})
.catch((error: { code: string; error?: string }) => fail(error));
}

View File

@@ -0,0 +1,22 @@
/**
* @desc 增加/编辑物料
*/
import { defaultSuccess, defaultError, http } from '@/plugins/axios';
import type { AxiosResponse } from 'axios';
export default async function (
/** 请求体 */
requestBody: material.Material,
success: (data: material.Material) => void = defaultSuccess,
fail: (error: { code: string; error?: string }) => void = defaultError,
): Promise<void> {
return http({
method: 'put',
url: `/material`,
data: requestBody,
})
.then((data: AxiosResponse<material.Material, unknown>) => {
success(data.data);
})
.catch((error: { code: string; error?: string }) => fail(error));
}

72
src/api/pontCore.ts Normal file
View File

@@ -0,0 +1,72 @@
/**
* @description pont内置请求单例
*/
class PontCoreManager {
static singleInstance = null as PontCoreManager;
static getSignleInstance() {
if (!PontCoreManager.singleInstance) {
PontCoreManager.singleInstance = new PontCoreManager();
return PontCoreManager.singleInstance;
}
return PontCoreManager.singleInstance;
}
/**
* fetch请求
* @param url 请求url
* @param options fetch 请求配置
*/
fetch(url: string, options = {}) {
return fetch(url, options).then((res) => {
return res.json();
});
}
/**
* 使用外部传入的请求方法替换默认的fetch请求
*/
useFetch(fetch: (url: string, options?: any) => Promise<any>) {
if (typeof fetch !== 'function') {
console.error('fetch should be a function ');
return;
}
this.fetch = fetch;
}
getUrl(path: string, queryParams: any, method: string) {
const params = {
...(queryParams || ({} as any)),
};
const url = path.replace(/\{([^\\}]*(?:\\.[^\\}]*)*)\}/gm, (match, key) => {
// eslint-disable-next-line no-param-reassign
key = key.trim();
if (params[key] !== undefined) {
const value = params[key];
delete params[key];
return value;
}
console.warn('Please set value for template key: ', key);
return '';
});
const paramStr = Object.keys(params)
.map((key) => {
return params[key] === undefined ? '' : `${key}=${params[key]}`;
})
.filter((id) => id)
.join('&');
if (paramStr) {
return `${url}?${paramStr}`;
}
return url;
}
}
export const PontCore = PontCoreManager.getSignleInstance();

BIN
src/assets/PDF.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

BIN
src/assets/WORD.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

BIN
src/assets/crun.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
src/assets/excel.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

BIN
src/assets/iam.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

BIN
src/assets/image.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

BIN
src/assets/login-bg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 297 KiB

BIN
src/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

1
src/assets/vue.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

After

Width:  |  Height:  |  Size: 496 B

297
src/auto-imports.d.ts vendored Normal file
View File

@@ -0,0 +1,297 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
// biome-ignore lint: disable
export {}
declare global {
const EffectScope: typeof import('vue')['EffectScope']
const asyncComputed: typeof import('@vueuse/core')['asyncComputed']
const autoResetRef: typeof import('@vueuse/core')['autoResetRef']
const computed: typeof import('vue')['computed']
const computedAsync: typeof import('@vueuse/core')['computedAsync']
const computedEager: typeof import('@vueuse/core')['computedEager']
const computedInject: typeof import('@vueuse/core')['computedInject']
const computedWithControl: typeof import('@vueuse/core')['computedWithControl']
const controlledComputed: typeof import('@vueuse/core')['controlledComputed']
const controlledRef: typeof import('@vueuse/core')['controlledRef']
const createApp: typeof import('vue')['createApp']
const createEventHook: typeof import('@vueuse/core')['createEventHook']
const createGlobalState: typeof import('@vueuse/core')['createGlobalState']
const createInjectionState: typeof import('@vueuse/core')['createInjectionState']
const createReactiveFn: typeof import('@vueuse/core')['createReactiveFn']
const createReusableTemplate: typeof import('@vueuse/core')['createReusableTemplate']
const createSharedComposable: typeof import('@vueuse/core')['createSharedComposable']
const createTemplatePromise: typeof import('@vueuse/core')['createTemplatePromise']
const createUnrefFn: typeof import('@vueuse/core')['createUnrefFn']
const customRef: typeof import('vue')['customRef']
const debouncedRef: typeof import('@vueuse/core')['debouncedRef']
const debouncedWatch: typeof import('@vueuse/core')['debouncedWatch']
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
const defineComponent: typeof import('vue')['defineComponent']
const eagerComputed: typeof import('@vueuse/core')['eagerComputed']
const effectScope: typeof import('vue')['effectScope']
const extendRef: typeof import('@vueuse/core')['extendRef']
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
const getCurrentScope: typeof import('vue')['getCurrentScope']
const h: typeof import('vue')['h']
const ignorableWatch: typeof import('@vueuse/core')['ignorableWatch']
const inject: typeof import('vue')['inject']
const injectLocal: typeof import('@vueuse/core')['injectLocal']
const isDefined: typeof import('@vueuse/core')['isDefined']
const isProxy: typeof import('vue')['isProxy']
const isReactive: typeof import('vue')['isReactive']
const isReadonly: typeof import('vue')['isReadonly']
const isRef: typeof import('vue')['isRef']
const makeDestructurable: typeof import('@vueuse/core')['makeDestructurable']
const markRaw: typeof import('vue')['markRaw']
const nextTick: typeof import('vue')['nextTick']
const onActivated: typeof import('vue')['onActivated']
const onBeforeMount: typeof import('vue')['onBeforeMount']
const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave']
const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate']
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
const onClickOutside: typeof import('@vueuse/core')['onClickOutside']
const onDeactivated: typeof import('vue')['onDeactivated']
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
const onKeyStroke: typeof import('@vueuse/core')['onKeyStroke']
const onLongPress: typeof import('@vueuse/core')['onLongPress']
const onMounted: typeof import('vue')['onMounted']
const onRenderTracked: typeof import('vue')['onRenderTracked']
const onRenderTriggered: typeof import('vue')['onRenderTriggered']
const onScopeDispose: typeof import('vue')['onScopeDispose']
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
const onStartTyping: typeof import('@vueuse/core')['onStartTyping']
const onUnmounted: typeof import('vue')['onUnmounted']
const onUpdated: typeof import('vue')['onUpdated']
const onWatcherCleanup: typeof import('vue')['onWatcherCleanup']
const pausableWatch: typeof import('@vueuse/core')['pausableWatch']
const provide: typeof import('vue')['provide']
const provideLocal: typeof import('@vueuse/core')['provideLocal']
const reactify: typeof import('@vueuse/core')['reactify']
const reactifyObject: typeof import('@vueuse/core')['reactifyObject']
const reactive: typeof import('vue')['reactive']
const reactiveComputed: typeof import('@vueuse/core')['reactiveComputed']
const reactiveOmit: typeof import('@vueuse/core')['reactiveOmit']
const reactivePick: typeof import('@vueuse/core')['reactivePick']
const readonly: typeof import('vue')['readonly']
const ref: typeof import('vue')['ref']
const refAutoReset: typeof import('@vueuse/core')['refAutoReset']
const refDebounced: typeof import('@vueuse/core')['refDebounced']
const refDefault: typeof import('@vueuse/core')['refDefault']
const refThrottled: typeof import('@vueuse/core')['refThrottled']
const refWithControl: typeof import('@vueuse/core')['refWithControl']
const resolveComponent: typeof import('vue')['resolveComponent']
const resolveRef: typeof import('@vueuse/core')['resolveRef']
const resolveUnref: typeof import('@vueuse/core')['resolveUnref']
const shallowReactive: typeof import('vue')['shallowReactive']
const shallowReadonly: typeof import('vue')['shallowReadonly']
const shallowRef: typeof import('vue')['shallowRef']
const syncRef: typeof import('@vueuse/core')['syncRef']
const syncRefs: typeof import('@vueuse/core')['syncRefs']
const templateRef: typeof import('@vueuse/core')['templateRef']
const throttledRef: typeof import('@vueuse/core')['throttledRef']
const throttledWatch: typeof import('@vueuse/core')['throttledWatch']
const toRaw: typeof import('vue')['toRaw']
const toReactive: typeof import('@vueuse/core')['toReactive']
const toRef: typeof import('vue')['toRef']
const toRefs: typeof import('vue')['toRefs']
const toValue: typeof import('vue')['toValue']
const triggerRef: typeof import('vue')['triggerRef']
const tryOnBeforeMount: typeof import('@vueuse/core')['tryOnBeforeMount']
const tryOnBeforeUnmount: typeof import('@vueuse/core')['tryOnBeforeUnmount']
const tryOnMounted: typeof import('@vueuse/core')['tryOnMounted']
const tryOnScopeDispose: typeof import('@vueuse/core')['tryOnScopeDispose']
const tryOnUnmounted: typeof import('@vueuse/core')['tryOnUnmounted']
const unref: typeof import('vue')['unref']
const unrefElement: typeof import('@vueuse/core')['unrefElement']
const until: typeof import('@vueuse/core')['until']
const useActiveElement: typeof import('@vueuse/core')['useActiveElement']
const useAnimate: typeof import('@vueuse/core')['useAnimate']
const useArrayDifference: typeof import('@vueuse/core')['useArrayDifference']
const useArrayEvery: typeof import('@vueuse/core')['useArrayEvery']
const useArrayFilter: typeof import('@vueuse/core')['useArrayFilter']
const useArrayFind: typeof import('@vueuse/core')['useArrayFind']
const useArrayFindIndex: typeof import('@vueuse/core')['useArrayFindIndex']
const useArrayFindLast: typeof import('@vueuse/core')['useArrayFindLast']
const useArrayIncludes: typeof import('@vueuse/core')['useArrayIncludes']
const useArrayJoin: typeof import('@vueuse/core')['useArrayJoin']
const useArrayMap: typeof import('@vueuse/core')['useArrayMap']
const useArrayReduce: typeof import('@vueuse/core')['useArrayReduce']
const useArraySome: typeof import('@vueuse/core')['useArraySome']
const useArrayUnique: typeof import('@vueuse/core')['useArrayUnique']
const useAsyncQueue: typeof import('@vueuse/core')['useAsyncQueue']
const useAsyncState: typeof import('@vueuse/core')['useAsyncState']
const useAttrs: typeof import('vue')['useAttrs']
const useBase64: typeof import('@vueuse/core')['useBase64']
const useBattery: typeof import('@vueuse/core')['useBattery']
const useBluetooth: typeof import('@vueuse/core')['useBluetooth']
const useBreakpoints: typeof import('@vueuse/core')['useBreakpoints']
const useBroadcastChannel: typeof import('@vueuse/core')['useBroadcastChannel']
const useBrowserLocation: typeof import('@vueuse/core')['useBrowserLocation']
const useCached: typeof import('@vueuse/core')['useCached']
const useClipboard: typeof import('@vueuse/core')['useClipboard']
const useClipboardItems: typeof import('@vueuse/core')['useClipboardItems']
const useCloned: typeof import('@vueuse/core')['useCloned']
const useColorMode: typeof import('@vueuse/core')['useColorMode']
const useConfirmDialog: typeof import('@vueuse/core')['useConfirmDialog']
const useCounter: typeof import('@vueuse/core')['useCounter']
const useCssModule: typeof import('vue')['useCssModule']
const useCssVar: typeof import('@vueuse/core')['useCssVar']
const useCssVars: typeof import('vue')['useCssVars']
const useCurrentElement: typeof import('@vueuse/core')['useCurrentElement']
const useCycleList: typeof import('@vueuse/core')['useCycleList']
const useDark: typeof import('@vueuse/core')['useDark']
const useDateFormat: typeof import('@vueuse/core')['useDateFormat']
const useDebounce: typeof import('@vueuse/core')['useDebounce']
const useDebounceFn: typeof import('@vueuse/core')['useDebounceFn']
const useDebouncedRefHistory: typeof import('@vueuse/core')['useDebouncedRefHistory']
const useDeviceMotion: typeof import('@vueuse/core')['useDeviceMotion']
const useDeviceOrientation: typeof import('@vueuse/core')['useDeviceOrientation']
const useDevicePixelRatio: typeof import('@vueuse/core')['useDevicePixelRatio']
const useDevicesList: typeof import('@vueuse/core')['useDevicesList']
const useDisplayMedia: typeof import('@vueuse/core')['useDisplayMedia']
const useDocumentVisibility: typeof import('@vueuse/core')['useDocumentVisibility']
const useDraggable: typeof import('@vueuse/core')['useDraggable']
const useDropZone: typeof import('@vueuse/core')['useDropZone']
const useElementBounding: typeof import('@vueuse/core')['useElementBounding']
const useElementByPoint: typeof import('@vueuse/core')['useElementByPoint']
const useElementHover: typeof import('@vueuse/core')['useElementHover']
const useElementSize: typeof import('@vueuse/core')['useElementSize']
const useElementVisibility: typeof import('@vueuse/core')['useElementVisibility']
const useEventBus: typeof import('@vueuse/core')['useEventBus']
const useEventListener: typeof import('@vueuse/core')['useEventListener']
const useEventSource: typeof import('@vueuse/core')['useEventSource']
const useEyeDropper: typeof import('@vueuse/core')['useEyeDropper']
const useFavicon: typeof import('@vueuse/core')['useFavicon']
const useFetch: typeof import('@vueuse/core')['useFetch']
const useFileDialog: typeof import('@vueuse/core')['useFileDialog']
const useFileSystemAccess: typeof import('@vueuse/core')['useFileSystemAccess']
const useFocus: typeof import('@vueuse/core')['useFocus']
const useFocusWithin: typeof import('@vueuse/core')['useFocusWithin']
const useFps: typeof import('@vueuse/core')['useFps']
const useFullscreen: typeof import('@vueuse/core')['useFullscreen']
const useGamepad: typeof import('@vueuse/core')['useGamepad']
const useGeolocation: typeof import('@vueuse/core')['useGeolocation']
const useId: typeof import('vue')['useId']
const useIdle: typeof import('@vueuse/core')['useIdle']
const useImage: typeof import('@vueuse/core')['useImage']
const useInfiniteScroll: typeof import('@vueuse/core')['useInfiniteScroll']
const useIntersectionObserver: typeof import('@vueuse/core')['useIntersectionObserver']
const useInterval: typeof import('@vueuse/core')['useInterval']
const useIntervalFn: typeof import('@vueuse/core')['useIntervalFn']
const useKeyModifier: typeof import('@vueuse/core')['useKeyModifier']
const useLastChanged: typeof import('@vueuse/core')['useLastChanged']
const useLink: typeof import('vue-router')['useLink']
const useLocalStorage: typeof import('@vueuse/core')['useLocalStorage']
const useMagicKeys: typeof import('@vueuse/core')['useMagicKeys']
const useManualRefHistory: typeof import('@vueuse/core')['useManualRefHistory']
const useMediaControls: typeof import('@vueuse/core')['useMediaControls']
const useMediaQuery: typeof import('@vueuse/core')['useMediaQuery']
const useMemoize: typeof import('@vueuse/core')['useMemoize']
const useMemory: typeof import('@vueuse/core')['useMemory']
const useModel: typeof import('vue')['useModel']
const useMounted: typeof import('@vueuse/core')['useMounted']
const useMouse: typeof import('@vueuse/core')['useMouse']
const useMouseInElement: typeof import('@vueuse/core')['useMouseInElement']
const useMousePressed: typeof import('@vueuse/core')['useMousePressed']
const useMutationObserver: typeof import('@vueuse/core')['useMutationObserver']
const useNavigatorLanguage: typeof import('@vueuse/core')['useNavigatorLanguage']
const useNetwork: typeof import('@vueuse/core')['useNetwork']
const useNow: typeof import('@vueuse/core')['useNow']
const useObjectUrl: typeof import('@vueuse/core')['useObjectUrl']
const useOffsetPagination: typeof import('@vueuse/core')['useOffsetPagination']
const useOnline: typeof import('@vueuse/core')['useOnline']
const usePageLeave: typeof import('@vueuse/core')['usePageLeave']
const useParallax: typeof import('@vueuse/core')['useParallax']
const useParentElement: typeof import('@vueuse/core')['useParentElement']
const usePerformanceObserver: typeof import('@vueuse/core')['usePerformanceObserver']
const usePermission: typeof import('@vueuse/core')['usePermission']
const usePointer: typeof import('@vueuse/core')['usePointer']
const usePointerLock: typeof import('@vueuse/core')['usePointerLock']
const usePointerSwipe: typeof import('@vueuse/core')['usePointerSwipe']
const usePreferredColorScheme: typeof import('@vueuse/core')['usePreferredColorScheme']
const usePreferredContrast: typeof import('@vueuse/core')['usePreferredContrast']
const usePreferredDark: typeof import('@vueuse/core')['usePreferredDark']
const usePreferredLanguages: typeof import('@vueuse/core')['usePreferredLanguages']
const usePreferredReducedMotion: typeof import('@vueuse/core')['usePreferredReducedMotion']
const usePrevious: typeof import('@vueuse/core')['usePrevious']
const useRafFn: typeof import('@vueuse/core')['useRafFn']
const useRefHistory: typeof import('@vueuse/core')['useRefHistory']
const useResizeObserver: typeof import('@vueuse/core')['useResizeObserver']
const useRoute: typeof import('vue-router')['useRoute']
const useRouter: typeof import('vue-router')['useRouter']
const useScreenOrientation: typeof import('@vueuse/core')['useScreenOrientation']
const useScreenSafeArea: typeof import('@vueuse/core')['useScreenSafeArea']
const useScriptTag: typeof import('@vueuse/core')['useScriptTag']
const useScroll: typeof import('@vueuse/core')['useScroll']
const useScrollLock: typeof import('@vueuse/core')['useScrollLock']
const useSessionStorage: typeof import('@vueuse/core')['useSessionStorage']
const useShare: typeof import('@vueuse/core')['useShare']
const useSlots: typeof import('vue')['useSlots']
const useSorted: typeof import('@vueuse/core')['useSorted']
const useSpeechRecognition: typeof import('@vueuse/core')['useSpeechRecognition']
const useSpeechSynthesis: typeof import('@vueuse/core')['useSpeechSynthesis']
const useStepper: typeof import('@vueuse/core')['useStepper']
const useStorage: typeof import('@vueuse/core')['useStorage']
const useStorageAsync: typeof import('@vueuse/core')['useStorageAsync']
const useStyleTag: typeof import('@vueuse/core')['useStyleTag']
const useSupported: typeof import('@vueuse/core')['useSupported']
const useSwipe: typeof import('@vueuse/core')['useSwipe']
const useTemplateRef: typeof import('vue')['useTemplateRef']
const useTemplateRefsList: typeof import('@vueuse/core')['useTemplateRefsList']
const useTextDirection: typeof import('@vueuse/core')['useTextDirection']
const useTextSelection: typeof import('@vueuse/core')['useTextSelection']
const useTextareaAutosize: typeof import('@vueuse/core')['useTextareaAutosize']
const useThrottle: typeof import('@vueuse/core')['useThrottle']
const useThrottleFn: typeof import('@vueuse/core')['useThrottleFn']
const useThrottledRefHistory: typeof import('@vueuse/core')['useThrottledRefHistory']
const useTimeAgo: typeof import('@vueuse/core')['useTimeAgo']
const useTimeout: typeof import('@vueuse/core')['useTimeout']
const useTimeoutFn: typeof import('@vueuse/core')['useTimeoutFn']
const useTimeoutPoll: typeof import('@vueuse/core')['useTimeoutPoll']
const useTimestamp: typeof import('@vueuse/core')['useTimestamp']
const useTitle: typeof import('@vueuse/core')['useTitle']
const useToNumber: typeof import('@vueuse/core')['useToNumber']
const useToString: typeof import('@vueuse/core')['useToString']
const useToggle: typeof import('@vueuse/core')['useToggle']
const useTransition: typeof import('@vueuse/core')['useTransition']
const useUrlSearchParams: typeof import('@vueuse/core')['useUrlSearchParams']
const useUserMedia: typeof import('@vueuse/core')['useUserMedia']
const useVModel: typeof import('@vueuse/core')['useVModel']
const useVModels: typeof import('@vueuse/core')['useVModels']
const useVibrate: typeof import('@vueuse/core')['useVibrate']
const useVirtualList: typeof import('@vueuse/core')['useVirtualList']
const useWakeLock: typeof import('@vueuse/core')['useWakeLock']
const useWebNotification: typeof import('@vueuse/core')['useWebNotification']
const useWebSocket: typeof import('@vueuse/core')['useWebSocket']
const useWebWorker: typeof import('@vueuse/core')['useWebWorker']
const useWebWorkerFn: typeof import('@vueuse/core')['useWebWorkerFn']
const useWindowFocus: typeof import('@vueuse/core')['useWindowFocus']
const useWindowScroll: typeof import('@vueuse/core')['useWindowScroll']
const useWindowSize: typeof import('@vueuse/core')['useWindowSize']
const watch: typeof import('vue')['watch']
const watchArray: typeof import('@vueuse/core')['watchArray']
const watchAtMost: typeof import('@vueuse/core')['watchAtMost']
const watchDebounced: typeof import('@vueuse/core')['watchDebounced']
const watchDeep: typeof import('@vueuse/core')['watchDeep']
const watchEffect: typeof import('vue')['watchEffect']
const watchIgnorable: typeof import('@vueuse/core')['watchIgnorable']
const watchImmediate: typeof import('@vueuse/core')['watchImmediate']
const watchOnce: typeof import('@vueuse/core')['watchOnce']
const watchPausable: typeof import('@vueuse/core')['watchPausable']
const watchPostEffect: typeof import('vue')['watchPostEffect']
const watchSyncEffect: typeof import('vue')['watchSyncEffect']
const watchThrottled: typeof import('@vueuse/core')['watchThrottled']
const watchTriggerable: typeof import('@vueuse/core')['watchTriggerable']
const watchWithFilter: typeof import('@vueuse/core')['watchWithFilter']
const whenever: typeof import('@vueuse/core')['whenever']
}
// for type re-export
declare global {
// @ts-ignore
export type { Component, ComponentPublicInstance, ComputedRef, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, VNode, WritableComputedRef } from 'vue'
import('vue')
}

99
src/components.d.ts vendored Normal file
View File

@@ -0,0 +1,99 @@
/* eslint-disable */
// @ts-nocheck
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
AAlert: typeof import('ant-design-vue/es')['Alert']
AAvatar: typeof import('ant-design-vue/es')['Avatar']
ABadge: typeof import('ant-design-vue/es')['Badge']
ABadgeRibbon: typeof import('ant-design-vue/es')['BadgeRibbon']
AButton: typeof import('ant-design-vue/es')['Button']
ACard: typeof import('ant-design-vue/es')['Card']
ACardGrid: typeof import('ant-design-vue/es')['CardGrid']
ACheckbox: typeof import('ant-design-vue/es')['Checkbox']
ACol: typeof import('ant-design-vue/es')['Col']
AConfigProvider: typeof import('ant-design-vue/es')['ConfigProvider']
ADatePicker: typeof import('ant-design-vue/es')['DatePicker']
ADescriptions: typeof import('ant-design-vue/es')['Descriptions']
ADescriptionsItem: typeof import('ant-design-vue/es')['DescriptionsItem']
ADivider: typeof import('ant-design-vue/es')['Divider']
ADrawer: typeof import('ant-design-vue/es')['Drawer']
ADropdown: typeof import('ant-design-vue/es')['Dropdown']
AFlex: typeof import('ant-design-vue/es')['Flex']
AFloatButton: typeof import('ant-design-vue/es')['FloatButton']
AForm: typeof import('ant-design-vue/es')['Form']
AFormItem: typeof import('ant-design-vue/es')['FormItem']
AImage: typeof import('ant-design-vue/es')['Image']
AInput: typeof import('ant-design-vue/es')['Input']
AInputPassword: typeof import('ant-design-vue/es')['InputPassword']
AInputSearch: typeof import('ant-design-vue/es')['InputSearch']
ALayout: typeof import('ant-design-vue/es')['Layout']
ALayoutContent: typeof import('ant-design-vue/es')['LayoutContent']
ALayoutFooter: typeof import('ant-design-vue/es')['LayoutFooter']
ALayoutHeader: typeof import('ant-design-vue/es')['LayoutHeader']
ALayoutSider: typeof import('ant-design-vue/es')['LayoutSider']
AMentionsOption: typeof import('ant-design-vue/es')['MentionsOption']
AMenu: typeof import('ant-design-vue/es')['Menu']
AMenuItem: typeof import('ant-design-vue/es')['MenuItem']
AModal: typeof import('ant-design-vue/es')['Modal']
APageHeader: typeof import('ant-design-vue/es')['PageHeader']
APopconfirm: typeof import('ant-design-vue/es')['Popconfirm']
ARadioButton: typeof import('ant-design-vue/es')['RadioButton']
ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup']
AResult: typeof import('ant-design-vue/es')['Result']
ARow: typeof import('ant-design-vue/es')['Row']
ASelect: typeof import('ant-design-vue/es')['Select']
ASelectOption: typeof import('ant-design-vue/es')['SelectOption']
ASlider: typeof import('ant-design-vue/es')['Slider']
ASpace: typeof import('ant-design-vue/es')['Space']
ASpin: typeof import('ant-design-vue/es')['Spin']
AStatistic: typeof import('ant-design-vue/es')['Statistic']
ASteps: typeof import('ant-design-vue/es')['Steps']
ASubMenu: typeof import('ant-design-vue/es')['SubMenu']
ASwitch: typeof import('ant-design-vue/es')['Switch']
ATable: typeof import('ant-design-vue/es')['Table']
ATabPane: typeof import('ant-design-vue/es')['TabPane']
ATabs: typeof import('ant-design-vue/es')['Tabs']
ATag: typeof import('ant-design-vue/es')['Tag']
ATooltip: typeof import('ant-design-vue/es')['Tooltip']
ATransfer: typeof import('ant-design-vue/es')['Transfer']
ATree: typeof import('ant-design-vue/es')['Tree']
ATypographyLink: typeof import('ant-design-vue/es')['TypographyLink']
ATypographyParagraph: typeof import('ant-design-vue/es')['TypographyParagraph']
ATypographyTitle: typeof import('ant-design-vue/es')['TypographyTitle']
AuxiliaryItemRender: typeof import('./components/form-render/auxiliary-item-render.vue')['default']
AWatermark: typeof import('ant-design-vue/es')['Watermark']
CodeGeneratorDrawer: typeof import('./components/form-designer/component-container/code-generator-drawer.vue')['default']
ColComponentItem: typeof import('./components/form-designer/component-container/col-component-item.vue')['default']
ComponentContainer: typeof import('./components/form-designer/component-container/component-container.vue')['default']
ComponentItem: typeof import('./components/form-designer/component-container/component-item.vue')['default']
ComponentPanel: typeof import('./components/form-designer/component-panel/component-panel.vue')['default']
ConfigPanel: typeof import('./components/form-designer/config-panel/config-panel.vue')['default']
DataSourceTable: typeof import('./components/form-designer/config-panel/data-source-table.vue')['default']
FormConfigPanel: typeof import('./components/form-designer/config-panel/form-config-panel.vue')['default']
FormDesigner: typeof import('./components/form-designer/form-designer.vue')['default']
FormDrawer: typeof import('./components/form-render/form-drawer.vue')['default']
FormItemConfigPanel: typeof import('./components/form-designer/config-panel/form-item-config-panel.vue')['default']
FormItemRender: typeof import('./components/form-render/form-item-render.vue')['default']
FormModal: typeof import('./components/form-render/form-modal.vue')['default']
FormPreviewDrawer: typeof import('./components/form-designer/component-container/form-preview-drawer.vue')['default']
FormRender: typeof import('./components/form-render/form-render.vue')['default']
IconFont: typeof import('./components/icon-font/icon-font.vue')['default']
ImageUploader: typeof import('./components/image-uploader/image-uploader.vue')['default']
InputItem: typeof import('./components/form-designer/component-panel/input-item.vue')['default']
OptionTable: typeof import('./components/form-designer/config-panel/option-table.vue')['default']
PageContainer: typeof import('./components/page-container/page-container.vue')['default']
PropertyConfig: typeof import('./components/form-designer/config-panel/property-config.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
RowComponentItem: typeof import('./components/form-designer/component-container/row-component-item.vue')['default']
SearchTree: typeof import('./components/search-tree.vue')['default']
SelectedTree: typeof import('./components/search-tree.vue')['default']
TreeDataTable: typeof import('./components/form-designer/config-panel/tree-data-table.vue')['default']
ValidateRuleConfig: typeof import('./components/form-designer/config-panel/validate-rule-config.vue')['default']
}
}

View File

@@ -0,0 +1,253 @@
<template>
<a-drawer v-model:open="visible" title="生成结果" width="50%" :closable="false">
<a-tabs v-model:active-key="activeKey">
<template #rightExtra>
<a-button type="primary" size="small" @click="copy(code)">
<template #icon>
<icon-font type="icon-copy"></icon-font>
</template>
复制
</a-button>
</template>
<a-tab-pane key="form" tab="form.ts"></a-tab-pane>
<a-tab-pane key="render" tab="form.vue"></a-tab-pane>
<a-tab-pane key="modal" tab="modal.vue"></a-tab-pane>
</a-tabs>
<codemirror
v-model="code"
disabled
:style="{ height: 'calc(100vh - 175px)' }"
:autofocus="true"
:indent-with-tab="true"
:tab-size="2"
:extensions="extensions"
/>
</a-drawer>
</template>
<script lang="ts" setup>
import { PropType, computed, ref, toRefs, watch } from 'vue'
import { Codemirror } from 'vue-codemirror'
import { java } from '@codemirror/lang-java'
import { javascript } from '@codemirror/lang-javascript'
import { oneDark } from '@codemirror/theme-one-dark'
import XEUtils from 'xe-utils'
import { notification } from 'ant-design-vue'
import { useClipboard } from '@vueuse/core'
import { FormDesignerItem } from '../form-designer-types'
import { FormConfig, FormItem } from '@/components/form-render/form-render-types'
const extensions = [java(), javascript({ jsx: true, typescript: true }), oneDark]
const visible = ref(false)
const show = () => {
visible.value = true
}
defineExpose({
show,
})
const props = defineProps({
components: {
type: Array as PropType<FormDesignerItem[]>,
required: true,
},
config: {
type: Object as PropType<FormConfig>,
required: true,
},
})
const { components, config } = toRefs(props)
const hanldConfig = (item: FormDesignerItem) => {
if (item.formItem.config) {
delete item.formItem.config.labelColFixedWidthInPx
delete item.formItem.config.labelColSpanWidth
delete item.formItem.config.labelType
delete item.formItem.config.useFormConfig
if (item.formItem.config.labelType === 'fixed') {
delete item.formItem.config.labelCol?.span
delete item.formItem.config.labelCol?.offset
} else {
delete item.formItem.config.labelCol?.style
}
}
if (item.formItem.properties.options) {
item.formItem.properties.options.forEach((item) => {
delete item.uuid
})
}
if (item.formItem.rules?.length) {
item.formItem.rules.forEach((item) => {
;(item as { uuid?: string }).uuid = undefined
if (item.enum) {
item.enum = String(item.enum).split(',')
}
})
}
}
const activeKey = ref('form')
const configTransfer = (item: FormDesignerItem): FormItem => {
if (item.formItem) {
delete item.formItem.propertyConfigFormComponents
}
if (item.formItem?.config) {
hanldConfig(item)
}
if (item.formItem.propertiesTransfer) {
item.formItem.properties = item.formItem.propertiesTransfer(item.formItem.properties)
}
//递归一下
if (item.children) {
return XEUtils.assign(item.formItem, { children: item.children.map((item) => configTransfer(item)) })
}
return item.formItem
}
const code = computed(() => {
const formConfig = XEUtils.clone(config.value, true)
if (formConfig.labelType === 'fixed') {
delete formConfig.labelCol?.span
delete formConfig.labelCol?.offset
} else {
delete formConfig.labelCol?.style
}
delete formConfig.labelColFixedWidthInPx
delete formConfig.labelColSpanWidth
delete formConfig.labelType
switch (activeKey.value) {
case 'form':
return `import { FormItem, FormConfig } from '@/components/form-render/form-render-types';
export const config: FormConfig = ${JSON.stringify(formConfig, undefined, 2)}
export const formItems: FormItem[] = ${JSON.stringify(
XEUtils.clone(components.value, true).map((item) => configTransfer(item)),
undefined,
2,
)}
`
case 'modal':
return `<template>
<div>
<a-button type="primary" @click="add">添加</a-button>
<form-modal
ref="formModal"
v-model="data"
:form-items="items"
:config="formConfig"
:title="title"
:hidden-fields="hiddenFields"
:disabled-fields="disabledFields"
@ok="hanldOk"
></form-modal>
</div>
</template>
<script lang="ts" setup>
import FormModal from '@/components/form-render/form-modal.vue';
import { config, formItems } from './form';
import { FormDataType } from '@/components/form-render/form-render-types';
const items = computed(() => {
return formItems;
});
const formConfig = computed(() => {
return config;
});
const data = ref<FormDataType>({});
const title = computed(() => {
//TODO 根据数据情况设置标题
return '添加';
});
const hiddenFields = computed(() => {
//TODO 根据数据情况设置隐藏字段
return [];
});
const hanldOk = (_data: FormDataType) => {
//TODO 根据业务情况处理
console.log(_data);
formModal.value?.close(); //关闭弹窗
};
const disabledFields = computed(() => {
//TODO 根据数据情况设置隐藏字段
return [];
});
const formModal = ref<typeof FormModal>();
const add = () => {
formModal.value?.open(); //通过ref可调用的其他方法有 open, close,reset, validateFields,resetFields,getModalContentWrapper, getPopupContainerId, closeLoading,
};
// 其他属性包含 maxHeight,width,wrapClassName,modalContentWrapperClassName
<\\/script>
`.replace('\\', '')
case 'render':
return `<template>
<form-render
v-model="data"
:items="items"
:config="formConfig"
:hidden-fields="hiddenFields"
:disabled-fields="disabledFields"
></form-render>
</template>
<script lang="ts" setup>
import { config, formItems } from './form';
import { FormDataType } from '@/components/form-render/form-render-types';
const items = computed(() => {
return formItems;
});
const formConfig = computed(() => {
return config;
});
const data = ref<FormDataType>({});
const hiddenFields = computed(() => {
//TODO 根据数据情况设置隐藏字段
return [];
});
const disabledFields = computed(() => {
//TODO 根据数据情况设置隐藏字段
return [];
});
// 可通过ref调用render的方法有 validateFields, resetFields, clearValidate
<\\/script>
`.replace('\\', '')
default:
return `
<template>
<form-render v-model="formData" :form-config="formConfig" :form-items="formItems" />
</template>
<script lang="ts" setup>
import FormRender from '@/components/form-render/form-render.vue';
import { useFormItems } from './form-items';
import type { FormConfig } from '@/components/form-render/types/form-designer';
import type { FormData } from '@/components/form-render/types/form-render';
const formData = ref<FormData>({
layout: 'horizontal',
colon: true,
hideRequiredMark: false,
labelAlign: 'right',
labelType: 'fixed',
scrollToFirstError: false,
validateOnRuleChange: true,
labelCol: {}
});
const formConfig = ref<FormConfig>();
const { formItems } = useFormItems();
<\\/script>
`.replace('\\', '')
}
})
const { copy, text } = useClipboard()
watch(text, (newVal) => {
if (newVal) {
notification.success({
message: '操作成功',
description: '代码已经成功复制',
})
}
})
</script>

View File

@@ -0,0 +1,119 @@
<template>
<a-col :id="id" v-bind="Object.assign({}, $attrs, item.formItem.properties)" :data-id="item.uuid">
<component-item
v-for="(c, index) in item.children"
:key="c.uuid"
:data-id="c.uuid"
:item="c"
:selected="selected"
@selected="selectedChange"
@copy="copyItem(index)"
@delete="deleteItem(index, $event)"
/>
</a-col>
</template>
<script lang="ts" setup>
import { FormDesignerItem } from '../form-designer-types'
import Sortable from 'sortablejs'
import allComponents from '../component-panel/components'
import XEUtils from 'xe-utils'
import { notification } from 'ant-design-vue'
const props = defineProps({
item: {
type: Object as PropType<FormDesignerItem>,
required: true,
},
selected: {
type: Object as PropType<FormDesignerItem>,
required: false,
default: () => null,
},
components: {
type: Array as PropType<Array<FormDesignerItem>>,
required: false,
default: () => [],
},
})
const { item, selected, components } = toRefs(props)
// 复制表单项
const copyItem = (index: number) => {
if (item.value.children) {
const copyItem = XEUtils.assign(XEUtils.clone(item.value.children[index], true), { uuid: XEUtils.uniqueId() })
if (copyItem.formItem.config?.name)
copyItem.formItem.config.name = copyItem.formItem.config.name.split('_')[0] + '_' + XEUtils.uniqueId()
// 必须深拷贝,否则对象类型的字段会拷贝引用
item.value.children.splice(index + 1, 0, copyItem)
}
}
// 删除表单项
const deleteItem = (index: number, _item: FormDesignerItem) => {
if (item.value.children) {
item.value.children.splice(index, 1)
console.log(_item) //需要判定删除的组件是不是选中的组件
}
}
const emit = defineEmits<{
selectedChange: [config?: FormDesignerItem]
}>()
const selectedChange = (_config?: FormDesignerItem) => {
emit('selectedChange', _config)
}
const id = computed(() => {
return `drag-col-${item.value.uuid}`
})
nextTick(() => {
const dom = document.getElementById(id.value)
if (dom) {
Sortable.create(dom, {
animation: 150,
sort: true,
group: { name: 'components', pull: true, put: true },
onEnd: (evt) => {
item.value.children = [...evt.from.children]
.filter((c) => (c as HTMLElement).style.display !== 'none')
.map((c) => {
const id = (c as HTMLElement).dataset.id
return (item.value.children ?? []).find((c) => c.uuid === id)
}) as FormDesignerItem[]
},
onAdd: (evt) => {
;[...evt.to.children]
.filter((item) => item.classList.contains('ant-card-grid'))
.forEach((item) => {
;(item as HTMLElement).style.display = 'none'
}) //原始元素都别给我显示出来了
const uuid = evt.clone.dataset.id
const type = evt.clone.dataset.type
const newItem = type
? allComponents.find((item) => item.type === type)
: XEUtils.findTree(components.value, (item) => item.uuid === uuid).item
if (newItem?.formItem.group === 'layout') {
notification.warning({
message: '提示',
description: '暂不支持布局嵌套,请拖入表单组件',
duration: 3,
})
return
}
if (newItem) {
item.value.children?.push(XEUtils.assign(XEUtils.clone(newItem, true), { uuid: XEUtils.uniqueId() }))
}
},
})
}
})
</script>
<style scoped>
.ant-col,
.ant-row {
min-height: 50px;
padding: 10px;
}
</style>

View File

@@ -0,0 +1,267 @@
<template>
<a-card size="small">
<template #title>
<a-tabs centered size="small">
<a-tab-pane key="form" tab="预览区" />
</a-tabs>
</template>
<template #extra>
<a-space v-if="notEmpty">
<a-button size="small" type="primary" @click="generate">
<template #icon>
<icon-font type="icon-code" />
</template>
生成
</a-button>
<a-button size="small" type="primary" @click="preview">
<template #icon>
<icon-font type="icon-preview" />
</template>
预览
</a-button>
<a-popconfirm title="确定清空设计?" ok-text="确定" cancel-text="取消" @confirm="reset">
<a-button size="small" type="primary" danger>
<template #icon>
<icon-font type="icon-reset" />
</template>
清空
</a-button>
</a-popconfirm>
</a-space>
</template>
<div :style="{ minHeight: 'calc(100vh - 190px)', maxHeight: 'calc(100vh - 190px)', overflowY: 'auto' }">
<a-watermark :content="waterMark" :rotate="22" :font="{ color: 'rgba(0,0,0,.3)', fontSize: 20 }">
<a-form
v-bind="formConfig"
ref="formRef"
:style="{ '--ant-primary-color': token.colorPrimaryBorder }"
@click="selectedChange()"
>
<component-item
v-for="(c, index) in components"
:key="c.uuid"
:data-id="c.uuid"
:item="c"
:components="components"
:disabled-fields="disabledFields"
:selected="selected"
@selected="selectedChange"
@copy="copyItem(index)"
@delete="deleteItem(index, $event)"
/>
</a-form>
</a-watermark>
</div>
<code-generator-drawer
ref="codeGeneratorDrawer"
:components="components"
:config="formConfig"
></code-generator-drawer>
<form-preview-drawer ref="formPreviewModal" :components="components" :config="formConfig"></form-preview-drawer>
</a-card>
</template>
<script lang="ts" setup>
import { FormDesignerItem } from '../form-designer-types'
import Sortable from 'sortablejs'
import allComponents from '../component-panel/components'
import { FormConfig, FormItem } from '@/components/form-render/form-render-types'
import XEUtils from 'xe-utils'
import { notification, theme } from 'ant-design-vue'
import FormPreviewDrawer from './form-preview-drawer.vue'
import CodeGeneratorDrawer from './code-generator-drawer.vue'
const { useToken } = theme
const { token } = useToken()
const props = defineProps({
formConfig: {
type: Object as PropType<FormConfig>,
required: true,
},
// 选中的组件
selected: {
type: Object as PropType<FormDesignerItem>,
required: false,
default: () => {
return undefined
},
},
// 禁用的表单项
disabledFields: {
type: Array as PropType<Array<string>>,
required: false,
default: () => {
return []
},
},
isGenerate: {
type: Boolean,
default: () => true,
},
})
const { selected } = toRefs(props)
const selectedKey = computed(() => {
return selected?.value?.uuid
})
const emit = defineEmits<{
selectedChange: [config?: FormDesignerItem]
'update:form-items': [formItems: Array<FormItem>]
}>()
const selectedChange = (config?: FormDesignerItem) => {
emit('selectedChange', config)
}
// 复制表单项
const copyItem = (index: number) => {
const item = XEUtils.assign(XEUtils.clone(components.value[index], true), { uuid: XEUtils.uniqueId() })
if (item.formItem.config?.name)
item.formItem.config.name = item.formItem.config.name.split('_')[0] + '_' + XEUtils.uniqueId()
// 必须深拷贝,否则对象类型的字段会拷贝引用
components.value.splice(index + 1, 0, item)
}
// 删除表单项
const deleteItem = (index: number, item: FormDesignerItem) => {
components.value.splice(index, 1)
if (item.uuid == selectedKey.value) {
// 如果删除的是当前选中的组件,则要清理选中组件
selectedChange()
}
}
const components = ref<Array<FormDesignerItem>>([])
const notEmpty = computed<boolean>(() => {
return components.value.length > 0
})
const waterMark = computed<string>(() => {
return notEmpty.value ? ' ' : '拖动组件到这里'
})
const codeGeneratorDrawer = ref<typeof CodeGeneratorDrawer>()
// 生成
const generate = () => {
codeGeneratorDrawer.value?.show()
}
const formPreviewModal = ref<typeof FormPreviewDrawer>()
// 预览
const preview = () => {
formPreviewModal.value?.show()
}
// 清空
const reset = () => {
// 清空容器中的组件
components.value = []
selectedChange()
}
const sort = ref<Sortable>()
nextTick(() => {
const form = document.querySelector('.ant-form')
// 整体的组件
if (form) {
sort.value = Sortable.create(form as HTMLElement, {
animation: 150,
sort: true,
group: { name: 'components', pull: true, put: true },
onEnd: (evt) => {
components.value = [...evt.from.children]
.filter((item) => (item as HTMLElement).style.display !== 'none')
.map((item) => {
const id = (item as HTMLElement).dataset.id
return components.value.find((item) => item.uuid === id)
}) as FormDesignerItem[]
},
onAdd: (evt) => {
;[...evt.to.children]
.filter((item) => item.classList.contains('ant-card-grid'))
.forEach((item) => {
;(item as HTMLElement).style.display = 'none'
})
const uuid = evt.clone.dataset.id
const type = evt.clone.dataset.type
if (type === 'col') {
notification.warning({
message: '提示',
description: '列组件需要放入行组件',
duration: 3,
})
return
}
const item = type
? allComponents.find((item) => item.type === type)
: XEUtils.findTree(components.value, (item) => item.uuid === uuid).item
if (item) {
components.value.push(XEUtils.assign(XEUtils.clone(item, true), { uuid: XEUtils.uniqueId() }))
}
nextTick(() => {
rowDrag()
})
},
})
}
})
const rowDrag = () => {
;[...document.querySelectorAll('.list-group-item .drag-row')].forEach((item) => {
Sortable.create(item as HTMLElement, {
animation: 150,
sort: true,
group: { name: 'components', pull: true, put: true },
onEnd: (evt) => {
const item = components.value.find((a) => a.uuid === evt.to.dataset.id)
if (item) {
item.children = [...evt.from.children]
.filter((c) => (c as HTMLElement).style.display !== 'none')
.map((c) => {
const id = (c as HTMLElement).dataset.id
return (item.children ?? []).find((c) => c.uuid === id)
}) as FormDesignerItem[]
}
},
onAdd: (evt) => {
;[...evt.to.children]
.filter((item) => item.classList.contains('ant-card-grid'))
.forEach((item) => {
;(item as HTMLElement).style.display = 'none'
}) //原始元素都别给我显示出来了
//获取拖到了哪个行,添加拖入的列
const item = components.value.find((a) => a.uuid === evt.to.dataset.id)
const type = evt.clone.dataset.type
if (!type) {
notification.warning({
message: '提示',
description: '已设置组件放入布局将无法恢复!',
duration: 3,
})
return
}
if (type !== 'col') {
notification.warning({
message: '提示',
description: '行组件只接受放入列组件',
duration: 3,
})
return
}
const newItem = allComponents.find((item) => item.type === type) //仅仅找到原始组件
if (newItem) {
item?.children?.push(XEUtils.assign(XEUtils.clone(newItem, true), { uuid: XEUtils.uniqueId() }))
}
},
})
})
}
</script>
<style scoped>
.ant-form {
height: calc(100vh - 190px);
padding: 15px;
overflow-x: hidden;
background-color: #fff;
border: 1px dashed var(--ant-primary-color);
}
.ant-pro-pro-layout-watermark-wrapper {
min-height: calc(100vh - 190px) !important;
}
</style>

View File

@@ -0,0 +1,118 @@
<template>
<a-badge-ribbon color="volcano">
<template #text>
<a-space>
<icon-font type="icon-copy" @click.stop="onCopy" />
<icon-font type="icon-delete" style="color: #fff" @click.stop="onDelete" />
</a-space>
</template>
<div
:style="{ '--ant-primary-color': token.colorPrimaryBorder }"
:class="`list-group-item list-group-item-masked designer-item ${
selected?.uuid === item.uuid ? 'designer-item-active' : ''
}`"
@click.stop="onClick"
>
<form-item-render v-if="item.formItem?.group === 'form'" :item="item.formItem" :disabled="true" />
<auxiliary-item-render
v-else-if="item.formItem?.group === 'auxiliary'"
:item="item.formItem"
></auxiliary-item-render>
<row-component-item
v-else-if="item.formItem.group === 'layout' && item.formItem.type === FormItemTypeEnum.ROW"
:data-id="item.uuid"
:item="XEUtils.assign(item.formItem, { children: item.children?.map((item) => item.formItem) })"
>
<template v-if="item.children">
<col-component-item
v-for="(c, index) in item.children"
:key="index"
:item="c"
:components="components"
:style="{
borderStyle: selected && selected.uuid === c.uuid ? 'solid' : 'dashed',
borderWidth: selected && selected.uuid === c.uuid ? '2px' : '1px',
borderColor: token.colorPrimaryBorder,
}"
:selected="selected"
@click.stop="emit('selected', c)"
@selected-change="(_c?: FormDesignerItem) => emit('selected', _c)"
></col-component-item>
</template>
</row-component-item>
</div>
</a-badge-ribbon>
</template>
<script lang="ts" setup>
import type { PropType } from 'vue'
import { FormDesignerItem } from '../form-designer-types'
import { theme } from 'ant-design-vue'
import { FormItemTypeEnum } from '@/components/form-render/form-render-types'
import XEUtils from 'xe-utils'
const { useToken } = theme
const { token } = useToken()
const props = defineProps({
item: {
type: Object as PropType<FormDesignerItem>,
required: true,
},
selected: {
type: Object as PropType<FormDesignerItem>,
required: false,
default: () => null,
},
components: {
type: Array as PropType<Array<FormDesignerItem>>,
required: false,
default: () => [],
},
})
const { item, selected, components } = toRefs(props)
watch(
selected,
(v) => {
if (v && item.value.uuid !== v.uuid && v.formItem.type === FormItemTypeEnum.COL) {
//嵌套的列
if (item.value.children) {
const index = item.value.children?.findIndex((i) => i.uuid === v.uuid)
item.value.children.splice(index, 1, v)
}
}
},
{ immediate: true, deep: true },
)
const emit = defineEmits<{
selected: [itemConfig?: FormDesignerItem]
copy: [itemConfig: FormDesignerItem]
delete: [itemConfig: FormDesignerItem]
}>()
const onClick = () => {
emit('selected', item.value)
}
const onCopy = () => {
emit('copy', item.value)
}
const onDelete = () => {
emit('delete', item.value)
}
</script>
<style lang="css" scoped>
.designer-item {
padding: 10px 50px 10px 10px;
cursor: pointer;
border: 1px dashed var(--ant-primary-color);
}
.designer-item-active {
border: 2px solid var(--ant-primary-color);
}
.list-group-item-masked {
z-index: 3000;
background: rgb(238 236 236 / 30%);
}
</style>

View File

@@ -0,0 +1,54 @@
<template>
<a-drawer v-model:open="visible" title="表单预览" width="50%" :closable="false">
<form-render v-model="formData" :items="items" :config="config"></form-render>
<template #extra>
<a-button type="primary" size="small" @click="handleOk">
<template #icon>
<icon-font type="icon-submit"></icon-font>
</template>
确定
</a-button>
</template>
</a-drawer>
</template>
<script lang="ts" setup>
import { FormConfig } from '@/components/form-render/form-render-types'
import { FormDesignerItem } from '../form-designer-types'
import { PropType, computed, ref, toRefs } from 'vue'
import { notification } from 'ant-design-vue'
import XEUtils from 'xe-utils'
const visible = ref<boolean>(false)
const show = () => {
visible.value = true
formData.value = {}
}
defineExpose({ show })
const handleOk = () => {
notification.open({
message: '表单数据',
description: JSON.stringify(formData.value),
duration: 0,
})
visible.value = false
}
const props = defineProps({
components: {
type: Array as PropType<FormDesignerItem[]>,
required: true,
},
config: {
type: Object as PropType<FormConfig>,
required: true,
},
})
const { components, config } = toRefs(props)
const items = computed(() => {
return XEUtils.mapTree(components.value, (item) => item.formItem)
})
const formData = ref({})
</script>

View File

@@ -0,0 +1,28 @@
<template>
<a-row
class="drag-row"
v-bind="Object.assign({}, $attrs, item.properties)"
:style="{ '--ant-primary-color': token.colorPrimaryBorder }"
>
<slot></slot>
</a-row>
</template>
<script lang="ts" setup>
import { FormItem } from '@/components/form-render/form-render-types'
import { theme } from 'ant-design-vue'
const { useToken } = theme
const { token } = useToken()
const props = defineProps({
item: {
type: Object as PropType<FormItem>,
required: true,
},
})
const { item } = toRefs(props)
</script>
<style scoped>
.ant-row {
min-height: 50px;
border: 1px dashed var(--ant-primary-color);
}
</style>

View File

@@ -0,0 +1,91 @@
<template>
<a-card size="small">
<template #title>
<a-tabs centered size="small">
<a-tab-pane key="form" tab="组件区" />
</a-tabs>
</template>
<div :style="{ minHeight: 'calc(100vh - 190px)', maxHeight: 'calc(100vh - 190px)', overflowY: 'auto' }">
<a-card id="form-items" title="表单组件" size="small">
<a-card-grid
v-for="element in formComponents"
:key="element.uuid"
:data-id="element.uuid"
:data-type="element.type"
class="list-group-item"
style="width: 25%; padding: 0 5px; text-align: center"
>
<input-item :name="element.name" :icon="element.icon" />
</a-card-grid>
</a-card>
<a-card id="auxiliary-items" title="辅助组件" size="small">
<a-card-grid
v-for="element in auxiliaryComponents"
:key="element.uuid"
:data-id="element.uuid"
:data-type="element.type"
class="list-group-item"
style="width: 25%; padding: 0 5px; text-align: center"
>
<input-item :name="element.name" :icon="element.icon" />
</a-card-grid>
</a-card>
<a-card id="layout-items" title="布局组件" size="small">
<a-card-grid
v-for="element in layoutComponents"
:key="element.uuid"
:data-id="element.uuid"
:data-type="element.type"
class="list-group-item"
style="width: 25%; padding: 0 5px; text-align: center"
>
<input-item :name="element.name" :icon="element.icon" />
</a-card-grid>
</a-card>
</div>
</a-card>
</template>
<script lang="ts" setup>
import components from './components'
import Sortable, { Options } from 'sortablejs'
const formComponents = computed(() => {
return components.filter((item) => item.formItem?.group === 'form')
})
const auxiliaryComponents = computed(() => {
return components.filter((item) => item.formItem?.group === 'auxiliary')
})
const layoutComponents = computed(() => {
return components.filter((item) => item.formItem?.group === 'layout')
})
const auxiliarySort = ref<Sortable>()
const formSort = ref<Sortable>()
const layoutSort = ref<Sortable>()
const dragOptions = (): Options => {
return {
animation: 150,
sort: false,
group: { name: 'components', pull: 'clone', put: false },
}
}
nextTick(() => {
const formElement = document.querySelector('#form-items .ant-card-body')
if (formElement) {
formSort.value = Sortable.create(formElement as HTMLElement, dragOptions())
}
const auxiliaryElement = document.querySelector('#auxiliary-items .ant-card-body')
if (auxiliaryElement) {
auxiliarySort.value = Sortable.create(auxiliaryElement as HTMLElement, dragOptions())
}
const layoutElement = document.querySelector('#layout-items .ant-card-body')
if (layoutElement) {
layoutSort.value = Sortable.create(layoutElement as HTMLElement, dragOptions())
}
})
</script>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,23 @@
<template>
<a-space direction="vertical" style="margin-top: 10px">
<a-avatar shape="square">
<template #icon>
<icon-font :type="icon"></icon-font>
</template>
</a-avatar>
<a-typography-paragraph>{{ name }}</a-typography-paragraph>
</a-space>
</template>
<script lang="ts" setup>
const props = defineProps({
icon: {
type: String,
required: true,
},
name: {
type: String,
required: true,
},
})
const { icon, name } = toRefs(props)
</script>

View File

@@ -0,0 +1,107 @@
<template>
<a-card size="small">
<template #title>
<a-tabs v-model:active-key="activeKey" centered size="small">
<a-tab-pane v-if="currentItem.uuid" key="item" tab="组件配置" />
<a-tab-pane key="form" tab="表单配置" />
</a-tabs>
</template>
<div :style="{ minHeight: 'calc(100vh - 190px)', maxHeight: 'calc(100vh - 190px)', overflowY: 'auto' }">
<div v-if="activeKey === 'form'">
<form-config-panel :form-config="formConfig" @update:form-config="onUpdateFormConfig"></form-config-panel>
</div>
<div v-else>
<a-tabs v-model:active-key="currentSection" size="small" animated>
<a-tab-pane v-if="currentItem.formItem?.group === 'form'" key="config" tab="组件配置">
<form-item-config-panel
:form-item="currentItem.formItem"
:form-config="formConfig"
@update:form-item-config="onUpdateFormItemConfig"
></form-item-config-panel>
</a-tab-pane>
<a-tab-pane key="attribute" tab="组件属性">
<property-config :form-item="currentItem.formItem" @update:form-item="onUpdateFormItem"></property-config>
</a-tab-pane>
<a-tab-pane v-if="currentItem.formItem?.group === 'form'" key="rule" tab="验证规则">
<validate-rule-config
:rules="currentItem.formItem.rules"
@update:rules="onUpdateRules"
></validate-rule-config>
</a-tab-pane>
</a-tabs>
</div>
</div>
</a-card>
</template>
<script lang="ts" setup>
import { FormConfig, FormItem, FormItemConfig } from '@/components/form-render/form-render-types'
import { FormDesignerItem } from '../form-designer-types'
import XEUtils from 'xe-utils'
import { Rule } from 'ant-design-vue/es/form'
const props = defineProps({
currentItem: {
type: Object as PropType<FormDesignerItem>,
required: false,
default: () => {
return {}
},
},
formConfig: {
type: Object as PropType<FormConfig>,
required: true,
},
})
const { currentItem, formConfig } = toRefs(props)
const emit = defineEmits<{
'update:form-item': [formItem: FormDesignerItem]
'update:form-config': [formConfig: FormConfig]
}>()
const activeKey = ref('form')
const currentSection = ref('config')
watch(
currentItem,
(newVal) => {
activeKey.value = newVal.uuid ? 'item' : 'form'
},
{ immediate: true, deep: true },
)
const itemType = computed(() => {
return currentItem.value.formItem.group
})
watch(
itemType,
(v) => {
currentSection.value = v === 'form' ? 'config' : 'attribute'
},
{ immediate: true, deep: true },
)
const onUpdateFormConfig = (newFormConfig: FormConfig) => {
emit('update:form-config', newFormConfig)
}
const onUpdateFormItemConfig = (_formItemConfig: FormItemConfig) => {
emit(
'update:form-item',
XEUtils.assign(currentItem.value, {
formItem: XEUtils.assign(currentItem.value.formItem, { config: _formItemConfig }),
}),
)
}
const onUpdateFormItem = (formItem: FormItem) => {
emit('update:form-item', XEUtils.assign(currentItem.value, { formItem }))
}
const onUpdateRules = (_rules: Rule[]) => {
emit(
'update:form-item',
XEUtils.assign(currentItem.value, {
formItem: XEUtils.assign(currentItem.value.formItem, { rules: _rules }),
}),
)
}
</script>

View File

@@ -0,0 +1,187 @@
<template>
<a-table size="small" :data-source="shownDataSource" :columns="columns" :pagination="false" row-key="uuid">
<template #title>
<a-button type="primary" size="small" @click="add">
<template #icon>
<icon-font type="icon-plus"></icon-font>
</template>
添加
</a-button>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'ops'">
<a-space>
<a-button type="primary" size="small" @click="edit(record as DataSource)">
<template #icon>
<icon-font type="icon-edit"></icon-font>
</template>
</a-button>
<a-popconfirm title="确定删除这个选项?" @confirm="deleteDataSource(record as DataSource)">
<a-button type="primary" size="small" danger>
<template #icon>
<icon-font type="icon-delete"></icon-font>
</template>
</a-button>
</a-popconfirm>
</a-space>
</template>
</template>
</a-table>
<form-modal ref="formModal" v-model="data" :form-items="formItems" :title="title" @ok="hanldOk"></form-modal>
</template>
<script lang="ts" setup>
import FormModal from '@/components/form-render/form-modal.vue'
import { DataSource, FormDataType, FormItem, FormItemTypeEnum } from '@/components/form-render/form-render-types'
import { PropType } from 'vue'
import XEUtils from 'xe-utils'
const props = defineProps({
dataSource: {
type: Array as PropType<Array<DataSource>>,
required: false,
default: () => [],
},
})
const { dataSource } = toRefs(props)
const shownDataSource = computed(() => {
return XEUtils.mapTree(XEUtils.clone(dataSource.value, true), (item) => {
return XEUtils.assign(XEUtils.clone(item, true), { uuid: XEUtils.uniqueId() })
})
})
const columns = [
{
title: '键',
dataIndex: 'key',
key: 'key',
},
{
title: '标题',
dataIndex: 'title',
key: 'title',
},
{
title: '描述',
dataIndex: 'description',
key: 'description',
},
{
title: '操作',
key: 'ops',
width: 100,
},
]
const data = ref<Partial<DataSource>>({})
const isAdd = ref(true)
const title = computed(() => {
return isAdd.value ? '添加数据源' : '编辑数据源'
})
const formItems = reactive<FormItem[]>([
{
group: 'form',
type: FormItemTypeEnum.INPUT,
config: {
autoLink: true,
hasFeedback: false,
label: '键',
name: 'key',
required: true,
},
properties: {
size: 'small',
type: 'text',
allowClear: true,
bordered: true,
showCount: true,
placeholder: '请输入Key',
},
rules: [],
},
{
group: 'form',
type: FormItemTypeEnum.INPUT,
config: {
autoLink: true,
hasFeedback: false,
label: '标题',
name: 'title',
required: true,
},
properties: {
size: 'small',
type: 'text',
allowClear: true,
bordered: true,
showCount: true,
placeholder: '请输入标题',
},
rules: [],
},
{
type: FormItemTypeEnum.TEXTAREA,
group: 'form',
config: {
autoLink: true,
hasFeedback: false,
label: '描述',
name: 'description',
},
properties: {
size: 'small',
visibilityToggle: true,
allowClear: true,
bordered: true,
showCount: true,
autoSize: {
minRows: 2,
maxRows: 6,
},
minRows: 2,
maxRows: 6,
placeholder: '请输入描述',
},
rules: [],
},
])
const formModal = ref<typeof FormModal>()
const add = () => {
data.value = {}
isAdd.value = true
formModal.value?.open()
}
const edit = (_dataSource: DataSource) => {
data.value = _dataSource
isAdd.value = false
formModal.value?.open()
}
const emit = defineEmits<{
'update:data-source': [value: DataSource[]]
}>()
const deleteDataSource = (_dataSource: DataSource & { uuid?: string }) => {
emit(
'update:data-source',
XEUtils.filter(shownDataSource.value, (item) => item.uuid !== _dataSource.uuid),
)
}
const hanldOk = (_data: FormDataType) => {
let d = XEUtils.clone(shownDataSource.value, true)
if (_data.uuid) {
d = XEUtils.map(d, (item: DataSource & { uuid: string }) => {
if (item.uuid === _data.uuid) {
return _data as DataSource & { uuid: string }
}
return item
})
} else {
d.push(_data as DataSource & { uuid: string })
}
emit('update:data-source', d)
formModal.value?.close()
}
</script>

View File

@@ -0,0 +1,222 @@
<template>
<form-render
v-model="formData"
:config="{
layout: 'vertical',
colon: true,
hideRequiredMark: false,
labelAlign: 'right',
labelType: 'fixed',
scrollToFirstError: false,
validateOnRuleChange: true,
labelCol: {},
}"
:hidden-fields="hiddenFields"
:items="formItems"
></form-render>
</template>
<script lang="ts" setup>
import { FormConfig, FormItem, FormItemTypeEnum } from '@/components/form-render/form-render-types'
import { PropType, computed, ref, toRefs } from 'vue'
const emit = defineEmits<{
'update:form-config': [formConfig: FormConfig]
}>()
const props = defineProps({
formConfig: {
type: Object as PropType<FormConfig>,
required: true,
},
})
const { formConfig } = toRefs(props)
const formItems = ref<FormItem[]>([
{
type: FormItemTypeEnum.RADIO,
group: 'form',
config: {
label: '布局形式',
name: 'layout',
},
properties: {
optionType: 'button',
buttonStyle: 'solid',
size: 'small',
options: [
{
label: '水平',
value: 'horizontal',
},
{
label: '垂直',
value: 'vertical',
},
{
label: '行内',
value: 'inline',
},
],
disabled: false,
},
rules: [],
},
{
type: FormItemTypeEnum.SWITCH,
group: 'form',
config: {
label: '显示冒号',
name: 'colon',
},
properties: {
size: 'small',
checkedChildren: '显示',
unCheckedChildren: '隐藏',
disabled: false,
},
rules: [],
},
{
type: FormItemTypeEnum.SWITCH,
group: 'form',
config: {
label: '隐藏必选标记',
name: 'hideRequiredMark',
},
properties: {
size: 'small',
checkedChildren: '隐藏',
unCheckedChildren: '显示',
disabled: false,
},
rules: [],
},
{
type: FormItemTypeEnum.RADIO,
group: 'form',
config: {
label: '标签对齐方式',
name: 'labelAlign',
},
properties: {
size: 'small',
optionType: 'button',
buttonStyle: 'solid',
options: [
{
label: '左对齐',
value: 'left',
},
{
label: '右对齐',
value: 'right',
},
],
disabled: false,
},
rules: [],
},
{
type: FormItemTypeEnum.RADIO,
group: 'form',
config: {
label: '标签模式',
name: 'labelType',
},
properties: {
size: 'small',
optionType: 'button',
buttonStyle: 'solid',
options: [
{
label: '固定宽度',
value: 'fixed',
},
{
label: '响应式',
value: 'responsive',
},
],
disabled: false,
},
rules: [],
},
{
type: FormItemTypeEnum.INPUT_NUMBER,
group: 'form',
config: {
label: '标签宽度',
name: 'labelColFixedWidthInPx',
},
properties: {
size: 'small',
disabled: false,
addonAfter: 'px',
},
rules: [],
},
{
type: FormItemTypeEnum.INPUT_NUMBER,
group: 'form',
config: {
label: '标签宽度',
name: 'labelColSpanWidth',
},
properties: {
placeholder: '请输入标签宽度',
size: 'small',
disabled: false,
addonBefore: 'span',
max: 12,
min: 1,
},
rules: [],
},
{
type: FormItemTypeEnum.SWITCH,
group: 'form',
config: {
label: '提交失败自动滚动到第一个错误字段 ',
name: 'scrollToFirstError',
},
properties: {
size: 'small',
disabled: false,
},
rules: [],
},
{
type: FormItemTypeEnum.SWITCH,
group: 'form',
config: {
label: '是否在 rules 属性改变后立即触发一次验证 ',
name: 'validateOnRuleChange',
},
properties: {
size: 'small',
disabled: false,
},
rules: [],
},
])
const hiddenFields = computed(() => {
return [
...(formConfig.value.layout === 'horizontal' ? [] : ['colon']),
...(formConfig.value.labelType === 'fixed' ? ['labelColSpanWidth'] : ['labelColFixedWidthInPx']),
]
})
const formData = computed({
get: () => {
return formConfig.value
},
set: (val) => {
if (val.labelType === 'fixed') {
val.labelCol = { style: { width: `${val.labelColFixedWidthInPx}px` } }
} else {
val.labelCol = { span: val.labelColSpanWidth }
}
emit('update:form-config', val)
},
})
</script>

View File

@@ -0,0 +1,379 @@
<template>
<form-render
v-model="formData"
:config="{
layout: 'vertical',
colon: true,
hideRequiredMark: false,
labelAlign: 'right',
labelType: 'fixed',
scrollToFirstError: false,
validateOnRuleChange: true,
labelCol: {},
}"
:hidden-fields="hiddenFields"
:items="formItems"
/>
</template>
<script lang="ts" setup>
import XEUtils from 'xe-utils'
import { PropType } from 'vue'
import { FormConfig, FormItem, FormItemConfig, FormItemTypeEnum } from '@/components/form-render/form-render-types'
const props = defineProps({
formItem: {
type: Object as PropType<FormItem>,
required: true,
},
formConfig: {
type: Object as PropType<FormConfig>,
required: false,
default: () => {
return {}
},
},
})
const { formItem, formConfig } = toRefs(props)
const formItems = ref<FormItem[]>([
{
type: FormItemTypeEnum.INPUT,
group: 'form',
config: {
label: '表单域',
name: 'name',
},
properties: {
placeholder: '请输入表单域',
disabled: false,
required: true,
},
rules: [],
},
{
type: FormItemTypeEnum.INPUT,
group: 'form',
config: {
label: '标签文本',
name: 'label',
},
properties: { placeholder: '请输入标签文本' },
rules: [],
},
{
type: FormItemTypeEnum.SWITCH,
group: 'form',
config: {
label: '是否必填',
name: 'required',
},
properties: {
size: 'small',
disabled: false,
},
rules: [],
},
{
type: FormItemTypeEnum.SWITCH,
group: 'form',
config: {
label: '使用表单配置',
name: 'useFormConfig',
},
properties: {
size: 'small',
disabled: false,
},
rules: [],
},
{
type: FormItemTypeEnum.CHECKBOX,
group: 'form',
config: {
label: '字段校验的时机',
name: 'validateTrigger',
},
properties: {
options: [
{
label: '失去焦点',
value: 'blur',
},
{
label: '值变化',
value: 'change',
},
],
disabled: false,
},
rules: [],
},
{
type: FormItemTypeEnum.SWITCH,
group: 'form',
config: {
label: '检验不通过停止校验下一项',
name: 'validateFirst',
},
properties: {
size: 'small',
disabled: false,
},
rules: [],
},
{
type: FormItemTypeEnum.SWITCH,
group: 'form',
config: {
label: '展示校验状态图标',
name: 'hasFeedback',
},
properties: {
size: 'small',
disabled: false,
},
rules: [],
},
{
type: FormItemTypeEnum.RADIO,
group: 'form',
config: {
label: '校验状态',
name: 'validateStatus',
},
properties: {
size: 'small',
optionType: 'button',
buttonStyle: 'solid',
options: [
{
label: '成功',
value: 'success',
},
{
label: '警告',
value: 'warning',
},
{
label: '错误',
value: 'error',
},
{
label: '验证中',
value: 'validating',
},
],
disabled: false,
},
rules: [],
},
{
type: FormItemTypeEnum.INPUT,
group: 'form',
config: {
label: '提示信息',
name: 'help',
},
properties: {
disabled: false,
placeholder: '请输入提示信息',
},
rules: [],
},
{
type: FormItemTypeEnum.INPUT,
group: 'form',
config: {
label: '额外的提示信息',
name: 'extra',
},
properties: {
disabled: false,
placeholder: '请输入额外的提示信息',
},
rules: [],
},
{
type: FormItemTypeEnum.SWITCH,
group: 'form',
config: {
label: '显示冒号',
name: 'colon',
},
properties: {
size: 'small',
checkedChildren: '显示',
unCheckedChildren: '隐藏',
disabled: false,
},
rules: [],
},
{
type: FormItemTypeEnum.SWITCH,
group: 'form',
config: {
label: '是否自动关联表单域',
name: 'autoLink',
},
properties: {
size: 'small',
disabled: false,
},
rules: [],
},
{
type: FormItemTypeEnum.INPUT,
group: 'form',
config: {
label: '子元素 label htmlFor 属性',
name: 'htmlFor',
},
properties: {
disabled: false,
placeholder: '请输入子元素 label htmlFor 属性',
},
rules: [],
},
{
type: FormItemTypeEnum.RADIO,
group: 'form',
config: {
label: '标签对齐方式',
name: 'labelAlign',
},
properties: {
size: 'small',
optionType: 'button',
buttonStyle: 'solid',
options: [
{
label: '左对齐',
value: 'left',
},
{
label: '右对齐',
value: 'right',
},
],
disabled: false,
},
rules: [],
},
{
type: FormItemTypeEnum.RADIO,
group: 'form',
config: {
label: '标签模式',
name: 'labelType',
},
properties: {
size: 'small',
optionType: 'button',
buttonStyle: 'solid',
options: [
{
label: '固定宽度',
value: 'fixed',
},
{
label: '响应式',
value: 'responsive',
},
],
disabled: false,
},
rules: [],
},
{
type: FormItemTypeEnum.INPUT_NUMBER,
group: 'form',
config: {
label: '标签宽度',
name: 'labelColFixedWidthInPx',
},
properties: {
size: 'small',
disabled: false,
placeholder: '请输入标签宽度,单位(px)',
addonAfter: 'px',
},
rules: [],
},
{
type: FormItemTypeEnum.INPUT_NUMBER,
group: 'form',
config: {
label: '标签宽度',
name: 'labelColSpanWidth',
},
properties: {
size: 'small',
disabled: false,
addonBefore: 'span',
placeholder: '请输入标签宽度.响应式span数值',
max: 12,
min: 1,
},
rules: [],
},
])
const hiddenFields = computed(() => {
return [
...(formData.value.useFormConfig
? ['validateTrigger', 'colon', 'labelAlign', 'labelType', 'labelColSpanWidth', 'labelColFixedWidthInPx']
: []),
...(formData.value.labelType === 'fixed' ? ['labelColSpanWidth'] : ['labelColFixedWidthInPx']),
]
})
const emit = defineEmits<{
'update:form-item-config': [formConfig: FormItemConfig]
}>()
const formData = computed({
get: (): FormItemConfig => {
return XEUtils.assign(
{
autoLink: true,
colon: true,
hasFeedback: false,
labelAlign: 'right',
labelType: 'responsive',
useFormConfig: true,
validateTrigger: ['blur'],
labelCol: {
style: {
width: '120px',
},
span: 4,
offset: 0,
},
labelColFixedWidthInPx: 120,
labelColSpanWidth: 4,
},
XEUtils.clone(formConfig.value, true),
formItem.value.config,
)
},
set: (val) => {
if (val.labelType === 'fixed') {
val.labelCol = { style: { width: `${val.labelColFixedWidthInPx}px` } }
} else {
val.labelCol = { span: val.labelColSpanWidth }
}
if (val.useFormConfig) {
delete val.validateTrigger
delete val.colon
delete val.labelAlign
delete val.labelType
delete val.labelColFixedWidthInPx
delete val.labelColSpanWidth
delete val.labelCol
}
delete (val as FormConfig).layout
delete (val as FormConfig).hideRequiredMark
delete (val as FormConfig).scrollToFirstError
delete (val as FormConfig).validateOnRuleChange
emit('update:form-item-config', val)
},
})
</script>

View File

@@ -0,0 +1,188 @@
<template>
<a-table size="small" :data-source="shownOptions" :columns="columns" :pagination="false" row-key="uuid">
<template #title>
<a-button type="primary" size="small" @click="addOption">
<template #icon>
<icon-font type="icon-plus"></icon-font>
</template>
添加
</a-button>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'ops'">
<a-space>
<a-button type="primary" size="small" @click="editOption(record)">
<template #icon>
<icon-font type="icon-edit"></icon-font>
</template>
</a-button>
<a-button v-if="tree" type="primary" size="small" @click="addClildOption(record)">
<template #icon>
<icon-font type="icon-tree"></icon-font>
</template>
</a-button>
<a-popconfirm title="确定删除这个选项?" @confirm="deleteOption(record)">
<a-button type="primary" size="small" danger>
<template #icon>
<icon-font type="icon-delete"></icon-font>
</template>
</a-button>
</a-popconfirm>
</a-space>
</template>
</template>
</a-table>
<form-modal ref="formModal" v-model="option" :form-items="formItems" :title="title" @ok="hanldOk"></form-modal>
</template>
<script lang="ts" setup>
import FormModal from '@/components/form-render/form-modal.vue'
import { FormDataType, FormItem, FormItemTypeEnum, Option } from '@/components/form-render/form-render-types'
import { PropType } from 'vue'
import XEUtils from 'xe-utils'
const props = defineProps({
options: {
type: Array as PropType<Option[]>,
required: false,
default: () => [],
},
tree: {
type: Boolean,
required: false,
default: () => false,
},
})
const { options, tree } = toRefs(props)
const columns = [
{
title: '标签',
dataIndex: 'label',
key: 'label',
},
{
title: '值',
dataIndex: 'value',
key: 'value',
},
{
title: '操作',
key: 'ops',
width: 100,
},
]
const emit = defineEmits<{
'update:options': [value: Option[]]
}>()
const shownOptions = computed(() => {
return XEUtils.mapTree(XEUtils.clone(options.value, true), (item) => {
return XEUtils.assign(XEUtils.clone(item, true), { uuid: XEUtils.uniqueId() })
})
})
const formItems = reactive<FormItem[]>([
{
group: 'form',
type: FormItemTypeEnum.INPUT,
config: {
autoLink: true,
hasFeedback: false,
label: '标签',
name: 'label',
required: true,
},
properties: {
size: 'default',
type: 'text',
allowClear: true,
bordered: true,
showCount: true,
placeholder: '请输入选项标签',
},
rules: [],
},
{
group: 'form',
type: FormItemTypeEnum.INPUT,
config: {
autoLink: true,
hasFeedback: false,
label: '值',
name: 'value',
required: true,
},
properties: {
size: 'default',
type: 'text',
allowClear: true,
bordered: true,
showCount: true,
placeholder: '请输入选项值',
},
rules: [],
},
])
const selected = ref<Partial<Option & { uuid: string }>>({})
const option = ref({})
const formModal = ref<typeof FormModal>()
const title = ref('添加选项')
const addClildOption = (_option: Option) => {
title.value = '添加子选项'
option.value = {}
selected.value = _option
formModal.value?.open()
}
const addOption = () => {
option.value = {}
selected.value = {}
title.value = '添加选项'
formModal.value?.open()
}
const editOption = (_option: Option) => {
selected.value = {}
option.value = _option
title.value = '编辑选项'
formModal.value?.open()
}
const deleteOption = (_option: Option) => {
emit(
'update:options',
XEUtils.mapTree(
XEUtils.searchTree(shownOptions.value, (item) => item.uuid !== _option.uuid),
(item) => {
if (item.children && (item.children as Array<unknown>).length === 0) {
delete item.children
}
return item
},
),
)
}
const hanldOk = (_option: FormDataType) => {
let options = XEUtils.clone(shownOptions.value, true)
if (_option.uuid) {
options = XEUtils.mapTree(options, (item) => {
if (item.uuid === _option.uuid) {
item = _option as Option & { uuid: string }
}
return item
})
} else if (!selected.value.uuid) {
options.push(_option as Option & { uuid: string })
} else {
options = XEUtils.mapTree(options, (item) => {
if (item.uuid === selected.value.uuid) {
if (item.children) {
item.children = (item.children as Array<unknown>).concat([_option])
} else {
item.children = [_option]
}
}
return item
})
}
emit('update:options', options)
formModal.value?.close()
}
</script>

View File

@@ -0,0 +1,108 @@
<template>
<div>
<form-render
v-model="formData"
:config="{
layout: 'vertical',
colon: true,
hideRequiredMark: false,
labelAlign: 'right',
labelType: 'fixed',
scrollToFirstError: false,
validateOnRuleChange: true,
labelCol: {},
}"
:hidden-fields="hiddenFields"
:items="formItems"
>
<template v-if="formItem.type === FormItemTypeEnum.TRANSFER" #before-oneWay>
<a-form-item label="数据源">
<data-source-table
:data-source="formItem.properties.dataSource || []"
@update:data-source="dataSourceChange"
></data-source-table>
</a-form-item>
</template>
<template v-if="formItem.type === FormItemTypeEnum.TREE_SELECT" #after-size>
<a-form-item label="数据源">
<tree-data-table
:tree-data="formItem.properties.treeData || []"
@update:tree-data="treeDataChange"
></tree-data-table>
</a-form-item>
</template>
</form-render>
<option-table
v-if="showOptionsTable"
:options="formItem.properties.options"
:tree="tree"
@update:options="optionsChange"
></option-table>
</div>
</template>
<script lang="ts" setup>
import { DataSource, FormItem, FormItemTypeEnum, Option } from '@/components/form-render/form-render-types'
import { TreeDataNode } from 'ant-design-vue/es/vc-tree-select/interface'
const props = defineProps({
formItem: {
type: Object as PropType<FormItem>,
required: true,
},
})
const { formItem } = toRefs(props)
const tree = computed(() => {
return formItem.value.type === FormItemTypeEnum.CASCADER
})
const showOptionsTable = computed(() => {
return (
formItem.value.type === FormItemTypeEnum.RADIO ||
formItem.value.type === FormItemTypeEnum.SELECT ||
formItem.value.type === FormItemTypeEnum.AUTO_COMPLETE ||
formItem.value.type === FormItemTypeEnum.MENTION ||
formItem.value.type === FormItemTypeEnum.CHECKBOX ||
formItem.value.type === FormItemTypeEnum.CASCADER
)
})
const formItems = computed(() => {
return formItem.value.propertyConfigFormComponents ?? []
})
const hiddenFields = computed(() => {
if (formItem.value.hiddenFieldsFun) {
return formItem.value.hiddenFieldsFun(formData.value)
}
return []
})
const emit = defineEmits<{
'update:form-item': [item: FormItem]
}>()
const optionsChange = (_options: Option[]) => {
formItem.value.properties.options = _options
emit('update:form-item', formItem.value)
}
const dataSourceChange = (_dataSource: DataSource[]) => {
formItem.value.properties.dataSource = _dataSource
emit('update:form-item', formItem.value)
}
const treeDataChange = (_treeData: TreeDataNode[]) => {
formItem.value.properties.treeData = _treeData
emit('update:form-item', formItem.value)
}
const formData = computed({
get: () => {
return formItem.value.properties
},
set: (val) => {
formItem.value.properties = val
emit('update:form-item', formItem.value)
},
})
</script>

View File

@@ -0,0 +1,194 @@
<template>
<a-table size="small" :data-source="shownTreeData" :columns="columns" :pagination="false" row-key="uuid">
<template #title>
<a-button type="primary" size="small" @click="add">
<template #icon>
<icon-font type="icon-plus"></icon-font>
</template>
添加
</a-button>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'ops'">
<a-space>
<a-button type="primary" size="small" @click="edit(record as DataSource)">
<template #icon>
<icon-font type="icon-edit"></icon-font>
</template>
</a-button>
<a-button type="primary" size="small" @click="addChild(record as DataSource)">
<template #icon>
<icon-font type="icon-tree"></icon-font>
</template>
</a-button>
<a-popconfirm title="确定删除这个选项?" @confirm="deleteData(record as DataSource)">
<a-button type="primary" size="small" danger>
<template #icon>
<icon-font type="icon-delete"></icon-font>
</template>
</a-button>
</a-popconfirm>
</a-space>
</template>
</template>
</a-table>
<form-modal ref="formModal" v-model="data" :form-items="formItems" :title="title" @ok="hanldOk"></form-modal>
</template>
<script lang="ts" setup>
import { DataSource, FormDataType, FormItem, FormItemTypeEnum } from '@/components/form-render/form-render-types'
import { TreeDataNode } from 'ant-design-vue/es/vc-tree-select/interface'
import FormModal from '@/components/form-render/form-modal.vue'
import XEUtils from 'xe-utils'
const props = defineProps({
treeData: {
type: Array as PropType<Array<TreeDataNode>>,
required: false,
default: () => [],
},
})
const { treeData } = toRefs(props)
const shownTreeData = computed(() => {
return XEUtils.mapTree(XEUtils.clone(treeData.value, true), (item) => {
return XEUtils.assign(XEUtils.clone(item, true), { uuid: XEUtils.uniqueId() })
})
})
const columns = [
{
title: '标签',
dataIndex: 'label',
key: 'label',
},
{
title: '值',
dataIndex: 'value',
key: 'value',
},
{
title: '操作',
key: 'ops',
width: 100,
},
]
const formItems = reactive<FormItem[]>([
{
group: 'form',
type: FormItemTypeEnum.INPUT,
config: {
autoLink: true,
hasFeedback: false,
label: '标签',
name: 'label',
required: true,
},
properties: {
size: 'small',
type: 'text',
allowClear: true,
bordered: true,
showCount: true,
placeholder: '请输入标签',
},
rules: [],
},
{
group: 'form',
type: FormItemTypeEnum.INPUT,
config: {
autoLink: true,
hasFeedback: false,
label: '值',
name: 'value',
required: true,
},
properties: {
size: 'small',
type: 'text',
allowClear: true,
bordered: true,
showCount: true,
placeholder: '请输入值',
},
rules: [],
},
])
const formModal = ref<typeof FormModal>()
const data = ref<Partial<TreeDataNode>>({})
const state = ref<'add' | 'child' | 'edit'>('add')
const title = computed(() => {
switch (state.value) {
case 'add':
return '添加数据项'
case 'child':
return '添加数据子项'
default:
return '编辑数据项'
}
})
const add = () => {
data.value = {}
state.value = 'add'
formModal.value?.open()
}
const selected = ref<TreeDataNode>()
const addChild = (_data: TreeDataNode) => {
data.value = {}
state.value = 'child'
selected.value = _data
formModal.value?.open()
}
const edit = (_data: TreeDataNode) => {
data.value = _data
state.value = 'edit'
formModal.value?.open()
}
const emit = defineEmits<{
'update:tree-data': [value: TreeDataNode[]]
}>()
const hanldOk = (_data: FormDataType) => {
let options = XEUtils.clone(shownTreeData.value, true)
if (_data.uuid) {
options = XEUtils.mapTree(options, (item) => {
if (item.uuid === _data.uuid) {
item = _data as TreeDataNode & { uuid: string }
}
return item
})
} else if (!selected.value?.uuid) {
options.push(_data as TreeDataNode & { uuid: string })
} else {
options = XEUtils.mapTree(options, (item) => {
if (item.uuid === selected.value?.uuid) {
if (item.children) {
item.children = (item.children as Array<TreeDataNode>).concat([_data as TreeDataNode])
} else {
item.children = [_data as TreeDataNode]
}
}
return item
})
}
emit('update:tree-data', options)
formModal.value?.close()
}
const deleteData = (_data: TreeDataNode) => {
emit(
'update:tree-data',
XEUtils.mapTree(
XEUtils.searchTree(shownTreeData.value, (item) => item.uuid !== _data.uuid),
(item) => {
if (item.children && (item.children as Array<unknown>).length === 0) {
delete item.children
}
return item
},
),
)
}
</script>

View File

@@ -0,0 +1,413 @@
<template>
<div>
<a-list item-layout="vertical" :data-source="shownRules">
<template #header>
<a-button type="primary" size="small" @click="add">
<template #icon>
<icon-font type="icon-plus"></icon-font>
</template>
添加
</a-button>
</template>
<template #renderItem="{ item }">
<a-list-item>
<template #actions>
<a-space>
<span v-if="item.len">
长度:{{ item.len }}
<a-divider type="vertical" />
</span>
<span v-if="item.max">
最大:{{ item.max }}
<a-divider type="vertical" />
</span>
<span v-if="item.min">
最小:{{ item.min }}
<a-divider type="vertical" />
</span>
<span v-if="item.required">
必填
<a-divider type="vertical" />
</span>
<span v-if="item.whitespace">空格判错</span>
</a-space>
</template>
<a-list-item-meta>
<template #title>类型: {{ typeTransfer(item.type) }}</template>
<template #description>
<span v-if="item.trigger">触发: {{ triggerTransfer(item.trigger) }}</span>
<br />
<span v-if="item.enum">枚举值: {{ item.enum }}</span>
<br />
验证消息: {{ item.message }}
<br />
<span v-if="item.pattern">验证正则: {{ item.pattern }}</span>
</template>
</a-list-item-meta>
<template #extra>
<a-space>
<a-button type="primary" size="small" @click="edit(item)">
<template #icon>
<icon-font type="icon-edit"></icon-font>
</template>
</a-button>
<a-popconfirm title="确定删除这个校验规则?" @confirm="deleteRule(item)">
<a-button type="primary" danger size="small">
<template #icon>
<icon-font type="icon-delete"></icon-font>
</template>
</a-button>
</a-popconfirm>
</a-space>
</template>
</a-list-item>
</template>
</a-list>
<form-modal
ref="formModal"
v-model="rule"
:form-items="formItems"
:config="config"
:title="title"
:hidden-fields="hiddenFields"
@ok="hanldOk"
></form-modal>
</div>
</template>
<script lang="ts" setup>
import { FormConfig, FormDataType, FormItem } from '@/components/form-render/form-render-types'
import { Rule } from 'ant-design-vue/lib/form'
import { PropType } from 'vue'
import FormModal from '@/components/form-render/form-modal.vue'
import XEUtils from 'xe-utils'
const props = defineProps({
rules: {
type: Array as PropType<Rule[]>,
requried: false,
default: () => [],
},
})
const { rules } = toRefs(props)
const shownRules = computed(() => {
return XEUtils.mapTree(XEUtils.clone(rules.value, true), (item) => {
return XEUtils.assign(XEUtils.clone(item, true), { uuid: XEUtils.uniqueId() })
})
})
const formItems = reactive<FormItem[]>([
{
type: 'radio',
group: 'form',
config: {
autoLink: true,
hasFeedback: false,
label: '校验类型',
name: 'type',
required: true,
},
properties: {
size: 'small',
optionType: 'button',
buttonStyle: 'solid',
options: [
{
label: '字符串',
value: 'string',
},
{
value: 'date',
label: '日期',
},
{
value: 'url',
label: 'URL',
},
{
value: 'hex',
label: 'HEX',
},
{
value: 'email',
label: '邮件',
},
{
label: '数字',
value: 'number',
},
{
value: 'integer',
label: '整数',
},
{
value: 'float',
label: '小数',
},
{
value: 'enum',
label: '枚举',
},
{
value: 'boolean',
label: '布尔',
},
{
value: 'method',
label: '方法',
},
{
value: 'regexp',
label: '正则',
},
{
value: 'array',
label: '数组',
},
{
value: 'object',
label: '对象',
},
],
},
rules: [],
},
{
type: 'checkbox',
group: 'form',
config: {
autoLink: true,
hasFeedback: false,
label: '校验触发时机',
name: 'trigger',
},
properties: {
options: [
{
label: '失焦',
value: 'blur',
},
{
label: '变化',
value: 'change',
},
],
},
rules: [],
},
{
type: 'switch',
group: 'form',
config: {
autoLink: true,
hasFeedback: false,
label: '是否必填',
name: 'required',
},
properties: {
size: 'small',
},
rules: [],
},
{
type: 'switch',
group: 'form',
config: {
autoLink: true,
hasFeedback: false,
label: '空格视为错误',
name: 'whitespace',
},
properties: {
size: 'small',
},
rules: [],
},
{
group: 'form',
type: 'input',
config: {
autoLink: true,
hasFeedback: false,
label: '枚举类型',
name: 'enum',
required: true,
},
properties: {
size: 'small',
type: 'text',
allowClear: true,
bordered: true,
showCount: true,
placeholder: "请输入枚举类型,','分隔",
},
rules: [],
},
{
group: 'form',
type: 'input',
config: {
autoLink: true,
hasFeedback: false,
label: '校验文案',
name: 'message',
required: true,
},
properties: {
size: 'small',
type: 'text',
allowClear: true,
bordered: true,
showCount: true,
placeholder: '请输入校验文案',
},
rules: [],
},
{
type: 'input-number',
group: 'form',
config: {
autoLink: true,
hasFeedback: false,
label: '字段长度',
name: 'len',
},
properties: {
size: 'small',
controls: true,
placeholder: '请输入字段长度',
},
rules: [],
},
{
type: 'input-number',
group: 'form',
config: {
autoLink: true,
hasFeedback: false,
label: '最大长度',
name: 'max',
},
properties: {
size: 'small',
controls: true,
placeholder: '请输入最大长度',
},
rules: [],
},
{
type: 'input-number',
group: 'form',
config: {
autoLink: true,
hasFeedback: false,
label: '最小长度',
name: 'min',
},
properties: {
size: 'small',
controls: true,
placeholder: '请输入最小长度',
},
rules: [],
},
{
group: 'form',
type: 'input',
config: {
autoLink: true,
hasFeedback: false,
label: '检验表达式',
name: 'pattern',
},
properties: {
size: 'small',
type: 'text',
allowClear: true,
bordered: true,
showCount: true,
placeholder: '请输入检验表达式',
},
rules: [],
},
])
const typeTransfer = (type: string) => {
const option = formItems[0].properties.options?.find((item) => item.value === type)
return option ? option.label : type
}
const triggerTransfer = (_trigger: string[]) => {
return _trigger
.map((item) => {
return [
{
label: '失焦',
value: 'blur',
},
{
label: '变化',
value: 'change',
},
].find((o) => o.value === item)?.label
})
.join(',')
}
const config = reactive<FormConfig>({
layout: 'horizontal',
colon: true,
hideRequiredMark: false,
labelAlign: 'right',
labelType: 'responsive',
scrollToFirstError: false,
validateOnRuleChange: true,
labelCol: { span: 6 },
})
const rule = ref<FormDataType>({ required: true, whitespace: true, trigger: ['blur'] })
const title = ref('添加规则')
const hiddenFields = computed(() => {
return [...(rule.value.required ? [] : ['whitespace']), ...(rule.value.type === 'enum' ? [] : ['enum'])]
})
const formModal = ref<typeof FormModal>()
const add = () => {
rule.value = { type: 'string', required: true, whitespace: true, trigger: ['blur'] }
title.value = '添加规则'
formModal.value?.open()
}
const edit = (_rule: FormDataType) => {
rule.value = _rule
title.value = '编辑规则'
formModal.value?.open()
}
const emit = defineEmits<{
'update:rules': [value: Rule[]]
}>()
const deleteRule = (_rule: FormDataType) => {
emit(
'update:rules',
XEUtils.filter(shownRules.value, (item) => item.uuid !== _rule.uuid),
)
}
const hanldOk = (_data: FormDataType) => {
let d = XEUtils.clone(shownRules.value, true)
if (_data.uuid) {
d = XEUtils.map(d, (item: Rule & { uuid: string }) => {
if (item.uuid === _data.uuid) {
return _data as unknown as Rule & { uuid: string }
}
return item
})
} else {
d.push(_data as unknown as Rule & { uuid: string })
}
emit('update:rules', d)
formModal.value?.close()
}
</script>

View File

@@ -0,0 +1,10 @@
import { FormItem } from '../form-render/form-render-types'
export type FormDesignerItem = {
uuid?: string
type: string
name: string
icon: string
formItem: FormItem
children?: FormDesignerItem[]
}

View File

@@ -0,0 +1,57 @@
<template>
<a-row :gutter="[8, 8]">
<a-col v-if="showDrag" :span="8">
<ComponentPanel />
</a-col>
<a-col :span="16">
<ComponentContainer :form-config="formConfig" :selected="selected" @selected-change="selectedChange" />
</a-col>
<a-col v-if="!showDrag" :span="8">
<ConfigPanel
:form-config="formConfig"
:current-item="selected"
@update:form-config="onUpdateFormConfig"
@update:form-item="onUpdateFormItem"
/>
</a-col>
</a-row>
</template>
<script lang="ts" setup>
import XEUtils from 'xe-utils'
import { FormConfig } from '../form-render/form-render-types'
import { FormDesignerItem } from './form-designer-types'
const showDrag = ref(true)
const formConfig = ref<FormConfig>({
layout: 'horizontal',
colon: true,
hideRequiredMark: false,
labelAlign: 'right',
labelType: 'responsive',
scrollToFirstError: false,
validateOnRuleChange: true,
labelCol: {
style: {
width: '120px',
},
span: 4,
offset: 0,
},
labelColFixedWidthInPx: 120,
labelColSpanWidth: 4,
})
const selected = ref<FormDesignerItem>()
const selectedChange = (formDesignerItem?: FormDesignerItem) => {
showDrag.value = !formDesignerItem
selected.value = formDesignerItem
}
const onUpdateFormConfig = (_formConfig: FormConfig) => {
formConfig.value = _formConfig
}
const onUpdateFormItem = (_formItem: FormDesignerItem) => {
selected.value = XEUtils.assign(selected.value, _formItem)
}
</script>

View File

@@ -0,0 +1,52 @@
<template>
<div class="auxiliary-item-wrapper">
<component :is="typeMap(item.type)" v-bind="Object.assign({}, $attrs, item.properties)">
<template v-if="item.type === FormItemTypeEnum.DIVIDER">
{{ item.properties.text }}
</template>
<template v-if="item.type === FormItemTypeEnum.SPACE">
<span>&nbsp;</span>
</template>
<template v-if="item.type === FormItemTypeEnum.TYPOGRAPHY_TEXT && !item.properties.editable">
{{ item.properties.content }}
</template>
</component>
</div>
</template>
<script lang="ts">
// 取消自动继承到跟节点的属性,否则会导致弹出两次事件(父节点与子节点分别弹出)
export default {
inheritAttrs: false,
}
</script>
<script lang="ts" setup>
import { toRefs, type PropType } from 'vue'
import { FormItem, FormItemTypeEnum } from './form-render-types'
const props = defineProps({
item: {
type: Object as PropType<FormItem>,
required: true,
},
})
const { item } = toRefs(props)
/**
* 将组件类型映射为辅助项组件名称
* 可作为扩展点,自定义映射关系,这里先按照 antd 写死
*/
const typeMap = (type: FormItemTypeEnum | string): string => {
return 'a-' + type
}
</script>
<style lang="css" scoped>
.auxiliary-item-wrapper {
.text-item {
padding: 10px 0;
word-break: break-all;
word-wrap: break-word;
}
}
</style>

View File

@@ -0,0 +1,198 @@
<template>
<a-drawer
v-model:open="open"
class="custom-class"
:root-class-name="wrapClassName"
:title="title"
placement="right"
:width="width"
:mask-closable="false"
:footer-style="{ textAlign: 'right' }"
@after-open-change="afterOpenChange"
>
<form-render
ref="formRender"
v-model="bindValue"
:config="config"
:popup-container-id="wrapClassId"
:items="formItems"
:disabled-fields="disabledFields"
:hidden-fields="hiddenFields"
v-bind="$attrs"
>
<!-- 透传插槽 -->
<template v-for="(item, key, index) in $slots" :key="index" #[key]>
<template v-if="key != 'modal-footer'"><slot :name="key" :item="item" /></template>
</template>
</form-render>
<template #footer>
<a-button style="margin-right: 8px" @click="open = false">
<template #icon>
<icon-font type="icon-reset"></icon-font>
</template>
{{ $t('form.drawer.cancel') }}
</a-button>
<a-button type="primary" @click="handleOk">
<template #icon>
<icon-font type="icon-send"></icon-font>
</template>
{{ $t('form.drawer.submit') }}
</a-button>
</template>
</a-drawer>
</template>
<script setup lang="ts">
import XEUtils from 'xe-utils'
import FormRender from './form-render.vue'
import { FormValueType, FormConfig, FormItem } from './form-render-types'
import { NamePath } from 'ant-design-vue/es/form/interface'
const props = defineProps({
wrapClassName: { type: String, default: '' },
modalContentWrapperClassName: { type: String, default: '' },
modelValue: {
type: Object as PropType<Record<string, FormValueType>>,
required: true,
},
title: {
type: String,
default: () => {
return undefined
},
},
// 表单项
formItems: {
type: Array as PropType<Array<FormItem>>,
default: () => {
return []
},
},
config: {
type: Object as PropType<FormConfig>,
required: false,
default: () => {
return {
layout: 'horizontal',
colon: true,
hideRequiredMark: false,
labelAlign: 'right',
labelType: 'fixed',
scrollToFirstError: false,
validateOnRuleChange: true,
labelCol: { span: 4 },
}
},
},
disabledFields: {
type: Array as PropType<Array<string>>,
required: false,
default: () => {
return []
},
},
// 要隐藏的表单项
hiddenFields: {
type: Array as PropType<Array<string>>,
required: false,
default: () => {
return []
},
},
// 最大高度
maxHeight: {
type: [String, Number],
required: false,
default: 'none',
},
// 弹框的宽
width: {
type: String,
required: false,
default: '750px',
},
})
const { modelValue, title, width, wrapClassName, config, disabledFields, hiddenFields } = toRefs(props)
const open = ref<boolean>(false)
const afterOpenChange = (bool: boolean) => {
if (bool) {
formRender.value?.clearValidate()
}
}
const emit = defineEmits<{
'update:model-value': [value: Record<string, FormValueType>]
ok: [value: Record<string, FormValueType>]
}>()
const wrapClassId = ref(XEUtils.uniqueId())
const bindValue = computed({
get: function () {
return modelValue.value
},
set: function (value) {
emit('update:model-value', value)
},
})
const formRender = ref<typeof FormRender>()
// 清空表单状态
const reset = () => {
bindValue.value = {}
formRender.value?.clearValidate()
}
// 打开弹框
const show = () => {
open.value = true
}
// 关闭弹框,并清空表单状态
const close = () => {
reset()
open.value = false
}
watch(
open,
(newVal) => {
if (newVal) {
nextTick(() => {
formRender.value?.clearValidate()
})
}
},
{ immediate: true },
)
const handleOk = async () => {
if (!formRender.value) {
throw new Error('获取 formRender 失败')
}
try {
await formRender.value.validateFields()
emit('ok', bindValue.value)
} catch (errorInfo) {
console.error('errorInfo', errorInfo)
}
}
// 校验表单
const validateFields = async (nameList?: Array<NamePath>) => {
if (!formRender.value) {
throw new Error('获取 formRender 失败')
}
return await formRender.value.validateFields(nameList)
}
// 重置表单
const resetFields = () => {
formRender.value?.resetFields()
}
defineExpose({
show,
close,
reset,
validateFields,
resetFields,
})
</script>

View File

@@ -0,0 +1,118 @@
<template>
<a-form-item v-bind="item.config" :rules="item.rules">
<slot v-if="item.type == FormItemTypeEnum.CUSTOM" :name="item.config?.name">
{{ item.config?.name }}
</slot>
<component
:is="typeMap(item.type)"
v-else-if="
item.type == FormItemTypeEnum.UPLOAD ||
item.type == FormItemTypeEnum.SELECT ||
item.type == FormItemTypeEnum.MENTION
"
v-model:[bindPropName(item.type)]="bindValue"
v-bind="Object.assign({}, $attrs, properties)"
:disabled="disabled"
>
<a-button v-if="item.type == FormItemTypeEnum.UPLOAD">
<upload-outlined />
</a-button>
<template v-else-if="item.type == FormItemTypeEnum.SELECT && properties.options">
<a-select-option v-for="(option, index2) in properties.options" :key="index2" :value="option.value">
{{ option.label }}
</a-select-option>
</template>
<template v-else-if="item.type == FormItemTypeEnum.MENTION && properties.options">
<a-mentions-option v-for="(option, index2) in properties.options" :key="index2" :value="String(option.value)">
{{ option.label }}
</a-mentions-option>
</template>
</component>
<component
:is="typeMap(item.type)"
v-else
v-model:[bindPropName(item.type)]="bindValue"
v-bind="Object.assign({}, $attrs, properties)"
:disabled="disabled"
/>
</a-form-item>
</template>
<script lang="ts" setup>
import { UploadOutlined } from '@ant-design/icons-vue'
import { PropType } from 'vue'
import { FormItem, FormItemTypeEnum, FormValueType } from './form-render-types'
import XEUtils from 'xe-utils'
const props = defineProps({
item: {
type: Object as PropType<FormItem>,
required: false,
default: () => {
return {}
},
},
disabled: {
type: Boolean,
default: () => false,
},
value: {
type: [String, Number, Boolean, Array, Symbol, Object] as PropType<FormValueType>,
required: false,
default: () => {
return undefined
},
},
})
const { item, value, disabled } = toRefs(props)
const typeMap = (type: FormItemTypeEnum | string): string => {
if (type == FormItemTypeEnum.RADIO) {
return 'a-radio-group'
}
if (type == FormItemTypeEnum.CHECKBOX) {
return 'a-checkbox-group'
}
return 'a-' + type
}
const properties = computed(() => {
const p = item.value.properties
if (item.value.type === FormItemTypeEnum.TRANSFER && !item.value.properties.render) {
p.render = (item) => item.title
}
if (item.value.propertiesTransfer) {
return item.value.propertiesTransfer(XEUtils.clone(p, true))
}
return p
})
const bindPropName = (type: string): string => {
if (type == 'switch') {
return 'checked'
}
if (type == 'transfer') {
return 'target-keys'
}
return 'value'
}
const emit = defineEmits<{
'update:value': [value: FormValueType]
}>()
// 双向绑定
const bindValue = computed({
get: function (): FormValueType {
return value?.value ? value.value : undefined
},
set: function (value: FormValueType) {
emit('update:value', value)
},
})
</script>
<style lang="css" scoped>
.ant-input-number,
.ant-input-number-affix-wrapper,
.ant-input-number-group-wrapper,
.ant-picker {
width: 100% !important;
}
</style>

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