This commit is contained in:
zhangjianjun 2026-03-25 14:51:25 +08:00
parent 72fb06a4cf
commit 8b4832ca45
6 changed files with 454 additions and 55 deletions

View File

@ -20,7 +20,7 @@
</div> </div>
</template> </template>
<el-form :key="forceUpdateKey" v-if="show" ref="formRef" :model="formData" :rules="formRules" <el-form :key="forceUpdateKey" v-if="show" ref="formRef" :model="formData" :rules="formRules"
label-position="left" v-loading="detailLoading"> label-position="left" v-loading="detailLoading" style="border-top: 1px solid var(--el-border-color);">
<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">
@ -37,28 +37,28 @@
:value="opt.key" /> :value="opt.key" />
</el-select> </el-select>
<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" v-if="item.type.includes('image')">
<div class="upload-label">图片</div> <div class="upload-label">图片</div>
<UploadInput :value="formData[item.key].image" <UploadInput :value="formData[item.key].image"
:type="item.type.includes('images') ? 'images' : 'image'" :uploadFile="uploadFun" :type="item.type.includes('images') ? 'images' : 'image'" :uploadFile="uploadFun"
@updateValue="(val: any) => { hideUploadButton :onUpdateValue="(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.map((item: any) => item.url).filter(Boolean); formData[item.key].image = val.map((item: any) => item.url).filter(Boolean);
}" /> }" />
</div> </div>
<div class="upload-group"> <div class="upload-group" v-if="item.type.includes('video')">
<div class="upload-label">视频</div> <div class="upload-label">视频</div>
<UploadInput :value="formData[item.key]?.video" type="video" :uploadFile="uploadFun" <UploadInput :value="formData[item.key]?.video" type="video" :uploadFile="uploadFun"
@updateValue="(val: any) => { :onUpdateValue="(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.map((item: any) => item.url).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 :value="formData[item.key]" type="file" :accept="item.accept || '*'" <UploadInput :value="formData[item.key] && [{ url: formData[item.key], name: formData[item.key] }]" type="file" :accept="item.accept || '*'"
:uploadFile="uploadFun" :uploadFile="uploadFun" hideUploadButton
@updateValue="(val: any) => { formData[item.key] = val[0]?.url || '' }" /> :onUpdateValue="(val: any) => { formData[item.key] = val[0]?.url || '' }" />
</div> </div>
</el-form-item> </el-form-item>
</div> </div>
@ -90,15 +90,10 @@
v-model="formData.translations[primaryLocaleKey][item.key]" type="textarea" :rows="4" v-model="formData.translations[primaryLocaleKey][item.key]" type="textarea" :rows="4"
:placeholder="`请输入${item.name}`" /> :placeholder="`请输入${item.name}`" />
<WangEditor v-else-if="item.type === 'richtext'" <WangEditor v-else-if="item.type === 'richtext'"
:content="formData.translations[primaryLocaleKey][item.key]" :content="formData.translations[primaryLocaleKey][item.key]" :maxHeight="300"
:maxHeight="300" style="z-index: 9999" :key="item.key" :upload-file="uploadFun" @update:content="(val: string) => {
style="z-index: 9999"
:key="item.key"
:upload-file="uploadFun"
@update:content="(val: string) => {
formData.translations[primaryLocaleKey][item.key] = val; formData.translations[primaryLocaleKey][item.key] = val;
}" }">
>
</WangEditor> </WangEditor>
<el-select v-else-if="item.type === 'select'" <el-select v-else-if="item.type === 'select'"
v-model="formData.translations[primaryLocaleKey][item.key]" :placeholder="`请选择${item.name}`" v-model="formData.translations[primaryLocaleKey][item.key]" :placeholder="`请选择${item.name}`"
@ -106,6 +101,11 @@
<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>
<div v-else-if="item.type === 'upload:file'" style="width:100%;" class="upload-file">
<UploadInput :value="formData.translations[primaryLocaleKey][item.key]" type="file"
:accept="item.accept || '*'" :uploadFile="uploadFun" hideUploadButton
:onUpdateValue="(val: any) => { formData.translations[primaryLocaleKey][item.key] = val[0]?.url || '' }" />
</div>
</el-form-item> </el-form-item>
</div> </div>
<div class="in18-form-center"> <div class="in18-form-center">
@ -132,16 +132,11 @@
</el-button> </el-button>
</div> </div>
<div v-else-if="item.type === 'richtext'" class="field-with-translate"> <div v-else-if="item.type === 'richtext'" class="field-with-translate">
<WangEditor <WangEditor :content="formData.translations[activeSecondaryLocale?.key || ''][item.key]"
:content="formData.translations[activeSecondaryLocale?.key || ''][item.key]" :maxHeight="300" style="z-index: 9999" :key="item.key" :upload-file="uploadFun"
:maxHeight="300"
style="z-index: 9999"
:key="item.key"
:upload-file="uploadFun"
@update:content="(val: string) => { @update:content="(val: string) => {
formData.translations[activeSecondaryLocale?.key || ''][item.key] = val; formData.translations[activeSecondaryLocale?.key || ''][item.key] = val;
}" }">
>
</WangEditor> </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)"
@ -157,6 +152,11 @@
<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>
<div v-else-if="item.type === 'upload:file'" style="width:100%;" class="upload-file">
<UploadInput :value="formData.translations[activeSecondaryLocale?.key || ''][item.key]" type="file"
:accept="item.accept || '*'" :uploadFile="uploadFun" hideUploadButton
:onUpdateValue="(val: any) => { formData.translations[activeSecondaryLocale?.key || ''][item.key] = val[0]?.url || '' }" />
</div>
</el-form-item> </el-form-item>
</div> </div>
</div> </div>
@ -174,7 +174,7 @@ import { reactive, computed, watch, ref, nextTick } from 'vue'
import type { FormInstance, FormRules } from 'element-plus' 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 "./UploadInput.vue";
import WangEditor from "lib/components/WangEditor.vue"; import WangEditor from "lib/components/WangEditor.vue";
type FormItemType = type FormItemType =
| 'input' | 'input'
@ -206,7 +206,7 @@ type LocaleItem = {
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
fullscreen: boolean fullscreen?: boolean
show: boolean show: boolean
id: string | number id: string | number
title?: string title?: string
@ -239,7 +239,7 @@ const normalFields = computed(() => props.form.filter((f) => !f.shouldTranslate)
const translateFields = computed(() => props.form.filter((f) => f.shouldTranslate)) const translateFields = computed(() => props.form.filter((f) => f.shouldTranslate))
function isUploadMediaType(type: string): boolean { function isUploadMediaType(type: string): boolean {
return type === 'upload:images,video' || type === 'upload:image,video' return type === 'upload:images,video' || type === 'upload:image,video' || type === 'upload:image' || type === 'upload:video'
} }
function getSelectItems(item: FormItem): { key: string; name: string }[] { function getSelectItems(item: FormItem): { key: string; name: string }[] {
@ -250,6 +250,68 @@ function parseTranslateResult(res: any): string {
return res?.data?.text ?? res?.data ?? res?.result ?? (typeof res === 'string' ? res : '') return res?.data?.text ?? res?.data ?? res?.result ?? (typeof res === 'string' ? res : '')
} }
function extractMediaFromHtml(html: string): { textWithPlaceholders: string; mediaParts: string[] } {
if (!html || typeof html !== 'string') {
return { textWithPlaceholders: html || '', mediaParts: [] }
}
if (!html.includes('<')) {
return { textWithPlaceholders: html, mediaParts: [] }
}
const div = document.createElement('div')
div.innerHTML = html
const mediaParts: string[] = []
const mediaElements = Array.from(div.querySelectorAll('img, video, iframe, audio'))
mediaElements.forEach((el, index) => {
mediaParts.push((el as HTMLElement).outerHTML)
// 使 HTML API
const placeholder = document.createTextNode(`<x-media id="${index}"/>`)
el.parentNode?.replaceChild(placeholder, el)
})
return {
textWithPlaceholders: div.innerHTML,
mediaParts
}
}
// API split
const PLACEHOLDER_SPLIT_REGEX = /(&lt;x-media\s+id\s*=\s*["']?\d+["']?\s*\/&gt;)/gi
function reassembleHtmlWithMedia(translatedText: string, mediaParts: string[]): string {
if (!mediaParts.length) return translatedText
let result = translatedText
mediaParts.forEach((media, index) => {
// API
const placeholderRegex = new RegExp(
`&lt;x-media\\s+id\\s*=\\s*["']?${index}["']?\\s*\\/&gt;`,
'gi'
)
result = result.replace(placeholderRegex, media)
})
return result
}
/** 方案二:按占位符拆分,只翻译文本段落,占位符不发给 API */
async function translateRichtextBySegments(
textWithPlaceholders: string,
mediaParts: string[],
fromKey: string,
toKey: string
): Promise<string> {
const segments = textWithPlaceholders.split(PLACEHOLDER_SPLIT_REGEX)
const placeholderTest = /^&lt;x-media\s+id\s*=\s*["']?\d+["']?\s*\/&gt;$/i
const translatedSegments: string[] = []
for (const seg of segments) {
if (seg === '' || placeholderTest.test(seg)) {
translatedSegments.push(seg)
} else {
const res = await props.translateApi({ text: seg, from: fromKey, to: toKey })
translatedSegments.push(parseTranslateResult(res) || seg)
}
}
const concatenated = translatedSegments.join('')
return reassembleHtmlWithMedia(concatenated, mediaParts)
}
const formRef = ref<FormInstance>() const formRef = ref<FormInstance>()
const formRules = computed<FormRules>(() => { const formRules = computed<FormRules>(() => {
@ -391,12 +453,17 @@ async function handleTranslate() {
const fromKey = primaryLocale.value.key const fromKey = primaryLocale.value.key
const toKey = activeSecondaryLocale.value.key const toKey = activeSecondaryLocale.value.key
const texts: { key: string; text: string }[] = [] const texts: { key: string; text: string; type: FormItemType; mediaParts?: string[] }[] = []
translateFields.value.forEach((item) => { translateFields.value.forEach((item) => {
const val = formData.translations?.[fromKey]?.[item.key] const val = formData.translations?.[fromKey]?.[item.key]
if (val != null && String(val).trim()) { if (val != null && String(val).trim() && !val.startsWith('http')) {
texts.push({ key: item.key, text: String(val).trim() }) if (item.type === 'richtext') {
const { textWithPlaceholders, mediaParts } = extractMediaFromHtml(String(val).trim())
texts.push({ key: item.key, text: textWithPlaceholders, type: item.type, mediaParts })
} else {
texts.push({ key: item.key, text: String(val).trim(), type: item.type })
}
} }
}) })
@ -405,9 +472,15 @@ async function handleTranslate() {
translating.value = true translating.value = true
try { try {
const results = await Promise.allSettled( const results = await Promise.allSettled(
texts.map(async ({ key, text }) => { texts.map(async ({ key, text, type, mediaParts }) => {
const res = await props.translateApi({ text, from: fromKey, to: toKey }) let translated: string
return { key, translated: parseTranslateResult(res) } if (type === 'richtext' && mediaParts && mediaParts.length > 0) {
translated = await translateRichtextBySegments(text, mediaParts, fromKey, toKey)
} else {
const res = await props.translateApi({ text, from: fromKey, to: toKey })
translated = parseTranslateResult(res)
}
return { key, translated }
}) })
) )
let failCount = 0 let failCount = 0
@ -438,10 +511,25 @@ async function handleTranslateField(itemKey: string) {
const toKey = activeSecondaryLocale.value.key const toKey = activeSecondaryLocale.value.key
const text = formData.translations?.[fromKey]?.[itemKey] const text = formData.translations?.[fromKey]?.[itemKey]
if (text == null || !String(text).trim()) return if (text == null || !String(text).trim()) return
const item = translateFields.value.find((f) => f.key === itemKey)
const isRichtext = item?.type === 'richtext'
let textToTranslate = String(text).trim()
let mediaParts: string[] = []
if (isRichtext) {
const extracted = extractMediaFromHtml(textToTranslate)
textToTranslate = extracted.textWithPlaceholders
mediaParts = extracted.mediaParts
}
translatingFieldKey.value = itemKey translatingFieldKey.value = itemKey
try { try {
const res = await props.translateApi({ text: String(text).trim(), from: fromKey, to: toKey }) let translated: string
if (formData.translations[toKey]) formData.translations[toKey][itemKey] = parseTranslateResult(res) if (isRichtext && mediaParts.length > 0) {
translated = await translateRichtextBySegments(textToTranslate, mediaParts, fromKey, toKey)
} else {
const res = await props.translateApi({ text: textToTranslate, from: fromKey, to: toKey })
translated = parseTranslateResult(res)
}
if (formData.translations[toKey]) formData.translations[toKey][itemKey] = translated
} catch (e) { } catch (e) {
console.error('Translate failed:', e) console.error('Translate failed:', e)
ElMessage.error('翻译失败') ElMessage.error('翻译失败')

View File

@ -73,8 +73,8 @@ import {
ElCollapse, ElCollapseItem, ElIcon, ElMessage ElCollapse, ElCollapseItem, ElIcon, ElMessage
} from 'element-plus' } from 'element-plus'
import { translateApi } from 'src/api/common' import { translateApi } from 'src/api/common'
import UploadInput from 'lib/components/UploadInput.vue' import UploadInput from './UploadInput.vue'
import WangEditor from 'lib/components/WangEditor.vue'
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
show: boolean show: boolean
title?: string title?: string
@ -298,19 +298,23 @@ const NodeEditor: Component = defineComponent({
} }
if (typeof value === 'string') { if (typeof value === 'string') {
// HTML </p></div>
const hasHtmlClosingTag = /<\/[a-zA-Z][\w:-]*\s*>/i.test(value)
// const isLong = value.length > 50 // const isLong = value.length > 50
const needUpload = value.startsWith('http') // const needUpload = value.startsWith('http')
const isImage = needUpload && value.includes('.jpg') || value.includes('.png') || value.includes('.jpeg') || value.includes('.gif') || value.includes('.bmp') || value.includes('.webp') const isImage = value.includes('.jpg') || value.includes('.png') || value.includes('.jpeg') || value.includes('.gif') || value.includes('.bmp') || value.includes('.webp')
const isVideo = needUpload && value.includes('.mp4') || value.includes('.avi') || value.includes('.mov') || value.includes('.wmv') || value.includes('.flv') || value.includes('.mkv') const isVideo = value.includes('.mp4') || value.includes('.avi') || value.includes('.mov') || value.includes('.wmv') || value.includes('.flv') || value.includes('.mkv')
const isFile = needUpload && value.includes('.pdf') || value.includes('.doc') || value.includes('.docx') || value.includes('.xls') || value.includes('.xlsx') || value.includes('.ppt') || value.includes('.pptx') const isFile = value.includes('.pdf') || value.includes('.doc') || value.includes('.docx') || value.includes('.xls') || value.includes('.xlsx') || value.includes('.ppt') || value.includes('.pptx')
let uploadNode = null; let uploadNode = null;
if (needUpload && (isFile || isImage || isVideo)) { if ((isFile || isImage || isVideo)) {
uploadNode = h(UploadInput, { uploadNode = h(UploadInput, {
value: [{url: value}], value: [{url: value}],
type: isImage ? 'image' : isVideo ? 'video' : 'file', type: isImage ? 'image' : isVideo ? 'video' : 'file',
uploadFile: props.uploadFile, uploadFile: props.uploadFile,
hideUploadButton: true,
transport: `#jfd-${key}`, transport: `#jfd-${key}`,
onUpdateValue: (v: any) => { onUpdateValue: (v: any) => {
if(v.length > 0) { if(v.length > 0) {
@ -325,8 +329,19 @@ const NodeEditor: Component = defineComponent({
'onUpdate:modelValue': (v: string) => { p.parent[key] = v }, 'onUpdate:modelValue': (v: string) => { p.parent[key] = v },
}) })
const wangEditor = h(WangEditor, {
content: value,
showSaveButton: false,
transport: `#jfd-${key}`,
'onUpdate:content': (v: any) => {
if(v.length > 0) {
p.parent[key] = v || ''
}
},
})
const formItemNode = [ const formItemNode = [
uploadNode || inputNode, uploadNode || (hasHtmlClosingTag ? wangEditor : inputNode),
] ]
@ -367,9 +382,26 @@ const NodeEditor: Component = defineComponent({
const zhStr = Array.isArray(zhArr) && typeof zhArr[i] === 'string' ? zhArr[i] : undefined const zhStr = Array.isArray(zhArr) && typeof zhArr[i] === 'string' ? zhArr[i] : undefined
const showBtn = p.lang === 'EN' && zhStr && hasChinese(zhStr) const showBtn = p.lang === 'EN' && zhStr && hasChinese(zhStr)
const isImage = value[i].includes('.jpg') || value[i].includes('.png') || value[i].includes('.jpeg') || value[i].includes('.gif') || value[i].includes('.bmp') || value[i].includes('.webp')
const isVideo = value[i].includes('.mp4') || value[i].includes('.avi') || value[i].includes('.mov') || value[i].includes('.wmv') || value[i].includes('.flv') || value[i].includes('.mkv')
const isFile = value[i].includes('.pdf') || value[i].includes('.doc') || value[i].includes('.docx') || value[i].includes('.xls') || value[i].includes('.xlsx') || value[i].includes('.ppt') || value[i].includes('.pptx')
let uploadNode = null;
if ((isFile || isImage || isVideo)) {
uploadNode = h(UploadInput, {
value: [{url: value[i], name: value[i]}],
type: isImage ? 'image' : isVideo ? 'video' : 'file',
uploadFile: props.uploadFile,
hideUploadButton: true,
transport: `#jfd-${key}`,
onUpdateValue: (v: any) => {
if(v.length > 0) {
value[i] = v[0].url || ''
}
},
})
}
return h('div', { class: 'jfd-string-item', key: i }, [ return h('div', { class: 'jfd-string-item', key: i }, [
h(ElInput, { uploadNode || h(ElInput, {
modelValue: value[i], modelValue: value[i],
'onUpdate:modelValue': (v: string) => { value[i] = v }, 'onUpdate:modelValue': (v: string) => { value[i] = v },
}), }),
@ -392,7 +424,7 @@ const NodeEditor: Component = defineComponent({
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))),
]) ])
}) })
]) ])
@ -424,12 +456,12 @@ const NodeEditor: Component = defineComponent({
const zhItem = Array.isArray(zhArr) ? zhArr[i] : undefined 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 }) 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',
onClick: () => value.splice(i, 1), // onClick: () => value.splice(i, 1),
}, () => [h(ElIcon, { size: 14 }, () => h(Delete)), ' 删除此项']) // }, () => [h(ElIcon, { size: 14 }, () => h(Delete)), ' '])
) // )
]) ])
}) })
) )
@ -572,4 +604,5 @@ const NodeEditor: Component = defineComponent({
.jfd-obj-array .el-collapse-item__header { .jfd-obj-array .el-collapse-item__header {
font-size: 13px; font-size: 13px;
} }
</style> </style>

260
components/UploadInput.vue Normal file
View File

@ -0,0 +1,260 @@
<template>
<div v-loading="isUploading" element-loading-text="上传中..." class="upload-loading-wrapper">
<el-upload ref="uploadRef" v-model:file-list="fileList" action="*" :on-preview="handlePictureCardPreview"
v-bind="options" :http-request="handleHttpRequest" :on-change="handleChange"
:class="isMaxCount ? 'max-count' : ''" style="width: 100%">
<el-icon v-show="['image', 'images'].includes(type)">
<Plus />
</el-icon>
<div v-show="['file', 'files', 'video', 'videos'].includes(type)">
<el-icon class="el-icon--upload">
<upload-filled />
</el-icon>
<div class="el-upload__text">拖拽到这里或者 <em>点击上传</em></div>
</div>
</el-upload>
<el-button v-show="showUploadButton" type="primary" @click="handleUpload">上传</el-button>
</div>
</template>
<script lang="ts" setup>
import {
computed,
ref,
createVNode,
render,
} from "vue";
import { Plus } from "@element-plus/icons-vue";
import type { UploadFile, UploadFiles, UploadUserFile } from "element-plus";
import { ElImageViewer } from "element-plus";
import utils from "lib/utils";
interface Props {
value: {
data?: FormData;
uid?: string;
url: string;
name?: string;
id?: string;
}[];
uploadFile: (file: FormData) => Promise<any>;
type?: "image" | "images" | "file" | "files" | "video" | "videos";
transport?: string;
accept?: string;
}
const props = withDefaults(defineProps<Props>(), {
type: "image",
transport: "",
accept: "*",
value: [] as any,
uploadFile: () => Promise.resolve({ url: "" }),
});
const showUploadButton = computed(() => {
return options.value.autoUpload === false;
});
const emit = defineEmits(["updateValue"])
const fileList = ref<UploadUserFile[]>([...props.value] as any);
const uploadRef = ref<any>(null);
const options = computed(() => {
switch (props.type) {
case "image":
return {
limit: 1,
listType: "picture-card" as const, //
accept: "image/*",
};
case "images":
return {
limit: 9,
listType: "picture-card" as const, //
accept: "image/*",
multiple: true,
autoUpload: false,
};
case "video":
return {
limit: 1,
listType: "text" as const, //
accept: "video/*",
};
case "videos":
return {
limit: 9,
listType: "text" as const, //
accept: "video/*",
multiple: true,
autoUpload: false,
};
case "file":
return {
limit: 1,
accept: props.accept,
drag: true,
listType: "text" as const, // listType
};
case "files":
return {
limit: 10,
accept: props.accept,
drag: true,
listType: "text" as const, // listType
multiple: true,
autoUpload: false,
};
default:
return {
limit: 1,
listType: "picture-card" as const,
accept: props.accept,
};
}
});
const isMaxCount = computed(() => {
return fileList.value.length >= options.value.limit;
});
const isUploading = ref(false);
interface UploadRequestOptions {
file: File & { uid?: number; raw?: File, url?: string };
filename: string;
onSuccess: (res: any) => void;
onError: (err: any) => void;
}
const handleHttpRequest = async (options: UploadRequestOptions) => {
isUploading.value = true;
const formData = new FormData();
const rawFile = options.file?.raw ?? options.file;
formData.append(options.filename || "file", rawFile, rawFile.name);
try {
const res = await props.uploadFile(formData);
const url = res?.data?.url ?? res?.url;
if (url) {
options.file.url = url;
}
options.onSuccess(res);
isUploading.value = false;
} catch (err) {
console.log('err', err)
options.onError(err as any);
isUploading.value = false;
}
};
const handleRemove = async (uploadFile: UploadFile) => {
// props.onFileChange(uploadFile);
// emit("onUpdateValue", fileList.value.filter((f: any) => f.uid !== uploadFile.uid));
};
const handleChange = (file: any, list: any) => {
const updateList = fileList.value
.filter((f: any) => f.status === 'success')
.map((f: any) => {
return {
url: f.raw?.url,
uid: f.raw?.uid,
name: f.raw?.name,
id: f.raw?.id,
}
})
emit("updateValue", updateList);
}
const handlePictureCardPreview = (file: any) => {
const imageUrl = file.url;
if (["file", "files"].includes(props.type)) {
//
utils.downloadfile(imageUrl, imageUrl, "image/png");
return;
}
//
if (["video", "videos"].includes(props.type)) {
window.open(imageUrl, "_blank");
return;
}
if (!imageUrl) return;
// el-image-viewer
const container = document.createElement("div");
const parentNode =
document.querySelector(props.transport || "body") || document.body;
parentNode.appendChild(container);
// el-image-viewer
const vnode = createVNode(ElImageViewer as any, {
urlList: [imageUrl],
onClose: () => {
render(null, container);
parentNode.removeChild(container);
},
});
render(vnode, container);
};
const handleUpload = () => {
uploadRef.value.submit();
};
</script>
<style lang="less" scoped>
.upload-loading-wrapper {
position: relative;
min-height: 100px;
}
:deep(.el-upload--picture-card) {
--el-upload-picture-card-size: 100px;
}
:deep(.el-upload-list--picture-card) {
--el-upload-list-picture-card-size: 100px;
}
:deep(.el-icon--close-tip) {
display: none !important;
}
//
:deep(.el-upload-list__item.is-uploading .el-progress) {
.el-progress-circle {
transform: scale(0.7);
}
}
.max-count {
:deep(.el-upload--picture-card) {
display: none;
}
:deep(.el-upload) {
display: none;
}
}
:deep(.el-upload.el-upload--text) {
border: 1px solid #dcdfe6;
border-radius: 4px;
width: 100%;
min-height: 100px;
&>div {
display: flex;
flex-direction: column;
align-items: center;
.el-icon.el-icon--upload {
font-size: 20px;
}
}
}
</style>

View File

@ -55,6 +55,7 @@ const data: TableData = {
(self: any) => { (self: any) => {
return self.bean && <JsonFormDialog return self.bean && <JsonFormDialog
uploadFile={async (file: FormData) => { uploadFile={async (file: FormData) => {
console.log('-------')
return await self.api?.upload(file) return await self.api?.upload(file)
}} }}
show={self.bean.showJsonFormDialog} show={self.bean.showJsonFormDialog}

View File

@ -28,12 +28,23 @@ const data: TableData = {
onSubmit={async (data) => { onSubmit={async (data) => {
const params = { const params = {
...data, ...data,
id: data.id ? String(data.id) : '',
category_id: data.category_id ? String(data.category_id) : '', category_id: data.category_id ? String(data.category_id) : '',
id: data.id ? String(data.id) : '',
} }
params.translations.EN.path = params.path
params.translations.ZH.path = params.path
delete params.path
params.translations.ZH.cover = params.cover.image[0] || ''
params.translations.EN.cover = params.cover.image[0] || ''
delete params.cover
if(data.id) { if(data.id) {
await self.api?.updateData(params) await self.api?.updateData(params)
} else { } else {
delete params.id
await self.api?.addData(params) await self.api?.addData(params)
} }
self.bean.showEditorDialog = false; self.bean.showEditorDialog = false;
@ -51,6 +62,9 @@ const data: TableData = {
if(fieldKey === 'name') { if(fieldKey === 'name') {
data[langKey]['file_name'] = langData[fieldKey] data[langKey]['file_name'] = langData[fieldKey]
} }
if(fieldKey === 'cover') {
data[langKey]['cover'] = {image: data[langKey]['cover'] ? [{url: data[langKey]['cover']}] : []}
}
if (!field) return; if (!field) return;
data[langKey][fieldKey] = langData[fieldKey]; data[langKey][fieldKey] = langData[fieldKey];
}) })
@ -173,7 +187,7 @@ const data: TableData = {
{ {
key: 'cover', key: 'cover',
name: '封面', name: '封面',
type: 'input', width: '80px',
image: true, image: true,
}, },
{ {

View File

@ -26,6 +26,10 @@ const data: TableData = {
...data, ...data,
id: data.id ? String(data.id) : undefined, id: data.id ? String(data.id) : undefined,
category_id: data.category_id ? String(data.category_id) : undefined, category_id: data.category_id ? String(data.category_id) : undefined,
cover_resource: {
image: data.cover_resource.image.map((i: any) => (i.url || i)),
video: data.cover_resource.video.map((i: any) => (i.url || i)),
}
} }
if(data.id) { if(data.id) {
await self.api?.updateData(params) await self.api?.updateData(params)
@ -132,7 +136,6 @@ const data: TableData = {
[self.bean.newsTypes] = results.map( [self.bean.newsTypes] = results.map(
(res) => toOptions(res?.data?.items ?? []) (res) => toOptions(res?.data?.items ?? [])
) )
console.log('newsTypes', self.bean.newsTypes)
} }
} }
], ],