发布7.x

This commit is contained in:
神仙都没用 2023-09-28 13:50:15 +08:00
parent 76c7045cf8
commit 1735d6258e
623 changed files with 48615 additions and 77206 deletions

View File

@ -1,21 +1,5 @@
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
yarn.lock
# Editor directories and files
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
.DS_Store
dist
dist-ssr
*.local

5
.env Normal file
View File

@ -0,0 +1,5 @@
# 应用名称
VITE_NAME = "COOL-ADMIN"
# 网络超时请求时间
VITE_TIMEOUT = 30000

View File

@ -1,6 +1 @@
/public/
/dist/
/node_modules/
/src/icons/svg/
/mock/
vue.config.js
vite.config.ts

View File

@ -1,14 +1,67 @@
module.exports = {
root: true,
env: {
node: true
},
extends: ["plugin:vue/essential", "@vue/prettier"],
rules: {
"no-console": "off",
"comma-dangle": [2, "never"]
browser: true,
node: true,
es6: true
},
parser: "vue-eslint-parser",
parserOptions: {
parser: "@typescript-eslint/parser"
parser: "@typescript-eslint/parser",
ecmaVersion: 2020,
sourceType: "module",
jsxPragma: "React",
ecmaFeatures: {
jsx: true,
tsx: true
}
},
extends: [
"plugin:vue/vue3-recommended",
"plugin:@typescript-eslint/recommended",
"prettier",
"plugin:prettier/recommended"
],
rules: {
"@typescript-eslint/ban-ts-ignore": "off",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-var-requires": "off",
"@typescript-eslint/no-empty-function": "off",
"vue/component-name-in-template-casing": ["error", "kebab-case"],
"vue/component-definition-name-casing": ["error", "kebab-case"],
"no-use-before-define": "off",
"@typescript-eslint/no-use-before-define": "off",
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/ban-types": "off",
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-namespace": "off",
"@typescript-eslint/no-unused-vars": [
"error",
{
argsIgnorePattern: "^h$",
varsIgnorePattern: "^h$"
}
],
"no-unused-vars": [
"error",
{
argsIgnorePattern: "^h$",
varsIgnorePattern: "^h$"
}
],
"space-before-function-paren": "off",
"vue/attributes-order": "off",
"vue/one-component-per-file": "off",
"vue/html-closing-bracket-newline": "off",
"vue/max-attributes-per-line": "off",
"vue/multiline-html-element-content-newline": "off",
"vue/multi-word-component-names": "off",
"vue/singleline-html-element-content-newline": "off",
"vue/attribute-hyphenation": "off",
"vue/html-self-closing": "off",
"vue/require-default-prop": "off",
"vue/v-on-event-hyphenation": "off"
}
};

4
.gitattributes vendored Normal file
View File

@ -0,0 +1,4 @@
*.js text eol=lf
*.json text eol=lf
*.ts text eol=lf
*.vue text eol=lf

24
.gitignore vendored
View File

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

16
.hintrc Normal file
View 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"
}
}

View File

@ -2,7 +2,6 @@
"tabWidth": 4,
"useTabs": true,
"semi": true,
"jsxBracketSameLine": true,
"singleQuote": false,
"printWidth": 100,
"trailingComma": "none"

15
.vscode/config.code-snippets vendored Normal file
View 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"
}
}

View File

