[fix] init
This commit is contained in:
commit
1311e08484
4
.babelrc
Normal file
4
.babelrc
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"presets": ["@babel/preset-env", "@vue/babel-preset-jsx"],
|
||||
"plugins": ["@babel/plugin-transform-runtime"]
|
||||
}
|
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
/node_modules
|
||||
yarn-error.log
|
||||
yarn.lock
|
5
.idea/.gitignore
vendored
Normal file
5
.idea/.gitignore
vendored
Normal 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
12
.idea/cl-crud2-main.iml
Normal 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>
|
58
.idea/codeStyles/Project.xml
Normal file
58
.idea/codeStyles/Project.xml
Normal 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>
|
5
.idea/codeStyles/codeStyleConfig.xml
Normal file
5
.idea/codeStyles/codeStyleConfig.xml
Normal 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
8
.idea/modules.xml
Normal 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
9
.prettierrc
Normal 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
3
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"liveServer.settings.port": 5501
|
||||
}
|
46
README.md
Normal file
46
README.md
Normal 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
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
31
dist/cl-crud2.min.js.LICENSE.txt
vendored
Normal 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
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
938
example/index.html
Normal 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
12014
example/vue2.js
Normal file
File diff suppressed because it is too large
Load Diff
47
package.json
Normal file
47
package.json
Normal 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
70
src/app.js
Normal 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
423
src/assets/css/index.styl
Normal 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
11
src/common/index.js
Normal 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
28
src/components/add-btn.js
Normal 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
28
src/components/adv-btn.js
Normal 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>
|
||||
);
|
||||
}
|
||||
};
|
258
src/components/adv-search.js
Normal file
258
src/components/adv-search.js
Normal 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
338
src/components/crud.js
Normal 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
332
src/components/dialog.js
Normal 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>
|
||||
);
|
||||
}
|
||||
};
|
13
src/components/error-message.js
Normal file
13
src/components/error-message.js
Normal 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
18
src/components/filter.js
Normal 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
7
src/components/flex1.js
Normal 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
375
src/components/form.js
Normal 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
35
src/components/index.js
Normal 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
|
||||
};
|
29
src/components/multi-delete-btn.js
Normal file
29
src/components/multi-delete-btn.js
Normal 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>
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
72
src/components/pagination.js
Normal file
72
src/components/pagination.js
Normal 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
107
src/components/query.js
Normal 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>
|
||||
);
|
||||
}
|
||||
};
|
25
src/components/refresh-btn.js
Normal file
25
src/components/refresh-btn.js
Normal 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>
|
||||
);
|
||||
}
|
||||
};
|
128
src/components/search-key.js
Normal file
128
src/components/search-key.js
Normal 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
450
src/components/table.js
Normal 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
338
src/components/upsert.js
Normal 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
5
src/global.js
Normal 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
33
src/index.js
Normal 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
34
src/mixins/emitter.js
Normal 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
88
src/mixins/form.js
Normal 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
5
src/mixins/index.js
Normal 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
34
src/mixins/screen.js
Normal 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
148
src/utils/index.js
Normal 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
33
src/utils/parse.js
Normal 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
175
src/utils/vnode.js
Normal 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
43
webpack.config.js
Normal 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;
|
Loading…
Reference in New Issue
Block a user