Files
zkxw/web-admin/src/utils/oa-form-create-upload.ts
T
zlwuandCursor 8020e667f1 fix: 多张大图并行上传时不再拆掉进行中的请求,并放宽附件上传超时。
审批附件在第一张成功后会重建上传组件,其余请求被取消并显示超时。现保留进行中的文件,附件请求与 Nginx 上传超时放宽到 5 分钟,存储写入不再堵住事件循环。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-29 10:42:07 +08:00

174 lines
5.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* FormCreate 运行时补丁:
* - 内置 upload 默认 action 为「/」,会 POST 到前端原点导致 404;接管为附件上传 API
* - 运行时注册 FcRow(flex ._fd-row),间距与铺满由 oa-fc-runtime.scss 处理
* - 覆盖内置 upload:picture-card 下非图片用文档/视频图标展示,避免破图
*/
import { ElMessage } from 'element-plus'
import { fetchUploadAttachment } from '@/api/business'
import { isHttpError } from '@/utils/http/error'
import { attachmentFullUrl } from '@/composables/useAttachmentActions'
import OaFcRow from '@/components/oa-approval/OaFcRow.vue'
import OaFcUpload from '@/components/oa-approval/OaFcUpload.vue'
type FcFetchOptions = {
source?: string
file?: File
filename?: string
data?: Record<string, unknown>
action?: string
onSuccess?: (res: unknown) => void
onError?: (err: unknown) => void
[key: string]: unknown
}
const FLAG = '__oaUploadConfigured'
const UPLOAD_TYPES = new Set(['upload', 'fcUpload'])
/** 与业务表单 `.form-grid-2` 横向间距对齐(顶层 option.row.gutter / CSS --oa-fc-gutter) */
export const OA_FC_DEFAULT_GUTTER = 20
/** 从上传接口响应中取出可回显的 url(兼容 FormCreate 默认 onSuccess) */
export function resolveUploadResponseUrl(res: any): string {
const raw = String(
res?.data?.url || res?.data?.fileUrl || res?.fileUrl || res?.url || ''
).trim()
if (!raw) return ''
return attachmentFullUrl(raw) || raw
}
function ensureUploadRule(rule: any) {
if (!UPLOAD_TYPES.has(String(rule.type || ''))) return
const props = { ...(rule.props || {}) }
if (!props.action || props.action === '/') {
props.action = '#'
}
const prevSuccess = props.onSuccess
// `$FNX:...` 须留给 FormCreate 编译;替换成函数会丢掉官方 file.url = res.data.url
if (typeof prevSuccess === 'string' && prevSuccess.includes('$FNX')) {
// keep
} else if (typeof prevSuccess === 'function') {
props.onSuccess = (res: any, file: any) => {
try {
prevSuccess(res, file)
} catch {
/* ignore */
}
const url = resolveUploadResponseUrl(res)
// Element Plus 选文件时已写入 blob: URL,必须强制覆盖
if (url && file) {
file.url = url
}
}
} else if (!prevSuccess) {
props.onSuccess = (res: any, file: any) => {
const url = resolveUploadResponseUrl(res)
if (url && file) {
file.url = url
}
}
}
rule.props = props
}
/**
* 运行时补齐 FormCreate rule(upload action / onSuccess)
*/
export function ensureOaUploadRuleProps(rules: unknown): any[] {
if (!Array.isArray(rules)) return []
return rules.map((raw) => {
if (!raw || typeof raw !== 'object') return raw
const rule: any = { ...(raw as object) }
ensureUploadRule(rule)
if (Array.isArray(rule.children)) {
rule.children = ensureOaUploadRuleProps(rule.children)
}
if (Array.isArray(rule.props?.rule)) {
rule.props = {
...rule.props,
rule: ensureOaUploadRuleProps(rule.props.rule)
}
}
if (Array.isArray(rule.control)) {
rule.control = rule.control.map((c: any) => {
if (!c || typeof c !== 'object') return c
if (Array.isArray(c.rule)) {
return { ...c, rule: ensureOaUploadRuleProps(c.rule) }
}
return c
})
}
return rule
})
}
export function setupFormCreateUpload(formCreate: {
fetch?: (opts: FcFetchOptions) => void
component?: (name: string, component: unknown) => void
}) {
if (!formCreate || (formCreate as any)[FLAG]) return
// 主应用 formCreate 默认未挂设计器 FcRow;用 flex 行保证栅格铺满,间距走 CSS
if (typeof formCreate.component === 'function') {
formCreate.component('fcRow', OaFcRow)
formCreate.component('FcRow', OaFcRow)
// 覆盖默认 fcUpload:非图片不再走 <img> 破图
formCreate.component('upload', OaFcUpload)
formCreate.component('fcUpload', OaFcUpload)
}
const prevFetch = formCreate.fetch
formCreate.fetch = (options: FcFetchOptions) => {
const isUpload = options?.source === 'upload' || !!options?.file
if (!isUpload) {
if (typeof prevFetch === 'function') {
return prevFetch(options)
}
return
}
void (async () => {
try {
const file = options.file
if (!file) {
throw new Error('未选择文件')
}
const fd = new FormData()
fd.append('file', file)
fd.append('folder', 'oa-approval')
const extra = options.data
if (extra && typeof extra === 'object') {
Object.entries(extra).forEach(([k, v]) => {
if (v != null && k !== 'file' && k !== 'folder') {
fd.append(k, String(v))
}
})
}
const res: any = await fetchUploadAttachment(fd)
const rawUrl = String(res?.fileUrl || res?.data?.fileUrl || '').trim()
if (!rawUrl) {
throw new Error('上传失败:未返回文件地址')
}
// 预览用完整 URL;设计器默认 onSuccess 为 file.url = res.data.url
const url = attachmentFullUrl(rawUrl) || rawUrl
options.onSuccess?.({
data: { url, fileUrl: rawUrl },
fileUrl: rawUrl,
url
})
} catch (e) {
// HttpError 已由请求层弹出,这里再弹一次会叠成两条「请求超时」
if (!isHttpError(e)) {
const msg = e instanceof Error ? e.message : '上传失败'
ElMessage.error(msg)
}
options.onError?.(e)
}
})()
}
;(formCreate as any)[FLAG] = true
}