將製作ejcees(中國象棋打譜程式)

分享和讨论Javascript相关的话题
回复
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5337
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 176 次
被圈友点赞: 204 次
联系:

Re: 將製作ejcees(中國象棋打譜程式)

帖子 ejsoon »

將該程式改成js,同時將其對象化:
一,整個Class設為Ejcees,將所有變量和方法都用this.來歸放。

二,style,svg.ejceespb都是動態加入。

三,當頁面上存在div.ejcees[mode=ejceesmain]時,說明它是單頁面,整個body只存在這個元素以及這個js的引用。則表現跟現在完全一樣。

四,當頁面上存在div.ejcees[mode=ejceessingle]時,說明它將插入到頁面上的一個主要的地方,比如文章正文。則它的css最大寬度的計算應跟它所插入的地方的父元素一致。div.ejcees[mode=ejceessingle]在一個頁面上只能存在一個,能使用鍵盤上的快捷鍵。

五,當頁面上存在一個或多個div.ejcees[mode=ejceesmini]時,說明將有ejceesmini插入到頁面上的任意地方,它的css最大寬度的計算應跟它所插入的地方的父元素一致。其css以及顯示元素要經過較大調整,ejceestool和ejceestext將不再顯示。ejceesstep的最左邊將放置ejceesoutput,固定其長寬,並且只顯示當前步數和總步數(如20/80)。ejceesstep的最右邊將顯示btn-autoplay按鈕。當有分支時,它將直接絕對定位背景半透明顯示在ejceessvg的左上角,當有評論時,它將直接絕對定位背景半透明顯示在ejceessvg的最底部。div.ejcees[mode=ejceesmini]將不能使用鍵盤上的快捷鍵。

先給出工作流程,製作方法以及注意事項。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5337
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 176 次
被圈友点赞: 204 次
联系:

Re: 將製作ejcees(中國象棋打譜程式)

帖子 ejsoon »

gemini pro說任務很繁重,你以為我不會交給你來做?我只是分階段交:

代码: 全选

這是一個非常龐大且經典的前端重構任務。將面條式(Spaghetti code)的腳本轉換為物件導向(OOP),並且要支援多實例(Multi-instance)與不同的響應式佈局模式,需要非常嚴謹的架構規劃。

以下為您規劃的**工作流程**、**製作方法**以及**核心注意事項**,並附上重構的**程式碼骨架**。

---

### 一、 工作流程 (Workflow)

1.  **環境與基礎類別建立:**
    * 定義 `class Ejcees`。
    * 在建構子(`constructor`)中接收 DOM 元素和配置參數。
    * 設定靜態方法掃描頁面上的 `div.ejcees` 並自動實例化。
2.  **動態資源注入 (CSS & SVG):**
    * 將原本的 CSS 寫成字串,並在第一個實例創建時,動態插入到 `<head>` 中(全域只需插入一次)。
    * 將 SVG 結構與 UI 結構寫成模板字串,根據不同的 `mode` 動態生成並插入到該實例的容器中。
3.  **狀態與變數封裝:**
    * 將原本全域的 `let` 變數(如 `currentTurn`, `historyFEN`, `isEditMode` 等)全部轉換為實例屬性(`this.currentTurn`, `this.historyFEN` 等)。
4.  **DOM 選擇器作用域限制:**
    * 將所有 `document.getElementById` 或 `document.querySelector` 替換為 `this.container.querySelector`,確保多實例情況下不會互相干擾。
5.  **事件綁定與 `this` 指向修正:**
    * 所有事件監聽器(Event Listeners)必須使用箭頭函數 `() => {}` 或 `.bind(this)`,否則 `this` 會指向 DOM 元素而非 `Ejcees` 實例。
6.  **模式特異化開發 (Mode Specifics):**
    * **ejceesmain:** 預設模式,滿版顯示。
    * **ejceessingle:** 插入文章流中,寬度依賴父元素,需掛載鍵盤快捷鍵(全域唯一)。
    * **ejceesmini:** 極簡模式,移除工具列與紀錄區,重構底部控制條,並將分支與評論改為絕對定位的懸浮層(Overlay)。禁止綁定鍵盤事件。
