新增 README 並優化載入效能
根本原因: 專案缺少說明文件;bundle 為單一 910KB 主檔改版快取效率差,且每次開頁 與連續操作都會重複呼叫 Google Sheets API。 影響: 主 chunk 降至 216KB(react/antd 拆出可長期快取,jsrsasign 僅非安全來源 才動態載入);平台清單 sessionStorage 快取 5 分鐘、平台分頁記憶體快取 60 秒;機台圖片懶載入;InputNumber 改用 suffix 消除棄用警告。 修法: - vite.config.ts 加入 manualChunks 拆分 react / antd+dayjs - GoogleSheetService 加入兩層快取 - Image 元件預設 loading="lazy" - SDGame 三個 InputNumber 的 addonAfter 改為 suffix - 新增 README.md(功能、開發、技術備註、部署) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||||
|
```
|
||||||
@@ -246,7 +246,7 @@ const SDGame = (props: ISDGame) => {
|
|||||||
step={0.1}
|
step={0.1}
|
||||||
value={delay}
|
value={delay}
|
||||||
onChange={(v: number) => setDelay(+v || 0)}
|
onChange={(v: number) => setDelay(+v || 0)}
|
||||||
addonAfter="秒"
|
suffix="秒"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -262,7 +262,7 @@ const SDGame = (props: ISDGame) => {
|
|||||||
min={0}
|
min={0}
|
||||||
value={ratioStop}
|
value={ratioStop}
|
||||||
onChange={(v: number) => { const n = +v || 0; setRatioStop(n); GameManager.SlotData.RatioStop = n; }}
|
onChange={(v: number) => { const n = +v || 0; setRatioStop(n); GameManager.SlotData.RatioStop = n; }}
|
||||||
addonAfter="倍"
|
suffix="倍"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@@ -279,7 +279,7 @@ const SDGame = (props: ISDGame) => {
|
|||||||
min={0}
|
min={0}
|
||||||
value={countStop}
|
value={countStop}
|
||||||
onChange={(v: number) => { const n = +v || 0; setCountStop(n); GameManager.SlotData.CountStop = n; }}
|
onChange={(v: number) => { const n = +v || 0; setCountStop(n); GameManager.SlotData.CountStop = n; }}
|
||||||
addonAfter="轉"
|
suffix="轉"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ const Image = (props: AvatarProps) => {
|
|||||||
event.target.src = fallImg;
|
event.target.src = fallImg;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<img {...props} onError={onError} />
|
<img loading="lazy" {...props} onError={onError} />
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -180,8 +180,32 @@ function projectToCode(project: string): string {
|
|||||||
return code;
|
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<PlatformOption[]> {
|
export async function fetchPlatformList(): Promise<PlatformOption[]> {
|
||||||
|
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<PlatformOption[]> {
|
||||||
const rows: string[][] = await getValues(`${PROJECT_LIST_SHEET}!A4:B`);
|
const rows: string[][] = await getValues(`${PROJECT_LIST_SHEET}!A4:B`);
|
||||||
const options: PlatformOption[] = [];
|
const options: PlatformOption[] = [];
|
||||||
let emptyCount: number = 0;
|
let emptyCount: number = 0;
|
||||||
@@ -235,10 +259,18 @@ function ensureSlash(url: string): string {
|
|||||||
return url.endsWith("/") ? url : `${url}/`;
|
return url.endsWith("/") ? url : `${url}/`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 讀平台分頁全部列 */
|
const PLATFORM_ROWS_CACHE_TTL: number = 60 * 1000;
|
||||||
|
const _platformRowsCache: Map<string, { ts: number; rows: string[][] }> = new Map();
|
||||||
|
|
||||||
|
/** 讀平台分頁全部列(記憶體快取 60 秒,避免「開遊戲」與「登入」連續操作重複抓表) */
|
||||||
async function getPlatformRows(platform: PlatformOption): Promise<{ sheetTitle: string; rows: string[][] }> {
|
async function getPlatformRows(platform: PlatformOption): Promise<{ sheetTitle: string; rows: string[][] }> {
|
||||||
const sheetTitle: string = await findSheetTitle(platform);
|
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`);
|
const rows: string[][] = await getValues(`${sheetTitle}!A1:E20`);
|
||||||
|
_platformRowsCache.set(sheetTitle, { ts: Date.now(), rows });
|
||||||
return { sheetTitle, rows };
|
return { sheetTitle, rows };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+10
-1
@@ -21,7 +21,16 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
publicDir: "build-templates",
|
publicDir: "build-templates",
|
||||||
build: {
|
build: {
|
||||||
outDir: "./build"
|
outDir: "./build",
|
||||||
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
// 拆分第三方套件,縮小主 chunk 並提升瀏覽器快取命中率
|
||||||
|
manualChunks: {
|
||||||
|
react: ["react", "react-dom", "react-router-dom"],
|
||||||
|
antd: ["antd", "dayjs"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
base: "./",
|
base: "./",
|
||||||
// envDir: "./viteEnv",
|
// envDir: "./viteEnv",
|
||||||
|
|||||||
Reference in New Issue
Block a user