后台游戏对局实时显示-优化
This commit is contained in:
@@ -55,4 +55,44 @@ class Live extends Backend
|
|||||||
'event' => 'bet-updated',
|
'event' => 'bet-updated',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function calculate(WebmanRequest $request): Response
|
||||||
|
{
|
||||||
|
$response = $this->initializeBackend($request);
|
||||||
|
if ($response !== null) {
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
if ($request->method() !== 'POST') {
|
||||||
|
return $this->error(__('Parameter error'));
|
||||||
|
}
|
||||||
|
$recordIdRaw = $request->post('record_id');
|
||||||
|
$recordId = is_numeric((string) $recordIdRaw) ? (int) $recordIdRaw : null;
|
||||||
|
$manualRaw = $request->post('manual_number');
|
||||||
|
$manualNumber = is_numeric((string) $manualRaw) ? (int) $manualRaw : null;
|
||||||
|
$res = GameLiveService::calculateResult($recordId, $manualNumber);
|
||||||
|
if (!($res['ok'] ?? false)) {
|
||||||
|
return $this->error((string) ($res['msg'] ?? '计算失败'));
|
||||||
|
}
|
||||||
|
return $this->success((string) $res['msg'], $res);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function draw(WebmanRequest $request): Response
|
||||||
|
{
|
||||||
|
$response = $this->initializeBackend($request);
|
||||||
|
if ($response !== null) {
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
if ($request->method() !== 'POST') {
|
||||||
|
return $this->error(__('Parameter error'));
|
||||||
|
}
|
||||||
|
$recordIdRaw = $request->post('record_id');
|
||||||
|
$recordId = is_numeric((string) $recordIdRaw) ? (int) $recordIdRaw : null;
|
||||||
|
$manualRaw = $request->post('manual_number');
|
||||||
|
$manualNumber = is_numeric((string) $manualRaw) ? (int) $manualRaw : null;
|
||||||
|
$res = GameLiveService::drawResult($recordId, $manualNumber);
|
||||||
|
if (!($res['ok'] ?? false)) {
|
||||||
|
return $this->error((string) ($res['msg'] ?? '开奖失败'));
|
||||||
|
}
|
||||||
|
return $this->success((string) $res['msg'], $res);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,7 @@
|
|||||||
namespace app\admin\controller\game;
|
namespace app\admin\controller\game;
|
||||||
|
|
||||||
use app\common\controller\Backend;
|
use app\common\controller\Backend;
|
||||||
use app\common\service\GameRecordService;
|
|
||||||
use support\Response;
|
use support\Response;
|
||||||
use Throwable;
|
|
||||||
use Webman\Http\Request as WebmanRequest;
|
use Webman\Http\Request as WebmanRequest;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -33,85 +31,39 @@ class Record extends Backend
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function recordSettings(WebmanRequest $request): Response
|
public function add(WebmanRequest $request): Response
|
||||||
{
|
{
|
||||||
$response = $this->initializeBackend($request);
|
$response = $this->initializeBackend($request);
|
||||||
if ($response !== null) {
|
if ($response !== null) {
|
||||||
return $response;
|
return $response;
|
||||||
}
|
}
|
||||||
if ($request->method() === 'GET') {
|
return $this->error('游戏对局记录由系统自动生成,禁止后台手工新增');
|
||||||
return $this->success('', GameRecordService::getRecordSettings());
|
}
|
||||||
|
|
||||||
|
public function edit(WebmanRequest $request): Response
|
||||||
|
{
|
||||||
|
$response = $this->initializeBackend($request);
|
||||||
|
if ($response !== null) {
|
||||||
|
return $response;
|
||||||
}
|
}
|
||||||
if ($request->method() === 'POST') {
|
if ($request->method() === 'POST') {
|
||||||
$data = $request->post();
|
return $this->error('游戏对局记录不可编辑');
|
||||||
if (!is_array($data)) {
|
|
||||||
return $this->error(__('Parameter %s can not be empty', ['']));
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
GameRecordService::saveRecordSettings($data);
|
|
||||||
} catch (Throwable $e) {
|
|
||||||
return $this->error($e->getMessage());
|
|
||||||
}
|
|
||||||
return $this->success(__('Saved successfully'));
|
|
||||||
}
|
}
|
||||||
return $this->error(__('Parameter error'));
|
$pk = $this->model->getPk();
|
||||||
|
$id = $request->get($pk);
|
||||||
|
$row = $this->model->find($id);
|
||||||
|
if (!$row) {
|
||||||
|
return $this->error(__('Record not found'));
|
||||||
|
}
|
||||||
|
return $this->success('', ['row' => $row]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function createNextManual(WebmanRequest $request): Response
|
public function del(WebmanRequest $request): Response
|
||||||
{
|
{
|
||||||
$response = $this->initializeBackend($request);
|
$response = $this->initializeBackend($request);
|
||||||
if ($response !== null) {
|
if ($response !== null) {
|
||||||
return $response;
|
return $response;
|
||||||
}
|
}
|
||||||
if ($request->method() !== 'POST') {
|
return $this->error('游戏对局记录不可删除');
|
||||||
return $this->error(__('Parameter error'));
|
|
||||||
}
|
|
||||||
$result = GameRecordService::createNextRecordForManual();
|
|
||||||
if ($result['ok']) {
|
|
||||||
return $this->success($result['msg'], ['period_no' => $result['period_no'] ?? '']);
|
|
||||||
}
|
|
||||||
return $this->error($result['msg']);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function _add(): Response
|
|
||||||
{
|
|
||||||
if ($this->request && $this->request->method() === 'POST') {
|
|
||||||
$data = $this->request->post();
|
|
||||||
if (!is_array($data)) {
|
|
||||||
return $this->error(__('Parameter %s can not be empty', ['']));
|
|
||||||
}
|
|
||||||
$data = $this->applyInputFilter($data);
|
|
||||||
$data = $this->excludeFields($data);
|
|
||||||
if (!isset($data['period_start_at']) || $data['period_start_at'] === '' || $data['period_start_at'] === null) {
|
|
||||||
$data['period_start_at'] = time();
|
|
||||||
}
|
|
||||||
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) {
|
|
||||||
return $this->success(__('Added successfully'));
|
|
||||||
}
|
|
||||||
return $this->error(__('No rows were added'));
|
|
||||||
}
|
|
||||||
return $this->error(__('Parameter error'));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ final class GameLiveService
|
|||||||
private const BASE_ODDS = 33;
|
private const BASE_ODDS = 33;
|
||||||
private const CHANNEL = 'game-live';
|
private const CHANNEL = 'game-live';
|
||||||
private const EVENT = 'bet-updated';
|
private const EVENT = 'bet-updated';
|
||||||
|
private const KEY_PERIOD_SECONDS = 'period_seconds';
|
||||||
|
private const KEY_BET_SECONDS = 'bet_seconds';
|
||||||
|
private const KEY_PICK_MAX_NUMBER_COUNT = 'pick_max_number_count';
|
||||||
|
|
||||||
public static function buildSnapshot(?int $recordId = null): array
|
public static function buildSnapshot(?int $recordId = null): array
|
||||||
{
|
{
|
||||||
@@ -23,10 +26,25 @@ final class GameLiveService
|
|||||||
'bets' => [],
|
'bets' => [],
|
||||||
'candidate_numbers' => [],
|
'candidate_numbers' => [],
|
||||||
'ai_default_number' => null,
|
'ai_default_number' => null,
|
||||||
|
'calc_number' => null,
|
||||||
|
'period_seconds' => self::getConfigInt(self::KEY_PERIOD_SECONDS, 30),
|
||||||
|
'bet_seconds' => self::getConfigInt(self::KEY_BET_SECONDS, 20),
|
||||||
|
'pick_max_number_count' => self::getPickMaxNumberCount(),
|
||||||
|
'remaining_seconds' => 0,
|
||||||
|
'bet_remaining_seconds' => 0,
|
||||||
|
'can_calculate' => false,
|
||||||
|
'can_draw' => false,
|
||||||
'server_time' => time(),
|
'server_time' => time(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$periodSeconds = self::getConfigInt(self::KEY_PERIOD_SECONDS, 30);
|
||||||
|
$betSeconds = self::getConfigInt(self::KEY_BET_SECONDS, 20);
|
||||||
|
$pickMax = self::getPickMaxNumberCount();
|
||||||
|
$elapsed = max(0, time() - (int) $record['period_start_at']);
|
||||||
|
$remaining = max(0, $periodSeconds - $elapsed);
|
||||||
|
$betRemaining = max(0, $betSeconds - $elapsed);
|
||||||
|
|
||||||
$bets = Db::name('bet_order')
|
$bets = Db::name('bet_order')
|
||||||
->where('period_id', (int) $record['id'])
|
->where('period_id', (int) $record['id'])
|
||||||
->order('id', 'desc')
|
->order('id', 'desc')
|
||||||
@@ -37,15 +55,19 @@ final class GameLiveService
|
|||||||
$candidates = [];
|
$candidates = [];
|
||||||
$bestNumber = null;
|
$bestNumber = null;
|
||||||
$bestLoss = null;
|
$bestLoss = null;
|
||||||
for ($n = 1; $n <= 36; $n++) {
|
$status = (int) $record['status'];
|
||||||
$loss = self::estimateLossForNumber($bets, $n);
|
$canCalculate = $elapsed >= $betSeconds && ($status === 0 || $status === 1);
|
||||||
$candidates[] = [
|
if ($canCalculate) {
|
||||||
'number' => $n,
|
for ($n = 1; $n <= $pickMax; $n++) {
|
||||||
'estimated_loss' => $loss,
|
$loss = self::estimateLossForNumber($bets, $n);
|
||||||
];
|
$candidates[] = [
|
||||||
if ($bestLoss === null || bccomp((string) $loss, (string) $bestLoss, 4) < 0) {
|
'number' => $n,
|
||||||
$bestLoss = $loss;
|
'estimated_loss' => $loss,
|
||||||
$bestNumber = $n;
|
];
|
||||||
|
if ($bestLoss === null || bccomp((string) $loss, (string) $bestLoss, 4) < 0) {
|
||||||
|
$bestLoss = $loss;
|
||||||
|
$bestNumber = $n;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,10 +87,134 @@ final class GameLiveService
|
|||||||
}, $bets),
|
}, $bets),
|
||||||
'candidate_numbers' => $candidates,
|
'candidate_numbers' => $candidates,
|
||||||
'ai_default_number' => $bestNumber,
|
'ai_default_number' => $bestNumber,
|
||||||
|
'calc_number' => $bestNumber,
|
||||||
|
'period_seconds' => $periodSeconds,
|
||||||
|
'bet_seconds' => $betSeconds,
|
||||||
|
'pick_max_number_count' => $pickMax,
|
||||||
|
'remaining_seconds' => $remaining,
|
||||||
|
'bet_remaining_seconds' => $betRemaining,
|
||||||
|
'can_calculate' => $canCalculate,
|
||||||
|
'can_draw' => $canCalculate,
|
||||||
'server_time' => time(),
|
'server_time' => time(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function calculateResult(?int $recordId, ?int $manualNumber = null): array
|
||||||
|
{
|
||||||
|
$record = self::resolveRecord($recordId);
|
||||||
|
if (!$record) {
|
||||||
|
return ['ok' => false, 'msg' => '未找到进行中的对局'];
|
||||||
|
}
|
||||||
|
if (!in_array((int) $record['status'], [0, 1], true)) {
|
||||||
|
return ['ok' => false, 'msg' => '当前对局状态不可计算'];
|
||||||
|
}
|
||||||
|
$periodSeconds = self::getConfigInt(self::KEY_PERIOD_SECONDS, 30);
|
||||||
|
$betSeconds = self::getConfigInt(self::KEY_BET_SECONDS, 20);
|
||||||
|
$elapsed = max(0, time() - (int) $record['period_start_at']);
|
||||||
|
if ($elapsed < $betSeconds) {
|
||||||
|
return ['ok' => false, 'msg' => '下注开放时长未结束,暂不可计算'];
|
||||||
|
}
|
||||||
|
if ((int) $record['status'] === 0) {
|
||||||
|
Db::name('game_record')->where('id', (int) $record['id'])->update([
|
||||||
|
'status' => 1,
|
||||||
|
'update_time' => time(),
|
||||||
|
]);
|
||||||
|
$record['status'] = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pickMax = self::getPickMaxNumberCount();
|
||||||
|
if ($manualNumber !== null && ($manualNumber < 1 || $manualNumber > $pickMax)) {
|
||||||
|
return ['ok' => false, 'msg' => '手动开奖号码超出允许范围'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$bets = Db::name('bet_order')->where('period_id', (int) $record['id'])->select()->toArray();
|
||||||
|
$candidates = [];
|
||||||
|
$bestNumber = null;
|
||||||
|
$bestLoss = null;
|
||||||
|
for ($n = 1; $n <= $pickMax; $n++) {
|
||||||
|
$loss = self::estimateLossForNumber($bets, $n);
|
||||||
|
$candidates[] = ['number' => $n, 'estimated_loss' => $loss];
|
||||||
|
if ($bestLoss === null || bccomp((string) $loss, (string) $bestLoss, 4) < 0) {
|
||||||
|
$bestLoss = $loss;
|
||||||
|
$bestNumber = $n;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$finalNumber = $manualNumber ?? $bestNumber;
|
||||||
|
$finalLoss = '0.0000';
|
||||||
|
if ($finalNumber !== null) {
|
||||||
|
$finalLoss = self::estimateLossForNumber($bets, $finalNumber);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'ok' => true,
|
||||||
|
'msg' => '计算完成',
|
||||||
|
'record' => $record,
|
||||||
|
'period_seconds' => $periodSeconds,
|
||||||
|
'bet_seconds' => $betSeconds,
|
||||||
|
'pick_max_number_count' => $pickMax,
|
||||||
|
'candidate_numbers' => $candidates,
|
||||||
|
'ai_default_number' => $bestNumber,
|
||||||
|
'final_number' => $finalNumber,
|
||||||
|
'final_estimated_loss' => $finalLoss,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function drawResult(?int $recordId, ?int $manualNumber = null): array
|
||||||
|
{
|
||||||
|
$calc = self::calculateResult($recordId, $manualNumber);
|
||||||
|
if (!($calc['ok'] ?? false)) {
|
||||||
|
return $calc;
|
||||||
|
}
|
||||||
|
$record = $calc['record'];
|
||||||
|
$finalNumber = (int) $calc['final_number'];
|
||||||
|
$now = time();
|
||||||
|
Db::startTrans();
|
||||||
|
try {
|
||||||
|
Db::name('game_record')->where('id', (int) $record['id'])->update([
|
||||||
|
'status' => 4,
|
||||||
|
'result_number' => $finalNumber,
|
||||||
|
'draw_mode' => $manualNumber === null ? 0 : 1,
|
||||||
|
'update_time' => $now,
|
||||||
|
]);
|
||||||
|
GameRecordService::createNextRecordAfterDraw();
|
||||||
|
Db::commit();
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
Db::rollback();
|
||||||
|
return ['ok' => false, 'msg' => $e->getMessage()];
|
||||||
|
}
|
||||||
|
|
||||||
|
self::publishSnapshot(null);
|
||||||
|
return [
|
||||||
|
'ok' => true,
|
||||||
|
'msg' => '开奖完成',
|
||||||
|
'result_number' => $finalNumber,
|
||||||
|
'estimated_loss' => $calc['final_estimated_loss'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function tickAutoDraw(): void
|
||||||
|
{
|
||||||
|
$record = self::resolveRecord(null);
|
||||||
|
if (!$record || !in_array((int) $record['status'], [0, 1], true)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$betSeconds = self::getConfigInt(self::KEY_BET_SECONDS, 20);
|
||||||
|
$periodSeconds = self::getConfigInt(self::KEY_PERIOD_SECONDS, 30);
|
||||||
|
$elapsed = max(0, time() - (int) $record['period_start_at']);
|
||||||
|
if ($elapsed >= $betSeconds && (int) $record['status'] === 0) {
|
||||||
|
Db::name('game_record')->where('id', (int) $record['id'])->update([
|
||||||
|
'status' => 1,
|
||||||
|
'update_time' => time(),
|
||||||
|
]);
|
||||||
|
$record['status'] = 1;
|
||||||
|
}
|
||||||
|
if ($elapsed < $periodSeconds) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self::drawResult((int) $record['id'], null);
|
||||||
|
}
|
||||||
|
|
||||||
public static function publishSnapshot(?int $recordId = null): void
|
public static function publishSnapshot(?int $recordId = null): void
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
@@ -94,6 +240,34 @@ final class GameLiveService
|
|||||||
return Db::name('game_record')->whereIn('status', [0, 1, 2, 3])->order('id', 'desc')->find();
|
return Db::name('game_record')->whereIn('status', [0, 1, 2, 3])->order('id', 'desc')->find();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static function getConfigInt(string $key, int $default): int
|
||||||
|
{
|
||||||
|
$row = Db::name('game_config')->where('config_key', $key)->find();
|
||||||
|
if (!$row) {
|
||||||
|
return $default;
|
||||||
|
}
|
||||||
|
$v = $row['config_value'] ?? null;
|
||||||
|
if ($v === null || $v === '') {
|
||||||
|
return $default;
|
||||||
|
}
|
||||||
|
if (!is_numeric((string) $v)) {
|
||||||
|
return $default;
|
||||||
|
}
|
||||||
|
return (int) $v;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function getPickMaxNumberCount(): int
|
||||||
|
{
|
||||||
|
$max = self::getConfigInt(self::KEY_PICK_MAX_NUMBER_COUNT, 36);
|
||||||
|
if ($max < 1) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if ($max > 36) {
|
||||||
|
return 36;
|
||||||
|
}
|
||||||
|
return $max;
|
||||||
|
}
|
||||||
|
|
||||||
private static function estimateLossForNumber(array $bets, int $number): string
|
private static function estimateLossForNumber(array $bets, int $number): string
|
||||||
{
|
{
|
||||||
$payout = '0.0000';
|
$payout = '0.0000';
|
||||||
|
|||||||
@@ -78,6 +78,14 @@ final class GameRecordService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function createNextRecordAfterDraw(): ?string
|
||||||
|
{
|
||||||
|
if (self::hasActiveRecord()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return self::createNextRecordRow();
|
||||||
|
}
|
||||||
|
|
||||||
private static function createNextRecordRow(): string
|
private static function createNextRecordRow(): string
|
||||||
{
|
{
|
||||||
$periodNo = self::generatePeriodNo();
|
$periodNo = self::generatePeriodNo();
|
||||||
|
|||||||
20
app/process/GameLiveTicker.php
Normal file
20
app/process/GameLiveTicker.php
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\process;
|
||||||
|
|
||||||
|
use app\common\service\GameLiveService;
|
||||||
|
use Workerman\Timer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 实时对局:按单局时长自动开奖
|
||||||
|
*/
|
||||||
|
class GameLiveTicker
|
||||||
|
{
|
||||||
|
public function onWorkerStart(): void
|
||||||
|
{
|
||||||
|
Timer::add(1, static function (): void {
|
||||||
|
GameLiveService::tickAutoDraw();
|
||||||
|
GameLiveService::publishSnapshot(null);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -41,6 +41,11 @@ return [
|
|||||||
'count' => 1,
|
'count' => 1,
|
||||||
'reloadable' => false,
|
'reloadable' => false,
|
||||||
],
|
],
|
||||||
|
'gameLiveTicker' => [
|
||||||
|
'handler' => app\process\GameLiveTicker::class,
|
||||||
|
'count' => 1,
|
||||||
|
'reloadable' => false,
|
||||||
|
],
|
||||||
|
|
||||||
// File update detection and automatic reload
|
// File update detection and automatic reload
|
||||||
'monitor' => [
|
'monitor' => [
|
||||||
|
|||||||
@@ -2,6 +2,15 @@ export default {
|
|||||||
tip: 'Listen to pushed bet stream in real time and show the AI default number (minimum estimated platform loss).',
|
tip: 'Listen to pushed bet stream in real time and show the AI default number (minimum estimated platform loss).',
|
||||||
current_record: 'Current round',
|
current_record: 'Current round',
|
||||||
ai_default_number: 'AI default number',
|
ai_default_number: 'AI default number',
|
||||||
|
countdown: 'Countdown',
|
||||||
|
bet_countdown: 'Bet left',
|
||||||
|
draw_countdown: 'Draw left',
|
||||||
|
btn_calc: 'Calculate PnL',
|
||||||
|
btn_draw: 'Draw now',
|
||||||
|
calc_result_number: 'Calculated number',
|
||||||
|
calc_estimated_loss: 'Estimated payout',
|
||||||
|
push_connected: 'Push connected, realtime updates running',
|
||||||
|
push_disconnected: 'Push disconnected, please check service status',
|
||||||
candidate_title: 'Candidate payout estimates',
|
candidate_title: 'Candidate payout estimates',
|
||||||
number: 'Number',
|
number: 'Number',
|
||||||
estimated_loss: 'Estimated payout',
|
estimated_loss: 'Estimated payout',
|
||||||
|
|||||||
@@ -2,6 +2,15 @@ export default {
|
|||||||
tip: '实时监听页面推送的压注记录,并展示AI默认最优开奖号码(平台预估亏损最少)',
|
tip: '实时监听页面推送的压注记录,并展示AI默认最优开奖号码(平台预估亏损最少)',
|
||||||
current_record: '当前对局',
|
current_record: '当前对局',
|
||||||
ai_default_number: 'AI默认开奖号码',
|
ai_default_number: 'AI默认开奖号码',
|
||||||
|
countdown: '倒计时',
|
||||||
|
bet_countdown: '下注剩余',
|
||||||
|
draw_countdown: '开奖剩余',
|
||||||
|
btn_calc: '计算法盈亏',
|
||||||
|
btn_draw: '开奖',
|
||||||
|
calc_result_number: '计算开奖号码',
|
||||||
|
calc_estimated_loss: '计算预估赔付',
|
||||||
|
push_connected: '推送服务已连接,页面数据实时更新中',
|
||||||
|
push_disconnected: '推送服务连接中断,请检查服务是否启动',
|
||||||
candidate_title: '候选号码赔付预估',
|
candidate_title: '候选号码赔付预估',
|
||||||
number: '号码',
|
number: '号码',
|
||||||
estimated_loss: '预估赔付',
|
estimated_loss: '预估赔付',
|
||||||
|
|||||||
@@ -1,14 +1,29 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="default-main">
|
<div class="default-main">
|
||||||
<el-alert type="info" :title="t('game.live.tip')" show-icon class="mb-12" />
|
<el-alert type="info" :title="t('game.live.tip')" show-icon class="mb-12" />
|
||||||
|
<el-alert :type="pushConnected ? 'success' : 'error'" :title="pushConnected ? t('game.live.push_connected') : t('game.live.push_disconnected')" show-icon class="mb-12" />
|
||||||
|
|
||||||
<el-card shadow="never" class="mb-12">
|
<el-card shadow="never" class="mb-12">
|
||||||
<div class="header-row">
|
<div class="header-row">
|
||||||
<div>
|
<div>
|
||||||
<div>{{ t('game.live.current_record') }}: {{ snapshot.record?.period_no || '-' }}</div>
|
<div>{{ t('game.live.current_record') }}: {{ snapshot.record?.period_no || '-' }}</div>
|
||||||
<div>{{ t('game.live.ai_default_number') }}: {{ snapshot.ai_default_number ?? '-' }}</div>
|
<div>{{ t('game.live.ai_default_number') }}: {{ snapshot.ai_default_number ?? '-' }}</div>
|
||||||
|
<div>{{ t('game.live.countdown') }}: {{ countdownText }}</div>
|
||||||
</div>
|
</div>
|
||||||
<el-button type="primary" :loading="loading" @click="loadSnapshot">{{ t('Refresh') }}</el-button>
|
<div class="header-actions">
|
||||||
|
<el-input-number v-model="manualNumber" :min="1" :max="36" :step="1" />
|
||||||
|
<el-button :loading="calcLoading" :disabled="!snapshot.can_calculate" @click="onCalculate">
|
||||||
|
{{ t('game.live.btn_calc') }}
|
||||||
|
</el-button>
|
||||||
|
<el-button type="primary" :loading="drawLoading" :disabled="!snapshot.can_draw" @click="onDraw">
|
||||||
|
{{ t('game.live.btn_draw') }}
|
||||||
|
</el-button>
|
||||||
|
<el-button :loading="loading" @click="loadSnapshot">{{ t('Refresh') }}</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="result-row">
|
||||||
|
<span>{{ t('game.live.calc_result_number') }}: {{ calcResultNumber ?? '-' }}</span>
|
||||||
|
<span>{{ t('game.live.calc_estimated_loss') }}: {{ calcEstimatedLoss }}</span>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
@@ -43,7 +58,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, reactive, ref } from 'vue'
|
import { computed, onMounted, onUnmounted, reactive, ref } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import createAxios from '/@/utils/axios'
|
import createAxios from '/@/utils/axios'
|
||||||
|
|
||||||
@@ -52,19 +67,42 @@ interface Snapshot {
|
|||||||
bets: anyObj[]
|
bets: anyObj[]
|
||||||
candidate_numbers: anyObj[]
|
candidate_numbers: anyObj[]
|
||||||
ai_default_number: number | null
|
ai_default_number: number | null
|
||||||
|
period_seconds?: number
|
||||||
|
bet_seconds?: number
|
||||||
|
pick_max_number_count?: number
|
||||||
|
remaining_seconds?: number
|
||||||
|
bet_remaining_seconds?: number
|
||||||
|
can_calculate?: boolean
|
||||||
|
can_draw?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
const pushConnected = ref(false)
|
||||||
|
const lastPushAt = ref(0)
|
||||||
const snapshot = reactive<Snapshot>({
|
const snapshot = reactive<Snapshot>({
|
||||||
record: null,
|
record: null,
|
||||||
bets: [],
|
bets: [],
|
||||||
candidate_numbers: [],
|
candidate_numbers: [],
|
||||||
ai_default_number: null,
|
ai_default_number: null,
|
||||||
|
period_seconds: 30,
|
||||||
|
bet_seconds: 20,
|
||||||
|
pick_max_number_count: 36,
|
||||||
|
remaining_seconds: 0,
|
||||||
|
bet_remaining_seconds: 0,
|
||||||
|
can_calculate: false,
|
||||||
|
can_draw: false,
|
||||||
})
|
})
|
||||||
|
const calcLoading = ref(false)
|
||||||
|
const drawLoading = ref(false)
|
||||||
|
const manualNumber = ref<number | null>(1)
|
||||||
|
const calcResultNumber = ref<number | null>(null)
|
||||||
|
const calcEstimatedLoss = ref<string>('0.0000')
|
||||||
|
|
||||||
let pushClient: any = null
|
let pushClient: any = null
|
||||||
let pushChannel: any = null
|
let pushChannel: any = null
|
||||||
|
let pollTimer: number | null = null
|
||||||
|
let pushWatchdogTimer: number | null = null
|
||||||
|
|
||||||
function formatPicks(v: unknown): string {
|
function formatPicks(v: unknown): string {
|
||||||
if (Array.isArray(v)) return JSON.stringify(v)
|
if (Array.isArray(v)) return JSON.stringify(v)
|
||||||
@@ -81,6 +119,14 @@ async function loadSnapshot() {
|
|||||||
snapshot.bets = res.data.bets || []
|
snapshot.bets = res.data.bets || []
|
||||||
snapshot.candidate_numbers = res.data.candidate_numbers || []
|
snapshot.candidate_numbers = res.data.candidate_numbers || []
|
||||||
snapshot.ai_default_number = res.data.ai_default_number
|
snapshot.ai_default_number = res.data.ai_default_number
|
||||||
|
snapshot.period_seconds = res.data.period_seconds ?? 30
|
||||||
|
snapshot.bet_seconds = res.data.bet_seconds ?? 20
|
||||||
|
snapshot.pick_max_number_count = 36
|
||||||
|
snapshot.remaining_seconds = res.data.remaining_seconds ?? 0
|
||||||
|
snapshot.bet_remaining_seconds = res.data.bet_remaining_seconds ?? 0
|
||||||
|
snapshot.can_calculate = !!res.data.can_calculate
|
||||||
|
snapshot.can_draw = !!res.data.can_draw
|
||||||
|
if (manualNumber.value === null || manualNumber.value < 1 || manualNumber.value > 36) manualNumber.value = 1
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
@@ -90,23 +136,49 @@ async function loadSnapshot() {
|
|||||||
async function initPush() {
|
async function initPush() {
|
||||||
const cfgRes = await createAxios({ url: '/admin/game.Live/pushConfig', method: 'get', showCodeMessage: false })
|
const cfgRes = await createAxios({ url: '/admin/game.Live/pushConfig', method: 'get', showCodeMessage: false })
|
||||||
if (cfgRes.code !== 1 || !cfgRes.data) {
|
if (cfgRes.code !== 1 || !cfgRes.data) {
|
||||||
|
pushConnected.value = false
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const { url, app_key, channel, event } = cfgRes.data
|
const { url, app_key, channel, event } = cfgRes.data
|
||||||
|
|
||||||
await loadPushJs()
|
try {
|
||||||
const PushCtor = (window as any).Push
|
await loadPushJs()
|
||||||
if (!PushCtor) {
|
} catch {
|
||||||
|
pushConnected.value = false
|
||||||
|
startPolling()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
pushClient = new PushCtor({ url, app_key })
|
const PushCtor = (window as any).Push
|
||||||
pushChannel = pushClient.subscribe(channel)
|
if (!PushCtor) {
|
||||||
pushChannel.on(event, (payload: anyObj) => {
|
pushConnected.value = false
|
||||||
snapshot.record = payload.record || null
|
startPolling()
|
||||||
snapshot.bets = payload.bets || []
|
return
|
||||||
snapshot.candidate_numbers = payload.candidate_numbers || []
|
}
|
||||||
snapshot.ai_default_number = payload.ai_default_number ?? null
|
try {
|
||||||
})
|
pushClient = new PushCtor({ url, app_key })
|
||||||
|
pushChannel = pushClient.subscribe(channel)
|
||||||
|
pushConnected.value = false
|
||||||
|
startPushWatchdog()
|
||||||
|
stopPolling()
|
||||||
|
pushChannel.on(event, (payload: anyObj) => {
|
||||||
|
lastPushAt.value = Date.now()
|
||||||
|
pushConnected.value = true
|
||||||
|
snapshot.record = payload.record || null
|
||||||
|
snapshot.bets = payload.bets || []
|
||||||
|
snapshot.candidate_numbers = payload.candidate_numbers || []
|
||||||
|
snapshot.ai_default_number = payload.ai_default_number ?? null
|
||||||
|
snapshot.period_seconds = payload.period_seconds ?? 30
|
||||||
|
snapshot.bet_seconds = payload.bet_seconds ?? 20
|
||||||
|
snapshot.pick_max_number_count = 36
|
||||||
|
snapshot.remaining_seconds = payload.remaining_seconds ?? 0
|
||||||
|
snapshot.bet_remaining_seconds = payload.bet_remaining_seconds ?? 0
|
||||||
|
snapshot.can_calculate = !!payload.can_calculate
|
||||||
|
snapshot.can_draw = !!payload.can_draw
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
pushConnected.value = false
|
||||||
|
startPolling()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadPushJs() {
|
async function loadPushJs() {
|
||||||
@@ -122,10 +194,113 @@ async function loadPushJs() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function onCalculate() {
|
||||||
|
if (!snapshot.record) return
|
||||||
|
calcLoading.value = true
|
||||||
|
try {
|
||||||
|
const res = await createAxios({
|
||||||
|
url: '/admin/game.Live/calculate',
|
||||||
|
method: 'post',
|
||||||
|
data: {
|
||||||
|
record_id: snapshot.record.id,
|
||||||
|
manual_number: manualNumber.value,
|
||||||
|
},
|
||||||
|
showSuccessMessage: true,
|
||||||
|
})
|
||||||
|
if (res.code === 1 && res.data) {
|
||||||
|
snapshot.candidate_numbers = res.data.candidate_numbers || []
|
||||||
|
snapshot.ai_default_number = res.data.ai_default_number ?? null
|
||||||
|
calcResultNumber.value = res.data.final_number ?? null
|
||||||
|
calcEstimatedLoss.value = String(res.data.final_estimated_loss ?? '0.0000')
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
calcLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onDraw() {
|
||||||
|
if (!snapshot.record) return
|
||||||
|
drawLoading.value = true
|
||||||
|
try {
|
||||||
|
await createAxios({
|
||||||
|
url: '/admin/game.Live/draw',
|
||||||
|
method: 'post',
|
||||||
|
data: {
|
||||||
|
record_id: snapshot.record.id,
|
||||||
|
manual_number: manualNumber.value,
|
||||||
|
},
|
||||||
|
showSuccessMessage: true,
|
||||||
|
})
|
||||||
|
await loadSnapshot()
|
||||||
|
} finally {
|
||||||
|
drawLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const countdownText = computed(() => {
|
||||||
|
const total = snapshot.remaining_seconds ?? 0
|
||||||
|
const bet = snapshot.bet_remaining_seconds ?? 0
|
||||||
|
return `${t('game.live.bet_countdown')} ${bet}s / ${t('game.live.draw_countdown')} ${total}s`
|
||||||
|
})
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await loadSnapshot()
|
await loadSnapshot()
|
||||||
await initPush()
|
try {
|
||||||
|
await initPush()
|
||||||
|
} catch {
|
||||||
|
pushConnected.value = false
|
||||||
|
startPolling()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
try {
|
||||||
|
if (pushClient && typeof pushClient.disconnect === 'function') {
|
||||||
|
pushClient.disconnect()
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
stopPolling()
|
||||||
|
stopPushWatchdog()
|
||||||
|
})
|
||||||
|
|
||||||
|
function startPolling() {
|
||||||
|
if (pollTimer !== null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pollTimer = window.setInterval(() => {
|
||||||
|
void loadSnapshot()
|
||||||
|
}, 2000)
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopPolling() {
|
||||||
|
if (pollTimer !== null) {
|
||||||
|
window.clearInterval(pollTimer)
|
||||||
|
pollTimer = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function startPushWatchdog() {
|
||||||
|
if (pushWatchdogTimer !== null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pushWatchdogTimer = window.setInterval(() => {
|
||||||
|
const state = pushClient && pushClient.connection ? String(pushClient.connection.state || '') : ''
|
||||||
|
const stateConnected = state === 'connected' || state === 'connecting'
|
||||||
|
const hasRecentPush = lastPushAt.value > 0 && Date.now() - lastPushAt.value <= 6000
|
||||||
|
if (!stateConnected || !hasRecentPush) {
|
||||||
|
pushConnected.value = false
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopPushWatchdog() {
|
||||||
|
if (pushWatchdogTimer !== null) {
|
||||||
|
window.clearInterval(pushWatchdogTimer)
|
||||||
|
pushWatchdogTimer = null
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
@@ -136,5 +311,16 @@ onMounted(async () => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.header-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.result-row {
|
||||||
|
margin-top: 12px;
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -2,31 +2,8 @@
|
|||||||
<div class="default-main ba-table-box">
|
<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 />
|
<el-alert class="ba-table-alert" v-if="baTable.table.remark" :title="baTable.table.remark" type="info" show-icon />
|
||||||
|
|
||||||
<el-card v-if="canSettings" class="record-settings-card" shadow="never">
|
|
||||||
<template #header>
|
|
||||||
<span>{{ t('game.record.section_auto') }}</span>
|
|
||||||
</template>
|
|
||||||
<div v-loading="settingsLoading" class="record-settings-body">
|
|
||||||
<div class="record-setting-row">
|
|
||||||
<span class="record-setting-label">{{ t('game.record.auto_create_label') }}</span>
|
|
||||||
<el-switch v-model="autoCreate" :disabled="settingsSaving" @change="onSwitchChange" />
|
|
||||||
<span class="record-setting-tip">{{ t('game.record.auto_create_tip') }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="record-setting-row">
|
|
||||||
<span class="record-setting-label">{{ t('game.record.manual_create_label') }}</span>
|
|
||||||
<el-switch v-model="manualCreate" :disabled="settingsSaving" @change="onSwitchChange" />
|
|
||||||
<span class="record-setting-tip">{{ t('game.record.manual_create_tip') }}</span>
|
|
||||||
</div>
|
|
||||||
<div v-if="canManual" class="record-setting-actions">
|
|
||||||
<el-button type="primary" :loading="createLoading" @click="onCreateNextManual">
|
|
||||||
{{ t('game.record.btn_create_next') }}
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</el-card>
|
|
||||||
|
|
||||||
<TableHeader
|
<TableHeader
|
||||||
:buttons="['refresh', 'add', 'edit', 'delete', 'comSearch', 'quickSearch', 'columnDisplay']"
|
:buttons="['refresh', 'comSearch', 'quickSearch', 'columnDisplay']"
|
||||||
:quick-search-placeholder="t('Quick search placeholder', { fields: t('game.record.quick Search Fields') })"
|
:quick-search-placeholder="t('Quick search placeholder', { fields: t('game.record.quick Search Fields') })"
|
||||||
></TableHeader>
|
></TableHeader>
|
||||||
|
|
||||||
@@ -37,16 +14,14 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, provide, ref, 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 { baTableApi } from '/@/api/common'
|
import { baTableApi } from '/@/api/common'
|
||||||
import { defaultOptButtons } from '/@/components/table'
|
import { defaultOptButtons } from '/@/components/table'
|
||||||
import TableHeader from '/@/components/table/header/index.vue'
|
import TableHeader from '/@/components/table/header/index.vue'
|
||||||
import Table from '/@/components/table/index.vue'
|
import Table from '/@/components/table/index.vue'
|
||||||
import createAxios from '/@/utils/axios'
|
|
||||||
import baTableClass from '/@/utils/baTable'
|
import baTableClass from '/@/utils/baTable'
|
||||||
import { auth } from '/@/utils/common'
|
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'game/record',
|
name: 'game/record',
|
||||||
@@ -54,17 +29,7 @@ defineOptions({
|
|||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const tableRef = useTemplateRef('tableRef')
|
const tableRef = useTemplateRef('tableRef')
|
||||||
const optButtons: OptButton[] = defaultOptButtons(['edit', 'delete'])
|
const optButtons: OptButton[] = defaultOptButtons(['edit'])
|
||||||
|
|
||||||
const settingsLoading = ref(false)
|
|
||||||
const settingsSaving = ref(false)
|
|
||||||
const createLoading = ref(false)
|
|
||||||
const autoCreate = ref(false)
|
|
||||||
const manualCreate = ref(false)
|
|
||||||
const settingsReady = ref(false)
|
|
||||||
|
|
||||||
const canSettings = computed(() => auth('recordSettings'))
|
|
||||||
const canManual = computed(() => auth('createNextManual'))
|
|
||||||
|
|
||||||
const baTable = new baTableClass(
|
const baTable = new baTableClass(
|
||||||
new baTableApi('/admin/game.Record/'),
|
new baTableApi('/admin/game.Record/'),
|
||||||
@@ -112,92 +77,13 @@ const baTable = new baTableClass(
|
|||||||
|
|
||||||
provide('baTable', baTable)
|
provide('baTable', baTable)
|
||||||
|
|
||||||
async function loadRecordSettings() {
|
|
||||||
if (!canSettings.value) return
|
|
||||||
settingsLoading.value = true
|
|
||||||
try {
|
|
||||||
const res = await createAxios({ url: '/admin/game.Record/recordSettings', method: 'get', showCodeMessage: false })
|
|
||||||
if (res.code === 1 && res.data) {
|
|
||||||
autoCreate.value = res.data.period_auto_create_enabled === 1
|
|
||||||
manualCreate.value = res.data.period_manual_create_enabled === 1
|
|
||||||
settingsReady.value = true
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
settingsLoading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function onSaveSettings() {
|
|
||||||
if (!canSettings.value) return
|
|
||||||
settingsSaving.value = true
|
|
||||||
try {
|
|
||||||
await createAxios({
|
|
||||||
url: '/admin/game.Record/recordSettings',
|
|
||||||
method: 'post',
|
|
||||||
data: {
|
|
||||||
period_auto_create_enabled: autoCreate.value ? 1 : 0,
|
|
||||||
period_manual_create_enabled: manualCreate.value ? 1 : 0,
|
|
||||||
},
|
|
||||||
showSuccessMessage: true,
|
|
||||||
})
|
|
||||||
} finally {
|
|
||||||
settingsSaving.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onSwitchChange() {
|
|
||||||
if (!settingsReady.value) return
|
|
||||||
void onSaveSettings()
|
|
||||||
}
|
|
||||||
|
|
||||||
async function onCreateNextManual() {
|
|
||||||
if (!canManual.value) return
|
|
||||||
createLoading.value = true
|
|
||||||
try {
|
|
||||||
await createAxios({ url: '/admin/game.Record/createNextManual', method: 'post', showSuccessMessage: true })
|
|
||||||
await baTable.getData()
|
|
||||||
} finally {
|
|
||||||
createLoading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
baTable.table.ref = tableRef.value
|
baTable.table.ref = tableRef.value
|
||||||
baTable.mount()
|
baTable.mount()
|
||||||
void loadRecordSettings()
|
|
||||||
baTable.getData()?.then(() => {
|
baTable.getData()?.then(() => {
|
||||||
baTable.initSort()
|
baTable.initSort()
|
||||||
baTable.dragSort()
|
baTable.dragSort()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
<style scoped lang="scss"></style>
|
||||||
<style scoped lang="scss">
|
|
||||||
.record-settings-card {
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
.record-settings-body {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
.record-setting-row {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
.record-setting-label {
|
|
||||||
min-width: 160px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
.record-setting-tip {
|
|
||||||
color: var(--el-text-color-secondary);
|
|
||||||
font-size: 13px;
|
|
||||||
flex: 1;
|
|
||||||
min-width: 200px;
|
|
||||||
}
|
|
||||||
.record-setting-actions {
|
|
||||||
margin-top: 4px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
</template>
|
</template>
|
||||||
<el-scrollbar v-loading="baTable.form.loading" class="ba-table-form-scrollbar">
|
<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)'">
|
<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">
|
<el-form v-if="!baTable.form.loading" ref="formRef" :model="baTable.form.items" :label-position="config.layout.shrink ? 'top' : 'right'" :label-width="baTable.form.labelWidth + 'px'" :rules="rules" :disabled="true">
|
||||||
<FormItem :label="t('game.record.period_no')" type="string" v-model="baTable.form.items!.period_no" prop="period_no" />
|
<FormItem :label="t('game.record.period_no')" type="string" v-model="baTable.form.items!.period_no" prop="period_no" />
|
||||||
<FormItem :label="t('game.record.period_start_at')" type="datetime" v-model="baTable.form.items!.period_start_at" prop="period_start_at" />
|
<FormItem :label="t('game.record.period_start_at')" type="datetime" v-model="baTable.form.items!.period_start_at" prop="period_start_at" />
|
||||||
<FormItem
|
<FormItem
|
||||||
@@ -33,9 +33,6 @@
|
|||||||
<template #footer>
|
<template #footer>
|
||||||
<div :style="'width: calc(100% - ' + baTable.form.labelWidth! / 1.8 + 'px)'">
|
<div :style="'width: calc(100% - ' + baTable.form.labelWidth! / 1.8 + 'px)'">
|
||||||
<el-button @click="baTable.toggleForm()">{{ t('Cancel') }}</el-button>
|
<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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ const viteConfig = ({ mode }: ConfigEnv): UserConfig => {
|
|||||||
'/api': { target: 'http://localhost:8787', changeOrigin: true },
|
'/api': { target: 'http://localhost:8787', changeOrigin: true },
|
||||||
'/admin': { target: 'http://localhost:8787', changeOrigin: true },
|
'/admin': { target: 'http://localhost:8787', changeOrigin: true },
|
||||||
'/install': { target: 'http://localhost:8787', changeOrigin: true },
|
'/install': { target: 'http://localhost:8787', changeOrigin: true },
|
||||||
|
'/plugin': { target: 'http://localhost:8787', changeOrigin: true },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
|
|||||||
Reference in New Issue
Block a user