mirror of
https://github.com/cool-team-official/cool-admin-vue.git
synced 2026-03-29 16:50:45 +00:00
发布7.x
This commit is contained in:
parent
76c7045cf8
commit
1735d6258e
@ -1,21 +1,5 @@
|
|||||||
.DS_Store
|
|
||||||
node_modules
|
node_modules
|
||||||
/dist
|
.DS_Store
|
||||||
|
dist
|
||||||
# local env files
|
dist-ssr
|
||||||
.env.local
|
*.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?
|
|
||||||
|
|||||||
5
.env
Normal file
5
.env
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# 应用名称
|
||||||
|
VITE_NAME = "COOL-ADMIN"
|
||||||
|
|
||||||
|
# 网络超时请求时间
|
||||||
|
VITE_TIMEOUT = 30000
|
||||||
@ -1,6 +1 @@
|
|||||||
/public/
|
vite.config.ts
|
||||||
/dist/
|
|
||||||
/node_modules/
|
|
||||||
/src/icons/svg/
|
|
||||||
/mock/
|
|
||||||
vue.config.js
|
|
||||||
67
.eslintrc.js
67
.eslintrc.js
@ -1,14 +1,67 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
root: true,
|
root: true,
|
||||||
env: {
|
env: {
|
||||||
node: true
|
browser: true,
|
||||||
},
|
node: true,
|
||||||
extends: ["plugin:vue/essential", "@vue/prettier"],
|
es6: true
|
||||||
rules: {
|
|
||||||
"no-console": "off",
|
|
||||||
"comma-dangle": [2, "never"]
|
|
||||||
},
|
},
|
||||||
|
parser: "vue-eslint-parser",
|
||||||
parserOptions: {
|
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
|
node_modules
|
||||||
/dist
|
.DS_Store
|
||||||
|
dist
|
||||||
# local env files
|
dist-ssr
|
||||||
.env.local
|
*.local
|
||||||
.env.*.local
|
pnpm-lock.yaml
|
||||||
|
|
||||||
# Log files
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
|
|
||||||
# Editor directories and files
|
|
||||||
.idea
|
|
||||||
*.suo
|
|
||||||
*.ntvs*
|
|
||||||
*.njsproj
|
|
||||||
*.sln
|
|
||||||
*.sw?
|
|
||||||
|
|||||||
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,
|
"tabWidth": 4,
|
||||||
"useTabs": true,
|
"useTabs": true,
|
||||||
"semi": true,
|
"semi": true,
|
||||||
"jsxBracketSameLine": true,
|
|
||||||
"singleQuote": false,
|
"singleQuote": false,
|
||||||
"printWidth": 100,
|
"printWidth": 100,
|
||||||
"trailingComma": "none"
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
89
.vscode/crud.code-snippets
vendored
89
.vscode/crud.code-snippets
vendored
@ -1,10 +1,11 @@
|
|||||||
{
|
{
|
||||||
"cl-crud": {
|
"cl-crud": {
|
||||||
"prefix": "cl-crud",
|
"prefix": "cl-crud",
|
||||||
|
"scope": "vue",
|
||||||
"body": [
|
"body": [
|
||||||
"<template>",
|
"<template>",
|
||||||
" <cl-crud ref=\"crud\" @load=\"onLoad\">",
|
" <cl-crud ref=\"Crud\">",
|
||||||
" <el-row type=\"flex\" align=\"middle\">",
|
" <cl-row>",
|
||||||
" <!-- 刷新按钮 -->",
|
" <!-- 刷新按钮 -->",
|
||||||
" <cl-refresh-btn />",
|
" <cl-refresh-btn />",
|
||||||
" <!-- 新增按钮 -->",
|
" <!-- 新增按钮 -->",
|
||||||
@ -14,48 +15,88 @@
|
|||||||
" <cl-flex1 />",
|
" <cl-flex1 />",
|
||||||
" <!-- 关键字搜索 -->",
|
" <!-- 关键字搜索 -->",
|
||||||
" <cl-search-key />",
|
" <cl-search-key />",
|
||||||
" </el-row>",
|
" </cl-row>",
|
||||||
"",
|
"",
|
||||||
" <el-row>",
|
" <cl-row>",
|
||||||
" <!-- 数据表格 -->",
|
" <!-- 数据表格 -->",
|
||||||
" <cl-table v-bind=\"table\"></cl-table>",
|
" <cl-table ref=\"Table\" />",
|
||||||
" </el-row>",
|
" </cl-row>",
|
||||||
"",
|
"",
|
||||||
" <el-row type=\"flex\">",
|
" <cl-row>",
|
||||||
" <cl-flex1 />",
|
" <cl-flex1 />",
|
||||||
" <!-- 分页控件 -->",
|
" <!-- 分页控件 -->",
|
||||||
" <cl-pagination />",
|
" <cl-pagination />",
|
||||||
" </el-row>",
|
" </cl-row>",
|
||||||
"",
|
"",
|
||||||
" <!-- 新增、编辑 -->",
|
" <!-- 新增、编辑 -->",
|
||||||
" <cl-upsert ref=\"upsert\" v-bind=\"upsert\"></cl-upsert>",
|
" <cl-upsert ref=\"Upsert\" />",
|
||||||
" </cl-crud>",
|
" </cl-crud>",
|
||||||
"</template>",
|
"</template>",
|
||||||
"",
|
"",
|
||||||
"<script>",
|
"<script lang=\"ts\" name=\"菜单名称\" setup>",
|
||||||
"export default {",
|
"import { useCrud, useTable, useUpsert } from \"@cool-vue/crud\";",
|
||||||
" data() {",
|
"import { useCool } from \"/@/cool\";",
|
||||||
" return {",
|
"",
|
||||||
" // 新增、编辑配置",
|
"const { service } = useCool();",
|
||||||
" upsert: {",
|
"",
|
||||||
|
"// cl-upsert",
|
||||||
|
"const Upsert = useUpsert({",
|
||||||
" items: []",
|
" items: []",
|
||||||
" },",
|
"});",
|
||||||
" // 表格配置",
|
"",
|
||||||
" table: {",
|
"// cl-table",
|
||||||
|
"const Table = useTable({",
|
||||||
" columns: []",
|
" columns: []",
|
||||||
" }",
|
"});",
|
||||||
" };",
|
"",
|
||||||
|
"// cl-crud",
|
||||||
|
"const Crud = useCrud(",
|
||||||
|
" {",
|
||||||
|
" service: service.demo.goods",
|
||||||
" },",
|
" },",
|
||||||
" methods: {",
|
" (app) => {",
|
||||||
" onLoad({ ctx, app }) {",
|
|
||||||
" ctx.service(${1}).done();",
|
|
||||||
" app.refresh();",
|
" app.refresh();",
|
||||||
" }",
|
" }",
|
||||||
|
");",
|
||||||
|
"",
|
||||||
|
"// 刷新",
|
||||||
|
"function refresh(params?: any) {",
|
||||||
|
" Crud.value?.refresh(params);",
|
||||||
"}",
|
"}",
|
||||||
"};",
|
|
||||||
"</script>",
|
"</script>",
|
||||||
""
|
""
|
||||||
],
|
],
|
||||||
"description": "cl-crud snippets"
|
"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
|
FROM node:lts-alpine
|
||||||
WORKDIR /build
|
WORKDIR /build
|
||||||
# 设置Node-Sass的镜像地址
|
# 设置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镜像
|
# 设置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
|
COPY package.json /build/package.json
|
||||||
RUN npm install
|
RUN yarn
|
||||||
COPY ./ /build
|
COPY ./ /build
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
|
|||||||
258
README.md
258
README.md
@ -1,10 +1,10 @@
|
|||||||
# cool-admin [vue2]
|
# cool-admin [vue3 - ts - vite]
|
||||||
|
|
||||||
<p align="center">
|
<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>
|
<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>
|
||||||
|
|
||||||
<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">
|
<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" />
|
<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)
|
- [⚡️ v5 vue3 + element-plus + ts + vite](https://github.com/cool-team-official/cool-admin-vue/tree/5.x)
|
||||||
|
|
||||||
- [📌 vue3.x + element-plus + ts + vite](https://github.com/cool-team-official/cool-admin-vue/tree/vue3-ts-vite)
|
|
||||||
|
|
||||||
- [🌐 码云仓库地址](https://gitee.com/cool-team-official/cool-admin-vue)
|
- [🌐 码云仓库地址](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/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`:
|
推荐使用 `yarn`:
|
||||||
@ -58,244 +44,14 @@
|
|||||||
yarn
|
yarn
|
||||||
```
|
```
|
||||||
|
|
||||||
解决 `node-sass` 网络慢的方法:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
yarn config set sass-binary-site http://npm.taobao.org/mirrors/node-sass
|
|
||||||
```
|
|
||||||
|
|
||||||
## 运行应用程序
|
## 运行应用程序
|
||||||
|
|
||||||
安装过程完成后,运行以下命令启动服务。您可以在浏览器中预览网站 [http://localhost:9000](http://localhost:9000)
|
安装过程完成后,运行以下命令启动服务。您可以在浏览器中预览网站 [http://localhost:9000](http://localhost:9000)
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
yarn serve
|
yarn dev
|
||||||
```
|
```
|
||||||
|
|
||||||
## 极速 CRUD
|
### 低价服务器
|
||||||
|
|
||||||
1. `vscode` 编辑器下输入关键字 `cl-crud`,会生成对应的模板代码:
|
[阿里云、腾讯云、华为云低价云服务器,不限新老](https://cool-js.com/ad/server.html)
|
||||||
|
|
||||||
```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. 效果预览
|
|
||||||
|
|
||||||

|
|
||||||
|
|||||||
@ -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;
|
access_log /var/log/nginx/access.log main;
|
||||||
sendfile on;
|
sendfile on;
|
||||||
keepalive_timeout 65;
|
keepalive_timeout 65;
|
||||||
|
upstream backend {
|
||||||
|
server midway:8001;
|
||||||
|
}
|
||||||
|
|
||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
@ -25,7 +28,7 @@ http {
|
|||||||
}
|
}
|
||||||
location /api/
|
location /api/
|
||||||
{
|
{
|
||||||
proxy_pass http://midway:7001/;
|
proxy_pass http://backend/;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
@ -48,6 +51,42 @@ http {
|
|||||||
|
|
||||||
#expires 12h;
|
#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/
|
location /adminer/
|
||||||
{
|
{
|
||||||
|
|||||||
110
package.json
110
package.json
@ -1,65 +1,67 @@
|
|||||||
{
|
{
|
||||||
"name": "cool-admin-vue",
|
"name": "cool-admin",
|
||||||
"version": "3.2.2",
|
"version": "7.0.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"serve": "vue-cli-service serve",
|
"dev": "vite --host",
|
||||||
"build": "vue-cli-service build",
|
"build": "vite build",
|
||||||
"report": "vue-cli-service build --report",
|
"serve": "vite preview",
|
||||||
"lint": "vue-cli-service lint",
|
"lint:prettier": "prettier --write --loglevel warn \"src/**/*.{js,json,tsx,css,less,scss,vue,html,md}\"",
|
||||||
"inspect": "vue inspect --mode=production > output.js"
|
"lint:eslint": "eslint \"{src}/**/*.{vue,ts,tsx}\" --fix"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^0.21.1",
|
"@cool-vue/crud": "^7.0.0-beta9",
|
||||||
"cl-admin": "^1.5.3",
|
"@element-plus/icons-vue": "^2.1.0",
|
||||||
"cl-admin-crud": "^1.6.15",
|
"@vueuse/core": "^10.4.0",
|
||||||
"cl-admin-theme": "^0.0.5",
|
"@wangeditor/editor": "^5.1.23",
|
||||||
"clipboard": "^2.0.7",
|
"@wangeditor/editor-for-vue": "^5.1.12",
|
||||||
"codemirror": "^5.59.4",
|
"axios": "^1.5.0",
|
||||||
"core-js": "^3.6.5",
|
"chardet": "^1.6.0",
|
||||||
"dayjs": "^1.10.4",
|
"core-js": "^3.32.1",
|
||||||
"echarts": "^5.0.2",
|
"dayjs": "^1.11.9",
|
||||||
"element-ui": "^2.15.1",
|
"echarts": "^5.4.3",
|
||||||
"js-beautify": "^1.13.5",
|
"element-plus": "^2.3.12",
|
||||||
|
"file-saver": "^2.0.5",
|
||||||
|
"lodash-es": "^4.17.21",
|
||||||
|
"mitt": "^3.0.1",
|
||||||
"mockjs": "^1.1.0",
|
"mockjs": "^1.1.0",
|
||||||
|
"monaco-editor": "0.36.0",
|
||||||
|
"mqtt": "^4.3.7",
|
||||||
"nprogress": "^0.2.0",
|
"nprogress": "^0.2.0",
|
||||||
"qs": "^6.9.1",
|
"pinia": "^2.1.6",
|
||||||
"quill": "^1.3.7",
|
"socket.io-client": "^4.7.2",
|
||||||
"socket.io-client": "2.3.1",
|
|
||||||
"store": "^2.0.12",
|
"store": "^2.0.12",
|
||||||
"uuid": "^8.3.2",
|
"ts-wps": "^1.0.5",
|
||||||
"vue": "^2.6.11",
|
"vue": "^3.3.4",
|
||||||
"vue-codemirror": "^4.0.6",
|
"vue-echarts": "^6.6.1",
|
||||||
"vue-cron": "^1.0.9",
|
"vue-router": "^4.2.4",
|
||||||
"vue-echarts": "^6.0.0-rc.3",
|
"vuedraggable": "^4.1.0",
|
||||||
"vue-router": "^3.2.0",
|
"xlsx": "^0.18.5"
|
||||||
"vuedraggable": "^2.24.3",
|
|
||||||
"vuex": "^3.4.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@typescript-eslint/parser": "^3.0.0",
|
"@types/lodash-es": "^4.17.8",
|
||||||
"@vue/babel-helper-vue-jsx-merge-props": "^1.0.0",
|
"@types/mockjs": "^1.0.7",
|
||||||
"@vue/babel-preset-jsx": "^1.1.2",
|
"@types/node": "^20.5.6",
|
||||||
"@vue/cli-plugin-babel": "~4.5.0",
|
"@types/nprogress": "^0.2.0",
|
||||||
"@vue/cli-plugin-eslint": "~4.5.0",
|
"@types/prettier": "^2.7.3",
|
||||||
"@vue/cli-plugin-router": "~4.5.0",
|
"@types/quill": "^2.0.10",
|
||||||
"@vue/cli-plugin-vuex": "~4.5.0",
|
"@types/store": "^2.0.2",
|
||||||
"@vue/cli-service": "~4.5.0",
|
"@typescript-eslint/eslint-plugin": "^6.4.1",
|
||||||
"@vue/composition-api": "^1.0.0-rc.5",
|
"@typescript-eslint/parser": "^6.4.1",
|
||||||
"@vue/eslint-config-prettier": "^6.0.0",
|
"@vitejs/plugin-vue": "^4.3.3",
|
||||||
"babel-eslint": "^10.1.0",
|
"@vitejs/plugin-vue-jsx": "^3.0.2",
|
||||||
"babel-plugin-component": "^1.1.1",
|
"@vue/compiler-sfc": "^3.3.4",
|
||||||
"babel-plugin-jsx-v-model": "^2.0.3",
|
"eslint": "^8.48.0",
|
||||||
"clean-webpack-plugin": "^3.0.0",
|
"eslint-config-prettier": "^9.0.0",
|
||||||
"eslint": "^6.7.2",
|
"eslint-plugin-prettier": "^5.0.0",
|
||||||
"eslint-plugin-prettier": "^3.1.3",
|
"eslint-plugin-vue": "^9.17.0",
|
||||||
"eslint-plugin-vue": "^6.2.2",
|
"lodash": "^4.17.21",
|
||||||
"hard-source-webpack-plugin": "^0.13.1",
|
"magic-string": "^0.30.3",
|
||||||
"node-sass": "^4.12.0",
|
"prettier": "^2.8.4",
|
||||||
"prettier": "^1.19.1",
|
"rollup-plugin-visualizer": "^5.9.2",
|
||||||
"sass-loader": "^8.0.2",
|
"sass": "^1.66.1",
|
||||||
"svg-sprite-loader": "^5.0.0",
|
"terser": "^5.19.2",
|
||||||
"typescript": "^3.9.3",
|
"typescript": "^5.2.2",
|
||||||
"vue-template-compiler": "^2.6.11",
|
"vite": "^4.4.9",
|
||||||
"webpack-cli": "^3.3.12"
|
"vite-plugin-compression": "^0.5.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,2 +1,3 @@
|
|||||||
> 1%
|
> 1%
|
||||||
last 2 versions
|
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…
x
Reference in New Issue
Block a user