数通智联化工云平台
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

590 lines
16 KiB

<!--
@ 作者: 秦东
@ 时间: 2023-07-14 08:50:36
@ 备注: 表单项目
-->
<script lang='ts' setup>
import {
inject,
onMounted,
computed,
watch,
onUnmounted,
markRaw
} from 'vue'
import { Md5 } from 'ts-md5'
import { ElMessage } from 'element-plus'
import Tooltips from '@/components/DesignForm/tooltip.vue'
import TinymceEdit from './tinymce.vue'
import { FormItem, FormList } from '@/api/DesignForm/types'
import {
formatNumber,
objectToArray,
constControlChange,
constSetFormOptions,
constFormProps,
constGetControlByName
} from '@/api/DesignForm/utils'
import validate from '@/api/DesignForm/validate'
import ExpandUser from '@/components/DesignForm/public/expand/user.vue'
import AKSelect from './select.vue'
import { uploadUrl,getRequest } from '@/api/DesignForm'
import formatResult from '@/utils/DesignForm/formatResult'
import { debounce } from '@/utils/DesignForm/index'
import { useRoute } from 'vue-router'
import TextImg from "@/assets/image/tinymce.png"
const props = withDefaults(
defineProps<{
data: FormList
modelValue?: any // 子表和弹性布局时时有传
tProp?: string // 子表时的form-item的prop值,用于子表校验用
}>(),
{}
)
const emits = defineEmits<{
(e: 'update:modelValue', val: any): void
}>()
const route = useRoute()
const formProps = inject(constFormProps, {}) as any
const type = computed(() => {
return formProps.value.type
})
const config = computed(() => {
return props.data.config || {}
})
const control = computed(() => {
return props.data.control
})
const options = ref(props.data.options)
const changeEvent = inject(constControlChange, '') as any
const updateModel = (val: any) => {
changeEvent &&
changeEvent({
key: props.data.name,
value: val,
data: props.data,
tProp: props.tProp
})
}
const value = computed({
get() {
if (props.tProp) {
// 表格和弹性布局
return props.modelValue
} else {
return formProps.value.model[props.data.name]
}
},
set(newVal: any) {
if (props.tProp) {
emits('update:modelValue', newVal)
}
updateModel(newVal)
}
})
// 选择数据转换,默认尝试转数字
const transformOption = (val: string | number, type?: string) => {
switch (config.value.transformData || type) {
case 'none':
return val
case 'string':
try {
return val.toString()
} catch (e) {
return val
}
}
return formatNumber(val)
}
// 当通用修改属性功能添加新字段时,数组更新但toRefs没更新
const getControlByName = inject(constGetControlByName) as any
const sourceFunKey = computed(() => {
const iReg = new RegExp('(?<=\\${)(.*?)(?=})', 'g')
//const iReg = new RegExp('\\${.*?}', 'g') // 结果会包含开头和结尾=>${name}
const apiUrl = config.value.optionsFun
const replace = apiUrl?.match(iReg)
return replace && replace[0]
})
const getLabel = (ele: FormItem) => {
const showColon = formProps.value.showColon ? ':' : ''
if (ele) {
return ele.showLabel ? '' : ele.label + showColon
} else {
return ''
}
}
const currentComponent = computed(() => {
if (props.data.type === 'component') {
// 自定义组件
return config.value.componentName
}
if (props.data.type === 'expand-user') {
return markRaw(ExpandUser)
}
return `el-${props.data.type}`
})
// 控制编辑模式下是否可用
const editDisabled = computed(() => {
if (type.value === 3) {
return true // 查看模式,为不可编辑状态
}
if (type.value === 1 && config.value.addDisabled) {
return true
}
if (type.value === 2 && config.value.editDisabled) {
return true // 编辑模式
}
return control.value.disabled
})
// 返回当前item项的校验规则
const itemRules = computed(() => {
let temp
const itemR: any = props.data.item?.rules || []
const customR = formatCustomRules()
// 如果三个都没有设置,则返回undefined
if (itemR?.length || customR?.length) {
temp = [...customR, ...itemR]
}
return temp
})
// data 根据条件搜索,select远程搜索里data有值
const getAxiosOptions = debounce((data?: any) => {
const {
optionsType,
optionsFun,
method = 'post',
afterResponse,
beforeRequest,
label,
value,
debug // =true可用于调试,不存sessionStorage
} = config.value
if (optionsType !== 0) {
let sourceFun = optionsFun
// 接口数据源
if (optionsType === 1 && sourceFun) {
// 当前控件为动态获取数据,防多次加载,先从本地取。data=true时直接请求
let md5Object:any = new Md5()
md5Object.appendAsciiStr(JSON.stringify(sourceFun + data))
let md5Str = md5Object.end()
// const key = 'getOptions_' + props.data.name + md5(sourceFun + data)
const key = 'getOptions_' + props.data.name + md5Str
const storage = window.sessionStorage.getItem(key)
if (storage && !data && !debug) {
const val = JSON.parse(storage)
if (props.data.type === 'treeSelect') {
control.value.data = val
} else {
options.value = val
}
} else {
// 从url里提取一个动态值,${name}形式提取name
if (sourceFunKey.value) {
const val = formProps.value.model[sourceFunKey.value]
const string = '${' + sourceFunKey.value + '}'
sourceFun = sourceFun.replace(string, val)
}
// 处理请求前的数据
//let newData = Object.assign({}, data || {}, queryParams)
let newData = data || {}
if (typeof beforeRequest === 'function') {
newData =
beforeRequest(newData, route, formProps.value.model) ?? data
}
if (newData === false) {
return
}
if (method === 'get') {
newData = { params: newData }
}
getRequest(sourceFun, newData, { method: method })
.then((res: any) => {
const result = res.data.list || res.data
let formatRes: any = result
// 这里做数据转换,很多时候后端并不能提供完全符合且一样的数据
if (typeof afterResponse === 'string' && afterResponse) {
formatRes = formatResult(result, afterResponse)
} else if (typeof afterResponse === 'function') {
// 没有return时,使用原来的,相当于没处理
formatRes = afterResponse(result) ?? result
} else if (label || value) {
// 没有设置afterResponse时,这里将数据转换为[{label:'',value:''}]形式。只处理一级
formatRes = []
result.forEach((item: any) => {
formatRes.push({
label: item[label] || item.label,
value: item[value] || item.value
})
})
}
if (formatRes === false) {
return
}
// console.log('formatRes', formatRes)
if (props.data.type === 'treeSelect') {
control.value.data = formatRes
} else {
options.value = formatRes
}
if (typeof formatRes === 'object') {
window.sessionStorage.setItem(key, JSON.stringify(formatRes)) //缓存,例如子表添加时不用每添加一行就请求一次
}
})
.catch((res: any) => {
if (props.data.type === 'treeSelect') {
control.value.data = []
} else {
options.value = []
}
console.log(res)
})
}
}
setFormDict(formProps.value.dict) // 表格里新增时行时需要重新设一次
}
})
watch(
() => formProps.value.model[sourceFunKey.value],
() => {
getAxiosOptions()
}
)
// 处理自定义校验规则,将customRules转换后追加到rules里
const formatCustomRules = () => {
const rulesReg: any = {}
validate &&
validate.forEach(item => {
rulesReg[item.type] = item.regExp
})
// 获取校验方法 父级使用provide方法注入
const temp: any = []
props.data.customRules?.forEach((item: any) => {
if (!item.message && item.type !== 'methods') {
return // 方法时允许提示信息为空
}
let obj = {}
if (item.type === 'required') {
obj = { required: true }
} else if (item.type === 'rules') {
// 自定义表达式
obj = { pattern: item.rules }
} else if (item.type === 'methods') {
// 方法时
const methods: any = item.methods
if (methods) {
obj = { validator: inject(methods, {}) }
}
} else if (item.type) {
obj = { pattern: rulesReg[item.type as string] }
}
// 这里判断下防某些条件下重复push的可能或存重复校验类型
let message: any = { message: item.message }
if (!item.message) {
// 当使用validator校验时,如果存在message字段则不能使用 callback(new Error('x'));的提示
message = {}
}
temp.push(
Object.assign(
{
trigger: item.trigger || 'blur'
},
obj,
message
)
)
})
return temp
}
// 从数据接口获取数据设置options,在表单添加或编辑时数据加载完成
const setFormDict = (val: any) => {
if (val && config.value.optionsType === 2) {
const opt = val[config.value.optionsFun] || val[props.data.name] // 不填写默认为当前字段名
if (opt !== undefined) {
options.value = objectToArray(opt)
}
}
}
// 从接口返回的dict会在这里触发
watch(
() => formProps.value.dict,
(val: any) => {
setFormDict(val)
},
{
/*deep: true*/
}
)
// 对单选多选select设置options
const formOptions = inject(constSetFormOptions, {}) as any
watch(
() => formOptions.value,
(val: any) => {
const opt = val[props.data.name]
// 子表内的需要注意下,只有在子表有记录时才生效
if (val && opt !== undefined) {
if (props.data.type === 'treeSelect') {
// 树结构的参数为data
control.value.data = objectToArray(opt)
} else {
options.value = objectToArray(opt)
}
}
}
)
// ------------图片上传处理-----------
const fileList = computed<any>(() => {
const imgVal = formProps.value.model[props.data.name]
if (imgVal && typeof imgVal === 'string') {
const temp: any = []
imgVal.split(',').forEach((item: string) => {
temp.push({
name: item,
url: item
})
})
return temp
}
return imgVal || [] // 这样可支持默认值为array([name:'',url:''这种形式])
})
// 上传成功时
const uploadSuccess = (response: any, uploadFile: any, uploadFiles: any) => {
console.log("response==>",response)
console.log("uploadFile==>",uploadFile)
console.log("uploadFiles==>",uploadFiles)
const oldList = []
fileList.value.forEach((item: any) => {
oldList.push(item.url)
})
oldList.push(response.path)
updateModel(oldList.join(','))
control.value.onSuccess &&
control.value.onSuccess(response, uploadFile, uploadFiles)
console.log("uploadSuccess===>",control.value)
console.log("uploadSuccess=fileList==>",fileList)
}
// 从列表移除
const uploadRemove = (uploadFile: any, uploadFiles: any) => {
const oldList: any = []
fileList.value.forEach((item: any) => {
if (item.url !== uploadFile.url) {
oldList.push(item.url)
}
})
updateModel(oldList.join(','))
control.value.onRemove && control.value.onRemove(uploadFile, uploadFiles)
// todo 需从服务端删除已上传图片时,这里需要发删除请求接口
}
// 上传错误
const uploadError = (err: any, file: any, fileList: any) => {
// console.log('uploadError')
ElMessage.error(file.name + '上传失败')
control.value.onError && control.value.onError(err, file, fileList)
}
// -------------图片上传结束----------------
/****input slot处理***/
const getInputSlot = (key?: string) => {
const slot = key === 'p' ? config.value.prepend : config.value.append
const has = slot.indexOf('key:') === 0
if (!has) {
return false
}
const slotKey = slot.replace('key:', '')
const control = getControlByName(slotKey)
if (!control || Object.keys(control)?.length === 0) {
return false
}
return control
}
const inputSlotChange = (val: string | number, name: string) => {
changeEvent &&
changeEvent({
key: name,
value: val,
data: {},
tProp: ''
})
}
/****input slot处理结束***/
// treeSelect
// const filterMethod = (val: string) => {
// if (props.data.type === 'treeSelect') {
// // 请求参数名,可使用config.queryName传进来
// const queryName = config.value.queryName || 'name'
// control.value.filterMethod && control.value.filterMethod(val)
// getAxiosOptions({ [queryName]: val })
// }
// }
onMounted(() => {
getAxiosOptions()
})
onUnmounted(() => {})
</script>
<template>
<el-form-item
v-bind="data.item"
:prop="tProp || data.name"
:class="config.className"
:rules="itemRules as any"
:label="getLabel(data.item as FormItem)"
>
<template #label v-if="config.help">
{{ getLabel(data.item) }}
<Tooltips :content="config.help" />
</template>
<div class="form-value" v-if="type === 4" v-html="value"></div>
<template v-else>
<el-input
v-bind="control"
v-model="value"
:disabled="editDisabled"
:type="data.type === 'password' ? 'password' : 'text'"
v-if="['input', 'password'].includes(data.type)"
>
<template #prepend v-if="config.prepend">
<div v-if="getInputSlot('p')">
<AKSelect
:data="getInputSlot('p')"
:disabled="editDisabled"
:transform-option="transformOption"
@change="inputSlotChange"
type="slot"
/>
</div>
<span v-else>{{ config.prepend }}</span>
</template>
<template #append v-if="config.append">
<div v-if="getInputSlot()">
<AKSelect
:data="getInputSlot()"
:disabled="editDisabled"
:transform-option="transformOption"
type="slot"
@change="inputSlotChange"
/>
</div>
<span v-else>{{ config.append }}</span>
</template>
</el-input>
<el-input
v-bind="control"
v-model="value"
:disabled="editDisabled"
type="textarea"
v-if="data.type === 'textarea'"
/>
<el-radio-group
v-bind="control"
:disabled="editDisabled"
v-model="value"
v-if="data.type === 'radio'"
>
<el-radio
:key="index"
:label="transformOption(item.value)"
v-for="(item, index) in options"
>
{{ item.label }}
</el-radio>
</el-radio-group>
<el-checkbox-group
v-bind="control"
:disabled="editDisabled"
v-model="value"
v-if="data.type === 'checkbox'"
>
<el-checkbox
v-for="(item, index) in options"
:key="index"
:label="transformOption(item.value)"
>
{{ item.label }}
</el-checkbox>
</el-checkbox-group>
<AKSelect
v-if="
data.type === 'select' || (type === 5 && data.type === 'inputSlot')
"
:data="data"
:disabled="editDisabled"
v-model="value"
:options="options"
:remote-method="getAxiosOptions"
:transformOption="transformOption"
/>
<el-upload
class="upload-style"
v-if="data.type === 'upload'"
:action="uploadUrl"
v-bind="control"
:name="control.file || 'file'"
:disabled="editDisabled"
:file-list="fileList as any"
:class="{limit: control.limit <= fileList.length}"
:on-error="uploadError"
:on-success="uploadSuccess"
:on-remove="uploadRemove"
>
<el-button type="primary" v-if="config?.btnText">
{{ config?.btnText }}
</el-button>
<i class="icon-plus" v-else></i>
<template #tip v-if="config?.tip">
<div class="el-upload__tip">
{{ config?.tip }}
</div>
</template>
</el-upload>
<component
v-if="['cascader', 'treeSelect'].includes(data.type)"
:is="currentComponent"
v-bind="control"
:disabled="editDisabled"
:options="options"
v-model="value"
/>
<component
v-if="
[
'rate',
'slider',
'switch',
'inputNumber',
'colorPicker',
'timePicker',
'datePicker',
'component',
'expand-user'
].includes(data.type)
"
:is="currentComponent"
v-bind="control"
:disabled="editDisabled"
v-model="value"
/>
<template v-if="data.type === 'tinymce'">
<!-- 设计模式时拖动会出现异常设计模式暂用图片代替-->
<tinymce-edit
v-bind="control"
:config="config"
:disabled="editDisabled"
v-model="value"
v-if="[1, 2, 3].includes(type as number)"
/>
<img
alt=""
:src="TextImg"
v-if="type === 5"
style="max-width: 100%"
/>
</template>
</template>
</el-form-item>
</template>
<style lang='scss' scoped>
</style>