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

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

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

帖子 ejsoon »

改進:
在pgn-meta-modal窗口中,增加一個「Review」,指對局終的點評。

例如:
[Game "Chinese Chess"]
[Result "0-1"]
[Rotate "true"]
[Format "Chinese"]

{吳可欣贏網友一盤棋}
1. 炮八平五 馬2進3
2. 兵七進一 車1平2
3. 馬八進七 卒7進1
4. 車九平八 馬8進7
5. 馬二進一 車9進1
6. 炮二平四{估計是這步棋使局勢急轉直下,因為它導致紅方右車完全出不來}
馬7進8
7. 炮四進三 卒7進1
8. 兵三進一 馬8進6
9. 馬七進六 車9平4
{吳姐說希望他踩中兵}
10. 馬六進五 馬3進5
11. 炮五進四 車4進3
12. 兵三進一 車4平5
{吳姐說差點走馬6進8}
13. 車一平二 砲8進4
14. 炮五平四 車5進2
15. 仕四進五 馬6進4
16. 車八進六 馬4進3
17. 帥五平四 砲8平6
18. 前炮平六 砲6退1
19. 炮六退五 車5平6
20. 帥四平五 車6平4
21. 帥五平四 車4進2
22. 車二進四 車4退2
23. 車二平四 砲2平6
24. 車八進三 砲6進3
{紅方認負}
0-1
{吳可欣正常發揮,拿下對手}

則「紅方認負」是最後一步棋的評論,而「吳可欣正常發揮,拿下對手」是Review」。

只需給出所要修改的地方,所有的代碼和注釋都要使用英文。


gemini pro:

代码: 全选

Here are the modifications needed to support the post-game "Review" feature. All code and comments are provided in English as requested.

### 1. Update Constructor
Add the `Review` field to the default PGN metadata arrays so it appears in the `pgn-meta-modal`.

**Find this part in `constructor(container, mode)`:**
```javascript
    this.pgnMetadata = [
      { name: 'Event', value: '' },
      { name: 'Site', value: '' },
      // ... (other fields)
      { name: 'Annotator', value: '' },
      { name: 'Termination', value: '' }
    ];
    this.defaultPgnKeys = [
      'Event',
      'Site',
      // ... (other fields)
      'Annotator',
      'Termination'
    ];
```

**Change it to:**
```javascript
    this.pgnMetadata = [
      { name: 'Event', value: '' },
      { name: 'Site', value: '' },
      { name: 'Date', value: '' },
      { name: 'Round', value: '' },
      { name: 'RedTeam', value: '' },
      { name: 'Red', value: '' },
      { name: 'BlackTeam', value: '' },
      { name: 'Black', value: '' },
      { name: 'Result', value: '' },
      { name: 'Time', value: '' },
      { name: 'TimeControl', value: '' },
      { name: 'Opening', value: '' },
      { name: 'Variation', value: '' },
      { name: 'ECCO', value: '' },
      { name: 'Annotator', value: '' },
      { name: 'Termination', value: '' },
      { name: 'Review', value: '' } // Added Review field
    ];
    this.defaultPgnKeys = [
      'Event',
      'Site',
      'Date',
      'Round',
      'RedTeam',
      'Red',
      'BlackTeam',
      'Black',
      'Result',
      'Time',
      'TimeControl',
      'Opening',
      'Variation',
      'ECCO',
      'Annotator',
      'Termination',
      'Review' // Added Review field
    ];
```

---

### 2. Update PGN Parser
Modify the PGN import logic to extract the trailing review comment that appears *after* the game result.

**Find this part in `importExportedText(text, importedFen = null)`:**
```javascript
      // Extract notation body
      let bodyStartIndex = text.lastIndexOf(']');
      let bodyText = text.substring(bodyStartIndex + 1).trim();
      bodyText = bodyText.replace(/(?:1-0|0-1|1\/2-1\/2|\*)$/, '').trim();
```

**Change it to:**
```javascript
      // Extract notation body
      let bodyStartIndex = text.lastIndexOf(']');
      let bodyText = text.substring(bodyStartIndex + 1).trim();

      // Extract trailing Review comment if it exists after the result
      const reviewRegex = /(?:1-0|0-1|1\/2-1\/2|\*)\s*\{([^}]*)\}\s*$/;
      const reviewMatch = bodyText.match(reviewRegex);
      if (reviewMatch) {
        const existingReview = this.pgnMetadata.find(m => m.name === 'Review');
        if (existingReview) {
          existingReview.value = reviewMatch[1].trim();
        } else {
          this.pgnMetadata.push({ name: 'Review', value: reviewMatch[1].trim() });
        }
        // Remove the result and review part from bodyText
        bodyText = bodyText.replace(reviewRegex, '').trim();
      }

      bodyText = bodyText.replace(/(?:1-0|0-1|1\/2-1\/2|\*)$/, '').trim();
```

---

### 3. Update PGN Generator
Modify the PGN generation logic to prevent `Review` from being printed as a standard tag at the top, and instead append it in brackets at the very end of the file.

**Find the beginning of `generatePGNFormat(oddState)`:**
```javascript
  generatePGNFormat(oddState) {
    let text = '[Game "Chinese Chess"]\n';
    let userResult = '';

    this.pgnMetadata.forEach(meta => {
      let val = meta.value ? meta.value.trim() : '';
      let isResultEmpty = meta.name === 'Result' && val === '*';
      if (val !== '' && !isResultEmpty) {
        text += `[${meta.name} "${val}"]\n`;
        if (meta.name === 'Result') userResult = val;
      }
    });
```

**Change it to:**
```javascript
  generatePGNFormat(oddState) {
    let text = '[Game "Chinese Chess"]\n';
    let userResult = '';
    let reviewText = '';

    this.pgnMetadata.forEach(meta => {
      let val = meta.value ? meta.value.trim() : '';
      let isResultEmpty = meta.name === 'Result' && val === '*';
      if (val !== '' && !isResultEmpty) {
        if (meta.name === 'Review') {
          reviewText = val; // Store review text to append at the end
        } else {
          text += `[${meta.name} "${val}"]\n`;
          if (meta.name === 'Result') userResult = val;
        }
      }
    });
```

**Then, find the end of `generatePGNFormat(oddState)`:**
```javascript
    let startIsRed = this.historyFEN.fen.split(' ')[1] === 'w';
    let initMoveNum = parseInt(this.historyFEN.fen.split(' ')[5], 10) || 1;
    text += processPGNSequence(this.historyFEN, initMoveNum, startIsRed);
    if (!text.endsWith('\n')) text += '\n';
    return text + finalTerminator;
  }
```

