676 lines
27 KiB
Vue
676 lines
27 KiB
Vue
<template>
|
||
<el-dialog :model-value="show" width="800" :fullscreen="isFullscreen" :close-on-click-modal="false" @close="close"
|
||
:show-close="false">
|
||
<template #header>
|
||
<div class="dialog-header">
|
||
<span class="dialog-title">{{ title }}</span>
|
||
<div class="dialog-header-actions">
|
||
<el-button link circle @click="toggleFullscreen" :title="isFullscreen ? '退出全屏' : '全屏'">
|
||
<el-icon>
|
||
<CopyDocument v-if="isFullscreen" />
|
||
<FullScreen v-else />
|
||
</el-icon>
|
||
</el-button>
|
||
<el-button link circle @click="close" title="关闭">
|
||
<el-icon size="18">
|
||
<Close />
|
||
</el-icon>
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
<el-form :key="forceUpdateKey" v-if="show" ref="formRef" :model="formData" :rules="formRules"
|
||
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">
|
||
<template #label>
|
||
{{ item.name }}
|
||
</template>
|
||
<el-input v-if="item.type === 'input'" v-model="formData[item.key]"
|
||
:placeholder="`请输入${item.name}`" />
|
||
<el-input v-else-if="item.type === 'textarea'" v-model="formData[item.key]" type="textarea"
|
||
:rows="4" :placeholder="`请输入${item.name}`" />
|
||
<el-select v-else-if="item.type === 'select'" v-model="formData[item.key]"
|
||
:placeholder="`请选择${item.name}`" clearable style="width: 100%">
|
||
<el-option v-for="opt in getSelectItems(item)" :key="opt.key" :label="opt.name"
|
||
:value="opt.key" />
|
||
</el-select>
|
||
<div v-else-if="isUploadMediaType(item.type)" style="width:100%;">
|
||
<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"
|
||
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" v-if="item.type.includes('video')">
|
||
<div class="upload-label">视频</div>
|
||
<UploadInput :value="formData[item.key]?.video.map((item: any) => ({...item, name: item.url}))" type="video" :uploadFile="uploadFun"
|
||
: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] && [{ 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>
|
||
|
||
<div class="in18-form-header">
|
||
<div class="in18-form-header-left">
|
||
<span>{{ primaryLocale?.name }}</span>
|
||
</div>
|
||
<div class="in18-form-header-right">
|
||
<span v-for="locale in secondaryLocales" :key="locale.key" class="locale-tab"
|
||
:class="{ active: activeSecondaryLocale?.key === locale.key }"
|
||
@click="activeSecondaryLocale = locale">
|
||
{{ locale.name }}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="translateFields.length && secondaryLocales.length" class="in18-form-wrapper">
|
||
<div class="in18-form-left">
|
||
<el-form-item v-for="item in translateFields" :key="item.key"
|
||
:prop="`translations.${primaryLocaleKey}.${item.key}`">
|
||
<template #label>
|
||
{{ item.name }}
|
||
</template>
|
||
<el-input v-if="item.type === 'input'"
|
||
v-model="formData.translations[primaryLocaleKey][item.key]"
|
||
:placeholder="`请输入${item.name}`" />
|
||
<el-input v-else-if="item.type === 'textarea'"
|
||
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) => {
|
||
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}`"
|
||
clearable style="width: 100%">
|
||
<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">
|
||
<el-button v-if="activeSecondaryLocale" type="primary" :loading="translating"
|
||
@click="handleTranslate">
|
||
翻译
|
||
</el-button>
|
||
</div>
|
||
<div v-if="activeSecondaryLocale" class="in18-form-right">
|
||
<el-form-item v-for="item in translateFields" :key="item.key">
|
||
<template #label>
|
||
{{ item.name }}
|
||
</template>
|
||
<div v-if="item.type === 'input' || item.type === 'textarea'" class="field-with-translate">
|
||
<el-input v-model="formData.translations[activeSecondaryLocale?.key || ''][item.key]"
|
||
:type="item.type === 'textarea' ? 'textarea' : undefined"
|
||
:rows="item.type === 'textarea' ? 4 : undefined" :placeholder="`请输入${item.name}`" />
|
||
<el-button link size="small" class="translate-icon"
|
||
:loading="translatingFieldKey === item.key" @click="handleTranslateField(item.key)"
|
||
title="翻译该字段">
|
||
<el-icon color="#409eff">
|
||
<IconTranslate size={20} color="#409eff" />
|
||
</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"
|
||
:loading="translatingFieldKey === item.key" @click="handleTranslateField(item.key)"
|
||
title="翻译该字段">
|
||
<el-icon color="#409eff">
|
||
<IconTranslate size={20} color="#409eff" />
|
||
</el-icon>
|
||
</el-button>
|
||
</div>
|
||
<el-select v-else-if="item.type === 'select'"
|
||
v-model="formData.translations[activeSecondaryLocale?.key || ''][item.key]"
|
||
:placeholder="`请选择${item.name}`" clearable style="width: 100%">
|
||
<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>
|
||
</el-form>
|
||
|
||
<template #footer>
|
||
<el-button @click="close">取消</el-button>
|
||
<el-button type="primary" @click="submit">确认提交</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { reactive, computed, watch, ref, nextTick } from 'vue'
|
||
import type { FormInstance, FormRules } from 'element-plus'
|
||
import { ElMessage } from 'element-plus'
|
||
import { Close, FullScreen, CopyDocument } from '@element-plus/icons-vue'
|
||
import UploadInput from "./UploadInput.vue";
|
||
import WangEditor from "lib/components/WangEditor.vue";
|
||
import { IconTranslate } from './Icons'
|
||
type FormItemType =
|
||
| 'input'
|
||
| 'select'
|
||
| 'textarea'
|
||
| 'richtext'
|
||
| 'upload:images,video'
|
||
| 'upload:image,video'
|
||
| 'upload:file'
|
||
|
||
type FormItem = {
|
||
name: string
|
||
key: string
|
||
type: FormItemType
|
||
value: string | number | { image: string[]; video: string[] }
|
||
items?: { key: string; name: string }[]
|
||
getItems?: () => { key: string; name: string }[]
|
||
must?: boolean
|
||
shouldTranslate?: boolean
|
||
accept?: string
|
||
[prop: string]: any
|
||
}
|
||
|
||
type LocaleItem = {
|
||
name: string
|
||
key: string
|
||
isPrimary?: boolean
|
||
}
|
||
|
||
const props = withDefaults(
|
||
defineProps<{
|
||
fullscreen?: boolean
|
||
show: boolean
|
||
id: string | number
|
||
title?: string
|
||
form: FormItem[]
|
||
locales: LocaleItem[]
|
||
translateApi: (data: { text: string; from?: string; to?: string }) => Promise<any>
|
||
detailApi: (id: number) => Promise<any>
|
||
uploadFun?: (formData: FormData) => Promise<any>
|
||
}>(),
|
||
{
|
||
fullscreen: false,
|
||
show: false,
|
||
id: '',
|
||
title: '详情',
|
||
form: () => [],
|
||
locales: () => [],
|
||
translateApi: () => Promise.reject({ code: 500, message: 'translateApi not implemented' }),
|
||
detailApi: () => Promise.reject({ code: 500, message: 'detailApi not implemented' }),
|
||
uploadFun: () => Promise.reject({ code: 500, message: 'uploadFun not implemented' }),
|
||
}
|
||
)
|
||
|
||
const emit = defineEmits(['close', 'submit'])
|
||
|
||
const primaryLocale = computed(() => props.locales.find((l) => l.isPrimary) || props.locales[0])
|
||
const primaryLocaleKey = computed(() => primaryLocale.value?.key || 'ZH')
|
||
const secondaryLocales = computed(() => props.locales.filter((l) => !l.isPrimary))
|
||
const activeSecondaryLocale = ref<LocaleItem | null>(null)
|
||
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' || type === 'upload:image' || type === 'upload:video'
|
||
}
|
||
|
||
function getSelectItems(item: FormItem): { key: string; name: string }[] {
|
||
return item.items ?? item.getItems?.() ?? []
|
||
}
|
||
|
||
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>(() => {
|
||
const rules: FormRules = {}
|
||
normalFields.value.forEach((item) => {
|
||
if (item.must) {
|
||
rules[item.key] = [{ required: true, message: `请输入${item.name}`, trigger: ['blur', 'change'] }]
|
||
}
|
||
})
|
||
translateFields.value.forEach((item) => {
|
||
if (item.must) {
|
||
const prop = `translations.${primaryLocaleKey.value}.${item.key}`
|
||
rules[prop] = [{ required: true, message: `请输入${item.name}`, trigger: ['blur', 'change'] }]
|
||
}
|
||
})
|
||
return rules
|
||
})
|
||
|
||
const formData = reactive<{
|
||
[key: string]: any
|
||
translations: Record<string, Record<string, any>>
|
||
}>({
|
||
translations: {}
|
||
})
|
||
|
||
const translating = ref(false)
|
||
const translatingFieldKey = ref<string>('')
|
||
const isFullscreen = ref(false)
|
||
const detailLoading = ref(false)
|
||
|
||
|
||
const forceUpdateKey = ref(0)
|
||
function forceUpdate() {
|
||
forceUpdateKey.value++
|
||
}
|
||
|
||
function toggleFullscreen() {
|
||
isFullscreen.value = !isFullscreen.value
|
||
}
|
||
|
||
function resetFormData() {
|
||
Object.keys(formData).forEach((k) => {
|
||
if (k !== 'translations') delete formData[k]
|
||
})
|
||
formData.translations = {}
|
||
}
|
||
|
||
function initFormData() {
|
||
const normal: Record<string, any> = {}
|
||
const translations: Record<string, Record<string, any>> = {}
|
||
|
||
props.form.forEach((item) => {
|
||
if (item.shouldTranslate) {
|
||
props.locales.forEach((locale) => {
|
||
if (!translations[locale.key]) translations[locale.key] = {}
|
||
translations[locale.key][item.key] = item.value ?? ''
|
||
})
|
||
} else if (isUploadMediaType(item.type)) {
|
||
const val: any = item.value
|
||
normal[item.key] = (val && typeof val === 'object' && Array.isArray(val.image) && Array.isArray(val.video))
|
||
? val
|
||
: { image: [], video: [] }
|
||
} else {
|
||
normal[item.key] = item.value ?? ''
|
||
}
|
||
})
|
||
|
||
Object.keys(formData).forEach((k) => {
|
||
if (k !== 'translations') delete formData[k]
|
||
})
|
||
Object.keys(normal).forEach((k) => {
|
||
formData[k] = normal[k]
|
||
})
|
||
formData.translations = translations
|
||
|
||
if (secondaryLocales.value.length > 0 && !activeSecondaryLocale.value) {
|
||
activeSecondaryLocale.value = secondaryLocales.value[0]
|
||
} else if (secondaryLocales.value.length > 0) {
|
||
const stillExists = secondaryLocales.value.some((l) => l.key === activeSecondaryLocale.value?.key)
|
||
if (!stillExists) {
|
||
activeSecondaryLocale.value = secondaryLocales.value[0]
|
||
}
|
||
} else {
|
||
activeSecondaryLocale.value = null
|
||
}
|
||
nextTick(() => formRef.value?.clearValidate())
|
||
}
|
||
|
||
watch(
|
||
() => [props.show, props.form, props.locales],
|
||
async () => {
|
||
if (props.show) {
|
||
isFullscreen.value = props.fullscreen
|
||
initFormData()
|
||
if (props.id) {
|
||
detailLoading.value = true
|
||
try {
|
||
const data = await props.detailApi(Number(props.id))
|
||
console.log('data', data)
|
||
if (data) {
|
||
const pKey = primaryLocaleKey.value
|
||
Object.keys(data).forEach((localeKey) => {
|
||
const localeData = data[localeKey]
|
||
if (!localeData || typeof localeData !== 'object' || !props.locales.some((l) => l.key === localeKey)) return
|
||
Object.keys(localeData).forEach((fieldKey) => {
|
||
const field = props.form.find((f) => f.key === fieldKey)
|
||
if (!field) return
|
||
if (field.shouldTranslate) {
|
||
if (!formData.translations[localeKey]) formData.translations[localeKey] = {}
|
||
formData.translations[localeKey][fieldKey] = localeData[fieldKey]
|
||
} else if (localeKey === pKey) {
|
||
formData[fieldKey] = localeData[fieldKey]
|
||
}
|
||
})
|
||
})
|
||
}
|
||
} catch (e) {
|
||
console.error('Failed to load detail:', e)
|
||
ElMessage.error('加载详情失败')
|
||
} finally {
|
||
detailLoading.value = false;
|
||
// 强制刷新
|
||
forceUpdate()
|
||
}
|
||
}
|
||
}
|
||
},
|
||
{ immediate: true, deep: true }
|
||
)
|
||
|
||
const close = () => {
|
||
isFullscreen.value = false
|
||
resetFormData()
|
||
emit('close')
|
||
}
|
||
|
||
async function handleTranslate() {
|
||
if (!primaryLocale.value || !activeSecondaryLocale.value || translating.value) return
|
||
|
||
const fromKey = primaryLocale.value.key
|
||
const toKey = activeSecondaryLocale.value.key
|
||
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() && !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 })
|
||
}
|
||
}
|
||
})
|
||
|
||
if (texts.length === 0) return
|
||
|
||
translating.value = true
|
||
try {
|
||
const results = await Promise.allSettled(
|
||
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 })
|
||
translated = parseTranslateResult(res)
|
||
}
|
||
return { key, translated }
|
||
})
|
||
)
|
||
let failCount = 0
|
||
for (const result of results) {
|
||
if (result.status === 'fulfilled') {
|
||
const { key, translated } = result.value
|
||
if (formData.translations[toKey]) {
|
||
formData.translations[toKey][key] = translated
|
||
}
|
||
} else {
|
||
failCount++
|
||
}
|
||
}
|
||
if (failCount > 0) {
|
||
ElMessage.warning(`${failCount} 个字段翻译失败`)
|
||
}
|
||
} catch (e) {
|
||
console.error('Translate failed:', e)
|
||
ElMessage.error('翻译失败')
|
||
} finally {
|
||
translating.value = false
|
||
}
|
||
}
|
||
|
||
async function handleTranslateField(itemKey: string) {
|
||
if (!primaryLocale.value || !activeSecondaryLocale.value || translatingFieldKey.value) return
|
||
const fromKey = primaryLocale.value.key
|
||
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 {
|
||
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('翻译失败')
|
||
} finally {
|
||
translatingFieldKey.value = ''
|
||
}
|
||
}
|
||
|
||
async function submit() {
|
||
try {
|
||
await formRef.value?.validate()
|
||
} catch {
|
||
return
|
||
}
|
||
const data: Record<string, any> = { id: props.id || '' }
|
||
normalFields.value.forEach((item) => {
|
||
data[item.key] = formData[item.key]
|
||
})
|
||
if (Object.keys(formData.translations || {}).length > 0) {
|
||
data.translations = formData.translations
|
||
}
|
||
emit('submit', data)
|
||
}
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.dialog-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
width: 100%;
|
||
padding-right: 40px;
|
||
|
||
.dialog-title {
|
||
font-size: 18px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.dialog-header-actions {
|
||
display: flex;
|
||
gap: 4px;
|
||
}
|
||
}
|
||
|
||
.in18-form-header {
|
||
display: flex;
|
||
border-bottom: 1px solid var(--el-border-color);
|
||
|
||
.in18-form-header-left,
|
||
.in18-form-header-right {
|
||
flex: 1;
|
||
padding: 15px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.in18-form-header-right {
|
||
display: flex;
|
||
gap: 8px;
|
||
align-items: center;
|
||
|
||
.locale-tab {
|
||
padding: 5px 12px;
|
||
border: 1px solid var(--el-border-color);
|
||
border-radius: 5px;
|
||
cursor: pointer;
|
||
font-size: 14px;
|
||
|
||
&.active {
|
||
background-color: var(--el-color-primary);
|
||
color: #fff;
|
||
border-color: var(--el-color-primary);
|
||
}
|
||
|
||
&:hover:not(.active) {
|
||
background-color: var(--el-fill-color-light);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.in18-form-top {
|
||
padding: 15px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 12px;
|
||
|
||
:deep(.el-form-item) {
|
||
width: 100%;
|
||
max-width: 400px;
|
||
margin-bottom: 0;
|
||
}
|
||
}
|
||
|
||
.in18-form-wrapper {
|
||
display: flex;
|
||
flex-direction: row;
|
||
min-height: 200px;
|
||
border-bottom: 1px solid var(--el-border-color);
|
||
|
||
.in18-form-left,
|
||
.in18-form-right {
|
||
flex: 1;
|
||
padding: 15px;
|
||
overflow-y: auto;
|
||
|
||
:deep(.el-form-item) {
|
||
margin-bottom: 16px;
|
||
}
|
||
}
|
||
|
||
.in18-form-left {
|
||
border-right: 1px solid var(--el-border-color);
|
||
}
|
||
|
||
.field-with-translate {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 8px;
|
||
width: 100%;
|
||
|
||
.el-input,
|
||
.el-textarea {
|
||
flex: 1;
|
||
}
|
||
|
||
.translate-icon {
|
||
flex-shrink: 0;
|
||
padding: 8px 0;
|
||
}
|
||
}
|
||
|
||
.in18-form-center {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 0 15px;
|
||
min-width: 80px;
|
||
border-right: 1px solid var(--el-border-color);
|
||
}
|
||
}
|
||
</style>
|