2991692032 1 month ago
parent d90126b45c
commit 67283cf653

@ -1,5 +1,11 @@
import userApi from './user'; import userApi from './user';
import postApi from './post';
import resourceApi from './resource';
import scheduleApi from './schedule';
export { export {
userApi userApi,
postApi,
resourceApi,
scheduleApi
}; };

@ -0,0 +1,102 @@
import { get, post, put, del } from './request';
// 资源类型定义
export interface ResourceItem {
id: number;
title: string;
description?: string;
fileUrl: string;
fileSize: number;
fileType: string;
userId: number;
nickname: string;
avatar?: string;
categoryId: number;
categoryName: string;
downloadCount: number;
likeCount: number;
isLiked?: boolean;
createdAt: string;
updatedAt: string;
}
export interface ResourceCategory {
id: number;
name: string;
description?: string;
icon?: string;
}
export interface ResourceParams {
page?: number;
size?: number;
categoryId?: number;
keyword?: string;
userId?: number;
}
export interface UploadResourceParams {
title: string;
description?: string;
categoryId: number;
file: File;
}
export interface UpdateResourceParams {
title?: string;
description?: string;
categoryId?: number;
}
// 资源API
export default {
// 获取资源列表
getResources(params: ResourceParams = {}) {
return get<{ code: number; data: { total: number; list: ResourceItem[]; pages: number } }>('/resources', params);
},
// 获取资源详情
getResourceDetail(id: number) {
return get<{ code: number; data: ResourceItem }>(`/resources/${id}`);
},
// 上传资源
uploadResource(data: FormData) {
return post<{ code: number; data: { resourceId: number } }>('/resources', data);
},
// 更新资源信息
updateResource(id: number, data: UpdateResourceParams) {
return put<{ code: number; message: string }>(`/resources/${id}`, data);
},
// 删除资源
deleteResource(id: number) {
return del<{ code: number; message: string }>(`/resources/${id}`);
},
// 下载资源
downloadResource(id: number) {
return get<{ code: number; data: { fileUrl: string; fileName: string; fileType: string } }>(`/resources/${id}/download`);
},
// 点赞/取消点赞资源
likeResource(id: number) {
return post<{ code: number; message: string }>(`/resources/${id}/like`);
},
// 获取用户上传的资源列表
getUserResources(userId: number) {
return get<{ code: number; data: { total: number; list: ResourceItem[]; pages: number } }>(`/resources/user/${userId}`);
},
// 获取当前用户上传的资源列表
getMyResources(params: { page?: number; size?: number } = {}) {
return get<{ code: number; data: { total: number; list: ResourceItem[]; pages: number } }>('/resources/my', params);
},
// 获取资源分类
getResourceCategories() {
return get<{ code: number; data: { list: ResourceCategory[]; total: number } }>('/categories');
}
};

@ -0,0 +1,145 @@
import { get, post, put, del } from './request';
// 课程类型定义
export interface CourseItem {
id: number;
userId: number;
name: string;
teacher?: string;
location?: string;
dayOfWeek: number; // 1-7 表示周一到周日
startTime: string; // 格式: "08:00:00"
endTime: string; // 格式: "09:40:00"
startWeek: number; // 开始周次
endWeek: number; // 结束周次
color?: string; // 颜色,如 "#4CAF50"
status: number; // 状态0-删除, 1-正常)
createdAt: string;
updatedAt: string;
}
// 日程类型定义
export interface ScheduleItem {
id: number;
userId: number;
title: string;
description?: string;
startTime: string; // 格式: "2023-05-10T14:00:00"
endTime: string; // 格式: "2023-05-10T16:00:00"
location?: string;
isAllDay: number; // 是否全天0-否, 1-是)
reminder?: number; // 提醒时间(分钟)
color?: string; // 颜色,如 "#FF5722"
status: number; // 状态0-删除, 1-正常)
createdAt: string;
updatedAt: string;
}
export interface CreateCourseParams {
name: string;
teacher?: string;
location?: string;
dayOfWeek: number;
startTime: string;
endTime: string;
startWeek: number;
endWeek: number;
color?: string;
}
export interface UpdateCourseParams extends Partial<CreateCourseParams> {}
export interface CreateScheduleParams {
title: string;
description?: string;
startTime: string;
endTime: string;
location?: string;
isAllDay?: number;
reminder?: number;
color?: string;
}
export interface UpdateScheduleParams extends Partial<CreateScheduleParams> {}
export interface ConflictCheckResult {
hasConflict: boolean;
conflictCount: number;
}
// 课程表与日程管理API
export default {
// 课程相关API
// 创建课程
createCourse(data: CreateCourseParams) {
return post<{ code: number; data: { courseId: number } }>('/courses', data);
},
// 获取课程详情
getCourseDetail(id: number) {
return get<{ code: number; data: CourseItem }>(`/courses/${id}`);
},
// 获取用户的所有课程
getAllCourses() {
return get<{ code: number; data: { total: number; list: CourseItem[] } }>('/courses');
},
// 获取用户在指定星期几的课程
getCoursesByDay(dayOfWeek: number) {
return get<{ code: number; data: { total: number; list: CourseItem[] } }>(`/courses/day/${dayOfWeek}`);
},
// 更新课程
updateCourse(id: number, data: UpdateCourseParams) {
return put<{ code: number; message: string }>(`/courses/${id}`, data);
},
// 删除课程
deleteCourse(id: number) {
return del<{ code: number; message: string }>(`/courses/${id}`);
},
// 检查课程时间冲突
checkCourseConflict(params: { dayOfWeek: number; startTime: string; endTime: string; excludeCourseId?: number }) {
return get<{ code: number; data: ConflictCheckResult }>('/courses/check-conflict', params);
},
// 日程相关API
// 创建日程
createSchedule(data: CreateScheduleParams) {
return post<{ code: number; data: { scheduleId: number } }>('/schedules', data);
},
// 获取日程详情
getScheduleDetail(id: number) {
return get<{ code: number; data: ScheduleItem }>(`/schedules/${id}`);
},
// 获取用户的所有日程
getAllSchedules() {
return get<{ code: number; data: { total: number; list: ScheduleItem[] } }>('/schedules');
},
// 获取用户在指定时间范围内的日程
getSchedulesByRange(params: { startTime: string; endTime: string }) {
return get<{ code: number; data: { total: number; list: ScheduleItem[] } }>('/schedules/range', params);
},
// 更新日程
updateSchedule(id: number, data: UpdateScheduleParams) {
return put<{ code: number; message: string }>(`/schedules/${id}`, data);
},
// 删除日程
deleteSchedule(id: number) {
return del<{ code: number; message: string }>(`/schedules/${id}`);
},
// 检查日程时间冲突
checkScheduleConflict(params: { startTime: string; endTime: string; excludeScheduleId?: number }) {
return get<{ code: number; data: ConflictCheckResult }>('/schedules/check-conflict', params);
}
};

