This commit is contained in:
eibons
2025-08-15 21:37:29 +08:00
parent 2d7f77a984
commit ce999372ae
183 changed files with 21567 additions and 5 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
.idea
assets

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<module version="4">
<component name="AdditionalModuleElements">
<content url="file://$MODULE_DIR$" dumb="true">
<sourceFolder url="file://$MODULE_DIR$/target/generated-sources/annotations" isTestSource="false" />
</content>
</component>
</module>

View File

@@ -5,8 +5,6 @@ import cn.hutool.core.util.ObjectUtil;
import cn.hutool.json.JSONUtil;
import com.cool.core.util.ConvertUtil;
import jakarta.annotation.PostConstruct;
import java.time.Duration;
import java.util.Arrays;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.cache.CacheType;
@@ -17,6 +15,9 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.stereotype.Component;
import java.time.Duration;
import java.util.Arrays;
/**
* 缓存工具类
*/
@@ -174,7 +175,7 @@ public class CoolCache {
cache.put(key, value);
} else if (type.equalsIgnoreCase(CacheType.REDIS.name())) {
redisCache.put(cacheName, key.getBytes(), ObjectUtil.serialize(value),
java.time.Duration.ofSeconds(ttl));
Duration.ofSeconds(ttl));
}
}
}

View File

@@ -0,0 +1,7 @@
package com.cool.core.plugin.event;
public enum PluginActionEnum {
INSTALL,
UNINSTALL,
UPDATE,
}

View File

