mirror of
https://github.com/cool-team-official/cool-admin-vue.git
synced 2024-11-01 06:02:38 +08:00
发布7.x
This commit is contained in:
parent
76c7045cf8
commit
1735d6258e
@ -1,21 +1,5 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
/dist
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Log files
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
yarn.lock
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
5
.env
Normal file
5
.env
Normal file
@ -0,0 +1,5 @@
|
||||
# 应用名称
|
||||
VITE_NAME = "COOL-ADMIN"
|
||||
|
||||
# 网络超时请求时间
|
||||
VITE_TIMEOUT = 30000
|
@ -1,6 +1 @@
|
||||
/public/
|
||||
/dist/
|
||||
/node_modules/
|
||||
/src/icons/svg/
|
||||
/mock/
|
||||
vue.config.js
|
||||
vite.config.ts
|
67
.eslintrc.js
67
.eslintrc.js
@ -1,14 +1,67 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
node: true
|
||||
},
|
||||
extends: ["plugin:vue/essential", "@vue/prettier"],
|
||||
rules: {
|
||||
"no-console": "off",
|
||||
"comma-dangle": [2, "never"]
|
||||
browser: true,
|
||||
node: true,
|
||||
es6: true
|
||||
},
|
||||
parser: "vue-eslint-parser",
|
||||
parserOptions: {
|
||||
parser: "@typescript-eslint/parser"
|
||||
parser: "@typescript-eslint/parser",
|
||||
ecmaVersion: 2020,
|
||||
sourceType: "module",
|
||||
jsxPragma: "React",
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
tsx: true
|
||||
}
|
||||
},
|
||||
extends: [
|
||||
"plugin:vue/vue3-recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"prettier",
|
||||
"plugin:prettier/recommended"
|
||||
],
|
||||
rules: {
|
||||
"@typescript-eslint/ban-ts-ignore": "off",
|
||||
"@typescript-eslint/explicit-function-return-type": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-var-requires": "off",
|
||||
"@typescript-eslint/no-empty-function": "off",
|
||||
"vue/component-name-in-template-casing": ["error", "kebab-case"],
|
||||
"vue/component-definition-name-casing": ["error", "kebab-case"],
|
||||
"no-use-before-define": "off",
|
||||
"@typescript-eslint/no-use-before-define": "off",
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
"@typescript-eslint/ban-types": "off",
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||
"@typescript-eslint/no-namespace": "off",
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"error",
|
||||
{
|
||||
argsIgnorePattern: "^h$",
|
||||
varsIgnorePattern: "^h$"
|
||||
}
|
||||
],
|
||||
"no-unused-vars": [
|
||||
"error",
|
||||
{
|
||||
argsIgnorePattern: "^h$",
|
||||
varsIgnorePattern: "^h$"
|
||||
}
|
||||
],
|
||||
"space-before-function-paren": "off",
|
||||
"vue/attributes-order": "off",
|
||||
"vue/one-component-per-file": "off",
|
||||
"vue/html-closing-bracket-newline": "off",
|
||||
"vue/max-attributes-per-line": "off",
|
||||
"vue/multiline-html-element-content-newline": "off",
|
||||
"vue/multi-word-component-names": "off",
|
||||
"vue/singleline-html-element-content-newline": "off",
|
||||
"vue/attribute-hyphenation": "off",
|
||||
"vue/html-self-closing": "off",
|
||||
"vue/require-default-prop": "off",
|
||||
"vue/v-on-event-hyphenation": "off"
|
||||
}
|
||||
};
|
||||
|
4
.gitattributes
vendored
Normal file
4
.gitattributes
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
*.js text eol=lf
|
||||
*.json text eol=lf
|
||||
*.ts text eol=lf
|
||||
*.vue text eol=lf
|
24
.gitignore
vendored
24
.gitignore
vendored
@ -1,20 +1,6 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
/dist
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Log files
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
pnpm-lock.yaml
|
||||
|
16
.hintrc
Normal file
16
.hintrc
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"extends": [
|
||||
"development"
|
||||
],
|
||||
"hints": {
|
||||
"meta-viewport": "off",
|
||||
"axe/text-alternatives": [
|
||||
"default",
|
||||
{
|
||||
"document-title": "off"
|
||||
}
|
||||
],
|
||||
"disown-opener": "off",
|
||||
"css-prefix-order": "off"
|
||||
}
|
||||
}
|
@ -2,7 +2,6 @@
|
||||
"tabWidth": 4,
|
||||
"useTabs": true,
|
||||
"semi": true,
|
||||
"jsxBracketSameLine": true,
|
||||
"singleQuote": false,
|
||||
"printWidth": 100,
|
||||
"trailingComma": "none"
|
||||
|
15
.vscode/config.code-snippets
vendored
Normal file
15
.vscode/config.code-snippets
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"module-config": {
|
||||
"prefix": "module-config",
|
||||
"scope": "typescript",
|
||||
"body": [
|
||||
"import { ModuleConfig } from \"/@/cool\";",
|
||||
"",
|
||||
"export default (): ModuleConfig => {",
|
||||
" return {};",
|
||||
"};",
|
||||
""
|
||||
],
|
||||
"description": "module config snippets"
|
||||
}
|
||||
}
|
97
.vscode/crud.code-snippets
vendored
97
.vscode/crud.code-snippets
vendored
@ -1,10 +1,11 @@
|
||||
{
|
||||
"cl-crud": {
|
||||
"prefix": "cl-crud",
|
||||
"scope": "vue",
|
||||
"body": [
|
||||
"<template>",
|
||||
" <cl-crud ref=\"crud\" @load=\"onLoad\">",
|
||||
" <el-row type=\"flex\" align=\"middle\">",
|
||||
" <cl-crud ref=\"Crud\">",
|
||||
" <cl-row>",
|
||||
" <!-- 刷新按钮 -->",
|
||||
" <cl-refresh-btn />",
|
||||
" <!-- 新增按钮 -->",
|
||||
@ -14,48 +15,88 @@
|
||||
" <cl-flex1 />",
|
||||
" <!-- 关键字搜索 -->",
|
||||
" <cl-search-key />",
|
||||
" </el-row>",
|
||||
" </cl-row>",
|
||||
"",
|
||||
" <el-row>",
|
||||
" <cl-row>",
|
||||
" <!-- 数据表格 -->",
|
||||
" <cl-table v-bind=\"table\"></cl-table>",
|
||||
" </el-row>",
|
||||
" <cl-table ref=\"Table\" />",
|
||||
" </cl-row>",
|
||||
"",
|
||||
" <el-row type=\"flex\">",
|
||||
" <cl-row>",
|
||||
" <cl-flex1 />",
|
||||
" <!-- 分页控件 -->",
|
||||
" <cl-pagination />",
|
||||
" </el-row>",
|
||||
" </cl-row>",
|
||||
"",
|
||||
" <!-- 新增、编辑 -->",
|
||||
" <cl-upsert ref=\"upsert\" v-bind=\"upsert\"></cl-upsert>",
|
||||
" <cl-upsert ref=\"Upsert\" />",
|
||||
" </cl-crud>",
|
||||
"</template>",
|
||||
"",
|
||||
"<script>",
|
||||
"export default {",
|
||||
" data() {",
|
||||
" return {",
|
||||
" // 新增、编辑配置",
|
||||
" upsert: {",
|
||||
" items: []",
|
||||
" },",
|
||||
" // 表格配置",
|
||||
" table: {",
|
||||
" columns: []",
|
||||
" }",
|
||||
" };",
|
||||
"<script lang=\"ts\" name=\"菜单名称\" setup>",
|
||||
"import { useCrud, useTable, useUpsert } from \"@cool-vue/crud\";",
|
||||
"import { useCool } from \"/@/cool\";",
|
||||
"",
|
||||
"const { service } = useCool();",
|
||||
"",
|
||||
"// cl-upsert",
|
||||
"const Upsert = useUpsert({",
|
||||
" items: []",
|
||||
"});",
|
||||
"",
|
||||
"// cl-table",
|
||||
"const Table = useTable({",
|
||||
" columns: []",
|
||||
"});",
|
||||
"",
|
||||
"// cl-crud",
|
||||
"const Crud = useCrud(",
|
||||
" {",
|
||||
" service: service.demo.goods",
|
||||
" },",
|
||||
" methods: {",
|
||||
" onLoad({ ctx, app }) {",
|
||||
" ctx.service(${1}).done();",
|
||||
" app.refresh();",
|
||||
" }",
|
||||
" (app) => {",
|
||||
" app.refresh();",
|
||||
" }",
|
||||
"};",
|
||||
");",
|
||||
"",
|
||||
"// 刷新",
|
||||
"function refresh(params?: any) {",
|
||||
" Crud.value?.refresh(params);",
|
||||
"}",
|
||||
"</script>",
|
||||
""
|
||||
],
|
||||
"description": "cl-crud snippets"
|
||||
},
|
||||
"cl-filter": {
|
||||
"prefix": "cl-filter",
|
||||
"scope": "html",
|
||||
"body": [
|
||||
"<cl-filter label=\"\">",
|
||||
" <cl-select :options=\"[$1]\" prop=\"\" />",
|
||||
"</cl-filter>"
|
||||
],
|
||||
"description": "cl-filter snippets"
|
||||
},
|
||||
"item": {
|
||||
"prefix": "item",
|
||||
"scope": "typescript",
|
||||
"body": [
|
||||
"{",
|
||||
" label: \"$1\",",
|
||||
" prop: \"\",",
|
||||
" component: {",
|
||||
" name: \"\"",
|
||||
" }",
|
||||
"},",
|
||||
""
|
||||
],
|
||||
"description": "item snippets"
|
||||
},
|
||||
"column": {
|
||||
"prefix": "column",
|
||||
"scope": "typescript",
|
||||
"body": ["{", " label: \"$1\",", " prop: \"\",", "},", ""],
|
||||
"description": "column snippets"
|
||||
}
|
||||
}
|
||||
|
4
.vscode/settings.json
vendored
Normal file
4
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"editor.cursorSmoothCaretAnimation": "on",
|
||||
"editor.formatOnSave": true,
|
||||
}
|
@ -1,11 +1,11 @@
|
||||
FROM node:lts-alpine
|
||||
WORKDIR /build
|
||||
# 设置Node-Sass的镜像地址
|
||||
RUN npm config set sass_binary_site https://repo.huaweicloud.com/node-sass
|
||||
RUN npm config set sass_binary_site=https://npm.taobao.org/mirrors/node-sass/
|
||||
# 设置npm镜像
|
||||
RUN npm config set registry https://repo.huaweicloud.com/repository/npm/
|
||||
RUN npm config set registry https://registry.npm.taobao.org
|
||||
COPY package.json /build/package.json
|
||||
RUN npm install
|
||||
RUN yarn
|
||||
COPY ./ /build
|
||||
RUN npm run build
|
||||
|
||||
@ -13,4 +13,4 @@ FROM nginx
|
||||
RUN mkdir /app
|
||||
COPY --from=0 /build/dist /app
|
||||
COPY --from=0 /build/nginx.conf /etc/nginx/nginx.conf
|
||||
EXPOSE 80
|
||||
EXPOSE 80
|
||||
|
258
README.md
258
README.md
@ -1,10 +1,10 @@
|
||||
# cool-admin [vue2]
|
||||
# cool-admin [vue3 - ts - vite]
|
||||
|
||||
<p align="center">
|
||||
<a href="https://show.cool-admin.com/" target="blank"><img src="https://admin.cool-js.com/logo.png" width="200" alt="cool-admin Logo" /></a>
|
||||
</p>
|
||||
|
||||
<p align="center">cool-admin 一个很酷的后台权限管理系统,开源免费,模块化、插件化、极速开发 CRUD,方便快速构建迭代后台管理系统, 到论坛 进一步了解</p>
|
||||
<p align="center">cool-admin 一个很酷的后台权限管理系统,开源免费,模块化、插件化、极速开发 CRUD,方便快速构建迭代后台管理系统, 到<a href="https://cool-js.com" target="_blank">文档</a> 进一步了解</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/cool-team-official/cool-admin-vue/blob/master/LICENSE" target="_blank"><img src="https://img.shields.io/badge/license-MIT-green?style=flat-square" alt="GitHub license" />
|
||||
@ -14,11 +14,9 @@
|
||||
|
||||
## 地址
|
||||
|
||||
- [⚡️ vue2.x + element-ui](https://github.com/cool-team-official/cool-admin-vue)
|
||||
- [📌 v6 vue3 + element-plus + ts + vite](https://github.com/cool-team-official/cool-admin-vue/tree/6.x)
|
||||
|
||||
- [⚡️ vue3.x + element-plus + ts + webpack](https://github.com/cool-team-official/cool-admin-vue/tree/vue3-ts-webpack)
|
||||
|
||||
- [📌 vue3.x + element-plus + ts + vite](https://github.com/cool-team-official/cool-admin-vue/tree/vue3-ts-vite)
|
||||
- [⚡️ v5 vue3 + element-plus + ts + vite](https://github.com/cool-team-official/cool-admin-vue/tree/5.x)
|
||||
|
||||
- [🌐 码云仓库地址](https://gitee.com/cool-team-official/cool-admin-vue)
|
||||
|
||||
@ -38,18 +36,6 @@
|
||||
|
||||
<img width="260" src="https://cool-show.oss-cn-shanghai.aliyuncs.com/admin/wechat.jpeg" alt="Admin Wechat"></a>
|
||||
|
||||
## 微信公众号
|
||||
|
||||
<img width="260" src="https://cool-show.oss-cn-shanghai.aliyuncs.com/admin/mp.jpg" alt="Admin Wechat"></a>
|
||||
|
||||
## 在线社区
|
||||
|
||||
[https://bbs.cool-js.com/](https://bbs.cool-js.com/)
|
||||
|
||||
## 使用条件
|
||||
|
||||
请确保您的操作系统上安装了 Node.js(> = 8.9.0)、@vue/cli (> 3.0.0)。
|
||||
|
||||
## 安装项目依赖
|
||||
|
||||
推荐使用 `yarn`:
|
||||
@ -58,244 +44,14 @@
|
||||
yarn
|
||||
```
|
||||
|
||||
解决 `node-sass` 网络慢的方法:
|
||||
|
||||
```shell
|
||||
yarn config set sass-binary-site http://npm.taobao.org/mirrors/node-sass
|
||||
```
|
||||
|
||||
## 运行应用程序
|
||||
|
||||
安装过程完成后,运行以下命令启动服务。您可以在浏览器中预览网站 [http://localhost:9000](http://localhost:9000)
|
||||
|
||||
```shell
|
||||
yarn serve
|
||||
yarn dev
|
||||
```
|
||||
|
||||
## 极速 CRUD
|
||||
### 低价服务器
|
||||
|
||||
1. `vscode` 编辑器下输入关键字 `cl-crud`,会生成对应的模板代码:
|
||||
|
||||
```html
|
||||
<template>
|
||||
<cl-crud ref="crud" @load="onLoad">
|
||||
<el-row type="flex" align="middle">
|
||||
<!-- 刷新按钮 -->
|
||||
<cl-refresh-btn />
|
||||
<!-- 新增按钮 -->
|
||||
<cl-add-btn />
|
||||
<!-- 删除按钮 -->
|
||||
<cl-multi-delete-btn />
|
||||
<cl-flex1 />
|
||||
<!-- 关键字搜索 -->
|
||||
<cl-search-key />
|
||||
</el-row>
|
||||
|
||||
<el-row>
|
||||
<!-- 数据表格 -->
|
||||
<cl-table v-bind="table"></cl-table>
|
||||
</el-row>
|
||||
|
||||
<el-row type="flex">
|
||||
<cl-flex1 />
|
||||
<!-- 分页控件 -->
|
||||
<cl-pagination />
|
||||
</el-row>
|
||||
|
||||
<!-- 新增、编辑 -->
|
||||
<cl-upsert ref="upsert" v-bind="upsert"></cl-upsert>
|
||||
</cl-crud>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
// 新增、编辑配置
|
||||
upsert: {
|
||||
items: []
|
||||
},
|
||||
// 表格配置
|
||||
table: {
|
||||
columns: []
|
||||
}
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
onLoad({ ctx, app }) {
|
||||
// crud 配置
|
||||
ctx.service().done();
|
||||
// 发送 page 接口请求
|
||||
app.refresh();
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
```
|
||||
|
||||
2. 编辑数据表格 `cl-table`:
|
||||
|
||||
```js
|
||||
{
|
||||
table: {
|
||||
// 参数与 el-table-column 一致,并支持许多骚操作
|
||||
columns: [
|
||||
// 多选列
|
||||
{
|
||||
type: "selection",
|
||||
width: 60
|
||||
},
|
||||
// 自定义列
|
||||
{
|
||||
label: "昵称",
|
||||
prop: "name"
|
||||
},
|
||||
{
|
||||
label: "账户",
|
||||
prop: "price",
|
||||
sortable: "custom" // 是否添加排序
|
||||
},
|
||||
{
|
||||
label: "状态",
|
||||
prop: "status",
|
||||
// 字典匹配,使用 el-tag 展示
|
||||
dict: [
|
||||
{
|
||||
label: "启用",
|
||||
value: 1,
|
||||
type: "primary"
|
||||
},
|
||||
{
|
||||
label: "禁用",
|
||||
value: 0,
|
||||
type: "danger"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: "创建时间",
|
||||
prop: "createTime"
|
||||
},
|
||||
// 操作按钮列,默认包含:编辑、删除
|
||||
{
|
||||
type: "op"
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. 编辑新增、编辑表单 `cl-upsert`:
|
||||
|
||||
```js
|
||||
{
|
||||
upsert: {
|
||||
items: [
|
||||
{
|
||||
label: "昵称",
|
||||
prop: "name",
|
||||
// 参数与 el-form-item 一致
|
||||
props: {},
|
||||
value: "神仙都没用", // 昵称默认值
|
||||
// 渲染参数,支持 slot, 组件实例,jsx
|
||||
component: {
|
||||
name: "el-input", // 可以是任意已注册的组件名
|
||||
props: {}, // 组件的参数
|
||||
on: {} // 组件的回调事件
|
||||
},
|
||||
// 验证规则,与 el-form 一致
|
||||
rules: {
|
||||
required: true,
|
||||
message: "昵称不呢为空"
|
||||
}
|
||||
},
|
||||
{
|
||||
label: "存款",
|
||||
prop: "price",
|
||||
component: {
|
||||
name: "el-input-number",
|
||||
props: {
|
||||
min: 0,
|
||||
max: 10000
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
label: "状态",
|
||||
prop: "status",
|
||||
value: 1,
|
||||
component: {
|
||||
name: "el-radio-group",
|
||||
options: [
|
||||
{
|
||||
label: "启用",
|
||||
value: 1
|
||||
},
|
||||
{
|
||||
label: "禁用",
|
||||
value: 0
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
4. 绑定 `service`。在 `src/service/` 下新建文件 `test.js`,并编辑:
|
||||
|
||||
```js
|
||||
// src/service/test.js
|
||||
import { BaseService, Service, Permission } from "cl-admin";
|
||||
|
||||
// 请求接口的路径
|
||||
@Service("test")
|
||||
class Test extends BaseService {
|
||||
// 继承 BaseService 后,拥有 page, list, add, delete, update, info 6个基本接口
|
||||
|
||||
// 自定义其他接口
|
||||
@Permission("product") // 权限装饰器,可选
|
||||
product(id) {
|
||||
// this.request() 参数与 axios 一致
|
||||
return this.request({
|
||||
url: "/product",
|
||||
method: "POST",
|
||||
data: {
|
||||
id
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default Test;
|
||||
```
|
||||
|
||||
在 `src/service/` 下的文件,框架会自动根据 `目录结构` 和 `文件名称` 格式化成对应的 `$service` 对象。你现在可以这么使用它:
|
||||
|
||||
```js
|
||||
this.$service.test.page({ page: 1 });
|
||||
this.$service.test.product(1);
|
||||
```
|
||||
|
||||
`service` 编写好后,我们把它绑定在 `crud` 上:
|
||||
|
||||
```js
|
||||
export default {
|
||||
methods: {
|
||||
onLoad({ ctx, app }) {
|
||||
// 绑定 service,这边指定到 test 即可
|
||||
ctx.service(this.$service.test).done();
|
||||
|
||||
// 发起 page 请求
|
||||
app.refresh({
|
||||
// 请求默认参数
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
5. 效果预览
|
||||
|
||||
![](https://cool-show.oss-cn-shanghai.aliyuncs.com/admin/crud.png)
|
||||
[阿里云、腾讯云、华为云低价云服务器,不限新老](https://cool-js.com/ad/server.html)
|
||||
|
@ -1,13 +0,0 @@
|
||||
module.exports = {
|
||||
presets: ["@vue/app"],
|
||||
plugins: [
|
||||
["jsx-v-model"],
|
||||
[
|
||||
"component",
|
||||
{
|
||||
libraryName: "element-ui",
|
||||
styleLibraryName: "theme-chalk"
|
||||
}
|
||||
]
|
||||
]
|
||||
};
|
39
build/cool/eps/config.ts
Normal file
39
build/cool/eps/config.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { join } from "path";
|
||||
|
||||
// 打包路径
|
||||
export const DistPath = join(__dirname, "../dist");
|
||||
|
||||
// 实体描述
|
||||
export const Entity = {
|
||||
mapping: [
|
||||
{
|
||||
// 自定义匹配
|
||||
custom: ({ propertyName, type }) => {
|
||||
// status 原本是tinyint,如果是1的话,== true 是可以的,但是不能 === true,请谨慎使用
|
||||
if (propertyName === "status" && type == "tinyint") return "boolean";
|
||||
// 如果没有,返回null或者不返回,则继续遍历其他匹配规则
|
||||
return null;
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "string",
|
||||
test: ["varchar", "text", "simple-json"]
|
||||
},
|
||||
{
|
||||
type: "string[]",
|
||||
test: ["simple-array"]
|
||||
},
|
||||
{
|
||||
type: "Date",
|
||||
test: ["datetime", "date"]
|
||||
},
|
||||
{
|
||||
type: "number",
|
||||
test: ["tinyint", "int", "decimal"]
|
||||
},
|
||||
{
|
||||
type: "BigInt",
|
||||
test: ["bigint"]
|
||||
}
|
||||
]
|
||||
};
|
450
build/cool/eps/index.ts
Normal file
450
build/cool/eps/index.ts
Normal file
@ -0,0 +1,450 @@
|
||||
import { createDir, error, firstUpperCase, readFile, toCamel } from "../utils";
|
||||
import { join } from "path";
|
||||
import { Entity, DistPath } from "./config";
|
||||
import axios from "axios";
|
||||
import { isArray, isEmpty, last } from "lodash";
|
||||
import { createWriteStream } from "fs";
|
||||
import prettier from "prettier";
|
||||
import { proxy } from "../../../src/config/proxy";
|
||||
|
||||
// 实体类型
|
||||
type Entity = {
|
||||
api: {
|
||||
dts: {
|
||||
parameters?: {
|
||||
description: string;
|
||||
name: string;
|
||||
required: boolean;
|
||||
schema: {
|
||||
type: string;
|
||||
};
|
||||
}[];
|
||||
};
|
||||
name: string;
|
||||
method: string;
|
||||
path: string;
|
||||
prefix: string;
|
||||
summary: string;
|
||||
tag: string;
|
||||
}[];
|
||||
columns: {
|
||||
comment: string;
|
||||
length: string;
|
||||
nullable: boolean;
|
||||
propertyName: string;
|
||||
type: string;
|
||||
}[];
|
||||
module: string;
|
||||
name: string;
|
||||
prefix: string;
|
||||
};
|
||||
|
||||
// 获取方法名
|
||||
function getNames(v: any) {
|
||||
return Object.keys(v).filter((e) => !["namespace", "permission"].includes(e));
|
||||
}
|
||||
|
||||
// 获取数据
|
||||
async function getData(temps: any[]) {
|
||||
let list: Entity[] = [];
|
||||
|
||||
// 本地文件
|
||||
try {
|
||||
list = JSON.parse(readFile(join(DistPath, "eps.json")) || "[]");
|
||||
} catch (err) {
|
||||
error(`[eps] ${join(DistPath, "eps.json")} 文件异常, ${err.message}`);
|
||||
}
|
||||
|
||||
// 远程数据
|
||||
const url = proxy["/dev/"].target + "/admin/base/open/eps";
|
||||
|
||||
await axios
|
||||
.get(url, {
|
||||
timeout: 5000
|
||||
})
|
||||
.then((res) => {
|
||||
const { code, data, message } = res.data;
|
||||
|
||||
if (code === 1000) {
|
||||
if (!isEmpty(data) && data) {
|
||||
// @ts-ignore
|
||||
list = Object.values(data).flat();
|
||||
}
|
||||
} else {
|
||||
error(`[eps] ${message}`);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
error(`[eps] 获取失败, ${url} 无法访问!`);
|
||||
});
|
||||
|
||||
return [...list, ...temps];
|
||||
}
|
||||
|
||||
// 创建数据文件
|
||||
function createJson(eps: Entity[]) {
|
||||
createWriteStream(join(DistPath, "eps.json"), {
|
||||
flags: "w"
|
||||
}).write(
|
||||
JSON.stringify(
|
||||
eps.map((e) => {
|
||||
return {
|
||||
prefix: e.prefix,
|
||||
name: e.name || "",
|
||||
api: e.api.map((e) => {
|
||||
return {
|
||||
name: e.name,
|
||||
method: e.method,
|
||||
path: e.path
|
||||
};
|
||||
})
|
||||
};
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// 创建描述文件
|
||||
async function createDescribe({ list, service }: { list: Entity[]; service: any }) {
|
||||
// 获取类型
|
||||
function getType({ propertyName, type }) {
|
||||
for (const map of Entity.mapping) {
|
||||
if (map.custom) {
|
||||
const resType = map.custom({ propertyName, type });
|
||||
if (resType) return resType;
|
||||
}
|
||||
if (map.test) {
|
||||
if (map.test.includes(type)) return map.type;
|
||||
}
|
||||
}
|
||||
return type;
|
||||
}
|
||||
|
||||
// 创建 Entity
|
||||
function createEntity() {
|
||||
const t0: string[][] = [];
|
||||
|
||||
for (const item of list) {
|
||||
if (!item.name) continue;
|
||||
const t = [`interface ${item.name} {`];
|
||||
for (const col of item.columns || []) {
|
||||
// 描述
|
||||
t.push("\n");
|
||||
t.push("/**\n");
|
||||
t.push(` * ${col.comment}\n`);
|
||||
t.push(" */\n");
|
||||
t.push(
|
||||
`${col.propertyName}?: ${getType({
|
||||
propertyName: col.propertyName,
|
||||
type: col.type
|
||||
})};`
|
||||
);
|
||||
}
|
||||
t.push("\n");
|
||||
t.push("/**\n");
|
||||
t.push(` * 任意键值\n`);
|
||||
t.push(" */\n");
|
||||
t.push(`[key: string]: any;`);
|
||||
t.push("}");
|
||||
t0.push(t);
|
||||
}
|
||||
|
||||
return t0.map((e) => e.join("")).join("\n\n");
|
||||
}
|
||||
|
||||
// 创建 Service
|
||||
function createDts() {
|
||||
const t0: string[][] = [];
|
||||
|
||||
const t1 = [
|
||||
`
|
||||
type json = any;
|
||||
|
||||
type Service = {
|
||||
request(options?: {
|
||||
url: string;
|
||||
method?: "POST" | "GET" | "PUT" | "DELETE" | "PATCH" | "HEAD" | "OPTIONS";
|
||||
data?: any;
|
||||
params?: any;
|
||||
headers?: {
|
||||
[key: string]: any;
|
||||
},
|
||||
timeout?: number;
|
||||
proxy?: boolean;
|
||||
[key: string]: any;
|
||||
}): Promise<any>;
|
||||
`
|
||||
];
|
||||
|
||||
// 处理数据
|
||||
function deep(d: any, k?: string) {
|
||||
if (!k) k = "";
|
||||
|
||||
for (const i in d) {
|
||||
const name = k + toCamel(firstUpperCase(i.replace(/[:]/g, "")));
|
||||
|
||||
if (d[i].namespace) {
|
||||
// 查找配置
|
||||
const item = list.find((e) => (e.prefix || "").includes(d[i].namespace));
|
||||
|
||||
if (item) {
|
||||
const t = [`interface ${name} {`];
|
||||
|
||||
t1.push(`${i}: ${name};`);
|
||||
|
||||
// 插入方法
|
||||
if (item.api) {
|
||||
// 权限列表
|
||||
const permission: string[] = [];
|
||||
|
||||
item.api.forEach((a) => {
|
||||
// 方法名
|
||||
const n = toCamel(a.name || last(a.path.split("/")) || "").replace(
|
||||
/[:\/-]/g,
|
||||
""
|
||||
);
|
||||
|
||||
if (n) {
|
||||
// 参数类型
|
||||
let q: string[] = [];
|
||||
|
||||
// 参数列表
|
||||
const { parameters = [] } = a.dts || {};
|
||||
|
||||
parameters.forEach((p) => {
|
||||
if (p.description) {
|
||||
q.push(`\n/** ${p.description} */\n`);
|
||||
}
|
||||
|
||||
if (p.name.includes(":")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const a = `${p.name}${p.required ? "" : "?"}`;
|
||||
const b = `${p.schema.type || "string"}`;
|
||||
|
||||
q.push(`${a}: ${b},`);
|
||||
});
|
||||
|
||||
if (isEmpty(q)) {
|
||||
q = ["any"];
|
||||
} else {
|
||||
q.unshift("{");
|
||||
q.push("}");
|
||||
}
|
||||
|
||||
// 返回类型
|
||||
let res = "";
|
||||
|
||||
// 实体名
|
||||
const en = item.name || "any";
|
||||
|
||||
switch (a.path) {
|
||||
case "/page":
|
||||
res = `
|
||||
{
|
||||
pagination: { size: number; page: number; total: number; [key: string]: any };
|
||||
list: ${en} [];
|
||||
[key: string]: any;
|
||||
}
|
||||
`;
|
||||
break;
|
||||
|
||||
case "/list":
|
||||
res = `${en} []`;
|
||||
break;
|
||||
|
||||
case "/info":
|
||||
res = en;
|
||||
break;
|
||||
|
||||
default:
|
||||
res = "any";
|
||||
break;
|
||||
}
|
||||
|
||||
// 描述
|
||||
t.push("\n");
|
||||
t.push("/**\n");
|
||||
t.push(` * ${a.summary || n}\n`);
|
||||
t.push(" */\n");
|
||||
|
||||
t.push(
|
||||
`${n}(data${q.length == 1 ? "?" : ""}: ${q.join(
|
||||
""
|
||||
)}): Promise<${res}>;`
|
||||
);
|
||||
}
|
||||
|
||||
permission.push(n);
|
||||
});
|
||||
|
||||
// 权限标识
|
||||
t.push("\n");
|
||||
t.push("/**\n");
|
||||
t.push(" * 权限标识\n");
|
||||
t.push(" */\n");
|
||||
t.push(
|
||||
`permission: { ${permission
|
||||
.map((e) => `${e}: string;`)
|
||||
.join("\n")} };`
|
||||
);
|
||||
|
||||
// 权限状态
|
||||
t.push("\n");
|
||||
t.push("/**\n");
|
||||
t.push(" * 权限状态\n");
|
||||
t.push(" */\n");
|
||||
t.push(
|
||||
`_permission: { ${permission
|
||||
.map((e) => `${e}: boolean;`)
|
||||
.join("\n")} };`
|
||||
);
|
||||
|
||||
// 请求
|
||||
t.push("\n");
|
||||
t.push("/**\n");
|
||||
t.push(" * 请求\n");
|
||||
t.push(" */\n");
|
||||
t.push(`request: Service['request']`);
|
||||
}
|
||||
|
||||
t.push("}");
|
||||
t0.push(t);
|
||||
}
|
||||
} else {
|
||||
t1.push(`${i}: {`);
|
||||
deep(d[i], name);
|
||||
t1.push(`},`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 深度
|
||||
deep(service);
|
||||
|
||||
// 结束
|
||||
t1.push("}");
|
||||
|
||||
// 追加
|
||||
t0.push(t1);
|
||||
|
||||
return t0.map((e) => e.join("")).join("\n\n");
|
||||
}
|
||||
|
||||
// 文件内容
|
||||
const text = `
|
||||
declare namespace Eps {
|
||||
${createEntity()}
|
||||
${createDts()}
|
||||
}
|
||||
`;
|
||||
|
||||
// 文本内容
|
||||
const content = await prettier.format(text, {
|
||||
parser: "typescript",
|
||||
useTabs: true,
|
||||
tabWidth: 4,
|
||||
endOfLine: "lf",
|
||||
semi: true,
|
||||
singleQuote: false,
|
||||
printWidth: 100,
|
||||
trailingComma: "none"
|
||||
});
|
||||
|
||||
// 创建 eps 描述文件
|
||||
createWriteStream(join(DistPath, "eps.d.ts"), {
|
||||
flags: "w"
|
||||
}).write(content);
|
||||
}
|
||||
|
||||
// 创建服务
|
||||
function createService(data: Entity[]) {
|
||||
const list: Entity[] = [];
|
||||
const service = {};
|
||||
const d = { data };
|
||||
|
||||
for (const i in d) {
|
||||
if (isArray(d[i])) {
|
||||
d[i].forEach((e: Entity) => {
|
||||
// 分隔路径
|
||||
const arr = e.prefix
|
||||
.replace(/\//, "")
|
||||
.replace("admin", "")
|
||||
.split("/")
|
||||
.filter(Boolean)
|
||||
.map(toCamel);
|
||||
|
||||
// 遍历
|
||||
function deep(d: any, i: number) {
|
||||
const k = arr[i];
|
||||
|
||||
if (k) {
|
||||
// 是否最后一个
|
||||
if (arr[i + 1]) {
|
||||
if (!d[k]) {
|
||||
d[k] = {};
|
||||
}
|
||||
|
||||
deep(d[k], i + 1);
|
||||
} else {
|
||||
// 本地不存在则创建实例
|
||||
if (!d[k]) {
|
||||
d[k] = {
|
||||
namespace: e.prefix.substring(1, e.prefix.length),
|
||||
permission: {}
|
||||
};
|
||||
}
|
||||
|
||||
// 创建方法
|
||||
e.api.forEach((a) => {
|
||||
// 方法名
|
||||
const n = a.path.replace("/", "");
|
||||
|
||||
if (n && !/[-:]/g.test(n)) {
|
||||
d[k][n] = a;
|
||||
}
|
||||
});
|
||||
|
||||
// 创建权限
|
||||
getNames(d[k]).forEach((e) => {
|
||||
d[k].permission[e] = `${d[k].namespace.replace(
|
||||
"admin/",
|
||||
""
|
||||
)}/${e}`.replace(/\//g, ":");
|
||||
});
|
||||
|
||||
list.push(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deep(service, 0);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return { service, list };
|
||||
}
|
||||
|
||||
// 创建 eps
|
||||
export async function createEps(query?: { list: any[] }) {
|
||||
// 获取数据
|
||||
const data = await getData(query?.list || []);
|
||||
|
||||
// 生成数据
|
||||
const { service, list } = createService(data);
|
||||
|
||||
// 创建临时目录
|
||||
createDir(DistPath);
|
||||
|
||||
// 创建数据文件
|
||||
createJson(data);
|
||||
|
||||
// 创建描述文件
|
||||
createDescribe({ service, list });
|
||||
|
||||
return `
|
||||
export const eps = ${JSON.stringify({ service, list })}
|
||||
`;
|
||||
}
|
73
build/cool/index.ts
Normal file
73
build/cool/index.ts
Normal file
@ -0,0 +1,73 @@
|
||||
import { Plugin } from "vite";
|
||||
import { createSvg } from "./svg";
|
||||
import { createTag } from "./tag";
|
||||
import { createEps } from "./eps";
|
||||
import { createModule } from "./module";
|
||||
import { createMenu } from "./menu";
|
||||
import { parseJson } from "./utils";
|
||||
|
||||
export function cool(): Plugin {
|
||||
// 虚拟模块
|
||||
const virtualModuleIds = ["virtual:eps", "virtual:module"];
|
||||
|
||||
return {
|
||||
name: "vite-cool",
|
||||
enforce: "pre",
|
||||
configureServer(server) {
|
||||
server.middlewares.use(async (req, res, next) => {
|
||||
function done(data: any) {
|
||||
res.writeHead(200, { "Content-Type": "text/html;charset=UTF-8" });
|
||||
res.end(JSON.stringify(data));
|
||||
}
|
||||
|
||||
if (req.url?.includes("__cool")) {
|
||||
const body = await parseJson(req);
|
||||
|
||||
switch (req.url) {
|
||||
// 快速创建菜单
|
||||
case "/__cool_createMenu":
|
||||
await createMenu(body);
|
||||
break;
|
||||
|
||||
// 创建描述文件
|
||||
case "/__cool_eps":
|
||||
await createEps(body);
|
||||
break;
|
||||
|
||||
default:
|
||||
return done({
|
||||
code: 1001,
|
||||
message: "未知请求"
|
||||
});
|
||||
}
|
||||
|
||||
done({
|
||||
code: 1000
|
||||
});
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
});
|
||||
},
|
||||
transform(code, id) {
|
||||
return createTag(code, id);
|
||||
},
|
||||
transformIndexHtml(html) {
|
||||
return createSvg(html);
|
||||
},
|
||||
resolveId(id) {
|
||||
if (virtualModuleIds.includes(id)) {
|
||||
return "\0" + id;
|
||||
}
|
||||
},
|
||||
async load(id) {
|
||||
if (id === "\0virtual:eps") {
|
||||
return createEps();
|
||||
}
|
||||
|
||||
if (id === "\0virtual:module") {
|
||||
return createModule();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
34
build/cool/menu/index.ts
Normal file
34
build/cool/menu/index.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import { createWriteStream } from "fs";
|
||||
import prettier from "prettier";
|
||||
import { join } from "path";
|
||||
import { mkdirs } from "../utils";
|
||||
|
||||
// 创建文件
|
||||
export async function createMenu(options: { viewPath: string; code: string }) {
|
||||
// 格式化内容
|
||||
const content = prettier.format(options.code, {
|
||||
parser: "vue",
|
||||
useTabs: true,
|
||||
tabWidth: 4,
|
||||
endOfLine: "lf",
|
||||
semi: true,
|
||||
jsxBracketSameLine: true,
|
||||
singleQuote: false,
|
||||
printWidth: 100,
|
||||
trailingComma: "none"
|
||||
});
|
||||
|
||||
// 目录路径
|
||||
const dir = (options.viewPath || "").split("/");
|
||||
|
||||
// 文件名
|
||||
const fname = dir.pop();
|
||||
|
||||
// 创建目录
|
||||
const path = mkdirs(`./src/${dir.join("/")}`);
|
||||
|
||||
// 创建文件
|
||||
createWriteStream(join(path, fname || "demo"), {
|
||||
flags: "w"
|
||||
}).write(content);
|
||||
}
|
14
build/cool/module/index.ts
Normal file
14
build/cool/module/index.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import fs from "fs";
|
||||
|
||||
export function createModule() {
|
||||
let dirs: string[] = [];
|
||||
|
||||
try {
|
||||
dirs = fs.readdirSync("./src/modules");
|
||||
dirs = dirs.filter((e) => !e.includes("."));
|
||||
} catch (err) {}
|
||||
|
||||
return `
|
||||
export const dirs = ${JSON.stringify(dirs)}
|
||||
`;
|
||||
}
|
54
build/cool/svg/index.ts
Normal file
54
build/cool/svg/index.ts
Normal file
@ -0,0 +1,54 @@
|
||||
import { readFileSync, readdirSync } from "fs";
|
||||
import { extname } from "path";
|
||||
|
||||
function findFiles(dir: string): string[] {
|
||||
const res: string[] = [];
|
||||
const dirs = readdirSync(dir, {
|
||||
withFileTypes: true
|
||||
});
|
||||
for (const d of dirs) {
|
||||
if (d.isDirectory()) {
|
||||
res.push(...findFiles(dir + d.name + "/"));
|
||||
} else {
|
||||
if (extname(d.name) == ".svg") {
|
||||
const svg = readFileSync(dir + d.name)
|
||||
.toString()
|
||||
.replace(/(\r)|(\n)/g, "")
|
||||
.replace(/<svg([^>+].*?)>/, (_: any, $2: any) => {
|
||||
let width = 0;
|
||||
let height = 0;
|
||||
let content = $2.replace(
|
||||
/(width|height)="([^>+].*?)"/g,
|
||||
(_: any, s2: any, s3: any) => {
|
||||
if (s2 === "width") {
|
||||
width = s3;
|
||||
} else if (s2 === "height") {
|
||||
height = s3;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
);
|
||||
if (!/(viewBox="[^>+].*?")/g.test($2)) {
|
||||
content += `viewBox="0 0 ${width} ${height}"`;
|
||||
}
|
||||
return `<symbol id="icon-${d.name.replace(".svg", "")}" ${content}>`;
|
||||
})
|
||||
.replace("</svg>", "</symbol>");
|
||||
res.push(svg);
|
||||
}
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
export function createSvg(html: string) {
|
||||
const res = findFiles("./src/modules/");
|
||||
|
||||
return html.replace(
|
||||
"<body>",
|
||||
`<body>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="position: absolute; width: 0; height: 0">
|
||||
${res.join("")}
|
||||
</svg>`
|
||||
);
|
||||
}
|
32
build/cool/tag/index.ts
Normal file
32
build/cool/tag/index.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { parse, compileScript } from "@vue/compiler-sfc";
|
||||
import magicString from "magic-string";
|
||||
|
||||
export function createTag(code: string, id: string) {
|
||||
if (/\.vue$/.test(id)) {
|
||||
let s: any;
|
||||
const str = () => s || (s = new magicString(code));
|
||||
const { descriptor } = parse(code);
|
||||
|
||||
if (!descriptor.script && descriptor.scriptSetup) {
|
||||
const res = compileScript(descriptor, { id });
|
||||
const { name, lang }: any = res.attrs;
|
||||
|
||||
str().appendLeft(
|
||||
0,
|
||||
`<script lang="${lang}">
|
||||
import { defineComponent } from 'vue'
|
||||
export default defineComponent({
|
||||
name: "${name}"
|
||||
})
|
||||
<\/script>`
|
||||
);
|
||||
|
||||
return {
|
||||
map: str().generateMap(),
|
||||
code: str().toString()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
74
build/cool/utils/index.ts
Normal file
74
build/cool/utils/index.ts
Normal file
@ -0,0 +1,74 @@
|
||||
import fs from "fs";
|
||||
import { join } from "path";
|
||||
|
||||
// 首字母大写
|
||||
export function firstUpperCase(value: string): string {
|
||||
return value.replace(/\b(\w)(\w*)/g, function ($0, $1, $2) {
|
||||
return $1.toUpperCase() + $2;
|
||||
});
|
||||
}
|
||||
|
||||
// 横杠转驼峰
|
||||
export function toCamel(str: string): string {
|
||||
return str.replace(/([^-])(?:-+([^-]))/g, function ($0, $1, $2) {
|
||||
return $1 + $2.toUpperCase();
|
||||
});
|
||||
}
|
||||
|
||||
// 创建目录
|
||||
export function createDir(path: string) {
|
||||
if (!fs.existsSync(path)) fs.mkdirSync(path);
|
||||
}
|
||||
|
||||
// 读取文件
|
||||
export function readFile(name: string) {
|
||||
try {
|
||||
return fs.readFileSync(name, "utf8");
|
||||
} catch (e) {}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
// 解析body
|
||||
export function parseJson(req: any): Promise<any> {
|
||||
return new Promise((resolve) => {
|
||||
let d = "";
|
||||
req.on("data", function (chunk: Buffer) {
|
||||
d += chunk;
|
||||
});
|
||||
req.on("end", function () {
|
||||
try {
|
||||
resolve(JSON.parse(d));
|
||||
} catch {
|
||||
resolve({});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 深度创建目录
|
||||
export function mkdirs(path: string) {
|
||||
const arr = path.split("/");
|
||||
let p = "";
|
||||
|
||||
arr.forEach((e) => {
|
||||
const t = join(p, e);
|
||||
|
||||
try {
|
||||
fs.statSync(t);
|
||||
} catch (err) {
|
||||
try {
|
||||
fs.mkdirSync(t);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
p = t;
|
||||
});
|
||||
|
||||
return p;
|
||||
}
|
||||
|
||||
export function error(message: string) {
|
||||
console.log("\x1B[31m%s\x1B[0m", message);
|
||||
}
|
173
index.html
Normal file
173
index.html
Normal file
@ -0,0 +1,173 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="referer" content="never" />
|
||||
<meta name="renderer" content="webkit" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=0"
|
||||
/>
|
||||
<title></title>
|
||||
<link rel="icon" href="./favicon.ico" />
|
||||
<style>
|
||||
html,
|
||||
body,
|
||||
#app {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB",
|
||||
"Microsoft YaHei", Arial, sans-serif;
|
||||
}
|
||||
|
||||
.preload__wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
letter-spacing: 1px;
|
||||
background-color: #2f3447;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
z-index: 9999;
|
||||
transition: all 0.3s ease-in;
|
||||
opacity: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.preload__wrap.is-hide {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.preload__container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.preload__name {
|
||||
font-size: 30px;
|
||||
color: #fff;
|
||||
letter-spacing: 5px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.preload__title {
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
margin: 30px 0 20px 0;
|
||||
}
|
||||
|
||||
.preload__sub-title {
|
||||
color: #ababab;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.preload__footer {
|
||||
text-align: center;
|
||||
padding: 10px 0 20px 0;
|
||||
}
|
||||
|
||||
.preload__footer a {
|
||||
font-size: 12px;
|
||||
color: #ababab;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.preload__loading {
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
border-radius: 30px;
|
||||
border: 7px solid currentColor;
|
||||
border-bottom-color: #2f3447 !important;
|
||||
position: relative;
|
||||
animation:
|
||||
r 1s infinite cubic-bezier(0.17, 0.67, 0.83, 0.67),
|
||||
bc 2s infinite ease-in;
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
@keyframes r {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.preload__loading::after,
|
||||
.preload__loading::before {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
bottom: -2px;
|
||||
height: 7px;
|
||||
width: 7px;
|
||||
border-radius: 10px;
|
||||
background-color: currentColor;
|
||||
}
|
||||
|
||||
.preload__loading::after {
|
||||
left: -1px;
|
||||
}
|
||||
|
||||
.preload__loading::before {
|
||||
right: -1px;
|
||||
}
|
||||
|
||||
@keyframes bc {
|
||||
0% {
|
||||
color: #689cc5;
|
||||
}
|
||||
|
||||
25% {
|
||||
color: #b3b7e2;
|
||||
}
|
||||
|
||||
50% {
|
||||
color: #93dbe9;
|
||||
}
|
||||
|
||||
75% {
|
||||
color: #abbd81;
|
||||
}
|
||||
|
||||
100% {
|
||||
color: #689cc5;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="preload__wrap" id="Loading">
|
||||
<div class="preload__container">
|
||||
<p class="preload__name">%VITE_NAME%</p>
|
||||
<div class="preload__loading"></div>
|
||||
<p class="preload__title">正在加载资源...</p>
|
||||
<p class="preload__sub-title">初次加载资源可能需要较多时间 请耐心等待</p>
|
||||
</div>
|
||||
|
||||
<div class="preload__footer">
|
||||
<a href="https://cool-js.com" target="_blank"> https://cool-js.com </a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
41
nginx.conf
41
nginx.conf
@ -14,6 +14,9 @@ http {
|
||||
access_log /var/log/nginx/access.log main;
|
||||
sendfile on;
|
||||
keepalive_timeout 65;
|
||||
upstream backend {
|
||||
server midway:8001;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
@ -25,7 +28,7 @@ http {
|
||||
}
|
||||
location /api/
|
||||
{
|
||||
proxy_pass http://midway:7001/;
|
||||
proxy_pass http://backend/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
@ -48,6 +51,42 @@ http {
|
||||
|
||||
#expires 12h;
|
||||
}
|
||||
# location /im {
|
||||
# proxy_pass http://backend/im;
|
||||
# proxy_connect_timeout 3600s; #配置点1
|
||||
# proxy_read_timeout 3600s; #配置点2,如果没效,可以考虑这个时间配置长一点
|
||||
# proxy_send_timeout 3600s; #配置点3
|
||||
# proxy_set_header Host $host;
|
||||
# proxy_set_header X-Real-IP $remote_addr;
|
||||
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
# proxy_set_header REMOTE-HOST $remote_addr;
|
||||
# #proxy_bind $remote_addr transparent;
|
||||
# proxy_http_version 1.1;
|
||||
# proxy_set_header Upgrade $http_upgrade;
|
||||
# proxy_set_header Connection "upgrade";
|
||||
# # rewrite /socket/(.*) /$1 break;
|
||||
# proxy_redirect off;
|
||||
|
||||
# }
|
||||
|
||||
# location /socket {
|
||||
# proxy_pass http://backend/socket;
|
||||
# proxy_connect_timeout 3600s; #配置点1
|
||||
# proxy_read_timeout 3600s; #配置点2,如果没效,可以考虑这个时间配置长一点
|
||||
# proxy_send_timeout 3600s; #配置点3
|
||||
# proxy_set_header Host $host;
|
||||
# proxy_set_header X-Real-IP $remote_addr;
|
||||
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
# proxy_set_header REMOTE-HOST $remote_addr;
|
||||
# #proxy_bind $remote_addr transparent;
|
||||
# proxy_http_version 1.1;
|
||||
# proxy_set_header Upgrade $http_upgrade;
|
||||
# proxy_set_header Connection "upgrade";
|
||||
# rewrite /socket/(.*) /$1 break;
|
||||
# proxy_redirect off;
|
||||
|
||||
# }
|
||||
|
||||
|
||||
location /adminer/
|
||||
{
|
||||
|
110
package.json
110
package.json
@ -1,65 +1,67 @@
|
||||
{
|
||||
"name": "cool-admin-vue",
|
||||
"version": "3.2.2",
|
||||
"name": "cool-admin",
|
||||
"version": "7.0.0",
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build",
|
||||
"report": "vue-cli-service build --report",
|
||||
"lint": "vue-cli-service lint",
|
||||
"inspect": "vue inspect --mode=production > output.js"
|
||||
"dev": "vite --host",
|
||||
"build": "vite build",
|
||||
"serve": "vite preview",
|
||||
"lint:prettier": "prettier --write --loglevel warn \"src/**/*.{js,json,tsx,css,less,scss,vue,html,md}\"",
|
||||
"lint:eslint": "eslint \"{src}/**/*.{vue,ts,tsx}\" --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^0.21.1",
|
||||
"cl-admin": "^1.5.3",
|
||||
"cl-admin-crud": "^1.6.15",
|
||||
"cl-admin-theme": "^0.0.5",
|
||||
"clipboard": "^2.0.7",
|
||||
"codemirror": "^5.59.4",
|
||||
"core-js": "^3.6.5",
|
||||
"dayjs": "^1.10.4",
|
||||
"echarts": "^5.0.2",
|
||||
"element-ui": "^2.15.1",
|
||||
"js-beautify": "^1.13.5",
|
||||
"@cool-vue/crud": "^7.0.0-beta9",
|
||||
"@element-plus/icons-vue": "^2.1.0",
|
||||
"@vueuse/core": "^10.4.0",
|
||||
"@wangeditor/editor": "^5.1.23",
|
||||
"@wangeditor/editor-for-vue": "^5.1.12",
|
||||
"axios": "^1.5.0",
|
||||
"chardet": "^1.6.0",
|
||||
"core-js": "^3.32.1",
|
||||
"dayjs": "^1.11.9",
|
||||
"echarts": "^5.4.3",
|
||||
"element-plus": "^2.3.12",
|
||||
"file-saver": "^2.0.5",
|
||||
"lodash-es": "^4.17.21",
|
||||
"mitt": "^3.0.1",
|
||||
"mockjs": "^1.1.0",
|
||||
"monaco-editor": "0.36.0",
|
||||
"mqtt": "^4.3.7",
|
||||
"nprogress": "^0.2.0",
|
||||
"qs": "^6.9.1",
|
||||
"quill": "^1.3.7",
|
||||
"socket.io-client": "2.3.1",
|
||||
"pinia": "^2.1.6",
|
||||
"socket.io-client": "^4.7.2",
|
||||
"store": "^2.0.12",
|
||||
"uuid": "^8.3.2",
|
||||
"vue": "^2.6.11",
|
||||
"vue-codemirror": "^4.0.6",
|
||||
"vue-cron": "^1.0.9",
|
||||
"vue-echarts": "^6.0.0-rc.3",
|
||||
"vue-router": "^3.2.0",
|
||||
"vuedraggable": "^2.24.3",
|
||||
"vuex": "^3.4.0"
|
||||
"ts-wps": "^1.0.5",
|
||||
"vue": "^3.3.4",
|
||||
"vue-echarts": "^6.6.1",
|
||||
"vue-router": "^4.2.4",
|
||||
"vuedraggable": "^4.1.0",
|
||||
"xlsx": "^0.18.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@typescript-eslint/parser": "^3.0.0",
|
||||
"@vue/babel-helper-vue-jsx-merge-props": "^1.0.0",
|
||||
"@vue/babel-preset-jsx": "^1.1.2",
|
||||
"@vue/cli-plugin-babel": "~4.5.0",
|
||||
"@vue/cli-plugin-eslint": "~4.5.0",
|
||||
"@vue/cli-plugin-router": "~4.5.0",
|
||||
"@vue/cli-plugin-vuex": "~4.5.0",
|
||||
"@vue/cli-service": "~4.5.0",
|
||||
"@vue/composition-api": "^1.0.0-rc.5",
|
||||
"@vue/eslint-config-prettier": "^6.0.0",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"babel-plugin-component": "^1.1.1",
|
||||
"babel-plugin-jsx-v-model": "^2.0.3",
|
||||
"clean-webpack-plugin": "^3.0.0",
|
||||
"eslint": "^6.7.2",
|
||||
"eslint-plugin-prettier": "^3.1.3",
|
||||
"eslint-plugin-vue": "^6.2.2",
|
||||
"hard-source-webpack-plugin": "^0.13.1",
|
||||
"node-sass": "^4.12.0",
|
||||
"prettier": "^1.19.1",
|
||||
"sass-loader": "^8.0.2",
|
||||
"svg-sprite-loader": "^5.0.0",
|
||||
"typescript": "^3.9.3",
|
||||
"vue-template-compiler": "^2.6.11",
|
||||
"webpack-cli": "^3.3.12"
|
||||
"@types/lodash-es": "^4.17.8",
|
||||
"@types/mockjs": "^1.0.7",
|
||||
"@types/node": "^20.5.6",
|
||||
"@types/nprogress": "^0.2.0",
|
||||
"@types/prettier": "^2.7.3",
|
||||
"@types/quill": "^2.0.10",
|
||||
"@types/store": "^2.0.2",
|
||||
"@typescript-eslint/eslint-plugin": "^6.4.1",
|
||||
"@typescript-eslint/parser": "^6.4.1",
|
||||
"@vitejs/plugin-vue": "^4.3.3",
|
||||
"@vitejs/plugin-vue-jsx": "^3.0.2",
|
||||
"@vue/compiler-sfc": "^3.3.4",
|
||||
"eslint": "^8.48.0",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-plugin-prettier": "^5.0.0",
|
||||
"eslint-plugin-vue": "^9.17.0",
|
||||
"lodash": "^4.17.21",
|
||||
"magic-string": "^0.30.3",
|
||||
"prettier": "^2.8.4",
|
||||
"rollup-plugin-visualizer": "^5.9.2",
|
||||
"sass": "^1.66.1",
|
||||
"terser": "^5.19.2",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^4.4.9",
|
||||
"vite-plugin-compression": "^0.5.1"
|
||||
}
|
||||
}
|
||||
|
@ -1,2 +1,3 @@
|
||||
> 1%
|
||||
last 2 versions
|
||||
not dead
|
23
packages/crud/.gitignore
vendored
Normal file
23
packages/crud/.gitignore
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
/dist
|
||||
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Log files
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
.vscode
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
9
packages/crud/.prettierrc
Normal file
9
packages/crud/.prettierrc
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"tabWidth": 4,
|
||||
"useTabs": true,
|
||||
"semi": true,
|
||||
"jsxBracketSameLine": true,
|
||||
"singleQuote": false,
|
||||
"printWidth": 100,
|
||||
"trailingComma": "none"
|
||||
}
|
35
packages/crud/README.md
Normal file
35
packages/crud/README.md
Normal file
@ -0,0 +1,35 @@
|
||||
# 介绍
|
||||
|
||||
**cool-admin for vue**是基于[Vue.js](https://v3.cn.vuejs.org)开发的,[官方文档](https://v3.cn.vuejs.org)。
|
||||
|
||||
Vue.js 是一套用于构建用户界面的渐进式框架。与其它大型框架不同的是,Vue 被设计为可以自底向上逐层应用。
|
||||
|
||||
尝试 `cool-admin` 最简单的方法就是查看文档及运行示例。
|
||||
|
||||
<img src='https://cool-js.com/assets/login.350e25ec.png' />
|
||||
|
||||
<img src='https://cool-js.com/assets/home.1706ac70.png' />
|
||||
|
||||
<span style="font-size: 18px; color: #F56C6C">v6.0.0 新增 Ai 极速编码 ~~~~</span>
|
||||
|
||||
<img src='https://cool-js.com/assets/ai-code2.9a122008.png' />
|
||||
|
||||
## 代码仓库
|
||||
|
||||
**cool-admin for vue** 是开源免费的,遵循[MIT](https://baike.baidu.com/item/MIT/10772952)开源协议,意味着您无需支付任何费用,也无需授权,即可将它应用到您的产品中。
|
||||
|
||||
开源免费,并不意味着您可以将 cool-admin 应用到非法的领域,比如涉及赌博,暴力等方面。如因此产生纠纷等法律问题,`cool-admin`不承担任何责任。
|
||||
|
||||
[https://github.com/cool-team-official/cool-admin-vue](https://github.com/cool-team-official/cool-admin-vue)
|
||||
|
||||
```shell
|
||||
git clone https://github.com/cool-team-official/cool-admin-vue.git
|
||||
```
|
||||
|
||||
## 技术选型
|
||||
|
||||
- [Vue.js](https://v3.cn.vuejs.org),基础框架;
|
||||
- [VueRouter](https://router.vuejs.org),Vue.js 官方路由;
|
||||
- [Pinia](https://pinia.vuejs.org),轻量级状态管理库;
|
||||
- [ElementPlus](https://element-plus.gitee.io/zh-CN),桌面端组件库;
|
||||
- [Vite](https://vitejs.cn),构建工具;
|
3
packages/crud/babel.config.js
Normal file
3
packages/crud/babel.config.js
Normal file
@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
presets: ["@vue/cli-plugin-babel/preset"]
|
||||
};
|
1
packages/crud/env.d.ts
vendored
Normal file
1
packages/crud/env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
/// <reference types="./index" />
|
730
packages/crud/index.d.ts
vendored
Normal file
730
packages/crud/index.d.ts
vendored
Normal file
@ -0,0 +1,730 @@
|
||||
// vue
|
||||
declare namespace Vue {
|
||||
interface Ref<T = any> {
|
||||
value: T;
|
||||
}
|
||||
|
||||
type Emit = (name: any, ...args: any[]) => void;
|
||||
}
|
||||
|
||||
// element-plus
|
||||
declare namespace ElementPlus {
|
||||
type Size = "large" | "default" | "small";
|
||||
type Align = "large" | "default" | "small";
|
||||
|
||||
interface FormProps {
|
||||
inline?: boolean;
|
||||
labelPosition?: "left" | "right" | "top";
|
||||
labelWidth?: string | number;
|
||||
labelSuffix?: string;
|
||||
hideRequiredAsterisk?: boolean;
|
||||
showMessage?: boolean;
|
||||
inlineMessage?: boolean;
|
||||
statusIcon?: boolean;
|
||||
validateOnRuleChange?: boolean;
|
||||
size?: Size;
|
||||
disabled?: boolean;
|
||||
[key: string]: any;
|
||||
}
|
||||
}
|
||||
|
||||
// 方法
|
||||
declare type fn = () => void;
|
||||
|
||||
// 对象
|
||||
declare type obj = {
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
// 全部可选
|
||||
declare type DeepPartial<T> = T extends Function
|
||||
? T
|
||||
: T extends object
|
||||
? { [P in keyof T]?: DeepPartial<T[P]> }
|
||||
: T;
|
||||
|
||||
// 合并
|
||||
declare type Merge<A, B> = Omit<A, keyof B> & B;
|
||||
|
||||
// 移除 [key]
|
||||
declare type RemoveIndex<T> = {
|
||||
[P in keyof T as string extends P ? never : number extends P ? never : P]: T[P];
|
||||
};
|
||||
|
||||
// 任用列表
|
||||
declare type List<T> = Array<DeepPartial<T> | (() => DeepPartial<T>)>;
|
||||
|
||||
// 字典选项
|
||||
declare type DictOptions = {
|
||||
label: string;
|
||||
value: any;
|
||||
color?: string;
|
||||
type?: string;
|
||||
[key: string]: any;
|
||||
}[];
|
||||
|
||||
// emitter
|
||||
declare interface EmitterItem {
|
||||
name: string;
|
||||
callback(data: any, events: { refresh(params: any): void; crudList: ClCrud.Ref[] }): void;
|
||||
}
|
||||
|
||||
declare interface Emitter {
|
||||
list: EmitterItem[];
|
||||
init(events: any): void;
|
||||
emit(name: string, data?: any): void;
|
||||
on(name: string, callback: (data: any) => void): void;
|
||||
}
|
||||
|
||||
// browser
|
||||
declare type Browser = {
|
||||
screen: string;
|
||||
isMini: boolean;
|
||||
};
|
||||
|
||||
// hook
|
||||
declare namespace Hook {
|
||||
interface Options {
|
||||
form: obj;
|
||||
prop: string;
|
||||
method: "submit" | "bind";
|
||||
}
|
||||
|
||||
type fn = (value: any, options: Options) => any;
|
||||
|
||||
type FormPipe =
|
||||
| "number"
|
||||
| "string"
|
||||
| "split"
|
||||
| "join"
|
||||
| "boolean"
|
||||
| "booleanNumber"
|
||||
| "datetimeRange"
|
||||
| "splitJoin"
|
||||
| "json"
|
||||
| "empty"
|
||||
| fn;
|
||||
|
||||
type FormPipes = FormPipe | FormPipe[];
|
||||
|
||||
type Form =
|
||||
| string
|
||||
| {
|
||||
bind?: FormPipes;
|
||||
submit?: FormPipes;
|
||||
};
|
||||
}
|
||||
|
||||
// render
|
||||
declare namespace Render {
|
||||
type OpButton =
|
||||
| `slot-${string}`
|
||||
| {
|
||||
label: string;
|
||||
type?: string;
|
||||
hidden?: boolean;
|
||||
onClick(options: { scope: obj }): void;
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
interface Props {
|
||||
onChange?(value: any): void;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
interface Component {
|
||||
name?: string;
|
||||
options?: DictOptions | Vue.Ref<DictOptions>;
|
||||
props?: Props | Vue.Ref<Props>;
|
||||
style?: obj;
|
||||
functionSlot?: boolean;
|
||||
vm?: any;
|
||||
[key: string]: any;
|
||||
}
|
||||
}
|
||||
|
||||
declare namespace ClCrud {
|
||||
interface Label {
|
||||
op: string;
|
||||
add: string;
|
||||
delete: string;
|
||||
multiDelete: string;
|
||||
update: string;
|
||||
refresh: string;
|
||||
info: string;
|
||||
search: string;
|
||||
reset: string;
|
||||
clear: string;
|
||||
save: string;
|
||||
close: string;
|
||||
confirm: string;
|
||||
advSearch: string;
|
||||
searchKey: string;
|
||||
placeholder: string;
|
||||
tips: string;
|
||||
saveSuccess: string;
|
||||
deleteSuccess: string;
|
||||
deleteConfirm: string;
|
||||
empty: string;
|
||||
desc: string;
|
||||
asc: string;
|
||||
select: string;
|
||||
deselect: string;
|
||||
seeMore: string;
|
||||
hideContent: string;
|
||||
nonEmpty: string;
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
interface Dict {
|
||||
primaryId: string;
|
||||
api: {
|
||||
list: string;
|
||||
add: string;
|
||||
update: string;
|
||||
delete: string;
|
||||
info: string;
|
||||
page: string;
|
||||
};
|
||||
pagination: {
|
||||
page: string;
|
||||
size: string;
|
||||
};
|
||||
search: {
|
||||
keyWord: string;
|
||||
query: string;
|
||||
};
|
||||
sort: {
|
||||
order: string;
|
||||
prop: string;
|
||||
};
|
||||
label: Label;
|
||||
}
|
||||
|
||||
interface Permission {
|
||||
page?: boolean;
|
||||
list?: boolean;
|
||||
add?: boolean;
|
||||
delete?: boolean;
|
||||
update?: boolean;
|
||||
info?: boolean;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
interface Params {
|
||||
page: {
|
||||
page?: number;
|
||||
size?: number;
|
||||
[key: string]: any;
|
||||
};
|
||||
list: obj;
|
||||
add: obj;
|
||||
delete: {
|
||||
ids?: any[];
|
||||
[key: string]: any;
|
||||
};
|
||||
update: {
|
||||
id?: any;
|
||||
[key: string]: any;
|
||||
};
|
||||
info: {
|
||||
id?: any;
|
||||
[key: string]: any;
|
||||
};
|
||||
}
|
||||
|
||||
interface Response {
|
||||
page: {
|
||||
list: any[];
|
||||
pagination: {
|
||||
total: number;
|
||||
page: number;
|
||||
size: number;
|
||||
[key: string]: any;
|
||||
};
|
||||
[key: string]: any;
|
||||
};
|
||||
list: any[];
|
||||
add: any;
|
||||
update: any;
|
||||
info: any;
|
||||
delete: any;
|
||||
}
|
||||
|
||||
interface Service {
|
||||
api: {
|
||||
page(params?: Params["page"]): Promise<Response["page"]>;
|
||||
list(params?: Params["list"]): Promise<Response["list"]>;
|
||||
add(params?: Params["add"]): Promise<Response["add"]>;
|
||||
update(params?: Params["update"]): Promise<Response["update"]>;
|
||||
info(params?: Params["info"]): Promise<Response["info"]>;
|
||||
delete(params?: Params["delete"]): Promise<Response["delete"]>;
|
||||
[key: string]: (params?: any) => Promise<any>;
|
||||
};
|
||||
}
|
||||
|
||||
interface Config {
|
||||
name: string;
|
||||
service: Service["api"];
|
||||
permission: Permission;
|
||||
dict: Dict;
|
||||
onRefresh(
|
||||
params: obj,
|
||||
event: {
|
||||
done: fn;
|
||||
next: Service["api"]["page"];
|
||||
render: (
|
||||
list: Response["page"]["list"],
|
||||
pagination?: Response["page"]["pagination"]
|
||||
) => void;
|
||||
}
|
||||
): void;
|
||||
onDelete(
|
||||
selection: obj[],
|
||||
event: {
|
||||
next: Service["api"]["delete"];
|
||||
}
|
||||
): void;
|
||||
}
|
||||
|
||||
interface Options extends Config {
|
||||
service: any;
|
||||
}
|
||||
|
||||
interface Ref {
|
||||
"cl-table": ClTable.Ref;
|
||||
"cl-upsert": ClUpsert.Ref;
|
||||
name: string;
|
||||
routePath: string;
|
||||
permission: Permission;
|
||||
dict: Dict;
|
||||
service: Service["api"];
|
||||
loading: boolean;
|
||||
params: obj;
|
||||
selection: obj[];
|
||||
set(key: "dict" | "style" | "service" | "permission", value: any): void;
|
||||
done(): void;
|
||||
getParams(): obj;
|
||||
getPermission(key?: string): boolean;
|
||||
rowInfo(data: obj): void;
|
||||
rowAdd(): void;
|
||||
rowEdit(data: obj): void;
|
||||
rowAppend(data?: obj): void;
|
||||
rowClose(): void;
|
||||
rowDelete(...selection: obj[]): void;
|
||||
proxy(name: string, data?: any[]): any;
|
||||
paramsReplace(params: obj): obj;
|
||||
refresh: Service["api"]["page"];
|
||||
[key: string]: any;
|
||||
}
|
||||
}
|
||||
|
||||
declare namespace ClTable {
|
||||
type OpButton = Array<"info" | "edit" | "delete" | Render.OpButton>;
|
||||
|
||||
interface Column {
|
||||
type: "index" | "selection" | "expand" | "op";
|
||||
hidden: boolean | Vue.Ref<boolean>;
|
||||
component: Render.Component;
|
||||
dict: DictOptions | Vue.Ref<DictOptions>;
|
||||
dictFormatter: (values: DictOptions) => string;
|
||||
dictColor: boolean;
|
||||
buttons: OpButton | ((options: { scope: obj }) => OpButton);
|
||||
align: "left" | "center" | "right";
|
||||
label: string | Vue.Ref<string>;
|
||||
className: string;
|
||||
prop: string;
|
||||
orderNum: number;
|
||||
width: number;
|
||||
minWidth: number | string;
|
||||
renderHeader: (options: { column: any; $index: number }) => any;
|
||||
sortable: boolean | "desc" | "descending" | "ascending" | "asc" | "custom";
|
||||
sortMethod: fn;
|
||||
sortBy: string | ((row: any, index: number) => any) | any[];
|
||||
resizable: boolean;
|
||||
columnKey: string;
|
||||
headerAlign: string;
|
||||
showOverflowTooltip: boolean;
|
||||
fixed: boolean | string;
|
||||
formatter: (row: any, column: any, value: any, index: number) => any;
|
||||
selectable: (row: any, index: number) => boolean;
|
||||
reserveSelection: boolean;
|
||||
filterMethod: fn;
|
||||
filteredValue: unknown[];
|
||||
filters: unknown[];
|
||||
filterPlacement: string;
|
||||
filterMultiple: boolean;
|
||||
index: ((index: number) => number) | number;
|
||||
sortOrders: unknown[];
|
||||
children: Column[];
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
type ContextMenu = Array<
|
||||
| ClContextMenu.Item
|
||||
| ((row: obj, column: obj, event: PointerEvent) => ClContextMenu.Item)
|
||||
| "refresh"
|
||||
| "check"
|
||||
| "update"
|
||||
| "edit"
|
||||
| "delete"
|
||||
| "info"
|
||||
| "order-desc"
|
||||
| "order-asc"
|
||||
>;
|
||||
|
||||
interface Config {
|
||||
columns: Column[];
|
||||
autoHeight: boolean;
|
||||
height: string | number;
|
||||
contextMenu: ContextMenu;
|
||||
defaultSort: {
|
||||
prop: string;
|
||||
order: "descending" | "ascending";
|
||||
};
|
||||
sortRefresh: boolean;
|
||||
emptyText: string;
|
||||
rowKey: string;
|
||||
onRowContextmenu?(row: any, column: any, event: any): void;
|
||||
}
|
||||
|
||||
interface Ref {
|
||||
Table: any;
|
||||
config: obj;
|
||||
selection: obj[];
|
||||
data: obj[];
|
||||
columns: Column[];
|
||||
reBuild(cb?: fn): void;
|
||||
calcMaxHeight(): void;
|
||||
setData(data: any[]): void;
|
||||
setColumns(columns: Column[]): void;
|
||||
showColumn(props: string | string[], status?: boolean): void;
|
||||
hideColumn(props: string | string[]): void;
|
||||
changeSort(prop: string, order: string): void;
|
||||
clearSelection(): void;
|
||||
getSelectionRows(): any[];
|
||||
toggleRowSelection(row: any, selected?: boolean): void;
|
||||
toggleAllSelection(): void;
|
||||
toggleRowExpansion(row: any, expanded?: boolean): void;
|
||||
setCurrentRow(row: any): void;
|
||||
clearSort(): void;
|
||||
clearFilter(columnKeys: string[]): void;
|
||||
doLayout(): void;
|
||||
sort(prop: string, order: string): void;
|
||||
scrollTo(position: { top?: number; left?: number }): void;
|
||||
setScrollTop(top: number): void;
|
||||
setScrollLeft(left: number): void;
|
||||
}
|
||||
|
||||
interface Options extends Config {
|
||||
columns: List<ClTable.Column>;
|
||||
}
|
||||
}
|
||||
|
||||
declare namespace ClForm {
|
||||
type CloseAction = "close" | "save";
|
||||
|
||||
interface Rule {
|
||||
type?:
|
||||
| "string"
|
||||
| "number"
|
||||
| "boolean"
|
||||
| "method"
|
||||
| "regexp"
|
||||
| "integer"
|
||||
| "float"
|
||||
| "array"
|
||||
| "object"
|
||||
| "enum"
|
||||
| "date"
|
||||
| "url"
|
||||
| "hex"
|
||||
| "email"
|
||||
| "any";
|
||||
required?: boolean;
|
||||
message?: string;
|
||||
min?: number;
|
||||
max?: number;
|
||||
trigger?: any;
|
||||
validator?(rule: any, value: any, callback: (error?: Error) => void): void;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
interface Item {
|
||||
type?: "tabs";
|
||||
prop?: string;
|
||||
props?: {
|
||||
labels?: Array<{ label: string; value: string; name?: string; icon?: any }>;
|
||||
justify?: "left" | "center" | "right";
|
||||
color?: string;
|
||||
mergeProp?: boolean;
|
||||
labelWidth?: string;
|
||||
error?: string;
|
||||
showMessage?: boolean;
|
||||
inlineMessage?: boolean;
|
||||
size?: "medium" | "default" | "small";
|
||||
[key: string]: any;
|
||||
};
|
||||
span?: number;
|
||||
col?: {
|
||||
span: number;
|
||||
offset: number;
|
||||
push: number;
|
||||
pull: number;
|
||||
xs: any;
|
||||
sm: any;
|
||||
md: any;
|
||||
lg: any;
|
||||
xl: any;
|
||||
tag: string;
|
||||
};
|
||||
hook?: Hook.Form;
|
||||
group?: string;
|
||||
collapse?: boolean;
|
||||
value?: any;
|
||||
label?: string;
|
||||
renderLabel?: any;
|
||||
flex?: boolean;
|
||||
hidden?: boolean | Vue.Ref<boolean> | ((options: { scope: obj }) => boolean);
|
||||
prepend?: Render.Component;
|
||||
component?: Render.Component;
|
||||
append?: Render.Component;
|
||||
rules?: Rule | Rule[];
|
||||
required?: boolean;
|
||||
children?: Item[];
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
type Plugin = (options: {
|
||||
exposed: Ref;
|
||||
onOpen(cb: () => void): void;
|
||||
onClose(cb: () => void): void;
|
||||
onSubmit(cb: (data: obj) => obj): void;
|
||||
}) => void;
|
||||
|
||||
interface Config {
|
||||
title?: any;
|
||||
height?: string;
|
||||
width?: string;
|
||||
props: ElementPlus.FormProps;
|
||||
items: Item[];
|
||||
form: obj;
|
||||
isReset?: boolean;
|
||||
on?: {
|
||||
open?(data: obj): void;
|
||||
close?(action: CloseAction, done: fn): void;
|
||||
submit?(data: obj, event: { close: fn; done: fn }): void;
|
||||
};
|
||||
op: {
|
||||
hidden?: boolean;
|
||||
saveButtonText?: string;
|
||||
closeButtonText?: string;
|
||||
justify?: "flex-start" | "center" | "flex-end";
|
||||
buttons?: Array<CloseAction | Render.OpButton>;
|
||||
};
|
||||
dialog: {
|
||||
title?: any;
|
||||
height?: string;
|
||||
width?: string;
|
||||
hideHeader?: boolean;
|
||||
controls?: Array<"fullscreen" | "close">;
|
||||
[key: string]: any;
|
||||
};
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
type Items = List<Item>;
|
||||
|
||||
interface Options extends Config {
|
||||
items: Items;
|
||||
}
|
||||
|
||||
interface Ref {
|
||||
Form: any;
|
||||
form: obj;
|
||||
config: {
|
||||
items: Item[];
|
||||
[key: string]: any;
|
||||
};
|
||||
open(options: DeepPartial<Options>, plugins?: Plugin[]): void;
|
||||
close(action?: CloseAction): void;
|
||||
done(): void;
|
||||
clear(): void;
|
||||
reset(): void;
|
||||
showLoading(): void;
|
||||
hideLoading(): void;
|
||||
setDisabled(flag?: boolean): void;
|
||||
setData(prop: string, value: any): void;
|
||||
bindForm(data: obj): void;
|
||||
getForm(prop?: string): any;
|
||||
setForm(prop: string, value: any): void;
|
||||
setOptions(prop: string, list: DictOptions): void;
|
||||
setProps(prop: string, value: any): void;
|
||||
setConfig(path: string, value: any): void;
|
||||
showItem(props: string[] | string): void;
|
||||
hideItem(props: string[] | string): void;
|
||||
toggleItem(prop: string, flag?: boolean): void;
|
||||
resetFields(): void;
|
||||
clearValidate(props?: string[] | string): void;
|
||||
validateField(
|
||||
props?: string[] | string,
|
||||
callback?: (isValid: boolean, invalidFields: any[]) => void
|
||||
): Promise<void>;
|
||||
validate(callback: (isValid: boolean, invalidFields: any[]) => void): Promise<void>;
|
||||
changeTab(value: any, valid?: boolean): Promise<any>;
|
||||
setTitle(value: string): void;
|
||||
submit(cb?: (data: obj) => void): void;
|
||||
[key: string]: any;
|
||||
}
|
||||
}
|
||||
|
||||
declare namespace ClUpsert {
|
||||
interface Config {
|
||||
items: ClForm.Item[];
|
||||
props: ClForm.Config["props"];
|
||||
sync: boolean;
|
||||
op: ClForm.Config["op"];
|
||||
dialog: ClForm.Config["dialog"];
|
||||
onOpen?(data: obj): void;
|
||||
onOpened?(data: obj): void;
|
||||
onClose?(action: ClForm.CloseAction, done: fn): void;
|
||||
onClosed?(): void;
|
||||
onInfo?(
|
||||
data: obj,
|
||||
event: { close: fn; done(data: obj): void; next: ClCrud.Service["api"]["info"] }
|
||||
): void;
|
||||
onSubmit?(
|
||||
data: obj,
|
||||
event: { close: fn; done: fn; next: ClCrud.Service["api"]["update"] }
|
||||
): void;
|
||||
plugins?: ClForm.Plugin[];
|
||||
}
|
||||
|
||||
interface Ref extends ClForm.Ref {
|
||||
mode: "add" | "update" | "info";
|
||||
}
|
||||
|
||||
interface Options extends Config {
|
||||
items: List<ClForm.Item>;
|
||||
}
|
||||
}
|
||||
|
||||
declare namespace ClAdvSearch {
|
||||
interface Config {
|
||||
items?: ClForm.Item[];
|
||||
title?: string;
|
||||
size?: string | number;
|
||||
op?: Array<"clear" | "reset" | "close" | "search">;
|
||||
onSearch?(data: obj, options: { next: ClCrud.Service["api"]["page"]; close(): void }): void;
|
||||
}
|
||||
|
||||
interface Options extends Config {
|
||||
items: ClForm.Items;
|
||||
}
|
||||
|
||||
interface Ref extends ClForm.Ref {}
|
||||
}
|
||||
|
||||
declare namespace ClSearch {
|
||||
interface Config {
|
||||
items?: ClForm.Item[];
|
||||
data?: obj;
|
||||
resetBtn?: boolean;
|
||||
onLoad?(data: obj): void;
|
||||
onSearch?(data: obj, options: { next: ClCrud.Service["api"]["page"] }): void;
|
||||
}
|
||||
|
||||
interface Options extends Config {
|
||||
items: ClForm.Items;
|
||||
}
|
||||
|
||||
interface Ref extends ClForm.Ref {
|
||||
search(params?: obj): void;
|
||||
reset(): void;
|
||||
}
|
||||
}
|
||||
|
||||
declare namespace ClContextMenu {
|
||||
interface Item {
|
||||
label: string;
|
||||
icon?: string;
|
||||
prefixIcon?: string;
|
||||
suffixIcon?: string;
|
||||
ellipsis?: boolean;
|
||||
disabled?: boolean;
|
||||
hidden?: boolean;
|
||||
children?: Item[];
|
||||
showChildren?: boolean;
|
||||
callback?(done: fn): void;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
interface Event {
|
||||
pageX: number;
|
||||
pageY: number;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
interface Options {
|
||||
hover?:
|
||||
| boolean
|
||||
| {
|
||||
target?: string;
|
||||
className?: string;
|
||||
};
|
||||
list: Item[];
|
||||
}
|
||||
|
||||
interface Ref {
|
||||
open(event: Event, options: Options): Ref;
|
||||
close(): void;
|
||||
}
|
||||
}
|
||||
|
||||
declare namespace ClDialog {
|
||||
interface Provide {
|
||||
visible: Vue.Ref<boolean>;
|
||||
fullscreen: Vue.Ref<boolean>;
|
||||
}
|
||||
}
|
||||
|
||||
declare interface Config {
|
||||
dict: ClCrud.Dict;
|
||||
permission: ClCrud.Permission;
|
||||
events: {
|
||||
[key: string]: (...args: any[]) => any;
|
||||
};
|
||||
render: {
|
||||
functionSlots: {
|
||||
exclude: string[];
|
||||
};
|
||||
};
|
||||
style: {
|
||||
size: ElementPlus.Size;
|
||||
colors: string[];
|
||||
form: {
|
||||
labelPostion: ElementPlus.FormProps["labelPosition"];
|
||||
labelWidth: ElementPlus.FormProps["labelWidth"];
|
||||
span: number;
|
||||
};
|
||||
table: {
|
||||
stripe: boolean;
|
||||
border: boolean;
|
||||
highlightCurrentRow: boolean;
|
||||
resizable: boolean;
|
||||
autoHeight: boolean;
|
||||
contextMenu: ClTable.ContextMenu;
|
||||
column: {
|
||||
minWidth: number;
|
||||
align: ElementPlus.Align;
|
||||
headerAlign: ElementPlus.Align;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
declare type Options = DeepPartial<Config>;
|
||||
|
||||
declare interface CrudOptions {
|
||||
options: Options;
|
||||
}
|
38
packages/crud/package.json
Normal file
38
packages/crud/package.json
Normal file
@ -0,0 +1,38 @@
|
||||
{
|
||||
"name": "@cool-vue/crud",
|
||||
"version": "7.0.1",
|
||||
"private": false,
|
||||
"main": "./dist/index.umd.min.js",
|
||||
"typings": "types/index.d.ts",
|
||||
"scripts": {
|
||||
"dev": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build",
|
||||
"dist": "tsc && yarn build --target lib --name index ./src/index.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"array.prototype.flat": "^1.2.4",
|
||||
"core-js": "^3.21.1",
|
||||
"element-plus": "^2.3.9",
|
||||
"lodash": "^4.17.21",
|
||||
"lodash-es": "^4.17.21",
|
||||
"mitt": "^3.0.1",
|
||||
"vue": "^3.3.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/array.prototype.flat": "^1.2.1",
|
||||
"@types/clone-deep": "^4.0.1",
|
||||
"@vue/cli-plugin-babel": "^5.0.1",
|
||||
"@vue/cli-plugin-typescript": "^5.0.3",
|
||||
"@vue/cli-service": "^5.0.3",
|
||||
"@vue/compiler-sfc": "^3.2.39",
|
||||
"prettier": "^2.4.1",
|
||||
"sass": "^1.55.0",
|
||||
"sass-loader": "^12.6.0",
|
||||
"typescript": "^4.6.2"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"types",
|
||||
"index.d.ts"
|
||||
]
|
||||
}
|
3
packages/crud/src/App.vue
Normal file
3
packages/crud/src/App.vue
Normal file
@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<div>CRUD v7.0.0</div>
|
||||
</template>
|
21
packages/crud/src/components/add-btn/index.tsx
Normal file
21
packages/crud/src/components/add-btn/index.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import { defineComponent } from "vue";
|
||||
import { useConfig, useCore } from "../../hooks";
|
||||
|
||||
export default defineComponent({
|
||||
name: "cl-add-btn",
|
||||
|
||||
setup(_, { slots }) {
|
||||
const { crud } = useCore();
|
||||
const { style } = useConfig();
|
||||
|
||||
return () => {
|
||||
return (
|
||||
crud.getPermission("add") && (
|
||||
<el-button type="primary" size={style.size} onClick={crud.rowAdd}>
|
||||
{slots.default?.() || crud.dict.label.add}
|
||||
</el-button>
|
||||
)
|
||||
);
|
||||
};
|
||||
}
|
||||
});
|
31
packages/crud/src/components/adv/btn.tsx
Normal file
31
packages/crud/src/components/adv/btn.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
import { useConfig, useCore } from "../../hooks";
|
||||
import { defineComponent } from "vue";
|
||||
import { Search } from "@element-plus/icons-vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "cl-adv-btn",
|
||||
|
||||
components: {
|
||||
Search
|
||||
},
|
||||
|
||||
setup(_, { slots }) {
|
||||
const { crud, mitt } = useCore();
|
||||
const { style } = useConfig();
|
||||
|
||||
function open() {
|
||||
mitt.emit("crud.openAdvSearch");
|
||||
}
|
||||
|
||||
return () => {
|
||||
return (
|
||||
<el-button size={style.size} onClick={open} class="cl-adv-btn">
|
||||
<el-icon>
|
||||
<Search />
|
||||
</el-icon>
|
||||
{slots.default?.() || crud.dict.label.advSearch}
|
||||
</el-button>
|
||||
);
|
||||
};
|
||||
}
|
||||
});
|
176
packages/crud/src/components/adv/search.tsx
Normal file
176
packages/crud/src/components/adv/search.tsx
Normal file
@ -0,0 +1,176 @@
|
||||
import { defineComponent, h, inject, mergeProps, nextTick, PropType, reactive, ref } from "vue";
|
||||
import { Close } from "@element-plus/icons-vue";
|
||||
import { useBrowser, useConfig, useCore } from "../../hooks";
|
||||
import { renderNode } from "../../utils/vnode";
|
||||
import { useApi } from "../form/helper";
|
||||
|
||||
export default defineComponent({
|
||||
name: "cl-adv-search",
|
||||
|
||||
components: {
|
||||
Close
|
||||
},
|
||||
|
||||
props: {
|
||||
// 表单项
|
||||
items: {
|
||||
type: Array as PropType<ClForm.Item[]>,
|
||||
default: () => []
|
||||
},
|
||||
// 标题
|
||||
title: String,
|
||||
// 窗体大小
|
||||
size: {
|
||||
type: [Number, String],
|
||||
default: "30%"
|
||||
},
|
||||
// 操作按钮
|
||||
op: {
|
||||
type: Array,
|
||||
default: () => ["clear", "reset", "close", "search"]
|
||||
},
|
||||
// 搜索钩子
|
||||
onSearch: Function
|
||||
},
|
||||
|
||||
emits: ["reset", "clear"],
|
||||
|
||||
setup(props, { emit, slots, expose }) {
|
||||
const { crud, mitt } = useCore();
|
||||
const { style } = useConfig();
|
||||
const browser = useBrowser();
|
||||
|
||||
// 配置
|
||||
const config = reactive<ClAdvSearch.Config>(
|
||||
mergeProps(props, inject("useAdvSearch__options") || {})
|
||||
);
|
||||
|
||||
// cl-form
|
||||
const Form = ref<ClForm.Ref>();
|
||||
|
||||
// el-drawer
|
||||
const Drawer = ref();
|
||||
|
||||
// 是否可见
|
||||
const visible = ref(false);
|
||||
|
||||
// 打开
|
||||
function open() {
|
||||
visible.value = true;
|
||||
|
||||
nextTick(function () {
|
||||
Form.value?.open({
|
||||
items: config.items || [],
|
||||
op: {
|
||||
hidden: true
|
||||
},
|
||||
isReset: false
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 关闭
|
||||
function close() {
|
||||
Drawer.value.handleClose();
|
||||
}
|
||||
|
||||
// 重置数据
|
||||
function reset() {
|
||||
Form.value?.reset();
|
||||
emit("reset");
|
||||
}
|
||||
|
||||
// 清空数据
|
||||
function clear() {
|
||||
Form.value?.clear();
|
||||
emit("clear");
|
||||
}
|
||||
|
||||
// 搜素请求
|
||||
function search() {
|
||||
Form.value?.submit((data) => {
|
||||
function next(params: any) {
|
||||
Form.value?.done();
|
||||
close();
|
||||
|
||||
return crud.refresh({
|
||||
...params,
|
||||
page: 1
|
||||
});
|
||||
}
|
||||
|
||||
if (config.onSearch) {
|
||||
config.onSearch(data, { next, close });
|
||||
} else {
|
||||
next(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 消息事件
|
||||
mitt.on("crud.openAdvSearch", open);
|
||||
|
||||
// 渲染表单
|
||||
function renderForm() {
|
||||
return h(<cl-form ref={Form} inner />, {}, slots);
|
||||
}
|
||||
|
||||
// 渲染底部
|
||||
function renderFooter() {
|
||||
const fns = { search, reset, clear, close };
|
||||
|
||||
return config.op?.map((e: string) => {
|
||||
switch (e) {
|
||||
case "search":
|
||||
case "reset":
|
||||
case "clear":
|
||||
case "close":
|
||||
return h(
|
||||
<el-button />,
|
||||
{
|
||||
type: e == "search" ? "primary" : null,
|
||||
size: style.size,
|
||||
onClick: fns[e]
|
||||
},
|
||||
{ default: () => crud.dict.label[e] }
|
||||
);
|
||||
|
||||
default:
|
||||
return renderNode(e, {
|
||||
scope: Form.value?.getForm(),
|
||||
slots
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
expose({
|
||||
open,
|
||||
close,
|
||||
clear,
|
||||
reset,
|
||||
...useApi({ Form })
|
||||
});
|
||||
|
||||
return () => {
|
||||
return (
|
||||
<el-drawer
|
||||
ref={Drawer}
|
||||
modal-class="cl-adv-search"
|
||||
v-model={visible.value}
|
||||
direction="rtl"
|
||||
with-header={false}
|
||||
size={browser.isMini ? "100%" : props.size}>
|
||||
<div class="cl-adv-search__header">
|
||||
<span class="text">{props.title || crud.dict.label.advSearch}</span>
|
||||
<el-icon size={20} onClick={close}>
|
||||
<Close />
|
||||
</el-icon>
|
||||
</div>
|
||||
<div class="cl-adv-search__container">{renderForm()}</div>
|
||||
<div class="cl-adv-search__footer">{renderFooter()}</div>
|
||||
</el-drawer>
|
||||
);
|
||||
};
|
||||
}
|
||||
});
|
249
packages/crud/src/components/context-menu/index.tsx
Normal file
249
packages/crud/src/components/context-menu/index.tsx
Normal file
@ -0,0 +1,249 @@
|
||||
import { defineComponent, nextTick, onMounted, reactive, ref, h, render } from "vue";
|
||||
import { isString } from "lodash-es";
|
||||
import { addClass, contains, removeClass } from "../../utils";
|
||||
import { useRefs } from "../../hooks";
|
||||
|
||||
const ClContextMenu = defineComponent({
|
||||
name: "cl-context-menu",
|
||||
|
||||
props: {
|
||||
show: Boolean,
|
||||
options: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
event: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
setup(props, { expose, slots }) {
|
||||
const { refs, setRefs } = useRefs();
|
||||
|
||||
// 是否可见
|
||||
const visible = ref(props.show || false);
|
||||
|
||||
// 按钮列表
|
||||
const list = ref<ClContextMenu.Item[]>([]);
|
||||
|
||||
// 样式
|
||||
const style = reactive({
|
||||
left: "0px",
|
||||
top: "0px"
|
||||
});
|
||||
|
||||
// 选中值
|
||||
const ids = ref("");
|
||||
|
||||
// 阻止默认事件
|
||||
function stopDefault(e: MouseEvent) {
|
||||
if (e.preventDefault) {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
if (e.stopPropagation) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
// 解析列表
|
||||
function parseList(list: ClContextMenu.Item[]) {
|
||||
function deep(list: ClContextMenu.Item[]) {
|
||||
list.forEach((e) => {
|
||||
e.showChildren = false;
|
||||
|
||||
if (e.children) {
|
||||
deep(e.children);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
deep(list);
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
// 目标元素
|
||||
let targetEl: any = null;
|
||||
|
||||
// 关闭
|
||||
function close() {
|
||||
visible.value = false;
|
||||
ids.value = "";
|
||||
removeClass(targetEl, "cl-context-menu__target");
|
||||
}
|
||||
|
||||
// 打开
|
||||
function open(event: any, options?: any) {
|
||||
let left = event.pageX;
|
||||
let top = event.pageY;
|
||||
|
||||
if (!options) {
|
||||
options = {};
|
||||
}
|
||||
|
||||
// 点击样式
|
||||
if (options.hover) {
|
||||
let d = options.hover === true ? {} : options.hover;
|
||||
targetEl = event.target;
|
||||
|
||||
if (targetEl && isString(targetEl.className)) {
|
||||
if (d.target) {
|
||||
while (!targetEl.className.includes(d.target)) {
|
||||
targetEl = targetEl.parentNode;
|
||||
}
|
||||
}
|
||||
|
||||
addClass(targetEl, d.className || "cl-context-menu__target");
|
||||
}
|
||||
}
|
||||
|
||||
if (options.list) {
|
||||
list.value = parseList(options.list);
|
||||
}
|
||||
|
||||
// 阻止默认事件
|
||||
stopDefault(event);
|
||||
|
||||
// 显示
|
||||
visible.value = true;
|
||||
|
||||
nextTick(() => {
|
||||
const { clientHeight: h1, clientWidth: w1 } = event.target.ownerDocument.body;
|
||||
const { clientHeight: h2, clientWidth: w2 } =
|
||||
refs["context-menu"].querySelector(".cl-context-menu__box");
|
||||
|
||||
if (top + h2 > h1) {
|
||||
top = h1 - h2 - 5;
|
||||
}
|
||||
|
||||
if (left + w2 > w1) {
|
||||
left = w1 - w2 - 5;
|
||||
}
|
||||
|
||||
style.left = left + "px";
|
||||
style.top = top + "px";
|
||||
});
|
||||
|
||||
return {
|
||||
close
|
||||
};
|
||||
}
|
||||
|
||||
// 行点击
|
||||
function rowClick(item: ClContextMenu.Item, id: string) {
|
||||
ids.value = id;
|
||||
|
||||
if (item.disabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (item.callback) {
|
||||
return item.callback(close);
|
||||
}
|
||||
|
||||
if (item.children) {
|
||||
item.showChildren = !item.showChildren;
|
||||
} else {
|
||||
close();
|
||||
}
|
||||
}
|
||||
|
||||
expose({
|
||||
open,
|
||||
close
|
||||
});
|
||||
|
||||
onMounted(function () {
|
||||
if (visible.value) {
|
||||
const { body, documentElement } = props.event.target.ownerDocument;
|
||||
|
||||
// 添加到 body 下
|
||||
body.appendChild(refs["context-menu"]);
|
||||
// 关闭事件
|
||||
(documentElement || body).addEventListener("mousedown", (e: any) => {
|
||||
const el = refs["context-menu"];
|
||||
if (!contains(el, e.target) && el != e.target) {
|
||||
close();
|
||||
}
|
||||
});
|
||||
|
||||
// 默认打开
|
||||
open(props.event, props.options);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
function deep(list: ClContextMenu.Item[], pId: string, level: number) {
|
||||
return (
|
||||
<div class={["cl-context-menu__box", level > 1 && "is-append"]}>
|
||||
{list
|
||||
.filter((e) => !e.hidden)
|
||||
.map((e, i) => {
|
||||
const id = `${pId}-${i}`;
|
||||
|
||||
return (
|
||||
<div
|
||||
class={{
|
||||
"is-active": ids.value.includes(id),
|
||||
"is-ellipsis": e.ellipsis,
|
||||
"is-disabled": e.disabled
|
||||
}}>
|
||||
{/* 前缀图标 */}
|
||||
{e.prefixIcon && <i class={e.prefixIcon}></i>}
|
||||
|
||||
{/* 标题 */}
|
||||
<span
|
||||
onClick={() => {
|
||||
rowClick(e, id);
|
||||
}}>
|
||||
{e.label}
|
||||
</span>
|
||||
|
||||
{/* 后缀图标 */}
|
||||
{e.suffixIcon && <i class={e.suffixIcon}></i>}
|
||||
|
||||
{/* 子集*/}
|
||||
{e.children &&
|
||||
e.showChildren &&
|
||||
deep(e.children, id, level + 1)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
visible.value && (
|
||||
<div
|
||||
class="cl-context-menu"
|
||||
ref={setRefs("context-menu")}
|
||||
style={style}
|
||||
onContextmenu={stopDefault}>
|
||||
{slots.default ? slots.default() : deep(list.value, "0", 1)}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
export const ContextMenu = {
|
||||
open(event: any, options: ClContextMenu.Options) {
|
||||
const vm: any = h(ClContextMenu, {
|
||||
show: true,
|
||||
event,
|
||||
options
|
||||
});
|
||||
|
||||
render(vm, event.target.ownerDocument.createElement("div"));
|
||||
}
|
||||
};
|
||||
|
||||
export default ClContextMenu;
|
276
packages/crud/src/components/crud/helper.ts
Normal file
276
packages/crud/src/components/crud/helper.ts
Normal file
@ -0,0 +1,276 @@
|
||||
import { ElMessageBox, ElMessage } from "element-plus";
|
||||
import { Mitt } from "../../utils/mitt";
|
||||
import { ref } from "vue";
|
||||
import { isArray, isFunction, merge } from "lodash-es";
|
||||
|
||||
interface Options {
|
||||
mitt: Mitt;
|
||||
config: ClCrud.Config;
|
||||
crud: ClCrud.Ref;
|
||||
}
|
||||
|
||||
export function useHelper({ config, crud, mitt }: Options) {
|
||||
// 刷新随机值,避免脏数据
|
||||
const refreshRd = ref(0);
|
||||
|
||||
// 获取权限
|
||||
function getPermission(key: "page" | "list" | "info" | "update" | "add" | "delete"): boolean {
|
||||
return Boolean(crud.permission[key]);
|
||||
}
|
||||
|
||||
// 根据字典替换请求参数
|
||||
function paramsReplace(params: obj) {
|
||||
const { pagination, search, sort } = crud.dict;
|
||||
|
||||
// 请求参数
|
||||
const a: any = { ...params };
|
||||
|
||||
// 字典
|
||||
const b: any = { ...pagination, ...search, ...sort };
|
||||
|
||||
for (const i in b) {
|
||||
if (a[i]) {
|
||||
if (i != b[i]) {
|
||||
a[`_${b[i]}`] = a[i];
|
||||
|
||||
delete a[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const i in a) {
|
||||
if (i[0] === "_") {
|
||||
a[i.substr(1)] = a[i];
|
||||
|
||||
delete a[i];
|
||||
}
|
||||
}
|
||||
|
||||
return a;
|
||||
}
|
||||
|
||||
// 刷新请求
|
||||
function refresh(params?: obj) {
|
||||
const { service, dict } = crud;
|
||||
|
||||
return new Promise((end) => {
|
||||
// 合并请求参数
|
||||
const reqParams = paramsReplace(Object.assign(crud.params, params));
|
||||
|
||||
// Loading
|
||||
crud.loading = true;
|
||||
|
||||
// 预防脏数据
|
||||
const rd = (refreshRd.value = Math.random());
|
||||
|
||||
// 完成事件
|
||||
function done() {
|
||||
crud.loading = false;
|
||||
end(true);
|
||||
}
|
||||
|
||||
// 渲染
|
||||
function render(list: any[], pagination?: any) {
|
||||
mitt.emit("crud.refresh", { list, pagination });
|
||||
done();
|
||||
}
|
||||
|
||||
// 下一步
|
||||
function next(params: obj): Promise<any> {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
await service[dict.api.page](params)
|
||||
.then((res) => {
|
||||
if (rd != refreshRd.value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isArray(res)) {
|
||||
render(res);
|
||||
} else {
|
||||
render(res.list, res.pagination);
|
||||
}
|
||||
|
||||
resolve(res);
|
||||
done();
|
||||
})
|
||||
.catch((err) => {
|
||||
ElMessage.error(err.message);
|
||||
reject(err);
|
||||
done();
|
||||
});
|
||||
|
||||
end(true);
|
||||
});
|
||||
}
|
||||
|
||||
// 刷新钩子
|
||||
if (config.onRefresh) {
|
||||
config.onRefresh(reqParams, { next, done, render });
|
||||
} else {
|
||||
next(reqParams);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 打开详情
|
||||
function rowInfo(data: any) {
|
||||
mitt.emit("crud.proxy", {
|
||||
name: "info",
|
||||
data: [data]
|
||||
});
|
||||
}
|
||||
|
||||
// 打开新增
|
||||
function rowAdd() {
|
||||
mitt.emit("crud.proxy", {
|
||||
name: "add"
|
||||
});
|
||||
}
|
||||
|
||||
// 打开编辑
|
||||
function rowEdit(data: any) {
|
||||
mitt.emit("crud.proxy", {
|
||||
name: "edit",
|
||||
data: [data]
|
||||
});
|
||||
}
|
||||
|
||||
// 打开追加
|
||||
function rowAppend(data: any) {
|
||||
mitt.emit("crud.proxy", {
|
||||
name: "append",
|
||||
data: [data]
|
||||
});
|
||||
}
|
||||
|
||||
// 关闭新增、编辑弹窗
|
||||
function rowClose() {
|
||||
mitt.emit("crud.proxy", {
|
||||
name: "close"
|
||||
});
|
||||
}
|
||||
|
||||
// 删除请求
|
||||
function rowDelete(...selection: any[]) {
|
||||
const { service, dict } = crud;
|
||||
|
||||
// 参数
|
||||
const params = {
|
||||
ids: selection.map((e) => e[dict.primaryId])
|
||||
};
|
||||
|
||||
// 下一步
|
||||
async function next(data: obj) {
|
||||
return new Promise((resolve, reject) => {
|
||||
ElMessageBox({
|
||||
type: "warning",
|
||||
title: dict.label.tips,
|
||||
message: dict.label.deleteConfirm,
|
||||
confirmButtonText: dict.label.confirm,
|
||||
cancelButtonText: dict.label.close,
|
||||
showCancelButton: true,
|
||||
async beforeClose(action, instance, done) {
|
||||
if (action === "confirm") {
|
||||
instance.confirmButtonLoading = true;
|
||||
|
||||
await service[dict.api.delete]({ ...params, ...data })
|
||||
.then((res) => {
|
||||
ElMessage.success(dict.label.deleteSuccess);
|
||||
|
||||
refresh();
|
||||
resolve(res);
|
||||
})
|
||||
.catch((err) => {
|
||||
ElMessage.error(err.message);
|
||||
reject(err);
|
||||
});
|
||||
|
||||
instance.confirmButtonLoading = false;
|
||||
}
|
||||
|
||||
done();
|
||||
}
|
||||
}).catch(() => null);
|
||||
});
|
||||
}
|
||||
|
||||
// 删除钩子
|
||||
if (config.onDelete) {
|
||||
config.onDelete(selection, { next });
|
||||
} else {
|
||||
next(params);
|
||||
}
|
||||
}
|
||||
|
||||
// 代理
|
||||
function proxy(name: string, data?: any[]) {
|
||||
mitt.emit("crud.proxy", {
|
||||
name,
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
// 获取请求参数
|
||||
function getParams() {
|
||||
return crud.params;
|
||||
}
|
||||
|
||||
// 设置
|
||||
function set(key: string, value: any) {
|
||||
if (!value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (key) {
|
||||
// 服务
|
||||
case "service":
|
||||
Object.assign(crud.service, value);
|
||||
crud.service.__proto__ = value.__proto__;
|
||||
if (value._permission) {
|
||||
for (const i in value._permission) {
|
||||
crud.permission[i] = value._permission[i];
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
// 权限
|
||||
case "permission":
|
||||
if (isFunction(value)) {
|
||||
merge(crud.permission, value(crud));
|
||||
} else {
|
||||
merge(crud.permission, value);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
merge(crud[key], value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 监听事件
|
||||
function on(name: string, callback: fn) {
|
||||
mitt.on(`${name}-${crud.id}`, callback);
|
||||
}
|
||||
|
||||
// 默认值
|
||||
set("dict", config.dict);
|
||||
set("service", config.service);
|
||||
set("permission", config.permission);
|
||||
|
||||
return {
|
||||
proxy,
|
||||
set,
|
||||
on,
|
||||
rowInfo,
|
||||
rowAdd,
|
||||
rowEdit,
|
||||
rowAppend,
|
||||
rowDelete,
|
||||
rowClose,
|
||||
refresh,
|
||||
getPermission,
|
||||
paramsReplace,
|
||||
getParams
|
||||
};
|
||||
}
|
87
packages/crud/src/components/crud/index.tsx
Normal file
87
packages/crud/src/components/crud/index.tsx
Normal file
@ -0,0 +1,87 @@
|
||||
import { defineComponent, getCurrentInstance, inject, provide, reactive } from "vue";
|
||||
import { cloneDeep } from "lodash-es";
|
||||
import { useHelper } from "./helper";
|
||||
import { Mitt } from "../../utils/mitt";
|
||||
import { mergeConfig, merge } from "../../utils";
|
||||
import { crudList } from "../../emitter";
|
||||
import { useConfig } from "../../hooks";
|
||||
|
||||
export default defineComponent({
|
||||
name: "cl-crud",
|
||||
|
||||
props: {
|
||||
// 组件名
|
||||
name: String,
|
||||
// 是否有边框
|
||||
border: Boolean,
|
||||
// 内间距
|
||||
padding: {
|
||||
type: String,
|
||||
default: "10px"
|
||||
}
|
||||
},
|
||||
|
||||
setup(props, { slots, expose }) {
|
||||
// 当前实例
|
||||
const inst = getCurrentInstance();
|
||||
|
||||
// 配置
|
||||
const config = reactive<ClCrud.Config>(mergeConfig(inject("useCrud__options") || {}));
|
||||
|
||||
// 事件
|
||||
const mitt = new Mitt(inst?.uid);
|
||||
|
||||
// 全局配置
|
||||
const { dict, permission } = useConfig();
|
||||
|
||||
// 参数
|
||||
const crud = reactive(
|
||||
merge(
|
||||
{
|
||||
id: props.name || inst?.uid,
|
||||
// 绑定的路由地址
|
||||
routePath: location.pathname || "/",
|
||||
// 表格加载状态
|
||||
loading: false,
|
||||
// 表格已选列
|
||||
selection: [],
|
||||
// 请求参数
|
||||
params: {
|
||||
page: 1,
|
||||
size: 20
|
||||
},
|
||||
// 请求服务
|
||||
service: {},
|
||||
// 字典
|
||||
dict: {},
|
||||
// 权限
|
||||
permission: {}
|
||||
},
|
||||
cloneDeep({ dict, permission })
|
||||
)
|
||||
);
|
||||
|
||||
// 追加参数
|
||||
merge(crud, useHelper({ config, crud, mitt }));
|
||||
|
||||
// 集合
|
||||
crudList.push(crud);
|
||||
|
||||
// 值穿透
|
||||
provide("crud", crud);
|
||||
provide("mitt", mitt);
|
||||
|
||||
// 导出
|
||||
expose(crud);
|
||||
|
||||
return () => {
|
||||
return (
|
||||
<div
|
||||
class={["cl-crud", { "is-border": props.border }]}
|
||||
style={{ padding: props.padding }}>
|
||||
{slots.default?.()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
});
|
269
packages/crud/src/components/dialog/index.tsx
Normal file
269
packages/crud/src/components/dialog/index.tsx
Normal file
@ -0,0 +1,269 @@
|
||||
import { defineComponent, h, ref, watch, computed, provide } from "vue";
|
||||
import { Close, FullScreen, Minus } from "@element-plus/icons-vue";
|
||||
import { renderNode } from "../../utils/vnode";
|
||||
import { isArray, isBoolean } from "lodash-es";
|
||||
import { useBrowser } from "../../hooks";
|
||||
|
||||
export default defineComponent({
|
||||
name: "cl-dialog",
|
||||
|
||||
components: {
|
||||
Close,
|
||||
FullScreen,
|
||||
Minus
|
||||
},
|
||||
|
||||
props: {
|
||||
// 是否可见
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// Extraneous non-props attributes
|
||||
props: Object,
|
||||
// 标题
|
||||
title: {
|
||||
type: String,
|
||||
default: "-"
|
||||
},
|
||||
// 高度
|
||||
height: String,
|
||||
// 宽度
|
||||
width: {
|
||||
type: String,
|
||||
default: "50%"
|
||||
},
|
||||
// 內间距
|
||||
padding: {
|
||||
type: String,
|
||||
default: "20px"
|
||||
},
|
||||
// 是否缓存
|
||||
keepAlive: Boolean,
|
||||
// 是否全屏
|
||||
fullscreen: Boolean,
|
||||
// 控制按钮
|
||||
controls: {
|
||||
type: Array,
|
||||
default: () => ["fullscreen", "close"]
|
||||
},
|
||||
// 隐藏头部元素
|
||||
hideHeader: Boolean,
|
||||
// 关闭前
|
||||
beforeClose: Function
|
||||
},
|
||||
|
||||
emits: ["update:modelValue", "fullscreen-change"],
|
||||
|
||||
setup(props, { emit, expose, slots }) {
|
||||
const browser = useBrowser();
|
||||
|
||||
// el-dialog
|
||||
const Dialog = ref();
|
||||
|
||||
// 是否全屏
|
||||
const fullscreen = ref(false);
|
||||
|
||||
// 是否可见
|
||||
const visible = ref(false);
|
||||
|
||||
// 缓存数
|
||||
const cacheKey = ref(0);
|
||||
|
||||
// 是否全屏
|
||||
const isFullscreen = computed(() => {
|
||||
return browser && browser.isMini ? true : fullscreen.value;
|
||||
});
|
||||
|
||||
// 监听绑定值
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(val) => {
|
||||
visible.value = val;
|
||||
if (val && !props.keepAlive) {
|
||||
cacheKey.value += 1;
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true
|
||||
}
|
||||
);
|
||||
|
||||
// 监听 fullscreen 变化
|
||||
watch(
|
||||
() => props.fullscreen,
|
||||
(val) => {
|
||||
fullscreen.value = val;
|
||||
},
|
||||
{
|
||||
immediate: true
|
||||
}
|
||||
);
|
||||
|
||||
// fullscreen-change 回调
|
||||
watch(fullscreen, (val: boolean) => {
|
||||
emit("fullscreen-change", val);
|
||||
});
|
||||
|
||||
// 提供
|
||||
provide("dialog", {
|
||||
visible,
|
||||
fullscreen: isFullscreen
|
||||
});
|
||||
|
||||
// 打开
|
||||
function open() {
|
||||
fullscreen.value = true;
|
||||
}
|
||||
|
||||
// 关闭
|
||||
function close() {
|
||||
function done() {
|
||||
onClose();
|
||||
}
|
||||
|
||||
if (props.beforeClose) {
|
||||
props.beforeClose(done);
|
||||
} else {
|
||||
done();
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭后
|
||||
function onClose() {
|
||||
emit("update:modelValue", false);
|
||||
}
|
||||
|
||||
// 切换全屏
|
||||
function changeFullscreen(val?: boolean) {
|
||||
fullscreen.value = isBoolean(val) ? Boolean(val) : !fullscreen.value;
|
||||
}
|
||||
|
||||
// 双击全屏
|
||||
function dblClickFullscreen() {
|
||||
if (isArray(props.controls) && props.controls.includes("fullscreen")) {
|
||||
changeFullscreen();
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染头部
|
||||
function renderHeader() {
|
||||
return (
|
||||
props.hideHeader || (
|
||||
<div class="cl-dialog__header" onDblclick={dblClickFullscreen}>
|
||||
<span class="cl-dialog__title">{props.title}</span>
|
||||
|
||||
<div class="cl-dialog__controls">
|
||||
{props.controls.map((e: any) => {
|
||||
switch (e) {
|
||||
//全屏按钮
|
||||
case "fullscreen":
|
||||
if (browser.screen === "xs") {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 是否显示全屏按钮
|
||||
if (isFullscreen.value) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
class="minimize"
|
||||
onClick={() => {
|
||||
changeFullscreen(false);
|
||||
}}>
|
||||
<el-icon>
|
||||
<Minus />
|
||||
</el-icon>
|
||||
</button>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
class="maximize"
|
||||
onClick={() => {
|
||||
changeFullscreen(true);
|
||||
}}>
|
||||
<el-icon>
|
||||
<FullScreen />
|
||||
</el-icon>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
// 关闭按钮
|
||||
case "close":
|
||||
return (
|
||||
<button type="button" class="close" onClick={close}>
|
||||
<el-icon>
|
||||
<Close />
|
||||
</el-icon>
|
||||
</button>
|
||||
);
|
||||
|
||||
// 自定义按钮
|
||||
default:
|
||||
return renderNode(e, {
|
||||
slots
|
||||
});
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
expose({
|
||||
Dialog,
|
||||
visible,
|
||||
isFullscreen,
|
||||
open,
|
||||
close,
|
||||
changeFullscreen
|
||||
});
|
||||
|
||||
return () => {
|
||||
return h(
|
||||
<el-dialog
|
||||
ref={Dialog}
|
||||
class="cl-dialog"
|
||||
width={props.width}
|
||||
beforeClose={props.beforeClose}
|
||||
show-close={false}
|
||||
append-to-body
|
||||
fullscreen={isFullscreen.value}
|
||||
v-model={visible.value}
|
||||
onClose={onClose}
|
||||
/>,
|
||||
{},
|
||||
{
|
||||
header() {
|
||||
return renderHeader();
|
||||
},
|
||||
default() {
|
||||
return (
|
||||
<el-scrollbar
|
||||
class="cl-dialog__container"
|
||||
key={cacheKey.value}
|
||||
style={{ height: props.height }}>
|
||||
<div class="cl-dialog__default" style={{ padding: props.padding }}>
|
||||
{slots.default?.()}
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
);
|
||||
},
|
||||
footer() {
|
||||
const d = slots.footer?.();
|
||||
|
||||
if (d && d[0]?.shapeFlag) {
|
||||
return <div class="cl-dialog__footer">{d}</div>;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
||||
});
|
15
packages/crud/src/components/error-message/index.tsx
Normal file
15
packages/crud/src/components/error-message/index.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
import { defineComponent } from "vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "cl-error-message",
|
||||
|
||||
props: {
|
||||
title: String
|
||||
},
|
||||
|
||||
setup(props) {
|
||||
return () => {
|
||||
return <el-alert title={props.title} type="error" />;
|
||||
};
|
||||
}
|
||||
});
|
23
packages/crud/src/components/filter/index.tsx
Normal file
23
packages/crud/src/components/filter/index.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import { defineComponent } from "vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "cl-filter",
|
||||
|
||||
props: {
|
||||
label: String
|
||||
},
|
||||
|
||||
setup(props, { slots }) {
|
||||
return () => {
|
||||
return (
|
||||
<div class="cl-filter">
|
||||
<span class="cl-filter__label" v-show={props.label}>
|
||||
{props.label}
|
||||
</span>
|
||||
|
||||
{slots.default?.()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
});
|
11
packages/crud/src/components/flex1/index.tsx
Normal file
11
packages/crud/src/components/flex1/index.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
import { defineComponent } from "vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "cl-flex1",
|
||||
|
||||
setup() {
|
||||
return () => {
|
||||
return <div class="cl-flex1" />;
|
||||
};
|
||||
}
|
||||
});
|
51
packages/crud/src/components/form-card/index.tsx
Normal file
51
packages/crud/src/components/form-card/index.tsx
Normal file
@ -0,0 +1,51 @@
|
||||
import { defineComponent, ref } from "vue";
|
||||
import { ArrowDown, ArrowUp } from "@element-plus/icons-vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "cl-form-card",
|
||||
|
||||
components: {
|
||||
ArrowDown,
|
||||
ArrowUp
|
||||
},
|
||||
|
||||
props: {
|
||||
label: String,
|
||||
// 展开状态
|
||||
expand: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 是否能展开、收起
|
||||
isExpand: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
|
||||
setup(props, { slots }) {
|
||||
const visible = ref(props.expand);
|
||||
|
||||
function toExpand() {
|
||||
if (props.isExpand) {
|
||||
visible.value = !visible.value;
|
||||
}
|
||||
}
|
||||
|
||||
return () => {
|
||||
return (
|
||||
<div class={["cl-form-card", { "is-expand": visible.value }]}>
|
||||
<div class="cl-form-card__header" v-show={props.label} onClick={toExpand}>
|
||||
<span>{props.label}</span>
|
||||
|
||||
<el-icon v-show={props.isExpand}>
|
||||
<arrow-down v-show={!visible.value} />
|
||||
<arrow-up v-show={visible.value} />
|
||||
</el-icon>
|
||||
</div>
|
||||
<div class="cl-form-card__container">{slots.default?.()}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
});
|
145
packages/crud/src/components/form-tabs/index.tsx
Normal file
145
packages/crud/src/components/form-tabs/index.tsx
Normal file
@ -0,0 +1,145 @@
|
||||
import {
|
||||
defineComponent,
|
||||
h,
|
||||
nextTick,
|
||||
onMounted,
|
||||
PropType,
|
||||
reactive,
|
||||
ref,
|
||||
toRaw,
|
||||
watch
|
||||
} from "vue";
|
||||
import { isEmpty } from "lodash-es";
|
||||
import { useRefs, useDialog } from "../../hooks";
|
||||
|
||||
export default defineComponent({
|
||||
name: "cl-form-tabs",
|
||||
|
||||
props: {
|
||||
modelValue: [String, Number],
|
||||
labels: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
justify: {
|
||||
type: String as PropType<
|
||||
"start" | "end" | "left" | "right" | "center" | "justify" | "match-parent"
|
||||
>,
|
||||
default: "center"
|
||||
},
|
||||
type: {
|
||||
type: String as PropType<"card" | "default">,
|
||||
default: "default"
|
||||
}
|
||||
},
|
||||
|
||||
emits: ["update:modelValue", "change"],
|
||||
|
||||
setup(props, { emit, expose }) {
|
||||
const { refs, setRefs } = useRefs();
|
||||
|
||||
// 标识
|
||||
const active = ref("");
|
||||
|
||||
// 切换列表
|
||||
const list = ref<any[]>([]);
|
||||
|
||||
// 下划线
|
||||
const line = reactive({
|
||||
width: "",
|
||||
offsetLeft: "",
|
||||
transform: "",
|
||||
backgroundColor: ""
|
||||
});
|
||||
|
||||
function update(val: any) {
|
||||
if (!val) {
|
||||
return false;
|
||||
}
|
||||
|
||||
nextTick(() => {
|
||||
const index = list.value.findIndex((e) => e.value === val);
|
||||
const item = refs[`tab-${index}`];
|
||||
|
||||
if (item) {
|
||||
// 下划线位置
|
||||
line.width = item.offsetWidth + "px";
|
||||
line.transform = `translateX(${item.offsetLeft}px)`;
|
||||
|
||||
// 靠左位置
|
||||
let left = item.offsetLeft + item.clientWidth / 2 - 414 / 2 + 15;
|
||||
|
||||
if (left < 0) {
|
||||
left = 0;
|
||||
}
|
||||
|
||||
// 设置滚动距离
|
||||
refs.tabs.scrollLeft = left;
|
||||
}
|
||||
});
|
||||
|
||||
active.value = val;
|
||||
emit("update:modelValue", val);
|
||||
}
|
||||
|
||||
// 监听绑定值变化
|
||||
watch(() => props.modelValue, update);
|
||||
|
||||
// 监听值修改
|
||||
watch(
|
||||
() => active.value,
|
||||
(val) => {
|
||||
emit("change", val);
|
||||
}
|
||||
);
|
||||
|
||||
useDialog({
|
||||
onFullscreen() {
|
||||
update(active.value);
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(function () {
|
||||
if (!isEmpty(props.labels)) {
|
||||
list.value = props.labels;
|
||||
update(isEmpty(props.modelValue) ? list.value[0].value : props.modelValue);
|
||||
}
|
||||
});
|
||||
|
||||
expose({
|
||||
active,
|
||||
list,
|
||||
line,
|
||||
update
|
||||
});
|
||||
|
||||
return () => {
|
||||
return (
|
||||
<div class={["cl-form-tabs", `cl-form-tabs--${props.type}`]}>
|
||||
<div
|
||||
class="cl-form-tabs__wrap"
|
||||
style={{ textAlign: props.justify }}
|
||||
ref={setRefs("tabs")}>
|
||||
<ul>
|
||||
{list.value.map((e, i) => {
|
||||
return (
|
||||
<li
|
||||
ref={setRefs(`tab-${i}`)}
|
||||
class={{ "is-active": e.value === active.value }}
|
||||
onClick={() => {
|
||||
update(e.value);
|
||||
}}>
|
||||
{e.icon && <el-icon>{h(toRaw(e.icon))}</el-icon>}
|
||||
<span>{e.label}</span>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
|
||||
{line.width && <div class="cl-form-tabs__line" style={line}></div>}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
});
|
145
packages/crud/src/components/form/helper/action.ts
Normal file
145
packages/crud/src/components/form/helper/action.ts
Normal file
@ -0,0 +1,145 @@
|
||||
import { dataset } from "../../../utils";
|
||||
|
||||
export function useAction({
|
||||
config,
|
||||
form,
|
||||
Form
|
||||
}: {
|
||||
config: ClForm.Config;
|
||||
form: obj;
|
||||
Form: Vue.Ref<any>;
|
||||
}) {
|
||||
// 设置数据
|
||||
function set(
|
||||
{
|
||||
prop,
|
||||
key,
|
||||
path
|
||||
}: { prop?: string; key?: "options" | "props" | "hidden" | "hidden-toggle"; path?: string },
|
||||
data?: any
|
||||
) {
|
||||
let p: string = path || "";
|
||||
|
||||
if (path) {
|
||||
dataset(config, p, data);
|
||||
} else {
|
||||
let d: any;
|
||||
|
||||
if (prop) {
|
||||
function deep(arr: ClForm.Item[]) {
|
||||
arr.forEach((e) => {
|
||||
if (e.prop == prop) {
|
||||
d = e;
|
||||
} else {
|
||||
if (e.children) {
|
||||
deep(e.children);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
deep(config.items);
|
||||
}
|
||||
|
||||
if (d) {
|
||||
switch (key) {
|
||||
case "options":
|
||||
d.component.options = data;
|
||||
break;
|
||||
|
||||
case "props":
|
||||
Object.assign(d.component.props, data);
|
||||
break;
|
||||
|
||||
case "hidden":
|
||||
d.hidden = data;
|
||||
break;
|
||||
|
||||
case "hidden-toggle":
|
||||
d.hidden = data === undefined ? !d.hidden : !data;
|
||||
break;
|
||||
|
||||
default:
|
||||
Object.assign(d, data);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
console.error(`Prop[${prop}] is not found`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取表单值
|
||||
function getForm(prop: string) {
|
||||
return prop ? form[prop] : form;
|
||||
}
|
||||
|
||||
// 设置表单值
|
||||
function setForm(prop: string, value: any) {
|
||||
form[prop] = value;
|
||||
}
|
||||
|
||||
// 设置配置
|
||||
function setConfig(path: string, value: any) {
|
||||
set({ path }, value);
|
||||
}
|
||||
|
||||
// 设置数据
|
||||
function setData(prop: string, value: any) {
|
||||
set({ prop }, value);
|
||||
}
|
||||
|
||||
// 设置表单项的下拉数据列表
|
||||
function setOptions(prop: string, value: any[]) {
|
||||
set({ prop, key: "options" }, value);
|
||||
}
|
||||
|
||||
// 设置表单项的组件参数
|
||||
function setProps(prop: string, value: any) {
|
||||
set({ prop, key: "props" }, value);
|
||||
}
|
||||
|
||||
// 切换表单项的显示、隐藏
|
||||
function toggleItem(prop: string, value?: boolean) {
|
||||
set({ prop, key: "hidden-toggle" }, value);
|
||||
}
|
||||
|
||||
// 对部分表单项隐藏
|
||||
function hideItem(...props: string[]) {
|
||||
props.forEach((prop) => {
|
||||
set({ prop, key: "hidden" }, true);
|
||||
});
|
||||
}
|
||||
|
||||
// 对部分表单项显示
|
||||
function showItem(...props: string[]) {
|
||||
props.forEach((prop) => {
|
||||
set({ prop, key: "hidden" }, false);
|
||||
});
|
||||
}
|
||||
|
||||
// 设置标题
|
||||
function setTitle(value: string) {
|
||||
config.title = value;
|
||||
}
|
||||
|
||||
// 是否展开表单项
|
||||
function collapseItem(e: any) {
|
||||
Form.value?.clearValidate(e.prop);
|
||||
e.collapse = !e.collapse;
|
||||
}
|
||||
|
||||
return {
|
||||
getForm,
|
||||
setForm,
|
||||
setData,
|
||||
setConfig,
|
||||
setOptions,
|
||||
setProps,
|
||||
toggleItem,
|
||||
hideItem,
|
||||
showItem,
|
||||
setTitle,
|
||||
collapseItem
|
||||
};
|
||||
}
|
34
packages/crud/src/components/form/helper/api.ts
Normal file
34
packages/crud/src/components/form/helper/api.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import { useElApi } from "../../../hooks";
|
||||
|
||||
export function useApi({ Form }: { Form: Vue.Ref<any> }) {
|
||||
return useElApi(
|
||||
[
|
||||
"open",
|
||||
"close",
|
||||
"clear",
|
||||
"reset",
|
||||
"submit",
|
||||
"bindForm",
|
||||
"changeTab",
|
||||
"setTitle",
|
||||
"showLoading",
|
||||
"hideLoading",
|
||||
"collapseItem",
|
||||
"getForm",
|
||||
"setForm",
|
||||
"setData",
|
||||
"setConfig",
|
||||
"setOptions",
|
||||
"setProps",
|
||||
"toggleItem",
|
||||
"hideItem",
|
||||
"showItem",
|
||||
"validate",
|
||||
"validateField",
|
||||
"resetFields",
|
||||
"scrollToField",
|
||||
"clearValidate"
|
||||
],
|
||||
Form
|
||||
);
|
||||
}
|
62
packages/crud/src/components/form/helper/index.ts
Normal file
62
packages/crud/src/components/form/helper/index.ts
Normal file
@ -0,0 +1,62 @@
|
||||
import { reactive, ref } from "vue";
|
||||
import { useConfig } from "../../../hooks";
|
||||
|
||||
export function useForm() {
|
||||
const { dict } = useConfig();
|
||||
|
||||
// 表单配置
|
||||
const config = reactive<ClForm.Config>({
|
||||
title: "-",
|
||||
height: undefined,
|
||||
width: "50%",
|
||||
props: {
|
||||
labelWidth: 100
|
||||
},
|
||||
on: {},
|
||||
op: {
|
||||
hidden: false,
|
||||
saveButtonText: dict.label.save,
|
||||
closeButtonText: dict.label.close,
|
||||
buttons: ["close", "save"]
|
||||
},
|
||||
dialog: {
|
||||
closeOnClickModal: false,
|
||||
appendToBody: true
|
||||
},
|
||||
items: [],
|
||||
form: {},
|
||||
_data: {}
|
||||
});
|
||||
|
||||
const Form = ref();
|
||||
|
||||
// 表单数据
|
||||
const form = reactive<obj>({});
|
||||
|
||||
// 表单是否可见
|
||||
const visible = ref(false);
|
||||
|
||||
// 表单提交保存状态
|
||||
const saving = ref(false);
|
||||
|
||||
// 表单加载状态
|
||||
const loading = ref(false);
|
||||
|
||||
// 表单禁用状态
|
||||
const disabled = ref(false);
|
||||
|
||||
return {
|
||||
Form,
|
||||
config,
|
||||
form,
|
||||
visible,
|
||||
saving,
|
||||
loading,
|
||||
disabled
|
||||
};
|
||||
}
|
||||
|
||||
export * from "./action";
|
||||
export * from "./api";
|
||||
export * from "./plugins";
|
||||
export * from "./tabs";
|
86
packages/crud/src/components/form/helper/plugins.ts
Normal file
86
packages/crud/src/components/form/helper/plugins.ts
Normal file
@ -0,0 +1,86 @@
|
||||
import { Ref, WatchStopHandle, getCurrentInstance, watch } from "vue";
|
||||
|
||||
export function usePlugins({ visible }: { visible: Ref<boolean> }) {
|
||||
const that: any = getCurrentInstance();
|
||||
|
||||
interface Event {
|
||||
onOpen: (() => void)[];
|
||||
onClose: (() => void)[];
|
||||
onSubmit: ((data: obj) => obj)[];
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
// 事件
|
||||
const ev: Event = {
|
||||
onOpen: [],
|
||||
onClose: [],
|
||||
onSubmit: []
|
||||
};
|
||||
|
||||
// 监听器
|
||||
let timer: WatchStopHandle | null = null;
|
||||
|
||||
// 插件创建
|
||||
function create(plugins?: ClForm.Plugin[]) {
|
||||
for (const i in ev) {
|
||||
ev[i] = [];
|
||||
}
|
||||
|
||||
if (timer) {
|
||||
timer();
|
||||
}
|
||||
|
||||
if (plugins) {
|
||||
plugins.forEach((p) => {
|
||||
p({
|
||||
exposed: that.exposed,
|
||||
onOpen(cb: any) {
|
||||
ev.onOpen.push(cb);
|
||||
},
|
||||
onClose(cb: any) {
|
||||
ev.onClose.push(cb);
|
||||
},
|
||||
onSubmit(cb: any) {
|
||||
ev.onSubmit.push(cb);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
timer = watch(
|
||||
visible,
|
||||
(val) => {
|
||||
if (val) {
|
||||
setTimeout(() => {
|
||||
ev.onOpen.forEach((e) => e());
|
||||
}, 10);
|
||||
} else {
|
||||
ev.onClose.forEach((e) => e());
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 表单提交
|
||||
async function submit(data: any) {
|
||||
let d = data;
|
||||
|
||||
for (let i = 0; i < ev.onSubmit.length; i++) {
|
||||
const d2 = await ev.onSubmit[i](d);
|
||||
|
||||
if (d2) {
|
||||
d = d2;
|
||||
}
|
||||
}
|
||||
|
||||
return d;
|
||||
}
|
||||
|
||||
return {
|
||||
create,
|
||||
submit
|
||||
};
|
||||
}
|
84
packages/crud/src/components/form/helper/tabs.ts
Normal file
84
packages/crud/src/components/form/helper/tabs.ts
Normal file
@ -0,0 +1,84 @@
|
||||
import { ref } from "vue";
|
||||
|
||||
export function useTabs({ config, Form }: { config: ClForm.Config; Form: Vue.Ref<any> }) {
|
||||
// 选中
|
||||
const active = ref<any>();
|
||||
|
||||
// 获取参数
|
||||
function get() {
|
||||
return config.items.find((e) => e.type === "tabs");
|
||||
}
|
||||
|
||||
function set(data: any) {
|
||||
active.value = data;
|
||||
}
|
||||
|
||||
function clear() {
|
||||
active.value = null;
|
||||
}
|
||||
|
||||
// 切换
|
||||
function change(value: any, isValid = true) {
|
||||
return new Promise((resolve: Function, reject: Function) => {
|
||||
function next() {
|
||||
active.value = value;
|
||||
resolve();
|
||||
}
|
||||
|
||||
if (isValid) {
|
||||
let isError = false;
|
||||
|
||||
const arr = config.items
|
||||
.filter((e: any) => e.group == active.value && !e._hidden && e.prop)
|
||||
.map((e: any) => {
|
||||
return new Promise((r: Function) => {
|
||||
// 验证表单
|
||||
Form.value.validateField(e.prop, (valid: string) => {
|
||||
if (valid) {
|
||||
isError = true;
|
||||
}
|
||||
|
||||
r(valid);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Promise.all(arr).then((msg) => {
|
||||
if (isError) {
|
||||
reject(msg.filter(Boolean));
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 合并
|
||||
function mergeProp(item: ClForm.Item) {
|
||||
const d = get();
|
||||
|
||||
if (d && d.props) {
|
||||
const { mergeProp, labels = [] } = d.props;
|
||||
|
||||
if (mergeProp) {
|
||||
const t = labels.find((e) => e.value == item.group);
|
||||
|
||||
if (t && t.name) {
|
||||
item.prop = `${t.name}-${item.prop}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
active,
|
||||
get,
|
||||
set,
|
||||
change,
|
||||
clear,
|
||||
mergeProp
|
||||
};
|
||||
}
|
625
packages/crud/src/components/form/index.tsx
Normal file
625
packages/crud/src/components/form/index.tsx
Normal file
@ -0,0 +1,625 @@
|
||||
import { defineComponent, h, nextTick } from "vue";
|
||||
import { cloneDeep, isBoolean, isEmpty, merge } from "lodash-es";
|
||||
import { useAction, useForm, usePlugins, useTabs } from "./helper";
|
||||
import { useBrowser, useConfig, useElApi } from "../../hooks";
|
||||
import { getValue } from "../../utils";
|
||||
import formHook from "../../utils/form-hook";
|
||||
import { renderNode } from "../../utils/vnode";
|
||||
import { parseFormHidden } from "../../utils/parse";
|
||||
|
||||
export default defineComponent({
|
||||
name: "cl-form",
|
||||
|
||||
props: {
|
||||
inner: Boolean,
|
||||
inline: Boolean
|
||||
},
|
||||
|
||||
setup(props, { expose, slots }) {
|
||||
const { style, dict } = useConfig();
|
||||
const browser = useBrowser();
|
||||
const { Form, config, form, visible, saving, loading, disabled } = useForm();
|
||||
|
||||
// 关闭的操作类型
|
||||
let closeAction: ClForm.CloseAction = "close";
|
||||
|
||||
// 旧表单数据
|
||||
let defForm: obj | undefined;
|
||||
|
||||
// 选项卡
|
||||
const Tabs = useTabs({ config, Form });
|
||||
|
||||
// 操作
|
||||
const Action = useAction({ config, form, Form });
|
||||
|
||||
// 方法
|
||||
const ElFormApi = useElApi(
|
||||
["validate", "validateField", "resetFields", "scrollToField", "clearValidate"],
|
||||
Form
|
||||
);
|
||||
|
||||
// 插件
|
||||
const plugin = usePlugins({ visible });
|
||||
|
||||
// 显示加载中
|
||||
function showLoading() {
|
||||
loading.value = true;
|
||||
}
|
||||
|
||||
// 隐藏加载
|
||||
function hideLoading() {
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
// 设置是否禁用
|
||||
function setDisabled(val: boolean = true) {
|
||||
disabled.value = val;
|
||||
}
|
||||
|
||||
// 请求表单保存状态
|
||||
function done() {
|
||||
saving.value = false;
|
||||
}
|
||||
|
||||
// 关闭表单
|
||||
function close(action?: ClForm.CloseAction) {
|
||||
if (action) {
|
||||
closeAction = action;
|
||||
}
|
||||
|
||||
beforeClose(() => {
|
||||
visible.value = false;
|
||||
done();
|
||||
});
|
||||
}
|
||||
|
||||
// 关闭前
|
||||
function beforeClose(done: fn) {
|
||||
if (config.on?.close) {
|
||||
config.on.close(closeAction, done);
|
||||
} else {
|
||||
done();
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭后
|
||||
function onClosed() {
|
||||
Tabs.clear();
|
||||
Form.value?.clearValidate();
|
||||
}
|
||||
|
||||
// 清空表单验证
|
||||
function clear() {
|
||||
for (const i in form) {
|
||||
delete form[i];
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
Form.value?.clearValidate();
|
||||
}, 0);
|
||||
}
|
||||
|
||||
// 重置
|
||||
function reset() {
|
||||
if (defForm) {
|
||||
for (const i in defForm) {
|
||||
form[i] = cloneDeep(defForm[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 表单提交
|
||||
function submit(callback?: fn) {
|
||||
// 验证表单
|
||||
Form.value.validate(async (valid: boolean, error: any) => {
|
||||
if (valid) {
|
||||
saving.value = true;
|
||||
|
||||
// 拷贝表单值
|
||||
const d = cloneDeep(form);
|
||||
|
||||
// 过滤隐藏的表单项
|
||||
config.items.forEach((e) => {
|
||||
if (e._hidden) {
|
||||
if (e.prop) {
|
||||
delete d[e.prop];
|
||||
}
|
||||
}
|
||||
|
||||
if (e.hook) {
|
||||
formHook.submit({
|
||||
...e,
|
||||
value: e.prop ? d[e.prop] : undefined,
|
||||
form: d
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 处理 "-" 多层级
|
||||
for (const i in d) {
|
||||
if (i.includes("-")) {
|
||||
// 结构参数
|
||||
const [a, ...arr] = i.split("-");
|
||||
|
||||
// 关键值的key
|
||||
const k: string = arr.pop() || "";
|
||||
|
||||
if (!d[a]) {
|
||||
d[a] = {};
|
||||
}
|
||||
|
||||
let f: any = d[a];
|
||||
|
||||
// 设置默认值
|
||||
arr.forEach((e: any) => {
|
||||
if (!f[e]) {
|
||||
f[e] = {};
|
||||
}
|
||||
|
||||
f = f[e];
|
||||
});
|
||||
|
||||
// 设置关键值
|
||||
f[k] = d[i];
|
||||
|
||||
delete d[i];
|
||||
}
|
||||
}
|
||||
|
||||
const submit = callback || config.on?.submit;
|
||||
|
||||
// 提交事件
|
||||
if (submit) {
|
||||
submit(await plugin.submit(d), {
|
||||
close() {
|
||||
close("save");
|
||||
},
|
||||
done
|
||||
});
|
||||
} else {
|
||||
done();
|
||||
}
|
||||
} else {
|
||||
// 判断是否使用form-tabs,切换到对应的选项卡
|
||||
const keys = Object.keys(error);
|
||||
|
||||
if (Tabs.active.value) {
|
||||
const item = config.items.find((e) => e.prop === keys[0]);
|
||||
|
||||
if (item) {
|
||||
Tabs.set(item.group);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 打开表单
|
||||
function open(options?: ClForm.Options, plugins?: ClForm.Plugin[]) {
|
||||
if (!options) {
|
||||
return console.error("Options is not null");
|
||||
}
|
||||
|
||||
// 清空
|
||||
if (options.isReset !== false) {
|
||||
clear();
|
||||
}
|
||||
|
||||
// 显示对话框
|
||||
visible.value = true;
|
||||
|
||||
// 默认关闭方式
|
||||
closeAction = "close";
|
||||
|
||||
// 合并配置
|
||||
for (const i in config) {
|
||||
switch (i) {
|
||||
// 表单项
|
||||
case "items":
|
||||
function deep(arr: any[]): any[] {
|
||||
return arr.map((e) => {
|
||||
const d = getValue(e);
|
||||
|
||||
return {
|
||||
...d,
|
||||
children: d?.children ? deep(d.children) : undefined
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
config.items = deep(options.items || []);
|
||||
break;
|
||||
// 事件、参数、操作
|
||||
case "on":
|
||||
case "op":
|
||||
case "props":
|
||||
case "dialog":
|
||||
case "_data":
|
||||
merge(config[i], options[i] || {});
|
||||
break;
|
||||
// 其他
|
||||
default:
|
||||
config[i] = options[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 预设表单值
|
||||
if (options?.form) {
|
||||
for (const i in options.form) {
|
||||
form[i] = options.form[i];
|
||||
}
|
||||
}
|
||||
|
||||
// 设置表单数据
|
||||
config.items.map((e) => {
|
||||
function deep(e: ClForm.Item) {
|
||||
if (e.prop) {
|
||||
// 解析 prop
|
||||
if (e.prop.includes(".")) {
|
||||
e.prop = e.prop.replace(/\./g, "-");
|
||||
}
|
||||
|
||||
// prop 合并
|
||||
Tabs.mergeProp(e);
|
||||
|
||||
// 绑定值
|
||||
formHook.bind({
|
||||
...e,
|
||||
value: form[e.prop] !== undefined ? form[e.prop] : cloneDeep(e.value),
|
||||
form
|
||||
});
|
||||
|
||||
// 表单验证
|
||||
if (e.required) {
|
||||
e.rules = {
|
||||
required: true,
|
||||
message: `${e.label}${dict.label.nonEmpty}`
|
||||
};
|
||||
}
|
||||
|
||||
// 子集
|
||||
if (e.children) {
|
||||
e.children.forEach(deep);
|
||||
}
|
||||
}
|
||||
|
||||
// 设置 tabs 默认值
|
||||
if (e.type == "tabs") {
|
||||
Tabs.set(e.value);
|
||||
}
|
||||
}
|
||||
|
||||
deep(e);
|
||||
});
|
||||
|
||||
// 设置默认值
|
||||
if (!defForm) {
|
||||
defForm = cloneDeep(form);
|
||||
}
|
||||
|
||||
// 创建插件
|
||||
plugin.create(plugins);
|
||||
|
||||
// 打开回调
|
||||
nextTick(() => {
|
||||
setTimeout(() => {
|
||||
// 打开事件
|
||||
if (config.on?.open) {
|
||||
config.on.open(form);
|
||||
}
|
||||
}, 10);
|
||||
});
|
||||
}
|
||||
|
||||
// 绑定表单数据
|
||||
function bindForm(data: any) {
|
||||
config.items.forEach((e) => {
|
||||
formHook.bind({
|
||||
...e,
|
||||
value: e.prop ? data[e.prop] : undefined,
|
||||
form: data
|
||||
});
|
||||
});
|
||||
|
||||
Object.assign(form, data);
|
||||
}
|
||||
|
||||
// 渲染表单项
|
||||
function renderFormItem(e: ClForm.Item) {
|
||||
const { isDisabled } = config._data;
|
||||
|
||||
if (e.type == "tabs") {
|
||||
return <cl-form-tabs v-model={Tabs.active.value} {...e.props} />;
|
||||
}
|
||||
|
||||
// 是否隐藏
|
||||
e._hidden = parseFormHidden(e.hidden, {
|
||||
scope: form
|
||||
});
|
||||
|
||||
// 分组显示
|
||||
const inGroup =
|
||||
isEmpty(Tabs.active.value) || isEmpty(e.group)
|
||||
? true
|
||||
: e.group === Tabs.active.value;
|
||||
|
||||
// 表单项
|
||||
const FormItem = e.component
|
||||
? h(
|
||||
<el-form-item
|
||||
class={{
|
||||
"no-label": !(e.renderLabel || e.label),
|
||||
"has-children": !!e.children
|
||||
}}
|
||||
label-width={props.inline ? "auto" : ""}
|
||||
label={e.label}
|
||||
prop={e.prop}
|
||||
rules={isDisabled ? null : e.rules}
|
||||
v-show={inGroup}
|
||||
/>,
|
||||
e.props,
|
||||
{
|
||||
label() {
|
||||
return e.renderLabel
|
||||
? renderNode(e.renderLabel, {
|
||||
scope: form,
|
||||
render: "slot",
|
||||
slots
|
||||
})
|
||||
: e.label;
|
||||
},
|
||||
default() {
|
||||
return (
|
||||
<div>
|
||||
<div class="cl-form-item">
|
||||
{["prepend", "component", "append"]
|
||||
.filter((k) => e[k])
|
||||
.map((name) => {
|
||||
const children = e.children && (
|
||||
<div class="cl-form-item__children">
|
||||
<el-row gutter={10}>
|
||||
{e.children.map(renderFormItem)}
|
||||
</el-row>
|
||||
</div>
|
||||
);
|
||||
|
||||
const Item = renderNode(e[name], {
|
||||
item: e,
|
||||
prop: e.prop,
|
||||
scope: form,
|
||||
slots,
|
||||
children,
|
||||
_data: {
|
||||
isDisabled
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
v-show={!e.collapse}
|
||||
class={[
|
||||
`cl-form-item__${name}`,
|
||||
{
|
||||
flex1: e.flex !== false
|
||||
}
|
||||
]}
|
||||
style={e[name].style}>
|
||||
{Item}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{isBoolean(e.collapse) && (
|
||||
<div
|
||||
class="cl-form-item__collapse"
|
||||
onClick={() => {
|
||||
Action.collapseItem(e);
|
||||
}}>
|
||||
<el-divider content-position="center">
|
||||
{e.collapse
|
||||
? dict.label.seeMore
|
||||
: dict.label.hideContent}
|
||||
</el-divider>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
)
|
||||
: null;
|
||||
|
||||
// 隐藏
|
||||
if (e._hidden) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 行内
|
||||
if (props.inline) {
|
||||
return FormItem;
|
||||
}
|
||||
|
||||
return (
|
||||
<el-col key={e.prop} span={e.span || style.form.span} {...e.col}>
|
||||
{FormItem}
|
||||
</el-col>
|
||||
);
|
||||
}
|
||||
|
||||
// 渲染表单
|
||||
function renderContainer() {
|
||||
// 表单项列表
|
||||
const children = config.items.map(renderFormItem);
|
||||
|
||||
return (
|
||||
<div class="cl-form__container">
|
||||
{h(
|
||||
<el-form
|
||||
ref={Form}
|
||||
size={style.size}
|
||||
label-position={
|
||||
browser.isMini && !props.inline ? "top" : style.form.labelPostion
|
||||
}
|
||||
label-width={style.form.labelWidth}
|
||||
inline={props.inline}
|
||||
disabled={saving.value}
|
||||
scroll-to-error
|
||||
model={form}
|
||||
onSubmit={(e: Event) => {
|
||||
submit();
|
||||
e.preventDefault();
|
||||
}}
|
||||
/>,
|
||||
config.props,
|
||||
{
|
||||
default: () => {
|
||||
return (
|
||||
<div class="cl-form__items">
|
||||
{/* 前 */}
|
||||
{slots.prepend && slots.prepend({ scope: form })}
|
||||
|
||||
{/* 项 */}
|
||||
{props.inline ? (
|
||||
children
|
||||
) : (
|
||||
<el-row gutter={10} v-loading={loading.value}>
|
||||
{children}
|
||||
</el-row>
|
||||
)}
|
||||
|
||||
{/* 后 */}
|
||||
{slots.append && slots.append({ scope: form })}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 渲染表单按钮
|
||||
function renderFooter() {
|
||||
const { hidden, buttons, saveButtonText, closeButtonText, justify } = config.op;
|
||||
|
||||
if (hidden) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const Btns = buttons?.map((e: any) => {
|
||||
switch (e) {
|
||||
case "save":
|
||||
return (
|
||||
<el-button
|
||||
type="success"
|
||||
size={style.size}
|
||||
disabled={loading.value}
|
||||
loading={saving.value}
|
||||
onClick={() => {
|
||||
submit();
|
||||
}}>
|
||||
{saveButtonText}
|
||||
</el-button>
|
||||
);
|
||||
case "close":
|
||||
return (
|
||||
<el-button
|
||||
size={style.size}
|
||||
onClick={() => {
|
||||
close("close");
|
||||
}}>
|
||||
{closeButtonText}
|
||||
</el-button>
|
||||
);
|
||||
default:
|
||||
return renderNode(e, {
|
||||
scope: form,
|
||||
slots,
|
||||
custom({ scope }) {
|
||||
return (
|
||||
<el-button
|
||||
text
|
||||
type={e.type}
|
||||
bg
|
||||
onClick={() => {
|
||||
e.onClick({ scope });
|
||||
}}>
|
||||
{e.label}
|
||||
</el-button>
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
class="cl-form__footer"
|
||||
style={{
|
||||
justifyContent: justify || "flex-end"
|
||||
}}>
|
||||
{Btns}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
expose({
|
||||
Form,
|
||||
visible,
|
||||
saving,
|
||||
form,
|
||||
config,
|
||||
loading,
|
||||
disabled,
|
||||
open,
|
||||
close,
|
||||
done,
|
||||
clear,
|
||||
reset,
|
||||
submit,
|
||||
bindForm,
|
||||
showLoading,
|
||||
hideLoading,
|
||||
setDisabled,
|
||||
Tabs,
|
||||
...Action,
|
||||
...ElFormApi
|
||||
});
|
||||
|
||||
return () => {
|
||||
const Form = (
|
||||
<div class="cl-form">
|
||||
{renderContainer()}
|
||||
{renderFooter()}
|
||||
</div>
|
||||
);
|
||||
|
||||
if (props.inner) {
|
||||
return visible.value && Form;
|
||||
} else {
|
||||
return h(
|
||||
<cl-dialog v-model={visible.value} class="cl-form" />,
|
||||
{
|
||||
title: config.title,
|
||||
height: config.height,
|
||||
width: config.width,
|
||||
...config.dialog,
|
||||
beforeClose,
|
||||
onClosed,
|
||||
keepAlive: false
|
||||
},
|
||||
{
|
||||
default() {
|
||||
return renderContainer();
|
||||
},
|
||||
footer() {
|
||||
return renderFooter();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
48
packages/crud/src/components/index.tsx
Normal file
48
packages/crud/src/components/index.tsx
Normal file
@ -0,0 +1,48 @@
|
||||
import { App } from "vue";
|
||||
import Crud from "./crud";
|
||||
import AddBtn from "./add-btn";
|
||||
import AdvBtn from "./adv/btn";
|
||||
import AdvSearch from "./adv/search";
|
||||
import Flex from "./flex1";
|
||||
import Form from "./form";
|
||||
import FormTabs from "./form-tabs";
|
||||
import FormCard from "./form-card";
|
||||
import MultiDeleteBtn from "./multi-delete-btn";
|
||||
import Pagination from "./pagination";
|
||||
import RefreshBtn from "./refresh-btn";
|
||||
import SearchKey from "./search-key";
|
||||
import Table from "./table";
|
||||
import Upsert from "./upsert";
|
||||
import Dialog from "./dialog";
|
||||
import Filter from "./filter";
|
||||
import Search from "./search";
|
||||
import ErrorMessage from "./error-message";
|
||||
import Row from "./row";
|
||||
|
||||
export const components: { [key: string]: any } = {
|
||||
Crud,
|
||||
AddBtn,
|
||||
AdvBtn,
|
||||
AdvSearch,
|
||||
Flex,
|
||||
Form,
|
||||
FormTabs,
|
||||
FormCard,
|
||||
MultiDeleteBtn,
|
||||
Pagination,
|
||||
RefreshBtn,
|
||||
SearchKey,
|
||||
Table,
|
||||
Upsert,
|
||||
Dialog,
|
||||
Filter,
|
||||
Search,
|
||||
ErrorMessage,
|
||||
Row
|
||||
};
|
||||
|
||||
export function useComponent(app: App) {
|
||||
for (const i in components) {
|
||||
app.component(components[i].name, components[i]);
|
||||
}
|
||||
}
|
27
packages/crud/src/components/multi-delete-btn/index.tsx
Normal file
27
packages/crud/src/components/multi-delete-btn/index.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
import { defineComponent } from "vue";
|
||||
import { useConfig, useCore } from "../../hooks";
|
||||
|
||||
export default defineComponent({
|
||||
name: "cl-multi-delete-btn",
|
||||
|
||||
setup(_, { slots }) {
|
||||
const { crud } = useCore();
|
||||
const { style } = useConfig();
|
||||
|
||||
return () => {
|
||||
return (
|
||||
crud.getPermission("delete") && (
|
||||
<el-button
|
||||
type="danger"
|
||||
size={style.size}
|
||||
disabled={crud.selection.length === 0}
|
||||
onClick={() => {
|
||||
crud.rowDelete(...crud.selection);
|
||||
}}>
|
||||
{slots.default?.() || crud.dict.label.multiDelete}
|
||||
</el-button>
|
||||
)
|
||||
);
|
||||
};
|
||||
}
|
||||
});
|
89
packages/crud/src/components/pagination/index.tsx
Normal file
89
packages/crud/src/components/pagination/index.tsx
Normal file
@ -0,0 +1,89 @@
|
||||
import { defineComponent, h, onMounted, onUnmounted, ref } from "vue";
|
||||
import { useBrowser, useConfig, useCore } from "../../hooks";
|
||||
|
||||
export default defineComponent({
|
||||
name: "cl-pagination",
|
||||
|
||||
setup(_, { expose }) {
|
||||
const { crud, mitt } = useCore();
|
||||
const { style } = useConfig();
|
||||
const browser = useBrowser();
|
||||
|
||||
// 总数
|
||||
const total = ref(0);
|
||||
|
||||
// 当前页数
|
||||
const currentPage = ref(1);
|
||||
|
||||
// 每页大小
|
||||
const pageSize = ref(20);
|
||||
|
||||
// 页数发生变化
|
||||
function onCurrentChange(index: number) {
|
||||
crud.refresh({
|
||||
page: index
|
||||
});
|
||||
}
|
||||
|
||||
// 条目发生变化
|
||||
function onSizeChange(size: number) {
|
||||
crud.refresh({
|
||||
page: 1,
|
||||
size
|
||||
});
|
||||
}
|
||||
|
||||
// 设置分页信息
|
||||
function setPagination(res: obj) {
|
||||
if (res) {
|
||||
currentPage.value = res.currentPage || res.page || 1;
|
||||
pageSize.value = res.pageSize || res.size || 20;
|
||||
total.value = res.total || 0;
|
||||
crud.params.size = pageSize.value;
|
||||
}
|
||||
}
|
||||
|
||||
// 数据刷新
|
||||
function onRefresh(res: ClCrud.Response["page"]) {
|
||||
setPagination(res.pagination);
|
||||
}
|
||||
|
||||
// 监听刷新事件
|
||||
onMounted(() => {
|
||||
mitt.on("crud.refresh", onRefresh);
|
||||
});
|
||||
|
||||
// 移除监听事件
|
||||
onUnmounted(() => {
|
||||
mitt.off("crud.refresh", onRefresh);
|
||||
});
|
||||
|
||||
expose({
|
||||
total,
|
||||
currentPage,
|
||||
pageSize,
|
||||
setPagination
|
||||
});
|
||||
|
||||
return () => {
|
||||
return h(
|
||||
<el-pagination
|
||||
small={style.size == "small" || browser.isMini}
|
||||
background
|
||||
page-sizes={[10, 20, 30, 40, 50, 100]}
|
||||
pager-count={browser.isMini ? 5 : 7}
|
||||
layout={
|
||||
browser.isMini ? "total, pager" : "total, sizes, prev, pager, next, jumper"
|
||||
}
|
||||
/>,
|
||||
{
|
||||
onSizeChange,
|
||||
onCurrentChange,
|
||||
total: total.value,
|
||||
currentPage: currentPage.value,
|
||||
pageSize: pageSize.value
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
||||
});
|
23
packages/crud/src/components/refresh-btn/index.tsx
Normal file
23
packages/crud/src/components/refresh-btn/index.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import { defineComponent } from "vue";
|
||||
import { useConfig, useCore } from "../../hooks";
|
||||
|
||||
export default defineComponent({
|
||||
name: "cl-refresh-btn",
|
||||
|
||||
setup(_, { slots }) {
|
||||
const { crud } = useCore();
|
||||
const { style } = useConfig();
|
||||
|
||||
return () => {
|
||||
return (
|
||||
<el-button
|
||||
size={style.size}
|
||||
onClick={() => {
|
||||
crud.refresh();
|
||||
}}>
|
||||
{slots.default?.() || crud.dict.label.refresh}
|
||||
</el-button>
|
||||
);
|
||||
};
|
||||
}
|
||||
});
|
11
packages/crud/src/components/row/index.tsx
Normal file
11
packages/crud/src/components/row/index.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
import { defineComponent } from "vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "cl-row",
|
||||
|
||||
setup(_, { slots }) {
|
||||
return () => {
|
||||
return <el-row class="cl-row">{slots.default && slots.default()}</el-row>;
|
||||
};
|
||||
}
|
||||
});
|
177
packages/crud/src/components/search-key/index.tsx
Normal file
177
packages/crud/src/components/search-key/index.tsx
Normal file
@ -0,0 +1,177 @@
|
||||
import { defineComponent, ref, watch, computed, PropType } from "vue";
|
||||
import { useConfig, useCore } from "../../hooks";
|
||||
import { parsePx } from "../../utils";
|
||||
|
||||
export default defineComponent({
|
||||
name: "cl-search-key",
|
||||
|
||||
props: {
|
||||
// 绑定值
|
||||
modelValue: String,
|
||||
// 选中字段
|
||||
field: {
|
||||
type: String,
|
||||
default: "keyWord"
|
||||
},
|
||||
// 字段列表
|
||||
fieldList: {
|
||||
type: Array as PropType<Array<{ label: string; value: string }>>,
|
||||
default: () => []
|
||||
},
|
||||
// 搜索时的钩子
|
||||
onSearch: Function,
|
||||
// 输入框占位内容
|
||||
placeholder: String,
|
||||
// 宽度
|
||||
width: {
|
||||
type: [String, Number],
|
||||
default: 300
|
||||
}
|
||||
},
|
||||
|
||||
emits: ["update:modelValue", "change", "field-change"],
|
||||
|
||||
setup(props, { emit, expose }) {
|
||||
const { crud } = useCore();
|
||||
const { style } = useConfig();
|
||||
|
||||
// 选中字段
|
||||
const selectField = ref(props.field);
|
||||
|
||||
// 加载状态
|
||||
const loading = ref(false);
|
||||
|
||||
// 文字提示
|
||||
const placeholder = computed(() => {
|
||||
if (props.placeholder) {
|
||||
return props.placeholder;
|
||||
} else {
|
||||
const item = props.fieldList.find((e) => e.value == selectField.value);
|
||||
|
||||
if (item) {
|
||||
return crud.dict.label.placeholder + item.label;
|
||||
} else {
|
||||
return crud.dict.label.searchKey;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 搜索内容
|
||||
const value = ref("");
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(val) => {
|
||||
value.value = val || "";
|
||||
},
|
||||
{
|
||||
immediate: true
|
||||
}
|
||||
);
|
||||
|
||||
// 锁
|
||||
let lock = false;
|
||||
|
||||
// 搜索
|
||||
function search() {
|
||||
if (!lock) {
|
||||
const params: obj = {};
|
||||
|
||||
props.fieldList.forEach((e) => {
|
||||
params[e.value] = undefined;
|
||||
});
|
||||
|
||||
async function next(newParams?: obj) {
|
||||
loading.value = true;
|
||||
|
||||
await crud.refresh({
|
||||
page: 1,
|
||||
...params,
|
||||
[selectField.value]: value.value || undefined,
|
||||
...newParams
|
||||
});
|
||||
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
if (props.onSearch) {
|
||||
props.onSearch(params, { next });
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 回车搜索
|
||||
function onKeydown({ key }: KeyboardEvent) {
|
||||
if (key === "Enter") {
|
||||
search();
|
||||
}
|
||||
}
|
||||
|
||||
// 监听输入
|
||||
function onInput(val: string) {
|
||||
emit("update:modelValue", val);
|
||||
emit("change", val);
|
||||
}
|
||||
|
||||
// 监听变化
|
||||
function onChange() {
|
||||
search();
|
||||
lock = true;
|
||||
|
||||
setTimeout(() => {
|
||||
lock = false;
|
||||
}, 300);
|
||||
}
|
||||
|
||||
// 监听字段选择
|
||||
function onFieldChange() {
|
||||
emit("field-change", selectField.value);
|
||||
onInput("");
|
||||
value.value = "";
|
||||
}
|
||||
|
||||
expose({
|
||||
search
|
||||
});
|
||||
|
||||
return () => {
|
||||
return (
|
||||
<div class="cl-search-key">
|
||||
<el-select
|
||||
class="cl-search-key__select"
|
||||
filterable
|
||||
size={style.size}
|
||||
v-model={selectField.value}
|
||||
v-show={props.fieldList.length > 0}
|
||||
onChange={onFieldChange}>
|
||||
{props.fieldList.map((e, i) => (
|
||||
<el-option key={i} label={e.label} value={e.value} />
|
||||
))}
|
||||
</el-select>
|
||||
|
||||
<div class="cl-search-key__wrap" style={{ width: parsePx(props.width) }}>
|
||||
<el-input
|
||||
v-model={value.value}
|
||||
size={style.size}
|
||||
placeholder={placeholder.value}
|
||||
onKeydown={onKeydown}
|
||||
onInput={onInput}
|
||||
onChange={onChange}
|
||||
clearable
|
||||
/>
|
||||
|
||||
<el-button
|
||||
size={style.size}
|
||||
type="primary"
|
||||
loading={loading.value}
|
||||
onClick={search}>
|
||||
{crud.dict.label.search}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
});
|
149
packages/crud/src/components/search/index.tsx
Normal file
149
packages/crud/src/components/search/index.tsx
Normal file
@ -0,0 +1,149 @@
|
||||
import { useConfig, useCore, useForm } from "../../hooks";
|
||||
import { isEmpty } from "lodash-es";
|
||||
import { onMounted, PropType, defineComponent, ref, h, reactive, inject, mergeProps } from "vue";
|
||||
import { useApi } from "../form/helper";
|
||||
|
||||
export default defineComponent({
|
||||
name: "cl-search",
|
||||
|
||||
props: {
|
||||
// 表单值
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
|
||||
// 列
|
||||
items: {
|
||||
type: Array as PropType<ClForm.Item[]>,
|
||||
default: () => []
|
||||
},
|
||||
|
||||
// 是否需要重置按钮
|
||||
resetBtn: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
|
||||
// 初始化
|
||||
onLoad: Function,
|
||||
|
||||
// 搜索时钩子
|
||||
onSearch: Function
|
||||
},
|
||||
|
||||
setup(props, { slots, expose, emit }) {
|
||||
const { crud } = useCore();
|
||||
const { style } = useConfig();
|
||||
|
||||
// 配置
|
||||
const config = reactive<ClSearch.Config>(
|
||||
mergeProps(props, inject("useSearch__options") || {})
|
||||
);
|
||||
|
||||
// cl-form
|
||||
const Form = useForm();
|
||||
|
||||
// 加载中
|
||||
const loading = ref(false);
|
||||
|
||||
// 搜索
|
||||
function search(params?: any) {
|
||||
const form = Form.value?.getForm();
|
||||
|
||||
async function next(data?: any) {
|
||||
loading.value = true;
|
||||
|
||||
const d = {
|
||||
page: 1,
|
||||
...form,
|
||||
...data,
|
||||
...params
|
||||
};
|
||||
|
||||
for (const i in d) {
|
||||
if (d[i] === "") {
|
||||
d[i] = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
const res = await crud.refresh(d);
|
||||
|
||||
loading.value = false;
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
if (config.onSearch) {
|
||||
config.onSearch(form, { next });
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
}
|
||||
|
||||
// 重置
|
||||
function reset() {
|
||||
Form.value?.reset();
|
||||
emit("reset");
|
||||
}
|
||||
|
||||
expose({
|
||||
search,
|
||||
reset,
|
||||
...useApi({ Form })
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
Form.value?.open({
|
||||
op: {
|
||||
hidden: true
|
||||
},
|
||||
items: config.items,
|
||||
form: config.data,
|
||||
on: {
|
||||
open(data) {
|
||||
config.onLoad?.(data);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return () => {
|
||||
return (
|
||||
isEmpty(config.items) || (
|
||||
<div class="cl-search">
|
||||
{h(
|
||||
<cl-form ref={Form} inner inline />,
|
||||
{},
|
||||
{
|
||||
append() {
|
||||
return (
|
||||
<el-form-item>
|
||||
<el-button
|
||||
type="primary"
|
||||
loading={loading.value}
|
||||
size={style.size}
|
||||
onClick={() => {
|
||||
search();
|
||||
}}>
|
||||
{crud.dict.label.search}
|
||||
</el-button>
|
||||
{config.resetBtn && (
|
||||
<el-button size={style.size} onClick={reset}>
|
||||
{crud.dict.label.reset}
|
||||
</el-button>
|
||||
)}
|
||||
</el-form-item>
|
||||
);
|
||||
},
|
||||
...slots
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
};
|
||||
}
|
||||
});
|
35
packages/crud/src/components/table/helper/data.ts
Normal file
35
packages/crud/src/components/table/helper/data.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { nextTick, ref } from "vue";
|
||||
import { useCore } from "../../../hooks";
|
||||
|
||||
export function useData({ config, Table }: { config: ClTable.Config; Table: Vue.Ref<any> }) {
|
||||
const { mitt, crud } = useCore();
|
||||
|
||||
// 列表数据
|
||||
const data = ref<obj[]>([]);
|
||||
|
||||
// 设置数据
|
||||
function setData(list: obj[]) {
|
||||
data.value = list;
|
||||
}
|
||||
|
||||
// 监听刷新
|
||||
mitt.on("crud.refresh", ({ list }: ClCrud.Response["page"]) => {
|
||||
data.value = list;
|
||||
|
||||
// 显示选中行
|
||||
nextTick(() => {
|
||||
crud.selection.forEach((e) => {
|
||||
const d = list.find((a) => a[config.rowKey] == e[config.rowKey]);
|
||||
|
||||
if (d) {
|
||||
Table.value.toggleRowSelection(d, true);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
data,
|
||||
setData
|
||||
};
|
||||
}
|
94
packages/crud/src/components/table/helper/height.ts
Normal file
94
packages/crud/src/components/table/helper/height.ts
Normal file
@ -0,0 +1,94 @@
|
||||
import { debounce, last } from "lodash-es";
|
||||
import { nextTick, onActivated, onMounted, ref } from "vue";
|
||||
import { addClass } from "../../../utils";
|
||||
import { mitt } from "../../../utils/mitt";
|
||||
|
||||
// 表格高度
|
||||
export function useHeight({ config, Table }: { Table: Vue.Ref<any>; config: ClTable.Config }) {
|
||||
// 最大高度
|
||||
const maxHeight = ref(0);
|
||||
|
||||
// 计算表格最大高度
|
||||
const update = debounce(async () => {
|
||||
await nextTick();
|
||||
|
||||
let vm = Table.value;
|
||||
|
||||
if (vm) {
|
||||
while (!vm.$parent?.$el.className.includes("cl-crud")) {
|
||||
vm = vm.$parent;
|
||||
}
|
||||
|
||||
if (vm) {
|
||||
const p = vm.$parent.$el;
|
||||
|
||||
await nextTick();
|
||||
|
||||
// 高度
|
||||
let h = 0;
|
||||
|
||||
// 表格下间距
|
||||
if (vm.$el.className.includes("cl-row")) {
|
||||
h += 10;
|
||||
}
|
||||
|
||||
// 上高度
|
||||
h += vm.$el.offsetTop;
|
||||
|
||||
// 获取下高度
|
||||
let n = vm.$el.nextSibling;
|
||||
|
||||
// 集合
|
||||
let arr = [vm.$el];
|
||||
|
||||
while (n) {
|
||||
if (n.offsetHeight > 0) {
|
||||
h += n.offsetHeight || 0;
|
||||
arr.push(n);
|
||||
|
||||
if (n.className.includes("cl-row--last")) {
|
||||
h += 10;
|
||||
}
|
||||
}
|
||||
|
||||
n = n.nextSibling;
|
||||
}
|
||||
|
||||
// 最后一个可视元素
|
||||
const z = last(arr);
|
||||
|
||||
// 去掉 cl-row 下间距高度
|
||||
if (z?.className.includes("cl-row")) {
|
||||
addClass(z, "cl-row--last");
|
||||
h -= 10;
|
||||
}
|
||||
|
||||
// 上间距
|
||||
h += parseInt(window.getComputedStyle(p).paddingTop, 10);
|
||||
|
||||
// 设置最大高度
|
||||
if (config.autoHeight) {
|
||||
maxHeight.value = p.clientHeight - h;
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 100);
|
||||
|
||||
// 窗口大小改变事件
|
||||
mitt.on("resize", () => {
|
||||
update();
|
||||
});
|
||||
|
||||
onMounted(function () {
|
||||
update();
|
||||
});
|
||||
|
||||
onActivated(function () {
|
||||
update();
|
||||
});
|
||||
|
||||
return {
|
||||
maxHeight,
|
||||
calcMaxHeight: update
|
||||
};
|
||||
}
|
31
packages/crud/src/components/table/helper/index.ts
Normal file
31
packages/crud/src/components/table/helper/index.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { inject, reactive, ref } from "vue";
|
||||
import { useConfig } from "../../../hooks";
|
||||
import { getValue, mergeConfig } from "../../../utils";
|
||||
|
||||
export function useTable(props: any) {
|
||||
const { style } = useConfig();
|
||||
|
||||
const Table = ref();
|
||||
|
||||
// 配置
|
||||
const config = reactive<ClTable.Config>(mergeConfig(props, inject("useTable__options") || {}));
|
||||
|
||||
// 列表项动态处理
|
||||
config.columns = (config.columns || []).map((e) => getValue(e));
|
||||
|
||||
// 自动高度
|
||||
config.autoHeight = config.autoHeight ?? style.table.autoHeight;
|
||||
|
||||
// 右键菜单
|
||||
config.contextMenu = config.contextMenu ?? style.table.contextMenu;
|
||||
|
||||
return { Table, config };
|
||||
}
|
||||
|
||||
export * from "./data";
|
||||
export * from "./height";
|
||||
export * from "./op";
|
||||
export * from "./render";
|
||||
export * from "./row";
|
||||
export * from "./selection";
|
||||
export * from "./sort";
|
69
packages/crud/src/components/table/helper/op.ts
Normal file
69
packages/crud/src/components/table/helper/op.ts
Normal file
@ -0,0 +1,69 @@
|
||||
import { nextTick, ref } from "vue";
|
||||
import { useCore } from "../../../hooks";
|
||||
import { isArray, isBoolean } from "lodash-es";
|
||||
|
||||
export function useOp({ config }: { config: ClTable.Config }) {
|
||||
const { mitt } = useCore();
|
||||
|
||||
// 是否可见,用于解决一些显示隐藏的副作用
|
||||
const visible = ref(true);
|
||||
|
||||
// 重新构建
|
||||
async function reBuild(cb?: fn) {
|
||||
visible.value = false;
|
||||
|
||||
await nextTick();
|
||||
|
||||
if (cb) {
|
||||
cb();
|
||||
}
|
||||
|
||||
visible.value = true;
|
||||
|
||||
await nextTick();
|
||||
|
||||
mitt.emit("resize");
|
||||
}
|
||||
|
||||
// 显示列
|
||||
function showColumn(prop: string | string[], status?: boolean) {
|
||||
const keys = isArray(prop) ? prop : [prop];
|
||||
|
||||
// 多级表头
|
||||
function deep(list: ClTable.Column[]) {
|
||||
list.forEach((e) => {
|
||||
if (e.prop && keys.includes(e.prop)) {
|
||||
e.hidden = isBoolean(status) ? !status : false;
|
||||
}
|
||||
|
||||
if (e.children) {
|
||||
deep(e.children);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
deep(config.columns);
|
||||
}
|
||||
|
||||
// 隐藏列
|
||||
function hideColumn(prop: string | string[]) {
|
||||
showColumn(prop, false);
|
||||
}
|
||||
|
||||
// 设置列
|
||||
function setColumns(list: ClTable.Column[]) {
|
||||
if (list) {
|
||||
reBuild(() => {
|
||||
config.columns.splice(0, config.columns.length, ...list);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
visible,
|
||||
reBuild,
|
||||
showColumn,
|
||||
hideColumn,
|
||||
setColumns
|
||||
};
|
||||
}
|
174
packages/crud/src/components/table/helper/render.tsx
Normal file
174
packages/crud/src/components/table/helper/render.tsx
Normal file
@ -0,0 +1,174 @@
|
||||
import { h, useSlots } from "vue";
|
||||
import { useCore, useBrowser, useConfig } from "../../../hooks";
|
||||
import { cloneDeep, isEmpty, orderBy } from "lodash-es";
|
||||
import { getValue } from "../../../utils";
|
||||
import { parseTableDict, parseTableOpButtons } from "../../../utils/parse";
|
||||
import { renderNode } from "../../../utils/vnode";
|
||||
|
||||
// 渲染
|
||||
export function useRender() {
|
||||
const browser = useBrowser();
|
||||
const slots = useSlots();
|
||||
const { crud } = useCore();
|
||||
const { style } = useConfig();
|
||||
|
||||
// 渲染列
|
||||
function renderColumn(columns: ClTable.Column[]) {
|
||||
const arr = columns.map((e) => {
|
||||
const d = getValue(e);
|
||||
|
||||
if (!d.orderNum) {
|
||||
d.orderNum = 0;
|
||||
}
|
||||
|
||||
return d;
|
||||
});
|
||||
|
||||
return orderBy(arr, "orderNum", "asc")
|
||||
.map((item, index) => {
|
||||
if (item.hidden) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const ElTableColumn = (
|
||||
<el-table-column
|
||||
key={`cl-table-column__${index}`}
|
||||
align={style.table.column.align}
|
||||
header-align={style.table.column.headerAlign}
|
||||
minWidth={style.table.column.minWidth}
|
||||
/>
|
||||
);
|
||||
|
||||
// 操作按钮
|
||||
if (item.type === "op") {
|
||||
return h(
|
||||
ElTableColumn,
|
||||
{
|
||||
label: crud.dict.label.op,
|
||||
width: "160px",
|
||||
fixed: browser.isMini ? null : "right",
|
||||
...item
|
||||
},
|
||||
{
|
||||
default: (scope: any) => {
|
||||
return (
|
||||
<div class="cl-table__op">
|
||||
{parseTableOpButtons(item.buttons, { scope })}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
// 多选,序号
|
||||
else if (["selection", "index"].includes(item.type)) {
|
||||
return h(ElTableColumn, item);
|
||||
}
|
||||
// 默认
|
||||
else {
|
||||
function deep(item: ClTable.Column) {
|
||||
if (item.hidden) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const props: obj = cloneDeep(item);
|
||||
|
||||
// Cannot set property children of #<Element>
|
||||
delete props.children;
|
||||
|
||||
return h(ElTableColumn, props, {
|
||||
header(scope: any) {
|
||||
const slot = slots[`header-${item.prop}`];
|
||||
|
||||
if (slot) {
|
||||
return slot({
|
||||
scope
|
||||
});
|
||||
} else {
|
||||
return scope.column.label;
|
||||
}
|
||||
},
|
||||
default(scope: any) {
|
||||
if (item.children) {
|
||||
return item.children.map(deep);
|
||||
}
|
||||
|
||||
// 使用插槽
|
||||
const slot = slots[`column-${item.prop}`];
|
||||
|
||||
if (slot) {
|
||||
return slot({
|
||||
scope,
|
||||
item
|
||||
});
|
||||
} else {
|
||||
// 绑定值
|
||||
let value = scope.row[item.prop];
|
||||
|
||||
// 格式化
|
||||
if (item.formatter) {
|
||||
value = item.formatter(
|
||||
scope.row,
|
||||
scope.column,
|
||||
value,
|
||||
scope.$index
|
||||
);
|
||||
}
|
||||
|
||||
// 自定义渲染
|
||||
if (item.component) {
|
||||
return renderNode(item.component, {
|
||||
prop: item.prop,
|
||||
scope: scope.row,
|
||||
_data: {
|
||||
column: scope.column,
|
||||
index: scope.$index,
|
||||
row: scope.row
|
||||
}
|
||||
});
|
||||
}
|
||||
// 字典状态
|
||||
else if (item.dict) {
|
||||
return parseTableDict(value, item);
|
||||
}
|
||||
// 空数据
|
||||
else if (isEmpty(value)) {
|
||||
return scope.emptyText;
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return deep(item);
|
||||
}
|
||||
})
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
// 插槽 empty
|
||||
function renderEmpty(emptyText: String) {
|
||||
return (
|
||||
<div class="cl-table__empty">
|
||||
{slots.empty ? (
|
||||
slots.empty()
|
||||
) : (
|
||||
<el-empty image-size={100} description={emptyText}></el-empty>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 插槽 append
|
||||
function renderAppend() {
|
||||
return <div class="cl-table__append">{slots.append && slots.append()}</div>;
|
||||
}
|
||||
|
||||
return {
|
||||
renderColumn,
|
||||
renderEmpty,
|
||||
renderAppend
|
||||
};
|
||||
}
|
130
packages/crud/src/components/table/helper/row.ts
Normal file
130
packages/crud/src/components/table/helper/row.ts
Normal file
@ -0,0 +1,130 @@
|
||||
import { isEmpty, isFunction } from "lodash-es";
|
||||
import { useCore } from "../../../hooks";
|
||||
import { ContextMenu } from "../../context-menu";
|
||||
|
||||
// 单元行事件
|
||||
export function useRow({
|
||||
Table,
|
||||
config,
|
||||
Sort
|
||||
}: {
|
||||
Table: Vue.Ref<any>;
|
||||
config: ClTable.Config;
|
||||
Sort: {
|
||||
defaultSort: {
|
||||
prop?: string;
|
||||
order?: string;
|
||||
};
|
||||
changeSort(prop: string, order: string): void;
|
||||
};
|
||||
}) {
|
||||
const { crud } = useCore();
|
||||
|
||||
// 右键菜单
|
||||
function onRowContextMenu(row: obj, column: obj, event: PointerEvent) {
|
||||
// 菜单按钮
|
||||
const buttons = config.contextMenu;
|
||||
// 是否开启
|
||||
const enable = !isEmpty(buttons);
|
||||
|
||||
if (enable) {
|
||||
// 高亮
|
||||
Table.value.setCurrentRow(row);
|
||||
|
||||
// 解析按钮
|
||||
const list = buttons
|
||||
.map((e) => {
|
||||
switch (e) {
|
||||
case "refresh":
|
||||
return {
|
||||
label: crud.dict.label.refresh,
|
||||
callback(done: fn) {
|
||||
crud.refresh();
|
||||
done();
|
||||
}
|
||||
};
|
||||
case "edit":
|
||||
case "update":
|
||||
return {
|
||||
label: crud.dict.label.update,
|
||||
hidden: !crud.getPermission("update"),
|
||||
callback(done: fn) {
|
||||
crud.rowEdit(row);
|
||||
done();
|
||||
}
|
||||
};
|
||||
case "delete":
|
||||
return {
|
||||
label: crud.dict.label.delete,
|
||||
hidden: !crud.getPermission("delete"),
|
||||
callback(done: fn) {
|
||||
crud.rowDelete(row);
|
||||
done();
|
||||
}
|
||||
};
|
||||
case "info":
|
||||
return {
|
||||
label: crud.dict.label.info,
|
||||
hidden: !crud.getPermission("info"),
|
||||
callback(done: fn) {
|
||||
crud.rowInfo(row);
|
||||
done();
|
||||
}
|
||||
};
|
||||
case "check":
|
||||
return {
|
||||
label: crud.selection.find((e) => e.id == row.id)
|
||||
? crud.dict.label.deselect
|
||||
: crud.dict.label.select,
|
||||
hidden: !config.columns.find((e) => e.type === "selection"),
|
||||
callback(done: fn) {
|
||||
Table.value.toggleRowSelection(row);
|
||||
done();
|
||||
}
|
||||
};
|
||||
case "order-desc":
|
||||
return {
|
||||
label: `${column.label} - ${crud.dict.label.desc}`,
|
||||
hidden: !column.sortable,
|
||||
callback(done: fn) {
|
||||
Sort.changeSort(column.property, "desc");
|
||||
done();
|
||||
}
|
||||
};
|
||||
case "order-asc":
|
||||
return {
|
||||
label: `${column.label} - ${crud.dict.label.asc}`,
|
||||
hidden: !column.sortable,
|
||||
callback(done: fn) {
|
||||
Sort.changeSort(column.property, "asc");
|
||||
done();
|
||||
}
|
||||
};
|
||||
default:
|
||||
if (isFunction(e)) {
|
||||
return e(row, column, event);
|
||||
} else {
|
||||
return e;
|
||||
}
|
||||
}
|
||||
})
|
||||
.filter((e) => Boolean(e) && !e.hidden);
|
||||
|
||||
// 打开菜单
|
||||
if (!isEmpty(list)) {
|
||||
ContextMenu.open(event, {
|
||||
list
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 回调
|
||||
if (config.onRowContextmenu) {
|
||||
config.onRowContextmenu(row, column, event);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
onRowContextMenu
|
||||
};
|
||||
}
|
16
packages/crud/src/components/table/helper/selection.ts
Normal file
16
packages/crud/src/components/table/helper/selection.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { useCore } from "../../../hooks";
|
||||
|
||||
export function useSelection({ emit }: { emit: Vue.Emit }) {
|
||||
const { crud } = useCore();
|
||||
|
||||
// 选择项发生变化
|
||||
function onSelectionChange(selection: any[]) {
|
||||
crud.selection.splice(0, crud.selection.length, ...selection);
|
||||
emit("selection-change", crud.selection);
|
||||
}
|
||||
|
||||
return {
|
||||
selection: crud.selection,
|
||||
onSelectionChange
|
||||
};
|
||||
}
|
86
packages/crud/src/components/table/helper/sort.ts
Normal file
86
packages/crud/src/components/table/helper/sort.ts
Normal file
@ -0,0 +1,86 @@
|
||||
import { useCore } from "../../../hooks";
|
||||
|
||||
// 排序
|
||||
export function useSort({
|
||||
config,
|
||||
Table,
|
||||
emit
|
||||
}: {
|
||||
config: ClTable.Config;
|
||||
Table: Vue.Ref<any>;
|
||||
emit: Vue.Emit;
|
||||
}) {
|
||||
const { crud } = useCore();
|
||||
|
||||
// 设置默认排序Ï
|
||||
const defaultSort = (function () {
|
||||
let { prop, order } = config.defaultSort || {};
|
||||
|
||||
const item = config.columns.find((e) =>
|
||||
["desc", "asc", "descending", "ascending"].find((a) => a == e.sortable)
|
||||
);
|
||||
|
||||
if (item) {
|
||||
prop = item.prop;
|
||||
order = ["descending", "desc"].find((a) => a == item.sortable)
|
||||
? "descending"
|
||||
: "ascending";
|
||||
}
|
||||
|
||||
if (order && prop) {
|
||||
crud.params.order = ["descending", "desc"].includes(order) ? "desc" : "asc";
|
||||
crud.params.prop = prop;
|
||||
|
||||
return {
|
||||
prop,
|
||||
order
|
||||
};
|
||||
}
|
||||
|
||||
return {};
|
||||
})();
|
||||
|
||||
// 排序监听
|
||||
function onSortChange({ prop, order }: { prop: string | undefined; order: string }) {
|
||||
if (config.sortRefresh) {
|
||||
if (order === "descending") {
|
||||
order = "desc";
|
||||
}
|
||||
|
||||
if (order === "ascending") {
|
||||
order = "asc";
|
||||
}
|
||||
|
||||
if (!order) {
|
||||
prop = undefined;
|
||||
}
|
||||
|
||||
crud.refresh({
|
||||
prop,
|
||||
order,
|
||||
page: 1
|
||||
});
|
||||
}
|
||||
|
||||
emit("sort-change", { prop, order });
|
||||
}
|
||||
|
||||
// 改变排序
|
||||
function changeSort(prop: string, order: string) {
|
||||
if (order === "desc") {
|
||||
order = "descending";
|
||||
}
|
||||
|
||||
if (order === "asc") {
|
||||
order = "ascending";
|
||||
}
|
||||
|
||||
Table.value?.sort(prop, order);
|
||||
}
|
||||
|
||||
return {
|
||||
defaultSort,
|
||||
onSortChange,
|
||||
changeSort
|
||||
};
|
||||
}
|
157
packages/crud/src/components/table/index.tsx
Normal file
157
packages/crud/src/components/table/index.tsx
Normal file
@ -0,0 +1,157 @@
|
||||
import { defineComponent, h } from "vue";
|
||||
import {
|
||||
useRow,
|
||||
useHeight,
|
||||
useRender,
|
||||
useSort,
|
||||
useData,
|
||||
useSelection,
|
||||
useOp,
|
||||
useTable
|
||||
} from "./helper";
|
||||
import { useCore, useProxy, useElApi, useConfig } from "../../hooks";
|
||||
|
||||
export default defineComponent({
|
||||
name: "cl-table",
|
||||
|
||||
props: {
|
||||
// 列配置
|
||||
columns: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
// 是否自动计算高度
|
||||
autoHeight: {
|
||||
type: Boolean,
|
||||
default: null
|
||||
},
|
||||
// 固定高度
|
||||
height: null,
|
||||
// 右键菜单
|
||||
contextMenu: {
|
||||
type: [Array, Boolean],
|
||||
default: null
|
||||
},
|
||||
// 默认排序
|
||||
defaultSort: Object,
|
||||
// 排序后是否刷新
|
||||
sortRefresh: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 空数据显示文案
|
||||
emptyText: String,
|
||||
// 当前行的 key
|
||||
rowKey: {
|
||||
type: String,
|
||||
default: "id"
|
||||
}
|
||||
},
|
||||
|
||||
emits: ["selection-change", "sort-change"],
|
||||
|
||||
setup(props, { emit, expose }) {
|
||||
const { crud } = useCore();
|
||||
const { style } = useConfig();
|
||||
const { Table, config } = useTable(props);
|
||||
|
||||
// 排序
|
||||
const Sort = useSort({ config, emit, Table });
|
||||
|
||||
// 行
|
||||
const Row = useRow({
|
||||
config,
|
||||
Table,
|
||||
Sort
|
||||
});
|
||||
|
||||
// 高度
|
||||
const Height = useHeight({ config, Table });
|
||||
|
||||
// 数据
|
||||
const Data = useData({ config, Table });
|
||||
|
||||
// 多选
|
||||
const Selection = useSelection({ emit });
|
||||
|
||||
// 操作
|
||||
const Op = useOp({ config });
|
||||
|
||||
// 方法
|
||||
const ElTableApi = useElApi(
|
||||
[
|
||||
"clearSelection",
|
||||
"getSelectionRows",
|
||||
"toggleRowSelection",
|
||||
"toggleAllSelection",
|
||||
"toggleRowExpansion",
|
||||
"setCurrentRow",
|
||||
"clearSort",
|
||||
"clearFilter",
|
||||
"doLayout",
|
||||
"sort",
|
||||
"scrollTo",
|
||||
"setScrollTop",
|
||||
"setScrollLeft"
|
||||
],
|
||||
Table
|
||||
);
|
||||
|
||||
const ctx = {
|
||||
Table,
|
||||
columns: config.columns,
|
||||
...Selection,
|
||||
...Data,
|
||||
...Sort,
|
||||
...Row,
|
||||
...Height,
|
||||
...Op,
|
||||
...ElTableApi
|
||||
};
|
||||
|
||||
useProxy(ctx);
|
||||
expose(ctx);
|
||||
|
||||
return () => {
|
||||
const { renderColumn, renderAppend, renderEmpty } = useRender();
|
||||
|
||||
return (
|
||||
ctx.visible.value &&
|
||||
h(
|
||||
<el-table class="cl-table" ref={Table} v-loading={crud.loading} />,
|
||||
{
|
||||
// config
|
||||
maxHeight: config.autoHeight ? ctx.maxHeight.value : null,
|
||||
height: config.autoHeight ? config.height : null,
|
||||
rowKey: config.rowKey,
|
||||
|
||||
// ctx
|
||||
defaultSort: ctx.defaultSort,
|
||||
data: ctx.data.value,
|
||||
onRowContextmenu: ctx.onRowContextMenu,
|
||||
onSelectionChange: ctx.onSelectionChange,
|
||||
onSortChange: ctx.onSortChange,
|
||||
|
||||
// style
|
||||
size: style.size,
|
||||
border: style.table.border,
|
||||
highlightCurrentRow: style.table.highlightCurrentRow,
|
||||
resizable: style.table.resizable,
|
||||
stripe: style.table.stripe
|
||||
},
|
||||
{
|
||||
default() {
|
||||
return renderColumn(ctx.columns);
|
||||
},
|
||||
empty() {
|
||||
return renderEmpty(config.emptyText || crud.dict.label.empty);
|
||||
},
|
||||
append() {
|
||||
return renderAppend();
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
};
|
||||
}
|
||||
});
|
306
packages/crud/src/components/upsert/index.tsx
Normal file
306
packages/crud/src/components/upsert/index.tsx
Normal file
@ -0,0 +1,306 @@
|
||||
import { defineComponent, h, inject, reactive, ref, toRefs } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { useCore, useProxy } from "../../hooks";
|
||||
import { useApi } from "../form/helper";
|
||||
import { mergeConfig } from "../../utils";
|
||||
|
||||
export default defineComponent({
|
||||
name: "cl-upsert",
|
||||
|
||||
props: {
|
||||
// 表单项
|
||||
items: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
// <el-form /> 参数
|
||||
props: Object,
|
||||
// 编辑时是否同步打开
|
||||
sync: Boolean,
|
||||
// 操作按钮参数
|
||||
op: Object,
|
||||
// <cl-dialog /> 参数
|
||||
dialog: Object,
|
||||
// 打开表单钩子
|
||||
onOpen: Function,
|
||||
// 打开表单后钩子
|
||||
onOpened: Function,
|
||||
// 关闭表单钩子
|
||||
onClose: Function,
|
||||
// 关闭表单后钩子
|
||||
onClosed: Function,
|
||||
// 获取表单数据钩子
|
||||
onInfo: Function,
|
||||
// 表单提交钩子
|
||||
onSubmit: Function
|
||||
},
|
||||
|
||||
emits: ["opened", "closed"],
|
||||
|
||||
setup(props, { slots, expose }) {
|
||||
const { crud } = useCore();
|
||||
|
||||
const config = reactive<ClUpsert.Config>(
|
||||
mergeConfig(props, inject("useUpsert__options") || {})
|
||||
);
|
||||
|
||||
// el-form
|
||||
const Form = ref<ClForm.Ref>();
|
||||
|
||||
// 模式
|
||||
const mode = ref<ClUpsert.Ref["mode"]>("info");
|
||||
|
||||
// 关闭表单
|
||||
function close(action?: ClForm.CloseAction) {
|
||||
Form.value?.close(action);
|
||||
}
|
||||
|
||||
// 关闭后
|
||||
function onClosed() {
|
||||
Form.value?.hideLoading();
|
||||
|
||||
if (config.onClosed) {
|
||||
config.onClosed();
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭前
|
||||
function beforeClose(action: ClForm.CloseAction, done: fn) {
|
||||
function next() {
|
||||
done();
|
||||
onClosed();
|
||||
}
|
||||
|
||||
if (config.onClose) {
|
||||
config.onClose(action, next);
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
}
|
||||
|
||||
// 提交
|
||||
function submit(data: obj) {
|
||||
const { service, dict, refresh } = crud;
|
||||
|
||||
function done() {
|
||||
Form.value?.done();
|
||||
}
|
||||
|
||||
function next(data: obj) {
|
||||
return new Promise((resolve, reject) => {
|
||||
// 发送请求
|
||||
service[dict.api[mode.value]](data)
|
||||
.then((res) => {
|
||||
ElMessage.success(dict.label.saveSuccess);
|
||||
done();
|
||||
close("save");
|
||||
refresh();
|
||||
resolve(res);
|
||||
})
|
||||
.catch((err) => {
|
||||
ElMessage.error(err.message);
|
||||
done();
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 提交钩子
|
||||
if (config.onSubmit) {
|
||||
config.onSubmit(data, {
|
||||
done,
|
||||
next,
|
||||
close() {
|
||||
close("save");
|
||||
}
|
||||
});
|
||||
} else {
|
||||
next(data);
|
||||
}
|
||||
}
|
||||
|
||||
// 打开表单
|
||||
function open() {
|
||||
// 是否禁用
|
||||
const isDisabled = mode.value == "info";
|
||||
|
||||
return new Promise((resolve) => {
|
||||
if (!Form.value) {
|
||||
return console.error("<cl-upsert /> is not found");
|
||||
}
|
||||
|
||||
Form.value?.open(
|
||||
{
|
||||
title: crud.dict.label[mode.value],
|
||||
props: {
|
||||
...config.props,
|
||||
disabled: isDisabled
|
||||
},
|
||||
op: {
|
||||
...config.op,
|
||||
hidden: isDisabled
|
||||
},
|
||||
dialog: config.dialog,
|
||||
items: config.items || [],
|
||||
on: {
|
||||
open(data) {
|
||||
if (config.onOpen) {
|
||||
config.onOpen(data);
|
||||
}
|
||||
|
||||
resolve(true);
|
||||
},
|
||||
submit,
|
||||
close: beforeClose
|
||||
},
|
||||
form: {},
|
||||
_data: {
|
||||
isDisabled
|
||||
}
|
||||
},
|
||||
config.plugins
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// 打开后事件
|
||||
function onOpened() {
|
||||
const data = Form.value?.getForm();
|
||||
|
||||
if (config.onOpened) {
|
||||
config.onOpened(data);
|
||||
}
|
||||
}
|
||||
|
||||
// 新增
|
||||
async function add() {
|
||||
mode.value = "add";
|
||||
|
||||
// 打开中
|
||||
await open();
|
||||
|
||||
// 打开后
|
||||
onOpened();
|
||||
}
|
||||
|
||||
// 追加
|
||||
async function append(data: any) {
|
||||
mode.value = "add";
|
||||
|
||||
// 打开中
|
||||
await open();
|
||||
|
||||
// 绑定值
|
||||
if (data) {
|
||||
Form.value?.bindForm(data);
|
||||
}
|
||||
|
||||
// 打开后
|
||||
onOpened();
|
||||
}
|
||||
|
||||
// 编辑
|
||||
function edit(data?: any) {
|
||||
mode.value = "update";
|
||||
getInfo(data);
|
||||
}
|
||||
|
||||
// 详情
|
||||
function info(data?: any) {
|
||||
mode.value = "info";
|
||||
getInfo(data);
|
||||
}
|
||||
|
||||
// 信息
|
||||
function getInfo(data: any) {
|
||||
// 显示加载中
|
||||
Form.value?.showLoading();
|
||||
|
||||
// 是否同步打开
|
||||
if (!config.sync) {
|
||||
open();
|
||||
}
|
||||
|
||||
// 完成
|
||||
async function done(data?: any) {
|
||||
// 加载完成
|
||||
Form.value?.hideLoading();
|
||||
|
||||
// 合并数据
|
||||
if (data) {
|
||||
Form.value?.bindForm(data);
|
||||
}
|
||||
|
||||
// 同步打开表单
|
||||
if (config.sync) {
|
||||
await open();
|
||||
}
|
||||
|
||||
onOpened();
|
||||
}
|
||||
|
||||
// 获取详情
|
||||
function next(data: any): Promise<any> {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
// 发送请求
|
||||
await crud.service[crud.dict.api.info]({
|
||||
[crud.dict.primaryId]: data[crud.dict.primaryId]
|
||||
})
|
||||
.then((res) => {
|
||||
done(res);
|
||||
resolve(res);
|
||||
})
|
||||
.catch((err) => {
|
||||
ElMessage.error(err.message);
|
||||
reject(err);
|
||||
});
|
||||
|
||||
// 隐藏加载框
|
||||
Form.value?.hideLoading();
|
||||
});
|
||||
}
|
||||
|
||||
// 详情钩子
|
||||
if (config.onInfo) {
|
||||
config.onInfo(data, {
|
||||
close,
|
||||
next,
|
||||
done
|
||||
});
|
||||
} else {
|
||||
next(data);
|
||||
}
|
||||
}
|
||||
|
||||
// 完成
|
||||
function done() {
|
||||
Form.value?.hideLoading();
|
||||
}
|
||||
|
||||
const ctx = {
|
||||
config,
|
||||
...toRefs(config),
|
||||
...useApi({ Form }),
|
||||
Form,
|
||||
get form() {
|
||||
return Form.value?.form || {};
|
||||
},
|
||||
mode,
|
||||
add,
|
||||
append,
|
||||
edit,
|
||||
info,
|
||||
open,
|
||||
close,
|
||||
done,
|
||||
submit
|
||||
};
|
||||
|
||||
useProxy(ctx);
|
||||
expose(ctx);
|
||||
|
||||
return () => {
|
||||
return <div class="cl-upsert">{h(<cl-form ref={Form} />, {}, slots)}</div>;
|
||||
};
|
||||
}
|
||||
});
|
27
packages/crud/src/emitter.ts
Normal file
27
packages/crud/src/emitter.ts
Normal file
@ -0,0 +1,27 @@
|
||||
export const crudList: ClCrud.Ref[] = [];
|
||||
|
||||
export const emitter: Emitter = {
|
||||
list: [],
|
||||
init(events) {
|
||||
for (const i in events) {
|
||||
this.on(i, events[i]);
|
||||
}
|
||||
},
|
||||
emit(name, data) {
|
||||
this.list.forEach((e: EmitterItem) => {
|
||||
const [_name] = e.name.split("-");
|
||||
|
||||
if (name == _name) {
|
||||
e.callback(data, {
|
||||
crudList,
|
||||
refresh(params) {
|
||||
crudList.forEach((c) => c.refresh(params));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
on(name, callback) {
|
||||
this.list.push({ name, callback });
|
||||
}
|
||||
};
|
1
packages/crud/src/env.d.ts
vendored
Normal file
1
packages/crud/src/env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
/// <reference types="../index" />
|
167
packages/crud/src/hooks/crud.ts
Normal file
167
packages/crud/src/hooks/crud.ts
Normal file
@ -0,0 +1,167 @@
|
||||
import { watch, ref, nextTick, getCurrentInstance, Ref, inject, provide } from "vue";
|
||||
|
||||
// 获取上级
|
||||
function useParent(name: string, r: Ref) {
|
||||
const d = getCurrentInstance();
|
||||
|
||||
if (d) {
|
||||
let parent = d.proxy?.$.parent;
|
||||
|
||||
if (parent) {
|
||||
while (parent && parent.type?.name != name && parent.type?.name != "cl-crud") {
|
||||
parent = parent?.parent;
|
||||
}
|
||||
|
||||
if (parent) {
|
||||
if (parent.type.name == name) {
|
||||
r.value = parent.exposed;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 多事件
|
||||
function useEvent(names: string[], { r, options, clear }: any) {
|
||||
const d: any = {};
|
||||
|
||||
if (!r.__ev) r.__ev = {};
|
||||
|
||||
names.forEach((k) => {
|
||||
if (!r.__ev[k]) r.__ev[k] = [];
|
||||
|
||||
if (options[k]) {
|
||||
r.__ev[k].push(options[k]);
|
||||
}
|
||||
|
||||
d[k] = (...args: any[]) => {
|
||||
r.__ev[k].filter(Boolean).forEach((e: any) => {
|
||||
e(...args);
|
||||
});
|
||||
|
||||
if (clear == k) {
|
||||
for (const i in r.__ev) {
|
||||
r.__ev[i].splice(1, 999);
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
return d;
|
||||
}
|
||||
|
||||
// crud
|
||||
export function useCrud(options?: DeepPartial<ClCrud.Options>, cb?: (app: ClCrud.Ref) => void) {
|
||||
const Crud = ref<ClCrud.Ref>();
|
||||
useParent("cl-crud", Crud);
|
||||
|
||||
if (options) {
|
||||
provide("useCrud__options", options);
|
||||
}
|
||||
|
||||
watch(Crud, (val: any) => {
|
||||
if (val) {
|
||||
if (cb) {
|
||||
cb(val);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return Crud;
|
||||
}
|
||||
|
||||
// 新增、编辑
|
||||
export function useUpsert(options?: DeepPartial<ClUpsert.Options>) {
|
||||
const Upsert = ref<ClUpsert.Ref>();
|
||||
useParent("cl-upsert", Upsert);
|
||||
|
||||
if (options) {
|
||||
provide("useUpsert__options", options);
|
||||
}
|
||||
|
||||
watch(
|
||||
Upsert,
|
||||
(val: any) => {
|
||||
if (val) {
|
||||
if (options) {
|
||||
const event = useEvent(["onOpen", "onOpened", "onClosed"], {
|
||||
r: val,
|
||||
options,
|
||||
clear: "onClosed"
|
||||
});
|
||||
|
||||
Object.assign(val.config, event);
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true
|
||||
}
|
||||
);
|
||||
|
||||
return Upsert;
|
||||
}
|
||||
|
||||
// 表格
|
||||
export function useTable(options?: DeepPartial<ClTable.Options>) {
|
||||
const Table = ref<ClTable.Ref>();
|
||||
useParent("cl-table", Table);
|
||||
|
||||
if (options) {
|
||||
provide("useTable__options", options);
|
||||
}
|
||||
|
||||
return Table;
|
||||
}
|
||||
|
||||
// 表单
|
||||
export function useForm(cb?: (app: ClForm.Ref) => void) {
|
||||
const Form = ref<ClForm.Ref>();
|
||||
useParent("cl-form", Form);
|
||||
|
||||
nextTick(() => {
|
||||
if (cb && Form.value) {
|
||||
cb(Form.value);
|
||||
}
|
||||
});
|
||||
|
||||
return Form;
|
||||
}
|
||||
|
||||
// 高级搜索
|
||||
export function useAdvSearch(options?: DeepPartial<ClAdvSearch.Options>) {
|
||||
const AdvSearch = ref<ClAdvSearch.Ref>();
|
||||
useParent("cl-adv-search", AdvSearch);
|
||||
|
||||
if (options) {
|
||||
provide("useAdvSearch__options", options);
|
||||
}
|
||||
|
||||
return AdvSearch;
|
||||
}
|
||||
|
||||
// 搜索
|
||||
export function useSearch(options?: DeepPartial<ClSearch.Options>) {
|
||||
const Search = ref<ClSearch.Ref>();
|
||||
useParent("cl-search", Search);
|
||||
|
||||
if (options) {
|
||||
provide("useSearch__options", options);
|
||||
}
|
||||
|
||||
return Search;
|
||||
}
|
||||
|
||||
// 对话框
|
||||
export function useDialog(options?: { onFullscreen(visible: boolean): void }) {
|
||||
const Dialog = inject("dialog") as ClDialog.Provide;
|
||||
|
||||
watch(
|
||||
() => Dialog?.fullscreen.value,
|
||||
(val: any) => {
|
||||
options?.onFullscreen(val);
|
||||
}
|
||||
);
|
||||
|
||||
return Dialog;
|
||||
}
|
81
packages/crud/src/hooks/index.ts
Normal file
81
packages/crud/src/hooks/index.ts
Normal file
@ -0,0 +1,81 @@
|
||||
import { Mitt } from "../utils/mitt";
|
||||
import { isFunction } from "lodash-es";
|
||||
import { getCurrentInstance, inject, reactive } from "vue";
|
||||
|
||||
export function useCore() {
|
||||
const crud = inject("crud") as ClCrud.Ref;
|
||||
const mitt = inject("mitt") as Mitt;
|
||||
|
||||
return {
|
||||
crud,
|
||||
mitt
|
||||
};
|
||||
}
|
||||
|
||||
export function useConfig() {
|
||||
return inject("__config__") as Config;
|
||||
}
|
||||
|
||||
export function useBrowser() {
|
||||
return inject("__browser__") as Browser;
|
||||
}
|
||||
|
||||
export function useRefs() {
|
||||
const refs = reactive<{ [key: string]: obj }>({});
|
||||
|
||||
function setRefs(name: string) {
|
||||
return (el: any) => {
|
||||
refs[name] = el;
|
||||
};
|
||||
}
|
||||
|
||||
return { refs, setRefs };
|
||||
}
|
||||
|
||||
export function useProxy(ctx: any) {
|
||||
const { type }: any = getCurrentInstance();
|
||||
const { mitt, crud } = useCore();
|
||||
|
||||
// 挂载
|
||||
crud[type.name] = ctx;
|
||||
|
||||
// 事件
|
||||
mitt.on("crud.proxy", ({ name, data = [], callback }: any) => {
|
||||
if (ctx[name]) {
|
||||
let d = null;
|
||||
|
||||
if (isFunction(ctx[name])) {
|
||||
d = ctx[name](...data);
|
||||
} else {
|
||||
d = ctx[name];
|
||||
}
|
||||
|
||||
if (callback) {
|
||||
callback(d);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return ctx;
|
||||
}
|
||||
|
||||
export function useElApi(keys: string[], el: any) {
|
||||
const apis: obj = {};
|
||||
|
||||
keys.forEach((e) => {
|
||||
apis[e] = (...args: any[]) => {
|
||||
return el.value[e](...args);
|
||||
};
|
||||
});
|
||||
|
||||
return apis;
|
||||
}
|
||||
|
||||
export function useEventListener(name: string, cb: () => any) {
|
||||
window.removeEventListener(name, cb);
|
||||
window.addEventListener(name, cb);
|
||||
|
||||
cb();
|
||||
}
|
||||
|
||||
export * from "./crud";
|
31
packages/crud/src/index.ts
Normal file
31
packages/crud/src/index.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { App } from "vue";
|
||||
import { useComponent } from "./components";
|
||||
import { useProvide } from "./provide";
|
||||
import temp from "./utils/temp";
|
||||
import "./static/index.scss";
|
||||
|
||||
const Crud = {
|
||||
install(app: App, options?: Options) {
|
||||
// 临时
|
||||
temp.set("__CrudApp__", app);
|
||||
|
||||
// 穿透值
|
||||
useProvide(app, options);
|
||||
|
||||
// 设置组件
|
||||
useComponent(app);
|
||||
|
||||
return {
|
||||
name: "cl-crud"
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export default Crud;
|
||||
|
||||
export * from "./emitter";
|
||||
export * from "./hooks";
|
||||
export * from "./plugins";
|
||||
export * from "./locale";
|
||||
export { registerFormHook } from "./utils/form-hook";
|
||||
export { ContextMenu } from "./components/context-menu";
|
31
packages/crud/src/locale/en.ts
Normal file
31
packages/crud/src/locale/en.ts
Normal file
@ -0,0 +1,31 @@
|
||||
export default {
|
||||
op: "Operation",
|
||||
add: "Add",
|
||||
delete: "Delete",
|
||||
multiDelete: "Delete",
|
||||
update: "Edit",
|
||||
refresh: "Refresh",
|
||||
info: "Details",
|
||||
search: "Search",
|
||||
reset: "Reset",
|
||||
clear: "Clear",
|
||||
save: "Save",
|
||||
close: "Cancel",
|
||||
confirm: "Confirm",
|
||||
advSearch: "Advanced Search",
|
||||
searchKey: "Search Keyword",
|
||||
placeholder: "Please enter",
|
||||
tips: "Tips",
|
||||
saveSuccess: "Save successful",
|
||||
deleteSuccess: "Delete successful",
|
||||
deleteConfirm:
|
||||
"This operation will permanently delete the selected data. Do you want to continue?",
|
||||
empty: "No data available",
|
||||
desc: "Descending",
|
||||
asc: "Ascending",
|
||||
select: "Select",
|
||||
deselect: "Deselect",
|
||||
seeMore: "See more",
|
||||
hideContent: "Hide content",
|
||||
nonEmpty: "Cannot be empty"
|
||||
};
|
11
packages/crud/src/locale/index.ts
Normal file
11
packages/crud/src/locale/index.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import en from "./en";
|
||||
import ja from "./ja";
|
||||
import zhCn from "./zh-cn";
|
||||
import zhTw from "./zh-tw";
|
||||
|
||||
export const locale = {
|
||||
en,
|
||||
ja,
|
||||
zhCn,
|
||||
zhTw
|
||||
};
|
30
packages/crud/src/locale/ja.ts
Normal file
30
packages/crud/src/locale/ja.ts
Normal file
@ -0,0 +1,30 @@
|
||||
export default {
|
||||
op: "操作",
|
||||
add: "追加",
|
||||
delete: "削除",
|
||||
multiDelete: "削除",
|
||||
update: "編集",
|
||||
refresh: "リフレッシュ",
|
||||
info: "詳細",
|
||||
search: "検索",
|
||||
reset: "リセット",
|
||||
clear: "クリア",
|
||||
save: "保存",
|
||||
close: "キャンセル",
|
||||
confirm: "確認",
|
||||
advSearch: "高度な検索",
|
||||
searchKey: "検索キーワード",
|
||||
placeholder: "入力してください",
|
||||
tips: "ヒント",
|
||||
saveSuccess: "保存が成功しました",
|
||||
deleteSuccess: "削除が成功しました",
|
||||
deleteConfirm: "この操作は選択したデータを永久に削除します。続行しますか?",
|
||||
empty: "データがありません",
|
||||
desc: "降順",
|
||||
asc: "昇順",
|
||||
select: "選択",
|
||||
deselect: "選択解除",
|
||||
seeMore: "詳細を表示",
|
||||
hideContent: "コンテンツを非表示",
|
||||
nonEmpty: "空にできません"
|
||||
};
|
30
packages/crud/src/locale/zh-cn.ts
Normal file
30
packages/crud/src/locale/zh-cn.ts
Normal file
@ -0,0 +1,30 @@
|
||||
export default {
|
||||
op: "操作",
|
||||
add: "新增",
|
||||
delete: "删除",
|
||||
multiDelete: "删除",
|
||||
update: "编辑",
|
||||
refresh: "刷新",
|
||||
info: "详情",
|
||||
search: "搜索",
|
||||
reset: "重置",
|
||||
clear: "清空",
|
||||
save: "保存",
|
||||
close: "取消",
|
||||
confirm: "确定",
|
||||
advSearch: "高级搜索",
|
||||
searchKey: "搜索关键字",
|
||||
placeholder: "请输入",
|
||||
tips: "提示",
|
||||
saveSuccess: "保存成功",
|
||||
deleteSuccess: "删除成功",
|
||||
deleteConfirm: "此操作将永久删除选中数据,是否继续?",
|
||||
empty: "暂无数据",
|
||||
desc: "降序",
|
||||
asc: "升序",
|
||||
select: "选择",
|
||||
deselect: "取消选择",
|
||||
seeMore: "查看更多",
|
||||
hideContent: "隐藏内容",
|
||||
nonEmpty: "不能为空"
|
||||
};
|
30
packages/crud/src/locale/zh-tw.ts
Normal file
30
packages/crud/src/locale/zh-tw.ts
Normal file
@ -0,0 +1,30 @@
|
||||
export default {
|
||||
op: "操作",
|
||||
add: "新增",
|
||||
delete: "刪除",
|
||||
multiDelete: "刪除",
|
||||
update: "編輯",
|
||||
refresh: "刷新",
|
||||
info: "詳情",
|
||||
search: "搜尋",
|
||||
reset: "重置",
|
||||
clear: "清空",
|
||||
save: "保存",
|
||||
close: "取消",
|
||||
confirm: "確定",
|
||||
advSearch: "高級搜索",
|
||||
searchKey: "搜索關鍵字",
|
||||
placeholder: "請輸入",
|
||||
tips: "提示",
|
||||
saveSuccess: "保存成功",
|
||||
deleteSuccess: "刪除成功",
|
||||
deleteConfirm: "此操作將永久刪除選中數據,是否繼續?",
|
||||
empty: "暫無數據",
|
||||
desc: "降序",
|
||||
asc: "升序",
|
||||
select: "選擇",
|
||||
deselect: "取消選擇",
|
||||
seeMore: "查看更多",
|
||||
hideContent: "隱藏內容",
|
||||
nonEmpty: "不能為空"
|
||||
};
|
26
packages/crud/src/main.ts
Normal file
26
packages/crud/src/main.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { createApp } from "vue";
|
||||
import App from "./App.vue";
|
||||
import Crud, { locale } from "./index";
|
||||
|
||||
import ElementPlus from "element-plus";
|
||||
import "element-plus/dist/index.css";
|
||||
|
||||
const app = createApp(App);
|
||||
|
||||
app.use(ElementPlus)
|
||||
.use(Crud, {
|
||||
dict: {
|
||||
sort: {
|
||||
prop: "order",
|
||||
order: "sort"
|
||||
},
|
||||
label: locale.en
|
||||
},
|
||||
style: {
|
||||
// size: "default"
|
||||
},
|
||||
render: {
|
||||
autoHeight: true
|
||||
}
|
||||
})
|
||||
.mount("#app");
|
34
packages/crud/src/plugins/index.ts
Normal file
34
packages/crud/src/plugins/index.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import { useRefs } from "../hooks";
|
||||
|
||||
/**
|
||||
* 设置聚焦,prop为空则默认第一个选项
|
||||
* @param prop
|
||||
* @returns
|
||||
*/
|
||||
export function setFocus(prop?: string): ClForm.Plugin {
|
||||
const { refs, setRefs } = useRefs();
|
||||
|
||||
return ({ exposed, onOpen }) => {
|
||||
const name = prop || exposed.config.items[0].prop;
|
||||
|
||||
if (name) {
|
||||
function deep(arr: ClForm.Item[]) {
|
||||
arr.forEach((e) => {
|
||||
if (e.prop == name && name) {
|
||||
if (e.component) {
|
||||
e.component.ref = setRefs(name);
|
||||
}
|
||||
} else {
|
||||
deep(e.children || []);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
deep(exposed.config.items);
|
||||
|
||||
onOpen(() => {
|
||||
refs[name]?.focus();
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
133
packages/crud/src/provide.ts
Normal file
133
packages/crud/src/provide.ts
Normal file
@ -0,0 +1,133 @@
|
||||
import { App, reactive } from "vue";
|
||||
import { mitt } from "./utils/mitt";
|
||||
import { emitter } from "./emitter";
|
||||
import { locale } from "./locale";
|
||||
import { merge } from "./utils";
|
||||
|
||||
// 设置配置
|
||||
function setConfig(app: App, options: Options = {}) {
|
||||
const config = merge(
|
||||
{
|
||||
permission: {
|
||||
update: true,
|
||||
page: true,
|
||||
info: true,
|
||||
list: true,
|
||||
add: true,
|
||||
delete: true
|
||||
},
|
||||
dict: {
|
||||
primaryId: "id",
|
||||
api: {
|
||||
list: "list",
|
||||
add: "add",
|
||||
update: "update",
|
||||
delete: "delete",
|
||||
info: "info",
|
||||
page: "page"
|
||||
},
|
||||
pagination: {
|
||||
page: "page",
|
||||
size: "size"
|
||||
},
|
||||
search: {
|
||||
keyWord: "keyWord",
|
||||
query: "query"
|
||||
},
|
||||
sort: {
|
||||
order: "order",
|
||||
prop: "prop"
|
||||
},
|
||||
label: locale.zhCn
|
||||
},
|
||||
style: {
|
||||
colors: [
|
||||
"#d42ca8",
|
||||
"#1c109d",
|
||||
"#6d17c3",
|
||||
"#6dc9f1",
|
||||
"#04c273",
|
||||
"#06b31c",
|
||||
"#f9f494",
|
||||
"#aa7a24",
|
||||
"#d57121",
|
||||
"#e93f4d"
|
||||
],
|
||||
form: {
|
||||
labelPostion: "right",
|
||||
labelWidth: "100px",
|
||||
span: 24
|
||||
},
|
||||
table: {
|
||||
border: true,
|
||||
highlightCurrentRow: true,
|
||||
autoHeight: true,
|
||||
contextMenu: ["refresh", "check", "edit", "delete", "order-asc", "order-desc"],
|
||||
column: {
|
||||
align: "center"
|
||||
}
|
||||
}
|
||||
},
|
||||
events: {},
|
||||
render: {
|
||||
functionSlots: {
|
||||
exclude: ["el-date-picker", "el-cascader", "el-time-select"]
|
||||
}
|
||||
}
|
||||
},
|
||||
options || {}
|
||||
);
|
||||
|
||||
// 初始化事件
|
||||
if (config.events) {
|
||||
emitter.init(config.events);
|
||||
}
|
||||
|
||||
app.provide("__config__", config);
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
// 设置浏览器
|
||||
function setBrowser(app: App) {
|
||||
// 浏览器信息
|
||||
const browser = reactive({
|
||||
isMini: false,
|
||||
screen: "full"
|
||||
});
|
||||
|
||||
// 更新信息
|
||||
function update() {
|
||||
const w = document.body.clientWidth;
|
||||
|
||||
if (w < 768) {
|
||||
browser.screen = "xs";
|
||||
} else if (w < 992) {
|
||||
browser.screen = "sm";
|
||||
} else if (w < 1200) {
|
||||
browser.screen = "md";
|
||||
} else if (w < 1920) {
|
||||
browser.screen = "xl";
|
||||
} else {
|
||||
browser.screen = "full";
|
||||
}
|
||||
|
||||
browser.isMini = browser.screen === "xs";
|
||||
}
|
||||
|
||||
// 监听浏览器窗口变化
|
||||
window.addEventListener("resize", () => {
|
||||
update();
|
||||
|
||||
// 事件
|
||||
mitt.emit("resize");
|
||||
});
|
||||
|
||||
update();
|
||||
app.provide("__browser__", browser);
|
||||
}
|
||||
|
||||
export function useProvide(app: App, options: Options = {}) {
|
||||
setBrowser(app);
|
||||
setConfig(app, options);
|
||||
}
|
658
packages/crud/src/static/index.scss
Normal file
658
packages/crud/src/static/index.scss
Normal file
@ -0,0 +1,658 @@
|
||||
.cl-crud {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
background-color: #fff;
|
||||
|
||||
&.is-border {
|
||||
border: 1px solid var(--el-border-color);
|
||||
border-radius: var(--el-border-radius-base);
|
||||
}
|
||||
|
||||
& > .cl-row {
|
||||
width: 100%;
|
||||
|
||||
&:not(.cl-row--last) > * {
|
||||
margin: 0 10px 10px 0;
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.cl-flex1 {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cl-flex1 {
|
||||
flex: 1;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.cl-search-key {
|
||||
display: inline-flex;
|
||||
|
||||
&__select {
|
||||
margin-right: 10px;
|
||||
|
||||
.el-input__inner {
|
||||
width: 60px;
|
||||
}
|
||||
}
|
||||
|
||||
&__wrap {
|
||||
display: inline-flex;
|
||||
|
||||
.el-input {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.el-button {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cl-table {
|
||||
width: 100%;
|
||||
|
||||
.el-table {
|
||||
&.el-loading-parent--relative {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
&__header {
|
||||
.el-table__cell {
|
||||
background-color: #f5f7fa !important;
|
||||
color: #333;
|
||||
|
||||
.cell {
|
||||
line-height: normal;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__empty-block {
|
||||
height: auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
.el-loading-mask {
|
||||
.el-loading-spinner {
|
||||
.el-icon-loading {
|
||||
font-size: 25px;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.el-loading-text {
|
||||
color: #666;
|
||||
margin-top: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__op {
|
||||
margin-bottom: -5px;
|
||||
|
||||
.el-button {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cl-filter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0 10px;
|
||||
|
||||
&__label {
|
||||
font-size: 12px;
|
||||
margin-right: 10px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.el-select {
|
||||
min-width: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
.cl-search {
|
||||
margin-bottom: 0px !important;
|
||||
|
||||
.el-form--inline {
|
||||
.el-form-item {
|
||||
margin: 0 10px 10px 0;
|
||||
|
||||
.el-date-editor {
|
||||
box-sizing: border-box;
|
||||
|
||||
.el-range-input {
|
||||
&:nth-child(2) {
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cl-adv-btn {
|
||||
margin-left: 10px;
|
||||
|
||||
.el-icon {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.cl-adv-search {
|
||||
&.el-drawer {
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.el-drawer__body {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 50px;
|
||||
padding: 0 15px 0 20px;
|
||||
user-select: none;
|
||||
|
||||
.text {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.el-icon {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__container {
|
||||
height: calc(100% - 110px);
|
||||
overflow-y: auto;
|
||||
padding: 10px 20px;
|
||||
box-sizing: border-box;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
border-radius: 6px;
|
||||
background-color: rgba(144, 147, 153, 0.3);
|
||||
}
|
||||
|
||||
.el-form-item__content {
|
||||
& > div {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
height: 60px;
|
||||
border-top: 1px solid var(--el-border-color-extra-light);
|
||||
padding: 0 10px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
|
||||
.cl-form {
|
||||
[class*="el-col-"].is-guttered {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.el-form-item {
|
||||
.el-input-number {
|
||||
&__decrease,
|
||||
&__increase {
|
||||
border: 0;
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
&__label {
|
||||
.el-tooltip {
|
||||
i {
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__content {
|
||||
min-width: 0px;
|
||||
|
||||
& > div {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&.no-label {
|
||||
& > .el-form-item__label {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-item {
|
||||
display: flex;
|
||||
|
||||
&__component {
|
||||
display: flex;
|
||||
|
||||
&.flex1 {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
|
||||
& > div {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__prepend {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
&__append {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
&__collapse {
|
||||
width: 100%;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
|
||||
.el-divider {
|
||||
margin: 16px 0;
|
||||
|
||||
&__text {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
i {
|
||||
margin-left: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
&__children {
|
||||
.el-form-item {
|
||||
margin-bottom: 18px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.el-table__header tr {
|
||||
line-height: normal;
|
||||
}
|
||||
}
|
||||
|
||||
&__footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.cl-crud {
|
||||
line-height: normal;
|
||||
}
|
||||
}
|
||||
|
||||
.cl-form-tabs {
|
||||
border-bottom: 1px solid var(--el-border-color);
|
||||
overflow: hidden;
|
||||
width: calc(100% - 10px);
|
||||
margin: 0 5px 20px 5px;
|
||||
|
||||
&__wrap {
|
||||
height: 35px;
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
position: relative;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
display: inline-flex;
|
||||
white-space: nowrap;
|
||||
|
||||
li {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
list-style: none;
|
||||
padding: 0 20px;
|
||||
height: 35px;
|
||||
cursor: pointer;
|
||||
|
||||
.el-icon {
|
||||
margin-right: 5px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__line {
|
||||
height: 3px;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
bottom: -1px;
|
||||
left: 0;
|
||||
transition: transform 0.3s ease-in-out, width 0.2s 0.1s cubic-bezier(0.645, 0.045, 0.355, 1);
|
||||
background-color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
&--card {
|
||||
.cl-form-tabs__line {
|
||||
display: none;
|
||||
}
|
||||
|
||||
ul {
|
||||
border: 1px solid var(--el-border-color);
|
||||
border-top-left-radius: 5px;
|
||||
border-top-right-radius: 5px;
|
||||
|
||||
li {
|
||||
border-left: 1px solid var(--el-border-color);
|
||||
|
||||
&:first-child {
|
||||
border-left-width: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cl-form-card {
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-size: 15px;
|
||||
padding: 0 5px;
|
||||
}
|
||||
|
||||
&__container {
|
||||
border: 1px solid transparent;
|
||||
border-top: 1px solid var(--el-border-color);
|
||||
border-radius: 0;
|
||||
transition: all 0.3s;
|
||||
display: grid;
|
||||
grid-template-rows: 0fr;
|
||||
|
||||
> .cl-form-item__children {
|
||||
margin: 10px 10px 10px 0px;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-expand {
|
||||
> .cl-form-card__container {
|
||||
border: 1px solid var(--el-border-color);
|
||||
border-radius: var(--el-border-radius-base);
|
||||
grid-template-rows: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.cl-form-card {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.cl-dialog {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 6px;
|
||||
|
||||
.el-dialog {
|
||||
&__header {
|
||||
padding: 0;
|
||||
margin-right: 0;
|
||||
|
||||
&-slot {
|
||||
&.is-drag {
|
||||
-moz-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-ms-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
user-select: none;
|
||||
cursor: move;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__body {
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&__footer {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__header {
|
||||
position: relative;
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid var(--el-border-color-extra-light);
|
||||
text-align: center;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
&__container {
|
||||
& > .el-scrollbar__wrap > .el-scrollbar__view {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&__default {
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
&__footer {
|
||||
border-top: 1px solid var(--el-border-color-extra-light);
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
&__title {
|
||||
display: block;
|
||||
font-size: 15px;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
&__controls {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
z-index: 9;
|
||||
width: 100%;
|
||||
|
||||
&-icon,
|
||||
.minimize,
|
||||
.maximize,
|
||||
.close {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
border: 0;
|
||||
background-color: transparent;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
|
||||
i {
|
||||
font-size: 18px;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.hidden-header {
|
||||
.el-dialog__header {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-fullscreen {
|
||||
height: 100vh !important;
|
||||
border-radius: 0;
|
||||
overflow: hidden;
|
||||
|
||||
.cl-dialog__container {
|
||||
height: 100% !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cl-context-menu {
|
||||
position: absolute;
|
||||
z-index: 9999;
|
||||
|
||||
&__box {
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
width: 160px;
|
||||
background-color: #fff;
|
||||
border-radius: 4px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
||||
&.is-append {
|
||||
right: calc(-100% - 5px);
|
||||
top: -5px;
|
||||
}
|
||||
|
||||
& > div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 35px;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
padding: 0 15px;
|
||||
color: #666;
|
||||
position: relative;
|
||||
|
||||
&:first-child {
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
span {
|
||||
height: 35px;
|
||||
line-height: 35px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: #f7f7f7;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
i {
|
||||
&:first-child {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
background-color: #f7f7f7;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
&.is-ellipsis {
|
||||
span {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-disabled {
|
||||
span {
|
||||
color: #ccc;
|
||||
|
||||
&:hover {
|
||||
color: #ccc;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__target {
|
||||
position: relative;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 768px) {
|
||||
.el-table {
|
||||
&__body {
|
||||
&-wrapper {
|
||||
&::-webkit-scrollbar {
|
||||
height: 6px;
|
||||
width: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cl-search-key {
|
||||
width: 100%;
|
||||
margin-right: 0 !important;
|
||||
|
||||
&__wrap {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
}
|
146
packages/crud/src/utils/form-hook.ts
Normal file
146
packages/crud/src/utils/form-hook.ts
Normal file
@ -0,0 +1,146 @@
|
||||
import { isArray, isFunction, isObject, isString } from "lodash-es";
|
||||
|
||||
export const format: { [key: string]: Hook.fn } = {
|
||||
number(value) {
|
||||
return value ? (isArray(value) ? value.map(Number) : Number(value)) : value;
|
||||
},
|
||||
string(value) {
|
||||
return value ? (isArray(value) ? value.map(String) : String(value)) : value;
|
||||
},
|
||||
split(value) {
|
||||
if (isString(value)) {
|
||||
return value.split(",").filter(Boolean);
|
||||
} else if (isArray(value)) {
|
||||
return value;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
join(value) {
|
||||
return isArray(value) ? value.join(",") : value;
|
||||
},
|
||||
boolean(value) {
|
||||
return Boolean(value);
|
||||
},
|
||||
booleanNumber(value) {
|
||||
return value ? 1 : 0;
|
||||
},
|
||||
datetimeRange(value, { form, method, prop }) {
|
||||
const key = prop.charAt(0).toUpperCase() + prop.slice(1);
|
||||
|
||||
const start = `start${key}`;
|
||||
const end = `end${key}`;
|
||||
|
||||
if (method == "bind") {
|
||||
return [form[start], form[end]];
|
||||
} else {
|
||||
const [startTime, endTime] = value || [];
|
||||
form[start] = startTime;
|
||||
form[end] = endTime;
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
splitJoin(value, { method }) {
|
||||
if (method == "bind") {
|
||||
return isString(value) ? value.split(",").filter(Boolean) : value;
|
||||
} else {
|
||||
return isArray(value) ? value.join(",") : value;
|
||||
}
|
||||
},
|
||||
json(value, { method }) {
|
||||
if (method == "bind") {
|
||||
try {
|
||||
return JSON.parse(value);
|
||||
} catch (e) {
|
||||
return {};
|
||||
}
|
||||
} else {
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
},
|
||||
empty(value) {
|
||||
if (isString(value)) {
|
||||
return value === "" ? undefined : value;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
};
|
||||
|
||||
function init({ value, form, prop }: any) {
|
||||
if (prop) {
|
||||
const [a, b] = prop.split("-");
|
||||
if (b) {
|
||||
form[prop] = form[a] ? form[a][b] : form[a];
|
||||
} else {
|
||||
form[prop] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function parse(method: "submit" | "bind", { value, hook: pipe, form, prop }: any) {
|
||||
init({ value, method, form, prop });
|
||||
|
||||
if (!pipe) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let pipes = [];
|
||||
|
||||
if (isString(pipe)) {
|
||||
if (format[pipe]) {
|
||||
pipes = [pipe];
|
||||
} else {
|
||||
console.error(`Hook[${pipe}] is not found`);
|
||||
}
|
||||
} else if (isArray(pipe)) {
|
||||
pipes = pipe;
|
||||
} else if (isObject(pipe)) {
|
||||
// @ts-ignore
|
||||
pipes = isArray(pipe[method]) ? pipe[method] : [pipe[method]];
|
||||
} else if (isFunction(pipe)) {
|
||||
pipes = [pipe];
|
||||
} else {
|
||||
console.error(`Hook error`);
|
||||
}
|
||||
|
||||
let v = value;
|
||||
|
||||
pipes.forEach((e: any) => {
|
||||
let f = null;
|
||||
|
||||
if (isString(e)) {
|
||||
f = format[e];
|
||||
} else if (isFunction(e)) {
|
||||
f = e;
|
||||
}
|
||||
|
||||
if (f) {
|
||||
v = f(v, {
|
||||
method,
|
||||
form,
|
||||
prop
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (prop) {
|
||||
form[prop] = v;
|
||||
}
|
||||
}
|
||||
|
||||
const formHook = {
|
||||
bind(data: any) {
|
||||
parse("bind", data);
|
||||
},
|
||||
|
||||
submit(data: any) {
|
||||
parse("submit", data);
|
||||
}
|
||||
};
|
||||
|
||||
export function registerFormHook(name: string, fn: Hook.fn) {
|
||||
format[name] = fn;
|
||||
}
|
||||
|
||||
export default formHook;
|
134
packages/crud/src/utils/index.ts
Normal file
134
packages/crud/src/utils/index.ts
Normal file
@ -0,0 +1,134 @@
|
||||
import { isRef, mergeProps } from "vue";
|
||||
import { flatMap, isArray, isFunction, isNumber, isString, mergeWith } from "lodash-es";
|
||||
|
||||
export function isObject(val: any) {
|
||||
return val !== null && typeof val === "object";
|
||||
}
|
||||
|
||||
// 解析px
|
||||
export function parsePx(val: string | number) {
|
||||
return isNumber(val) ? `${val}px` : val;
|
||||
}
|
||||
|
||||
// 数据设置
|
||||
export function dataset(obj: any, key: string, value: any): any {
|
||||
const isGet = value === undefined;
|
||||
let d = obj;
|
||||
|
||||
const arr = flatMap(
|
||||
key.split(".").map((e) => {
|
||||
if (e.includes("[")) {
|
||||
return e.split("[").map((e) => e.replace(/"/g, ""));
|
||||
} else {
|
||||
return e;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
try {
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
const e: any = arr[i];
|
||||
let n: any = null;
|
||||
|
||||
if (e.includes("]")) {
|
||||
const [k, v] = e.replace("]", "").split(":");
|
||||
|
||||
if (v) {
|
||||
n = d.findIndex((x: any) => x[k] == v);
|
||||
} else {
|
||||
n = Number(k);
|
||||
}
|
||||
} else {
|
||||
n = e;
|
||||
}
|
||||
|
||||
if (i != arr.length - 1) {
|
||||
d = d[n];
|
||||
} else {
|
||||
if (isGet) {
|
||||
return d[n];
|
||||
} else {
|
||||
if (isObject(value)) {
|
||||
Object.assign(d[n], value);
|
||||
} else {
|
||||
d[n] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return obj;
|
||||
} catch (e) {
|
||||
console.error("Format error", `${key}`);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
// 元素是否包含
|
||||
export function contains(parent: any, node: any) {
|
||||
return parent !== node && parent && parent.contains(node);
|
||||
}
|
||||
|
||||
// 合并配置
|
||||
export function mergeConfig(a: any, b?: any): any {
|
||||
return b ? mergeProps(a, b) : a;
|
||||
}
|
||||
|
||||
// 合并数据
|
||||
export function merge(d1: any, d2: any) {
|
||||
return mergeWith(d1, d2, (_, b) => {
|
||||
if (isArray(b)) {
|
||||
return b;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 添加元素
|
||||
export function addClass(el: Element, name: string) {
|
||||
if (isString(el?.className)) {
|
||||
const f = el.className.includes(name);
|
||||
|
||||
if (!f) {
|
||||
el.className += " " + name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 移除元素
|
||||
export function removeClass(el: Element, name: string) {
|
||||
if (isString(el?.className)) {
|
||||
el.className = el.className.replace(name, "");
|
||||
}
|
||||
}
|
||||
|
||||
// 获取值
|
||||
export function getValue(data: any, params?: any) {
|
||||
if (isRef(data)) {
|
||||
return data.value;
|
||||
} else {
|
||||
if (isFunction(data)) {
|
||||
return data(params);
|
||||
} else {
|
||||
return data;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 深度查找
|
||||
export function deepFind(value: any, list: any[]) {
|
||||
function deep(arr: any[]): any | undefined {
|
||||
for (const e of arr) {
|
||||
if (e.value === value) {
|
||||
return e;
|
||||
} else if (e.children) {
|
||||
const d = deep(e.children);
|
||||
if (d !== undefined) {
|
||||
return d;
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return deep(list);
|
||||
}
|
30
packages/crud/src/utils/mitt.ts
Normal file
30
packages/crud/src/utils/mitt.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import _mitt from "mitt";
|
||||
|
||||
const mitt = _mitt();
|
||||
|
||||
class Mitt {
|
||||
id: number;
|
||||
|
||||
constructor(id?: number) {
|
||||
this.id = id || 0;
|
||||
}
|
||||
|
||||
send(type: "emit" | "off" | "on", name: string, ...args: any[]) {
|
||||
// @ts-ignore
|
||||
mitt[type](`${this.id}__${name}`, ...args);
|
||||
}
|
||||
|
||||
emit(name: string, ...args: any[]) {
|
||||
this.send("emit", name, ...args);
|
||||
}
|
||||
|
||||
off(name: string, handler: (...args: any[]) => void) {
|
||||
this.send("off", name, handler);
|
||||
}
|
||||
|
||||
on(name: string, handler: (...args: any[]) => void) {
|
||||
this.send("on", name, handler);
|
||||
}
|
||||
}
|
||||
|
||||
export { Mitt, mitt };
|
193
packages/crud/src/utils/parse.tsx
Normal file
193
packages/crud/src/utils/parse.tsx
Normal file
@ -0,0 +1,193 @@
|
||||
import { h, useSlots } from "vue";
|
||||
import { useConfig, useCore } from "../hooks";
|
||||
import { isBoolean, isFunction, isArray, isString, cloneDeep } from "lodash-es";
|
||||
import { renderNode } from "./vnode";
|
||||
import { deepFind, getValue, isObject } from ".";
|
||||
|
||||
/**
|
||||
* 解析 form.hidden
|
||||
*/
|
||||
export function parseFormHidden(value: any, { scope }: any) {
|
||||
if (isBoolean(value)) {
|
||||
return value;
|
||||
} else if (isFunction(value)) {
|
||||
return value({ scope });
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 table.dict
|
||||
*/
|
||||
export function parseTableDict(value: any, item: ClTable.Column) {
|
||||
const { style } = useConfig();
|
||||
|
||||
// 选项列表
|
||||
const options: DictOptions = cloneDeep(getValue(item.dict || []));
|
||||
|
||||
// 设置颜色
|
||||
if (item.dictColor) {
|
||||
options.forEach((e, i) => {
|
||||
e.color = style.colors[i];
|
||||
});
|
||||
}
|
||||
|
||||
// 格式化方法
|
||||
const formatter = item.dictFormatter;
|
||||
|
||||
// 多个值
|
||||
const values = isArray(value) ? value : [value];
|
||||
|
||||
// 返回值
|
||||
const list = values.map((v) => {
|
||||
const d = deepFind(v, options) || { label: v, value: v };
|
||||
delete d.children;
|
||||
|
||||
return d;
|
||||
});
|
||||
|
||||
// 是否格式化
|
||||
if (formatter) {
|
||||
return formatter(list);
|
||||
} else {
|
||||
return list.map((e) => {
|
||||
return h(
|
||||
<el-tag disable-transitions effect="dark" style="margin: 2px; border: 0" />,
|
||||
e,
|
||||
{
|
||||
default: () => e.label
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 table.op.buttons
|
||||
*/
|
||||
export function parseTableOpButtons(buttons: any, { scope }: any) {
|
||||
const { crud } = useCore();
|
||||
const { style } = useConfig();
|
||||
const slots = useSlots();
|
||||
|
||||
const list = getValue(buttons, { scope }) || ["edit", "delete"];
|
||||
|
||||
return list.map((vnode: any) => {
|
||||
if (vnode === "info") {
|
||||
return (
|
||||
<el-button
|
||||
text
|
||||
bg
|
||||
size={style.size}
|
||||
v-show={crud.getPermission("info")}
|
||||
onClick={() => {
|
||||
crud.rowInfo(scope.row);
|
||||
}}>
|
||||
{crud.dict.label?.info}
|
||||
</el-button>
|
||||
);
|
||||
} else if (vnode === "edit") {
|
||||
return (
|
||||
<el-button
|
||||
text
|
||||
bg
|
||||
type="primary"
|
||||
size={style.size}
|
||||
v-show={crud.getPermission("update")}
|
||||
onClick={() => {
|
||||
crud.rowEdit(scope.row);
|
||||
}}>
|
||||
{crud.dict.label?.update}
|
||||
</el-button>
|
||||
);
|
||||
} else if (vnode === "delete") {
|
||||
return (
|
||||
<el-button
|
||||
text
|
||||
bg
|
||||
type="danger"
|
||||
size={style.size}
|
||||
v-show={crud.getPermission("delete")}
|
||||
onClick={() => {
|
||||
crud.rowDelete(scope.row);
|
||||
}}>
|
||||
{crud.dict.label?.delete}
|
||||
</el-button>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
!vnode.hidden &&
|
||||
renderNode(vnode, {
|
||||
scope,
|
||||
slots,
|
||||
custom(vnode) {
|
||||
return (
|
||||
<el-button
|
||||
text
|
||||
type={vnode.type}
|
||||
bg
|
||||
onClick={() => {
|
||||
vnode.onClick({ scope });
|
||||
}}>
|
||||
{vnode.label}
|
||||
</el-button>
|
||||
);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析扩展组件
|
||||
*/
|
||||
export function parseExtensionComponent(vnode: any) {
|
||||
if (["el-select", "el-radio-group", "el-checkbox-group"].includes(vnode.name)) {
|
||||
const list = getValue(vnode.options) || [];
|
||||
|
||||
const children = (
|
||||
<div>
|
||||
{list.map((e: any, i: number) => {
|
||||
let label: any;
|
||||
let value: any;
|
||||
|
||||
if (isString(e)) {
|
||||
label = value = e;
|
||||
} else if (isObject(e)) {
|
||||
label = e.label;
|
||||
value = e.value;
|
||||
} else {
|
||||
return <cl-error-message title={`组件渲染失败,options 参数错误`} />;
|
||||
}
|
||||
|
||||
switch (vnode.name) {
|
||||
case "el-select":
|
||||
return <el-option key={i} label={label} value={value} {...e.props} />;
|
||||
case "el-radio-group":
|
||||
return (
|
||||
<el-radio key={i} label={value} {...e.props}>
|
||||
{label}
|
||||
</el-radio>
|
||||
);
|
||||
case "el-checkbox-group":
|
||||
return (
|
||||
<el-checkbox key={i} label={value} {...e.props}>
|
||||
{label}
|
||||
</el-checkbox>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
|
||||
return {
|
||||
children
|
||||
};
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
}
|
17
packages/crud/src/utils/temp.ts
Normal file
17
packages/crud/src/utils/temp.ts
Normal file
@ -0,0 +1,17 @@
|
||||
// @ts-nocheck
|
||||
|
||||
import { App } from "vue";
|
||||
|
||||
export default {
|
||||
get vue(): App {
|
||||
return window.__CrudApp__;
|
||||
},
|
||||
|
||||
get(key: string) {
|
||||
return window[key];
|
||||
},
|
||||
|
||||
set(key: string, value: any) {
|
||||
window[key] = value;
|
||||
}
|
||||
};
|
191
packages/crud/src/utils/vnode.tsx
Normal file
191
packages/crud/src/utils/vnode.tsx
Normal file
@ -0,0 +1,191 @@
|
||||
import { h, resolveComponent, toRaw, VNode } from "vue";
|
||||
import { isObject } from "./index";
|
||||
import { parseExtensionComponent } from "./parse";
|
||||
import temp from "./temp";
|
||||
import { useConfig } from "../hooks";
|
||||
import { isFunction, isString } from "lodash-es";
|
||||
|
||||
// 配置
|
||||
interface Options {
|
||||
// 标识
|
||||
prop?: string;
|
||||
// 数据值
|
||||
scope?: any;
|
||||
// 当前行
|
||||
item?: any;
|
||||
// 插槽
|
||||
slots?: any;
|
||||
// 子集
|
||||
children?: any[] & any;
|
||||
// 自定义
|
||||
custom?: (vnode: any) => any;
|
||||
// 渲染方式
|
||||
render?: "slot" | null;
|
||||
// 其他
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
// 临时注册组件列表
|
||||
const regs: Map<string, any> = new Map();
|
||||
|
||||
// 解析节点
|
||||
export function parseNode(vnode: any, options: Options): VNode {
|
||||
const { scope, prop, slots, children, _data } = options || [];
|
||||
const {
|
||||
render: { functionSlots }
|
||||
} = useConfig();
|
||||
|
||||
// 渲染后组件
|
||||
let comp: VNode | null = null;
|
||||
|
||||
// 插槽模式渲染
|
||||
if (vnode.name.includes("slot-")) {
|
||||
const rn = slots[vnode.name];
|
||||
|
||||
if (rn) {
|
||||
return rn({ scope, prop, ..._data });
|
||||
} else {
|
||||
return <cl-error-message title={`${vnode.name} is not found`} />;
|
||||
}
|
||||
}
|
||||
|
||||
// 实例模式下,先注册到全局,再分解组件渲染
|
||||
if (vnode.vm && !regs.get(vnode.name)) {
|
||||
temp.vue.component(vnode.name, { ...vnode.vm });
|
||||
regs.set(vnode.name, { ...vnode.vm });
|
||||
}
|
||||
|
||||
// 处理 props
|
||||
if (isFunction(vnode.props)) {
|
||||
vnode.props = vnode.props({ scope, prop, ..._data });
|
||||
}
|
||||
|
||||
// 组件参数
|
||||
const props = {
|
||||
...vnode.props,
|
||||
..._data,
|
||||
prop,
|
||||
scope
|
||||
};
|
||||
|
||||
// 是否禁用
|
||||
props.disabled = _data?.isDisabled || props.disabled;
|
||||
|
||||
// 添加双向绑定
|
||||
if (props && scope) {
|
||||
if (prop) {
|
||||
props.modelValue = scope[prop];
|
||||
props["onUpdate:modelValue"] = function (val: any) {
|
||||
scope[prop] = val;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 组件实例渲染
|
||||
if (vnode.vm) {
|
||||
comp = h(regs.get(vnode.name), props);
|
||||
} else {
|
||||
// 是否函数式插槽
|
||||
const isFunctionSlot =
|
||||
!functionSlots.exclude?.includes(vnode.name) &&
|
||||
(vnode.functionSlot === undefined ? true : vnode.functionSlot);
|
||||
|
||||
// 渲染组件
|
||||
comp = h(
|
||||
toRaw(resolveComponent(vnode.name)),
|
||||
props,
|
||||
isFunctionSlot ? () => children : children
|
||||
);
|
||||
}
|
||||
|
||||
// 挂载到 refs 中
|
||||
if (isFunction(vnode.ref)) {
|
||||
setTimeout(() => {
|
||||
vnode.ref(comp?.component?.exposed);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
return comp;
|
||||
}
|
||||
|
||||
// 渲染节点
|
||||
export function renderNode(vnode: any, options: Options) {
|
||||
const { item, scope, children, _data, render } = options || {};
|
||||
|
||||
if (!vnode) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (vnode.__v_isVNode) {
|
||||
return vnode;
|
||||
}
|
||||
|
||||
// 默认参数配置
|
||||
if (item) {
|
||||
if (item.component) {
|
||||
if (!item.component.props) {
|
||||
item.component.props = {};
|
||||
}
|
||||
|
||||
// 占位符
|
||||
let placeholder = "";
|
||||
|
||||
switch (item.component?.name) {
|
||||
case "el-input":
|
||||
placeholder = "请填写";
|
||||
break;
|
||||
|
||||
case "el-select":
|
||||
placeholder = "请选择";
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (placeholder) {
|
||||
if (!item.component.props.placeholder) {
|
||||
item.component.props.placeholder = placeholder + item.label;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 组件实例
|
||||
if (vnode.vm) {
|
||||
if (!vnode.name) {
|
||||
vnode.name = vnode.vm?.name || vnode.vm?.__hmrId;
|
||||
}
|
||||
|
||||
return parseNode(vnode, options);
|
||||
}
|
||||
|
||||
// 组件名渲染
|
||||
if (isString(vnode)) {
|
||||
if (render == "slot") {
|
||||
if (!vnode.includes("slot-")) {
|
||||
return vnode;
|
||||
}
|
||||
}
|
||||
|
||||
return parseNode({ name: vnode }, options);
|
||||
}
|
||||
|
||||
// 方法回调
|
||||
if (isFunction(vnode)) {
|
||||
return vnode({ scope, h, ..._data });
|
||||
}
|
||||
|
||||
// jsx 模式
|
||||
if (isObject(vnode)) {
|
||||
if (vnode.name) {
|
||||
return parseNode(vnode, { ...options, children, ...parseExtensionComponent(vnode) });
|
||||
} else {
|
||||
if (options.custom) {
|
||||
return options.custom(vnode);
|
||||
}
|
||||
|
||||
return <cl-error-message title={`Error,name is required`} />;
|
||||
}
|
||||
}
|
||||
}
|
25
packages/crud/tsconfig.json
Normal file
25
packages/crud/tsconfig.json
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "esnext",
|
||||
"module": "esnext",
|
||||
"strict": true,
|
||||
"jsx": "preserve",
|
||||
"importHelpers": true,
|
||||
"moduleResolution": "node",
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"sourceMap": false,
|
||||
"declaration": true,
|
||||
"declarationDir": "types",
|
||||
"inlineSourceMap": false,
|
||||
"disableSizeLimit": true,
|
||||
"baseUrl": ".",
|
||||
"outDir": "dist",
|
||||
"types": ["webpack-env"],
|
||||
"paths": {},
|
||||
"lib": ["esnext", "dom", "dom.iterable", "scripthost"]
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx"],
|
||||
"exclude": ["node_modules", "src/demo/*", "src/main.ts", "src/components/*"]
|
||||
}
|
2
packages/crud/types/components/add-btn.d.ts
vendored
Normal file
2
packages/crud/types/components/add-btn.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
declare const _default: import("vue").DefineComponent<{}, () => false | JSX.Element, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps, Readonly<import("vue").ExtractPropTypes<{}>>, {}>;
|
||||
export default _default;
|
2
packages/crud/types/components/add-btn/index.d.ts
vendored
Normal file
2
packages/crud/types/components/add-btn/index.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
declare const _default: import("vue").DefineComponent<{}, () => false | JSX.Element, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps, Readonly<import("vue").ExtractPropTypes<{}>>, {}, {}>;
|
||||
export default _default;
|
2
packages/crud/types/components/adv-btn.d.ts
vendored
Normal file
2
packages/crud/types/components/adv-btn.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
declare const _default: import("vue").DefineComponent<{}, () => JSX.Element, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps, Readonly<import("vue").ExtractPropTypes<{}>>, {}>;
|
||||
export default _default;
|
50
packages/crud/types/components/adv-search.d.ts
vendored
Normal file
50
packages/crud/types/components/adv-search.d.ts
vendored
Normal file
@ -0,0 +1,50 @@
|
||||
/// <reference types="../index" />
|
||||
import { PropType } from "vue";
|
||||
declare const _default: import("vue").DefineComponent<{
|
||||
items: {
|
||||
type: PropType<ClForm.Item[]>;
|
||||
default: () => never[];
|
||||
};
|
||||
title: StringConstructor;
|
||||
size: {
|
||||
type: (NumberConstructor | StringConstructor)[];
|
||||
default: string;
|
||||
};
|
||||
op: {
|
||||
type: ArrayConstructor;
|
||||
default: () => string[];
|
||||
};
|
||||
onSearch: FunctionConstructor;
|
||||
}, {
|
||||
open: () => void;
|
||||
close: () => void;
|
||||
reset: () => void;
|
||||
clear: () => void;
|
||||
search: () => void;
|
||||
Drawer: import("vue").Ref<any>;
|
||||
Form: import("vue").Ref<ClForm.Ref | undefined>;
|
||||
visible: import("vue").Ref<boolean>;
|
||||
}, unknown, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, ("clear" | "reset")[], "clear" | "reset", import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps, Readonly<import("vue").ExtractPropTypes<{
|
||||
items: {
|
||||
type: PropType<ClForm.Item[]>;
|
||||
default: () => never[];
|
||||
};
|
||||
title: StringConstructor;
|
||||
size: {
|
||||
type: (NumberConstructor | StringConstructor)[];
|
||||
default: string;
|
||||
};
|
||||
op: {
|
||||
type: ArrayConstructor;
|
||||
default: () => string[];
|
||||
};
|
||||
onSearch: FunctionConstructor;
|
||||
}>> & {
|
||||
onReset?: ((...args: any[]) => any) | undefined;
|
||||
onClear?: ((...args: any[]) => any) | undefined;
|
||||
}, {
|
||||
items: ClForm.Item[];
|
||||
op: unknown[];
|
||||
size: string | number;
|
||||
}>;
|
||||
export default _default;
|
2
packages/crud/types/components/adv/btn.d.ts
vendored
Normal file
2
packages/crud/types/components/adv/btn.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
declare const _default: import("vue").DefineComponent<{}, () => JSX.Element, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps, Readonly<import("vue").ExtractPropTypes<{}>>, {}, {}>;
|
||||
export default _default;
|
41
packages/crud/types/components/adv/search.d.ts
vendored
Normal file
41
packages/crud/types/components/adv/search.d.ts
vendored
Normal file
@ -0,0 +1,41 @@
|
||||
/// <reference types="../index" />
|
||||
import { PropType } from "vue";
|
||||
declare const _default: import("vue").DefineComponent<{
|
||||
items: {
|
||||
type: PropType<ClForm.Item[]>;
|
||||
default: () => never[];
|
||||
};
|
||||
title: StringConstructor;
|
||||
size: {
|
||||
type: (StringConstructor | NumberConstructor)[];
|
||||
default: string;
|
||||
};
|
||||
op: {
|
||||
type: ArrayConstructor;
|
||||
default: () => string[];
|
||||
};
|
||||
onSearch: FunctionConstructor;
|
||||
}, () => JSX.Element, unknown, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, ("clear" | "reset")[], "clear" | "reset", import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps, Readonly<import("vue").ExtractPropTypes<{
|
||||
items: {
|
||||
type: PropType<ClForm.Item[]>;
|
||||
default: () => never[];
|
||||
};
|
||||
title: StringConstructor;
|
||||
size: {
|
||||
type: (StringConstructor | NumberConstructor)[];
|
||||
default: string;
|
||||
};
|
||||
op: {
|
||||
type: ArrayConstructor;
|
||||
default: () => string[];
|
||||
};
|
||||
onSearch: FunctionConstructor;
|
||||
}>> & {
|
||||
onReset?: ((...args: any[]) => any) | undefined;
|
||||
onClear?: ((...args: any[]) => any) | undefined;
|
||||
}, {
|
||||
items: ClForm.Item[];
|
||||
op: unknown[];
|
||||
size: string | number;
|
||||
}, {}>;
|
||||
export default _default;
|
30
packages/crud/types/components/context-menu/index.d.ts
vendored
Normal file
30
packages/crud/types/components/context-menu/index.d.ts
vendored
Normal file
@ -0,0 +1,30 @@
|
||||
/// <reference types="../index" />
|
||||
declare const ClContextMenu: import("vue").DefineComponent<{
|
||||
show: BooleanConstructor;
|
||||
options: {
|
||||
type: ObjectConstructor;
|
||||
default: () => {};
|
||||
};
|
||||
event: {
|
||||
type: ObjectConstructor;
|
||||
default: () => {};
|
||||
};
|
||||
}, () => false | JSX.Element, unknown, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps, Readonly<import("vue").ExtractPropTypes<{
|
||||
show: BooleanConstructor;
|
||||
options: {
|
||||
type: ObjectConstructor;
|
||||
default: () => {};
|
||||
};
|
||||
event: {
|
||||
type: ObjectConstructor;
|
||||
default: () => {};
|
||||
};
|
||||
}>>, {
|
||||
options: Record<string, any>;
|
||||
show: boolean;
|
||||
event: Record<string, any>;
|
||||
}, {}>;
|
||||
export declare const ContextMenu: {
|
||||
open(event: any, options: ClContextMenu.Options): void;
|
||||
};
|
||||
export default ClContextMenu;
|
23
packages/crud/types/components/crud/helper.d.ts
vendored
Normal file
23
packages/crud/types/components/crud/helper.d.ts
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
/// <reference types="../index" />
|
||||
import { Mitt } from "../../utils/mitt";
|
||||
interface Options {
|
||||
mitt: Mitt;
|
||||
config: ClCrud.Config;
|
||||
crud: ClCrud.Ref;
|
||||
}
|
||||
export declare function useHelper({ config, crud, mitt }: Options): {
|
||||
proxy: (name: string, data?: any[]) => void;
|
||||
set: (key: string, value: any) => false | undefined;
|
||||
on: (name: string, callback: fn) => void;
|
||||
rowInfo: (data: any) => void;
|
||||
rowAdd: () => void;
|
||||
rowEdit: (data: any) => void;
|
||||
rowAppend: (data: any) => void;
|
||||
rowDelete: (...selection: any[]) => void;
|
||||
rowClose: () => void;
|
||||
refresh: (params?: obj) => Promise<unknown>;
|
||||
getPermission: (key: "page" | "list" | "info" | "update" | "add" | "delete") => boolean;
|
||||
paramsReplace: (params: obj) => any;
|
||||
getParams: () => obj;
|
||||
};
|
||||
export {};
|
19
packages/crud/types/components/crud/index.d.ts
vendored
Normal file
19
packages/crud/types/components/crud/index.d.ts
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
declare const _default: import("vue").DefineComponent<{
|
||||
name: StringConstructor;
|
||||
border: BooleanConstructor;
|
||||
padding: {
|
||||
type: StringConstructor;
|
||||
default: string;
|
||||
};
|
||||
}, () => JSX.Element, unknown, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps, Readonly<import("vue").ExtractPropTypes<{
|
||||
name: StringConstructor;
|
||||
border: BooleanConstructor;
|
||||
padding: {
|
||||
type: StringConstructor;
|
||||
default: string;
|
||||
};
|
||||
}>>, {
|
||||
border: boolean;
|
||||
padding: string;
|
||||
}, {}>;
|
||||
export default _default;
|
70
packages/crud/types/components/dialog/index.d.ts
vendored
Normal file
70
packages/crud/types/components/dialog/index.d.ts
vendored
Normal file
@ -0,0 +1,70 @@
|
||||
declare const _default: import("vue").DefineComponent<{
|
||||
modelValue: {
|
||||
type: BooleanConstructor;
|
||||
default: boolean;
|
||||
};
|
||||
props: ObjectConstructor;
|
||||
title: {
|
||||
type: StringConstructor;
|
||||
default: string;
|
||||
};
|
||||
height: StringConstructor;
|
||||
width: {
|
||||
type: StringConstructor;
|
||||
default: string;
|
||||
};
|
||||
padding: {
|
||||
type: StringConstructor;
|
||||
default: string;
|
||||
};
|
||||
keepAlive: BooleanConstructor;
|
||||
fullscreen: BooleanConstructor;
|
||||
controls: {
|
||||
type: ArrayConstructor;
|
||||
default: () => string[];
|
||||
};
|
||||
hideHeader: BooleanConstructor;
|
||||
beforeClose: FunctionConstructor;
|
||||
}, () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
|
||||
[key: string]: any;
|
||||
}>, unknown, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, ("update:modelValue" | "fullscreen-change")[], "update:modelValue" | "fullscreen-change", import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps, Readonly<import("vue").ExtractPropTypes<{
|
||||
modelValue: {
|
||||
type: BooleanConstructor;
|
||||
default: boolean;
|
||||
};
|
||||
props: ObjectConstructor;
|
||||
title: {
|
||||
type: StringConstructor;
|
||||
default: string;
|
||||
};
|
||||
height: StringConstructor;
|
||||
width: {
|
||||
type: StringConstructor;
|
||||
default: string;
|
||||
};
|
||||
padding: {
|
||||
type: StringConstructor;
|
||||
default: string;
|
||||
};
|
||||
keepAlive: BooleanConstructor;
|
||||
fullscreen: BooleanConstructor;
|
||||
controls: {
|
||||
type: ArrayConstructor;
|
||||
default: () => string[];
|
||||
};
|
||||
hideHeader: BooleanConstructor;
|
||||
beforeClose: FunctionConstructor;
|
||||
}>> & {
|
||||
"onUpdate:modelValue"?: ((...args: any[]) => any) | undefined;
|
||||
"onFullscreen-change"?: ((...args: any[]) => any) | undefined;
|
||||
}, {
|
||||
title: string;
|
||||
padding: string;
|
||||
width: string;
|
||||
keepAlive: boolean;
|
||||
hideHeader: boolean;
|
||||
controls: unknown[];
|
||||
fullscreen: boolean;
|
||||
modelValue: boolean;
|
||||
}, {}>;
|
||||
export default _default;
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user