This commit is contained in:
parent
72fb06a4cf
commit
8b4832ca45
|
|
@ -20,7 +20,7 @@
|
|||
</div>
|
||||
</template>
|
||||
<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">
|
||||
<el-form-item v-for="item in normalFields" :key="item.key" :prop="item.key">
|
||||
|
|
@ -37,28 +37,28 @@
|
|||
:value="opt.key" />
|
||||
</el-select>
|
||||
<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>
|
||||
<UploadInput :value="formData[item.key].image"
|
||||
: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: [] };
|
||||
formData[item.key].image = val.map((item: any) => item.url).filter(Boolean);
|
||||
}" />
|
||||
</div>
|
||||
<div class="upload-group">
|
||||
<div class="upload-group" v-if="item.type.includes('video')">
|
||||
<div class="upload-label">视频</div>
|
||||
<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: [] };
|
||||
formData[item.key].video = val.map((item: any) => item.url).filter(Boolean);
|
||||
}" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="item.type === 'upload:file'" style="width:100%;" class="upload-file">
|
||||
<UploadInput :value="formData[item.key]" type="file" :accept="item.accept || '*'"
|
||||
:uploadFile="uploadFun"
|
||||
@updateValue="(val: any) => { formData[item.key] = val[0]?.url || '' }" />
|
||||
<UploadInput :value="formData[item.key] && [{ url: formData[item.key], name: formData[item.key] }]" type="file" :accept="item.accept || '*'"
|
||||
:uploadFile="uploadFun" hideUploadButton
|
||||
:onUpdateValue="(val: any) => { formData[item.key] = val[0]?.url || '' }" />
|
||||
</div>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
|
@ -90,15 +90,10 @@
|
|||
v-model="formData.translations[primaryLocaleKey][item.key]" type="textarea" :rows="4"
|
||||
: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) => {
|
||||
: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'"
|
||||
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"
|
||||
:value="opt.key" />
|
||||
</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>
|
||||
</div>
|
||||
<div class="in18-form-center">
|
||||
|
|
@ -132,16 +132,11 @@
|
|||
</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"
|
||||
<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"
|
||||
: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"
|
||||
:value="opt.key" />
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -174,7 +174,7 @@ import { reactive, computed, watch, ref, nextTick } from 'vue'
|
|||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { ElMessage } from 'element-plus'
|
||||
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";
|
||||
type FormItemType =
|
||||
| 'input'
|
||||
|
|
@ -206,7 +206,7 @@ type LocaleItem = {
|
|||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
fullscreen: boolean
|
||||
fullscreen?: boolean
|
||||
show: boolean
|
||||
id: string | number
|
||||
title?: string
|
||||
|
|
@ -239,7 +239,7 @@ const normalFields = computed(() => props.form.filter((f) => !f.shouldTranslate)
|
|||
const translateFields = computed(() => props.form.filter((f) => f.shouldTranslate))
|
||||
|
||||
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 }[] {
|
||||
|
|
@ -250,6 +250,68 @@ function parseTranslateResult(res: any): string {
|
|||
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 = /(<x-media\s+id\s*=\s*["']?\d+["']?\s*\/>)/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(
|
||||
`<x-media\\s+id\\s*=\\s*["']?${index}["']?\\s*\\/>`,
|
||||
'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 = /^<x-media\s+id\s*=\s*["']?\d+["']?\s*\/>$/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 formRules = computed<FormRules>(() => {
|
||||
|
|
@ -391,12 +453,17 @@ async function handleTranslate() {
|
|||
|
||||
const fromKey = primaryLocale.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) => {
|
||||
const val = formData.translations?.[fromKey]?.[item.key]
|
||||
if (val != null && String(val).trim()) {
|
||||
texts.push({ key: item.key, text: String(val).trim() })
|
||||
if (val != null && String(val).trim() && !val.startsWith('http')) {
|
||||
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
|
||||
try {
|
||||
const results = await Promise.allSettled(
|
||||
texts.map(async ({ key, text }) => {
|
||||
texts.map(async ({ key, text, type, mediaParts }) => {
|
||||
let translated: string
|
||||
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 })
|
||||
return { key, translated: parseTranslateResult(res) }
|
||||
translated = parseTranslateResult(res)
|
||||
}
|
||||
return { key, translated }
|
||||
})
|
||||
)
|
||||
let failCount = 0
|
||||
|
|
@ -438,10 +511,25 @@ async function handleTranslateField(itemKey: string) {
|
|||
const toKey = activeSecondaryLocale.value.key
|
||||
const text = formData.translations?.[fromKey]?.[itemKey]
|
||||
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
|
||||
try {
|
||||
const res = await props.translateApi({ text: String(text).trim(), from: fromKey, to: toKey })
|
||||
if (formData.translations[toKey]) formData.translations[toKey][itemKey] = parseTranslateResult(res)
|
||||
let translated: string
|
||||
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) {
|
||||
console.error('Translate failed:', e)
|
||||
ElMessage.error('翻译失败')
|
||||
|
|
|
|||
|
|
@ -73,8 +73,8 @@ import {
|
|||
ElCollapse, ElCollapseItem, ElIcon, ElMessage
|
||||
} from 'element-plus'
|
||||
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<{
|
||||
show: boolean
|
||||
title?: string
|
||||
|
|
@ -298,19 +298,23 @@ const NodeEditor: Component = defineComponent({
|
|||
}
|
||||
|
||||
if (typeof value === 'string') {
|
||||
// 含任意 HTML 闭合标签(如 </p>、</div>)时用富文本编辑器
|
||||
const hasHtmlClosingTag = /<\/[a-zA-Z][\w:-]*\s*>/i.test(value)
|
||||
|
||||
// const isLong = value.length > 50
|
||||
|
||||
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 isVideo = needUpload && 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 needUpload = value.startsWith('http')
|
||||
const isImage = value.includes('.jpg') || value.includes('.png') || value.includes('.jpeg') || value.includes('.gif') || value.includes('.bmp') || value.includes('.webp')
|
||||
const isVideo = value.includes('.mp4') || value.includes('.avi') || value.includes('.mov') || value.includes('.wmv') || value.includes('.flv') || value.includes('.mkv')
|
||||
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;
|
||||
if (needUpload && (isFile || isImage || isVideo)) {
|
||||
if ((isFile || isImage || isVideo)) {
|
||||
uploadNode = h(UploadInput, {
|
||||
value: [{url: value}],
|
||||
type: isImage ? 'image' : isVideo ? 'video' : 'file',
|
||||
uploadFile: props.uploadFile,
|
||||
hideUploadButton: true,
|
||||
transport: `#jfd-${key}`,
|
||||
onUpdateValue: (v: any) => {
|
||||
if(v.length > 0) {
|
||||
|
|
@ -325,8 +329,19 @@ const NodeEditor: Component = defineComponent({
|
|||
'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 = [
|
||||
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 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 }, [
|
||||
h(ElInput, {
|
||||
uploadNode || h(ElInput, {
|
||||
modelValue: value[i],
|
||||
'onUpdate:modelValue': (v: string) => { value[i] = v },
|
||||
}),
|
||||
|
|
@ -392,7 +424,7 @@ const NodeEditor: Component = defineComponent({
|
|||
h(ElButton, {
|
||||
size: 'small', type: 'danger', link: true,
|
||||
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
|
||||
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(ElButton, {
|
||||
size: 'small', type: 'danger',
|
||||
onClick: () => value.splice(i, 1),
|
||||
}, () => [h(ElIcon, { size: 14 }, () => h(Delete)), ' 删除此项'])
|
||||
)
|
||||
// h('div', { class: 'jfd-item-actions' },
|
||||
// h(ElButton, {
|
||||
// size: 'small', type: 'danger',
|
||||
// onClick: () => value.splice(i, 1),
|
||||
// }, () => [h(ElIcon, { size: 14 }, () => h(Delete)), ' 删除此项'])
|
||||
// )
|
||||
])
|
||||
})
|
||||
)
|
||||
|
|
@ -572,4 +604,5 @@ const NodeEditor: Component = defineComponent({
|
|||
.jfd-obj-array .el-collapse-item__header {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
@ -55,6 +55,7 @@ const data: TableData = {
|
|||
(self: any) => {
|
||||
return self.bean && <JsonFormDialog
|
||||
uploadFile={async (file: FormData) => {
|
||||
console.log('-------')
|
||||
return await self.api?.upload(file)
|
||||
}}
|
||||
show={self.bean.showJsonFormDialog}
|
||||
|
|
|
|||
|
|
@ -28,12 +28,23 @@ const data: TableData = {
|
|||
onSubmit={async (data) => {
|
||||
const params = {
|
||||
...data,
|
||||
id: data.id ? String(data.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) {
|
||||
await self.api?.updateData(params)
|
||||
} else {
|
||||
delete params.id
|
||||
await self.api?.addData(params)
|
||||
}
|
||||
self.bean.showEditorDialog = false;
|
||||
|
|
@ -51,6 +62,9 @@ const data: TableData = {
|
|||
if(fieldKey === 'name') {
|
||||
data[langKey]['file_name'] = langData[fieldKey]
|
||||
}
|
||||
if(fieldKey === 'cover') {
|
||||
data[langKey]['cover'] = {image: data[langKey]['cover'] ? [{url: data[langKey]['cover']}] : []}
|
||||
}
|
||||
if (!field) return;
|
||||
data[langKey][fieldKey] = langData[fieldKey];
|
||||
})
|
||||
|
|
@ -173,7 +187,7 @@ const data: TableData = {
|
|||
{
|
||||
key: 'cover',
|
||||
name: '封面',
|
||||
type: 'input',
|
||||
width: '80px',
|
||||
image: true,
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -26,6 +26,10 @@ const data: TableData = {
|
|||
...data,
|
||||
id: data.id ? String(data.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) {
|
||||
await self.api?.updateData(params)
|
||||
|
|
@ -132,7 +136,6 @@ const data: TableData = {
|
|||
[self.bean.newsTypes] = results.map(
|
||||
(res) => toOptions(res?.data?.items ?? [])
|
||||
)
|
||||
console.log('newsTypes', self.bean.newsTypes)
|
||||
}
|
||||
}
|
||||
],
|
||||
|
|
|
|||
Loading…
Reference in New Issue