355 lines
7.8 KiB
Vue
355 lines
7.8 KiB
Vue
|
|
<template>
|
||
|
|
<div class="task-card">
|
||
|
|
<!-- 卡片头部 -->
|
||
|
|
<div class="card-header">
|
||
|
|
<div class="header-content">
|
||
|
|
<h3 class="task-title">{{ task.learningTask.name }}</h3>
|
||
|
|
<div class="task-meta">
|
||
|
|
<!-- <div class="meta-item">
|
||
|
|
<img class="icon" src="@/assets/imgs/usermange.svg" alt="部门" />
|
||
|
|
<span class="meta-text">{{ getDeptName }}</span>
|
||
|
|
</div> -->
|
||
|
|
<div class="meta-item">
|
||
|
|
<img class="icon" src="@/assets/imgs/calendar-icon.svg" alt="日期" />
|
||
|
|
<span class="meta-text">截止: {{ formatDate(task.learningTask.endTime) }}</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="status-badge" :class="getStatusClass(task.learningTask.status)">
|
||
|
|
{{ getStatusText(task.learningTask.status, task.learningTask.startTime, task.learningTask.endTime) }}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- 统计信息 -->
|
||
|
|
<div class="card-stats">
|
||
|
|
<div class="stat-item">
|
||
|
|
<div class="stat-label">分配人数</div>
|
||
|
|
<div class="stat-value">{{ task.totalTaskNum || 0 }}</div>
|
||
|
|
</div>
|
||
|
|
<div class="stat-item stat-completed">
|
||
|
|
<div class="stat-label">已完成</div>
|
||
|
|
<div class="stat-value stat-value-completed">{{ task.completedTaskNum || 0 }}</div>
|
||
|
|
</div>
|
||
|
|
<div class="stat-item stat-rate">
|
||
|
|
<div class="stat-label">完成率</div>
|
||
|
|
<div class="stat-value stat-value-rate">{{ completionRate }}</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- 进度条 -->
|
||
|
|
<div class="card-progress">
|
||
|
|
<div class="progress-header">
|
||
|
|
<span class="progress-label">完成进度</span>
|
||
|
|
<span class="progress-value">{{ completionRate }}</span>
|
||
|
|
</div>
|
||
|
|
<div class="progress-bar">
|
||
|
|
<div class="progress-fill" :style="{ width: completionRate }"></div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- 操作按钮 -->
|
||
|
|
<div class="card-footer">
|
||
|
|
<div class="card-actions">
|
||
|
|
<button class="btn-link btn-primary" @click="$emit('view', task)">查看</button>
|
||
|
|
<button class="btn-link btn-warning" @click="$emit('edit', task)" v-if="task.learningTask.status !== 1">编辑</button>
|
||
|
|
<button class="btn-link btn-success" @click="$emit('publish', task)" v-if="task.learningTask.status !== 1">发布</button>
|
||
|
|
<button class="btn-link btn-warning" @click="$emit('unpublish', task)" v-if="task.learningTask.status === 1">下架</button>
|
||
|
|
<button class="btn-link btn-primary" @click="$emit('statistics', task)" v-if="task.learningTask.status !== 0">统计</button>
|
||
|
|
<button class="btn-link btn-warning" @click="$emit('update-user', task)">修改人员</button>
|
||
|
|
<button class="btn-link btn-danger" @click="$emit('delete', task)" v-if="task.learningTask.status === 0">删除</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</template>
|
||
|
|
|
||
|
|
<script setup lang="ts">
|
||
|
|
import { computed } from 'vue';
|
||
|
|
import type { TaskVO } from '@/types/study';
|
||
|
|
|
||
|
|
interface Props {
|
||
|
|
task: TaskVO;
|
||
|
|
}
|
||
|
|
|
||
|
|
const props = defineProps<Props>();
|
||
|
|
|
||
|
|
defineEmits<{
|
||
|
|
view: [task: TaskVO];
|
||
|
|
edit: [task: TaskVO];
|
||
|
|
publish: [task: TaskVO];
|
||
|
|
unpublish: [task: TaskVO];
|
||
|
|
statistics: [task: TaskVO];
|
||
|
|
'update-user': [task: TaskVO];
|
||
|
|
delete: [task: TaskVO];
|
||
|
|
}>();
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 获取部门名称
|
||
|
|
*/
|
||
|
|
// const getDeptName = computed(() => {
|
||
|
|
// if (props.task.taskUsers && props.task.taskUsers.length > 0) {
|
||
|
|
// const deptNames = new Set(props.task.taskUsers.map(u => u.deptName).filter(Boolean));
|
||
|
|
// if (deptNames.size === 0) return '全校';
|
||
|
|
// if (deptNames.size === 1) return Array.from(deptNames)[0] || '全校';
|
||
|
|
// return '多个部门';
|
||
|
|
// }
|
||
|
|
// return '全校';
|
||
|
|
// });
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 格式化日期
|
||
|
|
*/
|
||
|
|
function formatDate(dateStr?: string): string {
|
||
|
|
if (!dateStr) return '-';
|
||
|
|
const date = new Date(dateStr);
|
||
|
|
return date.toLocaleDateString('zh-CN', {
|
||
|
|
year: 'numeric',
|
||
|
|
month: '2-digit',
|
||
|
|
day: '2-digit'
|
||
|
|
}).replace(/\//g, '-');
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 获取状态样式类
|
||
|
|
*/
|
||
|
|
function getStatusClass(status?: number): string {
|
||
|
|
switch (status) {
|
||
|
|
case 0:
|
||
|
|
return 'status-draft';
|
||
|
|
case 1:
|
||
|
|
return 'status-ongoing';
|
||
|
|
case 2:
|
||
|
|
return 'status-ended';
|
||
|
|
default:
|
||
|
|
return '';
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 获取状态文本
|
||
|
|
*/
|
||
|
|
function getStatusText(status?: number, startTime?: string, endTime?: string): string {
|
||
|
|
if (status === 0) return '草稿';
|
||
|
|
if (status === 1) {
|
||
|
|
const now = new Date();
|
||
|
|
const start = new Date(startTime!);
|
||
|
|
const end = new Date(endTime!);
|
||
|
|
if (now >= start && now <= end) {
|
||
|
|
return '进行中';
|
||
|
|
} else if (now < start) {
|
||
|
|
return '未开始';
|
||
|
|
} else {
|
||
|
|
return '已结束';
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (status === 2) return '下架';
|
||
|
|
return '未知';
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 计算完成率
|
||
|
|
*/
|
||
|
|
const completionRate = computed(() => {
|
||
|
|
const total = props.task.totalTaskNum || 0;
|
||
|
|
const completed = props.task.completedTaskNum || 0;
|
||
|
|
if (total === 0) return '0%';
|
||
|
|
const rate = Math.round((completed / total) * 100);
|
||
|
|
return `${rate}%`;
|
||
|
|
});
|
||
|
|
</script>
|
||
|
|
|
||
|
|
<style lang="scss" scoped>
|
||
|
|
.task-card {
|
||
|
|
background: #F9FAFB;
|
||
|
|
border: 1px solid rgba(0, 0, 0, 0.1);
|
||
|
|
border-radius: 14px;
|
||
|
|
padding: 24px;
|
||
|
|
display: flex;
|
||
|
|
flex-direction: column;
|
||
|
|
gap: 24px;
|
||
|
|
transition: all 0.3s;
|
||
|
|
|
||
|
|
&:hover {
|
||
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 卡片头部
|
||
|
|
.card-header {
|
||
|
|
display: flex;
|
||
|
|
justify-content: space-between;
|
||
|
|
gap: 16px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.header-content {
|
||
|
|
flex: 1;
|
||
|
|
display: flex;
|
||
|
|
flex-direction: column;
|
||
|
|
gap: 8px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.task-title {
|
||
|
|
font-size: 16px;
|
||
|
|
font-weight: 400;
|
||
|
|
line-height: 1.5em;
|
||
|
|
letter-spacing: -1.953125%;
|
||
|
|
color: #101828;
|
||
|
|
margin: 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
.task-meta {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: 16px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.meta-item {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: 4px;
|
||
|
|
|
||
|
|
.icon {
|
||
|
|
width: 16px;
|
||
|
|
height: 16px;
|
||
|
|
flex-shrink: 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
.meta-text {
|
||
|
|
font-size: 14px;
|
||
|
|
font-weight: 400;
|
||
|
|
line-height: 1.428571em;
|
||
|
|
letter-spacing: -1.07421875%;
|
||
|
|
color: #4A5565;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
.status-badge {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
justify-content: center;
|
||
|
|
padding: 2px 8px;
|
||
|
|
height: 22px;
|
||
|
|
border-radius: 8px;
|
||
|
|
font-size: 12px;
|
||
|
|
font-weight: 500;
|
||
|
|
line-height: 1.333333em;
|
||
|
|
white-space: nowrap;
|
||
|
|
|
||
|
|
&.status-draft {
|
||
|
|
background: #F4F4F5;
|
||
|
|
color: #909399;
|
||
|
|
border: 1px solid transparent;
|
||
|
|
}
|
||
|
|
|
||
|
|
&.status-ongoing {
|
||
|
|
background: #DBEAFE;
|
||
|
|
color: #1447E6;
|
||
|
|
border: 1px solid transparent;
|
||
|
|
}
|
||
|
|
|
||
|
|
&.status-ended {
|
||
|
|
background: #F0F9FF;
|
||
|
|
color: #409EFF;
|
||
|
|
border: 1px solid transparent;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 统计信息
|
||
|
|
.card-stats {
|
||
|
|
display: grid;
|
||
|
|
grid-template-columns: repeat(3, 1fr);
|
||
|
|
gap: 16px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.stat-item {
|
||
|
|
background: #FFFFFF;
|
||
|
|
border-radius: 10px;
|
||
|
|
padding: 12px;
|
||
|
|
display: flex;
|
||
|
|
flex-direction: column;
|
||
|
|
gap: 4px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.stat-label {
|
||
|
|
font-size: 14px;
|
||
|
|
font-weight: 400;
|
||
|
|
line-height: 1.428571em;
|
||
|
|
letter-spacing: -1.07421875%;
|
||
|
|
color: #4A5565;
|
||
|
|
}
|
||
|
|
|
||
|
|
.stat-value {
|
||
|
|
font-size: 20px;
|
||
|
|
font-weight: 400;
|
||
|
|
line-height: 1.4em;
|
||
|
|
letter-spacing: -2.24609375%;
|
||
|
|
color: #101828;
|
||
|
|
}
|
||
|
|
|
||
|
|
.stat-value-completed {
|
||
|
|
color: #00A63E;
|
||
|
|
}
|
||
|
|
|
||
|
|
.stat-value-rate {
|
||
|
|
color: #155DFC;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 进度条
|
||
|
|
.card-progress {
|
||
|
|
display: flex;
|
||
|
|
flex-direction: column;
|
||
|
|
gap: 8px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.progress-header {
|
||
|
|
display: flex;
|
||
|
|
justify-content: space-between;
|
||
|
|
align-items: center;
|
||
|
|
}
|
||
|
|
|
||
|
|
.progress-label {
|
||
|
|
font-size: 14px;
|
||
|
|
font-weight: 400;
|
||
|
|
line-height: 1.428571em;
|
||
|
|
letter-spacing: -1.07421875%;
|
||
|
|
color: #4A5565;
|
||
|
|
}
|
||
|
|
|
||
|
|
.progress-value {
|
||
|
|
font-size: 14px;
|
||
|
|
font-weight: 400;
|
||
|
|
line-height: 1.428571em;
|
||
|
|
letter-spacing: -1.07421875%;
|
||
|
|
color: #101828;
|
||
|
|
}
|
||
|
|
|
||
|
|
.progress-bar {
|
||
|
|
width: 100%;
|
||
|
|
height: 8px;
|
||
|
|
background: rgba(3, 2, 19, 0.2);
|
||
|
|
border-radius: 16777200px;
|
||
|
|
overflow: hidden;
|
||
|
|
}
|
||
|
|
|
||
|
|
.progress-fill {
|
||
|
|
height: 100%;
|
||
|
|
background: #030213;
|
||
|
|
border-radius: 16777200px;
|
||
|
|
transition: width 0.3s ease;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 卡片底部
|
||
|
|
.card-footer {
|
||
|
|
padding-top: 16px;
|
||
|
|
border-top: 1px solid rgba(0, 0, 0, 0.05);
|
||
|
|
}
|
||
|
|
|
||
|
|
// 操作按钮
|
||
|
|
.card-actions {
|
||
|
|
display: flex;
|
||
|
|
justify-content: flex-end;
|
||
|
|
flex-wrap: wrap;
|
||
|
|
gap: 6px;
|
||
|
|
}
|
||
|
|
</style>
|
||
|
|
|