@ithinkdt/naive
Version:
iThinkDT Naive UI
539 lines (498 loc) • 24.6 kB
JSX
import { defineComponent, ref, h, watch, resolveComponent, mergeProps, nextTick, onMounted } from 'vue'
import { NGrid, gridProps, NGi, NFlex, NButton, NIcon, NTooltip, NForm, NFormItem } from 'ithinkdt-ui'
import { promiseTimeout } from '@vueuse/core'
import { clone } from '@ithinkdt/common'
import { $msg, useI18n } from '@ithinkdt/core'
import { c, cB, cE, cM, CSS_MOUNT_ANCHOR_META_NAME, CSS_STYLE_PREFIX as p } from '@ithinkdt/core/cssr'
import { IHelp, IUp, IDown } from './assets'
export const DtForm = defineComponent({
name: 'DtForm',
props: {
...gridProps,
collapsed: undefined,
cols: {
type: [Number, String],
required: false,
default: '12 768:18 1280:24 1536:30',
},
yGap: { type: [Number, String], required: false, default: undefined },
items: { type: Array, required: true },
model: { type: Object, required: true },
preset: { type: String, required: false, default: undefined },
loading: { type: Boolean, required: false, default: false },
disabled: { type: Boolean, required: false, default: false },
showColon: { type: Boolean, required: false, default: false },
labelWidth: { type: [String, Number], required: false, default: undefined },
labelAlign: { type: String, required: false, default: undefined },
labelPlacement: { type: String, required: false, default: 'left' },
requireMarkPlacement: { type: String, required: false, default: 'left' },
showRequireMark: { type: Boolean, required: false, default: undefined },
showFeedback: { type: Boolean, required: false, default: undefined },
readonly: { type: Boolean, required: false, default: false },
onReset: { type: [Array, Function], default: undefined },
defaultCollapsed: { type: Boolean, default: true },
actionAlign: { type: String, default: 'right' },
showAction: { type: Boolean, default: undefined },
submitText: { type: String, default: undefined },
},
emits: ['submit'],
setup(props, { emit, expose }) {
const cls = `${p}-grid-form`
createStyle(cls)
const formRef = ref({})
const delegate = {
validate: (...p) => {
return Promise.all(props.items.map((it) => it && !it.hidden && it.submit?.())).then(() =>
formRef.value?.validate(...p),
)
},
restoreValidation: () => formRef.value?.restoreValidation(),
}
expose(delegate)
const collapsed = ref(true)
onMounted(async () => {
await nextTick()
await promiseTimeout(10)
watch(
() => props.defaultCollapsed,
(defaultCollapsed) => {
collapsed.value = defaultCollapsed
},
{ immediate: true },
)
})
let modelBak = {}
watch(
() => props.model,
(model, oModel) => {
if (model && model !== oModel) modelBak = clone(model)
},
{ immediate: true },
)
const cacheRule = Symbol()
const validing = ref(false)
const onSubmit = (e) => {
e?.preventDefault?.()
validing.value = true
delegate
.validate()
.then(() => {
emit('submit', props.model, e)
})
.catch((error) => {
console.debug('form: invalid value on submit', error)
if (props.preset === 'search') {
// TODO 优化提示
$msg.warning(error?.message || error?.[0]?.[0]?.message || error)
}
})
.finally(() => {
validing.value = false
})
}
const gridRef = ref()
return () => {
const {
items,
model,
preset,
readonly,
loading,
showColon,
labelWidth,
labelPlacement,
labelAlign = labelPlacement === 'top' ? 'left' : 'right',
requireMarkPlacement,
showRequireMark,
showFeedback,
disabled,
xGap,
yGap,
cols,
...gridProps
} = props
const spans = formRef.value?.$el ? gridRef.value?.responsiveCols ?? 0 : 0
const isSearch = preset === 'search'
const _showFeedback = !readonly && (showFeedback ?? !isSearch)
let sumspan = 0
const _gridProps = mergeProps(gridProps, { class: `${cls}__grid` })
return (
<NForm
ref={formRef}
class={{ [cls]: true, [`${cls}--readonly`]: readonly }}
model={model}
labelWidth={labelWidth ?? (preset === 'search' ? (showColon ? '6.2em' : '5.8em') : '7.2em')}
labelAlign={labelAlign}
labelPlacement={labelPlacement}
requireMarkPlacement={requireMarkPlacement}
showFeedback={_showFeedback}
showRequireMark={readonly || showRequireMark === false ? false : undefined}
disabled={disabled}
onReset={(e) => {
e?.preventDefault?.()
formRef.value?.restoreValidation()
let reset = props.onReset
if (reset) {
reset = Array.isArray(reset) ? reset : [reset]
for (const it of reset) it()
} else {
for (const it of props.items) {
if (it?.name && !it.name.startsWith('$')) {
// eslint-disable-next-line unicorn/no-null
model[it.name] = modelBak[it.name] ?? null
}
}
Object.assign(model, modelBak)
}
if (props.preset === 'search') {
onSubmit()
}
}}
onSubmit={onSubmit}
>
<NGrid
{..._gridProps}
ref={gridRef}
cols={cols}
xGap={xGap}
yGap={readonly ? 0 : yGap ?? (_showFeedback ? 0 : 20)}
collapsed={preset === 'search' && collapsed.value}
itemResponsive
>
{
((sumspan = 0),
items.map((item, i) => {
if (!item[cacheRule]) {
item[cacheRule] = (item.rule ? [item.rule].flat() : []).map((it) => {
const validator = (_, v) => {
return Promise.resolve(
(typeof it === 'function' ? it : it.validator)(v, props.model),
).then((res) => {
if (res === false || (res && res !== true)) {
throw res === false ? new Error(item.tip) : res
}
})
}
return typeof it === 'function'
? { validator, trigger: 'blur' }
: { ...it, validator }
})
}
let {
type,
name,
readonly: readonly2,
modelValue,
trim: _0,
props: cProps,
slots,
tip: _tip,
parse,
transform,
rule: _1,
hidden,
label,
labelPlacement: lp2 = labelPlacement,
labelProps = {},
required,
showColon: itemColon,
first = true,
view,
component: _2,
showFeedback,
offset,
span,
rowSpan,
suffix,
...it
} = item
const tip = _tip && typeof _tip !== 'function' ? () => _tip : _tip
readonly2 ||= readonly
if (hidden) return
sumspan += span * (rowSpan || 1)
const _labelProps = mergeProps(labelProps, { class: `${cls}__label` })
if (i === items.length - 1 && readonly2 && preset !== 'search') {
span += sumspan % spans
}
return name && !name.startsWith('$') ? (
<NGi
class={`${cls}__gi`}
offset={offset}
span={span}
style={rowSpan ? { gridRowEnd: `span ${rowSpan}` } : undefined}
suffix={suffix}
key={name}
>
<NFormItem
{...it}
class={`${cls}__item`}
path={name}
required={required && !readonly2}
first={first}
rule={item[cacheRule]}
labelPlacement={lp2}
labelProps={_labelProps}
showFeedback={showFeedback}
>
{{
label: () => (
<span style="display: inline-flex; align-items: flex-start; gap: 3px">
<span>{typeof label === 'function' ? label() : label}</span>
{lp2 === 'left' && isSearch && tip ? (
<NTooltip>
{{
default: tip,
trigger: () => (
<NButton
text
style="font-size: 1.25em; opacity: 0.8"
>
<NIcon>
<IHelp />
</NIcon>
</NButton>
),
}}
</NTooltip>
) : undefined}
{showColon && itemColon !== false ? (
<span style="place-self: center">:</span>
) : undefined}
{lp2 === 'top' && !isSearch && tip ? (
<span class={[`${cls}__tip`, `${cls}__tip--top`]}>
{tip()}
</span>
) : undefined}
</span>
),
default: () => {
let fVNode
let v = model[name]
if (readonly2 || !type || type === 'view') {
fVNode = (
<span style="width: 100%">
{view ? view(v, { model }) : v}
</span>
)
} else {
v = transform ? transform(v) : v
const c =
typeof type === 'string' ? resolveComponent(type) : type
const _modelValue = modelValue ?? 'value'
let _cProps = mergeProps(
{
name,
},
cProps,
{
// eslint-disable-next-line unicorn/no-null
[_modelValue]: v ?? null,
['onUpdate:' + _modelValue]: (v) => {
model[name] = parse ? parse(v) : v
},
},
)
fVNode = h(c, _cProps, slots)
}
return isSearch ? (
fVNode
) : (
<div
style={`width: 100%; display: flex; flex-direction: column;`}
>
<div style="min-height: 32px; display: flex; align-items: center">
{fVNode}
</div>
{lp2 === 'left' && !readonly2 && tip ? (
<span class={`${cls}__tip`}>{tip()}</span>
) : undefined}
</div>
)
},
}}
</NFormItem>
</NGi>
) : (
<NGi
class={`${cls}__gi`}
offset={offset}
span={span}
style={rowSpan ? { gridRowEnd: `span ${rowSpan}` } : undefined}
suffix={suffix}
key={name || i}
>
{h(typeof type === 'string' ? resolveComponent(type) : type, cProps, slots)}
</NGi>
)
}))
}
{!readonly && preset && props.showAction !== false ? (
<NGi
suffix={preset === 'search' && spans !== 0 && sumspan + 6 > spans}
span={preset === 'search' ? 6 : 30}
key="preset"
>
{{
default: () => {
return (
<NFormItem
label={isSearch || props.actionAlign === 'left' ? ' ' : undefined}
showLabel={isSearch || props.labelPlacement !== 'top'}
>
<DtFormAction
v-model:collapsed={collapsed.value}
preset={preset}
justify={
preset === 'form'
? props.actionAlign === 'left'
? 'start'
: props.actionAlign === 'right'
? 'end'
: 'center'
: undefined
}
overflow={spans !== 0 && sumspan + 6 > spans}
loading={loading || validing.value}
disabled={disabled || validing.value}
submitText={props.submitText}
style="flex: 1 1 auto"
/>
</NFormItem>
)
},
}}
</NGi>
) : undefined}
</NGrid>
</NForm>
)
}
},
})
let style
function createStyle(cls) {
if (!style) {
style = cB('grid-form', [
cM('readonly', [
cE('grid', {
paddingBottom: '1px',
}),
cE('gi', {
border: '1px solid var(--dt-border-color)',
margin: '0 -1px -1px 0',
}),
cE('label', {
backgroundColor: 'var(--dt-tab-color)',
borderRight: '1px solid var(--dt-border-color)',
height: '100%',
}),
cE(
'item',
{
height: '100%',
gridTemplateAreas: `'label blank'`,
gridTemplateRows: 'auto',
},
[
c('.n-form-item-blank', {
height: '100%',
padding: '6px 14px',
}),
],
),
]),
cE(
'tip',
{
color: `var(--${p}-text-color3)`,
padding: '4px 1px 0',
fontSize: '13px',
},
[
cM('top', {
padding: '0px 0 0 12px',
placeSelf: 'flex-end',
}),
],
),
])
style.mount({
id: cls,
anchorMetaName: CSS_MOUNT_ANCHOR_META_NAME,
})
}
}
const DtFormAction = defineComponent({
props: {
collapsed: {
type: Boolean,
required: true,
},
preset: {
type: String,
required: true,
},
justify: {
type: String,
default: 'start',
},
overflow: {
type: Boolean,
required: true,
},
loading: {
type: Boolean,
required: true,
},
disabled: {
type: Boolean,
default: false,
},
submitText: {
type: String,
default: undefined,
},
},
emits: {
'update:collapsed': () => true,
reset: () => true,
},
setup(props, { emit }) {
const { t } = useI18n()
const showCollapsed = ref(false)
watch(
[() => props.collapsed, () => props.overflow],
([collapsed, overflow]) => {
if (collapsed) {
showCollapsed.value = overflow
}
},
{ immediate: true },
)
return () => {
const { preset, justify, disabled, loading, collapsed } = props
const isSearch = preset === 'search'
return (
<NFlex justify={isSearch && showCollapsed.value ? 'end' : justify} align="center">
<NButton type="primary" disabled={disabled} loading={loading} attrType="submit">
{props.submitText || t(isSearch ? 'form.search' : 'form.save')}
</NButton>
<NButton attrType="reset" onClick={() => emit('reset')} disabled={disabled || loading}>
{t('form.reset')}
</NButton>
{isSearch && showCollapsed.value ? (
<NButton
text
type="primary"
iconPlacement="right"
renderIcon={collapsed ? IDown : IUp}
onClick={() => emit('update:collapsed', !collapsed)}
>
{collapsed ? t('form.expand') : t('form.collapse')}
</NButton>
) : undefined}
</NFlex>
)
}
},
})