7.  **測試與除錯:**
    * 先測試單一實例(`ejceesmain`),確保基礎功能(走子、悔棋、動畫)正常。
    * 再測試多實例(多個 `ejceesmini` 同時存在),檢查變數是否污染、SVG 渲染是否衝突。

---

### 二、 製作方法與架構設計

#### 1. 初始化與入口
在檔案底部加入一段自動掃描腳本。當 DOM 載入完成後,尋找所有帶有 `class="ejcees"` 的元素,讀取其 `mode` 屬性,並 `new Ejcees(element, mode)`。

#### 2. CSS 隔離與動態注入
原本的 CSS 是針對全螢幕設計的(`100vw`, `100vh`)。重構時:
* `.ejceesmain` 保持使用 `100vw` / `100vh`。
* `.ejceessingle` 與 `.ejceesmini` 應設定 `width: 100%; height: auto; aspect-ratio: ...` 或依賴父容器。

#### 3. SVG ID 衝突解決方案
這是多實例最容易出錯的地方!原本的 SVG 定義了 `<defs>`,裡面有 `#tile0`, `#tile1` 等。
如果頁面上有三個 `ejceesmini`,就會有三個 `<g id="tile0">`。這在 HTML 標準中是不合法的,且會導致渲染錯誤。
**解法:** 在實例化時,生成一個唯一的 `instanceId`(例如 `ejcees-1234`),將所有的 ID 與 `href` 引用加上後綴(如 `id="tile0-ejcees-1234"`, `href="#tile0-ejcees-1234"`)。

#### 4. UI 渲染邏輯重構 (`ejceesmini` 專屬)
在 `renderUI` 階段判斷:
* 如果是 `mini`,隱藏或不生成 `.ejceestool` 和 `.ejceestext`。
* 修改 `.ejceesstep` 結構:左側放入 `.ejceesoutput`,中間進度條,右側放入 AutoPlay 按鈕。
* 將原本顯示在側邊欄的評論(`this.historyFEN.c`)改寫為監聽步驟變更時,若有評論,則顯示在 `.ejceessvg` 內的絕對定位 `div` 中。

---

### 三、 核心注意事項 (Gotchas)

* **不要再用 ID 綁定 DOM:** 原本的 `id="tool-new"` 在多實例會崩潰。請全部改用 `class="tool-btn tool-new"` 或 `data-action="new"` 來選取。
* **鍵盤事件防護:** `keydown` 事件是綁定在 `document` 上的。
    * 如果頁面上有一個 `ejceessingle` 和三個 `ejceesmini`,只有 `ejceessingle` 應該對按鍵做出反應。
    * 在 `keydown` 處理函數開頭加入判斷:`if(this.mode === 'ejceesmini') return;`
* **動畫效能:** 如果頁面上有 10 個 `ejceesmini` 實例,確保不在畫面內(Viewport)的實例不要自動播放動畫(可使用 `IntersectionObserver` 進行優化,不過這算進階功能,可先放著)。
* **拖曳/點擊座標計算:** 原本的 `pt.matrixTransform(svgElement.getScreenCTM().inverse())` 非常依賴 SVG 的渲染尺寸。當容器大小隨父元素縮放時(`single` / `mini`),這段程式碼依然有效,但必須確保 SVG 的 `viewBox` 設定正確,且不要用寫死的 PX 去推算滑鼠位置。

---

### 四、 程式碼骨架 (Code Skeleton)

您可以依照這個架構,將原本的邏輯填入對應的方法中。

