Compare commits
8 Commits
master-gam
...
master-tes
| Author | SHA1 | Date | |
|---|---|---|---|
| b9f78cacf1 | |||
| 74d2103290 | |||
| a478c902be | |||
| b0d25b30f9 | |||
| ac30d8d1c9 | |||
| c4c17180ee | |||
| 85da91e3f3 | |||
| a0f14015ed |
15
.env-example
15
.env-example
@@ -7,12 +7,17 @@ APP_DEFAULT_TIMEZONE = Asia/Shanghai
|
||||
|
||||
# 语言
|
||||
LANG_DEFAULT_LANG = zh-cn
|
||||
# Database
|
||||
|
||||
# 数据库(config/thinkorm.php/database.php)
|
||||
DATABASE_DRIVER = mysql
|
||||
DATABASE_TYPE = mysql
|
||||
DATABASE_HOSTNAME = 127.0.0.1
|
||||
DATABASE_DATABASE = webman-buildadmin-dafuweng
|
||||
DATABASE_USERNAME = webman-buildadmin-dafuweng
|
||||
DATABASE_PASSWORD = 6dzMaX32Xdsc4DjS
|
||||
DATABASE_DATABASE = buildadmin-webman
|
||||
DATABASE_USERNAME = buildadmin-webman
|
||||
DATABASE_PASSWORD = 123456
|
||||
DATABASE_HOSTPORT = 3306
|
||||
DATABASE_CHARSET = utf8mb4
|
||||
DATABASE_PREFIX =
|
||||
DATABASE_PREFIX =
|
||||
|
||||
# 缓存(config/cache.php)
|
||||
CACHE_DRIVER = file
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,8 +1,8 @@
|
||||
# 通过 Git 部署项目至线上时建议删除的忽略规则
|
||||
/vendor
|
||||
/modules
|
||||
/public/*.lock
|
||||
/public/index.html
|
||||
#/public/*.lock
|
||||
#/public/index.html
|
||||
/public/assets
|
||||
|
||||
# 通过 Git 部署项目至线上时可以考虑删除的忽略规则
|
||||
|
||||
@@ -180,9 +180,9 @@ php webman migrate
|
||||
```
|
||||
|
||||
3. **访问地址:**
|
||||
- 安装向导:http://localhost:1818/install/
|
||||
- 前台地址:http://localhost:1818/index.html/#/
|
||||
- 后台地址:http://localhost:1818/index.html/#/admin
|
||||
- 安装向导:http://localhost:1818/install/
|
||||
- 前台地址:http://localhost:1818/index.html/#/
|
||||
- 后台地址:http://localhost:1818/index.html/#/admin
|
||||
|
||||
> 注意:前端通过 Vite 代理将 `/api`、`/admin`、`/install` 转发到后端 8787 端口,请勿直接访问 8787 端口的前端页面,否则可能出现 404。
|
||||
|
||||
@@ -215,7 +215,7 @@ location ^~ / {
|
||||
## 六、路由说明
|
||||
|
||||
- **后台 API**:`/admin/{module}.{Controller}/{action}`
|
||||
- 示例:`/admin/mall.Player/index` → `app\admin\controller\mall\Player::index`
|
||||
- 示例:`/admin/mall.Player/index` → `app\admin\controller\mall\Player::index`
|
||||
- **前台 API**:`/api/...`
|
||||
- **安装**:`/api/Install/...`
|
||||
|
||||
|
||||
@@ -21,10 +21,7 @@ class Admin extends Backend
|
||||
|
||||
protected array|string $quickSearchField = ['username', 'nickname'];
|
||||
|
||||
/**
|
||||
* 开启数据范围;具体范围见重写的 getDataLimitAdminIds(角色组树:仅本人 + 下级组内管理员)
|
||||
*/
|
||||
protected bool|string|int $dataLimit = true;
|
||||
protected string|int|bool $dataLimit = 'allAuthAndOthers';
|
||||
|
||||
protected string $dataLimitField = 'id';
|
||||
|
||||
@@ -34,17 +31,6 @@ class Admin extends Backend
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 非超管:仅可管理「本人 + 树形下级组内」的管理员账号;与角色组管理页的可见范围一致(列表不含仅同级的其他管理员)
|
||||
*/
|
||||
protected function getDataLimitAdminIds(): array
|
||||
{
|
||||
if (!$this->dataLimit || !$this->auth || $this->auth->isSuperAdmin()) {
|
||||
return [];
|
||||
}
|
||||
return $this->auth->getSelfAndSubordinateAdminIds();
|
||||
}
|
||||
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
@@ -56,23 +42,11 @@ class Admin extends Backend
|
||||
}
|
||||
|
||||
list($where, $alias, $limit, $order) = $this->queryBuilder();
|
||||
$query = $this->model
|
||||
$res = $this->model
|
||||
->withoutField('login_failure,password,salt')
|
||||
->withJoin($this->withJoinTable, $this->withJoinType)
|
||||
->alias($alias)
|
||||
->where($where);
|
||||
|
||||
// 仅返回“顶级角色组(pid=0)”下的管理员(用于远程下拉等场景)
|
||||
$topGroup = $request->get('top_group') ?? $request->post('top_group');
|
||||
if ($topGroup === '1' || $topGroup === 1 || $topGroup === true) {
|
||||
$query = $query
|
||||
->join('admin_group_access aga', $alias['admin'] . '.id = aga.uid')
|
||||
->join('admin_group ag', 'aga.group_id = ag.id')
|
||||
->where('ag.pid', 0)
|
||||
->distinct(true);
|
||||
}
|
||||
|
||||
$res = $query
|
||||
->where($where)
|
||||
->order($order)
|
||||
->paginate($limit);
|
||||
|
||||
@@ -83,76 +57,6 @@ class Admin extends Backend
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 远程下拉(重写:支持 top_group=1 仅返回顶级组管理员)
|
||||
*/
|
||||
protected function _select(): Response
|
||||
{
|
||||
if (empty($this->model)) {
|
||||
return $this->success('', [
|
||||
'list' => [],
|
||||
'total' => 0,
|
||||
]);
|
||||
}
|
||||
|
||||
$pk = $this->model->getPk();
|
||||
|
||||
$fields = [$pk];
|
||||
$quickSearchArr = is_array($this->quickSearchField) ? $this->quickSearchField : explode(',', (string) $this->quickSearchField);
|
||||
foreach ($quickSearchArr as $f) {
|
||||
$f = trim((string) $f);
|
||||
if ($f === '') continue;
|
||||
$f = str_contains($f, '.') ? substr($f, strrpos($f, '.') + 1) : $f;
|
||||
if ($f !== '' && !in_array($f, $fields, true)) {
|
||||
$fields[] = $f;
|
||||
}
|
||||
}
|
||||
|
||||
list($where, $alias, $limit, $order) = $this->queryBuilder();
|
||||
$modelTable = strtolower($this->model->getTable());
|
||||
$mainAlias = ($alias[$modelTable] ?? $modelTable) . '.';
|
||||
|
||||
// 联表时避免字段歧义:主表字段统一 select 为 "admin.xxx as xxx"
|
||||
$selectFields = [];
|
||||
foreach ($fields as $f) {
|
||||
$f = trim((string) $f);
|
||||
if ($f === '') continue;
|
||||
$selectFields[] = $mainAlias . $f . ' as ' . $f;
|
||||
}
|
||||
|
||||
// 联表时避免排序字段歧义:无前缀的字段默认加主表前缀
|
||||
$qualifiedOrder = [];
|
||||
if (is_array($order)) {
|
||||
foreach ($order as $k => $v) {
|
||||
$k = (string) $k;
|
||||
$qualifiedOrder[str_contains($k, '.') ? $k : ($mainAlias . $k)] = $v;
|
||||
}
|
||||
}
|
||||
|
||||
$query = $this->model
|
||||
->field($selectFields)
|
||||
->alias($alias)
|
||||
->where($where);
|
||||
|
||||
$topGroup = $this->request ? ($this->request->get('top_group') ?? $this->request->post('top_group')) : null;
|
||||
if ($topGroup === '1' || $topGroup === 1 || $topGroup === true) {
|
||||
$query = $query
|
||||
->join('admin_group_access aga', $mainAlias . 'id = aga.uid')
|
||||
->join('admin_group ag', 'aga.group_id = ag.id')
|
||||
->where('ag.pid', 0)
|
||||
->distinct(true);
|
||||
}
|
||||
|
||||
$res = $query
|
||||
->order($qualifiedOrder ?: $order)
|
||||
->paginate($limit);
|
||||
|
||||
return $this->success('', [
|
||||
'list' => $res->items(),
|
||||
'total' => $res->total(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function add(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
@@ -371,12 +275,9 @@ class Admin extends Backend
|
||||
if ($this->auth->isSuperAdmin()) {
|
||||
return null;
|
||||
}
|
||||
$allowedGroupIds = array_values(array_unique(array_merge(
|
||||
Db::name('admin_group_access')->where('uid', $this->auth->id)->column('group_id'),
|
||||
$this->auth->getAdminChildGroups()
|
||||
)));
|
||||
$authGroups = $this->auth->getAllAuthGroups('allAuthAndOthers');
|
||||
foreach ($groups as $group) {
|
||||
if (!in_array($group, $allowedGroupIds, false)) {
|
||||
if (!in_array($group, $authGroups)) {
|
||||
return $this->error(__('You have no permission to add an administrator to this group!'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace app\admin\controller\auth;
|
||||
|
||||
use Throwable;
|
||||
use app\common\controller\Backend;
|
||||
use app\admin\model\AdminLog as AdminLogModel;
|
||||
use support\Response;
|
||||
@@ -35,10 +36,7 @@ class AdminLog extends Backend
|
||||
|
||||
list($where, $alias, $limit, $order) = $this->queryBuilder();
|
||||
if (!$this->auth->isSuperAdmin()) {
|
||||
$scopeIds = $this->auth->getSelfAndSubordinateAdminIds();
|
||||
if ($scopeIds !== []) {
|
||||
$where[] = ['admin_id', 'in', $scopeIds];
|
||||
}
|
||||
$where[] = ['admin_id', '=', $this->auth->id];
|
||||
}
|
||||
$res = $this->model
|
||||
->withJoin($this->withJoinTable, $this->withJoinType)
|
||||
|
||||
@@ -17,6 +17,8 @@ use Webman\Http\Request;
|
||||
|
||||
class Group extends Backend
|
||||
{
|
||||
protected string $authMethod = 'allAuthAndOthers';
|
||||
|
||||
protected ?object $model = null;
|
||||
|
||||
protected string|array $preExcludeFields = ['create_time', 'update_time'];
|
||||
@@ -80,9 +82,6 @@ class Group extends Backend
|
||||
$rulesRes = $this->handleRules($data);
|
||||
if ($rulesRes instanceof Response) return $rulesRes;
|
||||
|
||||
$pidRes = $this->validateGroupParentId($data['pid'] ?? null);
|
||||
if ($pidRes instanceof Response) return $pidRes;
|
||||
|
||||
$result = false;
|
||||
$this->model->startTrans();
|
||||
try {
|
||||
@@ -145,11 +144,6 @@ class Group extends Backend
|
||||
$rulesRes = $this->handleRules($data);
|
||||
if ($rulesRes instanceof Response) return $rulesRes;
|
||||
|
||||
if (array_key_exists('pid', $data)) {
|
||||
$pidRes = $this->validateGroupParentId($data['pid'] ?? null);
|
||||
if ($pidRes instanceof Response) return $pidRes;
|
||||
}
|
||||
|
||||
$result = false;
|
||||
$this->model->startTrans();
|
||||
try {
|
||||
@@ -300,6 +294,8 @@ class Group extends Backend
|
||||
$pk = $this->model->getPk();
|
||||
$initKey = $request->get('initKey') ?? $pk;
|
||||
|
||||
$absoluteAuth = $request->get('absoluteAuth') ?? false;
|
||||
|
||||
if ($this->keyword) {
|
||||
$keyword = explode(' ', $this->keyword);
|
||||
foreach ($keyword as $item) {
|
||||
@@ -312,14 +308,11 @@ class Group extends Backend
|
||||
}
|
||||
|
||||
if (!$this->auth->isSuperAdmin()) {
|
||||
$descendantIds = $this->auth->getAdminChildGroups();
|
||||
// 本人所在组 + 树形下级;不含同级、不含其它分支(与 getAllAuthGroups 的「权限多寡」脱钩)
|
||||
$visibleIds = array_values(array_unique(array_merge($this->adminGroups, $descendantIds)));
|
||||
if ($visibleIds === []) {
|
||||
$where[] = ['id', '=', -1];
|
||||
} else {
|
||||
$where[] = ['id', 'in', $visibleIds];
|
||||
$authGroups = $this->auth->getAllAuthGroups($this->authMethod, $where);
|
||||
if (!$absoluteAuth) {
|
||||
$authGroups = array_merge($this->adminGroups, $authGroups);
|
||||
}
|
||||
$where[] = ['id', 'in', $authGroups];
|
||||
}
|
||||
$data = $this->model->where($where)->select()->toArray();
|
||||
|
||||
@@ -344,43 +337,9 @@ class Group extends Backend
|
||||
|
||||
private function checkAuth($groupId): ?Response
|
||||
{
|
||||
if ($this->auth->isSuperAdmin()) {
|
||||
return null;
|
||||
}
|
||||
$descendantIds = $this->auth->getAdminChildGroups();
|
||||
if (!in_array($groupId, $descendantIds, false)) {
|
||||
return $this->error(__('You can only operate subordinate role groups in the tree hierarchy~'));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增/编辑时校验父级:非超管只能挂在本人所在组或其树形下级之下,不可建顶级(pid=0)
|
||||
*/
|
||||
private function validateGroupParentId(mixed $pid): ?Response
|
||||
{
|
||||
if ($this->auth->isSuperAdmin()) {
|
||||
return null;
|
||||
}
|
||||
if ($pid === null || $pid === '' || $pid === false) {
|
||||
return $this->error(__('Non super administrators cannot create top-level role groups'));
|
||||
}
|
||||
if ($pid === 0 || $pid === '0') {
|
||||
return $this->error(__('Non super administrators cannot create top-level role groups'));
|
||||
}
|
||||
if (!is_numeric($pid)) {
|
||||
return $this->error(__('The parent group is not within your manageable scope'));
|
||||
}
|
||||
$allowed = array_values(array_unique(array_merge($this->adminGroups, $this->auth->getAdminChildGroups())));
|
||||
$ok = false;
|
||||
foreach ($allowed as $aid) {
|
||||
if ($aid == $pid) {
|
||||
$ok = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!$ok) {
|
||||
return $this->error(__('The parent group is not within your manageable scope'));
|
||||
$authGroups = $this->auth->getAllAuthGroups($this->authMethod, []);
|
||||
if (!$this->auth->isSuperAdmin() && !in_array($groupId, $authGroups)) {
|
||||
return $this->error(__($this->authMethod == 'allAuth' ? 'You need to have all permissions of this group to operate this group~' : 'You need to have all the permissions of the group and have additional permissions before you can operate the group~'));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -581,13 +581,6 @@ class Crud extends Backend
|
||||
|
||||
private function parseModelMethods($field, &$modelData): void
|
||||
{
|
||||
// MySQL bigint/int 时间戳字段:显式声明为 integer,避免 ThinkORM 自动时间戳写入 'now' 字符串
|
||||
if (in_array($field['name'] ?? '', ['create_time', 'update_time', 'createtime', 'updatetime'], true)
|
||||
&& in_array($field['type'] ?? '', ['int', 'bigint'], true)
|
||||
) {
|
||||
$modelData['fieldType'][$field['name']] = 'integer';
|
||||
}
|
||||
|
||||
if (($field['designType'] ?? '') == 'array') {
|
||||
$modelData['fieldType'][$field['name']] = 'json';
|
||||
} elseif (!in_array($field['name'], ['create_time', 'update_time', 'updatetime', 'createtime'])
|
||||
|
||||
@@ -1,661 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace app\admin\controller\game;
|
||||
|
||||
use Throwable;
|
||||
use app\common\controller\Backend;
|
||||
use app\common\library\GameRewardConfigTemplate;
|
||||
use support\think\Db;
|
||||
use support\Response;
|
||||
use Webman\Http\Request as WebmanRequest;
|
||||
|
||||
/**
|
||||
* 渠道管理
|
||||
*/
|
||||
class Channel extends Backend
|
||||
{
|
||||
/**
|
||||
* GameChannel模型对象
|
||||
* @var object|null
|
||||
* @phpstan-var \app\common\model\GameChannel|null
|
||||
*/
|
||||
protected ?object $model = null;
|
||||
|
||||
protected array|string $preExcludeFields = ['id', 'user_count', 'profit_amount', 'create_time', 'update_time'];
|
||||
|
||||
protected array $withJoinTable = ['adminGroup', 'admin'];
|
||||
|
||||
protected string|array $quickSearchField = ['id', 'code', 'name'];
|
||||
|
||||
/**
|
||||
* 非超级管理员仅能操作 game_channel.admin_id 为当前账号的渠道;超管不限制
|
||||
* @see \app\common\controller\Backend::getDataLimitAdminIds()
|
||||
*/
|
||||
protected bool|string|int $dataLimit = true;
|
||||
|
||||
/**
|
||||
* adminTree 为辅助接口,默认权限节点名 game/channel/admintree 往往未在后台录入;
|
||||
* 与列表权限 game/channel/index 对齐,避免子管理员已勾「渠道管理」仍 401。
|
||||
*/
|
||||
protected array $noNeedPermission = ['adminTree', 'deleteRelatedCounts'];
|
||||
|
||||
protected function initController(WebmanRequest $request): ?Response
|
||||
{
|
||||
$this->model = new \app\common\model\GameChannel();
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表;附带 delete_related_counts=1 时返回删除前关联数据统计(走与 index 相同的路由入口,避免单独 URL 在部分环境下 404)
|
||||
*
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function index(WebmanRequest $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) {
|
||||
return $response;
|
||||
}
|
||||
$delPreview = $request->get('delete_related_counts');
|
||||
if ($delPreview === '1' || $delPreview === 1 || $delPreview === true) {
|
||||
return $this->deleteRelatedCountsResponse($request);
|
||||
}
|
||||
|
||||
return $this->_index();
|
||||
}
|
||||
|
||||
/**
|
||||
* 渠道-管理员树(父级=渠道,子级=管理员,仅可选择子级)
|
||||
*/
|
||||
public function adminTree(WebmanRequest $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
if (!$this->auth->check('game/channel/index')) {
|
||||
return $this->error(__('You have no permission'));
|
||||
}
|
||||
|
||||
$channelQuery = Db::name('game_channel')
|
||||
->field(['id', 'name', 'admin_group_id'])
|
||||
->order('id', 'asc');
|
||||
if (!$this->auth->isSuperAdmin()) {
|
||||
$channelQuery->where('admin_id', $this->auth->id);
|
||||
}
|
||||
$channels = $channelQuery->select()->toArray();
|
||||
|
||||
$groupChildrenCache = [];
|
||||
$getGroupChildren = function ($groupId) use (&$getGroupChildren, &$groupChildrenCache) {
|
||||
if ($groupId === null || $groupId === '') return [];
|
||||
if (array_key_exists($groupId, $groupChildrenCache)) return $groupChildrenCache[$groupId];
|
||||
$children = Db::name('admin_group')
|
||||
->where('pid', $groupId)
|
||||
->where('status', 1)
|
||||
->column('id');
|
||||
$all = [];
|
||||
foreach ($children as $cid) {
|
||||
$all[] = $cid;
|
||||
foreach ($getGroupChildren($cid) as $cc) {
|
||||
$all[] = $cc;
|
||||
}
|
||||
}
|
||||
$groupChildrenCache[$groupId] = $all;
|
||||
return $all;
|
||||
};
|
||||
|
||||
$tree = [];
|
||||
foreach ($channels as $ch) {
|
||||
$groupId = $ch['admin_group_id'] ?? null;
|
||||
$groupIds = [];
|
||||
if ($groupId !== null && $groupId !== '') {
|
||||
$groupIds[] = $groupId;
|
||||
foreach ($getGroupChildren($groupId) as $gid) {
|
||||
$groupIds[] = $gid;
|
||||
}
|
||||
}
|
||||
|
||||
$adminIds = [];
|
||||
if ($groupIds) {
|
||||
$adminIds = Db::name('admin_group_access')
|
||||
->where('group_id', 'in', array_unique($groupIds))
|
||||
->column('uid');
|
||||
}
|
||||
$adminIds = array_values(array_unique($adminIds));
|
||||
|
||||
$admins = [];
|
||||
if ($adminIds) {
|
||||
$admins = Db::name('admin')
|
||||
->field(['id', 'username'])
|
||||
->where('id', 'in', $adminIds)
|
||||
->order('id', 'asc')
|
||||
->select()
|
||||
->toArray();
|
||||
}
|
||||
|
||||
$children = [];
|
||||
foreach ($admins as $a) {
|
||||
$children[] = [
|
||||
'value' => (string) $a['id'],
|
||||
'label' => $a['username'],
|
||||
'channel_id' => $ch['id'],
|
||||
'is_leaf' => true,
|
||||
];
|
||||
}
|
||||
|
||||
$tree[] = [
|
||||
'value' => 'channel_' . $ch['id'],
|
||||
'label' => $ch['name'],
|
||||
'disabled' => true,
|
||||
'children' => $children,
|
||||
];
|
||||
}
|
||||
|
||||
return $this->success('', [
|
||||
'list' => $tree,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加(重写:管理员只选顶级组;admin_group_id 后端自动写入)
|
||||
* @throws Throwable
|
||||
*/
|
||||
protected function _add(): Response
|
||||
{
|
||||
if ($this->request && $this->request->method() === 'POST') {
|
||||
$data = $this->request->post();
|
||||
if (!$data) {
|
||||
return $this->error(__('Parameter %s can not be empty', ['']));
|
||||
}
|
||||
|
||||
$data = $this->applyInputFilter($data);
|
||||
$data = $this->excludeFields($data);
|
||||
|
||||
if (!$this->auth->isSuperAdmin()) {
|
||||
$data['admin_id'] = $this->auth->id;
|
||||
}
|
||||
|
||||
$adminId = $data['admin_id'] ?? null;
|
||||
if ($adminId === null || $adminId === '') {
|
||||
return $this->error(__('Parameter %s can not be empty', ['admin_id']));
|
||||
}
|
||||
|
||||
// 不允许前端填写,统一后端根据管理员所属“顶级角色组(pid=0)”自动回填
|
||||
if (array_key_exists('admin_group_id', $data)) {
|
||||
unset($data['admin_group_id']);
|
||||
}
|
||||
|
||||
$topGroupId = Db::name('admin_group_access')
|
||||
->alias('aga')
|
||||
->join('admin_group ag', 'aga.group_id = ag.id')
|
||||
->where('aga.uid', $adminId)
|
||||
->where('ag.pid', 0)
|
||||
->value('ag.id');
|
||||
|
||||
if ($topGroupId === null || $topGroupId === '') {
|
||||
return $this->error(__('Record not found'));
|
||||
}
|
||||
$data['admin_group_id'] = $topGroupId;
|
||||
|
||||
if ($this->dataLimit && $this->dataLimitFieldAutoFill) {
|
||||
$data[$this->dataLimitField] = $this->auth->id;
|
||||
}
|
||||
|
||||
$result = false;
|
||||
$this->model->startTrans();
|
||||
try {
|
||||
if ($this->modelValidate) {
|
||||
$validate = str_replace("\\model\\", "\\validate\\", get_class($this->model));
|
||||
if (class_exists($validate)) {
|
||||
$validate = new $validate();
|
||||
if ($this->modelSceneValidate) {
|
||||
$validate->scene('add');
|
||||
}
|
||||
$validate->check($data);
|
||||
}
|
||||
}
|
||||
$result = $this->model->save($data);
|
||||
$this->model->commit();
|
||||
} catch (Throwable $e) {
|
||||
$this->model->rollback();
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
if ($result !== false) {
|
||||
$newChannelId = $this->resolveNewChannelIdAfterInsert($data);
|
||||
if (!$this->isPositiveChannelId($newChannelId)) {
|
||||
$code = $data['code'] ?? null;
|
||||
if (is_string($code) && trim($code) !== '') {
|
||||
$newChannelId = Db::name('game_channel')->where('code', trim($code))->order('id', 'desc')->value('id');
|
||||
}
|
||||
}
|
||||
if ($this->isPositiveChannelId($newChannelId)) {
|
||||
try {
|
||||
$this->copyGameConfigFromChannelZero($newChannelId);
|
||||
$this->copyRewardConfigFromTemplate($newChannelId);
|
||||
} catch (Throwable $e) {
|
||||
return $this->error(__('Game channel copy default config failed') . ': ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
return $this->success(__('Added successfully'));
|
||||
}
|
||||
return $this->error(__('No rows were added'));
|
||||
}
|
||||
|
||||
return $this->error(__('Parameter error'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑(重写:管理员只选顶级组;admin_group_id 后端自动写入)
|
||||
* @throws Throwable
|
||||
*/
|
||||
protected function _edit(): Response
|
||||
{
|
||||
$pk = $this->model->getPk();
|
||||
$id = $this->request ? ($this->request->post($pk) ?? $this->request->get($pk)) : null;
|
||||
$row = $this->model->find($id);
|
||||
if (!$row) {
|
||||
return $this->error(__('Record not found'));
|
||||
}
|
||||
|
||||
$dataLimitAdminIds = $this->getDataLimitAdminIds();
|
||||
if ($dataLimitAdminIds && !in_array($row[$this->dataLimitField], $dataLimitAdminIds)) {
|
||||
return $this->error(__('You have no permission'));
|
||||
}
|
||||
|
||||
if ($this->request && $this->request->method() === 'POST') {
|
||||
$data = $this->request->post();
|
||||
if (!$data) {
|
||||
return $this->error(__('Parameter %s can not be empty', ['']));
|
||||
}
|
||||
|
||||
$data = $this->applyInputFilter($data);
|
||||
$data = $this->excludeFields($data);
|
||||
|
||||
// 不允许前端填写,统一后端根据管理员所属“顶级角色组(pid=0)”自动回填
|
||||
if (array_key_exists('admin_group_id', $data)) {
|
||||
unset($data['admin_group_id']);
|
||||
}
|
||||
|
||||
if (!$this->auth->isSuperAdmin()) {
|
||||
unset($data['admin_id']);
|
||||
}
|
||||
|
||||
$nextAdminId = array_key_exists('admin_id', $data) ? $data['admin_id'] : ($row['admin_id'] ?? null);
|
||||
if ($nextAdminId !== null && $nextAdminId !== '') {
|
||||
$topGroupId = Db::name('admin_group_access')
|
||||
->alias('aga')
|
||||
->join('admin_group ag', 'aga.group_id = ag.id')
|
||||
->where('aga.uid', $nextAdminId)
|
||||
->where('ag.pid', 0)
|
||||
->value('ag.id');
|
||||
|
||||
if ($topGroupId === null || $topGroupId === '') {
|
||||
return $this->error(__('Record not found'));
|
||||
}
|
||||
$data['admin_group_id'] = $topGroupId;
|
||||
}
|
||||
|
||||
$result = false;
|
||||
$this->model->startTrans();
|
||||
try {
|
||||
if ($this->modelValidate) {
|
||||
$validate = str_replace("\\model\\", "\\validate\\", get_class($this->model));
|
||||
if (class_exists($validate)) {
|
||||
$validate = new $validate();
|
||||
if ($this->modelSceneValidate) {
|
||||
$validate->scene('edit');
|
||||
}
|
||||
$data[$pk] = $row[$pk];
|
||||
$validate->check($data);
|
||||
}
|
||||
}
|
||||
$result = $row->save($data);
|
||||
$this->model->commit();
|
||||
} catch (Throwable $e) {
|
||||
$this->model->rollback();
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
if ($result !== false) {
|
||||
return $this->success(__('Update successful'));
|
||||
}
|
||||
return $this->error(__('No rows updated'));
|
||||
}
|
||||
|
||||
return $this->success('', [
|
||||
'row' => $row
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查看
|
||||
* @throws Throwable
|
||||
*/
|
||||
protected function _index(): Response
|
||||
{
|
||||
// 如果是 select 则转发到 select 方法,若未重写该方法,其实还是继续执行 index
|
||||
if ($this->request && $this->request->get('select')) {
|
||||
return $this->select($this->request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. withJoin 不可使用 alias 方法设置表别名,别名将自动使用关联模型名称(小写下划线命名规则)
|
||||
* 2. 以下的别名设置了主表别名,同时便于拼接查询参数等
|
||||
* 3. paginate 数据集可使用链式操作 each(function($item, $key) {}) 遍历处理
|
||||
*/
|
||||
list($where, $alias, $limit, $order) = $this->queryBuilder();
|
||||
$res = $this->model
|
||||
->withJoin($this->withJoinTable, $this->withJoinType)
|
||||
->with($this->withJoinTable)
|
||||
->visible(['adminGroup' => ['name'], 'admin' => ['username']])
|
||||
->alias($alias)
|
||||
->where($where)
|
||||
->order($order)
|
||||
->paginate($limit);
|
||||
|
||||
$list = $this->buildChannelListWithRealtimeUserCounts($res->items());
|
||||
|
||||
return $this->success('', [
|
||||
'list' => $list,
|
||||
'total' => $res->total(),
|
||||
'remark' => get_route_remark(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表 user_count 按 game_user.game_channel_id 实时 COUNT,与库字段无关(用户增删改时会回写 game_channel.user_count)
|
||||
*
|
||||
* @param iterable<int|string, mixed> $items
|
||||
* @return list<array<string, mixed>>
|
||||
*/
|
||||
private function buildChannelListWithRealtimeUserCounts(iterable $items): array
|
||||
{
|
||||
$rows = [];
|
||||
foreach ($items as $item) {
|
||||
$rows[] = is_array($item) ? $item : $item->toArray();
|
||||
}
|
||||
if ($rows === []) {
|
||||
return [];
|
||||
}
|
||||
$ids = [];
|
||||
foreach ($rows as $r) {
|
||||
if (isset($r['id'])) {
|
||||
$ids[] = $r['id'];
|
||||
}
|
||||
}
|
||||
if ($ids === []) {
|
||||
return $rows;
|
||||
}
|
||||
$agg = Db::name('game_user')
|
||||
->where('game_channel_id', 'in', $ids)
|
||||
->field('game_channel_id, count(*) as cnt')
|
||||
->group('game_channel_id')
|
||||
->select()
|
||||
->toArray();
|
||||
$countMap = [];
|
||||
foreach ($agg as $a) {
|
||||
$countMap[$a['game_channel_id']] = (int) $a['cnt'];
|
||||
}
|
||||
foreach ($rows as &$r) {
|
||||
$cid = $r['id'] ?? null;
|
||||
$r['user_count'] = ($cid !== null && $cid !== '') ? ($countMap[$cid] ?? 0) : 0;
|
||||
}
|
||||
unset($r);
|
||||
|
||||
return $rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除前统计:与当前选中渠道相关的游戏配置条数、游戏用户条数(须具备 game/channel/del)
|
||||
*
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function deleteRelatedCounts(WebmanRequest $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
return $this->deleteRelatedCountsResponse($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Throwable
|
||||
*/
|
||||
private function deleteRelatedCountsResponse(WebmanRequest $request): Response
|
||||
{
|
||||
if (!$this->auth->check('game/channel/del')) {
|
||||
return $this->error(__('You have no permission'));
|
||||
}
|
||||
|
||||
$channelIds = $this->getAuthorizedChannelIdsForIncomingIds($request);
|
||||
if ($channelIds === []) {
|
||||
return $this->success('', [
|
||||
'game_config_count' => 0,
|
||||
'game_user_count' => 0,
|
||||
]);
|
||||
}
|
||||
|
||||
// 实时统计:game_config.channel_id、game_user.game_channel_id(与渠道 id 一致)
|
||||
$configCount = Db::name('game_config')->where('channel_id', 'in', $channelIds)->count();
|
||||
$userCount = Db::name('game_user')->where('game_channel_id', 'in', $channelIds)->count();
|
||||
|
||||
return $this->success('', [
|
||||
'game_config_count' => $configCount,
|
||||
'game_user_count' => $userCount,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除渠道:若存在关联的游戏配置或用户,须带 confirm_cascade=1;同时级联删除关联数据
|
||||
*
|
||||
* @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') ?? []) : [];
|
||||
if (!is_array($ids)) {
|
||||
$ids = $ids !== null && $ids !== '' ? [$ids] : [];
|
||||
}
|
||||
if ($ids === []) {
|
||||
return $this->error(__('Parameter error'));
|
||||
}
|
||||
|
||||
$pk = $this->model->getPk();
|
||||
$where[] = [$pk, 'in', $ids];
|
||||
|
||||
$data = $this->model->where($where)->select();
|
||||
if (count($data) === 0) {
|
||||
return $this->error(__('No rows were deleted'));
|
||||
}
|
||||
|
||||
$channelIds = [];
|
||||
foreach ($data as $v) {
|
||||
$channelIds[] = $v[$pk];
|
||||
}
|
||||
|
||||
// 删除确认用实时条数:game_config.channel_id、game_user.game_channel_id
|
||||
$configCount = Db::name('game_config')->where('channel_id', 'in', $channelIds)->count();
|
||||
$userCount = Db::name('game_user')->where('game_channel_id', 'in', $channelIds)->count();
|
||||
|
||||
$confirmCascade = $this->request->get('confirm_cascade');
|
||||
if ($confirmCascade === null || $confirmCascade === '') {
|
||||
$confirmCascade = $this->request->post('confirm_cascade');
|
||||
}
|
||||
$confirmed = $confirmCascade === 1 || $confirmCascade === '1' || $confirmCascade === true;
|
||||
|
||||
if (($configCount > 0 || $userCount > 0) && !$confirmed) {
|
||||
return $this->error(__('Game channel delete need confirm related'), [
|
||||
'need_confirm' => true,
|
||||
'game_config_count' => $configCount,
|
||||
'game_user_count' => $userCount,
|
||||
]);
|
||||
}
|
||||
|
||||
$count = 0;
|
||||
$this->model->startTrans();
|
||||
try {
|
||||
Db::name('game_config')->where('channel_id', 'in', $channelIds)->delete();
|
||||
Db::name('game_user')->where('game_channel_id', 'in', $channelIds)->delete();
|
||||
foreach ($data as $v) {
|
||||
$count += $v->delete();
|
||||
}
|
||||
$this->model->commit();
|
||||
} catch (Throwable $e) {
|
||||
$this->model->rollback();
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
|
||||
if ($count) {
|
||||
return $this->success(__('Deleted successfully'));
|
||||
}
|
||||
|
||||
return $this->error(__('No rows were deleted'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string|int> $ids
|
||||
* @return list<int|string>
|
||||
*/
|
||||
private function getAuthorizedChannelIdsForIncomingIds(WebmanRequest $request): array
|
||||
{
|
||||
$where = [];
|
||||
$dataLimitAdminIds = $this->getDataLimitAdminIds();
|
||||
if ($dataLimitAdminIds) {
|
||||
$where[] = [$this->dataLimitField, 'in', $dataLimitAdminIds];
|
||||
}
|
||||
|
||||
$ids = $request->post('ids') ?? $request->get('ids') ?? [];
|
||||
if (!is_array($ids)) {
|
||||
$ids = $ids !== null && $ids !== '' ? [$ids] : [];
|
||||
}
|
||||
if ($ids === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$pk = $this->model->getPk();
|
||||
$where[] = [$pk, 'in', $ids];
|
||||
|
||||
return $this->model->where($where)->column($pk);
|
||||
}
|
||||
|
||||
/**
|
||||
* ThinkORM 在连接池/部分驱动下,insert 后 getKey() 可能未及时带上自增 id,这里多路兜底
|
||||
*
|
||||
* @param array<string, mixed> $postedChannelData 已过滤后的入库数据(含 code 等)
|
||||
*/
|
||||
private function resolveNewChannelIdAfterInsert(array $postedChannelData): int|string|null
|
||||
{
|
||||
$pk = $this->model->getPk();
|
||||
$id = $this->model->getKey();
|
||||
if ($this->isPositiveChannelId($id)) {
|
||||
return $id;
|
||||
}
|
||||
$rowData = $this->model->getData();
|
||||
if (is_array($rowData)) {
|
||||
if (isset($rowData[$pk]) && $this->isPositiveChannelId($rowData[$pk])) {
|
||||
return $rowData[$pk];
|
||||
}
|
||||
if (isset($rowData['id']) && $this->isPositiveChannelId($rowData['id'])) {
|
||||
return $rowData['id'];
|
||||
}
|
||||
}
|
||||
$lastInsId = $this->model->db()->getLastInsID();
|
||||
if ($this->isPositiveChannelId($lastInsId)) {
|
||||
return $lastInsId;
|
||||
}
|
||||
$lastInsId2 = Db::name('game_channel')->getLastInsID();
|
||||
if ($this->isPositiveChannelId($lastInsId2)) {
|
||||
return $lastInsId2;
|
||||
}
|
||||
$code = $postedChannelData['code'] ?? null;
|
||||
if (is_string($code) && trim($code) !== '') {
|
||||
$found = Db::name('game_channel')->where('code', trim($code))->order('id', 'desc')->value('id');
|
||||
if ($this->isPositiveChannelId($found)) {
|
||||
return $found;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function isPositiveChannelId(mixed $id): bool
|
||||
{
|
||||
if ($id === null || $id === '') {
|
||||
return false;
|
||||
}
|
||||
if (is_numeric($id)) {
|
||||
return $id > 0;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 新建渠道后:game_reward_config 优先从 game_channel_id=0 的默认模板复制;若无则使用 resource JSON 模板
|
||||
*
|
||||
* @param int|string $newChannelId 新建 game_channel.id
|
||||
*/
|
||||
private function copyRewardConfigFromTemplate(int|string $newChannelId): void
|
||||
{
|
||||
$exists = Db::name('game_reward_config')->where('game_channel_id', $newChannelId)->count();
|
||||
if ($exists > 0) {
|
||||
return;
|
||||
}
|
||||
$now = time();
|
||||
$tpl = Db::name('game_reward_config')->whereIn('game_channel_id', [0, '0'])->order('id', 'asc')->find();
|
||||
if ($tpl) {
|
||||
Db::name('game_reward_config')->insert([
|
||||
'game_channel_id' => $newChannelId,
|
||||
'tier_reward_form' => $tpl['tier_reward_form'],
|
||||
'bigwin_form' => $tpl['bigwin_form'],
|
||||
'create_time' => $now,
|
||||
'update_time' => $now,
|
||||
]);
|
||||
return;
|
||||
}
|
||||
$cols = GameRewardConfigTemplate::getDefaultJsonColumns();
|
||||
Db::name('game_reward_config')->insert([
|
||||
'game_channel_id' => $newChannelId,
|
||||
'tier_reward_form' => $cols['tier_reward_form'],
|
||||
'bigwin_form' => $cols['bigwin_form'],
|
||||
'create_time' => $now,
|
||||
'update_time' => $now,
|
||||
]);
|
||||
}
|
||||
|
||||
private function copyGameConfigFromChannelZero(int|string $newChannelId): void
|
||||
{
|
||||
$exists = Db::name('game_config')->where('channel_id', $newChannelId)->count();
|
||||
if ($exists > 0) {
|
||||
return;
|
||||
}
|
||||
// 全局模板:channel_id 为 0 或字符串 '0'(与业务约定一致)
|
||||
$rows = Db::name('game_config')
|
||||
->whereIn('channel_id', [0, '0'])
|
||||
->select()
|
||||
->toArray();
|
||||
if ($rows === []) {
|
||||
return;
|
||||
}
|
||||
$now = time();
|
||||
foreach ($rows as $row) {
|
||||
foreach (['ID', 'id', 'Id'] as $pkField) {
|
||||
unset($row[$pkField]);
|
||||
}
|
||||
$row['channel_id'] = $newChannelId;
|
||||
$row['create_time'] = $now;
|
||||
$row['update_time'] = $now;
|
||||
Db::name('game_config')->strict(false)->insert($row);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 若需重写查看、编辑、删除等方法,请复制 @see \app\admin\library\traits\Backend 中对应的方法至此进行重写
|
||||
*/
|
||||
}
|
||||
@@ -1,443 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace app\admin\controller\game;
|
||||
|
||||
use Throwable;
|
||||
use app\common\controller\Backend;
|
||||
use support\think\Db;
|
||||
use support\Response;
|
||||
use Webman\Http\Request as WebmanRequest;
|
||||
|
||||
/**
|
||||
* 游戏配置
|
||||
*/
|
||||
class Config extends Backend
|
||||
{
|
||||
/**
|
||||
* GameConfig模型对象
|
||||
* @var object|null
|
||||
* @phpstan-var \app\common\model\GameConfig|null
|
||||
*/
|
||||
protected ?object $model = null;
|
||||
|
||||
/**
|
||||
* 数据范围:非超管仅本人 + 下级角色组内管理员;game_config 无 admin_id,通过 channel_id 关联 game_channel.admin_id 限定
|
||||
*/
|
||||
protected bool|string|int $dataLimit = 'parent';
|
||||
|
||||
/**
|
||||
* 列表/删除等条件字段为 channel_id(见 {@see getDataLimitAdminIds()} 实际返回渠道 ID)
|
||||
*/
|
||||
protected string $dataLimitField = 'channel_id';
|
||||
|
||||
/**
|
||||
* 表无 admin_id,勿自动写入
|
||||
*/
|
||||
protected bool $dataLimitFieldAutoFill = false;
|
||||
|
||||
protected string|array $defaultSortField = 'channel_id,desc';
|
||||
|
||||
protected array $withJoinTable = ['channel'];
|
||||
|
||||
protected array|string $preExcludeFields = ['create_time', 'update_time'];
|
||||
|
||||
protected string|array $quickSearchField = ['ID'];
|
||||
|
||||
/** default_tier_weight / default_kill_score_weight:每项≤100 且各项之和必须=100 */
|
||||
private const WEIGHT_SUM_100_NAMES = ['default_tier_weight', 'default_kill_score_weight'];
|
||||
|
||||
/** default_bigwin_weight:仅校验每项整数、0~10000(5/30 固定 10000),不参与「和≤100」 */
|
||||
private const BIGWIN_WEIGHT_NAME = 'default_bigwin_weight';
|
||||
|
||||
protected function initController(WebmanRequest $request): ?Response
|
||||
{
|
||||
$this->model = new \app\common\model\GameConfig();
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将「可访问管理员 ID」转为「其负责的渠道 ID」,供 queryBuilder 使用 channel_id IN (...)
|
||||
*
|
||||
* @return list<int|string>
|
||||
*/
|
||||
protected function getDataLimitAdminIds(): array
|
||||
{
|
||||
if (!$this->dataLimit || !$this->auth || $this->auth->isSuperAdmin()) {
|
||||
return [];
|
||||
}
|
||||
$adminIds = parent::getDataLimitAdminIds();
|
||||
if ($adminIds === []) {
|
||||
return [];
|
||||
}
|
||||
$channelIds = Db::name('game_channel')->where('admin_id', 'in', $adminIds)->column('id');
|
||||
if ($channelIds === []) {
|
||||
return [-1];
|
||||
}
|
||||
return array_values(array_unique($channelIds));
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Throwable
|
||||
*/
|
||||
protected function _add(): Response
|
||||
{
|
||||
if ($this->request && $this->request->method() === 'POST') {
|
||||
$data = $this->request->post();
|
||||
if (!$data) {
|
||||
return $this->error(__('Parameter %s can not be empty', ['']));
|
||||
}
|
||||
|
||||
$data = $this->applyInputFilter($data);
|
||||
$data = $this->excludeFields($data);
|
||||
|
||||
$err = $this->validateGameWeightPayload($data, null);
|
||||
if ($err !== null) {
|
||||
return $this->error($err);
|
||||
}
|
||||
|
||||
if (!$this->auth->isSuperAdmin()) {
|
||||
$allowedChannelIds = $this->getDataLimitAdminIds();
|
||||
$cid = $data['channel_id'] ?? null;
|
||||
if ($cid === null || $cid === '') {
|
||||
return $this->error(__('Parameter %s can not be empty', ['channel_id']));
|
||||
}
|
||||
if ($allowedChannelIds !== [] && !in_array($cid, $allowedChannelIds)) {
|
||||
return $this->error(__('You have no permission'));
|
||||
}
|
||||
}
|
||||
|
||||
$result = false;
|
||||
$this->model->startTrans();
|
||||
try {
|
||||
if ($this->modelValidate) {
|
||||
$validate = str_replace("\\model\\", "\\validate\\", get_class($this->model));
|
||||
if (class_exists($validate)) {
|
||||
$validate = new $validate();
|
||||
if ($this->modelSceneValidate) {
|
||||
$validate->scene('add');
|
||||
}
|
||||
$validate->check($data);
|
||||
}
|
||||
}
|
||||
$result = $this->model->save($data);
|
||||
$this->model->commit();
|
||||
} catch (Throwable $e) {
|
||||
$this->model->rollback();
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
if ($result !== false) {
|
||||
return $this->success(__('Added successfully'));
|
||||
}
|
||||
return $this->error(__('No rows were added'));
|
||||
}
|
||||
|
||||
return $this->error(__('Parameter error'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Throwable
|
||||
*/
|
||||
protected function _edit(): Response
|
||||
{
|
||||
$pk = $this->model->getPk();
|
||||
$id = $this->request ? ($this->request->post($pk) ?? $this->request->get($pk)) : null;
|
||||
$row = $this->model->find($id);
|
||||
if (!$row) {
|
||||
return $this->error(__('Record not found'));
|
||||
}
|
||||
|
||||
$dataLimitAdminIds = $this->getDataLimitAdminIds();
|
||||
if ($dataLimitAdminIds && !in_array($row[$this->dataLimitField], $dataLimitAdminIds)) {
|
||||
return $this->error(__('You have no permission'));
|
||||
}
|
||||
|
||||
if ($this->request && $this->request->method() === 'POST') {
|
||||
$data = $this->request->post();
|
||||
if (!$data) {
|
||||
return $this->error(__('Parameter %s can not be empty', ['']));
|
||||
}
|
||||
|
||||
$data = $this->applyInputFilter($data);
|
||||
$data = $this->excludeFields($data);
|
||||
|
||||
if (!$this->auth->isSuperAdmin()) {
|
||||
$data['channel_id'] = $row['channel_id'];
|
||||
$data['group'] = $row['group'];
|
||||
$data['name'] = $row['name'];
|
||||
$data['title'] = $row['title'];
|
||||
} elseif (!isset($data['name']) || $data['name'] === '' || $data['name'] === null) {
|
||||
// JSON/表单未带 name 时回退库值,避免走「每项≤10000」
|
||||
$data['name'] = $row['name'] ?? '';
|
||||
}
|
||||
// 超管编辑时若未传 group,用库值参与 game_weight 校验(与 name 回退一致)
|
||||
if ($this->auth->isSuperAdmin() && trim((string) ($data['group'] ?? '')) === '') {
|
||||
$data['group'] = (string) ($row['group'] ?? '');
|
||||
}
|
||||
|
||||
$err = $this->validateGameWeightPayload($data, $row['value'] ?? null);
|
||||
if ($err !== null) {
|
||||
return $this->error($err);
|
||||
}
|
||||
|
||||
$result = false;
|
||||
$this->model->startTrans();
|
||||
try {
|
||||
if ($this->modelValidate) {
|
||||
$validate = str_replace("\\model\\", "\\validate\\", get_class($this->model));
|
||||
if (class_exists($validate)) {
|
||||
$validate = new $validate();
|
||||
if ($this->modelSceneValidate) {
|
||||
$validate->scene('edit');
|
||||
}
|
||||
$data[$pk] = $row[$pk];
|
||||
$validate->check($data);
|
||||
}
|
||||
}
|
||||
$result = $row->save($data);
|
||||
$this->model->commit();
|
||||
} catch (Throwable $e) {
|
||||
$this->model->rollback();
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
if ($result !== false) {
|
||||
return $this->success(__('Update successful'));
|
||||
}
|
||||
return $this->error(__('No rows updated'));
|
||||
}
|
||||
|
||||
return $this->success('', [
|
||||
'row' => $row
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 与前端 gameWeightFixed.normalizeGameWeightConfigName 一致:trim、去括号后说明、小写
|
||||
*/
|
||||
private function normalizeGameWeightConfigName(string $raw): string
|
||||
{
|
||||
$s = trim($raw);
|
||||
if (preg_match('/^([^((]+)/u', $s, $m)) {
|
||||
$s = trim($m[1]);
|
||||
}
|
||||
|
||||
return strtolower($s);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 game_weight JSON 里每一项的权重:支持 int/float、数字字符串(与前端 JSON.stringify 一致常为字符串)
|
||||
*/
|
||||
private function parseGameWeightScalarToFloat(mixed $v): ?float
|
||||
{
|
||||
if (is_int($v) || is_float($v)) {
|
||||
return (float) $v;
|
||||
}
|
||||
if (is_bool($v) || is_array($v) || $v === null) {
|
||||
return null;
|
||||
}
|
||||
$s = trim((string) $v);
|
||||
if ($s === '') {
|
||||
return null;
|
||||
}
|
||||
$f = filter_var($s, FILTER_VALIDATE_FLOAT);
|
||||
if ($f !== false) {
|
||||
return $f;
|
||||
}
|
||||
$i = filter_var($s, FILTER_VALIDATE_INT);
|
||||
if ($i !== false) {
|
||||
return (float) $i;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* game_weight:tier/kill 每项≤100 且和必须=100;bigwin 每项 0~10000;编辑时键不可改
|
||||
*
|
||||
* @param array<string, mixed> $data
|
||||
*/
|
||||
private function validateGameWeightPayload(array &$data, ?string $originalValue): ?string
|
||||
{
|
||||
$group = strtolower(trim((string) ($data['group'] ?? '')));
|
||||
if ($group !== 'game_weight') {
|
||||
return null;
|
||||
}
|
||||
$rawName = (string) ($data['name'] ?? '');
|
||||
$rawName = preg_replace('/[\x{200B}-\x{200D}\x{FEFF}]/u', '', $rawName);
|
||||
$name = $this->normalizeGameWeightConfigName($rawName);
|
||||
|
||||
$rawValue = $data['value'] ?? null;
|
||||
$valueWasArray = false;
|
||||
if (is_array($rawValue)) {
|
||||
$decoded = $rawValue;
|
||||
$valueWasArray = true;
|
||||
} elseif (is_string($rawValue)) {
|
||||
$s = trim($rawValue);
|
||||
if (str_starts_with($s, "\xEF\xBB\xBF")) {
|
||||
$s = substr($s, 3);
|
||||
}
|
||||
if ($s === '') {
|
||||
return __('Parameter error');
|
||||
}
|
||||
$decoded = json_decode($s, true);
|
||||
// 双重 JSON 字符串(外层已解析为字符串时)
|
||||
if (is_string($decoded)) {
|
||||
$decoded = json_decode(trim($decoded), true);
|
||||
}
|
||||
} else {
|
||||
return __('Parameter error');
|
||||
}
|
||||
if (!is_array($decoded)) {
|
||||
return __('Parameter error');
|
||||
}
|
||||
|
||||
// 骰子键 5~30 结构一律按大奖 0~10000 校验(避免库中 name 与 JSON 不一致时误走 tier/kill 单项上限)
|
||||
$diceBigwin = $this->gameWeightDecodedIsBigwinDiceKeys($decoded);
|
||||
// 归一化后的 name + 原始串包含 default_bigwin_weight(兼容说明后缀、异常空格),避免误走单项上限
|
||||
$rawLower = strtolower($rawName);
|
||||
$useBigwinRules = $diceBigwin
|
||||
|| ($name === self::BIGWIN_WEIGHT_NAME)
|
||||
|| str_contains($rawLower, 'default_bigwin_weight');
|
||||
|
||||
$keys = [];
|
||||
$numbers = [];
|
||||
foreach ($decoded as $item) {
|
||||
if (!is_array($item)) {
|
||||
return __('Parameter error');
|
||||
}
|
||||
foreach ($item as $k => $v) {
|
||||
$keys[] = (string) $k;
|
||||
$num = $this->parseGameWeightScalarToFloat($v);
|
||||
if ($num === null) {
|
||||
return __('Game config weight value must be numeric');
|
||||
}
|
||||
if ($useBigwinRules) {
|
||||
if ($num < 0 || $num > 10000) {
|
||||
return __('Game config bigwin weight each 0 10000');
|
||||
}
|
||||
$ks = strval($k);
|
||||
if (($ks === '5' || $ks === '30') && abs($num - 10000.0) > 0.000001) {
|
||||
return __('Game config bigwin weight locked 5 30');
|
||||
}
|
||||
} else {
|
||||
if ($num > 100) {
|
||||
return __('Game config weight each value must not exceed 100');
|
||||
}
|
||||
}
|
||||
$numbers[] = $num;
|
||||
}
|
||||
}
|
||||
|
||||
if (count($numbers) === 0) {
|
||||
return __('Parameter %s can not be empty', ['value']);
|
||||
}
|
||||
|
||||
if ($originalValue !== null && $originalValue !== '') {
|
||||
$oldKeys = $this->extractGameWeightKeys($originalValue);
|
||||
if ($oldKeys !== $keys) {
|
||||
return __('Game config weight keys cannot be modified');
|
||||
}
|
||||
}
|
||||
|
||||
if (!$useBigwinRules && in_array($name, self::WEIGHT_SUM_100_NAMES, true)) {
|
||||
$sum = array_sum($numbers);
|
||||
if (abs($sum - 100.0) > 0.000001) {
|
||||
return __('Game config weight sum must equal 100');
|
||||
}
|
||||
}
|
||||
|
||||
if ($valueWasArray) {
|
||||
$data['value'] = json_encode($decoded, JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否为 default_bigwin_weight 的固定骰子键集合(与前端 BIGWIN_WEIGHT_KEYS 一致)
|
||||
*
|
||||
* @param array<mixed> $decoded
|
||||
*/
|
||||
private function gameWeightDecodedIsBigwinDiceKeys(array $decoded): bool
|
||||
{
|
||||
$keys = [];
|
||||
foreach ($decoded as $item) {
|
||||
if (!is_array($item)) {
|
||||
return false;
|
||||
}
|
||||
foreach (array_keys($item) as $k) {
|
||||
$keys[] = trim((string) $k);
|
||||
}
|
||||
}
|
||||
if (count($keys) !== 6) {
|
||||
return false;
|
||||
}
|
||||
$ints = [];
|
||||
foreach ($keys as $k) {
|
||||
if ($k === '' || !ctype_digit($k)) {
|
||||
return false;
|
||||
}
|
||||
$ints[] = (int) $k;
|
||||
}
|
||||
sort($ints, SORT_NUMERIC);
|
||||
|
||||
return $ints === [5, 10, 15, 20, 25, 30];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
private function extractGameWeightKeys(string $value): array
|
||||
{
|
||||
$decoded = json_decode($value, true);
|
||||
if (!is_array($decoded)) {
|
||||
return [];
|
||||
}
|
||||
$keys = [];
|
||||
foreach ($decoded as $item) {
|
||||
if (!is_array($item)) {
|
||||
continue;
|
||||
}
|
||||
foreach ($item as $k => $_) {
|
||||
$keys[] = (string) $k;
|
||||
}
|
||||
}
|
||||
return $keys;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查看
|
||||
* @throws Throwable
|
||||
*/
|
||||
protected function _index(): Response
|
||||
{
|
||||
// 如果是 select 则转发到 select 方法,若未重写该方法,其实还是继续执行 index
|
||||
if ($this->request && $this->request->get('select')) {
|
||||
return $this->select($this->request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. withJoin 不可使用 alias 方法设置表别名,别名将自动使用关联模型名称(小写下划线命名规则)
|
||||
* 2. 以下的别名设置了主表别名,同时便于拼接查询参数等
|
||||
* 3. paginate 数据集可使用链式操作 each(function($item, $key) {}) 遍历处理
|
||||
*/
|
||||
list($where, $alias, $limit, $order) = $this->queryBuilder();
|
||||
$res = $this->model
|
||||
->withJoin($this->withJoinTable, $this->withJoinType)
|
||||
->with($this->withJoinTable)
|
||||
->visible(['channel' => ['name']])
|
||||
->alias($alias)
|
||||
->where($where)
|
||||
->order($order)
|
||||
->paginate($limit);
|
||||
|
||||
return $this->success('', [
|
||||
'list' => $res->items(),
|
||||
'total' => $res->total(),
|
||||
'remark' => get_route_remark(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 若需重写查看、编辑、删除等方法,请复制 @see \app\admin\library\traits\Backend 中对应方法至此进行重写
|
||||
*/
|
||||
}
|
||||
@@ -1,277 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\admin\controller\game;
|
||||
|
||||
use Throwable;
|
||||
use app\common\controller\Backend;
|
||||
use app\common\library\GameRewardConfigTemplate;
|
||||
use app\common\library\GameRewardTierBoardGenerator;
|
||||
use app\common\library\GameRewardWeightSeeder;
|
||||
use app\common\model\GameRewardConfig;
|
||||
use app\common\validate\GameRewardConfig as GameRewardConfigValidate;
|
||||
use support\think\Db;
|
||||
use support\Response;
|
||||
use Webman\Http\Request as WebmanRequest;
|
||||
|
||||
/**
|
||||
* 游戏奖励配置(渠道表单页,路由 /admin/game/rewardConfig)
|
||||
* 约定:game_channel_id = 0 为超管维护的「全渠道默认模板」,新建渠道时优先从此行复制到新渠道
|
||||
*/
|
||||
class RewardConfig extends Backend
|
||||
{
|
||||
/** 默认模板渠道主键(非真实渠道,仅存库一行) */
|
||||
private const DEFAULT_TEMPLATE_CHANNEL_ID = 0;
|
||||
|
||||
public function index(WebmanRequest $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
[$channelId, $err] = $this->resolveTargetChannelId($request, false);
|
||||
if ($err !== null) {
|
||||
return $err;
|
||||
}
|
||||
|
||||
$row = GameRewardConfig::where('game_channel_id', $channelId)->find();
|
||||
if ($row) {
|
||||
return $this->success('', [
|
||||
'row' => $row->toArray(),
|
||||
]);
|
||||
}
|
||||
|
||||
$defaults = GameRewardConfigTemplate::getDefaultJsonColumns();
|
||||
|
||||
return $this->success('', [
|
||||
'row' => [
|
||||
'id' => null,
|
||||
'game_channel_id' => $channelId,
|
||||
'tier_reward_form' => $defaults['tier_reward_form'],
|
||||
'bigwin_form' => $defaults['bigwin_form'],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function save(WebmanRequest $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
if ($request->method() !== 'POST') {
|
||||
return $this->error(__('Parameter error'));
|
||||
}
|
||||
|
||||
$data = $request->post();
|
||||
if (!$data) {
|
||||
return $this->error(__('Parameter %s can not be empty', ['']));
|
||||
}
|
||||
|
||||
[$channelId, $err] = $this->resolveTargetChannelId($request, true);
|
||||
if ($err !== null) {
|
||||
return $err;
|
||||
}
|
||||
|
||||
if (!$this->channelExists($channelId)) {
|
||||
return $this->error(__('Record not found'));
|
||||
}
|
||||
|
||||
$tier = $data['tier_reward_form'] ?? null;
|
||||
$big = $data['bigwin_form'] ?? null;
|
||||
if (!is_string($tier) || !is_string($big)) {
|
||||
return $this->error(__('Parameter error'));
|
||||
}
|
||||
|
||||
try {
|
||||
$validate = new GameRewardConfigValidate();
|
||||
$validate->scene('channel_form')->check([
|
||||
'tier_reward_form' => $tier,
|
||||
'bigwin_form' => $big,
|
||||
]);
|
||||
} catch (Throwable $e) {
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
|
||||
$existing = GameRewardConfig::where('game_channel_id', $channelId)->find();
|
||||
|
||||
try {
|
||||
if ($existing) {
|
||||
$existing->save([
|
||||
'tier_reward_form' => $tier,
|
||||
'bigwin_form' => $big,
|
||||
]);
|
||||
} else {
|
||||
$m = new GameRewardConfig();
|
||||
$m->save([
|
||||
'game_channel_id' => $channelId,
|
||||
'tier_reward_form' => $tier,
|
||||
'bigwin_form' => $big,
|
||||
]);
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
|
||||
return $this->success(__('Update successful'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 按条数与结算标准生成 26 格档位奖励并保存(保留当前 bigwin_form)
|
||||
*/
|
||||
public function generateTierBoard(WebmanRequest $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
if ($request->method() !== 'POST') {
|
||||
return $this->error(__('Parameter error'));
|
||||
}
|
||||
|
||||
$data = $request->post();
|
||||
if (!$data || !is_array($data)) {
|
||||
return $this->error(__('Parameter %s can not be empty', ['']));
|
||||
}
|
||||
|
||||
[$channelId, $err] = $this->resolveTargetChannelId($request, true);
|
||||
if ($err !== null) {
|
||||
return $err;
|
||||
}
|
||||
|
||||
if (!$this->channelExists($channelId)) {
|
||||
return $this->error(__('Record not found'));
|
||||
}
|
||||
|
||||
try {
|
||||
$out = GameRewardTierBoardGenerator::generate($data);
|
||||
} catch (Throwable $e) {
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
|
||||
$tier = $out['tier_reward_form'];
|
||||
$existing = GameRewardConfig::where('game_channel_id', $channelId)->find();
|
||||
if ($existing && is_string($existing->bigwin_form) && trim($existing->bigwin_form) !== '') {
|
||||
$big = $existing->bigwin_form;
|
||||
} else {
|
||||
$defaults = GameRewardConfigTemplate::getDefaultJsonColumns();
|
||||
$big = $defaults['bigwin_form'];
|
||||
}
|
||||
|
||||
try {
|
||||
$validate = new GameRewardConfigValidate();
|
||||
$validate->scene('channel_form')->check([
|
||||
'tier_reward_form' => $tier,
|
||||
'bigwin_form' => $big,
|
||||
]);
|
||||
} catch (Throwable $e) {
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
|
||||
try {
|
||||
if ($existing) {
|
||||
$existing->save([
|
||||
'tier_reward_form' => $tier,
|
||||
'bigwin_form' => $big,
|
||||
]);
|
||||
} else {
|
||||
$m = new GameRewardConfig();
|
||||
$m->save([
|
||||
'game_channel_id' => $channelId,
|
||||
'tier_reward_form' => $tier,
|
||||
'bigwin_form' => $big,
|
||||
]);
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
|
||||
return $this->success(__('Update successful'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据当前档位奖励 JSON 生成 game_reward_weight(先清空该渠道再写入 52 条)
|
||||
*/
|
||||
public function generateRewardWeight(WebmanRequest $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
if ($request->method() !== 'POST') {
|
||||
return $this->error(__('Parameter error'));
|
||||
}
|
||||
|
||||
[$channelId, $err] = $this->resolveTargetChannelId($request, true);
|
||||
if ($err !== null) {
|
||||
return $err;
|
||||
}
|
||||
|
||||
if (!$this->channelExists($channelId)) {
|
||||
return $this->error(__('Record not found'));
|
||||
}
|
||||
|
||||
$row = GameRewardConfig::where('game_channel_id', $channelId)->find();
|
||||
if (!$row || !is_string($row->tier_reward_form) || trim($row->tier_reward_form) === '') {
|
||||
return $this->error('请先保存档位奖励配置');
|
||||
}
|
||||
|
||||
try {
|
||||
GameRewardWeightSeeder::syncFromTierRewardForm($channelId, $row->tier_reward_form);
|
||||
} catch (Throwable $e) {
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
|
||||
return $this->success(__('Update successful'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{0: int, 1: Response|null}
|
||||
*/
|
||||
private function resolveTargetChannelId(WebmanRequest $request, bool $isPost): array
|
||||
{
|
||||
if ($this->auth->isSuperAdmin()) {
|
||||
$raw = $isPost ? ($request->post('game_channel_id') ?? $request->get('game_channel_id')) : $request->get('game_channel_id');
|
||||
if ($raw === null || $raw === '') {
|
||||
return [self::DEFAULT_TEMPLATE_CHANNEL_ID, null];
|
||||
}
|
||||
if (!is_numeric($raw)) {
|
||||
return [self::DEFAULT_TEMPLATE_CHANNEL_ID, $this->error(__('Parameter error'))];
|
||||
}
|
||||
$cid = intval(strval($raw));
|
||||
if ($cid < 0) {
|
||||
return [self::DEFAULT_TEMPLATE_CHANNEL_ID, $this->error(__('Parameter error'))];
|
||||
}
|
||||
if ($cid === self::DEFAULT_TEMPLATE_CHANNEL_ID) {
|
||||
return [self::DEFAULT_TEMPLATE_CHANNEL_ID, null];
|
||||
}
|
||||
|
||||
return [$cid, null];
|
||||
}
|
||||
|
||||
$ids = Db::name('game_channel')->where('admin_id', $this->auth->id)->order('id', 'asc')->column('id');
|
||||
if ($ids === []) {
|
||||
return [self::DEFAULT_TEMPLATE_CHANNEL_ID, $this->error(__('Record not found'))];
|
||||
}
|
||||
|
||||
return [intval(strval($ids[0])), null];
|
||||
}
|
||||
|
||||
private function channelExists(int $channelId): bool
|
||||
{
|
||||
if ($channelId === self::DEFAULT_TEMPLATE_CHANNEL_ID) {
|
||||
return $this->auth->isSuperAdmin();
|
||||
}
|
||||
|
||||
if ($this->auth->isSuperAdmin()) {
|
||||
return Db::name('game_channel')->where('id', $channelId)->count() > 0;
|
||||
}
|
||||
|
||||
return Db::name('game_channel')->where('id', $channelId)->where('admin_id', $this->auth->id)->count() > 0;
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace app\admin\controller\game;
|
||||
|
||||
use Throwable;
|
||||
use app\common\controller\Backend;
|
||||
use support\Response;
|
||||
use Webman\Http\Request as WebmanRequest;
|
||||
|
||||
/**
|
||||
* 游戏奖励权重配置
|
||||
*/
|
||||
class RewardWeight extends Backend
|
||||
{
|
||||
/**
|
||||
* GameRewardWeight模型对象
|
||||
* @var object|null
|
||||
* @phpstan-var \app\common\model\GameRewardWeight|null
|
||||
*/
|
||||
protected ?object $model = null;
|
||||
|
||||
protected array|string $preExcludeFields = ['id', 'create_time', 'update_time'];
|
||||
|
||||
protected array $withJoinTable = ['gameChannel'];
|
||||
|
||||
protected string|array $quickSearchField = ['id'];
|
||||
|
||||
protected function initController(WebmanRequest $request): ?Response
|
||||
{
|
||||
$this->model = new \app\common\model\GameRewardWeight();
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查看
|
||||
* @throws Throwable
|
||||
*/
|
||||
protected function _index(): Response
|
||||
{
|
||||
// 如果是 select 则转发到 select 方法,若未重写该方法,其实还是继续执行 index
|
||||
if ($this->request && $this->request->get('select')) {
|
||||
return $this->select($this->request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. withJoin 不可使用 alias 方法设置表别名,别名将自动使用关联模型名称(小写下划线命名规则)
|
||||
* 2. 以下的别名设置了主表别名,同时便于拼接查询参数等
|
||||
* 3. paginate 数据集可使用链式操作 each(function($item, $key) {}) 遍历处理
|
||||
*/
|
||||
list($where, $alias, $limit, $order) = $this->queryBuilder();
|
||||
$res = $this->model
|
||||
->withJoin($this->withJoinTable, $this->withJoinType)
|
||||
->with($this->withJoinTable)
|
||||
->visible(['gameChannel' => ['name']])
|
||||
->alias($alias)
|
||||
->where($where)
|
||||
->order($order)
|
||||
->paginate($limit);
|
||||
|
||||
return $this->success('', [
|
||||
'list' => $res->items(),
|
||||
'total' => $res->total(),
|
||||
'remark' => get_route_remark(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 若需重写查看、编辑、删除等方法,请复制 @see \app\admin\library\traits\Backend 中对应的方法至此进行重写
|
||||
*/
|
||||
}
|
||||
@@ -1,660 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace app\admin\controller\game;
|
||||
|
||||
use Throwable;
|
||||
use app\common\controller\Backend;
|
||||
use app\common\service\GameChannelUserCount;
|
||||
use support\think\Db;
|
||||
use support\Response;
|
||||
use Webman\Http\Request as WebmanRequest;
|
||||
|
||||
/**
|
||||
* 用户管理
|
||||
*/
|
||||
class User extends Backend
|
||||
{
|
||||
/**
|
||||
* GameUser模型对象
|
||||
* @var object|null
|
||||
* @phpstan-var \app\common\model\GameUser|null
|
||||
*/
|
||||
protected ?object $model = null;
|
||||
|
||||
/**
|
||||
* 数据范围:非超管仅本人 + 下级角色组内管理员(与 auth.Admin 一致,见 Backend::getDataLimitAdminIds parent)
|
||||
*/
|
||||
protected bool|string|int $dataLimit = 'parent';
|
||||
|
||||
/**
|
||||
* admin_id 由表单选择归属管理员,勿在保存时强制改为当前登录账号
|
||||
*/
|
||||
protected bool $dataLimitFieldAutoFill = false;
|
||||
|
||||
protected array|string $preExcludeFields = ['id', 'uuid', 'create_time', 'update_time'];
|
||||
|
||||
protected array $withJoinTable = ['gameChannel', 'admin'];
|
||||
|
||||
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
|
||||
{
|
||||
$this->model = new \app\common\model\GameUser();
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 抽奖券 ticket_count:空串、[]、[""]、无有效 {ante,count} 时写入 NULL,避免库里出现无意义 JSON
|
||||
*
|
||||
* @param array<string, mixed> $data
|
||||
*/
|
||||
private function normalizeEmptyTicketCount(array &$data): void
|
||||
{
|
||||
if (!array_key_exists('ticket_count', $data)) {
|
||||
return;
|
||||
}
|
||||
$v = $data['ticket_count'];
|
||||
if ($v === null) {
|
||||
$data['ticket_count'] = null;
|
||||
|
||||
return;
|
||||
}
|
||||
if ($v === '') {
|
||||
$data['ticket_count'] = null;
|
||||
|
||||
return;
|
||||
}
|
||||
if (!is_string($v)) {
|
||||
return;
|
||||
}
|
||||
$s = trim($v);
|
||||
if ($s === '' || $s === '[]' || strtolower($s) === 'null') {
|
||||
$data['ticket_count'] = null;
|
||||
|
||||
return;
|
||||
}
|
||||
$decoded = json_decode($s, true);
|
||||
if ($decoded === null && json_last_error() !== JSON_ERROR_NONE) {
|
||||
return;
|
||||
}
|
||||
if (!is_array($decoded)) {
|
||||
return;
|
||||
}
|
||||
if ($decoded === []) {
|
||||
$data['ticket_count'] = null;
|
||||
|
||||
return;
|
||||
}
|
||||
$valid = false;
|
||||
foreach ($decoded as $item) {
|
||||
if (!is_array($item)) {
|
||||
continue;
|
||||
}
|
||||
if (!array_key_exists('ante', $item) || !array_key_exists('count', $item)) {
|
||||
continue;
|
||||
}
|
||||
$ante = $item['ante'];
|
||||
$count = $item['count'];
|
||||
if ($ante === '' || $ante === null || $count === '' || $count === null) {
|
||||
continue;
|
||||
}
|
||||
if (!is_numeric($ante) || !is_numeric($count)) {
|
||||
continue;
|
||||
}
|
||||
$valid = true;
|
||||
break;
|
||||
}
|
||||
if (!$valid) {
|
||||
$data['ticket_count'] = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加(重写:password 使用 Admin 同款加密;uuid 由 username+channel_id 生成)
|
||||
* @throws Throwable
|
||||
*/
|
||||
protected function _add(): Response
|
||||
{
|
||||
if ($this->request && $this->request->method() === 'POST') {
|
||||
$data = $this->request->post();
|
||||
if (!$data) {
|
||||
return $this->error(__('Parameter %s can not be empty', ['']));
|
||||
}
|
||||
|
||||
$data = $this->applyInputFilter($data);
|
||||
$data = $this->excludeFields($data);
|
||||
$this->normalizeEmptyTicketCount($data);
|
||||
|
||||
$password = $data['password'] ?? null;
|
||||
if (!is_string($password) || trim($password) === '') {
|
||||
return $this->error(__('Parameter %s can not be empty', ['password']));
|
||||
}
|
||||
$data['password'] = hash_password($password);
|
||||
|
||||
$username = $data['username'] ?? '';
|
||||
$channelId = $data['channel_id'] ?? ($data['game_channel_id'] ?? null);
|
||||
if (!is_string($username) || trim($username) === '' || $channelId === null || $channelId === '') {
|
||||
return $this->error(__('Parameter %s can not be empty', ['username/channel_id']));
|
||||
}
|
||||
$data['uuid'] = md5(trim($username) . '|' . $channelId);
|
||||
|
||||
if ($this->gameUserUsernameExistsInChannel($username, $channelId)) {
|
||||
return $this->error(__('Game user username exists in channel'));
|
||||
}
|
||||
|
||||
if (!$this->auth->isSuperAdmin()) {
|
||||
$allowed = $this->getDataLimitAdminIds();
|
||||
$adminIdNew = $data['admin_id'] ?? null;
|
||||
if ($adminIdNew === null || $adminIdNew === '') {
|
||||
return $this->error(__('Parameter %s can not be empty', ['admin_id']));
|
||||
}
|
||||
if ($allowed !== [] && !in_array($adminIdNew, $allowed)) {
|
||||
return $this->error(__('You have no permission'));
|
||||
}
|
||||
}
|
||||
|
||||
$result = false;
|
||||
$this->model->startTrans();
|
||||
try {
|
||||
if ($this->modelValidate) {
|
||||
$validate = str_replace("\\model\\", "\\validate\\", get_class($this->model));
|
||||
if (class_exists($validate)) {
|
||||
$validate = new $validate();
|
||||
if ($this->modelSceneValidate) {
|
||||
$validate->scene('add');
|
||||
}
|
||||
$validate->check($data);
|
||||
}
|
||||
}
|
||||
$result = $this->model->save($data);
|
||||
$this->model->commit();
|
||||
} catch (Throwable $e) {
|
||||
$this->model->rollback();
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
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->error(__('No rows were added'));
|
||||
}
|
||||
|
||||
return $this->error(__('Parameter error'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑(重写:password 使用 Admin 同款加密;uuid 由 username+channel_id 生成)
|
||||
* @throws Throwable
|
||||
*/
|
||||
protected function _edit(): Response
|
||||
{
|
||||
$pk = $this->model->getPk();
|
||||
$id = $this->request ? ($this->request->post($pk) ?? $this->request->get($pk)) : null;
|
||||
$row = $this->model->find($id);
|
||||
if (!$row) {
|
||||
return $this->error(__('Record not found'));
|
||||
}
|
||||
|
||||
$dataLimitAdminIds = $this->getDataLimitAdminIds();
|
||||
if ($dataLimitAdminIds && !in_array($row[$this->dataLimitField], $dataLimitAdminIds)) {
|
||||
return $this->error(__('You have no permission'));
|
||||
}
|
||||
|
||||
$oldChannelId = $row['game_channel_id'] ?? $row['channel_id'] ?? null;
|
||||
|
||||
if ($this->request && $this->request->method() === 'POST') {
|
||||
$data = $this->request->post();
|
||||
if (!$data) {
|
||||
return $this->error(__('Parameter %s can not be empty', ['']));
|
||||
}
|
||||
|
||||
$data = $this->applyInputFilter($data);
|
||||
$data = $this->excludeFields($data);
|
||||
$this->normalizeEmptyTicketCount($data);
|
||||
|
||||
if (array_key_exists('password', $data)) {
|
||||
$password = $data['password'];
|
||||
if (!is_string($password) || trim($password) === '') {
|
||||
unset($data['password']);
|
||||
} else {
|
||||
$data['password'] = hash_password($password);
|
||||
}
|
||||
}
|
||||
|
||||
$nextUsername = array_key_exists('username', $data) ? $data['username'] : $row['username'];
|
||||
$nextChannelId = null;
|
||||
if (array_key_exists('channel_id', $data)) {
|
||||
$nextChannelId = $data['channel_id'];
|
||||
} elseif (array_key_exists('game_channel_id', $data)) {
|
||||
$nextChannelId = $data['game_channel_id'];
|
||||
} else {
|
||||
$nextChannelId = $row['channel_id'] ?? $row['game_channel_id'] ?? null;
|
||||
}
|
||||
|
||||
if (is_string($nextUsername) && trim($nextUsername) !== '' && $nextChannelId !== null && $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()) {
|
||||
$allowed = $this->getDataLimitAdminIds();
|
||||
$adminIdAfter = array_key_exists('admin_id', $data) ? $data['admin_id'] : ($row['admin_id'] ?? null);
|
||||
if ($allowed !== [] && $adminIdAfter !== null && $adminIdAfter !== '' && !in_array($adminIdAfter, $allowed)) {
|
||||
return $this->error(__('You have no permission'));
|
||||
}
|
||||
}
|
||||
|
||||
$result = false;
|
||||
$this->model->startTrans();
|
||||
try {
|
||||
if ($this->modelValidate) {
|
||||
$validate = str_replace("\\model\\", "\\validate\\", get_class($this->model));
|
||||
if (class_exists($validate)) {
|
||||
$validate = new $validate();
|
||||
if ($this->modelSceneValidate) {
|
||||
$validate->scene('edit');
|
||||
}
|
||||
$data[$pk] = $row[$pk];
|
||||
$validate->check($data);
|
||||
}
|
||||
}
|
||||
$result = $row->save($data);
|
||||
$this->model->commit();
|
||||
} catch (Throwable $e) {
|
||||
$this->model->rollback();
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
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->error(__('No rows updated'));
|
||||
}
|
||||
|
||||
// GET: 返回编辑数据时,剔除敏感字段
|
||||
unset($row['password'], $row['salt'], $row['token'], $row['refresh_token']);
|
||||
return $this->success('', [
|
||||
'row' => $row
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除后按 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
|
||||
*/
|
||||
protected function _index(): Response
|
||||
{
|
||||
// 如果是 select 则转发到 select 方法,若未重写该方法,其实还是继续执行 index
|
||||
if ($this->request && $this->request->get('select')) {
|
||||
return $this->select($this->request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. withJoin 不可使用 alias 方法设置表别名,别名将自动使用关联模型名称(小写下划线命名规则)
|
||||
* 2. 以下的别名设置了主表别名,同时便于拼接查询参数等
|
||||
* 3. paginate 数据集可使用链式操作 each(function($item, $key) {}) 遍历处理
|
||||
*/
|
||||
list($where, $alias, $limit, $order) = $this->queryBuilder();
|
||||
$res = $this->model
|
||||
->withJoin($this->withJoinTable, $this->withJoinType)
|
||||
->with($this->withJoinTable)
|
||||
->visible(['gameChannel' => ['name'], 'admin' => ['username']])
|
||||
->alias($alias)
|
||||
->where($where)
|
||||
->order($order)
|
||||
->paginate($limit);
|
||||
|
||||
return $this->success('', [
|
||||
'list' => $res->items(),
|
||||
'total' => $res->total(),
|
||||
'remark' => get_route_remark(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 新建用户时:超管可选各渠道 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 中对应的方法至此进行重写
|
||||
*/
|
||||
}
|
||||
149
app/admin/controller/mall/Player.php
Normal file
149
app/admin/controller/mall/Player.php
Normal file
@@ -0,0 +1,149 @@
|
||||
<?php
|
||||
|
||||
namespace app\admin\controller\mall;
|
||||
|
||||
use app\common\controller\Backend;
|
||||
use support\Response;
|
||||
use Throwable;
|
||||
use Webman\Http\Request;
|
||||
|
||||
/**
|
||||
* 积分商城用户
|
||||
*/
|
||||
class Player extends Backend
|
||||
{
|
||||
/**
|
||||
* Player模型对象
|
||||
* @var object|null
|
||||
* @phpstan-var \app\admin\model\mall\Player|null
|
||||
*/
|
||||
protected ?object $model = null;
|
||||
|
||||
protected array|string $preExcludeFields = ['id', 'create_time', 'update_time', 'password'];
|
||||
|
||||
protected string|array $quickSearchField = ['id'];
|
||||
|
||||
/** 列表不返回密码字段 */
|
||||
protected string|array $indexField = ['id', 'username', 'create_time', 'update_time', 'score'];
|
||||
|
||||
public function initialize(): void
|
||||
{
|
||||
parent::initialize();
|
||||
$this->model = new \app\admin\model\mall\Player();
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加(重写以支持密码加密)
|
||||
*/
|
||||
public function add(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response instanceof Response) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
if ($request->method() !== 'POST') {
|
||||
$this->error(__('Parameter error'));
|
||||
}
|
||||
|
||||
$data = $request->post();
|
||||
if (!$data) {
|
||||
$this->error(__('Parameter %s can not be empty', ['']));
|
||||
}
|
||||
|
||||
$passwd = $data['password'] ?? '';
|
||||
if (empty($passwd)) {
|
||||
$this->error(__('Parameter %s can not be empty', [__('Password')]));
|
||||
}
|
||||
|
||||
$data = $this->applyInputFilter($data);
|
||||
$data = $this->excludeFields($data);
|
||||
|
||||
$result = false;
|
||||
$this->model->startTrans();
|
||||
try {
|
||||
if ($this->modelValidate) {
|
||||
$validate = str_replace("\\model\\", "\\validate\\", get_class($this->model));
|
||||
if (class_exists($validate)) {
|
||||
$validate = new $validate();
|
||||
if ($this->modelSceneValidate) {
|
||||
$validate->scene('add');
|
||||
}
|
||||
$validate->check($data);
|
||||
}
|
||||
}
|
||||
$result = $this->model->save($data);
|
||||
if ($result !== false && $passwd) {
|
||||
$this->model->resetPassword((int) $this->model->id, $passwd);
|
||||
}
|
||||
$this->model->commit();
|
||||
} catch (Throwable $e) {
|
||||
$this->model->rollback();
|
||||
$this->error($e->getMessage());
|
||||
}
|
||||
|
||||
$result !== false ? $this->success(__('Added successfully')) : $this->error(__('No rows were added'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑(重写以支持编辑时密码可选)
|
||||
*/
|
||||
public function edit(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response instanceof Response) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$pk = $this->model->getPk();
|
||||
$id = $request->post($pk) ?? $request->get($pk);
|
||||
$row = $this->model->find($id);
|
||||
if (!$row) {
|
||||
$this->error(__('Record not found'));
|
||||
}
|
||||
|
||||
if ($request->method() === 'POST') {
|
||||
$data = $request->post();
|
||||
if (!$data) {
|
||||
$this->error(__('Parameter %s can not be empty', ['']));
|
||||
}
|
||||
|
||||
if (!empty($data['password'])) {
|
||||
$this->model->resetPassword((int) $row->id, $data['password']);
|
||||
}
|
||||
|
||||
$data = $this->applyInputFilter($data);
|
||||
$data = $this->excludeFields($data);
|
||||
|
||||
$result = false;
|
||||
$this->model->startTrans();
|
||||
try {
|
||||
if ($this->modelValidate) {
|
||||
$validate = str_replace("\\model\\", "\\validate\\", get_class($this->model));
|
||||
if (class_exists($validate)) {
|
||||
$validate = new $validate();
|
||||
if ($this->modelSceneValidate) {
|
||||
$validate->scene('edit');
|
||||
}
|
||||
$validate->check(array_merge($data, [$pk => $row[$pk]]));
|
||||
}
|
||||
}
|
||||
$result = $row->save($data);
|
||||
$this->model->commit();
|
||||
} catch (Throwable $e) {
|
||||
$this->model->rollback();
|
||||
$this->error($e->getMessage());
|
||||
}
|
||||
|
||||
return $result !== false ? $this->success(__('Update successful')) : $this->error(__('No rows updated'));
|
||||
}
|
||||
|
||||
unset($row['password']);
|
||||
$row['password'] = '';
|
||||
$this->success('', ['row' => $row]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 若需重写查看、删除等方法,请复制 @see \app\admin\library\traits\Backend 中对应的方法至此进行重写
|
||||
*/
|
||||
}
|
||||
@@ -34,7 +34,6 @@ return [
|
||||
'The uploaded file is too large (%sMiB), Maximum file size:%sMiB' => 'The uploaded file is too large (%sMiB), maximum file size:%sMiB',
|
||||
'No files have been uploaded or the file size exceeds the upload limit of the server' => 'No files have been uploaded or the file size exceeds the server upload limit.',
|
||||
'Unknown' => 'Unknown',
|
||||
'Global default' => 'Global default (channel_id=0)',
|
||||
'Super administrator' => 'Super administrator',
|
||||
'No permission' => 'No permission',
|
||||
'%first% etc. %count% items' => '%first% etc. %count% items',
|
||||
@@ -96,16 +95,4 @@ return [
|
||||
'%d records and files have been deleted' => '%d records and files have been deleted',
|
||||
'Please input correct username' => 'Please enter the correct username',
|
||||
'Group Name Arr' => 'Group Name Arr',
|
||||
'Game config weight keys cannot be modified' => 'Weight config keys cannot be modified',
|
||||
'Game config weight value must be numeric' => 'Weight values must be numeric',
|
||||
'Game config weight each value must not exceed 100' => 'Weight must not exceed 10000',
|
||||
'Game config weight each value must not exceed 10000' => 'Weight must not exceed 10000',
|
||||
'Game config weight sum must equal 100' => 'The sum of weights for default_tier_weight / default_kill_score_weight must equal 100',
|
||||
'Game config weight sum must not exceed 100' => 'The sum of weights for default_tier_weight / default_kill_score_weight must not exceed 100',
|
||||
'Game config bigwin weight must be integer' => 'Each default big win weight must be an integer',
|
||||
'Game config bigwin weight each 0 10000' => 'Each default big win weight must be between 0 and 10000',
|
||||
'Game config bigwin weight locked 5 30' => 'Dice totals 5 and 30 are guaranteed leopard (豹子); weight is fixed at 10000 and cannot be changed',
|
||||
'Game user username exists in channel' => 'This username already exists in the selected channel',
|
||||
'Game channel delete need confirm related' => 'Please confirm deletion of related game configuration and user data first',
|
||||
'Game channel copy default config failed' => 'Channel was created, but copying default game configuration failed',
|
||||
];
|
||||
@@ -9,8 +9,5 @@ return [
|
||||
'You need to have all the permissions of the group and have additional permissions before you can operate the group~' => 'You need to have all the permissions of the group and have additional permissions before you can operate the group~',
|
||||
'Role group has all your rights, please contact the upper administrator to add or do not need to add!' => 'Role group has all your rights, please contact the upper administrator to add or do not need to add!',
|
||||
'The group permission node exceeds the range that can be allocated' => 'The group permission node exceeds the range that can be allocated, please refresh and try again~',
|
||||
'You can only operate subordinate role groups in the tree hierarchy~' => 'You can only manage role groups that are strictly below yours in the role-group tree (not peers or other branches).',
|
||||
'Non super administrators cannot create top-level role groups' => 'Non super administrators cannot create top-level role groups',
|
||||
'The parent group is not within your manageable scope' => 'The selected parent group is outside your manageable scope',
|
||||
'Remark lang' => 'Role groups follow the tree (parent/child): you may only manage groups under your own membership branch; peers, other branches and ancestors cannot be managed here. When assigning rules you can still only select permission nodes you own.',
|
||||
'Remark lang' => 'For system security, the hierarchical relationship of role groups is for reference only. The actual hierarchy is determined by the number of permission nodes: same permissions = peer, containing and having additional permissions = superior. Peers cannot manage peers; superiors can assign their permission nodes to subordinates. For special cases where an admin needs to become a superior, create a virtual permission node.',
|
||||
];
|
||||
|
||||
@@ -37,7 +37,6 @@ return [
|
||||
'Topic format error' => '上传存储子目录格式错误!',
|
||||
'Driver %s not supported' => '不支持的驱动:%s',
|
||||
'Unknown' => '未知',
|
||||
'Global default' => '全局默认(channel_id=0)',
|
||||
// 权限类语言包-s
|
||||
'Super administrator' => '超级管理员',
|
||||
'No permission' => '无权限',
|
||||
@@ -115,16 +114,4 @@ return [
|
||||
'%d records and files have been deleted' => '已删除%d条记录和文件',
|
||||
'Please input correct username' => '请输入正确的用户名',
|
||||
'Group Name Arr' => '分组名称数组',
|
||||
'Game config weight keys cannot be modified' => '权重配置的键不可修改',
|
||||
'Game config weight value must be numeric' => '权重值必须为数字',
|
||||
'Game config weight each value must not exceed 100' => '权重不能超过10000',
|
||||
'Game config weight each value must not exceed 10000' => '权重不能超过10000',
|
||||
'Game config weight sum must equal 100' => 'default_tier_weight / default_kill_score_weight 的权重之和必须等于100',
|
||||
'Game config weight sum must not exceed 100' => 'default_tier_weight / default_kill_score_weight 的权重之和不能超过100',
|
||||
'Game config bigwin weight must be integer' => '默认大奖权重每项必须为整数',
|
||||
'Game config bigwin weight each 0 10000' => '默认大奖权重每项须在 0~10000 之间',
|
||||
'Game config bigwin weight locked 5 30' => '点数 5 与 30 为豹子号必中,权重固定为 10000,不可修改',
|
||||
'Game user username exists in channel' => '该渠道下已存在相同用户名的用户',
|
||||
'Game channel delete need confirm related' => '请先确认是否同时删除关联的游戏配置与用户数据',
|
||||
'Game channel copy default config failed' => '渠道已创建,但复制默认游戏配置失败',
|
||||
];
|
||||
@@ -9,8 +9,5 @@ return [
|
||||
'You need to have all the permissions of the group and have additional permissions before you can operate the group~' => '您需要拥有该分组的所有权限且还有额外权限时,才可以操作该分组~',
|
||||
'Role group has all your rights, please contact the upper administrator to add or do not need to add!' => '角色组拥有您的全部权限,请联系上级管理员添加或无需添加!',
|
||||
'The group permission node exceeds the range that can be allocated' => '分组权限节点超出可分配范围,请刷新重试~',
|
||||
'You can only operate subordinate role groups in the tree hierarchy~' => '仅可管理在角色组树中属于您下级的角色组(不含同级、不含其他分支)~',
|
||||
'Non super administrators cannot create top-level role groups' => '非超级管理员不能创建顶级角色组',
|
||||
'The parent group is not within your manageable scope' => '所选父级角色组不在您可管理的范围内',
|
||||
'Remark lang' => '角色组以「树形父子关系」为准:仅可管理本人所在组之下的下级组;同级、其他分支及上级组不可在此管理。分配权限时仍只能勾选您自身拥有的权限节点。',
|
||||
'Remark lang' => '为保障系统安全,角色组本身的上下级关系仅供参考,系统的实际上下级划分是根据`权限多寡`来确定的,两位管理员的权限节点:相同被认为是`同级`、包含且有额外权限才被认为是`上级`,同级不可管理同级,上级可为下级分配自己拥有的权限节点;若有特殊情况管理员需转`上级`,可建立一个虚拟权限节点',
|
||||
];
|
||||
|
||||
@@ -297,26 +297,6 @@ class Auth extends \ba\Auth
|
||||
return array_unique($children);
|
||||
}
|
||||
|
||||
/**
|
||||
* 本人 + 树形下级角色组内的管理员 ID(与管理员管理列表数据范围一致)
|
||||
*/
|
||||
public function getSelfAndSubordinateAdminIds(): array
|
||||
{
|
||||
if ($this->isSuperAdmin()) {
|
||||
return [];
|
||||
}
|
||||
$descendantGroupIds = $this->getAdminChildGroups();
|
||||
$adminIds = [];
|
||||
if ($descendantGroupIds !== []) {
|
||||
$adminIds = Db::name('admin_group_access')
|
||||
->where('group_id', 'in', $descendantGroupIds)
|
||||
->column('uid');
|
||||
}
|
||||
$adminIds[] = $this->id;
|
||||
|
||||
return array_values(array_unique($adminIds));
|
||||
}
|
||||
|
||||
public function getGroupChildGroups(int $groupId, array &$children): void
|
||||
{
|
||||
$childrenTemp = AdminGroup::where('pid', $groupId)
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
namespace {%namespace%};
|
||||
{%use%}
|
||||
use app\common\controller\Backend;
|
||||
use support\Response;
|
||||
use Webman\Http\Request as WebmanRequest;
|
||||
|
||||
/**
|
||||
* {%tableComment%}
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
* 查看
|
||||
* @throws Throwable
|
||||
*/
|
||||
protected function _index(): Response
|
||||
public function index(): void
|
||||
{
|
||||
// 如果是 select 则转发到 select 方法,若未重写该方法,其实还是继续执行 index
|
||||
if ($this->request && $this->request->get('select')) {
|
||||
return $this->select($this->request);
|
||||
if ($this->request->param('select')) {
|
||||
$this->select();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -18,14 +18,13 @@
|
||||
list($where, $alias, $limit, $order) = $this->queryBuilder();
|
||||
$res = $this->model
|
||||
->withJoin($this->withJoinTable, $this->withJoinType)
|
||||
->with($this->withJoinTable)
|
||||
{%relationVisibleFields%}
|
||||
->alias($alias)
|
||||
->where($where)
|
||||
->order($order)
|
||||
->paginate($limit);
|
||||
|
||||
return $this->success('', [
|
||||
$this->success('', [
|
||||
'list' => $res->items(),
|
||||
'total' => $res->total(),
|
||||
'remark' => get_route_remark(),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
|
||||
protected function initController(WebmanRequest $request): ?Response
|
||||
public function initialize(): void
|
||||
{
|
||||
parent::initialize();
|
||||
$this->model = new \{%modelNamespace%}\{%modelName%}();{%filterRule%}
|
||||
return null;
|
||||
}
|
||||
@@ -50,7 +50,6 @@ trait Backend
|
||||
$res = $this->model
|
||||
->field($this->indexField)
|
||||
->withJoin($this->withJoinTable, $this->withJoinType)
|
||||
->with($this->withJoinTable)
|
||||
->alias($alias)
|
||||
->where($where)
|
||||
->order($order)
|
||||
@@ -302,40 +301,7 @@ trait Backend
|
||||
/**
|
||||
* 加载为 select(远程下拉选择框)数据,子类可覆盖
|
||||
*/
|
||||
protected function _select(): Response
|
||||
protected function _select(): void
|
||||
{
|
||||
if (empty($this->model)) {
|
||||
return $this->success('', [
|
||||
'list' => [],
|
||||
'total' => 0,
|
||||
]);
|
||||
}
|
||||
|
||||
$pk = $this->model->getPk();
|
||||
|
||||
// 远程下拉只要求包含主键与可显示字段;这里尽量返回主键 + quickSearch 字段,避免全量字段带来性能问题
|
||||
$fields = [$pk];
|
||||
$quickSearchArr = is_array($this->quickSearchField) ? $this->quickSearchField : explode(',', (string) $this->quickSearchField);
|
||||
foreach ($quickSearchArr as $f) {
|
||||
$f = trim((string) $f);
|
||||
if ($f === '') continue;
|
||||
$f = str_contains($f, '.') ? substr($f, strrpos($f, '.') + 1) : $f;
|
||||
if ($f !== '' && !in_array($f, $fields, true)) {
|
||||
$fields[] = $f;
|
||||
}
|
||||
}
|
||||
|
||||
list($where, $alias, $limit, $order) = $this->queryBuilder();
|
||||
$res = $this->model
|
||||
->field($fields)
|
||||
->alias($alias)
|
||||
->where($where)
|
||||
->order($order)
|
||||
->paginate($limit);
|
||||
|
||||
return $this->success('', [
|
||||
'list' => $res->items(),
|
||||
'total' => $res->total(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
28
app/admin/model/mall/Player.php
Normal file
28
app/admin/model/mall/Player.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace app\admin\model\mall;
|
||||
|
||||
use app\common\model\traits\TimestampInteger;
|
||||
use support\think\Model;
|
||||
|
||||
/**
|
||||
* Player
|
||||
*/
|
||||
class Player extends Model
|
||||
{
|
||||
use TimestampInteger;
|
||||
|
||||
// 表名
|
||||
protected $name = 'mall_player';
|
||||
|
||||
// 自动写入时间戳字段
|
||||
protected $autoWriteTimestamp = true;
|
||||
|
||||
/**
|
||||
* 重置密码
|
||||
*/
|
||||
public function resetPassword(int $id, string $newPassword): bool
|
||||
{
|
||||
return $this->where(['id' => $id])->update(['password' => hash_password($newPassword)]) !== false;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\validate;
|
||||
namespace app\admin\validate\mall;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
class GameConfig extends Validate
|
||||
class Player extends Validate
|
||||
{
|
||||
protected $failException = true;
|
||||
|
||||
@@ -12,7 +12,6 @@ use ba\Filesystem;
|
||||
use app\common\controller\Api;
|
||||
use app\admin\model\Admin as AdminModel;
|
||||
use app\admin\model\User as UserModel;
|
||||
use app\process\Monitor;
|
||||
use support\Response;
|
||||
use Webman\Http\Request;
|
||||
use Phinx\Config\Config as PhinxConfig;
|
||||
@@ -429,20 +428,6 @@ class Install extends Api
|
||||
]);
|
||||
}
|
||||
|
||||
// Windows 下 php windows.php 会每秒检测监控目录;写入 config/.env 等会触发 taskkill 整进程,导致 POST 被中断(ERR_CONNECTION_RESET)
|
||||
Monitor::pause();
|
||||
try {
|
||||
return $this->baseConfigPost($request, $envOk, $rootPath, $migrateCommand);
|
||||
} finally {
|
||||
Monitor::resume();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 系统基础配置 POST:写入配置并执行迁移
|
||||
*/
|
||||
private function baseConfigPost(Request $request, bool $envOk, string $rootPath, string $migrateCommand): Response
|
||||
{
|
||||
$connectData = $databaseParam = $request->only(['hostname', 'username', 'password', 'hostport', 'database', 'prefix']);
|
||||
|
||||
// 数据库配置测试
|
||||
@@ -470,9 +455,6 @@ class Install extends Api
|
||||
return "\$env('database.{$key}', '" . addslashes($value) . "')";
|
||||
};
|
||||
$dbConfigText = preg_replace_callback("/\\\$env\('database\.(hostname|database|username|password|hostport|prefix)',\s*'[^']*'\)/", $callback, $dbConfigContent);
|
||||
if ($dbConfigText === null) {
|
||||
return $this->error(__('Failed to update database config file:%s', ['config/' . self::$dbConfigFileName]));
|
||||
}
|
||||
$result = @file_put_contents($dbConfigFile, $dbConfigText);
|
||||
if (!$result) {
|
||||
return $this->error(__('File has no write permission:%s', ['config/' . self::$dbConfigFileName]));
|
||||
|
||||
@@ -214,8 +214,9 @@ class Backend extends Api
|
||||
public function select(WebmanRequest $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response instanceof Response) return $response;
|
||||
return $this->_select();
|
||||
if ($response !== null) return $response;
|
||||
$this->_select();
|
||||
return $this->success();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\common\library;
|
||||
|
||||
/**
|
||||
* 游戏奖励配置:默认模板(与 resource/game_reward_config_template.json 一致)
|
||||
*/
|
||||
class GameRewardConfigTemplate
|
||||
{
|
||||
private static ?array $cached = null;
|
||||
|
||||
public static function templatePath(): string
|
||||
{
|
||||
return dirname(__DIR__, 3) . DIRECTORY_SEPARATOR . 'resource' . DIRECTORY_SEPARATOR . 'game_reward_config_template.json';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{tier_reward_form: string, bigwin_form: string}
|
||||
*/
|
||||
public static function getDefaultJsonColumns(): array
|
||||
{
|
||||
if (self::$cached !== null) {
|
||||
return self::$cached;
|
||||
}
|
||||
$path = self::templatePath();
|
||||
if (!is_file($path)) {
|
||||
throw new \RuntimeException('game_reward_config_template.json missing: ' . $path);
|
||||
}
|
||||
$raw = file_get_contents($path);
|
||||
if ($raw === false || trim($raw) === '') {
|
||||
throw new \RuntimeException('game_reward_config_template.json read failed');
|
||||
}
|
||||
$decoded = json_decode($raw, true);
|
||||
if (!is_array($decoded)) {
|
||||
throw new \RuntimeException('game_reward_config_template.json invalid JSON');
|
||||
}
|
||||
$tier = $decoded['tier_reward_form'] ?? null;
|
||||
$big = $decoded['bigwin_form'] ?? null;
|
||||
if (!is_array($tier) || !is_array($big)) {
|
||||
throw new \RuntimeException('game_reward_config_template.json missing tier_reward_form or bigwin_form');
|
||||
}
|
||||
$tierJson = json_encode($tier, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
$bigJson = json_encode($big, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
if (!is_string($tierJson) || !is_string($bigJson)) {
|
||||
throw new \RuntimeException('game_reward_config_template encode failed');
|
||||
}
|
||||
self::$cached = [
|
||||
'tier_reward_form' => $tierJson,
|
||||
'bigwin_form' => $bigJson,
|
||||
];
|
||||
return self::$cached;
|
||||
}
|
||||
}
|
||||
@@ -1,273 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\common\library;
|
||||
|
||||
/**
|
||||
* 按「顺/逆时针摇点落格」条数约束生成 26 格档位盘面(写入 tier_reward_form JSON)
|
||||
*/
|
||||
final class GameRewardTierBoardGenerator
|
||||
{
|
||||
private const LEOPARD = [5, 10, 15, 20, 25, 30];
|
||||
|
||||
private static function landingCw(int $d): int
|
||||
{
|
||||
$start = $d - 5;
|
||||
|
||||
return ($start + $d) % 26;
|
||||
}
|
||||
|
||||
/** 图二:逆时针 end = start − D,若小于 0 则 26 + start − D */
|
||||
private static function landingCcw(int $d): int
|
||||
{
|
||||
$start = $d - 5;
|
||||
$x = $start - $d;
|
||||
|
||||
return $x >= 0 ? $x : 26 + $start - $d;
|
||||
}
|
||||
|
||||
/**
|
||||
* 豹子点数:该次摇取顺、逆落点档位不能为 T4、T5
|
||||
*
|
||||
* @param list<string> $tier
|
||||
*/
|
||||
private static function leopardOk(array $tier): bool
|
||||
{
|
||||
foreach (self::LEOPARD as $d) {
|
||||
foreach ([self::landingCw($d), self::landingCcw($d)] as $idx) {
|
||||
$t = $tier[$idx];
|
||||
if ($t === 'T4' || $t === 'T5') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $params
|
||||
* @return array{tier_reward_form: string}
|
||||
*/
|
||||
public static function generate(array $params): array
|
||||
{
|
||||
$t1Cw = self::intParam($params, 't1_fixed_cw');
|
||||
$t1Ccw = self::intParam($params, 't1_fixed_ccw');
|
||||
$t2MinCw = self::intParam($params, 't2_min_cw');
|
||||
$t2MinCcw = self::intParam($params, 't2_min_ccw');
|
||||
$t4Cw = self::intParam($params, 't4_fixed_cw');
|
||||
$t4Ccw = self::intParam($params, 't4_fixed_ccw');
|
||||
$t5Cw = self::intParam($params, 't5_fixed_cw');
|
||||
$t5Ccw = self::intParam($params, 't5_fixed_ccw');
|
||||
|
||||
$amt1 = self::numParam($params, 'amt_t1');
|
||||
$amt2 = self::numParam($params, 'amt_t2');
|
||||
$amt3 = self::numParam($params, 'amt_t3');
|
||||
$amt4 = self::numParam($params, 'amt_t4');
|
||||
|
||||
$bestTier = null;
|
||||
$bestScore = INF;
|
||||
|
||||
for ($attempt = 0; $attempt < 32; $attempt++) {
|
||||
$tier = self::randomInitialTier($attempt);
|
||||
$temp = 5.0;
|
||||
for ($step = 0; $step < 8000; $step++) {
|
||||
$score = self::score($tier, $t1Cw, $t1Ccw, $t2MinCw, $t2MinCcw, $t4Cw, $t4Ccw, $t5Cw, $t5Ccw);
|
||||
if ($score < $bestScore) {
|
||||
$bestScore = $score;
|
||||
$bestTier = $tier;
|
||||
}
|
||||
$i = mt_rand(0, 25);
|
||||
$j = mt_rand(0, 25);
|
||||
if ($i === $j) {
|
||||
continue;
|
||||
}
|
||||
$oldI = $tier[$i];
|
||||
$oldJ = $tier[$j];
|
||||
$tier[$i] = $oldJ;
|
||||
$tier[$j] = $oldI;
|
||||
if (!self::leopardOk($tier)) {
|
||||
$tier[$i] = $oldI;
|
||||
$tier[$j] = $oldJ;
|
||||
continue;
|
||||
}
|
||||
$newScore = self::score($tier, $t1Cw, $t1Ccw, $t2MinCw, $t2MinCcw, $t4Cw, $t4Ccw, $t5Cw, $t5Ccw);
|
||||
$delta = $newScore - $score;
|
||||
$u = mt_rand() / max(1, mt_getrandmax());
|
||||
if ($delta < 0 || ($temp > 0.02 && exp(-$delta / $temp) > $u)) {
|
||||
// keep
|
||||
} else {
|
||||
$tier[$i] = $oldI;
|
||||
$tier[$j] = $oldJ;
|
||||
}
|
||||
$temp *= 0.999;
|
||||
}
|
||||
}
|
||||
|
||||
if ($bestTier === null || $bestScore > 45) {
|
||||
throw new \RuntimeException('无法在豹子与条数约束下收敛盘面,请调整条数后重试');
|
||||
}
|
||||
|
||||
$json = self::buildJson($bestTier, $amt1, $amt2, $amt3, $amt4);
|
||||
|
||||
return ['tier_reward_form' => $json];
|
||||
}
|
||||
|
||||
/** @return list<string> */
|
||||
private static function randomInitialTier(int $seedBias): array
|
||||
{
|
||||
mt_srand((int) (microtime(true) * 1000000) + $seedBias * 10007);
|
||||
$tier = [];
|
||||
for ($p = 0; $p < 26; $p++) {
|
||||
$tier[$p] = ['T1', 'T2', 'T3'][mt_rand(0, 2)];
|
||||
}
|
||||
if (!self::leopardOk($tier)) {
|
||||
for ($p = 0; $p < 26; $p++) {
|
||||
$tier[$p] = 'T3';
|
||||
}
|
||||
}
|
||||
|
||||
return $tier;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $tier
|
||||
*/
|
||||
private static function score(
|
||||
array $tier,
|
||||
int $t1Cw,
|
||||
int $t1Ccw,
|
||||
int $t2MinCw,
|
||||
int $t2MinCcw,
|
||||
int $t4Cw,
|
||||
int $t4Ccw,
|
||||
int $t5Cw,
|
||||
int $t5Ccw
|
||||
): float {
|
||||
if (!self::leopardOk($tier)) {
|
||||
return 1e9;
|
||||
}
|
||||
$h = self::histogram($tier);
|
||||
$s = 0.0;
|
||||
$s += ($h['cw']['T1'] - $t1Cw) ** 2;
|
||||
$s += ($h['ccw']['T1'] - $t1Ccw) ** 2;
|
||||
$s += max(0, $t2MinCw - $h['cw']['T2']) ** 2 * 8;
|
||||
$s += max(0, $t2MinCcw - $h['ccw']['T2']) ** 2 * 8;
|
||||
$s += ($h['cw']['T4'] - $t4Cw) ** 2;
|
||||
$s += ($h['ccw']['T4'] - $t4Ccw) ** 2;
|
||||
$s += ($h['cw']['T5'] - $t5Cw) ** 2;
|
||||
$s += ($h['ccw']['T5'] - $t5Ccw) ** 2;
|
||||
|
||||
return $s;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $tier
|
||||
* @return array{cw: array<string, int>, ccw: array<string, int>}
|
||||
*/
|
||||
private static function histogram(array $tier): array
|
||||
{
|
||||
$cw = ['T1' => 0, 'T2' => 0, 'T3' => 0, 'T4' => 0, 'T5' => 0];
|
||||
$ccw = ['T1' => 0, 'T2' => 0, 'T3' => 0, 'T4' => 0, 'T5' => 0];
|
||||
for ($d = 5; $d <= 30; $d++) {
|
||||
$cw[$tier[self::landingCw($d)]]++;
|
||||
$ccw[$tier[self::landingCcw($d)]]++;
|
||||
}
|
||||
|
||||
return ['cw' => $cw, 'ccw' => $ccw];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $tier
|
||||
*/
|
||||
private static function remarkForTier(string $t, float $amt2): string
|
||||
{
|
||||
if ($t === 'T1') {
|
||||
return '大奖';
|
||||
}
|
||||
if ($t === 'T2') {
|
||||
return $amt2 < 100 ? '完美回本' : '小赚';
|
||||
}
|
||||
if ($t === 'T3') {
|
||||
return '抽水';
|
||||
}
|
||||
if ($t === 'T4') {
|
||||
return '惩罚';
|
||||
}
|
||||
if ($t === 'T5') {
|
||||
return '再来一次';
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
private static function buildJson(array $tier, float $amt1, float $amt2, float $amt3, float $amt4): string
|
||||
{
|
||||
$rows = [];
|
||||
for ($i = 0; $i < 26; $i++) {
|
||||
$t = $tier[$i];
|
||||
if ($t === 'T5') {
|
||||
$ui = '再来一次';
|
||||
$uiEn = 'Once again';
|
||||
$ev = '0';
|
||||
} else {
|
||||
$a = match ($t) {
|
||||
'T1' => $amt1,
|
||||
'T2' => $amt2,
|
||||
'T3' => $amt3,
|
||||
'T4' => $amt4,
|
||||
default => $amt3,
|
||||
};
|
||||
$ui = self::fmtMoney($a);
|
||||
$uiEn = self::fmtMoney($a);
|
||||
$ev = self::fmtMoney($a);
|
||||
}
|
||||
$rows[] = [
|
||||
'grid_number' => strval(5 + $i),
|
||||
'ui_text' => $ui,
|
||||
'ui_text_en' => $uiEn,
|
||||
'real_ev' => $ev,
|
||||
'tier' => $t,
|
||||
'remark' => self::remarkForTier($t, $amt2),
|
||||
];
|
||||
}
|
||||
|
||||
return json_encode($rows, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);
|
||||
}
|
||||
|
||||
private static function fmtMoney(float $v): string
|
||||
{
|
||||
if (abs($v - round($v)) < 0.000001) {
|
||||
return strval((int) round($v));
|
||||
}
|
||||
|
||||
return rtrim(rtrim(sprintf('%.4f', $v), '0'), '.');
|
||||
}
|
||||
|
||||
private static function intParam(array $params, string $key): int
|
||||
{
|
||||
$v = $params[$key] ?? 0;
|
||||
if (is_string($v) && trim($v) === '') {
|
||||
return 0;
|
||||
}
|
||||
if (!is_numeric($v)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return intval(strval($v));
|
||||
}
|
||||
|
||||
private static function numParam(array $params, string $key): float
|
||||
{
|
||||
$v = $params[$key] ?? 0;
|
||||
if (is_string($v) && trim($v) === '') {
|
||||
return 0.0;
|
||||
}
|
||||
if (!is_numeric($v)) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
return floatval(strval($v));
|
||||
}
|
||||
}
|
||||
@@ -1,172 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\common\library;
|
||||
|
||||
use support\think\Db;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* 根据档位奖励 JSON 生成 game_reward_weight 对照(先删后插)
|
||||
*/
|
||||
final class GameRewardWeightSeeder
|
||||
{
|
||||
private const LEOPARD = [5, 10, 15, 20, 25, 30];
|
||||
|
||||
/**
|
||||
* @throws Throwable
|
||||
*/
|
||||
public static function syncFromTierRewardForm(int $gameChannelId, string $tierRewardFormJson): void
|
||||
{
|
||||
$decoded = json_decode($tierRewardFormJson, true);
|
||||
if (!is_array($decoded) || count($decoded) !== 26) {
|
||||
throw new \RuntimeException('档位奖励表单必须为 26 条且为合法 JSON');
|
||||
}
|
||||
|
||||
$byGrid = [];
|
||||
foreach ($decoded as $idx => $row) {
|
||||
if (!is_array($row)) {
|
||||
throw new \RuntimeException('档位奖励表单第' . strval($idx + 1) . '条格式错误');
|
||||
}
|
||||
$g = $row['grid_number'] ?? null;
|
||||
if (!is_numeric($g)) {
|
||||
throw new \RuntimeException('档位奖励表单第' . strval($idx + 1) . '条点数无效');
|
||||
}
|
||||
$gi = intval(strval($g));
|
||||
if ($gi < 5 || $gi > 30) {
|
||||
throw new \RuntimeException('档位奖励表单点数须在 5~30');
|
||||
}
|
||||
$byGrid[$gi] = $row;
|
||||
}
|
||||
|
||||
$cells = [];
|
||||
for ($i = 0; $i < 26; $i++) {
|
||||
$g = 5 + $i;
|
||||
if (!isset($byGrid[$g])) {
|
||||
throw new \RuntimeException('档位奖励表单缺少点数 ' . strval($g));
|
||||
}
|
||||
$cells[$i] = self::normalizeCell($byGrid[$g], $i + 1);
|
||||
}
|
||||
|
||||
self::assertLeopardOk($cells);
|
||||
|
||||
$batch = [];
|
||||
for ($d = 5; $d <= 30; $d++) {
|
||||
$start = $d - 5;
|
||||
$endCw = ($start + $d) % 26;
|
||||
$x = $start - $d;
|
||||
$endCcw = $x >= 0 ? $x : 26 + $start - $d;
|
||||
$batch[] = self::buildInsertRow($gameChannelId, 0, $d, $start, $endCw, $cells[$endCw]);
|
||||
$batch[] = self::buildInsertRow($gameChannelId, 1, $d, $start, $endCcw, $cells[$endCcw]);
|
||||
}
|
||||
|
||||
$now = time();
|
||||
foreach ($batch as $k => $_) {
|
||||
$batch[$k]['create_time'] = $now;
|
||||
$batch[$k]['update_time'] = $now;
|
||||
}
|
||||
|
||||
Db::startTrans();
|
||||
try {
|
||||
Db::name('game_reward_weight')->where('game_channel_id', $gameChannelId)->delete();
|
||||
Db::name('game_reward_weight')->insertAll($batch);
|
||||
Db::commit();
|
||||
} catch (Throwable $e) {
|
||||
Db::rollback();
|
||||
$msg = $e->getMessage();
|
||||
if (str_contains($msg, 'game_reward_weight') || str_contains($msg, "doesn't exist")) {
|
||||
throw new \RuntimeException('写入失败:请确认已创建数据表 game_reward_weight 并已执行迁移。' . $msg);
|
||||
}
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<array{ui_text: string, real_ev: float, tier: string, remark: string}> $cells
|
||||
*/
|
||||
private static function assertLeopardOk(array $cells): void
|
||||
{
|
||||
foreach (self::LEOPARD as $d) {
|
||||
$start = $d - 5;
|
||||
$endCw = ($start + $d) % 26;
|
||||
$x = $start - $d;
|
||||
$endCcw = $x >= 0 ? $x : 26 + $start - $d;
|
||||
foreach ([$endCw, $endCcw] as $idx) {
|
||||
$t = $cells[$idx]['tier'];
|
||||
if ($t === 'T4' || $t === 'T5') {
|
||||
throw new \RuntimeException(
|
||||
'豹子点数 ' . strval($d) . ' 的落点不能为 T4/T5,请先在档位表中调整后再生成权重对照'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $row
|
||||
* @return array{ui_text: string, real_ev: float, tier: string, remark: string}
|
||||
*/
|
||||
private static function normalizeCell(array $row, int $rowNo): array
|
||||
{
|
||||
$ui = $row['ui_text'] ?? null;
|
||||
$ev = $row['real_ev'] ?? null;
|
||||
$tier = $row['tier'] ?? null;
|
||||
if (!is_string($ui) || trim($ui) === '') {
|
||||
throw new \RuntimeException('档位奖励表单第' . strval($rowNo) . '条显示文本不能为空');
|
||||
}
|
||||
if ($ev === null || $ev === '' || !is_numeric($ev)) {
|
||||
throw new \RuntimeException('档位奖励表单第' . strval($rowNo) . '条实际中奖无效');
|
||||
}
|
||||
if (!is_string($tier) || !in_array($tier, ['T1', 'T2', 'T3', 'T4', 'T5'], true)) {
|
||||
throw new \RuntimeException('档位奖励表单第' . strval($rowNo) . '条档位无效');
|
||||
}
|
||||
$remark = $row['remark'] ?? '';
|
||||
$remarkStr = is_string($remark) ? $remark : '';
|
||||
|
||||
return [
|
||||
'ui_text' => $ui,
|
||||
'real_ev' => floatval(strval($ev)),
|
||||
'tier' => $tier,
|
||||
'remark' => $remarkStr,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{ui_text: string, real_ev: float, tier: string, remark: string} $cell
|
||||
* @return array<string, int|float|string>
|
||||
*/
|
||||
private static function buildInsertRow(
|
||||
int $gameChannelId,
|
||||
int $direction,
|
||||
int $gridNumber,
|
||||
int $startIndex,
|
||||
int $endIndex,
|
||||
array $cell
|
||||
): array {
|
||||
return [
|
||||
'game_channel_id' => $gameChannelId,
|
||||
'direction' => $direction,
|
||||
'grid_number' => $gridNumber,
|
||||
'start_index' => $startIndex,
|
||||
'end_index' => $endIndex,
|
||||
'ui_text' => $cell['ui_text'],
|
||||
'real_ev' => $cell['real_ev'],
|
||||
'tier' => $cell['tier'],
|
||||
'type' => self::tierToType($cell['tier']),
|
||||
'remark' => $cell['remark'],
|
||||
'weight' => 1,
|
||||
];
|
||||
}
|
||||
|
||||
private static function tierToType(string $tier): int
|
||||
{
|
||||
return match ($tier) {
|
||||
'T1' => 3,
|
||||
'T2' => 2,
|
||||
'T3' => -1,
|
||||
'T4' => -2,
|
||||
'T5' => 1,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -11,15 +11,12 @@ use Exception;
|
||||
*/
|
||||
class TokenExpirationException extends Exception
|
||||
{
|
||||
protected array $data = [];
|
||||
|
||||
public function __construct(
|
||||
string $message = '',
|
||||
int $code = 409,
|
||||
array $data = [],
|
||||
protected string $message = '',
|
||||
protected int $code = 409,
|
||||
protected array $data = [],
|
||||
?\Throwable $previous = null
|
||||
) {
|
||||
$this->data = $data;
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\model;
|
||||
|
||||
use support\think\Model;
|
||||
|
||||
/**
|
||||
* GameChannel
|
||||
*/
|
||||
class GameChannel extends Model
|
||||
{
|
||||
// 表名
|
||||
protected $name = 'game_channel';
|
||||
|
||||
// 自动写入时间戳字段
|
||||
protected $autoWriteTimestamp = true;
|
||||
|
||||
// 字段类型转换
|
||||
protected $type = [
|
||||
'create_time' => 'integer',
|
||||
'update_time' => 'integer',
|
||||
];
|
||||
|
||||
|
||||
public function getprofitAmountAttr($value): ?float
|
||||
{
|
||||
return is_null($value) ? null : (float)$value;
|
||||
}
|
||||
|
||||
public function adminGroup(): \think\model\relation\BelongsTo
|
||||
{
|
||||
return $this->belongsTo(\app\admin\model\AdminGroup::class, 'admin_group_id', 'id');
|
||||
}
|
||||
|
||||
public function admin(): \think\model\relation\BelongsTo
|
||||
{
|
||||
return $this->belongsTo(\app\admin\model\Admin::class, 'admin_id', 'id');
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\model;
|
||||
|
||||
use support\think\Model;
|
||||
|
||||
/**
|
||||
* GameConfig
|
||||
*/
|
||||
class GameConfig extends Model
|
||||
{
|
||||
// 表主键
|
||||
protected $pk = 'ID';
|
||||
|
||||
// 表名
|
||||
protected $name = 'game_config';
|
||||
|
||||
// 自动写入时间戳字段
|
||||
protected $autoWriteTimestamp = true;
|
||||
|
||||
// 字段类型转换
|
||||
protected $type = [
|
||||
'create_time' => 'integer',
|
||||
'update_time' => 'integer',
|
||||
];
|
||||
|
||||
|
||||
public function channel(): \think\model\relation\BelongsTo
|
||||
{
|
||||
return $this->belongsTo(\app\common\model\GameChannel::class, 'channel_id', 'id');
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\model;
|
||||
|
||||
use support\think\Model;
|
||||
|
||||
/**
|
||||
* GameRewardConfig
|
||||
*/
|
||||
class GameRewardConfig extends Model
|
||||
{
|
||||
// 表名
|
||||
protected $name = 'game_reward_config';
|
||||
|
||||
// 自动写入时间戳字段
|
||||
protected $autoWriteTimestamp = true;
|
||||
|
||||
// 字段类型转换(避免时间戳被写成 'now' 字符串)
|
||||
protected $type = [
|
||||
'create_time' => 'integer',
|
||||
'update_time' => 'integer',
|
||||
];
|
||||
|
||||
|
||||
public function gameChannel(): \think\model\relation\BelongsTo
|
||||
{
|
||||
return $this->belongsTo(\app\common\model\GameChannel::class, 'game_channel_id', 'id');
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\model;
|
||||
|
||||
use support\think\Model;
|
||||
|
||||
/**
|
||||
* GameRewardWeight
|
||||
*/
|
||||
class GameRewardWeight extends Model
|
||||
{
|
||||
// 表名
|
||||
protected $name = 'game_reward_weight';
|
||||
|
||||
// 自动写入时间戳字段
|
||||
protected $autoWriteTimestamp = true;
|
||||
|
||||
// 字段类型转换
|
||||
protected $type = [
|
||||
'create_time' => 'integer',
|
||||
'update_time' => 'integer',
|
||||
];
|
||||
|
||||
|
||||
public function gameChannel(): \think\model\relation\BelongsTo
|
||||
{
|
||||
return $this->belongsTo(\app\common\model\GameChannel::class, 'game_channel_id', 'id');
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\model;
|
||||
|
||||
use support\think\Model;
|
||||
|
||||
/**
|
||||
* GameUser
|
||||
*/
|
||||
class GameUser extends Model
|
||||
{
|
||||
// 表名
|
||||
protected $name = 'game_user';
|
||||
|
||||
// 自动写入时间戳字段
|
||||
protected $autoWriteTimestamp = true;
|
||||
|
||||
// 字段类型转换
|
||||
protected $type = [
|
||||
'create_time' => 'integer',
|
||||
'update_time' => 'integer',
|
||||
];
|
||||
|
||||
|
||||
public function getcoinAttr($value): ?float
|
||||
{
|
||||
return is_null($value) ? null : (float)$value;
|
||||
}
|
||||
|
||||
public function gameChannel(): \think\model\relation\BelongsTo
|
||||
{
|
||||
return $this->belongsTo(\app\common\model\GameChannel::class, 'game_channel_id', 'id');
|
||||
}
|
||||
|
||||
public function admin(): \think\model\relation\BelongsTo
|
||||
{
|
||||
return $this->belongsTo(\app\admin\model\Admin::class, 'admin_id', 'id');
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\common\service;
|
||||
|
||||
use support\think\Db;
|
||||
|
||||
/**
|
||||
* 按 game_user 表统计各渠道用户数,回写 game_channel.user_count
|
||||
*/
|
||||
class GameChannelUserCount
|
||||
{
|
||||
/**
|
||||
* 统计 game_user.game_channel_id = 该渠道 id 的行数,更新 game_channel.user_count
|
||||
*/
|
||||
public static function syncFromGameUser(int|string|null $channelId): void
|
||||
{
|
||||
if ($channelId === null || $channelId === '') {
|
||||
return;
|
||||
}
|
||||
if (is_numeric($channelId) && (float) $channelId < 1) {
|
||||
return;
|
||||
}
|
||||
$count = Db::name('game_user')->where('game_channel_id', $channelId)->count();
|
||||
Db::name('game_channel')->where('id', $channelId)->update(['user_count' => $count]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<int|string|null> $channelIds
|
||||
*/
|
||||
public static function syncChannels(array $channelIds): void
|
||||
{
|
||||
$seen = [];
|
||||
foreach ($channelIds as $cid) {
|
||||
if ($cid === null || $cid === '') {
|
||||
continue;
|
||||
}
|
||||
$k = (string) $cid;
|
||||
if (isset($seen[$k])) {
|
||||
continue;
|
||||
}
|
||||
$seen[$k] = true;
|
||||
self::syncFromGameUser($cid);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
class GameChannel extends Validate
|
||||
{
|
||||
protected $failException = true;
|
||||
|
||||
/**
|
||||
* 验证规则
|
||||
*/
|
||||
protected $rule = [
|
||||
];
|
||||
|
||||
/**
|
||||
* 提示消息
|
||||
*/
|
||||
protected $message = [
|
||||
];
|
||||
|
||||
/**
|
||||
* 验证场景
|
||||
*/
|
||||
protected $scene = [
|
||||
'add' => [],
|
||||
'edit' => [],
|
||||
];
|
||||
|
||||
}
|
||||
@@ -1,145 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
class GameRewardConfig extends Validate
|
||||
{
|
||||
protected $failException = true;
|
||||
|
||||
/**
|
||||
* 验证规则
|
||||
*/
|
||||
protected $rule = [
|
||||
'game_channel_id' => 'require|integer|gt:0',
|
||||
'tier_reward_form' => 'require|checkTierRewardForm',
|
||||
'bigwin_form' => 'require|checkBigwinForm',
|
||||
];
|
||||
|
||||
/**
|
||||
* 提示消息
|
||||
*/
|
||||
protected $message = [
|
||||
'game_channel_id.require' => '请选择渠道',
|
||||
'game_channel_id.integer' => '渠道参数格式错误',
|
||||
'game_channel_id.gt' => '渠道参数格式错误',
|
||||
'tier_reward_form.require' => '档位奖励表单不能为空',
|
||||
'bigwin_form.require' => '超级大奖表单不能为空',
|
||||
];
|
||||
|
||||
/**
|
||||
* 验证场景
|
||||
*/
|
||||
protected $scene = [
|
||||
'add' => ['game_channel_id', 'tier_reward_form', 'bigwin_form'],
|
||||
'edit' => ['game_channel_id', 'tier_reward_form', 'bigwin_form'],
|
||||
/** 渠道表单页:渠道由后端固定,仅校验两份 JSON */
|
||||
'channel_form' => ['tier_reward_form', 'bigwin_form'],
|
||||
];
|
||||
|
||||
private function parseJsonArray(mixed $value, string $label): array|string
|
||||
{
|
||||
if (!is_string($value) || trim($value) === '') {
|
||||
return $label . '不能为空';
|
||||
}
|
||||
|
||||
try {
|
||||
$decoded = json_decode($value, true, 512, JSON_THROW_ON_ERROR);
|
||||
} catch (\Throwable $e) {
|
||||
return $label . '必须为合法 JSON';
|
||||
}
|
||||
|
||||
if (!is_array($decoded)) {
|
||||
return $label . '格式错误';
|
||||
}
|
||||
return $decoded;
|
||||
}
|
||||
|
||||
protected function checkTierRewardForm($value, $rule, array $data = []): bool|string
|
||||
{
|
||||
$decoded = $this->parseJsonArray($value, '档位奖励表单');
|
||||
if (!is_array($decoded)) {
|
||||
return $decoded;
|
||||
}
|
||||
|
||||
$expectedGrid = range(5, 30);
|
||||
if (count($decoded) !== 26) {
|
||||
return '档位奖励表单必须固定 26 条';
|
||||
}
|
||||
$allowTiers = ['T1', 'T2', 'T3', 'T4', 'T5'];
|
||||
foreach ($decoded as $idx => $row) {
|
||||
$rowNo = $idx + 1;
|
||||
if (!is_array($row)) {
|
||||
return '档位奖励表单第' . $rowNo . '条格式错误';
|
||||
}
|
||||
$gridNumber = $row['grid_number'] ?? null;
|
||||
$uiText = $row['ui_text'] ?? null;
|
||||
$realEv = $row['real_ev'] ?? null;
|
||||
$tier = $row['tier'] ?? null;
|
||||
|
||||
if (!is_numeric($gridNumber)) {
|
||||
return '档位奖励表单第' . $rowNo . '条点数必须为数字';
|
||||
}
|
||||
$gridInt = intval(strval($gridNumber));
|
||||
if ($gridInt !== $expectedGrid[$idx]) {
|
||||
return '档位奖励表单点数必须固定为 5-30 且不可修改';
|
||||
}
|
||||
if (!is_string($uiText) || trim($uiText) === '') {
|
||||
return '档位奖励表单第' . $rowNo . '条显示文本不能为空';
|
||||
}
|
||||
if ($realEv === null || $realEv === '' || !is_numeric($realEv)) {
|
||||
return '档位奖励表单第' . $rowNo . '条实际中奖必须为数字';
|
||||
}
|
||||
if (!is_string($tier) || !in_array($tier, $allowTiers, true)) {
|
||||
return '档位奖励表单第' . $rowNo . '条档位只能是 T1-T5';
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function checkBigwinForm($value, $rule, array $data = []): bool|string
|
||||
{
|
||||
$decoded = $this->parseJsonArray($value, '超级大奖表单');
|
||||
if (!is_array($decoded)) {
|
||||
return $decoded;
|
||||
}
|
||||
|
||||
$expectedGrid = [5, 10, 15, 20, 25, 30];
|
||||
if (count($decoded) !== 6) {
|
||||
return '超级大奖表单必须固定 6 条';
|
||||
}
|
||||
foreach ($decoded as $idx => $row) {
|
||||
$rowNo = $idx + 1;
|
||||
if (!is_array($row)) {
|
||||
return '超级大奖表单第' . $rowNo . '条格式错误';
|
||||
}
|
||||
|
||||
$gridNumber = $row['grid_number'] ?? null;
|
||||
$uiText = $row['ui_text'] ?? null;
|
||||
$realEv = $row['real_ev'] ?? null;
|
||||
$tier = $row['tier'] ?? null;
|
||||
|
||||
if (!is_numeric($gridNumber)) {
|
||||
return '超级大奖表单第' . $rowNo . '条点数必须为数字';
|
||||
}
|
||||
$gridInt = intval(strval($gridNumber));
|
||||
if ($gridInt !== $expectedGrid[$idx]) {
|
||||
return '超级大奖表单点数必须固定为 5、10、15、20、25、30 且不可修改';
|
||||
}
|
||||
|
||||
if (!is_string($uiText) || trim($uiText) === '') {
|
||||
return '超级大奖表单第' . $rowNo . '条显示文本不能为空';
|
||||
}
|
||||
if ($realEv === null || $realEv === '' || !is_numeric($realEv)) {
|
||||
return '超级大奖表单第' . $rowNo . '条实际中奖必须为数字';
|
||||
}
|
||||
if (!is_string($tier) || $tier !== 'BIGWIN') {
|
||||
return '超级大奖表单第' . $rowNo . '条档位必须为 BIGWIN';
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
class GameRewardWeight extends Validate
|
||||
{
|
||||
protected $failException = true;
|
||||
|
||||
/**
|
||||
* 验证规则
|
||||
*/
|
||||
protected $rule = [
|
||||
];
|
||||
|
||||
/**
|
||||
* 提示消息
|
||||
*/
|
||||
protected $message = [
|
||||
];
|
||||
|
||||
/**
|
||||
* 验证场景
|
||||
*/
|
||||
protected $scene = [
|
||||
'add' => [],
|
||||
'edit' => [],
|
||||
];
|
||||
|
||||
}
|
||||
@@ -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' => [],
|
||||
];
|
||||
|
||||
}
|
||||
@@ -188,8 +188,7 @@ if (!function_exists('get_controller_path')) {
|
||||
$parts = explode('\\', $relative);
|
||||
$path = [];
|
||||
foreach ($parts as $p) {
|
||||
// 与 BuildAdmin admin_rule.name 一致:多段类名用 camelCase(如 auth/adminLog),不用 admin_log
|
||||
$path[] = lcfirst($p);
|
||||
$path[] = strtolower(preg_replace('/([a-z])([A-Z])/', '$1_$2', $p));
|
||||
}
|
||||
return implode('/', $path);
|
||||
}
|
||||
@@ -205,24 +204,7 @@ if (!function_exists('get_controller_path')) {
|
||||
if (count($parts) < 2) {
|
||||
return $parts[0] ?? null;
|
||||
}
|
||||
$segments = array_slice($parts, 1, -1);
|
||||
if ($segments === []) {
|
||||
return $parts[1] ?? null;
|
||||
}
|
||||
// ThinkPHP 风格段 game.Config -> game/config,与 $request->controller 解析结果一致(否则权限节点对不上)
|
||||
$normalized = [];
|
||||
foreach ($segments as $seg) {
|
||||
if (str_contains($seg, '.')) {
|
||||
$dotPos = strpos($seg, '.');
|
||||
$mod = substr($seg, 0, $dotPos);
|
||||
$ctrl = substr($seg, $dotPos + 1);
|
||||
$normalized[] = lcfirst($mod);
|
||||
$normalized[] = lcfirst($ctrl);
|
||||
} else {
|
||||
$normalized[] = lcfirst($seg);
|
||||
}
|
||||
}
|
||||
return implode('/', $normalized);
|
||||
return implode('/', array_slice($parts, 1, -1)) ?: $parts[1];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
|
||||
return [
|
||||
// 允许跨域访问的域名(* 表示任意;开发可用 *,生产建议填具体域名)
|
||||
'cors_request_domain' => '*',
|
||||
'cors_request_domain' => '*,test.zhenhui666.top',
|
||||
// 是否开启会员登录验证码
|
||||
'user_login_captcha' => false,
|
||||
'user_login_captcha' => true,
|
||||
// 是否开启管理员登录验证码
|
||||
'admin_login_captcha' => false,
|
||||
'admin_login_captcha' => true,
|
||||
// 会员登录失败可重试次数,false则无限
|
||||
'user_login_retry' => 10,
|
||||
// 管理员登录失败可重试次数,false则无限
|
||||
@@ -44,7 +44,7 @@ return [
|
||||
// 默认驱动方式
|
||||
'default' => 'mysql',
|
||||
// 加密key
|
||||
'key' => 'u4w3NzEr5QTv2ygjYOoMVZ6snKAePxJp',
|
||||
'key' => 'L1iYVS0PChKA9pjcFdmOGb4zfDIHo5xw',
|
||||
// 加密方式
|
||||
'algo' => 'ripemd160',
|
||||
// 驱动
|
||||
|
||||
@@ -169,9 +169,8 @@ Route::post('/admin/auth/rule/edit', [\app\admin\controller\auth\Rule::class, 'e
|
||||
Route::post('/admin/auth/rule/del', [\app\admin\controller\auth\Rule::class, 'del']);
|
||||
Route::get('/admin/auth/rule/select', [\app\admin\controller\auth\Rule::class, 'select']);
|
||||
|
||||
// admin/auth/adminLog(兼容 ThinkPHP 风格 /admin/auth.AdminLog/index)
|
||||
// admin/auth/adminLog
|
||||
Route::get('/admin/auth/adminLog/index', [\app\admin\controller\auth\AdminLog::class, 'index']);
|
||||
Route::get('/admin/auth.AdminLog/index', [\app\admin\controller\auth\AdminLog::class, 'index']);
|
||||
|
||||
// admin/user/user
|
||||
Route::get('/admin/user/user/index', [\app\admin\controller\user\User::class, 'index']);
|
||||
@@ -246,21 +245,11 @@ Route::get('/admin/security/dataRecycleLog/index', [\app\admin\controller\securi
|
||||
Route::post('/admin/security/dataRecycleLog/restore', [\app\admin\controller\security\DataRecycleLog::class, 'restore']);
|
||||
Route::get('/admin/security/dataRecycleLog/info', [\app\admin\controller\security\DataRecycleLog::class, 'info']);
|
||||
|
||||
// ==================== 显式路由(优先级高,避免动态路由误匹配) ====================
|
||||
Route::add(['GET', 'POST', 'HEAD'], '/admin/game.User/defaultWeightPresets', [\app\admin\controller\game\User::class, 'defaultWeightPresets']);
|
||||
Route::add(['GET', 'POST', 'HEAD'], '/admin/game.User/defaultWeightByChannel', [\app\admin\controller\game\User::class, 'defaultWeightByChannel']);
|
||||
Route::add(['GET', 'POST', 'HEAD'], '/admin/game.user/defaultWeightPresets', [\app\admin\controller\game\User::class, 'defaultWeightPresets']);
|
||||
Route::add(['GET', 'POST', 'HEAD'], '/admin/game.user/defaultWeightByChannel', [\app\admin\controller\game\User::class, 'defaultWeightByChannel']);
|
||||
|
||||
// 游戏渠道:删除前统计关联数据(显式路由,避免动态路由 404)
|
||||
Route::add(['GET', 'POST', 'HEAD'], '/admin/game.Channel/deleteRelatedCounts', [\app\admin\controller\game\Channel::class, 'deleteRelatedCounts']);
|
||||
Route::add(['GET', 'POST', 'HEAD'], '/admin/game.channel/deleteRelatedCounts', [\app\admin\controller\game\Channel::class, 'deleteRelatedCounts']);
|
||||
|
||||
// ==================== 兼容 ThinkPHP 风格 URL(module.Controller/action) ====================
|
||||
// 前端使用 /admin/user.Rule/index 格式,需转换为控制器调用
|
||||
Route::add(
|
||||
['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD'],
|
||||
'/admin/{controllerPart:[a-zA-Z]+\\.[a-zA-Z0-9]+}/{action:[^/]+}',
|
||||
'/admin/{controllerPart:[a-zA-Z]+\\.[a-zA-Z0-9]+}/{action}',
|
||||
function (\Webman\Http\Request $request, string $controllerPart, string $action) {
|
||||
$pos = strpos($controllerPart, '.');
|
||||
if ($pos === false) {
|
||||
@@ -268,21 +257,8 @@ Route::add(
|
||||
}
|
||||
$module = substr($controllerPart, 0, $pos);
|
||||
$controller = substr($controllerPart, $pos + 1);
|
||||
// game.user / game.User 等:小写控制器名需解析为 User.php(PSR-4 类名 StudlyCase)
|
||||
$class = null;
|
||||
$candidates = array_unique([
|
||||
$controller,
|
||||
ucfirst($controller),
|
||||
ucfirst(strtolower($controller)),
|
||||
]);
|
||||
foreach ($candidates as $base) {
|
||||
$tryClass = '\\app\\admin\\controller\\' . strtolower($module) . '\\' . $base;
|
||||
if (class_exists($tryClass)) {
|
||||
$class = $tryClass;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($class === null) {
|
||||
$class = '\\app\\admin\\controller\\' . strtolower($module) . '\\' . $controller;
|
||||
if (!class_exists($class)) {
|
||||
return new Response(404, ['Content-Type' => 'application/json'], json_encode(['code' => 404, 'msg' => '404 Not Found', 'data' => []], JSON_UNESCAPED_UNICODE));
|
||||
}
|
||||
if (!method_exists($class, $action)) {
|
||||
@@ -304,26 +280,6 @@ Route::add(
|
||||
}
|
||||
);
|
||||
|
||||
// 兜底:closure 显式调用,避免路由绑定在某些情况下仍落到 404
|
||||
Route::add(['GET', 'POST', 'HEAD'], '/admin/game.User/defaultWeightByChannel/', function (\Webman\Http\Request $request) {
|
||||
return (new \app\admin\controller\game\User())->defaultWeightByChannel($request);
|
||||
});
|
||||
Route::add(['GET', 'POST', 'HEAD'], '/admin/game.user/defaultWeightByChannel/', function (\Webman\Http\Request $request) {
|
||||
return (new \app\admin\controller\game\User())->defaultWeightByChannel($request);
|
||||
});
|
||||
Route::add(['GET', 'POST', 'HEAD'], '/admin/game.User/defaultWeightPresets/', function (\Webman\Http\Request $request) {
|
||||
return (new \app\admin\controller\game\User())->defaultWeightPresets($request);
|
||||
});
|
||||
Route::add(['GET', 'POST', 'HEAD'], '/admin/game.user/defaultWeightPresets/', function (\Webman\Http\Request $request) {
|
||||
return (new \app\admin\controller\game\User())->defaultWeightPresets($request);
|
||||
});
|
||||
Route::add(['GET', 'POST', 'HEAD'], '/admin/game.Channel/deleteRelatedCounts/', function (\Webman\Http\Request $request) {
|
||||
return (new \app\admin\controller\game\Channel())->deleteRelatedCounts($request);
|
||||
});
|
||||
Route::add(['GET', 'POST', 'HEAD'], '/admin/game.channel/deleteRelatedCounts/', function (\Webman\Http\Request $request) {
|
||||
return (new \app\admin\controller\game\Channel())->deleteRelatedCounts($request);
|
||||
});
|
||||
|
||||
// ==================== CORS 预检(OPTIONS) ====================
|
||||
// 放在最后注册;显式加上前端会请求的路径,再加固通配
|
||||
Route::add('OPTIONS', '/api/index/index', [\app\common\middleware\AllowCrossDomain::class, 'optionsResponse']);
|
||||
|
||||
@@ -42,11 +42,11 @@ return [
|
||||
// 服务器地址
|
||||
'hostname' => $env('database.hostname', '127.0.0.1'),
|
||||
// 数据库名(与 database.php / .env 一致)
|
||||
'database' => $env('database.database', 'webman-buildadmin-dafuweng'),
|
||||
'database' => $env('database.database', 'buildadmin-webman'),
|
||||
// 用户名(与 .env DATABASE_USERNAME 一致,默认勿用 root 以免与本机 MySQL 不符)
|
||||
'username' => $env('database.username', 'webman-buildadmin-dafuweng'),
|
||||
'username' => $env('database.username', 'buildadmin-webman'),
|
||||
// 密码(与 .env DATABASE_PASSWORD 一致)
|
||||
'password' => $env('database.password', '6dzMaX32Xdsc4DjS'),
|
||||
'password' => $env('database.password', '123456'),
|
||||
// 端口
|
||||
'hostport' => $env('database.hostport', '3306'),
|
||||
// 数据库连接参数(MYSQL_ATTR_USE_BUFFERED_QUERY 避免 "Cannot execute queries while other unbuffered queries are active")
|
||||
|
||||
@@ -227,8 +227,6 @@ class Install extends AbstractMigration
|
||||
->addColumn('extend', 'string', ['limit' => 255, 'default' => '', 'comment' => '扩展属性', 'null' => false])
|
||||
->addColumn('allow_del', 'integer', ['signed' => false, 'limit' => MysqlAdapter::INT_TINY, 'default' => 0, 'comment' => '允许删除:0=否,1=是', 'null' => false])
|
||||
->addColumn('weigh', 'integer', ['comment' => '权重', 'default' => 0, 'null' => false])
|
||||
->addColumn('update_time', 'biginteger', ['limit' => 16, 'signed' => false, 'null' => true, 'default' => null, 'comment' => '更新时间'])
|
||||
->addColumn('create_time', 'biginteger', ['limit' => 16, 'signed' => false, 'null' => true, 'default' => null, 'comment' => '创建时间'])
|
||||
->addIndex(['name'], [
|
||||
'unique' => true,
|
||||
])
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* config 表在初始 install 中未含 create_time/update_time,而 Config 模型开启自动时间戳,
|
||||
* Version205 等迁移使用 Model::save() 会生成对 update_time 的 UPDATE,导致 1054 错误。
|
||||
* 本迁移在 Version205 之前执行,补齐字段。
|
||||
*/
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
class AddConfigTimestamps extends AbstractMigration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
if (!$this->hasTable('config')) {
|
||||
return;
|
||||
}
|
||||
$config = $this->table('config');
|
||||
if (!$config->hasColumn('update_time')) {
|
||||
$config->addColumn('update_time', 'biginteger', [
|
||||
'limit' => 16,
|
||||
'signed' => false,
|
||||
'null' => true,
|
||||
'default' => null,
|
||||
'comment' => '更新时间',
|
||||
'after' => 'weigh',
|
||||
])->save();
|
||||
}
|
||||
$config = $this->table('config');
|
||||
if (!$config->hasColumn('create_time')) {
|
||||
$config->addColumn('create_time', 'biginteger', [
|
||||
'limit' => 16,
|
||||
'signed' => false,
|
||||
'null' => true,
|
||||
'default' => null,
|
||||
'comment' => '创建时间',
|
||||
'after' => 'update_time',
|
||||
])->save();
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
if (!$this->hasTable('config')) {
|
||||
return;
|
||||
}
|
||||
$config = $this->table('config');
|
||||
if ($config->hasColumn('create_time')) {
|
||||
$config->removeColumn('create_time')->save();
|
||||
}
|
||||
$config = $this->table('config');
|
||||
if ($config->hasColumn('update_time')) {
|
||||
$config->removeColumn('update_time')->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,40 +0,0 @@
|
||||
# BuildAdmin Webman - Nginx 反向代理示例
|
||||
# 将 server_name 和 root 改为实际值后,放入 nginx 的 conf.d 或 sites-available
|
||||
|
||||
upstream webman {
|
||||
server 127.0.0.1:8787;
|
||||
keepalive 10240;
|
||||
}
|
||||
|
||||
server {
|
||||
server_name 你的域名;
|
||||
listen 80;
|
||||
access_log off;
|
||||
root /path/to/dafuweng-webman/public;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ @proxy;
|
||||
}
|
||||
|
||||
location @proxy {
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Connection "";
|
||||
proxy_pass http://webman;
|
||||
}
|
||||
|
||||
location ~ \.php$ {
|
||||
return 404;
|
||||
}
|
||||
|
||||
location ~ ^/\.well-known/ {
|
||||
allow all;
|
||||
}
|
||||
|
||||
location ~ /\. {
|
||||
return 404;
|
||||
}
|
||||
}
|
||||
@@ -42,27 +42,8 @@ if (!function_exists('env')) {
|
||||
require $baseDir . '/vendor/workerman/webman-framework/src/support/helpers.php';
|
||||
require $baseDir . '/app/functions.php';
|
||||
|
||||
use Webman\Config;
|
||||
|
||||
Config::clear();
|
||||
Config::load($baseDir . '/config', ['route', 'middleware', 'process', 'server', 'static']);
|
||||
// 与 Webman\ThinkOrm\ThinkOrm::start() 一致,并与 phinx.php 中 $thinkorm 来源一致
|
||||
$thinkorm = array_replace_recursive(config('thinkorm', []), config('think-orm', []));
|
||||
Webman\Config::load($baseDir . '/config', ['route', 'middleware', 'process', 'server', 'static']);
|
||||
$thinkorm = config('thinkorm', []);
|
||||
if (!empty($thinkorm)) {
|
||||
support\think\Db::setConfig($thinkorm);
|
||||
// Webman DbManager 使用连接池且忽略 force;安装向导在同进程内迁移时若不清理,会沿用旧前缀连接,
|
||||
// 导致 Phinx 已建带前缀表而 Db::name() 仍查无前缀表(如 menu_rule 不存在)。
|
||||
if (class_exists(\Webman\ThinkOrm\DbManager::class)) {
|
||||
$ref = new \ReflectionClass(\Webman\ThinkOrm\DbManager::class);
|
||||
if ($ref->hasProperty('pools')) {
|
||||
$poolsProp = $ref->getProperty('pools');
|
||||
$poolsProp->setAccessible(true);
|
||||
$poolsProp->setValue(null, []);
|
||||
}
|
||||
}
|
||||
if (class_exists(\Webman\Context::class)) {
|
||||
foreach (array_keys($thinkorm['connections'] ?? []) as $connName) {
|
||||
\Webman\Context::set('think-orm.connections.' . $connName, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
15
phinx.php
15
phinx.php
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
/**
|
||||
* Phinx 数据库迁移配置
|
||||
* 与 Webman ThinkOrm 引导一致:Config 加载后合并 thinkorm + think-orm,供 phinx migrate 使用
|
||||
* 从 config/thinkorm.php 读取数据库连接,用于 php vendor/bin/phinx migrate
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
@@ -36,18 +36,7 @@ if (!function_exists('env')) {
|
||||
}
|
||||
}
|
||||
|
||||
if (!defined('BASE_PATH')) {
|
||||
define('BASE_PATH', $baseDir);
|
||||
}
|
||||
|
||||
require $baseDir . '/vendor/workerman/webman-framework/src/support/helpers.php';
|
||||
require $baseDir . '/app/functions.php';
|
||||
|
||||
use Webman\Config;
|
||||
|
||||
Config::clear();
|
||||
Config::load($baseDir . '/config', ['route', 'middleware', 'process', 'server', 'static']);
|
||||
$thinkorm = array_replace_recursive(config('thinkorm', []), config('think-orm', []));
|
||||
$thinkorm = require $baseDir . '/config/thinkorm.php';
|
||||
$conn = $thinkorm['connections'][$thinkorm['default'] ?? 'mysql'] ?? [];
|
||||
$prefix = $conn['prefix'] ?? '';
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
install-end
|
||||
2026-03-18 11:17:18
|
||||
@@ -11,98 +11,6 @@
|
||||
fetch('/api/install/accessUrls').then(function(r){return r.json();}).then(function(res){
|
||||
if (res && res.data) { urls.adminUrl = res.data.adminUrl || ''; urls.frontUrl = res.data.frontUrl || ''; }
|
||||
}).catch(function(){});
|
||||
function ensureQuickPanel() {
|
||||
if (!urls.adminUrl && !urls.frontUrl) return;
|
||||
if (document.getElementById('__ba_install_quick_urls__')) return;
|
||||
var wrap = document.createElement('div');
|
||||
wrap.id = '__ba_install_quick_urls__';
|
||||
wrap.style.position = 'fixed';
|
||||
wrap.style.right = '16px';
|
||||
wrap.style.bottom = '16px';
|
||||
wrap.style.zIndex = '99999';
|
||||
wrap.style.maxWidth = '560px';
|
||||
wrap.style.fontFamily = 'ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji", "Segoe UI Emoji"';
|
||||
wrap.innerHTML =
|
||||
'<div style="background:rgba(255,255,255,.96);border:1px solid rgba(0,0,0,.08);box-shadow:0 8px 24px rgba(0,0,0,.12);border-radius:12px;overflow:hidden">' +
|
||||
'<div style="padding:10px 12px;border-bottom:1px solid rgba(0,0,0,.06);display:flex;gap:8px;align-items:center;justify-content:space-between">' +
|
||||
'<div style="font-weight:600;color:#111">安装完成快捷入口</div>' +
|
||||
'<button type="button" aria-label="close" style="border:0;background:transparent;cursor:pointer;font-size:16px;line-height:16px;color:#666;padding:4px 6px">×</button>' +
|
||||
'</div>' +
|
||||
'<div style="padding:12px;display:flex;flex-direction:column;gap:10px">' +
|
||||
'<div>' +
|
||||
'<div style="font-size:12px;color:#666;margin-bottom:6px">后台地址</div>' +
|
||||
'<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">' +
|
||||
'<a data-k="admin" target="_blank" rel="noreferrer" style="color:#1677ff;text-decoration:none;word-break:break-all"></a>' +
|
||||
'<button data-copy="admin" type="button" style="border:1px solid rgba(0,0,0,.12);background:#fff;border-radius:8px;padding:6px 10px;cursor:pointer">复制</button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div>' +
|
||||
'<div style="font-size:12px;color:#666;margin-bottom:6px">前台地址</div>' +
|
||||
'<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">' +
|
||||
'<a data-k="front" target="_blank" rel="noreferrer" style="color:#1677ff;text-decoration:none;word-break:break-all"></a>' +
|
||||
'<button data-copy="front" type="button" style="border:1px solid rgba(0,0,0,.12);background:#fff;border-radius:8px;padding:6px 10px;cursor:pointer">复制</button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div data-msg style="font-size:12px;color:#52c41a;min-height:16px"></div>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
document.body.appendChild(wrap);
|
||||
|
||||
var closeBtn = wrap.querySelector('button[aria-label="close"]');
|
||||
if (closeBtn) closeBtn.addEventListener('click', function(){ wrap.remove(); });
|
||||
|
||||
function setLink(which, val) {
|
||||
var a = wrap.querySelector('a[data-k="' + which + '"]');
|
||||
if (!a) return;
|
||||
a.textContent = val || '';
|
||||
a.href = val || 'javascript:void(0)';
|
||||
}
|
||||
setLink('admin', urls.adminUrl);
|
||||
setLink('front', urls.frontUrl);
|
||||
|
||||
function showMsg(text, ok) {
|
||||
var el = wrap.querySelector('[data-msg]');
|
||||
if (!el) return;
|
||||
el.style.color = ok ? '#52c41a' : '#ff4d4f';
|
||||
el.textContent = text;
|
||||
window.clearTimeout(el.__t);
|
||||
el.__t = window.setTimeout(function(){ el.textContent = ''; }, 1800);
|
||||
}
|
||||
function copyText(text) {
|
||||
if (!text) return Promise.reject(new Error('empty'));
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
return navigator.clipboard.writeText(text);
|
||||
}
|
||||
return new Promise(function(resolve, reject){
|
||||
try {
|
||||
var ta = document.createElement('textarea');
|
||||
ta.value = text;
|
||||
ta.setAttribute('readonly', 'readonly');
|
||||
ta.style.position = 'fixed';
|
||||
ta.style.left = '-9999px';
|
||||
document.body.appendChild(ta);
|
||||
ta.select();
|
||||
var ok = document.execCommand('copy');
|
||||
document.body.removeChild(ta);
|
||||
ok ? resolve() : reject(new Error('copy failed'));
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
wrap.addEventListener('click', function(e){
|
||||
var t = e.target;
|
||||
if (!t || !t.getAttribute) return;
|
||||
var which = t.getAttribute('data-copy');
|
||||
if (!which) return;
|
||||
var text = which === 'admin' ? urls.adminUrl : urls.frontUrl;
|
||||
copyText(text).then(function(){
|
||||
showMsg('已复制:' + text, true);
|
||||
}).catch(function(){
|
||||
showMsg('复制失败,请手动复制', false);
|
||||
});
|
||||
});
|
||||
}
|
||||
function applyUrls() {
|
||||
if (!urls.adminUrl && !urls.frontUrl) return;
|
||||
document.querySelectorAll('input[type="text"], input:not([type])').forEach(function(inp){
|
||||
@@ -116,7 +24,6 @@
|
||||
document.querySelectorAll('a[href*="#/"]').forEach(function(a){
|
||||
if (urls.frontUrl && a.href.indexOf('#/admin') < 0) a.href = urls.frontUrl;
|
||||
});
|
||||
ensureQuickPanel();
|
||||
}
|
||||
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', function(){ setInterval(applyUrls, 800); });
|
||||
else setInterval(applyUrls, 800);
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
{
|
||||
"tier_reward_form": [
|
||||
{"grid_number": 5, "ui_text": "", "real_ev": "", "tier": "T1"},
|
||||
{"grid_number": 6, "ui_text": "", "real_ev": "", "tier": "T1"},
|
||||
{"grid_number": 7, "ui_text": "", "real_ev": "", "tier": "T1"},
|
||||
{"grid_number": 8, "ui_text": "", "real_ev": "", "tier": "T1"},
|
||||
{"grid_number": 9, "ui_text": "", "real_ev": "", "tier": "T1"},
|
||||
{"grid_number": 10, "ui_text": "", "real_ev": "", "tier": "T1"},
|
||||
{"grid_number": 11, "ui_text": "", "real_ev": "", "tier": "T1"},
|
||||
{"grid_number": 12, "ui_text": "", "real_ev": "", "tier": "T1"},
|
||||
{"grid_number": 13, "ui_text": "", "real_ev": "", "tier": "T1"},
|
||||
{"grid_number": 14, "ui_text": "", "real_ev": "", "tier": "T1"},
|
||||
{"grid_number": 15, "ui_text": "", "real_ev": "", "tier": "T1"},
|
||||
{"grid_number": 16, "ui_text": "", "real_ev": "", "tier": "T1"},
|
||||
{"grid_number": 17, "ui_text": "", "real_ev": "", "tier": "T1"},
|
||||
{"grid_number": 18, "ui_text": "", "real_ev": "", "tier": "T1"},
|
||||
{"grid_number": 19, "ui_text": "", "real_ev": "", "tier": "T1"},
|
||||
{"grid_number": 20, "ui_text": "", "real_ev": "", "tier": "T1"},
|
||||
{"grid_number": 21, "ui_text": "", "real_ev": "", "tier": "T1"},
|
||||
{"grid_number": 22, "ui_text": "", "real_ev": "", "tier": "T1"},
|
||||
{"grid_number": 23, "ui_text": "", "real_ev": "", "tier": "T1"},
|
||||
{"grid_number": 24, "ui_text": "", "real_ev": "", "tier": "T1"},
|
||||
{"grid_number": 25, "ui_text": "", "real_ev": "", "tier": "T1"},
|
||||
{"grid_number": 26, "ui_text": "", "real_ev": "", "tier": "T1"},
|
||||
{"grid_number": 27, "ui_text": "", "real_ev": "", "tier": "T1"},
|
||||
{"grid_number": 28, "ui_text": "", "real_ev": "", "tier": "T1"},
|
||||
{"grid_number": 29, "ui_text": "", "real_ev": "", "tier": "T1"},
|
||||
{"grid_number": 30, "ui_text": "", "real_ev": "", "tier": "T1"}
|
||||
],
|
||||
"bigwin_form": [
|
||||
{"grid_number": 5, "ui_text": "", "real_ev": "", "tier": "BIGWIN"},
|
||||
{"grid_number": 10, "ui_text": "", "real_ev": "", "tier": "BIGWIN"},
|
||||
{"grid_number": 15, "ui_text": "", "real_ev": "", "tier": "BIGWIN"},
|
||||
{"grid_number": 20, "ui_text": "", "real_ev": "", "tier": "BIGWIN"},
|
||||
{"grid_number": 25, "ui_text": "", "real_ev": "", "tier": "BIGWIN"},
|
||||
{"grid_number": 30, "ui_text": "", "real_ev": "", "tier": "BIGWIN"}
|
||||
]
|
||||
}
|
||||
@@ -8,4 +8,4 @@ VITE_BASE_PATH = '/'
|
||||
VITE_OUT_DIR = 'dist'
|
||||
|
||||
# 线上环境接口地址 - 'getCurrentDomain:表示获取当前域名'
|
||||
VITE_AXIOS_BASE_URL = 'getCurrentDomain'
|
||||
VITE_AXIOS_BASE_URL = 'https://test-api.zhenhui666.top'
|
||||
|
||||
@@ -11,6 +11,4 @@ export default {
|
||||
[adminBaseRoutePath + '/user/rule']: ['./backend/${lang}/auth/rule.ts'],
|
||||
[adminBaseRoutePath + '/user/scoreLog']: ['./backend/${lang}/user/moneyLog.ts'],
|
||||
[adminBaseRoutePath + '/crud/crud']: ['./backend/${lang}/crud/log.ts', './backend/${lang}/crud/state.ts'],
|
||||
// /admin/game/rewardConfig 会加载 rewardConfig.ts;页面标题等仍在 rewardConfigForm.ts(game.rewardConfigForm)
|
||||
[adminBaseRoutePath + '/game/rewardConfig']: ['./backend/${lang}/game/rewardConfigForm.ts'],
|
||||
}
|
||||
|
||||
@@ -5,5 +5,5 @@ export default {
|
||||
'Parent group': 'Superior group',
|
||||
'The parent group cannot be the group itself': 'The parent group cannot be the group itself',
|
||||
'Manage subordinate role groups here':
|
||||
'You can only manage role groups under your branch in the tree; peers, other branches and ancestors are out of scope. You can still only assign permission nodes you own.',
|
||||
'In managing a subordinate role group (excluding a peer role group), you have all the rights of a subordinate role group and additional rights',
|
||||
}
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
export default {
|
||||
delete_confirm_title: 'Delete channel',
|
||||
delete_confirm_related:
|
||||
'This will also delete {countConfig} game config row(s) and {countUser} game user row(s) under this channel. This cannot be undone. Continue?',
|
||||
id: 'id',
|
||||
code: 'code',
|
||||
name: 'name',
|
||||
user_count: 'user_count',
|
||||
profit_amount: 'profit_amount',
|
||||
status: 'status',
|
||||
'status 0': 'status 0',
|
||||
'status 1': 'status 1',
|
||||
remark: 'remark',
|
||||
admin_group_id: 'admin_group_id',
|
||||
admingroup__name: 'name',
|
||||
admin_id: 'admin_id',
|
||||
admin__username: 'username',
|
||||
create_time: 'create_time',
|
||||
update_time: 'update_time',
|
||||
'quick Search Fields': 'id,code,name',
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
export default {
|
||||
ID: 'ID',
|
||||
channel_id: 'channel_id',
|
||||
channel__name: 'name',
|
||||
group: 'group',
|
||||
name: 'name',
|
||||
title: 'title',
|
||||
value: 'value',
|
||||
'weight key': 'Key',
|
||||
'weight value': 'Value',
|
||||
'weight sum must 100': 'The sum of weights for default_tier_weight / default_kill_score_weight must equal 100',
|
||||
'weight sum max 100': 'The sum of weights for default_tier_weight / default_kill_score_weight must not exceed 100',
|
||||
'weight each max 100': 'Each weight value must not exceed 100',
|
||||
'weight each max 10000': 'Weight must not exceed 10000',
|
||||
'bigwin weight must integer': 'Each big win weight must be an integer',
|
||||
'name opt game_rule': 'game_rule',
|
||||
'name opt game_rule_en': 'game_rule_en',
|
||||
'name opt default_tier_weight': 'default_tier_weight',
|
||||
'name opt default_kill_score_weight': 'default_kill_score_weight',
|
||||
'name opt default_bigwin_weight': 'default_bigwin_weight',
|
||||
default_bigwin_weight_help:
|
||||
'Big win weight range is 0–10000: 0 means never, 10000 means 100%. Totals 5 and 30 are guaranteed leopard (豹子); fixed at 10000 and cannot be changed.',
|
||||
'bigwin weight each 0 10000': 'Each weight (except fixed keys) must be between 0 and 10000',
|
||||
'bigwin weight locked 5 30': 'Weights for totals 5 and 30 are fixed at 10000',
|
||||
'weight value numeric': 'Weight values must be valid numbers',
|
||||
sort: 'sort',
|
||||
instantiation: 'instantiation',
|
||||
'instantiation 0': '---',
|
||||
'instantiation 1': 'YES',
|
||||
create_time: 'create_time',
|
||||
update_time: 'update_time',
|
||||
'quick Search Fields': 'ID',
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
export default {
|
||||
id: 'id',
|
||||
game_channel_id: 'game_channel_id',
|
||||
gamechannel__name: 'name',
|
||||
tier_reward_form: 'tier_reward_form',
|
||||
bigwin_form: 'bigwin_form',
|
||||
create_time: 'create_time',
|
||||
update_time: 'update_time',
|
||||
grid_number: 'grid_number',
|
||||
ui_text: 'ui_text',
|
||||
ui_text_en: 'ui_text (EN)',
|
||||
remark: 'remark',
|
||||
real_ev: 'real_ev',
|
||||
tier: 'tier',
|
||||
tier_t1: 'T1',
|
||||
tier_t2: 'T2',
|
||||
tier_t3: 'T3',
|
||||
tier_t4: 'T4',
|
||||
tier_t5: 'T5',
|
||||
tier_bigwin: 'BIGWIN',
|
||||
tier_reward_form_help:
|
||||
'Fixed 26 rows (5-30), no add/delete. Editable: ui_text, ui_text_en, real_ev, tier, remark.',
|
||||
bigwin_form_help: 'Fixed 6 rows (5,10,15,20,25,30), no add/delete. Editable: ui_text, real_ev.',
|
||||
'quick Search Fields': 'id',
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
export default {
|
||||
title: 'Game reward config',
|
||||
intro:
|
||||
'Super admins can maintain the global default template (game_channel_id=0). New channels copy this reward config first; if no template row exists yet, the built-in JSON template is used. Channel admins can only edit their own channel.',
|
||||
super_scope_label: 'Scope',
|
||||
super_scope_template: 'Global default template',
|
||||
super_scope_channel: 'Specific channel',
|
||||
super_scope_hint: 'Pick a channel below, then click Refresh to load.',
|
||||
btn_add: 'Save',
|
||||
btn_reset: 'Reset',
|
||||
btn_gen_tier: 'Generate tier board',
|
||||
btn_gen_weight: 'Generate reward weight table',
|
||||
gen_tier_title: 'Generate reward index by rules',
|
||||
gen_tier_rule:
|
||||
'[Same logic as reward comparison]\n' +
|
||||
'• 26 cells id 0–25; grid_number 5–30 unique.\n' +
|
||||
'• Roll D: start_index = id where grid_number=D; CW end=(start+D)%26; CCW end=start−D, if <0 then +26.\n' +
|
||||
'• Comparison rows use D as dice points; tier/settlement/copy from landing cell.\n\n' +
|
||||
'[Leopard rolls]\n' +
|
||||
'For D in 5,10,15,20,25,30, CW/CCW landing tier cannot be T4/T5.\n\n' +
|
||||
'[Settlement vs tier]\n' +
|
||||
'<0→T4; 0–100→T3; 100–200→T2; >200→T1; T5 amount 0. Below you set per-tier amounts; T1–T4 zh/en display text = amount string; T5 fixed.\n\n' +
|
||||
'[Inputs]\n' +
|
||||
'Counts: T1/T4/T5 fixed per direction; T2 minimum per direction.',
|
||||
gen_tier_footer_hint: 'T2 is a lower bound; if generation fails, relax counts. You can still edit the table after.',
|
||||
gen_tier_cancel: 'Cancel',
|
||||
gen_tier_submit: 'Generate and save',
|
||||
gen_t1_label: 'T1 grand prize',
|
||||
gen_t1_fixed: 'Fixed count (CW/CCW)',
|
||||
gen_t2_label: 'T2 small profit / break-even',
|
||||
gen_t2_min: 'Minimum count',
|
||||
gen_t3_label: 'T3 commission',
|
||||
gen_t3_amt_only: 'Settlement amount',
|
||||
gen_t4_label: 'T4 penalty',
|
||||
gen_t4_fixed: 'Fixed count (CW/CCW)',
|
||||
gen_t5_label: 'T5 try again',
|
||||
gen_t5_fixed: 'Fixed count (CW/CCW)',
|
||||
gen_settlement: 'Settlement amount',
|
||||
gen_dir_cw: 'Clockwise',
|
||||
gen_dir_ccw: 'Counter-clockwise',
|
||||
gen_weight_confirm_title: 'Create reward comparison',
|
||||
gen_weight_confirm_body:
|
||||
'Rules: start_index = id of the cell whose grid_number equals roll D; CW end_index=(start_index+D)%26; CCW end_index = start_index−D if ≥0 else 26+start_index−D. Existing rows for this channel in game_reward_weight will be deleted, then 52 rows created (D=5..30 × two directions). Tier, settlement, display text and remark come from the landing cell in the tier table. Continue?',
|
||||
gen_weight_confirm_ok: 'Confirm',
|
||||
gen_weight_need_channel: 'Select a channel and refresh before generating weights.',
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
export default {
|
||||
id: 'id',
|
||||
game_channel_id: 'game_channel_id',
|
||||
gamechannel__name: 'name',
|
||||
direction: 'direction',
|
||||
'direction 0': 'direction 0',
|
||||
'direction 1': 'direction 1',
|
||||
grid_number: 'grid_number',
|
||||
start_index: 'start_index',
|
||||
end_index: 'end_index',
|
||||
ui_text: 'ui_text',
|
||||
real_ev: 'real_ev',
|
||||
tier: 'tier',
|
||||
type: 'type',
|
||||
'type -2': 'type -2',
|
||||
'type -1': 'type -1',
|
||||
'type 0': 'type 0',
|
||||
'type 1': 'type 1',
|
||||
'type 2': 'type 2',
|
||||
'type 3': 'type 3',
|
||||
remark: 'remark',
|
||||
weight: 'weight',
|
||||
create_time: 'create_time',
|
||||
update_time: 'update_time',
|
||||
'quick Search Fields': 'id',
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
export default {
|
||||
id: 'id',
|
||||
username: 'username',
|
||||
password: 'password',
|
||||
uuid: 'uuid',
|
||||
phone: 'phone',
|
||||
remark: 'remark',
|
||||
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 0': 'status 0',
|
||||
'status 1': 'status 1',
|
||||
game_channel_id: 'game_channel_id',
|
||||
gamechannel__name: 'name',
|
||||
admin_id: 'admin_id',
|
||||
admin__username: 'username',
|
||||
create_time: 'create_time',
|
||||
update_time: 'update_time',
|
||||
'quick Search Fields': 'id,username,phone',
|
||||
}
|
||||
9
web/src/lang/backend/en/mall/player.ts
Normal file
9
web/src/lang/backend/en/mall/player.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export default {
|
||||
id: 'id',
|
||||
username: 'username',
|
||||
password: 'password',
|
||||
create_time: 'create_time',
|
||||
update_time: 'update_time',
|
||||
score: 'score',
|
||||
quickSearchFields: 'id',
|
||||
}
|
||||
@@ -4,6 +4,5 @@ export default {
|
||||
jurisdiction: '权限',
|
||||
'Parent group': '上级分组',
|
||||
'The parent group cannot be the group itself': '上级分组不能是分组本身',
|
||||
'Manage subordinate role groups here':
|
||||
'在此仅可管理「角色组树」中您所在组之下的下级组;同级、其他分支与上级组不在管理范围内。分配权限时仍只能勾选您自身拥有的节点。',
|
||||
'Manage subordinate role groups here': '在此管理下级角色组(您拥有下级角色组的所有权限并且拥有额外的权限,不含同级)',
|
||||
}
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
export default {
|
||||
delete_confirm_title: '删除渠道',
|
||||
delete_confirm_related:
|
||||
'将同时删除该渠道下 {countConfig} 条游戏配置、{countUser} 条游戏用户数据,此操作不可恢复。确定删除所选渠道吗?',
|
||||
id: 'ID',
|
||||
code: '渠道标识',
|
||||
name: '渠道名',
|
||||
user_count: '用户数',
|
||||
profit_amount: '利润',
|
||||
status: '状态',
|
||||
'status 0': '禁用',
|
||||
'status 1': '启用',
|
||||
remark: '备注',
|
||||
admin_group_id: '管理角色组',
|
||||
admingroup__name: '组名',
|
||||
admin_id: '管理员',
|
||||
admin__username: '用户名',
|
||||
create_time: '创建时间',
|
||||
update_time: '修改时间',
|
||||
'quick Search Fields': 'ID、渠道标识、渠道名',
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
export default {
|
||||
ID: 'ID',
|
||||
channel_id: '渠道id',
|
||||
channel__name: '渠道名',
|
||||
group: '分组',
|
||||
name: '配置标识',
|
||||
title: '配置名称',
|
||||
value: '值',
|
||||
'weight key': '键',
|
||||
'weight value': '数值',
|
||||
'weight sum must 100': 'default_tier_weight / default_kill_score_weight 的权重之和必须等于 100',
|
||||
'weight sum max 100': 'default_tier_weight / default_kill_score_weight 的权重之和不能超过 100',
|
||||
'weight each max 100': '每项权重不能超过 100',
|
||||
'weight each max 10000': '权重不能超过10000',
|
||||
'bigwin weight must integer': '大奖权重每项必须为整数',
|
||||
'name opt game_rule': 'game_rule(游戏规则)',
|
||||
'name opt game_rule_en': 'game_rule_en(游戏规则英文)',
|
||||
'name opt default_tier_weight': 'default_tier_weight(默认档位权重)',
|
||||
'name opt default_kill_score_weight': 'default_kill_score_weight(默认击杀分权重)',
|
||||
'name opt default_bigwin_weight': 'default_bigwin_weight(默认大奖权重)',
|
||||
default_bigwin_weight_help:
|
||||
'大奖权重区间为 0~10000:0 表示不可能出现,10000 表示 100% 出现。点数 5 与 30 为豹子号必中组合,权重固定为 10000,不可修改。',
|
||||
'bigwin weight each 0 10000': '除固定项外,每项大奖权重须在 0~10000 之间',
|
||||
'bigwin weight locked 5 30': '点数 5 与 30 权重固定为 10000,不可修改',
|
||||
'weight value numeric': '权重值必须为有效数字',
|
||||
sort: '排序',
|
||||
instantiation: '实例化',
|
||||
'instantiation 0': '---',
|
||||
'instantiation 1': '需要',
|
||||
create_time: '创建时间',
|
||||
update_time: '更新时间',
|
||||
'quick Search Fields': 'ID',
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
export default {
|
||||
id: 'ID',
|
||||
game_channel_id: '渠道',
|
||||
gamechannel__name: '渠道名',
|
||||
tier_reward_form: '档位奖励表单',
|
||||
bigwin_form: '超级大奖表单',
|
||||
create_time: '创建时间',
|
||||
update_time: '更新时间',
|
||||
grid_number: '色子点数',
|
||||
ui_text: '显示文本',
|
||||
ui_text_en: '显示文本(en)',
|
||||
remark: '备注',
|
||||
real_ev: '实际中奖',
|
||||
tier: '档位',
|
||||
tier_t1: 'T1',
|
||||
tier_t2: 'T2',
|
||||
tier_t3: 'T3',
|
||||
tier_t4: 'T4',
|
||||
tier_t5: 'T5',
|
||||
tier_bigwin: 'BIGWIN',
|
||||
tier_reward_form_help:
|
||||
'固定 26 条(点数 5-30),不可新增或删除;可修改显示文本、英文显示、实际中奖、档位与备注(生成器会预填英文与备注)',
|
||||
bigwin_form_help: '固定 6 条(点数 5、10、15、20、25、30),不可新增或删除,仅可修改显示文本、实际中奖',
|
||||
'quick Search Fields': 'ID',
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
export default {
|
||||
title: '游戏奖励配置',
|
||||
intro:
|
||||
'超级管理员可维护「全渠道默认模板」(对应库中 game_channel_id=0)。新建渠道时,会优先复制该模板的档位奖励与超级大奖配置;若尚未保存过模板,则使用项目内置 JSON 模板。渠道管理员仅能编辑自己负责渠道的配置。',
|
||||
super_scope_label: '维护范围',
|
||||
super_scope_template: '全渠道默认模板',
|
||||
super_scope_channel: '指定渠道',
|
||||
super_scope_hint: '选择「指定渠道」后请在下拉框中选择具体渠道并点击刷新加载。',
|
||||
btn_add: '新增',
|
||||
btn_reset: '重置',
|
||||
btn_gen_tier: '生成游戏奖励配置',
|
||||
btn_gen_weight: '生成游戏奖励权重配置',
|
||||
gen_tier_title: '按规则生成奖励索引',
|
||||
gen_tier_rule:
|
||||
'【生成逻辑(与创建奖励对照一致)】\n' +
|
||||
'• 盘面 26 格按 id 升序为位置 0~25;每条配置的 grid_number 为 5~30 且不重复。\n' +
|
||||
'• 摇取点数 D(5~30):起点为「grid_number=D」所在格位的 id(start_index),顺时针落点 = (起点 + D) mod 26,逆时针落点 = 起点 − D(若小于 0 则 +26)。\n' +
|
||||
'• 对照表每条记录的「色子点数」列为 D;档位、真实结算、显示文案取自落点格位对应 id 的配置。\n\n' +
|
||||
'【豹子摇取点数】\n' +
|
||||
'摇取点数为 5、10、15、20、25、30 时,其顺/逆时针落点档位不能为 T4、T5。\n\n' +
|
||||
'【结算金额与档位】\n' +
|
||||
'结算金额 < 0 → T4;0 < 结算金额 < 100 → T3;100 < 结算金额 < 200 → T2;200 < 结算金额 → T1;T5 结算金额=0。\n' +
|
||||
'下方填写各档位统一结算金额标准;T1~T4 的中/英文显示文本将等于该金额字符串;T5 固定「再来一次」/「Once again」。\n\n' +
|
||||
'【本弹窗输入】\n' +
|
||||
'条数:T1/T4/T5 为顺时针与逆时针各自的固定条数;T2 为顺时针与逆时针各自「不少于」的条数。生成后仍可在主表中微调。',
|
||||
gen_tier_footer_hint:
|
||||
'T1/T4/T5 为精确条数,T2 为下限;生成失败时请放宽条数或稍后再试。生成后可在上方表格中继续修改。',
|
||||
gen_tier_cancel: '取消',
|
||||
gen_tier_submit: '生成并保存',
|
||||
gen_t1_label: 'T1 大奖',
|
||||
gen_t1_fixed: '固定条数(顺/逆)',
|
||||
gen_t2_label: 'T2 小赚/回本',
|
||||
gen_t2_min: '最少条数',
|
||||
gen_t3_label: 'T3 抽水',
|
||||
gen_t3_amt_only: '结算金额',
|
||||
gen_t4_label: 'T4 惩罚',
|
||||
gen_t4_fixed: '固定条数(顺/逆)',
|
||||
gen_t5_label: 'T5 再来一次',
|
||||
gen_t5_fixed: '固定条数(顺/逆)',
|
||||
gen_settlement: '结算金额',
|
||||
gen_dir_cw: '顺时针',
|
||||
gen_dir_ccw: '逆时针',
|
||||
gen_weight_confirm_title: '创建奖励对照',
|
||||
gen_weight_confirm_body:
|
||||
'按规则创建奖励对照:起始索引 start_index 为奖励配置中 grid_number 与摇取点数 D 相同的那一格的 id;顺时针 end_index=(start_index+摇取点数)%26;逆时针 end_index=start_index−摇取点数,若≥0 则取该值,否则 26+start_index−摇取点数。将先清空该渠道 game_reward_weight 表中现有数据,再为 5~30 共 26 个点数、顺/逆时针各生成一条(共 52 条)。档位、真实结算、显示文案、备注取自落点格位在档位表中的配置。是否继续?',
|
||||
gen_weight_confirm_ok: '确定创建',
|
||||
gen_weight_need_channel: '请先选择具体渠道并刷新后再生成权重对照。',
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
export default {
|
||||
id: 'ID',
|
||||
game_channel_id: '渠道',
|
||||
gamechannel__name: '渠道名',
|
||||
direction: '方向',
|
||||
'direction 0': '顺时针',
|
||||
'direction 1': '逆时针',
|
||||
grid_number: '点数',
|
||||
start_index: '起始索引',
|
||||
end_index: '结束索引',
|
||||
ui_text: '显示文本',
|
||||
real_ev: '实际中奖金额',
|
||||
tier: '档位',
|
||||
type: '奖励类型',
|
||||
'type -2': '唯一惩罚',
|
||||
'type -1': '抽水',
|
||||
'type 0': '回本',
|
||||
'type 1': '再来一次',
|
||||
'type 2': '小赚',
|
||||
'type 3': '大奖格',
|
||||
remark: '备注',
|
||||
weight: '权重',
|
||||
create_time: '创建时间',
|
||||
update_time: '修改时间',
|
||||
'quick Search Fields': 'ID',
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
export default {
|
||||
id: 'ID',
|
||||
username: '用户名',
|
||||
password: '密码',
|
||||
uuid: '用户唯一标识',
|
||||
phone: '手机号',
|
||||
remark: '备注',
|
||||
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 0': '禁用',
|
||||
'status 1': '启用',
|
||||
game_channel_id: '所属渠道',
|
||||
gamechannel__name: '渠道名',
|
||||
admin_id: '所属管理员',
|
||||
admin__username: '用户名',
|
||||
create_time: '创建时间',
|
||||
update_time: '修改时间',
|
||||
'quick Search Fields': 'ID、用户名、手机号',
|
||||
}
|
||||
9
web/src/lang/backend/zh-cn/mall/player.ts
Normal file
9
web/src/lang/backend/zh-cn/mall/player.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export default {
|
||||
id: 'ID',
|
||||
username: '用户名',
|
||||
password: '密码',
|
||||
create_time: '创建时间',
|
||||
update_time: '修改时间',
|
||||
score: '积分',
|
||||
quickSearchFields: 'ID',
|
||||
}
|
||||
@@ -19,8 +19,6 @@ const staticRoutes: Array<RouteRecordRaw> = [
|
||||
meta: {
|
||||
title: pageTitle('home'),
|
||||
},
|
||||
redirect: adminBaseRoutePath,
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
// 管理员登录页 - 不放在 adminBaseRoute.children 因为登录页不需要使用后台的布局
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
/** 档位权重固定键(与 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
|
||||
}
|
||||
@@ -1,185 +0,0 @@
|
||||
<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.channel.quick Search Fields') })"
|
||||
></TableHeader>
|
||||
|
||||
<!-- 表格 -->
|
||||
<!-- 表格列有多种自定义渲染方式,比如自定义组件、具名插槽等,参见文档 -->
|
||||
<!-- 要使用 el-table 组件原有的属性,直接加在 Table 标签上即可 -->
|
||||
<Table ref="tableRef"></Table>
|
||||
|
||||
<!-- 表单 -->
|
||||
<PopupForm />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, provide, useTemplateRef } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
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'
|
||||
import createAxios from '/@/utils/axios'
|
||||
import { adminBaseRoutePath } from '/@/router/static/adminBase'
|
||||
|
||||
defineOptions({
|
||||
name: 'game/channel',
|
||||
})
|
||||
|
||||
const { t } = useI18n()
|
||||
const tableRef = useTemplateRef('tableRef')
|
||||
const optButtons: OptButton[] = defaultOptButtons(['edit', 'delete'])
|
||||
|
||||
/**
|
||||
* baTable 内包含了表格的所有数据且数据具备响应性,然后通过 provide 注入给了后代组件
|
||||
*/
|
||||
const channelApiBase = `${adminBaseRoutePath}/game.Channel/`
|
||||
|
||||
const baTable = new baTableClass(
|
||||
new baTableApi(channelApiBase),
|
||||
{
|
||||
pk: 'id',
|
||||
column: [
|
||||
{ type: 'selection', align: 'center', operator: false },
|
||||
{ label: t('game.channel.id'), prop: 'id', align: 'center', width: 70, operator: 'RANGE', sortable: 'custom' },
|
||||
{
|
||||
label: t('game.channel.code'),
|
||||
prop: 'code',
|
||||
align: 'center',
|
||||
operatorPlaceholder: t('Fuzzy query'),
|
||||
sortable: false,
|
||||
operator: 'LIKE',
|
||||
},
|
||||
{
|
||||
label: t('game.channel.name'),
|
||||
prop: 'name',
|
||||
align: 'center',
|
||||
operatorPlaceholder: t('Fuzzy query'),
|
||||
sortable: false,
|
||||
operator: 'LIKE',
|
||||
},
|
||||
{ label: t('game.channel.user_count'), prop: 'user_count', align: 'center', sortable: false, operator: 'RANGE' },
|
||||
{ label: t('game.channel.profit_amount'), prop: 'profit_amount', align: 'center', sortable: false, operator: 'RANGE' },
|
||||
{
|
||||
label: t('game.channel.status'),
|
||||
prop: 'status',
|
||||
align: 'center',
|
||||
operator: 'eq',
|
||||
sortable: false,
|
||||
render: 'switch',
|
||||
replaceValue: { '0': t('game.channel.status 0'), '1': t('game.channel.status 1') },
|
||||
},
|
||||
{
|
||||
label: t('game.channel.admingroup__name'),
|
||||
prop: 'adminGroup.name',
|
||||
align: 'center',
|
||||
minWidth: 110,
|
||||
operatorPlaceholder: t('Fuzzy query'),
|
||||
render: 'tags',
|
||||
operator: 'LIKE',
|
||||
comSearchRender: 'string',
|
||||
},
|
||||
{
|
||||
label: t('game.channel.admin__username'),
|
||||
prop: 'admin.username',
|
||||
align: 'center',
|
||||
minWidth: 90,
|
||||
operatorPlaceholder: t('Fuzzy query'),
|
||||
render: 'tags',
|
||||
operator: 'LIKE',
|
||||
comSearchRender: 'string',
|
||||
},
|
||||
{
|
||||
label: t('game.channel.create_time'),
|
||||
prop: 'create_time',
|
||||
align: 'center',
|
||||
render: 'datetime',
|
||||
operator: 'RANGE',
|
||||
comSearchRender: 'datetime',
|
||||
sortable: 'custom',
|
||||
width: 160,
|
||||
timeFormat: 'yyyy-mm-dd hh:MM:ss',
|
||||
},
|
||||
{
|
||||
label: t('game.channel.update_time'),
|
||||
prop: 'update_time',
|
||||
align: 'center',
|
||||
render: 'datetime',
|
||||
operator: 'RANGE',
|
||||
comSearchRender: 'datetime',
|
||||
sortable: 'custom',
|
||||
width: 160,
|
||||
timeFormat: 'yyyy-mm-dd hh:MM:ss',
|
||||
},
|
||||
{ label: t('Operate'), align: 'center', width: 80, render: 'buttons', buttons: optButtons, operator: false, fixed: 'right' },
|
||||
],
|
||||
dblClickNotEditColumn: [undefined, 'status'],
|
||||
},
|
||||
{
|
||||
defaultItems: { status: '1' },
|
||||
}
|
||||
)
|
||||
|
||||
provide('baTable', baTable)
|
||||
|
||||
baTable.postDel = (ids: string[]) => {
|
||||
if (baTable.runBefore('postDel', { ids }) === false) return
|
||||
void (async () => {
|
||||
try {
|
||||
const stats = await createAxios(
|
||||
{
|
||||
url: channelApiBase + 'index',
|
||||
method: 'get',
|
||||
params: { delete_related_counts: '1', ids },
|
||||
},
|
||||
{ showSuccessMessage: false }
|
||||
)
|
||||
const counts = stats.data as { game_config_count?: number; game_user_count?: number }
|
||||
const countConfig = Number(counts?.game_config_count ?? 0)
|
||||
const countUser = Number(counts?.game_user_count ?? 0)
|
||||
await ElMessageBox.confirm(
|
||||
t('game.channel.delete_confirm_related', { countConfig, countUser }),
|
||||
t('game.channel.delete_confirm_title'),
|
||||
{
|
||||
type: 'warning',
|
||||
confirmButtonText: t('Confirm'),
|
||||
cancelButtonText: t('Cancel'),
|
||||
}
|
||||
)
|
||||
const delRes = await createAxios(
|
||||
{
|
||||
url: channelApiBase + 'del',
|
||||
method: 'DELETE',
|
||||
params: { ids, confirm_cascade: 1 },
|
||||
},
|
||||
{ showSuccessMessage: true }
|
||||
)
|
||||
baTable.onTableHeaderAction('refresh', { event: 'delete', ids })
|
||||
baTable.runAfter('postDel', { res: delRes })
|
||||
} catch {
|
||||
// 用户取消或接口失败(axios 已提示)
|
||||
}
|
||||
})()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
baTable.table.ref = tableRef.value
|
||||
baTable.mount()
|
||||
baTable.getData()?.then(() => {
|
||||
baTable.initSort()
|
||||
baTable.dragSort()
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
@@ -1,93 +0,0 @@
|
||||
<template>
|
||||
<div v-if="isGameWeight && weightTagLabels.length" class="game-config-value-tags">
|
||||
<el-tag
|
||||
v-for="(label, idx) in weightTagLabels"
|
||||
:key="idx"
|
||||
class="m-4"
|
||||
effect="light"
|
||||
type="primary"
|
||||
size="default"
|
||||
>
|
||||
{{ label }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<span v-else class="game-config-value-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
|
||||
}>()
|
||||
|
||||
const isGameWeight = computed(() => props.renderRow?.group === 'game_weight')
|
||||
|
||||
/**
|
||||
* value 形如 [{"T1":"5"},{"T2":"20"},...] 或同结构的 JSON 字符串
|
||||
*/
|
||||
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 weightTagLabels = 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-config-value-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 4px 0;
|
||||
}
|
||||
.m-4 {
|
||||
margin: 4px;
|
||||
}
|
||||
.game-config-value-plain {
|
||||
word-break: break-all;
|
||||
}
|
||||
</style>
|
||||
@@ -1,526 +0,0 @@
|
||||
<template>
|
||||
<!-- 对话框表单 -->
|
||||
<!-- 建议使用 Prettier 格式化代码 -->
|
||||
<!-- el-form 内可以混用 el-form-item、FormItem、ba-input 等输入组件 -->
|
||||
<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('game.config.channel_id')"
|
||||
type="remoteSelect"
|
||||
v-model="baTable.form.items!.channel_id"
|
||||
prop="channel_id"
|
||||
:input-attr="{ ...channelRemoteAttr, disabled: metaFieldsDisabled }"
|
||||
:placeholder="t('Please select field', { field: t('game.config.channel_id') })"
|
||||
/>
|
||||
<FormItem
|
||||
:label="t('game.config.group')"
|
||||
type="select"
|
||||
v-model="baTable.form.items!.group"
|
||||
prop="group"
|
||||
:input-attr="{ content: groupSelectContentFiltered, disabled: metaFieldsDisabled }"
|
||||
:placeholder="t('Please select field', { field: t('game.config.group') })"
|
||||
/>
|
||||
<FormItem
|
||||
:label="t('game.config.name')"
|
||||
type="select"
|
||||
v-model="baTable.form.items!.name"
|
||||
prop="name"
|
||||
:input-attr="{ content: nameSelectContent, disabled: metaFieldsDisabled }"
|
||||
:placeholder="t('Please select field', { field: t('game.config.name') })"
|
||||
/>
|
||||
<FormItem
|
||||
:label="t('game.config.title')"
|
||||
type="string"
|
||||
v-model="baTable.form.items!.title"
|
||||
prop="title"
|
||||
:input-attr="{ disabled: metaFieldsDisabled }"
|
||||
:placeholder="t('Please input field', { field: t('game.config.title') })"
|
||||
/>
|
||||
<!-- game_weight:数组形式编辑,存库仍为 JSON 字符串 -->
|
||||
<el-form-item v-if="isGameWeight" :label="t('game.config.value')" prop="value">
|
||||
<div class="weight-value-editor">
|
||||
<div v-for="(row, idx) in weightRows" :key="idx" class="weight-value-row">
|
||||
<el-input
|
||||
v-model="row.key"
|
||||
class="weight-key"
|
||||
:readonly="weightKeyReadonly"
|
||||
:clearable="!weightKeyReadonly"
|
||||
:placeholder="t('Please input field', { field: t('game.config.weight key') })"
|
||||
@input="onWeightRowChange"
|
||||
/>
|
||||
<span class="weight-sep">:</span>
|
||||
<el-input
|
||||
v-model="row.val"
|
||||
class="weight-val"
|
||||
:placeholder="t('Please input field', { field: t('game.config.weight value') })"
|
||||
clearable
|
||||
:disabled="isDefaultBigwinWeight && isBigwinDiceLockedKey(row.key)"
|
||||
@input="onWeightRowChange"
|
||||
/>
|
||||
<el-button v-if="canEditWeightStructure" type="danger" link @click="removeWeightRow(idx)">
|
||||
{{ t('Delete') }}
|
||||
</el-button>
|
||||
</div>
|
||||
<el-button v-if="canEditWeightStructure" type="primary" link @click="addWeightRow">{{ t('Add') }}</el-button>
|
||||
<div v-if="isDefaultBigwinWeight" class="form-help">{{ t('game.config.default_bigwin_weight_help') }}</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<FormItem
|
||||
v-else
|
||||
:label="t('game.config.value')"
|
||||
type="textarea"
|
||||
v-model="baTable.form.items!.value"
|
||||
prop="value"
|
||||
:input-attr="{ rows: 3 }"
|
||||
@keyup.enter.stop=""
|
||||
@keyup.ctrl.enter="baTable.onSubmit(formRef)"
|
||||
:placeholder="t('Please input field', { field: t('game.config.value') })"
|
||||
/>
|
||||
<FormItem
|
||||
:label="t('game.config.sort')"
|
||||
type="number"
|
||||
v-model="baTable.form.items!.sort"
|
||||
prop="sort"
|
||||
:input-attr="{ step: 1 }"
|
||||
:placeholder="t('Please input field', { field: t('game.config.sort') })"
|
||||
/>
|
||||
<FormItem
|
||||
:label="t('game.config.instantiation')"
|
||||
type="switch"
|
||||
v-model="baTable.form.items!.instantiation"
|
||||
prop="instantiation"
|
||||
:input-attr="{ content: { '0': t('game.config.instantiation 0'), '1': t('game.config.instantiation 1') } }"
|
||||
/>
|
||||
</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 { computed, inject, reactive, ref, useTemplateRef, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import FormItem from '/@/components/formItem/index.vue'
|
||||
import { useConfig } from '/@/stores/config'
|
||||
import { useAdminInfo } from '/@/stores/adminInfo'
|
||||
import type baTableClass from '/@/utils/baTable'
|
||||
import { buildValidatorData } from '/@/utils/validate'
|
||||
import {
|
||||
fixedRowsFromKeys,
|
||||
getFixedKeysForGameConfigName,
|
||||
isBigwinDiceLockedKey,
|
||||
jsonStringFromFixedKeys,
|
||||
normalizeGameWeightConfigName,
|
||||
parseWeightJsonToMap,
|
||||
rowsToMap,
|
||||
weightRowsMatchBigwinDiceKeys,
|
||||
type WeightRow,
|
||||
} from '/@/utils/gameWeightFixed'
|
||||
|
||||
const config = useConfig()
|
||||
const formRef = useTemplateRef('formRef')
|
||||
const baTable = inject('baTable') as baTableClass
|
||||
const adminInfo = useAdminInfo()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const isSuperAdmin = computed(() => adminInfo.super === true)
|
||||
|
||||
/** 编辑且非超级管理员:渠道、分组、配置标识、配置名称不可改 */
|
||||
const metaFieldsDisabled = computed(() => !isSuperAdmin.value && baTable.form.operate === 'Edit')
|
||||
|
||||
const channelRemoteAttr = {
|
||||
pk: 'game_channel.id',
|
||||
field: 'name',
|
||||
remoteUrl: '/admin/game.Channel/index',
|
||||
}
|
||||
|
||||
const groupSelectBase = {
|
||||
game_config: 'game_config',
|
||||
game_weight: 'game_weight',
|
||||
}
|
||||
|
||||
/** 非超级管理员新增时不可选 game_weight(需先由超级管理员建好键结构) */
|
||||
const groupSelectContentFiltered = computed(() => {
|
||||
if (!isSuperAdmin.value && baTable.form.operate === 'Add') {
|
||||
return { game_config: groupSelectBase.game_config }
|
||||
}
|
||||
return groupSelectBase
|
||||
})
|
||||
|
||||
/** default_tier_weight / default_bigwin_weight(及 default_kill_score_weight):键固定,仅值可改,不可增删行 */
|
||||
const isFixedGameWeightConfig = computed(() => getFixedKeysForGameConfigName(baTable.form.items?.name) !== null)
|
||||
|
||||
/** game_weight:编辑或非超管时键只读;仅超管新增非固定项时可增删行、改键 */
|
||||
const weightKeyReadonly = computed(() => {
|
||||
if (!isGameWeight.value) return false
|
||||
if (isFixedGameWeightConfig.value) return true
|
||||
if (baTable.form.operate === 'Edit') return true
|
||||
return !isSuperAdmin.value
|
||||
})
|
||||
|
||||
const canEditWeightStructure = computed(
|
||||
() => isGameWeight.value && baTable.form.operate === 'Add' && isSuperAdmin.value && !isFixedGameWeightConfig.value
|
||||
)
|
||||
|
||||
/** 默认大奖权重:仅校验每项整数与 0~10000(5/30 固定 10000),不参与 tier/kill 的「和≤100」 */
|
||||
const isDefaultBigwinWeight = computed(() => normalizeGameWeightConfigName(baTable.form.items?.name) === 'default_bigwin_weight')
|
||||
|
||||
const weightRows = ref<WeightRow[]>([{ key: '', val: '' }])
|
||||
|
||||
/** default_tier_weight / default_kill_score_weight:每项≤100,且权重之和必须=100 */
|
||||
const WEIGHT_SUM100_NAMES = ['default_tier_weight', 'default_kill_score_weight']
|
||||
|
||||
/** 配置标识:按分组限定可选项;编辑时若库中旧值不在列表中则临时追加一条 */
|
||||
const nameSelectContent = computed((): Record<string, string> => {
|
||||
const g = baTable.form.items?.group
|
||||
let base: Record<string, string> = {}
|
||||
if (g === 'game_config') {
|
||||
base = {
|
||||
game_rule: t('game.config.name opt game_rule'),
|
||||
game_rule_en: t('game.config.name opt game_rule_en'),
|
||||
}
|
||||
} else if (g === 'game_weight') {
|
||||
base = {
|
||||
default_tier_weight: t('game.config.name opt default_tier_weight'),
|
||||
default_kill_score_weight: t('game.config.name opt default_kill_score_weight'),
|
||||
default_bigwin_weight: t('game.config.name opt default_bigwin_weight'),
|
||||
}
|
||||
}
|
||||
const n = baTable.form.items?.name
|
||||
if (typeof n === 'string' && n.trim() !== '' && base[n] === undefined) {
|
||||
const norm = normalizeGameWeightConfigName(n)
|
||||
if (norm !== '' && base[norm] !== undefined) {
|
||||
return { ...base, [n]: base[norm] }
|
||||
}
|
||||
return { ...base, [n]: n }
|
||||
}
|
||||
return base
|
||||
})
|
||||
|
||||
const isGameWeight = computed(() => baTable.form.items?.group === 'game_weight')
|
||||
|
||||
function parseValueToWeightRows(raw: unknown): WeightRow[] {
|
||||
if (raw === null || raw === undefined || raw === '') {
|
||||
return [{ key: '', val: '' }]
|
||||
}
|
||||
if (typeof raw === 'string') {
|
||||
const s = raw.trim()
|
||||
if (!s) return [{ key: '', val: '' }]
|
||||
try {
|
||||
const parsed = JSON.parse(s)
|
||||
return arrayToWeightRows(parsed)
|
||||
} catch {
|
||||
return [{ key: '', val: '' }]
|
||||
}
|
||||
}
|
||||
if (Array.isArray(raw)) {
|
||||
return arrayToWeightRows(raw)
|
||||
}
|
||||
return [{ key: '', val: '' }]
|
||||
}
|
||||
|
||||
function arrayToWeightRows(arr: unknown): WeightRow[] {
|
||||
if (!Array.isArray(arr)) {
|
||||
return [{ key: '', val: '' }]
|
||||
}
|
||||
const out: WeightRow[] = []
|
||||
for (const item of arr) {
|
||||
if (item !== null && typeof item === 'object' && !Array.isArray(item)) {
|
||||
for (const [k, v] of Object.entries(item)) {
|
||||
out.push({ key: k, val: v === null || v === undefined ? '' : String(v) })
|
||||
}
|
||||
}
|
||||
}
|
||||
return out.length ? out : [{ key: '', val: '' }]
|
||||
}
|
||||
|
||||
function weightRowsToJsonString(rows: WeightRow[]): string {
|
||||
const pairs: Record<string, string>[] = []
|
||||
for (const r of rows) {
|
||||
const k = r.key.trim()
|
||||
if (k === '') continue
|
||||
const one: Record<string, string> = {}
|
||||
one[k] = r.val
|
||||
pairs.push(one)
|
||||
}
|
||||
return JSON.stringify(pairs)
|
||||
}
|
||||
|
||||
function syncWeightRowsToFormValue() {
|
||||
const items = baTable.form.items
|
||||
if (!items) return
|
||||
const fixedKeys = getFixedKeysForGameConfigName(items.name)
|
||||
if (fixedKeys) {
|
||||
const map = rowsToMap(weightRows.value)
|
||||
items.value = jsonStringFromFixedKeys(fixedKeys, map)
|
||||
return
|
||||
}
|
||||
items.value = weightRowsToJsonString(weightRows.value)
|
||||
}
|
||||
|
||||
function enforceDefaultBigwinLockedValues() {
|
||||
if (normalizeGameWeightConfigName(baTable.form.items?.name) !== 'default_bigwin_weight') {
|
||||
return
|
||||
}
|
||||
for (const r of weightRows.value) {
|
||||
if (isBigwinDiceLockedKey(r.key)) {
|
||||
r.val = '10000'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onWeightRowChange() {
|
||||
if (isGameWeight.value) {
|
||||
enforceDefaultBigwinLockedValues()
|
||||
syncWeightRowsToFormValue()
|
||||
}
|
||||
}
|
||||
|
||||
function addWeightRow() {
|
||||
if (!canEditWeightStructure.value) return
|
||||
weightRows.value.push({ key: '', val: '' })
|
||||
syncWeightRowsToFormValue()
|
||||
}
|
||||
|
||||
function removeWeightRow(idx: number) {
|
||||
if (!canEditWeightStructure.value) return
|
||||
if (weightRows.value.length <= 1) {
|
||||
weightRows.value = [{ key: '', val: '' }]
|
||||
} else {
|
||||
weightRows.value.splice(idx, 1)
|
||||
}
|
||||
syncWeightRowsToFormValue()
|
||||
}
|
||||
|
||||
function hydrateWeightRowsFromForm() {
|
||||
if (!isGameWeight.value) return
|
||||
const fixedKeys = getFixedKeysForGameConfigName(baTable.form.items?.name)
|
||||
if (fixedKeys) {
|
||||
const map = parseWeightJsonToMap(baTable.form.items?.value)
|
||||
weightRows.value = fixedRowsFromKeys(fixedKeys, map)
|
||||
enforceDefaultBigwinLockedValues()
|
||||
syncWeightRowsToFormValue()
|
||||
return
|
||||
}
|
||||
weightRows.value = parseValueToWeightRows(baTable.form.items?.value)
|
||||
}
|
||||
|
||||
watch(isGameWeight, (gw) => {
|
||||
if (gw) {
|
||||
hydrateWeightRowsFromForm()
|
||||
}
|
||||
})
|
||||
|
||||
watch(
|
||||
() => baTable.form.loading,
|
||||
(loading) => {
|
||||
if (loading === false && baTable.form.items?.group === 'game_weight') {
|
||||
hydrateWeightRowsFromForm()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => baTable.form.items?.group,
|
||||
() => {
|
||||
if (baTable.form.items?.group === 'game_weight') {
|
||||
hydrateWeightRowsFromForm()
|
||||
}
|
||||
const items = baTable.form.items
|
||||
if (!items || !isSuperAdmin.value) {
|
||||
return
|
||||
}
|
||||
const c = nameSelectContent.value
|
||||
const keys = Object.keys(c)
|
||||
if (keys.length === 0) {
|
||||
return
|
||||
}
|
||||
if (typeof items.name !== 'string' || items.name === '' || c[items.name] === undefined) {
|
||||
items.name = keys[0]
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => baTable.form.items?.name,
|
||||
() => {
|
||||
if (baTable.form.items?.group === 'game_weight') {
|
||||
hydrateWeightRowsFromForm()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
function validateGameWeightRules(): string | undefined {
|
||||
if (baTable.form.items?.group !== 'game_weight') {
|
||||
return undefined
|
||||
}
|
||||
const configName = normalizeGameWeightConfigName(baTable.form.items?.name)
|
||||
const fixedKeys = getFixedKeysForGameConfigName(configName)
|
||||
const nums: number[] = []
|
||||
if (fixedKeys) {
|
||||
const map = rowsToMap(weightRows.value)
|
||||
if (configName === 'default_bigwin_weight') {
|
||||
for (const k of fixedKeys) {
|
||||
const vs = (map[k] ?? '').trim()
|
||||
if (vs === '') {
|
||||
return t('Please input field', { field: t('game.config.weight value') })
|
||||
}
|
||||
const n = Number(vs)
|
||||
if (!Number.isFinite(n)) {
|
||||
return t('game.config.weight value numeric')
|
||||
}
|
||||
if (isBigwinDiceLockedKey(k)) {
|
||||
if (n !== 10000) {
|
||||
return t('game.config.bigwin weight locked 5 30')
|
||||
}
|
||||
} else if (n < 0 || n > 10000) {
|
||||
return t('game.config.bigwin weight each 0 10000')
|
||||
}
|
||||
nums.push(n)
|
||||
}
|
||||
} else {
|
||||
for (const k of fixedKeys) {
|
||||
const vs = (map[k] ?? '').trim()
|
||||
if (vs === '') {
|
||||
return t('Please input field', { field: t('game.config.weight value') })
|
||||
}
|
||||
const n = Number(vs)
|
||||
if (!Number.isFinite(n)) {
|
||||
return t('game.config.weight value numeric')
|
||||
}
|
||||
if (n > 100) {
|
||||
return t('game.config.weight each max 100')
|
||||
}
|
||||
nums.push(n)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 非固定键名但行结构已是 5~30 骰子时,按大奖 0~10000 校验(避免 name 格式异常时误走每项≤10000)
|
||||
const treatAsBigwin = configName === 'default_bigwin_weight' || weightRowsMatchBigwinDiceKeys(weightRows.value)
|
||||
for (const r of weightRows.value) {
|
||||
const k = r.key.trim()
|
||||
if (k === '') continue
|
||||
const vs = r.val.trim()
|
||||
if (vs === '') {
|
||||
return t('Please input field', { field: t('game.config.weight value') })
|
||||
}
|
||||
const n = Number(vs)
|
||||
if (!Number.isFinite(n)) {
|
||||
return t('game.config.weight value numeric')
|
||||
}
|
||||
if (treatAsBigwin) {
|
||||
if (isBigwinDiceLockedKey(k)) {
|
||||
if (n !== 10000) {
|
||||
return t('game.config.bigwin weight locked 5 30')
|
||||
}
|
||||
} else if (n < 0 || n > 10000) {
|
||||
return t('game.config.bigwin weight each 0 10000')
|
||||
}
|
||||
} else if (n > 10000) {
|
||||
return t('game.config.weight each max 10000')
|
||||
}
|
||||
nums.push(n)
|
||||
}
|
||||
if (nums.length === 0) {
|
||||
return t('Please input field', { field: t('game.config.value') })
|
||||
}
|
||||
}
|
||||
if (WEIGHT_SUM100_NAMES.includes(configName)) {
|
||||
let sum = 0
|
||||
for (const x of nums) {
|
||||
sum += x
|
||||
}
|
||||
if (Math.abs(sum - 100) > 0.000001) {
|
||||
return t('game.config.weight sum must 100')
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
const rules: Partial<Record<string, FormItemRule[]>> = reactive({
|
||||
group: [buildValidatorData({ name: 'required', title: t('game.config.group') })],
|
||||
name: [buildValidatorData({ name: 'required', title: t('game.config.name') })],
|
||||
title: [buildValidatorData({ name: 'required', title: t('game.config.title') })],
|
||||
sort: [buildValidatorData({ name: 'number', title: t('game.config.sort') })],
|
||||
instantiation: [buildValidatorData({ name: 'number', title: t('game.config.instantiation') })],
|
||||
value: [
|
||||
{
|
||||
validator: (_rule, _val, callback) => {
|
||||
const err = validateGameWeightRules()
|
||||
if (err) {
|
||||
callback(new Error(err))
|
||||
return
|
||||
}
|
||||
callback()
|
||||
},
|
||||
trigger: ['blur', 'change'],
|
||||
},
|
||||
],
|
||||
create_time: [buildValidatorData({ name: 'date', title: t('game.config.create_time') })],
|
||||
update_time: [buildValidatorData({ name: 'date', title: t('game.config.update_time') })],
|
||||
})
|
||||
</script>
|
||||
|
||||
<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);
|
||||
}
|
||||
.form-help {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
</style>
|
||||
@@ -1,892 +0,0 @@
|
||||
<template>
|
||||
<div class="default-main ba-table-box">
|
||||
<el-card shadow="never" class="reward-form-card" v-loading="pageLoading">
|
||||
<template #header>
|
||||
<span class="card-title">{{ t('game.rewardConfigForm.title') }}</span>
|
||||
</template>
|
||||
|
||||
<el-alert
|
||||
v-if="showTemplateIntro"
|
||||
class="intro-alert"
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
>
|
||||
<template #title>{{ t('game.rewardConfigForm.intro') }}</template>
|
||||
</el-alert>
|
||||
|
||||
<el-form
|
||||
ref="formRef"
|
||||
class="reward-form-body"
|
||||
@submit.prevent=""
|
||||
:model="formModel"
|
||||
:label-position="config.layout.shrink ? 'top' : 'right'"
|
||||
:label-width="formLabelWidth + 'px'"
|
||||
:rules="rules"
|
||||
>
|
||||
<template v-if="isSuperAdmin">
|
||||
<el-form-item :label="t('game.rewardConfigForm.super_scope_label')">
|
||||
<el-radio-group v-model="superEditScope" @change="onSuperScopeChange">
|
||||
<el-radio-button value="template">{{ t('game.rewardConfigForm.super_scope_template') }}</el-radio-button>
|
||||
<el-radio-button value="channel">{{ t('game.rewardConfigForm.super_scope_channel') }}</el-radio-button>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-alert
|
||||
v-if="superEditScope === 'channel'"
|
||||
class="scope-hint"
|
||||
type="warning"
|
||||
:closable="false"
|
||||
:title="t('game.rewardConfigForm.super_scope_hint')"
|
||||
/>
|
||||
<div v-if="superEditScope === 'channel'" class="channel-bar">
|
||||
<div class="channel-picker">
|
||||
<FormItem
|
||||
:label="t('game.rewardConfig.game_channel_id')"
|
||||
type="remoteSelect"
|
||||
v-model="formModel.game_channel_id"
|
||||
prop="game_channel_id"
|
||||
:input-attr="channelRemoteAttr"
|
||||
:placeholder="t('Please select field', { field: t('game.rewardConfig.game_channel_id') })"
|
||||
/>
|
||||
</div>
|
||||
<el-button type="primary" @click="loadData" :disabled="!formModel.game_channel_id">{{ t('Refresh') }}</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="showInner">
|
||||
<el-form-item :label="t('game.rewardConfig.tier_reward_form')" prop="tier_reward_form">
|
||||
<div class="block-editor tier-editor">
|
||||
<div class="line line-head">
|
||||
<span>{{ t('game.rewardConfig.grid_number') }}</span>
|
||||
<span>{{ t('game.rewardConfig.ui_text') }}</span>
|
||||
<span>{{ t('game.rewardConfig.ui_text_en') }}</span>
|
||||
<span>{{ t('game.rewardConfig.real_ev') }}</span>
|
||||
<span>{{ t('game.rewardConfig.tier') }}</span>
|
||||
<span>{{ t('game.rewardConfig.remark') }}</span>
|
||||
</div>
|
||||
<div v-for="(row, idx) in tierRows" :key="'tier-' + idx" class="line">
|
||||
<el-input v-model="row.grid_number" disabled />
|
||||
<el-input
|
||||
v-model="row.ui_text"
|
||||
:placeholder="t('Please input field', { field: t('game.rewardConfig.ui_text') })"
|
||||
@input="syncPayload"
|
||||
/>
|
||||
<el-input
|
||||
v-model="row.ui_text_en"
|
||||
:placeholder="t('Please input field', { field: t('game.rewardConfig.ui_text_en') })"
|
||||
@input="syncPayload"
|
||||
/>
|
||||
<el-input
|
||||
v-model="row.real_ev"
|
||||
:placeholder="t('Please input field', { field: t('game.rewardConfig.real_ev') })"
|
||||
@input="syncPayload"
|
||||
/>
|
||||
<el-select v-model="row.tier" style="width: 100px" @change="syncPayload">
|
||||
<el-option :label="t('game.rewardConfig.tier_t1')" value="T1" />
|
||||
<el-option :label="t('game.rewardConfig.tier_t2')" value="T2" />
|
||||
<el-option :label="t('game.rewardConfig.tier_t3')" value="T3" />
|
||||
<el-option :label="t('game.rewardConfig.tier_t4')" value="T4" />
|
||||
<el-option :label="t('game.rewardConfig.tier_t5')" value="T5" />
|
||||
</el-select>
|
||||
<el-input
|
||||
v-model="row.remark"
|
||||
:placeholder="t('Please input field', { field: t('game.rewardConfig.remark') })"
|
||||
@input="syncPayload"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-help">{{ t('game.rewardConfig.tier_reward_form_help') }}</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('game.rewardConfig.bigwin_form')" prop="bigwin_form">
|
||||
<div class="block-editor bigwin-editor">
|
||||
<div class="line line-head">
|
||||
<span>{{ t('game.rewardConfig.grid_number') }}</span>
|
||||
<span>{{ t('game.rewardConfig.ui_text') }}</span>
|
||||
<span>{{ t('game.rewardConfig.real_ev') }}</span>
|
||||
<span>{{ t('game.rewardConfig.tier') }}</span>
|
||||
</div>
|
||||
<div v-for="(row, idx) in bigwinRows" :key="'bigwin-' + idx" class="line">
|
||||
<el-input v-model="row.grid_number" disabled />
|
||||
<el-input
|
||||
v-model="row.ui_text"
|
||||
:placeholder="t('Please input field', { field: t('game.rewardConfig.ui_text') })"
|
||||
@input="syncPayload"
|
||||
/>
|
||||
<el-input
|
||||
v-model="row.real_ev"
|
||||
:placeholder="t('Please input field', { field: t('game.rewardConfig.real_ev') })"
|
||||
@input="syncPayload"
|
||||
/>
|
||||
<el-input v-model="row.tier" disabled style="width: 120px" />
|
||||
</div>
|
||||
<div class="form-help">{{ t('game.rewardConfig.bigwin_form_help') }}</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" :loading="submitLoading" @click="onSubmit">{{ t('game.rewardConfigForm.btn_add') }}</el-button>
|
||||
<el-button @click="onReset">{{ t('game.rewardConfigForm.btn_reset') }}</el-button>
|
||||
<el-button v-auth="'generateTierBoard'" @click="openGenTierDialog">{{ t('game.rewardConfigForm.btn_gen_tier') }}</el-button>
|
||||
<el-button v-auth="'generateRewardWeight'" :loading="genWeightSubmitting" @click="onGenWeightClick">{{
|
||||
t('game.rewardConfigForm.btn_gen_weight')
|
||||
}}</el-button>
|
||||
</el-form-item>
|
||||
</template>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<el-dialog
|
||||
v-model="genTierDialogVisible"
|
||||
class="gen-tier-dialog"
|
||||
:title="t('game.rewardConfigForm.gen_tier_title')"
|
||||
:width="genTierDialogWidth"
|
||||
:fullscreen="genTierDialogFullscreen"
|
||||
:align-center="!genTierDialogFullscreen"
|
||||
append-to-body
|
||||
:close-on-click-modal="false"
|
||||
destroy-on-close
|
||||
>
|
||||
<el-scrollbar :max-height="genTierScrollMaxHeight" class="gen-tier-scroll">
|
||||
<div class="gen-rule">{{ t('game.rewardConfigForm.gen_tier_rule') }}</div>
|
||||
<el-form
|
||||
:model="genTierForm"
|
||||
class="gen-tier-form"
|
||||
:label-position="genTierFormLabelPosition"
|
||||
:label-width="genTierFormLabelWidth"
|
||||
>
|
||||
<div class="gen-tier-block">
|
||||
<div class="gen-tier-block-title">{{ t('game.rewardConfigForm.gen_t1_label') }}</div>
|
||||
<el-form-item :label="t('game.rewardConfigForm.gen_t1_fixed') + '(' + t('game.rewardConfigForm.gen_dir_cw') + ')'">
|
||||
<el-input-number v-model="genTierForm.t1_fixed_cw" :min="0" :max="26" :step="1" controls-position="right" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('game.rewardConfigForm.gen_t1_fixed') + '(' + t('game.rewardConfigForm.gen_dir_ccw') + ')'">
|
||||
<el-input-number v-model="genTierForm.t1_fixed_ccw" :min="0" :max="26" :step="1" controls-position="right" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('game.rewardConfigForm.gen_settlement')">
|
||||
<el-input-number v-model="genTierForm.amt_t1" :min="0" :step="1" controls-position="right" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="gen-tier-block">
|
||||
<div class="gen-tier-block-title">{{ t('game.rewardConfigForm.gen_t2_label') }}</div>
|
||||
<el-form-item :label="t('game.rewardConfigForm.gen_t2_min') + '(' + t('game.rewardConfigForm.gen_dir_cw') + ')'">
|
||||
<el-input-number v-model="genTierForm.t2_min_cw" :min="0" :max="26" :step="1" controls-position="right" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('game.rewardConfigForm.gen_t2_min') + '(' + t('game.rewardConfigForm.gen_dir_ccw') + ')'">
|
||||
<el-input-number v-model="genTierForm.t2_min_ccw" :min="0" :max="26" :step="1" controls-position="right" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('game.rewardConfigForm.gen_settlement')">
|
||||
<el-input-number v-model="genTierForm.amt_t2" :min="0" :precision="2" :step="0.1" controls-position="right" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="gen-tier-block">
|
||||
<div class="gen-tier-block-title">{{ t('game.rewardConfigForm.gen_t3_label') }}</div>
|
||||
<el-form-item :label="t('game.rewardConfigForm.gen_t3_amt_only')">
|
||||
<el-input-number v-model="genTierForm.amt_t3" :min="0" :precision="2" :step="0.1" controls-position="right" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="gen-tier-block">
|
||||
<div class="gen-tier-block-title">{{ t('game.rewardConfigForm.gen_t4_label') }}</div>
|
||||
<el-form-item :label="t('game.rewardConfigForm.gen_t4_fixed') + '(' + t('game.rewardConfigForm.gen_dir_cw') + ')'">
|
||||
<el-input-number v-model="genTierForm.t4_fixed_cw" :min="0" :max="26" :step="1" controls-position="right" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('game.rewardConfigForm.gen_t4_fixed') + '(' + t('game.rewardConfigForm.gen_dir_ccw') + ')'">
|
||||
<el-input-number v-model="genTierForm.t4_fixed_ccw" :min="0" :max="26" :step="1" controls-position="right" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('game.rewardConfigForm.gen_settlement')">
|
||||
<el-input-number v-model="genTierForm.amt_t4" :precision="2" :step="0.1" controls-position="right" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="gen-tier-block">
|
||||
<div class="gen-tier-block-title">{{ t('game.rewardConfigForm.gen_t5_label') }}</div>
|
||||
<el-form-item :label="t('game.rewardConfigForm.gen_t5_fixed') + '(' + t('game.rewardConfigForm.gen_dir_cw') + ')'">
|
||||
<el-input-number v-model="genTierForm.t5_fixed_cw" :min="0" :max="26" :step="1" controls-position="right" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('game.rewardConfigForm.gen_t5_fixed') + '(' + t('game.rewardConfigForm.gen_dir_ccw') + ')'">
|
||||
<el-input-number v-model="genTierForm.t5_fixed_ccw" :min="0" :max="26" :step="1" controls-position="right" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('game.rewardConfigForm.gen_settlement')">
|
||||
<el-input-number :model-value="0" disabled />
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
<div class="gen-tier-footer-hint">{{ t('game.rewardConfigForm.gen_tier_footer_hint') }}</div>
|
||||
</el-scrollbar>
|
||||
<template #footer>
|
||||
<div class="gen-tier-dialog-footer">
|
||||
<el-button @click="genTierDialogVisible = false">{{ t('game.rewardConfigForm.gen_tier_cancel') }}</el-button>
|
||||
<el-button v-auth="'generateTierBoard'" type="primary" :loading="genTierSubmitting" @click="onGenTierSubmit">{{
|
||||
t('game.rewardConfigForm.gen_tier_submit')
|
||||
}}</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { FormInstance, FormItemRule } from 'element-plus'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { useWindowSize } from '@vueuse/core'
|
||||
import { computed, onMounted, reactive, ref, useTemplateRef, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import FormItem from '/@/components/formItem/index.vue'
|
||||
import { useConfig } from '/@/stores/config'
|
||||
import { useAdminInfo } from '/@/stores/adminInfo'
|
||||
import createAxios from '/@/utils/axios'
|
||||
|
||||
defineOptions({
|
||||
name: 'game/rewardConfig',
|
||||
})
|
||||
|
||||
type RewardRow = {
|
||||
grid_number: string
|
||||
ui_text: string
|
||||
ui_text_en: string
|
||||
real_ev: string
|
||||
tier: string
|
||||
remark: string
|
||||
}
|
||||
|
||||
const { t } = useI18n()
|
||||
const config = useConfig()
|
||||
const adminInfo = useAdminInfo()
|
||||
const { width: windowWidth } = useWindowSize()
|
||||
|
||||
/** 生成弹窗:窄屏全屏/宽百分比,宽屏固定最大宽度 */
|
||||
const genTierDialogFullscreen = computed(() => windowWidth.value <= 520)
|
||||
const genTierDialogWidth = computed(() => {
|
||||
if (windowWidth.value <= 520) {
|
||||
return '100%'
|
||||
}
|
||||
if (windowWidth.value <= 768) {
|
||||
return '92%'
|
||||
}
|
||||
return '720px'
|
||||
})
|
||||
|
||||
const genTierFormLabelPosition = computed(() => (windowWidth.value < 640 ? 'top' : 'right'))
|
||||
const genTierFormLabelWidth = computed(() => (windowWidth.value < 640 ? 'auto' : '150px'))
|
||||
|
||||
const genTierScrollMaxHeight = computed(() => (genTierDialogFullscreen.value ? 'calc(100vh - 140px)' : 'min(70vh, 640px)'))
|
||||
const formRef = useTemplateRef<FormInstance>('formRef')
|
||||
|
||||
const TIER_GRIDS = Array.from({ length: 26 }, (_v, i) => String(i + 5))
|
||||
const BIGWIN_GRIDS = ['5', '10', '15', '20', '25', '30']
|
||||
|
||||
const isSuperAdmin = computed(() => adminInfo.super === true)
|
||||
const formLabelWidth = computed(() => (config.layout.shrink ? 100 : 140))
|
||||
const channelRemoteAttr = { pk: 'game_channel.id', field: 'name', remoteUrl: '/admin/game.Channel/index' }
|
||||
|
||||
const pageLoading = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
const genTierDialogVisible = ref(false)
|
||||
const genTierSubmitting = ref(false)
|
||||
const genWeightSubmitting = ref(false)
|
||||
const channelChangeSilent = ref(false)
|
||||
const lastAutoLoadChannelId = ref<number | null>(null)
|
||||
|
||||
const genTierForm = reactive({
|
||||
t1_fixed_cw: 3,
|
||||
t1_fixed_ccw: 3,
|
||||
t2_min_cw: 5,
|
||||
t2_min_ccw: 5,
|
||||
t4_fixed_cw: 1,
|
||||
t4_fixed_ccw: 1,
|
||||
t5_fixed_cw: 1,
|
||||
t5_fixed_ccw: 1,
|
||||
amt_t1: 3,
|
||||
amt_t2: 1.5,
|
||||
amt_t3: 0.5,
|
||||
amt_t4: -0.4,
|
||||
})
|
||||
/** 超管:template = game_channel_id 0 默认模板;channel = 编辑指定渠道 */
|
||||
const superEditScope = ref<'template' | 'channel'>('template')
|
||||
/** 仅超管维护「全渠道默认模板」(game_channel_id=0、未绑定具体渠道)时展示顶部说明;渠道管理员或超管选「指定渠道」时不展示 */
|
||||
const showTemplateIntro = computed(() => isSuperAdmin.value && superEditScope.value === 'template')
|
||||
|
||||
const formModel = reactive({
|
||||
id: null as number | string | null,
|
||||
game_channel_id: 0 as number | string,
|
||||
tier_reward_form: '',
|
||||
bigwin_form: '',
|
||||
})
|
||||
|
||||
const tierRows = ref<RewardRow[]>(
|
||||
TIER_GRIDS.map((g) => ({ grid_number: g, ui_text: '', ui_text_en: '', real_ev: '', tier: 'T1', remark: '' }))
|
||||
)
|
||||
const bigwinRows = ref<RewardRow[]>(
|
||||
BIGWIN_GRIDS.map((g) => ({ grid_number: g, ui_text: '', ui_text_en: '', real_ev: '', tier: 'BIGWIN', remark: '' }))
|
||||
)
|
||||
|
||||
const showInner = computed(() => {
|
||||
if (!isSuperAdmin.value) {
|
||||
return true
|
||||
}
|
||||
if (superEditScope.value === 'template') {
|
||||
return true
|
||||
}
|
||||
return !!formModel.game_channel_id
|
||||
})
|
||||
|
||||
function parseRows(raw: unknown): RewardRow[] {
|
||||
if (typeof raw !== 'string' || raw.trim() === '') return []
|
||||
try {
|
||||
const parsed = JSON.parse(raw) as unknown
|
||||
if (!Array.isArray(parsed)) return []
|
||||
const out: RewardRow[] = []
|
||||
for (const item of parsed) {
|
||||
if (item === null || typeof item !== 'object' || Array.isArray(item)) continue
|
||||
const obj = item as Record<string, unknown>
|
||||
out.push({
|
||||
grid_number: String(obj.grid_number ?? ''),
|
||||
ui_text: String(obj.ui_text ?? ''),
|
||||
ui_text_en: String(obj.ui_text_en ?? ''),
|
||||
real_ev: String(obj.real_ev ?? ''),
|
||||
tier: String(obj.tier ?? ''),
|
||||
remark: String(obj.remark ?? ''),
|
||||
})
|
||||
}
|
||||
return out
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
function toTierRows(raw: unknown): RewardRow[] {
|
||||
const parsed = parseRows(raw)
|
||||
const map = new Map<string, RewardRow>()
|
||||
for (const r of parsed) map.set(r.grid_number, r)
|
||||
return TIER_GRIDS.map((g) => {
|
||||
const row = map.get(g)
|
||||
return {
|
||||
grid_number: g,
|
||||
ui_text: row?.ui_text ?? '',
|
||||
ui_text_en: row?.ui_text_en ?? '',
|
||||
real_ev: row?.real_ev ?? '',
|
||||
tier: row?.tier && row.tier !== 'BIGWIN' ? row.tier : 'T1',
|
||||
remark: row?.remark ?? '',
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function toBigwinRows(raw: unknown): RewardRow[] {
|
||||
const parsed = parseRows(raw)
|
||||
const map = new Map<string, RewardRow>()
|
||||
for (const r of parsed) map.set(r.grid_number, r)
|
||||
return BIGWIN_GRIDS.map((g) => {
|
||||
const row = map.get(g)
|
||||
return {
|
||||
grid_number: g,
|
||||
ui_text: row?.ui_text ?? '',
|
||||
ui_text_en: row?.ui_text_en ?? '',
|
||||
real_ev: row?.real_ev ?? '',
|
||||
tier: 'BIGWIN',
|
||||
remark: row?.remark ?? '',
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function syncPayload() {
|
||||
formModel.tier_reward_form = JSON.stringify(tierRows.value)
|
||||
formModel.bigwin_form = JSON.stringify(bigwinRows.value)
|
||||
}
|
||||
|
||||
function applyRowToForm(row: Record<string, unknown>) {
|
||||
channelChangeSilent.value = true
|
||||
formModel.id = (row.id as number | string | null | undefined) ?? null
|
||||
formModel.game_channel_id = (row.game_channel_id as number | string) ?? 0
|
||||
tierRows.value = toTierRows(row.tier_reward_form)
|
||||
bigwinRows.value = toBigwinRows(row.bigwin_form)
|
||||
syncPayload()
|
||||
channelChangeSilent.value = false
|
||||
}
|
||||
|
||||
function validateForms(): string | undefined {
|
||||
if (tierRows.value.length !== 26 || bigwinRows.value.length !== 6) {
|
||||
return t('Parameter error')
|
||||
}
|
||||
for (let i = 0; i < tierRows.value.length; i++) {
|
||||
const row = tierRows.value[i]
|
||||
if (row.grid_number !== TIER_GRIDS[i]) return t('Parameter error')
|
||||
if (row.ui_text.trim() === '' || row.real_ev.trim() === '') return t('Please input field', { field: t('game.rewardConfig.ui_text') })
|
||||
if (!Number.isFinite(Number(row.real_ev))) return t('game.rewardConfig.real_ev')
|
||||
if (!['T1', 'T2', 'T3', 'T4', 'T5'].includes(row.tier)) return t('Parameter error')
|
||||
}
|
||||
for (let i = 0; i < bigwinRows.value.length; i++) {
|
||||
const row = bigwinRows.value[i]
|
||||
if (row.grid_number !== BIGWIN_GRIDS[i]) return t('Parameter error')
|
||||
if (row.tier !== 'BIGWIN') return t('Parameter error')
|
||||
if (row.ui_text.trim() === '' || row.real_ev.trim() === '') return t('Please input field', { field: t('game.rewardConfig.ui_text') })
|
||||
if (!Number.isFinite(Number(row.real_ev))) return t('game.rewardConfig.real_ev')
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
const rules: Partial<Record<string, FormItemRule[]>> = reactive({
|
||||
game_channel_id: [
|
||||
{
|
||||
validator: (_rule, _val, callback) => {
|
||||
if (!isSuperAdmin.value || superEditScope.value !== 'channel') {
|
||||
callback()
|
||||
return
|
||||
}
|
||||
if (!_val && _val !== 0) {
|
||||
callback(new Error(t('Please select field', { field: t('game.rewardConfig.game_channel_id') })))
|
||||
return
|
||||
}
|
||||
if (_val === 0 || _val === '0') {
|
||||
callback(new Error(t('Please select field', { field: t('game.rewardConfig.game_channel_id') })))
|
||||
return
|
||||
}
|
||||
callback()
|
||||
},
|
||||
trigger: ['change', 'blur'],
|
||||
},
|
||||
],
|
||||
tier_reward_form: [
|
||||
{
|
||||
validator: (_rule, _val, callback) => {
|
||||
const err = validateForms()
|
||||
if (err) return callback(new Error(err))
|
||||
callback()
|
||||
},
|
||||
trigger: ['blur', 'change'],
|
||||
},
|
||||
],
|
||||
bigwin_form: [
|
||||
{
|
||||
validator: (_rule, _val, callback) => {
|
||||
const err = validateForms()
|
||||
if (err) return callback(new Error(err))
|
||||
callback()
|
||||
},
|
||||
trigger: ['blur', 'change'],
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
function resolveRequestChannelId(): number | null {
|
||||
if (!isSuperAdmin.value) {
|
||||
return null
|
||||
}
|
||||
if (superEditScope.value === 'template') {
|
||||
return 0
|
||||
}
|
||||
const v = formModel.game_channel_id
|
||||
if (!v && v !== 0) return null
|
||||
const n = Number(v)
|
||||
if (!Number.isFinite(n) || n <= 0) return null
|
||||
return n
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
if (isSuperAdmin.value && superEditScope.value === 'channel' && !formModel.game_channel_id) {
|
||||
return
|
||||
}
|
||||
pageLoading.value = true
|
||||
try {
|
||||
const params: Record<string, number> = {}
|
||||
const cid = resolveRequestChannelId()
|
||||
if (cid !== null) {
|
||||
params.game_channel_id = cid
|
||||
}
|
||||
const res = await createAxios<{ row?: Record<string, unknown> }>(
|
||||
{
|
||||
url: '/admin/game.RewardConfig/index',
|
||||
method: 'get',
|
||||
params,
|
||||
},
|
||||
{ showErrorMessage: true, loading: false }
|
||||
)
|
||||
const row = res.data?.row
|
||||
if (row && typeof row === 'object' && !Array.isArray(row)) {
|
||||
applyRowToForm(row as Record<string, unknown>)
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
const err = error as { code?: string; message?: string }
|
||||
if (err.code === 'ERR_CANCELED' || String(err.message ?? '').toLowerCase().includes('canceled')) {
|
||||
return
|
||||
}
|
||||
} finally {
|
||||
pageLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function onSuperScopeChange(val: string | number | boolean | undefined) {
|
||||
if (val === 'template') {
|
||||
formModel.game_channel_id = 0
|
||||
loadData()
|
||||
} else {
|
||||
formModel.game_channel_id = ''
|
||||
lastAutoLoadChannelId.value = null
|
||||
formRef.value?.clearValidate(['game_channel_id'])
|
||||
}
|
||||
}
|
||||
|
||||
async function onSubmit() {
|
||||
const form = formRef.value
|
||||
if (!form) return
|
||||
syncPayload()
|
||||
await form.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
submitLoading.value = true
|
||||
try {
|
||||
const body: Record<string, string | number> = {
|
||||
tier_reward_form: formModel.tier_reward_form,
|
||||
bigwin_form: formModel.bigwin_form,
|
||||
}
|
||||
const cid = resolveRequestChannelId()
|
||||
if (cid !== null) {
|
||||
body.game_channel_id = cid
|
||||
}
|
||||
await createAxios(
|
||||
{
|
||||
url: '/admin/game.RewardConfig/save',
|
||||
method: 'post',
|
||||
data: body,
|
||||
},
|
||||
{ showSuccessMessage: true, loading: false }
|
||||
)
|
||||
await loadData()
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function onReset() {
|
||||
formRef.value?.clearValidate()
|
||||
await loadData()
|
||||
}
|
||||
|
||||
function openGenTierDialog() {
|
||||
genTierDialogVisible.value = true
|
||||
}
|
||||
|
||||
async function onGenTierSubmit() {
|
||||
genTierSubmitting.value = true
|
||||
try {
|
||||
const body: Record<string, string | number> = {
|
||||
t1_fixed_cw: genTierForm.t1_fixed_cw,
|
||||
t1_fixed_ccw: genTierForm.t1_fixed_ccw,
|
||||
t2_min_cw: genTierForm.t2_min_cw,
|
||||
t2_min_ccw: genTierForm.t2_min_ccw,
|
||||
t4_fixed_cw: genTierForm.t4_fixed_cw,
|
||||
t4_fixed_ccw: genTierForm.t4_fixed_ccw,
|
||||
t5_fixed_cw: genTierForm.t5_fixed_cw,
|
||||
t5_fixed_ccw: genTierForm.t5_fixed_ccw,
|
||||
amt_t1: genTierForm.amt_t1,
|
||||
amt_t2: genTierForm.amt_t2,
|
||||
amt_t3: genTierForm.amt_t3,
|
||||
amt_t4: genTierForm.amt_t4,
|
||||
}
|
||||
const cid = resolveRequestChannelId()
|
||||
if (cid !== null) {
|
||||
body.game_channel_id = cid
|
||||
}
|
||||
await createAxios(
|
||||
{
|
||||
url: '/admin/game.RewardConfig/generateTierBoard',
|
||||
method: 'post',
|
||||
data: body,
|
||||
},
|
||||
{ showSuccessMessage: true, loading: false }
|
||||
)
|
||||
genTierDialogVisible.value = false
|
||||
await loadData()
|
||||
} finally {
|
||||
genTierSubmitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function onGenWeightClick() {
|
||||
if (isSuperAdmin.value && superEditScope.value === 'channel' && !formModel.game_channel_id) {
|
||||
ElMessage.warning(t('game.rewardConfigForm.gen_weight_need_channel'))
|
||||
return
|
||||
}
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
t('game.rewardConfigForm.gen_weight_confirm_body'),
|
||||
t('game.rewardConfigForm.gen_weight_confirm_title'),
|
||||
{
|
||||
type: 'warning',
|
||||
confirmButtonText: t('game.rewardConfigForm.gen_weight_confirm_ok'),
|
||||
cancelButtonText: t('Cancel'),
|
||||
}
|
||||
)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
genWeightSubmitting.value = true
|
||||
try {
|
||||
const body: Record<string, number> = {}
|
||||
const cid = resolveRequestChannelId()
|
||||
if (cid !== null) {
|
||||
body.game_channel_id = cid
|
||||
}
|
||||
await createAxios(
|
||||
{
|
||||
url: '/admin/game.RewardConfig/generateRewardWeight',
|
||||
method: 'post',
|
||||
data: body,
|
||||
},
|
||||
{ showSuccessMessage: true, loading: false }
|
||||
)
|
||||
} finally {
|
||||
genWeightSubmitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (isSuperAdmin.value) {
|
||||
superEditScope.value = 'template'
|
||||
formModel.game_channel_id = 0
|
||||
loadData()
|
||||
} else {
|
||||
loadData()
|
||||
}
|
||||
})
|
||||
|
||||
watch(
|
||||
() => formModel.game_channel_id,
|
||||
(val, oldVal) => {
|
||||
if (!isSuperAdmin.value || superEditScope.value !== 'channel') {
|
||||
return
|
||||
}
|
||||
if (channelChangeSilent.value) {
|
||||
return
|
||||
}
|
||||
const nextCid = Number(val)
|
||||
const prevCid = Number(oldVal)
|
||||
if (Number.isFinite(nextCid) && Number.isFinite(prevCid) && nextCid === prevCid) {
|
||||
return
|
||||
}
|
||||
if (!Number.isFinite(nextCid) || nextCid <= 0) {
|
||||
return
|
||||
}
|
||||
if (lastAutoLoadChannelId.value === nextCid) {
|
||||
return
|
||||
}
|
||||
lastAutoLoadChannelId.value = nextCid
|
||||
void loadData()
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.reward-form-card {
|
||||
max-width: 1280px;
|
||||
}
|
||||
.card-title {
|
||||
font-weight: 600;
|
||||
}
|
||||
.intro-alert {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.scope-hint {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.channel-bar {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.channel-picker {
|
||||
flex: 1 1 360px;
|
||||
min-width: 320px;
|
||||
max-width: 520px;
|
||||
}
|
||||
.channel-picker :deep(.el-form-item) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.channel-picker :deep(.el-select) {
|
||||
width: 100%;
|
||||
}
|
||||
.reward-form-body {
|
||||
margin-top: 8px;
|
||||
}
|
||||
.block-editor {
|
||||
width: 100%;
|
||||
}
|
||||
.tier-editor .line {
|
||||
display: grid;
|
||||
grid-template-columns: 64px minmax(88px, 1fr) minmax(88px, 1fr) minmax(88px, 1fr) minmax(96px, 110px) minmax(124px, 1fr);
|
||||
column-gap: 10px;
|
||||
row-gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.bigwin-editor .line {
|
||||
display: grid;
|
||||
grid-template-columns: 110px 1fr 1fr 120px;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.line-head {
|
||||
margin-bottom: 6px;
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.form-help {
|
||||
margin-top: 6px;
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.reward-form-card :deep(.el-card__body) {
|
||||
padding: 12px 10px;
|
||||
}
|
||||
.reward-form-body :deep(.el-form-item) {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.channel-picker {
|
||||
min-width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
.channel-bar {
|
||||
gap: 8px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.channel-bar > .el-button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 移动端主配置表:保持列宽,允许左右滚动,避免被压扁 */
|
||||
.tier-editor,
|
||||
.bigwin-editor {
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
.tier-editor .line {
|
||||
min-width: 820px;
|
||||
grid-template-columns: 48px 80px 80px 80px 100px 130px;
|
||||
column-gap: 10px;
|
||||
row-gap: 8px;
|
||||
}
|
||||
.bigwin-editor .line {
|
||||
min-width: 560px;
|
||||
grid-template-columns: 48px 80px 80px 100px;
|
||||
}
|
||||
.line-head {
|
||||
font-size: 11px;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.reward-form-body :deep(.el-form-item:last-child .el-form-item__content) {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
.reward-form-body :deep(.el-form-item:last-child .el-button) {
|
||||
flex: 1 1 calc(50% - 8px);
|
||||
min-width: 128px;
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- 弹窗 append-to-body 后不在当前组件 DOM 内,scoped 样式无法作用,单独写全局选择器 -->
|
||||
<style lang="scss">
|
||||
.gen-tier-dialog.el-dialog {
|
||||
box-sizing: border-box;
|
||||
max-width: 100vw;
|
||||
}
|
||||
|
||||
.gen-tier-dialog .el-dialog__header {
|
||||
padding: 12px 14px;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.gen-tier-dialog .el-dialog__body {
|
||||
padding: 8px 12px 12px;
|
||||
box-sizing: border-box;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.gen-tier-dialog .el-dialog__footer {
|
||||
padding: 10px 12px 14px;
|
||||
}
|
||||
|
||||
.gen-tier-dialog .gen-tier-scroll {
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
.gen-tier-dialog .gen-rule {
|
||||
white-space: pre-line;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
background: var(--el-fill-color-light);
|
||||
padding: 12px 12px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 12px;
|
||||
font-size: 13px;
|
||||
line-height: 1.55;
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
|
||||
.gen-tier-dialog .gen-tier-block {
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||
}
|
||||
|
||||
.gen-tier-dialog .gen-tier-block:last-of-type {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.gen-tier-dialog .gen-tier-block-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
color: var(--el-text-color-primary);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.gen-tier-dialog .gen-tier-form .el-form-item {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.gen-tier-dialog .gen-tier-form.el-form--label-top .el-form-item__label {
|
||||
line-height: 1.4;
|
||||
margin-bottom: 6px;
|
||||
height: auto;
|
||||
white-space: normal;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.gen-tier-dialog .gen-tier-form .el-input-number {
|
||||
width: 100%;
|
||||
max-width: 320px;
|
||||
}
|
||||
|
||||
.gen-tier-dialog .gen-tier-footer-hint {
|
||||
margin-top: 4px;
|
||||
padding: 0 2px 8px;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
color: var(--el-text-color-secondary);
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.gen-tier-dialog .gen-tier-dialog-footer {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (max-width: 639px) {
|
||||
.gen-tier-dialog .gen-tier-form .el-input-number {
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,149 +0,0 @@
|
||||
<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.rewardWeight.quick Search Fields') })"
|
||||
></TableHeader>
|
||||
|
||||
<!-- 表格 -->
|
||||
<!-- 表格列有多种自定义渲染方式,比如自定义组件、具名插槽等,参见文档 -->
|
||||
<!-- 要使用 el-table 组件原有的属性,直接加在 Table 标签上即可 -->
|
||||
<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: 'game/rewardWeight',
|
||||
})
|
||||
|
||||
const { t } = useI18n()
|
||||
const tableRef = useTemplateRef('tableRef')
|
||||
const optButtons: OptButton[] = defaultOptButtons(['edit', 'delete'])
|
||||
|
||||
/**
|
||||
* baTable 内包含了表格的所有数据且数据具备响应性,然后通过 provide 注入给了后代组件
|
||||
*/
|
||||
const baTable = new baTableClass(
|
||||
new baTableApi('/admin/game.RewardWeight/'),
|
||||
{
|
||||
pk: 'id',
|
||||
column: [
|
||||
{ type: 'selection', align: 'center', operator: false },
|
||||
{ label: t('game.rewardWeight.id'), prop: 'id', align: 'center', width: 70, operator: 'RANGE', sortable: 'custom' },
|
||||
{
|
||||
label: t('game.rewardWeight.gamechannel__name'),
|
||||
prop: 'gameChannel.name',
|
||||
align: 'center',
|
||||
operatorPlaceholder: t('Fuzzy query'),
|
||||
render: 'tags',
|
||||
operator: 'LIKE',
|
||||
comSearchRender: 'string',
|
||||
},
|
||||
{
|
||||
label: t('game.rewardWeight.direction'),
|
||||
prop: 'direction',
|
||||
align: 'center',
|
||||
operator: 'eq',
|
||||
sortable: false,
|
||||
render: 'tag',
|
||||
replaceValue: { '0': t('game.rewardWeight.direction 0'), '1': t('game.rewardWeight.direction 1') },
|
||||
},
|
||||
{ label: t('game.rewardWeight.grid_number'), prop: 'grid_number', align: 'center', sortable: false, operator: 'RANGE' },
|
||||
{ label: t('game.rewardWeight.start_index'), prop: 'start_index', align: 'center', sortable: false, operator: 'RANGE' },
|
||||
{ label: t('game.rewardWeight.end_index'), prop: 'end_index', align: 'center', sortable: false, operator: 'RANGE' },
|
||||
{
|
||||
label: t('game.rewardWeight.ui_text'),
|
||||
prop: 'ui_text',
|
||||
align: 'center',
|
||||
operatorPlaceholder: t('Fuzzy query'),
|
||||
sortable: false,
|
||||
operator: 'LIKE',
|
||||
},
|
||||
{ label: t('game.rewardWeight.real_ev'), prop: 'real_ev', align: 'center', sortable: false, operator: 'RANGE' },
|
||||
{
|
||||
label: t('game.rewardWeight.tier'),
|
||||
prop: 'tier',
|
||||
align: 'center',
|
||||
operatorPlaceholder: t('Fuzzy query'),
|
||||
sortable: false,
|
||||
operator: 'LIKE',
|
||||
},
|
||||
{
|
||||
label: t('game.rewardWeight.type'),
|
||||
prop: 'type',
|
||||
align: 'center',
|
||||
operator: 'eq',
|
||||
sortable: false,
|
||||
render: 'tag',
|
||||
replaceValue: {
|
||||
'-2': t('game.rewardWeight.type -2'),
|
||||
'-1': t('game.rewardWeight.type -1'),
|
||||
'0': t('game.rewardWeight.type 0'),
|
||||
'1': t('game.rewardWeight.type 1'),
|
||||
'2': t('game.rewardWeight.type 2'),
|
||||
'3': t('game.rewardWeight.type 3'),
|
||||
},
|
||||
},
|
||||
{ label: t('game.rewardWeight.remark'), prop: 'remark', align: 'center', operatorPlaceholder: t('Fuzzy query'), operator: 'LIKE' },
|
||||
{ label: t('game.rewardWeight.weight'), prop: 'weight', align: 'center', sortable: false, operator: 'RANGE' },
|
||||
{
|
||||
label: t('game.rewardWeight.create_time'),
|
||||
prop: 'create_time',
|
||||
align: 'center',
|
||||
render: 'datetime',
|
||||
operator: 'RANGE',
|
||||
comSearchRender: 'datetime',
|
||||
sortable: 'custom',
|
||||
width: 160,
|
||||
timeFormat: 'yyyy-mm-dd hh:MM:ss',
|
||||
},
|
||||
{
|
||||
label: t('game.rewardWeight.update_time'),
|
||||
prop: 'update_time',
|
||||
align: 'center',
|
||||
render: 'datetime',
|
||||
operator: 'RANGE',
|
||||
comSearchRender: 'datetime',
|
||||
sortable: 'custom',
|
||||
width: 160,
|
||||
timeFormat: 'yyyy-mm-dd hh:MM:ss',
|
||||
},
|
||||
{ label: t('Operate'), align: 'center', width: 100, render: 'buttons', buttons: optButtons, operator: false },
|
||||
],
|
||||
dblClickNotEditColumn: [undefined],
|
||||
},
|
||||
{
|
||||
defaultItems: { direction: '0', type: '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>
|
||||
@@ -1,87 +0,0 @@
|
||||
<template>
|
||||
<!-- 对话框表单 -->
|
||||
<!-- 建议使用 Prettier 格式化代码 -->
|
||||
<!-- el-form 内可以混用 el-form-item、FormItem、ba-input 等输入组件 -->
|
||||
<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('game.rewardWeight.game_channel_id')" type="remoteSelect" v-model="baTable.form.items!.game_channel_id" prop="game_channel_id" :input-attr="{ pk: 'game_channel.id', field: 'name', remoteUrl: '/admin/game.Channel/index' }" :placeholder="t('Please select field', { field: t('game.rewardWeight.game_channel_id') })" />
|
||||
<FormItem :label="t('game.rewardWeight.direction')" type="radio" v-model="baTable.form.items!.direction" prop="direction" :input-attr="{ content: { '0': t('game.rewardWeight.direction 0'), '1': t('game.rewardWeight.direction 1') } }" :placeholder="t('Please select field', { field: t('game.rewardWeight.direction') })" />
|
||||
<FormItem :label="t('game.rewardWeight.grid_number')" type="number" v-model="baTable.form.items!.grid_number" prop="grid_number" :input-attr="{ step: 1 }" :placeholder="t('Please input field', { field: t('game.rewardWeight.grid_number') })" />
|
||||
<FormItem :label="t('game.rewardWeight.start_index')" type="number" v-model="baTable.form.items!.start_index" prop="start_index" :input-attr="{ step: 1 }" :placeholder="t('Please input field', { field: t('game.rewardWeight.start_index') })" />
|
||||
<FormItem :label="t('game.rewardWeight.end_index')" type="number" v-model="baTable.form.items!.end_index" prop="end_index" :input-attr="{ step: 1 }" :placeholder="t('Please input field', { field: t('game.rewardWeight.end_index') })" />
|
||||
<FormItem :label="t('game.rewardWeight.ui_text')" type="string" v-model="baTable.form.items!.ui_text" prop="ui_text" :placeholder="t('Please input field', { field: t('game.rewardWeight.ui_text') })" />
|
||||
<FormItem :label="t('game.rewardWeight.real_ev')" type="number" v-model="baTable.form.items!.real_ev" prop="real_ev" :input-attr="{ step: 1 }" :placeholder="t('Please input field', { field: t('game.rewardWeight.real_ev') })" />
|
||||
<FormItem :label="t('game.rewardWeight.tier')" type="string" v-model="baTable.form.items!.tier" prop="tier" :placeholder="t('Please input field', { field: t('game.rewardWeight.tier') })" />
|
||||
<FormItem :label="t('game.rewardWeight.type')" type="select" v-model="baTable.form.items!.type" prop="type" :input-attr="{ content: { '-2': t('game.rewardWeight.type -2'), '-1': t('game.rewardWeight.type -1'), '0': t('game.rewardWeight.type 0'), '1': t('game.rewardWeight.type 1'), '2': t('game.rewardWeight.type 2'), '3': t('game.rewardWeight.type 3') } }" :placeholder="t('Please select field', { field: t('game.rewardWeight.type') })" />
|
||||
<FormItem :label="t('game.rewardWeight.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.rewardWeight.remark') })" />
|
||||
<FormItem :label="t('game.rewardWeight.weight')" type="number" v-model="baTable.form.items!.weight" prop="weight" :input-attr="{ step: 1 }" :placeholder="t('Please input field', { field: t('game.rewardWeight.weight') })" />
|
||||
</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'
|
||||
import { buildValidatorData } from '/@/utils/validate'
|
||||
|
||||
const config = useConfig()
|
||||
const formRef = useTemplateRef('formRef')
|
||||
const baTable = inject('baTable') as baTableClass
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const rules: Partial<Record<string, FormItemRule[]>> = reactive({
|
||||
direction: [buildValidatorData({ name: 'required', title: t('game.rewardWeight.direction') })],
|
||||
grid_number: [buildValidatorData({ name: 'integer', title: t('game.rewardWeight.grid_number') }), buildValidatorData({ name: 'required', title: t('game.rewardWeight.grid_number') })],
|
||||
start_index: [buildValidatorData({ name: 'integer', title: t('game.rewardWeight.start_index') }), buildValidatorData({ name: 'required', title: t('game.rewardWeight.start_index') })],
|
||||
end_index: [buildValidatorData({ name: 'integer', title: t('game.rewardWeight.end_index') }), buildValidatorData({ name: 'required', title: t('game.rewardWeight.end_index') })],
|
||||
ui_text: [buildValidatorData({ name: 'required', title: t('game.rewardWeight.ui_text') })],
|
||||
real_ev: [buildValidatorData({ name: 'number', title: t('game.rewardWeight.real_ev') }), buildValidatorData({ name: 'required', title: t('game.rewardWeight.real_ev') })],
|
||||
tier: [buildValidatorData({ name: 'required', title: t('game.rewardWeight.tier') })],
|
||||
type: [buildValidatorData({ name: 'required', title: t('game.rewardWeight.type') })],
|
||||
weight: [buildValidatorData({ name: 'integer', title: t('game.rewardWeight.weight') })],
|
||||
create_time: [buildValidatorData({ name: 'date', title: t('game.rewardWeight.create_time') })],
|
||||
update_time: [buildValidatorData({ name: 'date', title: t('game.rewardWeight.update_time') })],
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
@@ -1,83 +0,0 @@
|
||||
<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>
|
||||
@@ -1,82 +0,0 @@
|
||||
<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>
|
||||
@@ -1,195 +0,0 @@
|
||||
<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') })"
|
||||
></TableHeader>
|
||||
|
||||
<!-- 表格 -->
|
||||
<!-- 表格列有多种自定义渲染方式,比如自定义组件、具名插槽等,参见文档 -->
|
||||
<!-- 要使用 el-table 组件原有的属性,直接加在 Table 标签上即可 -->
|
||||
<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 GameUserTicketJsonCell from './GameUserTicketJsonCell.vue'
|
||||
import GameUserWeightJsonCell from './GameUserWeightJsonCell.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'
|
||||
import { BIGWIN_WEIGHT_KEYS, TIER_WEIGHT_KEYS, jsonStringFromFixedKeys } from '/@/utils/gameWeightFixed'
|
||||
|
||||
defineOptions({
|
||||
name: 'game/user',
|
||||
})
|
||||
|
||||
const { t } = useI18n()
|
||||
const tableRef = useTemplateRef('tableRef')
|
||||
const optButtons: OptButton[] = defaultOptButtons(['edit', 'delete'])
|
||||
|
||||
/**
|
||||
* baTable 内包含了表格的所有数据且数据具备响应性,然后通过 provide 注入给了后代组件
|
||||
*/
|
||||
const baTable = new baTableClass(
|
||||
new baTableApi('/admin/game.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('game.user.username'),
|
||||
prop: 'username',
|
||||
align: 'center',
|
||||
operatorPlaceholder: t('Fuzzy query'),
|
||||
sortable: false,
|
||||
operator: 'LIKE',
|
||||
},
|
||||
{
|
||||
label: t('game.user.uuid'),
|
||||
prop: 'uuid',
|
||||
align: 'center',
|
||||
showOverflowTooltip: true,
|
||||
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.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'),
|
||||
prop: 'status',
|
||||
align: 'center',
|
||||
operator: 'eq',
|
||||
sortable: false,
|
||||
render: 'switch',
|
||||
replaceValue: { '0': t('game.user.status 0'), '1': t('game.user.status 1') },
|
||||
},
|
||||
{
|
||||
label: t('game.user.gamechannel__name'),
|
||||
prop: 'gameChannel.name',
|
||||
align: 'center',
|
||||
minWidth: 100,
|
||||
operatorPlaceholder: t('Fuzzy query'),
|
||||
render: 'tags',
|
||||
operator: 'LIKE',
|
||||
comSearchRender: 'string',
|
||||
},
|
||||
{
|
||||
label: t('game.user.admin__username'),
|
||||
prop: 'admin.username',
|
||||
align: 'center',
|
||||
minWidth: 90,
|
||||
effect: 'plain',
|
||||
operatorPlaceholder: t('Fuzzy query'),
|
||||
render: 'tags',
|
||||
operator: 'LIKE',
|
||||
comSearchRender: 'string',
|
||||
//修改tag颜色
|
||||
customRenderAttr: {
|
||||
tag: () => ({
|
||||
color: '#e8f3ff',
|
||||
style: { color: '#1677ff', borderColor: '#91caff' },
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
label: t('game.user.remark'),
|
||||
prop: 'remark',
|
||||
align: 'center',
|
||||
minWidth: 100,
|
||||
showOverflowTooltip: true,
|
||||
operatorPlaceholder: t('Fuzzy query'),
|
||||
},
|
||||
{
|
||||
label: t('game.user.create_time'),
|
||||
prop: 'create_time',
|
||||
align: 'center',
|
||||
render: 'datetime',
|
||||
operator: 'RANGE',
|
||||
comSearchRender: 'datetime',
|
||||
sortable: 'custom',
|
||||
width: 160,
|
||||
timeFormat: 'yyyy-mm-dd hh:MM:ss',
|
||||
},
|
||||
{
|
||||
label: t('game.user.update_time'),
|
||||
prop: 'update_time',
|
||||
align: 'center',
|
||||
render: 'datetime',
|
||||
operator: 'RANGE',
|
||||
comSearchRender: 'datetime',
|
||||
sortable: 'custom',
|
||||
width: 160,
|
||||
timeFormat: 'yyyy-mm-dd hh:MM:ss',
|
||||
},
|
||||
{ label: t('Operate'), align: 'center', width: 80, render: 'buttons', buttons: optButtons, operator: false, fixed: 'right' },
|
||||
],
|
||||
dblClickNotEditColumn: [undefined, 'status'],
|
||||
},
|
||||
{
|
||||
defaultItems: {
|
||||
status: '1',
|
||||
tier_weight: jsonStringFromFixedKeys(TIER_WEIGHT_KEYS, {}),
|
||||
bigwin_weight: jsonStringFromFixedKeys(BIGWIN_WEIGHT_KEYS, {}),
|
||||
ticket_count: null,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
provide('baTable', baTable)
|
||||
|
||||
onMounted(() => {
|
||||
baTable.table.ref = tableRef.value
|
||||
baTable.mount()
|
||||
baTable.getData()?.then(() => {
|
||||
baTable.initSort()
|
||||
baTable.dragSort()
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
@@ -1,654 +0,0 @@
|
||||
<template>
|
||||
<!-- 对话框表单 -->
|
||||
<!-- 建议使用 Prettier 格式化代码 -->
|
||||
<!-- el-form 内可以混用 el-form-item、FormItem、ba-input 等输入组件 -->
|
||||
<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('game.user.username')"
|
||||
type="string"
|
||||
v-model="baTable.form.items!.username"
|
||||
prop="username"
|
||||
:placeholder="t('Please input field', { field: t('game.user.username') })"
|
||||
/>
|
||||
<FormItem
|
||||
:label="t('game.user.password')"
|
||||
type="password"
|
||||
v-model="baTable.form.items!.password"
|
||||
prop="password"
|
||||
:placeholder="t('Please input field', { field: t('game.user.password') })"
|
||||
/>
|
||||
<FormItem
|
||||
:label="t('game.user.phone')"
|
||||
type="string"
|
||||
v-model="baTable.form.items!.phone"
|
||||
prop="phone"
|
||||
:placeholder="t('Please input field', { field: t('game.user.phone') })"
|
||||
/>
|
||||
<FormItem
|
||||
:label="t('game.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') })"
|
||||
/>
|
||||
<FormItem
|
||||
:label="t('game.user.coin')"
|
||||
type="number"
|
||||
v-model="baTable.form.items!.coin"
|
||||
prop="coin"
|
||||
:input-attr="{ step: 1 }"
|
||||
: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
|
||||
:label="t('game.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') } }"
|
||||
/>
|
||||
<el-form-item :label="t('game.user.game_channel_id')" prop="admin_id">
|
||||
<el-tree-select
|
||||
v-model="adminIdForTree"
|
||||
class="w100"
|
||||
clearable
|
||||
filterable
|
||||
:data="channelAdminTree"
|
||||
:props="treeProps"
|
||||
:render-after-expand="false"
|
||||
:placeholder="t('Please select field', { field: t('game.user.admin_id') })"
|
||||
@change="onAdminTreeChange"
|
||||
/>
|
||||
</el-form-item>
|
||||
</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 { computed, inject, onMounted, reactive, ref, useTemplateRef, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import FormItem from '/@/components/formItem/index.vue'
|
||||
import { useAdminInfo } from '/@/stores/adminInfo'
|
||||
import { useConfig } from '/@/stores/config'
|
||||
import type baTableClass from '/@/utils/baTable'
|
||||
import { buildValidatorData, regularPassword } from '/@/utils/validate'
|
||||
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 formRef = useTemplateRef('formRef')
|
||||
const baTable = inject('baTable') as baTableClass
|
||||
const adminInfo = useAdminInfo()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const isSuperAdmin = computed(() => adminInfo.super === true)
|
||||
|
||||
type TreeNode = {
|
||||
value: string
|
||||
label: string
|
||||
disabled?: boolean
|
||||
children?: TreeNode[]
|
||||
channel_id?: number
|
||||
is_leaf?: boolean
|
||||
}
|
||||
|
||||
const channelAdminTree = ref<TreeNode[]>([])
|
||||
const adminIdToChannelId = ref<Record<string, number>>({})
|
||||
|
||||
const treeProps = {
|
||||
value: 'value',
|
||||
label: 'label',
|
||||
children: 'children',
|
||||
disabled: 'disabled',
|
||||
}
|
||||
|
||||
/** 与 adminTree 叶子 value(字符串)一致,避免 number 与 string 不一致导致 el-tree-select 只显示原始 ID */
|
||||
const adminIdForTree = computed<string | undefined>({
|
||||
get() {
|
||||
const v = baTable.form.items?.admin_id
|
||||
if (v === undefined || v === null || v === '') return undefined
|
||||
return String(v)
|
||||
},
|
||||
set(v: string | number | undefined | null) {
|
||||
if (!baTable.form.items) return
|
||||
if (v === undefined || v === null || v === '') {
|
||||
baTable.form.items.admin_id = undefined
|
||||
return
|
||||
}
|
||||
const n = Number(v)
|
||||
if (Number.isFinite(n)) {
|
||||
baTable.form.items.admin_id = n
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const loadChannelAdminTree = async () => {
|
||||
const res = await createAxios({
|
||||
url: '/admin/game.Channel/adminTree',
|
||||
method: 'get',
|
||||
})
|
||||
const list = (res.data?.list ?? []) as TreeNode[]
|
||||
channelAdminTree.value = list
|
||||
|
||||
const map: Record<string, number> = {}
|
||||
const walk = (nodes: TreeNode[]) => {
|
||||
for (const n of nodes) {
|
||||
if (n.children && n.children.length) {
|
||||
walk(n.children)
|
||||
} else if (n.is_leaf && n.channel_id !== undefined) {
|
||||
map[n.value] = n.channel_id
|
||||
}
|
||||
}
|
||||
}
|
||||
walk(list)
|
||||
adminIdToChannelId.value = map
|
||||
}
|
||||
|
||||
const onAdminTreeChange = (val: string | number | null) => {
|
||||
if (val === null || val === undefined || val === '') {
|
||||
return
|
||||
}
|
||||
const key = typeof val === 'number' ? String(val) : String(val)
|
||||
const channelId = adminIdToChannelId.value[key]
|
||||
if (channelId !== undefined) {
|
||||
baTable.form.items!.game_channel_id = channelId
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
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 })
|
||||
}
|
||||
if (body.length === 0) {
|
||||
return ''
|
||||
}
|
||||
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 = null
|
||||
return
|
||||
}
|
||||
const json = ticketRowsToJsonString(ticketRows.value)
|
||||
items.ticket_count = json === '' ? null : json
|
||||
}
|
||||
|
||||
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(
|
||||
() => baTable.form.items?.admin_id,
|
||||
(val) => {
|
||||
if (val === undefined || val === null || val === '') return
|
||||
onAdminTreeChange(typeof val === 'number' ? val : Number(val))
|
||||
}
|
||||
)
|
||||
|
||||
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 operate = baTable.form.operate
|
||||
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 (!regularPassword(v)) return callback(new Error(t('validate.Please enter the correct password')))
|
||||
return callback()
|
||||
}
|
||||
|
||||
// 编辑:可空;非空则校验格式
|
||||
if (!v) return callback()
|
||||
if (!regularPassword(v)) return callback(new Error(t('validate.Please enter the correct password')))
|
||||
return callback()
|
||||
}
|
||||
|
||||
const rules: Partial<Record<string, FormItemRule[]>> = reactive({
|
||||
username: [buildValidatorData({ name: 'required', title: t('game.user.username') })],
|
||||
password: [{ validator: validatorGameUserPassword, trigger: 'blur' }],
|
||||
phone: [buildValidatorData({ name: 'required', title: t('game.user.phone') })],
|
||||
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') })],
|
||||
create_time: [buildValidatorData({ name: 'date', title: t('game.user.create_time') })],
|
||||
update_time: [buildValidatorData({ name: 'date', title: t('game.user.update_time') })],
|
||||
})
|
||||
</script>
|
||||
|
||||
<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>
|
||||
@@ -6,7 +6,7 @@
|
||||
<!-- 自定义按钮请使用插槽,甚至公共搜索也可以使用具名插槽渲染,参见文档 -->
|
||||
<TableHeader
|
||||
:buttons="['refresh', 'add', 'edit', 'delete', 'comSearch', 'quickSearch', 'columnDisplay']"
|
||||
:quick-search-placeholder="t('Quick search placeholder', { fields: t('game.config.quick Search Fields') })"
|
||||
:quick-search-placeholder="t('Quick search placeholder', { fields: t('mall.player.quickSearchFields') })"
|
||||
></TableHeader>
|
||||
|
||||
<!-- 表格 -->
|
||||
@@ -23,7 +23,6 @@
|
||||
import { onMounted, provide, useTemplateRef } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import PopupForm from './popupForm.vue'
|
||||
import GameConfigValueCell from './GameConfigValueCell.vue'
|
||||
import { baTableApi } from '/@/api/common'
|
||||
import { defaultOptButtons } from '/@/components/table'
|
||||
import TableHeader from '/@/components/table/header/index.vue'
|
||||
@@ -31,7 +30,7 @@ import Table from '/@/components/table/index.vue'
|
||||
import baTableClass from '/@/utils/baTable'
|
||||
|
||||
defineOptions({
|
||||
name: 'game/config',
|
||||
name: 'mall/player',
|
||||
})
|
||||
|
||||
const { t } = useI18n()
|
||||
@@ -42,83 +41,22 @@ const optButtons: OptButton[] = defaultOptButtons(['edit', 'delete'])
|
||||
* baTable 内包含了表格的所有数据且数据具备响应性,然后通过 provide 注入给了后代组件
|
||||
*/
|
||||
const baTable = new baTableClass(
|
||||
new baTableApi('/admin/game.Config/'),
|
||||
new baTableApi('/admin/mall.Player/'),
|
||||
{
|
||||
pk: 'ID',
|
||||
pk: 'id',
|
||||
column: [
|
||||
{ type: 'selection', align: 'center', operator: false },
|
||||
{ label: t('game.config.ID'), prop: 'ID', align: 'center', width: 70, operator: 'RANGE', sortable: 'custom' },
|
||||
{ label: t('mall.player.id'), prop: 'id', align: 'center', width: 70, operator: 'RANGE', sortable: 'custom' },
|
||||
{
|
||||
label: t('game.config.channel_id'),
|
||||
prop: 'channel_id',
|
||||
align: 'center',
|
||||
show: false,
|
||||
width: 88,
|
||||
operator: 'RANGE',
|
||||
sortable: 'custom',
|
||||
},
|
||||
{
|
||||
label: t('game.config.channel__name'),
|
||||
prop: 'channel.name',
|
||||
align: 'center',
|
||||
minWidth: 100,
|
||||
sortable: 'true',
|
||||
operatorPlaceholder: t('Fuzzy query'),
|
||||
render: 'tags',
|
||||
operator: 'LIKE',
|
||||
comSearchRender: 'string',
|
||||
},
|
||||
{
|
||||
label: t('game.config.group'),
|
||||
prop: 'group',
|
||||
label: t('mall.player.username'),
|
||||
prop: 'username',
|
||||
align: 'center',
|
||||
operatorPlaceholder: t('Fuzzy query'),
|
||||
sortable: false,
|
||||
operator: 'LIKE',
|
||||
},
|
||||
{
|
||||
label: t('game.config.name'),
|
||||
prop: 'name',
|
||||
align: 'center',
|
||||
minWidth: 180,
|
||||
operatorPlaceholder: t('Fuzzy query'),
|
||||
render: 'tag',
|
||||
sortable: false,
|
||||
operator: 'LIKE',
|
||||
},
|
||||
{
|
||||
label: t('game.config.title'),
|
||||
prop: 'title',
|
||||
align: 'center',
|
||||
minWidth: 85,
|
||||
operatorPlaceholder: t('Fuzzy query'),
|
||||
sortable: false,
|
||||
operator: 'LIKE',
|
||||
},
|
||||
{
|
||||
label: t('game.config.value'),
|
||||
prop: 'value',
|
||||
align: 'center',
|
||||
minWidth: 220,
|
||||
sortable: false,
|
||||
operator: 'LIKE',
|
||||
comSearchRender: 'string',
|
||||
render: 'customRender',
|
||||
customRender: GameConfigValueCell,
|
||||
},
|
||||
{
|
||||
label: t('game.config.instantiation'),
|
||||
prop: 'instantiation',
|
||||
align: 'center',
|
||||
custom: { 0: 'error', 1: 'primary' },
|
||||
operator: 'RANGE',
|
||||
sortable: false,
|
||||
render: 'tag',
|
||||
replaceValue: { '0': t('game.config.instantiation 0'), '1': t('game.config.instantiation 1') },
|
||||
},
|
||||
{ label: t('game.config.sort'), prop: 'sort', align: 'center', sortable: false, operator: 'RANGE' },
|
||||
{
|
||||
label: t('game.config.create_time'),
|
||||
label: t('mall.player.create_time'),
|
||||
prop: 'create_time',
|
||||
align: 'center',
|
||||
render: 'datetime',
|
||||
@@ -129,7 +67,7 @@ const baTable = new baTableClass(
|
||||
timeFormat: 'yyyy-mm-dd hh:MM:ss',
|
||||
},
|
||||
{
|
||||
label: t('game.config.update_time'),
|
||||
label: t('mall.player.update_time'),
|
||||
prop: 'update_time',
|
||||
align: 'center',
|
||||
render: 'datetime',
|
||||
@@ -139,13 +77,13 @@ const baTable = new baTableClass(
|
||||
width: 160,
|
||||
timeFormat: 'yyyy-mm-dd hh:MM:ss',
|
||||
},
|
||||
{ label: t('mall.player.score'), prop: 'score', align: 'center', sortable: false, operator: 'RANGE' },
|
||||
{ label: t('Operate'), align: 'center', width: 100, render: 'buttons', buttons: optButtons, operator: false },
|
||||
],
|
||||
dblClickNotEditColumn: [undefined, 'instantiation'],
|
||||
defaultOrder: { prop: 'channel_id', order: 'desc' },
|
||||
dblClickNotEditColumn: [undefined],
|
||||
},
|
||||
{
|
||||
defaultItems: { sort: 100 },
|
||||
defaultItems: {},
|
||||
}
|
||||
)
|
||||
|
||||
@@ -30,44 +30,26 @@
|
||||
:rules="rules"
|
||||
>
|
||||
<FormItem
|
||||
:label="t('game.channel.code')"
|
||||
:label="t('mall.player.username')"
|
||||
type="string"
|
||||
v-model="baTable.form.items!.code"
|
||||
prop="code"
|
||||
:placeholder="t('Please input field', { field: t('game.channel.code') })"
|
||||
v-model="baTable.form.items!.username"
|
||||
prop="username"
|
||||
:placeholder="t('Please input field', { field: t('mall.player.username') })"
|
||||
/>
|
||||
<FormItem
|
||||
:label="t('game.channel.name')"
|
||||
type="string"
|
||||
v-model="baTable.form.items!.name"
|
||||
prop="name"
|
||||
:placeholder="t('Please input field', { field: t('game.channel.name') })"
|
||||
:label="t('mall.player.password')"
|
||||
type="password"
|
||||
v-model="baTable.form.items!.password"
|
||||
prop="password"
|
||||
:placeholder="t('Please input field', { field: t('mall.player.password') })"
|
||||
/>
|
||||
<FormItem
|
||||
:label="t('game.channel.status')"
|
||||
type="switch"
|
||||
v-model="baTable.form.items!.status"
|
||||
prop="status"
|
||||
:input-attr="{ content: { '0': t('game.channel.status 0'), '1': t('game.channel.status 1') } }"
|
||||
/>
|
||||
<FormItem
|
||||
:label="t('game.channel.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.channel.remark') })"
|
||||
/>
|
||||
<FormItem
|
||||
v-if="isSuperAdmin"
|
||||
:label="t('game.channel.admin_id')"
|
||||
type="remoteSelect"
|
||||
v-model="baTable.form.items!.admin_id"
|
||||
prop="admin_id"
|
||||
:input-attr="{ pk: 'admin.id', field: 'username', remoteUrl: '/admin/auth.Admin/index', params: { top_group: '1' } }"
|
||||
:placeholder="t('Please select field', { field: t('game.channel.admin_id') })"
|
||||
:label="t('mall.player.score')"
|
||||
type="number"
|
||||
v-model="baTable.form.items!.score"
|
||||
prop="score"
|
||||
:input-attr="{ step: 1 }"
|
||||
:placeholder="t('Please input field', { field: t('mall.player.score') })"
|
||||
/>
|
||||
</el-form>
|
||||
</div>
|
||||
@@ -85,34 +67,43 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { FormItemRule } from 'element-plus'
|
||||
import { computed, inject, useTemplateRef } from 'vue'
|
||||
import { inject, reactive, useTemplateRef } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import FormItem from '/@/components/formItem/index.vue'
|
||||
import { useConfig } from '/@/stores/config'
|
||||
import { useAdminInfo } from '/@/stores/adminInfo'
|
||||
import type baTableClass from '/@/utils/baTable'
|
||||
import { buildValidatorData } from '/@/utils/validate'
|
||||
import { buildValidatorData, regularPassword } from '/@/utils/validate'
|
||||
|
||||
const config = useConfig()
|
||||
const formRef = useTemplateRef('formRef')
|
||||
const baTable = inject('baTable') as baTableClass
|
||||
const adminInfo = useAdminInfo()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const isSuperAdmin = computed(() => adminInfo.super === true)
|
||||
|
||||
const rules = computed<Partial<Record<string, FormItemRule[]>>>(() => ({
|
||||
code: [buildValidatorData({ name: 'required', title: t('game.channel.code') })],
|
||||
name: [buildValidatorData({ name: 'required', title: t('game.channel.name') })],
|
||||
user_count: [buildValidatorData({ name: 'integer', title: t('game.channel.user_count') })],
|
||||
profit_amount: [buildValidatorData({ name: 'float', title: t('game.channel.profit_amount') })],
|
||||
admin_id: isSuperAdmin.value
|
||||
? [buildValidatorData({ name: 'required', title: t('game.channel.admin_id') })]
|
||||
: [],
|
||||
create_time: [buildValidatorData({ name: 'date', title: t('game.channel.create_time') })],
|
||||
update_time: [buildValidatorData({ name: 'date', title: t('game.channel.update_time') })],
|
||||
}))
|
||||
const rules: Partial<Record<string, FormItemRule[]>> = reactive({
|
||||
username: [buildValidatorData({ name: 'required', title: t('mall.player.username') })],
|
||||
password: [
|
||||
{
|
||||
validator: (_rule: unknown, val: string, callback: (error?: Error) => void) => {
|
||||
if (baTable.form.operate === 'Add') {
|
||||
if (!val) {
|
||||
return callback(new Error(t('Please input field', { field: t('mall.player.password') })))
|
||||
}
|
||||
} else {
|
||||
if (!val) {
|
||||
return callback()
|
||||
}
|
||||
}
|
||||
if (!regularPassword(val)) {
|
||||
return callback(new Error(t('validate.Please enter the correct password')))
|
||||
}
|
||||
return callback()
|
||||
},
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
score: [buildValidatorData({ name: 'number', title: t('mall.player.score') })],
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
Reference in New Issue
Block a user