|
@@ -0,0 +1,2215 @@
|
|
|
|
|
+<template>
|
|
|
|
|
+ <div class="task-stats-container stats-page">
|
|
|
|
|
+ <!-- 顶部筛选条 -->
|
|
|
|
|
+ <div class="filter-section stats-card" style="margin-bottom: 16px;">
|
|
|
|
|
+ <!-- 快捷时间选择 -->
|
|
|
|
|
+ <div class="time-shortcuts">
|
|
|
|
|
+ <span class="shortcuts-label">快速选择:</span>
|
|
|
|
|
+ <div class="shortcut-pills">
|
|
|
|
|
+ <button
|
|
|
|
|
+ v-for="shortcut in timeShortcuts"
|
|
|
|
|
+ :key="shortcut.value"
|
|
|
|
|
+ :class="['shortcut-pill', { active: currentShortcut === shortcut.value }]"
|
|
|
|
|
+ @click="handleShortcutClick(shortcut)"
|
|
|
|
|
+ >
|
|
|
|
|
+ {{ shortcut.label }}
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="filter-divider"></div>
|
|
|
|
|
+
|
|
|
|
|
+ <el-form
|
|
|
|
|
+ :model="filters"
|
|
|
|
|
+ ref="filterForm"
|
|
|
|
|
+ :inline="true"
|
|
|
|
|
+ size="small"
|
|
|
|
|
+ label-width="80px"
|
|
|
|
|
+ class="filter-form"
|
|
|
|
|
+ >
|
|
|
|
|
+ <el-form-item label="时间范围" prop="dateRange">
|
|
|
|
|
+ <el-date-picker
|
|
|
|
|
+ v-model="filters.dateRange"
|
|
|
|
|
+ type="daterange"
|
|
|
|
|
+ range-separator="至"
|
|
|
|
|
+ start-placeholder="开始日期"
|
|
|
|
|
+ end-placeholder="结束日期"
|
|
|
|
|
+ value-format="yyyy-MM-dd"
|
|
|
|
|
+ style="width: 300px"
|
|
|
|
|
+ :picker-options="pickerOptions"
|
|
|
|
|
+ @change="handleDateRangeChange"
|
|
|
|
|
+ />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+
|
|
|
|
|
+ <el-form-item label="农场" prop="farms">
|
|
|
|
|
+ <el-select
|
|
|
|
|
+ v-model="filters.farms"
|
|
|
|
|
+ placeholder="请选择农场"
|
|
|
|
|
+ multiple
|
|
|
|
|
+ collapse-tags
|
|
|
|
|
+ style="width: 180px"
|
|
|
|
|
+ >
|
|
|
|
|
+ <el-option
|
|
|
|
|
+ v-for="item in farmOptions"
|
|
|
|
|
+ :key="item.value"
|
|
|
|
|
+ :label="item.label"
|
|
|
|
|
+ :value="item.value"
|
|
|
|
|
+ />
|
|
|
|
|
+ </el-select>
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+
|
|
|
|
|
+ <el-form-item label="地块" prop="plots">
|
|
|
|
|
+ <el-select
|
|
|
|
|
+ v-model="filters.plots"
|
|
|
|
|
+ placeholder="请选择地块"
|
|
|
|
|
+ multiple
|
|
|
|
|
+ collapse-tags
|
|
|
|
|
+ style="width: 180px"
|
|
|
|
|
+ >
|
|
|
|
|
+ <el-option
|
|
|
|
|
+ v-for="item in plotOptions"
|
|
|
|
|
+ :key="item.value"
|
|
|
|
|
+ :label="item.label"
|
|
|
|
|
+ :value="item.value"
|
|
|
|
|
+ />
|
|
|
|
|
+ </el-select>
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+
|
|
|
|
|
+ <el-form-item label="作物" prop="crops">
|
|
|
|
|
+ <el-select
|
|
|
|
|
+ v-model="filters.crops"
|
|
|
|
|
+ placeholder="请选择作物"
|
|
|
|
|
+ multiple
|
|
|
|
|
+ collapse-tags
|
|
|
|
|
+ style="width: 180px"
|
|
|
|
|
+ >
|
|
|
|
|
+ <el-option
|
|
|
|
|
+ v-for="item in cropOptions"
|
|
|
|
|
+ :key="item.value"
|
|
|
|
|
+ :label="item.label"
|
|
|
|
|
+ :value="item.value"
|
|
|
|
|
+ />
|
|
|
|
|
+ </el-select>
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+
|
|
|
|
|
+ <el-form-item label="任务类型" prop="taskTypes">
|
|
|
|
|
+ <el-select
|
|
|
|
|
+ v-model="filters.taskTypes"
|
|
|
|
|
+ placeholder="请选择任务类型"
|
|
|
|
|
+ multiple
|
|
|
|
|
+ collapse-tags
|
|
|
|
|
+ style="width: 180px"
|
|
|
|
|
+ >
|
|
|
|
|
+ <el-option
|
|
|
|
|
+ v-for="item in taskTypeOptions"
|
|
|
|
|
+ :key="item.value"
|
|
|
|
|
+ :label="item.label"
|
|
|
|
|
+ :value="item.value"
|
|
|
|
|
+ />
|
|
|
|
|
+ </el-select>
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+
|
|
|
|
|
+ <el-form-item label="执行人" prop="executors">
|
|
|
|
|
+ <el-select
|
|
|
|
|
+ v-model="filters.executors"
|
|
|
|
|
+ placeholder="请选择执行人"
|
|
|
|
|
+ multiple
|
|
|
|
|
+ collapse-tags
|
|
|
|
|
+ style="width: 180px"
|
|
|
|
|
+ >
|
|
|
|
|
+ <el-option
|
|
|
|
|
+ v-for="item in executorOptions"
|
|
|
|
|
+ :key="item.value"
|
|
|
|
|
+ :label="item.label"
|
|
|
|
|
+ :value="item.value"
|
|
|
|
|
+ />
|
|
|
|
|
+ </el-select>
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+
|
|
|
|
|
+ <el-form-item label="状态" prop="statuses">
|
|
|
|
|
+ <el-select
|
|
|
|
|
+ v-model="filters.statuses"
|
|
|
|
|
+ placeholder="请选择状态"
|
|
|
|
|
+ multiple
|
|
|
|
|
+ collapse-tags
|
|
|
|
|
+ style="width: 180px"
|
|
|
|
|
+ >
|
|
|
|
|
+ <el-option
|
|
|
|
|
+ v-for="item in statusOptions"
|
|
|
|
|
+ :key="item.value"
|
|
|
|
|
+ :label="item.label"
|
|
|
|
|
+ :value="item.value"
|
|
|
|
|
+ />
|
|
|
|
|
+ </el-select>
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+
|
|
|
|
|
+ <el-form-item>
|
|
|
|
|
+ <div class="action-buttons">
|
|
|
|
|
+ <el-button type="primary" icon="el-icon-search" @click="handleQuery" class="btn-primary">
|
|
|
|
|
+ 查询
|
|
|
|
|
+ </el-button>
|
|
|
|
|
+ <el-button icon="el-icon-refresh" @click="handleReset" class="btn-secondary">
|
|
|
|
|
+ 重置
|
|
|
|
|
+ </el-button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ </el-form>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- KPI 卡片区域 -->
|
|
|
|
|
+ <div class="kpi-section stats-card">
|
|
|
|
|
+ <div class="kpi-grid">
|
|
|
|
|
+ <div
|
|
|
|
|
+ v-for="(kpi, index) in kpiData"
|
|
|
|
|
+ :key="index"
|
|
|
|
|
+ class="kpi-card"
|
|
|
|
|
+ @click="handleKpiClick(kpi.key)"
|
|
|
|
|
+ >
|
|
|
|
|
+ <div class="kpi-card__icon" :style="{ backgroundColor: kpi.iconBg }">
|
|
|
|
|
+ <i :class="kpi.icon" :style="{ color: kpi.color }"></i>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="kpi-card__content">
|
|
|
|
|
+ <div class="kpi-card__meta">{{ kpi.title }}</div>
|
|
|
|
|
+ <div class="kpi-card__value">
|
|
|
|
|
+ <span class="kpi-number">
|
|
|
|
|
+ <count-to
|
|
|
|
|
+ :start-val="0"
|
|
|
|
|
+ :end-val="kpi.value"
|
|
|
|
|
+ :duration="1500"
|
|
|
|
|
+ :decimals="kpi.type === 'percent' || kpi.type === 'duration' ? 1 : 0"
|
|
|
|
|
+ :suffix="kpi.type === 'percent' ? '%' : ''"
|
|
|
|
|
+ class="animated-number"
|
|
|
|
|
+ />
|
|
|
|
|
+ </span>
|
|
|
|
|
+ <span v-if="kpi.unit" class="kpi-unit">{{ kpi.unit }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <el-dropdown trigger="click" @command="handleKpiAction" class="kpi-card__menu">
|
|
|
|
|
+ <span class="kpi-menu-trigger">
|
|
|
|
|
+ <i class="el-icon-more"></i>
|
|
|
|
|
+ </span>
|
|
|
|
|
+ <el-dropdown-menu slot="dropdown">
|
|
|
|
|
+ <el-dropdown-item :command="{ action: 'export', key: kpi.key }">
|
|
|
|
|
+ <i class="el-icon-download"></i> 导出数据
|
|
|
|
|
+ </el-dropdown-item>
|
|
|
|
|
+ <el-dropdown-item :command="{ action: 'detail', key: kpi.key }">
|
|
|
|
|
+ <i class="el-icon-view"></i> 查看明细
|
|
|
|
|
+ </el-dropdown-item>
|
|
|
|
|
+ </el-dropdown-menu>
|
|
|
|
|
+ </el-dropdown>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 主内容区域 -->
|
|
|
|
|
+ <el-row :gutter="16" class="main-content">
|
|
|
|
|
+ <!-- 左侧主栏 -->
|
|
|
|
|
+ <el-col :xs="24" :sm="24" :md="16" :lg="18" :xl="19">
|
|
|
|
|
+ <!-- 趋势图 -->
|
|
|
|
|
+ <div class="chart-section stats-card" style="margin-bottom: 16px;">
|
|
|
|
|
+ <div class="chart-header">
|
|
|
|
|
+ <h3 class="chart-title">任务趋势</h3>
|
|
|
|
|
+ <div class="chart-controls">
|
|
|
|
|
+ <div class="metric-segment">
|
|
|
|
|
+ <button
|
|
|
|
|
+ v-for="metric in trendMetrics"
|
|
|
|
|
+ :key="metric.value"
|
|
|
|
|
+ :class="['segment-btn', { active: trendMetric === metric.value }]"
|
|
|
|
|
+ @click="handleTrendMetricChange(metric.value)"
|
|
|
|
|
+ >
|
|
|
|
|
+ {{ metric.label }}
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="chart-tools">
|
|
|
|
|
+ <el-tooltip content="下载图片" placement="top">
|
|
|
|
|
+ <el-button size="mini" circle icon="el-icon-download" @click="downloadChart('trend')" />
|
|
|
|
|
+ </el-tooltip>
|
|
|
|
|
+ <el-tooltip content="全屏显示" placement="top">
|
|
|
|
|
+ <el-button size="mini" circle icon="el-icon-full-screen" @click="fullscreenChart('trend')" />
|
|
|
|
|
+ </el-tooltip>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div
|
|
|
|
|
+ ref="trendChart"
|
|
|
|
|
+ v-loading="trendLoading"
|
|
|
|
|
+ class="chart-container"
|
|
|
|
|
+ style="height: 380px"
|
|
|
|
|
+ >
|
|
|
|
|
+ <div v-if="!trendLoading && trendEmpty" class="chart-empty">
|
|
|
|
|
+ <i class="el-icon-s-data chart-empty-icon"></i>
|
|
|
|
|
+ <p class="chart-empty-text">暂无数据</p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 维度对比图 -->
|
|
|
|
|
+ <div class="chart-section stats-card" style="margin-top: 16px">
|
|
|
|
|
+ <div class="chart-header">
|
|
|
|
|
+ <h3 class="chart-title">维度对比</h3>
|
|
|
|
|
+ <div class="chart-controls">
|
|
|
|
|
+ <div class="dimension-controls">
|
|
|
|
|
+ <div class="control-group">
|
|
|
|
|
+ <span class="control-label">维度:</span>
|
|
|
|
|
+ <div class="metric-segment">
|
|
|
|
|
+ <button
|
|
|
|
|
+ v-for="dimension in dimensionTypes"
|
|
|
|
|
+ :key="dimension.value"
|
|
|
|
|
+ :class="['segment-btn', { active: dimensionType === dimension.value }]"
|
|
|
|
|
+ @click="handleDimensionTypeChange(dimension.value)"
|
|
|
|
|
+ >
|
|
|
|
|
+ {{ dimension.label }}
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="control-group">
|
|
|
|
|
+ <span class="control-label">指标:</span>
|
|
|
|
|
+ <div class="metric-segment">
|
|
|
|
|
+ <button
|
|
|
|
|
+ v-for="metric in dimensionMetrics"
|
|
|
|
|
+ :key="metric.value"
|
|
|
|
|
+ :class="['segment-btn', { active: dimensionMetric === metric.value }]"
|
|
|
|
|
+ @click="handleDimensionMetricChange(metric.value)"
|
|
|
|
|
+ >
|
|
|
|
|
+ {{ metric.label }}
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="chart-tools">
|
|
|
|
|
+ <el-tooltip content="下载图片" placement="top">
|
|
|
|
|
+ <el-button size="mini" circle icon="el-icon-download" @click="downloadChart('dimension')" />
|
|
|
|
|
+ </el-tooltip>
|
|
|
|
|
+ <el-tooltip content="全屏显示" placement="top">
|
|
|
|
|
+ <el-button size="mini" circle icon="el-icon-full-screen" @click="fullscreenChart('dimension')" />
|
|
|
|
|
+ </el-tooltip>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div
|
|
|
|
|
+ ref="dimensionChart"
|
|
|
|
|
+ v-loading="dimensionLoading"
|
|
|
|
|
+ class="chart-container"
|
|
|
|
|
+ style="height: 380px"
|
|
|
|
|
+ >
|
|
|
|
|
+ <div v-if="!dimensionLoading && dimensionEmpty" class="chart-empty">
|
|
|
|
|
+ <i class="el-icon-s-data chart-empty-icon"></i>
|
|
|
|
|
+ <p class="chart-empty-text">暂无数据</p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </el-col>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 右侧固钉侧栏 -->
|
|
|
|
|
+ <el-col :xs="24" :sm="24" :md="8" :lg="6" :xl="5">
|
|
|
|
|
+ <div class="sidebar-sticky">
|
|
|
|
|
+ <!-- 今日到期 -->
|
|
|
|
|
+ <div class="todo-section stats-card">
|
|
|
|
|
+ <div class="todo-header" @click="toggleTodoSection('today')">
|
|
|
|
|
+ <div class="todo-title-wrapper">
|
|
|
|
|
+ <h4>今日到期</h4>
|
|
|
|
|
+ <el-badge :value="todoData.today.length" class="todo-badge" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <i :class="['el-icon-arrow-down', 'toggle-icon', { collapsed: collapsedSections.today }]"></i>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <el-collapse-transition>
|
|
|
|
|
+ <div v-show="!collapsedSections.today" class="todo-list">
|
|
|
|
|
+ <div class="timeline-container">
|
|
|
|
|
+ <div
|
|
|
|
|
+ v-for="task in todoData.today"
|
|
|
|
|
+ :key="task.id"
|
|
|
|
|
+ class="todo-item today"
|
|
|
|
|
+ @click="handleTodoClick(task)"
|
|
|
|
|
+ @mouseenter="showTodoAction(task.id)"
|
|
|
|
|
+ @mouseleave="hideTodoAction"
|
|
|
|
|
+ >
|
|
|
|
|
+ <div class="timeline-dot today-dot"></div>
|
|
|
|
|
+ <div class="todo-content">
|
|
|
|
|
+ <div class="task-name">{{ task.taskName }}</div>
|
|
|
|
|
+ <div class="task-meta">
|
|
|
|
|
+ <span>{{ task.plotName }}</span> |
|
|
|
|
|
+ <span>{{ task.executor }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="task-time">{{ task.deadline }}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div v-show="hoveredTaskId === task.id" class="todo-action">
|
|
|
|
|
+ <el-button size="mini" type="text" class="action-btn">查看</el-button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </el-collapse-transition>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 未来3天到期 -->
|
|
|
|
|
+ <div class="todo-section stats-card" style="margin-top: 16px">
|
|
|
|
|
+ <div class="todo-header" @click="toggleTodoSection('next3Days')">
|
|
|
|
|
+ <div class="todo-title-wrapper">
|
|
|
|
|
+ <h4>未来3天到期</h4>
|
|
|
|
|
+ <el-badge :value="todoData.next3Days.length" class="todo-badge" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <i :class="['el-icon-arrow-down', 'toggle-icon', { collapsed: collapsedSections.next3Days }]"></i>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <el-collapse-transition>
|
|
|
|
|
+ <div v-show="!collapsedSections.next3Days" class="todo-list">
|
|
|
|
|
+ <div class="timeline-container">
|
|
|
|
|
+ <div
|
|
|
|
|
+ v-for="task in todoData.next3Days"
|
|
|
|
|
+ :key="task.id"
|
|
|
|
|
+ class="todo-item next"
|
|
|
|
|
+ @click="handleTodoClick(task)"
|
|
|
|
|
+ @mouseenter="showTodoAction(task.id)"
|
|
|
|
|
+ @mouseleave="hideTodoAction"
|
|
|
|
|
+ >
|
|
|
|
|
+ <div class="timeline-dot next-dot"></div>
|
|
|
|
|
+ <div class="todo-content">
|
|
|
|
|
+ <div class="task-name">{{ task.taskName }}</div>
|
|
|
|
|
+ <div class="task-meta">
|
|
|
|
|
+ <span>{{ task.plotName }}</span> |
|
|
|
|
|
+ <span>{{ task.executor }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="task-time">{{ task.deadline }}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div v-show="hoveredTaskId === task.id" class="todo-action">
|
|
|
|
|
+ <el-button size="mini" type="text" class="action-btn">查看</el-button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </el-collapse-transition>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 已逾期 -->
|
|
|
|
|
+ <div class="todo-section stats-card" style="margin-top: 16px">
|
|
|
|
|
+ <div class="todo-header" @click="toggleTodoSection('overdue')">
|
|
|
|
|
+ <div class="todo-title-wrapper">
|
|
|
|
|
+ <h4>已逾期</h4>
|
|
|
|
|
+ <el-badge :value="todoData.overdue.length" class="todo-badge danger" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <i :class="['el-icon-arrow-down', 'toggle-icon', { collapsed: collapsedSections.overdue }]"></i>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <el-collapse-transition>
|
|
|
|
|
+ <div v-show="!collapsedSections.overdue" class="todo-list">
|
|
|
|
|
+ <div class="timeline-container">
|
|
|
|
|
+ <div
|
|
|
|
|
+ v-for="task in todoData.overdue"
|
|
|
|
|
+ :key="task.id"
|
|
|
|
|
+ class="todo-item overdue"
|
|
|
|
|
+ @click="handleTodoClick(task)"
|
|
|
|
|
+ @mouseenter="showTodoAction(task.id)"
|
|
|
|
|
+ @mouseleave="hideTodoAction"
|
|
|
|
|
+ >
|
|
|
|
|
+ <div class="timeline-dot overdue-dot"></div>
|
|
|
|
|
+ <div class="todo-content">
|
|
|
|
|
+ <div class="task-name">{{ task.taskName }}</div>
|
|
|
|
|
+ <div class="task-meta">
|
|
|
|
|
+ <span>{{ task.plotName }}</span> |
|
|
|
|
|
+ <span>{{ task.executor }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="task-time danger">{{ task.deadline }}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div v-show="hoveredTaskId === task.id" class="todo-action">
|
|
|
|
|
+ <el-button size="mini" type="text" class="action-btn">查看</el-button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </el-collapse-transition>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </el-col>
|
|
|
|
|
+ </el-row>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 任务明细抽屉 -->
|
|
|
|
|
+ <el-drawer
|
|
|
|
|
+ :title="drawerTitle"
|
|
|
|
|
+ :visible.sync="drawerVisible"
|
|
|
|
|
+ direction="rtl"
|
|
|
|
|
+ size="70%"
|
|
|
|
|
+ :before-close="handleDrawerClose"
|
|
|
|
|
+ >
|
|
|
|
|
+ <div class="drawer-content">
|
|
|
|
|
+ <el-table
|
|
|
|
|
+ v-loading="drawerLoading"
|
|
|
|
|
+ :data="drawerData"
|
|
|
|
|
+ border
|
|
|
|
|
+ style="width: 100%; margin-bottom: 20px"
|
|
|
|
|
+ >
|
|
|
|
|
+ <el-table-column label="任务名称" prop="taskName" min-width="180" />
|
|
|
|
|
+ <el-table-column label="类型" prop="taskType" width="90">
|
|
|
|
|
+ <template slot-scope="scope">
|
|
|
|
|
+ <el-tag size="mini" :type="getTaskTypeTagType(scope.row.taskType)">
|
|
|
|
|
+ {{ getTaskTypeName(scope.row.taskType) }}
|
|
|
|
|
+ </el-tag>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column label="作物" prop="cropName" width="90" />
|
|
|
|
|
+ <el-table-column label="地块" prop="plotName" min-width="140" />
|
|
|
|
|
+ <el-table-column label="执行人" prop="executor" width="90" />
|
|
|
|
|
+ <el-table-column label="计划开始" prop="startTime" width="140" />
|
|
|
|
|
+ <el-table-column label="计划结束" prop="endTime" width="140" />
|
|
|
|
|
+ <el-table-column label="状态" prop="status" width="110">
|
|
|
|
|
+ <template slot-scope="scope">
|
|
|
|
|
+ <el-tag size="mini" :type="getStatusTagType(scope.row.status)">
|
|
|
|
|
+ {{ getStatusName(scope.row.status) }}
|
|
|
|
|
+ </el-tag>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ </el-table>
|
|
|
|
|
+
|
|
|
|
|
+ <el-pagination
|
|
|
|
|
+ :current-page="drawerPagination.page"
|
|
|
|
|
+ :page-sizes="[10, 20, 50, 100]"
|
|
|
|
|
+ :page-size="drawerPagination.size"
|
|
|
|
|
+ :total="drawerPagination.total"
|
|
|
|
|
+ layout="total, sizes, prev, pager, next, jumper"
|
|
|
|
|
+ @size-change="handleDrawerSizeChange"
|
|
|
|
|
+ @current-change="handleDrawerCurrentChange"
|
|
|
|
|
+ />
|
|
|
|
|
+
|
|
|
|
|
+ <div class="drawer-actions" style="margin-top: 20px">
|
|
|
|
|
+ <el-button type="primary" @click="handleExportCsv">
|
|
|
|
|
+ <i class="el-icon-download"></i>
|
|
|
|
|
+ 导出CSV
|
|
|
|
|
+ </el-button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </el-drawer>
|
|
|
|
|
+ </div>
|
|
|
|
|
+</template>
|
|
|
|
|
+
|
|
|
|
|
+<script>
|
|
|
|
|
+import * as echarts from 'echarts'
|
|
|
|
|
+import seedrandom from 'seedrandom'
|
|
|
|
|
+import CountTo from 'vue-count-to'
|
|
|
|
|
+
|
|
|
|
|
+export default {
|
|
|
|
|
+ name: 'TaskStats',
|
|
|
|
|
+ components: {
|
|
|
|
|
+ CountTo
|
|
|
|
|
+ },
|
|
|
|
|
+ data() {
|
|
|
|
|
+ return {
|
|
|
|
|
+ // 快捷时间选择
|
|
|
|
|
+ timeShortcuts: [
|
|
|
|
|
+ { label: '近7天', value: 7 },
|
|
|
|
|
+ { label: '近30天', value: 30 },
|
|
|
|
|
+ { label: '近90天', value: 90 }
|
|
|
|
|
+ ],
|
|
|
|
|
+ currentShortcut: 30,
|
|
|
|
|
+
|
|
|
|
|
+ // 趋势图指标选项
|
|
|
|
|
+ trendMetrics: [
|
|
|
|
|
+ { label: '创建数', value: 'created' },
|
|
|
|
|
+ { label: '完成数', value: 'completed' },
|
|
|
|
|
+ { label: '完成率', value: 'completion_rate' }
|
|
|
|
|
+ ],
|
|
|
|
|
+
|
|
|
|
|
+ // 维度对比选项
|
|
|
|
|
+ dimensionTypes: [
|
|
|
|
|
+ { label: '地块', value: 'plot' },
|
|
|
|
|
+ { label: '作物', value: 'crop' },
|
|
|
|
|
+ { label: '任务类型', value: 'taskType' }
|
|
|
|
|
+ ],
|
|
|
|
|
+ dimensionMetrics: [
|
|
|
|
|
+ { label: '完成率', value: 'completion_rate' },
|
|
|
|
|
+ { label: '准时率', value: 'ontime_rate' }
|
|
|
|
|
+ ],
|
|
|
|
|
+
|
|
|
|
|
+ // 侧栏折叠状态
|
|
|
|
|
+ collapsedSections: {
|
|
|
|
|
+ today: false,
|
|
|
|
|
+ next3Days: false,
|
|
|
|
|
+ overdue: false
|
|
|
|
|
+ },
|
|
|
|
|
+ hoveredTaskId: null,
|
|
|
|
|
+
|
|
|
|
|
+ // 图表空状态
|
|
|
|
|
+ trendEmpty: false,
|
|
|
|
|
+ dimensionEmpty: false,
|
|
|
|
|
+
|
|
|
|
|
+ // 筛选参数
|
|
|
|
|
+ filters: {
|
|
|
|
|
+ dateRange: [],
|
|
|
|
|
+ farms: [],
|
|
|
|
|
+ plots: [],
|
|
|
|
|
+ crops: [],
|
|
|
|
|
+ taskTypes: [],
|
|
|
|
|
+ executors: [],
|
|
|
|
|
+ statuses: []
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 下拉选项
|
|
|
|
|
+ farmOptions: [],
|
|
|
|
|
+ plotOptions: [],
|
|
|
|
|
+ cropOptions: [],
|
|
|
|
|
+ taskTypeOptions: [
|
|
|
|
|
+ { label: '施肥', value: 'fertilize' },
|
|
|
|
|
+ { label: '灌溉', value: 'irrigate' },
|
|
|
|
|
+ { label: '打药', value: 'spray' },
|
|
|
|
|
+ { label: '采摘', value: 'harvest' },
|
|
|
|
|
+ { label: '巡检', value: 'inspect' },
|
|
|
|
|
+ { label: '除草', value: 'weed' },
|
|
|
|
|
+ { label: '其他', value: 'other' }
|
|
|
|
|
+ ],
|
|
|
|
|
+ executorOptions: [],
|
|
|
|
|
+ statusOptions: [
|
|
|
|
|
+ { label: '未开始', value: 'not_started' },
|
|
|
|
|
+ { label: '进行中', value: 'in_progress' },
|
|
|
|
|
+ { label: '已完成', value: 'completed' },
|
|
|
|
|
+ { label: '已逾期', value: 'overdue' },
|
|
|
|
|
+ { label: '已取消', value: 'cancelled' }
|
|
|
|
|
+ ],
|
|
|
|
|
+
|
|
|
|
|
+ // 日期选择器配置
|
|
|
|
|
+ pickerOptions: {
|
|
|
|
|
+ shortcuts: [
|
|
|
|
|
+ {
|
|
|
|
|
+ text: '近7天',
|
|
|
|
|
+ onClick: (picker) => {
|
|
|
|
|
+ const end = new Date()
|
|
|
|
|
+ const start = new Date()
|
|
|
|
|
+ start.setTime(start.getTime() - 3600 * 1000 * 24 * 7)
|
|
|
|
|
+ picker.$emit('pick', [start, end])
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ text: '近30天',
|
|
|
|
|
+ onClick: (picker) => {
|
|
|
|
|
+ const end = new Date()
|
|
|
|
|
+ const start = new Date()
|
|
|
|
|
+ start.setTime(start.getTime() - 3600 * 1000 * 24 * 30)
|
|
|
|
|
+ picker.$emit('pick', [start, end])
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ text: '近90天',
|
|
|
|
|
+ onClick: (picker) => {
|
|
|
|
|
+ const end = new Date()
|
|
|
|
|
+ const start = new Date()
|
|
|
|
|
+ start.setTime(start.getTime() - 3600 * 1000 * 24 * 90)
|
|
|
|
|
+ picker.$emit('pick', [start, end])
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ ]
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // KPI 数据
|
|
|
|
|
+ kpiData: [],
|
|
|
|
|
+
|
|
|
|
|
+ // 趋势图
|
|
|
|
|
+ trendChart: null,
|
|
|
|
|
+ trendMetric: 'created',
|
|
|
|
|
+ trendLoading: false,
|
|
|
|
|
+
|
|
|
|
|
+ // 维度对比图
|
|
|
|
|
+ dimensionChart: null,
|
|
|
|
|
+ dimensionType: 'plot',
|
|
|
|
|
+ dimensionMetric: 'completion_rate',
|
|
|
|
|
+ dimensionLoading: false,
|
|
|
|
|
+
|
|
|
|
|
+ // 待办数据
|
|
|
|
|
+ todoData: {
|
|
|
|
|
+ today: [],
|
|
|
|
|
+ next3Days: [],
|
|
|
|
|
+ overdue: []
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 抽屉
|
|
|
|
|
+ drawerVisible: false,
|
|
|
|
|
+ drawerTitle: '任务明细',
|
|
|
|
|
+ drawerData: [],
|
|
|
|
|
+ drawerLoading: false,
|
|
|
|
|
+ drawerPagination: {
|
|
|
|
|
+ page: 1,
|
|
|
|
|
+ size: 20,
|
|
|
|
|
+ total: 0
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ mounted() {
|
|
|
|
|
+ this.initDefaultFilters()
|
|
|
|
|
+ this.initOptions()
|
|
|
|
|
+ this.loadAllData()
|
|
|
|
|
+ this.initCharts()
|
|
|
|
|
+
|
|
|
|
|
+ // 监听窗口大小变化
|
|
|
|
|
+ window.addEventListener('resize', this.handleResize)
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ beforeDestroy() {
|
|
|
|
|
+ if (this.trendChart) {
|
|
|
|
|
+ this.trendChart.dispose()
|
|
|
|
|
+ }
|
|
|
|
|
+ if (this.dimensionChart) {
|
|
|
|
|
+ this.dimensionChart.dispose()
|
|
|
|
|
+ }
|
|
|
|
|
+ window.removeEventListener('resize', this.handleResize)
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ methods: {
|
|
|
|
|
+ // 初始化默认筛选条件
|
|
|
|
|
+ initDefaultFilters() {
|
|
|
|
|
+ const end = new Date()
|
|
|
|
|
+ const start = new Date()
|
|
|
|
|
+ start.setTime(start.getTime() - 3600 * 1000 * 24 * 30) // 默认近30天
|
|
|
|
|
+
|
|
|
|
|
+ this.filters.dateRange = [
|
|
|
|
|
+ this.formatDate(start),
|
|
|
|
|
+ this.formatDate(end)
|
|
|
|
|
+ ]
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 初始化选项数据
|
|
|
|
|
+ initOptions() {
|
|
|
|
|
+ this.farmOptions = this.mockFarms()
|
|
|
|
|
+ this.plotOptions = this.mockPlots()
|
|
|
|
|
+ this.cropOptions = this.mockCrops()
|
|
|
|
|
+ this.executorOptions = this.mockExecutors()
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 加载所有数据
|
|
|
|
|
+ loadAllData() {
|
|
|
|
|
+ this.loadKpiData()
|
|
|
|
|
+ this.loadTrendData()
|
|
|
|
|
+ this.loadDimensionData()
|
|
|
|
|
+ this.loadTodoData()
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 初始化图表
|
|
|
|
|
+ initCharts() {
|
|
|
|
|
+ this.$nextTick(() => {
|
|
|
|
|
+ this.initTrendChart()
|
|
|
|
|
+ this.initDimensionChart()
|
|
|
|
|
+ })
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 处理查询
|
|
|
|
|
+ handleQuery() {
|
|
|
|
|
+ this.loadAllData()
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 处理重置
|
|
|
|
|
+ handleReset() {
|
|
|
|
|
+ this.$refs.filterForm.resetFields()
|
|
|
|
|
+ this.initDefaultFilters()
|
|
|
|
|
+ this.loadAllData()
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 处理快捷时间选择
|
|
|
|
|
+ handleShortcutClick(shortcut) {
|
|
|
|
|
+ this.currentShortcut = shortcut.value
|
|
|
|
|
+ const end = new Date()
|
|
|
|
|
+ const start = new Date()
|
|
|
|
|
+ start.setTime(start.getTime() - 3600 * 1000 * 24 * shortcut.value)
|
|
|
|
|
+
|
|
|
|
|
+ this.filters.dateRange = [
|
|
|
|
|
+ this.formatDate(start),
|
|
|
|
|
+ this.formatDate(end)
|
|
|
|
|
+ ]
|
|
|
|
|
+ this.loadAllData()
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 处理日期范围变更
|
|
|
|
|
+ handleDateRangeChange() {
|
|
|
|
|
+ this.currentShortcut = null
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 处理KPI菜单操作
|
|
|
|
|
+ handleKpiAction(command) {
|
|
|
|
|
+ if (command.action === 'export') {
|
|
|
|
|
+ this.exportKpiData(command.key)
|
|
|
|
|
+ } else if (command.action === 'detail') {
|
|
|
|
|
+ this.handleKpiClick(command.key)
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 导出KPI数据
|
|
|
|
|
+ exportKpiData(key) {
|
|
|
|
|
+ const kpi = this.kpiData.find(item => item.key === key)
|
|
|
|
|
+ if (kpi) {
|
|
|
|
|
+ this.$message.success(`导出 ${kpi.title} 数据成功`)
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 下载图表
|
|
|
|
|
+ downloadChart(type) {
|
|
|
|
|
+ const chart = type === 'trend' ? this.trendChart : this.dimensionChart
|
|
|
|
|
+ if (chart) {
|
|
|
|
|
+ const url = chart.getDataURL({
|
|
|
|
|
+ type: 'png',
|
|
|
|
|
+ backgroundColor: '#fff'
|
|
|
|
|
+ })
|
|
|
|
|
+ const link = document.createElement('a')
|
|
|
|
|
+ link.href = url
|
|
|
|
|
+ link.download = `${type === 'trend' ? '任务趋势' : '维度对比'}.png`
|
|
|
|
|
+ link.click()
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 全屏显示图表
|
|
|
|
|
+ fullscreenChart(type) {
|
|
|
|
|
+ this.$message.info('全屏功能开发中...')
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 切换侧栏折叠状态
|
|
|
|
|
+ toggleTodoSection(section) {
|
|
|
|
|
+ this.collapsedSections[section] = !this.collapsedSections[section]
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 显示待办操作按钮
|
|
|
|
|
+ showTodoAction(taskId) {
|
|
|
|
|
+ this.hoveredTaskId = taskId
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 隐藏待办操作按钮
|
|
|
|
|
+ hideTodoAction() {
|
|
|
|
|
+ this.hoveredTaskId = null
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 加载KPI数据
|
|
|
|
|
+ loadKpiData() {
|
|
|
|
|
+ const kpi = this.mockKpi(this.filters)
|
|
|
|
|
+ this.kpiData = [
|
|
|
|
|
+ {
|
|
|
|
|
+ title: '任务总数',
|
|
|
|
|
+ value: kpi.total,
|
|
|
|
|
+ type: 'number',
|
|
|
|
|
+ color: '#1F2937',
|
|
|
|
|
+ key: 'total',
|
|
|
|
|
+ icon: 'el-icon-s-data',
|
|
|
|
|
+ iconBg: 'rgba(74, 144, 226, 0.12)'
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ title: '已完成',
|
|
|
|
|
+ value: kpi.done,
|
|
|
|
|
+ type: 'number',
|
|
|
|
|
+ color: '#3BB44A',
|
|
|
|
|
+ key: 'completed',
|
|
|
|
|
+ icon: 'el-icon-circle-check',
|
|
|
|
|
+ iconBg: 'rgba(59, 180, 74, 0.12)'
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ title: '完成率',
|
|
|
|
|
+ value: kpi.doneRate * 100,
|
|
|
|
|
+ type: 'percent',
|
|
|
|
|
+ color: '#3BB44A',
|
|
|
|
|
+ key: 'completion_rate',
|
|
|
|
|
+ icon: 'el-icon-pie-chart',
|
|
|
|
|
+ iconBg: 'rgba(59, 180, 74, 0.12)'
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ title: '准时率',
|
|
|
|
|
+ value: kpi.ontimeRate * 100,
|
|
|
|
|
+ type: 'percent',
|
|
|
|
|
+ color: '#4A90E2',
|
|
|
|
|
+ key: 'ontime_rate',
|
|
|
|
|
+ icon: 'el-icon-timer',
|
|
|
|
|
+ iconBg: 'rgba(74, 144, 226, 0.12)'
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ title: '平均完成时长',
|
|
|
|
|
+ value: kpi.avgDuration,
|
|
|
|
|
+ type: 'duration',
|
|
|
|
|
+ color: '#20B2AA',
|
|
|
|
|
+ key: 'avg_duration',
|
|
|
|
|
+ icon: 'el-icon-stopwatch',
|
|
|
|
|
+ iconBg: 'rgba(32, 178, 170, 0.12)',
|
|
|
|
|
+ unit: kpi.durationUnit
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ title: '逾期数',
|
|
|
|
|
+ value: kpi.overdue,
|
|
|
|
|
+ type: 'number',
|
|
|
|
|
+ color: '#E85D75',
|
|
|
|
|
+ key: 'overdue',
|
|
|
|
|
+ icon: 'el-icon-warning',
|
|
|
|
|
+ iconBg: 'rgba(232, 93, 117, 0.12)'
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ title: '进行中',
|
|
|
|
|
+ value: kpi.inProgress,
|
|
|
|
|
+ type: 'number',
|
|
|
|
|
+ color: '#4A90E2',
|
|
|
|
|
+ key: 'in_progress',
|
|
|
|
|
+ icon: 'el-icon-loading',
|
|
|
|
|
+ iconBg: 'rgba(74, 144, 226, 0.12)'
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ title: '未开始',
|
|
|
|
|
+ value: kpi.notStarted,
|
|
|
|
|
+ type: 'number',
|
|
|
|
|
+ color: '#8B949E',
|
|
|
|
|
+ key: 'not_started',
|
|
|
|
|
+ icon: 'el-icon-time',
|
|
|
|
|
+ iconBg: 'rgba(139, 148, 158, 0.12)'
|
|
|
|
|
+ }
|
|
|
|
|
+ ]
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 加载趋势数据
|
|
|
|
|
+ loadTrendData() {
|
|
|
|
|
+ this.trendLoading = true
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ const data = this.mockTrend(this.filters, this.trendMetric)
|
|
|
|
|
+ this.updateTrendChart(data)
|
|
|
|
|
+ this.trendLoading = false
|
|
|
|
|
+ }, 500)
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 加载维度对比数据
|
|
|
|
|
+ loadDimensionData() {
|
|
|
|
|
+ this.dimensionLoading = true
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ const data = this.mockDimension(this.filters, this.dimensionType, this.dimensionMetric)
|
|
|
|
|
+ this.updateDimensionChart(data)
|
|
|
|
|
+ this.dimensionLoading = false
|
|
|
|
|
+ }, 500)
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 加载待办数据
|
|
|
|
|
+ loadTodoData() {
|
|
|
|
|
+ this.todoData = this.mockTodo(this.filters)
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 初始化趋势图
|
|
|
|
|
+ initTrendChart() {
|
|
|
|
|
+ this.trendChart = echarts.init(this.$refs.trendChart)
|
|
|
|
|
+ this.loadTrendData()
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 更新趋势图
|
|
|
|
|
+ updateTrendChart(data) {
|
|
|
|
|
+ if (!this.trendChart) return
|
|
|
|
|
+
|
|
|
|
|
+ const isPercent = this.trendMetric === 'completion_rate'
|
|
|
|
|
+
|
|
|
|
|
+ const option = {
|
|
|
|
|
+ grid: {
|
|
|
|
|
+ left: '6%',
|
|
|
|
|
+ right: '6%',
|
|
|
|
|
+ bottom: '12%',
|
|
|
|
|
+ top: '20%',
|
|
|
|
|
+ containLabel: true
|
|
|
|
|
+ },
|
|
|
|
|
+ tooltip: {
|
|
|
|
|
+ trigger: 'axis',
|
|
|
|
|
+ backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
|
|
|
|
+ borderColor: '#E4E7ED',
|
|
|
|
|
+ borderWidth: 1,
|
|
|
|
|
+ textStyle: {
|
|
|
|
|
+ color: '#606266'
|
|
|
|
|
+ },
|
|
|
|
|
+ formatter: (params) => {
|
|
|
|
|
+ const param = params[0]
|
|
|
|
|
+ const value = isPercent ? (param.value * 100).toFixed(1) + '%' : param.value
|
|
|
|
|
+ return `${param.axisValue}<br/>${param.seriesName}: ${value}`
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ xAxis: {
|
|
|
|
|
+ type: 'category',
|
|
|
|
|
+ data: data.map(item => item.date),
|
|
|
|
|
+ boundaryGap: false,
|
|
|
|
|
+ axisLabel: {
|
|
|
|
|
+ color: '#909399',
|
|
|
|
|
+ fontSize: 12
|
|
|
|
|
+ },
|
|
|
|
|
+ axisTick: {
|
|
|
|
|
+ show: false
|
|
|
|
|
+ },
|
|
|
|
|
+ axisLine: {
|
|
|
|
|
+ lineStyle: {
|
|
|
|
|
+ color: '#E4E7ED'
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ yAxis: {
|
|
|
|
|
+ type: 'value',
|
|
|
|
|
+ min: isPercent ? 0 : undefined,
|
|
|
|
|
+ max: isPercent ? 1 : undefined,
|
|
|
|
|
+ axisLabel: {
|
|
|
|
|
+ color: '#909399',
|
|
|
|
|
+ fontSize: 12,
|
|
|
|
|
+ formatter: isPercent ? (value) => (value * 100) + '%' : undefined
|
|
|
|
|
+ },
|
|
|
|
|
+ axisTick: {
|
|
|
|
|
+ show: false
|
|
|
|
|
+ },
|
|
|
|
|
+ axisLine: {
|
|
|
|
|
+ show: false
|
|
|
|
|
+ },
|
|
|
|
|
+ splitLine: {
|
|
|
|
|
+ lineStyle: {
|
|
|
|
|
+ color: '#F5F7FA'
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ series: [{
|
|
|
|
|
+ name: this.getTrendMetricName(this.trendMetric),
|
|
|
|
|
+ type: 'line',
|
|
|
|
|
+ data: data.map(item => item.value),
|
|
|
|
|
+ smooth: true,
|
|
|
|
|
+ symbolSize: 5,
|
|
|
|
|
+ symbol: 'circle',
|
|
|
|
|
+ lineStyle: {
|
|
|
|
|
+ width: 2,
|
|
|
|
|
+ color: '#12B67F'
|
|
|
|
|
+ },
|
|
|
|
|
+ areaStyle: {
|
|
|
|
|
+ color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
|
|
|
|
+ { offset: 0, color: 'rgba(18, 182, 127, 0.25)' },
|
|
|
|
|
+ { offset: 1, color: 'rgba(18, 182, 127, 0)' }
|
|
|
|
|
+ ])
|
|
|
|
|
+ },
|
|
|
|
|
+ itemStyle: {
|
|
|
|
|
+ color: '#12B67F',
|
|
|
|
|
+ borderColor: '#fff',
|
|
|
|
|
+ borderWidth: 2
|
|
|
|
|
+ },
|
|
|
|
|
+ emphasis: {
|
|
|
|
|
+ focus: 'series',
|
|
|
|
|
+ itemStyle: {
|
|
|
|
|
+ shadowBlur: 10,
|
|
|
|
|
+ shadowColor: 'rgba(18, 182, 127, 0.3)'
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }]
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ this.trendChart.setOption(option)
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 初始化维度对比图
|
|
|
|
|
+ initDimensionChart() {
|
|
|
|
|
+ this.dimensionChart = echarts.init(this.$refs.dimensionChart)
|
|
|
|
|
+ this.loadDimensionData()
|
|
|
|
|
+
|
|
|
|
|
+ // 添加点击事件
|
|
|
|
|
+ this.dimensionChart.on('click', (params) => {
|
|
|
|
|
+ this.openDrawer({
|
|
|
|
|
+ type: 'dimension',
|
|
|
|
|
+ dimension: this.dimensionType,
|
|
|
|
|
+ name: params.name,
|
|
|
|
|
+ metric: this.dimensionMetric
|
|
|
|
|
+ })
|
|
|
|
|
+ })
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 更新维度对比图
|
|
|
|
|
+ updateDimensionChart(data) {
|
|
|
|
|
+ if (!this.dimensionChart) return
|
|
|
|
|
+
|
|
|
|
|
+ const option = {
|
|
|
|
|
+ grid: {
|
|
|
|
|
+ left: '5%',
|
|
|
|
|
+ right: '8%',
|
|
|
|
|
+ bottom: '8%',
|
|
|
|
|
+ top: '5%',
|
|
|
|
|
+ containLabel: true
|
|
|
|
|
+ },
|
|
|
|
|
+ tooltip: {
|
|
|
|
|
+ trigger: 'axis',
|
|
|
|
|
+ axisPointer: { type: 'shadow' },
|
|
|
|
|
+ backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
|
|
|
|
+ borderColor: '#E4E7ED',
|
|
|
|
|
+ borderWidth: 1,
|
|
|
|
|
+ textStyle: {
|
|
|
|
|
+ color: '#606266'
|
|
|
|
|
+ },
|
|
|
|
|
+ formatter: (params) => {
|
|
|
|
|
+ const param = params[0]
|
|
|
|
|
+ const value = (param.value * 100).toFixed(1) + '%'
|
|
|
|
|
+ return `${param.axisValue}<br/>${param.seriesName}: ${value}`
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ xAxis: {
|
|
|
|
|
+ type: 'value',
|
|
|
|
|
+ max: 1,
|
|
|
|
|
+ axisLabel: {
|
|
|
|
|
+ color: '#909399',
|
|
|
|
|
+ fontSize: 12,
|
|
|
|
|
+ formatter: v => (v * 100).toFixed(0) + '%'
|
|
|
|
|
+ },
|
|
|
|
|
+ axisTick: {
|
|
|
|
|
+ show: false
|
|
|
|
|
+ },
|
|
|
|
|
+ axisLine: {
|
|
|
|
|
+ show: false
|
|
|
|
|
+ },
|
|
|
|
|
+ splitLine: {
|
|
|
|
|
+ lineStyle: {
|
|
|
|
|
+ color: '#F5F7FA'
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ yAxis: {
|
|
|
|
|
+ type: 'category',
|
|
|
|
|
+ data: data.map(item => item.name),
|
|
|
|
|
+ axisLabel: {
|
|
|
|
|
+ color: '#606266',
|
|
|
|
|
+ fontSize: 12,
|
|
|
|
|
+ width: 100,
|
|
|
|
|
+ overflow: 'truncate'
|
|
|
|
|
+ },
|
|
|
|
|
+ axisTick: {
|
|
|
|
|
+ show: false
|
|
|
|
|
+ },
|
|
|
|
|
+ axisLine: {
|
|
|
|
|
+ lineStyle: {
|
|
|
|
|
+ color: '#E4E7ED'
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ series: [{
|
|
|
|
|
+ name: this.getDimensionMetricName(this.dimensionMetric),
|
|
|
|
|
+ type: 'bar',
|
|
|
|
|
+ barWidth: 18,
|
|
|
|
|
+ data: data.map(item => item.value),
|
|
|
|
|
+ itemStyle: {
|
|
|
|
|
+ color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
|
|
|
|
|
+ { offset: 0, color: '#3BB44A' },
|
|
|
|
|
+ { offset: 1, color: '#66CC6A' }
|
|
|
|
|
+ ]),
|
|
|
|
|
+ borderRadius: [0, 8, 8, 0],
|
|
|
|
|
+ shadowBlur: 6,
|
|
|
|
|
+ shadowColor: 'rgba(59, 180, 74, 0.3)',
|
|
|
|
|
+ shadowOffsetX: 2
|
|
|
|
|
+ },
|
|
|
|
|
+ backgroundStyle: {
|
|
|
|
|
+ color: 'rgba(0, 0, 0, 0.05)',
|
|
|
|
|
+ borderRadius: [0, 8, 8, 0]
|
|
|
|
|
+ },
|
|
|
|
|
+ showBackground: true,
|
|
|
|
|
+ label: {
|
|
|
|
|
+ show: true,
|
|
|
|
|
+ position: 'right',
|
|
|
|
|
+ formatter: p => (p.value * 100).toFixed(1) + '%',
|
|
|
|
|
+ color: '#4B5563',
|
|
|
|
|
+ fontSize: 12,
|
|
|
|
|
+ fontWeight: 500,
|
|
|
|
|
+ backgroundColor: '#fff',
|
|
|
|
|
+ borderRadius: 6,
|
|
|
|
|
+ padding: [4, 8],
|
|
|
|
|
+ borderColor: '#E5E7EB',
|
|
|
|
|
+ borderWidth: 1,
|
|
|
|
|
+ shadowBlur: 4,
|
|
|
|
|
+ shadowColor: 'rgba(0, 0, 0, 0.1)'
|
|
|
|
|
+ },
|
|
|
|
|
+ emphasis: {
|
|
|
|
|
+ itemStyle: {
|
|
|
|
|
+ color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
|
|
|
|
|
+ { offset: 0, color: '#2D9A3A' },
|
|
|
|
|
+ { offset: 1, color: '#5ABF60' }
|
|
|
|
|
+ ]),
|
|
|
|
|
+ shadowBlur: 10,
|
|
|
|
|
+ shadowColor: 'rgba(59, 180, 74, 0.5)',
|
|
|
|
|
+ shadowOffsetX: 3
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }]
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ this.dimensionChart.setOption(option)
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 处理趋势指标变更
|
|
|
|
|
+ handleTrendMetricChange(metric) {
|
|
|
|
|
+ if (metric) {
|
|
|
|
|
+ this.trendMetric = metric
|
|
|
|
|
+ }
|
|
|
|
|
+ this.loadTrendData()
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 处理维度类型变更
|
|
|
|
|
+ handleDimensionTypeChange(type) {
|
|
|
|
|
+ if (type) {
|
|
|
|
|
+ this.dimensionType = type
|
|
|
|
|
+ }
|
|
|
|
|
+ this.loadDimensionData()
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 处理维度指标变更
|
|
|
|
|
+ handleDimensionMetricChange(metric) {
|
|
|
|
|
+ if (metric) {
|
|
|
|
|
+ this.dimensionMetric = metric
|
|
|
|
|
+ }
|
|
|
|
|
+ this.loadDimensionData()
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 处理KPI点击
|
|
|
|
|
+ handleKpiClick(key) {
|
|
|
|
|
+ this.openDrawer({ type: 'kpi', key })
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 处理待办点击
|
|
|
|
|
+ handleTodoClick(task) {
|
|
|
|
|
+ this.openDrawer({ type: 'todo', task })
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 处理维度图点击
|
|
|
|
|
+ handleDimensionClick(params) {
|
|
|
|
|
+ this.openDrawer({
|
|
|
|
|
+ type: 'dimension',
|
|
|
|
|
+ dimension: this.dimensionType,
|
|
|
|
|
+ name: params.name,
|
|
|
|
|
+ metric: this.dimensionMetric
|
|
|
|
|
+ })
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 打开抽屉
|
|
|
|
|
+ openDrawer(params) {
|
|
|
|
|
+ this.drawerVisible = true
|
|
|
|
|
+ this.drawerTitle = this.getDrawerTitle(params)
|
|
|
|
|
+ this.loadDrawerData(params)
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 获取抽屉标题
|
|
|
|
|
+ getDrawerTitle(params) {
|
|
|
|
|
+ if (params.type === 'kpi') {
|
|
|
|
|
+ const kpi = this.kpiData.find(item => item.key === params.key)
|
|
|
|
|
+ return `任务明细 - ${kpi ? kpi.title : ''}`
|
|
|
|
|
+ } else if (params.type === 'todo') {
|
|
|
|
|
+ return `任务明细 - ${params.task.taskName}`
|
|
|
|
|
+ } else if (params.type === 'dimension') {
|
|
|
|
|
+ return `任务明细 - ${params.name}`
|
|
|
|
|
+ }
|
|
|
|
|
+ return '任务明细'
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 加载抽屉数据
|
|
|
|
|
+ loadDrawerData(params) {
|
|
|
|
|
+ this.drawerLoading = true
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ const data = this.mockDetail(params)
|
|
|
|
|
+ this.drawerData = data.slice(
|
|
|
|
|
+ (this.drawerPagination.page - 1) * this.drawerPagination.size,
|
|
|
|
|
+ this.drawerPagination.page * this.drawerPagination.size
|
|
|
|
|
+ )
|
|
|
|
|
+ this.drawerPagination.total = data.length
|
|
|
|
|
+ this.drawerLoading = false
|
|
|
|
|
+ }, 500)
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 处理抽屉分页大小变更
|
|
|
|
|
+ handleDrawerSizeChange(size) {
|
|
|
|
|
+ this.drawerPagination.size = size
|
|
|
|
|
+ this.drawerPagination.page = 1
|
|
|
|
|
+ // 重新加载数据
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 处理抽屉页码变更
|
|
|
|
|
+ handleDrawerCurrentChange(page) {
|
|
|
|
|
+ this.drawerPagination.page = page
|
|
|
|
|
+ // 重新加载数据
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 处理抽屉关闭
|
|
|
|
|
+ handleDrawerClose() {
|
|
|
|
|
+ this.drawerVisible = false
|
|
|
|
|
+ this.drawerPagination.page = 1
|
|
|
|
|
+ this.drawerPagination.size = 20
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 导出CSV
|
|
|
|
|
+ handleExportCsv() {
|
|
|
|
|
+ const csvContent = this.generateCsvContent(this.drawerData)
|
|
|
|
|
+ this.downloadCsv(csvContent, '任务明细.csv')
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 生成CSV内容
|
|
|
|
|
+ generateCsvContent(data) {
|
|
|
|
|
+ const headers = ['任务名称', '类型', '作物', '地块', '执行人', '计划开始', '计划结束', '状态']
|
|
|
|
|
+ const rows = data.map(item => [
|
|
|
|
|
+ item.taskName,
|
|
|
|
|
+ this.getTaskTypeName(item.taskType),
|
|
|
|
|
+ item.cropName,
|
|
|
|
|
+ item.plotName,
|
|
|
|
|
+ item.executor,
|
|
|
|
|
+ item.startTime,
|
|
|
|
|
+ item.endTime,
|
|
|
|
|
+ this.getStatusName(item.status)
|
|
|
|
|
+ ])
|
|
|
|
|
+
|
|
|
|
|
+ return [headers, ...rows].map(row => row.join(',')).join('\n')
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 下载CSV
|
|
|
|
|
+ downloadCsv(content, filename) {
|
|
|
|
|
+ const blob = new Blob(['\ufeff' + content], { type: 'text/csv;charset=utf-8;' })
|
|
|
|
|
+ const link = document.createElement('a')
|
|
|
|
|
+ const url = URL.createObjectURL(blob)
|
|
|
|
|
+ link.setAttribute('href', url)
|
|
|
|
|
+ link.setAttribute('download', filename)
|
|
|
|
|
+ link.style.visibility = 'hidden'
|
|
|
|
|
+ document.body.appendChild(link)
|
|
|
|
|
+ link.click()
|
|
|
|
|
+ document.body.removeChild(link)
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 处理窗口大小变更
|
|
|
|
|
+ handleResize() {
|
|
|
|
|
+ if (this.trendChart) {
|
|
|
|
|
+ this.trendChart.resize()
|
|
|
|
|
+ }
|
|
|
|
|
+ if (this.dimensionChart) {
|
|
|
|
|
+ this.dimensionChart.resize()
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 格式化KPI值
|
|
|
|
|
+ formatKpiValue(value, type) {
|
|
|
|
|
+ if (type === 'percent') {
|
|
|
|
|
+ return (value * 100).toFixed(1) + '%'
|
|
|
|
|
+ }
|
|
|
|
|
+ return value.toLocaleString()
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 获取趋势指标名称
|
|
|
|
|
+ getTrendMetricName(metric) {
|
|
|
|
|
+ const names = {
|
|
|
|
|
+ created: '创建数',
|
|
|
|
|
+ completed: '完成数',
|
|
|
|
|
+ completion_rate: '完成率'
|
|
|
|
|
+ }
|
|
|
|
|
+ return names[metric] || metric
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 获取维度指标名称
|
|
|
|
|
+ getDimensionMetricName(metric) {
|
|
|
|
|
+ const names = {
|
|
|
|
|
+ completion_rate: '完成率',
|
|
|
|
|
+ ontime_rate: '准时率'
|
|
|
|
|
+ }
|
|
|
|
|
+ return names[metric] || metric
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 获取任务类型名称
|
|
|
|
|
+ getTaskTypeName(type) {
|
|
|
|
|
+ const names = {
|
|
|
|
|
+ fertilize: '施肥',
|
|
|
|
|
+ irrigate: '灌溉',
|
|
|
|
|
+ spray: '打药',
|
|
|
|
|
+ harvest: '采摘',
|
|
|
|
|
+ inspect: '巡检',
|
|
|
|
|
+ weed: '除草',
|
|
|
|
|
+ other: '其他'
|
|
|
|
|
+ }
|
|
|
|
|
+ return names[type] || type
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 获取任务类型标签类型
|
|
|
|
|
+ getTaskTypeTagType(type) {
|
|
|
|
|
+ const types = {
|
|
|
|
|
+ fertilize: 'success',
|
|
|
|
|
+ irrigate: 'primary',
|
|
|
|
|
+ spray: 'warning',
|
|
|
|
|
+ harvest: 'danger',
|
|
|
|
|
+ inspect: 'info',
|
|
|
|
|
+ weed: '',
|
|
|
|
|
+ other: 'default'
|
|
|
|
|
+ }
|
|
|
|
|
+ return types[type] || ''
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 获取状态名称
|
|
|
|
|
+ getStatusName(status) {
|
|
|
|
|
+ const names = {
|
|
|
|
|
+ not_started: '未开始',
|
|
|
|
|
+ in_progress: '进行中',
|
|
|
|
|
+ completed: '已完成',
|
|
|
|
|
+ overdue: '已逾期',
|
|
|
|
|
+ cancelled: '已取消'
|
|
|
|
|
+ }
|
|
|
|
|
+ return names[status] || status
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 获取状态标签类型
|
|
|
|
|
+ getStatusTagType(status) {
|
|
|
|
|
+ const types = {
|
|
|
|
|
+ not_started: 'info',
|
|
|
|
|
+ in_progress: 'primary',
|
|
|
|
|
+ completed: 'success',
|
|
|
|
|
+ overdue: 'danger',
|
|
|
|
|
+ cancelled: 'default'
|
|
|
|
|
+ }
|
|
|
|
|
+ return types[status] || ''
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 格式化日期
|
|
|
|
|
+ formatDate(date) {
|
|
|
|
|
+ const d = new Date(date)
|
|
|
|
|
+ const year = d.getFullYear()
|
|
|
|
|
+ const month = String(d.getMonth() + 1).padStart(2, '0')
|
|
|
|
|
+ const day = String(d.getDate()).padStart(2, '0')
|
|
|
|
|
+ return `${year}-${month}-${day}`
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 生成哈希种子
|
|
|
|
|
+ generateSeed(filters) {
|
|
|
|
|
+ const str = JSON.stringify(filters)
|
|
|
|
|
+ let hash = 0
|
|
|
|
|
+ for (let i = 0; i < str.length; i++) {
|
|
|
|
|
+ const char = str.charCodeAt(i)
|
|
|
|
|
+ hash = ((hash << 5) - hash) + char
|
|
|
|
|
+ hash = hash & hash // Convert to 32bit integer
|
|
|
|
|
+ }
|
|
|
|
|
+ return Math.abs(hash)
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // Mock 数据生成方法
|
|
|
|
|
+ mockFarms() {
|
|
|
|
|
+ return [
|
|
|
|
|
+ { label: '阳光农场', value: 'farm1' },
|
|
|
|
|
+ { label: '绿野农场', value: 'farm2' },
|
|
|
|
|
+ { label: '丰收农场', value: 'farm3' },
|
|
|
|
|
+ { label: '田园农场', value: 'farm4' },
|
|
|
|
|
+ { label: '科技农场', value: 'farm5' }
|
|
|
|
|
+ ]
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ mockPlots() {
|
|
|
|
|
+ return [
|
|
|
|
|
+ { label: '东区1号田', value: 'plot1' },
|
|
|
|
|
+ { label: '西区2号田', value: 'plot2' },
|
|
|
|
|
+ { label: '南区果园', value: 'plot3' },
|
|
|
|
|
+ { label: '北区菜地', value: 'plot4' },
|
|
|
|
|
+ { label: '温室大棚A区', value: 'plot5' },
|
|
|
|
|
+ { label: '智能温室B区', value: 'plot6' }
|
|
|
|
|
+ ]
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ mockCrops() {
|
|
|
|
|
+ return [
|
|
|
|
|
+ { label: '水稻', value: 'rice' },
|
|
|
|
|
+ { label: '玉米', value: 'corn' },
|
|
|
|
|
+ { label: '小麦', value: 'wheat' },
|
|
|
|
|
+ { label: '番茄', value: 'tomato' },
|
|
|
|
|
+ { label: '黄瓜', value: 'cucumber' },
|
|
|
|
|
+ { label: '草莓', value: 'strawberry' }
|
|
|
|
|
+ ]
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ mockExecutors() {
|
|
|
|
|
+ return [
|
|
|
|
|
+ { label: '张三', value: 'user1' },
|
|
|
|
|
+ { label: '李四', value: 'user2' },
|
|
|
|
|
+ { label: '王五', value: 'user3' },
|
|
|
|
|
+ { label: '赵六', value: 'user4' },
|
|
|
|
|
+ { label: '钱七', value: 'user5' }
|
|
|
|
|
+ ]
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ mockKpi(filters) {
|
|
|
|
|
+ const seed = this.generateSeed(filters)
|
|
|
|
|
+ const random = seedrandom(seed)
|
|
|
|
|
+
|
|
|
|
|
+ const total = Math.floor(random() * 500) + 200
|
|
|
|
|
+ const done = Math.floor(random() * total * 0.8)
|
|
|
|
|
+ const overdue = Math.floor(random() * total * 0.1)
|
|
|
|
|
+ const inProgress = Math.floor(random() * (total - done - overdue) * 0.6)
|
|
|
|
|
+ const notStarted = total - done - overdue - inProgress
|
|
|
|
|
+
|
|
|
|
|
+ const doneRate = total > 0 ? done / total : 0
|
|
|
|
|
+ const ontimeRate = done > 0 ? (done - Math.floor(random() * done * 0.2)) / done : 0
|
|
|
|
|
+
|
|
|
|
|
+ // 生成平均完成时长数据
|
|
|
|
|
+ const duration = random() * 96 + 4 // 4-100小时
|
|
|
|
|
+ let avgDuration, durationUnit
|
|
|
|
|
+
|
|
|
|
|
+ if (duration >= 24) {
|
|
|
|
|
+ avgDuration = Number((duration / 24).toFixed(1))
|
|
|
|
|
+ durationUnit = '天'
|
|
|
|
|
+ } else {
|
|
|
|
|
+ avgDuration = Number(duration.toFixed(1))
|
|
|
|
|
+ durationUnit = '小时'
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ total,
|
|
|
|
|
+ done,
|
|
|
|
|
+ doneRate,
|
|
|
|
|
+ ontimeRate,
|
|
|
|
|
+ overdue,
|
|
|
|
|
+ inProgress,
|
|
|
|
|
+ notStarted,
|
|
|
|
|
+ avgDuration,
|
|
|
|
|
+ durationUnit
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ mockTrend(filters, metric) {
|
|
|
|
|
+ const seed = this.generateSeed({ ...filters, metric })
|
|
|
|
|
+ const random = seedrandom(seed)
|
|
|
|
|
+
|
|
|
|
|
+ const data = []
|
|
|
|
|
+ const days = 30
|
|
|
|
|
+ const startDate = new Date(filters.dateRange[0])
|
|
|
|
|
+
|
|
|
|
|
+ for (let i = 0; i < days; i++) {
|
|
|
|
|
+ const date = new Date(startDate)
|
|
|
|
|
+ date.setDate(date.getDate() + i)
|
|
|
|
|
+
|
|
|
|
|
+ let value
|
|
|
|
|
+ if (metric === 'completion_rate') {
|
|
|
|
|
+ value = 0.6 + random() * 0.3 // 60%-90%
|
|
|
|
|
+ } else {
|
|
|
|
|
+ value = Math.floor(random() * 50) + 10 // 10-60
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ data.push({
|
|
|
|
|
+ date: this.formatDate(date),
|
|
|
|
|
+ value
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return data
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ mockDimension(filters, type, metric) {
|
|
|
|
|
+ const seed = this.generateSeed({ ...filters, type, metric })
|
|
|
|
|
+ const random = seedrandom(seed)
|
|
|
|
|
+
|
|
|
|
|
+ let names = []
|
|
|
|
|
+ if (type === 'plot') {
|
|
|
|
|
+ names = this.plotOptions.map(item => item.label)
|
|
|
|
|
+ } else if (type === 'crop') {
|
|
|
|
|
+ names = this.cropOptions.map(item => item.label)
|
|
|
|
|
+ } else if (type === 'taskType') {
|
|
|
|
|
+ names = this.taskTypeOptions.map(item => item.label)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return names.map(name => ({
|
|
|
|
|
+ name,
|
|
|
|
|
+ value: 0.5 + random() * 0.4 // 50%-90%
|
|
|
|
|
+ }))
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ mockTodo(filters) {
|
|
|
|
|
+ const seed = this.generateSeed(filters)
|
|
|
|
|
+ const random = seedrandom(seed)
|
|
|
|
|
+
|
|
|
|
|
+ const today = []
|
|
|
|
|
+ const next3Days = []
|
|
|
|
|
+ const overdue = []
|
|
|
|
|
+
|
|
|
|
|
+ // 生成今日到期任务
|
|
|
|
|
+ for (let i = 0; i < 3; i++) {
|
|
|
|
|
+ today.push({
|
|
|
|
|
+ id: `today_${i}`,
|
|
|
|
|
+ taskName: `任务${i + 1}`,
|
|
|
|
|
+ plotName: this.plotOptions[Math.floor(random() * this.plotOptions.length)].label,
|
|
|
|
|
+ executor: this.executorOptions[Math.floor(random() * this.executorOptions.length)].label,
|
|
|
|
|
+ deadline: '今日 18:00'
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 生成未来3天到期任务
|
|
|
|
|
+ for (let i = 0; i < 5; i++) {
|
|
|
|
|
+ next3Days.push({
|
|
|
|
|
+ id: `next_${i}`,
|
|
|
|
|
+ taskName: `任务${i + 4}`,
|
|
|
|
|
+ plotName: this.plotOptions[Math.floor(random() * this.plotOptions.length)].label,
|
|
|
|
|
+ executor: this.executorOptions[Math.floor(random() * this.executorOptions.length)].label,
|
|
|
|
|
+ deadline: `${i + 1}天后`
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 生成逾期任务
|
|
|
|
|
+ for (let i = 0; i < 2; i++) {
|
|
|
|
|
+ overdue.push({
|
|
|
|
|
+ id: `overdue_${i}`,
|
|
|
|
|
+ taskName: `逾期任务${i + 1}`,
|
|
|
|
|
+ plotName: this.plotOptions[Math.floor(random() * this.plotOptions.length)].label,
|
|
|
|
|
+ executor: this.executorOptions[Math.floor(random() * this.executorOptions.length)].label,
|
|
|
|
|
+ deadline: `逾期${i + 1}天`
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return { today, next3Days, overdue }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ mockDetail(params) {
|
|
|
|
|
+ const seed = this.generateSeed(params)
|
|
|
|
|
+ const random = seedrandom(seed)
|
|
|
|
|
+
|
|
|
|
|
+ const data = []
|
|
|
|
|
+ const count = Math.floor(random() * 30) + 20
|
|
|
|
|
+
|
|
|
|
|
+ for (let i = 0; i < count; i++) {
|
|
|
|
|
+ data.push({
|
|
|
|
|
+ id: i + 1,
|
|
|
|
|
+ taskName: `任务${i + 1}`,
|
|
|
|
|
+ taskType: this.taskTypeOptions[Math.floor(random() * this.taskTypeOptions.length)].value,
|
|
|
|
|
+ cropName: this.cropOptions[Math.floor(random() * this.cropOptions.length)].label,
|
|
|
|
|
+ plotName: this.plotOptions[Math.floor(random() * this.plotOptions.length)].label,
|
|
|
|
|
+ executor: this.executorOptions[Math.floor(random() * this.executorOptions.length)].label,
|
|
|
|
|
+ startTime: '2024-01-15 08:00',
|
|
|
|
|
+ endTime: '2024-01-15 18:00',
|
|
|
|
|
+ status: this.statusOptions[Math.floor(random() * this.statusOptions.length)].value
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return data
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+</script>
|
|
|
|
|
+
|
|
|
|
|
+<style scoped>
|
|
|
|
|
+/* 页面背景与容器 */
|
|
|
|
|
+.stats-page {
|
|
|
|
|
+ background: #F7F8FA;
|
|
|
|
|
+ min-height: 100vh;
|
|
|
|
|
+ padding: 16px;
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ box-sizing: border-box;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 通用卡片底板 */
|
|
|
|
|
+.stats-card {
|
|
|
|
|
+ --card-bg: #fff;
|
|
|
|
|
+ --card-radius: 10px;
|
|
|
|
|
+ --card-border: 1px solid #EEF1F5;
|
|
|
|
|
+ --card-shadow: 0 6px 18px rgba(22,31,41,.06);
|
|
|
|
|
+ --card-shadow-hover: 0 10px 24px rgba(22,31,41,.10);
|
|
|
|
|
+
|
|
|
|
|
+ background: var(--card-bg);
|
|
|
|
|
+ border: var(--card-border);
|
|
|
|
|
+ border-radius: var(--card-radius);
|
|
|
|
|
+ box-shadow: var(--card-shadow);
|
|
|
|
|
+ padding: 20px;
|
|
|
|
|
+ margin-left: 0;
|
|
|
|
|
+ margin-right: 0;
|
|
|
|
|
+ transition: box-shadow .2s ease, transform .2s ease;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.stats-card:hover {
|
|
|
|
|
+ box-shadow: var(--card-shadow-hover);
|
|
|
|
|
+ transform: translateY(-2px);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* KPI 卡片区域 */
|
|
|
|
|
+.kpi-section {
|
|
|
|
|
+ margin-bottom: 16px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* KPI 网格布局 */
|
|
|
|
|
+.kpi-grid {
|
|
|
|
|
+ display: grid;
|
|
|
|
|
+ grid-template-columns: repeat(8, 1fr);
|
|
|
|
|
+ gap: 12px;
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.kpi-card {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 8px;
|
|
|
|
|
+ padding: 12px;
|
|
|
|
|
+ border: 1px solid #E5E7EB;
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ background: #ffffff;
|
|
|
|
|
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04), 0 1px 3px rgba(0, 0, 0, 0.06);
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ transition: all 0.25s ease;
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ min-height: 68px;
|
|
|
|
|
+ box-sizing: border-box;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.kpi-card:hover {
|
|
|
|
|
+ transform: translateY(-2px);
|
|
|
|
|
+ box-shadow: 0 8px 25px rgba(0, 0, 0, 0.08), 0 3px 10px rgba(0, 0, 0, 0.1);
|
|
|
|
|
+ border-color: #D1D5DB;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.kpi-card__icon {
|
|
|
|
|
+ width: 28px;
|
|
|
|
|
+ height: 28px;
|
|
|
|
|
+ border-radius: 6px;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.kpi-card__content {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ min-width: 0;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.kpi-card__meta {
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: #6B7280;
|
|
|
|
|
+ line-height: 16px;
|
|
|
|
|
+ margin-bottom: 4px;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.kpi-card__value {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: baseline;
|
|
|
|
|
+ gap: 4px;
|
|
|
|
|
+ line-height: 1.2;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.kpi-number {
|
|
|
|
|
+ font-size: 18px;
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+ color: #111827;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.kpi-unit {
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+ color: #6B7280;
|
|
|
|
|
+ margin-left: 2px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.kpi-card__menu {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ top: 8px;
|
|
|
|
|
+ right: 8px;
|
|
|
|
|
+ opacity: 0;
|
|
|
|
|
+ transition: opacity 0.2s ease;
|
|
|
|
|
+ z-index: 10;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.kpi-card:hover .kpi-card__menu {
|
|
|
|
|
+ opacity: 1;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.kpi-menu-trigger {
|
|
|
|
|
+ display: inline-flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ width: 20px;
|
|
|
|
|
+ height: 20px;
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+ color: #9CA3AF;
|
|
|
|
|
+ transition: all 0.2s ease;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.kpi-menu-trigger:hover {
|
|
|
|
|
+ background: #F3F4F6;
|
|
|
|
|
+ color: #6B7280;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 保底措施 */
|
|
|
|
|
+.el-card {
|
|
|
|
|
+ border: 1px solid #EEF1F5 !important;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.el-card__body {
|
|
|
|
|
+ padding: 0 !important;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 筛选区域 */
|
|
|
|
|
+.time-shortcuts {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ margin-bottom: 16px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.shortcuts-label {
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ color: #606266;
|
|
|
|
|
+ margin-right: 12px;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.shortcut-pills {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ gap: 8px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.shortcut-pill {
|
|
|
|
|
+ padding: 6px 12px;
|
|
|
|
|
+ border: 1px solid #EEF1F5;
|
|
|
|
|
+ border-radius: 16px;
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+ color: #606266;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ transition: all 0.2s ease;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.shortcut-pill:hover {
|
|
|
|
|
+ border-color: #12B67F;
|
|
|
|
|
+ color: #12B67F;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.shortcut-pill.active {
|
|
|
|
|
+ background: #12B67F;
|
|
|
|
|
+ border-color: #12B67F;
|
|
|
|
|
+ color: #fff;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.filter-divider {
|
|
|
|
|
+ height: 1px;
|
|
|
|
|
+ background: #EFF2F7;
|
|
|
|
|
+ margin: 16px 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.filter-form .el-form-item {
|
|
|
|
|
+ margin-right: 12px;
|
|
|
|
|
+ margin-bottom: 12px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.action-buttons {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ gap: 8px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.btn-primary {
|
|
|
|
|
+ background: linear-gradient(135deg, #12B67F 0%, #21ba45 100%) !important;
|
|
|
|
|
+ border: none !important;
|
|
|
|
|
+ border-radius: 8px !important;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ padding: 8px 20px !important;
|
|
|
|
|
+ transition: all 0.3s ease;
|
|
|
|
|
+ transform: translateY(0);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.btn-primary:hover {
|
|
|
|
|
+ transform: translateY(-1px);
|
|
|
|
|
+ box-shadow: 0 4px 12px rgba(18, 182, 127, 0.3);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.btn-secondary {
|
|
|
|
|
+ background: #fff !important;
|
|
|
|
|
+ border: 1px solid #EEF1F5 !important;
|
|
|
|
|
+ color: #606266 !important;
|
|
|
|
|
+ border-radius: 8px !important;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+ padding: 8px 20px !important;
|
|
|
|
|
+ transition: all 0.3s ease;
|
|
|
|
|
+ transform: translateY(0);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.btn-secondary:hover {
|
|
|
|
|
+ transform: translateY(-1px);
|
|
|
|
|
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
|
|
|
+ border-color: #12B67F !important;
|
|
|
|
|
+ color: #12B67F !important;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 图表区域 */
|
|
|
|
|
+.main-content {
|
|
|
|
|
+ margin-bottom: 0;
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 响应式调整 */
|
|
|
|
|
+@media (max-width: 1400px) {
|
|
|
|
|
+ .kpi-grid {
|
|
|
|
|
+ grid-template-columns: repeat(4, 1fr);
|
|
|
|
|
+ gap: 10px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .kpi-card {
|
|
|
|
|
+ padding: 10px;
|
|
|
|
|
+ gap: 6px;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+@media (max-width: 992px) {
|
|
|
|
|
+ .kpi-grid {
|
|
|
|
|
+ grid-template-columns: repeat(2, 1fr);
|
|
|
|
|
+ gap: 12px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .kpi-card {
|
|
|
|
|
+ padding: 14px;
|
|
|
|
|
+ gap: 8px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .kpi-card__icon {
|
|
|
|
|
+ width: 28px;
|
|
|
|
|
+ height: 28px;
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .kpi-number {
|
|
|
|
|
+ font-size: 18px;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+@media (max-width: 768px) {
|
|
|
|
|
+ .stats-page {
|
|
|
|
|
+ padding: 12px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .kpi-grid {
|
|
|
|
|
+ grid-template-columns: repeat(2, 1fr);
|
|
|
|
|
+ gap: 10px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .kpi-card {
|
|
|
|
|
+ padding: 12px;
|
|
|
|
|
+ gap: 8px;
|
|
|
|
|
+ min-height: 70px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .kpi-card__icon {
|
|
|
|
|
+ width: 26px;
|
|
|
|
|
+ height: 26px;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .kpi-card__meta {
|
|
|
|
|
+ font-size: 11px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .kpi-number {
|
|
|
|
|
+ font-size: 16px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .kpi-unit {
|
|
|
|
|
+ font-size: 10px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .chart-container {
|
|
|
|
|
+ height: 300px !important;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+@media (max-width: 480px) {
|
|
|
|
|
+ .kpi-grid {
|
|
|
|
|
+ grid-template-columns: 1fr;
|
|
|
|
|
+ gap: 8px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .kpi-card {
|
|
|
|
|
+ padding: 10px;
|
|
|
|
|
+ min-height: 60px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .kpi-card__icon {
|
|
|
|
|
+ width: 24px;
|
|
|
|
|
+ height: 24px;
|
|
|
|
|
+ font-size: 11px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .kpi-number {
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.chart-header {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ align-items: flex-start;
|
|
|
|
|
+ margin-bottom: 24px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.chart-title {
|
|
|
|
|
+ margin: 0;
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #1F2329;
|
|
|
|
|
+ margin-bottom: 4px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.chart-controls {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 20px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.dimension-controls {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 20px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.control-group {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 8px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.control-label {
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: #606266;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.metric-segment {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ background: #F7F8FA;
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ padding: 2px;
|
|
|
|
|
+ border: 1px solid #EEF1F5;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.segment-btn {
|
|
|
|
|
+ padding: 6px 12px;
|
|
|
|
|
+ border: none;
|
|
|
|
|
+ background: transparent;
|
|
|
|
|
+ color: #606266;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+ border-radius: 6px;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ transition: all 0.2s ease;
|
|
|
|
|
+ white-space: nowrap;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.segment-btn:hover {
|
|
|
|
|
+ color: #12B67F;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.segment-btn.active {
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+ color: #12B67F;
|
|
|
|
|
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.chart-tools {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ gap: 8px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.chart-container {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ padding: 0;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 确保图表在卡片中居中 */
|
|
|
|
|
+.chart-section .chart-container > div {
|
|
|
|
|
+ width: 100% !important;
|
|
|
|
|
+ height: 100% !important;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.chart-empty {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ height: 300px;
|
|
|
|
|
+ color: #909399;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.chart-empty-icon {
|
|
|
|
|
+ font-size: 48px;
|
|
|
|
|
+ margin-bottom: 12px;
|
|
|
|
|
+ opacity: 0.5;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.chart-empty-text {
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ margin: 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 右侧侧栏 */
|
|
|
|
|
+.sidebar-sticky {
|
|
|
|
|
+ position: sticky;
|
|
|
|
|
+ top: 88px;
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 主内容区域对齐 */
|
|
|
|
|
+.main-content > .el-col {
|
|
|
|
|
+ padding-left: 8px;
|
|
|
|
|
+ padding-right: 8px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.main-content > .el-col:first-child {
|
|
|
|
|
+ padding-left: 0 !important;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.main-content > .el-col:last-child {
|
|
|
|
|
+ padding-right: 0 !important;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 统一对齐样式 - 确保所有区域都完全对齐 */
|
|
|
|
|
+.main-content {
|
|
|
|
|
+ margin-left: 0 !important;
|
|
|
|
|
+ margin-right: 0 !important;
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 确保待办卡片完全撑满容器 */
|
|
|
|
|
+.todo-section {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ margin-bottom: 16px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.todo-section:last-child {
|
|
|
|
|
+ margin-bottom: 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+@media (max-width: 992px) {
|
|
|
|
|
+ .sidebar-sticky {
|
|
|
|
|
+ position: static;
|
|
|
|
|
+ margin-top: 16px;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.todo-header {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ user-select: none;
|
|
|
|
|
+ margin-bottom: 16px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.todo-title-wrapper {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 8px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.todo-header h4 {
|
|
|
|
|
+ margin: 0;
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #1F2329;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.todo-badge {
|
|
|
|
|
+ line-height: 1;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.toggle-icon {
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: #909399;
|
|
|
|
|
+ transition: transform 0.3s ease;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.toggle-icon.collapsed {
|
|
|
|
|
+ transform: rotate(-90deg);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.todo-list {
|
|
|
|
|
+ max-height: 300px;
|
|
|
|
|
+ overflow-y: auto;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.timeline-container {
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.todo-item {
|
|
|
|
|
+ padding: 12px 0;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ transition: all 0.2s ease;
|
|
|
|
|
+ border-bottom: 1px dashed #EFF2F7;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: flex-start;
|
|
|
|
|
+ gap: 12px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.todo-item:last-child {
|
|
|
|
|
+ border-bottom: none;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.todo-item:hover {
|
|
|
|
|
+ background-color: #F7F8FA;
|
|
|
|
|
+ margin: 0 -12px;
|
|
|
|
|
+ padding: 12px;
|
|
|
|
|
+ border-radius: 6px;
|
|
|
|
|
+ border-bottom: 1px dashed #EFF2F7;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.todo-item:last-child:hover {
|
|
|
|
|
+ border-bottom: none;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.todo-item.overdue {
|
|
|
|
|
+ background: #FFF2F0;
|
|
|
|
|
+ border-radius: 6px;
|
|
|
|
|
+ padding: 8px 10px;
|
|
|
|
|
+ margin: 4px 0;
|
|
|
|
|
+ border-bottom: none;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.todo-item.overdue:hover {
|
|
|
|
|
+ background: #FFE8E6;
|
|
|
|
|
+ margin: 4px -12px;
|
|
|
|
|
+ padding: 8px 22px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.todo-content {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ min-width: 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.task-name {
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #1F2329;
|
|
|
|
|
+ margin-bottom: 4px;
|
|
|
|
|
+ line-height: 1.4;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.task-meta {
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: #606266;
|
|
|
|
|
+ margin-bottom: 4px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.task-time {
|
|
|
|
|
+ font-size: 11px;
|
|
|
|
|
+ color: #909399;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.task-time.danger {
|
|
|
|
|
+ color: #F56C6C;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.todo-action {
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+ opacity: 0;
|
|
|
|
|
+ transition: opacity 0.2s ease;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.todo-item:hover .todo-action {
|
|
|
|
|
+ opacity: 1;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.action-btn {
|
|
|
|
|
+ padding: 4px 8px;
|
|
|
|
|
+ font-size: 11px;
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+ color: #12B67F;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.action-btn:hover {
|
|
|
|
|
+ background: rgba(18, 182, 127, 0.1);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.drawer-content {
|
|
|
|
|
+ padding: 20px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.drawer-content .el-table {
|
|
|
|
|
+ margin-bottom: 20px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.drawer-content .el-table th {
|
|
|
|
|
+ background-color: #f8f9fa;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.drawer-content .el-table td {
|
|
|
|
|
+ padding: 12px 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.drawer-actions {
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+}
|
|
|
|
|
+</style>
|