修复字段类型控件无法覆盖核心控件的问题

This commit is contained in:
jingrow 2025-10-25 23:27:15 +08:00
parent b9363f6f37
commit 5ba984a499
2 changed files with 211 additions and 30 deletions

View File

@ -8,6 +8,20 @@ const allControlOverrides: Record<string, AsyncComponentLoader> = import.meta.gl
'/src/views/pagetype/**/form/controls/**.vue'
)
// 预计算路径映射,提高查找效率
const pathMap = new Map<string, string>()
// 初始化路径映射
Object.keys(allControlOverrides).forEach(path => {
const segments = path.split('/').filter(Boolean)
if (segments.length >= 7) {
const entity = segments[3]
const fieldtype = segments[6].replace(/\.vue$/i, '')
const key = `${entity}:${fieldtype}`
pathMap.set(key, path)
}
})
/**
* pagetype fieldtype
* @param entity pagetype名称
@ -16,41 +30,20 @@ const allControlOverrides: Record<string, AsyncComponentLoader> = import.meta.gl
export async function resolveControlOverride(entity: string, fieldtype: string): Promise<any | null> {
if (!entity || !fieldtype) return null
// 将entity转换为下划线格式与文件系统一致
const entityUnderscore = entity.toLowerCase().replace(/-/g, '_')
const fieldtypeNormalized = fieldtype.replace(/\s+/g, '')
// 标准化输入
const entityKey = entity.toLowerCase().replace(/-/g, '_')
const fieldtypeKey = fieldtype.replace(/\s+/g, '')
// 查找匹配的控件覆盖
const candidates = Object.keys(allControlOverrides).filter((path) => {
const segments = path.split('/').filter(Boolean)
const len = segments.length
if (len < 6) return false // 至少需要 6 段路径views/pagetype/{entity}/form/controls/{fieldtype}.vue
// 检查路径结构views/pagetype/{entity}/form/controls/{fieldtype}.vue
const fileName = segments[len - 1]
const fieldtypeInPath = fileName.replace(/\.vue$/i, '')
return (
segments[0] === 'views' &&
segments[1] === 'pagetype' &&
segments[2] === entityUnderscore &&
segments[3] === 'form' &&
segments[4] === 'controls' &&
fieldtypeInPath === fieldtypeNormalized
)
})
// 直接查找匹配的路径
const key = `${entityKey}:${fieldtypeKey}`
const path = pathMap.get(key)
if (candidates.length === 0) return null
if (!path) return null
// 如果有多个匹配,选择路径最短的(更接近根目录的优先)
candidates.sort((a, b) => a.length - b.length)
const selectedPath = candidates[0]
try {
const module = await allControlOverrides[selectedPath]()
const module = await allControlOverrides[path]()
return module?.default ?? module
} catch (_e) {
} catch {
return null
}
}

View File

@ -0,0 +1,188 @@
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
const props = defineProps<{ df: any; record: Record<string, any>; canEdit: boolean; ctx: any }>()
// Label(vertical) (horizontal)
const labelLayout = computed(() => props.df.label_layout || 'vertical')
// HTML
const htmlContent = ref('')
// HTMLJSget_content
const getContent = () => {
// Filepreview_html
if (props.df.fieldname === 'preview_html') {
return generateFilePreview()
}
let content = props.df.options || ""
//
content = props.ctx.t ? props.ctx.t(content) : content
try {
// 使
if (props.ctx.render) {
return props.ctx.render(content, props)
}
return content
} catch (e) {
console.warn('HTML模板渲染失败:', e)
return content
}
}
// FileJSpreview_file
const generateFilePreview = () => {
const record = props.record
const fileUrl = record.file_url
const fileType = record.file_type?.toLowerCase()
if (!fileUrl) return ''
//
if (isImageFile(fileUrl)) {
return `<div class="img_preview">
<img
class="img-responsive"
src="${escapeHtml(fileUrl)}"
onerror="this.parentElement.style.display='none'"
/>
</div>`
}
//
if (isVideoFile(fileUrl)) {
return `<div class="img_preview">
<video width="480" height="320" controls>
<source src="${escapeHtml(fileUrl)}">
${props.ctx.t ? props.ctx.t('Your browser does not support the video element.') : 'Your browser does not support the video element.'}
</video>
</div>`
}
// PDF
if (fileType === 'pdf') {
return `<div class="img_preview">
<object style="background:#323639;" width="100%">
<embed
style="background:#323639;"
width="100%"
height="1190"
src="${escapeHtml(fileUrl)}" type="application/pdf"
>
</object>
</div>`
}
// MP3
if (fileType === 'mp3') {
return `<div class="img_preview">
<audio width="480" height="60" controls>
<source src="${escapeHtml(fileUrl)}" type="audio/mpeg">
${props.ctx.t ? props.ctx.t('Your browser does not support the audio element.') : 'Your browser does not support the audio element.'}
</audio>
</div>`
}
return ''
}
//
const isImageFile = (url: string) => {
const imageExtensions = /\.(gif|jpg|jpeg|tiff|png|webp|bmp|svg)$/i
return imageExtensions.test(url)
}
//
const isVideoFile = (url: string) => {
const videoExtensions = /\.(mp4|avi|mov|wmv|flv|webm|mkv)$/i
return videoExtensions.test(url)
}
// HTML
const escapeHtml = (text: string) => {
const div = document.createElement('div')
div.textContent = text
return div.innerHTML
}
// HTML
const refreshContent = () => {
htmlContent.value = getContent()
}
// HTMLJSset_value
const setValue = (html: string | any) => {
if (html && typeof html === 'object' && html.appendTo) {
// jQueryVue
console.warn('jQuery对象在Vue中需要特殊处理')
props.df.options = html.toString()
} else {
// HTML
props.df.options = html
}
refreshContent()
}
// df.options
watch(() => props.df.options, () => {
refreshContent()
}, { immediate: true })
// recordFilefile_url
watch(() => props.record, () => {
refreshContent()
}, { deep: true, immediate: true })
//
defineExpose({
setValue,
refreshContent,
getContent
})
</script>
<template>
<div :class="['field-wrapper', `layout-${labelLayout}`]">
<label class="field-label">
{{ ctx.t(df.label || df.fieldname) }}
<span v-if="df.reqd" class="required">*</span>
</label>
<div class="html-content" v-html="htmlContent"></div>
</div>
</template>
<style scoped>
.field-wrapper :deep(.html-content) {
flex: 1;
}
/* File预览样式 */
.field-wrapper :deep(.img_preview) {
width: 100%;
text-align: center;
margin: 8px 0;
}
.field-wrapper :deep(.img_preview img) {
max-width: 100%;
height: auto;
}
.field-wrapper :deep(.img_preview video) {
max-width: 100%;
height: auto;
border-radius: 6px;
}
.field-wrapper :deep(.img_preview audio) {
width: 100%;
margin: 8px 0;
}
.field-wrapper :deep(.img_preview object) {
border-radius: 6px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
</style>