@@ -1,8 +1,8 @@
✨🌈✨[cool-admin-java-plus](https://gitee.com/hlc4417/cool-admin-java-plus)✨🌈✨
✨🌈✨===================================================================================✨🌈✨
______ ___ ___ _____ _ ______ ____ ____ _____ ____ _____
.' ___ | .' `. .' `.|_ _| V8.x / \ |_ _ `.|_ \ / _||_ _||_ \|_ _|
/ .' \_|/ .-. \/ .-. \ | | ______ / _ \ | | `. \ | \/ | | | | \ | |
| | | | | || | | | | | _|______|/ ___ \ | | | | | |\ /| | | | | |\ \| |
\ `.___.'\\ `-' /\ `-' /_| |__/ | _/ / \ \_ _| |_.' /_| |_\/_| |_ _| |_ _| |_\ |_
`.____ .' `.___.' `.___.'|________| |____| |____||______.'|_____||_____||_____||_____|\____|
:: https://java.cool-admin.com ::
✨🌈✨===================================================================================✨🌈✨

23
cool-admin-vue/packages/crud/.gitignore vendored Normal file
View File

@@ -0,0 +1,23 @@
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@@ -0,0 +1,9 @@
{
"tabWidth": 4,
"useTabs": true,
"semi": true,
"jsxBracketSameLine": true,
"singleQuote": false,
"printWidth": 100,
"trailingComma": "none"
}

View File

@@ -0,0 +1,33 @@
# 介绍
**cool-admin for vue**是基于[Vue.js](https://v3.cn.vuejs.org)开发。
[cool-admin 官方文档](https://cool-js.com/)
尝试 `cool-admin` 最简单的方法就是查看文档及运行示例。
<img src='https://vue.cool-admin.com/show/admin.png' />
[Ai极速编码 🔥 在线体验](https://show.cool-admin.com/helper/ai-code)
<img src='https://vue.cool-admin.com/show/code.png' />
## 代码仓库
**cool-admin for vue** 是开源免费的,遵循[MIT](https://baike.baidu.com/item/MIT/10772952)开源协议,意味着您无需支付任何费用,也无需授权,即可将它应用到您的产品中。
开源免费,并不意味着您可以将 cool-admin 应用到非法的领域,比如涉及赌博,暴力等方面。如因此产生纠纷等法律问题,`cool-admin`不承担任何责任。
[https://github.com/cool-team-official/cool-admin-vue](https://github.com/cool-team-official/cool-admin-vue)
```shell
git clone https://github.com/cool-team-official/cool-admin-vue.git
```
## 技术选型
- [Vue.js](https://v3.cn.vuejs.org),基础框架;
- [VueRouter](https://router.vuejs.org)Vue.js 官方路由;
- [Pinia](https://pinia.vuejs.org),轻量级状态管理库;
- [ElementPlus](https://element-plus.gitee.io/zh-CN),桌面端组件库;
- [Vite](https://vitejs.cn),构建工具;

811
cool-admin-vue/packages/crud/index.d.ts vendored Normal file
View File

@@ -0,0 +1,811 @@
// vue
declare namespace Vue {
interface Ref<T = any> {
value: T;
}
type Emit = (name: any, ...args: any[]) => void;
}
// element-plus
declare namespace ElementPlus {
type Size = "large" | "default" | "small";
type Align = "left" | "center" | "right";
interface FormProps {
inline?: boolean;
labelPosition?: "left" | "right" | "top";
labelWidth?: string | number;
labelSuffix?: string;
hideRequiredAsterisk?: boolean;
showMessage?: boolean;
inlineMessage?: boolean;
statusIcon?: boolean;
validateOnRuleChange?: boolean;
size?: Size;
disabled?: boolean;
[key: string]: any;
}
}
// mitt
declare interface Mitt {
on(name: string, callback: (data: any) => void): void;
off(name: string, callback: (data: any) => void): void;
emit(name: string, data?: any): void;
}
// emitter
declare interface EmitterItem {
name: string;
callback(data: any, events: { refresh(params: any): void; crudList: ClCrud.Ref[] }): void;
}
declare interface Emitter {
list: EmitterItem[];
init(events: any): void;
on(name: string, callback: (data: any) => void): void;
emit(name: string, data?: any): void;
}
// 方法
declare type fn = () => void;
// 对象
declare type obj = {
[key: string]: any;
};
// 全部可选
declare type DeepPartial<T> = T extends Function
? T
: T extends object
? { [P in keyof T]?: DeepPartial<T[P]> }
: T;
// 合并
declare type Merge<A, B> = Omit<A, keyof B> & B;
// 移除 [key]
declare type RemoveIndex<T> = {
[P in keyof T as string extends P ? never : number extends P ? never : P]: T[P];
};
// 任用列表
declare type List<T> = Array<DeepPartial<T> | (() => DeepPartial<T>)>;
// 获取keys
declare type PropKey<T> = keyof RemoveIndex<T> | (string & {});
// 任意字符串
declare type AnyString = string & {};
// 类型或者 Ref 泛型
declare type RefData<T = any> = T | Vue.Ref<T>;
// browser
declare type Browser = {
screen: string;
isMini: boolean;
};
// 字典选项
declare type DictOptions = {
label?: string;
value?: any;
color?: string;
type?: string;
[key: string]: any;
}[];
// render
declare namespace Render {
type OpButton =
| `slot-${string}`
| {
label: string;
type?: string;
hidden?: boolean;
onClick(options: { scope: obj }): void;
[key: string]: any;
};
interface Props {
onChange?(value: any): void;
[key: string]: any;
}
interface Component {
name?: string;
options?: RefData<DictOptions>;
props?: RefData<Props>;
style?: obj;
slots?: {
[key: string]: (data?: any) => any;
};
vm?: any;
[key: string]: any;
}
}
// crud
declare namespace ClCrud {
declare interface Field {
comment: string;
source: string;
propertyName: string;
type: string;
dict: string | string[];
nullable: boolean;
}
interface Label {
op: string;
add: string;
delete: string;
multiDelete: string;
update: string;
refresh: string;
info: string;
search: string;
reset: string;
clear: string;
save: string;
close: string;
confirm: string;
advSearch: string;
searchKey: string;
placeholder: string;
placeholderSelect: string;
tips: string;
saveSuccess: string;
deleteSuccess: string;
deleteConfirm: string;
empty: string;
desc: string;
asc: string;
select: string;
deselect: string;
seeMore: string;
hideContent: string;
nonEmpty: string;
collapse: string;
expand: string;
[key: string]: string;
}
interface Dict {
primaryId: string;
api: {
list: string;
add: string;
update: string;
delete: string;
info: string;
page: string;
};
pagination: {
page: string;
size: string;
};
search: {
keyWord: string;
query: string;
};
sort: {
order: string;
prop: string;
};
label: Label;
}
interface Permission {
page?: boolean;
list?: boolean;
add?: boolean;
delete?: boolean;
update?: boolean;
info?: boolean;
[key: string]: any;
}
interface Params {
page: {
page?: number;
size?: number;
[key: string]: any;
};
list: obj;
add: obj;
delete: {
ids?: any[];
[key: string]: any;
};
update: {
id?: any;
[key: string]: any;
};
info: {
id?: any;
[key: string]: any;
};
}
interface Response {
page: {
list: any[];
pagination: {
total: number;
page: number;
size: number;
[key: string]: any;
};
[key: string]: any;
};
list: any[];
add: any;
update: any;
info: any;
delete: any;
}
interface Service {
api: {
page(params?: Params["page"]): Promise<Response["page"]>;
list(params?: Params["list"]): Promise<Response["list"]>;
add(params?: Params["add"]): Promise<Response["add"]>;
update(params?: Params["update"]): Promise<Response["update"]>;
info(params?: Params["info"]): Promise<Response["info"]>;
delete(params?: Params["delete"]): Promise<Response["delete"]>;
permission: Permission;
search: {
fieldEq: Field[];
fieldLike: Field[];
keyWordLikeFields: Field[];
};
[key: string]: any;
};
}
interface Config {
name: string;
service: Service["api"];
permission: Permission;
dict: Dict;
onRefresh(
params: obj,
event: {
done: fn;
next: Service["api"]["page"];
render: (data: any | any[], pagination?: Response["page"]["pagination"]) => void;
}
): void;
onDelete(
selection: obj[],
event: {
next: Service["api"]["delete"];
}
): void;
}
interface Ref {
"cl-table": ClTable.Ref;
"cl-upsert": ClUpsert.Ref;
id: number;
mitt: Mitt;
name: string;
routePath: string;
permission: Permission;
dict: Dict;
service: Service["api"];
loading: boolean;
params: obj;
selection: obj[];
set(key: "dict" | "style" | "service" | "permission", value: any): void;
done(): void;
getParams(): obj;
setParams(data: obj): void;
getPermission(key?: string): boolean;
rowInfo(data: obj): void;
rowAdd(): void;
rowEdit(data: obj): void;
rowAppend(data?: obj): void;
rowClose(): void;
rowDelete(...selection: obj[]): void;
proxy(name: string, data?: any[]): any;
paramsReplace(params: obj): obj;
refresh: Service["api"]["page"];
[key: string]: any;
}
interface Options extends DeepPartial<Config> {
service?: any;
}
}
declare namespace ClTable {
type OpButton = Array<"info" | "edit" | "delete" | AnyString | Render.OpButton>;
type ColumnType = "index" | "selection" | "expand" | "op" | AnyString;
interface Column<T = any> {
type: ColumnType;
hidden: RefData<boolean>;
component: Render.Component;
search: {
isInput: boolean;
value: any;
icon: () => any;
refreshOnChange: boolean;
component: Render.Component;
};
dict: RefData<DictOptions>;
dictFormatter: (values: DictOptions) => string;
dictColor: boolean;
dictSeparator: string;
dictAllLevels: boolean;
buttons: OpButton | ((options: { scope: T }) => OpButton);
align: ElementPlus.Align;
label: any;
renderLabel: (options: { column: any; $index: number }) => any;
className: string;
prop: PropKey<T>;
orderNum: number;
width: RefData<number | string>;
minWidth: RefData<number | string>;
renderHeader: (options: { column: any; $index: number }) => any;
sortable: boolean | "desc" | "descending" | "ascending" | "asc" | "custom";
sortMethod: fn;
sortBy: string | ((row: T, index: number) => any) | any[];
resizable: boolean;
columnKey: string;
headerAlign: ElementPlus.Align;
showOverflowTooltip: boolean;
fixed: boolean | string;
render: (row: T, column: any, value: any, index: number) => any;
formatter: (row: T, column: any, value: any, index: number) => any;
selectable: (row: T, index: number) => boolean;
reserveSelection: boolean;
filterMethod: fn;
filteredValue: unknown[];
filters: unknown[];
filterPlacement: string;
filterMultiple: boolean;
index: ((index: number) => number) | number;
sortOrders: unknown[];
children: Column<T>[];
[key: string]: any;
}
type ContextMenu = Array<
| ClContextMenu.Item
| ((row: obj, column: obj, event: PointerEvent) => ClContextMenu.Item)
| "refresh"
| "check"
| "update"
| "edit"
| "delete"
| "info"
| "order-desc"
| "order-asc"
>;
type Plugin = (options: { exposed: Ref }) => void;
interface Config<T = any> {
columns: Column<T>[];
autoHeight: boolean;
height: any;
contextMenu: ContextMenu;
defaultSort: {
prop: string;
order: "descending" | "ascending";
};
sortRefresh: boolean;
emptyText: string;
rowKey: string;
on?: {
[key: string]: (...args: any[]) => void;
};
props?: {
[key: string]: any;
};
plugins?: Plugin[];
onRowContextmenu?(row: T, column: any, event: any): void;
}
interface Ref<T = any> {
Table: any;
config: obj;
selection: T[];
data: T[];
columns: Column<T>[];
reBuild(cb?: fn): void;
calcMaxHeight(): void;
setData(data: T[]): void;
setColumns(columns: Column[]): void;
showColumn(props: PropKey<T> | PropKey<T>[], status?: boolean): void;
hideColumn(props: PropKey<T> | PropKey<T>[]): void;
changeSort(prop: PropKey<T>, order: string): void;
clearSelection(): void;
getSelectionRows(): any[];
toggleRowSelection(row: T, selected?: boolean): void;
toggleAllSelection(): void;
toggleRowExpansion(row: T, expanded?: boolean): void;
setCurrentRow(row: T): void;
clearSort(): void;
clearFilter(columnKeys: PropKey<T>[]): void;
doLayout(): void;
sort(prop: PropKey<T>, order: string): void;
scrollTo(position: { top?: number; left?: number }): void;
setScrollTop(top: number): void;
setScrollLeft(left: number): void;
updateKeyChildren(key: string, children: any[]): void;
}
interface Options<T = any> extends DeepPartial<Config<T>> {
columns?: List<ClTable.Column<T>>;
}
}
declare namespace ClFormTabs {
type labels = {
label: string;
value: string;
name?: string;
icon?: any;
lazy?: boolean;
[key: string]: any;
}[];
}
declare namespace ClForm {
type CloseAction = "close" | "save" | AnyString;
interface Rule {
type?:
| "string"
| "number"
| "boolean"
| "method"
| "regexp"
| "integer"
| "float"
| "array"
| "object"
| "enum"
| "date"
| "url"
| "hex"
| "email"
| "any";
required?: boolean;
message?: string;
min?: number;
max?: number;
trigger?: any;
validator?(rule: any, value: any, callback: (error?: Error) => void): void;
[key: string]: any;
}
interface Hook {
Fn: (value: any, options: { form: obj; prop: string; method: "submit" | "bind" }) => any;
Key:
| "number"
| "string"
| "split"
| "join"
| "boolean"
| "booleanNumber"
| "datetimeRange"
| "splitJoin"
| "json"
| "empty"
| AnyString;
Pipe: Hook["Key"] | Hook["Fn"];
Event: {
bind?: Hook["Pipe"] | Hook["Pipe"][];
submit?: Hook["Pipe"] | Hook["Pipe"][];
reset?: (prop: string) => string[];
};
}
interface Item<T = any> {
type?: "tabs";
prop?: PropKey<T>;
props?: {
labels?: ClFormTabs.labels;
justify?: "left" | "center" | "right";
color?: string;
mergeProp?: boolean;
labelWidth?: string;
error?: string;
showMessage?: boolean;
inlineMessage?: boolean;
size?: "medium" | "default" | "small";
[key: string]: any;
};
span?: number;
col?: {
span: number;
offset: number;
push: number;
pull: number;
xs: any;
sm: any;
md: any;
lg: any;
xl: any;
tag: string;
};
group?: string;
collapse?: boolean;
value?: any;
label?: string;
renderLabel?: any;
flex?: boolean;
hook?: Hook["Event"] | Hook["Key"];
hidden?: boolean | ((options: { scope: obj }) => boolean);
prepend?: Render.Component;
component?: Render.Component;
append?: Render.Component;
rules?: Rule | Rule[];
required?: boolean;
children?: Item[];
[key: string]: any;
}
interface Config<T = any> {
title?: any;
height?: any;
width?: any;
props: ElementPlus.FormProps;
items: Item[];
form: obj;
isReset?: boolean;
on?: {
open?(data: T): void;
close?(action: CloseAction, done: fn): void;
submit?(data: T, event: { close: fn; done: fn }): void;
change?(data: T, prop: string): void;
};
op: {
hidden?: boolean;
saveButtonText?: string;
closeButtonText?: string;
justify?: "flex-start" | "center" | "flex-end";
buttons?: Array<CloseAction | Render.OpButton>;
};
dialog: {
title?: any;
height?: string;
width?: string;
hideHeader?: boolean;
controls?: Array<"fullscreen" | "close" | AnyString>;
[key: string]: any;
};
[key: string]: any;
}
type Plugin = (options: {
exposed: Ref;
onOpen(cb: () => void): void;
onClose(cb: () => void): void;
onSubmit(cb: (data: obj) => obj): void;
}) => void;
type Items<T = any> = List<Item<T>>;
interface Ref<T = any> {
Form: any;
form: T;
config: {
items: Item[];
[key: string]: any;
};
open(options: Options<T>, plugins?: Plugin[]): void;
close(action?: CloseAction): void;
done(): void;
clear(): void;
reset(): void;
showLoading(): void;
hideLoading(): void;
setDisabled(flag?: boolean): void;
invokeData(data: any): void;
setData(prop: string, value: any): void;
bindForm(data: obj): void;
getForm(prop?: string): any;
setForm(prop: string, value: any): void;
setOptions(prop: string, list: DictOptions): void;
setProps(prop: string, value: any): void;
setConfig(path: string, value: any): void;
showItem(props: string[] | string): void;
hideItem(props: string[] | string): void;
toggleItem(prop: string, flag?: boolean): void;
resetFields(): void;
clearValidate(props?: string[] | string): void;
validateField(
props?: string[] | string,
callback?: (isValid: boolean, invalidFields: any[]) => void
): Promise<void>;
validate(callback: (isValid: boolean, invalidFields: any[]) => void): Promise<void>;
changeTab(value: any, valid?: boolean): Promise<any>;
setTitle(value: string): void;
submit(cb?: (data: obj) => void): void;
Tabs: {
active: RefData<string>;
list: ClFormTabs.labels;
change(value: any, valid?: boolean): Promise<any>;
[key: string]: any;
};
[key: string]: any;
}
interface Options<T = any> extends DeepPartial<Config> {
items?: Items<T>;
}
}
declare namespace ClUpsert {
interface Config<T = any> {
sync: boolean;
items: ClForm.Item[];
props: ClForm.Config["props"];
op: ClForm.Config["op"];
dialog: ClForm.Config["dialog"];
onOpen?(): void;
onOpened?(data: T): void;
onClose?(action: ClForm.CloseAction, done: fn): void;
onClosed?(): void;
onInfo?(
data: T,
event: { close: fn; done(data: T): void; next: ClCrud.Service["api"]["info"] }
): void;
onSubmit?(
data: T,
event: { close: fn; done: fn; next: ClCrud.Service["api"]["update"] }
): void;
plugins?: ClForm.Plugin[];
}
interface Ref<T = any> extends ClForm.Ref<T> {
mode: "add" | "update" | "info" | AnyString;
}
interface Options<T = any> extends DeepPartial<Config<T>> {
items?: ClForm.Items<T>;
}
}
declare namespace ClAdvSearch {
interface Config<T = any> {
items?: ClForm.Item[];
title?: string;
size?: string | number;
op?: ("clear" | "reset" | "close" | "search" | `slot-${string}`)[];
onSearch?(data: T, options: { next: ClCrud.Service["api"]["page"]; close(): void }): void;
}
interface Ref<T = any> extends ClForm.Ref<T> {}
interface Options<T = any> extends DeepPartial<Config<T>> {
items?: ClForm.Items<T>;
}
}
declare namespace ClSearch {
type Plugin = (options: { exposed: Ref }) => void;
interface Config<T = any> {
inline?: boolean;
items?: ClForm.Item[];
data?: T;
props?: ElementPlus.FormProps;
resetBtn?: boolean;
collapse?: boolean;
Form?: ClForm.Ref;
onChange?(data: T, prop: string): void;
onLoad?(data: T): void;
onSearch?(data: T, options: { next: ClCrud.Service["api"]["page"] }): void;
plugins?: Plugin[];
}
interface Ref<T = any> extends ClForm.Ref<T> {
search(params?: obj): void;
reset(): void;
}
interface Options<T = any> extends DeepPartial<Config<T>> {
items?: ClForm.Items<T>;
}
}
declare namespace ClContextMenu {
interface Item {
label: string;
prefixIcon?: any;
suffixIcon?: any;
ellipsis?: boolean;
disabled?: boolean;
hidden?: boolean;
children?: Item[];
showChildren?: boolean;
callback?(done: fn): void;
[key: string]: any;
}
interface Event {
pageX: number;
pageY: number;
[key: string]: any;
}
interface Options {
class?: string;
hover?:
| boolean
| {
target?: string;
className?: string;
};
list?: Item[];
}
interface Ref {
open(event: Event, options: Options): Exposed;
close(): void;
}
interface Exposed {
close(): void;
}
}
declare namespace ClDialog {
interface Provide {
visible: Vue.Ref<boolean>;
fullscreen: Vue.Ref<boolean>;
}
}
declare interface Config {
dict: ClCrud.Dict;
permission: ClCrud.Permission;
events: {
[key: string]: (...args: any[]) => any;
};
style: {
size: ElementPlus.Size;
colors: string[];
form: {
labelPosition: ElementPlus.FormProps["labelPosition"];
labelWidth: ElementPlus.FormProps["labelWidth"];
span: number;
plugins: ClForm.Plugin[];
};
table: {
stripe: boolean;
border: boolean;
highlightCurrentRow: boolean;
resizable: boolean;
autoHeight: boolean;
contextMenu: ClTable.ContextMenu;
column: {
minWidth: number | string;
align: ElementPlus.Align;
headerAlign: ElementPlus.Align;
opWidth: number | string;
};
plugins: ClTable.Plugin[];
};
search: {
plugins: ClSearch.Plugin[];
};
};
}
declare type Options = DeepPartial<Config>;
declare interface CrudOptions {
options: Options;
}

View File

@@ -0,0 +1,29 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="referer" content="never" />
<meta name="renderer" content="webkit" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=0"
/>
<title></title>
<link rel="icon" href="./favicon.ico" />
</head>
<body>
<div class="preload__wrap" id="Loading">
<div class="preload__container">
<p class="preload__name"></p>
<div class="preload__loading"></div>
<p class="preload__title"></p>
<p class="preload__sub-title"></p>
</div>
</div>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View File

@@ -0,0 +1,38 @@
{
"name": "@cool-vue/crud",
"version": "8.0.6",
"private": false,
"main": "./dist/index.umd.js",
"module": "./dist/index.es.js",
"types": "types/entry.d.ts",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vue-tsc && vite build",
"preview": "vite preview"
},
"dependencies": {
"@vue/runtime-core": "^3.5.13",
"element-plus": "^2.10.4",
"lodash-es": "^4.17.21",
"vue": "^3.5.13"
},
"devDependencies": {
"@types/node": "^20.11.16",
"@vitejs/plugin-vue": "^5.2.1",
"@vitejs/plugin-vue-jsx": "^4.1.1",
"prettier": "^3.5.1",
"sass": "^1.85.0",
"sass-loader": "^16.0.5",
"typescript": "^5.3.3",
"vite": "^6.1.0",
"vite-plugin-dts": "^4.5.0",
"vue-tsc": "^2.2.2"
},
"files": [
"types",
"dist",
"index.d.ts",
"index.ts"
]
}

2350
cool-admin-vue/packages/crud/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,208 @@
<template>
<div>
<div class="title">CRUD DEMO v8</div>
<cl-crud ref="Crud">
<div class="search">
<cl-search ref="Search" />
</div>
<cl-row>
<cl-add-btn />
<cl-flex1 />
<cl-search-key
field="name"
:field-list="[
{
label: '昵称',
value: 'name'
},
{
label: '手机号',
value: 'phone'
}
]"
refreshOnInput
/>
</cl-row>
<cl-row>
<cl-table ref="Table" :auto-height="false"></cl-table>
</cl-row>
<cl-row>
<cl-flex1 />
<cl-pagination />
</cl-row>
<cl-upsert ref="Upsert"></cl-upsert>
<cl-form ref="Form"></cl-form>
</cl-crud>
</div>
</template>
<script setup lang="tsx">
import { useCrud, useForm, useSearch, useTable, useUpsert } from "./hooks";
import { EditPen } from "@element-plus/icons-vue";
interface Data {
name?: string;
age?: number;
[key: string]: any;
}
const Upsert = useUpsert<Data>({
items: [
{
type: "tabs",
props: {
labels: [
{
label: "基础",
value: "A",
icon: EditPen
},
{
label: "高级",
value: "B"
}
]
}
},
{
group: "A",
prop: "age",
label: "年龄",
component: {
name: "el-input"
}
},
{
group: "A",
prop: "name",
label: "昵称",
component: {
name: "el-input"
},
hidden({ scope }) {
return scope.age < 18;
}
},
{
group: "B",
prop: "phone",
label: "手机",
component: {
name: "el-input"
},
hidden({ scope }) {
return scope.age < 18;
}
},
() => {
return {
group: "A",
hidden: Upsert.value?.mode == "add"
};
}
],
onOpened(data) {
console.log(data);
Upsert.value?.setForm("age", "18");
}
});
const Table = useTable<Data>(
{
contextMenu: [
{
label: "带图标",
prefixIcon: EditPen
},
{
label: "多层级",
children: [
{
label: "A",
children: [
{
label: "A-1"
}
]
},
{
label: "B"
}
]
}
],
columns: [
{
label: "姓名",
prop: "name",
search: {
component: {
name: "el-date-picker"
}
}
},
{
label: "手机号",
prop: "phone",
search: {
component: {
name: "el-date-picker"
}
}
},
{
type: "op"
}
]
},
(table) => {
console.log(table);
}
);
const Crud = useCrud(
{
service: "test"
},
(app) => {
app.refresh();
}
);
const Form = useForm<Data>();
const Search = useSearch({
collapse: true,
resetBtn: true,
items: [
{
label: "姓名",
prop: "name",
component: {
name: "el-input"
},
hook: {
reset() {
return [];
}
}
}
]
});
</script>
<style scoped>
.title {
text-align: center;
font-size: 14px;
font-weight: bold;
}
</style>

View File

@@ -0,0 +1,21 @@
import { defineComponent } from "vue";
import { useConfig, useCore } from "../../hooks";
export default defineComponent({
name: "cl-add-btn",
setup(_, { slots }) {
const { crud } = useCore();
const { style } = useConfig();
return () => {
return (
crud.getPermission("add") && (
<el-button type="primary" size={style.size} onClick={crud.rowAdd}>
{slots.default?.() || crud.dict.label.add}
</el-button>
)
);
};
}
});

View File

@@ -0,0 +1,31 @@
import { useConfig, useCore } from "../../hooks";
import { defineComponent } from "vue";
import { Search } from "@element-plus/icons-vue";
export default defineComponent({
name: "cl-adv-btn",
components: {
Search
},
setup(_, { slots }) {
const { crud, mitt } = useCore();
const { style } = useConfig();
function open() {
mitt.emit("crud.openAdvSearch");
}
return () => {
return (
<el-button size={style.size} onClick={open} class="cl-adv-btn">
<el-icon>
<Search />
</el-icon>
{slots.default?.() || crud.dict.label.advSearch}
</el-button>
);
};
}
});

View File

@@ -0,0 +1,212 @@
import {
defineComponent,
h,
inject,
mergeProps,
nextTick,
type PropType,
reactive,
ref
} from "vue";
import { Close } from "@element-plus/icons-vue";
import { useBrowser, useConfig, useCore } from "../../hooks";
import { renderNode } from "../../utils/vnode";
import { useApi } from "../form/helper";
import { isArray } from "lodash-es";
export default defineComponent({
name: "cl-adv-search",
components: {
Close
},
props: {
// 表单项
items: {
type: Array as PropType<ClForm.Item[]>,
default: () => []
},
// 标题
title: String,
// 窗体大小
size: {
type: [Number, String],
default: "30%"
},
// 操作按钮
op: {
type: Array,
default: () => ["clear", "reset", "close", "search"]
},
// 搜索钩子
onSearch: Function
},
emits: ["reset", "clear"],
setup(props, { emit, slots, expose }) {
const { crud, mitt } = useCore();
const { style } = useConfig();
const browser = useBrowser();
// 配置
const config = reactive<ClAdvSearch.Config>(
mergeProps(props, inject("useAdvSearch__options") || {})
);
// cl-form
const Form = ref<ClForm.Ref>();
// el-drawer
const Drawer = ref();
// 是否可见
const visible = ref(false);
// 打开
function open() {
visible.value = true;
nextTick(() => {
Form.value?.open({
items: config.items || [],
op: {
hidden: true
},
isReset: false
});
});
}
// 关闭
function close() {
Drawer.value.handleClose();
}
// 重置数据
function reset() {
const d: any = {};
config.items?.map((e) => {
if (typeof e.hook != "string" && e.hook?.reset) {
const props = e.hook.reset(e.prop!);
if (isArray(props)) {
props.forEach((prop) => {
d[prop] = undefined;
});
}
}
d[e.prop!] = undefined;
});
// 重置表单
Form.value?.reset();
// 列表刷新
search();
// 重置事件
emit("reset", d);
}
// 清空数据
function clear() {
Form.value?.clear();
emit("clear");
}
// 搜素请求
function search(params?: any) {
const form = Form.value?.getForm();
function next(data: any) {
Form.value?.done();
close();
return crud.refresh({
...data,
...params,
page: 1
});
}
if (config.onSearch) {
config.onSearch(form, { next, close });
} else {
next(form);
}
}
// 消息事件
mitt.on("crud.openAdvSearch", open);
// 渲染表单
function renderForm() {
return h(<cl-form ref={Form} inner enable-plugin={false} />, {}, slots);
}
// 渲染底部
function renderFooter() {
const fns = { search, reset, clear, close };
return config.op?.map((e: string) => {
switch (e) {
case "search":
case "reset":
case "clear":
case "close":
return h(
<el-button />,
{
type: e == "search" ? "primary" : null,
size: style.size,
onClick: () => {
fns[e]();
}
},
{ default: () => crud.dict.label[e] }
);
default:
return renderNode(e, {
scope: Form.value?.getForm(),
slots
});
}
});
}
expose({
open,
close,
clear,
...useApi({ Form }),
reset,
Form
});
return () => {
return (
<el-drawer
ref={Drawer}
modal-class="cl-adv-search"
v-model={visible.value}
direction="rtl"
with-header={false}
size={browser.isMini ? "100%" : config.size}>
<div class="cl-adv-search__header">
<span class="text">{config.title || crud.dict.label.advSearch}</span>
<el-icon size={20} onClick={close}>
<Close />
</el-icon>
</div>
<div class="cl-adv-search__container">{renderForm()}</div>
<div class="cl-adv-search__footer">{renderFooter()}</div>
</el-drawer>
);
};
}
});

View File

@@ -0,0 +1,279 @@
import {
defineComponent,
h,
nextTick,
onMounted,
type PropType,
reactive,
ref,
render,
toRaw
} from "vue";
import { isString } from "lodash-es";
import { addClass, contains, removeClass } from "../../utils";
import { useRefs } from "../../hooks";
import { ElIcon } from "element-plus";
import { ArrowRight } from "@element-plus/icons-vue";
const ClContextMenu = defineComponent({
name: "cl-context-menu",
props: {
show: Boolean,
options: {
type: Object as PropType<ClContextMenu.Options>,
default: () => ({})
},
event: {
type: Object,
default: () => ({})
}
},
setup(props, { expose, slots }) {
const { refs, setRefs } = useRefs();
// 是否可见
const visible = ref(props.show || false);
// 按钮列表
const list = ref<ClContextMenu.Item[]>([]);
// 样式
const style = reactive({
left: "0px",
top: "0px"
});
// 选中值
const ids = ref("");
// 阻止默认事件
function stopDefault(e: any) {
if (e.preventDefault) {
e.preventDefault();
}
if (e.stopPropagation) {
e.stopPropagation();
}
}
// 解析列表
function parseList(list: ClContextMenu.Item[]) {
function deep(list: ClContextMenu.Item[]) {
list.forEach((e) => {
e.showChildren = false;
if (e.children) {
deep(e.children);
}
});
}
deep(list);
return list;
}
// 目标元素
let targetEl: any;
// 关闭
function close() {
visible.value = false;
ids.value = "";
if (targetEl) {
removeClass(targetEl, "cl-context-menu__target");
}
}
// 打开
function open(event: any, options: ClContextMenu.Options = {}) {
// 阻止默认事件
stopDefault(event);
// 显示
visible.value = true;
// 元素
const el = refs["context-menu"].querySelector(".cl-context-menu__box") as HTMLElement;
// 点击样式
if (options?.hover) {
const d = options.hover === true ? {} : options.hover;
targetEl = event.target;
if (targetEl && isString(targetEl.className)) {
if (d.target) {
while (!targetEl.className.includes(d.target)) {
targetEl = targetEl.parentNode;
}
}
addClass(targetEl, d.className || "cl-context-menu__target");
}
}
// 自定义样式
if (options?.class) {
addClass(el, options.class);
}
// 菜单列表
if (options?.list) {
list.value = parseList(options.list);
}
nextTick(() => {
// 计算位置
let left = event.pageX;
let top = event.pageY;
// 组件方式用 offset 计算
if (!props.show) {
left = event.offsetX;
top = event.offsetY;
}
const { clientHeight: h1, clientWidth: w1 } = event.target?.ownerDocument.body;
const { clientHeight: h2, clientWidth: w2 } = el;
if (top + h2 > h1) {
top = h1 - h2 - 5;
}
if (left + w2 > w1) {
left = w1 - w2 - 5;
}
style.left = left + "px";
style.top = top + "px";
});
return {
close
};
}
// 行点击
function rowClick(item: ClContextMenu.Item, id: string) {
ids.value = id;
if (item.disabled) {
return false;
}
if (item.callback) {
return item.callback(close);
}
if (item.children) {
item.showChildren = !item.showChildren;
} else {
close();
}
}
expose({
open,
close
});
onMounted(function () {
if (visible.value) {
const { body, documentElement } = props.event.target.ownerDocument;
// 添加到 body 下
body.appendChild(refs["context-menu"]);
// 关闭事件
(documentElement || body).addEventListener("mousedown", (e: any) => {
const el = refs["context-menu"];
if (!contains(el, e.target) && el != e.target) {
close();
}
});
// 默认打开
open(props.event, props?.options);
}
});
return () => {
function deep(list: ClContextMenu.Item[], pId: string, level: number) {
return (
<div class={["cl-context-menu__box", level > 1 && "is-append"]}>
{list
.filter((e) => !e.hidden)
.map((e, i) => {
const id = `${pId}-${i}`;
if (!e.suffixIcon) {
// 默认图标
if (e.children) {
e.suffixIcon = ArrowRight;
}
}
return (
<div
class={{
"is-active": ids.value.includes(id),
"is-ellipsis": e.ellipsis ?? true,
"is-disabled": e.disabled
}}
onClick={(ev: MouseEvent) => {
rowClick(e, id);
ev.stopPropagation();
}}>
{/* 前缀图标 */}
{e.prefixIcon && <ElIcon>{h(toRaw(e.prefixIcon))}</ElIcon>}
{/* 标题 */}
<span>{e.label}</span>
{/* 后缀图标 */}
{e.suffixIcon && <ElIcon>{h(toRaw(e.suffixIcon))}</ElIcon>}
{/* 子集 */}
{e.children &&
e.showChildren &&
deep(e.children, id, level + 1)}
</div>
);
})}
</div>
);
}
return (
visible.value && (
<div
class="cl-context-menu"
ref={setRefs("context-menu")}
style={style}
onContextmenu={stopDefault}>
{slots.default ? slots.default() : deep(list.value, "0", 1)}
</div>
)
);
};
}
});
export const ContextMenu = {
open(event: any, options: ClContextMenu.Options) {
const vm = h(ClContextMenu, {
show: true,
event,
options
});
render(vm, event.target.ownerDocument.createElement("div"));
return vm.component?.exposed as ClContextMenu.Exposed;
}
};
export default ClContextMenu;

View File

@@ -0,0 +1,287 @@
import { ElMessage, ElMessageBox } from "element-plus";
import { Mitt } from "../../utils/mitt";
import { ref } from "vue";
import { assign, isArray, isFunction } from "lodash-es";
import { merge } from "../../utils";
interface Options {
mitt: Mitt;
config: ClCrud.Config;
crud: ClCrud.Ref;
}
export function useHelper({ config, crud, mitt }: Options) {
// 刷新随机值,避免脏数据
const refreshRd = ref(0);
// 获取权限
function getPermission(key: "page" | "list" | "info" | "update" | "add" | "delete"): boolean {
return Boolean(crud.permission[key]);
}
// 根据字典替换请求参数
function paramsReplace(params: obj) {
const { pagination, search, sort } = crud.dict;
// 请求参数
const a: any = { ...params };
// 字典
const b: any = { ...pagination, ...search, ...sort };
for (const i in b) {
if (a[i]) {
if (i != b[i]) {
a[`_${b[i]}`] = a[i];
delete a[i];
}
}
}
for (const i in a) {
if (i[0] === "_") {
a[i.substr(1)] = a[i];
delete a[i];
}
}
return a;
}
// 刷新请求
function refresh(params?: obj) {
const { service, dict } = crud;
return new Promise((success, error) => {
// 合并请求参数
const reqParams = paramsReplace(assign(crud.params, params));
// Loading
crud.loading = true;
// 预防脏数据
const rd = (refreshRd.value = Math.random());
// 完成事件
function done() {
crud.loading = false;
}
// 渲染
function render(data: any | any[], pagination?: any) {
const res = isArray(data) ? { list: data, pagination } : data;
done();
success(res);
mitt.emit("crud.refresh", res);
}
// 下一步
function next(params: obj): Promise<any> {
return new Promise(async (resolve, reject) => {
await service[dict.api.page](params)
.then((res) => {
if (rd != refreshRd.value) {
return false;
}
if (isArray(res)) {
res = {
list: res,
pagination: {
total: res.length
}
};
}
render(res);
resolve(res);
})
.catch((err) => {
ElMessage.error(err.message);
error(err);
reject(err);
});
done();
});
}
// 刷新钩子
if (config.onRefresh) {
config.onRefresh(reqParams, { next, done, render });
} else {
next(reqParams);
}
});
}
// 打开详情
function rowInfo(data: any) {
mitt.emit("crud.proxy", {
name: "info",
data: [data]
});
}
// 打开新增
function rowAdd() {
mitt.emit("crud.proxy", {
name: "add"
});
}
// 打开编辑
function rowEdit(data: any) {
mitt.emit("crud.proxy", {
name: "edit",
data: [data]
});
}
// 打开追加
function rowAppend(data: any) {
mitt.emit("crud.proxy", {
name: "append",
data: [data]
});
}
// 关闭新增、编辑弹窗
function rowClose() {
mitt.emit("crud.proxy", {
name: "close"
});
}
// 删除请求
function rowDelete(...selection: any[]) {
const { service, dict } = crud;
// 参数
const params = {
ids: selection.map((e) => e[dict.primaryId])
};
// 下一步
async function next(data: obj) {
return new Promise((resolve, reject) => {
ElMessageBox({
type: "warning",
title: dict.label.tips,
message: dict.label.deleteConfirm,
confirmButtonText: dict.label.confirm,
cancelButtonText: dict.label.close,
showCancelButton: true,
async beforeClose(action, instance, done) {
if (action === "confirm") {
instance.confirmButtonLoading = true;
await service[dict.api.delete]({ ...params, ...data })
.then((res) => {
ElMessage.success(dict.label.deleteSuccess);
refresh();
resolve(res);
})
.catch((err) => {
ElMessage.error(err.message);
reject(err);
});
instance.confirmButtonLoading = false;
}
done();
}
}).catch(() => null);
});
}
// 删除钩子
if (config.onDelete) {
config.onDelete(selection, { next });
} else {
next(params);
}
}
// 代理
function proxy(name: string, data?: any[]) {
mitt.emit("crud.proxy", {
name,
data
});
}
// 获取请求参数
function getParams() {
return crud.params;
}
// 替换请求参数
function setParams(data: obj) {
merge(crud.params, data);
}
// 设置
function set(key: string, value: any) {
if (!value) {
return false;
}
switch (key) {
// 服务
case "service":
Object.assign(crud.service, value);
crud.service.__proto__ = value.__proto__;
if (value._permission) {
for (const i in value._permission) {
crud.permission[i] = value._permission[i];
}
}
break;
// 权限
case "permission":
if (isFunction(value)) {
merge(crud.permission, value(crud));
} else {
merge(crud.permission, value);
}
break;
default:
merge(crud[key], value);
break;
}
}
// 监听事件
function on(name: string, callback: fn) {
mitt.on(`${name}-${crud.id}`, callback);
}
// 默认值
set("dict", config.dict);
set("service", config.service);
set("permission", config.permission);
return {
proxy,
set,
on,
rowInfo,
rowAdd,
rowEdit,
rowAppend,
rowDelete,
rowClose,
refresh,
getPermission,
paramsReplace,
getParams,
setParams
};
}

View File

@@ -0,0 +1,91 @@
import { defineComponent, getCurrentInstance, inject, provide, reactive } from "vue";
import { cloneDeep } from "lodash-es";
import { useHelper } from "./helper";
import { Mitt } from "../../utils/mitt";
import { merge, mergeConfig } from "../../utils";
import { crudList } from "../../emitter";
import { useConfig } from "../../hooks";
export default defineComponent({
name: "cl-crud",
props: {
// 组件名
name: String,
// 是否有边框
border: Boolean,
// 内间距
padding: {
type: String,
default: "10px"
}
},
setup(props, { slots, expose }) {
// 当前实例
const inst = getCurrentInstance();
// 配置
const config = reactive<ClCrud.Config>(mergeConfig(inject("useCrud__options") || {}));
// 事件
const mitt = new Mitt(inst?.uid);
// 全局配置
const { dict, permission } = useConfig();
// 参数
const crud = reactive(
merge(
{
id: props.name || inst?.uid,
// 绑定的路由地址
routePath: location.pathname || "/",
// 表格加载状态
loading: false,
// 表格已选列
selection: [],
// 请求参数
params: {
page: 1,
size: 20
},
// 请求服务
service: {},
// 字典
dict: {},
// 权限
permission: {},
// 事件
mitt,
// 配置
config
},
cloneDeep({ dict, permission })
)
);
// 追加参数
merge(crud, useHelper({ config, crud, mitt }));
// 集合
crudList.push(crud);
// 值穿透
provide("crud", crud);
provide("mitt", mitt);
// 导出
expose(crud);
return () => {
return (
<div
class={["cl-crud", { "is-border": props.border }]}
style={{ padding: props.padding }}>
{slots.default?.()}
</div>
);
};
}
});

View File

@@ -0,0 +1,288 @@
import { computed, defineComponent, h, provide, ref, watch } from "vue";
import { Close, FullScreen, Minus } from "@element-plus/icons-vue";
import { renderNode } from "../../utils/vnode";
import { isArray, isBoolean } from "lodash-es";
import { useBrowser } from "../../hooks";
export default defineComponent({
name: "cl-dialog",
components: {
Close,
FullScreen,
Minus
},
props: {
// 是否可见
modelValue: {
type: Boolean,
default: false
},
// Extraneous non-props attributes
props: Object,
// 标题
title: {
type: String,
default: "-"
},
// 高度
height: String,
// 宽度
width: {
type: String,
default: "50%"
},
// 內间距
padding: {
type: String,
default: "20px"
},
// 是否缓存
keepAlive: Boolean,
// 是否全屏
fullscreen: Boolean,
// 控制按钮
controls: {
type: Array,
default: () => ["fullscreen", "close"]
},
// 隐藏头部元素
hideHeader: Boolean,
// 关闭前
beforeClose: Function,
// 是否需要滚动条
scrollbar: {
type: Boolean,
default: true
},
// 背景透明
transparent: Boolean
},
emits: ["update:modelValue", "fullscreen-change"],
setup(props, { emit, expose, slots }) {
const browser = useBrowser();
// el-dialog
const Dialog = ref();
// 是否全屏
const fullscreen = ref(false);
// 是否可见
const visible = ref(false);
// 缓存数
const cacheKey = ref(0);
// 是否全屏
const isFullscreen = computed(() => {
return browser && browser.isMini ? true : fullscreen.value;
});
// 监听绑定值
watch(
() => props.modelValue,
(val) => {
visible.value = val;
if (val && !props.keepAlive) {
cacheKey.value += 1;
}
},
{
immediate: true
}
);
// 监听 fullscreen 变化
watch(
() => props.fullscreen,
(val) => {
fullscreen.value = val;
},
{
immediate: true
}
);
// fullscreen-change 回调
watch(fullscreen, (val: boolean) => {
emit("fullscreen-change", val);
});
// 提供
provide("dialog", {
visible,
fullscreen: isFullscreen
});
// 打开
function open() {
fullscreen.value = true;
}
// 关闭
function close() {
function done() {
onClose();
}
if (props.beforeClose) {
props.beforeClose(done);
} else {
done();
}
}
// 关闭后
function onClose() {
emit("update:modelValue", false);
}
// 切换全屏
function changeFullscreen(val?: boolean) {
fullscreen.value = isBoolean(val) ? Boolean(val) : !fullscreen.value;
}
// 双击全屏
function dblClickFullscreen() {
if (isArray(props.controls) && props.controls.includes("fullscreen")) {
changeFullscreen();
}
}
// 渲染头部
function renderHeader() {
return (
props.hideHeader || (
<div class="cl-dialog__header" onDblclick={dblClickFullscreen}>
<span class="cl-dialog__title">{props.title}</span>
<div class="cl-dialog__controls">
{props.controls.map((e: any) => {
switch (e) {
//全屏按钮
case "fullscreen":
if (browser.screen === "xs") {
return null;
}
// 是否显示全屏按钮
if (isFullscreen.value) {
return (
<button
type="button"
class="minimize"
onClick={() => {
changeFullscreen(false);
}}>
<el-icon>
<Minus />
</el-icon>
</button>
);
} else {
return (
<button
type="button"
class="maximize"
onClick={() => {
changeFullscreen(true);
}}>
<el-icon>
<FullScreen />
</el-icon>
</button>
);
}
// 关闭按钮
case "close":
return (
<button type="button" class="close" onClick={close}>
<el-icon>
<Close />
</el-icon>
</button>
);
// 自定义按钮
default:
return renderNode(e, {
slots
});
}
})}
</div>
</div>
)
);
}
expose({
Dialog,
visible,
isFullscreen,
open,
close,
changeFullscreen
});
return () => {
return h(
<el-dialog
ref={Dialog}
class={["cl-dialog", { "is-transparent": props.transparent }]}
width={props.width}
beforeClose={props.beforeClose}
show-close={false}
append-to-body
fullscreen={isFullscreen.value}
v-model={visible.value}
onClose={onClose}
/>,
{},
{
header() {
return renderHeader();
},
default() {
const height = isFullscreen.value ? "100%" : props.height;
const style = {
padding: props.padding,
height
};
function content() {
return (
<div class="cl-dialog__default" style={style} key={cacheKey.value}>
{slots.default?.()}
</div>
);
}
if (props.scrollbar) {
style.height = "auto";
return <el-scrollbar height={height}>{content()}</el-scrollbar>;
} else {
return content();
}
},
footer() {
const d = slots.footer?.();
if (d && d[0]?.shapeFlag) {
return <div class="cl-dialog__footer">{d}</div>;
}
return null;
}
}
);
};
}
});

View File

@@ -0,0 +1,15 @@
import { defineComponent } from "vue";
export default defineComponent({
name: "cl-error-message",
props: {
title: String
},
setup(props) {
return () => {
return <div class="cl-error-message">{props.title}</div>;
};
}
});

View File

@@ -0,0 +1,23 @@
import { defineComponent } from "vue";
export default defineComponent({
name: "cl-filter",
props: {
label: String
},
setup(props, { slots }) {
return () => {
return (
<div class="cl-filter">
<span class="cl-filter__label" v-show={props.label}>
{props.label}
</span>
{slots.default?.()}
</div>
);
};
}
});

View File

@@ -0,0 +1,11 @@
import { defineComponent } from "vue";
export default defineComponent({
name: "cl-flex1",
setup() {
return () => {
return <div class="cl-flex1" />;
};
}
});

View File

@@ -0,0 +1,51 @@
import { defineComponent, ref } from "vue";
import { ArrowDown, ArrowUp } from "@element-plus/icons-vue";
export default defineComponent({
name: "cl-form-card",
components: {
ArrowDown,
ArrowUp
},
props: {
label: String,
// 展开状态
expand: {
type: Boolean,
default: true
},
// 是否能展开、收起
isExpand: {
type: Boolean,
default: true
}
},
setup(props, { slots }) {
const visible = ref(props.expand);
function toExpand() {
if (props.isExpand) {
visible.value = !visible.value;
}
}
return () => {
return (
<div class={["cl-form-card", { "is-expand": visible.value }]}>
<div class="cl-form-card__header" v-show={props.label} onClick={toExpand}>
<span>{props.label}</span>
<el-icon v-show={props.isExpand}>
<arrow-down v-show={!visible.value} />
<arrow-up v-show={visible.value} />
</el-icon>
</div>
<div class="cl-form-card__container">{slots.default?.()}</div>
</div>
);
};
}
});

View File

@@ -0,0 +1,145 @@
import {
defineComponent,
h,
nextTick,
onMounted,
PropType,
reactive,
ref,
toRaw,
watch
} from "vue";
import { isEmpty } from "lodash-es";
import { useDialog, useRefs } from "../../hooks";
export default defineComponent({
name: "cl-form-tabs",
props: {
modelValue: [String, Number],
labels: {
type: Array,
default: () => []
},
justify: {
type: String as PropType<
"start" | "end" | "left" | "right" | "center" | "justify" | "match-parent"
>,
default: "center"
},
type: {
type: String as PropType<"card" | "default">,
default: "default"
}
},
emits: ["update:modelValue", "change"],
setup(props, { emit, expose }) {
const { refs, setRefs } = useRefs();
// 标识
const active = ref("");
// 切换列表
const list = ref<any[]>([]);
// 下划线
const line = reactive({
width: "",
offsetLeft: "",
transform: "",
backgroundColor: ""
});
function update(val: any) {
if (!val) {
return false;
}
nextTick(() => {
const index = list.value.findIndex((e) => e.value === val);
const item = refs[`tab-${index}`];
if (item) {
// 下划线位置
line.width = item.offsetWidth + "px";
line.transform = `translateX(${item.offsetLeft}px)`;
// 靠左位置
let left = item.offsetLeft + item.clientWidth / 2 - 414 / 2 + 15;
if (left < 0) {
left = 0;
}
// 设置滚动距离
refs.tabs.scrollLeft = left;
}
});
active.value = val;
emit("update:modelValue", val);
}
// 监听绑定值变化
watch(() => props.modelValue, update);
// 监听值修改
watch(
() => active.value,
(val) => {
emit("change", val);
}
);
useDialog({
onFullscreen() {
update(active.value);
}
});
onMounted(function () {
if (!isEmpty(props.labels)) {
list.value = props.labels;
update(isEmpty(props.modelValue) ? list.value[0].value : props.modelValue);
}
});
expose({
active,
list,
line,
update
});
return () => {
return (
<div class={["cl-form-tabs", `cl-form-tabs--${props.type}`]}>
<div
class="cl-form-tabs__wrap"
style={{ textAlign: props.justify }}
ref={setRefs("tabs")}>
<ul>
{list.value.map((e, i) => {
return (
<li
ref={setRefs(`tab-${i}`)}
class={{ "is-active": e.value === active.value }}
onClick={() => {
update(e.value);
}}>
{e.icon && <el-icon>{h(toRaw(e.icon))}</el-icon>}
<span>{e.label}</span>
</li>
);
})}
{line.width && <div class="cl-form-tabs__line" style={line}></div>}
</ul>
</div>
</div>
);
};
}
});

View File

@@ -0,0 +1,146 @@
import { assign } from "lodash-es";
import { dataset } from "../../../utils";
export function useAction({
config,
form,
Form
}: {
config: ClForm.Config;
form: obj;
Form: Vue.Ref<any>;
}) {
// 设置数据
function set(
{
prop,
key,
path
}: { prop?: string; key?: "options" | "props" | "hidden" | "hidden-toggle"; path?: string },
data?: any
) {
const p: string = path || "";
if (path) {
dataset(config, p, data);
} else {
let d: any;
if (prop) {
function deep(arr: ClForm.Item[]) {
arr.forEach((e) => {
if (e.prop == prop) {
d = e;
} else {
if (e.children) {
deep(e.children);
}
}
});
}
deep(config.items);
}
if (d) {
switch (key) {
case "options":
d.component.options = data;
break;
case "props":
assign(d.component.props, data);
break;
case "hidden":
d.hidden = data;
break;
case "hidden-toggle":
d.hidden = data === undefined ? !d.hidden : !data;
break;
default:
assign(d, data);
break;
}
} else {
console.error(`[set] ${prop} is not found`);
}
}
}
// 获取表单值
function getForm(prop: string) {
return prop ? form[prop] : form;
}
// 设置表单值
function setForm(prop: string, value: any) {
form[prop] = value;
}
// 设置配置
function setConfig(path: string, value: any) {
set({ path }, value);
}
// 设置数据
function setData(prop: string, value: any) {
set({ prop }, value);
}
// 设置表单项的下拉数据列表
function setOptions(prop: string, value: any[]) {
set({ prop, key: "options" }, value);
}
// 设置表单项的组件参数
function setProps(prop: string, value: any) {
set({ prop, key: "props" }, value);
}
// 切换表单项的显示、隐藏
function toggleItem(prop: string, value?: boolean) {
set({ prop, key: "hidden-toggle" }, value);
}
// 对部分表单项隐藏
function hideItem(...props: string[]) {
props.forEach((prop) => {
set({ prop, key: "hidden" }, true);
});
}
// 对部分表单项显示
function showItem(...props: string[]) {
props.forEach((prop) => {
set({ prop, key: "hidden" }, false);
});
}
// 设置标题
function setTitle(value: string) {
config.title = value;
}
// 是否展开表单项
function collapseItem(e: any) {
Form.value?.clearValidate(e.prop);
e.collapse = !e.collapse;
}
return {
getForm,
setForm,
setData,
setConfig,
setOptions,
setProps,
toggleItem,
hideItem,
showItem,
setTitle,
collapseItem
};
}

View File

@@ -0,0 +1,36 @@
import { useElApi } from "../../../hooks";
export function useApi({ Form }: { Form: Vue.Ref<any> }) {
return useElApi(
[
"open",
"close",
"clear",
"reset",
"submit",
"bindForm",
"changeTab",
"setTitle",
"showLoading",
"hideLoading",
"collapseItem",
"getForm",
"setForm",
"invokeData",
"setData",
"setConfig",
"setOptions",
"setProps",
"toggleItem",
"hideItem",
"showItem",
"validate",
"validateField",
"resetFields",
"scrollToField",
"clearValidate",
"fields"
],
Form
);
}

View File

@@ -0,0 +1,85 @@
import { reactive, ref, watch } from "vue";
import { useConfig } from "../../../hooks";
import { cloneDeep } from "lodash-es";
export function useForm() {
const { dict } = useConfig();
// 表单配置
const config = reactive<ClForm.Config>({
title: "-",
height: undefined,
width: "50%",
props: {
labelWidth: 100
},
on: {},
op: {
hidden: false,
saveButtonText: dict.label.save,
closeButtonText: dict.label.close,
buttons: ["close", "save"]
},
dialog: {
closeOnClickModal: false,
appendToBody: true
},
items: [],
form: {},
_data: {}
});
const Form = ref();
// 表单数据
const form = reactive<obj>({});
// 表单数据备份
const oldForm = ref<obj>({});
// 表单是否可见
const visible = ref(false);
// 表单提交保存状态
const saving = ref(false);
// 表单加载状态
const loading = ref(false);
// 表单禁用状态
const disabled = ref(false);
// 监听表单变化
watch(
() => form,
(val) => {
if (config.on?.change) {
for (const i in val) {
if (form[i] !== oldForm.value[i]) {
config.on?.change(val, i);
}
}
}
oldForm.value = cloneDeep(val);
},
{
deep: true
}
);
return {
Form,
config,
form,
visible,
saving,
loading,
disabled
};
}
export * from "./action";
export * from "./api";
export * from "./plugins";
export * from "./tabs";

View File

@@ -0,0 +1,92 @@
import { getCurrentInstance, type Ref, watch, type WatchStopHandle } from "vue";
import { useConfig } from "../../../hooks";
import { uniqueFns } from "../../../utils";
export function usePlugins(enable: boolean, { visible }: { visible: Ref<boolean> }) {
const that: any = getCurrentInstance();
const { style } = useConfig();
interface Event {
onOpen: (() => void)[];
onClose: (() => void)[];
onSubmit: ((data: obj) => Promise<obj> | obj)[];
[key: string]: any;
}
// 事件
const ev: Event = {
onOpen: [],
onClose: [],
onSubmit: []
};
// 监听器
let timer: WatchStopHandle | null = null;
// 插件创建
function create(plugins: ClForm.Plugin[] = []) {
if (!enable) {
return false;
}
for (const i in ev) {
ev[i] = [];
}
// 停止监听
if (timer) {
timer();
}
// 执行
uniqueFns([...(style.form.plugins || []), ...plugins]).forEach((p) => {
const d: any = {
exposed: that.exposed
};
for (const i in ev) {
d[i] = (cb: any) => {
ev[i].push(cb);
};
}
p(d);
});
timer = watch(
visible,
(val) => {
if (val) {
setTimeout(() => {
ev.onOpen.forEach((e) => e());
}, 10);
} else {
ev.onClose.forEach((e) => e());
}
},
{
immediate: true
}
);
}
// 表单提交
async function submit(data: any) {
let d = data;
for (let i = 0; i < ev.onSubmit.length; i++) {
const d2 = await ev.onSubmit[i](d);
if (d2) {
d = d2;
}
}
return d;
}
return {
create,
submit
};
}

View File

@@ -0,0 +1,151 @@
import { computed, ref } from "vue";
export function useTabs({ config, Form }: { config: ClForm.Config; Form: Vue.Ref<any> }) {
// 选中
const active = ref<string | undefined>();
// 列表
const list = computed(() => {
return get()?.props?.labels || [];
});
// 获取选项
function getItem(value: any) {
return list.value.find((e) => e.value == value);
}
// 是否已加载
function isLoaded(value: any) {
const d = getItem(value);
return d?.lazy ? d.loaded : true;
}
// 加载后
function onLoad(value: any) {
const d = getItem(value);
d!.loaded = true;
}
// 查找分组
function toGroup(opts: { config: ClForm.Config; prop: string; refs: any }) {
if (active.value) {
let name;
// 查找标签上绑定的数据
const el = opts.refs.form.querySelector(`[data-prop="${opts.prop}"]`);
// 各自判断
if (el) {
name = el?.getAttribute("data-group");
} else {
function deep(d: ClForm.Item) {
if (d.prop == opts.prop) {
name = d.group;
} else {
if (d.children) {
d.children.forEach(deep);
}
}
}
config.items.forEach(deep);
}
if (name) {
set(name);
}
}
}
// 获取参数
function get() {
return config.items.find((e) => e.type === "tabs");
}
// 设置参数
function set(data: any) {
active.value = data;
}
// 清空
function clear() {
// 清空选中
active.value = undefined;
// 清空加载状态
list.value.forEach((e) => {
if (e.lazy && e.loaded) {
e.loaded = undefined;
}
});
}
// 切换
function change(value: any, isValid = true) {
return new Promise((resolve: Function, reject: Function) => {
function next() {
active.value = value;
resolve();
}
if (isValid) {
let isError = false;
const arr = config.items
.filter((e) => e.group == active.value && !e._hidden && e.prop)
.map((e) => {
return new Promise((r: Function) => {
// 验证表单
Form.value.validateField(e.prop, (valid: string) => {
if (valid) {
isError = true;
}
r(valid);
});
});
});
Promise.all(arr).then((msg) => {
if (isError) {
reject(msg.filter(Boolean));
} else {
next();
}
});
} else {
next();
}
});
}
// 合并
function mergeProp(item: ClForm.Item) {
const d = get();
if (d && d.props) {
const { mergeProp, labels = [] } = d.props;
if (mergeProp) {
const t = labels.find((e) => e.value == item.group);
if (t && t.name) {
item.prop = `${t.name}-${item.prop}`;
}
}
}
}
return {
active,
list,
isLoaded,
onLoad,
get,
set,
change,
clear,
mergeProp,
toGroup
};
}

View File

@@ -0,0 +1,683 @@
import { defineComponent, h, nextTick } from "vue";
import { assign, cloneDeep, isBoolean, isFunction, keys } from "lodash-es";
import { useAction, useForm, usePlugins, useTabs } from "./helper";
import { useBrowser, useConfig, useElApi, useRefs } from "../../hooks";
import { getValue, merge } from "../../utils";
import formHook from "../../utils/form-hook";
import { renderNode } from "../../utils/vnode";
export default defineComponent({
name: "cl-form",
props: {
name: String,
inner: Boolean,
inline: Boolean,
enablePlugin: {
type: Boolean,
default: true
}
},
setup(props, { expose, slots }) {
const { refs, setRefs } = useRefs();
const { style, dict } = useConfig();
const browser = useBrowser();
const { Form, config, form, visible, saving, loading, disabled } = useForm();
// 关闭的操作类型
let closeAction: ClForm.CloseAction = "close";
// 旧表单数据
let defForm: obj | undefined;
// 选项卡
const Tabs = useTabs({ config, Form });
// 操作
const Action = useAction({ config, form, Form });
// 方法
const ElFormApi = useElApi(
[
"validate",
"validateField",
"resetFields",
"scrollToField",
"clearValidate",
"fields"
],
Form
);
// 插件
const plugin = usePlugins(props.enablePlugin, { visible });
// 显示加载中
function showLoading() {
loading.value = true;
}
// 隐藏加载
function hideLoading() {
loading.value = false;
}
// 设置是否禁用
function setDisabled(val: boolean = true) {
disabled.value = val;
}
// 请求表单保存状态
function done() {
saving.value = false;
}
// 关闭表单
function close(action?: ClForm.CloseAction) {
if (action) {
closeAction = action;
}
beforeClose(() => {
visible.value = false;
done();
});
}
// 关闭前
function beforeClose(done: fn) {
if (config.on?.close) {
config.on.close(closeAction, done);
} else {
done();
}
}
// 关闭后
function onClosed() {
Tabs.clear();
Form.value?.clearValidate();
}
// 清空表单验证
function clear() {
for (const i in form) {
delete form[i];
}
setTimeout(() => {
Form.value?.clearValidate();
}, 0);
}
// 重置
function reset() {
if (defForm) {
for (const i in defForm) {
form[i] = cloneDeep(defForm[i]);
}
}
}
// 转换表单值,处理多层级等数据
function invokeData(d: any) {
for (const i in d) {
if (i.includes("-")) {
// 结构参数
const [a, ...arr] = i.split("-");
// 关键值的key
const k: string = arr.pop() || "";
if (!d[a]) {
d[a] = {};
}
let f: any = d[a];
// 设置默认值
arr.forEach((e) => {
if (!f[e]) {
f[e] = {};
}
f = f[e];
});
// 设置关键值
f[k] = d[i];
delete d[i];
}
}
}
// 表单提交
function submit(callback?: fn) {
// 验证表单
Form.value.validate(async (valid: boolean, error: any) => {
if (valid) {
saving.value = true;
// 拷贝表单值
const d = cloneDeep(form);
config.items.forEach((e) => {
function deep(e: ClForm.Item) {
if (e.prop) {
// 过滤隐藏的表单项
if (e._hidden) {
if (e.prop) {
delete d[e.prop];
}
}
// hook 提交处理
if (e.hook) {
formHook.submit({
...e,
value: e.prop ? d[e.prop] : undefined,
form: d
});
}
}
if (e.children) {
e.children.forEach(deep);
}
}
deep(e);
});
// 处理数据
invokeData(d);
const submit = callback || config.on?.submit;
// 提交事件
if (submit) {
submit(await plugin.submit(d), {
close() {
close("save");
},
done
});
} else {
done();
}
} else {
// 切换到对应的选项卡
Tabs.toGroup({
refs,
config,
prop: keys(error)[0]
});
}
});
}
// 打开表单
function open(options?: ClForm.Options, plugins?: ClForm.Plugin[]) {
if (!options) {
return console.error("Options is not null");
}
// 清空
if (options.isReset !== false) {
clear();
}
// 显示对话框
visible.value = true;
// 默认关闭方式
closeAction = "close";
// 合并配置
for (const i in config) {
switch (i) {
// 表单项
case "items":
function deep(arr: any[]): any[] {
return arr.map((e) => {
const d = getValue(e);
return {
...d,
children: d?.children ? deep(d.children) : undefined
};
});
}
config.items = deep(options.items || []);
break;
// 事件、参数、操作
case "on":
case "op":
case "props":
case "dialog":
case "_data":
merge(config[i], options[i] || {});
break;
// 其他
default:
config[i] = options[i];
break;
}
}
// 预设表单值
if (options?.form) {
for (const i in options.form) {
form[i] = options.form[i];
}
}
// 设置表单数据
config.items.forEach((e) => {
function deep(e: ClForm.Item) {
if (e.prop) {
// 解析 prop
if (e.prop.includes(".")) {
e.prop = e.prop.replace(/\./g, "-");
}
// prop 合并
Tabs.mergeProp(e);
// hook 绑定值
formHook.bind({
...e,
value: form[e.prop] !== undefined ? form[e.prop] : cloneDeep(e.value),
form
});
// 表单验证
if (e.required) {
e.rules = {
required: true,
message: dict.label.nonEmpty.replace("{label}", e.label || "")
};
}
}
// 设置 tabs 默认值
if (e.type == "tabs") {
Tabs.set(e.value);
}
// 子集
if (e.children) {
e.children.forEach(deep);
}
}
deep(e);
});
// 设置默认值
if (!defForm) {
defForm = cloneDeep(form);
}
// 创建插件
plugin.create(plugins);
// 打开回调
nextTick(() => {
setTimeout(() => {
// 打开事件
if (config.on?.open) {
config.on.open(form);
}
}, 10);
});
}
// 绑定表单数据
function bindForm(data: any) {
config.items.forEach((e) => {
function deep(e: ClForm.Item) {
formHook.bind({
...e,
value: e.prop ? data[e.prop] : undefined,
form: data
});
if (e.children) {
e.children.forEach(deep);
}
}
deep(e);
});
assign(form, data);
}
// 渲染表单项
function renderFormItem(e: ClForm.Item) {
const { isDisabled } = config._data;
if (e.type == "tabs") {
return (
<cl-form-tabs v-model={Tabs.active.value} {...e.props} onChange={Tabs.onLoad} />
);
}
// 是否隐藏
e._hidden = parseHidden(e.hidden, {
scope: form
});
// 分组显示
const inGroup = e.group ? e.group === Tabs.active.value : true;
// 是否已加载完成
const isLoaded = e.component && Tabs.isLoaded(e.group);
// 表单项
const FormItem = h(
<el-form-item
class={{
"no-label": !(e.renderLabel || e.label),
"has-children": !!e.children
}}
key={e.prop}
data-group={e.group || "-"}
data-prop={e.prop || "-"}
label-width={props.inline ? "auto" : ""}
label={e.label}
prop={e.prop}
rules={isDisabled ? null : e.rules}
required={e._hidden ? false : e.required}
v-show={inGroup && !e._hidden}
/>,
e.props,
{
label() {
if (e.renderLabel) {
return renderNode(e.renderLabel, {
scope: form,
render: "slot",
slots
});
} else {
return e.label;
}
},
default() {
return (
<div>
<div class="cl-form-item">
{["prepend", "component", "append"]
.filter((k) => e[k])
.map((name) => {
const children = e.children && (
<div class="cl-form-item__children">
<el-row gutter={10}>
{e.children.map(renderFormItem)}
</el-row>
</div>
);
const Item = renderNode(e[name], {
item: e,
prop: e.prop,
scope: form,
slots,
children,
_data: {
isDisabled
}
});
return (
<div
v-show={!e.collapse}
class={[
`cl-form-item__${name}`,
{
flex1: e.flex !== false
}
]}
style={e[name].style}>
{Item}
</div>
);
})}
</div>
{isBoolean(e.collapse) && (
<div
class="cl-form-item__collapse"
onClick={() => {
Action.collapseItem(e);
}}>
<el-divider content-position="center">
{e.collapse
? dict.label.seeMore
: dict.label.hideContent}
</el-divider>
</div>
)}
</div>
);
}
}
);
let span = e.span || style.form.span;
if (browser.isMini) {
span = 24;
}
// 是否行内
const Item = props.inline ? (
FormItem
) : (
<el-col span={span} {...e.col} v-show={inGroup && !e._hidden}>
{FormItem}
</el-col>
);
return isLoaded ? Item : null;
}
// 渲染表单
function renderContainer() {
// 表单项列表
const children = config.items.map(renderFormItem);
// 表单标签位置
const labelPosition =
browser.isMini && !props.inline
? "top"
: config.props.labelPosition || style.form.labelPosition;
return (
<div class="cl-form__container" ref={setRefs("form")}>
{h(
<el-form
ref={Form}
size={style.size}
label-width={style.form.labelWidth}
inline={props.inline}
require-asterisk-position="right"
disabled={saving.value}
scroll-to-error
model={form}
onSubmit={(e: Event) => {
submit();
e.preventDefault();
}}
/>,
{
...config.props,
labelPosition
},
{
default: () => {
const items = [
slots.prepend && slots.prepend({ scope: form }),
children,
slots.append && slots.append({ scope: form })
];
return (
<div class="cl-form__items" v-loading={loading.value}>
{props.inline ? (
items
) : (
<el-row gutter={10}>{items}</el-row>
)}
</div>
);
}
}
)}
</div>
);
}
// 渲染表单底部按钮
function renderFooter() {
const { hidden, buttons, saveButtonText, closeButtonText, justify } = config.op;
if (hidden) {
return null;
}
const Btns = buttons?.map((e: any) => {
switch (e) {
case "save":
return (
<el-button
type="success"
size={style.size}
disabled={loading.value}
loading={saving.value}
onClick={() => {
submit();
}}>
{saveButtonText}
</el-button>
);
case "close":
return (
<el-button
size={style.size}
onClick={() => {
close("close");
}}>
{closeButtonText}
</el-button>
);
default:
return renderNode(e, {
scope: form,
slots,
custom() {
return (
<el-button
type={e.type}
{...e.props}
onClick={() => {
e.onClick({ scope: form });
}}>
{e.label}
</el-button>
);
}
});
}
});
return (
<div
class="cl-form__footer"
style={{
justifyContent: justify || "flex-end"
}}>
{Btns}
</div>
);
}
// Tools
function parseHidden(value: any, { scope }: any) {
if (isBoolean(value)) {
return value;
} else if (isFunction(value)) {
return value({ scope });
}
return false;
}
const ctx = {
name: props.name,
refs,
Form,
visible,
saving,
form,
config,
loading,
disabled,
open,
close,
done,
clear,
reset,
submit,
invokeData,
bindForm,
showLoading,
hideLoading,
setDisabled,
Tabs,
...Action,
...ElFormApi
};
expose(ctx);
return () => {
if (props.inner) {
return (
visible.value && (
<div class="cl-form">
{renderContainer()}
{renderFooter()}
</div>
)
);
} else {
return h(
<cl-dialog v-model={visible.value} class="cl-form" />,
{
title: config.title,
height: config.height,
width: config.width,
...config.dialog,
beforeClose,
onClosed,
keepAlive: false
},
{
default() {
return renderContainer();
},
footer() {
return renderFooter();
}
}
);
}
};
}
});

View File

@@ -0,0 +1,50 @@
import { App } from "vue";
import Crud from "./crud";
import AddBtn from "./add-btn";
import AdvBtn from "./adv/btn";
import AdvSearch from "./adv/search";
import Flex from "./flex1";
import Form from "./form";
import FormTabs from "./form-tabs";
import FormCard from "./form-card";
import MultiDeleteBtn from "./multi-delete-btn";
import Pagination from "./pagination";
import RefreshBtn from "./refresh-btn";
import SearchKey from "./search-key";
import Table from "./table";
import Upsert from "./upsert";
import Dialog from "./dialog";
import Filter from "./filter";
import Search from "./search";
import ErrorMessage from "./error-message";
import Row from "./row";
import ContextMenu from "./context-menu";
export const components: { [key: string]: any } = {
Crud,
AddBtn,
AdvBtn,
AdvSearch,
Flex,
Form,
FormTabs,
FormCard,
MultiDeleteBtn,
Pagination,
RefreshBtn,
SearchKey,
Table,
Upsert,
Dialog,
Filter,
Search,
ErrorMessage,
Row,
ContextMenu
};
export function useComponent(app: App) {
for (const i in components) {
app.component(components[i].name, components[i]);
}
}

View File

@@ -0,0 +1,27 @@
import { defineComponent } from "vue";
import { useConfig, useCore } from "../../hooks";
export default defineComponent({
name: "cl-multi-delete-btn",
setup(_, { slots }) {
const { crud } = useCore();
const { style } = useConfig();
return () => {
return (
crud.getPermission("delete") && (
<el-button
type="danger"
size={style.size}
disabled={crud.selection.length === 0}
onClick={() => {
crud.rowDelete(...crud.selection);
}}>
{slots.default?.() || crud.dict.label.multiDelete}
</el-button>
)
);
};
}
});

View File

@@ -0,0 +1,90 @@
import { defineComponent, h, onMounted, onUnmounted, ref } from "vue";
import { useBrowser, useConfig, useCore } from "../../hooks";
export default defineComponent({
name: "cl-pagination",
setup(_, { expose }) {
const { crud, mitt } = useCore();
const { style } = useConfig();
const browser = useBrowser();
// 总数
const total = ref(0);
// 当前页数
const currentPage = ref(1);
// 每页大小
const pageSize = ref(20);
// 页数发生变化
function onCurrentChange(index: number) {
crud.refresh({
page: index
});
}
// 条目发生变化
function onSizeChange(size: number) {
crud.refresh({
page: 1,
size
});
}
// 设置分页信息
function setPagination(res: obj) {
if (res) {
currentPage.value = res.currentPage || res.page || 1;
pageSize.value = res.pageSize || res.size || 20;
total.value = res.total || 0;
crud.params.size = pageSize.value;
}
}
// 数据刷新
function onRefresh(res: ClCrud.Response["page"]) {
setPagination(res.pagination);
}
// 监听刷新事件
onMounted(() => {
mitt.on("crud.refresh", onRefresh);
});
// 移除监听事件
onUnmounted(() => {
mitt.off("crud.refresh", onRefresh);
});
expose({
total,
currentPage,
pageSize,
setPagination
});
return () => {
return h(
<el-pagination
class="cl-pagination"
size={browser.isMini ? "small" : style.size}
background
page-sizes={[10, 20, 30, 40, 50, 100]}
pager-count={browser.isMini ? 5 : 7}
layout={
browser.isMini ? "total, pager" : "total, sizes, prev, pager, next, jumper"
}
/>,
{
onSizeChange,
onCurrentChange,
total: total.value,
currentPage: currentPage.value,
pageSize: pageSize.value
}
);
};
}
});

View File

@@ -0,0 +1,23 @@
import { defineComponent } from "vue";
import { useConfig, useCore } from "../../hooks";
export default defineComponent({
name: "cl-refresh-btn",
setup(_, { slots }) {
const { crud } = useCore();
const { style } = useConfig();
return () => {
return (
<el-button
size={style.size}
onClick={() => {
crud.refresh();
}}>
{slots.default?.() || crud.dict.label.refresh}
</el-button>
);
};
}
});

View File

@@ -0,0 +1,11 @@
import { defineComponent } from "vue";
export default defineComponent({
name: "cl-row",
setup(_, { slots }) {
return () => {
return <el-row class="cl-row">{slots.default && slots.default()}</el-row>;
};
}
});

View File

@@ -0,0 +1,179 @@
import { computed, defineComponent, type PropType, ref, useModel } from "vue";
import { useConfig, useCore } from "../../hooks";
import { parsePx } from "../../utils";
import { debounce } from "lodash-es";
export default defineComponent({
name: "cl-search-key",
props: {
// 绑定值
modelValue: String,
// 选中字段
field: {
type: String,
default: "keyWord"
},
// 字段列表
fieldList: {
type: Array as PropType<Array<{ label: string; value: string }>>,
default: () => []
},
// 搜索时的钩子
onSearch: Function,
// 输入框占位内容
placeholder: String,
// 宽度
width: {
type: [String, Number],
default: 280
},
// 是否实时刷新
refreshOnInput: Boolean
},
emits: ["update:modelValue", "change", "field-change"],
setup(props, { emit, expose }) {
const { crud } = useCore();
const { style } = useConfig();
// 选中字段
const selectField = ref(props.field);
// 加载状态
const loading = ref(false);
// 文字提示
const placeholder = computed(() => {
if (props.placeholder) {
return props.placeholder;
} else {
const item = props.fieldList.find((e) => e.value == selectField.value);
if (item) {
return crud.dict.label.placeholder + item.label;
} else {
return crud.dict.label.searchKey;
}
}
});
// 搜索内容
const value = useModel(props, "modelValue");
// 锁
let lock = false;
// 搜索
function search() {
if (!lock) {
const params: obj = {};
props.fieldList.forEach((e) => {
params[e.value] = undefined;
});
async function next(newParams?: obj) {
loading.value = true;
await crud
.refresh({
page: 1,
...params,
[selectField.value]: value.value || undefined,
...newParams
})
.catch((err) => {
console.error(err);
});
loading.value = false;
}
if (props.onSearch) {
props.onSearch(params, { next });
} else {
next();
}
}
}
// 回车搜索
function onKeydown({ key }: KeyboardEvent) {
if (key === "Enter") {
search();
}
}
// 监听变化
function onChange(val: string) {
if (!props.refreshOnInput) {
search();
lock = true;
setTimeout(() => {
lock = false;
}, 300);
emit("change", val);
}
}
// 监听输入
const onInput = debounce((val: string) => {
emit("change", val);
if (props.refreshOnInput) {
search();
}
}, 300);
// 监听字段选择
function onFieldChange() {
emit("field-change", selectField.value);
value.value = undefined;
}
expose({
search
});
return () => {
return (
<div class="cl-search-key">
<el-select
class="cl-search-key__select"
size={style.size}
v-model={selectField.value}
v-show={props.fieldList.length > 0}
onChange={onFieldChange}>
{props.fieldList.map((e, i) => (
<el-option key={i} label={e.label} value={e.value} />
))}
</el-select>
<div class="cl-search-key__wrap" style={{ width: parsePx(props.width) }}>
<el-input
v-model={value.value}
size={style.size}
placeholder={placeholder.value}
onKeydown={onKeydown}
onChange={onChange}
onInput={onInput}
clearable
/>
<el-button
size={style.size}
type="primary"
loading={loading.value}
onClick={search}>
{crud.dict.label.search}
</el-button>
</div>
</div>
);
};
}
});

View File

@@ -0,0 +1,21 @@
import { getCurrentInstance } from "vue";
import { useConfig } from "../../../hooks";
import { uniqueFns } from "../../../utils";
export function usePlugins() {
const that: any = getCurrentInstance();
const { style } = useConfig();
// 插件创建
function create(plugins: ClSearch.Plugin[] = []) {
uniqueFns([...(style.search?.plugins || []), ...plugins]).forEach((p) => {
p({
exposed: that.exposed
});
});
}
return {
create
};
}

View File

@@ -0,0 +1,307 @@
import { useConfig, useCore, useForm, useProxy, useRefs } from "../../hooks";
import {
defineComponent,
h,
inject,
mergeProps,
nextTick,
onMounted,
onUnmounted,
PropType,
reactive,
ref
} from "vue";
import { useApi } from "../form/helper";
import { Bottom, Refresh, Search, Top } from "@element-plus/icons-vue";
import { mitt } from "../../utils/mitt";
import { isArray, isEmpty } from "lodash-es";
import { usePlugins } from "./helper/plugins";
export default defineComponent({
name: "cl-search",
props: {
// 是否行内
inline: {
type: Boolean,
default: true
},
// cl-form 表单配置
props: {
type: Object,
default: () => ({})
},
// 表单值
data: {
type: Object,
default: () => ({})
},
// 列
items: {
type: Array as PropType<ClForm.Item[]>,
default: () => []
},
// 是否需要重置按钮
resetBtn: {
type: Boolean,
default: false
},
// 是否需要折叠
collapse: {
type: Boolean,
default: false
},
// 初始化
onLoad: Function,
// 搜索时钩子
onSearch: Function
},
emits: ["reset"],
setup(props, { slots, expose, emit }) {
const { crud } = useCore();
const { refs, setRefs } = useRefs();
const { style } = useConfig();
const plugin = usePlugins();
// 配置
const config = reactive<ClSearch.Config>(
mergeProps(props, inject("useSearch__options") || {})
);
// cl-form
const Form = useForm();
// 加载中
const loading = ref(false);
// 展开
const isExpand = ref(!config.collapse);
// 显示展开、收起按钮
const showExpandBtn = ref(false);
// 搜索
function search(params?: any) {
const form = Form.value?.getForm();
async function next(data?: any) {
loading.value = true;
const d = {
page: 1,
...form,
...data,
...params
};
for (const i in d) {
if (d[i] === "") {
d[i] = undefined;
}
}
const res = await crud.refresh(d);
loading.value = false;
return res;
}
if (config.onSearch) {
config.onSearch(form, { next });
} else {
next();
}
}
// 重置
function reset() {
const d: any = {};
config.items?.map((e) => {
if (typeof e.hook != "string" && e.hook?.reset) {
const props = e.hook.reset(e.prop!);
if (isArray(props)) {
props.forEach((prop) => {
d[prop] = undefined;
});
}
}
d[e.prop!] = undefined;
});
// 重置表单
Form.value?.reset();
// 列表刷新
search(d);
// 重置事件
emit("reset", d);
}
// 收起、展开
function expand() {
isExpand.value = !isExpand.value;
nextTick(() => {
crud?.["cl-table"].calcMaxHeight();
});
}
// 判断展开状态
function onExpand() {
if (config.collapse) {
const el = refs.form?.querySelector(".cl-form__items");
if (el) {
showExpandBtn.value = el.clientHeight > 84;
}
}
}
function onResize() {
onExpand();
}
const ctx = {
search,
reset,
Form,
config,
...useApi({ Form })
};
useProxy(ctx);
expose(ctx);
plugin.create(config.plugins);
onMounted(() => {
Form.value?.open({
op: {
hidden: true
},
props: {
labelPosition: "right",
...config.props
},
items: config.items?.map((e) => {
return {
col: {
sm: 12,
md: 8,
xs: 24,
lg: 6
},
...e
};
}),
form: config.data,
on: {
open(data) {
config.onLoad?.(data);
onExpand();
},
change(data, prop) {
config.onChange?.(data, prop);
}
}
});
mitt.on("resize", onResize);
});
onUnmounted(() => {
mitt.off("resize", onResize);
});
return () => {
const btnEl = (
<el-form-item label=" " class="cl-search__btns">
{/* 重置按钮 */}
{config.resetBtn && (
<el-button size={style.size} icon={Refresh} onClick={reset}>
{crud.dict.label.reset}
</el-button>
)}
{/* 搜索按钮 */}
<el-button
type="primary"
loading={loading.value}
size={style.size}
icon={Search}
onClick={() => {
search();
}}>
{crud.dict.label.search}
</el-button>
{/* 自定义按钮 */}
{slots?.buttons?.(Form.value?.form)}
</el-form-item>
);
return (
<div
class={[
"cl-search",
isExpand.value ? "is-expand" : "is-fold",
{
"is-inline": config.inline,
"is-collapse": config.collapse
}
]}>
<div class="cl-search__form" ref={setRefs("form")}>
{h(
<cl-form
ref={Form}
inner
inline={config.inline}
enable-plugin={false}
name="search"
/>,
{},
{
append() {
return config.collapse ? null : isEmpty(config.items) || btnEl;
},
...slots
}
)}
</div>
{config.collapse && (
<div class="cl-search__more">
{showExpandBtn.value && (
<el-button onClick={expand}>
<span>
{isExpand.value
? crud.dict.label.collapse
: crud.dict.label.expand}
</span>
<el-icon>{isExpand.value ? <Top /> : <Bottom />}</el-icon>
</el-button>
)}
<cl-flex1 />
{btnEl}
</div>
)}
</div>
);
};
}
});

View File

@@ -0,0 +1,35 @@
import { nextTick, ref } from "vue";
import { useCore } from "../../../hooks";
export function useData({ config, Table }: { config: ClTable.Config; Table: Vue.Ref<any> }) {
const { mitt, crud } = useCore();
// 列表数据
const data = ref<obj[]>([]);
// 设置数据
function setData(list: obj[]) {
data.value = list;
}
// 监听刷新
mitt.on("crud.refresh", ({ list }: ClCrud.Response["page"]) => {
data.value = list;
// 显示选中行
nextTick(() => {
crud.selection.forEach((e) => {
const d = list.find((a) => a[config.rowKey] == e[config.rowKey]);
if (d) {
Table.value.toggleRowSelection(d, true);
}
});
});
});
return {
data,
setData
};
}

View File

@@ -0,0 +1,91 @@
import { CloseBold, Search } from "@element-plus/icons-vue";
import { h } from "vue";
import { useCrud } from "../../../hooks";
import { renderNode } from "../../../utils/vnode";
export function renderHeader(item: ClTable.Column, { scope, slots }: any) {
const crud = useCrud();
const slot = slots[`header-${item.prop}`];
if (slot) {
return slot({
scope
});
}
if (!item.search || !item.search.component) {
return item.label;
}
// 显示输入框
function show(e: MouseEvent) {
item.search.isInput = true;
e.stopPropagation();
}
// 隐藏输入框
function hide() {
if (item.search.value !== undefined) {
item.search.value = undefined;
refresh();
}
item.search.isInput = false;
}
// 刷新
function refresh(params?: any) {
const { value } = item.search;
crud.value?.refresh({
page: 1,
[item.prop]: value === "" ? undefined : value,
...params
});
}
// 文字
const text = (
<div class="cl-table-header__search-label" onClick={show}>
<el-icon size={14}>{item.search.icon?.() ?? <Search />}</el-icon>
{item.renderLabel ? item.renderLabel(scope) : item.label}
</div>
);
// 输入框
const input = h(renderNode(item.search.component, { prop: item.prop }), {
clearable: true,
modelValue: item.search.value,
onVnodeMounted(vn) {
// 默认聚焦
vn.component?.exposed?.focus?.();
},
onInput(val: any) {
item.search.value = val;
},
onChange(val: any) {
item.search.value = val;
// 更改时刷新列表
if (item.search.refreshOnChange) {
refresh();
}
}
});
return (
<div class={["cl-table-header__search", { "is-input": item.search.isInput }]}>
<div class="cl-table-header__search-inner">{item.search.isInput ? input : text}</div>
{item.search.isInput && (
<div class="cl-table-header__search-close" onClick={hide}>
<el-icon>
<CloseBold />
</el-icon>
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,99 @@
import { debounce, last } from "lodash-es";
import { nextTick, onActivated, onMounted, ref } from "vue";
import { addClass, removeClass } from "../../../utils";
import { mitt } from "../../../utils/mitt";
// 表格高度
export function useHeight({ config, Table }: { Table: Vue.Ref<any>; config: ClTable.Config }) {
// 最大高度
const maxHeight = ref(0);
// 计算表格最大高度
const update = debounce(async () => {
await nextTick();
let vm = Table.value;
if (vm) {
while (!vm.$parent?.$el.className?.includes("cl-crud")) {
vm = vm.$parent;
}
if (vm) {
const p = vm.$parent.$el;
await nextTick();
// 高度
let h = 0;
// 表格下间距
if (vm.$el.className.includes("cl-row")) {
h += 10;
}
// 上高度
h += vm.$el.offsetTop;
// 获取下高度
let n = vm.$el.nextSibling;
// 集合
const arr = [vm.$el];
while (n) {
if (n.offsetHeight > 0) {
h += n.offsetHeight || 0;
arr.push(n);
if (n.className.includes("cl-row--last")) {
h += 10;
}
}
n = n.nextSibling;
}
// 移除 cl-row--last
arr.forEach((e) => {
removeClass(e, "cl-row--last");
});
// 最后一个可视元素
const z = last(arr);
// 去掉 cl-row 下间距高度
if (z?.className.includes("cl-row")) {
addClass(z, "cl-row--last");
h -= 10;
}
// 上间距
h += parseInt(window.getComputedStyle(p).paddingTop, 10);
// 设置最大高度
if (config.autoHeight) {
maxHeight.value = p.clientHeight - h;
}
}
}
}, 100);
// 窗口大小改变事件
mitt.on("resize", () => {
update();
});
onMounted(function () {
update();
});
onActivated(function () {
update();
});
return {
maxHeight,
calcMaxHeight: update
};
}

View File

@@ -0,0 +1,43 @@
import { inject, reactive, ref } from "vue";
import { useConfig } from "../../../hooks";
import { getValue, mergeConfig } from "../../../utils";
import type { TableInstance } from "element-plus";
export function useTable(props: any) {
const { style } = useConfig();
const Table = ref<TableInstance>();
// 配置
const config = reactive<ClTable.Config>(mergeConfig(props, inject("useTable__options") || {}));
// 列表项动态处理
config.columns = (config.columns || []).map((e) => getValue(e));
// 自动高度
config.autoHeight = config.autoHeight ?? style.table.autoHeight;
// 右键菜单
config.contextMenu = config.contextMenu ?? style.table.contextMenu;
// 事件
if (!config.on) {
config.on = {};
}
// 参数
if (!config.props) {
config.props = {};
}
return { Table, config };
}
export * from "./data";
export * from "./height";
export * from "./op";
export * from "./render";
export * from "./row";
export * from "./selection";
export * from "./sort";
export * from "./header";

View File

@@ -0,0 +1,69 @@
import { nextTick, ref } from "vue";
import { useCore } from "../../../hooks";
import { isArray, isBoolean } from "lodash-es";
export function useOp({ config }: { config: ClTable.Config }) {
const { mitt } = useCore();
// 是否可见,用于解决一些显示隐藏的副作用
const visible = ref(true);
// 重新构建
async function reBuild(cb?: fn) {
visible.value = false;
await nextTick();
if (cb) {
cb();
}
visible.value = true;
await nextTick();
mitt.emit("resize");
}
// 显示列
function showColumn(prop: string | string[], status?: boolean) {
const keys = isArray(prop) ? prop : [prop];
// 多级表头
function deep(list: ClTable.Column[]) {
list.forEach((e) => {
if (e.prop && keys.includes(e.prop)) {
e.hidden = isBoolean(status) ? !status : false;
}
if (e.children) {
deep(e.children);
}
});
}
deep(config.columns);
}
// 隐藏列
function hideColumn(prop: string | string[]) {
showColumn(prop, false);
}
// 设置列
function setColumns(list: ClTable.Column[]) {
if (list) {
reBuild(() => {
config.columns.splice(0, config.columns.length, ...list);
});
}
}
return {
visible,
reBuild,
showColumn,
hideColumn,
setColumns
};
}

View File

@@ -0,0 +1,22 @@
import { getCurrentInstance } from "vue";
import { useConfig } from "../../../hooks";
import { uniqueFns } from "../../../utils";
export function usePlugins() {
const that: any = getCurrentInstance();
const { style } = useConfig();
// 插件创建
function create(plugins: ClTable.Plugin[] = []) {
// 执行
uniqueFns([...(style.table.plugins || []), ...plugins]).forEach((p) => {
p({
exposed: that.exposed
});
});
}
return {
create
};
}

View File

@@ -0,0 +1,327 @@
import { h, useSlots } from "vue";
import { useBrowser, useConfig, useCore } from "../../../hooks";
import { assign, cloneDeep, isArray, isEmpty, isObject, isString, orderBy } from "lodash-es";
import { deepFind, getValue } from "../../../utils";
import { renderNode } from "../../../utils/vnode";
import { renderHeader } from "./header";
// 渲染
export function useRender() {
const browser = useBrowser();
const slots = useSlots();
const { crud } = useCore();
const { style } = useConfig();
// 渲染列
function renderColumn(columns: ClTable.Column[]) {
const arr = columns.map((e) => {
const d = getValue(e);
if (!d.orderNum) {
d.orderNum = 0;
}
return d;
});
return orderBy(arr, "orderNum", "asc")
.map((item, index) => {
if (item.hidden) {
return null;
}
const ElTableColumn = (
<el-table-column
key={`cl-table-column__${index}`}
align={style.table.column.align}
header-align={style.table.column.headerAlign}
minWidth={style.table.column.minWidth}
/>
);
// 操作按钮
if (item.type === "op") {
const props = assign(
{
label: crud.dict.label.op,
width: style.table.column.opWidth,
fixed: browser.isMini ? null : "right"
},
item
);
return h(ElTableColumn, props, {
default: (scope: any) => {
return (
<div class="cl-table__op">
{renderOpButtons(item.buttons, { scope })}
</div>
);
}
});
}
// 多选,序号
else if (["selection", "index"].includes(item.type)) {
return h(ElTableColumn, item);
}
// 默认
else {
function deep(item: ClTable.Column) {
if (item.hidden) {
return null;
}
const props: obj = cloneDeep(item);
// Cannot set property children of #<Element>
delete props.children;
return h(ElTableColumn, props, {
header(scope: any) {
return renderHeader(item, { scope, slots });
},
default(scope: any) {
if (item.children) {
return item.children.map(deep);
}
// 使用插槽
const slot = slots[`column-${item.prop}`];
if (slot) {
return slot({
scope,
item
});
} else {
// 绑定值
let value = scope.row[item.prop];
// 格式化
if (item.formatter) {
value = item.formatter(
scope.row,
scope.column,
value,
scope.$index
);
if (isObject(value)) {
return value;
}
}
// 自定义渲染
if (item.render) {
return item.render(
scope.row,
scope.column,
value,
scope.$index
);
}
// 自定义渲染2
else if (item.component) {
return renderNode(item.component, {
prop: item.prop,
scope: scope.row,
_data: {
column: scope.column,
index: scope.$index,
row: scope.row
}
});
}
// 字典状态
else if (item.dict) {
return renderDict(value, item);
}
// 空数据
else if (isEmpty(value)) {
return scope.emptyText;
} else {
return value;
}
}
}
});
}
return deep(item);
}
})
.filter(Boolean);
}
// 渲染操作按钮
function renderOpButtons(buttons: any, { scope }: any) {
const list = getValue(buttons || ["edit", "delete"], { scope }) as ClTable.OpButton;
return list.map((vnode) => {
if (vnode === "info") {
return (
<el-button
plain
size={style.size}
v-show={crud.getPermission("info")}
onClick={(e: MouseEvent) => {
crud.rowInfo(scope.row);
e.stopPropagation();
}}>
{crud.dict.label?.info}
</el-button>
);
} else if (vnode === "edit") {
return (
<el-button
text
type="primary"
size={style.size}
v-show={crud.getPermission("update")}
onClick={(e: MouseEvent) => {
crud.rowEdit(scope.row);
e.stopPropagation();
}}>
{crud.dict.label?.update}
</el-button>
);
} else if (vnode === "delete") {
return (
<el-button
text
type="danger"
size={style.size}
v-show={crud.getPermission("delete")}
onClick={(e: MouseEvent) => {
crud.rowDelete(scope.row);
e.stopPropagation();
}}>
{crud.dict.label?.delete}
</el-button>
);
} else {
if (typeof vnode === "object") {
if (vnode.hidden) {
return null;
}
}
return renderNode(vnode, {
scope,
slots,
custom(vnode) {
return (
<el-button
text
type={vnode.type}
{...vnode?.props}
onClick={(e: MouseEvent) => {
vnode.onClick({ scope });
e.stopPropagation();
}}>
{vnode.label}
</el-button>
);
}
});
}
});
}
// 渲染字典
function renderDict(value: any, item: ClTable.Column) {
// 选项列表
const list = cloneDeep(item.dict || []) as DictOptions;
// 字符串分隔符
const separator = item.dictSeparator === undefined ? "," : item.dictSeparator;
// 设置颜色
if (item.dictColor) {
list.forEach((e, i) => {
if (!e.color) {
e.color = style.colors[i];
}
});
}
// 绑定值
let values: any[] = [];
// 格式化值
if (isArray(value)) {
values = value;
} else if (isString(value)) {
if (separator) {
values = value.split(separator);
} else {
values = [value];
}
} else {
values = [value];
}
// 返回值
const result = values
.filter((e) => e !== undefined && e !== null && e !== "")
.map((v) => {
const d = deepFind(v, list, { allLevels: item.dictAllLevels }) || {
label: v,
value: v
};
return {
...d,
children: []
};
});
// 格式化返回
if (item.dictFormatter) {
return item.dictFormatter(result);
} else {
// tag 返回
return result.map((e) => {
return h(
<el-tag disable-transitions style="margin: 2px; border: 0" />,
{
type: e.type,
closable: e.closable,
hit: e.hit,
color: e.color,
size: e.size,
effect: e.effect || "dark",
round: e.round
},
{
default: () => <span>{e.label}</span>
}
);
});
}
}
// 插槽 empty
function renderEmpty(emptyText: string) {
return (
<div class="cl-table__empty">
{slots.empty ? (
slots.empty()
) : (
<el-empty image-size={100} description={emptyText}></el-empty>
)}
</div>
);
}
// 插槽 append
function renderAppend() {
return <div class="cl-table__append">{slots.append && slots.append()}</div>;
}
return {
renderColumn,
renderEmpty,
renderAppend
};
}

View File

@@ -0,0 +1,130 @@
import { isEmpty, isFunction } from "lodash-es";
import { useCore } from "../../../hooks";
import { ContextMenu } from "../../context-menu";
// 单元行事件
export function useRow({
Table,
config,
Sort
}: {
Table: Vue.Ref<any>;
config: ClTable.Config;
Sort: {
defaultSort: {
prop?: string;
order?: string;
};
changeSort(prop: string, order: string): void;
};
}) {
const { crud } = useCore();
// 右键菜单
function onRowContextMenu(row: obj, column: obj, event: PointerEvent) {
// 菜单按钮
const buttons = config.contextMenu;
// 是否开启
const enable = !isEmpty(buttons);
if (enable) {
// 高亮
Table.value.setCurrentRow(row);
// 解析按钮
const list = buttons
.map((e) => {
switch (e) {
case "refresh":
return {
label: crud.dict.label.refresh,
callback(done: fn) {
crud.refresh();
done();
}
};
case "edit":
case "update":
return {
label: crud.dict.label.update,
hidden: !crud.getPermission("update"),
callback(done: fn) {
crud.rowEdit(row);
done();
}
};
case "delete":
return {
label: crud.dict.label.delete,
hidden: !crud.getPermission("delete"),
callback(done: fn) {
crud.rowDelete(row);
done();
}
};
case "info":
return {
label: crud.dict.label.info,
hidden: !crud.getPermission("info"),
callback(done: fn) {
crud.rowInfo(row);
done();
}
};
case "check":
return {
label: crud.selection.find((e) => e.id == row.id)
? crud.dict.label.deselect
: crud.dict.label.select,
hidden: !config.columns.find((e) => e.type === "selection"),
callback(done: fn) {
Table.value.toggleRowSelection(row);
done();
}
};
case "order-desc":
return {
label: `${column.label} - ${crud.dict.label.desc}`,
hidden: !column.sortable,
callback(done: fn) {
Sort.changeSort(column.property, "desc");
done();
}
};
case "order-asc":
return {
label: `${column.label} - ${crud.dict.label.asc}`,
hidden: !column.sortable,
callback(done: fn) {
Sort.changeSort(column.property, "asc");
done();
}
};
default:
if (isFunction(e)) {
return e(row, column, event);
} else {
return e;
}
}
})
.filter((e) => Boolean(e) && !e.hidden);
// 打开菜单
if (!isEmpty(list)) {
ContextMenu.open(event, {
list
});
}
}
// 回调
if (config.onRowContextmenu) {
config.onRowContextmenu(row, column, event);
}
}
return {
onRowContextMenu
};
}

View File

@@ -0,0 +1,16 @@
import { useCore } from "../../../hooks";
export function useSelection({ emit }: { emit: Vue.Emit }) {
const { crud } = useCore();
// 选择项发生变化
function onSelectionChange(selection: any[]) {
crud.selection.splice(0, crud.selection.length, ...selection);
emit("selection-change", crud.selection);
}
return {
selection: crud.selection,
onSelectionChange
};
}

View File

@@ -0,0 +1,86 @@
import { useCore } from "../../../hooks";
// 排序
export function useSort({
config,
Table,
emit
}: {
config: ClTable.Config;
Table: Vue.Ref<any>;
emit: Vue.Emit;
}) {
const { crud } = useCore();
// 设置默认排序Ï
const defaultSort = (function () {
let { prop, order } = config.defaultSort || {};
const item = config.columns.find((e) =>
["desc", "asc", "descending", "ascending"].find((a) => a == e.sortable)
);
if (item) {
prop = item.prop;
order = ["descending", "desc"].find((a) => a == item.sortable)
? "descending"
: "ascending";
}
if (order && prop) {
crud.params.order = ["descending", "desc"].includes(order) ? "desc" : "asc";
crud.params.prop = prop;
return {
prop,
order
};
}
return {};
})();
// 排序监听
function onSortChange({ prop, order }: { prop: string | undefined; order: string }) {
if (config.sortRefresh) {
if (order === "descending") {
order = "desc";
}
if (order === "ascending") {
order = "asc";
}
if (!order) {
prop = undefined;
}
crud.refresh({
prop,
order,
page: 1
});
}
emit("sort-change", { prop, order });
}
// 改变排序
function changeSort(prop: string, order: string) {
if (order === "desc") {
order = "descending";
}
if (order === "asc") {
order = "ascending";
}
Table.value?.sort(prop, order);
}
return {
defaultSort,
onSortChange,
changeSort
};
}

View File

@@ -0,0 +1,165 @@
import { defineComponent, h } from "vue";
import {
useData,
useHeight,
useOp,
useRender,
useRow,
useSelection,
useSort,
useTable
} from "./helper";
import { useConfig, useCore, useElApi, useProxy } from "../../hooks";
import { usePlugins } from "./helper/plugins";
export default defineComponent({
name: "cl-table",
props: {
// 列配置
columns: {
type: Array,
default: () => []
},
// 是否自动计算高度
autoHeight: {
type: Boolean,
default: null
},
// 固定高度
height: null,
// 右键菜单
contextMenu: {
type: [Array, Boolean],
default: null
},
// 默认排序
defaultSort: Object,
// 排序后是否刷新
sortRefresh: {
type: Boolean,
default: true
},
// 空数据显示文案
emptyText: String,
// 当前行的 key
rowKey: {
type: String,
default: "id"
}
},
emits: ["selection-change", "sort-change"],
setup(props, { emit, expose }) {
const { crud } = useCore();
const { style } = useConfig();
const { Table, config } = useTable(props);
const plugin = usePlugins();
// 排序
const Sort = useSort({ config, emit, Table });
// 行
const Row = useRow({
config,
Table,
Sort
});
// 高度
const Height = useHeight({ config, Table });
// 数据
const Data = useData({ config, Table });
// 多选
const Selection = useSelection({ emit });
// 操作
const Op = useOp({ config });
// 方法
const ElTableApi = useElApi(
[
"clearSelection",
"getSelectionRows",
"toggleRowSelection",
"toggleAllSelection",
"toggleRowExpansion",
"setCurrentRow",
"clearSort",
"clearFilter",
"doLayout",
"sort",
"scrollTo",
"setScrollTop",
"setScrollLeft",
"updateKeyChildren"
],
Table
);
const ctx = {
Table,
config,
columns: config.columns,
...Selection,
...Data,
...Sort,
...Row,
...Height,
...Op,
...ElTableApi
};
useProxy(ctx);
expose(ctx);
plugin.create(config.plugins);
return () => {
const { renderColumn, renderAppend, renderEmpty } = useRender();
return (
ctx.visible.value &&
h(
<el-table class="cl-table" ref={Table} v-loading={crud.loading} />,
{
...config.on,
...config.props,
// config
maxHeight: config.autoHeight ? ctx.maxHeight.value : null,
height: config.autoHeight ? config.height : null,
rowKey: config.rowKey,
// ctx
defaultSort: ctx.defaultSort,
data: ctx.data.value,
onRowContextmenu: ctx.onRowContextMenu,
onSelectionChange: ctx.onSelectionChange,
onSortChange: ctx.onSortChange,
// style
size: style.size,
border: style.table.border,
highlightCurrentRow: style.table.highlightCurrentRow,
resizable: style.table.resizable,
stripe: style.table.stripe
},
{
default() {
return renderColumn(ctx.columns);
},
empty() {
return renderEmpty(config.emptyText || crud.dict.label.empty);
},
append() {
return renderAppend();
}
}
)
);
};
}
});

View File

@@ -0,0 +1,306 @@
import { defineComponent, h, inject, reactive, ref, toRefs } from "vue";
import { ElMessage } from "element-plus";
import { useCore, useProxy } from "../../hooks";
import { useApi } from "../form/helper";
import { mergeConfig } from "../../utils";
export default defineComponent({
name: "cl-upsert",
props: {
// 表单项
items: {
type: Array,
default: () => []
},
// <el-form /> 参数
props: Object,
// 编辑时是否同步打开
sync: Boolean,
// 操作按钮参数
op: Object,
// <cl-dialog /> 参数
dialog: Object,
// 打开表单钩子
onOpen: Function,
// 打开表单后钩子
onOpened: Function,
// 关闭表单钩子
onClose: Function,
// 关闭表单后钩子
onClosed: Function,
// 获取表单数据钩子
onInfo: Function,
// 表单提交钩子
onSubmit: Function
},
emits: ["opened", "closed"],
setup(props, { slots, expose }) {
const { crud } = useCore();
const config = reactive<ClUpsert.Config>(
mergeConfig(props, inject("useUpsert__options") || {})
);
// el-form
const Form = ref<ClForm.Ref>();
// 模式
const mode = ref<ClUpsert.Ref["mode"]>("info");
// 关闭表单
function close(action?: ClForm.CloseAction) {
Form.value?.close(action);
}
// 关闭后
function onClosed() {
Form.value?.hideLoading();
if (config.onClosed) {
config.onClosed();
}
}
// 关闭前
function beforeClose(action: ClForm.CloseAction, done: fn) {
function next() {
done();
onClosed();
}
if (config.onClose) {
config.onClose(action, next);
} else {
next();
}
}
// 提交
function submit(data: obj) {
const { service, dict, refresh } = crud;
function done() {
Form.value?.done();
}
function next(data: obj) {
return new Promise((resolve, reject) => {
// 发送请求
service[dict.api[mode.value]](data)
.then((res) => {
ElMessage.success(dict.label.saveSuccess);
done();
close("save");
refresh();
resolve(res);
})
.catch((err) => {
ElMessage.error(err.message);
done();
reject(err);
});
});
}
// 提交钩子
if (config.onSubmit) {
config.onSubmit(data, {
done,
next,
close() {
close("save");
}
});
} else {
next(data);
}
}
// 打开表单
function open() {
// 是否禁用
const isDisabled = mode.value == "info";
return new Promise((resolve) => {
if (!Form.value) {
return console.error("<cl-upsert /> is not found");
}
Form.value?.open(
{
title: crud.dict.label[mode.value],
props: {
...config.props,
disabled: isDisabled
},
op: {
...config.op,
hidden: isDisabled
},
dialog: config.dialog,
items: config.items || [],
on: {
open() {
if (config.onOpen) {
config.onOpen();
}
resolve(true);
},
submit,
close: beforeClose
},
form: {},
_data: {
isDisabled
}
},
config.plugins
);
});
}
// 打开后事件
function onOpened() {
const data = Form.value?.getForm();
if (config.onOpened) {
config.onOpened(data);
}
}
// 新增
async function add() {
mode.value = "add";
// 打开中
await open();
// 打开后
onOpened();
}
// 追加
async function append(data: any) {
mode.value = "add";
// 打开中
await open();
// 绑定值
if (data) {
Form.value?.bindForm(data);
}
// 打开后
onOpened();
}
// 编辑
function edit(data?: any) {
mode.value = "update";
getInfo(data);
}
// 详情
function info(data?: any) {
mode.value = "info";
getInfo(data);
}
// 信息
function getInfo(data: any) {
// 显示加载中
Form.value?.showLoading();
// 是否同步打开
if (!config.sync) {
open();
}
// 完成
async function done(data?: any) {
// 加载完成
Form.value?.hideLoading();
// 合并数据
if (data) {
Form.value?.bindForm(data);
}
// 同步打开表单
if (config.sync) {
await open();
}
onOpened();
}
// 获取详情
function next(data: any): Promise<any> {
return new Promise(async (resolve, reject) => {
// 发送请求
await crud.service[crud.dict.api.info]({
[crud.dict.primaryId]: data[crud.dict.primaryId]
})
.then((res) => {
done(res);
resolve(res);
})
.catch((err) => {
ElMessage.error(err.message);
reject(err);
});
// 隐藏加载框
Form.value?.hideLoading();
});
}
// 详情钩子
if (config.onInfo) {
config.onInfo(data, {
close,
next,
done
});
} else {
next(data);
}
}
// 完成
function done() {
Form.value?.hideLoading();
}
const ctx = {
config,
...toRefs(config),
...useApi({ Form }),
Form,
get form() {
return Form.value?.form || {};
},
mode,
add,
append,
edit,
info,
open,
close,
done,
submit
};
useProxy(ctx);
expose(ctx);
return () => {
return <div class="cl-upsert">{h(<cl-form ref={Form} />, {}, slots)}</div>;
};
}
});

View File

@@ -0,0 +1,27 @@
export const crudList: ClCrud.Ref[] = [];
export const emitter: Emitter = {
list: [],
init(events) {
for (const i in events) {
this.on(i, events[i]);
}
},
emit(name, data) {
this.list.forEach((e: EmitterItem) => {
const [_name] = e.name.split("-");
if (name == _name) {
e.callback(data, {
crudList,
refresh(params) {
crudList.forEach((c) => c.refresh(params));
}
});
}
});
},
on(name, callback) {
this.list.push({ name, callback });
}
};

View File

@@ -0,0 +1,30 @@
import { type App } from "vue";
import { useComponent } from "./components";
import { useProvide } from "./provide";
import global from "./utils/global";
import "./static/index.scss";
const Crud = {
install(app: App, options?: any) {
global.set("__CrudApp__", app);
// 穿透值
useProvide(app, options);
// 设置组件
useComponent(app);
return {
name: "cl-crud"
};
}
};
export { Crud };
export * from "./emitter";
export * from "./hooks";
export * from "./locale";
export { registerFormHook } from "./utils/form-hook";
export { renderNode } from "./utils/vnode";
export { ContextMenu } from "./components/context-menu";

View File

@@ -0,0 +1,191 @@
import { assign } from "lodash-es";
import { TestService } from "../test/service";
import { getCurrentInstance, inject, nextTick, provide, ref, type Ref, watch } from "vue";
// 获取上级
function useParent(name: string, r: Ref) {
const d = getCurrentInstance();
if (d) {
let parent = d.proxy?.$.parent;
if (parent) {
while (parent && parent.type?.name != name && parent.type?.name != "cl-crud") {
parent = parent?.parent;
}
if (parent) {
if (parent.type.name == name) {
r.value = parent.exposed;
}
}
}
}
}
// 多事件
function useEvent(
names: string[],
{ r, options, clear, isChild }: { r: any; options: any; clear?: string; isChild?: boolean }
) {
if (!r.__ev) r.__ev = {};
const d: { [key: string]: (args: any[]) => void } = {};
const ev = r.__ev as { [key: string]: { fn: any; isChild?: boolean }[] };
names.forEach((k) => {
if (!ev[k]) ev[k] = [];
if (options[k]) {
ev[k].push({
fn: options[k],
isChild
});
}
d[k] = (...args: any[]) => {
ev[k].forEach((e) => {
if (e.fn) {
e.fn(...args);
}
});
if (clear == k) {
for (const i in ev) {
ev[i] = ev[i].filter((e) => !e.isChild);
}
}
};
});
return d;
}
// crud
export function useCrud(options?: ClCrud.Options, cb?: (app: ClCrud.Ref) => void) {
const Crud = ref<ClCrud.Ref>();
useParent("cl-crud", Crud);
if (options) {
// 测试模式
if (options.service == "test") {
options.service = new TestService();
}
provide("useCrud__options", options);
}
watch(Crud, (val) => {
if (val) {
if (cb) {
cb(val);
}
}
});
return Crud;
}
// 新增、编辑
export function useUpsert<T = any>(options?: ClUpsert.Options<T>) {
const Upsert = ref<ClUpsert.Ref>();
useParent("cl-upsert", Upsert);
const isChild = !!Upsert.value;
if (options) {
provide("useUpsert__options", options);
}
watch(
Upsert,
(val) => {
if (val) {
if (options) {
const event = useEvent(["onOpen", "onOpened", "onClosed"], {
r: val,
options,
clear: "onClosed",
isChild
});
assign(val.config, event);
}
}
},
{
immediate: true
}
);
return Upsert;
}
// 表格
export function useTable<T = any>(options?: ClTable.Options<T>, cb?: (table: ClTable.Ref) => void) {
const Table = ref<ClTable.Ref<T>>();
useParent("cl-table", Table);
if (options) {
provide("useTable__options", options);
}
watch(Table, (val) => {
if (val) {
if (cb) {
cb(val);
}
}
});
return Table;
}
// 表单
export function useForm<T = any>(cb?: (app: ClForm.Ref<T>) => void) {
const Form = ref<ClForm.Ref<T>>();
useParent("cl-form", Form);
nextTick(() => {
if (cb && Form.value) {
cb(Form.value);
}
});
return Form;
}
// 高级搜索
export function useAdvSearch<T = any>(options?: ClAdvSearch.Options<T>) {
const AdvSearch = ref<ClAdvSearch.Ref<T>>();
useParent("cl-adv-search", AdvSearch);
if (options) {
provide("useAdvSearch__options", options);
}
return AdvSearch;
}
// 搜索
export function useSearch<T = any>(options?: ClSearch.Options<T>) {
const Search = ref<ClSearch.Ref<T>>();
useParent("cl-search", Search);
provide("useSearch__options", options);
return Search;
}
// 对话框
export function useDialog(options?: { onFullscreen(visible: boolean): void }) {
const Dialog = inject("dialog") as ClDialog.Provide;
watch(
() => Dialog?.fullscreen.value,
(val) => {
options?.onFullscreen(val);
}
);
return Dialog;
}

View File

@@ -0,0 +1,74 @@
import { Mitt } from "../utils/mitt";
import { isFunction } from "lodash-es";
import { getCurrentInstance, inject, reactive } from "vue";
export function useCore() {
const crud = inject("crud") as ClCrud.Ref;
const mitt = inject("mitt") as Mitt;
return {
crud,
mitt
};
}
export function useConfig() {
return inject("__config__") as Config;
}
export function useBrowser() {
return inject("__browser__") as Browser;
}
export function useRefs() {
const refs = reactive<{ [key: string]: obj }>({});
function setRefs(name: string) {
return (el: any) => {
refs[name] = el;
};
}
return { refs, setRefs };
}
export function useProxy(ctx: any) {
const { type }: any = getCurrentInstance();
const { mitt, crud } = useCore();
// 挂载
crud[type.name] = ctx;
// 事件
mitt.on("crud.proxy", ({ name, data = [], callback }: any) => {
if (ctx[name]) {
let d = null;
if (isFunction(ctx[name])) {
d = ctx[name](...data);
} else {
d = ctx[name];
}
if (callback) {
callback(d);
}
}
});
return ctx;
}
export function useElApi(keys: string[], el: any) {
const apis: obj = {};
keys.forEach((e) => {
apis[e] = (...args: any[]) => {
return el.value[e](...args);
};
});
return apis;
}
export * from "./crud";

View File

@@ -0,0 +1 @@
export * from "./entry";

View File

@@ -0,0 +1,33 @@
export default {
op: "Operation",
add: "Add",
delete: "Delete",
multiDelete: "Delete",
update: "Edit",
refresh: "Refresh",
info: "Details",
search: "Search",
reset: "Reset",
clear: "Clear",
save: "Save",
close: "Cancel",
confirm: "Confirm",
advSearch: "Advanced Search",
searchKey: "Search Keyword",
placeholder: "Please enter",
tips: "Tips",
saveSuccess: "Save successful",
deleteSuccess: "Delete successful",
deleteConfirm:
"This operation will permanently delete the selected data. Do you want to continue?",
empty: "No data available",
desc: "Descending",
asc: "Ascending",
select: "Select",
deselect: "Deselect",
seeMore: "See more",
hideContent: "Hide content",
nonEmpty: "{label} cannot be empty",
collapse: "Collapse",
expand: "Expand"
};

View File

@@ -0,0 +1,11 @@
import en from "./en";
import ja from "./ja";
import zhCn from "./zh-cn";
import zhTw from "./zh-tw";
export const locale = {
en,
ja,
"zh-cn": zhCn,
"zh-tw": zhTw
};

View File

@@ -0,0 +1,32 @@
export default {
op: "操作",
add: "追加",
delete: "削除",
multiDelete: "削除",
update: "編集",
refresh: "リフレッシュ",
info: "詳細",
search: "検索",
reset: "リセット",
clear: "クリア",
save: "保存",
close: "キャンセル",
confirm: "確認",
advSearch: "高度な検索",
searchKey: "検索キーワード",
placeholder: "入力してください",
tips: "ヒント",
saveSuccess: "保存が成功しました",
deleteSuccess: "削除が成功しました",
deleteConfirm: "この操作は選択したデータを永久に削除します。続行しますか?",
empty: "データがありません",
desc: "降順",
asc: "昇順",
select: "選択",
deselect: "選択解除",
seeMore: "詳細を表示",
hideContent: "コンテンツを非表示",
nonEmpty: "{label}は空にできません",
collapse: "折り畳む",
expand: "展開"
};

View File

@@ -0,0 +1,32 @@
export default {
op: "操作",
add: "新增",
delete: "删除",
multiDelete: "删除",
update: "编辑",
refresh: "刷新",
info: "详情",
search: "搜索",
reset: "重置",
clear: "清空",
save: "保存",
close: "取消",
confirm: "确定",
advSearch: "高级搜索",
searchKey: "搜索关键字",
placeholder: "请输入",
tips: "提示",
saveSuccess: "保存成功",
deleteSuccess: "删除成功",
deleteConfirm: "此操作将永久删除选中数据,是否继续?",
empty: "暂无数据",
desc: "降序",
asc: "升序",
select: "选择",
deselect: "取消选择",
seeMore: "查看更多",
hideContent: "隐藏内容",
nonEmpty: "{label}不能为空",
collapse: "收起",
expand: "展开更多"
};

View File

@@ -0,0 +1,32 @@
export default {
op: "操作",
add: "新增",
delete: "刪除",
multiDelete: "刪除",
update: "編輯",
refresh: "刷新",
info: "詳情",
search: "搜尋",
reset: "重置",
clear: "清空",
save: "保存",
close: "取消",
confirm: "確定",
advSearch: "高級搜索",
searchKey: "搜索關鍵字",
placeholder: "請輸入",
tips: "提示",
saveSuccess: "保存成功",
deleteSuccess: "刪除成功",
deleteConfirm: "此操作將永久刪除選中數據,是否繼續?",
empty: "暫無數據",
desc: "降序",
asc: "升序",
select: "選擇",
deselect: "取消選擇",
seeMore: "查看更多",
hideContent: "隱藏內容",
nonEmpty: "{label}不能為空",
collapse: "收起",
expand: "展開"
};

View File

@@ -0,0 +1,27 @@
import { createApp } from "vue";
import App from "./App.vue";
import { Crud, locale } from "./entry";
// import Crud, { locale } from "../dist/index.umd";
// import "../dist/index.css";
import ElementPlus from "element-plus";
import "element-plus/dist/index.css";
const app = createApp(App);
app.use(ElementPlus)
.use(Crud, {
dict: {
sort: {
prop: "order",
order: "sort"
},
label: locale["zh-cn"]
},
style: {
// size: "default"
},
render: {
autoHeight: true
}
})
.mount("#app");

View File

@@ -0,0 +1,129 @@
import { type App, reactive } from "vue";
import { mitt } from "./utils/mitt";
import { emitter } from "./emitter";
import { locale } from "./locale";
import { merge } from "./utils";
// 设置配置
function setConfig(app: App, options: Options = {}) {
const config = merge(
{
permission: {
update: true,
page: true,
info: true,
list: true,
add: true,
delete: true
},
dict: {
primaryId: "id",
api: {
list: "list",
add: "add",
update: "update",
delete: "delete",
info: "info",
page: "page"
},
pagination: {
page: "page",
size: "size"
},
search: {
keyWord: "keyWord",
query: "query"
},
sort: {
order: "order",
prop: "prop"
},
label: locale["zh-cn"]
},
style: {
colors: [
"#d42ca8",
"#1c109d",
"#6d17c3",
"#6dc9f1",
"#04c273",
"#06b31c",
"#f9f494",
"#aa7a24",
"#d57121",
"#e93f4d"
],
form: {
labelPostion: "right",
labelWidth: "100px",
span: 24
},
table: {
border: true,
highlightCurrentRow: true,
autoHeight: true,
contextMenu: ["refresh", "check", "edit", "delete", "order-asc", "order-desc"],
column: {
align: "center",
opWidth: 180
}
}
},
events: {}
} as Options,
options
);
// 初始化事件
if (config.events) {
emitter.init(config.events);
}
app.provide("__config__", config);
return config;
}
// 设置浏览器
function setBrowser(app: App) {
// 浏览器信息
const browser = reactive({
isMini: false,
screen: "full"
});
// 更新信息
function update() {
const w = document.body.clientWidth;
if (w < 768) {
browser.screen = "xs";
} else if (w < 992) {
browser.screen = "sm";
} else if (w < 1200) {
browser.screen = "md";
} else if (w < 1920) {
browser.screen = "xl";
} else {
browser.screen = "full";
}
browser.isMini = browser.screen === "xs";
}
// 监听浏览器窗口变化
window.addEventListener("resize", () => {
update();
// 事件
mitt.emit("resize");
});
update();
app.provide("__browser__", browser);
}
export function useProvide(app: App, options: Options = {}) {
setBrowser(app);
setConfig(app, options);
}

View File

@@ -0,0 +1,862 @@
.cl-crud {
height: 100%;
overflow: hidden auto;
position: relative;
box-sizing: border-box;
background-color: #fff;
&.is-border {
border: 1px solid var(--el-border-color);
border-radius: var(--el-border-radius-base);
}
& > .cl-row {
width: 100%;
&:not(.cl-row--last) > * {
margin: 0 10px 10px 0;
&:last-child {
margin-right: 0;
}
}
.cl-flex1 {
margin-right: 0;
}
}
}
.cl-flex1 {
flex: 1;
font-size: 12px;
}
.cl-search-key {
display: inline-flex;
&__select {
margin-right: 10px;
&.el-select {
width: 100px;
}
}
&__wrap {
display: inline-flex;
.el-input {
flex: 1;
}
.el-button {
margin-left: 10px;
}
}
}
.cl-table {
width: 100%;
.el-table {
&.el-loading-parent--relative {
box-sizing: border-box;
}
&__header {
.el-table__cell {
background-color: var(--el-fill-color-lighter) !important;
color: var(--el-text-color-primary);
.cell {
line-height: normal;
}
&.is-sortable {
.cl-table-header__search {
width: auto;
&.is-input {
width: calc(100% - 24px) !important;
}
}
}
}
}
&__empty-block {
height: auto !important;
}
}
.el-loading-mask {
.el-loading-spinner {
.el-icon-loading {
font-size: 25px;
color: #000;
}
.el-loading-text {
color: var(--el-text-color-secondary);
margin-top: 5px;
}
}
}
&__op {
margin-bottom: -5px;
.el-button {
margin-bottom: 5px;
outline-offset: -2px !important;
&.is-text {
border: 1px solid var(--el-button-border-color) !important;
&:hover {
background-color: var(--el-button-bg-color) !important;
}
}
}
}
.cl-table-header__search {
display: inline-flex;
align-items: center;
gap: 5px;
width: 100%;
cursor: pointer;
&-label {
display: flex;
align-items: center;
justify-content: center;
gap: 5px;
}
&:hover {
color: var(--el-color-primary);
}
&-inner {
flex: 1;
& > div {
width: 100%;
.el-date-editor {
margin-right: 0;
}
}
}
&-close {
font-size: 14px;
height: 30px;
width: 30px;
border: 1px solid var(--el-border-color);
border-radius: 6px;
background-color: var(--el-fill-color-blank);
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
color: var(--el-text-color-secondary);
&:hover {
border-color: var(--el-border-color-hover);
color: var(--el-text-color-primary);
}
}
}
.is-left {
.cl-table-header__search-label {
justify-content: flex-start;
}
}
.is-right {
.cl-table-header__search-label {
justify-content: flex-end;
}
}
}
.cl-pagination {
&.el-pagination {
--el-pagination-border-radius: var(--el-border-radius-base);
}
.btn-prev,
.btn-next,
.el-pager li {
border-radius: var(--el-border-radius-base);
}
}
.cl-filter {
display: flex;
align-items: center;
margin: 0 10px;
&__label {
font-size: 12px;
margin-right: 10px;
white-space: nowrap;
}
.el-select {
min-width: 120px;
}
}
.cl-search {
&__btns {
margin-left: 5px;
}
&__more {
display: flex;
align-items: center;
justify-content: space-between;
.el-form-item {
margin-bottom: 0;
}
}
.el-form:not(.el-form--label-top) {
.el-form-item__label {
&:empty {
display: none;
}
}
.cl-search__btns {
.el-form-item__label {
display: none;
}
}
}
.cl-search__btns {
.el-button + .el-button {
margin-left: 10px;
}
}
&.is-inline {
margin-bottom: 0 !important;
}
&.is-collapse {
background-color: var(--el-fill-color-lighter);
padding: 10px;
margin-bottom: 10px !important;
border-radius: 6px;
border: 1px solid var(--el-border-color-extra-light);
&.is-fold {
.cl-search__form {
max-height: 42px;
overflow: hidden;
&:has(.el-form--label-top) {
max-height: 68px;
}
}
}
}
}
.cl-adv-btn {
margin-left: 10px;
.el-icon {
margin-right: 5px;
}
}
.cl-adv-search {
&.el-drawer {
background-color: #fff;
}
.el-drawer__body {
padding: 0;
}
&__header {
display: flex;
align-items: center;
justify-content: space-between;
height: 50px;
padding: 0 15px 0 20px;
user-select: none;
.text {
font-size: 16px;
}
.el-icon {
cursor: pointer;
&:hover {
color: red;
}
}
}
&__container {
height: calc(100% - 110px);
overflow-y: auto;
padding: 10px 20px;
box-sizing: border-box;
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-thumb {
border-radius: 6px;
background-color: rgba(144, 147, 153, 0.3);
}
.el-form-item__content {
& > div {
width: 100%;
}
}
}
&__footer {
display: flex;
align-items: center;
justify-content: flex-end;
height: 60px;
border-top: 1px solid var(--el-border-color-extra-light);
padding: 0 10px;
box-sizing: border-box;
}
}
.cl-form {
[class*="el-col-"].is-guttered {
min-height: 0;
}
&-item {
display: flex;
&__component {
display: flex;
&.flex1 {
flex: 1;
width: 100%;
& > div {
width: 100%;
}
}
}
&__prepend {
margin-right: 10px;
}
&__append {
margin-left: 10px;
}
&__collapse {
width: 100%;
font-size: 12px;
cursor: pointer;
.el-divider {
margin: 16px 0;
&__text {
font-size: 12px;
}
}
i {
margin-left: 6px;
}
}
.el-table__header tr {
line-height: normal;
}
}
&__footer {
display: flex;
justify-content: flex-end;
}
.cl-crud {
line-height: normal;
}
.el-form {
.el-form-item {
.el-form-item {
margin-bottom: 18px;
}
.el-input-number {
&__decrease,
&__increase {
border: 0;
background-color: transparent;
}
}
&__label {
.el-tooltip {
i {
margin-left: 5px;
}
}
}
&__content {
min-width: 0px;
& > div {
width: 100%;
}
}
}
&:not(.el-form--label-top) {
.el-form-item {
&.no-label {
& > .el-form-item__label {
padding: 0;
display: none;
}
}
}
}
&.el-form--label-top {
.el-form-item {
margin-bottom: 22px;
}
.el-form-item__label {
margin: 0 0 4px 0;
min-height: 22px;
}
.el-form-item__error {
padding-top: 4px;
}
}
&.el-form--inline {
.cl-form__items {
display: flex;
flex-wrap: wrap;
}
.el-form-item {
margin: 0 10px 10px 0;
.el-date-editor {
box-sizing: border-box;
.el-range-input {
&:nth-child(2) {
margin-left: 5px;
}
}
}
.el-select {
width: 173px;
}
&:last-child {
margin-right: 0;
}
}
}
}
}
.cl-form-tabs {
border-bottom: 1px solid var(--el-border-color);
overflow: hidden;
width: calc(100% - 10px);
margin: 0 5px 20px 5px;
&__wrap {
height: 35px;
width: 100%;
overflow-x: auto;
overflow-y: hidden;
scrollbar-width: none;
-ms-overflow-style: none;
position: relative;
&::-webkit-scrollbar {
display: none;
}
}
ul {
display: inline-flex;
white-space: nowrap;
margin: 0;
padding: 0;
li {
display: inline-flex;
align-items: center;
list-style: none;
padding: 0 20px;
height: 35px;
cursor: pointer;
.el-icon {
margin-right: 5px;
font-size: 16px;
}
&.is-active {
color: var(--el-color-primary);
}
}
}
&__line {
height: 3px;
width: 100%;
position: absolute;
bottom: -1px;
left: 0;
transition:
transform 0.3s ease-in-out,
width 0.2s 0.1s cubic-bezier(0.645, 0.045, 0.355, 1);
background-color: var(--el-color-primary);
}
&--card {
.cl-form-tabs__line {
display: none;
}
ul {
border: 1px solid var(--el-border-color);
border-top-left-radius: 5px;
border-top-right-radius: 5px;
li {
border-left: 1px solid var(--el-border-color);
&:first-child {
border-left-width: 0;
}
}
}
}
}
.cl-form-card {
margin-top: 0;
&__header {
display: flex;
align-items: center;
justify-content: space-between;
font-size: 14px;
height: 32px;
line-height: normal;
font-weight: bold;
padding: 0 10px;
background-color: var(--el-fill-color-lighter);
border-radius: var(--el-border-radius-base);
cursor: pointer;
user-select: none;
&:hover {
background-color: var(--el-fill-color-light);
}
}
&__container {
transition: all 0.3s;
display: grid;
grid-template-rows: 0fr;
> .cl-form-item__children {
min-height: 0;
overflow: hidden;
.el-row {
margin-top: 10px;
}
}
}
&.is-expand {
> .cl-form-card__container {
grid-template-rows: 1fr;
margin-bottom: -18px;
}
}
.cl-form-card {
margin-left: 10px;
}
}
.cl-dialog {
display: flex;
flex-direction: column;
&.el-dialog {
padding: 0;
border-radius: 8px;
}
.el-dialog {
&__header {
padding: 0;
margin-right: 0;
&-slot {
&.is-drag {
-moz-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
-khtml-user-select: none;
user-select: none;
cursor: move;
}
}
}
&__body {
padding: 0;
box-sizing: border-box;
flex: 1;
overflow: hidden;
}
&__footer {
padding: 0;
}
}
&__header {
position: relative;
padding: 10px;
border-bottom: 1px solid var(--el-border-color-extra-light);
text-align: center;
user-select: none;
}
&__default {
box-sizing: border-box;
}
&__footer {
border-top: 1px solid var(--el-border-color-extra-light);
padding: 20px;
}
&__title {
display: block;
font-size: 15px;
letter-spacing: 0.5px;
}
&__controls {
display: flex;
justify-content: flex-end;
position: absolute;
right: 8px;
top: 8px;
z-index: 9;
width: 100%;
&-icon,
.minimize,
.maximize,
.close {
display: flex;
align-items: center;
justify-content: center;
height: 28px;
width: 28px;
border: 0;
background-color: transparent;
cursor: pointer;
outline: none;
border-radius: 6px;
margin-left: 5px;
i {
font-size: 18px;
}
&:hover {
background-color: var(--el-fill-color-darker);
}
}
}
&.hidden-header {
.el-dialog__header {
display: none;
}
}
&.is-fullscreen {
height: 100vh !important;
border-radius: 0;
overflow: hidden;
.cl-dialog__container {
height: 100% !important;
}
}
&.is-transparent {
background-color: transparent !important;
}
}
.cl-context-menu {
position: absolute;
z-index: 9999;
&__box {
width: 160px;
background-color: var(--el-bg-color);
border: 1px solid var(--el-border-color-extra-light);
box-shadow:
0 4px 6px -1px rgba(0, 0, 0, 0.1),
0 2px 4px -2px rgba(0, 0, 0, 0.1);
border-radius: 6px;
position: absolute;
top: 0;
padding: 5px;
&.is-append {
right: calc(-100% - 5px);
top: -5px;
}
& > div {
display: flex;
align-items: center;
font-size: 12px;
cursor: pointer;
padding: 5px 15px;
color: var(--el-text-color-primary);
position: relative;
border-radius: 4px;
span {
flex: 1;
user-select: none;
}
&:hover {
background-color: var(--el-fill-color-lighter);
}
i {
&:first-child {
margin-right: 5px;
}
&:last-child {
margin-left: 5px;
}
}
&.is-active {
background-color: #f7f7f7;
color: #000;
}
&.is-ellipsis {
span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
&.is-disabled {
span {
color: #ccc;
&:hover {
color: #ccc;
}
}
}
}
}
&__target {
position: relative;
&::after {
content: "";
display: block;
position: absolute;
left: 0;
top: 0;
height: 100%;
width: 100%;
background-color: rgba(0, 0, 0, 0.05);
}
}
}
@media only screen and (max-width: 768px) {
.el-table {
&__body {
&-wrapper {
&::-webkit-scrollbar {
height: 6px;
width: 6px;
}
}
}
}
.cl-search-key {
width: 100%;
margin-right: 0 !important;
&__wrap {
width: 100% !important;
}
}
}
.cl-error-message {
display: flex;
align-items: center;
height: 32px;
padding: 0 10px;
font-size: 14px;
color: var(--el-color-danger);
border: 1px solid var(--el-color-danger);
border-radius: 4px;
box-sizing: border-box;
user-select: none;
}

View File

@@ -0,0 +1,231 @@
import { assign, orderBy } from "lodash-es";
import { uuid } from "../utils";
const userList = [
{
id: 1,
name: "楚行云",
createTime: "1996-09-14",
wages: 73026,
status: 1,
account: "chuxingyun",
occupation: 4,
phone: 13797353874
},
{
id: 2,
name: "秦尘",
createTime: "1977-11-09",
wages: 74520,
status: 0,
account: "qincheng",
occupation: 3,
phone: 18593911044
},
{
id: 3,
name: "叶凡",
createTime: "1982-11-28",
wages: 81420,
status: 0,
account: "yefan",
occupation: 1,
phone: 16234136338
},
{
id: 4,
name: "白小纯",
createTime: "2012-12-17",
wages: 65197,
status: 1,
account: "baixiaochun",
occupation: 2,
phone: 16325661110
},
{
id: 5,
name: "韩立",
createTime: "1982-07-10",
wages: 99107,
status: 1,
account: "hanli",
occupation: 2,
phone: 18486594866
},
{
id: 6,
name: "唐三",
createTime: "2019-07-31",
wages: 80658,
status: 1,
account: "tangsan",
occupation: 5,
phone: 15565014642
},
{
id: 7,
name: "王林",
createTime: "2009-07-26",
wages: 57408,
status: 1,
account: "wanglin",
occupation: 1,
phone: 13852767084
},
{
id: 8,
name: "李强",
createTime: "2016-04-26",
wages: 71782,
status: 1,
account: "liqiang",
occupation: 3,
phone: 18365332834
},
{
id: 9,
name: "秦羽",
createTime: "1984-01-18",
wages: 87860,
status: 1,
account: "qinyu",
occupation: 0,
phone: 18149247129
}
];
class TestService {
// 分页列表
async page(params: any) {
const { keyWord, page, size, sort, order } = params || {};
// 关键字查询
const keyWordLikeFields = ["phone", "name"];
// 等值查询
const fieldEq = ["createTime", "occupation", "status"];
// 模糊查询
const likeFields = ["phone", "name"];
// 过滤后的列表
const list = orderBy(userList, order, sort).filter((e: any) => {
let f = true;
if (keyWord !== undefined) {
f = !!keyWordLikeFields.find((k) => String(e[k]).includes(String(params.keyWord)));
}
fieldEq.forEach((k) => {
if (f) {
if (params[k] !== undefined) {
f = e[k] == params[k];
}
}
});
likeFields.forEach((k) => {
if (f) {
if (params[k] !== undefined) {
f = String(e[k]).includes(String(params[k]));
}
}
});
return f;
});
return new Promise((resolve) => {
// 模拟延迟
setTimeout(() => {
resolve({
list: list.slice((page - 1) * size, page * size),
pagination: {
total: list.length,
page,
size
},
subData: {
wages: list.reduce((a, b) => {
return a + b.wages;
}, 0)
}
});
}, 500);
});
}
// 更新
async update(params: { id: any; [key: string]: any }) {
const item = userList.find((e) => e.id == params.id);
if (item) {
assign(item, params);
}
}
// 新增
async add(params: any) {
const id = uuid();
userList.push({
id,
...params
});
return id;
}
// 详情
async info(params: { id: any }) {
const { id } = params || {};
return userList.find((e) => e.id == id);
}
// 删除
async delete(params: { ids: any[] }) {
const { ids = [] } = params || {};
ids.forEach((id) => {
const index = userList.findIndex((e) => e.id == id);
userList.splice(index, 1);
});
}
// 全部列表
async list() {
return userList;
}
search = {
fieldEq: [
{
propertyName: "occupation",
comment: "工作",
source: "a.occupation"
}
],
fieldLike: [
{
propertyName: "status",
comment: "状态",
dict: ["关闭", "开启"],
source: "a.status"
}
],
keyWordLikeFields: [
{
propertyName: "name",
comment: "姓名",
source: "a.name"
},
{
propertyName: "phone",
comment: "手机号",
source: "a.phone"
}
]
};
}
export { TestService };

View File

@@ -0,0 +1,149 @@
import { isArray, isEmpty, isFunction, isObject, isString } from "lodash-es";
export const format: { [key: string]: ClForm.Hook["Fn"] } = {
number(value) {
return value ? (isArray(value) ? value.map(Number) : Number(value)) : value;
},
string(value) {
return value ? (isArray(value) ? value.map(String) : String(value)) : value;
},
split(value) {
if (isString(value)) {
return value.split(",").filter(Boolean);
} else if (isArray(value)) {
return value;
} else {
return [];
}
},
join(value) {
return isArray(value) ? value.join(",") : value;
},
boolean(value) {
return Boolean(value);
},
booleanNumber(value) {
return value ? 1 : 0;
},
datetimeRange(value, { form, method, prop }) {
const key = prop.charAt(0).toUpperCase() + prop.slice(1);
const start = `start${key}`;
const end = `end${key}`;
if (method == "bind") {
return [form[start], form[end]];
} else {
const [startTime, endTime] = value || [];
form[start] = startTime;
form[end] = endTime;
return undefined;
}
},
splitJoin(value, { method }) {
if (method == "bind") {
return isString(value) ? value.split(",").filter(Boolean) : value;
} else {
return isArray(value) ? value.join(",") : value;
}
},
json(value, { method }) {
if (method == "bind") {
try {
return JSON.parse(value);
} catch (e) {
return {};
}
} else {
return JSON.stringify(value);
}
},
empty(value) {
if (isString(value)) {
return value === "" ? undefined : value;
}
if (isArray(value)) {
return isEmpty(value) ? undefined : value;
}
return value;
}
};
function init({ value, form, prop }: any) {
if (prop) {
const [a, b] = prop.split("-");
if (b) {
form[prop] = form[a] ? form[a][b] : form[a];
} else {
form[prop] = value;
}
}
}
function parse(method: "submit" | "bind", { value, hook: pipe, form, prop }: any) {
init({ value, method, form, prop });
if (!pipe) {
return false;
}
let pipes: any[] = [];
if (isString(pipe)) {
if (format[pipe]) {
pipes = [pipe];
} else {
console.error(`[hook] ${pipe} is not found`);
}
} else if (isArray(pipe)) {
pipes = pipe;
} else if (isObject(pipe)) {
pipes = isArray(pipe[method]) ? pipe[method] : [pipe[method]];
} else if (isFunction(pipe)) {
pipes = [pipe];
} else {
console.error(`[hook] ${pipe} format error`);
}
let v = value;
pipes.forEach((e) => {
let f: any = null;
if (isString(e)) {
f = format[e];
} else if (isFunction(e)) {
f = e;
}
if (f) {
v = f(v, {
method,
form,
prop
});
}
});
if (prop) {
form[prop] = v;
}
}
const formHook = {
bind(data: any) {
parse("bind", data);
},
submit(data: any) {
parse("submit", data);
}
};
export function registerFormHook(name: string, fn: ClForm.Hook["Fn"]) {
format[name] = fn;
}
export default formHook;

View File

@@ -0,0 +1,16 @@
// @ts-nocheck
import { type App } from "vue";
export default {
get vue(): App {
return window.__CrudApp__;
},
get(key: string) {
return window[key];
},
set(key: string, value: any) {
window[key] = value;
}
};

View File

@@ -0,0 +1,164 @@
import { isRef, mergeProps, toValue } from "vue";
import { assign, flatMap, isArray, isFunction, isNumber, mergeWith } from "lodash-es";
// 是否对象
export function isObject(val: any) {
return val !== null && typeof val === "object";
}
// 解析px
export function parsePx(val: string | number) {
return isNumber(val) ? `${val}px` : val;
}
// 数据设置
export function dataset(obj: any, key: string, value: any): any {
const isGet = value === undefined;
let d = obj;
const arr = flatMap(
key.split(".").map((e) => {
if (e.includes("[")) {
return e.split("[").map((e) => e.replace(/"/g, ""));
} else {
return e;
}
})
);
try {
for (let i = 0; i < arr.length; i++) {
const e: any = arr[i];
let n: any = null;
if (e.includes("]")) {
const [k, v] = e.replace("]", "").split(":");
if (v) {
n = d.findIndex((x: any) => x[k] == v);
} else {
n = Number(k);
}
} else {
n = e;
}
if (i != arr.length - 1) {
d = d[n];
} else {
if (isGet) {
return d[n];
} else {
if (isObject(value)) {
assign(d[n], value);
} else {
d[n] = value;
}
}
}
}
return obj;
} catch (e) {
console.error("[dataset] format error", `${key}`);
return {};
}
}
// 元素是否包含
export function contains(parent: any, node: any) {
return parent !== node && parent && parent.contains(node);
}
// 合并配置
export function mergeConfig(a: any, b?: any): any {
return b ? mergeProps(a, b) : a;
}
// 合并数据
export function merge(d1: any, d2: any) {
return mergeWith(d1, d2, (_, b) => {
if (isArray(b)) {
return b;
}
});
}
// 添加元素
export function addClass(el: Element, name: string) {
if (el?.classList) {
el.classList.add(name);
}
}
// 移除元素
export function removeClass(el: Element, name: string) {
if (el?.classList) {
el.classList.remove(name);
}
}
// 获取值
export function getValue<T = any>(value: T | Vue.Ref<T> | ((d: any) => T), data?: any): T {
if (isRef(value)) {
return toValue(value) as T;
} else {
if (isFunction(value)) {
return value(data);
} else {
return value as T;
}
}
}
// 深度查找
export function deepFind(value: any, list: any[], options?: { allLevels: boolean }) {
const { allLevels = true } = options || {};
function deep(arr: DictOptions, name: string[]): any | undefined {
for (const e of arr) {
if (e.value === value) {
if (allLevels) {
return {
...e,
label: [...name, e.label].join(" / ")
};
} else {
return e;
}
} else if (e.children) {
const d = deep(e.children, [...name!, e.label!]);
if (d !== undefined) {
return d;
}
}
}
return undefined;
}
return deep(list, []);
}
// uuid
export function uuid(separator = "-"): string {
const s: any[] = [];
const hexDigits = "0123456789abcdef";
for (let i = 0; i < 36; i++) {
s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1);
}
s[14] = "4";
s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1);
s[8] = s[13] = s[18] = s[23] = separator;
return s.join("");
}
// 移除相同的方法
export function uniqueFns(fns: any[]) {
const arr = new Map();
fns.forEach((fn) => {
arr.set(fn.name, fn);
});
return Array.from(arr.values());
}

View File

@@ -0,0 +1,30 @@
import _mitt from "mitt";
const mitt = _mitt();
class Mitt {
id: number;
constructor(id?: number) {
this.id = id || 0;
}
send(type: "emit" | "off" | "on", name: string, ...args: any[]) {
// @ts-expect-error
mitt[type](`${this.id}__${name}`, ...args);
}
emit(name: string, ...args: any[]) {
this.send("emit", name, ...args);
}
off(name: string, handler: (...args: any[]) => void) {
this.send("off", name, handler);
}
on(name: string, handler: (...args: any[]) => void) {
this.send("on", name, handler);
}
}
export { Mitt, mitt };

View File

@@ -0,0 +1,52 @@
import { isString } from "lodash-es";
import { getValue, isObject } from ".";
// 解析扩展组件
export function parseExtensionComponent(vnode: Render.Component) {
if (["el-select", "el-radio-group", "el-checkbox-group"].includes(vnode.name!)) {
const list = getValue(vnode.options || []);
const children = (
<div>
{list.map((e, i) => {
let label: any;
let value: any;
if (isString(e)) {
label = value = e;
} else if (isObject(e)) {
label = e.label;
value = e.value;
} else {
return <cl-error-message title={`Component options error`} />;
}
switch (vnode.name) {
case "el-select":
return <el-option key={i} label={label} value={value} {...e.props} />;
case "el-radio-group":
return (
<el-radio key={i} value={value} {...e.props}>
{label}
</el-radio>
);
case "el-checkbox-group":
return (
<el-checkbox key={i} value={value} {...e.props}>
{label}
</el-checkbox>
);
default:
return null;
}
})}
</div>
);
return {
children
};
} else {
return {};
}
}

View File

@@ -0,0 +1,182 @@
import { h, resolveComponent, toRaw, type VNode } from "vue";
import { isObject } from "./index";
import { parseExtensionComponent } from "./parse";
import global from "./global";
import { useConfig } from "../hooks";
import { isFunction, isString } from "lodash-es";
// 配置
interface Options {
// 标识
prop?: string;
// 数据值
scope?: any;
// 当前行
item?: any;
// 插槽
slots?: any;
// 子集
children?: any[] & any;
// 自定义
custom?: (vnode: any) => any;
// 渲染方式
render?: "slot" | null;
// 其他
[key: string]: any;
}
// 临时注册组件列表
const regs: Map<string, any> = new Map();
// 解析节点
export function parseNode(vnode: any, options: Options): VNode {
const { scope, prop, slots, children, _data } = options || {};
// 渲染后组件
let comp: VNode | null = null;
// 插槽模式渲染
if (vnode.name?.includes("slot-")) {
const rn = slots[vnode.name];
if (rn) {
return rn({ scope, prop, ..._data });
} else {
return <cl-error-message title={`${vnode.name} is not found`} />;
}
}
// 实例模式下,先注册到全局,再分解组件渲染
if (vnode.vm && !regs.get(vnode.name)) {
global.vue.component(vnode.name, { ...vnode.vm });
regs.set(vnode.name, { ...vnode.vm });
}
// 处理 props
if (isFunction(vnode.props)) {
vnode.props = vnode.props({ scope, prop, ..._data });
}
// 组件参数
const props = {
...vnode.props,
..._data,
prop,
scope
};
// 是否禁用
props.disabled = _data?.isDisabled || props.disabled;
// 添加双向绑定
if (props && scope) {
if (prop) {
props.modelValue = scope[prop];
props["onUpdate:modelValue"] = function (val: any) {
scope[prop] = val;
};
}
}
// 组件实例渲染
if (vnode.vm) {
comp = h(regs.get(vnode.name), props);
} else {
const slots = {
...vnode.slots
};
if (children) {
slots.default = () => children;
}
// 渲染组件
comp = h(toRaw(resolveComponent(vnode.name)), props, slots);
}
// 挂载到 refs 中
const refBind = vnode.ref || options.ref;
if (isFunction(refBind)) {
setTimeout(() => {
refBind(comp?.component?.exposed);
}, 0);
}
return comp;
}
// 渲染节点
export function renderNode(vnode: any, options: Options) {
const config = useConfig();
const { item, scope, children, _data, render } = options || {};
if (!vnode) {
return null;
}
if (vnode.__v_isVNode) {
return vnode;
}
// 默认参数配置
if (item) {
if (item.component) {
if (!item.component.props) {
item.component.props = {};
}
// 占位符
let placeholder = "";
switch (item.component?.name) {
case "el-input":
placeholder = config.dict.label.placeholder;
break;
}
if (placeholder) {
if (!item.component.props.placeholder) {
item.component.props.placeholder = placeholder;
}
}
}
}
// 组件实例
if (vnode.vm) {
if (!vnode.name) {
vnode.name = vnode.vm?.name || vnode.vm?.__hmrId;
}
return parseNode(vnode, options);
}
// 组件名渲染
if (isString(vnode)) {
if (render == "slot") {
if (!vnode.includes("slot-")) {
return vnode;
}
}
return parseNode({ name: vnode }, options);
}
// 方法回调
if (isFunction(vnode)) {
return vnode({ scope, h, ..._data });
}
// jsx 模式
if (isObject(vnode)) {
if (vnode.name) {
return parseNode(vnode, { ...options, children, ...parseExtensionComponent(vnode) });
} else {
if (options.custom) {
return options.custom(vnode);
}
return <cl-error-message title="Errorcomponent name is required" />;
}
}
}

View File

@@ -0,0 +1,34 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
"strict": false,
"noImplicitAny": false,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve",
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"types": ["./index.d.ts"],
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
},
"declaration": true,
"declarationDir": "types",
"emitDeclarationOnly": true
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
"exclude": ["src/App.vue", "node_modules", "dist"]
}

View File

@@ -0,0 +1,2 @@
declare const _default: import('vue').DefineComponent<{}, () => any, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, true, {}, any>;
export default _default;

View File

@@ -0,0 +1,4 @@
declare const _default: import('vue').DefineComponent<{}, () => any, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {
Search: import('vue').DefineComponent<{}, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').VNodeProps & import('vue').AllowedComponentProps & import('vue').ComponentCustomProps, Readonly<import('vue').ExtractPropTypes<{}>>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, true, {}, any>;
}, {}, string, import('vue').ComponentProvideOptions, true, {}, any>;
export default _default;

View File

@@ -0,0 +1,90 @@
import { PropType } from "vue";
declare const _default: import("vue").DefineComponent<
import("vue").ExtractPropTypes<{
items: {
type: PropType<ClForm.Item[]>;
default: () => any[];
};
title: StringConstructor;
size: {
type: (NumberConstructor | StringConstructor)[];
default: string;
};
op: {
type: ArrayConstructor;
default: () => string[];
};
onSearch: FunctionConstructor;
}>,
() => any,
{},
{},
{},
import("vue").ComponentOptionsMixin,
import("vue").ComponentOptionsMixin,
("clear" | "reset")[],
"clear" | "reset",
import("vue").PublicProps,
Readonly<
import("vue").ExtractPropTypes<{
items: {
type: PropType<ClForm.Item[]>;
default: () => any[];
};
title: StringConstructor;
size: {
type: (NumberConstructor | StringConstructor)[];
default: string;
};
op: {
type: ArrayConstructor;
default: () => string[];
};
onSearch: FunctionConstructor;
}>
> &
Readonly<{
onReset?: (...args: any[]) => any;
onClear?: (...args: any[]) => any;
}>,
{
size: string | number;
items: ClForm.Item<any>[];
op: unknown[];
},
{},
{
Close: import("vue").DefineComponent<
{},
{},
{},
{},
{},
import("vue").ComponentOptionsMixin,
import("vue").ComponentOptionsMixin,
{},
string,
import("vue").VNodeProps &
import("vue").AllowedComponentProps &
import("vue").ComponentCustomProps,
Readonly<import("vue").ExtractPropTypes<{}>>,
{},
{},
{},
{},
string,
import("vue").ComponentProvideOptions,
true,
{},
any
>;
},
{},
string,
import("vue").ComponentProvideOptions,
true,
{},
any
>;
export default _default;

View File

@@ -0,0 +1,55 @@
import { PropType } from "vue";
declare const ClContextMenu: import("vue").DefineComponent<
import("vue").ExtractPropTypes<{
show: BooleanConstructor;
options: {
type: PropType<ClContextMenu.Options>;
default: () => {};
};
event: {
type: ObjectConstructor;
default: () => {};
};
}>,
() => any,
{},
{},
{},
import("vue").ComponentOptionsMixin,
import("vue").ComponentOptionsMixin,
{},
string,
import("vue").PublicProps,
Readonly<
import("vue").ExtractPropTypes<{
show: BooleanConstructor;
options: {
type: PropType<ClContextMenu.Options>;
default: () => {};
};
event: {
type: ObjectConstructor;
default: () => {};
};
}>
> &
Readonly<{}>,
{
options: ClContextMenu.Options;
show: boolean;
event: Record<string, any>;
},
{},
{},
{},
string,
import("vue").ComponentProvideOptions,
true,
{},
any
>;
export declare const ContextMenu: {
open(event: any, options: ClContextMenu.Options): ClContextMenu.Exposed;
};
export default ClContextMenu;

View File

@@ -0,0 +1,24 @@
import { Mitt } from "../../utils/mitt";
interface Options {
mitt: Mitt;
config: ClCrud.Config;
crud: ClCrud.Ref;
}
export declare function useHelper({ config, crud, mitt }: Options): {
proxy: (name: string, data?: any[]) => void;
set: (key: string, value: any) => boolean;
on: (name: string, callback: fn) => void;
rowInfo: (data: any) => void;
rowAdd: () => void;
rowEdit: (data: any) => void;
rowAppend: (data: any) => void;
rowDelete: (...selection: any[]) => void;
rowClose: () => void;
refresh: (params?: obj) => Promise<unknown>;
getPermission: (key: "page" | "list" | "info" | "update" | "add" | "delete") => boolean;
paramsReplace: (params: obj) => any;
getParams: () => obj;
setParams: (data: obj) => void;
};
export {};

View File

@@ -0,0 +1,19 @@
declare const _default: import('vue').DefineComponent<import('vue').ExtractPropTypes<{
name: StringConstructor;
border: BooleanConstructor;
padding: {
type: StringConstructor;
default: string;
};
}>, () => any, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<import('vue').ExtractPropTypes<{
name: StringConstructor;
border: BooleanConstructor;
padding: {
type: StringConstructor;
default: string;
};
}>> & Readonly<{}>, {
border: boolean;
padding: string;
}, {}, {}, {}, string, import('vue').ComponentProvideOptions, true, {}, any>;
export default _default;

View File

@@ -0,0 +1,86 @@
declare const _default: import('vue').DefineComponent<import('vue').ExtractPropTypes<{
modelValue: {
type: BooleanConstructor;
default: boolean;
};
props: ObjectConstructor;
title: {
type: StringConstructor;
default: string;
};
height: StringConstructor;
width: {
type: StringConstructor;
default: string;
};
padding: {
type: StringConstructor;
default: string;
};
keepAlive: BooleanConstructor;
fullscreen: BooleanConstructor;
controls: {
type: ArrayConstructor;
default: () => string[];
};
hideHeader: BooleanConstructor;
beforeClose: FunctionConstructor;
scrollbar: {
type: BooleanConstructor;
default: boolean;
};
transparent: BooleanConstructor;
}>, () => import('vue').VNode<import('vue').RendererNode, import('vue').RendererElement, {
[key: string]: any;
}>, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, ("update:modelValue" | "fullscreen-change")[], "update:modelValue" | "fullscreen-change", import('vue').PublicProps, Readonly<import('vue').ExtractPropTypes<{
modelValue: {
type: BooleanConstructor;
default: boolean;
};
props: ObjectConstructor;
title: {
type: StringConstructor;
default: string;
};
height: StringConstructor;
width: {
type: StringConstructor;
default: string;
};
padding: {
type: StringConstructor;
default: string;
};
keepAlive: BooleanConstructor;
fullscreen: BooleanConstructor;
controls: {
type: ArrayConstructor;
default: () => string[];
};
hideHeader: BooleanConstructor;
beforeClose: FunctionConstructor;
scrollbar: {
type: BooleanConstructor;
default: boolean;
};
transparent: BooleanConstructor;
}>> & Readonly<{
"onUpdate:modelValue"?: (...args: any[]) => any;
"onFullscreen-change"?: (...args: any[]) => any;
}>, {
title: string;
padding: string;
width: string;
hideHeader: boolean;
controls: unknown[];
fullscreen: boolean;
keepAlive: boolean;
modelValue: boolean;
transparent: boolean;
scrollbar: boolean;
}, {}, {
Close: import('vue').DefineComponent<{}, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').VNodeProps & import('vue').AllowedComponentProps & import('vue').ComponentCustomProps, Readonly<import('vue').ExtractPropTypes<{}>>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, true, {}, any>;
FullScreen: import('vue').DefineComponent<{}, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').VNodeProps & import('vue').AllowedComponentProps & import('vue').ComponentCustomProps, Readonly<import('vue').ExtractPropTypes<{}>>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, true, {}, any>;
Minus: import('vue').DefineComponent<{}, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').VNodeProps & import('vue').AllowedComponentProps & import('vue').ComponentCustomProps, Readonly<import('vue').ExtractPropTypes<{}>>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, true, {}, any>;
}, {}, string, import('vue').ComponentProvideOptions, true, {}, any>;
export default _default;

View File

@@ -0,0 +1,6 @@
declare const _default: import('vue').DefineComponent<import('vue').ExtractPropTypes<{
title: StringConstructor;
}>, () => any, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<import('vue').ExtractPropTypes<{
title: StringConstructor;
}>> & Readonly<{}>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, true, {}, any>;
export default _default;

View File

@@ -0,0 +1,6 @@
declare const _default: import('vue').DefineComponent<import('vue').ExtractPropTypes<{
label: StringConstructor;
}>, () => any, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<import('vue').ExtractPropTypes<{
label: StringConstructor;
}>> & Readonly<{}>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, true, {}, any>;
export default _default;

View File

@@ -0,0 +1,2 @@
declare const _default: import('vue').DefineComponent<{}, () => any, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, true, {}, any>;
export default _default;

View File

@@ -0,0 +1,28 @@
declare const _default: import('vue').DefineComponent<import('vue').ExtractPropTypes<{
label: StringConstructor;
expand: {
type: BooleanConstructor;
default: boolean;
};
isExpand: {
type: BooleanConstructor;
default: boolean;
};
}>, () => any, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<import('vue').ExtractPropTypes<{
label: StringConstructor;
expand: {
type: BooleanConstructor;
default: boolean;
};
isExpand: {
type: BooleanConstructor;
default: boolean;
};
}>> & Readonly<{}>, {
expand: boolean;
isExpand: boolean;
}, {}, {
ArrowDown: import('vue').DefineComponent<{}, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').VNodeProps & import('vue').AllowedComponentProps & import('vue').ComponentCustomProps, Readonly<import('vue').ExtractPropTypes<{}>>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, true, {}, any>;
ArrowUp: import('vue').DefineComponent<{}, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').VNodeProps & import('vue').AllowedComponentProps & import('vue').ComponentCustomProps, Readonly<import('vue').ExtractPropTypes<{}>>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, true, {}, any>;
}, {}, string, import('vue').ComponentProvideOptions, true, {}, any>;
export default _default;

View File

@@ -0,0 +1,67 @@
import { PropType } from "vue";
declare const _default: import("vue").DefineComponent<
import("vue").ExtractPropTypes<{
modelValue: (NumberConstructor | StringConstructor)[];
labels: {
type: ArrayConstructor;
default: () => any[];
};
justify: {
type: PropType<
"start" | "end" | "left" | "right" | "center" | "justify" | "match-parent"
>;
default: string;
};
type: {
type: PropType<"card" | "default">;
default: string;
};
}>,
() => any,
{},
{},
{},
import("vue").ComponentOptionsMixin,
import("vue").ComponentOptionsMixin,
("change" | "update:modelValue")[],
"change" | "update:modelValue",
import("vue").PublicProps,
Readonly<
import("vue").ExtractPropTypes<{
modelValue: (NumberConstructor | StringConstructor)[];
labels: {
type: ArrayConstructor;
default: () => any[];
};
justify: {
type: PropType<
"start" | "end" | "left" | "right" | "center" | "justify" | "match-parent"
>;
default: string;
};
type: {
type: PropType<"card" | "default">;
default: string;
};
}>
> &
Readonly<{
onChange?: (...args: any[]) => any;
"onUpdate:modelValue"?: (...args: any[]) => any;
}>,
{
type: "default" | "card";
labels: unknown[];
justify: "left" | "center" | "right" | "justify" | "start" | "end" | "match-parent";
},
{},
{},
{},
string,
import("vue").ComponentProvideOptions,
true,
{},
any
>;
export default _default;

View File

@@ -0,0 +1,17 @@
export declare function useAction({ config, form, Form }: {
config: ClForm.Config;
form: obj;
Form: Vue.Ref<any>;
}): {
getForm: (prop: string) => any;
setForm: (prop: string, value: any) => void;
setData: (prop: string, value: any) => void;
setConfig: (path: string, value: any) => void;
setOptions: (prop: string, value: any[]) => void;
setProps: (prop: string, value: any) => void;
toggleItem: (prop: string, value?: boolean) => void;
hideItem: (...props: string[]) => void;
showItem: (...props: string[]) => void;
setTitle: (value: string) => void;
collapseItem: (e: any) => void;
};

View File

@@ -0,0 +1,3 @@
export declare function useApi({ Form }: {
Form: Vue.Ref<any>;
}): obj;

View File

@@ -0,0 +1,237 @@
export declare function useForm(): {
Form: import('vue').Ref<any, any>;
config: {
[x: string]: any;
title?: any;
height?: any;
width?: any;
props: {
[x: string]: any;
inline?: boolean;
labelPosition?: "left" | "right" | "top";
labelWidth?: string | number;
labelSuffix?: string;
hideRequiredAsterisk?: boolean;
showMessage?: boolean;
inlineMessage?: boolean;
statusIcon?: boolean;
validateOnRuleChange?: boolean;
size?: ElementPlus.Size;
disabled?: boolean;
};
items: {
[x: string]: any;
type?: "tabs";
prop?: string & {};
props?: {
[x: string]: any;
labels?: {
[x: string]: any;
label: string;
value: string;
name?: string;
icon?: any;
lazy?: boolean;
}[];
justify?: "left" | "center" | "right";
color?: string;
mergeProp?: boolean;
labelWidth?: string;
error?: string;
showMessage?: boolean;
inlineMessage?: boolean;
size?: "medium" | "default" | "small";
};
span?: number;
col?: {
span: number;
offset: number;
push: number;
pull: number;
xs: any;
sm: any;
md: any;
lg: any;
xl: any;
tag: string;
};
group?: string;
collapse?: boolean;
value?: any;
label?: string;
renderLabel?: any;
flex?: boolean;
hook?: "string" | "number" | "boolean" | "join" | "split" | AnyString | "empty" | "booleanNumber" | "datetimeRange" | "splitJoin" | "json" | {
bind?: ClForm.Hook["Pipe"] | ClForm.Hook["Pipe"][];
submit?: ClForm.Hook["Pipe"] | ClForm.Hook["Pipe"][];
reset?: (prop: string) => string[];
};
hidden?: boolean | ((options: {
scope: obj;
}) => boolean);
prepend?: {
[x: string]: any;
name?: string;
options?: {
[x: string]: any;
label?: string;
value?: any;
color?: string;
type?: string;
}[] | {
value: {
[x: string]: any;
label?: string;
value?: any;
color?: string;
type?: string;
}[];
};
props?: {
[x: string]: any;
onChange?: (value: any) => void;
} | {
value: {
[x: string]: any;
onChange?: (value: any) => void;
};
};
style?: obj;
slots?: {
[key: string]: (data?: any) => any;
};
vm?: any;
};
component?: {
[x: string]: any;
name?: string;
options?: {
[x: string]: any;
label?: string;
value?: any;
color?: string;
type?: string;
}[] | {
value: {
[x: string]: any;
label?: string;
value?: any;
color?: string;
type?: string;
}[];
};
props?: {
[x: string]: any;
onChange?: (value: any) => void;
} | {
value: {
[x: string]: any;
onChange?: (value: any) => void;
};
};
style?: obj;
slots?: {
[key: string]: (data?: any) => any;
};
vm?: any;
};
append?: {
[x: string]: any;
name?: string;
options?: {
[x: string]: any;
label?: string;
value?: any;
color?: string;
type?: string;
}[] | {
value: {
[x: string]: any;
label?: string;
value?: any;
color?: string;
type?: string;
}[];
};
props?: {
[x: string]: any;
onChange?: (value: any) => void;
} | {
value: {
[x: string]: any;
onChange?: (value: any) => void;
};
};
style?: obj;
slots?: {
[key: string]: (data?: any) => any;
};
vm?: any;
};
rules?: {
[x: string]: any;
type?: "string" | "number" | "boolean" | "method" | "regexp" | "integer" | "float" | "array" | "object" | "enum" | "date" | "url" | "hex" | "email" | "any";
required?: boolean;
message?: string;
min?: number;
max?: number;
trigger?: any;
validator?: (rule: any, value: any, callback: (error?: Error) => void) => void;
} | {
[x: string]: any;
type?: "string" | "number" | "boolean" | "method" | "regexp" | "integer" | "float" | "array" | "object" | "enum" | "date" | "url" | "hex" | "email" | "any";
required?: boolean;
message?: string;
min?: number;
max?: number;
trigger?: any;
validator?: (rule: any, value: any, callback: (error?: Error) => void) => void;
}[];
required?: boolean;
children?: /*elided*/ any[];
}[];
form: obj;
isReset?: boolean;
on?: {
open?: (data: any) => void;
close?: (action: ClForm.CloseAction, done: fn) => void;
submit?: (data: any, event: {
close: fn;
done: fn;
}) => void;
change?: (data: any, prop: string) => void;
};
op: {
hidden?: boolean;
saveButtonText?: string;
closeButtonText?: string;
justify?: "flex-start" | "center" | "flex-end";
buttons?: (`slot-${string}` | ClForm.CloseAction | {
[x: string]: any;
label: string;
type?: string;
hidden?: boolean;
onClick: (options: {
scope: obj;
}) => void;
})[];
};
dialog: {
[x: string]: any;
title?: any;
height?: string;
width?: string;
hideHeader?: boolean;
controls?: Array<"fullscreen" | "close" | AnyString>;
};
};
form: obj;
visible: import('vue').Ref<boolean, boolean>;
saving: import('vue').Ref<boolean, boolean>;
loading: import('vue').Ref<boolean, boolean>;
disabled: import('vue').Ref<boolean, boolean>;
};
export * from './action';
export * from './api';
export * from './plugins';
export * from './tabs';

View File

@@ -0,0 +1,13 @@
import { Ref } from "vue";
export declare function usePlugins(
enable: boolean,
{
visible
}: {
visible: Ref<boolean>;
}
): {
create: (plugins?: ClForm.Plugin[]) => boolean;
submit: (data: any) => Promise<any>;
};

View File

@@ -0,0 +1,19 @@
export declare function useTabs({ config, Form }: {
config: ClForm.Config;
Form: Vue.Ref<any>;
}): {
active: import('vue').Ref<string, string>;
list: import('vue').ComputedRef<ClFormTabs.labels>;
isLoaded: (value: any) => any;
onLoad: (value: any) => void;
get: () => ClForm.Item<any>;
set: (data: any) => void;
change: (value: any, isValid?: boolean) => Promise<unknown>;
clear: () => void;
mergeProp: (item: ClForm.Item) => void;
toGroup: (opts: {
config: ClForm.Config;
prop: string;
refs: any;
}) => void;
};

View File

@@ -0,0 +1,22 @@
declare const _default: import('vue').DefineComponent<import('vue').ExtractPropTypes<{
name: StringConstructor;
inner: BooleanConstructor;
inline: BooleanConstructor;
enablePlugin: {
type: BooleanConstructor;
default: boolean;
};
}>, () => any, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<import('vue').ExtractPropTypes<{
name: StringConstructor;
inner: BooleanConstructor;
inline: BooleanConstructor;
enablePlugin: {
type: BooleanConstructor;
default: boolean;
};
}>> & Readonly<{}>, {
inline: boolean;
inner: boolean;
enablePlugin: boolean;
}, {}, {}, {}, string, import('vue').ComponentProvideOptions, true, {}, any>;
export default _default;

View File

@@ -0,0 +1,6 @@
import { App } from "vue";
export declare const components: {
[key: string]: any;
};
export declare function useComponent(app: App): void;

View File

@@ -0,0 +1,2 @@
declare const _default: import('vue').DefineComponent<{}, () => any, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, true, {}, any>;
export default _default;

View File

@@ -0,0 +1,4 @@
declare const _default: import('vue').DefineComponent<{}, () => import('vue').VNode<import('vue').RendererNode, import('vue').RendererElement, {
[key: string]: any;
}>, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, true, {}, any>;
export default _default;

View File

@@ -0,0 +1,2 @@
declare const _default: import('vue').DefineComponent<{}, () => any, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, true, {}, any>;
export default _default;

View File

@@ -0,0 +1,2 @@
declare const _default: import('vue').DefineComponent<{}, () => any, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, true, {}, any>;
export default _default;

View File

@@ -0,0 +1,84 @@
import { PropType } from "vue";
declare const _default: import("vue").DefineComponent<
import("vue").ExtractPropTypes<{
modelValue: StringConstructor;
field: {
type: StringConstructor;
default: string;
};
fieldList: {
type: PropType<
Array<{
label: string;
value: string;
}>
>;
default: () => any[];
};
onSearch: FunctionConstructor;
placeholder: StringConstructor;
width: {
type: (NumberConstructor | StringConstructor)[];
default: number;
};
refreshOnInput: BooleanConstructor;
}>,
() => any,
{},
{},
{},
import("vue").ComponentOptionsMixin,
import("vue").ComponentOptionsMixin,
("change" | "update:modelValue" | "field-change")[],
"change" | "update:modelValue" | "field-change",
import("vue").PublicProps,
Readonly<
import("vue").ExtractPropTypes<{
modelValue: StringConstructor;
field: {
type: StringConstructor;
default: string;
};
fieldList: {
type: PropType<
Array<{
label: string;
value: string;
}>
>;
default: () => any[];
};
onSearch: FunctionConstructor;
placeholder: StringConstructor;
width: {
type: (NumberConstructor | StringConstructor)[];
default: number;
};
refreshOnInput: BooleanConstructor;
}>
> &
Readonly<{
onChange?: (...args: any[]) => any;
"onUpdate:modelValue"?: (...args: any[]) => any;
"onField-change"?: (...args: any[]) => any;
}>,
{
width: string | number;
refreshOnInput: boolean;
field: string;
fieldList: {
label: string;
value: string;
}[];
},
{},
{},
{},
string,
import("vue").ComponentProvideOptions,
true,
{},
any
>;
export default _default;

View File

@@ -0,0 +1,3 @@
export declare function usePlugins(): {
create: (plugins?: ClSearch.Plugin[]) => void;
};

View File

@@ -0,0 +1,91 @@
import { PropType } from "vue";
declare const _default: import("vue").DefineComponent<
import("vue").ExtractPropTypes<{
inline: {
type: BooleanConstructor;
default: boolean;
};
props: {
type: ObjectConstructor;
default: () => {};
};
data: {
type: ObjectConstructor;
default: () => {};
};
items: {
type: PropType<ClForm.Item[]>;
default: () => any[];
};
resetBtn: {
type: BooleanConstructor;
default: boolean;
};
collapse: {
type: BooleanConstructor;
default: boolean;
};
onLoad: FunctionConstructor;
onSearch: FunctionConstructor;
}>,
() => any,
{},
{},
{},
import("vue").ComponentOptionsMixin,
import("vue").ComponentOptionsMixin,
"reset"[],
"reset",
import("vue").PublicProps,
Readonly<
import("vue").ExtractPropTypes<{
inline: {
type: BooleanConstructor;
default: boolean;
};
props: {
type: ObjectConstructor;
default: () => {};
};
data: {
type: ObjectConstructor;
default: () => {};
};
items: {
type: PropType<ClForm.Item[]>;
default: () => any[];
};
resetBtn: {
type: BooleanConstructor;
default: boolean;
};
collapse: {
type: BooleanConstructor;
default: boolean;
};
onLoad: FunctionConstructor;
onSearch: FunctionConstructor;
}>
> &
Readonly<{
onReset?: (...args: any[]) => any;
}>,
{
data: Record<string, any>;
props: Record<string, any>;
items: ClForm.Item<any>[];
inline: boolean;
resetBtn: boolean;
collapse: boolean;
},
{},
{},
{},
string,
import("vue").ComponentProvideOptions,
true,
{},
any
>;
export default _default;

View File

@@ -0,0 +1,7 @@
export declare function useData({ config, Table }: {
config: ClTable.Config;
Table: Vue.Ref<any>;
}): {
data: import('vue').Ref<obj[], obj[]>;
setData: (list: obj[]) => void;
};

View File

@@ -0,0 +1 @@
export declare function renderHeader(item: ClTable.Column, { scope, slots }: any): any;

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