perf: 输入框样式调整
This commit is contained in:
@ -1,32 +1,28 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="highlight-textarea-container">
|
<div class="highlight-textarea-container">
|
||||||
<!-- 透明输入层 -->
|
|
||||||
<a-textarea
|
<a-textarea
|
||||||
|
ref="textareaRef"
|
||||||
v-model="inputValue"
|
v-model="inputValue"
|
||||||
placeholder="请输入作品描述"
|
placeholder="请输入作品描述"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
show-word-limit
|
show-word-limit
|
||||||
maxlength="1000"
|
:max-length="1000"
|
||||||
size="large"
|
size="large"
|
||||||
class="textarea-input h-full w-full"
|
class="textarea-input h-full w-full"
|
||||||
@input="handleInput"
|
@input="handleInput"
|
||||||
@scroll="syncScroll"
|
|
||||||
:style="textareaStyle"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 高亮显示层 -->
|
|
||||||
<div
|
<div
|
||||||
|
ref="highlightRef"
|
||||||
class="textarea-highlight"
|
class="textarea-highlight"
|
||||||
:style="{ visibility: inputValue ? 'visible' : 'hidden' }"
|
:style="{ visibility: inputValue ? 'visible' : 'hidden' }"
|
||||||
v-html="highlightedHtml"
|
v-html="highlightedHtml"
|
||||||
@scroll="syncScroll"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, watch, defineProps, defineEmits } from 'vue';
|
import { ref, computed, watch, defineProps, defineEmits, onMounted, onUnmounted } from 'vue';
|
||||||
import { isString } from '@/utils/is';
|
|
||||||
import { escapeRegExp } from './constants';
|
import { escapeRegExp } from './constants';
|
||||||
|
|
||||||
// 定义Props类型
|
// 定义Props类型
|
||||||
@ -43,9 +39,7 @@ const props = defineProps<{
|
|||||||
modelValue?: string;
|
modelValue?: string;
|
||||||
prohibitedWords?: ViolationItem[];
|
prohibitedWords?: ViolationItem[];
|
||||||
levelMap?: Map<number, LevelMapItem>;
|
levelMap?: Map<number, LevelMapItem>;
|
||||||
placeholder?: string;
|
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
maxLength?: number;
|
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@ -53,6 +47,9 @@ const emit = defineEmits<{
|
|||||||
}>();
|
}>();
|
||||||
|
|
||||||
// 内部状态管理
|
// 内部状态管理
|
||||||
|
// 修复highlightRef类型定义
|
||||||
|
const textareaRef = ref<HTMLTextAreaElement | null>(null);
|
||||||
|
const highlightRef = ref<HTMLTextAreaElement | null>(null);
|
||||||
const inputValue = ref(props.modelValue || '');
|
const inputValue = ref(props.modelValue || '');
|
||||||
const scrollTop = ref(0);
|
const scrollTop = ref(0);
|
||||||
const highlightedHtml = computed(() => generateHighlightedHtml());
|
const highlightedHtml = computed(() => generateHighlightedHtml());
|
||||||
@ -62,7 +59,7 @@ watch(
|
|||||||
() => props.modelValue,
|
() => props.modelValue,
|
||||||
(newVal) => {
|
(newVal) => {
|
||||||
if (newVal !== inputValue.value) {
|
if (newVal !== inputValue.value) {
|
||||||
inputValue.value = newVal || '';
|
inputValue.value = (newVal || '').slice(0, 1000);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -114,9 +111,26 @@ const generateHighlightedHtml = (): string => {
|
|||||||
const levelStyle = props.levelMap?.get(wordInfo.risk_level);
|
const levelStyle = props.levelMap?.get(wordInfo.risk_level);
|
||||||
const color = levelStyle?.color || '#F64B31';
|
const color = levelStyle?.color || '#F64B31';
|
||||||
|
|
||||||
return `<span class="s1" style="color: ${color};">${escapeHtml(match)}</span>`;
|
return `<span class="text-14px font-400 lh-22px" style="color: ${color};">${escapeHtml(match)}</span>`;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
document.querySelector('.textarea-input .arco-textarea')?.addEventListener('scroll', handleTextareaScroll);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
document.querySelector('.textarea-input .arco-textarea')?.removeEventListener('scroll', handleTextareaScroll);
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleTextareaScroll = (e: Event) => {
|
||||||
|
const _scrollTop = (e.target as HTMLTextAreaElement).scrollTop;
|
||||||
|
|
||||||
|
const highlightElement = document.querySelector('.textarea-highlight');
|
||||||
|
if (highlightElement) {
|
||||||
|
highlightElement.scrollTop = _scrollTop;
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
@ -124,8 +138,10 @@ const generateHighlightedHtml = (): string => {
|
|||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
.textarea-input,
|
@mixin textarea-padding {
|
||||||
.textarea-highlight {
|
padding: 8px 12px;
|
||||||
|
}
|
||||||
|
@mixin textarea-style {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@ -135,38 +151,46 @@ const generateHighlightedHtml = (): string => {
|
|||||||
border: 1px solid #e5e6eb;
|
border: 1px solid #e5e6eb;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
resize: vertical;
|
resize: vertical;
|
||||||
// font-family: inherit;
|
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
}
|
}
|
||||||
.s1 {
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 22px;
|
|
||||||
font-weight: 400px;
|
|
||||||
}
|
|
||||||
.textarea-input {
|
|
||||||
:deep(.arco-textarea) {
|
|
||||||
overflow: hidden;
|
|
||||||
position: absolute;
|
|
||||||
z-index: 1;
|
|
||||||
width: 100%;
|
|
||||||
padding: 8px 12px;
|
|
||||||
background: transparent;
|
|
||||||
color: transparent;
|
|
||||||
caret-color: #211f24 !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.textarea-highlight {
|
.textarea-highlight {
|
||||||
|
@include textarea-style;
|
||||||
|
padding: 8px 12px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
padding: 8px 12px;
|
@include textarea-padding;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
:deep(.arco-textarea-wrapper) {
|
||||||
|
@include textarea-style;
|
||||||
|
.arco-textarea {
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1;
|
||||||
|
width: 100%;
|
||||||
|
background: transparent;
|
||||||
|
color: transparent;
|
||||||
|
caret-color: #211f24 !important;
|
||||||
|
@include textarea-padding;
|
||||||
|
|
||||||
|
// -webkit-text-fill-color: transparent !important;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
.arco-textarea-word-limit {
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
&.arco-textarea-disabled {
|
||||||
|
.arco-textarea {
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* 字数统计样式 */
|
/* 字数统计样式 */
|
||||||
.word-count {
|
.word-count {
|
||||||
|
|||||||
@ -1,32 +1,28 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="highlight-textarea-container">
|
<div class="highlight-textarea-container">
|
||||||
<!-- 透明输入层 -->
|
|
||||||
<a-textarea
|
<a-textarea
|
||||||
|
ref="textareaRef"
|
||||||
v-model="inputValue"
|
v-model="inputValue"
|
||||||
placeholder="请输入作品描述"
|
placeholder="请输入作品描述"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
show-word-limit
|
show-word-limit
|
||||||
maxlength="1000"
|
:max-length="1000"
|
||||||
size="large"
|
size="large"
|
||||||
class="textarea-input h-full w-full"
|
class="textarea-input h-full w-full"
|
||||||
@input="handleInput"
|
@input="handleInput"
|
||||||
@scroll="syncScroll"
|
|
||||||
:style="textareaStyle"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 高亮显示层 -->
|
|
||||||
<div
|
<div
|
||||||
|
ref="highlightRef"
|
||||||
class="textarea-highlight"
|
class="textarea-highlight"
|
||||||
:style="{ visibility: inputValue ? 'visible' : 'hidden' }"
|
:style="{ visibility: inputValue ? 'visible' : 'hidden' }"
|
||||||
v-html="highlightedHtml"
|
v-html="highlightedHtml"
|
||||||
@scroll="syncScroll"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, watch, defineProps, defineEmits } from 'vue';
|
import { ref, computed, watch, defineProps, defineEmits, onMounted, onUnmounted } from 'vue';
|
||||||
import { isString } from '@/utils/is';
|
|
||||||
import { escapeRegExp } from './constants';
|
import { escapeRegExp } from './constants';
|
||||||
|
|
||||||
// 定义Props类型
|
// 定义Props类型
|
||||||
@ -43,9 +39,7 @@ const props = defineProps<{
|
|||||||
modelValue?: string;
|
modelValue?: string;
|
||||||
prohibitedWords?: ViolationItem[];
|
prohibitedWords?: ViolationItem[];
|
||||||
levelMap?: Map<number, LevelMapItem>;
|
levelMap?: Map<number, LevelMapItem>;
|
||||||
placeholder?: string;
|
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
maxLength?: number;
|
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@ -53,6 +47,9 @@ const emit = defineEmits<{
|
|||||||
}>();
|
}>();
|
||||||
|
|
||||||
// 内部状态管理
|
// 内部状态管理
|
||||||
|
// 修复highlightRef类型定义
|
||||||
|
const textareaRef = ref<HTMLTextAreaElement | null>(null);
|
||||||
|
const highlightRef = ref<HTMLTextAreaElement | null>(null);
|
||||||
const inputValue = ref(props.modelValue || '');
|
const inputValue = ref(props.modelValue || '');
|
||||||
const scrollTop = ref(0);
|
const scrollTop = ref(0);
|
||||||
const highlightedHtml = computed(() => generateHighlightedHtml());
|
const highlightedHtml = computed(() => generateHighlightedHtml());
|
||||||
@ -62,7 +59,7 @@ watch(
|
|||||||
() => props.modelValue,
|
() => props.modelValue,
|
||||||
(newVal) => {
|
(newVal) => {
|
||||||
if (newVal !== inputValue.value) {
|
if (newVal !== inputValue.value) {
|
||||||
inputValue.value = newVal || '';
|
inputValue.value = (newVal || '').slice(0, 1000);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -114,9 +111,26 @@ const generateHighlightedHtml = (): string => {
|
|||||||
const levelStyle = props.levelMap?.get(wordInfo.risk_level);
|
const levelStyle = props.levelMap?.get(wordInfo.risk_level);
|
||||||
const color = levelStyle?.color || '#F64B31';
|
const color = levelStyle?.color || '#F64B31';
|
||||||
|
|
||||||
return `<span class="s1" style="color: ${color};">${escapeHtml(match)}</span>`;
|
return `<span class="text-14px font-400 lh-22px" style="color: ${color};">${escapeHtml(match)}</span>`;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
document.querySelector('.textarea-input .arco-textarea')?.addEventListener('scroll', handleTextareaScroll);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
document.querySelector('.textarea-input .arco-textarea')?.removeEventListener('scroll', handleTextareaScroll);
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleTextareaScroll = (e: Event) => {
|
||||||
|
const _scrollTop = (e.target as HTMLTextAreaElement).scrollTop;
|
||||||
|
|
||||||
|
const highlightElement = document.querySelector('.textarea-highlight');
|
||||||
|
if (highlightElement) {
|
||||||
|
highlightElement.scrollTop = _scrollTop;
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
@ -124,8 +138,10 @@ const generateHighlightedHtml = (): string => {
|
|||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
.textarea-input,
|
@mixin textarea-padding {
|
||||||
.textarea-highlight {
|
padding: 8px 12px;
|
||||||
|
}
|
||||||
|
@mixin textarea-style {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@ -135,37 +151,44 @@ const generateHighlightedHtml = (): string => {
|
|||||||
border: 1px solid #e5e6eb;
|
border: 1px solid #e5e6eb;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
resize: vertical;
|
resize: vertical;
|
||||||
// font-family: inherit;
|
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
}
|
}
|
||||||
.s1 {
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 22px;
|
|
||||||
font-weight: 400px;
|
|
||||||
}
|
|
||||||
.textarea-input {
|
|
||||||
:deep(.arco-textarea) {
|
|
||||||
overflow: hidden;
|
|
||||||
position: absolute;
|
|
||||||
z-index: 1;
|
|
||||||
width: 100%;
|
|
||||||
padding: 8px 12px;
|
|
||||||
background: transparent;
|
|
||||||
color: transparent;
|
|
||||||
caret-color: #211f24 !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.textarea-highlight {
|
.textarea-highlight {
|
||||||
|
@include textarea-style;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
padding: 8px 12px;
|
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
@include textarea-padding;
|
||||||
|
}
|
||||||
|
:deep(.arco-textarea-wrapper) {
|
||||||
|
@include textarea-style;
|
||||||
|
.arco-textarea {
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1;
|
||||||
|
width: 100%;
|
||||||
|
background: transparent;
|
||||||
|
color: transparent;
|
||||||
|
caret-color: #211f24 !important;
|
||||||
|
@include textarea-padding;
|
||||||
|
|
||||||
|
// -webkit-text-fill-color: transparent !important;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
.arco-textarea-word-limit {
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
&.arco-textarea-disabled {
|
||||||
|
.arco-textarea {
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 字数统计样式 */
|
/* 字数统计样式 */
|
||||||
|
|||||||
Reference in New Issue
Block a user