fix: bug修改字段缺失
This commit is contained in:
parent
5ae8614b1d
commit
0e94beb477
|
|
@ -50,7 +50,7 @@
|
|||
|
||||
<script>
|
||||
import { nextTick, onMounted, ref, watch } from 'vue'
|
||||
import { uploadFile } from '@/utils/file'
|
||||
import { uploadFile, extractUploadUrlFromResponse, PORTAL_TENCENT_COS_UPLOAD_URL } from '@/utils/file'
|
||||
|
||||
export default {
|
||||
name: 'RichTextEditor',
|
||||
|
|
@ -105,13 +105,13 @@ export default {
|
|||
|
||||
// 上传到后端
|
||||
const res = await uploadFile({
|
||||
url: '/api/cos/upload', // 使用腾讯云COS上传接口
|
||||
url: PORTAL_TENCENT_COS_UPLOAD_URL,
|
||||
file: file,
|
||||
name: 'file'
|
||||
})
|
||||
|
||||
if (res && res.code === 200 && res.data) {
|
||||
const imageUrl = typeof res.data === 'string' ? res.data : (res.data.url || res.data)
|
||||
const imageUrl = extractUploadUrlFromResponse(res)
|
||||
if (res && (Number(res.code) === 200 || res.code === 200) && imageUrl) {
|
||||
insertImage(imageUrl, file.name)
|
||||
|
||||
// 通知父组件有新图片上传成功(用于@功能)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,549 @@
|
|||
<template>
|
||||
<div class="video-editor-root">
|
||||
<div v-if="showToolbar" class="toolbar">
|
||||
<button class="tool-btn" type="button" @click="openImagePicker">插入参考素材</button>
|
||||
<button class="tool-btn" type="button" @click="clear">清空所有参考素材和文字</button>
|
||||
</div>
|
||||
<input
|
||||
ref="fileInputRef"
|
||||
class="hidden-input"
|
||||
type="file"
|
||||
accept="image/*,video/*,audio/*"
|
||||
multiple
|
||||
@change="handleSelectFiles" />
|
||||
<div class="editor-wrapper">
|
||||
<div
|
||||
ref="editorRef"
|
||||
class="user-input video-rich-editor"
|
||||
contenteditable="true"
|
||||
:data-placeholder="placeholder || '请输入文本生成视频...'"
|
||||
@input="handleInput"
|
||||
@paste="handlePaste"
|
||||
@keyup="handleKeyup"
|
||||
@click="handleEditorClick"></div>
|
||||
<div v-if="mentionVisible" class="mention-panel">
|
||||
<div
|
||||
v-for="item in mentionImageList"
|
||||
:key="item.url"
|
||||
class="mention-item"
|
||||
@mousedown.prevent="insertMentionImage(item)">
|
||||
<img v-if="item.mediaType === 'image'" :src="item.url" class="mention-thumb" alt="" />
|
||||
<span v-else class="mention-type-badge">{{ item.mediaType === 'video' ? '视频' : '音频' }}</span>
|
||||
<span class="mention-label">参考{{ item.refNo }}</span>
|
||||
</div>
|
||||
<div v-if="mentionImageList.length === 0" class="mention-empty">暂无可引用素材</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { nextTick, onMounted, ref } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import { uploadFile, extractUploadUrlFromResponse, PORTAL_TENCENT_COS_UPLOAD_URL } from '@/utils/file'
|
||||
|
||||
const props = defineProps({
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
/** 文生视频等场景可隐藏工具栏,仅保留纯文本编辑 */
|
||||
showToolbar: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['text-change'])
|
||||
|
||||
const editorRef = ref(null)
|
||||
const fileInputRef = ref(null)
|
||||
const savedSelectionRange = ref(null)
|
||||
const MAX_IMAGE_COUNT = 9
|
||||
const mentionVisible = ref(false)
|
||||
const mentionImageList = ref([])
|
||||
|
||||
const detectMediaType = (file) => {
|
||||
const mime = (file?.type || '').toLowerCase()
|
||||
if (mime.startsWith('image/')) return 'image'
|
||||
if (mime.startsWith('video/')) return 'video'
|
||||
if (mime.startsWith('audio/')) return 'audio'
|
||||
const lowerName = (file?.name || '').toLowerCase()
|
||||
if (/\.(png|jpe?g|gif|webp|bmp|svg)$/.test(lowerName)) return 'image'
|
||||
if (/\.(mp4|mov|webm|m4v|avi|mkv)$/.test(lowerName)) return 'video'
|
||||
if (/\.(mp3|wav|aac|m4a|ogg|flac)$/.test(lowerName)) return 'audio'
|
||||
return 'image'
|
||||
}
|
||||
|
||||
const getPlainText = () => (editorRef.value?.innerText || '').trim()
|
||||
|
||||
const focusEditorToEnd = () => {
|
||||
if (!editorRef.value) return
|
||||
editorRef.value.focus()
|
||||
const selection = window.getSelection()
|
||||
if (!selection) return
|
||||
const range = document.createRange()
|
||||
range.selectNodeContents(editorRef.value)
|
||||
range.collapse(false)
|
||||
selection.removeAllRanges()
|
||||
selection.addRange(range)
|
||||
savedSelectionRange.value = range.cloneRange()
|
||||
}
|
||||
|
||||
const saveSelection = () => {
|
||||
const selection = window.getSelection()
|
||||
if (!selection || selection.rangeCount === 0) return
|
||||
const range = selection.getRangeAt(0)
|
||||
if (editorRef.value?.contains(range.commonAncestorContainer)) {
|
||||
savedSelectionRange.value = range.cloneRange()
|
||||
}
|
||||
}
|
||||
|
||||
const restoreSelection = () => {
|
||||
const selection = window.getSelection()
|
||||
if (!selection || !editorRef.value) return
|
||||
selection.removeAllRanges()
|
||||
if (savedSelectionRange.value) {
|
||||
selection.addRange(savedSelectionRange.value)
|
||||
} else {
|
||||
const range = document.createRange()
|
||||
range.selectNodeContents(editorRef.value)
|
||||
range.collapse(false)
|
||||
selection.addRange(range)
|
||||
}
|
||||
}
|
||||
|
||||
const handleInput = () => {
|
||||
emit('text-change', getPlainText())
|
||||
saveSelection()
|
||||
syncMentionPanelVisibility()
|
||||
}
|
||||
|
||||
const insertPlainTextAtCursor = (text) => {
|
||||
if (!editorRef.value) return
|
||||
const normalizedText = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n')
|
||||
editorRef.value.focus()
|
||||
restoreSelection()
|
||||
const selection = window.getSelection()
|
||||
if (!selection || selection.rangeCount === 0) return
|
||||
let range = selection.getRangeAt(0)
|
||||
if (!editorRef.value.contains(range.commonAncestorContainer)) {
|
||||
focusEditorToEnd()
|
||||
const currentSelection = window.getSelection()
|
||||
if (!currentSelection || currentSelection.rangeCount === 0) return
|
||||
range = currentSelection.getRangeAt(0)
|
||||
}
|
||||
range.deleteContents()
|
||||
const lines = normalizedText.split('\n')
|
||||
const fragment = document.createDocumentFragment()
|
||||
lines.forEach((line, index) => {
|
||||
if (line) fragment.appendChild(document.createTextNode(line))
|
||||
if (index < lines.length - 1) fragment.appendChild(document.createElement('br'))
|
||||
})
|
||||
range.insertNode(fragment)
|
||||
range.collapse(false)
|
||||
selection.removeAllRanges()
|
||||
selection.addRange(range)
|
||||
saveSelection()
|
||||
}
|
||||
|
||||
const handlePaste = (event) => {
|
||||
if (!editorRef.value) return
|
||||
event.preventDefault()
|
||||
const clipboardText = event.clipboardData?.getData('text/plain') || ''
|
||||
if (!clipboardText) return
|
||||
insertPlainTextAtCursor(clipboardText)
|
||||
emit('text-change', getPlainText())
|
||||
syncMentionPanelVisibility()
|
||||
}
|
||||
|
||||
const updateMentionImageList = () => {
|
||||
mentionImageList.value = mentionImageList.value.slice(0, MAX_IMAGE_COUNT)
|
||||
}
|
||||
|
||||
const hideMentionPanel = () => {
|
||||
mentionVisible.value = false
|
||||
}
|
||||
|
||||
const hasActiveMentionTriggerBeforeCursor = () => {
|
||||
const selection = window.getSelection()
|
||||
if (!selection || selection.rangeCount === 0) return false
|
||||
const range = selection.getRangeAt(0)
|
||||
if (!range.collapsed) return false
|
||||
if (!editorRef.value?.contains(range.commonAncestorContainer)) return false
|
||||
const container = range.startContainer
|
||||
if (container.nodeType !== Node.TEXT_NODE) return false
|
||||
const before = container.data.slice(0, range.startOffset)
|
||||
const atIndex = before.lastIndexOf('@')
|
||||
if (atIndex < 0) return false
|
||||
const afterAt = before.slice(atIndex + 1)
|
||||
return !/\s/.test(afterAt)
|
||||
}
|
||||
|
||||
const syncMentionPanelVisibility = () => {
|
||||
const shouldShow = hasActiveMentionTriggerBeforeCursor()
|
||||
if (!shouldShow) {
|
||||
hideMentionPanel()
|
||||
return
|
||||
}
|
||||
updateMentionImageList()
|
||||
mentionVisible.value = true
|
||||
}
|
||||
|
||||
const removeMentionKeywordBeforeCursor = () => {
|
||||
const selection = window.getSelection()
|
||||
if (!selection || selection.rangeCount === 0) return
|
||||
const range = selection.getRangeAt(0)
|
||||
if (!range.collapsed) return
|
||||
const container = range.startContainer
|
||||
if (container.nodeType !== Node.TEXT_NODE) return
|
||||
const before = container.data.slice(0, range.startOffset)
|
||||
const atIndex = before.lastIndexOf('@')
|
||||
if (atIndex < 0) return
|
||||
const afterAt = before.slice(atIndex + 1)
|
||||
if (/\s/.test(afterAt)) return
|
||||
const removeRange = document.createRange()
|
||||
removeRange.setStart(container, atIndex)
|
||||
removeRange.setEnd(container, range.startOffset)
|
||||
removeRange.deleteContents()
|
||||
}
|
||||
|
||||
const insertReference = (item) => {
|
||||
if (!item?.url || !editorRef.value) return
|
||||
editorRef.value.focus()
|
||||
restoreSelection()
|
||||
const selection = window.getSelection()
|
||||
if (!selection || selection.rangeCount === 0) return
|
||||
const range = selection.getRangeAt(0)
|
||||
if (!editorRef.value.contains(range.commonAncestorContainer)) return
|
||||
|
||||
const refNoText = `[图${item.refNo}]`
|
||||
let node
|
||||
if (item.mediaType === 'image') {
|
||||
const img = document.createElement('img')
|
||||
img.src = item.url
|
||||
img.alt = `reference-${item.refNo}`
|
||||
img.className = 'inline-rich-image'
|
||||
img.setAttribute('data-image-url', item.url)
|
||||
img.setAttribute('data-mention-reference', '1')
|
||||
img.setAttribute('data-reference-no', String(item.refNo))
|
||||
node = img
|
||||
} else {
|
||||
const tag = document.createElement('span')
|
||||
tag.className = 'inline-rich-reference'
|
||||
tag.textContent = refNoText
|
||||
tag.setAttribute('data-reference-url', item.url)
|
||||
tag.setAttribute('data-mention-reference', '1')
|
||||
tag.setAttribute('data-reference-no', String(item.refNo))
|
||||
tag.setAttribute('data-reference-type', item.mediaType)
|
||||
node = tag
|
||||
}
|
||||
|
||||
range.insertNode(node)
|
||||
const space = document.createTextNode(' ')
|
||||
range.setStartAfter(node)
|
||||
range.insertNode(space)
|
||||
range.setStartAfter(space)
|
||||
range.collapse(true)
|
||||
selection.removeAllRanges()
|
||||
selection.addRange(range)
|
||||
saveSelection()
|
||||
}
|
||||
|
||||
const insertMentionImage = (item) => {
|
||||
if (!item?.url || !editorRef.value) return
|
||||
editorRef.value.focus()
|
||||
restoreSelection()
|
||||
removeMentionKeywordBeforeCursor()
|
||||
insertReference(item)
|
||||
hideMentionPanel()
|
||||
emit('text-change', getPlainText())
|
||||
}
|
||||
|
||||
const handleKeyup = (event) => {
|
||||
saveSelection()
|
||||
if (event.key === '@') {
|
||||
syncMentionPanelVisibility()
|
||||
return
|
||||
}
|
||||
if (event.key === 'Escape') {
|
||||
hideMentionPanel()
|
||||
return
|
||||
}
|
||||
syncMentionPanelVisibility()
|
||||
}
|
||||
|
||||
const handleEditorClick = () => {
|
||||
const selection = window.getSelection()
|
||||
if (!selection || selection.rangeCount === 0) {
|
||||
focusEditorToEnd()
|
||||
return
|
||||
}
|
||||
const range = selection.getRangeAt(0)
|
||||
if (!editorRef.value?.contains(range.commonAncestorContainer)) {
|
||||
focusEditorToEnd()
|
||||
return
|
||||
}
|
||||
saveSelection()
|
||||
syncMentionPanelVisibility()
|
||||
}
|
||||
|
||||
const getInsertedUniqueImageCount = () => mentionImageList.value.length
|
||||
|
||||
const openImagePicker = () => {
|
||||
const current = getInsertedUniqueImageCount()
|
||||
if (current >= MAX_IMAGE_COUNT) {
|
||||
Message.warning(`最多插入 ${MAX_IMAGE_COUNT} 个参考素材`)
|
||||
return
|
||||
}
|
||||
fileInputRef.value?.click()
|
||||
}
|
||||
|
||||
const uploadToCos = async (file) => {
|
||||
const res = await uploadFile({
|
||||
url: PORTAL_TENCENT_COS_UPLOAD_URL,
|
||||
file,
|
||||
name: 'file'
|
||||
})
|
||||
const codeOk = res && (Number(res.code) === 200 || res.code === 200)
|
||||
const url = extractUploadUrlFromResponse(res)
|
||||
if (codeOk && url) return url
|
||||
throw new Error(res?.msg || '上传失败:未返回文件地址')
|
||||
}
|
||||
|
||||
const handleSelectFiles = async (event) => {
|
||||
const input = event.target
|
||||
const files = Array.from(input.files || [])
|
||||
if (!files.length) return
|
||||
const remain = MAX_IMAGE_COUNT - getInsertedUniqueImageCount()
|
||||
if (remain <= 0) {
|
||||
Message.warning(`最多插入 ${MAX_IMAGE_COUNT} 个参考素材`)
|
||||
input.value = ''
|
||||
return
|
||||
}
|
||||
const selected = files.slice(0, remain)
|
||||
if (files.length > remain) {
|
||||
Message.warning(`最多插入 ${MAX_IMAGE_COUNT} 个参考素材,本次仅插入 ${remain} 个`)
|
||||
}
|
||||
for (const file of selected) {
|
||||
const mediaType = detectMediaType(file)
|
||||
try {
|
||||
const url = await uploadToCos(file)
|
||||
if (url && !mentionImageList.value.some((item) => item.url === url)) {
|
||||
const refNo = mentionImageList.value.length + 1
|
||||
mentionImageList.value.push({ url, refNo, mediaType })
|
||||
}
|
||||
} catch (e) {
|
||||
Message.error('参考素材上传失败')
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
input.value = ''
|
||||
emit('text-change', getPlainText())
|
||||
}
|
||||
|
||||
const getContentItems = () => {
|
||||
const editor = editorRef.value
|
||||
const items = []
|
||||
if (!editor) return items
|
||||
let textBuffer = ''
|
||||
const flushText = () => {
|
||||
const normalized = textBuffer.replace(/\u00a0/g, ' ').replace(/\s+\n/g, '\n')
|
||||
if (normalized.trim()) items.push({ type: 'text', text: normalized.trim() })
|
||||
textBuffer = ''
|
||||
}
|
||||
const walk = (node) => {
|
||||
if (node.nodeType === Node.TEXT_NODE) {
|
||||
textBuffer += node.textContent || ''
|
||||
return
|
||||
}
|
||||
if (node.nodeType !== Node.ELEMENT_NODE) return
|
||||
const el = node
|
||||
const referenceUrl = (
|
||||
el.dataset?.referenceUrl ||
|
||||
el.dataset?.imageUrl ||
|
||||
el.getAttribute('src') ||
|
||||
''
|
||||
).trim()
|
||||
if (el.dataset?.mentionReference === '1' && referenceUrl) {
|
||||
flushText()
|
||||
items.push({
|
||||
type: 'image_url',
|
||||
image_url: { url: referenceUrl },
|
||||
role: 'reference_image'
|
||||
})
|
||||
return
|
||||
}
|
||||
if (el.tagName === 'IMG' && (el.dataset?.imageUrl || el.getAttribute('src'))) {
|
||||
return
|
||||
}
|
||||
if (el.tagName === 'BR') {
|
||||
textBuffer += '\n'
|
||||
return
|
||||
}
|
||||
if (['DIV', 'P', 'LI'].includes(el.tagName) && textBuffer && !textBuffer.endsWith('\n')) textBuffer += '\n'
|
||||
Array.from(el.childNodes).forEach(walk)
|
||||
if (['DIV', 'P', 'LI'].includes(el.tagName) && textBuffer && !textBuffer.endsWith('\n')) textBuffer += '\n'
|
||||
}
|
||||
Array.from(editor.childNodes).forEach(walk)
|
||||
flushText()
|
||||
return items
|
||||
}
|
||||
|
||||
const clear = () => {
|
||||
if (!editorRef.value) return
|
||||
editorRef.value.innerHTML = ''
|
||||
emit('text-change', '')
|
||||
savedSelectionRange.value = null
|
||||
mentionImageList.value = []
|
||||
hideMentionPanel()
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
getContentItems,
|
||||
getPlainText,
|
||||
clear
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
focusEditorToEnd()
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.video-editor-root {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
.toolbar {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.hidden-input {
|
||||
display: none;
|
||||
}
|
||||
.editor-wrapper {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 200px;
|
||||
}
|
||||
.tool-btn {
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
border-radius: 8px;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
color: rgba(255, 255, 255, 0.88);
|
||||
padding: 6px 12px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
transition: border-color 0.2s, background 0.2s;
|
||||
}
|
||||
.tool-btn:hover {
|
||||
border-color: rgba(0, 202, 224, 0.4);
|
||||
background: rgba(0, 202, 224, 0.08);
|
||||
}
|
||||
.video-rich-editor {
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
min-height: 220px;
|
||||
font-size: 15px;
|
||||
line-height: 1.6;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
overflow-y: auto;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
caret-color: #00cae0;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 14px;
|
||||
padding: 14px 16px;
|
||||
background: rgba(0, 0, 0, 0.25);
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
.video-rich-editor:focus {
|
||||
border-color: rgba(0, 202, 224, 0.45);
|
||||
box-shadow: 0 0 0 2px rgba(0, 202, 224, 0.15);
|
||||
outline: none;
|
||||
}
|
||||
.video-rich-editor:empty::before {
|
||||
content: attr(data-placeholder);
|
||||
color: rgba(255, 255, 255, 0.35);
|
||||
pointer-events: none;
|
||||
display: block;
|
||||
}
|
||||
.video-rich-editor :deep(.inline-rich-image) {
|
||||
height: 1.1em;
|
||||
width: auto;
|
||||
vertical-align: baseline;
|
||||
margin: 0 2px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.video-rich-editor :deep(.inline-rich-reference) {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0 6px;
|
||||
margin: 0 2px;
|
||||
border-radius: 4px;
|
||||
background: rgba(0, 202, 224, 0.15);
|
||||
color: #5eebf5;
|
||||
font-size: 0.85em;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.mention-panel {
|
||||
position: absolute;
|
||||
left: 8px;
|
||||
right: 8px;
|
||||
bottom: 8px;
|
||||
max-height: 180px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 10px;
|
||||
background: rgba(22, 24, 30, 0.98);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45);
|
||||
z-index: 20;
|
||||
}
|
||||
.mention-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.mention-item:hover {
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
.mention-thumb {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 4px;
|
||||
object-fit: cover;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
.mention-type-badge {
|
||||
min-width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 11px;
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
background: rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
.mention-label {
|
||||
font-size: 12px;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
}
|
||||
.mention-empty {
|
||||
padding: 8px 10px;
|
||||
font-size: 12px;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -50,6 +50,9 @@ import cloneDeep from 'lodash-es/cloneDeep'
|
|||
import { constantRoutes } from '@/router/index'
|
||||
import { generateTitle, generateLang } from '@/utils/i18n'
|
||||
|
||||
/** 左侧导航仅显示这些路由(name 与 router/index.js 一致) */
|
||||
const SIDEBAR_ONLY_ROUTE_NAMES = ['video-gen']
|
||||
|
||||
defineProps({
|
||||
collapsed: Boolean
|
||||
})
|
||||
|
|
@ -62,9 +65,14 @@ const $base = inject('$base')
|
|||
const $message = inject('$message')
|
||||
|
||||
const menuItems = computed(() => {
|
||||
return constantRoutes
|
||||
.find((d) => d.path == '/')
|
||||
.children.map((route) => ({
|
||||
const root = constantRoutes.find((d) => d.path === '/')
|
||||
const children = root?.children ?? []
|
||||
return children
|
||||
.filter(
|
||||
(r) =>
|
||||
r.meta?.menuItem !== false && SIDEBAR_ONLY_ROUTE_NAMES.includes(r.name)
|
||||
)
|
||||
.map((route) => ({
|
||||
key: route.name,
|
||||
label: generateTitle(route.meta?.title),
|
||||
meta: route.meta,
|
||||
|
|
|
|||
|
|
@ -130,6 +130,23 @@ export const convertBase64ToUrl = (base64) => {
|
|||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 从若依 AjaxResult / 兼容仅返回顶层 url 的上传响应中解析访问地址
|
||||
*/
|
||||
export const extractUploadUrlFromResponse = (res) => {
|
||||
if (!res) return ''
|
||||
if (typeof res.data === 'string' && res.data.trim()) return res.data.trim()
|
||||
if (res.data && typeof res.data === 'object') {
|
||||
const u = res.data.url || res.data.path
|
||||
if (typeof u === 'string' && u.trim()) return u.trim()
|
||||
}
|
||||
if (typeof res.url === 'string' && res.url.trim()) return res.url.trim()
|
||||
return ''
|
||||
}
|
||||
|
||||
/** 门户统一腾讯云 COS:后端 FileController `/api/file/upload`,与 mf-image-upload 一致 */
|
||||
export const PORTAL_TENCENT_COS_UPLOAD_URL = '/api/file/upload'
|
||||
|
||||
/**
|
||||
* 文件上传
|
||||
* @param {Object} {url, file,name} 文件上传地址 file 文件 name 文件名参数
|
||||
|
|
@ -148,12 +165,10 @@ export const uploadFile = ({
|
|||
formData.append(key, data[key])
|
||||
})
|
||||
}
|
||||
// 不要手动设置 Content-Type,否则缺少 boundary,服务端无法解析 multipart,文件参数字段为空
|
||||
return request({
|
||||
url: url,
|
||||
method: 'POST',
|
||||
data: formData,
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
data: formData
|
||||
})
|
||||
}
|
||||
Loading…
Reference in New Issue