551 lines
22 KiB
Vue
551 lines
22 KiB
Vue
<script lang="ts">
|
|
import { isArray, isString } from 'lodash-es'
|
|
import type { PropType, VNode } from 'vue'
|
|
import { computed, createVNode, defineComponent, reactive, resolveComponent } from 'vue'
|
|
import { dayjs } from 'element-plus'
|
|
import { getArea } from '/@/api/common'
|
|
import type { InputAttr, InputData, ModelValueTypes } from '/@/components/baInput'
|
|
import { inputTypes } from '/@/components/baInput'
|
|
import Array from '/@/components/baInput/components/array.vue'
|
|
import BaUpload from '/@/components/baInput/components/baUpload.vue'
|
|
import Editor from '/@/components/baInput/components/editor.vue'
|
|
import IconSelector from '/@/components/baInput/components/iconSelector.vue'
|
|
import RemoteSelect from '/@/components/baInput/components/remoteSelect.vue'
|
|
|
|
export default defineComponent({
|
|
name: 'baInput',
|
|
props: {
|
|
// 输入框类型,支持的输入框见 inputTypes
|
|
type: {
|
|
type: String,
|
|
required: true,
|
|
validator: (value: string) => {
|
|
return inputTypes.includes(value)
|
|
},
|
|
},
|
|
// 双向绑定值
|
|
modelValue: {
|
|
type: null,
|
|
required: true,
|
|
},
|
|
// 输入框的附加属性
|
|
attr: {
|
|
type: Object as PropType<InputAttr>,
|
|
default: () => {},
|
|
},
|
|
// 额外数据,radio、checkbox的选项等数据
|
|
data: {
|
|
type: Object as PropType<InputData>,
|
|
default: () => {},
|
|
},
|
|
},
|
|
emits: ['update:modelValue'],
|
|
setup(props, { emit, slots }) {
|
|
const normalizeDateTimeValue = (value: unknown, format: string) => {
|
|
if (value === null || value === undefined || value === '') {
|
|
return value
|
|
}
|
|
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
const ms = value > 9999999999 ? value : value * 1000
|
|
const d = dayjs(ms)
|
|
return d.isValid() ? d.format(format) : value
|
|
}
|
|
if (typeof value === 'string') {
|
|
const trimmed = value.trim()
|
|
if (/^\d{10,13}$/.test(trimmed)) {
|
|
const num = Number(trimmed)
|
|
if (Number.isFinite(num)) {
|
|
const ms = trimmed.length === 13 ? num : num * 1000
|
|
const d = dayjs(ms)
|
|
return d.isValid() ? d.format(format) : value
|
|
}
|
|
}
|
|
}
|
|
return value
|
|
}
|
|
|
|
// 合并 props.attr 和 props.data
|
|
const attrs = computed(() => {
|
|
return { ...props.attr, ...props.data }
|
|
})
|
|
|
|
// 通用值更新函数
|
|
const onValueUpdate = (value: ModelValueTypes) => {
|
|
emit('update:modelValue', value)
|
|
}
|
|
|
|
// 基础用法 string textarea password
|
|
const bases = () => {
|
|
return () =>
|
|
createVNode(
|
|
resolveComponent('el-input'),
|
|
{
|
|
type: props.type == 'string' ? 'text' : props.type,
|
|
...attrs.value,
|
|
modelValue: props.modelValue,
|
|
'onUpdate:modelValue': onValueUpdate,
|
|
},
|
|
slots
|
|
)
|
|
}
|
|
// radio checkbox
|
|
const rc = () => {
|
|
if (!attrs.value.content) {
|
|
console.warn('请传递 ' + props.type + ' 的 content')
|
|
}
|
|
|
|
const vNodes = computed(() => {
|
|
const vNode: VNode[] = []
|
|
const contentIsArray = isArray(attrs.value.content)
|
|
const type = attrs.value.button ? props.type + '-button' : props.type
|
|
for (const key in attrs.value.content) {
|
|
let nodeProps = {}
|
|
if (contentIsArray) {
|
|
if (typeof attrs.value.content[key].value == 'number') {
|
|
console.warn(props.type + ' 的 content.value 不能是数字')
|
|
}
|
|
|
|
nodeProps = {
|
|
...attrs.value.content[key],
|
|
border: attrs.value.border ? attrs.value.border : false,
|
|
...(attrs.value.childrenAttr || {}),
|
|
}
|
|
} else {
|
|
nodeProps = {
|
|
value: key,
|
|
label: attrs.value.content[key],
|
|
border: attrs.value.border ? attrs.value.border : false,
|
|
...(attrs.value.childrenAttr || {}),
|
|
}
|
|
}
|
|
vNode.push(createVNode(resolveComponent('el-' + type), nodeProps, slots))
|
|
}
|
|
return vNode
|
|
})
|
|
|
|
return () => {
|
|
const valueComputed = computed(() => {
|
|
if (props.type == 'radio') {
|
|
if (props.modelValue == undefined) return ''
|
|
return '' + props.modelValue
|
|
} else {
|
|
let modelValueArr: anyObj = []
|
|
for (const key in props.modelValue) {
|
|
modelValueArr[key] = '' + props.modelValue[key]
|
|
}
|
|
return modelValueArr
|
|
}
|
|
})
|
|
return createVNode(
|
|
resolveComponent('el-' + props.type + '-group'),
|
|
{
|
|
...attrs.value,
|
|
modelValue: valueComputed.value,
|
|
'onUpdate:modelValue': onValueUpdate,
|
|
},
|
|
() => vNodes.value
|
|
)
|
|
}
|
|
}
|
|
// select selects
|
|
const select = () => {
|
|
if (!attrs.value.content) {
|
|
console.warn('请传递 ' + props.type + '的 content')
|
|
}
|
|
|
|
const vNodes = computed(() => {
|
|
const vNode: VNode[] = []
|
|
for (const key in attrs.value.content) {
|
|
vNode.push(
|
|
createVNode(
|
|
resolveComponent('el-option'),
|
|
{
|
|
key: key,
|
|
label: attrs.value.content[key],
|
|
value: key,
|
|
...(attrs.value.childrenAttr || {}),
|
|
},
|
|
slots
|
|
)
|
|
)
|
|
}
|
|
return vNode
|
|
})
|
|
|
|
return () => {
|
|
const valueComputed = computed(() => {
|
|
if (props.type == 'select') {
|
|
if (props.modelValue == undefined) return ''
|
|
return '' + props.modelValue
|
|
} else {
|
|
let modelValueArr: anyObj = []
|
|
for (const key in props.modelValue) {
|
|
modelValueArr[key] = '' + props.modelValue[key]
|
|
}
|
|
return modelValueArr
|
|
}
|
|
})
|
|
return createVNode(
|
|
resolveComponent('el-select'),
|
|
{
|
|
class: 'w100',
|
|
multiple: props.type == 'select' ? false : true,
|
|
clearable: true,
|
|
...attrs.value,
|
|
modelValue: valueComputed.value,
|
|
'onUpdate:modelValue': onValueUpdate,
|
|
},
|
|
() => vNodes.value
|
|
)
|
|
}
|
|
}
|
|
// datetime
|
|
const datetime = () => {
|
|
let valueFormat = 'YYYY-MM-DD HH:mm:ss'
|
|
switch (props.type) {
|
|
case 'date':
|
|
valueFormat = 'YYYY-MM-DD'
|
|
break
|
|
case 'year':
|
|
valueFormat = 'YYYY'
|
|
break
|
|
}
|
|
const valueComputed = computed(() => normalizeDateTimeValue(props.modelValue, valueFormat))
|
|
return () =>
|
|
createVNode(
|
|
resolveComponent('el-date-picker'),
|
|
{
|
|
class: 'w100',
|
|
type: props.type,
|
|
'value-format': valueFormat,
|
|
...attrs.value,
|
|
modelValue: valueComputed.value,
|
|
'onUpdate:modelValue': onValueUpdate,
|
|
},
|
|
slots
|
|
)
|
|
}
|
|
// upload
|
|
const upload = () => {
|
|
return () =>
|
|
createVNode(
|
|
BaUpload,
|
|
{
|
|
type: props.type,
|
|
modelValue: props.modelValue,
|
|
'onUpdate:modelValue': onValueUpdate,
|
|
...attrs.value,
|
|
},
|
|
slots
|
|
)
|
|
}
|
|
|
|
// remoteSelect remoteSelects
|
|
const remoteSelect = () => {
|
|
return () =>
|
|
createVNode(
|
|
RemoteSelect,
|
|
{
|
|
modelValue: props.modelValue,
|
|
'onUpdate:modelValue': onValueUpdate,
|
|
multiple: props.type == 'remoteSelect' ? false : true,
|
|
...attrs.value,
|
|
},
|
|
slots
|
|
)
|
|
}
|
|
|
|
const buildFun = new Map([
|
|
['string', bases],
|
|
[
|
|
'number',
|
|
() => {
|
|
return () =>
|
|
createVNode(
|
|
resolveComponent('el-input-number'),
|
|
{
|
|
class: 'w100',
|
|
'controls-position': 'right',
|
|
...attrs.value,
|
|
modelValue: isString(props.modelValue) ? Number(props.modelValue) : props.modelValue,
|
|
'onUpdate:modelValue': onValueUpdate,
|
|
},
|
|
slots
|
|
)
|
|
},
|
|
],
|
|
['textarea', bases],
|
|
['password', bases],
|
|
['radio', rc],
|
|
['checkbox', rc],
|
|
[
|
|
'switch',
|
|
() => {
|
|
// 值类型:string,number,boolean,custom
|
|
const valueType = computed(() => {
|
|
if (typeof attrs.value.activeValue !== 'undefined' && typeof attrs.value.inactiveValue !== 'undefined') {
|
|
return 'custom'
|
|
}
|
|
return typeof props.modelValue
|
|
})
|
|
|
|
// 要传递给 el-switch 组件的绑定值,该组件对传入值有限制,先做处理
|
|
const valueComputed = computed(() => {
|
|
if (valueType.value === 'boolean' || valueType.value === 'custom') {
|
|
return props.modelValue
|
|
} else {
|
|
let valueTmp = parseInt(props.modelValue as string)
|
|
return isNaN(valueTmp) || valueTmp <= 0 ? false : true
|
|
}
|
|
})
|
|
return () =>
|
|
createVNode(
|
|
resolveComponent('el-switch'),
|
|
{
|
|
...attrs.value,
|
|
modelValue: valueComputed.value,
|
|
'onUpdate:modelValue': (value: boolean) => {
|
|
let newValue: boolean | string | number = value
|
|
switch (valueType.value) {
|
|
case 'string':
|
|
newValue = value ? '1' : '0'
|
|
break
|
|
case 'number':
|
|
newValue = value ? 1 : 0
|
|
}
|
|
emit('update:modelValue', newValue)
|
|
},
|
|
},
|
|
slots
|
|
)
|
|
},
|
|
],
|
|
['datetime', datetime],
|
|
[
|
|
'year',
|
|
() => {
|
|
return () => {
|
|
const valueComputed = computed(() => normalizeDateTimeValue(props.modelValue, 'YYYY'))
|
|
return createVNode(
|
|
resolveComponent('el-date-picker'),
|
|
{
|
|
class: 'w100',
|
|
type: props.type,
|
|
'value-format': 'YYYY',
|
|
...attrs.value,
|
|
modelValue: valueComputed.value,
|
|
'onUpdate:modelValue': onValueUpdate,
|
|
},
|
|
slots
|
|
)
|
|
}
|
|
},
|
|
],
|
|
['date', datetime],
|
|
[
|
|
'time',
|
|
() => {
|
|
return () =>
|
|
createVNode(
|
|
resolveComponent('el-time-picker'),
|
|
{
|
|
class: 'w100',
|
|
clearable: true,
|
|
format: 'HH:mm:ss',
|
|
valueFormat: 'HH:mm:ss',
|
|
...attrs.value,
|
|
modelValue: props.modelValue,
|
|
'onUpdate:modelValue': onValueUpdate,
|
|
},
|
|
slots
|
|
)
|
|
},
|
|
],
|
|
['select', select],
|
|
['selects', select],
|
|
[
|
|
'array',
|
|
() => {
|
|
return () =>
|
|
createVNode(
|
|
Array,
|
|
{
|
|
modelValue: props.modelValue,
|
|
'onUpdate:modelValue': onValueUpdate,
|
|
...attrs.value,
|
|
},
|
|
slots
|
|
)
|
|
},
|
|
],
|
|
['remoteSelect', remoteSelect],
|
|
['remoteSelects', remoteSelect],
|
|
[
|
|
'city',
|
|
() => {
|
|
type Node = { value?: number; label?: string; leaf?: boolean }
|
|
let maxLevel = attrs.value.level ? attrs.value.level - 1 : 2
|
|
const lastLazyValue: {
|
|
value: string | number[] | unknown
|
|
nodes: Node[]
|
|
key: string
|
|
currentRequest: any
|
|
} = reactive({
|
|
value: 'ready',
|
|
nodes: [],
|
|
key: '',
|
|
currentRequest: null,
|
|
})
|
|
|
|
// 请求到的node备份-s
|
|
let nodeEbak: anyObj = {}
|
|
const getNodes = (level: number, key: string) => {
|
|
if (nodeEbak[level] && nodeEbak[level][key]) {
|
|
return nodeEbak[level][key]
|
|
}
|
|
return false
|
|
}
|
|
const setNodes = (level: number, key: string, nodes: Node[] = []) => {
|
|
if (!nodeEbak[level]) {
|
|
nodeEbak[level] = {}
|
|
}
|
|
nodeEbak[level][key] = nodes
|
|
}
|
|
// 请求到的node备份-e
|
|
|
|
return () =>
|
|
createVNode(
|
|
resolveComponent('el-cascader'),
|
|
{
|
|
modelValue: props.modelValue,
|
|
'onUpdate:modelValue': onValueUpdate,
|
|
class: 'w100',
|
|
clearable: true,
|
|
// city 数据使用 varchar 存储,所以清空时使用 empty string 而不是 null
|
|
valueOnClear: '',
|
|
props: {
|
|
lazy: true,
|
|
lazyLoad(node: any, resolve: any) {
|
|
// lazyLoad会频繁触发,在本地存储请求结果,供重复触发时直接读取
|
|
const { level, pathValues } = node
|
|
let key = pathValues.join(',')
|
|
key = key ? key : 'init'
|
|
|
|
let locaNode = getNodes(level, key)
|
|
if (locaNode) {
|
|
return resolve(locaNode)
|
|
}
|
|
|
|
if (lastLazyValue.key == key && lastLazyValue.value == props.modelValue) {
|
|
if (lastLazyValue.currentRequest) {
|
|
return lastLazyValue.currentRequest
|
|
}
|
|
return resolve(lastLazyValue.nodes)
|
|
}
|
|
|
|
let nodes: Node[] = []
|
|
lastLazyValue.key = key
|
|
lastLazyValue.value = props.modelValue
|
|
lastLazyValue.currentRequest = getArea(pathValues).then((res) => {
|
|
let toStr = false
|
|
if (props.modelValue && typeof (props.modelValue as anyObj)[0] === 'string') {
|
|
toStr = true
|
|
}
|
|
for (const key in res.data) {
|
|
if (toStr) {
|
|
res.data[key].value = res.data[key].value.toString()
|
|
}
|
|
res.data[key].leaf = level >= maxLevel
|
|
nodes.push(res.data[key])
|
|
}
|
|
lastLazyValue.nodes = nodes
|
|
lastLazyValue.currentRequest = null
|
|
setNodes(level, key, nodes)
|
|
resolve(nodes)
|
|
})
|
|
},
|
|
},
|
|
...attrs.value,
|
|
},
|
|
slots
|
|
)
|
|
},
|
|
],
|
|
['image', upload],
|
|
['images', upload],
|
|
['file', upload],
|
|
['files', upload],
|
|
[
|
|
'icon',
|
|
() => {
|
|
return () =>
|
|
createVNode(
|
|
IconSelector,
|
|
{
|
|
modelValue: props.modelValue,
|
|
'onUpdate:modelValue': onValueUpdate,
|
|
...attrs.value,
|
|
},
|
|
slots
|
|
)
|
|
},
|
|
],
|
|
[
|
|
'color',
|
|
() => {
|
|
return () =>
|
|
createVNode(
|
|
resolveComponent('el-color-picker'),
|
|
{
|
|
modelValue: props.modelValue,
|
|
'onUpdate:modelValue': (newValue: string | null) => {
|
|
// color 数据使用 varchar 存储,点击清空时的 null 值使用 empty string 代替
|
|
emit('update:modelValue', newValue === null ? '' : newValue)
|
|
},
|
|
...attrs.value,
|
|
},
|
|
slots
|
|
)
|
|
},
|
|
],
|
|
[
|
|
'editor',
|
|
() => {
|
|
return () =>
|
|
createVNode(
|
|
Editor,
|
|
{
|
|
class: 'w100',
|
|
modelValue: props.modelValue,
|
|
'onUpdate:modelValue': onValueUpdate,
|
|
...attrs.value,
|
|
},
|
|
slots
|
|
)
|
|
},
|
|
],
|
|
[
|
|
'default',
|
|
() => {
|
|
console.warn('暂不支持' + props.type + '的输入框类型,你可以自行在 BaInput 组件内添加逻辑')
|
|
},
|
|
],
|
|
])
|
|
|
|
let action = buildFun.get(props.type) || buildFun.get('default')
|
|
return action!.call(this)
|
|
},
|
|
})
|
|
</script>
|
|
|
|
<style scoped lang="scss">
|
|
.ba-upload-image :deep(.el-upload--picture-card) {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
.ba-upload-file :deep(.el-upload-list) {
|
|
margin-left: -10px;
|
|
}
|
|
</style>
|