Compare commits

..

2 Commits

Author SHA1 Message Date
old burden 9f7e43a21a fix: 参考图@插入图bug修复 2026-04-02 18:04:15 +08:00
old burden f3c3e78901 fix: 参考图bug修复 2026-04-02 17:55:33 +08:00
3 changed files with 215 additions and 16 deletions

View File

@ -47,6 +47,16 @@
</div> </div>
<button type="button" class="vg-compose-remove-btn" @click.stop="removeItem(item)">×</button> <button type="button" class="vg-compose-remove-btn" @click.stop="removeItem(item)">×</button>
</div> </div>
<div
v-if="mediaList.length < maxMediaCount"
class="vg-compose-media-item vg-compose-media-item--tile vg-compose-add-tile"
@click="onReferenceEmptyAreaClick"
:title="`还可以继续上传,最多 ${maxMediaCount} 张`">
<div class="vg-compose-media-preview vg-compose-add-preview">
<div class="vg-compose-add-icon">+</div>
<div class="vg-compose-add-text">继续上传</div>
</div>
</div>
</div> </div>
</div> </div>
<div class="vg-mod-foot muted">{{ mediaList.length }}/{{ maxMediaCount }}</div> <div class="vg-mod-foot muted">{{ mediaList.length }}/{{ maxMediaCount }}</div>
@ -677,12 +687,12 @@ const confirmPickAssets = () => {
} }
} else { } else {
const remain = props.maxMediaCount - mediaList.value.length const remain = props.maxMediaCount - mediaList.value.length
if (remain <= 0 && !isReferenceMode) { if (remain <= 0) {
Message.warning(`最多添加 ${props.maxMediaCount} 个参考素材`) Message.warning(`最多添加 ${props.maxMediaCount} 个参考素材`)
return return
} }
const selected = files.slice(0, remain || files.length) const selected = files.slice(0, remain)
const uploadingEntries = [] const uploadingEntries = []
for (const file of selected) { for (const file of selected) {
@ -1987,6 +1997,38 @@ defineExpose({
flex-shrink: 0; flex-shrink: 0;
} }
.vg-compose-add-tile {
cursor: pointer;
border: 1px dashed rgba(0, 202, 224, 0.45);
background: rgba(0, 202, 224, 0.06);
transition: all 0.15s ease-in-out;
}
.vg-compose-add-tile:hover {
border-color: rgba(0, 202, 224, 0.75);
background: rgba(0, 202, 224, 0.1);
}
.vg-compose-add-preview {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 4px;
}
.vg-compose-add-icon {
font-size: 24px;
color: rgba(0, 202, 224, 0.75);
line-height: 1;
}
.vg-compose-add-text {
font-size: 11px;
color: rgba(255, 255, 255, 0.65);
line-height: 1.2;
}
.vg-compose-media-preview { .vg-compose-media-preview {
width: 100%; width: 100%;
height: 100%; height: 100%;
@ -2042,7 +2084,8 @@ defineExpose({
padding: 8px 10px 10px; padding: 8px 10px 10px;
background: rgba(0, 0, 0, 0.18); background: rgba(0, 0, 0, 0.18);
box-sizing: border-box; box-sizing: border-box;
overflow: hidden; /* @候选面板 absolute 需要溢出显示;否则会被父级裁切 */
overflow: visible;
} }
.vg-compose-right-body { .vg-compose-right-body {
@ -2079,7 +2122,8 @@ defineExpose({
flex: 1 1 0%; flex: 1 1 0%;
min-height: 0; min-height: 0;
min-width: 0; min-width: 0;
overflow: hidden; /* @候选面板为 absolute会在内容较多时溢出裁切需要显示完整 */
overflow: visible;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
@ -2191,7 +2235,6 @@ defineExpose({
max-height: 72px; max-height: 72px;
object-fit: cover; object-fit: cover;
border-radius: 6px; border-radius: 6px;
vertical-align: middle;
pointer-events: none; pointer-events: none;
} }
@ -2202,7 +2245,6 @@ defineExpose({
border-radius: 6px; border-radius: 6px;
object-fit: cover; object-fit: cover;
pointer-events: none; pointer-events: none;
vertical-align: middle;
} }
.vg-rich-editor :deep(.vg-inline-ref-audio) { .vg-rich-editor :deep(.vg-inline-ref-audio) {
@ -2248,14 +2290,17 @@ defineExpose({
position: absolute; position: absolute;
left: 8px; left: 8px;
right: 8px; right: 8px;
top: 8px;
bottom: 8px; bottom: 8px;
max-height: 180px; /* 高度与富文本框一致;内容超出时内部滚动 */
max-height: none;
overflow-y: auto; overflow-y: auto;
background: rgba(22, 24, 30, 0.98); background: rgba(22, 24, 30, 0.98);
border: 1px solid rgba(255, 255, 255, 0.12); border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 10px; border-radius: 10px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45); box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45);
z-index: 20; /* 层级要足够高,避免 @候选面板在参考图模式下被上层区域遮挡 */
z-index: 99999;
} }
.vg-mention-item { .vg-mention-item {

View File

@ -182,7 +182,28 @@
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item label="上传文件" required> <a-form-item label="上传文件" required>
<input class="asset-file-input" type="file" @change="onFileChange" /> <div
class="asset-file-dropzone"
:class="{ 'asset-file-dropzone--dragover': createUploadDragOver }"
@click="triggerCreateFilePicker"
@dragenter.prevent="onCreateUploadDragEnter"
@dragover.prevent="onCreateUploadDragOver"
@dragleave.prevent="onCreateUploadDragLeave"
@drop.prevent="onCreateUploadDrop">
<input
ref="createAssetFileInput"
class="asset-file-input"
type="file"
style="display: none"
@change="onFileChange" />
<div class="asset-file-dropzone-content">
<div class="asset-file-dropzone-icon">+</div>
<div class="asset-file-dropzone-text">点击选择或拖拽上传</div>
</div>
</div>
<div v-if="createForm.previewUrl && createForm.assetType === 'Image'" class="asset-file-preview">
<img :src="createForm.previewUrl" alt="" />
</div>
<div class="asset-file-hint">{{ createForm.fileName || '未选择文件' }}</div> <div class="asset-file-hint">{{ createForm.fileName || '未选择文件' }}</div>
</a-form-item> </a-form-item>
<a-form-item label="素材名称"> <a-form-item label="素材名称">
@ -237,9 +258,12 @@ export default {
groupId: '', groupId: '',
file: null, file: null,
fileName: '', fileName: '',
previewUrl: '',
localPreviewUrl: '',
name: '', name: '',
assetType: 'Image' assetType: 'Image'
}, },
createUploadDragOver: false,
filters: { filters: {
groupId: '', groupId: '',
name: '', name: '',
@ -265,7 +289,40 @@ export default {
mounted() { mounted() {
this.loadGroups() this.loadGroups()
}, },
watch: {
createAssetVisible(val) {
if (!val) this.clearCreateUploadPreview()
}
},
methods: { methods: {
clearCreateUploadPreview() {
if (this.createForm?.localPreviewUrl) {
try {
URL.revokeObjectURL(this.createForm.localPreviewUrl)
} catch (_) {}
}
this.createForm.previewUrl = ''
this.createForm.localPreviewUrl = ''
},
setCreateFormFile(file) {
this.createForm.file = file || null
this.createForm.fileName = file?.name || ''
const mime = String(file?.type || '').toLowerCase()
//
if (mime.startsWith('image/')) this.createForm.assetType = 'Image'
else if (mime.startsWith('video/')) this.createForm.assetType = 'Video'
else if (mime.startsWith('audio/')) this.createForm.assetType = 'Audio'
//
this.clearCreateUploadPreview()
if (mime.startsWith('image/')) {
try {
this.createForm.localPreviewUrl = URL.createObjectURL(file)
this.createForm.previewUrl = this.createForm.localPreviewUrl
} catch (_) {}
}
},
formatAssetTypeLabel(it) { formatAssetTypeLabel(it) {
const t = String(it?.AssetType || it?.assetType || '').trim() const t = String(it?.AssetType || it?.assetType || '').trim()
if (!t) return '-' if (!t) return '-'
@ -333,6 +390,7 @@ export default {
this.createForm.fileName = '' this.createForm.fileName = ''
this.createForm.name = '' this.createForm.name = ''
this.createForm.assetType = 'Image' this.createForm.assetType = 'Image'
this.clearCreateUploadPreview()
this.createAssetVisible = true this.createAssetVisible = true
}, },
async loadGroups() { async loadGroups() {
@ -370,10 +428,28 @@ export default {
this.createForm.groupId = gid this.createForm.groupId = gid
this.searchAssets(1) this.searchAssets(1)
}, },
triggerCreateFilePicker() {
const el = this.$refs.createAssetFileInput
if (el && typeof el.click === 'function') el.click()
},
onCreateUploadDragEnter() {
this.createUploadDragOver = true
},
onCreateUploadDragOver() {
this.createUploadDragOver = true
},
onCreateUploadDragLeave() {
this.createUploadDragOver = false
},
onCreateUploadDrop(e) {
this.createUploadDragOver = false
const file = e?.dataTransfer?.files?.[0]
if (!file) return
this.setCreateFormFile(file)
},
onFileChange(e) { onFileChange(e) {
const file = e?.target?.files?.[0] const file = e?.target?.files?.[0]
this.createForm.file = file || null this.setCreateFormFile(file)
this.createForm.fileName = file?.name || ''
}, },
async createAsset() { async createAsset() {
const groupId = String(this.createForm.groupId || '').trim() const groupId = String(this.createForm.groupId || '').trim()
@ -390,6 +466,12 @@ export default {
const uploadUrl = extractUploadUrlFromResponse(uploadRes) const uploadUrl = extractUploadUrlFromResponse(uploadRes)
if (!uploadUrl) throw new Error(uploadRes?.msg || '上传后未返回文件地址') if (!uploadUrl) throw new Error(uploadRes?.msg || '上传后未返回文件地址')
//
if (this.createForm.assetType === 'Image') {
this.clearCreateUploadPreview()
this.createForm.previewUrl = uploadUrl
}
// 2) /api/byteAsset/createAsset(JSON) url // 2) /api/byteAsset/createAsset(JSON) url
const res = await this.$axios({ const res = await this.$axios({
url: ASSET_CREATE_API, url: ASSET_CREATE_API,
@ -402,11 +484,18 @@ export default {
} }
}) })
if (res.code === 200) { if (res.code === 200) {
this.$message.success('新增素材成功') this.$message.success(this.createForm.assetType === 'Image' ? '上传图片成功' : '上传成功')
this.createForm.file = null this.createForm.file = null
this.createForm.fileName = ''
this.createForm.name = '' this.createForm.name = ''
this.createAssetVisible = false const isImage = this.createForm.assetType === 'Image'
if (isImage) {
//
setTimeout(() => {
this.createAssetVisible = false
}, 500)
} else {
this.createAssetVisible = false
}
this.searchAssets(1) this.searchAssets(1)
} else { } else {
this.$message.error(res.msg || '新增素材失败') this.$message.error(res.msg || '新增素材失败')
@ -772,6 +861,68 @@ export default {
color: rgba(255, 255, 255, 0.88); color: rgba(255, 255, 255, 0.88);
} }
.asset-file-dropzone {
width: 100%;
max-width: 100%;
min-height: 38px;
padding: 10px 12px;
border-radius: 10px;
border: 1px dashed rgba(255, 255, 255, 0.18);
background: rgba(0, 0, 0, 0.18);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.15s ease-in-out;
box-sizing: border-box;
}
.asset-file-dropzone--dragover {
border-color: rgba(0, 202, 224, 0.65);
background: rgba(0, 202, 224, 0.06);
}
.asset-file-dropzone-content {
display: flex;
align-items: center;
gap: 10px;
}
.asset-file-dropzone-icon {
width: 28px;
height: 28px;
border-radius: 8px;
border: 1px solid rgba(0, 202, 224, 0.35);
background: rgba(0, 202, 224, 0.08);
color: rgba(0, 202, 224, 0.8);
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
flex-shrink: 0;
}
.asset-file-dropzone-text {
font-size: 13px;
color: rgba(255, 255, 255, 0.7);
}
.asset-file-preview {
margin-top: 10px;
width: 100%;
max-width: 160px;
border-radius: 10px;
overflow: hidden;
border: 1px solid rgba(255, 255, 255, 0.14);
background: rgba(0, 0, 0, 0.25);
}
.asset-file-preview img {
display: block;
width: 100%;
height: auto;
}
.asset-file-hint { .asset-file-hint {
margin-top: 6px; margin-top: 6px;
font-size: 12px; font-size: 12px;

View File

@ -1081,9 +1081,12 @@ export default {
} }
params.text = first.text || text params.text = first.text || text
params.content = contentItems params.content = contentItems
const firstPreview = attachments.find((x) => x?.mediaType === 'image' && /^https?:\/\//i.test(String(x?.url || ''))) // reference_url 使asset://assetId asset://
// https url content/reference_url assetId
const firstPreview = attachments.find((x) => x?.mediaType === 'image')
if (firstPreview) { if (firstPreview) {
params.referenceUrl = firstPreview.url const aid = String(firstPreview?.assetId || '').trim()
params.referenceUrl = aid ? `asset://${aid}` : firstPreview.url
} }
} }