fix(blog): 后台博客图片支持本地上传 + 封面图上传错误信息暴露

正文编辑器:
- 新增"图片"按钮: 点击弹文件选择 -> 调 /api/admin/upload -> 自动插入 <img src=/uploads/...>
- 保留"图片URL"按钮兼容外链/已有路径(原来唯一的 prompt 路径)
- 上传中按钮 disabled, 失败用 naive-ui message 提示

封面图 ImageUpload:
- 改用 useAdmin().adminFetch 替代裸 \$fetch + useState 取 token 的双轨写法
- 错误信息从单一字符串"上传失败"改为 e.data?.message || e.message || HTTP statusCode, 让线上"上传失败"自暴露真实根因

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
这个提交包含在:
wx 2026-05-04 14:19:07 +08:00
父节点 9a0afb7cce
当前提交 b5c13d5f24
共有 2 个文件被更改,包括 40 次插入7 次删除

查看文件

@ -90,11 +90,9 @@ async function uploadFile(file) {
const formData = new FormData() const formData = new FormData()
formData.append('file', file) formData.append('file', file)
const token = useState('admin_token') const result = await adminFetch('/api/admin/upload', {
const result = await $fetch('/api/admin/upload', {
method: 'POST', method: 'POST',
body: formData, body: formData,
headers: { Authorization: `Bearer ${token.value}` },
}) })
if (result.files?.length) { if (result.files?.length) {
@ -102,7 +100,7 @@ async function uploadFile(file) {
imgError.value = false imgError.value = false
} }
} catch (e) { } catch (e) {
uploadError.value = e.data?.message || '上传失败' uploadError.value = e?.data?.message || e?.message || `上传失败 (HTTP ${e?.statusCode ?? '?'})`
} finally { } finally {
uploading.value = false uploading.value = false
} }

查看文件

@ -14,7 +14,8 @@
<button type="button" :class="btn(editor.isActive('blockquote'))" @click="editor.chain().focus().toggleBlockquote().run()" title="引用">" 引用</button> <button type="button" :class="btn(editor.isActive('blockquote'))" @click="editor.chain().focus().toggleBlockquote().run()" title="引用">" 引用</button>
<span class="divider" /> <span class="divider" />
<button type="button" :class="btn(editor.isActive('link'))" @click="setLink" title="链接">🔗 链接</button> <button type="button" :class="btn(editor.isActive('link'))" @click="setLink" title="链接">🔗 链接</button>
<button type="button" class="toolbar-btn" @click="addImage" title="插入图片">🖼 图片</button> <button type="button" class="toolbar-btn" :disabled="uploading" @click="addImage" :title="uploading ? '上传中...' : '插入图片'">🖼 {{ uploading ? '上传中' : '图片' }}</button>
<button type="button" class="toolbar-btn" @click="addImageByUrl" title="插入网络图片URL">🌐 图片URL</button>
<span class="divider" /> <span class="divider" />
<button type="button" class="toolbar-btn" @click="editor.chain().focus().undo().run()" title="撤销"></button> <button type="button" class="toolbar-btn" @click="editor.chain().focus().undo().run()" title="撤销"></button>
<button type="button" class="toolbar-btn" @click="editor.chain().focus().redo().run()" title="重做"></button> <button type="button" class="toolbar-btn" @click="editor.chain().focus().redo().run()" title="重做"></button>
@ -22,6 +23,8 @@
<button type="button" class="toolbar-btn" :class="{ active: showHtml }" @click="showHtml = !showHtml" title="源代码视图">&lt;/&gt; HTML</button> <button type="button" class="toolbar-btn" :class="{ active: showHtml }" @click="showHtml = !showHtml" title="源代码视图">&lt;/&gt; HTML</button>
</div> </div>
<input ref="fileInput" type="file" accept="image/jpeg,image/png,image/webp,image/gif,image/svg+xml" style="display: none" @change="handleFileChange" />
<EditorContent v-show="!showHtml" :editor="editor" class="rich-editor-body" /> <EditorContent v-show="!showHtml" :editor="editor" class="rich-editor-body" />
<textarea <textarea
v-show="showHtml" v-show="showHtml"
@ -39,6 +42,7 @@ import StarterKit from '@tiptap/starter-kit'
import Link from '@tiptap/extension-link' import Link from '@tiptap/extension-link'
import Image from '@tiptap/extension-image' import Image from '@tiptap/extension-image'
import { watch, ref, onBeforeUnmount } from 'vue' import { watch, ref, onBeforeUnmount } from 'vue'
import { useMessage } from 'naive-ui'
const props = defineProps({ const props = defineProps({
modelValue: { type: String, default: '' }, modelValue: { type: String, default: '' },
@ -47,6 +51,10 @@ const props = defineProps({
const emit = defineEmits(['update:modelValue']) const emit = defineEmits(['update:modelValue'])
const showHtml = ref(false) const showHtml = ref(false)
const fileInput = ref(null)
const uploading = ref(false)
const message = useMessage()
const { adminFetch } = useAdmin()
const editor = useEditor({ const editor = useEditor({
content: props.modelValue, content: props.modelValue,
@ -79,8 +87,35 @@ function setLink() {
} }
function addImage() { function addImage() {
const url = window.prompt('图片 URL可填 /uploads/ 或 /images/ 路径)') if (uploading.value) return
if (url) editor.value.chain().focus().setImage({ src: url }).run() fileInput.value?.click()
}
function addImageByUrl() {
const url = window.prompt('图片 URL可填 /uploads/ 或 /images/ 路径,或外部 https:// 链接)')
if (url) editor.value.chain().focus().setImage({ src: url.trim() }).run()
}
async function handleFileChange(e) {
const file = e.target.files?.[0]
e.target.value = ''
if (!file) return
if (file.size > 10 * 1024 * 1024) { message.error('文件过大,最大 10MB'); return }
uploading.value = true
try {
const formData = new FormData()
formData.append('file', file)
const result = await adminFetch('/api/admin/upload', { method: 'POST', body: formData })
const url = result?.files?.[0]?.url
if (!url) throw new Error('上传响应缺少 url 字段')
editor.value.chain().focus().setImage({ src: url }).run()
message.success('图片已插入')
} catch (err) {
message.error(err?.data?.message || err?.message || `上传失败 (HTTP ${err?.statusCode ?? '?'})`)
} finally {
uploading.value = false
}
} }
</script> </script>