feat: 优化 GiForm(同步 GiDemo 更新)

This commit is contained in:
2025-02-27 22:44:04 +08:00
parent 15ae164eef
commit 47769f9ad8
32 changed files with 589 additions and 586 deletions

View File

@@ -1,12 +1,16 @@
<template>
<div class="gi-edit-table">
<a-form ref="formRef" :model="form">
<a-form ref="formRef" scroll-to-first-error :model="form">
<a-table :data="form.tableData" :bordered="{ cell: true }" :pagination="false" v-bind="attrs">
<template #columns>
<a-table-column
v-for="col in props.columns" :key="col.dataIndex" :title="col.title"
:data-index="col.dataIndex" :header-cell-class="headerCellClass(col)" v-bind="col.columnProps"
v-for="col in props.columns" :key="col.dataIndex" :data-index="col.dataIndex"
:header-cell-class="headerCellClass(col)" v-bind="col.columnProps" :title="col.title"
>
<template #title>
<component :is="col?.columnProps?.title" v-if="typeof col?.columnProps?.title === 'function'"></component>
<template v-else>{{ col?.columnProps?.title || col.title }}</template>
</template>
<template #cell="{ record, rowIndex, column }">
<a-form-item
:field="`tableData[${rowIndex}].${col.dataIndex}`" :label-col-style="{ display: 'none' }"
@@ -25,21 +29,31 @@
</template>
</a-table-column>
</template>
<template #tr="{ record, rowIndex }">
<tr class="gi-edit-table-tr" @dblclick="emit('tr-dblclick', { record, rowIndex })"></tr>
</template>
<template #td="{ record, column, rowIndex }">
<td class="gi-edit-table-td" @dblclick="emit('td-dblclick', { record, column, rowIndex })"></td>
</template>
</a-table>
</a-form>
</div>
</template>
<script lang='ts' setup generic="T extends TableData">
import type { TableData } from '@arco-design/web-vue'
import type { TableColumnData, TableData } from '@arco-design/web-vue'
import type { ColumnItem, Disabled } from './type'
defineOptions({ name: 'GiEditTable', inheritAttrs: false })
const props = withDefaults(defineProps<Props>(), {
cellDisabled: false,
})
const emit = defineEmits<{
(e: 'tr-dblclick', value: { record: any, rowIndex: number }): void
(e: 'td-dblclick', value: { record: any, rowIndex: number, column: TableColumnData }): void
}>()
defineSlots<{
[propsName: string]: (props: { record: T, rowIndex: number, column: ColumnItem }) => void
}>()
@@ -55,64 +69,41 @@ const attrs = useAttrs()
const form = computed(() => ({ tableData: props.data }))
const formRef = useTemplateRef('formRef')
const headerCellClass = (col: ColumnItem) => {
return col.required ? 'gi_column_require' : ''
}
const getComponentBindProps = (col: ColumnItem) => {
const obj: Partial<ColumnItem['props'] & { placeholder: string }> = {}
if (col.type === 'input') {
obj.allowClear = true
obj.placeholder = `请输入${col.title}`
obj.maxLength = 50
}
if (col.type === 'input-number') {
obj.placeholder = `输入${col.title}`
}
if (col.type === 'textarea') {
obj.allowClear = true
obj.placeholder = `填写${col.title}`
obj.maxLength = 200
}
if (col.type === 'select') {
obj.allowClear = true
obj.placeholder = `请选择${col.title}`
}
if (col.type === 'cascader') {
obj.allowClear = true
obj.placeholder = `请选择${col.title}`
}
if (col.type === 'tree-select') {
obj.allowClear = true
obj.placeholder = `请选择${col.title}`
}
if (col.type === 'date-picker') {
obj.placeholder = '请选择日期'
}
if (col.type === 'time-picker') {
obj.allowClear = true
obj.placeholder = `请选择时间`
}
return { ...obj, ...col.props }
// 组件默认配置映射表
const ConfigMap = new Map<ColumnItem['type'], Partial<Omit<ColumnItem['props'], 'placeholder'> & { placeholder?: string | string[] }>>([
['input', { allowClear: true, placeholder: `请输入${col.title}`, maxLength: 50 }],
['input-number', { placeholder: `请输入${col.title}` }],
['textarea', { allowClear: false, placeholder: `请填写${col.title}`, maxLength: 200 }],
['input-tag', { allowClear: true, placeholder: `请输入${col.title}` }],
['mention', { allowClear: true, placeholder: `请输入${col.title}` }],
['select', { allowClear: true, placeholder: `选择${col.title}` }],
['tree-select', { allowClear: true, placeholder: `请选择${col.title}` }],
['cascader', { allowClear: true, placeholder: `请选择${col.title}` }],
['radio-group', {}],
['checkbox-group', {}],
['date-picker', { allowClear: true, placeholder: '请选择日期' }],
['time-picker', { allowClear: true, placeholder: '请选择时间' }],
])
// 获取默认配置
const defaultProps = ConfigMap.get(col.type) || {}
// 合并默认配置和自定义配置
return { ...defaultProps, ...col.props }
}
const getRuleMessage = (col: ColumnItem) => {
if (['input', 'input-number'].includes(col.type ?? '')) {
if (['input', 'input-number', 'input-tag', 'mention'].includes(col.type ?? '')) {
return `请输入${col.title}`
}
if (['textarea'].includes(col.type ?? '')) {
return `请填写${col.title}`
}
if (['select', 'cascader', 'tree-select'].includes(col.type ?? '')) {
return `请选择${col.title}`
}
if (['date-picker'].includes(col.type ?? '')) {
return `请选择日期`
}
if (['time-picker'].includes(col.type ?? '')) {
return `请选择时间`
}
return ''
return `请选择${col.title}`
}
const isDisabled: Props['cellDisabled'] = (p) => {
@@ -120,6 +111,7 @@ const isDisabled: Props['cellDisabled'] = (p) => {
if (typeof props?.cellDisabled === 'function') return props.cellDisabled(p)
return false
}
defineExpose({ formRef })
</script>

View File

@@ -1,49 +1,69 @@
import type * as A from '@arco-design/web-vue'
import type { VNode } from 'vue'
export type ColumnItemType =
| 'input'
| 'input-number'
| 'input-tag'
| 'textarea'
| 'select'
| 'tree-select'
| 'radio-group'
| 'checkbox-group'
| 'date-picker'
| 'year-picker'
| 'quarter-picker'
| 'month-picker'
| 'week-picker'
| 'time-picker'
| 'range-picker'
| 'color-picker'
| 'rate'
| 'switch'
| 'slider'
| 'cascader'
| 'upload'
| 'auto-complete'
| 'mention'
| ''
export type ComponentProps =
& A.InputInstance['$props']
& A.InputNumberInstance['$props']
& A.InputTagInstance['$props']
& A.TextareaInstance['$props']
& A.SelectInstance['$props']
& A.TreeSelectInstance['$props']
& A.RadioGroupInstance['$props']
& A.CheckboxGroupInstance['$props']
& A.DatePickerInstance['$props']
& A.YearPickerInstance['$props']
& A.QuarterPickerInstance['$props']
& A.MonthPickerInstance['$props']
& A.WeekPickerInstance['$props']
& A.TimePickerInstance['$props']
& A.RangePickerInstance['$props']
& A.ColorPickerInstance['$props']
& A.RateInstance['$props']
& A.SwitchInstance['$props']
& A.SliderInstance['$props']
& A.CascaderInstance['$props']
& A.UploadInstance['$props']
& A.AutoCompleteInstance['$props']
& A.MentionInstance['$props']
interface ColumnItemProps extends Partial<Omit<ComponentProps, 'placeholder'>> {
placeholder?: string | string[]
}
export interface ColumnItem {
type?:
| 'input'
| 'select'
| 'radio-group'
| 'checkbox-group'
| 'textarea'
| 'date-picker'
| 'year-picker'
| 'quarter-picker'
| 'week-picker'
| 'range-picker'
| 'month-picker'
| 'time-picker'
| 'color-picker'
| 'input-number'
| 'rate'
| 'switch'
| 'slider'
| 'cascader'
| 'tree-select'
| 'upload'
| ''
type?: ColumnItemType
title: string
dataIndex: string
required?: boolean
rules?: A.FormItemInstance['$props']['rules'] // 表单校验规则
props?:
& A.InputInstance['$props']
& A.SelectInstance['$props']
& A.TextareaInstance['$props']
& A.DatePickerInstance['$props']
& A.TimePickerInstance['$props']
& A.RadioGroupInstance['$props']
& A.CheckboxGroupInstance['$props']
& A.InputNumberInstance['$props']
& A.RateInstance['$props']
& A.SwitchInstance['$props']
& A.SliderInstance['$props']
& A.CascaderInstance['$props']
& A.TreeSelectInstance['$props']
& A.UploadInstance['$props']
& A.AlertInstance['$props']
columnProps?: A.TableColumnInstance['$props']
props?: ColumnItemProps
columnProps?: Partial<Omit<A.TableColumnInstance['$props'], 'title'>> & { title?: string | (() => VNode) }
formItemProps?: A.FormItemInstance['$props']
slotName?: string
}

View File

@@ -1,6 +1,5 @@
import GiForm from './src/GiForm.vue'
import { useGiForm } from './src/hooks'
export type * from './src/type'
export { GiForm, useGiForm }
export { GiForm }
export default GiForm

View File

@@ -1,15 +1,14 @@
<template>
<a-form ref="formRef" :auto-label-width="true" v-bind="options.form" :model="modelValue">
<a-grid class="w-full" :col-gap="8" v-bind="options.grid" :collapsed="collapsed">
<template v-for="(item, index) in columns" :key="item.field">
<a-form ref="formRef" v-bind="formProps" :model="modelValue" :size="props.size ?? 'large'" :layout="props.layout ?? (props.search ? 'inline' : 'horizontal')">
<a-grid class="w-full" :col-gap="8" v-bind="props.gridProps" :collapsed="collapsed">
<template v-for="item in columns" :key="item.field">
<a-grid-item
v-if="item.show !== undefined ? isShow(item) : !isHide(item)"
v-show="colVShow(index)"
v-bind="item.gridItemProps || props.options.gridItem"
:span="item.span || options.gridItem?.span"
v-bind="item.gridItemProps || defaultGridItemProps"
:span="item.span || item.gridItemProps?.span || defaultGridItemProps?.span"
>
<a-form-item
v-bind="item.formItemProps" :field="item.field" :rules="item.rules"
v-bind="item.formItemProps" :field="item.field" :rules="getFormItemRules(item)"
:disabled="isDisabled(item)"
>
<template #label>
@@ -32,31 +31,40 @@
:model-value="modelValue[item.field as keyof typeof modelValue]"
@update:model-value="valueChange($event, item.field)"
>
<template v-for="(slotValue, slotKey) in item?.slots" :key="slotKey" #[slotKey]>
<template v-for="(slotValue, slotKey) in item?.slots" :key="slotKey" #[slotKey]="scope">
<template v-if="typeof slotValue === 'string'">{{ slotValue }}</template>
<component :is="slotValue" v-else></component>
<template v-else-if="slotValue">
<component :is="slotValue(scope)"></component>
</template>
</template>
</component>
</slot>
<slot v-else name="group-title">
<a-alert v-bind="item.props">{{ item.label }}</a-alert>
</slot>
<template v-for="(slotValue, slotKey) in item?.formItemSlots" :key="slotKey" #[slotKey]>
<template v-if="typeof slotValue === 'string'">{{ slotValue }}</template>
<component :is="slotValue" v-else></component>
</template>
</a-form-item>
</a-grid-item>
</template>
<a-grid-item v-if="!options.btns?.hide" :suffix="options.fold?.enable">
<a-space wrap :size="[8, 16]" style="flex-wrap: nowrap">
<a-grid-item
v-if="props.search" v-bind="defaultGridItemProps" :span="defaultGridItemProps?.span"
:suffix="props.search && props.suffix"
>
<a-space wrap>
<slot name="suffix">
<a-button type="primary" @click="emit('search')">
<template #icon><icon-search /></template>
<template #default>{{ options.btns?.searchBtnText || '搜索' }}</template>
<template #default>{{ props.searchBtnText }}</template>
</a-button>
<a-button @click="emit('reset')">
<template #icon><icon-refresh /></template>
<template #default>重置</template>
</a-button>
<a-button
v-if="options.fold?.enable" class="gi-form__fold-btn" type="text" size="mini"
v-if="!props.hideFoldBtn" class="gi-form__fold-btn" type="text" size="mini"
@click="collapsed = !collapsed"
>
<template #icon>
@@ -73,17 +81,42 @@
</template>
<script setup lang="ts">
import { cloneDeep } from 'lodash-es'
import type { ColumnsItem, Options } from './type'
import { cloneDeep, omit } from 'lodash-es'
import type { FormInstance, GridItemProps, GridProps } from '@arco-design/web-vue'
import type { ColumnItem } from './type'
interface Props {
modelValue: any
options?: Options
columns: ColumnsItem[]
layout?: FormInstance['layout']
size?: FormInstance['size']
labelColProps?: FormInstance['labelColProps']
wrapperColProps?: FormInstance['wrapperColProps']
labelAlign?: FormInstance['labelAlign']
disabled?: FormInstance['disabled']
rules?: FormInstance['rules']
autoLabelWidth?: FormInstance['autoLabelWidth']
id?: FormInstance['id']
scrollToFirstError?: FormInstance['scrollToFirstError']
// 额外自定义属性
columns: ColumnItem[]
gridProps?: GridProps
gridItemProps?: GridItemProps
search?: boolean // 搜索模式
defaultCollapsed?: boolean // 折叠按钮默认折叠
searchBtnText?: string // 搜索按钮文字
hideFoldBtn?: boolean // 隐藏展开收起按钮,在表单项少的时候手动隐藏
suffix?: boolean
}
const props = withDefaults(defineProps<Props>(), {
options: () => ({}),
autoLabelWidth: true,
scrollToFirstError: true,
defaultCollapsed: false,
search: false,
gridItemProps: { span: { xs: 24, sm: 12, xxl: 8 } },
searchBtnText: '搜索',
hideFoldBtn: false,
suffix: true,
})
const emit = defineEmits<{
@@ -92,68 +125,57 @@ const emit = defineEmits<{
(e: 'reset'): void
}>()
const options = computed(() => ({
grid: { cols: 1 },
gridItem: { span: { xs: 2, sm: 1 } },
...props.options,
}
))
const formProps = computed(() => {
const baseProps = omit(props, ['columns', 'gridProps', 'gridItemProps', 'search', 'defaultCollapsed', 'searchBtnText', 'hideFoldBtn', 'suffix', 'layout'])
return { ...baseProps }
})
const defaultGridItemProps = computed(() => {
return props.gridItemProps
})
const formRef = useTemplateRef('formRef')
const collapsed = ref(props.options.fold?.defaultCollapsed ?? false)
const collapsed = ref(props.defaultCollapsed)
const dicData: Record<string, any> = reactive({})
// col组件的显示隐藏
const colVShow = (index: number) => {
return index <= (props.options.fold?.index || 0) || (index >= (props.options.fold?.index || 0) && !collapsed.value)
}
// 组件的默认props配置
const getComponentBindProps = (item: ColumnsItem) => {
const obj: Partial<ColumnsItem['props'] & { placeholder: string }> = {}
switch (item.type) {
case 'input':
case 'input-password':
case 'input-number':
obj.allowClear = true
obj.placeholder = `请输入${item.label}`
break
case 'textarea':
obj.placeholder = `输入${item.label}`
obj.maxLength = 200
break
case 'select':
case 'cascader':
obj.allowClear = true
obj.placeholder = `请选择${item.label}`
obj.options = dicData[item.field] || item.options
break
case 'tree-select':
obj.allowClear = true
obj.placeholder = `请选择${item.label}`
obj.data = dicData[item.field] || item.data
break
case 'radio-group':
case 'checkbox-group':
obj.options = dicData[item.field] || item.options
break
case 'date-picker':
obj.placeholder = '请选择日期'
break
case 'time-picker':
obj.allowClear = true
obj.placeholder = `请选择时间`
break
}
return { ...obj, ...item.props }
const getComponentBindProps = (item: ColumnItem) => {
// 组件默认配置映射表
const ConfigMap = new Map<ColumnItem['type'], Partial<ColumnItem['props'] & { placeholder: string }>>([
['input', { allowClear: true, placeholder: `请输入${item.label}`, maxLength: 20 }],
['input-number', { placeholder: `请输入${item.label}` }],
['textarea', { allowClear: false, placeholder: `请输入${item.label}`, maxLength: 200 }],
['input-tag', { allowClear: true, placeholder: `请输入${item.label}` }],
['mention', { allowClear: true, placeholder: `请输入${item.label}` }],
['select', { allowClear: true, placeholder: `请选择${item.label}`, options: dicData[item.field] || [] }],
['tree-select', { allowClear: true, placeholder: `请选择${item.label}` }],
['cascader', { allowClear: true, placeholder: `选择${item.label}`, options: dicData[item.field] || [] }],
['radio-group', { options: dicData[item.field] || [] }],
['checkbox-group', { options: dicData[item.field] || [] }],
['date-picker', { allowClear: true, placeholder: '请选择日期' }],
['time-picker', { allowClear: true, placeholder: '请选择时间' }],
])
// 获取默认配置
const defaultProps = ConfigMap.get(item.type) || {}
// 合并默认配置和自定义配置
return { ...defaultProps, ...item.props }
}
/** 表单数据更新 */
const valueChange = (value: any, field: string) => {
emit('update:modelValue', Object.assign(props.modelValue, { [field]: value }))
}
/** 表单项校验规则 */
const getFormItemRules = (item: ColumnItem) => {
if (item.required) {
return [{ required: true, message: `${item.label}为必填项` }, ...(Array.isArray(item.rules) ? item.rules : [])]
}
return item.rules
}
/** 显示表单项 */
const isShow = (item: ColumnsItem) => {
const isShow = (item: ColumnItem) => {
if (typeof item.show === 'boolean') return item.show
if (typeof item.show === 'function') {
return item.show(props.modelValue)
@@ -161,7 +183,7 @@ const isShow = (item: ColumnsItem) => {
}
/** 隐藏表单项 */
const isHide = (item: ColumnsItem) => {
const isHide = (item: ColumnItem) => {
if (item.hide === undefined) return false
if (typeof item.hide === 'boolean') return item.hide
if (typeof item.hide === 'function') {
@@ -170,7 +192,7 @@ const isHide = (item: ColumnsItem) => {
}
/** 禁用表单项 */
const isDisabled = (item: ColumnsItem) => {
const isDisabled = (item: ColumnItem) => {
if (item.disabled === undefined) return false
if (typeof item.disabled === 'boolean') return item.disabled
if (typeof item.disabled === 'function') {
@@ -189,7 +211,7 @@ props.columns.forEach((item) => {
// 先找出有级联的项
// 如果这个字段改变了值那么就找出它的cascader属性对应的字段项去请求里面的request
const hasCascaderColumns: ColumnsItem[] = []
const hasCascaderColumns: ColumnItem[] = []
props.columns.forEach((item) => {
const arr = hasCascaderColumns.map((i) => i.field)
if (item.cascader?.length && !arr.includes(item.field)) {
@@ -211,12 +233,12 @@ watch(cloneForm as any, (newVal, oldVal) => {
i.request(props.modelValue).then((res) => {
dicData[i.field] = i.resultFormat ? i.resultFormat(res) : res.data
if (!dicData[i.field].map((i: any) => i.value).includes(props.modelValue[i.field])) {
emit('update:modelValue', Object.assign(props.modelValue, { [i.field]: '' }))
emit('update:modelValue', Object.assign(props.modelValue, { [i.field]: Array.isArray(props.modelValue[i.field]) ? [] : '' }))
}
})
} else if (i.request && !newVal[item.field]) {
dicData[i.field] = []
emit('update:modelValue', Object.assign(props.modelValue, { [i.field]: '' }))
emit('update:modelValue', Object.assign(props.modelValue, { [i.field]: Array.isArray(props.modelValue[i.field]) ? [] : '' }))
}
})
}
@@ -226,10 +248,7 @@ watch(cloneForm as any, (newVal, oldVal) => {
defineExpose({ formRef })
</script>
<style scoped lang="scss">
:deep(.arco-form-item-layout-inline) {
margin-right: 0;
}
<style lang="scss" scoped>
.gi-form__fold-btn {
padding: 0 5px;
}

View File

@@ -1,48 +0,0 @@
import { reactive } from 'vue'
import { cloneDeep } from 'lodash-es'
import { Message } from '@arco-design/web-vue'
import type { Columns, ColumnsItem, ColumnsItemPropsKey } from './type'
export function useGiForm(initValue: Columns) {
const getInitValue = () => cloneDeep(initValue)
const columns = reactive(getInitValue())
const resetColumns = () => {
Object.assign(columns, getInitValue())
}
const setValue = <T>(field: string, key: keyof ColumnsItem, value: T) => {
if (!columns.length) return
const obj = columns.find((i) => i.field === field)
if (obj) {
obj[key] = value as never
} else {
Message.warning(`没有这个field属性值-${field},请检查!`)
}
}
const setPropsValue = <T>(field: string, key: ColumnsItemPropsKey, value: T) => {
if (!columns.length) return
const obj = columns.find((i) => i.field === field)
if (obj) {
if (!obj.props) {
obj.props = {}
}
obj.props[key as keyof ColumnsItem['props']] = value as never
} else {
Message.warning(`没有这个field属性值-${field},请检查!`)
}
}
return {
/** 配置项 */
columns,
/** 重置 columns */
resetColumns,
/** 设置 columns 某个对象属性的值 */
setValue,
/** 设置 columns.props 某个属性的值 */
setPropsValue,
}
}

View File

@@ -1,124 +1,215 @@
import type * as A from '@arco-design/web-vue'
import type { VNode } from 'vue'
export type FormType =
export type ColumnItemType =
| 'input'
| 'input-password'
| 'input-number'
| 'input-tag'
| 'textarea'
| 'select'
| 'tree-select'
| 'radio-group'
| 'checkbox-group'
| 'textarea'
| 'date-picker'
| 'year-picker'
| 'quarter-picker'
| 'week-picker'
| 'range-picker'
| 'month-picker'
| 'week-picker'
| 'time-picker'
| 'range-picker'
| 'color-picker'
| 'rate'
| 'switch'
| 'slider'
| 'cascader'
| 'tree-select'
| 'upload'
| 'auto-complete'
| 'mention'
| 'group-title'
export type ColumnsItemPropsKey =
| keyof A.InputInstance['$props']
| keyof A.InputPasswordInstance['$props']
| keyof A.InputNumberInstance['$props']
| keyof A.SelectInstance['$props']
| keyof A.TextareaInstance['$props']
| keyof A.DatePickerInstance['$props']
| keyof A.TimePickerInstance['$props']
| keyof A.YearPickerInstance['$props']
| keyof A.MonthPickerInstance['$props']
| keyof A.QuarterPickerInstance['$props']
| keyof A.WeekPickerInstance['$props']
| keyof A.RangePickerInstance['$props']
| keyof A.RadioGroupInstance['$props']
| keyof A.CheckboxGroupInstance['$props']
| keyof A.ColorPickerInstance['$props']
| keyof A.RateInstance['$props']
| keyof A.SwitchInstance['$props']
| keyof A.SliderInstance['$props']
| keyof A.CascaderInstance['$props']
| keyof A.TreeSelectInstance['$props']
| keyof A.UploadInstance['$props']
| keyof A.AlertInstance['$props']
export type ComponentProps =
& A.InputInstance['$props']
& A.InputNumberInstance['$props']
& A.InputTagInstance['$props']
& A.TextareaInstance['$props']
& A.SelectInstance['$props']
& A.TreeSelectInstance['$props']
& A.RadioGroupInstance['$props']
& A.CheckboxGroupInstance['$props']
& A.DatePickerInstance['$props']
& A.YearPickerInstance['$props']
& A.QuarterPickerInstance['$props']
& A.MonthPickerInstance['$props']
& A.WeekPickerInstance['$props']
& A.TimePickerInstance['$props']
& A.RangePickerInstance['$props']
& A.ColorPickerInstance['$props']
& A.RateInstance['$props']
& A.SwitchInstance['$props']
& A.SliderInstance['$props']
& A.CascaderInstance['$props']
& A.UploadInstance['$props']
& A.AutoCompleteInstance['$props']
& A.MentionInstance['$props']
& A.AlertInstance['$props']
export type ColumnsItemHide<F> = boolean | ((form: F) => boolean)
export type ColumnsItemShow<F> = boolean | ((form: F) => boolean)
export type ColumnsItemDisabled<F> = boolean | ((form: F) => boolean)
export type ColumnsItemRequest<F = any> = (form: F) => Promise<any>
export type ColumnsItemFormat<T = any> = (
res: T
) =>
| A.SelectInstance['$props']['options']
| A.RadioGroupInstance['$props']['options']
| A.CheckboxGroupInstance['$props']['options']
| A.CascaderInstance['$props']['options']
| A.TreeSelectInstance['$props']['data']
interface ColumnItemProps extends Partial<Omit<ComponentProps, 'placeholder'>> {
placeholder?: string | string[]
}
export type ColumnsItemOptionsOrData =
export type ColumnItemOptions =
| A.SelectInstance['$props']['options']
| A.RadioGroupInstance['$props']['options']
| A.CheckboxGroupInstance['$props']['options']
| A.CascaderInstance['$props']['options']
| A.TreeSelectInstance['$props']['data']
export interface ColumnsItem<F = any> {
type?: FormType // 类型
export type ColumnItemData =
| A.TreeSelectInstance['$props']['data']
| A.AutoCompleteInstance['$props']['data']
| A.MentionInstance['$props']['data']
interface AutoCompleteSlots {
option: (e: { data: (string | number | A.SelectOptionData | A.SelectOptionGroup)[] }) => VNode
footer: () => VNode
}
interface CascaderSlots {
'label': (e: { data: A.CascaderOption }) => VNode
'prefix': () => VNode
'arrow-icon': () => VNode
'loading-icon': () => VNode
'search-icon': () => VNode
'empty': () => VNode
'option': (e: { data: A.CascaderOption }) => VNode
}
interface CheckboxGroupSlots {
checkbox: (e: { checked: boolean, disabled: string }) => VNode
label: (e: { data: A.CheckboxOption }) => VNode
}
interface RadioGroupSlots {
radio: (e: { checked: boolean, disabled: string }) => VNode
label: (e: { data: any }) => VNode
}
interface DatePickerSlots {
'prefix': () => VNode
'suffix-icon': () => VNode
'icon-next-double': () => VNode
'icon-prev-double': () => VNode
'icon-next': () => VNode
'icon-prev': () => VNode
'cell': (e: { data: Date }) => VNode
'extra': () => VNode
}
interface InputSlots {
append: (() => VNode) | string
prepend: (() => VNode) | string
suffix: (() => VNode) | string
prefix: (() => VNode) | string
}
interface InputNumberSlots {
minus: (() => VNode) | string
plus: (() => VNode) | string
append: (() => VNode) | string
prepend: (() => VNode) | string
suffix: (() => VNode) | string
}
interface InputTagSlots {
tag: (e: { data: A.TagData }) => VNode
prefix: (() => VNode) | string
suffix: (() => VNode) | string
}
interface RateSlots {
character: (e: { index: number }) => VNode
}
interface SelectSlots {
'trigger': () => VNode
'prefix': () => VNode
'search-icon': () => VNode
'loading-icon': () => VNode
'arrow-icon': () => VNode
'footer': () => VNode
'header': () => VNode
'label': (e: { data: A.SelectOptionData }) => VNode
'option': (e: { data: A.SelectOptionData }) => VNode
'empty': () => VNode
}
interface SwitchSlots {
'checked-icon': () => VNode
'unchecked-icon': () => VNode
'checked': () => VNode
'unchecked': () => VNode
}
interface TreeSelectSlots {
'trigger': () => VNode
'prefix': () => VNode
'label': (e: { data: any }) => VNode
'header': () => VNode
'loader': () => VNode
'empty': () => VNode
'footer': () => VNode
'tree-slot-extra': () => VNode
'tree-slot-title': (e: { title: string }) => VNode
'tree-slot-icon': (e: { node: A.TreeNodeData }) => VNode
'tree-slot-switcher-icon': () => VNode
}
interface MentionSlots {
option: (e: { data: any }) => VNode
}
export type ComponentSlots =
& AutoCompleteSlots
& CascaderSlots
& CheckboxGroupSlots
& RadioGroupSlots
& DatePickerSlots
& InputSlots
& InputNumberSlots
& InputTagSlots
& RateSlots
& SelectSlots
& SwitchSlots
& TreeSelectSlots
& MentionSlots
export interface ColumnItemSlots extends Omit<ComponentSlots, 'label' | 'option'> {
label?: (e: { data: A.CheckboxOption | A.SelectOptionData | A.CascaderOption }) => VNode
option?: (e: { data: (string | number | A.SelectOptionData | A.SelectOptionGroup)[] | A.CascaderOption | A.SelectOptionData }) => VNode
}
export type ColumnItemHide<F> = boolean | ((form: F) => boolean)
export type ColumnItemShow<F> = boolean | ((form: F) => boolean)
export type ColumnItemDisabled<F> = boolean | ((form: F) => boolean)
export type ColumnItemRequest<F = any> = (form: F) => Promise<any>
export type ColumnItemFormat<T = any> = (res: T) => ColumnItemOptions | ColumnItemData
export interface ColumnItem<F = any> {
type?: ColumnItemType // 类型
label?: A.FormItemInstance['label'] | (() => VNode) // 标签
field: A.FormItemInstance['field'] // 字段(必须唯一)
span?: A.GridItemProps['span']
props?: ColumnItemProps
gridItemProps?: A.GridItemProps
formItemProps?: Omit<A.FormItemInstance['$props'], 'label' | 'field'> // a-form-item的props
props?:
& A.InputInstance['$props']
& A.InputPasswordInstance['$props']
& A.InputNumberInstance['$props']
& A.SelectInstance['$props']
& A.TextareaInstance['$props']
& A.DatePickerInstance['$props']
& A.TimePickerInstance['$props']
& A.RadioGroupInstance['$props']
& A.CheckboxGroupInstance['$props']
& A.RateInstance['$props']
& A.SwitchInstance['$props']
& A.SliderInstance['$props']
& A.CascaderInstance['$props']
& A.TreeSelectInstance['$props']
& A.UploadInstance['$props']
& A.AlertInstance['$props']
required?: boolean // 是否必填
rules?: A.FormItemInstance['$props']['rules'] // 表单校验规则
// 下拉列表|复选框组|单选框组|级联选择组件的options
options?:
| A.SelectInstance['$props']['options']
| A.RadioGroupInstance['$props']['options']
| A.CheckboxGroupInstance['$props']['options']
| A.CascaderInstance['$props']['options']
// 下拉树组件的data
span?: A.GridItemProps['span']
data?: A.TreeSelectInstance['$props']['data']
show?: ColumnsItemShow<F> // 是否显示优先级比hide高
hide?: ColumnsItemHide<F> // 是否隐藏
disabled?: ColumnsItemDisabled<F> // 是否禁用
request?: ColumnsItemRequest<F> // 接口请求api
resultFormat?: ColumnsItemFormat // 结果集格式化
hide?: ColumnItemHide<F> // 是否隐藏
show?: ColumnItemShow<F> // 是否显示优先级比hide高
disabled?: ColumnItemDisabled<F> // 是否禁用
request?: ColumnItemRequest<F> // 接口请求api
resultFormat?: ColumnItemFormat // 结果集格式化
init?: boolean // 初始化请求
cascader?: string[] // 级联的field字段列表
slots?: Partial<Record<'prepend' | 'append' | 'suffix' | 'prefix', string | (() => VNode)>>
slots?: Partial<ColumnItemSlots>
formItemSlots?: Partial<Record<'help' | 'extra', string | (() => VNode)>>
}
export interface Options {
form?: Omit<A.FormInstance['$props'], 'model'>
grid?: A.GridProps
gridItem?: A.GridItemProps
btns?: { hide?: boolean, searchBtnText?: string }
fold?: { enable?: boolean, index?: number, defaultCollapsed?: boolean }
}
export type Columns<F = any> = ColumnsItem<F>[]

View File

@@ -1,11 +1,13 @@
<template>
<div ref="rootRef" class="ca-split-panel" :class="{
'is-vertical': vertical,
'is-resizing': resizing,
'is-collapse': isCollapse,
'is-responsive': isResponsive,
'is-mobile': isMobile,
}" :style="customStyle">
<div
ref="rootRef" class="ca-split-panel" :class="{
'is-vertical': vertical,
'is-resizing': resizing,
'is-collapse': isCollapse,
'is-responsive': isResponsive,
'is-mobile': isMobile,
}" :style="customStyle"
>
<div class="container" :style="sideStyle">
<div ref="sideRef" class="ca-split-panel__side">
<div class="ca-split-panel__content">
@@ -15,10 +17,12 @@
<!-- 竖线和按钮 -->
<div class="divider-container">
<div v-show="!isCollapse" class="divider"></div>
<div v-if="allowCollapse" class="ca-split-panel__collapse-trigger" :class="{
'is-collapse': isCollapse,
'is-mobile': isMobile,
}" @click="toggleCollapse">
<div
v-if="allowCollapse" class="ca-split-panel__collapse-trigger" :class="{
'is-collapse': isCollapse,
'is-mobile': isMobile,
}" @click="toggleCollapse"
>
<div class="ca-split-panel__collapse-trigger-icon">
<IconRight v-if="isCollapse" size="20" />
<IconLeft v-else size="20" />

View File

@@ -2,4 +2,3 @@ export * from './useMenu'
export * from './useDept'
export * from './useRole'
export * from './useDict'
export * from './useFormCurd'

View File

@@ -1,107 +0,0 @@
import { type Ref, computed, reactive, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { type FormInstance, Message, Modal } from '@arco-design/web-vue'
import { isEqual } from 'lodash-es'
interface Option<T> {
key?: string
formRef?: Ref<FormInstance>
initApi: () => Promise<ApiRes<T>>
detailApi: (form: T) => Promise<ApiRes<T>>
addApi: (form: T) => Promise<ApiRes<T>>
editApi: (form: T) => Promise<ApiRes<T>>
onError?: (error: any) => void
onSuccess?: (result: T) => void
addToEdit?: boolean // 新增成功调到编辑
}
export function useFormCurd<T = any>(option: Option<T>) {
const route = useRoute()
const router = useRouter()
const form = reactive({})
const originForm = reactive({}) // 原始表单数据
const isEdit = computed(() => !!route.query[option?.key || 'id'])
const isChanged = ref(false) // 表单的数据是否改变过
const loading = ref(false)
const saveLoading = ref(false) // 保存按钮的加载状态
const title = computed(() => (isEdit.value ? '编辑' : '新增'))
const initForm = async () => {
try {
loading.value = true
const res = isEdit.value ? await option.detailApi(form as T) : await option.initApi()
if (res.success) {
Object.assign(form, res.data)
Object.assign(originForm, res.data)
isChanged.value = false
}
} catch (error) {
option.onError && option.onError(error)
} finally {
loading.value = false
}
}
initForm()
watch(
() => route.query,
() => {
initForm()
},
)
watch(
() => form,
(newVal) => {
// console.log('newVal', toRaw(newVal))
// console.log('originForm', toRaw(originForm))
if (!isEqual(newVal, originForm)) {
isChanged.value = true
}
},
{ immediate: true, deep: true },
)
const save = async () => {
try {
const valid = await option?.formRef?.value?.validate()
if (valid) return
saveLoading.value = true
const res = isEdit.value ? await option.editApi(form as T) : await option.addApi(form as T)
if (res.success) {
Message.success(isEdit.value ? '修改成功' : '新增成功')
if (!isEdit.value && option.addToEdit === true) {
router.replace({ path: route.fullPath, query: { [option.key as string]: res.data[option.key as string] } })
}
option.onSuccess && option.onSuccess(res.data)
}
} catch (error) {
option.onError && option.onError(error)
} finally {
saveLoading.value = false
}
}
const back = () => {
if (isChanged.value) {
Modal.warning({
title: '提示',
content: '您确定丢弃更改的内容吗?',
hideCancel: false,
onOk: () => {
router.back()
},
})
} else {
router.back()
}
}
const reset = () => {
option?.formRef?.value?.resetFields()
}
return { form: form as T, title, loading, isEdit, back, save, saveLoading, reset }
}

View File

@@ -0,0 +1,31 @@
<template>
<icon-refresh class="reload-icon" :class="{ 'reload-icon--spin': loading }" :size="18" @click="reload" />
</template>
<script lang="ts" setup>
import { useTabsStore } from '@/stores'
const tabsStore = useTabsStore()
const loading = ref(false)
// 重载页面
const reload = () => {
if (loading.value) return
loading.value = true
tabsStore.reloadPage()
setTimeout(() => {
loading.value = false
}, 600)
}
</script>
<style lang="scss" scoped>
.reload-icon {
cursor: pointer;
&--spin {
animation-name: arco-loading-circle;
animation-duration: 0.6s;
}
}
</style>

View File

@@ -51,17 +51,12 @@
</template>
<template #default>关闭其他</template>
</a-doption>
<a-doption @click="tabsStore.closeAll">
<template #icon>
<icon-minus />
</template>
<template #default>关闭全部</template>
</a-doption>
</template>
</a-dropdown>
</template>
</a-tab-pane>
<template #extra>
<ReloadIcon></ReloadIcon>
<a-dropdown trigger="hover">
<a-button type="text">
<template #icon>
@@ -69,12 +64,6 @@
</template>
</a-button>
<template #content>
<a-doption @click="reload">
<template #icon>
<icon-refresh class="reload-icon" :class="{ 'reload-icon--spin': loading }" />
</template>
<template #default>重新加载</template>
</a-doption>
<a-doption @click="tabsStore.closeCurrent(route.path)">
<template #icon>
<icon-close />
@@ -114,6 +103,7 @@
<script setup lang="ts">
import type { RouteLocationNormalized } from 'vue-router'
import ReloadIcon from './ReloadIcon.vue'
import { useAppStore, useTabsStore } from '@/stores'
defineOptions({ name: 'Tabs' })

View File

@@ -9,7 +9,7 @@ const storeSetup = () => {
const tabList = ref<RouteLocationNormalized[]>([]) // 保存页签tab的数组
const cacheList = ref<RouteRecordName[]>([]) // keep-alive缓存的数组元素是组件名
// 添加一个页签, 如果当前路由已经打开, 则不再重复添加
// 添加一个页签如果当前路由已经打开则不再重复添加
const addTabItem = (item: RouteLocationNormalized) => {
const index = tabList.value.findIndex((i) => i.path === item.path)
if (index >= 0) {
@@ -45,6 +45,17 @@ const storeSetup = () => {
tabList.value = arr
}
// 设置当前tab页签名称
const setTabTitle = (title: string) => {
if (!title) return false
const route = router.currentRoute.value
const path = route?.fullPath || route.path
const index = tabList.value.findIndex((i) => i.fullPath === path)
if (index >= 0) {
tabList.value[index].meta.title = title
}
}
// 添加缓存页
const addCacheItem = (item: RouteLocationNormalized) => {
if (!item.name) return
@@ -142,6 +153,7 @@ const storeSetup = () => {
addTabItem,
deleteTabItem,
clearTabList,
setTabTitle,
addCacheItem,
deleteCacheItem,
clearCacheList,

View File

@@ -10,7 +10,7 @@
>
<a-tabs v-model:active-key="activeKey">
<a-tab-pane key="1" title="生成配置">
<GiForm ref="formRef" v-model="form" :options="options" :columns="formColumns" />
<GiForm ref="formRef" v-model="form" :columns="formColumns" />
</a-tab-pane>
<a-tab-pane key="2" title="字段配置">
<GiTable
@@ -124,7 +124,7 @@ import { useWindowSize } from '@vueuse/core'
import { type FieldConfigResp, type GeneratorConfigResp, getGenConfig, listFieldConfig, listFieldConfigDict, saveGenConfig } from '@/apis/code/generator'
import type { LabelValueState } from '@/types/global'
import type { TableInstanceColumns } from '@/components/GiTable/type'
import { type Columns, GiForm, type Options } from '@/components/GiForm'
import { type ColumnItem, GiForm } from '@/components/GiForm'
import { useResetReactive } from '@/hooks'
import { useDict } from '@/hooks/app'
@@ -141,15 +141,10 @@ const formRef = ref<InstanceType<typeof GiForm>>()
const { form_type_enum, query_type_enum } = useDict('form_type_enum', 'query_type_enum')
const dictList = ref<LabelValueState[]>([])
const options: Options = {
form: { size: 'large' },
grid: { cols: 2 },
btns: { hide: true },
}
const [form, resetForm] = useResetReactive({
isOverride: false,
})
const formColumns: Columns = reactive([
const formColumns: ColumnItem[] = reactive([
{
label: '作者名称',
field: 'author',

View File

@@ -72,7 +72,7 @@ import { Message, type TreeNodeData } from '@arco-design/web-vue'
import { useClipboard } from '@vueuse/core'
import { type GeneratePreviewResp, genPreview } from '@/apis/code/generator'
const emit = defineEmits([ 'download','generate'])
const emit = defineEmits(['download', 'generate'])
const { copy, copied } = useClipboard()
const genPreviewList = ref<GeneratePreviewResp[]>([])

View File

@@ -9,7 +9,7 @@
@before-ok="save"
@close="reset"
>
<GiForm ref="formRef" v-model="form" :options="options" :columns="columns" />
<GiForm ref="formRef" v-model="form" :columns="columns" />
</a-modal>
</template>
@@ -17,7 +17,7 @@
import { Message } from '@arco-design/web-vue'
import { useWindowSize } from '@vueuse/core'
import { addApp, getApp, updateApp } from '@/apis/open/app'
import { type Columns, GiForm, type Options } from '@/components/GiForm'
import { type ColumnItem, GiForm } from '@/components/GiForm'
import { useResetReactive } from '@/hooks'
const emit = defineEmits<{
@@ -32,26 +32,23 @@ const isUpdate = computed(() => !!dataId.value)
const title = computed(() => (isUpdate.value ? '修改应用' : '新增应用'))
const formRef = ref<InstanceType<typeof GiForm>>()
const options: Options = {
form: { size: 'large' },
btns: { hide: true },
}
const [form, resetForm] = useResetReactive({
status: 1,
})
const columns: Columns = reactive([
const columns: ColumnItem[] = reactive([
{
label: '名称',
field: 'name',
type: 'input',
span: 24,
rules: [{ required: true, message: '请输入名称' }],
},
{
label: '失效时间',
field: 'expireTime',
type: 'date-picker',
span: 24,
props: {
placeholder: '请选择失效时间',
showTime: true,
@@ -61,6 +58,7 @@ const columns: Columns = reactive([
label: '描述',
field: 'description',
type: 'textarea',
span: 24,
props: {
maxLength: 200,
autoSize: { minRows: 3, maxRows: 5 },
@@ -70,6 +68,7 @@ const columns: Columns = reactive([
label: '状态',
field: 'status',
type: 'switch',
span: 24,
props: {
type: 'round',
checkedValue: 1,

View File

@@ -1,5 +1,5 @@
<template>
<a-drawer v-model:visible="visible" title="应用详情" :width="width >= 600 ? 600 : '100%'" :footer="false">
<a-drawer v-model:visible="visible" title="应用详情" :width="width >= 500 ? 500 : '100%'" :footer="false">
<a-descriptions :column="2" size="large" class="general-description">
<a-descriptions-item label="ID">{{ dataDetail?.id }}</a-descriptions-item>
<a-descriptions-item label="名称">{{ dataDetail?.name }}</a-descriptions-item>

View File

@@ -1,5 +1,5 @@
<template>
<a-drawer v-model:visible="visible" title="任务详情" :width="width >= 600 ? 600 : '100%'" :footer="false">
<a-drawer v-model:visible="visible" title="任务详情" :width="width >= 500 ? 500 : '100%'" :footer="false">
<a-descriptions :column="2" size="large" class="general-description">
<a-descriptions-item label="ID" :span="2">
<a-typography-paragraph copyable>{{ dataDetail?.id }}</a-typography-paragraph>

View File

@@ -1,9 +1,9 @@
<template>
<a-modal
v-model:visible="visible" :title="title" :mask-closable="false" :esc-to-close="false"
:width="width >= 600 ? 600 : '100%'" draggable @before-ok="save" @ok="saveAfter" @close="reset"
:width="width >= 500 ? 500 : '100%'" draggable @before-ok="save" @ok="saveAfter" @close="reset"
>
<GiForm ref="formRef" v-model="form" :options="options" :columns="columns">
<GiForm ref="formRef" v-model="form" :columns="columns">
<template #captcha>
<a-input v-model="form.captcha" placeholder="请输入验证码" :max-length="6" allow-clear style="flex: 1 1" />
<a-button
@@ -28,7 +28,7 @@ import NProgress from 'nprogress'
import { type BehaviorCaptchaReq, getEmailCaptcha, updateUserEmail, updateUserPassword, updateUserPhone } from '@/apis'
import { encryptByRsa } from '@/utils/encrypt'
import { useUserStore } from '@/stores'
import { type Columns, GiForm, type Options } from '@/components/GiForm'
import { type ColumnItem, GiForm } from '@/components/GiForm'
import { useResetReactive } from '@/hooks'
import * as Regexp from '@/utils/regexp'
import modalErrorWrapper from '@/utils/modal-error-wrapper'
@@ -44,11 +44,6 @@ const title = computed(
)
const formRef = ref<InstanceType<typeof GiForm>>()
const options: Options = {
form: { size: 'large' },
btns: { hide: true },
}
const [form, resetForm] = useResetReactive({
phone: '',
email: '',
@@ -58,11 +53,12 @@ const [form, resetForm] = useResetReactive({
rePassword: '',
})
const columns: Columns = reactive([
const columns: ColumnItem[] = reactive([
{
label: '手机号',
field: 'phone',
type: 'input',
span: 24,
rules: [
{ required: true, message: '请输入手机号' },
{ match: Regexp.Phone, message: '请输入正确的手机号' },
@@ -75,6 +71,7 @@ const columns: Columns = reactive([
label: '邮箱',
field: 'email',
type: 'input',
span: 24,
rules: [
{ required: true, message: '请输入邮箱' },
{ match: Regexp.Email, message: '请输入正确的邮箱' },
@@ -87,6 +84,7 @@ const columns: Columns = reactive([
label: '验证码',
field: 'captcha',
type: 'input',
span: 24,
rules: [{ required: true, message: '请输入验证码' }],
hide: () => {
return !['phone', 'email'].includes(verifyType.value)
@@ -96,6 +94,7 @@ const columns: Columns = reactive([
label: '当前密码',
field: 'oldPassword',
type: 'input-password',
span: 24,
rules: [{ required: true, message: '请输入当前密码' }],
hide: () => {
return !userInfo.value.pwdResetTime
@@ -105,6 +104,7 @@ const columns: Columns = reactive([
label: '新密码',
field: 'newPassword',
type: 'input-password',
span: 24,
rules: [
{ required: true, message: '请输入新密码' },
{
@@ -125,6 +125,7 @@ const columns: Columns = reactive([
label: '确认新密码',
field: 'rePassword',
type: 'input-password',
span: 24,
props: {
placeholder: '请再次输入新密码',
},

View File

@@ -9,7 +9,7 @@
@before-ok="save"
@close="reset"
>
<GiForm ref="formRef" v-model="form" :options="options" :columns="columns" />
<GiForm ref="formRef" v-model="form" :columns="columns" />
</a-modal>
</template>
@@ -17,7 +17,7 @@
import { useWindowSize } from '@vueuse/core'
import { Message } from '@arco-design/web-vue'
import { updateUserBaseInfo } from '@/apis/system'
import { type Columns, GiForm, type Options } from '@/components/GiForm'
import { type ColumnItem, GiForm } from '@/components/GiForm'
import { useUserStore } from '@/stores'
import { useResetReactive } from '@/hooks'
@@ -28,32 +28,31 @@ const userInfo = computed(() => userStore.userInfo)
const visible = ref(false)
const formRef = ref<InstanceType<typeof GiForm>>()
const options: Options = {
form: { size: 'large' },
btns: { hide: true },
}
const [form, resetForm] = useResetReactive({
nickname: userInfo.value.nickname,
gender: userInfo.value.gender,
})
const columns: Columns = reactive([
const columns: ColumnItem[] = reactive([
{
label: '昵称',
field: 'nickname',
type: 'input',
span: 24,
rules: [{ required: true, message: '请输入昵称' }],
},
{
label: '性别',
field: 'gender',
type: 'radio-group',
options: [
{ label: '男', value: 1 },
{ label: '女', value: 2 },
{ label: '未知', value: 0, disabled: true },
],
span: 24,
props: {
options: [
{ label: '', value: 1 },
{ label: '女', value: 2 },
{ label: '未知', value: 0, disabled: true },
],
},
rules: [{ required: true, message: '请选择性别' }],
},
])

View File

@@ -9,7 +9,7 @@
@before-ok="save"
@close="reset"
>
<GiForm ref="formRef" v-model="form" :options="options" :columns="columns" />
<GiForm ref="formRef" v-model="form" :columns="columns" layout="vertical" />
</a-modal>
</template>
@@ -18,7 +18,7 @@ import { Message } from '@arco-design/web-vue'
import { useWindowSize } from '@vueuse/core'
import CryptoJS from 'crypto-js'
import { addClient, getClient, updateClient } from '@/apis/system/client'
import { type Columns, GiForm, type Options } from '@/components/GiForm'
import { type ColumnItem, GiForm } from '@/components/GiForm'
import { DisEnableStatusList } from '@/constant/common'
import { useResetReactive } from '@/hooks'
import { useDict } from '@/hooks/app'
@@ -36,12 +36,6 @@ const title = computed(() => (isUpdate.value ? '修改终端' : '新增终端'))
const formRef = ref<InstanceType<typeof GiForm>>()
const { client_type, auth_type_enum } = useDict('auth_type_enum', 'client_type')
const options: Options = {
form: { size: 'large', layout: 'vertical' },
btns: { hide: true },
grid: { cols: 2 },
}
const [form, resetForm] = useResetReactive({
activeTimeout: 1800,
timeout: 86400,
@@ -54,13 +48,13 @@ const handleGenerate = () => {
form.clientSecret = CryptoJS.MD5(`${timestamp}`).toString(CryptoJS.enc.Hex)
}
const columns: Columns = reactive([
const columns: ColumnItem[] = reactive([
{
label: '终端 Key',
field: 'clientKey',
type: 'input',
rules: [{ required: true, message: '请输入终端 Key' }],
span: 2,
span: 24,
disabled: () => {
return isUpdate.value
},
@@ -70,7 +64,7 @@ const columns: Columns = reactive([
field: 'clientSecret',
type: 'input',
rules: [{ required: true, message: '请输入终端秘钥' }],
span: 2,
span: 24,
disabled: () => {
return isUpdate.value
},
@@ -89,8 +83,8 @@ const columns: Columns = reactive([
label: '认证类型',
field: 'authType',
type: 'select',
options: auth_type_enum,
props: {
options: auth_type_enum,
multiple: true,
maxTagCount: 2,
},
@@ -100,7 +94,9 @@ const columns: Columns = reactive([
label: '终端类型',
field: 'clientType',
type: 'select',
options: client_type,
props: {
options: client_type,
},
rules: [{ required: true, message: '请选择终端类型' }],
},
{

View File

@@ -1,5 +1,5 @@
<template>
<a-drawer v-model:visible="visible" title="终端详情" :width="width >= 600 ? 600 : '100%'" :footer="false">
<a-drawer v-model:visible="visible" title="终端详情" :width="width >= 500 ? 500 : '100%'" :footer="false">
<a-descriptions :column="1" size="large" class="general-description">
<a-descriptions-item label="ID">{{ dataDetail?.id }}</a-descriptions-item>
<a-descriptions-item label="终端ID"><a-typography-paragraph :copyable="!!dataDetail?.clientId">{{ dataDetail?.clientId }}</a-typography-paragraph></a-descriptions-item>

View File

@@ -9,7 +9,7 @@
@before-ok="save"
@close="reset"
>
<GiForm ref="formRef" v-model="form" :options="options" :columns="columns" />
<GiForm ref="formRef" v-model="form" :columns="columns" />
</a-modal>
</template>
@@ -18,7 +18,7 @@ import { Message } from '@arco-design/web-vue'
import { useWindowSize } from '@vueuse/core'
import { mapTree } from 'xe-utils'
import { type DeptResp, addDept, getDept, updateDept } from '@/apis/system/dept'
import { type Columns, GiForm, type Options } from '@/components/GiForm'
import { type ColumnItem, GiForm } from '@/components/GiForm'
import { useResetReactive } from '@/hooks'
interface Props {
@@ -50,26 +50,19 @@ const deptSelectTree = computed(() => {
}))
})
const options: Options = {
form: { size: 'large' },
btns: { hide: true },
}
const [form, resetForm] = useResetReactive({
sort: 999,
status: 1,
})
const columns: Columns = reactive([
const columns: ColumnItem[] = reactive([
{
label: '上级部门',
field: 'parentId',
type: 'tree-select',
data: deptSelectTree,
hide: (form) => {
return form.parentId === 0
},
span: 24,
props: {
data: deptSelectTree,
allowClear: true,
allowSearch: true,
fallbackOption: false,
@@ -81,20 +74,25 @@ const columns: Columns = reactive([
},
},
rules: [{ required: true, message: '请选择上级部门' }],
hide: (form) => {
return form.parentId === 0
},
},
{
label: '名称',
field: 'name',
type: 'input',
rules: [{ required: true, message: '请输入名称' }],
span: 24,
props: {
maxLength: 30,
},
rules: [{ required: true, message: '请输入名称' }],
},
{
label: '排序',
field: 'sort',
type: 'input-number',
span: 24,
props: {
min: 1,
mode: 'button',
@@ -104,6 +102,7 @@ const columns: Columns = reactive([
label: '描述',
field: 'description',
type: 'textarea',
span: 24,
props: {
maxLength: 200,
autoSize: { minRows: 3, maxRows: 5 },
@@ -113,6 +112,7 @@ const columns: Columns = reactive([
label: '状态',
field: 'status',
type: 'switch',
span: 24,
props: {
type: 'round',
checkedValue: 1,

View File

@@ -9,7 +9,7 @@
@before-ok="save"
@close="reset"
>
<GiForm ref="formRef" v-model="form" :options="options" :columns="columns">
<GiForm ref="formRef" v-model="form" :columns="columns">
<template #color>
<a-input v-model="form.color" placeholder="请选择或输入标签颜色" allow-clear>
<template #suffix>
@@ -25,7 +25,7 @@
import { Message } from '@arco-design/web-vue'
import { useWindowSize } from '@vueuse/core'
import { addDictItem, getDictItem, updateDictItem } from '@/apis/system/dict'
import { type Columns, GiForm, type Options } from '@/components/GiForm'
import { type ColumnItem, GiForm } from '@/components/GiForm'
import { useResetReactive } from '@/hooks'
const emit = defineEmits<{
@@ -41,25 +41,38 @@ const isUpdate = computed(() => !!dataId.value)
const title = computed(() => (isUpdate.value ? '修改字典项' : '新增字典项'))
const formRef = ref<InstanceType<typeof GiForm>>()
const options: Options = {
form: { size: 'large' },
btns: { hide: true },
}
const [form, resetForm] = useResetReactive({
color: 'blue',
sort: 999,
status: 1,
})
const columns: Columns = reactive([
{ label: '标签', field: 'label', type: 'input', rules: [{ required: true, message: '请输入标签' }] },
{ label: '值', field: 'value', type: 'input', rules: [{ required: true, message: '请输入值' }] },
{ label: '标签颜色', field: 'color', type: 'input' },
const columns: ColumnItem[] = reactive([
{
label: '标签',
field: 'label',
type: 'input',
span: 24,
rules: [{ required: true, message: '请输入标签' }],
},
{
label: '值',
field: 'value',
type: 'input',
span: 24,
rules: [{ required: true, message: '请输入值' }],
},
{
label: '标签颜色',
field: 'color',
type: 'input',
span: 24,
},
{
label: '排序',
field: 'sort',
type: 'input-number',
span: 24,
props: {
min: 1,
mode: 'button',
@@ -69,6 +82,7 @@ const columns: Columns = reactive([
label: '描述',
field: 'description',
type: 'textarea',
span: 24,
props: {
maxLength: 200,
autoSize: { minRows: 3, maxRows: 5 },
@@ -78,6 +92,7 @@ const columns: Columns = reactive([
label: '状态',
field: 'status',
type: 'switch',
span: 24,
props: {
type: 'round',
checkedValue: 1,

View File

@@ -9,7 +9,7 @@
@before-ok="save"
@close="reset"
>
<GiForm ref="formRef" v-model="form" :options="options" :columns="columns" />
<GiForm ref="formRef" v-model="form" :columns="columns" />
</a-modal>
</template>
@@ -17,7 +17,7 @@
import { Message } from '@arco-design/web-vue'
import { useWindowSize } from '@vueuse/core'
import { addDict, getDict, updateDict } from '@/apis/system/dict'
import { type Columns, GiForm, type Options } from '@/components/GiForm'
import { type ColumnItem, GiForm } from '@/components/GiForm'
import { useResetReactive } from '@/hooks'
const emit = defineEmits<{
@@ -32,20 +32,30 @@ const isUpdate = computed(() => !!dataId.value)
const title = computed(() => (isUpdate.value ? '修改字典' : '新增字典'))
const formRef = ref<InstanceType<typeof GiForm>>()
const options: Options = {
form: { size: 'large' },
btns: { hide: true },
}
const [form, resetForm] = useResetReactive({})
const columns: Columns = reactive([
{ label: '名称', field: 'name', type: 'input', rules: [{ required: true, message: '请输入名称' }] },
{ label: '编码', field: 'code', type: 'input', disabled: () => isUpdate.value, rules: [{ required: true, message: '请输入编码' }] },
const columns: ColumnItem[] = reactive([
{
label: '名称',
field: 'name',
type: 'input',
span: 24,
rules: [{ required: true, message: '请输入名称',
}],
},
{
label: '编码',
field: 'code',
type: 'input',
span: 24,
rules: [{ required: true, message: '请输入编码' }],
disabled: () => isUpdate.value,
},
{
label: '描述',
field: 'description',
type: 'textarea',
span: 24,
props: {
maxLength: 200,
autoSize: { minRows: 3, maxRows: 5 },

View File

@@ -18,7 +18,7 @@
</a-affix>
</div>
<div class="detail_content" style="display: flex; flex-direction: column;">
<GiForm ref="formRef" v-model="form" :options="options" :columns="columns">
<GiForm ref="formRef" v-model="form" :columns="columns">
<template #noticeUsers>
<a-select
v-model="form.noticeUsers"
@@ -61,7 +61,7 @@ import { useWindowSize } from '@vueuse/core'
import AiEditor from './components/index.vue'
import { addNotice, getNotice, updateNotice } from '@/apis/system/notice'
import { listUserDict } from '@/apis'
import { type Columns, GiForm, type Options } from '@/components/GiForm'
import { type ColumnItem, GiForm } from '@/components/GiForm'
import type { LabelValueState } from '@/types/global'
import { useTabsStore } from '@/stores'
import { useResetReactive } from '@/hooks'
@@ -79,12 +79,6 @@ const containerRef = ref<HTMLElement | null>()
const formRef = ref<InstanceType<typeof GiForm>>()
const { notice_type } = useDict('notice_type')
const options: Options = {
form: { size: 'large' },
grid: { cols: 2 },
btns: { hide: true },
}
const [form, resetForm] = useResetReactive({
title: '',
type: '',
@@ -94,7 +88,7 @@ const [form, resetForm] = useResetReactive({
noticeScope: 1,
})
const columns: Columns = reactive([
const columns: ColumnItem[] = reactive([
{
label: '标题',
field: 'title',
@@ -109,7 +103,9 @@ const columns: Columns = reactive([
label: '类型',
field: 'type',
type: 'select',
options: notice_type,
props: {
options: notice_type,
},
rules: [{ required: true, message: '请输入类型' }],
},
{
@@ -132,7 +128,9 @@ const columns: Columns = reactive([
label: '通知范围',
field: 'noticeScope',
type: 'radio-group',
options: [{ label: '所有人', value: 1 }, { label: '指定用户', value: 2 }],
props: {
options: [{ label: '所有人', value: 1 }, { label: '指定用户', value: 2 }],
},
rules: [{ required: true, message: '请选择通知范围' }],
},
{

View File

@@ -4,7 +4,7 @@
:title="title"
:mask-closable="false"
:esc-to-close="false"
:width="width >= 600 ? 600 : '100%'"
:width="width >= 500 ? 500 : '100%'"
@before-ok="save"
@close="reset"
>

View File

@@ -4,7 +4,7 @@
:title="title"
:mask-closable="false"
:esc-to-close="false"
:width="width >= 600 ? 600 : '100%'"
:width="width >= 500 ? 500 : '100%'"
@before-ok="save"
@close="reset"
>

View File

@@ -4,11 +4,11 @@
:title="title"
:mask-closable="false"
:esc-to-close="false"
:width="width >= 600 ? 600 : '100%'"
:width="width >= 500 ? 500 : '100%'"
@before-ok="save"
@close="reset"
>
<GiForm ref="formRef" v-model="form" :options="options" :columns="columns" />
<GiForm ref="formRef" v-model="form" :columns="columns" />
</a-drawer>
</template>
@@ -16,7 +16,7 @@
import { Message, type TreeNodeData } from '@arco-design/web-vue'
import { useWindowSize } from '@vueuse/core'
import { addUser, getUser, updateUser } from '@/apis/system/user'
import { type Columns, GiForm, type Options } from '@/components/GiForm'
import { type ColumnItem, GiForm } from '@/components/GiForm'
import type { Gender, Status } from '@/types/global'
import { GenderList } from '@/constant/common'
import { useResetReactive } from '@/hooks'
@@ -37,21 +37,17 @@ const formRef = ref<InstanceType<typeof GiForm>>()
const { roleList, getRoleList } = useRole()
const { deptList, getDeptList } = useDept()
const options: Options = {
form: { size: 'large' },
btns: { hide: true },
}
const [form, resetForm] = useResetReactive({
gender: 1 as Gender,
status: 1 as Status,
})
const columns: Columns = reactive([
const columns: ColumnItem[] = reactive([
{
label: '用户名',
field: 'username',
type: 'input',
span: 24,
props: {
maxLength: 64,
showWordLimit: true,
@@ -62,6 +58,7 @@ const columns: Columns = reactive([
label: '昵称',
field: 'nickname',
type: 'input',
span: 24,
props: {
maxLength: 30,
showWordLimit: true,
@@ -72,6 +69,7 @@ const columns: Columns = reactive([
label: '密码',
field: 'password',
type: 'input-password',
span: 24,
props: {
maxLength: 32,
showWordLimit: true,
@@ -85,6 +83,7 @@ const columns: Columns = reactive([
label: '手机号码',
field: 'phone',
type: 'input',
span: 24,
props: {
maxLength: 11,
},
@@ -93,6 +92,7 @@ const columns: Columns = reactive([
label: '邮箱',
field: 'email',
type: 'input',
span: 24,
props: {
maxLength: 255,
},
@@ -101,14 +101,18 @@ const columns: Columns = reactive([
label: '性别',
field: 'gender',
type: 'radio-group',
options: GenderList,
span: 24,
props: {
options: GenderList,
},
},
{
label: '所属部门',
field: 'deptId',
type: 'tree-select',
data: deptList,
span: 24,
props: {
data: deptList,
allowClear: true,
allowSearch: true,
fallbackOption: false,
@@ -125,8 +129,9 @@ const columns: Columns = reactive([
label: '角色',
field: 'roleIds',
type: 'select',
options: roleList,
span: 24,
props: {
options: roleList,
multiple: true,
allowClear: true,
allowSearch: true,
@@ -137,6 +142,7 @@ const columns: Columns = reactive([
label: '描述',
field: 'description',
type: 'textarea',
span: 24,
props: {
maxLength: 200,
showWordLimit: true,
@@ -147,6 +153,7 @@ const columns: Columns = reactive([
label: '状态',
field: 'status',
type: 'switch',
span: 24,
props: {
type: 'round',
checkedValue: 1,

View File

@@ -1,5 +1,5 @@
<template>
<a-drawer v-model:visible="visible" title="用户详情" :width="width >= 600 ? 600 : '100%'" :footer="false">
<a-drawer v-model:visible="visible" title="用户详情" :width="width >= 500 ? 500 : '100%'" :footer="false">
<a-descriptions :column="2" size="large" class="general-description">
<a-descriptions-item label="ID" :span="2">
<a-typography-paragraph copyable>{{ dataDetail?.id }}</a-typography-paragraph>

View File

@@ -4,20 +4,20 @@
title="重置密码"
:mask-closable="false"
:esc-to-close="false"
:modal-style="{ maxWidth: '520px' }"
width="90%"
:width="width >= 500 ? 500 : '100%'"
draggable
@before-ok="save"
@close="reset"
>
<GiForm ref="formRef" v-model="form" :options="options" :columns="columns" />
<GiForm ref="formRef" v-model="form" :columns="columns" />
</a-modal>
</template>
<script setup lang="ts">
import { Message } from '@arco-design/web-vue'
import { useWindowSize } from '@vueuse/core'
import { resetUserPwd } from '@/apis/system'
import { type Columns, GiForm } from '@/components/GiForm'
import { type ColumnItem, GiForm } from '@/components/GiForm'
import { useResetReactive } from '@/hooks'
import { encryptByRsa } from '@/utils/encrypt'
@@ -25,19 +25,15 @@ const emit = defineEmits<{
(e: 'save-success'): void
}>()
const { width } = useWindowSize()
const dataId = ref('')
const visible = ref(false)
const formRef = ref<InstanceType<typeof GiForm>>()
const options: Options = {
form: { size: 'large' },
btns: { hide: true },
}
const [form, resetForm] = useResetReactive({})
const columns: Columns = reactive([
{ label: '密码', field: 'newPassword', type: 'input-password', rules: [{ required: true, message: '请输入密码' }] },
const columns: ColumnItem[] = reactive([
{ label: '密码', field: 'newPassword', type: 'input-password', span: 24, rules: [{ required: true, message: '请输入密码' }] },
])
// 重置

View File

@@ -4,12 +4,12 @@
title="分配角色"
:mask-closable="false"
:esc-to-close="false"
:width="width >= 600 ? 600 : '100%'"
:width="width >= 500 ? 500 : '100%'"
draggable
@before-ok="save"
@close="reset"
>
<GiForm ref="formRef" v-model="form" :options="options" :columns="columns" />
<GiForm ref="formRef" v-model="form" :columns="columns" />
</a-modal>
</template>
@@ -17,7 +17,7 @@
import { Message } from '@arco-design/web-vue'
import { useWindowSize } from '@vueuse/core'
import { getUser, updateUserRole } from '@/apis/system'
import { type Columns, GiForm, type Options } from '@/components/GiForm'
import { type ColumnItem, GiForm } from '@/components/GiForm'
import { useResetReactive } from '@/hooks'
import { useRole } from '@/hooks/app'
@@ -31,20 +31,16 @@ const visible = ref(false)
const formRef = ref<InstanceType<typeof GiForm>>()
const { roleList, getRoleList } = useRole()
const options: Options = {
form: { size: 'large' },
btns: { hide: true },
}
const [form, resetForm] = useResetReactive({})
const columns: Columns = reactive([
const columns: ColumnItem[] = reactive([
{
label: '角色',
field: 'roleIds',
type: 'select',
options: roleList,
span: 24,
props: {
options: roleList,
multiple: true,
allowClear: true,
allowSearch: { retainInputValue: true },

View File

@@ -1,10 +1,5 @@
<template>
<div class="gi_page">
<!-- <a-row justify="space-between" align="center" class="header page_header">
<a-space wrap>
<div class="title">用户管理</div>
</a-space>
</a-row> -->
<SplitPanel size="20%">
<template #left>
<DeptTree @node-click="handleSelectDept" />
@@ -22,7 +17,7 @@
@refresh="search"
>
<template #top>
<GiForm v-model="queryForm" :options="options" :columns="queryFormColumns" @search="search" @reset="reset"></GiForm>
<GiForm v-model="queryForm" search :columns="queryFormColumns" size="medium" @search="search" @reset="reset"></GiForm>
</template>
<template #toolbar-left>
<a-button v-permission="['system:user:add']" type="primary" @click="onAdd">
@@ -102,24 +97,19 @@ import UserDetailDrawer from './UserDetailDrawer.vue'
import UserResetPwdModal from './UserResetPwdModal.vue'
import UserUpdateRoleModal from './UserUpdateRoleModal.vue'
import { type UserResp, deleteUser, exportUser, listUser } from '@/apis/system/user'
import type { Columns, Options } from '@/components/GiForm'
import type { TableInstanceColumns } from '@/components/GiTable/type'
import { DisEnableStatusList } from '@/constant/common'
import { useDownload, useResetReactive, useTable } from '@/hooks'
import { isMobile } from '@/utils'
import has from '@/utils/has'
import type { ColumnItem } from '@/components/GiForm'
defineOptions({ name: 'SystemUser' })
const options: Options = reactive({
form: { layout: 'inline' },
grid: { cols: { xs: 1, sm: 1, md: 2, lg: 3, xl: 3, xxl: 3 } },
fold: { enable: true, index: 1, defaultCollapsed: true },
})
const [queryForm, resetForm] = useResetReactive({
sort: ['t1.id,desc'],
})
const queryFormColumns: Columns = reactive([
const queryFormColumns: ColumnItem[] = reactive([
{
type: 'input',
field: 'description',
@@ -133,18 +123,17 @@ const queryFormColumns: Columns = reactive([
{
type: 'select',
field: 'status',
options: DisEnableStatusList,
formItemProps: {
hideLabel: true,
},
props: {
options: DisEnableStatusList,
placeholder: '请选择状态',
},
},
{
type: 'range-picker',
field: 'createTime',
span: { lg: 2, xl: 2, xxl: 1 },
formItemProps: {
hideLabel: true,
},