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

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

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

帖子 ejsoon »

附件
ejcees20260407arrange99977752.7z
(46.25 KiB) 已下载 4 次
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5008
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 173 次
被圈友点赞: 202 次
联系:

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

帖子 ejsoon »

更改前面的一個需求:當處於「文本導出」狀態或點擊複製按鈕時,如果不處於範圍選取狀態,只有engine的範圍是從開頭到currentStep,其它三個仍然跟之前一樣導出全部。

只需給出所要修改的地方 ,所有的代碼和注釋都要使用英文。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5008
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 173 次
被圈友点赞: 202 次
联系:

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

帖子 ejsoon »

gemini pro:

代码: 全选

Here are the modifications to implement the requested feature. We will add a parameter to the `getGamePath` function to conditionally limit the end step, and then apply this parameter only when generating the Engine format.

### 1. Update the `getGamePath` function
Find the `getGamePath()` function definition and replace it completely with the following code. This adds the `limitToCurrentStep` parameter, defaulting to `false` for `cn`, `en`, and `url` formats to export the full depth, but allowing the Engine format to stop at `currentStepIndex`.

**Replace this block:**
```javascript
        // Extract the linear path corresponding to the currently selected branch
        function getGamePath() {
            let newStart = 0;
            // Modification: Default end is now currentStepIndex instead of full depth
            let newEnd = currentStepIndex;
            let newHistory = {
                fen: historyFEN.fen,
                move: null,
                lastMove: null,
                c: historyFEN.c,
                v: []
            };
            
            if (isRangeMode) {
                // Restore original full depth default if in range mode but end isn't picked yet
                newEnd = getPathDepth(historyFEN, currentBranch) - 1;
// ... (rest of the function)
```

**With this:**
```javascript
        // Extract the linear path corresponding to the currently selected branch
        function getGamePath(limitToCurrentStep = false) {
            let newStart = 0;
            // Modification: Default to currentStepIndex only if explicitly requested (for engine format)
            let newEnd = limitToCurrentStep ? currentStepIndex : getPathDepth(historyFEN, currentBranch) - 1;
            let newHistory = {
                fen: historyFEN.fen,
                move: null,
                lastMove: null,
                c: historyFEN.c,
                v: []
            };
            if (isRangeMode) {
                if (rangeStart === null && rangeEnd === null) {
                    newStart = currentStepIndex;
                    newEnd = currentStepIndex;
                } else if (rangeStart !== null && rangeEnd !== null && rangeStart === rangeEnd) {
                    newStart = rangeStart;
                    newEnd = rangeEnd;
                } else if (rangeStart !== null && rangeEnd === null) {
                    newStart = rangeStart;
                } else if (rangeStart !== null && rangeEnd !== null && rangeStart !== rangeEnd) {
                    newStart = Math.min(rangeStart, rangeEnd);
                    newEnd = Math.max(rangeStart, rangeEnd);
                }
                const startNode = getNodeAtStep(newStart);
                newHistory = {
                    fen: startNode.fen,
                    move: null,
                    lastMove: null,
                    c: startNode.c,
                    v: []
                };
            }
            let curr = newHistory;
            for (let i = newStart + 1; i <= newEnd; i++) {
                const node = getNodeAtStep(i);
                let newNode = {
                    fen: node.fen,
                    move: node.move,
                    lastMove: node.lastMove,
                    c: node.c,
                    v: []
                };
                curr.v.push(newNode);
                curr = newNode;
            }
            return newHistory;
        }
```

### 2. Update `performCopy` to pass `true` for Engine format
Locate the global `performCopy()` function that you added in the previous step. Find the `else if (fmt === 'engine') {` block and update the `getGamePath` call.

**Replace this line:**
```javascript
            } else if (fmt === 'engine') {
                let path = getGamePath();
```

**With this:**
```javascript
            } else if (fmt === 'engine') {
                // Modification: Pass true to limit engine format to currentStepIndex
                let path = getGamePath(true);
```