**Change it to:**
```javascript
    let startIsRed = this.historyFEN.fen.split(' ')[1] === 'w';
    let initMoveNum = parseInt(this.historyFEN.fen.split(' ')[5], 10) || 1;
    text += processPGNSequence(this.historyFEN, initMoveNum, startIsRed);
    if (!text.endsWith('\n')) text += '\n';
    text += finalTerminator;

    // Append Review comment at the very end if it exists
    if (reviewText !== '') {
      text += `\n{${reviewText}}`;
    }

    return text;
  }
```
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5443
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 176 次
被圈友点赞: 204 次
联系:

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

帖子 ejsoon »

東萍分支導入出錯:

代码: 全选

      for (let i = 0; i < lines.length; i++) {
        let line = lines[i];
        if (!line) continue;

        if (!/^\d+\./.test(line)) {
          // Rule 3: Lines not starting with "^\d+\." are comments or branch declarations
          let circles = line.match(/[①-⑳]|\(\d+\)/g) || [];
          let isBranchDecl = false;

          // Rule 4 & 5: Find the branch insertion point (second occurrence)
          for (let c of circles) {
            console.log('(circles, branchMap[c] ,c)', circles, branchMap[c] ,c);
            if (seenCircles.has(c) && branchMap[c]) {
              currNode = branchMap[c].parentNode;
              currentFen = branchMap[c].fen;
              vMap = this.buildVirtualMap(currentFen);
              currentCommentNode = currNode;
              isBranchDecl = true;
              delete branchMap[c]; // Only care about the first two occurrences
              break; // Switch context once per line
            }
          }

          // Accumulate comments
          if (!isBranchDecl) {
            if (!currentCommentNode.c) currentCommentNode.c = '';
            currentCommentNode.c += (currentCommentNode.c ? '\n' : '') + line;
          }
        } else {
          // Rule 1 & 2: Line starting with "^\d+\.", strip the number, then split by spaces
          let cleanLine = line.replace(/^\d+\./, '').trim();
          let tokens = cleanLine.split(/[\s,,!!??;;]+/);

          for (let token of tokens) {
            // Ignore standalone ellipses
            if (token === '...' || token === '…') continue;

            let circles = line.match(/[①-⑳]|\(\d+\)/g) || [];
            // Strip circled numbers and ellipses to get the pure move string
            let moveStr = token.replace(/[①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳…\.]|\(\d+\)/g, '');

            console.log(moveStr, currentFen);
            if (moveStr.length >= 4) {
              let dc = null;
              let moveEn = '';
              let isRed = currentFen.includes(' w');

              if (/^[a-i][0-9][a-i][0-9]$/.test(moveStr)) {
                let startX = moveStr.charCodeAt(0) - 97;
                let startY = 9 - parseInt(moveStr.charAt(1), 10);
                let endX = moveStr.charCodeAt(2) - 97;
                let endY = 9 - parseInt(moveStr.charAt(3), 10);
                dc = {
                  startX,
                  startY,
                  endX,
                  endY
                };

                let pId = vMap.get(`${startX},${startY}`);
                if (!pId) {
                  hasError = true;
                  continue;
                }
                moveEn = this.coordsToEngine(startX, startY, endX, endY);
              } else {
                let isEnglish = /^[a-zA-Z]/.test(moveStr) || /^[+\-=1-9][a-zA-Z0-9]/.test(moveStr);
                moveEn = isEnglish ? moveStr : this.NotationConverter.toEnglish(moveStr, isRed);
                if (/^[RNBAKCPrnbakcp+\-=1-9]{2}[+\-=][1-9]/.test(moveEn)) {
                  dc = this.deriveCoordsFromPgnMove(currentFen, moveEn, isRed);
                } else {
                  dc = moveEn;
                }
              }

              if (moveEn === dc) {
                console.log('is here currentFen', currentFen);
                if (!currentCommentNode.c) {
                  currentCommentNode.c = '';
                }
                // Append the token, separating with a space if a comment already exists
                currentCommentNode.c += (currentCommentNode.c ? ' ' : '') + token;
                continue; // Skip node creation and continue parsing
              }

              if (!dc) {
                const cleanInitial = this.INITIAL_FEN.replace(/ - - 0 1$/, '');
                if (importedFen !== cleanInitial && importedFen !== this.INITIAL_FEN) {
                  this.importExportedText(text, this.INITIAL_FEN);
                  return;
                } else {
                  alert('Parsed partially due to an invalid move notation.');
                  this.renderRecordUI();
                  this.renderNoteUI();
                  this.updateToolHighlights();
                  return;
                }
              }

              // Rule 4: The variation is attached to the parent of the move
              lastMoveParent = currNode;
              lastMoveParentFen = currentFen;

              let nextFen = this.simulateMove(currentFen, dc);
              let childIdx = currNode.v.findIndex(c => c.move === moveEn && c.fen === nextFen);

              if (childIdx === -1) {
                let newNode = {
                  fen: nextFen,
                  move: this.coordsToEngine(dc.startX, dc.startY, dc.endX, dc.endY),
                  c: '',
                  v: []
                };
                currNode.v.push(newNode);
                childIdx = currNode.v.length - 1;
              }

              // If this.this creates a fork on the main path, update this.currentBranch
              if (currNode.v.length > 1 && currNode === attachNode) {
                this.currentBranch[forkCount] = childIdx;
                forkCount++;
                attachNode = currNode.v[childIdx]; // Follow the first parsed line as the main path
              }

              currNode = currNode.v[childIdx];
              currentFen = nextFen;
              currentCommentNode = currNode;

              let pId = vMap.get(`${dc.startX},${dc.startY}`);
              vMap.delete(`${dc.startX},${dc.startY}`);
              if (pId) vMap.set(`${dc.endX},${dc.endY}`, pId);
            } else {
              // lastMoveParent = currNode;
              // lastMoveParentFen = currentFen;
            }

            // Rule 4, 7 & 8: Process multiple circled numbers wherever they occur
            for (let c of circles) {
              if (!seenCircles.has(c)) {
                console.log('first c', c);
                seenCircles.add(c);
                // Map the first occurrence to the parent node of the currently parsed move
                console.log('lastMoveParent, lastMoveParentFen', lastMoveParent, lastMoveParentFen);
                branchMap[c] = {
                  parentNode: lastMoveParent,
                  fen: lastMoveParentFen
                };
              }
            }
          }
        }
      }



當我用以上的js程式導入以下文本:
「
吳可欣翻車現場

