0.5.1 更新首页

This commit is contained in:
磷叶 2025-02-24 14:56:31 +08:00
parent 3da7554476
commit a0636c093f
15 changed files with 420 additions and 455 deletions

View File

@ -11,4 +11,4 @@ VUE_APP_BASE_API = '/prod-api'
VUE_APP_QINIU_DOMAIN = 'https://api.ccttiot.com' VUE_APP_QINIU_DOMAIN = 'https://api.ccttiot.com'
# WebSocket地址 # WebSocket地址
VUE_APP_WS_HOST = 'wss://pm.chuangtewl.com' VUE_APP_WS_HOST = 'wss://pm.chuangtewl.com/prod-api'

View File

@ -1,13 +1,5 @@
import request from '@/utils/request' import request from '@/utils/request'
// 获取本人概览
export function getMineBrief() {
return request({
url: '/dashboard/mineBrief',
method: 'get'
})
}
// 获取概览 // 获取概览
export function getBrief(params) { export function getBrief(params) {
return request({ return request({

View File

@ -43,14 +43,16 @@ export default {
}, },
backgroundColor() { backgroundColor() {
const colors = [ const colors = [
'#BBDEFB', // '#90CAF9', //
'#E1BEE7', // '#CE93D8', //
'#C8E6C9', // 绿 '#FFB74D', //
'#FFE0B2', // '#81D4FA', //
'#CFD8DC', // '#F48FB1', //
'#B3E5FC', // '#E57373', //
'#F8BBD0', // '#9575CD', //
'#D7CCC8' // '#4DB6AC', //
'#FF8A65', //
'#7986CB' //
] ]
// //
const nameStr = this.name || '' const nameStr = this.name || ''

View File

@ -0,0 +1,121 @@
<template>
<el-card class="stat-card" shadow="hover">
<div class="stat-header">
<div class="stat-title">
<i :class="icon" :style="{ color: iconColor }"></i>
<span>{{ title }}</span>
</div>
</div>
<div class="stat-main">
<div class="main-value">
<count-to
:start-val="0"
:end-val="mainValue"
:duration="2000"
class="main-count"
/>
<div class="main-label">{{ mainLabel }}</div>
</div>
</div>
<div class="stat-footer">
<div v-for="(item, index) in items" :key="index" class="footer-item">
<span class="item-label">{{ item.label }}</span>
<count-to
:start-val="0"
:end-val="item.value"
:duration="2000"
class="item-value"
/>
</div>
</div>
</el-card>
</template>
<script>
import CountTo from 'vue-count-to'
export default {
name: 'StatCard',
components: {
CountTo
},
props: {
title: {
type: String,
required: true
},
icon: {
type: String,
required: true
},
iconColor: {
type: String,
default: '#409EFF'
},
mainValue: {
type: Number,
required: true
},
mainLabel: {
type: String,
required: true
},
items: {
type: Array,
default: () => []
}
}
}
</script>
<style lang="scss" scoped>
.stat-card {
height: 100%;
.stat-header {
margin-bottom: 20px;
.stat-title {
display: flex;
align-items: center;
font-size: 16px;
i {
margin-right: 8px;
font-size: 20px;
}
}
}
.stat-main {
margin-bottom: 20px;
.main-value {
text-align: center;
.main-count {
font-size: 36px;
font-weight: bold;
color: #303133;
line-height: 1.2;
}
.main-label {
margin-top: 8px;
font-size: 14px;
color: #909399;
}
}
}
.stat-footer {
display: flex;
justify-content: space-between;
.footer-item {
text-align: center;
.item-label {
display: block;
font-size: 12px;
color: #909399;
margin-bottom: 4px;
}
.item-value {
font-size: 16px;
color: #606266;
}
}
}
}
</style>

View File

@ -110,3 +110,29 @@ export const MessageBstType = {
CUSTOMER: "customer", // 客户 CUSTOMER: "customer", // 客户
} }
// 统计数据键值
export const BriefKeys = {
// 项目
PROJECT_STATUS: "projectStatus", // 项目状态
PROJECT_DEV_COMPLETED: "projectDevCompleted", // 项目开发完成
PROJECT_OVERDUE_DEV_UNCOMPLETED: "projectOverdueDevUncompleted", // 项目逾期开发未完成
PROJECT_OVERDUE_DEV_COMPLETED: "projectOverdueDevCompleted", // 项目逾期开发完成
PROJECT_DEV_OVERDUE: "projectDevOverdue", // 项目开发逾期
PROJECT_DEV_SOON_EXPIRE: "projectDevSoonExpire", // 开发即将到期项目
// 任务
TASK_STATUS: "taskStatus", // 状态数据
TASK_TYPE: "taskType", // 类型分组数据
TASK_OVERDUE_UNCOMPLETED: "taskOverdueUncompleted", // 逾期未完成
TASK_OVERDUE_COMPLETED: "taskOverdueCompleted", // 逾期完成
TASK_TODAY_COMPLETED: "taskTodayCompleted", // 今日完成任务
TASK_SOON_EXPIRE: "taskSoonExpire", // 即将到期任务
// 客户
CUSTOMER_STATUS: "customerStatus", // 状态分组数据
CUSTOMER_TODAY_CREATE: "customerTodayCreate", // 今日新增客户
CUSTOMER_TODAY_FOLLOWED: "customerTodayFollowed", // 今日已跟进客户
CUSTOMER_TODAY_WAIT_FOLLOW: "customerTodayWaitFollow", // 今日待跟进客户
CUSTOMER_SOON_FOLLOW: "customerSoonFollow", // 即将需要跟进的客户
}

View File

@ -222,6 +222,17 @@ export default {
mixins: [$showColumns], mixins: [$showColumns],
dicts: ['customer_intent_level', 'customer_status'], dicts: ['customer_intent_level', 'customer_status'],
components: {FormCol, CustomerLink, CustomerFollowEditDialog}, components: {FormCol, CustomerLink, CustomerFollowEditDialog},
props: {
query: {
type: Object,
default: () => ({})
},
//
initShowSearch: {
type: Boolean,
default: true
}
},
data() { data() {
return { return {
showFollowDialog: false, showFollowDialog: false,
@ -229,7 +240,7 @@ export default {
span: 24, span: 24,
// //
columns: [ columns: [
{key: 'id', visible: true, label: '编号', minWidth: null, sortable: true, overflow: false, align: 'center', width: "80"}, {key: 'id', visible: false, label: '编号', minWidth: null, sortable: true, overflow: false, align: 'center', width: "80"},
{key: 'name', visible: true, label: '名称', minWidth: null, sortable: true, overflow: false, align: 'center', width: null}, {key: 'name', visible: true, label: '名称', minWidth: null, sortable: true, overflow: false, align: 'center', width: null},
{key: 'status', visible: true, label: '状态', minWidth: null, sortable: true, overflow: false, align: 'center', width: null}, {key: 'status', visible: true, label: '状态', minWidth: null, sortable: true, overflow: false, align: 'center', width: null},
{key: 'intentLevel', visible: true, label: '意向强度', minWidth: null, sortable: true, overflow: false, align: 'center', width: null}, {key: 'intentLevel', visible: true, label: '意向强度', minWidth: null, sortable: true, overflow: false, align: 'center', width: null},
@ -289,6 +300,14 @@ export default {
}; };
}, },
created() { created() {
this.initColumns();
this.showSearch = this.initShowSearch;
this.queryParams = {
...this.queryParams,
...this.query
}
this.getList(); this.getList();
}, },
methods: { methods: {

View File

@ -99,9 +99,7 @@ export default {
detail: {}, detail: {},
queryParams: { queryParams: {
pageNum: 1, pageNum: 1,
pageSize: 5, pageSize: 2,
orderByColumn: 'top',
isAsc: 'desc'
}, },
} }
}, },

View File

@ -1,11 +1,32 @@
<template> <template>
<div class="app-container"> <div class="app-container">
<el-row :gutter="12"> <el-row :gutter="12">
<el-col :xs="24" :lg="5"> <el-col :xs="24" :lg="19" class="mb8">
<el-row :gutter="12" class="mb8">
<el-col :xs="24" :lg="6">
<notice-panel/>
</el-col>
<el-col :xs="24" :lg="18">
<panel-group /> <panel-group />
</el-col> </el-col>
<el-col :xs="24" :lg="14"> </el-row>
<project-list-panel/> <el-card>
<el-tabs >
<el-tab-pane label="项目列表" lazy v-if="checkPermi(['bst:project:list'])">
<project :hide-columns="projectHideColumns" :init-show-search="false"/>
</el-tab-pane>
<el-tab-pane label="任务列表" lazy v-if="checkPermi(['bst:task:list'])">
<task :hide-columns="taskHideColumns" :init-show-search="false"/>
</el-tab-pane>
<el-tab-pane label="今日完成任务" lazy v-if="checkPermi(['bst:task:list'])">
<task :hide-columns="taskHideColumns" :query="{passDateRange: [today, today]}" :init-show-search="false"/>
</el-tab-pane>
<el-tab-pane label="今日新增客户" lazy v-if="checkPermi(['bst:customer:list'])">
<customer :hide-columns="customerHideColumns" :query="{createDate: today}" :init-show-search="false"/>
</el-tab-pane>
</el-tabs>
</el-card>
<!-- <project-list-panel/> -->
</el-col> </el-col>
<el-col :xs="24" :lg="5"> <el-col :xs="24" :lg="5">
<el-card class="card-box"> <el-card class="card-box">
@ -14,7 +35,6 @@
<el-card class="card-box"> <el-card class="card-box">
<month-project-chart height="180px" bar-width="50%"/> <month-project-chart height="180px" bar-width="50%"/>
</el-card> </el-card>
<notice-panel/>
</el-col> </el-col>
</el-row> </el-row>
</div> </div>
@ -26,13 +46,60 @@ import NoticePanel from '@/views/bst/index/components/NoticePanel.vue';
import MonthProjectChart from '@/views/dashboard/MonthProjectChart.vue'; import MonthProjectChart from '@/views/dashboard/MonthProjectChart.vue';
import ProjectRateChart from '@/views/dashboard/ProjectRateChart.vue'; import ProjectRateChart from '@/views/dashboard/ProjectRateChart.vue';
import ProjectListPanel from './components/ProjectListPanel.vue'; import ProjectListPanel from './components/ProjectListPanel.vue';
import Project from '@/views/bst/project/index.vue';
import Task from '@/views/bst/task/index.vue';
import Customer from '@/views/bst/customer/index.vue';
import { checkPermi } from '@/utils/permission';
import { parseTime } from '@/utils/ruoyi';
export default { export default {
name: 'Index', name: 'Index',
components: { PanelGroup, NoticePanel, MonthProjectChart, ProjectRateChart, ProjectListPanel }, components: { PanelGroup, NoticePanel, MonthProjectChart, ProjectRateChart, ProjectListPanel, Project, Task, Customer },
data() { data() {
return { return {
today: parseTime(new Date(), '{y}-{m}-{d}'),
projectHideColumns: [
'id',
'no',
'status',
'customerName',
'expireTime',
'expectedCompleteDate',
'maintenanceEndDate',
'ownerName',
'followName',
'amount',
'receivedAmount',
'operationAmount',
'remark',
'createTime'
],
taskHideColumns: [
'id',
'no',
'status',
'level',
'type',
'createId',
'createName',
'createTime',
'remark',
'submitCount',
'passCount',
],
customerHideColumns: [
'id',
'code',
'source',
'followName',
'remark',
'createName',
'createTime'
],
} }
}, },
methods: {
checkPermi,
}
} }
</script> </script>

View File

@ -153,7 +153,9 @@ export default {
// //
let message = JSON.parse(event.data); let message = JSON.parse(event.data);
this.showDesktopNotification(message); this.showDesktopNotification(message);
if (message.id != null) {
this.unreadCount ++; this.unreadCount ++;
}
}; };
this.ws.onclose = (event) => { this.ws.onclose = (event) => {

View File

@ -117,7 +117,7 @@
/ /
<el-link class="task-count" type="success" :underline="false">{{ d.row.taskPassCount | dv }}</el-link> <el-link class="task-count" type="success" :underline="false">{{ d.row.taskPassCount | dv }}</el-link>
/ /
<el-link class="task-count" type="warning" :underline="false">{{ d.row.taskWaitConfirmCount | dv }}</el-link> <el-link class="task-count" type="warning" :underline="false">{{ d.row.taskProcessCount | dv }}</el-link>
</template> </template>
<template v-else-if="['no','name'].includes(column.key)"> <template v-else-if="['no','name'].includes(column.key)">
<div class="flex-row" style="justify-content: left;"> <div class="flex-row" style="justify-content: left;">
@ -232,7 +232,12 @@
<!-- 新增任务弹窗 --> <!-- 新增任务弹窗 -->
<task-edit-dialog <task-edit-dialog
:show.sync="showTaskDialog" :show.sync="showTaskDialog"
:init-data="{projectId: row.id}" :init-data="{
projectId: row.id,
projectOwnerId: row.ownerId,
projectFollowId: row.followId,
projectMemberIds: row.memberIds
}"
@success="getList" @success="getList"
/> />
</div> </div>
@ -280,6 +285,10 @@ export default {
initData: { initData: {
type: Object, type: Object,
default: () => {} default: () => {}
},
initShowSearch: {
type: Boolean,
default: true
} }
}, },
data() { data() {
@ -309,7 +318,7 @@ export default {
{key: 'remark', visible: true, label: '备注', minWidth: null, sortable: true, overflow: true, align: 'center', width: null}, {key: 'remark', visible: true, label: '备注', minWidth: null, sortable: true, overflow: true, align: 'center', width: null},
{key: 'time', visible: true, label: '截止时间', minWidth: null, sortable: false, overflow: false, align: 'center', width: "100"}, {key: 'time', visible: true, label: '截止时间', minWidth: null, sortable: false, overflow: false, align: 'center', width: "100"},
{key: 'createTime', visible: false, label: '创建时间', minWidth: null, sortable: true, overflow: false, align: 'center', width: "100"}, {key: 'createTime', visible: false, label: '创建时间', minWidth: null, sortable: true, overflow: false, align: 'center', width: "100"},
{key: 'taskCount', visible: true, label: '任务(总数/已通过/待审核)', minWidth: null, sortable: true, overflow: false, align: 'center', width: "200"}, {key: 'taskCount', visible: true, label: '任务(总数/已通过/进行中)', minWidth: null, sortable: false, overflow: false, align: 'center', width: "200"},
], ],
// //
orderSorts: ['ascending', 'descending', null], orderSorts: ['ascending', 'descending', null],
@ -350,6 +359,7 @@ export default {
}, },
created() { created() {
this.initColumns(); this.initColumns();
this.showSearch = this.initShowSearch;
this.queryParams = { this.queryParams = {
...this.queryParams, ...this.queryParams,

View File

@ -39,6 +39,6 @@ export const ProjectUtils = {
const expireTime = new Date(value); const expireTime = new Date(value);
const now = new Date(); const now = new Date();
const days = (expireTime - now) / 24 / 60 / 60 / 1000; const days = (expireTime - now) / 24 / 60 / 60 / 1000;
return days <= 7 && days > 0; return days <= 2 && days > 0;
} }
} }

View File

@ -0,0 +1,43 @@
<template>
<el-alert title="任务统计" type="info" v-loading="loading">
今日完成{{ brief.task.todayCompleted | dv }}
</el-alert>
</template>
<script>
import { getBrief } from '@/api/dashboard/dashboard'
import { BriefKeys } from '@/utils/enums'
export default {
name: 'TaskStatistics',
data() {
return {
loading: false,
brief: {
task: {}
}
}
},
created() {
this.getBrief()
},
methods: {
getBrief() {
this.loading = true
getBrief({ keys: [
BriefKeys.TASK_TODAY_COMPLETED,
]}).then(res => {
if (res.code === 200 && res.data) {
this.brief = res.data
}
}).finally(() => {
this.loading = false
})
}
}
}
</script>
<style lang="scss" scoped>
</style>

View File

@ -41,6 +41,17 @@
<el-radio-button :label="false">正常</el-radio-button> <el-radio-button :label="false">正常</el-radio-button>
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
<el-form-item label="完成日期" prop="passDateRange" v-if="isShow('expireTime')">
<el-date-picker
v-model="queryParams.passDateRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="yyyy-MM-dd"
@change="handleQuery"
/>
</el-form-item>
<el-form-item label="创建人" prop="createId" v-if="isShow('createName')"> <el-form-item label="创建人" prop="createId" v-if="isShow('createName')">
<user-select v-model="queryParams.createId" @change="handleQuery"/> <user-select v-model="queryParams.createId" @change="handleQuery"/>
</el-form-item> </el-form-item>
@ -88,6 +99,10 @@
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList" :columns="columns"></right-toolbar> <right-toolbar :showSearch.sync="showSearch" @queryTable="getList" :columns="columns"></right-toolbar>
</el-row> </el-row>
<el-row class="mb8">
<task-statistics />
</el-row>
<el-table v-loading="loading" :data="taskList" @selection-change="handleSelectionChange" :default-sort="defaultSort" @sort-change="onSortChange"> <el-table v-loading="loading" :data="taskList" @selection-change="handleSelectionChange" :default-sort="defaultSort" @sort-change="onSortChange">
<el-table-column type="selection" width="55" align="center" /> <el-table-column type="selection" width="55" align="center" />
<template v-for="column of showColumns"> <template v-for="column of showColumns">
@ -119,7 +134,7 @@
<template v-else-if="column.key === 'projectName'"> <template v-else-if="column.key === 'projectName'">
<project-link :id="d.row.projectId" :text="d.row.projectName"/> <project-link :id="d.row.projectId" :text="d.row.projectName"/>
</template> </template>
<template v-else-if="column.key === 'createTime'"> <template v-else-if="column.key === 'expireTime'">
创建:{{d.row.createTime | dv}}<br/> 创建:{{d.row.createTime | dv}}<br/>
截止:{{d.row.expireTime | dv}}<br/> 截止:{{d.row.expireTime | dv}}<br/>
完成:{{d.row.passTime | dv}}<br/> 完成:{{d.row.passTime | dv}}<br/>
@ -135,7 +150,7 @@
</template> </template>
</el-table-column> </el-table-column>
</template> </template>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="180" fixed="right"> <el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="120" fixed="right">
<template slot-scope="scope"> <template slot-scope="scope">
<el-button <el-button
size="mini" size="mini"
@ -200,6 +215,7 @@ import AvatarList from '@/components/AvatarList/index.vue';
import UserSelect from '@/components/Business/User/UserSelect.vue'; import UserSelect from '@/components/Business/User/UserSelect.vue';
import {TaskStatus} from '@/utils/enums' import {TaskStatus} from '@/utils/enums'
import BooleanTag from '@/components/BooleanTag' import BooleanTag from '@/components/BooleanTag'
import TaskStatistics from '@/views/bst/task/components/TaskStatistics.vue'
// //
const defaultSort = { const defaultSort = {
prop: "createTime", prop: "createTime",
@ -210,7 +226,7 @@ export default {
name: "Task", name: "Task",
mixins: [$showColumns, $task], mixins: [$showColumns, $task],
dicts: ['task_status', 'task_level', 'task_type'], dicts: ['task_status', 'task_level', 'task_type'],
components: {FormCol, TaskEditDialog, ProjectSelect, TaskViewDialog, ProjectLink, AvatarList, UserSelect, BooleanTag}, components: {FormCol, TaskEditDialog, ProjectSelect, TaskViewDialog, ProjectLink, AvatarList, UserSelect, BooleanTag, TaskStatistics},
props: { props: {
initData: { initData: {
type: Object, type: Object,
@ -219,6 +235,10 @@ export default {
query: { query: {
type: Object, type: Object,
default: () => ({}) default: () => ({})
},
initShowSearch: {
type: Boolean,
default: true
} }
}, },
data() { data() {
@ -235,7 +255,7 @@ export default {
{key: 'createName', visible: true, label: '创建人', minWidth: null, sortable: true, overflow: false, align: 'center', width: null}, {key: 'createName', visible: true, label: '创建人', minWidth: null, sortable: true, overflow: false, align: 'center', width: null},
{key: 'submitCount', visible: true, label: '提交', minWidth: null, sortable: false, overflow: false, align: 'center', width: "50"}, {key: 'submitCount', visible: true, label: '提交', minWidth: null, sortable: false, overflow: false, align: 'center', width: "50"},
{key: 'passCount', visible: true, label: '完成', minWidth: null, sortable: false, overflow: false, align: 'center', width: "50"}, {key: 'passCount', visible: true, label: '完成', minWidth: null, sortable: false, overflow: false, align: 'center', width: "50"},
{key: 'createTime', visible: true, label: '时间', minWidth: null, sortable: true, overflow: false, align: 'left', width: "190"}, {key: 'expireTime', visible: true, label: '时间', minWidth: null, sortable: true, overflow: false, align: 'left', width: "190"},
], ],
// //
orderSorts: ['ascending', 'descending', null], orderSorts: ['ascending', 'descending', null],
@ -264,6 +284,7 @@ export default {
pageSize: 20, pageSize: 20,
orderByColumn: defaultSort.prop, orderByColumn: defaultSort.prop,
isAsc: defaultSort.order, isAsc: defaultSort.order,
passDateRange: [],
id: null, id: null,
projectId: null, projectId: null,
name: null, name: null,
@ -281,7 +302,7 @@ export default {
}, },
created() { created() {
this.initColumns(); this.initColumns();
this.showSearch = this.initShowSearch;
this.queryParams = { this.queryParams = {
...this.queryParams, ...this.queryParams,
...this.query ...this.query

View File

@ -1,269 +1,67 @@
<template> <template>
<div class="dashboard-panel" v-loading="loading"> <div class="dashboard-panel" v-loading="loading">
<!-- 项目统计 --> <el-row :gutter="12">
<el-card class="card-box" header="参与项目">
<el-row :gutter="10">
<el-col :span="8"> <el-col :span="8">
<div class="stat-card project" :class="{'hover': hoveredCard === 'project-total'}"> <stat-card
<div class="card-header"> title="任务"
<div class="card-icon"> icon="el-icon-s-order"
<i class="el-icon-folder"></i> icon-color="#409EFF"
</div> :main-value="data.task.todayCompleted"
<div class="card-label">合计项目</div> main-label="今日完成任务"
</div> :items="[
<div class="card-value"> { label: '待完成任务', value: data.task.waitCompleted },
<count-to :start-val="0" :end-val="data.project.total || 0" :duration="2000" class="num"/> { label: '即将到期', value: data.task.soonExpire },
</div> { label: '逾期任务', value: data.task.overdueUncompleted }
</div> ]"
/>
</el-col> </el-col>
<el-col :span="8"> <el-col :span="8">
<div class="stat-card project" :class="{'hover': hoveredCard === 'project-progress'}"> <stat-card
<div class="card-header"> title="项目"
<div class="card-icon"> icon="el-icon-s-cooperation"
<i class="el-icon-refresh"></i> icon-color="#67C23A"
</div> :main-value="data.project.inProgress"
<div class="card-label">进行中</div> main-label="进行中项目"
</div> :items="[
<div class="card-value"> { label: '运维中', value: data.project.maintenance },
<count-to :start-val="0" :end-val="data.project.inProgress || 0" :duration="2000" class="num"/> { label: '即将到期', value: data.project.devSoonExpire },
</div> { label: '开发超期', value: data.project.devOverdue }
</div> ]"
/>
</el-col> </el-col>
<el-col :span="8"> <el-col :span="8">
<div class="stat-card project" :class="{'hover': hoveredCard === 'project-completed'}"> <stat-card
<div class="card-header"> title="客户"
<div class="card-icon"> icon="el-icon-user"
<i class="el-icon-check"></i> icon-color="#E6A23C"
</div> :main-value="data.customer.today"
<div class="card-label">已完成</div> main-label="今日新增客户"
</div> :items="[
<div class="card-value"> { label: '今日已跟进', value: data.customer.todayFollowed },
<count-to :start-val="0" :end-val="data.project.completed || 0" :duration="2000" class="num"/> { label: '今日待跟进', value: data.customer.todayWaitFollow },
</div> { label: '即将跟进', value: data.customer.soonFollow }
</div> ]"
</el-col> />
<el-col :span="8">
<div class="stat-card project" :class="{'hover': hoveredCard === 'project-overdue'}">
<div class="card-header">
<div class="card-icon">
<i class="el-icon-refresh"></i>
</div>
<div class="card-label">维护中</div>
</div>
<div class="card-value">
<count-to :start-val="0" :end-val="data.project.maintenance || 0" :duration="2000" class="num"/>
</div>
</div>
</el-col>
<el-col :span="8">
<div class="stat-card project warning" :class="{'hover': hoveredCard === 'project-overdue'}">
<div class="card-header">
<div class="card-icon">
<i class="el-icon-warning"></i>
</div>
<div class="card-label">开发超期</div>
</div>
<div class="card-value">
<count-to :start-val="0" :end-val="data.project.developmentOverdue || 0" :duration="2000" class="num"/>
</div>
</div>
</el-col>
<el-col :span="8">
<div class="stat-card project warning" :class="{'hover': hoveredCard === 'project-overdue'}">
<div class="card-header">
<div class="card-icon">
<i class="el-icon-warning"></i>
</div>
<div class="card-label">维护到期</div>
</div>
<div class="card-value">
<count-to :start-val="0" :end-val="data.project.maintenanceOverdue || 0" :duration="2000" class="num"/>
</div>
</div>
</el-col> </el-col>
</el-row> </el-row>
</el-card>
<!-- 任务统计 -->
<el-card class="card-box" header="负责任务">
<el-row :gutter="10">
<el-col :span="8">
<div class="stat-card task" :class="{'hover': hoveredCard === 'task-total'}">
<div class="card-header">
<div class="card-icon">
<i class="el-icon-tickets"></i>
</div>
<div class="card-label">总任务</div>
</div>
<div class="card-value">
<count-to :start-val="0" :end-val="data.task.total || 0" :duration="2000" class="num"/>
</div>
</div>
</el-col>
<el-col :span="8">
<div class="stat-card task" :class="{'hover': hoveredCard === 'task-wait'}">
<div class="card-header">
<div class="card-icon">
<i class="el-icon-timer"></i>
</div>
<div class="card-label">待完成</div>
</div>
<div class="card-value">
<count-to :start-val="0" :end-val="data.task.waitCompleted || 0" :duration="2000" class="num"/>
</div>
</div>
</el-col>
<el-col :span="8">
<div class="stat-card task" :class="{'hover': hoveredCard === 'task-progress'}">
<div class="card-header">
<div class="card-icon">
<i class="el-icon-refresh"></i>
</div>
<div class="card-label">进行中</div>
</div>
<div class="card-value">
<count-to :start-val="0" :end-val="data.task.inProgress || 0" :duration="2000" class="num"/>
</div>
</div>
</el-col>
<el-col :span="8">
<div class="stat-card task" :class="{'hover': hoveredCard === 'task-confirm'}">
<div class="card-header">
<div class="card-icon">
<i class="el-icon-document-checked"></i>
</div>
<div class="card-label">待审核</div>
</div>
<div class="card-value">
<count-to :start-val="0" :end-val="data.task.waitConfirm || 0" :duration="2000" class="num"/>
</div>
</div>
</el-col>
<el-col :span="8">
<div class="stat-card task warning" :class="{'hover': hoveredCard === 'task-confirm'}">
<div class="card-header">
<div class="card-icon">
<i class="el-icon-warning"></i>
</div>
<div class="card-label">已驳回</div>
</div>
<div class="card-value">
<count-to :start-val="0" :end-val="data.task.reject || 0" :duration="2000" class="num"/>
</div>
</div>
</el-col>
<el-col :span="8">
<div class="stat-card task success" :class="{'hover': hoveredCard === 'task-completed'}">
<div class="card-header">
<div class="card-icon">
<i class="el-icon-circle-check"></i>
</div>
<div class="card-label">已完成</div>
</div>
<div class="card-value">
<count-to :start-val="0" :end-val="data.task.completed || 0" :duration="2000" class="num"/>
</div>
</div>
</el-col>
</el-row>
</el-card>
<!-- 客户统计 -->
<el-card class="card-box" header="我的客户">
<el-row :gutter="10">
<el-col :span="8">
<div class="stat-card customer" :class="{'hover': hoveredCard === 'customer-total'}">
<div class="card-header">
<div class="card-icon">
<i class="el-icon-user"></i>
</div>
<div class="card-label">总客户</div>
</div>
<div class="card-value">
<count-to :start-val="0" :end-val="data.customer.total || 0" :duration="2000" class="num"/>
</div>
</div>
</el-col>
<el-col :span="8">
<div class="stat-card customer" :class="{'hover': hoveredCard === 'customer-today'}">
<div class="card-header">
<div class="card-icon">
<i class="el-icon-date"></i>
</div>
<div class="card-label">今日新增</div>
</div>
<div class="card-value">
<count-to :start-val="0" :end-val="data.customer.today || 0" :duration="2000" class="num"/>
</div>
</div>
</el-col>
<el-col :span="8">
<div class="stat-card customer" :class="{'hover': hoveredCard === 'customer-potential'}">
<div class="card-header">
<div class="card-icon">
<i class="el-icon-user-solid"></i>
</div>
<div class="card-label">潜在客户</div>
</div>
<div class="card-value">
<count-to :start-val="0" :end-val="data.customer.potential || 0" :duration="2000" class="num"/>
</div>
</div>
</el-col>
<el-col :span="8">
<div class="stat-card customer" :class="{'hover': hoveredCard === 'customer-intention'}">
<div class="card-header">
<div class="card-icon">
<i class="el-icon-star-on"></i>
</div>
<div class="card-label">意向客户</div>
</div>
<div class="card-value">
<count-to :start-val="0" :end-val="data.customer.intention || 0" :duration="2000" class="num"/>
</div>
</div>
</el-col>
<el-col :span="8">
<div class="stat-card customer success" :class="{'hover': hoveredCard === 'customer-transaction'}">
<div class="card-header">
<div class="card-icon">
<i class="el-icon-shopping-cart-full"></i>
</div>
<div class="card-label">成交客户</div>
</div>
<div class="card-value">
<count-to :start-val="0" :end-val="data.customer.transaction || 0" :duration="2000" class="num"/>
</div>
</div>
</el-col>
<el-col :span="8">
<div class="stat-card customer warning" :class="{'hover': hoveredCard === 'customer-invalid'}">
<div class="card-header">
<div class="card-icon">
<i class="el-icon-close"></i>
</div>
<div class="card-label">失效客户</div>
</div>
<div class="card-value">
<count-to :start-val="0" :end-val="data.customer.invalid || 0" :duration="2000" class="num"/>
</div>
</div>
</el-col>
</el-row>
</el-card>
</div> </div>
</template> </template>
<script> <script>
import CountTo from 'vue-count-to' import CountTo from 'vue-count-to'
import {getMineBrief} from "@/api/dashboard/dashboard"; import StatCard from '@/components/Dashboard/StatCard'
import { getBrief } from "@/api/dashboard/dashboard"
import { BriefKeys } from '@/utils/enums'
export default { export default {
name: 'PanelGroup',
components: {
CountTo,
StatCard
},
data() { data() {
return { return {
loading: false, loading: false,
hoveredCard: '',
data: { data: {
project: {}, project: {},
task: {}, task: {},
@ -271,177 +69,33 @@ export default {
} }
} }
}, },
components: {
CountTo
},
created() { created() {
this.getData() this.getData()
}, },
methods: { methods: {
getData() { getData() {
this.loading = true; this.loading = true
getMineBrief().then(res => { getBrief({
this.data = res.data; keys: [
BriefKeys.TASK_STATUS,
BriefKeys.TASK_TODAY_COMPLETED,
BriefKeys.TASK_SOON_EXPIRE,
BriefKeys.TASK_OVERDUE_UNCOMPLETED,
BriefKeys.PROJECT_STATUS,
BriefKeys.PROJECT_DEV_SOON_EXPIRE,
BriefKeys.PROJECT_DEV_OVERDUE,
BriefKeys.CUSTOMER_TODAY_CREATE,
BriefKeys.CUSTOMER_TODAY_FOLLOWED,
BriefKeys.CUSTOMER_TODAY_WAIT_FOLLOW,
BriefKeys.CUSTOMER_SOON_FOLLOW,
]
}).then(res => {
this.data = res.data
}).finally(() => { }).finally(() => {
this.loading = false; this.loading = false
}) })
} }
} }
} }
</script> </script>
<style lang="scss" scoped>
.dashboard-panel {
.stat-card {
background: #fff;
border-radius: 4px;
padding: 10px;
text-align: left;
box-shadow: 0 1px 4px rgba(0,0,0,.1);
transition: all .3s;
margin-bottom: 10px;
cursor: pointer;
position: relative;
overflow: hidden;
display: flex;
flex-direction: column;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 0;
opacity: 1;
transition: opacity .3s;
}
&:hover::before {
opacity: 1.2;
}
.card-header {
display: flex;
align-items: center;
margin-bottom: 6px;
position: relative;
z-index: 1;
.card-icon {
margin-right: 6px;
i {
font-size: 16px;
}
}
.card-label {
font-size: 13px;
color: #909399;
flex: 1;
}
}
.card-value {
position: relative;
z-index: 1;
.num {
font-size: 22px;
font-weight: bold;
color: #1f2f3d;
}
}
&.project {
&::before {
background: linear-gradient(135deg, rgba(128, 100, 255, 0.08) 0%, rgba(255, 255, 255, 0) 45%, rgba(128, 100, 255, 0.1) 100%);
}
.card-icon {
color: #8064ff;
}
}
&.task {
&::before {
background: linear-gradient(135deg, rgba(255, 156, 110, 0.08) 0%, rgba(255, 255, 255, 0) 45%, rgba(255, 156, 110, 0.1) 100%);
}
.card-icon {
color: #ff9c6e;
}
}
&.customer {
&::before {
background: linear-gradient(135deg, rgba(32, 201, 151, 0.08) 0%, rgba(255, 255, 255, 0) 45%, rgba(32, 201, 151, 0.1) 100%);
}
.card-icon {
color: #20c997;
}
}
&.warning {
&::before {
background: linear-gradient(135deg, rgba(255, 77, 79, 0.08) 0%, rgba(255, 255, 255, 0) 45%, rgba(255, 77, 79, 0.1) 100%);
}
.card-icon {
color: #ff4d4f;
}
}
&.success {
&::before {
background: linear-gradient(135deg, rgba(82, 196, 26, 0.08) 0%, rgba(255, 255, 255, 0) 45%, rgba(82, 196, 26, 0.1) 100%);
}
.card-icon {
color: #52c41a;
}
}
&:hover, &.hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,.15);
}
}
}
@media screen and (max-width: 768px) {
.dashboard-panel {
padding: 10px;
.stat-section {
margin-bottom: 15px;
.section-title {
font-size: 14px;
margin-bottom: 10px;
}
.stat-card {
padding: 8px;
.card-header {
margin-bottom: 4px;
.card-icon {
i {
font-size: 14px;
}
}
.card-label {
font-size: 12px;
}
}
.card-value {
.num {
font-size: 18px;
}
}
}
}
}
}
</style>

View File

@ -87,6 +87,7 @@
<script> <script>
import StatisticsCard from '@/components/StatisticsCard' import StatisticsCard from '@/components/StatisticsCard'
import { getBrief } from '@/api/dashboard/dashboard' import { getBrief } from '@/api/dashboard/dashboard'
import { BriefKeys } from '@/utils/enums'
export default { export default {
name: 'UserStatistics', name: 'UserStatistics',
@ -112,7 +113,16 @@ export default {
methods: { methods: {
getBrief() { getBrief() {
this.loading = true this.loading = true
getBrief({ joinUserId: this.userId }).then(res => { getBrief({ joinUserId: this.userId, keys: [
BriefKeys.PROJECT_STATUS,
BriefKeys.PROJECT_DEV_COMPLETED,
BriefKeys.PROJECT_OVERDUE_DEV_UNCOMPLETED,
BriefKeys.PROJECT_OVERDUE_DEV_COMPLETED,
BriefKeys.TASK_STATUS,
BriefKeys.TASK_TYPE,
BriefKeys.TASK_OVERDUE_UNCOMPLETED,
BriefKeys.TASK_OVERDUE_COMPLETED,
]}).then(res => {
if (res.code === 200 && res.data) { if (res.code === 200 && res.data) {
this.task = res.data.task this.task = res.data.task
this.project = res.data.project this.project = res.data.project