### 3. Update `renderExportTextUI` to pass `true` for Engine format
Locate the `renderExportTextUI()` function (around the area where the `urlTextToggled` is defined). Find the section where the Engine text is generated and update the `getGamePath` call.

**Replace this block:**
```javascript
            const urlText = generateUrlHash(false);
            const urlTextToggled = generateUrlHash(true);

            // Generate UCCI Engine Format String
            let path = getGamePath();
```

**With this:**
```javascript
            const urlText = generateUrlHash(false);
            const urlTextToggled = generateUrlHash(true);

            // Generate UCCI Engine Format String
            // Modification: Pass true to limit engine format to currentStepIndex
            let path = getGamePath(true);
```
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5008
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 173 次
被圈友点赞: 202 次
联系:

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

帖子 ejsoon »

增加功能:當不處於「編輯局面、編輯評論、導出文本、改動間隔及移動時間」狀態時,按下空白鍵,就等於是按下「自動播放」按鈕。

代码: 全选

Here is the modification to add the **Spacebar** as a shortcut for the **Auto Play** button.

### Update the `keydown` Event Listener
Locate the main keyboard event listener `document.addEventListener('keydown', (e) => {` (around line `1854`). Insert the logic for the Spacebar inside the main block, ensuring it only triggers when the specified states are inactive.

**Search for this section:**
```javascript
        document.addEventListener('keydown', (e) => {
            if (isExportTextMode || isEditingComment || isEditMode || isEditingSettings) return;
```

**Replace that specific check and add the Spacebar logic like this:**
```javascript
        document.addEventListener('keydown', (e) => {
            // Check if any modal or edit mode is active
            const isExportModalOpen = document.querySelector('.ejcees-export-modal') !== null;
            const isRestrictedMode = isExportTextMode || isEditingComment || isEditMode || isEditingSettings || isExportModalOpen;

            // Feature: Spacebar for Auto Play
            if (e.code === 'Space' || e.key === ' ') {
                if (!isRestrictedMode) {
                    e.preventDefault(); // Prevent page scrolling
                    const autoPlayBtn = document.getElementById('btn-auto-play');
                    if (autoPlayBtn) {
                        autoPlayBtn.click();
                    }
                    return;
                }
            }

            if (isRestrictedMode) return;

            if (e.ctrlKey) {
                // ... (existing Ctrl shortcut logic)
```

### Explanation of the change:
* **`e.code === 'Space'`**: This reliably detects the spacebar across different keyboard layouts.
* **`e.preventDefault()`**: This is crucial because the default browser behavior for the spacebar is to scroll the page down; this line prevents that.
* **State Check**: It uses the same logic as your other shortcuts to ensure you don't accidentally toggle auto-play while typing a comment or editing settings.
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5008
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 173 次
被圈友点赞: 202 次
联系:

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

帖子 ejsoon »

附件
ejcees20260407engineStep99977753.7z
(46.37 KiB) 已下载 6 次
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5008
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 173 次
被圈友点赞: 202 次
联系:

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

帖子 ejsoon »

我有一個html+css+js程式,現在我想把它做成一個js,當頁面存在至少一個.ejcees時,它將把css放進去,然後每一個.ejcees都用js來操作dom以及賦予事件。每個.ejcees都互不影響,包括undo-redo。

應該要怎麼做?是否要把js對象化?

代码: 全选

要將原本的 HTML/CSS/JS 程式轉換為一個可複用的組件,並支援多個實例(例如頁面上有多個 `.ejcees` 容器且互不干擾),**將 JS「對象化」(物件導向,Object-Oriented)是絕對必要的**。

以下是實現這個目標的建議架構與步驟:

### 1. 核心觀念:封裝與範圍限制
在目前的代碼中,許多變量是全域的(如 `historyFEN`, `currentStepIndex` 等),且 DOM 操作是使用 `document.querySelector`。要達成多個實例並存,必須:
* **使用 Class (類別)**:將所有邏輯封裝在一個 `class Ejcees` 中。
* **Scoped DOM**:不要用 `document` 查找,而是從實例傳入的 `container` 節點開始查找。
* **私有狀態**:每個實例擁有自己的一套 `undoStack`, `redoStack`。

### 2. 實作步驟建議

#### A. CSS 的處理
為了讓 CSS 隨 JS 自動載入,你可以將原本 `<style>` 標籤中的內容 存為一個 JS 字串。
```javascript
const EJCEES_CSS = `
  .ejceesmain { display: flex; ... } 
  /* 將原本的所有 CSS 放進這裡 */
`;

function injectCSS() {
    if (!document.getElementById('ejcees-styles')) {
        const style = document.createElement('style');
        style.id = 'ejcees-styles';
        style.textContent = EJCEES_CSS;
        document.head.appendChild(style);
    }
}
```

#### B. 將 JS 對象化 (Class 結構)
你需要將 `DOMContentLoaded` 中的邏輯 搬移到 Class 的 `constructor` 或 `init` 方法中。

```javascript
class Ejcees {
    constructor(element) {
        this.container = element; // 這是對應的 .ejcees 元素
        this.historyFEN = { fen: INITIAL_FEN, v: [] }; // 獨立的狀態
        this.undoStack = []; // 獨立的撤銷棧
        this.currentStepIndex = 0;
        
        this.init();
    }

    init() {
        // 1. 生成內部 HTML 結構 (原本 body 裡的 ejceesmain 內容)
        this.container.innerHTML = `...原本的 HTML 結構...`;
        
        // 2. 獲取內部元素(使用 this.container.querySelector)
        this.svgElement = this.container.querySelector('.ejceespb');
        this.outputElement = this.container.querySelector('.ejceesoutput');
        
        // 3. 綁定事件
        this.bindEvents();
        
        // 4. 初始化棋盤狀態
        this.loadFEN(this.historyFEN.fen);
    }

    bindEvents() {
        // 使用箭頭函數確保 this 指向實例
        this.svgElement.addEventListener('click', (e) => this.handleBoardClick(e));
        this.container.querySelector('#tool-undo').addEventListener('click', () => this.undo());
        // ...其他按鈕事件
    }

    undo() {
        if (this.undoStack.length <= 1) return;
        // 實作該實例專屬的 undo 邏輯
    }

    // 將原本的所有 function (如 loadFEN, renderRecordUI) 變成 class 的 method
    loadFEN(fenStr) { ... }
}
```

#### C. 主控入口
在 JS 檔案的最下方,檢查頁面上是否存在目標元素並實例化:

```javascript
function initAllEjcees() {
    const elements = document.querySelectorAll('.ejcees');
    if (elements.length > 0) {
        injectCSS(); // 載入 CSS
        elements.forEach(el => new Ejcees(el));
    }
}

// 頁面載入後執行
if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', initAllEjcees);
} else {
    initAllEjcees();
}
```

### 3. 注意事項
1.  **ID 衝突**:原本 HTML 中使用了許多 `id`(如 `id="tool-undo"`)。在多個實例中,`id` 必須唯一。建議將 HTML 中的 `id` 改為 `class`,並在 JS 中透過 `this.container.querySelector('.tool-undo')` 來選取。
2.  **LocalStorage**:原本代碼會將狀態存入 `localStorage.setItem('ejcees_saved_state', ...)`。如果頁面有多個棋盤,它們會互相覆蓋。建議給每個實例一個唯一的 ID(例如 `data-id`),存儲時鍵名改為 `ejcees_state_${id}`。
3.  **Global Event Listeners**:代碼中有對 `document` 綁定 `keydown` 事件。這需要小心處理,否則按下鍵盤時所有棋盤都會同時動作。建議檢查 `document.activeElement` 是否在 `this.container` 內部,才觸發該實例的邏輯。

