10 Commits

Author SHA1 Message Date
fb16321b7e 执行文档 2026-04-15 18:06:15 +08:00
7e695d32cf [代理与结算]代理结算周期 2026-04-15 18:06:04 +08:00
080ead200b [代理与结算]代理佣金记录 2026-04-15 18:05:56 +08:00
0b0c0ee04a [游戏管理]游戏配置 2026-04-15 17:46:34 +08:00
518642ddb0 [游戏管理]36字花字典-来自游戏配置扩展 2026-04-15 17:46:30 +08:00
569f9e7749 [游戏管理]玩家钱包流水 2026-04-15 17:46:26 +08:00
f88afbcadb [订单管理]提现订单 2026-04-15 17:46:22 +08:00
92545067d2 [游戏管理]压注订单 2026-04-15 17:46:18 +08:00
6bca594769 [订单管理]充值订单 2026-04-15 17:46:14 +08:00
56df105af6 [游戏管理]用户管理-优化表单样式 2026-04-15 17:46:04 +08:00
58 changed files with 2329 additions and 317 deletions

View File

@@ -0,0 +1,34 @@
<?php
namespace app\admin\controller\agent;
use app\common\controller\Backend;
use support\Response;
use Webman\Http\Request as WebmanRequest;
/**
* 代理佣金记录
*/
class CommissionRecord extends Backend
{
protected ?object $model = null;
protected string|array $preExcludeFields = ['id', 'create_time', 'update_time'];
protected string|array $quickSearchField = ['id', 'remark'];
protected string|array $defaultSortField = ['id' => 'desc'];
protected string|array $orderGuarantee = ['id' => 'desc'];
protected array $withJoinTable = ['settlementPeriod', 'channel', 'admin'];
protected bool $modelValidate = false;
protected function initController(WebmanRequest $request): ?Response
{
$this->model = new \app\common\model\AgentCommissionRecord();
return null;
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace app\admin\controller\agent;
use app\common\controller\Backend;
use support\Response;
use Webman\Http\Request as WebmanRequest;
/**
* 代理结算周期
*/
class SettlementPeriod extends Backend
{
protected ?object $model = null;
protected string|array $preExcludeFields = ['id', 'create_time', 'update_time'];
protected string|array $quickSearchField = ['id', 'settlement_no', 'remark'];
protected string|array $defaultSortField = ['id' => 'desc'];
protected string|array $orderGuarantee = ['id' => 'desc'];
protected bool $modelValidate = false;
protected function initController(WebmanRequest $request): ?Response
{
$this->model = new \app\common\model\AgentSettlementPeriod();
return null;
}
}

View File

@@ -0,0 +1,64 @@
<?php
namespace app\admin\controller\config;
use app\common\controller\Backend;
use app\common\library\game\ZiHuaDictionary as ZiHuaDictionaryLib;
use support\Response;
use Webman\Http\Request as WebmanRequest;
/**
* 游戏配置game_config
*/
class GameConfig extends Backend
{
protected ?object $model = null;
protected string|array $preExcludeFields = ['id', 'create_time', 'update_time'];
protected string|array $quickSearchField = ['id', 'config_key', 'remark'];
protected string|array $defaultSortField = ['id' => 'asc'];
protected string|array $orderGuarantee = ['id' => 'asc'];
protected bool $modelValidate = false;
protected function initController(WebmanRequest $request): ?Response
{
$this->model = new \app\common\model\GameConfig();
return null;
}
/**
* 列表:排除独立表单维护的 36 字花字典
*/
protected function _index(): Response
{
if ($this->request && $this->request->get('select')) {
return $this->select($this->request);
}
list($where, $alias, $limit, $order) = $this->queryBuilder();
$table = strtolower($this->model->getTable());
$mainShort = $alias[$table] ?? '';
if ($mainShort !== '') {
$where[] = [$mainShort . '.config_key', '<>', ZiHuaDictionaryLib::CONFIG_KEY];
}
$res = $this->model
->field($this->indexField)
->withJoin($this->withJoinTable, $this->withJoinType)
->with($this->withJoinTable)
->alias($alias)
->where($where)
->order($order)
->paginate($limit);
return $this->success('', [
'list' => $res->items(),
'total' => $res->total(),
'remark' => get_route_remark(),
]);
}
}

View File

@@ -0,0 +1,130 @@
<?php
namespace app\admin\controller\config;
use app\common\controller\Backend;
use app\common\library\game\ZiHuaDictionary as ZiHuaDictionaryLib;
use InvalidArgumentException;
use support\think\Db;
use support\Response;
use Throwable;
use Webman\Http\Request as WebmanRequest;
/**
* 36 字花字典独立编辑(仅 game_config.zi_hua_36_dictionary
*/
class ZiHuaDictionary extends Backend
{
protected bool $modelValidate = false;
protected array $noNeedPermission = ['index', 'save'];
private function hasNodePermission(WebmanRequest $request, string $action): bool
{
if (!$this->auth) {
return false;
}
$controllerPath = get_controller_path($request);
if (!$controllerPath) {
return false;
}
$paths = [];
$paths[] = $controllerPath . '/' . $action;
$parts = explode('/', $controllerPath);
foreach ($parts as &$part) {
if (str_contains($part, '_')) {
$part = lcfirst(str_replace(' ', '', ucwords(str_replace('_', ' ', $part))));
}
}
$paths[] = implode('/', $parts) . '/' . $action;
foreach (array_values(array_unique($paths)) as $path) {
if ($this->auth->check($path)) {
return true;
}
}
return false;
}
protected function initController(WebmanRequest $request): ?Response
{
return null;
}
/**
* GET读取 36 条POST 不支持
*/
public function index(WebmanRequest $request): Response
{
$response = $this->initializeBackend($request);
if ($response !== null) {
return $response;
}
if (!$this->hasNodePermission($request, 'index')) {
return $this->error(__('You have no permission'), [], 401);
}
if ($request->method() !== 'GET') {
return $this->error(__('Parameter error'));
}
$row = Db::name('game_config')->where('config_key', ZiHuaDictionaryLib::CONFIG_KEY)->find();
$items = ZiHuaDictionaryLib::parseFromConfigValue($row['config_value'] ?? null);
return $this->success('', [
'items' => $items,
'categories' => ZiHuaDictionaryLib::CATEGORIES,
]);
}
/**
* 保存 JSON 数组(仅 value_type=json
*/
public function save(WebmanRequest $request): Response
{
$response = $this->initializeBackend($request);
if ($response !== null) {
return $response;
}
if (!$this->hasNodePermission($request, 'save')) {
return $this->error(__('You have no permission'), [], 401);
}
if ($request->method() !== 'POST') {
return $this->error(__('Parameter error'));
}
$payload = $request->post();
if (!is_array($payload)) {
return $this->error(__('Parameter %s can not be empty', ['']));
}
$items = $payload['items'] ?? null;
if (!is_array($items)) {
return $this->error('items 必须为数组');
}
try {
$clean = ZiHuaDictionaryLib::prepareItemsForSave($items);
$json = ZiHuaDictionaryLib::encodeForDb($clean);
} catch (InvalidArgumentException $e) {
return $this->error($e->getMessage());
}
$now = time();
try {
$exists = Db::name('game_config')->where('config_key', ZiHuaDictionaryLib::CONFIG_KEY)->find();
if ($exists) {
Db::name('game_config')->where('config_key', ZiHuaDictionaryLib::CONFIG_KEY)->update([
'config_value' => $json,
'value_type' => 'json',
'update_time' => $now,
]);
} else {
Db::name('game_config')->insert([
'config_key' => ZiHuaDictionaryLib::CONFIG_KEY,
'config_value' => $json,
'value_type' => 'json',
'remark' => '36字花字典 JSON 数组(独立表单维护)',
'create_time' => $now,
'update_time' => $now,
]);
}
} catch (Throwable $e) {
return $this->error($e->getMessage());
}
return $this->success(__('Saved successfully'));
}
}

View File

@@ -16,6 +16,19 @@ use Webman\Http\Request as WebmanRequest;
class ZiHuaDictionary extends Backend
{
protected bool $modelValidate = false;
protected array $noNeedPermission = ['index', 'save'];
private function hasNodePermission(WebmanRequest $request, string $action): bool
{
if (!$this->auth) {
return false;
}
$controllerPath = get_controller_path($request);
if (!$controllerPath) {
return false;
}
return $this->auth->check($controllerPath . '/' . $action);
}
protected function initController(WebmanRequest $request): ?Response
{
@@ -31,6 +44,9 @@ class ZiHuaDictionary extends Backend
if ($response !== null) {
return $response;
}
if (!$this->hasNodePermission($request, 'index')) {
return $this->error(__('You have no permission'), [], 401);
}
if ($request->method() !== 'GET') {
return $this->error(__('Parameter error'));
}
@@ -51,6 +67,9 @@ class ZiHuaDictionary extends Backend
if ($response !== null) {
return $response;
}
if (!$this->hasNodePermission($request, 'save')) {
return $this->error(__('You have no permission'), [], 401);
}
if ($request->method() !== 'POST') {
return $this->error(__('Parameter error'));
}

View File

@@ -1,6 +1,6 @@
<?php
namespace app\admin\controller\game;
namespace app\admin\controller\order;
use app\common\controller\Backend;
use support\think\Db;
@@ -22,11 +22,11 @@ class BetOrder extends Backend
protected string|array $orderGuarantee = ['id' => 'desc'];
protected array $withJoinTable = ['gameUser', 'channel', 'gamePeriod'];
protected array $withJoinTable = ['user', 'channel', 'gamePeriod'];
protected function initController(WebmanRequest $request): ?Response
{
$this->model = new \app\common\model\GameBetOrder();
$this->model = new \app\common\model\BetOrder();
return null;
}
@@ -87,7 +87,7 @@ class BetOrder extends Backend
->withJoin($this->withJoinTable, $this->withJoinType)
->with($this->withJoinTable)
->visible([
'gameUser' => ['username', 'phone'],
'user' => ['username', 'phone'],
'channel' => ['name'],
'gamePeriod' => ['period_no', 'status'],
])

View File

@@ -0,0 +1,86 @@
<?php
namespace app\admin\controller\order;
use app\common\controller\Backend;
use support\think\Db;
use support\Response;
use Webman\Http\Request as WebmanRequest;
/**
* 充值订单
*/
class DepositOrder extends Backend
{
protected ?object $model = null;
protected bool $modelValidate = false;
protected string|array $quickSearchField = ['id', 'order_no', 'pay_channel', 'remark'];
protected string|array $defaultSortField = ['id' => 'desc'];
protected string|array $orderGuarantee = ['id' => 'desc'];
protected array $withJoinTable = ['user', 'channel'];
protected function initController(WebmanRequest $request): ?Response
{
$this->model = new \app\common\model\DepositOrder();
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();
$table = strtolower($this->model->getTable());
$mainShort = $alias[$table] ?? '';
if ($mainShort !== '' && $this->auth && !$this->auth->isSuperAdmin()) {
$channelIds = $this->getScopedChannelIdsForFilter();
$where[] = [$mainShort . '.channel_id', 'in', $channelIds !== [] ? $channelIds : [0]];
}
$res = $this->model
->withJoin($this->withJoinTable, $this->withJoinType)
->with($this->withJoinTable)
->visible([
'user' => ['username', 'phone'],
'channel' => ['name'],
])
->alias($alias)
->where($where)
->order($order)
->paginate($limit);
return $this->success('', [
'list' => $res->items(),
'total' => $res->total(),
'remark' => get_route_remark(),
]);
}
/**
* @return int[]
*/
private function getScopedChannelIdsForFilter(): array
{
if (!$this->auth) {
return [0];
}
if ($this->auth->isSuperAdmin()) {
return [];
}
$admin = Db::name('admin')->field(['id', 'channel_id'])->where('id', $this->auth->id)->find();
$ids = [];
if ($admin && !empty($admin['channel_id'])) {
$ids[] = $admin['channel_id'];
}
$owned = Db::name('channel')->where('top_admin_id', $this->auth->id)->column('id');
$created = Db::name('channel')->where('admin_id', $this->auth->id)->column('id');
return array_values(array_unique(array_merge($ids, $owned, $created)));
}
}

View File

@@ -0,0 +1,87 @@
<?php
namespace app\admin\controller\order;
use app\common\controller\Backend;
use support\think\Db;
use support\Response;
use Webman\Http\Request as WebmanRequest;
/**
* 提现订单
*/
class WithdrawOrder extends Backend
{
protected ?object $model = null;
protected bool $modelValidate = false;
protected string|array $quickSearchField = ['id', 'order_no', 'remark'];
protected string|array $defaultSortField = ['id' => 'desc'];
protected string|array $orderGuarantee = ['id' => 'desc'];
protected array $withJoinTable = ['user', 'channel', 'reviewAdmin'];
protected function initController(WebmanRequest $request): ?Response
{
$this->model = new \app\common\model\WithdrawOrder();
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();
$table = strtolower($this->model->getTable());
$mainShort = $alias[$table] ?? '';
if ($mainShort !== '' && $this->auth && !$this->auth->isSuperAdmin()) {
$channelIds = $this->getScopedChannelIdsForFilter();
$where[] = [$mainShort . '.channel_id', 'in', $channelIds !== [] ? $channelIds : [0]];
}
$res = $this->model
->withJoin($this->withJoinTable, $this->withJoinType)
->with($this->withJoinTable)
->visible([
'user' => ['username', 'phone'],
'channel' => ['name'],
'reviewAdmin' => ['username'],
])
->alias($alias)
->where($where)
->order($order)
->paginate($limit);
return $this->success('', [
'list' => $res->items(),
'total' => $res->total(),
'remark' => get_route_remark(),
]);
}
/**
* @return int[]
*/
private function getScopedChannelIdsForFilter(): array
{
if (!$this->auth) {
return [0];
}
if ($this->auth->isSuperAdmin()) {
return [];
}
$admin = Db::name('admin')->field(['id', 'channel_id'])->where('id', $this->auth->id)->find();
$ids = [];
if ($admin && !empty($admin['channel_id'])) {
$ids[] = $admin['channel_id'];
}
$owned = Db::name('channel')->where('top_admin_id', $this->auth->id)->column('id');
$created = Db::name('channel')->where('admin_id', $this->auth->id)->column('id');
return array_values(array_unique(array_merge($ids, $owned, $created)));
}
}

View File

@@ -1,6 +1,6 @@
<?php
namespace app\admin\controller\game;
namespace app\admin\controller\record;
use app\common\controller\Backend;
use support\think\Db;
@@ -22,11 +22,11 @@ class UserWalletRecord extends Backend
protected string|array $orderGuarantee = ['id' => 'desc'];
protected array $withJoinTable = ['gameUser', 'channel', 'operatorAdmin'];
protected array $withJoinTable = ['user', 'channel', 'operatorAdmin'];
protected function initController(WebmanRequest $request): ?Response
{
$this->model = new \app\common\model\GameUserWalletRecord();
$this->model = new \app\common\model\UserWalletRecord();
return null;
}
@@ -87,7 +87,7 @@ class UserWalletRecord extends Backend
->withJoin($this->withJoinTable, $this->withJoinType)
->with($this->withJoinTable)
->visible([
'gameUser' => ['username', 'phone'],
'user' => ['username', 'phone'],
'channel' => ['name'],
'operatorAdmin' => ['username'],
])

View File

@@ -1,6 +1,6 @@
<?php
namespace app\admin\controller\game;
namespace app\admin\controller\user;
use Throwable;
use app\common\controller\Backend;
@@ -14,9 +14,9 @@ use Webman\Http\Request as WebmanRequest;
class User extends Backend
{
/**
* GameUser模型对象
* User模型对象
* @var object|null
* @phpstan-var \app\common\model\GameUser|null
* @phpstan-var \app\common\model\User|null
*/
protected ?object $model = null;
@@ -28,7 +28,7 @@ class User extends Backend
protected function initController(WebmanRequest $request): ?Response
{
$this->model = new \app\common\model\GameUser();
$this->model = new \app\common\model\User();
return null;
}

View File

@@ -0,0 +1,38 @@
<?php
namespace app\common\model;
use support\think\Model;
class AgentCommissionRecord extends Model
{
protected $name = 'agent_commission_record';
protected $autoWriteTimestamp = true;
protected $type = [
'create_time' => 'integer',
'update_time' => 'integer',
'settled_at' => 'integer',
'commission_rate' => 'string',
'calc_base_amount' => 'string',
'commission_amount' => 'string',
'status' => 'integer',
];
public function channel(): \think\model\relation\BelongsTo
{
return $this->belongsTo(Channel::class, 'channel_id', 'id');
}
public function admin(): \think\model\relation\BelongsTo
{
return $this->belongsTo(\app\admin\model\Admin::class, 'admin_id', 'id');
}
public function settlementPeriod(): \think\model\relation\BelongsTo
{
return $this->belongsTo(AgentSettlementPeriod::class, 'settlement_period_id', 'id');
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace app\common\model;
use support\think\Model;
class AgentSettlementPeriod extends Model
{
protected $name = 'agent_settlement_period';
protected $autoWriteTimestamp = true;
protected $type = [
'create_time' => 'integer',
'update_time' => 'integer',
'period_start_at' => 'integer',
'period_end_at' => 'integer',
'total_bet_amount' => 'string',
'total_payout_amount' => 'string',
'platform_profit_amount' => 'string',
'status' => 'integer',
];
}

View File

@@ -4,9 +4,9 @@ namespace app\common\model;
use support\think\Model;
class GameBetOrder extends Model
class BetOrder extends Model
{
protected $name = 'game_bet_order';
protected $name = 'bet_order';
protected $autoWriteTimestamp = true;
@@ -24,9 +24,9 @@ class GameBetOrder extends Model
'is_auto' => 'integer',
];
public function gameUser(): \think\model\relation\BelongsTo
public function user(): \think\model\relation\BelongsTo
{
return $this->belongsTo(GameUser::class, 'user_id', 'id');
return $this->belongsTo(User::class, 'user_id', 'id');
}
public function channel(): \think\model\relation\BelongsTo
@@ -39,3 +39,4 @@ class GameBetOrder extends Model
return $this->belongsTo(GamePeriod::class, 'period_id', 'id');
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace app\common\model;
use support\think\Model;
class DepositOrder extends Model
{
protected $name = 'deposit_order';
protected $autoWriteTimestamp = true;
protected $type = [
'create_time' => 'integer',
'update_time' => 'integer',
'pay_time' => 'integer',
'amount' => 'string',
'status' => 'integer',
];
public function user(): \think\model\relation\BelongsTo
{
return $this->belongsTo(User::class, 'user_id', 'id');
}
public function channel(): \think\model\relation\BelongsTo
{
return $this->belongsTo(Channel::class, 'channel_id', 'id');
}
}

View File

@@ -1,38 +0,0 @@
<?php
namespace app\common\model;
use support\think\Model;
/**
* GameUser
*/
class GameUser extends Model
{
// 表名
protected $name = 'game_user';
// 自动写入时间戳字段
protected $autoWriteTimestamp = true;
// 字段类型转换(金额 decimal(18,4) 用字符串避免浮点误差)
protected $type = [
'create_time' => 'integer',
'update_time' => 'integer',
'coin' => 'string',
'total_deposit_coin' => 'string',
'total_valid_bet_coin' => 'string',
'risk_flags' => 'integer',
'current_streak' => 'integer',
];
public function channel(): \think\model\relation\BelongsTo
{
return $this->belongsTo(\app\common\model\Channel::class, 'channel_id', 'id');
}
public function admin(): \think\model\relation\BelongsTo
{
return $this->belongsTo(\app\admin\model\Admin::class, 'admin_id', 'id');
}
}

View File

@@ -2,42 +2,34 @@
namespace app\common\model;
use app\common\model\traits\TimestampInteger;
use support\think\Model;
/**
* 会员公共模型
* 用户主表
*/
class User extends Model
{
use TimestampInteger;
protected $name = 'user';
protected string $table = 'user';
protected string $pk = 'id';
protected bool $autoWriteTimestamp = true;
protected $autoWriteTimestamp = true;
public function getAvatarAttr($value): string
protected $type = [
'create_time' => 'integer',
'update_time' => 'integer',
'coin' => 'string',
'total_deposit_coin' => 'string',
'total_valid_bet_coin' => 'string',
'risk_flags' => 'integer',
'current_streak' => 'integer',
];
public function channel(): \think\model\relation\BelongsTo
{
return full_url($value, false, config('buildadmin.default_avatar'));
return $this->belongsTo(Channel::class, 'channel_id', 'id');
}
public function setAvatarAttr($value): string
public function admin(): \think\model\relation\BelongsTo
{
return $value == full_url('', false, config('buildadmin.default_avatar')) ? '' : $value;
}
public function resetPassword($uid, $newPassword)
{
return $this->where(['id' => $uid])->update(['password' => hash_password($newPassword), 'salt' => '']);
}
public function getMoneyAttr($value): string
{
return bcdiv((string)$value, '100', 2);
}
public function setMoneyAttr($value): string
{
return bcmul((string)$value, '100', 2);
return $this->belongsTo(\app\admin\model\Admin::class, 'admin_id', 'id');
}
}

View File

@@ -5,11 +5,11 @@ namespace app\common\model;
use support\think\Model;
/**
* 玩家游戏币钱包流水(只增不改;余额变更须与 game_user.coin 条件更新同事务
* 用户钱包流水(原 game_user_wallet_record
*/
class GameUserWalletRecord extends Model
class UserWalletRecord extends Model
{
protected $name = 'game_user_wallet_record';
protected $name = 'user_wallet_record';
protected $autoWriteTimestamp = false;
@@ -24,9 +24,9 @@ class GameUserWalletRecord extends Model
'balance_after' => 'string',
];
public function gameUser(): \think\model\relation\BelongsTo
public function user(): \think\model\relation\BelongsTo
{
return $this->belongsTo(GameUser::class, 'user_id', 'id');
return $this->belongsTo(User::class, 'user_id', 'id');
}
public function channel(): \think\model\relation\BelongsTo

View File

@@ -0,0 +1,37 @@
<?php
namespace app\common\model;
use support\think\Model;
class WithdrawOrder extends Model
{
protected $name = 'withdraw_order';
protected $autoWriteTimestamp = true;
protected $type = [
'create_time' => 'integer',
'update_time' => 'integer',
'review_time' => 'integer',
'amount' => 'string',
'fee' => 'string',
'actual_amount' => 'string',
'status' => 'integer',
];
public function user(): \think\model\relation\BelongsTo
{
return $this->belongsTo(User::class, 'user_id', 'id');
}
public function channel(): \think\model\relation\BelongsTo
{
return $this->belongsTo(Channel::class, 'channel_id', 'id');
}
public function reviewAdmin(): \think\model\relation\BelongsTo
{
return $this->belongsTo(\app\admin\model\Admin::class, 'review_admin_id', 'id');
}
}

View File

@@ -1,31 +0,0 @@
<?php
namespace app\common\validate;
use think\Validate;
class GameUser extends Validate
{
protected $failException = true;
/**
* 验证规则
*/
protected $rule = [
];
/**
* 提示消息
*/
protected $message = [
];
/**
* 验证场景
*/
protected $scene = [
'add' => [],
'edit' => [],
];
}

View File

@@ -1,11 +1,11 @@
# 「36字花」数据库与实施计划
**文档版本**V1.4
**依据**《36字花 PRD》《业务流程说明书》《后端系统规格书》及现有表 `game_user`
**文档版本**V1.10
**依据**《36字花 PRD》《业务流程说明书》《后端系统规格书》及现有表 `user`
**目标**:明确分阶段落地步骤、需新建表、两表适配方向与可执行验证清单。
**DDL 脚本(可执行)**[database/dfw_36zihua_schema_v1.sql](../database/dfw_36zihua_schema_v1.sql)
内含:新建 `channel` 表,及 `game_user` / `admin` / `admin_group``ALTER` 语句,以及新建表完整 `CREATE TABLE`(字段级 `COMMENT` 即字段清单)。
内含:新建 `channel` 表,及 `user` / `admin` / `admin_group``ALTER` 语句,以及新建表完整 `CREATE TABLE`(字段级 `COMMENT` 即字段清单)。
---
@@ -14,22 +14,93 @@
| 表名 | 当前角色 | 目标角色 |
|------|----------|----------|
| `channel` | 新渠道主表 | **顶级代理渠道分红配置表**:由超管创建,维护分红方式、分红金额、总利润等渠道参数 |
| `game_user` | 用户coin、channel_id 等) | **C 端玩家主表**:余额与精度、归属代理、流水与风控字段 |
| `user` | 用户coin、channel_id 等) | **C 端玩家主表**:余额与精度、归属代理、流水与风控字段 |
| `admin` | 后台管理员 | **子代理承载表**:顶级代理下级代理放在 `admin`,通过 `invite_code` 管理客户,统一无开奖控制权 |
| `admin_group` | 角色组 | **按渠道隔离**:角色组归属 `channel`,渠道仅管理本渠道角色组 |
### 1.1 全局对局与渠道职责(重要)
与「多渠道」并存的一条硬规则:**对局与开奖全局唯一**,渠道**不**切分牌桌、**不**各自开奖。
| 维度 | 口径 |
|------|------|
| **期号 / 对局** | **全平台共用一套** `game_period`(及全网唯一的当前 `period_no`、同一套状态机)。不存在「每个渠道一局」或「每渠道独立期号表」。 |
| **开奖结果** | **全渠道玩家共享同一期、同一 `result_number`**。所有玩家(无论来自哪个 `channel_id`)压的是**同一场**;避免用户感知「不同入口有不同局、可被渠道操纵」。 |
| **渠道的用途** | `channel``user.channel_id`、注单上的 **`channel_id` 快照**(若有)仅用于 **代理归属、分润、风控与后台数据范围****不**用于生成多套并行对局或独立开奖。 |
| **Redis / 接口** | 当前期倒计时、封盘、算票、开奖号码等 **热状态全局一份**;不得在业务上按渠道维护多套「当前期」或多套开奖结果。 |
> **与 DDL 的对应**`game_period` **不设** `channel_id``bet_order.channel_id` 为下单时用户归属快照,不改变「全场一局」语义。详见 §11.5。
---
## 二、分阶段执行计划
## 二、分阶段执行计划与「先核心、后细节」顺序
**原则**:先打通 **账号 / 渠道 / 余额与流水 / 最小可演示闭环**,再叠 **玩法细则、自动开奖调度、公告与站内信等展示类能力**。避免一上来就做全量 DDL 与全部后台菜单,导致联调面过大。
### 2.1 总览(与下文 P0P4 对应)
| 阶段 | 目标 | 主要内容 |
|------|------|----------|
| **P0** | 数据地基 | 新建 `channel` 并适配 `game_user``admin``admin_group`;建立账本类最小表集(金额精度统一) |
| **P0** | 数据地基 | 新建 `channel` 并适配 `user``admin``admin_group`;建立账本类最小表集(金额精度统一) |
| **P1** | 游戏核心 | 期号、注单、开奖、系统配置Redis 状态机 + MQ 异步落库 |
| **P2** | 资金与风控 | 充提订单、流水账、提现审核、Jackpot / 大额拦截 |
| **P3** | 代理与结算 | 流水占比分桶、级差、联营占成与负结转(表结构 + 跑批骨架) |
| **P4** | 运营与审计 | 公告(含 Pop-out、站内信、后台 RBAC 与操作审计 |
### 2.2 建议先做的:第一批建表(核心路径)
按依赖顺序执行 DDL或 Phinx**优先保证下列表/变更已落地**,其余表可分期补建:
| 顺序 | 类型 | 对象 | 说明 |
|------|------|------|------|
| 1 | 新建 | **`channel`** | 渠道与顶级代理参数;替代历史 `game_channel` 方向 |
| 2 | 适配 | **`user``admin``admin_group`** | `coin` 精度、`channel_id`、邀请码与风控等(见 §四~§六) |
| 3 | 新建 | **`game_config`** | 全局可调参数(局时长、下注窗口等),避免写死 |
| 4 | 新建 | **`user_wallet_record`** | 玩家钱包流水,与余额变更同事务、可审计 |
| 5 | 新建 | **`game_period`**、**`bet_order`** | 最小「一期号 + 注单」闭环;先有表再写状态机与接口 |
**第一批可暂缓的表(归入后续迭代)**`game_bet_auto`(自动托管)、`operation_notice` / `user_notice_read` / `user_site_message`(纯展示与触达)、`game_agent_wallet` 及结算域各表(代理分佣与联营跑批)。
### 2.3 建议先做的:第一批后台菜单(与核心能力对齐)
在 BuildAdmin **`admin_rule`** 中,**优先保证**与下列能力相关的菜单与按钮权限可配置(具体 `component` 路径以仓库为准):
| 优先级 | 菜单能力 | 目的 |
|--------|----------|------|
| 高 | **渠道**、**游戏用户**、**角色组 / 管理员**(含渠道与邀请码相关字段) | 搭好「谁管哪条线、玩家归谁」 |
| 高 | **用户管理 / 用户钱包流水** | 覆盖玩家主数据与账务对账 |
| 高 | **游戏对局 / 压注订单** | 核心玩法与对账入口 |
| 中 | **游戏配置 / 36字花字典** | 运营参数与字典维护 |
| 中 | **充值订单 / 提现订单** | 充提流程管理与审核 |
| 低 | **公告 / 站内信**、**代理结算** 等 | 属 **P3P4**,核心跑通后再接 |
### 2.4 明确放到「细节阶段」的能力(勿阻塞第一期)
下列项依赖 **P1 状态机、定时任务、前端 C 端页面** 较完整后再做,避免与「账号 + 余额 + 最小下注」抢工期:
- **玩法细则**选号上限、连胜倍率、Jackpot 条件等在 `game_config` 与代码中逐步细化即可。
- **自动开奖 / 调度**依赖期号服务、Redis 与可选MQ表结构可先建**调度与算票逻辑后迭代**。
- **游戏公告、强弹窗、站内信展示**:表可后建;**后台编辑 + C 端展示** 属于运营体验增强,放在核心闭环之后。
### 2.5 最小可验收里程碑(建议)
1. **P0 完成**:能创建渠道与子代理管理员,玩家归属正确,`coin``user_wallet_record` 手工或脚本入账可对上。
2. **P1 骨架完成**:能创建期号、写入注单、产生钱包流水(即使开奖仍为**手动或脚本模拟**)。
3. 再推进 **自动流程、充提、公告展示****P3 结算**
### 2.6 本次重构迁移顺序(已执行口径)
1. 新建订单表:`deposit_order``withdraw_order`(带 `order_no` 唯一索引、`user_id/status/create_time` 常用索引)。
2. 硬重命名:`game_user -> user``game_bet_order -> bet_order`
3. 重建菜单与权限目录:`user``order``game``config``record`
4. 新增菜单节点:
- `user/user`
- `order/betOrder``order/depositOrder``order/withdrawOrder`
- `game/period`
- `config/gameConfig``config/ziHuaDictionary`
- `record/userWalletRecord`
5. 清理收口:删除迁移过程中的冗余权限别名规则,仅保留最终模块节点(`user/order/game/config/record`)。
---
## 三、`channel` 设计(替代 `game_channel`
@@ -54,7 +125,7 @@
---
## 四、`game_user` 适配(用户表)
## 四、`user` 适配(用户表)
### 4.1 建议保留字段
@@ -109,11 +180,13 @@
### 7.1 游戏引擎
**对局范围**:与 §1.1 一致——**全平台一套期号**,不按渠道分桌;详见 `game_period``channel_id` 的 DDL。
| 表名 | 用途 |
|------|------|
| `game_period`(或 `game_issue` | 期号、状态机阶段、开奖号码、自动/手动、作废标记 |
| `game_bet_order` | 注单:期号、用户、选号、单注额、总额、状态(扣款 / 结算 / 退款) |
| `game_bet_auto`(可选) | 自动托管:剩余局数、选号快照、启停状态 |
| `game_period`(或 `game_issue` | **全局**期号、状态机阶段、开奖号码、自动/手动、作废标记**非**每渠道一局) |
| `bet_order` | 注单:关联全局 `period_id`;可选 `channel_id` 为用户归属快照(分润/查询),**非**独立牌桌 |
| `game_bet_auto`(可选) | 自动托管:剩余局数、选号快照、启停状态(仍挂在全局期号下) |
### 7.2 系统配置
@@ -125,7 +198,7 @@
| 表名 | 用途 |
|------|------|
| `user_wallet_ledger`(或 `game_user_balance_log` | 充值、下注扣款、派彩、提现冻结、手续费、人工调账;金额 **`decimal(18,4)`** |
| **`user_wallet_record`**(原草案名 `user_wallet_ledger`,以 DDL 为准) | **玩家游戏币钱包流水**:充值、提现(含冻结/解冻)、平台划入划出、管理员加扣币、下注、派彩、手续费、作废退款、调账;金额 **`decimal(18,4)`****只增不改**;与 `user.coin` 变更须同事务 + 条件更新 |
| `deposit_order` | 第三方充值、法币金额、汇率、游戏币到账、回调状态 |
| `withdraw_order` | 提现申请、手续费、审核状态、Jackpot / 大额标记 |
@@ -155,15 +228,27 @@
---
## 、验证步骤
## 、验证步骤
### 7.1 数据库与模型
### 8.1 数据库与模型
1. DDL 执行后检查:金额字段均为 **`decimal(18,4)`**`channel``game_user``admin``admin_group`、期号等关联字段有合理索引。
1. DDL 执行后检查:金额字段均为 **`decimal(18,4)`**`channel``user``admin``admin_group`、期号等关联字段有合理索引。
2. 创建测试渠道(顶级)→ 创建子代理 `admin`(设置 `parent_admin_id``channel_id``invite_code`)→ 创建用户并挂载 `channel_id`,链路与后台筛选正常。
3. 扣款更新采用「**条件更新**」语义(如 `WHERE balance >= 扣款额`),压测或单测验证**不出现负余额**。
### 7.2 业务规则(对照 PRD / 业务流程
### 8.1.1 菜单与权限回归(本次新增
1. 使用超管登录,确认五大目录 `user/order/game/config/record` 可见。
2. 使用普通角色登录,逐项验证按钮权限(`index/add/edit/del/save`)是否与授权一致。
3. 抽查重点页面:
- `user/user`
- `order/depositOrder``order/withdrawOrder``order/betOrder`
- `game/period`
- `config/gameConfig``config/ziHuaDictionary`
- `record/userWalletRecord`
4. 验证 `config/ziHuaDictionary` 的读取与保存权限均命中,且仅授权角色可操作。
### 8.2 业务规则(对照 PRD / 业务流程)
| 场景 | 验证要点 |
|------|----------|
@@ -175,14 +260,14 @@
| 提现 | 有效投注 ≥ 总充值×配置倍数;最低提现与 0.5% 手续费;大额 / Jackpot 进审核 |
| 灾难恢复 | 模拟期卡在「计算中」,启动退本流程:期作废、本金退回账本 |
### 7.3 代理与结算(逻辑就绪后)
### 8.3 代理与结算(逻辑就绪后)
1. 构造多代理树与多用户下注,跑一期结算脚本或单元测试纯算法。
2. **大盘亏损**:本期代理分佣为 0。
3. **大盘盈利**:流水占比分桶 + 级差,各级金额之和与线总包一致(可对照文档数值验算)。
4. **联营**:客损为负产生负结转;下期盈利先抵扣再分佣。
### 7.4 非功能
### 8.4 非功能
- Redis当前期注单池、倒计时、近 30 期开奖缓存。
- MQ派彩异步消费**幂等**(如 `period_id` + 用户 + 业务单号)。
@@ -190,15 +275,16 @@
---
## 、风险与依赖
## 、风险与依赖
1. **子代理数据源**:子代理统一在 `admin`,避免在 `channel` 重复建树造成双主数据源。
2. **现有 `profit_amount`decimal(5,2)**:与游戏币 **18,4** 精度不一致,演进时改为 `decimal(18,4)` 或迁移至结算域,避免对账误差。
3. 文档要求 **AI 算票在 Redis 内完成**,主库避免结算期同步锁表;账本与注单以异步一致性与幂等为准。
3. 文档要求 **AI 算票在 Redis 内完成**,主库避免结算期同步锁表;账本与注单以异步一致性与幂等为准。
4. **全局对局一致性**:任何需求(多租户展示、渠道后台)均不得引入「按渠道独立期号/独立开奖」;若出现产品歧义,以 **§1.1** 为准,避免公平性质疑与客诉。
---
## 、相关文档索引
## 、相关文档索引
| 文档 | 说明 |
|------|------|
@@ -208,15 +294,15 @@
| `docs/CRUD生成逻辑说明.md` | BuildAdmin CRUD 路径与表名规范 |
| `database/dfw_36zihua_schema_v1.sql` | 具体 DDL字段、索引、注释 |
### 9.1 新建表一览(字段以 SQL 为准)
### 10.1 新建表一览(字段以 SQL 为准)
| 序号 | 表名 | 说明 |
|------|------|------|
| 1 | `game_config` | 动态参数 KV |
| 2 | `game_period` | 期号与状态机 |
| 3 | `game_bet_order` | 注单 |
| 3 | `bet_order` | 注单 |
| 4 | `game_bet_auto` | 自动托管 |
| 5 | `user_wallet_ledger` | 用户游戏币账本 |
| 5 | `user_wallet_record` | 玩家游戏币钱包流水(替代旧名 `user_wallet_ledger` |
| 6 | `deposit_order` | 充值订单 |
| 7 | `withdraw_order` | 提现订单 |
| 8 | `game_agent_wallet` | 代理钱包 |
@@ -229,7 +315,7 @@
| 15 | `user_notice_read` | 公告已读/确认 |
| 16 | `user_site_message` | 站内信 |
**既有表变更**:新建 `channel` 替代 `game_channel``admin` 增加子代理字段(`parent_admin_id``channel_id``invite_code``agent_role`)用于代理邀请链路管理;`admin_group` 增加 `channel_id` 实现角色组按渠道隔离;`game_user` 调整 `coin` 精度并增加 `channel_id``email``register_invite_code`、累计流水与风控、连胜兜底等字段
**既有表变更**:新建 `channel` 替代 `game_channel``admin` 增加子代理字段(`parent_admin_id``channel_id``invite_code``agent_role`)用于代理邀请链路管理;`admin_group` 增加 `channel_id` 实现角色组按渠道隔离;`game_user` 已硬重命名为 `user``game_bet_order` 已硬重命名为 `bet_order``user_wallet_record``game_config` 保持现名
---
@@ -242,3 +328,102 @@
| V1.2 | 2026-04-14 | 修正口径:`game_channel` 仅顶级分红参数,子代理迁移至 `admin` |
| V1.3 | 2026-04-14 | 收敛权限:仅超管可开奖,渠道/子代理仅拉用户 |
| V1.4 | 2026-04-14 | 新建 `channel` 替代 `game_channel``admin_group` 按渠道隔离 |
| V1.5 | 2026-04-15 | 落地 `game_user_wallet_record` 玩家钱包流水表;文档增补字段说明附录;与 DDL 统一命名 |
| V1.6 | 2026-04-15 | 增加「先核心后细节」实施顺序:先建表/菜单清单、可暂缓表与细节阶段能力;修正章节编号(验证步骤改为第八章) |
| V1.7 | 2026-04-15 | 明确「多渠道、单场对局」:全平台共用 `game_period` 与同一开奖;渠道仅归属/分润;附录 `game_period`/`bet_order` 注释对齐 |
| V1.8 | 2026-04-15 | §7.1 游戏引擎表与 §1.1 对齐:显式说明全局期号、`bet_order.channel_id` 仅为归属快照 |
| V1.9 | 2026-04-15 | 落地模块化重构:`game_user->user``game_bet_order->bet_order`;新增 `deposit_order`/`withdraw_order`;后台菜单重组为 `user/order/game/config/record` 五大目录 |
| V1.10 | 2026-04-15 | 完成表名与权限规则收口:`user_wallet_record``game_config` 保持不变;移除冗余 snake alias 菜单规则,文档口径与线上结构一致 |
---
## 十一、附录:主要表字段说明(与 `database/dfw_36zihua_schema_v1.sql` 同步)
以下为各表**字段含义与使用要点**,便于评审与联调;**以仓库内 DDL 为最终口径**。
### 11.1 `channel`(渠道 / 顶级代理分红参数)
| 字段 | 作用 |
|------|------|
| `id` | 主键 |
| `code` | 渠道标识,业务唯一 |
| `invite_code` | 渠道侧邀请码(与 PRD「邀请码由管理员生成」并存时以产品定义为准 |
| `name` | 渠道名称 |
| `agent_mode` | `turnover` 普通刷水 / `affiliate` 联营,决定分红与契约字段使用方式 |
| `profit_amount` / `total_profit_amount` / `commission_pool_amount` | 经营与分红池快照类金额,**decimal(18,4)** |
| `turnover_share_rate` / `affiliate_share_rate` / `affiliate_fee_rate` | 分红与联营费率类参数 |
| `carryover_balance` | 联营负结转余额(可负) |
| `top_admin_id` | 顶级代理管理员,关联 `admin.id` |
| `admin_id` / `admin_group_id` | 创建人、渠道绑定的角色组 |
| `status` / `remark` / `create_time` / `update_time` | 状态、备注、时间戳 |
### 11.2 `user`C 端玩家)
| 字段 | 作用 |
|------|------|
| `coin` | 当前游戏币余额,**decimal(18,4)**,更新须条件更新防负余额 |
| `channel_id` | 归属渠道;历史 `game_channel_id` 若仍存在,迁移期注意双写/对照 |
| `register_invite_code` | 注册时邀请码快照,用于审计与归属 |
| `total_deposit_coin` / `total_valid_bet_coin` | 累计充值入账、累计有效投注;提现流水倍数校验用 |
| `risk_flags` | 风控位(如禁止登录/下注/提现,按位定义) |
| `current_streak` / `last_bet_period_no` | 连胜与期号兜底;高频仍以 Redis 为准 |
| `admin_id` | 归属子代理管理员 |
| 其余 | 账号、头像、状态、时间戳等见 DDL |
### 11.3 `admin` / `admin_group`(子代理与角色组)
| 表/字段 | 作用 |
|---------|------|
| `admin.parent_admin_id` | 子代理上下级 |
| `admin.channel_id` | 所属渠道 |
| `admin.invite_code` | 子代理邀请码,注册归属 |
| `admin.agent_role` | 角色类型(均无开奖权) |
| `admin_group.channel_id` | 角色组归属渠道;`NULL` 可为系统级(仅超管) |
| `admin_group.commission_rate` | 角色组分红比例(百分比) |
### 11.4 `game_config`(动态参数)
| 字段 | 作用 |
|------|------|
| `config_key` | 全局唯一键 |
| `config_value` / `value_type` | 参数值及类型(含 JSON |
| `remark` | 说明,禁止业务写死应读此表 |
### 11.5 `game_period` / `bet_order` / `game_bet_auto`
| 表 | 要点 |
|----|------|
| `game_period` | **全平台唯一**期号:`period_no` 全局唯一;`status` 状态机;开奖结果与作废原因。**无 `channel_id` 字段**:对局不按渠道拆分。 |
| `bet_order` | 注单关联**全局** `period_id``channel_id`(若有)为**用户/归属快照**,便于分润与查询,**不表示**独立牌桌或独立开奖。`idempotency_key` 幂等;金额 **18,4**`win_amount` / `jackpot_extra_amount` |
| `game_bet_auto` | 托管剩余局数、选号快照、抖动时间等;仍挂在**全局**期号下 |
### 11.6 `user_wallet_record`(玩家钱包流水)
| 字段 | 作用 |
|------|------|
| `user_id` | 玩家 `user.id` |
| `channel_id` | **账务发生时**渠道快照,便于按渠道查询与对账;入账逻辑应写入 |
| `biz_type` | 业务类型:`deposit``withdraw``platform_in``platform_out``admin_credit``admin_deduct``bet``payout``fee``void_refund``adjust` 等(字符串枚举,与代码常量一致) |
| `direction` | `1` 入金 / `2` 出金;与 `amount` 恒正配合使用 |
| `amount` | 变动额,**恒正** |
| `balance_before` / `balance_after` | 变动前后余额,对账与审计 |
| `ref_type` / `ref_id` | 关联订单或业务主键(如 `deposit_order``bet_order` |
| `idempotency_key` | 幂等键,防重复记账(唯一索引,允许多 `NULL` |
| `operator_admin_id` | 人工加扣币时的操作者 `admin.id` |
| `remark` | 说明 |
| `create_time` | 流水时间;**本表不设 `update_time`,禁止 UPDATE** |
### 11.7 `deposit_order` / `withdraw_order`
| 表 | 要点 |
|----|------|
| `deposit_order` | 法币、`fx_rate``coin_amount`、网关单号、回调存档 |
| `withdraw_order` | 申请额、手续费、审核与 Jackpot 标记、驳回原因 |
### 11.8 代理钱包与结算域(`game_agent_wallet`、`game_agent_wallet_ledger`、`agent_settlement_period`、`agent_commission_record`、`affiliate_*`
见 DDL 注释:代理余额与流水、结算周期大盘快照、佣金行、联营契约与负结转;金额均为 **decimal(18,4)**,与玩家账本区分职责。
### 11.9 运营与消息(`operation_notice`、`user_notice_read`、`user_site_message`
公告类型(含强弹)、已读确认;站内信与公告分离存储,详见各表 `COMMENT`

View File

@@ -0,0 +1,19 @@
export default {
'quick Search Fields': 'ID/Settlement period ID/Remark',
id: 'ID',
settlement_period_id: 'Settlement period ID',
channel_id: 'Channel ID',
admin_id: 'Agent admin ID',
commission_rate: 'Commission rate',
calc_base_amount: 'Calculation base amount',
commission_amount: 'Commission amount',
status: 'Status',
'status 0': 'Pending',
'status 1': 'Paid',
'status 2': 'Reverted',
settled_at: 'Settled at',
remark: 'Remark',
create_time: 'Created',
update_time: 'Updated',
}

View File

@@ -0,0 +1,19 @@
export default {
'quick Search Fields': 'ID/Settlement No./Remark',
id: 'ID',
settlement_no: 'Settlement No.',
period_start_at: 'Period start',
period_end_at: 'Period end',
total_bet_amount: 'Total bet amount',
total_payout_amount: 'Total payout amount',
platform_profit_amount: 'Platform profit',
status: 'Status',
'status 0': 'Pending',
'status 1': 'Processing',
'status 2': 'Completed',
'status 3': 'Closed',
remark: 'Remark',
create_time: 'Created',
update_time: 'Updated',
}

View File

@@ -0,0 +1,3 @@
import gameConfig from '../game/config'
export default gameConfig

View File

@@ -0,0 +1,3 @@
import dict from '../game/ziHuaDictionary'
export default dict

View File

@@ -22,14 +22,8 @@ export default {
idempotency_key: 'Idempotency key',
create_time: 'Created',
update_time: 'Updated',
gamePeriod: {
period_no: 'Period (relation)',
status: 'Period status',
},
gameUser: {
username: 'Username',
},
channel: {
name: 'Channel',
},
gamePeriod_period_no: 'Period (relation)',
gamePeriod_status: 'Period status',
user_username: 'Username',
channel_name: 'Channel',
}

View File

@@ -1,7 +1,7 @@
export default {
id: 'Record ID',
user_id: 'User ID',
'game_user__username': 'Username',
'user__username': 'Username',
'channel__name': 'Channel',
biz_type: 'Biz type',
direction: 'Direction',

View File

@@ -0,0 +1,29 @@
export default {
'quick Search Fields': 'ID / Period / Idempotency',
id: 'ID',
period_id: 'Period ID',
period_no: 'Period No.',
user_id: 'User ID',
channel_id: 'Channel ID',
pick_numbers: 'Picks',
unit_amount: 'Unit amount',
pick_count: 'Pick count',
total_amount: 'Total',
streak_at_bet: 'Streak at bet',
is_auto: 'Auto',
'is_auto 0': 'Manual',
'is_auto 1': 'Auto bet',
win_amount: 'Payout',
jackpot_extra_amount: 'Jackpot extra',
status: 'Status',
'status 1': 'Pending draw',
'status 2': 'Settled',
'status 3': 'Refunded',
idempotency_key: 'Idempotency key',
create_time: 'Created',
update_time: 'Updated',
gamePeriod_period_no: 'Period (relation)',
gamePeriod_status: 'Period status',
user_username: 'Username',
channel_name: 'Channel',
}

View File

@@ -0,0 +1,20 @@
export default {
'quick Search Fields': 'Order No./User ID/Pay channel',
id: 'ID',
order_no: 'Order No.',
user_id: 'User ID',
channel_id: 'Channel ID',
amount: 'Amount',
status: 'Status',
'status 0': 'Pending',
'status 1': 'Success',
'status 2': 'Failed',
'status 3': 'Canceled',
pay_channel: 'Pay channel',
pay_time: 'Pay time',
remark: 'Remark',
create_time: 'Created',
update_time: 'Updated',
user_username: 'Username',
channel_name: 'Channel',
}

View File

@@ -0,0 +1,23 @@
export default {
'quick Search Fields': 'Order No./User ID',
id: 'ID',
order_no: 'Order No.',
user_id: 'User ID',
channel_id: 'Channel ID',
amount: 'Apply amount',
fee: 'Fee',
actual_amount: 'Actual amount',
status: 'Status',
'status 0': 'Pending review',
'status 1': 'Approved',
'status 2': 'Rejected',
'status 3': 'Paid',
review_admin_id: 'Reviewer',
review_time: 'Review time',
remark: 'Remark',
create_time: 'Created',
update_time: 'Updated',
user_username: 'Username',
channel_name: 'Channel',
review_admin_username: 'Reviewer',
}

View File

@@ -0,0 +1,33 @@
export default {
id: 'Record ID',
user_id: 'User ID',
'user__username': 'Username',
'channel__name': 'Channel',
biz_type: 'Biz type',
direction: 'Direction',
amount: 'Amount',
balance_before: 'Balance before',
balance_after: 'Balance after',
ref_type: 'Ref type',
ref_id: 'Ref ID',
idempotency_key: 'Idempotency key',
'operator_admin__username': 'Operator',
remark: 'Remark',
create_time: 'Created at',
'quick Search Fields': 'ID, biz type, ref type, remark, idempotency key',
'direction in': 'Credit',
'direction out': 'Debit',
'biz deposit': 'Deposit',
'biz withdraw': 'Withdraw',
'biz withdraw_freeze': 'Withdraw freeze',
'biz withdraw_unfreeze': 'Withdraw unfreeze',
'biz platform_in': 'Platform credit',
'biz platform_out': 'Platform debit',
'biz admin_credit': 'Admin credit',
'biz admin_deduct': 'Admin debit',
'biz bet': 'Bet',
'biz payout': 'Payout',
'biz fee': 'Fee',
'biz void_refund': 'Void refund',
'biz adjust': 'Adjustment',
}

View File

@@ -0,0 +1,45 @@
export default {
id: 'ID',
username: 'Username',
password: 'Password',
uuid: 'UUID',
phone: 'Phone',
email: 'Email',
email_placeholder: 'Optional (register with phone or email)',
head_image: 'Avatar',
remark: 'Remark',
coin: 'Coin balance',
coin_placeholder: 'decimal(18,4)',
total_deposit_coin: 'Total deposit (coin)',
total_valid_bet_coin: 'Total valid bet (coin)',
risk_flags: 'Risk',
risk_none: 'None',
risk_no_login: 'No login',
risk_no_bet: 'No bet',
risk_no_withdraw: 'No withdraw',
current_streak: 'Win streak',
last_bet_period_no: 'Last bet period',
last_bet_period_no_placeholder: 'DB fallback for streak',
register_invite_code: 'Invite code (snapshot)',
register_invite_code_placeholder: 'Invite code at registration',
status: 'Status',
'status 0': 'Disabled',
'status 1': 'Enabled',
section_admin_attribution: 'Administrator',
admin_affiliation: 'Assigned admin',
admin_affiliation_placeholder: 'Role group tree — only admins in your scope',
register_invite_code_auto_placeholder: 'Filled from selected admin invite code',
channel_id: 'Channel',
channel__name: 'Channel',
admin_id: 'Admin',
admin__username: 'Admin',
create_time: 'Created',
update_time: 'Updated',
'quick Search Fields': 'ID, username, phone, email, invite code',
section_basic: 'Account',
section_register: 'Registration',
section_finance: 'Balance & turnover',
section_risk: 'Risk control',
section_streak: 'Streak (fallback)',
section_other: 'Other',
}

View File

@@ -0,0 +1,19 @@
export default {
'quick Search Fields': 'ID/结算周期ID/备注',
id: 'ID',
settlement_period_id: '结算周期ID',
channel_id: '渠道ID',
admin_id: '代理管理员ID',
commission_rate: '佣金比例',
calc_base_amount: '结算基数',
commission_amount: '佣金金额',
status: '状态',
'status 0': '待发放',
'status 1': '已发放',
'status 2': '已回退',
settled_at: '发放时间',
remark: '备注',
create_time: '创建时间',
update_time: '更新时间',
}

View File

@@ -0,0 +1,19 @@
export default {
'quick Search Fields': 'ID/结算周期号/备注',
id: 'ID',
settlement_no: '结算周期号',
period_start_at: '周期开始',
period_end_at: '周期结束',
total_bet_amount: '总投注额',
total_payout_amount: '总派彩额',
platform_profit_amount: '平台盈亏',
status: '状态',
'status 0': '待结算',
'status 1': '结算中',
'status 2': '已完成',
'status 3': '已关闭',
remark: '备注',
create_time: '创建时间',
update_time: '更新时间',
}

View File

@@ -0,0 +1,3 @@
import gameConfig from '../game/config'
export default gameConfig

View File

@@ -0,0 +1,3 @@
import dict from '../game/ziHuaDictionary'
export default dict

View File

@@ -22,15 +22,8 @@ export default {
idempotency_key: '幂等键',
create_time: '创建时间',
update_time: '更新时间',
/** 关联展示列(须嵌套,供 t('game.betOrder.gamePeriod.xxx') */
gamePeriod: {
period_no: '对局期号',
status: '期状态',
},
gameUser: {
username: '用户名',
},
channel: {
name: '渠道',
},
gamePeriod_period_no: '对局期号',
gamePeriod_status: '期状态',
user_username: '用户名',
channel_name: '渠道',
}

View File

@@ -1,7 +1,7 @@
export default {
id: '流水ID',
user_id: '用户ID',
'game_user__username': '用户名',
'user__username': '用户名',
'channel__name': '渠道',
biz_type: '业务类型',
direction: '方向',

View File

@@ -0,0 +1,29 @@
export default {
'quick Search Fields': 'ID/期号/幂等键',
id: 'ID',
period_id: '期ID',
period_no: '期号',
user_id: '用户ID',
channel_id: '渠道ID',
pick_numbers: '选号',
unit_amount: '单号金额',
pick_count: '选号个数',
total_amount: '总金额',
streak_at_bet: '下注时连胜',
is_auto: '托管',
'is_auto 0': '手动',
'is_auto 1': '托管',
win_amount: '派彩',
jackpot_extra_amount: 'Jackpot',
status: '状态',
'status 1': '待开奖',
'status 2': '已结算',
'status 3': '已退款',
idempotency_key: '幂等键',
create_time: '创建时间',
update_time: '更新时间',
gamePeriod_period_no: '对局期号',
gamePeriod_status: '期状态',
user_username: '用户名',
channel_name: '渠道',
}

View File

@@ -0,0 +1,20 @@
export default {
'quick Search Fields': '订单号/用户ID/支付通道',
id: 'ID',
order_no: '订单号',
user_id: '用户ID',
user_username: '用户名',
channel_id: '渠道ID',
channel_name: '渠道',
amount: '金额',
status: '状态',
'status 0': '待处理',
'status 1': '成功',
'status 2': '失败',
'status 3': '已取消',
pay_channel: '支付通道',
pay_time: '支付时间',
remark: '备注',
create_time: '创建时间',
update_time: '更新时间',
}

View File

@@ -0,0 +1,23 @@
export default {
'quick Search Fields': '订单号/用户ID',
id: 'ID',
order_no: '订单号',
user_id: '用户ID',
channel_id: '渠道ID',
amount: '申请金额',
fee: '手续费',
actual_amount: '实际到账',
status: '状态',
'status 0': '待审核',
'status 1': '已通过',
'status 2': '已拒绝',
'status 3': '已打款',
review_admin_id: '审核管理员',
review_time: '审核时间',
remark: '备注',
create_time: '创建时间',
update_time: '更新时间',
user_username: '用户名',
channel_name: '渠道',
review_admin_username: '审核人',
}

View File

@@ -0,0 +1,33 @@
export default {
id: '流水ID',
user_id: '用户ID',
'user__username': '用户名',
'channel__name': '渠道',
biz_type: '业务类型',
direction: '方向',
amount: '变动额',
balance_before: '变动前余额',
balance_after: '变动后余额',
ref_type: '关联类型',
ref_id: '关联ID',
idempotency_key: '幂等键',
'operator_admin__username': '操作管理员',
remark: '备注',
create_time: '创建时间',
'quick Search Fields': 'ID、业务类型、关联类型、备注、幂等键',
'direction in': '入金',
'direction out': '出金',
'biz deposit': '充值入账',
'biz withdraw': '提现出账',
'biz withdraw_freeze': '提现冻结',
'biz withdraw_unfreeze': '提现解冻',
'biz platform_in': '平台划入',
'biz platform_out': '平台划出',
'biz admin_credit': '管理员加币',
'biz admin_deduct': '管理员扣币',
'biz bet': '下注扣款',
'biz payout': '派彩',
'biz fee': '手续费',
'biz void_refund': '作废退款',
'biz adjust': '其他调账',
}

View File

@@ -0,0 +1,45 @@
export default {
id: 'ID',
username: '用户名',
password: '密码',
uuid: '用户唯一标识',
phone: '手机号',
email: '邮箱',
email_placeholder: '可选,与手机号二选一注册时填写',
head_image: '头像',
remark: '备注',
coin: '游戏币余额',
coin_placeholder: 'decimal(18,4),禁止业务用浮点存库',
total_deposit_coin: '累计充值(币)',
total_valid_bet_coin: '累计有效投注(币)',
risk_flags: '风控',
risk_none: '无限制',
risk_no_login: '禁止登录',
risk_no_bet: '禁止下注',
risk_no_withdraw: '禁止提现',
current_streak: '当前连胜',
last_bet_period_no: '最近下注期号',
last_bet_period_no_placeholder: '连胜兜底同步用,可与 Redis 对照',
register_invite_code: '注册邀请码快照',
register_invite_code_placeholder: '注册时绑定渠道/代理邀请码',
status: '状态',
'status 0': '禁用',
'status 1': '启用',
section_admin_attribution: '管理员归属',
admin_affiliation: '归属管理员',
admin_affiliation_placeholder: '按角色组展开,仅展示您可管理范围内的管理员',
register_invite_code_auto_placeholder: '随所选管理员邀请码自动带出',
channel_id: '所属渠道',
channel__name: '渠道名',
admin_id: '归属管理员',
admin__username: '管理员',
create_time: '创建时间',
update_time: '修改时间',
'quick Search Fields': 'ID、用户名、手机号、邮箱、邀请码',
section_basic: '账号信息',
section_register: '注册与邀请',
section_finance: '资金与流水',
section_risk: '风控',
section_streak: '连胜(兜底)',
section_other: '其他',
}

View File

@@ -37,6 +37,12 @@ router.beforeEach((to, from, next) => {
// 按需动态加载页面的语言包-start
let loadPath: string[] = []
const toCamelPath = (path: string): string => {
return path
.split('/')
.map((seg) => seg.replace(/_([a-z])/g, (_m, c: string) => c.toUpperCase()))
.join('/')
}
const config = useConfig()
if (to.path in langAutoLoadMap) {
loadPath.push(...langAutoLoadMap[to.path as keyof typeof langAutoLoadMap])
@@ -47,7 +53,10 @@ router.beforeEach((to, from, next) => {
// 去除 path 中的 /admin
const adminPath = to.path.slice(to.path.indexOf(adminBaseRoutePath) + adminBaseRoutePath.length)
if (adminPath) loadPath.push(prefix + adminPath + '.ts')
if (adminPath) {
loadPath.push(prefix + adminPath + '.ts')
loadPath.push(prefix + toCamelPath(adminPath) + '.ts')
}
} else {
prefix = './frontend/' + config.lang.defaultLang
loadPath.push(prefix + to.path + '.ts')
@@ -55,7 +64,9 @@ router.beforeEach((to, from, next) => {
// 根据路由 name 加载的语言包
if (to.name) {
loadPath.push(prefix + '/' + to.name.toString() + '.ts')
const routeName = to.name.toString()
loadPath.push(prefix + '/' + routeName + '.ts')
loadPath.push(prefix + '/' + toCamelPath(routeName) + '.ts')
}
if (!window.loadLangHandle.publicMessageLoaded) window.loadLangHandle.publicMessageLoaded = []

View File

@@ -0,0 +1,80 @@
<template>
<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 />
<TableHeader
:buttons="['refresh', 'add', 'edit', 'delete', 'comSearch', 'quickSearch', 'columnDisplay']"
:quick-search-placeholder="t('Quick search placeholder', { fields: t('agent.commissionRecord.quick Search Fields') })"
></TableHeader>
<Table ref="tableRef"></Table>
<PopupForm />
</div>
</template>
<script setup lang="ts">
import { onMounted, provide, useTemplateRef } from 'vue'
import { useI18n } from 'vue-i18n'
import PopupForm from './popupForm.vue'
import { baTableApi } from '/@/api/common'
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'
defineOptions({
name: 'agent/commissionRecord',
})
const { t } = useI18n()
const tableRef = useTemplateRef('tableRef')
const optButtons: OptButton[] = defaultOptButtons(['edit', 'delete'])
const baTable = new baTableClass(
new baTableApi('/admin/agent.CommissionRecord/'),
{
pk: 'id',
column: [
{ type: 'selection', align: 'center', operator: false },
{ label: t('agent.commissionRecord.id'), prop: 'id', align: 'center', width: 80, operator: 'RANGE', sortable: 'custom' },
{ label: t('agent.commissionRecord.settlement_period_id'), prop: 'settlement_period_id', align: 'center', width: 130, operator: 'RANGE' },
{ label: t('agent.commissionRecord.channel_id'), prop: 'channel_id', align: 'center', width: 100, operator: 'RANGE' },
{ label: t('agent.commissionRecord.admin_id'), prop: 'admin_id', align: 'center', width: 100, operator: 'RANGE' },
{ label: t('agent.commissionRecord.commission_rate'), prop: 'commission_rate', align: 'center', minWidth: 110, operator: 'RANGE' },
{ label: t('agent.commissionRecord.calc_base_amount'), prop: 'calc_base_amount', align: 'center', minWidth: 120, operator: 'RANGE' },
{ label: t('agent.commissionRecord.commission_amount'), prop: 'commission_amount', align: 'center', minWidth: 120, operator: 'RANGE' },
{
label: t('agent.commissionRecord.status'),
prop: 'status',
align: 'center',
width: 100,
operator: 'eq',
render: 'tag',
replaceValue: { '0': t('agent.commissionRecord.status 0'), '1': t('agent.commissionRecord.status 1'), '2': t('agent.commissionRecord.status 2') },
},
{ label: t('agent.commissionRecord.settled_at'), prop: 'settled_at', align: 'center', render: 'datetime', operator: 'RANGE', comSearchRender: 'datetime', width: 170, sortable: 'custom', timeFormat: 'yyyy-mm-dd hh:MM:ss' },
{ label: t('agent.commissionRecord.remark'), prop: 'remark', align: 'center', minWidth: 160, operator: 'LIKE', operatorPlaceholder: t('Fuzzy query'), showOverflowTooltip: true },
{ label: t('agent.commissionRecord.create_time'), prop: 'create_time', align: 'center', render: 'datetime', operator: 'RANGE', comSearchRender: 'datetime', width: 170, sortable: 'custom', timeFormat: 'yyyy-mm-dd hh:MM:ss' },
{ label: t('agent.commissionRecord.update_time'), prop: 'update_time', align: 'center', render: 'datetime', operator: 'RANGE', comSearchRender: 'datetime', width: 170, sortable: 'custom', timeFormat: 'yyyy-mm-dd hh:MM:ss' },
{ label: t('Operate'), align: 'center', width: 100, render: 'buttons', buttons: optButtons, operator: false, fixed: 'right' },
],
},
{
defaultItems: { status: 0, commission_rate: '0.0000' },
}
)
provide('baTable', baTable)
onMounted(() => {
baTable.table.ref = tableRef.value
baTable.mount()
baTable.getData()?.then(() => {
baTable.initSort()
baTable.dragSort()
})
})
</script>
<style scoped lang="scss"></style>

View File

@@ -0,0 +1,49 @@
<template>
<el-dialog class="ba-operate-dialog" :close-on-click-modal="false" :model-value="['Add', 'Edit'].includes(baTable.form.operate!)" @close="baTable.toggleForm">
<template #header>
<div class="title" v-drag="['.ba-operate-dialog', '.el-dialog__header']" v-zoom="'.ba-operate-dialog'">{{ baTable.form.operate ? t(baTable.form.operate) : '' }}</div>
</template>
<el-scrollbar v-loading="baTable.form.loading" class="ba-table-form-scrollbar">
<div class="ba-operate-form" :class="'ba-' + baTable.form.operate + '-form'" :style="config.layout.shrink ? '' : 'width: calc(100% - ' + baTable.form.labelWidth! / 2 + 'px)'">
<el-form v-if="!baTable.form.loading" ref="formRef" @submit.prevent="" @keyup.enter="baTable.onSubmit(formRef)" :model="baTable.form.items" :label-position="config.layout.shrink ? 'top' : 'right'" :label-width="baTable.form.labelWidth + 'px'" :rules="rules">
<FormItem :label="t('agent.commissionRecord.settlement_period_id')" type="number" v-model="baTable.form.items!.settlement_period_id" prop="settlement_period_id" :input-attr="{ min: 1, step: 1 }" />
<FormItem :label="t('agent.commissionRecord.channel_id')" type="number" v-model="baTable.form.items!.channel_id" prop="channel_id" :input-attr="{ min: 1, step: 1 }" />
<FormItem :label="t('agent.commissionRecord.admin_id')" type="number" v-model="baTable.form.items!.admin_id" prop="admin_id" :input-attr="{ min: 1, step: 1 }" />
<FormItem :label="t('agent.commissionRecord.commission_rate')" type="number" v-model="baTable.form.items!.commission_rate" prop="commission_rate" :input-attr="{ min: 0, precision: 4, step: 0.0001 }" />
<FormItem :label="t('agent.commissionRecord.calc_base_amount')" type="number" v-model="baTable.form.items!.calc_base_amount" prop="calc_base_amount" :input-attr="{ min: 0, precision: 4, step: 0.0001 }" />
<FormItem :label="t('agent.commissionRecord.commission_amount')" type="number" v-model="baTable.form.items!.commission_amount" prop="commission_amount" :input-attr="{ precision: 4, step: 0.0001 }" />
<FormItem :label="t('agent.commissionRecord.status')" type="radio" v-model="baTable.form.items!.status" prop="status" :input-attr="{ content: { '0': t('agent.commissionRecord.status 0'), '1': t('agent.commissionRecord.status 1'), '2': t('agent.commissionRecord.status 2') } }" />
<FormItem :label="t('agent.commissionRecord.settled_at')" type="datetime" v-model="baTable.form.items!.settled_at" prop="settled_at" />
<FormItem :label="t('agent.commissionRecord.remark')" type="textarea" v-model="baTable.form.items!.remark" prop="remark" :input-attr="{ rows: 2 }" />
</el-form>
</div>
</el-scrollbar>
<template #footer>
<div :style="'width: calc(100% - ' + baTable.form.labelWidth! / 1.8 + 'px)'">
<el-button @click="baTable.toggleForm()">{{ t('Cancel') }}</el-button>
<el-button v-blur :loading="baTable.form.submitLoading" @click="baTable.onSubmit(formRef)" type="primary">{{ baTable.form.operateIds && baTable.form.operateIds.length > 1 ? t('Save and edit next item') : t('Save') }}</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import type { FormItemRule } from 'element-plus'
import { inject, reactive, useTemplateRef } from 'vue'
import { useI18n } from 'vue-i18n'
import FormItem from '/@/components/formItem/index.vue'
import { useConfig } from '/@/stores/config'
import type baTableClass from '/@/utils/baTable'
const config = useConfig()
const formRef = useTemplateRef('formRef')
const baTable = inject('baTable') as baTableClass
const { t } = useI18n()
const rules: Partial<Record<string, FormItemRule[]>> = reactive({
settlement_period_id: [{ required: true, message: t('Please input field', { field: t('agent.commissionRecord.settlement_period_id') }) }],
})
</script>
<style scoped lang="scss"></style>

View File

@@ -0,0 +1,79 @@
<template>
<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 />
<TableHeader
:buttons="['refresh', 'add', 'edit', 'delete', 'comSearch', 'quickSearch', 'columnDisplay']"
:quick-search-placeholder="t('Quick search placeholder', { fields: t('agent.settlementPeriod.quick Search Fields') })"
></TableHeader>
<Table ref="tableRef"></Table>
<PopupForm />
</div>
</template>
<script setup lang="ts">
import { onMounted, provide, useTemplateRef } from 'vue'
import { useI18n } from 'vue-i18n'
import PopupForm from './popupForm.vue'
import { baTableApi } from '/@/api/common'
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'
defineOptions({
name: 'agent/settlementPeriod',
})
const { t } = useI18n()
const tableRef = useTemplateRef('tableRef')
const optButtons: OptButton[] = defaultOptButtons(['edit', 'delete'])
const baTable = new baTableClass(
new baTableApi('/admin/agent.SettlementPeriod/'),
{
pk: 'id',
column: [
{ type: 'selection', align: 'center', operator: false },
{ label: t('agent.settlementPeriod.id'), prop: 'id', align: 'center', width: 80, operator: 'RANGE', sortable: 'custom' },
{ label: t('agent.settlementPeriod.settlement_no'), prop: 'settlement_no', align: 'center', minWidth: 160, operator: 'LIKE', operatorPlaceholder: t('Fuzzy query') },
{ label: t('agent.settlementPeriod.period_start_at'), prop: 'period_start_at', align: 'center', render: 'datetime', operator: 'RANGE', comSearchRender: 'datetime', width: 170, sortable: 'custom', timeFormat: 'yyyy-mm-dd hh:MM:ss' },
{ label: t('agent.settlementPeriod.period_end_at'), prop: 'period_end_at', align: 'center', render: 'datetime', operator: 'RANGE', comSearchRender: 'datetime', width: 170, sortable: 'custom', timeFormat: 'yyyy-mm-dd hh:MM:ss' },
{ label: t('agent.settlementPeriod.total_bet_amount'), prop: 'total_bet_amount', align: 'center', operator: 'RANGE', minWidth: 120 },
{ label: t('agent.settlementPeriod.total_payout_amount'), prop: 'total_payout_amount', align: 'center', operator: 'RANGE', minWidth: 120 },
{ label: t('agent.settlementPeriod.platform_profit_amount'), prop: 'platform_profit_amount', align: 'center', operator: 'RANGE', minWidth: 120 },
{
label: t('agent.settlementPeriod.status'),
prop: 'status',
align: 'center',
width: 100,
operator: 'eq',
render: 'tag',
replaceValue: { '0': t('agent.settlementPeriod.status 0'), '1': t('agent.settlementPeriod.status 1'), '2': t('agent.settlementPeriod.status 2'), '3': t('agent.settlementPeriod.status 3') },
},
{ label: t('agent.settlementPeriod.remark'), prop: 'remark', align: 'center', minWidth: 160, operator: 'LIKE', operatorPlaceholder: t('Fuzzy query'), showOverflowTooltip: true },
{ label: t('agent.settlementPeriod.create_time'), prop: 'create_time', align: 'center', render: 'datetime', operator: 'RANGE', comSearchRender: 'datetime', width: 170, sortable: 'custom', timeFormat: 'yyyy-mm-dd hh:MM:ss' },
{ label: t('agent.settlementPeriod.update_time'), prop: 'update_time', align: 'center', render: 'datetime', operator: 'RANGE', comSearchRender: 'datetime', width: 170, sortable: 'custom', timeFormat: 'yyyy-mm-dd hh:MM:ss' },
{ label: t('Operate'), align: 'center', width: 100, render: 'buttons', buttons: optButtons, operator: false, fixed: 'right' },
],
},
{
defaultItems: { status: 0 },
}
)
provide('baTable', baTable)
onMounted(() => {
baTable.table.ref = tableRef.value
baTable.mount()
baTable.getData()?.then(() => {
baTable.initSort()
baTable.dragSort()
})
})
</script>
<style scoped lang="scss"></style>

View File

@@ -0,0 +1,48 @@
<template>
<el-dialog class="ba-operate-dialog" :close-on-click-modal="false" :model-value="['Add', 'Edit'].includes(baTable.form.operate!)" @close="baTable.toggleForm">
<template #header>
<div class="title" v-drag="['.ba-operate-dialog', '.el-dialog__header']" v-zoom="'.ba-operate-dialog'">{{ baTable.form.operate ? t(baTable.form.operate) : '' }}</div>
</template>
<el-scrollbar v-loading="baTable.form.loading" class="ba-table-form-scrollbar">
<div class="ba-operate-form" :class="'ba-' + baTable.form.operate + '-form'" :style="config.layout.shrink ? '' : 'width: calc(100% - ' + baTable.form.labelWidth! / 2 + 'px)'">
<el-form v-if="!baTable.form.loading" ref="formRef" @submit.prevent="" @keyup.enter="baTable.onSubmit(formRef)" :model="baTable.form.items" :label-position="config.layout.shrink ? 'top' : 'right'" :label-width="baTable.form.labelWidth + 'px'" :rules="rules">
<FormItem :label="t('agent.settlementPeriod.settlement_no')" type="string" v-model="baTable.form.items!.settlement_no" prop="settlement_no" />
<FormItem :label="t('agent.settlementPeriod.period_start_at')" type="datetime" v-model="baTable.form.items!.period_start_at" prop="period_start_at" />
<FormItem :label="t('agent.settlementPeriod.period_end_at')" type="datetime" v-model="baTable.form.items!.period_end_at" prop="period_end_at" />
<FormItem :label="t('agent.settlementPeriod.total_bet_amount')" type="number" v-model="baTable.form.items!.total_bet_amount" prop="total_bet_amount" :input-attr="{ min: 0, precision: 4, step: 0.0001 }" />
<FormItem :label="t('agent.settlementPeriod.total_payout_amount')" type="number" v-model="baTable.form.items!.total_payout_amount" prop="total_payout_amount" :input-attr="{ min: 0, precision: 4, step: 0.0001 }" />
<FormItem :label="t('agent.settlementPeriod.platform_profit_amount')" type="number" v-model="baTable.form.items!.platform_profit_amount" prop="platform_profit_amount" :input-attr="{ precision: 4, step: 0.0001 }" />
<FormItem :label="t('agent.settlementPeriod.status')" type="radio" v-model="baTable.form.items!.status" prop="status" :input-attr="{ content: { '0': t('agent.settlementPeriod.status 0'), '1': t('agent.settlementPeriod.status 1'), '2': t('agent.settlementPeriod.status 2'), '3': t('agent.settlementPeriod.status 3') } }" />
<FormItem :label="t('agent.settlementPeriod.remark')" type="textarea" v-model="baTable.form.items!.remark" prop="remark" :input-attr="{ rows: 2 }" />
</el-form>
</div>
</el-scrollbar>
<template #footer>
<div :style="'width: calc(100% - ' + baTable.form.labelWidth! / 1.8 + 'px)'">
<el-button @click="baTable.toggleForm()">{{ t('Cancel') }}</el-button>
<el-button v-blur :loading="baTable.form.submitLoading" @click="baTable.onSubmit(formRef)" type="primary">{{ baTable.form.operateIds && baTable.form.operateIds.length > 1 ? t('Save and edit next item') : t('Save') }}</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import type { FormItemRule } from 'element-plus'
import { inject, reactive, useTemplateRef } from 'vue'
import { useI18n } from 'vue-i18n'
import FormItem from '/@/components/formItem/index.vue'
import { useConfig } from '/@/stores/config'
import type baTableClass from '/@/utils/baTable'
const config = useConfig()
const formRef = useTemplateRef('formRef')
const baTable = inject('baTable') as baTableClass
const { t } = useI18n()
const rules: Partial<Record<string, FormItemRule[]>> = reactive({
settlement_no: [{ required: true, message: t('Please input field', { field: t('agent.settlementPeriod.settlement_no') }) }],
})
</script>
<style scoped lang="scss"></style>

View File

@@ -0,0 +1,127 @@
<template>
<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 />
<TableHeader
:buttons="['refresh', 'add', 'edit', 'delete', 'comSearch', 'quickSearch', 'columnDisplay']"
:quick-search-placeholder="t('Quick search placeholder', { fields: t('config.gameConfig.quick Search Fields') })"
></TableHeader>
<Table ref="tableRef"></Table>
<PopupForm />
</div>
</template>
<script setup lang="ts">
import { onMounted, provide, useTemplateRef } from 'vue'
import { useI18n } from 'vue-i18n'
import PopupForm from './popupForm.vue'
import { baTableApi } from '/@/api/common'
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'
defineOptions({
name: 'config/gameConfig',
})
const { t } = useI18n()
const tableRef = useTemplateRef('tableRef')
const optButtons: OptButton[] = defaultOptButtons(['edit', 'delete'])
const baTable = new baTableClass(
new baTableApi('/admin/config.GameConfig/'),
{
pk: 'id',
column: [
{ type: 'selection', align: 'center', operator: false },
{ label: t('config.gameConfig.id'), prop: 'id', align: 'center', width: 80, operator: 'RANGE', sortable: 'custom' },
{
label: t('config.gameConfig.config_key'),
prop: 'config_key',
align: 'center',
minWidth: 200,
operatorPlaceholder: t('Fuzzy query'),
operator: 'LIKE',
showOverflowTooltip: true,
},
{
label: t('config.gameConfig.config_value'),
prop: 'config_value',
align: 'center',
minWidth: 160,
operatorPlaceholder: t('Fuzzy query'),
operator: 'LIKE',
showOverflowTooltip: true,
},
{
label: t('config.gameConfig.value_type'),
prop: 'value_type',
align: 'center',
width: 110,
operator: 'eq',
render: 'tag',
replaceValue: {
string: t('config.gameConfig.value_type string'),
int: t('config.gameConfig.value_type int'),
decimal: t('config.gameConfig.value_type decimal'),
json: t('config.gameConfig.value_type json'),
},
},
{
label: t('config.gameConfig.remark'),
prop: 'remark',
align: 'center',
minWidth: 200,
operatorPlaceholder: t('Fuzzy query'),
operator: 'LIKE',
showOverflowTooltip: true,
},
{
label: t('config.gameConfig.create_time'),
prop: 'create_time',
align: 'center',
render: 'datetime',
operator: 'RANGE',
comSearchRender: 'datetime',
sortable: 'custom',
width: 170,
timeFormat: 'yyyy-mm-dd hh:MM:ss',
},
{
label: t('config.gameConfig.update_time'),
prop: 'update_time',
align: 'center',
render: 'datetime',
operator: 'RANGE',
comSearchRender: 'datetime',
sortable: 'custom',
width: 170,
timeFormat: 'yyyy-mm-dd hh:MM:ss',
},
{ label: t('Operate'), align: 'center', width: 100, render: 'buttons', buttons: optButtons, operator: false, fixed: 'right' },
],
dblClickNotEditColumn: [undefined],
},
{
defaultItems: { value_type: 'string', remark: '' },
}
)
provide('baTable', baTable)
onMounted(() => {
baTable.table.ref = tableRef.value
baTable.mount()
baTable.getData()?.then(() => {
baTable.initSort()
baTable.dragSort()
})
})
</script>
<style scoped lang="scss"></style>

View File

@@ -0,0 +1,107 @@
<template>
<el-dialog
class="ba-operate-dialog"
:close-on-click-modal="false"
:model-value="['Add', 'Edit'].includes(baTable.form.operate!)"
@close="baTable.toggleForm"
>
<template #header>
<div class="title" v-drag="['.ba-operate-dialog', '.el-dialog__header']" v-zoom="'.ba-operate-dialog'">
{{ baTable.form.operate ? t(baTable.form.operate) : '' }}
</div>
</template>
<el-scrollbar v-loading="baTable.form.loading" class="ba-table-form-scrollbar">
<div
class="ba-operate-form"
:class="'ba-' + baTable.form.operate + '-form'"
:style="config.layout.shrink ? '' : 'width: calc(100% - ' + baTable.form.labelWidth! / 2 + 'px)'"
>
<el-form
v-if="!baTable.form.loading"
ref="formRef"
@submit.prevent=""
@keyup.enter="baTable.onSubmit(formRef)"
:model="baTable.form.items"
:label-position="config.layout.shrink ? 'top' : 'right'"
:label-width="baTable.form.labelWidth + 'px'"
:rules="rules"
>
<FormItem
:label="t('config.gameConfig.config_key')"
type="string"
v-model="baTable.form.items!.config_key"
prop="config_key"
:placeholder="t('Please input field', { field: t('config.gameConfig.config_key') })"
:input-attr="{ disabled: baTable.form.operate === 'Edit' }"
/>
<FormItem
:label="t('config.gameConfig.config_value')"
type="textarea"
v-model="baTable.form.items!.config_value"
prop="config_value"
:input-attr="{ rows: 5 }"
:placeholder="t('Please input field', { field: t('config.gameConfig.config_value') })"
/>
<FormItem
:label="t('config.gameConfig.value_type')"
type="radio"
v-model="baTable.form.items!.value_type"
prop="value_type"
:input-attr="{
content: {
string: t('config.gameConfig.value_type string'),
int: t('config.gameConfig.value_type int'),
decimal: t('config.gameConfig.value_type decimal'),
json: t('config.gameConfig.value_type json'),
},
}"
/>
<FormItem
:label="t('config.gameConfig.remark')"
type="textarea"
v-model="baTable.form.items!.remark"
prop="remark"
:input-attr="{ rows: 2 }"
/>
</el-form>
</div>
</el-scrollbar>
<template #footer>
<div :style="'width: calc(100% - ' + baTable.form.labelWidth! / 1.8 + 'px)'">
<el-button @click="baTable.toggleForm()">{{ t('Cancel') }}</el-button>
<el-button v-blur :loading="baTable.form.submitLoading" @click="baTable.onSubmit(formRef)" type="primary">
{{ baTable.form.operateIds && baTable.form.operateIds.length > 1 ? t('Save and edit next item') : t('Save') }}
</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import type { FormItemRule } from 'element-plus'
import { inject, reactive, useTemplateRef } from 'vue'
import { useI18n } from 'vue-i18n'
import FormItem from '/@/components/formItem/index.vue'
import { useConfig } from '/@/stores/config'
import type baTableClass from '/@/utils/baTable'
const config = useConfig()
const formRef = useTemplateRef('formRef')
const baTable = inject('baTable') as baTableClass
const { t } = useI18n()
const rules: Partial<Record<string, FormItemRule[]>> = reactive({
config_key: [buildRequired('config.gameConfig.config_key')],
value_type: [buildRequired('config.gameConfig.value_type')],
})
function buildRequired(langKey: string): FormItemRule {
return {
required: true,
message: t('Please input field', { field: t(langKey) }),
}
}
</script>
<style scoped lang="scss"></style>

View File

@@ -0,0 +1,104 @@
<template>
<div class="default-main ba-table-box zi-hua-dict-page">
<el-alert type="info" :closable="false" show-icon>
{{ t('config.ziHuaDictionary.desc') }}
</el-alert>
<div class="toolbar">
<el-button type="primary" :loading="saving" :disabled="loading" @click="onSave">
{{ t('config.ziHuaDictionary.btn_save') }}
</el-button>
</div>
<el-table v-loading="loading" border stripe :data="items" row-key="no" max-height="640">
<el-table-column prop="no" :label="t('config.ziHuaDictionary.no')" width="72" align="center" />
<el-table-column :label="t('config.ziHuaDictionary.name')" min-width="120">
<template #default="{ row }">
<el-input v-model="row.name" maxlength="32" show-word-limit />
</template>
</el-table-column>
<el-table-column :label="t('config.ziHuaDictionary.category')" min-width="160">
<template #default="{ row }">
<el-select v-model="row.category" style="width: 100%">
<el-option v-for="c in categories" :key="c" :label="categoryLabel(c)" :value="c" />
</el-select>
</template>
</el-table-column>
</el-table>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import createAxios from '/@/utils/axios'
import { auth } from '/@/utils/common'
defineOptions({
name: 'config/ziHuaDictionary',
})
const { t } = useI18n()
type Item = { no: number; name: string; category: string }
const loading = ref(false)
const saving = ref(false)
const items = ref<Item[]>([])
const categories = ref<string[]>(['zodiac', 'beast', 'fowl', 'vermin', 'divine'])
function categoryLabel(cat: string): string {
return t('config.ziHuaDictionary.category_label.' + cat)
}
async function load() {
loading.value = true
try {
const res = await createAxios({
url: '/admin/config.ZiHuaDictionary/index',
method: 'get',
})
if (res.code === 1 && res.data) {
const list = res.data.items as Item[]
items.value = Array.isArray(list) ? list : []
if (Array.isArray(res.data.categories) && res.data.categories.length) {
categories.value = res.data.categories as string[]
}
}
} finally {
loading.value = false
}
}
async function onSave() {
if (!auth('save')) {
return
}
saving.value = true
try {
await createAxios({
url: '/admin/config.ZiHuaDictionary/save',
method: 'post',
data: { items: items.value },
showSuccessMessage: true,
})
await load()
} finally {
saving.value = false
}
}
onMounted(() => {
void load()
})
</script>
<style scoped lang="scss">
.zi-hua-dict-page {
.toolbar {
margin: 12px 0;
}
}
</style>

View File

@@ -4,7 +4,7 @@
<TableHeader
:buttons="['refresh', 'comSearch', 'quickSearch', 'columnDisplay']"
:quick-search-placeholder="t('Quick search placeholder', { fields: t('game.betOrder.quick Search Fields') })"
:quick-search-placeholder="t('Quick search placeholder', { fields: t('order.betOrder.quick Search Fields') })"
></TableHeader>
<Table ref="tableRef"></Table>
@@ -20,7 +20,7 @@ import Table from '/@/components/table/index.vue'
import baTableClass from '/@/utils/baTable'
defineOptions({
name: 'game/betOrder',
name: 'order/betOrder',
})
const { t } = useI18n()
@@ -53,14 +53,14 @@ function formatAmount(_row: anyObj, _column: any, cellValue: unknown) {
}
const baTable = new baTableClass(
new baTableApi('/admin/game.BetOrder/'),
new baTableApi('/admin/order.BetOrder/'),
{
pk: 'id',
column: [
{ label: t('game.betOrder.id'), prop: 'id', align: 'center', width: 100, operator: 'RANGE', sortable: 'custom' },
{ label: t('game.betOrder.period_id'), prop: 'period_id', align: 'center', width: 100, operator: 'RANGE' },
{ label: t('order.betOrder.id'), prop: 'id', align: 'center', width: 100, operator: 'RANGE', sortable: 'custom' },
{ label: t('order.betOrder.period_id'), prop: 'period_id', align: 'center', width: 100, operator: 'RANGE' },
{
label: t('game.betOrder.period_no'),
label: t('order.betOrder.period_no'),
prop: 'period_no',
align: 'center',
minWidth: 160,
@@ -68,7 +68,7 @@ const baTable = new baTableClass(
operator: 'LIKE',
},
{
label: t('game.betOrder.gamePeriod.period_no'),
label: t('order.betOrder.gamePeriod_period_no'),
prop: 'gamePeriod.period_no',
align: 'center',
minWidth: 160,
@@ -77,25 +77,25 @@ const baTable = new baTableClass(
render: 'tags',
},
{
label: t('game.betOrder.gamePeriod.status'),
label: t('order.betOrder.gamePeriod_status'),
prop: 'gamePeriod.status',
align: 'center',
width: 100,
operator: 'eq',
render: 'tag',
replaceValue: {
'0': t('game.period.status 0'),
'1': t('game.period.status 1'),
'2': t('game.period.status 2'),
'3': t('game.period.status 3'),
'4': t('game.period.status 4'),
'5': t('game.period.status 5'),
'0': '下注开放',
'1': '已封盘',
'2': '算票中',
'3': '派彩中',
'4': '已结束',
'5': '已作废',
},
},
{ label: t('game.betOrder.user_id'), prop: 'user_id', align: 'center', width: 90, operator: 'RANGE' },
{ label: t('order.betOrder.user_id'), prop: 'user_id', align: 'center', width: 90, operator: 'RANGE' },
{
label: t('game.betOrder.gameUser.username'),
prop: 'gameUser.username',
label: t('order.betOrder.user_username'),
prop: 'user.username',
align: 'center',
minWidth: 100,
operatorPlaceholder: t('Fuzzy query'),
@@ -103,7 +103,7 @@ const baTable = new baTableClass(
render: 'tags',
},
{
label: t('game.betOrder.channel.name'),
label: t('order.betOrder.channel_name'),
prop: 'channel.name',
align: 'center',
minWidth: 100,
@@ -112,7 +112,7 @@ const baTable = new baTableClass(
render: 'tags',
},
{
label: t('game.betOrder.pick_numbers'),
label: t('order.betOrder.pick_numbers'),
prop: 'pick_numbers',
align: 'center',
minWidth: 120,
@@ -120,37 +120,37 @@ const baTable = new baTableClass(
formatter: formatPickNumbers,
},
{
label: t('game.betOrder.unit_amount'),
label: t('order.betOrder.unit_amount'),
prop: 'unit_amount',
align: 'center',
minWidth: 110,
operator: 'RANGE',
formatter: formatAmount,
},
{ label: t('game.betOrder.pick_count'), prop: 'pick_count', align: 'center', width: 90, operator: 'RANGE' },
{ label: t('order.betOrder.pick_count'), prop: 'pick_count', align: 'center', width: 90, operator: 'RANGE' },
{
label: t('game.betOrder.total_amount'),
label: t('order.betOrder.total_amount'),
prop: 'total_amount',
align: 'center',
minWidth: 110,
operator: 'RANGE',
formatter: formatAmount,
},
{ label: t('game.betOrder.streak_at_bet'), prop: 'streak_at_bet', align: 'center', width: 110, operator: 'RANGE' },
{ label: t('order.betOrder.streak_at_bet'), prop: 'streak_at_bet', align: 'center', width: 110, operator: 'RANGE' },
{
label: t('game.betOrder.is_auto'),
label: t('order.betOrder.is_auto'),
prop: 'is_auto',
align: 'center',
width: 90,
operator: 'eq',
render: 'tag',
replaceValue: {
'0': t('game.betOrder.is_auto 0'),
'1': t('game.betOrder.is_auto 1'),
'0': t('order.betOrder.is_auto 0'),
'1': t('order.betOrder.is_auto 1'),
},
},
{
label: t('game.betOrder.win_amount'),
label: t('order.betOrder.win_amount'),
prop: 'win_amount',
align: 'center',
minWidth: 110,
@@ -158,7 +158,7 @@ const baTable = new baTableClass(
formatter: formatAmount,
},
{
label: t('game.betOrder.jackpot_extra_amount'),
label: t('order.betOrder.jackpot_extra_amount'),
prop: 'jackpot_extra_amount',
align: 'center',
minWidth: 120,
@@ -166,20 +166,20 @@ const baTable = new baTableClass(
formatter: formatAmount,
},
{
label: t('game.betOrder.status'),
label: t('order.betOrder.status'),
prop: 'status',
align: 'center',
width: 100,
operator: 'eq',
render: 'tag',
replaceValue: {
'1': t('game.betOrder.status 1'),
'2': t('game.betOrder.status 2'),
'3': t('game.betOrder.status 3'),
'1': t('order.betOrder.status 1'),
'2': t('order.betOrder.status 2'),
'3': t('order.betOrder.status 3'),
},
},
{
label: t('game.betOrder.idempotency_key'),
label: t('order.betOrder.idempotency_key'),
prop: 'idempotency_key',
align: 'center',
minWidth: 140,
@@ -188,7 +188,7 @@ const baTable = new baTableClass(
showOverflowTooltip: true,
},
{
label: t('game.betOrder.create_time'),
label: t('order.betOrder.create_time'),
prop: 'create_time',
align: 'center',
render: 'datetime',
@@ -199,7 +199,7 @@ const baTable = new baTableClass(
timeFormat: 'yyyy-mm-dd hh:MM:ss',
},
{
label: t('game.betOrder.update_time'),
label: t('order.betOrder.update_time'),
prop: 'update_time',
align: 'center',
render: 'datetime',
@@ -228,3 +228,6 @@ onMounted(() => {
</script>
<style scoped lang="scss"></style>

View File

@@ -0,0 +1,153 @@
<template>
<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 />
<TableHeader
:buttons="['refresh', 'add', 'edit', 'delete', 'comSearch', 'quickSearch', 'columnDisplay']"
:quick-search-placeholder="t('Quick search placeholder', { fields: t('order.depositOrder.quick Search Fields') })"
></TableHeader>
<Table ref="tableRef"></Table>
<PopupForm />
</div>
</template>
<script setup lang="ts">
import { onMounted, provide, useTemplateRef } from 'vue'
import { useI18n } from 'vue-i18n'
import PopupForm from './popupForm.vue'
import { baTableApi } from '/@/api/common'
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'
defineOptions({
name: 'order/depositOrder',
})
const { t } = useI18n()
const tableRef = useTemplateRef('tableRef')
const optButtons: OptButton[] = defaultOptButtons(['edit', 'delete'])
const baTable = new baTableClass(
new baTableApi('/admin/order.DepositOrder/'),
{
pk: 'id',
column: [
{ type: 'selection', align: 'center', operator: false },
{ label: t('order.depositOrder.id'), prop: 'id', align: 'center', width: 80, operator: 'RANGE', sortable: 'custom' },
{
label: t('order.depositOrder.order_no'),
prop: 'order_no',
align: 'center',
minWidth: 170,
operator: 'LIKE',
operatorPlaceholder: t('Fuzzy query'),
},
{ label: t('order.depositOrder.user_id'), prop: 'user_id', align: 'center', width: 90, operator: 'RANGE' },
{
label: t('order.depositOrder.user_username'),
prop: 'user.username',
align: 'center',
minWidth: 110,
operator: 'LIKE',
operatorPlaceholder: t('Fuzzy query'),
render: 'tags',
},
{
label: t('order.depositOrder.channel_name'),
prop: 'channel.name',
align: 'center',
minWidth: 110,
operator: 'LIKE',
operatorPlaceholder: t('Fuzzy query'),
render: 'tags',
},
{ label: t('order.depositOrder.amount'), prop: 'amount', align: 'center', minWidth: 110, operator: 'RANGE' },
{
label: t('order.depositOrder.status'),
prop: 'status',
align: 'center',
width: 100,
operator: 'eq',
render: 'tag',
replaceValue: {
'0': t('order.depositOrder.status 0'),
'1': t('order.depositOrder.status 1'),
'2': t('order.depositOrder.status 2'),
'3': t('order.depositOrder.status 3'),
},
},
{
label: t('order.depositOrder.pay_channel'),
prop: 'pay_channel',
align: 'center',
minWidth: 110,
operator: 'LIKE',
operatorPlaceholder: t('Fuzzy query'),
},
{
label: t('order.depositOrder.pay_time'),
prop: 'pay_time',
align: 'center',
render: 'datetime',
operator: 'RANGE',
comSearchRender: 'datetime',
sortable: 'custom',
width: 170,
timeFormat: 'yyyy-mm-dd hh:MM:ss',
},
{
label: t('order.depositOrder.remark'),
prop: 'remark',
align: 'center',
minWidth: 150,
operator: 'LIKE',
operatorPlaceholder: t('Fuzzy query'),
showOverflowTooltip: true,
},
{
label: t('order.depositOrder.create_time'),
prop: 'create_time',
align: 'center',
render: 'datetime',
operator: 'RANGE',
comSearchRender: 'datetime',
sortable: 'custom',
width: 170,
timeFormat: 'yyyy-mm-dd hh:MM:ss',
},
{
label: t('order.depositOrder.update_time'),
prop: 'update_time',
align: 'center',
render: 'datetime',
operator: 'RANGE',
comSearchRender: 'datetime',
sortable: 'custom',
width: 170,
timeFormat: 'yyyy-mm-dd hh:MM:ss',
},
{ label: t('Operate'), align: 'center', width: 90, render: 'buttons', buttons: optButtons, operator: false, fixed: 'right' },
],
},
{
defaultItems: { status: 0, amount: '0.0000' },
}
)
provide('baTable', baTable)
onMounted(() => {
baTable.table.ref = tableRef.value
baTable.mount()
baTable.getData()?.then(() => {
baTable.initSort()
baTable.dragSort()
})
})
</script>
<style scoped lang="scss"></style>

View File

@@ -0,0 +1,48 @@
<template>
<el-dialog class="ba-operate-dialog" :close-on-click-modal="false" :model-value="['Add', 'Edit'].includes(baTable.form.operate!)" @close="baTable.toggleForm">
<template #header>
<div class="title" v-drag="['.ba-operate-dialog', '.el-dialog__header']" v-zoom="'.ba-operate-dialog'">{{ baTable.form.operate ? t(baTable.form.operate) : '' }}</div>
</template>
<el-scrollbar v-loading="baTable.form.loading" class="ba-table-form-scrollbar">
<div class="ba-operate-form" :class="'ba-' + baTable.form.operate + '-form'" :style="config.layout.shrink ? '' : 'width: calc(100% - ' + baTable.form.labelWidth! / 2 + 'px)'">
<el-form v-if="!baTable.form.loading" ref="formRef" @submit.prevent="" @keyup.enter="baTable.onSubmit(formRef)" :model="baTable.form.items" :label-position="config.layout.shrink ? 'top' : 'right'" :label-width="baTable.form.labelWidth + 'px'" :rules="rules">
<FormItem :label="t('order.depositOrder.order_no')" type="string" v-model="baTable.form.items!.order_no" prop="order_no" />
<FormItem :label="t('order.depositOrder.user_id')" type="number" v-model="baTable.form.items!.user_id" prop="user_id" :input-attr="{ min: 1, step: 1 }" />
<FormItem :label="t('order.depositOrder.channel_id')" type="number" v-model="baTable.form.items!.channel_id" prop="channel_id" :input-attr="{ min: 1, step: 1 }" />
<FormItem :label="t('order.depositOrder.amount')" type="number" v-model="baTable.form.items!.amount" prop="amount" :input-attr="{ step: 0.0001, precision: 4, min: 0 }" />
<FormItem :label="t('order.depositOrder.status')" type="radio" v-model="baTable.form.items!.status" prop="status" :input-attr="{ content: { '0': t('order.depositOrder.status 0'), '1': t('order.depositOrder.status 1'), '2': t('order.depositOrder.status 2'), '3': t('order.depositOrder.status 3') } }" />
<FormItem :label="t('order.depositOrder.pay_channel')" type="string" v-model="baTable.form.items!.pay_channel" prop="pay_channel" />
<FormItem :label="t('order.depositOrder.pay_time')" type="datetime" v-model="baTable.form.items!.pay_time" prop="pay_time" />
<FormItem :label="t('order.depositOrder.remark')" type="textarea" v-model="baTable.form.items!.remark" prop="remark" :input-attr="{ rows: 2 }" />
</el-form>
</div>
</el-scrollbar>
<template #footer>
<div :style="'width: calc(100% - ' + baTable.form.labelWidth! / 1.8 + 'px)'">
<el-button @click="baTable.toggleForm()">{{ t('Cancel') }}</el-button>
<el-button v-blur :loading="baTable.form.submitLoading" @click="baTable.onSubmit(formRef)" type="primary">{{ baTable.form.operateIds && baTable.form.operateIds.length > 1 ? t('Save and edit next item') : t('Save') }}</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import type { FormItemRule } from 'element-plus'
import { inject, reactive, useTemplateRef } from 'vue'
import { useI18n } from 'vue-i18n'
import FormItem from '/@/components/formItem/index.vue'
import { useConfig } from '/@/stores/config'
import type baTableClass from '/@/utils/baTable'
const config = useConfig()
const formRef = useTemplateRef('formRef')
const baTable = inject('baTable') as baTableClass
const { t } = useI18n()
const rules: Partial<Record<string, FormItemRule[]>> = reactive({
order_no: [{ required: true, message: t('Please input field', { field: t('order.depositOrder.order_no') }) }],
user_id: [{ required: true, message: t('Please input field', { field: t('order.depositOrder.user_id') }) }],
})
</script>
<style scoped lang="scss"></style>

View File

@@ -0,0 +1,82 @@
<template>
<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 />
<TableHeader
:buttons="['refresh', 'add', 'edit', 'delete', 'comSearch', 'quickSearch', 'columnDisplay']"
:quick-search-placeholder="t('Quick search placeholder', { fields: t('order.withdrawOrder.quick Search Fields') })"
></TableHeader>
<Table ref="tableRef"></Table>
<PopupForm />
</div>
</template>
<script setup lang="ts">
import { onMounted, provide, useTemplateRef } from 'vue'
import { useI18n } from 'vue-i18n'
import PopupForm from './popupForm.vue'
import { baTableApi } from '/@/api/common'
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'
defineOptions({
name: 'order/withdrawOrder',
})
const { t } = useI18n()
const tableRef = useTemplateRef('tableRef')
const optButtons: OptButton[] = defaultOptButtons(['edit', 'delete'])
const baTable = new baTableClass(
new baTableApi('/admin/order.WithdrawOrder/'),
{
pk: 'id',
column: [
{ type: 'selection', align: 'center', operator: false },
{ label: t('order.withdrawOrder.id'), prop: 'id', align: 'center', width: 80, operator: 'RANGE', sortable: 'custom' },
{ label: t('order.withdrawOrder.order_no'), prop: 'order_no', align: 'center', minWidth: 170, operator: 'LIKE', operatorPlaceholder: t('Fuzzy query') },
{ label: t('order.withdrawOrder.user_id'), prop: 'user_id', align: 'center', width: 90, operator: 'RANGE' },
{ label: t('order.withdrawOrder.user_username'), prop: 'user.username', align: 'center', minWidth: 110, operator: 'LIKE', operatorPlaceholder: t('Fuzzy query'), render: 'tags' },
{ label: t('order.withdrawOrder.channel_name'), prop: 'channel.name', align: 'center', minWidth: 110, operator: 'LIKE', operatorPlaceholder: t('Fuzzy query'), render: 'tags' },
{ label: t('order.withdrawOrder.amount'), prop: 'amount', align: 'center', minWidth: 110, operator: 'RANGE' },
{ label: t('order.withdrawOrder.fee'), prop: 'fee', align: 'center', minWidth: 110, operator: 'RANGE' },
{ label: t('order.withdrawOrder.actual_amount'), prop: 'actual_amount', align: 'center', minWidth: 110, operator: 'RANGE' },
{
label: t('order.withdrawOrder.status'),
prop: 'status',
align: 'center',
width: 100,
operator: 'eq',
render: 'tag',
replaceValue: { '0': t('order.withdrawOrder.status 0'), '1': t('order.withdrawOrder.status 1'), '2': t('order.withdrawOrder.status 2'), '3': t('order.withdrawOrder.status 3') },
},
{ label: t('order.withdrawOrder.review_admin_username'), prop: 'reviewAdmin.username', align: 'center', minWidth: 100, operator: 'LIKE', operatorPlaceholder: t('Fuzzy query'), render: 'tags' },
{ label: t('order.withdrawOrder.review_time'), prop: 'review_time', align: 'center', render: 'datetime', operator: 'RANGE', comSearchRender: 'datetime', sortable: 'custom', width: 170, timeFormat: 'yyyy-mm-dd hh:MM:ss' },
{ label: t('order.withdrawOrder.remark'), prop: 'remark', align: 'center', minWidth: 150, operator: 'LIKE', operatorPlaceholder: t('Fuzzy query'), showOverflowTooltip: true },
{ label: t('order.withdrawOrder.create_time'), prop: 'create_time', align: 'center', render: 'datetime', operator: 'RANGE', comSearchRender: 'datetime', sortable: 'custom', width: 170, timeFormat: 'yyyy-mm-dd hh:MM:ss' },
{ label: t('order.withdrawOrder.update_time'), prop: 'update_time', align: 'center', render: 'datetime', operator: 'RANGE', comSearchRender: 'datetime', sortable: 'custom', width: 170, timeFormat: 'yyyy-mm-dd hh:MM:ss' },
{ label: t('Operate'), align: 'center', width: 90, render: 'buttons', buttons: optButtons, operator: false, fixed: 'right' },
],
},
{
defaultItems: { status: 0, amount: '0.0000', fee: '0.0000', actual_amount: '0.0000' },
}
)
provide('baTable', baTable)
onMounted(() => {
baTable.table.ref = tableRef.value
baTable.mount()
baTable.getData()?.then(() => {
baTable.initSort()
baTable.dragSort()
})
})
</script>
<style scoped lang="scss"></style>

View File

@@ -0,0 +1,50 @@
<template>
<el-dialog class="ba-operate-dialog" :close-on-click-modal="false" :model-value="['Add', 'Edit'].includes(baTable.form.operate!)" @close="baTable.toggleForm">
<template #header>
<div class="title" v-drag="['.ba-operate-dialog', '.el-dialog__header']" v-zoom="'.ba-operate-dialog'">{{ baTable.form.operate ? t(baTable.form.operate) : '' }}</div>
</template>
<el-scrollbar v-loading="baTable.form.loading" class="ba-table-form-scrollbar">
<div class="ba-operate-form" :class="'ba-' + baTable.form.operate + '-form'" :style="config.layout.shrink ? '' : 'width: calc(100% - ' + baTable.form.labelWidth! / 2 + 'px)'">
<el-form v-if="!baTable.form.loading" ref="formRef" @submit.prevent="" @keyup.enter="baTable.onSubmit(formRef)" :model="baTable.form.items" :label-position="config.layout.shrink ? 'top' : 'right'" :label-width="baTable.form.labelWidth + 'px'" :rules="rules">
<FormItem :label="t('order.withdrawOrder.order_no')" type="string" v-model="baTable.form.items!.order_no" prop="order_no" />
<FormItem :label="t('order.withdrawOrder.user_id')" type="number" v-model="baTable.form.items!.user_id" prop="user_id" :input-attr="{ min: 1, step: 1 }" />
<FormItem :label="t('order.withdrawOrder.channel_id')" type="number" v-model="baTable.form.items!.channel_id" prop="channel_id" :input-attr="{ min: 1, step: 1 }" />
<FormItem :label="t('order.withdrawOrder.amount')" type="number" v-model="baTable.form.items!.amount" prop="amount" :input-attr="{ step: 0.0001, precision: 4, min: 0 }" />
<FormItem :label="t('order.withdrawOrder.fee')" type="number" v-model="baTable.form.items!.fee" prop="fee" :input-attr="{ step: 0.0001, precision: 4, min: 0 }" />
<FormItem :label="t('order.withdrawOrder.actual_amount')" type="number" v-model="baTable.form.items!.actual_amount" prop="actual_amount" :input-attr="{ step: 0.0001, precision: 4, min: 0 }" />
<FormItem :label="t('order.withdrawOrder.status')" type="radio" v-model="baTable.form.items!.status" prop="status" :input-attr="{ content: { '0': t('order.withdrawOrder.status 0'), '1': t('order.withdrawOrder.status 1'), '2': t('order.withdrawOrder.status 2'), '3': t('order.withdrawOrder.status 3') } }" />
<FormItem :label="t('order.withdrawOrder.review_admin_id')" type="number" v-model="baTable.form.items!.review_admin_id" prop="review_admin_id" :input-attr="{ min: 1, step: 1 }" />
<FormItem :label="t('order.withdrawOrder.review_time')" type="datetime" v-model="baTable.form.items!.review_time" prop="review_time" />
<FormItem :label="t('order.withdrawOrder.remark')" type="textarea" v-model="baTable.form.items!.remark" prop="remark" :input-attr="{ rows: 2 }" />
</el-form>
</div>
</el-scrollbar>
<template #footer>
<div :style="'width: calc(100% - ' + baTable.form.labelWidth! / 1.8 + 'px)'">
<el-button @click="baTable.toggleForm()">{{ t('Cancel') }}</el-button>
<el-button v-blur :loading="baTable.form.submitLoading" @click="baTable.onSubmit(formRef)" type="primary">{{ baTable.form.operateIds && baTable.form.operateIds.length > 1 ? t('Save and edit next item') : t('Save') }}</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import type { FormItemRule } from 'element-plus'
import { inject, reactive, useTemplateRef } from 'vue'
import { useI18n } from 'vue-i18n'
import FormItem from '/@/components/formItem/index.vue'
import { useConfig } from '/@/stores/config'
import type baTableClass from '/@/utils/baTable'
const config = useConfig()
const formRef = useTemplateRef('formRef')
const baTable = inject('baTable') as baTableClass
const { t } = useI18n()
const rules: Partial<Record<string, FormItemRule[]>> = reactive({
order_no: [{ required: true, message: t('Please input field', { field: t('order.withdrawOrder.order_no') }) }],
user_id: [{ required: true, message: t('Please input field', { field: t('order.withdrawOrder.user_id') }) }],
})
</script>
<style scoped lang="scss"></style>

View File

@@ -4,7 +4,7 @@
<TableHeader
:buttons="['refresh', 'comSearch', 'quickSearch', 'columnDisplay']"
:quick-search-placeholder="t('Quick search placeholder', { fields: t('game.walletRecord.quick Search Fields') })"
:quick-search-placeholder="t('Quick search placeholder', { fields: t('record.userWalletRecord.quick Search Fields') })"
></TableHeader>
<Table ref="tableRef"></Table>
@@ -20,7 +20,7 @@ import Table from '/@/components/table/index.vue'
import baTableClass from '/@/utils/baTable'
defineOptions({
name: 'game/walletRecord',
name: 'record/userWalletRecord',
})
const { t } = useI18n()
@@ -39,34 +39,34 @@ function formatAmount(_row: anyObj, _column: any, cellValue: unknown) {
}
const bizReplace = {
deposit: t('game.walletRecord.biz deposit'),
withdraw: t('game.walletRecord.biz withdraw'),
withdraw_freeze: t('game.walletRecord.biz withdraw_freeze'),
withdraw_unfreeze: t('game.walletRecord.biz withdraw_unfreeze'),
platform_in: t('game.walletRecord.biz platform_in'),
platform_out: t('game.walletRecord.biz platform_out'),
admin_credit: t('game.walletRecord.biz admin_credit'),
admin_deduct: t('game.walletRecord.biz admin_deduct'),
bet: t('game.walletRecord.biz bet'),
payout: t('game.walletRecord.biz payout'),
fee: t('game.walletRecord.biz fee'),
void_refund: t('game.walletRecord.biz void_refund'),
adjust: t('game.walletRecord.biz adjust'),
deposit: t('record.userWalletRecord.biz deposit'),
withdraw: t('record.userWalletRecord.biz withdraw'),
withdraw_freeze: t('record.userWalletRecord.biz withdraw_freeze'),
withdraw_unfreeze: t('record.userWalletRecord.biz withdraw_unfreeze'),
platform_in: t('record.userWalletRecord.biz platform_in'),
platform_out: t('record.userWalletRecord.biz platform_out'),
admin_credit: t('record.userWalletRecord.biz admin_credit'),
admin_deduct: t('record.userWalletRecord.biz admin_deduct'),
bet: t('record.userWalletRecord.biz bet'),
payout: t('record.userWalletRecord.biz payout'),
fee: t('record.userWalletRecord.biz fee'),
void_refund: t('record.userWalletRecord.biz void_refund'),
adjust: t('record.userWalletRecord.biz adjust'),
}
const dirReplace = {
'1': t('game.walletRecord.direction in'),
'2': t('game.walletRecord.direction out'),
'1': t('record.userWalletRecord.direction in'),
'2': t('record.userWalletRecord.direction out'),
}
const baTable = new baTableClass(
new baTableApi('/admin/game.UserWalletRecord/'),
new baTableApi('/admin/record.UserWalletRecord/'),
{
pk: 'id',
column: [
{ label: t('game.walletRecord.id'), prop: 'id', align: 'center', width: 100, operator: 'RANGE', sortable: 'custom' },
{ label: t('record.userWalletRecord.id'), prop: 'id', align: 'center', width: 100, operator: 'RANGE', sortable: 'custom' },
{
label: t('game.walletRecord.user_id'),
label: t('record.userWalletRecord.user_id'),
prop: 'user_id',
align: 'center',
width: 90,
@@ -74,8 +74,8 @@ const baTable = new baTableClass(
sortable: false,
},
{
label: t('game.walletRecord.game_user__username'),
prop: 'gameUser.username',
label: t('record.userWalletRecord.user__username'),
prop: 'user.username',
align: 'center',
minWidth: 110,
operatorPlaceholder: t('Fuzzy query'),
@@ -84,7 +84,7 @@ const baTable = new baTableClass(
comSearchRender: 'string',
},
{
label: t('game.walletRecord.channel__name'),
label: t('record.userWalletRecord.channel__name'),
prop: 'channel.name',
align: 'center',
minWidth: 100,
@@ -94,7 +94,7 @@ const baTable = new baTableClass(
comSearchRender: 'string',
},
{
label: t('game.walletRecord.biz_type'),
label: t('record.userWalletRecord.biz_type'),
prop: 'biz_type',
align: 'center',
minWidth: 120,
@@ -103,7 +103,7 @@ const baTable = new baTableClass(
replaceValue: bizReplace,
},
{
label: t('game.walletRecord.direction'),
label: t('record.userWalletRecord.direction'),
prop: 'direction',
align: 'center',
width: 90,
@@ -111,20 +111,20 @@ const baTable = new baTableClass(
render: 'tag',
replaceValue: dirReplace,
},
{ label: t('game.walletRecord.amount'), prop: 'amount', align: 'center', minWidth: 110, operator: 'RANGE', formatter: formatAmount },
{ label: t('game.walletRecord.balance_before'), prop: 'balance_before', align: 'center', minWidth: 110, operator: 'RANGE', formatter: formatAmount },
{ label: t('game.walletRecord.balance_after'), prop: 'balance_after', align: 'center', minWidth: 110, operator: 'RANGE', formatter: formatAmount },
{ label: t('record.userWalletRecord.amount'), prop: 'amount', align: 'center', minWidth: 110, operator: 'RANGE', formatter: formatAmount },
{ label: t('record.userWalletRecord.balance_before'), prop: 'balance_before', align: 'center', minWidth: 110, operator: 'RANGE', formatter: formatAmount },
{ label: t('record.userWalletRecord.balance_after'), prop: 'balance_after', align: 'center', minWidth: 110, operator: 'RANGE', formatter: formatAmount },
{
label: t('game.walletRecord.ref_type'),
label: t('record.userWalletRecord.ref_type'),
prop: 'ref_type',
align: 'center',
minWidth: 100,
showOverflowTooltip: true,
operator: 'LIKE',
},
{ label: t('game.walletRecord.ref_id'), prop: 'ref_id', align: 'center', width: 100, operator: 'RANGE' },
{ label: t('record.userWalletRecord.ref_id'), prop: 'ref_id', align: 'center', width: 100, operator: 'RANGE' },
{
label: t('game.walletRecord.idempotency_key'),
label: t('record.userWalletRecord.idempotency_key'),
prop: 'idempotency_key',
align: 'center',
minWidth: 120,
@@ -132,7 +132,7 @@ const baTable = new baTableClass(
operator: 'LIKE',
},
{
label: t('game.walletRecord.operator_admin__username'),
label: t('record.userWalletRecord.operator_admin__username'),
prop: 'operatorAdmin.username',
align: 'center',
minWidth: 100,
@@ -142,7 +142,7 @@ const baTable = new baTableClass(
comSearchRender: 'string',
},
{
label: t('game.walletRecord.remark'),
label: t('record.userWalletRecord.remark'),
prop: 'remark',
align: 'center',
minWidth: 140,
@@ -150,7 +150,7 @@ const baTable = new baTableClass(
operator: 'LIKE',
},
{
label: t('game.walletRecord.create_time'),
label: t('record.userWalletRecord.create_time'),
prop: 'create_time',
align: 'center',
render: 'datetime',
@@ -178,3 +178,5 @@ onMounted(() => {
</script>
<style scoped lang="scss"></style>

View File

@@ -1,10 +1,10 @@
<template>
<template>
<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 />
<TableHeader
:buttons="['refresh', 'add', 'edit', 'delete', 'comSearch', 'quickSearch', 'columnDisplay']"
:quick-search-placeholder="t('Quick search placeholder', { fields: t('game.user.quick Search Fields') })"
:quick-search-placeholder="t('Quick search placeholder', { fields: t('user.user.quick Search Fields') })"
></TableHeader>
<Table ref="tableRef"></Table>
@@ -24,7 +24,7 @@ import Table from '/@/components/table/index.vue'
import baTableClass from '/@/utils/baTable'
defineOptions({
name: 'game/user',
name: 'user/user',
})
const { t } = useI18n()
@@ -43,36 +43,36 @@ function formatCoin(_row: anyObj, _column: any, cellValue: unknown) {
return n.toFixed(4)
}
/** 返回多标签文案数组,供 render: tags 使用 */
/** 杩斿洖澶氭爣绛炬枃妗堟暟缁勶紝渚?render: tags 浣跨敤 */
function formatRiskFlags(row: anyObj, _column: any, cellValue: unknown) {
const raw = cellValue !== undefined && cellValue !== null && cellValue !== '' ? cellValue : row.risk_flags
const f = typeof raw === 'number' ? raw : parseInt(String(raw ?? '0'), 10) || 0
const parts: string[] = []
if (f & 1) parts.push(t('game.user.risk_no_login'))
if (f & 2) parts.push(t('game.user.risk_no_bet'))
if (f & 4) parts.push(t('game.user.risk_no_withdraw'))
if (!parts.length) parts.push(t('game.user.risk_none'))
if (f & 1) parts.push(t('user.user.risk_no_login'))
if (f & 2) parts.push(t('user.user.risk_no_bet'))
if (f & 4) parts.push(t('user.user.risk_no_withdraw'))
if (!parts.length) parts.push(t('user.user.risk_none'))
return parts
}
const baTable = new baTableClass(
new baTableApi('/admin/game.User/'),
new baTableApi('/admin/user.User/'),
{
pk: 'id',
column: [
{ type: 'selection', align: 'center', operator: false },
{ label: t('game.user.id'), prop: 'id', align: 'center', width: 70, operator: 'RANGE', sortable: 'custom' },
{ label: t('user.user.id'), prop: 'id', align: 'center', width: 70, operator: 'RANGE', sortable: 'custom' },
{
label: t('game.user.username'),
label: t('user.user.username'),
prop: 'username',
align: 'center',
operatorPlaceholder: t('Fuzzy query'),
sortable: false,
operator: 'LIKE',
},
{ label: t('game.user.phone'), prop: 'phone', align: 'center', operatorPlaceholder: t('Fuzzy query'), sortable: false, operator: 'LIKE' },
{ label: t('user.user.phone'), prop: 'phone', align: 'center', operatorPlaceholder: t('Fuzzy query'), sortable: false, operator: 'LIKE' },
{
label: t('game.user.email'),
label: t('user.user.email'),
prop: 'email',
align: 'center',
minWidth: 120,
@@ -81,7 +81,7 @@ const baTable = new baTableClass(
operator: 'LIKE',
},
{
label: t('game.user.head_image'),
label: t('user.user.head_image'),
prop: 'head_image',
align: 'center',
width: 72,
@@ -89,7 +89,7 @@ const baTable = new baTableClass(
render: 'image',
},
{
label: t('game.user.uuid'),
label: t('user.user.uuid'),
prop: 'uuid',
align: 'center',
showOverflowTooltip: true,
@@ -98,16 +98,16 @@ const baTable = new baTableClass(
operator: 'LIKE',
},
{
label: t('game.user.register_invite_code'),
label: t('user.user.register_invite_code'),
prop: 'register_invite_code',
align: 'center',
minWidth: 100,
showOverflowTooltip: true,
operator: 'LIKE',
},
{ label: t('game.user.coin'), prop: 'coin', align: 'center', sortable: false, operator: 'RANGE', formatter: formatCoin },
{ label: t('user.user.coin'), prop: 'coin', align: 'center', sortable: false, operator: 'RANGE', formatter: formatCoin },
{
label: t('game.user.total_deposit_coin'),
label: t('user.user.total_deposit_coin'),
prop: 'total_deposit_coin',
align: 'center',
minWidth: 110,
@@ -116,7 +116,7 @@ const baTable = new baTableClass(
formatter: formatCoin,
},
{
label: t('game.user.total_valid_bet_coin'),
label: t('user.user.total_valid_bet_coin'),
prop: 'total_valid_bet_coin',
align: 'center',
minWidth: 110,
@@ -125,7 +125,7 @@ const baTable = new baTableClass(
formatter: formatCoin,
},
{
label: t('game.user.risk_flags'),
label: t('user.user.risk_flags'),
prop: 'risk_flags',
align: 'center',
render: 'tags',
@@ -133,15 +133,15 @@ const baTable = new baTableClass(
operator: false,
formatter: formatRiskFlags,
custom: {
[t('game.user.risk_none')]: 'primary',
[t('game.user.risk_no_login')]: 'danger',
[t('game.user.risk_no_bet')]: 'danger',
[t('game.user.risk_no_withdraw')]: 'danger',
[t('user.user.risk_none')]: 'primary',
[t('user.user.risk_no_login')]: 'danger',
[t('user.user.risk_no_bet')]: 'danger',
[t('user.user.risk_no_withdraw')]: 'danger',
},
},
{ label: t('game.user.current_streak'), prop: 'current_streak', align: 'center', width: 90, operator: 'RANGE' },
{ label: t('user.user.current_streak'), prop: 'current_streak', align: 'center', width: 90, operator: 'RANGE' },
{
label: t('game.user.last_bet_period_no'),
label: t('user.user.last_bet_period_no'),
prop: 'last_bet_period_no',
align: 'center',
minWidth: 120,
@@ -149,16 +149,16 @@ const baTable = new baTableClass(
operator: 'LIKE',
},
{
label: t('game.user.status'),
label: t('user.user.status'),
prop: 'status',
align: 'center',
operator: 'eq',
sortable: false,
render: 'switch',
replaceValue: { '0': t('game.user.status 0'), '1': t('game.user.status 1') },
replaceValue: { '0': t('user.user.status 0'), '1': t('user.user.status 1') },
},
{
label: t('game.user.channel__name'),
label: t('user.user.channel__name'),
prop: 'channel.name',
align: 'center',
minWidth: 100,
@@ -168,7 +168,7 @@ const baTable = new baTableClass(
comSearchRender: 'string',
},
{
label: t('game.user.admin__username'),
label: t('user.user.admin__username'),
prop: 'admin.username',
align: 'center',
minWidth: 90,
@@ -185,7 +185,7 @@ const baTable = new baTableClass(
},
},
{
label: t('game.user.remark'),
label: t('user.user.remark'),
prop: 'remark',
align: 'center',
minWidth: 100,
@@ -193,7 +193,7 @@ const baTable = new baTableClass(
operatorPlaceholder: t('Fuzzy query'),
},
{
label: t('game.user.create_time'),
label: t('user.user.create_time'),
prop: 'create_time',
align: 'center',
render: 'datetime',
@@ -204,7 +204,7 @@ const baTable = new baTableClass(
timeFormat: 'yyyy-mm-dd hh:MM:ss',
},
{
label: t('game.user.update_time'),
label: t('user.user.update_time'),
prop: 'update_time',
align: 'center',
render: 'datetime',
@@ -247,3 +247,5 @@ onMounted(() => {
</script>
<style scoped lang="scss"></style>

View File

@@ -27,47 +27,47 @@
:rules="rules"
>
<!-- <el-alert class="game-user-form-tip" type="info" :closable="false" show-icon>-->
<!-- {{ t('game.user.form_tip') }}-->
<!-- {{ t('user.user.form_tip') }}-->
<!-- </el-alert>-->
<el-divider content-position="left">{{ t('game.user.section_basic') }}</el-divider>
<el-divider content-position="left">{{ t('user.user.section_basic') }}</el-divider>
<FormItem
:label="t('game.user.username')"
:label="t('user.user.username')"
type="string"
v-model="baTable.form.items!.username"
prop="username"
:placeholder="t('Please input field', { field: t('game.user.username') })"
:placeholder="t('Please input field', { field: t('user.user.username') })"
/>
<FormItem
:label="t('game.user.password')"
:label="t('user.user.password')"
type="password"
v-model="baTable.form.items!.password"
prop="password"
:placeholder="t('Please input field', { field: t('game.user.password') })"
:placeholder="t('Please input field', { field: t('user.user.password') })"
/>
<FormItem
:label="t('game.user.phone')"
:label="t('user.user.phone')"
type="string"
v-model="baTable.form.items!.phone"
prop="phone"
:placeholder="t('Please input field', { field: t('game.user.phone') })"
:placeholder="t('Please input field', { field: t('user.user.phone') })"
/>
<FormItem
:label="t('game.user.email')"
:label="t('user.user.email')"
type="string"
v-model="baTable.form.items!.email"
prop="email"
:placeholder="t('game.user.email_placeholder')"
:placeholder="t('user.user.email_placeholder')"
/>
<FormItem
:label="t('game.user.head_image')"
:label="t('user.user.head_image')"
type="image"
v-model="baTable.form.items!.head_image"
prop="head_image"
/>
<el-divider content-position="left">{{ t('game.user.section_admin_attribution') }}</el-divider>
<el-form-item :label="t('game.user.admin_affiliation')" prop="admin_id">
<el-divider content-position="left">{{ t('user.user.section_admin_attribution') }}</el-divider>
<el-form-item :label="t('user.user.admin_affiliation')" prop="admin_id">
<el-tree-select
v-model="baTable.form.items!.admin_id"
class="w100"
@@ -76,55 +76,55 @@
:data="adminScopeTree"
:props="treeProps"
:render-after-expand="false"
:placeholder="t('game.user.admin_affiliation_placeholder')"
:placeholder="t('user.user.admin_affiliation_placeholder')"
@update:model-value="onAdminTreeChange"
/>
</el-form-item>
<FormItem
v-if="baTable.form.operate === 'Edit'"
:label="t('game.user.uuid')"
:label="t('user.user.uuid')"
type="string"
v-model="baTable.form.items!.uuid"
prop="uuid"
:input-attr="{ readonly: true, disabled: true }"
/>
<el-divider content-position="left">{{ t('game.user.section_register') }}</el-divider>
<el-divider content-position="left">{{ t('user.user.section_register') }}</el-divider>
<FormItem
:label="t('game.user.register_invite_code')"
:label="t('user.user.register_invite_code')"
type="string"
v-model="baTable.form.items!.register_invite_code"
prop="register_invite_code"
:input-attr="{ readonly: true }"
:placeholder="t('game.user.register_invite_code_auto_placeholder')"
:placeholder="t('user.user.register_invite_code_auto_placeholder')"
/>
<el-divider content-position="left">{{ t('game.user.section_finance') }}</el-divider>
<el-divider content-position="left">{{ t('user.user.section_finance') }}</el-divider>
<FormItem
:label="t('game.user.coin')"
:label="t('user.user.coin')"
type="number"
v-model="baTable.form.items!.coin"
prop="coin"
:input-attr="{ step: 0.0001, min: 0, precision: 4 }"
:placeholder="t('game.user.coin_placeholder')"
:placeholder="t('user.user.coin_placeholder')"
/>
<FormItem
:label="t('game.user.total_deposit_coin')"
:label="t('user.user.total_deposit_coin')"
type="number"
v-model="baTable.form.items!.total_deposit_coin"
prop="total_deposit_coin"
:input-attr="{ step: 0.0001, min: 0, precision: 4 }"
/>
<FormItem
:label="t('game.user.total_valid_bet_coin')"
:label="t('user.user.total_valid_bet_coin')"
type="number"
v-model="baTable.form.items!.total_valid_bet_coin"
prop="total_valid_bet_coin"
:input-attr="{ step: 0.0001, min: 0, precision: 4 }"
/>
<el-divider content-position="left">{{ t('game.user.section_risk') }}</el-divider>
<el-form-item :label="t('game.user.risk_flags')">
<el-divider content-position="left">{{ t('user.user.section_risk') }}</el-divider>
<el-form-item :label="t('user.user.risk_flags')">
<div class="risk-flag-row">
<el-tag
:type="riskBits.noLogin ? 'danger' : 'info'"
@@ -132,7 +132,7 @@
class="risk-flag-tag"
@click="riskBits.noLogin = !riskBits.noLogin"
>
{{ t('game.user.risk_no_login') }}
{{ t('user.user.risk_no_login') }}
</el-tag>
<el-tag
:type="riskBits.noBet ? 'danger' : 'info'"
@@ -140,7 +140,7 @@
class="risk-flag-tag"
@click="riskBits.noBet = !riskBits.noBet"
>
{{ t('game.user.risk_no_bet') }}
{{ t('user.user.risk_no_bet') }}
</el-tag>
<el-tag
:type="riskBits.noWithdraw ? 'danger' : 'info'"
@@ -148,44 +148,44 @@
class="risk-flag-tag"
@click="riskBits.noWithdraw = !riskBits.noWithdraw"
>
{{ t('game.user.risk_no_withdraw') }}
{{ t('user.user.risk_no_withdraw') }}
</el-tag>
</div>
</el-form-item>
<el-divider content-position="left">{{ t('game.user.section_streak') }}</el-divider>
<el-divider content-position="left">{{ t('user.user.section_streak') }}</el-divider>
<FormItem
:label="t('game.user.current_streak')"
:label="t('user.user.current_streak')"
type="number"
v-model="baTable.form.items!.current_streak"
prop="current_streak"
:input-attr="{ step: 1, min: 0, precision: 0 }"
/>
<FormItem
:label="t('game.user.last_bet_period_no')"
:label="t('user.user.last_bet_period_no')"
type="string"
v-model="baTable.form.items!.last_bet_period_no"
prop="last_bet_period_no"
:placeholder="t('game.user.last_bet_period_no_placeholder')"
:placeholder="t('user.user.last_bet_period_no_placeholder')"
/>
<el-divider content-position="left">{{ t('game.user.section_other') }}</el-divider>
<el-divider content-position="left">{{ t('user.user.section_other') }}</el-divider>
<FormItem
:label="t('game.user.remark')"
:label="t('user.user.remark')"
type="textarea"
v-model="baTable.form.items!.remark"
prop="remark"
:input-attr="{ rows: 3 }"
@keyup.enter.stop=""
@keyup.ctrl.enter="baTable.onSubmit(formRef)"
:placeholder="t('Please input field', { field: t('game.user.remark') })"
:placeholder="t('Please input field', { field: t('user.user.remark') })"
/>
<FormItem
:label="t('game.user.status')"
:label="t('user.user.status')"
type="switch"
v-model="baTable.form.items!.status"
prop="status"
:input-attr="{ content: { '0': t('game.user.status 0'), '1': t('game.user.status 1') } }"
:input-attr="{ content: { '0': t('user.user.status 0'), '1': t('user.user.status 1') } }"
/>
</el-form>
</div>
@@ -263,7 +263,7 @@ function packRiskFlags(): number {
return r
}
/** 识别管理员叶子:value group_* 且无子节点(不依赖 is_leaf,避免 el-tree 丢弃自定义字段) */
/** 璇嗗埆绠$悊鍛樺彾瀛愶細value 闈?group_* 涓旀棤瀛愯妭鐐癸紙涓嶄緷璧?is_leaf锛岄伩鍏?el-tree 涓㈠純鑷畾涔夊瓧娈碉級 */
function isAdminTreeLeaf(n: TreeNode): boolean {
const v = n.value
if (v === undefined || v === null) return false
@@ -300,7 +300,7 @@ function buildAdminMapsFromTree(nodes: TreeNode[]) {
return { mapCh, mapInv }
}
/** 映射未命中时从原始树查找(防止 props 裁剪或异步时序问题) */
/** 鏄犲皠鏈懡涓椂浠庡師濮嬫爲鏌ユ壘锛堥槻姝?props 瑁佸壀鎴栧紓姝ユ椂搴忛棶棰橈級 */
function findAdminMetaInTree(nodes: TreeNode[], adminId: string): { channel_id?: number; invite_code: string } | null {
const target = String(adminId).trim()
for (const n of nodes) {
@@ -329,7 +329,7 @@ function findAdminMetaInTree(nodes: TreeNode[], adminId: string): { channel_id?:
const loadAdminScopeTree = async () => {
const res = await createAxios({
url: '/admin/game.User/adminScopeTree',
url: '/admin/user.User/adminScopeTree',
method: 'get',
})
const list = (res.data?.list ?? []) as TreeNode[]
@@ -389,7 +389,7 @@ watch(
}
)
//
//
watch(
() => [baTable.form.items?.admin_id, adminScopeTree.value.length] as const,
() => {
@@ -426,7 +426,7 @@ watch(
}
)
// el-tree-select value
// el-tree-select value
watch(
() => [baTable.form.operate, baTable.form.items?.id] as const,
() => {
@@ -445,7 +445,7 @@ const validatorGameUserPassword = (rule: any, val: string, callback: (error?: Er
const v = typeof val === 'string' ? val.trim() : ''
if (operate === 'Add') {
if (!v) return callback(new Error(t('Please input field', { field: t('game.user.password') })))
if (!v) return callback(new Error(t('Please input field', { field: t('user.user.password') })))
if (!regularPassword(v)) return callback(new Error(t('validate.Please enter the correct password')))
return callback()
}
@@ -470,15 +470,15 @@ const decimalRule = (fieldTitle: string): FormItemRule => ({
})
const rules: Partial<Record<string, FormItemRule[]>> = reactive({
username: [buildValidatorData({ name: 'required', title: t('game.user.username') })],
username: [buildValidatorData({ name: 'required', title: t('user.user.username') })],
password: [{ validator: validatorGameUserPassword, trigger: 'blur' }],
phone: [buildValidatorData({ name: 'required', title: t('game.user.phone') })],
coin: [decimalRule(t('game.user.coin'))],
total_deposit_coin: [decimalRule(t('game.user.total_deposit_coin'))],
total_valid_bet_coin: [decimalRule(t('game.user.total_valid_bet_coin'))],
admin_id: [buildValidatorData({ name: 'required', title: t('game.user.admin_affiliation') })],
create_time: [buildValidatorData({ name: 'date', title: t('game.user.create_time') })],
update_time: [buildValidatorData({ name: 'date', title: t('game.user.update_time') })],
phone: [buildValidatorData({ name: 'required', title: t('user.user.phone') })],
coin: [decimalRule(t('user.user.coin'))],
total_deposit_coin: [decimalRule(t('user.user.total_deposit_coin'))],
total_valid_bet_coin: [decimalRule(t('user.user.total_valid_bet_coin'))],
admin_id: [buildValidatorData({ name: 'required', title: t('user.user.admin_affiliation') })],
create_time: [buildValidatorData({ name: 'date', title: t('user.user.create_time') })],
update_time: [buildValidatorData({ name: 'date', title: t('user.user.update_time') })],
})
</script>
@@ -497,3 +497,5 @@ const rules: Partial<Record<string, FormItemRule[]>> = reactive({
user-select: none;
}
</style>