feat: 移除旧的表格表单组件
parent
d2b8bc2f8e
commit
9a15a88eb0
6
.env
6
.env
|
|
@ -2,13 +2,13 @@
|
||||||
# 应用配置
|
# 应用配置
|
||||||
# =====================================================================================
|
# =====================================================================================
|
||||||
# 网站标题
|
# 网站标题
|
||||||
VITE_TITLE = Appnify
|
VITE_TITLE = 绝弹管理中心
|
||||||
# 网站副标题
|
# 网站副标题
|
||||||
VITE_SUBTITLE = 快速开发web应用的模板工具
|
VITE_SUBTITLE = 绝弹管理中心
|
||||||
# 部署路径: 当为 ./ 时路由模式需为 hash
|
# 部署路径: 当为 ./ 时路由模式需为 hash
|
||||||
VITE_BASE = /
|
VITE_BASE = /
|
||||||
# 接口前缀:参见 axios 的 baseURL
|
# 接口前缀:参见 axios 的 baseURL
|
||||||
VITE_API = https://appnify.app.juetan.cn/
|
VITE_API = http://127.0.0.1:3030/
|
||||||
# 首页路径
|
# 首页路径
|
||||||
VITE_HOME_PATH = /home
|
VITE_HOME_PATH = /home
|
||||||
# 路由模式:web(路径) hash(锚点)
|
# 路由模式:web(路径) hash(锚点)
|
||||||
|
|
|
||||||
|
|
@ -7,14 +7,12 @@ import pkg from "../../package.json";
|
||||||
* 项目 logo
|
* 项目 logo
|
||||||
* @description 内容:APPTIFY
|
* @description 内容:APPTIFY
|
||||||
*/
|
*/
|
||||||
const LOGO = `
|
const LOGO = ` _ _______ _______ ____ _____ _____ ________ ____ ____
|
||||||
________ ______ ______ _________ ________ ______ __ __
|
/ \\\\ |_ __ \\\\|_ __ \\\\|_ \\\\|_ _||_ _||_ __ ||_ _||_ _|
|
||||||
/_______/\\\\ /_____/\\\\ /_____/\\\\ /________/\\\\/_______/\\\\/_____/\\\\ /_/\\\\/_/\\\\
|
/ _ \\\\ | |__) | | |__) | | \\\\ | | | | | |_ \\\\_| \\\\ \\\\ / /
|
||||||
\\\\::: _ \\\\ \\\\\\\\:::_ \\\\ \\\\\\\\:::_ \\\\ \\\\\\\\__.::.__\\\\/\\\\__.::._\\\\/\\\\::::_\\\\/_\\\\ \\\\ \\\\ \\\\ \\\\
|
/ ___ \\\\ | ___/ | ___/ | |\\\\ \\\\| | | | | _| \\\\ \\\\/ /
|
||||||
\\\\::(_) \\\\ \\\\\\\\:(_) \\\\ \\\\\\\\:(_) \\\\ \\\\ \\\\::\\\\ \\\\ \\\\::\\\\ \\\\ \\\\:\\\\/___/\\\\\\\\:\\\\_\\\\ \\\\ \\\\
|
_/ / \\\\ \\\\_ _| |_ _| |_ _| |_\\\\ |_ _| |_ _| |_ _| |_
|
||||||
\\\\:: __ \\\\ \\\\\\\\: ___\\\\/ \\\\: ___\\\\/ \\\\::\\\\ \\\\ _\\\\::\\\\ \\\\__\\\\:::._\\\\/ \\\\::::_\\\\/
|
|____| |____||_____| |_____| |_____|\\\\____||_____||_____| |______|
|
||||||
\\\\:.\\\\ \\\\ \\\\ \\\\\\\\ \\\\ \\\\ \\\\ \\\\ \\\\ \\\\::\\\\ \\\\ /__\\\\::\\\\__/\\\\\\\\:\\\\ \\\\ \\\\::\\\\ \\\\
|
|
||||||
\\\\__\\\\/\\\\__\\\\/ \\\\_\\\\/ \\\\_\\\\/ \\\\__\\\\/ \\\\________\\\\/ \\\\_\\\\/ \\\\__\\\\/
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -47,7 +45,7 @@ const getBuildInfo = async () => {
|
||||||
const latestTag = await exec("git describe --tags --abbrev=0");
|
const latestTag = await exec("git describe --tags --abbrev=0");
|
||||||
const commits = await exec(`git rev-list --count ${latestTag}..HEAD`);
|
const commits = await exec(`git rev-list --count ${latestTag}..HEAD`);
|
||||||
const version = commits ? `${latestTag}.${commits}` : `v${pkg.version}`;
|
const version = commits ? `${latestTag}.${commits}` : `v${pkg.version}`;
|
||||||
const content = ` 欢迎访问!版本: ${version} 标识: ${hash} 构建时间: ${time}`;
|
const content = `欢迎访问!版本: ${version} 标识: ${hash} 构建: ${time}`;
|
||||||
const style = `"color: #09f; font-weight: 900;", "font-size: 12px; color: #09f; font-family: ''"`;
|
const style = `"color: #09f; font-weight: 900;", "font-size: 12px; color: #09f; font-family: ''"`;
|
||||||
const script = `console.log(\`%c${LOGO} \n%c${content}\n\`, ${style});\n`;
|
const script = `console.log(\`%c${LOGO} \n%c${content}\n\`, ${style});\n`;
|
||||||
return script;
|
return script;
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ export function addExceptionInterceptor(axios: AxiosInstance, exipreHandler?: (.
|
||||||
logoutTipShowing = true;
|
logoutTipShowing = true;
|
||||||
Notification.warning({
|
Notification.warning({
|
||||||
title: '登陆提示',
|
title: '登陆提示',
|
||||||
content: '当前登陆已过期,请重新登陆!',
|
content: '登陆已过期,请重新登陆!',
|
||||||
onClose: () => (logoutTipShowing = false),
|
onClose: () => (logoutTipShowing = false),
|
||||||
});
|
});
|
||||||
exipreHandler?.(error);
|
exipreHandler?.(error);
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,7 @@ export function useFormModal(options: FormModalUseOptions) {
|
||||||
<AnFormModal
|
<AnFormModal
|
||||||
ref={(el: any) => (modalRef.value = el)}
|
ref={(el: any) => (modalRef.value = el)}
|
||||||
title={props.title}
|
title={props.title}
|
||||||
trigger={props.title}
|
trigger={props.trigger}
|
||||||
modalProps={props.modalProps as any}
|
modalProps={props.modalProps as any}
|
||||||
model={props.model}
|
model={props.model}
|
||||||
items={props.items}
|
items={props.items}
|
||||||
|
|
|
||||||
|
|
@ -1,237 +0,0 @@
|
||||||
### 介绍
|
|
||||||
基于`Arco-Design`组件封装的表单,旨在通过较少的配置提升开发效率,将一些通过的状态管理内置,使得开发者只需关注核心内容即可快速开发通用型表单。
|
|
||||||
|
|
||||||
本表单适用于通用型表单,对于自定义要求较高的需求,可能不太适合。
|
|
||||||
|
|
||||||
### 功能
|
|
||||||
- 配置化编写代码,保证UI一致性,提供开发效率。
|
|
||||||
- 提供typesciprt类型提示
|
|
||||||
- 表单项和校验规则之间可联动、可动态显示/隐藏
|
|
||||||
- 内置常用校验规则,开箱即用
|
|
||||||
- 支持组件参数透传,让每个组件都能自定义。
|
|
||||||
|
|
||||||
### 基本功能
|
|
||||||
基本用法:
|
|
||||||
```tsx
|
|
||||||
<template>
|
|
||||||
<Form v-bind="form" />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { Form, useForm } from '@/components'
|
|
||||||
|
|
||||||
const form = useForm({
|
|
||||||
model: {
|
|
||||||
id: undefined
|
|
||||||
},
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
field: 'username',
|
|
||||||
label: '用户名称',
|
|
||||||
type: 'input'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
submit: async ({ model, items }) => {
|
|
||||||
await new Promise(res => setTimeout(res, 2000));
|
|
||||||
return { message: '操作成功!' }
|
|
||||||
},
|
|
||||||
formProps: {},
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
```
|
|
||||||
以上, 只有四个参数,只需定义关注的内容,剩下的内容如内部状态等, 由表单管理。
|
|
||||||
| 参数 | 说明 | 类型 |
|
|
||||||
| :--- | :--- | :--- |
|
|
||||||
| model | 表单数据(可选),默认从`items`每一项的`field`和`initialValue`生成,如果存在同名属性,将与其合并。 | `Record<string, any>` |
|
|
||||||
| items | 表单项,具体用法看下文。| `FormItem[]` |
|
|
||||||
| submit | 提交表单的函数,可为同步/异步函数。当有返回值且返回值为包含`message`的对象时,将弹出成功提示。| `({ model, items }) => Promise<any>` |
|
|
||||||
| formProps | 传递给`AForm`组件的参数(可选),具体可参考`Arco-Design`的`Form`组件,部分参数不可用,如`model`等。 | `FormInstance['$props']` |
|
|
||||||
|
|
||||||
### 表单数据
|
|
||||||
`model`表示当前表单的数据,可为空。当使用`useForm`时,将从`items`中每一项的`field`和`initialValue`生成。如果`model`中的属性与`field`值同名,且`initialValue`值不为空,则原`model`中的同名属性值将被覆盖。
|
|
||||||
|
|
||||||
对于日期范围框、级联选择器等值为数组的组件,提供有一份便捷的语法,请看如下示例:
|
|
||||||
```typescript
|
|
||||||
const form = useForm({
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
field: `[startDate, endDate]`,
|
|
||||||
label: '日期范围',
|
|
||||||
type: 'dateRange',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: '[provice: number, city: number, town: number]',
|
|
||||||
label: '省市区',
|
|
||||||
type: 'cascader',
|
|
||||||
options: []
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
```
|
|
||||||
以上,`field` 使用的是类似Typescript元组的写法,类型目前支持 number 和 boolean,在提交时将得到如下数据:
|
|
||||||
```typescript
|
|
||||||
{
|
|
||||||
startDate: '2023',
|
|
||||||
endDate: '2024',
|
|
||||||
province: 1,
|
|
||||||
city: 2,
|
|
||||||
town: 3
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 表单项
|
|
||||||
用法示例:
|
|
||||||
```typescript
|
|
||||||
const form = useForm({
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
field: 'username',
|
|
||||||
initialValue: 'apptify',
|
|
||||||
|
|
||||||
label: '用户名称',
|
|
||||||
type: 'input',
|
|
||||||
|
|
||||||
itemProps: {},
|
|
||||||
nodeProps: {},
|
|
||||||
|
|
||||||
visible: ({ model, item, items }) => true,
|
|
||||||
disable: ({ model, item, items }) => true,
|
|
||||||
|
|
||||||
required: true,
|
|
||||||
rules: ['email'],
|
|
||||||
|
|
||||||
options: ({ model }) => api.xx(model.id)
|
|
||||||
|
|
||||||
component: ({ model, item, items }) => <div> </div>,
|
|
||||||
help: string |
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
```
|
|
||||||
用法说明:
|
|
||||||
| 参数 | 说明 | 类型 | 默认值 |
|
|
||||||
| :--- | :--- | :--- | :--- |
|
|
||||||
| field | 字段名,将合并合并到`model`中,默认值为`undefined`,可通过`initalValue`指定初始值 | string | - |
|
|
||||||
| initialValue | 初始值, 作为默认初始值以及通过`formRef.reset`重置表单数据时的值 | any | undefined |
|
|
||||||
| label | 标签名,可为字符串或函数, 作用同`AFormItem`的`label`参数 | string \| ({ model,item }) => JSX.Element | - |
|
|
||||||
| type | 输入控件的类型,具体可参考下文 | NodeType | 'input' |
|
|
||||||
| visible | 动态控制该表单项是否显示 | boolean \| ({ model,item }) => boolean | - |
|
|
||||||
| disable | 动态控制该表单项是否禁止,作用同`FormItem`的`disabled`属性 | boolean \| ({ model, item }) => boolean | - |
|
|
||||||
| required | 是否必填,作用同`AFormItem`的`required`属性 | boolean | - |
|
|
||||||
| rules | 校验规则,内置常用规则,并支持动态生效,详见下文 | RuleType[] | - |
|
|
||||||
| options | 作用域`select`等多选项组件,支持动态获取 | (Option[]) \| ({ model, item }) => Option[] | - |
|
|
||||||
| itemProps | 传递给`AFormItem`组件的参数,部分参数不可用,如上面的`field`等参数 | FormItemInstance['$props'] | - |
|
|
||||||
| nodeProps | 传递给`type`属性对应组件的参数,如当`type`为`input`时, `nodeProps`类型为`Input`组件的props。 | NodeProps | - |
|
|
||||||
|
|
||||||
### 控件类型
|
|
||||||
表单项的`type`指定表单控件的类型,当输入具体的值时,`nodeProps`会提供对应的参数类型提示。内置有常见的组件,且带有默认的参数,具体默认参数可在`src/components/form/form-node.tsx`中查看:
|
|
||||||
|
|
||||||
| 类型 | 说明 |
|
|
||||||
| :--- | :--- |
|
|
||||||
| input | 同 [Input](https://arco.design/vue/component/input) 组件
|
|
||||||
| number | 同 [InputNumber](https://arco.design/vue/component/input-number) 组件
|
|
||||||
| password | 同 [InputPassword](https://arco.design/vue/component/input#password) 组件
|
|
||||||
| select | 同 [Select](https://arco.design/vue/component/select) 组件
|
|
||||||
| time | 同 [TimePicker](https://arco.design/vue/component/time-picker) 组件
|
|
||||||
| date | 同 [DatePicker](https://arco.design/vue/component/date-picker) 组件
|
|
||||||
| dateRange | 同 [RangePicker](https://arco.design/vue/component/date-picker#range) 组件
|
|
||||||
| textarea | 同 [Textarea](https://arco.design/vue/component/textarea) 组件
|
|
||||||
| cascader | 同 [Cascader](https://arco.design/vue/component/cascader) 组件
|
|
||||||
| checkbox | 同 [Checkbox](https://arco.design/vue/component/checkbox) 组件
|
|
||||||
| radio | 同 [Radio](https://arco.design/vue/component/radio) 组件
|
|
||||||
| slider | 同 [Slider](https://arco.design/vue/component/slider) 组件
|
|
||||||
| submit | 提交表单按钮,应只有一个。
|
|
||||||
| custom | 自定义组件,通过表单项的`component`属性定义,需返回一个JSX元素。
|
|
||||||
|
|
||||||
对于`select`、`checkbox`、`radio`和`cascader`类型,其`options`参数不通过`nodeProps`传递,而是写在表单项的`options`属性。该属性支持数组和函数类型,当为数组类型时将直接传递给控件,当为函数时可动态请求,返回值需为数组类型。
|
|
||||||
|
|
||||||
以上描述,示例如下:
|
|
||||||
```typescript
|
|
||||||
const form = useForm({
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
field: 'gender',
|
|
||||||
label: '性别',
|
|
||||||
type: 'select',
|
|
||||||
options: [
|
|
||||||
{
|
|
||||||
label: '男',
|
|
||||||
value: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '女',
|
|
||||||
value: 2,
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'departmentId',
|
|
||||||
label: '部门',
|
|
||||||
type: 'cascader',
|
|
||||||
options: async ({ model, item }) => {
|
|
||||||
const res = await api.getDepartments(model.xx);
|
|
||||||
return res.data;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### 表单校验
|
|
||||||
|
|
||||||
跟表单校验相关的属性有2个,`required`(必填)和`rules`属性,其中`rules`内置常见的校验规则,参考如下:
|
|
||||||
| 校验规则 | 说明 |
|
|
||||||
| :--- | :--- |
|
|
||||||
| string | 格式为字符串 |
|
|
||||||
| number | 格式为数字 |
|
|
||||||
| passwod | 格式为密码类型,即至少包含大写字母、小写字母、数字和特殊字符。|
|
|
||||||
| required | 该项必填 |
|
|
||||||
| email | 格式为邮箱类型,例如: xx@abc.com |
|
|
||||||
| url | 格式为URL类型, 例如: https://abc.com |
|
|
||||||
| ip | 格式为IP类型, 例如: 101.10.10.302 |
|
|
||||||
| phone | 格式为11位手机号,例如: 15912345678 |
|
|
||||||
| idcard | 格式为18位身份证号,例如: 12345619991205131x |
|
|
||||||
| alphabet | 格式为26字母,例如:apptify |
|
|
||||||
|
|
||||||
当以上规则不满足需求时,可通过对象自定义校验规则,具体语法可参考`AFormItem`的 [FieldRule](https://arco.design/vue/component/form#FieldRule) 文档。在其基础上,可添加一个`disable`函数,用于动态禁止/允许当前校验规则。
|
|
||||||
|
|
||||||
用法示例:
|
|
||||||
```typescript
|
|
||||||
const form = useForm({
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
rules: [
|
|
||||||
'email',
|
|
||||||
{
|
|
||||||
match: /\d{2,3}/,
|
|
||||||
message: '请输入2~3位数字',
|
|
||||||
disable: ({ model, item, items }) => !model.username
|
|
||||||
}
|
|
||||||
],
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### 提交表单
|
|
||||||
`submit`为提交表单的函数,通常返回一个`promise`,当该函数抛出异常,则默认为提交失败。该函数有一个可选的返回值,如果返回值为包含`message`的对象时,将弹出一个包含`message`值的成功提示。
|
|
||||||
|
|
||||||
示例如下:
|
|
||||||
```typescript
|
|
||||||
const form = useForm({
|
|
||||||
submit: async ({ model, items }) => {
|
|
||||||
const res = await api.xx(model);
|
|
||||||
return { message: res.msg }
|
|
||||||
}
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### 常见问题
|
|
||||||
- Q:为什么不是模板形式?
|
|
||||||
- A:状态驱动,配置式更易于描述逻辑,模板介入和引入的组件比较多,且对于做typescript类型提示不是很方便。
|
|
||||||
- Q:为什么不是JSON形式?
|
|
||||||
- A:对于自定义组件支持、联动等不是非常友好,尽管可以通过解析字符串执行等方式实现,对typescript提示不是很友好。
|
|
||||||
|
|
||||||
### 最后
|
|
||||||
尽管看起来是低代码,但其实我更倾向于是业务组件。
|
|
||||||
|
|
@ -1,113 +0,0 @@
|
||||||
export const config = {
|
|
||||||
item: {
|
|
||||||
defaults: {
|
|
||||||
type: "input",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* 获取API错误信息
|
|
||||||
*/
|
|
||||||
getApiErrorMessage(error: any) {
|
|
||||||
return error?.response?.data?.message || error?.message || "Error";
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* 设置表单数据
|
|
||||||
*/
|
|
||||||
setModel: function setModel(model: any, data: any) {
|
|
||||||
for (const key of Object.keys(model)) {
|
|
||||||
// 数组类型
|
|
||||||
if (/^\[.+\]$/.test(key)) {
|
|
||||||
const subkeysStr = key.replaceAll(/\s/g, "").match(/^\[(.+)\]$/)?.[1];
|
|
||||||
if (!subkeysStr) {
|
|
||||||
model[key] = data[key];
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const subkeys = subkeysStr.split(",");
|
|
||||||
const value = new Array(subkeys.length);
|
|
||||||
subkeys.forEach((subkey, index) => {
|
|
||||||
if (/.+:number$/.test(subkey)) {
|
|
||||||
subkey = subkey.replace(/:number$/, "");
|
|
||||||
value[index] = Number(data[subkey]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (/.+:boolean$/.test(subkey)) {
|
|
||||||
subkey = subkey.replace(/:boolean$/, "");
|
|
||||||
value[index] = Boolean(data[subkey]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
value[index] = data[subkey];
|
|
||||||
});
|
|
||||||
model[key] = value;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// 默认类型
|
|
||||||
model[key] = data[key];
|
|
||||||
}
|
|
||||||
return model;
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* 获取表单数据
|
|
||||||
*/
|
|
||||||
getModel: function getModel(model: Record<string, any>) {
|
|
||||||
const data: any = {};
|
|
||||||
for (const [key, val] of Object.entries(model)) {
|
|
||||||
// 数组类型
|
|
||||||
if (/^\[.+\]$/.test(key)) {
|
|
||||||
const subkeysStr = key.replaceAll(/\s/g, "").match(/^\[(.+)\]$/)?.[1];
|
|
||||||
if (!subkeysStr) {
|
|
||||||
data[key] = val;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const subkeys = subkeysStr.split(",");
|
|
||||||
subkeys.forEach((subkey, index) => {
|
|
||||||
if (/(.+)?:number$/.test(subkey)) {
|
|
||||||
subkey = subkey.replace(/:number$/, "");
|
|
||||||
data[subkey] = val?.[index] && Number(val[index]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (/(.+)?:boolean$/.test(subkey)) {
|
|
||||||
subkey = subkey.replace(/:boolean$/, "");
|
|
||||||
data[subkey] = val?.[index] && Boolean(val[index]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
data[subkey] = val?.[index];
|
|
||||||
});
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// 默认类型
|
|
||||||
data[key] = val;
|
|
||||||
}
|
|
||||||
return data;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export function initOptions({ item, model }: any, key = "options") {
|
|
||||||
if (Array.isArray(item.options)) {
|
|
||||||
item.nodeProps[key] = item.options;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (item.options && typeof item.options === "object") {
|
|
||||||
const { value, source } = item.options;
|
|
||||||
item._updateOptions = async () => {};
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (typeof item.options === "function") {
|
|
||||||
const loadData = item.options;
|
|
||||||
item.nodeProps[key] = reactive([]);
|
|
||||||
item._updateOptions = async () => {
|
|
||||||
let data = await loadData({ item, model });
|
|
||||||
if (Array.isArray(data?.data?.data)) {
|
|
||||||
data = data.data.data.map((i: any) => ({
|
|
||||||
...i,
|
|
||||||
label: i.name,
|
|
||||||
value: i.id,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
if (Array.isArray(data)) {
|
|
||||||
item.nodeProps[key].splice(0);
|
|
||||||
item.nodeProps[key].push(...data);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
item._updateOptions();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,180 +0,0 @@
|
||||||
import { FormItem as BaseFormItem, FieldRule, FormItemInstance, SelectOptionData } from "@arco-design/web-vue";
|
|
||||||
import { NodeType, NodeUnion, nodeMap } from "./form-node";
|
|
||||||
import { RuleMap } from "./form-rules";
|
|
||||||
|
|
||||||
export type FieldStringRule = keyof typeof RuleMap;
|
|
||||||
export type FieldObjectRule = FieldRule & {
|
|
||||||
disable?: (arg: { item: IFormItem; model: Record<string, any> }) => boolean;
|
|
||||||
};
|
|
||||||
export type FieldRuleType = FieldStringRule | FieldObjectRule;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 表单项
|
|
||||||
*/
|
|
||||||
export const FormItem = (props: any, { emit }: any) => {
|
|
||||||
const { item } = props;
|
|
||||||
const args = {
|
|
||||||
...props,
|
|
||||||
field: item.field,
|
|
||||||
};
|
|
||||||
|
|
||||||
const rules = computed(() => {
|
|
||||||
const result = [];
|
|
||||||
if (item.required) {
|
|
||||||
result.push(RuleMap.required);
|
|
||||||
}
|
|
||||||
item.rules?.forEach((rule: any) => {
|
|
||||||
if (typeof rule === "string") {
|
|
||||||
result.push(RuleMap[rule as FieldStringRule]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!rule.disable) {
|
|
||||||
result.push(rule);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!rule.disable({ model: props.model, item, items: props.items })) {
|
|
||||||
result.push(rule);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return result;
|
|
||||||
});
|
|
||||||
|
|
||||||
const disabled = computed(() => {
|
|
||||||
if (item.disable === undefined) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (typeof item.disable === "function") {
|
|
||||||
return item.disable(args);
|
|
||||||
}
|
|
||||||
return item.disable;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (item.visible && !item.visible(args)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<BaseFormItem {...item.itemProps} rules={rules.value} disabled={disabled.value} field={item.field}>
|
|
||||||
{{
|
|
||||||
default: () => {
|
|
||||||
if (item.component) {
|
|
||||||
return <item.component {...item.nodeProps} model={props.model} item={props.item} />;
|
|
||||||
}
|
|
||||||
const comp = nodeMap[item.type as NodeType]?.component;
|
|
||||||
if (!comp) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (item.type === "submit") {
|
|
||||||
return <comp loading={props.loading} onSubmit={() => emit("submit")} onCancel={emit("cancel")} />;
|
|
||||||
}
|
|
||||||
return <comp v-model={props.model[item.field]} {...item.nodeProps} />;
|
|
||||||
},
|
|
||||||
label: item.label && (() => (typeof item.label === "string" ? item.label : item.label?.(args))),
|
|
||||||
help: item.help && (() => (typeof item.help === "string" ? item.help : item.help?.(args))),
|
|
||||||
extra: item.extra && (() => (typeof item.extra === "string" ? item.extra : item.extra?.(args))),
|
|
||||||
}}
|
|
||||||
</BaseFormItem>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
type FormItemBase = {
|
|
||||||
/**
|
|
||||||
* 字段名,用于表单、校验和输入框绑定,支持特殊语法。
|
|
||||||
* @example
|
|
||||||
* ```typescript
|
|
||||||
* {
|
|
||||||
* field: '[v1,v2]',
|
|
||||||
* type: 'dateRange',
|
|
||||||
* }
|
|
||||||
* // 将得到
|
|
||||||
* {
|
|
||||||
* v1: '2021',
|
|
||||||
* v2: '2021',
|
|
||||||
* }
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
field: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 初始值
|
|
||||||
* @description 若指定该参数,将覆盖model中的同名属性。
|
|
||||||
*/
|
|
||||||
initial?: any;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 标签名
|
|
||||||
* @description 同FormItem组件的label属性
|
|
||||||
*/
|
|
||||||
label?: string | ((args: { item: IFormItem; model: Record<string, any> }) => any);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 传递给`FormItem`组件的参数
|
|
||||||
* @description 部分属性会不可用,如field、label、required、rules、disabled等
|
|
||||||
*/
|
|
||||||
itemProps?: Partial<Omit<FormItemInstance["$props"], "field" | "label" | "required" | "rules" | "disabled">>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 是否必填
|
|
||||||
* @description 默认值为false
|
|
||||||
*/
|
|
||||||
required?: boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 校验规则数组
|
|
||||||
* @description 支持字符串(内置)、对象形式
|
|
||||||
* @example
|
|
||||||
* ```typescript
|
|
||||||
* rules: [
|
|
||||||
* // 内置
|
|
||||||
* 'idcard',
|
|
||||||
* // 自定义
|
|
||||||
* {
|
|
||||||
* match: /\d+/,
|
|
||||||
* message: '请输入数字',
|
|
||||||
* },
|
|
||||||
* ]
|
|
||||||
*```
|
|
||||||
* @see https://arco.design/vue/component/form#FieldRule
|
|
||||||
*/
|
|
||||||
rules?: FieldRuleType[];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 是否可见
|
|
||||||
* @description 动态控制表单项是否可见
|
|
||||||
*/
|
|
||||||
visible?: (arg: { item: IFormItem; model: Record<string, any> }) => boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 是否禁用
|
|
||||||
* @description 动态控制表单项是否禁用
|
|
||||||
*/
|
|
||||||
disable?: (arg: { item: IFormItem; model: Record<string, any> }) => boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 选项,数组或者函数
|
|
||||||
* @description 用于下拉框、单选框、多选框等组件, 支持动态加载
|
|
||||||
*/
|
|
||||||
options?: SelectOptionData[] | ((arg: { item: IFormItem; model: Record<string, any> }) => Promise<any>);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 表单项内容的渲染函数
|
|
||||||
* @description 用于自定义表单项内容
|
|
||||||
*/
|
|
||||||
component?: (args: { item: IFormItem; model: Record<string, any>; field: string }) => any;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 帮助提示
|
|
||||||
* @description 同FormItem组件的help插槽
|
|
||||||
* @see https://arco.design/vue/component/form#form-item%20Slots
|
|
||||||
*/
|
|
||||||
help?: string | ((args: { item: IFormItem; model: Record<string, any> }) => any);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 额外内容
|
|
||||||
* @description 同FormItem组件的extra插槽
|
|
||||||
* @see https://arco.design/vue/component/form#form-item%20Slots
|
|
||||||
*/
|
|
||||||
extra?: string | ((args: { item: IFormItem; model: Record<string, any> }) => any);
|
|
||||||
};
|
|
||||||
|
|
||||||
export type IFormItem = FormItemBase & NodeUnion;
|
|
||||||
|
|
@ -1,194 +0,0 @@
|
||||||
import { Button, ButtonInstance, FormInstance, Message, Modal } from "@arco-design/web-vue";
|
|
||||||
import { assign, cloneDeep, omit } from "lodash-es";
|
|
||||||
import { PropType, VNode, defineComponent } from "vue";
|
|
||||||
import { Form } from "./form";
|
|
||||||
import { config } from "./form-config";
|
|
||||||
import { IFormItem } from "./form-item";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 表单弹窗组件
|
|
||||||
*/
|
|
||||||
export const FormModal = defineComponent({
|
|
||||||
name: "FormModal",
|
|
||||||
inheritAttrs: false,
|
|
||||||
props: {
|
|
||||||
/**
|
|
||||||
* 弹窗标题
|
|
||||||
* @default '添加'
|
|
||||||
*/
|
|
||||||
title: {
|
|
||||||
type: [String, Function] as PropType<
|
|
||||||
string | ((args: { model: Record<string, any>; items: IFormItem[] }) => string)
|
|
||||||
>,
|
|
||||||
default: "添加",
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* 触发元素
|
|
||||||
*/
|
|
||||||
trigger: {
|
|
||||||
type: [Boolean, Function, Object] as PropType<
|
|
||||||
| boolean
|
|
||||||
| ((props: { model: any; items: any[] }) => VNode)
|
|
||||||
| {
|
|
||||||
text?: string;
|
|
||||||
buttonProps?: ButtonInstance["$props"];
|
|
||||||
}
|
|
||||||
>,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* 传递给Modal组件的props
|
|
||||||
*/
|
|
||||||
modalProps: {
|
|
||||||
type: Object as PropType<Omit<InstanceType<typeof Modal>["$props"], "visible" | "title" | "onBeforeOk">>,
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* 表单数据
|
|
||||||
*/
|
|
||||||
model: {
|
|
||||||
type: Object as PropType<Record<any, any>>,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* 表单各项
|
|
||||||
*/
|
|
||||||
items: {
|
|
||||||
type: Array as PropType<IFormItem[]>,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* 提交表单的函数
|
|
||||||
* @description 可返回`{ message }`类型,用于显示提示信息
|
|
||||||
*/
|
|
||||||
submit: {
|
|
||||||
type: Function as PropType<(args: { model: any; items: IFormItem[] }) => PromiseLike<any>>,
|
|
||||||
default: () => true,
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* 传递给Form组件的props
|
|
||||||
*/
|
|
||||||
formProps: {
|
|
||||||
type: Object as PropType<Omit<FormInstance["$props"], "model">>,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
emits: ["close", "submited"],
|
|
||||||
setup(props, { slots, emit, attrs }) {
|
|
||||||
const origin = cloneDeep(props.model);
|
|
||||||
const formRef = ref<InstanceType<typeof Form>>();
|
|
||||||
const loading = ref(false);
|
|
||||||
const visible = ref(false);
|
|
||||||
|
|
||||||
const open = async (data: Record<string, any> = {}) => {
|
|
||||||
visible.value = true;
|
|
||||||
await nextTick();
|
|
||||||
config.setModel(props.model, data);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onBeforeOk = async () => {
|
|
||||||
if (typeof attrs.onBeforeOk === "function") {
|
|
||||||
const isOk = await attrs.onBeforeOk();
|
|
||||||
if (!isOk) return false;
|
|
||||||
}
|
|
||||||
const errors = await formRef.value?.formRef?.validate();
|
|
||||||
if (errors) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const model = formRef.value?.getModel() || {};
|
|
||||||
const res = await props.submit?.({ items: props.items, model });
|
|
||||||
res?.data?.message && Message.success(`提示: ${res.data.message}`);
|
|
||||||
emit("submited", res);
|
|
||||||
} catch (error: any) {
|
|
||||||
const message = config.getApiErrorMessage(error);
|
|
||||||
if (message) {
|
|
||||||
Message.error(`提示: ${message}`);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const onClose = () => {
|
|
||||||
visible.value = false;
|
|
||||||
assign(props.model, origin);
|
|
||||||
emit("close");
|
|
||||||
};
|
|
||||||
|
|
||||||
const modalTitle = computed(() => {
|
|
||||||
if (typeof props.title === "string") {
|
|
||||||
return props.title;
|
|
||||||
}
|
|
||||||
if (typeof props.title === "function") {
|
|
||||||
return props.title({ model: props.model, items: props.items });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const modalTrigger = computed(() => {
|
|
||||||
if (!props.trigger) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
let content;
|
|
||||||
if (typeof props.trigger === "boolean" || typeof props.trigger === "string") {
|
|
||||||
content = (
|
|
||||||
<Button type="primary">
|
|
||||||
{{
|
|
||||||
default: () => (typeof props.trigger === "string" ? props.trigger : "新增"),
|
|
||||||
icon: () => <i class="icon-park-outline-add" />,
|
|
||||||
}}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (typeof props.trigger === "function") {
|
|
||||||
content = props.trigger({ model: props.model, items: props.items });
|
|
||||||
}
|
|
||||||
if (typeof props.trigger === "object") {
|
|
||||||
content = (
|
|
||||||
<Button type="primary" {...props.trigger.buttonProps}>
|
|
||||||
{props.trigger?.text || "新增"}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (slots.trigger) {
|
|
||||||
content = slots.trigger({ model: props.model, items: props.items });
|
|
||||||
}
|
|
||||||
return <span onClick={() => open()}>{content}</span>;
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
origin,
|
|
||||||
formRef,
|
|
||||||
loading,
|
|
||||||
visible,
|
|
||||||
modalTitle,
|
|
||||||
modalTrigger,
|
|
||||||
open,
|
|
||||||
onClose,
|
|
||||||
onBeforeOk,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{this.modalTrigger}
|
|
||||||
<Modal
|
|
||||||
{...this.modalProps}
|
|
||||||
v-model:visible={this.visible}
|
|
||||||
onBeforeOk={this.onBeforeOk}
|
|
||||||
onClose={this.onClose}
|
|
||||||
title={this.modalTitle}
|
|
||||||
class="ani-form-modal"
|
|
||||||
>
|
|
||||||
{this.visible && (
|
|
||||||
<Form ref={(el: any) => (this.formRef = el)} {...this.formProps} model={this.model} items={this.items}>
|
|
||||||
{{ ...this.$slots }}
|
|
||||||
</Form>
|
|
||||||
)}
|
|
||||||
</Modal>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export type FormModalInstance = InstanceType<typeof FormModal>;
|
|
||||||
export type FormModalProps = FormModalInstance["$props"];
|
|
||||||
export default FormModal;
|
|
||||||
|
|
@ -1,233 +0,0 @@
|
||||||
import {
|
|
||||||
AutoComplete,
|
|
||||||
Button,
|
|
||||||
Cascader,
|
|
||||||
CheckboxGroup,
|
|
||||||
DatePicker,
|
|
||||||
Input,
|
|
||||||
InputNumber,
|
|
||||||
InputPassword,
|
|
||||||
InputSearch,
|
|
||||||
RadioGroup,
|
|
||||||
RangePicker,
|
|
||||||
Select,
|
|
||||||
Slider,
|
|
||||||
Textarea,
|
|
||||||
TimePicker,
|
|
||||||
TreeSelect,
|
|
||||||
} from "@arco-design/web-vue";
|
|
||||||
import { initOptions } from "./form-config";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 表单项组件映射
|
|
||||||
*/
|
|
||||||
export const nodeMap = {
|
|
||||||
/**
|
|
||||||
* 输入框
|
|
||||||
*/
|
|
||||||
input: {
|
|
||||||
component: Input,
|
|
||||||
nodeProps: {
|
|
||||||
placeholder: "请输入",
|
|
||||||
allowClear: true,
|
|
||||||
} as InstanceType<typeof Input>["$props"],
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* 搜索框
|
|
||||||
*/
|
|
||||||
search: {
|
|
||||||
component: InputSearch,
|
|
||||||
nodeProps: {
|
|
||||||
placeholder: "请输入",
|
|
||||||
allowClear: true,
|
|
||||||
} as InstanceType<typeof Input>["$props"] & InstanceType<typeof InputSearch>["$props"],
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* 文本域
|
|
||||||
*/
|
|
||||||
textarea: {
|
|
||||||
component: Textarea,
|
|
||||||
nodeProps: {
|
|
||||||
placeholder: "请输入",
|
|
||||||
allowClear: true,
|
|
||||||
} as InstanceType<typeof Textarea>["$props"],
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* 数值输入框
|
|
||||||
*/
|
|
||||||
number: {
|
|
||||||
component: InputNumber,
|
|
||||||
nodeProps: {
|
|
||||||
placeholder: "请输入",
|
|
||||||
defaultValue: 0,
|
|
||||||
allowClear: true,
|
|
||||||
} as InstanceType<typeof InputNumber>["$props"],
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* 密码输入框
|
|
||||||
*/
|
|
||||||
password: {
|
|
||||||
component: InputPassword,
|
|
||||||
nodeProps: {
|
|
||||||
placeholder: "请输入",
|
|
||||||
} as InstanceType<typeof InputPassword>["$props"],
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* 选择框
|
|
||||||
*/
|
|
||||||
select: {
|
|
||||||
component: Select,
|
|
||||||
nodeProps: {
|
|
||||||
placeholder: "请选择",
|
|
||||||
allowClear: true,
|
|
||||||
allowSearch: true,
|
|
||||||
options: [{}],
|
|
||||||
} as InstanceType<typeof Select>["$props"],
|
|
||||||
init: initOptions,
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* 选择框
|
|
||||||
*/
|
|
||||||
treeSelect: {
|
|
||||||
component: TreeSelect,
|
|
||||||
nodeProps: {
|
|
||||||
placeholder: "请选择",
|
|
||||||
allowClear: true,
|
|
||||||
allowSearch: true,
|
|
||||||
options: [],
|
|
||||||
onChange(value) {
|
|
||||||
value;
|
|
||||||
},
|
|
||||||
} as InstanceType<typeof TreeSelect>["$props"],
|
|
||||||
init: (arg: any) => initOptions(arg, "data"),
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* 级联选择框
|
|
||||||
*/
|
|
||||||
cascader: {
|
|
||||||
component: Cascader,
|
|
||||||
init: initOptions,
|
|
||||||
nodeProps: {
|
|
||||||
placeholder: "请选择",
|
|
||||||
allowClear: true,
|
|
||||||
expandTrigger: "hover",
|
|
||||||
} as InstanceType<typeof Cascader>["$props"],
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* 时间选择框
|
|
||||||
*/
|
|
||||||
time: {
|
|
||||||
component: TimePicker,
|
|
||||||
nodeProps: {
|
|
||||||
allowClear: true,
|
|
||||||
} as InstanceType<typeof TimePicker>["$props"],
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* 日期选择框
|
|
||||||
*/
|
|
||||||
date: {
|
|
||||||
component: DatePicker,
|
|
||||||
nodeProps: {
|
|
||||||
allowClear: true,
|
|
||||||
} as InstanceType<typeof DatePicker>["$props"],
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* 日期范围选择框
|
|
||||||
*/
|
|
||||||
dateRange: {
|
|
||||||
component: RangePicker,
|
|
||||||
nodeProps: {
|
|
||||||
allowClear: true,
|
|
||||||
} as InstanceType<typeof RangePicker>["$props"],
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* 复选框
|
|
||||||
*/
|
|
||||||
checkbox: {
|
|
||||||
component: CheckboxGroup,
|
|
||||||
nodeProps: {
|
|
||||||
allowClear: true,
|
|
||||||
} as InstanceType<typeof CheckboxGroup>["$props"],
|
|
||||||
init: initOptions,
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* 复选框
|
|
||||||
*/
|
|
||||||
radio: {
|
|
||||||
component: RadioGroup,
|
|
||||||
nodeProps: {
|
|
||||||
allowClear: true,
|
|
||||||
} as InstanceType<typeof RadioGroup>["$props"],
|
|
||||||
init: initOptions,
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* 滑动输入条
|
|
||||||
*/
|
|
||||||
slider: {
|
|
||||||
component: Slider,
|
|
||||||
nodeProps: {
|
|
||||||
allowClear: true,
|
|
||||||
} as InstanceType<typeof Slider>["$props"],
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* 自动完成输入框
|
|
||||||
*/
|
|
||||||
autoComplete: {
|
|
||||||
component: AutoComplete,
|
|
||||||
nodeProps: {
|
|
||||||
allowClear: true,
|
|
||||||
} as InstanceType<typeof AutoComplete>["$props"],
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* 底部
|
|
||||||
*/
|
|
||||||
submit: {
|
|
||||||
component: (props: any, { emit }: any) => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Button type="primary" loading={props.loading} onClick={() => emit("submit")} class="mr-3">
|
|
||||||
立即提交
|
|
||||||
</Button>
|
|
||||||
{/* <Button loading={props.loading} onClick={() => emit("cancel")}>
|
|
||||||
重置
|
|
||||||
</Button> */}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
nodeProps: {},
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* 自定义组件
|
|
||||||
*/
|
|
||||||
custom: {
|
|
||||||
nodeProps: {},
|
|
||||||
component: () => null,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 所有组件类型
|
|
||||||
*/
|
|
||||||
export type NodeMap = typeof nodeMap;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 组件类型
|
|
||||||
*/
|
|
||||||
export type NodeType = keyof NodeMap;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 提供给`FormItem`的联合类型
|
|
||||||
* @description 当输入type,nodeProps会提供对应类型提示
|
|
||||||
*/
|
|
||||||
export type NodeUnion = {
|
|
||||||
[key in NodeType]: {
|
|
||||||
/**
|
|
||||||
* 输入框类型,默认为`input`
|
|
||||||
*/
|
|
||||||
type: key;
|
|
||||||
/**
|
|
||||||
* 传递给`type`属性对应组件的参数
|
|
||||||
*/
|
|
||||||
nodeProps?: NodeMap[key]["nodeProps"];
|
|
||||||
};
|
|
||||||
}[NodeType];
|
|
||||||
|
|
@ -1,46 +0,0 @@
|
||||||
import { FieldRule } from "@arco-design/web-vue";
|
|
||||||
|
|
||||||
const defineRuleMap = <T extends Record<string, FieldRule>>(ruleMap: T) => ruleMap;
|
|
||||||
|
|
||||||
export const RuleMap = defineRuleMap({
|
|
||||||
required: {
|
|
||||||
required: true,
|
|
||||||
message: "该项不能为空",
|
|
||||||
},
|
|
||||||
string: {
|
|
||||||
type: "string",
|
|
||||||
message: "请输入字符串",
|
|
||||||
},
|
|
||||||
number: {
|
|
||||||
type: "number",
|
|
||||||
message: "请输入数字",
|
|
||||||
},
|
|
||||||
email: {
|
|
||||||
type: "email",
|
|
||||||
message: "邮箱格式错误,示例: xx@abc.com",
|
|
||||||
},
|
|
||||||
url: {
|
|
||||||
type: "url",
|
|
||||||
message: "URL格式错误, 示例: www.abc.com",
|
|
||||||
},
|
|
||||||
ip: {
|
|
||||||
type: "ip",
|
|
||||||
message: "IP格式错误, 示例: 101.10.10.30",
|
|
||||||
},
|
|
||||||
phone: {
|
|
||||||
match: /^(?:(?:\+|00)86)?1\d{10}$/,
|
|
||||||
message: "手机格式错误, 示例(11位): 15912345678",
|
|
||||||
},
|
|
||||||
idcard: {
|
|
||||||
match: /^[1-9]\d{5}(?:18|19|20)\d{2}(?:0[1-9]|10|11|12)(?:0[1-9]|[1-2]\d|30|31)\d{3}[\dXx]$/,
|
|
||||||
message: "身份证格式错误, 长度为15或18位",
|
|
||||||
},
|
|
||||||
alphabet: {
|
|
||||||
match: /^[a-zA-Z]\w{4,15}$/,
|
|
||||||
message: "请输入英文字母, 长度为4~15位",
|
|
||||||
},
|
|
||||||
password: {
|
|
||||||
match: /^\S*(?=\S{6,})(?=\S*\d)(?=\S*[A-Z])(?=\S*[a-z])(?=\S*[!@#$%^&*? ])\S*$/,
|
|
||||||
message: "至少包含大写字母、小写字母、数字和特殊字符",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
@ -1,120 +0,0 @@
|
||||||
import { Form as BaseForm, FormInstance as BaseFormInstance, Message } from "@arco-design/web-vue";
|
|
||||||
import { assign, cloneDeep, defaultsDeep } from "lodash-es";
|
|
||||||
import { PropType } from "vue";
|
|
||||||
import { config } from "./form-config";
|
|
||||||
import { FormItem, IFormItem } from "./form-item";
|
|
||||||
import { NodeType, nodeMap } from "./form-node";
|
|
||||||
|
|
||||||
type SubmitFn = (arg: { model: Record<string, any>; items: IFormItem[] }) => Promise<any>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 表单组件
|
|
||||||
*/
|
|
||||||
export const Form = defineComponent({
|
|
||||||
name: "Form",
|
|
||||||
props: {
|
|
||||||
/**
|
|
||||||
* 表单数据
|
|
||||||
*/
|
|
||||||
model: {
|
|
||||||
type: Object as PropType<Record<any, any>>,
|
|
||||||
default: () => reactive({}),
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* 表单项
|
|
||||||
*/
|
|
||||||
items: {
|
|
||||||
type: Array as PropType<IFormItem[]>,
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* 提交表单
|
|
||||||
*/
|
|
||||||
submit: {
|
|
||||||
type: Function as PropType<SubmitFn>,
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* 传给Form组件的参数
|
|
||||||
*/
|
|
||||||
formProps: {
|
|
||||||
type: Object as PropType<Omit<BaseFormInstance["$props"], "model">>,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
setup(props) {
|
|
||||||
const model = cloneDeep(props.model);
|
|
||||||
const formRef = ref<InstanceType<typeof BaseForm>>();
|
|
||||||
const loading = ref(false);
|
|
||||||
|
|
||||||
props.items.forEach((item: any) => {
|
|
||||||
const node = nodeMap[item.type as NodeType];
|
|
||||||
defaultsDeep(item, { nodeProps: node?.nodeProps ?? {} });
|
|
||||||
(node as any)?.init?.({ item, model: props.model });
|
|
||||||
});
|
|
||||||
|
|
||||||
const getItem = (field: string) => {
|
|
||||||
return props.items.find((item) => item.field === field);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getModel = () => {
|
|
||||||
return config.getModel(props.model);
|
|
||||||
};
|
|
||||||
|
|
||||||
const setModel = (data: Record<string, any>) => {
|
|
||||||
config.setModel(props.model, data);
|
|
||||||
};
|
|
||||||
|
|
||||||
const resetModel = () => {
|
|
||||||
assign(props.model, model);
|
|
||||||
};
|
|
||||||
|
|
||||||
const submitForm = async () => {
|
|
||||||
if (await formRef.value?.validate()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const model: Record<string, any> = getModel();
|
|
||||||
try {
|
|
||||||
loading.value = true;
|
|
||||||
const res = await props.submit?.({ model, items: props.items });
|
|
||||||
res?.message && Message.success(`提示: ${res.message}`);
|
|
||||||
} catch (error: any) {
|
|
||||||
const message = error?.response?.data?.message || error?.message;
|
|
||||||
message && Message.error(`提示: ${message}`);
|
|
||||||
} finally {
|
|
||||||
loading.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
formRef,
|
|
||||||
loading,
|
|
||||||
getItem,
|
|
||||||
submitForm,
|
|
||||||
resetModel,
|
|
||||||
setModel,
|
|
||||||
getModel,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
render() {
|
|
||||||
(this.items as any).instance = this;
|
|
||||||
|
|
||||||
const props = {
|
|
||||||
items: this.items,
|
|
||||||
model: this.model,
|
|
||||||
slots: this.$slots,
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<BaseForm ref="formRef" layout="vertical" model={this.model} {...this.$attrs} {...this.formProps}>
|
|
||||||
{this.items.map((item) => (
|
|
||||||
<FormItem loading={this.loading} onSubmit={this.submitForm} item={item} {...props}></FormItem>
|
|
||||||
))}
|
|
||||||
</BaseForm>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export type FormInstance = InstanceType<typeof Form>;
|
|
||||||
|
|
||||||
export type FormProps = FormInstance["$props"];
|
|
||||||
|
|
||||||
export type FormDefinedProps = Pick<FormProps, "model" | "items" | "submit" | "formProps">;
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
export * from "./form";
|
|
||||||
export * from "./use-form";
|
|
||||||
export * from "./form-modal";
|
|
||||||
export * from "./use-form-modal";
|
|
||||||
|
|
@ -1,61 +0,0 @@
|
||||||
import { Modal } from "@arco-design/web-vue";
|
|
||||||
import { merge } from "lodash-es";
|
|
||||||
import { Component, Ref, reactive } from "vue";
|
|
||||||
import { useForm } from "./use-form";
|
|
||||||
import FormModal, { FormModalInstance, FormModalProps } from "./form-modal";
|
|
||||||
|
|
||||||
const defaults: Partial<InstanceType<typeof Modal>> = {
|
|
||||||
width: 1080,
|
|
||||||
titleAlign: "start",
|
|
||||||
closable: false,
|
|
||||||
maskClosable: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 构建传给FormModal组件的参数
|
|
||||||
* @see src/components/form/use-form-modal.tsx
|
|
||||||
*/
|
|
||||||
export const useFormModal = (options: Partial<FormModalProps>): FormModalProps => {
|
|
||||||
const { model = {}, items = [] } = options || {};
|
|
||||||
const form = useForm({ model, items });
|
|
||||||
|
|
||||||
return reactive(
|
|
||||||
merge(
|
|
||||||
{
|
|
||||||
modalProps: { ...defaults },
|
|
||||||
formProps: {
|
|
||||||
layout: "vertical",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
...options,
|
|
||||||
...form,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
interface Context {
|
|
||||||
props: any;
|
|
||||||
modalRef: Ref<FormModalInstance | null>;
|
|
||||||
open: (args?: Record<string, any>) => Promise<void> | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useAniFormModal = (options: Partial<FormModalProps>): [Component, Context] => {
|
|
||||||
const props = useFormModal(options);
|
|
||||||
const modalRef = ref<FormModalInstance | null>(null);
|
|
||||||
const onModalRef = (el: any) => (modalRef.value = el);
|
|
||||||
const component = defineComponent({
|
|
||||||
name: "AniFormModalWrapper",
|
|
||||||
render() {
|
|
||||||
return <FormModal {...this.$attrs} {...props} ref={onModalRef} />;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const component1 = (p: any) => <FormModal {...p} {...props} ref={onModalRef} />;
|
|
||||||
const context = {
|
|
||||||
props,
|
|
||||||
modalRef,
|
|
||||||
open: (args?: Record<string, any>) => modalRef.value?.open(args),
|
|
||||||
};
|
|
||||||
return [component1, context];
|
|
||||||
};
|
|
||||||
|
|
@ -1,59 +0,0 @@
|
||||||
import { FormInstance } from "@arco-design/web-vue";
|
|
||||||
import { merge } from "lodash-es";
|
|
||||||
import { IFormItem } from "./form-item";
|
|
||||||
|
|
||||||
export type Options = {
|
|
||||||
/**
|
|
||||||
* 表单数据模型
|
|
||||||
*/
|
|
||||||
model?: Record<string, any>;
|
|
||||||
/**
|
|
||||||
* 表单项数组
|
|
||||||
*/
|
|
||||||
items: IFormItem[];
|
|
||||||
/**
|
|
||||||
* 提交表单
|
|
||||||
*/
|
|
||||||
submit?: (arg: { model: Record<string, any>; items: IFormItem[] }) => Promise<any>;
|
|
||||||
/**
|
|
||||||
* 表单实例属性
|
|
||||||
*/
|
|
||||||
formProps?: Partial<FormInstance["$props"]>;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 构建表单组件的参数
|
|
||||||
* @see src/components/form/use-form.tsx
|
|
||||||
*/
|
|
||||||
export const useForm = (options: Options) => {
|
|
||||||
const { model: _model = {} } = options;
|
|
||||||
const model: Record<string, any> = { id: undefined, ..._model };
|
|
||||||
const items: IFormItem[] = [];
|
|
||||||
|
|
||||||
for (const item of options.items) {
|
|
||||||
if (!item.nodeProps) {
|
|
||||||
item.nodeProps = {} as any;
|
|
||||||
}
|
|
||||||
model[item.field] = model[item.field] ?? item.initial;
|
|
||||||
items.push(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.submit) {
|
|
||||||
const submit = items.find((item) => item.type === "submit") || {};
|
|
||||||
items.push(
|
|
||||||
merge(
|
|
||||||
{},
|
|
||||||
{
|
|
||||||
field: "id",
|
|
||||||
type: "submit",
|
|
||||||
itemProps: {
|
|
||||||
hideLabel: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
submit
|
|
||||||
) as any
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return reactive({ ...options, model, items }) as any;
|
|
||||||
};
|
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
import { cloneDeep } from "lodash-es";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取表单数据
|
|
||||||
*/
|
|
||||||
export function getModel(model: any) {
|
|
||||||
const data: Record<string, any> = {};
|
|
||||||
for (const key of Object.keys(model)) {
|
|
||||||
if (/[^:]+:[^:]+/.test(key)) {
|
|
||||||
const keys = key.split(":");
|
|
||||||
const vals = cloneDeep(model[key] || []);
|
|
||||||
for (const k of keys) {
|
|
||||||
data[k] = vals.shift();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
data[key] = cloneDeep(model[key]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置表单数据
|
|
||||||
*/
|
|
||||||
export function setModel(model: any, data: Record<string, any>) {
|
|
||||||
for (const key of Object.keys(model)) {
|
|
||||||
if (/[^:]+:[^:]+/.test(key)) {
|
|
||||||
const subKeys = key.split(":");
|
|
||||||
model[key] = subKeys.map((k) => data[k]);
|
|
||||||
} else {
|
|
||||||
model[key] = data[key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
export * from './form';
|
|
||||||
export * from './table';
|
|
||||||
|
|
@ -1,162 +0,0 @@
|
||||||
### 基本用法
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
<template>
|
|
||||||
<Table v-bind="table" />
|
|
||||||
</template>
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { Table, useTable } from '@/components'
|
|
||||||
const table = useTable({
|
|
||||||
data: (search, paging) => {
|
|
||||||
return {
|
|
||||||
data: [
|
|
||||||
{
|
|
||||||
username: '用户A'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
total: 30
|
|
||||||
};
|
|
||||||
},
|
|
||||||
columns: [
|
|
||||||
{
|
|
||||||
title: "用户名称",
|
|
||||||
dataIndex: "username",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
pagination: {
|
|
||||||
pageSize: 10,
|
|
||||||
showTotal: true
|
|
||||||
},
|
|
||||||
search: {
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
extend: "username",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
title: "新建用户",
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
field: "username",
|
|
||||||
label: "用户名称",
|
|
||||||
type: "input",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
submit: async ({ model }) => {
|
|
||||||
return api.xx(model);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
modify: {
|
|
||||||
title: "修改用户",
|
|
||||||
extend: true,
|
|
||||||
submit: async ({ model }) => {
|
|
||||||
return api.xx(model);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
```
|
|
||||||
|
|
||||||
以上,就是一个 CRUD 表格的简单用法。参数描述:
|
|
||||||
| 参数 | 说明 | 类型 |
|
|
||||||
| :--- | :--- | :--- |
|
|
||||||
| data | 表格数据,可为数组或函数(发起 HTTP 请求) | BaseData[] | ((search, paging) => Promise<any>) |
|
|
||||||
| columns | 表格列,参见 [TableColumnData](https://arco.design/vue/component/table#TableColumnData) 文档,增加和扩展部分属性,详见下文。 | TableColumnData[] |
|
|
||||||
| pagination | 分页参数,参见 [Pagination](https://arco.design/vue/component/pagination) 文档,默认 15/每页。| Pagination |
|
|
||||||
| search | 搜索表单的配置,参见 [Form]() 说明,其中 `submit` 参数不可用 | FormProps |
|
|
||||||
| common | 新增和修改表单弹窗的公用参数,参见 [FormModal]() 说明。 | FormModalProps |
|
|
||||||
| create | 新增表单弹窗的参数,参见 [FormModal]() 说明, 将与`common`参数合并。 | FormModalProps |
|
|
||||||
| modify | 修改表单弹窗的参数,参见 [FormModal]() 说明, 将与`common`参数合并。 | FormModalProps |
|
|
||||||
| tableProps | 传递给`Table`组件的参数,参见 [Table](https://arco.design/vue/component/table) 文档,其中`columns`参数不可用。| TableProps |
|
|
||||||
|
|
||||||
### 表格数据
|
|
||||||
|
|
||||||
`data`定义表格数据,可以是数组或函数。
|
|
||||||
|
|
||||||
- 当是数组时,直接用作数据源。
|
|
||||||
- 当是函数时,传入查询参数和分页参数,可返回数组或对象,返回数组作用同上,返回对象时需遵循`{ data: [], total: number }`格式,用于分页处理。
|
|
||||||
|
|
||||||
用法示例:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
const table = useTable({
|
|
||||||
data: async (search, paging) {
|
|
||||||
const res = await api.xx({ ...search, ...paging });
|
|
||||||
return {
|
|
||||||
data: res.data,
|
|
||||||
total: res.total
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### 表格列
|
|
||||||
|
|
||||||
`columns`定义表格列,并在原本基础上增加默认值并扩展部分属性。增加和扩展的属性如下:
|
|
||||||
|
|
||||||
| 参数 | 说明 | 类型 |
|
|
||||||
| :------ | :--------------------------------------------------------------------------------------------------- | :------- | -------- |
|
|
||||||
| type | 特殊类型, 目前支持`index`(表示行数)、`button`(行操作按钮) | 'index' | 'button' |
|
|
||||||
| buttons | 当`type`为`button`时的按钮数组,如果子项是对象则为`Button`组件的参数,如果为函数则为自定义渲染函数。 | Button[] |
|
|
||||||
|
|
||||||
### 表格分页
|
|
||||||
|
|
||||||
`pagination`定义分页行为,具体参数可参考 [Pagination](https://arco.design/vue/component/pagination) 文档。当`data`为数组时,将作为数据源进行分页;当`data`为函数且返回值为对象时,则根据`total`值进行分页。
|
|
||||||
|
|
||||||
### 搜索表单
|
|
||||||
|
|
||||||
参阅
|
|
||||||
|
|
||||||
### 公共参数
|
|
||||||
|
|
||||||
参数为`FormModal`的参数,主要作为新增和修改的公共参数。在大多数情况,新增和修改的配置大多是相似的,没必要写两份,把相同的参数写在这里即可,不同的参数在`create`和`modify`中单独配置。
|
|
||||||
|
|
||||||
注意,这里的`items`也可以被搜索表单复用,搜索表单可通过`extends: <field>`继承`common.items`中对应的字段配置。使用示例如下:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
const table = useTable({
|
|
||||||
common: {
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
field: "username",
|
|
||||||
label: "用户名称",
|
|
||||||
type: "input",
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
search: {
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
extend: "usernam",
|
|
||||||
required: false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### 新增弹窗
|
|
||||||
|
|
||||||
`create`为新增表单弹窗的参数,即`useFormModal`对应的参数。参阅。当指定该参数时,会在表格左上添加新建按钮,如需自定义按钮样式或自定义渲染,可通过`create.trigger`参数配置。
|
|
||||||
|
|
||||||
### 修改弹窗
|
|
||||||
|
|
||||||
`modify`为新增表单弹窗的参数,即`useFormModal`对应的参数。参阅。当指定该参数时,会在表格行添加修改按钮。
|
|
||||||
|
|
||||||
### 表格参数
|
|
||||||
|
|
||||||
`tableProps`为传递给`Table`组件的额外参数,其中部分参数不可用,如`data`和`columns`等。此外,部分参数有默认值,具体参数可查看`src/components/table/table.config.ts`文件。
|
|
||||||
|
|
||||||
### 插槽
|
|
||||||
|
|
||||||
- `Table`组件的插槽可正常使用
|
|
||||||
- `action`插槽用作表格左上方的操作区。
|
|
||||||
|
|
||||||
## 问题
|
|
||||||
|
|
||||||
- 问题:日期范围框值为数组,处理不方便
|
|
||||||
- 解决:字段名使用`v1:v2`格式,提交时会生成`{ v1: '00:00:01', v2: '00:00:02' }`数据
|
|
||||||
- 问题:搜索表单、新增表单和修改表单通常用到同一表单项,如何避免重复定义
|
|
||||||
- 解决:表单项使用`{ extends: <field-name> }`会在`common.items`中寻找相同的项,并合并值。
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
import { dayjs } from "@/libs/dayjs";
|
|
||||||
import { TableColumn } from "./use-interface";
|
|
||||||
|
|
||||||
const defineColumn = <T extends TableColumn>(column: T) => {
|
|
||||||
return column;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const updateColumn = defineColumn({
|
|
||||||
title: "更新用户",
|
|
||||||
dataIndex: "createdAt",
|
|
||||||
width: 190,
|
|
||||||
render({ record }) {
|
|
||||||
return (
|
|
||||||
<div class="flex flex-col overflow-hidden">
|
|
||||||
<span>{record.updatedBy ?? "无"}</span>
|
|
||||||
<span class="text-gray-400 text-xs truncate">
|
|
||||||
{dayjs(record.updatedAt).format()}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const createColumn = defineColumn({
|
|
||||||
title: "创建用户",
|
|
||||||
dataIndex: "createdAt",
|
|
||||||
width: 190,
|
|
||||||
render({ record }) {
|
|
||||||
return (
|
|
||||||
<div class="flex flex-col overflow-hidden">
|
|
||||||
<span>{record.createdBy ?? "无"}</span>
|
|
||||||
<span class="text-gray-400 text-xs truncate">
|
|
||||||
{dayjs(record.createdAt).format()}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
export * from "./colume";
|
|
||||||
export * from "./table";
|
|
||||||
export * from "./use-table";
|
|
||||||
|
|
||||||
|
|
@ -1,78 +0,0 @@
|
||||||
import { Button } from "@arco-design/web-vue";
|
|
||||||
import { IconRefresh, IconSearch } from "@arco-design/web-vue/es/icon";
|
|
||||||
|
|
||||||
export const config = {
|
|
||||||
searchInlineCount: 3,
|
|
||||||
searchFormProps: {
|
|
||||||
labelAlign: "left",
|
|
||||||
autoLabelWidth: true,
|
|
||||||
model: {},
|
|
||||||
},
|
|
||||||
searchItemSubmit: {
|
|
||||||
field: "id",
|
|
||||||
type: "custom",
|
|
||||||
label: ' ',
|
|
||||||
itemProps: {
|
|
||||||
class: "table-search-item col-start-4 !mr-0 grid grid-cols-[0_1fr]",
|
|
||||||
// hideLabel: true,
|
|
||||||
},
|
|
||||||
component: () => {
|
|
||||||
const tableRef = inject<any>("ref:table");
|
|
||||||
return (
|
|
||||||
<div class="w-full flex gap-x-2 justify-end">
|
|
||||||
{(tableRef.search?.items?.length || 0) > config.searchInlineCount && (
|
|
||||||
<Button disabled={tableRef?.loading.value} onClick={() => tableRef?.reloadData()}>
|
|
||||||
{{ icon: () => <IconRefresh></IconRefresh>, default: () => "重置" }}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
<Button type="primary" loading={tableRef?.loading.value} onClick={() => tableRef?.reloadData()}>
|
|
||||||
{{ icon: () => <IconSearch></IconSearch>, default: () => "查询" }}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
pagination: {
|
|
||||||
current: 1,
|
|
||||||
pageSize: 10,
|
|
||||||
total: 300,
|
|
||||||
showTotal: true,
|
|
||||||
},
|
|
||||||
columnBase: {
|
|
||||||
ellipsis: true,
|
|
||||||
tooltip: true,
|
|
||||||
render: ({ record, column }: any) => record[column.dataIndex] || "-",
|
|
||||||
},
|
|
||||||
columnIndex: {
|
|
||||||
title: "序号",
|
|
||||||
width: 60,
|
|
||||||
align: "center",
|
|
||||||
render: ({ rowIndex }: any) => {
|
|
||||||
const table = inject<any>("ref:table");
|
|
||||||
const page = table.pagination.current;
|
|
||||||
const size = table.pagination.pageSize;
|
|
||||||
return size * (page - 1) + rowIndex + 1;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
columnButtonBase: {
|
|
||||||
buttonProps: {
|
|
||||||
// type: "text",
|
|
||||||
// size: "mini",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
columnButtonDelete: {
|
|
||||||
title: "删除确认",
|
|
||||||
content: "确认删除当前数据吗?",
|
|
||||||
modalClass: "text-center",
|
|
||||||
hideCancel: false,
|
|
||||||
maskClosable: false,
|
|
||||||
},
|
|
||||||
columnDropdownModify: {
|
|
||||||
text: "修改",
|
|
||||||
icon: "icon-park-outline-edit",
|
|
||||||
},
|
|
||||||
getApiErrorMessage(error: any): string {
|
|
||||||
const message = error?.response?.data?.message || error?.message || "请求失败";
|
|
||||||
return '';
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
@ -1,213 +0,0 @@
|
||||||
import AniEmpty from "@/components/AnEmpty/AnEmpty.vue";
|
|
||||||
import { TableColumnData as BaseColumn, TableData as BaseData, Table as BaseTable } from "@arco-design/web-vue";
|
|
||||||
import { merge } from "lodash-es";
|
|
||||||
import { PropType, computed, defineComponent, reactive, ref } from "vue";
|
|
||||||
import { Form, FormInstance, FormModal, FormModalInstance, FormModalProps, FormProps } from "../form";
|
|
||||||
import { config } from "./table.config";
|
|
||||||
|
|
||||||
type DataFn = (search: Record<string, any>, paging: { page: number; size: number }) => Promise<any>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 表格组件
|
|
||||||
* @see src/components/table/table.tsx
|
|
||||||
*/
|
|
||||||
export const Table = defineComponent({
|
|
||||||
name: "Table",
|
|
||||||
props: {
|
|
||||||
/**
|
|
||||||
* 表格数据
|
|
||||||
* @description 可以是数组或者函数,函数返回值为`{ data: BaseData[], total: number }`类型
|
|
||||||
*/
|
|
||||||
data: {
|
|
||||||
type: [Array, Function] as PropType<BaseData[] | DataFn>,
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* 表格列设置
|
|
||||||
*/
|
|
||||||
columns: {
|
|
||||||
type: Array as PropType<BaseColumn[]>,
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* 分页参数配置
|
|
||||||
*/
|
|
||||||
pagination: {
|
|
||||||
type: Object as PropType<any>,
|
|
||||||
default: () => reactive(config.pagination),
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* 搜索表单配置
|
|
||||||
*/
|
|
||||||
search: {
|
|
||||||
type: Object as PropType<FormProps>,
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* 新建弹窗配置
|
|
||||||
*/
|
|
||||||
create: {
|
|
||||||
type: Object as PropType<FormModalProps>,
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* 修改弹窗配置
|
|
||||||
*/
|
|
||||||
modify: {
|
|
||||||
type: Object as PropType<FormModalProps>,
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* 详情弹窗配置
|
|
||||||
*/
|
|
||||||
detail: {
|
|
||||||
type: Object as PropType<any>,
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* 传递给 Table 组件的属性
|
|
||||||
*/
|
|
||||||
tableProps: {
|
|
||||||
type: Object as PropType<InstanceType<typeof BaseTable>["$props"]>,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
setup(props) {
|
|
||||||
const loading = ref(false);
|
|
||||||
const tableRef = ref<InstanceType<typeof BaseTable>>();
|
|
||||||
const searchRef = ref<FormInstance>();
|
|
||||||
const createRef = ref<FormModalInstance>();
|
|
||||||
const modifyRef = ref<FormModalInstance>();
|
|
||||||
const renderData = ref<BaseData[]>([]);
|
|
||||||
const inlined = computed(() => (props.search?.items?.length ?? 0) <= config.searchInlineCount);
|
|
||||||
const reloadData = () => loadData({ current: 1, pageSize: 10 });
|
|
||||||
const openModifyModal = (data: any) => modifyRef.value?.open(data);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 加载数据
|
|
||||||
* @param pagination 自定义分页
|
|
||||||
*/
|
|
||||||
const loadData = async (pagination: Partial<any> = {}) => {
|
|
||||||
const merged = { ...props.pagination, ...pagination };
|
|
||||||
const paging = { page: merged.current, size: merged.pageSize };
|
|
||||||
const model = searchRef.value?.getModel() ?? {};
|
|
||||||
|
|
||||||
// 本地加载
|
|
||||||
if (Array.isArray(props.data)) {
|
|
||||||
const filters = Object.entries(model);
|
|
||||||
const data = props.data.filter((item) => {
|
|
||||||
return filters.every(([key, value]) => {
|
|
||||||
if (typeof value === "string") {
|
|
||||||
return item[key].includes(value);
|
|
||||||
}
|
|
||||||
return item[key] === value;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
renderData.value = data;
|
|
||||||
props.pagination.total = renderData.value.length;
|
|
||||||
props.pagination.current = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 远程加载
|
|
||||||
if (typeof props.data === "function") {
|
|
||||||
try {
|
|
||||||
loading.value = true;
|
|
||||||
const resData = await props.data(model, paging);
|
|
||||||
const { data = [], total = 0 } = resData?.data || {};
|
|
||||||
renderData.value = data;
|
|
||||||
props.pagination.total = total;
|
|
||||||
props.pagination.current = paging.page;
|
|
||||||
} catch (e) {
|
|
||||||
// todo
|
|
||||||
} finally {
|
|
||||||
loading.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
watchEffect(() => {
|
|
||||||
if (Array.isArray(props.data)) {
|
|
||||||
renderData.value = props.data;
|
|
||||||
props.pagination.total = props.data.length;
|
|
||||||
props.pagination.current = 1;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
loadData();
|
|
||||||
});
|
|
||||||
|
|
||||||
if (props.search) {
|
|
||||||
merge(props.search, { formProps: { layout: "inline" } });
|
|
||||||
}
|
|
||||||
|
|
||||||
const state = {
|
|
||||||
loading,
|
|
||||||
inlined,
|
|
||||||
tableRef,
|
|
||||||
searchRef,
|
|
||||||
createRef,
|
|
||||||
modifyRef,
|
|
||||||
renderData,
|
|
||||||
loadData,
|
|
||||||
reloadData,
|
|
||||||
openModifyModal,
|
|
||||||
};
|
|
||||||
|
|
||||||
provide("ref:table", { ...state, ...props });
|
|
||||||
|
|
||||||
return state;
|
|
||||||
},
|
|
||||||
render() {
|
|
||||||
(this.columns as any).instance = this;
|
|
||||||
return (
|
|
||||||
<div class="table w-full">
|
|
||||||
{!this.inlined && (
|
|
||||||
<div class="border-b pb-0 border-slate-200 mb-3">
|
|
||||||
<Form ref="searchRef" class="!grid grid-cols-4 gap-x-6" {...this.search}></Form>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div class={`mb-3 flex toolbar justify-between ${!this.inlined && "mt-2"}`}>
|
|
||||||
<div class={`${this.create || this.$slots.action ? null : "!hidden"} flex-1 flex gap-2 `}>
|
|
||||||
{this.create && (
|
|
||||||
<FormModal {...(this.create as any)} ref="createRef" onSubmited={this.reloadData}></FormModal>
|
|
||||||
)}
|
|
||||||
{this.modify && (
|
|
||||||
<FormModal
|
|
||||||
{...(this.modify as any)}
|
|
||||||
ref="modifyRef"
|
|
||||||
onSubmited={this.reloadData}
|
|
||||||
trigger={false}
|
|
||||||
></FormModal>
|
|
||||||
)}
|
|
||||||
{this.$slots.action?.()}
|
|
||||||
</div>
|
|
||||||
<div>{this.inlined && <Form ref="searchRef" {...this.search}></Form>}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<BaseTable
|
|
||||||
ref="tableRef"
|
|
||||||
row-key="id"
|
|
||||||
bordered={false}
|
|
||||||
{...this.$attrs}
|
|
||||||
{...this.tableProps}
|
|
||||||
loading={this.loading}
|
|
||||||
pagination={this.pagination}
|
|
||||||
data={this.renderData}
|
|
||||||
columns={this.columns}
|
|
||||||
onPageChange={(current: number) => this.loadData({ current })}
|
|
||||||
>
|
|
||||||
{{
|
|
||||||
empty: () => <AniEmpty />,
|
|
||||||
...this.$slots,
|
|
||||||
}}
|
|
||||||
</BaseTable>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 表格组件实例
|
|
||||||
*/
|
|
||||||
export type TableInstance = InstanceType<typeof Table>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 表格组件参数
|
|
||||||
*/
|
|
||||||
export type TableProps = TableInstance["$props"];
|
|
||||||
|
|
@ -1,164 +0,0 @@
|
||||||
import { Doption, Link, TableColumnData, TableData } from "@arco-design/web-vue";
|
|
||||||
import { RenderFunction } from "vue";
|
|
||||||
import { FormModalProps, FormProps } from "../form";
|
|
||||||
import { IFormItem } from "../form/form-item";
|
|
||||||
import { TableProps } from "./table";
|
|
||||||
|
|
||||||
interface UseColumnRenderOptions {
|
|
||||||
/**
|
|
||||||
* 当前行数据
|
|
||||||
*/
|
|
||||||
record: TableData;
|
|
||||||
/**
|
|
||||||
* 当前列配置
|
|
||||||
*/
|
|
||||||
column: TableColumnData;
|
|
||||||
/**
|
|
||||||
* 当前行索引
|
|
||||||
*/
|
|
||||||
rowIndex: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ColumnRender = (options: UseColumnRenderOptions) => any;
|
|
||||||
|
|
||||||
export interface TableColumnButton {
|
|
||||||
/**
|
|
||||||
* 按钮文本
|
|
||||||
*/
|
|
||||||
text?: string;
|
|
||||||
/**
|
|
||||||
* 操作类型
|
|
||||||
* @description `delete` 需配置`onClick`属性,`modify` 需配置根对象下的 `modify` 属性
|
|
||||||
*/
|
|
||||||
type?: "delete" | "modify";
|
|
||||||
/**
|
|
||||||
* 处理函数
|
|
||||||
*/
|
|
||||||
onClick?: (data: UseColumnRenderOptions) => any;
|
|
||||||
/**
|
|
||||||
* 是否禁用按钮
|
|
||||||
*/
|
|
||||||
disabled?: (data: UseColumnRenderOptions) => boolean;
|
|
||||||
/**
|
|
||||||
* 是否显示按钮
|
|
||||||
*/
|
|
||||||
visible?: (data: UseColumnRenderOptions) => boolean;
|
|
||||||
/**
|
|
||||||
* 传递给按钮的props
|
|
||||||
*/
|
|
||||||
buttonProps?: Partial<Omit<InstanceType<typeof Link>["$props"], "onClick" | "disabled">>;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TableColumnDropdown {
|
|
||||||
/**
|
|
||||||
* 特殊类型
|
|
||||||
*/
|
|
||||||
type?: "modify" | "delete";
|
|
||||||
/**
|
|
||||||
* 下拉菜单文本
|
|
||||||
*/
|
|
||||||
text?: string;
|
|
||||||
/**
|
|
||||||
* 下拉菜单图标
|
|
||||||
*/
|
|
||||||
icon?: string | RenderFunction;
|
|
||||||
/**
|
|
||||||
* 是否禁用
|
|
||||||
*/
|
|
||||||
disabled?: (data: UseColumnRenderOptions) => boolean;
|
|
||||||
/**
|
|
||||||
* 是否显示
|
|
||||||
*/
|
|
||||||
visibled?: (data: UseColumnRenderOptions) => boolean;
|
|
||||||
/**
|
|
||||||
* 处理事件
|
|
||||||
*/
|
|
||||||
onClick?: (data: UseColumnRenderOptions) => any;
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
doptionProps?: Partial<InstanceType<typeof Doption> & Record<string, any>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TableColumn extends TableColumnData {
|
|
||||||
/**
|
|
||||||
* 表格列类型
|
|
||||||
*/
|
|
||||||
type?: "index" | "button" | "dropdown";
|
|
||||||
/**
|
|
||||||
* 按钮配置列表
|
|
||||||
*/
|
|
||||||
buttons?: TableColumnButton[];
|
|
||||||
/**
|
|
||||||
* 下拉菜单配置列表
|
|
||||||
*/
|
|
||||||
dropdowns?: TableColumnDropdown[];
|
|
||||||
}
|
|
||||||
|
|
||||||
type ExtendedFormItem = Partial<IFormItem> & {
|
|
||||||
/**
|
|
||||||
* 继承 `create.items` 中指定 `field` 值的项
|
|
||||||
*/
|
|
||||||
extend?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type SearchFormItem = ExtendedFormItem & {
|
|
||||||
enableLoad?: boolean;
|
|
||||||
searchable?: boolean;
|
|
||||||
enterable?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
type Search = Partial<
|
|
||||||
Omit<FormProps, "items"> & {
|
|
||||||
/**
|
|
||||||
* 表单项
|
|
||||||
*/
|
|
||||||
items?: SearchFormItem[];
|
|
||||||
/**
|
|
||||||
* 显示/隐藏搜索按钮
|
|
||||||
*/
|
|
||||||
button?: boolean;
|
|
||||||
}
|
|
||||||
>;
|
|
||||||
|
|
||||||
type Modify = Partial<
|
|
||||||
Omit<FormModalProps, "items"> & {
|
|
||||||
/**
|
|
||||||
* 是否继承 `create` 弹窗配置
|
|
||||||
*/
|
|
||||||
extend: boolean;
|
|
||||||
/**
|
|
||||||
* 表单项
|
|
||||||
*/
|
|
||||||
items?: ExtendedFormItem[];
|
|
||||||
}
|
|
||||||
>;
|
|
||||||
|
|
||||||
export interface UseTableOptions extends Omit<TableProps, "search" | "create" | "modify" | "columns"> {
|
|
||||||
/**
|
|
||||||
* 表格列配置
|
|
||||||
* @see https://arco.design/web-vue/components/table/#tablecolumn
|
|
||||||
*/
|
|
||||||
columns: TableColumn[];
|
|
||||||
/**
|
|
||||||
* 搜索表单配置
|
|
||||||
* @see FormProps
|
|
||||||
*/
|
|
||||||
search?: Search;
|
|
||||||
/**
|
|
||||||
* 新建弹窗配置
|
|
||||||
*/
|
|
||||||
create?: Partial<FormModalProps>;
|
|
||||||
/**
|
|
||||||
* 新建弹窗配置
|
|
||||||
*/
|
|
||||||
modify?: Modify;
|
|
||||||
/**
|
|
||||||
* 详情弹窗配置
|
|
||||||
*/
|
|
||||||
detail?: any;
|
|
||||||
/**
|
|
||||||
* 批量删除配置
|
|
||||||
*/
|
|
||||||
delete?: any;
|
|
||||||
}
|
|
||||||
|
|
@ -1,287 +0,0 @@
|
||||||
import { delConfirm } from "@/utils";
|
|
||||||
import { Divider, Doption, Dropdown, Link, Message, TableColumnData } from "@arco-design/web-vue";
|
|
||||||
import { isArray, merge } from "lodash-es";
|
|
||||||
import { Component, Ref, reactive } from "vue";
|
|
||||||
import { useFormModal } from "../form";
|
|
||||||
import { Table, TableInstance, TableProps } from "./table";
|
|
||||||
import { config } from "./table.config";
|
|
||||||
import { UseTableOptions } from "./use-interface";
|
|
||||||
|
|
||||||
const onClick = async (item: any, columnData: any, getTable: any) => {
|
|
||||||
if (item.type === "modify") {
|
|
||||||
const data = (await item.onClick?.(columnData)) ?? columnData.record;
|
|
||||||
getTable()?.openModifyModal(data);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (item.type === "delete") {
|
|
||||||
await delConfirm();
|
|
||||||
try {
|
|
||||||
const resData: any = await item?.onClick?.(columnData);
|
|
||||||
const message = resData?.data?.message;
|
|
||||||
message && Message.success(`提示:${message}`);
|
|
||||||
getTable()?.loadData();
|
|
||||||
} catch (error: any) {
|
|
||||||
const message = error.response?.data?.message;
|
|
||||||
message && Message.warning(`提示:${message}`);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
item.onClick?.(columnData);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 表格组件hook
|
|
||||||
* @see `src/components/table/use-table.tsx`
|
|
||||||
*/
|
|
||||||
export const useTable = (optionsOrFn: UseTableOptions | (() => UseTableOptions)): any => {
|
|
||||||
const options: UseTableOptions = typeof optionsOrFn === "function" ? optionsOrFn() : optionsOrFn;
|
|
||||||
const columns: TableColumnData[] = [];
|
|
||||||
const getTable = (): TableInstance => (columns as any).instance;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 表格列处理
|
|
||||||
*/
|
|
||||||
for (let column of options.columns) {
|
|
||||||
/**
|
|
||||||
* 索引列处理
|
|
||||||
*/
|
|
||||||
if (column.type === "index") {
|
|
||||||
column = merge({}, config.columnIndex, column);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 按钮列处理
|
|
||||||
*/
|
|
||||||
if (column.type === "button" && isArray(column.buttons)) {
|
|
||||||
const buttons = column.buttons;
|
|
||||||
let hasModify = false;
|
|
||||||
let hasDelete = false;
|
|
||||||
for (let i = 0; i < buttons.length; i++) {
|
|
||||||
let btn = merge({}, config.columnButtonBase);
|
|
||||||
if (buttons[i].type === "modify") {
|
|
||||||
btn = merge(btn, buttons[i]);
|
|
||||||
hasModify = true;
|
|
||||||
}
|
|
||||||
if (buttons[i].type === "delete") {
|
|
||||||
btn = merge(btn, buttons[i]);
|
|
||||||
hasDelete = true;
|
|
||||||
}
|
|
||||||
buttons[i] = merge(btn, buttons[i]);
|
|
||||||
}
|
|
||||||
if (!hasModify) {
|
|
||||||
buttons.push(merge({}, config.columnButtonBase));
|
|
||||||
}
|
|
||||||
if (!hasDelete) {
|
|
||||||
buttons.push(merge({}, config.columnButtonBase));
|
|
||||||
}
|
|
||||||
column.render = (columnData) => {
|
|
||||||
return column.buttons?.map((btn, index) => {
|
|
||||||
if (btn.visible?.(columnData) === false) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{index !== 0 ? <Divider direction="vertical" margin={2} class="!border-gray-300"></Divider> : null}
|
|
||||||
<Link
|
|
||||||
{...btn.buttonProps}
|
|
||||||
onClick={() => onClick(btn, columnData, getTable)}
|
|
||||||
disabled={btn.disabled?.(columnData)}
|
|
||||||
>
|
|
||||||
{btn.text}
|
|
||||||
</Link>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 菜单列处理
|
|
||||||
*/
|
|
||||||
if (column.type === "dropdown" && Array.isArray(column.dropdowns)) {
|
|
||||||
if (options.modify) {
|
|
||||||
const index = column.dropdowns?.findIndex((i) => i.type === "modify");
|
|
||||||
if (index !== undefined) {
|
|
||||||
column.dropdowns[index] = merge({}, config.columnDropdownModify, column.dropdowns[index]);
|
|
||||||
} else {
|
|
||||||
column.dropdowns?.unshift(merge({}, config.columnDropdownModify));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
column.render = (columnData) => {
|
|
||||||
const content = column.dropdowns?.map((dropdown) => {
|
|
||||||
const { text, icon, disabled, visibled, doptionProps } = dropdown;
|
|
||||||
if (visibled?.(columnData) === false) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<Doption
|
|
||||||
{...doptionProps}
|
|
||||||
onClick={() => onClick(dropdown, columnData, getTable)}
|
|
||||||
disabled={disabled?.(columnData)}
|
|
||||||
>
|
|
||||||
{{
|
|
||||||
icon: typeof icon === "function" ? icon() : () => <i class={icon} />,
|
|
||||||
default: text,
|
|
||||||
}}
|
|
||||||
</Doption>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
const trigger = () => (
|
|
||||||
<span class="px-2 py-[1px] h-6 vertical-b rounded cursor-pointer text-[rgb(var(--link-6))] hover:bg-[var(--color-fill-2)]">
|
|
||||||
<i class="icon-park-outline-more"></i>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Link>编辑</Link>
|
|
||||||
<Link>详情</Link>
|
|
||||||
<Dropdown position="br">
|
|
||||||
{{
|
|
||||||
default: trigger,
|
|
||||||
content: content,
|
|
||||||
}}
|
|
||||||
</Dropdown>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
columns.push({ ...config.columnBase, ...column });
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 新增表单处理
|
|
||||||
*/
|
|
||||||
if (options.create) {
|
|
||||||
options.create = useFormModal(options.create as any) as any;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 搜索表单的处理
|
|
||||||
*/
|
|
||||||
if (options.search && options.search.items) {
|
|
||||||
const searchItems: any[] = [];
|
|
||||||
const createItems = options.create?.items ?? [];
|
|
||||||
for (const item of options.search.items) {
|
|
||||||
if (item.extend) {
|
|
||||||
const createItem = createItems.find((i) => i.field === item.extend);
|
|
||||||
if (createItem) {
|
|
||||||
searchItems.push(merge({}, createItem, item));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const onSearch = item.searchable ? () => getTable().reloadData() : undefined;
|
|
||||||
const onPressEnter = item.enterable ? () => getTable().reloadData() : undefined;
|
|
||||||
searchItems.push(merge({ nodeProps: { onSearch, onPressEnter } }, item));
|
|
||||||
}
|
|
||||||
if (options.search.button !== false) {
|
|
||||||
searchItems.push(config.searchItemSubmit);
|
|
||||||
}
|
|
||||||
options.search.items = searchItems;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 修改表单处理
|
|
||||||
*/
|
|
||||||
if (options.modify) {
|
|
||||||
if (options.modify.extend && options.create) {
|
|
||||||
const createItems = options.create.items;
|
|
||||||
const modifyItems = options.modify.items;
|
|
||||||
if (modifyItems && createItems) {
|
|
||||||
for (let i = 0; i < modifyItems.length; i++) {
|
|
||||||
if (modifyItems[i].extend) {
|
|
||||||
modifyItems[i] = merge({}, createItems[i], modifyItems[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const merged = merge(
|
|
||||||
{ modalProps: { titleAlign: "start", closable: false }, model: { id: undefined } },
|
|
||||||
options.create,
|
|
||||||
options.modify
|
|
||||||
);
|
|
||||||
options.modify = useFormModal(merged as any) as any;
|
|
||||||
} else {
|
|
||||||
options.modify = useFormModal(options.modify as any) as any;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return reactive({ ...options, columns });
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 提供操作的上下文
|
|
||||||
*/
|
|
||||||
interface TableContext {
|
|
||||||
/**
|
|
||||||
* 传递给表格的参数(响应式)
|
|
||||||
*/
|
|
||||||
props: TableProps;
|
|
||||||
/**
|
|
||||||
* 表格实例
|
|
||||||
*/
|
|
||||||
tableRef: Ref<TableInstance | null>;
|
|
||||||
/**
|
|
||||||
* 刷新表格
|
|
||||||
*/
|
|
||||||
refresh: () => void;
|
|
||||||
/**
|
|
||||||
* 重置表格
|
|
||||||
*/
|
|
||||||
reload?: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
type TableReturnType = [
|
|
||||||
/**
|
|
||||||
* 绑定好参数的组件
|
|
||||||
*/
|
|
||||||
Component,
|
|
||||||
/**
|
|
||||||
* 提供操作的上下文
|
|
||||||
*/
|
|
||||||
TableContext
|
|
||||||
];
|
|
||||||
|
|
||||||
export const useAniTable = (options: UseTableOptions): TableReturnType => {
|
|
||||||
const props = useTable(options);
|
|
||||||
const tableRef = ref<TableInstance | null>(null);
|
|
||||||
const context = {
|
|
||||||
props,
|
|
||||||
tableRef,
|
|
||||||
refresh: () => tableRef.value?.reloadData(),
|
|
||||||
getTableInstance() {
|
|
||||||
return tableRef.value?.tableRef;
|
|
||||||
},
|
|
||||||
getSearchInstance() {
|
|
||||||
return tableRef.value?.searchRef;
|
|
||||||
},
|
|
||||||
getCreateInstance() {
|
|
||||||
return tableRef.value?.createRef;
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* 获取创建表单组件实例
|
|
||||||
*/
|
|
||||||
getCreateFormInstance() {
|
|
||||||
return this.getCreateInstance()?.formRef;
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* 获取修改表单弹窗组件实例
|
|
||||||
*/
|
|
||||||
getModifyInstance() {
|
|
||||||
return tableRef.value?.modifyRef;
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* 获取修改表单组件实例
|
|
||||||
*/
|
|
||||||
getModifyFormInstance() {
|
|
||||||
return this.getModifyInstance()?.formRef;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const aniTable = defineComponent({
|
|
||||||
name: "AniTableWrapper",
|
|
||||||
setup(p, { slots }) {
|
|
||||||
const onRef = (el: TableInstance) => (tableRef.value = el);
|
|
||||||
return () => <Table ref={onRef} {...props}>{slots}</Table>;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return [aniTable, context];
|
|
||||||
};
|
|
||||||
|
|
@ -23,7 +23,7 @@
|
||||||
</div>
|
</div>
|
||||||
</a-doption>
|
</a-doption>
|
||||||
<a-divider :margin="4"></a-divider>
|
<a-divider :margin="4"></a-divider>
|
||||||
<a-doption @click="password.open()">
|
<a-doption @click="open()">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<i class="icon-park-outline-lock"></i>
|
<i class="icon-park-outline-lock"></i>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -47,51 +47,52 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useAniFormModal } from "@/components";
|
import { useFormModal } from '@/components/AnForm';
|
||||||
import { useUserStore } from "@/store";
|
import { useUserStore } from '@/store';
|
||||||
import { delConfirm } from "@/utils";
|
import { delConfirm } from '@/utils';
|
||||||
import { Message } from "@arco-design/web-vue";
|
import { Message } from '@arco-design/web-vue';
|
||||||
|
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const logout = async () => {
|
const logout = async () => {
|
||||||
await delConfirm('退出后将跳转到登录页面,确定退出吗?')
|
await delConfirm({
|
||||||
|
content: '退出后将跳转到登录页面,确定退出吗?',
|
||||||
|
okText: '确定退出',
|
||||||
|
});
|
||||||
userStore.clearUser();
|
userStore.clearUser();
|
||||||
Message.success("提示:已退出登陆!");
|
Message.success('提示:已退出登陆!');
|
||||||
router.push({ path: "/login", query: { redirect: route.path } });
|
router.push({ path: '/login', query: { redirect: route.path } });
|
||||||
};
|
};
|
||||||
|
|
||||||
const [PasswordModal, password] = useAniFormModal({
|
const { component: PasswordModal, open } = useFormModal({
|
||||||
title: "修改密码",
|
title: '修改密码',
|
||||||
trigger: false,
|
trigger: false,
|
||||||
modalProps: {
|
width: 452,
|
||||||
width: 452,
|
|
||||||
},
|
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
field: "password",
|
field: 'password',
|
||||||
label: "原密码",
|
label: '原密码',
|
||||||
type: "input",
|
setter: 'input',
|
||||||
nodeProps: {
|
setterProps: {
|
||||||
placeholder: "请输入原密码",
|
placeholder: '请输入原密码',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "password1",
|
field: 'password1',
|
||||||
label: "新密码",
|
label: '新密码',
|
||||||
type: "input",
|
setter: 'input',
|
||||||
nodeProps: {
|
setterProps: {
|
||||||
placeholder: "请输入新密码",
|
placeholder: '请输入新密码',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "password2",
|
field: 'password2',
|
||||||
label: "确认密码",
|
label: '确认密码',
|
||||||
type: "input",
|
setter: 'input',
|
||||||
nodeProps: {
|
setterProps: {
|
||||||
placeholder: "请再次输入新密码",
|
placeholder: '请再次输入新密码',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -6,14 +6,9 @@
|
||||||
<div class="h-13 flex items-center">
|
<div class="h-13 flex items-center">
|
||||||
<router-link to="/" class="px-2 py-1 rounded flex items-center gap-2 text-slate-700">
|
<router-link to="/" class="px-2 py-1 rounded flex items-center gap-2 text-slate-700">
|
||||||
<img src="/favicon.ico" alt="" width="22" height="22" class="" />
|
<img src="/favicon.ico" alt="" width="22" height="22" class="" />
|
||||||
<h1 class="relative text-lg leading-[19px] dark:text-white m-0 p-0">
|
<h1 class="relative text-lg leading-[20px] dark:text-white m-0 p-0 font-semibold">
|
||||||
{{ appStore.title }}
|
{{ appStore.title }}
|
||||||
<!-- <span
|
<span class="absolute -right-10 -top-1 font-normal text-xs text-gray-400"> v0.0.1 </span>
|
||||||
v-if="isDev"
|
|
||||||
class="absolute -right-14 -top-1 text-xs font-normal text-brand-500 bg-brand-50 px-1.5 rounded-full"
|
|
||||||
>
|
|
||||||
本地版
|
|
||||||
</span> -->
|
|
||||||
</h1>
|
</h1>
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -41,7 +36,7 @@
|
||||||
:collapsible="true"
|
:collapsible="true"
|
||||||
:collapsed="isCollapsed"
|
:collapsed="isCollapsed"
|
||||||
:hide-trigger="false"
|
:hide-trigger="false"
|
||||||
@collapse="(val) => (isCollapsed = val)"
|
@collapse="val => (isCollapsed = val)"
|
||||||
>
|
>
|
||||||
<a-scrollbar outer-class="h-full overflow-hidden" class="h-full overflow-hidden pt-1">
|
<a-scrollbar outer-class="h-full overflow-hidden" class="h-full overflow-hidden pt-1">
|
||||||
<Menu />
|
<Menu />
|
||||||
|
|
@ -72,14 +67,14 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { useAppStore } from "@/store";
|
import { useAppStore } from '@/store';
|
||||||
import { useMenuStore } from "@/store/menu";
|
import { useMenuStore } from '@/store/menu';
|
||||||
import { Message } from "@arco-design/web-vue";
|
import { Message } from '@arco-design/web-vue';
|
||||||
import { IconSync } from "@arco-design/web-vue/es/icon";
|
import { IconSync } from '@arco-design/web-vue/es/icon';
|
||||||
import Menu from "./components/menu.vue";
|
import Menu from './components/menu.vue';
|
||||||
import userDropdown from "./components/userDropdown.vue";
|
import userDropdown from './components/userDropdown.vue';
|
||||||
|
|
||||||
defineOptions({ name: "LayoutPage" });
|
defineOptions({ name: 'LayoutPage' });
|
||||||
|
|
||||||
const appStore = useAppStore();
|
const appStore = useAppStore();
|
||||||
const menuStore = useMenuStore();
|
const menuStore = useMenuStore();
|
||||||
|
|
@ -88,24 +83,24 @@ const themeConfig = ref({ visible: false });
|
||||||
|
|
||||||
const buttons = [
|
const buttons = [
|
||||||
{
|
{
|
||||||
icon: "icon-park-outline-remind",
|
icon: 'icon-park-outline-remind',
|
||||||
tooltip: "通知",
|
tooltip: '通知',
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
Message.info("暂无通知");
|
Message.info('暂无通知');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: "icon-park-outline-config",
|
icon: 'icon-park-outline-config',
|
||||||
tooltip: "设置",
|
tooltip: '设置',
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
themeConfig.value.visible = true;
|
themeConfig.value.visible = true;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: "icon-park-outline-github",
|
icon: 'icon-park-outline-github',
|
||||||
tooltip: "仓库",
|
tooltip: '仓库',
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
window.open("https://github.com/appnify/starter-vue", "_blank");
|
window.open('https://github.com/appnify/starter-vue', '_blank');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -105,8 +105,8 @@ const onSubmitForm = async () => {
|
||||||
const res = await api.auth.login(model);
|
const res = await api.auth.login(model);
|
||||||
userStore.setAccessToken(res.data.data);
|
userStore.setAccessToken(res.data.data);
|
||||||
Notification.success({
|
Notification.success({
|
||||||
title: "登陆提示",
|
title: "提示",
|
||||||
content: `欢迎,您已成功登陆系统!`,
|
content: `${meridiem}好,您已成功登陆本系统!`,
|
||||||
});
|
});
|
||||||
router.push({ path: (route.query.redirect as string) || "/" });
|
router.push({ path: (route.query.redirect as string) || "/" });
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
|
|
||||||
|
|
@ -1,77 +1,90 @@
|
||||||
<template>
|
<template>
|
||||||
<BreadPage>
|
<BreadPage>
|
||||||
<Table v-bind="table"></Table>
|
<CategoryTable />
|
||||||
</BreadPage>
|
</BreadPage>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="tsx">
|
<script setup lang="tsx">
|
||||||
import { api } from "@/api";
|
import { api } from '@/api';
|
||||||
import { Table, useTable } from "@/components";
|
import { useCreateColumn, useTable, useUpdateColumn } from '@/components/AnTable';
|
||||||
import { dayjs } from "@/libs/dayjs";
|
|
||||||
import { Tag } from "@arco-design/web-vue";
|
|
||||||
|
|
||||||
const table = useTable({
|
const { component: CategoryTable } = useTable({
|
||||||
data: async (model, paging) => {
|
|
||||||
return api.log.getLoginLogs({ ...model, ...paging });
|
|
||||||
},
|
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
title: "登陆账号",
|
title: '文章标题',
|
||||||
dataIndex: "nickname",
|
dataIndex: 'title',
|
||||||
width: 140,
|
render: ({ record }) => (
|
||||||
|
<div class="overflow-hidden">
|
||||||
|
<span>{record.title}</span>
|
||||||
|
<span class="text-gray-400 text-xs truncate">{record.description.substr(0, 80)}</span>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
|
useCreateColumn(),
|
||||||
|
useUpdateColumn(),
|
||||||
{
|
{
|
||||||
title: "操作描述",
|
type: 'button',
|
||||||
dataIndex: "description",
|
title: '操作',
|
||||||
render: ({ record: { status, description } }) => {
|
|
||||||
return (
|
|
||||||
<span>
|
|
||||||
<Tag color={status === null || status ? "green" : "red"} class="mr-2">
|
|
||||||
{ status === null || status ? "成功" : "失败" }
|
|
||||||
</Tag>
|
|
||||||
{description}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "登陆地址",
|
|
||||||
dataIndex: "ip",
|
|
||||||
width: 200,
|
|
||||||
render: ({ record }) => `${record.addr || "未知"}(${record.ip})`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "操作系统",
|
|
||||||
dataIndex: "os",
|
|
||||||
width: 160,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "浏览器",
|
|
||||||
dataIndex: "browser",
|
|
||||||
width: 160,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "登陆时间",
|
|
||||||
dataIndex: "createdAt",
|
|
||||||
width: 120,
|
width: 120,
|
||||||
render: ({ record }) => dayjs(record.createdAt).fromNow(),
|
buttons: [
|
||||||
|
{
|
||||||
|
type: 'modify',
|
||||||
|
text: '修改',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'delete',
|
||||||
|
text: '删除',
|
||||||
|
onClick: props => api.post.delPost(props.record.id),
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
search: {
|
source: async model => api.post.getPosts(model),
|
||||||
|
search: [
|
||||||
|
{
|
||||||
|
field: 'nickname',
|
||||||
|
label: '文章标题',
|
||||||
|
setter: 'search',
|
||||||
|
enterable: true,
|
||||||
|
searchable: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
create: {
|
||||||
|
title: '添加文章',
|
||||||
|
width: 1080,
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
field: "nickname",
|
field: 'title',
|
||||||
label: "登陆账号",
|
label: '标题',
|
||||||
type: "input",
|
setter: 'input',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'slug',
|
||||||
|
label: '别名',
|
||||||
|
setter: 'input',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'description',
|
||||||
|
label: '内容',
|
||||||
|
setter: 'textarea',
|
||||||
required: false,
|
required: false,
|
||||||
nodeProps: {
|
setterProps: {
|
||||||
placeholder: '请输入登陆账号',
|
maxLength: 2000,
|
||||||
},
|
},
|
||||||
itemProps: {
|
|
||||||
hideLabel: true,
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
submit: model => {
|
||||||
|
return api.post.addPost(model as any);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
modify: {
|
||||||
|
extend: true,
|
||||||
|
title: '修改文章',
|
||||||
|
submit: model => {
|
||||||
|
return api.post.updatePost(model.id, model);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,12 @@
|
||||||
<div class="w-[210px] h-full overflow-hidden grid grid-rows-[auto_1fr]">
|
<div class="w-[210px] h-full overflow-hidden grid grid-rows-[auto_1fr]">
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<a-input-search allow-clear placeholder="分类名称" class="mb-2" @search="updateFileCategories"></a-input-search>
|
<a-input-search allow-clear placeholder="分类名称" class="mb-2" @search="updateFileCategories"></a-input-search>
|
||||||
<a-button @click="formCtx.open">
|
<a-button @click="() => open()">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<i class="icon-park-outline-add"></i>
|
<i class="icon-park-outline-add"></i>
|
||||||
</template>
|
</template>
|
||||||
</a-button>
|
</a-button>
|
||||||
<form-modal></form-modal>
|
<CategoryModal></CategoryModal>
|
||||||
</div>
|
</div>
|
||||||
<a-scrollbar outer-class="h-full overflow-hidden" class="h-full overflow-auto">
|
<a-scrollbar outer-class="h-full overflow-hidden" class="h-full overflow-auto">
|
||||||
<a-spin :loading="loading" class="w-full h-full">
|
<a-spin :loading="loading" class="w-full h-full">
|
||||||
|
|
@ -30,7 +30,7 @@
|
||||||
</template>
|
</template>
|
||||||
</a-button>
|
</a-button>
|
||||||
<template #content>
|
<template #content>
|
||||||
<a-doption @click="formCtx.open(item)">
|
<a-doption @click="open(item)">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<i class="icon-park-outline-edit"></i>
|
<i class="icon-park-outline-edit"></i>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -55,7 +55,7 @@
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { FileCategory, api } from '@/api';
|
import { FileCategory, api } from '@/api';
|
||||||
import { useAniFormModal } from '@/components';
|
import { useFormModal } from '@/components/AnForm';
|
||||||
import { delConfirm } from '@/utils';
|
import { delConfirm } from '@/utils';
|
||||||
import { Message } from '@arco-design/web-vue';
|
import { Message } from '@arco-design/web-vue';
|
||||||
import { PropType } from 'vue';
|
import { PropType } from 'vue';
|
||||||
|
|
@ -92,38 +92,33 @@ const onDeleteRow = async (row: FileCategory) => {
|
||||||
Message.success(res.data.message);
|
Message.success(res.data.message);
|
||||||
};
|
};
|
||||||
|
|
||||||
const [formModal, formCtx] = useAniFormModal({
|
const { component: CategoryModal, open } = useFormModal({
|
||||||
title: ({ model }) => (!model.id ? '新建分类' : '修改分类'),
|
title: model => (!model.id ? '新建分类' : '修改分类'),
|
||||||
trigger: false,
|
trigger: false,
|
||||||
modalProps: {
|
width: 580,
|
||||||
width: 580,
|
|
||||||
},
|
|
||||||
model: {
|
|
||||||
id: undefined,
|
|
||||||
},
|
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
field: 'name',
|
field: 'name',
|
||||||
label: '分类名称',
|
label: '名称',
|
||||||
type: 'input',
|
setter: 'input',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'code',
|
field: 'code',
|
||||||
label: '分类编码',
|
label: '编码',
|
||||||
type: 'input',
|
setter: 'input',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'description',
|
field: 'description',
|
||||||
label: '备注',
|
label: '备注',
|
||||||
type: 'textarea',
|
setter: 'textarea',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
submit: async ({ model }) => {
|
submit: async model => {
|
||||||
let res;
|
let res;
|
||||||
if (model.id) {
|
if (model.id) {
|
||||||
res = await api.fileCategory.setFileCategory(model.id, model);
|
res = await api.fileCategory.setFileCategory(model.id, model);
|
||||||
} else {
|
} else {
|
||||||
res = await api.fileCategory.addFileCategory(model);
|
res = await api.fileCategory.addFileCategory(model as any);
|
||||||
}
|
}
|
||||||
updateFileCategories();
|
updateFileCategories();
|
||||||
return res;
|
return res;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<a-button type="primary" @click="visible = true">
|
<a-button type="primary" @click="visible = true">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<i class="icon-park-outline-upload"></i>
|
<i class="icon-park-outline-upload-one"></i>
|
||||||
</template>
|
</template>
|
||||||
上传
|
上传
|
||||||
</a-button>
|
</a-button>
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
<div class="bg-white p-4">
|
<div class="bg-white p-4">
|
||||||
<MaterialTable>
|
<MaterialTable>
|
||||||
<template #action>
|
<template #action>
|
||||||
<AnUpload></AnUpload>
|
<AnUpload @success="() => tableRef?.refresh()"></AnUpload>
|
||||||
</template>
|
</template>
|
||||||
</MaterialTable>
|
</MaterialTable>
|
||||||
<a-image-preview v-model:visible="visible" :src="image"></a-image-preview>
|
<a-image-preview v-model:visible="visible" :src="image"></a-image-preview>
|
||||||
|
|
@ -61,39 +61,45 @@ const {
|
||||||
{
|
{
|
||||||
title: '文件名称',
|
title: '文件名称',
|
||||||
dataIndex: 'name',
|
dataIndex: 'name',
|
||||||
render: ({ record }) => (
|
render: ({ record }) => {
|
||||||
<div class="group flex items-center gap-2">
|
return (
|
||||||
<div class="w-8 flex justify-center">
|
<div class="group flex items-center gap-2">
|
||||||
{record.mimetype.startsWith('image') ? (
|
<div class="w-8 flex justify-center">
|
||||||
<a-avatar size={26} shape="square">
|
{record.mimetype.startsWith('image') ? (
|
||||||
<img src={record.path}></img>
|
<a-avatar size={26} shape="square">
|
||||||
</a-avatar>
|
<img src={record.path}></img>
|
||||||
) : (
|
</a-avatar>
|
||||||
<i class={`${getIcon(record.mimetype)} text-4xl`}></i>
|
) : (
|
||||||
)}
|
<i class={`${getIcon(record.mimetype)} text-4xl`}></i>
|
||||||
</div>
|
)}
|
||||||
<div class="flex flex-col overflow-hidden">
|
</div>
|
||||||
<span class="flex items-center gap-2">
|
<div class="flex flex-col overflow-hidden">
|
||||||
<span
|
<span class="flex items-center gap-2">
|
||||||
class="truncate hover:text-brand-500 hover:decoration-underline underline-offset-2 cursor-pointer"
|
<span
|
||||||
onClick={() => preview(record)}
|
class="truncate hover:text-brand-500 hover:decoration-underline underline-offset-2 cursor-pointer"
|
||||||
>
|
onClick={() => preview(record)}
|
||||||
{record.name}
|
>
|
||||||
|
{record.name}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="inline-block w-5 text-xs text-gray-400 ml-0"
|
||||||
|
title="复制地址"
|
||||||
|
onClick={() => copyLink(record)}
|
||||||
|
>
|
||||||
|
<i class="hidden! group-hover:inline-block! icon-park-outline-copy hover:text-gray-700 cursor-pointer"></i>
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="inline-block w-5 text-xs text-gray-400 ml-0" title="复制地址" onClick={() => copyLink(record)}>
|
<div class="h-5 inline-flex items-center text-xs text-gray-400 space-x-2">
|
||||||
<i class="hidden! group-hover:inline-block! icon-park-outline-copy hover:text-gray-700 cursor-pointer"></i>
|
<span>
|
||||||
</span>
|
<i class="icon-park-outline-folder-close mr-1"></i>
|
||||||
</span>
|
{record.category?.name ?? '默认分类'}
|
||||||
<div class="h-5 inline-flex items-center text-xs text-gray-400 space-x-4">
|
</span>
|
||||||
<span>
|
<span>{numeral(record.size).format('0 b')}</span>
|
||||||
<i class="icon-park-outline-folder-close mr-1"></i>
|
</div>
|
||||||
{record.category || '默认分类'}
|
|
||||||
</span>
|
|
||||||
<span>{numeral(record.size).format('0 b')}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
),
|
},
|
||||||
},
|
},
|
||||||
useCreateColumn(),
|
useCreateColumn(),
|
||||||
useUpdateColumn(),
|
useUpdateColumn(),
|
||||||
|
|
@ -119,8 +125,8 @@ const {
|
||||||
return api.file.delFile(props.record.id);
|
return api.file.delFile(props.record.id);
|
||||||
},
|
},
|
||||||
buttonProps: {
|
buttonProps: {
|
||||||
status: 'danger'
|
status: 'danger',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
@ -147,17 +153,18 @@ const {
|
||||||
title: '修改素材',
|
title: '修改素材',
|
||||||
width: 580,
|
width: 580,
|
||||||
items: [
|
items: [
|
||||||
|
{
|
||||||
|
field: 'name',
|
||||||
|
label: '文件名',
|
||||||
|
setter: 'input',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
field: 'categoryId',
|
field: 'categoryId',
|
||||||
label: '分类',
|
label: '分类',
|
||||||
setter: 'select',
|
setter: 'select',
|
||||||
options: () => api.fileCategory.getFileCategorys({ size: 0 }) as any,
|
options: () => api.fileCategory.getFileCategorys({ size: 0 }) as any,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
field: 'name',
|
|
||||||
label: '名称',
|
|
||||||
setter: 'input',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
field: 'description',
|
field: 'description',
|
||||||
label: '描述',
|
label: '描述',
|
||||||
|
|
|
||||||
|
|
@ -7,25 +7,19 @@
|
||||||
<script setup lang="tsx">
|
<script setup lang="tsx">
|
||||||
import { api } from '@/api';
|
import { api } from '@/api';
|
||||||
import { useCreateColumn, useTable, useUpdateColumn } from '@/components/AnTable';
|
import { useCreateColumn, useTable, useUpdateColumn } from '@/components/AnTable';
|
||||||
import { listToTree } from '@/utils/listToTree';
|
|
||||||
|
|
||||||
const { component: CategoryTable } = useTable({
|
const { component: CategoryTable } = useTable({
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
title: '名称',
|
title: '文章标题',
|
||||||
dataIndex: 'title',
|
dataIndex: 'title',
|
||||||
width: 240,
|
|
||||||
render: ({ record }) => (
|
render: ({ record }) => (
|
||||||
<div class="flex flex-col overflow-hidden">
|
<div class="overflow-hidden">
|
||||||
<span>{record.title}</span>
|
<span>{record.title}</span>
|
||||||
<span class="text-gray-400 text-xs truncate">#{record.slug}</span>
|
<div class="text-gray-400 text-xs truncate">{record.description}</div>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: '描述',
|
|
||||||
dataIndex: 'description',
|
|
||||||
},
|
|
||||||
useCreateColumn(),
|
useCreateColumn(),
|
||||||
useUpdateColumn(),
|
useUpdateColumn(),
|
||||||
{
|
{
|
||||||
|
|
@ -40,59 +34,56 @@ const { component: CategoryTable } = useTable({
|
||||||
{
|
{
|
||||||
type: 'delete',
|
type: 'delete',
|
||||||
text: '删除',
|
text: '删除',
|
||||||
onClick({ record }) {
|
onClick: props => api.post.delPost(props.record.id),
|
||||||
return api.category.delCategory(record.id);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
source: async model => {
|
source: async model => api.post.getPosts(model),
|
||||||
const res = await api.category.getCategories(model);
|
|
||||||
const data = listToTree(res.data.data ?? []);
|
|
||||||
return { data: { data, total: (res.data as any).total } };
|
|
||||||
},
|
|
||||||
search: [
|
search: [
|
||||||
{
|
{
|
||||||
field: 'nickname',
|
field: 'nickname',
|
||||||
label: '登陆账号',
|
label: '文章标题',
|
||||||
setter: 'search',
|
setter: 'search',
|
||||||
enterable: true,
|
enterable: true,
|
||||||
searchable: true,
|
searchable: true,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
create: {
|
create: {
|
||||||
title: '添加分类',
|
title: '添加文章',
|
||||||
width: 580,
|
width: 1080,
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
field: 'title',
|
field: 'title',
|
||||||
label: '分类名称',
|
label: '标题',
|
||||||
setter: 'input',
|
setter: 'input',
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'slug',
|
field: 'slug',
|
||||||
label: '分类别名',
|
label: '别名',
|
||||||
setter: 'input',
|
setter: 'input',
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'description',
|
field: 'description',
|
||||||
label: '描述',
|
label: '内容',
|
||||||
setter: 'textarea',
|
setter: 'textarea',
|
||||||
required: false,
|
required: false,
|
||||||
|
setterProps: {
|
||||||
|
maxLength: 2000,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
submit: model => {
|
submit: model => {
|
||||||
return api.category.addCategory(model as any);
|
return api.post.addPost(model as any);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
modify: {
|
modify: {
|
||||||
extend: true,
|
extend: true,
|
||||||
title: '修改分类',
|
title: '修改文章',
|
||||||
submit: model => {
|
submit: model => {
|
||||||
return api.category.setCategory(model.id, model as any);
|
return api.post.updatePost(model.id, model);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -73,12 +73,7 @@ const { component: UserTable } = useTable({
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
source: search => {
|
source: search => api.user.getUsers(search),
|
||||||
return api.user.getUsers(search);
|
|
||||||
},
|
|
||||||
paging: {
|
|
||||||
hide: false,
|
|
||||||
},
|
|
||||||
search: [
|
search: [
|
||||||
{
|
{
|
||||||
field: 'username',
|
field: 'username',
|
||||||
|
|
@ -106,7 +101,7 @@ const { component: UserTable } = useTable({
|
||||||
setter: 'input',
|
setter: 'input',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
submit: async model => {
|
submit: model => {
|
||||||
return 1;
|
return 1;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,35 +1,32 @@
|
||||||
<template>
|
<template>
|
||||||
<BreadPage>
|
<BreadPage>
|
||||||
<Table v-bind="table"></Table>
|
<OperationTable></OperationTable>
|
||||||
</BreadPage>
|
</BreadPage>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="tsx">
|
<script setup lang="tsx">
|
||||||
import { api } from "@/api";
|
import { api } from '@/api';
|
||||||
import { Table, useTable } from "@/components";
|
import { useTable } from '@/components/AnTable';
|
||||||
import { dayjs } from "@/libs/dayjs";
|
import { dayjs } from '@/libs/dayjs';
|
||||||
import { Tag } from "@arco-design/web-vue";
|
import { Tag } from '@arco-design/web-vue';
|
||||||
|
|
||||||
defineOptions({ name: "SystemLogoPage" })
|
defineOptions({ name: 'SystemLogoPage' });
|
||||||
|
|
||||||
const table = useTable({
|
const { component: OperationTable } = useTable({
|
||||||
data: async (model, paging) => {
|
|
||||||
return api.log.getLoginLogs({ ...model, ...paging });
|
|
||||||
},
|
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
title: "登陆账号",
|
title: '登陆账号',
|
||||||
dataIndex: "nickname",
|
dataIndex: 'nickname',
|
||||||
width: 140,
|
width: 140,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "操作描述",
|
title: '操作描述',
|
||||||
dataIndex: "description",
|
dataIndex: 'description',
|
||||||
render: ({ record: { status, description } }) => {
|
render: ({ record: { status, description } }) => {
|
||||||
return (
|
return (
|
||||||
<span>
|
<span>
|
||||||
<Tag color={status === null || status ? "green" : "red"} class="mr-2">
|
<Tag color={status === null || status ? 'green' : 'red'} class="mr-2">
|
||||||
{ status === null || status ? "成功" : "失败" }
|
{status === null || status ? '成功' : '失败'}
|
||||||
</Tag>
|
</Tag>
|
||||||
{description}
|
{description}
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -37,44 +34,37 @@ const table = useTable({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "登陆地址",
|
title: '登陆地址',
|
||||||
dataIndex: "ip",
|
dataIndex: 'ip',
|
||||||
width: 200,
|
width: 200,
|
||||||
render: ({ record }) => `${record.addr || "未知"}(${record.ip})`,
|
render: ({ record }) => `${record.addr || '未知'}(${record.ip})`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "操作系统",
|
title: '操作系统',
|
||||||
dataIndex: "os",
|
dataIndex: 'os',
|
||||||
width: 160,
|
width: 160,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "浏览器",
|
title: '浏览器',
|
||||||
dataIndex: "browser",
|
dataIndex: 'browser',
|
||||||
width: 160,
|
width: 160,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "登陆时间",
|
title: '登陆时间',
|
||||||
dataIndex: "createdAt",
|
dataIndex: 'createdAt',
|
||||||
width: 120,
|
width: 120,
|
||||||
render: ({ record }) => dayjs(record.createdAt).fromNow(),
|
render: ({ record }) => dayjs(record.createdAt).fromNow(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
search: {
|
source: model => api.log.getLoginLogs(model),
|
||||||
items: [
|
search: [
|
||||||
{
|
{
|
||||||
field: "nickname",
|
field: 'nickname',
|
||||||
label: "登陆账号",
|
label: '登陆账号',
|
||||||
type: "input",
|
setter: 'input',
|
||||||
required: false,
|
required: false,
|
||||||
nodeProps: {
|
},
|
||||||
placeholder: '请输入登陆账号',
|
],
|
||||||
},
|
|
||||||
itemProps: {
|
|
||||||
hideLabel: true,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,41 +5,35 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="tsx">
|
<script setup lang="tsx">
|
||||||
import { api } from "@/api";
|
import { api } from '@/api';
|
||||||
import { createColumn, updateColumn, useAniTable } from "@/components";
|
import { useCreateColumn, useTable, useUpdateColumn } from '@/components/AnTable';
|
||||||
import { MenuType, MenuTypes } from "@/constants/menu";
|
import { MenuType, MenuTypes } from '@/constants/menu';
|
||||||
import { flatMenus } from "@/router";
|
import { flatMenus } from '@/router';
|
||||||
import { listToTree } from "@/utils/listToTree";
|
import { listToTree } from '@/utils/listToTree';
|
||||||
|
|
||||||
defineOptions({ name: 'SystemMenuPage' })
|
defineOptions({ name: 'SystemMenuPage' });
|
||||||
|
|
||||||
const menuArr = flatMenus.map((i) => ({ label: i.title, value: i.id }));
|
const menuArr = flatMenus.map(i => ({ label: i.title, value: i.id }));
|
||||||
const expanded = ref(false);
|
const expanded = ref(false);
|
||||||
const toggleExpand = () => {
|
const toggleExpand = () => {
|
||||||
expanded.value = !expanded.value;
|
expanded.value = !expanded.value;
|
||||||
menu.tableRef.value?.tableRef?.expandAll(expanded.value);
|
tableRef.value?.tableRef?.expandAll(expanded.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const [menuTable, menu] = useAniTable({
|
const { component: MenuTable, tableRef } = useTable({
|
||||||
data: (search, paging) => {
|
|
||||||
return api.menu.getMenus({ ...search, ...paging, tree: true, size: 0 });
|
|
||||||
},
|
|
||||||
tableProps: {
|
|
||||||
defaultExpandAllRows: true,
|
|
||||||
},
|
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
title: () => (
|
title: () => (
|
||||||
<span>
|
<span>
|
||||||
菜单名称
|
菜单名称
|
||||||
<a-link class="ml-1 select-none" onClick={toggleExpand}>
|
<a-link class="ml-1 select-none" onClick={toggleExpand}>
|
||||||
{expanded.value ? "收起全部" : "展开全部"}
|
{expanded.value ? '收起全部' : '展开全部'}
|
||||||
</a-link>
|
</a-link>
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
dataIndex: "name",
|
dataIndex: 'name',
|
||||||
render({ record }) {
|
render({ record }) {
|
||||||
let id = "";
|
let id = '';
|
||||||
if (record.type === MenuType.PAGE) {
|
if (record.type === MenuType.PAGE) {
|
||||||
id = ` => ${record.path}`;
|
id = ` => ${record.path}`;
|
||||||
}
|
}
|
||||||
|
|
@ -48,7 +42,7 @@ const [menuTable, menu] = useAniTable({
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
<a-tag bordered color={MenuTypes.fmt(record.type, "color")}>
|
<a-tag bordered color={MenuTypes.fmt(record.type, 'color')}>
|
||||||
{{
|
{{
|
||||||
default: () => MenuTypes.fmt(record.type),
|
default: () => MenuTypes.fmt(record.type),
|
||||||
}}
|
}}
|
||||||
|
|
@ -56,7 +50,7 @@ const [menuTable, menu] = useAniTable({
|
||||||
<div class="flex-1 flex overflow-hidden ml-1">
|
<div class="flex-1 flex overflow-hidden ml-1">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<i class={`${record.icon} mr-1`}></i>
|
<i class={`${record.icon} mr-1`}></i>
|
||||||
<span>{record.name ?? "无"}</span>
|
<span>{record.name ?? '无'}</span>
|
||||||
<span class="text-gray-400 text-xs truncate">{id}</span>
|
<span class="text-gray-400 text-xs truncate">{id}</span>
|
||||||
</div>
|
</div>
|
||||||
<a-switch checked-color="#3c9" size="small"></a-switch>
|
<a-switch checked-color="#3c9" size="small"></a-switch>
|
||||||
|
|
@ -65,27 +59,27 @@ const [menuTable, menu] = useAniTable({
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
createColumn,
|
useCreateColumn(),
|
||||||
updateColumn,
|
useUpdateColumn(),
|
||||||
{
|
{
|
||||||
title: "操作",
|
title: '操作',
|
||||||
type: "button",
|
type: 'button',
|
||||||
width: 200,
|
width: 200,
|
||||||
buttons: [
|
buttons: [
|
||||||
{
|
{
|
||||||
text: "新增子项",
|
text: '新增子项',
|
||||||
disabled: ({ record }) => record.type === MenuType.BUTTON,
|
disable: ({ record }) => record.type === MenuType.BUTTON,
|
||||||
onClick: ({ record }) => {
|
onClick: ({ record }) => {
|
||||||
console.log(record);
|
console.log(record);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "modify",
|
type: 'modify',
|
||||||
text: "修改",
|
text: '修改',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: "删除",
|
text: '删除',
|
||||||
type: "delete",
|
type: 'delete',
|
||||||
onClick: ({ record }) => {
|
onClick: ({ record }) => {
|
||||||
return api.menu.delMenu(record.id);
|
return api.menu.delMenu(record.id);
|
||||||
},
|
},
|
||||||
|
|
@ -93,36 +87,35 @@ const [menuTable, menu] = useAniTable({
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
search: {
|
source: search => api.menu.getMenus({ ...search, tree: true, size: 0 }),
|
||||||
items: [
|
search: [
|
||||||
{
|
{
|
||||||
extend: "name",
|
extend: 'name',
|
||||||
required: false,
|
required: false,
|
||||||
nodeProps: {
|
setterProps: {
|
||||||
placeholder: "菜单名称",
|
placeholder: '菜单名称',
|
||||||
},
|
|
||||||
},
|
},
|
||||||
],
|
},
|
||||||
},
|
],
|
||||||
create: {
|
create: {
|
||||||
title: "新建菜单",
|
title: '新建菜单',
|
||||||
modalProps: {
|
width: 980,
|
||||||
width: 732,
|
formClass: '!grid grid-cols-2 gap-x-4',
|
||||||
maskClosable: false,
|
|
||||||
},
|
|
||||||
formProps: {
|
|
||||||
layout: "vertical",
|
|
||||||
class: "!grid grid-cols-2 gap-x-4",
|
|
||||||
},
|
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
field: "parentId",
|
field: 'parentId',
|
||||||
initial: 0,
|
value: 0,
|
||||||
label: "父级",
|
label: '父级',
|
||||||
type: "treeSelect",
|
setter: 'treeSelect',
|
||||||
|
setterProps: {
|
||||||
|
fieldNames: {
|
||||||
|
key: 'id',
|
||||||
|
title: 'name',
|
||||||
|
},
|
||||||
|
},
|
||||||
async options() {
|
async options() {
|
||||||
const res = await api.menu.getMenus({ size: 0 });
|
const res = await api.menu.getMenus({ size: 0 });
|
||||||
const data = res.data.data?.filter((i) => i.type !== MenuType.BUTTON) ?? [];
|
const data = res.data.data?.filter(i => i.type !== MenuType.BUTTON) ?? [];
|
||||||
for (const item of data) {
|
for (const item of data) {
|
||||||
const type = MenuTypes.fmt(item.type);
|
const type = MenuTypes.fmt(item.type);
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
|
@ -132,92 +125,82 @@ const [menuTable, menu] = useAniTable({
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
id: 0,
|
id: 0,
|
||||||
name: "主类目",
|
name: '主类目',
|
||||||
children: list,
|
children: list,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
nodeProps: {
|
|
||||||
fieldNames: {
|
|
||||||
key: "id",
|
|
||||||
title: "name",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "type",
|
field: 'type',
|
||||||
initial: 1,
|
value: 1,
|
||||||
label: "类型",
|
label: '类型',
|
||||||
type: "radio",
|
setter: 'input',
|
||||||
options: MenuTypes.raw,
|
options: MenuTypes.raw,
|
||||||
nodeProps: {
|
|
||||||
type: "button",
|
|
||||||
class: "w-full",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "name",
|
field: 'name',
|
||||||
label: "名称",
|
label: '名称',
|
||||||
type: "input",
|
setter: 'input',
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "code",
|
field: 'code',
|
||||||
label: "标识",
|
label: '标识',
|
||||||
type: "input",
|
setter: 'input',
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "icon",
|
field: 'icon',
|
||||||
label: "图标",
|
label: '图标',
|
||||||
type: "input",
|
setter: 'input',
|
||||||
required: true,
|
required: true,
|
||||||
visible: ({ model }) => model.type !== MenuType.BUTTON,
|
visible: ({ model }) => model.type !== MenuType.BUTTON,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "path",
|
field: 'path',
|
||||||
label: "路径",
|
label: '路径',
|
||||||
type: "input",
|
setter: 'input',
|
||||||
required: true,
|
required: true,
|
||||||
visible: ({ model }) => model.type !== MenuType.BUTTON,
|
visible: ({ model }) => model.type !== MenuType.BUTTON,
|
||||||
nodeProps: {
|
setterProps: {
|
||||||
placeholder: "内链请以 / 开头,外链请以 http 开头",
|
placeholder: '内链请以 / 开头,外链请以 http 开头',
|
||||||
},
|
},
|
||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
match: /^(\/|http)/,
|
match: /^(\/|http)/,
|
||||||
message: "请以 / 或 http 开头",
|
message: '请以 / 或 http 开头',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "component",
|
field: 'component',
|
||||||
label: "关联组件",
|
label: '关联组件',
|
||||||
type: "select",
|
setter: 'select',
|
||||||
required: true,
|
required: true,
|
||||||
visible: ({ model }) => model.type === MenuType.PAGE,
|
visible: ({ model }) => model.type === MenuType.PAGE,
|
||||||
options: menuArr,
|
options: menuArr,
|
||||||
nodeProps: {
|
setterProps: {
|
||||||
placeholder: "当前页面对应的前端组件",
|
placeholder: '当前页面对应的前端组件',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "description",
|
field: 'description',
|
||||||
label: "菜单描述",
|
label: '菜单描述',
|
||||||
type: "textarea",
|
setter: 'textarea',
|
||||||
itemProps: {
|
itemProps: {
|
||||||
class: "col-span-2",
|
class: 'col-span-2',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
submit: ({ model }) => {
|
submit: model => {
|
||||||
return api.menu.addMenu(model);
|
return api.menu.addMenu(model as any);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
modify: {
|
modify: {
|
||||||
extend: true,
|
extend: true,
|
||||||
title: "修改菜单",
|
title: '修改菜单',
|
||||||
submit: ({ model }) => {
|
submit: model => {
|
||||||
return api.menu.setMenu(model.id, model);
|
return api.menu.setMenu(model.id, model);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -5,53 +5,49 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="tsx">
|
<script setup lang="tsx">
|
||||||
import { api } from "@/api";
|
import { api } from '@/api';
|
||||||
import { createColumn, updateColumn, useAniTable } from "@/components";
|
import { useCreateColumn, useTable, useUpdateColumn } from '@/components/AnTable';
|
||||||
|
|
||||||
defineOptions({ name: 'SystemRolePage' })
|
defineOptions({ name: 'SystemRolePage' });
|
||||||
|
|
||||||
const [roleTable, roleCtx] = useAniTable({
|
const { component: RoleTable } = useTable({
|
||||||
data: async () => {
|
source: () => {
|
||||||
return api.role.getRoles();
|
return api.role.getRoles();
|
||||||
},
|
},
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
title: "角色名称",
|
title: '角色名称',
|
||||||
dataIndex: "username",
|
dataIndex: 'username',
|
||||||
width: 180,
|
render: ({ record }) => (
|
||||||
render({ record }) {
|
<div class="flex flex-col overflow-hidden">
|
||||||
return (
|
<span>
|
||||||
<div class="flex flex-col overflow-hidden">
|
{record.name}
|
||||||
<span>{record.name}</span>
|
<span class="text-gray-400 text-xs truncate ml-2">@{record.code}</span>
|
||||||
<span class="text-gray-400 text-xs truncate">#{record.slug}</span>
|
</span>
|
||||||
</div>
|
<div class="text-gray-400 text-xs truncate mt-0.5">{record.description}</div>
|
||||||
);
|
</div>
|
||||||
},
|
),
|
||||||
},
|
},
|
||||||
|
useCreateColumn(),
|
||||||
|
useUpdateColumn(),
|
||||||
{
|
{
|
||||||
title: "角色描述",
|
title: '操作',
|
||||||
dataIndex: "description",
|
type: 'button',
|
||||||
},
|
|
||||||
createColumn,
|
|
||||||
updateColumn,
|
|
||||||
{
|
|
||||||
title: "操作",
|
|
||||||
type: "button",
|
|
||||||
width: 200,
|
width: 200,
|
||||||
buttons: [
|
buttons: [
|
||||||
{
|
{
|
||||||
type: "modify",
|
text: '分配权限',
|
||||||
text: "修改",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: "分配权限",
|
|
||||||
onClick: ({ record }) => {
|
onClick: ({ record }) => {
|
||||||
console.log(record);
|
console.log(record);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: "删除",
|
type: 'modify',
|
||||||
type: "delete",
|
text: '修改',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: '删除',
|
||||||
|
type: 'delete',
|
||||||
onClick: ({ record }) => {
|
onClick: ({ record }) => {
|
||||||
return api.role.delRole(record.id);
|
return api.role.delRole(record.id);
|
||||||
},
|
},
|
||||||
|
|
@ -59,64 +55,43 @@ const [roleTable, roleCtx] = useAniTable({
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
search: {
|
search: [
|
||||||
items: [
|
{
|
||||||
{
|
field: 'name',
|
||||||
field: "name",
|
label: '角色名称',
|
||||||
type: "input",
|
setter: 'input',
|
||||||
nodeProps: {
|
},
|
||||||
placeholder: "角色名称",
|
],
|
||||||
},
|
|
||||||
itemProps: {
|
|
||||||
hideLabel: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
create: {
|
create: {
|
||||||
title: "新建角色",
|
title: '新建角色',
|
||||||
modalProps: {
|
width: 580,
|
||||||
width: 580,
|
|
||||||
maskClosable: false,
|
|
||||||
},
|
|
||||||
formProps: {
|
|
||||||
layout: "vertical",
|
|
||||||
},
|
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
field: "name",
|
field: 'name',
|
||||||
label: "角色名称",
|
label: '名称',
|
||||||
type: "input",
|
setter: 'input',
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "code",
|
field: 'code',
|
||||||
label: "角色标识",
|
label: '标识',
|
||||||
type: "input",
|
setter: 'input',
|
||||||
|
required: true,
|
||||||
},
|
},
|
||||||
// {
|
|
||||||
// field: "menuIds",
|
|
||||||
// label: "关联权限",
|
|
||||||
// type: "select",
|
|
||||||
// options: () => api.menu.getMenus({ size: 0 }),
|
|
||||||
// nodeProps: {
|
|
||||||
// multiple: true,
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
{
|
{
|
||||||
field: "description",
|
field: 'description',
|
||||||
label: "个人描述",
|
label: '个人描述',
|
||||||
type: "textarea",
|
setter: 'textarea',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
submit: ({ model }) => {
|
submit: model => {
|
||||||
return api.role.addRole(model);
|
return api.role.addRole(model as any);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
modify: {
|
modify: {
|
||||||
extend: true,
|
extend: true,
|
||||||
title: "修改角色",
|
title: '修改角色',
|
||||||
submit: ({ model }) => {
|
submit: model => {
|
||||||
return api.role.updateRole(model.id, model);
|
return api.role.updateRole(model.id, model);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -131,7 +106,7 @@ const [roleTable, roleCtx] = useAniTable({
|
||||||
"name": "SystemRolePage",
|
"name": "SystemRolePage",
|
||||||
"sort": 10302,
|
"sort": 10302,
|
||||||
"title": "角色管理",
|
"title": "角色管理",
|
||||||
"icon": "icon-park-outline-key"
|
"icon": "icon-park-outline-shield"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</route>
|
</route>
|
||||||
|
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
import { api } from '@/api';
|
|
||||||
import { useAniFormModal } from '@/components';
|
|
||||||
|
|
||||||
export const usePassworModal = () => {
|
|
||||||
return useAniFormModal({
|
|
||||||
title: '重置密码',
|
|
||||||
trigger: false,
|
|
||||||
modalProps: {
|
|
||||||
width: 432,
|
|
||||||
},
|
|
||||||
model: {
|
|
||||||
id: undefined,
|
|
||||||
nickname: undefined,
|
|
||||||
},
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
field: 'password',
|
|
||||||
label: ({ model }) => `${model.nickname} 的新密码:`,
|
|
||||||
type: 'input',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
submit: async ({ model }) => {
|
|
||||||
return api.user.setUser(model.id, model);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
@ -1,22 +1,36 @@
|
||||||
<template>
|
<template>
|
||||||
<BreadPage>
|
<BreadPage>
|
||||||
<UserTable />
|
<UserTable />
|
||||||
<pass-modal></pass-modal>
|
<PasswordModal></PasswordModal>
|
||||||
</BreadPage>
|
</BreadPage>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="tsx">
|
<script setup lang="tsx">
|
||||||
import { api } from '@/api';
|
import { api } from '@/api';
|
||||||
import { useCreateColumn, useTable, useUpdateColumn } from '@/components/AnTable';
|
import { useCreateColumn, useTable, useUpdateColumn } from '@/components/AnTable';
|
||||||
import { usePassworModal } from './components/password';
|
import { useFormModal } from '@/components/AnForm';
|
||||||
|
|
||||||
defineOptions({ name: 'SystemUserPage' });
|
defineOptions({ name: 'SystemUserPage' });
|
||||||
const [passModal, passCtx] = usePassworModal();
|
|
||||||
|
const { component: PasswordModal, open } = useFormModal({
|
||||||
|
title: '重置密码',
|
||||||
|
trigger: false,
|
||||||
|
width: 432,
|
||||||
|
model: {
|
||||||
|
id: undefined,
|
||||||
|
nickname: undefined,
|
||||||
|
},
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
field: 'password',
|
||||||
|
label: '新密码',
|
||||||
|
setter: 'input',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
submit: model => api.user.setUser(model.id, model as any),
|
||||||
|
});
|
||||||
|
|
||||||
const { component: UserTable } = useTable({
|
const { component: UserTable } = useTable({
|
||||||
source: async model => {
|
|
||||||
return api.user.getUsers(model);
|
|
||||||
},
|
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
title: '用户昵称',
|
title: '用户昵称',
|
||||||
|
|
@ -54,9 +68,7 @@ const { component: UserTable } = useTable({
|
||||||
buttons: [
|
buttons: [
|
||||||
{
|
{
|
||||||
text: '重置密码',
|
text: '重置密码',
|
||||||
onClick({ record }) {
|
onClick: ({ record }) => open(record),
|
||||||
passCtx.open(record);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'modify',
|
type: 'modify',
|
||||||
|
|
@ -72,16 +84,14 @@ const { component: UserTable } = useTable({
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
search: {
|
source: model => api.user.getUsers(model),
|
||||||
hideSearch: true,
|
search: [
|
||||||
items: [
|
{
|
||||||
{
|
field: 'nickname',
|
||||||
field: 'nickname',
|
label: '用户昵称',
|
||||||
label: '用户昵称',
|
setter: 'input',
|
||||||
setter: 'input',
|
},
|
||||||
},
|
],
|
||||||
],
|
|
||||||
},
|
|
||||||
create: {
|
create: {
|
||||||
title: '新建用户',
|
title: '新建用户',
|
||||||
width: 820,
|
width: 820,
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,10 @@
|
||||||
body {
|
body {
|
||||||
// --border-radius-small: 4px;
|
// --border-radius-small: 4px;
|
||||||
|
|
||||||
|
.arco-table .arco-table-element {
|
||||||
|
table-layout: fixed;
|
||||||
|
}
|
||||||
|
|
||||||
.arco-icon-hover::before {
|
.arco-icon-hover::before {
|
||||||
width: 28px;
|
width: 28px;
|
||||||
height: 28px;
|
height: 28px;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue