优化首页

This commit is contained in:
2026-04-18 16:14:47 +08:00
parent 7d0f11fe43
commit 68657e2648
5 changed files with 757 additions and 564 deletions

View File

@@ -5,18 +5,389 @@ declare(strict_types=1);
namespace app\admin\controller;
use app\common\controller\Backend;
use support\think\Db;
use Webman\Http\Request;
use support\Response;
/**
* 后台首页统计:按渠道数据范围(非超管仅本渠道)汇总用户、充值、提现、投注等核心指标。
*/
class Dashboard extends Backend
{
public function index(Request $request): Response
{
$response = $this->initializeBackend($request);
if ($response !== null) return $response;
if ($response !== null) {
return $response;
}
$scope = $this->channelScopeOrNull();
$todayStart = strtotime(date('Y-m-d'));
$todayEnd = $todayStart + 86400 - 1;
$yesterdayStart = $todayStart - 86400;
$yesterdayEnd = $todayStart - 1;
$userTotal = $this->countUsers($scope);
$newToday = $this->countUsersInRange($scope, $todayStart, $todayEnd);
$newYesterday = $this->countUsersInRange($scope, $yesterdayStart, $yesterdayEnd);
$growthPct = null;
if ($newYesterday > 0) {
$growthPct = round(($newToday - $newYesterday) / $newYesterday * 100, 1);
} elseif ($newToday > 0) {
$growthPct = 100.0;
}
$depositAgg = $this->aggregateDepositToday($scope, $todayStart, $todayEnd);
$withdrawPending = $this->countWithdrawPending($scope);
$betAgg = $this->aggregateBetToday($scope, $todayStart, $todayEnd);
$trend = $this->buildSevenDayTrend($scope);
$channelShare = $this->buildChannelShare($scope);
$depositAmountChannelShare = $this->buildDepositAmountChannelShare($scope);
$recentUsers = $this->fetchRecentUsers($scope, 10);
return $this->success('', [
'remark' => get_route_remark()
'remark' => get_route_remark(),
'stats' => [
'user_total' => $userTotal,
'user_new_today' => $newToday,
'user_new_yesterday' => $newYesterday,
'user_new_growth_pct' => $growthPct,
'deposit_today_amount' => $depositAgg['amount'],
'deposit_today_count' => $depositAgg['count'],
'withdraw_pending' => $withdrawPending,
'bet_today_amount' => $betAgg['amount'],
'bet_today_count' => $betAgg['count'],
],
'trend' => $trend,
'channel_share' => $channelShare,
'deposit_amount_channel_share' => $depositAmountChannelShare,
'recent_users' => $recentUsers,
]);
}
/**
* @param int[]|null $scope null=超管不限制;非 null 时 whereIn channel_id
*/
private function countUsers(?array $scope): int
{
$q = Db::name('user');
if ($scope !== null) {
$q->whereIn('channel_id', $scope);
}
return intval($q->count());
}
/**
* @param int[]|null $scope
*/
private function countUsersInRange(?array $scope, int $start, int $end): int
{
$q = Db::name('user')
->where('create_time', '>=', $start)
->where('create_time', '<=', $end);
if ($scope !== null) {
$q->whereIn('channel_id', $scope);
}
return intval($q->count());
}
/**
* 今日成功充值status=1按创建日落在今日与 mock 即时成功一致)。
*
* @param int[]|null $scope
* @return array{count:int, amount:string}
*/
private function aggregateDepositToday(?array $scope, int $todayStart, int $todayEnd): array
{
$q = Db::name('deposit_order')
->where('status', 1)
->where('create_time', '>=', $todayStart)
->where('create_time', '<=', $todayEnd);
if ($scope !== null) {
$q->whereIn('channel_id', $scope);
}
$rows = $q->fieldRaw('COUNT(*) AS c, COALESCE(SUM(CAST(amount AS DECIMAL(18,4))),0) AS s')->find();
if (!is_array($rows)) {
$rows = [];
}
$count = isset($rows['c']) ? intval($rows['c']) : 0;
$sum = isset($rows['s']) ? strval($rows['s']) : '0';
$amount = $this->formatMoney2($sum);
return ['count' => $count, 'amount' => $amount];
}
/**
* @param int[]|null $scope
*/
private function countWithdrawPending(?array $scope): int
{
$q = Db::name('withdraw_order')->where('status', 0);
if ($scope !== null) {
$q->whereIn('channel_id', $scope);
}
return intval($q->count());
}
/**
* 今日投注创建时间在今日且订单未作废status 1 或 2
*
* @param int[]|null $scope
* @return array{count:int, amount:string}
*/
private function aggregateBetToday(?array $scope, int $todayStart, int $todayEnd): array
{
$q = Db::name('bet_order')
->whereIn('status', [1, 2])
->where('create_time', '>=', $todayStart)
->where('create_time', '<=', $todayEnd);
if ($scope !== null) {
$q->whereIn('channel_id', $scope);
}
$rows = $q->fieldRaw('COUNT(*) AS c, COALESCE(SUM(CAST(total_amount AS DECIMAL(18,4))),0) AS s')->find();
if (!is_array($rows)) {
$rows = [];
}
$count = isset($rows['c']) ? intval($rows['c']) : 0;
$sum = isset($rows['s']) ? strval($rows['s']) : '0';
return ['count' => $count, 'amount' => $this->formatMoney2($sum)];
}
/**
* @param int[]|null $scope
* @return array{days:string[], new_users:int[], deposit_amount:string[], bet_amount:string[]}
*/
private function buildSevenDayTrend(?array $scope): array
{
$days = [];
$newUsers = [];
$depositAmounts = [];
$betAmounts = [];
for ($i = 6; $i >= 0; $i--) {
$dayStart = strtotime(date('Y-m-d', strtotime('-' . $i . ' day')));
$dayEnd = $dayStart + 86400 - 1;
$days[] = date('m-d', $dayStart);
$newUsers[] = $this->countUsersInRange($scope, $dayStart, $dayEnd);
$dq = Db::name('deposit_order')
->where('status', 1)
->where('create_time', '>=', $dayStart)
->where('create_time', '<=', $dayEnd);
if ($scope !== null) {
$dq->whereIn('channel_id', $scope);
}
$drow = $dq->fieldRaw('COALESCE(SUM(CAST(amount AS DECIMAL(18,4))),0) AS s')->find();
$dsum = is_array($drow) && isset($drow['s']) ? strval($drow['s']) : '0';
$depositAmounts[] = $this->formatMoney2($dsum);
$bq = Db::name('bet_order')
->whereIn('status', [1, 2])
->where('create_time', '>=', $dayStart)
->where('create_time', '<=', $dayEnd);
if ($scope !== null) {
$bq->whereIn('channel_id', $scope);
}
$brow = $bq->fieldRaw('COALESCE(SUM(CAST(total_amount AS DECIMAL(18,4))),0) AS s')->find();
$bsum = is_array($brow) && isset($brow['s']) ? strval($brow['s']) : '0';
$betAmounts[] = $this->formatMoney2($bsum);
}
return [
'days' => $days,
'new_users' => $newUsers,
'deposit_amount' => $depositAmounts,
'bet_amount' => $betAmounts,
];
}
/**
* 用户按渠道分布(取前 8 名,其余合并为「其他」)。
*
* @param int[]|null $scope
* @return list<array{name:string, value:int}>
*/
private function buildChannelShare(?array $scope): array
{
$q = Db::name('user')->fieldRaw('channel_id, COUNT(*) AS c')->group('channel_id');
if ($scope !== null) {
$q->whereIn('channel_id', $scope);
}
$rows = $q->orderRaw('c DESC')->select()->toArray();
if ($rows === []) {
return [];
}
$channelNames = Db::name('channel')->column('name', 'id');
$list = [];
foreach ($rows as $row) {
$cid = $row['channel_id'];
$cnt = intval($row['c'] ?? 0);
if ($cid === null || $cid === '' || intval(strval($cid)) === 0) {
$name = '未分配渠道';
} else {
$id = intval(strval($cid));
$name = $channelNames[$id] ?? ('#' . strval($id));
}
$list[] = ['name' => $name, 'value' => $cnt];
}
if (count($list) <= 8) {
return $list;
}
$head = array_slice($list, 0, 8);
$rest = array_slice($list, 8);
$other = 0;
foreach ($rest as $item) {
$other += $item['value'];
}
if ($other > 0) {
$head[] = ['name' => '其他', 'value' => $other];
}
return $head;
}
/**
* 成功充值金额按订单归属渠道汇总status=1受渠道范围限制
*
* @param int[]|null $scope
* @return list<array{name:string, value:string}> value 为两位小数字符串,供前端饼图展示
*/
private function buildDepositAmountChannelShare(?array $scope): array
{
$q = Db::name('deposit_order')
->where('status', 1)
->fieldRaw('channel_id, COALESCE(SUM(CAST(amount AS DECIMAL(18,4))),0) AS s')
->group('channel_id');
if ($scope !== null) {
$q->whereIn('channel_id', $scope);
}
$rows = $q->select()->toArray();
if ($rows === []) {
return [];
}
$channelNames = Db::name('channel')->column('name', 'id');
$list = [];
foreach ($rows as $row) {
$cid = $row['channel_id'];
$sumRaw = isset($row['s']) ? strval($row['s']) : '0';
$amountStr = $this->formatMoney2($sumRaw);
if (bccomp($amountStr, '0', 2) <= 0) {
continue;
}
if ($cid === null || $cid === '' || intval(strval($cid)) === 0) {
$name = '未分配渠道';
} else {
$id = intval(strval($cid));
$name = $channelNames[$id] ?? ('#' . strval($id));
}
$list[] = [
'name' => $name,
'value' => $amountStr,
];
}
usort($list, static function (array $a, array $b): int {
return bccomp($b['value'], $a['value'], 2);
});
if (count($list) <= 8) {
return $list;
}
$head = array_slice($list, 0, 8);
$rest = array_slice($list, 8);
$other = '0';
foreach ($rest as $item) {
$other = bcadd($other, $item['value'], 4);
}
$otherFormatted = $this->formatMoney2($other);
if (bccomp($otherFormatted, '0', 2) > 0) {
$head[] = [
'name' => '其他',
'value' => $otherFormatted,
];
}
return $head;
}
/**
* @param int[]|null $scope
* @return list<array{id:int, username:string, create_time:int, channel_name:string, head_image:string}>
*/
private function fetchRecentUsers(?array $scope, int $limit): array
{
$q = Db::name('user')
->field(['id', 'username', 'create_time', 'channel_id', 'head_image'])
->order('id', 'desc')
->limit($limit);
if ($scope !== null) {
$q->whereIn('channel_id', $scope);
}
$rows = $q->select()->toArray();
if ($rows === []) {
return [];
}
$channelNames = Db::name('channel')->column('name', 'id');
$out = [];
foreach ($rows as $row) {
$cid = $row['channel_id'] ?? null;
if ($cid === null || $cid === '' || intval(strval($cid)) === 0) {
$cname = '';
} else {
$id = intval(strval($cid));
$cname = $channelNames[$id] ?? '';
}
$out[] = [
'id' => intval($row['id'] ?? 0),
'username' => strval($row['username'] ?? ''),
'create_time' => intval($row['create_time'] ?? 0),
'channel_name' => $cname,
'head_image' => strval($row['head_image'] ?? ''),
];
}
return $out;
}
private function formatMoney2(string $amount): string
{
if (!is_numeric($amount)) {
return '0.00';
}
$normalized = bcadd($amount, '0', 4);
return bcadd($normalized, '0', 2);
}
/**
* 非超管:按管理员所属渠道过滤;未绑定渠道时按 channel_id IN (0) 与列表页一致。
* 超管:返回 null表示 SQL 不加渠道条件。
*
* @return int[]|null
*/
private function channelScopeOrNull(): ?array
{
if (!$this->auth || $this->auth->isSuperAdmin()) {
return null;
}
$admin = Db::name('admin')->field(['id', 'channel_id'])->where('id', $this->auth->id)->find();
$ids = [];
if ($admin && !empty($admin['channel_id'])) {
$ids[] = $admin['channel_id'];
}
return $ids !== [] ? array_values(array_unique($ids)) : [0];
}
}

