[fix] init

This commit is contained in:
陶林 2022-06-07 09:04:28 +08:00
commit 1311e08484
46 changed files with 16870 additions and 0 deletions

4
.babelrc Normal file
View File

@ -0,0 +1,4 @@
{
"presets": ["@babel/preset-env", "@vue/babel-preset-jsx"],
"plugins": ["@babel/plugin-transform-runtime"]
}

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
/node_modules
yarn-error.log
yarn.lock

5
.idea/.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/

12
.idea/cl-crud2-main.iml Normal file
View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/temp" />
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
<excludeFolder url="file://$MODULE_DIR$/tmp" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View File

@ -0,0 +1,58 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<HTMLCodeStyleSettings>
<option name="HTML_SPACE_INSIDE_EMPTY_TAG" value="true" />
<option name="HTML_ENFORCE_QUOTES" value="true" />
</HTMLCodeStyleSettings>
<JSCodeStyleSettings version="0">
<option name="FORCE_SEMICOLON_STYLE" value="true" />
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
<option name="FORCE_QUOTE_STYlE" value="true" />
<option name="ENFORCE_TRAILING_COMMA" value="Remove" />
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
<option name="SPACES_WITHIN_IMPORTS" value="true" />
</JSCodeStyleSettings>
<TypeScriptCodeStyleSettings version="0">
<option name="FORCE_SEMICOLON_STYLE" value="true" />
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
<option name="FORCE_QUOTE_STYlE" value="true" />
<option name="ENFORCE_TRAILING_COMMA" value="Remove" />
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
<option name="SPACES_WITHIN_IMPORTS" value="true" />
</TypeScriptCodeStyleSettings>
<VueCodeStyleSettings>
<option name="INTERPOLATION_NEW_LINE_AFTER_START_DELIMITER" value="false" />
<option name="INTERPOLATION_NEW_LINE_BEFORE_END_DELIMITER" value="false" />
</VueCodeStyleSettings>
<editorconfig>
<option name="ENABLED" value="false" />
</editorconfig>
<codeStyleSettings language="HTML">
<option name="SOFT_MARGINS" value="100" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
<option name="USE_TAB_CHARACTER" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="JavaScript">
<option name="SOFT_MARGINS" value="100" />
<indentOptions>
<option name="USE_TAB_CHARACTER" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="TypeScript">
<option name="SOFT_MARGINS" value="100" />
<indentOptions>
<option name="USE_TAB_CHARACTER" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="Vue">
<option name="SOFT_MARGINS" value="100" />
<indentOptions>
<option name="INDENT_SIZE" value="4" />
<option name="TAB_SIZE" value="4" />
<option name="USE_TAB_CHARACTER" value="true" />
</indentOptions>
</codeStyleSettings>
</code_scheme>
</component>

View File

@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>

8
.idea/modules.xml Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/cl-crud2-main.iml" filepath="$PROJECT_DIR$/.idea/cl-crud2-main.iml" />
</modules>
</component>
</project>

9
.prettierrc Normal file
View File

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

3
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"liveServer.settings.port": 5501
}

46
README.md Normal file
View File

@ -0,0 +1,46 @@
#### Browser
```html
<script src="https://cdn.jsdelivr.net/npm/cl-crud2@0.3.3/dist/cl-crud2.min.js"></script>
```
## Document
[https://docs-admin-pro.cool-js.com/#/front/crud](https://docs-admin-pro.cool-js.com/#/front/crud)
## Version
- 0.4.1 处理 cl-dialog 关闭按钮点击刷新页面问题
- 0.4.0 弃用 cl-dialog, destroy-on-close改用 key 缓存
- 0.3.8 添加 saveButtonText, closeButtonText 字典
- 0.3.7 cl-upsert 添加 showLoading, hiddenLoading
- 0.3.5 添加 dict.label
- 0.3.4 优化 cl-dialog cl-form 周期
- 0.3.3 解决 cl-form 作用域改变问题
- 0.3.0 解决 cl-form 默认值不监听问题,对组件添加响应式布局
- 0.2.11 解决 cl-query props.list 不更新问题
- 0.2.9 优化 cl-form, cl-adv-search 对表单操作错误的问题
- 0.2.8 优化
- 0.2.7 解决 cl-form props 异常
- 0.2.6 解决 cl-dialog 关闭异常问题
- 0.2.5 过滤 cl-upsert, cl-form 事件(submit) hidden = true 的值
- 0.2.4 解决 cl-upsert opList hiddenOp 异常
- 0.2.3 解决 cl-form form.done 异常
- 0.2.2 解决 cl-search-key field 取值错误
- 0.2.1 解决 cl-dialog.props.fullscreen 异常
- 0.2.0 解决 uniapp cloud 命名冲突问题
- 0.1.9 cl-crud 添加 doLayout 方法
- 0.1.8 解决 cl-form resetForm 异常hidden 添加 isEdit 参数
- 0.1.7 cl-form cl-table 添加缓存处理
- 0.1.6 upsert.hidden adv-search.hidden 添加 @[prop] | function 方式
- 0.1.5 解决 cl-upser onOpen 参数缺少问题
- 0.1.4 解决 rowAppend 参数无法传递异常
- 0.1.3 添加 cl-table 的 emit 事件, cl-table 添加 component 方式渲染
- 0.1.2 解决 clearForm 后,监听事件失效问题
- 0.1.1 table.column.dict 支持 el-tag 标签
- 0.1.0 重写 permission 设置途径
- 0.0.20 setPermission 返回 this
- 0.0.19 添加 setPermission
- 0.0.18 重构 el-adv-search, el-upsert, el-form 周期事件
- 0.0.1 初始化

3
dist/cl-crud2.min.js vendored Normal file

File diff suppressed because one or more lines are too long

31
dist/cl-crud2.min.js.LICENSE.txt vendored Normal file
View File

@ -0,0 +1,31 @@
/*!
* The buffer module from node.js, for the browser.
*
* @author Feross Aboukhadijeh <http://feross.org>
* @license MIT
*/
/*!
* is-plain-object <https://github.com/jonschlinkert/is-plain-object>
*
* Copyright (c) 2014-2017, Jon Schlinkert.
* Released under the MIT License.
*/
/*!
* isobject <https://github.com/jonschlinkert/isobject>
*
* Copyright (c) 2014-2017, Jon Schlinkert.
* Released under the MIT License.
*/
/*!
* shallow-clone <https://github.com/jonschlinkert/shallow-clone>
*
* Copyright (c) 2015-present, Jon Schlinkert.
* Released under the MIT License.
*/
/*! ieee754. BSD-3-Clause License. Feross Aboukhadijeh <https://feross.org/opensource> */
/*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */

1
dist/cl-crud2.min.js.map vendored Normal file

File diff suppressed because one or more lines are too long

938
example/index.html Normal file
View File

@ -0,0 +1,938 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>CRUD Example</title>
<link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
<script src="./vue2.js"></script>
<script src="https://unpkg.com/element-ui/lib/index.js"></script>
<script src="../dist/cl-crud2.min.js"></script>
<style>
html,
body,
#app {
height: 100%;
width: 100%;
}
* {
padding: 0;
margin: 0;
}
</style>
</head>
<body>
<div id="app">
<cl-crud ref="crud" @load="onLoad">
<el-row type="flex" align="middle">
<cl-refresh-btn></cl-refresh-btn>
<cl-add-btn></cl-add-btn>
<cl-multi-delete-btn></cl-multi-delete-btn>
<cl-query
:list="[
{
label: '启用',
value: 1
},
{
label: '禁用',
value: 0
}
]"
></cl-query>
<cl-filter label="状态">
<el-select
size="mini"
v-model="selects.status"
@change="val => {
refresh({
status: val,
page: 1
})
}"
>
<el-option value="" label="全部"></el-option>
<el-option value="0" label="禁用"></el-option>
<el-option value="1" label="启用"></el-option>
</el-select>
</cl-filter>
<el-button size="mini" @click="openForm">自定义测试表单</el-button>
<el-button size="mini" @click="openDialog">自定义对话框</el-button>
<cl-flex1></cl-flex1>
<cl-search-key
field="name"
:field-list="[
{
label: '姓名',
value: 'name'
},
{
label: '身份证',
value: 'idCard'
}
]"
></cl-search-key>
<cl-adv-btn></cl-adv-btn>
</el-row>
<el-row>
<cl-table ref="table" v-bind="table.props" v-on="table.on"> </cl-table>
</el-row>
<el-row>
<cl-pagination></cl-pagination>
</el-row>
<!-- 高级搜索 -->
<cl-adv-search ref="adv-search" v-bind="advSearch.props" v-on="advSearch.on">
</cl-adv-search>
<!-- 编辑、新增 -->
<cl-upsert
v-model="upsert.form"
ref="upsert"
v-bind="upsert.props"
v-on="upsert.on"
>
</cl-upsert>
<!-- 自定义表单 -->
<cl-form ref="form">
<!-- 动态增减表单验证 -->
<template #slot-validate="{scope}">
<el-form-item
v-for="(item, index) in scope.vads"
:key="index"
:prop="'vads.' + index + '.val'"
:rules="{ required: true, message: '请输入' }"
>
<el-input v-model="item.val"></el-input>
</el-form-item>
<el-button @click="addVad(scope.vads)">添加行</el-button>
</template>
<!-- 测试设置表单值 -->
<template #slot-var="{ scope }">
<el-input v-model="scope._name"></el-input>
<el-button @click="setFormValue(scope)">设置</el-button>
</template>
<!-- 内嵌crud -->
<template #slot-crud="{scope}">
<cl-crud @load="onUpsertCrudLoad">
<cl-table
:props="{
'max-height': '300px'
}"
:columns="[
{
label: '姓名',
prop: 'name'
},
{
label: '存款',
prop: 'price',
},
{
label: '创建时间',
prop: 'createTime'
},
]"
></cl-table>
</cl-crud>
</template>
</cl-form>
<!-- 自定义对话框 -->
<cl-dialog
title="自定义对话框"
:visible.sync="dialog.visible"
v-bind="dialog.props"
v-on="dialog.on"
>
<p>自定义对话框</p>
<test></test>
</cl-dialog>
</cl-crud>
</div>
<script>
// 表格数据
const userList = [
{
id: 1,
name: "刘一",
process: 42.2,
createTime: "2019年09月02日",
price: 75.99,
salesRate: 52.2,
status: 1,
images: [
"https://cool-comm.oss-cn-shenzhen.aliyuncs.com/show/imgs/chat/avatar/1.jpg"
]
},
{
id: 2,
name: "陈二",
process: 35.2,
createTime: "2019年09月05日",
price: 242.1,
salesRate: 72.1,
status: 1,
images: [
"https://cool-comm.oss-cn-shenzhen.aliyuncs.com/show/imgs/chat/avatar/2.jpg"
]
},
{
id: 3,
name: "张三",
process: 10.2,
createTime: "2019年09月12日",
price: 74.11,
salesRate: 23.9,
status: 0,
images: [
"https://cool-comm.oss-cn-shenzhen.aliyuncs.com/show/imgs/chat/avatar/3.jpg"
]
},
{
id: 4,
name: "李四",
process: 75.5,
createTime: "2019年09月13日",
price: 276.64,
salesRate: 47.2,
status: 0,
images: [
"https://cool-comm.oss-cn-shenzhen.aliyuncs.com/show/imgs/chat/avatar/4.jpg"
]
},
{
id: 5,
name: "王五",
process: 25.4,
createTime: "2019年09月18日",
price: 160.23,
salesRate: 28.3,
status: 1,
images: [
"https://cool-comm.oss-cn-shenzhen.aliyuncs.com/show/imgs/chat/avatar/5.jpg"
]
}
];
// 网络请求
const testService = {
page: p => {
console.log("GET[page]", p);
return Promise.resolve({
list: userList,
pagination: {
page: p.page,
size: p.size,
total: 5
}
});
},
info: d => {
console.log("GET[info]", d);
return new Promise(resolve => {
resolve({
id: 1,
name: d.id,
price: 100,
ids: "0,3,2"
});
});
},
add: d => {
console.log("POST[add]", d);
return Promise.resolve();
},
delete: d => {
console.log("POST[delete]", d);
return Promise.resolve();
},
update: d => {
console.log("POST[update]", d);
return Promise.resolve();
}
};
// 注册组件
Vue.use(CRUD, {
crud: {
event: {
refresh: (data, { app }) => {
app.refresh(data);
}
},
dict: {
label: {
add: "添加",
update: "更新",
delete: "移除",
multiDelete: "批量移除",
advSearch: "过滤",
saveButtonText: "确认"
// closeButtonText: "取消"
}
}
}
});
Vue.component("test", {
data() {
return {
text: ""
};
},
mounted() {
console.log("test load");
setInterval(() => {
this.text = Math.random();
}, 1000);
},
render(h) {
return h("p", this.text);
}
});
new Vue({
el: "#app",
data() {
return {
selects: {
status: ""
},
table: {
on: {
"row-click": row => {
console.log("行点击", row);
}
},
props: {
columns: [
{
type: "selection",
align: "center",
width: 60
},
{
label: "姓名",
prop: "name",
align: "center",
"min-width": 120
},
{
label: "存款",
prop: "price",
sortable: true,
align: "center",
"min-width": 120
},
{
label: "状态",
prop: "status",
align: "center",
"min-width": 120,
dict: [
{
label: "启用",
value: 1,
type: "primary"
},
{
label: "禁用",
value: 0,
type: "danger"
}
]
},
{
label: "创建时间",
prop: "createTime",
align: "center",
"min-width": 150
},
{
label: "操作",
type: "op",
align: "center",
layout: [
"edit",
"delete",
({ h }) => {
return h(
"el-button",
{
props: {
type: "text",
size: "mini"
},
on: {
click: () => {
this.$refs["crud"].rowAppend({
name: "icssoa append"
});
}
}
},
"追加"
);
}
]
}
]
}
},
upsert: {
on: {
open(isEdit, data) {
console.log("cl-upsert 打开", isEdit, data.name);
},
close() {
console.log("cl-upsert 关闭");
}
},
props: {
props: {
width: "1000px",
"label-position": "top"
},
onOpen: (isEdit, data, { done, submit, close }) => {
console.log("cl-upsert 打开钩子", isEdit, data);
},
onClose(done) {
console.log("cl-upsert 关闭钩子");
done();
},
onInfo(data, { next, done, close }) {
console.log("cl-upsert 详情钩子", data);
next(data);
},
onSubmit(isEdit, data, { next, close, done }) {
console.log("cl-upsert 提交钩子", `是否编辑 ${isEdit}`, data);
next(data);
},
items: [
{
label: "姓名",
prop: "name",
component: {
name: "el-input"
},
rules: {
required: true,
message: "姓名不能为空"
}
},
{
label: "是否显示存款",
prop: "isPrice",
flex: false,
component: {
name: "el-switch"
}
},
{
label: "存款",
prop: "price",
hidden: "@isPrice",
component: {
name: "el-input-number"
},
rules: {
required: true,
message: "存款不能为空"
}
}
],
op: {
layout: [
"close",
"save",
({ h }) => {
return h(
"el-button",
{
props: {
size: "small"
},
on: {
click: () => {
// this.$refs["upsert"].setForm(
// "name",
// "神仙都没用"
// );
this.upsert.form.name = "神仙都没用";
}
}
},
"设置名称"
);
}
]
},
hdr: {
opList: ["fullscreen", "close"]
}
},
form: {
name: "xxxx"
}
},
advSearch: {
on: {
open(data) {
console.log("adv-search 打开", data);
},
close() {
console.log("adv-search 关闭");
},
reset() {
console.log("adv-search 重置");
},
clear() {
console.log("adv-search 清空");
}
},
props: {
onOpen(data, { next }) {
console.log("adv-search 打开钩子", data);
next();
},
onClose(done) {
console.log("adv-search 关闭钩子");
done();
},
onSearch(data, { next, close }) {
console.log("adv-search 搜索钩子", data);
next(data);
},
opList: ["search", "reset", "clear", "close"],
items: [
{
label: "金额",
prop: "price",
value: 100,
component: {
name: "el-input-number",
props: {
min: 1,
max: 1000000
}
}
},
{
label: "销售率",
prop: "salesRate",
component: {
name: "el-input-number",
props: {
precision: 2,
min: 0,
max: 100
}
}
}
]
}
},
dialog: {
visible: false,
props: {
title: "自定义对话框",
props: {
"before-close"(done) {
console.log("dialog before-close");
setTimeout(() => {
done();
}, 300);
}
}
},
on: {
open() {
console.log("dialog open");
},
closed() {
console.log("dialog closed");
},
opened() {
console.log("dialog opened");
},
close() {
console.log("dialog close");
}
}
}
};
},
methods: {
openForm() {
const { open, setForm } = this.$refs["form"];
const rd = Math.random();
open({
props: {
title: "自定义表单",
"label-width": "150px",
width: "1000px"
},
form: {
qs: [1],
_name: "羊姜" + rd
},
items: [
{
label: "表单名称",
prop: "name",
component: {
name: "test"
}
},
{
props: {
"label-width": "0px"
},
component: ({ h }) => {
return h(
"el-divider",
{
props: {
"content-position": "left"
}
},
"测试设置表单值"
);
}
},
{
component: {
name: "slot-var"
}
},
{
props: {
"label-width": "0px"
},
component: ({ h }) => {
return h(
"el-divider",
{
props: {
"content-position": "left"
}
},
"测试验证el-select 清空表单"
);
}
},
{
label: "类型",
prop: "type",
component: {
name: "el-select",
on: {
change: v => {
setForm("name", v);
}
},
options: [
{
label: "羊姜",
value: "羊姜"
},
{
label: "神仙都没用",
value: "神仙都没用"
}
]
}
},
{
props: {
"label-width": "0px"
},
component: ({ h }) => {
return h(
"el-divider",
{
props: {
"content-position": "left"
}
},
"测试内嵌CRUD"
);
}
},
{
props: {
"label-width": "0px"
},
component: {
name: "slot-crud"
}
},
{
props: {
"label-width": "0px"
},
component: ({ h }) => {
return h(
"el-divider",
{
props: {
"content-position": "left"
}
},
"测试验证规则"
);
}
},
{
prop: "vads",
value: [],
label: "动态增减表单验证",
component: {
name: "slot-validate"
}
},
{
props: {
"label-width": "0px"
},
component: ({ h }) => {
return h(
"el-divider",
{
props: {
"content-position": "left"
}
},
"测试显隐"
);
}
},
{
label: "奇术",
prop: "qs",
value: [],
component: {
name: "el-select",
attrs: {
placeholder: "请选择奇术"
},
props: {
multiple: true
},
options: [
{
label: "烟水还魂",
value: 1
},
{
label: "雨恨云愁",
value: 2
}
]
}
},
{
label: "技能",
prop: "jn",
value: 1,
component: {
name: "el-select",
attrs: {
placeholder: "请选择技能"
},
options: [
{
label: "飞羽箭",
value: 1
},
{
label: "落星式",
value: 2
}
]
}
},
{
label: "五行",
prop: "wx",
value: 0,
hidden: ({ scope }) => {
return scope.jn == 1;
},
component: {
name: "el-radio-group",
options: [
{
label: "水",
value: 0
},
{
label: "火",
value: 1
},
{
label: "雷",
value: 2
},
{
label: "风",
value: 3
},
{
label: "土",
value: 4
}
]
}
},
{
label: "雨润",
prop: "s1",
hidden: ({ scope }) => {
return scope.wx != 0;
},
component: ({ h }) => {
return h("p", "以甘甜雨露的滋润使人精力充沛");
}
},
{
label: "风雪冰天",
prop: "s2",
hidden: ({ scope }) => {
return scope.wx != 0;
},
component: ({ h }) => {
return h("p", "召唤漫天风雪,对敌方造成巨大的杀伤力");
}
},
{
label: "三昧真火",
prop: "h",
hidden: ({ scope }) => {
return scope.wx != 1;
},
component: ({ h }) => {
return h("p", "召唤三昧真火焚烧敌方的仙术");
}
},
{
label: "惊雷闪",
prop: "l",
hidden: ({ scope }) => {
return scope.wx != 2;
},
component: ({ h }) => {
return h(
"p",
"召唤惊雷无数,对敌方全体进行攻击,是十分强力的仙术"
);
}
},
{
label: "如沐春风",
prop: "f",
hidden: ({ scope }) => {
return scope.wx != 3;
},
component: ({ h }) => {
return h("p", "温暖柔和的复苏春风,使人回复活力");
}
},
{
label: "艮山壁障",
prop: "t",
hidden: ({ scope }) => {
return scope.wx != 4;
},
component: ({ h }) => {
return h(
"p",
"以艮山之灵形成一道壁障,受此壁障守护者刀枪不入"
);
}
}
],
on: {
open: (data, { close, submit, done }) => {
console.log("cl-form open", data);
},
close(done) {
done();
console.log("cl-form close");
},
submit: (data, { close, done, next }) => {
console.log("cl-form submit", data);
setTimeout(() => {
this.$message.success("提交成功");
close();
}, 1500);
}
}
});
},
openDialog() {
this.dialog.visible = true;
},
setFormValue(scope) {
this.$refs["form"].setForm("_name", "神仙");
},
onLoad({ ctx, app }) {
ctx.service(testService)
.permission(() => {
return {
add: true,
update: true,
delete: true
};
})
.done();
app.refresh();
},
refresh(params) {
this.$refs["crud"].refresh(params);
},
onUpsertCrudLoad({ ctx, app }) {
ctx.service(testService).done();
app.refresh();
},
addVad(list) {
list.push({
val: ""
});
}
}
});
</script>
</body>
</html>

