fix(upload): 响应式更新上传项以恢复外部保存按钮。
This commit is contained in:
1 parent
1ede95841c
commit
eb12c755d3
2 files changed
+120
-2
No files matched your search
@@ -0,0 +1,118 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { parse, compileScript } from '@vue/compiler-sfc'
|
||||
import ts from 'typescript'
|
||||
import * as vue from 'vue'
|
||||
function compiledSfc(path: string, dependencies: Record<string, any>, inlineTemplate = true) {
|
||||
const descriptor = parse(readFileSync(new URL(path, import.meta.url), 'utf8')).descriptor
|
||||
const compiled = compileScript(descriptor, { id: path, inlineTemplate })
|
||||
const source = ts.createSourceFile('compiled.ts', compiled.content, ts.ScriptTarget.Latest, true)
|
||||
const env: Record<string, any> = Object.fromEntries(
|
||||
Object.entries(vue).filter(([key]) => /^[a-zA-Z_$][\w$]*$/.test(key) && key !== 'default')
|
||||
)
|
||||
Object.assign(env, dependencies)
|
||||
for (const statement of source.statements) {
|
||||
if (!ts.isImportDeclaration(statement) || statement.importClause?.isTypeOnly) continue
|
||||
const clause = statement.importClause
|
||||
if (clause?.name) env[clause.name.text] = dependencies[clause.name.text] || {}
|
||||
if (clause?.namedBindings && ts.isNamedImports(clause.namedBindings)) {
|
||||
for (const item of clause.namedBindings.elements) {
|
||||
if (item.isTypeOnly) continue
|
||||
const name = (item.propertyName || item.name).text
|
||||
env[item.name.text] =
|
||||
statement.moduleSpecifier.getText(source) === '"vue"' ||
|
||||
statement.moduleSpecifier.getText(source) === "'vue'"
|
||||
? (vue as any)[name]
|
||||
: dependencies[name]
|
||||
}
|
||||
}
|
||||
}
|
||||
const code = ts.transpile(
|
||||
source.statements
|
||||
.filter((s) => !ts.isImportDeclaration(s))
|
||||
.map((s) => s.getText(source))
|
||||
.join('\n')
|
||||
.replace('export default', 'const component ='),
|
||||
{ target: ts.ScriptTarget.ESNext }
|
||||
)
|
||||
return new Function(...Object.keys(env), `${code}; return component`)(...Object.values(env))
|
||||
}
|
||||
|
||||
for (const outcome of ['success', 'failure'] as const)
|
||||
test(`real attachment ${outcome} releases reactive external save guard`, async () => {
|
||||
let resolve!: (value: any) => void, reject!: (reason: any) => void
|
||||
const pending = new Promise((a, b) => {
|
||||
resolve = a
|
||||
reject = b
|
||||
})
|
||||
const events: string[] = []
|
||||
const browser = { addEventListener() {}, removeEventListener() {} }
|
||||
const Attachment = compiledSfc(
|
||||
'../src/components/business/attachment-upload-field/index.vue',
|
||||
{
|
||||
useRoute: () => ({ path: '/pl/legal-use-document' }),
|
||||
isMpWebviewPath: () => false,
|
||||
fetchUploadAttachment: () => pending,
|
||||
ElMessage: { warning: (v: string) => events.push(v), error: (v: string) => events.push(v) },
|
||||
document: browser,
|
||||
window: browser
|
||||
},
|
||||
false
|
||||
)
|
||||
Attachment.render = () => null
|
||||
const upload = vue.ref<any>(),
|
||||
model = vue.ref('')
|
||||
// An external footer/computed consumes the exposed guard independently of v-model.
|
||||
const savingDisabled = vue.computed(() => !!upload.value?.hasUploading())
|
||||
const buttons: any[] = []
|
||||
const renderer = vue.createRenderer<any, any>({
|
||||
createElement: (tag: string) => {
|
||||
const el: any = { tag }
|
||||
if (tag === 'button') buttons.push(el)
|
||||
return el
|
||||
},
|
||||
createText: () => ({}),
|
||||
createComment: () => ({}),
|
||||
setText() {},
|
||||
setElementText() {},
|
||||
parentNode: () => null,
|
||||
nextSibling: () => null,
|
||||
insert() {},
|
||||
remove() {},
|
||||
patchProp: (el: any, key: string, _prev: any, value: any) => {
|
||||
el[key] = value
|
||||
}
|
||||
})
|
||||
const app = renderer.createApp({
|
||||
render: () =>
|
||||
vue.h('section', [
|
||||
vue.h(Attachment, {
|
||||
ref: upload,
|
||||
modelValue: model.value,
|
||||
'onUpdate:modelValue': (value: string) => {
|
||||
model.value = value
|
||||
}
|
||||
}),
|
||||
vue.h('button', { disabled: savingDisabled.value }, '保存本段资料')
|
||||
])
|
||||
})
|
||||
app.mount({})
|
||||
await vue.nextTick()
|
||||
assert.equal(buttons[0].disabled, false)
|
||||
const operation = upload.value.$.setupState.handleAttachmentChange({
|
||||
raw: new File(['synthetic'], 'proof.pdf', { type: 'application/pdf' })
|
||||
})
|
||||
await vue.nextTick()
|
||||
assert.equal(buttons[0].disabled, true)
|
||||
assert.equal(upload.value.hasUploading(), true)
|
||||
if (outcome === 'success')
|
||||
resolve({ fileUrl: 'legal/proof.pdf', fileName: 'proof.pdf', fileType: 'application/pdf' })
|
||||
else reject(Error('上传失败'))
|
||||
await operation
|
||||
await vue.nextTick()
|
||||
assert.equal(upload.value.hasUploading(), false)
|
||||
assert.equal(buttons[0].disabled, false, '上传结束必须通知外部保存按钮更新')
|
||||
assert.equal(model.value, outcome === 'success' ? 'legal/proof.pdf' : '')
|
||||
app.unmount()
|
||||
})
|
||||
@@ -535,13 +535,13 @@ async function uploadRawFile(rawFile: File) {
|
||||
return false
|
||||
}
|
||||
|
||||
const item: AttachmentItem = {
|
||||
const item = reactive<AttachmentItem>({
|
||||
id: `upload-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
||||
fileName: rawFile.name,
|
||||
fileUrl: '',
|
||||
fileType: rawFile.type,
|
||||
_uploading: true
|
||||
}
|
||||
})
|
||||
attachments.value.push(item)
|
||||
|
||||
const formData = props.bizType
|
||||
|
||||
Reference in new issue
Block a user