469 lines
16 KiB
Vue
469 lines
16 KiB
Vue
<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="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.Daily new players') }}</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" />
|
||
</div>
|
||
<div class="content-right color-info">{{ t('dashboard.Today') }}</div>
|
||
</div>
|
||
</div>
|
||
</el-col>
|
||
<el-col :sm="12" :lg="6">
|
||
<div class="small-panel file suspension">
|
||
<div class="small-panel-title">{{ t('dashboard.Yesterday points') }}</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" />
|
||
</div>
|
||
<div class="content-right color-info">{{ t('dashboard.Yesterday') }}</div>
|
||
</div>
|
||
</div>
|
||
</el-col>
|
||
<el-col :sm="12" :lg="6">
|
||
<div class="small-panel users suspension">
|
||
<div class="small-panel-title">{{ t('dashboard.Yesterday redeem') }}</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" />
|
||
</div>
|
||
<div class="content-right color-info">{{ t('dashboard.Orders') }}</div>
|
||
</div>
|
||
</div>
|
||
</el-col>
|
||
<el-col :sm="12" :lg="6">
|
||
<div class="small-panel addons suspension">
|
||
<div class="small-panel-title">{{ t('dashboard.Pending physical to ship') }}</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" />
|
||
</div>
|
||
<div class="content-right color-info">{{ t('dashboard.Pending') }}</div>
|
||
</div>
|
||
</div>
|
||
</el-col>
|
||
</el-row>
|
||
</div>
|
||
|
||
<div class="growth-chart">
|
||
<el-row :gutter="20">
|
||
<el-col :xs="24" :sm="24" :md="24" :lg="24">
|
||
<el-card shadow="hover" :header="t('dashboard.Yesterday item redeem stat')">
|
||
<div class="playx-kpis">
|
||
<div class="playx-kpi">
|
||
<div class="playx-kpi-title">{{ t('dashboard.Yesterday redeem points sum') }}</div>
|
||
<div class="playx-kpi-value">{{ state.playx?.yesterday_redeem?.points_cost_sum ?? 0 }}</div>
|
||
</div>
|
||
<div class="playx-kpi">
|
||
<div class="playx-kpi-title">{{ t('dashboard.Yesterday redeem amount sum') }}</div>
|
||
<div class="playx-kpi-value">{{ state.playx?.yesterday_redeem?.amount_sum ?? 0 }}</div>
|
||
</div>
|
||
<div class="playx-kpi">
|
||
<div class="playx-kpi-title">{{ t('dashboard.Grant failed retryable') }}</div>
|
||
<div class="playx-kpi-value">{{ state.playx?.grant_failed_retryable ?? 0 }}</div>
|
||
</div>
|
||
</div>
|
||
|
||
<el-table
|
||
v-loading="state.playxLoading"
|
||
:data="state.playx?.yesterday_redeem?.by_item ?? []"
|
||
size="small"
|
||
style="width: 100%; margin-top: 12px"
|
||
>
|
||
<el-table-column prop="mall_item_id" :label="t('dashboard.Item ID')" width="100" />
|
||
<el-table-column prop="title" :label="t('dashboard.Item title')" min-width="220" />
|
||
<el-table-column prop="order_count" :label="t('dashboard.Order count')" width="120" />
|
||
<el-table-column prop="completed_count" :label="t('dashboard.Completed')" width="120" />
|
||
<el-table-column prop="rejected_count" :label="t('dashboard.Rejected')" width="120" />
|
||
<el-table-column prop="points_cost_sum" :label="t('dashboard.Points sum')" width="140" />
|
||
<el-table-column prop="amount_sum" :label="t('dashboard.Amount sum')" width="140" />
|
||
</el-table>
|
||
</el-card>
|
||
</el-col>
|
||
</el-row>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { useTransition } from '@vueuse/core'
|
||
import { CSSProperties, onMounted, onUnmounted, reactive, toRefs } 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 { getGreet } from '/@/utils/common'
|
||
import { Local } from '/@/utils/storage'
|
||
let workTimer: number
|
||
|
||
defineOptions({
|
||
name: 'dashboard',
|
||
})
|
||
|
||
const d = new Date()
|
||
const { t } = useI18n()
|
||
const adminInfo = useAdminInfo()
|
||
|
||
const state: {
|
||
remark: string
|
||
workingTimeFormat: string
|
||
pauseWork: boolean
|
||
playx: any | null
|
||
playxLoading: boolean
|
||
} = reactive({
|
||
remark: 'dashboard.Loading',
|
||
workingTimeFormat: '',
|
||
pauseWork: false,
|
||
playx: null,
|
||
playxLoading: true,
|
||
})
|
||
|
||
/**
|
||
* 带有数字向上变化特效的数据
|
||
*/
|
||
const countUp = reactive({
|
||
userRegNumber: 0,
|
||
fileNumber: 0,
|
||
usersNumber: 0,
|
||
addonsNumber: 0,
|
||
})
|
||
|
||
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
|
||
state.playx = res.data.playx ?? null
|
||
state.playxLoading = false
|
||
initCountUp()
|
||
}).catch(() => {
|
||
state.playxLoading = false
|
||
})
|
||
|
||
const initCountUp = () => {
|
||
const playx = state.playx ?? {}
|
||
const yesterdayRedeem = playx.yesterday_redeem ?? {}
|
||
countUpRefs.userRegNumber.value = playx.new_players_today ?? 0
|
||
countUpRefs.fileNumber.value = playx.yesterday_points_claimed ?? 0
|
||
countUpRefs.usersNumber.value = yesterdayRedeem.order_count ?? 0
|
||
countUpRefs.addonsNumber.value = playx.pending_physical_to_ship ?? 0
|
||
}
|
||
|
||
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 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)
|
||
}
|
||
|
||
let startPauseTime = 0
|
||
if (workingTime.startPauseTime <= 0) {
|
||
state.pauseWork = false
|
||
startPauseTime = 0
|
||
} else {
|
||
state.pauseWork = true
|
||
startPauseTime = time - workingTime.startPauseTime // 已暂停时间
|
||
}
|
||
|
||
let workingSeconds = time - workingTime.startTime - workingTime.pauseTime - startPauseTime
|
||
|
||
state.workingTimeFormat = formatSeconds(workingSeconds)
|
||
if (!state.pauseWork) {
|
||
workTimer = window.setInterval(() => {
|
||
workingSeconds++
|
||
state.workingTimeFormat = formatSeconds(workingSeconds)
|
||
}, 1000)
|
||
}
|
||
}
|
||
|
||
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)
|
||
}
|
||
}
|
||
}
|
||
|
||
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
|
||
}
|
||
|
||
onMounted(() => {
|
||
startWork()
|
||
})
|
||
|
||
onUnmounted(() => {
|
||
clearInterval(workTimer)
|
||
})
|
||
</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;
|
||
}
|
||
.small-panel {
|
||
background-color: #e9edf2;
|
||
border-radius: var(--el-border-radius-base);
|
||
padding: 25px;
|
||
margin-bottom: 20px;
|
||
.small-panel-title {
|
||
color: #92969a;
|
||
font-size: 15px;
|
||
}
|
||
.small-panel-content {
|
||
display: flex;
|
||
align-items: flex-end;
|
||
margin-top: 20px;
|
||
color: #2c3f5d;
|
||
.content-left {
|
||
display: flex;
|
||
align-items: center;
|
||
font-size: 24px;
|
||
.icon {
|
||
margin-right: 10px;
|
||
}
|
||
}
|
||
.content-right {
|
||
font-size: 18px;
|
||
margin-left: auto;
|
||
}
|
||
.color-success {
|
||
color: var(--el-color-success);
|
||
}
|
||
.color-warning {
|
||
color: var(--el-color-warning);
|
||
}
|
||
.color-danger {
|
||
color: var(--el-color-danger);
|
||
}
|
||
.color-info {
|
||
color: var(--el-text-color-secondary);
|
||
}
|
||
}
|
||
}
|
||
.growth-chart {
|
||
margin-bottom: 20px;
|
||
}
|
||
.playx-kpis {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||
gap: 12px;
|
||
}
|
||
.playx-kpi {
|
||
background-color: var(--ba-bg-color-overlay);
|
||
border: 1px solid var(--ba-border-color);
|
||
border-radius: var(--el-border-radius-base);
|
||
padding: 12px;
|
||
}
|
||
.playx-kpi-title {
|
||
font-size: 13px;
|
||
color: var(--el-text-color-secondary);
|
||
}
|
||
.playx-kpi-value {
|
||
margin-top: 6px;
|
||
font-size: 20px;
|
||
font-weight: 600;
|
||
color: var(--el-text-color-primary);
|
||
}
|
||
@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 {
|
||
color: var(--el-text-color-regular);
|
||
}
|
||
}
|
||
.new-user-item {
|
||
.new-user-base {
|
||
color: var(--el-text-color-regular);
|
||
}
|
||
}
|
||
}
|
||
</style>
|