feat: support announcement image upload
# Conflicts: # database.py # db/migrations.py # routes/admin_api/core.py # static/admin/.vite/manifest.json # static/admin/assets/AnnouncementsPage-Btl9JP7M.js # static/admin/assets/EmailPage-CwqlBGU2.js # static/admin/assets/FeedbacksPage-B_qDNL3q.js # static/admin/assets/LogsPage-DzdymdrQ.js # static/admin/assets/ReportPage-Bp26gOA-.js # static/admin/assets/SettingsPage-__r25pN8.js # static/admin/assets/SystemPage-C1OfxrU-.js # static/admin/assets/UsersPage-DhnABKcY.js # static/admin/assets/email-By53DCWv.js # static/admin/assets/email-ByiJ74rd.js # static/admin/assets/email-DkWacopQ.js # static/admin/assets/index-D5wU2pVd.js # static/admin/assets/tasks-1acmkoIX.js # static/admin/assets/update-DdQLVpC3.js # static/admin/assets/users-B1w166uc.js # static/admin/assets/users-CPJP5r-B.js # static/admin/assets/users-CnIyvFWm.js # static/admin/index.html # static/app/.vite/manifest.json # static/app/assets/AccountsPage-C48gJL8c.js # static/app/assets/AccountsPage-D387XNsv.js # static/app/assets/AccountsPage-DBJCAsJz.js # static/app/assets/LoginPage-BgK_Vl6X.js # static/app/assets/RegisterPage-CwADxWfe.js # static/app/assets/ResetPasswordPage-CVfZX_5z.js # static/app/assets/SchedulesPage-CWuZpJ5h.js # static/app/assets/SchedulesPage-Dw-mXbG5.js # static/app/assets/SchedulesPage-DwzGOBuc.js # static/app/assets/ScreenshotsPage-C6vX2U3V.js # static/app/assets/ScreenshotsPage-CreOSjVc.js # static/app/assets/ScreenshotsPage-DuTeRzLR.js # static/app/assets/VerifyResultPage-BzGlCgtE.js # static/app/assets/VerifyResultPage-CN_nr4V6.js # static/app/assets/VerifyResultPage-CNbQc83z.js # static/app/assets/accounts-BFaVMUve.js # static/app/assets/accounts-BYq3lLev.js # static/app/assets/accounts-Bc9j2moH.js # static/app/assets/auth-Dk_ApO4B.js # static/app/assets/index-BIng7uZJ.css # static/app/assets/index-CDxVo_1Z.js # static/app/index.html
This commit is contained in:
@@ -10,6 +10,13 @@ export async function createAnnouncement(payload) {
|
||||
return data
|
||||
}
|
||||
|
||||
export async function uploadAnnouncementImage(file) {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
const { data } = await api.post('/announcements/upload_image', formData)
|
||||
return data
|
||||
}
|
||||
|
||||
export async function activateAnnouncement(id) {
|
||||
const { data } = await api.post(`/announcements/${id}/activate`)
|
||||
return data
|
||||
@@ -24,4 +31,3 @@ export async function deleteAnnouncement(id) {
|
||||
const { data } = await api.delete(`/announcements/${id}`)
|
||||
return data
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { h, onMounted, ref } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus } from '@element-plus/icons-vue'
|
||||
|
||||
import {
|
||||
activateAnnouncement,
|
||||
@@ -8,10 +9,14 @@ import {
|
||||
deactivateAnnouncement,
|
||||
deleteAnnouncement,
|
||||
fetchAnnouncements,
|
||||
uploadAnnouncementImage,
|
||||
} from '../api/announcements'
|
||||
|
||||
const formTitle = ref('')
|
||||
const formContent = ref('')
|
||||
const formImageUrl = ref('')
|
||||
const imageInputRef = ref(null)
|
||||
const uploading = ref(false)
|
||||
|
||||
const loading = ref(false)
|
||||
const list = ref([])
|
||||
@@ -30,18 +35,56 @@ async function load() {
|
||||
function clearForm() {
|
||||
formTitle.value = ''
|
||||
formContent.value = ''
|
||||
formImageUrl.value = ''
|
||||
if (imageInputRef.value) imageInputRef.value.value = ''
|
||||
}
|
||||
|
||||
function openImagePicker() {
|
||||
imageInputRef.value?.click()
|
||||
}
|
||||
|
||||
function clearImage() {
|
||||
formImageUrl.value = ''
|
||||
if (imageInputRef.value) imageInputRef.value.value = ''
|
||||
}
|
||||
|
||||
async function onImageFileChange(event) {
|
||||
const file = event.target?.files?.[0]
|
||||
if (!file) return
|
||||
if (file.type && !file.type.startsWith('image/')) {
|
||||
ElMessage.error('请选择图片文件')
|
||||
event.target.value = ''
|
||||
return
|
||||
}
|
||||
|
||||
uploading.value = true
|
||||
try {
|
||||
const res = await uploadAnnouncementImage(file)
|
||||
if (!res?.success || !res?.url) {
|
||||
ElMessage.error(res?.error || '上传失败')
|
||||
return
|
||||
}
|
||||
formImageUrl.value = res.url
|
||||
ElMessage.success('上传成功')
|
||||
} catch {
|
||||
// handled by interceptor
|
||||
} finally {
|
||||
uploading.value = false
|
||||
event.target.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
async function submit(isActive) {
|
||||
const title = formTitle.value.trim()
|
||||
const content = formContent.value.trim()
|
||||
const image_url = formImageUrl.value.trim()
|
||||
if (!title || !content) {
|
||||
ElMessage.error('标题和内容不能为空')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await createAnnouncement({ title, content, is_active: Boolean(isActive) })
|
||||
const res = await createAnnouncement({ title, content, image_url, is_active: Boolean(isActive) })
|
||||
if (!res?.success) {
|
||||
ElMessage.error(res?.error || '保存失败')
|
||||
return
|
||||
@@ -55,7 +98,17 @@ async function submit(isActive) {
|
||||
}
|
||||
|
||||
async function view(row) {
|
||||
await ElMessageBox.alert(row.content || '', row.title || '公告', {
|
||||
const body = h('div', { class: 'announcement-view' }, [
|
||||
row.content ? h('div', { class: 'announcement-view-text' }, row.content) : null,
|
||||
row.image_url
|
||||
? h('img', {
|
||||
class: 'announcement-view-image',
|
||||
src: row.image_url,
|
||||
alt: '公告图片',
|
||||
})
|
||||
: null,
|
||||
])
|
||||
await ElMessageBox.alert(body, row.title || '公告', {
|
||||
confirmButtonText: '关闭',
|
||||
dangerouslyUseHTMLString: false,
|
||||
})
|
||||
@@ -162,8 +215,26 @@ onMounted(load)
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="公告图片">
|
||||
<div class="image-upload-row">
|
||||
<el-button :icon="Plus" :loading="uploading" @click="openImagePicker">上传图片</el-button>
|
||||
<el-button v-if="formImageUrl" @click="clearImage">移除</el-button>
|
||||
<span v-if="formImageUrl" class="image-url">{{ formImageUrl }}</span>
|
||||
<input
|
||||
ref="imageInputRef"
|
||||
class="image-input"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
@change="onImageFileChange"
|
||||
/>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<div v-if="formImageUrl" class="image-preview">
|
||||
<img :src="formImageUrl" alt="公告图片预览" />
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<el-button type="primary" @click="submit(true)">发布并启用</el-button>
|
||||
<el-button @click="submit(false)">保存但不启用</el-button>
|
||||
@@ -193,6 +264,12 @@ onMounted(load)
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="图片" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.image_url" type="success" effect="light">有图</el-tag>
|
||||
<span v-else class="app-muted">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="created_at" label="创建时间" width="180" />
|
||||
<el-table-column label="操作" width="260" fixed="right">
|
||||
<template #default="{ row }">
|
||||
@@ -234,6 +311,57 @@ onMounted(load)
|
||||
color: var(--app-muted);
|
||||
}
|
||||
|
||||
.image-preview {
|
||||
margin: 6px 0 2px;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.image-preview img {
|
||||
max-width: 280px;
|
||||
max-height: 160px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--app-border);
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.image-upload-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.image-input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.image-url {
|
||||
font-size: 12px;
|
||||
color: var(--app-muted);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.announcement-view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.announcement-view-text {
|
||||
white-space: pre-wrap;
|
||||
line-height: 1.6;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.announcement-view-image {
|
||||
max-width: 100%;
|
||||
max-height: 320px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--app-border);
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.table-wrap {
|
||||
overflow-x: auto;
|
||||
}
|
||||
@@ -252,4 +380,3 @@ onMounted(load)
|
||||
gap: 8px;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user