1495 lines
52 KiB
Vue
1495 lines
52 KiB
Vue
<template>
|
||
<div class="default-main bookkeeping-dashboard">
|
||
<section class="dashboard-grid">
|
||
<div class="dashboard-panel bank-panel">
|
||
<div class="panel-title">{{ t('dashboard.Available Bank Balance') }}</div>
|
||
<div class="table-scroll">
|
||
<table class="bank-table">
|
||
<thead>
|
||
<tr>
|
||
<th>{{ t('dashboard.Bank Name') }}</th>
|
||
<th>{{ t('dashboard.Current Balance') }}</th>
|
||
<th>{{ t('dashboard.Transaction Breakdown') }}</th>
|
||
<th>{{ t('dashboard.Safe Alert') }}</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr
|
||
v-for="bank in visibleBanks"
|
||
:key="bank.id"
|
||
:class="[bank.rowClass, { 'bank-colored': bank.labelColor }]"
|
||
:style="bankRowStyle(bank)"
|
||
>
|
||
<td>
|
||
<strong>{{ bank.name }}</strong>
|
||
<small>{{ bank.account }}</small>
|
||
</td>
|
||
<td class="balance">AUD {{ money(bank.balance) }}</td>
|
||
<td>
|
||
<div class="bank-operate">
|
||
<input :id="calculatorToggleId(bank)" class="calculator-toggle" type="checkbox" />
|
||
<div class="bank-operate-main">
|
||
<label
|
||
class="calculator-button"
|
||
:for="calculatorToggleId(bank)"
|
||
:aria-label="t('dashboard.Fast Calculation')"
|
||
:title="t('dashboard.Fast Calculation')"
|
||
>
|
||
<Icon name="fa fa-calculator" size="13" color="currentColor" />
|
||
</label>
|
||
<el-button size="small" :icon="Switch" @click="openTransfer(bank)">
|
||
{{ t('dashboard.Transfer') }}
|
||
</el-button>
|
||
<div class="breakdown">
|
||
<div class="breakdown-row breakdown-in">
|
||
<span class="breakdown-label">入</span>
|
||
<span class="breakdown-count">({{ bank.depositCount }})</span>
|
||
<strong>{{ money(bank.deposit) }}</strong>
|
||
</div>
|
||
<div class="breakdown-row breakdown-out">
|
||
<span class="breakdown-label">出</span>
|
||
<span class="breakdown-count">({{ bank.withdrawCount }})</span>
|
||
<strong>{{ money(bank.withdraw) }}</strong>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="bank-calculator">
|
||
<div class="calculator-formula">
|
||
<el-input
|
||
v-model="calculatorFormulas[calculatorKey(bank)]"
|
||
class="calculator-input"
|
||
size="small"
|
||
:placeholder="t('dashboard.Calculation placeholder')"
|
||
clearable
|
||
/>
|
||
<span class="calculator-equals">=</span>
|
||
<strong class="calculator-result" :class="{ 'is-invalid': isCalculatorInvalid(bank) }">
|
||
{{ calculatorResultText(bank) }}
|
||
</strong>
|
||
</div>
|
||
<div class="calculator-hint">
|
||
{{ t('dashboard.Fast Calculation Support') }}
|
||
<span>+ − × ÷</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</td>
|
||
<td>
|
||
<el-tag size="small" effect="plain" :type="bank.alertCode !== '0' ? 'danger' : 'info'">
|
||
{{ safeAlertText(bank.alertCode) }}
|
||
</el-tag>
|
||
</td>
|
||
</tr>
|
||
<tr v-if="!visibleBanks.length">
|
||
<td class="empty-banks" colspan="4">{{ t('dashboard.No bank data') }}</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<button v-if="banks.length > 4" class="more-info" type="button" @click="showAllBanks = !showAllBanks">
|
||
{{ showAllBanks ? t('dashboard.Show less') : t('dashboard.More info') }}
|
||
<Icon :name="showAllBanks ? 'fa fa-caret-up' : 'fa fa-caret-down'" />
|
||
</button>
|
||
</div>
|
||
|
||
<aside class="summary-side">
|
||
<div class="dashboard-panel summary-panel">
|
||
<div class="panel-title">{{ t('dashboard.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">{{ t('dashboard.Create New Transaction') }}</el-button>
|
||
</aside>
|
||
</section>
|
||
|
||
<el-alert class="webhook-alert" type="warning" :closable="true" show-icon>
|
||
<template #title>
|
||
<strong>{{ t('dashboard.Webhook enabled') }}</strong>
|
||
</template>
|
||
<p>{{ t('dashboard.Webhook hint') }}</p>
|
||
</el-alert>
|
||
|
||
<section class="transaction-section">
|
||
<div class="filter-row">
|
||
<div class="date-filter">
|
||
<label>{{ t('dashboard.Start Date') }}:</label>
|
||
<el-date-picker v-model="filters.startDate" type="date" value-format="YYYY-MM-DD" />
|
||
<label>{{ t('dashboard.End Date') }}:</label>
|
||
<el-date-picker v-model="filters.endDate" type="date" value-format="YYYY-MM-DD" />
|
||
<el-button @click="search">{{ t('Search') }}</el-button>
|
||
<el-button @click="setToday">{{ t('dashboard.Today') }}</el-button>
|
||
</div>
|
||
<div class="totals">
|
||
<span>{{ t('dashboard.Date of data') }}: {{ dateRangeText }}</span>
|
||
<b
|
||
>{{ t('dashboard.Total Deposit / IN') }}: <em>AUD {{ money(totalDeposit) }}</em></b
|
||
>
|
||
<b
|
||
>{{ t('dashboard.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="t('dashboard.Created by')" width="110" />
|
||
<el-table-column prop="createdTime" :label="t('dashboard.Created Time')" width="170" />
|
||
<el-table-column :label="t('dashboard.Category')" width="100">
|
||
<template #default="{ row }">{{ categoryText(row.categoryId) }}</template>
|
||
</el-table-column>
|
||
<el-table-column prop="username" :label="t('dashboard.Username')" width="110" />
|
||
<el-table-column prop="remark" :label="t('dashboard.Remark')" min-width="205" />
|
||
<el-table-column prop="bank" :label="t('dashboard.Bank')" min-width="165" />
|
||
<el-table-column :label="t('dashboard.Type')" width="95">
|
||
<template #default="{ row }">{{ typeText(row.typeId) }}</template>
|
||
</el-table-column>
|
||
<el-table-column :label="t('dashboard.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 :label="t('dashboard.Label')" width="100">
|
||
<template #default="{ row }">{{ transactionLabelText(row.labelId) }}</template>
|
||
</el-table-column>
|
||
<el-table-column :label="t('dashboard.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="t('dashboard.Action')" fixed="right" width="180">
|
||
<template #default="{ row }">
|
||
<el-button link type="primary" size="small" @click="openHistory(row)">{{ t('dashboard.History') }}</el-button>
|
||
<el-button link type="primary" size="small" @click="openEdit(row)">{{ t('Edit') }}</el-button>
|
||
<el-button link type="danger" size="small" @click="removeTransaction(row)">{{ t('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="transactionDialogTitle" width="680px">
|
||
<el-form :model="transactionForm" label-width="145px">
|
||
<el-form-item :label="t('dashboard.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">{{ t('dashboard.Auto') }}</el-radio>
|
||
<el-radio value="Manual">{{ t('dashboard.Manual') }}</el-radio>
|
||
</el-radio-group>
|
||
</el-form-item>
|
||
<el-form-item :label="t('dashboard.Category')">
|
||
<el-select v-model="transactionForm.category">
|
||
<el-option :label="t('dashboard.Customer')" :value="1" />
|
||
<el-option :label="t('dashboard.Other Adjust')" :value="2" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item :label="t('dashboard.Type')">
|
||
<el-radio-group v-model="transactionForm.type">
|
||
<el-radio-button :value="1">{{ t('dashboard.Deposit') }}</el-radio-button>
|
||
<el-radio-button :value="2">{{ t('dashboard.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="t('dashboard.Username')">
|
||
<el-input
|
||
v-model="transactionForm.username"
|
||
:disabled="transactionDialog.mode === 'edit'"
|
||
:placeholder="t('dashboard.Username placeholder')"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item :label="t('dashboard.Remark')">
|
||
<el-input v-model="transactionForm.remark" :placeholder="t('dashboard.Optional')" />
|
||
</el-form-item>
|
||
<el-form-item :label="t('dashboard.Amount (AUD)')">
|
||
<el-input-number v-model="transactionForm.amount" :min="0" :precision="2" />
|
||
</el-form-item>
|
||
<el-form-item :label="t('dashboard.Bank')">
|
||
<el-select v-model="transactionForm.bank" :placeholder="t('dashboard.Optional select')">
|
||
<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="t('dashboard.Label')">
|
||
<el-select v-model="transactionForm.label" :placeholder="t('dashboard.Optional select')">
|
||
<el-option :label="t('dashboard.First Deposit')" :value="1" />
|
||
<el-option :label="t('dashboard.Unclaim')" :value="2" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item :label="t('dashboard.Game Ticket Auto')">
|
||
<el-checkbox v-model="transactionForm.ticketAuto">{{ t('dashboard.Auto generate ticket') }}</el-checkbox>
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button type="primary" :loading="transactionDialog.loading" @click="submitTransaction">{{ t('Confirm') }}</el-button>
|
||
<el-button @click="transactionDialog.visible = false">{{ t('Cancel') }}</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<el-dialog v-model="historyDialog.visible" :title="t('dashboard.Transaction Edit History')" width="720px">
|
||
<p class="dialog-note">{{ t('dashboard.History retention note') }}</p>
|
||
<el-table v-loading="historyDialog.loading" :data="historyDialog.rows" border size="small" :empty-text="t('dashboard.No Record')">
|
||
<el-table-column prop="id" :label="t('dashboard.Tx ID')" width="100" />
|
||
<el-table-column prop="editedBy" :label="t('dashboard.Edit By')" width="120" />
|
||
<el-table-column prop="editedTime" :label="t('dashboard.Edit Time')" width="170" />
|
||
<el-table-column :label="t('dashboard.Changes')">
|
||
<template #default="{ row }">{{ historyChangeText(row) }}</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
<template #footer>
|
||
<el-button @click="historyDialog.visible = false">{{ t('Cancel') }}</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<el-dialog v-model="transferDialog.visible" :title="t('dashboard.Bank Transfer')" width="560px">
|
||
<el-form :model="transferForm" label-width="125px">
|
||
<el-form-item :label="t('dashboard.Transfer (From)')">
|
||
<el-input v-model="transferForm.fromName" disabled />
|
||
</el-form-item>
|
||
<el-form-item :label="t('dashboard.Transfer (To)')">
|
||
<el-select v-model="transferForm.bankTo" :placeholder="t('dashboard.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="t('dashboard.Amount')">
|
||
<el-input-number v-model="transferForm.money" :min="0" :precision="2" />
|
||
</el-form-item>
|
||
<el-form-item :label="t('dashboard.Remark')">
|
||
<el-input v-model="transferForm.remark" :placeholder="t('dashboard.Optional')" />
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button type="primary" :loading="transferDialog.loading" @click="submitBankTransfer">{{ t('Confirm') }}</el-button>
|
||
<el-button @click="transferDialog.visible = false">{{ t('Cancel') }}</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { Switch } from '@element-plus/icons-vue'
|
||
import { computed, onMounted, reactive, ref } from 'vue'
|
||
import { useI18n } from 'vue-i18n'
|
||
import { bankTransact, delTransact, editTransact, index as getDashboard, logHistory, newTransact } from '/@/api/backend/dashboard'
|
||
import type { DashboardTransactPayload } from '/@/api/backend/dashboard'
|
||
|
||
defineOptions({
|
||
name: 'dashboard',
|
||
})
|
||
|
||
const { t } = useI18n()
|
||
|
||
interface Bank {
|
||
id: number | string
|
||
name: string
|
||
account: string
|
||
balance: number
|
||
deposit: number
|
||
withdraw: number
|
||
depositCount: number
|
||
withdrawCount: number
|
||
alertCode: string
|
||
rowClass?: string
|
||
labelColor?: string
|
||
}
|
||
|
||
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
|
||
label_color?: string
|
||
}
|
||
|
||
interface Transaction {
|
||
id: number
|
||
createdBy: string
|
||
createdTime: string
|
||
categoryId: string
|
||
username: string
|
||
remark: string
|
||
bank: string
|
||
bankId: number | string
|
||
typeId: string
|
||
flow: 'in' | 'out'
|
||
amount: number
|
||
labelId: 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
|
||
bankBefore: string
|
||
bankAfter: string
|
||
}
|
||
|
||
interface CalculationResult {
|
||
valid: boolean
|
||
value: number
|
||
}
|
||
|
||
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 toNumber = (value: unknown) => {
|
||
const number = Number(value)
|
||
return Number.isFinite(number) ? number : 0
|
||
}
|
||
|
||
const normalizeCalculationFormula = (value: string) =>
|
||
value
|
||
.replace(/,/g, '')
|
||
.replace(/乘以/g, '*')
|
||
.replace(/除以/g, '/')
|
||
.replace(/加/g, '+')
|
||
.replace(/减/g, '-')
|
||
.replace(/[+]/g, '+')
|
||
.replace(/[-–—]/g, '-')
|
||
.replace(/[×*xX]/g, '*')
|
||
.replace(/[÷/]/g, '/')
|
||
.replace(/\s+/g, '')
|
||
.replace(/^=/, '')
|
||
|
||
const evaluateExpression = (expression: string) => {
|
||
let index = 0
|
||
|
||
const parseNumber = () => {
|
||
const matched = expression.slice(index).match(/^(?:\d+(?:\.\d*)?|\.\d+)/)
|
||
if (!matched) return null
|
||
|
||
index += matched[0].length
|
||
return Number(matched[0])
|
||
}
|
||
|
||
const parseFactor = (): number | null => {
|
||
const char = expression[index]
|
||
|
||
if (char === '+') {
|
||
index += 1
|
||
return parseFactor()
|
||
}
|
||
|
||
if (char === '-') {
|
||
index += 1
|
||
const value = parseFactor()
|
||
return value === null ? null : -value
|
||
}
|
||
|
||
if (char === '(') {
|
||
index += 1
|
||
const value = parseExpression()
|
||
|
||
if (value === null || expression[index] !== ')') return null
|
||
|
||
index += 1
|
||
return value
|
||
}
|
||
|
||
return parseNumber()
|
||
}
|
||
|
||
const parseTerm = (): number | null => {
|
||
let value = parseFactor()
|
||
if (value === null) return null
|
||
|
||
while (expression[index] === '*' || expression[index] === '/') {
|
||
const operator = expression[index]
|
||
index += 1
|
||
const right = parseFactor()
|
||
|
||
if (right === null) return null
|
||
|
||
value = operator === '*' ? value * right : right === 0 ? Number.NaN : value / right
|
||
}
|
||
|
||
return value
|
||
}
|
||
|
||
function parseExpression(): number | null {
|
||
let value = parseTerm()
|
||
if (value === null) return null
|
||
|
||
while (expression[index] === '+' || expression[index] === '-') {
|
||
const operator = expression[index]
|
||
index += 1
|
||
const right = parseTerm()
|
||
|
||
if (right === null) return null
|
||
|
||
value = operator === '+' ? value + right : value - right
|
||
}
|
||
|
||
return value
|
||
}
|
||
|
||
const result = parseExpression()
|
||
|
||
return result !== null && index === expression.length && Number.isFinite(result) ? result : null
|
||
}
|
||
|
||
const calculateBalancePreview = (balance: number, formula: string): CalculationResult => {
|
||
const normalized = normalizeCalculationFormula(formula)
|
||
|
||
if (!normalized) {
|
||
return { valid: true, value: balance }
|
||
}
|
||
|
||
if (!/^[\d+\-*/().]+$/.test(normalized)) {
|
||
return { valid: false, value: balance }
|
||
}
|
||
|
||
const expression = /^[+\-*/]/.test(normalized) ? `${balance}${normalized}` : `${balance}+${normalized}`
|
||
const value = evaluateExpression(expression)
|
||
|
||
return value === null ? { valid: false, value: balance } : { valid: true, value }
|
||
}
|
||
|
||
const clampRgbChannel = (value: number) => Math.min(255, Math.max(0, Math.round(value)))
|
||
|
||
const parseColorChannel = (value: string) => {
|
||
const number = Number(value)
|
||
return Number.isFinite(number) ? clampRgbChannel(number) : 0
|
||
}
|
||
|
||
const parseAlphaChannel = (value?: string) => {
|
||
if (value === undefined) return 1
|
||
|
||
const number = Number(value)
|
||
return Number.isFinite(number) ? Math.min(1, Math.max(0, number)) : 1
|
||
}
|
||
|
||
const parseColorToRgb = (value: string) => {
|
||
const color = value.trim()
|
||
const hex = color.match(/^#([0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i)
|
||
|
||
if (hex) {
|
||
const raw = hex[1]
|
||
const full =
|
||
raw.length === 3
|
||
? raw
|
||
.split('')
|
||
.map((char) => char + char)
|
||
.join('')
|
||
: raw
|
||
|
||
return {
|
||
r: Number.parseInt(full.slice(0, 2), 16),
|
||
g: Number.parseInt(full.slice(2, 4), 16),
|
||
b: Number.parseInt(full.slice(4, 6), 16),
|
||
a: full.length === 8 ? Number.parseInt(full.slice(6, 8), 16) / 255 : 1,
|
||
}
|
||
}
|
||
|
||
const rgb = color.match(/^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)(?:\s*,\s*([\d.]+))?\s*\)$/i)
|
||
|
||
if (!rgb) return null
|
||
|
||
return {
|
||
r: parseColorChannel(rgb[1]),
|
||
g: parseColorChannel(rgb[2]),
|
||
b: parseColorChannel(rgb[3]),
|
||
a: parseAlphaChannel(rgb[4]),
|
||
}
|
||
}
|
||
|
||
const rgbToCss = (rgb: { r: number; g: number; b: number }) => `rgb(${clampRgbChannel(rgb.r)}, ${clampRgbChannel(rgb.g)}, ${clampRgbChannel(rgb.b)})`
|
||
|
||
const mixRgb = (source: { r: number; g: number; b: number }, target: { r: number; g: number; b: number }, targetWeight: number) => ({
|
||
r: source.r * (1 - targetWeight) + target.r * targetWeight,
|
||
g: source.g * (1 - targetWeight) + target.g * targetWeight,
|
||
b: source.b * (1 - targetWeight) + target.b * targetWeight,
|
||
})
|
||
|
||
const polishedBankRowColor = (backgroundColor: string) => {
|
||
const rgb = parseColorToRgb(backgroundColor)
|
||
if (!rgb) return backgroundColor
|
||
|
||
const blended = {
|
||
r: rgb.r * rgb.a + 255 * (1 - rgb.a),
|
||
g: rgb.g * rgb.a + 255 * (1 - rgb.a),
|
||
b: rgb.b * rgb.a + 255 * (1 - rgb.a),
|
||
}
|
||
const max = Math.max(blended.r, blended.g, blended.b)
|
||
const min = Math.min(blended.r, blended.g, blended.b)
|
||
|
||
if (max - min < 18) {
|
||
return rgbToCss(mixRgb(blended, { r: 241, g: 245, b: 249 }, 0.72))
|
||
}
|
||
|
||
if (blended.g >= blended.r && blended.g >= blended.b) {
|
||
return '#d9f3e5'
|
||
}
|
||
|
||
if (blended.r >= blended.g && blended.r >= blended.b) {
|
||
return '#f8dde2'
|
||
}
|
||
|
||
return '#dbeafe'
|
||
}
|
||
|
||
const srgbToLinear = (channel: number) => {
|
||
const value = channel / 255
|
||
return value <= 0.03928 ? value / 12.92 : Math.pow((value + 0.055) / 1.055, 2.4)
|
||
}
|
||
|
||
const relativeLuminance = (rgb: { r: number; g: number; b: number }) =>
|
||
0.2126 * srgbToLinear(rgb.r) + 0.7152 * srgbToLinear(rgb.g) + 0.0722 * srgbToLinear(rgb.b)
|
||
|
||
const contrastRatio = (first: number, second: number) => {
|
||
const lighter = Math.max(first, second)
|
||
const darker = Math.min(first, second)
|
||
return (lighter + 0.05) / (darker + 0.05)
|
||
}
|
||
|
||
const readableTextColor = (backgroundColor: string) => {
|
||
const rgb = parseColorToRgb(backgroundColor)
|
||
if (!rgb) return '#172033'
|
||
|
||
const blended = {
|
||
r: rgb.r * rgb.a + 255 * (1 - rgb.a),
|
||
g: rgb.g * rgb.a + 255 * (1 - rgb.a),
|
||
b: rgb.b * rgb.a + 255 * (1 - rgb.a),
|
||
}
|
||
const luminance = relativeLuminance(blended)
|
||
const slateContrast = contrastRatio(luminance, relativeLuminance({ r: 23, g: 32, b: 51 }))
|
||
const whiteContrast = contrastRatio(luminance, 1)
|
||
const darkContrast = contrastRatio(luminance, 0)
|
||
|
||
if (slateContrast >= 4.5 || slateContrast >= whiteContrast) {
|
||
return '#172033'
|
||
}
|
||
|
||
return whiteContrast >= darkContrast ? '#ffffff' : '#000000'
|
||
}
|
||
|
||
const bankRowStyle = (bank: Bank): Record<string, string> => {
|
||
if (!bank.labelColor) return {}
|
||
|
||
const backgroundColor = polishedBankRowColor(bank.labelColor)
|
||
const textColor = readableTextColor(backgroundColor)
|
||
const isLightText = textColor === '#ffffff'
|
||
|
||
return {
|
||
backgroundColor,
|
||
'--bank-row-text-color': textColor,
|
||
'--bank-row-muted-color': isLightText ? 'rgba(255, 255, 255, 0.82)' : 'rgba(0, 0, 0, 0.72)',
|
||
'--bank-row-border-color': isLightText ? 'rgba(255, 255, 255, 0.24)' : 'rgba(0, 0, 0, 0.16)',
|
||
'--bank-row-control-bg': isLightText ? 'rgba(255, 255, 255, 0.16)' : 'rgba(255, 255, 255, 0.72)',
|
||
'--bank-row-control-hover-bg': isLightText ? 'rgba(255, 255, 255, 0.26)' : 'rgba(255, 255, 255, 0.88)',
|
||
}
|
||
}
|
||
|
||
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),
|
||
alertCode: safeAlert,
|
||
rowClass: String(bank.status ?? '1') === '0' ? 'bank-muted' : '',
|
||
labelColor: bank.label_color || '',
|
||
}
|
||
}
|
||
|
||
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 transactionTotals = reactive({
|
||
totalDeposit: 0,
|
||
totalWithdraw: 0,
|
||
})
|
||
|
||
const filters = reactive({
|
||
startDate: today(),
|
||
endDate: today(),
|
||
})
|
||
const transactionPage = reactive({
|
||
count: 0,
|
||
currentPage: 1,
|
||
lastPage: 1,
|
||
pageSize: 10,
|
||
})
|
||
const showAllBanks = ref(false)
|
||
const calculatorFormulas = reactive<Record<string, string>>({})
|
||
const visibleBanks = computed(() => (showAllBanks.value ? banks.value : banks.value.slice(0, 4)))
|
||
const totalDeposit = computed(() => transactionTotals.totalDeposit)
|
||
const totalWithdraw = computed(() => transactionTotals.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 t('dashboard.Date range', {
|
||
start: filters.startDate,
|
||
end: filters.endDate,
|
||
days: diffDays,
|
||
unit: t(diffDays === 1 ? 'dashboard.Day' : 'dashboard.Days'),
|
||
})
|
||
})
|
||
const summary = computed(() => [
|
||
{ label: t('dashboard.Total Deposit / IN'), value: `AUD ${money(customerSummary.totalDeposit)}` },
|
||
{ label: t('dashboard.Total Withdraw / OUT'), value: `AUD ${money(customerSummary.totalWithdraw)}` },
|
||
{ label: t('dashboard.Deposit Count'), value: customerSummary.countDeposit },
|
||
{ label: t('dashboard.Withdraw Count'), value: customerSummary.countWithdraw },
|
||
{ label: t('dashboard.Active Player'), value: customerSummary.activePlayer },
|
||
{ label: t('dashboard.First Deposit Player'), value: customerSummary.firstDeposit },
|
||
{ label: t('dashboard.Unclaimed Amount'), value: `AUD ${money(customerSummary.unclaimAmount)}` },
|
||
{ label: t('dashboard.Unclaimed Receipt'), value: customerSummary.unclaimReceipt },
|
||
])
|
||
|
||
const calculatorKey = (bank: Bank) => String(bank.id)
|
||
|
||
const calculatorToggleId = (bank: Bank) => `bank-calculator-${calculatorKey(bank).replace(/[^a-zA-Z0-9_-]/g, '-')}`
|
||
|
||
const calculatorResult = (bank: Bank) => calculateBalancePreview(bank.balance, calculatorFormulas[calculatorKey(bank)] || '')
|
||
|
||
const isCalculatorInvalid = (bank: Bank) => !calculatorResult(bank).valid
|
||
|
||
const calculatorResultText = (bank: Bank) => {
|
||
const result = calculatorResult(bank)
|
||
return result.valid ? `AUD ${money(result.value)}` : 'AUD --'
|
||
}
|
||
|
||
const transactionDialog = reactive<{ visible: boolean; loading: boolean; mode: 'create' | 'edit'; editId: number | string | '' }>({
|
||
visible: false,
|
||
loading: false,
|
||
mode: 'create',
|
||
editId: '',
|
||
})
|
||
const transactionDialogTitle = computed(() =>
|
||
transactionDialog.mode === 'edit' ? t('dashboard.Edit Transaction', { id: transactionDialog.editId }) : t('dashboard.Create New Transaction')
|
||
)
|
||
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 safeAlertText = (code: string) => {
|
||
const labels: Record<string, string> = {
|
||
'1': t('dashboard.Hourly Alert'),
|
||
'2': t('dashboard.Daily Alert'),
|
||
'3': t('dashboard.Weekly Alert'),
|
||
'4': t('dashboard.Monthly Alert'),
|
||
'5': t('dashboard.Yearly Alert'),
|
||
'6': t('dashboard.Lifetime Alert'),
|
||
}
|
||
return labels[code] || t('dashboard.No Alert')
|
||
}
|
||
|
||
const typeText = (type: string) => {
|
||
const labels: Record<string, string> = {
|
||
'1': t('dashboard.Deposit'),
|
||
'2': t('dashboard.Withdraw'),
|
||
'3': 'IN',
|
||
'4': 'OUT',
|
||
}
|
||
return labels[type] || type
|
||
}
|
||
|
||
const categoryText = (category: string) => {
|
||
const labels: Record<string, string> = {
|
||
'1': t('dashboard.Customer'),
|
||
'2': t('dashboard.Other Adjust'),
|
||
}
|
||
return labels[category] || category
|
||
}
|
||
|
||
const transactionLabelText = (label: string) => {
|
||
const labels: Record<string, string> = {
|
||
'1': t('dashboard.First Deposit'),
|
||
'2': t('dashboard.Unclaim'),
|
||
}
|
||
return labels[label] || ''
|
||
}
|
||
|
||
const mapTransaction = (transaction: DashboardTransaction): Transaction => {
|
||
const type = String(transaction.type ?? '')
|
||
return {
|
||
id: transaction.id,
|
||
createdBy: transaction.created_by || '',
|
||
createdTime: formatDateTime(transaction.create_time),
|
||
categoryId: String(transaction.category ?? ''),
|
||
username: transaction.user_name || '',
|
||
remark: transaction.memo || '',
|
||
bank: transaction.bank_name || '',
|
||
bankId: transaction.bank_id || '',
|
||
typeId: type,
|
||
flow: ['1', '3'].includes(type) ? 'in' : 'out',
|
||
amount: toNumber(transaction.money),
|
||
labelId: 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
|
||
transactionTotals.totalDeposit = toNumber(transactionData?.total_deposit)
|
||
transactionTotals.totalWithdraw = toNumber(transactionData?.total_withdraw)
|
||
|
||
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 resetTransactionForm = () => {
|
||
Object.assign(transactionForm, {
|
||
time: '',
|
||
timeMode: 'Auto',
|
||
category: 1,
|
||
type: 1,
|
||
username: '',
|
||
remark: '',
|
||
amount: 0,
|
||
bank: '',
|
||
label: '',
|
||
ticketAuto: true,
|
||
})
|
||
}
|
||
|
||
const openCreate = () => {
|
||
resetTransactionForm()
|
||
transactionDialog.mode = 'create'
|
||
transactionDialog.editId = ''
|
||
transactionDialog.visible = true
|
||
}
|
||
|
||
const openEdit = (row: Transaction) => {
|
||
Object.assign(transactionForm, {
|
||
time: row.createdTime,
|
||
timeMode: 'Manual',
|
||
category: toNumber(row.categoryId) || 1,
|
||
type: toNumber(row.typeId) || 1,
|
||
username: row.username,
|
||
remark: row.remark,
|
||
amount: row.amount,
|
||
bank: row.bankId || banks.value.find((bank) => bank.name === row.bank)?.id || '',
|
||
label: row.labelId ? toNumber(row.labelId) : '',
|
||
ticketAuto: row.ticket.length > 0,
|
||
})
|
||
transactionDialog.mode = 'edit'
|
||
transactionDialog.editId = row.id
|
||
transactionDialog.visible = true
|
||
}
|
||
|
||
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 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),
|
||
bankBefore: String(history.bank_befter ?? ''),
|
||
bankAfter: String(history.bank_after ?? ''),
|
||
})
|
||
|
||
const historyChangeText = (row: HistoryRow) => t('dashboard.Bank change', { before: row.bankBefore, after: row.bankAfter })
|
||
|
||
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(() => {
|
||
loadDashboard().catch(() => {
|
||
// Request errors are displayed by the shared Axios interceptor.
|
||
})
|
||
})
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.bookkeeping-dashboard {
|
||
color: var(--el-text-color-primary);
|
||
font-size: 13px;
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
.dashboard-grid {
|
||
display: grid;
|
||
grid-template-columns: minmax(0, 1fr) 310px;
|
||
gap: var(--ba-main-space);
|
||
}
|
||
.dashboard-panel,
|
||
.transaction-section {
|
||
border: 1px solid var(--ba-border-color-soft);
|
||
border-radius: var(--ba-radius-panel);
|
||
background: var(--ba-bg-color-overlay);
|
||
box-shadow: var(--ba-shadow-card);
|
||
overflow: hidden;
|
||
}
|
||
.panel-title {
|
||
padding: 13px 16px;
|
||
border-bottom: 1px solid var(--ba-border-color-soft);
|
||
background: var(--ba-bg-color-soft);
|
||
color: var(--el-text-color-primary);
|
||
font-size: 15px;
|
||
font-weight: 700;
|
||
line-height: 1.4;
|
||
}
|
||
.table-scroll {
|
||
overflow-x: auto;
|
||
}
|
||
.bank-table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
th,
|
||
td {
|
||
padding: 11px 14px;
|
||
border-bottom: 1px solid var(--ba-border-color-soft);
|
||
text-align: left;
|
||
vertical-align: middle;
|
||
}
|
||
th {
|
||
color: var(--el-text-color-secondary);
|
||
font-size: 12px;
|
||
font-weight: 700;
|
||
background: var(--ba-bg-color-soft);
|
||
white-space: nowrap;
|
||
}
|
||
tbody tr {
|
||
transition: background-color 0.18s ease;
|
||
}
|
||
tbody tr:hover:not(.bank-colored) {
|
||
background: var(--el-color-primary-light-9);
|
||
}
|
||
tbody tr.bank-colored {
|
||
color: var(--bank-row-text-color);
|
||
text-shadow: none;
|
||
|
||
td {
|
||
border-bottom-color: var(--bank-row-border-color);
|
||
}
|
||
|
||
small {
|
||
color: var(--bank-row-muted-color);
|
||
}
|
||
|
||
.balance {
|
||
color: inherit;
|
||
}
|
||
|
||
:deep(.el-button) {
|
||
--el-button-text-color: var(--bank-row-text-color);
|
||
--el-button-bg-color: var(--bank-row-control-bg);
|
||
--el-button-border-color: var(--bank-row-border-color);
|
||
--el-button-hover-text-color: var(--bank-row-text-color);
|
||
--el-button-hover-bg-color: var(--bank-row-control-hover-bg);
|
||
--el-button-hover-border-color: var(--bank-row-border-color);
|
||
--el-button-active-text-color: var(--bank-row-text-color);
|
||
--el-button-active-bg-color: var(--bank-row-control-hover-bg);
|
||
--el-button-active-border-color: var(--bank-row-border-color);
|
||
}
|
||
|
||
.calculator-button {
|
||
color: var(--bank-row-text-color);
|
||
border-color: var(--bank-row-border-color);
|
||
background: var(--bank-row-control-bg);
|
||
&:hover,
|
||
&:focus-visible {
|
||
background: var(--bank-row-control-hover-bg);
|
||
}
|
||
}
|
||
|
||
:deep(.el-tag) {
|
||
color: var(--bank-row-text-color);
|
||
border-color: var(--bank-row-border-color);
|
||
background: var(--bank-row-control-bg);
|
||
}
|
||
}
|
||
strong,
|
||
small {
|
||
display: block;
|
||
}
|
||
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;
|
||
}
|
||
}
|
||
.bank-muted {
|
||
background: var(--el-fill-color-lighter);
|
||
}
|
||
.bank-operate {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: flex-start;
|
||
gap: 8px;
|
||
:deep(.el-button.is-circle) {
|
||
width: 30px;
|
||
height: 30px;
|
||
}
|
||
}
|
||
.bank-operate-main {
|
||
display: flex;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
}
|
||
.calculator-toggle {
|
||
position: absolute;
|
||
width: 1px;
|
||
height: 1px;
|
||
opacity: 0;
|
||
pointer-events: none;
|
||
}
|
||
.calculator-toggle:not(:checked) ~ .bank-calculator {
|
||
display: none;
|
||
}
|
||
.calculator-toggle:checked + .bank-operate-main .calculator-button {
|
||
border-color: var(--el-color-primary);
|
||
background: var(--el-color-primary-light-9);
|
||
color: var(--el-color-primary);
|
||
}
|
||
.calculator-toggle:focus-visible + .bank-operate-main .calculator-button {
|
||
outline: 2px solid var(--el-color-primary-light-5);
|
||
outline-offset: 2px;
|
||
}
|
||
.calculator-button {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 30px;
|
||
height: 30px;
|
||
padding: 0;
|
||
border: 1px solid var(--el-border-color);
|
||
border-radius: 50%;
|
||
background: var(--el-bg-color);
|
||
color: var(--el-text-color-regular);
|
||
cursor: pointer;
|
||
line-height: 1;
|
||
transition:
|
||
background-color 0.18s ease,
|
||
border-color 0.18s ease,
|
||
color 0.18s ease;
|
||
&:hover,
|
||
&:focus-visible {
|
||
border-color: var(--el-color-primary);
|
||
color: var(--el-color-primary);
|
||
background: var(--el-color-primary-light-9);
|
||
outline: none;
|
||
}
|
||
}
|
||
.bank-calculator {
|
||
width: min(300px, 100%);
|
||
padding: 6px 8px 5px;
|
||
border: 1px solid var(--ba-border-color-soft);
|
||
border-radius: 2px;
|
||
background: rgba(255, 255, 255, 0.94);
|
||
box-shadow: 0 6px 14px rgba(15, 23, 42, 0.08);
|
||
color: var(--el-text-color-primary);
|
||
}
|
||
.calculator-formula {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 5px;
|
||
}
|
||
.calculator-input {
|
||
flex: 1 1 125px;
|
||
min-width: 112px;
|
||
:deep(.el-input__wrapper) {
|
||
min-height: 28px;
|
||
border-radius: 0;
|
||
box-shadow: 0 0 0 1px rgba(148, 163, 184, 0.72) inset;
|
||
}
|
||
}
|
||
.calculator-equals {
|
||
color: var(--el-text-color-primary);
|
||
font-size: 15px;
|
||
font-weight: 700;
|
||
line-height: 1;
|
||
}
|
||
.calculator-result {
|
||
min-width: 92px;
|
||
color: var(--el-text-color-primary);
|
||
font-size: 15px;
|
||
font-weight: 700;
|
||
line-height: 1;
|
||
white-space: nowrap;
|
||
&.is-invalid {
|
||
color: var(--el-color-danger);
|
||
}
|
||
}
|
||
.calculator-hint {
|
||
margin-top: 3px;
|
||
color: var(--el-text-color-regular);
|
||
font-size: 11px;
|
||
line-height: 1.2;
|
||
span {
|
||
margin-left: 4px;
|
||
font-size: 12px;
|
||
font-weight: 700;
|
||
letter-spacing: 1px;
|
||
}
|
||
}
|
||
.breakdown {
|
||
display: inline-flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
min-width: 118px;
|
||
padding: 5px 8px;
|
||
border: 1px solid rgba(148, 163, 184, 0.28);
|
||
border-radius: 4px;
|
||
background: rgba(248, 250, 252, 0.94);
|
||
color: var(--el-text-color-regular);
|
||
box-shadow: 0 1px 0 rgba(15, 23, 42, 0.04);
|
||
}
|
||
.breakdown-row {
|
||
display: grid;
|
||
grid-template-columns: 18px auto minmax(0, 1fr);
|
||
align-items: center;
|
||
gap: 4px;
|
||
font-size: 12px;
|
||
font-weight: 700;
|
||
line-height: 1.15;
|
||
white-space: nowrap;
|
||
}
|
||
.breakdown-label {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 18px;
|
||
height: 18px;
|
||
border-radius: 3px;
|
||
color: #ffffff;
|
||
font-size: 11px;
|
||
font-weight: 700;
|
||
}
|
||
.breakdown-count {
|
||
color: var(--el-text-color-secondary);
|
||
font-weight: 600;
|
||
}
|
||
.breakdown-in {
|
||
color: var(--el-color-success);
|
||
.breakdown-label {
|
||
background: var(--el-color-success);
|
||
}
|
||
}
|
||
.breakdown-out {
|
||
color: var(--el-color-danger);
|
||
.breakdown-label {
|
||
background: var(--el-color-danger);
|
||
}
|
||
}
|
||
.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: 11px;
|
||
border: 0;
|
||
border-top: 1px solid var(--ba-border-color-soft);
|
||
background: var(--ba-bg-color-soft);
|
||
color: var(--el-color-primary);
|
||
cursor: pointer;
|
||
font-weight: 700;
|
||
transition: background-color 0.18s ease;
|
||
&:hover {
|
||
background: var(--el-color-primary-light-9);
|
||
}
|
||
}
|
||
.summary-side {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: var(--ba-main-space);
|
||
}
|
||
.summary-panel dl {
|
||
display: grid;
|
||
grid-template-columns: 1fr auto;
|
||
gap: 0;
|
||
margin: 0;
|
||
}
|
||
.summary-panel dt,
|
||
.summary-panel dd {
|
||
margin: 0;
|
||
padding: 10px 14px;
|
||
border-bottom: 1px solid var(--ba-border-color-soft);
|
||
}
|
||
.summary-panel dt {
|
||
color: var(--el-text-color-secondary);
|
||
font-weight: 600;
|
||
}
|
||
.summary-panel dd {
|
||
color: var(--el-color-primary);
|
||
font-weight: 700;
|
||
text-align: right;
|
||
}
|
||
.create-button {
|
||
width: 100%;
|
||
min-height: 46px;
|
||
font-weight: 700;
|
||
}
|
||
.webhook-alert {
|
||
margin: var(--ba-main-space) 0;
|
||
border: 1px solid rgba(183, 121, 31, 0.24);
|
||
p {
|
||
margin: 6px 0 0;
|
||
line-height: 1.5;
|
||
}
|
||
}
|
||
.filter-row {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
gap: 14px;
|
||
padding: 14px 16px;
|
||
border-bottom: 1px solid var(--ba-border-color-soft);
|
||
background: var(--ba-bg-color-soft);
|
||
}
|
||
.date-filter,
|
||
.totals {
|
||
display: flex;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
}
|
||
.date-filter :deep(.el-date-editor) {
|
||
width: 150px;
|
||
}
|
||
.date-filter label {
|
||
color: var(--el-text-color-secondary);
|
||
font-weight: 600;
|
||
}
|
||
.totals {
|
||
justify-content: flex-end;
|
||
color: var(--el-text-color-regular);
|
||
b {
|
||
font-weight: 700;
|
||
}
|
||
em {
|
||
color: var(--el-color-primary);
|
||
font-style: normal;
|
||
}
|
||
}
|
||
.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: 14px 16px;
|
||
border-top: 1px solid var(--ba-border-color-soft);
|
||
background: var(--ba-bg-color-soft);
|
||
}
|
||
.inline-mode {
|
||
margin-left: 12px;
|
||
}
|
||
.dialog-note {
|
||
margin: 0 0 8px;
|
||
color: var(--el-text-color-secondary);
|
||
}
|
||
:deep(.el-dialog__body) {
|
||
padding-top: 16px;
|
||
}
|
||
: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;
|
||
}
|
||
}
|
||
@media screen and (max-width: 720px) {
|
||
.filter-row {
|
||
flex-direction: column;
|
||
}
|
||
.date-filter {
|
||
align-items: stretch;
|
||
label {
|
||
width: 100%;
|
||
}
|
||
:deep(.el-date-editor),
|
||
:deep(.el-button) {
|
||
width: 100%;
|
||
}
|
||
}
|
||
.totals {
|
||
justify-content: flex-start;
|
||
}
|
||
.bank-table {
|
||
min-width: 760px;
|
||
}
|
||
.pagination {
|
||
justify-content: center;
|
||
}
|
||
}
|
||
</style>
|