1584 lines
43 KiB
Vue
1584 lines
43 KiB
Vue
<template>
|
||
<div class="submit-page">
|
||
<Navbar />
|
||
<Loading :visible="isLoading" :text="loadingText" />
|
||
|
||
<!-- 课程选择模态窗口 -->
|
||
<div class="course-selector-modal" v-if="showCourseSelector">
|
||
<div class="modal-overlay" @click="closeCourseSelector"></div>
|
||
<div class="modal-container">
|
||
<div class="modal-header">
|
||
<h3>选择课程</h3>
|
||
<button class="close-btn" @click="closeCourseSelector">×</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="search-bar">
|
||
<input
|
||
type="text"
|
||
v-model="courseSearch"
|
||
placeholder="请输入课程名称"
|
||
class="search-input"
|
||
@keyup.enter="searchCourses"
|
||
/>
|
||
<button class="search-btn" @click="searchCourses">搜索</button>
|
||
</div>
|
||
|
||
<div class="course-results">
|
||
<div v-if="searchLoading" class="course-loading">
|
||
<div class="spinner"></div>
|
||
<span>搜索中...</span>
|
||
</div>
|
||
<div v-else-if="searchError" class="search-error">
|
||
{{ searchError }}
|
||
</div>
|
||
<div v-else-if="courseResults.length === 0" class="no-results">
|
||
未找到相关课程
|
||
</div>
|
||
<table v-else class="course-table">
|
||
<thead>
|
||
<tr>
|
||
<th>选择</th>
|
||
<th>课程名称</th>
|
||
<th>课程类别</th>
|
||
<th>授课教师</th>
|
||
<th>评分</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr
|
||
v-for="course in courseResults"
|
||
:key="course.course_id"
|
||
@click="selectCourse(course)"
|
||
:class="{ 'selected': selectedCourse && selectedCourse.course_id === course.course_id }"
|
||
>
|
||
<td>
|
||
<input
|
||
type="radio"
|
||
:checked="selectedCourse && selectedCourse.course_id === course.course_id"
|
||
@click.stop="selectCourse(course)"
|
||
/>
|
||
</td>
|
||
<td>{{ course.course_name }}</td>
|
||
<td>{{ getCategoryName(course.category_id) }}</td>
|
||
<td>{{ course.teachers }}</td>
|
||
<td>
|
||
<span class="rating">{{ course.rating }}</span>
|
||
<span class="rating-count">({{ course.rating_count }}人评)</span>
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
|
||
<!-- 分页控件 -->
|
||
<div class="pagination" v-if="totalPages > 1">
|
||
<button
|
||
class="page-btn"
|
||
:disabled="currentPage === 1"
|
||
@click="changePage(currentPage - 1)"
|
||
>
|
||
上一页
|
||
</button>
|
||
|
||
<span class="page-info">第 {{ currentPage }} / {{ totalPages }} 页</span>
|
||
|
||
<button
|
||
class="page-btn"
|
||
:disabled="currentPage === totalPages"
|
||
@click="changePage(currentPage + 1)"
|
||
>
|
||
下一页
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button
|
||
class="btn btn-secondary"
|
||
@click="closeCourseSelector"
|
||
>
|
||
取消
|
||
</button>
|
||
<button
|
||
class="btn btn-primary"
|
||
@click="confirmCourseSelection"
|
||
:disabled="!selectedCourse"
|
||
>
|
||
确定
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="container">
|
||
<div class="page-header">
|
||
<h1>{{ isEditMode ? '编辑投稿内容' : '内容投稿' }}</h1>
|
||
<p class="sub-title">感谢您分享您的学习资源或独到见解!</p>
|
||
</div>
|
||
|
||
<!-- 投稿表单 -->
|
||
<div class="submission-form">
|
||
<div class="form-section">
|
||
<div class="form-group">
|
||
<label for="title">标题 {{ isEditMode ? '(如需更改标题,请不要更改【edit】前缀)' : '' }}</label>
|
||
<input
|
||
type="text"
|
||
id="title"
|
||
v-model="formData.title"
|
||
class="form-control"
|
||
placeholder="请输入投稿标题"
|
||
maxlength="50"
|
||
/>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label for="section">板块</label>
|
||
<select id="section" v-model="formData.section" class="form-control">
|
||
<option value="" disabled>请选择板块</option>
|
||
<option value="攻略指南">攻略指南</option>
|
||
<option value="资源共享">资源共享</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label for="note">备注信息 (选填)</label>
|
||
<input
|
||
type="text"
|
||
id="note"
|
||
v-model="formData.note"
|
||
class="form-control"
|
||
placeholder="可添加备注信息"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 图片上传区 -->
|
||
<!-- <div class="form-section">
|
||
<h3>图片上传</h3>
|
||
<p class="section-desc">您可以上传图片并将图片标签插入到文章中</p>
|
||
|
||
<div class="upload-area">
|
||
<div class="file-input-container">
|
||
<label for="imagePicker" class="upload-label">
|
||
<span class="upload-icon">📷</span>
|
||
<span>选择图片</span>
|
||
</label>
|
||
<input
|
||
type="file"
|
||
id="imagePicker"
|
||
ref="imagePicker"
|
||
@change="uploadImage"
|
||
accept="image/*"
|
||
class="file-input"
|
||
/>
|
||
</div>
|
||
|
||
<div v-if="imageUrl" class="preview-area">
|
||
<div class="image-preview">
|
||
<img :src="imageUrl" alt="预览图" ref="imagePreview" />
|
||
</div>
|
||
<div class="image-info">
|
||
<p>图片HTML标签:</p>
|
||
<div class="tag-container">
|
||
<code>{{ imageTag }}</code>
|
||
</div>
|
||
<button type="button" class="btn btn-secondary" @click="copyImageUrl">
|
||
复制标签
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div> -->
|
||
|
||
<!-- 附件上传区 -->
|
||
<div class="form-section">
|
||
<h3>附件上传</h3>
|
||
<p class="section-desc">您可以上传最多10个文件,总大小不超过500MB。</p>
|
||
|
||
<div class="upload-area">
|
||
<div class="file-input-container">
|
||
<label for="filePicker" class="upload-label">
|
||
<span class="upload-icon">📎</span>
|
||
<span>选择文件</span>
|
||
</label>
|
||
<input
|
||
type="file"
|
||
id="filePicker"
|
||
ref="filePicker"
|
||
@change="validateFiles"
|
||
multiple
|
||
class="file-input"
|
||
/>
|
||
</div>
|
||
|
||
<div v-if="selectedFiles.length > 0" class="file-list">
|
||
<h4>已选择的文件:</h4>
|
||
<ul>
|
||
<li v-for="(file, index) in selectedFiles" :key="index">
|
||
<span class="file-name">{{ file.name }}</span>
|
||
<span class="file-size">({{ formatFileSize(file.size) }})</span>
|
||
</li>
|
||
</ul>
|
||
<p class="total-size">总大小: {{ formatFileSize(totalFileSize) }}</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 编辑器 -->
|
||
<div class="form-section">
|
||
<h3>内容编辑</h3>
|
||
<p class="section-desc">此部分主要用于"攻略指南"板块,"资源共享"板块可以留空。</p>
|
||
<p class="section-desc">使用Markdown语法编辑您的投稿内容。插入图片后,如需修改图片大小,请在编辑器中找到插入的图片标签,修改其width属性。</p>
|
||
<p class="section-desc">如上传了附件且需在文中指定位置展示,<strong>可以用[附件名]来表示附件位置</strong>。如不指定,则审核人员将按照文章内容在适当位置展示。</p>
|
||
<p class="section-desc"><strong>草稿功能和文章页预览只针对此部分内容,上传的附件等将不会保存并不支持文章页预览。</strong></p>
|
||
<p class="section-desc" style="color: purple;">您可以轻松地插入课程卡片(点击<svg data-v-bcfef25c="" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-linecap="round" stroke-linejoin="round" width="20" height="24" stroke-width="1.1" style="vertical-align: middle; display: inline-block; margin-bottom: 2px;"><path data-v-bcfef25c="" d="M22 9l-10 -4l-10 4l10 4l10 -4v6"></path><path data-v-bcfef25c="" d="M6 10.6v5.4a6 3 0 0 0 12 0v-5.4"></path></svg>按钮),引用课程评分系统中的课程数据。<strong>注意:由于网站的安全设置,课程卡片前必须有其他内容,请勿在内容第一行插入课程卡片,否则将无法正常显示!</strong></p>
|
||
<p v-if="isEditMode" class="section-desc" style="color: red;"><strong>检测到您当前正在编辑已有的投稿内容,请注意您之前如有其他投稿的草稿内容,这些内容在此页面保存草稿或提交编辑后将不会保留。</strong></p>
|
||
|
||
<div class="editor-container">
|
||
<MdEditor
|
||
v-model="formData.content"
|
||
ref="markdownEditor"
|
||
:theme="editorTheme"
|
||
:toolbars="editorToolbars"
|
||
language="zh-CN"
|
||
preview-theme="github"
|
||
:placeholder="editorPlaceholder"
|
||
@onUploadImg="handleImageUpload"
|
||
@onSave="saveDraftWithConfirm"
|
||
>
|
||
<template #defToolbars>
|
||
<span class="md-editor-toolbar-item" title="添加课程卡片" @click="chooseCourse">
|
||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" width="20" height="24" stroke-width="1.1">
|
||
<path d="M22 9l-10 -4l-10 4l10 4l10 -4v6"></path>
|
||
<path d="M6 10.6v5.4a6 3 0 0 0 12 0v-5.4"></path>
|
||
</svg>
|
||
</span>
|
||
</template>
|
||
</MdEditor>
|
||
</div>
|
||
|
||
<div class="draft-actions">
|
||
<button
|
||
type="button"
|
||
class="btn btn-secondary"
|
||
@click="loadDraft"
|
||
>
|
||
加载草稿
|
||
</button>
|
||
<button
|
||
type="button"
|
||
class="btn btn-secondary"
|
||
@click="saveDraftWithConfirm"
|
||
>
|
||
保存草稿
|
||
</button>
|
||
<button
|
||
type="button"
|
||
class="btn btn-secondary preview-btn"
|
||
@click="previewDraft"
|
||
>
|
||
在文章页中预览
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 提交按钮 -->
|
||
<div class="form-actions">
|
||
<button type="button" class="btn btn-primary" @click="submitForm">
|
||
{{ isEditMode ? '提交编辑' : '提交投稿' }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
import Navbar from '@/components/NavBar.vue';
|
||
import MdEditor from 'md-editor-v3';
|
||
import 'md-editor-v3/lib/style.css';
|
||
import Cookies from 'js-cookie';
|
||
import messageBox from '@/utils/messageBox.js';
|
||
import Loading from '@/components/Loading.vue';
|
||
|
||
export default {
|
||
name: 'SubmitPage',
|
||
components: {
|
||
Navbar,
|
||
MdEditor,
|
||
Loading
|
||
},
|
||
data() {
|
||
return {
|
||
formData: {
|
||
title: '',
|
||
section: '',
|
||
note: '',
|
||
content: ''
|
||
},
|
||
selectedFiles: [],
|
||
totalFileSize: 0,
|
||
imageUrl: '',
|
||
imageTag: '',
|
||
editorTheme: 'light',
|
||
editorPlaceholder: '在此输入您的内容,支持直接把图片粘贴到编辑器中',
|
||
editorToolbars: [
|
||
'bold', 'italic', 'underline', 'sub', 'sup', '-', 'title', 'unorderedList',
|
||
'orderedList', '-', 'quote', 'link', 'image', 'table', 'codeRow', 'code', 'mermaid', 0,
|
||
'=', 'save', 'revoke', 'next', '-', 'preview', 'pageFullscreen', 'fullscreen'
|
||
],
|
||
isEditMode: false,
|
||
editArticleId: null,
|
||
isLoading: false,
|
||
loadingText: '获取投稿内容中...',
|
||
// 课程选择器相关状态
|
||
showCourseSelector: false,
|
||
courseSearch: '',
|
||
searchLoading: false,
|
||
searchError: null,
|
||
courseResults: [],
|
||
selectedCourse: null,
|
||
currentPage: 1,
|
||
totalPages: 1,
|
||
// 类别映射
|
||
categoryMap: {
|
||
1: '通识选修类',
|
||
2: '人文选修类',
|
||
3: '专业方向类',
|
||
4: '体育类',
|
||
5: '学科基础类',
|
||
6: '暑期国际课',
|
||
7: '数学与自然科学类',
|
||
8: '重修专栏',
|
||
9: '数学与自然科学类(必修)',
|
||
10: '人文社会科学类(必修)',
|
||
11: '学科基础类(必修)',
|
||
12: '专业方向类(必修)',
|
||
13: '实践类(必修)'
|
||
}
|
||
};
|
||
},
|
||
created() {
|
||
document.title = '内容投稿 - NEU小站';
|
||
// 检查登录状态,未登录则跳转到登录页面
|
||
this.checkLoginStatus().then(() => {
|
||
// 检查是否是编辑模式
|
||
this.checkEditMode();
|
||
});
|
||
},
|
||
methods: {
|
||
// 检查登录状态
|
||
async checkLoginStatus() {
|
||
const token = Cookies.get('token');
|
||
if (!token) {
|
||
this.$router.push('/login');
|
||
return Promise.reject(new Error('未登录'));
|
||
}
|
||
|
||
try {
|
||
const response = await fetch('https://newfront.xn--xhq44jb2fzpc.com/user/islogin', {
|
||
method: 'GET',
|
||
headers: {
|
||
'Authorization': token
|
||
}
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error('验证失败');
|
||
}
|
||
|
||
const data = await response.json();
|
||
if (!data.isLoggedIn) {
|
||
// 删除无效的 token
|
||
Cookies.remove('token');
|
||
this.$router.push('/login');
|
||
return Promise.reject(new Error('登录已过期'));
|
||
}
|
||
|
||
return Promise.resolve();
|
||
} catch (error) {
|
||
console.error('登录验证失败:', error);
|
||
// 删除可能无效的 token
|
||
Cookies.remove('token');
|
||
this.$router.push('/login');
|
||
return Promise.reject(error);
|
||
}
|
||
},
|
||
|
||
// 检查是否是编辑模式
|
||
async checkEditMode() {
|
||
// 获取查询参数中的文章ID
|
||
const urlParams = new URLSearchParams(window.location.search);
|
||
const articleId = urlParams.get('article');
|
||
|
||
if (!articleId) return;
|
||
|
||
try {
|
||
// 更新界面状态
|
||
this.isEditMode = true;
|
||
this.editArticleId = articleId;
|
||
|
||
// 显示加载中组件
|
||
this.isLoading = true;
|
||
|
||
// 获取文章详情
|
||
const token = Cookies.get('token');
|
||
const response = await fetch(`https://newfront.xn--xhq44jb2fzpc.com/article?id=${articleId}&edit=true`, {
|
||
method: 'GET',
|
||
headers: {
|
||
'Authorization': token
|
||
}
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error('获取文章失败');
|
||
}
|
||
|
||
const articleData = await response.json();
|
||
|
||
// 填充表单
|
||
this.formData.title = `【edit${articleId}】${articleData.title}`;
|
||
this.formData.section = articleData.section;
|
||
this.formData.content = articleData.content;
|
||
|
||
// 更新标题
|
||
document.title = `编辑投稿内容 - NEU小站`;
|
||
|
||
} catch (error) {
|
||
console.error('获取文章信息失败:', error);
|
||
|
||
// 获取失败则回退到普通投稿模式
|
||
this.isEditMode = false;
|
||
this.editArticleId = null;
|
||
|
||
// 可以显示友好提示
|
||
// messageBox.alert("非法操作!", "提示");
|
||
} finally {
|
||
// 隐藏加载中组件
|
||
this.isLoading = false;
|
||
}
|
||
},
|
||
|
||
// 图片上传处理
|
||
async handleImageUpload(files, callback) {
|
||
// 这个方法处理编辑器内部的图片上传
|
||
// files 是上传的文件数组,callback 是处理上传完成后返回URL的回调函数
|
||
const token = Cookies.get('token');
|
||
|
||
if (!token) {
|
||
messageBox.alert('请先登录!', '提示');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const uploadPromises = files.map(async file => {
|
||
// 检查文件大小(2MB)
|
||
if (file.size > 2 * 1024 * 1024) {
|
||
throw new Error(`图片 ${file.name} 大小不能超过2MB!`);
|
||
}
|
||
|
||
const postfix = file.name.split('.').pop().toLowerCase();
|
||
const type = file.type;
|
||
|
||
// 请求上传URL
|
||
const prepareResponse = await fetch('https://userlogin.xn--xhq44jb2fzpc.com/submission/prepare-image-upload', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Authorization': token,
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({ postfix, type })
|
||
});
|
||
|
||
if (!prepareResponse.ok) {
|
||
throw new Error('准备上传失败');
|
||
}
|
||
|
||
const { uploadUrl, path } = await prepareResponse.json();
|
||
|
||
// 上传文件
|
||
const uploadResponse = await fetch(uploadUrl, {
|
||
method: 'PUT',
|
||
body: file,
|
||
headers: {
|
||
'Content-Type': type,
|
||
'Authorization': token
|
||
}
|
||
});
|
||
|
||
if (!uploadResponse.ok) {
|
||
throw new Error(`文件 ${file.name} 上传失败`);
|
||
}
|
||
|
||
// 返回图片URL - 现在返回HTML img标签格式
|
||
const imageUrl = `https://download.xn--xhq44jb2fzpc.com/${path}`;
|
||
|
||
// 返回HTML img标签
|
||
return imageUrl;
|
||
});
|
||
|
||
// 等待所有上传完成
|
||
const htmlTags = await Promise.all(uploadPromises);
|
||
|
||
// 在回调中返回HTML标签数组,编辑器会直接插入这些标签
|
||
callback(htmlTags);
|
||
|
||
// 提示用户如何调整图片大小
|
||
if (htmlTags.length > 0) {
|
||
setTimeout(() => {
|
||
messageBox.alert(
|
||
'您已成功上传图片。如需修改图片大小,请在编辑器中找到刚插入的图片标签,修改其width属性。',
|
||
'图片大小调整提示'
|
||
);
|
||
}, 800);
|
||
}
|
||
} catch (error) {
|
||
console.error('图片上传失败:', error);
|
||
messageBox.alert('图片上传失败:' + error.message, '错误');
|
||
}
|
||
},
|
||
|
||
// 验证上传的文件
|
||
validateFiles(event) {
|
||
const maxFiles = 10;
|
||
const maxTotalFileSize = 500 * 1024 * 1024; // 500MB in bytes
|
||
const files = event.target.files;
|
||
|
||
if (files.length > maxFiles) {
|
||
messageBox.alert(`您只能选择最多 ${maxFiles} 个文件。`, '提示');
|
||
event.target.value = ''; // 清除已选文件
|
||
this.selectedFiles = []; // 清空状态
|
||
this.totalFileSize = 0;
|
||
return;
|
||
}
|
||
|
||
let totalSize = 0;
|
||
const validFiles = [];
|
||
|
||
// 检查文件限制
|
||
for (let i = 0; i < files.length; i++) {
|
||
const file = files[i];
|
||
const fileSize = file.size;
|
||
|
||
// 更新总文件大小
|
||
totalSize += fileSize;
|
||
|
||
// 检查文件总大小是否超过限制
|
||
if (totalSize > maxTotalFileSize) {
|
||
messageBox.alert(`所有文件的总大小不能超过 500MB。`, '提示');
|
||
event.target.value = ''; // 清除已选文件
|
||
this.selectedFiles = []; // 清空状态
|
||
this.totalFileSize = 0;
|
||
return;
|
||
}
|
||
|
||
validFiles.push(file);
|
||
}
|
||
|
||
// 验证通过,更新状态
|
||
this.selectedFiles = validFiles;
|
||
this.totalFileSize = totalSize;
|
||
},
|
||
|
||
// 格式化文件大小显示
|
||
formatFileSize(bytes) {
|
||
if (bytes === 0) return '0 Bytes';
|
||
|
||
const k = 1024;
|
||
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||
|
||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||
},
|
||
|
||
// 上传图片
|
||
async uploadImage(event) {
|
||
const file = event.target.files[0];
|
||
|
||
// 基础检查
|
||
if (!file) {
|
||
messageBox.alert("请先选择图片文件!", '提示');
|
||
return;
|
||
}
|
||
|
||
// 检查文件大小(2MB)
|
||
if (file.size > 2 * 1024 * 1024) {
|
||
messageBox.alert("图片文件大小不能超过2MB!", '提示');
|
||
event.target.value = '';
|
||
return;
|
||
}
|
||
|
||
// 检查文件类型
|
||
if (!file.type.startsWith('image/')) {
|
||
messageBox.alert("请选择图片文件!", '提示');
|
||
event.target.value = '';
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const token = Cookies.get('token');
|
||
|
||
if (!token) {
|
||
messageBox.alert('请先登录!', '提示');
|
||
return;
|
||
}
|
||
|
||
const postfix = file.name.split('.').pop().toLowerCase();
|
||
// 使用文件的MIME类型
|
||
const type = file.type;
|
||
|
||
// 请求上传URL,传递MIME类型
|
||
const prepareResponse = await fetch('https://userlogin.xn--xhq44jb2fzpc.com/submission/prepare-image-upload', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Authorization': token,
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({ postfix, type })
|
||
});
|
||
|
||
if (!prepareResponse.ok) {
|
||
throw new Error('准备上传失败');
|
||
}
|
||
|
||
const { uploadUrl, path } = await prepareResponse.json();
|
||
|
||
// 使用签名URL上传文件,使用相同的MIME类型
|
||
const uploadResponse = await fetch(uploadUrl, {
|
||
method: 'PUT',
|
||
body: file,
|
||
headers: {
|
||
'Content-Type': type,
|
||
'Authorization': token
|
||
}
|
||
});
|
||
|
||
if (!uploadResponse.ok) {
|
||
// 尝试解析错误响应
|
||
const text = await uploadResponse.text();
|
||
if (text.includes('<?xml')) {
|
||
const parser = new DOMParser();
|
||
const xmlDoc = parser.parseFromString(text, "text/xml");
|
||
const requestId = xmlDoc.getElementsByTagName("RequestId")[0]?.textContent;
|
||
throw new Error(`文件上传失败 (RequestId: ${requestId || '未知'})`);
|
||
}
|
||
throw new Error('文件上传失败');
|
||
}
|
||
|
||
// 构造完整URL
|
||
const imageUrl = `https://download.xn--xhq44jb2fzpc.com/${path}`;
|
||
const imageTag = `<img src="${imageUrl}" alt="自定义图片文字" width="150">`;
|
||
|
||
// 更新界面显示
|
||
this.imageUrl = imageUrl;
|
||
this.imageTag = imageTag;
|
||
|
||
} catch (error) {
|
||
console.error('上传失败:', error);
|
||
messageBox.alert('图片上传失败:' + error.message, '错误');
|
||
event.target.value = '';
|
||
}
|
||
},
|
||
|
||
// 复制图片HTML标签到剪贴板
|
||
copyImageUrl() {
|
||
// 创建一个临时文本区域元素
|
||
const textArea = document.createElement("textarea");
|
||
textArea.value = this.imageTag;
|
||
document.body.appendChild(textArea);
|
||
textArea.select();
|
||
|
||
try {
|
||
const successful = document.execCommand('copy');
|
||
const msg = successful ? '标签已复制到剪贴板!请直接粘贴到编辑区中,并根据预览效果调整大小。' : '复制失败!';
|
||
messageBox.alert(msg, '提示');
|
||
} catch (err) {
|
||
messageBox.alert(`复制失败!${err}`, '错误');
|
||
}
|
||
|
||
document.body.removeChild(textArea);
|
||
},
|
||
|
||
// 预览草稿
|
||
async previewDraft() {
|
||
const { content } = this.formData;
|
||
|
||
if (!content.trim()) {
|
||
messageBox.alert("内容不能为空!", '提示');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
// 添加确认提示框
|
||
const confirmResult = await messageBox.confirm("此操作会自动保存草稿。确认继续吗?如您之前有草稿记录,此操作会覆盖之前的草稿内容。", '确认');
|
||
|
||
// 如果用户取消了操作,直接返回
|
||
if (!confirmResult) return;
|
||
|
||
// 保存草稿后跳转到预览页面
|
||
this.saveDraft(true).then(() => {
|
||
window.open('/preview', '_blank');
|
||
}).catch(error => {
|
||
console.error("无法预览草稿:", error);
|
||
});
|
||
} catch (error) {
|
||
console.error("预览草稿操作被取消:", error);
|
||
}
|
||
},
|
||
|
||
// 包装保存草稿方法,确保调用确认对话框
|
||
async saveDraftWithConfirm() {
|
||
const { content } = this.formData;
|
||
|
||
if (!content.trim()) {
|
||
messageBox.alert("内容不能为空!", '提示');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
// 显示确认对话框
|
||
const confirmResult = await messageBox.confirm("确认保存草稿内容吗?如您之前有草稿记录,此操作会覆盖之前的草稿内容。", '确认');
|
||
|
||
// 确认后保存草稿
|
||
if (confirmResult) {
|
||
this.saveDraft(true);
|
||
}
|
||
} catch (error) {
|
||
console.error("保存草稿操作被取消:", error);
|
||
}
|
||
},
|
||
|
||
// 保存草稿
|
||
async saveDraft(silent = false) {
|
||
const token = Cookies.get('token');
|
||
|
||
if (!token) {
|
||
messageBox.alert("非法操作!请先登录。", '警告');
|
||
return Promise.reject(new Error("未登录"));
|
||
}
|
||
|
||
const draftContent = this.formData.content;
|
||
|
||
if (!draftContent.trim()) {
|
||
messageBox.alert("内容不能为空!", '提示');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
let confirmResult = true;
|
||
|
||
if (!silent) {
|
||
confirmResult = await messageBox.confirm("确认保存草稿内容吗?如您之前有草稿记录,此操作会覆盖之前的草稿内容。", '确认');
|
||
}
|
||
|
||
// 如果用户取消了操作,直接返回
|
||
if (!confirmResult) return;
|
||
|
||
const response = await fetch('https://userlogin.xn--xhq44jb2fzpc.com/submission/save-draft', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Authorization': token,
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({ content: draftContent }) // 将草稿内容发送到后端
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const errorData = await response.json();
|
||
throw new Error(errorData.message || "保存草稿失败");
|
||
}
|
||
|
||
if (!silent) {
|
||
messageBox.alert("草稿已保存!", '成功');
|
||
}
|
||
|
||
return Promise.resolve();
|
||
} catch (error) {
|
||
console.error("保存草稿时出错:", error);
|
||
if (!silent) {
|
||
messageBox.alert("保存草稿失败,请稍后再试。", '错误');
|
||
}
|
||
return Promise.reject(error);
|
||
}
|
||
},
|
||
|
||
// 加载草稿
|
||
async loadDraft() {
|
||
const token = Cookies.get('token');
|
||
|
||
if (!token) {
|
||
messageBox.alert("非法操作!请先登录。", '警告');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const response = await fetch('https://userlogin.xn--xhq44jb2fzpc.com/submission/load-draft', {
|
||
method: 'GET',
|
||
headers: {
|
||
'Authorization': token
|
||
}
|
||
});
|
||
|
||
if (!response.ok) {
|
||
if (response.status === 404) {
|
||
messageBox.alert("没有草稿记录!", '提示');
|
||
} else {
|
||
const errorData = await response.json();
|
||
throw new Error(errorData.message || "无法加载草稿内容");
|
||
}
|
||
return;
|
||
}
|
||
|
||
const draftData = await response.json();
|
||
|
||
try {
|
||
const confirmResult = await messageBox.confirm("此操作会覆盖您当前的输入内容,确认加载草稿吗?", '确认');
|
||
|
||
// 如果用户取消了操作,直接返回
|
||
if (!confirmResult) return;
|
||
|
||
// 设置编辑器中的内容
|
||
this.formData.content = draftData.content;
|
||
messageBox.alert("草稿已加载!", '成功');
|
||
} catch (error) {
|
||
// 用户取消了加载操作,不执行任何操作
|
||
console.log("用户取消了加载草稿操作");
|
||
}
|
||
} catch (error) {
|
||
console.error("加载草稿时出错:", error);
|
||
messageBox.alert("加载草稿失败,请稍后再试。", '错误');
|
||
}
|
||
},
|
||
|
||
// 提交表单
|
||
async submitForm() {
|
||
const { title, section, note, content } = this.formData;
|
||
const token = Cookies.get('token');
|
||
const files = this.selectedFiles;
|
||
|
||
if (!title || !section) {
|
||
messageBox.alert("请填写内容的标题和板块。", '提示');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
let confirmMessage = '请仔细检查后提交,多次提交无关内容将被禁止访问网站!';
|
||
if (this.isEditMode) {
|
||
confirmMessage = '确认提交修改?修改后的内容将重新进入审核流程。';
|
||
}
|
||
|
||
const confirmResult = await messageBox.confirm(confirmMessage, '提交确认');
|
||
|
||
// 如果用户取消了操作,直接返回
|
||
if (!confirmResult) return;
|
||
|
||
// 准备文件信息
|
||
const fileData = [];
|
||
for (let i = 0; i < files.length; i++) {
|
||
const file = files[i];
|
||
const postfix = file.name.split('.').pop().toLowerCase();
|
||
fileData.push({
|
||
filename: file.name,
|
||
postfix: postfix,
|
||
type: file.type || 'application/octet-stream',
|
||
size: (file.size / (1024 * 1024)).toFixed(2)
|
||
});
|
||
}
|
||
|
||
// 提交表单和获取上传URL
|
||
let apiUrl = 'https://userlogin.xn--xhq44jb2fzpc.com/submission/submit';
|
||
|
||
// 提交参数
|
||
const submitData = {
|
||
title,
|
||
section,
|
||
detail: content || null,
|
||
submission_note: note || null,
|
||
has_file: files.length > 0 ? 1 : 0,
|
||
files: fileData
|
||
};
|
||
|
||
// 如果是编辑模式,添加文章ID
|
||
if (this.isEditMode && this.editArticleId) {
|
||
submitData.article_id = this.editArticleId;
|
||
}
|
||
|
||
const response = await fetch(apiUrl, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Authorization': token,
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify(submitData)
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const errorData = await response.json();
|
||
throw new Error(errorData.message || '投稿失败,请重试!');
|
||
}
|
||
|
||
const result = await response.json();
|
||
|
||
// 如果有文件需要上传
|
||
if (result.fileUploadData && result.fileUploadData.length > 0) {
|
||
// 显示加载中状态
|
||
this.loadingText = "文件正在上传中,请耐心等待...";
|
||
this.isLoading = true;
|
||
|
||
for (let i = 0; i < files.length; i++) {
|
||
const file = files[i];
|
||
const uploadInfo = result.fileUploadData[i];
|
||
|
||
const uploadResponse = await fetch(uploadInfo.uploadUrl, {
|
||
method: 'PUT',
|
||
body: file,
|
||
headers: {
|
||
'Content-Type': file.type || 'application/octet-stream',
|
||
'Authorization': token
|
||
}
|
||
});
|
||
|
||
if (!uploadResponse.ok) {
|
||
// 隐藏加载中状态
|
||
this.isLoading = false;
|
||
throw new Error(`文件 ${file.name} 上传失败`);
|
||
}
|
||
}
|
||
|
||
// 隐藏加载中状态
|
||
this.isLoading = false;
|
||
}
|
||
|
||
let successMessage = `投稿成功!您将会在审核状态变化后收到邮件通知,敬请留意~`;
|
||
if (this.isEditMode) {
|
||
successMessage = `修改提交成功!您将会在审核状态变化后收到邮件通知,敬请留意~`;
|
||
}
|
||
await messageBox.alert(successMessage, '成功');
|
||
this.$router.push('/user-center'); // 提交成功后跳转到用户中心
|
||
|
||
} catch (error) {
|
||
// 确保隐藏加载中状态
|
||
this.isLoading = false;
|
||
|
||
console.error("投稿失败:", error);
|
||
// 判断是否为用户取消操作
|
||
if (error && error.action === 'cancel') {
|
||
// 用户主动取消操作,不显示错误提示
|
||
return;
|
||
}
|
||
messageBox.alert(`投稿失败:${error.message || '未知错误'}`, '错误');
|
||
}
|
||
},
|
||
chooseCourse() {
|
||
this.showCourseSelector = true;
|
||
this.courseSearch = '';
|
||
this.courseResults = [];
|
||
this.selectedCourse = null;
|
||
this.currentPage = 1;
|
||
this.totalPages = 1;
|
||
this.searchError = null;
|
||
},
|
||
|
||
closeCourseSelector() {
|
||
this.showCourseSelector = false;
|
||
},
|
||
|
||
async searchCourses(page = 1) {
|
||
// 如果参数是事件对象,设为默认页码1
|
||
if (page && typeof page === 'object' && page.type) {
|
||
page = 1;
|
||
}
|
||
|
||
if (!this.courseSearch.trim()) {
|
||
messageBox.alert('请输入课程名称', '提示');
|
||
return;
|
||
}
|
||
|
||
this.searchLoading = true;
|
||
this.searchError = null;
|
||
this.selectedCourse = null;
|
||
this.currentPage = page;
|
||
|
||
try {
|
||
// 构建API URL
|
||
const encodedSearch = encodeURIComponent(this.courseSearch.trim());
|
||
const url = `https://coursesystem.xn--xhq44jb2fzpc.com/list/courses?search=${encodedSearch}&page=${page}`;
|
||
|
||
// 发送请求
|
||
const response = await fetch(url);
|
||
|
||
if (!response.ok) {
|
||
throw new Error('搜索课程失败');
|
||
}
|
||
|
||
const data = await response.json();
|
||
|
||
// 更新数据
|
||
this.courseResults = data.courses || [];
|
||
this.currentPage = data.currentPage || 1;
|
||
this.totalPages = data.totalPages || 1;
|
||
|
||
} catch (error) {
|
||
console.error('课程搜索出错:', error);
|
||
this.searchError = '搜索课程时发生错误,请稍后重试';
|
||
} finally {
|
||
this.searchLoading = false;
|
||
}
|
||
},
|
||
|
||
getCategoryName(categoryId) {
|
||
return this.categoryMap[categoryId] || '未知类别';
|
||
},
|
||
|
||
selectCourse(course) {
|
||
this.selectedCourse = course;
|
||
},
|
||
|
||
changePage(page) {
|
||
if (page < 1 || page > this.totalPages) return;
|
||
this.searchCourses(page);
|
||
},
|
||
|
||
confirmCourseSelection() {
|
||
if (!this.selectedCourse) {
|
||
messageBox.alert('请先选择一个课程', '提示');
|
||
return;
|
||
}
|
||
|
||
// 获取编辑器实例
|
||
const editor = this.$refs.markdownEditor;
|
||
|
||
// 构建要插入的内容
|
||
const courseCardCode = `<CourseCard id="${this.selectedCourse.course_id}" />\n`;
|
||
|
||
// 在光标位置插入内容
|
||
editor.insert((selectedText) => {
|
||
return {
|
||
targetValue: courseCardCode,
|
||
select: false,
|
||
deviationStart: 0,
|
||
deviationEnd: 0
|
||
};
|
||
});
|
||
|
||
// 关闭模态窗口
|
||
this.closeCourseSelector();
|
||
}
|
||
},
|
||
computed: {
|
||
displayedPages() {
|
||
// 最多显示5个页码
|
||
const maxButtons = 5;
|
||
const halfButtons = Math.floor(maxButtons / 2);
|
||
|
||
let startPage = Math.max(1, this.currentPage - halfButtons);
|
||
let endPage = Math.min(this.totalPages, startPage + maxButtons - 1);
|
||
|
||
// 调整开始页码,确保显示正确数量的按钮
|
||
if (endPage - startPage + 1 < maxButtons) {
|
||
startPage = Math.max(1, endPage - maxButtons + 1);
|
||
}
|
||
|
||
const pages = [];
|
||
for (let i = startPage; i <= endPage; i++) {
|
||
pages.push(i);
|
||
}
|
||
|
||
return pages;
|
||
}
|
||
}
|
||
};
|
||
</script>
|
||
|
||
<style scoped>
|
||
.submit-page {
|
||
background-color: #f5f7fa;
|
||
min-height: 100vh;
|
||
padding-top: 60px; /* 为导航栏预留空间 */
|
||
}
|
||
|
||
.container {
|
||
max-width: 1000px;
|
||
margin: 0 auto;
|
||
padding: 30px 20px;
|
||
}
|
||
|
||
.page-header {
|
||
margin-bottom: 30px;
|
||
text-align: center;
|
||
}
|
||
|
||
.page-header h1 {
|
||
font-size: 28px;
|
||
color: #333;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.sub-title {
|
||
color: #666;
|
||
font-size: 16px;
|
||
}
|
||
|
||
.submission-form {
|
||
background: white;
|
||
border-radius: 10px;
|
||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||
overflow: hidden;
|
||
}
|
||
|
||
.form-section {
|
||
padding: 30px;
|
||
border-bottom: 1px solid #eee;
|
||
}
|
||
|
||
.form-section:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.form-section h3 {
|
||
margin-top: 0;
|
||
margin-bottom: 10px;
|
||
font-size: 18px;
|
||
color: #333;
|
||
}
|
||
|
||
.section-desc {
|
||
color: #666;
|
||
font-size: 14px;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.form-group {
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.form-group:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.form-group label {
|
||
display: block;
|
||
margin-bottom: 8px;
|
||
font-weight: 600;
|
||
color: #333;
|
||
}
|
||
|
||
.form-control {
|
||
width: 100%;
|
||
padding: 10px 12px;
|
||
border: 1px solid #ddd;
|
||
border-radius: 4px;
|
||
font-size: 14px;
|
||
transition: border-color 0.3s;
|
||
}
|
||
|
||
.form-control:focus {
|
||
border-color: #3273dc;
|
||
outline: none;
|
||
}
|
||
|
||
select.form-control {
|
||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='5'%3E%3Cpath fill='%23333' d='M0 0h8L4 5z'/%3E%3C/svg%3E");
|
||
background-repeat: no-repeat;
|
||
background-position: right 1em center;
|
||
background-size: 8px 5px;
|
||
padding-right: 30px;
|
||
appearance: none;
|
||
}
|
||
|
||
.upload-area {
|
||
background-color: #f9f9f9;
|
||
border: 2px dashed #ddd;
|
||
border-radius: 6px;
|
||
padding: 20px;
|
||
text-align: center;
|
||
transition: all 0.3s;
|
||
}
|
||
|
||
.upload-area:hover {
|
||
border-color: #3273dc;
|
||
}
|
||
|
||
.file-input-container {
|
||
display: inline-block;
|
||
}
|
||
|
||
.upload-label {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
cursor: pointer;
|
||
padding: 10px 20px;
|
||
background-color: #fff;
|
||
border: 1px solid #ddd;
|
||
border-radius: 4px;
|
||
transition: all 0.3s;
|
||
}
|
||
|
||
.upload-label:hover {
|
||
background-color: #f0f0f0;
|
||
}
|
||
|
||
.upload-icon {
|
||
font-size: 20px;
|
||
}
|
||
|
||
.file-input {
|
||
display: none;
|
||
}
|
||
|
||
.preview-area {
|
||
margin-top: 20px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 15px;
|
||
}
|
||
|
||
.image-preview {
|
||
max-width: 300px;
|
||
max-height: 200px;
|
||
overflow: hidden;
|
||
border: 1px solid #ddd;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.image-preview img {
|
||
max-width: 100%;
|
||
max-height: 100%;
|
||
object-fit: contain;
|
||
}
|
||
|
||
.image-info {
|
||
width: 100%;
|
||
max-width: 500px;
|
||
}
|
||
|
||
.tag-container {
|
||
background-color: #f0f0f0;
|
||
border: 1px solid #ddd;
|
||
border-radius: 4px;
|
||
padding: 10px;
|
||
margin-bottom: 10px;
|
||
font-family: monospace;
|
||
word-break: break-all;
|
||
font-size: 13px;
|
||
color: #333;
|
||
}
|
||
|
||
.file-list {
|
||
margin-top: 20px;
|
||
text-align: left;
|
||
}
|
||
|
||
.file-list h4 {
|
||
margin-top: 0;
|
||
font-size: 16px;
|
||
}
|
||
|
||
.file-list ul {
|
||
padding-left: 20px;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.file-list li {
|
||
margin-bottom: 5px;
|
||
}
|
||
|
||
.file-name {
|
||
font-weight: 500;
|
||
}
|
||
|
||
.file-size {
|
||
color: #666;
|
||
margin-left: 5px;
|
||
}
|
||
|
||
.total-size {
|
||
font-weight: 500;
|
||
color: #333;
|
||
}
|
||
|
||
.editor-container {
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
/* 确保编辑器高度适中 */
|
||
.editor-container :deep(.md-editor) {
|
||
height: 400px;
|
||
}
|
||
|
||
.draft-actions {
|
||
display: flex;
|
||
gap: 10px;
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
.form-actions {
|
||
padding: 30px;
|
||
text-align: center;
|
||
}
|
||
|
||
.btn {
|
||
padding: 10px 20px;
|
||
border: none;
|
||
border-radius: 4px;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
cursor: pointer;
|
||
transition: all 0.3s;
|
||
}
|
||
|
||
.btn-primary {
|
||
background-color: #3273dc;
|
||
color: white;
|
||
}
|
||
|
||
.btn-primary:hover {
|
||
background-color: #2366d1;
|
||
}
|
||
|
||
.btn-secondary {
|
||
background-color: #f0f0f0;
|
||
color: #333;
|
||
}
|
||
|
||
.btn-secondary:hover {
|
||
background-color: #e0e0e0;
|
||
}
|
||
|
||
.preview-btn {
|
||
background-color: #4a9eff;
|
||
color: white;
|
||
}
|
||
|
||
.preview-btn:hover {
|
||
background-color: #3789e9;
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.container {
|
||
padding: 20px 15px;
|
||
}
|
||
|
||
.form-section {
|
||
padding: 20px;
|
||
}
|
||
|
||
.preview-area {
|
||
flex-direction: column;
|
||
}
|
||
|
||
.image-info {
|
||
width: 100%;
|
||
}
|
||
}
|
||
|
||
/* 课程选择器样式 */
|
||
.course-selector-modal {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
z-index: 1000;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
}
|
||
|
||
.modal-overlay {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background-color: rgba(0, 0, 0, 0.5);
|
||
}
|
||
|
||
.modal-container {
|
||
width: 90%;
|
||
max-width: 900px;
|
||
max-height: 90vh;
|
||
background-color: white;
|
||
border-radius: 8px;
|
||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
||
z-index: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.modal-header {
|
||
padding: 16px;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
border-bottom: 1px solid #eee;
|
||
}
|
||
|
||
.modal-header h3 {
|
||
margin: 0;
|
||
font-size: 18px;
|
||
}
|
||
|
||
.close-btn {
|
||
background: none;
|
||
border: none;
|
||
font-size: 24px;
|
||
cursor: pointer;
|
||
color: #666;
|
||
}
|
||
|
||
.modal-body {
|
||
padding: 16px;
|
||
overflow-y: auto;
|
||
flex-grow: 1;
|
||
}
|
||
|
||
.search-bar {
|
||
display: flex;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.search-input {
|
||
flex-grow: 1;
|
||
border: 1px solid #ddd;
|
||
border-radius: 4px;
|
||
padding: 8px 12px;
|
||
font-size: 14px;
|
||
margin-right: 8px;
|
||
}
|
||
|
||
.search-btn {
|
||
background-color: #3273dc;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 4px;
|
||
padding: 8px 16px;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.course-loading {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
padding: 30px;
|
||
}
|
||
|
||
.spinner {
|
||
width: 30px;
|
||
height: 30px;
|
||
border: 3px solid rgba(0, 0, 0, 0.1);
|
||
border-radius: 50%;
|
||
border-top-color: #3273dc;
|
||
animation: spin 1s linear infinite;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
@keyframes spin {
|
||
to { transform: rotate(360deg); }
|
||
}
|
||
|
||
.search-error, .no-results {
|
||
text-align: center;
|
||
padding: 20px;
|
||
color: #e74c3c;
|
||
}
|
||
|
||
.no-results {
|
||
color: #666;
|
||
}
|
||
|
||
.course-table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
}
|
||
|
||
.course-table th, .course-table td {
|
||
padding: 12px;
|
||
text-align: left;
|
||
border-bottom: 1px solid #eee;
|
||
}
|
||
|
||
.course-table th {
|
||
background-color: #f8f9fa;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.course-table tr {
|
||
cursor: pointer;
|
||
transition: background-color 0.2s;
|
||
}
|
||
|
||
.course-table tr:hover {
|
||
background-color: #f5f7fa;
|
||
}
|
||
|
||
.course-table tr.selected {
|
||
background-color: #e8f0ff;
|
||
}
|
||
|
||
.rating {
|
||
font-weight: 600;
|
||
color: #ff9800;
|
||
}
|
||
|
||
.rating-count {
|
||
color: #888;
|
||
font-size: 0.9em;
|
||
margin-left: 4px;
|
||
}
|
||
|
||
.pagination {
|
||
display: flex;
|
||
justify-content: center;
|
||
margin-top: 20px;
|
||
gap: 5px;
|
||
align-items: center;
|
||
}
|
||
|
||
.page-btn {
|
||
min-width: 36px;
|
||
height: 36px;
|
||
padding: 0 8px;
|
||
border: 1px solid #ddd;
|
||
background-color: white;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.page-btn:hover:not(:disabled) {
|
||
background-color: #f5f7fa;
|
||
border-color: #ccc;
|
||
}
|
||
|
||
.page-btn.active {
|
||
background-color: #3273dc;
|
||
color: white;
|
||
border-color: #3273dc;
|
||
}
|
||
|
||
.page-btn:disabled {
|
||
cursor: not-allowed;
|
||
opacity: 0.6;
|
||
}
|
||
|
||
.modal-footer {
|
||
padding: 16px;
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
gap: 12px;
|
||
border-top: 1px solid #eee;
|
||
}
|
||
|
||
.page-info {
|
||
margin: 0 10px;
|
||
font-size: 14px;
|
||
color: #666;
|
||
}
|
||
</style>
|