表面上看,吳姐似乎是因為失誤翻車的,實際上走到最後已經是必敗無疑了
1. 兵七進一 卒7進1
2. 炮二平三 砲8平5
3. 馬八進七 馬8進7
4. 相七進五 馬7進6
5. 仕六進五 車9平8
6. 馬二進一 馬2進3
吳姐說,只要你堅持一直做一件事情,你就一定會成功的。但是呢,還要學會總結。
7. 車九平六 砲2平1
吳姐說,今天的翻車已經結束了,再也不能讓你們看到我翻車的名場面了,這個不允許啊
8. 車一進一  車1平2
9. 車一平四 馬6進5
10. 馬七進五 砲5進4
吳姐說,這個棋我以前是不是輸給王子涵啊?有點像…

吳姐很想車六進三
11. 兵一進一
AI也是這樣想的
11. ………… 車2進6
吳姐說,這個人壞滴很!
12. 車四進三
吳姐說,這個棋我們應該還是要猛起來,我們一兇,對面就慫了
12. ………… 砲1進4 ②
13. 馬一進二
吳姐說這棋應該已經算明白了
13. ………… 砲1退1
14. 兵七進一 砲1平8
15. 兵七進一 砲8進4
16. 兵七進一 車8進8
吳姐說剛才是算明白了,但是女人是善變的
17. 車六進一 車2平3
18. 炮八平七 車8平7 ④
黑方應該吃兵,否則紅方帥坐出就會反先
19. 帥五平六 


②
12. ………… 象7進5
24層-59分
13. 馬一進二 士6進5
14. 炮三平二 車8平7

④
18. ………… 車3退4
25層195分
19. 車六進二 砲5退2
20. 車六平五 車3平2
21. 炮七平八 車8平7
22. 車四平二 砲8平9
」

如果去掉注釋:
              // lastMoveParent = currNode;
              // lastMoveParentFen = currentFen;
④就不能導入,如果注釋的話,②就無法正確導入。

請說明原因,並提出修改辦法。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5443
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 176 次
被圈友点赞: 204 次
联系:

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

帖子 ejsoon »

grok出馬:

代码: 全选

**問題診斷正確**。

目前程式碼的邏輯在這一行有明顯缺陷:

```js
lastMoveParent = currNode;           // 這一步執行得太晚
```

當分支第一次出現在**本步走法之後**(如 `12. ... 砲1進4①`),`lastMoveParent` 還沒來得及更新,就已經用舊值記錄到 `branchMap` 了,導致後續第二次出現圈號時跳錯位置。

---

### 正確的語意應該是:

> **圈號①出現在哪一步的後面,就代表該分支是從「這一步」之後產生的變招。**

因此,`branchMap[c]` 應該記錄的是**剛剛處理完的這一步**(`currNode`),而不是它的父節點。

### 建議修正方式(推薦)

把圈號處理的時機**移到建立新節點之後**,並且明確記錄「當前走完的節點」作為分支插入點。

#### 修改後的核心片段:

```js
for (let token of tokens) {
    if (token === '...' || token === '…') continue;

    let circles = line.match(/[①-⑳]|\(\d+\)/g) || [];
    let moveStr = token.replace(/[①-⑳…\.]|\(\d+\)/g, '');

    // ... 原有的走法解析、建立節點的程式碼 ...

    if (moveStr.length >= 4) {
        // ... 所有建立新節點的邏輯保持不變 ...

        // === 重點修改:成功走一步後再處理圈號 ===
        if (dc && moveEn !== dc) {   // 成功建立走法的情況
            lastMoveParent = currNode;           // ← 現在 currNode 已經是新建立的這一步
            lastMoveParentFen = currentFen;

            // Rule 4,7,8:處理圈號
            for (let c of circles) {
                if (!seenCircles.has(c)) {
                    seenCircles.add(c);
                    branchMap[c] = {
                        parentNode: lastMoveParent,   // 這一步就是分支的父節點
                        fen: lastMoveParentFen
                    };
                    console.log(`[Branch] First occurrence of ${c} attached to move: ${moveEn}`);
                }
            }
        }
    } 
    else {
        // 無法解析的 token 當註解處理...
    }

    // === 移除原本放在 for token 迴圈最後的圈號處理 ===
}
```

---

### 額外建議優化

1. **把 `circles` 從 `line` 改成從 `token` 提取**(更精準):
   ```js
   let circles = token.match(/[①-⑳]|\(\d+\)/g) || [];
   ```

2. **增加對「... ①」這種情況的支援**(省略號後直接標分支):
   ```js
   // 在處理 token 前先檢查整行是否有圈號但沒有實質走法
   if (circles.length && cleanLine.includes('...')) {
       // 直接把分支掛在 currNode(前一步)
       for (let c of circles) {
           if (!seenCircles.has(c)) {
               seenCircles.add(c);
               branchMap[c] = { parentNode: currNode, fen: currentFen };
           }
       }
   }
   ```

3. **第二次出現時的處理**(建議加上防護):
   ```js
   if (seenCircles.has(c) && branchMap[c]) {
       currNode = branchMap[c].parentNode;
       currentFen = branchMap[c].fen;
       vMap = this.buildVirtualMap(currentFen);
       currentCommentNode = currNode;
       delete branchMap[c];
       isBranchDecl = true;
       // 可選:break; 改成 continue; 讓一行可以有多個分支切換
   }
   ```

---

### 為什麼原本會錯?

- 原程式把 `lastMoveParent = currNode` 放在**走法處理前面**。
- 當這行是 `12. ... 砲1進4①` 時,`currNode` 還是第11步,導致分支被錯誤掛在第11步。

修正後,只要**成功走完一步**,就把 `currNode` 當作分支父節點,就符合「圈號出現在哪步後面,分支就從哪步開始」的直覺。

---

需要我幫你把**完整修正後的迴圈**重新整理出來嗎?
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5443
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 176 次
被圈友点赞: 204 次
联系:

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

帖子 ejsoon »

ejceesobj202605021716

修復了東萍多分支導入問題。

還得是gemini pro啊!!!
附件
ejceesobj202605021716.js
(305.48 KiB) 已下载 6 次
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5443
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 176 次
被圈友点赞: 204 次
联系:

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

帖子 ejsoon »

mini模式的ctrl+右,不太對。

mini模式,play位置要跟設置調換。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5443
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 176 次
被圈友点赞: 204 次
联系:

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

帖子 ejsoon »

改進:
在pgn-meta-modal窗口中,Review將固定顯示在最後面(png-add-btn的下面),其前面將沒有序號,後面將沒有刪除按鈕。

