|
|
@@ -0,0 +1,807 @@
|
|
|
+<template>
|
|
|
+ <div class="dashboard-container" ref="dashboardContainer">
|
|
|
+ <!-- 顶部标题和日期选择 -->
|
|
|
+ <div class="dashboard-header">
|
|
|
+ <h2 class="dashboard-title">工时看板</h2>
|
|
|
+ <el-date-picker
|
|
|
+ v-model="selectedMonth"
|
|
|
+ type="month"
|
|
|
+ placeholder="选择年月"
|
|
|
+ value-format="yyyy-MM"
|
|
|
+ :clearable="false"
|
|
|
+ @change="onMonthChange"
|
|
|
+ size="small"
|
|
|
+ style="width: 160px;"
|
|
|
+ ></el-date-picker>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 四个图表区域 -->
|
|
|
+ <div class="charts-grid">
|
|
|
+ <!-- 图表1:项目工时排名前十 -->
|
|
|
+ <div class="chart-card">
|
|
|
+ <div class="chart-title">
|
|
|
+ <span>项目工时排名前十</span>
|
|
|
+ <el-tooltip content="保存为图片" placement="top">
|
|
|
+ <el-button
|
|
|
+ class="save-img-btn"
|
|
|
+ type="text"
|
|
|
+ icon="el-icon-download"
|
|
|
+ :disabled="loading1 || top10Data.length === 0"
|
|
|
+ @click="saveChartImage(chart1, '项目工时排名前十')"
|
|
|
+ ></el-button>
|
|
|
+ </el-tooltip>
|
|
|
+ </div>
|
|
|
+ <div v-if="loading1" class="chart-loading">
|
|
|
+ <i class="el-icon-loading"></i> 加载中...
|
|
|
+ </div>
|
|
|
+ <div v-else-if="top10Data.length === 0" class="chart-empty">暂无数据</div>
|
|
|
+ <div v-show="!loading1 && top10Data.length > 0" ref="chart1" class="chart-body"></div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 图表2:工时前三项目部门分配 -->
|
|
|
+ <div class="chart-card">
|
|
|
+ <div class="chart-title">
|
|
|
+ <span>工时前三项目部门分配表</span>
|
|
|
+ <div class="chart-title-actions">
|
|
|
+ <el-radio-group
|
|
|
+ v-model="top3PieMode"
|
|
|
+ size="mini"
|
|
|
+ :disabled="loading2 || top3DeptData.length === 0"
|
|
|
+ @change="renderChart2"
|
|
|
+ >
|
|
|
+ <el-radio-button label="working">工时</el-radio-button>
|
|
|
+ <el-radio-button label="overtime">加班工时</el-radio-button>
|
|
|
+ </el-radio-group>
|
|
|
+ <el-tooltip content="保存为图片" placement="top">
|
|
|
+ <el-button
|
|
|
+ class="save-img-btn"
|
|
|
+ type="text"
|
|
|
+ icon="el-icon-download"
|
|
|
+ :disabled="loading2 || top3DeptData.length === 0"
|
|
|
+ @click="saveChartImage(chart2, '工时前三项目部门分配表')"
|
|
|
+ ></el-button>
|
|
|
+ </el-tooltip>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div v-if="loading2" class="chart-loading">
|
|
|
+ <i class="el-icon-loading"></i> 加载中...
|
|
|
+ </div>
|
|
|
+ <div v-else-if="top3DeptData.length === 0" class="chart-empty">暂无数据</div>
|
|
|
+ <div v-show="!loading2 && top3DeptData.length > 0" ref="chart2" class="chart-body"></div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 图表3:各部门总工时和人均工时 -->
|
|
|
+ <div class="chart-card">
|
|
|
+ <div class="chart-title">
|
|
|
+ <span>各部门总工时和人均工时表</span>
|
|
|
+ <el-tooltip content="保存为图片" placement="top">
|
|
|
+ <el-button
|
|
|
+ class="save-img-btn"
|
|
|
+ type="text"
|
|
|
+ icon="el-icon-download"
|
|
|
+ :disabled="loading3 || deptHoursData.length === 0"
|
|
|
+ @click="saveChartImage(chart3, '各部门总工时和人均工时表')"
|
|
|
+ ></el-button>
|
|
|
+ </el-tooltip>
|
|
|
+ </div>
|
|
|
+ <div v-if="loading3" class="chart-loading">
|
|
|
+ <i class="el-icon-loading"></i> 加载中...
|
|
|
+ </div>
|
|
|
+ <div v-else-if="deptHoursData.length === 0" class="chart-empty">暂无数据</div>
|
|
|
+ <div v-show="!loading3 && deptHoursData.length > 0" ref="chart3" class="chart-body"></div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 图表4:各部门参与项目数量 -->
|
|
|
+ <div class="chart-card">
|
|
|
+ <div class="chart-title">
|
|
|
+ <span>各部门参与项目数量表</span>
|
|
|
+ <el-tooltip content="保存为图片" placement="top">
|
|
|
+ <el-button
|
|
|
+ class="save-img-btn"
|
|
|
+ type="text"
|
|
|
+ icon="el-icon-download"
|
|
|
+ :disabled="loading4 || deptProjectCountData.length === 0"
|
|
|
+ @click="saveChartImage(chart4, '各部门参与项目数量表')"
|
|
|
+ ></el-button>
|
|
|
+ </el-tooltip>
|
|
|
+ </div>
|
|
|
+ <div v-if="loading4" class="chart-loading">
|
|
|
+ <i class="el-icon-loading"></i> 加载中...
|
|
|
+ </div>
|
|
|
+ <div v-else-if="deptProjectCountData.length === 0" class="chart-empty">暂无数据</div>
|
|
|
+ <div v-show="!loading4 && deptProjectCountData.length > 0" ref="chart4" class="chart-body"></div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+import * as echarts from 'echarts'
|
|
|
+
|
|
|
+export default {
|
|
|
+ name: 'Dashboard',
|
|
|
+ data() {
|
|
|
+ const now = new Date()
|
|
|
+ const year = now.getFullYear()
|
|
|
+ const month = String(now.getMonth() + 1).padStart(2, '0')
|
|
|
+ return {
|
|
|
+ user: JSON.parse(sessionStorage.getItem('user')),
|
|
|
+ selectedMonth: `${year}-${month}`,
|
|
|
+ loading1: false,
|
|
|
+ loading2: false,
|
|
|
+ loading3: false,
|
|
|
+ loading4: false,
|
|
|
+ top10Data: [],
|
|
|
+ top3DeptData: [],
|
|
|
+ deptHoursData: [],
|
|
|
+ deptProjectCountData: [],
|
|
|
+ chart1: null,
|
|
|
+ chart2: null,
|
|
|
+ chart3: null,
|
|
|
+ chart4: null,
|
|
|
+ top3PieMode: 'working',
|
|
|
+ }
|
|
|
+ },
|
|
|
+ mounted() {
|
|
|
+ this.loadAllCharts()
|
|
|
+ window.addEventListener('resize', this.handleResize)
|
|
|
+ },
|
|
|
+ beforeDestroy() {
|
|
|
+ window.removeEventListener('resize', this.handleResize)
|
|
|
+ if (this.chart1) { this.chart1.dispose(); this.chart1 = null }
|
|
|
+ if (this.chart2) { this.chart2.dispose(); this.chart2 = null }
|
|
|
+ if (this.chart3) { this.chart3.dispose(); this.chart3 = null }
|
|
|
+ if (this.chart4) { this.chart4.dispose(); this.chart4 = null }
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ onMonthChange() {
|
|
|
+ this.loadAllCharts()
|
|
|
+ },
|
|
|
+
|
|
|
+ loadAllCharts() {
|
|
|
+ this.loadTop10Project()
|
|
|
+ this.loadTop3ProjectDept()
|
|
|
+ this.loadDeptHours()
|
|
|
+ this.loadDeptProjectCount()
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 保存图表为图片
|
|
|
+ * @param {Object} chartInstance - ECharts 实例
|
|
|
+ * @param {String} fileName - 下载文件名(不含扩展名)
|
|
|
+ */
|
|
|
+ saveChartImage(chartInstance, fileName) {
|
|
|
+ if (!chartInstance) {
|
|
|
+ this.$message({ message: '图表尚未加载完成', type: 'warning' })
|
|
|
+ return
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ const url = chartInstance.getDataURL({
|
|
|
+ type: 'png',
|
|
|
+ pixelRatio: 2,
|
|
|
+ backgroundColor: '#fff'
|
|
|
+ })
|
|
|
+ const link = document.createElement('a')
|
|
|
+ link.href = url
|
|
|
+ link.download = `${fileName}_${this.selectedMonth}.png`
|
|
|
+ document.body.appendChild(link)
|
|
|
+ link.click()
|
|
|
+ document.body.removeChild(link)
|
|
|
+ } catch (e) {
|
|
|
+ this.$message({ message: '图片保存失败,请稍后重试', type: 'error' })
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // 企业微信部门名称转译
|
|
|
+ // 通过 WWOpenData.prefetch 获取转译后的纯文本名称(用于坐标轴等 canvas 渲染场景)
|
|
|
+ // 同时在 data 中保留原始 openid(_deptOpenId),供 tooltip 中 ww-open-data 使用
|
|
|
+ translateDeptNames(dataList, nameKey, callback) {
|
|
|
+ if (!dataList || dataList.length === 0) {
|
|
|
+ callback(dataList)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ // 先保存原始 openid,供 tooltip 的 ww-open-data 使用
|
|
|
+ const dataWithOpenId = dataList.map(item => ({
|
|
|
+ ...item,
|
|
|
+ _deptOpenId: item[nameKey]
|
|
|
+ }))
|
|
|
+ if (typeof WWOpenData !== 'undefined') {
|
|
|
+ if (WWOpenData.initCanvas) {
|
|
|
+ WWOpenData.initCanvas()
|
|
|
+ }
|
|
|
+ const items = dataList.map(item => ({
|
|
|
+ type: 'departmentName',
|
|
|
+ id: item[nameKey]
|
|
|
+ }))
|
|
|
+ const myFun = async () => {
|
|
|
+ try {
|
|
|
+ const result = await new Promise((resolve, reject) => {
|
|
|
+ if (WWOpenData.prefetch) {
|
|
|
+ WWOpenData.prefetch({ items }, (err, data) => {
|
|
|
+ if (err) { return reject(err) }
|
|
|
+ resolve(data)
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ resolve({ items: items.map(i => ({ data: i.id })) })
|
|
|
+ }
|
|
|
+ })
|
|
|
+ const translated = dataWithOpenId.map((item, idx) => ({
|
|
|
+ ...item,
|
|
|
+ [nameKey]: (result.items[idx] && result.items[idx].data) ? result.items[idx].data : item[nameKey]
|
|
|
+ }))
|
|
|
+ callback(translated)
|
|
|
+ } catch (e) {
|
|
|
+ callback(dataWithOpenId)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ myFun()
|
|
|
+ } else {
|
|
|
+ callback(dataWithOpenId)
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // 生成部门名称的 tooltip HTML
|
|
|
+ // 企业微信环境下优先用 _deptOpenId(原始 openid)渲染 ww-open-data,
|
|
|
+ // 若无 openid 则降级显示转译后的纯文本
|
|
|
+ deptNameHtml(name, openId) {
|
|
|
+ if (this.user.userNameNeedTranslate == 1 && openId) {
|
|
|
+ return `<ww-open-data type="departmentName" openid="${openId}"></ww-open-data>`
|
|
|
+ }
|
|
|
+ return name
|
|
|
+ },
|
|
|
+
|
|
|
+ // 接口1:项目工时排名前十(不涉及部门,无需转译)
|
|
|
+ loadTop10Project() {
|
|
|
+ this.loading1 = true
|
|
|
+ this.top10Data = []
|
|
|
+ this.http.post('/report/getTop10ProjectReport', { ymonth: this.selectedMonth },
|
|
|
+ res => {
|
|
|
+ this.loading1 = false
|
|
|
+ if (res.code === 'ok') {
|
|
|
+ this.top10Data = res.data || []
|
|
|
+ this.$nextTick(() => {
|
|
|
+ this.renderChart1()
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ this.$message({ message: res.msg, type: 'error' })
|
|
|
+ }
|
|
|
+ },
|
|
|
+ err => {
|
|
|
+ this.loading1 = false
|
|
|
+ this.$message({ message: err, type: 'error' })
|
|
|
+ }
|
|
|
+ )
|
|
|
+ },
|
|
|
+
|
|
|
+ // 接口2:工时前三项目部门分配(按部门,需转译)
|
|
|
+ loadTop3ProjectDept() {
|
|
|
+ this.loading2 = true
|
|
|
+ this.top3DeptData = []
|
|
|
+ this.http.post('/report/getTop3ProjectReportGroupByDept', { ymonth: this.selectedMonth },
|
|
|
+ res => {
|
|
|
+ this.loading2 = false
|
|
|
+ if (res.code === 'ok') {
|
|
|
+ const data = res.data || []
|
|
|
+ if (this.user.userNameNeedTranslate == 1 && data.length > 0) {
|
|
|
+ this.translateDeptNames(data, 'departmentName', translated => {
|
|
|
+ this.top3DeptData = translated
|
|
|
+ this.$nextTick(() => { this.renderChart2() })
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ this.top3DeptData = data
|
|
|
+ this.$nextTick(() => { this.renderChart2() })
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ this.$message({ message: res.msg, type: 'error' })
|
|
|
+ }
|
|
|
+ },
|
|
|
+ err => {
|
|
|
+ this.loading2 = false
|
|
|
+ this.$message({ message: err, type: 'error' })
|
|
|
+ }
|
|
|
+ )
|
|
|
+ },
|
|
|
+
|
|
|
+ // 接口3:各部门总工时和人均工时(按部门,需转译)
|
|
|
+ loadDeptHours() {
|
|
|
+ this.loading3 = true
|
|
|
+ this.deptHoursData = []
|
|
|
+ this.http.post('/report/getProjectReportGroupByDept', { ymonth: this.selectedMonth },
|
|
|
+ res => {
|
|
|
+ this.loading3 = false
|
|
|
+ if (res.code === 'ok') {
|
|
|
+ const data = res.data || []
|
|
|
+ if (this.user.userNameNeedTranslate == 1 && data.length > 0) {
|
|
|
+ this.translateDeptNames(data, 'departmentName', translated => {
|
|
|
+ this.deptHoursData = translated
|
|
|
+ this.$nextTick(() => { this.renderChart3() })
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ this.deptHoursData = data
|
|
|
+ this.$nextTick(() => { this.renderChart3() })
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ this.$message({ message: res.msg, type: 'error' })
|
|
|
+ }
|
|
|
+ },
|
|
|
+ err => {
|
|
|
+ this.loading3 = false
|
|
|
+ this.$message({ message: err, type: 'error' })
|
|
|
+ }
|
|
|
+ )
|
|
|
+ },
|
|
|
+
|
|
|
+ // 接口4:各部门参与项目数量(按部门,需转译)
|
|
|
+ loadDeptProjectCount() {
|
|
|
+ this.loading4 = true
|
|
|
+ this.deptProjectCountData = []
|
|
|
+ this.http.post('/report/getDeptProjectCount', { ymonth: this.selectedMonth },
|
|
|
+ res => {
|
|
|
+ this.loading4 = false
|
|
|
+ if (res.code === 'ok') {
|
|
|
+ const data = res.data || []
|
|
|
+ if (this.user.userNameNeedTranslate == 1 && data.length > 0) {
|
|
|
+ this.translateDeptNames(data, 'departmentName', translated => {
|
|
|
+ this.deptProjectCountData = translated
|
|
|
+ this.$nextTick(() => { this.renderChart4() })
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ this.deptProjectCountData = data
|
|
|
+ this.$nextTick(() => { this.renderChart4() })
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ this.$message({ message: res.msg, type: 'error' })
|
|
|
+ }
|
|
|
+ },
|
|
|
+ err => {
|
|
|
+ this.loading4 = false
|
|
|
+ this.$message({ message: err, type: 'error' })
|
|
|
+ }
|
|
|
+ )
|
|
|
+ },
|
|
|
+
|
|
|
+ // 图表1:项目工时排名前十 - 水平条形图
|
|
|
+ renderChart1() {
|
|
|
+ if (!this.$refs.chart1) return
|
|
|
+ if (this.chart1) { this.chart1.dispose(); this.chart1 = null }
|
|
|
+ const el = this.$refs.chart1
|
|
|
+ const parentEl = el.parentElement
|
|
|
+ const width = parentEl ? parentEl.clientWidth - 32 : el.clientWidth
|
|
|
+ this.chart1 = echarts.init(el, null, { width: width, height: 300 })
|
|
|
+
|
|
|
+ const data = this.top10Data.slice().reverse()
|
|
|
+ const names = data.map(item => item.projectName || item.projectCode || '未知项目')
|
|
|
+ const workingTimes = data.map(item => Number(item.workingTime || 0))
|
|
|
+ const overtimeTimes = data.map(item => Number(item.overtimeHours || 0))
|
|
|
+
|
|
|
+ const option = {
|
|
|
+ tooltip: {
|
|
|
+ trigger: 'axis',
|
|
|
+ axisPointer: { type: 'shadow' },
|
|
|
+ formatter: function(params) {
|
|
|
+ let result = params[0].name + '<br/>'
|
|
|
+ params.forEach(p => {
|
|
|
+ result += p.marker + p.seriesName + ': ' + p.value + ' h<br/>'
|
|
|
+ })
|
|
|
+ return result
|
|
|
+ }
|
|
|
+ },
|
|
|
+ legend: {
|
|
|
+ data: ['工时(h)', '加班工时(h)'],
|
|
|
+ top: 10
|
|
|
+ },
|
|
|
+ grid: {
|
|
|
+ left: '3%',
|
|
|
+ right: '8%',
|
|
|
+ bottom: '3%',
|
|
|
+ top: 50,
|
|
|
+ containLabel: true
|
|
|
+ },
|
|
|
+ xAxis: {
|
|
|
+ type: 'value',
|
|
|
+ axisLabel: { formatter: '{value} h' }
|
|
|
+ },
|
|
|
+ yAxis: {
|
|
|
+ type: 'category',
|
|
|
+ data: names,
|
|
|
+ axisLabel: {
|
|
|
+ width: 120,
|
|
|
+ overflow: 'truncate',
|
|
|
+ ellipsis: '...'
|
|
|
+ }
|
|
|
+ },
|
|
|
+ series: [
|
|
|
+ {
|
|
|
+ name: '工时(h)',
|
|
|
+ type: 'bar',
|
|
|
+ data: workingTimes,
|
|
|
+ itemStyle: { color: '#5B9BD5' },
|
|
|
+ label: { show: true, position: 'right', formatter: '{c} h' }
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: '加班工时(h)',
|
|
|
+ type: 'bar',
|
|
|
+ data: overtimeTimes,
|
|
|
+ itemStyle: { color: '#ED7D31' },
|
|
|
+ label: { show: true, position: 'right', formatter: '{c} h' }
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ }
|
|
|
+ this.chart1.setOption(option)
|
|
|
+ },
|
|
|
+
|
|
|
+ // 图表2:工时前三项目部门分配 - 环形饼图(支持工时/加班工时切换)
|
|
|
+ renderChart2() {
|
|
|
+ if (!this.$refs.chart2) return
|
|
|
+ if (this.chart2) { this.chart2.dispose(); this.chart2 = null }
|
|
|
+ const el = this.$refs.chart2
|
|
|
+ const parentEl = el.parentElement
|
|
|
+ const width = parentEl ? parentEl.clientWidth - 32 : el.clientWidth
|
|
|
+ this.chart2 = echarts.init(el, null, { width: width, height: 300 })
|
|
|
+
|
|
|
+ const isOvertime = this.top3PieMode === 'overtime'
|
|
|
+ const seriesName = isOvertime ? '部门加班工时分配' : '部门工时分配'
|
|
|
+ const valueLabel = isOvertime ? '加班工时' : '工时'
|
|
|
+
|
|
|
+ // 部门名称已在 loadTop3ProjectDept 中完成转译(如需要)
|
|
|
+ // pieData 中额外存储 openId 供 tooltip 使用
|
|
|
+ const pieData = this.top3DeptData.map(item => ({
|
|
|
+ name: item.departmentName,
|
|
|
+ openId: item._deptOpenId || item.departmentName,
|
|
|
+ value: isOvertime
|
|
|
+ ? Number(item.overtimeHours || 0)
|
|
|
+ : Number(item.workingTime || 0)
|
|
|
+ }))
|
|
|
+
|
|
|
+ const deptNameHtml = this.deptNameHtml.bind(this)
|
|
|
+ const needTranslate = this.user.userNameNeedTranslate == 1
|
|
|
+ const option = {
|
|
|
+ tooltip: {
|
|
|
+ trigger: 'item',
|
|
|
+ enterable: needTranslate,
|
|
|
+ formatter: function(params) {
|
|
|
+ const openId = params.data && params.data.openId ? params.data.openId : null
|
|
|
+ return deptNameHtml(params.name, openId) + '<br/>' + valueLabel + ': ' + params.value + ' h<br/>占比: ' + params.percent + '%'
|
|
|
+ }
|
|
|
+ },
|
|
|
+ legend: {
|
|
|
+ orient: 'vertical',
|
|
|
+ left: 'left',
|
|
|
+ top: 'center'
|
|
|
+ },
|
|
|
+ series: [
|
|
|
+ {
|
|
|
+ name: seriesName,
|
|
|
+ type: 'pie',
|
|
|
+ radius: ['35%', '65%'],
|
|
|
+ center: ['60%', '50%'],
|
|
|
+ avoidLabelOverlap: true,
|
|
|
+ itemStyle: {
|
|
|
+ borderRadius: 6,
|
|
|
+ borderColor: '#fff',
|
|
|
+ borderWidth: 2
|
|
|
+ },
|
|
|
+ label: {
|
|
|
+ show: true,
|
|
|
+ formatter: '{b}\n{d}%'
|
|
|
+ },
|
|
|
+ emphasis: {
|
|
|
+ label: { show: true, fontSize: 14, fontWeight: 'bold' }
|
|
|
+ },
|
|
|
+ data: pieData
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ }
|
|
|
+ this.chart2.setOption(option)
|
|
|
+ },
|
|
|
+
|
|
|
+ // 图表3:各部门总工时和人均工时 - 双轴柱线图
|
|
|
+ renderChart3() {
|
|
|
+ if (!this.$refs.chart3) return
|
|
|
+ if (this.chart3) { this.chart3.dispose(); this.chart3 = null }
|
|
|
+ const el = this.$refs.chart3
|
|
|
+ const parentEl = el.parentElement
|
|
|
+ const width = parentEl ? parentEl.clientWidth - 32 : el.clientWidth
|
|
|
+ this.chart3 = echarts.init(el, null, { width: width, height: 300 })
|
|
|
+
|
|
|
+ // 部门名称已在 loadDeptHours 中完成转译(如需要)
|
|
|
+ const depts = this.deptHoursData.map(item => item.departmentName)
|
|
|
+ const totalHours = this.deptHoursData.map(item => Number((item.workingTime || 0).toFixed(2)))
|
|
|
+ const avgHours = this.deptHoursData.map(item => Number((item.avgWorkingTime || 0).toFixed(2)))
|
|
|
+ const totalOvertime = this.deptHoursData.map(item => Number((item.overtimeHours || 0).toFixed(2)))
|
|
|
+ const avgOvertime = this.deptHoursData.map(item => Number((item.avgOvertimeHours || 0).toFixed(2)))
|
|
|
+
|
|
|
+ // 建立 转译名称 -> 原始openId 的映射,供 tooltip 使用
|
|
|
+ const deptOpenIdMap3 = {}
|
|
|
+ this.deptHoursData.forEach(item => {
|
|
|
+ deptOpenIdMap3[item.departmentName] = item._deptOpenId || item.departmentName
|
|
|
+ })
|
|
|
+
|
|
|
+ const deptNameHtml3 = this.deptNameHtml.bind(this)
|
|
|
+ const needTranslate3 = this.user.userNameNeedTranslate == 1
|
|
|
+ const option = {
|
|
|
+ tooltip: {
|
|
|
+ trigger: 'axis',
|
|
|
+ axisPointer: { type: 'shadow' },
|
|
|
+ enterable: needTranslate3,
|
|
|
+ formatter: function(params) {
|
|
|
+ const openId = deptOpenIdMap3[params[0].name] || null
|
|
|
+ let result = deptNameHtml3(params[0].name, openId) + '<br/>'
|
|
|
+ params.forEach(p => {
|
|
|
+ result += p.marker + p.seriesName + ': ' + p.value + ' h<br/>'
|
|
|
+ })
|
|
|
+ return result
|
|
|
+ }
|
|
|
+ },
|
|
|
+ legend: {
|
|
|
+ data: ['总工时(h)', '总加班工时(h)', '人均工时(h)', '人均加班工时(h)'],
|
|
|
+ top: 10,
|
|
|
+ textStyle: { fontSize: 11 }
|
|
|
+ },
|
|
|
+ grid: {
|
|
|
+ left: '3%',
|
|
|
+ right: '5%',
|
|
|
+ bottom: '15%',
|
|
|
+ top: 60,
|
|
|
+ containLabel: true
|
|
|
+ },
|
|
|
+ xAxis: {
|
|
|
+ type: 'category',
|
|
|
+ data: depts,
|
|
|
+ axisLabel: { rotate: 30, interval: 0, fontSize: 11 }
|
|
|
+ },
|
|
|
+ yAxis: [
|
|
|
+ {
|
|
|
+ type: 'value',
|
|
|
+ name: '总工时(h)',
|
|
|
+ axisLabel: { formatter: '{value} h' }
|
|
|
+ },
|
|
|
+ {
|
|
|
+ type: 'value',
|
|
|
+ name: '人均工时(h)',
|
|
|
+ axisLabel: { formatter: '{value} h' }
|
|
|
+ }
|
|
|
+ ],
|
|
|
+ series: [
|
|
|
+ {
|
|
|
+ name: '总工时(h)',
|
|
|
+ type: 'bar',
|
|
|
+ yAxisIndex: 0,
|
|
|
+ data: totalHours,
|
|
|
+ itemStyle: { color: '#5B9BD5' },
|
|
|
+ barMaxWidth: 40
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: '总加班工时(h)',
|
|
|
+ type: 'bar',
|
|
|
+ yAxisIndex: 0,
|
|
|
+ data: totalOvertime,
|
|
|
+ itemStyle: { color: '#ED7D31' },
|
|
|
+ barMaxWidth: 40
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: '人均工时(h)',
|
|
|
+ type: 'line',
|
|
|
+ yAxisIndex: 1,
|
|
|
+ data: avgHours,
|
|
|
+ itemStyle: { color: '#70AD47' },
|
|
|
+ lineStyle: { width: 2 },
|
|
|
+ symbol: 'circle',
|
|
|
+ symbolSize: 6
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: '人均加班工时(h)',
|
|
|
+ type: 'line',
|
|
|
+ yAxisIndex: 1,
|
|
|
+ data: avgOvertime,
|
|
|
+ itemStyle: { color: '#FFC000' },
|
|
|
+ lineStyle: { width: 2 },
|
|
|
+ symbol: 'circle',
|
|
|
+ symbolSize: 6
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ }
|
|
|
+ this.chart3.setOption(option)
|
|
|
+ },
|
|
|
+
|
|
|
+ // 图表4:各部门参与项目数量 - 柱状图
|
|
|
+ renderChart4() {
|
|
|
+ if (!this.$refs.chart4) return
|
|
|
+ if (this.chart4) { this.chart4.dispose(); this.chart4 = null }
|
|
|
+ const el = this.$refs.chart4
|
|
|
+ const parentEl = el.parentElement
|
|
|
+ const width = parentEl ? parentEl.clientWidth - 32 : el.clientWidth
|
|
|
+ this.chart4 = echarts.init(el, null, { width: width, height: 300 })
|
|
|
+
|
|
|
+ // 部门名称已在 loadDeptProjectCount 中完成转译(如需要)
|
|
|
+ const depts = this.deptProjectCountData.map(item => item.departmentName)
|
|
|
+ const counts = this.deptProjectCountData.map(item => Number(item.projectCount || 0))
|
|
|
+
|
|
|
+ // 建立 转译名称 -> 原始openId 的映射,供 tooltip 使用
|
|
|
+ const deptOpenIdMap4 = {}
|
|
|
+ this.deptProjectCountData.forEach(item => {
|
|
|
+ deptOpenIdMap4[item.departmentName] = item._deptOpenId || item.departmentName
|
|
|
+ })
|
|
|
+
|
|
|
+ const deptNameHtml4 = this.deptNameHtml.bind(this)
|
|
|
+ const needTranslate4 = this.user.userNameNeedTranslate == 1
|
|
|
+ const option = {
|
|
|
+ tooltip: {
|
|
|
+ trigger: 'axis',
|
|
|
+ axisPointer: { type: 'shadow' },
|
|
|
+ enterable: needTranslate4,
|
|
|
+ formatter: function(params) {
|
|
|
+ const openId = deptOpenIdMap4[params[0].name] || null
|
|
|
+ return deptNameHtml4(params[0].name, openId) + '<br/>' + params[0].marker + '参与项目数: ' + params[0].value + ' 个'
|
|
|
+ }
|
|
|
+ },
|
|
|
+ grid: {
|
|
|
+ left: '3%',
|
|
|
+ right: '5%',
|
|
|
+ bottom: '15%',
|
|
|
+ top: 30,
|
|
|
+ containLabel: true
|
|
|
+ },
|
|
|
+ xAxis: {
|
|
|
+ type: 'category',
|
|
|
+ data: depts,
|
|
|
+ axisLabel: { rotate: 30, interval: 0, fontSize: 11 }
|
|
|
+ },
|
|
|
+ yAxis: {
|
|
|
+ type: 'value',
|
|
|
+ name: '项目数量(个)',
|
|
|
+ minInterval: 1,
|
|
|
+ axisLabel: { formatter: '{value} 个' }
|
|
|
+ },
|
|
|
+ series: [
|
|
|
+ {
|
|
|
+ name: '参与项目数',
|
|
|
+ type: 'bar',
|
|
|
+ data: counts,
|
|
|
+ barMaxWidth: 50,
|
|
|
+ itemStyle: {
|
|
|
+ color: function(params) {
|
|
|
+ const colorList = [
|
|
|
+ '#5B9BD5', '#ED7D31', '#70AD47', '#FFC000',
|
|
|
+ '#4472C4', '#FF6B6B', '#48CAE4', '#9B59B6',
|
|
|
+ '#2ECC71', '#E74C3C'
|
|
|
+ ]
|
|
|
+ return colorList[params.dataIndex % colorList.length]
|
|
|
+ }
|
|
|
+ },
|
|
|
+ label: {
|
|
|
+ show: true,
|
|
|
+ position: 'top',
|
|
|
+ formatter: '{c} 个'
|
|
|
+ }
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ }
|
|
|
+ this.chart4.setOption(option)
|
|
|
+ },
|
|
|
+
|
|
|
+ handleResize() {
|
|
|
+ const resizeChart = (chart, refName) => {
|
|
|
+ if (!chart || !this.$refs[refName]) return
|
|
|
+ const el = this.$refs[refName]
|
|
|
+ const parentEl = el.parentElement
|
|
|
+ const width = parentEl ? parentEl.clientWidth - 32 : el.clientWidth
|
|
|
+ chart.resize({ width: width })
|
|
|
+ }
|
|
|
+ resizeChart(this.chart1, 'chart1')
|
|
|
+ resizeChart(this.chart2, 'chart2')
|
|
|
+ resizeChart(this.chart3, 'chart3')
|
|
|
+ resizeChart(this.chart4, 'chart4')
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.dashboard-container {
|
|
|
+ padding: 12px 20px;
|
|
|
+ background: #f5f6fa;
|
|
|
+ min-height: 100%;
|
|
|
+ box-sizing: border-box;
|
|
|
+ width: 100%;
|
|
|
+ max-width: 100%;
|
|
|
+ overflow-x: hidden;
|
|
|
+}
|
|
|
+
|
|
|
+.dashboard-header {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ margin-bottom: 12px;
|
|
|
+ background: #fff;
|
|
|
+ padding: 12px 20px;
|
|
|
+ border-radius: 8px;
|
|
|
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
|
|
+ box-sizing: border-box;
|
|
|
+}
|
|
|
+
|
|
|
+.dashboard-title {
|
|
|
+ margin: 0;
|
|
|
+ font-size: 16px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #303133;
|
|
|
+}
|
|
|
+
|
|
|
+.charts-grid {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: 1fr 1fr;
|
|
|
+ gap: 16px;
|
|
|
+ width: 100%;
|
|
|
+ box-sizing: border-box;
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+
|
|
|
+.chart-card {
|
|
|
+ background: #fff;
|
|
|
+ border-radius: 8px;
|
|
|
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
|
|
+ padding: 16px;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ box-sizing: border-box;
|
|
|
+ overflow: hidden;
|
|
|
+ min-width: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.chart-title {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ font-size: 15px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #303133;
|
|
|
+ margin-bottom: 12px;
|
|
|
+ padding-bottom: 10px;
|
|
|
+ border-bottom: 2px solid #409EFF;
|
|
|
+ flex-shrink: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.chart-title-actions {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.save-img-btn {
|
|
|
+ font-size: 16px;
|
|
|
+ color: #909399;
|
|
|
+ padding: 0;
|
|
|
+ line-height: 1;
|
|
|
+ transition: color 0.2s;
|
|
|
+}
|
|
|
+
|
|
|
+.save-img-btn:hover:not([disabled]) {
|
|
|
+ color: #409EFF;
|
|
|
+}
|
|
|
+
|
|
|
+.save-img-btn[disabled] {
|
|
|
+ color: #c0c4cc;
|
|
|
+ cursor: not-allowed;
|
|
|
+}
|
|
|
+
|
|
|
+.chart-body {
|
|
|
+ height: 300px;
|
|
|
+ width: 100%;
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+
|
|
|
+.chart-loading,
|
|
|
+.chart-empty {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ color: #909399;
|
|
|
+ font-size: 14px;
|
|
|
+ height: 300px;
|
|
|
+}
|
|
|
+
|
|
|
+.chart-loading i {
|
|
|
+ margin-right: 6px;
|
|
|
+ font-size: 18px;
|
|
|
+}
|
|
|
+</style>
|