12014
example/vue2.js Normal file

File diff suppressed because it is too large Load Diff

47
package.json Normal file
View File

@ -0,0 +1,47 @@
{
"name": "cl-crud2",
"version": "0.4.1",
"description": "cool-admin: cl-crud、cl-form、cl-dialog",
"main": "dist/cl-crud2.min.js",
"scripts": {
"dist": "webpack --config webpack.config.js",
"dev": "webpack -w"
},
"author": "cool-team-official",
"license": "ISC",
"keywords": [
"crud",
"admin",
"vue",
"element-ui",
"upsert"
],
"homepage": "https://github.com/cool-team-official/cl-crud2",
"files": [
"dist",
"src",
"webpack.config.js",
"example"
],
"devDependencies": {
"@babel/core": "^7.9.6",
"@babel/plugin-transform-runtime": "^7.9.6",
"@babel/preset-env": "^7.10.2",
"@vue/babel-preset-jsx": "^1.1.2",
"babel-loader": "^8.1.0",
"css-loader": "^3.2.0",
"progress-bar-webpack-plugin": "^2.1.0",
"style-loader": "^1.0.0",
"stylus": "^0.54.7",
"stylus-loader": "^3.0.2",
"terser-webpack-plugin": "^3.0.1",
"typescript": "^3.9.3",
"webpack": "^4.41.2",
"webpack-cli": "^3.3.11"
},
"dependencies": {
"array.prototype.flat": "^1.2.3",
"clone-deep": "^4.0.1"
},
"sideEffects": false
}

70
src/app.js Normal file
View File

@ -0,0 +1,70 @@
import { deepMerge, isFunction } from "@/utils";
import { __plugins, __inst } from "@/global";
export const bootstrap = (that) => {
// eslint-disable-next-line
const { conf, refresh, event, id, fn } = that;
const app = {
refresh(d) {
return isFunction(d) ? d(that.params, refresh) : refresh(d);
}
};
const ctx = (data) => {
deepMerge(that, data);
return ctx;
};
ctx.id = id;
ctx.conf = (d) => {
deepMerge(conf, d);
return ctx;
};
ctx.service = (d) => {
that.service = d;
if (fn.permission) {
that.permission = fn.permission(that);
}
return ctx;
};
ctx.permission = (x) => {
if (isFunction(x)) {
that.permission = x(that);
} else {
deepMerge(that.permission, x);
}
return ctx;
};
ctx.set = (key, value) => {
deepMerge(that[key], value);
return ctx;
};
["on", "once"].forEach((n) => {
ctx[n] = (name, cb) => {
event[name] = {
mode: n,
callback: cb
};
return ctx;
};
});
ctx.done = () => {
that.done();
};
return { ctx, app };
};

423
src/assets/css/index.styl Normal file
View File

@ -0,0 +1,423 @@
.cl-crud {
display: flex;
flex-direction: column;
height: 100%;
position: relative;
padding: 10px;
box-sizing: border-box;
background-color: #fff;
overflow: hidden;
&>.el-row {
margin-bottom: 10px;
&:last-child {
margin-bottom: 0;
}
}
.cl-flex1 {
flex: 1;
font-size: 12px;
}
.cl-search-key {
display: flex;
margin-left: 10px;
&__input {
width: 250px;
}
&__select {
width: 150px;
margin-right: 10px;
}
&__button {
margin-left: 10px;
}
}
.cl-adv-btn {
& > .el-button {
margin-left: 10px;
i {
margin-right: 5px;
}
}
}
.cl-table {
width: 100%;
.el-table {
.el-loading-mask {
.el-loading-spinner {
.el-icon-loading {
font-size: 25px;
color: #000;
}
.el-loading-text {
color: #666;
margin-top: 5px;
}
}
}
&.el-loading-parent--relative {
box-sizing: border-box;
}
}
&__op {
.el-dropdown-link {
cursor: pointer;
font-size: 12px;
}
}
}
.cl-query {
display: inline-flex;
margin: 0 10px;
border-radius: 3px;
button {
border: 0;
background-color: #fff;
font-size: 12px;
outline: none;
cursor: pointer;
color: #666;
white-space: nowrap;
&:hover {
color: #6FA8FF;
}
&.is-active {
color: #409EFF;
}
span {
display: inline-block;
padding: 0 15px;
border-right: 1px solid #ddd;
}
&:last-child {
span {
border: 0;
}
}
}
}
.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;
}
}
.el-input-number {
&__decrease, &__increase {
border: 0;
background-color: transparent;
}
}
& > .el-row {
overflow-x: auto;
overflow-y: hidden;
padding-bottom: 5px;
margin-bottom: 5px;
min-height: 33px;
&::-webkit-scrollbar {
height: 4px;
}
&::-webkit-scrollbar-thumb {
background-color: rgba(144, 147, 153, 0.3);
border-radius: 5px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
}
}
.cl-adv-search {
&__container {
height: calc(100% - 50px);
overflow-y: auto;
padding: 10px 20px;
.el-form-item__content {
&>div {
width: 100%;
}
}
}
&__footer {
display: flex;
align-items: center;
justify-content: flex-end;
height: 40px;
margin: 0 10px;
}
.el-drawer {
outline: none;
&__header {
span {
outline: none;
font-size: 15px;
}
}
&__close-btn {
outline: none;
}
}
}
.cl-form {
.el-form-item {
.el-input-number {
&__decrease, &__increase {
border: 0;
background-color: transparent;
}
}
}
&-item {
display: flex;
&__prepend {
margin-right: 10px;
}
&__component {
&.is-flex {
flex: 1;
width: 100%;
&>div {
width: 100%;
}
}
}
&__append {
margin-left: 10px;
}
&__collapse {
height: 33px;
width: 100%;
cursor: pointer;
font-size: 12px;
.el-divider {
margin: 16px 0;
&__text {
font-size: 12px;
}
}
i {
margin-left: 6px;
}
}
}
&__footer {
display: flex;
justify-content: flex-end;
}
}
.cl-dialog {
.el-dialog {
&__header {
padding: 10px !important;
text-align: center;
border-bottom: 1px solid #f7f7f7;
.el-dialog__title {
font-size: 15px;
letter-spacing: 0.5px;
}
.el-dialog__headerbtn {
display: none;
top: 13px;
.el-dialog__close {
font-size: 18px;
}
}
&-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: 20px;
}
&__footer {
padding-bottom: 15px;
}
}
&__header {
height: 25px;
line-height: 25px;
text-align: center;
position: relative;
}
&__title {
display: block;
font-size: 15px;
letter-spacing: 0.5px;
}
&__headerbtn {
display: flex;
justify-content: flex-end;
position: absolute;
right: 0;
top: 0;
z-index: 9;
.minimize, .maximize, .close {
display: flex;
align-items: center;
justify-content: center;
height: 25px;
width: 40px;
border: 0;
background-color: #fff;
cursor: pointer;
outline: none;
i {
font-size: 16px;
&:hover {
opacity: 0.7;
}
}
}
}
&.hidden-header {
.el-dialog__header {
display: none;
}
}
}
.cl-crud__op-dropdown-menu {
.el-button {
width: 100%;
text-align: left;
}
}
// Element-ui Theme
.el-message {
&.el-message--success, &.el-message--error, &.el-message--info, &.el-message--warning {
min-width: auto;
background-color: #fff;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
border: 0;
padding: 12px 20px 12px 15px;
.el-message__content {
color: #999;
}
}
}
.el-table {
&__header {
th {
padding: 0 !important;
background-color: #ebeef5 !important;
height: 36px;
line-height: 36px;
}
.cell {
color: $color-main;
font-weight: normal;
}
}
&__column {
&-filter-trigger {
margin-left: 5px;
}
}
&-column--selection {
.cell {
padding: 0 14px !important;
}
}
}
.el-table-filter {
margin-top: 5px !important;
.el-checkbox__label {
font-size: 12px;
}
}
@media only screen and (max-width: 768px) {
.el-message-box {
width: 90% !important;
}
.el-table {
&__body {
&-wrapper {
&::-webkit-scrollbar {
height: 6px;
width: 6px;
}
}
}
}
}

11
src/common/index.js Normal file
View File

@ -0,0 +1,11 @@
Promise.prototype.done = function (cb) {
let P = this.constructor;
return this.then(
(value) => P.resolve(cb()).then(() => value),
(reason) =>
P.resolve(cb()).then(() => {
throw reason;
})
);
};

28
src/components/add-btn.js Normal file
View File

@ -0,0 +1,28 @@
export default {
name: "cl-add-btn",
componentName: "ClAddBtn",
inject: ["crud"],
props: {
// el-button props
props: Object
},
render() {
return (
this.crud.getPermission("add") && (
<el-button
{...{
props: {
size: "mini",
type: "primary",
...this.props
},
on: {
click: this.crud.rowAdd
}
}}>
{this.$slots.default || this.crud.dict.label.add}
</el-button>
)
);
}
};

28
src/components/adv-btn.js Normal file
View File

@ -0,0 +1,28 @@
export default {
name: "cl-adv-btn",
componentName: "ClAdvBtn",
inject: ["crud"],
props: {
// el-button props
props: Object
},
render() {
return (
<div class="cl-adv-btn">
<el-button
{...{
props: {
size: "mini",
...this.props
},
on: {
click: this.crud.openAdvSearch
}
}}>
<i class="el-icon-search" />
{this.$slots.default || this.crud.dict.label.advSearch}
</el-button>
</div>
);
}
};

View File

