Merge remote-tracking branch 'origin/feature/v1.2灵机空间-内容上传审核_rxd' into test

# Conflicts:
#	src/views/creative-generation-workshop/manuscript/check/components/content-card/highlight-textarea.vue
This commit is contained in:
rd
2025-08-13 14:47:54 +08:00
2 changed files with 115 additions and 67 deletions

View File

@ -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 {

View File

@ -1,21 +1,19 @@
<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"
@ -24,8 +22,7 @@
</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类型
@ -42,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<{
@ -52,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());
@ -61,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);
} }
}, },
); );
@ -113,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">
@ -123,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%;
@ -134,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;
}
}
} }
/* 字数统计样式 */ /* 字数统计样式 */