```javascript
class Ejcees {
    // 靜態屬性:確保全域 CSS 只注入一次
    static cssInjected = false;
    // 靜態屬性:實例計數器,用於生成唯一 ID
    static instanceCount = 0;

    constructor(container, mode) {
        this.container = container;
        this.mode = mode || 'ejceesmain'; // 'ejceesmain' | 'ejceessingle' | 'ejceesmini'
        this.instanceId = `ejcees-inst-${Ejcees.instanceCount++}`;

        // === 1. 核心狀態變數 (從原本的 let 搬過來) ===
        this.INITIAL_FEN = 'rnbakabnr/9/1c5c1/p1p1p1p1p/9/9/P1P1P1P1P/1C5C1/9/RNBAKABNR w - - 0 1';
        this.currentTurn = 'w';
        this.halfMoveClock = 0;
        this.fullMoveNumber = 1;
        this.historyFEN = { fen: this.INITIAL_FEN, move: null, lastMove: null, v: [] };
        this.currentBranch = [];
        this.currentStepIndex = 0;
        
        // 棋盤映射
        this.tileMap = new Map();
        this.piecePos = new Map();
        
        // UI 狀態
        this.isEditMode = false;
        this.isAutoPlaying = false;
        // ... 其他原本的 let 變數 ...

        // === 2. 初始化 ===
        this.injectCSS();
        this.renderDOM();
        this.cacheDOM();
        this.bindEvents();
        this.initGame();
    }

    // --- 方法:全域 CSS 注入 ---
    injectCSS() {
        if (Ejcees.cssInjected) return;
        const style = document.createElement('style');
        style.innerHTML = `
            /* 這裡放入原本的 CSS,並根據 mode 加上前綴限制作用域 */
            .ejcees-container[data-mode="ejceesmain"] { width: 100vw; height: 100vh; display: flex; }
            .ejcees-container[data-mode="ejceessingle"] { width: 100%; max-width: 100%; display: flex; }
            .ejcees-container[data-mode="ejceesmini"] { width: 100%; position: relative; display: flex; flex-direction: column; }
            
            /* Mini 模式專屬 Overlay */
            .ejcees-overlay-comment { position: absolute; bottom: 0; left: 0; width: 100%; background: rgba(0,0,0,0.7); color: #fff; padding: 10px; display: none; }
            .ejcees-overlay-branch { position: absolute; top: 0; left: 0; background: rgba(255,255,255,0.8); display: none; }
            /* ... 其餘原本的 CSS ... */
        `;
        document.head.appendChild(style);
        Ejcees.cssInjected = true;
    }

    // --- 方法:動態生成 DOM 結構 ---
    renderDOM() {
        this.container.classList.add('ejcees-container');
        this.container.setAttribute('data-mode', this.mode);

        // 動態替換 SVG 內的 ID,避免多實例衝突
        let svgDefString = `... 這裡放原本 SVG <defs> 的內容,但所有的 id="tile0" 要改成 id="tile0-${this.instanceId}" ...`;

        let htmlString = '';
        if (this.mode === 'ejceesmain' || this.mode === 'ejceessingle') {
            htmlString = `
                <div class="ejceesmain">
                    <div class="ejceessvg-wrapper">
                        </div>
                    <div class="ejceesctrl">
                        <div class="ejceesoutput">...</div>
                        </div>
                </div>
            `;
        } else if (this.mode === 'ejceesmini') {
            htmlString = `
                <div class="ejceesmini-wrapper" style="position: relative;">
                    <div class="ejcees-overlay-branch"></div>
                    <div class="ejcees-overlay-comment"></div>
                </div>
                <div class="ejceesstep mini-step-bar">
                    <div class="ejceesoutput mini-output">0/0</div>
                    <input type="range" class="ejceesstepdrop" min="0" max="0" value="0" />
                    <div class="btn-autoplay">Play</div>
                </div>
            `;
        }
        
        this.container.innerHTML = htmlString;
    }

    // --- 方法:快取 DOM 元素 ---
    cacheDOM() {
        this.svgElement = this.container.querySelector('.ejceespb');
        this.outputElement = this.container.querySelector('.ejceesoutput');
        this.stepSlider = this.container.querySelector('.ejceesstepdrop');
        // ... 取代原本的 document.querySelector ...
    }

    // --- 方法:綁定事件 ---
    bindEvents() {
        // SVG 點擊事件 (注意 bind(this) 或是用箭頭函數)
        if (this.svgElement) {
            this.svgElement.addEventListener('click', (e) => this.handleSvgClick(e));
        }

        // 滑桿事件
        if (this.stepSlider) {
            this.stepSlider.addEventListener('input', (e) => this.handleSlider(e));
        }

        // 全域鍵盤事件 (只有 Single 或 Main 模式掛載)
        if (this.mode !== 'ejceesmini') {
            document.addEventListener('keydown', (e) => this.handleKeydown(e));
        }
    }

    // --- 方法:核心邏輯區 ---
    initGame() {
        this.loadFEN(this.historyFEN.fen);
        this.renderRecordUI();
    }

    loadFEN(fenStr) {
        // 實作原本的 loadFEN,注意裡面創建 <use> 時,href 要加上 this.instanceId
    }

    handleSvgClick(event) {
        // 實作原本的點擊走子邏輯
    }

    handleKeydown(e) {
        // 實作快捷鍵
    }

    // --- 方法:UI 更新 ---
    renderNoteUI() {
        // 針對不同模式處理評論顯示
        const node = this.getNodeAtStep(this.currentStepIndex);
        if (!node) return;

        if (this.mode === 'ejceesmini') {
            const commentOverlay = this.container.querySelector('.ejcees-overlay-comment');
            if (node.c && node.c.trim() !== '') {
                commentOverlay.textContent = node.c;
                commentOverlay.style.display = 'block';
            } else {
                commentOverlay.style.display = 'none';
            }
        } else {
            // 原本 Main / Single 的右下角 UI 更新邏輯
        }
    }

    // ... 其餘幾十個方法 (boardToFEN, isValidMove, etc.) 全部包進來 ...
}

// === 自動初始化腳本 ===
document.addEventListener('DOMContentLoaded', () => {
    // 尋找所有帶有 class="ejcees" 的容器
    const containers = document.querySelectorAll('.ejcees');
    containers.forEach(container => {
        const mode = container.getAttribute('mode');
        // 實例化該組件
        new Ejcees(container, mode);
    });
});
```

根據這個骨架,您就可以開始將那 2000 多行的函數與物件逐步搬移進 `class Ejcees` 裡面。這是一個需要耐心的過程,強烈建議**先從 DOM 的綁定與 SVG 的隔離著手**,確保在一個頁面放兩個 `div.ejcees` 時,它們的版面和 SVG 渲染不會互相干擾,再來搬移核心的走棋邏輯。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5337
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 176 次
被圈友点赞: 204 次
联系:

Re: 將製作ejcees(中國象棋打譜程式)

帖子 ejsoon »

寫出constructor(container, mode) {}內的所有內容,如果太長可以省略表示。

代码: 全选

這是在類別化重構中最重要的部分。在 `constructor` 中,我們需要將所有原本在全域範圍的變數轉化為**實例屬性**,並且處理 **SVG ID 唯一化**以防止多實例衝突。

以下是 `Ejcees` 類別的 `constructor` 實作:

```javascript
class Ejcees {
    constructor(container, mode) {
        // 1. 基本屬性與模式設定
        this.container = container;
        this.mode = mode || 'ejceesmain'; // 'ejceesmain', 'ejceessingle', 'ejceesmini'
        // 生成唯一 ID,用於區分多個實例的 SVG Defs 引用
        this.instanceId = 'ej_' + Math.random().toString(36).substr(2, 9);

        // 2. 棋局核心狀態 (原本的全域 let 變數)
        this.INITIAL_FEN = 'rnbakabnr/9/1c5c1/p1p1p1p1p/9/9/P1P1P1P1P/1C5C1/9/RNBAKABNR w - - 0 1';
        this.currentTurn = 'w';
        this.halfMoveClock = 0;
        this.fullMoveNumber = 1;
        
        // 歷史與分支數據
        this.historyFEN = { 
            fen: this.INITIAL_FEN, 
            move: null, 
            lastMove: null, 
            c: '', // 評論
            v: []  // 分支變體
        };
        this.currentBranch = [];
        this.currentStepIndex = 0;
        this.undoStack = [];

        // 棋盤邏輯映射
        this.tileMap = new Map();  // 座標 -> 元素
        this.piecePos = new Map(); // 元素 -> 座標
        
        // 3. UI 與 動畫狀態
        this.selectedTile = null;
        this.isEditMode = false;
        this.isAutoPlaying = false;
        this.moveInterval = 1000;
        this.moveSpeed = 300;
        this.isRotateEnabled = false;
        this.isFlipEnabled = false;
        this.pgnMetadata = { Event: 'Casual Game', Site: 'Local', Date: '', White: '', Black: '', Result: '*' };

        // 4. 動態資源初始化
        this.injectGlobalStyles(); // 靜態方法,全域只注入一次 CSS
        this.renderLayout();       // 根據 mode 生成 HTML 結構
        this.initSvgDefinitions(); // 初始化並唯一化 SVG <defs>

        // 5. DOM 元素快取 (從 container 內尋找,不可使用 document.getElementById)
        this.svgElement = this.container.querySelector('.ejceespb');
        this.boardGroup = this.container.querySelector('.board-group');
        this.piecesGroup = this.container.querySelector('.pieces-group');
        this.marksGroup = this.container.querySelector('.marks-group');
        this.stepSlider = this.container.querySelector('.ejceesstepdrop');
        this.outputElement = this.container.querySelector('.ejceesoutput');
        this.recordContainer = this.container.querySelector('.ejceestext-content');
        this.commentArea = this.container.querySelector('.ejceestool-comment'); // 或 mini 模式的 overlay

        // 6. 座標系統轉換工具 (SVG Point)
        this.svgPoint = this.svgElement.createSVGPoint();

        // 7. 綁定事件監聽
        this.bindEvents();

        // 8. 載入初始棋局
        this.loadFEN(this.INITIAL_FEN);
        this.updateUI();
    }

    // --- 以下為配套的私有初始化方法簡述 ---

    renderLayout() {
        // 這裡判斷 this.mode
        // 若為 ejceesmini:
        //   - 隱藏 .ejceestool 和 .ejceestext
        //   - 在 .ejceesstep 左邊插入 .ejceesoutput (20/80 格式)
        //   - 在右邊插入 Play 按鈕
        //   - 插入懸浮的 Branch 和 Comment div
        
        const layout = this.mode === 'ejceesmini' 
            ? this.getMiniTemplate() 
            : this.getMainTemplate();
        this.container.innerHTML = layout;
    }

    initSvgDefinitions() {
        // 重點:將原本 SVG 的 <use href="#red_k"> 改成 <use href="#red_k_${this.instanceId}">
        // 並且將 <g id="red_k"> 改成 <g id="red_k_${this.instanceId}">
        // 這樣頁面上同時存在多個棋盤時,圖形才不會互相抓錯。
    }

    bindEvents() {
        // 點擊棋盤
        this.svgElement.addEventListener('click', (e) => this.handleBoardClick(e));
        
        // 滑桿與按鈕
        if(this.stepSlider) {
            this.stepSlider.addEventListener('input', (e) => this.jumpToStep(parseInt(e.target.value)));
        }

        // 快捷鍵:僅在非 mini 模式下掛載於 document
        if (this.mode !== 'ejceesmini') {
            document.addEventListener('keydown', this.handleGlobalKeydown.bind(this));
        }
    }
}
```

### 注意事項提醒:

1.  **ID 唯一化**:在 `constructor` 產生的 `this.instanceId` 必須應用到所有內部 HTML/SVG 的 `id` 屬性上。否則,如果你在同一個頁面放兩個 `ejceesmini`,第一個棋盤的棋子可能會「飛」到第二個棋盤上(因為 CSS 或 SVG 引用了重複的 ID)。
2.  **鍵盤事件 (Mode 四 vs 五)**:`ejceesmini` 明確規定不能使用快捷鍵。所以在 `bindEvents` 中我加了判斷。如果是 `ejceessingle`,雖然可以使用快捷鍵,但要注意如果頁面上有多個 instance(雖你說 single 只有一個),通常會建議檢查 `document.activeElement` 是否在該容器內。
3.  **this 綁定**:所有事件回調(Callbacks)建議使用箭頭函數 `(e) => this.method(e)`,這能確保函數內部的 `this` 永遠指向 `Ejcees` 的實例物件。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5337
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 176 次
被圈友点赞: 204 次
联系:

Re: 將製作ejcees(中國象棋打譜程式)

帖子 ejsoon »

js如何將svg文本放進<object>中,並操作這些svg元素?

代码: 全选

這是一個非常經典的需求。雖然 `<object>` 標籤通常用於加載外部檔案,但如果你手頭上的是 **SVG 字符串(文本)**,直接放入 `<object>` 會稍微繞一點路,因為 `<object>` 的 `data` 屬性預期的是一個 URL。

要實現這個目標,最優雅的方式是利用 **Blob** 或 **Data URI**。

---

## 💡 核心流程

1.  **轉換**:將 SVG 字符串轉換為 `Blob` 對象。
2.  **生成 URL**:使用 `URL.createObjectURL()` 生成一個臨時網址。
3.  **掛載**:將該網址賦值給 `<object>` 的 `data` 屬性。
4.  **操作**:在 `load` 事件觸發後,透過 `contentDocument` 存取內部元素。

---

## 🛠️ 實作程式碼

### 1. HTML 結構
```html
<div id="container">
  <object id="mySvgObject" type="image/svg+xml" width="300" height="300"></object>
</div>
```

### 2. JavaScript 邏輯
```javascript
const svgString = `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
  <circle id="myCircle" cx="50" cy="50" r="40" fill="royalblue" />
</svg>`;

const objElement = document.getElementById('mySvgObject');

// --- 第一步:將字符串轉為 URL ---
const blob = new Blob([svgString], { type: 'image/svg+xml' });
const url = URL.createObjectURL(blob);

// --- 第二步:加載 SVG ---
objElement.data = url;

// --- 第三步:操作內部的 DOM ---
objElement.addEventListener('load', () => {
    // 必須在 load 事件後才能獲取 contentDocument
    const svgDoc = objElement.contentDocument;
    
    // 像操作普通 DOM 一樣操作它
    const circle = svgDoc.getElementById('myCircle');
    
    if (circle) {
        circle.setAttribute('fill', 'orange');
        
        circle.addEventListener('click', () => {
            alert('你點擊了 SVG 內部的圓形!');
        });
    }

    // 記得釋放記憶體(如果不再需要動態更換)
    // URL.revokeObjectURL(url);
});
```

---

## ⚠️ 關鍵注意事項

### 1. 同源策略 (CORS)
雖然你是用 `Blob` 生成的 URL,通常不會有跨域問題,但如果你是加載 **遠端伺服器** 的 SVG 檔案到 `<object>` 中,瀏覽器會限制你存取 `contentDocument`。

### 2. 為什麼不直接用 `innerHTML`?
如果你只是想操作 SVG,將 SVG 字符串直接放入一個 `<div>` 的 `innerHTML` 是最簡單的:
* **優點**:直接渲染,操作方便,不需考慮異步加載。
* **缺點**:SVG 的樣式可能會受全域 CSS 影響(有時這反而是優點)。

> **如果你選擇使用 `<object>`**,通常是因為你希望將 SVG 的作用域「隔離」開來,避免 CSS 或 ID 衝突。

### 3. 操作時機
**這是最常出錯的地方:** 你不能在設置 `objElement.data` 後立即操作元素。因為 `<object>` 加載資源是異步的,必須監聽 `load` 事件,否則 `contentDocument` 將會是 `null`。

---

## 🔍 比較表

| 方法 | 操作複雜度 | 樣式隔離 | 適用場景 |
| :--- | :--- | :--- | :--- |
| **Object + Blob** | 中(需處理異步) | **高**(完全獨立) | 需要封裝感、避免樣式污染。 |
| **innerHTML** | 低(同步操作) | **低**(受全域影響) | 快速開發、需要與頁面 CSS 互動。 |
| **Iframe** | 中 | **極高** | 幾乎不建議僅為了 SVG 使用。 |

你目前的專案需求,是否必須限制在 `<object>` 容器內呢?如果沒有特殊隔離需求,直接把 SVG 文本注入到 `div` 會讓開發輕鬆許多。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5337
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 176 次
被圈友点赞: 204 次
联系:

Re: 將製作ejcees(中國象棋打譜程式)

帖子 ejsoon »

對象化進行中……
附件
ejceesobj20260426.7z
(54.88 KiB) 已下载 4 次
https://ejsoon.vip/
弈趣極光:享受思維樂趣
回复

在线用户

正浏览此版面之用户: Bing [Bot] 和 1 访客