修复字段类型控件无法覆盖核心控件的问题
This commit is contained in:
parent
b9363f6f37
commit
5ba984a499
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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('')
|
||||
|
||||
// 获取HTML内容,对齐JS版本的get_content方法
|
||||
const getContent = () => {
|
||||
// 特殊处理:File详情页的preview_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
|
||||
}
|
||||
}
|
||||
|
||||
// 生成File预览内容,对齐云端JS版本的preview_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()
|
||||
}
|
||||
|
||||
// 设置HTML内容,对齐JS版本的set_value方法
|
||||
const setValue = (html: string | any) => {
|
||||
if (html && typeof html === 'object' && html.appendTo) {
|
||||
// jQuery对象处理(在Vue中转换为字符串)
|
||||
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 })
|
||||
|
||||
// 监听record变化(特别是File详情页的file_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>
|
||||
Loading…
x
Reference in New Issue
Block a user