This commit is contained in:
zhangjianjun 2026-03-19 10:58:31 +08:00
parent 7acef4c569
commit 3ba5da9b98
9 changed files with 333 additions and 63 deletions

View File

@ -1,17 +1,4 @@
import Request from "lib/utils/requests"; import { uploadApi } from "./common";
export default { export default {
upload(data: any) { upload: uploadApi.upload
return Request({
url: "/admin/cmp_upload",
method: "post",
data,
params: {
expire: 24 * 30 * 12 * 10,
},
headers: {
"Content-Type": "multipart/form-data",
},
}).then(res => ({ data: {url: res.data} }))
},
}; };

View File

@ -57,6 +57,8 @@ export const uploadApi = {
headers: { headers: {
"Content-Type": "multipart/form-data", "Content-Type": "multipart/form-data",
}, },
}).then(res => {
return {data: {url: res.data}}
}) })
}, },
} }

View File

@ -1,4 +1,5 @@
import Request from "lib/utils/requests"; import Request from "lib/utils/requests";
import { uploadApi } from "../common";
export default { export default {
getDataList(params: any) { getDataList(params: any) {
return Request({ return Request({
@ -28,5 +29,6 @@ export default {
data: params data: params
}) })
}, },
upload: uploadApi.upload,
} }

View File

@ -19,8 +19,8 @@
</div> </div>
</div> </div>
</template> </template>
<el-form v-if="show" ref="formRef" :model="formData" :rules="formRules" label-position="left" <el-form :key="forceUpdateKey" v-if="show" ref="formRef" :model="formData" :rules="formRules"
v-loading="detailLoading"> label-position="left" v-loading="detailLoading">
<div v-if="normalFields.length" class="in18-form-top"> <div v-if="normalFields.length" class="in18-form-top">
<el-form-item v-for="item in normalFields" :key="item.key" :prop="item.key"> <el-form-item v-for="item in normalFields" :key="item.key" :prop="item.key">
@ -39,26 +39,26 @@
<div v-else-if="isUploadMediaType(item.type)" style="width:100%;"> <div v-else-if="isUploadMediaType(item.type)" style="width:100%;">
<div class="upload-group"> <div class="upload-group">
<div class="upload-label">图片</div> <div class="upload-label">图片</div>
<UploadInput :model-value="(formData[item.key]?.image || []).join(',')" <UploadInput :value="formData[item.key].image"
:type="item.type.includes('images') ? 'images' : 'image'" :type="item.type.includes('images') ? 'images' : 'image'" :uploadFile="uploadFun"
:uploadFile="uploadFun" @update:modelValue="(val: string) => { @updateValue="(val: any) => {
if (!formData[item.key]) formData[item.key] = { image: [], video: [] }; if (!formData[item.key]) formData[item.key] = { image: [], video: [] };
formData[item.key].image = val ? val.split(',').filter(Boolean) : []; formData[item.key].image = val.map((item: any) => item.url).filter(Boolean);
}" /> }" />
</div> </div>
<div class="upload-group"> <div class="upload-group">
<div class="upload-label">视频</div> <div class="upload-label">视频</div>
<UploadInput :model-value="(formData[item.key]?.video || []).join(',')" type="video" <UploadInput :value="formData[item.key]?.video" type="video" :uploadFile="uploadFun"
:uploadFile="uploadFun" @update:modelValue="(val: string) => { @updateValue="(val: any) => {
if (!formData[item.key]) formData[item.key] = { image: [], video: [] }; if (!formData[item.key]) formData[item.key] = { image: [], video: [] };
formData[item.key].video = val ? val.split(',').filter(Boolean) : []; formData[item.key].video = val.map((item: any) => item.url).filter(Boolean);
}" /> }" />
</div> </div>
</div> </div>
<div v-else-if="item.type === 'upload:file'" style="width:100%;" class="upload-file"> <div v-else-if="item.type === 'upload:file'" style="width:100%;" class="upload-file">
<UploadInput :model-value="formData[item.key]" type="file" :accept="item.accept || '*'" <UploadInput :value="formData[item.key]" type="file" :accept="item.accept || '*'"
:uploadFile="uploadFun" :uploadFile="uploadFun"
@update:modelValue="(val: string) => { formData[item.key] = val }" /> @updateValue="(val: any) => { formData[item.key] = val[0]?.url || '' }" />
</div> </div>
</el-form-item> </el-form-item>
</div> </div>
@ -87,11 +87,22 @@
v-model="formData.translations[primaryLocaleKey][item.key]" v-model="formData.translations[primaryLocaleKey][item.key]"
:placeholder="`请输入${item.name}`" /> :placeholder="`请输入${item.name}`" />
<el-input v-else-if="item.type === 'textarea'" <el-input v-else-if="item.type === 'textarea'"
v-model="formData.translations[primaryLocaleKey][item.key]" type="textarea" v-model="formData.translations[primaryLocaleKey][item.key]" type="textarea" :rows="4"
:rows="4" :placeholder="`请输入${item.name}`" /> :placeholder="`请输入${item.name}`" />
<WangEditor v-else-if="item.type === 'richtext'"
:content="formData.translations[primaryLocaleKey][item.key]"
:maxHeight="300"
style="z-index: 9999"
:key="item.key"
:upload-file="uploadFun"
@update:content="(val: string) => {
formData.translations[primaryLocaleKey][item.key] = val;
}"
>
</WangEditor>
<el-select v-else-if="item.type === 'select'" <el-select v-else-if="item.type === 'select'"
v-model="formData.translations[primaryLocaleKey][item.key]" v-model="formData.translations[primaryLocaleKey][item.key]" :placeholder="`请选择${item.name}`"
:placeholder="`请选择${item.name}`" clearable style="width: 100%"> clearable style="width: 100%">
<el-option v-for="opt in getSelectItems(item)" :key="opt.key" :label="opt.name" <el-option v-for="opt in getSelectItems(item)" :key="opt.key" :label="opt.name"
:value="opt.key" /> :value="opt.key" />
</el-select> </el-select>
@ -109,11 +120,29 @@
{{ item.name }} {{ item.name }}
</template> </template>
<div v-if="item.type === 'input' || item.type === 'textarea'" class="field-with-translate"> <div v-if="item.type === 'input' || item.type === 'textarea'" class="field-with-translate">
<el-input <el-input v-model="formData.translations[activeSecondaryLocale?.key || ''][item.key]"
v-model="formData.translations[activeSecondaryLocale?.key || ''][item.key]"
:type="item.type === 'textarea' ? 'textarea' : undefined" :type="item.type === 'textarea' ? 'textarea' : undefined"
:rows="item.type === 'textarea' ? 4 : undefined" :rows="item.type === 'textarea' ? 4 : undefined" :placeholder="`请输入${item.name}`" />
:placeholder="`请输入${item.name}`" /> <el-button link size="small" class="translate-icon"
:loading="translatingFieldKey === item.key" @click="handleTranslateField(item.key)"
title="翻译该字段">
<el-icon color="#409eff">
<Promotion />
</el-icon>
</el-button>
</div>
<div v-else-if="item.type === 'richtext'" class="field-with-translate">
<WangEditor
:content="formData.translations[activeSecondaryLocale?.key || ''][item.key]"
:maxHeight="300"
style="z-index: 9999"
:key="item.key"
:upload-file="uploadFun"
@update:content="(val: string) => {
formData.translations[activeSecondaryLocale?.key || ''][item.key] = val;
}"
>
</WangEditor>
<el-button link size="small" class="translate-icon" <el-button link size="small" class="translate-icon"
:loading="translatingFieldKey === item.key" @click="handleTranslateField(item.key)" :loading="translatingFieldKey === item.key" @click="handleTranslateField(item.key)"
title="翻译该字段"> title="翻译该字段">
@ -146,11 +175,12 @@ import type { FormInstance, FormRules } from 'element-plus'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { Close, Promotion, FullScreen, CopyDocument } from '@element-plus/icons-vue' import { Close, Promotion, FullScreen, CopyDocument } from '@element-plus/icons-vue'
import UploadInput from "lib/components/UploadInput.vue"; import UploadInput from "lib/components/UploadInput.vue";
import WangEditor from "lib/components/WangEditor.vue";
type FormItemType = type FormItemType =
| 'input' | 'input'
| 'select' | 'select'
| 'textarea' | 'textarea'
| 'richtext'
| 'upload:images,video' | 'upload:images,video'
| 'upload:image,video' | 'upload:image,video'
| 'upload:file' | 'upload:file'
@ -176,6 +206,7 @@ type LocaleItem = {
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
fullscreen: boolean
show: boolean show: boolean
id: string | number id: string | number
title?: string title?: string
@ -186,6 +217,7 @@ const props = withDefaults(
uploadFun?: (formData: FormData) => Promise<any> uploadFun?: (formData: FormData) => Promise<any>
}>(), }>(),
{ {
fullscreen: false,
show: false, show: false,
id: '', id: '',
title: '详情', title: '详情',
@ -245,9 +277,14 @@ const formData = reactive<{
const translating = ref(false) const translating = ref(false)
const translatingFieldKey = ref<string>('') const translatingFieldKey = ref<string>('')
const isFullscreen = ref(false) const isFullscreen = ref(props.fullscreen || false)
const detailLoading = ref(false) const detailLoading = ref(false)
const forceUpdateKey = ref(0)
function forceUpdate() {
forceUpdateKey.value++
}
function toggleFullscreen() { function toggleFullscreen() {
isFullscreen.value = !isFullscreen.value isFullscreen.value = !isFullscreen.value
} }
@ -309,6 +346,7 @@ watch(
detailLoading.value = true detailLoading.value = true
try { try {
const data = await props.detailApi(Number(props.id)) const data = await props.detailApi(Number(props.id))
console.log('data', data)
if (data) { if (data) {
const pKey = primaryLocaleKey.value const pKey = primaryLocaleKey.value
Object.keys(data).forEach((localeKey) => { Object.keys(data).forEach((localeKey) => {
@ -330,7 +368,9 @@ watch(
console.error('Failed to load detail:', e) console.error('Failed to load detail:', e)
ElMessage.error('加载详情失败') ElMessage.error('加载详情失败')
} finally { } finally {
detailLoading.value = false detailLoading.value = false;
//
forceUpdate()
} }
} }
} }

View File

@ -1,3 +1,23 @@
<!--
JsonFormDialog - JSON 多语言配置编辑弹窗
功能
- JSON 对象按语言 key ZHEN拆分为 tab 页签递归渲染为可编辑表单
- 支持 string / number / boolean / object / array含对象数组字符串数组类型自动识别与编辑
- EN tab 下支持"一键翻译" ZH 内容复制到 EN 并批量调用翻译接口仅翻译含汉字的字段
- EN tab 下每个文本框支持单字段翻译按钮当对应 ZH 值含中文时显示
- 翻译请求分批执行BATCH_SIZE / BATCH_DELAY防止接口过载
Props
- show: boolean 控制弹窗显示
- title: string 弹窗标题默认 '配置编辑'
- data: Record<string, any> JSON 数据顶层 key 作为语言 tab
- fieldMap: Record<string, string> 字段名映射用于显示中文 label
Events
- close 关闭弹窗
- submit(data) 保存返回编辑后的完整 JSON 数据
-->
<template> <template>
<el-dialog :model-value="show" width="900" :fullscreen="isFullscreen" :close-on-click-modal="false" @close="close" <el-dialog :model-value="show" width="900" :fullscreen="isFullscreen" :close-on-click-modal="false" @close="close"
:show-close="false"> :show-close="false">
@ -21,12 +41,15 @@
</div> </div>
</template> </template>
<el-tabs v-if="langKeys.length" v-model="activeTab" type="border-card"> <el-tabs v-if="langKeys.length" v-model="activeTab" type="border-card"
v-loading="translating" element-loading-text="正在翻译中..."
element-loading-background="rgba(255,255,255,0.8)">
<el-tab-pane v-for="lang in langKeys" :key="lang" :label="lang" :name="lang"> <el-tab-pane v-for="lang in langKeys" :key="lang" :label="lang" :name="lang">
<el-scrollbar max-height="60vh"> <el-scrollbar max-height="60vh">
<div class="jfd-content"> <div class="jfd-content">
<NodeEditor v-for="sKey in getSectionKeys(lang)" :key="sKey" :parent="formData[lang]" <NodeEditor v-for="sKey in getSectionKeys(lang)" :key="sKey" :parent="formData[lang]"
:field-key="sKey" :field-map="fieldMap" :depth="0" /> :field-key="sKey" :field-map="fieldMap" :depth="0" :lang="lang"
:zh-parent="lang === 'EN' ? formData['ZH'] : undefined" />
</div> </div>
</el-scrollbar> </el-scrollbar>
</el-tab-pane> </el-tab-pane>
@ -34,6 +57,8 @@
<el-empty v-else description="无数据" /> <el-empty v-else description="无数据" />
<template #footer> <template #footer>
<el-button v-if="activeTab === 'EN'" type="warning" :loading="translating"
@click="handleTranslateAll">一键翻译</el-button>
<el-button @click="close">取消</el-button> <el-button @click="close">取消</el-button>
<el-button type="primary" @click="submit">保存</el-button> <el-button type="primary" @click="submit">保存</el-button>
</template> </template>
@ -42,22 +67,26 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, watch, defineComponent, h, type PropType, type VNode, type Component } from 'vue' import { ref, computed, watch, defineComponent, h, type PropType, type VNode, type Component } from 'vue'
import { Close, FullScreen, CopyDocument, Plus, Delete } from '@element-plus/icons-vue' import { Close, FullScreen, CopyDocument, Plus, Delete, Promotion } from '@element-plus/icons-vue'
import { import {
ElFormItem, ElInput, ElInputNumber, ElSwitch, ElButton, ElFormItem, ElInput, ElInputNumber, ElSwitch, ElButton,
ElCollapse, ElCollapseItem, ElIcon ElCollapse, ElCollapseItem, ElIcon, ElMessage
} from 'element-plus' } from 'element-plus'
import { translateApi } from 'src/api/common'
import UploadInput from 'lib/components/UploadInput.vue'
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
show: boolean show: boolean
title?: string title?: string
data: Record<string, any> data: Record<string, any>
fieldMap?: Record<string, string> fieldMap?: Record<string, string>
uploadFile: (file: FormData) => Promise<any>
}>(), { }>(), {
show: false, show: false,
title: '配置编辑', title: '配置编辑',
data: () => ({}), data: () => ({}),
fieldMap: () => ({}), fieldMap: () => ({}),
uploadFile: () => Promise.resolve({}),
}) })
const emit = defineEmits(['close', 'submit']) const emit = defineEmits(['close', 'submit'])
@ -65,9 +94,41 @@ const emit = defineEmits(['close', 'submit'])
const isFullscreen = ref(false) const isFullscreen = ref(false)
const formData = ref<Record<string, any>>({}) const formData = ref<Record<string, any>>({})
const activeTab = ref('') const activeTab = ref('')
const translating = ref(false)
const translatingPath = ref('')
const langKeys = computed(() => Object.keys(formData.value)) const langKeys = computed(() => Object.keys(formData.value))
function hasChinese(str: string): boolean {
return /[\u4e00-\u9fa5]/.test(str)
}
function parseTranslateResult(res: any): string {
return res?.data?.text ?? res?.data ?? res?.result ?? (typeof res === 'string' ? res : '')
}
function collectChineseStrings(obj: any, result: { parent: any; key: string | number }[]) {
if (obj === null || obj === undefined) return
if (Array.isArray(obj)) {
obj.forEach((item, i) => {
if (typeof item === 'string' && hasChinese(item)) {
result.push({ parent: obj, key: i })
} else if (typeof item === 'object') {
collectChineseStrings(item, result)
}
})
} else if (typeof obj === 'object') {
for (const k of Object.keys(obj)) {
const val = obj[k]
if (typeof val === 'string' && hasChinese(val)) {
result.push({ parent: obj, key: k })
} else if (typeof val === 'object') {
collectChineseStrings(val, result)
}
}
}
}
function getSectionKeys(lang: string) { function getSectionKeys(lang: string) {
const langData = formData.value[lang] const langData = formData.value[lang]
return langData && typeof langData === 'object' ? Object.keys(langData) : [] return langData && typeof langData === 'object' ? Object.keys(langData) : []
@ -116,6 +177,74 @@ function submit() {
emit('submit', JSON.parse(JSON.stringify(formData.value))) emit('submit', JSON.parse(JSON.stringify(formData.value)))
} }
const BATCH_SIZE = 3
const BATCH_DELAY = 300
function delay(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms))
}
async function handleTranslateAll() {
if (!formData.value.ZH || translating.value) return
formData.value.EN = JSON.parse(JSON.stringify(formData.value.ZH))
const items: { parent: any; key: string | number }[] = []
collectChineseStrings(formData.value.EN, items)
if (!items.length) {
ElMessage.info('没有需要翻译的中文内容')
return
}
translating.value = true
let failCount = 0
try {
for (let i = 0; i < items.length; i += BATCH_SIZE) {
const batch = items.slice(i, i + BATCH_SIZE)
const results = await Promise.allSettled(
batch.map(async ({ parent, key }) => {
const text = String(parent[key]).trim()
const res = await translateApi.translate({ text, from: 'ZH', to: 'EN' })
return { parent, key, translated: parseTranslateResult(res) }
})
)
for (const result of results) {
if (result.status === 'fulfilled') {
const { parent, key, translated } = result.value
if (translated) parent[key] = translated
} else {
failCount++
}
}
if (i + BATCH_SIZE < items.length) await delay(BATCH_DELAY)
}
if (failCount > 0) {
ElMessage.warning(`${failCount} 个字段翻译失败`)
} else {
ElMessage.success('翻译完成')
}
} catch (e) {
console.error('Translate failed:', e)
ElMessage.error('翻译失败')
} finally {
translating.value = false
}
}
async function handleTranslateField(parent: any, key: string | number, zhParent: any) {
if (!zhParent || translatingPath.value) return
const zhText = String(zhParent[key] ?? '').trim()
if (!hasChinese(zhText)) return
translatingPath.value = `${key}`
try {
const res = await translateApi.translate({ text: zhText, from: 'ZH', to: 'EN' })
const translated = parseTranslateResult(res)
if (translated) parent[key] = translated
} catch (e) {
console.error('Translate failed:', e)
ElMessage.error('翻译失败')
} finally {
translatingPath.value = ''
}
}
function createEmpty(template: any): any { function createEmpty(template: any): any {
if (typeof template === 'string') return '' if (typeof template === 'string') return ''
if (typeof template === 'number') return 0 if (typeof template === 'number') return 0
@ -136,6 +265,8 @@ const NodeEditor: Component = defineComponent({
fieldKey: { type: [String, Number] as PropType<string | number>, required: true }, fieldKey: { type: [String, Number] as PropType<string | number>, required: true },
fieldMap: { type: Object as PropType<Record<string, string>>, default: () => ({}) }, fieldMap: { type: Object as PropType<Record<string, string>>, default: () => ({}) },
depth: { type: Number, default: 0 }, depth: { type: Number, default: 0 },
lang: { type: String, default: '' },
zhParent: { type: Object as PropType<any>, default: undefined },
}, },
setup(p) { setup(p) {
return () => { return () => {
@ -167,14 +298,53 @@ const NodeEditor: Component = defineComponent({
} }
if (typeof value === 'string') { if (typeof value === 'string') {
const isLong = value.length > 50 // const isLong = value.length > 50
return h(ElFormItem, { label, class: 'jfd-form-item' }, () =>
h(ElInput, { const needUpload = value.startsWith('http')
modelValue: value, const isImage = needUpload && value.includes('.jpg') || value.includes('.png') || value.includes('.jpeg') || value.includes('.gif') || value.includes('.bmp') || value.includes('.webp')
...(isLong ? { type: 'textarea', autosize: { minRows: 2, maxRows: 6 } } : {}), const isVideo = needUpload && value.includes('.mp4') || value.includes('.avi') || value.includes('.mov') || value.includes('.wmv') || value.includes('.flv') || value.includes('.mkv')
'onUpdate:modelValue': (v: string) => { p.parent[key] = v }, const isFile = needUpload && value.includes('.pdf') || value.includes('.doc') || value.includes('.docx') || value.includes('.xls') || value.includes('.xlsx') || value.includes('.ppt') || value.includes('.pptx')
let uploadNode = null;
if (needUpload && (isFile || isImage || isVideo)) {
uploadNode = h(UploadInput, {
value: [{url: value}],
type: isImage ? 'image' : isVideo ? 'video' : 'file',
uploadFile: props.uploadFile,
transport: `#jfd-${key}`,
onUpdateValue: (v: any) => {
if(v.length > 0) {
p.parent[key] = v[0].url || ''
}
},
}) })
) }
const inputNode = h(ElInput, {
modelValue: value,
...({ type: 'textarea', autosize: { minRows: 1, maxRows: 6 } }),
'onUpdate:modelValue': (v: string) => { p.parent[key] = v },
})
const formItemNode = [
uploadNode || inputNode,
]
const zhVal = p.zhParent?.[key]
const showTranslateBtn = p.lang === 'EN' && typeof zhVal === 'string' && hasChinese(zhVal)
if (showTranslateBtn) {
return h(ElFormItem, { label, class: 'jfd-form-item' }, () =>
h('div', { class: 'jfd-field-with-translate' }, [
formItemNode,
h(ElButton, {
link: true, size: 'small', class: 'jfd-translate-icon',
loading: translatingPath.value === `${key}`,
onClick: () => handleTranslateField(p.parent, key, p.zhParent),
}, () => h(ElIcon, { color: '#409eff' }, () => h(Promotion)))
])
)
}
return h(ElFormItem, { label, class: 'jfd-form-item' }, () => formItemNode)
} }
if (Array.isArray(value)) { if (Array.isArray(value)) {
@ -192,18 +362,39 @@ const NodeEditor: Component = defineComponent({
onClick: () => value.push(''), onClick: () => value.push(''),
}, () => [h(ElIcon, { size: 14 }, () => h(Plus)), ' 添加']) }, () => [h(ElIcon, { size: 14 }, () => h(Plus)), ' 添加'])
]), ]),
...value.map((_: any, i: number) => ...value.map((_: any, i: number) => {
h('div', { class: 'jfd-string-item', key: i }, [ const zhArr = p.zhParent?.[key]
const zhStr = Array.isArray(zhArr) && typeof zhArr[i] === 'string' ? zhArr[i] : undefined
const showBtn = p.lang === 'EN' && zhStr && hasChinese(zhStr)
return h('div', { class: 'jfd-string-item', key: i }, [
h(ElInput, { h(ElInput, {
modelValue: value[i], modelValue: value[i],
'onUpdate:modelValue': (v: string) => { value[i] = v }, 'onUpdate:modelValue': (v: string) => { value[i] = v },
}), }),
showBtn ? h(ElButton, {
link: true, size: 'small', class: 'jfd-translate-icon',
loading: translatingPath.value === `${key}-${i}`,
onClick: async () => {
translatingPath.value = `${key}-${i}`
try {
const res = await translateApi.translate({ text: zhStr!, from: 'ZH', to: 'EN' })
const translated = parseTranslateResult(res)
if (translated) value[i] = translated
} catch {
ElMessage.error('翻译失败')
} finally {
translatingPath.value = ''
}
},
}, () => h(ElIcon, { color: '#409eff' }, () => h(Promotion))) : null,
h(ElButton, { h(ElButton, {
size: 'small', type: 'danger', link: true, size: 'small', type: 'danger', link: true,
onClick: () => value.splice(i, 1), onClick: () => value.splice(i, 1),
}, () => h(ElIcon, { size: 14 }, () => h(Delete))) }, () => h(ElIcon, { size: 14 }, () => h(Delete)))
]) ])
) })
]) ])
} }
@ -222,15 +413,17 @@ const NodeEditor: Component = defineComponent({
]), ]),
h(ElCollapse, { class: 'jfd-obj-array' }, () => h(ElCollapse, { class: 'jfd-obj-array' }, () =>
value.map((item: any, i: number) => { value.map((item: any, i: number) => {
const tag = item.title || item.name || item.text || '' const tag = item.title || item.name || item.text || item.label || item.year || ''
return h(ElCollapseItem, { return h(ElCollapseItem, {
title: `${label} #${i + 1}${tag ? ' - ' + tag : ''}`, title: `${label} #${i + 1}${tag ? ' - ' + tag : ''}`,
name: `${String(key)}-${i}`, name: `${String(key)}-${i}`,
key: i, key: i,
}, () => [ }, () => [
...Object.keys(item).map(k => ...Object.keys(item).map(k => {
h(NodeEditor, { parent: item, fieldKey: k, fieldMap: fm, depth: depth + 1, key: k }) const zhArr = p.zhParent?.[key]
), const zhItem = Array.isArray(zhArr) ? zhArr[i] : undefined
return h(NodeEditor, { parent: item, fieldKey: k, fieldMap: fm, depth: depth + 1, key: k, lang: p.lang, zhParent: zhItem })
}),
h('div', { class: 'jfd-item-actions' }, h('div', { class: 'jfd-item-actions' },
h(ElButton, { h(ElButton, {
size: 'small', type: 'danger', size: 'small', type: 'danger',
@ -244,8 +437,9 @@ const NodeEditor: Component = defineComponent({
} }
if (typeof value === 'object') { if (typeof value === 'object') {
const zhObj = p.zhParent?.[key]
const children = Object.keys(value).map(k => const children = Object.keys(value).map(k =>
h(NodeEditor, { parent: value, fieldKey: k, fieldMap: fm, depth: depth + 1, key: k }) h(NodeEditor, { parent: value, fieldKey: k, fieldMap: fm, depth: depth + 1, key: k, lang: p.lang, zhParent: zhObj })
) )
if (depth === 0) { if (depth === 0) {
@ -358,6 +552,23 @@ const NodeEditor: Component = defineComponent({
margin-bottom: 8px; margin-bottom: 8px;
} }
.jfd-field-with-translate {
display: flex;
align-items: flex-start;
gap: 8px;
width: 100%;
.el-input,
.el-textarea {
flex: 1;
}
.jfd-translate-icon {
flex-shrink: 0;
padding: 8px 0;
}
}
.jfd-obj-array .el-collapse-item__header { .jfd-obj-array .el-collapse-item__header {
font-size: 13px; font-size: 13px;
} }

View File

@ -54,6 +54,9 @@ const data: TableData = {
addNods: [ addNods: [
(self: any) => { (self: any) => {
return self.bean && <JsonFormDialog return self.bean && <JsonFormDialog
uploadFile={async (file: FormData) => {
return await self.api?.upload(file)
}}
show={self.bean.showJsonFormDialog} show={self.bean.showJsonFormDialog}
title={self.bean?.currentRow?.name || '配置'} title={self.bean?.currentRow?.name || '配置'}
data={self.bean.jsonFormData} data={self.bean.jsonFormData}
@ -65,6 +68,7 @@ const data: TableData = {
await self.api?.updateData({ await self.api?.updateData({
id: String(self.bean.currentRow.id), id: String(self.bean.currentRow.id),
content: JSON.stringify(data), content: JSON.stringify(data),
pid: String(self.bean.currentRow.pid)
}) })
self.bean.showJsonFormDialog = false self.bean.showJsonFormDialog = false
self.methods.fetchData() self.methods.fetchData()
@ -134,7 +138,7 @@ const data: TableData = {
{ {
key: 'id', key: 'id',
name: 'ID', name: 'ID',
width: '80px', width: '100px',
showJson: '*' showJson: '*'
}, },
{ {
@ -159,8 +163,8 @@ const data: TableData = {
width: '200px', width: '200px',
editor: { editor: {
type: 'json', type: 'json',
subFun(self, data) { subFun(self, data, row) {
return self.api?.updateData({...data, id: String(data.id)}) return self.api?.updateData({...data, id: String(data.id), pid: String(row.pid)})
} }
} }
}, },
@ -195,6 +199,7 @@ const data: TableData = {
key: 'weight', key: 'weight',
name: '权重', name: '权重',
width: '100px', width: '100px',
sort: 'desc',
editor: { editor: {
type: 'input', type: 'input',
subFun(self, data, row) { subFun(self, data, row) {

View File

@ -83,6 +83,13 @@ const data: TableData = {
must: true, must: true,
shouldTranslate: false, shouldTranslate: false,
}, },
{
name: '封面',
key: 'cover',
type: 'upload:image',
must: false,
shouldTranslate: false,
},
{ {
name: '文件名', name: '文件名',
key: 'file_name', key: 'file_name',
@ -136,6 +143,7 @@ const data: TableData = {
return self.bean ? self.bean.docTypes : [] return self.bean ? self.bean.docTypes : []
}, },
}, },
{ {
key: 'file_name', key: 'file_name',
name: '文件名', name: '文件名',
@ -162,6 +170,12 @@ const data: TableData = {
width: '80px', width: '80px',
showJson: '*' showJson: '*'
}, },
{
key: 'cover',
name: '封面',
type: 'input',
image: true,
},
{ {
key: 'name', key: 'name',
name: '文件名', name: '文件名',

View File

@ -26,7 +26,11 @@ const data: TableData = {
self.bean.id = null; self.bean.id = null;
}} }}
onSubmit={async (data) => { onSubmit={async (data) => {
await self.api?.addData(data) if(data.id) {
await self.api?.updateData({...data, id: String(data.id), category_id: String(data.category_id)})
} else {
await self.api?.addData({...data, category_id: String(data.category_id)})
}
self.bean.showEditorDialog = false; self.bean.showEditorDialog = false;
self.bean.id = null; self.bean.id = null;
self.methods.fetchData() self.methods.fetchData()

View File

@ -12,6 +12,7 @@ const data: TableData = {
return ( return (
self.bean && self.bean &&
<In18FormDialog <In18FormDialog
fullscreen={true}
show={self.bean.showEditorDialog} show={self.bean.showEditorDialog}
id={self.bean.id} id={self.bean.id}
form={self.bean.form} form={self.bean.form}
@ -44,7 +45,10 @@ const data: TableData = {
Object.keys(langData).forEach((fieldKey) => { Object.keys(langData).forEach((fieldKey) => {
const field = self.bean.form.find((f:any) => f.key === fieldKey); const field = self.bean.form.find((f:any) => f.key === fieldKey);
if (fieldKey === 'covers') { if (fieldKey === 'covers') {
data[langKey]['cover_resource'] = langData[fieldKey]; data[langKey]['cover_resource'] = {
image: langData[fieldKey].image.map((item: any) => ({url: item})).filter(Boolean),
video: langData[fieldKey].video.map((item: any) => ({url: item})).filter(Boolean),
}
} }
if (!field) return; if (!field) return;
data[langKey][fieldKey] = langData[fieldKey]; data[langKey][fieldKey] = langData[fieldKey];
@ -107,7 +111,7 @@ const data: TableData = {
{ {
name: '内容', name: '内容',
key: 'content', key: 'content',
type: 'textarea', type: 'richtext',
value: '', value: '',
must: true, must: true,
shouldTranslate: true, shouldTranslate: true,
@ -234,7 +238,8 @@ const data: TableData = {
{ {
key: 'create_time', key: 'create_time',
name: '发布时间', name: '发布时间',
width: '150px' width: '150px',
sort: 'desc'
}, },
{ {
key: 'count', key: 'count',