mirror of
https://github.com/Tencent/tmagic-editor.git
synced 2026-09-22 03:46:27 +00:00
272 lines
7.3 KiB
Vue
272 lines
7.3 KiB
Vue
<template>
|
||
<div :class="['m-fields-text', { 'm-fields-text--static': config.static }]">
|
||
<div v-if="config.disabled"></div>
|
||
<!-- 新增静态展示文本样式 -->
|
||
<template v-if="config.static">
|
||
<span v-if="config.prepend" class="m-fields-text__prepend">{{ config.prepend }}</span>
|
||
<span class="m-fields-text__value">{{ value }}</span>
|
||
<template v-if="appendConfig">
|
||
<TMagicButton
|
||
v-if="appendConfig.type === 'button'"
|
||
style="color: #0056ea"
|
||
:size="size"
|
||
@click.prevent="buttonClickHandler"
|
||
>
|
||
{{ appendConfig.text }}
|
||
</TMagicButton>
|
||
<img
|
||
v-else-if="appendConfig.type === 'icon'"
|
||
class="m-fields-text__icon"
|
||
:src="appendConfig.text"
|
||
@click="buttonClickHandler"
|
||
/>
|
||
<span v-else>{{ appendConfig.text }}</span>
|
||
</template>
|
||
</template>
|
||
|
||
<TMagicInput
|
||
v-else
|
||
v-model="value"
|
||
ref="input"
|
||
:clearable="config.clearable ?? true"
|
||
:size="size"
|
||
:placeholder="config.placeholder"
|
||
:disabled="disabled"
|
||
@change="changeHandler"
|
||
@input="inputHandler"
|
||
@keyup="keyUpHandler($event)"
|
||
>
|
||
<template #prepend v-if="config.prepend">
|
||
<span>{{ config.prepend }}</span>
|
||
</template>
|
||
<template #append v-if="appendConfig">
|
||
<TMagicButton
|
||
v-if="appendConfig.type === 'button'"
|
||
style="color: #0056ea"
|
||
:size="size"
|
||
@click.prevent="buttonClickHandler"
|
||
>
|
||
{{ appendConfig.text }}
|
||
</TMagicButton>
|
||
<img v-else-if="appendConfig.type === 'icon'" :src="appendConfig.text" @click="buttonClickHandler" />
|
||
<span v-else>{{ appendConfig.text }}</span>
|
||
</template>
|
||
</TMagicInput>
|
||
|
||
<Teleport to="body" v-if="!config.static">
|
||
<div v-if="popoverVisible" class="tmagic-form-text-popper m-form-item__content" ref="popoverEl">
|
||
<div class="m-form-validate__warning">输入内容前后有空格,是否移除空格?</div>
|
||
<div style="display: flex; justify-content: flex-end">
|
||
<TMagicButton link size="small" @click="popoverVisible = false">保持原样</TMagicButton>
|
||
<TMagicButton type="primary" size="small" @click="confirmTrimHandler">移除空格</TMagicButton>
|
||
</div>
|
||
<span class="tmagic-form-text-popper-arrow" data-popper-arrow></span>
|
||
</div>
|
||
</Teleport>
|
||
</div>
|
||
</template>
|
||
|
||
<script lang="ts" setup>
|
||
import { computed, inject, readonly, ref, shallowRef, watch } from 'vue';
|
||
import type { Instance } from '@popperjs/core';
|
||
import { createPopper } from '@popperjs/core';
|
||
import { debounce } from 'lodash-es';
|
||
|
||
import { TMagicButton, TMagicInput } from '@tmagic/design';
|
||
import { isNumber } from '@tmagic/utils';
|
||
|
||
import type { ChangeRecord, ContainerChangeEventData, FieldProps, FormState, TextConfig } from '../schema';
|
||
import { useAddField } from '../utils/useAddField';
|
||
|
||
defineOptions({
|
||
name: 'MFormText',
|
||
});
|
||
|
||
const props = defineProps<FieldProps<TextConfig>>();
|
||
|
||
const emit = defineEmits<{
|
||
change: [value: string, eventData?: ContainerChangeEventData];
|
||
input: [value: string];
|
||
}>();
|
||
|
||
useAddField(props.prop);
|
||
|
||
const mForm = inject<FormState | undefined>('mForm');
|
||
|
||
const value = ref('');
|
||
|
||
watch(
|
||
() => props.model[props.name],
|
||
(v) => {
|
||
value.value = v;
|
||
},
|
||
{
|
||
immediate: true,
|
||
},
|
||
);
|
||
|
||
const appendConfig = computed(() => {
|
||
const { append } = props.config;
|
||
|
||
// 字符串形态:纯文本 append。
|
||
if (typeof append === 'string') {
|
||
return {
|
||
type: 'text',
|
||
text: append,
|
||
handler: undefined,
|
||
};
|
||
}
|
||
|
||
// 对象形态:按 schema 定义,`type` 可为 `'button' | 'icon'`,`handler` 可选。
|
||
//
|
||
// 旧逻辑里只要带 handler 就强制覆盖为 `type: 'button'`,导致 schema 中显式传入的
|
||
// `type: 'icon'` 在到达模板前被抹掉(参见 `@editor/utils/props.ts` 中 ID 字段
|
||
// 的复制按钮配置)。这里直接返回原对象,模板按 `type` 分支渲染 button / icon / text。
|
||
if (typeof append === 'object' && append) {
|
||
// `value === 0` 沿用旧约定:作为「隐藏 append」的开关。
|
||
if (append.value === 0) return false;
|
||
return append;
|
||
}
|
||
|
||
return false;
|
||
});
|
||
|
||
const popoverVisible = ref(false);
|
||
|
||
const confirmTrimHandler = () => {
|
||
emit('change', props.model[props.name].trim() || '');
|
||
popoverVisible.value = false;
|
||
};
|
||
|
||
const checkWhiteSpace = debounce((value: unknown) => {
|
||
if (typeof value === 'string' && !props.config.trim) {
|
||
popoverVisible.value = value.trim() !== value;
|
||
}
|
||
}, 300);
|
||
|
||
const changeHandler = (value: string) => {
|
||
emit('change', value);
|
||
};
|
||
|
||
const inputHandler = (v: string) => {
|
||
checkWhiteSpace(v);
|
||
emit('input', v);
|
||
mForm?.$emit('field-input', props.prop, v);
|
||
};
|
||
|
||
const buttonClickHandler = async () => {
|
||
if (!appendConfig.value) return;
|
||
if (typeof appendConfig.value.handler === 'function') {
|
||
const newChangeRecords: ChangeRecord[] = [];
|
||
const setModel = (key: string, value: any) => {
|
||
newChangeRecords.push({ propPath: props.prop.replace(`${props.name}`, key), value });
|
||
};
|
||
|
||
const setFormValue = (key: string, value: any) => {
|
||
newChangeRecords.push({ propPath: key, value });
|
||
};
|
||
|
||
await appendConfig.value.handler(mForm, {
|
||
model: props.model,
|
||
values: mForm ? readonly(mForm.initValues) : null,
|
||
formValue: props.values || {},
|
||
setModel,
|
||
setFormValue,
|
||
});
|
||
|
||
if (newChangeRecords.length > 0) {
|
||
emit('change', props.model[props.name], {
|
||
changeRecords: newChangeRecords,
|
||
});
|
||
}
|
||
}
|
||
};
|
||
|
||
const keyUpHandler = ($event: KeyboardEvent) => {
|
||
if (!props.model) return;
|
||
if (!props.name) return;
|
||
|
||
const arrowUp = $event.key === 'ArrowUp';
|
||
const arrowDown = $event.key === 'ArrowDown';
|
||
|
||
if (!arrowUp && !arrowDown) {
|
||
return;
|
||
}
|
||
|
||
const value = props.model[props.name];
|
||
let num;
|
||
let unit;
|
||
if (isNumber(value)) {
|
||
num = +value;
|
||
} else {
|
||
value.replace(/^([0-9.]+)([a-z%]+)$/, ($0: string, $1: string, $2: string) => {
|
||
num = +$1;
|
||
unit = $2;
|
||
});
|
||
}
|
||
|
||
if (num === undefined) {
|
||
return;
|
||
}
|
||
|
||
const ctrl = navigator.platform.match('Mac') ? $event.metaKey : $event.ctrlKey;
|
||
const shift = $event.shiftKey;
|
||
const alt = $event.altKey;
|
||
|
||
if (arrowUp) {
|
||
if (ctrl) {
|
||
num += 100;
|
||
} else if (alt) {
|
||
num = (num * 10000 + 1000) / 10000;
|
||
} else if (shift) {
|
||
num = num + 10;
|
||
} else {
|
||
num += 1;
|
||
}
|
||
} else if (arrowDown) {
|
||
if (ctrl) {
|
||
num -= 100;
|
||
} else if (alt) {
|
||
num = (num * 10000 - 1000) / 10000;
|
||
} else if (shift) {
|
||
num -= 10;
|
||
} else {
|
||
num -= 1;
|
||
}
|
||
}
|
||
|
||
props.model[props.name] = `${num}${unit || ''}`;
|
||
emit('change', props.model[props.name]);
|
||
};
|
||
|
||
const popoverEl = ref<HTMLDivElement>();
|
||
const input = ref<InstanceType<typeof TMagicInput>>();
|
||
const instanceRef = shallowRef<Instance | undefined>();
|
||
|
||
watch(popoverEl, (el) => {
|
||
destroyPopover();
|
||
|
||
if (!input.value?.$el || !el) return;
|
||
|
||
instanceRef.value = createPopper(input.value.$el, el, {
|
||
placement: props.config.tooltip ? 'top' : 'bottom',
|
||
strategy: 'absolute',
|
||
modifiers: [
|
||
{
|
||
name: 'offset',
|
||
options: {
|
||
offset: [0, 10],
|
||
},
|
||
},
|
||
],
|
||
});
|
||
});
|
||
|
||
const destroyPopover = () => {
|
||
if (!instanceRef.value) return;
|
||
|
||
instanceRef.value.destroy();
|
||
instanceRef.value = undefined;
|
||
};
|
||
</script>
|