透過這種「類別封裝」的方式,你可以輕鬆在同一個網頁中放入多個象棋編輯器,且彼此的棋譜、編輯模式與撤銷紀錄都會完全獨立。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5008
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 173 次
被圈友点赞: 202 次
联系:

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

帖子 ejsoon »

將所上傳的html+css+js程式改寫成一個對象化的js程式。

一,當頁面中出現.ejceesmain時:

如果body內除了一個.ejceesmain以及<script src=ejceesobj.js>之外沒有其它元素,則表現將跟現在一樣。

如果body內除了.ejceesmain還含有其它元素,.ejceesmain的父元素也並非body,則.ejceesmain應不超過其父元素或螢幕的高寬。

如果頁面上存在多個.ejceesmain,則只有第一個生效。

.ejceesmain的innerHTML如果不為空,則是初始化時導入的棋譜文本。

二,當頁面中不存在.ejceesmain,而存在一至多個.ejceesmini時,則這些.ejceesmini都將實例化且互不影響。每一個.ejceesmini的高度不超過螢幕高度或它的父元素高度。

.ejceesmini的innerHTML如果不為空,則是初始化時導入的棋譜文本。

請先列出詳細的工作流程。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5008
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 173 次
被圈友点赞: 202 次
联系:

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

帖子 ejsoon »

grok:

代码: 全选

**詳細的工作流程(重構為物件化 JS 程式)**

1. **腳本載入與全域準備**  
   - `ejceesobj.js` 被引入後,立即在 `DOMContentLoaded` 事件中執行。  
   - 僅**一次**將原 HTML 內的全部 `<style>` 內容注入 `<head>`(使用 `<style id="ejcees-global-css">` 防止重複注入)。  
   - 定義單一公開類別 `Ejcees`(所有狀態、方法、DOM 操作全部封裝在實例內)。

2. **DOM 掃描與模式判斷**  
   - 查詢所有 `.ejceesmain`。  
     - 若存在 → 只取**第一個**作為主容器(後續的 `.ejceesmain` 被忽略)。  
     - 判斷是否為「全頁模式」:  
       - `container.parentElement.tagName === 'BODY'` **且** `document.body.children.length === 1`(只有這一個 `.ejceesmain`)→ 保留原 100vw/100vh 行為。  
       - 否則 → 進入「受限模式」:設定 `max-width: 100%`、`max-height: 100%`(以父元素或視窗為上限),並加上 `ejcees-constrained` class。  
     - 若 `.ejceesmain` 的 `innerHTML.trim() !== ''`,視為**初始化棋譜文本**(FEN、走法、URL hash、JSON 等)。  
   - 若**完全沒有** `.ejceesmain`,則查詢所有 `.ejceesmini`。  
     - 每個 `.ejceesmini` 獨立建立 `Ejcees` 實例(互不影響)。  
     - 每個容器強制加上 `ejcees-constrained` class,並確保高度不超過父元素或視窗高度。

3. **建立 Ejcees 實例**  
   - `new Ejcees(container, options)`  
     - `options.isMain`:boolean(`.ejceesmain` 為 true,`.ejceesmini` 為 false)。  
     - `options.initText`:若 container.innerHTML 非空,則傳入該字串。  
   - 建構子內部流程(每個實例完全獨立):  
     a. `this.container = container`  
     b. 若 `this.container.innerHTML` 為空 → 呼叫 `this._buildInternalStructure()` 產生完整的 `.ejceessvg`、`.ejceesctrl`、工具列、步驟滑桿、記錄區、註解區等 DOM。  
     c. 注入/覆蓋必要的 class(`ejceesmain` 或 `ejceesmini`)。  
     d. 解析 `initText`(呼叫原有的 `importExportedText` 邏輯,現在是 `this.importExportedText(text)`)。  
     e. 初始化所有實例屬性(`this.historyFEN`、`this.currentBranch`、`this.currentStepIndex`、`this.tileMap`、`this.piecePos` 等,原腳本所有全域變數全部變成 `this.xxx`)。  
     f. 呼叫 `this.loadFEN(...)`、`this.renderRecordUI()`、`this.renderNoteUI()`、`this.reapplyVisualPositions()` 等。  
     g. 綁定所有事件監聽器(使用 `this._bindEvents()`),所有 callback 皆使用 `arrow function` 或 `bind(this)`。  
     h. 呼叫 `this.saveStateToUndo()` 與 `this.updateToolHighlights()`。

4. **尺寸與響應式處理**  
   - CSS 已包含原有的 `@media` 規則,僅需在 constrained 模式下讓 `.ejceesmain` / `.ejceesmini` 的 `flex` 容器自動適配父元素。  
   - 可選:為 `.ejceesmini` 額外加上更小的預設尺寸(例如 `width: 480px; height: 540px;`),但仍受父元素/視窗限制。  
   - 使用 `ResizeObserver`(可選)在容器大小改變時呼叫 `this.reapplyVisualPositions()`。

5. **多實例共存與隔離**  
   - 每個 `Ejcees` 實例擁有完全獨立的 `tileMap`、`historyFEN`、`undoStack`、`currentStepIndex` 等。  
   - SVG id、動畫 id、record step id 等皆在實例內產生唯一識別(例如 `this.instanceId = 'ejcees-' + Date.now()`,並在 DOM 中加上 `data-ejcees-id`)。  
   - `document.querySelector` 全部改為 `this.container.querySelector` 或 `this._qs(selector)` 輔助方法。  
   - 自動播放、範圍選擇、編輯模式、匯出/匯入等全部以 `this.` 為前綴,不會互相干擾。

6. **棋譜初始化與匯入流程**  
   - 若 `initText` 存在 → 呼叫 `this.importExportedText(initText)`(原腳本已有的完整解析邏輯,現在變成實例方法)。  
   - 支援 FEN、走法文字、URL hash、JSON 等所有原有格式。  
   - 載入完成後立即 `this.jumpToStep(0)` 並 render 所有 UI。

7. **後續操作流程(與原腳本完全一致,但物件化)**  
   - 點擊 SVG → `this._handleBoardClick(e)`  
   - 工具列按鈕 → `this._handleToolClick(toolId)`  
   - 步驟滑桿 → `this._handleStepChange()`  
   - 自動播放 → `this.startAutoPlay()` / `this.playNextAnimated()`(使用 `this.moveInterval`、`this.moveSpeed`)  
   - 所有 render 方法(`renderRecordUI`、`renderNoteUI`、`renderExportTextUI` 等)皆為 `this.xxx`。  
   - Undo/Redo、分支、註解編輯、SVG 動畫匯出等全部封裝在實例內。

8. **公開 API(供外部呼叫)**  
   ```js
   const board = new Ejcees(container);
   board.loadFEN(fen);           // 手動載入 FEN
   board.importExportedText(text);
   board.getCurrentFEN();        // 取得目前 FEN
   board.getGamePath();          // 取得目前棋譜路徑
   board.destroy();              // 解除事件、清除 DOM(供多實例清理)
   ```

9. **最終輸出**  
   - 只產出一個檔案 `ejceesobj.js`(類別 `Ejcees` + 全域 CSS 注入 + DOM 掃描啟動程式)。  
   - 原 HTML 檔案只需保留 `<div class="ejceesmain"></div>`(或多個 `.ejceesmini`)與 `<script src="ejceesobj.js"></script>` 即可運行,行為與原版完全相同(全頁模式)或成為可嵌入的元件(mini 模式)。

---

**下一步**  
我已完成完整的工作流程規劃。  
請確認以上流程無誤後,我會立即輸出**完整的 `ejceesobj.js` 原始碼**(已將原 1000+ 行腳本全部物件化、消除全域變數、支援多實例、尺寸自動適配、innerHTML 初始化棋譜)。  

是否直接開始輸出程式碼?(或您有任何調整需求)
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5008
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 173 次
被圈友点赞: 202 次
联系:

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

帖子 ejsoon »

gemini pro:

