修改:标题输入框右侧没有显示字数限制0/30 。富文本右下角显示限制输入字数,超出不允许输入
3个文件已修改
175 ■■■■■ 已修改文件
src/components/custom/Editor/AiRichTextEditor/index.vue 171 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/userContent/contentCenter/agreementVersion.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/userContent/contentCenter/components/AgreementVersionModal.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/custom/Editor/AiRichTextEditor/index.vue
@@ -1,6 +1,6 @@
<script setup lang="ts">
import type { AiEditor as AiEditorInstance, AiEditorOptions, Uploader } from 'aieditor'
import { nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { AiEditor } from 'aieditor'
import 'aieditor/dist/style.css'
import { uploadSingleFile } from '@/utils/upload'
@@ -58,6 +58,7 @@
  value?: string
  placeholder?: string
  height?: string
  maxLength?: number
  toolbarKeys?: AiEditorOptions['toolbarKeys']
}
@@ -66,9 +67,13 @@
const editorRef = ref<HTMLElement | null>(null)
const editor = ref<AiEditorInstance | null>(null)
const lastHtml = ref('')
const currentTextCount = ref(0)
const isUnmounted = ref(false)
const isApplyingExternalContent = ref(false)
let applyContentVersion = 0
const showCount = computed(() => typeof props.maxLength === 'number' && props.maxLength > 0)
const isCountOverflow = computed(() => showCount.value && currentTextCount.value > (props.maxLength || 0))
const commonUploader: Uploader = async (file) => {
  const res = await uploadSingleFile(file)
@@ -265,12 +270,91 @@
  return preserveConsecutiveSpaces(formatRichTextList(formatTableForMiniProgram(html)))
}
function syncModelValue(html: string) {
  const formattedHtml = formatRichTextContent(html)
  if (isApplyingExternalContent.value) {
    lastHtml.value = formattedHtml
    return
function getTextLengthByHtml(html: string) {
  if (!html)
    return 0
  if (typeof document === 'undefined')
    return html.replace(/<[^>]+>/g, '').replace(/&nbsp;/g, ' ').length
  const container = document.createElement('div')
  container.innerHTML = html
  return (container.textContent || '').length
}
function truncateHtmlByTextLength(html: string, maxLength: number) {
  if (!html || maxLength < 0 || typeof document === 'undefined')
    return html || ''
  const container = document.createElement('div')
  container.innerHTML = html
  let remaining = maxLength
  const trimNode = (node: Node) => {
    if (node.nodeType === Node.TEXT_NODE) {
      const textNode = node as Text
      const text = textNode.nodeValue || ''
      if (!text)
        return false
      if (remaining <= 0)
        return false
      if (text.length <= remaining) {
        remaining -= text.length
        return true
      }
      textNode.nodeValue = text.slice(0, remaining)
      remaining = 0
      return true
    }
    if (node.nodeType !== Node.ELEMENT_NODE)
      return true
    const element = node as Element
    const childNodes = Array.from(element.childNodes)
    if (!childNodes.length)
      return true
    for (const child of childNodes) {
      if (remaining <= 0) {
        child.remove()
        continue
      }
      if (!trimNode(child))
        child.remove()
    }
    return true
  }
  Array.from(container.childNodes).forEach((child) => {
    if (remaining <= 0) {
      child.remove()
      return
    }
    if (!trimNode(child))
      child.remove()
  })
  return container.innerHTML
}
function syncTextCount(html: string) {
  currentTextCount.value = getTextLengthByHtml(html)
}
function getEffectiveMaxLength() {
  return typeof props.maxLength === 'number' && props.maxLength > 0
    ? props.maxLength
    : undefined
}
function applyFormattedValue(formattedHtml: string) {
  syncTextCount(formattedHtml)
  if (formattedHtml === lastHtml.value)
    return
@@ -279,6 +363,16 @@
    model.value = formattedHtml
  emit('input', formattedHtml)
  emit('change', formattedHtml)
}
function syncModelValue(html: string) {
  const formattedHtml = formatRichTextContent(html)
  if (isApplyingExternalContent.value) {
    lastHtml.value = formattedHtml
    syncTextCount(formattedHtml)
    return
  }
  applyFormattedValue(formattedHtml)
}
function destroyEditor() {
@@ -312,7 +406,20 @@
    video: createUploadConfig(),
    attachment: createUploadConfig(),
    onChange: (currentEditor) => {
      syncModelValue(currentEditor.getHtml())
      const currentHtml = currentEditor.getHtml()
      const maxLength = getEffectiveMaxLength()
      const textLength = currentEditor.getText().length
      if (maxLength !== undefined && textLength > maxLength) {
        const truncatedHtml = formatRichTextContent(truncateHtmlByTextLength(currentHtml, maxLength))
        applyFormattedValue(truncatedHtml)
        isApplyingExternalContent.value = true
        currentEditor.setContent(truncatedHtml)
        requestAnimationFrame(() => {
          isApplyingExternalContent.value = false
        })
        return
      }
      syncModelValue(currentHtml)
    },
    onFocus: () => {
      emit('focus')
@@ -327,6 +434,7 @@
  editor.value = instance
  lastHtml.value = initialContent
  syncTextCount(initialContent)
}
async function rebuildEditor(content: string, focus = false) {
@@ -336,6 +444,7 @@
  const formattedContent = formatRichTextContent(content || '')
  if (formattedContent === lastHtml.value && editor.value) {
    lastHtml.value = formattedContent
    syncTextCount(formattedContent)
    return
  }
@@ -367,6 +476,7 @@
  if (!isUnmounted.value && editorRef.value)
    initEditor('')
  lastHtml.value = ''
  syncTextCount('')
  syncModelValue('')
}
@@ -428,6 +538,13 @@
  },
)
watch(
  () => props.maxLength,
  () => {
    syncTextCount(getIncomingContent() || lastHtml.value)
  },
)
onMounted(() => {
  isUnmounted.value = false
  void nextTick(() => {
@@ -457,11 +574,19 @@
      class="ai-editor-host"
      :style="{ minHeight: height }"
    />
    <div
      v-if="showCount"
      class="ai-editor-count"
      :class="{ 'ai-editor-count--overflow': isCountOverflow }"
    >
      {{ currentTextCount }} / {{ props.maxLength }}
    </div>
  </div>
</template>
<style scoped>
.ai-editor-container {
  position: relative;
  width: 100%;
  border-radius: 4px;
  background: #fff;
@@ -483,16 +608,36 @@
  min-height: v-bind(height);
}
.ai-editor-count {
  position: absolute;
  right: 12px;
  bottom: 10px;
  z-index: 2;
  padding: 0 4px;
  font-size: 12px;
  line-height: 16px;
  color: rgba(23, 31, 38, 0.45);
  pointer-events: none;
  background: rgba(255, 255, 255, 0.92);
  border-radius: 2px;
}
.ai-editor-count--overflow {
  color: #d03050;
}
:deep(aie-footer) {
  display: none !important;
}
:deep(.aie-container) {
  display: flex;
  flex-direction: column;
  border-radius: 4px;
  overflow: hidden;
  border: 1px solid rgba(23, 31, 38, 0.15);
  box-sizing: border-box;
  min-height: v-bind(height);
  min-height: 100%;
}
:deep(.aie-container aie-header) {
@@ -513,7 +658,15 @@
}
:deep(.aie-main) {
  min-height: v-bind(height);
  flex: 1;
   min-height: v-bind(height);
  overflow: auto;
}
:deep(.aie-content) {
  min-height: 100%;
  padding-bottom: 28px;
  box-sizing: border-box;
}
:deep(.aie-content ul:not([data-type='taskList'])) {
src/views/userContent/contentCenter/agreementVersion.vue
@@ -608,7 +608,7 @@
        <n-form label-placement="left" require-mark-placement="left">
          <div class="form-row">
            <n-form-item label="协议名称" required class="agreement-name-item">
              <NInput v-model:value="agreementName" placeholder="请输入" maxlength="30" clearable class="w-236px" />
              <NInput v-model:value="agreementName" placeholder="请输入" maxlength="30" show-count clearable class="w-236px" />
            </n-form-item>
          </div>
        </n-form>
src/views/userContent/contentCenter/components/AgreementVersionModal.vue
@@ -113,7 +113,7 @@
          <div class="editor-font-tip">
            推荐字号:大标题(21)小标题(15)正文(14)
          </div>
          <AiRichTextEditor v-model="formModel.content" :read-only="readonly" height="360px" />
          <AiRichTextEditor v-model="formModel.content" :read-only="readonly" height="360px" :maxLength="5000" />
        </div>
      </NFormItem>
    </NForm>