完成speaker管理页面
This commit is contained in:
@@ -2,11 +2,18 @@
|
|||||||
<el-config-provider :locale="zhCn">
|
<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="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">
|
<div class="flex justify-between items-center mb-6">
|
||||||
<span class="text-2xl font-bold text-gray-700">会议管理列表</span>
|
<span class="text-2xl font-bold text-gray-700">会议管理列表</span>
|
||||||
|
<div class="flex gap-3">
|
||||||
|
<el-button type="primary" :icon="Image" @click="openPageImageDialog">
|
||||||
|
编辑页面图片
|
||||||
|
</el-button>
|
||||||
<el-button type="primary" :icon="Plus" @click="handleAdd">新增会议</el-button>
|
<el-button type="primary" :icon="Plus" @click="handleAdd">新增会议</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 会议列表表格 -->
|
||||||
<el-table :data="tableData" style="width: 100%" row-key="id" max-height="82vh">
|
<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 prop="id" label="ID" width="80" sortable fixed></el-table-column>
|
||||||
<el-table-column label="会议封面" width="180">
|
<el-table-column label="会议封面" width="180">
|
||||||
@@ -70,7 +77,7 @@
|
|||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
<!-- 演讲人员管理弹窗 -->
|
<!-- 演讲人员管理弹窗 -->
|
||||||
<el-dialog v-model="speakerDialogVisible" title="演讲人员管理" :width="`800px`" :before-close="handleCloseSpeakerDialog">
|
<el-dialog v-model="speakerDialogVisible" title="演讲人员管理" :width="`1200px`" :before-close="handleCloseSpeakerDialog">
|
||||||
<div class="mb-4 flex justify-end">
|
<div class="mb-4 flex justify-end">
|
||||||
<el-button type="primary" :icon="Plus" @click="handleAddSpeaker">新增演讲嘉宾</el-button>
|
<el-button type="primary" :icon="Plus" @click="handleAddSpeaker">新增演讲嘉宾</el-button>
|
||||||
</div>
|
</div>
|
||||||
@@ -89,7 +96,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="sort" label="排序" width="80" align="center"></el-table-column>
|
<el-table-column prop="sort" label="排序" width="80" align="center"></el-table-column>
|
||||||
<el-table-column label="操作" width="140" align="center">
|
<el-table-column label="操作" width="280" align="center">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-button size="mini" type="primary" :icon="Edit" @click="handleEditSpeaker(scope.row)">编辑</el-button>
|
<el-button size="mini" type="primary" :icon="Edit" @click="handleEditSpeaker(scope.row)">编辑</el-button>
|
||||||
<el-button size="mini" type="danger" :icon="Delete" @click="handleDeleteSpeaker(scope.row)">删除</el-button>
|
<el-button size="mini" type="danger" :icon="Delete" @click="handleDeleteSpeaker(scope.row)">删除</el-button>
|
||||||
@@ -97,6 +104,21 @@
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
|
|
||||||
|
<!-- 演讲嘉宾分页组件 -->
|
||||||
|
<div class="mt-4 flex justify-end">
|
||||||
|
<el-pagination
|
||||||
|
class="custom-pagination"
|
||||||
|
background
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
:total="speakerTotal"
|
||||||
|
v-model:current-page="currentSpeakerPage"
|
||||||
|
v-model:page-size="speakerPageSize"
|
||||||
|
:page-sizes="[5, 10, 20, 50]"
|
||||||
|
@size-change="handleSpeakerSizeChange"
|
||||||
|
@current-change="handleSpeakerCurrentChange"
|
||||||
|
></el-pagination>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 新增/编辑演讲嘉宾表单 -->
|
<!-- 新增/编辑演讲嘉宾表单 -->
|
||||||
<el-drawer v-model="speakerDrawerVisible" :title="currentSpeaker.id ? '编辑演讲嘉宾' : '新增演讲嘉宾'" direction="rtl" size="50%" destroy-on-close>
|
<el-drawer v-model="speakerDrawerVisible" :title="currentSpeaker.id ? '编辑演讲嘉宾' : '新增演讲嘉宾'" direction="rtl" size="50%" destroy-on-close>
|
||||||
<div class="p-4">
|
<div class="p-4">
|
||||||
@@ -140,6 +162,60 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-drawer>
|
</el-drawer>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 新增:页面图片管理弹窗 -->
|
||||||
|
<el-dialog v-model="pageImageDialogVisible" title="会议页面封面图管理" :width="`800px`" :before-close="handleClosePageImageDialog">
|
||||||
|
<div class="page-image-management">
|
||||||
|
<h3 class="section-title text-lg font-semibold mb-3">页面顶部封面图</h3>
|
||||||
|
<p class="section-desc text-gray-600 mb-4">上传会议列表页面的顶部封面图,建议16:9比例,支持JPG/PNG/WEBP格式,大小不超过5MB</p>
|
||||||
|
|
||||||
|
<div class="cover-uploader">
|
||||||
|
<!-- 已上传图片预览 -->
|
||||||
|
<div v-if="pageImageUrl" class="cover-preview">
|
||||||
|
<img :src="pageImageUrl" alt="会议页面封面" class="cover-img">
|
||||||
|
<button
|
||||||
|
class="remove-cover-btn"
|
||||||
|
@click="removePageImage"
|
||||||
|
title="删除封面图"
|
||||||
|
:disabled="isPageImageSaving"
|
||||||
|
>
|
||||||
|
<el-icon><Close /></el-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 未上传时的上传区域 -->
|
||||||
|
<el-upload
|
||||||
|
v-else
|
||||||
|
class="cover-upload-area"
|
||||||
|
:action="`${API_BASE_URL}/upload/cover`"
|
||||||
|
name="image"
|
||||||
|
:show-file-list="false"
|
||||||
|
:on-success="handlePageImageSuccess"
|
||||||
|
:before-upload="beforePageImageUpload"
|
||||||
|
:on-error="handlePageImageError"
|
||||||
|
:disabled="isPageImageSaving"
|
||||||
|
>
|
||||||
|
<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 flex justify-end">
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
@click="savePageImage"
|
||||||
|
:loading="isPageImageSaving"
|
||||||
|
:disabled="!pageImageUrl || isPageImageSaving"
|
||||||
|
>
|
||||||
|
<el-icon><Check /></el-icon>
|
||||||
|
保存封面图
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 分页组件 -->
|
<!-- 分页组件 -->
|
||||||
@@ -181,11 +257,11 @@
|
|||||||
<div class="form-group time-group grid grid-cols-2 gap-4">
|
<div class="form-group time-group grid grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label class="form-label required">开始时间</label>
|
<label class="form-label required">开始时间</label>
|
||||||
<el-date-picker v-model="form.start_time" type="datetime" placeholder="选择会议开始时间" :disabled="isSubmitting" value-format="YYYY-MM-DD HH:mm:ss" timezone="GMT+8" />
|
<el-date-picker v-model="form.start_time" type="datetime" placeholder="选择会议开始时间" :disabled="isSubmitting" value-format="YYYY-MM-DD HH:mm:ss" timezone="GMT" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="form-label required">结束时间</label>
|
<label class="form-label required">结束时间</label>
|
||||||
<el-date-picker v-model="form.end_time" type="datetime" placeholder="选择会议结束时间" :disabled="isSubmitting" value-format="YYYY-MM-DD HH:mm:ss" timezone="GMT+8" />
|
<el-date-picker v-model="form.end_time" type="datetime" placeholder="选择会议结束时间" :disabled="isSubmitting" value-format="YYYY-MM-DD HH:mm:ss" timezone="GMT" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -212,7 +288,7 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, onMounted, watch, nextTick } from 'vue';
|
import { ref, onMounted, watch, nextTick } from 'vue';
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||||
import { Edit, Delete, Plus, User } from '@element-plus/icons-vue';
|
import { Edit, Delete, Plus, User, Upload, Check, Close } from '@element-plus/icons-vue';
|
||||||
import Quill from 'quill';
|
import Quill from 'quill';
|
||||||
import 'quill/dist/quill.snow.css';
|
import 'quill/dist/quill.snow.css';
|
||||||
import type { UploadProps } from 'element-plus';
|
import type { UploadProps } from 'element-plus';
|
||||||
@@ -441,6 +517,23 @@ const handleDrawerClose = () => {
|
|||||||
drawerVisible.value = false;
|
drawerVisible.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const subtractEightHours = (timeStr: string): string => {
|
||||||
|
if (!timeStr) return '';
|
||||||
|
// 转换为Date对象(假设原时间字符串格式为 "YYYY-MM-DD HH:mm:ss")
|
||||||
|
const date = new Date(timeStr);
|
||||||
|
if (isNaN(date.getTime())) return timeStr; // 无效时间则返回原字符串
|
||||||
|
// 减去8小时(8*60*60*1000毫秒)
|
||||||
|
date.setTime(date.getTime() - 8 * 60 * 60 * 1000);
|
||||||
|
// 格式化回 "YYYY-MM-DD HH:mm:ss" 格式
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(date.getDate()).padStart(2, '0');
|
||||||
|
const hours = String(date.getHours()).padStart(2, '0');
|
||||||
|
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||||
|
const seconds = String(date.getSeconds()).padStart(2, '0');
|
||||||
|
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
||||||
|
};
|
||||||
|
|
||||||
// --- 提交会议 ---
|
// --- 提交会议 ---
|
||||||
const submitMeeting = async () => {
|
const submitMeeting = async () => {
|
||||||
if (!form.value.theme.trim()) {
|
if (!form.value.theme.trim()) {
|
||||||
@@ -470,8 +563,8 @@ const submitMeeting = async () => {
|
|||||||
intro: form.value.intro || '',
|
intro: form.value.intro || '',
|
||||||
cover_url: form.value.cover_url || '',
|
cover_url: form.value.cover_url || '',
|
||||||
schedule_image_url: form.value.schedule_image_url || '',
|
schedule_image_url: form.value.schedule_image_url || '',
|
||||||
start_time: form.value.start_time,
|
start_time: subtractEightHours(form.value.start_time),
|
||||||
end_time: form.value.end_time,
|
end_time: subtractEightHours(form.value.end_time),
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -673,10 +766,15 @@ const currentSpeaker = ref<MeetingSpeaker>({
|
|||||||
intro: '',
|
intro: '',
|
||||||
sort: 0
|
sort: 0
|
||||||
});
|
});
|
||||||
|
// 演讲嘉宾分页状态
|
||||||
|
const currentSpeakerPage = ref(1); // 嘉宾列表当前页码
|
||||||
|
const speakerPageSize = ref(10); // 嘉宾列表每页条数
|
||||||
|
const speakerTotal = ref(0); // 嘉宾列表总条数
|
||||||
|
|
||||||
// --- 打开演讲人员管理弹窗 ---
|
// --- 打开演讲人员管理弹窗 ---
|
||||||
const handleManageSpeaker = async (row: Meeting) => {
|
const handleManageSpeaker = async (row: Meeting) => {
|
||||||
currentMeetingId.value = row.id;
|
currentMeetingId.value = row.id;
|
||||||
|
currentSpeakerPage.value = 1;
|
||||||
speakerDialogVisible.value = true;
|
speakerDialogVisible.value = true;
|
||||||
await fetchSpeakerList();
|
await fetchSpeakerList();
|
||||||
};
|
};
|
||||||
@@ -684,16 +782,36 @@ const handleManageSpeaker = async (row: Meeting) => {
|
|||||||
// --- 获取演讲嘉宾列表 ---
|
// --- 获取演讲嘉宾列表 ---
|
||||||
const fetchSpeakerList = async () => {
|
const fetchSpeakerList = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_BASE_URL}/meetings/${currentMeetingId.value}/speakers`, {
|
if (!currentMeetingId.value || currentMeetingId.value <= 0) {
|
||||||
method: 'GET',
|
ElMessage.warning('会议ID无效,无法获取嘉宾列表');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const reqData = {
|
||||||
|
meetingId: currentMeetingId.value,
|
||||||
|
page: currentSpeakerPage.value,
|
||||||
|
page_size: speakerPageSize.value
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await fetch(`${API_BASE_URL}/speakers/meetingspeakers`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(reqData)
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) throw new Error('获取演讲嘉宾列表失败');
|
if (!response.ok) {
|
||||||
|
const errData = await response.json().catch(() => null);
|
||||||
|
throw new Error(errData?.message || '获取演讲嘉宾列表失败');
|
||||||
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
speakerList.value = data.speakers || [];
|
speakerList.value = data.speakers || [];
|
||||||
|
speakerTotal.value = data.total || 0;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[ERROR] 获取演讲嘉宾列表失败:', error);
|
console.error('[ERROR] 获取演讲嘉宾列表失败:', error);
|
||||||
ElMessage.error('获取演讲嘉宾列表失败,请重试!');
|
ElMessage.error((error as Error).message || '获取演讲嘉宾列表失败,请重试!');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -702,6 +820,8 @@ const handleCloseSpeakerDialog = () => {
|
|||||||
speakerDialogVisible.value = false;
|
speakerDialogVisible.value = false;
|
||||||
speakerList.value = [];
|
speakerList.value = [];
|
||||||
currentMeetingId.value = 0;
|
currentMeetingId.value = 0;
|
||||||
|
speakerTotal.value = 0;
|
||||||
|
currentSpeakerPage.value = 1;
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- 新增演讲嘉宾 ---
|
// --- 新增演讲嘉宾 ---
|
||||||
@@ -737,9 +857,13 @@ const handleDeleteSpeaker = (row: MeetingSpeaker) => {
|
|||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) throw new Error('删除失败');
|
if (!response.ok) {
|
||||||
|
const errData = await response.json().catch(() => null);
|
||||||
|
throw new Error(errData?.message || '删除失败');
|
||||||
|
}
|
||||||
|
|
||||||
ElMessage.success('删除成功!');
|
ElMessage.success('删除成功!');
|
||||||
fetchSpeakerList();
|
await fetchSpeakerList();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[ERROR] 删除演讲嘉宾失败:', error);
|
console.error('[ERROR] 删除演讲嘉宾失败:', error);
|
||||||
ElMessage.error(`删除失败: ${(error as Error).message}`);
|
ElMessage.error(`删除失败: ${(error as Error).message}`);
|
||||||
@@ -752,40 +876,93 @@ const handleDeleteSpeaker = (row: MeetingSpeaker) => {
|
|||||||
|
|
||||||
// --- 提交演讲嘉宾 ---
|
// --- 提交演讲嘉宾 ---
|
||||||
const submitSpeaker = async () => {
|
const submitSpeaker = async () => {
|
||||||
|
const isUpdate = !!currentSpeaker.value.id;
|
||||||
|
|
||||||
|
// 1. 基础参数校验
|
||||||
|
if (isUpdate) {
|
||||||
|
if (currentSpeaker.value.id === null || currentSpeaker.value.id <= 0) {
|
||||||
|
ElMessage.warning('嘉宾ID无效,请重新选择嘉宾');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (currentSpeaker.value.name !== undefined && currentSpeaker.value.name.trim() === '') {
|
||||||
|
ElMessage.warning('嘉宾姓名不能为空');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
if (!currentSpeaker.value.name.trim()) {
|
if (!currentSpeaker.value.name.trim()) {
|
||||||
ElMessage.warning('请输入嘉宾姓名');
|
ElMessage.warning('请输入嘉宾姓名(必填)');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (currentSpeaker.value.meeting_id <= 0) {
|
||||||
|
ElMessage.warning('会议ID无效,请重新选择会议');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 公共字段校验
|
||||||
|
if (currentSpeaker.value.sort === undefined || currentSpeaker.value.sort < 0) {
|
||||||
|
currentSpeaker.value.sort = 0;
|
||||||
|
}
|
||||||
|
if (currentSpeaker.value.name && currentSpeaker.value.name.length > 100) {
|
||||||
|
ElMessage.warning('嘉宾姓名长度不能超过100字');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (currentSpeaker.value.title && currentSpeaker.value.title.length > 200) {
|
||||||
|
ElMessage.warning('嘉宾头衔长度不能超过200字');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (currentSpeaker.value.avatar && currentSpeaker.value.avatar.length > 512) {
|
||||||
|
ElMessage.warning('头像URL长度不能超过512字符');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
isSpeakerSubmitting.value = true;
|
isSpeakerSubmitting.value = true;
|
||||||
const submitData = { ...currentSpeaker.value };
|
// 3. 构造提交数据
|
||||||
|
const submitData: any = {
|
||||||
|
name: currentSpeaker.value.name?.trim() || '',
|
||||||
|
title: currentSpeaker.value.title?.trim() || '',
|
||||||
|
avatar: currentSpeaker.value.avatar?.trim() || '',
|
||||||
|
intro: currentSpeaker.value.intro || '',
|
||||||
|
sort: currentSpeaker.value.sort
|
||||||
|
};
|
||||||
|
|
||||||
try {
|
if (isUpdate) {
|
||||||
let url = `${API_BASE_URL}/speakers`;
|
submitData.id = currentSpeaker.value.id;
|
||||||
let method = 'POST';
|
} else {
|
||||||
|
submitData.meeting_id = currentSpeaker.value.meeting_id;
|
||||||
if (submitData.id) {
|
|
||||||
method = 'PUT';
|
|
||||||
url = `${url}/${submitData.id}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 4. 处理请求路径和方法
|
||||||
|
let url = `${API_BASE_URL}/speakers`;
|
||||||
|
let method = 'POST';
|
||||||
|
if (isUpdate) {
|
||||||
|
method = 'PUT';
|
||||||
|
url = `${url}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
method: method,
|
method: method,
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
body: JSON.stringify(submitData),
|
body: JSON.stringify(submitData),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errData = await response.json().catch(() => null);
|
const errData = await response.json().catch(() => null);
|
||||||
throw new Error(errData?.message || (submitData.id ? '更新失败' : '创建失败'));
|
throw new Error(
|
||||||
|
errData?.message ||
|
||||||
|
(isUpdate ? '更新嘉宾失败' : '创建嘉宾失败')
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
ElMessage.success(submitData.id ? '嘉宾更新成功!' : '嘉宾创建成功!');
|
ElMessage.success(isUpdate ? '嘉宾更新成功!' : '创建嘉宾成功!');
|
||||||
speakerDrawerVisible.value = false;
|
speakerDrawerVisible.value = false;
|
||||||
fetchSpeakerList();
|
await fetchSpeakerList();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const err = error as Error;
|
const err = error as Error;
|
||||||
ElMessage.error(`${submitData.id ? '更新' : '创建'}失败: ${err.message}`);
|
ElMessage.error(`${isUpdate ? '更新' : '创建'}失败: ${err.message}`);
|
||||||
} finally {
|
} finally {
|
||||||
isSpeakerSubmitting.value = false;
|
isSpeakerSubmitting.value = false;
|
||||||
}
|
}
|
||||||
@@ -818,6 +995,127 @@ const beforeAvatarUpload: UploadProps['beforeUpload'] = (rawFile) => {
|
|||||||
const handleAvatarError = () => {
|
const handleAvatarError = () => {
|
||||||
ElMessage.error('头像上传失败');
|
ElMessage.error('头像上传失败');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// --- 演讲嘉宾分页事件 ---
|
||||||
|
const handleSpeakerSizeChange = (val: number) => {
|
||||||
|
speakerPageSize.value = val;
|
||||||
|
currentSpeakerPage.value = 1;
|
||||||
|
fetchSpeakerList();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSpeakerCurrentChange = (val: number) => {
|
||||||
|
currentSpeakerPage.value = val;
|
||||||
|
fetchSpeakerList();
|
||||||
|
};
|
||||||
|
|
||||||
|
// =================================================================
|
||||||
|
// 页面图片管理相关状态与逻辑
|
||||||
|
// =================================================================
|
||||||
|
const pageImageDialogVisible = ref(false); // 页面图片管理弹窗显示状态
|
||||||
|
const pageImageUrl = ref(''); // 页面封面图URL
|
||||||
|
const isPageImageSaving = ref(false); // 图片保存加载状态
|
||||||
|
|
||||||
|
// 打开页面图片管理弹窗(加载现有图片)
|
||||||
|
const openPageImageDialog = async () => {
|
||||||
|
pageImageDialogVisible.value = true;
|
||||||
|
await fetchPageImage();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 从后端获取当前页面图片
|
||||||
|
const fetchPageImage = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/page-image/get`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ page: 'AcademicExchange' })
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.message === '查询成功' && Array.isArray(data.images) && data.images.length) {
|
||||||
|
pageImageUrl.value = data.images[0].image_url;
|
||||||
|
} else {
|
||||||
|
pageImageUrl.value = '';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[ERROR] 获取页面图片失败:', error);
|
||||||
|
ElMessage.warning('未获取到现有页面图片,可重新上传');
|
||||||
|
pageImageUrl.value = '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 页面图片上传前校验
|
||||||
|
const beforePageImageUpload: 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;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 页面图片上传成功
|
||||||
|
const handlePageImageSuccess: UploadProps['onSuccess'] = (response) => {
|
||||||
|
const ossUrl = response.data?.url;
|
||||||
|
if (ossUrl) {
|
||||||
|
pageImageUrl.value = ossUrl;
|
||||||
|
ElMessage.success('图片上传成功');
|
||||||
|
} else {
|
||||||
|
ElMessage.error('图片上传失败:未获取到图片地址');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 页面图片上传失败
|
||||||
|
const handlePageImageError = () => {
|
||||||
|
ElMessage.error('图片上传失败,请重试');
|
||||||
|
};
|
||||||
|
|
||||||
|
// 删除页面图片
|
||||||
|
const removePageImage = () => {
|
||||||
|
pageImageUrl.value = '';
|
||||||
|
ElMessage.info('页面图片已删除');
|
||||||
|
};
|
||||||
|
|
||||||
|
// 保存页面图片到后端
|
||||||
|
const savePageImage = async () => {
|
||||||
|
try {
|
||||||
|
isPageImageSaving.value = true;
|
||||||
|
if (!pageImageUrl.value) {
|
||||||
|
ElMessage.warning('请先上传图片再保存');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(`${API_BASE_URL}/page-image/save`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
id: 6,
|
||||||
|
image_url: pageImageUrl.value,
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.success || data.message === '保存成功') {
|
||||||
|
ElMessage.success('页面图片保存成功');
|
||||||
|
pageImageDialogVisible.value = false;
|
||||||
|
} else {
|
||||||
|
ElMessage.error('页面图片保存失败:' + (data.message || '未知错误'));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[ERROR] 保存页面图片失败:', error);
|
||||||
|
ElMessage.error('页面图片保存失败,请重试');
|
||||||
|
} finally {
|
||||||
|
isPageImageSaving.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 关闭页面图片管理弹窗
|
||||||
|
const handleClosePageImageDialog = () => {
|
||||||
|
pageImageDialogVisible.value = false;
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -863,4 +1161,113 @@ const handleAvatarError = () => {
|
|||||||
.avatar-uploader-icon { font-size: 24px; color: #8c939d; width: 80px; height: 80px; text-align: center; line-height: 80px; border: 1px dashed #dcdfe6; border-radius: 6px; }
|
.avatar-uploader-icon { font-size: 24px; color: #8c939d; width: 80px; height: 80px; text-align: center; line-height: 80px; border: 1px dashed #dcdfe6; border-radius: 6px; }
|
||||||
.avatar-preview { width: 80px; height: 80px; display: block; object-fit: cover; border-radius: 6px; }
|
.avatar-preview { width: 80px; height: 80px; display: block; object-fit: cover; border-radius: 6px; }
|
||||||
.el-dialog__body .el-table { margin-bottom: 16px; }
|
.el-dialog__body .el-table { margin-bottom: 16px; }
|
||||||
|
.el-dialog__body .custom-pagination { margin-top: 8px; }
|
||||||
|
|
||||||
|
/* 页面图片管理弹窗样式 */
|
||||||
|
.page-image-management {
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
color: #333;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-desc {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover-uploader {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover-preview {
|
||||||
|
width: 100%;
|
||||||
|
height: 225px; /* 16:9比例适配 */
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.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 {
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-dialog__body) {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
Reference in New Issue
Block a user