Enhance application structure with new modules and login improvements
- Add new menu sections for Activity Management and User Feedback - Update router configuration with new route paths - Refactor login API and user store for better error handling - Modify login view to improve navigation and error logging - Update patrol tasks view with more detailed table columns - Fix import paths for SCSS variables in patrol views
This commit is contained in:
parent
595054eb43
commit
f22ce91e1a
87
src/api/activity/course.js
Normal file
87
src/api/activity/course.js
Normal file
@ -0,0 +1,87 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
/**
|
||||
* 获取课程列表
|
||||
* @param {Object} params - 查询参数
|
||||
* @param {number} [params.page=1] - 页码
|
||||
* @param {number} [params.pageSize=10] - 每页条数
|
||||
* @param {string} [params.title] - 课程标题
|
||||
* @param {string} [params.category] - 课程类型
|
||||
* @param {number} [params.status] - 状态
|
||||
* @returns {Promise} 返回课程列表数据
|
||||
*/
|
||||
export function getCourseList(params = {}) {
|
||||
return request.get('/api/education/courses', {
|
||||
params: {
|
||||
page_num: params.page || 1,
|
||||
page_size: params.pageSize || 10,
|
||||
title: params.title || undefined,
|
||||
category: params.category || undefined,
|
||||
status: params.status === '' ? undefined : params.status
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建课程
|
||||
* @param {Object} data - 课程数据
|
||||
* @returns {Promise} 返回创建结果
|
||||
*/
|
||||
export function createCourse(data) {
|
||||
return request.post('/api/education/courses', data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取课程详情
|
||||
* @param {string|number} id - 课程ID
|
||||
* @returns {Promise} 返回课程详情数据
|
||||
*/
|
||||
export function getCourseDetail(id) {
|
||||
return request.get(`/api/education/courses/${id}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新课程
|
||||
* @param {string|number} id - 课程ID
|
||||
* @param {Object} data - 更新数据
|
||||
* @returns {Promise} 返回更新结果
|
||||
*/
|
||||
export function updateCourse(id, data) {
|
||||
return request.put(`/api/education/courses/${id}`, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除课程
|
||||
* @param {string|number} id - 课程ID
|
||||
* @returns {Promise} 返回删除结果
|
||||
*/
|
||||
export function deleteCourse(id) {
|
||||
return request.delete(`/api/education/courses/${id}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新课程状态
|
||||
* @param {string|number} id - 课程ID
|
||||
* @param {number} status - 状态值:1-进行中,0-已结束
|
||||
* @returns {Promise} 返回状态更新结果
|
||||
*/
|
||||
export function updateCourseStatus(id, status) {
|
||||
return request.put(`/api/education/courses/${id}/status`, {
|
||||
status: status === 1 ? 1 : 0 // 确保只发送 1 或 0
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取热门课程
|
||||
* @param {Object} params - 查询参数
|
||||
* @param {number} [params.limit=5] - 获取数量
|
||||
* @returns {Promise} 返回热门课程列表
|
||||
*/
|
||||
export function getHotCourses(params = {}) {
|
||||
return request.get('/api/education/courses/hot', {
|
||||
params: {
|
||||
limit: 5,
|
||||
...params
|
||||
}
|
||||
})
|
||||
}
|
||||
95
src/api/activity/knowledge.js
Normal file
95
src/api/activity/knowledge.js
Normal file
@ -0,0 +1,95 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
/**
|
||||
* 创建知识条目
|
||||
* @param {Object} data - 知识条目数据
|
||||
* @returns {Promise} 返回创建结果
|
||||
*/
|
||||
export function createKnowledge(data) {
|
||||
return request.post('/api/education/knowledge', data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取知识列表
|
||||
* @param {Object} params - 查询参数
|
||||
* @param {number} [params.page=1] - 页码
|
||||
* @param {number} [params.pageSize=10] - 每页条数
|
||||
* @param {string} [params.title] - 标题搜索
|
||||
* @param {string} [params.category] - 分类筛选
|
||||
* @param {number} [params.status] - 状态筛选
|
||||
* @returns {Promise} 返回知识列表数据
|
||||
*/
|
||||
export function getKnowledgeList(params = {}) {
|
||||
const { page = 1, pageSize = 10, ...restParams } = params
|
||||
return request.get('/api/education/knowledge', {
|
||||
params: {
|
||||
page_size: pageSize,
|
||||
page_num: page,
|
||||
...Object.fromEntries(
|
||||
Object.entries(restParams).filter(([_, value]) => value !== '')
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取知识详情
|
||||
* @param {string|number} id - 知识条目ID
|
||||
* @returns {Promise} 返回知识详情数据
|
||||
*/
|
||||
export function getKnowledgeDetail(id) {
|
||||
return request.get(`/api/education/knowledge/${id}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新知识条目
|
||||
* @param {string|number} id - 知识条目ID
|
||||
* @param {Object} data - 更新数据
|
||||
* @returns {Promise} 返回更新结果
|
||||
*/
|
||||
export function updateKnowledge(id, data) {
|
||||
return request.put(`/api/education/knowledge/${id}`, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新知识状态
|
||||
* @param {string|number} id - 知识条目ID
|
||||
* @param {number} status - 状态值:0-草稿,1-已发布
|
||||
* @returns {Promise} 返回状态更新结果
|
||||
*/
|
||||
export function updateKnowledgeStatus(id, status) {
|
||||
return request.put(`/api/education/knowledge/${id}/status`, {
|
||||
status: status === 1 ? 1 : 0 // 确保只发送 1 或 0
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量更新知识状态
|
||||
* @param {Array<string|number>} ids - 知识条目ID列表
|
||||
* @param {number} status - 状态值:0-草稿,1-已发布
|
||||
* @returns {Promise} 返回批量状态更新结果
|
||||
*/
|
||||
export function batchUpdateKnowledgeStatus(ids, status) {
|
||||
return request.put('/api/education/knowledge/batch/status', {
|
||||
ids,
|
||||
status: status === 1 ? 1 : 0
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除知识
|
||||
* @param {Array<string|number>} ids - 知识条目ID列表
|
||||
* @returns {Promise} 返回批量删除结果
|
||||
*/
|
||||
export function batchDeleteKnowledge(ids) {
|
||||
return request.post('/api/education/knowledge/batch/delete', { ids })
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除知识条目
|
||||
* @param {string|number} id - 知识条目ID
|
||||
* @returns {Promise} 返回删除结果
|
||||
*/
|
||||
export function deleteKnowledge(id) {
|
||||
return request.delete(`/api/education/knowledge/${id}`)
|
||||
}
|
||||
91
src/api/activity/study.js
Normal file
91
src/api/activity/study.js
Normal file
@ -0,0 +1,91 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
/**
|
||||
* 创建研学活动
|
||||
* @param {Object} data - 活动数据
|
||||
* @returns {Promise} 返回创建结果
|
||||
*/
|
||||
export function createActivity(data) {
|
||||
return request.post('/api/education/activities', data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取研学活动列表
|
||||
* @returns {Promise} 返回活动列表数据
|
||||
*/
|
||||
export function getActivityList() {
|
||||
return request.get('/api/education/activities')
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取研学活动详情
|
||||
* @param {string|number} id - 活动ID
|
||||
* @returns {Promise} 返回活动详情数据
|
||||
*/
|
||||
export function getActivityDetail(id) {
|
||||
return request.get(`/api/education/activities/${id}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消研学活动
|
||||
* @param {string|number} id - 活动ID
|
||||
* @returns {Promise} 返回取消结果
|
||||
*/
|
||||
export function cancelActivity(id) {
|
||||
return request.post(`/api/education/activities/${id}/cancel`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量取消研学活动
|
||||
* @param {Array<string|number>} ids - 活动ID列表
|
||||
* @returns {Promise} 返回批量取消结果
|
||||
*/
|
||||
export function batchCancelActivities(ids) {
|
||||
return request.post('/api/education/activities/batch/cancel', { ids })
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新研学活动
|
||||
* @param {string|number} id - 活动ID
|
||||
* @param {Object} data - 更新数据
|
||||
* @returns {Promise} 返回更新结果
|
||||
*/
|
||||
export function updateActivity(id, data) {
|
||||
return request.put(`/api/education/activities/${id}`, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新研学活动状态
|
||||
* @param {string|number} id - 活动ID
|
||||
* @param {number} status - 状态值
|
||||
* @returns {Promise} 返回状态更新结果
|
||||
*/
|
||||
export function updateActivityStatus(id, status) {
|
||||
return request.put(`/api/education/activities/${id}/status`, { status })
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查研学活动容量
|
||||
* @param {string|number} id - 活动ID
|
||||
* @returns {Promise} 返回容量检查结果
|
||||
*/
|
||||
export function checkActivityCapacity(id) {
|
||||
return request.get(`/api/education/activities/${id}/capacity`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定分类的研学活动
|
||||
* @param {Object} params - 查询参数
|
||||
* @param {number} [params.page=1] - 页码
|
||||
* @param {number} [params.pageSize=10] - 每页条数
|
||||
* @returns {Promise} 返回分类活动列表
|
||||
*/
|
||||
export function getCategoryActivities(params = {}) {
|
||||
return request.get('/api/education/activities/category/field_study', {
|
||||
params: {
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
...params
|
||||
}
|
||||
})
|
||||
}
|
||||
120
src/api/feedback/index.js
Normal file
120
src/api/feedback/index.js
Normal file
@ -0,0 +1,120 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
/**
|
||||
* 获取意见反馈列表
|
||||
* @param {Object} params - 查询参数
|
||||
* @param {number} [params.page=1] - 页码
|
||||
* @param {number} [params.page_size=10] - 每页条数
|
||||
* @param {string} [params.keyword] - 搜索关键词
|
||||
* @param {string} [params.feedback_type] - 反馈类型
|
||||
* @param {number} [params.status] - 状态筛选
|
||||
* @param {string} [params.start_date] - 开始日期
|
||||
* @param {string} [params.end_date] - 结束日期
|
||||
* @returns {Promise} 返回反馈列表数据
|
||||
*/
|
||||
export function getFeedbackList(params = {}) {
|
||||
return request.get('/api/admin/feedbacks', {
|
||||
params: {
|
||||
page: params.page || 1,
|
||||
page_size: params.page_size || 10,
|
||||
keyword: params.keyword || undefined,
|
||||
feedback_type: params.feedback_type || undefined,
|
||||
status: params.status === '' ? undefined : params.status,
|
||||
start_date: params.start_date || undefined,
|
||||
end_date: params.end_date || undefined
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取反馈详情
|
||||
* @param {string|number} id - 反馈ID
|
||||
* @returns {Promise} 返回反馈详情数据
|
||||
*/
|
||||
export function getFeedbackDetail(id) {
|
||||
return request.get(`/api/admin/feedbacks/${id}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新反馈状态和处理结果
|
||||
* @param {string|number} id - 反馈ID
|
||||
* @param {number} status - 状态值:0-待处理,1-处理中,2-已处理,3-已关闭
|
||||
* @param {string} [handling_result] - 处理结果,仅在status为2时需要
|
||||
* @returns {Promise} 返回更新结果
|
||||
*/
|
||||
export function updateFeedbackStatus(id, status, handling_result) {
|
||||
const data = { status }
|
||||
|
||||
// 当状态为已处理(2)时,需要包含处理结果
|
||||
if (status === 2 && handling_result) {
|
||||
data.handling_result = handling_result
|
||||
}
|
||||
|
||||
return request.put(`/api/admin/feedbacks/${id}/status`, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取满意度调查列表
|
||||
* @param {Object} params - 查询参数
|
||||
* @param {number} [params.page=1] - 页码
|
||||
* @param {number} [params.page_size=10] - 每页条数
|
||||
* @param {string} [params.keyword] - 搜索关键词
|
||||
* @param {number} [params.satisfaction_level] - 满意度等级
|
||||
* @param {string} [params.start_date] - 开始日期
|
||||
* @param {string} [params.end_date] - 结束日期
|
||||
* @returns {Promise} 返回满意度调查列表数据
|
||||
*/
|
||||
export function getSatisfactionList(params = {}) {
|
||||
return request.get('/api/admin/surveys', {
|
||||
params: {
|
||||
page: params.page || 1,
|
||||
page_size: params.page_size || 10,
|
||||
keyword: params.keyword || undefined,
|
||||
satisfaction_level: params.satisfaction_level || undefined,
|
||||
start_date: params.start_date || undefined,
|
||||
end_date: params.end_date || undefined
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建满意度调查
|
||||
* @param {Object} data - 调查数据
|
||||
* @param {string} data.user_name - 用户名称
|
||||
* @param {string} data.contact - 联系方式
|
||||
* @param {string} data.course_name - 课程名称
|
||||
* @param {number} data.satisfaction_level - 满意度等级(1-5)
|
||||
* @param {string} data.comment - 评价内容
|
||||
* @returns {Promise} 返回创建结果
|
||||
*/
|
||||
export function createSurvey(data) {
|
||||
return request.post('/api/admin/surveys', data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新满意度调查
|
||||
* @param {string|number} id - 调查ID
|
||||
* @param {Object} data - 更新数据
|
||||
* @returns {Promise} 返回更新结果
|
||||
*/
|
||||
export function updateSurvey(id, data) {
|
||||
return request.put(`/api/admin/surveys/${id}`, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取满意度调查详情
|
||||
* @param {string|number} id - 调查ID
|
||||
* @returns {Promise} 返回调查详情数据
|
||||
*/
|
||||
export function getSurveyDetail(id) {
|
||||
return request.get(`/api/admin/surveys/${id}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除满意度调查
|
||||
* @param {string|number} id - 调查ID
|
||||
* @returns {Promise} 返回删除结果
|
||||
*/
|
||||
export function deleteSurvey(id) {
|
||||
return request.delete(`/api/admin/surveys/${id}`)
|
||||
}
|
||||
@ -8,13 +8,8 @@ import request from '@/utils/request'
|
||||
* @returns {Promise} 返回包含token和用户信息的Promise
|
||||
*/
|
||||
export function login(data) {
|
||||
return request.post('/api/users/login', data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 退出登录
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function logout() {
|
||||
return request.post('/api/users/logout')
|
||||
return request.post('/api/users/login', {
|
||||
username: data.username,
|
||||
password: data.password
|
||||
})
|
||||
}
|
||||
|
||||
84
src/api/system/carousel.js
Normal file
84
src/api/system/carousel.js
Normal file
@ -0,0 +1,84 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
/**
|
||||
* 获取轮播图列表
|
||||
* @returns {Promise} 返回轮播图列表数据
|
||||
*/
|
||||
export function getCarouselList() {
|
||||
return request.get('/api/admin/carousels')
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建轮播图
|
||||
* @param {Object} data - 轮播图数据
|
||||
* @returns {Promise} 返回创建结果
|
||||
*/
|
||||
export function createCarousel(data) {
|
||||
return request.post('/api/admin/carousels', data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新轮播图
|
||||
* @param {string|number} id - 轮播图ID
|
||||
* @param {Object} data - 更新数据
|
||||
* @returns {Promise} 返回更新结果
|
||||
*/
|
||||
export function updateCarousel(id, data) {
|
||||
return request.put(`/api/admin/carousels/${id}`, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除轮播图
|
||||
* @param {string|number} id - 轮播图ID
|
||||
* @returns {Promise} 返回删除结果
|
||||
*/
|
||||
export function deleteCarousel(id) {
|
||||
return request.delete(`/api/admin/carousels/${id}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取轮播图详情
|
||||
* @param {string|number} id - 轮播图ID
|
||||
* @returns {Promise} 返回轮播图详情
|
||||
*/
|
||||
export function getCarouselDetail(id) {
|
||||
return request.get(`/api/admin/carousels/${id}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量更新轮播图排序
|
||||
* @param {Array<{id: number|string, sort_order: number}>} sortData - 排序数据
|
||||
* @returns {Promise} 返回排序更新结果
|
||||
*/
|
||||
export function batchUpdateCarouselSort(sortData) {
|
||||
if (!Array.isArray(sortData) || sortData.length === 0) {
|
||||
return Promise.reject(new Error('排序数据不能为空'))
|
||||
}
|
||||
return request.put('/api/admin/carousels/sort/batch', {
|
||||
sorts: sortData.map(item => ({
|
||||
id: item.id,
|
||||
sort_order: Number(item.sort_order)
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新轮播图状态
|
||||
* @param {string|number} id - 轮播图ID
|
||||
* @param {number} status - 状态值:0-禁用,1-启用
|
||||
* @returns {Promise} 返回状态更新结果
|
||||
*/
|
||||
export function updateCarouselStatus(id, status) {
|
||||
return request.put(`/api/admin/carousels/${id}/status`, {
|
||||
status: status === 1 ? 1 : 0
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除轮播图
|
||||
* @param {Array<string|number>} ids - 轮播图ID列表
|
||||
* @returns {Promise} 返回批量删除结果
|
||||
*/
|
||||
export function batchDeleteCarousels(ids) {
|
||||
return request.post('/api/admin/carousels/batch/delete', { ids })
|
||||
}
|
||||
@ -15,6 +15,7 @@ import {
|
||||
Tools,
|
||||
Document,
|
||||
DataLine,
|
||||
Collection,
|
||||
} from "@element-plus/icons-vue";
|
||||
|
||||
const router = useRouter();
|
||||
@ -34,7 +35,8 @@ const icons = {
|
||||
DataBoard: markRaw(DataBoard),
|
||||
Tools: markRaw(Tools),
|
||||
Document: markRaw(Document),
|
||||
DataLine: markRaw(DataLine)
|
||||
DataLine: markRaw(DataLine),
|
||||
Collection: markRaw(Collection),
|
||||
};
|
||||
|
||||
// 监听路由变化
|
||||
@ -111,6 +113,7 @@ const handleLogout = () => {
|
||||
<el-menu-item index="/system/settings">系统设置</el-menu-item>
|
||||
<el-menu-item index="/system/logs">系统日志</el-menu-item>
|
||||
<el-menu-item index="/system/data">数据管理</el-menu-item>
|
||||
<el-menu-item index="/system/carousel">轮播图管理</el-menu-item>
|
||||
</el-sub-menu>
|
||||
|
||||
<el-sub-menu index="monitor">
|
||||
@ -129,6 +132,7 @@ const handleLogout = () => {
|
||||
</template>
|
||||
<el-menu-item index="/patrol/tasks">巡护任务</el-menu-item>
|
||||
<el-menu-item index="/patrol/records">巡护记录</el-menu-item>
|
||||
<el-menu-item index="/patrol/events">安防事件</el-menu-item>
|
||||
</el-sub-menu>
|
||||
|
||||
<el-sub-menu index="report">
|
||||
@ -141,6 +145,25 @@ const handleLogout = () => {
|
||||
<el-menu-item index="/report/analysis">分析报告</el-menu-item>
|
||||
<el-menu-item index="/report/about">项目背景</el-menu-item>
|
||||
</el-sub-menu>
|
||||
|
||||
<el-sub-menu index="activity">
|
||||
<template #title>
|
||||
<el-icon><component :is="icons.Collection" /></el-icon>
|
||||
<span>活动管理</span>
|
||||
</template>
|
||||
<el-menu-item index="/activity/course">课程管理</el-menu-item>
|
||||
<el-menu-item index="/activity/study">研学管理</el-menu-item>
|
||||
<el-menu-item index="/activity/knowledge">知识库管理</el-menu-item>
|
||||
</el-sub-menu>
|
||||
|
||||
<el-sub-menu index="feedback">
|
||||
<template #title>
|
||||
<el-icon><component :is="icons.DataLine" /></el-icon>
|
||||
<span>用户反馈</span>
|
||||
</template>
|
||||
<el-menu-item index="/feedback/suggestions">意见反馈</el-menu-item>
|
||||
<el-menu-item index="/feedback/satisfaction">满意度调查</el-menu-item>
|
||||
</el-sub-menu>
|
||||
</el-menu>
|
||||
</el-aside>
|
||||
|
||||
|
||||
@ -64,6 +64,11 @@ const router = createRouter({
|
||||
name: 'PatrolTasks',
|
||||
component: () => import('../views/patrol/tasks/index.vue')
|
||||
},
|
||||
{
|
||||
path: 'patrol/events',
|
||||
name: 'PatrolEvents',
|
||||
component: () => import('../views/patrol/events/index.vue')
|
||||
},
|
||||
{
|
||||
path: 'patrol/records',
|
||||
name: 'PatrolRecords',
|
||||
@ -108,6 +113,43 @@ const router = createRouter({
|
||||
path: 'system/data',
|
||||
name: 'DataManagement',
|
||||
component: () => import('../views/system/data/index.vue')
|
||||
},
|
||||
{
|
||||
path: 'system/carousel',
|
||||
name: 'SystemCarousel',
|
||||
component: () => import('../views/system/carousel/index.vue'),
|
||||
meta: { title: '轮播图管理', icon: 'picture' }
|
||||
},
|
||||
{
|
||||
path: 'activity/course',
|
||||
name: 'CourseManagement',
|
||||
component: () => import('../views/activity/course/index.vue')
|
||||
},
|
||||
{
|
||||
path: 'activity/study',
|
||||
name: 'StudyManagement',
|
||||
component: () => import('../views/activity/study/index.vue')
|
||||
},
|
||||
{
|
||||
path: 'activity/knowledge',
|
||||
name: 'KnowledgeManagement',
|
||||
component: () => import('../views/activity/knowledge/index.vue')
|
||||
},
|
||||
{
|
||||
path: 'feedback/suggestions',
|
||||
name: 'FeedbackSuggestions',
|
||||
component: () => import('../views/feedback/suggestions/index.vue')
|
||||
},
|
||||
{
|
||||
path: 'feedback/satisfaction',
|
||||
name: 'FeedbackSatisfaction',
|
||||
component: () => import('../views/feedback/satisfaction/index.vue')
|
||||
},
|
||||
{
|
||||
path: 'data',
|
||||
name: 'SystemData',
|
||||
component: () => import('@/views/system/data/index.vue'),
|
||||
meta: { title: '数据管理', icon: 'data' }
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { login as loginApi, logout as logoutApi } from '@/api/login'
|
||||
import { login as loginApi } from '@/api/login'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
// 安全的 JSON 解析函数
|
||||
const safeJSONParse = (str, defaultValue = null) => {
|
||||
@ -31,36 +32,47 @@ export const useUserStore = defineStore('user', {
|
||||
async login(username, password) {
|
||||
try {
|
||||
// 调用登录接口
|
||||
const { token, userInfo } = await loginApi({ username, password })
|
||||
const response = await loginApi({ username, password })
|
||||
|
||||
// 检查响应状态
|
||||
if (!response.success) {
|
||||
console.error('登录失败:', response.message)
|
||||
ElMessage.error(response.message || '用户名或密码错误')
|
||||
return false
|
||||
}
|
||||
|
||||
// 从响应中获取token和用户信息
|
||||
const { user, token } = response.data
|
||||
|
||||
if (!token) {
|
||||
console.error('登录失败: 未获取到token')
|
||||
ElMessage.error('登录失败,请稍后重试')
|
||||
return false
|
||||
}
|
||||
|
||||
// 保存token和用户信息
|
||||
this.token = token
|
||||
this.userInfo = userInfo
|
||||
this.userInfo = user
|
||||
|
||||
// 持久化存储
|
||||
localStorage.setItem('token', token)
|
||||
localStorage.setItem('userInfo', JSON.stringify(userInfo))
|
||||
localStorage.setItem('userInfo', JSON.stringify(user))
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('登录失败:', error)
|
||||
ElMessage.error(error.response?.data?.message || '登录失败,请稍后重试')
|
||||
return false
|
||||
}
|
||||
},
|
||||
|
||||
// 退出登录
|
||||
async logout() {
|
||||
try {
|
||||
await logoutApi()
|
||||
} catch (error) {
|
||||
console.error('退出登录失败:', error)
|
||||
} finally {
|
||||
// 无论是否成功调用退出接口,都清除本地存储
|
||||
this.token = null
|
||||
this.userInfo = null
|
||||
localStorage.removeItem('token')
|
||||
localStorage.removeItem('userInfo')
|
||||
}
|
||||
logout() {
|
||||
// 清除本地存储
|
||||
this.token = null
|
||||
this.userInfo = null
|
||||
localStorage.removeItem('token')
|
||||
localStorage.removeItem('userInfo')
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
964
src/views/activity/course/index.vue
Normal file
964
src/views/activity/course/index.vue
Normal file
@ -0,0 +1,964 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, computed, watch } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus, Search, Refresh } from '@element-plus/icons-vue'
|
||||
import { formatDateTime } from '@/utils/format'
|
||||
import {
|
||||
getCourseList,
|
||||
createCourse,
|
||||
updateCourse,
|
||||
deleteCourse,
|
||||
updateCourseStatus
|
||||
} from '@/api/activity/course'
|
||||
|
||||
// 课程类型选项
|
||||
const categoryOptions = [
|
||||
{ label: '湿地科普', value: 'wetland' },
|
||||
{ label: '生态保护', value: 'ecology' },
|
||||
{ label: '物种知识', value: 'species' },
|
||||
{ label: '环境保护', value: 'environment' }
|
||||
]
|
||||
|
||||
// 难度等级选项
|
||||
const difficultyOptions = [
|
||||
{ label: '入门', value: 1 },
|
||||
{ label: '初级', value: 2 },
|
||||
{ label: '中级', value: 3 },
|
||||
{ label: '高级', value: 4 }
|
||||
]
|
||||
|
||||
// 适用对象选项
|
||||
const audienceOptions = [
|
||||
{ label: '所有人群', value: 'all' },
|
||||
{ label: '儿童', value: 'children' },
|
||||
{ label: '青少年', value: 'youth' },
|
||||
{ label: '成人', value: 'adult' }
|
||||
]
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = ref({
|
||||
title: '',
|
||||
category: '',
|
||||
status: ''
|
||||
})
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref([])
|
||||
const loading = ref(false)
|
||||
const allData = ref([]) // 存储所有数据
|
||||
|
||||
// 分页配置
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const total = ref(0)
|
||||
|
||||
// 添加过滤数据的计算属性
|
||||
const filteredData = computed(() => {
|
||||
let result = [...allData.value]
|
||||
|
||||
// 按课程名称搜索
|
||||
if (searchForm.value.title) {
|
||||
const keyword = searchForm.value.title.toLowerCase()
|
||||
result = result.filter(item =>
|
||||
item.title.toLowerCase().includes(keyword)
|
||||
)
|
||||
}
|
||||
|
||||
// 按课程类型过滤
|
||||
if (searchForm.value.category) {
|
||||
result = result.filter(item =>
|
||||
item.category === searchForm.value.category
|
||||
)
|
||||
}
|
||||
|
||||
// 按状态过滤
|
||||
if (searchForm.value.status !== '') {
|
||||
result = result.filter(item =>
|
||||
item.status === Number(searchForm.value.status)
|
||||
)
|
||||
}
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
// 获取课程列表
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getCourseList({
|
||||
page: currentPage.value,
|
||||
pageSize: pageSize.value,
|
||||
title: searchForm.value.title || undefined,
|
||||
category: searchForm.value.category || undefined,
|
||||
status: searchForm.value.status === '' ? undefined : Number(searchForm.value.status)
|
||||
})
|
||||
|
||||
if (res.success) {
|
||||
// 存储所有数据
|
||||
allData.value = res.data || []
|
||||
total.value = res.data.length || 0
|
||||
|
||||
// 更新表格数据
|
||||
tableData.value = allData.value.map(item => ({
|
||||
...item,
|
||||
created_at: formatDateTime(item.created_at),
|
||||
updated_at: formatDateTime(item.updated_at)
|
||||
}))
|
||||
} else {
|
||||
ElMessage.error(res.message || '获取课程列表失败')
|
||||
allData.value = []
|
||||
tableData.value = []
|
||||
total.value = 0
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取课程列表错误:', error)
|
||||
ElMessage.error('获取课程列表失败')
|
||||
allData.value = []
|
||||
tableData.value = []
|
||||
total.value = 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 修改分页相关的计算逻辑
|
||||
const handleCurrentChange = (val) => {
|
||||
currentPage.value = val
|
||||
getList()
|
||||
}
|
||||
|
||||
const handleSizeChange = (val) => {
|
||||
pageSize.value = val
|
||||
currentPage.value = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
// 移除 watch 监听器,因为我们使用后端分页和过滤
|
||||
const watchStopHandle = watch(
|
||||
[() => searchForm.value.title, () => searchForm.value.category, () => searchForm.value.status],
|
||||
() => {
|
||||
currentPage.value = 1
|
||||
getList()
|
||||
}
|
||||
)
|
||||
|
||||
// 处理搜索
|
||||
const handleSearch = () => {
|
||||
currentPage.value = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
// 重置搜索
|
||||
const resetSearch = () => {
|
||||
searchForm.value = {
|
||||
title: '',
|
||||
category: '',
|
||||
status: ''
|
||||
}
|
||||
currentPage.value = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
// 刷新列表
|
||||
const handleRefresh = () => {
|
||||
getList()
|
||||
}
|
||||
|
||||
// 新增/编辑对话框
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('')
|
||||
const formMode = ref('create')
|
||||
const form = ref({
|
||||
title: '',
|
||||
category: '',
|
||||
cover_image: '',
|
||||
video: '',
|
||||
video_name: '',
|
||||
video_url: '',
|
||||
video_duration: 0,
|
||||
video_size: 0,
|
||||
description: '',
|
||||
duration: 60,
|
||||
status: 0,
|
||||
imageInputType: 'upload',
|
||||
videoInputType: 'upload'
|
||||
})
|
||||
|
||||
// 表单规则
|
||||
const formRules = {
|
||||
title: [
|
||||
{ required: true, message: '请输入课程标题', trigger: 'blur' },
|
||||
{ min: 1, max: 200, message: '长度在 1 到 200 个字符', trigger: 'blur' }
|
||||
],
|
||||
category: [
|
||||
{ required: true, message: '请选择课程类型', trigger: 'change' }
|
||||
],
|
||||
duration: [
|
||||
{ type: 'number', min: 1, message: '时长必须为正整数', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
// 图片上传相关
|
||||
const handleCoverError = (error) => {
|
||||
console.error('上传图片错误:', error)
|
||||
form.value.cover_image = ''
|
||||
ElMessage.error('上传图片失败,请重试')
|
||||
}
|
||||
|
||||
const handleImageSuccess = (response) => {
|
||||
if (response.success) {
|
||||
console.log('上传返回的图片信息:', response.data)
|
||||
// 保存图片的所有相关信息
|
||||
const fileName = response.data.url.split('/').pop()
|
||||
const imageUrl = `http://localhost:3000/uploads/courses/images/${fileName}`
|
||||
|
||||
// 设置表单中的图片相关字段
|
||||
form.value = {
|
||||
...form.value,
|
||||
cover_image: fileName, // 只保存文件名,用于提交到数据库
|
||||
cover_image_url: imageUrl // 完整URL用于预览
|
||||
}
|
||||
|
||||
console.log('设置后的图片信息:', {
|
||||
cover_image: form.value.cover_image,
|
||||
cover_image_url: form.value.cover_image_url
|
||||
})
|
||||
ElMessage.success('图片上传成功')
|
||||
} else {
|
||||
form.value.cover_image = ''
|
||||
form.value.cover_image_url = ''
|
||||
ElMessage.error(response.message || '图片上传失败')
|
||||
}
|
||||
}
|
||||
|
||||
const beforeCoverUpload = (file) => {
|
||||
const isImage = file.type.startsWith('image/')
|
||||
const isLt2M = file.size / 1024 / 1024 < 2
|
||||
|
||||
if (!isImage) {
|
||||
ElMessage.error('上传封面图片只能是图片格式!')
|
||||
return false
|
||||
}
|
||||
if (!isLt2M) {
|
||||
ElMessage.error('上传封面图片大小不能超过 2MB!')
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// 新增课程
|
||||
const handleAdd = () => {
|
||||
formMode.value = 'create'
|
||||
dialogTitle.value = '新增课程'
|
||||
form.value = {
|
||||
title: '',
|
||||
category: '',
|
||||
cover_image: '',
|
||||
video: '',
|
||||
video_name: '',
|
||||
video_url: '',
|
||||
description: '',
|
||||
duration: 60,
|
||||
status: 0,
|
||||
imageInputType: 'upload',
|
||||
videoInputType: 'upload'
|
||||
}
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 编辑课程
|
||||
const handleEdit = (row) => {
|
||||
formMode.value = 'edit'
|
||||
dialogTitle.value = '编辑课程'
|
||||
form.value = {
|
||||
id: row.id,
|
||||
title: row.title,
|
||||
category: row.category,
|
||||
cover_image: row.cover_image,
|
||||
cover_image_url: row.cover_image ? (
|
||||
row.cover_image.startsWith('http')
|
||||
? row.cover_image
|
||||
: `http://localhost:3000/uploads/courses/images/${row.cover_image}`
|
||||
) : '',
|
||||
video: row.video || '', // 保存视频文件名
|
||||
video_url: row.video ? (
|
||||
row.video.startsWith('http')
|
||||
? row.video
|
||||
: `http://localhost:3000/uploads/courses/videos/${row.video}`
|
||||
) : '', // 完整的视频URL用于预览
|
||||
video_duration: row.video_duration || 0,
|
||||
video_size: row.video_size || 0,
|
||||
description: row.description || '',
|
||||
duration: row.duration || 60,
|
||||
status: row.status,
|
||||
imageInputType: 'upload',
|
||||
videoInputType: 'upload'
|
||||
}
|
||||
console.log('编辑表单数据:', form.value)
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const formRef = ref(null)
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
try {
|
||||
// 检查是否上传了图片和视频
|
||||
if (!form.value.cover_image) {
|
||||
ElMessage.warning('请上传课程封面图片')
|
||||
return
|
||||
}
|
||||
if (!form.value.video) {
|
||||
ElMessage.warning('请上传课程视频')
|
||||
return
|
||||
}
|
||||
|
||||
// 构建提交数据
|
||||
const submitData = {
|
||||
title: form.value.title.trim(),
|
||||
category: form.value.category,
|
||||
description: form.value.description?.trim() || '',
|
||||
duration: Number(form.value.duration) || 60,
|
||||
status: Number(form.value.status),
|
||||
// 图片信息
|
||||
cover_image: form.value.cover_image, // 文件名
|
||||
// 视频信息
|
||||
video: form.value.video, // 文件名
|
||||
video_duration: form.value.video_duration || 0,
|
||||
video_size: form.value.video_size || 0
|
||||
}
|
||||
|
||||
console.log('提交的课程数据:', submitData)
|
||||
let res
|
||||
if (formMode.value === 'create') {
|
||||
res = await createCourse(submitData)
|
||||
} else {
|
||||
const id = form.value.id
|
||||
res = await updateCourse(id, submitData)
|
||||
}
|
||||
|
||||
if (res.success) {
|
||||
ElMessage.success(`${formMode.value === 'create' ? '新增' : '编辑'}成功`)
|
||||
dialogVisible.value = false
|
||||
getList()
|
||||
} else {
|
||||
ElMessage.error(res.message || `${formMode.value === 'create' ? '新增' : '编辑'}失败`)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`${formMode.value === 'create' ? '新增' : '编辑'}课程错误:`, error)
|
||||
ElMessage.error('操作失败,请检查输入是否正确')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 删除课程
|
||||
const handleDelete = (row) => {
|
||||
// 如果课程已有学员报名,不允许删除
|
||||
if (row.enrolled > 0) {
|
||||
ElMessage.warning(`该课程已有${row.enrolled}人报名,不能删除`)
|
||||
return
|
||||
}
|
||||
|
||||
ElMessageBox.confirm('确认删除该课程吗?此操作不可恢复', '警告', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
try {
|
||||
const res = await deleteCourse(row.id)
|
||||
if (res.success) {
|
||||
ElMessage.success('删除成功')
|
||||
getList()
|
||||
} else {
|
||||
ElMessage.error(res.message || '删除失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除课程错误:', error)
|
||||
ElMessage.error('删除失败')
|
||||
}
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
// 更新课程状态
|
||||
const handleStatusChange = async (row) => {
|
||||
try {
|
||||
const newStatus = row.status === 1 ? 0 : 1
|
||||
const res = await updateCourseStatus(row.id, newStatus)
|
||||
if (res.success) {
|
||||
ElMessage.success('状态更新成功')
|
||||
getList()
|
||||
} else {
|
||||
ElMessage.error(res.message || '状态更新失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('更新课程状态错误:', error)
|
||||
ElMessage.error('状态更新失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 添加token的获取逻辑
|
||||
const token = computed(() => localStorage.getItem('token') || '')
|
||||
const uploadHeaders = computed(() => ({
|
||||
Authorization: `Bearer ${token.value}`
|
||||
}))
|
||||
|
||||
// 处理图片类型切换
|
||||
const handleImageTypeChange = () => {
|
||||
form.value.cover_image = ''
|
||||
}
|
||||
|
||||
// 处理图片错误
|
||||
const handleImageError = (e) => {
|
||||
console.error('图片加载失败:', e)
|
||||
ElMessage.warning('图片加载失败,但不影响保存')
|
||||
}
|
||||
|
||||
// 处理视频上传
|
||||
const handleVideoSuccess = (response) => {
|
||||
if (response.success) {
|
||||
console.log('上传返回的视频信息:', response.data)
|
||||
// 保存视频的所有相关信息
|
||||
const fileName = response.data.url.split('/').pop()
|
||||
const videoUrl = `http://localhost:3000/uploads/courses/videos/${fileName}`
|
||||
|
||||
// 设置表单中的视频相关字段
|
||||
form.value = {
|
||||
...form.value,
|
||||
video: fileName, // 只保存文件名,用于提交到数据库
|
||||
video_url: videoUrl, // 完整URL用于预览
|
||||
video_duration: response.data.duration || 0,
|
||||
video_size: response.data.size || 0,
|
||||
duration: Math.ceil((response.data.duration || 0) / 60) // 将视频时长(秒)转换为分钟并向上取整
|
||||
}
|
||||
|
||||
console.log('设置后的视频信息:', {
|
||||
video: form.value.video,
|
||||
video_url: form.value.video_url,
|
||||
video_duration: form.value.video_duration,
|
||||
video_size: form.value.video_size,
|
||||
duration: form.value.duration
|
||||
})
|
||||
ElMessage.success('视频上传成功')
|
||||
} else {
|
||||
form.value.video = ''
|
||||
form.value.video_url = ''
|
||||
form.value.video_duration = 0
|
||||
form.value.video_size = 0
|
||||
ElMessage.error(response.message || '视频上传失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleVideoError = (error) => {
|
||||
console.error('上传视频错误:', error)
|
||||
form.value.video = ''
|
||||
form.value.video_url = ''
|
||||
ElMessage.error('上传视频失败,请重试')
|
||||
}
|
||||
|
||||
const beforeVideoUpload = (file) => {
|
||||
const isVideo = file.type.startsWith('video/')
|
||||
const isLt500M = file.size / 1024 / 1024 < 500
|
||||
|
||||
if (!isVideo) {
|
||||
ElMessage.error('只能上传视频文件!')
|
||||
return false
|
||||
}
|
||||
if (!isLt500M) {
|
||||
ElMessage.error('视频大小不能超过 500MB!')
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// 处理视频类型切换
|
||||
const handleVideoTypeChange = () => {
|
||||
form.value.video = ''
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="course-management">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>课程管理</span>
|
||||
<div class="header-btns">
|
||||
<el-button type="primary" :icon="Plus" @click="handleAdd">新增课程</el-button>
|
||||
<el-button :icon="Refresh" @click="handleRefresh">刷新</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 搜索表单 -->
|
||||
<el-form :model="searchForm" inline class="search-form">
|
||||
<el-form-item label="课程标题" label-width="80px">
|
||||
<el-input
|
||||
v-model="searchForm.title"
|
||||
placeholder="请输入课程标题"
|
||||
clearable
|
||||
@keyup.enter="handleSearch"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="课程类型" label-width="80px">
|
||||
<el-select
|
||||
v-model="searchForm.category"
|
||||
placeholder="请选择类型"
|
||||
clearable
|
||||
>
|
||||
<el-option
|
||||
v-for="item in categoryOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" label-width="80px">
|
||||
<el-select
|
||||
v-model="searchForm.status"
|
||||
placeholder="请选择状态"
|
||||
clearable
|
||||
>
|
||||
<el-option :value="1" label="已发布" />
|
||||
<el-option :value="0" label="未发布" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<div class="search-buttons">
|
||||
<el-button type="primary" :icon="Search" @click="handleSearch">搜索</el-button>
|
||||
<el-button :icon="Refresh" @click="resetSearch">重置</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 课程列表 -->
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="tableData"
|
||||
border
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-table-column type="index" label="序号" width="60" align="center"/>
|
||||
<el-table-column prop="course_code" label="课程编号" width="120" show-overflow-tooltip/>
|
||||
<el-table-column prop="title" label="课程标题" min-width="150" show-overflow-tooltip/>
|
||||
<el-table-column prop="category" label="课程类型" width="120" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
:type="row.category === 'wetland' ? 'success' :
|
||||
row.category === 'ecology' ? 'primary' :
|
||||
row.category === 'species' ? 'warning' :
|
||||
row.category === 'nature' ? 'danger' : 'info'"
|
||||
>
|
||||
{{ categoryOptions.find(item => item.value === row.category)?.label || '未知' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="duration" label="时长(分钟)" width="100" align="center"/>
|
||||
<el-table-column prop="view_count" label="浏览量" width="100" align="center"/>
|
||||
<el-table-column prop="enrollment_count" label="报名人数" width="100" align="center"/>
|
||||
<el-table-column prop="status" label="状态" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 1 ? 'success' : 'info'">
|
||||
{{ row.status === 1 ? '已发布' : '未发布' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="created_at" label="创建时间" width="180" align="center" show-overflow-tooltip/>
|
||||
<el-table-column label="操作" width="180" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button
|
||||
type="success"
|
||||
link
|
||||
v-if="row.status === 0"
|
||||
@click="handleStatusChange(row)"
|
||||
>发布</el-button>
|
||||
<el-button
|
||||
type="info"
|
||||
link
|
||||
v-else
|
||||
@click="handleStatusChange(row)"
|
||||
>下线</el-button>
|
||||
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页器 -->
|
||||
<div class="pagination-container">
|
||||
<el-pagination
|
||||
v-model:current-page="currentPage"
|
||||
v-model:page-size="pageSize"
|
||||
:total="total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
background
|
||||
layout="total, sizes, prev, pager, next"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 新增/编辑对话框 -->
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogTitle"
|
||||
width="800px"
|
||||
destroy-on-close
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="formRules"
|
||||
label-width="100px"
|
||||
>
|
||||
<el-form-item label="课程标题" prop="title">
|
||||
<el-input
|
||||
v-model="form.title"
|
||||
placeholder="请输入课程标题"
|
||||
maxlength="200"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="课程类型" prop="category">
|
||||
<el-select
|
||||
v-model="form.category"
|
||||
placeholder="请选择课程类型"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in categoryOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="课程时长" prop="duration">
|
||||
<el-input-number
|
||||
v-model="form.duration"
|
||||
:min="1"
|
||||
:max="999"
|
||||
placeholder="请输入课程时长(分钟)"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="课程封面">
|
||||
<div class="image-input-container">
|
||||
<el-radio-group v-model="form.imageInputType" class="mb-4" @change="handleImageTypeChange">
|
||||
<el-radio :value="'url'">输入图片地址</el-radio>
|
||||
<el-radio :value="'upload'">上传图片</el-radio>
|
||||
</el-radio-group>
|
||||
|
||||
<template v-if="form.imageInputType === 'url'">
|
||||
<el-input
|
||||
v-model="form.cover_image"
|
||||
placeholder="请输入封面图片地址"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<el-upload
|
||||
class="image-upload"
|
||||
action="http://localhost:3000/api/education/knowledge/upload"
|
||||
:headers="uploadHeaders"
|
||||
name="cover_image"
|
||||
:show-file-list="false"
|
||||
:on-success="handleImageSuccess"
|
||||
:on-error="handleCoverError"
|
||||
:before-upload="beforeCoverUpload"
|
||||
>
|
||||
<template v-if="form.cover_image">
|
||||
<img
|
||||
:src="form.cover_image_url"
|
||||
class="uploaded-image"
|
||||
crossorigin="anonymous"
|
||||
@error="handleImageError"
|
||||
/>
|
||||
</template>
|
||||
<div v-else class="upload-placeholder">
|
||||
<el-icon class="image-upload-icon"><Plus /></el-icon>
|
||||
<div class="upload-text">点击上传图片</div>
|
||||
</div>
|
||||
</el-upload>
|
||||
<div class="form-tips">支持 jpg、png 格式,大小不超过 2MB</div>
|
||||
</template>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="课程视频">
|
||||
<div class="video-input-container">
|
||||
<el-radio-group v-model="form.videoInputType" class="mb-4" @change="handleVideoTypeChange">
|
||||
<el-radio :value="'url'">输入视频地址</el-radio>
|
||||
<el-radio :value="'upload'">上传视频</el-radio>
|
||||
</el-radio-group>
|
||||
|
||||
<template v-if="form.videoInputType === 'url'">
|
||||
<el-input
|
||||
v-model="form.video"
|
||||
placeholder="请输入视频地址"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<el-upload
|
||||
class="video-upload"
|
||||
action="http://localhost:3000/api/education/courses/upload/video"
|
||||
:headers="uploadHeaders"
|
||||
name="video"
|
||||
:show-file-list="false"
|
||||
:on-success="handleVideoSuccess"
|
||||
:on-error="handleVideoError"
|
||||
:before-upload="beforeVideoUpload"
|
||||
>
|
||||
<template v-if="form.video_url">
|
||||
<div class="video-preview">
|
||||
<video
|
||||
:src="form.video_url"
|
||||
controls
|
||||
class="uploaded-video"
|
||||
crossorigin="anonymous"
|
||||
@error="handleVideoError"
|
||||
>
|
||||
您的浏览器不支持视频播放
|
||||
</video>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else class="upload-placeholder">
|
||||
<el-icon class="video-upload-icon"><Plus /></el-icon>
|
||||
<div class="upload-text">点击上传视频</div>
|
||||
</div>
|
||||
</el-upload>
|
||||
<div class="form-tips">支持常见视频格式,大小不超过 500MB</div>
|
||||
</template>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="课程描述">
|
||||
<el-input
|
||||
v-model="form.description"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
placeholder="请输入课程描述"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-radio-group v-model="form.status">
|
||||
<el-radio :value="1">上架</el-radio>
|
||||
<el-radio :value="0">下架</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">确定</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.course-management {
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
span {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.header-btns {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.search-form {
|
||||
margin-bottom: 24px;
|
||||
padding: 24px;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 16px;
|
||||
margin-right: 24px;
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-input),
|
||||
:deep(.el-select) {
|
||||
width: 220px;
|
||||
}
|
||||
|
||||
.search-buttons {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.image-input-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
|
||||
.mb-4 {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.image-upload {
|
||||
:deep(.el-upload) {
|
||||
border: 1px dashed #d9d9d9;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: var(--el-transition-duration-fast);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.upload-placeholder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 178px;
|
||||
height: 178px;
|
||||
border: 1px dashed #d9d9d9;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: var(--el-transition-duration-fast);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.image-upload-icon {
|
||||
font-size: 28px;
|
||||
color: #8c939d;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.upload-text {
|
||||
font-size: 14px;
|
||||
color: #8c939d;
|
||||
}
|
||||
}
|
||||
|
||||
.uploaded-image {
|
||||
width: 178px;
|
||||
height: 178px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.form-tips {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: #8c939d;
|
||||
}
|
||||
}
|
||||
|
||||
.video-input-container {
|
||||
.video-preview {
|
||||
width: 320px;
|
||||
height: 180px;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
background-color: #000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 10px;
|
||||
|
||||
.uploaded-video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
|
||||
.video-upload {
|
||||
:deep(.el-upload) {
|
||||
width: 320px;
|
||||
height: 180px;
|
||||
border: 1px dashed #d9d9d9;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: var(--el-transition-duration-fast);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.upload-placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.video-upload-icon {
|
||||
font-size: 28px;
|
||||
color: #8c939d;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.upload-text {
|
||||
font-size: 14px;
|
||||
color: #8c939d;
|
||||
}
|
||||
}
|
||||
|
||||
.form-tips {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: #8c939d;
|
||||
}
|
||||
}
|
||||
|
||||
.video-preview-cell {
|
||||
width: 120px;
|
||||
height: 68px;
|
||||
margin: 0 auto;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
background-color: #000;
|
||||
|
||||
.preview-video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
844
src/views/activity/knowledge/index.vue
Normal file
844
src/views/activity/knowledge/index.vue
Normal file
@ -0,0 +1,844 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, computed, watch } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus, Search, Refresh, Upload, Picture } from '@element-plus/icons-vue'
|
||||
import { formatDateTime } from '@/utils/format'
|
||||
import {
|
||||
getKnowledgeList,
|
||||
createKnowledge,
|
||||
updateKnowledge,
|
||||
deleteKnowledge,
|
||||
batchDeleteKnowledge,
|
||||
updateKnowledgeStatus,
|
||||
batchUpdateKnowledgeStatus
|
||||
} from '@/api/activity/knowledge'
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = ref({
|
||||
title: '',
|
||||
category: '',
|
||||
status: ''
|
||||
})
|
||||
|
||||
// 知识分类选项
|
||||
const categoryOptions = [
|
||||
{ label: '湿地科学', value: 'wetland_science' },
|
||||
{ label: '物种图鉴', value: 'species_guide' },
|
||||
{ label: '环境保护', value: 'environmental_protection' },
|
||||
{ label: '生态教育', value: 'ecological_education' }
|
||||
]
|
||||
|
||||
// 状态选项
|
||||
const statusOptions = [
|
||||
{ label: '上架', value: 1 },
|
||||
{ label: '下架', value: 0 }
|
||||
]
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref([])
|
||||
const loading = ref(false)
|
||||
const selectedRows = ref([])
|
||||
|
||||
// 分页配置
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const total = ref(0)
|
||||
|
||||
// 添加一个存储所有数据的数组
|
||||
const allData = ref([])
|
||||
|
||||
// 添加一个计算属性用于过滤数据
|
||||
const filteredData = computed(() => {
|
||||
let result = [...allData.value]
|
||||
|
||||
// 按标题搜索
|
||||
if (searchForm.value.title) {
|
||||
const keyword = searchForm.value.title.toLowerCase()
|
||||
result = result.filter(item =>
|
||||
item.title.toLowerCase().includes(keyword)
|
||||
)
|
||||
}
|
||||
|
||||
// 按分类过滤
|
||||
if (searchForm.value.category) {
|
||||
result = result.filter(item =>
|
||||
item.category === searchForm.value.category
|
||||
)
|
||||
}
|
||||
|
||||
// 按状态过滤
|
||||
if (searchForm.value.status !== '') {
|
||||
result = result.filter(item =>
|
||||
item.status === Number(searchForm.value.status)
|
||||
)
|
||||
}
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
// 获取知识库列表
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getKnowledgeList({
|
||||
page: currentPage.value,
|
||||
pageSize: pageSize.value,
|
||||
category: searchForm.value.category // 只传递分类参数给后端
|
||||
})
|
||||
if (res.success) {
|
||||
// 存储所有数据
|
||||
allData.value = res.data.map(item => ({
|
||||
...item,
|
||||
cover_image: item.cover_image
|
||||
? (item.cover_image.startsWith('http')
|
||||
? item.cover_image
|
||||
: `http://localhost:3000${item.cover_image}`)
|
||||
: '',
|
||||
created_at: formatDateTime(item.created_at),
|
||||
updated_at: formatDateTime(item.updated_at)
|
||||
}))
|
||||
|
||||
// 使用计算属性进行前端过滤
|
||||
const filtered = filteredData.value
|
||||
|
||||
// 分页处理
|
||||
const start = (currentPage.value - 1) * pageSize.value
|
||||
const end = start + pageSize.value
|
||||
tableData.value = filtered.slice(start, end)
|
||||
total.value = filtered.length
|
||||
} else {
|
||||
ElMessage.error(res.message || '获取知识库列表失败')
|
||||
allData.value = []
|
||||
tableData.value = []
|
||||
total.value = 0
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取知识库列表错误:', error)
|
||||
ElMessage.error('获取知识库列表失败')
|
||||
allData.value = []
|
||||
tableData.value = []
|
||||
total.value = 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 修改分页相关的计算逻辑
|
||||
const handleCurrentChange = (val) => {
|
||||
currentPage.value = val
|
||||
const start = (val - 1) * pageSize.value
|
||||
const end = start + pageSize.value
|
||||
tableData.value = filteredData.value.slice(start, end)
|
||||
}
|
||||
|
||||
const handleSizeChange = (val) => {
|
||||
pageSize.value = val
|
||||
currentPage.value = 1
|
||||
const end = pageSize.value
|
||||
tableData.value = filteredData.value.slice(0, end)
|
||||
}
|
||||
|
||||
// 监听搜索条件变化
|
||||
watch([() => searchForm.value.title, () => searchForm.value.category, () => searchForm.value.status], () => {
|
||||
currentPage.value = 1 // 重置到第一页
|
||||
const filtered = filteredData.value
|
||||
tableData.value = filtered.slice(0, pageSize.value)
|
||||
total.value = filtered.length
|
||||
})
|
||||
|
||||
// 处理搜索
|
||||
const handleSearch = () => {
|
||||
currentPage.value = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
// 重置搜索
|
||||
const resetSearch = () => {
|
||||
searchForm.value = {
|
||||
title: '',
|
||||
category: '',
|
||||
status: ''
|
||||
}
|
||||
currentPage.value = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
// 刷新列表
|
||||
const handleRefresh = () => {
|
||||
getList()
|
||||
}
|
||||
|
||||
// 新增/编辑对话框
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('')
|
||||
const formMode = ref('create')
|
||||
const form = ref({
|
||||
title: '',
|
||||
category: '',
|
||||
content: '',
|
||||
cover_image: '',
|
||||
tags: [],
|
||||
status: 1,
|
||||
imageInputType: 'url'
|
||||
})
|
||||
|
||||
// 表单规则
|
||||
const formRules = {
|
||||
title: [
|
||||
{ required: true, message: '请输入标题', trigger: 'blur' },
|
||||
{ min: 2, max: 100, message: '长度在 2 到 100 个字符', trigger: 'blur' }
|
||||
],
|
||||
category: [
|
||||
{ required: true, message: '请选择分类', trigger: 'change' }
|
||||
],
|
||||
content: [
|
||||
{ required: true, message: '请输入内容', trigger: 'blur' }
|
||||
],
|
||||
cover_image: [
|
||||
{
|
||||
required: true,
|
||||
validator: (rule, value, callback) => {
|
||||
if (form.value.imageInputType === 'url' && !value) {
|
||||
callback(new Error('请输入封面图片地址'))
|
||||
} else if (form.value.imageInputType === 'upload' && !value) {
|
||||
callback(new Error('请上传封面图片'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
},
|
||||
trigger: ['blur', 'change']
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// 新增知识
|
||||
const handleAdd = () => {
|
||||
formMode.value = 'create'
|
||||
dialogTitle.value = '新增知识'
|
||||
form.value = {
|
||||
title: '',
|
||||
category: '',
|
||||
content: '',
|
||||
cover_image: '',
|
||||
tags: [],
|
||||
status: 1,
|
||||
imageInputType: 'url'
|
||||
}
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 编辑知识
|
||||
const handleEdit = (row) => {
|
||||
formMode.value = 'edit'
|
||||
dialogTitle.value = '编辑知识'
|
||||
form.value = {
|
||||
id: row.id,
|
||||
title: row.title,
|
||||
category: row.category,
|
||||
content: row.content,
|
||||
cover_image: row.cover_image && !row.cover_image.startsWith('http')
|
||||
? `http://localhost:3000${row.cover_image}`
|
||||
: row.cover_image,
|
||||
tags: row.tags || [],
|
||||
status: row.status,
|
||||
imageInputType: 'upload'
|
||||
}
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const formRef = ref(null)
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
try {
|
||||
const submitData = {
|
||||
title: form.value.title.trim(),
|
||||
category: form.value.category,
|
||||
content: form.value.content.trim(),
|
||||
cover_image: form.value.cover_image.replace('http://localhost:3000', ''),
|
||||
tags: form.value.tags,
|
||||
status: Number(form.value.status)
|
||||
}
|
||||
|
||||
let res
|
||||
if (formMode.value === 'create') {
|
||||
res = await createKnowledge(submitData)
|
||||
} else {
|
||||
const id = form.value.id
|
||||
res = await updateKnowledge(id, submitData)
|
||||
}
|
||||
|
||||
if (res.success) {
|
||||
ElMessage.success(`${formMode.value === 'create' ? '新增' : '编辑'}成功`)
|
||||
dialogVisible.value = false
|
||||
getList()
|
||||
} else {
|
||||
ElMessage.error(res.message || `${formMode.value === 'create' ? '新增' : '编辑'}失败`)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`${formMode.value === 'create' ? '新增' : '编辑'}知识条目错误:`, error)
|
||||
ElMessage.error('操作失败,请检查输入是否正确')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 删除知识
|
||||
const handleDelete = (row) => {
|
||||
ElMessageBox.confirm('确认删除该知识条目吗?此操作不可恢复', '警告', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
try {
|
||||
const res = await deleteKnowledge(row.id)
|
||||
if (res.success) {
|
||||
ElMessage.success('删除成功')
|
||||
getList()
|
||||
} else {
|
||||
ElMessage.error(res.message || '删除失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除知识条目错误:', error)
|
||||
ElMessage.error('删除失败')
|
||||
}
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
// 批量删除知识
|
||||
const handleBatchDelete = () => {
|
||||
if (selectedRows.value.length === 0) {
|
||||
ElMessage.warning('请选择要删除的知识条目')
|
||||
return
|
||||
}
|
||||
|
||||
ElMessageBox.confirm(`确认删除选中的 ${selectedRows.value.length} 个知识条目吗?此操作不可恢复`, '警告', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
try {
|
||||
const ids = selectedRows.value.map(row => row.id)
|
||||
const res = await batchDeleteKnowledge(ids)
|
||||
if (res.success) {
|
||||
ElMessage.success('批量删除成功')
|
||||
getList()
|
||||
} else {
|
||||
ElMessage.error(res.message || '批量删除失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('批量删除知识条目错误:', error)
|
||||
ElMessage.error('批量删除失败')
|
||||
}
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
// 更新知识状态
|
||||
const handleStatusChange = async (row) => {
|
||||
try {
|
||||
const res = await updateKnowledgeStatus(row.id, row.status)
|
||||
if (res.success) {
|
||||
ElMessage.success('状态更新成功')
|
||||
getList()
|
||||
} else {
|
||||
ElMessage.error(res.message || '状态更新失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('更新知识状态错误:', error)
|
||||
ElMessage.error('状态更新失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 批量更新状态
|
||||
const handleBatchUpdateStatus = async (status) => {
|
||||
if (selectedRows.value.length === 0) {
|
||||
ElMessage.warning('请选择要更新的知识条目')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const ids = selectedRows.value.map(row => row.id)
|
||||
const res = await batchUpdateKnowledgeStatus(ids, status)
|
||||
if (res.success) {
|
||||
ElMessage.success('批量更新状态成功')
|
||||
getList()
|
||||
} else {
|
||||
ElMessage.error(res.message || '批量更新状态失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('批量更新知识状态错误:', error)
|
||||
ElMessage.error('批量更新状态失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 表格选择
|
||||
const handleSelectionChange = (rows) => {
|
||||
selectedRows.value = rows
|
||||
}
|
||||
|
||||
// 添加标签选项
|
||||
const tagOptions = [
|
||||
{ label: '湿地保护', value: '湿地保护' },
|
||||
{ label: '生态系统', value: '生态系统' },
|
||||
{ label: '生物多样性', value: '生物多样性' },
|
||||
{ label: '环境教育', value: '环境教育' },
|
||||
{ label: '科普知识', value: '科普知识' },
|
||||
{ label: '物种保护', value: '物种保护' },
|
||||
{ label: '自然观察', value: '自然观察' },
|
||||
{ label: '生态修复', value: '生态修复' }
|
||||
]
|
||||
|
||||
// 移除不需要的标签处理函数
|
||||
const handleTagsInput = (value) => {
|
||||
form.value.tags = value
|
||||
}
|
||||
|
||||
const formatTagsForDisplay = (tags) => {
|
||||
if (Array.isArray(tags)) {
|
||||
return tags.join(', ')
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
// 图片上传相关方法
|
||||
const beforeImageUpload = (file) => {
|
||||
const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png'
|
||||
const isLt2M = file.size / 1024 / 1024 < 2
|
||||
|
||||
if (!isJpgOrPng) {
|
||||
ElMessage.error('只能上传 JPG 或 PNG 格式的图片!')
|
||||
return false
|
||||
}
|
||||
if (!isLt2M) {
|
||||
ElMessage.error('图片大小不能超过 2MB!')
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// 添加图片加载错误处理函数
|
||||
const handleImageError = (e) => {
|
||||
console.error('图片加载失败:', e)
|
||||
ElMessage.warning('图片加载失败,但不影响保存')
|
||||
}
|
||||
|
||||
// 修改上传成功处理函数
|
||||
const handleUploadSuccess = (response) => {
|
||||
if (response.success) {
|
||||
const imageUrl = response.data.url
|
||||
// 确保使用完整的URL
|
||||
form.value.cover_image = imageUrl.startsWith('http')
|
||||
? imageUrl
|
||||
: `http://localhost:3000${imageUrl}`
|
||||
ElMessage.success('图片上传成功')
|
||||
} else {
|
||||
form.value.cover_image = ''
|
||||
ElMessage.error(response.message || '图片上传失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleUploadError = (error) => {
|
||||
console.error('上传图片错误:', error)
|
||||
form.value.cover_image = ''
|
||||
ElMessage.error('上传图片失败,请重试')
|
||||
}
|
||||
|
||||
// 监听图片输入类型变化
|
||||
const handleImageTypeChange = () => {
|
||||
form.value.cover_image = ''
|
||||
}
|
||||
|
||||
// 修改uploadHeaders计算属性
|
||||
const uploadHeaders = computed(() => ({
|
||||
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
||||
}))
|
||||
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="knowledge-management">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>知识库管理</span>
|
||||
<div class="header-btns">
|
||||
<el-button type="primary" :icon="Plus" @click="handleAdd">新增知识</el-button>
|
||||
<el-button :icon="Refresh" @click="handleRefresh">刷新</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 搜索表单 -->
|
||||
<el-form :model="searchForm" inline class="search-form">
|
||||
<el-form-item label="标题" label-width="80px">
|
||||
<el-input
|
||||
v-model="searchForm.title"
|
||||
placeholder="请输入标题"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="分类" label-width="80px">
|
||||
<el-select
|
||||
v-model="searchForm.category"
|
||||
placeholder="请选择分类"
|
||||
clearable
|
||||
>
|
||||
<el-option
|
||||
v-for="item in categoryOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" label-width="80px">
|
||||
<el-select
|
||||
v-model="searchForm.status"
|
||||
placeholder="请选择状态"
|
||||
clearable
|
||||
>
|
||||
<el-option
|
||||
v-for="item in statusOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item class="search-buttons">
|
||||
<el-button type="primary" :icon="Search" @click="handleSearch">搜索</el-button>
|
||||
<el-button @click="resetSearch">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 知识库列表 -->
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="tableData"
|
||||
style="width: 100%"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<el-table-column type="selection" width="55" align="center"/>
|
||||
<el-table-column type="index" label="序号" width="60" align="center"/>
|
||||
<el-table-column prop="title" label="标题" min-width="200" show-overflow-tooltip align="center" />
|
||||
<el-table-column prop="category" label="分类" width="120" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
:type="row.category === 'wetland_science' ? 'success' :
|
||||
row.category === 'species_guide' ? 'warning' :
|
||||
row.category === 'environmental_protection' ? 'info' :
|
||||
row.category === 'ecological_education' ? 'danger' : ''"
|
||||
>
|
||||
{{ categoryOptions.find(item => item.value === row.category)?.label || row.category }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="view_count" label="浏览量" width="100" align="center"/>
|
||||
<el-table-column prop="like_count" label="点赞数" width="100" align="center"/>
|
||||
<el-table-column prop="status" label="状态" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 1 ? 'success' : 'info'">
|
||||
{{ row.status === 1 ? '上架' : '下架' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="created_at" label="创建时间" width="160" align="center"/>
|
||||
<el-table-column prop="updated_at" label="更新时间" width="160" align="center"/>
|
||||
<el-table-column prop="tags" label="标签" min-width="200" show-overflow-tooltip align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
v-for="tag in row.tags"
|
||||
:key="tag"
|
||||
class="mx-1"
|
||||
size="small"
|
||||
>
|
||||
{{ tag }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="250" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页器 -->
|
||||
<div class="pagination-container">
|
||||
<el-pagination
|
||||
v-model:current-page="currentPage"
|
||||
v-model:page-size="pageSize"
|
||||
:total="total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
background
|
||||
layout="total, sizes, prev, pager, next"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 新增/编辑对话框 -->
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogTitle"
|
||||
width="800px"
|
||||
destroy-on-close
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="formRules"
|
||||
label-width="100px"
|
||||
>
|
||||
<el-form-item label="标题" prop="title">
|
||||
<el-input
|
||||
v-model="form.title"
|
||||
placeholder="请输入标题"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="分类" prop="category">
|
||||
<el-select
|
||||
v-model="form.category"
|
||||
placeholder="请选择分类"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in categoryOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="内容" prop="content">
|
||||
<el-input
|
||||
v-model="form.content"
|
||||
type="textarea"
|
||||
:rows="10"
|
||||
placeholder="请输入内容"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="封面图片" prop="cover_image">
|
||||
<div class="image-input-container">
|
||||
<el-radio-group v-model="form.imageInputType" class="mb-4" @change="handleImageTypeChange">
|
||||
<el-radio :value="'url'">输入图片地址</el-radio>
|
||||
<el-radio :value="'upload'">上传图片</el-radio>
|
||||
</el-radio-group>
|
||||
|
||||
<template v-if="form.imageInputType === 'url'">
|
||||
<el-input
|
||||
v-model="form.cover_image"
|
||||
placeholder="请输入封面图片地址"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<el-upload
|
||||
class="image-upload"
|
||||
action="http://localhost:3000/api/education/knowledge/upload"
|
||||
:headers="uploadHeaders"
|
||||
name="cover_image"
|
||||
:show-file-list="false"
|
||||
:on-success="handleUploadSuccess"
|
||||
:on-error="handleUploadError"
|
||||
:before-upload="beforeImageUpload"
|
||||
>
|
||||
<template v-if="form.cover_image">
|
||||
<img
|
||||
:src="form.cover_image"
|
||||
class="uploaded-image"
|
||||
crossorigin="anonymous"
|
||||
@error="handleImageError"
|
||||
/>
|
||||
</template>
|
||||
<div v-else class="upload-placeholder">
|
||||
<el-icon class="image-upload-icon"><Upload /></el-icon>
|
||||
<div class="upload-text">点击上传图片</div>
|
||||
</div>
|
||||
</el-upload>
|
||||
<div class="form-tips">支持 jpg、png 格式,大小不超过 2MB</div>
|
||||
</template>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="标签">
|
||||
<el-select
|
||||
v-model="form.tags"
|
||||
multiple
|
||||
filterable
|
||||
placeholder="请选择标签"
|
||||
style="width: 100%"
|
||||
:multiple-limit="5"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in tagOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
<div class="form-tips">最多可选择5个标签</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" v-if="formMode === 'edit'">
|
||||
<el-radio-group v-model="form.status">
|
||||
<el-radio
|
||||
v-for="item in statusOptions"
|
||||
:key="item.value"
|
||||
:value="item.value"
|
||||
>{{ item.label }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">确定</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.knowledge-management {
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
span {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.header-btns {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.search-form {
|
||||
margin-bottom: 24px;
|
||||
padding: 24px;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 16px;
|
||||
margin-right: 24px;
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-input),
|
||||
:deep(.el-select) {
|
||||
width: 220px;
|
||||
}
|
||||
|
||||
.search-buttons {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.form-tips {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
margin-top: 4px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.mx-1 {
|
||||
margin: 0 4px;
|
||||
}
|
||||
|
||||
:deep(.el-select) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.el-tag {
|
||||
margin: 2px 4px;
|
||||
}
|
||||
|
||||
.image-input-container {
|
||||
.mb-4 {
|
||||
margin-bottom: 16px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.image-upload {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
border: 1px dashed var(--el-border-color);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: var(--el-transition-duration);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.upload-placeholder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.image-upload-icon {
|
||||
font-size: 28px;
|
||||
color: #8c939d;
|
||||
}
|
||||
|
||||
.upload-text {
|
||||
color: #8c939d;
|
||||
font-size: 14px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.uploaded-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.image-error {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
color: #909399;
|
||||
|
||||
.el-icon {
|
||||
font-size: 24px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.uploaded-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
757
src/views/activity/study/index.vue
Normal file
757
src/views/activity/study/index.vue
Normal file
@ -0,0 +1,757 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, watch } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus, Search, Refresh } from '@element-plus/icons-vue'
|
||||
import { formatDateTime } from '@/utils/format'
|
||||
import {
|
||||
getActivityList,
|
||||
createActivity,
|
||||
updateActivity,
|
||||
cancelActivity,
|
||||
batchCancelActivities,
|
||||
updateActivityStatus,
|
||||
checkActivityCapacity
|
||||
} from '@/api/activity/study'
|
||||
|
||||
// 格式化日期时间
|
||||
const formatDate = (date) => {
|
||||
if (!date) return ''
|
||||
const d = new Date(date)
|
||||
const pad = (num) => (num < 10 ? `0${num}` : num)
|
||||
|
||||
const year = d.getFullYear()
|
||||
const month = pad(d.getMonth() + 1)
|
||||
const day = pad(d.getDate())
|
||||
const hours = pad(d.getHours())
|
||||
const minutes = pad(d.getMinutes())
|
||||
const seconds = pad(d.getSeconds())
|
||||
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
|
||||
}
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = ref({
|
||||
title: '',
|
||||
category: '',
|
||||
status: ''
|
||||
})
|
||||
|
||||
// 研学活动类型选项
|
||||
const categoryOptions = [
|
||||
{ label: '实地考察', value: 'field_study' },
|
||||
{ label: '科普教育', value: 'science' },
|
||||
{ label: '文化体验', value: 'culture' },
|
||||
{ label: '环保实践', value: 'environment' }
|
||||
]
|
||||
|
||||
// 状态选项
|
||||
const statusOptions = [
|
||||
{ label: '报名中', value: 1 },
|
||||
{ label: '进行中', value: 2 },
|
||||
{ label: '已结束', value: 0 }
|
||||
]
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref([])
|
||||
const allData = ref([]) // 添加一个存储所有数据的数组
|
||||
const loading = ref(false)
|
||||
const selectedRows = ref([])
|
||||
|
||||
// 分页配置
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const total = ref(0)
|
||||
|
||||
// 获取研学活动列表
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getActivityList()
|
||||
if (res.success) {
|
||||
// 保存所有数据
|
||||
allData.value = res.data.map(item => ({
|
||||
id: item.id,
|
||||
title: item.title,
|
||||
category: item.category,
|
||||
start_time: item.start_time,
|
||||
end_time: item.end_time,
|
||||
location: item.location,
|
||||
capacity: item.capacity,
|
||||
enrolled: item.enrolled_count,
|
||||
description: item.description,
|
||||
requirements: item.requirements,
|
||||
cost: item.cost,
|
||||
status: item.status,
|
||||
created_at: item.created_at,
|
||||
updated_at: item.updated_at,
|
||||
activity_code: item.activity_code
|
||||
}))
|
||||
// 前端搜索和分页
|
||||
filterAndPaginateData()
|
||||
} else {
|
||||
ElMessage.error(res.message || '获取研学活动列表失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取研学活动列表错误:', error)
|
||||
ElMessage.error('获取研学活动列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 前端搜索和分页
|
||||
const filterAndPaginateData = () => {
|
||||
// 1. 先进行搜索过滤
|
||||
let filteredData = [...allData.value]
|
||||
|
||||
// 按活动名称搜索
|
||||
if (searchForm.value.title) {
|
||||
const keyword = searchForm.value.title.toLowerCase()
|
||||
filteredData = filteredData.filter(item =>
|
||||
item.title.toLowerCase().includes(keyword)
|
||||
)
|
||||
}
|
||||
|
||||
// 按活动类型过滤
|
||||
if (searchForm.value.category) {
|
||||
filteredData = filteredData.filter(item =>
|
||||
item.category === searchForm.value.category
|
||||
)
|
||||
}
|
||||
|
||||
// 按状态过滤 - 只在状态值不为空字符串时进行过滤
|
||||
if (searchForm.value.status !== '') {
|
||||
filteredData = filteredData.filter(item =>
|
||||
item.status === Number(searchForm.value.status)
|
||||
)
|
||||
}
|
||||
|
||||
// 按id正序排序
|
||||
filteredData.sort((a, b) => a.id - b.id)
|
||||
|
||||
// 2. 更新总数
|
||||
total.value = filteredData.length
|
||||
|
||||
// 3. 进行分页
|
||||
const start = (currentPage.value - 1) * pageSize.value
|
||||
const end = start + pageSize.value
|
||||
tableData.value = filteredData.slice(start, end)
|
||||
}
|
||||
|
||||
// 处理搜索
|
||||
const handleSearch = () => {
|
||||
currentPage.value = 1 // 重置到第一页
|
||||
filterAndPaginateData()
|
||||
}
|
||||
|
||||
// 重置搜索
|
||||
const resetSearch = () => {
|
||||
searchForm.value = {
|
||||
title: '',
|
||||
category: '',
|
||||
status: ''
|
||||
}
|
||||
currentPage.value = 1
|
||||
filterAndPaginateData()
|
||||
}
|
||||
|
||||
// 处理清空操作
|
||||
const handleClear = (field) => {
|
||||
// 当任何字段被清空时,都执行重置操作
|
||||
resetSearch()
|
||||
}
|
||||
|
||||
// 监听分页变化
|
||||
watch([currentPage, pageSize], () => {
|
||||
filterAndPaginateData()
|
||||
})
|
||||
|
||||
// 监听搜索条件变化
|
||||
watch(searchForm, (newVal, oldVal) => {
|
||||
// 如果是状态字段发生变化且变为空字符串,说明是清空操作
|
||||
if (oldVal.status !== '' && newVal.status === '') {
|
||||
// 重置页码
|
||||
currentPage.value = 1
|
||||
// 直接更新数据,不进行状态过滤
|
||||
filterAndPaginateData()
|
||||
return
|
||||
}
|
||||
|
||||
// 其他情况下重置到第一页并更新数据
|
||||
currentPage.value = 1
|
||||
filterAndPaginateData()
|
||||
}, { deep: true })
|
||||
|
||||
// 刷新列表
|
||||
const handleRefresh = () => {
|
||||
getList()
|
||||
}
|
||||
|
||||
// 新增/编辑对话框
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('')
|
||||
const formMode = ref('create')
|
||||
const form = ref({
|
||||
title: '',
|
||||
category: 'field_study',
|
||||
start_time: '',
|
||||
end_time: '',
|
||||
location: '',
|
||||
capacity: 30,
|
||||
description: '',
|
||||
requirements: '',
|
||||
cost: 50.00,
|
||||
status: 1
|
||||
})
|
||||
|
||||
// 表单规则
|
||||
const formRules = {
|
||||
title: [
|
||||
{ required: true, message: '请输入活动名称', trigger: 'blur' },
|
||||
{ min: 2, max: 50, message: '长度在 2 到 50 个字符', trigger: 'blur' }
|
||||
],
|
||||
location: [
|
||||
{ required: true, message: '请输入活动地点', trigger: 'blur' }
|
||||
],
|
||||
capacity: [
|
||||
{ required: true, message: '请输入人数上限', trigger: 'blur' },
|
||||
{ type: 'number', min: 1, message: '人数上限必须大于0', trigger: 'blur' }
|
||||
],
|
||||
start_time: [
|
||||
{ required: true, message: '请选择开始时间', trigger: 'change' }
|
||||
],
|
||||
end_time: [
|
||||
{ required: true, message: '请选择结束时间', trigger: 'change' }
|
||||
],
|
||||
cost: [
|
||||
{ required: true, message: '请输入活动费用', trigger: 'blur' },
|
||||
{ type: 'number', min: 0, message: '费用不能小于0', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
// 新增研学活动
|
||||
const handleAdd = () => {
|
||||
formMode.value = 'create'
|
||||
dialogTitle.value = '新增研学活动'
|
||||
form.value = {
|
||||
title: '',
|
||||
category: 'field_study',
|
||||
start_time: '',
|
||||
end_time: '',
|
||||
location: '',
|
||||
capacity: 30,
|
||||
description: '',
|
||||
requirements: '',
|
||||
cost: 50.00,
|
||||
status: 1
|
||||
}
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 编辑研学活动
|
||||
const handleEdit = (row) => {
|
||||
formMode.value = 'edit'
|
||||
dialogTitle.value = '编辑研学活动'
|
||||
form.value = {
|
||||
...row,
|
||||
category: row.category || 'field_study'
|
||||
}
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const formRef = ref(null)
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
try {
|
||||
// 格式化提交数据
|
||||
const submitData = {
|
||||
title: form.value.title.trim(),
|
||||
category: form.value.category,
|
||||
start_time: form.value.start_time ? formatDate(new Date(form.value.start_time)) : '',
|
||||
end_time: form.value.end_time ? formatDate(new Date(form.value.end_time)) : '',
|
||||
location: form.value.location.trim(),
|
||||
capacity: Number(form.value.capacity),
|
||||
description: (form.value.description || '').trim(),
|
||||
requirements: (form.value.requirements || '').trim(),
|
||||
cost: Number(form.value.cost),
|
||||
status: Number(form.value.status)
|
||||
}
|
||||
|
||||
// 验证必填字段
|
||||
if (!submitData.title || !submitData.start_time || !submitData.end_time || !submitData.location) {
|
||||
ElMessage.error('请填写必填字段')
|
||||
return
|
||||
}
|
||||
|
||||
// 验证时间
|
||||
const startTime = new Date(submitData.start_time)
|
||||
const endTime = new Date(submitData.end_time)
|
||||
const now = new Date()
|
||||
|
||||
// 验证结束时间必须大于开始时间
|
||||
if (endTime <= startTime) {
|
||||
ElMessage.error('结束时间必须大于开始时间')
|
||||
return
|
||||
}
|
||||
|
||||
// 编辑模式下的时间验证
|
||||
if (formMode.value === 'edit') {
|
||||
// 计算距离结束时间还有多少小时
|
||||
const hoursUntilEnd = (endTime - now) / (1000 * 60 * 60)
|
||||
|
||||
// 如果结束时间早于当前时间或距离结束不足2小时,直接拦截
|
||||
if (endTime < now || hoursUntilEnd <= 2) {
|
||||
ElMessage.warning(endTime < now ? '活动已结束,无法更新活动信息' : '距离活动结束不足2小时,无法更新活动信息')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
let res
|
||||
if (formMode.value === 'create') {
|
||||
res = await createActivity(submitData)
|
||||
} else {
|
||||
const id = form.value.id
|
||||
res = await updateActivity(id, submitData)
|
||||
}
|
||||
|
||||
if (res.success) {
|
||||
ElMessage.success(`${formMode.value === 'create' ? '新增' : '编辑'}成功`)
|
||||
dialogVisible.value = false
|
||||
getList()
|
||||
} else {
|
||||
ElMessage.error(res.message || `${formMode.value === 'create' ? '新增' : '编辑'}失败`)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`${formMode.value === 'create' ? '新增' : '编辑'}研学活动错误:`, error)
|
||||
ElMessage.error('操作失败,请检查输入是否正确')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 取消活动
|
||||
const handleCancel = (row) => {
|
||||
// 检查活动状态
|
||||
if (row.status === 0) {
|
||||
ElMessage.warning('已结束的活动无法取消')
|
||||
return
|
||||
}
|
||||
|
||||
ElMessageBox.confirm('确认取消该研学活动吗?此操作不可恢复', '警告', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
try {
|
||||
const res = await cancelActivity(row.id)
|
||||
if (res.success) {
|
||||
ElMessage.success('取消成功')
|
||||
getList()
|
||||
} else {
|
||||
ElMessage.error(res.message || '取消失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('取消研学活动错误:', error)
|
||||
ElMessage.error('取消失败')
|
||||
}
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
// 批量取消活动
|
||||
const handleBatchCancel = () => {
|
||||
if (selectedRows.value.length === 0) {
|
||||
ElMessage.warning('请选择要取消的活动')
|
||||
return
|
||||
}
|
||||
|
||||
// 检查选中的活动是否有已结束的
|
||||
const endedActivities = selectedRows.value.filter(row => row.status === 0)
|
||||
if (endedActivities.length > 0) {
|
||||
ElMessage.warning('已结束的活动无法取消')
|
||||
return
|
||||
}
|
||||
|
||||
// 获取可以取消的活动ID列表
|
||||
const cancelableActivities = selectedRows.value.filter(row => row.status !== 0)
|
||||
if (cancelableActivities.length === 0) {
|
||||
ElMessage.warning('所选活动都无法取消')
|
||||
return
|
||||
}
|
||||
|
||||
ElMessageBox.confirm(`确认取消选中的 ${cancelableActivities.length} 个研学活动吗?此操作不可恢复`, '警告', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
try {
|
||||
const ids = cancelableActivities.map(row => row.id)
|
||||
const res = await batchCancelActivities(ids)
|
||||
if (res.success) {
|
||||
ElMessage.success('批量取消成功')
|
||||
getList()
|
||||
} else {
|
||||
ElMessage.error(res.message || '批量取消失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('批量取消研学活动错误:', error)
|
||||
ElMessage.error('批量取消失败')
|
||||
}
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
// 更新活动状态
|
||||
const handleStatusChange = async (row) => {
|
||||
try {
|
||||
const res = await updateActivityStatus(row.id, row.status)
|
||||
if (res.success) {
|
||||
ElMessage.success('状态更新成功')
|
||||
getList()
|
||||
} else {
|
||||
ElMessage.error(res.message || '状态更新失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('更新研学活动状态错误:', error)
|
||||
ElMessage.error('状态更新失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 检查活动容量
|
||||
const checkCapacity = async (row) => {
|
||||
try {
|
||||
const res = await checkActivityCapacity(row.id)
|
||||
if (res.success) {
|
||||
ElMessage.success(`当前报名人数:${res.data.currentParticipants}/${res.data.maxParticipants}`)
|
||||
} else {
|
||||
ElMessage.error(res.message || '检查容量失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('检查研学活动容量错误:', error)
|
||||
ElMessage.error('检查容量失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 表格选择
|
||||
const handleSelectionChange = (rows) => {
|
||||
selectedRows.value = rows
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="study-management">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>研学管理</span>
|
||||
<div class="header-btns">
|
||||
<el-button type="primary" :icon="Plus" @click="handleAdd">新增活动</el-button>
|
||||
<el-button :icon="Refresh" @click="handleRefresh">刷新</el-button>
|
||||
<el-button
|
||||
v-if="selectedRows.length > 0"
|
||||
type="danger"
|
||||
@click="handleBatchCancel"
|
||||
>批量取消</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 搜索表单 -->
|
||||
<el-form :model="searchForm" inline class="search-form">
|
||||
<el-form-item label="活动名称" label-width="80px">
|
||||
<el-input
|
||||
v-model="searchForm.title"
|
||||
placeholder="请输入活动名称"
|
||||
clearable
|
||||
@clear="handleClear('title')"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="活动类型" label-width="80px">
|
||||
<el-select
|
||||
v-model="searchForm.category"
|
||||
placeholder="请选择类型"
|
||||
clearable
|
||||
@clear="handleClear('category')"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in categoryOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" label-width="80px">
|
||||
<el-select
|
||||
v-model="searchForm.status"
|
||||
placeholder="请选择状态"
|
||||
clearable
|
||||
@clear="handleClear('status')"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in statusOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item class="search-buttons">
|
||||
<el-button type="primary" :icon="Search" @click="handleSearch">搜索</el-button>
|
||||
<el-button @click="resetSearch">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 研学活动列表 -->
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="tableData"
|
||||
style="width: 100%"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<el-table-column type="selection" :selectable="row => row.status !== 0" width="55" align="center"/>
|
||||
<el-table-column type="index" label="序号" width="60" align="center"/>
|
||||
<el-table-column prop="title" label="活动名称" min-width="150" show-overflow-tooltip align="center"/>
|
||||
<el-table-column prop="category" label="活动类型" width="120" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
:type="row.category === 'field_study' ? 'success' :
|
||||
row.category === 'science' ? 'warning' :
|
||||
row.category === 'culture' ? 'info' :
|
||||
row.category === 'environment' ? 'danger' : ''"
|
||||
>
|
||||
{{ categoryOptions.find(item => item.value === row.category)?.label || '未知' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="capacity" label="人数上限" width="100" align="center"/>
|
||||
<el-table-column prop="enrolled" label="已报名" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link @click="checkCapacity(row)">
|
||||
{{ row.enrolled }}/{{ row.capacity }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="location" label="活动地点" width="150" show-overflow-tooltip align="center"/>
|
||||
<el-table-column prop="start_time" label="开始时间" width="160" align="center">
|
||||
<template #default="{ row }">
|
||||
{{ formatDateTime(row.start_time) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="end_time" label="结束时间" width="160" align="center">
|
||||
<template #default="{ row }">
|
||||
{{ formatDateTime(row.end_time) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="cost" label="费用" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
¥{{ row.cost }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 1 ? 'success' :
|
||||
row.status === 2 ? 'warning' : 'info'">
|
||||
{{ row.status === 1 ? '报名中' :
|
||||
row.status === 2 ? '进行中' : '已结束' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="250" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
@click="handleEdit(row)"
|
||||
>编辑</el-button>
|
||||
<el-button
|
||||
v-if="row.status !== 0"
|
||||
type="danger"
|
||||
link
|
||||
@click="handleCancel(row)"
|
||||
>取消</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页器 -->
|
||||
<div class="pagination-container">
|
||||
<el-pagination
|
||||
v-model:current-page="currentPage"
|
||||
v-model:page-size="pageSize"
|
||||
:total="total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
background
|
||||
layout="total, sizes, prev, pager, next"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 新增/编辑对话框 -->
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogTitle"
|
||||
width="800px"
|
||||
destroy-on-close
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="formRules"
|
||||
label-width="100px"
|
||||
>
|
||||
<el-form-item label="活动名称" prop="title">
|
||||
<el-input
|
||||
v-model="form.title"
|
||||
placeholder="请输入活动名称"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="活动类型" prop="category">
|
||||
<el-select
|
||||
v-model="form.category"
|
||||
placeholder="请选择活动类型"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in categoryOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="活动地点" prop="location">
|
||||
<el-input
|
||||
v-model="form.location"
|
||||
placeholder="请输入活动地点"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="活动时间" required>
|
||||
<el-col :span="11">
|
||||
<el-form-item prop="start_time">
|
||||
<el-date-picker
|
||||
v-model="form.start_time"
|
||||
type="datetime"
|
||||
placeholder="开始时间"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="2" class="text-center">
|
||||
<span class="text-gray-500">-</span>
|
||||
</el-col>
|
||||
<el-col :span="11">
|
||||
<el-form-item prop="end_time">
|
||||
<el-date-picker
|
||||
v-model="form.end_time"
|
||||
type="datetime"
|
||||
placeholder="结束时间"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-form-item>
|
||||
<el-form-item label="活动费用">
|
||||
<el-input
|
||||
v-model="form.cost"
|
||||
placeholder="请输入活动费用"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="活动描述">
|
||||
<el-input
|
||||
v-model="form.description"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
placeholder="请输入活动描述"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="报名要求">
|
||||
<el-input
|
||||
v-model="form.requirements"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
placeholder="请输入报名要求"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="活动状态">
|
||||
<el-select v-model="form.status">
|
||||
<el-option
|
||||
v-for="item in statusOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">确定</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.study-management {
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
span {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.header-btns {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.search-form {
|
||||
margin-bottom: 24px;
|
||||
padding: 24px;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 16px;
|
||||
margin-right: 24px;
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-input),
|
||||
:deep(.el-select) {
|
||||
width: 220px;
|
||||
}
|
||||
|
||||
.search-buttons {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
1101
src/views/feedback/satisfaction/index.vue
Normal file
1101
src/views/feedback/satisfaction/index.vue
Normal file
File diff suppressed because it is too large
Load Diff
645
src/views/feedback/suggestions/index.vue
Normal file
645
src/views/feedback/suggestions/index.vue
Normal file
@ -0,0 +1,645 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, computed, watch } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Search, Refresh } from '@element-plus/icons-vue'
|
||||
import { formatDateTime } from '@/utils/format'
|
||||
import { getFeedbackList, getFeedbackDetail, updateFeedbackStatus } from '@/api/feedback'
|
||||
|
||||
// 反馈类型选项
|
||||
const feedbackTypeOptions = [
|
||||
{ label: '设施损坏', value: 'facility_damage' },
|
||||
{ label: '环境问题', value: 'environment_issue' },
|
||||
{ label: '安全隐患', value: 'safety_hazard' },
|
||||
{ label: '服务建议', value: 'service_suggestion' },
|
||||
{ label: '其他', value: 'other' }
|
||||
]
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = ref({
|
||||
keyword: '',
|
||||
status: '',
|
||||
feedback_type: '',
|
||||
date_range: []
|
||||
})
|
||||
|
||||
// 状态选项
|
||||
const statusOptions = [
|
||||
{ label: '待处理', value: 0 },
|
||||
{ label: '处理中', value: 1 },
|
||||
{ label: '已处理', value: 2 },
|
||||
{ label: '已关闭', value: 3 }
|
||||
]
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref([])
|
||||
const loading = ref(false)
|
||||
const allData = ref([]) // 添加存储所有数据的数组
|
||||
|
||||
// 添加计算属性用于过滤数据
|
||||
const filteredData = computed(() => {
|
||||
let result = [...allData.value]
|
||||
|
||||
// 按关键词搜索
|
||||
if (searchForm.value.keyword) {
|
||||
const keyword = searchForm.value.keyword.toLowerCase()
|
||||
result = result.filter(item =>
|
||||
item.title?.toLowerCase().includes(keyword) ||
|
||||
item.content?.toLowerCase().includes(keyword)
|
||||
)
|
||||
}
|
||||
|
||||
// 按反馈类型过滤
|
||||
if (searchForm.value.feedback_type) {
|
||||
result = result.filter(item =>
|
||||
item.feedback_type === searchForm.value.feedback_type
|
||||
)
|
||||
}
|
||||
|
||||
// 按状态过滤
|
||||
if (searchForm.value.status !== '') {
|
||||
result = result.filter(item =>
|
||||
item.status === Number(searchForm.value.status)
|
||||
)
|
||||
}
|
||||
|
||||
// 按日期范围过滤
|
||||
if (searchForm.value.date_range?.length === 2) {
|
||||
const startDate = new Date(searchForm.value.date_range[0])
|
||||
const endDate = new Date(searchForm.value.date_range[1])
|
||||
result = result.filter(item => {
|
||||
const itemDate = new Date(item.created_at)
|
||||
return itemDate >= startDate && itemDate <= endDate
|
||||
})
|
||||
}
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
// 分页配置
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const total = ref(0)
|
||||
|
||||
// 获取意见反馈列表
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getFeedbackList()
|
||||
if (res.success) {
|
||||
// 存储所有数据
|
||||
allData.value = res.data.list.map(item => ({
|
||||
...item,
|
||||
image_urls: Array.isArray(item.image_urls) ? item.image_urls :
|
||||
typeof item.image_urls === 'string' ?
|
||||
(item.image_urls.startsWith('[') ? JSON.parse(item.image_urls) : [item.image_urls]) :
|
||||
[]
|
||||
}))
|
||||
|
||||
// 使用计算属性进行前端过滤
|
||||
const filtered = filteredData.value
|
||||
|
||||
// 分页处理
|
||||
const start = (currentPage.value - 1) * pageSize.value
|
||||
const end = start + pageSize.value
|
||||
tableData.value = filtered.slice(start, end)
|
||||
total.value = filtered.length
|
||||
} else {
|
||||
ElMessage.error(res.message || '获取列表失败')
|
||||
allData.value = []
|
||||
tableData.value = []
|
||||
total.value = 0
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取意见反馈列表错误:', error)
|
||||
ElMessage.error('获取列表失败')
|
||||
allData.value = []
|
||||
tableData.value = []
|
||||
total.value = 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 修改分页相关的计算逻辑
|
||||
const handleSizeChange = (val) => {
|
||||
pageSize.value = val
|
||||
currentPage.value = 1
|
||||
const end = pageSize.value
|
||||
tableData.value = filteredData.value.slice(0, end)
|
||||
}
|
||||
|
||||
const handleCurrentChange = (val) => {
|
||||
currentPage.value = val
|
||||
const start = (val - 1) * pageSize.value
|
||||
const end = start + pageSize.value
|
||||
tableData.value = filteredData.value.slice(start, end)
|
||||
}
|
||||
|
||||
// 处理搜索
|
||||
const handleSearch = () => {
|
||||
currentPage.value = 1
|
||||
const filtered = filteredData.value
|
||||
tableData.value = filtered.slice(0, pageSize.value)
|
||||
total.value = filtered.length
|
||||
}
|
||||
|
||||
// 重置搜索
|
||||
const resetSearch = () => {
|
||||
searchForm.value = {
|
||||
keyword: '',
|
||||
status: '',
|
||||
feedback_type: '',
|
||||
date_range: []
|
||||
}
|
||||
currentPage.value = 1
|
||||
const filtered = filteredData.value
|
||||
tableData.value = filtered.slice(0, pageSize.value)
|
||||
total.value = filtered.length
|
||||
}
|
||||
|
||||
// 添加监听搜索条件变化
|
||||
watch([
|
||||
() => searchForm.value.keyword,
|
||||
() => searchForm.value.feedback_type,
|
||||
() => searchForm.value.status,
|
||||
() => searchForm.value.date_range
|
||||
], () => {
|
||||
currentPage.value = 1 // 重置到第一页
|
||||
const filtered = filteredData.value
|
||||
tableData.value = filtered.slice(0, pageSize.value)
|
||||
total.value = filtered.length
|
||||
}, { deep: true })
|
||||
|
||||
// 刷新列表
|
||||
const handleRefresh = () => {
|
||||
getList()
|
||||
}
|
||||
|
||||
// 回复对话框
|
||||
const replyDialogVisible = ref(false)
|
||||
const currentFeedback = ref(null)
|
||||
const replyForm = ref({
|
||||
reply: '',
|
||||
status: 2 // 默认更新为已处理状态
|
||||
})
|
||||
|
||||
// 打开回复对话框
|
||||
const handleReply = async (row) => {
|
||||
try {
|
||||
const res = await getFeedbackDetail(row.id)
|
||||
if (res.success) {
|
||||
currentFeedback.value = res.data
|
||||
replyForm.value.reply = res.data.handling_result || ''
|
||||
replyForm.value.status = res.data.status
|
||||
replyDialogVisible.value = true
|
||||
} else {
|
||||
ElMessage.error(res.message || '获取反馈详情失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取反馈详情错误:', error)
|
||||
ElMessage.error('获取反馈详情失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 监听状态变化
|
||||
const handleStatusChange = (newStatus) => {
|
||||
// 如果当前状态是已处理(2),且试图改为处理中(1)
|
||||
if (currentFeedback.value.status === 2 && newStatus === 1) {
|
||||
ElMessage.warning('已处理的反馈不能改为处理中状态')
|
||||
replyForm.value.status = currentFeedback.value.status
|
||||
return
|
||||
}
|
||||
replyForm.value.status = newStatus
|
||||
}
|
||||
|
||||
// 提交回复
|
||||
const handleSubmitReply = async () => {
|
||||
// 当状态为"已处理"时,必须填写处理结果
|
||||
if (replyForm.value.status === 2 && !replyForm.value.reply.trim()) {
|
||||
ElMessage.warning('请输入处理结果')
|
||||
return
|
||||
}
|
||||
|
||||
// 如果状态没有变化且处理结果也没有变化,直接关闭对话框
|
||||
if (replyForm.value.status === currentFeedback.value.status &&
|
||||
replyForm.value.reply === currentFeedback.value.handling_result) {
|
||||
replyDialogVisible.value = false
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await updateFeedbackStatus(
|
||||
currentFeedback.value.id,
|
||||
replyForm.value.status,
|
||||
replyForm.value.status === 2 ? replyForm.value.reply : undefined
|
||||
)
|
||||
if (res.success) {
|
||||
ElMessage.success('处理成功')
|
||||
replyDialogVisible.value = false
|
||||
getList()
|
||||
} else {
|
||||
ElMessage.error(res.message || '处理失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('提交回复错误:', error)
|
||||
ElMessage.error('处理失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭反馈
|
||||
const handleClose = (row) => {
|
||||
ElMessageBox.confirm('确认关闭该反馈吗?', '警告', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
try {
|
||||
const res = await updateFeedbackStatus(row.id, 3) // 更新为已关闭状态
|
||||
if (res.success) {
|
||||
ElMessage.success('关闭成功')
|
||||
getList()
|
||||
} else {
|
||||
ElMessage.error(res.message || '关闭失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('关闭反馈错误:', error)
|
||||
ElMessage.error('关闭失败')
|
||||
}
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
// 图片预览
|
||||
const previewVisible = ref(false)
|
||||
const previewImages = ref([])
|
||||
const currentPreviewIndex = ref(0)
|
||||
|
||||
const handlePreviewImage = (urls, index = 0) => {
|
||||
if (!urls || urls.length === 0) return
|
||||
previewImages.value = typeof urls === 'string' ? JSON.parse(urls) : urls
|
||||
currentPreviewIndex.value = index
|
||||
previewVisible.value = true
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="feedback-suggestions">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>意见反馈</span>
|
||||
<div class="header-btns">
|
||||
<el-button :icon="Refresh" @click="handleRefresh">刷新</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 搜索表单 -->
|
||||
<el-form :model="searchForm" inline class="search-form">
|
||||
<el-form-item label="关键词" label-width="80px">
|
||||
<el-input
|
||||
v-model="searchForm.keyword"
|
||||
placeholder="请输入标题或内容"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="反馈类型" label-width="80px">
|
||||
<el-select
|
||||
v-model="searchForm.feedback_type"
|
||||
placeholder="请选择类型"
|
||||
clearable
|
||||
>
|
||||
<el-option
|
||||
v-for="item in feedbackTypeOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" label-width="80px">
|
||||
<el-select
|
||||
v-model="searchForm.status"
|
||||
placeholder="请选择状态"
|
||||
clearable
|
||||
>
|
||||
<el-option
|
||||
v-for="item in statusOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="提交时间" label-width="80px">
|
||||
<el-date-picker
|
||||
v-model="searchForm.date_range"
|
||||
type="daterange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item class="search-buttons">
|
||||
<el-button type="primary" :icon="Search" @click="handleSearch">搜索</el-button>
|
||||
<el-button @click="resetSearch">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 反馈列表 -->
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="tableData"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-table-column type="index" label="序号" width="60" align="center"/>
|
||||
<el-table-column prop="feedback_type" label="反馈类型" width="120" align="center">
|
||||
|
||||
</el-table-column>
|
||||
<el-table-column prop="title" label="标题" min-width="150" show-overflow-tooltip/>
|
||||
<el-table-column prop="content" label="反馈内容" min-width="200" show-overflow-tooltip/>
|
||||
<el-table-column prop="location_description" label="位置" width="150" show-overflow-tooltip/>
|
||||
<el-table-column label="图片" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
v-if="row.image_urls && row.image_urls.length > 0"
|
||||
type="primary"
|
||||
link
|
||||
@click="handlePreviewImage(row.image_urls)"
|
||||
>
|
||||
查看图片({{ row.image_urls.length }})
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="user_name" label="提交人" width="120" align="center"/>
|
||||
<el-table-column prop="status" label="状态" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
:type="row.status === 0 ? 'info' :
|
||||
row.status === 1 ? 'warning' :
|
||||
row.status === 2 ? 'success' :
|
||||
'danger'"
|
||||
>
|
||||
{{ statusOptions.find(item => item.value === row.status)?.label }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="handler_name" label="处理人" width="120" align="center"/>
|
||||
<el-table-column prop="handling_time" label="处理时间" width="160" align="center">
|
||||
<template #default="{ row }">
|
||||
{{ row.handling_time ? formatDateTime(row.handling_time) : '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="created_at" label="提交时间" width="160" align="center">
|
||||
<template #default="{ row }">
|
||||
{{ formatDateTime(row.created_at) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
v-if="row.status !== 3"
|
||||
type="primary"
|
||||
link
|
||||
@click="handleReply(row)"
|
||||
>{{ row.handling_result ? '修改处理' : '处理' }}</el-button>
|
||||
<el-button
|
||||
v-if="row.status !== 3"
|
||||
type="danger"
|
||||
link
|
||||
@click="handleClose(row)"
|
||||
>关闭</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页器 -->
|
||||
<div class="pagination-container">
|
||||
<el-pagination
|
||||
v-model:current-page="currentPage"
|
||||
v-model:page-size="pageSize"
|
||||
:total="total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
background
|
||||
layout="total, sizes, prev, pager, next"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 处理对话框 -->
|
||||
<el-dialog
|
||||
v-model="replyDialogVisible"
|
||||
title="处理反馈"
|
||||
width="800px"
|
||||
destroy-on-close
|
||||
>
|
||||
<div class="feedback-detail">
|
||||
<div class="detail-row">
|
||||
<div class="detail-item">
|
||||
<strong>反馈标题:</strong>
|
||||
<span>{{ currentFeedback?.title }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<strong>反馈类型:</strong>
|
||||
<span>{{ feedbackTypeOptions.find(item => item.value === currentFeedback?.feedback_type)?.label }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-item">
|
||||
<strong>提交人:</strong>
|
||||
<span>{{ currentFeedback?.user_name }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<strong>提交时间:</strong>
|
||||
<span>{{ formatDateTime(currentFeedback?.created_at) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-item full-width">
|
||||
<strong>反馈内容:</strong>
|
||||
<span>{{ currentFeedback?.content }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-item full-width">
|
||||
<strong>位置信息:</strong>
|
||||
<span>{{ currentFeedback?.location_description }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="currentFeedback?.image_urls && currentFeedback.image_urls.length > 0" class="detail-row">
|
||||
<div class="detail-item full-width">
|
||||
<strong>相关图片:</strong>
|
||||
<div class="image-list">
|
||||
<el-image
|
||||
v-for="(url, index) in currentFeedback.image_urls"
|
||||
:key="index"
|
||||
:src="url"
|
||||
:preview-src-list="currentFeedback.image_urls"
|
||||
fit="cover"
|
||||
class="feedback-image"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-form :model="replyForm" label-width="100px">
|
||||
<el-form-item label="处理状态" required>
|
||||
<el-radio-group v-model="replyForm.status" @change="handleStatusChange">
|
||||
<el-radio :value="1" :disabled="currentFeedback?.status === 2">处理中</el-radio>
|
||||
<el-radio :value="2">已处理</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="处理结果" required>
|
||||
<el-input
|
||||
v-model="replyForm.reply"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
placeholder="请输入处理结果"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="replyDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmitReply">确定</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 图片预览 -->
|
||||
<el-dialog v-model="previewVisible" title="图片预览" width="800px">
|
||||
<div class="preview-container">
|
||||
<el-carousel
|
||||
v-if="previewImages.length > 0"
|
||||
:initial-index="currentPreviewIndex"
|
||||
height="400px"
|
||||
indicator-position="outside"
|
||||
>
|
||||
<el-carousel-item v-for="(url, index) in previewImages" :key="index">
|
||||
<el-image
|
||||
:src="url"
|
||||
fit="contain"
|
||||
class="preview-image"
|
||||
/>
|
||||
</el-carousel-item>
|
||||
</el-carousel>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.feedback-suggestions {
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
span {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.header-btns {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.search-form {
|
||||
margin-bottom: 24px;
|
||||
padding: 24px;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 16px;
|
||||
margin-right: 24px;
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-input),
|
||||
:deep(.el-select) {
|
||||
width: 220px;
|
||||
}
|
||||
|
||||
.search-buttons {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.feedback-detail {
|
||||
margin-bottom: 20px;
|
||||
padding: 16px;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 4px;
|
||||
|
||||
.detail-row {
|
||||
display: flex;
|
||||
margin-bottom: 16px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
|
||||
&.full-width {
|
||||
flex: 0 0 100%;
|
||||
}
|
||||
|
||||
strong {
|
||||
margin-right: 8px;
|
||||
color: #606266;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
span {
|
||||
flex: 1;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.image-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
|
||||
.feedback-image {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.preview-container {
|
||||
.preview-image {
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -66,9 +66,19 @@ const handleLogin = async (formEl) => {
|
||||
duration: 2000
|
||||
});
|
||||
|
||||
// 获取重定向地址
|
||||
// 等待一下确保状态更新完成
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
// 获取重定向地址并等待导航完成
|
||||
const redirect = route.query.redirect;
|
||||
router.push(redirect || "/dashboard");
|
||||
try {
|
||||
// 使用replace而不是push,这样不会留下历史记录
|
||||
await router.replace(redirect || "/dashboard");
|
||||
} catch (navigationError) {
|
||||
console.error('导航错误:', navigationError);
|
||||
// 如果导航失败,尝试强制刷新页面
|
||||
window.location.replace(redirect || "/dashboard");
|
||||
}
|
||||
} else {
|
||||
// 记录失败日志
|
||||
systemLogStore.addLog({
|
||||
|
||||
16
src/views/patrol/events/index.vue
Normal file
16
src/views/patrol/events/index.vue
Normal file
@ -0,0 +1,16 @@
|
||||
<script setup>
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="event-container">
|
||||
<!-- 搜索区域 -->
|
||||
<div class="search-container">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
||||
@ -318,7 +318,7 @@ const pointTypes = [
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@use "./styles/variables" as v;
|
||||
@use "../../../styles/variables.scss" as *;
|
||||
|
||||
.point-container {
|
||||
.el-card {
|
||||
|
||||
@ -231,7 +231,7 @@ const handleCurrentChange = (val) => {
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@use "./styles/variables" as v;
|
||||
@use "../../../styles/variables.scss" as *;
|
||||
|
||||
.record-container {
|
||||
.el-card {
|
||||
|
||||
@ -372,10 +372,13 @@ const resetFilters = () => {
|
||||
|
||||
<!-- 更新表格数据源 -->
|
||||
<el-table :data="filteredTableData" style="width: 100%; margin-top: 20px">
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="title" label="任务名称" min-width="150" />
|
||||
<el-table-column prop="area" label="巡护区域" min-width="120" />
|
||||
<el-table-column prop="assignee" label="负责人" width="100" />
|
||||
<el-table-column prop="lan_id" label="巡护计划ID" width="100" />
|
||||
<el-table-column prop="task_name" label="任务名称" min-width="150" />
|
||||
<el-table-column prop="task_type" label="任务类型" min-width="150" />
|
||||
<el-table-column prop="patrol_date" label="任务巡护日" min-width="150" />
|
||||
<el-table-column prop="start_time" label="开始时间" min-width="150" />
|
||||
<el-table-column prop="end_time" label="结束时间" min-width="150" />
|
||||
<el-table-column prop="executor_ids" label="执行人ID列表" min-width="120" />
|
||||
<el-table-column prop="status" label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getStatusType(row.status)">
|
||||
@ -390,9 +393,6 @@ const resetFilters = () => {
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="执行时间" min-width="300">
|
||||
<template #default="{ row }"> {{ row.startTime }} 至 {{ row.endTime }} </template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" size="small" @click="handleView(row)">
|
||||
@ -527,7 +527,7 @@ const resetFilters = () => {
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@use "./styles/variables" as v;
|
||||
@use "../../../styles/variables.scss" as *;
|
||||
|
||||
.task-container {
|
||||
.el-card {
|
||||
|
||||
549
src/views/system/carousel/index.vue
Normal file
549
src/views/system/carousel/index.vue
Normal file
@ -0,0 +1,549 @@
|
||||
<!-- 轮播图管理 -->
|
||||
<script setup>
|
||||
import { ref, onMounted, watch, computed } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus, Search, Refresh, Delete } from '@element-plus/icons-vue'
|
||||
import { formatDateTime } from '@/utils/format'
|
||||
import {
|
||||
getCarouselList,
|
||||
createCarousel,
|
||||
updateCarousel,
|
||||
deleteCarousel,
|
||||
batchUpdateCarouselSort
|
||||
} from '@/api/system/carousel'
|
||||
|
||||
// 获取上传请求头
|
||||
const uploadHeaders = computed(() => {
|
||||
const token = localStorage.getItem('token')
|
||||
return {
|
||||
Authorization: token ? `Bearer ${token}` : ''
|
||||
}
|
||||
})
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref([])
|
||||
const allData = ref([]) // 添加一个存储所有数据的数组
|
||||
const loading = ref(false)
|
||||
|
||||
// 分页配置
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const total = ref(0)
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = ref({
|
||||
title: '',
|
||||
})
|
||||
|
||||
// 表单对话框
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('新增轮播图')
|
||||
const formMode = ref('create')
|
||||
const form = ref({
|
||||
title: '',
|
||||
image_url: '',
|
||||
link_url: '',
|
||||
sort: 0,
|
||||
imageInputType: 'url'
|
||||
})
|
||||
|
||||
// 表单规则
|
||||
const formRules = {
|
||||
title: [
|
||||
{ required: true, message: '请输入轮播图标题', trigger: 'blur' },
|
||||
{ min: 2, max: 50, message: '长度在 2 到 50 个字符', trigger: 'blur' }
|
||||
],
|
||||
image_url: [
|
||||
{ required: true, message: '请上传轮播图片', trigger: 'change' }
|
||||
],
|
||||
sort: [
|
||||
{ required: true, message: '请输入排序号', trigger: 'blur' },
|
||||
{ type: 'number', message: '排序号必须为数字', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
const formRef = ref(null)
|
||||
|
||||
// 图片预览
|
||||
const previewVisible = ref(false)
|
||||
const previewImage = ref('')
|
||||
|
||||
// 处理图片预览
|
||||
const handlePreview = (url) => {
|
||||
previewImage.value = url
|
||||
previewVisible.value = true
|
||||
}
|
||||
|
||||
// 获取轮播图列表
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getCarouselList()
|
||||
if (res.success) {
|
||||
// 保存所有数据
|
||||
allData.value = res.data.list
|
||||
filterAndPaginateData()
|
||||
} else {
|
||||
ElMessage.error(res.message || '获取列表失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取轮播图列表错误:', error)
|
||||
ElMessage.error('获取列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 前端搜索和分页
|
||||
const filterAndPaginateData = () => {
|
||||
// 1. 先进行搜索过滤
|
||||
let filteredData = [...allData.value]
|
||||
|
||||
// 按标题搜索
|
||||
if (searchForm.value.title) {
|
||||
const keyword = searchForm.value.title.toLowerCase()
|
||||
filteredData = filteredData.filter(item =>
|
||||
item.title.toLowerCase().includes(keyword)
|
||||
)
|
||||
}
|
||||
|
||||
// 2. 更新总数
|
||||
total.value = filteredData.length
|
||||
|
||||
// 3. 进行分页
|
||||
const start = (currentPage.value - 1) * pageSize.value
|
||||
const end = start + pageSize.value
|
||||
tableData.value = filteredData.slice(start, end)
|
||||
}
|
||||
|
||||
// 处理搜索
|
||||
const handleSearch = () => {
|
||||
currentPage.value = 1 // 重置到第一页
|
||||
filterAndPaginateData()
|
||||
}
|
||||
|
||||
// 重置搜索
|
||||
const resetSearch = () => {
|
||||
searchForm.value = {
|
||||
title: ''
|
||||
}
|
||||
currentPage.value = 1
|
||||
filterAndPaginateData()
|
||||
}
|
||||
|
||||
// 刷新列表
|
||||
const handleRefresh = () => {
|
||||
getList()
|
||||
}
|
||||
|
||||
// 新增轮播图
|
||||
const handleAdd = () => {
|
||||
formMode.value = 'create'
|
||||
dialogTitle.value = '新增轮播图'
|
||||
form.value = {
|
||||
title: '',
|
||||
image_url: '',
|
||||
link_url: '',
|
||||
sort: 0,
|
||||
imageInputType: 'url'
|
||||
}
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 编辑轮播图
|
||||
const handleEdit = (row) => {
|
||||
formMode.value = 'edit'
|
||||
dialogTitle.value = '编辑轮播图'
|
||||
form.value = {
|
||||
id: row.id,
|
||||
title: row.title,
|
||||
image_url: row.image_url,
|
||||
link_url: row.link_url,
|
||||
sort: row.sort_order,
|
||||
imageInputType: row.image_url?.startsWith('http') ? 'url' : 'upload'
|
||||
}
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
try {
|
||||
const submitData = {
|
||||
title: form.value.title.trim(),
|
||||
image_url: form.value.image_url,
|
||||
link_url: form.value.link_url.trim(),
|
||||
sort_order: Number(form.value.sort)
|
||||
}
|
||||
|
||||
let res
|
||||
if (formMode.value === 'create') {
|
||||
res = await createCarousel(submitData)
|
||||
} else {
|
||||
res = await updateCarousel(form.value.id, submitData)
|
||||
}
|
||||
|
||||
if (res.success) {
|
||||
ElMessage.success(`${formMode.value === 'create' ? '新增' : '编辑'}成功`)
|
||||
dialogVisible.value = false
|
||||
getList()
|
||||
} else {
|
||||
ElMessage.error(res.message || `${formMode.value === 'create' ? '新增' : '编辑'}失败`)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`${formMode.value === 'create' ? '新增' : '编辑'}轮播图错误:`, error)
|
||||
ElMessage.error('操作失败,请检查输入是否正确')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 删除轮播图
|
||||
const handleDelete = (row) => {
|
||||
ElMessageBox.confirm('确认删除该轮播图吗?', '警告', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
try {
|
||||
const res = await deleteCarousel(row.id)
|
||||
if (res.success) {
|
||||
ElMessage.success('删除成功')
|
||||
getList()
|
||||
} else {
|
||||
ElMessage.error(res.message || '删除失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除轮播图错误:', error)
|
||||
ElMessage.error('删除失败')
|
||||
}
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
// 处理图片输入类型切换
|
||||
const handleImageTypeChange = () => {
|
||||
if (formMode.value === 'create') {
|
||||
form.value.image_url = ''
|
||||
}
|
||||
}
|
||||
|
||||
// 处理图片上传成功
|
||||
const handleUploadSuccess = (response) => {
|
||||
console.log('上传响应:', response) // 添加调试日志
|
||||
try {
|
||||
if (response.success) {
|
||||
// 确保获取正确的图片URL
|
||||
const imageUrl = response.data?.url || response.data?.image_url || response.url
|
||||
if (!imageUrl) {
|
||||
ElMessage.error('上传成功但未获取到图片地址')
|
||||
return
|
||||
}
|
||||
form.value.image_url = imageUrl.startsWith('http') ? imageUrl : `http://localhost:3000${imageUrl}`
|
||||
ElMessage.success('图片上传成功')
|
||||
} else {
|
||||
ElMessage.error(response.message || '图片上传失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('处理上传响应错误:', error)
|
||||
ElMessage.error('处理上传响应失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 处理上传错误
|
||||
const handleUploadError = (error) => {
|
||||
console.error('上传图片错误:', error)
|
||||
let errorMessage = '上传失败'
|
||||
try {
|
||||
if (typeof error.message === 'string') {
|
||||
const errorData = JSON.parse(error.message)
|
||||
errorMessage = errorData.message || '上传失败'
|
||||
}
|
||||
} catch (e) {
|
||||
errorMessage = error.message || '上传失败'
|
||||
}
|
||||
ElMessage.error(`上传失败: ${errorMessage}`)
|
||||
}
|
||||
|
||||
// 上传前验证
|
||||
const beforeUpload = (file) => {
|
||||
const isImage = file.type.startsWith('image/')
|
||||
const isLt2M = file.size / 1024 / 1024 < 2
|
||||
|
||||
if (!isImage) {
|
||||
ElMessage.error('只能上传图片文件!')
|
||||
return false
|
||||
}
|
||||
if (!isLt2M) {
|
||||
ElMessage.error('图片大小不能超过 2MB!')
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// 监听分页变化
|
||||
watch([currentPage, pageSize], () => {
|
||||
filterAndPaginateData()
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="carousel-manage">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>轮播图管理</span>
|
||||
<div class="header-btns">
|
||||
<el-button type="primary" :icon="Plus" @click="handleAdd">新增轮播图</el-button>
|
||||
<el-button :icon="Refresh" @click="handleRefresh">刷新</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 搜索表单 -->
|
||||
<el-form :model="searchForm" inline class="search-form" @submit.prevent>
|
||||
<el-form-item label="标题" label-width="80px">
|
||||
<el-input
|
||||
v-model="searchForm.title"
|
||||
placeholder="请输入标题"
|
||||
@keyup.enter="handleSearch"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item class="search-buttons">
|
||||
<el-button type="primary" :icon="Search" @click="handleSearch">搜索</el-button>
|
||||
<el-button @click="resetSearch">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 轮播图列表 -->
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="tableData"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-table-column type="index" label="序号" width="60" align="center" />
|
||||
<el-table-column prop="title" label="标题" min-width="150" show-overflow-tooltip align="center"/>
|
||||
<el-table-column prop="image_url" label="图片" width="120" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-image
|
||||
:src="row.image_url"
|
||||
fit="cover"
|
||||
style="width: 80px; height: 45px; cursor: pointer"
|
||||
:preview-src-list="[row.image_url]"
|
||||
:initial-index="0"
|
||||
crossorigin="anonymous"
|
||||
preview-teleported
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="link_url" label="跳转链接" min-width="200" show-overflow-tooltip align="center"/>
|
||||
<el-table-column prop="sort_order" label="排序" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
{{ row.sort_order || 0 }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="150" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
@click="handleEdit(row)"
|
||||
>编辑</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
link
|
||||
@click="handleDelete(row)"
|
||||
>删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页器 -->
|
||||
<div class="pagination-container">
|
||||
<el-pagination
|
||||
v-model:current-page="currentPage"
|
||||
v-model:page-size="pageSize"
|
||||
:total="total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
background
|
||||
layout="total, sizes, prev, pager, next"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 新增/编辑对话框 -->
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogTitle"
|
||||
width="600px"
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="formRules"
|
||||
label-width="100px"
|
||||
>
|
||||
<el-form-item label="标题" prop="title">
|
||||
<el-input v-model="form.title" placeholder="请输入轮播图标题" />
|
||||
</el-form-item>
|
||||
<el-form-item label="图片" prop="image_url">
|
||||
<el-radio-group v-model="form.imageInputType" class="mb-4" @change="handleImageTypeChange">
|
||||
<el-radio :value="'url'">输入图片地址</el-radio>
|
||||
<el-radio :value="'upload'">上传图片</el-radio>
|
||||
</el-radio-group>
|
||||
|
||||
<template v-if="form.imageInputType === 'url'">
|
||||
<el-input
|
||||
v-model="form.image_url"
|
||||
placeholder="请输入图片URL地址"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<el-upload
|
||||
class="carousel-uploader"
|
||||
action="http://localhost:3000/api/admin/carousels/upload"
|
||||
:headers="uploadHeaders"
|
||||
:show-file-list="false"
|
||||
:on-success="handleUploadSuccess"
|
||||
:on-error="handleUploadError"
|
||||
:before-upload="beforeUpload"
|
||||
name="image"
|
||||
accept="image/*"
|
||||
>
|
||||
<img v-if="form.image_url" :src="form.image_url" class="carousel-image" crossorigin="anonymous">
|
||||
<el-icon v-else class="carousel-uploader-icon"><Plus /></el-icon>
|
||||
</el-upload>
|
||||
</template>
|
||||
<div class="upload-tip">建议尺寸:1920x540px,大小不超过2MB</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="跳转链接" prop="link_url">
|
||||
<el-input v-model="form.link_url" placeholder="请输入跳转链接" />
|
||||
</el-form-item>
|
||||
<el-form-item label="排序" prop="sort">
|
||||
<el-input-number
|
||||
v-model="form.sort"
|
||||
:min="0"
|
||||
:max="999"
|
||||
placeholder="请输入排序号"
|
||||
style="width: 180px"
|
||||
/>
|
||||
<div class="form-tip">数字越小越靠前</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">确定</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.carousel-manage {
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
span {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.header-btns {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.search-form {
|
||||
margin-bottom: 24px;
|
||||
padding: 24px;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 16px;
|
||||
margin-right: 24px;
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-input),
|
||||
:deep(.el-select) {
|
||||
width: 220px;
|
||||
}
|
||||
|
||||
.search-buttons {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.carousel-uploader {
|
||||
:deep(.el-upload) {
|
||||
border: 1px dashed #d9d9d9;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: var(--el-transition-duration-fast);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.carousel-image {
|
||||
width: 360px;
|
||||
height: 120px;
|
||||
display: block;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.carousel-uploader-icon {
|
||||
font-size: 28px;
|
||||
color: #8c939d;
|
||||
width: 360px;
|
||||
height: 120px;
|
||||
text-align: center;
|
||||
line-height: 120px;
|
||||
}
|
||||
}
|
||||
|
||||
.upload-tip {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.mb-4 {
|
||||
margin-bottom: 16px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.form-tip {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Loading…
x
Reference in New Issue
Block a user