commit
e97978d0b7
768 changed files with 83991 additions and 0 deletions
@ -0,0 +1,305 @@ |
|||||
|
--- |
||||
|
description: cl-adv-search 组件示例 |
||||
|
globs: *.tsx, *.ts, *.vue |
||||
|
--- |
||||
|
## 起步 示例 |
||||
|
|
||||
|
```vue |
||||
|
<template> |
||||
|
<div class="scope"> |
||||
|
<div class="h"> |
||||
|
<el-tag size="small" effect="dark" disable-transitions>base</el-tag> |
||||
|
<span>起步</span> |
||||
|
</div> |
||||
|
|
||||
|
<div class="c"> |
||||
|
<el-button @click="open">预览</el-button> |
||||
|
<demo-code :files="['adv-search/base.vue']" /> |
||||
|
|
||||
|
<!-- 自定义表格组件 --> |
||||
|
<cl-dialog v-model="visible" title="起步" width="80%"> |
||||
|
<cl-crud ref="Crud"> |
||||
|
<cl-row> |
||||
|
<!--【很重要】高级搜索组件按钮 --> |
||||
|
<cl-adv-btn /> |
||||
|
</cl-row> |
||||
|
|
||||
|
<cl-row> |
||||
|
<cl-table ref="Table" /> |
||||
|
</cl-row> |
||||
|
|
||||
|
<cl-row> |
||||
|
<cl-flex1 /> |
||||
|
<cl-pagination /> |
||||
|
</cl-row> |
||||
|
|
||||
|
<!--【很重要】高级搜索组件 --> |
||||
|
<cl-adv-search ref="AdvSearch" /> |
||||
|
</cl-crud> |
||||
|
</cl-dialog> |
||||
|
</div> |
||||
|
|
||||
|
<div class="f"> |
||||
|
<span class="date">2024-01-01</span> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script setup lang="ts"> |
||||
|
import { useCrud, useAdvSearch, useTable } from '@cool-vue/crud'; |
||||
|
import { ref } from 'vue'; |
||||
|
import { useDict } from '/$/dict'; |
||||
|
|
||||
|
const { dict } = useDict(); |
||||
|
|
||||
|
// cl-crud 配置 |
||||
|
const Crud = useCrud( |
||||
|
{ |
||||
|
service: 'test' |
||||
|
}, |
||||
|
app => { |
||||
|
app.refresh(); |
||||
|
} |
||||
|
); |
||||
|
|
||||
|
// cl-table 配置 |
||||
|
const Table = useTable({ |
||||
|
autoHeight: false, |
||||
|
contextMenu: ['refresh'], |
||||
|
|
||||
|
columns: [ |
||||
|
{ |
||||
|
label: '姓名', |
||||
|
prop: 'name', |
||||
|
minWidth: 140 |
||||
|
}, |
||||
|
{ |
||||
|
label: '手机号', |
||||
|
prop: 'phone', |
||||
|
minWidth: 140 |
||||
|
}, |
||||
|
{ |
||||
|
label: '工作', |
||||
|
prop: 'occupation', |
||||
|
dict: dict.get('occupation'), |
||||
|
minWidth: 140 |
||||
|
}, |
||||
|
{ |
||||
|
label: '创建时间', |
||||
|
prop: 'createTime', |
||||
|
minWidth: 170, |
||||
|
sortable: 'desc' |
||||
|
} |
||||
|
] |
||||
|
}); |
||||
|
|
||||
|
// cl-adv-search 配置 |
||||
|
//【很重要】该组件基于 cl-form 故很多示例都可复用 |
||||
|
const AdvSearch = useAdvSearch({ |
||||
|
// 配置如 cl-form 一样 |
||||
|
items: [ |
||||
|
{ |
||||
|
label: '姓名', |
||||
|
prop: 'name', |
||||
|
component: { |
||||
|
name: 'el-input', |
||||
|
props: { |
||||
|
clearable: true |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
label: '手机号', |
||||
|
prop: 'phone', |
||||
|
component: { |
||||
|
name: 'el-input', |
||||
|
props: { |
||||
|
clearable: true |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
label: '工作', |
||||
|
prop: 'occupation', |
||||
|
component: { |
||||
|
name: 'cl-select', |
||||
|
props: { |
||||
|
tree: true, |
||||
|
checkStrictly: true, |
||||
|
options: dict.get('occupation') |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
] |
||||
|
}); |
||||
|
|
||||
|
function refresh(params?: any) { |
||||
|
Crud.value?.refresh(params); |
||||
|
} |
||||
|
|
||||
|
const visible = ref(false); |
||||
|
|
||||
|
function open() { |
||||
|
visible.value = true; |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
``` |
||||
|
|
||||
|
## 自定义 示例 |
||||
|
|
||||
|
```vue |
||||
|
<template> |
||||
|
<div class="scope"> |
||||
|
<div class="h"> |
||||
|
<el-tag size="small" effect="dark" disable-transitions>custom</el-tag> |
||||
|
<span>自定义</span> |
||||
|
</div> |
||||
|
|
||||
|
<div class="c"> |
||||
|
<el-button @click="open">预览</el-button> |
||||
|
<demo-code :files="['adv-search/custom.vue']" /> |
||||
|
|
||||
|
<!-- 自定义表格组件 --> |
||||
|
<cl-dialog v-model="visible" title="自定义" width="80%"> |
||||
|
<cl-crud ref="Crud"> |
||||
|
<cl-row> |
||||
|
<!--【很重要】高级搜索组件按钮 --> |
||||
|
<cl-adv-btn>更多搜索</cl-adv-btn> |
||||
|
</cl-row> |
||||
|
|
||||
|
<cl-row> |
||||
|
<cl-table ref="Table" /> |
||||
|
</cl-row> |
||||
|
|
||||
|
<cl-row> |
||||
|
<cl-flex1 /> |
||||
|
<cl-pagination /> |
||||
|
</cl-row> |
||||
|
|
||||
|
<!--【很重要】高级搜索组件 --> |
||||
|
<cl-adv-search ref="AdvSearch"> |
||||
|
<!-- 自定义按钮 --> |
||||
|
<template #slot-btn> |
||||
|
<el-button @click="toSearch">自定义</el-button> |
||||
|
</template> |
||||
|
</cl-adv-search> |
||||
|
</cl-crud> |
||||
|
</cl-dialog> |
||||
|
</div> |
||||
|
|
||||
|
<div class="f"> |
||||
|
<span class="date">2024-01-01</span> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script setup lang="ts"> |
||||
|
import { useCrud, useAdvSearch, useTable } from '@cool-vue/crud'; |
||||
|
import { ref } from 'vue'; |
||||
|
import { useDict } from '/$/dict'; |
||||
|
|
||||
|
const { dict } = useDict(); |
||||
|
|
||||
|
// cl-crud 配置 |
||||
|
const Crud = useCrud( |
||||
|
{ |
||||
|
service: 'test' |
||||
|
}, |
||||
|
app => { |
||||
|
app.refresh(); |
||||
|
} |
||||
|
); |
||||
|
|
||||
|
// cl-table 配置 |
||||
|
const Table = useTable({ |
||||
|
autoHeight: false, |
||||
|
contextMenu: ['refresh'], |
||||
|
|
||||
|
columns: [ |
||||
|
{ |
||||
|
label: '姓名', |
||||
|
prop: 'name', |
||||
|
minWidth: 140 |
||||
|
}, |
||||
|
{ |
||||
|
label: '手机号', |
||||
|
prop: 'phone', |
||||
|
minWidth: 140 |
||||
|
}, |
||||
|
{ |
||||
|
label: '工作', |
||||
|
prop: 'occupation', |
||||
|
dict: dict.get('occupation'), |
||||
|
minWidth: 140 |
||||
|
}, |
||||
|
{ |
||||
|
label: '创建时间', |
||||
|
prop: 'createTime', |
||||
|
minWidth: 170, |
||||
|
sortable: 'desc' |
||||
|
} |
||||
|
] |
||||
|
}); |
||||
|
|
||||
|
// cl-adv-search 配置 |
||||
|
//【很重要】该组件基于 cl-form 故很多示例都可复用 |
||||
|
const AdvSearch = useAdvSearch({ |
||||
|
// 配置如 cl-form 一样 |
||||
|
items: [ |
||||
|
{ |
||||
|
label: '姓名', |
||||
|
prop: 'name', |
||||
|
component: { |
||||
|
name: 'el-input', |
||||
|
props: { |
||||
|
clearable: true |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
label: '手机号', |
||||
|
prop: 'phone', |
||||
|
component: { |
||||
|
name: 'el-input', |
||||
|
props: { |
||||
|
clearable: true |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
label: '工作', |
||||
|
prop: 'occupation', |
||||
|
component: { |
||||
|
name: 'cl-select', |
||||
|
props: { |
||||
|
tree: true, |
||||
|
checkStrictly: true, |
||||
|
options: dict.get('occupation') |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
], |
||||
|
|
||||
|
title: '更多搜索', |
||||
|
size: '50%', |
||||
|
op: ['close', 'search', 'slot-btn'] |
||||
|
}); |
||||
|
|
||||
|
function refresh(params?: any) { |
||||
|
Crud.value?.refresh(params); |
||||
|
} |
||||
|
|
||||
|
// 自定义搜索 |
||||
|
function toSearch() { |
||||
|
refresh({ page: 1 }); |
||||
|
} |
||||
|
|
||||
|
const visible = ref(false); |
||||
|
|
||||
|
function open() { |
||||
|
visible.value = true; |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
``` |
||||
File diff suppressed because it is too large
File diff suppressed because it is too large
@ -0,0 +1,464 @@ |
|||||
|
--- |
||||
|
description: module | plugins 模块、插件 |
||||
|
globs: |
||||
|
--- |
||||
|
# 模块/插件开发 |
||||
|
|
||||
|
## 目录结构 |
||||
|
|
||||
|
在 `src/modules` 或 `src/plugins` 下添加一个目录 `demo`: |
||||
|
|
||||
|
```js |
||||
|
demo |
||||
|
├──pages // 页面路由 |
||||
|
├──views // 视图路由 |
||||
|
├──hooks // 常用函数 |
||||
|
├──components // 常用组件 |
||||
|
├──directives // 指令 |
||||
|
├──static // 静态文件目录 |
||||
|
├──store // 状态管理 |
||||
|
├──... // 其他自定义文件 |
||||
|
├──config.ts // 配置文件 |
||||
|
└──index.ts // 入口文件 |
||||
|
``` |
||||
|
|
||||
|
::: warning |
||||
|
约定的目录名称不可修改,但可自行添加或者删除。 |
||||
|
::: |
||||
|
|
||||
|
## pages、views |
||||
|
|
||||
|
1. 页面参与权限控制,所以不主动注册目录下的路由,通过 `菜单列表` 中配置注册。或者在 `config.ts` 中手动配置: |
||||
|
|
||||
|
```js |
||||
|
import { type ModuleConfig } from "/@/cool"; |
||||
|
|
||||
|
export default (): ModuleConfig => { |
||||
|
return { |
||||
|
views: [ |
||||
|
{ |
||||
|
path: "/demo", |
||||
|
meta: { |
||||
|
label: "测试", |
||||
|
}, |
||||
|
component: () => import("./views/demo.vue"), |
||||
|
}, |
||||
|
], |
||||
|
pages: [ |
||||
|
{ |
||||
|
path: "/demo2", |
||||
|
meta: { |
||||
|
label: "测试", |
||||
|
}, |
||||
|
component: () => import("./pages/demo.vue"), |
||||
|
}, |
||||
|
], |
||||
|
}; |
||||
|
}; |
||||
|
``` |
||||
|
|
||||
|
2. 使页面参与路由缓存,配置 `name` 参数 |
||||
|
|
||||
|
:::warning |
||||
|
|
||||
|
`path` 与 `name` 的匹配规则: |
||||
|
|
||||
|
- /demo/t1 = demo-t1 |
||||
|
- /demo/t1-det = demo-t1-det |
||||
|
|
||||
|
::: |
||||
|
|
||||
|
方式 1: |
||||
|
|
||||
|
```html |
||||
|
<script lang="ts" setup> |
||||
|
defineOptions({ |
||||
|
name: "demo", |
||||
|
}); |
||||
|
</script> |
||||
|
``` |
||||
|
|
||||
|
方式 2: |
||||
|
|
||||
|
```html |
||||
|
<script lang="ts"> |
||||
|
export default defineComponent({ |
||||
|
name: "demo", |
||||
|
}); |
||||
|
</script> |
||||
|
``` |
||||
|
|
||||
|
## components |
||||
|
|
||||
|
目录下的组件,全局注册配置方法如下: |
||||
|
|
||||
|
```js |
||||
|
import { ModuleConfig } from "/@/cool"; |
||||
|
|
||||
|
export default (): ModuleConfig => { |
||||
|
return { |
||||
|
components: [ |
||||
|
import("./components/demo.vue"), |
||||
|
import("./components/demo1.vue"), |
||||
|
], |
||||
|
}; |
||||
|
}; |
||||
|
``` |
||||
|
|
||||
|
## directives |
||||
|
|
||||
|
`directives` 会以目录下的文件名分别注册指令 |
||||
|
|
||||
|
```ts |
||||
|
// demo/directives/test.ts |
||||
|
export default { |
||||
|
created(el, binding) {}, |
||||
|
mounted() {}, |
||||
|
... |
||||
|
}; |
||||
|
``` |
||||
|
|
||||
|
使用 |
||||
|
|
||||
|
```html |
||||
|
<div v-test></div> |
||||
|
``` |
||||
|
|
||||
|
## store |
||||
|
|
||||
|
使用 `pinia` 的推荐写法: |
||||
|
|
||||
|
```ts |
||||
|
import { defineStore } from "pinia"; |
||||
|
import { ref } from "vue"; |
||||
|
|
||||
|
export const useTestStore = defineStore("test", function () { |
||||
|
const count = ref(0); |
||||
|
|
||||
|
function add() { |
||||
|
count.value += 1; |
||||
|
} |
||||
|
|
||||
|
return { |
||||
|
count, |
||||
|
add, |
||||
|
}; |
||||
|
}); |
||||
|
``` |
||||
|
|
||||
|
使用 |
||||
|
|
||||
|
```ts |
||||
|
import { useTestStore } from "/$/demo/store"; |
||||
|
|
||||
|
const test = useTestStore(); |
||||
|
|
||||
|
test.add(); |
||||
|
|
||||
|
console.log(test.count); // 1 |
||||
|
``` |
||||
|
|
||||
|
::: tip |
||||
|
参考 `base` 模块下 `store` 的导出方式 |
||||
|
::: |
||||
|
|
||||
|
## config.ts |
||||
|
|
||||
|
模块的配置,程序运行时会读取该文件。 |
||||
|
|
||||
|
- 全局组件、路由的导入 |
||||
|
|
||||
|
- 事件钩子 |
||||
|
|
||||
|
输入 `module-config` 关键字,`vscode` 中会自动生成: |
||||
|
|
||||
|
```ts |
||||
|
import { ModuleConfig } from "/@/cool"; |
||||
|
import { Vue } from "vue"; |
||||
|
|
||||
|
export default (): ModuleConfig => { |
||||
|
return { |
||||
|
// 是否启用 |
||||
|
enable: true, |
||||
|
|
||||
|
// 插件名称 |
||||
|
label: "插件名称", |
||||
|
|
||||
|
// 插件描述 |
||||
|
description: "插件描述", |
||||
|
|
||||
|
// 作者 |
||||
|
author: "作者", |
||||
|
version: "1.0.0", |
||||
|
updateTime: "2024-02-02", |
||||
|
logo: "", |
||||
|
|
||||
|
// 忽略 |
||||
|
ignore: { |
||||
|
// 忽略进度条的请求 |
||||
|
NProgress: [ |
||||
|
"/base/open/eps", |
||||
|
"/base/comm/person", |
||||
|
"/base/comm/permmenu", |
||||
|
"/base/comm/upload", |
||||
|
"/base/comm/uploadMode", |
||||
|
], |
||||
|
|
||||
|
// 忽略 token 的路由 |
||||
|
token: ["/login", "/401", "/403", "/404", "/500", "/502"], |
||||
|
}, |
||||
|
|
||||
|
// 排序 |
||||
|
order: 0, |
||||
|
|
||||
|
// 配置参数 |
||||
|
options: { |
||||
|
name: "神仙", |
||||
|
}, |
||||
|
|
||||
|
// 示例页面 |
||||
|
demo: [ |
||||
|
{ |
||||
|
name: "基础用法", |
||||
|
component: () => import("..."), |
||||
|
}, |
||||
|
], |
||||
|
|
||||
|
// 注册全局组件 |
||||
|
components: [], |
||||
|
|
||||
|
// 视图路由 |
||||
|
views: [], |
||||
|
|
||||
|
// 页面路由 |
||||
|
pages: [], |
||||
|
|
||||
|
// 顶部工具栏 |
||||
|
toolbar: { |
||||
|
order: 1, |
||||
|
pc: true, // 是否在 pc 端显示 |
||||
|
h5: true, // 是否在 h5 端显示 |
||||
|
component: import("./components/index.vue"), |
||||
|
}, |
||||
|
|
||||
|
// 注入全局组件 |
||||
|
index: { |
||||
|
component: import("./components/index.vue"), |
||||
|
}, |
||||
|
|
||||
|
// 安装时触发 |
||||
|
install(app: Vue) {}, |
||||
|
|
||||
|
// 加载时触发 |
||||
|
onLoad(events) {}, |
||||
|
}; |
||||
|
}; |
||||
|
``` |
||||
|
|
||||
|
- order 模块加载顺序,值越大越先 |
||||
|
|
||||
|
- options 提供给外部使用的参数配置: |
||||
|
|
||||
|
```ts |
||||
|
import { ModuleConfig } from "/@/cool"; |
||||
|
|
||||
|
export default (): ModuleConfig => { |
||||
|
return { |
||||
|
options: { |
||||
|
// 尺寸 |
||||
|
size: 120, |
||||
|
// 显示文案 |
||||
|
text: "选择文件", |
||||
|
// 限制 |
||||
|
limit: { |
||||
|
// 上传最大数量 |
||||
|
upload: 9, |
||||
|
// 文件空间选择数 |
||||
|
select: 9, |
||||
|
// 上传大小限制 |
||||
|
size: 100, |
||||
|
}, |
||||
|
}, |
||||
|
}; |
||||
|
}; |
||||
|
``` |
||||
|
|
||||
|
获取方式: |
||||
|
|
||||
|
```ts |
||||
|
import { module } from "/@/cool"; |
||||
|
|
||||
|
const config = module.config("模块名"); |
||||
|
``` |
||||
|
|
||||
|
- components 提供全局的组件: |
||||
|
|
||||
|
```ts |
||||
|
import type { ModuleConfig } from "/@/cool"; |
||||
|
|
||||
|
export default (): ModuleConfig => { |
||||
|
return { |
||||
|
components: [import("./components/test.vue")], |
||||
|
}; |
||||
|
}; |
||||
|
``` |
||||
|
|
||||
|
批量导入可以使用 [import.meta.glob](mdc:https:/vitejs.dev/guide/features.html#glob-import) 方法: |
||||
|
|
||||
|
```ts |
||||
|
import { ModuleConfig } from "/@/cool"; |
||||
|
|
||||
|
export default (): ModuleConfig => { |
||||
|
return { |
||||
|
components: Object.values(import.meta.glob("./components/**/*")), |
||||
|
}; |
||||
|
}; |
||||
|
``` |
||||
|
|
||||
|
- views 全局注册的视图路由,存放在 `/` 中的子路由 `children`: |
||||
|
|
||||
|
```ts |
||||
|
import { ModuleConfig } from "/@/cool"; |
||||
|
|
||||
|
export default (): ModuleConfig => { |
||||
|
return { |
||||
|
views: [ |
||||
|
{ |
||||
|
path: "/test", |
||||
|
meta: { |
||||
|
label: "测试中心", |
||||
|
}, |
||||
|
component: () => import("./views/test.vue"), |
||||
|
}, |
||||
|
], |
||||
|
}; |
||||
|
}; |
||||
|
``` |
||||
|
|
||||
|
- pages 全局注册的页面路由: |
||||
|
|
||||
|
```ts |
||||
|
import { ModuleConfig } from "/@/cool"; |
||||
|
|
||||
|
export default (): ModuleConfig => { |
||||
|
return { |
||||
|
pages: [ |
||||
|
{ |
||||
|
path: "/test", |
||||
|
meta: { |
||||
|
label: "测试中心", |
||||
|
}, |
||||
|
component: () => import("./views/test.vue"), |
||||
|
}, |
||||
|
], |
||||
|
}; |
||||
|
}; |
||||
|
``` |
||||
|
|
||||
|
- install 模块安装时触发。用于预先处理: |
||||
|
|
||||
|
```ts |
||||
|
import { ModuleConfig } from "/@/cool"; |
||||
|
import { Vue } from "vue"; |
||||
|
|
||||
|
export default (): ModuleConfig => { |
||||
|
return { |
||||
|
install(app: Vue) { |
||||
|
// 注册组件 |
||||
|
app.component("test", Test); |
||||
|
|
||||
|
// 注册指令 |
||||
|
app.directive("focus", { |
||||
|
created(el, bind) {}, |
||||
|
}); |
||||
|
}, |
||||
|
}; |
||||
|
}; |
||||
|
``` |
||||
|
|
||||
|
- onLoad 模块安装时触发,预先加载数据,如菜单配置、用户信息: |
||||
|
|
||||
|
1. 使用 `await` 等待加载完成后往下执行 |
||||
|
|
||||
|
2. 可往下模块导出某个方法和变量,如 `hasToken` 验证是否有登陆 |
||||
|
|
||||
|
```ts |
||||
|
import { ModuleConfig } from "/@/cool"; |
||||
|
import { Vue } from "vue"; |
||||
|
|
||||
|
export default (): ModuleConfig => { |
||||
|
return { |
||||
|
async onLoad() { |
||||
|
const { user, menu } = useStore(); |
||||
|
|
||||
|
if (user.token) { |
||||
|
// 获取用户信息 |
||||
|
user.get(); |
||||
|
// 获取菜单权限 |
||||
|
await menu.get(); |
||||
|
} |
||||
|
|
||||
|
return { |
||||
|
async hasToken(cb: () => Promise<any> | void) { |
||||
|
if (user.token) { |
||||
|
if (cb) await cb(); |
||||
|
} |
||||
|
}, |
||||
|
}; |
||||
|
}, |
||||
|
}; |
||||
|
}; |
||||
|
``` |
||||
|
|
||||
|
其他模块中接收 `hasToken` 方法: |
||||
|
|
||||
|
```ts |
||||
|
import { ModuleConfig } from "/@/cool"; |
||||
|
import { useDict } from "./index"; |
||||
|
|
||||
|
export default (): ModuleConfig => { |
||||
|
return { |
||||
|
onLoad({ hasToken }) { |
||||
|
const { dict } = useDict(); |
||||
|
|
||||
|
hasToken(() => { |
||||
|
dict.refresh(); |
||||
|
}); |
||||
|
}, |
||||
|
}; |
||||
|
}; |
||||
|
``` |
||||
|
|
||||
|
## index.ts |
||||
|
|
||||
|
该模块需要对外开放的变量及方法,方便于别人直接使用: |
||||
|
|
||||
|
```ts |
||||
|
// modules/test/index.ts |
||||
|
import { useStore } from "./store"; |
||||
|
|
||||
|
export function useTest() { |
||||
|
return { |
||||
|
// 导出 pinia |
||||
|
...useStore(), |
||||
|
|
||||
|
// 自定义方法 |
||||
|
test() {}, |
||||
|
|
||||
|
// 自定义变量 |
||||
|
data: { |
||||
|
description: "数据描述", |
||||
|
}, |
||||
|
}; |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
导出命名规则 `useBase` `useDemo` `useDict` use + 模块名 |
||||
|
|
||||
|
使用: |
||||
|
|
||||
|
```ts |
||||
|
import { useTest } from "/$/test"; |
||||
|
|
||||
|
const { data, test } = useTest(); |
||||
|
``` |
||||
@ -0,0 +1,743 @@ |
|||||
|
--- |
||||
|
description: cl-search 组件示例 |
||||
|
globs: *.tsx, *.ts, *.vue |
||||
|
--- |
||||
|
## 起步 示例 |
||||
|
|
||||
|
```vue |
||||
|
<template> |
||||
|
<div class="scope"> |
||||
|
<div class="h"> |
||||
|
<el-tag size="small" effect="dark" disable-transitions>base</el-tag> |
||||
|
<span>起步</span> |
||||
|
</div> |
||||
|
|
||||
|
<div class="c"> |
||||
|
<el-button @click="open">预览</el-button> |
||||
|
<demo-code :files="['search/base.vue']" /> |
||||
|
|
||||
|
<!-- 自定义表格组件 --> |
||||
|
<cl-dialog v-model="visible" title="起步" width="80%"> |
||||
|
<cl-crud ref="Crud"> |
||||
|
<cl-row> |
||||
|
<!--【很重要】搜索组件 --> |
||||
|
<cl-search ref="Search" /> |
||||
|
</cl-row> |
||||
|
|
||||
|
<cl-row> |
||||
|
<cl-table ref="Table" /> |
||||
|
</cl-row> |
||||
|
|
||||
|
<cl-row> |
||||
|
<cl-flex1 /> |
||||
|
<cl-pagination /> |
||||
|
</cl-row> |
||||
|
</cl-crud> |
||||
|
</cl-dialog> |
||||
|
</div> |
||||
|
|
||||
|
<div class="f"> |
||||
|
<span class="date">2024-01-01</span> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script setup lang="ts"> |
||||
|
import { useCrud, useSearch, useTable } from '@cool-vue/crud'; |
||||
|
import { ref } from 'vue'; |
||||
|
import { useDict } from '/$/dict'; |
||||
|
|
||||
|
const { dict } = useDict(); |
||||
|
|
||||
|
// cl-crud 配置 |
||||
|
const Crud = useCrud( |
||||
|
{ |
||||
|
service: 'test' |
||||
|
}, |
||||
|
app => { |
||||
|
app.refresh(); |
||||
|
} |
||||
|
); |
||||
|
|
||||
|
// cl-table 配置 |
||||
|
const Table = useTable({ |
||||
|
autoHeight: false, |
||||
|
contextMenu: ['refresh'], |
||||
|
|
||||
|
columns: [ |
||||
|
{ |
||||
|
label: '姓名', |
||||
|
prop: 'name', |
||||
|
minWidth: 140 |
||||
|
}, |
||||
|
{ |
||||
|
label: '手机号', |
||||
|
prop: 'phone', |
||||
|
minWidth: 140 |
||||
|
}, |
||||
|
{ |
||||
|
label: '工作', |
||||
|
prop: 'occupation', |
||||
|
dict: dict.get('occupation'), |
||||
|
minWidth: 140 |
||||
|
}, |
||||
|
{ |
||||
|
label: '创建时间', |
||||
|
prop: 'createTime', |
||||
|
minWidth: 170, |
||||
|
sortable: 'desc' |
||||
|
} |
||||
|
] |
||||
|
}); |
||||
|
|
||||
|
// cl-search 配置 |
||||
|
//【很重要】该组件基于 cl-form 故很多示例都可复用 |
||||
|
const Search = useSearch({ |
||||
|
// 配置如 cl-form 一样 |
||||
|
items: [ |
||||
|
{ |
||||
|
label: '姓名', |
||||
|
prop: 'name', |
||||
|
component: { |
||||
|
name: 'el-input', |
||||
|
props: { |
||||
|
clearable: true, |
||||
|
|
||||
|
// 值改变的时候刷新列表 |
||||
|
onChange(val: string) { |
||||
|
refresh({ |
||||
|
name: val, |
||||
|
page: 1 |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
label: '手机号', |
||||
|
prop: 'phone', |
||||
|
component: { |
||||
|
name: 'el-input', |
||||
|
props: { |
||||
|
clearable: true |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
label: '工作', |
||||
|
prop: 'occupation', |
||||
|
component: { |
||||
|
name: 'cl-select', |
||||
|
props: { |
||||
|
tree: true, |
||||
|
checkStrictly: true, |
||||
|
options: dict.get('occupation') |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
], |
||||
|
|
||||
|
onChange(data, prop) { |
||||
|
console.log(data, prop); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
function refresh(params?: any) { |
||||
|
Crud.value?.refresh(params); |
||||
|
} |
||||
|
|
||||
|
const visible = ref(false); |
||||
|
|
||||
|
function open() { |
||||
|
visible.value = true; |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
``` |
||||
|
|
||||
|
## 折叠 示例 |
||||
|
|
||||
|
```vue |
||||
|
<template> |
||||
|
<div class="scope"> |
||||
|
<div class="h"> |
||||
|
<el-tag size="small" effect="dark" disable-transitions>collapse</el-tag> |
||||
|
<span>折叠</span> |
||||
|
</div> |
||||
|
|
||||
|
<div class="c"> |
||||
|
<el-button @click="open">预览</el-button> |
||||
|
<demo-code :files="['search/collapse.vue']" /> |
||||
|
|
||||
|
<!-- 折叠表格组件 --> |
||||
|
<cl-dialog v-model="visible" title="折叠" width="80%"> |
||||
|
<cl-crud ref="Crud"> |
||||
|
<!--【collapse】折叠参数,【inline】是否行内 --> |
||||
|
<cl-search ref="Search" reset-btn collapse :inline="false" /> |
||||
|
|
||||
|
<cl-row> |
||||
|
<cl-table ref="Table" /> |
||||
|
</cl-row> |
||||
|
|
||||
|
<cl-row> |
||||
|
<cl-flex1 /> |
||||
|
<cl-pagination /> |
||||
|
</cl-row> |
||||
|
</cl-crud> |
||||
|
</cl-dialog> |
||||
|
</div> |
||||
|
|
||||
|
<div class="f"> |
||||
|
<span class="date">2024-12-26</span> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script setup lang="ts"> |
||||
|
import { useCrud, useSearch, useTable } from '@cool-vue/crud'; |
||||
|
import { ref } from 'vue'; |
||||
|
import { useDict } from '/$/dict'; |
||||
|
import { range } from 'lodash-es'; |
||||
|
|
||||
|
const { dict } = useDict(); |
||||
|
|
||||
|
// cl-crud 配置 |
||||
|
const Crud = useCrud( |
||||
|
{ |
||||
|
service: 'test' |
||||
|
}, |
||||
|
app => { |
||||
|
app.refresh(); |
||||
|
} |
||||
|
); |
||||
|
|
||||
|
// cl-table 配置 |
||||
|
const Table = useTable({ |
||||
|
autoHeight: false, |
||||
|
contextMenu: ['refresh'], |
||||
|
|
||||
|
columns: [ |
||||
|
{ |
||||
|
label: '姓名', |
||||
|
prop: 'name', |
||||
|
minWidth: 140 |
||||
|
}, |
||||
|
{ |
||||
|
label: '手机号', |
||||
|
prop: 'phone', |
||||
|
minWidth: 140 |
||||
|
}, |
||||
|
{ |
||||
|
label: '工作', |
||||
|
prop: 'occupation', |
||||
|
dict: dict.get('occupation'), |
||||
|
minWidth: 140 |
||||
|
}, |
||||
|
{ |
||||
|
label: '创建时间', |
||||
|
prop: 'createTime', |
||||
|
minWidth: 170, |
||||
|
sortable: 'desc' |
||||
|
} |
||||
|
] |
||||
|
}); |
||||
|
|
||||
|
// cl-search 配置 |
||||
|
const Search = useSearch({ |
||||
|
items: [ |
||||
|
...range(20).map(i => { |
||||
|
return { |
||||
|
label: '输入框', |
||||
|
prop: `T${i + 1}`, |
||||
|
component: { |
||||
|
name: 'el-input' |
||||
|
} |
||||
|
}; |
||||
|
}) |
||||
|
] |
||||
|
}); |
||||
|
|
||||
|
function refresh(params?: any) { |
||||
|
Crud.value?.refresh(params); |
||||
|
} |
||||
|
|
||||
|
const visible = ref(false); |
||||
|
|
||||
|
function open() { |
||||
|
visible.value = true; |
||||
|
} |
||||
|
</script> |
||||
|
x |
||||
|
|
||||
|
``` |
||||
|
|
||||
|
## 自定义 示例 |
||||
|
|
||||
|
```vue |
||||
|
<template> |
||||
|
<div class="scope"> |
||||
|
<div class="h"> |
||||
|
<el-tag size="small" effect="dark" disable-transitions>custom</el-tag> |
||||
|
<span>自定义</span> |
||||
|
</div> |
||||
|
|
||||
|
<div class="c"> |
||||
|
<el-button @click="open">预览</el-button> |
||||
|
<demo-code :files="['search/custom.vue']" /> |
||||
|
|
||||
|
<!-- 自定义表格组件 --> |
||||
|
<cl-dialog v-model="visible" title="自定义" width="80%"> |
||||
|
<cl-crud ref="Crud"> |
||||
|
<cl-row> |
||||
|
<!--【很重要】搜索组件 --> |
||||
|
<cl-search |
||||
|
ref="Search" |
||||
|
:reset-btn="true" |
||||
|
:on-load="onLoad" |
||||
|
:on-search="onSearch" |
||||
|
> |
||||
|
<!-- 自定义按钮 --> |
||||
|
<template #buttons="scope"> |
||||
|
<el-button @click="toSearch(scope)">自定义按钮</el-button> |
||||
|
</template> |
||||
|
</cl-search> |
||||
|
</cl-row> |
||||
|
|
||||
|
<cl-row> |
||||
|
<cl-table ref="Table" /> |
||||
|
</cl-row> |
||||
|
|
||||
|
<cl-row> |
||||
|
<cl-flex1 /> |
||||
|
<cl-pagination /> |
||||
|
</cl-row> |
||||
|
</cl-crud> |
||||
|
</cl-dialog> |
||||
|
</div> |
||||
|
|
||||
|
<div class="f"> |
||||
|
<span class="date">2024-01-01</span> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script setup lang="ts"> |
||||
|
import { useCrud, useSearch, useTable } from '@cool-vue/crud'; |
||||
|
import { ref } from 'vue'; |
||||
|
import { useDict } from '/$/dict'; |
||||
|
import { ElMessage } from 'element-plus'; |
||||
|
|
||||
|
const { dict } = useDict(); |
||||
|
|
||||
|
// cl-crud 配置 |
||||
|
const Crud = useCrud( |
||||
|
{ |
||||
|
service: 'test' |
||||
|
}, |
||||
|
app => { |
||||
|
app.refresh(); |
||||
|
} |
||||
|
); |
||||
|
|
||||
|
// cl-table 配置 |
||||
|
const Table = useTable({ |
||||
|
autoHeight: false, |
||||
|
contextMenu: ['refresh'], |
||||
|
|
||||
|
columns: [ |
||||
|
{ |
||||
|
label: '姓名', |
||||
|
prop: 'name', |
||||
|
minWidth: 140 |
||||
|
}, |
||||
|
{ |
||||
|
label: '手机号', |
||||
|
prop: 'phone', |
||||
|
minWidth: 140 |
||||
|
}, |
||||
|
{ |
||||
|
label: '工作', |
||||
|
prop: 'occupation', |
||||
|
dict: dict.get('occupation'), |
||||
|
minWidth: 140 |
||||
|
}, |
||||
|
{ |
||||
|
label: '创建时间', |
||||
|
prop: 'createTime', |
||||
|
minWidth: 170, |
||||
|
sortable: 'desc' |
||||
|
} |
||||
|
] |
||||
|
}); |
||||
|
|
||||
|
// cl-search 配置 |
||||
|
//【很重要】该组件基于 cl-form 故很多示例都可复用 |
||||
|
const Search = useSearch({ |
||||
|
// 配置如 cl-form 一样 |
||||
|
items: [ |
||||
|
{ |
||||
|
label: '姓名', |
||||
|
prop: 'name', |
||||
|
component: { |
||||
|
name: 'el-input', |
||||
|
props: { |
||||
|
clearable: true, |
||||
|
|
||||
|
// 值改变的时候刷新列表 |
||||
|
onChange(val: string) { |
||||
|
refresh({ |
||||
|
name: val, |
||||
|
page: 1 |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
label: '手机号', |
||||
|
prop: 'phone', |
||||
|
component: { |
||||
|
name: 'el-input', |
||||
|
props: { |
||||
|
clearable: true |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
label: '工作', |
||||
|
prop: 'occupation', |
||||
|
component: { |
||||
|
name: 'cl-select', |
||||
|
props: { |
||||
|
tree: true, |
||||
|
checkStrictly: true, |
||||
|
options: dict.get('occupation') |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
] |
||||
|
}); |
||||
|
|
||||
|
function refresh(params?: any) { |
||||
|
Crud.value?.refresh(params); |
||||
|
} |
||||
|
|
||||
|
// cl-search 初始化 |
||||
|
function onLoad(data: any) { |
||||
|
data.name = '白小纯'; |
||||
|
} |
||||
|
|
||||
|
// cl-search 配置 onSearch 后,必须使用 next 方法继续请求 |
||||
|
function onSearch(data: any, { next }: { next: (data: any) => void }) { |
||||
|
ElMessage.info('开始搜索'); |
||||
|
// 这边可以处理其他事务 |
||||
|
next(data); |
||||
|
} |
||||
|
|
||||
|
// 自定义搜索,data 为表单数据 |
||||
|
function toSearch(data: any) { |
||||
|
ElMessage.info('自定义搜索'); |
||||
|
|
||||
|
refresh({ |
||||
|
page: 1, |
||||
|
...data |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
const visible = ref(false); |
||||
|
|
||||
|
function open() { |
||||
|
visible.value = true; |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
``` |
||||
|
|
||||
|
## 布局 示例 |
||||
|
|
||||
|
```vue |
||||
|
<template> |
||||
|
<div class="scope"> |
||||
|
<div class="h"> |
||||
|
<el-tag size="small" effect="dark" disable-transitions>layout</el-tag> |
||||
|
<span>布局</span> |
||||
|
</div> |
||||
|
|
||||
|
<div class="c"> |
||||
|
<el-button @click="open">预览</el-button> |
||||
|
<demo-code :files="['search/layout.vue']" /> |
||||
|
|
||||
|
<!-- 自定义表格组件 --> |
||||
|
<cl-dialog v-model="visible" title="布局" width="80%"> |
||||
|
<cl-crud ref="Crud"> |
||||
|
<!--【很重要】搜索组件 --> |
||||
|
<cl-search ref="Search" :reset-btn="true" /> |
||||
|
|
||||
|
<cl-row> |
||||
|
<cl-table ref="Table" /> |
||||
|
</cl-row> |
||||
|
|
||||
|
<cl-row> |
||||
|
<cl-flex1 /> |
||||
|
<cl-pagination /> |
||||
|
</cl-row> |
||||
|
</cl-crud> |
||||
|
</cl-dialog> |
||||
|
</div> |
||||
|
|
||||
|
<div class="f"> |
||||
|
<span class="date">2024-01-01</span> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script setup lang="ts"> |
||||
|
import { useCrud, useSearch, useTable } from '@cool-vue/crud'; |
||||
|
import { ref } from 'vue'; |
||||
|
import { useDict } from '/$/dict'; |
||||
|
|
||||
|
const { dict } = useDict(); |
||||
|
|
||||
|
// cl-crud 配置 |
||||
|
const Crud = useCrud( |
||||
|
{ |
||||
|
service: 'test' |
||||
|
}, |
||||
|
app => { |
||||
|
app.refresh(); |
||||
|
} |
||||
|
); |
||||
|
|
||||
|
// cl-table 配置 |
||||
|
const Table = useTable({ |
||||
|
autoHeight: false, |
||||
|
contextMenu: ['refresh'], |
||||
|
|
||||
|
columns: [ |
||||
|
{ |
||||
|
label: '姓名', |
||||
|
prop: 'name', |
||||
|
minWidth: 140 |
||||
|
}, |
||||
|
{ |
||||
|
label: '手机号', |
||||
|
prop: 'phone', |
||||
|
minWidth: 140 |
||||
|
}, |
||||
|
{ |
||||
|
label: '工作', |
||||
|
prop: 'occupation', |
||||
|
dict: dict.get('occupation'), |
||||
|
minWidth: 140 |
||||
|
}, |
||||
|
{ |
||||
|
label: '创建时间', |
||||
|
prop: 'createTime', |
||||
|
minWidth: 170, |
||||
|
sortable: 'desc' |
||||
|
} |
||||
|
] |
||||
|
}); |
||||
|
|
||||
|
// cl-search 配置 |
||||
|
//【很重要】该组件基于 cl-form 故很多示例都可复用 |
||||
|
const Search = useSearch({ |
||||
|
// 取消行内表单模式 |
||||
|
inline: false, |
||||
|
|
||||
|
// 表单参数 |
||||
|
props: { |
||||
|
labelPosition: 'top' |
||||
|
}, |
||||
|
|
||||
|
// 配置如 cl-form 一样 |
||||
|
items: [ |
||||
|
{ |
||||
|
label: '姓名', |
||||
|
prop: 'name', |
||||
|
component: { |
||||
|
name: 'el-input', |
||||
|
props: { |
||||
|
clearable: true, |
||||
|
|
||||
|
// 值改变的时候刷新列表 |
||||
|
onChange(val: string) { |
||||
|
refresh({ |
||||
|
name: val, |
||||
|
page: 1 |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
label: '手机号', |
||||
|
prop: 'phone', |
||||
|
component: { |
||||
|
name: 'el-input', |
||||
|
props: { |
||||
|
clearable: true |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
label: '工作', |
||||
|
prop: 'occupation', |
||||
|
component: { |
||||
|
name: 'cl-select', |
||||
|
props: { |
||||
|
tree: true, |
||||
|
checkStrictly: true, |
||||
|
options: dict.get('occupation') |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
] |
||||
|
}); |
||||
|
|
||||
|
function refresh(params?: any) { |
||||
|
Crud.value?.refresh(params); |
||||
|
} |
||||
|
|
||||
|
const visible = ref(false); |
||||
|
|
||||
|
function open() { |
||||
|
visible.value = true; |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
``` |
||||
|
|
||||
|
## 使用插件 示例 |
||||
|
|
||||
|
```vue |
||||
|
<template> |
||||
|
<div class="scope"> |
||||
|
<div class="h"> |
||||
|
<el-tag size="small" effect="dark" disable-transitions>plugin</el-tag> |
||||
|
<span>使用插件</span> |
||||
|
</div> |
||||
|
|
||||
|
<div class="c"> |
||||
|
<el-button @click="open">预览</el-button> |
||||
|
<demo-code :files="['search/layout.vue']" /> |
||||
|
|
||||
|
<!-- 自定义表格组件 --> |
||||
|
<cl-dialog v-model="visible" title="使用插件" width="80%"> |
||||
|
<cl-crud ref="Crud"> |
||||
|
<cl-row> |
||||
|
<cl-flex1 /> |
||||
|
<!--【很重要】搜索组件 --> |
||||
|
<cl-search ref="Search" /> |
||||
|
</cl-row> |
||||
|
|
||||
|
<cl-row> |
||||
|
<cl-table ref="Table" /> |
||||
|
</cl-row> |
||||
|
|
||||
|
<cl-row> |
||||
|
<cl-flex1 /> |
||||
|
<cl-pagination /> |
||||
|
</cl-row> |
||||
|
</cl-crud> |
||||
|
</cl-dialog> |
||||
|
</div> |
||||
|
|
||||
|
<div class="f"> |
||||
|
<span class="date">2024-01-01</span> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script setup lang="ts"> |
||||
|
import { useCrud, useSearch, useTable } from '@cool-vue/crud'; |
||||
|
import { ref } from 'vue'; |
||||
|
import { useDict } from '/$/dict'; |
||||
|
import { Plugins } from '/#/crud'; |
||||
|
|
||||
|
const { dict } = useDict(); |
||||
|
|
||||
|
// cl-crud 配置 |
||||
|
const Crud = useCrud( |
||||
|
{ |
||||
|
service: 'test' |
||||
|
}, |
||||
|
app => { |
||||
|
app.refresh(); |
||||
|
} |
||||
|
); |
||||
|
|
||||
|
// cl-table 配置 |
||||
|
const Table = useTable({ |
||||
|
autoHeight: false, |
||||
|
contextMenu: ['refresh'], |
||||
|
|
||||
|
columns: [ |
||||
|
{ |
||||
|
label: '姓名', |
||||
|
prop: 'name', |
||||
|
minWidth: 140 |
||||
|
}, |
||||
|
{ |
||||
|
label: '手机号', |
||||
|
prop: 'phone', |
||||
|
minWidth: 140 |
||||
|
}, |
||||
|
{ |
||||
|
label: '工作', |
||||
|
prop: 'occupation', |
||||
|
dict: dict.get('occupation'), |
||||
|
minWidth: 140 |
||||
|
}, |
||||
|
{ |
||||
|
label: '创建时间', |
||||
|
prop: 'createTime', |
||||
|
minWidth: 170, |
||||
|
sortable: 'desc' |
||||
|
} |
||||
|
] |
||||
|
}); |
||||
|
|
||||
|
// cl-search 配置 |
||||
|
const Search = useSearch({ |
||||
|
// 【很重要】自动读取 service 下的 search 数据 |
||||
|
plugins: [ |
||||
|
Plugins.Search.setAuto({ |
||||
|
customComponent(field) { |
||||
|
if (field.propertyName == 'name') { |
||||
|
return { |
||||
|
name: 'cl-select', |
||||
|
props: { |
||||
|
options: [ |
||||
|
{ |
||||
|
label: '张三', |
||||
|
value: '1' |
||||
|
}, |
||||
|
{ |
||||
|
label: '李四', |
||||
|
value: '2' |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
// null 则不操作,按系统默认操作 |
||||
|
return null; |
||||
|
} |
||||
|
}) |
||||
|
] |
||||
|
}); |
||||
|
|
||||
|
function refresh(params?: any) { |
||||
|
Crud.value?.refresh(params); |
||||
|
} |
||||
|
|
||||
|
const visible = ref(false); |
||||
|
|
||||
|
function open() { |
||||
|
visible.value = true; |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
``` |
||||
File diff suppressed because it is too large
@ -0,0 +1,716 @@ |
|||||
|
--- |
||||
|
description: cl-upsert 组件示例 |
||||
|
globs: *.tsx, *.ts, *.vue |
||||
|
--- |
||||
|
## 起步 示例 |
||||
|
|
||||
|
```vue |
||||
|
<template> |
||||
|
<div class="scope"> |
||||
|
<div class="h"> |
||||
|
<el-tag size="small" effect="dark" disable-transitions>base</el-tag> |
||||
|
<span>起步</span> |
||||
|
</div> |
||||
|
|
||||
|
<div class="c"> |
||||
|
<el-button @click="open">预览</el-button> |
||||
|
<demo-code :files="['upsert/base.vue']" /> |
||||
|
|
||||
|
<!-- 自定义表格组件 --> |
||||
|
<cl-dialog v-model="visible" title="起步" width="80%"> |
||||
|
<cl-crud ref="Crud"> |
||||
|
<cl-row> |
||||
|
<!-- 打开新增表单的按钮 --> |
||||
|
<cl-add-btn /> |
||||
|
</cl-row> |
||||
|
|
||||
|
<cl-row> |
||||
|
<cl-table ref="Table" /> |
||||
|
</cl-row> |
||||
|
|
||||
|
<cl-row> |
||||
|
<cl-flex1 /> |
||||
|
<cl-pagination /> |
||||
|
</cl-row> |
||||
|
|
||||
|
<!--【很重要】新增、编辑的表单组件 --> |
||||
|
<cl-upsert ref="Upsert" /> |
||||
|
</cl-crud> |
||||
|
</cl-dialog> |
||||
|
</div> |
||||
|
|
||||
|
<div class="f"> |
||||
|
<span class="date">2024-01-01</span> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script setup lang="ts"> |
||||
|
import { useCrud, useTable, useUpsert } from '@cool-vue/crud'; |
||||
|
import { ref } from 'vue'; |
||||
|
import { useDict } from '/$/dict'; |
||||
|
|
||||
|
const { dict } = useDict(); |
||||
|
|
||||
|
// cl-crud 配置 |
||||
|
const Crud = useCrud( |
||||
|
{ |
||||
|
service: 'test' |
||||
|
}, |
||||
|
app => { |
||||
|
app.refresh(); |
||||
|
} |
||||
|
); |
||||
|
|
||||
|
// cl-table 配置 |
||||
|
const Table = useTable({ |
||||
|
autoHeight: false, |
||||
|
contextMenu: ['refresh'], |
||||
|
|
||||
|
columns: [ |
||||
|
{ |
||||
|
label: '姓名', |
||||
|
prop: 'name', |
||||
|
minWidth: 140 |
||||
|
}, |
||||
|
{ |
||||
|
label: '手机号', |
||||
|
prop: 'phone', |
||||
|
minWidth: 140 |
||||
|
}, |
||||
|
{ |
||||
|
label: '工作', |
||||
|
prop: 'occupation', |
||||
|
dict: dict.get('occupation'), |
||||
|
minWidth: 140 |
||||
|
}, |
||||
|
{ |
||||
|
label: '创建时间', |
||||
|
prop: 'createTime', |
||||
|
minWidth: 170, |
||||
|
sortable: 'desc' |
||||
|
}, |
||||
|
{ |
||||
|
type: 'op', |
||||
|
// edit 打开编辑表单 |
||||
|
buttons: ['edit', 'delete'] |
||||
|
} |
||||
|
] |
||||
|
}); |
||||
|
|
||||
|
// cl-upsert 配置 |
||||
|
//【很重要】该组件基于 cl-form 故很多示例都可复用 |
||||
|
const Upsert = useUpsert({ |
||||
|
// 配置如 cl-form 一样 |
||||
|
items: [ |
||||
|
{ |
||||
|
label: '姓名', |
||||
|
prop: 'name', |
||||
|
component: { |
||||
|
name: 'el-input' |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
label: '手机号', |
||||
|
prop: 'phone', |
||||
|
component: { |
||||
|
name: 'el-input' |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
label: '工作', |
||||
|
prop: 'occupation', |
||||
|
component: { |
||||
|
name: 'cl-select', |
||||
|
props: { |
||||
|
tree: true, |
||||
|
checkStrictly: true, |
||||
|
options: dict.get('occupation') |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
] |
||||
|
}); |
||||
|
|
||||
|
const visible = ref(false); |
||||
|
|
||||
|
function open() { |
||||
|
visible.value = true; |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
``` |
||||
|
|
||||
|
## 打开、关闭、提交等事件 示例 |
||||
|
|
||||
|
```vue |
||||
|
<template> |
||||
|
<div class="scope"> |
||||
|
<div class="h"> |
||||
|
<el-tag size="small" effect="dark" disable-transitions>event</el-tag> |
||||
|
<span>打开、关闭、提交等事件</span> |
||||
|
</div> |
||||
|
|
||||
|
<div class="c"> |
||||
|
<el-button @click="open">预览</el-button> |
||||
|
<demo-code :files="['upsert/event.vue']" /> |
||||
|
|
||||
|
<!-- 自定义表格组件 --> |
||||
|
<cl-dialog v-model="visible" title="事件" width="80%"> |
||||
|
<cl-crud ref="Crud"> |
||||
|
<cl-row> |
||||
|
<!-- 打开新增表单的按钮 --> |
||||
|
<cl-add-btn /> |
||||
|
</cl-row> |
||||
|
|
||||
|
<cl-row> |
||||
|
<cl-table ref="Table" /> |
||||
|
</cl-row> |
||||
|
|
||||
|
<cl-row> |
||||
|
<cl-flex1 /> |
||||
|
<cl-pagination /> |
||||
|
</cl-row> |
||||
|
|
||||
|
<!--【很重要】新增、编辑的表单组件 --> |
||||
|
<cl-upsert ref="Upsert" /> |
||||
|
</cl-crud> |
||||
|
</cl-dialog> |
||||
|
</div> |
||||
|
|
||||
|
<div class="f"> |
||||
|
<span class="date">2024-01-01</span> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script setup lang="ts"> |
||||
|
import { useCrud, useTable, useUpsert } from '@cool-vue/crud'; |
||||
|
import { ref } from 'vue'; |
||||
|
import { useDict } from '/$/dict'; |
||||
|
import { useCool } from '/@/cool'; |
||||
|
|
||||
|
const { service } = useCool(); |
||||
|
const { dict } = useDict(); |
||||
|
|
||||
|
// cl-crud 配置 |
||||
|
const Crud = useCrud( |
||||
|
{ |
||||
|
service: 'test' |
||||
|
}, |
||||
|
app => { |
||||
|
app.refresh(); |
||||
|
} |
||||
|
); |
||||
|
|
||||
|
// cl-table 配置 |
||||
|
const Table = useTable({ |
||||
|
autoHeight: false, |
||||
|
contextMenu: ['refresh'], |
||||
|
|
||||
|
columns: [ |
||||
|
{ |
||||
|
label: '姓名', |
||||
|
prop: 'name', |
||||
|
minWidth: 140 |
||||
|
}, |
||||
|
{ |
||||
|
label: '手机号', |
||||
|
prop: 'phone', |
||||
|
minWidth: 140 |
||||
|
}, |
||||
|
{ |
||||
|
label: '工作', |
||||
|
prop: 'occupation', |
||||
|
dict: dict.get('occupation'), |
||||
|
minWidth: 140 |
||||
|
}, |
||||
|
{ |
||||
|
label: '创建时间', |
||||
|
prop: 'createTime', |
||||
|
minWidth: 170, |
||||
|
sortable: 'desc' |
||||
|
}, |
||||
|
{ |
||||
|
type: 'op', |
||||
|
// edit 打开编辑表单 |
||||
|
buttons: ['edit', 'delete'] |
||||
|
} |
||||
|
] |
||||
|
}); |
||||
|
|
||||
|
// cl-upsert 配置 |
||||
|
const Upsert = useUpsert({ |
||||
|
items: [ |
||||
|
{ |
||||
|
label: '姓名', |
||||
|
prop: 'name', |
||||
|
component: { |
||||
|
name: 'el-input' |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
label: '手机号', |
||||
|
prop: 'phone', |
||||
|
component: { |
||||
|
name: 'el-input' |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
label: '工作', |
||||
|
prop: 'occupation', |
||||
|
component: { |
||||
|
name: 'cl-select', |
||||
|
props: { |
||||
|
tree: true, |
||||
|
checkStrictly: true, |
||||
|
options: dict.get('occupation') |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
], |
||||
|
|
||||
|
// 以下事件按顺序触发 |
||||
|
|
||||
|
// 弹窗打开的事件,这个时候还未有表单数据 |
||||
|
onOpen() { |
||||
|
console.log('onOpen'); |
||||
|
}, |
||||
|
|
||||
|
// 获取详情,编辑的时候会触发 |
||||
|
async onInfo(data, { next, done }) { |
||||
|
// 不配置 onInfo 的时候默认执行 next(data),调用 service 的 info 接口获取详情 |
||||
|
// next(data); |
||||
|
|
||||
|
// 自定义,需要对请求数据进行处理或者返回处理后的数据 |
||||
|
const res = await next({ |
||||
|
id: data.id |
||||
|
}); |
||||
|
|
||||
|
done({ |
||||
|
...res, |
||||
|
name: `[${res.name}]` |
||||
|
}); |
||||
|
}, |
||||
|
|
||||
|
// 弹窗打开后,已经得到了表单数据 |
||||
|
onOpened(data) { |
||||
|
// 判定是否编辑模式 |
||||
|
if (Upsert.value?.mode == 'update') { |
||||
|
// 对数据处理 |
||||
|
data.phone += '000'; |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
// 提交事件的钩子 |
||||
|
// data 表单提交数据 |
||||
|
// next 继续往下执行 |
||||
|
// done 关闭加载 |
||||
|
// close 关闭弹窗 |
||||
|
async onSubmit(data, { next, done, close }) { |
||||
|
// 不配置 onSubmit 的时候默认执行 next(data),提交后会去请求 service 的 update/add 接口 |
||||
|
// next(data); |
||||
|
|
||||
|
// 自定义如下 |
||||
|
// 场景1:提交时对参数额外的处理 |
||||
|
// next({ |
||||
|
// ...data, |
||||
|
// status: 1, |
||||
|
// createTime: dayjs().format("YYYY-MM-DD") |
||||
|
// }); |
||||
|
|
||||
|
// 场景2:提交前、后的操作 |
||||
|
// 之前,模拟获取 userId |
||||
|
const userId = await service.base.sys.user.info({ id: 1 }); |
||||
|
|
||||
|
// 返回值 |
||||
|
const res = await next({ |
||||
|
userId, |
||||
|
data |
||||
|
}); |
||||
|
|
||||
|
// 之后 |
||||
|
// console.log(res); |
||||
|
}, |
||||
|
|
||||
|
// 关闭时触发 |
||||
|
onClose(action, done) { |
||||
|
// action 关闭的类型 |
||||
|
console.log('action,', action); |
||||
|
|
||||
|
// 使用 done 关闭窗口 |
||||
|
done(); |
||||
|
}, |
||||
|
|
||||
|
// 关闭后触发 |
||||
|
onClosed() { |
||||
|
console.log('onClosed'); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
const visible = ref(false); |
||||
|
|
||||
|
function open() { |
||||
|
visible.value = true; |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
``` |
||||
|
|
||||
|
## Hook的使用 示例 |
||||
|
|
||||
|
```vue |
||||
|
<template> |
||||
|
<div class="scope"> |
||||
|
<div class="h"> |
||||
|
<el-tag size="small" effect="dark" disable-transitions>hook</el-tag> |
||||
|
<span>Hook的使用</span> |
||||
|
</div> |
||||
|
|
||||
|
<div class="c"> |
||||
|
<el-button @click="open">预览</el-button> |
||||
|
<demo-code :files="['upsert/hook/index.vue', 'upsert/hook/reg-pca2.ts']" /> |
||||
|
|
||||
|
<!-- 自定义表格组件 --> |
||||
|
<cl-dialog v-model="visible" title="Hook的使用" width="80%"> |
||||
|
<cl-crud ref="Crud"> |
||||
|
<cl-row> |
||||
|
<!-- 打开新增表单的按钮 --> |
||||
|
<cl-add-btn /> |
||||
|
</cl-row> |
||||
|
|
||||
|
<cl-row> |
||||
|
<cl-table ref="Table" /> |
||||
|
</cl-row> |
||||
|
|
||||
|
<cl-row> |
||||
|
<cl-flex1 /> |
||||
|
<cl-pagination /> |
||||
|
</cl-row> |
||||
|
|
||||
|
<!--【很重要】新增、编辑的表单组件 --> |
||||
|
<cl-upsert ref="Upsert" /> |
||||
|
</cl-crud> |
||||
|
</cl-dialog> |
||||
|
</div> |
||||
|
|
||||
|
<div class="f"> |
||||
|
<span class="date">2024-01-01</span> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script setup lang="ts"> |
||||
|
import { useCrud, useTable, useUpsert } from '@cool-vue/crud'; |
||||
|
import { ref } from 'vue'; |
||||
|
import { useDict } from '/$/dict'; |
||||
|
|
||||
|
const { dict } = useDict(); |
||||
|
|
||||
|
// cl-crud 配置 |
||||
|
const Crud = useCrud( |
||||
|
{ |
||||
|
service: 'test' |
||||
|
}, |
||||
|
app => { |
||||
|
app.refresh(); |
||||
|
} |
||||
|
); |
||||
|
|
||||
|
// cl-table 配置 |
||||
|
const Table = useTable({ |
||||
|
autoHeight: false, |
||||
|
contextMenu: ['refresh'], |
||||
|
|
||||
|
columns: [ |
||||
|
{ |
||||
|
label: '姓名', |
||||
|
prop: 'name', |
||||
|
minWidth: 140 |
||||
|
}, |
||||
|
{ |
||||
|
label: '手机号', |
||||
|
prop: 'phone', |
||||
|
minWidth: 140 |
||||
|
}, |
||||
|
{ |
||||
|
label: '省市区', |
||||
|
prop: 'pca', |
||||
|
formatter(row) { |
||||
|
return row.province ? row.province + '-' + row.city + '-' + row.district : '-'; |
||||
|
}, |
||||
|
minWidth: 140 |
||||
|
}, |
||||
|
{ |
||||
|
label: '工作', |
||||
|
prop: 'occupation', |
||||
|
dict: dict.get('occupation'), |
||||
|
minWidth: 140 |
||||
|
}, |
||||
|
{ |
||||
|
label: '创建时间', |
||||
|
prop: 'createTime', |
||||
|
minWidth: 170, |
||||
|
sortable: 'desc' |
||||
|
}, |
||||
|
{ |
||||
|
type: 'op', |
||||
|
buttons: ['edit', 'delete'] |
||||
|
} |
||||
|
] |
||||
|
}); |
||||
|
|
||||
|
// cl-upsert 配置 |
||||
|
const Upsert = useUpsert({ |
||||
|
items: [ |
||||
|
{ |
||||
|
label: '姓名', |
||||
|
prop: 'name', |
||||
|
component: { |
||||
|
name: 'el-input' |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
label: '手机号', |
||||
|
prop: 'phone', |
||||
|
component: { |
||||
|
name: 'el-input' |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
label: '省市区', |
||||
|
prop: 'pca2', |
||||
|
|
||||
|
//【很重要】hook 参数配置 |
||||
|
hook: { |
||||
|
bind(value, { form }) { |
||||
|
// 将3个参数合并成一个数组,带入级联选择器 |
||||
|
return [form.province, form.city, form.district]; |
||||
|
}, |
||||
|
submit(value, { form, prop }) { |
||||
|
// 提交的时候将数组拆分成3个字段提交 |
||||
|
const [province, city, district] = value || []; |
||||
|
form.province = province; |
||||
|
form.city = city; |
||||
|
form.district = district; |
||||
|
|
||||
|
// 删除 prop 绑定值 |
||||
|
form[prop] = undefined; |
||||
|
} |
||||
|
}, |
||||
|
// 注册到全局后可直接使用,注册代码看 ./reg-pca2.ts |
||||
|
// hook: "pca2", |
||||
|
|
||||
|
component: { |
||||
|
name: 'cl-distpicker' |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
label: '标签', |
||||
|
prop: 'labels', |
||||
|
//【很重要】使用内置方法,避免一些辣鸡后端要你这么传给他 |
||||
|
hook: { |
||||
|
// labels 的数据为 1,2,3 |
||||
|
|
||||
|
// 绑定的时候将 labels 按 , 分割成数组 |
||||
|
bind: ['split', 'number'], |
||||
|
|
||||
|
// 提交的时候将 labels 拼接成字符串 |
||||
|
submit: ['join'] |
||||
|
}, |
||||
|
component: { |
||||
|
name: 'el-select', |
||||
|
props: { |
||||
|
multiple: true |
||||
|
}, |
||||
|
options: [ |
||||
|
{ |
||||
|
label: '帅气', |
||||
|
value: 1 |
||||
|
}, |
||||
|
{ |
||||
|
label: '多金', |
||||
|
value: 2 |
||||
|
}, |
||||
|
{ |
||||
|
label: '有才华', |
||||
|
value: 3 |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
label: '工作', |
||||
|
prop: 'occupation', |
||||
|
component: { |
||||
|
name: 'cl-select', |
||||
|
props: { |
||||
|
tree: true, |
||||
|
checkStrictly: true, |
||||
|
options: dict.get('occupation') |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
] |
||||
|
}); |
||||
|
|
||||
|
const visible = ref(false); |
||||
|
|
||||
|
function open() { |
||||
|
visible.value = true; |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
``` |
||||
|
|
||||
|
## 新增、编辑、详情模式 示例 |
||||
|
|
||||
|
```vue |
||||
|
<template> |
||||
|
<div class="scope"> |
||||
|
<div class="h"> |
||||
|
<el-tag size="small" effect="dark" disable-transitions>mode</el-tag> |
||||
|
<span>新增、编辑、详情模式</span> |
||||
|
</div> |
||||
|
|
||||
|
<div class="c"> |
||||
|
<el-button @click="open">预览</el-button> |
||||
|
<demo-code :files="['upsert/mode.vue']" /> |
||||
|
|
||||
|
<!-- 自定义表格组件 --> |
||||
|
<cl-dialog v-model="visible" title="不同模式" width="80%"> |
||||
|
<cl-crud ref="Crud"> |
||||
|
<cl-row> |
||||
|
<!-- 打开新增表单的按钮 --> |
||||
|
<cl-add-btn /> |
||||
|
</cl-row> |
||||
|
|
||||
|
<cl-row> |
||||
|
<cl-table ref="Table" /> |
||||
|
</cl-row> |
||||
|
|
||||
|
<cl-row> |
||||
|
<cl-flex1 /> |
||||
|
<cl-pagination /> |
||||
|
</cl-row> |
||||
|
|
||||
|
<!--【很重要】新增、编辑的表单组件 --> |
||||
|
<cl-upsert ref="Upsert" /> |
||||
|
</cl-crud> |
||||
|
</cl-dialog> |
||||
|
</div> |
||||
|
|
||||
|
<div class="f"> |
||||
|
<span class="date">2024-01-01</span> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script setup lang="ts"> |
||||
|
import { useCrud, useTable, useUpsert } from '@cool-vue/crud'; |
||||
|
import { ref } from 'vue'; |
||||
|
import { useDict } from '/$/dict'; |
||||
|
import { ElMessage } from 'element-plus'; |
||||
|
|
||||
|
const { dict } = useDict(); |
||||
|
|
||||
|
// cl-crud 配置 |
||||
|
const Crud = useCrud( |
||||
|
{ |
||||
|
service: 'test' |
||||
|
}, |
||||
|
app => { |
||||
|
app.refresh(); |
||||
|
} |
||||
|
); |
||||
|
|
||||
|
// cl-table 配置 |
||||
|
const Table = useTable({ |
||||
|
autoHeight: false, |
||||
|
contextMenu: ['refresh'], |
||||
|
|
||||
|
columns: [ |
||||
|
{ |
||||
|
label: '姓名', |
||||
|
prop: 'name', |
||||
|
minWidth: 140 |
||||
|
}, |
||||
|
{ |
||||
|
label: '手机号', |
||||
|
prop: 'phone', |
||||
|
minWidth: 140 |
||||
|
}, |
||||
|
{ |
||||
|
label: '工作', |
||||
|
prop: 'occupation', |
||||
|
dict: dict.get('occupation'), |
||||
|
minWidth: 140 |
||||
|
}, |
||||
|
{ |
||||
|
label: '创建时间', |
||||
|
prop: 'createTime', |
||||
|
minWidth: 170, |
||||
|
sortable: 'desc' |
||||
|
}, |
||||
|
{ |
||||
|
type: 'op', |
||||
|
width: 240, |
||||
|
buttons: ['info', 'edit', 'delete'] |
||||
|
} |
||||
|
] |
||||
|
}); |
||||
|
|
||||
|
// cl-upsert 配置 |
||||
|
const Upsert = useUpsert({ |
||||
|
items: [ |
||||
|
{ |
||||
|
label: '姓名', |
||||
|
prop: 'name', |
||||
|
component: { |
||||
|
name: 'el-input' |
||||
|
} |
||||
|
}, |
||||
|
//【很重要】只有返回方法的时候才能使用 Upsert |
||||
|
() => { |
||||
|
return { |
||||
|
label: '手机号', |
||||
|
prop: 'phone', |
||||
|
|
||||
|
// 新增的时候隐藏 |
||||
|
// hidden: Upsert.value?.mode == "add", |
||||
|
|
||||
|
component: { |
||||
|
name: 'el-input', |
||||
|
props: { |
||||
|
// 编辑的时候禁用 |
||||
|
disabled: Upsert.value?.mode == 'update' |
||||
|
} |
||||
|
} |
||||
|
}; |
||||
|
}, |
||||
|
{ |
||||
|
label: '工作', |
||||
|
prop: 'occupation', |
||||
|
component: { |
||||
|
name: 'cl-select', |
||||
|
props: { |
||||
|
tree: true, |
||||
|
checkStrictly: true, |
||||
|
options: dict.get('occupation') |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
], |
||||
|
onOpen() { |
||||
|
ElMessage.info(`当前模式:` + Upsert.value?.mode); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
const visible = ref(false); |
||||
|
|
||||
|
function open() { |
||||
|
visible.value = true; |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
``` |
||||
@ -0,0 +1,74 @@ |
|||||
|
# 项目背景 |
||||
|
|
||||
|
- 库:typescript、javaScript、scss、vue、tailwind |
||||
|
- 框架:cool-admin-vue |
||||
|
- 项目版本:8.x |
||||
|
|
||||
|
# 项目目录 |
||||
|
|
||||
|
├── .vscode(代码片段,根据关键字可以快速地生成代码) |
||||
|
├── public(静态资源文件) |
||||
|
├── packages(源码包:@cool-vue/crud、@cool-vue/vite-plugin) |
||||
|
├── build |
||||
|
│ └── cool() |
||||
|
│ │ └── eps.json(Eps 配置文件) |
||||
|
│ │ └── eps.d.ts(Eps 描述文件) |
||||
|
├── src |
||||
|
│ └── cool(核心文件) |
||||
|
│ └── modules(项目模块) |
||||
|
│ │ └── base(基础模块) |
||||
|
│ │ └── demo(示例模块) |
||||
|
│ │ └── dict(字典模块) |
||||
|
│ │ └── helper(辅助模块) |
||||
|
│ │ └── recycle(回收站模块) |
||||
|
│ │ └── space(cl-upload-space 文件空间模块) |
||||
|
│ │ └── task(任务模块) |
||||
|
│ │ └── user(用户模块) |
||||
|
│ └── plugins(项目插件) |
||||
|
│ │ └── crud(cl-crud、@cool-vue/crud) |
||||
|
│ │ └── distpicker(cl-distpicker、省市区选择器) |
||||
|
│ │ └── echarts(图标) |
||||
|
│ │ └── editor-preview(编辑器预览组件) |
||||
|
│ │ └── editor-wange(wang富文本编辑器) |
||||
|
│ │ └── element-ui(element-plus 组件) |
||||
|
│ │ └── excel(excel导入、导出组件) |
||||
|
│ │ └── i18n(多语言) |
||||
|
│ │ └── iconfont(iconfont 图标) |
||||
|
│ │ └── theme(cl-theme 主题组件) |
||||
|
│ │ └── upload(cl-upload 文件上传组件) |
||||
|
│ │ └── view(cl-view-group、cl-view-head 视图组件) |
||||
|
│ └── config |
||||
|
│ │ └── index.ts(默认配置) |
||||
|
│ │ └── dev.ts(开发环境) |
||||
|
│ │ └── prod.ts(生产环境) |
||||
|
│ │ └── proxy.ts(代理配置) |
||||
|
│ └── App.vue(入口文件) |
||||
|
│ └── main.ts(入口文件) |
||||
|
├── package.json(依赖管理,项目信息) |
||||
|
└── ... |
||||
|
|
||||
|
模块、插件目录 |
||||
|
├── modules/plugins |
||||
|
│ └── base(模块名) |
||||
|
│ │ └── components(全局组件) |
||||
|
│ │ └── directives(全局指令) |
||||
|
│ │ └── locales(国际化) |
||||
|
│ │ └── router(路由) |
||||
|
│ │ └── store(状态管理) |
||||
|
│ │ └── utils(工具函数) |
||||
|
│ │ └── views(视图) |
||||
|
│ │ └── config.ts(必须,模块的配置) |
||||
|
│ │ └── index.ts(模块导出) |
||||
|
|
||||
|
# 其它 |
||||
|
|
||||
|
- 文件、组件命名用 - 连接,如:student-info.vue |
||||
|
- service 的描述类型,查看 build/cool/eps.d.ts 描述文件 |
||||
|
- 创建模块、插件代码需要读取.cursor/rules的module.mdc,其它的rules根据需要进行参考 |
||||
|
|
||||
|
# import 引用别名 |
||||
|
|
||||
|
- "/@" 对应 "./src" |
||||
|
- "/$" 对应 "./src/modules" |
||||
|
- "/#" 对应 "./src/plugins" |
||||
|
- "/~" 对应 "./packages" |
||||
@ -0,0 +1,5 @@ |
|||||
|
node_modules |
||||
|
.DS_Store |
||||
|
dist |
||||
|
dist-ssr |
||||
|
*.local |
||||
@ -0,0 +1,7 @@ |
|||||
|
[*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue}] |
||||
|
charset = utf-8 |
||||
|
end_of_line = lf |
||||
|
indent_style = tab |
||||
|
indent_size = 4 |
||||
|
trim_trailing_whitespace = true |
||||
|
insert_final_newline = true |
||||
@ -0,0 +1,5 @@ |
|||||
|
# 应用名称 |
||||
|
VITE_NAME = "COOL-ADMIN" |
||||
|
|
||||
|
# 网络超时请求时间 |
||||
|
VITE_TIMEOUT = 30000 |
||||
@ -0,0 +1,4 @@ |
|||||
|
*.js text eol=lf |
||||
|
*.json text eol=lf |
||||
|
*.ts text eol=lf |
||||
|
*.vue text eol=lf |
||||
@ -0,0 +1,20 @@ |
|||||
|
.DS_Store |
||||
|
node_modules/ |
||||
|
/dist/ |
||||
|
dist-ssr/ |
||||
|
|
||||
|
# Log files |
||||
|
npm-debug.log* |
||||
|
yarn-debug.log* |
||||
|
yarn-error.log* |
||||
|
|
||||
|
# Editor directories and files |
||||
|
.project |
||||
|
.idea |
||||
|
*.suo |
||||
|
*.ntvs* |
||||
|
*.njsproj |
||||
|
*.sln |
||||
|
*.sw* |
||||
|
|
||||
|
vite.config.ts.timestamp* |
||||
@ -0,0 +1,16 @@ |
|||||
|
{ |
||||
|
"extends": [ |
||||
|
"development" |
||||
|
], |
||||
|
"hints": { |
||||
|
"meta-viewport": "off", |
||||
|
"axe/text-alternatives": [ |
||||
|
"default", |
||||
|
{ |
||||
|
"document-title": "off" |
||||
|
} |
||||
|
], |
||||
|
"disown-opener": "off", |
||||
|
"css-prefix-order": "off" |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,10 @@ |
|||||
|
{ |
||||
|
"$schema": "https://json.schemastore.org/prettierrc", |
||||
|
"semi": true, |
||||
|
"useTabs": true, |
||||
|
"tabWidth": 4, |
||||
|
"printWidth": 100, |
||||
|
"singleQuote": true, |
||||
|
"arrowParens": "avoid", |
||||
|
"trailingComma": "none" |
||||
|
} |
||||
@ -0,0 +1,15 @@ |
|||||
|
{ |
||||
|
"module-config": { |
||||
|
"prefix": "module-config", |
||||
|
"scope": "typescript", |
||||
|
"body": [ |
||||
|
"import { type ModuleConfig } from \"/@/cool\";", |
||||
|
"", |
||||
|
"export default (): ModuleConfig => {", |
||||
|
" return {};", |
||||
|
"};", |
||||
|
"" |
||||
|
], |
||||
|
"description": "module config snippets" |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,100 @@ |
|||||
|
{ |
||||
|
"cl-crud": { |
||||
|
"prefix": "cl-crud", |
||||
|
"scope": "vue", |
||||
|
"body": [ |
||||
|
"<template>", |
||||
|
" <cl-crud ref=\"Crud\">", |
||||
|
" <cl-row>", |
||||
|
" <!-- 刷新按钮 -->", |
||||
|
" <cl-refresh-btn />", |
||||
|
" <!-- 新增按钮 -->", |
||||
|
" <cl-add-btn />", |
||||
|
" <!-- 删除按钮 -->", |
||||
|
" <cl-multi-delete-btn />", |
||||
|
" <cl-flex1 />", |
||||
|
" <!-- 条件搜索 -->", |
||||
|
" <cl-search ref=\"Search\" />", |
||||
|
" </cl-row>", |
||||
|
"", |
||||
|
" <cl-row>", |
||||
|
" <!-- 数据表格 -->", |
||||
|
" <cl-table ref=\"Table\" />", |
||||
|
" </cl-row>", |
||||
|
"", |
||||
|
" <cl-row>", |
||||
|
" <cl-flex1 />", |
||||
|
" <!-- 分页控件 -->", |
||||
|
" <cl-pagination />", |
||||
|
" </cl-row>", |
||||
|
"", |
||||
|
" <!-- 新增、编辑 -->", |
||||
|
" <cl-upsert ref=\"Upsert\" />", |
||||
|
" </cl-crud>", |
||||
|
"</template>", |
||||
|
"", |
||||
|
"<script lang=\"ts\" setup>", |
||||
|
"defineOptions({", |
||||
|
" name: \"$1\"", |
||||
|
"});", |
||||
|
"", |
||||
|
"import { useCrud, useTable, useUpsert, useSearch } from \"@cool-vue/crud\";", |
||||
|
"import { useCool } from \"/@/cool\";", |
||||
|
"", |
||||
|
"const { service } = useCool();", |
||||
|
"", |
||||
|
"// cl-upsert", |
||||
|
"const Upsert = useUpsert({", |
||||
|
" items: []", |
||||
|
"});", |
||||
|
"", |
||||
|
"// cl-table", |
||||
|
"const Table = useTable({", |
||||
|
" columns: []", |
||||
|
"});", |
||||
|
"", |
||||
|
"// cl-search", |
||||
|
"const Search = useSearch();", |
||||
|
"", |
||||
|
"// cl-crud", |
||||
|
"const Crud = useCrud(", |
||||
|
" {", |
||||
|
" service: service$2", |
||||
|
" },", |
||||
|
" (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" |
||||
|
}, |
||||
|
"slot-item": { |
||||
|
"prefix": "slot item", |
||||
|
"scope": "html", |
||||
|
"body": ["<template #slot-$1=\"{ scope }\">", "</template>", ""], |
||||
|
"description": "slot snippets" |
||||
|
}, |
||||
|
"slot-column": { |
||||
|
"prefix": "slot column", |
||||
|
"scope": "html", |
||||
|
"body": ["<template #column-$1=\"{ scope }\">", " ", "</template>", ""], |
||||
|
"description": "column slot snippets" |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,3 @@ |
|||||
|
{ |
||||
|
"recommendations": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode"] |
||||
|
} |
||||
@ -0,0 +1,17 @@ |
|||||
|
{ |
||||
|
"editor.cursorSmoothCaretAnimation": "on", |
||||
|
"explorer.fileNesting.enabled": true, |
||||
|
"explorer.fileNesting.patterns": { |
||||
|
"tsconfig.json": "tsconfig.*.json, env.d.ts", |
||||
|
"vite.config.*": "jsconfig*, vitest.config.*, cypress.config.*, playwright.config.*", |
||||
|
"package.json": "package-lock.json, pnpm*, .yarnrc*, yarn*, .eslint*, eslint*, .prettier*, prettier*, .editorconfig" |
||||
|
}, |
||||
|
"editor.codeActionsOnSave": { |
||||
|
"source.fixAll": "explicit" |
||||
|
}, |
||||
|
"editor.formatOnSave": true, |
||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode", |
||||
|
"[scss]": { |
||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode" |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,14 @@ |
|||||
|
FROM node:lts-alpine |
||||
|
WORKDIR /build |
||||
|
# 设置npm镜像 |
||||
|
RUN npm config set registry https://registry.npmmirror.com |
||||
|
COPY package.json /build/package.json |
||||
|
RUN npm install |
||||
|
COPY ./ /build |
||||
|
RUN npm run build |
||||
|
|
||||
|
FROM nginx |
||||
|
RUN mkdir /app |
||||
|
COPY --from=0 /build/dist /app |
||||
|
COPY --from=0 /build/nginx.conf /etc/nginx/nginx.conf |
||||
|
EXPOSE 80 |
||||
@ -0,0 +1,33 @@ |
|||||
|
MIT License |
||||
|
|
||||
|
Copyright (c) [2025] [厦门闪酷科技开发有限公司] |
||||
|
|
||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy |
||||
|
of this software and associated documentation files (the "Software"), to deal |
||||
|
in the Software without restriction, including without limitation the rights |
||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
||||
|
copies of the Software, and to permit persons to whom the Software is |
||||
|
furnished to do so, subject to the following conditions: |
||||
|
|
||||
|
The above copyright notice and this permission notice shall be included in all |
||||
|
copies or substantial portions of the Software. |
||||
|
|
||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||||
|
SOFTWARE. |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
MIT 许可证 |
||||
|
|
||||
|
版权所有 (c) [2025] [厦门闪酷科技开发有限公司] |
||||
|
|
||||
|
特此免费授予获得本软件及相关文档文件(“软件”)副本的任何人无限制地处理本软件的权限,包括但不限于使用、复制、修改、合并、发布、分发、再许可和/或销售软件的副本,并允许软件提供给其的人员这样做,但须符合以下条件: |
||||
|
|
||||
|
上述版权声明和本许可声明应包含在软件的所有副本或主要部分中。 |
||||
|
|
||||
|
本软件按“原样”提供,不提供任何明示或暗示的担保,包括但不限于对适销性、特定用途适用性和非侵权的担保。在任何情况下,作者或版权持有人均不对因软件或软件使用或其他交易而产生的任何索赔、损害或其他责任负责,无论是在合同诉讼、侵权诉讼或其他诉讼中。 |
||||
@ -0,0 +1,78 @@ |
|||||
|
# 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,方便快速构建迭代后台管理系统, 到<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" /> |
||||
|
<a href=""><img src="https://img.shields.io/github/package-json/v/cool-team-official/cool-admin-vue?style=flat-square" alt="GitHub tag"></a> |
||||
|
<img src="https://img.shields.io/github/last-commit/cool-team-official/cool-admin-vue?style=flat-square" alt="GitHub tag"></a> |
||||
|
</p> |
||||
|
|
||||
|
## 特性 |
||||
|
|
||||
|
Ai时代,很多老旧的框架已经无法满足现代化的开发需求,Cool-Admin开发了一系列的功能,让开发变得更简单、更快速、更高效。 |
||||
|
|
||||
|
- **Ai编码**:通过微调大模型学习框架特有写法,实现简单功能从Api接口到前端页面的一键生成 |
||||
|
- **流程编排**:通过拖拽编排方式,即可实现类似像智能客服这样的功能 |
||||
|
- **模块化**:代码是模块化的,清晰明了,方便维护 |
||||
|
- **插件化**:插件化的设计,可以通过安装插件的方式扩展如:支付、短信、邮件等功能 |
||||
|
|
||||
|
 |
||||
|
|
||||
|
## 地址 |
||||
|
|
||||
|
- [📌 v7 版本](https://github.com/cool-team-official/cool-admin-vue/tree/7.x) |
||||
|
|
||||
|
- [🌐 码云仓库](https://gitee.com/cool-team-official/cool-admin-vue) |
||||
|
|
||||
|
## 视频教程 |
||||
|
|
||||
|
[官方 B 站视频教程](https://www.bilibili.com/video/BV1j1421R7aB) |
||||
|
|
||||
|
## 演示 |
||||
|
|
||||
|
[https://show.cool-admin.com](https://show.cool-admin.com) |
||||
|
|
||||
|
账户:admin,密码:123456 |
||||
|
|
||||
|
<img src="https://cool-show.oss-cn-shanghai.aliyuncs.com/admin/home-mini.png" alt="Admin Home" ></a> |
||||
|
|
||||
|
## 项目后端 |
||||
|
|
||||
|
[https://github.com/cool-team-official/cool-admin-midway](https://github.com/cool-team-official/cool-admin-midway) |
||||
|
|
||||
|
或 |
||||
|
|
||||
|
[https://gitee.com/cool-team-official/cool-admin-midway](https://gitee.com/cool-team-official/cool-admin-midway) |
||||
|
|
||||
|
或 |
||||
|
|
||||
|
[https://gitcode.com/cool_team/cool-admin-midway](https://gitcode.com/cool_team/cool-admin-midway) |
||||
|
|
||||
|
## 微信群 |
||||
|
|
||||
|
<img width="260" src="https://cool-show.oss-cn-shanghai.aliyuncs.com/admin/wechat.jpeg" alt="Admin Wechat"></a> |
||||
|
|
||||
|
## 安装项目依赖 |
||||
|
|
||||
|
推荐使用 `pnpm`: |
||||
|
|
||||
|
```shell |
||||
|
pnpm i |
||||
|
``` |
||||
|
|
||||
|
## 运行应用程序 |
||||
|
|
||||
|
安装过程完成后,运行以下命令启动服务。您可以在浏览器中预览网站 [http://localhost:9000](http://localhost:9000) |
||||
|
|
||||
|
```shell |
||||
|
pnpm dev |
||||
|
``` |
||||
|
|
||||
|
### 低价服务器 |
||||
|
|
||||
|
[阿里云、腾讯云、华为云低价云服务器,不限新老](https://cool-js.com/service/cloud) |
||||
File diff suppressed because it is too large
File diff suppressed because one or more lines are too long
@ -0,0 +1,10 @@ |
|||||
|
/// <reference types="vite/client" />
|
||||
|
|
||||
|
interface ImportMetaEnv { |
||||
|
readonly VITE_NAME: string; |
||||
|
readonly VITE_TIMEOUT: number; |
||||
|
} |
||||
|
|
||||
|
interface ImportMeta { |
||||
|
readonly env: ImportMetaEnv; |
||||
|
} |
||||
@ -0,0 +1,73 @@ |
|||||
|
import pluginVue from 'eslint-plugin-vue'; |
||||
|
import vueTsEslintConfig from '@vue/eslint-config-typescript'; |
||||
|
import prettier from 'eslint-plugin-prettier'; |
||||
|
import skipFormatting from '@vue/eslint-config-prettier/skip-formatting'; |
||||
|
|
||||
|
export default [ |
||||
|
{ |
||||
|
name: 'app/files-to-lint', |
||||
|
files: ['**/*.{ts,mts,tsx,vue}'], |
||||
|
}, |
||||
|
|
||||
|
{ |
||||
|
name: 'app/files-to-ignore', |
||||
|
ignores: [ |
||||
|
'**/dist/**', |
||||
|
'**/dist-ssr/**', |
||||
|
'**/coverage/**', |
||||
|
'**/packages/**', |
||||
|
'**/build/**', |
||||
|
], |
||||
|
}, |
||||
|
|
||||
|
...pluginVue.configs['flat/recommended'], |
||||
|
...vueTsEslintConfig(), |
||||
|
skipFormatting, |
||||
|
|
||||
|
{ |
||||
|
languageOptions: { |
||||
|
parserOptions: { |
||||
|
ecmaVersion: 2020, |
||||
|
ecmaFeatures: { |
||||
|
jsx: true, |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
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', |
||||
|
'@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': 'off', |
||||
|
'@typescript-eslint/no-empty-object-type': 'off', |
||||
|
'space-before-function-paren': 'off', |
||||
|
'no-unused-vars': 'off', |
||||
|
'no-use-before-define': 'off', |
||||
|
'no-self-assign': 'off', |
||||
|
'vue/no-mutating-props': 'off', |
||||
|
'vue/no-template-shadow': 'off', |
||||
|
'vue/no-v-html': 'off', |
||||
|
'vue/component-name-in-template-casing': ['error', 'kebab-case'], |
||||
|
'vue/component-definition-name-casing': ['error', 'kebab-case'], |
||||
|
'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', |
||||
|
'vue/block-lang': 'off', |
||||
|
}, |
||||
|
}, |
||||
|
]; |
||||
@ -0,0 +1,178 @@ |
|||||
|
<!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%; |
||||
|
overflow: hidden; |
||||
|
} |
||||
|
|
||||
|
* { |
||||
|
-webkit-font-smoothing: antialiased; |
||||
|
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; |
||||
|
min-height: 50px; |
||||
|
} |
||||
|
|
||||
|
.preload__title { |
||||
|
color: #fff; |
||||
|
font-size: 14px; |
||||
|
margin: 30px 0 20px 0; |
||||
|
min-height: 20px; |
||||
|
} |
||||
|
|
||||
|
.preload__sub-title { |
||||
|
color: #ababab; |
||||
|
font-size: 12px; |
||||
|
min-height: 20px; |
||||
|
} |
||||
|
|
||||
|
.preload__name, |
||||
|
.preload__title, |
||||
|
.preload__sub-title { |
||||
|
animation: s 0.5s ease-in; |
||||
|
} |
||||
|
|
||||
|
@keyframes s { |
||||
|
from { |
||||
|
opacity: 0; |
||||
|
} |
||||
|
to { |
||||
|
opacity: 1; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.preload__loading { |
||||
|
height: 44px; |
||||
|
width: 44px; |
||||
|
border-radius: 30px; |
||||
|
border: 7px solid currentColor; |
||||
|
border-bottom-color: #2f3447; |
||||
|
position: relative; |
||||
|
animation: |
||||
|
r 1s infinite cubic-bezier(0.17, 0.67, 0.83, 0.67), |
||||
|
bc 2s infinite ease-in; |
||||
|
transform: rotate(0deg); |
||||
|
box-sizing: border-box; |
||||
|
} |
||||
|
|
||||
|
@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"> |
||||
|
<div class="preload__name"></div> |
||||
|
<div class="preload__loading"></div> |
||||
|
<div class="preload__title"></div> |
||||
|
<div class="preload__sub-title"></div> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div id="app"></div> |
||||
|
<script type="module" src="/src/main.ts"></script> |
||||
|
</body> |
||||
|
</html> |
||||
@ -0,0 +1,72 @@ |
|||||
|
user nginx; |
||||
|
worker_processes 1; |
||||
|
error_log /var/log/nginx/error.log warn; |
||||
|
pid /var/run/nginx.pid; |
||||
|
events { |
||||
|
worker_connections 1024; |
||||
|
} |
||||
|
http { |
||||
|
include /etc/nginx/mime.types; |
||||
|
default_type application/octet-stream; |
||||
|
log_format main '$remote_addr - $remote_user [$time_local] "$request" ' |
||||
|
'$status $body_bytes_sent "$http_referer" ' |
||||
|
'"$http_user_agent" "$http_x_forwarded_for"'; |
||||
|
access_log /var/log/nginx/access.log main; |
||||
|
sendfile on; |
||||
|
keepalive_timeout 65; |
||||
|
upstream cool { |
||||
|
server midway:8001; |
||||
|
} |
||||
|
|
||||
|
server { |
||||
|
listen 80; |
||||
|
server_name localhost; |
||||
|
location / { |
||||
|
root /app; |
||||
|
index index.html; |
||||
|
try_files $uri $uri/ /index.html; |
||||
|
} |
||||
|
location /api/ { |
||||
|
proxy_pass http://cool/; |
||||
|
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_cache cache_one; |
||||
|
#proxy_cache_key $host$request_uri$is_args$args; |
||||
|
#proxy_cache_valid 200 304 301 302 1h; |
||||
|
|
||||
|
#持久化连接相关配置 |
||||
|
proxy_connect_timeout 3000s; |
||||
|
proxy_read_timeout 86400s; |
||||
|
proxy_send_timeout 3000s; |
||||
|
#proxy_http_version 1.1; |
||||
|
#proxy_set_header Upgrade $http_upgrade; |
||||
|
#proxy_set_header Connection "upgrade"; |
||||
|
|
||||
|
add_header X-Cache $upstream_cache_status; |
||||
|
|
||||
|
#expires 12h; |
||||
|
} |
||||
|
|
||||
|
# socket需额外配置 |
||||
|
location /socket { |
||||
|
proxy_pass http://cool/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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
File diff suppressed because it is too large
@ -0,0 +1,77 @@ |
|||||
|
{ |
||||
|
"name": "cool-admin-vue", |
||||
|
"version": "8.0.0", |
||||
|
"type": "module", |
||||
|
"scripts": { |
||||
|
"dev": "vite --host", |
||||
|
"build": "vite build", |
||||
|
"build-static": "vite build --mode static", |
||||
|
"build-demo": "vite build --mode demo", |
||||
|
"preview": "vite preview", |
||||
|
"type-check": "vue-tsc --build --force", |
||||
|
"lint": "eslint . --fix", |
||||
|
"format": "prettier --write src/" |
||||
|
}, |
||||
|
"dependencies": { |
||||
|
"@cool-vue/crud": "^8.0.6", |
||||
|
"@element-plus/icons-vue": "^2.3.1", |
||||
|
"@vueuse/core": "^12.5.0", |
||||
|
"@wangeditor/editor": "^5.1.23", |
||||
|
"@wangeditor/editor-for-vue": "^5.1.12", |
||||
|
"axios": "^1.7.9", |
||||
|
"chardet": "^2.0.0", |
||||
|
"core-js": "^3.40.0", |
||||
|
"dayjs": "^1.11.13", |
||||
|
"echarts": "^5.6.0", |
||||
|
"element-plus": "2.10.2", |
||||
|
"file-saver": "^2.0.5", |
||||
|
"lodash-es": "^4.17.21", |
||||
|
"marked": "^14.1.3", |
||||
|
"mitt": "^3.0.1", |
||||
|
"nprogress": "^0.2.0", |
||||
|
"pinia": "^2.3.1", |
||||
|
"socket.io-client": "^4.8.3", |
||||
|
"store": "^2.0.12", |
||||
|
"vue": "^3.5.13", |
||||
|
"vue-echarts": "^7.0.3", |
||||
|
"vue-i18n": "^11.0.1", |
||||
|
"vue-router": "^4.5.0", |
||||
|
"vuedraggable": "^4.1.0", |
||||
|
"xlsx": "^0.18.5" |
||||
|
}, |
||||
|
"devDependencies": { |
||||
|
"@cool-vue/vite-plugin": "^8.2.2", |
||||
|
"@intlify/unplugin-vue-i18n": "^6.0.3", |
||||
|
"@rushstack/eslint-patch": "^1.10.5", |
||||
|
"@tsconfig/node20": "^20.1.4", |
||||
|
"@types/file-saver": "^2.0.7", |
||||
|
"@types/lodash-es": "^4.17.12", |
||||
|
"@types/mockjs": "^1.0.10", |
||||
|
"@types/node": "^20.19.37", |
||||
|
"@types/nprogress": "^0.2.3", |
||||
|
"@types/store": "^2.0.5", |
||||
|
"@vitejs/plugin-vue": "^5.2.1", |
||||
|
"@vitejs/plugin-vue-jsx": "^4.1.1", |
||||
|
"@vue/compiler-sfc": "^3.5.13", |
||||
|
"@vue/eslint-config-prettier": "^10.2.0", |
||||
|
"@vue/eslint-config-typescript": "^14.3.0", |
||||
|
"@vue/runtime-core": "^3.5.30", |
||||
|
"@vue/test-utils": "^2.4.6", |
||||
|
"@vue/tsconfig": "^0.5.1", |
||||
|
"autoprefixer": "^10.4.20", |
||||
|
"eslint": "^9.19.0", |
||||
|
"eslint-plugin-prettier": "^5.2.3", |
||||
|
"eslint-plugin-vue": "^9.32.0", |
||||
|
"postcss": "^8.5.1", |
||||
|
"prettier": "^3.4.2", |
||||
|
"rollup-plugin-visualizer": "^5.14.0", |
||||
|
"sass": "1.81.0", |
||||
|
"tailwindcss": "^3.4.17", |
||||
|
"terser": "^5.36.0", |
||||
|
"typescript": "~5.5.4", |
||||
|
"vite": "^5.4.14", |
||||
|
"vite-plugin-compression": "^0.5.1", |
||||
|
"vite-plugin-vue-devtools": "^7.7.1", |
||||
|
"vue-tsc": "^2.2.0" |
||||
|
} |
||||
|
} |
||||
@ -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? |
||||
@ -0,0 +1,9 @@ |
|||||
|
{ |
||||
|
"tabWidth": 4, |
||||
|
"useTabs": true, |
||||
|
"semi": true, |
||||
|
"jsxBracketSameLine": true, |
||||
|
"singleQuote": false, |
||||
|
"printWidth": 100, |
||||
|
"trailingComma": "none" |
||||
|
} |
||||
@ -0,0 +1,33 @@ |
|||||
|
# 介绍 |
||||
|
|
||||
|
**cool-admin for vue**是基于[Vue.js](https://v3.cn.vuejs.org)开发。 |
||||
|
|
||||
|
[cool-admin 官方文档](https://cool-js.com/) |
||||
|
|
||||
|
尝试 `cool-admin` 最简单的方法就是查看文档及运行示例。 |
||||
|
|
||||
|
<img src='https://vue.cool-admin.com/show/admin.png' /> |
||||
|
|
||||
|
[Ai极速编码 🔥 在线体验](https://show.cool-admin.com/helper/ai-code) |
||||
|
|
||||
|
<img src='https://vue.cool-admin.com/show/code.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),构建工具; |
||||
@ -0,0 +1,811 @@ |
|||||
|
// 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 = "left" | "center" | "right"; |
||||
|
|
||||
|
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; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// mitt
|
||||
|
declare interface Mitt { |
||||
|
on(name: string, callback: (data: any) => void): void; |
||||
|
off(name: string, callback: (data: any) => void): void; |
||||
|
emit(name: string, data?: any): void; |
||||
|
} |
||||
|
|
||||
|
// 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; |
||||
|
on(name: string, callback: (data: any) => void): void; |
||||
|
emit(name: string, data?: any): void; |
||||
|
} |
||||
|
|
||||
|
// 方法
|
||||
|
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>)>; |
||||
|
|
||||
|
// 获取keys
|
||||
|
declare type PropKey<T> = keyof RemoveIndex<T> | (string & {}); |
||||
|
|
||||
|
// 任意字符串
|
||||
|
declare type AnyString = string & {}; |
||||
|
|
||||
|
// 类型或者 Ref 泛型
|
||||
|
declare type RefData<T = any> = T | Vue.Ref<T>; |
||||
|
|
||||
|
// browser
|
||||
|
declare type Browser = { |
||||
|
screen: string; |
||||
|
isMini: boolean; |
||||
|
}; |
||||
|
|
||||
|
// 字典选项
|
||||
|
declare type DictOptions = { |
||||
|
label?: string; |
||||
|
value?: any; |
||||
|
color?: string; |
||||
|
type?: string; |
||||
|
[key: string]: any; |
||||
|
}[]; |
||||
|
|
||||
|
// 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?: RefData<DictOptions>; |
||||
|
props?: RefData<Props>; |
||||
|
style?: obj; |
||||
|
slots?: { |
||||
|
[key: string]: (data?: any) => any; |
||||
|
}; |
||||
|
vm?: any; |
||||
|
[key: string]: any; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// crud
|
||||
|
declare namespace ClCrud { |
||||
|
declare interface Field { |
||||
|
comment: string; |
||||
|
source: string; |
||||
|
propertyName: string; |
||||
|
type: string; |
||||
|
dict: string | string[]; |
||||
|
nullable: boolean; |
||||
|
} |
||||
|
|
||||
|
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; |
||||
|
placeholderSelect: 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; |
||||
|
collapse: string; |
||||
|
expand: 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"]>; |
||||
|
permission: Permission; |
||||
|
search: { |
||||
|
fieldEq: Field[]; |
||||
|
fieldLike: Field[]; |
||||
|
keyWordLikeFields: Field[]; |
||||
|
}; |
||||
|
[key: string]: any; |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
interface Config { |
||||
|
name: string; |
||||
|
service: Service["api"]; |
||||
|
permission: Permission; |
||||
|
dict: Dict; |
||||
|
onRefresh( |
||||
|
params: obj, |
||||
|
event: { |
||||
|
done: fn; |
||||
|
next: Service["api"]["page"]; |
||||
|
render: (data: any | any[], pagination?: Response["page"]["pagination"]) => void; |
||||
|
} |
||||
|
): void; |
||||
|
onDelete( |
||||
|
selection: obj[], |
||||
|
event: { |
||||
|
next: Service["api"]["delete"]; |
||||
|
} |
||||
|
): void; |
||||
|
} |
||||
|
|
||||
|
interface Ref { |
||||
|
"cl-table": ClTable.Ref; |
||||
|
"cl-upsert": ClUpsert.Ref; |
||||
|
id: number; |
||||
|
mitt: Mitt; |
||||
|
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; |
||||
|
setParams(data: obj): void; |
||||
|
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; |
||||
|
} |
||||
|
|
||||
|
interface Options extends DeepPartial<Config> { |
||||
|
service?: any; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
declare namespace ClTable { |
||||
|
type OpButton = Array<"info" | "edit" | "delete" | AnyString | Render.OpButton>; |
||||
|
|
||||
|
type ColumnType = "index" | "selection" | "expand" | "op" | AnyString; |
||||
|
|
||||
|
interface Column<T = any> { |
||||
|
type: ColumnType; |
||||
|
hidden: RefData<boolean>; |
||||
|
component: Render.Component; |
||||
|
search: { |
||||
|
isInput: boolean; |
||||
|
value: any; |
||||
|
icon: () => any; |
||||
|
refreshOnChange: boolean; |
||||
|
component: Render.Component; |
||||
|
}; |
||||
|
dict: RefData<DictOptions>; |
||||
|
dictFormatter: (values: DictOptions) => string; |
||||
|
dictColor: boolean; |
||||
|
dictSeparator: string; |
||||
|
dictAllLevels: boolean; |
||||
|
buttons: OpButton | ((options: { scope: T }) => OpButton); |
||||
|
align: ElementPlus.Align; |
||||
|
label: any; |
||||
|
renderLabel: (options: { column: any; $index: number }) => any; |
||||
|
className: string; |
||||
|
prop: PropKey<T>; |
||||
|
orderNum: number; |
||||
|
width: RefData<number | string>; |
||||
|
minWidth: RefData<number | string>; |
||||
|
renderHeader: (options: { column: any; $index: number }) => any; |
||||
|
sortable: boolean | "desc" | "descending" | "ascending" | "asc" | "custom"; |
||||
|
sortMethod: fn; |
||||
|
sortBy: string | ((row: T, index: number) => any) | any[]; |
||||
|
resizable: boolean; |
||||
|
columnKey: string; |
||||
|
headerAlign: ElementPlus.Align; |
||||
|
showOverflowTooltip: boolean; |
||||
|
fixed: boolean | string; |
||||
|
render: (row: T, column: any, value: any, index: number) => any; |
||||
|
formatter: (row: T, column: any, value: any, index: number) => any; |
||||
|
selectable: (row: T, index: number) => boolean; |
||||
|
reserveSelection: boolean; |
||||
|
filterMethod: fn; |
||||
|
filteredValue: unknown[]; |
||||
|
filters: unknown[]; |
||||
|
filterPlacement: string; |
||||
|
filterMultiple: boolean; |
||||
|
index: ((index: number) => number) | number; |
||||
|
sortOrders: unknown[]; |
||||
|
children: Column<T>[]; |
||||
|
[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" |
||||
|
>; |
||||
|
|
||||
|
type Plugin = (options: { exposed: Ref }) => void; |
||||
|
|
||||
|
interface Config<T = any> { |
||||
|
columns: Column<T>[]; |
||||
|
autoHeight: boolean; |
||||
|
height: any; |
||||
|
contextMenu: ContextMenu; |
||||
|
defaultSort: { |
||||
|
prop: string; |
||||
|
order: "descending" | "ascending"; |
||||
|
}; |
||||
|
sortRefresh: boolean; |
||||
|
emptyText: string; |
||||
|
rowKey: string; |
||||
|
on?: { |
||||
|
[key: string]: (...args: any[]) => void; |
||||
|
}; |
||||
|
props?: { |
||||
|
[key: string]: any; |
||||
|
}; |
||||
|
plugins?: Plugin[]; |
||||
|
onRowContextmenu?(row: T, column: any, event: any): void; |
||||
|
} |
||||
|
|
||||
|
interface Ref<T = any> { |
||||
|
Table: any; |
||||
|
config: obj; |
||||
|
selection: T[]; |
||||
|
data: T[]; |
||||
|
columns: Column<T>[]; |
||||
|
reBuild(cb?: fn): void; |
||||
|
calcMaxHeight(): void; |
||||
|
setData(data: T[]): void; |
||||
|
setColumns(columns: Column[]): void; |
||||
|
showColumn(props: PropKey<T> | PropKey<T>[], status?: boolean): void; |
||||
|
hideColumn(props: PropKey<T> | PropKey<T>[]): void; |
||||
|
changeSort(prop: PropKey<T>, order: string): void; |
||||
|
clearSelection(): void; |
||||
|
getSelectionRows(): any[]; |
||||
|
toggleRowSelection(row: T, selected?: boolean): void; |
||||
|
toggleAllSelection(): void; |
||||
|
toggleRowExpansion(row: T, expanded?: boolean): void; |
||||
|
setCurrentRow(row: T): void; |
||||
|
clearSort(): void; |
||||
|
clearFilter(columnKeys: PropKey<T>[]): void; |
||||
|
doLayout(): void; |
||||
|
sort(prop: PropKey<T>, order: string): void; |
||||
|
scrollTo(position: { top?: number; left?: number }): void; |
||||
|
setScrollTop(top: number): void; |
||||
|
setScrollLeft(left: number): void; |
||||
|
updateKeyChildren(key: string, children: any[]): void; |
||||
|
} |
||||
|
|
||||
|
interface Options<T = any> extends DeepPartial<Config<T>> { |
||||
|
columns?: List<ClTable.Column<T>>; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
declare namespace ClFormTabs { |
||||
|
type labels = { |
||||
|
label: string; |
||||
|
value: string; |
||||
|
name?: string; |
||||
|
icon?: any; |
||||
|
lazy?: boolean; |
||||
|
[key: string]: any; |
||||
|
}[]; |
||||
|
} |
||||
|
|
||||
|
declare namespace ClForm { |
||||
|
type CloseAction = "close" | "save" | AnyString; |
||||
|
|
||||
|
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 Hook { |
||||
|
Fn: (value: any, options: { form: obj; prop: string; method: "submit" | "bind" }) => any; |
||||
|
Key: |
||||
|
| "number" |
||||
|
| "string" |
||||
|
| "split" |
||||
|
| "join" |
||||
|
| "boolean" |
||||
|
| "booleanNumber" |
||||
|
| "datetimeRange" |
||||
|
| "splitJoin" |
||||
|
| "json" |
||||
|
| "empty" |
||||
|
| AnyString; |
||||
|
Pipe: Hook["Key"] | Hook["Fn"]; |
||||
|
Event: { |
||||
|
bind?: Hook["Pipe"] | Hook["Pipe"][]; |
||||
|
submit?: Hook["Pipe"] | Hook["Pipe"][]; |
||||
|
reset?: (prop: string) => string[]; |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
interface Item<T = any> { |
||||
|
type?: "tabs"; |
||||
|
prop?: PropKey<T>; |
||||
|
props?: { |
||||
|
labels?: ClFormTabs.labels; |
||||
|
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; |
||||
|
}; |
||||
|
group?: string; |
||||
|
collapse?: boolean; |
||||
|
value?: any; |
||||
|
label?: string; |
||||
|
renderLabel?: any; |
||||
|
flex?: boolean; |
||||
|
hook?: Hook["Event"] | Hook["Key"]; |
||||
|
hidden?: boolean | ((options: { scope: obj }) => boolean); |
||||
|
prepend?: Render.Component; |
||||
|
component?: Render.Component; |
||||
|
append?: Render.Component; |
||||
|
rules?: Rule | Rule[]; |
||||
|
required?: boolean; |
||||
|
children?: Item[]; |
||||
|
[key: string]: any; |
||||
|
} |
||||
|
|
||||
|
interface Config<T = any> { |
||||
|
title?: any; |
||||
|
height?: any; |
||||
|
width?: any; |
||||
|
props: ElementPlus.FormProps; |
||||
|
items: Item[]; |
||||
|
form: obj; |
||||
|
isReset?: boolean; |
||||
|
on?: { |
||||
|
open?(data: T): void; |
||||
|
close?(action: CloseAction, done: fn): void; |
||||
|
submit?(data: T, event: { close: fn; done: fn }): void; |
||||
|
change?(data: T, prop: string): 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" | AnyString>; |
||||
|
[key: string]: any; |
||||
|
}; |
||||
|
[key: string]: any; |
||||
|
} |
||||
|
|
||||
|
type Plugin = (options: { |
||||
|
exposed: Ref; |
||||
|
onOpen(cb: () => void): void; |
||||
|
onClose(cb: () => void): void; |
||||
|
onSubmit(cb: (data: obj) => obj): void; |
||||
|
}) => void; |
||||
|
|
||||
|
type Items<T = any> = List<Item<T>>; |
||||
|
|
||||
|
interface Ref<T = any> { |
||||
|
Form: any; |
||||
|
form: T; |
||||
|
config: { |
||||
|
items: Item[]; |
||||
|
[key: string]: any; |
||||
|
}; |
||||
|
open(options: Options<T>, plugins?: Plugin[]): void; |
||||
|
close(action?: CloseAction): void; |
||||
|
done(): void; |
||||
|
clear(): void; |
||||
|
reset(): void; |
||||
|
showLoading(): void; |
||||
|
hideLoading(): void; |
||||
|
setDisabled(flag?: boolean): void; |
||||
|
invokeData(data: any): 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; |
||||
|
Tabs: { |
||||
|
active: RefData<string>; |
||||
|
list: ClFormTabs.labels; |
||||
|
change(value: any, valid?: boolean): Promise<any>; |
||||
|
[key: string]: any; |
||||
|
}; |
||||
|
[key: string]: any; |
||||
|
} |
||||
|
|
||||
|
interface Options<T = any> extends DeepPartial<Config> { |
||||
|
items?: Items<T>; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
declare namespace ClUpsert { |
||||
|
interface Config<T = any> { |
||||
|
sync: boolean; |
||||
|
items: ClForm.Item[]; |
||||
|
props: ClForm.Config["props"]; |
||||
|
op: ClForm.Config["op"]; |
||||
|
dialog: ClForm.Config["dialog"]; |
||||
|
onOpen?(): void; |
||||
|
onOpened?(data: T): void; |
||||
|
onClose?(action: ClForm.CloseAction, done: fn): void; |
||||
|
onClosed?(): void; |
||||
|
onInfo?( |
||||
|
data: T, |
||||
|
event: { close: fn; done(data: T): void; next: ClCrud.Service["api"]["info"] } |
||||
|
): void; |
||||
|
onSubmit?( |
||||
|
data: T, |
||||
|
event: { close: fn; done: fn; next: ClCrud.Service["api"]["update"] } |
||||
|
): void; |
||||
|
plugins?: ClForm.Plugin[]; |
||||
|
} |
||||
|
|
||||
|
interface Ref<T = any> extends ClForm.Ref<T> { |
||||
|
mode: "add" | "update" | "info" | AnyString; |
||||
|
} |
||||
|
|
||||
|
interface Options<T = any> extends DeepPartial<Config<T>> { |
||||
|
items?: ClForm.Items<T>; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
declare namespace ClAdvSearch { |
||||
|
interface Config<T = any> { |
||||
|
items?: ClForm.Item[]; |
||||
|
title?: string; |
||||
|
size?: string | number; |
||||
|
op?: ("clear" | "reset" | "close" | "search" | `slot-${string}`)[]; |
||||
|
onSearch?(data: T, options: { next: ClCrud.Service["api"]["page"]; close(): void }): void; |
||||
|
} |
||||
|
|
||||
|
interface Ref<T = any> extends ClForm.Ref<T> {} |
||||
|
|
||||
|
interface Options<T = any> extends DeepPartial<Config<T>> { |
||||
|
items?: ClForm.Items<T>; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
declare namespace ClSearch { |
||||
|
type Plugin = (options: { exposed: Ref }) => void; |
||||
|
|
||||
|
interface Config<T = any> { |
||||
|
inline?: boolean; |
||||
|
items?: ClForm.Item[]; |
||||
|
data?: T; |
||||
|
props?: ElementPlus.FormProps; |
||||
|
resetBtn?: boolean; |
||||
|
collapse?: boolean; |
||||
|
Form?: ClForm.Ref; |
||||
|
onChange?(data: T, prop: string): void; |
||||
|
onLoad?(data: T): void; |
||||
|
onSearch?(data: T, options: { next: ClCrud.Service["api"]["page"] }): void; |
||||
|
plugins?: Plugin[]; |
||||
|
} |
||||
|
|
||||
|
interface Ref<T = any> extends ClForm.Ref<T> { |
||||
|
search(params?: obj): void; |
||||
|
reset(): void; |
||||
|
} |
||||
|
|
||||
|
interface Options<T = any> extends DeepPartial<Config<T>> { |
||||
|
items?: ClForm.Items<T>; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
declare namespace ClContextMenu { |
||||
|
interface Item { |
||||
|
label: string; |
||||
|
prefixIcon?: any; |
||||
|
suffixIcon?: any; |
||||
|
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 { |
||||
|
class?: string; |
||||
|
hover?: |
||||
|
| boolean |
||||
|
| { |
||||
|
target?: string; |
||||
|
className?: string; |
||||
|
}; |
||||
|
list?: Item[]; |
||||
|
} |
||||
|
|
||||
|
interface Ref { |
||||
|
open(event: Event, options: Options): Exposed; |
||||
|
close(): void; |
||||
|
} |
||||
|
|
||||
|
interface Exposed { |
||||
|
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; |
||||
|
}; |
||||
|
style: { |
||||
|
size: ElementPlus.Size; |
||||
|
colors: string[]; |
||||
|
form: { |
||||
|
labelPosition: ElementPlus.FormProps["labelPosition"]; |
||||
|
labelWidth: ElementPlus.FormProps["labelWidth"]; |
||||
|
span: number; |
||||
|
plugins: ClForm.Plugin[]; |
||||
|
}; |
||||
|
table: { |
||||
|
stripe: boolean; |
||||
|
border: boolean; |
||||
|
highlightCurrentRow: boolean; |
||||
|
resizable: boolean; |
||||
|
autoHeight: boolean; |
||||
|
contextMenu: ClTable.ContextMenu; |
||||
|
column: { |
||||
|
minWidth: number | string; |
||||
|
align: ElementPlus.Align; |
||||
|
headerAlign: ElementPlus.Align; |
||||
|
opWidth: number | string; |
||||
|
}; |
||||
|
plugins: ClTable.Plugin[]; |
||||
|
}; |
||||
|
search: { |
||||
|
plugins: ClSearch.Plugin[]; |
||||
|
}; |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
declare type Options = DeepPartial<Config>; |
||||
|
|
||||
|
declare interface CrudOptions { |
||||
|
options: Options; |
||||
|
} |
||||
@ -0,0 +1,29 @@ |
|||||
|
<!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" /> |
||||
|
</head> |
||||
|
|
||||
|
<body> |
||||
|
<div class="preload__wrap" id="Loading"> |
||||
|
<div class="preload__container"> |
||||
|
<p class="preload__name"></p> |
||||
|
<div class="preload__loading"></div> |
||||
|
<p class="preload__title"></p> |
||||
|
<p class="preload__sub-title"></p> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div id="app"></div> |
||||
|
<script type="module" src="/src/main.ts"></script> |
||||
|
</body> |
||||
|
</html> |
||||
@ -0,0 +1,38 @@ |
|||||
|
{ |
||||
|
"name": "@cool-vue/crud", |
||||
|
"version": "8.0.6", |
||||
|
"private": false, |
||||
|
"main": "./dist/index.umd.js", |
||||
|
"module": "./dist/index.es.js", |
||||
|
"types": "types/entry.d.ts", |
||||
|
"type": "module", |
||||
|
"scripts": { |
||||
|
"dev": "vite", |
||||
|
"build": "vue-tsc && vite build", |
||||
|
"preview": "vite preview" |
||||
|
}, |
||||
|
"dependencies": { |
||||
|
"@vue/runtime-core": "^3.5.13", |
||||
|
"element-plus": "^2.10.4", |
||||
|
"lodash-es": "^4.17.21", |
||||
|
"vue": "^3.5.13" |
||||
|
}, |
||||
|
"devDependencies": { |
||||
|
"@types/node": "^20.11.16", |
||||
|
"@vitejs/plugin-vue": "^5.2.1", |
||||
|
"@vitejs/plugin-vue-jsx": "^4.1.1", |
||||
|
"prettier": "^3.5.1", |
||||
|
"sass": "^1.85.0", |
||||
|
"sass-loader": "^16.0.5", |
||||
|
"typescript": "^5.3.3", |
||||
|
"vite": "^6.1.0", |
||||
|
"vite-plugin-dts": "^4.5.0", |
||||
|
"vue-tsc": "^2.2.2" |
||||
|
}, |
||||
|
"files": [ |
||||
|
"types", |
||||
|
"dist", |
||||
|
"index.d.ts", |
||||
|
"index.ts" |
||||
|
] |
||||
|
} |
||||
File diff suppressed because it is too large
@ -0,0 +1,208 @@ |
|||||
|
<template> |
||||
|
<div> |
||||
|
<div class="title">CRUD DEMO v8</div> |
||||
|
|
||||
|
<cl-crud ref="Crud"> |
||||
|
<div class="search"> |
||||
|
<cl-search ref="Search" /> |
||||
|
</div> |
||||
|
|
||||
|
<cl-row> |
||||
|
<cl-add-btn /> |
||||
|
|
||||
|
<cl-flex1 /> |
||||
|
|
||||
|
<cl-search-key |
||||
|
field="name" |
||||
|
:field-list="[ |
||||
|
{ |
||||
|
label: '昵称', |
||||
|
value: 'name' |
||||
|
}, |
||||
|
{ |
||||
|
label: '手机号', |
||||
|
value: 'phone' |
||||
|
} |
||||
|
]" |
||||
|
refreshOnInput |
||||
|
/> |
||||
|
</cl-row> |
||||
|
|
||||
|
<cl-row> |
||||
|
<cl-table ref="Table" :auto-height="false"></cl-table> |
||||
|
</cl-row> |
||||
|
|
||||
|
<cl-row> |
||||
|
<cl-flex1 /> |
||||
|
<cl-pagination /> |
||||
|
</cl-row> |
||||
|
|
||||
|
<cl-upsert ref="Upsert"></cl-upsert> |
||||
|
<cl-form ref="Form"></cl-form> |
||||
|
</cl-crud> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script setup lang="tsx"> |
||||
|
import { useTable, useForm, useUpsert, useCrud, useSearch } from "./hooks"; |
||||
|
import { EditPen } from "@element-plus/icons-vue"; |
||||
|
|
||||
|
interface Data { |
||||
|
name?: string; |
||||
|
age?: number; |
||||
|
[key: string]: any; |
||||
|
} |
||||
|
|
||||
|
const Upsert = useUpsert<Data>({ |
||||
|
items: [ |
||||
|
{ |
||||
|
type: "tabs", |
||||
|
props: { |
||||
|
labels: [ |
||||
|
{ |
||||
|
label: "基础", |
||||
|
value: "A", |
||||
|
icon: EditPen |
||||
|
}, |
||||
|
{ |
||||
|
label: "高级", |
||||
|
value: "B" |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
group: "A", |
||||
|
prop: "age", |
||||
|
label: "年龄", |
||||
|
component: { |
||||
|
name: "el-input" |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
group: "A", |
||||
|
prop: "name", |
||||
|
label: "昵称", |
||||
|
component: { |
||||
|
name: "el-input" |
||||
|
}, |
||||
|
hidden({ scope }) { |
||||
|
return scope.age < 18; |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
group: "B", |
||||
|
prop: "phone", |
||||
|
label: "手机", |
||||
|
component: { |
||||
|
name: "el-input" |
||||
|
}, |
||||
|
hidden({ scope }) { |
||||
|
return scope.age < 18; |
||||
|
} |
||||
|
}, |
||||
|
() => { |
||||
|
return { |
||||
|
group: "A", |
||||
|
hidden: Upsert.value?.mode == "add" |
||||
|
}; |
||||
|
} |
||||
|
], |
||||
|
onOpened(data) { |
||||
|
console.log(data); |
||||
|
Upsert.value?.setForm("age", "18"); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
const Table = useTable<Data>( |
||||
|
{ |
||||
|
contextMenu: [ |
||||
|
{ |
||||
|
label: "带图标", |
||||
|
prefixIcon: EditPen |
||||
|
}, |
||||
|
{ |
||||
|
label: "多层级", |
||||
|
children: [ |
||||
|
{ |
||||
|
label: "A", |
||||
|
children: [ |
||||
|
{ |
||||
|
label: "A-1" |
||||
|
} |
||||
|
] |
||||
|
}, |
||||
|
{ |
||||
|
label: "B" |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
], |
||||
|
|
||||
|
columns: [ |
||||
|
{ |
||||
|
label: "姓名", |
||||
|
prop: "name", |
||||
|
search: { |
||||
|
component: { |
||||
|
name: "el-date-picker" |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
label: "手机号", |
||||
|
prop: "phone", |
||||
|
search: { |
||||
|
component: { |
||||
|
name: "el-date-picker" |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
type: "op" |
||||
|
} |
||||
|
] |
||||
|
}, |
||||
|
(table) => { |
||||
|
console.log(table); |
||||
|
} |
||||
|
); |
||||
|
|
||||
|
const Crud = useCrud( |
||||
|
{ |
||||
|
service: "test" |
||||
|
}, |
||||
|
(app) => { |
||||
|
app.refresh(); |
||||
|
} |
||||
|
); |
||||
|
|
||||
|
const Form = useForm<Data>(); |
||||
|
|
||||
|
const Search = useSearch({ |
||||
|
collapse: true, |
||||
|
resetBtn: true, |
||||
|
items: [ |
||||
|
{ |
||||
|
label: "姓名", |
||||
|
prop: "name", |
||||
|
component: { |
||||
|
name: "el-input" |
||||
|
}, |
||||
|
hook: { |
||||
|
reset() { |
||||
|
return []; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
] |
||||
|
}); |
||||
|
</script> |
||||
|
|
||||
|
<style scoped> |
||||
|
.title { |
||||
|
text-align: center; |
||||
|
font-size: 14px; |
||||
|
font-weight: bold; |
||||
|
} |
||||
|
</style> |
||||
@ -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> |
||||
|
) |
||||
|
); |
||||
|
}; |
||||
|
} |
||||
|
}); |
||||
@ -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> |
||||
|
); |
||||
|
}; |
||||
|
} |
||||
|
}); |
||||
@ -0,0 +1,203 @@ |
|||||
|
import { defineComponent, h, inject, mergeProps, nextTick, type 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"; |
||||
|
import { isArray } from "lodash-es"; |
||||
|
|
||||
|
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(() => { |
||||
|
Form.value?.open({ |
||||
|
items: config.items || [], |
||||
|
op: { |
||||
|
hidden: true |
||||
|
}, |
||||
|
isReset: false |
||||
|
}); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
// 关闭
|
||||
|
function close() { |
||||
|
Drawer.value.handleClose(); |
||||
|
} |
||||
|
|
||||
|
// 重置数据
|
||||
|
function reset() { |
||||
|
const d: any = {}; |
||||
|
|
||||
|
config.items?.map((e) => { |
||||
|
if (typeof e.hook != 'string' && e.hook?.reset) { |
||||
|
const props = e.hook.reset(e.prop!) |
||||
|
|
||||
|
if (isArray(props)) { |
||||
|
props.forEach((prop) => { |
||||
|
d[prop] = undefined; |
||||
|
}) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
d[e.prop!] = undefined; |
||||
|
}); |
||||
|
|
||||
|
// 重置表单
|
||||
|
Form.value?.reset(); |
||||
|
|
||||
|
// 列表刷新
|
||||
|
search(); |
||||
|
|
||||
|
// 重置事件
|
||||
|
emit("reset", d); |
||||
|
} |
||||
|
|
||||
|
// 清空数据
|
||||
|
function clear() { |
||||
|
Form.value?.clear(); |
||||
|
emit("clear"); |
||||
|
} |
||||
|
|
||||
|
// 搜素请求
|
||||
|
function search(params?: any) { |
||||
|
const form = Form.value?.getForm(); |
||||
|
|
||||
|
function next(data: any) { |
||||
|
Form.value?.done(); |
||||
|
close(); |
||||
|
|
||||
|
return crud.refresh({ |
||||
|
...data, |
||||
|
...params, |
||||
|
page: 1 |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
if (config.onSearch) { |
||||
|
config.onSearch(form, { next, close }); |
||||
|
} else { |
||||
|
next(form); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 消息事件
|
||||
|
mitt.on("crud.openAdvSearch", open); |
||||
|
|
||||
|
// 渲染表单
|
||||
|
function renderForm() { |
||||
|
return h(<cl-form ref={Form} inner enable-plugin={false} />, {}, 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, |
||||
|
...useApi({ Form }), |
||||
|
reset, |
||||
|
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%" : config.size}> |
||||
|
<div class="cl-adv-search__header"> |
||||
|
<span class="text">{config.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> |
||||
|
); |
||||
|
}; |
||||
|
} |
||||
|
}); |
||||
@ -0,0 +1,282 @@ |
|||||
|
import { |
||||
|
defineComponent, |
||||
|
nextTick, |
||||
|
onMounted, |
||||
|
reactive, |
||||
|
ref, |
||||
|
h, |
||||
|
render, |
||||
|
toRaw, |
||||
|
type PropType |
||||
|
} from "vue"; |
||||
|
import { isString } from "lodash-es"; |
||||
|
import { addClass, contains, removeClass } from "../../utils"; |
||||
|
import { useRefs } from "../../hooks"; |
||||
|
import { ElIcon } from "element-plus"; |
||||
|
import { ArrowRight } from "@element-plus/icons-vue"; |
||||
|
|
||||
|
const ClContextMenu = defineComponent({ |
||||
|
name: "cl-context-menu", |
||||
|
|
||||
|
props: { |
||||
|
show: Boolean, |
||||
|
options: { |
||||
|
type: Object as PropType<ClContextMenu.Options>, |
||||
|
default: () => ({}) |
||||
|
}, |
||||
|
event: { |
||||
|
type: Object, |
||||
|
default: () => ({}) |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
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: any) { |
||||
|
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; |
||||
|
|
||||
|
// 关闭
|
||||
|
function close() { |
||||
|
visible.value = false; |
||||
|
ids.value = ""; |
||||
|
|
||||
|
if (targetEl) { |
||||
|
removeClass(targetEl, "cl-context-menu__target"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 打开
|
||||
|
function open(event: any, options: ClContextMenu.Options = {}) { |
||||
|
// 阻止默认事件
|
||||
|
stopDefault(event); |
||||
|
|
||||
|
// 显示
|
||||
|
visible.value = true; |
||||
|
|
||||
|
// 元素
|
||||
|
const el = refs["context-menu"].querySelector(".cl-context-menu__box") as HTMLElement; |
||||
|
|
||||
|
// 点击样式
|
||||
|
if (options?.hover) { |
||||
|
const 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?.class) { |
||||
|
addClass(el, options.class); |
||||
|
} |
||||
|
|
||||
|
// 菜单列表
|
||||
|
if (options?.list) { |
||||
|
list.value = parseList(options.list); |
||||
|
} |
||||
|
|
||||
|
nextTick(() => { |
||||
|
// 计算位置
|
||||
|
let left = event.pageX; |
||||
|
let top = event.pageY; |
||||
|
|
||||
|
// 组件方式用 offset 计算
|
||||
|
if (!props.show) { |
||||
|
left = event.offsetX; |
||||
|
top = event.offsetY; |
||||
|
} |
||||
|
|
||||
|
const { clientHeight: h1, clientWidth: w1 } = event.target?.ownerDocument.body; |
||||
|
const { clientHeight: h2, clientWidth: w2 } = el; |
||||
|
|
||||
|
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}`; |
||||
|
|
||||
|
if (!e.suffixIcon) { |
||||
|
// 默认图标
|
||||
|
if (e.children) { |
||||
|
e.suffixIcon = ArrowRight; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return ( |
||||
|
<div |
||||
|
class={{ |
||||
|
"is-active": ids.value.includes(id), |
||||
|
"is-ellipsis": e.ellipsis ?? true, |
||||
|
"is-disabled": e.disabled |
||||
|
}} |
||||
|
onClick={(ev: MouseEvent) => { |
||||
|
rowClick(e, id); |
||||
|
ev.stopPropagation(); |
||||
|
}} |
||||
|
> |
||||
|
{/* 前缀图标 */} |
||||
|
{e.prefixIcon && <ElIcon>{h(toRaw(e.prefixIcon))}</ElIcon>} |
||||
|
|
||||
|
{/* 标题 */} |
||||
|
<span> |
||||
|
{e.label} |
||||
|
</span> |
||||
|
|
||||
|
{/* 后缀图标 */} |
||||
|
{e.suffixIcon && <ElIcon>{h(toRaw(e.suffixIcon))}</ElIcon>} |
||||
|
|
||||
|
{/* 子集 */} |
||||
|
{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 = h(ClContextMenu, { |
||||
|
show: true, |
||||
|
event, |
||||
|
options |
||||
|
}); |
||||
|
|
||||
|
render(vm, event.target.ownerDocument.createElement("div")); |
||||
|
|
||||
|
return vm.component?.exposed as ClContextMenu.Exposed; |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
export default ClContextMenu; |
||||
@ -0,0 +1,287 @@ |
|||||
|
import { ElMessageBox, ElMessage } from "element-plus"; |
||||
|
import { Mitt } from "../../utils/mitt"; |
||||
|
import { ref } from "vue"; |
||||
|
import { assign, isArray, isFunction } from "lodash-es"; |
||||
|
import { merge } from "../../utils"; |
||||
|
|
||||
|
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((success, error) => { |
||||
|
// 合并请求参数
|
||||
|
const reqParams = paramsReplace(assign(crud.params, params)); |
||||
|
|
||||
|
// Loading
|
||||
|
crud.loading = true; |
||||
|
|
||||
|
// 预防脏数据
|
||||
|
const rd = (refreshRd.value = Math.random()); |
||||
|
|
||||
|
// 完成事件
|
||||
|
function done() { |
||||
|
crud.loading = false; |
||||
|
} |
||||
|
|
||||
|
// 渲染
|
||||
|
function render(data: any | any[], pagination?: any) { |
||||
|
const res = isArray(data) ? { list: data, pagination } : data; |
||||
|
done(); |
||||
|
success(res); |
||||
|
mitt.emit("crud.refresh", res); |
||||
|
} |
||||
|
|
||||
|
// 下一步
|
||||
|
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)) { |
||||
|
res = { |
||||
|
list: res, |
||||
|
pagination: { |
||||
|
total: res.length |
||||
|
} |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
render(res); |
||||
|
resolve(res); |
||||
|
}) |
||||
|
.catch((err) => { |
||||
|
ElMessage.error(err.message); |
||||
|
error(err); |
||||
|
reject(err); |
||||
|
}); |
||||
|
|
||||
|
done(); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
// 刷新钩子
|
||||
|
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 setParams(data: obj) { |
||||
|
merge(crud.params, data); |
||||
|
} |
||||
|
|
||||
|
// 设置
|
||||
|
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, |
||||
|
setParams |
||||
|
}; |
||||
|
} |
||||
@ -0,0 +1,91 @@ |
|||||
|
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: {}, |
||||
|
// 事件
|
||||
|
mitt, |
||||
|
// 配置
|
||||
|
config |
||||
|
}, |
||||
|
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> |
||||
|
); |
||||
|
}; |
||||
|
} |
||||
|
}); |
||||
@ -0,0 +1,288 @@ |
|||||
|
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, |
||||
|
// 是否需要滚动条
|
||||
|
scrollbar: { |
||||
|
type: Boolean, |
||||
|
default: true |
||||
|
}, |
||||
|
// 背景透明
|
||||
|
transparent: Boolean |
||||
|
}, |
||||
|
|
||||
|
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", { 'is-transparent': props.transparent }]} |
||||
|
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() { |
||||
|
const height = isFullscreen.value ? "100%" : props.height; |
||||
|
|
||||
|
const style = { |
||||
|
padding: props.padding, |
||||
|
height |
||||
|
}; |
||||
|
|
||||
|
function content() { |
||||
|
return ( |
||||
|
<div class="cl-dialog__default" style={style} key={cacheKey.value}> |
||||
|
{slots.default?.()} |
||||
|
</div> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
if (props.scrollbar) { |
||||
|
style.height = "auto"; |
||||
|
|
||||
|
return <el-scrollbar height={height}>{content()}</el-scrollbar>; |
||||
|
} else { |
||||
|
return content(); |
||||
|
} |
||||
|
}, |
||||
|
footer() { |
||||
|
const d = slots.footer?.(); |
||||
|
|
||||
|
if (d && d[0]?.shapeFlag) { |
||||
|
return <div class="cl-dialog__footer">{d}</div>; |
||||
|
} |
||||
|
|
||||
|
return null; |
||||
|
} |
||||
|
} |
||||
|
); |
||||
|
}; |
||||
|
} |
||||
|
}); |
||||
@ -0,0 +1,17 @@ |
|||||
|
import { defineComponent } from "vue"; |
||||
|
|
||||
|
export default defineComponent({ |
||||
|
name: "cl-error-message", |
||||
|
|
||||
|
props: { |
||||
|
title: String |
||||
|
}, |
||||
|
|
||||
|
setup(props) { |
||||
|
return () => { |
||||
|
return <div class="cl-error-message"> |
||||
|
{props.title} |
||||
|
</div>; |
||||
|
}; |
||||
|
} |
||||
|
}); |
||||
@ -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> |
||||
|
); |
||||
|
}; |
||||
|
} |
||||
|
}); |
||||
@ -0,0 +1,11 @@ |
|||||
|
import { defineComponent } from "vue"; |
||||
|
|
||||
|
export default defineComponent({ |
||||
|
name: "cl-flex1", |
||||
|
|
||||
|
setup() { |
||||
|
return () => { |
||||
|
return <div class="cl-flex1" />; |
||||
|
}; |
||||
|
} |
||||
|
}); |
||||
@ -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> |
||||
|
); |
||||
|
}; |
||||
|
} |
||||
|
}); |
||||
@ -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> |
||||
|
); |
||||
|
}; |
||||
|
} |
||||
|
}); |
||||
@ -0,0 +1,146 @@ |
|||||
|
import { assign } from "lodash-es"; |
||||
|
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 |
||||
|
) { |
||||
|
const 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": |
||||
|
assign(d.component.props, data); |
||||
|
break; |
||||
|
|
||||
|
case "hidden": |
||||
|
d.hidden = data; |
||||
|
break; |
||||
|
|
||||
|
case "hidden-toggle": |
||||
|
d.hidden = data === undefined ? !d.hidden : !data; |
||||
|
break; |
||||
|
|
||||
|
default: |
||||
|
assign(d, data); |
||||
|
break; |
||||
|
} |
||||
|
} else { |
||||
|
console.error(`[set] ${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 |
||||
|
}; |
||||
|
} |
||||
@ -0,0 +1,36 @@ |
|||||
|
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", |
||||
|
"invokeData", |
||||
|
"setData", |
||||
|
"setConfig", |
||||
|
"setOptions", |
||||
|
"setProps", |
||||
|
"toggleItem", |
||||
|
"hideItem", |
||||
|
"showItem", |
||||
|
"validate", |
||||
|
"validateField", |
||||
|
"resetFields", |
||||
|
"scrollToField", |
||||
|
"clearValidate", |
||||
|
"fields" |
||||
|
], |
||||
|
Form |
||||
|
); |
||||
|
} |
||||
@ -0,0 +1,85 @@ |
|||||
|
import { reactive, ref, watch } from "vue"; |
||||
|
import { useConfig } from "../../../hooks"; |
||||
|
import { cloneDeep } from "lodash-es"; |
||||
|
|
||||
|
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 oldForm = ref<obj>({}); |
||||
|
|
||||
|
// 表单是否可见
|
||||
|
const visible = ref(false); |
||||
|
|
||||
|
// 表单提交保存状态
|
||||
|
const saving = ref(false); |
||||
|
|
||||
|
// 表单加载状态
|
||||
|
const loading = ref(false); |
||||
|
|
||||
|
// 表单禁用状态
|
||||
|
const disabled = ref(false); |
||||
|
|
||||
|
// 监听表单变化
|
||||
|
watch( |
||||
|
() => form, |
||||
|
(val) => { |
||||
|
if (config.on?.change) { |
||||
|
for (const i in val) { |
||||
|
if (form[i] !== oldForm.value[i]) { |
||||
|
config.on?.change(val, i); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
oldForm.value = cloneDeep(val); |
||||
|
}, |
||||
|
{ |
||||
|
deep: true |
||||
|
} |
||||
|
); |
||||
|
|
||||
|
return { |
||||
|
Form, |
||||
|
config, |
||||
|
form, |
||||
|
visible, |
||||
|
saving, |
||||
|
loading, |
||||
|
disabled |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
export * from "./action"; |
||||
|
export * from "./api"; |
||||
|
export * from "./plugins"; |
||||
|
export * from "./tabs"; |
||||
@ -0,0 +1,92 @@ |
|||||
|
import { type Ref, type WatchStopHandle, getCurrentInstance, watch } from "vue"; |
||||
|
import { useConfig } from "../../../hooks"; |
||||
|
import { uniqueFns } from "../../../utils"; |
||||
|
|
||||
|
export function usePlugins(enable: boolean, { visible }: { visible: Ref<boolean> }) { |
||||
|
const that: any = getCurrentInstance(); |
||||
|
const { style } = useConfig(); |
||||
|
|
||||
|
interface Event { |
||||
|
onOpen: (() => void)[]; |
||||
|
onClose: (() => void)[]; |
||||
|
onSubmit: ((data: obj) => Promise<obj> | obj)[]; |
||||
|
[key: string]: any; |
||||
|
} |
||||
|
|
||||
|
// 事件
|
||||
|
const ev: Event = { |
||||
|
onOpen: [], |
||||
|
onClose: [], |
||||
|
onSubmit: [] |
||||
|
}; |
||||
|
|
||||
|
// 监听器
|
||||
|
let timer: WatchStopHandle | null = null; |
||||
|
|
||||
|
// 插件创建
|
||||
|
function create(plugins: ClForm.Plugin[] = []) { |
||||
|
if (!enable) { |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
for (const i in ev) { |
||||
|
ev[i] = []; |
||||
|
} |
||||
|
|
||||
|
// 停止监听
|
||||
|
if (timer) { |
||||
|
timer(); |
||||
|
} |
||||
|
|
||||
|
// 执行
|
||||
|
uniqueFns([...(style.form.plugins || []), ...plugins]).forEach((p) => { |
||||
|
const d: any = { |
||||
|
exposed: that.exposed |
||||
|
}; |
||||
|
|
||||
|
for (const i in ev) { |
||||
|
d[i] = (cb: any) => { |
||||
|
ev[i].push(cb); |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
p(d); |
||||
|
}); |
||||
|
|
||||
|
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 |
||||
|
}; |
||||
|
} |
||||
@ -0,0 +1,151 @@ |
|||||
|
import { computed, ref } from "vue"; |
||||
|
|
||||
|
export function useTabs({ config, Form }: { config: ClForm.Config; Form: Vue.Ref<any> }) { |
||||
|
// 选中
|
||||
|
const active = ref<string | undefined>(); |
||||
|
|
||||
|
// 列表
|
||||
|
const list = computed(() => { |
||||
|
return get()?.props?.labels || []; |
||||
|
}); |
||||
|
|
||||
|
// 获取选项
|
||||
|
function getItem(value: any) { |
||||
|
return list.value.find((e) => e.value == value); |
||||
|
} |
||||
|
|
||||
|
// 是否已加载
|
||||
|
function isLoaded(value: any) { |
||||
|
const d = getItem(value); |
||||
|
return d?.lazy ? d.loaded : true; |
||||
|
} |
||||
|
|
||||
|
// 加载后
|
||||
|
function onLoad(value: any) { |
||||
|
const d = getItem(value); |
||||
|
d!.loaded = true; |
||||
|
} |
||||
|
|
||||
|
// 查找分组
|
||||
|
function toGroup(opts: { config: ClForm.Config; prop: string; refs: any }) { |
||||
|
if (active.value) { |
||||
|
let name; |
||||
|
|
||||
|
// 查找标签上绑定的数据
|
||||
|
const el = opts.refs.form.querySelector(`[data-prop="${opts.prop}"]`); |
||||
|
|
||||
|
// 各自判断
|
||||
|
if (el) { |
||||
|
name = el?.getAttribute("data-group"); |
||||
|
} else { |
||||
|
function deep(d: ClForm.Item) { |
||||
|
if (d.prop == opts.prop) { |
||||
|
name = d.group; |
||||
|
} else { |
||||
|
if (d.children) { |
||||
|
d.children.forEach(deep); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
config.items.forEach(deep); |
||||
|
} |
||||
|
|
||||
|
if (name) { |
||||
|
set(name); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 获取参数
|
||||
|
function get() { |
||||
|
return config.items.find((e) => e.type === "tabs"); |
||||
|
} |
||||
|
|
||||
|
// 设置参数
|
||||
|
function set(data: any) { |
||||
|
active.value = data; |
||||
|
} |
||||
|
|
||||
|
// 清空
|
||||
|
function clear() { |
||||
|
// 清空选中
|
||||
|
active.value = undefined; |
||||
|
|
||||
|
// 清空加载状态
|
||||
|
list.value.forEach((e) => { |
||||
|
if (e.lazy && e.loaded) { |
||||
|
e.loaded = undefined; |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
// 切换
|
||||
|
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) => e.group == active.value && !e._hidden && e.prop) |
||||
|
.map((e) => { |
||||
|
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, |
||||
|
list, |
||||
|
isLoaded, |
||||
|
onLoad, |
||||
|
get, |
||||
|
set, |
||||
|
change, |
||||
|
clear, |
||||
|
mergeProp, |
||||
|
toGroup |
||||
|
}; |
||||
|
} |
||||
@ -0,0 +1,683 @@ |
|||||
|
import { defineComponent, h, nextTick } from "vue"; |
||||
|
import { assign, cloneDeep, isBoolean, isFunction, keys } from "lodash-es"; |
||||
|
import { useAction, useForm, usePlugins, useTabs } from "./helper"; |
||||
|
import { useBrowser, useConfig, useElApi, useRefs } from "../../hooks"; |
||||
|
import { getValue, merge } from "../../utils"; |
||||
|
import formHook from "../../utils/form-hook"; |
||||
|
import { renderNode } from "../../utils/vnode"; |
||||
|
|
||||
|
export default defineComponent({ |
||||
|
name: "cl-form", |
||||
|
|
||||
|
props: { |
||||
|
name: String, |
||||
|
inner: Boolean, |
||||
|
inline: Boolean, |
||||
|
enablePlugin: { |
||||
|
type: Boolean, |
||||
|
default: true |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
setup(props, { expose, slots }) { |
||||
|
const { refs, setRefs } = useRefs(); |
||||
|
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", |
||||
|
"fields" |
||||
|
], |
||||
|
Form |
||||
|
); |
||||
|
|
||||
|
// 插件
|
||||
|
const plugin = usePlugins(props.enablePlugin, { 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 invokeData(d: any) { |
||||
|
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) => { |
||||
|
if (!f[e]) { |
||||
|
f[e] = {}; |
||||
|
} |
||||
|
|
||||
|
f = f[e]; |
||||
|
}); |
||||
|
|
||||
|
// 设置关键值
|
||||
|
f[k] = d[i]; |
||||
|
|
||||
|
delete d[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) => { |
||||
|
function deep(e: ClForm.Item) { |
||||
|
if (e.prop) { |
||||
|
// 过滤隐藏的表单项
|
||||
|
if (e._hidden) { |
||||
|
if (e.prop) { |
||||
|
delete d[e.prop]; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// hook 提交处理
|
||||
|
if (e.hook) { |
||||
|
formHook.submit({ |
||||
|
...e, |
||||
|
value: e.prop ? d[e.prop] : undefined, |
||||
|
form: d |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (e.children) { |
||||
|
e.children.forEach(deep); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
deep(e); |
||||
|
}); |
||||
|
|
||||
|
// 处理数据
|
||||
|
invokeData(d); |
||||
|
|
||||
|
const submit = callback || config.on?.submit; |
||||
|
|
||||
|
// 提交事件
|
||||
|
if (submit) { |
||||
|
submit(await plugin.submit(d), { |
||||
|
close() { |
||||
|
close("save"); |
||||
|
}, |
||||
|
done |
||||
|
}); |
||||
|
} else { |
||||
|
done(); |
||||
|
} |
||||
|
} else { |
||||
|
// 切换到对应的选项卡
|
||||
|
Tabs.toGroup({ |
||||
|
refs, |
||||
|
config, |
||||
|
prop: keys(error)[0] |
||||
|
}); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
// 打开表单
|
||||
|
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.forEach((e) => { |
||||
|
function deep(e: ClForm.Item) { |
||||
|
if (e.prop) { |
||||
|
// 解析 prop
|
||||
|
if (e.prop.includes(".")) { |
||||
|
e.prop = e.prop.replace(/\./g, "-"); |
||||
|
} |
||||
|
|
||||
|
// prop 合并
|
||||
|
Tabs.mergeProp(e); |
||||
|
|
||||
|
// hook 绑定值
|
||||
|
formHook.bind({ |
||||
|
...e, |
||||
|
value: form[e.prop] !== undefined ? form[e.prop] : cloneDeep(e.value), |
||||
|
form |
||||
|
}); |
||||
|
|
||||
|
// 表单验证
|
||||
|
if (e.required) { |
||||
|
e.rules = { |
||||
|
required: true, |
||||
|
message: dict.label.nonEmpty.replace("{label}", e.label || "") |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 设置 tabs 默认值
|
||||
|
if (e.type == "tabs") { |
||||
|
Tabs.set(e.value); |
||||
|
} |
||||
|
|
||||
|
// 子集
|
||||
|
if (e.children) { |
||||
|
e.children.forEach(deep); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
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) => { |
||||
|
function deep(e: ClForm.Item) { |
||||
|
formHook.bind({ |
||||
|
...e, |
||||
|
value: e.prop ? data[e.prop] : undefined, |
||||
|
form: data |
||||
|
}); |
||||
|
|
||||
|
if (e.children) { |
||||
|
e.children.forEach(deep); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
deep(e); |
||||
|
}); |
||||
|
|
||||
|
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} onChange={Tabs.onLoad} /> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
// 是否隐藏
|
||||
|
e._hidden = parseHidden(e.hidden, { |
||||
|
scope: form |
||||
|
}); |
||||
|
|
||||
|
// 分组显示
|
||||
|
const inGroup = e.group ? e.group === Tabs.active.value : true; |
||||
|
|
||||
|
// 是否已加载完成
|
||||
|
const isLoaded = e.component && Tabs.isLoaded(e.group); |
||||
|
|
||||
|
// 表单项
|
||||
|
const FormItem = h( |
||||
|
<el-form-item |
||||
|
class={{ |
||||
|
"no-label": !(e.renderLabel || e.label), |
||||
|
"has-children": !!e.children |
||||
|
}} |
||||
|
key={e.prop} |
||||
|
data-group={e.group || "-"} |
||||
|
data-prop={e.prop || "-"} |
||||
|
label-width={props.inline ? "auto" : ""} |
||||
|
label={e.label} |
||||
|
prop={e.prop} |
||||
|
rules={isDisabled ? null : e.rules} |
||||
|
required={e._hidden ? false : e.required} |
||||
|
v-show={inGroup && !e._hidden} |
||||
|
/>, |
||||
|
e.props, |
||||
|
{ |
||||
|
label() { |
||||
|
if (e.renderLabel) { |
||||
|
return renderNode(e.renderLabel, { |
||||
|
scope: form, |
||||
|
render: "slot", |
||||
|
slots |
||||
|
}); |
||||
|
} else { |
||||
|
return 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> |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
); |
||||
|
|
||||
|
let span = e.span || style.form.span; |
||||
|
|
||||
|
if (browser.isMini) { |
||||
|
span = 24; |
||||
|
} |
||||
|
|
||||
|
// 是否行内
|
||||
|
const Item = props.inline ? ( |
||||
|
FormItem |
||||
|
) : ( |
||||
|
<el-col span={span} {...e.col} v-show={inGroup && !e._hidden}> |
||||
|
{FormItem} |
||||
|
</el-col> |
||||
|
); |
||||
|
|
||||
|
return isLoaded ? Item : null; |
||||
|
} |
||||
|
|
||||
|
// 渲染表单
|
||||
|
function renderContainer() { |
||||
|
// 表单项列表
|
||||
|
const children = config.items.map(renderFormItem); |
||||
|
|
||||
|
// 表单标签位置
|
||||
|
const labelPosition = |
||||
|
browser.isMini && !props.inline |
||||
|
? "top" |
||||
|
: config.props.labelPosition || style.form.labelPosition; |
||||
|
|
||||
|
return ( |
||||
|
<div class="cl-form__container" ref={setRefs("form")}> |
||||
|
{h( |
||||
|
<el-form |
||||
|
ref={Form} |
||||
|
size={style.size} |
||||
|
label-width={style.form.labelWidth} |
||||
|
inline={props.inline} |
||||
|
require-asterisk-position="right" |
||||
|
disabled={saving.value} |
||||
|
scroll-to-error |
||||
|
model={form} |
||||
|
onSubmit={(e: Event) => { |
||||
|
submit(); |
||||
|
e.preventDefault(); |
||||
|
}} |
||||
|
/>, |
||||
|
{ |
||||
|
...config.props, |
||||
|
labelPosition |
||||
|
}, |
||||
|
{ |
||||
|
default: () => { |
||||
|
const items = [ |
||||
|
slots.prepend && slots.prepend({ scope: form }), |
||||
|
children, |
||||
|
slots.append && slots.append({ scope: form }) |
||||
|
]; |
||||
|
|
||||
|
return ( |
||||
|
<div class="cl-form__items" v-loading={loading.value}> |
||||
|
{props.inline ? ( |
||||
|
items |
||||
|
) : ( |
||||
|
<el-row gutter={10}>{items}</el-row> |
||||
|
)} |
||||
|
</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() { |
||||
|
return ( |
||||
|
<el-button |
||||
|
type={e.type} |
||||
|
{...e.props} |
||||
|
onClick={() => { |
||||
|
e.onClick({ scope: form }); |
||||
|
}}> |
||||
|
{e.label} |
||||
|
</el-button> |
||||
|
); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
return ( |
||||
|
<div |
||||
|
class="cl-form__footer" |
||||
|
style={{ |
||||
|
justifyContent: justify || "flex-end" |
||||
|
}}> |
||||
|
{Btns} |
||||
|
</div> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
// Tools
|
||||
|
function parseHidden(value: any, { scope }: any) { |
||||
|
if (isBoolean(value)) { |
||||
|
return value; |
||||
|
} else if (isFunction(value)) { |
||||
|
return value({ scope }); |
||||
|
} |
||||
|
|
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
const ctx = { |
||||
|
name: props.name, |
||||
|
refs, |
||||
|
Form, |
||||
|
visible, |
||||
|
saving, |
||||
|
form, |
||||
|
config, |
||||
|
loading, |
||||
|
disabled, |
||||
|
open, |
||||
|
close, |
||||
|
done, |
||||
|
clear, |
||||
|
reset, |
||||
|
submit, |
||||
|
invokeData, |
||||
|
bindForm, |
||||
|
showLoading, |
||||
|
hideLoading, |
||||
|
setDisabled, |
||||
|
Tabs, |
||||
|
...Action, |
||||
|
...ElFormApi |
||||
|
}; |
||||
|
|
||||
|
expose(ctx); |
||||
|
|
||||
|
return () => { |
||||
|
if (props.inner) { |
||||
|
return ( |
||||
|
visible.value && ( |
||||
|
<div class="cl-form"> |
||||
|
{renderContainer()} |
||||
|
{renderFooter()} |
||||
|
</div> |
||||
|
) |
||||
|
); |
||||
|
} 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(); |
||||
|
} |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
}; |
||||
|
} |
||||
|
}); |
||||
@ -0,0 +1,50 @@ |
|||||
|
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"; |
||||
|
import ContextMenu from "./context-menu"; |
||||
|
|
||||
|
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, |
||||
|
ContextMenu |
||||
|
}; |
||||
|
|
||||
|
export function useComponent(app: App) { |
||||
|
for (const i in components) { |
||||
|
app.component(components[i].name, components[i]); |
||||
|
} |
||||
|
} |
||||
@ -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> |
||||
|
) |
||||
|
); |
||||
|
}; |
||||
|
} |
||||
|
}); |
||||
@ -0,0 +1,90 @@ |
|||||
|
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 |
||||
|
class="cl-pagination" |
||||
|
size={browser.isMini ? 'small' : style.size} |
||||
|
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 |
||||
|
} |
||||
|
); |
||||
|
}; |
||||
|
} |
||||
|
}); |
||||
@ -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> |
||||
|
); |
||||
|
}; |
||||
|
} |
||||
|
}); |
||||
@ -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>; |
||||
|
}; |
||||
|
} |
||||
|
}); |
||||
@ -0,0 +1,178 @@ |
|||||
|
import { defineComponent, ref, computed, type PropType, useModel } from "vue"; |
||||
|
import { useConfig, useCore } from "../../hooks"; |
||||
|
import { parsePx } from "../../utils"; |
||||
|
import { debounce } from "lodash-es"; |
||||
|
|
||||
|
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: 280 |
||||
|
}, |
||||
|
// 是否实时刷新
|
||||
|
refreshOnInput: Boolean |
||||
|
}, |
||||
|
|
||||
|
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 = useModel(props, "modelValue"); |
||||
|
|
||||
|
// 锁
|
||||
|
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 |
||||
|
}) |
||||
|
.catch(err => { |
||||
|
console.error(err); |
||||
|
}) |
||||
|
|
||||
|
loading.value = false; |
||||
|
} |
||||
|
|
||||
|
if (props.onSearch) { |
||||
|
props.onSearch(params, { next }); |
||||
|
} else { |
||||
|
next(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 回车搜索
|
||||
|
function onKeydown({ key }: KeyboardEvent) { |
||||
|
if (key === "Enter") { |
||||
|
search(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 监听变化
|
||||
|
function onChange(val: string) { |
||||
|
if (!props.refreshOnInput) { |
||||
|
search(); |
||||
|
lock = true; |
||||
|
|
||||
|
setTimeout(() => { |
||||
|
lock = false; |
||||
|
}, 300); |
||||
|
|
||||
|
emit("change", val); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 监听输入
|
||||
|
const onInput = debounce((val: string) => { |
||||
|
emit("change", val); |
||||
|
|
||||
|
if (props.refreshOnInput) { |
||||
|
search(); |
||||
|
} |
||||
|
}, 300); |
||||
|
|
||||
|
// 监听字段选择
|
||||
|
function onFieldChange() { |
||||
|
emit("field-change", selectField.value); |
||||
|
value.value = undefined; |
||||
|
} |
||||
|
|
||||
|
expose({ |
||||
|
search |
||||
|
}); |
||||
|
|
||||
|
return () => { |
||||
|
return ( |
||||
|
<div class="cl-search-key"> |
||||
|
<el-select |
||||
|
class="cl-search-key__select" |
||||
|
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} |
||||
|
onChange={onChange} |
||||
|
onInput={onInput} |
||||
|
clearable |
||||
|
/> |
||||
|
|
||||
|
<el-button |
||||
|
size={style.size} |
||||
|
type="primary" |
||||
|
loading={loading.value} |
||||
|
onClick={search}> |
||||
|
{crud.dict.label.search} |
||||
|
</el-button> |
||||
|
</div> |
||||
|
</div> |
||||
|
); |
||||
|
}; |
||||
|
} |
||||
|
}); |
||||
@ -0,0 +1,21 @@ |
|||||
|
import { getCurrentInstance } from "vue"; |
||||
|
import { useConfig } from "../../../hooks"; |
||||
|
import { uniqueFns } from "../../../utils"; |
||||
|
|
||||
|
export function usePlugins() { |
||||
|
const that: any = getCurrentInstance(); |
||||
|
const { style } = useConfig(); |
||||
|
|
||||
|
// 插件创建
|
||||
|
function create(plugins: ClSearch.Plugin[] = []) { |
||||
|
uniqueFns([...(style.search?.plugins || []), ...plugins]).forEach((p) => { |
||||
|
p({ |
||||
|
exposed: that.exposed |
||||
|
}); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
return { |
||||
|
create |
||||
|
}; |
||||
|
} |
||||
@ -0,0 +1,298 @@ |
|||||
|
import { useConfig, useCore, useForm, useProxy, useRefs } from "../../hooks"; |
||||
|
import { |
||||
|
onMounted, |
||||
|
PropType, |
||||
|
defineComponent, |
||||
|
ref, |
||||
|
h, |
||||
|
reactive, |
||||
|
inject, |
||||
|
mergeProps, |
||||
|
nextTick, |
||||
|
onUnmounted |
||||
|
} from "vue"; |
||||
|
import { useApi } from "../form/helper"; |
||||
|
import { Search, Refresh, Bottom, Top } from "@element-plus/icons-vue"; |
||||
|
import { mitt } from "../../utils/mitt"; |
||||
|
import { isArray, isEmpty } from "lodash-es"; |
||||
|
import { usePlugins } from "./helper/plugins"; |
||||
|
|
||||
|
export default defineComponent({ |
||||
|
name: "cl-search", |
||||
|
|
||||
|
props: { |
||||
|
// 是否行内
|
||||
|
inline: { |
||||
|
type: Boolean, |
||||
|
default: true |
||||
|
}, |
||||
|
|
||||
|
// cl-form 表单配置
|
||||
|
props: { |
||||
|
type: Object, |
||||
|
default: () => ({}) |
||||
|
}, |
||||
|
|
||||
|
// 表单值
|
||||
|
data: { |
||||
|
type: Object, |
||||
|
default: () => ({}) |
||||
|
}, |
||||
|
|
||||
|
// 列
|
||||
|
items: { |
||||
|
type: Array as PropType<ClForm.Item[]>, |
||||
|
default: () => [] |
||||
|
}, |
||||
|
|
||||
|
// 是否需要重置按钮
|
||||
|
resetBtn: { |
||||
|
type: Boolean, |
||||
|
default: false |
||||
|
}, |
||||
|
|
||||
|
// 是否需要折叠
|
||||
|
collapse: { |
||||
|
type: Boolean, |
||||
|
default: false |
||||
|
}, |
||||
|
|
||||
|
// 初始化
|
||||
|
onLoad: Function, |
||||
|
|
||||
|
// 搜索时钩子
|
||||
|
onSearch: Function |
||||
|
}, |
||||
|
|
||||
|
emits: ["reset"], |
||||
|
|
||||
|
setup(props, { slots, expose, emit }) { |
||||
|
const { crud } = useCore(); |
||||
|
const { refs, setRefs } = useRefs() |
||||
|
const { style } = useConfig(); |
||||
|
const plugin = usePlugins() |
||||
|
|
||||
|
// 配置
|
||||
|
const config = reactive<ClSearch.Config>( |
||||
|
mergeProps(props, inject("useSearch__options") || {}) |
||||
|
); |
||||
|
|
||||
|
// cl-form
|
||||
|
const Form = useForm(); |
||||
|
|
||||
|
// 加载中
|
||||
|
const loading = ref(false); |
||||
|
|
||||
|
// 展开
|
||||
|
const isExpand = ref(!config.collapse); |
||||
|
|
||||
|
// 显示展开、收起按钮
|
||||
|
const showExpandBtn = 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() { |
||||
|
const d: any = {}; |
||||
|
|
||||
|
config.items?.map((e) => { |
||||
|
if (typeof e.hook != 'string' && e.hook?.reset) { |
||||
|
const props = e.hook.reset(e.prop!) |
||||
|
|
||||
|
if (isArray(props)) { |
||||
|
props.forEach((prop) => { |
||||
|
d[prop] = undefined; |
||||
|
}) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
d[e.prop!] = undefined; |
||||
|
}); |
||||
|
|
||||
|
// 重置表单
|
||||
|
Form.value?.reset(); |
||||
|
|
||||
|
// 列表刷新
|
||||
|
search(d); |
||||
|
|
||||
|
// 重置事件
|
||||
|
emit("reset", d); |
||||
|
} |
||||
|
|
||||
|
// 收起、展开
|
||||
|
function expand() { |
||||
|
isExpand.value = !isExpand.value; |
||||
|
|
||||
|
nextTick(() => { |
||||
|
crud?.["cl-table"].calcMaxHeight() |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
// 判断展开状态
|
||||
|
function onExpand() { |
||||
|
if (config.collapse) { |
||||
|
const el = refs.form?.querySelector(".cl-form__items"); |
||||
|
|
||||
|
if (el) { |
||||
|
showExpandBtn.value = el.clientHeight > 84; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function onResize() { |
||||
|
onExpand(); |
||||
|
} |
||||
|
|
||||
|
const ctx = { |
||||
|
search, |
||||
|
reset, |
||||
|
Form, |
||||
|
config, |
||||
|
...useApi({ Form }) |
||||
|
}; |
||||
|
|
||||
|
useProxy(ctx); |
||||
|
expose(ctx); |
||||
|
plugin.create(config.plugins); |
||||
|
|
||||
|
onMounted(() => { |
||||
|
Form.value?.open({ |
||||
|
op: { |
||||
|
hidden: true |
||||
|
}, |
||||
|
props: { |
||||
|
labelPosition: 'right', |
||||
|
...config.props |
||||
|
}, |
||||
|
items: config.items?.map(e => { |
||||
|
return { |
||||
|
col: { |
||||
|
sm: 12, |
||||
|
md: 8, |
||||
|
xs: 24, |
||||
|
lg: 6, |
||||
|
}, |
||||
|
...e, |
||||
|
} |
||||
|
}), |
||||
|
form: config.data, |
||||
|
on: { |
||||
|
open(data) { |
||||
|
config.onLoad?.(data); |
||||
|
onExpand(); |
||||
|
}, |
||||
|
change(data, prop) { |
||||
|
config.onChange?.(data, prop); |
||||
|
} |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
mitt.on("resize", onResize); |
||||
|
}); |
||||
|
|
||||
|
onUnmounted(() => { |
||||
|
mitt.off("resize", onResize); |
||||
|
}) |
||||
|
|
||||
|
return () => { |
||||
|
const btnEl = ( |
||||
|
<el-form-item label=" " class="cl-search__btns"> |
||||
|
{/* 重置按钮 */} |
||||
|
{config.resetBtn && ( |
||||
|
<el-button size={style.size} icon={Refresh} onClick={reset}> |
||||
|
{crud.dict.label.reset} |
||||
|
</el-button> |
||||
|
)} |
||||
|
|
||||
|
{/* 搜索按钮 */} |
||||
|
<el-button |
||||
|
type="primary" |
||||
|
loading={loading.value} |
||||
|
size={style.size} |
||||
|
icon={Search} |
||||
|
onClick={() => { |
||||
|
search(); |
||||
|
}}> |
||||
|
{crud.dict.label.search} |
||||
|
</el-button> |
||||
|
|
||||
|
{/* 自定义按钮 */} |
||||
|
{slots?.buttons?.(Form.value?.form)} |
||||
|
</el-form-item> |
||||
|
); |
||||
|
|
||||
|
return ( |
||||
|
<div class={["cl-search", isExpand.value ? "is-expand" : "is-fold", { |
||||
|
'is-inline': config.inline, |
||||
|
'is-collapse': config.collapse, |
||||
|
}]}> |
||||
|
<div class="cl-search__form" ref={setRefs("form")}> |
||||
|
{h( |
||||
|
<cl-form |
||||
|
ref={Form} |
||||
|
inner |
||||
|
inline={config.inline} |
||||
|
enable-plugin={false} |
||||
|
name='search' |
||||
|
/>, |
||||
|
{}, |
||||
|
{ |
||||
|
append() { |
||||
|
return config.collapse ? null : (isEmpty(config.items) || btnEl); |
||||
|
}, |
||||
|
...slots |
||||
|
} |
||||
|
)} |
||||
|
</div> |
||||
|
|
||||
|
{config.collapse && ( |
||||
|
<div class="cl-search__more"> |
||||
|
{showExpandBtn.value && ( |
||||
|
<el-button onClick={expand}> |
||||
|
<span>{isExpand.value ? crud.dict.label.collapse : crud.dict.label.expand}</span> |
||||
|
<el-icon>{isExpand.value ? <Top /> : <Bottom />}</el-icon> |
||||
|
</el-button> |
||||
|
)} |
||||
|
|
||||
|
<cl-flex1 /> |
||||
|
|
||||
|
{btnEl} |
||||
|
</div> |
||||
|
)} |
||||
|
</div> |
||||
|
); |
||||
|
}; |
||||
|
} |
||||
|
}); |
||||
@ -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 |
||||
|
}; |
||||
|
} |
||||
@ -0,0 +1,91 @@ |
|||||
|
import { CloseBold, Search } from "@element-plus/icons-vue"; |
||||
|
import { h } from "vue"; |
||||
|
import { useCrud } from "../../../hooks"; |
||||
|
import { renderNode } from "../../../utils/vnode"; |
||||
|
|
||||
|
export function renderHeader(item: ClTable.Column, { scope, slots }: any) { |
||||
|
const crud = useCrud(); |
||||
|
|
||||
|
const slot = slots[`header-${item.prop}`]; |
||||
|
|
||||
|
if (slot) { |
||||
|
return slot({ |
||||
|
scope |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
if (!item.search || !item.search.component) { |
||||
|
return item.label; |
||||
|
} |
||||
|
|
||||
|
// 显示输入框
|
||||
|
function show(e: MouseEvent) { |
||||
|
item.search.isInput = true; |
||||
|
e.stopPropagation(); |
||||
|
} |
||||
|
|
||||
|
// 隐藏输入框
|
||||
|
function hide() { |
||||
|
if (item.search.value !== undefined) { |
||||
|
item.search.value = undefined; |
||||
|
refresh(); |
||||
|
} |
||||
|
|
||||
|
item.search.isInput = false; |
||||
|
} |
||||
|
|
||||
|
// 刷新
|
||||
|
function refresh(params?: any) { |
||||
|
const { value } = item.search; |
||||
|
|
||||
|
crud.value?.refresh({ |
||||
|
page: 1, |
||||
|
[item.prop]: value === "" ? undefined : value, |
||||
|
...params |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
// 文字
|
||||
|
const text = ( |
||||
|
<div class="cl-table-header__search-label" onClick={show}> |
||||
|
<el-icon size={14}>{item.search.icon?.() ?? <Search />}</el-icon> |
||||
|
|
||||
|
{item.renderLabel ? item.renderLabel(scope) : item.label} |
||||
|
</div> |
||||
|
); |
||||
|
|
||||
|
// 输入框
|
||||
|
const input = h(renderNode(item.search.component, { prop: item.prop }), { |
||||
|
clearable: true, |
||||
|
modelValue: item.search.value, |
||||
|
onVnodeMounted(vn) { |
||||
|
// 默认聚焦
|
||||
|
vn.component?.exposed?.focus?.(); |
||||
|
}, |
||||
|
onInput(val: any) { |
||||
|
item.search.value = val; |
||||
|
}, |
||||
|
onChange(val: any) { |
||||
|
item.search.value = val; |
||||
|
|
||||
|
// 更改时刷新列表
|
||||
|
if (item.search.refreshOnChange) { |
||||
|
refresh(); |
||||
|
} |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
return ( |
||||
|
<div class={["cl-table-header__search", { "is-input": item.search.isInput }]}> |
||||
|
<div class="cl-table-header__search-inner">{item.search.isInput ? input : text}</div> |
||||
|
|
||||
|
{item.search.isInput && ( |
||||
|
<div class="cl-table-header__search-close" onClick={hide}> |
||||
|
<el-icon> |
||||
|
<CloseBold /> |
||||
|
</el-icon> |
||||
|
</div> |
||||
|
)} |
||||
|
</div> |
||||
|
); |
||||
|
} |
||||
@ -0,0 +1,99 @@ |
|||||
|
import { debounce, last } from "lodash-es"; |
||||
|
import { nextTick, onActivated, onMounted, ref } from "vue"; |
||||
|
import { addClass, removeClass } 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; |
||||
|
|
||||
|
// 集合
|
||||
|
const 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; |
||||
|
} |
||||
|
|
||||
|
// 移除 cl-row--last
|
||||
|
arr.forEach((e) => { |
||||
|
removeClass(e, "cl-row--last"); |
||||
|
}); |
||||
|
|
||||
|
// 最后一个可视元素
|
||||
|
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 |
||||
|
}; |
||||
|
} |
||||
@ -0,0 +1,43 @@ |
|||||
|
import { inject, reactive, ref } from "vue"; |
||||
|
import { useConfig } from "../../../hooks"; |
||||
|
import { getValue, mergeConfig } from "../../../utils"; |
||||
|
import type { TableInstance } from "element-plus"; |
||||
|
|
||||
|
export function useTable(props: any) { |
||||
|
const { style } = useConfig(); |
||||
|
|
||||
|
const Table = ref<TableInstance>(); |
||||
|
|
||||
|
// 配置
|
||||
|
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; |
||||
|
|
||||
|
// 事件
|
||||
|
if (!config.on) { |
||||
|
config.on = {}; |
||||
|
} |
||||
|
|
||||
|
// 参数
|
||||
|
if (!config.props) { |
||||
|
config.props = {}; |
||||
|
} |
||||
|
|
||||
|
return { Table, config }; |
||||
|
} |
||||
|
|
||||
|
export * from "./data"; |
||||
|
export * from "./height"; |
||||
|
export * from "./op"; |
||||
|
export * from "./render"; |
||||
|
export * from "./row"; |
||||
|
export * from "./selection"; |
||||
|
export * from "./sort"; |
||||
|
export * from "./header"; |
||||
@ -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 |
||||
|
}; |
||||
|
} |
||||
@ -0,0 +1,22 @@ |
|||||
|
import { getCurrentInstance } from "vue"; |
||||
|
import { useConfig } from "../../../hooks"; |
||||
|
import { uniqueFns } from "../../../utils"; |
||||
|
|
||||
|
export function usePlugins() { |
||||
|
const that: any = getCurrentInstance(); |
||||
|
const { style } = useConfig(); |
||||
|
|
||||
|
// 插件创建
|
||||
|
function create(plugins: ClTable.Plugin[] = []) { |
||||
|
// 执行
|
||||
|
uniqueFns([...(style.table.plugins || []), ...plugins]).forEach((p) => { |
||||
|
p({ |
||||
|
exposed: that.exposed |
||||
|
}); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
return { |
||||
|
create |
||||
|
}; |
||||
|
} |
||||
@ -0,0 +1,327 @@ |
|||||
|
import { h, useSlots } from "vue"; |
||||
|
import { useCore, useBrowser, useConfig } from "../../../hooks"; |
||||
|
import { assign, cloneDeep, isArray, isEmpty, isObject, isString, orderBy } from "lodash-es"; |
||||
|
import { deepFind, getValue } from "../../../utils"; |
||||
|
import { renderNode } from "../../../utils/vnode"; |
||||
|
import { renderHeader } from "./header"; |
||||
|
|
||||
|
// 渲染
|
||||
|
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") { |
||||
|
const props = assign( |
||||
|
{ |
||||
|
label: crud.dict.label.op, |
||||
|
width: style.table.column.opWidth, |
||||
|
fixed: browser.isMini ? null : "right" |
||||
|
}, |
||||
|
item |
||||
|
); |
||||
|
|
||||
|
return h(ElTableColumn, props, { |
||||
|
default: (scope: any) => { |
||||
|
return ( |
||||
|
<div class="cl-table__op"> |
||||
|
{renderOpButtons(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) { |
||||
|
return renderHeader(item, { scope, slots }); |
||||
|
}, |
||||
|
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 (isObject(value)) { |
||||
|
return value; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 自定义渲染
|
||||
|
if (item.render) { |
||||
|
return item.render( |
||||
|
scope.row, |
||||
|
scope.column, |
||||
|
value, |
||||
|
scope.$index |
||||
|
); |
||||
|
} |
||||
|
// 自定义渲染2
|
||||
|
else 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 renderDict(value, item); |
||||
|
} |
||||
|
// 空数据
|
||||
|
else if (isEmpty(value)) { |
||||
|
return scope.emptyText; |
||||
|
} else { |
||||
|
return value; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
return deep(item); |
||||
|
} |
||||
|
}) |
||||
|
.filter(Boolean); |
||||
|
} |
||||
|
|
||||
|
// 渲染操作按钮
|
||||
|
function renderOpButtons(buttons: any, { scope }: any) { |
||||
|
const list = getValue(buttons || ["edit", "delete"], { scope }) as ClTable.OpButton; |
||||
|
|
||||
|
return list.map((vnode) => { |
||||
|
if (vnode === "info") { |
||||
|
return ( |
||||
|
<el-button |
||||
|
plain |
||||
|
size={style.size} |
||||
|
v-show={crud.getPermission("info")} |
||||
|
onClick={(e: MouseEvent) => { |
||||
|
crud.rowInfo(scope.row); |
||||
|
e.stopPropagation(); |
||||
|
}}> |
||||
|
{crud.dict.label?.info} |
||||
|
</el-button> |
||||
|
); |
||||
|
} else if (vnode === "edit") { |
||||
|
return ( |
||||
|
<el-button |
||||
|
text |
||||
|
type="primary" |
||||
|
size={style.size} |
||||
|
v-show={crud.getPermission("update")} |
||||
|
onClick={(e: MouseEvent) => { |
||||
|
crud.rowEdit(scope.row); |
||||
|
e.stopPropagation(); |
||||
|
}}> |
||||
|
{crud.dict.label?.update} |
||||
|
</el-button> |
||||
|
); |
||||
|
} else if (vnode === "delete") { |
||||
|
return ( |
||||
|
<el-button |
||||
|
text |
||||
|
type="danger" |
||||
|
size={style.size} |
||||
|
v-show={crud.getPermission("delete")} |
||||
|
onClick={(e: MouseEvent) => { |
||||
|
crud.rowDelete(scope.row); |
||||
|
e.stopPropagation(); |
||||
|
}}> |
||||
|
{crud.dict.label?.delete} |
||||
|
</el-button> |
||||
|
); |
||||
|
} else { |
||||
|
if (typeof vnode === "object") { |
||||
|
if (vnode.hidden) { |
||||
|
return null; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return renderNode(vnode, { |
||||
|
scope, |
||||
|
slots, |
||||
|
custom(vnode) { |
||||
|
return ( |
||||
|
<el-button |
||||
|
text |
||||
|
type={vnode.type} |
||||
|
{...vnode?.props} |
||||
|
onClick={(e: MouseEvent) => { |
||||
|
vnode.onClick({ scope }); |
||||
|
e.stopPropagation(); |
||||
|
}}> |
||||
|
{vnode.label} |
||||
|
</el-button> |
||||
|
); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
// 渲染字典
|
||||
|
function renderDict(value: any, item: ClTable.Column) { |
||||
|
// 选项列表
|
||||
|
const list = cloneDeep(item.dict || []) as DictOptions; |
||||
|
|
||||
|
// 字符串分隔符
|
||||
|
const separator = item.dictSeparator === undefined ? "," : item.dictSeparator; |
||||
|
|
||||
|
// 设置颜色
|
||||
|
if (item.dictColor) { |
||||
|
list.forEach((e, i) => { |
||||
|
if (!e.color) { |
||||
|
e.color = style.colors[i]; |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
// 绑定值
|
||||
|
let values: any[] = []; |
||||
|
|
||||
|
// 格式化值
|
||||
|
if (isArray(value)) { |
||||
|
values = value; |
||||
|
} else if (isString(value)) { |
||||
|
if (separator) { |
||||
|
values = value.split(separator); |
||||
|
} else { |
||||
|
values = [value]; |
||||
|
} |
||||
|
} else { |
||||
|
values = [value]; |
||||
|
} |
||||
|
|
||||
|
// 返回值
|
||||
|
const result = values |
||||
|
.filter((e) => e !== undefined && e !== null && e !== "") |
||||
|
.map((v) => { |
||||
|
const d = deepFind(v, list, { allLevels: item.dictAllLevels }) || { |
||||
|
label: v, |
||||
|
value: v |
||||
|
}; |
||||
|
|
||||
|
return { |
||||
|
...d, |
||||
|
children: [] |
||||
|
}; |
||||
|
}); |
||||
|
|
||||
|
// 格式化返回
|
||||
|
if (item.dictFormatter) { |
||||
|
return item.dictFormatter(result); |
||||
|
} else { |
||||
|
// tag 返回
|
||||
|
return result.map((e) => { |
||||
|
return h( |
||||
|
<el-tag disable-transitions style="margin: 2px; border: 0" />, |
||||
|
{ |
||||
|
type: e.type, |
||||
|
closable: e.closable, |
||||
|
hit: e.hit, |
||||
|
color: e.color, |
||||
|
size: e.size, |
||||
|
effect: e.effect || "dark", |
||||
|
round: e.round |
||||
|
}, |
||||
|
{ |
||||
|
default: () => <span>{e.label}</span> |
||||
|
} |
||||
|
); |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 插槽 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 |
||||
|
}; |
||||
|
} |
||||
@ -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 |
||||
|
}; |
||||
|
} |
||||
@ -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 |
||||
|
}; |
||||
|
} |
||||
@ -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 |
||||
|
}; |
||||
|
} |
||||
@ -0,0 +1,165 @@ |
|||||
|
import { defineComponent, h } from "vue"; |
||||
|
import { |
||||
|
useRow, |
||||
|
useHeight, |
||||
|
useRender, |
||||
|
useSort, |
||||
|
useData, |
||||
|
useSelection, |
||||
|
useOp, |
||||
|
useTable |
||||
|
} from "./helper"; |
||||
|
import { useCore, useProxy, useElApi, useConfig } from "../../hooks"; |
||||
|
import { usePlugins } from "./helper/plugins"; |
||||
|
|
||||
|
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 plugin = usePlugins(); |
||||
|
|
||||
|
// 排序
|
||||
|
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", |
||||
|
"updateKeyChildren" |
||||
|
], |
||||
|
Table |
||||
|
); |
||||
|
|
||||
|
const ctx = { |
||||
|
Table, |
||||
|
config, |
||||
|
columns: config.columns, |
||||
|
...Selection, |
||||
|
...Data, |
||||
|
...Sort, |
||||
|
...Row, |
||||
|
...Height, |
||||
|
...Op, |
||||
|
...ElTableApi |
||||
|
}; |
||||
|
|
||||
|
useProxy(ctx); |
||||
|
expose(ctx); |
||||
|
plugin.create(config.plugins); |
||||
|
|
||||
|
return () => { |
||||
|
const { renderColumn, renderAppend, renderEmpty } = useRender(); |
||||
|
|
||||
|
return ( |
||||
|
ctx.visible.value && |
||||
|
h( |
||||
|
<el-table class="cl-table" ref={Table} v-loading={crud.loading} />, |
||||
|
{ |
||||
|
...config.on, |
||||
|
...config.props, |
||||
|
|
||||
|
// 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(); |
||||
|
} |
||||
|
} |
||||
|
) |
||||
|
); |
||||
|
}; |
||||
|
} |
||||
|
}); |
||||
@ -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() { |
||||
|
if (config.onOpen) { |
||||
|
config.onOpen(); |
||||
|
} |
||||
|
|
||||
|
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>; |
||||
|
}; |
||||
|
} |
||||
|
}); |
||||
@ -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 }); |
||||
|
} |
||||
|
}; |
||||
@ -0,0 +1,30 @@ |
|||||
|
import { type App } from "vue"; |
||||
|
import { useComponent } from "./components"; |
||||
|
import { useProvide } from "./provide"; |
||||
|
import global from "./utils/global"; |
||||
|
import "./static/index.scss"; |
||||
|
|
||||
|
const Crud = { |
||||
|
install(app: App, options?: any) { |
||||
|
global.set("__CrudApp__", app); |
||||
|
|
||||
|
// 穿透值
|
||||
|
useProvide(app, options); |
||||
|
|
||||
|
// 设置组件
|
||||
|
useComponent(app); |
||||
|
|
||||
|
return { |
||||
|
name: "cl-crud" |
||||
|
}; |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
export { Crud }; |
||||
|
|
||||
|
export * from "./emitter"; |
||||
|
export * from "./hooks"; |
||||
|
export * from "./locale"; |
||||
|
export { registerFormHook } from "./utils/form-hook"; |
||||
|
export { renderNode } from "./utils/vnode"; |
||||
|
export { ContextMenu } from "./components/context-menu"; |
||||
@ -0,0 +1,191 @@ |
|||||
|
import { assign } from "lodash-es"; |
||||
|
import { TestService } from "../test/service"; |
||||
|
import { watch, ref, nextTick, getCurrentInstance, type 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, isChild }: { r: any; options: any; clear?: string; isChild?: boolean } |
||||
|
) { |
||||
|
if (!r.__ev) r.__ev = {}; |
||||
|
|
||||
|
const d: { [key: string]: (args: any[]) => void } = {}; |
||||
|
const ev = r.__ev as { [key: string]: { fn: any; isChild?: boolean }[] }; |
||||
|
|
||||
|
names.forEach((k) => { |
||||
|
if (!ev[k]) ev[k] = []; |
||||
|
|
||||
|
if (options[k]) { |
||||
|
ev[k].push({ |
||||
|
fn: options[k], |
||||
|
isChild |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
d[k] = (...args: any[]) => { |
||||
|
ev[k].forEach((e) => { |
||||
|
if (e.fn) { |
||||
|
e.fn(...args); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
if (clear == k) { |
||||
|
for (const i in ev) { |
||||
|
ev[i] = ev[i].filter((e) => !e.isChild); |
||||
|
} |
||||
|
} |
||||
|
}; |
||||
|
}); |
||||
|
|
||||
|
return d; |
||||
|
} |
||||
|
|
||||
|
// crud
|
||||
|
export function useCrud(options?: ClCrud.Options, cb?: (app: ClCrud.Ref) => void) { |
||||
|
const Crud = ref<ClCrud.Ref>(); |
||||
|
useParent("cl-crud", Crud); |
||||
|
|
||||
|
if (options) { |
||||
|
// 测试模式
|
||||
|
if (options.service == "test") { |
||||
|
options.service = new TestService(); |
||||
|
} |
||||
|
|
||||
|
provide("useCrud__options", options); |
||||
|
} |
||||
|
|
||||
|
watch(Crud, (val) => { |
||||
|
if (val) { |
||||
|
if (cb) { |
||||
|
cb(val); |
||||
|
} |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
return Crud; |
||||
|
} |
||||
|
|
||||
|
// 新增、编辑
|
||||
|
export function useUpsert<T = any>(options?: ClUpsert.Options<T>) { |
||||
|
const Upsert = ref<ClUpsert.Ref>(); |
||||
|
useParent("cl-upsert", Upsert); |
||||
|
const isChild = !!Upsert.value; |
||||
|
|
||||
|
if (options) { |
||||
|
provide("useUpsert__options", options); |
||||
|
} |
||||
|
|
||||
|
watch( |
||||
|
Upsert, |
||||
|
(val) => { |
||||
|
if (val) { |
||||
|
if (options) { |
||||
|
const event = useEvent(["onOpen", "onOpened", "onClosed"], { |
||||
|
r: val, |
||||
|
options, |
||||
|
clear: "onClosed", |
||||
|
isChild |
||||
|
}); |
||||
|
|
||||
|
assign(val.config, event); |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
immediate: true |
||||
|
} |
||||
|
); |
||||
|
|
||||
|
return Upsert; |
||||
|
} |
||||
|
|
||||
|
// 表格
|
||||
|
export function useTable<T = any>(options?: ClTable.Options<T>, cb?: (table: ClTable.Ref) => void) { |
||||
|
const Table = ref<ClTable.Ref<T>>(); |
||||
|
useParent("cl-table", Table); |
||||
|
|
||||
|
if (options) { |
||||
|
provide("useTable__options", options); |
||||
|
} |
||||
|
|
||||
|
watch(Table, (val) => { |
||||
|
if (val) { |
||||
|
if (cb) { |
||||
|
cb(val); |
||||
|
} |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
return Table; |
||||
|
} |
||||
|
|
||||
|
// 表单
|
||||
|
export function useForm<T = any>(cb?: (app: ClForm.Ref<T>) => void) { |
||||
|
const Form = ref<ClForm.Ref<T>>(); |
||||
|
useParent("cl-form", Form); |
||||
|
|
||||
|
nextTick(() => { |
||||
|
if (cb && Form.value) { |
||||
|
cb(Form.value); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
return Form; |
||||
|
} |
||||
|
|
||||
|
// 高级搜索
|
||||
|
export function useAdvSearch<T = any>(options?: ClAdvSearch.Options<T>) { |
||||
|
const AdvSearch = ref<ClAdvSearch.Ref<T>>(); |
||||
|
useParent("cl-adv-search", AdvSearch); |
||||
|
|
||||
|
if (options) { |
||||
|
provide("useAdvSearch__options", options); |
||||
|
} |
||||
|
|
||||
|
return AdvSearch; |
||||
|
} |
||||
|
|
||||
|
// 搜索
|
||||
|
export function useSearch<T = any>(options?: ClSearch.Options<T>) { |
||||
|
const Search = ref<ClSearch.Ref<T>>(); |
||||
|
useParent("cl-search", Search); |
||||
|
|
||||
|
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) => { |
||||
|
options?.onFullscreen(val); |
||||
|
} |
||||
|
); |
||||
|
|
||||
|
return Dialog; |
||||
|
} |
||||
@ -0,0 +1,74 @@ |
|||||
|
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 * from "./crud"; |
||||
@ -0,0 +1 @@ |
|||||
|
export * from "./entry"; |
||||
@ -0,0 +1,33 @@ |
|||||
|
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: "{label} cannot be empty", |
||||
|
collapse: "Collapse", |
||||
|
expand: "Expand" |
||||
|
}; |
||||
@ -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, |
||||
|
"zh-cn": zhCn, |
||||
|
"zh-tw": zhTw |
||||
|
}; |
||||
@ -0,0 +1,32 @@ |
|||||
|
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: "{label}は空にできません", |
||||
|
collapse: "折り畳む", |
||||
|
expand: "展開" |
||||
|
}; |
||||
@ -0,0 +1,32 @@ |
|||||
|
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: "{label}不能为空", |
||||
|
collapse: "收起", |
||||
|
expand: "展开更多" |
||||
|
}; |
||||
@ -0,0 +1,32 @@ |
|||||
|
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: "{label}不能為空", |
||||
|
collapse: "收起", |
||||
|
expand: "展開" |
||||
|
}; |
||||
@ -0,0 +1,28 @@ |
|||||
|
import { createApp } from "vue"; |
||||
|
import App from "./App.vue"; |
||||
|
import { Crud, locale } from "./entry"; |
||||
|
// import Crud, { locale } from "../dist/index.umd";
|
||||
|
// import "../dist/index.css";
|
||||
|
|
||||
|
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["zh-cn"] |
||||
|
}, |
||||
|
style: { |
||||
|
// size: "default"
|
||||
|
}, |
||||
|
render: { |
||||
|
autoHeight: true |
||||
|
} |
||||
|
}) |
||||
|
.mount("#app"); |
||||
@ -0,0 +1,129 @@ |
|||||
|
import { type 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["zh-cn"] |
||||
|
}, |
||||
|
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", |
||||
|
opWidth: 180 |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
events: {} |
||||
|
} as Options, |
||||
|
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); |
||||
|
} |
||||
@ -0,0 +1,862 @@ |
|||||
|
.cl-crud { |
||||
|
height: 100%; |
||||
|
overflow: hidden 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-select { |
||||
|
width: 100px; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
&__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: var(--el-fill-color-lighter) !important; |
||||
|
color: var(--el-text-color-primary); |
||||
|
|
||||
|
.cell { |
||||
|
line-height: normal; |
||||
|
} |
||||
|
|
||||
|
&.is-sortable { |
||||
|
.cl-table-header__search { |
||||
|
width: auto; |
||||
|
|
||||
|
&.is-input { |
||||
|
width: calc(100% - 24px) !important; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
&__empty-block { |
||||
|
height: auto !important; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.el-loading-mask { |
||||
|
.el-loading-spinner { |
||||
|
.el-icon-loading { |
||||
|
font-size: 25px; |
||||
|
color: #000; |
||||
|
} |
||||
|
|
||||
|
.el-loading-text { |
||||
|
color: var(--el-text-color-secondary); |
||||
|
margin-top: 5px; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
&__op { |
||||
|
margin-bottom: -5px; |
||||
|
|
||||
|
.el-button { |
||||
|
margin-bottom: 5px; |
||||
|
outline-offset: -2px !important; |
||||
|
|
||||
|
&.is-text { |
||||
|
border: 1px solid var(--el-button-border-color) !important; |
||||
|
|
||||
|
&:hover { |
||||
|
background-color: var(--el-button-bg-color) !important; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.cl-table-header__search { |
||||
|
display: inline-flex; |
||||
|
align-items: center; |
||||
|
gap: 5px; |
||||
|
width: 100%; |
||||
|
cursor: pointer; |
||||
|
|
||||
|
&-label { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
gap: 5px; |
||||
|
} |
||||
|
|
||||
|
&:hover { |
||||
|
color: var(--el-color-primary); |
||||
|
} |
||||
|
|
||||
|
&-inner { |
||||
|
flex: 1; |
||||
|
|
||||
|
& > div { |
||||
|
width: 100%; |
||||
|
|
||||
|
.el-date-editor { |
||||
|
margin-right: 0; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
&-close { |
||||
|
font-size: 14px; |
||||
|
height: 30px; |
||||
|
width: 30px; |
||||
|
border: 1px solid var(--el-border-color); |
||||
|
border-radius: 6px; |
||||
|
background-color: var(--el-fill-color-blank); |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
transition: all 0.2s; |
||||
|
color: var(--el-text-color-secondary); |
||||
|
|
||||
|
&:hover { |
||||
|
border-color: var(--el-border-color-hover); |
||||
|
color: var(--el-text-color-primary); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.is-left { |
||||
|
.cl-table-header__search-label { |
||||
|
justify-content: flex-start; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.is-right { |
||||
|
.cl-table-header__search-label { |
||||
|
justify-content: flex-end; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.cl-pagination { |
||||
|
&.el-pagination { |
||||
|
--el-pagination-border-radius: var(--el-border-radius-base); |
||||
|
} |
||||
|
|
||||
|
.btn-prev, |
||||
|
.btn-next, |
||||
|
.el-pager li { |
||||
|
border-radius: var(--el-border-radius-base); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.cl-filter { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
margin: 0 10px; |
||||
|
|
||||
|
&__label { |
||||
|
font-size: 12px; |
||||
|
margin-right: 10px; |
||||
|
white-space: nowrap; |
||||
|
} |
||||
|
|
||||
|
.el-select { |
||||
|
min-width: 120px; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.cl-search { |
||||
|
&__btns { |
||||
|
margin-left: 5px; |
||||
|
} |
||||
|
|
||||
|
&__more { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: space-between; |
||||
|
|
||||
|
.el-form-item { |
||||
|
margin-bottom: 0; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.el-form:not(.el-form--label-top) { |
||||
|
.el-form-item__label { |
||||
|
&:empty { |
||||
|
display: none; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.cl-search__btns { |
||||
|
.el-form-item__label { |
||||
|
display: none; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.cl-search__btns { |
||||
|
.el-button + .el-button { |
||||
|
margin-left: 10px; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
&.is-inline { |
||||
|
margin-bottom: 0 !important; |
||||
|
} |
||||
|
|
||||
|
&.is-collapse { |
||||
|
background-color: var(--el-fill-color-lighter); |
||||
|
padding: 10px; |
||||
|
margin-bottom: 10px !important; |
||||
|
border-radius: 6px; |
||||
|
border: 1px solid var(--el-border-color-extra-light); |
||||
|
|
||||
|
&.is-fold { |
||||
|
.cl-search__form { |
||||
|
max-height: 42px; |
||||
|
overflow: hidden; |
||||
|
|
||||
|
&:has(.el-form--label-top) { |
||||
|
max-height: 68px; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.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; |
||||
|
} |
||||
|
|
||||
|
&-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; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.el-table__header tr { |
||||
|
line-height: normal; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
&__footer { |
||||
|
display: flex; |
||||
|
justify-content: flex-end; |
||||
|
} |
||||
|
|
||||
|
.cl-crud { |
||||
|
line-height: normal; |
||||
|
} |
||||
|
|
||||
|
.el-form { |
||||
|
.el-form-item { |
||||
|
.el-form-item { |
||||
|
margin-bottom: 18px; |
||||
|
} |
||||
|
|
||||
|
.el-input-number { |
||||
|
&__decrease, |
||||
|
&__increase { |
||||
|
border: 0; |
||||
|
background-color: transparent; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
&__label { |
||||
|
.el-tooltip { |
||||
|
i { |
||||
|
margin-left: 5px; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
&__content { |
||||
|
min-width: 0px; |
||||
|
|
||||
|
& > div { |
||||
|
width: 100%; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
&:not(.el-form--label-top) { |
||||
|
.el-form-item { |
||||
|
&.no-label { |
||||
|
& > .el-form-item__label { |
||||
|
padding: 0; |
||||
|
display: none; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
&.el-form--label-top { |
||||
|
.el-form-item { |
||||
|
margin-bottom: 22px; |
||||
|
} |
||||
|
|
||||
|
.el-form-item__label { |
||||
|
margin: 0 0 4px 0; |
||||
|
min-height: 22px; |
||||
|
} |
||||
|
|
||||
|
.el-form-item__error { |
||||
|
padding-top: 4px; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
&.el-form--inline { |
||||
|
.cl-form__items { |
||||
|
display: flex; |
||||
|
flex-wrap: wrap; |
||||
|
} |
||||
|
|
||||
|
.el-form-item { |
||||
|
margin: 0 10px 10px 0; |
||||
|
|
||||
|
.el-date-editor { |
||||
|
box-sizing: border-box; |
||||
|
|
||||
|
.el-range-input { |
||||
|
&:nth-child(2) { |
||||
|
margin-left: 5px; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.el-select { |
||||
|
width: 173px; |
||||
|
} |
||||
|
|
||||
|
&:last-child { |
||||
|
margin-right: 0; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.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; |
||||
|
overflow-y: hidden; |
||||
|
scrollbar-width: none; |
||||
|
-ms-overflow-style: none; |
||||
|
position: relative; |
||||
|
|
||||
|
&::-webkit-scrollbar { |
||||
|
display: none; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
ul { |
||||
|
display: inline-flex; |
||||
|
white-space: nowrap; |
||||
|
margin: 0; |
||||
|
padding: 0; |
||||
|
|
||||
|
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 { |
||||
|
margin-top: 0; |
||||
|
|
||||
|
&__header { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: space-between; |
||||
|
font-size: 14px; |
||||
|
height: 32px; |
||||
|
line-height: normal; |
||||
|
font-weight: bold; |
||||
|
padding: 0 10px; |
||||
|
background-color: var(--el-fill-color-lighter); |
||||
|
border-radius: var(--el-border-radius-base); |
||||
|
cursor: pointer; |
||||
|
user-select: none; |
||||
|
|
||||
|
&:hover { |
||||
|
background-color: var(--el-fill-color-light); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
&__container { |
||||
|
transition: all 0.3s; |
||||
|
display: grid; |
||||
|
grid-template-rows: 0fr; |
||||
|
|
||||
|
> .cl-form-item__children { |
||||
|
min-height: 0; |
||||
|
overflow: hidden; |
||||
|
|
||||
|
.el-row { |
||||
|
margin-top: 10px; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
&.is-expand { |
||||
|
> .cl-form-card__container { |
||||
|
grid-template-rows: 1fr; |
||||
|
margin-bottom: -18px; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.cl-form-card { |
||||
|
margin-left: 10px; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.cl-dialog { |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
|
||||
|
&.el-dialog { |
||||
|
padding: 0; |
||||
|
border-radius: 8px; |
||||
|
} |
||||
|
|
||||
|
.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; |
||||
|
} |
||||
|
|
||||
|
&__default { |
||||
|
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: 8px; |
||||
|
top: 8px; |
||||
|
z-index: 9; |
||||
|
width: 100%; |
||||
|
|
||||
|
&-icon, |
||||
|
.minimize, |
||||
|
.maximize, |
||||
|
.close { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
height: 28px; |
||||
|
width: 28px; |
||||
|
border: 0; |
||||
|
background-color: transparent; |
||||
|
cursor: pointer; |
||||
|
outline: none; |
||||
|
border-radius: 6px; |
||||
|
margin-left: 5px; |
||||
|
|
||||
|
i { |
||||
|
font-size: 18px; |
||||
|
} |
||||
|
|
||||
|
&:hover { |
||||
|
background-color: var(--el-fill-color-darker); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
&.hidden-header { |
||||
|
.el-dialog__header { |
||||
|
display: none; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
&.is-fullscreen { |
||||
|
height: 100vh !important; |
||||
|
border-radius: 0; |
||||
|
overflow: hidden; |
||||
|
|
||||
|
.cl-dialog__container { |
||||
|
height: 100% !important; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
&.is-transparent { |
||||
|
background-color: transparent !important; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.cl-context-menu { |
||||
|
position: absolute; |
||||
|
z-index: 9999; |
||||
|
|
||||
|
&__box { |
||||
|
width: 160px; |
||||
|
background-color: var(--el-bg-color); |
||||
|
border: 1px solid var(--el-border-color-extra-light); |
||||
|
box-shadow: |
||||
|
0 4px 6px -1px rgba(0, 0, 0, 0.1), |
||||
|
0 2px 4px -2px rgba(0, 0, 0, 0.1); |
||||
|
border-radius: 6px; |
||||
|
position: absolute; |
||||
|
top: 0; |
||||
|
padding: 5px; |
||||
|
|
||||
|
&.is-append { |
||||
|
right: calc(-100% - 5px); |
||||
|
top: -5px; |
||||
|
} |
||||
|
|
||||
|
& > div { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
font-size: 12px; |
||||
|
cursor: pointer; |
||||
|
padding: 5px 15px; |
||||
|
color: var(--el-text-color-primary); |
||||
|
position: relative; |
||||
|
border-radius: 4px; |
||||
|
|
||||
|
span { |
||||
|
flex: 1; |
||||
|
user-select: none; |
||||
|
} |
||||
|
|
||||
|
&:hover { |
||||
|
background-color: var(--el-fill-color-lighter); |
||||
|
} |
||||
|
|
||||
|
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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.cl-error-message { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
height: 32px; |
||||
|
padding: 0 10px; |
||||
|
font-size: 14px; |
||||
|
color: var(--el-color-danger); |
||||
|
border: 1px solid var(--el-color-danger); |
||||
|
border-radius: 4px; |
||||
|
box-sizing: border-box; |
||||
|
user-select: none; |
||||
|
} |
||||
@ -0,0 +1,231 @@ |
|||||
|
import { assign, orderBy } from "lodash-es"; |
||||
|
import { uuid } from "../utils"; |
||||
|
|
||||
|
const userList = [ |
||||
|
{ |
||||
|
id: 1, |
||||
|
name: "楚行云", |
||||
|
createTime: "1996-09-14", |
||||
|
wages: 73026, |
||||
|
status: 1, |
||||
|
account: "chuxingyun", |
||||
|
occupation: 4, |
||||
|
phone: 13797353874 |
||||
|
}, |
||||
|
{ |
||||
|
id: 2, |
||||
|
name: "秦尘", |
||||
|
createTime: "1977-11-09", |
||||
|
wages: 74520, |
||||
|
status: 0, |
||||
|
account: "qincheng", |
||||
|
occupation: 3, |
||||
|
phone: 18593911044 |
||||
|
}, |
||||
|
{ |
||||
|
id: 3, |
||||
|
name: "叶凡", |
||||
|
createTime: "1982-11-28", |
||||
|
wages: 81420, |
||||
|
status: 0, |
||||
|
account: "yefan", |
||||
|
occupation: 1, |
||||
|
phone: 16234136338 |
||||
|
}, |
||||
|
{ |
||||
|
id: 4, |
||||
|
name: "白小纯", |
||||
|
createTime: "2012-12-17", |
||||
|
wages: 65197, |
||||
|
status: 1, |
||||
|
account: "baixiaochun", |
||||
|
occupation: 2, |
||||
|
phone: 16325661110 |
||||
|
}, |
||||
|
{ |
||||
|
id: 5, |
||||
|
name: "韩立", |
||||
|
createTime: "1982-07-10", |
||||
|
wages: 99107, |
||||
|
status: 1, |
||||
|
account: "hanli", |
||||
|
occupation: 2, |
||||
|
phone: 18486594866 |
||||
|
}, |
||||
|
{ |
||||
|
id: 6, |
||||
|
name: "唐三", |
||||
|
createTime: "2019-07-31", |
||||
|
wages: 80658, |
||||
|
status: 1, |
||||
|
account: "tangsan", |
||||
|
occupation: 5, |
||||
|
phone: 15565014642 |
||||
|
}, |
||||
|
{ |
||||
|
id: 7, |
||||
|
name: "王林", |
||||
|
createTime: "2009-07-26", |
||||
|
wages: 57408, |
||||
|
status: 1, |
||||
|
account: "wanglin", |
||||
|
occupation: 1, |
||||
|
phone: 13852767084 |
||||
|
}, |
||||
|
{ |
||||
|
id: 8, |
||||
|
name: "李强", |
||||
|
createTime: "2016-04-26", |
||||
|
wages: 71782, |
||||
|
status: 1, |
||||
|
account: "liqiang", |
||||
|
occupation: 3, |
||||
|
phone: 18365332834 |
||||
|
}, |
||||
|
{ |
||||
|
id: 9, |
||||
|
name: "秦羽", |
||||
|
createTime: "1984-01-18", |
||||
|
wages: 87860, |
||||
|
status: 1, |
||||
|
account: "qinyu", |
||||
|
occupation: 0, |
||||
|
phone: 18149247129 |
||||
|
} |
||||
|
]; |
||||
|
|
||||
|
class TestService { |
||||
|
// 分页列表
|
||||
|
async page(params: any) { |
||||
|
const { keyWord, page, size, sort, order } = params || {}; |
||||
|
|
||||
|
// 关键字查询
|
||||
|
const keyWordLikeFields = ["phone", "name"]; |
||||
|
|
||||
|
// 等值查询
|
||||
|
const fieldEq = ["createTime", "occupation", "status"]; |
||||
|
|
||||
|
// 模糊查询
|
||||
|
const likeFields = ["phone", "name"]; |
||||
|
|
||||
|
// 过滤后的列表
|
||||
|
const list = orderBy(userList, order, sort).filter((e: any) => { |
||||
|
let f = true; |
||||
|
|
||||
|
if (keyWord !== undefined) { |
||||
|
f = !!keyWordLikeFields.find((k) => String(e[k]).includes(String(params.keyWord))); |
||||
|
} |
||||
|
|
||||
|
fieldEq.forEach((k) => { |
||||
|
if (f) { |
||||
|
if (params[k] !== undefined) { |
||||
|
f = e[k] == params[k]; |
||||
|
} |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
likeFields.forEach((k) => { |
||||
|
if (f) { |
||||
|
if (params[k] !== undefined) { |
||||
|
f = String(e[k]).includes(String(params[k])); |
||||
|
} |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
return f; |
||||
|
}); |
||||
|
|
||||
|
return new Promise((resolve) => { |
||||
|
// 模拟延迟
|
||||
|
setTimeout(() => { |
||||
|
resolve({ |
||||
|
list: list.slice((page - 1) * size, page * size), |
||||
|
pagination: { |
||||
|
total: list.length, |
||||
|
page, |
||||
|
size |
||||
|
}, |
||||
|
subData: { |
||||
|
wages: list.reduce((a, b) => { |
||||
|
return a + b.wages; |
||||
|
}, 0) |
||||
|
} |
||||
|
}); |
||||
|
}, 500); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
// 更新
|
||||
|
async update(params: { id: any; [key: string]: any }) { |
||||
|
const item = userList.find((e) => e.id == params.id); |
||||
|
|
||||
|
if (item) { |
||||
|
assign(item, params); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 新增
|
||||
|
async add(params: any) { |
||||
|
const id = uuid(); |
||||
|
|
||||
|
userList.push({ |
||||
|
id, |
||||
|
...params |
||||
|
}); |
||||
|
|
||||
|
return id; |
||||
|
} |
||||
|
|
||||
|
// 详情
|
||||
|
async info(params: { id: any }) { |
||||
|
const { id } = params || {}; |
||||
|
return userList.find((e) => e.id == id); |
||||
|
} |
||||
|
|
||||
|
// 删除
|
||||
|
async delete(params: { ids: any[] }) { |
||||
|
const { ids = [] } = params || {}; |
||||
|
|
||||
|
ids.forEach((id) => { |
||||
|
const index = userList.findIndex((e) => e.id == id); |
||||
|
userList.splice(index, 1); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
// 全部列表
|
||||
|
async list() { |
||||
|
return userList; |
||||
|
} |
||||
|
|
||||
|
search = { |
||||
|
fieldEq: [ |
||||
|
{ |
||||
|
propertyName: "occupation", |
||||
|
comment: "工作", |
||||
|
source: "a.occupation" |
||||
|
} |
||||
|
], |
||||
|
fieldLike: [ |
||||
|
{ |
||||
|
propertyName: "status", |
||||
|
comment: "状态", |
||||
|
dict: ["关闭", "开启"], |
||||
|
source: "a.status" |
||||
|
} |
||||
|
], |
||||
|
keyWordLikeFields: [ |
||||
|
{ |
||||
|
propertyName: "name", |
||||
|
comment: "姓名", |
||||
|
source: "a.name" |
||||
|
}, |
||||
|
{ |
||||
|
propertyName: "phone", |
||||
|
comment: "手机号", |
||||
|
source: "a.phone" |
||||
|
} |
||||
|
] |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
export { TestService }; |
||||
@ -0,0 +1,149 @@ |
|||||
|
import { isArray, isEmpty, isFunction, isObject, isString } from "lodash-es"; |
||||
|
|
||||
|
export const format: { [key: string]: ClForm.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; |
||||
|
} |
||||
|
|
||||
|
if (isArray(value)) { |
||||
|
return isEmpty(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: any[] = []; |
||||
|
|
||||
|
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)) { |
||||
|
pipes = isArray(pipe[method]) ? pipe[method] : [pipe[method]]; |
||||
|
} else if (isFunction(pipe)) { |
||||
|
pipes = [pipe]; |
||||
|
} else { |
||||
|
console.error(`[hook] ${pipe} format error`); |
||||
|
} |
||||
|
|
||||
|
let v = value; |
||||
|
|
||||
|
pipes.forEach((e) => { |
||||
|
let f: any = 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: ClForm.Hook["Fn"]) { |
||||
|
format[name] = fn; |
||||
|
} |
||||
|
|
||||
|
export default formHook; |
||||
@ -0,0 +1,16 @@ |
|||||
|
// @ts-nocheck
|
||||
|
import { type 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; |
||||
|
} |
||||
|
}; |
||||
@ -0,0 +1,164 @@ |
|||||
|
import { isRef, mergeProps, toValue } from "vue"; |
||||
|
import { assign, flatMap, isArray, isFunction, isNumber, 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)) { |
||||
|
assign(d[n], value); |
||||
|
} else { |
||||
|
d[n] = value; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return obj; |
||||
|
} catch (e) { |
||||
|
console.error("[dataset] 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 (el?.classList) { |
||||
|
el.classList.add(name); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 移除元素
|
||||
|
export function removeClass(el: Element, name: string) { |
||||
|
if (el?.classList) { |
||||
|
el.classList.remove(name); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 获取值
|
||||
|
export function getValue<T = any>(value: T | Vue.Ref<T> | ((d: any) => T), data?: any): T { |
||||
|
if (isRef(value)) { |
||||
|
return toValue(value) as T; |
||||
|
} else { |
||||
|
if (isFunction(value)) { |
||||
|
return value(data); |
||||
|
} else { |
||||
|
return value as T; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 深度查找
|
||||
|
export function deepFind(value: any, list: any[], options?: { allLevels: boolean }) { |
||||
|
const { allLevels = true } = options || {}; |
||||
|
|
||||
|
function deep(arr: DictOptions, name: string[]): any | undefined { |
||||
|
for (const e of arr) { |
||||
|
if (e.value === value) { |
||||
|
if (allLevels) { |
||||
|
return { |
||||
|
...e, |
||||
|
label: [...name, e.label].join(" / ") |
||||
|
}; |
||||
|
} else { |
||||
|
return e; |
||||
|
} |
||||
|
} else if (e.children) { |
||||
|
const d = deep(e.children, [...name!, e.label!]); |
||||
|
|
||||
|
if (d !== undefined) { |
||||
|
return d; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
return undefined; |
||||
|
} |
||||
|
|
||||
|
return deep(list, []); |
||||
|
} |
||||
|
|
||||
|
// uuid
|
||||
|
export function uuid(separator = "-"): string { |
||||
|
const s: any[] = []; |
||||
|
const hexDigits = "0123456789abcdef"; |
||||
|
for (let i = 0; i < 36; i++) { |
||||
|
s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1); |
||||
|
} |
||||
|
s[14] = "4"; |
||||
|
s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1); |
||||
|
s[8] = s[13] = s[18] = s[23] = separator; |
||||
|
|
||||
|
return s.join(""); |
||||
|
} |
||||
|
|
||||
|
// 移除相同的方法
|
||||
|
export function uniqueFns(fns: any[]) { |
||||
|
const arr = new Map(); |
||||
|
fns.forEach((fn) => { |
||||
|
arr.set(fn.name, fn); |
||||
|
}); |
||||
|
return Array.from(arr.values()); |
||||
|
} |
||||
@ -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-expect-error
|
||||
|
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 }; |
||||
@ -0,0 +1,52 @@ |
|||||
|
import { isString } from "lodash-es"; |
||||
|
import { getValue, isObject } from "."; |
||||
|
|
||||
|
// 解析扩展组件
|
||||
|
export function parseExtensionComponent(vnode: Render.Component) { |
||||
|
if (["el-select", "el-radio-group", "el-checkbox-group"].includes(vnode.name!)) { |
||||
|
const list = getValue(vnode.options || []); |
||||
|
|
||||
|
const children = ( |
||||
|
<div> |
||||
|
{list.map((e, i) => { |
||||
|
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={`Component options error`} />; |
||||
|
} |
||||
|
|
||||
|
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} value={value} {...e.props}> |
||||
|
{label} |
||||
|
</el-radio> |
||||
|
); |
||||
|
case "el-checkbox-group": |
||||
|
return ( |
||||
|
<el-checkbox key={i} value={value} {...e.props}> |
||||
|
{label} |
||||
|
</el-checkbox> |
||||
|
); |
||||
|
default: |
||||
|
return null; |
||||
|
} |
||||
|
})} |
||||
|
</div> |
||||
|
); |
||||
|
|
||||
|
return { |
||||
|
children |
||||
|
}; |
||||
|
} else { |
||||
|
return {}; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,182 @@ |
|||||
|
import { h, resolveComponent, toRaw, type VNode } from "vue"; |
||||
|
import { isObject } from "./index"; |
||||
|
import { parseExtensionComponent } from "./parse"; |
||||
|
import global from "./global"; |
||||
|
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 || {}; |
||||
|
|
||||
|
// 渲染后组件
|
||||
|
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)) { |
||||
|
global.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 slots = { |
||||
|
...vnode.slots |
||||
|
}; |
||||
|
|
||||
|
if (children) { |
||||
|
slots.default = () => children; |
||||
|
} |
||||
|
|
||||
|
// 渲染组件
|
||||
|
comp = h(toRaw(resolveComponent(vnode.name)), props, slots); |
||||
|
} |
||||
|
|
||||
|
// 挂载到 refs 中
|
||||
|
const refBind = vnode.ref || options.ref; |
||||
|
if (isFunction(refBind)) { |
||||
|
setTimeout(() => { |
||||
|
refBind(comp?.component?.exposed); |
||||
|
}, 0); |
||||
|
} |
||||
|
|
||||
|
return comp; |
||||
|
} |
||||
|
|
||||
|
// 渲染节点
|
||||
|
export function renderNode(vnode: any, options: Options) { |
||||
|
const config = useConfig(); |
||||
|
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 = config.dict.label.placeholder; |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
if (placeholder) { |
||||
|
if (!item.component.props.placeholder) { |
||||
|
item.component.props.placeholder = placeholder; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 组件实例
|
||||
|
if (vnode.vm) { |
||||
|
if (!vnode.name) { |
||||
|
vnode.name = vnode.vm?.name || vnode.vm?.__hmrId; |
||||
|
} |
||||
|
|
||||
|
return parseNode(vnode, options); |
||||
|
} |
||||
|
|
||||
|
// 组件名渲染
|
||||
|
if (isString(vnode)) { |
||||
|
if (render == "slot") { |
||||
|
if (!vnode.includes("slot-")) { |
||||
|
return vnode; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return parseNode({ name: vnode }, options); |
||||
|
} |
||||
|
|
||||
|
// 方法回调
|
||||
|
if (isFunction(vnode)) { |
||||
|
return vnode({ scope, h, ..._data }); |
||||
|
} |
||||
|
|
||||
|
// jsx 模式
|
||||
|
if (isObject(vnode)) { |
||||
|
if (vnode.name) { |
||||
|
return parseNode(vnode, { ...options, children, ...parseExtensionComponent(vnode) }); |
||||
|
} else { |
||||
|
if (options.custom) { |
||||
|
return options.custom(vnode); |
||||
|
} |
||||
|
|
||||
|
return <cl-error-message title="Error,component name is required" />; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,34 @@ |
|||||
|
{ |
||||
|
"compilerOptions": { |
||||
|
"target": "ES2020", |
||||
|
"useDefineForClassFields": true, |
||||
|
"module": "ESNext", |
||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"], |
||||
|
"skipLibCheck": true, |
||||
|
"strict": false, |
||||
|
"noImplicitAny": false, |
||||
|
|
||||
|
"moduleResolution": "bundler", |
||||
|
"allowImportingTsExtensions": true, |
||||
|
"resolveJsonModule": true, |
||||
|
"isolatedModules": true, |
||||
|
"noEmit": true, |
||||
|
"jsx": "preserve", |
||||
|
|
||||
|
"noUnusedLocals": true, |
||||
|
"noUnusedParameters": true, |
||||
|
"noFallthroughCasesInSwitch": true, |
||||
|
|
||||
|
"types": ["./index.d.ts"], |
||||
|
|
||||
|
"baseUrl": ".", |
||||
|
"paths": { |
||||
|
"@/*": ["src/*"] |
||||
|
}, |
||||
|
"declaration": true, |
||||
|
"declarationDir": "types", |
||||
|
"emitDeclarationOnly": true |
||||
|
}, |
||||
|
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], |
||||
|
"exclude": ["src/App.vue", "node_modules", "dist"] |
||||
|
} |
||||
@ -0,0 +1,2 @@ |
|||||
|
declare const _default: import('vue').DefineComponent<{}, () => any, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, true, {}, any>; |
||||
|
export default _default; |
||||
@ -0,0 +1,4 @@ |
|||||
|
declare const _default: import('vue').DefineComponent<{}, () => any, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, { |
||||
|
Search: import('vue').DefineComponent<{}, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').VNodeProps & import('vue').AllowedComponentProps & import('vue').ComponentCustomProps, Readonly<import('vue').ExtractPropTypes<{}>>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, true, {}, any>; |
||||
|
}, {}, string, import('vue').ComponentProvideOptions, true, {}, any>; |
||||
|
export default _default; |
||||
@ -0,0 +1,42 @@ |
|||||
|
import { PropType } from 'vue'; |
||||
|
declare const _default: import('vue').DefineComponent<import('vue').ExtractPropTypes<{ |
||||
|
items: { |
||||
|
type: PropType<ClForm.Item[]>; |
||||
|
default: () => any[]; |
||||
|
}; |
||||
|
title: StringConstructor; |
||||
|
size: { |
||||
|
type: (NumberConstructor | StringConstructor)[]; |
||||
|
default: string; |
||||
|
}; |
||||
|
op: { |
||||
|
type: ArrayConstructor; |
||||
|
default: () => string[]; |
||||
|
}; |
||||
|
onSearch: FunctionConstructor; |
||||
|
}>, () => any, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, ("clear" | "reset")[], "clear" | "reset", import('vue').PublicProps, Readonly<import('vue').ExtractPropTypes<{ |
||||
|
items: { |
||||
|
type: PropType<ClForm.Item[]>; |
||||
|
default: () => any[]; |
||||
|
}; |
||||
|
title: StringConstructor; |
||||
|
size: { |
||||
|
type: (NumberConstructor | StringConstructor)[]; |
||||
|
default: string; |
||||
|
}; |
||||
|
op: { |
||||
|
type: ArrayConstructor; |
||||
|
default: () => string[]; |
||||
|
}; |
||||
|
onSearch: FunctionConstructor; |
||||
|
}>> & Readonly<{ |
||||
|
onReset?: (...args: any[]) => any; |
||||
|
onClear?: (...args: any[]) => any; |
||||
|
}>, { |
||||
|
size: string | number; |
||||
|
items: ClForm.Item<any>[]; |
||||
|
op: unknown[]; |
||||
|
}, {}, { |
||||
|
Close: import('vue').DefineComponent<{}, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').VNodeProps & import('vue').AllowedComponentProps & import('vue').ComponentCustomProps, Readonly<import('vue').ExtractPropTypes<{}>>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, true, {}, any>; |
||||
|
}, {}, string, import('vue').ComponentProvideOptions, true, {}, any>; |
||||
|
export default _default; |
||||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue