吴羡
5 天以前 14f31e66bae9fac4217bd5a18ed775952af57e94
Merge remote-tracking branch 'origin/master'
已添加1个文件
已修改6个文件
330 ■■■■■ 文件已修改
src/components/ReportWordEditor/index.vue 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/inspectionValueFormat.js 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/business/inspectionTask/components/InspectionWord.vue 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/business/inspectionTask/inspection.vue 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/business/inspectionView/index.vue 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/workers/InspectionWorker.worker.js 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tests/superdoc-script.smoke.mjs 153 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/ReportWordEditor/index.vue
@@ -6,7 +6,15 @@
        {{ label }} <input v-model.number="margins[side]" type="number" min="0" max="80" step="0.1"
                           :aria-label="`${label}页边距(毫米)`" :disabled="readonly || !ready || applyingMargins" />
      </label>
      <el-button size="small" :loading="applyingMargins" :disabled="readonly || !ready" @click="applyMargins">应用页边距</el-button>
      <el-button size="small" :loading="applyingMargins" :disabled="readonly || !ready || applyingScript" @click="applyMargins">应用页边距</el-button>
      <div class="script-settings" role="group" aria-label="上下标设置">
        <span>选中文字后设置:</span>
        <el-button v-for="option in scriptOptions" :key="option.value" size="small"
                   :disabled="readonly || !ready || applyingMargins || applyingScript"
                   :aria-label="option.label" :title="option.tip"
                   @pointerdown.prevent="rememberScriptSelection" @mousedown.prevent
                   @click="applyScript(option.value)">{{ option.label }}</el-button>
      </div>
    </div>
    <div ref="toolbar" class="word-toolbar" :inert="readonly || undefined"></div>
    <div class="word-canvas" :inert="readonly || undefined">