@ -0,0 +1,258 @@
import { cloneDeep } from "@/utils";
import { renderNode } from "@/utils/vnode";
import Parse from "@/utils/parse";
import { Form, Emitter, Screen } from "@/mixins";
export default {
name: "cl-adv-search",
componentName: "ClAdvSearch",
inject: ["crud"],
mixins: [Emitter, Screen, Form],
props: {
// Bind value
value: {
type: Object,
default: () => {
return {};
}
},
// Form items
items: {
type: Array,
default: () => []
},
// el-drawer props
props: {
type: Object,
default: () => {
return {};
}
},
// Op button ['search', 'reset', 'clear', 'close']
opList: {
type: Array,
default: () => ["close", "search"]
},
// Hooks by open { data, { next } }
onOpen: Function,
// Hooks by close { done }
onClose: Function,
// Hooks by search { data, { next, close } }
onSearch: Function
},
data() {
return {
form: {},
visible: false
};
},
watch: {
value: {
immediate: true,
deep: true,
handler(val) {
console.log(val)
this.form = val;
}
}
},
created() {
this.$on("crud.open", this.open);
this.$on("crud.close", this.close);
},
methods: {
// Open drawer
open() {
this.items.map((e) => {
if (this.form[e.prop] === undefined) {
this.$set(this.form, e.prop, e.value);
}
});
// Open event
const next = (data) => {
this.visible = true;
if (data) {
// Merge data
Object.assign(this.form, data);
}
this.$emit("open", this.form);
};
if (this.onOpen) {
this.onOpen(this.form, { next });
} else {
next(null);
}
},
// Close drawer
close() {
// Close event
const done = () => {
this.visible = false;
this.$emit("close");
};
if (this.onClose) {
this.onClose(done);
} else {
done();
}
},
// Reset data
reset() {
this.resetForm()
this.$emit("reset");
},
// Clear data
clear() {
for (let i in this.form) {
this.form[i] = undefined
}
this.clearForm()
this.$emit("clear");
},
// Search data
search() {
const params = cloneDeep(this.form);
// Search event
const next = (params) => {
this.crud.refresh({
...params,
page: 1
});
this.close();
};
if (this.onSearch) {
this.onSearch(params, { next, close: this.close });
} else {
next(params);
}
},
// Render form
renderForm() {
return (
<el-form
ref="form"
class="cl-form"
{...{
props: {
size: "small",
"label-width": "100px",
'label-position': this.isFullscreen ? 'top' : '',
disabled: this.saving,
model: this.form,
...this.props
}
}}>
<el-row
v-loading={this.loading}
{...{
attrs: {
...this["v-loading"]
}
}}>
{this.items.map((e, i) => {
return (
!Parse("hidden", {
value: e.hidden,
scope: this.form
}) && (
<el-col
{...{
props: {
key: i,
span: 24,
...e
}
}}>
<el-form-item
{...{
props: {
...e
}
}}>
{renderNode(e.component, {
prop: e.prop,
scope: this.form,
$scopedSlots: this.$scopedSlots
})}
</el-form-item>
</el-col>
)
);
})}
</el-row>
</el-form>
);
}
},
render() {
const ButtonText = {
search: "搜索",
reset: "重置",
clear: "清空",
close: "取消"
};
return (
<div class="cl-adv-search">
<el-drawer
{...{
props: {
visible: this.visible,
title: "高级搜索",
direction: "rtl",
size: this.isFullscreen ? '100%' : "500px",
...this.props
},
on: {
"update:visible": () => {
this.close();
},
...this.on
}
}}>
<div class="cl-adv-search__container">{this.renderForm()}</div>
<div class="cl-adv-search__footer">
{this.opList.map((e) => {
if (ButtonText[e]) {
return (
<el-button
{...{
props: {
size: this.props.size || "small",
type: e === "search" ? "primary" : ""
},
on: {
click: this[e]
}
}}>
{ButtonText[e]}
</el-button>
);
} else {
return renderNode(e, {
scope: this.form,
$scopedSlots: this.$scopedSlots
});
}
})}
</div>
</el-drawer>
</div>
);
}
};

338
src/components/crud.js Normal file
View File

@ -0,0 +1,338 @@
import { deepMerge, isArray, isString, isObject, isFunction } from "@/utils";
import { bootstrap } from "@/app";
import { __inst, __crud } from "@/global";
import { Emitter } from "@/mixins";
require("@/assets/css/index.styl");
export default {
name: "cl-crud",
componentName: "ClCrud",
props: {
name: String,
onDelete: Function,
onRefresh: Function
},
mixins: [Emitter],
provide() {
return {
crud: this
};
},
data() {
return {
service: null,
loading: false,
selection: [],
test: {
refreshRd: null,
sortLock: false,
process: false
},
permission: {
update: true,
page: true,
info: true,
list: true,
add: true,
delete: true
},
dict: {
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: {
add: "新增",
delete: "删除",
multiDelete: "删除",
update: "编辑",
refresh: "刷新",
advSearch: "高级搜索",
saveButtonText: "保存",
closeButtonText: "关闭"
}
},
params: {
page: 1,
size: 20
},
fn: {
permission: null
},
events: {}
};
},
created() {
this.$on("table.selection-change", ({ selection }) => {
this.selection = selection;
});
},
mounted() {
// Merge crud data
const res = bootstrap(deepMerge(this, __crud));
// Loaded
this.$emit("load", res);
// Register event
for (let i in this.events) {
let event = this.events[i];
let mode = null;
let callback = null;
if (isObject(event)) {
mode = event.mode;
callback = event.callback;
} else {
mode = "on";
callback = event;
}
if (!["on", "once"].includes(mode)) {
return console.error(`Event[${i}].mode must be (on / once)`);
}
if (!isFunction(callback)) {
return console.error(`Event[${i}].callback is not a function`);
}
__inst[`$${mode}`](i, (data) => {
callback(data, res);
});
}
// Window onresize
window.removeEventListener("resize", function () { });
window.addEventListener("resize", () => {
this.doLayout();
});
},
methods: {
// Get service permission
getPermission(key) {
switch (key) {
case "edit":
case "update":
return this.permission["update"];
default:
return this.permission[key];
}
},
// Upsert add
rowAdd() {
this.broadcast("cl-upsert", "crud.add");
},
// Upsert edit
rowEdit(data) {
this.broadcast("cl-upsert", "crud.edit", data);
},
// Upsert append
rowAppend(data) {
this.broadcast("cl-upsert", "crud.append", data);
},
// Upsert close
rowClose() {
this.broadcast("cl-upsert", "crud.close");
},
// Row delete
rowDelete(...selection) {
// Get request function
const reqName = this.dict.api.delete;
let params = {
ids: selection.map((e) => e.id).join(",")
};
// Delete
const next = (params) => {
return new Promise((resolve, reject) => {
this.$confirm(`此操作将永久删除选中数据,是否继续?`, "提示", {
type: "warning"
})
.then((res) => {
if (res === "confirm") {
// Validate
if (!this.service[reqName]) {
return reject(`Request function '${reqName}' is not fount`);
}
// Send request
this.service[reqName](params)
.then((res) => {
this.$message.success(`删除成功`);
this.refresh();
resolve(res);
})
.catch((err) => {
this.$message.error(err);
reject(err);
});
}
})
.catch(() => null);
});
};
if (this.onDelete) {
this.onDelete(selection, { next });
} else {
next(params);
}
},
// Multi delete
deleteMulti() {
this.rowDelete.apply(this, this.selection || []);
},
// Open advSearch
openAdvSearch() {
this.broadcast("cl-adv-search", "crud.open");
},
// close advSearch
closeAdvSearch() {
this.broadcast("cl-adv-search", "crud.close");
},
// Refresh params replace
paramsReplace(params) {
const { pagination, search, sort } = this.dict;
let a = { ...params };
let b = { ...pagination, ...search, ...sort };
for (let i in b) {
if (a.hasOwnProperty(i)) {
if (i != b[i]) {
a[`_${b[i]}`] = a[i];
delete a[i];
}
}
}
for (let i in a) {
if (i[0] === "_") {
a[i.substr(1)] = a[i];
delete a[i];
}
}
return a;
},
// Service refresh
refresh(newParams = {}) {
// 设置参数
let params = this.paramsReplace(Object.assign(this.params, newParams));
// Loading
this.loading = true;
// 预防脏数据
let rd = (this.test.refreshRd = Math.random());
// 完成事件
const done = () => {
this.loading = false;
};
// 渲染
const render = (list, pagination) => {
this.broadcast("cl-table", "crud.refresh", { list });
this.broadcast("cl-pagination", "crud.refresh", pagination);
done();
};
// 请求执行
const next = (params) => {
return new Promise((resolve, reject) => {
const reqName = this.dict.api.page;
if (!this.service[reqName]) {
done();
return reject(`Request function '${reqName}' is not fount`);
}
this.service[reqName](params)
.then((res) => {
if (rd != this.test.refreshRd) {
return false;
}
if (isString(res)) {
return reject("Response error");
}
if (isArray(res)) {
render(res);
} else if (isObject(res)) {
render(res.list, res.pagination);
}
resolve(res);
})
.catch((err) => {
console.error(err);
this.$message.error(err);
reject(err);
})
.done(() => {
done();
this.test.sortLock = true;
});
});
};
if (this.onRefresh) {
return this.onRefresh(params, { next, done, render });
} else {
return next(params);
}
},
// Layout again
doLayout() {
this.broadcast("ClTable", "resize");
},
done() {
// Done render
this.test.process = true;
}
},
render() {
return <div class="cl-crud">{this.$slots.default}</div>;
}
};

332
src/components/dialog.js Normal file
View File

