2025-07-21 16:47:04 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<view
|
|
|
|
|
|
class="cl-textarea"
|
|
|
|
|
|
:class="[
|
|
|
|
|
|
pt.className,
|
|
|
|
|
|
{
|
|
|
|
|
|
'is-dark': isDark,
|
|
|
|
|
|
'cl-textarea--border': border,
|
|
|
|
|
|
'cl-textarea--focus': isFocus,
|
2025-08-11 16:25:25 +08:00
|
|
|
|
'cl-textarea--disabled': isDisabled,
|
|
|
|
|
|
'cl-textarea--error': isError
|
2025-07-21 16:47:04 +08:00
|
|
|
|
}
|
|
|
|
|
|
]"
|
|
|
|
|
|
@tap="onTap"
|
|
|
|
|
|
>
|
|
|
|
|
|
<textarea
|
|
|
|
|
|
class="cl-textarea__inner"
|
|
|
|
|
|
:class="[
|
|
|
|
|
|
{
|
2025-08-06 10:18:17 +08:00
|
|
|
|
'is-disabled': isDisabled,
|
2025-07-21 16:47:04 +08:00
|
|
|
|
'is-dark': isDark
|
|
|
|
|
|
},
|
|
|
|
|
|
pt.inner?.className
|
|
|
|
|
|
]"
|
|
|
|
|
|
:style="{
|
|
|
|
|
|
height: parseRpx(height)
|
|
|
|
|
|
}"
|
|
|
|
|
|
:value="value"
|
|
|
|
|
|
:name="name"
|
2025-08-06 10:18:17 +08:00
|
|
|
|
:disabled="readonly ?? isDisabled"
|
2025-07-21 16:47:04 +08:00
|
|
|
|
:placeholder="placeholder"
|
|
|
|
|
|
:placeholder-class="`text-surface-400 ${placeholderClass}`"
|
|
|
|
|
|
:maxlength="maxlength"
|
|
|
|
|
|
:auto-focus="isFocus"
|
|
|
|
|
|
:cursor="cursor"
|
|
|
|
|
|
:cursor-spacing="cursorSpacing"
|
|
|
|
|
|
:cursor-color="cursorColor"
|
|
|
|
|
|
:show-confirm-bar="showConfirmBar"
|
|
|
|
|
|
:confirm-hold="confirmHold"
|
|
|
|
|
|
:auto-height="autoHeight"
|
|
|
|
|
|
:fixed="fixed"
|
|
|
|
|
|
:adjust-position="adjustPosition"
|
|
|
|
|
|
:hold-keyboard="holdKeyboard"
|
|
|
|
|
|
:inputmode="inputmode"
|
|
|
|
|
|
:disable-default-padding="disableDefaultPadding"
|
|
|
|
|
|
:adjust-keyboard-to="adjustKeyboardTo"
|
|
|
|
|
|
@confirm="onConfirm"
|
|
|
|
|
|
@input="onInput"
|
|
|
|
|
|
@linechange="onLineChange"
|
|
|
|
|
|
@blur="onBlur"
|
|
|
|
|
|
@keyboardheightchange="onKeyboardheightchange"
|
|
|
|
|
|
@focus="onFocus"
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
<text class="cl-textarea__count" v-if="showWordLimit"
|
|
|
|
|
|
>{{ value.length }}/{{ maxlength }}</text
|
|
|
|
|
|
>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
|
import { computed, nextTick, ref, watch, type PropType } from "vue";
|
|
|
|
|
|
import { parsePt, parseRpx } from "@/cool";
|
|
|
|
|
|
import type { PassThroughProps } from "../../types";
|
|
|
|
|
|
import { isDark } from "@/cool";
|
|
|
|
|
|
import { t } from "@/locale";
|
2025-08-11 16:25:25 +08:00
|
|
|
|
import { useForm, useFormItem } from "../../hooks";
|
2025-07-21 16:47:04 +08:00
|
|
|
|
|
|
|
|
|
|
defineOptions({
|
|
|
|
|
|
name: "cl-textarea"
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 组件属性定义
|
|
|
|
|
|
const props = defineProps({
|
|
|
|
|
|
// 透传样式
|
|
|
|
|
|
pt: {
|
|
|
|
|
|
type: Object,
|
|
|
|
|
|
default: () => ({})
|
|
|
|
|
|
},
|
|
|
|
|
|
// 绑定值
|
|
|
|
|
|
modelValue: {
|
|
|
|
|
|
type: String,
|
|
|
|
|
|
default: ""
|
|
|
|
|
|
},
|
|
|
|
|
|
// 是否显示边框
|
|
|
|
|
|
border: {
|
|
|
|
|
|
type: Boolean,
|
|
|
|
|
|
default: true
|
|
|
|
|
|
},
|
|
|
|
|
|
// 是否禁用
|
|
|
|
|
|
disabled: {
|
|
|
|
|
|
type: Boolean,
|
|
|
|
|
|
default: false
|
|
|
|
|
|
},
|
|
|
|
|
|
// 是否只读
|
|
|
|
|
|
readonly: {
|
|
|
|
|
|
type: Boolean,
|
|
|
|
|
|
default: null
|
|
|
|
|
|
},
|
|
|
|
|
|
// 是否显示字数统计
|
|
|
|
|
|
showWordLimit: {
|
|
|
|
|
|
type: Boolean,
|
|
|
|
|
|
default: true
|
|
|
|
|
|
},
|
|
|
|
|
|
// 名称
|
|
|
|
|
|
name: {
|
|
|
|
|
|
type: String,
|
|
|
|
|
|
default: ""
|
|
|
|
|
|
},
|
|
|
|
|
|
// 占位符
|
|
|
|
|
|
placeholder: {
|
|
|
|
|
|
type: String,
|
2025-08-06 18:14:11 +08:00
|
|
|
|
default: () => t("请输入")
|
2025-07-21 16:47:04 +08:00
|
|
|
|
},
|
|
|
|
|
|
// 占位符样式类
|
|
|
|
|
|
placeholderClass: {
|
|
|
|
|
|
type: String,
|
|
|
|
|
|
default: ""
|
|
|
|
|
|
},
|
|
|
|
|
|
// 最大输入长度
|
|
|
|
|
|
maxlength: {
|
|
|
|
|
|
type: Number,
|
|
|
|
|
|
default: 140
|
|
|
|
|
|
},
|
|
|
|
|
|
// 是否自动聚焦
|
|
|
|
|
|
autofocus: {
|
|
|
|
|
|
type: Boolean,
|
|
|
|
|
|
default: false
|
|
|
|
|
|
},
|
|
|
|
|
|
// 设置键盘右下角按钮的文字
|
|
|
|
|
|
confirmType: {
|
|
|
|
|
|
type: String as PropType<"done" | "go" | "next" | "search" | "send">,
|
|
|
|
|
|
default: "done"
|
|
|
|
|
|
},
|
|
|
|
|
|
// 指定focus时的光标位置
|
|
|
|
|
|
cursor: {
|
|
|
|
|
|
type: Number,
|
|
|
|
|
|
default: 0
|
|
|
|
|
|
},
|
|
|
|
|
|
// 点击键盘确认按钮时是否保持键盘不收起
|
|
|
|
|
|
confirmHold: {
|
|
|
|
|
|
type: Boolean,
|
|
|
|
|
|
default: false
|
|
|
|
|
|
},
|
|
|
|
|
|
// 高度
|
|
|
|
|
|
height: {
|
|
|
|
|
|
type: [Number, String],
|
|
|
|
|
|
default: 120
|
|
|
|
|
|
},
|
|
|
|
|
|
// 是否自动增高
|
|
|
|
|
|
autoHeight: {
|
|
|
|
|
|
type: Boolean,
|
|
|
|
|
|
default: false
|
|
|
|
|
|
},
|
|
|
|
|
|
// 如果 textarea 是在一个 position:fixed 的区域,需要显示指定属性 fixed 为 true
|
|
|
|
|
|
fixed: {
|
|
|
|
|
|
type: Boolean,
|
|
|
|
|
|
default: false
|
|
|
|
|
|
},
|
|
|
|
|
|
// 光标与键盘的距离
|
|
|
|
|
|
cursorSpacing: {
|
|
|
|
|
|
type: Number,
|
|
|
|
|
|
default: 5
|
|
|
|
|
|
},
|
|
|
|
|
|
// 指定光标颜色
|
|
|
|
|
|
cursorColor: {
|
|
|
|
|
|
type: String,
|
|
|
|
|
|
default: ""
|
|
|
|
|
|
},
|
|
|
|
|
|
// 是否显示键盘上方带有”完成“按钮那一栏
|
|
|
|
|
|
showConfirmBar: {
|
|
|
|
|
|
type: Boolean,
|
|
|
|
|
|
default: true
|
|
|
|
|
|
},
|
|
|
|
|
|
// 光标起始位置
|
|
|
|
|
|
selectionStart: {
|
|
|
|
|
|
type: Number,
|
|
|
|
|
|
default: -1
|
|
|
|
|
|
},
|
|
|
|
|
|
// 光标结束位置
|
|
|
|
|
|
selectionEnd: {
|
|
|
|
|
|
type: Number,
|
|
|
|
|
|
default: -1
|
|
|
|
|
|
},
|
|
|
|
|
|
// 盘弹起时,是否自动上推页面
|
|
|
|
|
|
adjustPosition: {
|
|
|
|
|
|
type: Boolean,
|
|
|
|
|
|
default: true
|
|
|
|
|
|
},
|
|
|
|
|
|
// 它提供了用户在编辑元素或其内容时可能输入的数据类型的提示。
|
|
|
|
|
|
inputmode: {
|
|
|
|
|
|
type: String as PropType<
|
|
|
|
|
|
"none" | "text" | "decimal" | "numeric" | "tel" | "search" | "email" | "url"
|
|
|
|
|
|
>,
|
|
|
|
|
|
default: "text"
|
|
|
|
|
|
},
|
|
|
|
|
|
// focus时,点击页面的时候不收起键盘
|
|
|
|
|
|
holdKeyboard: {
|
|
|
|
|
|
type: Boolean,
|
|
|
|
|
|
default: false
|
|
|
|
|
|
},
|
|
|
|
|
|
// 是否禁用默认内边距
|
|
|
|
|
|
disableDefaultPadding: {
|
|
|
|
|
|
type: Boolean,
|
|
|
|
|
|
default: true
|
|
|
|
|
|
},
|
|
|
|
|
|
// 键盘对齐位置
|
|
|
|
|
|
adjustKeyboardTo: {
|
|
|
|
|
|
type: String as PropType<"cursor" | "bottom">,
|
|
|
|
|
|
default: "cursor"
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 事件定义
|
|
|
|
|
|
const emit = defineEmits([
|
|
|
|
|
|
"update:modelValue",
|
|
|
|
|
|
"input",
|
|
|
|
|
|
"change",
|
|
|
|
|
|
"focus",
|
|
|
|
|
|
"blur",
|
|
|
|
|
|
"confirm",
|
|
|
|
|
|
"linechange",
|
|
|
|
|
|
"keyboardheightchange"
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
2025-08-06 10:18:17 +08:00
|
|
|
|
// cl-form 上下文
|
|
|
|
|
|
const { disabled } = useForm();
|
|
|
|
|
|
|
2025-08-11 16:25:25 +08:00
|
|
|
|
// cl-form-item 上下文
|
|
|
|
|
|
const { isError } = useFormItem();
|
|
|
|
|
|
|
2025-08-06 10:18:17 +08:00
|
|
|
|
// 是否禁用
|
|
|
|
|
|
const isDisabled = computed(() => {
|
|
|
|
|
|
return disabled.value || props.disabled;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-07-21 16:47:04 +08:00
|
|
|
|
// 透传样式类型定义
|
|
|
|
|
|
type PassThrough = {
|
|
|
|
|
|
className?: string;
|
|
|
|
|
|
inner?: PassThroughProps;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 解析透传样式
|
|
|
|
|
|
const pt = computed(() => parsePt<PassThrough>(props.pt));
|
|
|
|
|
|
|
|
|
|
|
|
// 绑定值
|
|
|
|
|
|
const value = ref(props.modelValue);
|
|
|
|
|
|
|
|
|
|
|
|
// 是否聚焦
|
|
|
|
|
|
const isFocus = ref<boolean>(props.autofocus);
|
|
|
|
|
|
|
|
|
|
|
|
// 获取焦点事件
|
|
|
|
|
|
function onFocus() {
|
|
|
|
|
|
isFocus.value = true;
|
|
|
|
|
|
emit("focus");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 失去焦点事件
|
|
|
|
|
|
function onBlur() {
|
|
|
|
|
|
emit("blur");
|
|
|
|
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
isFocus.value = false;
|
|
|
|
|
|
}, 0);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 输入事件
|
|
|
|
|
|
function onInput(e: UniInputEvent) {
|
|
|
|
|
|
const val = e.detail.value;
|
|
|
|
|
|
value.value = val;
|
|
|
|
|
|
|
|
|
|
|
|
emit("update:modelValue", val);
|
|
|
|
|
|
emit("change", val);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 点击确认按钮事件
|
|
|
|
|
|
function onConfirm(e: UniInputConfirmEvent) {
|
|
|
|
|
|
emit("confirm", e);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 键盘高度变化事件
|
|
|
|
|
|
function onKeyboardheightchange(e: UniInputKeyboardHeightChangeEvent) {
|
|
|
|
|
|
emit("keyboardheightchange", e);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 行数变化事件
|
|
|
|
|
|
function onLineChange(e: UniTextareaLineChangeEvent) {
|
|
|
|
|
|
emit("linechange", e);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 点击事件
|
|
|
|
|
|
function onTap() {
|
2025-08-06 10:18:17 +08:00
|
|
|
|
if (isDisabled.value) {
|
2025-07-21 16:47:04 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
isFocus.value = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 聚焦方法
|
|
|
|
|
|
function focus() {
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
isFocus.value = false;
|
|
|
|
|
|
|
|
|
|
|
|
nextTick(() => {
|
|
|
|
|
|
isFocus.value = true;
|
|
|
|
|
|
});
|
|
|
|
|
|
}, 0);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
watch(
|
|
|
|
|
|
computed(() => props.modelValue),
|
|
|
|
|
|
(val: string) => {
|
|
|
|
|
|
value.value = val;
|
|
|
|
|
|
}
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
defineExpose({
|
|
|
|
|
|
isFocus,
|
|
|
|
|
|
focus
|
|
|
|
|
|
});
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
|
|
|
|
.cl-textarea {
|
|
|
|
|
|
@apply flex flex-row items-center bg-white;
|
|
|
|
|
|
@apply rounded-lg;
|
|
|
|
|
|
padding: 16rpx 20rpx;
|
|
|
|
|
|
transition-property: border-color, background-color;
|
|
|
|
|
|
transition-duration: 0.2s;
|
|
|
|
|
|
|
|
|
|
|
|
:deep(.uni-textarea-compute) {
|
|
|
|
|
|
opacity: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&__inner {
|
|
|
|
|
|
@apply h-full text-surface-700;
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
font-size: 28rpx;
|
|
|
|
|
|
|
|
|
|
|
|
&.is-dark {
|
|
|
|
|
|
@apply text-white;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&__icon {
|
|
|
|
|
|
@apply flex items-center justify-center h-full;
|
|
|
|
|
|
padding-left: 20rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&__count {
|
|
|
|
|
|
@apply text-surface-400 text-sm absolute right-2 bottom-2;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&--border {
|
|
|
|
|
|
@apply border border-solid border-surface-200;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&--focus {
|
|
|
|
|
|
@apply border-primary-500;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&--disabled {
|
|
|
|
|
|
@apply bg-surface-100 opacity-70;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-11 16:25:25 +08:00
|
|
|
|
&--error {
|
|
|
|
|
|
@apply border-red-500;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-21 16:47:04 +08:00
|
|
|
|
&.is-dark {
|
|
|
|
|
|
@apply bg-surface-800;
|
|
|
|
|
|
|
|
|
|
|
|
&.cl-textarea--border {
|
|
|
|
|
|
@apply border-surface-700;
|
|
|
|
|
|
|
|
|
|
|
|
&.cl-textarea--focus {
|
|
|
|
|
|
@apply border-primary-500;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&.cl-textarea--disabled {
|
|
|
|
|
|
@apply bg-surface-700;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|