@@ -36,6 +44,12 @@
const editor = shallowRef(null)
const ready = ref(false)
const applyingMargins = ref(false)
const applyingScript = ref(false)
const scriptOptions = [
  { value: 'superscript', label: '上标 x²', tip: '将选中文字设为上标,例如 10⁻³ ä¸­çš„ -3' },
  { value: 'subscript', label: '下标 x₂', tip: '将选中文字设为下标,例如 H₂O ä¸­çš„ 2' },
  { value: 'baseline', label: '恢复正常', tip: '取消选中文字的上标或下标' }
]
const error = ref('')
const margins = ref({ top: 25.4, bottom: 25.4, left: 25.4, right: 25.4 })
const marginLabels = { top: '上', bottom: '下', left: 'å·¦', right: '右' }
@@ -44,6 +58,12 @@
let toolbarObserver
let fontSelectionTarget = false
let fontSelectionKnown = false
let scriptSelection = null
function rememberScriptSelection() {
  // åœ¨æŒ‰é’®ç¦ç”¨æˆ–焦点变化之前向 Worker è¯»å–选区,避免点击后选区被清空。
  scriptSelection = editor.value?.activeEditor.doc.selection.current().catch(() => null)
}
function rememberFontSelection(event) {
  if (!event.target?.closest?.('[data-item="btn-fontFamily"]')) return
@@ -94,6 +114,7 @@
async function openDocument() {
  const current = ++generation
  scriptSelection = null
  clearTimeout(readyTimer)
  editor.value?.destroy()
  editor.value = null
@@ -150,7 +171,7 @@
}
async function applyMargins() {
  if (!ready.value || props.readonly || applyingMargins.value) return
  if (!ready.value || props.readonly || applyingMargins.value || applyingScript.value) return
  applyingMargins.value = true
  try {
    const sections = editor.value.activeEditor.doc.sections
@@ -176,9 +197,41 @@
  finally { applyingMargins.value = false }
}
async function applyScript(vertAlign) {
  if (!ready.value || props.readonly || applyingMargins.value || applyingScript.value) return
  const current = generation
  const superdoc = editor.value
  const selectionPromise = scriptSelection || superdoc.activeEditor.doc.selection.current()
  scriptSelection = null
  applyingScript.value = true
  try {
    // è¯»å–实际选区,避免 UI å¿«ç…§å°šæœªæ›´æ–°æ—¶ä¿®æ”¹åˆ°ä¹‹å‰é€‰ä¸­çš„æ–‡å­—。
    const selection = await selectionPromise
    if (current !== generation || props.readonly) return
    if (!selection || selection.empty || !selection.selectionTarget) {
      ElMessage.info('请先选中需要设置上下标的文字,例如 10-3 ä¸­çš„ -3')
      return
    }
    const receipt = await superdoc.activeEditor.doc.format.apply({
      target: selection.selectionTarget,
      // æ¸…除原有手动升降位置,让 Word æŒ‰ä¸Šä¸‹æ ‡è§„则排版。
      inline: { vertAlign, position: null }
    })
    if (current !== generation) return
    if (!receipt.success && receipt.failure?.code !== 'NO_OP') {
      throw new Error(receipt.failure?.message || '设置上下标失败')
    }
    emit('change')
    ElMessage.success('文字格式已设置,点击保存后生效')
  } catch (cause) {
    if (current === generation) ElMessage.error(cause.message || '设置上下标失败')
  } finally { applyingScript.value = false }
}
async function exportDocx() {
  if (!ready.value || error.value || !editor.value) throw new Error('Word å°šæœªåŠ è½½å®Œæˆï¼Œä¸èƒ½ä¿å­˜')
  if (applyingMargins.value) throw new Error('页边距正在应用,请稍候再保存')
  if (applyingScript.value) throw new Error('上下标正在设置,请稍候再保存')
  const blob = await editor.value.export({ exportType: ['docx'], triggerDownload: false })
  if (!(blob instanceof Blob) || !blob.size) throw new Error('Word å¯¼å‡ºå¤±è´¥')
  return blob
@@ -213,6 +266,8 @@
.page-settings { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; padding: 8px; background: #fff; }
.page-settings label { display: flex; align-items: center; gap: 4px; }
.page-settings input { width: 70px; padding: 5px; border: 1px solid #dcdfe6; border-radius: 4px; }
.script-settings { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
.script-settings :deep(.el-button + .el-button) { margin-left: 0; }
.word-toolbar { flex-shrink: 0; }
.word-canvas { flex: 1; min-height: 0; overflow: auto; background: #f3f4f6; }
/* SuperDoc æ»šåŠ¨æ—¶ä¼šæŠŠæ–‡æ¡£å®¹å™¨ç¼©åˆ°çº¸å¼ å®½åº¦ï¼Œå¯¼è‡´æ•´é¡µé‡æ–°è´´åˆ°å·¦è¾¹ã€‚ */
src/utils/inspectionValueFormat.js
@@ -74,3 +74,9 @@
  const config = parseInspectionValueAnnotation(annotation);
  return config && config.error ? config.error : null;
}
/** ç»Ÿä¸€è¯†åˆ«å¸¦å°æ•°ä½ç­‰æ ¼å¼å‚数的数值批注。 */
export function isInspectionValueAnnotation(annotation, target) {
  const config = parseInspectionValueAnnotation(annotation);
  return !!config && !config.error && (!target || config.target === target);
}
src/views/business/inspectionTask/components/InspectionWord.vue
@@ -111,9 +111,9 @@
                        </el-option>
                      </el-select>
                    </template>
                    <template v-else-if="n.v.ps != undefined && n.v.ps.value === '计算值' && state == 1"><span
                    <template v-else-if="n.v.ps != undefined && isInspectionValueAnnotation(n.v.ps.value, '计算值') && state == 1"><span
                        :style="`font-family:${n.v.ff} !important;`">{{ n.v.v }}</span></template>
                    <template v-else-if="n.v.ps != undefined && n.v.ps.value === '最终值' && state == 1">
                    <template v-else-if="n.v.ps != undefined && isInspectionValueAnnotation(n.v.ps.value, '最终值') && state == 1">
                      <span :style="`font-family:${n.v.ff} !important;`">{{ n.v.v }}</span>
                    </template>
                    <template v-else-if="n.v.ps != undefined && n.v.ps.value === '样品编号'">
@@ -251,6 +251,7 @@
</template>
<script>
import { parseInspectionValueAnnotation, formatInspectionValue, isInspectionValueAnnotation } from "@/utils/inspectionValueFormat";
// Vue3 æ²¡æœ‰ this.$createElement(下面用 h é€  VNode å½“ $msgbox çš„ message)
import { h } from 'vue'
import { readDetectionTimeRange, isCompleteDetectionTimeRange, toDetectionTimePayload } from '@/utils/detectionTimeRange'
@@ -599,6 +600,7 @@
    this.stopWorker();
  },
  methods: {
    isInspectionValueAnnotation,
    closeAddVerifyDia() {
      this.addCheck = false
      this.checkUser = ''
@@ -1089,8 +1091,8 @@
            b.v.ps != undefined &&
            typeof b.v.ps.value === 'string' &&
            (b.v.ps.value.includes('检验值') ||
              b.v.ps.value === '计算值' ||
              b.v.ps.value === '最终值' ||
              isInspectionValueAnnotation(b.v.ps.value, '计算值') ||
              isInspectionValueAnnotation(b.v.ps.value, '最终值') ||
              b.v.ps.value === '设备名称')
          ) {
            b.v['v'] = ''
@@ -1151,12 +1153,12 @@
          if (value === undefined || value === null) {
            return
          }
          cell.v.v = value
          cell.v.v = formatInspectionValue(value, parseInspectionValueAnnotation(cell.v.ps && cell.v.ps.value))
          if (typeof item === 'object' && item.u) {
            cell.u = item.u
          }
          this.param[a.templateId][key] = {
            v: value,
            v: cell.v.v,
            u: typeof item === 'object' ? item.u : cell.u
          }
        })
@@ -1553,8 +1555,8 @@
            cell.v.ps != undefined &&
            typeof cell.v.ps.value === 'string' &&
            (cell.v.ps.value.includes('检验值') ||
              cell.v.ps.value === '计算值' ||
              cell.v.ps.value === '最终值' ||
              isInspectionValueAnnotation(cell.v.ps.value, '计算值') ||
              isInspectionValueAnnotation(cell.v.ps.value, '最终值') ||
              cell.v.ps.value === '设备名称')
          if (!isEditable) {
            return
@@ -1810,6 +1812,23 @@
  flex: 0 0 360px;
}
/* å¤æ ¸æ—¶é€‰æ‹©æ¡†ç¦ç”¨ï¼Œç›´æŽ¥å±•开完整设备名称和编号。 */
.template-panel:deep(.device-select .el-tag) {
  height: auto;
  min-height: 24px;
  padding-top: 2px;
  padding-bottom: 2px;
  max-width: 100%;
}
.template-panel:deep(.device-select .el-select__tags-text) {
  white-space: normal;
  overflow-wrap: anywhere;
  overflow: visible;
  text-overflow: clip;
  line-height: 1.5;
}
.template-panel__unit {
  margin-left: 4px;
  color: var(--lims-text-2);
src/views/business/inspectionTask/inspection.vue
@@ -280,14 +280,14 @@
                    </template>
                    <template v-else-if="
                      n.v.ps != undefined &&
                      n.v.ps.value === '计算值' &&
                      isInspectionValueAnnotation(n.v.ps.value, '计算值') &&
                      state == 1
                    "><span :style="`font-family:${n.v.ff} !important;`">{{
                      n.v.v
                    }}</span></template>
                    <template v-else-if="
                      n.v.ps != undefined &&
                      n.v.ps.value === '最终值' &&
                      isInspectionValueAnnotation(n.v.ps.value, '最终值') &&
                      state == 1
                    ">
                      <span :style="`font-family:${n.v.ff} !important;`">{{
@@ -523,7 +523,7 @@
import { mapGetters } from "@/utils/store-compat";
import viewManHourDia from "@/views/business/inspectionTask/components/viewManHourDia.vue";
import {getDicts} from "@/api/system/dict/data";
import { parseInspectionValueAnnotation, formatInspectionValue } from "@/utils/inspectionValueFormat";
import { parseInspectionValueAnnotation, formatInspectionValue, isInspectionValueAnnotation } from "@/utils/inspectionValueFormat";
export default {
  name: 'Inspection',
  components: {
@@ -838,6 +838,7 @@
    this.stopWorker();
  },
  methods: {
    isInspectionValueAnnotation,
    syncRouteContext() {
      const { sonLaboratory, orderId, state, inspectorList, typeSource, taskSource } = this.$route.query;
      const orderChanged = this.id !== orderId;
@@ -1509,8 +1510,8 @@
            b.v.ps != undefined &&
            typeof b.v.ps.value === "string" &&
            (b.v.ps.value.includes("检验值") ||
              b.v.ps.value === "计算值" ||
              b.v.ps.value === "最终值" ||
              isInspectionValueAnnotation(b.v.ps.value, "计算值") ||
              isInspectionValueAnnotation(b.v.ps.value, "最终值") ||
              b.v.ps.value === "设备名称")
          ) {
            b.v["v"] = "";
@@ -1575,12 +1576,12 @@
          if (value === undefined || value === null) {
            return;
          }
          cell.v.v = value;
          cell.v.v = formatInspectionValue(value, parseInspectionValueAnnotation(cell.v.ps && cell.v.ps.value));
          if (typeof item === "object" && item.u) {
            cell.u = item.u;
          }
          this.param[a.templateId][key] = {
            v: value,
            v: cell.v.v,
            u: typeof item === "object" ? item.u : cell.u,
          };
        });
@@ -2012,8 +2013,8 @@
            cell.v.ps != undefined &&
            typeof cell.v.ps.value === "string" &&
            (cell.v.ps.value.includes("检验值") ||
              cell.v.ps.value === "计算值" ||
              cell.v.ps.value === "最终值" ||
              isInspectionValueAnnotation(cell.v.ps.value, "计算值") ||
              isInspectionValueAnnotation(cell.v.ps.value, "最终值") ||
              cell.v.ps.value === "设备名称");
          const isExportResult = resultHeader && exportRows.has(cell.r) && cell.c === resultHeader.c;
          if (!isEditable && !isExportResult) {
@@ -2335,6 +2336,23 @@
  flex: 0 0 360px;
}
/* å¤æ ¸æ—¶é€‰æ‹©æ¡†ç¦ç”¨ï¼Œç›´æŽ¥å±•开完整设备名称和编号。 */
.template-panel:deep(.device-select .el-tag) {
  height: auto;
  min-height: 24px;
  padding-top: 2px;
  padding-bottom: 2px;
  max-width: 100%;
}
.template-panel:deep(.device-select .el-select__tags-text) {
  white-space: normal;
  overflow-wrap: anywhere;
  overflow: visible;
  text-overflow: clip;
  line-height: 1.5;
}
.template-panel__unit {
  margin-left: 4px;
  color: var(--lims-text-2);
src/views/business/inspectionView/index.vue
@@ -250,14 +250,14 @@
                  </template>
                  <template v-else-if="
                      n.v.ps != undefined &&
                      n.v.ps.value === '计算值' &&
                      isInspectionValueAnnotation(n.v.ps.value, '计算值') &&
                      state == 1
                    "><span :style="`font-family:${n.v.ff} !important;`">{{
                      n.v.v
                    }}</span></template>
                  <template v-else-if="
                      n.v.ps != undefined &&
                      n.v.ps.value === '最终值' &&
                      isInspectionValueAnnotation(n.v.ps.value, '最终值') &&
                      state == 1
                    ">
                      <span :style="`font-family:${n.v.ff} !important;`">{{
@@ -489,6 +489,7 @@
</template>
<script>
import { parseInspectionValueAnnotation, formatInspectionValue, isInspectionValueAnnotation } from "@/utils/inspectionValueFormat";
// Vue3 æ²¡æœ‰ this.$createElement(下面用 h é€  VNode å½“ $msgbox çš„ message)
import { h } from 'vue'
import { readDetectionTimeRange, isCompleteDetectionTimeRange, toDetectionTimePayload } from '@/utils/detectionTimeRange'
@@ -846,6 +847,7 @@
    this.stopWorker();
  },
  methods: {
    isInspectionValueAnnotation,
    syncRouteContext() {
      const { sonLaboratory, orderId, state, inspectorList, typeSource } = this.$route.query;
      const orderChanged = this.id !== orderId;
@@ -1779,8 +1781,8 @@
            b.v.ps != undefined &&
            typeof b.v.ps.value === "string" &&
            (b.v.ps.value.includes("检验值") ||
              b.v.ps.value === "计算值" ||
              b.v.ps.value === "最终值" ||
              isInspectionValueAnnotation(b.v.ps.value, "计算值") ||
              isInspectionValueAnnotation(b.v.ps.value, "最终值") ||
              b.v.ps.value === "设备名称")
          ) {
            b.v["v"] = "";
@@ -1845,12 +1847,12 @@
          if (value === undefined || value === null) {
            return;
          }
          cell.v.v = value;
          cell.v.v = formatInspectionValue(value, parseInspectionValueAnnotation(cell.v.ps && cell.v.ps.value));
          if (typeof item === "object" && item.u) {
            cell.u = item.u;
          }
          this.param[a.templateId][key] = {
            v: value,
            v: cell.v.v,
            u: typeof item === "object" ? item.u : cell.u,
          };
        });
@@ -2233,8 +2235,8 @@
            cell.v.ps != undefined &&
            typeof cell.v.ps.value === "string" &&
            (cell.v.ps.value.includes("检验值") ||
              cell.v.ps.value === "计算值" ||
              cell.v.ps.value === "最终值" ||
              isInspectionValueAnnotation(cell.v.ps.value, "计算值") ||
              isInspectionValueAnnotation(cell.v.ps.value, "最终值") ||
              cell.v.ps.value === "设备名称");
          const isExportResult = resultHeader && exportRows.has(cell.r) && cell.c === resultHeader.c;
          if (!isEditable && !isExportResult) {
@@ -2552,6 +2554,23 @@
  flex: 0 0 360px;
}
/* å¤æ ¸æ—¶é€‰æ‹©æ¡†ç¦ç”¨ï¼Œç›´æŽ¥å±•开完整设备名称和编号。 */
.template-panel:deep(.device-select .el-tag) {
  height: auto;
  min-height: 24px;
  padding-top: 2px;
  padding-bottom: 2px;
  max-width: 100%;
}
.template-panel:deep(.device-select .el-select__tags-text) {
  white-space: normal;
  overflow-wrap: anywhere;
  overflow: visible;
  text-overflow: clip;
  line-height: 1.5;
}
.template-panel__unit {
  margin-left: 4px;
  color: var(--lims-text-2);
src/workers/InspectionWorker.worker.js
@@ -1,6 +1,6 @@
// å¤šçº¿ç¨‹é‡Œé¢éœ€è¦ä¿å­˜çš„æ•°æ®
import Big from "big.js";
import { parseInspectionValueAnnotation, formatInspectionValue } from "../utils/inspectionValueFormat";
import { parseInspectionValueAnnotation, formatInspectionValue, isInspectionValueAnnotation } from "../utils/inspectionValueFormat";
let code = "";
// è¡¨æ ¼æ•°æ®ï¼ˆæ¸²æŸ“)
@@ -574,8 +574,8 @@
          b.v.ps != undefined &&
          typeof b.v.ps.value == "string" &&
          (b.v.ps.value.includes("检验值") ||
            b.v.ps.value === "计算值" ||
            b.v.ps.value === "最终值" ||
            isInspectionValueAnnotation(b.v.ps.value, "计算值") ||
            isInspectionValueAnnotation(b.v.ps.value, "最终值") ||
            b.v.ps.value === "设备名称");
        const isExportResult = resultHeader && exportRows.has(b.r) && b.c === resultHeader.c;
        if (!isEditable && !isExportResult) {
tests/superdoc-script.smoke.mjs
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,153 @@
// node tests/superdoc-script.smoke.mjs (requires Playwright and Chrome)
import assert from 'node:assert/strict'
import { mkdtemp, writeFile, readFile, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import path from 'node:path'
import { createServer } from 'node:http'
import { fileURLToPath } from 'node:url'
import { createServer as createViteServer } from 'vite'
import vue from '@vitejs/plugin-vue'
import JSZip from 'jszip'
import superdocHttpWorker from '../vite/plugins/superdoc-http-worker.js'
const { chromium } = await import(process.env.PLAYWRIGHT_MODULE || 'playwright')
const project = fileURLToPath(new URL('../', import.meta.url))
const temporary = await mkdtemp(path.join(tmpdir(), 'lims-superdoc-script-'))
const fixture = path.join(project, 'tests/.superdoc-script-smoke.html')
let browser, server, vite
try {
  await writeFile(fixture, `<!doctype html><html><body><div id="app"></div><script type="module">
    import { createApp, ref, h } from 'vue';
    import ElementPlus from 'element-plus';
    import 'element-plus/dist/index.css';
    import ReportWordEditor from '../src/components/ReportWordEditor/index.vue';
    window.documentState = ref(null);
    window.readonlyState = ref(false);
    window.changes = 0;
    const component = ref();
    window.openDocument = async (blob) => {
      window.result = null;
      documentState.value = { blob, title: 'script-test.docx' };
    };
    window.exportDocument = async () => Array.from(new Uint8Array(await (await component.value.exportDocx()).arrayBuffer()));
    createApp({ setup: () => () => documentState.value && h(ReportWordEditor, {
      ref: component, document: documentState.value, readonly: readonlyState.value,
      onReady: () => window.result = 'ready', onError: error => window.result = error,
      onChange: () => window.changes++
    }) }).use(ElementPlus).mount('#app');
    fetch('/fixture.docx').then(response => response.blob()).then(openDocument);
  </script></body></html>`)
  vite = await createViteServer({ configFile: false, root: project, logLevel: 'warn',
    cacheDir: path.join(temporary, 'cache'), server: { middlewareMode: true }, appType: 'custom',
    optimizeDeps: { entries: [fixture] },
    resolve: { alias: { '@': path.join(project, 'src') } },
    plugins: [vue(), superdocHttpWorker(), {
      name: 'capture-test-editor', enforce: 'pre',
      resolveId(id, importer) {
        if (importer === '\0test-editor-loader') return
        if (id === '@/utils/loadSuperDoc' || id.replaceAll('\\', '/').replace(/\.js$/, '') === path.join(project, 'src/utils/loadSuperDoc').replaceAll('\\', '/')) {
          return '\0test-editor-loader'
        }
      },
      load(id) {
        if (id !== '\0test-editor-loader') return
        return `import { loadSuperDoc as load } from ${JSON.stringify(path.join(project, 'src/utils/loadSuperDoc.js').replaceAll('\\', '/'))};
          export async function loadSuperDoc() {
            const module = await load();
            return { SuperDoc: class extends module.SuperDoc {
              constructor(options) { super(options); window.superdoc = this; }
            } };
          }`
      }
    }] })
  const zip = new JSZip()
  zip.file('[Content_Types].xml', '<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/></Types>')
  zip.file('_rels/.rels', '<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/></Relationships>')
  // Real report values live inside table cells; include existing bold to check preservation.
  zip.file('word/document.xml', '<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><w:body><w:tbl><w:tblPr><w:tblW w:w="9000" w:type="dxa"/></w:tblPr><w:tblGrid><w:gridCol w:w="9000"/></w:tblGrid><w:tr><w:tc><w:tcPr><w:tcW w:w="9000" w:type="dxa"/></w:tcPr><w:p><w:r><w:rPr><w:b/></w:rPr><w:t>10-3 H2O</w:t></w:r></w:p></w:tc></w:tr></w:tbl><w:p/><w:sectPr><w:pgSz w:w="11906" w:h="16838"/><w:pgMar w:top="1440" w:right="1440" w:bottom="1440" w:left="1440"/></w:sectPr></w:body></w:document>')
  const docx = await zip.generateAsync({ type: 'nodebuffer' })
  server = createServer(async (req, res) => {
    try {
      const url = new URL(req.url, 'http://localhost')
      if (url.pathname === '/fixture.docx') { res.end(docx); return }
      if (url.pathname === '/') {
        res.setHeader('Content-Type', 'text/html')
        res.end(await vite.transformIndexHtml('/tests/.superdoc-script-smoke.html', await readFile(fixture, 'utf8')))
        return
      }
      vite.middlewares(req, res, () => res.writeHead(404).end())
    } catch { res.writeHead(404).end() }
  })
  await new Promise(resolve => server.listen(0, '127.0.0.1', resolve))
  browser = await chromium.launch({ executablePath: process.env.CHROME_PATH || 'C:/Program Files/Google/Chrome/Application/chrome.exe', headless: true })
  const page = await browser.newPage({ viewport: { width: 1500, height: 1000 } })
  const errors = []
  page.on('pageerror', error => errors.push(error.message))
  await page.goto(`http://127.0.0.1:${server.address().port}/`)
  async function ready() {
    await page.waitForFunction(() => window.result, null, { timeout: 120000 })
    assert.equal(await page.evaluate(() => window.result), 'ready')
  }
  await ready()
  async function select(start, end) {
    await page.locator('.superdoc-page').first().click({ position: { x: 120, y: 110 } })
    await page.keyboard.press('Control+Home')
    for (let i = 0; i < start; i++) await page.keyboard.press('ArrowRight')
    for (let i = start; i < end; i++) await page.keyboard.press('Shift+ArrowRight')
    if (start !== end) await page.waitForFunction(async () => !(await superdoc.activeEditor.doc.selection.current()).empty)
  }
  async function xml() {
    const bytes = await page.evaluate(() => exportDocument())
    return (await JSZip.loadAsync(Buffer.from(bytes))).file('word/document.xml').async('string')
  }
  function runs(xml) {
    return [...xml.matchAll(/<w:r\b[^>]*>([\s\S]*?)<\/w:r>/g)].map(match => ({
      text: [...match[1].matchAll(/<w:t\b[^>]*>([\s\S]*?)<\/w:t>/g)].map(m => m[1]).join(''),
      align: match[1].match(/<w:vertAlign\b[^>]*w:val="([^"]+)"/)?.[1],
      bold: /<w:b(?:\s|\/|>)/.test(match[1])
    }))
  }
  async function apply(label) {
    const before = await page.evaluate(() => changes)
    await page.getByRole('button', { name: label, exact: true }).click()
    await page.locator('.el-message').last().waitFor()
    const message = await page.locator('.el-message').last().textContent()
    try { await page.waitForFunction(before => changes > before, before, { timeout: 5000 }) }
    catch (error) { throw new Error(`${error.message}: ${message}`) }
  }
  await select(2, 4)
  await apply('上标 x²')
  let formatted = runs(await xml())
  assert.ok(formatted.some(run => run.text === '-3' && run.align === 'superscript' && run.bold))
  assert.ok(formatted.filter(run => run.text && run.text !== '-3').every(run => run.align !== 'superscript'))
  await select(6, 7)
  await apply('下标 x₂')
  formatted = runs(await xml())
  assert.ok(formatted.some(run => run.text === '2' && run.align === 'subscript'))
  const saved = await page.evaluate(() => exportDocument())
  await page.evaluate(bytes => openDocument(new Blob([new Uint8Array(bytes)])), saved)
  await ready()
  formatted = runs(await xml())
  assert.ok(formatted.some(run => run.text === '-3' && run.align === 'superscript'))
  assert.ok(formatted.some(run => run.text === '2' && run.align === 'subscript'))
  await select(2, 4)
  await apply('恢复正常')
  assert.ok(runs(await xml()).filter(run => run.text.includes('-3')).every(run => !run.align || run.align === 'baseline'))
  await select(0, 0)
  const before = await xml()
  await page.getByRole('button', { name: '上标 x²', exact: true }).click()
  await page.getByText('请先选中需要设置上下标的文字,例如 10-3 ä¸­çš„ -3', { exact: true }).waitFor()
  assert.equal(await xml(), before)
  await page.evaluate(() => readonlyState.value = true)
  assert.equal(await page.getByRole('button', { name: '上标 x²', exact: true }).isDisabled(), true)
  assert.deepEqual(errors, [])
  console.log(JSON.stringify({ result: 'PASS', superscript: true, subscript: true, preservedBold: true,
    restoredBaseline: true, reopened: true, emptySelectionUnchanged: true, readonlyDisabled: true, browserErrors: errors }, null, 2))
} finally {
  await browser?.close()
  await vite?.close()
  if (server) await new Promise(resolve => server.close(resolve))
  await rm(fixture, { force: true })
  assert.ok(path.resolve(temporary).startsWith(path.resolve(tmpdir()) + path.sep + 'lims-superdoc-script-'))
  await rm(temporary, { recursive: true, force: true })
}