代码: 全选

將這個單一的 HTML/CSS/JS 應用程式改寫成物件導向的架構,並滿足你所提出的 DOM 環境自適應與多實例(Multi-instance)需求,是一項結構重構工程。

為了確保程式碼的穩定性與獨立性,以下是將其改寫為 `ejceesobj.js` 的**詳細工作流程**:

### 第一階段:樣式與 DOM 模板的封裝 (CSS & HTML Encapsulation)
1. **動態載入 CSS**:將原 HTML 中的 `<style>` 內容提取出來,寫入 JS 的字串中。在腳本加載時,檢查 `<head>` 中是否已有該樣式,若無則動態插入 `<style id="ejcees-style">`,確保多實例環境下樣式只加載一次且共用。
2. **重構 HTML 模板**:將 `.ejceesmain` 或 `.ejceesmini` 內部的 DOM 結構(包含 SVG 棋盤、控制面板、側邊欄等)轉化為 JS 模板字串或 DOM 建立函數。

### 第二階段:核心邏輯物件化 (Object-Oriented Refactoring)
1. **建立 `EjceesApp` 類別 (Class)**:將所有原本寫在全域 (Global) 的變數轉移為類別的實例屬性 (Instance Properties)。
   * *例如*:`this.currentTurn`, `this.historyFEN`, `this.currentBranch`, `this.tileMap`, `this.isEditMode` 等。
2. **封裝方法 (Methods)**:將原有的獨立函數(如 `loadFEN`, `boardToFEN`, `pushHistory`, `movePieceWithAnimation` 等)改寫為類別的方法。
3. **隔離 DOM 查詢**:將原本所有的 `document.querySelector` 或 `document.getElementById` 替換為 `this.container.querySelector`。這一步是**重中之重**,確保多個 `.ejceesmini` 存在時,按鈕點擊和棋子移動不會互相干擾。ID 綁定(如 `#ejceesstartdot` 或 `#tile0`)也需要考慮轉換為 class 選擇器,或者在 SVG 內部維持局部引用(SVG `<use>` 的 href 引用在同一 SVG 內是安全的)。

### 第三階段:容器尺寸與排版自適應 (Responsive Sizing Rules)
在 `EjceesApp` 的建構子 (Constructor) 中,針對傳入的容器進行樣式與尺寸的設定:
1. **判斷是否為「唯一全螢幕」環境**:
   * 檢查容器是否為 `.ejceesmain`,且它的父元素是否為 `<body>`。
   * 檢查 `<body>` 內是否除了該容器和 `<script>` 外沒有其他可視元素。
   * 若滿足,套用 `100vw` 與 `100vh`,表現與原版完全一致。
2. **嵌入式環境 (Embedded) 處理**:
   * 若不滿足上述條件(無論是嵌入的 `.ejceesmain` 還是 `.ejceesmini`),透過 CSS 設定 `width: 100%; height: 100%;`,並將 `max-width` 和 `max-height` 綁定為父元素尺寸。
   * 透過 `ResizeObserver` 或 CSS Flexbox 確保 SVG 棋盤與控制區(Record/Tools)在有限空間內正確縮放,不超出螢幕可視高度 (`max-height: 100vh`)。

### 第四階段:初始化資料提取 (Data Initialization)
1. **提取 `innerHTML`**:在清空容器並注入預設的 HTML 模板之前,先讀取容器原有的 `innerHTML`(並去除頭尾空白)。
2. **匯入棋譜邏輯**:如果提取到的文字不為空,將其存入 `this.initialData`。待 DOM 模板渲染完成、事件綁定後,自動呼叫現有的 `importExportedText(this.initialData)` 進行棋譜解析與盤面還原。

### 第五階段:啟動管理器 (Bootstrapper / Instance Manager)
在 `ejceesobj.js` 的最底部,撰寫自動執行的初始化腳本(監聽 `DOMContentLoaded`):
1. **尋找主容器**:執行 `document.querySelectorAll('.ejceesmain')`。
   * 如果找到,**只取第一個**元素,實例化 `new EjceesApp(element)`。忽略其他 `.ejceesmain` 以及所有的 `.ejceesmini`。
