Compare commits
7 Commits
1cdd597879
...
master-gam
| Author | SHA1 | Date | |
|---|---|---|---|
| d336831fad | |||
| 75e91fee13 | |||
| 9a3f3b747f | |||
| f6197a9af5 | |||
| 16a7ef7413 | |||
| eb4c17cadf | |||
| 7ab3db121c |
@@ -25,6 +25,7 @@ class Channel extends Backend
|
||||
'settleStats',
|
||||
'dividendRecordList',
|
||||
'directBetRecordList',
|
||||
'companyBetRecordList',
|
||||
'settlementBetRecordList',
|
||||
];
|
||||
|
||||
@@ -751,6 +752,7 @@ class Channel extends Backend
|
||||
}
|
||||
}
|
||||
$paidDividendTotal = '0.00';
|
||||
$companyTotalBetAmount = '0.00';
|
||||
if ($channelIdList !== []) {
|
||||
$paidRow = Db::name('agent_commission_record')
|
||||
->where('channel_id', 'in', $channelIdList)
|
||||
@@ -758,6 +760,12 @@ class Channel extends Backend
|
||||
->field('SUM(commission_amount) AS s')
|
||||
->find();
|
||||
$paidDividendTotal = bcadd(strval(is_array($paidRow) ? ($paidRow['s'] ?? '0') : '0'), '0', 2);
|
||||
|
||||
$companyBetRow = Db::name('game_play_record')
|
||||
->where('channel_id', 'in', $channelIdList)
|
||||
->field('SUM(total_amount) AS s')
|
||||
->find();
|
||||
$companyTotalBetAmount = bcadd(strval(is_array($companyBetRow) ? ($companyBetRow['s'] ?? '0') : '0'), '0', 2);
|
||||
}
|
||||
return $this->success('', [
|
||||
'channel_total' => $total,
|
||||
@@ -767,6 +775,7 @@ class Channel extends Backend
|
||||
'carryover_total' => $carryoverTotal,
|
||||
'carryover_positive_total' => $carryoverPositiveTotal,
|
||||
'paid_dividend_total' => $paidDividendTotal,
|
||||
'company_total_bet_amount' => $companyTotalBetAmount,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -779,7 +788,7 @@ class Channel extends Backend
|
||||
if ($response !== null) {
|
||||
return $response;
|
||||
}
|
||||
if (!$this->auth->check('channel/viewDividendRecords')) {
|
||||
if (!$this->auth->check('channel/index')) {
|
||||
return $this->error(__('You have no permission'));
|
||||
}
|
||||
|
||||
@@ -840,7 +849,7 @@ class Channel extends Backend
|
||||
}
|
||||
|
||||
/**
|
||||
* 渠道直属玩家下注记录(直属投注额列点击)
|
||||
* 渠道直属玩家下注记录(直属投注额列点击;该渠道名下玩家全部注单)
|
||||
*/
|
||||
public function directBetRecordList(WebmanRequest $request): Response
|
||||
{
|
||||
@@ -848,7 +857,7 @@ class Channel extends Backend
|
||||
if ($response !== null) {
|
||||
return $response;
|
||||
}
|
||||
if (!$this->auth->check('channel/viewDirectBetRecords')) {
|
||||
if (!$this->auth->check('channel/index')) {
|
||||
return $this->error(__('You have no permission'));
|
||||
}
|
||||
|
||||
@@ -861,27 +870,31 @@ class Channel extends Backend
|
||||
}
|
||||
|
||||
/**
|
||||
* 参与分红口径的下注记录(操作列「查看总投注金额」)
|
||||
* 公司总投注记录下注明细(顶部公司总投注额点击;含未结算,当前账号可见渠道范围)
|
||||
*/
|
||||
public function settlementBetRecordList(WebmanRequest $request): Response
|
||||
public function companyBetRecordList(WebmanRequest $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) {
|
||||
return $response;
|
||||
}
|
||||
if (!$this->auth->check('channel/viewSettlementBetRecords')) {
|
||||
if (!$this->auth->check('channel/index')) {
|
||||
return $this->error(__('You have no permission'));
|
||||
}
|
||||
|
||||
$channelId = (int) ($request->get('channel_id', 0));
|
||||
if ($channelId <= 0 || !$this->assertChannelAccessible($channelId)) {
|
||||
return $this->error(__('You have no permission'));
|
||||
}
|
||||
|
||||
return $this->success('', $this->fetchChannelPlayRecordListPayload($request, $channelId, true));
|
||||
return $this->success('', $this->fetchChannelPlayRecordListPayload($request, 0, false));
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated 请使用 companyBetRecordList
|
||||
*/
|
||||
public function settlementBetRecordList(WebmanRequest $request): Response
|
||||
{
|
||||
return $this->companyBetRecordList($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $channelId 0=当前账号可见全部渠道
|
||||
* @return array{list: array<int, array<string, mixed>>, total: int, summary: array{record_count:int,total_bet_amount:string,total_win_amount:string}}
|
||||
*/
|
||||
private function fetchChannelPlayRecordListPayload(WebmanRequest $request, int $channelId, bool $settledOnly): array
|
||||
@@ -1054,8 +1067,21 @@ class Channel extends Backend
|
||||
$query = Db::name('game_play_record')->alias('pr')
|
||||
->leftJoin('user u', 'u.id = pr.user_id')
|
||||
->leftJoin('game_record gr', 'gr.id = pr.period_id')
|
||||
->leftJoin('channel c', 'c.id = pr.channel_id')
|
||||
->where('pr.channel_id', $channelId);
|
||||
->leftJoin('channel c', 'c.id = pr.channel_id');
|
||||
if ($channelId > 0) {
|
||||
if (!$this->assertChannelAccessible($channelId)) {
|
||||
$query->where('pr.channel_id', 0);
|
||||
return $query;
|
||||
}
|
||||
$query->where('pr.channel_id', $channelId);
|
||||
} else {
|
||||
$scope = $this->readableChannelIds();
|
||||
if ($scope === []) {
|
||||
$query->where('pr.channel_id', 0);
|
||||
} elseif ($scope !== null) {
|
||||
$query->whereIn('pr.channel_id', $scope);
|
||||
}
|
||||
}
|
||||
if ($settledOnly) {
|
||||
$query->where('pr.status', 2);
|
||||
}
|
||||
|
||||
@@ -38,9 +38,9 @@ class AdminWallet extends Backend
|
||||
list($where, $alias, $limit, $order) = $this->queryBuilder();
|
||||
$table = strtolower($this->model->getTable());
|
||||
$mainShort = $alias[$table] ?? '';
|
||||
if ($mainShort !== '' && $this->auth && !$this->auth->isSuperAdmin()) {
|
||||
// 非超管仅可查看自己钱包
|
||||
$where[] = [$mainShort . '.admin_id', '=', intval($this->auth->id ?? 0)];
|
||||
$scopedAdminIds = $this->getManageableScopeAdminIds();
|
||||
if ($mainShort !== '' && $scopedAdminIds !== []) {
|
||||
$where[] = [$mainShort . '.admin_id', 'in', $scopedAdminIds];
|
||||
}
|
||||
$res = $this->model
|
||||
->withJoin($this->withJoinTable, $this->withJoinType)
|
||||
|
||||
@@ -37,8 +37,9 @@ class AdminWalletRecord extends Backend
|
||||
list($where, $alias, $limit, $order) = $this->queryBuilder();
|
||||
$table = strtolower($this->model->getTable());
|
||||
$mainShort = $alias[$table] ?? '';
|
||||
if ($mainShort !== '' && $this->auth && !$this->auth->isSuperAdmin()) {
|
||||
$where[] = [$mainShort . '.admin_id', '=', intval($this->auth->id ?? 0)];
|
||||
$scopedAdminIds = $this->getManageableScopeAdminIds();
|
||||
if ($mainShort !== '' && $scopedAdminIds !== []) {
|
||||
$where[] = [$mainShort . '.admin_id', 'in', $scopedAdminIds];
|
||||
}
|
||||
|
||||
$res = $this->model
|
||||
|
||||
@@ -32,5 +32,59 @@ class CommissionRecord extends Backend
|
||||
$this->model = new \app\common\model\AgentCommissionRecord();
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function _index(): Response
|
||||
{
|
||||
if ($this->request && $this->request->get('select')) {
|
||||
return $this->select($this->request);
|
||||
}
|
||||
|
||||
list($where, $alias, $limit, $order) = $this->queryBuilder();
|
||||
$res = $this->model
|
||||
->field($this->indexField)
|
||||
->withJoin($this->withJoinTable, $this->withJoinType)
|
||||
->with($this->withJoinTable)
|
||||
->alias($alias)
|
||||
->where($where)
|
||||
->order($order)
|
||||
->paginate($limit);
|
||||
|
||||
$list = $this->enrichCommissionRecordList($res->items());
|
||||
|
||||
return $this->success('', [
|
||||
'list' => $list,
|
||||
'total' => $res->total(),
|
||||
'remark' => get_route_remark(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, mixed> $items
|
||||
* @return array<int, mixed>
|
||||
*/
|
||||
private function enrichCommissionRecordList(array $items): array
|
||||
{
|
||||
if ($items === []) {
|
||||
return $items;
|
||||
}
|
||||
|
||||
$out = [];
|
||||
foreach ($items as $item) {
|
||||
$row = is_array($item) ? $item : (method_exists($item, 'toArray') ? $item->toArray() : []);
|
||||
$gross = strval($row['commission_amount'] ?? '0.00');
|
||||
$fee = strval($row['handling_fee'] ?? '0.00');
|
||||
$net = strval($row['net_commission_amount'] ?? '0.00');
|
||||
if (bccomp($net, '0', 2) <= 0 && (bccomp($gross, '0', 2) > 0 || bccomp($fee, '0', 2) > 0)) {
|
||||
$net = bcsub($gross, $fee, 2);
|
||||
if (bccomp($net, '0', 2) < 0) {
|
||||
$net = '0.00';
|
||||
}
|
||||
$row['net_commission_amount'] = $net;
|
||||
}
|
||||
$out[] = $row;
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ use Webman\Http\Request as WebmanRequest;
|
||||
*/
|
||||
class AdminWithdrawOrder extends Backend
|
||||
{
|
||||
protected array $noNeedPermission = ['stats', 'approve', 'reject'];
|
||||
protected array $noNeedPermission = ['stats'];
|
||||
|
||||
protected ?object $model = null;
|
||||
|
||||
@@ -42,9 +42,9 @@ class AdminWithdrawOrder extends Backend
|
||||
list($where, $alias, $limit, $order) = $this->queryBuilder();
|
||||
$table = strtolower($this->model->getTable());
|
||||
$mainShort = $alias[$table] ?? '';
|
||||
$channelScope = $this->readableChannelIds();
|
||||
if ($mainShort !== '' && $channelScope !== null) {
|
||||
$where[] = [$mainShort . '.channel_id', 'in', $channelScope];
|
||||
$scopedAdminIds = $this->getManageableScopeAdminIds();
|
||||
if ($mainShort !== '' && $scopedAdminIds !== []) {
|
||||
$where[] = [$mainShort . '.admin_id', 'in', $scopedAdminIds];
|
||||
}
|
||||
$res = $this->model
|
||||
->withJoin($this->withJoinTable, $this->withJoinType)
|
||||
@@ -59,13 +59,15 @@ class AdminWithdrawOrder extends Backend
|
||||
->order($order)
|
||||
->paginate($limit);
|
||||
|
||||
$list = $res->items();
|
||||
foreach ($list as $idx => $item) {
|
||||
$list[$idx]['can_review'] = $this->canReviewOrder(is_array($item) ? $item : []) ? 1 : 0;
|
||||
$listArr = [];
|
||||
foreach ($res->items() as $item) {
|
||||
$row = is_array($item) ? $item : $item->toArray();
|
||||
$row['can_review'] = $this->canReviewOrder($row) ? 1 : 0;
|
||||
$listArr[] = $row;
|
||||
}
|
||||
|
||||
return $this->success('', [
|
||||
'list' => $list,
|
||||
'list' => $listArr,
|
||||
'total' => $res->total(),
|
||||
'remark' => get_route_remark(),
|
||||
]);
|
||||
@@ -79,7 +81,7 @@ class AdminWithdrawOrder extends Backend
|
||||
return $this->error(__('Parameter error'));
|
||||
}
|
||||
if ($this->request && $this->request->method() === 'POST') {
|
||||
return $this->error(__('Please use approve/reject buttons to review'));
|
||||
return $this->error(__('Please use the review action to process this order'));
|
||||
}
|
||||
$row = $this->loadWithRelations(intval(strval($id)));
|
||||
if (!$row) {
|
||||
@@ -91,7 +93,10 @@ class AdminWithdrawOrder extends Backend
|
||||
return $this->success('', ['row' => $row]);
|
||||
}
|
||||
|
||||
public function approve(WebmanRequest $request): Response
|
||||
/**
|
||||
* 审核(通过 / 拒绝)
|
||||
*/
|
||||
public function review(WebmanRequest $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) {
|
||||
@@ -101,46 +106,12 @@ class AdminWithdrawOrder extends Backend
|
||||
return $this->error(__('Parameter error'));
|
||||
}
|
||||
$id = intval(strval($request->post('id', 0)));
|
||||
if ($id <= 0) {
|
||||
return $this->error(__('Parameter error'));
|
||||
}
|
||||
$order = Db::name('admin_withdraw_order')->where('id', $id)->find();
|
||||
if (!is_array($order)) {
|
||||
return $this->error(__('Record not found'));
|
||||
}
|
||||
if (!$this->canReviewOrder($order)) {
|
||||
return $this->error(__('You have no permission'));
|
||||
}
|
||||
if (intval($order['status'] ?? 0) !== 0) {
|
||||
return $this->error(__('This withdraw order has already been reviewed'));
|
||||
}
|
||||
$remark = trim((string) $request->post('remark', ''));
|
||||
Db::startTrans();
|
||||
try {
|
||||
AdminWalletService::approveWithdraw($order, intval($this->auth->id), $remark);
|
||||
Db::commit();
|
||||
} catch (Throwable $e) {
|
||||
Db::rollback();
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
return $this->success(__('Approved'));
|
||||
}
|
||||
|
||||
public function reject(WebmanRequest $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) {
|
||||
return $response;
|
||||
}
|
||||
if ($request->method() !== 'POST') {
|
||||
return $this->error(__('Parameter error'));
|
||||
}
|
||||
$id = intval(strval($request->post('id', 0)));
|
||||
if ($id <= 0) {
|
||||
$action = strtolower(trim((string) $request->post('action', '')));
|
||||
if ($id <= 0 || !in_array($action, ['approve', 'reject'], true)) {
|
||||
return $this->error(__('Parameter error'));
|
||||
}
|
||||
$remark = trim((string) $request->post('remark', ''));
|
||||
if ($remark === '') {
|
||||
if ($action === 'reject' && $remark === '') {
|
||||
return $this->error(__('Please provide reject reason'));
|
||||
}
|
||||
$order = Db::name('admin_withdraw_order')->where('id', $id)->find();
|
||||
@@ -155,13 +126,18 @@ class AdminWithdrawOrder extends Backend
|
||||
}
|
||||
Db::startTrans();
|
||||
try {
|
||||
AdminWalletService::rejectWithdraw($order, intval($this->auth->id), $remark);
|
||||
if ($action === 'approve') {
|
||||
AdminWalletService::approveWithdraw($order, intval($this->auth->id), $remark);
|
||||
} else {
|
||||
AdminWalletService::rejectWithdraw($order, intval($this->auth->id), $remark);
|
||||
}
|
||||
Db::commit();
|
||||
} catch (Throwable $e) {
|
||||
Db::rollback();
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
return $this->success(__('Rejected'));
|
||||
|
||||
return $this->success($action === 'approve' ? __('Approved') : __('Rejected'));
|
||||
}
|
||||
|
||||
public function stats(WebmanRequest $request): Response
|
||||
@@ -171,9 +147,9 @@ class AdminWithdrawOrder extends Backend
|
||||
return $response;
|
||||
}
|
||||
$query = Db::name('admin_withdraw_order');
|
||||
$channelScope = $this->readableChannelIds();
|
||||
if ($channelScope !== null) {
|
||||
$query->where('channel_id', 'in', $channelScope);
|
||||
$scopedAdminIds = $this->getManageableScopeAdminIds();
|
||||
if ($scopedAdminIds !== []) {
|
||||
$query->where('admin_id', 'in', $scopedAdminIds);
|
||||
}
|
||||
$rows = $query->field(['status', 'amount', 'actual_amount'])->select()->toArray();
|
||||
$total = count($rows);
|
||||
@@ -225,23 +201,40 @@ class AdminWithdrawOrder extends Backend
|
||||
}
|
||||
|
||||
private function canReviewOrder(array $order): bool
|
||||
{
|
||||
if (!$this->auth || intval($order['status'] ?? 0) !== 0) {
|
||||
return false;
|
||||
}
|
||||
if (!$this->hasAdminWithdrawReviewPermission()) {
|
||||
return false;
|
||||
}
|
||||
$adminId = intval($order['admin_id'] ?? 0);
|
||||
if ($adminId <= 0) {
|
||||
return false;
|
||||
}
|
||||
$scopedAdminIds = $this->getManageableScopeAdminIds();
|
||||
if ($scopedAdminIds === []) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return in_array($adminId, $scopedAdminIds, true);
|
||||
}
|
||||
|
||||
private function hasAdminWithdrawReviewPermission(): bool
|
||||
{
|
||||
if (!$this->auth) {
|
||||
return false;
|
||||
}
|
||||
if ($this->auth->isSuperAdmin() || $this->hasGlobalReadScope()) {
|
||||
if ($this->auth->isSuperAdmin()) {
|
||||
return true;
|
||||
}
|
||||
$channelId = intval($order['channel_id'] ?? 0);
|
||||
if ($channelId <= 0) {
|
||||
return false;
|
||||
}
|
||||
$allowed = $this->readableChannelIds();
|
||||
if ($allowed === null) {
|
||||
return true;
|
||||
foreach ($this->buildPermissionRoutePaths('order/adminWithdrawOrder', 'review') as $routePath) {
|
||||
if ($this->auth->check($routePath)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return in_array($channelId, $allowed, true);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -108,7 +108,8 @@ class Auth extends MobileBase
|
||||
|
||||
$ok = $this->auth->login($username, $password, true);
|
||||
if (!$ok) {
|
||||
return $this->mobileError(1101, 'Incorrect account or password');
|
||||
$detail = (string) $this->auth->getError();
|
||||
return $this->mobileError(1101, $detail !== '' ? $detail : 'Incorrect account or password');
|
||||
}
|
||||
|
||||
$this->bindMobileDeviceSession($request);
|
||||
|
||||
@@ -7,6 +7,8 @@ namespace app\common\controller;
|
||||
use Throwable;
|
||||
use app\admin\library\Auth;
|
||||
use app\common\service\AdminChannelScopeService;
|
||||
use app\common\service\AdminCommissionDistributionService;
|
||||
use support\think\Db;
|
||||
use app\common\library\token\TokenExpirationException;
|
||||
use app\admin\library\traits\Backend as BackendTrait;
|
||||
use support\Response;
|
||||
@@ -542,4 +544,49 @@ class Backend extends Api
|
||||
|
||||
return array_values(array_unique($paths));
|
||||
}
|
||||
|
||||
/**
|
||||
* 角色组管理范围内可见的管理员 ID(本人 + 代理树下级 + 本人所在组及下级组内管理员)
|
||||
* 超管或全平台只读范围返回空数组表示不限制
|
||||
*
|
||||
* @return int[]
|
||||
*/
|
||||
protected function getManageableScopeAdminIds(): array
|
||||
{
|
||||
if ($this->auth === null || !$this->auth->isLogin()) {
|
||||
return [0];
|
||||
}
|
||||
if ($this->auth->isSuperAdmin() || $this->hasGlobalReadScope()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$operatorId = intval($this->auth->id);
|
||||
$ids = AdminCommissionDistributionService::getVisibleAdminIdsForOperator($operatorId, false);
|
||||
|
||||
$ownGroupIds = Db::name('admin_group_access')->where('uid', $operatorId)->column('group_id');
|
||||
$childGroupIds = $this->auth->getAdminChildGroups();
|
||||
$groupIds = array_values(array_unique(array_merge(
|
||||
array_map(static fn($id) => intval(strval($id)), $ownGroupIds),
|
||||
array_map(static fn($id) => intval(strval($id)), $childGroupIds)
|
||||
)));
|
||||
if ($groupIds !== []) {
|
||||
$groupAdminIds = Db::name('admin_group_access')
|
||||
->where('group_id', 'in', $groupIds)
|
||||
->column('uid');
|
||||
foreach ($groupAdminIds as $uid) {
|
||||
$uidInt = intval(strval($uid));
|
||||
if ($uidInt > 0) {
|
||||
$ids[] = $uidInt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($operatorId > 0) {
|
||||
$ids[] = $operatorId;
|
||||
}
|
||||
|
||||
$ids = array_values(array_unique(array_filter($ids, static fn(int $id): bool => $id > 0)));
|
||||
|
||||
return $ids === [] ? [0] : $ids;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,9 +121,10 @@ return [
|
||||
'periodSettings' => 'Period settings',
|
||||
'manualSettle' => 'Manual settle',
|
||||
'batchSettlePending' => 'Batch settle pending channels',
|
||||
'viewCommissionRecords' => 'View commission records',
|
||||
'viewDividendRecords' => 'View paid dividend records',
|
||||
'viewDirectBetRecords' => 'View direct bet records',
|
||||
'viewSettlementBetRecords' => 'View settlement-scope bets',
|
||||
'viewSettlementBetRecords' => 'View company bet records',
|
||||
'viewAllChannels' => 'View all channels',
|
||||
|
||||
// 其它中文按钮文案
|
||||
|
||||
@@ -53,9 +53,10 @@ return [
|
||||
'periodSettings' => '期号设置',
|
||||
'manualSettle' => '手动结算',
|
||||
'batchSettlePending' => '批量结算待结算渠道',
|
||||
'viewCommissionRecords' => '查看代理佣金记录',
|
||||
'viewDividendRecords' => '查看已分红记录',
|
||||
'viewDirectBetRecords' => '查看直属投注记录',
|
||||
'viewSettlementBetRecords' => '查看总投注金额',
|
||||
'viewSettlementBetRecords' => '查看公司总投注记录',
|
||||
'viewAllChannels' => '查看所有渠道',
|
||||
'walletAdjust' => '钱包加减点',
|
||||
'Markdown文档' => 'Markdown文档',
|
||||
|
||||
@@ -177,12 +177,15 @@ class Auth extends \ba\Auth
|
||||
} elseif (preg_match('/^[a-zA-Z][a-zA-Z0-9_]{2,15}$/', $username)) {
|
||||
$accountType = 'username';
|
||||
}
|
||||
if (!$accountType) {
|
||||
$this->setError('Account not exist');
|
||||
return false;
|
||||
if ($accountType) {
|
||||
$this->model = User::where($accountType, $username)->find();
|
||||
} else {
|
||||
// 兼容历史纯数字账号、带 + 前缀手机号等非标准格式
|
||||
$this->model = User::where('username', $username)->whereOr('phone', $username)->find();
|
||||
if (!$this->model && str_starts_with($username, '+')) {
|
||||
$this->model = User::where('phone', substr($username, 1))->find();
|
||||
}
|
||||
}
|
||||
|
||||
$this->model = User::where($accountType, $username)->find();
|
||||
if (!$this->model) {
|
||||
$this->setError('Account not exist');
|
||||
return false;
|
||||
@@ -204,7 +207,7 @@ class Auth extends \ba\Auth
|
||||
if ($this->model->login_failure > 0 && $lastLoginTs > 0 && time() - $lastLoginTs >= 86400) {
|
||||
$this->model->login_failure = 0;
|
||||
$this->model->save();
|
||||
$this->model = User::where($accountType, $username)->find();
|
||||
$this->model = User::find($this->model->id);
|
||||
}
|
||||
if ($this->model->login_failure >= $userLoginRetry) {
|
||||
$this->setError('Please try again after 1 day');
|
||||
|
||||
@@ -15,8 +15,13 @@ class AgentCommissionRecord extends Model
|
||||
'update_time' => 'integer',
|
||||
'settled_at' => 'integer',
|
||||
'commission_rate' => 'string',
|
||||
'share_rate' => 'string',
|
||||
'calc_base_amount' => 'string',
|
||||
'commission_amount' => 'string',
|
||||
'commission_share_percent' => 'string',
|
||||
'handling_fee' => 'string',
|
||||
'handling_fee_rate' => 'string',
|
||||
'net_commission_amount' => 'string',
|
||||
'status' => 'integer',
|
||||
];
|
||||
|
||||
|
||||
@@ -194,6 +194,14 @@ class AdminCommissionDistributionService
|
||||
if ($nodes === []) {
|
||||
return [];
|
||||
}
|
||||
$shareRateByAdmin = [];
|
||||
foreach ($nodes as $node) {
|
||||
$nodeAdminId = intval($node['admin_id'] ?? 0);
|
||||
if ($nodeAdminId <= 0) {
|
||||
continue;
|
||||
}
|
||||
$shareRateByAdmin[$nodeAdminId] = strval($node['share_rate'] ?? '0.00');
|
||||
}
|
||||
$defaultRate = self::normalizeHandlingFeeRatePercent($defaultHandlingFeeRate);
|
||||
$merged = [];
|
||||
foreach ($nodes as $node) {
|
||||
@@ -223,6 +231,7 @@ class AdminCommissionDistributionService
|
||||
'commission_amount' => $gross,
|
||||
'commission_rate' => $effectiveRate,
|
||||
'calc_base_amount' => $settlementBase,
|
||||
'share_rate' => $shareRateByAdmin[$adminId] ?? '0.00',
|
||||
'commission_share_percent' => self::calcCommissionSharePercent($gross, $totalCommission),
|
||||
'handling_fee_rate' => $feeRate,
|
||||
'handling_fee' => $feeAmount,
|
||||
|
||||
@@ -73,7 +73,6 @@ class ChannelSettlementService
|
||||
if ($adminId <= 0) {
|
||||
continue;
|
||||
}
|
||||
unset($row['net_commission_amount']);
|
||||
$row['status'] = 1;
|
||||
$row['settled_at'] = $now;
|
||||
$row['remark'] = strval($row['remark'] ?? '') . ' | 超管结算直接发放';
|
||||
@@ -370,9 +369,12 @@ class ChannelSettlementService
|
||||
'channel_id' => $channelId,
|
||||
'admin_id' => $adminId,
|
||||
'commission_rate' => strval($dist['commission_rate'] ?? '0.0000'),
|
||||
'share_rate' => strval($dist['share_rate'] ?? '0.00'),
|
||||
'calc_base_amount' => strval($dist['calc_base_amount'] ?? '0.00'),
|
||||
'commission_amount' => $amount,
|
||||
'commission_share_percent' => strval($dist['commission_share_percent'] ?? '0.00'),
|
||||
'handling_fee' => strval($dist['handling_fee'] ?? '0.00'),
|
||||
'handling_fee_rate' => strval($dist['handling_fee_rate'] ?? '0.00'),
|
||||
'net_commission_amount' => strval($dist['net_commission_amount'] ?? '0.00'),
|
||||
'status' => 0,
|
||||
'settled_at' => null,
|
||||
|
||||
@@ -39,13 +39,38 @@ if (!function_exists('env')) {
|
||||
if (!function_exists('__')) {
|
||||
/**
|
||||
* 语言翻译(BuildAdmin 兼容)
|
||||
* ThinkPHP 风格占位符(%s / %d 等 + 数字下标 vars)在翻译后走 sprintf;
|
||||
* Symfony 风格占位符(%name% 或 '%s' => value 等字符串键)走 trans/strtr。
|
||||
*/
|
||||
function __(string $name, array $vars = [], string $lang = ''): mixed
|
||||
{
|
||||
if (is_numeric($name) || !$name) {
|
||||
return $name;
|
||||
}
|
||||
return function_exists('trans') ? trans($name, $vars, null, $lang ?: null) : $name;
|
||||
if (!function_exists('trans')) {
|
||||
return $name;
|
||||
}
|
||||
|
||||
$positional = [];
|
||||
$named = [];
|
||||
foreach ($vars as $k => $v) {
|
||||
if (is_int($k)) {
|
||||
$positional[$k] = $v;
|
||||
} else {
|
||||
$named[$k] = $v;
|
||||
}
|
||||
}
|
||||
|
||||
if ($positional !== [] && $named === []) {
|
||||
$translated = trans($name, [], null, $lang ?: null);
|
||||
if ($translated === '' || $translated === $name) {
|
||||
$translated = $name;
|
||||
}
|
||||
|
||||
return vsprintf($translated, array_values($positional));
|
||||
}
|
||||
|
||||
return trans($name, $vars, null, $lang ?: null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -194,7 +194,10 @@ class GameWebSocketServer
|
||||
GameWebSocketDispatcher::sendDirect($connection, 'ws.subscribed', [
|
||||
'topics' => $finalTopics,
|
||||
], 'subscribed');
|
||||
self::pushAdminTestOddsPreview($connection, $finalTopics);
|
||||
// 演示帧仅供后台 admin 联调;mobile 用户订阅后不应收到样例玩家的 bet.accepted / wallet.changed
|
||||
if (GameWebSocketSubscriptionRegistry::isAdmin($connection->id)) {
|
||||
self::pushAdminTestOddsPreview($connection, $finalTopics);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -128,6 +128,7 @@ Route::add(['GET', 'POST'], '/api/account/userProfile', [\app\api\controller\Acc
|
||||
Route::add(['GET', 'POST'], '/api/game/lobbyInit', [\app\api\controller\Game::class, 'lobbyInit']);
|
||||
Route::add(['GET', 'POST'], '/api/game/dictionaryList', [\app\api\controller\Game::class, 'dictionaryList']);
|
||||
Route::add(['GET', 'POST'], '/api/game/periodCurrent', [\app\api\controller\Game::class, 'periodCurrent']);
|
||||
Route::add(['GET', 'POST'], '/api/game/periodHistory', [\app\api\controller\Game::class, 'periodHistory']);
|
||||
Route::post('/api/game/betPlace', [\app\api\controller\Game::class, 'betPlace']);
|
||||
Route::add(['GET', 'POST'], '/api/game/betMyOrders', [\app\api\controller\Game::class, 'betMyOrders']);
|
||||
Route::post('/api/game/placeBet', [\app\api\controller\Game::class, 'placeBet']);
|
||||
|
||||
@@ -14,7 +14,8 @@ Documents the **Channel Management** page (`/admin/channel`): summary cards, lis
|
||||
| Enabled | `status = 1` |
|
||||
| Pending dividend (count) | `carryover_balance > 0` |
|
||||
| Pending dividend (amount) | Sum of those balances |
|
||||
| Paid dividend | Sum of paid `agent_commission_record` in scope; clickable dialog requires `viewDividendRecords` |
|
||||
| Paid dividend | Sum of paid `agent_commission_record` in scope; clickable dialog requires `channel/index` |
|
||||
| Company total bet | Total bets in readable scope; clickable records dialog requires `channel/index` |
|
||||
|
||||
List filters: **All / With balance / No balance / Enabled only / Disabled only** (UI search only).
|
||||
|
||||
@@ -36,9 +37,7 @@ List filters: **All / With balance / No balance / Enabled only / Disabled only**
|
||||
|
||||
| Node | Label | Behavior |
|
||||
|------|-------|----------|
|
||||
| `channel/viewDividendRecords` | Paid dividend records | Top card + dialog |
|
||||
| `channel/viewDirectBetRecords` | Direct bet records | Direct bet column click |
|
||||
| `channel/viewSettlementBetRecords` | Settlement-scope bets | Row action |
|
||||
| `channel/index` | View | List, stat card clicks, bet/dividend record dialogs |
|
||||
| `channel/manualSettle` | Manual settle | Preview + submit (readable channel) |
|
||||
| `channel/batchSettlePending` | Batch settle | Writable enabled channels in scope |
|
||||
|
||||
@@ -59,8 +58,8 @@ Re-login after role changes to refresh `authNode`.
|
||||
|
||||
| Entry | API | Data |
|
||||
|-------|-----|------|
|
||||
| Direct bet amount | `directBetRecordList` | All play records for channel |
|
||||
| View settlement bets | `settlementBetRecordList` | `status = 2` only |
|
||||
| Direct bet amount (column) | `directBetRecordList` | All play records for that channel |
|
||||
| Company total bet (top card) | `companyBetRecordList` | All play records in readable scope |
|
||||
|
||||
**Filters (GET):** `period_no`, `user_keyword`, `result_number`, `pick_number`, `win_hit` (`won`/`lost`/`pending`), `page`, `limit`.
|
||||
|
||||
|
||||
@@ -79,9 +79,9 @@
|
||||
* **创建总代/子代账号**:在 **管理员管理**(`/admin/auth/admin`)维护代理树:`parent_admin_id`、`commission_share_rate`(顶级角色组从渠道总佣金分得 %,子代理从上级实得抽取 %)、`channel_id`、邀请码。
|
||||
* **代理树状图 (Tree View)**:管理员列表以树形展示;非超管仅见本人及全部下级。
|
||||
* **渠道管理页**(`/admin/channel`):
|
||||
* 顶部统计:渠道数、待分红、已分红(可点开记录);列表支持分红余额/启用状态筛选。
|
||||
* 顶部统计:渠道数、待分红、已分红、**公司总投注额**(可点开全部下注记录);列表支持分红余额/启用状态筛选。
|
||||
* **数据范围**:`AdminChannelScopeService`;全平台只读条件见 `docs/渠道管理后台说明.md` §3。
|
||||
* **操作**:查看总投注金额 / 直属投注记录(弹窗 + 筛选);手动结算(超管或 `channel/manualSettle`)。
|
||||
* **操作**:直属投注额列点击 / 公司总投注额卡片(弹窗 + 筛选);手动结算(超管或 `channel/manualSettle`)。
|
||||
* **渠道佣金结算**:
|
||||
* 按渠道 `agent_mode` 与已结算注单计算渠道总佣金(非充值口径)。
|
||||
* 按代理树拆分各管理员实得,写入 `agent_commission_record` 并 **即时入账** `admin_wallet`。
|
||||
|
||||
@@ -16,7 +16,8 @@
|
||||
| 启用渠道 | `status = 1` 的渠道数 |
|
||||
| 待分红渠道 | `carryover_balance > 0` 的渠道数 |
|
||||
| 待分红总额 | 上述渠道 `carryover_balance` 合计 |
|
||||
| 已分红金额 | 可读范围内渠道下,已发放佣金(`agent_commission_record.status = 1`)合计;**可点击**打开已分红记录弹窗(需 `viewDividendRecords` 权限) |
|
||||
| 公司总投注额 | 可读范围内全部玩家总投注(含未结算);**可点击**打开全部下注记录(需 `channel/index`) |
|
||||
| 已分红金额 | 可读范围内渠道下,已发放佣金(`agent_commission_record.status = 1`)合计;**可点击**打开已分红记录弹窗(需 `channel/index`) |
|
||||
|
||||
列表上方筛选:**全部 / 有分红余额 / 无分红余额 / 仅启用 / 仅停用**(前端 `search` 条件,不改变数据范围规则)。
|
||||
|
||||
@@ -54,17 +55,15 @@
|
||||
### 4.1 常用列
|
||||
|
||||
- 渠道标识、名称、代理模式、联营负结转、契约编号、结算周期等
|
||||
- **直属投注额**:该渠道下 `game_play_record` 投注合计;**可点击**打开直属下注记录弹窗(需 `viewDirectBetRecords`)
|
||||
- 操作列:**查看总投注金额**、**手动结算**、编辑、删除(后两者受写权限约束)
|
||||
- **直属投注额**:该渠道名下全部玩家的总投注额(含未结算);**可点击**打开该渠道直属玩家游戏下注记录(需 `channel/index`)
|
||||
- 顶部统计卡片 **公司总投注额**:当前账号可见渠道范围内的真实码量合计(含未结算);**可点击**打开全部下注记录明细(需 `channel/index`)
|
||||
- 操作列:**手动结算**、编辑、删除(后两者受写权限约束)
|
||||
|
||||
### 4.2 操作按钮权限
|
||||
|
||||
| 按钮权限节点 | 名称 | 行为 |
|
||||
|--------------|------|------|
|
||||
| `channel/index` | 查看 | 列表与详情 |
|
||||
| `channel/viewDividendRecords` | 查看已分红记录 | 顶部「已分红金额」卡片与弹窗 |
|
||||
| `channel/viewDirectBetRecords` | 查看直属投注记录 | 「直属投注额」列点击 |
|
||||
| `channel/viewSettlementBetRecords` | 查看总投注金额 | 操作列;分红口径已结算注单 |
|
||||
| `channel/index` | 查看 | 列表、统计卡片点击、下注/分红记录弹窗 |
|
||||
| `channel/manualSettle` | 手动结算 | 操作列;预览并提交渠道结算(见 §5) |
|
||||
| `channel/batchSettlePending` | 一键批量结算 | 批量结算当前账号**可写范围**内启用渠道 |
|
||||
| `channel/add` / `edit` / `del` | 增删改 | 须对目标渠道具备写权限 |
|
||||
@@ -91,8 +90,8 @@
|
||||
|
||||
| 入口 | 接口 | 数据范围 |
|
||||
|------|------|----------|
|
||||
| 直属投注额 | `GET /admin/channel/directBetRecordList` | 该渠道全部游玩记录 |
|
||||
| 查看总投注金额 | `GET /admin/channel/settlementBetRecordList` | 该渠道 **已结算** 记录(`status = 2`,参与分红口径) |
|
||||
| 直属投注额(列点击) | `GET /admin/channel/directBetRecordList` | 该渠道名下玩家 **全部** 游玩记录(含未结算) |
|
||||
| 公司总投注额(顶部卡片) | `GET /admin/channel/companyBetRecordList` | 当前账号可见渠道范围内 **全部** 游玩记录(含未结算) |
|
||||
|
||||
### 6.1 顶部统计(Card)
|
||||
|
||||
@@ -124,7 +123,7 @@
|
||||
## 7. 已分红记录弹窗
|
||||
|
||||
- **接口**:`GET /admin/channel/dividendRecordList`
|
||||
- **权限**:`channel/viewDividendRecords`
|
||||
- **权限**:`channel/index`
|
||||
- **字段**:结算单号、渠道名、代理账号、分红金额、结算周期、发放时间等
|
||||
|
||||
---
|
||||
|
||||
@@ -7,11 +7,16 @@ export default {
|
||||
channel_name: 'Channel',
|
||||
admin_id: 'Agent admin',
|
||||
admin_username: 'Agent username',
|
||||
commission_rate: 'Commission rate',
|
||||
calc_base_amount: 'Calculation base amount',
|
||||
commission_amount: 'Commission amount (gross)',
|
||||
handling_fee: 'Handling fee amount',
|
||||
calc_base_amount: 'Settlement base',
|
||||
share_rate: 'Share rate',
|
||||
commission_rate: 'Effective rate',
|
||||
commission_amount: 'Commission (gross)',
|
||||
commission_share_percent: 'Share of total',
|
||||
handling_fee_rate: 'Handling fee rate',
|
||||
handling_fee: 'Handling fee',
|
||||
net_commission_amount: 'Net commission',
|
||||
filter_by_settlement_no: 'Filtered by settlement no.: {no}',
|
||||
reset_settlement_filter: 'Reset filter',
|
||||
status: 'Status',
|
||||
'status 0': 'Pending',
|
||||
'status 1': 'Paid',
|
||||
|
||||
@@ -15,5 +15,6 @@ export default {
|
||||
remark: 'Remark',
|
||||
create_time: 'Created',
|
||||
update_time: 'Updated',
|
||||
view_commission_records: 'View commission records',
|
||||
}
|
||||
|
||||
|
||||
@@ -110,11 +110,12 @@ export default {
|
||||
settle_stats_pending_dividend: 'Channels pending dividend',
|
||||
settle_stats_pending_amount: 'Pending dividend amount',
|
||||
settle_stats_paid_dividend: 'Paid dividend amount',
|
||||
settle_stats_company_total_bet: 'Company total bet',
|
||||
direct_bet_amount: 'Direct bet amount',
|
||||
view_settlement_bet: 'View settlement bets',
|
||||
direct_bet_amount_tip: 'Total bets from all players under this channel (including unsettled)',
|
||||
company_bet_record_dialog_title: 'Company bet records',
|
||||
direct_bet_record_dialog_title: 'Direct player game bets',
|
||||
dividend_record_dialog_title: 'Paid dividend records',
|
||||
direct_bet_record_dialog_title: 'Direct player bet records',
|
||||
settlement_bet_record_dialog_title: 'Dividend-scope bet records',
|
||||
bet_record_period_no: 'Period No.',
|
||||
bet_record_user_username: 'Player',
|
||||
bet_record_total_amount: 'Bet amount',
|
||||
|
||||
@@ -15,10 +15,15 @@ export default {
|
||||
review_admin_username: 'Reviewer',
|
||||
remark: 'Remark',
|
||||
create_time: 'Create time',
|
||||
review_btn: 'Review',
|
||||
review_title: 'Withdraw review',
|
||||
review_btn_approve: 'Approve',
|
||||
review_btn_reject: 'Reject',
|
||||
review_btn_back: 'Back',
|
||||
review_btn_confirm_reject: 'Confirm reject',
|
||||
review_approve_title: 'Approve order',
|
||||
review_reject_title: 'Reject order',
|
||||
review_reject_tip: 'Rejected orders will unfreeze the amount back to the admin wallet',
|
||||
review_remark_optional: 'Optional review remark',
|
||||
reject_reason_required: 'Please enter reject reason',
|
||||
stat_total_count: 'Total orders',
|
||||
|
||||
@@ -29,6 +29,7 @@ export default {
|
||||
section_admin_attribution: 'Administrator',
|
||||
admin_affiliation: 'Assigned admin',
|
||||
admin_affiliation_placeholder: 'Role group tree — only admins in your scope',
|
||||
admin_no_channel_bound: 'The selected admin is not bound to a channel; bind a channel in Admin management before creating users',
|
||||
register_invite_code_auto_placeholder: 'Filled from selected admin invite code',
|
||||
channel_id: 'Channel',
|
||||
channel__name: 'Channel',
|
||||
|
||||
@@ -7,11 +7,16 @@ export default {
|
||||
channel_name: '渠道名称',
|
||||
admin_id: '代理管理员',
|
||||
admin_username: '代理账号',
|
||||
commission_rate: '佣金比例',
|
||||
calc_base_amount: '结算基数',
|
||||
commission_amount: '佣金金额(费前)',
|
||||
handling_fee: '手续费金额',
|
||||
share_rate: '分配比例',
|
||||
commission_rate: '有效比例',
|
||||
commission_amount: '佣金(费前)',
|
||||
commission_share_percent: '占比',
|
||||
handling_fee_rate: '手续费比例',
|
||||
handling_fee: '手续费',
|
||||
net_commission_amount: '实发佣金',
|
||||
filter_by_settlement_no: '当前筛选结算周期号:{no}',
|
||||
reset_settlement_filter: '重置筛选',
|
||||
status: '状态',
|
||||
'status 0': '待发放',
|
||||
'status 1': '已发放',
|
||||
|
||||
@@ -15,5 +15,6 @@ export default {
|
||||
remark: '备注',
|
||||
create_time: '创建时间',
|
||||
update_time: '更新时间',
|
||||
view_commission_records: '查看代理佣金记录',
|
||||
}
|
||||
|
||||
|
||||
@@ -109,11 +109,12 @@ export default {
|
||||
settle_stats_pending_dividend: '待分红渠道',
|
||||
settle_stats_pending_amount: '待分红总额',
|
||||
settle_stats_paid_dividend: '已分红金额',
|
||||
settle_stats_company_total_bet: '公司总投注额',
|
||||
direct_bet_amount: '直属投注额',
|
||||
view_settlement_bet: '查看总投注金额',
|
||||
direct_bet_amount_tip: '该渠道名下全部玩家的总投注额(含未结算)',
|
||||
company_bet_record_dialog_title: '公司总投注记录下注明细',
|
||||
direct_bet_record_dialog_title: '直属玩家游戏下注记录',
|
||||
dividend_record_dialog_title: '已分红记录',
|
||||
direct_bet_record_dialog_title: '直属玩家下注记录',
|
||||
settlement_bet_record_dialog_title: '分红口径下注记录',
|
||||
bet_record_period_no: '游戏期号',
|
||||
bet_record_user_username: '玩家名',
|
||||
bet_record_total_amount: '投注金额',
|
||||
|
||||
@@ -15,10 +15,15 @@ export default {
|
||||
review_admin_username: '审核人',
|
||||
remark: '备注',
|
||||
create_time: '创建时间',
|
||||
review_btn: '审核',
|
||||
review_title: '提现审核',
|
||||
review_btn_approve: '通过',
|
||||
review_btn_reject: '拒绝',
|
||||
review_btn_back: '返回',
|
||||
review_btn_confirm_reject: '确认拒绝',
|
||||
review_approve_title: '通过审核',
|
||||
review_reject_title: '拒绝审核',
|
||||
review_reject_tip: '拒绝后冻结金额将退回管理员钱包',
|
||||
review_remark_optional: '可选填写审核备注',
|
||||
reject_reason_required: '请填写拒绝原因',
|
||||
stat_total_count: '提现总单数',
|
||||
|
||||
@@ -29,6 +29,7 @@ export default {
|
||||
section_admin_attribution: '管理员归属',
|
||||
admin_affiliation: '归属管理员',
|
||||
admin_affiliation_placeholder: '按角色组展开,仅展示您可管理范围内的管理员',
|
||||
admin_no_channel_bound: '所选归属管理员未绑定渠道,无法创建用户,请先在管理员管理中绑定渠道',
|
||||
register_invite_code_auto_placeholder: '随所选管理员邀请码自动带出',
|
||||
channel_id: '所属渠道',
|
||||
channel__name: '渠道名',
|
||||
|
||||
@@ -2,6 +2,21 @@
|
||||
<div class="default-main ba-table-box">
|
||||
<el-alert class="ba-table-alert" v-if="baTable.table.remark" :title="baTable.table.remark" type="info" show-icon />
|
||||
|
||||
<el-alert
|
||||
v-if="settlementNoFilter"
|
||||
class="commission-record-filter-alert"
|
||||
type="warning"
|
||||
:closable="false"
|
||||
show-icon
|
||||
>
|
||||
<template #title>
|
||||
{{ t('agent.commissionRecord.filter_by_settlement_no', { no: settlementNoFilter }) }}
|
||||
<el-button link type="primary" class="commission-record-filter-reset" @click="resetSettlementNoFilter">
|
||||
{{ t('agent.commissionRecord.reset_settlement_filter') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-alert>
|
||||
|
||||
<TableHeader
|
||||
:buttons="['refresh', 'add', 'edit', 'delete', 'comSearch', 'quickSearch', 'columnDisplay']"
|
||||
:quick-search-placeholder="t('Quick search placeholder', { fields: t('agent.commissionRecord.quick Search Fields') })"
|
||||
@@ -13,7 +28,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, provide, useTemplateRef } from 'vue'
|
||||
import { computed, onMounted, provide, useTemplateRef } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import PopupForm from './popupForm.vue'
|
||||
import { baTableApi } from '/@/api/common'
|
||||
@@ -21,12 +37,16 @@ import { defaultOptButtons } from '/@/components/table'
|
||||
import TableHeader from '/@/components/table/header/index.vue'
|
||||
import Table from '/@/components/table/index.vue'
|
||||
import baTableClass from '/@/utils/baTable'
|
||||
import { routePush } from '/@/utils/router'
|
||||
|
||||
defineOptions({
|
||||
name: 'agent/commissionRecord',
|
||||
})
|
||||
|
||||
const SETTLEMENT_NO_PROP = 'settlementPeriod.settlement_no'
|
||||
|
||||
const { t } = useI18n()
|
||||
const route = useRoute()
|
||||
const tableRef = useTemplateRef('tableRef')
|
||||
const optButtons: OptButton[] = defaultOptButtons(['edit', 'delete'])
|
||||
|
||||
@@ -41,6 +61,33 @@ function formatAmount2(_row: anyObj, _column: any, cellValue: unknown) {
|
||||
return n.toFixed(2)
|
||||
}
|
||||
|
||||
function formatPercent(_row: anyObj, _column: any, cellValue: unknown) {
|
||||
if (cellValue === null || cellValue === undefined || cellValue === '') {
|
||||
return '-'
|
||||
}
|
||||
const n = Number(cellValue)
|
||||
if (!Number.isFinite(n)) {
|
||||
return String(cellValue)
|
||||
}
|
||||
return `${n.toFixed(2)}%`
|
||||
}
|
||||
|
||||
function formatNetCommission(row: anyObj) {
|
||||
const preset = row?.net_commission_amount
|
||||
if (preset !== null && preset !== undefined && preset !== '') {
|
||||
const n = Number(preset)
|
||||
if (Number.isFinite(n)) {
|
||||
return n.toFixed(2)
|
||||
}
|
||||
}
|
||||
const gross = Number(row?.commission_amount ?? 0)
|
||||
const fee = Number(row?.handling_fee ?? 0)
|
||||
if (!Number.isFinite(gross) || !Number.isFinite(fee)) {
|
||||
return '-'
|
||||
}
|
||||
return Math.max(0, gross - fee).toFixed(2)
|
||||
}
|
||||
|
||||
const baTable = new baTableClass(
|
||||
new baTableApi('/admin/agent.CommissionRecord/'),
|
||||
{
|
||||
@@ -58,7 +105,7 @@ const baTable = new baTableClass(
|
||||
},
|
||||
{
|
||||
label: t('agent.commissionRecord.settlement_period_no'),
|
||||
prop: 'settlementPeriod.settlement_no',
|
||||
prop: SETTLEMENT_NO_PROP,
|
||||
align: 'center',
|
||||
minWidth: 170,
|
||||
operator: 'LIKE',
|
||||
@@ -86,36 +133,70 @@ const baTable = new baTableClass(
|
||||
render: 'tags',
|
||||
},
|
||||
{
|
||||
label: t('agent.commissionRecord.commission_rate'),
|
||||
prop: 'commission_rate',
|
||||
label: t('agent.commissionRecord.calc_base_amount'),
|
||||
prop: 'calc_base_amount',
|
||||
align: 'center',
|
||||
minWidth: 100,
|
||||
operator: 'RANGE',
|
||||
formatter: formatAmount2,
|
||||
},
|
||||
{
|
||||
label: t('agent.commissionRecord.share_rate'),
|
||||
prop: 'share_rate',
|
||||
align: 'center',
|
||||
minWidth: 96,
|
||||
operator: 'RANGE',
|
||||
formatter: formatPercent,
|
||||
},
|
||||
{
|
||||
label: t('agent.commissionRecord.commission_amount'),
|
||||
prop: 'commission_amount',
|
||||
align: 'center',
|
||||
minWidth: 110,
|
||||
operator: 'RANGE',
|
||||
formatter: formatAmount2,
|
||||
},
|
||||
{
|
||||
label: t('agent.commissionRecord.calc_base_amount'),
|
||||
prop: 'calc_base_amount',
|
||||
label: t('agent.commissionRecord.commission_share_percent'),
|
||||
prop: 'commission_share_percent',
|
||||
align: 'center',
|
||||
minWidth: 120,
|
||||
minWidth: 88,
|
||||
operator: 'RANGE',
|
||||
formatter: formatAmount2,
|
||||
formatter: formatPercent,
|
||||
},
|
||||
{
|
||||
label: t('agent.commissionRecord.commission_amount'),
|
||||
prop: 'commission_amount',
|
||||
label: t('agent.commissionRecord.handling_fee_rate'),
|
||||
prop: 'handling_fee_rate',
|
||||
align: 'center',
|
||||
minWidth: 120,
|
||||
minWidth: 96,
|
||||
operator: 'RANGE',
|
||||
formatter: formatAmount2,
|
||||
formatter: formatPercent,
|
||||
show: false,
|
||||
},
|
||||
{
|
||||
label: t('agent.commissionRecord.handling_fee'),
|
||||
prop: 'handling_fee',
|
||||
align: 'center',
|
||||
minWidth: 96,
|
||||
operator: 'RANGE',
|
||||
formatter: formatAmount2,
|
||||
},
|
||||
{
|
||||
label: t('agent.commissionRecord.net_commission_amount'),
|
||||
prop: 'net_commission_amount',
|
||||
align: 'center',
|
||||
minWidth: 96,
|
||||
operator: 'RANGE',
|
||||
formatter: (row: anyObj) => formatNetCommission(row),
|
||||
},
|
||||
{
|
||||
label: t('agent.commissionRecord.commission_rate'),
|
||||
prop: 'commission_rate',
|
||||
align: 'center',
|
||||
minWidth: 100,
|
||||
operator: 'RANGE',
|
||||
formatter: formatAmount2,
|
||||
show: false,
|
||||
},
|
||||
{
|
||||
label: t('agent.commissionRecord.status'),
|
||||
@@ -162,6 +243,7 @@ const baTable = new baTableClass(
|
||||
width: 170,
|
||||
sortable: 'custom',
|
||||
timeFormat: 'yyyy-mm-dd hh:MM:ss',
|
||||
show: false,
|
||||
},
|
||||
{
|
||||
label: t('agent.commissionRecord.update_time'),
|
||||
@@ -173,6 +255,7 @@ const baTable = new baTableClass(
|
||||
width: 170,
|
||||
sortable: 'custom',
|
||||
timeFormat: 'yyyy-mm-dd hh:MM:ss',
|
||||
show: false,
|
||||
},
|
||||
{ label: t('Operate'), align: 'center', minWidth: 80, render: 'buttons', buttons: optButtons, operator: false, fixed: 'right' },
|
||||
],
|
||||
@@ -182,6 +265,24 @@ const baTable = new baTableClass(
|
||||
}
|
||||
)
|
||||
|
||||
const settlementNoFilter = computed(() => {
|
||||
const fromQuery = route.query[SETTLEMENT_NO_PROP]
|
||||
if (typeof fromQuery === 'string' && fromQuery.trim() !== '') {
|
||||
return fromQuery.trim()
|
||||
}
|
||||
const fromForm = baTable.comSearch.form[SETTLEMENT_NO_PROP]
|
||||
return typeof fromForm === 'string' && fromForm.trim() !== '' ? fromForm.trim() : ''
|
||||
})
|
||||
|
||||
const resetSettlementNoFilter = () => {
|
||||
baTable.comSearch.form[SETTLEMENT_NO_PROP] = ''
|
||||
baTable.setFilterSearchData(baTable.getComSearchData(), 'cover')
|
||||
if (route.query[SETTLEMENT_NO_PROP]) {
|
||||
void routePush({ path: route.path, query: {} })
|
||||
}
|
||||
baTable.onTableHeaderAction('refresh', { event: 'reset-settlement-filter' })
|
||||
}
|
||||
|
||||
provide('baTable', baTable)
|
||||
|
||||
onMounted(() => {
|
||||
@@ -194,4 +295,13 @@ onMounted(() => {
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
<style scoped lang="scss">
|
||||
.commission-record-filter-alert {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.commission-record-filter-reset {
|
||||
margin-left: 8px;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -21,14 +21,43 @@ import { defaultOptButtons } from '/@/components/table'
|
||||
import TableHeader from '/@/components/table/header/index.vue'
|
||||
import Table from '/@/components/table/index.vue'
|
||||
import baTableClass from '/@/utils/baTable'
|
||||
import { auth } from '/@/utils/common'
|
||||
import { routePush } from '/@/utils/router'
|
||||
|
||||
defineOptions({
|
||||
name: 'agent/settlementPeriod',
|
||||
})
|
||||
|
||||
const SETTLEMENT_NO_PROP = 'settlementPeriod.settlement_no'
|
||||
|
||||
const { t } = useI18n()
|
||||
const tableRef = useTemplateRef('tableRef')
|
||||
const optButtons: OptButton[] = defaultOptButtons(['edit', 'delete'])
|
||||
const optButtons: OptButton[] = [
|
||||
{
|
||||
render: 'tipButton',
|
||||
name: 'viewCommissionRecords',
|
||||
title: 'agent.settlementPeriod.view_commission_records',
|
||||
text: '',
|
||||
type: 'primary',
|
||||
icon: 'fa fa-list-alt',
|
||||
class: 'table-row-view-commission-records',
|
||||
disabledTip: false,
|
||||
display: () => auth('viewCommissionRecords'),
|
||||
click: (row: TableRow) => {
|
||||
const settlementNo = String(row?.settlement_no ?? '').trim()
|
||||
if (settlementNo === '') {
|
||||
return
|
||||
}
|
||||
void routePush({
|
||||
path: '/admin/agent/commissionRecord',
|
||||
query: {
|
||||
[SETTLEMENT_NO_PROP]: settlementNo,
|
||||
},
|
||||
})
|
||||
},
|
||||
},
|
||||
...defaultOptButtons(['edit', 'delete']),
|
||||
]
|
||||
|
||||
function formatAmount2(_row: anyObj, _column: any, cellValue: unknown) {
|
||||
if (cellValue === null || cellValue === undefined || cellValue === '') {
|
||||
@@ -149,7 +178,7 @@ const baTable = new baTableClass(
|
||||
sortable: 'custom',
|
||||
timeFormat: 'yyyy-mm-dd hh:MM:ss',
|
||||
},
|
||||
{ label: t('Operate'), align: 'center', minWidth: 80, render: 'buttons', buttons: optButtons, operator: false, fixed: 'right' },
|
||||
{ label: t('Operate'), align: 'center', minWidth: 120, render: 'buttons', buttons: optButtons, operator: false, fixed: 'right' },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -27,7 +27,16 @@
|
||||
<el-card
|
||||
shadow="never"
|
||||
class="channel-stat-card"
|
||||
:class="{ 'channel-stat-card-clickable': auth('viewDividendRecords') }"
|
||||
:class="{ 'channel-stat-card-clickable': auth('index') }"
|
||||
@click="onCompanyTotalBetCardClick"
|
||||
>
|
||||
<div class="label">{{ t('channel.settle_stats_company_total_bet') }}</div>
|
||||
<div class="value">{{ settleStats.company_total_bet_amount }}</div>
|
||||
</el-card>
|
||||
<el-card
|
||||
shadow="never"
|
||||
class="channel-stat-card"
|
||||
:class="{ 'channel-stat-card-clickable': auth('index') }"
|
||||
@click="onPaidDividendCardClick"
|
||||
>
|
||||
<div class="label">{{ t('channel.settle_stats_paid_dividend') }}</div>
|
||||
@@ -67,40 +76,49 @@
|
||||
<div class="title">{{ t('channel.manual_settle') }}</div>
|
||||
</template>
|
||||
<div v-loading="manualSettle.previewLoading" class="manual-settle-dialog-body">
|
||||
<el-descriptions
|
||||
class="manual-settle-summary"
|
||||
:column="manualSettleSummaryColumns"
|
||||
border
|
||||
:size="manualSettleViewportMobile ? 'small' : 'default'"
|
||||
>
|
||||
<el-descriptions-item :label="t('channel.manual_settle_settlement_no')">
|
||||
{{ manualSettle.form.settlement_no || '-' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item :label="t('channel.manual_settle_period_start')">
|
||||
{{ manualSettle.form.period_start_at || '-' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item :label="t('channel.manual_settle_period_end')">
|
||||
{{ manualSettle.form.period_end_at || '-' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item :label="t('channel.manual_settle_total_bet')">
|
||||
{{ manualSettle.form.total_bet_amount || '-' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item :label="t('channel.manual_settle_total_payout')">
|
||||
{{ manualSettle.form.total_payout_amount || '-' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item :label="t('channel.manual_settle_platform_profit')">
|
||||
{{ manualSettle.form.platform_profit_amount || '-' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item :label="t('channel.manual_settle_commission_rate')">
|
||||
{{ manualSettle.form.commission_rate || '-' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item :label="t('channel.manual_settle_calc_base')">
|
||||
{{ manualSettle.form.calc_base_amount || '-' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item :label="t('channel.manual_settle_commission_amount')">
|
||||
<span class="manual-settle-summary-highlight">{{ manualSettle.form.commission_amount || '-' }}</span>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<el-form
|
||||
:model="manualSettle.form"
|
||||
:label-width="manualSettleFormLabelWidth"
|
||||
:label-position="manualSettleFormLabelPosition"
|
||||
label-position="top"
|
||||
:size="manualSettleFormSize"
|
||||
class="manual-settle-form"
|
||||
>
|
||||
<el-form-item :label="t('channel.manual_settle_settlement_no')">
|
||||
<el-input v-model="manualSettle.form.settlement_no" readonly />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('channel.manual_settle_period_start')">
|
||||
<el-input v-model="manualSettle.form.period_start_at" readonly />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('channel.manual_settle_period_end')">
|
||||
<el-input v-model="manualSettle.form.period_end_at" readonly />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('channel.manual_settle_total_bet')">
|
||||
<el-input v-model="manualSettle.form.total_bet_amount" readonly />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('channel.manual_settle_total_payout')">
|
||||
<el-input v-model="manualSettle.form.total_payout_amount" readonly />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('channel.manual_settle_platform_profit')">
|
||||
<el-input v-model="manualSettle.form.platform_profit_amount" readonly />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('channel.manual_settle_commission_rate')">
|
||||
<el-input v-model="manualSettle.form.commission_rate" readonly />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('channel.manual_settle_calc_base')">
|
||||
<el-input v-model="manualSettle.form.calc_base_amount" readonly />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('channel.manual_settle_commission_amount')">
|
||||
<el-input v-model="manualSettle.form.commission_amount" readonly />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('channel.share_config')" class="manual-settle-form-item-full">
|
||||
<el-form-item :label="t('channel.share_config')" class="manual-settle-form-item-full manual-settle-split-form-item">
|
||||
<div class="manual-settle-split-block">
|
||||
<div
|
||||
class="manual-settle-split-table-scroll"
|
||||
@@ -117,7 +135,7 @@
|
||||
:tree-props="{ children: 'children' }"
|
||||
default-expand-all
|
||||
border
|
||||
size="small"
|
||||
:size="manualSettleTableSize"
|
||||
:fit="!manualSettleViewportMobile"
|
||||
class="manual-settle-split-table"
|
||||
:class="{ 'is-mobile': manualSettleViewportMobile, 'w100': !manualSettleViewportMobile }"
|
||||
@@ -180,17 +198,13 @@
|
||||
{{ t('channel.manual_settle_split_scroll_tip') }}
|
||||
</p>
|
||||
</div>
|
||||
<el-alert
|
||||
class="manual-settle-calc-alert"
|
||||
:title="t('channel.manual_settle_calc_title')"
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
>
|
||||
<ul class="manual-settle-calc-list">
|
||||
<li v-for="(line, idx) in manualSettleCalcDescLines" :key="idx">{{ line }}</li>
|
||||
</ul>
|
||||
</el-alert>
|
||||
<el-collapse v-model="manualSettleCalcCollapse" class="manual-settle-calc-collapse">
|
||||
<el-collapse-item :title="t('channel.manual_settle_calc_title')" name="calc">
|
||||
<ul class="manual-settle-calc-list">
|
||||
<li v-for="(line, idx) in manualSettleCalcDescLines" :key="idx">{{ line }}</li>
|
||||
</ul>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('channel.manual_settle_remark')" class="manual-settle-form-item-full">
|
||||
@@ -348,6 +362,7 @@
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column
|
||||
v-if="betRecordDialog.mode === 'company'"
|
||||
prop="channel_name"
|
||||
:label="t('channel.bet_record_channel_name')"
|
||||
min-width="100"
|
||||
@@ -441,9 +456,12 @@ const { t } = useI18n()
|
||||
|
||||
const MANUAL_SETTLE_MOBILE_BREAKPOINT = 768
|
||||
const manualSettleViewportMobile = ref(typeof window !== 'undefined' ? window.innerWidth <= MANUAL_SETTLE_MOBILE_BREAKPOINT : false)
|
||||
const manualSettleDialogWidth = computed(() => (manualSettleViewportMobile.value ? '96%' : '860px'))
|
||||
const manualSettleFormLabelPosition = computed(() => (manualSettleViewportMobile.value ? 'top' : 'right'))
|
||||
const manualSettleFormLabelWidth = computed(() => (manualSettleViewportMobile.value ? 'auto' : '140px'))
|
||||
const manualSettleCalcCollapse = ref<string[]>([])
|
||||
const manualSettleDialogWidth = computed(() => (manualSettleViewportMobile.value ? '96%' : '920px'))
|
||||
const manualSettleSummaryColumns = computed(() => (manualSettleViewportMobile.value ? 1 : 3))
|
||||
const manualSettleFormSize = computed(() => (manualSettleViewportMobile.value ? 'small' : 'default'))
|
||||
const manualSettleTableSize = computed(() => (manualSettleViewportMobile.value ? 'small' : 'default'))
|
||||
const manualSettleFormLabelWidth = computed(() => 'auto')
|
||||
const manualSettleTableLayout = computed(() => 'fixed')
|
||||
const manualSettleSplitCol = computed(() => {
|
||||
if (manualSettleViewportMobile.value) {
|
||||
@@ -458,13 +476,13 @@ const manualSettleSplitCol = computed(() => {
|
||||
}
|
||||
}
|
||||
return {
|
||||
adminWidth: 108,
|
||||
baseWidth: 76,
|
||||
shareWidth: 58,
|
||||
grossWidth: 72,
|
||||
commissionShareWidth: 62,
|
||||
feeWidth: 64,
|
||||
netWidth: 72,
|
||||
adminWidth: 132,
|
||||
baseWidth: 88,
|
||||
shareWidth: 72,
|
||||
grossWidth: 88,
|
||||
commissionShareWidth: 76,
|
||||
feeWidth: 80,
|
||||
netWidth: 88,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -510,20 +528,6 @@ const dismissFloatingTooltips = () => {
|
||||
}
|
||||
const tableRef = useTemplateRef('tableRef')
|
||||
let optButtons: OptButton[] = [
|
||||
{
|
||||
render: 'tipButton',
|
||||
name: 'viewSettlementBet',
|
||||
title: 'channel.view_settlement_bet',
|
||||
text: '',
|
||||
type: 'info',
|
||||
icon: 'fa fa-list-alt',
|
||||
class: 'table-row-view-settlement-bet',
|
||||
disabledTip: false,
|
||||
display: () => auth('viewSettlementBetRecords'),
|
||||
click: (row: TableRow) => {
|
||||
void openBetRecordDialog('settlement', row)
|
||||
},
|
||||
},
|
||||
{
|
||||
render: 'tipButton',
|
||||
name: 'manualSettle',
|
||||
@@ -650,6 +654,7 @@ const settleStats = reactive({
|
||||
carryover_total: '0.00',
|
||||
carryover_positive_total: '0.00',
|
||||
paid_dividend_total: '0.00',
|
||||
company_total_bet_amount: '0.00',
|
||||
})
|
||||
|
||||
const dividendDialog = reactive({
|
||||
@@ -672,7 +677,7 @@ const createBetRecordFilters = () => ({
|
||||
const betRecordDialog = reactive({
|
||||
visible: false,
|
||||
loading: false,
|
||||
mode: '' as '' | 'direct' | 'settlement',
|
||||
mode: '' as '' | 'direct' | 'company',
|
||||
channelId: 0,
|
||||
title: '',
|
||||
page: 1,
|
||||
@@ -944,6 +949,7 @@ const loadSettleStats = async () => {
|
||||
settleStats.carryover_total = String(res.data.carryover_total ?? '0.00')
|
||||
settleStats.carryover_positive_total = String(res.data.carryover_positive_total ?? '0.00')
|
||||
settleStats.paid_dividend_total = String(res.data.paid_dividend_total ?? '0.00')
|
||||
settleStats.company_total_bet_amount = String(res.data.company_total_bet_amount ?? '0.00')
|
||||
}
|
||||
|
||||
const loadDividendRecords = async () => {
|
||||
@@ -965,7 +971,7 @@ const loadDividendRecords = async () => {
|
||||
}
|
||||
|
||||
const onPaidDividendCardClick = () => {
|
||||
if (!auth('viewDividendRecords')) {
|
||||
if (!auth('index')) {
|
||||
return
|
||||
}
|
||||
dividendDialog.page = 1
|
||||
@@ -973,6 +979,18 @@ const onPaidDividendCardClick = () => {
|
||||
void loadDividendRecords()
|
||||
}
|
||||
|
||||
const onCompanyTotalBetCardClick = () => {
|
||||
if (!auth('index')) {
|
||||
return
|
||||
}
|
||||
resetBetRecordDialog()
|
||||
betRecordDialog.mode = 'company'
|
||||
betRecordDialog.channelId = 0
|
||||
betRecordDialog.title = t('channel.company_bet_record_dialog_title')
|
||||
betRecordDialog.visible = true
|
||||
void loadBetRecords()
|
||||
}
|
||||
|
||||
const closeDividendDialog = () => {
|
||||
dividendDialog.visible = false
|
||||
dividendDialog.list = []
|
||||
@@ -1022,22 +1040,30 @@ const buildBetRecordFilterParams = () => {
|
||||
}
|
||||
|
||||
const loadBetRecords = async () => {
|
||||
if (!betRecordDialog.channelId || !betRecordDialog.mode) {
|
||||
if (!betRecordDialog.mode) {
|
||||
return
|
||||
}
|
||||
if (betRecordDialog.mode === 'direct' && !betRecordDialog.channelId) {
|
||||
return
|
||||
}
|
||||
const url =
|
||||
betRecordDialog.mode === 'direct' ? '/admin/channel/directBetRecordList' : '/admin/channel/settlementBetRecordList'
|
||||
betRecordDialog.mode === 'direct'
|
||||
? '/admin/channel/directBetRecordList'
|
||||
: '/admin/channel/companyBetRecordList'
|
||||
betRecordDialog.loading = true
|
||||
try {
|
||||
const params: Record<string, string | number> = {
|
||||
page: betRecordDialog.page,
|
||||
limit: betRecordDialog.limit,
|
||||
...buildBetRecordFilterParams(),
|
||||
}
|
||||
if (betRecordDialog.mode === 'direct') {
|
||||
params.channel_id = betRecordDialog.channelId
|
||||
}
|
||||
const res = await createAxios({
|
||||
url,
|
||||
method: 'get',
|
||||
params: {
|
||||
channel_id: betRecordDialog.channelId,
|
||||
page: betRecordDialog.page,
|
||||
limit: betRecordDialog.limit,
|
||||
...buildBetRecordFilterParams(),
|
||||
},
|
||||
params,
|
||||
})
|
||||
if (res.code !== 1 || !res.data) {
|
||||
return
|
||||
@@ -1055,9 +1081,8 @@ const loadBetRecords = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const openBetRecordDialog = (mode: 'direct' | 'settlement', row: TableRow) => {
|
||||
const permission = mode === 'direct' ? 'viewDirectBetRecords' : 'viewSettlementBetRecords'
|
||||
if (!auth(permission)) {
|
||||
const openBetRecordDialog = (mode: 'direct' | 'company', row: TableRow) => {
|
||||
if (!auth('index')) {
|
||||
return
|
||||
}
|
||||
resetBetRecordDialog()
|
||||
@@ -1066,7 +1091,7 @@ const openBetRecordDialog = (mode: 'direct' | 'settlement', row: TableRow) => {
|
||||
betRecordDialog.title =
|
||||
mode === 'direct'
|
||||
? `${t('channel.direct_bet_record_dialog_title')} - ${row.name ?? row.id}`
|
||||
: `${t('channel.settlement_bet_record_dialog_title')} - ${row.name ?? row.id}`
|
||||
: t('channel.company_bet_record_dialog_title')
|
||||
betRecordDialog.visible = true
|
||||
void loadBetRecords()
|
||||
}
|
||||
@@ -1226,8 +1251,8 @@ const baTable = new baTableClass(
|
||||
formatter: formatAmount2,
|
||||
customRenderAttr: {
|
||||
tag: ({ row }: { row: TableRow }) => ({
|
||||
class: auth('viewDirectBetRecords') ? 'channel-direct-bet-tag' : '',
|
||||
onClick: auth('viewDirectBetRecords') ? () => openDirectBetDialog(row) : undefined,
|
||||
class: auth('index') ? 'channel-direct-bet-tag' : '',
|
||||
onClick: auth('index') ? () => openDirectBetDialog(row) : undefined,
|
||||
}),
|
||||
},
|
||||
},
|
||||
@@ -1374,11 +1399,20 @@ onUnmounted(() => {
|
||||
|
||||
.channel-stats-cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
grid-template-columns: repeat(6, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.channel-stats-cards :deep(.el-card__body) {
|
||||
padding: 10px 8px;
|
||||
}
|
||||
|
||||
.channel-stat-card {
|
||||
text-align: center;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.channel-stat-card-clickable {
|
||||
cursor: pointer;
|
||||
transition: box-shadow 0.2s ease;
|
||||
@@ -1460,15 +1494,18 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
.channel-stat-card .label {
|
||||
font-size: 12px;
|
||||
font-size: 11px;
|
||||
color: var(--el-text-color-secondary);
|
||||
line-height: 1.3;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.channel-stat-card .value {
|
||||
margin-top: 6px;
|
||||
font-size: 20px;
|
||||
margin-top: 4px;
|
||||
font-size: 17px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.channel-action-row {
|
||||
@@ -1484,29 +1521,34 @@ onUnmounted(() => {
|
||||
display: flex !important;
|
||||
flex-direction: column;
|
||||
width: 92% !important;
|
||||
max-width: 860px;
|
||||
max-height: 92vh;
|
||||
margin: 4vh auto !important;
|
||||
max-width: 920px;
|
||||
max-height: 90vh;
|
||||
margin: 5vh auto !important;
|
||||
padding-bottom: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
:deep(.manual-settle-dialog .el-dialog__header) {
|
||||
flex-shrink: 0;
|
||||
padding: 12px 16px;
|
||||
padding: 14px 20px;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
:deep(.manual-settle-dialog .el-dialog__header .title) {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
:deep(.manual-settle-dialog .el-dialog__body) {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
height: auto !important;
|
||||
max-height: calc(92vh - 120px);
|
||||
max-height: calc(90vh - 116px);
|
||||
overflow-y: auto !important;
|
||||
overflow-x: hidden;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
overscroll-behavior: contain;
|
||||
padding: 12px 16px 16px;
|
||||
padding: 16px 20px 18px;
|
||||
}
|
||||
|
||||
:deep(.manual-settle-dialog .el-dialog__footer) {
|
||||
@@ -1526,16 +1568,45 @@ onUnmounted(() => {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.manual-settle-summary {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.manual-settle-summary :deep(.el-descriptions__label) {
|
||||
width: 148px;
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-secondary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.manual-settle-summary :deep(.el-descriptions__content) {
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.manual-settle-summary-highlight {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.manual-settle-form {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
column-gap: 16px;
|
||||
display: block;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.manual-settle-form :deep(.el-form-item) {
|
||||
margin-bottom: 12px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.manual-settle-split-form-item :deep(.el-form-item__label) {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 1.5;
|
||||
padding-bottom: 8px;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.manual-settle-form-item-full {
|
||||
@@ -1614,19 +1685,21 @@ onUnmounted(() => {
|
||||
|
||||
.manual-settle-split-table :deep(.el-table__header th.el-table__cell),
|
||||
.manual-settle-split-table :deep(.el-table__body td.el-table__cell) {
|
||||
padding: 6px 4px;
|
||||
padding: 10px 8px;
|
||||
}
|
||||
|
||||
.manual-settle-split-table :deep(.el-table__header .cell),
|
||||
.manual-settle-split-table :deep(.el-table__body .cell) {
|
||||
padding: 0 2px;
|
||||
font-size: 12px;
|
||||
line-height: 1.35;
|
||||
word-break: break-all;
|
||||
padding: 0 4px;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.manual-settle-split-table :deep(.el-table__header .cell) {
|
||||
white-space: normal;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.manual-settle-split-table.is-mobile :deep(.el-table__header th.el-table__cell),
|
||||
@@ -1648,15 +1721,36 @@ onUnmounted(() => {
|
||||
word-break: keep-all;
|
||||
}
|
||||
|
||||
.manual-settle-calc-alert {
|
||||
.manual-settle-calc-collapse {
|
||||
margin-top: 12px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.manual-settle-calc-collapse :deep(.el-collapse-item__header) {
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
border-bottom: none;
|
||||
background: var(--el-fill-color-light);
|
||||
border-radius: 6px;
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.manual-settle-calc-collapse :deep(.el-collapse-item__wrap) {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.manual-settle-calc-collapse :deep(.el-collapse-item__content) {
|
||||
padding: 10px 12px 4px;
|
||||
}
|
||||
|
||||
.manual-settle-calc-list {
|
||||
margin: 0;
|
||||
padding-left: 18px;
|
||||
line-height: 1.6;
|
||||
padding-left: 20px;
|
||||
line-height: 1.65;
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
|
||||
.manual-settle-calc-list li + li {
|
||||
@@ -1666,7 +1760,12 @@ onUnmounted(() => {
|
||||
.manual-settle-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
gap: 10px;
|
||||
padding: 4px 0 0;
|
||||
}
|
||||
|
||||
.manual-settle-footer .el-button {
|
||||
min-width: 88px;
|
||||
}
|
||||
|
||||
/* 渠道记录弹窗:可滚动、适配移动端(勿使用 ba-operate-dialog 固定高度) */
|
||||
@@ -1725,8 +1824,13 @@ onUnmounted(() => {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.manual-settle-form {
|
||||
grid-template-columns: 1fr;
|
||||
.manual-settle-summary :deep(.el-descriptions__label) {
|
||||
width: auto;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.manual-settle-summary :deep(.el-descriptions__content) {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1744,20 +1848,24 @@ onUnmounted(() => {
|
||||
|
||||
:deep(.manual-settle-dialog .el-dialog__body) {
|
||||
max-height: calc(94vh - 96px);
|
||||
padding: 10px 10px 12px;
|
||||
padding: 12px 12px 14px;
|
||||
}
|
||||
|
||||
:deep(.manual-settle-dialog .el-dialog__footer) {
|
||||
padding: 8px 10px 10px;
|
||||
}
|
||||
|
||||
.manual-settle-summary {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.manual-settle-form :deep(.el-form-item) {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.manual-settle-form :deep(.el-form-item__label) {
|
||||
padding-bottom: 4px;
|
||||
line-height: 1.4;
|
||||
.manual-settle-split-form-item :deep(.el-form-item__label) {
|
||||
font-size: 13px;
|
||||
padding-bottom: 6px;
|
||||
}
|
||||
|
||||
.manual-settle-form-item-full :deep(.el-form-item__content) {
|
||||
@@ -1778,22 +1886,12 @@ onUnmounted(() => {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.manual-settle-calc-alert {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.manual-settle-calc-alert :deep(.el-alert__title) {
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.manual-settle-calc-alert :deep(.el-alert__content) {
|
||||
max-height: none;
|
||||
overflow: visible;
|
||||
.manual-settle-calc-collapse :deep(.el-collapse-item__header) {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.manual-settle-calc-list {
|
||||
font-size: 12px;
|
||||
font-size: 11px;
|
||||
padding-left: 16px;
|
||||
}
|
||||
|
||||
|
||||
@@ -38,18 +38,20 @@
|
||||
</el-select>
|
||||
</div>
|
||||
<Table ref="tableRef"></Table>
|
||||
<PopupForm />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, provide, reactive, ref, useTemplateRef } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
import { baTableApi } from '/@/api/common'
|
||||
import { auth } from '/@/utils/common'
|
||||
import TableHeader from '/@/components/table/header/index.vue'
|
||||
import Table from '/@/components/table/index.vue'
|
||||
import baTableClass from '/@/utils/baTable'
|
||||
import createAxios from '/@/utils/axios'
|
||||
import PopupForm from './popupForm.vue'
|
||||
|
||||
defineOptions({
|
||||
name: 'order/adminWithdrawOrder',
|
||||
@@ -69,50 +71,29 @@ const stats = reactive({
|
||||
approved_amount: '0.00',
|
||||
})
|
||||
|
||||
const onReview = async (row: anyObj, action: 'approve' | 'reject') => {
|
||||
const needReason = action === 'reject'
|
||||
const { value } = await ElMessageBox.prompt(
|
||||
needReason ? t('order.adminWithdrawOrder.reject_reason_required') : t('order.adminWithdrawOrder.review_remark_optional'),
|
||||
action === 'approve' ? t('order.adminWithdrawOrder.review_approve_title') : t('order.adminWithdrawOrder.review_reject_title'),
|
||||
{
|
||||
confirmButtonText: t('Confirm'),
|
||||
cancelButtonText: t('Cancel'),
|
||||
inputPlaceholder: needReason ? t('order.adminWithdrawOrder.reject_reason_required') : t('order.adminWithdrawOrder.review_remark_optional'),
|
||||
inputPattern: needReason ? /.+/ : undefined,
|
||||
inputErrorMessage: needReason ? t('order.adminWithdrawOrder.reject_reason_required') : '',
|
||||
})
|
||||
await createAxios(
|
||||
{
|
||||
url: `/admin/order.AdminWithdrawOrder/${action}`,
|
||||
method: 'post',
|
||||
data: { id: row.id, remark: value || '' },
|
||||
},
|
||||
{ showSuccessMessage: true }
|
||||
)
|
||||
await loadStats()
|
||||
baTable.getData()
|
||||
}
|
||||
|
||||
const optButtons: OptButton[] = [
|
||||
{
|
||||
render: 'tipButton',
|
||||
name: 'approve',
|
||||
title: 'order.adminWithdrawOrder.review_btn_approve',
|
||||
name: 'review',
|
||||
title: 'order.adminWithdrawOrder.review_btn',
|
||||
text: '',
|
||||
type: 'success',
|
||||
icon: 'el-icon-Check',
|
||||
display: (row: TableRow) => Number(row.status) === 0 && Number((row as anyObj).can_review ?? 0) === 1,
|
||||
click: (row: TableRow) => void onReview(row as anyObj, 'approve'),
|
||||
},
|
||||
{
|
||||
render: 'tipButton',
|
||||
name: 'reject',
|
||||
title: 'order.adminWithdrawOrder.review_btn_reject',
|
||||
text: '',
|
||||
type: 'danger',
|
||||
icon: 'el-icon-Close',
|
||||
display: (row: TableRow) => Number(row.status) === 0 && Number((row as anyObj).can_review ?? 0) === 1,
|
||||
click: (row: TableRow) => void onReview(row as anyObj, 'reject'),
|
||||
type: 'warning',
|
||||
icon: 'fa fa-check-square-o',
|
||||
display: (row: TableRow) => {
|
||||
if (Number(row.status) !== 0) {
|
||||
return false
|
||||
}
|
||||
if (auth('review')) {
|
||||
return true
|
||||
}
|
||||
return Number((row as anyObj).can_review ?? 0) === 1
|
||||
},
|
||||
click: (row: TableRow) => {
|
||||
baTable.form.operate = 'Review'
|
||||
baTable.form.operateIds = [String(row[baTable.table.pk!])]
|
||||
baTable.form.items = { ...(row as anyObj) }
|
||||
baTable.form.loading = false
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
@@ -179,7 +160,9 @@ const baTable = new baTableClass(
|
||||
{ label: t('Operate'), align: 'center', width: 90, render: 'buttons', buttons: optButtons, operator: false, fixed: 'right' },
|
||||
],
|
||||
},
|
||||
{}
|
||||
{
|
||||
defaultItems: {},
|
||||
}
|
||||
)
|
||||
|
||||
const onStatusFilterChange = () => {
|
||||
@@ -274,4 +257,3 @@ onMounted(() => {
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
306
web/src/views/backend/order/adminWithdrawOrder/popupForm.vue
Normal file
306
web/src/views/backend/order/adminWithdrawOrder/popupForm.vue
Normal file
@@ -0,0 +1,306 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
class="ba-operate-dialog admin-withdraw-review-dialog"
|
||||
:close-on-click-modal="false"
|
||||
:model-value="isOpen"
|
||||
width="560px"
|
||||
@close="onDialogClose"
|
||||
>
|
||||
<template #header>
|
||||
<div class="title" v-drag="['.ba-operate-dialog', '.el-dialog__header']" v-zoom="'.ba-operate-dialog'">
|
||||
{{ step === 'reject' ? t('order.adminWithdrawOrder.review_reject_title') : t('order.adminWithdrawOrder.review_title') }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-scrollbar v-loading="loading" class="ba-table-form-scrollbar">
|
||||
<div
|
||||
class="ba-operate-form ba-edit-form"
|
||||
:style="config.layout.shrink ? '' : 'width: calc(100% - ' + (baTable.form.labelWidth ?? 120) / 2 + 'px)'"
|
||||
>
|
||||
<el-form
|
||||
v-if="!loading && step === 'review'"
|
||||
:label-position="config.layout.shrink ? 'top' : 'right'"
|
||||
:label-width="(baTable.form.labelWidth ?? 120) + 'px'"
|
||||
@submit.prevent=""
|
||||
>
|
||||
<el-form-item :label="t('order.adminWithdrawOrder.order_no')">
|
||||
<el-input v-model="form.order_no" readonly />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('order.adminWithdrawOrder.admin_username')">
|
||||
<el-input :model-value="form.admin_text" readonly />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('order.adminWithdrawOrder.channel_name')">
|
||||
<el-input :model-value="form.channel_text" readonly />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('order.adminWithdrawOrder.amount')">
|
||||
<el-input :model-value="form.amount_text" readonly />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('order.adminWithdrawOrder.actual_amount')">
|
||||
<el-input :model-value="form.actual_amount_text" readonly />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('order.adminWithdrawOrder.receive_type')">
|
||||
<el-input :model-value="form.receive_type_text" readonly />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('order.adminWithdrawOrder.receive_account')">
|
||||
<el-input :model-value="form.receive_account" readonly />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('order.adminWithdrawOrder.remark')">
|
||||
<el-input :model-value="form.remark" type="textarea" :rows="2" readonly />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-form
|
||||
v-if="!loading && step === 'reject'"
|
||||
ref="rejectFormRef"
|
||||
:model="rejectForm"
|
||||
:rules="rejectRules"
|
||||
:label-position="config.layout.shrink ? 'top' : 'right'"
|
||||
:label-width="(baTable.form.labelWidth ?? 120) + 'px'"
|
||||
@submit.prevent=""
|
||||
>
|
||||
<el-alert
|
||||
class="review-reject-hint"
|
||||
type="warning"
|
||||
show-icon
|
||||
:closable="false"
|
||||
:title="t('order.adminWithdrawOrder.review_reject_tip')"
|
||||
/>
|
||||
<el-form-item :label="t('order.adminWithdrawOrder.order_no')">
|
||||
<el-input v-model="form.order_no" readonly />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('order.adminWithdrawOrder.remark')" prop="remark">
|
||||
<el-input
|
||||
v-model="rejectForm.remark"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
maxlength="255"
|
||||
show-word-limit
|
||||
:placeholder="t('order.adminWithdrawOrder.reject_reason_required')"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
|
||||
<template #footer>
|
||||
<div class="review-footer">
|
||||
<template v-if="step === 'review'">
|
||||
<el-button @click="onDialogClose">{{ t('Cancel') }}</el-button>
|
||||
<el-button type="danger" :loading="submitting" @click="gotoReject">{{ t('order.adminWithdrawOrder.review_btn_reject') }}</el-button>
|
||||
<el-button type="primary" v-blur :loading="submitting" @click="submitApprove">{{ t('order.adminWithdrawOrder.review_btn_approve') }}</el-button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-button @click="backToReview">{{ t('order.adminWithdrawOrder.review_btn_back') }}</el-button>
|
||||
<el-button type="danger" v-blur :loading="submitting" @click="submitReject">{{ t('order.adminWithdrawOrder.review_btn_confirm_reject') }}</el-button>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { FormInstance, FormItemRule } from 'element-plus'
|
||||
import { computed, inject, reactive, ref, useTemplateRef, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import createAxios from '/@/utils/axios'
|
||||
import { useConfig } from '/@/stores/config'
|
||||
import type baTableClass from '/@/utils/baTable'
|
||||
|
||||
const config = useConfig()
|
||||
const baTable = inject('baTable') as baTableClass
|
||||
const { t } = useI18n()
|
||||
|
||||
const rejectFormRef = useTemplateRef<FormInstance>('rejectFormRef')
|
||||
|
||||
type Step = 'review' | 'reject'
|
||||
const step = ref<Step>('review')
|
||||
const loading = ref(false)
|
||||
const submitting = ref(false)
|
||||
|
||||
const form = reactive({
|
||||
id: 0,
|
||||
order_no: '',
|
||||
admin_text: '-',
|
||||
channel_text: '-',
|
||||
amount_text: '0.00',
|
||||
actual_amount_text: '0.00',
|
||||
receive_type_text: '-',
|
||||
receive_account: '-',
|
||||
remark: '',
|
||||
})
|
||||
|
||||
const rejectForm = reactive({
|
||||
remark: '',
|
||||
})
|
||||
|
||||
const receiveTypeLabels: Record<string, string> = {
|
||||
bank: 'order.adminWithdrawOrder.receive_type_bank',
|
||||
ewallet: 'order.adminWithdrawOrder.receive_type_ewallet',
|
||||
crypto: 'order.adminWithdrawOrder.receive_type_crypto',
|
||||
}
|
||||
|
||||
const isOpen = computed(() => ['Review'].includes(baTable.form.operate ?? ''))
|
||||
|
||||
watch(isOpen, (visible) => {
|
||||
if (!visible) {
|
||||
return
|
||||
}
|
||||
step.value = 'review'
|
||||
rejectForm.remark = ''
|
||||
})
|
||||
|
||||
watch(
|
||||
() => ({ visible: isOpen.value, loadingState: baTable.form.loading, items: baTable.form.items }),
|
||||
({ visible, loadingState }) => {
|
||||
if (!visible) {
|
||||
return
|
||||
}
|
||||
loading.value = loadingState === true
|
||||
if (loadingState) {
|
||||
return
|
||||
}
|
||||
hydrate()
|
||||
},
|
||||
{ deep: true, immediate: true }
|
||||
)
|
||||
|
||||
const hydrate = () => {
|
||||
const row = baTable.form.items as Record<string, unknown> | undefined
|
||||
if (!row || !row['id']) {
|
||||
return
|
||||
}
|
||||
form.id = Number(row['id'] ?? 0)
|
||||
form.order_no = String(row['order_no'] ?? '')
|
||||
form.amount_text = formatAmount(row['amount'])
|
||||
form.actual_amount_text = formatAmount(row['actual_amount'])
|
||||
form.receive_account = String(row['receive_account'] ?? '-')
|
||||
form.remark = String(row['remark'] ?? '')
|
||||
const receiveType = String(row['receive_type'] ?? '')
|
||||
const receiveKey = receiveTypeLabels[receiveType]
|
||||
form.receive_type_text = receiveKey ? t(receiveKey) : receiveType || '-'
|
||||
form.admin_text = resolveRelationText(row, 'admin', row['admin_id'])
|
||||
form.channel_text = resolveRelationText(row, 'channel', row['channel_id'])
|
||||
}
|
||||
|
||||
const rejectRules: Record<string, FormItemRule[]> = {
|
||||
remark: [
|
||||
{
|
||||
required: true,
|
||||
validator: (_r, value, cb) => {
|
||||
const text = typeof value === 'string' ? value.trim() : ''
|
||||
if (text === '') {
|
||||
cb(new Error(t('order.adminWithdrawOrder.reject_reason_required')))
|
||||
return
|
||||
}
|
||||
cb()
|
||||
},
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const onDialogClose = () => {
|
||||
if (submitting.value) {
|
||||
return
|
||||
}
|
||||
step.value = 'review'
|
||||
baTable.toggleForm()
|
||||
}
|
||||
|
||||
const gotoReject = () => {
|
||||
step.value = 'reject'
|
||||
rejectForm.remark = ''
|
||||
}
|
||||
|
||||
const backToReview = () => {
|
||||
step.value = 'review'
|
||||
}
|
||||
|
||||
const submitReview = async (action: 'approve' | 'reject', remark: string) => {
|
||||
submitting.value = true
|
||||
try {
|
||||
await createAxios(
|
||||
{
|
||||
url: '/admin/order.AdminWithdrawOrder/review',
|
||||
method: 'post',
|
||||
data: { id: form.id, action, remark },
|
||||
},
|
||||
{ showSuccessMessage: true }
|
||||
)
|
||||
baTable.onTableHeaderAction('refresh', {})
|
||||
baTable.toggleForm()
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const submitApprove = async () => {
|
||||
await submitReview('approve', '')
|
||||
}
|
||||
|
||||
const submitReject = async () => {
|
||||
const formEl = rejectFormRef.value
|
||||
if (!formEl) {
|
||||
return
|
||||
}
|
||||
const valid = await formEl.validate().catch(() => false)
|
||||
if (!valid) {
|
||||
return
|
||||
}
|
||||
await submitReview('reject', rejectForm.remark.trim())
|
||||
}
|
||||
|
||||
function formatAmount(raw: unknown): string {
|
||||
if (raw === null || raw === undefined || raw === '') {
|
||||
return '0.00'
|
||||
}
|
||||
const n = Number(raw)
|
||||
if (!Number.isFinite(n)) {
|
||||
return String(raw)
|
||||
}
|
||||
return n.toFixed(2)
|
||||
}
|
||||
|
||||
function resolveRelationText(row: Record<string, unknown>, relationKey: string, fallbackId: unknown): string {
|
||||
const rel = row[relationKey]
|
||||
if (rel && typeof rel === 'object') {
|
||||
const r = rel as Record<string, unknown>
|
||||
const name = r['username'] ?? r['name']
|
||||
if (typeof name === 'string' && name !== '') {
|
||||
return name
|
||||
}
|
||||
}
|
||||
if (fallbackId === null || fallbackId === undefined || fallbackId === '') {
|
||||
return '-'
|
||||
}
|
||||
return 'ID: ' + String(fallbackId)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.admin-withdraw-review-dialog {
|
||||
:deep(.el-dialog__body) {
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
:deep(.el-form-item__content) {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
:deep(.el-input),
|
||||
:deep(.el-textarea) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.review-reject-hint {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.review-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -277,7 +277,6 @@ function buildAdminMapsFromTree(nodes: TreeNode[]) {
|
||||
return { mapCh, mapInv }
|
||||
}
|
||||
|
||||
/** 鏄犲皠鏈懡涓椂浠庡師濮嬫爲鏌ユ壘锛堥槻姝?props 瑁佸壀鎴栧紓姝ユ椂搴忛棶棰橈級 */
|
||||
function findAdminMetaInTree(nodes: TreeNode[], adminId: string): { channel_id?: number; invite_code: string } | null {
|
||||
const target = String(adminId).trim()
|
||||
for (const n of nodes) {
|
||||
@@ -304,6 +303,28 @@ function findAdminMetaInTree(nodes: TreeNode[], adminId: string): { channel_id?:
|
||||
return null
|
||||
}
|
||||
|
||||
function resolveAdminChannelId(adminId: string | number | null | undefined): number | undefined {
|
||||
if (adminId === undefined || adminId === null || adminId === '') {
|
||||
return undefined
|
||||
}
|
||||
const key = typeof adminId === 'number' ? String(adminId) : String(adminId).trim()
|
||||
if (key === '') {
|
||||
return undefined
|
||||
}
|
||||
|
||||
let channelId = adminIdToChannelId.value[key]
|
||||
if (channelId === undefined) {
|
||||
const meta = findAdminMetaInTree(adminScopeTree.value, key)
|
||||
if (meta?.channel_id !== undefined) {
|
||||
channelId = meta.channel_id
|
||||
}
|
||||
}
|
||||
if (channelId === undefined || channelId === null || Number.isNaN(Number(channelId)) || Number(channelId) <= 0) {
|
||||
return undefined
|
||||
}
|
||||
return typeof channelId === 'number' ? channelId : parseInt(String(channelId), 10)
|
||||
}
|
||||
|
||||
const loadAdminScopeTree = async () => {
|
||||
const res = await createAxios({
|
||||
url: '/admin/user.User/adminScopeTree',
|
||||
@@ -360,8 +381,13 @@ const onAdminTreeChange = (val: string | number | null) => {
|
||||
|
||||
if (channelId !== undefined) {
|
||||
baTable.form.items.channel_id = channelId
|
||||
} else {
|
||||
delete baTable.form.items.channel_id
|
||||
}
|
||||
baTable.form.items.register_invite_code = inv !== undefined && inv !== null ? inv : ''
|
||||
nextTick(() => {
|
||||
formRef.value?.validateField('admin_id').catch(() => {})
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
@@ -435,6 +461,16 @@ watch(
|
||||
}
|
||||
)
|
||||
|
||||
const validatorAdminChannel = (_rule: unknown, val: string | number | null | undefined, callback: (error?: Error) => void) => {
|
||||
if (val === undefined || val === null || val === '') {
|
||||
return callback()
|
||||
}
|
||||
if (resolveAdminChannelId(val) === undefined) {
|
||||
return callback(new Error(t('user.user.admin_no_channel_bound')))
|
||||
}
|
||||
return callback()
|
||||
}
|
||||
|
||||
const validatorGameUserPassword = (rule: any, val: string, callback: (error?: Error) => void) => {
|
||||
const operate = baTable.form.operate
|
||||
const v = typeof val === 'string' ? val.trim() : ''
|
||||
@@ -454,7 +490,10 @@ const rules: Partial<Record<string, FormItemRule[]>> = reactive({
|
||||
username: [buildValidatorData({ name: 'required', title: t('user.user.username') })],
|
||||
password: [{ validator: validatorGameUserPassword, trigger: 'blur' }],
|
||||
phone: [buildValidatorData({ name: 'required', title: t('user.user.phone') })],
|
||||
admin_id: [buildValidatorData({ name: 'required', title: t('user.user.admin_affiliation') })],
|
||||
admin_id: [
|
||||
buildValidatorData({ name: 'required', title: t('user.user.admin_affiliation') }),
|
||||
{ validator: validatorAdminChannel, trigger: 'change' },
|
||||
],
|
||||
create_time: [buildValidatorData({ name: 'date', title: t('user.user.create_time') })],
|
||||
update_time: [buildValidatorData({ name: 'date', title: t('user.user.update_time') })],
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user