重构pagetype覆盖规则
This commit is contained in:
parent
24eb9f6199
commit
419beb76b2
@ -1,188 +0,0 @@
|
|||||||
<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>
|
|
||||||
@ -1,11 +1,11 @@
|
|||||||
// 字段控件覆盖机制
|
// 字段控件覆盖机制
|
||||||
// 约定:/src/apps/<app>/<module>/pagetype/<pagetype>/form/controls/<fieldtype>.vue
|
// 约定:/src/views/pagetype/<pagetype>/form/controls/<fieldtype>.vue
|
||||||
|
|
||||||
type AsyncComponentLoader = () => Promise<any>
|
type AsyncComponentLoader = () => Promise<any>
|
||||||
|
|
||||||
// 扫描所有可能的控件覆盖组件
|
// 扫描所有可能的控件覆盖组件
|
||||||
const allControlOverrides: Record<string, AsyncComponentLoader> = import.meta.glob(
|
const allControlOverrides: Record<string, AsyncComponentLoader> = import.meta.glob(
|
||||||
'/src/apps/**/pagetype/**/form/controls/**.vue'
|
'/src/views/pagetype/**/form/controls/**.vue'
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -20,31 +20,23 @@ export async function resolveControlOverride(entity: string, fieldtype: string):
|
|||||||
const entityUnderscore = entity.toLowerCase().replace(/-/g, '_')
|
const entityUnderscore = entity.toLowerCase().replace(/-/g, '_')
|
||||||
const fieldtypeNormalized = fieldtype.replace(/\s+/g, '')
|
const fieldtypeNormalized = fieldtype.replace(/\s+/g, '')
|
||||||
|
|
||||||
// 构建目标路径模式
|
|
||||||
const targetPattern = `/src/apps/**/pagetype/${entityUnderscore}/form/controls/${fieldtypeNormalized}.vue`
|
|
||||||
|
|
||||||
// 查找匹配的控件覆盖
|
// 查找匹配的控件覆盖
|
||||||
const candidates = Object.keys(allControlOverrides).filter((path) => {
|
const candidates = Object.keys(allControlOverrides).filter((path) => {
|
||||||
const segments = path.split('/').filter(Boolean)
|
const segments = path.split('/').filter(Boolean)
|
||||||
const len = segments.length
|
const len = segments.length
|
||||||
|
|
||||||
if (len < 7) return false // 至少需要 7 段路径
|
if (len < 6) return false // 至少需要 6 段路径:views/pagetype/{entity}/form/controls/{fieldtype}.vue
|
||||||
|
|
||||||
// 检查路径结构:.../pagetype/{entity}/form/controls/{fieldtype}.vue
|
// 检查路径结构:views/pagetype/{entity}/form/controls/{fieldtype}.vue
|
||||||
const pagetypeIndex = segments.findIndex(s => s === 'pagetype')
|
|
||||||
if (pagetypeIndex === -1 || pagetypeIndex >= len - 4) return false
|
|
||||||
|
|
||||||
const entityInPath = segments[pagetypeIndex + 1]
|
|
||||||
const formInPath = segments[pagetypeIndex + 2]
|
|
||||||
const controlsInPath = segments[pagetypeIndex + 3]
|
|
||||||
const fileName = segments[len - 1]
|
const fileName = segments[len - 1]
|
||||||
|
|
||||||
const fieldtypeInPath = fileName.replace(/\.vue$/i, '')
|
const fieldtypeInPath = fileName.replace(/\.vue$/i, '')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
entityInPath === entityUnderscore &&
|
segments[0] === 'views' &&
|
||||||
formInPath === 'form' &&
|
segments[1] === 'pagetype' &&
|
||||||
controlsInPath === 'controls' &&
|
segments[2] === entityUnderscore &&
|
||||||
|
segments[3] === 'form' &&
|
||||||
|
segments[4] === 'controls' &&
|
||||||
fieldtypeInPath === fieldtypeNormalized
|
fieldtypeInPath === fieldtypeNormalized
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
// 基于约定路径自动解析 pagetype 详情覆盖组件
|
// 基于约定路径自动解析 pagetype 详情覆盖组件
|
||||||
// 约定:/src/apps/<app>/<module>/pagetype/<pagetype>/<pagetype>.vue
|
// 约定:/src/views/pagetype/<pagetype>/<pagetype>.vue
|
||||||
|
|
||||||
type AsyncComponentLoader = () => Promise<any>
|
type AsyncComponentLoader = () => Promise<any>
|
||||||
|
|
||||||
// 扫描所有可能的覆盖组件(仅限 .vue)
|
// 扫描所有可能的覆盖组件(仅限 .vue)
|
||||||
// 使用 eager: false 延迟加载,生产环境下可按需分包
|
// 使用 eager: false 延迟加载,生产环境下可按需分包
|
||||||
const allPagetypeViews: Record<string, AsyncComponentLoader> = import.meta.glob(
|
const allPagetypeViews: Record<string, AsyncComponentLoader> = import.meta.glob(
|
||||||
'/src/apps/**/pagetype/**/**.vue'
|
'/src/views/pagetype/**/**.vue'
|
||||||
)
|
)
|
||||||
|
|
||||||
function getPathSegments(path: string): string[] {
|
function getPathSegments(path: string): string[] {
|
||||||
@ -24,20 +24,21 @@ export async function resolvePagetypeDetailOverride(pagetypeSlug: string): Promi
|
|||||||
const targetHyphen = pagetypeSlug.toLowerCase()
|
const targetHyphen = pagetypeSlug.toLowerCase()
|
||||||
const targetUnderscore = targetHyphen.replace(/-/g, '_')
|
const targetUnderscore = targetHyphen.replace(/-/g, '_')
|
||||||
|
|
||||||
// 匹配末尾形如 .../pagetype/<name>/<name>.vue 的文件
|
// 匹配形如 views/pagetype/<name>/<name>.vue 的文件
|
||||||
const candidates = Object.keys(allPagetypeViews).filter((file) => {
|
const candidates = Object.keys(allPagetypeViews).filter((file) => {
|
||||||
const segs = getPathSegments(file)
|
const segs = getPathSegments(file)
|
||||||
const len = segs.length
|
const len = segs.length
|
||||||
if (len < 5) return false
|
if (len < 5) return false
|
||||||
// 末两段应为 <name>.vue 与 <name>
|
// 路径应为 views/pagetype/<name>/<name>.vue
|
||||||
const fileName = segs[len - 1]
|
const fileName = segs[len - 1]
|
||||||
const folderName = segs[len - 2]
|
const folderName = segs[len - 2]
|
||||||
const parentFolder = segs.findIndex((s) => s === 'pagetype')
|
|
||||||
if (parentFolder === -1) return false
|
|
||||||
const baseName = fileName.replace(/\.vue$/i, '')
|
const baseName = fileName.replace(/\.vue$/i, '')
|
||||||
// 需要同时满足:位于 pagetype 下,且末两级目录名与目标一致(支持下划线形式)
|
// 需要同时满足:位于 views/pagetype 下,且末两级目录名与目标一致(支持下划线形式)
|
||||||
return (
|
return (
|
||||||
(folderName === targetUnderscore && baseName === targetUnderscore)
|
segs[1] === 'views' &&
|
||||||
|
segs[2] === 'pagetype' &&
|
||||||
|
folderName === targetUnderscore &&
|
||||||
|
baseName === targetUnderscore
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -61,7 +62,7 @@ export async function resolvePagetypeDetailOverride(pagetypeSlug: string): Promi
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 解析并返回指定 pagetype 的工具栏覆盖组件
|
* 解析并返回指定 pagetype 的工具栏覆盖组件
|
||||||
* 约定文件名:/src/apps/任意路径/pagetype/名称/名称_toolbar.vue
|
* 约定文件名:/src/views/pagetype/<name>/<name>_toolbar.vue
|
||||||
*/
|
*/
|
||||||
export async function resolvePagetypeToolbarOverride(pagetypeSlug: string): Promise<any | null> {
|
export async function resolvePagetypeToolbarOverride(pagetypeSlug: string): Promise<any | null> {
|
||||||
if (!pagetypeSlug) return null
|
if (!pagetypeSlug) return null
|
||||||
@ -72,13 +73,15 @@ export async function resolvePagetypeToolbarOverride(pagetypeSlug: string): Prom
|
|||||||
const segs = getPathSegments(file)
|
const segs = getPathSegments(file)
|
||||||
const len = segs.length
|
const len = segs.length
|
||||||
if (len < 5) return false
|
if (len < 5) return false
|
||||||
|
// 路径应为 views/pagetype/<name>/<name>_toolbar.vue
|
||||||
const fileName = segs[len - 1]
|
const fileName = segs[len - 1]
|
||||||
const folderName = segs[len - 2]
|
const folderName = segs[len - 2]
|
||||||
const parentFolder = segs.findIndex((s) => s === 'pagetype')
|
|
||||||
if (parentFolder === -1) return false
|
|
||||||
const baseName = fileName.replace(/\.vue$/i, '')
|
const baseName = fileName.replace(/\.vue$/i, '')
|
||||||
return (
|
return (
|
||||||
folderName === targetUnderscore && baseName === `${targetUnderscore}_toolbar`
|
segs[1] === 'views' &&
|
||||||
|
segs[2] === 'pagetype' &&
|
||||||
|
folderName === targetUnderscore &&
|
||||||
|
baseName === `${targetUnderscore}_toolbar`
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user