@ -54,15 +54,30 @@ const routes: Array<RouteRecordRaw> = [
{ {
path: 'resources', // URL: /resources path: 'resources', // URL: /resources
name: 'Resources', name: 'Resources',
component: () => import('../views/NotFound.vue'), // 占位符 component: () => import('../views/resource/ResourceListView.vue'),
meta: { title: '学习资源 - UniLife', requiresAuth: false } meta: { title: '学习资源 - UniLife', requiresAuth: false }
}, },
// 资源详情 - 无需登录
{
path: 'resource/:id', // URL: /resource/123
name: 'ResourceDetail',
component: () => import('../views/resource/ResourceDetailView.vue'),
props: true,
meta: { title: '资源详情 - UniLife', requiresAuth: false }
},
// 课程表 - 无需登录 // 课程表 - 无需登录
{ {
path: 'courses', // URL: /courses path: 'courses', // URL: /courses
name: 'Courses', name: 'Courses',
component: () => import('../views/NotFound.vue'), // 占位符 component: () => import('../views/schedule/CourseTableView.vue'),
meta: { title: '课程表 - UniLife', requiresAuth: false } meta: { title: '课程表 - UniLife', requiresAuth: false }
},
// 日程管理 - 需要登录
{
path: 'schedule', // URL: /schedule
name: 'Schedule',
component: () => import('../views/schedule/ScheduleView.vue'),
meta: { title: '日程管理 - UniLife', requiresAuth: true }
} }
] ]
}, },
@ -105,6 +120,12 @@ const routes: Array<RouteRecordRaw> = [
component: () => import('../views/forum/MyPostsView.vue'), component: () => import('../views/forum/MyPostsView.vue'),
meta: { title: '我的帖子 - UniLife' } meta: { title: '我的帖子 - UniLife' }
}, },
{
path: 'resources', // URL: /personal/resources
name: 'MyResources',
component: () => import('../views/resource/MyResourcesView.vue'),
meta: { title: '我的资源 - UniLife' }
},
{ {
path: 'messages', // URL: /personal/messages path: 'messages', // URL: /personal/messages
name: 'Messages', name: 'Messages',

@ -0,0 +1,507 @@
<template>
<div class="my-resources-container">
<div class="page-header">
<h1 class="page-title">我的资源</h1>
<el-button type="primary" @click="handleUpload">
<el-icon><Upload /></el-icon>
</el-button>
</div>
<el-tabs v-model="activeTab" @tab-click="handleTabChange">
<el-tab-pane label="我上传的资源" name="uploaded">
<el-table
v-loading="loading"
:data="resources"
style="width: 100%"
empty-text="暂无资源"
>
<el-table-column prop="title" label="资源标题" min-width="200">
<template #default="scope">
<div class="resource-title-cell">
<el-icon v-if="getFileIcon(scope.row.fileType)" :size="20">
<component :is="getFileIcon(scope.row.fileType)" />
</el-icon>
<router-link :to="`/resource/${scope.row.id}`" class="resource-link">
{{ scope.row.title }}
</router-link>
</div>
</template>
</el-table-column>
<el-table-column prop="categoryName" label="分类" width="120" />
<el-table-column prop="fileSize" label="大小" width="120">
<template #default="scope">
{{ formatFileSize(scope.row.fileSize) }}
</template>
</el-table-column>
<el-table-column prop="downloadCount" label="下载次数" width="100" />
<el-table-column prop="likeCount" label="点赞数" width="100" />
<el-table-column prop="createdAt" label="上传时间" width="180">
<template #default="scope">
{{ formatDate(scope.row.createdAt) }}
</template>
</el-table-column>
<el-table-column label="操作" width="180">
<template #default="scope">
<el-button size="small" @click="handleDownload(scope.row.id)"></el-button>
<el-button size="small" type="primary" @click="handleEdit(scope.row)"></el-button>
<el-button size="small" type="danger" @click="handleDelete(scope.row.id)"></el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination-container">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[10, 20, 30, 50]"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</el-tab-pane>
</el-tabs>
<!-- 上传资源对话框 -->
<el-dialog
v-model="uploadDialogVisible"
title="上传资源"
width="500px"
>
<el-form :model="uploadForm" label-width="80px" :rules="uploadRules" ref="uploadFormRef">
<el-form-item label="标题" prop="title">
<el-input v-model="uploadForm.title" placeholder="请输入资源标题"></el-input>
</el-form-item>
<el-form-item label="分类" prop="categoryId">
<el-select v-model="uploadForm.categoryId" placeholder="请选择分类">
<el-option
v-for="category in categories"
:key="category.id"
:label="category.name"
:value="category.id"
></el-option>
</el-select>
</el-form-item>
<el-form-item label="描述" prop="description">
<el-input
v-model="uploadForm.description"
type="textarea"
:rows="3"
placeholder="请输入资源描述"
></el-input>
</el-form-item>
<el-form-item label="文件" prop="file">
<el-upload
class="resource-upload"
:auto-upload="false"
:limit="1"
:on-change="handleFileChange"
:on-remove="handleFileRemove"
>
<el-button type="primary">选择文件</el-button>
<template #tip>
<div class="el-upload__tip">
文件大小不超过50MB
</div>
</template>
</el-upload>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="uploadDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitUpload" :loading="uploading">
上传
</el-button>
</span>
</template>
</el-dialog>
<!-- 编辑资源对话框 -->
<el-dialog
v-model="editDialogVisible"
title="编辑资源信息"
width="500px"
>
<el-form :model="editForm" label-width="80px" :rules="editRules" ref="editFormRef">
<el-form-item label="标题" prop="title">
<el-input v-model="editForm.title" placeholder="请输入资源标题"></el-input>
</el-form-item>
<el-form-item label="分类" prop="categoryId">
<el-select v-model="editForm.categoryId" placeholder="请选择分类">
<el-option
v-for="category in categories"
:key="category.id"
:label="category.name"
:value="category.id"
></el-option>
</el-select>
</el-form-item>
<el-form-item label="描述" prop="description">
<el-input
v-model="editForm.description"
type="textarea"
:rows="3"
placeholder="请输入资源描述"
></el-input>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="editDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitEdit" :loading="submitting">
保存
</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue';
import { useRouter } from 'vue-router';
import { ElMessage, ElMessageBox, FormInstance } from 'element-plus';
import { resourceApi } from '@/api';
import { Upload, Document, Picture, Files, Folder, Grid, Reading, Promotion } from '@element-plus/icons-vue';
const router = useRouter();
//
const resources = ref<any[]>([]);
const categories = ref<any[]>([]);
const currentPage = ref(1);
const pageSize = ref(10);
const total = ref(0);
const loading = ref(false);
const activeTab = ref('uploaded');
//
const uploadDialogVisible = ref(false);
const uploadFormRef = ref<FormInstance>();
const uploading = ref(false);
const uploadForm = reactive({
title: '',
description: '',
categoryId: undefined as number | undefined,
file: null as File | null
});
const uploadRules = {
title: [{ required: true, message: '请输入资源标题', trigger: 'blur' }],
categoryId: [{ required: true, message: '请选择分类', trigger: 'change' }],
file: [{ required: true, message: '请上传文件', trigger: 'change' }]
};
//
const editDialogVisible = ref(false);
const editFormRef = ref<FormInstance>();
const submitting = ref(false);
const currentEditId = ref<number | null>(null);
const editForm = reactive({
title: '',
description: '',
categoryId: undefined as number | undefined
});
const editRules = {
title: [{ required: true, message: '请输入资源标题', trigger: 'blur' }],
categoryId: [{ required: true, message: '请选择分类', trigger: 'change' }]
};
//
onMounted(async () => {
await Promise.all([
fetchMyResources(),
fetchCategories()
]);
});
//
const fetchMyResources = async () => {
loading.value = true;
try {
const params = {
page: currentPage.value,
size: pageSize.value
};
const res = await resourceApi.getMyResources(params);
if (res.code === 200) {
resources.value = res.data.list;
total.value = res.data.total;
}
} catch (error) {
console.error('获取我的资源列表失败:', error);
ElMessage.error('获取我的资源列表失败');
} finally {
loading.value = false;
}
};
//
const fetchCategories = async () => {
try {
const res = await resourceApi.getResourceCategories();
if (res.code === 200) {
categories.value = res.data.list;
}
} catch (error) {
console.error('获取分类列表失败:', error);
}
};
//
const handleTabChange = () => {
fetchMyResources();
};
//
const handleSizeChange = (val: number) => {
pageSize.value = val;
fetchMyResources();
};
//
const handleCurrentChange = (val: number) => {
currentPage.value = val;
fetchMyResources();
};
//
const handleUpload = () => {
uploadDialogVisible.value = true;
};
//
const handleFileChange = (file: any) => {
uploadForm.file = file.raw;
};
//
const handleFileRemove = () => {
uploadForm.file = null;
};
//
const submitUpload = async () => {
if (!uploadFormRef.value) return;
await uploadFormRef.value.validate(async (valid) => {
if (valid && uploadForm.file) {
uploading.value = true;
try {
const formData = new FormData();
formData.append('file', uploadForm.file);
formData.append('title', uploadForm.title);
formData.append('categoryId', String(uploadForm.categoryId));
formData.append('description', uploadForm.description || '');
const res = await resourceApi.uploadResource(formData);
if (res.code === 200) {
ElMessage.success('资源上传成功');
uploadDialogVisible.value = false;
resetUploadForm();
fetchMyResources();
}
} catch (error) {
console.error('上传资源失败:', error);
ElMessage.error('上传资源失败');
} finally {
uploading.value = false;
}
}
});
};
//
const resetUploadForm = () => {
uploadForm.title = '';
uploadForm.description = '';
uploadForm.categoryId = undefined;
uploadForm.file = null;
if (uploadFormRef.value) {
uploadFormRef.value.resetFields();
}
};
//
const handleEdit = (resource: any) => {
currentEditId.value = resource.id;
editForm.title = resource.title;
editForm.description = resource.description || '';
editForm.categoryId = resource.categoryId;
editDialogVisible.value = true;
};
//
const submitEdit = async () => {
if (!editFormRef.value || !currentEditId.value) return;
await editFormRef.value.validate(async (valid) => {
if (valid) {
submitting.value = true;
try {
const res = await resourceApi.updateResource(currentEditId.value, editForm);
if (res.code === 200) {
ElMessage.success('资源信息更新成功');
editDialogVisible.value = false;
fetchMyResources();
}
} catch (error) {
console.error('更新资源信息失败:', error);
ElMessage.error('更新资源信息失败');
} finally {
submitting.value = false;
}
}
});
};
//
const handleDownload = async (id: number) => {
try {
const res = await resourceApi.downloadResource(id);
if (res.code === 200) {
//
const link = document.createElement('a');
link.href = res.data.fileUrl;
link.download = res.data.fileName;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
//
fetchMyResources();
}
} catch (error) {
console.error('下载资源失败:', error);
ElMessage.error('下载资源失败');
}
};
//
const handleDelete = (id: number) => {
ElMessageBox.confirm(
'确定要删除该资源吗?此操作不可恢复',
'删除确认',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
).then(async () => {
try {
const res = await resourceApi.deleteResource(id);
if (res.code === 200) {
ElMessage.success('资源删除成功');
fetchMyResources();
}
} catch (error) {
console.error('删除资源失败:', error);
ElMessage.error('删除资源失败');
}
}).catch(() => {
//
});
};
//
const getFileIcon = (fileType: string) => {
if (fileType.includes('pdf')) {
return Document;
} else if (fileType.includes('image')) {
return Picture;
} else if (fileType.includes('word')) {
return Reading;
} else if (fileType.includes('excel')) {
return Grid;
} else if (fileType.includes('powerpoint')) {
return Promotion;
} else if (fileType.includes('zip') || fileType.includes('rar')) {
return Folder;
} else {
return Files;
}
};
//
const formatDate = (dateString: string) => {
const date = new Date(dateString);
return date.toLocaleDateString('zh-CN', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
};
//
const formatFileSize = (size: number) => {
if (size < 1024) {
return size + ' B';
} else if (size < 1024 * 1024) {
return (size / 1024).toFixed(2) + ' KB';
} else if (size < 1024 * 1024 * 1024) {
return (size / (1024 * 1024)).toFixed(2) + ' MB';
} else {
return (size / (1024 * 1024 * 1024)).toFixed(2) + ' GB';
}
};
</script>
<style scoped>
.my-resources-container {
padding: 20px;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.page-title {
font-size: 24px;
margin: 0;
}
.resource-title-cell {
display: flex;
align-items: center;
gap: 8px;
}
.resource-link {
color: var(--el-color-primary);
text-decoration: none;
}
.resource-link:hover {
text-decoration: underline;
}
.pagination-container {
margin-top: 20px;
display: flex;
justify-content: center;
}
.resource-upload {
width: 100%;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
}
</style>

@ -0,0 +1,487 @@
<template>
<div class="resource-detail-container">
<el-card class="resource-detail-card" v-loading="loading">
<template v-if="resource">
<div class="resource-header">
<div class="resource-title-section">
<h1 class="resource-title">{{ resource.title }}</h1>
<el-tag size="small" type="primary">{{ resource.categoryName }}</el-tag>
</div>
<div class="resource-actions">
<el-button type="primary" @click="handleDownload">
<el-icon><Download /></el-icon>
</el-button>
<el-button
:type="resource.isLiked ? 'danger' : 'default'"
@click="handleLike"
>
<el-icon><Star /></el-icon> {{ resource.isLiked ? '' : '' }} ({{ resource.likeCount }})
</el-button>
<el-dropdown v-if="isOwner" trigger="click">
<el-button>
<el-icon><More /></el-icon>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="handleEdit"></el-dropdown-item>
<el-dropdown-item @click="handleDelete" style="color: var(--el-color-danger)">删除</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
<el-divider />
<div class="resource-info">
<div class="resource-meta">
<div class="resource-uploader">
<el-avatar :size="40" :src="resource.avatar"></el-avatar>
<div class="uploader-info">
<div class="uploader-name">{{ resource.nickname }}</div>
<div class="upload-time">上传于 {{ formatDate(resource.createdAt) }}</div>
</div>
</div>
<div class="resource-stats">
<div class="stat-item">
<el-icon><View /></el-icon>
<span>下载次数: {{ resource.downloadCount }}</span>
</div>
<div class="stat-item">
<el-icon><Star /></el-icon>
<span>点赞数: {{ resource.likeCount }}</span>
</div>
<div class="stat-item">
<el-icon><Files /></el-icon>
<span>文件大小: {{ formatFileSize(resource.fileSize) }}</span>
</div>
<div class="stat-item">
<el-icon><Document /></el-icon>
<span>文件类型: {{ formatFileType(resource.fileType) }}</span>
</div>
</div>
</div>
<el-divider />
<div class="resource-description">
<h3>资源描述</h3>
<div class="description-content">
{{ resource.description || '暂无描述' }}
</div>
</div>
<el-divider />
<div class="resource-preview" v-if="canPreview">
<h3>预览</h3>
<div class="preview-container">
<img v-if="isImage" :src="resource.fileUrl" alt="资源预览" class="preview-image" />
<div v-else class="no-preview">
<el-icon size="48"><Document /></el-icon>
<p>该文件类型不支持在线预览</p>
</div>
</div>
</div>
</div>
</template>
<el-empty v-else-if="!loading" description="资源不存在或已被删除" />
</el-card>
<!-- 编辑资源对话框 -->
<el-dialog
v-model="editDialogVisible"
title="编辑资源信息"
width="500px"
>
<el-form :model="editForm" label-width="80px" :rules="editRules" ref="editFormRef">
<el-form-item label="标题" prop="title">
<el-input v-model="editForm.title" placeholder="请输入资源标题"></el-input>
</el-form-item>
<el-form-item label="分类" prop="categoryId">
<el-select v-model="editForm.categoryId" placeholder="请选择分类">
<el-option
v-for="category in categories"
:key="category.id"
:label="category.name"
:value="category.id"
></el-option>
</el-select>
</el-form-item>
<el-form-item label="描述" prop="description">
<el-input
v-model="editForm.description"
type="textarea"
:rows="3"
placeholder="请输入资源描述"
></el-input>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="editDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitEdit" :loading="submitting">
保存
</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, computed } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { ElMessage, ElMessageBox, FormInstance } from 'element-plus';
import { resourceApi } from '@/api';
import { useUserStore } from '@/stores';
import { Download, Star, More, View, Files, Document } from '@element-plus/icons-vue';
const route = useRoute();
const router = useRouter();
const userStore = useUserStore();
const resourceId = computed(() => Number(route.params.id));
//
const resource = ref<any>(null);
const loading = ref(true);
const categories = ref<any[]>([]);
//
const editDialogVisible = ref(false);
const editFormRef = ref<FormInstance>();
const submitting = ref(false);
const editForm = reactive({
title: '',
description: '',
categoryId: undefined as number | undefined
});
const editRules = {
title: [{ required: true, message: '请输入资源标题', trigger: 'blur' }],
categoryId: [{ required: true, message: '请选择分类', trigger: 'change' }]
};
//
const isOwner = computed(() => {
if (!resource.value || !userStore.user) return false;
return resource.value.userId === userStore.user.id;
});
const isImage = computed(() => {
if (!resource.value) return false;
return resource.value.fileType.includes('image');
});
const canPreview = computed(() => {
if (!resource.value) return false;
return isImage.value;
});
//
onMounted(async () => {
await Promise.all([
fetchResourceDetail(),
fetchCategories()
]);
});
//
const fetchResourceDetail = async () => {
loading.value = true;
try {
const res = await resourceApi.getResourceDetail(resourceId.value);
if (res.code === 200) {
resource.value = res.data;
}
} catch (error) {
console.error('获取资源详情失败:', error);
ElMessage.error('获取资源详情失败');
} finally {
loading.value = false;
}
};
//
const fetchCategories = async () => {
try {
const res = await resourceApi.getResourceCategories();
if (res.code === 200) {
categories.value = res.data.list;
}
} catch (error) {
console.error('获取分类列表失败:', error);
}
};
//
const handleDownload = async () => {
try {
const res = await resourceApi.downloadResource(resourceId.value);
if (res.code === 200) {
//
const link = document.createElement('a');
link.href = res.data.fileUrl;
link.download = res.data.fileName;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
//
fetchResourceDetail();
}
} catch (error) {
console.error('下载资源失败:', error);
ElMessage.error('下载资源失败');
}
};
//
const handleLike = async () => {
if (!userStore.isLoggedIn) {
ElMessage.warning('请先登录');
router.push('/login');
return;
}
try {
const res = await resourceApi.likeResource(resourceId.value);
if (res.code === 200) {
//
resource.value.isLiked = !resource.value.isLiked;
resource.value.likeCount += resource.value.isLiked ? 1 : -1;
ElMessage.success(resource.value.isLiked ? '点赞成功' : '取消点赞成功');
}
} catch (error) {
console.error('点赞操作失败:', error);
ElMessage.error('点赞操作失败');
}
};
//
const handleEdit = () => {
editForm.title = resource.value.title;
editForm.description = resource.value.description || '';
editForm.categoryId = resource.value.categoryId;
editDialogVisible.value = true;
};
//
const submitEdit = async () => {
if (!editFormRef.value) return;
await editFormRef.value.validate(async (valid) => {
if (valid) {
submitting.value = true;
try {
const res = await resourceApi.updateResource(resourceId.value, editForm);
if (res.code === 200) {
ElMessage.success('资源信息更新成功');
editDialogVisible.value = false;
fetchResourceDetail();
}
} catch (error) {
console.error('更新资源信息失败:', error);
ElMessage.error('更新资源信息失败');
} finally {
submitting.value = false;
}
}
});
};
//
const handleDelete = () => {
ElMessageBox.confirm(
'确定要删除该资源吗?此操作不可恢复',
'删除确认',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
).then(async () => {
try {
const res = await resourceApi.deleteResource(resourceId.value);
if (res.code === 200) {
ElMessage.success('资源删除成功');
router.push('/resources');
}
} catch (error) {
console.error('删除资源失败:', error);
ElMessage.error('删除资源失败');
}
}).catch(() => {
//
});
};
//
const formatDate = (dateString: string) => {
const date = new Date(dateString);
return date.toLocaleDateString('zh-CN', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
};
//
const formatFileSize = (size: number) => {
if (size < 1024) {
return size + ' B';
} else if (size < 1024 * 1024) {
return (size / 1024).toFixed(2) + ' KB';
} else if (size < 1024 * 1024 * 1024) {
return (size / (1024 * 1024)).toFixed(2) + ' MB';
} else {
return (size / (1024 * 1024 * 1024)).toFixed(2) + ' GB';
}
};
//
const formatFileType = (fileType: string) => {
const typeMap: Record<string, string> = {
'application/pdf': 'PDF文档',
'application/msword': 'Word文档',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'Word文档',
'application/vnd.ms-excel': 'Excel表格',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': 'Excel表格',
'application/vnd.ms-powerpoint': 'PowerPoint演示文稿',
'application/vnd.openxmlformats-officedocument.presentationml.presentation': 'PowerPoint演示文稿',
'application/zip': 'ZIP压缩文件',
'application/x-rar-compressed': 'RAR压缩文件',
'image/jpeg': 'JPEG图片',
'image/png': 'PNG图片',
'image/gif': 'GIF图片',
'text/plain': '文本文件'
};
return typeMap[fileType] || fileType;
};
</script>
<style scoped>
.resource-detail-container {
padding: 20px;
}
.resource-detail-card {
border-radius: 8px;
}
.resource-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 20px;
}
.resource-title-section {
display: flex;
flex-direction: column;
gap: 10px;
}
.resource-title {
font-size: 24px;
margin: 0;
}
.resource-actions {
display: flex;
gap: 10px;
}
.resource-meta {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
gap: 20px;
margin-bottom: 20px;
}
.resource-uploader {
display: flex;
align-items: center;
gap: 15px;
}
.uploader-info {
display: flex;
flex-direction: column;
}
.uploader-name {
font-weight: 500;
font-size: 16px;
}
.upload-time {
color: #999;
font-size: 14px;
margin-top: 5px;
}
.resource-stats {
display: flex;
flex-wrap: wrap;
gap: 20px;
}
.stat-item {
display: flex;
align-items: center;
gap: 5px;
color: #666;
}
.resource-description {
margin: 20px 0;
}
.description-content {
white-space: pre-line;
line-height: 1.6;
color: #333;
}
.resource-preview {
margin: 20px 0;
}
.preview-container {
margin-top: 15px;
border: 1px solid #eee;
border-radius: 8px;
padding: 20px;
display: flex;
justify-content: center;
align-items: center;
min-height: 300px;
}
.preview-image {
max-width: 100%;
max-height: 500px;
object-fit: contain;
}
.no-preview {
display: flex;
flex-direction: column;
align-items: center;
color: #999;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
}
</style>

@ -0,0 +1,499 @@
<template>
<div class="resource-list-container">
<div class="resource-header">
<h1 class="page-title">学习资源共享</h1>
<div class="resource-actions">
<el-button type="primary" @click="handleUpload" v-if="isLoggedIn">
<el-icon><Upload /></el-icon>
</el-button>
</div>
</div>
<div class="resource-filters">
<el-card shadow="never" class="filter-card">
<div class="filter-row">
<div class="filter-item">
<span class="filter-label">分类</span>
<el-radio-group v-model="filters.categoryId" @change="handleFilterChange">
<el-radio-button :label="null">全部</el-radio-button>
<el-radio-button v-for="category in categories" :key="category.id" :label="category.id">
{{ category.name }}
</el-radio-button>
</el-radio-group>
</div>
<div class="filter-item">
<el-input
v-model="filters.keyword"
placeholder="搜索资源"
class="search-input"
@keyup.enter="handleSearch"
clearable
@clear="handleSearch"
>
<template #suffix>
<el-icon class="search-icon" @click="handleSearch"><Search /></el-icon>
</template>
</el-input>
</div>
</div>
</el-card>
</div>
<div class="resource-content">
<el-empty v-if="resources.length === 0" description="暂无资源" />
<el-row :gutter="20" v-else>
<el-col :xs="24" :sm="12" :md="8" :lg="6" v-for="resource in resources" :key="resource.id" class="resource-col">
<el-card class="resource-card" shadow="hover" @click="viewResourceDetail(resource.id)">
<div class="resource-icon">
<el-icon v-if="resource.fileType.includes('pdf')" size="40"><Document /></el-icon>
<el-icon v-else-if="resource.fileType.includes('image')" size="40"><Picture /></el-icon>
<el-icon v-else-if="resource.fileType.includes('word')" size="40"><Reading /></el-icon>
<el-icon v-else-if="resource.fileType.includes('excel')" size="40"><Grid /></el-icon>
<el-icon v-else-if="resource.fileType.includes('powerpoint')" size="40"><Promotion /></el-icon>
<el-icon v-else-if="resource.fileType.includes('zip') || resource.fileType.includes('rar')" size="40"><Folder /></el-icon>
<el-icon v-else size="40"><Files /></el-icon>
</div>
<h3 class="resource-title">{{ resource.title }}</h3>
<p class="resource-description" v-if="resource.description">{{ resource.description }}</p>
<div class="resource-meta">
<span class="resource-category">{{ resource.categoryName }}</span>
<span class="resource-size">{{ formatFileSize(resource.fileSize) }}</span>
</div>
<div class="resource-footer">
<div class="resource-uploader">
<el-avatar :size="24" :src="resource.avatar"></el-avatar>
<span>{{ resource.nickname }}</span>
</div>
<div class="resource-stats">
<span class="resource-downloads">
<el-icon><Download /></el-icon> {{ resource.downloadCount }}
</span>
<span class="resource-likes">
<el-icon><Star /></el-icon> {{ resource.likeCount }}
</span>
</div>
</div>
</el-card>
</el-col>
</el-row>
<div class="pagination-container">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[12, 24, 36, 48]"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</div>
<!-- 上传资源对话框 -->
<el-dialog
v-model="uploadDialogVisible"
title="上传资源"
width="500px"
>
<el-form :model="uploadForm" label-width="80px" :rules="uploadRules" ref="uploadFormRef">
<el-form-item label="标题" prop="title">
<el-input v-model="uploadForm.title" placeholder="请输入资源标题"></el-input>
</el-form-item>
<el-form-item label="分类" prop="categoryId">
<el-select v-model="uploadForm.categoryId" placeholder="请选择分类">
<el-option
v-for="category in categories"
:key="category.id"
:label="category.name"
:value="category.id"
></el-option>
</el-select>
</el-form-item>
<el-form-item label="描述" prop="description">
<el-input
v-model="uploadForm.description"
type="textarea"
:rows="3"
placeholder="请输入资源描述"
></el-input>
</el-form-item>
<el-form-item label="文件" prop="file">
<el-upload
class="resource-upload"
:auto-upload="false"
:limit="1"
:on-change="handleFileChange"
:on-remove="handleFileRemove"
>
<el-button type="primary">选择文件</el-button>
<template #tip>
<div class="el-upload__tip">
文件大小不超过50MB
</div>
</template>
</el-upload>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="uploadDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitUpload" :loading="uploading">
上传
</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, computed } from 'vue';
import { useRouter } from 'vue-router';
import { ElMessage, ElMessageBox, FormInstance } from 'element-plus';
import { resourceApi } from '@/api';
import { useUserStore } from '@/stores';
import { Document, Picture, Reading, Grid, Promotion, Folder, Files, Upload, Download, Star, Search } from '@element-plus/icons-vue';
const router = useRouter();
const userStore = useUserStore();
const isLoggedIn = computed(() => userStore.isLoggedIn);
//
const resources = ref<any[]>([]);
const categories = ref<any[]>([]);
const currentPage = ref(1);
const pageSize = ref(12);
const total = ref(0);
const loading = ref(false);
//
const filters = reactive({
categoryId: null as number | null,
keyword: ''
});
//
const uploadDialogVisible = ref(false);
const uploadFormRef = ref<FormInstance>();
const uploading = ref(false);
const uploadForm = reactive({
title: '',
description: '',
categoryId: undefined as number | undefined,
file: null as File | null
});
const uploadRules = {
title: [{ required: true, message: '请输入资源标题', trigger: 'blur' }],
categoryId: [{ required: true, message: '请选择分类', trigger: 'change' }],
file: [{ required: true, message: '请上传文件', trigger: 'change' }]
};
//
onMounted(async () => {
await Promise.all([
fetchResources(),
fetchCategories()
]);
});
//
const fetchResources = async () => {
loading.value = true;
try {
const params = {
page: currentPage.value,
size: pageSize.value,
...filters
};
const res = await resourceApi.getResources(params);
if (res.code === 200) {
resources.value = res.data.list;
total.value = res.data.total;
}
} catch (error) {
console.error('获取资源列表失败:', error);
ElMessage.error('获取资源列表失败');
} finally {
loading.value = false;
}
};
//
const fetchCategories = async () => {
try {
const res = await resourceApi.getResourceCategories();
if (res.code === 200) {
categories.value = res.data.list;
}
} catch (error) {
console.error('获取分类列表失败:', error);
}
};
//
const handleFilterChange = () => {
currentPage.value = 1;
fetchResources();
};
//
const handleSearch = () => {
currentPage.value = 1;
fetchResources();
};
//
const handleSizeChange = (val: number) => {
pageSize.value = val;
fetchResources();
};
//
const handleCurrentChange = (val: number) => {
currentPage.value = val;
fetchResources();
};
//
const viewResourceDetail = (id: number) => {
router.push(`/resource/${id}`);
};
//
const handleUpload = () => {
if (!isLoggedIn.value) {
ElMessage.warning('请先登录');
router.push('/login');
return;
}
uploadDialogVisible.value = true;
};
//
const handleFileChange = (file: any) => {
uploadForm.file = file.raw;
};
//
const handleFileRemove = () => {
uploadForm.file = null;
};
//
const submitUpload = async () => {
if (!uploadFormRef.value) return;
await uploadFormRef.value.validate(async (valid) => {
if (valid && uploadForm.file) {
uploading.value = true;
try {
const formData = new FormData();
formData.append('file', uploadForm.file);
formData.append('title', uploadForm.title);
formData.append('categoryId', String(uploadForm.categoryId));
formData.append('description', uploadForm.description || '');
const res = await resourceApi.uploadResource(formData);
if (res.code === 200) {
ElMessage.success('资源上传成功');
uploadDialogVisible.value = false;
resetUploadForm();
fetchResources();
}
} catch (error) {
console.error('上传资源失败:', error);
ElMessage.error('上传资源失败');
} finally {
uploading.value = false;
}
}
});
};
//
const resetUploadForm = () => {
uploadForm.title = '';
uploadForm.description = '';
uploadForm.categoryId = undefined;
uploadForm.file = null;
if (uploadFormRef.value) {
uploadFormRef.value.resetFields();
}
};
//
const formatFileSize = (size: number) => {
if (size < 1024) {
return size + ' B';
} else if (size < 1024 * 1024) {
return (size / 1024).toFixed(2) + ' KB';
} else if (size < 1024 * 1024 * 1024) {
return (size / (1024 * 1024)).toFixed(2) + ' MB';
} else {
return (size / (1024 * 1024 * 1024)).toFixed(2) + ' GB';
}
};
</script>
<style scoped>
.resource-list-container {
padding: 20px;
}
.resource-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.page-title {
font-size: 24px;
margin: 0;
}
.resource-filters {
margin-bottom: 20px;
}
.filter-card {
border-radius: 8px;
}
.filter-row {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
gap: 16px;
}
.filter-item {
display: flex;
align-items: center;
}
.filter-label {
margin-right: 10px;
font-weight: 500;
}
.search-input {
width: 250px;
}
.search-icon {
cursor: pointer;
}
.resource-content {
margin-top: 20px;
}
.resource-col {
margin-bottom: 20px;
}
.resource-card {
height: 100%;
cursor: pointer;
transition: transform 0.3s;
}
.resource-card:hover {
transform: translateY(-5px);
}
.resource-icon {
display: flex;
justify-content: center;
margin-bottom: 15px;
color: var(--el-color-primary);
}
.resource-title {
font-size: 16px;
margin: 10px 0;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.resource-description {
color: #666;
font-size: 14px;
margin: 10px 0;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.resource-meta {
display: flex;
justify-content: space-between;
margin: 10px 0;
font-size: 13px;
}
.resource-category {
background-color: var(--el-color-primary-light-9);
color: var(--el-color-primary);
padding: 2px 8px;
border-radius: 4px;
}
.resource-size {
color: #999;
}
.resource-footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 15px;
font-size: 13px;
}
.resource-uploader {
display: flex;
align-items: center;
gap: 5px;
}
.resource-stats {
display: flex;
gap: 10px;
}
.resource-downloads, .resource-likes {
display: flex;
align-items: center;
gap: 4px;
color: #666;
}
.pagination-container {
margin-top: 30px;
display: flex;
justify-content: center;
}
.resource-upload {
width: 100%;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
}
</style>

@ -0,0 +1,753 @@
<template>
<div class="course-table-container">
<div class="course-header">
<h1 class="page-title">课程表</h1>
<div class="course-actions">
<el-button type="primary" @click="handleAddCourse" v-if="isLoggedIn">
<el-icon><Plus /></el-icon>
</el-button>
</div>
</div>
<div class="course-filters">
<el-card shadow="never" class="filter-card">
<div class="filter-row">
<div class="semester-selector">
<span class="filter-label">学期</span>
<el-select v-model="currentSemester" placeholder="选择学期">
<el-option label="2023-2024学年第一学期" value="2023-1"></el-option>
<el-option label="2023-2024学年第二学期" value="2023-2"></el-option>
</el-select>
</div>
<div class="week-selector">
<span class="filter-label">周次</span>
<el-select v-model="currentWeek" placeholder="选择周次">
<el-option v-for="week in 20" :key="week" :label="`第${week}周`" :value="week"></el-option>
</el-select>
</div>
<div class="view-selector">
<el-radio-group v-model="viewMode" size="small">
<el-radio-button label="week">周视图</el-radio-button>
<el-radio-button label="day">日视图</el-radio-button>
</el-radio-group>
</div>
</div>
</el-card>
</div>
<!-- 周视图 -->
<div v-if="viewMode === 'week'" class="week-view">
<el-card shadow="hover" class="timetable-card">
<div class="timetable">
<!-- 时间列 -->
<div class="time-column">
<div class="header-cell"></div>
<div class="time-cell" v-for="time in timeSlots" :key="time.id">
{{ time.label }}
</div>
</div>
<!-- 星期列 -->
<div v-for="day in 7" :key="day" class="day-column">
<div class="header-cell">{{ getDayLabel(day) }}</div>
<div
class="course-cell"
v-for="time in timeSlots"
:key="time.id"
@click="handleCellClick(day, time.id)"
>
<div
v-for="course in getCoursesForTimeSlot(day, time.id)"
:key="course.id"
class="course-item"
:style="{ backgroundColor: course.color || '#409EFF' }"
@click.stop="handleCourseClick(course)"
>
<div class="course-name">{{ course.name }}</div>
<div class="course-info">{{ course.location }}</div>
<div class="course-info">{{ course.teacher }}</div>
</div>
</div>
</div>
</div>
</el-card>
</div>
<!-- 日视图 -->
<div v-else class="day-view">
<el-card shadow="hover" class="day-card">
<div class="day-header">
<el-button-group>
<el-button @click="previousDay">
<el-icon><ArrowLeft /></el-icon>
</el-button>
<el-button>{{ getDayLabel(currentDay) }}</el-button>
<el-button @click="nextDay">
<el-icon><ArrowRight /></el-icon>
</el-button>
</el-button-group>
</div>
<div class="day-timetable">
<div class="time-column">
<div class="time-cell" v-for="time in timeSlots" :key="time.id">
{{ time.label }}
</div>
</div>
<div class="day-courses-column">
<div
class="course-cell"
v-for="time in timeSlots"
:key="time.id"
@click="handleCellClick(currentDay, time.id)"
>
<div
v-for="course in getCoursesForTimeSlot(currentDay, time.id)"
:key="course.id"
class="course-item day-course-item"
:style="{ backgroundColor: course.color || '#409EFF' }"
@click.stop="handleCourseClick(course)"
>
<div class="course-name">{{ course.name }}</div>
<div class="course-info">{{ course.location }}</div>
<div class="course-info">{{ course.teacher }}</div>
</div>
</div>
</div>
</div>
</el-card>
</div>
<!-- 添加/编辑课程对话框 -->
<el-dialog
v-model="courseDialogVisible"
:title="isEditing ? '编辑课程' : '添加课程'"
width="500px"
>
<el-form :model="courseForm" label-width="80px" :rules="courseRules" ref="courseFormRef">
<el-form-item label="课程名称" prop="name">
<el-input v-model="courseForm.name" placeholder="请输入课程名称"></el-input>
</el-form-item>
<el-form-item label="教师" prop="teacher">
<el-input v-model="courseForm.teacher" placeholder="请输入教师姓名"></el-input>
</el-form-item>
<el-form-item label="地点" prop="location">
<el-input v-model="courseForm.location" placeholder="请输入上课地点"></el-input>
</el-form-item>
<el-form-item label="星期" prop="dayOfWeek">
<el-select v-model="courseForm.dayOfWeek" placeholder="请选择星期">
<el-option v-for="day in 7" :key="day" :label="getDayLabel(day)" :value="day"></el-option>
</el-select>
</el-form-item>
<el-form-item label="开始时间" prop="startTime">
<el-time-select
v-model="courseForm.startTime"
placeholder="请选择开始时间"
start="08:00"
step="00:30"
end="22:00"
></el-time-select>
</el-form-item>
<el-form-item label="结束时间" prop="endTime">
<el-time-select
v-model="courseForm.endTime"
placeholder="请选择结束时间"
start="08:00"
step="00:30"
end="22:00"
:min-time="courseForm.startTime"
></el-time-select>
</el-form-item>
<el-form-item label="开始周次" prop="startWeek">
<el-input-number v-model="courseForm.startWeek" :min="1" :max="20"></el-input-number>
</el-form-item>
<el-form-item label="结束周次" prop="endWeek">
<el-input-number v-model="courseForm.endWeek" :min="courseForm.startWeek" :max="20"></el-input-number>
</el-form-item>
<el-form-item label="颜色" prop="color">
<el-color-picker v-model="courseForm.color"></el-color-picker>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="courseDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitCourse" :loading="submitting">
保存
</el-button>
</span>
</template>
</el-dialog>
<!-- 课程详情对话框 -->
<el-dialog
v-model="courseDetailVisible"
title="课程详情"
width="400px"
>
<div v-if="selectedCourse" class="course-detail">
<h3 class="detail-title">{{ selectedCourse.name }}</h3>
<div class="detail-item">
<span class="detail-label">教师</span>
<span>{{ selectedCourse.teacher || '未设置' }}</span>
</div>
<div class="detail-item">
<span class="detail-label">地点</span>
<span>{{ selectedCourse.location || '未设置' }}</span>
</div>
<div class="detail-item">
<span class="detail-label">时间</span>
<span>{{ getDayLabel(selectedCourse.dayOfWeek) }} {{ selectedCourse.startTime }} - {{ selectedCourse.endTime }}</span>
</div>
<div class="detail-item">
<span class="detail-label">周次</span>
<span>{{ selectedCourse.startWeek }} - {{ selectedCourse.endWeek }}</span>
</div>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="courseDetailVisible = false">关闭</el-button>
<el-button type="primary" @click="handleEditCourse" v-if="isOwner"></el-button>
<el-button type="danger" @click="handleDeleteCourse" v-if="isOwner"></el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue';
import { ElMessage, ElMessageBox, FormInstance } from 'element-plus';
import { scheduleApi } from '@/api';
import { useUserStore } from '@/stores';
import { Plus, ArrowLeft, ArrowRight } from '@element-plus/icons-vue';
const userStore = useUserStore();
const isLoggedIn = computed(() => userStore.isLoggedIn);
//
const courses = ref<any[]>([]);
const loading = ref(false);
const currentSemester = ref('2023-1');
const currentWeek = ref(1);
const viewMode = ref('week');
const currentDay = ref(1); // 1-7
//
const timeSlots = [
{ id: 1, label: '第1节 8:00-8:45' },
{ id: 2, label: '第2节 8:55-9:40' },
{ id: 3, label: '第3节 10:00-10:45' },
{ id: 4, label: '第4节 10:55-11:40' },
{ id: 5, label: '第5节 13:30-14:15' },
{ id: 6, label: '第6节 14:25-15:10' },
{ id: 7, label: '第7节 15:30-16:15' },
{ id: 8, label: '第8节 16:25-17:10' },
{ id: 9, label: '第9节 18:30-19:15' },
{ id: 10, label: '第10节 19:25-20:10' },
{ id: 11, label: '第11节 20:20-21:05' },
{ id: 12, label: '第12节 21:15-22:00' }
];
//
const courseDialogVisible = ref(false);
const courseFormRef = ref<FormInstance>();
const submitting = ref(false);
const isEditing = ref(false);
const courseForm = reactive({
id: undefined as number | undefined,
name: '',
teacher: '',
location: '',
dayOfWeek: 1,
startTime: '',
endTime: '',
startWeek: 1,
endWeek: 16,
color: '#409EFF'
});
const courseRules = {
name: [{ required: true, message: '请输入课程名称', trigger: 'blur' }],
dayOfWeek: [{ required: true, message: '请选择星期', trigger: 'change' }],
startTime: [{ required: true, message: '请选择开始时间', trigger: 'change' }],
endTime: [{ required: true, message: '请选择结束时间', trigger: 'change' }],
startWeek: [{ required: true, message: '请输入开始周次', trigger: 'blur' }],
endWeek: [{ required: true, message: '请输入结束周次', trigger: 'blur' }]
};
//
const courseDetailVisible = ref(false);
const selectedCourse = ref<any>(null);
const isOwner = computed(() => {
if (!selectedCourse.value || !userStore.user) return false;
return selectedCourse.value.userId === userStore.user.id;
});
//
onMounted(async () => {
await fetchCourses();
});
//
const fetchCourses = async () => {
loading.value = true;
try {
const res = await scheduleApi.getAllCourses();
if (res.code === 200) {
courses.value = res.data.list;
}
} catch (error) {
console.error('获取课程列表失败:', error);
ElMessage.error('获取课程列表失败');
} finally {
loading.value = false;
}
};
//
const getDayLabel = (day: number) => {
const days = ['', '周一', '周二', '周三', '周四', '周五', '周六', '周日'];
return days[day] || '';
};
//
const getCoursesForTimeSlot = (day: number, timeSlotId: number) => {
return courses.value.filter(course => {
//
if (course.dayOfWeek !== day) return false;
//
if (currentWeek.value < course.startWeek || currentWeek.value > course.endWeek) return false;
//
const courseStartHour = parseInt(course.startTime.split(':')[0]);
const courseStartMinute = parseInt(course.startTime.split(':')[1]);
const courseEndHour = parseInt(course.endTime.split(':')[0]);
const courseEndMinute = parseInt(course.endTime.split(':')[1]);
const slotStartHour = parseInt(timeSlots[timeSlotId - 1].label.split(' ')[1].split('-')[0].split(':')[0]);
const slotStartMinute = parseInt(timeSlots[timeSlotId - 1].label.split(' ')[1].split('-')[0].split(':')[1]);
const slotEndHour = parseInt(timeSlots[timeSlotId - 1].label.split(' ')[1].split('-')[1].split(':')[0]);
const slotEndMinute = parseInt(timeSlots[timeSlotId - 1].label.split(' ')[1].split('-')[1].split(':')[1]);
const courseStartTime = courseStartHour * 60 + courseStartMinute;
const courseEndTime = courseEndHour * 60 + courseEndMinute;
const slotStartTime = slotStartHour * 60 + slotStartMinute;
const slotEndTime = slotEndHour * 60 + slotEndMinute;
// true
return (courseStartTime <= slotEndTime && courseEndTime >= slotStartTime);
});
};
//
const handleCellClick = (day: number, timeSlotId: number) => {
if (!isLoggedIn.value) {
ElMessage.warning('请先登录');
return;
}
//
isEditing.value = false;
courseForm.id = undefined;
courseForm.name = '';
courseForm.teacher = '';
courseForm.location = '';
courseForm.dayOfWeek = day;
//
const timeSlot = timeSlots[timeSlotId - 1];
const timeRange = timeSlot.label.split(' ')[1].split('-');
courseForm.startTime = timeRange[0];
courseForm.endTime = timeRange[1];
courseForm.startWeek = currentWeek.value;
courseForm.endWeek = currentWeek.value + 15 > 20 ? 20 : currentWeek.value + 15;
courseForm.color = getRandomColor();
courseDialogVisible.value = true;
};
//
const handleCourseClick = (course: any) => {
selectedCourse.value = course;
courseDetailVisible.value = true;
};
//
const handleAddCourse = () => {
if (!isLoggedIn.value) {
ElMessage.warning('请先登录');
return;
}
isEditing.value = false;
courseForm.id = undefined;
courseForm.name = '';
courseForm.teacher = '';
courseForm.location = '';
courseForm.dayOfWeek = 1;
courseForm.startTime = '08:00';
courseForm.endTime = '09:40';
courseForm.startWeek = 1;
courseForm.endWeek = 16;
courseForm.color = getRandomColor();
courseDialogVisible.value = true;
};
//
const handleEditCourse = () => {
if (!selectedCourse.value) return;
isEditing.value = true;
courseForm.id = selectedCourse.value.id;
courseForm.name = selectedCourse.value.name;
courseForm.teacher = selectedCourse.value.teacher || '';
courseForm.location = selectedCourse.value.location || '';
courseForm.dayOfWeek = selectedCourse.value.dayOfWeek;
courseForm.startTime = selectedCourse.value.startTime;
courseForm.endTime = selectedCourse.value.endTime;
courseForm.startWeek = selectedCourse.value.startWeek;
courseForm.endWeek = selectedCourse.value.endWeek;
courseForm.color = selectedCourse.value.color || '#409EFF';
courseDetailVisible.value = false;
courseDialogVisible.value = true;
};
//
const handleDeleteCourse = () => {
if (!selectedCourse.value) return;
ElMessageBox.confirm(
'确定要删除该课程吗?此操作不可恢复',
'删除确认',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
).then(async () => {
try {
const res = await scheduleApi.deleteCourse(selectedCourse.value.id);
if (res.code === 200) {
ElMessage.success('课程删除成功');
courseDetailVisible.value = false;
fetchCourses();
}
} catch (error) {
console.error('删除课程失败:', error);
ElMessage.error('删除课程失败');
}
}).catch(() => {
//
});
};
//
const submitCourse = async () => {
if (!courseFormRef.value) return;
await courseFormRef.value.validate(async (valid) => {
if (valid) {
submitting.value = true;
try {
//
const conflictParams = {
dayOfWeek: courseForm.dayOfWeek,
startTime: courseForm.startTime,
endTime: courseForm.endTime,
excludeCourseId: courseForm.id
};
const conflictRes = await scheduleApi.checkCourseConflict(conflictParams);
if (conflictRes.code === 200 && conflictRes.data.hasConflict) {
ElMessageBox.confirm(
`检测到时间冲突,有${conflictRes.data.conflictCount}门课程与当前时间段冲突,是否继续?`,
'时间冲突',
{
confirmButtonText: '继续',
cancelButtonText: '取消',
type: 'warning'
}
).then(() => {
saveCourse();
}).catch(() => {
submitting.value = false;
});
} else {
saveCourse();
}
} catch (error) {
console.error('检查时间冲突失败:', error);
ElMessage.error('检查时间冲突失败');
submitting.value = false;
}
}
});
};
//
const saveCourse = async () => {
try {
let res;
if (isEditing.value && courseForm.id) {
//
res = await scheduleApi.updateCourse(courseForm.id, {
name: courseForm.name,
teacher: courseForm.teacher,
location: courseForm.location,
dayOfWeek: courseForm.dayOfWeek,
startTime: courseForm.startTime,
endTime: courseForm.endTime,
startWeek: courseForm.startWeek,
endWeek: courseForm.endWeek,
color: courseForm.color
});
} else {
//
res = await scheduleApi.createCourse({
name: courseForm.name,
teacher: courseForm.teacher,
location: courseForm.location,
dayOfWeek: courseForm.dayOfWeek,
startTime: courseForm.startTime,
endTime: courseForm.endTime,
startWeek: courseForm.startWeek,
endWeek: courseForm.endWeek,
color: courseForm.color
});
}
if (res.code === 200) {
ElMessage.success(isEditing.value ? '课程更新成功' : '课程添加成功');
courseDialogVisible.value = false;
fetchCourses();
}
} catch (error) {
console.error(isEditing.value ? '更新课程失败:' : '添加课程失败:', error);
ElMessage.error(isEditing.value ? '更新课程失败' : '添加课程失败');
} finally {
submitting.value = false;
}
};
//
const previousDay = () => {
currentDay.value = currentDay.value === 1 ? 7 : currentDay.value - 1;
};
//
const nextDay = () => {
currentDay.value = currentDay.value === 7 ? 1 : currentDay.value + 1;
};
//
const getRandomColor = () => {
const colors = [
'#409EFF', //
'#67C23A', // 绿
'#E6A23C', //
'#F56C6C', //
'#909399', //
'#9966CC', //
'#FF9900', //
'#19CAAD', //
'#8CC7B5', // 绿
'#A0EEE1', //
'#BEE7E9', //
'#BEEDC7', // 绿
'#D6D5B7', //
'#D1BA74', //
'#E6CEAC', //
'#ECAD9E' //
];
return colors[Math.floor(Math.random() * colors.length)];
};
</script>
<style scoped>
.course-table-container {
padding: 20px;
}
.course-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.page-title {
font-size: 24px;
margin: 0;
}
.course-filters {
margin-bottom: 20px;
}
.filter-card {
border-radius: 8px;
}
.filter-row {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
gap: 16px;
}
.filter-label {
margin-right: 10px;
font-weight: 500;
}
/* 周视图样式 */
.week-view {
margin-top: 20px;
}
.timetable-card {
border-radius: 8px;
}
.timetable {
display: flex;
min-height: 800px;
}
.time-column, .day-column {
flex: 1;
display: flex;
flex-direction: column;
border-right: 1px solid #eee;
}
.time-column {
flex: 0 0 120px;
}
.header-cell {
height: 50px;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
background-color: #f5f7fa;
border-bottom: 1px solid #eee;
}
.time-cell {
height: 60px;
display: flex;
align-items: center;
justify-content: center;
border-bottom: 1px solid #eee;
font-size: 12px;
}
.course-cell {
height: 60px;
border-bottom: 1px solid #eee;
position: relative;
cursor: pointer;
}
.course-item {
position: absolute;
top: 2px;
left: 2px;
right: 2px;
bottom: 2px;
border-radius: 4px;
padding: 4px;
color: white;
font-size: 12px;
overflow: hidden;
cursor: pointer;
transition: all 0.3s;
}
.course-item:hover {
transform: scale(1.02);
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.course-name {
font-weight: bold;
margin-bottom: 2px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.course-info {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-size: 10px;
}
/* 日视图样式 */
.day-view {
margin-top: 20px;
}
.day-card {
border-radius: 8px;
}
.day-header {
display: flex;
justify-content: center;
margin-bottom: 20px;
}
.day-timetable {
display: flex;
min-height: 800px;
}
.day-courses-column {
flex: 1;
display: flex;
flex-direction: column;
}
.day-course-item {
padding: 8px;
}
/* 课程详情样式 */
.course-detail {
padding: 10px;
}
.detail-title {
font-size: 18px;
margin-bottom: 15px;
color: #303133;
}
.detail-item {
margin-bottom: 10px;
display: flex;
}
.detail-label {
font-weight: 500;
width: 60px;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
}
</style>

@ -0,0 +1,839 @@
<template>
<div class="schedule-container">
<div class="schedule-header">
<h1 class="page-title">日程管理</h1>
<div class="schedule-actions">
<el-button type="primary" @click="handleAddSchedule" v-if="isLoggedIn">
<el-icon><Plus /></el-icon>
</el-button>
</div>
</div>
<div class="schedule-content">
<el-row :gutter="20">
<!-- 日历视图 -->
<el-col :span="16">
<el-card shadow="hover" class="calendar-card">
<div class="calendar-header">
<div class="month-selector">
<el-button-group>
<el-button @click="prevMonth">
<el-icon><ArrowLeft /></el-icon>
</el-button>
<el-button>{{ currentYearMonth }}</el-button>
<el-button @click="nextMonth">
<el-icon><ArrowRight /></el-icon>
</el-button>
</el-button-group>
</div>
<div class="view-selector">
<el-radio-group v-model="calendarView" size="small">
<el-radio-button label="month">月视图</el-radio-button>
<el-radio-button label="week">周视图</el-radio-button>
<el-radio-button label="day">日视图</el-radio-button>
</el-radio-group>
</div>
</div>
<div class="calendar-body">
<!-- 月视图 -->
<div v-if="calendarView === 'month'" class="month-view">
<!-- 星期标题 -->
<div class="week-header">
<div v-for="day in weekDays" :key="day" class="week-day">{{ day }}</div>
</div>
<!-- 日期网格 -->
<div class="month-grid">
<div
v-for="(date, index) in calendarDays"
:key="index"
class="date-cell"
:class="{
'other-month': date.otherMonth,
'today': isToday(date.date),
'has-events': hasEvents(date.date)
}"
@click="selectDate(date.date)"
>
<div class="date-number">{{ date.day }}</div>
<div class="date-events">
<div
v-for="event in getEventsForDate(date.date)"
:key="event.id"
class="event-dot"
:style="{ backgroundColor: event.color || '#409EFF' }"
:title="event.title"
></div>
</div>
</div>
</div>
</div>
<!-- 周视图 -->
<div v-else-if="calendarView === 'week'" class="week-view">
<!-- 星期标题 -->
<div class="week-header">
<div class="time-column-header"></div>
<div
v-for="(date, index) in weekDates"
:key="index"
class="week-day-header"
:class="{ 'today': isToday(date) }"
>
<div>{{ weekDays[index] }}</div>
<div>{{ formatDate(date, 'MM-DD') }}</div>
</div>
</div>
<!-- 时间网格 -->
<div class="week-grid">
<div class="time-column">
<div
v-for="hour in 24"
:key="hour"
class="time-cell"
>
{{ formatHour(hour - 1) }}
</div>
</div>
<div
v-for="(date, dayIndex) in weekDates"
:key="dayIndex"
class="day-column"
>
<div
v-for="hour in 24"
:key="hour"
class="hour-cell"
@click="handleAddScheduleAt(date, hour - 1)"
>
<div
v-for="event in getEventsForDateAndHour(date, hour - 1)"
:key="event.id"
class="event-item"
:style="{
backgroundColor: event.color || '#409EFF',
top: calculateEventTop(event, hour - 1) + 'px',
height: calculateEventHeight(event) + 'px'
}"
@click.stop="handleEventClick(event)"
>
<div class="event-title">{{ event.title }}</div>
<div class="event-time">{{ formatEventTime(event) }}</div>
</div>
</div>
</div>
</div>
</div>
<!-- 日视图 -->
<div v-else class="day-view">
<!-- 日期标题 -->
<div class="day-header">
<div class="time-column-header"></div>
<div
class="day-date-header"
:class="{ 'today': isToday(selectedDate) }"
>
<div>{{ formatDate(selectedDate, 'YYYY年MM月DD日') }}</div>
<div>{{ weekDays[new Date(selectedDate).getDay()] }}</div>
</div>
</div>
<!-- 时间网格 -->
<div class="day-grid">
<div class="time-column">
<div
v-for="hour in 24"
:key="hour"
class="time-cell"
>
{{ formatHour(hour - 1) }}
</div>
</div>
<div class="day-events-column">
<div
v-for="hour in 24"
:key="hour"
class="hour-cell"
@click="handleAddScheduleAt(selectedDate, hour - 1)"
>
<div
v-for="event in getEventsForDateAndHour(selectedDate, hour - 1)"
:key="event.id"
class="event-item day-event-item"
:style="{
backgroundColor: event.color || '#409EFF',
top: calculateEventTop(event, hour - 1) + 'px',
height: calculateEventHeight(event) + 'px'
}"
@click.stop="handleEventClick(event)"
>
<div class="event-title">{{ event.title }}</div>
<div class="event-time">{{ formatEventTime(event) }}</div>
<div v-if="event.location" class="event-location">
<el-icon><Location /></el-icon> {{ event.location }}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</el-card>
</el-col>
<!-- 日程列表 -->
<el-col :span="8">
<el-card shadow="hover" class="events-card">
<template #header>
<div class="events-header">
<span>{{ formatDate(selectedDate, 'YYYY年MM月DD日') }} 的日程</span>
<el-button
type="primary"
size="small"
@click="handleAddScheduleAt(selectedDate)"
v-if="isLoggedIn"
>
<el-icon><Plus /></el-icon>
</el-button>
</div>
</template>
<div class="events-list">
<el-empty v-if="selectedDateEvents.length === 0" description="暂无日程" />
<el-timeline v-else>
<el-timeline-item
v-for="event in selectedDateEvents"
:key="event.id"
:color="event.color || '#409EFF'"
:timestamp="formatEventTime(event)"
>
<el-card class="event-card" @click="handleEventClick(event)">
<h4>{{ event.title }}</h4>
<p v-if="event.location">
<el-icon><Location /></el-icon> {{ event.location }}
</p>
<p v-if="event.description" class="event-description">
{{ event.description }}
</p>
</el-card>
</el-timeline-item>
</el-timeline>
</div>
</el-card>
</el-col>
</el-row>
</div>
</div>
<!-- 日程对话框 -->
<ScheduleDialog
v-model:visible="scheduleDialogVisible"
:is-editing="isEditingSchedule"
:schedule="currentSchedule"
:initial-date="dialogInitialDate"
:initial-hour="dialogInitialHour"
@submit="handleScheduleSubmit"
/>
<!-- 日程详情对话框 -->
<ScheduleDetailDialog
v-model:visible="scheduleDetailVisible"
:schedule="currentSchedule"
@edit="handleEditSchedule"
@delete="handleDeleteSchedule"
/>
</template>
<style scoped>
.schedule-container {
padding: 20px;
}
.schedule-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.page-title {
font-size: 24px;
margin: 0;
}
.schedule-content {
margin-top: 20px;
}
.calendar-card, .events-card {
border-radius: 8px;
height: 100%;
}
.calendar-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
/* 月视图样式 */
.month-view {
min-height: 600px;
}
.week-header {
display: flex;
border-bottom: 1px solid #eee;
}
.week-day {
flex: 1;
text-align: center;
padding: 10px;
font-weight: bold;
}
.month-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
grid-auto-rows: minmax(100px, auto);
gap: 1px;
background-color: #eee;
}
.date-cell {
background-color: white;
padding: 5px;
min-height: 100px;
cursor: pointer;
}
.date-cell:hover {
background-color: #f5f7fa;
}
.date-cell.other-month {
color: #c0c4cc;
}
.date-cell.today {
background-color: #ecf5ff;
}
.date-cell.has-events .date-number {
font-weight: bold;
color: var(--el-color-primary);
}
.date-number {
font-size: 14px;
margin-bottom: 5px;
}
.date-events {
display: flex;
flex-wrap: wrap;
gap: 3px;
}
.event-dot {
width: 8px;
height: 8px;
border-radius: 50%;
}
/* 周视图和日视图样式 */
.week-view, .day-view {
min-height: 600px;
}
.week-day-header, .day-date-header {
flex: 1;
text-align: center;
padding: 10px;
font-weight: bold;
display: flex;
flex-direction: column;
gap: 5px;
}
.week-day-header.today, .day-date-header.today {
background-color: #ecf5ff;
color: var(--el-color-primary);
}
.time-column-header {
width: 60px;
}
.week-grid, .day-grid {
display: flex;
height: 600px;
overflow-y: auto;
}
.time-column {
width: 60px;
flex-shrink: 0;
border-right: 1px solid #eee;
}
.time-cell {
height: 60px;
display: flex;
align-items: center;
justify-content: center;
border-bottom: 1px solid #eee;
font-size: 12px;
}
.day-column, .day-events-column {
flex: 1;
position: relative;
border-right: 1px solid #eee;
}
.hour-cell {
height: 60px;
border-bottom: 1px solid #eee;
position: relative;
}
.event-item {
position: absolute;
left: 2px;
right: 2px;
border-radius: 4px;
padding: 4px;
color: white;
font-size: 12px;
overflow: hidden;
cursor: pointer;
z-index: 1;
transition: all 0.3s;
}
.event-item:hover {
transform: scale(1.02);
z-index: 2;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.event-title {
font-weight: bold;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.event-time, .event-location {
font-size: 10px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: flex;
align-items: center;
gap: 2px;
}
.day-event-item {
padding: 8px;
}
/* 日程列表样式 */
.events-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.events-list {
max-height: 600px;
overflow-y: auto;
}
.event-card {
cursor: pointer;
transition: all 0.3s;
}
.event-card:hover {
transform: translateY(-2px);
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.event-description {
color: #606266;
font-size: 12px;
margin-top: 5px;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
</style>
<script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue';
import { ElMessage, ElMessageBox, FormInstance } from 'element-plus';
import { scheduleApi } from '@/api';
import { useUserStore } from '@/stores';
import { Plus, ArrowLeft, ArrowRight, Location } from '@element-plus/icons-vue';
import dayjs from 'dayjs';
import ScheduleDialog from './components/ScheduleDialog.vue';
import ScheduleDetailDialog from './components/ScheduleDetailDialog.vue';
const userStore = useUserStore();
const isLoggedIn = computed(() => userStore.isLoggedIn);
//
const calendarView = ref('month');
const currentDate = ref(new Date());
const selectedDate = ref(new Date());
const events = ref<any[]>([]);
const loading = ref(false);
//
const weekDays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
//
const currentYearMonth = computed(() => {
return dayjs(currentDate.value).format('YYYY年MM月');
});
const calendarDays = computed(() => {
const year = currentDate.value.getFullYear();
const month = currentDate.value.getMonth();
//
const firstDay = new Date(year, month, 1).getDay();
//
const daysInMonth = new Date(year, month + 1, 0).getDate();
//
const daysInPrevMonth = new Date(year, month, 0).getDate();
const days = [];
//
for (let i = firstDay - 1; i >= 0; i--) {
const prevMonthDay = daysInPrevMonth - i;
const date = new Date(year, month - 1, prevMonthDay);
days.push({
day: prevMonthDay,
date,
otherMonth: true
});
}
//
for (let i = 1; i <= daysInMonth; i++) {
const date = new Date(year, month, i);
days.push({
day: i,
date,
otherMonth: false
});
}
// 4267
const remainingDays = 42 - days.length;
for (let i = 1; i <= remainingDays; i++) {
const date = new Date(year, month + 1, i);
days.push({
day: i,
date,
otherMonth: true
});
}
return days;
});
const weekDates = computed(() => {
const date = new Date(selectedDate.value);
const day = date.getDay(); // 0-6, 0 is Sunday
//
date.setDate(date.getDate() - day);
// 7
const dates = [];
for (let i = 0; i < 7; i++) {
const weekDate = new Date(date);
weekDate.setDate(date.getDate() + i);
dates.push(weekDate);
}
return dates;
});
const selectedDateEvents = computed(() => {
return events.value.filter(event => {
const eventDate = new Date(event.startTime);
return isSameDay(eventDate, selectedDate.value);
}).sort((a, b) => {
return new Date(a.startTime).getTime() - new Date(b.startTime).getTime();
});
});
//
onMounted(async () => {
await fetchEvents();
});
//
const fetchEvents = async () => {
loading.value = true;
try {
const res = await scheduleApi.getAllSchedules();
if (res.code === 200) {
events.value = res.data.list;
}
} catch (error) {
console.error('获取日程列表失败:', error);
ElMessage.error('获取日程列表失败');
} finally {
loading.value = false;
}
};
//
const prevMonth = () => {
const date = new Date(currentDate.value);
date.setMonth(date.getMonth() - 1);
currentDate.value = date;
};
const nextMonth = () => {
const date = new Date(currentDate.value);
date.setMonth(date.getMonth() + 1);
currentDate.value = date;
};
const selectDate = (date: Date) => {
selectedDate.value = date;
};
//
const isToday = (date: Date) => {
const today = new Date();
return isSameDay(date, today);
};
const isSameDay = (date1: Date, date2: Date) => {
return date1.getFullYear() === date2.getFullYear() &&
date1.getMonth() === date2.getMonth() &&
date1.getDate() === date2.getDate();
};
const formatDate = (date: Date, format: string = 'YYYY-MM-DD') => {
return dayjs(date).format(format);
};
const formatHour = (hour: number) => {
return `${hour.toString().padStart(2, '0')}:00`;
};
const formatEventTime = (event: any) => {
const startTime = dayjs(event.startTime);
const endTime = dayjs(event.endTime);
if (event.isAllDay) {
return '全天';
}
return `${startTime.format('HH:mm')} - ${endTime.format('HH:mm')}`;
};
//
const hasEvents = (date: Date) => {
return events.value.some(event => {
const eventDate = new Date(event.startTime);
return isSameDay(eventDate, date);
});
};
const getEventsForDate = (date: Date) => {
return events.value.filter(event => {
const eventDate = new Date(event.startTime);
return isSameDay(eventDate, date);
});
};
const getEventsForDateAndHour = (date: Date, hour: number) => {
return events.value.filter(event => {
const eventStartDate = new Date(event.startTime);
const eventEndDate = new Date(event.endTime);
//
if (!isSameDay(eventStartDate, date)) {
return false;
}
//
const eventStartHour = eventStartDate.getHours();
const eventEndHour = eventEndDate.getHours();
//
if (event.isAllDay) {
return true;
}
//
return (eventStartHour <= hour && eventEndHour > hour) ||
(eventStartHour === hour);
});
};
const calculateEventTop = (event: any, hour: number) => {
const eventStartDate = new Date(event.startTime);
const eventStartHour = eventStartDate.getHours();
const eventStartMinute = eventStartDate.getMinutes();
//
if (event.isAllDay) {
return 0;
}
//
if (eventStartHour === hour) {
return (eventStartMinute / 60) * 60; // 60px
}
return 0;
};
const calculateEventHeight = (event: any) => {
const eventStartDate = new Date(event.startTime);
const eventEndDate = new Date(event.endTime);
//
if (event.isAllDay) {
return 30;
}
//
const durationMinutes = (eventEndDate.getTime() - eventStartDate.getTime()) / (1000 * 60);
// 60px
return (durationMinutes / 60) * 60;
};
//
const scheduleDialogVisible = ref(false);
const scheduleDetailVisible = ref(false);
const isEditingSchedule = ref(false);
const currentSchedule = ref<any>(null);
const dialogInitialDate = ref<Date | null>(null);
const dialogInitialHour = ref<number | undefined>(undefined);
//
const handleAddSchedule = () => {
if (!isLoggedIn.value) {
ElMessage.warning('请先登录');
return;
}
// 使
handleAddScheduleAt(selectedDate.value);
};
//
const handleAddScheduleAt = (date: Date, hour?: number) => {
if (!isLoggedIn.value) {
ElMessage.warning('请先登录');
return;
}
isEditingSchedule.value = false;
currentSchedule.value = null;
dialogInitialDate.value = date;
dialogInitialHour.value = hour;
scheduleDialogVisible.value = true;
};
//
const handleEventClick = (event: any) => {
currentSchedule.value = event;
scheduleDetailVisible.value = true;
};
//
const handleEditSchedule = (schedule: any) => {
isEditingSchedule.value = true;
currentSchedule.value = schedule;
scheduleDialogVisible.value = true;
};
//
const handleDeleteSchedule = async (id: number) => {
try {
const res = await scheduleApi.deleteSchedule(id);
if (res.code === 200) {
ElMessage.success('日程删除成功');
fetchEvents();
}
} catch (error) {
console.error('删除日程失败:', error);
ElMessage.error('删除日程失败');
}
};
//
const handleScheduleSubmit = async (data: any) => {
try {
let res;
if (isEditingSchedule.value && data.id) {
//
res = await scheduleApi.updateSchedule(data.id, {
title: data.title,
description: data.description,
startTime: data.startTime,
endTime: data.endTime,
location: data.location,
isAllDay: data.isAllDay,
reminder: data.reminder,
color: data.color
});
if (res.code === 200) {
ElMessage.success('日程更新成功');
}
} else {
//
res = await scheduleApi.createSchedule({
title: data.title,
description: data.description,
startTime: data.startTime,
endTime: data.endTime,
location: data.location,
isAllDay: data.isAllDay,
reminder: data.reminder,
color: data.color
});
if (res.code === 200) {
ElMessage.success('日程创建成功');
}
}
//
fetchEvents();
} catch (error) {
console.error('提交日程失败:', error);
ElMessage.error('提交日程失败');
}
};
</script>

@ -0,0 +1,164 @@
<template>
<el-dialog
v-model="dialogVisible"
title="日程详情"
width="400px"
>
<div v-if="schedule" class="schedule-detail">
<h3 class="detail-title" :style="{ color: schedule.color || '#409EFF' }">{{ schedule.title }}</h3>
<div class="detail-item">
<span class="detail-label">时间</span>
<span>{{ formatScheduleTime(schedule) }}</span>
</div>
<div class="detail-item" v-if="schedule.location">
<span class="detail-label">地点</span>
<span>{{ schedule.location }}</span>
</div>
<div class="detail-item" v-if="schedule.reminder">
<span class="detail-label">提醒</span>
<span>{{ formatReminder(schedule.reminder) }}</span>
</div>
<div class="detail-item" v-if="schedule.description">
<span class="detail-label">描述</span>
<div class="detail-description">{{ schedule.description }}</div>
</div>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false">关闭</el-button>
<el-button type="primary" @click="handleEdit" v-if="isOwner"></el-button>
<el-button type="danger" @click="handleDelete" v-if="isOwner"></el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { computed, defineProps, defineEmits } from 'vue';
import { ElMessageBox } from 'element-plus';
import dayjs from 'dayjs';
import { useUserStore } from '@/stores';
const props = defineProps({
visible: {
type: Boolean,
default: false
},
schedule: {
type: Object,
default: () => ({})
}
});
const emit = defineEmits(['update:visible', 'edit', 'delete']);
//
const dialogVisible = computed({
get: () => props.visible,
set: (val) => emit('update:visible', val)
});
//
const userStore = useUserStore();
const isOwner = computed(() => {
if (!props.schedule || !userStore.user) return false;
return props.schedule.userId === userStore.user.id;
});
//
const formatScheduleTime = (schedule: any) => {
if (!schedule) return '';
const startTime = dayjs(schedule.startTime);
const endTime = dayjs(schedule.endTime);
if (schedule.isAllDay === 1) {
//
if (startTime.format('YYYY-MM-DD') === endTime.format('YYYY-MM-DD')) {
return `${startTime.format('YYYY年MM月DD日')} 全天`;
}
return `${startTime.format('YYYY年MM月DD日')} - ${endTime.format('YYYY年MM月DD日')} 全天`;
}
//
if (startTime.format('YYYY-MM-DD') === endTime.format('YYYY-MM-DD')) {
return `${startTime.format('YYYY年MM月DD日 HH:mm')} - ${endTime.format('HH:mm')}`;
}
return `${startTime.format('YYYY年MM月DD日 HH:mm')} - ${endTime.format('YYYY年MM月DD日 HH:mm')}`;
};
//
const formatReminder = (minutes: number) => {
if (minutes === 0) return '不提醒';
if (minutes === 5) return '5分钟前';
if (minutes === 15) return '15分钟前';
if (minutes === 30) return '30分钟前';
if (minutes === 60) return '1小时前';
if (minutes === 120) return '2小时前';
if (minutes === 1440) return '1天前';
return `${minutes}分钟前`;
};
//
const handleEdit = () => {
emit('edit', props.schedule);
dialogVisible.value = false;
};
//
const handleDelete = () => {
ElMessageBox.confirm(
'确定要删除该日程吗?此操作不可恢复',
'删除确认',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
).then(() => {
emit('delete', props.schedule.id);
dialogVisible.value = false;
}).catch(() => {
//
});
};
</script>
<style scoped>
.schedule-detail {
padding: 10px;
}
.detail-title {
font-size: 18px;
margin-bottom: 15px;
}
.detail-item {
margin-bottom: 10px;
display: flex;
}
.detail-label {
font-weight: 500;
width: 60px;
flex-shrink: 0;
}
.detail-description {
white-space: pre-line;
color: #606266;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
}
</style>

@ -0,0 +1,294 @@
<template>
<el-dialog
v-model="dialogVisible"
:title="isEditing ? '编辑日程' : '添加日程'"
width="500px"
@closed="handleClosed"
>
<el-form :model="form" label-width="80px" :rules="rules" ref="formRef">
<el-form-item label="标题" prop="title">
<el-input v-model="form.title" placeholder="请输入日程标题"></el-input>
</el-form-item>
<el-form-item label="全天" prop="isAllDay">
<el-switch v-model="form.isAllDay" :active-value="1" :inactive-value="0"></el-switch>
</el-form-item>
<el-form-item label="开始时间" prop="startTime">
<el-date-picker
v-if="form.isAllDay === 1"
v-model="form.startDate"
type="date"
placeholder="选择日期"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
></el-date-picker>
<el-date-picker
v-else
v-model="form.startTime"
type="datetime"
placeholder="选择日期时间"
format="YYYY-MM-DD HH:mm"
value-format="YYYY-MM-DDTHH:mm:00"
></el-date-picker>
</el-form-item>
<el-form-item label="结束时间" prop="endTime">
<el-date-picker
v-if="form.isAllDay === 1"
v-model="form.endDate"
type="date"
placeholder="选择日期"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
:disabled-date="disabledEndDate"
></el-date-picker>
<el-date-picker
v-else
v-model="form.endTime"
type="datetime"
placeholder="选择日期时间"
format="YYYY-MM-DD HH:mm"
value-format="YYYY-MM-DDTHH:mm:00"
:disabled-date="disabledEndDate"
></el-date-picker>
</el-form-item>
<el-form-item label="地点" prop="location">
<el-input v-model="form.location" placeholder="请输入地点"></el-input>
</el-form-item>
<el-form-item label="提醒" prop="reminder">
<el-select v-model="form.reminder" placeholder="请选择提醒时间">
<el-option label="不提醒" :value="0"></el-option>
<el-option label="5分钟前" :value="5"></el-option>
<el-option label="15分钟前" :value="15"></el-option>
<el-option label="30分钟前" :value="30"></el-option>
<el-option label="1小时前" :value="60"></el-option>
<el-option label="2小时前" :value="120"></el-option>
<el-option label="1天前" :value="1440"></el-option>
</el-select>
</el-form-item>
<el-form-item label="颜色" prop="color">
<el-color-picker v-model="form.color"></el-color-picker>
</el-form-item>
<el-form-item label="描述" prop="description">
<el-input
v-model="form.description"
type="textarea"
:rows="3"
placeholder="请输入日程描述"
></el-input>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitForm" :loading="submitting">
保存
</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, reactive, computed, watch, defineProps, defineEmits } from 'vue';
import { ElMessage, FormInstance } from 'element-plus';
import dayjs from 'dayjs';
const props = defineProps({
visible: {
type: Boolean,
default: false
},
isEditing: {
type: Boolean,
default: false
},
schedule: {
type: Object,
default: () => ({})
},
initialDate: {
type: Date,
default: () => new Date()
},
initialHour: {
type: Number,
default: undefined
}
});
const emit = defineEmits(['update:visible', 'submit', 'delete']);
//
const dialogVisible = computed({
get: () => props.visible,
set: (val) => emit('update:visible', val)
});
//
const formRef = ref<FormInstance>();
const submitting = ref(false);
//
const form = reactive({
id: undefined as number | undefined,
title: '',
isAllDay: 0,
startTime: '',
endTime: '',
startDate: '',
endDate: '',
location: '',
reminder: 0,
color: '#409EFF',
description: ''
});
//
const rules = {
title: [{ required: true, message: '请输入日程标题', trigger: 'blur' }],
startTime: [{ required: true, message: '请选择开始时间', trigger: 'change' }],
endTime: [{ required: true, message: '请选择结束时间', trigger: 'change' }],
startDate: [{ required: true, message: '请选择开始日期', trigger: 'change' }],
endDate: [{ required: true, message: '请选择结束日期', trigger: 'change' }]
};
// props
watch(() => props.visible, (val) => {
if (val) {
initForm();
}
});
//
const initForm = () => {
if (props.isEditing && props.schedule) {
//
form.id = props.schedule.id;
form.title = props.schedule.title;
form.isAllDay = props.schedule.isAllDay;
form.location = props.schedule.location || '';
form.reminder = props.schedule.reminder || 0;
form.color = props.schedule.color || '#409EFF';
form.description = props.schedule.description || '';
if (form.isAllDay === 1) {
form.startDate = dayjs(props.schedule.startTime).format('YYYY-MM-DD');
form.endDate = dayjs(props.schedule.endTime).format('YYYY-MM-DD');
} else {
form.startTime = dayjs(props.schedule.startTime).format('YYYY-MM-DDTHH:mm:00');
form.endTime = dayjs(props.schedule.endTime).format('YYYY-MM-DDTHH:mm:00');
}
} else {
//
form.id = undefined;
form.title = '';
form.isAllDay = 0;
form.location = '';
form.reminder = 0;
form.color = getRandomColor();
form.description = '';
//
const initialDate = props.initialDate || new Date();
const initialHour = props.initialHour !== undefined ? props.initialHour : initialDate.getHours();
const startDate = dayjs(initialDate);
const endDate = dayjs(initialDate).add(1, 'hour');
form.startDate = startDate.format('YYYY-MM-DD');
form.endDate = startDate.format('YYYY-MM-DD');
form.startTime = startDate.hour(initialHour).minute(0).format('YYYY-MM-DDTHH:mm:00');
form.endTime = startDate.hour(initialHour + 1).minute(0).format('YYYY-MM-DDTHH:mm:00');
}
};
//
const disabledEndDate = (time: Date) => {
if (form.isAllDay === 1) {
return dayjs(time).isBefore(dayjs(form.startDate));
} else {
return dayjs(time).isBefore(dayjs(form.startTime));
}
};
//
const submitForm = async () => {
if (!formRef.value) return;
await formRef.value.validate(async (valid) => {
if (valid) {
submitting.value = true;
try {
//
const data: any = {
title: form.title,
isAllDay: form.isAllDay,
location: form.location,
reminder: form.reminder,
color: form.color,
description: form.description
};
//
if (form.isAllDay === 1) {
data.startTime = `${form.startDate}T00:00:00`;
data.endTime = `${form.endDate}T23:59:59`;
} else {
data.startTime = form.startTime;
data.endTime = form.endTime;
}
//
emit('submit', {
id: form.id,
...data
});
dialogVisible.value = false;
} catch (error) {
console.error('提交日程失败:', error);
ElMessage.error('提交日程失败');
} finally {
submitting.value = false;
}
}
});
};
//
const handleClosed = () => {
formRef.value?.resetFields();
};
//
const getRandomColor = () => {
const colors = [
'#409EFF', //
'#67C23A', // 绿
'#E6A23C', //
'#F56C6C', //
'#909399', //
'#9966CC', //
'#FF9900', //
'#19CAAD', //
'#8CC7B5', // 绿
'#A0EEE1', //
'#BEE7E9', //
'#BEEDC7', // 绿
'#D6D5B7', //
'#D1BA74', //
'#E6CEAC', //
'#ECAD9E' //
];
return colors[Math.floor(Math.random() * colors.length)];
};
</script>
<style scoped>
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
}
</style>

@ -16,10 +16,19 @@ public class WebMvcConfig implements WebMvcConfigurer {
public void addInterceptors(InterceptorRegistry registry) { public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(jwtInterceptor).addPathPatterns("/**") registry.addInterceptor(jwtInterceptor).addPathPatterns("/**")
.excludePathPatterns( .excludePathPatterns(
// 用户登录注册相关
"/users/login", "/users/login",
"/users/register", "/users/register",
"/users/code", "/users/code",
"/users/login/code", "/users/login/code",
// 论坛相关 - 允许未登录用户访问帖子列表和帖子详情
"/posts", // 帖子列表
"/posts/*/", // 帖子详情
"/posts/{id}", // 帖子详情(另一种匹配方式)
"/categories", // 分类列表
// Swagger文档相关
"/swagger-resources/**", "/swagger-resources/**",
"/v3/api-docs/**", "/v3/api-docs/**",
"/doc.html", "/doc.html",

@ -83,4 +83,14 @@ public class PostController {
} }
return postService.likePost(postId, userId); return postService.likePost(postId, userId);
} }
@Operation(summary = "获取用户的帖子列表")
@GetMapping("/user/{userId}")
public Result<?> getUserPosts(
@PathVariable("userId") Long userId,
@RequestParam(value = "page", defaultValue = "1") Integer page,
@RequestParam(value = "size", defaultValue = "10") Integer size,
@RequestParam(value = "sort", defaultValue = "latest") String sort) {
return postService.getUserPosts(userId, page, size, sort);
}
} }

@ -79,4 +79,19 @@ public interface PostMapper {
* @param id ID * @param id ID
*/ */
void decrementCommentCount(Long id); void decrementCommentCount(Long id);
/**
*
* @param userId ID
* @param sort
* @return
*/
List<Post> getListByUserId(@Param("userId") Long userId, @Param("sort") String sort);
/**
*
* @param userId ID
* @return
*/
Integer getCountByUserId(@Param("userId") Long userId);
} }

@ -34,11 +34,7 @@ public class PostVO {
* ID * ID
*/ */
private Long userId; private Long userId;
/**
*
*/
private String nickname;
/** /**
* *

@ -58,4 +58,14 @@ public interface PostService {
* @return * @return
*/ */
Result likePost(Long postId, Long userId); Result likePost(Long postId, Long userId);
/**
*
* @param userId ID
* @param page
* @param size
* @param sort latest-hot-
* @return
*/
Result getUserPosts(Long userId, Integer page, Integer size, String sort);
} }