@ -0,0 +1,332 @@
import { renderNode } from "@/utils/vnode";
import { isBoolean } from "@/utils";
import { Screen } from '@/mixins'
export default {
name: "cl-dialog",
componentName: "ClDialog",
props: {
visible: Boolean,
title: {
type: String,
default: "对话框"
},
drag: {
type: Boolean,
default: true
},
props: {
type: Object,
default: () => {
return {};
}
},
on: {
type: Object,
default: () => {
return {};
}
},
opList: {
type: Array,
default: () => ["fullscreen", "close"]
},
hiddenOp: Boolean
},
mixins: [Screen],
data() {
return {
cacheKey: 0
}
},
watch: {
"props.fullscreen"(f) {
if (this.$el && this.$el.querySelector) {
const el = this.$el.querySelector(".el-dialog");
if (el) {
if (f) {
el.style = {
top: 0,
left: 0
};
} else {
el.style.marginBottom = "50px";
}
// Set header cursor state
el.querySelector(".el-dialog__header").style.cursor = f ? "text" : "move";
}
}
if (this.crud) {
// Fullscreen change event
this.crud.$emit("fullscreen-change");
}
},
visible: {
immediate: true,
handler(f) {
if (f) {
this.dragEvent();
} else {
setTimeout(() => {
this.changeFullscreen(false);
}, 300);
}
}
}
},
methods: {
open() {
this.cacheKey++;
this.$emit("update:visible", true);
this.$emit("open");
},
onOpened() {
this.$emit('opened')
},
beforeClose() {
if (this.props['before-close']) {
this.props['before-close'](this.close)
} else {
this.close()
}
},
close() {
this.$emit("update:visible", false);
},
onClose() {
this.$emit("close");
this.close();
},
onClosed() {
this.$emit('closed')
},
// Change dialog fullscreen status
changeFullscreen(f) {
this.$set(this.props, "fullscreen", isBoolean(f) ? f : !this.props.fullscreen);
this.$emit("update:props:fullscreen", this.props.fullscreen);
},
// Drag event
dragEvent() {
this.$nextTick(() => {
const dlg = this.$el.querySelector(".el-dialog");
const hdr = this.$el.querySelector(".el-dialog__header");
if (!hdr) {
return false;
}
hdr.onmousedown = (e) => {
// Props
const { fullscreen, top = "15vh" } = this.props;
// Body size
const { clientWidth, clientHeight } = document.body;
// Try drag
const isDrag = (() => {
if (fullscreen) {
return false;
}
if (!this.drag) {
return false;
}
// Determine height of the box is too large
let marginTop = 0;
if (["vh", "%"].some((e) => top.includes(e))) {
marginTop = clientHeight * (parseInt(top) / 100);
}
if (top.includes("px")) {
marginTop = top;
}
if (dlg.clientHeight > clientHeight - 50 - marginTop) {
return false;
}
return true;
})();
// Set header cursor state
if (!isDrag) {
return (hdr.style.cursor = "text");
} else {
hdr.style.cursor = "move";
}
// Set el-dialog style, hidden scroller
dlg.style.marginTop = 0;
dlg.style.marginBottom = 0;
dlg.style.top = dlg.style.top || top;
// Distance
const dis = {
left: e.clientX - hdr.offsetLeft,
top: e.clientY - hdr.offsetTop
};
// Calc left and top of the box
const box = (() => {
const { left, top } =
dlg.currentStyle || window.getComputedStyle(dlg, null);
if (left.includes("%")) {
return {
top: +clientHeight * (+top.replace(/\%/g, "") / 100),
left: +clientWidth * (+left.replace(/\%/g, "") / 100)
};
} else {
return {
top: +top.replace(/\px/g, ""),
left: +left.replace(/\px/g, "")
};
}
})();
// Screen limit
const pad = 5;
const minLeft = -(clientWidth - dlg.clientWidth) / 2 + pad;
const maxLeft =
(dlg.clientWidth >= clientWidth / 2
? dlg.clientWidth / 2 - (dlg.clientWidth - clientWidth / 2)
: dlg.clientWidth / 2 + clientWidth / 2 - dlg.clientWidth) - pad;
const minTop = pad;
const maxTop = clientHeight - dlg.clientHeight - pad;
// Start move
document.onmousemove = function (e) {
let left = e.clientX - dis.left + box.left;
let top = e.clientY - dis.top + box.top;
if (left < minLeft) {
left = minLeft;
} else if (left >= maxLeft) {
left = maxLeft;
}
if (top < minTop) {
top = minTop;
} else if (top >= maxTop) {
top = maxTop;
}
// Set dialog top and left
dlg.style.top = top + "px";
dlg.style.left = left + "px";
};
// Clear event
document.onmouseup = function () {
document.onmousemove = null;
document.onmouseup = null;
};
};
});
},
// Header
headerRender() {
return this.hiddenOp ? null : (
<div
class="cl-dialog__header"
{...{
on: {
dblclick: () => {
this.changeFullscreen();
}
}
}}>
{/* title */}
<span class="cl-dialog__title">{this.title}</span>
{/* op button */}
<div class="cl-dialog__headerbtn">
{this.opList.map((vnode) => {
// Fullscreen
if (vnode === "fullscreen") {
// Hidden fullscreen btn
if (this.screen === 'xs') {
return null
}
// Show diff icon
if (this.props.fullscreen) {
return (
<button type="button" class="minimize" on-click={this.changeFullscreen}>
<i class="el-icon-minus" />
</button>
)
} else {
return (
<button type="button" class="maximize" on-click={this.changeFullscreen}>
<i class="el-icon-full-screen" />
</button>
)
}
}
// Close
else if (vnode === "close") {
return (
<button type="button" class="close" on-click={this.beforeClose}>
<i class="el-icon-close" />
</button>
);
}
// Custom node render
else {
return renderNode(vnode, {
$scopedSlots: this.$scopedSlots
});
}
})}
</div>
</div>
);
},
},
render() {
return (
<el-dialog
custom-class={`cl-dialog ${this.hiddenOp ? "hidden-header" : ""}`}
{...{
props: {
...this.props,
fullscreen: this.isFullscreen ? true : this.props.fullscreen,
visible: this.visible,
"show-close": false
},
on: {
open: this.open,
opened: this.onOpened,
close: this.onClose,
closed: this.onClosed
}
}}>
{/* header */}
<template slot="title">{this.headerRender()}</template>
{/* container */}
<div class="cl-dialog__container" key={this.cacheKey}>
{this.$slots.default}
</div>
{/* footer */}
<div class="cl-dialog__footer" slot="footer">
{this.$slots.footer}
</div>
</el-dialog>
);
}
};

View File

@ -0,0 +1,13 @@
export default {
name: "cl-error-message",
props: {
title: String
},
render() {
return () => {
return <el-alert title={this.title} type="error"></el-alert>;
};
}
};

18
src/components/filter.js Normal file
View File

@ -0,0 +1,18 @@
export default {
name: "cl-filter",
componentName: "ClFilter",
props: {
label: String
},
render() {
return (
<div class="cl-filter">
<span class="cl-filter__label" v-show={this.label}>
{this.label}
</span>
{this.$slots.default}
</div>
);
}
};

7
src/components/flex1.js Normal file
View File

@ -0,0 +1,7 @@
export default {
name: "cl-flex1",
componentName: "ClFlex1",
render() {
return <div class="cl-flex1">{this.$slots.default}</div>;
}
};

375
src/components/form.js Normal file
View File

@ -0,0 +1,375 @@
import { deepMerge, isFunction, cloneDeep } from "@/utils";
import { renderNode } from "@/utils/vnode";
import Parse from "@/utils/parse";
import { Form, Emitter, Screen } from "@/mixins";
import { __inst } from "@/global";
export default {
name: "cl-form",
componentName: "ClForm",
mixins: [Emitter, Screen, Form],
props: {
// Bind value
value: {
type: Object,
default: () => {
return {};
}
}
},
data() {
return {
visible: false,
saving: false,
loading: false,
form: {},
conf: {
on: {
open: null,
submit: null,
close: null
},
props: {
fullscreen: false,
"close-on-click-modal": false,
"append-to-body": true,
},
op: {
hidden: false,
saveButtonText: "保存",
closeButtonText: "取消",
layout: ["close", "save"]
},
hdr: {
hidden: false,
opList: ["fullscreen", "close"]
},
items: [],
_data: {}
}
};
},
watch: {
value: {
immediate: true,
deep: true,
handler(val) {
this.form = val;
}
},
form: {
immediate: true,
handler(val) {
this.$emit("input", val);
}
}
},
methods: {
open(options = {}) {
// Merge conf
for (let i in this.conf) {
if (i == "items") {
this.conf.items = cloneDeep(options.items || []);
} else {
deepMerge(this.conf[i], options[i]);
}
}
// Show dialog
this.visible = true;
// Preset form
if (options.form) {
for (let i in options.form) {
this.$set(this.form, i, options.form[i]);
}
}
// Set form data by items
this.conf.items.map((e) => {
if (e.prop) {
// Priority use form data
this.$set(this.form, e.prop, this.form[e.prop] || cloneDeep(e.value));
}
});
// Open callback
const { open } = this.conf.on;
if (open) {
this.$nextTick(() => {
open(this.form, {
close: this.close,
submit: this.submit,
done: this.done
});
});
}
},
beforeClose() {
if (this.conf.on.close) {
this.conf.on.close(this.close);
} else {
this.close()
}
},
close() {
this.visible = false;
this.clear();
this.done();
},
done() {
this.saving = false;
},
clear() {
for (let i in this.form) {
delete this.form[i]
}
this.clearForm()
},
submit() {
// Validate form
this.$refs["form"].validate(async (valid) => {
if (valid) {
this.saving = true;
// Hooks event
const { submit } = this.conf.on;
// Get mount variable
const { $refs } = __inst;
// Hooks by onSubmit
if (isFunction(submit)) {
let d = cloneDeep(this.form);
// Filter hidden data
this.conf.items.forEach((e) => {
if (e._hidden) {
delete d[e.prop];
}
});
submit(d, {
done: this.done,
close: this.close,
$refs
});
} else {
console.error("on[submit] is not found");
}
}
});
},
showLoading() {
this.loading = true;
},
hiddenLoading() {
this.loading = false;
},
collapseItem(item) {
if (item.collapse !== undefined) {
item.collapse = !item.collapse;
}
},
formRender() {
const { props, items } = this.conf;
return (
<el-form
ref="form"
class="cl-form"
{...{
props: {
size: "small",
"label-width": "100px",
"label-position": this.isFullscreen ? "top" : "",
disabled: this.saving,
model: this.form,
...props
}
}}>
<el-row gutter={10} v-loading={this.loading}>
{items.map((e, i) => {
// Is hidden
e._hidden = Parse("hidden", {
value: e.hidden,
scope: this.form,
data: this.conf._data
});
// Is flex
if (e.flex === undefined) {
e.flex = true;
}
return (
!e._hidden && (
<el-col
key={`form-item-${i}`}
{...{
props: {
key: i,
span: 24,
...e
}
}}>
{e.component && (
<el-form-item
{...{
props: {
label: e.label,
prop: e.prop,
rules: e.rules,
...e.props
}
}}>
{/* Redefine label */}
<template slot="label">
<span
on-click={() => {
this.collapseItem(e);
}}>
{e.label}
</span>
</template>
{/* Form item */}
<div class="cl-form-item">
{/* Component */}
{["prepend", "component", "append"].map(
(name) => {
return (
e[name] && (
<div
class={[
`cl-form-item__${name}`,
{
"is-flex": e.flex
}
]}
v-show={!e.collapse}>
{renderNode(e[name], {
prop: e.prop,
scope: this.form,
$scopedSlots: this
.$scopedSlots
})}
</div>
)
);
}
)}
{/* Collapse button */}
<div
class="cl-form-item__collapse"
v-show={e.collapse}
on-click={() => {
this.collapseItem(e);
}}>
<el-divider content-position="center">
点击展开查看更多
<i class="el-icon-arrow-down"></i>
</el-divider>
</div>
</div>
</el-form-item>
)}
</el-col>
)
);
})}
</el-row>
</el-form>
);
},
footerRender() {
const { hidden, layout, saveButtonText, closeButtonText } = this.conf.op;
const { size = "small" } = this.conf.props;
return (
!hidden &&
layout.map((vnode) => {
if (vnode == "save") {
return (
<el-button
{...{
props: {
size,
type: "success",
disabled: this.loading,
loading: this.saving
},
on: {
click: this.submit
}
}}>
{saveButtonText}
</el-button>
);
} else if (vnode == "close") {
return (
<el-button
{...{
props: {
size
},
on: {
click: this.beforeClose
}
}}>
{closeButtonText}
</el-button>
);
} else {
return renderNode(vnode, {
scope: this.form,
$scopedSlots: this.$scopedSlots
});
}
})
);
}
},
render() {
const { props, hdr } = this.conf;
return (
<div class="cl-form">
<cl-dialog
visible={this.visible}
{...{
props: {
title: props.title,
opList: hdr.opList,
props: {
...props,
'before-close': this.beforeClose
}
},
on: {
'update:visible': (v) => (this.visible = v),
"update:props:fullscreen": (v) => (props.fullscreen = v)
}
}}>
<div class="cl-form__container">{this.formRender()}</div>
<div class="cl-form__footer" slot="footer">
{this.footerRender()}
</div>
</cl-dialog>
</div>
);
}
};

