OfficeSystem/components/ContentDashboard.vue
2025-11-07 11:40:13 +08:00

806 lines
19 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>
<scroll-view class="dashboard-scroll" scroll-y>
<!-- 加载状态 -->
<view v-if="loading" class="loading-container">
<view class="loading-text">加载中...</view>
</view>
<view class="dashboard-content" v-else>
<!-- 任务概览 -->
<view class="task-overview">
<view
class="task-card task-card-base"
v-for="item in taskStats"
:key="item.label"
:class="getTaskCardClass(item.label)"
@click="goToTaskList(item.label)"
>
<text class="task-count">{{ item.count }}</text>
<view class="task-label-wrapper">
<text class="task-label">{{ item.label }}</text>
<uv-tags
v-if="item.label === '逾期任务'"
text="紧急"
:type="getTaskStatusType('overdue')"
size="mini"
plain
:custom-style="{ marginTop: '4px' }"
></uv-tags>
<uv-tags
v-else-if="item.label === '即将预期'"
text="注意"
:type="getTaskStatusType('imminent')"
size="mini"
plain
:custom-style="{ marginTop: '4px' }"
></uv-tags>
</view>
</view>
</view>
<!-- 逾期任务详情 -->
<view class="overdue-section" v-if="overdueTasks.length > 0">
<view class="overdue-card task-card-base task-card-overdue" v-for="task in overdueTasks.slice(0, 1)" :key="task.id" @click="goToTaskDetail(task)">
<view class="task-header">
<view class="task-badge-wrapper">
<uv-tags
:text="getStatusText('overdue')"
:type="getTaskStatusType('overdue')"
size="mini"
:plain="false"
:custom-style="getTagCustomStyle('overdue')"
></uv-tags>
</view>
<view class="task-date-wrapper">
<text class="task-date">{{ task.date }}</text>
</view>
</view>
<view class="task-content">
<text class="task-project">所属项目: {{ task.project }}</text>
<text class="task-description">{{ task.description }}</text>
<view class="task-meta">
<text class="task-owner">负责人: {{ task.owner }}</text>
<text class="task-time">发布时间: {{ task.releaseTime }}</text>
</view>
</view>
<view class="task-action">
<uv-button type="error" size="small" @click.stop="handleOverdueTask(task)">立即处理</uv-button>
</view>
</view>
</view>
<!-- 公告事项 -->
<view class="announcement-section">
<view class="section-header">
<text class="section-icon">📢</text>
<text class="section-title">公告事项</text>
</view>
<view class="announcement-item task-card-base task-card-announcement" v-for="announcement in announcements" :key="announcement.id" @click="viewAnnouncement(announcement)">
<view class="announcement-content">
<text class="announcement-title">{{ announcement.title }}</text>
<text class="announcement-desc">{{ announcement.description }}</text>
<text class="announcement-time">{{ announcement.time }}</text>
</view>
<text class="arrow"></text>
</view>
</view>
<!-- 项目状态 -->
<view class="project-status-section">
<view class="section-header">
<text class="section-icon">💎</text>
<text class="section-title">项目状态</text>
</view>
<view class="status-grid">
<view
class="status-card task-card-base"
v-for="status in projectStatus"
:key="status.label"
:class="getProjectCardClass(status.label)"
>
<text class="status-count">{{ status.count }}</text>
<view class="status-label-wrapper">
<text class="status-label">{{ status.label }}</text>
<uv-tags
:text="getProjectStatusTag(status.label)"
:type="getProjectStatusType(status.label)"
size="mini"
plain
:custom-style="{ marginTop: '4px' }"
></uv-tags>
</view>
</view>
</view>
</view>
<!-- 客户状态 -->
<view class="customer-status-section">
<view class="section-header">
<text class="section-icon">👤</text>
<text class="section-title">客户状态</text>
</view>
<view class="status-grid">
<view
class="status-card task-card-base"
v-for="status in customerStatus"
:key="status.label"
:class="getCustomerCardClass(status.label)"
>
<text class="status-count">{{ status.count }}</text>
<view class="status-label-wrapper">
<text class="status-label">{{ status.label }}</text>
<uv-tags
:text="getCustomerStatusTag(status.label)"
:type="getCustomerStatusType(status.label)"
size="mini"
plain
:custom-style="{ marginTop: '4px' }"
></uv-tags>
</view>
</view>
</view>
</view>
</view>
</scroll-view>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { getTaskStatusType, getTaskStatusStyle, getStatusText } from '@/utils/taskConfig.js';
import { getDashboardBrief, getTaskList } from '@/common/api';
import { useUserStore } from '@/store/user';
// 任务统计
const taskStats = ref([
{ label: '完成任务', count: 0 },
{ label: '待完成任务', count: 0 },
{ label: '即将预期', count: 0 },
{ label: '逾期任务', count: 0 }
]);
// 逾期任务
const overdueTasks = ref([]);
// 公告事项
const announcements = ref([
{
id: 1,
title: '·国庆放假通知',
description: '国庆放假安排1号至6号,前后不调休...',
time: '2025-09-26 16:54:46'
}
]);
// 项目状态
const projectStatus = ref([
{ label: '运行中', count: 0 },
{ label: '运维中', count: 0 },
{ label: '即将到期', count: 0 },
{ label: '开发超期', count: 0 }
]);
// 客户状态
const customerStatus = ref([
{ label: '今日新增', count: 0 },
{ label: '今日已跟进', count: 0 },
{ label: '今日待跟进', count: 0 },
{ label: '即将跟进', count: 0 }
]);
// 加载状态
const loading = ref(false);
// 从 API 数据映射到组件数据
const mapApiDataToComponent = (apiData) => {
if (!apiData) return;
// 映射任务统计
const task = apiData.task || {};
taskStats.value = [
{ label: '完成任务', count: task.completed || 0 },
{ label: '待完成任务', count: task.inProgress || 0 },
{ label: '即将预期', count: task.soonExpire || 0 },
{ label: '逾期任务', count: task.overdueUncompleted || 0 }
];
// 映射项目状态
const project = apiData.project || {};
projectStatus.value = [
{ label: '运行中', count: project.inProgress || 0 },
{ label: '运维中', count: project.maintenance || 0 },
{ label: '即将到期', count: project.devSoonExpire || 0 },
{ label: '开发超期', count: project.developmentOverdue || 0 }
];
// 映射客户状态
const customer = apiData.customer || {};
customerStatus.value = [
{ label: '今日新增', count: customer.today || 0 },
{ label: '今日已跟进', count: customer.todayFollowed || 0 },
{ label: '今日待跟进', count: customer.todayWaitFollow || 0 },
{ label: '即将跟进', count: customer.soonFollow || 0 }
];
// 逾期任务列表(如果有详细数据,可以在这里处理)
// 目前 API 只返回了统计数字,如果有详细列表接口,可以在这里调用
// overdueTasks.value = [];
};
// 格式化日期:将 "2024-10-31 23:59:59" 转换为 "2024-10-31"
const formatDate = (dateStr) => {
if (!dateStr) return '';
// 如果包含空格,取日期部分
return dateStr.split(' ')[0];
};
// 提取负责人:从 memberList 中提取所有成员的名称
const getOwnerNames = (memberList) => {
if (!Array.isArray(memberList) || memberList.length === 0) return '';
return memberList.map(member => member.userName || member.name || '').filter(name => name).join('、');
};
// 加载逾期任务列表
const loadOverdueTasks = async () => {
try {
const res = await getTaskList({ overdue: true });
console.log('逾期任务列表加载成功:', res);
// 根据实际返回的数据结构:{ total: 27, rows: [...], code: 200, msg: "查询成功" }
if (res && res.rows && Array.isArray(res.rows)) {
overdueTasks.value = res.rows.map((item) => {
return {
id: item.id || '',
date: formatDate(item.expireTime) || '',
project: item.projectName || '',
description: item.description || '',
owner: getOwnerNames(item.memberList) || '',
releaseTime: formatDate(item.createTime) || ''
};
});
} else if (res && res.data && Array.isArray(res.data)) {
// 兼容 data 字段
overdueTasks.value = res.data.map((item) => {
return {
id: item.id || '',
date: formatDate(item.expireTime) || '',
project: item.projectName || '',
description: item.description || '',
owner: getOwnerNames(item.memberList) || '',
releaseTime: formatDate(item.createTime) || ''
};
});
} else if (res && Array.isArray(res)) {
// 兼容直接返回数组的情况
overdueTasks.value = res.map((item) => {
return {
id: item.id || '',
date: formatDate(item.expireTime) || '',
project: item.projectName || '',
description: item.description || '',
owner: getOwnerNames(item.memberList) || '',
releaseTime: formatDate(item.createTime) || ''
};
});
} else {
overdueTasks.value = [];
}
} catch (err) {
console.error('加载逾期任务列表失败:', err);
// 逾期任务加载失败不影响其他数据,只记录错误
overdueTasks.value = [];
}
};
// 加载看板数据
const loadDashboardData = async () => {
try {
loading.value = true;
const keys = [
'taskStatus',
'taskTodayCompleted',
'taskSoonExpire',
'taskOverdueUncompleted',
'projectStatus',
'projectDevSoonExpire',
'projectDevOverdue',
'customerTodayCreate',
'customerTodayFollowed',
'customerTodayWaitFollow',
'customerSoonFollow',
'customerMonthFollow',
'customerMonthFollowCount'
];
// 从 store 获取用户ID如果没有则使用默认值
const userStore = useUserStore();
// 尝试从 userInfo 中获取用户ID如果没有则使用默认值
const joinUserId = userStore.userInfo?.id || userStore.userInfo?.userId || '23';
const res = await getDashboardBrief({
joinUserId,
keys
});
console.log('看板数据加载成功:', res);
mapApiDataToComponent(res);
// 加载逾期任务列表
await loadOverdueTasks();
} catch (err) {
console.error('加载看板数据失败:', err);
uni.showToast({
title: '加载数据失败',
icon: 'none'
});
} finally {
loading.value = false;
}
};
// 组件挂载时加载数据
onMounted(() => {
loadDashboardData();
});
// 跳转到任务列表页
const goToTaskList = (label) => {
// 将中文标签映射为状态参数
const statusMap = {
'完成任务': 'completed',
'待完成任务': 'pending',
'即将预期': 'imminent',
'逾期任务': 'overdue'
};
const status = statusMap[label] || '';
uni.navigateTo({
url: `/pages/task/list/index?status=${status}&label=${encodeURIComponent(label)}`
});
};
// 跳转到任务详情页
const goToTaskDetail = (task) => {
// 将任务数据存储到本地,供详情页使用
uni.setStorageSync('taskDetailData', {
id: task.id,
name: task.description || '待办任务名称',
project: task.project || '所属项目',
statusTags: ['已逾期', '紧急'],
deadline: task.date || '2025-10-14 18:00',
creator: '张珊珊',
responsible: task.owner || '张珊珊、李志',
publishTime: task.releaseTime || '2025-10-17',
content: task.description || '任务内容任务。这里是详细的任务描述,可以包含多行文本。根据实际需求,这里可以展示任务的详细要求、步骤说明、注意事项等。任务内容应该清晰明了,便于负责人理解和执行。',
submitRecords: []
});
uni.navigateTo({
url: `/pages/task/detail/index?id=${task.id}`
});
};
// 处理逾期任务
const handleOverdueTask = (task) => {
goToTaskDetail(task);
};
// 查看公告
const viewAnnouncement = (announcement) => {
console.log('查看公告:', announcement);
uni.showToast({
title: '查看公告详情',
icon: 'none'
});
};
// 获取项目状态标签类型
const getProjectStatusType = (label) => {
const typeMap = {
'运行中': 'success',
'运维中': 'primary',
'即将到期': 'warning',
'开发超期': 'error'
};
return typeMap[label] || 'primary';
};
// 获取项目状态标签文本
const getProjectStatusTag = (label) => {
const tagMap = {
'运行中': '正常',
'运维中': '进行中',
'即将到期': '待处理',
'开发超期': '超期'
};
return tagMap[label] || '';
};
// 获取客户状态标签类型
const getCustomerStatusType = (label) => {
const typeMap = {
'今日新增': 'success',
'今日已跟进': 'primary',
'今日待跟进': 'warning',
'即将跟进': 'info'
};
return typeMap[label] || 'primary';
};
// 获取客户状态标签文本
const getCustomerStatusTag = (label) => {
const tagMap = {
'今日新增': '新增',
'今日已跟进': '已完成',
'今日待跟进': '待处理',
'即将跟进': '即将'
};
return tagMap[label] || '';
};
// 获取任务卡片样式类
const getTaskCardClass = (label) => {
const classMap = {
'完成任务': 'task-card-completed',
'待完成任务': 'task-card-pending',
'即将预期': 'task-card-imminent',
'逾期任务': 'task-card-overdue'
};
return classMap[label] || '';
};
// 获取项目状态卡片样式类
const getProjectCardClass = (label) => {
const classMap = {
'运行中': 'status-card-success',
'运维中': 'status-card-primary',
'即将到期': 'status-card-warning',
'开发超期': 'status-card-error'
};
return classMap[label] || '';
};
// 获取客户状态卡片样式类
const getCustomerCardClass = (label) => {
const classMap = {
'今日新增': 'status-card-success',
'今日已跟进': 'status-card-primary',
'今日待跟进': 'status-card-warning',
'即将跟进': 'status-card-info'
};
return classMap[label] || '';
};
// 使用全局配置获取标签自定义样式
const getTagCustomStyle = (status) => {
const styleConfig = getTaskStatusStyle(status);
return {
backgroundColor: styleConfig.backgroundColor,
color: styleConfig.color,
borderColor: styleConfig.borderColor
};
};
</script>
<style lang="scss" scoped>
.dashboard-scroll {
width: 100%;
height: 100%;
}
.dashboard-content {
padding: 10px 30rpx;
}
// 统一卡片基础样式
.task-card-base {
background: #fff;
border-radius: 12px;
padding: 16px;
position: relative;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
}
.task-card-base:active {
transform: scale(0.98);
opacity: 0.9;
}
.task-overview {
display: flex;
justify-content: space-between;
margin-top: 10px;
margin-bottom: 20px;
gap: 12px;
}
.task-card {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
// 任务卡片使用较小的padding
&.task-card-base {
padding: 12px 8px;
}
}
// 任务卡片状态样式
.task-card-imminent {
border-left: 4px solid #ff9800;
}
.task-card-pending {
border-left: 4px solid #2885ff;
}
.task-card-completed {
border-left: 4px solid #909399;
opacity: 0.85;
}
.task-card-overdue {
border-left: 4px solid #f56c6c;
}
.task-count {
font-size: 24px;
font-weight: 600;
color: #333;
margin-bottom: 8px;
}
.task-label-wrapper {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
}
.task-label {
font-size: 12px;
color: #666;
}
.overdue-section {
margin-bottom: 20px;
}
.overdue-card {
margin-bottom: 12px;
display: flex;
flex-direction: column;
}
.overdue-card.task-card-overdue {
background: linear-gradient(135deg, #fff5f5 0%, #ffe6e6 100%);
border-left: 4px solid #f56c6c;
box-shadow: 0 2px 12px rgba(255, 68, 68, 0.1);
}
// 任务卡片头部样式(用于逾期卡片)
.task-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
}
.task-badge-wrapper {
flex-shrink: 0;
}
.task-date-wrapper {
background: rgba(0, 0, 0, 0.04);
border-radius: 4px;
padding: 4px 8px;
}
.task-date {
font-size: 14px;
color: #333;
font-weight: 500;
}
.task-content {
flex: 1;
display: flex;
flex-direction: column;
gap: 8px;
}
.task-project {
font-size: 12px;
color: #666;
line-height: 1.5;
}
.task-description {
font-size: 14px;
color: #333;
line-height: 1.5;
margin-bottom: 4px;
}
.task-meta {
display: flex;
flex-direction: column;
gap: 4px;
}
.task-owner {
font-size: 12px;
color: #666;
}
.task-time {
font-size: 12px;
color: #666;
}
.task-action {
margin-top: 12px;
display: flex;
justify-content: flex-end;
}
.carousel-dots {
display: flex;
justify-content: center;
gap: 8px;
margin-top: 8px;
}
.dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: #ddd;
}
.dot.active {
background: #2885ff;
}
.announcement-section,
.project-status-section,
.customer-status-section {
margin-top: 8px;
}
.section-header {
display: flex;
align-items: center;
margin-bottom: 12px;
gap: 8px;
}
.section-icon {
font-size: 18px;
}
.section-title {
font-size: 16px;
font-weight: 600;
color: #333;
}
.announcement-item {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
}
.task-card-announcement {
border-left: 4px solid #2885ff;
}
.announcement-content {
flex: 1;
display: flex;
flex-direction: column;
gap: 8px;
}
.announcement-title {
font-size: 14px;
font-weight: 500;
color: #333;
}
.announcement-desc {
font-size: 12px;
color: #666;
line-height: 1.5;
}
.announcement-time {
font-size: 12px;
color: #999;
}
.arrow {
font-size: 20px;
color: #999;
margin-left: 12px;
}
.status-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
}
.status-card {
display: flex;
flex-direction: column;
align-items: center;
// 状态卡片使用较小的padding
&.task-card-base {
padding: 12px 8px;
}
}
// 状态卡片边框样式
.status-card-success {
border-left: 4px solid #67c23a;
}
.status-card-primary {
border-left: 4px solid #2885ff;
}
.status-card-warning {
border-left: 4px solid #ff9800;
}
.status-card-error {
border-left: 4px solid #f56c6c;
}
.status-card-info {
border-left: 4px solid #909399;
}
.status-count {
font-size: 20px;
font-weight: 600;
color: #333;
margin-bottom: 8px;
}
.status-label-wrapper {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
width: 100%;
}
.status-label {
font-size: 12px;
color: #666;
text-align: center;
}
.loading-container {
display: flex;
justify-content: center;
align-items: center;
padding: 40px 0;
}
.loading-text {
font-size: 14px;
color: #999;
}
</style>