@ -108,7 +108,6 @@ public class PostServiceImpl implements PostService {
.title(post.getTitle()) .title(post.getTitle())
.content(post.getContent()) .content(post.getContent())
.userId(post.getUserId()) .userId(post.getUserId())
.nickname(user != null ? user.getNickname() : "未知用户")
.avatar(user != null ? user.getAvatar() : null) .avatar(user != null ? user.getAvatar() : null)
.categoryId(post.getCategoryId()) .categoryId(post.getCategoryId())
.categoryName(categoryName) // 使用从数据库查询到的真实分类名称 .categoryName(categoryName) // 使用从数据库查询到的真实分类名称
@ -231,7 +230,7 @@ public class PostServiceImpl implements PostService {
return Result.success(null, "删除成功"); return Result.success(null, "删除成功");
} }
@Override @Override
public Result likePost(Long postId, Long userId) { public Result likePost(Long postId, Long userId) {
// 获取帖子 // 获取帖子
@ -255,6 +254,83 @@ public class PostServiceImpl implements PostService {
return Result.success(null, "点赞成功"); return Result.success(null, "点赞成功");
} }
} }
@Override
public Result getUserPosts(Long userId, Integer page, Integer size, String sort) {
// 参数校验
if (userId == null) {
return Result.error(400, "用户ID不能为空");
}
// 检查用户是否存在
User user = userMapper.getUserById(userId);
if (user == null) {
return Result.error(404, "用户不存在");
}
// 分页查询
PageHelper.startPage(page, size);
List<Post> posts = postMapper.getListByUserId(userId, sort);
PageInfo<Post> pageInfo = new PageInfo<>(posts);
// 获取分类信息
List<Long> categoryIds = posts.stream()
.map(Post::getCategoryId)
.distinct()
.collect(Collectors.toList());
// 获取分类名称映射
Map<Long, String> categoryMap;
if (!categoryIds.isEmpty()) {
// 获取所有分类,然后过滤出需要的分类
List<Category> allCategories = categoryMapper.getList(null);
// 过滤出匹配的分类
List<Category> filteredCategories = allCategories.stream()
.filter(category -> categoryIds.contains(category.getId()))
.collect(Collectors.toList());
// 构建分类ID到名称的映射
categoryMap = filteredCategories.stream()
.collect(Collectors.toMap(Category::getId, Category::getName));
} else {
categoryMap = new HashMap<>();
}
// 转换为VO
List<PostListVO> postVOs = posts.stream().map(post -> {
PostListVO vo = new PostListVO();
BeanUtil.copyProperties(post, vo);
// 填充分类名称
String categoryName = categoryMap.getOrDefault(post.getCategoryId(), "未知分类");
vo.setCategoryName(categoryName);
// 获取作者信息
User author = userMapper.getUserById(post.getUserId());
if (author != null) {
vo.setNickname(author.getNickname());
vo.setAvatar(author.getAvatar());
}
// 内容摘要
if (StrUtil.isNotBlank(post.getContent())) {
String content = post.getContent()
.replaceAll("<[^>]*>", "") // 去除HTML标签
.replaceAll("&[^;]+;", ""); // 去除HTML实体
vo.setSummary(StrUtil.maxLength(content, 100));
}
return vo;
}).collect(Collectors.toList());
// 构建返回数据
Map<String, Object> data = new HashMap<>();
data.put("total", pageInfo.getTotal());
data.put("pages", pageInfo.getPages());
data.put("list", postVOs);
return Result.success(data);
}
/** /**
* *

@ -108,4 +108,29 @@
SET comment_count = GREATEST(comment_count - 1, 0) SET comment_count = GREATEST(comment_count - 1, 0)
WHERE id = #{id} WHERE id = #{id}
</update> </update>
<select id="getListByUserId" resultType="com.unilife.model.entity.Post">
SELECT * FROM posts
WHERE user_id = #{userId} AND status != 0
<choose>
<when test="sort == 'hot'">
ORDER BY view_count DESC
</when>
<when test="sort == 'likes'">
ORDER BY like_count DESC
</when>
<when test="sort == 'comments'">
ORDER BY comment_count DESC
</when>
<otherwise>
ORDER BY created_at DESC
</otherwise>
</choose>
</select>
<select id="getCountByUserId" resultType="java.lang.Integer">
SELECT COUNT(*)
FROM posts
WHERE user_id = #{userId} AND status != 0
</select>
</mapper> </mapper>
Loading…
Cancel
Save