Compare commits
5 Commits
master-gam
...
b0d25b30f9
| Author | SHA1 | Date | |
|---|---|---|---|
| b0d25b30f9 | |||
| ac30d8d1c9 | |||
| c4c17180ee | |||
| 85da91e3f3 | |||
| a0f14015ed |
13
.env-example
13
.env-example
@@ -7,12 +7,17 @@ APP_DEFAULT_TIMEZONE = Asia/Shanghai
|
|||||||
|
|
||||||
# 语言
|
# 语言
|
||||||
LANG_DEFAULT_LANG = zh-cn
|
LANG_DEFAULT_LANG = zh-cn
|
||||||
# Database
|
|
||||||
|
# 数据库(config/thinkorm.php/database.php)
|
||||||
|
DATABASE_DRIVER = mysql
|
||||||
DATABASE_TYPE = mysql
|
DATABASE_TYPE = mysql
|
||||||
DATABASE_HOSTNAME = 127.0.0.1
|
DATABASE_HOSTNAME = 127.0.0.1
|
||||||
DATABASE_DATABASE = webman-buildadmin-dafuweng
|
DATABASE_DATABASE = buildadmin-webman
|
||||||
DATABASE_USERNAME = webman-buildadmin-dafuweng
|
DATABASE_USERNAME = buildadmin-webman
|
||||||
DATABASE_PASSWORD = 6dzMaX32Xdsc4DjS
|
DATABASE_PASSWORD = 123456
|
||||||
DATABASE_HOSTPORT = 3306
|
DATABASE_HOSTPORT = 3306
|
||||||
DATABASE_CHARSET = utf8mb4
|
DATABASE_CHARSET = utf8mb4
|
||||||
DATABASE_PREFIX =
|
DATABASE_PREFIX =
|
||||||
|
|
||||||
|
# 缓存(config/cache.php)
|
||||||
|
CACHE_DRIVER = file
|
||||||
|
|||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,8 +1,8 @@
|
|||||||
# 通过 Git 部署项目至线上时建议删除的忽略规则
|
# 通过 Git 部署项目至线上时建议删除的忽略规则
|
||||||
/vendor
|
/vendor
|
||||||
/modules
|
/modules
|
||||||
/public/*.lock
|
#/public/*.lock
|
||||||
/public/index.html
|
#/public/index.html
|
||||||
/public/assets
|
/public/assets
|
||||||
|
|
||||||
# 通过 Git 部署项目至线上时可以考虑删除的忽略规则
|
# 通过 Git 部署项目至线上时可以考虑删除的忽略规则
|
||||||
|
|||||||
24
README.md
24
README.md
@@ -186,30 +186,6 @@ php webman migrate
|
|||||||
|
|
||||||
> 注意:前端通过 Vite 代理将 `/api`、`/admin`、`/install` 转发到后端 8787 端口,请勿直接访问 8787 端口的前端页面,否则可能出现 404。
|
> 注意:前端通过 Vite 代理将 `/api`、`/admin`、`/install` 转发到后端 8787 端口,请勿直接访问 8787 端口的前端页面,否则可能出现 404。
|
||||||
|
|
||||||
### 5.6 生产环境 Nginx(反向代理 Webman)
|
|
||||||
|
|
||||||
部署到服务器时,若使用 **Nginx** 作为站点入口,需将请求转发到本机 **Webman** 进程(默认监听端口与 `config/process.php` 中 `listen` 一致,一般为 `8787`,反代目标使用 `127.0.0.1:8787`)。
|
|
||||||
|
|
||||||
在站点 **`server { }`** 块中可增加如下写法:**先由 Nginx 根据 `root` 判断是否存在对应静态文件;不存在则转发到 Webman**(`root` 建议指向项目 `public` 目录)。
|
|
||||||
|
|
||||||
```nginx
|
|
||||||
location ^~ / {
|
|
||||||
proxy_set_header Host $http_host;
|
|
||||||
proxy_set_header X-Forwarded-For $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Connection "";
|
|
||||||
if (!-f $request_filename) {
|
|
||||||
proxy_pass http://127.0.0.1:8787;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
修改配置后执行 `nginx -t` 校验,再重载 Nginx;并确保 Webman 已启动(如 `php start.php start -d`)。
|
|
||||||
|
|
||||||
若前端与接口为**不同域名**(跨域),除反代外还需保证 **HTTPS 证书与域名一致**,以及后端 **CORS / 预检(OPTIONS)** 与前端请求头(如 `think-lang`、`server` 等)配置一致,否则浏览器会报跨域相关错误。
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 六、路由说明
|
## 六、路由说明
|
||||||
|
|||||||
@@ -21,10 +21,7 @@ class Admin extends Backend
|
|||||||
|
|
||||||
protected array|string $quickSearchField = ['username', 'nickname'];
|
protected array|string $quickSearchField = ['username', 'nickname'];
|
||||||
|
|
||||||
/**
|
protected string|int|bool $dataLimit = 'allAuthAndOthers';
|
||||||
* 开启数据范围;具体范围见重写的 getDataLimitAdminIds(角色组树:仅本人 + 下级组内管理员)
|
|
||||||
*/
|
|
||||||
protected bool|string|int $dataLimit = true;
|
|
||||||
|
|
||||||
protected string $dataLimitField = 'id';
|
protected string $dataLimitField = 'id';
|
||||||
|
|
||||||
@@ -34,17 +31,6 @@ class Admin extends Backend
|
|||||||
return null;
|
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
|
public function index(Request $request): Response
|
||||||
{
|
{
|
||||||
$response = $this->initializeBackend($request);
|
$response = $this->initializeBackend($request);
|
||||||
@@ -56,23 +42,11 @@ class Admin extends Backend
|
|||||||
}
|
}
|
||||||
|
|
||||||
list($where, $alias, $limit, $order) = $this->queryBuilder();
|
list($where, $alias, $limit, $order) = $this->queryBuilder();
|
||||||
$query = $this->model
|
$res = $this->model
|
||||||
->withoutField('login_failure,password,salt')
|
->withoutField('login_failure,password,salt')
|
||||||
->withJoin($this->withJoinTable, $this->withJoinType)
|
->withJoin($this->withJoinTable, $this->withJoinType)
|
||||||
->alias($alias)
|
->alias($alias)
|
||||||
->where($where);
|
->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
|
|
||||||
->order($order)
|
->order($order)
|
||||||
->paginate($limit);
|
->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
|
public function add(Request $request): Response
|
||||||
{
|
{
|
||||||
$response = $this->initializeBackend($request);
|
$response = $this->initializeBackend($request);
|
||||||
@@ -371,12 +275,9 @@ class Admin extends Backend
|
|||||||
if ($this->auth->isSuperAdmin()) {
|
if ($this->auth->isSuperAdmin()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
$allowedGroupIds = array_values(array_unique(array_merge(
|
$authGroups = $this->auth->getAllAuthGroups('allAuthAndOthers');
|
||||||
Db::name('admin_group_access')->where('uid', $this->auth->id)->column('group_id'),
|
|
||||||
$this->auth->getAdminChildGroups()
|
|
||||||
)));
|
|
||||||
foreach ($groups as $group) {
|
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!'));
|
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;
|
namespace app\admin\controller\auth;
|
||||||
|
|
||||||
|
use Throwable;
|
||||||
use app\common\controller\Backend;
|
use app\common\controller\Backend;
|
||||||
use app\admin\model\AdminLog as AdminLogModel;
|
use app\admin\model\AdminLog as AdminLogModel;
|
||||||
use support\Response;
|
use support\Response;
|
||||||
@@ -35,10 +36,7 @@ class AdminLog extends Backend
|
|||||||
|
|
||||||
list($where, $alias, $limit, $order) = $this->queryBuilder();
|
list($where, $alias, $limit, $order) = $this->queryBuilder();
|
||||||
if (!$this->auth->isSuperAdmin()) {
|
if (!$this->auth->isSuperAdmin()) {
|
||||||
$scopeIds = $this->auth->getSelfAndSubordinateAdminIds();
|
$where[] = ['admin_id', '=', $this->auth->id];
|
||||||
if ($scopeIds !== []) {
|
|
||||||
$where[] = ['admin_id', 'in', $scopeIds];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
$res = $this->model
|
$res = $this->model
|
||||||
->withJoin($this->withJoinTable, $this->withJoinType)
|
->withJoin($this->withJoinTable, $this->withJoinType)
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ use Webman\Http\Request;
|
|||||||
|
|
||||||
class Group extends Backend
|
class Group extends Backend
|
||||||
{
|
{
|
||||||
|
protected string $authMethod = 'allAuthAndOthers';
|
||||||
|
|
||||||
protected ?object $model = null;
|
protected ?object $model = null;
|
||||||
|
|
||||||
protected string|array $preExcludeFields = ['create_time', 'update_time'];
|
protected string|array $preExcludeFields = ['create_time', 'update_time'];
|
||||||
@@ -80,9 +82,6 @@ class Group extends Backend
|
|||||||
$rulesRes = $this->handleRules($data);
|
$rulesRes = $this->handleRules($data);
|
||||||
if ($rulesRes instanceof Response) return $rulesRes;
|
if ($rulesRes instanceof Response) return $rulesRes;
|
||||||
|
|
||||||
$pidRes = $this->validateGroupParentId($data['pid'] ?? null);
|
|
||||||
if ($pidRes instanceof Response) return $pidRes;
|
|
||||||
|
|
||||||
$result = false;
|
$result = false;
|
||||||
$this->model->startTrans();
|
$this->model->startTrans();
|
||||||
try {
|
try {
|
||||||
@@ -145,11 +144,6 @@ class Group extends Backend
|
|||||||
$rulesRes = $this->handleRules($data);
|
$rulesRes = $this->handleRules($data);
|
||||||
if ($rulesRes instanceof Response) return $rulesRes;
|
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;
|
$result = false;
|
||||||
$this->model->startTrans();
|
$this->model->startTrans();
|
||||||
try {
|
try {
|
||||||
@@ -300,6 +294,8 @@ class Group extends Backend
|
|||||||
$pk = $this->model->getPk();
|
$pk = $this->model->getPk();
|
||||||
$initKey = $request->get('initKey') ?? $pk;
|
$initKey = $request->get('initKey') ?? $pk;
|
||||||
|
|
||||||
|
$absoluteAuth = $request->get('absoluteAuth') ?? false;
|
||||||
|
|
||||||
if ($this->keyword) {
|
if ($this->keyword) {
|
||||||
$keyword = explode(' ', $this->keyword);
|
$keyword = explode(' ', $this->keyword);
|
||||||
foreach ($keyword as $item) {
|
foreach ($keyword as $item) {
|
||||||
@@ -312,14 +308,11 @@ class Group extends Backend
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!$this->auth->isSuperAdmin()) {
|
if (!$this->auth->isSuperAdmin()) {
|
||||||
$descendantIds = $this->auth->getAdminChildGroups();
|
$authGroups = $this->auth->getAllAuthGroups($this->authMethod, $where);
|
||||||
// 本人所在组 + 树形下级;不含同级、不含其它分支(与 getAllAuthGroups 的「权限多寡」脱钩)
|
if (!$absoluteAuth) {
|
||||||
$visibleIds = array_values(array_unique(array_merge($this->adminGroups, $descendantIds)));
|
$authGroups = array_merge($this->adminGroups, $authGroups);
|
||||||
if ($visibleIds === []) {
|
|
||||||
$where[] = ['id', '=', -1];
|
|
||||||
} else {
|
|
||||||
$where[] = ['id', 'in', $visibleIds];
|
|
||||||
}
|
}
|
||||||
|
$where[] = ['id', 'in', $authGroups];
|
||||||
}
|
}
|
||||||
$data = $this->model->where($where)->select()->toArray();
|
$data = $this->model->where($where)->select()->toArray();
|
||||||
|
|
||||||
@@ -344,43 +337,9 @@ class Group extends Backend
|
|||||||
|
|
||||||
private function checkAuth($groupId): ?Response
|
private function checkAuth($groupId): ?Response
|
||||||
{
|
{
|
||||||
if ($this->auth->isSuperAdmin()) {
|
$authGroups = $this->auth->getAllAuthGroups($this->authMethod, []);
|
||||||
return null;
|
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~'));
|
||||||
$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'));
|
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -581,13 +581,6 @@ class Crud extends Backend
|
|||||||
|
|
||||||
private function parseModelMethods($field, &$modelData): void
|
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') {
|
if (($field['designType'] ?? '') == 'array') {
|
||||||
$modelData['fieldType'][$field['name']] = 'json';
|
$modelData['fieldType'][$field['name']] = 'json';
|
||||||
} elseif (!in_array($field['name'], ['create_time', 'update_time', 'updatetime', 'createtime'])
|
} elseif (!in_array($field['name'], ['create_time', 'update_time', 'updatetime', 'createtime'])
|
||||||
|
|||||||
@@ -1,631 +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 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);
|
|
||||||
} 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 新建渠道后:将 channel_id=0 的全局默认游戏配置复制一份,channel_id 指向新渠道主键
|
|
||||||
*
|
|
||||||
* @param int|string $newChannelId 新建 game_channel.id
|
|
||||||
*/
|
|
||||||
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,183 +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 RewardConfig extends Backend
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* GameRewardConfig模型对象
|
|
||||||
* @var object|null
|
|
||||||
* @phpstan-var \app\common\model\GameRewardConfig|null
|
|
||||||
*/
|
|
||||||
protected ?object $model = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 数据范围:非超管仅本人 + 下级管理员负责的渠道
|
|
||||||
*/
|
|
||||||
protected bool|string|int $dataLimit = 'parent';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 列表/删除按渠道字段限制(实际值为渠道 ID)
|
|
||||||
*/
|
|
||||||
protected string $dataLimitField = 'game_channel_id';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 表无 admin_id,勿自动写入
|
|
||||||
*/
|
|
||||||
protected bool $dataLimitFieldAutoFill = false;
|
|
||||||
|
|
||||||
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\GameRewardConfig();
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 将可访问管理员 ID 转换为可访问渠道 ID
|
|
||||||
*
|
|
||||||
* @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' && !$this->auth->isSuperAdmin()) {
|
|
||||||
$allowedChannelIds = $this->getDataLimitAdminIds();
|
|
||||||
$cid = $this->request->post('game_channel_id');
|
|
||||||
if ($cid === null || $cid === '' || ($allowedChannelIds !== [] && !in_array($cid, $allowedChannelIds))) {
|
|
||||||
return $this->error(__('You have no permission'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return parent::_add();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 编辑:非超管锁定渠道,不允许跨渠道改写
|
|
||||||
* @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[$this->dataLimitField] = $row[$this->dataLimitField];
|
|
||||||
}
|
|
||||||
|
|
||||||
$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(['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',
|
'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.',
|
'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',
|
'Unknown' => 'Unknown',
|
||||||
'Global default' => 'Global default (channel_id=0)',
|
|
||||||
'Super administrator' => 'Super administrator',
|
'Super administrator' => 'Super administrator',
|
||||||
'No permission' => 'No permission',
|
'No permission' => 'No permission',
|
||||||
'%first% etc. %count% items' => '%first% etc. %count% items',
|
'%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',
|
'%d records and files have been deleted' => '%d records and files have been deleted',
|
||||||
'Please input correct username' => 'Please enter the correct username',
|
'Please input correct username' => 'Please enter the correct username',
|
||||||
'Group Name Arr' => 'Group Name Arr',
|
'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~',
|
'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!',
|
'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~',
|
'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).',
|
'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.',
|
||||||
'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.',
|
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ return [
|
|||||||
'Topic format error' => '上传存储子目录格式错误!',
|
'Topic format error' => '上传存储子目录格式错误!',
|
||||||
'Driver %s not supported' => '不支持的驱动:%s',
|
'Driver %s not supported' => '不支持的驱动:%s',
|
||||||
'Unknown' => '未知',
|
'Unknown' => '未知',
|
||||||
'Global default' => '全局默认(channel_id=0)',
|
|
||||||
// 权限类语言包-s
|
// 权限类语言包-s
|
||||||
'Super administrator' => '超级管理员',
|
'Super administrator' => '超级管理员',
|
||||||
'No permission' => '无权限',
|
'No permission' => '无权限',
|
||||||
@@ -115,16 +114,4 @@ return [
|
|||||||
'%d records and files have been deleted' => '已删除%d条记录和文件',
|
'%d records and files have been deleted' => '已删除%d条记录和文件',
|
||||||
'Please input correct username' => '请输入正确的用户名',
|
'Please input correct username' => '请输入正确的用户名',
|
||||||
'Group Name Arr' => '分组名称数组',
|
'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~' => '您需要拥有该分组的所有权限且还有额外权限时,才可以操作该分组~',
|
'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' => '分组权限节点超出可分配范围,请刷新重试~',
|
||||||
'You can only operate subordinate role groups in the tree hierarchy~' => '仅可管理在角色组树中属于您下级的角色组(不含同级、不含其他分支)~',
|
'Remark lang' => '为保障系统安全,角色组本身的上下级关系仅供参考,系统的实际上下级划分是根据`权限多寡`来确定的,两位管理员的权限节点:相同被认为是`同级`、包含且有额外权限才被认为是`上级`,同级不可管理同级,上级可为下级分配自己拥有的权限节点;若有特殊情况管理员需转`上级`,可建立一个虚拟权限节点',
|
||||||
'Non super administrators cannot create top-level role groups' => '非超级管理员不能创建顶级角色组',
|
|
||||||
'The parent group is not within your manageable scope' => '所选父级角色组不在您可管理的范围内',
|
|
||||||
'Remark lang' => '角色组以「树形父子关系」为准:仅可管理本人所在组之下的下级组;同级、其他分支及上级组不可在此管理。分配权限时仍只能勾选您自身拥有的权限节点。',
|
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -297,26 +297,6 @@ class Auth extends \ba\Auth
|
|||||||
return array_unique($children);
|
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
|
public function getGroupChildGroups(int $groupId, array &$children): void
|
||||||
{
|
{
|
||||||
$childrenTemp = AdminGroup::where('pid', $groupId)
|
$childrenTemp = AdminGroup::where('pid', $groupId)
|
||||||
|
|||||||
@@ -3,8 +3,6 @@
|
|||||||
namespace {%namespace%};
|
namespace {%namespace%};
|
||||||
{%use%}
|
{%use%}
|
||||||
use app\common\controller\Backend;
|
use app\common\controller\Backend;
|
||||||
use support\Response;
|
|
||||||
use Webman\Http\Request as WebmanRequest;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {%tableComment%}
|
* {%tableComment%}
|
||||||
|
|||||||
@@ -3,11 +3,11 @@
|
|||||||
* 查看
|
* 查看
|
||||||
* @throws Throwable
|
* @throws Throwable
|
||||||
*/
|
*/
|
||||||
protected function _index(): Response
|
public function index(): void
|
||||||
{
|
{
|
||||||
// 如果是 select 则转发到 select 方法,若未重写该方法,其实还是继续执行 index
|
// 如果是 select 则转发到 select 方法,若未重写该方法,其实还是继续执行 index
|
||||||
if ($this->request && $this->request->get('select')) {
|
if ($this->request->param('select')) {
|
||||||
return $this->select($this->request);
|
$this->select();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -18,14 +18,13 @@
|
|||||||
list($where, $alias, $limit, $order) = $this->queryBuilder();
|
list($where, $alias, $limit, $order) = $this->queryBuilder();
|
||||||
$res = $this->model
|
$res = $this->model
|
||||||
->withJoin($this->withJoinTable, $this->withJoinType)
|
->withJoin($this->withJoinTable, $this->withJoinType)
|
||||||
->with($this->withJoinTable)
|
|
||||||
{%relationVisibleFields%}
|
{%relationVisibleFields%}
|
||||||
->alias($alias)
|
->alias($alias)
|
||||||
->where($where)
|
->where($where)
|
||||||
->order($order)
|
->order($order)
|
||||||
->paginate($limit);
|
->paginate($limit);
|
||||||
|
|
||||||
return $this->success('', [
|
$this->success('', [
|
||||||
'list' => $res->items(),
|
'list' => $res->items(),
|
||||||
'total' => $res->total(),
|
'total' => $res->total(),
|
||||||
'remark' => get_route_remark(),
|
'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%}
|
$this->model = new \{%modelNamespace%}\{%modelName%}();{%filterRule%}
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
@@ -50,7 +50,6 @@ trait Backend
|
|||||||
$res = $this->model
|
$res = $this->model
|
||||||
->field($this->indexField)
|
->field($this->indexField)
|
||||||
->withJoin($this->withJoinTable, $this->withJoinType)
|
->withJoin($this->withJoinTable, $this->withJoinType)
|
||||||
->with($this->withJoinTable)
|
|
||||||
->alias($alias)
|
->alias($alias)
|
||||||
->where($where)
|
->where($where)
|
||||||
->order($order)
|
->order($order)
|
||||||
@@ -302,40 +301,7 @@ trait Backend
|
|||||||
/**
|
/**
|
||||||
* 加载为 select(远程下拉选择框)数据,子类可覆盖
|
* 加载为 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
|
<?php
|
||||||
|
|
||||||
namespace app\common\validate;
|
namespace app\admin\validate\mall;
|
||||||
|
|
||||||
use think\Validate;
|
use think\Validate;
|
||||||
|
|
||||||
class GameUser extends Validate
|
class Player extends Validate
|
||||||
{
|
{
|
||||||
protected $failException = true;
|
protected $failException = true;
|
||||||
|
|
||||||
@@ -12,7 +12,6 @@ use ba\Filesystem;
|
|||||||
use app\common\controller\Api;
|
use app\common\controller\Api;
|
||||||
use app\admin\model\Admin as AdminModel;
|
use app\admin\model\Admin as AdminModel;
|
||||||
use app\admin\model\User as UserModel;
|
use app\admin\model\User as UserModel;
|
||||||
use app\process\Monitor;
|
|
||||||
use support\Response;
|
use support\Response;
|
||||||
use Webman\Http\Request;
|
use Webman\Http\Request;
|
||||||
use Phinx\Config\Config as PhinxConfig;
|
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']);
|
$connectData = $databaseParam = $request->only(['hostname', 'username', 'password', 'hostport', 'database', 'prefix']);
|
||||||
|
|
||||||
// 数据库配置测试
|
// 数据库配置测试
|
||||||
@@ -470,9 +455,6 @@ class Install extends Api
|
|||||||
return "\$env('database.{$key}', '" . addslashes($value) . "')";
|
return "\$env('database.{$key}', '" . addslashes($value) . "')";
|
||||||
};
|
};
|
||||||
$dbConfigText = preg_replace_callback("/\\\$env\('database\.(hostname|database|username|password|hostport|prefix)',\s*'[^']*'\)/", $callback, $dbConfigContent);
|
$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);
|
$result = @file_put_contents($dbConfigFile, $dbConfigText);
|
||||||
if (!$result) {
|
if (!$result) {
|
||||||
return $this->error(__('File has no write permission:%s', ['config/' . self::$dbConfigFileName]));
|
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
|
public function select(WebmanRequest $request): Response
|
||||||
{
|
{
|
||||||
$response = $this->initializeBackend($request);
|
$response = $this->initializeBackend($request);
|
||||||
if ($response instanceof Response) return $response;
|
if ($response !== null) return $response;
|
||||||
return $this->_select();
|
$this->_select();
|
||||||
|
return $this->success();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ class AllowCrossDomain implements MiddlewareInterface
|
|||||||
'Access-Control-Allow-Credentials' => 'true',
|
'Access-Control-Allow-Credentials' => 'true',
|
||||||
'Access-Control-Max-Age' => '1800',
|
'Access-Control-Max-Age' => '1800',
|
||||||
'Access-Control-Allow-Methods' => 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
|
'Access-Control-Allow-Methods' => 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
|
||||||
'Access-Control-Allow-Headers' => 'Content-Type, Authorization, batoken, ba-user-token, think-lang, server',
|
'Access-Control-Allow-Headers' => 'Content-Type, Authorization, batoken, ba-user-token, think-lang',
|
||||||
];
|
];
|
||||||
$origin = $request->header('origin');
|
$origin = $request->header('origin');
|
||||||
if (is_array($origin)) {
|
if (is_array($origin)) {
|
||||||
|
|||||||
@@ -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,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,31 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace app\common\validate;
|
|
||||||
|
|
||||||
use think\Validate;
|
|
||||||
|
|
||||||
class GameConfig extends Validate
|
|
||||||
{
|
|
||||||
protected $failException = true;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 验证规则
|
|
||||||
*/
|
|
||||||
protected $rule = [
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 提示消息
|
|
||||||
*/
|
|
||||||
protected $message = [
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 验证场景
|
|
||||||
*/
|
|
||||||
protected $scene = [
|
|
||||||
'add' => [],
|
|
||||||
'edit' => [],
|
|
||||||
];
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,143 +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'],
|
|
||||||
];
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -188,8 +188,7 @@ if (!function_exists('get_controller_path')) {
|
|||||||
$parts = explode('\\', $relative);
|
$parts = explode('\\', $relative);
|
||||||
$path = [];
|
$path = [];
|
||||||
foreach ($parts as $p) {
|
foreach ($parts as $p) {
|
||||||
// 与 BuildAdmin admin_rule.name 一致:多段类名用 camelCase(如 auth/adminLog),不用 admin_log
|
$path[] = strtolower(preg_replace('/([a-z])([A-Z])/', '$1_$2', $p));
|
||||||
$path[] = lcfirst($p);
|
|
||||||
}
|
}
|
||||||
return implode('/', $path);
|
return implode('/', $path);
|
||||||
}
|
}
|
||||||
@@ -205,24 +204,7 @@ if (!function_exists('get_controller_path')) {
|
|||||||
if (count($parts) < 2) {
|
if (count($parts) < 2) {
|
||||||
return $parts[0] ?? null;
|
return $parts[0] ?? null;
|
||||||
}
|
}
|
||||||
$segments = array_slice($parts, 1, -1);
|
return implode('/', array_slice($parts, 1, -1)) ?: $parts[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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,12 +3,12 @@
|
|||||||
namespace app\process;
|
namespace app\process;
|
||||||
|
|
||||||
use Webman\App;
|
use Webman\App;
|
||||||
|
use Webman\Http\Response;
|
||||||
|
|
||||||
class Http extends App
|
class Http extends App
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* 在父类处理前拦截 OPTIONS 预检,直接返回 CORS 头(避免预检未命中路由时无 CORS)
|
* 在父类处理前拦截 OPTIONS 预检,直接返回 CORS 头(避免预检未命中路由时无 CORS)
|
||||||
* 必须与 AllowCrossDomain::optionsResponse 一致,否则会覆盖中间件里对 Allow-Headers(如 server)的配置
|
|
||||||
*/
|
*/
|
||||||
public function onMessage($connection, $request): void
|
public function onMessage($connection, $request): void
|
||||||
{
|
{
|
||||||
@@ -18,8 +18,19 @@ class Http extends App
|
|||||||
$path = is_string($path) ? trim($path, '/') : '';
|
$path = is_string($path) ? trim($path, '/') : '';
|
||||||
$isApiOrAdmin = $path !== '' && (str_starts_with($path, 'api') || str_starts_with($path, 'admin'));
|
$isApiOrAdmin = $path !== '' && (str_starts_with($path, 'api') || str_starts_with($path, 'admin'));
|
||||||
if ($isApiOrAdmin) {
|
if ($isApiOrAdmin) {
|
||||||
$response = \app\common\middleware\AllowCrossDomain::optionsResponse($request);
|
$origin = $request->header('origin');
|
||||||
$connection->send($response);
|
$origin = is_array($origin) ? ($origin[0] ?? '') : (is_string($origin) ? trim($origin) : '');
|
||||||
|
if ($origin === '') {
|
||||||
|
$origin = '*';
|
||||||
|
}
|
||||||
|
$headers = [
|
||||||
|
'Access-Control-Allow-Origin' => $origin,
|
||||||
|
'Access-Control-Allow-Credentials' => 'true',
|
||||||
|
'Access-Control-Max-Age' => '1800',
|
||||||
|
'Access-Control-Allow-Methods' => 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
|
||||||
|
'Access-Control-Allow-Headers' => 'Content-Type, Authorization, batoken, ba-user-token, think-lang',
|
||||||
|
];
|
||||||
|
$connection->send(new Response(204, $headers, ''));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,11 +5,11 @@
|
|||||||
|
|
||||||
return [
|
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则无限
|
// 会员登录失败可重试次数,false则无限
|
||||||
'user_login_retry' => 10,
|
'user_login_retry' => 10,
|
||||||
// 管理员登录失败可重试次数,false则无限
|
// 管理员登录失败可重试次数,false则无限
|
||||||
@@ -44,7 +44,7 @@ return [
|
|||||||
// 默认驱动方式
|
// 默认驱动方式
|
||||||
'default' => 'mysql',
|
'default' => 'mysql',
|
||||||
// 加密key
|
// 加密key
|
||||||
'key' => 'u4w3NzEr5QTv2ygjYOoMVZ6snKAePxJp',
|
'key' => 'L1iYVS0PChKA9pjcFdmOGb4zfDIHo5xw',
|
||||||
// 加密方式
|
// 加密方式
|
||||||
'algo' => 'ripemd160',
|
'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::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']);
|
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']);
|
||||||
Route::get('/admin/auth.AdminLog/index', [\app\admin\controller\auth\AdminLog::class, 'index']);
|
|
||||||
|
|
||||||
// admin/user/user
|
// admin/user/user
|
||||||
Route::get('/admin/user/user/index', [\app\admin\controller\user\User::class, 'index']);
|
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::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::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) ====================
|
// ==================== 兼容 ThinkPHP 风格 URL(module.Controller/action) ====================
|
||||||
// 前端使用 /admin/user.Rule/index 格式,需转换为控制器调用
|
// 前端使用 /admin/user.Rule/index 格式,需转换为控制器调用
|
||||||
Route::add(
|
Route::add(
|
||||||
['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD'],
|
['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) {
|
function (\Webman\Http\Request $request, string $controllerPart, string $action) {
|
||||||
$pos = strpos($controllerPart, '.');
|
$pos = strpos($controllerPart, '.');
|
||||||
if ($pos === false) {
|
if ($pos === false) {
|
||||||
@@ -268,21 +257,8 @@ Route::add(
|
|||||||
}
|
}
|
||||||
$module = substr($controllerPart, 0, $pos);
|
$module = substr($controllerPart, 0, $pos);
|
||||||
$controller = substr($controllerPart, $pos + 1);
|
$controller = substr($controllerPart, $pos + 1);
|
||||||
// game.user / game.User 等:小写控制器名需解析为 User.php(PSR-4 类名 StudlyCase)
|
$class = '\\app\\admin\\controller\\' . strtolower($module) . '\\' . $controller;
|
||||||
$class = null;
|
if (!class_exists($class)) {
|
||||||
$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) {
|
|
||||||
return new Response(404, ['Content-Type' => 'application/json'], json_encode(['code' => 404, 'msg' => '404 Not Found', 'data' => []], JSON_UNESCAPED_UNICODE));
|
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)) {
|
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) ====================
|
// ==================== CORS 预检(OPTIONS) ====================
|
||||||
// 放在最后注册;显式加上前端会请求的路径,再加固通配
|
// 放在最后注册;显式加上前端会请求的路径,再加固通配
|
||||||
Route::add('OPTIONS', '/api/index/index', [\app\common\middleware\AllowCrossDomain::class, 'optionsResponse']);
|
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'),
|
'hostname' => $env('database.hostname', '127.0.0.1'),
|
||||||
// 数据库名(与 database.php / .env 一致)
|
// 数据库名(与 database.php / .env 一致)
|
||||||
'database' => $env('database.database', 'webman-buildadmin-dafuweng'),
|
'database' => $env('database.database', 'buildadmin-webman'),
|
||||||
// 用户名(与 .env DATABASE_USERNAME 一致,默认勿用 root 以免与本机 MySQL 不符)
|
// 用户名(与 .env DATABASE_USERNAME 一致,默认勿用 root 以免与本机 MySQL 不符)
|
||||||
'username' => $env('database.username', 'webman-buildadmin-dafuweng'),
|
'username' => $env('database.username', 'buildadmin-webman'),
|
||||||
// 密码(与 .env DATABASE_PASSWORD 一致)
|
// 密码(与 .env DATABASE_PASSWORD 一致)
|
||||||
'password' => $env('database.password', '6dzMaX32Xdsc4DjS'),
|
'password' => $env('database.password', '123456'),
|
||||||
// 端口
|
// 端口
|
||||||
'hostport' => $env('database.hostport', '3306'),
|
'hostport' => $env('database.hostport', '3306'),
|
||||||
// 数据库连接参数(MYSQL_ATTR_USE_BUFFERED_QUERY 避免 "Cannot execute queries while other unbuffered queries are active")
|
// 数据库连接参数(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('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('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('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'], [
|
->addIndex(['name'], [
|
||||||
'unique' => true,
|
'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
@@ -42,27 +42,8 @@ if (!function_exists('env')) {
|
|||||||
require $baseDir . '/vendor/workerman/webman-framework/src/support/helpers.php';
|
require $baseDir . '/vendor/workerman/webman-framework/src/support/helpers.php';
|
||||||
require $baseDir . '/app/functions.php';
|
require $baseDir . '/app/functions.php';
|
||||||
|
|
||||||
use Webman\Config;
|
Webman\Config::load($baseDir . '/config', ['route', 'middleware', 'process', 'server', 'static']);
|
||||||
|
$thinkorm = config('thinkorm', []);
|
||||||
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', []));
|
|
||||||
if (!empty($thinkorm)) {
|
if (!empty($thinkorm)) {
|
||||||
support\think\Db::setConfig($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
|
<?php
|
||||||
/**
|
/**
|
||||||
* Phinx 数据库迁移配置
|
* Phinx 数据库迁移配置
|
||||||
* 与 Webman ThinkOrm 引导一致:Config 加载后合并 thinkorm + think-orm,供 phinx migrate 使用
|
* 从 config/thinkorm.php 读取数据库连接,用于 php vendor/bin/phinx migrate
|
||||||
*/
|
*/
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
@@ -36,18 +36,7 @@ if (!function_exists('env')) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!defined('BASE_PATH')) {
|
$thinkorm = require $baseDir . '/config/thinkorm.php';
|
||||||
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', []));
|
|
||||||
$conn = $thinkorm['connections'][$thinkorm['default'] ?? 'mysql'] ?? [];
|
$conn = $thinkorm['connections'][$thinkorm['default'] ?? 'mysql'] ?? [];
|
||||||
$prefix = $conn['prefix'] ?? '';
|
$prefix = $conn['prefix'] ?? '';
|
||||||
|
|
||||||
|
|||||||
@@ -8,9 +8,9 @@
|
|||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Loading...</title>
|
<title>Loading...</title>
|
||||||
<script type="module" crossorigin src="/assets/index-C0rp3aqE.js"></script>
|
<script type="module" crossorigin src="/assets/index-CP9YHUOF.js"></script>
|
||||||
<link rel="modulepreload" crossorigin href="/assets/vue-Ce7h5hs3.js">
|
<link rel="modulepreload" crossorigin href="/assets/vue-BqYd3Ike.js">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/style-3bcQwxyw.css">
|
<link rel="stylesheet" crossorigin href="/assets/style-DAXxNixF.css">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -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){
|
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 || ''; }
|
if (res && res.data) { urls.adminUrl = res.data.adminUrl || ''; urls.frontUrl = res.data.frontUrl || ''; }
|
||||||
}).catch(function(){});
|
}).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() {
|
function applyUrls() {
|
||||||
if (!urls.adminUrl && !urls.frontUrl) return;
|
if (!urls.adminUrl && !urls.frontUrl) return;
|
||||||
document.querySelectorAll('input[type="text"], input:not([type])').forEach(function(inp){
|
document.querySelectorAll('input[type="text"], input:not([type])').forEach(function(inp){
|
||||||
@@ -116,7 +24,6 @@
|
|||||||
document.querySelectorAll('a[href*="#/"]').forEach(function(a){
|
document.querySelectorAll('a[href*="#/"]').forEach(function(a){
|
||||||
if (urls.frontUrl && a.href.indexOf('#/admin') < 0) a.href = urls.frontUrl;
|
if (urls.frontUrl && a.href.indexOf('#/admin') < 0) a.href = urls.frontUrl;
|
||||||
});
|
});
|
||||||
ensureQuickPanel();
|
|
||||||
}
|
}
|
||||||
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', function(){ setInterval(applyUrls, 800); });
|
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', function(){ setInterval(applyUrls, 800); });
|
||||||
else setInterval(applyUrls, 800);
|
else setInterval(applyUrls, 800);
|
||||||
|
|||||||
@@ -8,4 +8,4 @@ VITE_BASE_PATH = '/'
|
|||||||
VITE_OUT_DIR = 'dist'
|
VITE_OUT_DIR = 'dist'
|
||||||
|
|
||||||
# 线上环境接口地址 - 'getCurrentDomain:表示获取当前域名'
|
# 线上环境接口地址 - 'getCurrentDomain:表示获取当前域名'
|
||||||
VITE_AXIOS_BASE_URL = 'getCurrentDomain'
|
VITE_AXIOS_BASE_URL = 'https://test-api.zhenhui666.top'
|
||||||
|
|||||||
@@ -5,5 +5,5 @@ export default {
|
|||||||
'Parent group': 'Superior group',
|
'Parent group': 'Superior group',
|
||||||
'The parent group cannot be the group itself': 'The parent group cannot be the group itself',
|
'The parent group cannot be the group itself': 'The parent group cannot be the group itself',
|
||||||
'Manage subordinate role groups here':
|
'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,22 +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',
|
|
||||||
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, real_ev, tier.',
|
|
||||||
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,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: '权限',
|
jurisdiction: '权限',
|
||||||
'Parent group': '上级分组',
|
'Parent group': '上级分组',
|
||||||
'The parent group cannot be the group itself': '上级分组不能是分组本身',
|
'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,22 +0,0 @@
|
|||||||
export default {
|
|
||||||
id: 'ID',
|
|
||||||
game_channel_id: '渠道',
|
|
||||||
gamechannel__name: '渠道名',
|
|
||||||
tier_reward_form: '档位奖励表单',
|
|
||||||
bigwin_form: '超级大奖表单',
|
|
||||||
create_time: '创建时间',
|
|
||||||
update_time: '更新时间',
|
|
||||||
grid_number: '色子点数',
|
|
||||||
ui_text: '显示文本',
|
|
||||||
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,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: {
|
meta: {
|
||||||
title: pageTitle('home'),
|
title: pageTitle('home'),
|
||||||
},
|
},
|
||||||
redirect: adminBaseRoutePath,
|
|
||||||
children: [],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// 管理员登录页 - 不放在 adminBaseRoute.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,59 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="form-cell">
|
|
||||||
<div v-if="rows.length === 0" class="empty">-</div>
|
|
||||||
<div v-for="(row, idx) in rows" :key="idx" class="row">
|
|
||||||
<el-tag size="small" effect="light">{{ row.grid_number }}</el-tag>
|
|
||||||
<el-tag size="small" type="danger" effect="light">{{ row.tier }}</el-tag>
|
|
||||||
<span>{{ row.ui_text }}</span>
|
|
||||||
<span>{{ row.real_ev }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { computed } from 'vue'
|
|
||||||
|
|
||||||
const props = defineProps<{ row?: { bigwin_form?: unknown } }>()
|
|
||||||
|
|
||||||
type FormRow = { grid_number: string; ui_text: string; real_ev: string; tier: string }
|
|
||||||
|
|
||||||
const rows = computed<FormRow[]>(() => {
|
|
||||||
const raw = props.row?.bigwin_form
|
|
||||||
if (typeof raw !== 'string' || raw.trim() === '') return []
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(raw) as unknown
|
|
||||||
if (!Array.isArray(parsed)) return []
|
|
||||||
const out: FormRow[] = []
|
|
||||||
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 ?? ''),
|
|
||||||
real_ev: String(obj.real_ev ?? ''),
|
|
||||||
tier: String(obj.tier ?? ''),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
} catch {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.form-cell {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
.row {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
align-items: center;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
.empty {
|
|
||||||
color: var(--el-text-color-secondary);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="form-cell">
|
|
||||||
<div v-if="rows.length === 0" class="empty">-</div>
|
|
||||||
<div v-for="(row, idx) in rows" :key="idx" class="row">
|
|
||||||
<el-tag size="small" effect="light">{{ row.grid_number }}</el-tag>
|
|
||||||
<el-tag size="small" type="info" effect="light">{{ row.tier }}</el-tag>
|
|
||||||
<span>{{ row.ui_text }}</span>
|
|
||||||
<span>{{ row.real_ev }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { computed } from 'vue'
|
|
||||||
|
|
||||||
const props = defineProps<{ row?: { tier_reward_form?: unknown } }>()
|
|
||||||
|
|
||||||
type FormRow = { grid_number: string; ui_text: string; real_ev: string; tier: string }
|
|
||||||
|
|
||||||
const rows = computed<FormRow[]>(() => {
|
|
||||||
const raw = props.row?.tier_reward_form
|
|
||||||
if (typeof raw !== 'string' || raw.trim() === '') return []
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(raw) as unknown
|
|
||||||
if (!Array.isArray(parsed)) return []
|
|
||||||
const out: FormRow[] = []
|
|
||||||
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 ?? ''),
|
|
||||||
real_ev: String(obj.real_ev ?? ''),
|
|
||||||
tier: String(obj.tier ?? ''),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
} catch {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.form-cell {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
.row {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
align-items: center;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
.empty {
|
|
||||||
color: var(--el-text-color-secondary);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,141 +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="headerButtons"
|
|
||||||
:quick-search-placeholder="t('Quick search placeholder', { fields: t('game.rewardConfig.quick Search Fields') })"
|
|
||||||
></TableHeader>
|
|
||||||
|
|
||||||
<!-- 表格 -->
|
|
||||||
<!-- 表格列有多种自定义渲染方式,比如自定义组件、具名插槽等,参见文档 -->
|
|
||||||
<!-- 要使用 el-table 组件原有的属性,直接加在 Table 标签上即可 -->
|
|
||||||
<Table ref="tableRef"></Table>
|
|
||||||
|
|
||||||
<!-- 表单 -->
|
|
||||||
<PopupForm />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { computed, onMounted, provide, useTemplateRef } from 'vue'
|
|
||||||
import { useI18n } from 'vue-i18n'
|
|
||||||
import PopupForm from './popupForm.vue'
|
|
||||||
import TierRewardFormCell from './TierRewardFormCell.vue'
|
|
||||||
import BigwinFormCell from './BigwinFormCell.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 { useAdminInfo } from '/@/stores/adminInfo'
|
|
||||||
import baTableClass from '/@/utils/baTable'
|
|
||||||
|
|
||||||
defineOptions({
|
|
||||||
name: 'game/rewardConfig',
|
|
||||||
})
|
|
||||||
|
|
||||||
const { t } = useI18n()
|
|
||||||
const tableRef = useTemplateRef('tableRef')
|
|
||||||
const adminInfo = useAdminInfo()
|
|
||||||
const isSuperAdmin = computed(() => adminInfo.super === true)
|
|
||||||
const optButtons: OptButton[] = defaultOptButtons(['edit'])
|
|
||||||
const superHeaderButtons: HeaderOptButton[] = ['refresh', 'add', 'edit', 'delete', 'comSearch', 'quickSearch', 'columnDisplay']
|
|
||||||
const channelHeaderButtons: HeaderOptButton[] = ['refresh']
|
|
||||||
const tierRewardFormColumn: TableColumn = {
|
|
||||||
label: t('game.rewardConfig.tier_reward_form'),
|
|
||||||
prop: 'tier_reward_form',
|
|
||||||
align: 'center',
|
|
||||||
minWidth: 360,
|
|
||||||
operatorPlaceholder: t('Fuzzy query'),
|
|
||||||
sortable: false,
|
|
||||||
operator: 'LIKE',
|
|
||||||
comSearchRender: 'string',
|
|
||||||
render: 'customRender',
|
|
||||||
customRender: TierRewardFormCell,
|
|
||||||
}
|
|
||||||
const bigwinFormColumn: TableColumn = {
|
|
||||||
label: t('game.rewardConfig.bigwin_form'),
|
|
||||||
prop: 'bigwin_form',
|
|
||||||
align: 'center',
|
|
||||||
minWidth: 360,
|
|
||||||
operatorPlaceholder: t('Fuzzy query'),
|
|
||||||
sortable: false,
|
|
||||||
operator: 'LIKE',
|
|
||||||
comSearchRender: 'string',
|
|
||||||
render: 'customRender',
|
|
||||||
customRender: BigwinFormCell,
|
|
||||||
}
|
|
||||||
const headerButtons = computed(() => {
|
|
||||||
if (isSuperAdmin.value) {
|
|
||||||
return superHeaderButtons
|
|
||||||
}
|
|
||||||
return channelHeaderButtons
|
|
||||||
})
|
|
||||||
const columns: TableColumn[] = [
|
|
||||||
{ type: 'selection', align: 'center', operator: false },
|
|
||||||
{ label: t('game.rewardConfig.id'), prop: 'id', align: 'center', width: 70, operator: 'RANGE', sortable: 'custom' },
|
|
||||||
{
|
|
||||||
label: t('game.rewardConfig.gamechannel__name'),
|
|
||||||
prop: 'gameChannel.name',
|
|
||||||
align: 'center',
|
|
||||||
operatorPlaceholder: t('Fuzzy query'),
|
|
||||||
render: 'tags',
|
|
||||||
operator: 'LIKE',
|
|
||||||
comSearchRender: 'string',
|
|
||||||
},
|
|
||||||
...(isSuperAdmin.value ? [] : [tierRewardFormColumn, bigwinFormColumn]),
|
|
||||||
{
|
|
||||||
label: t('game.rewardConfig.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.rewardConfig.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 },
|
|
||||||
]
|
|
||||||
|
|
||||||
/**
|
|
||||||
* baTable 内包含了表格的所有数据且数据具备响应性,然后通过 provide 注入给了后代组件
|
|
||||||
*/
|
|
||||||
const baTable = new baTableClass(
|
|
||||||
new baTableApi('/admin/game.RewardConfig/'),
|
|
||||||
{
|
|
||||||
pk: 'id',
|
|
||||||
column: columns,
|
|
||||||
dblClickNotEditColumn: [undefined],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
defaultItems: {},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
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,264 +0,0 @@
|
|||||||
<template>
|
|
||||||
<el-dialog
|
|
||||||
class="ba-operate-dialog"
|
|
||||||
:close-on-click-modal="false"
|
|
||||||
:model-value="['Add', 'Edit'].includes(baTable.form.operate!)"
|
|
||||||
@close="baTable.toggleForm"
|
|
||||||
>
|
|
||||||
<template #header>
|
|
||||||
<div class="title" v-drag="['.ba-operate-dialog', '.el-dialog__header']" v-zoom="'.ba-operate-dialog'">
|
|
||||||
{{ baTable.form.operate ? t(baTable.form.operate) : '' }}
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<el-scrollbar v-loading="baTable.form.loading" class="ba-table-form-scrollbar">
|
|
||||||
<div
|
|
||||||
class="ba-operate-form"
|
|
||||||
:class="'ba-' + baTable.form.operate + '-form'"
|
|
||||||
:style="config.layout.shrink ? '' : 'width: calc(100% - ' + baTable.form.labelWidth! / 2 + 'px)'"
|
|
||||||
>
|
|
||||||
<el-form
|
|
||||||
v-if="!baTable.form.loading"
|
|
||||||
ref="formRef"
|
|
||||||
@submit.prevent=""
|
|
||||||
:model="baTable.form.items"
|
|
||||||
:label-position="config.layout.shrink ? 'top' : 'right'"
|
|
||||||
:label-width="baTable.form.labelWidth + 'px'"
|
|
||||||
:rules="rules"
|
|
||||||
>
|
|
||||||
<FormItem
|
|
||||||
:label="t('game.rewardConfig.game_channel_id')"
|
|
||||||
type="remoteSelect"
|
|
||||||
v-model="baTable.form.items!.game_channel_id"
|
|
||||||
prop="game_channel_id"
|
|
||||||
:input-attr="{ ...channelRemoteAttr, disabled: channelFieldDisabled }"
|
|
||||||
:placeholder="t('Please select field', { field: t('game.rewardConfig.game_channel_id') })"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<el-form-item :label="t('game.rewardConfig.tier_reward_form')" prop="tier_reward_form">
|
|
||||||
<div class="block-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 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.real_ev"
|
|
||||||
:placeholder="t('Please input field', { field: t('game.rewardConfig.real_ev') })"
|
|
||||||
@input="syncPayload"
|
|
||||||
/>
|
|
||||||
<el-select v-model="row.tier" style="width: 120px" @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>
|
|
||||||
</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">
|
|
||||||
<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>
|
|
||||||
</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'
|
|
||||||
|
|
||||||
type RewardRow = { grid_number: string; ui_text: string; real_ev: string; tier: string }
|
|
||||||
|
|
||||||
const config = useConfig()
|
|
||||||
const formRef = useTemplateRef('formRef')
|
|
||||||
const baTable = inject('baTable') as baTableClass
|
|
||||||
const adminInfo = useAdminInfo()
|
|
||||||
const { t } = useI18n()
|
|
||||||
|
|
||||||
const TIER_GRIDS = Array.from({ length: 26 }, (_v, i) => String(i + 5))
|
|
||||||
const BIGWIN_GRIDS = ['5', '10', '15', '20', '25', '30']
|
|
||||||
|
|
||||||
const channelRemoteAttr = { pk: 'game_channel.id', field: 'name', remoteUrl: '/admin/game.Channel/index' }
|
|
||||||
const isSuperAdmin = computed(() => adminInfo.super === true)
|
|
||||||
const channelFieldDisabled = computed(() => !isSuperAdmin.value && baTable.form.operate === 'Edit')
|
|
||||||
|
|
||||||
const tierRows = ref<RewardRow[]>(TIER_GRIDS.map((g) => ({ grid_number: g, ui_text: '', real_ev: '', tier: 'T1' })))
|
|
||||||
const bigwinRows = ref<RewardRow[]>(BIGWIN_GRIDS.map((g) => ({ grid_number: g, ui_text: '', real_ev: '', tier: 'BIGWIN' })))
|
|
||||||
|
|
||||||
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 ?? ''),
|
|
||||||
real_ev: String(obj.real_ev ?? ''),
|
|
||||||
tier: String(obj.tier ?? ''),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
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 ?? '', real_ev: row?.real_ev ?? '', tier: row?.tier && row.tier !== 'BIGWIN' ? row.tier : 'T1' }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
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 ?? '', real_ev: row?.real_ev ?? '', tier: 'BIGWIN' }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function syncPayload() {
|
|
||||||
const items = baTable.form.items
|
|
||||||
if (!items) return
|
|
||||||
items.tier_reward_form = JSON.stringify(tierRows.value)
|
|
||||||
items.bigwin_form = JSON.stringify(bigwinRows.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => baTable.form.loading,
|
|
||||||
(loading) => {
|
|
||||||
if (loading === false) {
|
|
||||||
tierRows.value = toTierRows(baTable.form.items?.tier_reward_form)
|
|
||||||
bigwinRows.value = toBigwinRows(baTable.form.items?.bigwin_form)
|
|
||||||
syncPayload()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const rules: Partial<Record<string, FormItemRule[]>> = reactive({
|
|
||||||
game_channel_id: [buildValidatorData({ name: 'required', title: t('game.rewardConfig.game_channel_id') })],
|
|
||||||
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'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.block-editor {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
</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
|
<TableHeader
|
||||||
:buttons="['refresh', 'add', 'edit', 'delete', 'comSearch', 'quickSearch', 'columnDisplay']"
|
: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>
|
></TableHeader>
|
||||||
|
|
||||||
<!-- 表格 -->
|
<!-- 表格 -->
|
||||||
@@ -23,7 +23,6 @@
|
|||||||
import { onMounted, provide, useTemplateRef } from 'vue'
|
import { onMounted, provide, useTemplateRef } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import PopupForm from './popupForm.vue'
|
import PopupForm from './popupForm.vue'
|
||||||
import GameConfigValueCell from './GameConfigValueCell.vue'
|
|
||||||
import { baTableApi } from '/@/api/common'
|
import { baTableApi } from '/@/api/common'
|
||||||
import { defaultOptButtons } from '/@/components/table'
|
import { defaultOptButtons } from '/@/components/table'
|
||||||
import TableHeader from '/@/components/table/header/index.vue'
|
import TableHeader from '/@/components/table/header/index.vue'
|
||||||
@@ -31,7 +30,7 @@ import Table from '/@/components/table/index.vue'
|
|||||||
import baTableClass from '/@/utils/baTable'
|
import baTableClass from '/@/utils/baTable'
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'game/config',
|
name: 'mall/player',
|
||||||
})
|
})
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
@@ -42,83 +41,22 @@ const optButtons: OptButton[] = defaultOptButtons(['edit', 'delete'])
|
|||||||
* baTable 内包含了表格的所有数据且数据具备响应性,然后通过 provide 注入给了后代组件
|
* baTable 内包含了表格的所有数据且数据具备响应性,然后通过 provide 注入给了后代组件
|
||||||
*/
|
*/
|
||||||
const baTable = new baTableClass(
|
const baTable = new baTableClass(
|
||||||
new baTableApi('/admin/game.Config/'),
|
new baTableApi('/admin/mall.Player/'),
|
||||||
{
|
{
|
||||||
pk: 'ID',
|
pk: 'id',
|
||||||
column: [
|
column: [
|
||||||
{ type: 'selection', align: 'center', operator: false },
|
{ 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'),
|
label: t('mall.player.username'),
|
||||||
prop: 'channel_id',
|
prop: 'username',
|
||||||
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',
|
|
||||||
align: 'center',
|
align: 'center',
|
||||||
operatorPlaceholder: t('Fuzzy query'),
|
operatorPlaceholder: t('Fuzzy query'),
|
||||||
sortable: false,
|
sortable: false,
|
||||||
operator: 'LIKE',
|
operator: 'LIKE',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t('game.config.name'),
|
label: t('mall.player.create_time'),
|
||||||
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'),
|
|
||||||
prop: 'create_time',
|
prop: 'create_time',
|
||||||
align: 'center',
|
align: 'center',
|
||||||
render: 'datetime',
|
render: 'datetime',
|
||||||
@@ -129,7 +67,7 @@ const baTable = new baTableClass(
|
|||||||
timeFormat: 'yyyy-mm-dd hh:MM:ss',
|
timeFormat: 'yyyy-mm-dd hh:MM:ss',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t('game.config.update_time'),
|
label: t('mall.player.update_time'),
|
||||||
prop: 'update_time',
|
prop: 'update_time',
|
||||||
align: 'center',
|
align: 'center',
|
||||||
render: 'datetime',
|
render: 'datetime',
|
||||||
@@ -139,13 +77,13 @@ const baTable = new baTableClass(
|
|||||||
width: 160,
|
width: 160,
|
||||||
timeFormat: 'yyyy-mm-dd hh:MM:ss',
|
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 },
|
{ label: t('Operate'), align: 'center', width: 100, render: 'buttons', buttons: optButtons, operator: false },
|
||||||
],
|
],
|
||||||
dblClickNotEditColumn: [undefined, 'instantiation'],
|
dblClickNotEditColumn: [undefined],
|
||||||
defaultOrder: { prop: 'channel_id', order: 'desc' },
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
defaultItems: { sort: 100 },
|
defaultItems: {},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -30,44 +30,26 @@
|
|||||||
:rules="rules"
|
:rules="rules"
|
||||||
>
|
>
|
||||||
<FormItem
|
<FormItem
|
||||||
:label="t('game.channel.code')"
|
:label="t('mall.player.username')"
|
||||||
type="string"
|
type="string"
|
||||||
v-model="baTable.form.items!.code"
|
v-model="baTable.form.items!.username"
|
||||||
prop="code"
|
prop="username"
|
||||||
:placeholder="t('Please input field', { field: t('game.channel.code') })"
|
:placeholder="t('Please input field', { field: t('mall.player.username') })"
|
||||||
/>
|
/>
|
||||||
<FormItem
|
<FormItem
|
||||||
:label="t('game.channel.name')"
|
:label="t('mall.player.password')"
|
||||||
type="string"
|
type="password"
|
||||||
v-model="baTable.form.items!.name"
|
v-model="baTable.form.items!.password"
|
||||||
prop="name"
|
prop="password"
|
||||||
:placeholder="t('Please input field', { field: t('game.channel.name') })"
|
:placeholder="t('Please input field', { field: t('mall.player.password') })"
|
||||||
/>
|
/>
|
||||||
<FormItem
|
<FormItem
|
||||||
:label="t('game.channel.status')"
|
:label="t('mall.player.score')"
|
||||||
type="switch"
|
type="number"
|
||||||
v-model="baTable.form.items!.status"
|
v-model="baTable.form.items!.score"
|
||||||
prop="status"
|
prop="score"
|
||||||
:input-attr="{ content: { '0': t('game.channel.status 0'), '1': t('game.channel.status 1') } }"
|
:input-attr="{ step: 1 }"
|
||||||
/>
|
:placeholder="t('Please input field', { field: t('mall.player.score') })"
|
||||||
<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') })"
|
|
||||||
/>
|
/>
|
||||||
</el-form>
|
</el-form>
|
||||||
</div>
|
</div>
|
||||||
@@ -85,34 +67,43 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { FormItemRule } from 'element-plus'
|
import type { FormItemRule } from 'element-plus'
|
||||||
import { computed, inject, useTemplateRef } from 'vue'
|
import { inject, reactive, useTemplateRef } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import FormItem from '/@/components/formItem/index.vue'
|
import FormItem from '/@/components/formItem/index.vue'
|
||||||
import { useConfig } from '/@/stores/config'
|
import { useConfig } from '/@/stores/config'
|
||||||
import { useAdminInfo } from '/@/stores/adminInfo'
|
|
||||||
import type baTableClass from '/@/utils/baTable'
|
import type baTableClass from '/@/utils/baTable'
|
||||||
import { buildValidatorData } from '/@/utils/validate'
|
import { buildValidatorData, regularPassword } from '/@/utils/validate'
|
||||||
|
|
||||||
const config = useConfig()
|
const config = useConfig()
|
||||||
const formRef = useTemplateRef('formRef')
|
const formRef = useTemplateRef('formRef')
|
||||||
const baTable = inject('baTable') as baTableClass
|
const baTable = inject('baTable') as baTableClass
|
||||||
const adminInfo = useAdminInfo()
|
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
const isSuperAdmin = computed(() => adminInfo.super === true)
|
const rules: Partial<Record<string, FormItemRule[]>> = reactive({
|
||||||
|
username: [buildValidatorData({ name: 'required', title: t('mall.player.username') })],
|
||||||
const rules = computed<Partial<Record<string, FormItemRule[]>>>(() => ({
|
password: [
|
||||||
code: [buildValidatorData({ name: 'required', title: t('game.channel.code') })],
|
{
|
||||||
name: [buildValidatorData({ name: 'required', title: t('game.channel.name') })],
|
validator: (_rule: unknown, val: string, callback: (error?: Error) => void) => {
|
||||||
user_count: [buildValidatorData({ name: 'integer', title: t('game.channel.user_count') })],
|
if (baTable.form.operate === 'Add') {
|
||||||
profit_amount: [buildValidatorData({ name: 'float', title: t('game.channel.profit_amount') })],
|
if (!val) {
|
||||||
admin_id: isSuperAdmin.value
|
return callback(new Error(t('Please input field', { field: t('mall.player.password') })))
|
||||||
? [buildValidatorData({ name: 'required', title: t('game.channel.admin_id') })]
|
}
|
||||||
: [],
|
} else {
|
||||||
create_time: [buildValidatorData({ name: 'date', title: t('game.channel.create_time') })],
|
if (!val) {
|
||||||
update_time: [buildValidatorData({ name: 'date', title: t('game.channel.update_time') })],
|
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>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss"></style>
|
<style scoped lang="scss"></style>
|
||||||
Reference in New Issue
Block a user