它的name將寫在一個label而不是input,使其無法更改。它的value的輸入框將是一個textarea。

只需給出所要修改的地方,所有的代碼和注釋都要使用英文。

代码: 全选

改進:
在pgn-meta-modal窗口中,Review將固定顯示在最後面(png-add-btn的下面),其前面將沒有序號,後面將沒有刪除按鈕。

它的name將寫在一個label而不是input,使其無法更改。它的value的輸入框將是一個textarea。

只需給出所要修改的地方,所有的代碼和注釋都要使用英文。

gemini pro:

代码: 全选

Here are the modifications to isolate the "Review" field into a dedicated, fixed `<textarea>` UI below the table. 

### 1. Extract `Review` from the table initialization
We need to capture the value of the `Review` tag and exclude it from the array that renders the table.

**Find this part in `showPGNMetaModal()`:**
```javascript
    // Improvement 1: Sort items (non-empty at top, empty defaults below, preserving order)
    let withValue = [];
    let emptyDefaults = [];
    let addedKeys = new Set();

    this.pgnMetadata.forEach(item => {
      let val = item.value ? item.value.trim() : '';
      // Improvement 3: Treat '*' for Result as an empty value
      let isResultEmpty = item.name === 'Result' && val === '*';
      let isRotateOrFlip = item.name === 'Rotate' || item.name === 'Flip';
      if (val !== '' && !isResultEmpty && !isRotateOrFlip) {
        withValue.push({ name: item.name, value: item.value });
        addedKeys.add(item.name);
      }
    });

    // Add defaults in original order if not already added
    this.defaultPgnKeys.forEach(key => {
      let isRotateOrFlip = key === 'Rotate' || key === 'Flip';
      if (!addedKeys.has(key) && !isRotateOrFlip) {
        emptyDefaults.push({ name: key, value: key === 'Result' ? '*' : '' });
        addedKeys.add(key);
      }
    });

    // Add any remaining custom fields that were empty
    this.pgnMetadata.forEach(item => {
      let isRotateOrFlip = item.name === 'Rotate' || item.name === 'Flip';
      if (!addedKeys.has(item.name) && !isRotateOrFlip) {
        emptyDefaults.push({ name: item.name, value: item.value });
        addedKeys.add(item.name);
      }
    });
```

**Change it to:**
```javascript
    // Improvement 1: Sort items (non-empty at top, empty defaults below, preserving order)
    let withValue = [];
    let emptyDefaults = [];
    let addedKeys = new Set();
    let reviewValue = ''; // Store Review value separately

    this.pgnMetadata.forEach(item => {
      let val = item.value ? item.value.trim() : '';
      let isResultEmpty = item.name === 'Result' && val === '*';
      let isRotateOrFlip = item.name === 'Rotate' || item.name === 'Flip';
      let isReview = item.name === 'Review';
      
      if (isReview) {
        reviewValue = val;
      } else if (val !== '' && !isResultEmpty && !isRotateOrFlip) {
        withValue.push({ name: item.name, value: item.value });
        addedKeys.add(item.name);
      }
    });

    // Add defaults in original order if not already added
    this.defaultPgnKeys.forEach(key => {
      let isRotateOrFlip = key === 'Rotate' || key === 'Flip';
      let isReview = key === 'Review';
      if (!addedKeys.has(key) && !isRotateOrFlip && !isReview) {
        emptyDefaults.push({ name: key, value: key === 'Result' ? '*' : '' });
        addedKeys.add(key);
      }
    });

    // Add any remaining custom fields that were empty
    this.pgnMetadata.forEach(item => {
      let isRotateOrFlip = item.name === 'Rotate' || item.name === 'Flip';
      let isReview = item.name === 'Review';
      if (!addedKeys.has(item.name) && !isRotateOrFlip && !isReview) {
        emptyDefaults.push({ name: item.name, value: item.value });
        addedKeys.add(item.name);
      }
    });
```

---

### 2. Exclude `Review` when clicking the "Default" button
Ensure that `Review` doesn't accidentally get pushed back into the table when the user clicks the "Default" button.

**Find this part in `showPGNMetaModal()`:**
```javascript
      // Add all default keys in their proper sequence
      this.defaultPgnKeys.forEach(key => {
        let val = currentMap.has(key) ? currentMap.get(key) : key === 'Result' ? '*' : '';
        newLocal.push({ name: key, value: val });
        currentMap.delete(key);
      });
```

**Change it to:**
```javascript
      // Add all default keys in their proper sequence
      this.defaultPgnKeys.forEach(key => {
        if (key === 'Review') return; // Skip Review
        let val = currentMap.has(key) ? currentMap.get(key) : key === 'Result' ? '*' : '';
        newLocal.push({ name: key, value: val });
        currentMap.delete(key);
      });
```

---

### 3. Retrieve `Review` before saving on close
Fetch the value from our custom `<textarea>` and append it back to the metadata list right before closing the modal.

**Find this part in `showPGNMetaModal()`:**
```javascript
    closeBtn.onclick = () => {
      syncDOM();

      // Only keep items with a value (Result '*' is treated as empty)
      localMetadata.filter(item => {
        const val = item.value ? item.value.trim() : '';
        return item.name && val !== '' && !(item.name === 'Result' && val === '*');
      });
```

**Change it to:**
```javascript
    closeBtn.onclick = () => {
      syncDOM();

      // Retrieve Review value from the custom textarea
      const reviewTextarea = box.querySelector('.pgn-meta-review-textarea');
      if (reviewTextarea) {
        localMetadata.push({ name: 'Review', value: reviewTextarea.value });
      }

      // Only keep items with a value (Result '*' is treated as empty).
      // Make sure to assign the filtered array back to localMetadata.
      localMetadata = localMetadata.filter(item => {
        const val = item.value ? item.value.trim() : '';
        return item.name && val !== '' && !(item.name === 'Result' && val === '*');
      });
```

---

### 4. Create and append the fixed `Review` UI
Build the `<label>` and `<textarea>` elements and append them beneath the `pgn-add-btn`.

**Find this part at the very bottom of `showPGNMetaModal()`:**
```javascript
    const addBtn = document.createElement('div');
    addBtn.className = 'pgn-add-btn';
    addBtn.textContent = '+';
    addBtn.onclick = () => {
      syncDOM();
      localMetadata.push({ name: '', value: '' });
      renderTable();
    };

    box.appendChild(closeBtn);
    box.appendChild(headerDiv);
    box.appendChild(table);
    box.appendChild(addBtn);
    modal.appendChild(box);
    document.body.appendChild(modal);
  }
```

