|
@@ -0,0 +1,285 @@
|
|
|
|
|
+<template>
|
|
|
|
|
+ <div class="org-dashboard">
|
|
|
|
|
+ <!-- 总览卡片 -->
|
|
|
|
|
+ <el-row :gutter="16" style="margin-bottom: 16px">
|
|
|
|
|
+ <el-col :span="4"><el-card shadow="never" class="stat-card">
|
|
|
|
|
+ <div class="stat-num">{{ overview.memberCount }}</div>
|
|
|
|
|
+ <div class="stat-label">成员总数</div>
|
|
|
|
|
+ </el-card></el-col>
|
|
|
|
|
+ <el-col :span="4"><el-card shadow="never" class="stat-card">
|
|
|
|
|
+ <div class="stat-num">{{ overview.plannerCount }}</div>
|
|
|
|
|
+ <div class="stat-label">规划师数</div>
|
|
|
|
|
+ </el-card></el-col>
|
|
|
|
|
+ <el-col :span="4"><el-card shadow="never" class="stat-card">
|
|
|
|
|
+ <div class="stat-num">{{ overview.userCount }}</div>
|
|
|
|
|
+ <div class="stat-label">普通用户</div>
|
|
|
|
|
+ </el-card></el-col>
|
|
|
|
|
+ <el-col :span="4"><el-card shadow="never" class="stat-card">
|
|
|
|
|
+ <div class="stat-num">{{ overview.familyCount }}</div>
|
|
|
|
|
+ <div class="stat-label">家庭数</div>
|
|
|
|
|
+ </el-card></el-col>
|
|
|
|
|
+ <el-col :span="4"><el-card shadow="never" class="stat-card">
|
|
|
|
|
+ <div class="stat-num">{{ overview.totalOrders }}</div>
|
|
|
|
|
+ <div class="stat-label">订单数</div>
|
|
|
|
|
+ </el-card></el-col>
|
|
|
|
|
+ <el-col :span="4"><el-card shadow="never" class="stat-card">
|
|
|
|
|
+ <div class="stat-num">{{ formatAmount(overview.totalConsumption) }}</div>
|
|
|
|
|
+ <div class="stat-label">总消费(元)</div>
|
|
|
|
|
+ </el-card></el-col>
|
|
|
|
|
+ </el-row>
|
|
|
|
|
+
|
|
|
|
|
+ <el-row :gutter="16" style="margin-bottom: 16px">
|
|
|
|
|
+ <!-- 活跃度趋势 -->
|
|
|
|
|
+ <el-col :span="16">
|
|
|
|
|
+ <el-card shadow="never">
|
|
|
|
|
+ <div slot="header">
|
|
|
|
|
+ <span>活跃度趋势(任务完成 / 打卡 / 报告上传)</span>
|
|
|
|
|
+ <el-date-picker v-model="activeDateRange" type="daterange" size="mini"
|
|
|
|
|
+ style="float:right;margin-left:10px" value-format="yyyy-MM-dd"
|
|
|
|
|
+ range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期"
|
|
|
|
|
+ @change="onActiveRangeChange"></el-date-picker>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div ref="activeChart" class="echarts-container"></div>
|
|
|
|
|
+ </el-card>
|
|
|
|
|
+ </el-col>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 趋势图:活跃成员/订单/收入 -->
|
|
|
|
|
+ <el-col :span="8">
|
|
|
|
|
+ <el-card shadow="never">
|
|
|
|
|
+ <div slot="header">
|
|
|
|
|
+ <span>趋势</span>
|
|
|
|
|
+ <el-date-picker v-model="trendDateRange" type="daterange" size="mini"
|
|
|
|
|
+ style="float:right;margin-left:10px" value-format="yyyy-MM-dd"
|
|
|
|
|
+ range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期"
|
|
|
|
|
+ @change="onTrendRangeChange"></el-date-picker>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div ref="trendChart" class="echarts-container" style="height: 280px"></div>
|
|
|
|
|
+ </el-card>
|
|
|
|
|
+ </el-col>
|
|
|
|
|
+ </el-row>
|
|
|
|
|
+
|
|
|
|
|
+ <el-row :gutter="16">
|
|
|
|
|
+ <!-- 家庭维度 -->
|
|
|
|
|
+ <el-col :span="8">
|
|
|
|
|
+ <el-card shadow="never">
|
|
|
|
|
+ <div slot="header">家庭维度</div>
|
|
|
|
|
+ <div style="padding: 10px 0">
|
|
|
|
|
+ <div class="dim-row"><span>活跃家庭数</span><b>{{ families.activeFamilyCount }}</b></div>
|
|
|
|
|
+ <div class="dim-row"><span>任务完成率</span><b>{{ families.taskCompletionRate }}%</b></div>
|
|
|
|
|
+ <div class="dim-row"><span>打卡率</span><b>{{ families.checkinRate }}%</b></div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </el-card>
|
|
|
|
|
+ </el-col>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 营收 -->
|
|
|
|
|
+ <el-col :span="16">
|
|
|
|
|
+ <el-card shadow="never">
|
|
|
|
|
+ <div slot="header">
|
|
|
|
|
+ <span>营收</span>
|
|
|
|
|
+ <el-date-picker v-model="revenueDateRange" type="daterange" size="mini"
|
|
|
|
|
+ style="float:right;margin-left:10px" value-format="yyyy-MM-dd"
|
|
|
|
|
+ range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期"
|
|
|
|
|
+ @change="onRevenueRangeChange"></el-date-picker>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div ref="revenueChart" class="echarts-container" style="height: 230px"></div>
|
|
|
|
|
+ </el-card>
|
|
|
|
|
+ </el-col>
|
|
|
|
|
+ </el-row>
|
|
|
|
|
+
|
|
|
|
|
+ <el-backtop :right="50" :bottom="50"></el-backtop>
|
|
|
|
|
+ </div>
|
|
|
|
|
+</template>
|
|
|
|
|
+
|
|
|
|
|
+<script>
|
|
|
|
|
+import * as echarts from 'echarts'
|
|
|
|
|
+import { orgMy, orgStatsOverview, orgStatsFamilies, orgStatsActive, orgStatsRevenue, orgStatsTrend } from '@/api/org'
|
|
|
|
|
+
|
|
|
|
|
+export default {
|
|
|
|
|
+ name: 'OrgDashboard',
|
|
|
|
|
+ data() {
|
|
|
|
|
+ return {
|
|
|
|
|
+ orgId: null,
|
|
|
|
|
+ orgName: '',
|
|
|
|
|
+ overview: { memberCount: 0, plannerCount: 0, userCount: 0, familyCount: 0, totalOrders: 0, totalConsumption: 0 },
|
|
|
|
|
+ families: { activeFamilyCount: 0, taskCompletionRate: 0, checkinRate: 0 },
|
|
|
|
|
+ activeDateRange: null,
|
|
|
|
|
+ trendDateRange: null,
|
|
|
|
|
+ revenueDateRange: null
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ created() {
|
|
|
|
|
+ this.loadMyOrg()
|
|
|
|
|
+ this.initCharts()
|
|
|
|
|
+ },
|
|
|
|
|
+ beforeDestroy() {
|
|
|
|
|
+ if (this.activeChart) this.activeChart.dispose()
|
|
|
|
|
+ if (this.trendChart) this.trendChart.dispose()
|
|
|
|
|
+ if (this.revenueChart) this.revenueChart.dispose()
|
|
|
|
|
+ },
|
|
|
|
|
+ methods: {
|
|
|
|
|
+ async loadMyOrg() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await orgMy()
|
|
|
|
|
+ if (res.data && res.data.id) {
|
|
|
|
|
+ this.orgId = res.data.id
|
|
|
|
|
+ this.orgName = res.data.name
|
|
|
|
|
+ this.$refs && this.$refs.header && (this.$refs.header.title = '组织大屏 — ' + res.data.name)
|
|
|
|
|
+ this.loadStats()
|
|
|
|
|
+ } else if (res.data && res.data.status === 'disabled') {
|
|
|
|
|
+ this.$message.error('组织已停用,无法查看统计大屏')
|
|
|
|
|
+ setTimeout(() => this.$router.replace('/login'), 1500)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ this.$message.warning('未关联任何组织')
|
|
|
|
|
+ setTimeout(() => this.$router.replace('/org'), 1500)
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ this.$message.error('加载组织失败')
|
|
|
|
|
+ setTimeout(() => this.$router.replace('/org'), 1500)
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ async loadStats() {
|
|
|
|
|
+ if (!this.orgId) return
|
|
|
|
|
+ const base = { orgId: this.orgId }
|
|
|
|
|
+ if (this.activeDateRange && this.activeDateRange.length === 2) {
|
|
|
|
|
+ base.startDate = this.activeDateRange[0]
|
|
|
|
|
+ base.endDate = this.activeDateRange[1]
|
|
|
|
|
+ }
|
|
|
|
|
+ if (this.revenueDateRange && this.revenueDateRange.length === 2) {
|
|
|
|
|
+ base.startDate = this.revenueDateRange[0]
|
|
|
|
|
+ base.endDate = this.revenueDateRange[1]
|
|
|
|
|
+ }
|
|
|
|
|
+ if (this.trendDateRange && this.trendDateRange.length === 2) {
|
|
|
|
|
+ base.startDate = this.trendDateRange[0]
|
|
|
|
|
+ base.endDate = this.trendDateRange[1]
|
|
|
|
|
+ }
|
|
|
|
|
+ try {
|
|
|
|
|
+ const [ov, fam, act, rev, tre] = await Promise.all([
|
|
|
|
|
+ orgStatsOverview(base),
|
|
|
|
|
+ orgStatsFamilies(base),
|
|
|
|
|
+ orgStatsActive(base),
|
|
|
|
|
+ orgStatsRevenue(base),
|
|
|
|
|
+ orgStatsTrend(base)
|
|
|
|
|
+ ])
|
|
|
|
|
+ this.overview = ov.data || {}
|
|
|
|
|
+ this.families = fam.data || {}
|
|
|
|
|
+ this.renderActiveChart(act.data || [])
|
|
|
|
|
+ this.renderRevenueChart(rev.data || [])
|
|
|
|
|
+ this.renderTrendChart(tre.data || [])
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ this.$message.error('加载统计失败')
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ formatAmount(v) {
|
|
|
|
|
+ if (!v) return '0'
|
|
|
|
|
+ return (v / 100).toFixed(2)
|
|
|
|
|
+ },
|
|
|
|
|
+ onActiveRangeChange() { this.loadStats() },
|
|
|
|
|
+ onTrendRangeChange() { this.loadStats() },
|
|
|
|
|
+ onRevenueRangeChange() { this.loadStats() },
|
|
|
|
|
+
|
|
|
|
|
+ initCharts() {
|
|
|
|
|
+ this.activeChart = echarts.init(this.$refs.activeChart)
|
|
|
|
|
+ this.trendChart = echarts.init(this.$refs.trendChart)
|
|
|
|
|
+ this.revenueChart = echarts.init(this.$refs.revenueChart)
|
|
|
|
|
+ window.addEventListener('resize', this.handleResize)
|
|
|
|
|
+ },
|
|
|
|
|
+ handleResize() {
|
|
|
|
|
+ this.activeChart && this.activeChart.resize()
|
|
|
|
|
+ this.trendChart && this.trendChart.resize()
|
|
|
|
|
+ this.revenueChart && this.revenueChart.resize()
|
|
|
|
|
+ },
|
|
|
|
|
+ renderActiveChart(data) {
|
|
|
|
|
+ const days = data.map(d => d.day).sort()
|
|
|
|
|
+ const tasks = data.map(d => d.taskCompleted || 0)
|
|
|
|
|
+ const checkins = data.map(d => d.checkins || 0)
|
|
|
|
|
+ const reports = data.map(d => d.reports || 0)
|
|
|
|
|
+ this.activeChart.setOption({
|
|
|
|
|
+ tooltip: { trigger: 'axis' },
|
|
|
|
|
+ legend: { data: ['任务完成', '打卡', '报告上传'] },
|
|
|
|
|
+ grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
|
|
|
|
|
+ xAxis: { type: 'category', data: days, axisLabel: { interval: 0, rotate: 30 } },
|
|
|
|
|
+ yAxis: { type: 'value' },
|
|
|
|
|
+ series: [
|
|
|
|
|
+ { name: '任务完成', type: 'line', smooth: true, data: tasks, itemStyle: { color: '#1677FF' } },
|
|
|
|
|
+ { name: '打卡', type: 'line', smooth: true, data: checkins, itemStyle: { color: '#10B981' } },
|
|
|
|
|
+ { name: '报告上传', type: 'line', smooth: true, data: reports, itemStyle: { color: '#F59E0B' } }
|
|
|
|
|
+ ]
|
|
|
|
|
+ })
|
|
|
|
|
+ },
|
|
|
|
|
+ renderRevenueChart(data) {
|
|
|
|
|
+ const days = data.map(d => d.day).sort()
|
|
|
|
|
+ const orderAmt = data.map(d => d.orderAmount || 0)
|
|
|
|
|
+ const subAmt = data.map(d => d.subAmount || 0)
|
|
|
|
|
+ const commission = data.map(d => d.commission || 0)
|
|
|
|
|
+ this.revenueChart.setOption({
|
|
|
|
|
+ tooltip: { trigger: 'axis' },
|
|
|
|
|
+ legend: { data: ['订单金额', '会员订阅', '佣金支出'] },
|
|
|
|
|
+ grid: { left: '3%', right: '4%', bottom: '3%', containLabel: 1 },
|
|
|
|
|
+ xAxis: { type: 'category', data: days, axisLabel: { interval: 0, rotate: 30 } },
|
|
|
|
|
+ yAxis: { type: 'value' },
|
|
|
|
|
+ series: [
|
|
|
|
|
+ { name: '订单金额', type: 'bar', stack: 'total', data: orderAmt, itemStyle: { color: '#1677FF' } },
|
|
|
|
|
+ { name: '会员订阅', type: 'bar', stack: 'total', data: subAmt, itemStyle: { color: '#10B981' } },
|
|
|
|
|
+ { name: '佣金支出', type: 'bar', stack: 'total', data: commission, itemStyle: { color: '#F59E0B' } }
|
|
|
|
|
+ ]
|
|
|
|
|
+ })
|
|
|
|
|
+ },
|
|
|
|
|
+ renderTrendChart(data) {
|
|
|
|
|
+ const days = data.map(d => d.day).sort()
|
|
|
|
|
+ const active = data.map(d => d.activeMembers || 0)
|
|
|
|
|
+ const orders = data.map(d => d.orderCount || 0)
|
|
|
|
|
+ const income = data.map(d => (d.income || 0) / 100)
|
|
|
|
|
+ this.trendChart.setOption({
|
|
|
|
|
+ tooltip: { trigger: 'axis' },
|
|
|
|
|
+ legend: { data: ['活跃成员数', '订单数', '收入(元)'] },
|
|
|
|
|
+ grid: { left: '3%', right: '4%', bottom: '3%', containLabel: 1 },
|
|
|
|
|
+ xAxis: { type: 'category', data: days, axisLabel: { interval: 0, rotate: 30 } },
|
|
|
|
|
+ yAxis: [
|
|
|
|
|
+ { type: 'value', name: '数量' },
|
|
|
|
|
+ { type: 'value', name: '收入(元)', position: 'right' }
|
|
|
|
|
+ ],
|
|
|
|
|
+ series: [
|
|
|
|
|
+ { name: '活跃成员数', type: 'line', smooth: true, yAxisIndex: 0, data: active, itemStyle: { color: '#1677FF' } },
|
|
|
|
|
+ { name: '订单数', type: 'line', smooth: true, yAxisIndex: 0, data: orders, itemStyle: { color: '#10B981' } },
|
|
|
|
|
+ { name: '收入(元)', type: 'line', smooth: true, yAxisIndex: 1, data: income, itemStyle: { color: '#F59E0B' } }
|
|
|
|
|
+ ]
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+</script>
|
|
|
|
|
+
|
|
|
|
|
+<style scoped>
|
|
|
|
|
+.org-dashboard {
|
|
|
|
|
+ padding: 16px;
|
|
|
|
|
+}
|
|
|
|
|
+.stat-card {
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+}
|
|
|
|
|
+.stat-card >>> .el-card__body {
|
|
|
|
|
+ padding: 16px 8px;
|
|
|
|
|
+}
|
|
|
|
|
+.stat-num {
|
|
|
|
|
+ font-size: 24px;
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+ color: #1a1a1a;
|
|
|
|
|
+ line-height: 1.2;
|
|
|
|
|
+}
|
|
|
|
|
+.stat-label {
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: #888;
|
|
|
|
|
+ margin-top: 4px;
|
|
|
|
|
+}
|
|
|
|
|
+.echarts-container {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: 260px;
|
|
|
|
|
+}
|
|
|
|
|
+.dim-row {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ padding: 8px 0;
|
|
|
|
|
+ border-bottom: 1px dashed #f0f0f0;
|
|
|
|
|
+}
|
|
|
|
|
+.dim-row:last-child { border-bottom: none; }
|
|
|
|
|
+</style>
|