2. **尋找迷你容器**:如果沒有找到 `.ejceesmain`,則執行 `document.querySelectorAll('.ejceesmini')`。
   * 針對返回的每一個 NodeList 元素,分別實例化 `new EjceesApp(element)`。它們將各自擁有獨立的記憶體狀態與 DOM 樹。

---

以上是將該程式封裝為對象化架構的具體步驟。確認這個流程符合您的預期後,我們可以開始動手編寫程式碼。請問您希望我直接提供完整的 `ejceesobj.js` 程式碼,還是分區塊(例如先給 CSS 與 HTML 模板封裝的部分)逐步為您建構?
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5008
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 173 次
被圈友点赞: 202 次
联系:

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

帖子 ejsoon »

先給 CSS 與 HTML 模板封裝的部分,只需告知所要修改的地方,所有的代碼和注釋都要使用英文。

gemini pro:

代码: 全选

了解。以下是第一階段(CSS 與 HTML 模板封裝)的具體修改指南。為了確保多實例(Multi-instance)環境下的獨立性,我們需要處理全域樣式的污染問題,以及 SVG 內部 `id` 的衝突問題。

請在你的專案中建立 `ejceesobj.js`,並依照以下說明建立基礎結構。

### 1. CSS Encapsulation & Injection
**What to modify:**
* Remove the global `*` and `body, html` selectors from your original CSS to prevent the widget from overriding the host page's styles.
* Add `.ejceesmini` alongside `.ejceesmain` in the layout rules so both classes share the same flexbox structure.
* Wrap the CSS in a static method inside the class. It checks if the `<style>` tag already exists in the `<head>` to ensure it's only loaded once, regardless of how many instances are created.

```javascript
class EjceesApp {
    constructor(container) {
        this.container = container;
        
        // Generate a unique ID for this instance to scope SVG <defs>
        this.instanceId = 'ejcees_' + Math.random().toString(36).substring(2, 11);
        
        // Inject CSS once globally
        EjceesApp.injectCSS();
        
        // Render the HTML structure
        this.renderDOM();
    }

    static injectCSS() {
        if (document.getElementById('ejcees-global-style')) return;

        const style = document.createElement('style');
        style.id = 'ejcees-global-style';
        style.textContent = `
            /* Use a scoped reset instead of global * selector */
            .ejceesmain *, .ejceesmini * {
                box-sizing: border-box;
                margin: 0;
                padding: 0;
            }

            /* Apply main layout rules to both main and mini containers */
            .ejceesmain, .ejceesmini {
                display: flex;
                background-color: #f4f4f9;
                overflow: hidden;
                font-family: sans-serif;
            }

            /* .ejceesmain takes full viewport if it's the only element */
            .ejceesmain {
                width: 100vw;
                height: 100vh;
            }

            /* .ejceesmini adapts to its parent container */
            .ejceesmini {
                width: 100%;
                height: 100%;
                max-height: 100vh; /* Prevent overflowing the screen height */
            }

            /* --- Paste the rest of your original CSS here exactly as is --- */
            .ejceessvg {
                display: flex;
                justify-content: center;
                align-items: center;
                /* ... rest of the original CSS ... */
            }
            
            /* Make sure to include all your original media queries and tool button styles */
        `;
        document.head.appendChild(style);
    }
}
```

### 2. HTML Template Generation
**What to modify:**
* Move the inner HTML of `.ejceesmain` into a method called `renderDOM()`.
* **CRITICAL FIX FOR MULTI-INSTANCE**: SVG elements rely heavily on `id` and `href="#id"` (e.g., `<g id="tile0">` and `<use href="#tile0">`). In a multi-instance setup, duplicate IDs across different `.ejceesmini` containers will cause browsers to render the wrong pieces or gradients. 
* You must append `${this.instanceId}` to **every** SVG definition `id` and its corresponding `href`.

