diff --git a/README.md b/README.md new file mode 100644 index 0000000..cc0b6ff --- /dev/null +++ b/README.md @@ -0,0 +1,46 @@ +# Line Project Bot + +LINE 平台遊戲測試 Bot 控制台(LP_Bot 重構版)。選擇平台與環境後以 Line Token 登入,可進入機台自動 Spin 測試。 + +## 功能 + +- **登入頁**:選擇平台(LP1、LC1...)與環境(Dev / B2B / Out)+ Line Token + - 平台清單與連線設定即時讀取 Google 試算表「專案_架構 Reference Guide」 + - 「開遊戲」按鈕:直接開啟該平台「跳過維護阻擋(ver2.0 use)」網址 + - 平台/環境/Token 皆記憶於 localStorage + - 下拉選單支援滑鼠滾輪切換選項 +- **大廳**:玩家資訊(aId / 暱稱 / 金幣)+ 機台清單(SD 機台有徽章標示) + - SD 機台 → 開啟自製 Spin 面板;其他機台 → 新視窗開啟 server 網址 +- **SD Spin 面板**:押注、延遲、倍率停、轉數停設定 + Spin/Stop + Log 終端 + +## 開發 + +```bash +npm install +npm run dev # http://localhost:8000 +npm run build # 輸出至 ./build +``` + +首次使用需將 Google service account 憑證放到 `config/credentials.json` +(格式參考 `config/credentials.example.json`,需有試算表唯讀權限)。 + +## 技術備註 + +- Vite + React 19 + antd 6,TypeScript +- **不可使用 React.StrictMode**:開發模式雙重掛載會使 NetManagerSD 等靜態單例的 + socket 連線互相覆蓋,請求全部失敗(status:-1) +- Google Sheets 讀取為純前端:service account 簽 JWT 換 token; + 非安全來源(區網 IP http)無 WebCrypto 時自動 fallback 至 jsrsasign +- SD socket 協定:依 gameUrl 本身的協定決定 ws/wss(bot 頁面協定不可靠) +- Spin 需帶 `mode:0` 的機台:1310、1311、1801、1803、1804、1805 + (特例類別在 `src/define/Game/SlotXXXX.ts`,依 SD3 客戶端原始碼) + - 新增特例:建立類別檔覆寫 `SpinMode`,並在 `src/define/Game/Base/Slot.ts` 匯出 +- 遊戲表 `build-templates/shared/jsons/` 來源:`SD2-Dev/_Debug/shared/jsons` +- 連線參數可用網址參數強制覆寫:`?host=`、`?port=`、`?patch=`、`?downloadurl=`、`?liffid=` + +## 部署 + +```bash +npm run build +# 將 ./build 內容複製到網站目錄,例如 W:\web\MyWeb\Line_Project_Bot +``` diff --git a/src/UI/Lobby/SDGame.tsx b/src/UI/Lobby/SDGame.tsx index 7e114d6..78a06cc 100644 --- a/src/UI/Lobby/SDGame.tsx +++ b/src/UI/Lobby/SDGame.tsx @@ -246,7 +246,7 @@ const SDGame = (props: ISDGame) => { step={0.1} value={delay} onChange={(v: number) => setDelay(+v || 0)} - addonAfter="秒" + suffix="秒" /> @@ -262,7 +262,7 @@ const SDGame = (props: ISDGame) => { min={0} value={ratioStop} onChange={(v: number) => { const n = +v || 0; setRatioStop(n); GameManager.SlotData.RatioStop = n; }} - addonAfter="倍" + suffix="倍" /> } @@ -279,7 +279,7 @@ const SDGame = (props: ISDGame) => { min={0} value={countStop} onChange={(v: number) => { const n = +v || 0; setCountStop(n); GameManager.SlotData.CountStop = n; }} - addonAfter="轉" + suffix="轉" /> } diff --git a/src/components/Image/Image.tsx b/src/components/Image/Image.tsx index 6fc2eae..4c2b2e9 100644 --- a/src/components/Image/Image.tsx +++ b/src/components/Image/Image.tsx @@ -8,7 +8,7 @@ const Image = (props: AvatarProps) => { event.target.src = fallImg; } return ( - + ); }; diff --git a/src/modules/GoogleSheet/GoogleSheetService.ts b/src/modules/GoogleSheet/GoogleSheetService.ts index a9bbb26..7494e5e 100644 --- a/src/modules/GoogleSheet/GoogleSheetService.ts +++ b/src/modules/GoogleSheet/GoogleSheetService.ts @@ -180,8 +180,32 @@ function projectToCode(project: string): string { return code; } -/** 讀「專案清單」取得平台選項(同 quickopen:略過停運與空列) */ +const PLATFORM_LIST_CACHE_KEY: string = "lp_bot_platform_list_cache"; +const PLATFORM_LIST_CACHE_TTL: number = 5 * 60 * 1000; + +/** 讀「專案清單」取得平台選項(同 quickopen:略過停運與空列);sessionStorage 快取 5 分鐘加速開頁 */ export async function fetchPlatformList(): Promise { + try { + const cached: string = sessionStorage.getItem(PLATFORM_LIST_CACHE_KEY); + if (cached) { + const { ts, data } = JSON.parse(cached); + if (Date.now() - ts < PLATFORM_LIST_CACHE_TTL && Array.isArray(data) && data.length > 0) { + return data; + } + } + } catch (e) { + // + } + const options: PlatformOption[] = await fetchPlatformListRemote(); + try { + sessionStorage.setItem(PLATFORM_LIST_CACHE_KEY, JSON.stringify({ ts: Date.now(), data: options })); + } catch (e) { + // + } + return options; +} + +async function fetchPlatformListRemote(): Promise { const rows: string[][] = await getValues(`${PROJECT_LIST_SHEET}!A4:B`); const options: PlatformOption[] = []; let emptyCount: number = 0; @@ -235,10 +259,18 @@ function ensureSlash(url: string): string { return url.endsWith("/") ? url : `${url}/`; } -/** 讀平台分頁全部列 */ +const PLATFORM_ROWS_CACHE_TTL: number = 60 * 1000; +const _platformRowsCache: Map = new Map(); + +/** 讀平台分頁全部列(記憶體快取 60 秒,避免「開遊戲」與「登入」連續操作重複抓表) */ async function getPlatformRows(platform: PlatformOption): Promise<{ sheetTitle: string; rows: string[][] }> { const sheetTitle: string = await findSheetTitle(platform); + const cached = _platformRowsCache.get(sheetTitle); + if (cached && Date.now() - cached.ts < PLATFORM_ROWS_CACHE_TTL) { + return { sheetTitle, rows: cached.rows }; + } const rows: string[][] = await getValues(`${sheetTitle}!A1:E20`); + _platformRowsCache.set(sheetTitle, { ts: Date.now(), rows }); return { sheetTitle, rows }; } diff --git a/vite.config.ts b/vite.config.ts index 9d7fe82..8d990f6 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -21,7 +21,16 @@ export default defineConfig({ }, publicDir: "build-templates", build: { - outDir: "./build" + outDir: "./build", + rollupOptions: { + output: { + // 拆分第三方套件,縮小主 chunk 並提升瀏覽器快取命中率 + manualChunks: { + react: ["react", "react-dom", "react-router-dom"], + antd: ["antd", "dayjs"], + }, + }, + }, }, base: "./", // envDir: "./viteEnv",