**Change it to:**
```javascript
    const addBtn = document.createElement('div');
    addBtn.className = 'pgn-add-btn';
    addBtn.textContent = '+';
    addBtn.onclick = () => {
      syncDOM();
      localMetadata.push({ name: '', value: '' });
      renderTable();
    };

    // Construct the fixed Review UI
    const reviewContainer = document.createElement('div');
    reviewContainer.style.marginTop = '20px';
    reviewContainer.style.display = 'flex';
    reviewContainer.style.flexDirection = 'column';
    reviewContainer.style.gap = '8px';

    const reviewLabel = document.createElement('label');
    reviewLabel.textContent = 'Review';
    reviewLabel.style.fontWeight = 'bold';
    reviewLabel.style.fontSize = '14px';
    reviewLabel.style.color = '#ccc';

    const reviewTextarea = document.createElement('textarea');
    reviewTextarea.className = 'pgn-meta-input pgn-meta-review-textarea';
    reviewTextarea.style.minHeight = '70px';
    reviewTextarea.style.resize = 'vertical';
    reviewTextarea.style.fontFamily = 'inherit';
    reviewTextarea.style.padding = '8px';
    reviewTextarea.placeholder = 'Enter post-game review/comments here...';
    reviewTextarea.value = reviewValue;

    reviewContainer.appendChild(reviewLabel);
    reviewContainer.appendChild(reviewTextarea);

    box.appendChild(closeBtn);
    box.appendChild(headerDiv);
    box.appendChild(table);
    box.appendChild(addBtn);
    box.appendChild(reviewContainer); // Append Review UI below the add button
    modal.appendChild(box);
    document.body.appendChild(modal);
  }
```
上次由 ejsoon 在 2026年 5月 2日 23:17,总共编辑 1 次。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5443
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 176 次
被圈友点赞: 204 次
联系:

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

帖子 ejsoon »

在一個html元素中,它的內容有可能是從「0/36」到「36/36」,也有可能是從「0/120」到「120/120」。

如何用js確定它的數字達到最大時的寬度,然後固定這個寬度?

代码: 全选

/**
 * 固定元素的寬度,使其足以容納最大數值
 * @param {HTMLElement} element - 目標 HTML 元素
 * @param {string} maxText - 可能出現的最長字串 (例如 "120/120")
 */
function fixToMaxContentWidth(element, maxText) {
    // 1. 建立一個臨時的 span 來測量寬度
    const clone = document.createElement('span');
    
    // 2. 複製目標元素的字體樣式,確保測量精準
    const style = window.getComputedStyle(element);
    clone.style.fontFamily = style.fontFamily;
    clone.style.fontSize = style.fontSize;
    clone.style.fontWeight = style.fontWeight;
    clone.style.letterSpacing = style.letterSpacing;
    
    // 3. 讓克隆元素在視覺上不可見,且不影響佈局
    clone.style.position = 'absolute';
    clone.style.visibility = 'hidden';
    clone.style.whiteSpace = 'nowrap';
    
    // 4. 設定為最大可能的內容
    clone.innerText = maxText;
    
    // 5. 加入 DOM 中以計算寬度
    document.body.appendChild(clone);
    const maxWidth = clone.offsetWidth;
    
    // 6. 移除臨時元素並套用寬度
    document.body.removeChild(clone);
    element.style.width = `${maxWidth}px`;
    element.style.display = 'inline-block'; // 確保 width 屬性生效
    element.style.textAlign = 'center';     // 讓數字在固定寬度內置中(可選)
}

// 使用範例:
const myLabel = document.querySelector('.status-label');
fixToMaxContentWidth(myLabel, "120/120");

代码: 全选

.number-display {
  /* 優先使用現代、漂亮的等寬字體,最後才交給系統決定 */
  font-family: 
    'Cascadia Code',   /* Windows 新版字體 */
    'Source Code Pro', /* Adobe 開源字體 */
    'Menlo',           /* Mac 經典 */
    'Monaco',          /* Mac 經典 */
    'Consolas',        /* Windows 經典 */
    'Roboto Mono',     /* Android 預設 */
    monospace;         /* 萬用保底 */
}
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5443
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 176 次
被圈友点赞: 204 次
联系:

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

帖子 ejsoon »

代码: 全选

<script>
/**
 * 強化調試版:使用 DOM API 構建頁面
 */
function openEjceesWindow() {
    console.group('Ejcees Window Debugger');
    
    // 1. 檢查 PHP 傳入的 URL
    const scriptUrl = "<?php echo wp_get_attachment_url(19088); ?>";
    console.log('解析出的 Script URL:', scriptUrl);

    if (!scriptUrl || scriptUrl.trim() === "") {
        console.error('錯誤: 無法獲取 WordPress 附件 URL,請檢查 attachmentId 是否正確。');
        console.groupEnd();
        return;
    }

    try {
        // 2. 構建文檔
        const doc = document.implementation.createHTMLDocument("Ejcees Object");
        
        // Mobile Viewport
        const meta = doc.createElement('meta');
        meta.name = "viewport";
        meta.content = "width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no";
        doc.head.appendChild(meta);

        const style = doc.createElement('style');
        style.textContent = `
            body, html { margin: 0; padding: 0; width: 100%; height: 100%; overflow: hidden; background: #222; color: white; }
            .ejcees { width: 100vw; height: 100vh; display: block; }
            #debug-overlay { position: fixed; top: 10px; left: 10px; background: rgba(0,0,0,0.5); font-size: 12px; z-index: 9999; pointer-events: none; }
        `;
        doc.head.appendChild(style);

        // 3. 容器與調試面板
        const container = doc.createElement('div');
        container.className = 'ejcees';
        container.setAttribute('mode', 'main');
        doc.body.appendChild(container);

        // 在新窗口內增加一個可見的調試層
        const debugDiv = doc.createElement('div');
        debugDiv.id = 'debug-overlay';
        debugDiv.textContent = 'Ejcees Loading...';
        debugDiv.style.display = 'none';
        doc.body.appendChild(debugDiv);

        // 4. 外部腳本 (添加載入監控)
        const extScript = doc.createElement('script');
        extScript.src = scriptUrl;
        extScript.onload = () => console.log('新窗口: 外部 JS 載入成功');
        extScript.onerror = () => console.error('新窗口: 外部 JS 載入失敗,路徑可能錯誤');
        doc.body.appendChild(extScript);

        // 5. 初始化腳本 (增加 try-catch)
        const initScript = doc.createElement('script');
        initScript.textContent = `
            console.log('新窗口: 初始化腳本開始執行');
            const debug = document.getElementById('debug-overlay');
            
            window.addEventListener('load', () => {
                debug.textContent += ' | Window Loaded';
                try {
                    const el = document.querySelector('.ejcees');
                    const mode = el.getAttribute('mode');
                    console.log('新窗口: 正在初始化 Ejcees, Mode:', mode);
                    
                    if (typeof Ejcees !== 'undefined') {
                        new Ejcees(el, mode);
                        debug.textContent = 'Ejcees Initialized';
                        console.log('新窗口: Ejcees 實例化完成');
                    } else {
                        throw new Error('找不到 Ejcees 構造函數,請檢查 JS 是否正確導出');
                    }
                } catch (err) {
                    console.error('新窗口運行報錯:', err);
                    debug.style.color = 'red';
                    debug.textContent = 'Error: ' + err.message;
                }
            });
        `;
        doc.body.appendChild(initScript);

        // 6. 序列化並打開
        // 使用 XMLSerializer 時要確保 meta 閉合,或者改用 innerHTML 構建字符串
        const htmlString = `<!DOCTYPE html><html>${doc.documentElement.innerHTML}</html>`;
        console.log('3. 生成的 HTML 字符串長度:', htmlString.length);

        const blob = new Blob([htmlString], { type: 'text/html' });
        const url = URL.createObjectURL(blob);
        
        console.log('4. Blob URL 已生成:', url);
        const newWin = window.open(url, '_blank');
        
        if (!newWin) {
            console.warn('警告: 彈窗被瀏覽器攔截');
        }

    } catch (globalErr) {
        console.error('構建過程出錯:', globalErr);
    }
    
    console.groupEnd();
}
</script>
<button onclick="openEjceesWindow()">打開Ejcees單頁面</button>
<br><br>