35
src/components/index.js Normal file
View File

@ -0,0 +1,35 @@
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 MultiDeleteBtn from "./multi-delete-btn";
import Pagination from "./pagination";
import Query from "./query";
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 ErrorMessage from "./error-message";
export {
Crud,
AddBtn,
AdvBtn,
AdvSearch,
Flex,
Form,
MultiDeleteBtn,
Pagination,
Query,
RefreshBtn,
SearchKey,
Table,
Upsert,
Dialog,
Filter,
ErrorMessage
};

View File

@ -0,0 +1,29 @@
export default {
name: "cl-multi-delete-btn",
componentName: "ClMultiDeleteBtn",
inject: ["crud"],
props: {
// el-button props
props: Object
},
render() {
return (
this.crud.getPermission("delete") && (
<el-button
{...{
props: {
size: "mini",
type: "danger",
disabled: this.crud.selection.length == 0,
...this.props
},
on: {
click: this.crud.deleteMulti
}
}}>
{this.$slots.default || this.crud.dict.label.multiDelete || this.crud.dict.label.delete}
</el-button>
)
);
}
};

View File

@ -0,0 +1,72 @@
export default {
name: "cl-pagination",
componentName: "ClPagination",
inject: ["crud"],
props: {
props: {
type: Object,
default: () => {
return {};
}
},
on: Object
},
data() {
return {
total: 0,
currentPage: 1,
pageSize: 20
};
},
watch: {
props: {
immediate: true,
handler: "setPagination"
}
},
created() {
this.$on("crud.refresh", this.setPagination);
},
methods: {
currentChange(index) {
this.crud.refresh({
page: index
});
},
sizeChange(size) {
this.crud.refresh({
page: 1,
size
});
},
setPagination(res) {
if (res) {
this.currentPage = res.currentPage || res.page || 1;
this.pageSize = res.pageSize || res.size || 20;
this.total = res.total | 0;
}
}
},
render() {
return (
<el-pagination
{...{
on: {
"size-change": this.sizeChange,
"current-change": this.currentChange,
...this.on
},
props: {
background: true,
layout: "total, sizes, prev, pager, next, jumper",
"page-sizes": [10, 20, 30, 40, 50, 100],
...this.props,
total: this.total,
"current-page": this.currentPage,
"page-size": this.pageSize
}
}}
/>
);
}
};

107
src/components/query.js Normal file
View File

@ -0,0 +1,107 @@
export default {
name: "cl-query",
componentName: "ClQuery",
inject: ["crud"],
props: {
value: null,
multiple: Boolean,
list: {
type: Array,
required: true
},
callback: Function,
field: {
type: String,
default: "query"
}
},
data() {
return {
list2: []
};
},
watch: {
value: {
immediate: true,
handler: 'setList'
},
list() {
this.setList(this.value)
}
},
methods: {
setList(val) {
let arr = [];
if (val instanceof Array) {
arr = val;
} else {
arr = [val];
}
if (!this.multiple) {
arr.splice(1);
}
this.list2 = (this.list || []).map((e) => {
this.$set(
e,
"active",
arr.some((v) => v === e.value)
);
return e;
});
},
selectRow(item) {
if (item.active) {
item.active = false;
} else {
if (this.multiple) {
item.active = true;
} else {
this.list2.map((e) => {
e.active = e.value == item.value;
});
}
}
const selects = this.list2.filter((e) => e.active).map((e) => e.value);
const value = this.multiple ? selects : selects[0];
if (this.callback) {
this.callback(value);
} else {
this.crud.refresh({
[this.field]: value
});
this.$emit('change', value)
}
}
},
render() {
return (
<div class="cl-query">
{this.list2.map((item, index) => {
return (
<button
key={index}
class={{ "is-active": item.active }}
on-click={(event) => {
this.selectRow(item);
event.preventDefault();
}}>
<span>{item.label}</span>
</button>
);
})}
</div>
);
}
};

View File

@ -0,0 +1,25 @@
export default {
name: "cl-refresh-btn",
componentName: "ClRefreshBtn",
inject: ["crud"],
props: {
// el-button props
props: Object
},
render() {
return (
<el-button
{...{
props: {
size: "mini",
...this.props
},
on: {
click: this.crud.refresh
}
}}>
{this.$slots.default || "刷新"}
</el-button>
);
}
};

View File

@ -0,0 +1,128 @@
export default {
name: "cl-search-key",
componentName: "ClSearchKey",
inject: ["crud"],
props: {
// 绑定值
value: [String, Number],
// 选中字段
field: {
type: String,
default: "keyWord"
},
// 字段列表
fieldList: {
type: Array,
default: () => []
},
// 搜索时的钩子
onSearch: Function,
// 输入框占位内容
placeholder: {
type: String,
default: "请输入关键字"
}
},
data() {
return {
field2: null,
value2: ""
};
},
watch: {
field: {
immediate: true,
handler(val) {
this.field2 = val;
}
},
value: {
immediate: true,
handler(val) {
this.value2 = val;
}
}
},
computed: {
selectList() {
return this.fieldList.map((e, i) => {
return <el-option key={i} label={e.label} value={e.value} />;
});
}
},
methods: {
onKeyup({ keyCode }) {
if (keyCode === 13) {
this.search();
}
},
search() {
let params = {};
this.fieldList.forEach((e) => {
params[e.value] = null;
});
const next = (params2) => {
this.crud.refresh({
page: 1,
...params,
[this.field2]: this.value2,
...params2
});
};
if (this.onSearch) {
this.onSearch(params, { next });
} else {
next();
}
},
onInput(val) {
this.$emit("input", val);
this.$emit("change", val);
},
onNameChange() {
this.$emit("field-change", this.field2);
this.onInput("");
this.value2 = "";
}
},
render() {
return (
<div class="cl-search-key">
<el-select
class="cl-search-key__select"
filterable
size="mini"
v-model={this.field2}
v-show={this.selectList.length > 0}
on-change={this.onNameChange}>
{this.selectList}
</el-select>
<el-input
class="cl-search-key__input"
v-model={this.value2}
placeholder={this.placeholder}
nativeOnKeyup={this.onKeyup}
on-input={this.onInput}
clearable
size="mini"
/>
<el-button
class="cl-search-key__button"
type="primary"
size="mini"
on-click={this.search}>
搜索
</el-button>
</div>
);
}
};

450
src/components/table.js Normal file
View File

