zjk
2026-07-01 01b7aaee963317f2bfeb6bc68094e7eee76f3b0c
Merge branch 'deploy-2.0' into feat/deploy-2.0
2个文件已修改
170 ■■■■■ 已修改文件
src/components/custom/Editor/AiRichTextEditor/index.vue 99 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/appVersion/appVersionManage/index.vue 71 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/custom/Editor/AiRichTextEditor/index.vue
@@ -284,7 +284,11 @@
  const container = document.createElement('div')
  container.innerHTML = html
  return (container.textContent || '').length
  // 必须严格使用 textContent,绝对不能用 innerText。
  // 因为 innerText 会根据 CSS 样式额外计算出换行和空格,导致总长度和底层 TreeWalker 遍历的节点长度不一致!
  // 这种不一致会导致长文本截断算法失效。
  const text = container.textContent || ''
  return text.replace(/[\r\n\u200B-\u200D\uFEFF]/g, '').length
}
function truncateHtmlByTextLength(html: string, maxLength: number) {
@@ -301,17 +305,28 @@
  let currentNode = walker.nextNode()
  while (currentNode) {
    const nodeText = currentNode.nodeValue || ''
    const nodeLength = nodeText.length
    // 计算长度时,剔除不可见字符,避免长度计算偏大导致错误截断
    const actualText = nodeText.replace(/[\r\n\u200B-\u200D\uFEFF]/g, '')
    const nodeLength = actualText.length
    if (currentLength + nodeLength > maxLength) {
      const allowedLength = Math.max(0, maxLength - currentLength)
      if (allowedLength > 0) {
        currentNode.nodeValue = nodeText.substring(0, allowedLength)
        currentLength += allowedLength
    if (currentLength >= maxLength) {
      nodesToRemove.push(currentNode)
    }
    else if (currentLength + nodeLength > maxLength) {
      const allowedLength = maxLength - currentLength
      // 为了确保保留原有的换行格式,我们在原字符串上进行子串截取
      // 这里采用更精确的方式:逐字符遍历原字符串,直到有效字符达到 allowedLength
      let sliceIndex = 0
      let validCharCount = 0
      while (sliceIndex < nodeText.length && validCharCount < allowedLength) {
        const char = nodeText[sliceIndex]
        if (!/[\r\n\u200B-\u200D\uFEFF]/.test(char)) {
          validCharCount++
        }
        sliceIndex++
      }
      else {
        nodesToRemove.push(currentNode)
      }
      currentNode.nodeValue = nodeText.substring(0, sliceIndex)
      currentLength += allowedLength
    }
    else {
      currentLength += nodeLength
@@ -321,14 +336,25 @@
  }
  nodesToRemove.forEach((node) => {
    let target: Node | null = node
    const parent = node.parentNode
    if (!parent)
      return
    parent.removeChild(node)
    let target: Node | null = parent
    while (target && target !== container && !target.textContent?.trim()) {
      const parent: Node | null = target.parentNode
      if (parent)
        parent.removeChild(target)
      target = parent
      const ancestor = target.parentNode
      if (ancestor)
        ancestor.removeChild(target)
      target = ancestor
    }
  })
  // 如果容器内没有文本内容了,直接返回空字符串,避免残留空标签
  if (!container.textContent?.replace(/[\r\n\u200B-\u200D\uFEFF]/g, '').trim()) {
    return ''
  }
  return container.innerHTML
}
@@ -379,6 +405,10 @@
  if (!selection || selection.rangeCount === 0)
    return 0
  // 当从外部复制富文本时,不应该算作编辑器内被选中的文本
  if (editorRef.value && !editorRef.value.contains(selection.anchorNode))
    return 0
  const range = selection.getRangeAt(0)
  const commonAncestor = range.commonAncestorContainer
  if (!editableRoot.value.contains(commonAncestor))
@@ -387,20 +417,17 @@
  return range.toString().length
}
function getRemainingInsertLength() {
  const maxLength = getEffectiveMaxLength()
  if (maxLength === undefined)
    return Number.POSITIVE_INFINITY
  const selectedTextLength = getSelectedTextLength()
  return Math.max(0, maxLength - (currentTextCount.value - selectedTextLength))
}
function shouldBlockInsert(incomingLength: number) {
  if (incomingLength <= 0)
    return false
  return incomingLength > getRemainingInsertLength()
  const maxLength = getEffectiveMaxLength()
  if (maxLength === undefined)
    return false
  const selectedTextLength = getSelectedTextLength()
  const currentLength = getTextLengthByHtml(getEditor()?.getHtml() || '')
  return currentLength - selectedTextLength + incomingLength > maxLength
}
function handleBeforeInput(event: Event) {
@@ -411,7 +438,8 @@
  if (!inputEvent.inputType?.startsWith('insert'))
    return
  if (inputEvent.inputType === 'insertFromPaste')
  // 如果是粘贴操作,交由 onChange 进行柔性截断
  if (inputEvent.inputType === 'insertFromPaste' || inputEvent.inputType === 'insertFromDrop')
    return
  const incomingLength = inputEvent.data?.length ?? 0
@@ -549,17 +577,24 @@
      const currentHtml = currentEditor.getHtml()
      const maxLength = getEffectiveMaxLength()
      const textLength = currentEditor.getText().length
      // 这里必须用纯文本长度判断,否则带格式的文本长度会被低估导致无法截断
      const textLength = getTextLengthByHtml(currentHtml)
      if (maxLength !== undefined && textLength > maxLength) {
        isTruncating.value = true
        // 计算出截断后的合法 HTML
        const truncatedHtml = truncateHtmlByTextLength(currentHtml, maxLength)
        currentEditor.setContent(truncatedHtml)
        currentEditor.focusEnd()
        
        // 【核心修复】必须使用 setTimeout 异步更新内容。
        // 因为 ProseMirror 在处理粘贴事务时,如果同步调用 setContent 替换整个文档,会被底层引擎直接忽略!
        setTimeout(() => {
          isTruncating.value = false
        }, 10)
          isTruncating.value = true
          currentEditor.setContent(truncatedHtml)
          currentEditor.focusEnd()
          setTimeout(() => {
            isTruncating.value = false
          }, 50)
        }, 0)
        hasMaxLengthOverflow.value = true
        syncModelValue(truncatedHtml)
src/views/system/appVersion/appVersionManage/index.vue
@@ -245,9 +245,30 @@
  })
}
let androidAbortController: AbortController | null = null
let iosAbortController: AbortController | null = null
function resetFormAndUploads() {
  if (androidAbortController) {
    androidAbortController.abort()
    androidAbortController = null
  }
  if (iosAbortController) {
    iosAbortController.abort()
    iosAbortController = null
  }
  androidUploading.value = false
  iosUploading.value = false
  resetForm()
}
function handleCancel() {
  showForm.value = false
  resetForm()
  resetFormAndUploads()
}
function handleClose() {
  resetFormAndUploads()
}
function beforeUploadApk(data: { file: UploadFileInfo, fileList: UploadFileInfo[] }) {
@@ -259,6 +280,11 @@
}
async function customRequestAndroid(options: any) {
  if (androidAbortController) {
    androidAbortController.abort()
  }
  androidAbortController = new AbortController()
  androidUploading.value = true
  logAppVersionUpload('android upload triggered', {
    fileName: options?.file?.name,
@@ -272,7 +298,10 @@
  }
  try {
    // 安卓包改为前端直传:先换取上传地址,再直接上传到 OSS
    const res = await customDirectUploadRequest(options as UploadCustomRequestOptions)
    const res = await customDirectUploadRequest({
      ...options,
      signal: androidAbortController.signal,
    } as UploadCustomRequestOptions)
    if (!res) {
      logAppVersionUpload('android upload result empty')
      formModel.value.androidFiles = []
@@ -297,6 +326,11 @@
}
async function customRequestIos(options: any) {
  if (iosAbortController) {
    iosAbortController.abort()
  }
  iosAbortController = new AbortController()
  iosUploading.value = true
  logAppVersionUpload('ios upload triggered', {
    fileName: options?.file?.name,
@@ -310,7 +344,10 @@
  }
  try {
    // iOS 包改为前端直传:上传成功后将 fileId 存到文件项 id 字段
    const res = await customDirectUploadRequest(options as UploadCustomRequestOptions)
    const res = await customDirectUploadRequest({
      ...options,
      signal: iosAbortController.signal,
    } as UploadCustomRequestOptions)
    if (!res) {
      logAppVersionUpload('ios upload result empty')
      formModel.value.iosFiles = []
@@ -392,21 +429,19 @@
      </div>
      <n-modal
        :show="showForm"
        :mask-closable="false"
        preset="card"
        :title="formMode === 'add' ? '新增版本号' : '查看版本号'"
        style="width: 600px"
        :segmented="{
          content: true,
          action: true,
        }"
        :content-style="{
          padding: '16px 24px 0px 24px',
        }"
        @close="handleCancel"
      >
        <n-form
          v-model:show="showForm"
          :mask-closable="false"
          preset="card"
          :title="formMode === 'add' ? '新增版本号' : '查看版本号'"
          style="width: 600px"
          :segmented="{ content: true, action: true }"
          :content-style="{
            padding: '16px 24px 0px 24px',
          }"
          :on-after-leave="handleClose"
          :on-update:show="(val) => { if (!val) handleCancel() }"
        >
          <n-form
          ref="formRef"
          :model="formModel"
          :rules="rules"