View File

@@ -2,6 +2,53 @@ import createAxios from '/@/utils/axios'
export const url = '/admin/Dashboard/'
export interface DashboardTrend {
days: string[]
new_users: number[]
deposit_amount: string[]
bet_amount: string[]
}
export interface DashboardChannelShareItem {
name: string
value: number
}
/** 成功充值金额按渠道value 为两位小数字符串) */
export interface DashboardDepositAmountChannelItem {
name: string
value: string
}
export interface DashboardRecentUser {
id: number
username: string
create_time: number
channel_name: string
head_image: string
}
export interface DashboardStats {
user_total: number
user_new_today: number
user_new_yesterday: number
user_new_growth_pct: number | null
deposit_today_amount: string
deposit_today_count: number
withdraw_pending: number
bet_today_amount: string
bet_today_count: number
}
export interface DashboardPayload {
remark: string
stats: DashboardStats
trend: DashboardTrend
channel_share: DashboardChannelShareItem[]
deposit_amount_channel_share: DashboardDepositAmountChannelItem[]
recent_users: DashboardRecentUser[]
}
export function index() {
return createAxios({
url: url + 'index',

View File

@@ -36,4 +36,27 @@ export default {
second: 'Second',
day: 'Day',
'Number of attachments Uploaded': 'Number of attachments upload',
stat_user_total: 'Total users',
stat_new_today: 'New users today',
stat_deposit_today: 'Deposits today (success)',
stat_withdraw_pending: 'Withdrawals pending review',
stat_hint_pending: 'Pending',
chart_new_user_deposit: 'Last 7 days: new users & daily deposits',
chart_bet_7d: 'Last 7 days: bet amount',
chart_channel_users: 'Users by channel',
chart_deposit_status: 'Deposit order status',
chart_deposit_amount_channel: 'Deposit amount by channel',
recent_users: 'Recent sign-ups',
no_data: 'No data',
bet_today_line: "Today's bet volume",
orders_unit: ' orders',
deposit_orders_today: '{n} successful today',
series_new_users: 'New users',
series_deposit_amount: 'Deposit amount',
series_bet_amount: 'Bet amount',
load_failed: 'Failed to load statistics. Please try again later.',
seconds_ago: 's ago',
minutes_ago: 'm ago',
hours_ago: 'h ago',
}

View File

@@ -36,4 +36,27 @@ export default {
second: '秒',
day: '天',
'Number of attachments Uploaded': '附件上传量',
stat_user_total: '会员总数',
stat_new_today: '今日新增用户',
stat_deposit_today: '今日充值(成功)',
stat_withdraw_pending: '待审核提现',
stat_hint_pending: '待处理',
chart_new_user_deposit: '近7日新增用户与每日充值',
chart_bet_7d: '近7日投注金额',
chart_channel_users: '用户渠道分布',
chart_deposit_status: '充值订单状态分布',
chart_deposit_amount_channel: '充值金额渠道分布',
recent_users: '最新注册用户',
no_data: '暂无数据',
bet_today_line: '今日投注流水',
orders_unit: '笔',
deposit_orders_today: '今日成功 {n} 笔',
series_new_users: '新增用户',
series_deposit_amount: '充值金额',
series_bet_amount: '投注金额',
load_failed: '统计数据加载失败,请稍后重试',
seconds_ago: '秒前',
minutes_ago: '分钟前',
hours_ago: '小时前',
}

View File

@@ -1,126 +1,84 @@
<template>
<div class="default-main">
<div class="banner">
<el-row :gutter="10">
<el-col :md="24" :lg="18">
<div class="welcome suspension">
<img class="welcome-img" :src="headerSvg" alt="" />
<div class="welcome-text">
<div class="welcome-title">{{ adminInfo.nickname + t('utils.comma') + getGreet() }}</div>
<div class="welcome-note">{{ state.remark }}</div>
</div>
</div>
</el-col>
<el-col :lg="6" class="hidden-md-and-down">
<div class="working">
<img class="working-coffee" :src="coffeeSvg" alt="" />
<div class="working-text">
{{ t('dashboard.You have worked today') }}<span class="time">{{ state.workingTimeFormat }}</span>
</div>
<div @click="onChangeWorkState()" class="working-opt working-rest">
{{ state.pauseWork ? t('dashboard.Continue to work') : t('dashboard.have a bit of rest') }}
</div>
</div>
</el-col>
</el-row>
</div>
<div class="default-main dashboard-page" v-loading="loading">
<div class="small-panel-box">
<el-row :gutter="20">
<el-col :sm="12" :lg="6">
<div class="small-panel user-reg suspension">
<div class="small-panel-title">{{ t('dashboard.Member registration') }}</div>
<div class="small-panel-title">{{ t('dashboard.stat_user_total') }}</div>
<div class="small-panel-content">
<div class="content-left">
<Icon color="#8595F4" size="20" name="fa fa-line-chart" />
<el-statistic :value="userRegNumberOutput" :value-style="statisticValueStyle" />
<Icon color="#8595F4" size="20" name="fa fa-users" />
<el-statistic :value="stats.user_total" :value-style="statisticValueStyle" />
</div>
<div class="content-right">+14%</div>
</div>
</div>
</el-col>
<el-col :sm="12" :lg="6">
<div class="small-panel file suspension">
<div class="small-panel-title">{{ t('dashboard.Number of attachments Uploaded') }}</div>
<div class="small-panel-title">{{ t('dashboard.stat_new_today') }}</div>
<div class="small-panel-content">
<div class="content-left">
<Icon color="#AD85F4" size="20" name="fa fa-file-text" />
<el-statistic :value="fileNumberOutput" :value-style="statisticValueStyle" />
<Icon color="#AD85F4" size="20" name="fa fa-user-plus" />
<el-statistic :value="stats.user_new_today" :value-style="statisticValueStyle" />
</div>
<div class="content-right">+50%</div>
<div class="content-right" :class="growthClass">{{ growthText }}</div>
</div>
</div>
</el-col>
<el-col :sm="12" :lg="6">
<div class="small-panel users suspension">
<div class="small-panel-title">{{ t('dashboard.Total number of members') }}</div>
<div class="small-panel-title">{{ t('dashboard.stat_deposit_today') }}</div>
<div class="small-panel-content">
<div class="content-left">
<Icon color="#74A8B5" size="20" name="fa fa-users" />
<el-statistic :value="usersNumberOutput" :value-style="statisticValueStyle" />
<Icon color="#74A8B5" size="20" name="fa fa-credit-card" />
<el-statistic :value="depositTodayDisplay" :value-style="statisticValueStyle" />
</div>
<div class="content-right">+28%</div>
<div class="content-right color-success">{{ depositCountText }}</div>
</div>
</div>
</el-col>
<el-col :sm="12" :lg="6">
<div class="small-panel addons suspension">
<div class="small-panel-title">{{ t('dashboard.Number of installed plug-ins') }}</div>
<div class="small-panel-title">{{ t('dashboard.stat_withdraw_pending') }}</div>
<div class="small-panel-content">
<div class="content-left">
<Icon color="#F48595" size="20" name="fa fa-object-group" />
<el-statistic :value="addonsNumberOutput" :value-style="statisticValueStyle" />
<Icon color="#F48595" size="20" name="fa fa-clock-o" />
<el-statistic :value="stats.withdraw_pending" :value-style="statisticValueStyle" />
</div>
<div class="content-right">+88%</div>
<div class="content-right color-warning">{{ t('dashboard.stat_hint_pending') }}</div>
</div>
</div>
</el-col>
</el-row>
</div>
<div class="growth-chart">
<el-row :gutter="20">
<el-col class="lg-mb-20" :xs="24" :sm="24" :md="12" :lg="9">
<el-card shadow="hover" :header="t('dashboard.Membership growth')">
<el-card shadow="hover" :header="t('dashboard.chart_new_user_deposit')">
<div class="user-growth-chart" :ref="chartRefs.set"></div>
</el-card>
</el-col>
<el-col class="lg-mb-20" :xs="24" :sm="24" :md="12" :lg="9">
<el-card shadow="hover" :header="t('dashboard.Annex growth')">
<el-card shadow="hover" :header="t('dashboard.chart_bet_7d')">
<div class="file-growth-chart" :ref="chartRefs.set"></div>
</el-card>
</el-col>
<el-col :xs="24" :sm="24" :md="24" :lg="6">
<el-card class="new-user-card" shadow="hover" :header="t('dashboard.New member')">
<el-card class="new-user-card" shadow="hover" :header="t('dashboard.recent_users')">
<div class="new-user-growth">
<el-scrollbar>
<div class="new-user-item">
<img class="new-user-avatar" src="~assets/login-header.png" alt="" />
<el-empty v-if="recentUsers.length === 0" :description="t('dashboard.no_data')" />
<el-scrollbar v-else>
<div v-for="u in recentUsers" :key="u.id" class="new-user-item">
<img
class="new-user-avatar"
:src="u.head_image ? fullUrl(u.head_image) : fullUrl('/static/images/avatar.png')"
alt=""
/>
<div class="new-user-base">
<div class="new-user-name">妙码生花</div>
<div class="new-user-time">12分钟前{{ t('dashboard.Joined us') }}</div>
</div>
<Icon class="new-user-arrow" color="#8595F4" name="fa fa-angle-right" />
</div>
<div class="new-user-item">
<img class="new-user-avatar" src="~assets/login-header.png" alt="" />
<div class="new-user-base">
<div class="new-user-name">码上生花</div>
<div class="new-user-time">12分钟前{{ t('dashboard.Joined us') }}</div>
</div>
<Icon class="new-user-arrow" color="#8595F4" name="fa fa-angle-right" />
</div>
<div class="new-user-item">
<img class="new-user-avatar" src="~assets/login-header.png" alt="" />
<div class="new-user-base">
<div class="new-user-name">Admin</div>
<div class="new-user-time">12分钟前{{ t('dashboard.Joined us') }}</div>
</div>
<Icon class="new-user-arrow" color="#8595F4" name="fa fa-angle-right" />
</div>
<div class="new-user-item">
<img class="new-user-avatar" :src="fullUrl('/static/images/avatar.png')" alt="" />
<div class="new-user-base">
<div class="new-user-name">纯属虚构</div>
<div class="new-user-time">12分钟前{{ t('dashboard.Joined us') }}</div>
<div class="new-user-name">{{ u.username || '-' }}</div>
<div class="new-user-time">{{ formatUserTime(u.create_time) }}</div>
<div v-if="u.channel_name" class="new-user-channel">{{ u.channel_name }}</div>
</div>
<Icon class="new-user-arrow" color="#8595F4" name="fa fa-angle-right" />
</div>
@@ -134,12 +92,12 @@
<div class="growth-chart">
<el-row :gutter="20">
<el-col class="lg-mb-20" :xs="24" :sm="24" :md="24" :lg="12">
<el-card shadow="hover" :header="t('dashboard.Member source')">
<el-card shadow="hover" :header="t('dashboard.chart_channel_users')">
<div class="user-source-chart" :ref="chartRefs.set"></div>
</el-card>
</el-col>
<el-col class="lg-mb-20" :xs="24" :sm="24" :md="24" :lg="12">
<el-card shadow="hover" :header="t('dashboard.Member last name')">
<el-card shadow="hover" :header="t('dashboard.chart_deposit_amount_channel')">
<div class="user-surname-chart" :ref="chartRefs.set"></div>
</el-card>
</el-col>
@@ -149,350 +107,235 @@
</template>
<script setup lang="ts">
import { useEventListener, useTemplateRefsList, useTransition } from '@vueuse/core'
import { useEventListener, useTemplateRefsList } from '@vueuse/core'
import * as echarts from 'echarts'
import { CSSProperties, nextTick, onActivated, onBeforeMount, onMounted, onUnmounted, reactive, toRefs, watch } from 'vue'
import type { EChartsType } from 'echarts/core'
import { CSSProperties, computed, nextTick, onActivated, onMounted, onUnmounted, reactive, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { index } from '/@/api/backend/dashboard'
import coffeeSvg from '/@/assets/dashboard/coffee.svg'
import headerSvg from '/@/assets/dashboard/header-1.svg'
import { useAdminInfo } from '/@/stores/adminInfo'
import { WORKING_TIME } from '/@/stores/constant/cacheKey'
import type { DashboardDepositAmountChannelItem, DashboardPayload, DashboardRecentUser } from '/@/api/backend/dashboard'
import { useNavTabs } from '/@/stores/navTabs'
import { fullUrl, getGreet } from '/@/utils/common'
import { Local } from '/@/utils/storage'
let workTimer: number
import { fullUrl } from '/@/utils/common'
defineOptions({
name: 'dashboard',
})
const d = new Date()
const { t } = useI18n()
const navTabs = useNavTabs()
const adminInfo = useAdminInfo()
const chartRefs = useTemplateRefsList<HTMLDivElement>()
const loading = ref(true)
const state: {
charts: any[]
remark: string
workingTimeFormat: string
pauseWork: boolean
charts: EChartsType[]
} = reactive({
charts: [],
remark: 'dashboard.Loading',
workingTimeFormat: '',
pauseWork: false,
})
/**
* 带有数字向上变化特效的数据
*/
const countUp = reactive({
userRegNumber: 0,
fileNumber: 0,
usersNumber: 0,
addonsNumber: 0,
const stats = reactive({
user_total: 0,
user_new_today: 0,
user_new_yesterday: 0,
user_new_growth_pct: null as number | null,
deposit_today_amount: '0.00',
deposit_today_count: 0,
withdraw_pending: 0,
bet_today_amount: '0.00',
bet_today_count: 0,
})
const trend = reactive({
days: [] as string[],
new_users: [] as number[],
deposit_amount: [] as string[],
bet_amount: [] as string[],
})
const channelShare = ref<{ name: string; value: number }[]>([])
const depositAmountChannelShare = ref<DashboardDepositAmountChannelItem[]>([])
const recentUsers = ref<DashboardRecentUser[]>([])
const depositTodayDisplay = computed(() => {
const n = parseFloat(stats.deposit_today_amount)
if (!Number.isFinite(n)) {
return stats.deposit_today_amount
}
return n.toFixed(2)
})
const growthText = computed(() => {
const p = stats.user_new_growth_pct
if (p === null || p === undefined) {
return '—'
}
const sign = p > 0 ? '+' : ''
return sign + p.toFixed(1) + '%'
})
const growthClass = computed(() => {
const p = stats.user_new_growth_pct
if (p === null || p === undefined) {
return 'color-info'
}
if (p > 0) {
return 'color-success'
}
if (p < 0) {
return 'color-danger'
}
return 'color-info'
})
const depositCountText = computed(() => {
return t('dashboard.deposit_orders_today', { n: stats.deposit_today_count })
})
const countUpRefs = toRefs(countUp)
const userRegNumberOutput = useTransition(countUpRefs.userRegNumber, { duration: 1500 })
const fileNumberOutput = useTransition(countUpRefs.fileNumber, { duration: 1500 })
const usersNumberOutput = useTransition(countUpRefs.usersNumber, { duration: 1500 })
const addonsNumberOutput = useTransition(countUpRefs.addonsNumber, { duration: 1500 })
const statisticValueStyle: CSSProperties = {
fontSize: '28px',
}
index().then((res) => {
state.remark = res.data.remark
})
const initCountUp = () => {
// 虚拟数据
countUpRefs.userRegNumber.value = 5456
countUpRefs.fileNumber.value = 1234
countUpRefs.usersNumber.value = 9486
countUpRefs.addonsNumber.value = 875
const disposeCharts = () => {
for (const c of state.charts) {
c.dispose()
}
state.charts = []
}
const initUserGrowthChart = () => {
const userGrowthChart = echarts.init(chartRefs.value[0] as HTMLElement)
const option = {
grid: {
top: 40,
right: 0,
bottom: 20,
left: 40,
},
xAxis: {
data: [
t('dashboard.Monday'),
t('dashboard.Tuesday'),
t('dashboard.Wednesday'),
t('dashboard.Thursday'),
t('dashboard.Friday'),
t('dashboard.Saturday'),
t('dashboard.Sunday'),
],
},
yAxis: {},
const parseMoneySeries = (arr: string[]) => arr.map((s) => parseFloat(String(s)) || 0)
const initTrendDepositChart = () => {
const el = chartRefs.value[0] as HTMLElement
if (!el) {
return
}
const chart = echarts.init(el)
chart.setOption({
color: ['#8595F4', '#67C23A'],
grid: { top: 48, right: 56, bottom: 24, left: 48 },
tooltip: { trigger: 'axis' },
legend: {
data: [t('dashboard.Visits'), t('dashboard.Registration volume')],
textStyle: {
color: '#73767a',
},
data: [t('dashboard.series_new_users'), t('dashboard.series_deposit_amount')],
textStyle: { color: '#73767a' },
top: 0,
},
series: [
xAxis: { type: 'category', data: trend.days, boundaryGap: false },
yAxis: [
{ type: 'value', name: t('dashboard.series_new_users'), splitLine: { lineStyle: { type: 'dashed' } } },
{
name: t('dashboard.Visits'),
data: [100, 160, 280, 230, 190, 200, 480],
type: 'line',
smooth: true,
areaStyle: {
color: '#8595F4',
},
},
{
name: t('dashboard.Registration volume'),
data: [45, 180, 146, 99, 210, 127, 288],
type: 'line',
smooth: true,
areaStyle: {
color: '#F48595',
opacity: 0.5,
},
type: 'value',
name: t('dashboard.series_deposit_amount'),
position: 'right',
splitLine: { show: false },
},
],
}
userGrowthChart.setOption(option)
state.charts.push(userGrowthChart)
}
const initFileGrowthChart = () => {
const fileGrowthChart = echarts.init(chartRefs.value[1] as HTMLElement)
const option = {
grid: {
top: 30,
right: 0,
bottom: 20,
left: 0,
},
tooltip: {
trigger: 'item',
},
legend: {
type: 'scroll',
bottom: 0,
data: (function () {
var list = []
for (var i = 1; i <= 28; i++) {
list.push(i + 2000 + '')
}
return list
})(),
textStyle: {
color: '#73767a',
},
},
visualMap: {
top: 'middle',
right: 10,
color: ['red', 'yellow'],
calculable: true,
},
radar: {
indicator: [
{ name: t('dashboard.picture') },
{ name: t('dashboard.file') },
{ name: t('dashboard.table') },
{ name: t('dashboard.Compressed package') },
{ name: t('dashboard.other') },
],
},
series: (function () {
var series = []
for (var i = 1; i <= 28; i++) {
series.push({
type: 'radar',
symbol: 'none',
lineStyle: {
width: 1,
},
emphasis: {
areaStyle: {
color: 'rgba(0,250,0,0.3)',
},
},
data: [
{
value: [(40 - i) * 10, (38 - i) * 4 + 60, i * 5 + 10, i * 9, (i * i) / 2],
name: i + 2000 + '',
},
],
})
}
return series
})(),
}
fileGrowthChart.setOption(option)
state.charts.push(fileGrowthChart)
}
const initUserSourceChart = () => {
const UserSourceChart = echarts.init(chartRefs.value[2] as HTMLElement)
const pathSymbols = {
reindeer:
'path://M-22.788,24.521c2.08-0.986,3.611-3.905,4.984-5.892 c-2.686,2.782-5.047,5.884-9.102,7.312c-0.992,0.005-0.25-2.016,0.34-2.362l1.852-0.41c0.564-0.218,0.785-0.842,0.902-1.347 c2.133-0.727,4.91-4.129,6.031-6.194c1.748-0.7,4.443-0.679,5.734-2.293c1.176-1.468,0.393-3.992,1.215-6.557 c0.24-0.754,0.574-1.581,1.008-2.293c-0.611,0.011-1.348-0.061-1.959-0.608c-1.391-1.245-0.785-2.086-1.297-3.313 c1.684,0.744,2.5,2.584,4.426,2.586C-8.46,3.012-8.255,2.901-8.04,2.824c6.031-1.952,15.182-0.165,19.498-3.937 c1.15-3.933-1.24-9.846-1.229-9.938c0.008-0.062-1.314-0.004-1.803-0.258c-1.119-0.771-6.531-3.75-0.17-3.33 c0.314-0.045,0.943,0.259,1.439,0.435c-0.289-1.694-0.92-0.144-3.311-1.946c0,0-1.1-0.855-1.764-1.98 c-0.836-1.09-2.01-2.825-2.992-4.031c-1.523-2.476,1.367,0.709,1.816,1.108c1.768,1.704,1.844,3.281,3.232,3.983 c0.195,0.203,1.453,0.164,0.926-0.468c-0.525-0.632-1.367-1.278-1.775-2.341c-0.293-0.703-1.311-2.326-1.566-2.711 c-0.256-0.384-0.959-1.718-1.67-2.351c-1.047-1.187-0.268-0.902,0.521-0.07c0.789,0.834,1.537,1.821,1.672,2.023 c0.135,0.203,1.584,2.521,1.725,2.387c0.102-0.259-0.035-0.428-0.158-0.852c-0.125-0.423-0.912-2.032-0.961-2.083 c-0.357-0.852-0.566-1.908-0.598-3.333c0.4-2.375,0.648-2.486,0.549-0.705c0.014,1.143,0.031,2.215,0.602,3.247 c0.807,1.496,1.764,4.064,1.836,4.474c0.561,3.176,2.904,1.749,2.281-0.126c-0.068-0.446-0.109-2.014-0.287-2.862 c-0.18-0.849-0.219-1.688-0.113-3.056c0.066-1.389,0.232-2.055,0.277-2.299c0.285-1.023,0.4-1.088,0.408,0.135 c-0.059,0.399-0.131,1.687-0.125,2.655c0.064,0.642-0.043,1.768,0.172,2.486c0.654,1.928-0.027,3.496,1,3.514 c1.805-0.424,2.428-1.218,2.428-2.346c-0.086-0.704-0.121-0.843-0.031-1.193c0.221-0.568,0.359-0.67,0.312-0.076 c-0.055,0.287,0.031,0.533,0.082,0.794c0.264,1.197,0.912,0.114,1.283-0.782c0.15-0.238,0.539-2.154,0.545-2.522 c-0.023-0.617,0.285-0.645,0.309,0.01c0.064,0.422-0.248,2.646-0.205,2.334c-0.338,1.24-1.105,3.402-3.379,4.712 c-0.389,0.12-1.186,1.286-3.328,2.178c0,0,1.729,0.321,3.156,0.246c1.102-0.19,3.707-0.027,4.654,0.269 c1.752,0.494,1.531-0.053,4.084,0.164c2.26-0.4,2.154,2.391-1.496,3.68c-2.549,1.405-3.107,1.475-2.293,2.984 c3.484,7.906,2.865,13.183,2.193,16.466c2.41,0.271,5.732-0.62,7.301,0.725c0.506,0.333,0.648,1.866-0.457,2.86 c-4.105,2.745-9.283,7.022-13.904,7.662c-0.977-0.194,0.156-2.025,0.803-2.247l1.898-0.03c0.596-0.101,0.936-0.669,1.152-1.139 c3.16-0.404,5.045-3.775,8.246-4.818c-4.035-0.718-9.588,3.981-12.162,1.051c-5.043,1.423-11.449,1.84-15.895,1.111 c-3.105,2.687-7.934,4.021-12.115,5.866c-3.271,3.511-5.188,8.086-9.967,10.414c-0.986,0.119-0.48-1.974,0.066-2.385l1.795-0.618 C-22.995,25.682-22.849,25.035-22.788,24.521z',
plane: 'path://M1.112,32.559l2.998,1.205l-2.882,2.268l-2.215-0.012L1.112,32.559z M37.803,23.96 c0.158-0.838,0.5-1.509,0.961-1.904c-0.096-0.037-0.205-0.071-0.344-0.071c-0.777-0.005-2.068-0.009-3.047-0.009 c-0.633,0-1.217,0.066-1.754,0.18l2.199,1.804H37.803z M39.738,23.036c-0.111,0-0.377,0.325-0.537,0.924h1.076 C40.115,23.361,39.854,23.036,39.738,23.036z M39.934,39.867c-0.166,0-0.674,0.705-0.674,1.986s0.506,1.986,0.674,1.986 s0.672-0.705,0.672-1.986S40.102,39.867,39.934,39.867z M38.963,38.889c-0.098-0.038-0.209-0.07-0.348-0.073 c-0.082,0-0.174,0-0.268-0.001l-7.127,4.671c0.879,0.821,2.42,1.417,4.348,1.417c0.979,0,2.27-0.006,3.047-0.01 c0.139,0,0.25-0.034,0.348-0.072c-0.646-0.555-1.07-1.643-1.07-2.967C37.891,40.529,38.316,39.441,38.963,38.889z M32.713,23.96 l-12.37-10.116l-4.693-0.004c0,0,4,8.222,4.827,10.121H32.713z M59.311,32.374c-0.248,2.104-5.305,3.172-8.018,3.172H39.629 l-25.325,16.61L9.607,52.16c0,0,6.687-8.479,7.95-10.207c1.17-1.6,3.019-3.699,3.027-6.407h-2.138 c-5.839,0-13.816-3.789-18.472-5.583c-2.818-1.085-2.396-4.04-0.031-4.04h0.039l-3.299-11.371h3.617c0,0,4.352,5.696,5.846,7.5 c2,2.416,4.503,3.678,8.228,3.87h30.727c2.17,0,4.311,0.417,6.252,1.046c3.49,1.175,5.863,2.7,7.199,4.027 C59.145,31.584,59.352,32.025,59.311,32.374z M22.069,30.408c0-0.815-0.661-1.475-1.469-1.475c-0.812,0-1.471,0.66-1.471,1.475 s0.658,1.475,1.471,1.475C21.408,31.883,22.069,31.224,22.069,30.408z M27.06,30.408c0-0.815-0.656-1.478-1.466-1.478 c-0.812,0-1.471,0.662-1.471,1.478s0.658,1.477,1.471,1.477C26.404,31.885,27.06,31.224,27.06,30.408z M32.055,30.408 c0-0.815-0.66-1.475-1.469-1.475c-0.808,0-1.466,0.66-1.466,1.475s0.658,1.475,1.466,1.475 C31.398,31.883,32.055,31.224,32.055,30.408z M37.049,30.408c0-0.815-0.658-1.478-1.467-1.478c-0.812,0-1.469,0.662-1.469,1.478 s0.656,1.477,1.469,1.477C36.389,31.885,37.049,31.224,37.049,30.408z M42.039,30.408c0-0.815-0.656-1.478-1.465-1.478 c-0.811,0-1.469,0.662-1.469,1.478s0.658,1.477,1.469,1.477C41.383,31.885,42.039,31.224,42.039,30.408z M55.479,30.565 c-0.701-0.436-1.568-0.896-2.627-1.347c-0.613,0.289-1.551,0.476-2.73,0.476c-1.527,0-1.639,2.263,0.164,2.316 C52.389,32.074,54.627,31.373,55.479,30.565z',
rocket: 'path://M-244.396,44.399c0,0,0.47-2.931-2.427-6.512c2.819-8.221,3.21-15.709,3.21-15.709s5.795,1.383,5.795,7.325C-237.818,39.679-244.396,44.399-244.396,44.399z M-260.371,40.827c0,0-3.881-12.946-3.881-18.319c0-2.416,0.262-4.566,0.669-6.517h17.684c0.411,1.952,0.675,4.104,0.675,6.519c0,5.291-3.87,18.317-3.87,18.317H-260.371z M-254.745,18.951c-1.99,0-3.603,1.676-3.603,3.744c0,2.068,1.612,3.744,3.603,3.744c1.988,0,3.602-1.676,3.602-3.744S-252.757,18.951-254.745,18.951z M-255.521,2.228v-5.098h1.402v4.969c1.603,1.213,5.941,5.069,7.901,12.5h-17.05C-261.373,7.373-257.245,3.558-255.521,2.228zM-265.07,44.399c0,0-6.577-4.721-6.577-14.896c0-5.942,5.794-7.325,5.794-7.325s0.393,7.488,3.211,15.708C-265.539,41.469-265.07,44.399-265.07,44.399z M-252.36,45.15l-1.176-1.22L-254.789,48l-1.487-4.069l-1.019,2.116l-1.488-3.826h8.067L-252.36,45.15z',
train: 'path://M67.335,33.596L67.335,33.596c-0.002-1.39-1.153-3.183-3.328-4.218h-9.096v-2.07h5.371 c-4.939-2.07-11.199-4.141-14.89-4.141H19.72v12.421v5.176h38.373c4.033,0,8.457-1.035,9.142-5.176h-0.027 c0.076-0.367,0.129-0.751,0.129-1.165L67.335,33.596L67.335,33.596z M27.999,30.413h-3.105v-4.141h3.105V30.413z M35.245,30.413 h-3.104v-4.141h3.104V30.413z M42.491,30.413h-3.104v-4.141h3.104V30.413z M49.736,30.413h-3.104v-4.141h3.104V30.413z M14.544,40.764c1.143,0,2.07-0.927,2.07-2.07V35.59V25.237c0-1.145-0.928-2.07-2.07-2.07H-9.265c-1.143,0-2.068,0.926-2.068,2.07 v10.351v3.105c0,1.144,0.926,2.07,2.068,2.07H14.544L14.544,40.764z M8.333,26.272h3.105v4.141H8.333V26.272z M1.087,26.272h3.105 v4.141H1.087V26.272z M-6.159,26.272h3.105v4.141h-3.105V26.272z M-9.265,41.798h69.352v1.035H-9.265V41.798z',
}
const option = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'none',
},
formatter: function (params: any) {
return params[0].name + ': ' + params[0].value
},
},
xAxis: {
data: [t('dashboard.Baidu'), t('dashboard.Direct access'), t('dashboard.take a plane'), t('dashboard.Take the high-speed railway')],
axisTick: { show: false },
axisLine: { show: false },
axisLabel: {
color: '#e54035',
},
},
yAxis: {
splitLine: { show: false },
axisTick: { show: false },
axisLine: { show: false },
axisLabel: { show: false },
},
color: ['#e54035'],
series: [
{
name: 'hill',
type: 'pictorialBar',
barCategoryGap: '-130%',
symbol: 'path://M0,10 L10,10 C5.5,10 5.5,5 5,0 C4.5,5 4.5,10 0,10 z',
itemStyle: {
opacity: 0.5,
},
emphasis: {
itemStyle: {
opacity: 1,
},
},
data: [123, 60, 25, 80],
z: 10,
name: t('dashboard.series_new_users'),
type: 'line',
smooth: true,
data: trend.new_users,
areaStyle: { color: 'rgba(133,149,244,0.15)' },
},
{
name: 'glyph',
type: 'pictorialBar',
barGap: '-100%',
symbolPosition: 'end',
symbolSize: 50,
symbolOffset: [0, '-120%'],
data: [
{
value: 123,
symbol: pathSymbols.reindeer,
symbolSize: [60, 60],
},
{
value: 60,
symbol: pathSymbols.rocket,
symbolSize: [50, 60],
},
{
value: 25,
symbol: pathSymbols.plane,
symbolSize: [65, 35],
},
{
value: 80,
symbol: pathSymbols.train,
symbolSize: [50, 30],
},
],
name: t('dashboard.series_deposit_amount'),
type: 'line',
smooth: true,
yAxisIndex: 1,
data: parseMoneySeries(trend.deposit_amount),
areaStyle: { color: 'rgba(103,194,58,0.12)' },
},
],
}
UserSourceChart.setOption(option)
state.charts.push(UserSourceChart)
})
state.charts.push(chart)
}
const initUserSurnameChart = () => {
const userSurnameChart = echarts.init(chartRefs.value[3] as HTMLElement)
const data = genData(20)
const option = {
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b} : {c} ({d}%)',
},
legend: {
type: 'scroll',
orient: 'vertical',
right: 10,
top: 20,
bottom: 20,
data: data.legendData,
textStyle: {
color: '#73767a',
},
},
const initBetTrendChart = () => {
const el = chartRefs.value[1] as HTMLElement
if (!el) {
return
}
const chart = echarts.init(el)
chart.setOption({
color: ['#E6A23C'],
grid: { top: 36, right: 16, bottom: 24, left: 48 },
tooltip: { trigger: 'axis' },
xAxis: { type: 'category', data: trend.days },
yAxis: { type: 'value', splitLine: { lineStyle: { type: 'dashed' } } },
series: [
{
name: t('dashboard.series_bet_amount'),
type: 'bar',
data: parseMoneySeries(trend.bet_amount),
itemStyle: { borderRadius: [4, 4, 0, 0] },
},
],
})
state.charts.push(chart)
}
const initChannelPie = () => {
const el = chartRefs.value[2] as HTMLElement
if (!el) {
return
}
const chart = echarts.init(el)
const data = channelShare.value.map((x) => ({ name: x.name, value: x.value }))
chart.setOption({
tooltip: { trigger: 'item' },
legend: { type: 'scroll', bottom: 0, textStyle: { color: '#73767a' } },
series: [
{
name: t('dashboard.full name'),
type: 'pie',
radius: '55%',
center: ['40%', '50%'],
data: data.seriesData,
radius: ['36%', '62%'],
center: ['50%', '46%'],
data,
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)',
},
itemStyle: { shadowBlur: 10, shadowOffsetX: 0, shadowColor: 'rgba(0,0,0,0.15)' },
},
},
],
})
state.charts.push(chart)
}
const initDepositAmountChannelPie = () => {
const el = chartRefs.value[3] as HTMLElement
if (!el) {
return
}
function genData(count: any) {
// prettier-ignore
const nameList = [
'赵', '钱', '孙', '李', '周', '吴', '郑', '王', '冯', '陈', '褚', '卫', '蒋', '沈', '韩', '杨', '朱', '秦', '尤', '许', '何', '吕', '施', '张', '孔', '曹', '严', '华', '金', '魏', '陶', '姜', '戚', '谢', '邹', '喻', '柏', '水', '窦', '章', '云', '苏', '潘', '葛', '奚', '范', '彭', '郎', '鲁', '韦', '昌', '马', '苗', '凤', '花', '方', '俞', '任', '袁', '柳', '酆', '鲍', '史', '唐', '费', '廉', '岑', '薛', '雷', '贺', '倪', '汤', '滕', '殷', '罗', '毕', '郝', '邬', '安', '常', '乐', '于', '时', '傅', '皮', '卞', '齐', '康', '伍', '余', '元', '卜', '顾', '孟', '平', '黄', '和', '穆', '萧', '尹', '姚', '邵', '湛', '汪', '祁', '毛', '禹', '狄', '米', '贝', '明', '臧', '计', '伏', '成', '戴', '谈', '宋', '茅', '庞', '熊', '纪', '舒', '屈', '项', '祝', '董', '梁', '杜', '阮', '蓝', '闵', '席', '季', '麻', '强', '贾', '路', '娄', '危'
];
const legendData = []
const seriesData = []
for (var i = 0; i < count; i++) {
var name = Math.random() > 0.85 ? makeWord(2, 1) + '·' + makeWord(2, 0) : makeWord(2, 1)
legendData.push(name)
seriesData.push({
name: name,
value: Math.round(Math.random() * 100000),
})
}
const chart = echarts.init(el)
const data = depositAmountChannelShare.value.map((x) => {
const v = parseFloat(String(x.value))
return {
legendData: legendData,
seriesData: seriesData,
name: x.name,
value: Number.isFinite(v) ? v : 0,
}
function makeWord(max: any, min: any) {
const nameLen = Math.ceil(Math.random() * max + min)
const name = []
for (var i = 0; i < nameLen; i++) {
name.push(nameList[Math.round(Math.random() * nameList.length - 1)])
}
return name.join('')
}
}
userSurnameChart.setOption(option)
state.charts.push(userSurnameChart)
})
chart.setOption({
tooltip: {
trigger: 'item',
formatter: (p: { name?: string; value?: number; percent?: number }) => {
const name = p.name ?? ''
const val = typeof p.value === 'number' && Number.isFinite(p.value) ? p.value : 0
const pct = typeof p.percent === 'number' && Number.isFinite(p.percent) ? p.percent : 0
return name + '<br/>' + val.toFixed(2) + ' (' + pct.toFixed(1) + '%)'
},
},
legend: { type: 'scroll', bottom: 0, textStyle: { color: '#73767a' } },
series: [
{
type: 'pie',
radius: ['32%', '58%'],
center: ['50%', '46%'],
data,
emphasis: {
itemStyle: { shadowBlur: 10, shadowOffsetX: 0, shadowColor: 'rgba(0,0,0,0.15)' },
},
},
],
})
state.charts.push(chart)
}
const echartsResize = () => {
@@ -503,95 +346,66 @@ const echartsResize = () => {
})
}
const onChangeWorkState = () => {
const time = parseInt((new Date().getTime() / 1000).toString())
const workingTime = Local.get(WORKING_TIME)
if (state.pauseWork) {
// 继续工作
workingTime.pauseTime += time - workingTime.startPauseTime
workingTime.startPauseTime = 0
Local.set(WORKING_TIME, workingTime)
state.pauseWork = false
startWork()
} else {
// 暂停工作
workingTime.startPauseTime = time
Local.set(WORKING_TIME, workingTime)
clearInterval(workTimer)
state.pauseWork = true
const applyPayload = (payload: DashboardPayload) => {
const s = payload.stats
stats.user_total = s.user_total
stats.user_new_today = s.user_new_today
stats.user_new_yesterday = s.user_new_yesterday
stats.user_new_growth_pct = s.user_new_growth_pct
stats.deposit_today_amount = s.deposit_today_amount
stats.deposit_today_count = s.deposit_today_count
stats.withdraw_pending = s.withdraw_pending
stats.bet_today_amount = s.bet_today_amount
stats.bet_today_count = s.bet_today_count
trend.days = payload.trend.days
trend.new_users = payload.trend.new_users
trend.deposit_amount = payload.trend.deposit_amount
trend.bet_amount = payload.trend.bet_amount
channelShare.value = payload.channel_share
depositAmountChannelShare.value = payload.deposit_amount_channel_share ?? []
recentUsers.value = payload.recent_users
}
const loadDashboard = async () => {
loading.value = true
try {
const res = await index()
const payload = res.data as unknown as DashboardPayload
applyPayload(payload)
await nextTick()
disposeCharts()
initTrendDepositChart()
initBetTrendChart()
initChannelPie()
initDepositAmountChannelPie()
echartsResize()
} catch (_e) {
// 统计数据加载失败时由 axios 拦截器提示;此处不重复展示
} finally {
loading.value = false
}
}
const startWork = () => {
const workingTime = Local.get(WORKING_TIME) || { date: '', startTime: 0, pauseTime: 0, startPauseTime: 0 }
const currentDate = d.getFullYear() + '-' + (d.getMonth() + 1) + '-' + d.getDate()
const time = parseInt((new Date().getTime() / 1000).toString())
if (workingTime.date != currentDate) {
workingTime.date = currentDate
workingTime.startTime = time
workingTime.pauseTime = workingTime.startPauseTime = 0
Local.set(WORKING_TIME, workingTime)
function formatUserTime(ts: number): string {
if (!ts || ts <= 0) {
return '—'
}
let startPauseTime = 0
if (workingTime.startPauseTime <= 0) {
state.pauseWork = false
startPauseTime = 0
} else {
state.pauseWork = true
startPauseTime = time - workingTime.startPauseTime // 已暂停时间
const now = Math.floor(Date.now() / 1000)
const diff = now - ts
if (diff < 60) {
return diff + t('dashboard.seconds_ago')
}
let workingSeconds = time - workingTime.startTime - workingTime.pauseTime - startPauseTime
state.workingTimeFormat = formatSeconds(workingSeconds)
if (!state.pauseWork) {
workTimer = window.setInterval(() => {
workingSeconds++
state.workingTimeFormat = formatSeconds(workingSeconds)
}, 1000)
if (diff < 3600) {
return Math.floor(diff / 60) + t('dashboard.minutes_ago')
}
}
const formatSeconds = (seconds: number) => {
var secondTime = 0 // 秒
var minuteTime = 0 // 分
var hourTime = 0 // 小时
var dayTime = 0 // 天
var result = ''
if (seconds < 60) {
secondTime = seconds
} else {
// 获取分钟除以60取整数得到整数分钟
minuteTime = Math.floor(seconds / 60)
// 获取秒数,秒数取佘,得到整数秒数
secondTime = Math.floor(seconds % 60)
// 如果分钟大于60将分钟转换成小时
if (minuteTime >= 60) {
// 获取小时获取分钟除以60得到整数小时
hourTime = Math.floor(minuteTime / 60)
// 获取小时后取佘的分获取分钟除以60取佘的分
minuteTime = Math.floor(minuteTime % 60)
if (hourTime >= 24) {
// 获取天数, 获取小时除以24得到整数天
dayTime = Math.floor(hourTime / 24)
// 获取小时后取余小时获取分钟除以24取余的分
hourTime = Math.floor(hourTime % 24)
}
}
if (diff < 86400) {
return Math.floor(diff / 3600) + t('dashboard.hours_ago')
}
result =
hourTime +
t('dashboard.hour') +
((minuteTime >= 10 ? minuteTime : '0' + minuteTime) + t('dashboard.minute')) +
((secondTime >= 10 ? secondTime : '0' + secondTime) + t('dashboard.second'))
if (dayTime > 0) {
result = dayTime + t('dashboard.day') + result
}
return result
const d = new Date(ts * 1000)
const pad = (n: number) => (n < 10 ? '0' + n : String(n))
return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()) + ' ' + pad(d.getHours()) + ':' + pad(d.getMinutes())
}
onActivated(() => {
@@ -599,23 +413,12 @@ onActivated(() => {
})
onMounted(() => {
startWork()
initCountUp()
initUserGrowthChart()
initFileGrowthChart()
initUserSourceChart()
initUserSurnameChart()
loadDashboard()
useEventListener(window, 'resize', echartsResize)
})
onBeforeMount(() => {
for (const key in state.charts) {
state.charts[key].dispose()
}
})
onUnmounted(() => {
clearInterval(workTimer)
disposeCharts()
})
watch(
@@ -624,89 +427,12 @@ watch(
echartsResize()
}
)
</script>
<style scoped lang="scss">
.welcome {
background: #e1eaf9;
border-radius: 6px;
display: flex;
align-items: center;
padding: 15px 20px !important;
box-shadow: 0 0 30px 0 rgba(82, 63, 105, 0.05);
.welcome-img {
height: 100px;
margin-right: 10px;
user-select: none;
}
.welcome-title {
font-size: 1.5rem;
line-height: 30px;
color: var(--ba-color-primary-light);
}
.welcome-note {
padding-top: 6px;
font-size: 15px;
color: var(--el-text-color-primary);
}
}
.working {
height: 130px;
display: flex;
justify-content: center;
flex-wrap: wrap;
height: 100%;
position: relative;
&:hover {
.working-coffee {
-webkit-transform: translateY(-4px) scale(1.02);
-moz-transform: translateY(-4px) scale(1.02);
-ms-transform: translateY(-4px) scale(1.02);
-o-transform: translateY(-4px) scale(1.02);
transform: translateY(-4px) scale(1.02);
z-index: 999;
}
}
.working-coffee {
transition: all 0.3s ease;
width: 80px;
}
.working-text {
display: block;
width: 100%;
font-size: 15px;
text-align: center;
color: var(--el-text-color-primary);
}
.working-opt {
position: absolute;
top: -40px;
right: 10px;
background-color: rgba($color: #000000, $alpha: 0.3);
padding: 10px 20px;
border-radius: 20px;
color: var(--ba-bg-color-overlay);
transition: all 0.3s ease;
cursor: pointer;
opacity: 0;
z-index: 999;
&:active {
background-color: rgba($color: #000000, $alpha: 0.6);
}
}
&:hover {
.working-opt {
opacity: 1;
top: 0;
}
.working-done {
opacity: 1;
top: 50px;
}
}
}
.small-panel-box {
margin-top: 20px;
margin-top: 0;
}
.small-panel {
background-color: #e9edf2;
@@ -731,8 +457,11 @@ watch(
}
}
.content-right {
font-size: 18px;
font-size: 14px;
margin-left: auto;
text-align: right;
max-width: 48%;
line-height: 1.3;
}
.color-success {
color: var(--el-color-success);
@@ -755,62 +484,62 @@ watch(
.file-growth-chart {
height: 260px;
}
.user-source-chart,
.user-surname-chart {
height: 360px;
}
.new-user-growth {
height: 300px;
}
.user-source-chart,
.user-surname-chart {
height: 400px;
}
.new-user-item {
display: flex;
align-items: center;
padding: 20px;
margin: 10px 15px;
padding: 16px 20px;
margin: 8px 12px;
box-shadow: 0 0 30px 0 rgba(82, 63, 105, 0.05);
background-color: var(--ba-bg-color-overlay);
.new-user-avatar {
height: 48px;
width: 48px;
border-radius: 50%;
object-fit: cover;
}
.new-user-base {
margin-left: 10px;
flex: 1;
min-width: 0;
color: #2c3f5d;
.new-user-name {
font-size: 15px;
font-weight: 500;
}
.new-user-time {
font-size: 13px;
color: var(--el-text-color-secondary);
}
.new-user-channel {
font-size: 12px;
color: var(--el-text-color-placeholder);
margin-top: 2px;
}
}
.new-user-arrow {
margin-left: auto;
flex-shrink: 0;
}
}
.new-user-card :deep(.el-card__body) {
padding: 0;
}
@media screen and (max-width: 425px) {
.welcome-img {
display: none;
}
}
@media screen and (max-width: 1200px) {
.lg-mb-20 {
margin-bottom: 20px;
}
}
html.dark {
.welcome {
background-color: var(--ba-bg-color-overlay);
}
.working-opt {
color: var(--el-text-color-primary);
background-color: var(--ba-border-color);
}
.small-panel {
background-color: var(--ba-bg-color-overlay);
.small-panel-content {