@ -0,0 +1,450 @@
import { renderNode } from "@/utils/vnode";
import { isNull } from "@/utils";
import { Emitter } from "@/mixins";
export default {
name: "cl-table",
componentName: "ClTable",
inject: ["crud"],
mixins: [Emitter],
props: {
columns: {
type: Array,
required: true,
default: () => []
},
on: {
type: Object,
default: () => {
return {};
}
},
props: {
type: Object,
default: () => {
return {};
}
}
},
data() {
return {
maxHeight: null,
data: [],
emit: {}
};
},
created() {
// Get default sort
const { order, prop } = this.props["default-sort"] || {};
// Set request params
this.crud.params.order = !order ? "" : order === "descending" ? "desc" : "asc";
this.crud.params.prop = prop;
// Crud event
this.$on("crud.resize", () => {
this.calcMaxHeight();
});
// Crud refresh
this.$on("crud.refresh", ({ list }) => {
this.data = list;
});
},
mounted() {
this.emptyRender();
this.calcMaxHeight();
this.bindEmit();
this.bindMethods()
},
methods: {
columnRender() {
return this.columns
.filter((e) => !e.hidden)
.map((item, index) => {
const deep = (item) => {
let params = {
props: item,
on: item.on
};
// If op
if (item.type === "op") {
return this.opRender(item);
}
// Default
if (!item.type || item.type === "expand") {
params.scopedSlots = {
default: (scope) => {
// Column-slot
let slot = this.$scopedSlots[`column-${item.prop}`];
let newScope = {
...scope,
...item
};
let value = scope.row[item.prop];
if (slot) {
// Use slot
return slot({
scope: newScope
});
} else {
// If component
if (item.component) {
return renderNode(item.component, {
prop: item.prop,
scope: newScope.row
});
}
// Formatter
else if (item.formatter) {
return item.formatter(
newScope.row,
newScope.column,
newScope.row[item.prop],
newScope.$index
);
}
// Dict tag
else if (item.dict) {
let data = item.dict.find((d) => d.value == value);
if (data) {
// Use el-tag
return (
<el-tag
{...{
props: {
size: "small",
"disable-transitions": true,
effect: "dark",
...data
}
}}>
{data.label}
</el-tag>
);
} else {
return value;
}
}
// Empty text
else if (isNull(value)) {
return scope.emptyText;
}
// Value
else {
return value;
}
}
},
header: (scope) => {
let slot = this.$scopedSlots[`header-${item.prop}`];
if (slot) {
return slot({
scope
});
} else {
return scope.column.label;
}
}
};
}
// Children element
const childrenEl = item.children ? item.children.map(deep) : null;
return (
<el-table-column key={`crud-table-column-${index}`} {...params}>
{childrenEl}
</el-table-column>
);
};
return deep(item);
});
},
opRender(item) {
const { rowEdit, rowDelete, getPermission } = this.crud;
if (!item) {
return null;
}
const render = (scope) => {
// Use op layout
return (item.layout || ["edit", "delete"]).map((vnode) => {
if (["edit", "update", "delete"].includes(vnode)) {
// Get permission
const perm = getPermission(vnode);
if (perm) {
let clickEvent = () => { };
let buttonText = null;
switch (vnode) {
case "edit":
case "update":
clickEvent = rowEdit;
buttonText = this.crud.dict.label.update;
break;
case "delete":
clickEvent = rowDelete;
buttonText = this.crud.dict.label.delete;
break;
}
return (
<el-button
size="mini"
type="text"
on-click={() => {
clickEvent(scope.row);
}}>
{buttonText}
</el-button>
);
}
} else {
// Use custom render
return renderNode(vnode, { scope, $scopedSlots: this.$scopedSlots });
}
});
};
return (
<el-table-column
{...{
props: {
label: "操作",
width: "160px",
...item
},
scopedSlots: {
default: (scope) => {
let el = null;
// Dropdown op
if (item.name == "dropdown-menu") {
const slot = this.$scopedSlots["table-op-dropdown-menu"];
const { width } = item["dropdown-menu"] || {};
const items = render(scope).map((e) => {
return <el-dropdown-item>{e}</el-dropdown-item>;
});
el = (
<el-dropdown
{...{
on,
props: {
trigger: "click",
...item.props
}
}}>
{slot ? (
slot({ scope })
) : (
<span class="el-dropdown-link">
<span>更多操作</span>
<i class="el-icon-arrow-down el-icon--right"></i>
</span>
)}
<el-dropdown-menu
style={{ width }}
class="cl-crud__op-dropdown-menu"
{...{ slot: "dropdown" }}>
{items}
</el-dropdown-menu>
</el-dropdown>
);
} else {
el = render(scope);
}
return <div class="cl-table__op">{el}</div>;
}
}
}}
/>
);
},
emptyRender() {
const empty = this.$scopedSlots["table-empty"];
const scope = {
h: this.$createElement,
scope: this
};
if (empty) {
this.$scopedSlots.empty = () => {
return empty(scope)[0];
};
}
},
appendRender() {
return this.$slots["append"];
},
changeSort(prop, order) {
if (order === "desc") {
order = "descending";
}
if (order === "asc") {
order = "ascending";
}
this.$refs["table"].sort(prop, order);
},
sortChange({ prop, order }) {
if (order === "descending") {
order = "desc";
}
if (order === "ascending") {
order = "asc";
}
if (!order) {
prop = null;
}
if (this.crud.test.sortLock) {
this.crud.refresh({
prop,
order,
page: 1
});
}
},
selectionChange(selection) {
this.dispatch("cl-crud", "table.selection-change", { selection });
this.$emit("selection-change", selection);
},
bindEmit() {
const funcs = [
"select",
"select-all",
"cell-mouse-enter",
"cell-mouse-leave",
"cell-click",
"cell-dblclick",
"row-click",
"row-contextmenu",
"row-dblclick",
"header-click",
"header-contextmenu",
"filter-change",
"current-change",
"header-dragend",
"expand-change"
];
funcs.forEach((name) => {
this.emit[name] = (...args) => {
this.$emit.apply(this, [name, ...args]);
};
});
},
bindMethods() {
[
"clearSelection",
"toggleRowSelection",
"toggleAllSelection",
"toggleRowExpansion",
"setCurrentRow",
"clearSort",
"clearFilter",
"doLayout",
"sort"
].forEach(e => {
this[e] = this.$refs["table"][e];
});
},
calcMaxHeight() {
return this.$nextTick(() => {
const el = this.crud.$el.parentNode;
let { height = "" } = this.props || {};
if (el) {
let rows = el.querySelectorAll(".cl-crud .el-row");
if (!rows[0] || !rows[0].isConnected) {
return false;
}
let h = 20;
for (let i = 0; i < rows.length; i++) {
let f = true;
for (let j = 0; j < rows[i].childNodes.length; j++) {
if (rows[i].childNodes[j].className == "cl-table") {
f = false;
}
}
if (f) {
h += rows[i].clientHeight + 10;
}
}
let h1 = Number(String(height).replace("px", ""));
let h2 = el.clientHeight - h;
this.maxHeight = h1 > h2 ? h1 : h2;
}
});
}
},
render() {
return (
<div class="cl-table">
{
<el-table
ref="table"
data={this.data}
v-loading={this.crud.loading}
{...{
on: {
"selection-change": this.selectionChange,
"sort-change": this.sortChange,
...this.emit,
...this.on
},
props: {
"max-height": this.maxHeight + "px",
border: true,
size: "mini",
...this.props
},
scopedSlots: {
...this.$scopedSlots
},
slots: {
...this.$slots
}
}}>
{this.columnRender()}
</el-table>
}
</div>
);
}
};

338
src/components/upsert.js Normal file
View File

@ -0,0 +1,338 @@
import { Emitter } from "@/mixins";
import { __inst } from "@/global";
export default {
name: "cl-upsert",
componentName: "ClUpsert",
inject: ["crud"],
mixins: [Emitter],
props: {
// Bind value
value: {
type: Object,
default: () => {
return {};
}
},
// Form items
items: Array,
// el-dialog attributes
props: {
type: Object,
default: () => {
return {};
}
},
// Edit sync
sync: Boolean,
// Hidden operation button
hiddenOp: Boolean,
// Op buttons
opList: {
type: Array,
default: () => ["close", "save"]
},
// Op object
op: Object,
// Dialog header object
hdr: Object,
// Save button text
saveButtonText: String,
// Close button text
closeButtonText: String,
// Hook by open { isEdit, data, { submit, done, close } }
onOpen: Function,
// Hook by close { action, done }
onClose: Function,
// Hook by info { data, { next, done, close } }
onInfo: Function,
// Hook by submit { isEdit, data, { next, done, close } }
onSubmit: Function
},
data() {
return {
isEdit: false,
form: {}
};
},
watch: {
value: {
immediate: true,
deep: true,
handler(val) {
this.form = val;
}
}
},
created() {
this.$on("crud.add", this.add);
this.$on("crud.append", this.append);
this.$on("crud.edit", this.edit);
this.$on("crud.close", this.close);
},
mounted() {
this.inject();
},
methods: {
// Add
async add() {
this.isEdit = false;
this.form = {};
await this.open();
this.$emit("open", false, {});
},
// Append data
async append(data) {
this.isEdit = false;
// Assign data
if (data) {
for (let i in data) {
this.$set(this.form, i, data[i]);
}
}
await this.open();
this.$emit("open", false, this.form);
},
// Edit
edit(data) {
const { showLoading, hiddenLoading } = this.$refs["form"];
// Is edit
this.isEdit = true;
// Start loading
showLoading();
// Async open form
if (!this.sync) {
this.open();
}
// Finish
const done = (data) => {
// Assign data
Object.assign(this.form, data);
hiddenLoading();
};
// Close
const close = () => {
hiddenLoading();
this.close();
};
// Submit
const next = (data) => {
// Get Service and Dict
const { dict, service } = this.crud;
// Get api.info
const reqName = dict.api.info;
return new Promise((resolve, reject) => {
// Validate
if (!service[reqName]) {
reject(`Request function '${reqName}' is not fount!`);
hiddenLoading();
return null;
}
// Send request
service[reqName]({
id: data.id
})
.then((res) => {
// Finish
done(res);
resolve(res);
// Sync open form
if (this.sync) {
this.open();
}
// Callback
this.$emit("open", this.isEdit, this.form);
})
.catch((err) => {
this.$message.error(err);
reject(err);
})
.done(() => {
hiddenLoading();
});
});
};
// Hook by onInfo
if (this.onInfo) {
this.onInfo(data, {
next,
done: (data) => {
done(data);
this.$emit("open", true, this.form);
},
close
});
} else {
next(data);
}
},
// Open
open() {
const { saveButtonText, closeButtonText } = this.crud.dict.label;
return new Promise((resolve) => {
this.$refs["form"].open({
items: this.items,
props: {
title: this.isEdit ? "编辑" : "新增",
...this.props
},
op: {
hidden: this.hiddenOp,
layout: this.opList,
saveButtonText: this.saveButtonText || saveButtonText,
closeButtonText: this.closeButtonText || closeButtonText,
...this.op
},
hdr: {
...this.hdr
},
on: {
open: (data, { done, close }) => {
if (this.onOpen) {
this.onOpen(this.isEdit, this.form, {
submit: () => {
this.submit(this.form);
},
done,
close
});
}
resolve();
},
submit: this.submit,
close: this.beforeClose
},
_data: {
isEdit: this.isEdit
}
});
});
},
// Close
close() {
this.$refs["form"].close();
this.$emit("close");
},
// Before close
beforeClose() {
if (this.onClose) {
this.onClose(this.close);
} else {
this.close();
}
},
/**
* Submit form
* @param {*} data
*/
submit(data, { done }) {
// Get Service and Dict
const { dict, service } = this.crud;
// Submit
const next = (data) => {
return new Promise((resolve, reject) => {
// Judge update or add
const func = this.isEdit ? "update" : "add";
// Get request function
const reqName = dict.api[func];
// Validate
if (!service[reqName]) {
done();
return reject(`Request function '${reqName}' is not fount!`);
}
// Send request
service[reqName](data)
.then((res) => {
this.$message.success("保存成功");
// Close
this.close("submit");
// Refresh
this.crud.refresh();
// Callback
resolve(res);
})
.catch((err) => {
this.$message.error(err);
reject(err);
})
.done(done);
});
};
// Hook by onSubmit
if (this.onSubmit) {
// Get mount variable
const { $refs } = __inst;
this.onSubmit(this.isEdit, data, {
$refs,
done,
next,
close: () => {
this.close("submit");
}
});
} else {
next(data);
}
},
// Inject form api
inject() {
const fns = [
"getForm",
"setForm",
"clearForm",
"setData",
"setOptions",
"toggleItem",
"hiddenItem",
"showItem",
"showLoading",
"hiddenLoading"
];
fns.forEach((e) => {
this[e] = this.$refs["form"][e];
});
}
},
render() {
return (
<div class="cl-upsert">
<cl-form
ref="form"
v-model={this.form}
{...{
scopedSlots: {
...this.$scopedSlots
}
}}></cl-form>
</div>
);
}
};

5
src/global.js Normal file
View File

@ -0,0 +1,5 @@
export let __crud = {};
export let __vue = {};
export let __inst = {};
export let __components = {};
export let __plugins = [];

33
src/index.js Normal file
View File

@ -0,0 +1,33 @@
import * as global from "./global";
import * as comps from "./components";
require("./common");
export const CRUD = {
version: "0.4.1",
install: function (Vue, options) {
const { crud, components, plugins } = options || {};
// 设置全局参数
global.__crud = crud;
global.__vue = Vue;
global.__components = components;
global.__plugins = plugins;
global.__inst = new Vue();
// 注册组件
for (let i in comps) {
Vue.component(comps[i].name, comps[i]);
}
// 挂载 $crud
Vue.prototype.$crud = {
emit: (name, callback) => {
global.__inst.$emit(name, callback);
}
};
}
};
export default CRUD;

