1.优化下注接口/api/game/betPlace
2.优化后台/admin/config/gameConfig中新增压注筹码配置
This commit is contained in:
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace app\api\controller;
|
namespace app\api\controller;
|
||||||
|
|
||||||
|
use app\common\library\game\BetChips;
|
||||||
use app\common\library\game\ZiHuaDictionary;
|
use app\common\library\game\ZiHuaDictionary;
|
||||||
use app\common\model\BetOrder;
|
use app\common\model\BetOrder;
|
||||||
use app\common\model\GameRecord;
|
use app\common\model\GameRecord;
|
||||||
@@ -57,12 +58,14 @@ class Game extends MobileBase
|
|||||||
'lock_at' => $lockAt,
|
'lock_at' => $lockAt,
|
||||||
'open_at' => $openAt,
|
'open_at' => $openAt,
|
||||||
],
|
],
|
||||||
'bet_config' => [
|
'bet_config' => array_merge(
|
||||||
|
[
|
||||||
'pick_max_number_count' => $this->getPickMaxNumberCount(),
|
'pick_max_number_count' => $this->getPickMaxNumberCount(),
|
||||||
'chips' => ['1.00', '5.00', '10.00', '25.00', '50.00', '100.00'],
|
|
||||||
'min_bet_per_number' => $this->getConfigValue('min_bet_per_number', '0.0100'),
|
'min_bet_per_number' => $this->getConfigValue('min_bet_per_number', '0.0100'),
|
||||||
'max_bet_per_number' => $this->getConfigValue('max_bet_per_number', '10000.0000'),
|
'max_bet_per_number' => $this->getConfigValue('max_bet_per_number', '10000.0000'),
|
||||||
],
|
],
|
||||||
|
BetChips::lobbyChipsPayload()
|
||||||
|
),
|
||||||
'dictionary' => $items,
|
'dictionary' => $items,
|
||||||
'user_snapshot' => [
|
'user_snapshot' => [
|
||||||
'coin' => $user->coin,
|
'coin' => $user->coin,
|
||||||
@@ -119,7 +122,7 @@ class Game extends MobileBase
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 兼容旧路由:/api/game/betPlace
|
* 兼容旧路由:/api/game/betPlace
|
||||||
* 新语义与 place_bet 一致:bet_amount 作为“单注金额”。
|
* 与 placeBet 一致:须传筹码标识 bet_id(1–6),单注金额取自后台 game_config 的 bet_chips。
|
||||||
*/
|
*/
|
||||||
public function betPlace(Request $request): Response
|
public function betPlace(Request $request): Response
|
||||||
{
|
{
|
||||||
@@ -127,8 +130,7 @@ class Game extends MobileBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 提交下注:入参为 period_no + numbers + single_bet_amount + idempotency_key。
|
* 提交下注:入参为 period_no + numbers + bet_id(1–6,对应 lobbyInit.bet_config.chips 的键)+ idempotency_key。
|
||||||
* 兼容前端传参 bet_amount(作为 single_bet_amount 同义字段)。
|
|
||||||
*/
|
*/
|
||||||
public function placeBet(Request $request): Response
|
public function placeBet(Request $request): Response
|
||||||
{
|
{
|
||||||
@@ -138,9 +140,14 @@ class Game extends MobileBase
|
|||||||
}
|
}
|
||||||
$periodNo = trim((string) $request->post('period_no', ''));
|
$periodNo = trim((string) $request->post('period_no', ''));
|
||||||
$numbersRaw = $request->post('numbers', '');
|
$numbersRaw = $request->post('numbers', '');
|
||||||
$singleBetAmount = trim((string) ($request->post('single_bet_amount', $request->post('bet_amount', ''))));
|
|
||||||
$idempotencyKey = trim((string) $request->post('idempotency_key', ''));
|
$idempotencyKey = trim((string) $request->post('idempotency_key', ''));
|
||||||
if ($periodNo === '' || $singleBetAmount === '' || $idempotencyKey === '') {
|
$chipPick = $this->resolveBetChipFromRequest($request);
|
||||||
|
if (isset($chipPick['error'])) {
|
||||||
|
return $chipPick['error'];
|
||||||
|
}
|
||||||
|
$betChipId = $chipPick['bet_id'];
|
||||||
|
$singleBetAmount = $chipPick['amount'];
|
||||||
|
if ($periodNo === '' || $idempotencyKey === '') {
|
||||||
return $this->mobileError(1001, 'Missing parameters');
|
return $this->mobileError(1001, 'Missing parameters');
|
||||||
}
|
}
|
||||||
if (!is_numeric($singleBetAmount) || bccomp($singleBetAmount, '0', 2) <= 0) {
|
if (!is_numeric($singleBetAmount) || bccomp($singleBetAmount, '0', 2) <= 0) {
|
||||||
@@ -275,6 +282,7 @@ class Game extends MobileBase
|
|||||||
'user_id' => $userId,
|
'user_id' => $userId,
|
||||||
'period_no' => $period->period_no,
|
'period_no' => $period->period_no,
|
||||||
'numbers' => $numbers,
|
'numbers' => $numbers,
|
||||||
|
'bet_id' => $betChipId,
|
||||||
'single_bet_amount' => $singleAmount,
|
'single_bet_amount' => $singleAmount,
|
||||||
'numbers_count' => count($numbers),
|
'numbers_count' => count($numbers),
|
||||||
'total_amount' => $totalAmount,
|
'total_amount' => $totalAmount,
|
||||||
@@ -291,6 +299,7 @@ class Game extends MobileBase
|
|||||||
'order_no' => $orderNo,
|
'order_no' => $orderNo,
|
||||||
'period_no' => $period->period_no,
|
'period_no' => $period->period_no,
|
||||||
'status' => 'accepted',
|
'status' => 'accepted',
|
||||||
|
'bet_id' => $betChipId,
|
||||||
'single_bet_amount' => $singleAmount,
|
'single_bet_amount' => $singleAmount,
|
||||||
'numbers_count' => count($numbers),
|
'numbers_count' => count($numbers),
|
||||||
'locked_balance' => '0.00',
|
'locked_balance' => '0.00',
|
||||||
@@ -321,9 +330,13 @@ class Game extends MobileBase
|
|||||||
|
|
||||||
$periodNo = trim((string) $request->post('period_no', ''));
|
$periodNo = trim((string) $request->post('period_no', ''));
|
||||||
$numbersRaw = $request->post('numbers', '');
|
$numbersRaw = $request->post('numbers', '');
|
||||||
$singleBetAmount = trim((string) ($request->post('single_bet_amount', $request->post('bet_amount', ''))));
|
|
||||||
$rounds = $this->intValue($request->post('rounds', 1));
|
$rounds = $this->intValue($request->post('rounds', 1));
|
||||||
if ($periodNo === '' || $singleBetAmount === '' || $rounds < 1) {
|
$chipPick = $this->resolveBetChipFromRequest($request);
|
||||||
|
if (isset($chipPick['error'])) {
|
||||||
|
return $chipPick['error'];
|
||||||
|
}
|
||||||
|
$singleBetAmount = $chipPick['amount'];
|
||||||
|
if ($periodNo === '' || $rounds < 1) {
|
||||||
return $this->mobileError(1001, 'Missing parameters');
|
return $this->mobileError(1001, 'Missing parameters');
|
||||||
}
|
}
|
||||||
if (!is_numeric($singleBetAmount) || bccomp($singleBetAmount, '0', 2) <= 0) {
|
if (!is_numeric($singleBetAmount) || bccomp($singleBetAmount, '0', 2) <= 0) {
|
||||||
@@ -340,6 +353,7 @@ class Game extends MobileBase
|
|||||||
GameWebSocketEventBus::publish('auto.spin.progress', [
|
GameWebSocketEventBus::publish('auto.spin.progress', [
|
||||||
'user_id' => $userIdValue,
|
'user_id' => $userIdValue,
|
||||||
'period_no' => $periodNo,
|
'period_no' => $periodNo,
|
||||||
|
'bet_id' => $chipPick['bet_id'],
|
||||||
'rounds' => $rounds,
|
'rounds' => $rounds,
|
||||||
'remaining_rounds' => $rounds,
|
'remaining_rounds' => $rounds,
|
||||||
'completed_rounds' => 0,
|
'completed_rounds' => 0,
|
||||||
@@ -351,6 +365,7 @@ class Game extends MobileBase
|
|||||||
'auto_mode' => true,
|
'auto_mode' => true,
|
||||||
'period_no' => $periodNo,
|
'period_no' => $periodNo,
|
||||||
'numbers' => $numbers,
|
'numbers' => $numbers,
|
||||||
|
'bet_id' => $chipPick['bet_id'],
|
||||||
'single_bet_amount' => bcadd($singleBetAmount, '0', 2),
|
'single_bet_amount' => bcadd($singleBetAmount, '0', 2),
|
||||||
'rounds' => $rounds,
|
'rounds' => $rounds,
|
||||||
'remaining_rounds' => $rounds,
|
'remaining_rounds' => $rounds,
|
||||||
@@ -376,7 +391,7 @@ class Game extends MobileBase
|
|||||||
'order_no' => (string) $item->id,
|
'order_no' => (string) $item->id,
|
||||||
'period_no' => $item->period_no,
|
'period_no' => $item->period_no,
|
||||||
'numbers' => $item->pick_numbers ?? [],
|
'numbers' => $item->pick_numbers ?? [],
|
||||||
// 整笔压注金额(与请求 bet_amount 语义一致)
|
// 整笔压注金额(本笔总扣款)
|
||||||
'bet_amount' => $item->total_amount,
|
'bet_amount' => $item->total_amount,
|
||||||
'total_amount' => $item->total_amount,
|
'total_amount' => $item->total_amount,
|
||||||
'result_number' => null,
|
'result_number' => null,
|
||||||
@@ -456,6 +471,28 @@ class Game extends MobileBase
|
|||||||
return 'finished';
|
return 'finished';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{bet_id: int, amount: string}|array{error: Response}
|
||||||
|
*/
|
||||||
|
private function resolveBetChipFromRequest(Request $request): array
|
||||||
|
{
|
||||||
|
$betIdRaw = $request->post('bet_id', '');
|
||||||
|
if ($betIdRaw === '' || $betIdRaw === null) {
|
||||||
|
return ['error' => $this->mobileError(1001, 'Missing parameters')];
|
||||||
|
}
|
||||||
|
$betId = filter_var($betIdRaw, FILTER_VALIDATE_INT);
|
||||||
|
if ($betId === false || $betId < 1 || $betId > 6) {
|
||||||
|
return ['error' => $this->mobileError(1003, 'Invalid parameter value')];
|
||||||
|
}
|
||||||
|
$resolved = BetChips::resolveFromHotData();
|
||||||
|
$amount = BetChips::amountForBetId($betId, $resolved['map']);
|
||||||
|
if ($amount === null) {
|
||||||
|
return ['error' => $this->mobileError(1003, 'Invalid parameter value')];
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['bet_id' => $betId, 'amount' => $amount];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 单注最多可选号码个数:`game_config.config_key = pick_max_number_count`
|
* 单注最多可选号码个数:`game_config.config_key = pick_max_number_count`
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -25,12 +25,15 @@ return [
|
|||||||
'Register only supports phone' => 'Register only supports phone',
|
'Register only supports phone' => 'Register only supports phone',
|
||||||
'Invite code required' => 'Invite code is required',
|
'Invite code required' => 'Invite code is required',
|
||||||
'Invite code not bound to channel' => 'This invite code is not bound to a valid channel',
|
'Invite code not bound to channel' => 'This invite code is not bound to a valid channel',
|
||||||
|
'Channel disabled' => 'Channel is disabled',
|
||||||
'Account already registered' => 'This phone number is already registered. Please sign in.',
|
'Account already registered' => 'This phone number is already registered. Please sign in.',
|
||||||
'Please enter the correct mobile number' => 'Please enter the correct mobile number',
|
'Please enter the correct mobile number' => 'Please enter the correct mobile number',
|
||||||
'Registered successfully but login failed' => 'Registered successfully but login failed',
|
'Registered successfully but login failed' => 'Registered successfully but login failed',
|
||||||
'Incorrect account or password' => 'Incorrect account or password',
|
'Incorrect account or password' => 'Incorrect account or password',
|
||||||
'Login status has expired' => 'Login status has expired',
|
'Login status has expired' => 'Login status has expired',
|
||||||
'Game period does not exist' => 'Game period does not exist',
|
'Game period does not exist' => 'Game period does not exist',
|
||||||
|
'Game is paused' => 'Game is paused',
|
||||||
|
'Bet amount out of range' => 'Bet amount out of range',
|
||||||
'Betting is closed' => 'Betting is closed',
|
'Betting is closed' => 'Betting is closed',
|
||||||
'Insufficient balance' => 'Insufficient balance',
|
'Insufficient balance' => 'Insufficient balance',
|
||||||
'Duplicate request' => 'Duplicate request',
|
'Duplicate request' => 'Duplicate request',
|
||||||
|
|||||||
@@ -57,12 +57,15 @@ return [
|
|||||||
'Register only supports phone' => '注册仅支持手机号',
|
'Register only supports phone' => '注册仅支持手机号',
|
||||||
'Invite code required' => '请填写邀请码',
|
'Invite code required' => '请填写邀请码',
|
||||||
'Invite code not bound to channel' => '该邀请码未绑定有效渠道',
|
'Invite code not bound to channel' => '该邀请码未绑定有效渠道',
|
||||||
|
'Channel disabled' => '渠道已关闭',
|
||||||
'Account already registered' => '该手机号已注册,请直接登录',
|
'Account already registered' => '该手机号已注册,请直接登录',
|
||||||
'Please enter the correct mobile number' => '请输入正确的手机号',
|
'Please enter the correct mobile number' => '请输入正确的手机号',
|
||||||
'Registered successfully but login failed' => '注册成功但登录失败',
|
'Registered successfully but login failed' => '注册成功但登录失败',
|
||||||
'Incorrect account or password' => '账号或密码错误',
|
'Incorrect account or password' => '账号或密码错误',
|
||||||
'Login status has expired' => '登录状态已过期',
|
'Login status has expired' => '登录状态已过期',
|
||||||
'Game period does not exist' => '对局不存在',
|
'Game period does not exist' => '对局不存在',
|
||||||
|
'Game is paused' => '游戏已暂停(维护或已关闭运行开关)',
|
||||||
|
'Bet amount out of range' => '单注金额超出允许范围',
|
||||||
'Betting is closed' => '已封盘,禁止下注',
|
'Betting is closed' => '已封盘,禁止下注',
|
||||||
'Insufficient balance' => '余额不足',
|
'Insufficient balance' => '余额不足',
|
||||||
'Duplicate request' => '重复请求(幂等冲突)',
|
'Duplicate request' => '重复请求(幂等冲突)',
|
||||||
|
|||||||
148
app/common/library/game/BetChips.php
Normal file
148
app/common/library/game/BetChips.php
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace app\common\library\game;
|
||||||
|
|
||||||
|
use app\common\service\GameHotDataRedis;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移动端快捷筹码(固定 1–6 档)及默认选中档位,对应 game_config:`bet_chips`(JSON)、`default_bet_chip_id`(int)。
|
||||||
|
*/
|
||||||
|
final class BetChips
|
||||||
|
{
|
||||||
|
public const CONFIG_KEY_CHIPS = 'bet_chips';
|
||||||
|
|
||||||
|
public const CONFIG_KEY_DEFAULT_ID = 'default_bet_chip_id';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int, string> 键 1..6 => 两位小数字符串面额
|
||||||
|
*/
|
||||||
|
public static function defaultChipAmounts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
1 => '1.00',
|
||||||
|
2 => '5.00',
|
||||||
|
3 => '10.00',
|
||||||
|
4 => '25.00',
|
||||||
|
5 => '50.00',
|
||||||
|
6 => '100.00',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{map: array<int, string>, default_id: int}
|
||||||
|
*/
|
||||||
|
public static function resolveFromHotData(): array
|
||||||
|
{
|
||||||
|
$rowChips = GameHotDataRedis::gameConfigRow(self::CONFIG_KEY_CHIPS);
|
||||||
|
$rowDefault = GameHotDataRedis::gameConfigRow(self::CONFIG_KEY_DEFAULT_ID);
|
||||||
|
$rawJson = $rowChips !== null ? ($rowChips['config_value'] ?? null) : null;
|
||||||
|
$map = self::parseChipsJson($rawJson);
|
||||||
|
|
||||||
|
$defaultRaw = $rowDefault !== null ? ($rowDefault['config_value'] ?? null) : null;
|
||||||
|
$defaultId = filter_var(trim('' . $defaultRaw), FILTER_VALIDATE_INT);
|
||||||
|
if ($defaultId === false || $defaultId < 1 || $defaultId > 6) {
|
||||||
|
$defaultId = 1;
|
||||||
|
}
|
||||||
|
$amt = $map[$defaultId] ?? null;
|
||||||
|
if ($amt === null || !is_numeric($amt) || bccomp(bcadd($amt, '0', 2), '0', 2) <= 0) {
|
||||||
|
$defaultId = self::firstPositiveBetId($map);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['map' => $map, 'default_id' => $defaultId];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<int, string> $map
|
||||||
|
*/
|
||||||
|
public static function amountForBetId(int $betId, array $map): ?string
|
||||||
|
{
|
||||||
|
if ($betId < 1 || $betId > 6) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!isset($map[$betId])) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$amt = $map[$betId];
|
||||||
|
if (!is_numeric($amt) || bccomp(bcadd($amt, '0', 2), '0', 2) <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return bcadd($amt, '0', 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* lobbyInit 用:筹码为字典,键为标识字符串 `"1"`…`"6"`,值为两位小数字符串面额。
|
||||||
|
*
|
||||||
|
* @return array{chips: array<string, string>, default_bet_chip_id: int}
|
||||||
|
*/
|
||||||
|
public static function lobbyChipsPayload(): array
|
||||||
|
{
|
||||||
|
$resolved = self::resolveFromHotData();
|
||||||
|
$chips = [];
|
||||||
|
foreach ($resolved['map'] as $id => $amount) {
|
||||||
|
$chips['' . $id] = $amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'chips' => $chips,
|
||||||
|
'default_bet_chip_id' => $resolved['default_id'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param mixed $rawJson
|
||||||
|
* @return array<int, string>
|
||||||
|
*/
|
||||||
|
private static function parseChipsJson($rawJson): array
|
||||||
|
{
|
||||||
|
$defaults = self::defaultChipAmounts();
|
||||||
|
$parsed = [];
|
||||||
|
if (is_string($rawJson) && $rawJson !== '') {
|
||||||
|
$decoded = json_decode($rawJson, true);
|
||||||
|
if (is_array($decoded)) {
|
||||||
|
foreach ($decoded as $k => $v) {
|
||||||
|
$id = filter_var($k, FILTER_VALIDATE_INT);
|
||||||
|
if ($id === false || $id < 1 || $id > 6) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$amtRaw = trim('' . $v);
|
||||||
|
if (!is_numeric($amtRaw) || bccomp(bcadd($amtRaw, '0', 2), '0', 2) <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$parsed[$id] = bcadd($amtRaw, '0', 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$out = [];
|
||||||
|
foreach ($defaults as $id => $def) {
|
||||||
|
if (isset($parsed[$id])) {
|
||||||
|
$out[$id] = $parsed[$id];
|
||||||
|
} else {
|
||||||
|
$out[$id] = $def;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ksort($out);
|
||||||
|
|
||||||
|
return $out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<int, string> $map
|
||||||
|
*/
|
||||||
|
private static function firstPositiveBetId(array $map): int
|
||||||
|
{
|
||||||
|
for ($i = 1; $i <= 6; $i++) {
|
||||||
|
if (!isset($map[$i])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$a = $map[$i];
|
||||||
|
if (is_numeric($a) && bccomp(bcadd($a, '0', 2), '0', 2) > 0) {
|
||||||
|
return $i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,7 +26,7 @@ class LoadLangPack implements MiddlewareInterface
|
|||||||
/**
|
/**
|
||||||
* 解析当前请求语言。
|
* 解析当前请求语言。
|
||||||
* - 后台 admin:优先请求头 think-lang(zh-cn / en),其次 lang 头,再次查询/表单参数 lang(支持 zh→zh-cn)。
|
* - 后台 admin:优先请求头 think-lang(zh-cn / en),其次 lang 头,再次查询/表单参数 lang(支持 zh→zh-cn)。
|
||||||
* - 对外 api:优先查询/表单参数 lang(zh / en),其次 lang 头,再次 think-lang;未显式指定时固定 zh-cn(不使用 Accept-Language)。
|
* - 对外 api:优先查询/表单参数 lang(zh / zh-cn / en),其次 lang 头,再次 think-lang;仅当解析结果为允许列表中的 zh-cn 或 en 时生效,否则固定 zh-cn(不使用 Accept-Language)。仅 lang=en(规范化后)返回英文文案。
|
||||||
*/
|
*/
|
||||||
protected function resolveLangSet(Request $request): string
|
protected function resolveLangSet(Request $request): string
|
||||||
{
|
{
|
||||||
@@ -38,13 +38,28 @@ class LoadLangPack implements MiddlewareInterface
|
|||||||
if ($queryRaw === null || $queryRaw === '') {
|
if ($queryRaw === null || $queryRaw === '') {
|
||||||
$queryRaw = $request->post('lang');
|
$queryRaw = $request->post('lang');
|
||||||
}
|
}
|
||||||
$queryLang = is_string($queryRaw) ? $queryRaw : '';
|
$queryLang = '';
|
||||||
|
if (is_string($queryRaw)) {
|
||||||
|
$queryLang = $queryRaw;
|
||||||
|
} elseif (is_scalar($queryRaw)) {
|
||||||
|
$queryLang = trim('' . $queryRaw);
|
||||||
|
}
|
||||||
|
|
||||||
$thinkRaw = $request->header('think-lang');
|
$thinkRaw = $request->header('think-lang', '');
|
||||||
$thinkLang = is_string($thinkRaw) ? $thinkRaw : '';
|
$thinkLang = '';
|
||||||
|
if (is_string($thinkRaw)) {
|
||||||
|
$thinkLang = $thinkRaw;
|
||||||
|
} elseif (is_array($thinkRaw) && isset($thinkRaw[0]) && is_string($thinkRaw[0])) {
|
||||||
|
$thinkLang = $thinkRaw[0];
|
||||||
|
}
|
||||||
|
|
||||||
$headerLangRaw = $request->header('lang');
|
$headerLangRaw = $request->header('lang', '');
|
||||||
$headerLang = is_string($headerLangRaw) ? $headerLangRaw : '';
|
$headerLang = '';
|
||||||
|
if (is_string($headerLangRaw)) {
|
||||||
|
$headerLang = $headerLangRaw;
|
||||||
|
} elseif (is_array($headerLangRaw) && isset($headerLangRaw[0]) && is_string($headerLangRaw[0])) {
|
||||||
|
$headerLang = $headerLangRaw[0];
|
||||||
|
}
|
||||||
|
|
||||||
$normalize = static function (string $raw): string {
|
$normalize = static function (string $raw): string {
|
||||||
$s = str_replace('_', '-', strtolower(trim($raw)));
|
$s = str_replace('_', '-', strtolower(trim($raw)));
|
||||||
|
|||||||
@@ -231,8 +231,10 @@
|
|||||||
- `open_at`:int(含义:预计开奖时间戳)
|
- `open_at`:int(含义:预计开奖时间戳)
|
||||||
- `bet_config`:object
|
- `bet_config`:object
|
||||||
- `pick_max_number_count`:int(含义:单注最多可选号码数,来自 `game_config.config_key = pick_max_number_count`,缺省与库内种子一致,通常为 10,合法范围 1–36)
|
- `pick_max_number_count`:int(含义:单注最多可选号码数,来自 `game_config.config_key = pick_max_number_count`,缺省与库内种子一致,通常为 10,合法范围 1–36)
|
||||||
- `chips`:array[string](如 `["1.00","5.00"]`,含义:快捷筹码面额)
|
- `chips`:object(含义:快捷筹码字典,固定 6 个键 `"1"`…`"6"`,值为该档单注面额字符串,两位小数;与后台 `game_config.bet_chips` 语义一致)
|
||||||
- `single_number_max_bet`:string(含义:单号码最大下注额)
|
- `default_bet_chip_id`:int(含义:默认选中的筹码标识,来自 `game_config.default_bet_chip_id`,非法或指向无效档位时服务端回退为首个有效档)
|
||||||
|
- `min_bet_per_number`:string(含义:单号码最小下注额,须 ≤ 所选筹码面额且受后台配置约束)
|
||||||
|
- `max_bet_per_number`:string(含义:单号码最大下注额)
|
||||||
- `dictionary`:array<object>
|
- `dictionary`:array<object>
|
||||||
- `number`:int(1-36,含义:字花编号)
|
- `number`:int(1-36,含义:字花编号)
|
||||||
- `name`:string(含义:字花名称)
|
- `name`:string(含义:字花名称)
|
||||||
@@ -263,20 +265,20 @@
|
|||||||
|
|
||||||
### 4.2 提交下注
|
### 4.2 提交下注
|
||||||
- **POST** `/api/game/placeBet`(兼容旧路径 `/api/game/betPlace`)
|
- **POST** `/api/game/placeBet`(兼容旧路径 `/api/game/betPlace`)
|
||||||
- 用途:单期手动下注;玩家传入**压注号码**与**单注金额 `single_bet_amount`**。服务端按 `single_bet_amount × numbers数量` 计算本笔总扣款(落库 `total_amount`),开奖只出一个号码,若该号码 ∈ 所选号码集合即视为中奖。
|
- 用途:单期手动下注;玩家传入**压注号码**与**筹码标识 `bet_id`(1–6)**。单注金额由后台 `game_config.bet_chips` 解析,服务端按 `单注金额 × numbers数量` 计算本笔总扣款(落库 `total_amount`),开奖只出一个号码,若该号码 ∈ 所选号码集合即视为中奖。
|
||||||
|
|
||||||
请求参数:
|
请求参数:
|
||||||
- `period_no`:string(含义:下注目标期号)
|
- `period_no`:string(含义:下注目标期号)
|
||||||
- `numbers`:string(含义:本次压注号码集合,**英文逗号分隔**,如 `1,8,16`;每个号码为 1–36 的整数,数量不超过 `pick_max_number_count`(同 `lobbyInit.bet_config`),重复号码会去重)
|
- `numbers`:string(含义:本次压注号码集合,**英文逗号分隔**,如 `1,8,16`;每个号码为 1–36 的整数,数量不超过 `pick_max_number_count`(同 `lobbyInit.bet_config`),重复号码会去重)
|
||||||
- `single_bet_amount`:string(含义:**单注金额**,> 0)
|
- `bet_id`:int(含义:**快捷筹码标识**,取值 1–6,须为 `lobbyInit.bet_config.chips` 中存在的键;不再使用 `single_bet_amount` / `bet_amount` 传参)
|
||||||
- `bet_amount`:string(兼容字段,含义同 `single_bet_amount`)
|
|
||||||
- `idempotency_key`:string(必填,含义:防止重复下单)
|
- `idempotency_key`:string(必填,含义:防止重复下单)
|
||||||
|
|
||||||
返回参数:
|
返回参数:
|
||||||
- `order_no`:string(含义:下注订单号)
|
- `order_no`:string(含义:下注订单号)
|
||||||
- `period_no`:string(含义:实际落单期号)
|
- `period_no`:string(含义:实际落单期号)
|
||||||
- `status`:string(`accepted`/`rejected`,含义:受理结果)
|
- `status`:string(`accepted`/`rejected`,含义:受理结果)
|
||||||
- `single_bet_amount`:string(含义:本次单注金额)
|
- `bet_id`:int(含义:本次使用的筹码标识)
|
||||||
|
- `single_bet_amount`:string(含义:本次单注金额,由 `bet_id` 对应档位解析得到)
|
||||||
- `numbers_count`:int(含义:本次号码数量)
|
- `numbers_count`:int(含义:本次号码数量)
|
||||||
- `locked_balance`:string(可选,含义:冻结金额)
|
- `locked_balance`:string(可选,含义:冻结金额)
|
||||||
- `balance_after`:string(含义:下单后余额)
|
- `balance_after`:string(含义:下单后余额)
|
||||||
@@ -294,12 +296,14 @@
|
|||||||
- `action`:string(`start`/`stop`)
|
- `action`:string(`start`/`stop`)
|
||||||
- `period_no`:string(`action=start` 时必填)
|
- `period_no`:string(`action=start` 时必填)
|
||||||
- `numbers`:string(`action=start` 时必填,英文逗号分隔)
|
- `numbers`:string(`action=start` 时必填,英文逗号分隔)
|
||||||
- `single_bet_amount`:string(`action=start` 时必填,支持兼容字段 `bet_amount`)
|
- `bet_id`:int(`action=start` 时必填,含义同 `placeBet`,快捷筹码 1–6)
|
||||||
- `rounds`:int(`action=start` 时必填,>=1)
|
- `rounds`:int(`action=start` 时必填,>=1)
|
||||||
|
|
||||||
返回参数:
|
返回参数:
|
||||||
- `status`:string(`scheduled`/`stopped`)
|
- `status`:string(`scheduled`/`stopped`)
|
||||||
- `auto_mode`:bool
|
- `auto_mode`:bool
|
||||||
|
- `bet_id`:int(仅 `start` 返回,本次托管使用的筹码标识)
|
||||||
|
- `single_bet_amount`:string(仅 `start` 返回,由 `bet_id` 解析得到的单注面额)
|
||||||
- `remaining_rounds`:int(仅 `start` 返回)
|
- `remaining_rounds`:int(仅 `start` 返回)
|
||||||
|
|
||||||
### 4.4 查询我的下注记录(最近1个月)
|
### 4.4 查询我的下注记录(最近1个月)
|
||||||
@@ -715,7 +719,7 @@
|
|||||||
- **客户端**:浏览器原生 `WebSocket`(`ws://` / `wss://`)
|
- **客户端**:浏览器原生 `WebSocket`(`ws://` / `wss://`)
|
||||||
- **连接时携带参数(建议)**:
|
- **连接时携带参数(建议)**:
|
||||||
- URL Query:`token`(用户登录态 user-token)、`auth_token`(接口鉴权)、`device_id`(设备标识)、`lang`(`zh/en`)
|
- URL Query:`token`(用户登录态 user-token)、`auth_token`(接口鉴权)、`device_id`(设备标识)、`lang`(`zh/en`)
|
||||||
- 示例:`wss://ws.example.com/game?token=xxx&auth_token=xxx&device_id=ios_001&lang=zh`
|
- 示例:`wss://ws.example.com/ws?token=xxx&auth_token=xxx&device_id=ios_001&lang=zh`
|
||||||
- **连接成功返回(服务端首帧建议)**:
|
- **连接成功返回(服务端首帧建议)**:
|
||||||
- `event`:`ws.connected`
|
- `event`:`ws.connected`
|
||||||
- `connection_id`:连接唯一标识
|
- `connection_id`:连接唯一标识
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
* 执行方法
|
* 执行方法
|
||||||
* php scripts/generate_auth_signature.php
|
* php scripts/generate_auth_signature.php
|
||||||
* php scripts/generate_auth_signature.php 设备码 密钥 时间戳
|
* php scripts/generate_auth_signature.php 设备码 密钥 时间戳
|
||||||
* php scripts/generate_auth_signature.php 1 564d14asdasd113e46542asd6das1a2a 1776331077
|
* php scripts/generate_auth_signature.php 1 564d14asdasd113e46542asd6das1a2a
|
||||||
*/
|
*/
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,12 @@ export default {
|
|||||||
'field_tip bet_seconds': 'How many seconds betting stays open in each period',
|
'field_tip bet_seconds': 'How many seconds betting stays open in each period',
|
||||||
'field pick_max_number_count': 'Max numbers per ticket',
|
'field pick_max_number_count': 'Max numbers per ticket',
|
||||||
'field_tip pick_max_number_count': 'Maximum amount of selectable numbers per ticket',
|
'field_tip pick_max_number_count': 'Maximum amount of selectable numbers per ticket',
|
||||||
|
'field bet_chips': 'Quick chip amounts',
|
||||||
|
'field_tip bet_chips': 'Exactly 6 tiers (ids 1–6, fixed). Edit amounts only; stored as JSON for lobbyInit.',
|
||||||
|
bet_chips_colon: ': ',
|
||||||
|
bet_chips_validate_slot: 'Chip tier {slot} must be a number greater than 0 (prefer ≥ min bet per number)',
|
||||||
|
'field default_bet_chip_id': 'Default selected chip id',
|
||||||
|
'field_tip default_bet_chip_id': 'Default highlighted chip id (1–6), must map to a valid amount in bet_chips',
|
||||||
'field min_bet_per_number': 'Min bet per number',
|
'field min_bet_per_number': 'Min bet per number',
|
||||||
'field_tip min_bet_per_number': 'Minimum bet amount per selected number',
|
'field_tip min_bet_per_number': 'Minimum bet amount per selected number',
|
||||||
'field max_bet_per_number': 'Max bet per number',
|
'field max_bet_per_number': 'Max bet per number',
|
||||||
|
|||||||
@@ -12,6 +12,12 @@ export default {
|
|||||||
'field_tip bet_seconds': '每一局允许下注的时长(秒)',
|
'field_tip bet_seconds': '每一局允许下注的时长(秒)',
|
||||||
'field pick_max_number_count': '单注最多号码个数',
|
'field pick_max_number_count': '单注最多号码个数',
|
||||||
'field_tip pick_max_number_count': '单注最多可选号码数量',
|
'field_tip pick_max_number_count': '单注最多可选号码数量',
|
||||||
|
'field bet_chips': '快捷筹码面额',
|
||||||
|
'field_tip bet_chips': '固定 6 档(标识 1–6 不可改),仅可修改每档面额;保存时写入为 JSON,与移动端 lobbyInit 一致',
|
||||||
|
bet_chips_colon: ':',
|
||||||
|
bet_chips_validate_slot: '第 {slot} 档筹码面额须为大于 0 的数字(建议 ≥ 单号最小下注额)',
|
||||||
|
'field default_bet_chip_id': '默认选中筹码',
|
||||||
|
'field_tip default_bet_chip_id': '大厅默认高亮的筹码标识,须为 1–6 且对应 bet_chips 有效面额',
|
||||||
'field min_bet_per_number': '单号最小下注额',
|
'field min_bet_per_number': '单号最小下注额',
|
||||||
'field_tip min_bet_per_number': '每个号码允许的最小下注金额',
|
'field_tip min_bet_per_number': '每个号码允许的最小下注金额',
|
||||||
'field max_bet_per_number': '单号最大下注额',
|
'field max_bet_per_number': '单号最大下注额',
|
||||||
|
|||||||
@@ -14,6 +14,34 @@
|
|||||||
<el-tabs v-model="state.activeTab" type="border-card">
|
<el-tabs v-model="state.activeTab" type="border-card">
|
||||||
<el-tab-pane :label="t('config.gameConfig.form tab label')" name="game_config">
|
<el-tab-pane :label="t('config.gameConfig.form tab label')" name="game_config">
|
||||||
<div class="config-form-item" v-for="item in state.configList" :key="item.id">
|
<div class="config-form-item" v-for="item in state.configList" :key="item.id">
|
||||||
|
<template v-if="item.config_key === BET_CHIPS_KEY">
|
||||||
|
<el-form-item :label="resolveFieldLabel(item.config_key)">
|
||||||
|
<div class="bet-chips-editor">
|
||||||
|
<div
|
||||||
|
v-for="slot in CHIP_SLOTS"
|
||||||
|
:key="slot"
|
||||||
|
class="bet-chips-row"
|
||||||
|
>
|
||||||
|
<span class="bet-chips-key">{{ slot }}</span>
|
||||||
|
<span class="bet-chips-sep">{{ t('config.gameConfig.bet_chips_colon') }}</span>
|
||||||
|
<el-input-number
|
||||||
|
v-model="state.betChipsAmounts[slot]"
|
||||||
|
:min="0.01"
|
||||||
|
:max="99999999"
|
||||||
|
:precision="2"
|
||||||
|
:step="1"
|
||||||
|
controls-position="right"
|
||||||
|
class="bet-chips-input"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="resolveFieldTip(item.config_key, item.remark)" class="bet-chips-tip">
|
||||||
|
{{ resolveFieldTip(item.config_key, item.remark) }}
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
<div class="config-form-item-name">{{ item.config_key }}</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
<FormItem
|
<FormItem
|
||||||
:label="resolveFieldLabel(item.config_key)"
|
:label="resolveFieldLabel(item.config_key)"
|
||||||
:type="resolveFormType(item.value_type)"
|
:type="resolveFormType(item.value_type)"
|
||||||
@@ -22,6 +50,7 @@
|
|||||||
:tip="resolveFieldTip(item.config_key, item.remark)"
|
:tip="resolveFieldTip(item.config_key, item.remark)"
|
||||||
/>
|
/>
|
||||||
<div class="config-form-item-name">{{ item.config_key }}</div>
|
<div class="config-form-item-name">{{ item.config_key }}</div>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<el-button @click="onReset">{{ t('Reset') }}</el-button>
|
<el-button @click="onReset">{{ t('Reset') }}</el-button>
|
||||||
<el-button v-if="canSave" type="primary" :loading="state.submitLoading" @click="onSubmit()">{{ t('Save') }}</el-button>
|
<el-button v-if="canSave" type="primary" :loading="state.submitLoading" @click="onSubmit()">{{ t('Save') }}</el-button>
|
||||||
@@ -34,6 +63,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
import { onMounted, reactive, useTemplateRef } from 'vue'
|
import { onMounted, reactive, useTemplateRef } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { baTableApi } from '/@/api/common'
|
import { baTableApi } from '/@/api/common'
|
||||||
@@ -53,6 +83,51 @@ interface GameConfigItem {
|
|||||||
remark: string
|
remark: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const BET_CHIPS_KEY = 'bet_chips'
|
||||||
|
const CHIP_SLOTS = [1, 2, 3, 4, 5, 6] as const
|
||||||
|
|
||||||
|
const defaultBetChipsAmounts = (): Record<number, number> => ({
|
||||||
|
1: 1,
|
||||||
|
2: 5,
|
||||||
|
3: 10,
|
||||||
|
4: 25,
|
||||||
|
5: 50,
|
||||||
|
6: 100,
|
||||||
|
})
|
||||||
|
|
||||||
|
const parseBetChipsFromServer = (raw: string): Record<number, number> => {
|
||||||
|
const out = defaultBetChipsAmounts()
|
||||||
|
if (!raw || typeof raw !== 'string') {
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const o = JSON.parse(raw) as unknown
|
||||||
|
if (typeof o !== 'object' || o === null) {
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
const rec = o as Record<string, unknown>
|
||||||
|
for (const slot of CHIP_SLOTS) {
|
||||||
|
const rawVal = rec[String(slot)]
|
||||||
|
const n = Number(rawVal)
|
||||||
|
if (Number.isFinite(n) && n > 0) {
|
||||||
|
out[slot] = n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
/* 使用内置默认 */
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
const serializeBetChips = (amounts: Record<number, number>): string => {
|
||||||
|
const o: Record<string, string> = {}
|
||||||
|
for (const slot of CHIP_SLOTS) {
|
||||||
|
const n = Number(amounts[slot])
|
||||||
|
o[String(slot)] = Number.isFinite(n) ? n.toFixed(2) : '0.00'
|
||||||
|
}
|
||||||
|
return JSON.stringify(o)
|
||||||
|
}
|
||||||
|
|
||||||
const { t, te } = useI18n()
|
const { t, te } = useI18n()
|
||||||
const formRef = useTemplateRef('formRef')
|
const formRef = useTemplateRef('formRef')
|
||||||
const api = new baTableApi('/admin/config.GameConfig/')
|
const api = new baTableApi('/admin/config.GameConfig/')
|
||||||
@@ -66,6 +141,7 @@ const state: {
|
|||||||
remark: string
|
remark: string
|
||||||
configList: GameConfigItem[]
|
configList: GameConfigItem[]
|
||||||
form: Record<string, string | number>
|
form: Record<string, string | number>
|
||||||
|
betChipsAmounts: Record<number, number>
|
||||||
} = reactive({
|
} = reactive({
|
||||||
loading: true,
|
loading: true,
|
||||||
submitLoading: false,
|
submitLoading: false,
|
||||||
@@ -73,6 +149,7 @@ const state: {
|
|||||||
remark: '',
|
remark: '',
|
||||||
configList: [],
|
configList: [],
|
||||||
form: {},
|
form: {},
|
||||||
|
betChipsAmounts: defaultBetChipsAmounts(),
|
||||||
})
|
})
|
||||||
|
|
||||||
const getData = () => {
|
const getData = () => {
|
||||||
@@ -84,6 +161,10 @@ const getData = () => {
|
|||||||
state.remark = res.data.remark || ''
|
state.remark = res.data.remark || ''
|
||||||
const nextForm: Record<string, string | number> = {}
|
const nextForm: Record<string, string | number> = {}
|
||||||
for (const item of list) {
|
for (const item of list) {
|
||||||
|
if (item.config_key === BET_CHIPS_KEY) {
|
||||||
|
state.betChipsAmounts = parseBetChipsFromServer(item.config_value ?? '')
|
||||||
|
continue
|
||||||
|
}
|
||||||
if (item.value_type === 'int' || item.value_type === 'decimal') {
|
if (item.value_type === 'int' || item.value_type === 'decimal') {
|
||||||
const parsed = Number(item.config_value)
|
const parsed = Number(item.config_value)
|
||||||
nextForm[item.config_key] = Number.isNaN(parsed) ? 0 : parsed
|
nextForm[item.config_key] = Number.isNaN(parsed) ? 0 : parsed
|
||||||
@@ -129,17 +210,34 @@ const resolveFieldTip = (configKey: string, fallbackRemark: string) => {
|
|||||||
return fallbackRemark || ''
|
return fallbackRemark || ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const validateBetChips = (): boolean => {
|
||||||
|
for (const slot of CHIP_SLOTS) {
|
||||||
|
const v = state.betChipsAmounts[slot]
|
||||||
|
const n = Number(v)
|
||||||
|
if (!Number.isFinite(n) || n <= 0) {
|
||||||
|
ElMessage.error(t('config.gameConfig.bet_chips_validate_slot', { slot }))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
const onSubmit = async () => {
|
const onSubmit = async () => {
|
||||||
const valid = await formRef.value?.validate().catch(() => false)
|
const valid = await formRef.value?.validate().catch(() => false)
|
||||||
if (!valid) return
|
if (!valid) return
|
||||||
|
if (!validateBetChips()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
state.submitLoading = true
|
state.submitLoading = true
|
||||||
try {
|
try {
|
||||||
|
const items = JSON.parse(JSON.stringify(state.form)) as Record<string, string | number>
|
||||||
|
items[BET_CHIPS_KEY] = serializeBetChips(state.betChipsAmounts)
|
||||||
await createAxios({
|
await createAxios({
|
||||||
url: '/admin/config.GameConfig/save',
|
url: '/admin/config.GameConfig/save',
|
||||||
method: 'post',
|
method: 'post',
|
||||||
data: {
|
data: {
|
||||||
items: JSON.parse(JSON.stringify(state.form)),
|
items,
|
||||||
},
|
},
|
||||||
showSuccessMessage: true,
|
showSuccessMessage: true,
|
||||||
})
|
})
|
||||||
@@ -179,7 +277,7 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
.config-form-item {
|
.config-form-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: flex-start;
|
||||||
.el-form-item {
|
.el-form-item {
|
||||||
flex: 13;
|
flex: 13;
|
||||||
}
|
}
|
||||||
@@ -189,11 +287,42 @@ onMounted(() => {
|
|||||||
color: var(--el-text-color-disabled);
|
color: var(--el-text-color-disabled);
|
||||||
padding-left: 20px;
|
padding-left: 20px;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
padding-top: 28px;
|
||||||
}
|
}
|
||||||
&:hover .config-form-item-name {
|
&:hover .config-form-item-name {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.bet-chips-editor {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 420px;
|
||||||
|
}
|
||||||
|
.bet-chips-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.bet-chips-key {
|
||||||
|
min-width: 18px;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--el-text-color-regular);
|
||||||
|
}
|
||||||
|
.bet-chips-sep {
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
.bet-chips-input {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.bet-chips-tip {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user