```javascript
    renderDOM() {
        // Read existing content as initial imported data (if any)
        this.initialData = this.container.innerHTML.trim();
        
        // Clear the container
        this.container.innerHTML = '';

        // Generate the DOM structure
        // Note: We dynamically append -${this.instanceId} to SVG IDs and hrefs
        this.container.innerHTML = `
            <div class="ejceessvg">
                <svg class="ejceespb" viewBox="0 0 432 480" preserveAspectRatio="xMidYMid meet" id="etani-${this.instanceId}" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg" width="432" height="480" rfm="repeat">
                    <defs mode="0" rotatestep="1" rotatestart="0" stepvalue="90" snapto="0" displaywait="1">
                        <g id="tile0-${this.instanceId}" frontfill="none" backfill="#2691c0">
                            </g>
                        <g id="tile1-${this.instanceId}" frontfill="none" backfill="#2691c0">
                            </g>
                    </defs>

                    <g class="etdrop" stroke="#777" stroke-width="2" stroke-linejoin="round" transform="translate(0,0) scale(1,1)" id="etanidrop-${this.instanceId}">
                        <g class="etboard">
                            <g id="ejceesstop-${this.instanceId}" stroke="#777">
                                <use href="#tile0-${this.instanceId}" fill="none" transform="translate(72,120) scale(1,1) rotate(0)" />
                                </g>
                            <use href="#ejceesstop-${this.instanceId}" transform="rotate(180 216,240)" />

                            <radialGradient id="whiteToTransparent-${this.instanceId}" cx="0.5" cy="0.5" r="0.5" fx="0.5" fy="0.5">
                                <stop offset="0%" stop-color="white" stop-opacity="1" />
                                <stop offset="100%" stop-color="white" stop-opacity="0" />
                            </radialGradient>
                            
                            <circle id="ejceesstartdot-${this.instanceId}" cx="-24" cy="-24" r="12" fill="url(#whiteToTransparent-${this.instanceId})" stroke="none" opacity="0.777" />

                            <use id="r0-${this.instanceId}" href="#tile2-${this.instanceId}" fill="none" stroke="none" transform="translate(24,24) scale(1,1) rotate(0)" />
                            </g>
                    </g>
                </svg>
            </div>
            <div class="ejceesctrl">
                <div class="ejceesoutput">Click on the SVG area to get coordinates.</div>
                <div class="ejceesstep">
                    <div class="ejceesstepminus">-</div>
                    <input type="range" class="ejceesstepdrop" min="0" max="0" value="0">
                    <div class="ejceesstepplus">+</div>
                </div>

                <div class="ejceestool">
                    </div>

                <div class="ejceestext">
                    <div class="ejceesrecord">
                        <div class="ejceesrcdstart" id="record-start-${this.instanceId}">棋局開始,紅方先行</div>
                    </div>
                    <div class="ejceesnote">
                        <div class="ejceescomment"></div>
                        <div class="ejceestextbtn"></div>
                    </div>
                </div>
            </div>
            <input type="file" class="file-input" style="display:none">
        `;
    }
```

### 重點提醒 (Key Takeaways for Phase 1):
1. **`<input type="file" id="file-input">`**: 我將它的 `id` 改成了 `class="file-input"`。在多實例環境中,應避免使用靜態 `id`,後續綁定事件時我們將使用 `this.container.querySelector('.file-input')`。
2. **SVG `<use>` References**: 請務必確保所有原代碼中的 `<g id="tileX">` 與 `<use href="#tileX">` 都替換為帶有 `${this.instanceId}` 的字串,否則在同一頁面渲染兩個棋盤時,圖形會互相引用導致破版。

完成這些替換後,我們就可以進入**第二階段:核心邏輯物件化與隔離 DOM 查詢**。如果您準備好繼續,請告訴我。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
回复

在线用户

正浏览此版面之用户: 没有注册用户 和 0 访客