Files
schoolNews/schoolNewsWeb/src/views/task/LearningTaskList.vue
2025-10-23 18:57:31 +08:00

1449 lines
32 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="learning-task-list">
<!-- 搜索栏 -->
<div class="search-bar">
<div class="search-group">
<label class="search-label">任务名称</label>
<input
v-model="searchParams.name"
type="text"
class="search-input"
placeholder="输入任务名称搜索"
@keyup.enter="handleSearch"
/>
</div>
<div class="search-group">
<label class="search-label">任务状态</label>
<select v-model="searchParams.status" class="search-select">
<option :value="undefined">全部</option>
<option :value="0">草稿</option>
<option :value="1">进行中</option>
<option :value="2">已结束</option>
</select>
</div>
<div class="search-actions">
<button class="btn-search" @click="handleSearch">搜索</button>
<button class="btn-reset" @click="handleReset">重置</button>
</div>
</div>
<!-- 操作按钮 -->
<div class="toolbar">
<button class="btn-primary" @click="handleAdd">
<span class="icon">+</span> 新增任务
</button>
</div>
<!-- 任务表格 -->
<div class="task-table-wrapper">
<table class="task-table">
<thead>
<tr>
<th>任务名称</th>
<th>任务描述</th>
<th width="120">开始时间</th>
<th width="120">结束时间</th>
<th width="100">状态</th>
<th width="120">创建时间</th>
<th width="200">操作</th>
</tr>
</thead>
<tbody v-if="loading">
<tr>
<td colspan="7" class="loading-cell">
<div class="loading-spinner"></div>
<span>加载中...</span>
</td>
</tr>
</tbody>
<tbody v-else-if="taskList.length === 0">
<tr>
<td colspan="7" class="empty-cell">
<div class="empty-icon">📋</div>
<p>暂无任务数据</p>
</td>
</tr>
</tbody>
<tbody v-else>
<tr v-for="task in taskList" :key="task.taskID" class="table-row">
<td class="task-name">{{ task.name }}</td>
<td class="task-desc">
<div class="desc-text">{{ task.description || '-' }}</div>
</td>
<td>{{ formatDate(task.startTime) }}</td>
<td>{{ formatDate(task.endTime) }}</td>
<td>
<span class="status-tag" :class="getStatusClass(task.status)">
{{ getStatusText(task.status) }}
</span>
</td>
<td>{{ formatDate(task.createTime) }}</td>
<td class="action-cell">
<button class="btn-link btn-primary" @click="handleView(task)">查看</button>
<button class="btn-link btn-warning" @click="handleEdit(task)" v-if="task.status === 0">编辑</button>
<button class="btn-link btn-success" @click="handlePublish(task)" v-if="task.status === 0">发布</button>
<button class="btn-link btn-warning" @click="handleUpdateUser(task)" v-if="task.status !== 2">修改人员</button>
<button class="btn-link btn-danger" @click="handleDelete(task)" v-if="task.status === 0">删除</button>
</td>
</tr>
</tbody>
</table>
</div>
<!-- 分页 -->
<div v-if="!loading && taskList.length > 0" class="pagination">
<div class="pagination-info">
{{ pagination.total }} 条数据每页 {{ pagination.pageSize }}
</div>
<div class="pagination-controls">
<button
class="page-btn"
:disabled="pagination.current === 1"
@click="handlePageChange(1)"
>
首页
</button>
<button
class="page-btn"
:disabled="pagination.current === 1"
@click="handlePageChange(pagination.current - 1)"
>
上一页
</button>
<div class="page-numbers">
<button
v-for="page in displayPages"
:key="page"
class="page-number"
:class="{ active: page === pagination.current }"
@click="handlePageChange(page)"
:disabled="page === -1"
>
{{ page === -1 ? '...' : page }}
</button>
</div>
<button
class="page-btn"
:disabled="pagination.current === pagination.totalPages"
@click="handlePageChange(pagination.current + 1)"
>
下一页
</button>
<button
class="page-btn"
:disabled="pagination.current === pagination.totalPages"
@click="handlePageChange(pagination.totalPages)"
>
末页
</button>
</div>
<div class="pagination-jump">
<span>跳转到</span>
<input
v-model.number="jumpPage"
type="number"
class="jump-input"
@keyup.enter="handleJumpPage"
/>
<span></span>
<button class="jump-btn" @click="handleJumpPage">跳转</button>
</div>
</div>
<!-- 任务详情弹窗 -->
<div v-if="showTaskDetail" class="modal-overlay" @click.self="showTaskDetail = false">
<div class="modal-content">
<div class="modal-header">
<h3 class="modal-title">任务详情</h3>
<button class="modal-close" @click="showTaskDetail = false"></button>
</div>
<div class="modal-body">
<div v-if="viewingTask" class="task-detail">
<!-- 基本信息 -->
<div class="detail-section">
<h4 class="detail-title">基本信息</h4>
<div class="detail-content">
<div class="detail-row">
<span class="detail-label">任务名称:</span>
<span class="detail-value">{{ viewingTask.learningTask.name }}</span>
</div>
<div class="detail-row">
<span class="detail-label">任务描述:</span>
<span class="detail-value">{{ viewingTask.learningTask.description || '暂无描述' }}</span>
</div>
<div class="detail-row">
<span class="detail-label">任务状态:</span>
<span class="detail-value">
<span class="status-badge" :class="getStatusClass(viewingTask.learningTask.status)">
{{ getStatusText(viewingTask.learningTask.status) }}
</span>
</span>
</div>
<div class="detail-row">
<span class="detail-label">开始时间:</span>
<span class="detail-value">{{ formatDate(viewingTask.learningTask.startTime) }}</span>
</div>
<div class="detail-row">
<span class="detail-label">结束时间:</span>
<span class="detail-value">{{ formatDate(viewingTask.learningTask.endTime) }}</span>
</div>
<div class="detail-row">
<span class="detail-label">创建时间:</span>
<span class="detail-value">{{ formatDate(viewingTask.learningTask.createTime) }}</span>
</div>
</div>
</div>
<!-- 关联课程 -->
<div class="detail-section" v-if="viewingTask.taskCourses && viewingTask.taskCourses.length > 0">
<h4 class="detail-title">关联课程 ({{ viewingTask.taskCourses.length }})</h4>
<div class="detail-list">
<div v-for="(course, index) in viewingTask.taskCourses" :key="course.id" class="list-item">
<span class="item-index">{{ index + 1 }}.</span>
<span class="item-name">{{ course.courseName }}</span>
</div>
</div>
</div>
<!-- 关联资源 -->
<div class="detail-section" v-if="viewingTask.taskResources && viewingTask.taskResources.length > 0">
<h4 class="detail-title">关联资源 ({{ viewingTask.taskResources.length }})</h4>
<div class="detail-list">
<div v-for="(resource, index) in viewingTask.taskResources" :key="resource.id" class="list-item">
<span class="item-index">{{ index + 1 }}.</span>
<span class="item-name">{{ resource.resourceName }}</span>
</div>
</div>
</div>
<!-- 关联用户 -->
<div class="detail-section" v-if="viewingTask.taskUsers && viewingTask.taskUsers.length > 0">
<h4 class="detail-title">关联用户 ({{ viewingTask.taskUsers.length }})</h4>
<div class="detail-list">
<div v-for="(user, index) in viewingTask.taskUsers" :key="user.id" class="list-item">
<span class="item-index">{{ index + 1 }}.</span>
<span class="item-name">{{ user.username }}</span>
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn-default" @click="showTaskDetail = false">关闭</button>
</div>
</div>
</div>
<!-- 确认删除弹窗 -->
<div v-if="showDeleteConfirm" class="modal-overlay" @click.self="showDeleteConfirm = false">
<div class="modal-content small">
<div class="modal-header">
<h3 class="modal-title">确认删除</h3>
<button class="modal-close" @click="showDeleteConfirm = false"></button>
</div>
<div class="modal-body">
<p class="confirm-text">确定要删除任务 "{{ deletingTask?.name }}" 此操作不可恢复</p>
</div>
<div class="modal-footer">
<button class="btn-danger" @click="confirmDelete" :disabled="deleting">
{{ deleting ? '删除中...' : '确定删除' }}
</button>
<button class="btn-default" @click="showDeleteConfirm = false">取消</button>
</div>
</div>
</div>
<!-- 人员列表弹窗 -->
<div v-if="showUserList" class="modal-overlay" @click.self="closeUserList">
<div class="modal-content">
<div class="modal-header">
<h3 class="modal-title">人员管理 - {{ managingTask?.name }}</h3>
<button class="modal-close" @click="closeUserList"></button>
</div>
<div class="modal-body">
<div class="user-list-actions">
<button class="btn-success" @click="showAddUserSelector">
<span class="icon">+</span> 添加人员
</button>
<button class="btn-danger" @click="showRemoveUserSelector" :disabled="currentUsers.length === 0">
<span class="icon">-</span> 删除人员
</button>
</div>
<div class="current-user-list">
<div class="list-header">
<h4>当前人员列表</h4>
<span class="count-badge">{{ currentUsers.length }} </span>
</div>
<div v-if="currentUsers.length === 0" class="empty-list">
<div class="empty-icon">👥</div>
<p>暂无人员</p>
</div>
<div v-else class="user-grid">
<div v-for="user in currentUsers" :key="user.id" class="user-card">
<div class="user-avatar">{{ user.username?.charAt(0) }}</div>
<div class="user-info">
<div class="user-name">{{ user.username }}</div>
<div class="user-dept" v-if="user.deptName">{{ user.deptName }}</div>
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn-default" @click="closeUserList">关闭</button>
</div>
</div>
</div>
<!-- 人员选择器组件 -->
<UserSelect
v-model:visible="showUserSelector"
:mode="selectorMode"
:title="selectorMode === 'add' ? '添加人员' : '删除人员'"
:left-title="selectorMode === 'add' ? '可添加人员' : '当前人员'"
:right-title="selectorMode === 'add' ? '待添加人员' : '待删除人员'"
:available-users="selectorMode === 'remove' ? availableUsers : []"
:initial-target-users="[]"
:loading="saving"
:use-pagination="selectorMode === 'add'"
:fetch-api="selectorMode === 'add' ? userApi.getUserPage : undefined"
:filter-params="userFilterParams"
:page-size="20"
@confirm="handleUserSelectConfirm"
@cancel="closeSelectorModal"
/>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue';
import { ElMessage } from 'element-plus';
import { learningTaskApi } from '@/apis/study';
import { userApi } from '@/apis/system';
import { UserSelect } from '@/components';
import type { LearningTask, TaskVO, PageParam, UserVO } from '@/types';
defineOptions({
name: 'LearningTaskList'
});
const emit = defineEmits<{
add: [];
edit: [task: LearningTask];
}>();
// 搜索参数
const searchParams = ref({
name: '',
status: undefined as number | undefined
});
// 任务列表
const taskList = ref<LearningTask[]>([]);
const loading = ref(false);
// 分页参数
const pagination = ref({
current: 1,
pageSize: 10,
total: 0,
totalPages: 0
});
// 跳转页码
const jumpPage = ref<number>();
// 弹窗控制
const showTaskDetail = ref(false);
const showDeleteConfirm = ref(false);
const showUserList = ref(false);
const showUserSelector = ref(false);
// 查看和删除的任务
const viewingTask = ref<TaskVO | null>(null);
const deletingTask = ref<LearningTask | null>(null);
const deleting = ref(false);
// 人员管理相关
const managingTask = ref<LearningTask | null>(null);
const selectorMode = ref<'add' | 'remove'>('add');
const currentUsers = ref<UserVO[]>([]);
const availableUsers = ref<UserVO[]>([]);
const saving = ref(false);
const userFilterParams = ref<any>({});
// 计算显示的页码
const displayPages = computed(() => {
const current = pagination.value.current;
const total = pagination.value.totalPages;
const pages: number[] = [];
if (total <= 7) {
for (let i = 1; i <= total; i++) {
pages.push(i);
}
} else {
if (current <= 4) {
for (let i = 1; i <= 5; i++) {
pages.push(i);
}
pages.push(-1);
pages.push(total);
} else if (current >= total - 3) {
pages.push(1);
pages.push(-1);
for (let i = total - 4; i <= total; i++) {
pages.push(i);
}
} else {
pages.push(1);
pages.push(-1);
for (let i = current - 1; i <= current + 1; i++) {
pages.push(i);
}
pages.push(-1);
pages.push(total);
}
}
return pages;
});
onMounted(() => {
loadTaskList();
});
// 加载任务列表
async function loadTaskList() {
loading.value = true;
try {
const pageParam: PageParam = {
page: pagination.value.current,
size: pagination.value.pageSize
};
const filter: any = {};
if (searchParams.value.name) {
filter.name = searchParams.value.name;
}
if (searchParams.value.status !== undefined) {
filter.status = searchParams.value.status;
}
const res = await learningTaskApi.getTaskPage(pageParam, filter);
if (res.success) {
taskList.value = res.dataList || [];
pagination.value.total = res.pageParam?.totalElements || 0;
pagination.value.totalPages = Math.ceil(pagination.value.total / pagination.value.pageSize);
} else {
ElMessage.error(res.message || '加载失败');
}
} catch (error) {
console.error('加载任务列表失败:', error);
ElMessage.error('加载任务列表失败');
} finally {
loading.value = false;
}
}
// 搜索
function handleSearch() {
pagination.value.current = 1;
loadTaskList();
}
// 重置
function handleReset() {
searchParams.value = {
name: '',
status: undefined
};
pagination.value.current = 1;
loadTaskList();
}
// 分页切换
function handlePageChange(page: number) {
if (page < 1 || page > pagination.value.totalPages || page === -1) return;
pagination.value.current = page;
loadTaskList();
}
// 跳转页面
function handleJumpPage() {
if (!jumpPage.value) return;
const page = Math.max(1, Math.min(jumpPage.value, pagination.value.totalPages));
handlePageChange(page);
jumpPage.value = undefined;
}
// 新增任务
function handleAdd() {
emit('add');
}
// 查看任务
async function handleView(task: LearningTask) {
try {
const res = await learningTaskApi.getTaskById(task.taskID!);
if (res.success && res.data) {
viewingTask.value = res.data;
showTaskDetail.value = true;
}
} catch (error) {
console.error('加载任务详情失败:', error);
ElMessage.error('加载任务详情失败');
}
}
// 编辑任务
function handleEdit(task: LearningTask) {
emit('edit', task);
}
// 修改人员 - 显示当前人员列表
async function handleUpdateUser(task: LearningTask) {
managingTask.value = task;
try {
// 加载任务详情(包含已分配的用户)
const taskRes = await learningTaskApi.getTaskById(task.taskID!);
if (taskRes.success && taskRes.data) {
const taskUsers = taskRes.data.taskUsers || [];
const assignedUserIds = taskUsers.map(tu => tu.userID!);
// 加载用户详情(批量查询)
if (assignedUserIds.length > 0) {
const usersRes = await userApi.getUserList({});
if (usersRes.success && usersRes.dataList) {
currentUsers.value = usersRes.dataList.filter(user =>
assignedUserIds.includes(user.id!)
);
}
} else {
currentUsers.value = [];
}
}
showUserList.value = true;
} catch (error) {
console.error('加载用户数据失败:', error);
ElMessage.error('加载用户数据失败');
}
}
// 关闭人员列表弹窗
function closeUserList() {
showUserList.value = false;
managingTask.value = null;
currentUsers.value = [];
}
// 显示添加人员选择器
function showAddUserSelector() {
selectorMode.value = 'add';
// 设置过滤参数(可以过滤掉已分配的用户,但在组件内部会自动过滤)
userFilterParams.value = {};
showUserSelector.value = true;
}
// 显示删除人员选择器
function showRemoveUserSelector() {
selectorMode.value = 'remove';
// 左侧显示当前用户
availableUsers.value = [...currentUsers.value];
showUserSelector.value = true;
}
// 关闭选择器弹窗
function closeSelectorModal() {
showUserSelector.value = false;
availableUsers.value = [];
}
// 处理用户选择器确认事件
async function handleUserSelectConfirm(selectedUsers: UserVO[]) {
if (!managingTask.value || selectedUsers.length === 0) {
ElMessage.warning('请选择要操作的人员');
return;
}
saving.value = true;
try {
const userIds = selectedUsers.map(u => u.id!);
if (selectorMode.value === 'add') {
// 执行添加操作
for (const userID of userIds) {
await learningTaskApi.assignTaskToUser(managingTask.value.taskID!, userID);
}
ElMessage.success(`成功添加 ${userIds.length} 位人员`);
// 更新当前用户列表
currentUsers.value.push(...selectedUsers);
} else {
// 执行删除操作
for (const userID of userIds) {
await learningTaskApi.removeTaskFromUser(managingTask.value.taskID!, userID);
}
ElMessage.success(`成功删除 ${userIds.length} 位人员`);
// 更新当前用户列表
currentUsers.value = currentUsers.value.filter(user =>
!userIds.includes(user.id!)
);
}
closeSelectorModal();
loadTaskList();
} catch (error) {
console.error('人员操作失败:', error);
ElMessage.error('人员操作失败');
} finally {
saving.value = false;
}
}
// 发布任务
async function handlePublish(task: LearningTask) {
try {
const res = await learningTaskApi.publishTask({
taskID: task.taskID!,
status: 1
});
if (res.success) {
ElMessage.success('任务发布成功');
loadTaskList();
} else {
ElMessage.error(res.message || '发布失败');
}
} catch (error) {
console.error('发布任务失败:', error);
ElMessage.error('发布任务失败');
}
}
// 删除任务
function handleDelete(task: LearningTask) {
deletingTask.value = task;
showDeleteConfirm.value = true;
}
// 确认删除
async function confirmDelete() {
if (!deletingTask.value) return;
deleting.value = true;
try {
const res = await learningTaskApi.deleteTask(deletingTask.value.taskID!);
if (res.success) {
ElMessage.success('删除成功');
showDeleteConfirm.value = false;
deletingTask.value = null;
loadTaskList();
} else {
ElMessage.error(res.message || '删除失败');
}
} catch (error) {
console.error('删除任务失败:', error);
ElMessage.error('删除任务失败');
} finally {
deleting.value = false;
}
}
// 获取状态样式类
function getStatusClass(status?: number) {
switch (status) {
case 0:
return 'status-draft';
case 1:
return 'status-ongoing';
case 2:
return 'status-ended';
default:
return '';
}
}
// 获取状态文本
function getStatusText(status?: number) {
switch (status) {
case 0:
return '草稿';
case 1:
return '进行中';
case 2:
return '已结束';
default:
return '未知';
}
}
// 格式化日期
function formatDate(dateStr?: string) {
if (!dateStr) return '-';
const date = new Date(dateStr);
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
}
defineExpose({
loadTaskList
});
</script>
<style lang="scss" scoped>
.learning-task-list {
padding: 20px;
}
.search-bar {
background: #fff;
padding: 20px;
border-radius: 8px;
margin-bottom: 20px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
display: flex;
gap: 16px;
flex-wrap: wrap;
align-items: flex-end;
}
.search-group {
display: flex;
flex-direction: column;
gap: 8px;
min-width: 200px;
}
.search-label {
font-size: 14px;
font-weight: 500;
color: #606266;
}
.search-input,
.search-select {
padding: 8px 12px;
border: 1px solid #dcdfe6;
border-radius: 4px;
font-size: 14px;
color: #606266;
transition: border-color 0.3s;
&:focus {
outline: none;
border-color: #409eff;
}
}
.search-select {
cursor: pointer;
}
.search-actions {
display: flex;
gap: 12px;
margin-left: auto;
}
.btn-search,
.btn-reset {
padding: 8px 20px;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
transition: all 0.3s;
border: none;
}
.btn-search {
background: #409eff;
color: #fff;
&:hover {
background: #66b1ff;
}
}
.btn-reset {
background: #fff;
color: #606266;
border: 1px solid #dcdfe6;
&:hover {
color: #409eff;
border-color: #409eff;
}
}
.toolbar {
margin-bottom: 20px;
}
.btn-primary {
padding: 10px 20px;
background: #409eff;
color: #fff;
border: none;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
transition: all 0.3s;
display: inline-flex;
align-items: center;
gap: 4px;
&:hover {
background: #66b1ff;
}
.icon {
font-size: 18px;
}
}
.task-table-wrapper {
background: #fff;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
.task-table {
width: 100%;
border-collapse: collapse;
th {
background: #f5f7fa;
color: #606266;
font-weight: 600;
font-size: 14px;
padding: 12px 16px;
text-align: left;
border-bottom: 2px solid #e0e0e0;
}
td {
padding: 12px 16px;
border-bottom: 1px solid #f0f0f0;
font-size: 14px;
color: #606266;
}
.table-row {
transition: background-color 0.3s;
&:hover {
background: #f5f7fa;
}
&:last-child td {
border-bottom: none;
}
}
}
.task-name {
font-weight: 500;
color: #303133;
}
.task-desc {
max-width: 300px;
}
.desc-text {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.status-tag {
display: inline-block;
padding: 4px 12px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
&.status-draft {
background: #f4f4f5;
color: #909399;
}
&.status-ongoing {
background: #e1f3d8;
color: #67c23a;
}
&.status-ended {
background: #f0f9ff;
color: #409eff;
}
}
.action-cell {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.btn-link {
border: none;
padding: 4px 8px;
font-size: 13px;
cursor: pointer;
transition: all 0.3s;
border-radius: 4px;
&.btn-primary {
&:hover {
background: #ecf5ff;
}
}
&.btn-warning {
color: #e6a23c;
&:hover {
background: #fdf6ec;
}
}
&.btn-success {
color: #67c23a;
&:hover {
background: #f0f9ff;
}
}
&.btn-danger {
color: #ffffff;
&:hover {
background: #fef0f0;
}
}
}
.loading-cell,
.empty-cell {
text-align: center;
padding: 60px 20px !important;
}
.loading-spinner {
width: 40px;
height: 40px;
border: 4px solid #f0f0f0;
border-top-color: #409eff;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 16px;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.empty-icon {
font-size: 64px;
margin-bottom: 16px;
}
.pagination {
background: #fff;
padding: 20px;
border-radius: 8px;
margin-top: 20px;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
.pagination-info {
font-size: 14px;
color: #606266;
}
.pagination-controls {
display: flex;
gap: 8px;
align-items: center;
}
.page-btn,
.page-number {
padding: 6px 12px;
border: 1px solid #dcdfe6;
background: #fff;
color: #606266;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: all 0.3s;
&:hover:not(:disabled) {
color: #409eff;
border-color: #409eff;
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
&.active {
background: #409eff;
color: #fff;
border-color: #409eff;
}
}
.page-numbers {
display: flex;
gap: 4px;
}
.pagination-jump {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
color: #606266;
}
.jump-input {
width: 60px;
padding: 6px 8px;
border: 1px solid #dcdfe6;
border-radius: 4px;
text-align: center;
&:focus {
outline: none;
border-color: #409eff;
}
}
.jump-btn {
padding: 6px 12px;
background: #409eff;
color: #fff;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
&:hover {
background: #66b1ff;
}
}
// 通用按钮样式
.btn-default,
.btn-danger {
padding: 10px 24px;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
transition: all 0.3s;
border: none;
&:disabled {
opacity: 0.6;
cursor: not-allowed;
}
}
.btn-default {
background: #fff;
color: #606266;
border: 1px solid #dcdfe6;
&:hover {
color: #409eff;
border-color: #409eff;
}
}
.btn-danger {
background: #f56c6c;
color: #fff;
&:hover:not(:disabled) {
background: #f78989;
}
}
// 弹窗样式
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 2000;
}
.modal-content {
background: #fff;
border-radius: 8px;
width: 90%;
max-width: 700px;
max-height: 80vh;
display: flex;
flex-direction: column;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
&.small {
max-width: 500px;
}
&.large {
max-width: 900px;
}
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 24px;
border-bottom: 1px solid #f0f0f0;
}
.modal-title {
font-size: 18px;
font-weight: 600;
color: #303133;
margin: 0;
}
.modal-close {
width: 32px;
height: 32px;
border: none;
background: #f5f7fa;
border-radius: 50%;
cursor: pointer;
font-size: 18px;
color: #909399;
transition: all 0.3s;
&:hover {
background: #ecf5ff;
color: #409eff;
}
}
.modal-body {
flex: 1;
overflow-y: auto;
padding: 20px 24px;
}
.modal-footer {
padding: 16px 24px;
border-top: 1px solid #f0f0f0;
display: flex;
justify-content: flex-end;
gap: 12px;
}
.confirm-text {
font-size: 15px;
color: #606266;
line-height: 1.6;
margin: 0;
}
// 任务详情样式
.task-detail {
display: flex;
flex-direction: column;
gap: 24px;
}
.detail-section {
background: #f5f7fa;
padding: 16px;
border-radius: 6px;
}
.detail-title {
font-size: 16px;
font-weight: 600;
color: #303133;
margin: 0 0 16px 0;
padding-bottom: 12px;
border-bottom: 2px solid #e0e0e0;
}
.detail-content {
display: flex;
flex-direction: column;
gap: 12px;
}
.detail-row {
display: flex;
font-size: 14px;
}
.detail-label {
color: #909399;
min-width: 100px;
font-weight: 500;
}
.detail-value {
color: #606266;
flex: 1;
}
.detail-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.list-item {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 12px;
background: #fff;
border-radius: 4px;
transition: all 0.3s;
&:hover {
background: #ecf5ff;
}
}
.item-index {
color: #909399;
font-weight: 600;
min-width: 24px;
}
.item-name {
color: #606266;
flex: 1;
}
.status-badge {
display: inline-block;
padding: 4px 12px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
&.status-draft {
background: #f4f4f5;
color: #909399;
}
&.status-ongoing {
background: #e1f3d8;
color: #67c23a;
}
&.status-ended {
background: #f0f9ff;
color: #409eff;
}
}
// 人员列表样式
.user-list-actions {
display: flex;
gap: 12px;
margin-bottom: 20px;
}
.btn-success,
.btn-danger {
padding: 10px 20px;
border: none;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
transition: all 0.3s;
display: inline-flex;
align-items: center;
gap: 6px;
.icon {
font-size: 18px;
font-weight: bold;
}
}
.btn-success {
background: #67c23a;
color: #fff;
&:hover {
background: #85ce61;
}
}
.btn-danger {
background: #f56c6c;
color: #fff;
&:hover:not(:disabled) {
background: #f78989;
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
}
.current-user-list {
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 20px;
background: #fafafa;
}
.list-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 2px solid #e0e0e0;
h4 {
font-size: 16px;
font-weight: 600;
color: #303133;
margin: 0;
}
}
.count-badge {
background: #409eff;
color: #fff;
padding: 4px 12px;
border-radius: 12px;
font-size: 13px;
font-weight: 500;
}
.empty-list {
text-align: center;
padding: 60px 20px;
color: #909399;
.empty-icon {
font-size: 64px;
margin-bottom: 16px;
}
p {
margin: 0;
font-size: 15px;
}
}
.user-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 12px;
}
.user-card {
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
background: #fff;
border: 1px solid #e0e0e0;
border-radius: 6px;
transition: all 0.3s;
&:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
border-color: #409eff;
}
}
.user-avatar {
width: 40px;
height: 40px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #fff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
font-weight: bold;
text-transform: uppercase;
}
.user-info {
flex: 1;
min-width: 0;
.user-name {
font-size: 14px;
font-weight: 500;
color: #303133;
margin-bottom: 4px;
}
.user-dept {
font-size: 12px;
color: #909399;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
</style>