后台编辑器支持短文配图:加载时显示、可增删、保存回传

短文的配图(images 字段)此前在后台编辑器里完全不可见——快发盒发的
带图短文一旦进后台重新编辑,站主既看不到图,也无法管理。现在:

- 编辑器加载短文时显示配图条(缩略图 + 单张移除 + 补传,最多 4 张,
  与快发盒同上限),配图条只在类型为短文时出现
- 保存时短文始终回传当前列表(后端约定 nil = 不修改,空数组 = 清空),
  长文不带该字段;自动保存同样回传,不会弄丢图
This commit is contained in:
Sakurasan committed 2026-10-02 18:59:53 +08:00
1 parent 125107d1c9
commit 2917db9e89
1 file changed
+121 -3
+121 -3
View File
@@ -38,9 +38,45 @@ const form = reactive({
status: 'draft',
published_at: '',
reading_minutes: null,
override_minutes: false
override_minutes: false,
images: []
})
// ---------- 短文配图:时间线 Twitter 式网格,最多 4 张 ----------
// 后端约定 images 为 nil = 不修改;编辑器对短文始终回传当前列表
// (增删都在这里完成),长文不带该字段。
const MAX_IMAGES = 4
const imgInput = ref(null)
const imgUploading = ref(false)
function pickImages() {
imgInput.value?.click()
}
async function onImagesPick(e) {
const files = [...(e.target.files || [])].slice(0, MAX_IMAGES - form.images.length)
e.target.value = ''
if (!files.length) {
toastErr('配图最多 4 张')
return
}
imgUploading.value = true
try {
const fd = new FormData()
for (const f of files) fd.append('file', f)
const data = await adminApi.uploadFiles(fd)
for (const f of data) form.images.push(f.url)
dirty.value = true
} catch (e2) {
toastErr(e2.message || '图片上传失败')
} finally {
imgUploading.value = false
}
}
function removeImage(i) {
form.images.splice(i, 1)
dirty.value = true
}
// ---------- mode: 'wysiwyg' | 'md' ----------
const mode = ref('wysiwyg')
@@ -115,6 +151,7 @@ async function loadData() {
form.cover_url = p.cover_url || ''
form.content_md = p.content_md || ''
form.tags = p.tags ? [...p.tags] : []
form.images = p.images ? [...p.images] : []
form.status = p.status || 'draft'
publishedLocal.value = isoToLocal(p.published_at)
if (p.reading_minutes && p.reading_minutes !== estimateMinutes(p.content_md || '')) {
@@ -284,6 +321,7 @@ function applySaved(saved) {
// 自动摘要/自动封面在服务端补齐的,回写表单让站主看见(清空仍会保留为空)
if (typeof saved.summary === 'string') form.summary = saved.summary
if (typeof saved.cover_url === 'string') form.cover_url = saved.cover_url
if (Array.isArray(saved.images)) form.images = [...saved.images]
publishedLocal.value = isoToLocal(saved.published_at)
}
@@ -313,7 +351,10 @@ function buildPayload() {
tags: [...form.tags],
status: form.status,
published_at: localToIso(publishedLocal.value) || new Date().toISOString(),
reading_minutes: form.override_minutes && form.reading_minutes ? Number(form.reading_minutes) : null
reading_minutes: form.override_minutes && form.reading_minutes ? Number(form.reading_minutes) : null,
// 短文配图:始终回传当前列表(后端 nil = 不修改;空数组 = 清空配图)。
// 长文不带该字段,undefined 不会进 JSON。
...(form.kind === 'short' ? { images: [...form.images] } : {})
}
}
@@ -938,6 +979,26 @@ const tbGroups = computed(() => [
:placeholder="form.kind === 'short' ? '标题(可留空,仅作归档索引)' : '标题'"
/>
<!-- 短文配图:时间线 Twitter 式网格展示,最多 4 张,可增删 -->
<div v-if="form.kind === 'short'" class="note-imgs">
<div v-for="(u, i) in form.images" :key="u" class="note-img">
<img :src="u" alt="" loading="lazy" />
<button type="button" class="rm" title="移除这张图" aria-label="移除这张图" @click="removeImage(i)">✕</button>
</div>
<button
v-if="form.images.length < MAX_IMAGES"
type="button"
class="note-img add"
:disabled="imgUploading"
title="添加配图"
@click="pickImages"
>{{ imgUploading ? '…' : '+' }}</button>
<input ref="imgInput" type="file" accept="image/png,image/jpeg,image/webp,image/gif" hidden multiple @change="onImagesPick" />
</div>
<p v-if="form.kind === 'short'" class="field-hint" style="margin: 4px 0 12px;">
配图在时间线以网格展示(最多 4 张),发布前后都能增删;正文里的行内图片不受这里影响。
</p>
<!-- 工具栏:仅 Markdown 模式显示 -->
<div v-if="mode === 'md'" class="toolbar">
<div v-for="g in tbGroups" :key="g.label" class="tb-group">
@@ -1152,4 +1213,61 @@ const tbGroups = computed(() => [
</aside>
</div>
</section>
</template>
</template>
<style scoped>
/* 短文配图条:与时间线网格同观感(圆角方块 + cover 裁切) */
.note-imgs {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 8px;
}
.note-img {
position: relative;
width: 96px;
height: 96px;
border-radius: 10px;
overflow: hidden;
border: 1px solid var(--admin-line);
}
.note-img img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.note-img .rm {
position: absolute;
top: 4px;
right: 4px;
width: 22px;
height: 22px;
border: 0;
border-radius: 50%;
background: rgba(0, 0, 0, 0.55);
color: #fff;
font-size: 12px;
line-height: 1;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
.note-img .rm:hover {
background: rgba(0, 0, 0, 0.75);
}
.note-img.add {
display: flex;
align-items: center;
justify-content: center;
border-style: dashed;
background: transparent;
color: var(--admin-muted);
font-size: 20px;
cursor: pointer;
}
.note-img.add:hover {
color: var(--admin-accent);
border-color: var(--admin-accent);
}
</style>