@ -1,10 +1,11 @@
{
"cl-crud": {
"prefix": "cl-crud",
"scope": "vue",
"body": [
"<template>",
" <cl-crud ref=\"crud\" @load=\"onLoad\">",
" <el-row type=\"flex\" align=\"middle\">",
" <cl-crud ref=\"Crud\">",
" <cl-row>",
" <!-- 刷新按钮 -->",
" <cl-refresh-btn />",
" <!-- 新增按钮 -->",
@ -14,48 +15,88 @@
" <cl-flex1 />",
" <!-- 关键字搜索 -->",
" <cl-search-key />",
" </el-row>",
" </cl-row>",
"",
" <el-row>",
" <cl-row>",
" <!-- 数据表格 -->",
" <cl-table v-bind=\"table\"></cl-table>",
" </el-row>",
" <cl-table ref=\"Table\" />",
" </cl-row>",
"",
" <el-row type=\"flex\">",
" <cl-row>",
" <cl-flex1 />",
" <!-- 分页控件 -->",
" <cl-pagination />",
" </el-row>",
" </cl-row>",
"",
" <!-- 新增、编辑 -->",
" <cl-upsert ref=\"upsert\" v-bind=\"upsert\"></cl-upsert>",
" <cl-upsert ref=\"Upsert\" />",
" </cl-crud>",
"</template>",
"",
"<script>",
"export default {",
" data() {",
" return {",
" // 新增、编辑配置",
" upsert: {",
" items: []",
" },",
" // 表格配置",
" table: {",
" columns: []",
" }",
" };",
"<script lang=\"ts\" name=\"菜单名称\" setup>",
"import { useCrud, useTable, useUpsert } from \"@cool-vue/crud\";",
"import { useCool } from \"/@/cool\";",
"",
"const { service } = useCool();",
"",
"// cl-upsert",
"const Upsert = useUpsert({",
" items: []",
"});",
"",
"// cl-table",
"const Table = useTable({",
" columns: []",
"});",
"",
"// cl-crud",
"const Crud = useCrud(",
" {",
" service: service.demo.goods",
" },",
" methods: {",
" onLoad({ ctx, app }) {",
" ctx.service(${1}).done();",
" app.refresh();",
" }",
" (app) => {",
" app.refresh();",
" }",
"};",
");",
"",
"// 刷新",
"function refresh(params?: any) {",
" Crud.value?.refresh(params);",
"}",
"</script>",
""
],
"description": "cl-crud snippets"
},
"cl-filter": {
"prefix": "cl-filter",
"scope": "html",
"body": [
"<cl-filter label=\"\">",
" <cl-select :options=\"[$1]\" prop=\"\" />",
"</cl-filter>"
],
"description": "cl-filter snippets"
},
"item": {
"prefix": "item",
"scope": "typescript",
"body": [
"{",
" label: \"$1\",",
" prop: \"\",",
" component: {",
" name: \"\"",
" }",
"},",
""
],
"description": "item snippets"
},
"column": {
"prefix": "column",
"scope": "typescript",
"body": ["{", " label: \"$1\",", " prop: \"\",", "},", ""],
"description": "column snippets"
}
}

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

@ -0,0 +1,4 @@
{
"editor.cursorSmoothCaretAnimation": "on",
"editor.formatOnSave": true,
}

View File

@ -1,11 +1,11 @@
FROM node:lts-alpine
WORKDIR /build
# 设置Node-Sass的镜像地址
RUN npm config set sass_binary_site https://repo.huaweicloud.com/node-sass
RUN npm config set sass_binary_site=https://npm.taobao.org/mirrors/node-sass/
# 设置npm镜像
RUN npm config set registry https://repo.huaweicloud.com/repository/npm/
RUN npm config set registry https://registry.npm.taobao.org
COPY package.json /build/package.json
RUN npm install
RUN yarn
COPY ./ /build
RUN npm run build
@ -13,4 +13,4 @@ FROM nginx
RUN mkdir /app
COPY --from=0 /build/dist /app
COPY --from=0 /build/nginx.conf /etc/nginx/nginx.conf
EXPOSE 80
EXPOSE 80

258
README.md
View File