以上代碼出錯:The character encoding of the document was not declared, so the encoding was guessed from content. The character encoding needs to be declared in the Content-Type HTTP header, using a meta tag, or using a byte order mark.

可能要加上以下兩個標籤或屬性:
<html lang="en-US">
<meta charset="UTF-8">

同時,去掉debug。

gemini fast:

代码: 全选

<script>
/**
 * 強化版:保持原有 DOM 構建邏輯,添加編碼聲明並移除調試功能
 */
function openEjceesWindow() {
    // 1. 獲取 WordPress 附件 URL
    const scriptUrl = "<?php echo wp_get_attachment_url(19088); ?>";

    if (!scriptUrl || scriptUrl.trim() === "") {
        return;
    }

    try {
        // 2. 構建文檔
        const doc = document.implementation.createHTMLDocument("Ejcees Object");
        
        // 設定 html 語言屬性
        doc.documentElement.setAttribute('lang', 'en-US');

        // 添加 Charset Meta (解決編碼警告關鍵)
        const metaCharset = doc.createElement('meta');
        metaCharset.setAttribute('charset', 'UTF-8');
        doc.head.appendChild(metaCharset);

        // Mobile Viewport
        const metaView = doc.createElement('meta');
        metaView.name = "viewport";
        metaView.content = "width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no";
        doc.head.appendChild(metaView);

        const style = doc.createElement('style');
        style.textContent = `
            body, html { margin: 0; padding: 0; width: 100%; height: 100%; overflow: hidden; background: #222; color: white; }
            .ejcees { width: 100vw; height: 100vh; display: block; }
        `;
        doc.head.appendChild(style);

        // 3. 容器
        const container = doc.createElement('div');
        container.className = 'ejcees';
        container.setAttribute('mode', 'main');
        doc.body.appendChild(container);

        // 4. 外部腳本
        const extScript = doc.createElement('script');
        extScript.src = scriptUrl;
        doc.body.appendChild(extScript);

        // 5. 初始化腳本 (移除 debug 邏輯)
        const initScript = doc.createElement('script');
        initScript.textContent = `
            window.addEventListener('load', () => {
                try {
                    const el = document.querySelector('.ejcees');
                    const mode = el.getAttribute('mode');
                    if (typeof Ejcees !== 'undefined') {
                        new Ejcees(el, mode);
                    }
                } catch (err) {
                    console.error('Initialization Error:', err);
                }
            });
        `;
        doc.body.appendChild(initScript);

        // 6. 序列化並打開
        // 確保包含 <!DOCTYPE html> 以避免觸發怪異模式 (Quirks Mode)
        const htmlString = `<!DOCTYPE html>\n${doc.documentElement.outerHTML}`;

        // 在 Blob 類型中明確指定 charset=utf-8
        const blob = new Blob([htmlString], { type: 'text/html;charset=utf-8' });
        const url = URL.createObjectURL(blob);
        
        const newWin = window.open(url, '_blank');
        
        if (!newWin) {
            console.warn('Pop-up blocked');
        }

    } catch (globalErr) {
        console.error('Process error:', globalErr);
    }
}
</script>

<button onclick="openEjceesWindow()">打開Ejcees單頁面</button>
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5443
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 176 次
被圈友点赞: 204 次
联系:

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

帖子 ejsoon »

const totalDepth = this.getPathDepth(this.historyFEN, this.currentBranch) - 1;
...
outputElement.innerHTML = div.crtStepIdxTxt + div.outputSlashTxt + div.totalDepthTxt;
...
按以下要求更改。

代码: 全选

將這段代碼:
const totalDepth = this.getPathDepth(this.historyFEN, this.currentBranch) - 1;
...
outputElement.innerHTML = div.crtStepIdxTxt + div.outputSlashTxt + div.totalDepthTxt;
...
按以下要求更改。

div.crtStepIdxTxt 、 div.outputSlash 、 div.totalDepthTxt這三個的display = inline-block。

div.crtStepIdxTxt.textContent = this.currentStepIndex
div.outputSlashTxt.textContent = '/';
div.totalDepthTxt.textContent = totalDepth

之後獲取到div.totalDepthTxt的寬度,然後將div.crtStepIdxTxt的寬度設為相同寬度且text-align = right。


gemini fast:

代码: 全选

// 1. 計算總深度
const totalDepth = this.getPathDepth(this.historyFEN, this.currentBranch) - 1;

// 2. 設置內容與樣式
// 假設 div.crtStepIdxTxt 等元素已經建立
const elements = [div.crtStepIdxTxt, div.outputSlashTxt, div.totalDepthTxt];

elements.forEach(el => {
    el.style.display = 'inline-block';
});

div.crtStepIdxTxt.textContent = this.currentStepIndex;
div.outputSlashTxt.textContent = '/';
div.totalDepthTxt.textContent = totalDepth;

