170 lines
5.4 KiB
TypeScript
170 lines
5.4 KiB
TypeScript
/**
|
||
* 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 { 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) {
|
||
const msg = e instanceof Error ? e.message : '上传失败'
|
||
ElMessage.error(msg)
|
||
options.onError?.(e)
|
||
}
|
||
})()
|
||
}
|
||
|
||
;(formCreate as any)[FLAG] = true
|
||
}
|