@ -1,10 +1,10 @@
# cool-admin [vue2]
# cool-admin [vue3 - ts - vite]
<p align="center">
<a href="https://show.cool-admin.com/" target="blank"><img src="https://admin.cool-js.com/logo.png" width="200" alt="cool-admin Logo" /></a>
</p>
<p align="center">cool-admin 一个很酷的后台权限管理系统,开源免费,模块化、插件化、极速开发 CRUD方便快速构建迭代后台管理系统论坛 进一步了解</p>
<p align="center">cool-admin 一个很酷的后台权限管理系统,开源免费,模块化、插件化、极速开发 CRUD方便快速构建迭代后台管理系统<a href="https://cool-js.com" target="_blank">文档</a> 进一步了解</p>
<p align="center">
<a href="https://github.com/cool-team-official/cool-admin-vue/blob/master/LICENSE" target="_blank"><img src="https://img.shields.io/badge/license-MIT-green?style=flat-square" alt="GitHub license" />
@ -14,11 +14,9 @@
## 地址
- [⚡️ vue2.x + element-ui](https://github.com/cool-team-official/cool-admin-vue)
- [📌 v6 vue3 + element-plus + ts + vite](https://github.com/cool-team-official/cool-admin-vue/tree/6.x)
- [⚡️ vue3.x + element-plus + ts + webpack](https://github.com/cool-team-official/cool-admin-vue/tree/vue3-ts-webpack)
- [📌 vue3.x + element-plus + ts + vite](https://github.com/cool-team-official/cool-admin-vue/tree/vue3-ts-vite)
- [⚡️ v5 vue3 + element-plus + ts + vite](https://github.com/cool-team-official/cool-admin-vue/tree/5.x)
- [🌐 码云仓库地址](https://gitee.com/cool-team-official/cool-admin-vue)
@ -38,18 +36,6 @@
<img width="260" src="https://cool-show.oss-cn-shanghai.aliyuncs.com/admin/wechat.jpeg" alt="Admin Wechat"></a>
## 微信公众号
<img width="260" src="https://cool-show.oss-cn-shanghai.aliyuncs.com/admin/mp.jpg" alt="Admin Wechat"></a>
## 在线社区
[https://bbs.cool-js.com/](https://bbs.cool-js.com/)
## 使用条件
请确保您的操作系统上安装了 Node.js> = 8.9.0)、@vue/cli > 3.0.0)。
## 安装项目依赖
推荐使用 `yarn`
@ -58,244 +44,14 @@
yarn
```
解决 `node-sass` 网络慢的方法:
```shell
yarn config set sass-binary-site http://npm.taobao.org/mirrors/node-sass
```
## 运行应用程序
安装过程完成后,运行以下命令启动服务。您可以在浏览器中预览网站 [http://localhost:9000](http://localhost:9000)
```shell
yarn serve
yarn dev
```
## 极速 CRUD
### 低价服务器
1. `vscode` 编辑器下输入关键字 `cl-crud`,会生成对应的模板代码:
```html
<template>
<cl-crud ref="crud" @load="onLoad">
<el-row type="flex" align="middle">
<!-- 刷新按钮 -->
<cl-refresh-btn />
<!-- 新增按钮 -->
<cl-add-btn />
<!-- 删除按钮 -->
<cl-multi-delete-btn />
<cl-flex1 />
<!-- 关键字搜索 -->
<cl-search-key />
</el-row>
<el-row>
<!-- 数据表格 -->
<cl-table v-bind="table"></cl-table>
</el-row>
<el-row type="flex">
<cl-flex1 />
<!-- 分页控件 -->
<cl-pagination />
</el-row>
<!-- 新增、编辑 -->
<cl-upsert ref="upsert" v-bind="upsert"></cl-upsert>
</cl-crud>
</template>
<script>
export default {
data() {
return {
// 新增、编辑配置
upsert: {
items: []
},
// 表格配置
table: {
columns: []
}
};
},
methods: {
onLoad({ ctx, app }) {
// crud 配置
ctx.service().done();
// 发送 page 接口请求
app.refresh();
}
}
};
</script>
```
2. 编辑数据表格 `cl-table`
```js
{
table: {
// 参数与 el-table-column 一致,并支持许多骚操作
columns: [
// 多选列
{
type: "selection",
width: 60
},
// 自定义列
{
label: "昵称",
prop: "name"
},
{
label: "账户",
prop: "price",
sortable: "custom" // 是否添加排序
},
{
label: "状态",
prop: "status",
// 字典匹配,使用 el-tag 展示
dict: [
{
label: "启用",
value: 1,
type: "primary"
},
{
label: "禁用",
value: 0,
type: "danger"
}
]
},
{
label: "创建时间",
prop: "createTime"
},
// 操作按钮列,默认包含:编辑、删除
{
type: "op"
}
];
}
}
```
3. 编辑新增、编辑表单 `cl-upsert`
```js
{
upsert: {
items: [
{
label: "昵称",
prop: "name",
// 参数与 el-form-item 一致
props: {},
value: "神仙都没用", // 昵称默认值
// 渲染参数,支持 slot, 组件实例jsx
component: {
name: "el-input", // 可以是任意已注册的组件名
props: {}, // 组件的参数
on: {} // 组件的回调事件
},
// 验证规则,与 el-form 一致
rules: {
required: true,
message: "昵称不呢为空"
}
},
{
label: "存款",
prop: "price",
component: {
name: "el-input-number",
props: {
min: 0,
max: 10000
}
}
},
{
label: "状态",
prop: "status",
value: 1,
component: {
name: "el-radio-group",
options: [
{
label: "启用",
value: 1
},
{
label: "禁用",
value: 0
}
]
}
}
];
}
}
```
4. 绑定 `service`。在 `src/service/` 下新建文件 `test.js`,并编辑:
```js
// src/service/test.js
import { BaseService, Service, Permission } from "cl-admin";
// 请求接口的路径
@Service("test")
class Test extends BaseService {
// 继承 BaseService 后,拥有 page, list, add, delete, update, info 6个基本接口
// 自定义其他接口
@Permission("product") // 权限装饰器,可选
product(id) {
// this.request() 参数与 axios 一致
return this.request({
url: "/product",
method: "POST",
data: {
id
}
});
}
}
export default Test;
```
`src/service/` 下的文件,框架会自动根据 `目录结构``文件名称` 格式化成对应的 `$service` 对象。你现在可以这么使用它:
```js
this.$service.test.page({ page: 1 });
this.$service.test.product(1);
```
`service` 编写好后,我们把它绑定在 `crud` 上:
```js
export default {
methods: {
onLoad({ ctx, app }) {
// 绑定 service这边指定到 test 即可
ctx.service(this.$service.test).done();
// 发起 page 请求
app.refresh({
// 请求默认参数
});
}
}
};
```
5. 效果预览
![](https://cool-show.oss-cn-shanghai.aliyuncs.com/admin/crud.png)
[阿里云、腾讯云、华为云低价云服务器,不限新老](https://cool-js.com/ad/server.html)

View File

@ -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
View 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
View 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
View 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
View 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);
}

View 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
View 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
View 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
View 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
View 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>

View File

@ -14,6 +14,9 @@ http {
access_log /var/log/nginx/access.log main;
sendfile on;
keepalive_timeout 65;
upstream backend {
server midway:8001;
}
server {
listen 80;
@ -25,7 +28,7 @@ http {
}
location /api/
{
proxy_pass http://midway:7001/;
proxy_pass http://backend/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
@ -48,6 +51,42 @@ http {
#expires 12h;
}
# location /im {
# proxy_pass http://backend/im;
# proxy_connect_timeout 3600s; #配置点1
# proxy_read_timeout 3600s; #配置点2,如果没效,可以考虑这个时间配置长一点
# proxy_send_timeout 3600s; #配置点3
# proxy_set_header Host $host;
# proxy_set_header X-Real-IP $remote_addr;
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# proxy_set_header REMOTE-HOST $remote_addr;
# #proxy_bind $remote_addr transparent;
# proxy_http_version 1.1;
# proxy_set_header Upgrade $http_upgrade;
# proxy_set_header Connection "upgrade";
# # rewrite /socket/(.*) /$1 break;
# proxy_redirect off;
# }
# location /socket {
# proxy_pass http://backend/socket;
# proxy_connect_timeout 3600s; #配置点1
# proxy_read_timeout 3600s; #配置点2,如果没效,可以考虑这个时间配置长一点
# proxy_send_timeout 3600s; #配置点3
# proxy_set_header Host $host;
# proxy_set_header X-Real-IP $remote_addr;
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# proxy_set_header REMOTE-HOST $remote_addr;
# #proxy_bind $remote_addr transparent;
# proxy_http_version 1.1;
# proxy_set_header Upgrade $http_upgrade;
# proxy_set_header Connection "upgrade";
# rewrite /socket/(.*) /$1 break;
# proxy_redirect off;
# }
location /adminer/
{

View File

@ -1,65 +1,67 @@
{
"name": "cool-admin-vue",
"version": "3.2.2",
"name": "cool-admin",
"version": "7.0.0",
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"report": "vue-cli-service build --report",
"lint": "vue-cli-service lint",
"inspect": "vue inspect --mode=production > output.js"
"dev": "vite --host",
"build": "vite build",
"serve": "vite preview",
"lint:prettier": "prettier --write --loglevel warn \"src/**/*.{js,json,tsx,css,less,scss,vue,html,md}\"",
"lint:eslint": "eslint \"{src}/**/*.{vue,ts,tsx}\" --fix"
},
"dependencies": {
"axios": "^0.21.1",
"cl-admin": "^1.5.3",
"cl-admin-crud": "^1.6.15",
"cl-admin-theme": "^0.0.5",
"clipboard": "^2.0.7",
"codemirror": "^5.59.4",
"core-js": "^3.6.5",
"dayjs": "^1.10.4",
"echarts": "^5.0.2",
"element-ui": "^2.15.1",
"js-beautify": "^1.13.5",
"@cool-vue/crud": "^7.0.0-beta9",
"@element-plus/icons-vue": "^2.1.0",
"@vueuse/core": "^10.4.0",
"@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "^5.1.12",
"axios": "^1.5.0",
"chardet": "^1.6.0",
"core-js": "^3.32.1",
"dayjs": "^1.11.9",
"echarts": "^5.4.3",
"element-plus": "^2.3.12",
"file-saver": "^2.0.5",
"lodash-es": "^4.17.21",
"mitt": "^3.0.1",
"mockjs": "^1.1.0",
"monaco-editor": "0.36.0",
"mqtt": "^4.3.7",
"nprogress": "^0.2.0",
"qs": "^6.9.1",
"quill": "^1.3.7",
"socket.io-client": "2.3.1",
"pinia": "^2.1.6",
"socket.io-client": "^4.7.2",
"store": "^2.0.12",
"uuid": "^8.3.2",
"vue": "^2.6.11",
"vue-codemirror": "^4.0.6",
"vue-cron": "^1.0.9",
"vue-echarts": "^6.0.0-rc.3",
"vue-router": "^3.2.0",
"vuedraggable": "^2.24.3",
"vuex": "^3.4.0"
"ts-wps": "^1.0.5",
"vue": "^3.3.4",
"vue-echarts": "^6.6.1",
"vue-router": "^4.2.4",
"vuedraggable": "^4.1.0",
"xlsx": "^0.18.5"
},
"devDependencies": {
"@typescript-eslint/parser": "^3.0.0",
"@vue/babel-helper-vue-jsx-merge-props": "^1.0.0",
"@vue/babel-preset-jsx": "^1.1.2",
"@vue/cli-plugin-babel": "~4.5.0",
"@vue/cli-plugin-eslint": "~4.5.0",
"@vue/cli-plugin-router": "~4.5.0",
"@vue/cli-plugin-vuex": "~4.5.0",
"@vue/cli-service": "~4.5.0",
"@vue/composition-api": "^1.0.0-rc.5",
"@vue/eslint-config-prettier": "^6.0.0",
"babel-eslint": "^10.1.0",
"babel-plugin-component": "^1.1.1",
"babel-plugin-jsx-v-model": "^2.0.3",
"clean-webpack-plugin": "^3.0.0",
"eslint": "^6.7.2",
"eslint-plugin-prettier": "^3.1.3",
"eslint-plugin-vue": "^6.2.2",
"hard-source-webpack-plugin": "^0.13.1",
"node-sass": "^4.12.0",
"prettier": "^1.19.1",
"sass-loader": "^8.0.2",
"svg-sprite-loader": "^5.0.0",
"typescript": "^3.9.3",
"vue-template-compiler": "^2.6.11",
"webpack-cli": "^3.3.12"
"@types/lodash-es": "^4.17.8",
"@types/mockjs": "^1.0.7",
"@types/node": "^20.5.6",
"@types/nprogress": "^0.2.0",
"@types/prettier": "^2.7.3",
"@types/quill": "^2.0.10",
"@types/store": "^2.0.2",
"@typescript-eslint/eslint-plugin": "^6.4.1",
"@typescript-eslint/parser": "^6.4.1",
"@vitejs/plugin-vue": "^4.3.3",
"@vitejs/plugin-vue-jsx": "^3.0.2",
"@vue/compiler-sfc": "^3.3.4",
"eslint": "^8.48.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.0",
"eslint-plugin-vue": "^9.17.0",
"lodash": "^4.17.21",
"magic-string": "^0.30.3",
"prettier": "^2.8.4",
"rollup-plugin-visualizer": "^5.9.2",
"sass": "^1.66.1",
"terser": "^5.19.2",
"typescript": "^5.2.2",
"vite": "^4.4.9",
"vite-plugin-compression": "^0.5.1"
}
}

View File

@ -1,2 +1,3 @@
> 1%
last 2 versions
not dead

23
packages/crud/.gitignore vendored Normal file
View File

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

View File

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

35
packages/crud/README.md Normal file
View 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),构建工具;

View File

@ -0,0 +1,3 @@
module.exports = {
presets: ["@vue/cli-plugin-babel/preset"]
};

1
packages/crud/env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="./index" />

730
packages/crud/index.d.ts vendored Normal file
View 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;
}

View 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"
]
}

View File

@ -0,0 +1,3 @@
<template>
<div>CRUD v7.0.0</div>
</template>

View File

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

View File

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

View File

@ -0,0 +1,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>
);
};
}
});

View 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;

View 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
};
}

View 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>
);
};
}
});

View 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;
}
}
);
};
}
});

View 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" />;
};
}
});

View File

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

View File

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

View File

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

View File

@ -0,0 +1,145 @@
import {
defineComponent,
h,
nextTick,
onMounted,
PropType,
reactive,
ref,
toRaw,
watch
} from "vue";
import { isEmpty } from "lodash-es";
import { 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>
);
};
}
});

View 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
};
}

View 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
);
}

View 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";

View 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
};
}

View 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
};
}

View 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();
}
}
);
}
};
}
});

View 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]);
}
}

View File

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

View File

@ -0,0 +1,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
}
);
};
}
});

View File

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

View File

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

View File

@ -0,0 +1,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>
);
};
}
});

View 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>
)
);
};
}
});

View File

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

View File

@ -0,0 +1,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
};
}

View 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";

View File

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

View File

@ -0,0 +1,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
};
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,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();
}
}
)
);
};
}
});

View File

@ -0,0 +1,306 @@
import { defineComponent, h, inject, reactive, ref, toRefs } from "vue";
import { ElMessage } from "element-plus";
import { useCore, useProxy } from "../../hooks";
import { useApi } from "../form/helper";
import { mergeConfig } from "../../utils";
export default defineComponent({
name: "cl-upsert",
props: {
// 表单项
items: {
type: Array,
default: () => []
},
// <el-form /> 参数
props: Object,
// 编辑时是否同步打开
sync: Boolean,
// 操作按钮参数
op: Object,
// <cl-dialog /> 参数
dialog: Object,
// 打开表单钩子
onOpen: Function,
// 打开表单后钩子
onOpened: Function,
// 关闭表单钩子
onClose: Function,
// 关闭表单后钩子
onClosed: Function,
// 获取表单数据钩子
onInfo: Function,
// 表单提交钩子
onSubmit: Function
},
emits: ["opened", "closed"],
setup(props, { slots, expose }) {
const { crud } = useCore();
const config = reactive<ClUpsert.Config>(
mergeConfig(props, inject("useUpsert__options") || {})
);
// el-form
const Form = ref<ClForm.Ref>();
// 模式
const mode = ref<ClUpsert.Ref["mode"]>("info");
// 关闭表单
function close(action?: ClForm.CloseAction) {
Form.value?.close(action);
}
// 关闭后
function onClosed() {
Form.value?.hideLoading();
if (config.onClosed) {
config.onClosed();
}
}
// 关闭前
function beforeClose(action: ClForm.CloseAction, done: fn) {
function next() {
done();
onClosed();
}
if (config.onClose) {
config.onClose(action, next);
} else {
next();
}
}
// 提交
function submit(data: obj) {
const { service, dict, refresh } = crud;
function done() {
Form.value?.done();
}
function next(data: obj) {
return new Promise((resolve, reject) => {
// 发送请求
service[dict.api[mode.value]](data)
.then((res) => {
ElMessage.success(dict.label.saveSuccess);
done();
close("save");
refresh();
resolve(res);
})
.catch((err) => {
ElMessage.error(err.message);
done();
reject(err);
});
});
}
// 提交钩子
if (config.onSubmit) {
config.onSubmit(data, {
done,
next,
close() {
close("save");
}
});
} else {
next(data);
}
}
// 打开表单
function open() {
// 是否禁用
const isDisabled = mode.value == "info";
return new Promise((resolve) => {
if (!Form.value) {
return console.error("<cl-upsert /> is not found");
}
Form.value?.open(
{
title: crud.dict.label[mode.value],
props: {
...config.props,
disabled: isDisabled
},
op: {
...config.op,
hidden: isDisabled
},
dialog: config.dialog,
items: config.items || [],
on: {
open(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>;
};
}
});

View File

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

1
packages/crud/src/env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="../index" />

View 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;
}

View 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";

View 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";

View 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"
};

View File

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

View 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: "空にできません"
};

View 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: "不能为空"
};

View 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
View 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");

View 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();
});
}
};
}

View 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);
}

View 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;
}
}
}

View 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;

View 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);
}

View File

@ -0,0 +1,30 @@
import _mitt from "mitt";
const mitt = _mitt();
class Mitt {
id: number;
constructor(id?: number) {
this.id = id || 0;
}
send(type: "emit" | "off" | "on", name: string, ...args: any[]) {
// @ts-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 };

View 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 {};
}
}

View 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;
}
};

View 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={`Errorname is required`} />;
}
}
}

View 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/*"]
}

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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 {};

View 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;

View 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