// 3. 組合並渲染到頁面
// 注意:必須先 append 到 outputElement,否則 offsetWidth 會是 0
outputElement.innerHTML = ''; // 清空舊內容
outputElement.appendChild(div.crtStepIdxTxt);
outputElement.appendChild(div.outputSlashTxt);
outputElement.appendChild(div.totalDepthTxt);

// 4. 動態對齊寬度
// 獲取總步數的寬度
const targetWidth = div.totalDepthTxt.offsetWidth;

// 將當前步數的寬度設為相同,並靠右對齊
if (targetWidth > 0) {
    div.crtStepIdxTxt.style.width = targetWidth + 'px';
    div.crtStepIdxTxt.style.textAlign = 'right';
}
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5443
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 176 次
被圈友点赞: 204 次
联系:

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

帖子 ejsoon »

gemini fast:

代码: 全选

// 1. Calculate the total depth
const totalDepth = this.getPathDepth(this.historyFEN, this.currentBranch) - 1;

// 2. Create elements dynamically
const crtStepIdxTxt = document.createElement('div');
const outputSlashTxt = document.createElement('div');
const totalDepthTxt = document.createElement('div');

// 3. Set content and basic styles
crtStepIdxTxt.textContent = this.currentStepIndex;
outputSlashTxt.textContent = '/';
totalDepthTxt.textContent = totalDepth;

const elements = [crtStepIdxTxt, outputSlashTxt, totalDepthTxt];
elements.forEach(el => {
    el.style.display = 'inline-block';
});

// 4. Clear existing content and append new elements to the DOM
// The elements must be in the DOM to calculate their width
outputElement.innerHTML = ''; 
outputElement.appendChild(crtStepIdxTxt);
outputElement.appendChild(outputSlashTxt);
outputElement.appendChild(totalDepthTxt);

// 5. Sync widths for alignment
// Get the width of the total depth element
const targetWidth = totalDepthTxt.offsetWidth;

// Apply the same width to the current step index and align text to the right
if (targetWidth > 0) {
    crtStepIdxTxt.style.width = targetWidth + 'px';
    crtStepIdxTxt.style.textAlign = 'right';
}
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5443
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 176 次
被圈友点赞: 204 次
联系:

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

帖子 ejsoon »

ejceesobj202605031024

為currentStep固定寬度,把Review固定在最下方。
附件
ejceesobj202605031024.js
(309.34 KiB) 已下载 5 次
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5443
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 176 次
被圈友点赞: 204 次
联系:

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

帖子 ejsoon »

// btoa() requires a string where every character is a single byte.
// unescape(encodeURIComponent()) handles potential UTF-8 characters.
const base64Data = btoa(unescape(encodeURIComponent(svgOutput)));
聞unescape已棄用,那這段代碼應該怎麼改?
附件
ejceesobj202605031031.js
(309.33 KiB) 已下载 4 次
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5443
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 176 次
被圈友点赞: 204 次
联系:

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

帖子 ejsoon »

gemini fast我們做朋友吧!


代码: 全选

你提到的這段程式碼是非常經典的「舊式解決方案」,主要是用來處理 `btoa()` 不支援 Unicode(中文字符)的問題。

在現代 JavaScript 中,這種「先編碼再解碼成二進位字串」的技巧(`unescape(encodeURIComponent())`)雖然還能跑,但確實不夠優雅。

這裡有兩種現代的替代方案:

---

### 方案一:使用 `Uint8Array` 與 `TextEncoder` (最推薦)
這是目前 Web 標準中最正統的做法。我們不再透過字串轉換的歪腦筋,而是直接將字串轉為位元組陣列(Uint8Array),再轉換為 Base64。

```javascript
/**
 * 現代化解法:不依賴 unescape
 */
function svgToBase64(svgString) {
    // 1. 將字串編碼為 UTF-8 的 Uint8Array
    const encoder = new TextEncoder();
    const data = encoder.encode(svgString);
    
    // 2. 將 Uint8Array 轉為二進位字串(btoa 要求的格式)
    const binaryString = String.fromCharCode(...data);
    
    // 3. 執行 Base64 編碼
    return btoa(binaryString);
}

const base64Data = svgToBase64(svgOutput);
```

---

### 方案二:使用 `Blob` 與 `FileReader` (非同步)
如果你在處理極大的 SVG,或者希望程式碼更簡潔且不擔心「非同步」操作,可以使用 `FileReader`。

```javascript
// 注意:這是非同步操作
const blob = new Blob([svgOutput], { type: 'image/svg+xml;charset=utf-8' });
const reader = new FileReader();

reader.onload = () => {
    const base64Data = reader.result.split(',')[1]; // 取得 Base64 部分
    console.log(base64Data);
};

reader.readAsDataURL(blob);
```

---

### 為什麼原本的程式碼要這樣寫?

為了讓你了解修改的邏輯,這裡快速拆解原本那行程式碼的「黑科技」:

1.  **`encodeURIComponent(svgOutput)`**: 將中文字轉為 `%E4%BD%A0` 這種格式(UTF-8 轉義)。
2.  **`unescape(...)`**: 這是最關鍵的步驟。它會把 `%E4%BD%A0` 視為單個位元組重新解碼。這會產生一串「表面上看起來是亂碼,但每個字元都嚴格佔據 1 Byte」的字串。
3.  **`btoa(...)`**: 因為字串現在每個字元都是 1 Byte 了,`btoa` 就不會噴錯。

**總結:**
如果你想追求程式碼的維護性與標準化,請採用 **方案一**。它完全捨棄了 `unescape`,且執行效率在處理小到中型 SVG 時非常優秀。

這段 SVG 是要用在 CSS 的 `background-image` 還是要提供使用者下載呢?
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5443
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 176 次
被圈友点赞: 204 次
联系:

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

帖子 ejsoon »

在 constructor(container, mode) {...}中的this.injectCSS()之前,增加一個對mode === 'svg'的判定。

代码: 全选

在  constructor(container, mode) {...}中的this.injectCSS()之前,增加一個對mode === 'svg'的判定。
如果mode === 'svg':

1.執行這個操作:
{
      this.loadFEN(this.INITIAL_FEN);
      this.historyFEN = {
        fen: this.INITIAL_FEN,
        move: null,
        r: 0,
        f: 0,
        c: '',
        v: []
      };
      this.currentBranch = [];
      this.currentStepIndex = 0;
      this.stepSlider.max = 0;
      this.stepSlider.value = 0;
      this.applyLastMoveVisuals(null);
      this.highlightActiveStep(0);
      if (this.initialTextContent) {
        this.importExportedText(this.initialTextContent);
      }
    }
