完成社会服务的管理界面

This commit is contained in:
2025-10-30 17:24:59 +08:00
parent 9f566027dc
commit 2033278329
6 changed files with 2577 additions and 2 deletions

View File

@@ -63,6 +63,42 @@
<el-icon><Memo /></el-icon>
<template #title>编辑会议</template>
</el-menu-item>
<el-sub-menu index="/service">
<!-- 父菜单标题含图标 -->
<template #title>
<el-icon><Memo /></el-icon>
<span>社会服务</span> <!-- 父菜单名称编辑会议改为更通用的父标题 -->
</template>
<el-menu-item index="/service/serviceimg">
<el-icon><CirclePlus /></el-icon>
<template #title>封面设置</template>
</el-menu-item>
<!-- 子菜单项 1 -->
<el-menu-item index="/service/schoolEnterprise">
<el-icon><CirclePlus /></el-icon>
<template #title>校企合作</template>
</el-menu-item>
<!-- 子菜单项 2编辑会议保留为子项 -->
<el-menu-item index="/service/internship">
<el-icon><Edit /></el-icon>
<template #title>研究实习项目</template>
</el-menu-item>
<!-- 子菜单项 3可按需添加更多 -->
<el-menu-item index="/service/government">
<el-icon><List /></el-icon>
<template #title>乡村政府项目</template>
</el-menu-item>
<!-- 支持嵌套子菜单可选如需三级菜单 -->
</el-sub-menu>
</el-menu>
</el-aside>

View File

