后台编辑器支持短文配图:加载时显示、可增删、保存回传
短文的配图(images 字段)此前在后台编辑器里完全不可见——快发盒发的 带图短文一旦进后台重新编辑,站主既看不到图,也无法管理。现在: - 编辑器加载短文时显示配图条(缩略图 + 单张移除 + 补传,最多 4 张, 与快发盒同上限),配图条只在类型为短文时出现 - 保存时短文始终回传当前列表(后端约定 nil = 不修改,空数组 = 清空), 长文不带该字段;自动保存同样回传,不会弄丢图
This commit is contained in:
1 parent
125107d1c9
commit
2917db9e89
1 file changed
+121
-3
@@ -38,9 +38,45 @@ const form = reactive({
|
|||||||
status: 'draft',
|
status: 'draft',
|
||||||
published_at: '',
|
published_at: '',
|
||||||
reading_minutes: null,
|
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' ----------
|
// ---------- mode: 'wysiwyg' | 'md' ----------
|
||||||
const mode = ref('wysiwyg')
|
const mode = ref('wysiwyg')
|
||||||
|
|
||||||
@@ -115,6 +151,7 @@ async function loadData() {
|
|||||||
form.cover_url = p.cover_url || ''
|
form.cover_url = p.cover_url || ''
|
||||||
form.content_md = p.content_md || ''
|
form.content_md = p.content_md || ''
|
||||||
form.tags = p.tags ? [...p.tags] : []
|
form.tags = p.tags ? [...p.tags] : []
|
||||||
|
form.images = p.images ? [...p.images] : []
|
||||||
form.status = p.status || 'draft'
|
form.status = p.status || 'draft'
|
||||||
publishedLocal.value = isoToLocal(p.published_at)
|
publishedLocal.value = isoToLocal(p.published_at)
|
||||||
if (p.reading_minutes && p.reading_minutes !== estimateMinutes(p.content_md || '')) {
|
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.summary === 'string') form.summary = saved.summary
|
||||||
if (typeof saved.cover_url === 'string') form.cover_url = saved.cover_url
|
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)
|
publishedLocal.value = isoToLocal(saved.published_at)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -313,7 +351,10 @@ function buildPayload() {
|
|||||||
tags: [...form.tags],
|
tags: [...form.tags],
|
||||||
status: form.status,
|
status: form.status,
|
||||||
published_at: localToIso(publishedLocal.value) || new Date().toISOString(),
|
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' ? '标题(可留空,仅作归档索引)' : '标题'"
|
: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 模式显示 -->
|
<!-- 工具栏:仅 Markdown 模式显示 -->
|
||||||
<div v-if="mode === 'md'" class="toolbar">
|
<div v-if="mode === 'md'" class="toolbar">
|
||||||
<div v-for="g in tbGroups" :key="g.label" class="tb-group">
|
<div v-for="g in tbGroups" :key="g.label" class="tb-group">
|
||||||
@@ -1152,4 +1213,61 @@ const tbGroups = computed(() => [
|
|||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</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>
|
||||||
Reference in new issue
Block a user