this.container還可能會有「interval」和「speed」兩個屬性,將其賦值給this.moveInterval和this.moveSpeed。

2.把this.container.querySelector('#tool-exp-svg').addEventListener('click', () => {...}打包成一個函式,之後調用這個函式,獲得一個svg圖檔。

3.將此svg用以下方法轉為img[src=base64],img的style.width將為100%,style.height=auto。
function svgToBase64(svgString) {
    const encoder = new TextEncoder();
    const data = encoder.encode(svgString);
    const binaryString = String.fromCharCode(...data);
    return btoa(binaryString);
  }
      const base64Data = svgToBase64(svgOutput);
      const imgSrc = `data:image/svg+xml;base64,${base64Data}`;

4.把this.container的innerHTML清空後,appendChild(這個img)。

5.return;停止後續步驟。

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

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

帖子 ejsoon »

gemini pro:

代码: 全选

Here are the modifications you need to make to the `constructor` of the `Ejcees` class. 

First, you need to extract the SVG generation logic from your `#tool-exp-svg` click listener into a standalone class method so it can be reused.

### 1. Refactor the SVG generation into a new method
Add this new method anywhere inside your `Ejcees` class:

```javascript
  // Refactored method from the '#tool-exp-svg' click listener
  generateSVGString() {
    // ... insert the code from your original addEventListener('click', () => {...}) here ...
    // Make sure this method returns the final SVG string (e.g., return svgString;)
  }
```

### 2. Modify the Constructor
Insert the following code inside `constructor(container, mode) { ... }` right before `this.injectCSS();`[cite: 1].

```javascript
    // ... existing code (this.NotationConverter = { ... };)

    // Insert the 'svg' mode check before injecting CSS
    if (this.mode === 'svg') {
      // 1. Execute initial operations
      this.loadFEN(this.INITIAL_FEN);
      this.historyFEN = {
        fen: this.INITIAL_FEN,
        move: null,
        r: 0,
        f: 0,
        c: '',
        v: []
      };
      this.currentBranch = [];
      this.currentStepIndex = 0;
      this.stepSlider.max = 0;
      this.stepSlider.value = 0;
      this.applyLastMoveVisuals(null);
      this.highlightActiveStep(0);
      if (this.initialTextContent) {
        this.importExportedText(this.initialTextContent);
      }

      // Check for 'interval' and 'speed' attributes on the container and assign them
      const containerInterval = this.container.getAttribute('interval');
      if (containerInterval !== null) {
        this.moveInterval = parseFloat(containerInterval);
      }
      
      const containerSpeed = this.container.getAttribute('speed');
      if (containerSpeed !== null) {
        this.moveSpeed = parseFloat(containerSpeed);
      }

      // 2. Call the refactored function to get the SVG output string
      const svgOutput = this.generateSVGString();

      // 3. Convert SVG string to Base64 image
      const svgToBase64 = function(svgString) {
        const encoder = new TextEncoder();
        const data = encoder.encode(svgString);
        const binaryString = String.fromCharCode(...data);
        return btoa(binaryString);
      };
      
      const base64Data = svgToBase64(svgOutput);
      const imgSrc = `data:image/svg+xml;base64,${base64Data}`;

      const img = document.createElement('img');
      img.src = imgSrc;
      img.style.width = '100%';
      img.style.height = 'auto';

      // 4. Clear the container's innerHTML and append the image
      this.container.innerHTML = '';
      this.container.appendChild(img);

      // 5. Stop subsequent steps
      return;
    }

    this.injectCSS(); //[cite: 1]
    this.renderDOM(); //[cite: 1]
    // ... rest of the constructor
```

檢查一下,如果沒有ui,導出svg的一些地方可能無法執行,可能需要先生成ui,在獲得svg後再將其清空。

代码: 全选

你說得非常對,這是一個很敏銳的發現!如果 `generateSVGString()` 或 `loadFEN()` 等方法依賴於 DOM 元素(例如棋盤節點或棋子),在 UI 尚未生成時調用它們確實會導致報錯。

我們需要調整邏輯順序:**先生成 UI,設置好狀態並匯出 SVG 後,再將容器清空並替換為圖片。**

請將 `if (this.mode === 'svg') { ... }` 的區塊移到 `this.injectCSS();` 和 `this.renderDOM();` **之後**。

以下是修改後的位置與代碼結構:

```javascript
    // ... [Previous variable initializations in constructor] ...

    // 1. Generate the UI first so DOM elements exist for state updates and SVG export
    this.injectCSS();
    this.renderDOM();

    // 2. Check for 'svg' mode after the UI is ready
    if (this.mode === 'svg') {
      this.loadFEN(this.INITIAL_FEN);
      this.historyFEN = {
        fen: this.INITIAL_FEN,
        move: null,
        r: 0,
        f: 0,
        c: '',
        v: []
      };
      this.currentBranch = [];
      this.currentStepIndex = 0;
      this.stepSlider.max = 0;
      this.stepSlider.value = 0;
      this.applyLastMoveVisuals(null);
      this.highlightActiveStep(0);
      
      if (this.initialTextContent) {
        this.importExportedText(this.initialTextContent);
      }

      // Check for 'interval' and 'speed' attributes
      const containerInterval = this.container.getAttribute('interval');
      if (containerInterval !== null) {
        this.moveInterval = parseFloat(containerInterval);
      }
      
      const containerSpeed = this.container.getAttribute('speed');
      if (containerSpeed !== null) {
        this.moveSpeed = parseFloat(containerSpeed);
      }

      // 3. DOM is fully rendered and state is set, safe to generate SVG
      const svgOutput = this.generateSVGString();

      // 4. Convert SVG string to Base64 image
      const svgToBase64 = function(svgString) {
        const encoder = new TextEncoder();
        const data = encoder.encode(svgString);
        const binaryString = String.fromCharCode(...data);
        return btoa(binaryString);
      };
      
      const base64Data = svgToBase64(svgOutput);
      const imgSrc = `data:image/svg+xml;base64,${base64Data}`;

      const img = document.createElement('img');
      img.src = imgSrc;
      img.style.width = '100%';
      img.style.height = 'auto';

      // 5. Clear the temporary UI and append the generated image
      this.container.innerHTML = '';
      this.container.appendChild(img);

      // 6. Stop subsequent steps (like binding interactive events)
      return;
    }

    // ... [Rest of the constructor (e.g., this.initEvents() or other normal mode setups)] ...
```
https://ejsoon.vip/
弈趣極光:享受思維樂趣
回复

在线用户

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