@@ -12,7 +12,10 @@ const ResourceView = () => import('../views/resource/ResourceView.vue')
const BaseOverview = ()=> import('../views/baseoverview/BaseOverView.vue')
const DevProjectView = ()=> import('../views/devproject/devprojectView.vue')
const MeetingView = ()=> import('../views/meeting/meetingView.vue')
const SchoolEnterpriseView = ()=> import('../views/socialservice/SchoolEnterprise/SchoolEnterpriseView.vue')
const IntershipView = () => import('../views/socialservice/Internship/InternshipView.vue')
const GovernmentView = ()=> import('../views/socialservice/Government/governmentView.vue')
const ServiceimgView= ()=> import('../views/socialservice/img/serviceimg.vue')
// 定义路由规则(现在 RouteRecordRaw 导入正确)
const routes: RouteRecordRaw[] = [
{
@@ -107,8 +110,58 @@ const routes: RouteRecordRaw[] = [
title: '编辑会议',
requiresAuth: false
}
}
},
{
path: '/service', // 父菜单对应路径(与 el-sub-menu 的 index 一致)
name: 'service',
meta: {
title: '社会服务', // 父菜单标题
requiresAuth: false
},
children: [
// 子菜单1校企合作对应菜单 index="/service/schoolEnterprise"
{
path: 'schoolEnterprise', // 完整路径为 /service/schoolEnterprise
name: 'serviceSchoolEnterprise',
component: SchoolEnterpriseView,
meta: {
title: '校企合作', // 与子菜单标题一致
requiresAuth: false
}
},
// 子菜单2研究实习项目对应菜单 index="/service/internship"
{
path: 'internship', // 完整路径为 /service/internship
name: 'serviceInternship',
component: IntershipView,
meta: {
title: '研究实习项目', // 与子菜单标题一致
requiresAuth: false
}
},
// 子菜单3乡村政府项目对应菜单 index="/service/government"
{
path: 'government', // 完整路径为 /service/government
name: 'serviceGovernment',
component: GovernmentView,
meta: {
title: '乡村政府项目', // 与子菜单标题一致
requiresAuth: false
}
},
// 子菜单3乡村政府项目对应菜单 index="/service/government"
{
path: 'serviceimg', // 完整路径为 /service/government
name: 'serviceimg',
component: ServiceimgView,
meta: {
title: '封面设置', // 与子菜单标题一致
requiresAuth: false
}
}
]
}
]
const router = createRouter({

View File

@@ -0,0 +1,713 @@
<template>
<el-config-provider :locale="zhCn">
<div class="p-6 bg-white rounded-lg shadow-md min-h-screen pb-24" v-loading="loading">
<div class="flex justify-between items-center mb-6">
<span class="text-2xl font-bold text-gray-700">社会服务管理列表</span>
<el-button type="primary" :icon="CirclePlus" @click="handleAdd">新增社会服务项目</el-button>
</div>
<el-table :data="tableData" style="width: 100%" row-key="id" max-height="82vh">
<el-table-column prop="id" label="ID" width="80" sortable fixed></el-table-column>
<el-table-column label="封面" width="180">
<template #default="scope">
<el-image
style="width: 120px; height: 70px; border-radius: 6px;"
:src="scope.row.cover_url"
:preview-src-list="[scope.row.cover_url]"
fit="cover"
:preview-teleported="true"
hide-on-click-modal
>
<template #error>
<div class="flex items-center justify-center w-full h-full bg-gray-100 text-gray-500">无封面</div>
</template>
</el-image>
</template>
</el-table-column>
<el-table-column prop="title" label="标题" min-width="200" show-overflow-tooltip>
<template #default="scope">
<span class="font-semibold">{{ scope.row.title }}</span>
</template>
</el-table-column>
<el-table-column prop="subtitle" label="副标题" min-width="200" show-overflow-tooltip></el-table-column>
<el-table-column prop="intro" label="简介" min-width="250" show-overflow-tooltip>
<template #default="scope">
<span>{{ scope.row.intro?.length > 50 ? `${scope.row.intro.slice(0, 50)}...` : scope.row.intro || '无' }}</span>
</template>
</el-table-column>
<el-table-column prop="chief_editor" label="总编辑" width="120" show-overflow-tooltip></el-table-column>
<el-table-column prop="image_editors" label="图片编辑" width="150" show-overflow-tooltip></el-table-column>
<el-table-column prop="publish_time" label="发布时间" width="180" sortable></el-table-column>
<el-table-column prop="update_time" label="更新时间" width="180" sortable></el-table-column>
<el-table-column label="操作" width="220" fixed="right">
<template #default="scope">
<div class="action-buttons">
<el-button size="small" type="primary" :icon="Edit" @click="handleEdit(scope.row)">编辑</el-button>
<el-button size="small" type="danger" :icon="Delete" @click="handleDelete(scope.row)">删除</el-button>
</div>
</template>
</el-table-column>
</el-table>
<!-- 内容详情弹窗 -->
<el-dialog v-model="contentDialogVisible" title="社会服务项目详情" :width="`800px`" :before-close="handleCloseDialog">
<h3 class="text-xl font-bold text-gray-800 mb-2">标题 {{ currentItem.title }}</h3>
<p class="text-gray-500 mb-4">副标题 {{ currentItem.subtitle || '无' }}</p>
<div class="flex flex-wrap gap-4 mb-6 text-sm">
<span class="bg-gray-100 px-3 py-1 rounded">总编辑{{ currentItem.chief_editor || '无' }}</span>
<span class="bg-gray-100 px-3 py-1 rounded">图片编辑{{ currentItem.image_editors || '无' }}</span>
<span class="bg-gray-100 px-3 py-1 rounded">文字编辑{{ currentItem.text_editors || '无' }}</span>
<span class="bg-gray-100 px-3 py-1 rounded">校对者{{ currentItem.proofreaders || '无' }}</span>
<span class="bg-gray-100 px-3 py-1 rounded">审核者{{ currentItem.reviewers || '无' }}</span>
<span class="bg-gray-100 px-3 py-1 rounded">发布时间{{ currentItem.publish_time }}</span>
</div>
<h4 class="text-lg font-semibold text-gray-700 mb-2">简介</h4>
<p class="text-gray-600 mb-6">{{ currentItem.intro || '无简介' }}</p>
<h4 class="text-lg font-semibold text-gray-700 mb-2">详细内容</h4>
<div class="content-full text-gray-700 leading-relaxed whitespace-pre-wrap" v-html="currentItem.content || '当前项目暂无详细内容'">
</div>
</el-dialog>
</div>
<!-- 分页器 -->
<div class="fixed bottom-0 right-0 w-full p-4 bg-white shadow-[0_-2px_5px_rgba(0,0,0,0.05)] flex justify-end z-10 border-t border-gray-200">
<el-pagination
class="custom-pagination"
background
layout="total, sizes, prev, pager, next, jumper"
:total="total"
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[10, 20, 50, 100]"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
></el-pagination>
</div>
<!-- 编辑/新增抽屉 -->
<el-drawer
v-model="drawerVisible"
:title="form.id ? '编辑社会服务项目' : '新增社会服务项目'"
direction="rtl"
size="60%"
:before-close="handleDrawerClose"
destroy-on-close
>
<div class="publish-form-container">
<div class="form-group title-group">
<label class="form-label required">项目标题</label>
<el-input
v-model="form.title"
placeholder="请输入项目标题"
clearable
:disabled="isSubmitting"
max-length="255"
/>
</div>
<div class="form-group subtitle-group">
<label class="form-label">副标题</label>
<el-input
v-model="form.subtitle"
placeholder="请输入项目副标题(可选)"
clearable
:disabled="isSubmitting"
max-length="255"
/>
</div>
<div class="form-group cover-group">
<label class="form-label">项目封面</label>
<el-upload
action="http://localhost:8080/api/upload/cover"
name="image"
:show-file-list="false"
:on-success="handleCoverSuccess"
:before-upload="beforeCoverUpload"
:on-error="handleCoverError"
:disabled="isSubmitting"
>
<img v-if="form.cover_url" :src="form.cover_url" class="cover-preview" alt="封面"/>
<el-icon v-else class="cover-uploader-icon"><Plus /></el-icon>
</el-upload>
<p class="text-xs text-gray-500 mt-2">支持JPGPNG格式大小不超过5MB</p>
</div>
<div class="form-group intro-group">
<label class="form-label">项目简介</label>
<el-input
v-model="form.intro"
type="textarea"
:rows="3"
placeholder="请输入项目简介(纯文字,可选)"
clearable
:disabled="isSubmitting"
max-length="1000"
/>
</div>
<div class="form-group editors-group grid grid-cols-2 gap-4">
<div>
<label class="form-label">图片编辑者</label>
<el-input
v-model="form.image_editors"
placeholder="多个用逗号分隔(可选)"
clearable
:disabled="isSubmitting"
max-length="512"
/>
</div>
<div>
<label class="form-label">文字编辑者</label>
<el-input
v-model="form.text_editors"
placeholder="多个用逗号分隔(可选)"
clearable
:disabled="isSubmitting"
max-length="512"
/>
</div>
<div>
<label class="form-label">总编辑</label>
<el-input
v-model="form.chief_editor"
placeholder="请输入总编辑(可选)"
clearable
:disabled="isSubmitting"
max-length="100"
/>
</div>
<div>
<label class="form-label">校对者</label>
<el-input
v-model="form.proofreaders"
placeholder="多个用逗号分隔(可选)"
clearable
:disabled="isSubmitting"
max-length="512"
/>
</div>
<div class="col-span-2">
<label class="form-label">审核者</label>
<el-input
v-model="form.reviewers"
placeholder="多个用逗号分隔(可选)"
clearable
:disabled="isSubmitting"
max-length="512"
/>
</div>
</div>
<div class="form-group content-group">
<label class="form-label">详细内容Markdown格式</label>
<div ref="editorRef" class="quill-editor"></div>
</div>
</div>
<template #footer>
<div style="flex: auto">
<el-button @click="handleDrawerClose">取消</el-button>
<el-button type="primary" @click="submitItem" :loading="isSubmitting">
{{ form.id ? '更新项目' : '新增项目' }}
</el-button>
</div>
</template>
</el-drawer>
</el-config-provider>
</template>
<script lang="ts" setup>
import { ref, onMounted, watch, nextTick } from 'vue';
import {
ElMessage, ElMessageBox, ElDialog, ElConfigProvider, ElDrawer, ElInput, ElUpload, ElPagination
} from 'element-plus';
import { Edit, Delete, Plus, CirclePlus } from '@element-plus/icons-vue';
import Quill from 'quill';
import 'quill/dist/quill.snow.css';
import type { UploadProps } from 'element-plus';
import zhCn from 'element-plus/es/locale/lang/zh-cn'; // 导入中文语言包
// --- 类型定义(对应社会服务表结构) ---
interface SocialService {
id: number;
title: string;
subtitle: string | null;
cover_url: string | null;
intro: string | null;
content: string | null;
image_editors: string | null;
text_editors: string | null;
chief_editor: string | null;
proofreaders: string | null;
reviewers: string | null;
publish_time: string;
update_time: string;
is_delete: number;
}
interface ListSocialServiceReq {
page: number;
page_size: number;
}
// --- API 基地址 ---
const API_BASE_URL = 'http://localhost:8080/api';
// =================================================================
// 列表页相关状态与逻辑
// =================================================================
const loading = ref(true);
const tableData = ref<SocialService[]>([]);
const currentPage = ref(1);
const pageSize = ref(10);
const total = ref(0);
const contentDialogVisible = ref(false);
const currentItem = ref<SocialService>({
id: 0,
title: '',
subtitle: null,
cover_url: null,
intro: null,
content: null,
image_editors: null,
text_editors: null,
chief_editor: null,
proofreaders: null,
reviewers: null,
publish_time: '',
update_time: '',
is_delete: 0
});
// --- HTML清理函数用于简介截取 ---
const stripHtml = (html: string | null) => {
if (!html) return '';
const tempDiv = document.createElement('div');
tempDiv.innerHTML = html;
return tempDiv.textContent || tempDiv.innerText || '';
};
// --- 获取社会服务列表数据 ---
const fetchData = async () => {
loading.value = true;
try {
const reqData: ListSocialServiceReq = {
page: currentPage.value,
page_size: pageSize.value
};
const response = await fetch(`${API_BASE_URL}/social-service/government-program/list`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(reqData)
});
console.log("获取response响应对象", response);
if (!response.ok) throw new Error(`请求失败!状态码:${response.status}`);
const data = await response.json();
console.log("后端返回的完整消息体:", data); // 打印完整返回数据,确认结构
// 核心修改适配后端返回的小写字段和list/total命名
tableData.value = data.data?.list || []; // 后端是 data.list小写
total.value = data.data?.total || 0; // 后端是 data.total小写
console.log("解析后的数据列表tableData", tableData.value);
console.log("解析后的总条数total", total.value);
} catch (error) {
console.error('[ERROR] 获取社会服务列表失败:', error);
ElMessage.error('获取社会服务列表失败,请检查接口或网络!');
} finally {
loading.value = false;
}
};
onMounted(fetchData);
// --- 查看详情 ---
const handleViewContent = (row: SocialService) => {
currentItem.value = { ...row };
contentDialogVisible.value = true;
};
const handleCloseDialog = () => {
contentDialogVisible.value = false;
};
// --- 删除功能 ---
const handleDelete = (row: SocialService) => {
ElMessageBox.confirm(
`确定要删除社会服务项目《${row.title}》吗?此操作无法撤销!`,
'警告',
{
confirmButtonText: '确定删除',
cancelButtonText: '取消',
type: 'warning',
}
)
.then(async () => {
try {
const response = await fetch(`${API_BASE_URL}/social-service/government-program/${row.id}`, {
method: 'DELETE',
});
if (!response.ok) {
const errData = await response.json().catch(() => null);
throw new Error(errData?.message || '删除失败');
}
ElMessage.success('删除成功!');
fetchData();
} catch (error) {
console.error('[ERROR] 删除社会服务项目失败:', error);
ElMessage.error(`删除失败: ${(error as Error).message}`);
}
})
.catch(() => {
ElMessage.info('已取消删除');
});
};
// --- 分页事件 ---
const handleSizeChange = (val: number) => {
pageSize.value = val;
currentPage.value = 1;
fetchData();
};
const handleCurrentChange = (val: number) => {
currentPage.value = val;
fetchData();
};
// =================================================================
// 抽屉编辑/新增相关状态与逻辑
// =================================================================
const drawerVisible = ref(false);
const isSubmitting = ref(false);
// --- 表单默认状态(对应社会服务表字段) ---
const defaultFormState = () => ({
id: null as number | null,
title: '',
subtitle: '',
cover_url: '',
intro: '',
content: '',
image_editors: '',
text_editors: '',
chief_editor: '',
proofreaders: '',
reviewers: '',
});
const form = ref(defaultFormState());
// --- 富文本编辑器初始化 ---
const editorRef = ref<HTMLDivElement | null>(null);
let quillInstance: Quill | null = null;
const initQuillEditor = () => {
if (editorRef.value && !quillInstance) {
quillInstance = new Quill(editorRef.value, {
theme: 'snow',
modules: {
toolbar: {
container: [
['bold', 'italic', 'underline'],
['link', 'image'],
[{ 'header': [1, 2, 3, false] }],
[{ 'list': 'ordered'}, { 'list': 'bullet' }]
],
handlers: {
image: handleEditorImageUpload,
}
}
},
placeholder: '请输入项目详细内容支持Markdown格式...'
});
// 监听粘贴图片转base64上传
quillInstance.on('text-change', (delta, _, source) => {
if (source === 'user') {
const pastedBase64 = getPastedBase64Image(delta);
if (pastedBase64) {
const selection = quillInstance?.getSelection();
if (!selection) return;
const imageIndex = selection.index;
quillInstance?.deleteText(imageIndex, 1);
const blob = base64ToBlob(pastedBase64);
if (blob) {
const file = new File([blob], `pasted-image-${Date.now()}.png`, { type: blob.type });
uploadEditorImage(file, imageIndex);
}
}
}
});
}
};
// --- 监听抽屉显示/隐藏,初始化/销毁编辑器 ---
watch(drawerVisible, (visible) => {
if (visible) {
nextTick(() => {
initQuillEditor();
if (quillInstance) {
quillInstance.root.innerHTML = form.value.content || '';
}
});
} else {
quillInstance = null;
}
});
// --- 编辑器图片上传相关函数 ---
function handleEditorImageUpload() {
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = 'image/*';
fileInput.onchange = () => {
const file = fileInput.files?.[0];
if (!file) return;
const selection = quillInstance?.getSelection();
const insertIndex = selection ? selection.index : quillInstance?.getLength() || 0;
uploadEditorImage(file, insertIndex);
fileInput.value = '';
};
fileInput.click();
}
async function uploadEditorImage(file: File, insertIndex: number) {
if (!quillInstance) return;
quillInstance.insertText(insertIndex, '[图片上传中...]', { color: '#999', italic: true });
try {
const formData = new FormData();
formData.append('image', file);
const response = await fetch(`${API_BASE_URL}/upload/image`, {
method: 'POST',
body: formData,
});
if (!response.ok) throw new Error(`HTTP状态码${response.status}`);
const result = await response.json();
const imageUrl = result.data?.url;
if (!imageUrl) throw new Error('未获取到图片地址');
quillInstance.deleteText(insertIndex, '[图片上传中...]'.length);
quillInstance.insertEmbed(insertIndex, 'image', imageUrl);
quillInstance.setSelection(insertIndex + 1);
} catch (err) {
const errMsg = err instanceof Error ? err.message : '未知错误';
quillInstance.deleteText(insertIndex, '[图片上传中...]'.length);
quillInstance.insertText(insertIndex, `[图片上传失败:${errMsg}]`, { color: '#f56c6c', italic: true });
ElMessage.error(`编辑器图片上传失败:${errMsg}`);
}
}
// --- base64图片处理 ---
function getPastedBase64Image(delta: any): string | null {
if (!delta?.ops) return null;
for (const op of delta.ops) {
if (
op.insert &&
typeof op.insert === 'object' &&
op.insert.image &&
op.insert.image.startsWith('data:image')
) {
return op.insert.image;
}
}
return null;
}
function base64ToBlob(base64: string): Blob | null {
try {
const [prefix, data] = base64.split(',');
if (!prefix || !data) throw new Error('base64格式错误');
const mimeMatch = prefix.match(/data:(.*?);/);
if (!mimeMatch) throw new Error('无法解析图片类型');
const mime = mimeMatch[1];
const binaryData = atob(data);
const bytes = new Uint8Array(binaryData.length);
for (let i = 0; i < binaryData.length; i++) {
bytes[i] = binaryData.charCodeAt(i);
}
return new Blob([bytes], { type: mime });
} catch (error) {
console.error('base64转Blob失败', error);
ElMessage.error('解析粘贴的图片失败!');
return null;
}
}
// --- 新增项目 ---
const handleAdd = () => {
form.value = defaultFormState();
drawerVisible.value = true;
};
// --- 编辑项目 ---
const handleEdit = (row: SocialService) => {
form.value = {
id: row.id,
title: row.title,
subtitle: row.subtitle || '',
cover_url: row.cover_url || '',
intro: row.intro || '',
content: row.content || '',
image_editors: row.image_editors || '',
text_editors: row.text_editors || '',
chief_editor: row.chief_editor || '',
proofreaders: row.proofreaders || '',
reviewers: row.reviewers || '',
};
drawerVisible.value = true;
};
const handleDrawerClose = () => {
drawerVisible.value = false;
form.value = defaultFormState();
};
// --- 提交表单(新增/更新) ---
const submitItem = async () => {
// 表单校验
if (!form.value.title.trim()) {
ElMessage.warning('请输入项目标题');
return;
}
isSubmitting.value = true;
const contentHTML = quillInstance?.root.innerHTML || '';
// 构造提交数据
const submitData = {
id:form.value.id,
title: form.value.title.trim(),
subtitle: form.value.subtitle.trim() || null,
cover_url: form.value.cover_url.trim() || null,
intro: form.value.intro.trim() || null,
content: contentHTML.trim() || null,
image_editors: form.value.image_editors.trim() || null,
text_editors: form.value.text_editors.trim() || null,
chief_editor: form.value.chief_editor.trim() || null,
proofreaders: form.value.proofreaders.trim() || null,
reviewers: form.value.reviewers.trim() || null,
};
try {
let url = `${API_BASE_URL}/social-service/government-program`;
let method = 'POST';
// 编辑模式修改URL和请求方法
if (form.value.id) {
url = `${API_BASE_URL}/social-service/government-program`;
method = 'PUT';
}
const response = await fetch(url, {
method: method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(submitData),
});
if (!response.ok) {
const errData = await response.json().catch(() => null);
throw new Error(errData?.message || '提交失败');
}
ElMessage.success(form.value.id ? '项目更新成功!' : '项目新增成功!');
drawerVisible.value = false;
form.value = defaultFormState();
fetchData();
} catch (error) {
const err = error as Error;
ElMessage.error(`${form.value.id ? '更新' : '新增'}失败: ${err.message}`);
} finally {
isSubmitting.value = false;
}
};
// --- 封面上传处理 ---
const handleCoverSuccess: UploadProps['onSuccess'] = (response) => {
const ossUrl = response.data?.url;
if (ossUrl) {
form.value.cover_url = ossUrl;
ElMessage.success('封面上传成功');
} else {
ElMessage.error('封面上传失败');
}
};
const beforeCoverUpload: UploadProps['beforeUpload'] = (rawFile) => {
const isImage = rawFile.type.startsWith('image/');
if (!isImage) {
ElMessage.error('请上传图片文件!');
return false;
}
const isLt5M = rawFile.size / 1024 / 1024 < 5;
if (!isLt5M) {
ElMessage.error('图片大小不能超过 5MB!');
}
return isImage && isLt5M;
};
const handleCoverError = () => {
ElMessage.error('封面上传失败');
};
</script>
<style scoped>
/* 基础样式复用+适配 */
.el-table .el-table__cell { vertical-align: middle; }
.el-table__header-wrapper th { background-color: #fafafa !important; font-weight: 600; color: #333; }
.action-buttons .el-button { margin-right: 8px; }
.content-full { min-height: 300px; padding: 20px; background-color: #f9fafb; border-radius: 8px; }
.el-dialog__title { font-size: 18px !important; font-weight: 600 !important; }
/* 分页器样式 */
.custom-pagination { justify-content: flex-end !important; }
.custom-pagination .el-pagination__total,
.custom-pagination .el-pagination__sizes,
.custom-pagination .el-pagination__jump { margin-right: 16px !important; }
.custom-pagination .el-pagination__jump .el-input { width: 60px !important; }
/* 抽屉内表单样式 */
.publish-form-container { padding: 0 20px; }
.form-group { margin-bottom: 24px; }
.form-label { display: block; margin-bottom: 8px; color: #333; font-size: 14px; font-weight: 600; }
.form-label.required::after { content: '*'; color: #f56c6c; margin-left: 4px; }
.quill-editor { height: 350px; border-radius: 4px; border: 1px solid #dcdfe6; }
.cover-uploader .el-upload { border: 1px dashed #d9d9d9; border-radius: 6px; cursor: pointer; position: relative; overflow: hidden; }
.cover-uploader .el-upload:hover { border-color: #409EFF; }
.cover-uploader-icon { font-size: 28px; color: #8c939d; width: 178px; height: 178px; text-align: center; line-height: 178px; }
.cover-preview { width: 178px; height: 178px; display: block; object-fit: cover; }
/* 编辑者信息网格布局 */
.grid { display: grid; }
.grid-cols-2 { grid-template-columns: repeat(2, 1fr); }
.gap-4 { gap: 16px; }
/* 详情页样式 */
.el-dialog .flex { display: flex; }
.el-dialog .flex-wrap { flex-wrap: wrap; }
.el-dialog .gap-4 { gap: 16px; }
.el-dialog .bg-gray-100 { background-color: #f3f4f6; }
.el-dialog .px-3 { padding-left: 12px; padding-right: 12px; }
.el-dialog .py-1 { padding-top: 4px; padding-bottom: 4px; }
.el-dialog .rounded { border-radius: 4px; }
.el-dialog .text-sm { font-size: 12px; }
</style>

View File

@@ -0,0 +1,713 @@
<template>
<el-config-provider :locale="zhCn">
<div class="p-6 bg-white rounded-lg shadow-md min-h-screen pb-24" v-loading="loading">
<div class="flex justify-between items-center mb-6">
<span class="text-2xl font-bold text-gray-700">社会服务管理列表</span>
<el-button type="primary" :icon="CirclePlus" @click="handleAdd">新增社会服务项目</el-button>
</div>
<el-table :data="tableData" style="width: 100%" row-key="id" max-height="82vh">
<el-table-column prop="id" label="ID" width="80" sortable fixed></el-table-column>
<el-table-column label="封面" width="180">
<template #default="scope">
<el-image
style="width: 120px; height: 70px; border-radius: 6px;"
:src="scope.row.cover_url"
:preview-src-list="[scope.row.cover_url]"
fit="cover"
:preview-teleported="true"
hide-on-click-modal
>
<template #error>
<div class="flex items-center justify-center w-full h-full bg-gray-100 text-gray-500">无封面</div>
</template>
</el-image>
</template>
</el-table-column>
<el-table-column prop="title" label="标题" min-width="200" show-overflow-tooltip>
<template #default="scope">
<span class="font-semibold">{{ scope.row.title }}</span>
</template>
</el-table-column>
<el-table-column prop="subtitle" label="副标题" min-width="200" show-overflow-tooltip></el-table-column>
<el-table-column prop="intro" label="简介" min-width="250" show-overflow-tooltip>
<template #default="scope">
<span>{{ scope.row.intro?.length > 50 ? `${scope.row.intro.slice(0, 50)}...` : scope.row.intro || '无' }}</span>
</template>
</el-table-column>
<el-table-column prop="chief_editor" label="总编辑" width="120" show-overflow-tooltip></el-table-column>
<el-table-column prop="image_editors" label="图片编辑" width="150" show-overflow-tooltip></el-table-column>
<el-table-column prop="publish_time" label="发布时间" width="180" sortable></el-table-column>
<el-table-column prop="update_time" label="更新时间" width="180" sortable></el-table-column>
<el-table-column label="操作" width="220" fixed="right">
<template #default="scope">
<div class="action-buttons">
<el-button size="small" type="primary" :icon="Edit" @click="handleEdit(scope.row)">编辑</el-button>
<el-button size="small" type="danger" :icon="Delete" @click="handleDelete(scope.row)">删除</el-button>
</div>
</template>
</el-table-column>
</el-table>
<!-- 内容详情弹窗 -->
<el-dialog v-model="contentDialogVisible" title="社会服务项目详情" :width="`800px`" :before-close="handleCloseDialog">
<h3 class="text-xl font-bold text-gray-800 mb-2">标题 {{ currentItem.title }}</h3>
<p class="text-gray-500 mb-4">副标题 {{ currentItem.subtitle || '无' }}</p>
<div class="flex flex-wrap gap-4 mb-6 text-sm">
<span class="bg-gray-100 px-3 py-1 rounded">总编辑{{ currentItem.chief_editor || '无' }}</span>
<span class="bg-gray-100 px-3 py-1 rounded">图片编辑{{ currentItem.image_editors || '无' }}</span>
<span class="bg-gray-100 px-3 py-1 rounded">文字编辑{{ currentItem.text_editors || '无' }}</span>
<span class="bg-gray-100 px-3 py-1 rounded">校对者{{ currentItem.proofreaders || '无' }}</span>
<span class="bg-gray-100 px-3 py-1 rounded">审核者{{ currentItem.reviewers || '无' }}</span>
<span class="bg-gray-100 px-3 py-1 rounded">发布时间{{ currentItem.publish_time }}</span>
</div>
<h4 class="text-lg font-semibold text-gray-700 mb-2">简介</h4>
<p class="text-gray-600 mb-6">{{ currentItem.intro || '无简介' }}</p>
<h4 class="text-lg font-semibold text-gray-700 mb-2">详细内容</h4>
<div class="content-full text-gray-700 leading-relaxed whitespace-pre-wrap" v-html="currentItem.content || '当前项目暂无详细内容'">
</div>
</el-dialog>
</div>
<!-- 分页器 -->
<div class="fixed bottom-0 right-0 w-full p-4 bg-white shadow-[0_-2px_5px_rgba(0,0,0,0.05)] flex justify-end z-10 border-t border-gray-200">
<el-pagination
class="custom-pagination"
background
layout="total, sizes, prev, pager, next, jumper"
:total="total"
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[10, 20, 50, 100]"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
></el-pagination>
</div>
<!-- 编辑/新增抽屉 -->
<el-drawer
v-model="drawerVisible"
:title="form.id ? '编辑社会服务项目' : '新增社会服务项目'"
direction="rtl"
size="60%"
:before-close="handleDrawerClose"
destroy-on-close
>
<div class="publish-form-container">
<div class="form-group title-group">
<label class="form-label required">项目标题</label>
<el-input
v-model="form.title"
placeholder="请输入项目标题"
clearable
:disabled="isSubmitting"
max-length="255"
/>
</div>
<div class="form-group subtitle-group">
<label class="form-label">副标题</label>
<el-input
v-model="form.subtitle"
placeholder="请输入项目副标题(可选)"
clearable
:disabled="isSubmitting"
max-length="255"
/>
</div>
<div class="form-group cover-group">
<label class="form-label">项目封面</label>
<el-upload
action="http://localhost:8080/api/upload/cover"
name="image"
:show-file-list="false"
:on-success="handleCoverSuccess"
:before-upload="beforeCoverUpload"
:on-error="handleCoverError"
:disabled="isSubmitting"
>
<img v-if="form.cover_url" :src="form.cover_url" class="cover-preview" alt="封面"/>
<el-icon v-else class="cover-uploader-icon"><Plus /></el-icon>
</el-upload>
<p class="text-xs text-gray-500 mt-2">支持JPGPNG格式大小不超过5MB</p>
</div>
<div class="form-group intro-group">
<label class="form-label">项目简介</label>
<el-input
v-model="form.intro"
type="textarea"
:rows="3"
placeholder="请输入项目简介(纯文字,可选)"
clearable
:disabled="isSubmitting"
max-length="1000"
/>
</div>
<div class="form-group editors-group grid grid-cols-2 gap-4">
<div>
<label class="form-label">图片编辑者</label>
<el-input
v-model="form.image_editors"
placeholder="多个用逗号分隔(可选)"
clearable
:disabled="isSubmitting"
max-length="512"
/>
</div>
<div>
<label class="form-label">文字编辑者</label>
<el-input
v-model="form.text_editors"
placeholder="多个用逗号分隔(可选)"
clearable
:disabled="isSubmitting"
max-length="512"
/>
</div>
<div>
<label class="form-label">总编辑</label>
<el-input
v-model="form.chief_editor"
placeholder="请输入总编辑(可选)"
clearable
:disabled="isSubmitting"
max-length="100"
/>
</div>
<div>
<label class="form-label">校对者</label>
<el-input
v-model="form.proofreaders"
placeholder="多个用逗号分隔(可选)"
clearable
:disabled="isSubmitting"
max-length="512"
/>
</div>
<div class="col-span-2">
<label class="form-label">审核者</label>
<el-input
v-model="form.reviewers"
placeholder="多个用逗号分隔(可选)"
clearable
:disabled="isSubmitting"
max-length="512"
/>
</div>
</div>
<div class="form-group content-group">
<label class="form-label">详细内容Markdown格式</label>
<div ref="editorRef" class="quill-editor"></div>
</div>
</div>
<template #footer>
<div style="flex: auto">
<el-button @click="handleDrawerClose">取消</el-button>
<el-button type="primary" @click="submitItem" :loading="isSubmitting">
{{ form.id ? '更新项目' : '新增项目' }}
</el-button>
</div>
</template>
</el-drawer>
</el-config-provider>
</template>
<script lang="ts" setup>
import { ref, onMounted, watch, nextTick } from 'vue';
import {
ElMessage, ElMessageBox, ElDialog, ElConfigProvider, ElDrawer, ElInput, ElUpload, ElPagination
} from 'element-plus';
import { Edit, Delete, Plus, CirclePlus } from '@element-plus/icons-vue';
import Quill from 'quill';
import 'quill/dist/quill.snow.css';
import type { UploadProps } from 'element-plus';
import zhCn from 'element-plus/es/locale/lang/zh-cn'; // 导入中文语言包
// --- 类型定义(对应社会服务表结构) ---
interface SocialService {
id: number;
title: string;
subtitle: string | null;
cover_url: string | null;
intro: string | null;
content: string | null;
image_editors: string | null;
text_editors: string | null;
chief_editor: string | null;
proofreaders: string | null;
reviewers: string | null;
publish_time: string;
update_time: string;
is_delete: number;
}
interface ListSocialServiceReq {
page: number;
page_size: number;
}
// --- API 基地址 ---
const API_BASE_URL = 'http://localhost:8080/api';
// =================================================================
// 列表页相关状态与逻辑
// =================================================================
const loading = ref(true);
const tableData = ref<SocialService[]>([]);
const currentPage = ref(1);
const pageSize = ref(10);
const total = ref(0);
const contentDialogVisible = ref(false);
const currentItem = ref<SocialService>({
id: 0,
title: '',
subtitle: null,
cover_url: null,
intro: null,
content: null,
image_editors: null,
text_editors: null,
chief_editor: null,
proofreaders: null,
reviewers: null,
publish_time: '',
update_time: '',
is_delete: 0
});
// --- HTML清理函数用于简介截取 ---
const stripHtml = (html: string | null) => {
if (!html) return '';
const tempDiv = document.createElement('div');
tempDiv.innerHTML = html;
return tempDiv.textContent || tempDiv.innerText || '';
};
// --- 获取社会服务列表数据 ---
const fetchData = async () => {
loading.value = true;
try {
const reqData: ListSocialServiceReq = {
page: currentPage.value,
page_size: pageSize.value
};
const response = await fetch(`${API_BASE_URL}/social-service/internship/list`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(reqData)
});
console.log("获取response响应对象", response);
if (!response.ok) throw new Error(`请求失败!状态码:${response.status}`);
const data = await response.json();
console.log("后端返回的完整消息体:", data); // 打印完整返回数据,确认结构
// 核心修改适配后端返回的小写字段和list/total命名
tableData.value = data.data?.list || []; // 后端是 data.list小写
total.value = data.data?.total || 0; // 后端是 data.total小写
console.log("解析后的数据列表tableData", tableData.value);
console.log("解析后的总条数total", total.value);
} catch (error) {
console.error('[ERROR] 获取社会服务列表失败:', error);
ElMessage.error('获取社会服务列表失败,请检查接口或网络!');
} finally {
loading.value = false;
}
};
onMounted(fetchData);
// --- 查看详情 ---
const handleViewContent = (row: SocialService) => {
currentItem.value = { ...row };
contentDialogVisible.value = true;
};
const handleCloseDialog = () => {
contentDialogVisible.value = false;
};
// --- 删除功能 ---
const handleDelete = (row: SocialService) => {
ElMessageBox.confirm(
`确定要删除社会服务项目《${row.title}》吗?此操作无法撤销!`,
'警告',
{
confirmButtonText: '确定删除',
cancelButtonText: '取消',
type: 'warning',
}
)
.then(async () => {
try {
const response = await fetch(`${API_BASE_URL}/social-service/internship/${row.id}`, {
method: 'DELETE',
});
if (!response.ok) {
const errData = await response.json().catch(() => null);
throw new Error(errData?.message || '删除失败');
}
ElMessage.success('删除成功!');
fetchData();
} catch (error) {
console.error('[ERROR] 删除社会服务项目失败:', error);
ElMessage.error(`删除失败: ${(error as Error).message}`);
}
})
.catch(() => {
ElMessage.info('已取消删除');
});
};
// --- 分页事件 ---
const handleSizeChange = (val: number) => {
pageSize.value = val;
currentPage.value = 1;
fetchData();
};
const handleCurrentChange = (val: number) => {
currentPage.value = val;
fetchData();
};
// =================================================================
// 抽屉编辑/新增相关状态与逻辑
// =================================================================
const drawerVisible = ref(false);
const isSubmitting = ref(false);
// --- 表单默认状态(对应社会服务表字段) ---
const defaultFormState = () => ({
id: null as number | null,
title: '',
subtitle: '',
cover_url: '',
intro: '',
content: '',
image_editors: '',
text_editors: '',
chief_editor: '',
proofreaders: '',
reviewers: '',
});
const form = ref(defaultFormState());
// --- 富文本编辑器初始化 ---
const editorRef = ref<HTMLDivElement | null>(null);
let quillInstance: Quill | null = null;
const initQuillEditor = () => {
if (editorRef.value && !quillInstance) {
quillInstance = new Quill(editorRef.value, {
theme: 'snow',
modules: {
toolbar: {
container: [
['bold', 'italic', 'underline'],
['link', 'image'],
[{ 'header': [1, 2, 3, false] }],
[{ 'list': 'ordered'}, { 'list': 'bullet' }]
],
handlers: {
image: handleEditorImageUpload,
}
}
},
placeholder: '请输入项目详细内容支持Markdown格式...'
});
// 监听粘贴图片转base64上传
quillInstance.on('text-change', (delta, _, source) => {
if (source === 'user') {
const pastedBase64 = getPastedBase64Image(delta);
if (pastedBase64) {
const selection = quillInstance?.getSelection();
if (!selection) return;
const imageIndex = selection.index;
quillInstance?.deleteText(imageIndex, 1);
const blob = base64ToBlob(pastedBase64);
if (blob) {
const file = new File([blob], `pasted-image-${Date.now()}.png`, { type: blob.type });
uploadEditorImage(file, imageIndex);
}
}
}
});
}
};
// --- 监听抽屉显示/隐藏,初始化/销毁编辑器 ---
watch(drawerVisible, (visible) => {
if (visible) {
nextTick(() => {
initQuillEditor();
if (quillInstance) {
quillInstance.root.innerHTML = form.value.content || '';
}
});
} else {
quillInstance = null;
}
});
// --- 编辑器图片上传相关函数 ---
function handleEditorImageUpload() {
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = 'image/*';
fileInput.onchange = () => {
const file = fileInput.files?.[0];
if (!file) return;
const selection = quillInstance?.getSelection();
const insertIndex = selection ? selection.index : quillInstance?.getLength() || 0;
uploadEditorImage(file, insertIndex);
fileInput.value = '';
};
fileInput.click();
}
async function uploadEditorImage(file: File, insertIndex: number) {
if (!quillInstance) return;
quillInstance.insertText(insertIndex, '[图片上传中...]', { color: '#999', italic: true });
try {
const formData = new FormData();
formData.append('image', file);
const response = await fetch(`${API_BASE_URL}/upload/image`, {
method: 'POST',
body: formData,
});
if (!response.ok) throw new Error(`HTTP状态码${response.status}`);
const result = await response.json();
const imageUrl = result.data?.url;
if (!imageUrl) throw new Error('未获取到图片地址');
quillInstance.deleteText(insertIndex, '[图片上传中...]'.length);
quillInstance.insertEmbed(insertIndex, 'image', imageUrl);
quillInstance.setSelection(insertIndex + 1);
} catch (err) {
const errMsg = err instanceof Error ? err.message : '未知错误';
quillInstance.deleteText(insertIndex, '[图片上传中...]'.length);
quillInstance.insertText(insertIndex, `[图片上传失败:${errMsg}]`, { color: '#f56c6c', italic: true });
ElMessage.error(`编辑器图片上传失败:${errMsg}`);
}
}
// --- base64图片处理 ---
function getPastedBase64Image(delta: any): string | null {
if (!delta?.ops) return null;
for (const op of delta.ops) {
if (
op.insert &&
typeof op.insert === 'object' &&
op.insert.image &&
op.insert.image.startsWith('data:image')
) {
return op.insert.image;
}
}
return null;
}
function base64ToBlob(base64: string): Blob | null {
try {
const [prefix, data] = base64.split(',');
if (!prefix || !data) throw new Error('base64格式错误');
const mimeMatch = prefix.match(/data:(.*?);/);
if (!mimeMatch) throw new Error('无法解析图片类型');
const mime = mimeMatch[1];
const binaryData = atob(data);
const bytes = new Uint8Array(binaryData.length);
for (let i = 0; i < binaryData.length; i++) {
bytes[i] = binaryData.charCodeAt(i);
}
return new Blob([bytes], { type: mime });
} catch (error) {
console.error('base64转Blob失败', error);
ElMessage.error('解析粘贴的图片失败!');
return null;
}
}
// --- 新增项目 ---
const handleAdd = () => {
form.value = defaultFormState();
drawerVisible.value = true;
};
// --- 编辑项目 ---
const handleEdit = (row: SocialService) => {
form.value = {
id: row.id,
title: row.title,
subtitle: row.subtitle || '',
cover_url: row.cover_url || '',
intro: row.intro || '',
content: row.content || '',
image_editors: row.image_editors || '',
text_editors: row.text_editors || '',
chief_editor: row.chief_editor || '',
proofreaders: row.proofreaders || '',
reviewers: row.reviewers || '',
};
drawerVisible.value = true;
};
const handleDrawerClose = () => {
drawerVisible.value = false;
form.value = defaultFormState();
};
// --- 提交表单(新增/更新) ---
const submitItem = async () => {
// 表单校验
if (!form.value.title.trim()) {
ElMessage.warning('请输入项目标题');
return;
}
isSubmitting.value = true;
const contentHTML = quillInstance?.root.innerHTML || '';
// 构造提交数据
const submitData = {
id:form.value.id,
title: form.value.title.trim(),
subtitle: form.value.subtitle.trim() || null,
cover_url: form.value.cover_url.trim() || null,
intro: form.value.intro.trim() || null,
content: contentHTML.trim() || null,
image_editors: form.value.image_editors.trim() || null,
text_editors: form.value.text_editors.trim() || null,
chief_editor: form.value.chief_editor.trim() || null,
proofreaders: form.value.proofreaders.trim() || null,
reviewers: form.value.reviewers.trim() || null,
};
try {
let url = `${API_BASE_URL}/social-service/internship`;
let method = 'POST';
// 编辑模式修改URL和请求方法
if (form.value.id) {
url = `${API_BASE_URL}/social-service/internship`;
method = 'PUT';
}
const response = await fetch(url, {
method: method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(submitData),
});
if (!response.ok) {
const errData = await response.json().catch(() => null);
throw new Error(errData?.message || '提交失败');
}
ElMessage.success(form.value.id ? '项目更新成功!' : '项目新增成功!');
drawerVisible.value = false;
form.value = defaultFormState();
fetchData();
} catch (error) {
const err = error as Error;
ElMessage.error(`${form.value.id ? '更新' : '新增'}失败: ${err.message}`);
} finally {
isSubmitting.value = false;
}
};
// --- 封面上传处理 ---
const handleCoverSuccess: UploadProps['onSuccess'] = (response) => {
const ossUrl = response.data?.url;
if (ossUrl) {
form.value.cover_url = ossUrl;
ElMessage.success('封面上传成功');
} else {
ElMessage.error('封面上传失败');
}
};
const beforeCoverUpload: UploadProps['beforeUpload'] = (rawFile) => {
const isImage = rawFile.type.startsWith('image/');
if (!isImage) {
ElMessage.error('请上传图片文件!');
return false;
}
const isLt5M = rawFile.size / 1024 / 1024 < 5;
if (!isLt5M) {
ElMessage.error('图片大小不能超过 5MB!');
}
return isImage && isLt5M;
};
const handleCoverError = () => {
ElMessage.error('封面上传失败');
};
</script>
<style scoped>
/* 基础样式复用+适配 */
.el-table .el-table__cell { vertical-align: middle; }
.el-table__header-wrapper th { background-color: #fafafa !important; font-weight: 600; color: #333; }
.action-buttons .el-button { margin-right: 8px; }
.content-full { min-height: 300px; padding: 20px; background-color: #f9fafb; border-radius: 8px; }
.el-dialog__title { font-size: 18px !important; font-weight: 600 !important; }
/* 分页器样式 */
.custom-pagination { justify-content: flex-end !important; }
.custom-pagination .el-pagination__total,
.custom-pagination .el-pagination__sizes,
.custom-pagination .el-pagination__jump { margin-right: 16px !important; }
.custom-pagination .el-pagination__jump .el-input { width: 60px !important; }
/* 抽屉内表单样式 */
.publish-form-container { padding: 0 20px; }
.form-group { margin-bottom: 24px; }
.form-label { display: block; margin-bottom: 8px; color: #333; font-size: 14px; font-weight: 600; }
.form-label.required::after { content: '*'; color: #f56c6c; margin-left: 4px; }
.quill-editor { height: 350px; border-radius: 4px; border: 1px solid #dcdfe6; }
.cover-uploader .el-upload { border: 1px dashed #d9d9d9; border-radius: 6px; cursor: pointer; position: relative; overflow: hidden; }
.cover-uploader .el-upload:hover { border-color: #409EFF; }
.cover-uploader-icon { font-size: 28px; color: #8c939d; width: 178px; height: 178px; text-align: center; line-height: 178px; }
.cover-preview { width: 178px; height: 178px; display: block; object-fit: cover; }
/* 编辑者信息网格布局 */
.grid { display: grid; }
.grid-cols-2 { grid-template-columns: repeat(2, 1fr); }
.gap-4 { gap: 16px; }
/* 详情页样式 */
.el-dialog .flex { display: flex; }
.el-dialog .flex-wrap { flex-wrap: wrap; }
.el-dialog .gap-4 { gap: 16px; }
.el-dialog .bg-gray-100 { background-color: #f3f4f6; }
.el-dialog .px-3 { padding-left: 12px; padding-right: 12px; }
.el-dialog .py-1 { padding-top: 4px; padding-bottom: 4px; }
.el-dialog .rounded { border-radius: 4px; }
.el-dialog .text-sm { font-size: 12px; }
</style>

View File

@@ -0,0 +1,713 @@
<template>
<el-config-provider :locale="zhCn">
<div class="p-6 bg-white rounded-lg shadow-md min-h-screen pb-24" v-loading="loading">
<div class="flex justify-between items-center mb-6">
<span class="text-2xl font-bold text-gray-700">社会服务管理列表</span>
<el-button type="primary" :icon="CirclePlus" @click="handleAdd">新增社会服务项目</el-button>
</div>
<el-table :data="tableData" style="width: 100%" row-key="id" max-height="82vh">
<el-table-column prop="id" label="ID" width="80" sortable fixed></el-table-column>
<el-table-column label="封面" width="180">
<template #default="scope">
<el-image
style="width: 120px; height: 70px; border-radius: 6px;"
:src="scope.row.cover_url"
:preview-src-list="[scope.row.cover_url]"
fit="cover"
:preview-teleported="true"
hide-on-click-modal
>
<template #error>
<div class="flex items-center justify-center w-full h-full bg-gray-100 text-gray-500">无封面</div>
</template>
</el-image>
</template>
</el-table-column>
<el-table-column prop="title" label="标题" min-width="200" show-overflow-tooltip>
<template #default="scope">
<span class="font-semibold">{{ scope.row.title }}</span>
</template>
</el-table-column>
<el-table-column prop="subtitle" label="副标题" min-width="200" show-overflow-tooltip></el-table-column>
<el-table-column prop="intro" label="简介" min-width="250" show-overflow-tooltip>
<template #default="scope">
<span>{{ scope.row.intro?.length > 50 ? `${scope.row.intro.slice(0, 50)}...` : scope.row.intro || '无' }}</span>
</template>
</el-table-column>
<el-table-column prop="chief_editor" label="总编辑" width="120" show-overflow-tooltip></el-table-column>
<el-table-column prop="image_editors" label="图片编辑" width="150" show-overflow-tooltip></el-table-column>
<el-table-column prop="publish_time" label="发布时间" width="180" sortable></el-table-column>
<el-table-column prop="update_time" label="更新时间" width="180" sortable></el-table-column>
<el-table-column label="操作" width="220" fixed="right">
<template #default="scope">
<div class="action-buttons">
<el-button size="small" type="primary" :icon="Edit" @click="handleEdit(scope.row)">编辑</el-button>
<el-button size="small" type="danger" :icon="Delete" @click="handleDelete(scope.row)">删除</el-button>
</div>
</template>
</el-table-column>
</el-table>
<!-- 内容详情弹窗 -->
<el-dialog v-model="contentDialogVisible" title="社会服务项目详情" :width="`800px`" :before-close="handleCloseDialog">
<h3 class="text-xl font-bold text-gray-800 mb-2">标题 {{ currentItem.title }}</h3>
<p class="text-gray-500 mb-4">副标题 {{ currentItem.subtitle || '无' }}</p>
<div class="flex flex-wrap gap-4 mb-6 text-sm">
<span class="bg-gray-100 px-3 py-1 rounded">总编辑{{ currentItem.chief_editor || '无' }}</span>
<span class="bg-gray-100 px-3 py-1 rounded">图片编辑{{ currentItem.image_editors || '无' }}</span>
<span class="bg-gray-100 px-3 py-1 rounded">文字编辑{{ currentItem.text_editors || '无' }}</span>
<span class="bg-gray-100 px-3 py-1 rounded">校对者{{ currentItem.proofreaders || '无' }}</span>
<span class="bg-gray-100 px-3 py-1 rounded">审核者{{ currentItem.reviewers || '无' }}</span>
<span class="bg-gray-100 px-3 py-1 rounded">发布时间{{ currentItem.publish_time }}</span>
</div>
<h4 class="text-lg font-semibold text-gray-700 mb-2">简介</h4>
<p class="text-gray-600 mb-6">{{ currentItem.intro || '无简介' }}</p>
<h4 class="text-lg font-semibold text-gray-700 mb-2">详细内容</h4>
<div class="content-full text-gray-700 leading-relaxed whitespace-pre-wrap" v-html="currentItem.content || '当前项目暂无详细内容'">
</div>
</el-dialog>
</div>
<!-- 分页器 -->
<div class="fixed bottom-0 right-0 w-full p-4 bg-white shadow-[0_-2px_5px_rgba(0,0,0,0.05)] flex justify-end z-10 border-t border-gray-200">
<el-pagination
class="custom-pagination"
background
layout="total, sizes, prev, pager, next, jumper"
:total="total"
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[10, 20, 50, 100]"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
></el-pagination>
</div>
<!-- 编辑/新增抽屉 -->
<el-drawer
v-model="drawerVisible"
:title="form.id ? '编辑社会服务项目' : '新增社会服务项目'"
direction="rtl"
size="60%"
:before-close="handleDrawerClose"
destroy-on-close
>
<div class="publish-form-container">
<div class="form-group title-group">
<label class="form-label required">项目标题</label>
<el-input
v-model="form.title"
placeholder="请输入项目标题"
clearable
:disabled="isSubmitting"
max-length="255"
/>
</div>
<div class="form-group subtitle-group">
<label class="form-label">副标题</label>
<el-input
v-model="form.subtitle"
placeholder="请输入项目副标题(可选)"
clearable
:disabled="isSubmitting"
max-length="255"
/>
</div>
<div class="form-group cover-group">
<label class="form-label">项目封面</label>
<el-upload
action="http://localhost:8080/api/upload/cover"
name="image"
:show-file-list="false"
:on-success="handleCoverSuccess"
:before-upload="beforeCoverUpload"
:on-error="handleCoverError"
:disabled="isSubmitting"
>
<img v-if="form.cover_url" :src="form.cover_url" class="cover-preview" alt="封面"/>
<el-icon v-else class="cover-uploader-icon"><Plus /></el-icon>
</el-upload>
<p class="text-xs text-gray-500 mt-2">支持JPGPNG格式大小不超过5MB</p>
</div>
<div class="form-group intro-group">
<label class="form-label">项目简介</label>
<el-input
v-model="form.intro"
type="textarea"
:rows="3"
placeholder="请输入项目简介(纯文字,可选)"
clearable
:disabled="isSubmitting"
max-length="1000"
/>
</div>
<div class="form-group editors-group grid grid-cols-2 gap-4">
<div>
<label class="form-label">图片编辑者</label>
<el-input
v-model="form.image_editors"
placeholder="多个用逗号分隔(可选)"
clearable
:disabled="isSubmitting"
max-length="512"
/>
</div>
<div>
<label class="form-label">文字编辑者</label>
<el-input
v-model="form.text_editors"
placeholder="多个用逗号分隔(可选)"
clearable
:disabled="isSubmitting"
max-length="512"
/>
</div>
<div>
<label class="form-label">总编辑</label>
<el-input
v-model="form.chief_editor"
placeholder="请输入总编辑(可选)"
clearable
:disabled="isSubmitting"
max-length="100"
/>
</div>
<div>
<label class="form-label">校对者</label>
<el-input
v-model="form.proofreaders"
placeholder="多个用逗号分隔(可选)"
clearable
:disabled="isSubmitting"
max-length="512"
/>
</div>
<div class="col-span-2">
<label class="form-label">审核者</label>
<el-input
v-model="form.reviewers"
placeholder="多个用逗号分隔(可选)"
clearable
:disabled="isSubmitting"
max-length="512"
/>
</div>
</div>
<div class="form-group content-group">
<label class="form-label">详细内容Markdown格式</label>
<div ref="editorRef" class="quill-editor"></div>
</div>
</div>
<template #footer>
<div style="flex: auto">
<el-button @click="handleDrawerClose">取消</el-button>
<el-button type="primary" @click="submitItem" :loading="isSubmitting">
{{ form.id ? '更新项目' : '新增项目' }}
</el-button>
</div>
</template>
</el-drawer>
</el-config-provider>
</template>
<script lang="ts" setup>
import { ref, onMounted, watch, nextTick } from 'vue';
import {
ElMessage, ElMessageBox, ElDialog, ElConfigProvider, ElDrawer, ElInput, ElUpload, ElPagination
} from 'element-plus';
import { Edit, Delete, Plus, CirclePlus } from '@element-plus/icons-vue';
import Quill from 'quill';
import 'quill/dist/quill.snow.css';
import type { UploadProps } from 'element-plus';
import zhCn from 'element-plus/es/locale/lang/zh-cn'; // 导入中文语言包
// --- 类型定义(对应社会服务表结构) ---
interface SocialService {
id: number;
title: string;
subtitle: string | null;
cover_url: string | null;
intro: string | null;
content: string | null;
image_editors: string | null;
text_editors: string | null;
chief_editor: string | null;
proofreaders: string | null;
reviewers: string | null;
publish_time: string;
update_time: string;
is_delete: number;
}
interface ListSocialServiceReq {
page: number;
page_size: number;
}
// --- API 基地址 ---
const API_BASE_URL = 'http://localhost:8080/api';
// =================================================================
// 列表页相关状态与逻辑
// =================================================================
const loading = ref(true);
const tableData = ref<SocialService[]>([]);
const currentPage = ref(1);
const pageSize = ref(10);
const total = ref(0);
const contentDialogVisible = ref(false);
const currentItem = ref<SocialService>({
id: 0,
title: '',
subtitle: null,
cover_url: null,
intro: null,
content: null,
image_editors: null,
text_editors: null,
chief_editor: null,
proofreaders: null,
reviewers: null,
publish_time: '',
update_time: '',
is_delete: 0
});
// --- HTML清理函数用于简介截取 ---
const stripHtml = (html: string | null) => {
if (!html) return '';
const tempDiv = document.createElement('div');
tempDiv.innerHTML = html;
return tempDiv.textContent || tempDiv.innerText || '';
};
// --- 获取社会服务列表数据 ---
const fetchData = async () => {
loading.value = true;
try {
const reqData: ListSocialServiceReq = {
page: currentPage.value,
page_size: pageSize.value
};
const response = await fetch(`${API_BASE_URL}/social-service/list`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(reqData)
});
console.log("获取response响应对象", response);
if (!response.ok) throw new Error(`请求失败!状态码:${response.status}`);
const data = await response.json();
console.log("后端返回的完整消息体:", data); // 打印完整返回数据,确认结构
// 核心修改适配后端返回的小写字段和list/total命名
tableData.value = data.data?.list || []; // 后端是 data.list小写
total.value = data.data?.total || 0; // 后端是 data.total小写
console.log("解析后的数据列表tableData", tableData.value);
console.log("解析后的总条数total", total.value);
} catch (error) {
console.error('[ERROR] 获取社会服务列表失败:', error);
ElMessage.error('获取社会服务列表失败,请检查接口或网络!');
} finally {
loading.value = false;
}
};
onMounted(fetchData);
// --- 查看详情 ---
const handleViewContent = (row: SocialService) => {
currentItem.value = { ...row };
contentDialogVisible.value = true;
};
const handleCloseDialog = () => {
contentDialogVisible.value = false;
};
// --- 删除功能 ---
const handleDelete = (row: SocialService) => {
ElMessageBox.confirm(
`确定要删除社会服务项目《${row.title}》吗?此操作无法撤销!`,
'警告',
{
confirmButtonText: '确定删除',
cancelButtonText: '取消',
type: 'warning',
}
)
.then(async () => {
try {
const response = await fetch(`${API_BASE_URL}/social-service/${row.id}`, {
method: 'DELETE',
});
if (!response.ok) {
const errData = await response.json().catch(() => null);
throw new Error(errData?.message || '删除失败');
}
ElMessage.success('删除成功!');
fetchData();
} catch (error) {
console.error('[ERROR] 删除社会服务项目失败:', error);
ElMessage.error(`删除失败: ${(error as Error).message}`);
}
})
.catch(() => {
ElMessage.info('已取消删除');
});
};
// --- 分页事件 ---
const handleSizeChange = (val: number) => {
pageSize.value = val;
currentPage.value = 1;
fetchData();
};
const handleCurrentChange = (val: number) => {
currentPage.value = val;
fetchData();
};
// =================================================================
// 抽屉编辑/新增相关状态与逻辑
// =================================================================
const drawerVisible = ref(false);
const isSubmitting = ref(false);
// --- 表单默认状态(对应社会服务表字段) ---
const defaultFormState = () => ({
id: null as number | null,
title: '',
subtitle: '',
cover_url: '',
intro: '',
content: '',
image_editors: '',
text_editors: '',
chief_editor: '',
proofreaders: '',
reviewers: '',
});
const form = ref(defaultFormState());
// --- 富文本编辑器初始化 ---
const editorRef = ref<HTMLDivElement | null>(null);
let quillInstance: Quill | null = null;
const initQuillEditor = () => {
if (editorRef.value && !quillInstance) {
quillInstance = new Quill(editorRef.value, {
theme: 'snow',
modules: {
toolbar: {
container: [
['bold', 'italic', 'underline'],
['link', 'image'],
[{ 'header': [1, 2, 3, false] }],
[{ 'list': 'ordered'}, { 'list': 'bullet' }]
],
handlers: {
image: handleEditorImageUpload,
}
}
},
placeholder: '请输入项目详细内容支持Markdown格式...'
});
// 监听粘贴图片转base64上传
quillInstance.on('text-change', (delta, _, source) => {
if (source === 'user') {
const pastedBase64 = getPastedBase64Image(delta);
if (pastedBase64) {
const selection = quillInstance?.getSelection();
if (!selection) return;
const imageIndex = selection.index;
quillInstance?.deleteText(imageIndex, 1);
const blob = base64ToBlob(pastedBase64);
if (blob) {
const file = new File([blob], `pasted-image-${Date.now()}.png`, { type: blob.type });
uploadEditorImage(file, imageIndex);
}
}
}
});
}
};
// --- 监听抽屉显示/隐藏,初始化/销毁编辑器 ---
watch(drawerVisible, (visible) => {
if (visible) {
nextTick(() => {
initQuillEditor();
if (quillInstance) {
quillInstance.root.innerHTML = form.value.content || '';
}
});
} else {
quillInstance = null;
}
});
// --- 编辑器图片上传相关函数 ---
function handleEditorImageUpload() {
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = 'image/*';
fileInput.onchange = () => {
const file = fileInput.files?.[0];
if (!file) return;
const selection = quillInstance?.getSelection();
const insertIndex = selection ? selection.index : quillInstance?.getLength() || 0;
uploadEditorImage(file, insertIndex);
fileInput.value = '';
};
fileInput.click();
}
async function uploadEditorImage(file: File, insertIndex: number) {
if (!quillInstance) return;
quillInstance.insertText(insertIndex, '[图片上传中...]', { color: '#999', italic: true });
try {
const formData = new FormData();
formData.append('image', file);
const response = await fetch(`${API_BASE_URL}/upload/image`, {
method: 'POST',
body: formData,
});
if (!response.ok) throw new Error(`HTTP状态码${response.status}`);
const result = await response.json();
const imageUrl = result.data?.url;
if (!imageUrl) throw new Error('未获取到图片地址');
quillInstance.deleteText(insertIndex, '[图片上传中...]'.length);
quillInstance.insertEmbed(insertIndex, 'image', imageUrl);
quillInstance.setSelection(insertIndex + 1);
} catch (err) {
const errMsg = err instanceof Error ? err.message : '未知错误';
quillInstance.deleteText(insertIndex, '[图片上传中...]'.length);
quillInstance.insertText(insertIndex, `[图片上传失败:${errMsg}]`, { color: '#f56c6c', italic: true });
ElMessage.error(`编辑器图片上传失败:${errMsg}`);
}
}
// --- base64图片处理 ---
function getPastedBase64Image(delta: any): string | null {
if (!delta?.ops) return null;
for (const op of delta.ops) {
if (
op.insert &&
typeof op.insert === 'object' &&
op.insert.image &&
op.insert.image.startsWith('data:image')
) {
return op.insert.image;
}
}
return null;
}
function base64ToBlob(base64: string): Blob | null {
try {
const [prefix, data] = base64.split(',');
if (!prefix || !data) throw new Error('base64格式错误');
const mimeMatch = prefix.match(/data:(.*?);/);
if (!mimeMatch) throw new Error('无法解析图片类型');
const mime = mimeMatch[1];
const binaryData = atob(data);
const bytes = new Uint8Array(binaryData.length);
for (let i = 0; i < binaryData.length; i++) {
bytes[i] = binaryData.charCodeAt(i);
}
return new Blob([bytes], { type: mime });
} catch (error) {
console.error('base64转Blob失败', error);
ElMessage.error('解析粘贴的图片失败!');
return null;
}
}
// --- 新增项目 ---
const handleAdd = () => {
form.value = defaultFormState();
drawerVisible.value = true;
};
// --- 编辑项目 ---
const handleEdit = (row: SocialService) => {
form.value = {
id: row.id,
title: row.title,
subtitle: row.subtitle || '',
cover_url: row.cover_url || '',
intro: row.intro || '',
content: row.content || '',
image_editors: row.image_editors || '',
text_editors: row.text_editors || '',
chief_editor: row.chief_editor || '',
proofreaders: row.proofreaders || '',
reviewers: row.reviewers || '',
};
drawerVisible.value = true;
};
const handleDrawerClose = () => {
drawerVisible.value = false;
form.value = defaultFormState();
};
// --- 提交表单(新增/更新) ---
const submitItem = async () => {
// 表单校验
if (!form.value.title.trim()) {
ElMessage.warning('请输入项目标题');
return;
}
isSubmitting.value = true;
const contentHTML = quillInstance?.root.innerHTML || '';
// 构造提交数据
const submitData = {
id:form.value.id,
title: form.value.title.trim(),
subtitle: form.value.subtitle.trim() || null,
cover_url: form.value.cover_url.trim() || null,
intro: form.value.intro.trim() || null,
content: contentHTML.trim() || null,
image_editors: form.value.image_editors.trim() || null,
text_editors: form.value.text_editors.trim() || null,
chief_editor: form.value.chief_editor.trim() || null,
proofreaders: form.value.proofreaders.trim() || null,
reviewers: form.value.reviewers.trim() || null,
};
try {
let url = `${API_BASE_URL}/social-service`;
let method = 'POST';
// 编辑模式修改URL和请求方法
if (form.value.id) {
url = `${API_BASE_URL}/social-service`;
method = 'PUT';
}
const response = await fetch(url, {
method: method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(submitData),
});
if (!response.ok) {
const errData = await response.json().catch(() => null);
throw new Error(errData?.message || '提交失败');
}
ElMessage.success(form.value.id ? '项目更新成功!' : '项目新增成功!');
drawerVisible.value = false;
form.value = defaultFormState();
fetchData();
} catch (error) {
const err = error as Error;
ElMessage.error(`${form.value.id ? '更新' : '新增'}失败: ${err.message}`);
} finally {
isSubmitting.value = false;
}
};
// --- 封面上传处理 ---
const handleCoverSuccess: UploadProps['onSuccess'] = (response) => {
const ossUrl = response.data?.url;
if (ossUrl) {
form.value.cover_url = ossUrl;
ElMessage.success('封面上传成功');
} else {
ElMessage.error('封面上传失败');
}
};
const beforeCoverUpload: UploadProps['beforeUpload'] = (rawFile) => {
const isImage = rawFile.type.startsWith('image/');
if (!isImage) {
ElMessage.error('请上传图片文件!');
return false;
}
const isLt5M = rawFile.size / 1024 / 1024 < 5;
if (!isLt5M) {
ElMessage.error('图片大小不能超过 5MB!');
}
return isImage && isLt5M;
};
const handleCoverError = () => {
ElMessage.error('封面上传失败');
};
</script>
<style scoped>
/* 基础样式复用+适配 */
.el-table .el-table__cell { vertical-align: middle; }
.el-table__header-wrapper th { background-color: #fafafa !important; font-weight: 600; color: #333; }
.action-buttons .el-button { margin-right: 8px; }
.content-full { min-height: 300px; padding: 20px; background-color: #f9fafb; border-radius: 8px; }
.el-dialog__title { font-size: 18px !important; font-weight: 600 !important; }
/* 分页器样式 */
.custom-pagination { justify-content: flex-end !important; }
.custom-pagination .el-pagination__total,
.custom-pagination .el-pagination__sizes,
.custom-pagination .el-pagination__jump { margin-right: 16px !important; }
.custom-pagination .el-pagination__jump .el-input { width: 60px !important; }
/* 抽屉内表单样式 */
.publish-form-container { padding: 0 20px; }
.form-group { margin-bottom: 24px; }
.form-label { display: block; margin-bottom: 8px; color: #333; font-size: 14px; font-weight: 600; }
.form-label.required::after { content: '*'; color: #f56c6c; margin-left: 4px; }
.quill-editor { height: 350px; border-radius: 4px; border: 1px solid #dcdfe6; }
.cover-uploader .el-upload { border: 1px dashed #d9d9d9; border-radius: 6px; cursor: pointer; position: relative; overflow: hidden; }
.cover-uploader .el-upload:hover { border-color: #409EFF; }
.cover-uploader-icon { font-size: 28px; color: #8c939d; width: 178px; height: 178px; text-align: center; line-height: 178px; }
.cover-preview { width: 178px; height: 178px; display: block; object-fit: cover; }
/* 编辑者信息网格布局 */
.grid { display: grid; }
.grid-cols-2 { grid-template-columns: repeat(2, 1fr); }
.gap-4 { gap: 16px; }
/* 详情页样式 */
.el-dialog .flex { display: flex; }
.el-dialog .flex-wrap { flex-wrap: wrap; }
.el-dialog .gap-4 { gap: 16px; }
.el-dialog .bg-gray-100 { background-color: #f3f4f6; }
.el-dialog .px-3 { padding-left: 12px; padding-right: 12px; }
.el-dialog .py-1 { padding-top: 4px; padding-bottom: 4px; }
.el-dialog .rounded { border-radius: 4px; }
.el-dialog .text-sm { font-size: 12px; }
</style>

View File

@@ -0,0 +1,347 @@
<template>
<div class="page-cover-management">
<!-- 页面头部 -->
<el-card class="page-header-card">
<h1 class="page-title">基地概况页面封面图管理</h1>
</el-card>
<!-- 封面图管理核心区域 -->
<el-card class="cover-management-card mt-4">
<div class="cover-uploader-container">
<h3 class="section-title">封面图上传</h3>
<p class="section-desc">建议上传16:9比例图片支持JPG/PNG/WEBP格式大小不超过5MB</p>
<div class="cover-uploader">
<!-- 已上传图片预览 -->
<div v-if="formData.pageImageUrl" class="cover-preview">
<img :src="formData.pageImageUrl" alt="基地概况页面封面" class="cover-img">
<button
class="remove-cover-btn"
@click="removePageImage"
title="删除封面图"
:disabled="isSaving"
>
<el-icon><Close /></el-icon>
</button>
</div>
<!-- 未上传时的上传区域 -->
<el-upload
v-else
class="cover-upload-area"
:action="uploadAction"
name="image"
:show-file-list="false"
:on-success="handleCoverSuccess"
:before-upload="beforeUpload"
:on-error="handlePageImageUploadError"
:disabled="isSaving"
>
<div class="upload-placeholder">
<el-icon class="upload-icon"><Upload /></el-icon>
<p class="upload-text">点击或拖拽图片至此处上传</p>
<p class="upload-subtext">支持JPG/PNG/WEBP最大5MB建议16:9比例</p>
</div>
</el-upload>
</div>
<!-- 操作按钮区域 -->
<div class="cover-action-buttons mt-4">
<el-button
type="primary"
@click="saveImage"
:loading="isSaving"
:disabled="!formData.pageImageUrl || isSaving"
>
<el-icon><Check /></el-icon>
保存封面图
</el-button>
</div>
</div>
</el-card>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive, onMounted } from 'vue';
import { ElMessage } from 'element-plus';
import { Close, Upload, Check } from '@element-plus/icons-vue';
import type { UploadProps } from 'element-plus';
import axios from "axios";
// 保存状态(防止重复提交)
const isSaving = ref(false);
// 上传接口地址
const uploadAction = import.meta.env.VITE_API_BASE_URL + '/upload/cover';
// 表单数据
const formData = reactive({
pageImageUrl: '' // 页面封面图URL
});
// 页面挂载时加载现有封面图
onMounted(() => {
fetchCoverImage();
});
// 从后端获取已保存的封面图
const fetchCoverImage = async () => {
try {
const response = await axios.post('http://localhost:8080/api/page-image/get', { page: 'SocialService' });
if (response.data.message === '查询成功' && Array.isArray(response.data.images) && response.data.images.length) {
formData.pageImageUrl = response.data.images[0].image_url;
ElMessage.success('已加载现有封面图');
} else {
ElMessage.info('暂无已保存的封面图,可上传新图片');
}
} catch (error) {
console.error('封面图加载失败:', error);
ElMessage.warning('未获取到现有封面图,可重新上传');
}
};
// 上传前校验
const beforeUpload: UploadProps['beforeUpload'] = (rawFile) => {
const allowTypes = ['image/jpeg', 'image/png', 'image/webp'];
if (!allowTypes.includes(rawFile.type)) {
ElMessage.error('仅支持JPG/PNG/WEBP格式的图片');
return false;
}
if (rawFile.size / 1024 / 1024 > 5) {
ElMessage.error('图片大小不能超过5MB');
return false;
}
console.log("图片校验成功");
return true;
};
// 封面上传成功回调
const handleCoverSuccess: UploadProps['onSuccess'] = (response) => {
const ossUrl = response.data?.url;
if (ossUrl) {
formData.pageImageUrl = ossUrl;
ElMessage.success('封面上传成功');
} else {
ElMessage.error('封面上传失败:未获取到图片地址');
}
};
// 上传失败处理
const handlePageImageUploadError = (error: any) => {
console.error('页面封面图上传错误:', error);
ElMessage.error('页面封面图上传失败,请重试');
};
// 删除封面图
const removePageImage = () => {
formData.pageImageUrl = '';
ElMessage.info('页面封面图已删除');
};
// 保存封面图到后端
const saveImage = async () => {
try {
isSaving.value = true;
if (!formData.pageImageUrl) {
ElMessage.warning('请先上传封面图再保存');
return;
}
const response = await axios.post('http://localhost:8080/api/page-image/save', {
id: 7,
page: 'SocialService',
image_url: formData.pageImageUrl,
});
if (response.data.success || response.data.message === '保存成功') {
ElMessage.success('封面图保存成功');
console.log('封面图保存成功,地址:', formData.pageImageUrl);
} else {
ElMessage.error('封面图保存失败:' + (response.data.message || '未知错误'));
}
} catch (error) {
console.error('封面图保存失败:', error);
ElMessage.error('封面图保存失败,请重试');
} finally {
isSaving.value = false;
}
};
</script>
<style scoped>
.page-cover-management {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.page-header-card {
padding: 25px;
margin-bottom: 20px;
}
.page-title {
font-size: 24px;
font-weight: 600;
margin: 0;
color: #1d2129;
}
.cover-management-card {
padding: 25px;
}
.cover-uploader-container {
max-width: 800px;
}
.section-title {
font-size: 16px;
font-weight: 500;
margin-bottom: 8px;
color: #333;
}
.section-desc {
font-size: 14px;
color: #666;
margin-bottom: 20px;
}
.cover-uploader {
width: 100%;
}
/* 已上传图片预览样式 */
.cover-preview {
width: 100%;
height: 225px; /* 16:9比例适配 */
border-radius: 8px;
overflow: hidden;
position: relative;
background: #f5f5f5;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
}
.cover-img {
width: 100%;
height: 100%;
object-fit: cover;
}
.remove-cover-btn {
position: absolute;
top: 10px;
right: 10px;
width: 36px;
height: 36px;
border-radius: 50%;
background: rgba(0, 0, 0, 0.6);
color: white;
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.3s;
z-index: 10;
}
.remove-cover-btn:hover {
background: rgba(255, 0, 0, 0.8);
}
.remove-cover-btn:disabled {
background: rgba(0, 0, 0, 0.3);
cursor: not-allowed;
}
/* 上传区域样式 */
.cover-upload-area {
width: 100%;
height: 225px;
}
.upload-placeholder {
width: 100%;
height: 100%;
border: 2px dashed #ccc;
border-radius: 8px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: #fafafa;
cursor: pointer;
transition: all 0.3s;
}
.upload-placeholder:hover {
border-color: #409eff;
background: #f0f7ff;
}
.upload-icon {
font-size: 36px;
color: #999;
margin-bottom: 12px;
}
.upload-text {
color: #666;
font-size: 14px;
margin-bottom: 6px;
}
.upload-subtext {
color: #999;
font-size: 12px;
}
/* 操作按钮区域 */
.cover-action-buttons {
display: flex;
gap: 15px;
align-items: center;
}
/* 响应式适配 */
@media (max-width: 768px) {
.page-cover-management {
padding: 15px;
}
.cover-management-card {
padding: 15px;
}
.cover-preview,
.cover-upload-area {
height: 180px;
}
.cover-action-buttons {
flex-direction: column;
align-items: flex-start;
}
.el-button {
width: 100%;
}
}
@media (max-width: 480px) {
.page-title {
font-size: 20px;
}
.cover-preview,
.cover-upload-area {
height: 150px;
}
.upload-icon {
font-size: 28px;
}
}
</style>