Compare commits
8 Commits
1975d24db3
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| b8731590e7 | |||
| 53162332e3 | |||
| 58655e8a3e | |||
| 78a8c1075f | |||
| 0771c62caa | |||
| 6ac618bc0a | |||
| 3ccbc9cc3b | |||
| 9873c14d17 |
@@ -37,13 +37,13 @@ class Dashboard extends Backend
|
||||
$bankList = Bank::field([
|
||||
'id', 'bank_name', 'bank_account', 'current_balance', 'safe_alert', 'label_color', 'weigh',
|
||||
// 子查询:入款总额
|
||||
"(SELECT SUM(money/100) FROM ba_user_money_log WHERE bank_id = ba_bank.id AND type = 1 and create_time BETWEEN {$start} AND {$end}) AS total_fund_in",
|
||||
"(SELECT SUM(money/100) FROM ba_user_money_log WHERE bank_id = ba_bank.id AND type in (1,3) and create_time BETWEEN {$start} AND {$end}) AS total_fund_in",
|
||||
// 子查询:出款总额
|
||||
"(SELECT SUM(money/100) FROM ba_user_money_log WHERE bank_id = ba_bank.id AND type = 2 and create_time BETWEEN {$start} AND {$end}) AS total_fund_out",
|
||||
"(SELECT SUM(money/100) FROM ba_user_money_log WHERE bank_id = ba_bank.id AND type in (2,4) and create_time BETWEEN {$start} AND {$end}) AS total_fund_out",
|
||||
// 子查询:入款次数
|
||||
"(SELECT COUNT(*) FROM ba_user_money_log WHERE bank_id = ba_bank.id AND type = 1 and create_time BETWEEN {$start} AND {$end}) AS count_fund_in",
|
||||
"(SELECT COUNT(*) FROM ba_user_money_log WHERE bank_id = ba_bank.id AND type in (1,3) and create_time BETWEEN {$start} AND {$end}) AS count_fund_in",
|
||||
// 子查询:出款次数
|
||||
"(SELECT COUNT(*) FROM ba_user_money_log WHERE bank_id = ba_bank.id AND type = 2 and create_time BETWEEN {$start} AND {$end}) AS count_fund_out",
|
||||
"(SELECT COUNT(*) FROM ba_user_money_log WHERE bank_id = ba_bank.id AND type in (2,4) and create_time BETWEEN {$start} AND {$end}) AS count_fund_out",
|
||||
])
|
||||
->where('status', 1)
|
||||
->order('weigh', 'desc')
|
||||
@@ -268,4 +268,56 @@ class Dashboard extends Backend
|
||||
}
|
||||
$this->success('', $history);
|
||||
}
|
||||
|
||||
public function editTransact(): void
|
||||
{
|
||||
$userMoneyLog = new UserMoneyLog();
|
||||
$pk = $userMoneyLog->getPk();
|
||||
$id = $this->request->param($pk);
|
||||
$row = $userMoneyLog->find($id);
|
||||
if (!$row) {
|
||||
$this->error(__('Record not found'));
|
||||
}
|
||||
|
||||
if ($this->request->isPost()) {
|
||||
$data = $this->request->post();
|
||||
if (!$data) {
|
||||
$this->error(__('Parameter %s can not be empty', ['']));
|
||||
}
|
||||
|
||||
$result = false;
|
||||
$userMoneyLog->startTrans();
|
||||
try {
|
||||
$result = $row->save($data);
|
||||
$userMoneyLog->commit();
|
||||
} catch (Throwable $e) {
|
||||
$userMoneyLog->rollback();
|
||||
$this->error($e->getMessage());
|
||||
}
|
||||
if ($result !== false) {
|
||||
$this->success(__('Update successful'));
|
||||
} else {
|
||||
$this->error(__('No rows updated'));
|
||||
}
|
||||
return;
|
||||
}
|
||||
$this->success('', [
|
||||
'row' => $row
|
||||
]);
|
||||
}
|
||||
|
||||
public function delTransact($id): void
|
||||
{
|
||||
$result = false;
|
||||
try {
|
||||
$result = Db::table('ba_user_money_log')->delete($id);
|
||||
} catch (Throwable $e) {
|
||||
$this->error(__('%d records and files have been deleted', [1]) . $e->getMessage());
|
||||
}
|
||||
if ($result) {
|
||||
$this->success(__('%d records and files have been deleted', [1]));
|
||||
} else {
|
||||
$this->error(__('No rows were deleted'));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -45,10 +45,6 @@ class UserMoneyLog extends model
|
||||
$new->save();
|
||||
|
||||
}
|
||||
public static function onBeforeDelete(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function getMoneyAttr($value): string
|
||||
{
|
||||
|
||||
@@ -2,9 +2,77 @@ import createAxios from '/@/utils/axios'
|
||||
|
||||
export const url = '/admin/Dashboard/'
|
||||
|
||||
export function index() {
|
||||
export function index(params: { start: string; end: string; page?: number }) {
|
||||
return createAxios({
|
||||
url: url + 'index',
|
||||
method: 'get',
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
export function bankTransact(data: { money: number | string; bank_from: number | string; bank_to: number | string; remark: string }) {
|
||||
return createAxios(
|
||||
{
|
||||
url: url + 'bankTransact',
|
||||
method: 'post',
|
||||
data,
|
||||
},
|
||||
{
|
||||
showSuccessMessage: true,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export interface DashboardTransactPayload {
|
||||
create_time: number
|
||||
category: number | string
|
||||
type: number | string
|
||||
user_name: string
|
||||
memo: string
|
||||
money: number | string
|
||||
bank_id: number | string
|
||||
label: number | string
|
||||
game_ticket: 0 | 1
|
||||
}
|
||||
|
||||
export function newTransact(data: DashboardTransactPayload) {
|
||||
return createAxios(
|
||||
{
|
||||
url: url + 'newTransact',
|
||||
method: 'post',
|
||||
data,
|
||||
},
|
||||
{
|
||||
showSuccessMessage: true,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export function editTransact(data: DashboardTransactPayload & { id: number | string }) {
|
||||
return createAxios(
|
||||
{
|
||||
url: url + 'editTransact',
|
||||
method: 'post',
|
||||
data,
|
||||
},
|
||||
{
|
||||
showSuccessMessage: true,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export function delTransact(data: { id: number | string }) {
|
||||
return createAxios({
|
||||
url: url + 'delTransact',
|
||||
method: 'get',
|
||||
params: data,
|
||||
})
|
||||
}
|
||||
|
||||
export function logHistory(params: { id: number | string }) {
|
||||
return createAxios({
|
||||
url: url + 'logHistory',
|
||||
method: 'get',
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -11,3 +11,11 @@ export function add(userId: string) {
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function logHistory(params: { id: number | string }) {
|
||||
return createAxios({
|
||||
url: url + 'logHistory',
|
||||
method: 'get',
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@ export default {
|
||||
'type_list': {
|
||||
1 : 'Deposit',
|
||||
2 : 'Withdraw',
|
||||
3 : 'IN',
|
||||
4 : 'OUT',
|
||||
},
|
||||
'Game Ticket' : 'Game Ticket',
|
||||
'game_type': {
|
||||
|
||||
@@ -19,6 +19,8 @@ export default {
|
||||
'type_list': {
|
||||
1 : '存款',
|
||||
2 : '取款',
|
||||
3 : '转入',
|
||||
4 : '转出',
|
||||
},
|
||||
'Game Ticket' : '游戏门票',
|
||||
'game_type': {
|
||||
|
||||
@@ -1,826 +1,929 @@
|
||||
<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.Member registration') }}</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">+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-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">+50%</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-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">+28%</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-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">+88%</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')">
|
||||
<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')">
|
||||
<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')">
|
||||
<div class="new-user-growth">
|
||||
<el-scrollbar>
|
||||
<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 class="default-main bookkeeping-dashboard">
|
||||
<section class="dashboard-grid">
|
||||
<div class="dashboard-panel bank-panel">
|
||||
<div class="panel-title">Available Bank Balance</div>
|
||||
<div class="table-scroll">
|
||||
<table class="bank-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Bank Name</th>
|
||||
<th>Current Balance</th>
|
||||
<th>Transaction Breakdown</th>
|
||||
<th>Safe Alert</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="bank in visibleBanks" :key="bank.id" :class="bank.rowClass">
|
||||
<td>
|
||||
<strong>{{ bank.name }}</strong>
|
||||
<small>{{ bank.account }}</small>
|
||||
</td>
|
||||
<td class="balance">AUD {{ money(bank.balance) }}</td>
|
||||
<td>
|
||||
<div class="bank-operate">
|
||||
<el-button size="small" :icon="Coin" circle />
|
||||
<el-button size="small" :icon="Switch" @click="openTransfer(bank)">Transfer</el-button>
|
||||
<div class="breakdown">
|
||||
<span><i class="dot income">↓</i> ({{ bank.depositCount }}) {{ money(bank.deposit) }}</span>
|
||||
<span><i class="dot outcome">↑</i> ({{ bank.withdrawCount }}) {{ money(bank.withdraw) }}</span>
|
||||
</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>
|
||||
<Icon class="new-user-arrow" color="#8595F4" name="fa fa-angle-right" />
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<el-tag size="small" effect="plain" :type="bank.alert ? 'danger' : 'info'">{{ bank.alert || 'No Alert' }}</el-tag>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="!visibleBanks.length">
|
||||
<td class="empty-banks" colspan="4">No bank data</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<button v-if="banks.length > 4" class="more-info" type="button" @click="showAllBanks = !showAllBanks">
|
||||
{{ showAllBanks ? 'show less' : 'more info' }}
|
||||
<Icon :name="showAllBanks ? 'fa fa-caret-up' : 'fa fa-caret-down'" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<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')">
|
||||
<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')">
|
||||
<div class="user-surname-chart" :ref="chartRefs.set"></div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
<aside class="summary-side">
|
||||
<div class="dashboard-panel summary-panel">
|
||||
<div class="panel-title">Customer Summary</div>
|
||||
<dl>
|
||||
<template v-for="item in summary" :key="item.label">
|
||||
<dt>{{ item.label }}:</dt>
|
||||
<dd>{{ item.value }}</dd>
|
||||
</template>
|
||||
</dl>
|
||||
</div>
|
||||
<el-button class="create-button" type="success" @click="openCreate">CREATE NEW TRANSACTION</el-button>
|
||||
</aside>
|
||||
</section>
|
||||
|
||||
<el-alert class="webhook-alert" type="warning" :closable="true" show-icon>
|
||||
<template #title>
|
||||
<strong>The Transaction Mode you currently set has the Webhook (JDK) feature enabled</strong>
|
||||
</template>
|
||||
<p>
|
||||
Hint: If a transaction approved in the JK backend is not automatically recorded here within 10 minutes, click "Create New Transaction"
|
||||
to record it manually.
|
||||
</p>
|
||||
<p>提示:如果 JK 后台 Approved 的 Transaction 在这里 10 分钟内没有自动记录,请点击 "Create New Transaction" 手动记录。</p>
|
||||
</el-alert>
|
||||
|
||||
<section class="transaction-section">
|
||||
<div class="filter-row">
|
||||
<div class="date-filter">
|
||||
<label>Start Date:</label>
|
||||
<el-date-picker v-model="filters.startDate" type="date" value-format="YYYY-MM-DD" />
|
||||
<label>End Date:</label>
|
||||
<el-date-picker v-model="filters.endDate" type="date" value-format="YYYY-MM-DD" />
|
||||
<el-button @click="search">Search</el-button>
|
||||
<el-button @click="setToday">Today</el-button>
|
||||
</div>
|
||||
<div class="totals">
|
||||
<span>Date of data: {{ dateRangeText }}</span>
|
||||
<b
|
||||
>Total Deposit / IN: <em>AUD {{ money(totalDeposit) }}</em></b
|
||||
>
|
||||
<b
|
||||
>Total Withdraw / OUT: <em>AUD {{ money(totalWithdraw) }}</em></b
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-table :data="transactions" border size="small" class="transaction-table" :row-class-name="transactionRowClass">
|
||||
<el-table-column prop="createdBy" label="Created by" width="110" />
|
||||
<el-table-column prop="createdTime" label="Created Time" width="170" />
|
||||
<el-table-column prop="category" label="Category" width="100" />
|
||||
<el-table-column prop="username" label="Username" width="110" />
|
||||
<el-table-column prop="remark" label="Remark" min-width="205" />
|
||||
<el-table-column prop="bank" label="Bank" min-width="165" />
|
||||
<el-table-column prop="type" label="Type" width="95" />
|
||||
<el-table-column label="Amount (AUD)" width="130" align="right">
|
||||
<template #default="{ row }">
|
||||
<strong :class="row.flow === 'in' ? 'amount-in' : 'amount-out'">{{ money(row.amount) }}</strong>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="label" label="Label" width="100" />
|
||||
<el-table-column label="Game Ticket" min-width="140">
|
||||
<template #default="{ row }">
|
||||
<div v-for="(ticket, index) in row.ticket" :key="`${ticket}-${index}`">{{ ticket }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="Action" fixed="right" width="180">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" size="small" @click="openHistory(row)">history</el-button>
|
||||
<el-button link type="primary" size="small" @click="openEdit(row)">edit</el-button>
|
||||
<el-button link type="danger" size="small" @click="removeTransaction(row)">delete</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="pagination">
|
||||
<el-pagination
|
||||
v-model:current-page="transactionPage.currentPage"
|
||||
background
|
||||
layout="prev, pager, next"
|
||||
:total="transactionPage.count"
|
||||
:page-size="transactionPage.pageSize"
|
||||
@current-change="onTransactionPageChange"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<el-dialog v-model="transactionDialog.visible" :title="transactionDialog.title" width="680px">
|
||||
<el-form :model="transactionForm" label-width="145px">
|
||||
<el-form-item label="Date & Time">
|
||||
<el-date-picker v-model="transactionForm.time" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" />
|
||||
<el-radio-group v-model="transactionForm.timeMode" class="inline-mode">
|
||||
<el-radio value="Auto">Auto</el-radio>
|
||||
<el-radio value="Manual">Manual</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="Category">
|
||||
<el-select v-model="transactionForm.category">
|
||||
<el-option label="Customer" :value="1" />
|
||||
<el-option label="Other Adjust" :value="2" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="Type">
|
||||
<el-radio-group v-model="transactionForm.type">
|
||||
<el-radio-button :value="1">Deposit</el-radio-button>
|
||||
<el-radio-button :value="2">Withdraw</el-radio-button>
|
||||
<el-radio-button :value="3">IN</el-radio-button>
|
||||
<el-radio-button :value="4">OUT</el-radio-button>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="Username">
|
||||
<el-input v-model="transactionForm.username" :disabled="transactionDialog.mode === 'edit'" placeholder="e.g. PLAYER001" />
|
||||
</el-form-item>
|
||||
<el-form-item label="Remark">
|
||||
<el-input v-model="transactionForm.remark" placeholder="(optional)" />
|
||||
</el-form-item>
|
||||
<el-form-item label="Amount (AUD)">
|
||||
<el-input-number v-model="transactionForm.amount" :min="0" :precision="2" />
|
||||
</el-form-item>
|
||||
<el-form-item label="Bank">
|
||||
<el-select v-model="transactionForm.bank" placeholder="- (optional) -">
|
||||
<el-option v-for="bank in banks" :key="bank.id" :label="bank.name" :value="bank.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="Label">
|
||||
<el-select v-model="transactionForm.label" placeholder="- (optional) -">
|
||||
<el-option label="First Deposit" :value="1" />
|
||||
<el-option label="Unclaim" :value="2" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="Game Ticket Auto">
|
||||
<el-checkbox v-model="transactionForm.ticketAuto">Auto generate ticket</el-checkbox>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button type="primary" :loading="transactionDialog.loading" @click="submitTransaction">CONFIRM</el-button>
|
||||
<el-button @click="transactionDialog.visible = false">CANCEL</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="historyDialog.visible" title="Transaction Edit History" width="720px">
|
||||
<p class="dialog-note">Transaction edit history is only kept for the most recent 12 months.</p>
|
||||
<p class="dialog-note">Transaction 的编辑历史记录仅保存最近 12 个月。</p>
|
||||
<el-table v-loading="historyDialog.loading" :data="historyDialog.rows" border size="small" empty-text="No Record">
|
||||
<el-table-column prop="id" label="Tx ID" width="100" />
|
||||
<el-table-column prop="editedBy" label="Edit By" width="120" />
|
||||
<el-table-column prop="editedTime" label="Edit Time" width="170" />
|
||||
<el-table-column prop="changes" label="Changes (Old → New)" />
|
||||
</el-table>
|
||||
<template #footer>
|
||||
<el-button @click="historyDialog.visible = false">CANCEL</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="transferDialog.visible" title="Bank Transfer" width="560px">
|
||||
<el-form :model="transferForm" label-width="125px">
|
||||
<el-form-item label="Transfer (From)">
|
||||
<el-input v-model="transferForm.fromName" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="Transfer (To)">
|
||||
<el-select v-model="transferForm.bankTo" placeholder="- Select Bank -">
|
||||
<el-option
|
||||
v-for="bank in banks"
|
||||
:key="bank.id"
|
||||
:label="bank.name"
|
||||
:value="bank.id"
|
||||
:disabled="bank.id === transferForm.bankFrom"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="Amount">
|
||||
<el-input-number v-model="transferForm.money" :min="0" :precision="2" />
|
||||
</el-form-item>
|
||||
<el-form-item label="Remark">
|
||||
<el-input v-model="transferForm.remark" placeholder="(optional)" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button type="primary" :loading="transferDialog.loading" @click="submitBankTransfer">CONFIRM</el-button>
|
||||
<el-button @click="transferDialog.visible = false">CANCEL</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useEventListener, useTemplateRefsList, useTransition } from '@vueuse/core'
|
||||
import * as echarts from 'echarts'
|
||||
import { CSSProperties, nextTick, onActivated, onBeforeMount, onMounted, onUnmounted, reactive, toRefs, 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 { useNavTabs } from '/@/stores/navTabs'
|
||||
import { fullUrl, getGreet } from '/@/utils/common'
|
||||
import { Local } from '/@/utils/storage'
|
||||
let workTimer: number
|
||||
import { Coin, Switch } from '@element-plus/icons-vue'
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { bankTransact, delTransact, editTransact, index as getDashboard, logHistory, newTransact } from '/@/api/backend/dashboard'
|
||||
import type { DashboardTransactPayload } from '/@/api/backend/dashboard'
|
||||
|
||||
defineOptions({
|
||||
name: 'dashboard',
|
||||
})
|
||||
|
||||
const d = new Date()
|
||||
const { t } = useI18n()
|
||||
const navTabs = useNavTabs()
|
||||
const adminInfo = useAdminInfo()
|
||||
const chartRefs = useTemplateRefsList<HTMLDivElement>()
|
||||
interface Bank {
|
||||
id: number | string
|
||||
name: string
|
||||
account: string
|
||||
balance: number
|
||||
deposit: number
|
||||
withdraw: number
|
||||
depositCount: number
|
||||
withdrawCount: number
|
||||
alert?: string
|
||||
rowClass?: string
|
||||
}
|
||||
|
||||
const state: {
|
||||
charts: any[]
|
||||
interface DashboardBank {
|
||||
id: number | string
|
||||
bank_name?: string
|
||||
bank_account?: string
|
||||
balance?: number | string
|
||||
current_balance?: number | string
|
||||
tx_in?: number | string
|
||||
tx_out?: number | string
|
||||
fund_in?: number | string
|
||||
fund_out?: number | string
|
||||
total_fund_in?: number | string
|
||||
total_fund_out?: number | string
|
||||
count_fund_in?: number | string
|
||||
count_fund_out?: number | string
|
||||
deposit_count?: number | string
|
||||
withdraw_count?: number | string
|
||||
safe_alert?: number | string
|
||||
status?: number | string
|
||||
}
|
||||
|
||||
interface Transaction {
|
||||
id: number
|
||||
createdBy: string
|
||||
createdTime: string
|
||||
category: string
|
||||
username: string
|
||||
remark: string
|
||||
workingTimeFormat: string
|
||||
pauseWork: boolean
|
||||
} = reactive({
|
||||
charts: [],
|
||||
remark: 'dashboard.Loading',
|
||||
workingTimeFormat: '',
|
||||
pauseWork: false,
|
||||
bank: string
|
||||
type: string
|
||||
flow: 'in' | 'out'
|
||||
amount: number
|
||||
label: string
|
||||
ticket: string[]
|
||||
}
|
||||
|
||||
interface DashboardScoreLog {
|
||||
game_type_text?: string
|
||||
money_log_id?: number | string
|
||||
game_type?: number | string
|
||||
score?: number | string
|
||||
}
|
||||
|
||||
interface DashboardTransaction {
|
||||
id: number
|
||||
user_id?: number | string
|
||||
money?: number | string
|
||||
before?: number | string
|
||||
after?: number | string
|
||||
type?: number | string
|
||||
transaction_id?: string
|
||||
created_by?: string
|
||||
memo?: string
|
||||
create_time?: number | string
|
||||
bank_id?: number | string
|
||||
category?: number | string
|
||||
user_name?: string
|
||||
bank_name?: string
|
||||
label?: number | string
|
||||
scoreLog?: DashboardScoreLog[]
|
||||
}
|
||||
|
||||
interface DashboardTransactionPage {
|
||||
count?: number | string
|
||||
current_page?: number | string
|
||||
last_page?: number | string
|
||||
list?: DashboardTransaction[]
|
||||
total_deposit?: number | string
|
||||
total_withdraw?: number | string
|
||||
}
|
||||
|
||||
interface DashboardCustomerSummary {
|
||||
total_deposit?: number | string
|
||||
total_withdraw?: number | string
|
||||
count_deposit?: number | string
|
||||
count_withdraw?: number | string
|
||||
active_player?: number | string
|
||||
first_deposit?: number | string
|
||||
unclaim_amount?: number | string
|
||||
unclaim_receipt?: number | string
|
||||
}
|
||||
|
||||
interface HistoryRow {
|
||||
id: number | string
|
||||
editedBy: string
|
||||
editedTime: string
|
||||
changes: string
|
||||
}
|
||||
|
||||
const today = () => {
|
||||
const date = new Date()
|
||||
const pad = (value: number) => value.toString().padStart(2, '0')
|
||||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`
|
||||
}
|
||||
|
||||
const money = (value: number) =>
|
||||
Number(value).toLocaleString('en-AU', {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
})
|
||||
|
||||
const safeAlertLabels: Record<string, string> = {
|
||||
'1': 'Hourly Alert',
|
||||
'2': 'Daily Alert',
|
||||
'3': 'Weekly Alert',
|
||||
'4': 'Monthly Alert',
|
||||
'5': 'Yearly Alert',
|
||||
'6': 'Lifetime Alert',
|
||||
}
|
||||
|
||||
const toNumber = (value: unknown) => {
|
||||
const number = Number(value)
|
||||
return Number.isFinite(number) ? number : 0
|
||||
}
|
||||
|
||||
const formatDateTime = (value: unknown) => {
|
||||
if (typeof value === 'string' && value.trim() && !Number.isFinite(Number(value))) {
|
||||
return value
|
||||
}
|
||||
|
||||
const timestamp = toNumber(value)
|
||||
if (!timestamp) return ''
|
||||
|
||||
const date = new Date(timestamp * 1000)
|
||||
const pad = (number: number) => number.toString().padStart(2, '0')
|
||||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
|
||||
}
|
||||
|
||||
const mapBank = (bank: DashboardBank): Bank => {
|
||||
const fundIn = toNumber(bank.total_fund_in ?? bank.fund_in)
|
||||
const fundOut = toNumber(bank.total_fund_out ?? bank.fund_out)
|
||||
const safeAlert = String(bank.safe_alert ?? '0')
|
||||
|
||||
return {
|
||||
id: bank.id,
|
||||
name: bank.bank_name || '-',
|
||||
account: bank.bank_account || '',
|
||||
balance: toNumber(bank.current_balance ?? bank.balance ?? fundIn - fundOut),
|
||||
deposit: fundIn,
|
||||
withdraw: fundOut,
|
||||
depositCount: toNumber(bank.count_fund_in ?? bank.deposit_count ?? bank.tx_in),
|
||||
withdrawCount: toNumber(bank.count_fund_out ?? bank.withdraw_count ?? bank.tx_out),
|
||||
alert: safeAlertLabels[safeAlert],
|
||||
rowClass: String(bank.status ?? '1') === '0' ? 'bank-muted' : '',
|
||||
}
|
||||
}
|
||||
|
||||
const banks = ref<Bank[]>([])
|
||||
const transactions = ref<Transaction[]>([])
|
||||
const customerSummary = reactive({
|
||||
totalDeposit: 0,
|
||||
totalWithdraw: 0,
|
||||
countDeposit: 0,
|
||||
countWithdraw: 0,
|
||||
activePlayer: 0,
|
||||
firstDeposit: 0,
|
||||
unclaimAmount: 0,
|
||||
unclaimReceipt: 0,
|
||||
})
|
||||
|
||||
/**
|
||||
* 带有数字向上变化特效的数据
|
||||
*/
|
||||
const countUp = reactive({
|
||||
userRegNumber: 0,
|
||||
fileNumber: 0,
|
||||
usersNumber: 0,
|
||||
addonsNumber: 0,
|
||||
const filters = reactive({
|
||||
startDate: today(),
|
||||
endDate: today(),
|
||||
})
|
||||
const transactionPage = reactive({
|
||||
count: 0,
|
||||
currentPage: 1,
|
||||
lastPage: 1,
|
||||
pageSize: 10,
|
||||
})
|
||||
const showAllBanks = ref(false)
|
||||
const visibleBanks = computed(() => (showAllBanks.value ? banks.value : banks.value.slice(0, 4)))
|
||||
const totalDeposit = computed(() => customerSummary.totalDeposit)
|
||||
const totalWithdraw = computed(() => customerSummary.totalWithdraw)
|
||||
const dateRangeText = computed(() => {
|
||||
const start = new Date(`${filters.startDate}T00:00:00`)
|
||||
const end = new Date(`${filters.endDate}T00:00:00`)
|
||||
const diffDays = Math.max(1, Math.floor((end.getTime() - start.getTime()) / 86400000) + 1)
|
||||
return `${filters.startDate} to ${filters.endDate} (${diffDays} ${diffDays === 1 ? 'Day' : 'Days'})`
|
||||
})
|
||||
const summary = computed(() => [
|
||||
{ label: 'Total Deposit / IN', value: `AUD ${money(totalDeposit.value)}` },
|
||||
{ label: 'Total Withdraw / OUT', value: `AUD ${money(totalWithdraw.value)}` },
|
||||
{ label: 'Deposit Count', value: customerSummary.countDeposit },
|
||||
{ label: 'Withdraw Count', value: customerSummary.countWithdraw },
|
||||
{ label: 'Active Player', value: customerSummary.activePlayer },
|
||||
{ label: 'First Deposit Player', value: customerSummary.firstDeposit },
|
||||
{ label: 'Unclaimed Amount', value: `AUD ${money(customerSummary.unclaimAmount)}` },
|
||||
{ label: 'Unclaimed Receipt', value: customerSummary.unclaimReceipt },
|
||||
])
|
||||
|
||||
const transactionDialog = reactive<{ visible: boolean; title: string; loading: boolean; mode: 'create' | 'edit'; editId: number | string | '' }>({
|
||||
visible: false,
|
||||
title: 'Create New Transaction',
|
||||
loading: false,
|
||||
mode: 'create',
|
||||
editId: '',
|
||||
})
|
||||
const transactionForm = reactive({
|
||||
time: '',
|
||||
timeMode: 'Auto',
|
||||
category: 1,
|
||||
type: 1,
|
||||
username: '',
|
||||
remark: '',
|
||||
amount: 0,
|
||||
bank: '',
|
||||
label: '',
|
||||
ticketAuto: true,
|
||||
})
|
||||
const historyDialog = reactive({
|
||||
visible: false,
|
||||
loading: false,
|
||||
rows: [] as HistoryRow[],
|
||||
})
|
||||
const transferDialog = reactive({ visible: false, loading: false })
|
||||
const transferForm = reactive<{ bankFrom: Bank['id'] | ''; fromName: string; bankTo: Bank['id'] | ''; money: number; remark: string }>({
|
||||
bankFrom: '',
|
||||
fromName: '',
|
||||
bankTo: '',
|
||||
money: 0,
|
||||
remark: '',
|
||||
})
|
||||
|
||||
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',
|
||||
const typeLabels: Record<string, string> = {
|
||||
'1': 'Deposit',
|
||||
'2': 'Withdraw',
|
||||
'3': 'IN',
|
||||
'4': 'OUT',
|
||||
}
|
||||
|
||||
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 categoryLabels: Record<string, string> = {
|
||||
'1': 'Customer',
|
||||
'2': 'Other Adjust',
|
||||
}
|
||||
|
||||
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: {},
|
||||
legend: {
|
||||
data: [t('dashboard.Visits'), t('dashboard.Registration volume')],
|
||||
textStyle: {
|
||||
color: '#73767a',
|
||||
},
|
||||
top: 0,
|
||||
},
|
||||
series: [
|
||||
{
|
||||
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,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
userGrowthChart.setOption(option)
|
||||
state.charts.push(userGrowthChart)
|
||||
const transactionLabelTexts: Record<string, string> = {
|
||||
'1': 'First Deposit',
|
||||
'2': 'Unclaim',
|
||||
}
|
||||
|
||||
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 categoryValues: Record<string, number> = {
|
||||
Customer: 1,
|
||||
'Other Adjust': 2,
|
||||
}
|
||||
|
||||
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: '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],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
UserSourceChart.setOption(option)
|
||||
state.charts.push(UserSourceChart)
|
||||
const typeValues: Record<string, number> = {
|
||||
Deposit: 1,
|
||||
Withdraw: 2,
|
||||
IN: 3,
|
||||
OUT: 4,
|
||||
}
|
||||
|
||||
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',
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: t('dashboard.full name'),
|
||||
type: 'pie',
|
||||
radius: '55%',
|
||||
center: ['40%', '50%'],
|
||||
data: data.seriesData,
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
shadowBlur: 10,
|
||||
shadowOffsetX: 0,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.5)',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
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),
|
||||
})
|
||||
}
|
||||
return {
|
||||
legendData: legendData,
|
||||
seriesData: seriesData,
|
||||
}
|
||||
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)
|
||||
const labelValues: Record<string, number> = {
|
||||
'First Deposit': 1,
|
||||
Unclaim: 2,
|
||||
}
|
||||
|
||||
const echartsResize = () => {
|
||||
nextTick(() => {
|
||||
for (const key in state.charts) {
|
||||
state.charts[key].resize()
|
||||
}
|
||||
const mapTransaction = (transaction: DashboardTransaction): Transaction => {
|
||||
const type = String(transaction.type ?? '')
|
||||
return {
|
||||
id: transaction.id,
|
||||
createdBy: transaction.created_by || '',
|
||||
createdTime: formatDateTime(transaction.create_time),
|
||||
category: categoryLabels[String(transaction.category ?? '')] || '',
|
||||
username: transaction.user_name || '',
|
||||
remark: transaction.memo || '',
|
||||
bank: transaction.bank_name || '',
|
||||
type: typeLabels[type] || type,
|
||||
flow: ['1', '3'].includes(type) ? 'in' : 'out',
|
||||
amount: toNumber(transaction.money),
|
||||
label: transactionLabelTexts[String(transaction.label ?? '')] || '',
|
||||
ticket: Array.isArray(transaction.scoreLog)
|
||||
? transaction.scoreLog.map((score) => `${score.game_type_text || ''} : ${score.score ?? ''}`).filter((score) => score.trim() !== ':')
|
||||
: [],
|
||||
}
|
||||
}
|
||||
|
||||
const loadDashboard = (page = transactionPage.currentPage) => {
|
||||
return getDashboard({
|
||||
start: filters.startDate,
|
||||
end: filters.endDate,
|
||||
page,
|
||||
}).then((res) => {
|
||||
const bankData = Array.isArray(res.data.bank) ? res.data.bank : res.data.bank?.list
|
||||
banks.value = Array.isArray(bankData) ? bankData.map(mapBank) : []
|
||||
|
||||
const transactionData = res.data.transaction as DashboardTransactionPage | undefined
|
||||
const transactionList = Array.isArray(transactionData?.list) ? transactionData.list : []
|
||||
transactions.value = transactionList.map(mapTransaction)
|
||||
transactionPage.count = toNumber(transactionData?.count)
|
||||
transactionPage.currentPage = toNumber(transactionData?.current_page) || page
|
||||
transactionPage.lastPage = toNumber(transactionData?.last_page) || 1
|
||||
transactionPage.pageSize = transactionPage.lastPage > 0 ? Math.max(1, Math.ceil(transactionPage.count / transactionPage.lastPage)) : 10
|
||||
|
||||
const customerData = res.data.customer as DashboardCustomerSummary | undefined
|
||||
customerSummary.totalDeposit = toNumber(customerData?.total_deposit)
|
||||
customerSummary.totalWithdraw = toNumber(customerData?.total_withdraw)
|
||||
customerSummary.countDeposit = toNumber(customerData?.count_deposit)
|
||||
customerSummary.countWithdraw = toNumber(customerData?.count_withdraw)
|
||||
customerSummary.activePlayer = toNumber(customerData?.active_player)
|
||||
customerSummary.firstDeposit = toNumber(customerData?.first_deposit)
|
||||
customerSummary.unclaimAmount = toNumber(customerData?.unclaim_amount)
|
||||
customerSummary.unclaimReceipt = toNumber(customerData?.unclaim_receipt)
|
||||
})
|
||||
}
|
||||
|
||||
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 resetTransactionForm = () => {
|
||||
Object.assign(transactionForm, {
|
||||
time: '',
|
||||
timeMode: 'Auto',
|
||||
category: 1,
|
||||
type: 1,
|
||||
username: '',
|
||||
remark: '',
|
||||
amount: 0,
|
||||
bank: '',
|
||||
label: '',
|
||||
ticketAuto: 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 openCreate = () => {
|
||||
resetTransactionForm()
|
||||
transactionDialog.title = 'Create New Transaction'
|
||||
transactionDialog.mode = 'create'
|
||||
transactionDialog.editId = ''
|
||||
transactionDialog.visible = true
|
||||
}
|
||||
|
||||
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
|
||||
const openEdit = (row: Transaction) => {
|
||||
Object.assign(transactionForm, {
|
||||
time: row.createdTime,
|
||||
timeMode: 'Manual',
|
||||
category: categoryValues[row.category] || 1,
|
||||
type: typeValues[row.type] || 1,
|
||||
username: row.username,
|
||||
remark: row.remark,
|
||||
amount: row.amount,
|
||||
bank: banks.value.find((bank) => bank.name === row.bank)?.id || '',
|
||||
label: labelValues[row.label] || '',
|
||||
ticketAuto: row.ticket.length > 0,
|
||||
})
|
||||
transactionDialog.title = `Edit Transaction #${row.id}`
|
||||
transactionDialog.mode = 'edit'
|
||||
transactionDialog.editId = row.id
|
||||
transactionDialog.visible = true
|
||||
}
|
||||
|
||||
onActivated(() => {
|
||||
echartsResize()
|
||||
const transactionTimestamp = () => {
|
||||
if (transactionForm.timeMode === 'Auto' || !transactionForm.time) {
|
||||
return Math.floor(Date.now() / 1000)
|
||||
}
|
||||
|
||||
const date = new Date(transactionForm.time.replace(' ', 'T'))
|
||||
const timestamp = Math.floor(date.getTime() / 1000)
|
||||
return Number.isFinite(timestamp) ? timestamp : Math.floor(Date.now() / 1000)
|
||||
}
|
||||
|
||||
const buildTransactPayload = (): DashboardTransactPayload => ({
|
||||
create_time: transactionTimestamp(),
|
||||
category: transactionForm.category,
|
||||
type: transactionForm.type,
|
||||
user_name: transactionForm.username,
|
||||
memo: transactionForm.remark,
|
||||
money: transactionForm.amount,
|
||||
bank_id: transactionForm.bank,
|
||||
label: transactionForm.label,
|
||||
game_ticket: transactionForm.ticketAuto ? 1 : 0,
|
||||
})
|
||||
|
||||
const submitTransaction = () => {
|
||||
transactionDialog.loading = true
|
||||
const request =
|
||||
transactionDialog.mode === 'edit' && transactionDialog.editId !== ''
|
||||
? editTransact({ ...buildTransactPayload(), id: transactionDialog.editId })
|
||||
: newTransact(buildTransactPayload())
|
||||
|
||||
request
|
||||
.then(() => {
|
||||
transactionDialog.visible = false
|
||||
transactionPage.currentPage = 1
|
||||
return loadDashboard(1)
|
||||
})
|
||||
.finally(() => {
|
||||
transactionDialog.loading = false
|
||||
})
|
||||
}
|
||||
|
||||
const stringifyChange = (history: Record<string, unknown>) => {
|
||||
if (history.bank_befter !== undefined || history.bank_after !== undefined) {
|
||||
return `Bank:${history.bank_befter ?? ''}→${history.bank_after ?? ''}`
|
||||
}
|
||||
|
||||
return JSON.stringify(history)
|
||||
}
|
||||
|
||||
const mapHistory = (history: Record<string, unknown>): HistoryRow => ({
|
||||
id: (history.id || history.money_log_id || '') as number | string,
|
||||
editedBy: String(history.admin_name || ''),
|
||||
editedTime: formatDateTime(history.create_time),
|
||||
changes: stringifyChange(history),
|
||||
})
|
||||
|
||||
const openHistory = (row: Transaction) => {
|
||||
historyDialog.visible = true
|
||||
historyDialog.loading = true
|
||||
historyDialog.rows = []
|
||||
|
||||
logHistory({ id: row.id })
|
||||
.then((res) => {
|
||||
const data = Array.isArray(res.data) ? res.data : res.data?.list
|
||||
historyDialog.rows = Array.isArray(data) ? data.map((item) => mapHistory(item as Record<string, unknown>)) : []
|
||||
})
|
||||
.finally(() => {
|
||||
historyDialog.loading = false
|
||||
})
|
||||
}
|
||||
|
||||
const openTransfer = (bank: Bank) => {
|
||||
Object.assign(transferForm, { bankFrom: bank.id, fromName: bank.name, bankTo: '', money: 0, remark: '' })
|
||||
transferDialog.visible = true
|
||||
}
|
||||
|
||||
const submitBankTransfer = () => {
|
||||
transferDialog.loading = true
|
||||
bankTransact({
|
||||
money: transferForm.money,
|
||||
bank_from: transferForm.bankFrom,
|
||||
bank_to: transferForm.bankTo,
|
||||
remark: transferForm.remark,
|
||||
})
|
||||
.then(() => {
|
||||
transferDialog.visible = false
|
||||
return loadDashboard()
|
||||
})
|
||||
.finally(() => {
|
||||
transferDialog.loading = false
|
||||
})
|
||||
}
|
||||
|
||||
const removeTransaction = (row: Transaction) => {
|
||||
delTransact({ id: row.id }).then(() => {
|
||||
return loadDashboard(transactionPage.currentPage)
|
||||
})
|
||||
}
|
||||
|
||||
const setToday = () => {
|
||||
filters.startDate = today()
|
||||
filters.endDate = today()
|
||||
transactionPage.currentPage = 1
|
||||
loadDashboard(1).catch(() => {
|
||||
// Request errors are displayed by the shared Axios interceptor.
|
||||
})
|
||||
}
|
||||
|
||||
const search = () => {
|
||||
transactionPage.currentPage = 1
|
||||
loadDashboard(1).catch(() => {
|
||||
// Request errors are displayed by the shared Axios interceptor.
|
||||
})
|
||||
}
|
||||
|
||||
const onTransactionPageChange = (page: number) => {
|
||||
loadDashboard(page).catch(() => {
|
||||
// Request errors are displayed by the shared Axios interceptor.
|
||||
})
|
||||
}
|
||||
|
||||
const transactionRowClass = ({ row }: { row: Transaction }) => (row.flow === 'in' ? 'transaction-in' : 'transaction-out')
|
||||
|
||||
onMounted(() => {
|
||||
startWork()
|
||||
initCountUp()
|
||||
initUserGrowthChart()
|
||||
initFileGrowthChart()
|
||||
initUserSourceChart()
|
||||
initUserSurnameChart()
|
||||
useEventListener(window, 'resize', echartsResize)
|
||||
loadDashboard().catch(() => {
|
||||
// Request errors are displayed by the shared Axios interceptor.
|
||||
})
|
||||
})
|
||||
|
||||
onBeforeMount(() => {
|
||||
for (const key in state.charts) {
|
||||
state.charts[key].dispose()
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
clearInterval(workTimer)
|
||||
})
|
||||
|
||||
watch(
|
||||
() => navTabs.state.tabFullScreen,
|
||||
() => {
|
||||
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);
|
||||
}
|
||||
.bookkeeping-dashboard {
|
||||
color: var(--el-text-color-primary);
|
||||
font-size: 13px;
|
||||
}
|
||||
.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;
|
||||
}
|
||||
.dashboard-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 310px;
|
||||
gap: 16px;
|
||||
}
|
||||
.dashboard-panel,
|
||||
.transaction-section {
|
||||
border: 1px solid var(--el-border-color);
|
||||
border-radius: 4px;
|
||||
background: var(--el-bg-color);
|
||||
}
|
||||
.panel-title {
|
||||
padding: 11px 14px;
|
||||
border-bottom: 1px solid var(--el-border-color);
|
||||
background: var(--el-fill-color-light);
|
||||
color: var(--el-text-color-primary);
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.table-scroll {
|
||||
overflow-x: auto;
|
||||
}
|
||||
.bank-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
th,
|
||||
td {
|
||||
padding: 9px 12px;
|
||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||
text-align: left;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.working-coffee {
|
||||
transition: all 0.3s ease;
|
||||
width: 80px;
|
||||
th {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.working-text {
|
||||
strong,
|
||||
small {
|
||||
display: block;
|
||||
width: 100%;
|
||||
font-size: 15px;
|
||||
}
|
||||
small {
|
||||
margin-top: 3px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
.balance {
|
||||
color: var(--el-color-primary);
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.empty-banks {
|
||||
padding: 24px;
|
||||
color: var(--el-text-color-secondary);
|
||||
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;
|
||||
.bank-muted {
|
||||
background: var(--el-fill-color-lighter);
|
||||
}
|
||||
.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;
|
||||
}
|
||||
.user-growth-chart,
|
||||
.file-growth-chart {
|
||||
height: 260px;
|
||||
}
|
||||
.new-user-growth {
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
.user-source-chart,
|
||||
.user-surname-chart {
|
||||
height: 400px;
|
||||
}
|
||||
.new-user-item {
|
||||
.bank-operate {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
margin: 10px 15px;
|
||||
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%;
|
||||
}
|
||||
.new-user-base {
|
||||
margin-left: 10px;
|
||||
color: #2c3f5d;
|
||||
.new-user-name {
|
||||
font-size: 15px;
|
||||
}
|
||||
.new-user-time {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
.new-user-arrow {
|
||||
margin-left: auto;
|
||||
gap: 7px;
|
||||
}
|
||||
.breakdown {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
color: var(--el-text-color-regular);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.dot {
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
}
|
||||
.income,
|
||||
.amount-in {
|
||||
color: var(--el-color-success);
|
||||
}
|
||||
.outcome,
|
||||
.amount-out {
|
||||
color: var(--el-color-danger);
|
||||
}
|
||||
.more-info {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 9px;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--el-color-primary);
|
||||
cursor: pointer;
|
||||
}
|
||||
.summary-side {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
}
|
||||
.summary-panel dl {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
gap: 0;
|
||||
margin: 0;
|
||||
}
|
||||
.summary-panel dt,
|
||||
.summary-panel dd {
|
||||
margin: 0;
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||
}
|
||||
.summary-panel dd {
|
||||
color: var(--el-color-primary);
|
||||
font-weight: 700;
|
||||
text-align: right;
|
||||
}
|
||||
.create-button {
|
||||
width: 100%;
|
||||
min-height: 44px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.webhook-alert {
|
||||
margin: 16px 0;
|
||||
p {
|
||||
margin: 6px 0 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
.new-user-card :deep(.el-card__body) {
|
||||
padding: 0;
|
||||
.filter-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 12px;
|
||||
border-bottom: 1px solid var(--el-border-color);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 425px) {
|
||||
.welcome-img {
|
||||
display: none;
|
||||
.date-filter,
|
||||
.totals {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
.date-filter :deep(.el-date-editor) {
|
||||
width: 145px;
|
||||
}
|
||||
.totals {
|
||||
justify-content: flex-end;
|
||||
em {
|
||||
color: var(--el-color-primary);
|
||||
font-style: normal;
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: 1200px) {
|
||||
.lg-mb-20 {
|
||||
margin-bottom: 20px;
|
||||
.transaction-table :deep(.transaction-in) {
|
||||
--el-table-tr-bg-color: var(--el-color-success-light-9);
|
||||
}
|
||||
.transaction-table :deep(.transaction-out) {
|
||||
--el-table-tr-bg-color: var(--el-color-danger-light-9);
|
||||
}
|
||||
.pagination {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 12px;
|
||||
}
|
||||
.inline-mode {
|
||||
margin-left: 12px;
|
||||
}
|
||||
.dialog-note {
|
||||
margin: 0 0 8px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
:deep(.el-dialog__body) {
|
||||
padding-top: 12px;
|
||||
}
|
||||
:deep(.el-form-item .el-select),
|
||||
:deep(.el-form-item .el-input) {
|
||||
width: 100%;
|
||||
}
|
||||
@media screen and (max-width: 1100px) {
|
||||
.dashboard-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
html.dark {
|
||||
.welcome {
|
||||
background-color: var(--ba-bg-color-overlay);
|
||||
@media screen and (max-width: 720px) {
|
||||
.filter-row {
|
||||
flex-direction: column;
|
||||
}
|
||||
.working-opt {
|
||||
color: var(--el-text-color-primary);
|
||||
background-color: var(--ba-border-color);
|
||||
.totals {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
.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);
|
||||
}
|
||||
.bank-table {
|
||||
min-width: 760px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
<!-- 表格顶部菜单 -->
|
||||
<TableHeader
|
||||
:buttons="['refresh', 'add', 'comSearch', 'quickSearch', 'columnDisplay']"
|
||||
:buttons="['refresh', 'add', 'delete', 'comSearch', 'quickSearch', 'columnDisplay']"
|
||||
:quick-search-placeholder="
|
||||
t('Quick search placeholder', { fields: t('user.moneyLog.User name') + '/' + t('user.moneyLog.User nickname') })
|
||||
"
|
||||
@@ -22,6 +22,20 @@
|
||||
|
||||
<!-- 表单 -->
|
||||
<PopupForm />
|
||||
|
||||
<el-dialog v-model="historyDialog.visible" title="Transaction Edit History" width="720px">
|
||||
<p class="dialog-note">Transaction edit history is only kept for the most recent 12 months.</p>
|
||||
<p class="dialog-note">Transaction 的编辑历史记录仅保存最近 12 个月。</p>
|
||||
<el-table v-loading="historyDialog.loading" :data="historyDialog.rows" border size="small" empty-text="No Record">
|
||||
<el-table-column prop="id" label="Tx ID" width="100" />
|
||||
<el-table-column prop="editedBy" label="Edit By" width="120" />
|
||||
<el-table-column prop="editedTime" label="Edit Time" width="170" />
|
||||
<el-table-column prop="changes" label="Changes (Old → New)" />
|
||||
</el-table>
|
||||
<template #footer>
|
||||
<el-button @click="historyDialog.visible = false">CANCEL</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -31,7 +45,7 @@ import { provide, reactive, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRoute } from 'vue-router'
|
||||
import PopupForm from './popupForm.vue'
|
||||
import { add, url } from '/@/api/backend/user/moneyLog'
|
||||
import { add, logHistory, url } from '/@/api/backend/user/moneyLog'
|
||||
import { baTableApi } from '/@/api/common'
|
||||
import TableHeader from '/@/components/table/header/index.vue'
|
||||
import Table from '/@/components/table/index.vue'
|
||||
@@ -48,6 +62,84 @@ const defalutUser = (route.query.user_id ?? '') as string
|
||||
const state = reactive({
|
||||
userInfo: {} as anyObj,
|
||||
})
|
||||
interface HistoryRow {
|
||||
id: number | string
|
||||
editedBy: string
|
||||
editedTime: string
|
||||
changes: string
|
||||
}
|
||||
|
||||
const historyDialog = reactive({
|
||||
visible: false,
|
||||
loading: false,
|
||||
rows: [] as HistoryRow[],
|
||||
})
|
||||
const gameTypeMap = scoreLog.game_type as Record<string, string>
|
||||
const optButtons: OptButton[] = [
|
||||
{
|
||||
render: 'tipButton',
|
||||
name: 'history',
|
||||
title: 'History',
|
||||
text: '',
|
||||
type: 'primary',
|
||||
icon: 'fa fa-history',
|
||||
class: 'table-row-history',
|
||||
disabledTip: false,
|
||||
click: (row: TableRow) => {
|
||||
openHistory(row)
|
||||
},
|
||||
},
|
||||
...defaultOptButtons(['edit']),
|
||||
...defaultOptButtons(['delete']),
|
||||
]
|
||||
|
||||
const toNumber = (value: unknown) => {
|
||||
const number = Number(value)
|
||||
return Number.isFinite(number) ? number : 0
|
||||
}
|
||||
|
||||
const formatDateTime = (value: unknown) => {
|
||||
if (typeof value === 'string' && value.trim() && !Number.isFinite(Number(value))) {
|
||||
return value
|
||||
}
|
||||
|
||||
const timestamp = toNumber(value)
|
||||
if (!timestamp) return ''
|
||||
|
||||
const date = new Date(timestamp * 1000)
|
||||
const pad = (number: number) => number.toString().padStart(2, '0')
|
||||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
|
||||
}
|
||||
|
||||
const stringifyChange = (history: Record<string, unknown>) => {
|
||||
if (history.bank_befter !== undefined || history.bank_after !== undefined) {
|
||||
return `Bank:${history.bank_befter ?? ''}→${history.bank_after ?? ''}`
|
||||
}
|
||||
|
||||
return JSON.stringify(history)
|
||||
}
|
||||
|
||||
const mapHistory = (history: Record<string, unknown>): HistoryRow => ({
|
||||
id: (history.id || history.money_log_id || '') as number | string,
|
||||
editedBy: String(history.admin_name || ''),
|
||||
editedTime: formatDateTime(history.create_time),
|
||||
changes: stringifyChange(history),
|
||||
})
|
||||
|
||||
const openHistory = (row: TableRow) => {
|
||||
historyDialog.visible = true
|
||||
historyDialog.loading = true
|
||||
historyDialog.rows = []
|
||||
|
||||
logHistory({ id: row.id })
|
||||
.then((res) => {
|
||||
const data = Array.isArray(res.data) ? res.data : res.data?.list
|
||||
historyDialog.rows = Array.isArray(data) ? data.map((item) => mapHistory(item as Record<string, unknown>)) : []
|
||||
})
|
||||
.finally(() => {
|
||||
historyDialog.loading = false
|
||||
})
|
||||
}
|
||||
|
||||
const baTable = new baTableClass(
|
||||
new baTableApi(url),
|
||||
@@ -61,16 +153,9 @@ const baTable = new baTableClass(
|
||||
width: 70,
|
||||
formatter: (row: TableRow) => {
|
||||
return row.admin?.username || 'web'
|
||||
}
|
||||
},
|
||||
{ label: t('user.moneyLog.User name'), prop: 'user.username', align: 'center', operator: 'LIKE', operatorPlaceholder: t('Fuzzy query') },
|
||||
{
|
||||
label: t('user.moneyLog.User nickname'),
|
||||
prop: 'user.nickname',
|
||||
align: 'center',
|
||||
operator: 'LIKE',
|
||||
operatorPlaceholder: t('Fuzzy query'),
|
||||
},
|
||||
},
|
||||
{ label: t('user.moneyLog.User name'), prop: 'user.jk_username', align: 'center', operator: 'LIKE', operatorPlaceholder: t('Fuzzy query') },
|
||||
{ label: t('user.moneyLog.Change balance'), prop: 'money', align: 'center', operator: 'RANGE', sortable: 'custom' },
|
||||
{ label: t('user.moneyLog.bank_id'), prop: 'bank.bank_name', align: 'center', operator: 'LIKE', operatorPlaceholder: t('Fuzzy query') },
|
||||
{ label: t('user.moneyLog.Transaction id'), prop: 'transaction_id', align: 'center', operator: 'RANGE' },
|
||||
@@ -81,11 +166,13 @@ const baTable = new baTableClass(
|
||||
// 使用渲染函数手动构建显示内容
|
||||
formatter: (row: TableRow) => {
|
||||
if (!row.scoreLog || row.scoreLog.length === 0) return '-'
|
||||
return row.scoreLog.map((item: any) => {
|
||||
const gameName = scoreLog.game_type[item.game_type] || item.game_type
|
||||
return `${gameName}: ${item.score}`
|
||||
}).join(' | ')
|
||||
}
|
||||
return row.scoreLog
|
||||
.map((item: any) => {
|
||||
const gameName = gameTypeMap[String(item.game_type)] || item.game_type
|
||||
return `${gameName}: ${item.score}`
|
||||
})
|
||||
.join(' | ')
|
||||
},
|
||||
},
|
||||
{
|
||||
label: t('user.moneyLog.type'),
|
||||
@@ -97,6 +184,8 @@ const baTable = new baTableClass(
|
||||
custom: {
|
||||
'1': 'success',
|
||||
'2': 'danger',
|
||||
'3': 'success',
|
||||
'4': 'danger',
|
||||
},
|
||||
replaceValue: { ...tm('user.moneyLog.type_list') },
|
||||
},
|
||||
@@ -109,7 +198,7 @@ const baTable = new baTableClass(
|
||||
showOverflowTooltip: true,
|
||||
},
|
||||
{ label: t('Create time'), prop: 'create_time', align: 'center', render: 'datetime', sortable: 'custom', operator: 'RANGE', width: 160 },
|
||||
{ label: t('Operate'), align: 'center', width: '60', render: 'buttons', buttons: defaultOptButtons(['edit']) },
|
||||
{ label: t('Operate'), align: 'center', width: '130', render: 'buttons', buttons: optButtons },
|
||||
],
|
||||
dblClickNotEditColumn: ['all'],
|
||||
},
|
||||
|
||||
@@ -19,28 +19,61 @@
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
@keyup.enter="baTable.onSubmit(formRef)"
|
||||
@keyup.enter="submitForm"
|
||||
:model="baTable.form.items"
|
||||
:label-position="config.layout.shrink ? 'top' : 'right'"
|
||||
:label-width="baTable.form.labelWidth + 'px'"
|
||||
:rules="rules"
|
||||
v-if="!baTable.form.loading"
|
||||
>
|
||||
<FormItem
|
||||
type="remoteSelect"
|
||||
prop="user_id"
|
||||
:label="t('user.moneyLog.User ID')"
|
||||
v-model="baTable.form.items!.user_id"
|
||||
:placeholder="t('Click select')"
|
||||
:input-attr="{
|
||||
pk: 'user.id',
|
||||
field: 'nickname_text',
|
||||
remoteUrl: '/admin/user.User/index',
|
||||
onChange: getAdd,
|
||||
disabled: baTable.form.operate == 'Edit'
|
||||
}"
|
||||
/>
|
||||
<el-form-item :label="t('user.moneyLog.bank_id')" prop="bank_id">
|
||||
<template v-if="baTable.form.operate == 'Edit'">
|
||||
<el-form-item label="Date & Time">
|
||||
<el-date-picker v-model="state.editTime" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" />
|
||||
<el-radio-group v-model="state.timeMode" class="inline-mode">
|
||||
<el-radio value="Auto">Auto</el-radio>
|
||||
<el-radio value="Manual">Manual</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="Category">
|
||||
<el-select v-model="baTable.form.items!.category">
|
||||
<el-option label="Customer" :value="1" />
|
||||
<el-option label="Other Adjust" :value="2" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="Type" prop="type">
|
||||
<el-radio-group v-model="baTable.form.items!.type">
|
||||
<el-radio-button :value="1">Deposit</el-radio-button>
|
||||
<el-radio-button :value="2">Withdraw</el-radio-button>
|
||||
<el-radio-button :value="3">IN</el-radio-button>
|
||||
<el-radio-button :value="4">OUT</el-radio-button>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="Username">
|
||||
<el-input v-model="baTable.form.items!.user_name" disabled placeholder="e.g. PLAYER001" />
|
||||
</el-form-item>
|
||||
<el-form-item label="Remark" prop="memo">
|
||||
<el-input v-model="baTable.form.items!.memo" placeholder="(optional)" />
|
||||
</el-form-item>
|
||||
<el-form-item label="Amount (AUD)" prop="money">
|
||||
<el-input-number v-model="baTable.form.items!.money" :min="0" :precision="2" />
|
||||
</el-form-item>
|
||||
</template>
|
||||
<template v-else>
|
||||
<FormItem
|
||||
type="remoteSelect"
|
||||
prop="user_id"
|
||||
:label="t('user.moneyLog.User ID')"
|
||||
v-model="baTable.form.items!.user_id"
|
||||
:placeholder="t('Click select')"
|
||||
:input-attr="{
|
||||
pk: 'user.id',
|
||||
field: 'nickname_text',
|
||||
remoteUrl: '/admin/user.User/index',
|
||||
onChange: getAdd,
|
||||
}"
|
||||
/>
|
||||
</template>
|
||||
<el-form-item :label="baTable.form.operate == 'Edit' ? 'Bank' : t('user.moneyLog.bank_id')" prop="bank_id">
|
||||
<el-select
|
||||
v-model="baTable.form.items!.bank_id"
|
||||
filterable
|
||||
@@ -51,57 +84,64 @@
|
||||
clearable
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in state.bankOptions"
|
||||
:key="item.id"
|
||||
:label="item.bank_name"
|
||||
:value="item.id"
|
||||
>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<el-option v-for="item in state.bankOptions" :key="item.id" :label="item.bank_name" :value="item.id">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center">
|
||||
<span>{{ item.bank_name }}</span>
|
||||
<span style="color: #a8abb2; font-size: 12px;">{{ item.bank_account }}</span>
|
||||
<span style="color: #a8abb2; font-size: 12px">{{ item.bank_account }}</span>
|
||||
</div>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<FormItem
|
||||
:label="t('user.moneyLog.type')"
|
||||
type="select"
|
||||
v-model="baTable.form.items!.type"
|
||||
prop="type"
|
||||
:input-attr="{
|
||||
content: {
|
||||
'1': t('user.moneyLog.type_list.1'),
|
||||
'2': t('user.moneyLog.type_list.2'),
|
||||
},
|
||||
}"
|
||||
:placeholder="t('Please select field', { field: t('user.moneyLog.type') })"
|
||||
@change="(val) => { baTable.form.items!.type = val; }"
|
||||
/>
|
||||
<el-form-item prop="money" :label="t('user.moneyLog.Change amount')">
|
||||
<el-input
|
||||
v-model="baTable.form.items!.money"
|
||||
type="number"
|
||||
:placeholder="t('user.moneyLog.Please enter the balance change amount')"
|
||||
@input="changeMoney"
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="memo" :label="t('user.moneyLog.remarks')">
|
||||
<el-input
|
||||
@keyup.enter.stop=""
|
||||
@keyup.ctrl.enter="baTable.onSubmit(formRef)"
|
||||
v-model="baTable.form.items!.memo"
|
||||
type="textarea"
|
||||
:placeholder="t('user.moneyLog.Please enter change remarks / description')"
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
<template v-if="baTable.form.operate == 'Edit'">
|
||||
<el-form-item label="Label">
|
||||
<el-select v-model="baTable.form.items!.label" placeholder="- (optional) -" clearable>
|
||||
<el-option label="First Deposit" :value="1" />
|
||||
<el-option label="Unclaim" :value="2" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="Game Ticket Auto">
|
||||
<el-checkbox v-model="state.gameTicketAuto">Auto generate ticket</el-checkbox>
|
||||
</el-form-item>
|
||||
</template>
|
||||
<template v-else>
|
||||
<FormItem
|
||||
:label="t('user.moneyLog.type')"
|
||||
type="select"
|
||||
v-model="baTable.form.items!.type"
|
||||
prop="type"
|
||||
:input-attr="{
|
||||
content: {
|
||||
'1': t('user.moneyLog.type_list.1'),
|
||||
'2': t('user.moneyLog.type_list.2'),
|
||||
},
|
||||
}"
|
||||
:placeholder="t('Please select field', { field: t('user.moneyLog.type') })"
|
||||
@change="onTypeChange"
|
||||
/>
|
||||
<el-form-item prop="money" :label="t('user.moneyLog.Change amount')">
|
||||
<el-input
|
||||
v-model="baTable.form.items!.money"
|
||||
type="number"
|
||||
:placeholder="t('user.moneyLog.Please enter the balance change amount')"
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="memo" :label="t('user.moneyLog.remarks')">
|
||||
<el-input
|
||||
@keyup.enter.stop=""
|
||||
@keyup.ctrl.enter="submitForm"
|
||||
v-model="baTable.form.items!.memo"
|
||||
type="textarea"
|
||||
:placeholder="t('user.moneyLog.Please enter change remarks / description')"
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
</template>
|
||||
</el-form>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
<template #footer>
|
||||
<div :style="'width: calc(100% - ' + baTable.form.labelWidth! / 1.8 + 'px)'">
|
||||
<el-button @click="baTable.toggleForm('')">{{ t('Cancel') }}</el-button>
|
||||
<el-button v-blur :loading="baTable.form.submitLoading" @click="baTable.onSubmit(formRef)" type="primary">
|
||||
<el-button v-blur :loading="baTable.form.submitLoading" @click="submitForm" type="primary">
|
||||
{{ baTable.form.operateIds!.length > 1 ? t('Save and edit next item') : t('Save') }}
|
||||
</el-button>
|
||||
</div>
|
||||
@@ -110,7 +150,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, inject, watch, useTemplateRef, nextTick } from 'vue'
|
||||
import { reactive, inject, watch, useTemplateRef } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import type baTableClass from '/@/utils/baTable'
|
||||
import { add } from '/@/api/backend/user/moneyLog'
|
||||
@@ -127,14 +167,16 @@ const getBankList = (query: string = '') => {
|
||||
params: {
|
||||
// BuildAdmin 后端标准的搜索与分页格式
|
||||
quickSearch: query,
|
||||
limit: 20
|
||||
}
|
||||
}).then((res) => {
|
||||
// BuildAdmin 列表的标准返回结构是 res.data.list
|
||||
state.bankOptions = res.data.list || []
|
||||
}).finally(() => {
|
||||
state.bankLoading = false
|
||||
limit: 20,
|
||||
},
|
||||
})
|
||||
.then((res) => {
|
||||
// BuildAdmin 列表的标准返回结构是 res.data.list
|
||||
state.bankOptions = res.data.list || []
|
||||
})
|
||||
.finally(() => {
|
||||
state.bankLoading = false
|
||||
})
|
||||
}
|
||||
const config = useConfig()
|
||||
const { t } = useI18n()
|
||||
@@ -162,13 +204,19 @@ const formRef = useTemplateRef('formRef')
|
||||
const state: {
|
||||
userInfo: anyObj
|
||||
after: number
|
||||
bankOptions: any[] // 新增:存放银行下拉选项
|
||||
bankLoading: boolean // 新增:下拉框加载状态
|
||||
bankOptions: any[]
|
||||
bankLoading: boolean
|
||||
editTime: string
|
||||
timeMode: 'Auto' | 'Manual'
|
||||
gameTicketAuto: boolean
|
||||
} = reactive({
|
||||
userInfo: {},
|
||||
after: 0,
|
||||
bankOptions: [], // 初始化
|
||||
bankLoading: false
|
||||
bankOptions: [],
|
||||
bankLoading: false,
|
||||
editTime: '',
|
||||
timeMode: 'Manual',
|
||||
gameTicketAuto: true,
|
||||
})
|
||||
|
||||
const getAdd = () => {
|
||||
@@ -190,9 +238,7 @@ watch(
|
||||
}
|
||||
|
||||
const inputAmount = newMoney === '' ? 0 : parseFloat(newMoney as string)
|
||||
const currentMoney = baTable.form.operate == 'Edit'
|
||||
? parseFloat(baTable.form.items!.before)
|
||||
: parseFloat(state.userInfo.money)
|
||||
const currentMoney = baTable.form.operate == 'Edit' ? parseFloat(baTable.form.items!.before) : parseFloat(state.userInfo.money)
|
||||
|
||||
// 核心逻辑判断
|
||||
let result = 0
|
||||
@@ -209,8 +255,71 @@ watch(
|
||||
{ deep: true } // 深度监听
|
||||
)
|
||||
|
||||
const changeMoney = (value: string) => {
|
||||
const pad = (number: number) => number.toString().padStart(2, '0')
|
||||
|
||||
const formatDateTime = (value: unknown) => {
|
||||
const timestamp = Number(value)
|
||||
if (!Number.isFinite(timestamp) || !timestamp) return ''
|
||||
|
||||
const date = new Date(timestamp * 1000)
|
||||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
|
||||
}
|
||||
|
||||
const timestampFromEditTime = () => {
|
||||
if (state.timeMode === 'Auto' || !state.editTime) {
|
||||
return Math.floor(Date.now() / 1000)
|
||||
}
|
||||
|
||||
const timestamp = Math.floor(new Date(state.editTime.replace(' ', 'T')).getTime() / 1000)
|
||||
return Number.isFinite(timestamp) ? timestamp : Math.floor(Date.now() / 1000)
|
||||
}
|
||||
|
||||
const onTypeChange = (val: number | string) => {
|
||||
baTable.form.items!.type = val
|
||||
}
|
||||
|
||||
const findTableRow = (rows: TableRow[] = [], id: number | string): TableRow | undefined => {
|
||||
for (const row of rows) {
|
||||
if (row.id == id) return row
|
||||
if (row.children) {
|
||||
const child = findTableRow(row.children, id)
|
||||
if (child) return child
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const getUsername = (items: anyObj) => {
|
||||
const tableRow = findTableRow(baTable.table.data, items.id)
|
||||
return items.user?.jk_username || tableRow?.user?.jk_username || ''
|
||||
}
|
||||
|
||||
const initEditForm = () => {
|
||||
const items = baTable.form.items
|
||||
if (!items || baTable.form.operate !== 'Edit') return
|
||||
|
||||
items.category = items.category || 1
|
||||
if (!items.user_name) {
|
||||
items.user_name = getUsername(items)
|
||||
}
|
||||
items.bank_id = items.bank_id || items.bank?.id || ''
|
||||
items.label = items.label || ''
|
||||
state.editTime = formatDateTime(items.create_time)
|
||||
state.timeMode = 'Manual'
|
||||
state.gameTicketAuto =
|
||||
items.game_ticket !== undefined ? Number(items.game_ticket) === 1 : Array.isArray(items.scoreLog) && items.scoreLog.length > 0
|
||||
|
||||
if (items.bank && !state.bankOptions.some((bank) => bank.id === items.bank_id)) {
|
||||
state.bankOptions.unshift(items.bank)
|
||||
}
|
||||
}
|
||||
|
||||
const submitForm = () => {
|
||||
if (baTable.form.operate === 'Edit') {
|
||||
baTable.form.items!.create_time = timestampFromEditTime()
|
||||
baTable.form.items!.game_ticket = state.gameTicketAuto ? 1 : 0
|
||||
}
|
||||
|
||||
baTable.onSubmit(formRef.value)
|
||||
}
|
||||
|
||||
// 打开表单时刷新用户数据
|
||||
@@ -219,9 +328,18 @@ watch(
|
||||
(newValue) => {
|
||||
if (newValue) {
|
||||
getAdd()
|
||||
getBankList()
|
||||
initEditForm()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => baTable.form.items,
|
||||
() => {
|
||||
initEditForm()
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@@ -229,4 +347,11 @@ watch(
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
}
|
||||
.inline-mode {
|
||||
margin-left: 12px;
|
||||
}
|
||||
:deep(.el-form-item .el-select),
|
||||
:deep(.el-form-item .el-input) {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -78,10 +78,9 @@ const baTable = new baTableClass(
|
||||
column: [
|
||||
{ type: 'selection', align: 'center', operator: false },
|
||||
{ label: t('Id'), prop: 'id', align: 'center', operator: '=', operatorPlaceholder: t('Id'), width: 70 },
|
||||
{ label: t('user.user.User name'), prop: 'username', align: 'center', operator: 'LIKE', operatorPlaceholder: t('Fuzzy query') },
|
||||
{ label: t('user.user.User name'), prop: 'jk_username', align: 'center', operator: 'LIKE', operatorPlaceholder: t('Fuzzy query') },
|
||||
{ label: t('user.user.nickname'), prop: 'nickname', align: 'center', operator: 'LIKE', operatorPlaceholder: t('Fuzzy query') },
|
||||
{ label: t('user.user.jk user id'), prop: 'jk_user_id', align: 'center', operator: 'LIKE', operatorPlaceholder: t('Fuzzy query') },
|
||||
{ label: t('user.user.jk user name'), prop: 'jk_username', align: 'center', operator: 'LIKE', operatorPlaceholder: t('Fuzzy query') },
|
||||
{ label: t('user.user.referrer code'), prop: 'referrer_code', align: 'center', operator: 'LIKE', operatorPlaceholder: t('Fuzzy query'), width: 120,},
|
||||
{
|
||||
label: t('user.user.group'),
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
>
|
||||
<el-form-item prop="username" :label="t('user.user.User name')">
|
||||
<el-input
|
||||
v-model="baTable.form.items!.username"
|
||||
v-model="baTable.form.items!.jk_username"
|
||||
type="string"
|
||||
:placeholder="t('Please input field', { field: t('user.user.User name') + '(' + t('user.user.Login account') + ')' })"
|
||||
:disabled="baTable.form.operate === 'Edit'"
|
||||
|
||||
Reference in New Issue
Block a user