游戏-用户管理-优化样式增强验证
This commit is contained in:
@@ -4,6 +4,8 @@ namespace app\admin\controller\game;
|
|||||||
|
|
||||||
use Throwable;
|
use Throwable;
|
||||||
use app\common\controller\Backend;
|
use app\common\controller\Backend;
|
||||||
|
use app\common\service\GameChannelUserCount;
|
||||||
|
use support\think\Db;
|
||||||
use support\Response;
|
use support\Response;
|
||||||
use Webman\Http\Request as WebmanRequest;
|
use Webman\Http\Request as WebmanRequest;
|
||||||
|
|
||||||
@@ -35,6 +37,24 @@ class User extends Backend
|
|||||||
|
|
||||||
protected string|array $quickSearchField = ['id', 'username', 'phone'];
|
protected string|array $quickSearchField = ['id', 'username', 'phone'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 与渠道 Config 类似:从游戏配置拉取默认权重;辅助接口,需具备用户管理列表权限
|
||||||
|
*/
|
||||||
|
protected array $noNeedPermission = ['defaultWeightPresets', 'defaultWeightByChannel'];
|
||||||
|
|
||||||
|
/** game_weight 分组下的配置名 */
|
||||||
|
private const GC_GROUP_WEIGHT = 'game_weight';
|
||||||
|
|
||||||
|
private const GC_NAME_TIER = 'default_tier_weight';
|
||||||
|
|
||||||
|
private const GC_NAME_BIGWIN_PRIMARY = 'default_bigwin_weight';
|
||||||
|
|
||||||
|
/** 档位权重固定键:T1~T5 */
|
||||||
|
private const TIER_WEIGHT_KEYS = ['T1', 'T2', 'T3', 'T4', 'T5'];
|
||||||
|
|
||||||
|
/** 中大奖权重固定键:5~30 */
|
||||||
|
private const BIGWIN_WEIGHT_KEYS = ['5', '10', '15', '20', '25', '30'];
|
||||||
|
|
||||||
protected function initController(WebmanRequest $request): ?Response
|
protected function initController(WebmanRequest $request): ?Response
|
||||||
{
|
{
|
||||||
$this->model = new \app\common\model\GameUser();
|
$this->model = new \app\common\model\GameUser();
|
||||||
@@ -69,6 +89,10 @@ class User extends Backend
|
|||||||
}
|
}
|
||||||
$data['uuid'] = md5(trim($username) . '|' . $channelId);
|
$data['uuid'] = md5(trim($username) . '|' . $channelId);
|
||||||
|
|
||||||
|
if ($this->gameUserUsernameExistsInChannel($username, $channelId)) {
|
||||||
|
return $this->error(__('Game user username exists in channel'));
|
||||||
|
}
|
||||||
|
|
||||||
if (!$this->auth->isSuperAdmin()) {
|
if (!$this->auth->isSuperAdmin()) {
|
||||||
$allowed = $this->getDataLimitAdminIds();
|
$allowed = $this->getDataLimitAdminIds();
|
||||||
$adminIdNew = $data['admin_id'] ?? null;
|
$adminIdNew = $data['admin_id'] ?? null;
|
||||||
@@ -100,6 +124,14 @@ class User extends Backend
|
|||||||
return $this->error($e->getMessage());
|
return $this->error($e->getMessage());
|
||||||
}
|
}
|
||||||
if ($result !== false) {
|
if ($result !== false) {
|
||||||
|
$cid = $data['game_channel_id'] ?? $data['channel_id'] ?? null;
|
||||||
|
if (($cid === null || $cid === '') && $this->model) {
|
||||||
|
$rowData = $this->model->getData();
|
||||||
|
$cid = $rowData['game_channel_id'] ?? $rowData['channel_id'] ?? null;
|
||||||
|
}
|
||||||
|
if ($cid !== null && $cid !== '') {
|
||||||
|
GameChannelUserCount::syncFromGameUser($cid);
|
||||||
|
}
|
||||||
return $this->success(__('Added successfully'));
|
return $this->success(__('Added successfully'));
|
||||||
}
|
}
|
||||||
return $this->error(__('No rows were added'));
|
return $this->error(__('No rows were added'));
|
||||||
@@ -126,6 +158,8 @@ class User extends Backend
|
|||||||
return $this->error(__('You have no permission'));
|
return $this->error(__('You have no permission'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$oldChannelId = $row['game_channel_id'] ?? $row['channel_id'] ?? null;
|
||||||
|
|
||||||
if ($this->request && $this->request->method() === 'POST') {
|
if ($this->request && $this->request->method() === 'POST') {
|
||||||
$data = $this->request->post();
|
$data = $this->request->post();
|
||||||
if (!$data) {
|
if (!$data) {
|
||||||
@@ -156,6 +190,9 @@ class User extends Backend
|
|||||||
|
|
||||||
if (is_string($nextUsername) && trim($nextUsername) !== '' && $nextChannelId !== null && $nextChannelId !== '') {
|
if (is_string($nextUsername) && trim($nextUsername) !== '' && $nextChannelId !== null && $nextChannelId !== '') {
|
||||||
$data['uuid'] = md5(trim($nextUsername) . '|' . $nextChannelId);
|
$data['uuid'] = md5(trim($nextUsername) . '|' . $nextChannelId);
|
||||||
|
if ($this->gameUserUsernameExistsInChannel($nextUsername, $nextChannelId, $row[$pk])) {
|
||||||
|
return $this->error(__('Game user username exists in channel'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$this->auth->isSuperAdmin()) {
|
if (!$this->auth->isSuperAdmin()) {
|
||||||
@@ -187,6 +224,14 @@ class User extends Backend
|
|||||||
return $this->error($e->getMessage());
|
return $this->error($e->getMessage());
|
||||||
}
|
}
|
||||||
if ($result !== false) {
|
if ($result !== false) {
|
||||||
|
$merged = array_merge($row->toArray(), $data);
|
||||||
|
$newChannelId = $merged['game_channel_id'] ?? $merged['channel_id'] ?? null;
|
||||||
|
if ($newChannelId !== null && $newChannelId !== '') {
|
||||||
|
GameChannelUserCount::syncFromGameUser($newChannelId);
|
||||||
|
}
|
||||||
|
if ($oldChannelId !== null && $oldChannelId !== '' && (string) $oldChannelId !== (string) $newChannelId) {
|
||||||
|
GameChannelUserCount::syncFromGameUser($oldChannelId);
|
||||||
|
}
|
||||||
return $this->success(__('Update successful'));
|
return $this->success(__('Update successful'));
|
||||||
}
|
}
|
||||||
return $this->error(__('No rows updated'));
|
return $this->error(__('No rows updated'));
|
||||||
@@ -199,6 +244,51 @@ class User extends Backend
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除后按 game_user 重算相关渠道的 user_count
|
||||||
|
*
|
||||||
|
* @throws Throwable
|
||||||
|
*/
|
||||||
|
protected function _del(): Response
|
||||||
|
{
|
||||||
|
$where = [];
|
||||||
|
$dataLimitAdminIds = $this->getDataLimitAdminIds();
|
||||||
|
if ($dataLimitAdminIds) {
|
||||||
|
$where[] = [$this->dataLimitField, 'in', $dataLimitAdminIds];
|
||||||
|
}
|
||||||
|
|
||||||
|
$ids = $this->request ? ($this->request->post('ids') ?? $this->request->get('ids') ?? []) : [];
|
||||||
|
$ids = is_array($ids) ? $ids : [];
|
||||||
|
$where[] = [$this->model->getPk(), 'in', $ids];
|
||||||
|
$data = $this->model->where($where)->select();
|
||||||
|
|
||||||
|
$channelIdsToSync = [];
|
||||||
|
foreach ($data as $v) {
|
||||||
|
$cid = $v['game_channel_id'] ?? $v['channel_id'] ?? null;
|
||||||
|
if ($cid !== null && $cid !== '') {
|
||||||
|
$channelIdsToSync[] = $cid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$count = 0;
|
||||||
|
$this->model->startTrans();
|
||||||
|
try {
|
||||||
|
foreach ($data as $v) {
|
||||||
|
$count += $v->delete();
|
||||||
|
}
|
||||||
|
$this->model->commit();
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
$this->model->rollback();
|
||||||
|
return $this->error($e->getMessage());
|
||||||
|
}
|
||||||
|
if ($count) {
|
||||||
|
GameChannelUserCount::syncChannels($channelIdsToSync);
|
||||||
|
return $this->success(__('Deleted successfully'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->error(__('No rows were deleted'));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查看
|
* 查看
|
||||||
* @throws Throwable
|
* @throws Throwable
|
||||||
@@ -232,6 +322,270 @@ class User extends Backend
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新建用户时:超管可选各渠道 default_tier_weight / default_bigwin_weight(大奖仅 default_bigwin_weight;default_kill_score_weight 为 T1~T5 击杀分档位,不作大奖回退)
|
||||||
|
*
|
||||||
|
* @throws Throwable
|
||||||
|
*/
|
||||||
|
public function defaultWeightPresets(WebmanRequest $request): Response
|
||||||
|
{
|
||||||
|
$response = $this->initializeBackend($request);
|
||||||
|
if ($response !== null) {
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->auth->check('game/user/index')) {
|
||||||
|
return $this->error(__('You have no permission'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$allowed = $this->getAllowedChannelIdsForGameConfig();
|
||||||
|
$channelQuery = Db::name('game_channel')->field(['id', 'name'])->order('id', 'asc');
|
||||||
|
if ($allowed !== null) {
|
||||||
|
$channelQuery->where('id', 'in', $allowed);
|
||||||
|
}
|
||||||
|
$channels = $channelQuery->select()->toArray();
|
||||||
|
|
||||||
|
$tierOut = [];
|
||||||
|
$bigwinOut = [];
|
||||||
|
/** 超管:首条为 game_config.channel_id=0 的全局默认权重 */
|
||||||
|
if ($this->auth->isSuperAdmin()) {
|
||||||
|
$gp = $this->fetchDefaultWeightsForChannelId(0);
|
||||||
|
$gname = __('Global default');
|
||||||
|
$tierOut[] = [
|
||||||
|
'channel_id' => 0,
|
||||||
|
'channel_name' => $gname,
|
||||||
|
'value' => $gp['tier_weight'],
|
||||||
|
];
|
||||||
|
$bigwinOut[] = [
|
||||||
|
'channel_id' => 0,
|
||||||
|
'channel_name' => $gname,
|
||||||
|
'value' => $gp['bigwin_weight'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($channels === []) {
|
||||||
|
return $this->success('', [
|
||||||
|
'tier' => $tierOut,
|
||||||
|
'bigwin' => $bigwinOut,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$channelIds = array_column($channels, 'id');
|
||||||
|
$nameList = [self::GC_NAME_TIER, self::GC_NAME_BIGWIN_PRIMARY];
|
||||||
|
$rows = Db::name('game_config')
|
||||||
|
->where('group', self::GC_GROUP_WEIGHT)
|
||||||
|
->where('name', 'in', $nameList)
|
||||||
|
->where('channel_id', 'in', $channelIds)
|
||||||
|
->field(['channel_id', 'name', 'value'])
|
||||||
|
->select()
|
||||||
|
->toArray();
|
||||||
|
|
||||||
|
$map = [];
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$cid = $row['channel_id'];
|
||||||
|
if (!isset($map[$cid])) {
|
||||||
|
$map[$cid] = [];
|
||||||
|
}
|
||||||
|
$map[$cid][$row['name']] = $row['value'];
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($channels as $ch) {
|
||||||
|
$cid = $ch['id'];
|
||||||
|
$cname = $ch['name'] ?? '';
|
||||||
|
$names = $map[$cid] ?? [];
|
||||||
|
$tierVal = $names[self::GC_NAME_TIER] ?? null;
|
||||||
|
$tierOut[] = [
|
||||||
|
'channel_id' => $cid,
|
||||||
|
'channel_name' => $cname,
|
||||||
|
'value' => ($tierVal !== null && $tierVal !== '') ? trim((string) $tierVal) : '[]',
|
||||||
|
];
|
||||||
|
$bigPrimary = $names[self::GC_NAME_BIGWIN_PRIMARY] ?? null;
|
||||||
|
$bigVal = ($bigPrimary !== null && $bigPrimary !== '') ? trim((string) $bigPrimary) : null;
|
||||||
|
$bigwinOut[] = [
|
||||||
|
'channel_id' => $cid,
|
||||||
|
'channel_name' => $cname,
|
||||||
|
'value' => $bigVal !== null ? $bigVal : '[]',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->success('', [
|
||||||
|
'tier' => $tierOut,
|
||||||
|
'bigwin' => $bigwinOut,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按渠道取默认档位/大奖权重(非超管仅可访问权限内渠道)
|
||||||
|
*
|
||||||
|
* @throws Throwable
|
||||||
|
*/
|
||||||
|
public function defaultWeightByChannel(WebmanRequest $request): Response
|
||||||
|
{
|
||||||
|
$response = $this->initializeBackend($request);
|
||||||
|
if ($response !== null) {
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->auth->check('game/user/index')) {
|
||||||
|
return $this->error(__('You have no permission'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$channelId = $request->get('channel_id', $request->post('channel_id'));
|
||||||
|
if ($channelId === null || $channelId === '') {
|
||||||
|
return $this->error(__('Parameter error'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$cid = (int) $channelId;
|
||||||
|
if ($cid === 0) {
|
||||||
|
if (!$this->auth->isSuperAdmin()) {
|
||||||
|
return $this->error(__('You have no permission'));
|
||||||
|
}
|
||||||
|
$pair = $this->fetchDefaultWeightsForChannelId(0);
|
||||||
|
|
||||||
|
return $this->success('', [
|
||||||
|
'tier_weight' => $pair['tier_weight'],
|
||||||
|
'bigwin_weight' => $pair['bigwin_weight'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($cid < 1 || !$this->canAccessChannelGameConfig($cid)) {
|
||||||
|
return $this->error(__('You have no permission'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$pair = $this->fetchDefaultWeightsForChannelId($cid);
|
||||||
|
|
||||||
|
return $this->success('', [
|
||||||
|
'tier_weight' => $pair['tier_weight'],
|
||||||
|
'bigwin_weight' => $pair['bigwin_weight'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<int|string>|null null 表示超管不限制渠道
|
||||||
|
*/
|
||||||
|
private function getAllowedChannelIdsForGameConfig(): ?array
|
||||||
|
{
|
||||||
|
if ($this->auth->isSuperAdmin()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$adminIds = parent::getDataLimitAdminIds();
|
||||||
|
if ($adminIds === []) {
|
||||||
|
return [-1];
|
||||||
|
}
|
||||||
|
$channelIds = Db::name('game_channel')->where('admin_id', 'in', $adminIds)->column('id');
|
||||||
|
if ($channelIds === []) {
|
||||||
|
return [-1];
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_values(array_unique($channelIds));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function canAccessChannelGameConfig(int $channelId): bool
|
||||||
|
{
|
||||||
|
$exists = Db::name('game_channel')->where('id', $channelId)->count();
|
||||||
|
if ($exists < 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$allowed = $this->getAllowedChannelIdsForGameConfig();
|
||||||
|
if ($allowed === null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return in_array($channelId, $allowed, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{tier_weight: string, bigwin_weight: string}
|
||||||
|
*/
|
||||||
|
private function fetchDefaultWeightsForChannelId(int $channelId): array
|
||||||
|
{
|
||||||
|
$rows = Db::name('game_config')
|
||||||
|
->where('channel_id', $channelId)
|
||||||
|
->where('group', self::GC_GROUP_WEIGHT)
|
||||||
|
->where('name', 'in', [self::GC_NAME_TIER, self::GC_NAME_BIGWIN_PRIMARY])
|
||||||
|
->column('value', 'name');
|
||||||
|
|
||||||
|
$tier = $rows[self::GC_NAME_TIER] ?? null;
|
||||||
|
$tierStr = ($tier !== null && $tier !== '') ? trim((string) $tier) : '[]';
|
||||||
|
|
||||||
|
$bigPrimary = $rows[self::GC_NAME_BIGWIN_PRIMARY] ?? null;
|
||||||
|
$bigStr = '[]';
|
||||||
|
if ($bigPrimary !== null && $bigPrimary !== '') {
|
||||||
|
$bigStr = trim((string) $bigPrimary);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 适配导入:强制返回固定键的 JSON(缺失键补空字符串)
|
||||||
|
$tierStr = $this->normalizeWeightJsonToFixedKeys($tierStr, self::TIER_WEIGHT_KEYS);
|
||||||
|
$bigStr = $this->normalizeWeightJsonToFixedKeys($bigStr, self::BIGWIN_WEIGHT_KEYS);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'tier_weight' => $tierStr,
|
||||||
|
'bigwin_weight' => $bigStr,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将 JSON 权重(数组形式:[{key:value}, ...])归一化到固定键顺序。
|
||||||
|
* 缺失键补空字符串;解析失败则返回固定键且值为空。
|
||||||
|
*/
|
||||||
|
private function normalizeWeightJsonToFixedKeys(string $raw, array $fixedKeys): string
|
||||||
|
{
|
||||||
|
$raw = trim($raw);
|
||||||
|
if ($raw === '' || $raw === '[]') {
|
||||||
|
$empty = [];
|
||||||
|
foreach ($fixedKeys as $k) {
|
||||||
|
$empty[] = [$k => ''];
|
||||||
|
}
|
||||||
|
return json_encode($empty, JSON_UNESCAPED_UNICODE);
|
||||||
|
}
|
||||||
|
|
||||||
|
$decoded = json_decode($raw, true);
|
||||||
|
if (!is_array($decoded)) {
|
||||||
|
$empty = [];
|
||||||
|
foreach ($fixedKeys as $k) {
|
||||||
|
$empty[] = [$k => ''];
|
||||||
|
}
|
||||||
|
return json_encode($empty, JSON_UNESCAPED_UNICODE);
|
||||||
|
}
|
||||||
|
|
||||||
|
$map = [];
|
||||||
|
foreach ($decoded as $item) {
|
||||||
|
if (!is_array($item)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
foreach ($item as $k => $v) {
|
||||||
|
if (!is_string($k) && !is_int($k)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$map[strval($k)] = $v === null ? '' : strval($v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$pairs = [];
|
||||||
|
foreach ($fixedKeys as $k) {
|
||||||
|
$pairs[] = [$k => $map[$k] ?? ''];
|
||||||
|
}
|
||||||
|
|
||||||
|
return json_encode($pairs, JSON_UNESCAPED_UNICODE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前渠道(game_channel_id)下是否已存在该用户名;编辑时排除当前记录主键
|
||||||
|
*/
|
||||||
|
private function gameUserUsernameExistsInChannel(string $username, int|string $channelId, string|int|null $excludePk = null): bool
|
||||||
|
{
|
||||||
|
$name = trim($username);
|
||||||
|
$cid = (int) $channelId;
|
||||||
|
$query = Db::name('game_user')
|
||||||
|
->where('game_channel_id', $cid)
|
||||||
|
->where('username', $name);
|
||||||
|
if ($excludePk !== null && $excludePk !== '') {
|
||||||
|
$query->where('id', '<>', $excludePk);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $query->count() > 0;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 若需重写查看、编辑、删除等方法,请复制 @see \app\admin\library\traits\Backend 中对应的方法至此进行重写
|
* 若需重写查看、编辑、删除等方法,请复制 @see \app\admin\library\traits\Backend 中对应的方法至此进行重写
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -6,6 +6,22 @@ export default {
|
|||||||
phone: 'phone',
|
phone: 'phone',
|
||||||
remark: 'remark',
|
remark: 'remark',
|
||||||
coin: 'coin',
|
coin: 'coin',
|
||||||
|
tier_weight: 'tier weight',
|
||||||
|
bigwin_weight: 'big win weight',
|
||||||
|
tier_weight_preset: 'tier weight preset (game config)',
|
||||||
|
bigwin_weight_preset: 'big win weight preset (game config)',
|
||||||
|
'weight value': 'weight value',
|
||||||
|
'weight value numeric': 'Weight values must be valid numbers',
|
||||||
|
'weight each max 100': 'Each weight value must not exceed 100',
|
||||||
|
tier_weight_help: 'Sum of T1~T5 must not exceed 100',
|
||||||
|
tier_weight_sum_max_100: 'Sum of tier weights (T1~T5) must not exceed 100',
|
||||||
|
bigwin_weight_help: 'Only requires each item ≤ 10000; points 5 and 30 are guaranteed big wins, fixed to 10000',
|
||||||
|
bigwin_weight_each_max_10000: 'Each big win weight must not exceed 10000',
|
||||||
|
ticket_count: 'tickets',
|
||||||
|
ticket_ante: 'bets',
|
||||||
|
ticket_count_times: 'times',
|
||||||
|
'ticket row incomplete': 'Each row must have both ante and count',
|
||||||
|
'ticket row numeric': 'ante and count must be valid numbers',
|
||||||
status: 'status',
|
status: 'status',
|
||||||
'status 0': 'status 0',
|
'status 0': 'status 0',
|
||||||
'status 1': 'status 1',
|
'status 1': 'status 1',
|
||||||
|
|||||||
@@ -6,6 +6,22 @@ export default {
|
|||||||
phone: '手机号',
|
phone: '手机号',
|
||||||
remark: '备注',
|
remark: '备注',
|
||||||
coin: '平台币',
|
coin: '平台币',
|
||||||
|
tier_weight: '档位权重',
|
||||||
|
bigwin_weight: '中大奖权重',
|
||||||
|
tier_weight_preset: '档位权重模板(游戏配置)',
|
||||||
|
bigwin_weight_preset: '大奖权重模板(游戏配置)',
|
||||||
|
'weight value': '权重数值',
|
||||||
|
'weight value numeric': '权重值必须为有效数字',
|
||||||
|
'weight each max 100': '每项权重 100',
|
||||||
|
tier_weight_help: 'T1~T5 权重之和不能超过 100',
|
||||||
|
tier_weight_sum_max_100: '档位权重(T1~T5)之和不能超过 100',
|
||||||
|
bigwin_weight_help: '仅限制每项权重不超过 10000;点数 5 和 30 为必中大奖组合,权重固定为 10000',
|
||||||
|
bigwin_weight_each_max_10000: '每项中大奖权重不能超过 10000',
|
||||||
|
ticket_count: '抽奖券',
|
||||||
|
ticket_ante: '注数',
|
||||||
|
ticket_count_times: '次数',
|
||||||
|
'ticket row incomplete': '每行需同时填写 ante 与 count',
|
||||||
|
'ticket row numeric': 'ante、count 须为有效数字',
|
||||||
status: '状态',
|
status: '状态',
|
||||||
'status 0': '禁用',
|
'status 0': '禁用',
|
||||||
'status 1': '启用',
|
'status 1': '启用',
|
||||||
|
|||||||
117
web/src/utils/gameWeightFixed.ts
Normal file
117
web/src/utils/gameWeightFixed.ts
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
/** 档位权重固定键(与 GameConfig / GameUser JSON 一致) */
|
||||||
|
export const TIER_WEIGHT_KEYS = ['T1', 'T2', 'T3', 'T4', 'T5'] as const
|
||||||
|
|
||||||
|
/** 中大奖权重固定键 */
|
||||||
|
export const BIGWIN_WEIGHT_KEYS = ['5', '10', '15', '20', '25', '30'] as const
|
||||||
|
|
||||||
|
/** 默认大奖权重:点数 5、30 为豹子号必中,权重固定 10000 */
|
||||||
|
export function isBigwinDiceLockedKey(key: string): boolean {
|
||||||
|
return key === '5' || key === '30'
|
||||||
|
}
|
||||||
|
|
||||||
|
export type WeightRow = { key: string; val: string }
|
||||||
|
|
||||||
|
export function weightArrayToMap(arr: unknown): Record<string, string> {
|
||||||
|
const map: Record<string, string> = {}
|
||||||
|
if (!Array.isArray(arr)) {
|
||||||
|
return map
|
||||||
|
}
|
||||||
|
for (const item of arr) {
|
||||||
|
if (item !== null && typeof item === 'object' && !Array.isArray(item)) {
|
||||||
|
for (const [k, v] of Object.entries(item)) {
|
||||||
|
map[k] = v === null || v === undefined ? '' : String(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 解析 game_weight JSON 为键值映射(键为 T1 / 5 等) */
|
||||||
|
export function parseWeightJsonToMap(raw: unknown): Record<string, string> {
|
||||||
|
if (raw === null || raw === undefined || raw === '') {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
if (typeof raw === 'string') {
|
||||||
|
const s = raw.trim()
|
||||||
|
if (!s || s === '[]') {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(s)
|
||||||
|
return weightArrayToMap(parsed)
|
||||||
|
} catch {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (Array.isArray(raw)) {
|
||||||
|
return weightArrayToMap(raw)
|
||||||
|
}
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fixedRowsFromKeys(keys: readonly string[], map: Record<string, string>): WeightRow[] {
|
||||||
|
const rows: WeightRow[] = []
|
||||||
|
for (const k of keys) {
|
||||||
|
rows.push({ key: k, val: map[k] ?? '' })
|
||||||
|
}
|
||||||
|
return rows
|
||||||
|
}
|
||||||
|
|
||||||
|
export function rowsToMap(rows: WeightRow[]): Record<string, string> {
|
||||||
|
const m: Record<string, string> = {}
|
||||||
|
for (const r of rows) {
|
||||||
|
m[r.key] = r.val
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
export function jsonStringFromFixedKeys(keys: readonly string[], map: Record<string, string>): string {
|
||||||
|
const pairs: Record<string, string>[] = []
|
||||||
|
for (const k of keys) {
|
||||||
|
const one: Record<string, string> = {}
|
||||||
|
one[k] = map[k] ?? ''
|
||||||
|
pairs.push(one)
|
||||||
|
}
|
||||||
|
return JSON.stringify(pairs)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统一配置标识:trim、去掉「 (说明)」「(说明)」「(说明)」等后缀、小写,避免下拉/接口返回不一致导致走错位校验(如误报每项≤10000)
|
||||||
|
*/
|
||||||
|
export function normalizeGameWeightConfigName(name: string | undefined): string {
|
||||||
|
if (name === undefined || name === null) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
let s = String(name).trim()
|
||||||
|
// 截到第一个半角/全角左括号之前(兼容 "key (说明)"、"key(说明)"、"key(说明)"、无前导空格)
|
||||||
|
const m = s.match(/^([^((]+)/u)
|
||||||
|
if (m) {
|
||||||
|
s = m[1].trim()
|
||||||
|
}
|
||||||
|
return s.toLowerCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 当前行是否为大奖骰子键 5~30(与 BIGWIN_WEIGHT_KEYS 一致) */
|
||||||
|
export function weightRowsMatchBigwinDiceKeys(rows: { key: string }[]): boolean {
|
||||||
|
const keys = rows
|
||||||
|
.map((r) => r.key.trim())
|
||||||
|
.filter((k) => k !== '')
|
||||||
|
.sort((a, b) => Number(a) - Number(b))
|
||||||
|
if (keys.length !== BIGWIN_WEIGHT_KEYS.length) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return BIGWIN_WEIGHT_KEYS.every((k, i) => keys[i] === k)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** GameConfig 中按 name 判断是否使用固定键编辑 */
|
||||||
|
export function getFixedKeysForGameConfigName(name: string | undefined): readonly string[] | null {
|
||||||
|
const n = normalizeGameWeightConfigName(name)
|
||||||
|
/** 击杀分权重与档位权重同为 T1~T5;库中 JSON 为 [{"T1":"0"},...],非骰子点 5~30 */
|
||||||
|
if (n === 'default_tier_weight' || n === 'default_kill_score_weight') {
|
||||||
|
return TIER_WEIGHT_KEYS
|
||||||
|
}
|
||||||
|
if (n === 'default_bigwin_weight') {
|
||||||
|
return BIGWIN_WEIGHT_KEYS
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
83
web/src/views/backend/game/user/GameUserTicketJsonCell.vue
Normal file
83
web/src/views/backend/game/user/GameUserTicketJsonCell.vue
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="tagLabels.length" class="game-user-ticket-tags">
|
||||||
|
<el-tag v-for="(label, idx) in tagLabels" :key="idx" class="m-4" effect="light" type="success" size="default">
|
||||||
|
{{ label }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
<span v-else class="game-user-json-plain">{{ plainText }}</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
renderRow: TableRow
|
||||||
|
renderField: TableColumn
|
||||||
|
renderValue: unknown
|
||||||
|
renderColumn: import('element-plus').TableColumnCtx<TableRow>
|
||||||
|
renderIndex: number
|
||||||
|
}>()
|
||||||
|
|
||||||
|
/** [{"ante":1,"count":1},...] */
|
||||||
|
function parseTicketTagLabels(raw: unknown): string[] {
|
||||||
|
if (raw === null || raw === undefined || raw === '') {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
let arr: unknown[] = []
|
||||||
|
if (typeof raw === 'string') {
|
||||||
|
const s = raw.trim()
|
||||||
|
if (!s) return []
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(s)
|
||||||
|
arr = Array.isArray(parsed) ? parsed : []
|
||||||
|
} catch {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
} else if (Array.isArray(raw)) {
|
||||||
|
arr = raw
|
||||||
|
} else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const labels: string[] = []
|
||||||
|
for (const item of arr) {
|
||||||
|
if (item !== null && typeof item === 'object' && !Array.isArray(item)) {
|
||||||
|
const o = item as Record<string, unknown>
|
||||||
|
const ante = o.ante
|
||||||
|
const count = o.count
|
||||||
|
labels.push(`ante:${String(ante)} count:${String(count)}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return labels
|
||||||
|
}
|
||||||
|
|
||||||
|
const tagLabels = computed(() => parseTicketTagLabels(props.renderValue))
|
||||||
|
|
||||||
|
const plainText = computed(() => {
|
||||||
|
const v = props.renderValue
|
||||||
|
if (v === null || v === undefined) return ''
|
||||||
|
if (typeof v === 'object') {
|
||||||
|
try {
|
||||||
|
return JSON.stringify(v)
|
||||||
|
} catch {
|
||||||
|
return String(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return String(v)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.game-user-ticket-tags {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 4px 0;
|
||||||
|
}
|
||||||
|
.m-4 {
|
||||||
|
margin: 4px;
|
||||||
|
}
|
||||||
|
.game-user-json-plain {
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
82
web/src/views/backend/game/user/GameUserWeightJsonCell.vue
Normal file
82
web/src/views/backend/game/user/GameUserWeightJsonCell.vue
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="tagLabels.length" class="game-user-json-weight-tags">
|
||||||
|
<el-tag v-for="(label, idx) in tagLabels" :key="idx" class="m-4" effect="light" type="primary" size="default">
|
||||||
|
{{ label }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
<span v-else class="game-user-json-plain">{{ plainText }}</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
renderRow: TableRow
|
||||||
|
renderField: TableColumn
|
||||||
|
renderValue: unknown
|
||||||
|
renderColumn: import('element-plus').TableColumnCtx<TableRow>
|
||||||
|
renderIndex: number
|
||||||
|
}>()
|
||||||
|
|
||||||
|
/** [{"T1":"5"},{"T2":"20"},...] */
|
||||||
|
function parseWeightTagLabels(raw: unknown): string[] {
|
||||||
|
if (raw === null || raw === undefined || raw === '') {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
let arr: unknown[] = []
|
||||||
|
if (typeof raw === 'string') {
|
||||||
|
const s = raw.trim()
|
||||||
|
if (!s) return []
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(s)
|
||||||
|
arr = Array.isArray(parsed) ? parsed : []
|
||||||
|
} catch {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
} else if (Array.isArray(raw)) {
|
||||||
|
arr = raw
|
||||||
|
} else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const labels: string[] = []
|
||||||
|
for (const item of arr) {
|
||||||
|
if (item !== null && typeof item === 'object' && !Array.isArray(item)) {
|
||||||
|
for (const [k, v] of Object.entries(item as Record<string, unknown>)) {
|
||||||
|
labels.push(`${k}:${String(v)}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return labels
|
||||||
|
}
|
||||||
|
|
||||||
|
const tagLabels = computed(() => parseWeightTagLabels(props.renderValue))
|
||||||
|
|
||||||
|
const plainText = computed(() => {
|
||||||
|
const v = props.renderValue
|
||||||
|
if (v === null || v === undefined) return ''
|
||||||
|
if (typeof v === 'object') {
|
||||||
|
try {
|
||||||
|
return JSON.stringify(v)
|
||||||
|
} catch {
|
||||||
|
return String(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return String(v)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.game-user-json-weight-tags {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 4px 0;
|
||||||
|
}
|
||||||
|
.m-4 {
|
||||||
|
margin: 4px;
|
||||||
|
}
|
||||||
|
.game-user-json-plain {
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -23,11 +23,14 @@
|
|||||||
import { onMounted, provide, useTemplateRef } from 'vue'
|
import { onMounted, provide, useTemplateRef } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import PopupForm from './popupForm.vue'
|
import PopupForm from './popupForm.vue'
|
||||||
|
import GameUserTicketJsonCell from './GameUserTicketJsonCell.vue'
|
||||||
|
import GameUserWeightJsonCell from './GameUserWeightJsonCell.vue'
|
||||||
import { baTableApi } from '/@/api/common'
|
import { baTableApi } from '/@/api/common'
|
||||||
import { defaultOptButtons } from '/@/components/table'
|
import { defaultOptButtons } from '/@/components/table'
|
||||||
import TableHeader from '/@/components/table/header/index.vue'
|
import TableHeader from '/@/components/table/header/index.vue'
|
||||||
import Table from '/@/components/table/index.vue'
|
import Table from '/@/components/table/index.vue'
|
||||||
import baTableClass from '/@/utils/baTable'
|
import baTableClass from '/@/utils/baTable'
|
||||||
|
import { BIGWIN_WEIGHT_KEYS, TIER_WEIGHT_KEYS, jsonStringFromFixedKeys } from '/@/utils/gameWeightFixed'
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'game/user',
|
name: 'game/user',
|
||||||
@@ -66,6 +69,36 @@ const baTable = new baTableClass(
|
|||||||
},
|
},
|
||||||
{ label: t('game.user.phone'), prop: 'phone', 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('game.user.coin'), prop: 'coin', align: 'center', sortable: false, operator: 'RANGE' },
|
{ label: t('game.user.coin'), prop: 'coin', align: 'center', sortable: false, operator: 'RANGE' },
|
||||||
|
{
|
||||||
|
label: t('game.user.tier_weight'),
|
||||||
|
prop: 'tier_weight',
|
||||||
|
align: 'center',
|
||||||
|
minWidth: 200,
|
||||||
|
sortable: false,
|
||||||
|
operator: false,
|
||||||
|
render: 'customRender',
|
||||||
|
customRender: GameUserWeightJsonCell,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('game.user.bigwin_weight'),
|
||||||
|
prop: 'bigwin_weight',
|
||||||
|
align: 'center',
|
||||||
|
minWidth: 200,
|
||||||
|
sortable: false,
|
||||||
|
operator: false,
|
||||||
|
render: 'customRender',
|
||||||
|
customRender: GameUserWeightJsonCell,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('game.user.ticket_count'),
|
||||||
|
prop: 'ticket_count',
|
||||||
|
align: 'center',
|
||||||
|
minWidth: 220,
|
||||||
|
sortable: false,
|
||||||
|
operator: false,
|
||||||
|
render: 'customRender',
|
||||||
|
customRender: GameUserTicketJsonCell,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: t('game.user.status'),
|
label: t('game.user.status'),
|
||||||
prop: 'status',
|
prop: 'status',
|
||||||
@@ -138,7 +171,12 @@ const baTable = new baTableClass(
|
|||||||
dblClickNotEditColumn: [undefined, 'status'],
|
dblClickNotEditColumn: [undefined, 'status'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
defaultItems: { status: '1' },
|
defaultItems: {
|
||||||
|
status: '1',
|
||||||
|
tier_weight: jsonStringFromFixedKeys(TIER_WEIGHT_KEYS, {}),
|
||||||
|
bigwin_weight: jsonStringFromFixedKeys(BIGWIN_WEIGHT_KEYS, {}),
|
||||||
|
ticket_count: '[]',
|
||||||
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -68,6 +68,65 @@
|
|||||||
:input-attr="{ step: 1 }"
|
:input-attr="{ step: 1 }"
|
||||||
:placeholder="t('Please input field', { field: t('game.user.coin') })"
|
:placeholder="t('Please input field', { field: t('game.user.coin') })"
|
||||||
/>
|
/>
|
||||||
|
<!-- 档位权重:键固定 T1–T5,仅可改值 -->
|
||||||
|
<el-form-item :label="t('game.user.tier_weight')" prop="tier_weight">
|
||||||
|
<div class="weight-value-editor">
|
||||||
|
<div v-for="(row, idx) in tierWeightRows" :key="'tw-' + idx" class="weight-value-row">
|
||||||
|
<el-input v-model="row.key" class="weight-key" readonly tabindex="-1" />
|
||||||
|
<span class="weight-sep">:</span>
|
||||||
|
<el-input
|
||||||
|
v-model="row.val"
|
||||||
|
class="weight-val"
|
||||||
|
:placeholder="t('Please input field', { field: t('game.user.weight value') })"
|
||||||
|
clearable
|
||||||
|
@input="onTierWeightRowChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="form-help">{{ t('game.user.tier_weight_help') }}</div>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
<!-- 中大奖权重:键固定 5–30,仅可改值 -->
|
||||||
|
<el-form-item :label="t('game.user.bigwin_weight')" prop="bigwin_weight">
|
||||||
|
<div class="weight-value-editor">
|
||||||
|
<div v-for="(row, idx) in bigwinWeightRows" :key="'bw-' + idx" class="weight-value-row">
|
||||||
|
<el-input v-model="row.key" class="weight-key" readonly tabindex="-1" />
|
||||||
|
<span class="weight-sep">:</span>
|
||||||
|
<el-input
|
||||||
|
v-model="row.val"
|
||||||
|
class="weight-val"
|
||||||
|
:placeholder="t('Please input field', { field: t('game.user.weight value') })"
|
||||||
|
clearable
|
||||||
|
:disabled="isBigwinValueLocked(row.key)"
|
||||||
|
@input="onBigwinWeightRowChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="form-help">{{ t('game.user.bigwin_weight_help') }}</div>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
<!-- 抽奖券:最多一条 JSON 元素 [{"ante":1,"count":1}],不可增行,可删除清空 -->
|
||||||
|
<el-form-item :label="t('game.user.ticket_count')" prop="ticket_count">
|
||||||
|
<div class="ticket-value-editor">
|
||||||
|
<div v-for="(row, idx) in ticketRows" :key="'tk-' + idx" class="ticket-value-row">
|
||||||
|
<span class="ticket-label">{{ t('game.user.ticket_ante') }}</span>
|
||||||
|
<el-input
|
||||||
|
v-model="row.ante"
|
||||||
|
class="ticket-field"
|
||||||
|
clearable
|
||||||
|
:placeholder="t('Please input field', { field: t('game.user.ticket_ante') })"
|
||||||
|
@input="onTicketRowChange"
|
||||||
|
/>
|
||||||
|
<span class="ticket-label">{{ t('game.user.ticket_count_times') }}</span>
|
||||||
|
<el-input
|
||||||
|
v-model="row.count"
|
||||||
|
class="ticket-field"
|
||||||
|
clearable
|
||||||
|
:placeholder="t('Please input field', { field: t('game.user.ticket_count_times') })"
|
||||||
|
@input="onTicketRowChange"
|
||||||
|
/>
|
||||||
|
<el-button type="danger" link @click="removeTicketRow">{{ t('Delete') }}</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
<FormItem
|
<FormItem
|
||||||
:label="t('game.user.status')"
|
:label="t('game.user.status')"
|
||||||
type="switch"
|
type="switch"
|
||||||
@@ -107,17 +166,30 @@ import type { FormItemRule } from 'element-plus'
|
|||||||
import { computed, inject, onMounted, reactive, ref, useTemplateRef, watch } from 'vue'
|
import { computed, inject, onMounted, reactive, ref, useTemplateRef, watch } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import FormItem from '/@/components/formItem/index.vue'
|
import FormItem from '/@/components/formItem/index.vue'
|
||||||
|
import { useAdminInfo } from '/@/stores/adminInfo'
|
||||||
import { useConfig } from '/@/stores/config'
|
import { useConfig } from '/@/stores/config'
|
||||||
import type baTableClass from '/@/utils/baTable'
|
import type baTableClass from '/@/utils/baTable'
|
||||||
import { buildValidatorData, regularPassword } from '/@/utils/validate'
|
import { buildValidatorData, regularPassword } from '/@/utils/validate'
|
||||||
import createAxios from '/@/utils/axios'
|
import createAxios from '/@/utils/axios'
|
||||||
|
import {
|
||||||
|
BIGWIN_WEIGHT_KEYS,
|
||||||
|
TIER_WEIGHT_KEYS,
|
||||||
|
fixedRowsFromKeys,
|
||||||
|
jsonStringFromFixedKeys,
|
||||||
|
parseWeightJsonToMap,
|
||||||
|
rowsToMap,
|
||||||
|
type WeightRow,
|
||||||
|
} from '/@/utils/gameWeightFixed'
|
||||||
|
|
||||||
const config = useConfig()
|
const config = useConfig()
|
||||||
const formRef = useTemplateRef('formRef')
|
const formRef = useTemplateRef('formRef')
|
||||||
const baTable = inject('baTable') as baTableClass
|
const baTable = inject('baTable') as baTableClass
|
||||||
|
const adminInfo = useAdminInfo()
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
const isSuperAdmin = computed(() => adminInfo.super === true)
|
||||||
|
|
||||||
type TreeNode = {
|
type TreeNode = {
|
||||||
value: string
|
value: string
|
||||||
label: string
|
label: string
|
||||||
@@ -194,6 +266,209 @@ onMounted(() => {
|
|||||||
loadChannelAdminTree()
|
loadChannelAdminTree()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
type TicketRow = { ante: string; count: string }
|
||||||
|
|
||||||
|
const tierWeightRows = ref<WeightRow[]>(fixedRowsFromKeys(TIER_WEIGHT_KEYS, {}))
|
||||||
|
const bigwinWeightRows = ref<WeightRow[]>(fixedRowsFromKeys(BIGWIN_WEIGHT_KEYS, {}))
|
||||||
|
const ticketRows = ref<TicketRow[]>([{ ante: '', count: '1' }])
|
||||||
|
|
||||||
|
function isBigwinValueLocked(key: string): boolean {
|
||||||
|
return key === '5' || key === '30'
|
||||||
|
}
|
||||||
|
|
||||||
|
function enforceBigwinFixedValues() {
|
||||||
|
for (const r of bigwinWeightRows.value) {
|
||||||
|
if (isBigwinValueLocked(r.key)) {
|
||||||
|
r.val = '10000'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncTierWeightToForm() {
|
||||||
|
const items = baTable.form.items
|
||||||
|
if (!items) return
|
||||||
|
items.tier_weight = jsonStringFromFixedKeys(TIER_WEIGHT_KEYS, rowsToMap(tierWeightRows.value))
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncBigwinWeightToForm() {
|
||||||
|
const items = baTable.form.items
|
||||||
|
if (!items) return
|
||||||
|
items.bigwin_weight = jsonStringFromFixedKeys(BIGWIN_WEIGHT_KEYS, rowsToMap(bigwinWeightRows.value))
|
||||||
|
}
|
||||||
|
|
||||||
|
function onTierWeightRowChange() {
|
||||||
|
syncTierWeightToForm()
|
||||||
|
}
|
||||||
|
|
||||||
|
function onBigwinWeightRowChange() {
|
||||||
|
enforceBigwinFixedValues()
|
||||||
|
syncBigwinWeightToForm()
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseTicketRows(raw: unknown): TicketRow[] {
|
||||||
|
if (raw === null || raw === undefined || raw === '') {
|
||||||
|
return [{ ante: '', count: '1' }]
|
||||||
|
}
|
||||||
|
if (typeof raw === 'string') {
|
||||||
|
const s = raw.trim()
|
||||||
|
if (!s) return [{ ante: '', count: '1' }]
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(s)
|
||||||
|
return arrayToTicketRows(parsed)
|
||||||
|
} catch {
|
||||||
|
return [{ ante: '', count: '1' }]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (Array.isArray(raw)) {
|
||||||
|
return arrayToTicketRows(raw)
|
||||||
|
}
|
||||||
|
return [{ ante: '', count: '1' }]
|
||||||
|
}
|
||||||
|
|
||||||
|
function arrayToTicketRows(arr: unknown): TicketRow[] {
|
||||||
|
if (!Array.isArray(arr)) {
|
||||||
|
return [{ ante: '', count: '1' }]
|
||||||
|
}
|
||||||
|
const out: TicketRow[] = []
|
||||||
|
for (const item of arr) {
|
||||||
|
if (item !== null && typeof item === 'object' && !Array.isArray(item)) {
|
||||||
|
const ante = Reflect.get(item, 'ante')
|
||||||
|
const count = Reflect.get(item, 'count')
|
||||||
|
out.push({
|
||||||
|
ante: ante === null || ante === undefined ? '' : String(ante),
|
||||||
|
count: count === null || count === undefined || String(count) === '' ? '1' : String(count),
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out.length ? out : [{ ante: '', count: '1' }]
|
||||||
|
}
|
||||||
|
|
||||||
|
function ticketRowsToJsonString(rows: TicketRow[]): string {
|
||||||
|
const body: { ante: number; count: number }[] = []
|
||||||
|
for (const r of rows) {
|
||||||
|
const a = r.ante.trim()
|
||||||
|
const c = r.count.trim()
|
||||||
|
if (a === '' && c === '') continue
|
||||||
|
if (a === '' || c === '') continue
|
||||||
|
const na = Number(a)
|
||||||
|
const nc = Number(c)
|
||||||
|
body.push({ ante: na, count: nc })
|
||||||
|
}
|
||||||
|
return JSON.stringify(body)
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncTicketToForm() {
|
||||||
|
const items = baTable.form.items
|
||||||
|
if (!items) return
|
||||||
|
const a = ticketRows.value[0]?.ante?.trim() ?? ''
|
||||||
|
const c = ticketRows.value[0]?.count?.trim() ?? ''
|
||||||
|
if (a === '' || c === '') {
|
||||||
|
items.ticket_count = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
items.ticket_count = ticketRowsToJsonString(ticketRows.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyTierBigwinFromJson(tierJson: string, bigwinJson: string) {
|
||||||
|
const items = baTable.form.items
|
||||||
|
if (!items) return
|
||||||
|
const tm = parseWeightJsonToMap(tierJson)
|
||||||
|
const bm = parseWeightJsonToMap(bigwinJson)
|
||||||
|
items.tier_weight = jsonStringFromFixedKeys(TIER_WEIGHT_KEYS, tm)
|
||||||
|
items.bigwin_weight = jsonStringFromFixedKeys(BIGWIN_WEIGHT_KEYS, bm)
|
||||||
|
tierWeightRows.value = fixedRowsFromKeys(TIER_WEIGHT_KEYS, tm)
|
||||||
|
bigwinWeightRows.value = fixedRowsFromKeys(BIGWIN_WEIGHT_KEYS, bm)
|
||||||
|
enforceBigwinFixedValues()
|
||||||
|
syncBigwinWeightToForm()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadAndApplyDefaultsForChannel(channelId: number) {
|
||||||
|
try {
|
||||||
|
const res = await createAxios(
|
||||||
|
{
|
||||||
|
url: '/admin/game.User/defaultWeightByChannel',
|
||||||
|
method: 'get',
|
||||||
|
params: { channel_id: channelId },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
showErrorMessage: false,
|
||||||
|
showCodeMessage: false,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
applyTierBigwinFromJson(res.data.tier_weight ?? '[]', res.data.bigwin_weight ?? '[]')
|
||||||
|
} catch {
|
||||||
|
// 路由或权限异常时不阻断打开表单,保持可手工编辑
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onTicketRowChange() {
|
||||||
|
syncTicketToForm()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 最多一条,删除/不填则 ticket_count 为空 */
|
||||||
|
function removeTicketRow() {
|
||||||
|
ticketRows.value = [{ ante: '', count: '1' }]
|
||||||
|
syncTicketToForm()
|
||||||
|
}
|
||||||
|
|
||||||
|
function hydrateJsonFieldsFromForm() {
|
||||||
|
const tm = parseWeightJsonToMap(baTable.form.items?.tier_weight)
|
||||||
|
const bm = parseWeightJsonToMap(baTable.form.items?.bigwin_weight)
|
||||||
|
tierWeightRows.value = fixedRowsFromKeys(TIER_WEIGHT_KEYS, tm)
|
||||||
|
bigwinWeightRows.value = fixedRowsFromKeys(BIGWIN_WEIGHT_KEYS, bm)
|
||||||
|
syncTierWeightToForm()
|
||||||
|
enforceBigwinFixedValues()
|
||||||
|
syncBigwinWeightToForm()
|
||||||
|
ticketRows.value = normalizeTicketRowsToOne(parseTicketRows(baTable.form.items?.ticket_count))
|
||||||
|
syncTicketToForm()
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeTicketRowsToOne(rows: TicketRow[]): TicketRow[] {
|
||||||
|
if (rows.length === 0) {
|
||||||
|
return [{ ante: '', count: '1' }]
|
||||||
|
}
|
||||||
|
const first = rows[0]
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
ante: first?.ante ?? '',
|
||||||
|
count: first?.count && String(first.count).trim() !== '' ? String(first.count) : '1',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => baTable.form.loading,
|
||||||
|
(loading) => {
|
||||||
|
if (loading === false) {
|
||||||
|
hydrateJsonFieldsFromForm()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [baTable.form.operate, baTable.form.loading] as const,
|
||||||
|
async ([op, loading]) => {
|
||||||
|
if (op !== 'Add' || loading !== false) return
|
||||||
|
if (!isSuperAdmin.value) return
|
||||||
|
await loadAndApplyDefaultsForChannel(0)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => baTable.form.items?.game_channel_id,
|
||||||
|
async (ch) => {
|
||||||
|
if (baTable.form.operate !== 'Add') return
|
||||||
|
if (ch === undefined || ch === null || ch === '') return
|
||||||
|
const cid = Number(ch)
|
||||||
|
if (!Number.isFinite(cid)) return
|
||||||
|
if (isSuperAdmin.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await loadAndApplyDefaultsForChannel(cid)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => baTable.form.items?.admin_id,
|
() => baTable.form.items?.admin_id,
|
||||||
(val) => {
|
(val) => {
|
||||||
@@ -202,6 +477,62 @@ watch(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
function validateTierWeightRows(): string | undefined {
|
||||||
|
let sum = 0
|
||||||
|
for (const r of tierWeightRows.value) {
|
||||||
|
const vs = r.val.trim()
|
||||||
|
if (vs === '') {
|
||||||
|
return t('Please input field', { field: t('game.user.weight value') })
|
||||||
|
}
|
||||||
|
const n = Number(vs)
|
||||||
|
if (!Number.isFinite(n)) {
|
||||||
|
return t('game.user.weight value numeric')
|
||||||
|
}
|
||||||
|
if (n > 100) {
|
||||||
|
return t('game.user.weight each max 100')
|
||||||
|
}
|
||||||
|
sum += n
|
||||||
|
}
|
||||||
|
if (sum > 100 + 0.000001) {
|
||||||
|
return t('game.user.tier_weight_sum_max_100')
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateBigwinWeightRows(): string | undefined {
|
||||||
|
for (const r of bigwinWeightRows.value) {
|
||||||
|
const vs = r.val.trim()
|
||||||
|
if (vs === '') {
|
||||||
|
return t('Please input field', { field: t('game.user.weight value') })
|
||||||
|
}
|
||||||
|
const n = Number(vs)
|
||||||
|
if (!Number.isFinite(n)) {
|
||||||
|
return t('game.user.weight value numeric')
|
||||||
|
}
|
||||||
|
if (n > 10000) {
|
||||||
|
return t('game.user.bigwin_weight_each_max_10000')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateTicketRowsField(): string | undefined {
|
||||||
|
for (const r of ticketRows.value) {
|
||||||
|
const a = r.ante.trim()
|
||||||
|
const c = r.count.trim()
|
||||||
|
if (a === '' && c === '') continue
|
||||||
|
if (a === '' || c === '') {
|
||||||
|
return t('game.user.ticket row incomplete')
|
||||||
|
}
|
||||||
|
const na = Number(a)
|
||||||
|
const nc = Number(c)
|
||||||
|
if (!Number.isFinite(na) || !Number.isFinite(nc)) {
|
||||||
|
return t('game.user.ticket row numeric')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
const validatorGameUserPassword = (rule: any, val: string, callback: (error?: Error) => void) => {
|
const validatorGameUserPassword = (rule: any, val: string, callback: (error?: Error) => void) => {
|
||||||
const operate = baTable.form.operate
|
const operate = baTable.form.operate
|
||||||
const v = typeof val === 'string' ? val.trim() : ''
|
const v = typeof val === 'string' ? val.trim() : ''
|
||||||
@@ -224,10 +555,96 @@ const rules: Partial<Record<string, FormItemRule[]>> = reactive({
|
|||||||
password: [{ validator: validatorGameUserPassword, trigger: 'blur' }],
|
password: [{ validator: validatorGameUserPassword, trigger: 'blur' }],
|
||||||
phone: [buildValidatorData({ name: 'required', title: t('game.user.phone') })],
|
phone: [buildValidatorData({ name: 'required', title: t('game.user.phone') })],
|
||||||
coin: [buildValidatorData({ name: 'number', title: t('game.user.coin') })],
|
coin: [buildValidatorData({ name: 'number', title: t('game.user.coin') })],
|
||||||
|
tier_weight: [
|
||||||
|
{
|
||||||
|
validator: (_rule, _val, callback) => {
|
||||||
|
const err = validateTierWeightRows()
|
||||||
|
if (err) {
|
||||||
|
callback(new Error(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
callback()
|
||||||
|
},
|
||||||
|
trigger: ['blur', 'change'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
bigwin_weight: [
|
||||||
|
{
|
||||||
|
validator: (_rule, _val, callback) => {
|
||||||
|
const err = validateBigwinWeightRows()
|
||||||
|
if (err) {
|
||||||
|
callback(new Error(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
callback()
|
||||||
|
},
|
||||||
|
trigger: ['blur', 'change'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
ticket_count: [
|
||||||
|
{
|
||||||
|
validator: (_rule, _val, callback) => {
|
||||||
|
const err = validateTicketRowsField()
|
||||||
|
if (err) {
|
||||||
|
callback(new Error(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
callback()
|
||||||
|
},
|
||||||
|
trigger: ['blur', 'change'],
|
||||||
|
},
|
||||||
|
],
|
||||||
admin_id: [buildValidatorData({ name: 'required', title: t('game.user.admin_id') })],
|
admin_id: [buildValidatorData({ name: 'required', title: t('game.user.admin_id') })],
|
||||||
create_time: [buildValidatorData({ name: 'date', title: t('game.user.create_time') })],
|
create_time: [buildValidatorData({ name: 'date', title: t('game.user.create_time') })],
|
||||||
update_time: [buildValidatorData({ name: 'date', title: t('game.user.update_time') })],
|
update_time: [buildValidatorData({ name: 'date', title: t('game.user.update_time') })],
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss"></style>
|
<style scoped lang="scss">
|
||||||
|
.weight-value-editor {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.weight-value-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.weight-key {
|
||||||
|
max-width: 140px;
|
||||||
|
}
|
||||||
|
.weight-val {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 80px;
|
||||||
|
}
|
||||||
|
.weight-sep {
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
}
|
||||||
|
.ticket-value-editor {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.ticket-value-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.ticket-field {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 100px;
|
||||||
|
max-width: 160px;
|
||||||
|
}
|
||||||
|
.ticket-label {
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.form-help {
|
||||||
|
margin-top: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 18px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user