完成社会服务的管理界面
This commit is contained in:
@@ -63,6 +63,42 @@
|
|||||||
<el-icon><Memo /></el-icon>
|
<el-icon><Memo /></el-icon>
|
||||||
<template #title>编辑会议</template>
|
<template #title>编辑会议</template>
|
||||||
</el-menu-item>
|
</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-menu>
|
||||||
|
|
||||||
</el-aside>
|
</el-aside>
|
||||||
|
|||||||
@@ -12,7 +12,10 @@ const ResourceView = () => import('../views/resource/ResourceView.vue')
|
|||||||
const BaseOverview = ()=> import('../views/baseoverview/BaseOverView.vue')
|
const BaseOverview = ()=> import('../views/baseoverview/BaseOverView.vue')
|
||||||
const DevProjectView = ()=> import('../views/devproject/devprojectView.vue')
|
const DevProjectView = ()=> import('../views/devproject/devprojectView.vue')
|
||||||
const MeetingView = ()=> import('../views/meeting/meetingView.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 导入正确)
|
// 定义路由规则(现在 RouteRecordRaw 导入正确)
|
||||||
const routes: RouteRecordRaw[] = [
|
const routes: RouteRecordRaw[] = [
|
||||||
{
|
{
|
||||||
@@ -107,8 +110,58 @@ const routes: RouteRecordRaw[] = [
|
|||||||
title: '编辑会议',
|
title: '编辑会议',
|
||||||
requiresAuth: false
|
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({
|
const router = createRouter({
|
||||||
|
|||||||
713
management/src/views/socialservice/Government/governmentView.vue
Normal file
713
management/src/views/socialservice/Government/governmentView.vue
Normal 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">支持JPG、PNG格式,大小不超过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>
|
||||||
713
management/src/views/socialservice/Internship/InternshipView.vue
Normal file
713
management/src/views/socialservice/Internship/InternshipView.vue
Normal 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">支持JPG、PNG格式,大小不超过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>
|
||||||
@@ -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">支持JPG、PNG格式,大小不超过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>
|
||||||
347
management/src/views/socialservice/img/serviceimg.vue
Normal file
347
management/src/views/socialservice/img/serviceimg.vue
Normal 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>
|
||||||
Reference in New Issue
Block a user