34
src/mixins/emitter.js Normal file
View File

@ -0,0 +1,34 @@
function broadcast(componentName, eventName, params) {
this.$children.forEach((child) => {
let name = child.$options._componentTag;
if (name === componentName) {
child.$emit.apply(child, [eventName].concat(params));
} else {
broadcast.apply(child, [componentName, eventName].concat([params]));
}
});
}
export default {
methods: {
dispatch(componentName, eventName, params) {
let parent = this.$parent || this.$root;
let name = parent.$options._componentTag;
while (parent && (!name || name !== componentName)) {
parent = parent.$parent;
if (parent) {
name = parent.$options._componentTag;
}
}
if (parent) {
parent.$emit.apply(parent, [eventName].concat(params));
}
},
broadcast(componentName, eventName, params) {
broadcast.call(this, componentName, eventName, params);
}
}
};

88
src/mixins/form.js Normal file
View File

@ -0,0 +1,88 @@
import { dataset } from "@/utils";
export default {
methods: {
_set({ prop, options, hidden, path }, data) {
let conf = null
switch (this.$options._componentTag) {
case 'cl-adv-search':
conf = this
break
case 'cl-form':
conf = this.conf
break
}
let p = path;
if (prop) {
p = `items[prop:${prop}]`;
}
if (options) {
p += `.component.options`;
}
if (hidden) {
p += ".hidden";
}
return dataset(conf, p, data);
},
// Get form
getForm(prop) {
return prop ? this.form[prop] : this.form;
},
// Set form
setForm(prop, value) {
// Add watch
this.$set(this.form, prop, value);
},
// Set [props, on]
setData(path, value) {
this._set({ path }, value);
},
// Set item component options
setOptions(prop, value) {
this._set({ options: true, prop }, value);
},
// Toggle item is hide or show
toggleItem(prop, value) {
if (value === undefined) {
value = this._set({ prop, hidden: true });
}
this._set({ hidden: true, prop }, !value);
},
// Hidden item
hiddenItem(...props) {
props.forEach((prop) => {
this._set({ hidden: true, prop }, true);
});
},
// Show item
showItem(...props) {
props.forEach((prop) => {
this._set({ hidden: true, prop }, false);
});
},
// Clear form data
clearForm() {
this.$refs["form"].clearValidate();
},
// Reset form data
resetForm() {
this.$refs['form'].resetFields()
}
}
};

5
src/mixins/index.js Normal file
View File

@ -0,0 +1,5 @@
import Emitter from './emitter'
import Form from './form'
import Screen from './screen'
export { Emitter, Form, Screen }

34
src/mixins/screen.js Normal file
View File

@ -0,0 +1,34 @@
export default {
data() {
return {
screen: 'full'
}
},
computed: {
isFullscreen() {
return this.screen === 'xs'
}
},
created() {
const fn = () => {
const w = document.body.clientWidth
if (w < 768) {
this.screen = 'xs'
} else if (w < 992) {
this.screen = 'sm'
} else if (w < 1200) {
this.screen = 'md'
} else if (w < 1920) {
this.screen = 'xl'
} else {
this.screen = 'full'
}
}
window.addEventListener('resize', fn)
fn()
}
}

148
src/utils/index.js Normal file
View File

@ -0,0 +1,148 @@
import cloneDeep from "clone-deep";
import flat from "array.prototype.flat";
import { __vue, __plugins, __inst } from "@/global";
export function throttle(fn, delay) {
let prev = Date.now();
return function () {
let args = arguments;
let context = this;
let now = Date.now();
if (now - prev > delay) {
fn.apply(context, args);
prev = Date.now();
}
};
}
export function isArray(value) {
if (typeof Array.isArray === "function") {
return Array.isArray(value);
} else {
return Object.prototype.toString.call(value) === "[object Array]";
}
}
export function isObject(value) {
return Object.prototype.toString.call(value) === "[object Object]";
}
export function isNumber(value) {
return !isNaN(Number(value));
}
export function isFunction(value) {
return typeof value === "function";
}
export function isString(value) {
return typeof value === "string";
}
export function isNull(value) {
return !value && value !== 0;
}
export function isBoolean(value) {
return typeof value === "boolean";
}
export function isEmpty(value) {
if (isArray(value)) {
return value.length === 0;
}
if (isObject(value)) {
return Object.keys(value).length === 0;
}
return value === "" || value === undefined || value === null;
}
export function clone(obj) {
return Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj));
}
export function getParent(name) {
let parent = this.$parent;
while (parent) {
if (parent.$options.componentName !== name) {
parent = parent.$parent;
} else {
return parent;
}
}
return null;
}
export function dataset(obj, key, value) {
const isGet = value === undefined;
let d = obj;
let arr = flat(
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++) {
let e = arr[i];
let n = null;
if (e.includes("]")) {
let [k, v] = e.replace("]", "").split(":");
if (v) {
n = d.findIndex((x) => x[k] == v);
} else {
n = Number(n);
}
} else {
n = e;
}
if (i != arr.length - 1) {
d = d[n];
} else {
if (isGet) {
return d[n];
} else {
__inst.$set(d, n, value);
}
}
}
return obj;
} catch (e) {
console.error("格式错误", `${key}`);
return {};
}
}
export function deepMerge(a, b) {
let k;
for (k in b) {
a[k] =
a[k] && a[k].toString() === "[object Object]" ? deepMerge(a[k], b[k]) : (a[k] = b[k]);
}
return a;
}
export function contains(parent, node) {
if (document.documentElement.contains) {
return parent !== node && parent.contains(node);
} else {
while (node && (node = node.parentNode)) if (node === parent) return true;
return false;
}
}
export { cloneDeep, flat };

33
src/utils/parse.js Normal file
View File

@ -0,0 +1,33 @@
import { isString, isBoolean, isFunction, isArray } from "./index";
/**
* parse hidden
* 1 Boolean
* 2 Function({ scope })
* 3 :[prop] is bind form[prop] value
* @param {*} value
*/
export default function (method, { value, scope, data = {} }) {
if (data) {
data.isAdd = !data.isEdit;
}
if (method === "hidden") {
if (isBoolean(value)) {
return value;
} else if (isString(value)) {
const prop = value.substring(1, value.length);
switch (value[0]) {
case "@":
return !scope[prop];
case ":":
return data[prop];
}
} else if (isFunction(value)) {
return value({ scope, ...data });
}
return false;
}
}

175
src/utils/vnode.js Normal file
View File

@ -0,0 +1,175 @@
import { isFunction, isString, cloneDeep, isObject } from "./index";
import { __inst, __plugins, __vue } from "@/global";
/**
* Parse JSX, filter params
* @param {*} vnode
* @param {{scope,prop,children}} options
*/
const parse_jsx = (vnode, options = {}) => {
const { scope, prop, $scopedSlots, children = [] } = options;
const h = __inst.$createElement;
if (vnode.name.indexOf("slot-") == 0) {
let rn = $scopedSlots[vnode.name];
if (rn) {
return rn({ scope });
} else {
return <cl-error-message title={`组件渲染失败,未找到插槽:${vnode.name}`} />;
}
}
if (vnode.render) {
if (!__inst.$root.$options.components[vnode.name]) {
__vue.component(vnode.name, cloneDeep(vnode));
}
// Avoid props prompts { type:null }
delete vnode.props;
}
const keys = [
"class",
"style",
"props",
"attrs",
"domProps",
"on",
"nativeOn",
"directives",
"scopedSlots",
"slot",
"key",
"ref",
"refInFor"
];
// Avoid loop update
let data = cloneDeep(vnode);
for (let i in data) {
if (!keys.includes(i)) {
delete data[i];
}
}
if (scope) {
if (!data.attrs) {
data.attrs = {};
}
if (!data.on) {
data.on = {};
}
// Set default value
data.attrs.value = scope[prop];
// Add input event
data.on.input = (val) => {
__inst.$set(scope, prop, val);
};
}
return h(vnode.name, cloneDeep(data), children);
};
/**
* Render vNode
* @param {*} vnode
* @param {*} options
*/
export function renderNode(vnode, { prop, scope, $scopedSlots }) {
const h = __inst.$createElement;
if (!vnode) {
return null;
}
// When slot or tagName
if (isString(vnode)) {
return parse_jsx({ name: vnode }, { scope, $scopedSlots });
}
// When customeize render function
if (isFunction(vnode)) {
return vnode({ scope, h });
}
// When jsx
if (isObject(vnode)) {
if (vnode.context) {
return vnode;
}
if (vnode.name) {
// Handle general component
const keys = ["el-select", "el-radio-group", "el-checkbox-group"]
if (keys.includes(vnode.name)) {
// Append component children
const children = (vnode.options || []).map((e, i) => {
if (vnode.name === 'el-select') {
let label, value;
if (isString(e)) {
label = value = e
} else if (isObject(e)) {
label = e.label
value = e.value
} else {
return <cl-error-message title={`组件渲染失败options 参数错误`} />;
}
return (
<el-option
{...{
props: {
key: i,
label,
value,
...e.props
}
}}
/>
);
}
else if (vnode.name === 'el-radio-group') {
return (
<el-radio {...{
props: {
key: i,
label: e.value,
...e.props
}
}}>
{e.label}
</el-radio>
);
} else if (vnode.name === 'el-checkbox-group') {
return (
<el-checkbox {...{
props: {
key: i,
label: e.value,
...e.props
}
}}>
{e.label}
</el-checkbox>
);
}
else {
return null
}
});
return parse_jsx(vnode, { prop, scope, children });
} else {
return parse_jsx(vnode, { prop, scope, $scopedSlots });
}
} else {
return <cl-error-message title={`组件渲染失败,组件 name 不能为空`} />;
}
}
}

43
webpack.config.js Normal file
View File

@ -0,0 +1,43 @@
const path = require("path");
const TerserPlugin = require("terser-webpack-plugin");
const resolve = (dir) => path.resolve(__dirname, dir);
const webpackConfig = {
devtool: "source-map",
mode: "production",
entry: "./src/index.js",
output: {
path: resolve("./dist"),
filename: "cl-crud2.min.js",
libraryTarget: "umd"
},
module: {
rules: [
{
test: /\.js$/,
loader: "babel-loader",
exclude: /node_modules/
},
{
test: /\.styl$/,
loaders: ["style-loader", "css-loader", "stylus-loader"]
}
]
},
resolve: {
alias: {
"@": resolve("src")
}
},
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
sourceMap: true
})
]
}
};
module.exports = webpackConfig;