添加会议管理后端页面
This commit is contained in:
@@ -59,7 +59,12 @@
|
||||
<el-icon><Memo /></el-icon>
|
||||
<template #title>编辑科学研究</template>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/meeting">
|
||||
<el-icon><Memo /></el-icon>
|
||||
<template #title>编辑会议</template>
|
||||
</el-menu-item>
|
||||
</el-menu>
|
||||
|
||||
</el-aside>
|
||||
|
||||
<!-- 右侧主内容容器 -->
|
||||
|
||||
@@ -11,6 +11,7 @@ const CommunityView = () => import('../views/community/CommunityView.vue')
|
||||
const ResourceView = () => import('../views/resource/ResourceView.vue')
|
||||
const BaseOverview = ()=> import('../views/baseoverview/BaseOverView.vue')
|
||||
const DevProjectView = ()=> import('../views/devproject/devprojectView.vue')
|
||||
const MeetingView = ()=> import('../views/meeting/meetingView.vue')
|
||||
|
||||
// 定义路由规则(现在 RouteRecordRaw 导入正确)
|
||||
const routes: RouteRecordRaw[] = [
|
||||
@@ -96,6 +97,16 @@ const routes: RouteRecordRaw[] = [
|
||||
title: '编辑科学研究',
|
||||
requiresAuth: false
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
path: '/meeting',
|
||||
name: 'meeting',
|
||||
component: MeetingView,
|
||||
meta: {
|
||||
title: '编辑会议',
|
||||
requiresAuth: false
|
||||
}
|
||||
}
|
||||
|
||||
]
|
||||
|
||||
615
management/src/views/meeting/meetingView.vue
Normal file
615
management/src/views/meeting/meetingView.vue
Normal file
@@ -0,0 +1,615 @@
|
||||
<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="Plus" @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="theme" label="会议主题" min-width="200" show-overflow-tooltip>
|
||||
<template #default="scope">
|
||||
<span class="font-semibold">{{ scope.row.theme }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="subtitle" label="副标题" min-width="180" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="会议简介" min-width="250">
|
||||
<template #default="scope">
|
||||
<div class="content-preview" :title="scope.row.intro">
|
||||
{{ scope.row.intro.length > 80 ? `${scope.row.intro.slice(0, 80)}...` : scope.row.intro }}
|
||||
</div>
|
||||
<el-button size="mini" type="text" class="mt-1 text-blue-600" @click="handleViewIntro(scope.row)">查看详情</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="日程图" width="120">
|
||||
<template #default="scope">
|
||||
<el-image style="width: 80px; height: 40px; border-radius: 4px;" :src="scope.row.schedule_image_url" :preview-src-list="[scope.row.schedule_image_url]" fit="cover" v-if="scope.row.schedule_image_url">
|
||||
<template #error>
|
||||
<div class="flex items-center justify-center w-full h-full bg-gray-100 text-gray-500 text-xs">加载失败</div>
|
||||
</template>
|
||||
</el-image>
|
||||
<span class="text-gray-400 text-xs" v-else>无日程图</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="start_time" label="开始时间" width="180" sortable></el-table-column>
|
||||
<el-table-column prop="end_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="200" 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="introDialogVisible" title="会议简介详情" :width="`800px`" :before-close="handleCloseDialog">
|
||||
<h3 class="text-xl font-bold text-gray-800 mb-4">会议主题: {{ currentMeeting.theme }}</h3>
|
||||
<div class="content-full text-gray-700 leading-relaxed whitespace-pre-wrap" v-html="currentMeeting.intro || '当前会议暂无简介'">
|
||||
</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.theme" placeholder="请输入会议主题" clearable :disabled="isSubmitting" maxlength="255" />
|
||||
</div>
|
||||
|
||||
<div class="form-group subtitle-group">
|
||||
<label class="form-label">副标题</label>
|
||||
<el-input v-model="form.subtitle" placeholder="请输入会议副标题(可选)" clearable :disabled="isSubmitting" maxlength="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 schedule-group">
|
||||
<label class="form-label">日程图片</label>
|
||||
<el-upload action="http://localhost:8080/api/upload/image" name="image" :show-file-list="false" :on-success="handleScheduleSuccess" :before-upload="beforeScheduleUpload" :on-error="handleScheduleError" :disabled="isSubmitting">
|
||||
<img v-if="form.schedule_image_url" :src="form.schedule_image_url" class="schedule-preview" alt="日程图"/>
|
||||
<el-icon v-else class="schedule-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 time-group grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="form-label required">开始时间</label>
|
||||
<!-- 修复:添加 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+8" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label required">结束时间</label>
|
||||
<!-- 修复:添加 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+8" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group intro-group">
|
||||
<label class="form-label">会议简介</label>
|
||||
<div ref="editorRef" class="quill-editor"></div>
|
||||
<p class="text-xs text-gray-500 mt-2">支持Markdown格式,可输入长文本内容</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div style="flex: auto">
|
||||
<el-button @click="handleDrawerClose">取消</el-button>
|
||||
<el-button type="primary" @click="submitMeeting" :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, ElDatePicker } from 'element-plus';
|
||||
import { Edit, Delete, Plus } 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/dist/locale/zh-cn.mjs';
|
||||
|
||||
// --- 类型定义(对齐meeting表结构)---
|
||||
interface Meeting {
|
||||
id: number;
|
||||
theme: string;
|
||||
subtitle: string;
|
||||
intro: string;
|
||||
cover_url: string;
|
||||
schedule_image_url: string;
|
||||
start_time: string;
|
||||
end_time: string;
|
||||
create_time: string;
|
||||
update_time: string;
|
||||
is_delete: number;
|
||||
}
|
||||
|
||||
interface ListMeetingReq {
|
||||
page: number;
|
||||
page_size: number;
|
||||
}
|
||||
|
||||
// --- API 基地址 ---
|
||||
const API_BASE_URL = 'http://localhost:8080/api';
|
||||
|
||||
// =================================================================
|
||||
// 列表页相关状态与逻辑
|
||||
// =================================================================
|
||||
const loading = ref(true);
|
||||
const tableData = ref<Meeting[]>([]);
|
||||
const currentPage = ref(1);
|
||||
const pageSize = ref(10);
|
||||
const total = ref(0);
|
||||
const introDialogVisible = ref(false);
|
||||
const currentMeeting = ref<Meeting>({
|
||||
id: 0, theme: '', subtitle: '', intro: '', cover_url: '',
|
||||
schedule_image_url: '', start_time: '', end_time: '',
|
||||
create_time: '', update_time: '', is_delete: 0
|
||||
});
|
||||
|
||||
// --- 获取会议列表(对齐GET /api/meetings 分页接口)---
|
||||
const fetchData = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
// 构造请求体(POST请求通过body传递分页参数)
|
||||
const reqData = {
|
||||
page: currentPage.value, // 对应分页页码
|
||||
page_size: pageSize.value // 对应每页条数
|
||||
};
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}/meetings/list`, {
|
||||
method: 'POST', // 改为POST方法
|
||||
headers: {
|
||||
'Content-Type': 'application/json' // 明确JSON格式
|
||||
},
|
||||
body: JSON.stringify(reqData) // 发送JSON格式的请求体
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error(`请求失败!状态码:${response.status}`);
|
||||
const data = await response.json();
|
||||
tableData.value = data.meetings || []; // 假设响应体中会议列表字段为meetings
|
||||
total.value = data.total || 0; // 假设响应体中总条数字段为total
|
||||
} catch (error) {
|
||||
console.error('[ERROR] 获取会议列表失败:', error);
|
||||
ElMessage.error('获取会议列表失败,请检查接口或网络!');
|
||||
} finally {
|
||||
loading.value = false; // 无论成功失败,都关闭加载状态
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(fetchData);
|
||||
|
||||
// --- 查看简介详情 ---
|
||||
const handleViewIntro = (row: Meeting) => {
|
||||
currentMeeting.value = { ...row };
|
||||
introDialogVisible.value = true;
|
||||
};
|
||||
|
||||
const handleCloseDialog = () => {
|
||||
introDialogVisible.value = false;
|
||||
};
|
||||
|
||||
// --- 删除会议 ---
|
||||
const handleDelete = (row: Meeting) => {
|
||||
ElMessageBox.confirm(`确定要删除会议《${row.theme}》吗?此操作无法撤销!`, '警告', {
|
||||
confirmButtonText: '确定删除',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
})
|
||||
.then(async () => {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/meetings/${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);
|
||||
|
||||
// --- 表单默认值(对齐meeting表字段)---
|
||||
const defaultFormState = () => ({
|
||||
id: null as number | null,
|
||||
theme: '',
|
||||
subtitle: '',
|
||||
cover_url: '',
|
||||
schedule_image_url: '',
|
||||
start_time: '',
|
||||
end_time: '',
|
||||
intro: '', // 对应富文本内容
|
||||
});
|
||||
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'],
|
||||
[{ 'list': 'ordered'}, { 'list': 'bullet' }]
|
||||
],
|
||||
handlers: {
|
||||
image: handleEditorImageUpload,
|
||||
}
|
||||
}
|
||||
},
|
||||
placeholder: '请输入会议简介(支持Markdown格式)...'
|
||||
});
|
||||
|
||||
// 编辑时回显内容
|
||||
if (form.value.intro) {
|
||||
quillInstance.root.innerHTML = form.value.intro;
|
||||
}
|
||||
|
||||
// 监听内容变化
|
||||
quillInstance.on('text-change', (_, __, source) => {
|
||||
if (source === 'user') {
|
||||
form.value.intro = quillInstance?.root.innerHTML || '';
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// --- 监听抽屉显示/隐藏 ---
|
||||
watch(drawerVisible, (visible) => {
|
||||
if (visible) {
|
||||
nextTick(() => {
|
||||
initQuillEditor();
|
||||
});
|
||||
} else {
|
||||
// 关闭时重置编辑器和表单
|
||||
quillInstance = null;
|
||||
form.value = defaultFormState();
|
||||
}
|
||||
});
|
||||
|
||||
// --- 新增会议 ---
|
||||
const handleAdd = () => {
|
||||
form.value = defaultFormState();
|
||||
drawerVisible.value = true;
|
||||
};
|
||||
|
||||
// --- 编辑会议 ---
|
||||
const handleEdit = (row: Meeting) => {
|
||||
form.value = {
|
||||
id: row.id,
|
||||
theme: row.theme,
|
||||
subtitle: row.subtitle,
|
||||
cover_url: row.cover_url,
|
||||
schedule_image_url: row.schedule_image_url,
|
||||
start_time: row.start_time,
|
||||
end_time: row.end_time,
|
||||
intro: row.intro,
|
||||
};
|
||||
drawerVisible.value = true;
|
||||
};
|
||||
|
||||
const handleDrawerClose = () => {
|
||||
drawerVisible.value = false;
|
||||
};
|
||||
|
||||
// --- 提交会议(新增/更新)---
|
||||
const submitMeeting = async () => {
|
||||
// 表单校验
|
||||
if (!form.value.theme.trim()) {
|
||||
ElMessage.warning('请输入会议主题');
|
||||
return;
|
||||
}
|
||||
if (!form.value.start_time) {
|
||||
ElMessage.warning('请选择会议开始时间');
|
||||
return;
|
||||
}
|
||||
if (!form.value.end_time) {
|
||||
ElMessage.warning('请选择会议结束时间');
|
||||
return;
|
||||
}
|
||||
// 修复:使用日期对象直接比较,避免字符串格式问题
|
||||
const startTime = new Date(form.value.start_time);
|
||||
const endTime = new Date(form.value.end_time);
|
||||
if (endTime < startTime) {
|
||||
ElMessage.warning('结束时间不能早于开始时间');
|
||||
return;
|
||||
}
|
||||
|
||||
isSubmitting.value = true;
|
||||
const submitData = {
|
||||
theme: form.value.theme.trim(),
|
||||
subtitle: form.value.subtitle.trim(),
|
||||
intro: form.value.intro || '',
|
||||
cover_url: form.value.cover_url || '',
|
||||
schedule_image_url: form.value.schedule_image_url || '',
|
||||
start_time: form.value.start_time,
|
||||
end_time: form.value.end_time,
|
||||
};
|
||||
|
||||
try {
|
||||
let url = `${API_BASE_URL}/meetings`;
|
||||
let method = 'POST';
|
||||
|
||||
// 编辑模式:添加ID参数
|
||||
if (form.value.id) {
|
||||
submitData['id'] = form.value.id;
|
||||
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 || (form.value.id ? '更新失败' : '创建失败'));
|
||||
}
|
||||
|
||||
ElMessage.success(form.value.id ? '会议更新成功!' : '会议创建成功!');
|
||||
drawerVisible.value = false;
|
||||
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('封面上传失败');
|
||||
};
|
||||
|
||||
// --- 日程图上传 ---
|
||||
const handleScheduleSuccess: UploadProps['onSuccess'] = (response) => {
|
||||
const ossUrl = response.data?.url;
|
||||
if (ossUrl) {
|
||||
form.value.schedule_image_url = ossUrl;
|
||||
ElMessage.success('日程图上传成功');
|
||||
} else {
|
||||
ElMessage.error('日程图上传失败');
|
||||
}
|
||||
};
|
||||
|
||||
const beforeScheduleUpload: 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 handleScheduleError = () => {
|
||||
ElMessage.error('日程图上传失败');
|
||||
};
|
||||
|
||||
// --- 编辑器图片上传(简介中插入图片)---
|
||||
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);
|
||||
form.value.intro = quillInstance.root.innerHTML;
|
||||
|
||||
} catch (err) {
|
||||
const errMsg = err instanceof Error ? err.message : '未知错误';
|
||||
quillInstance.deleteText(insertIndex, '[图片上传中...]'.length);
|
||||
quillInstance.insertText(insertIndex, `[图片上传失败:${errMsg}]`, { color: '#f56c6c', italic: true });
|
||||
ElMessage.error(`编辑器图片上传失败:${errMsg}`);
|
||||
}
|
||||
}
|
||||
|
||||
// --- 粘贴图片处理 ---
|
||||
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;
|
||||
}
|
||||
}
|
||||
</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-preview { color: #666; line-height: 1.5; word-break: break-all; }
|
||||
.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; }
|
||||
.grid { display: grid; }
|
||||
.grid-cols-2 { grid-template-columns: repeat(2, 1fr); }
|
||||
.gap-4 { gap: 16px; }
|
||||
|
||||
/* 富文本编辑器 */
|
||||
.quill-editor { height: 300px; border-radius: 4px; border: 1px solid #dcdfe6; }
|
||||
|
||||
/* 封面上传 */
|
||||
.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; border-radius: 6px; }
|
||||
|
||||
/* 日程图上传 */
|
||||
.schedule-uploader-icon { font-size: 24px; color: #8c939d; width: 120px; height: 80px; text-align: center; line-height: 80px; }
|
||||
.schedule-preview { width: 120px; height: 80px; display: block; object-fit: cover; border-radius: 6px; }
|
||||
|
||||
/* 日期选择器 */
|
||||
.el-date-picker { width: 100%; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user