代码: 全选
以下是針對您要求的功能所需要修改的部分代碼。請將它們對應替換到原有的 HTML 文件中。
### 1. 新增 CSS 樣式
在 `<style>` 區塊的最末尾(`</style>` 之前)加入 PGN 設置彈出窗口及按鈕的樣式:
```css
/* --- PGN Meta Modal Styles --- */
.pgn-settings-btn {
position: absolute;
top: 8px;
right: 8px;
width: 24px;
height: 24px;
background-color: #555;
border-radius: 4px;
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
z-index: 10;
opacity: 0.8;
}
.pgn-settings-btn:hover { opacity: 1; background-color: #666; }
.pgn-settings-btn svg { width: 16px; height: 16px; fill: #fff; }
.pgn-meta-modal {
position: fixed;
top: 0; left: 0; width: 100vw; height: 100vh;
background-color: rgba(255, 255, 255, 0.85);
z-index: 888;
display: flex;
justify-content: center;
align-items: center;
}
.pgn-meta-box {
background: #333; color: #fff;
padding: 20px; border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
width: 90vw; max-width: 500px;
max-height: 90vh; overflow-y: auto;
position: relative;
}
.pgn-meta-table { width: 100%; border-collapse: collapse; margin-top: 15px; font-size: 14px; }
.pgn-meta-table th, .pgn-meta-table td { border: 1px solid #555; padding: 6px; text-align: left; }
.pgn-meta-input { width: 100%; background: #222; color: #fff; border: 1px solid #444; outline: none; padding: 4px; box-sizing: border-box; }
.pgn-meta-highlight { color: #ffeb3b; font-weight: bold; }
.pgn-meta-row.selected { background-color: #555; }
.pgn-add-btn {
margin-top: 10px; cursor: pointer; padding: 6px;
background: #4caf50; color: #fff; text-align: center; font-weight: bold; border-radius: 4px;
}
.pgn-meta-trash { cursor: pointer; text-align: center; }
.pgn-meta-trash:hover { background-color: #f44336; }
```
### 2. 全局變量與 PGN 邏輯初始化
在 `<script>` 的頂部(大約在 `let formatOddStates = { cn: 0, en: 0, engine: 0, url: 0 };` 附近),修改為 `pgn` 並加入 metadata 狀態陣列:
```javascript
let formatOddStates = {
cn: 0,
pgn: 0,
engine: 0,
url: 0
};
let 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: "ECCO", value: "" },
{ name: "Annotator", value: "" },
{ name: "Termination", value: "" }
];
let pgnSelectedRowIndex = -1;
```
### 3. 加入 PGN 設置彈窗及 PGN 文本生成邏輯
將以下方法添加到 `<script>` 中(可以放在 `generateExportText` 或 `generateDPFormat` 的下方):
```javascript
function showPGNMetaModal() {
const modal = document.createElement('div');
modal.className = 'pgn-meta-modal';
const box = document.createElement('div');
box.className = 'pgn-meta-box';
const closeBtn = document.createElement('button');
closeBtn.className = 'close-btn';
closeBtn.style.top = '10px'; closeBtn.style.right = '10px';
closeBtn.onclick = () => document.body.removeChild(modal);
const title = document.createElement('h3');
title.textContent = 'PGN Metadata';
const table = document.createElement('table');
table.className = 'pgn-meta-table';
const thead = document.createElement('thead');
thead.innerHTML = `<tr><th>No.</th><th>Name</th><th>Value</th><th class="pgn-meta-trash" id="pgn-clear-all" title="Clear All">🗑️</th></tr>`;
const tbody = document.createElement('tbody');
table.appendChild(thead);
table.appendChild(tbody);
function renderTable() {
tbody.innerHTML = '';
const highlights = ["Event", "Site", "Date", "Round", "Red", "Black", "Result"];
pgnMetadata.forEach((item, index) => {
const tr = document.createElement('tr');
tr.className = 'pgn-meta-row';
if (pgnSelectedRowIndex === index) tr.classList.add('selected');
const tdNum = document.createElement('td');
tdNum.textContent = index + 1;
tdNum.style.cursor = 'pointer';
tdNum.onclick = () => {
if (pgnSelectedRowIndex === -1) {
pgnSelectedRowIndex = index;
} else if (pgnSelectedRowIndex === index) {
pgnSelectedRowIndex = -1;
} else {
const movedItem = pgnMetadata.splice(pgnSelectedRowIndex, 1)[0];
pgnMetadata.splice(index, 0, movedItem);
pgnSelectedRowIndex = -1;
}
renderTable();
};
tr.appendChild(tdNum);
const tdName = document.createElement('td');
const nameInput = document.createElement('input');
nameInput.className = 'pgn-meta-input';
if (highlights.includes(item.name)) nameInput.classList.add('pgn-meta-highlight');
nameInput.value = item.name;
nameInput.onchange = (e) => item.name = e.target.value;
tdName.appendChild(nameInput);
tr.appendChild(tdName);
const tdValue = document.createElement('td');
if (item.name === "Result") {
const sel = document.createElement('select');
sel.className = 'pgn-meta-input';
["*", "1-0", "0-1", "1/2-1/2"].forEach(opt => {
const option = document.createElement('option');
option.value = opt; option.textContent = opt;
if (opt === item.value) option.selected = true;
sel.appendChild(option);
});
sel.onchange = (e) => item.value = e.target.value;
tdValue.appendChild(sel);
} else {
const valInput = document.createElement('input');
valInput.className = 'pgn-meta-input';
valInput.value = item.value;
valInput.onchange = (e) => item.value = e.target.value;
tdValue.appendChild(valInput);
}
tr.appendChild(tdValue);
const tdDel = document.createElement('td');
tdDel.className = 'pgn-meta-trash';
tdDel.textContent = '🗑️';
tdDel.onclick = () => {
pgnMetadata.splice(index, 1);
if (pgnSelectedRowIndex === index) pgnSelectedRowIndex = -1;
else if (pgnSelectedRowIndex > index) pgnSelectedRowIndex--;
renderTable();
};
tr.appendChild(tdDel);
tbody.appendChild(tr);
});
}
renderTable();
thead.querySelector('#pgn-clear-all').onclick = () => {
pgnMetadata.forEach(item => {
if (item.name === "Result") item.value = "*";
else item.value = "";
});
renderTable();
};
const addBtn = document.createElement('div');
addBtn.className = 'pgn-add-btn';
addBtn.textContent = '+';
addBtn.onclick = () => {
pgnMetadata.push({ name: "", value: "" });
renderTable();
};
box.appendChild(closeBtn);
box.appendChild(title);
box.appendChild(table);
box.appendChild(addBtn);
modal.appendChild(box);
document.body.appendChild(modal);
}
function generatePGNFormat(oddState) {
let text = '[Game "Chinese Chess"]\n';
pgnMetadata.forEach(meta => {
if (meta.value && meta.value.trim() !== '') {
text += `[${meta.name} "${meta.value}"]\n`;
}
});
const initFenPart = INITIAL_FEN.split(' ')[0];
if (historyFEN.fen.split(' ')[0] !== initFenPart) {
let fParts = historyFEN.fen.split(' ').slice(0, 6);
text += `[FEN "${fParts.join(' ')}"]\n`;
}
const formatMap = { 0: "Chinese", 1: "ICCS", 2: "WXF" };
text += `[Format "${formatMap[oddState]}"]\n\n`;
if (historyFEN.c && historyFEN.c.trim() !== '') {
text += `{${historyFEN.c.trim()}}\n`;
}
let autoResult = "*";
let resultMeta = pgnMetadata.find(m => m.name === "Result");
if (resultMeta && resultMeta.value !== "*") {
autoResult = resultMeta.value;
} else {
let lastNode = historyFEN;
while(lastNode.v && lastNode.v.length > 0) lastNode = lastNode.v[0];
let vMap = buildVirtualMap(lastNode.fen);
let hasAttack = false;
for (let id of vMap.values()) {
if (['R','N','C','P','r','n','c','p'].includes(id[0])) { hasAttack = true; break; }
}
if (!hasAttack) {
autoResult = "1/2-1/2";
} else {
let turnNext = lastNode.fen.split(' ')[1] === 'w';
let moves = getAllLegalMoves(turnNext, vMap);
if (moves.length === 0) autoResult = turnNext ? "0-1" : "1-0";
}
}
function getMoveStr(node, isRed) {
if (oddState === 0) return NotationConverter.toChinese(node.move);
if (oddState === 2) return node.move;
if (oddState === 1 && node.lastMove) {
let dc = node.lastMove;
return String.fromCharCode(65 + dc.startX) + (9 - dc.startY) + String.fromCharCode(65 + dc.endX) + (9 - dc.endY);
}
return node.move;
}
function processPGNSequence(startNode, moveNum, isRed) {
let seqText = '';
let curr = startNode;
while (curr.v && curr.v.length > 0) {
let nextNode = curr.v[0];
let moveStr = getMoveStr(nextNode, isRed);
let currentMoveText = '';
if (isRed) {
currentMoveText = `${moveNum}. ${moveStr}`;
} else {
if (curr === startNode && !isRed) currentMoveText = `${moveNum}. ... ${moveStr}`;
else currentMoveText = `${moveStr}`;
}
seqText += (seqText.length > 0 && !seqText.endsWith('\n') && !isRed ? ' ' : '') + currentMoveText;
if (nextNode.c && nextNode.c.trim() !== '') {
seqText += `\n{${nextNode.c.trim()}}\n`;
}
if (curr.v.length > 1) {
for (let i = 1; i < curr.v.length; i++) {
let bNode = curr.v[i];
let bMoveStr = getMoveStr(bNode, isRed);
let bStartText = isRed ? `${moveNum}. ${bMoveStr}` : `${moveNum}. ... ${bMoveStr}`;
let innerText = bStartText;
if (bNode.c && bNode.c.trim() !== '') innerText += `\n{${bNode.c.trim()}}`;
let subBranch = processPGNSequence(bNode, isRed ? moveNum : moveNum + 1, !isRed);
if (subBranch) innerText += (innerText.endsWith('}') ? '\n' : ' ') + subBranch;
seqText += ` (${innerText.trim()})`;
}
if (!seqText.endsWith('\n')) seqText += '\n';
}
curr = nextNode;
if (!isRed) moveNum++;
isRed = !isRed;
}
return seqText.trim();
}
let startIsRed = historyFEN.fen.split(' ')[1] === 'w';
let initMoveNum = parseInt(historyFEN.fen.split(' ')[5], 10) || 1;
let bodyText = processPGNSequence(historyFEN, initMoveNum, startIsRed);
text += bodyText;
if (!text.endsWith('\n')) text += '\n';
text += autoResult;
return text;
}
```
### 4. 將設置按鈕加入 `renderRecordUI` 內
尋找 `function renderRecordUI()` 並在其中清空內容並加入起始 div 的地方加上生成齒輪按鈕的代碼:
```javascript
function renderRecordUI() {
const existingMenu = document.querySelector('.branch-menu');
if (existingMenu) existingMenu.remove();
const recordContainer = document.querySelector('.ejceesrecord');
recordContainer.innerHTML = '';
// 添加 PGN Metadata 設置按鈕
const settingsBtn = document.createElement('div');
settingsBtn.className = 'pgn-settings-btn';
settingsBtn.title = 'PGN Metadata';
settingsBtn.innerHTML = `<svg viewBox="0 0 24 24"><path d="M19.14,12.94c0.04-0.3,0.06-0.61,0.06-0.94c0-0.32-0.02-0.64-0.06-0.94l2.03-1.58c0.18-0.14,0.23-0.41,0.12-0.61 l-1.92-3.32c-0.12-0.22-0.37-0.29-0.59-0.22l-2.39,0.96c-0.5-0.38-1.03-0.7-1.62-0.94L14.4,2.81c-0.04-0.24-0.24-0.41-0.48-0.41 h-3.84c-0.24,0-0.43,0.17-0.47,0.41L9.25,5.35C8.66,5.59,8.12,5.92,7.63,6.29L5.24,5.33c-0.22-0.08-0.47,0-0.59,0.22L2.73,8.87 C2.62,9.08,2.66,9.34,2.86,9.48l2.03,1.58C4.84,11.36,4.8,11.69,4.8,12s0.02,0.64,0.06,0.94l-2.03,1.58 c-0.18,0.14-0.23,0.41-0.12,0.61l1.92,3.32c0.12,0.22,0.37,0.29,0.59,0.22l2.39-0.96c0.5,0.38,1.03,0.7,1.62,0.94l0.36,2.54 c0.05,0.24,0.24,0.41,0.48,0.41h3.84c0.24,0,0.43-0.17,0.47-0.41l0.36-2.54c0.59-0.24,1.13-0.56,1.62-0.94l2.39,0.96 c0.22,0.08,0.47,0,0.59-0.22l1.92-3.32c0.12-0.22,0.07-0.49-0.12-0.61L19.14,12.94z M12,15.6c-1.98,0-3.6-1.62-3.6-3.6 s1.62-3.6,3.6-3.6s3.6,1.62,3.6,3.6S13.98,15.6,12,15.6z"/></svg>`;
settingsBtn.onclick = showPGNMetaModal;
recordContainer.appendChild(settingsBtn);
const startDiv = document.createElement('div');
startDiv.id = 'record-start';
//... 接後面的程式碼 startDiv.classList.add('ejceesrcdstart');
```
### 5. 替換匯出文本邏輯 `renderExportTextUI` 與全局複製
請在 `renderExportTextUI` 函數中將原有的 `"en"` 及其按鈕完全替換為 `"pgn"`:
```javascript
// --- Refactored UI for Export Text ---
function renderExportTextUI() {
const recordContainer = document.querySelector('.ejceesrecord');
const commentDiv = document.querySelector('.ejceescomment');
const btnDiv = document.querySelector('.ejceestextbtn');
const cnText = generateExportText(false);
const cnTextWidthComment = generateTextWithComment(false);
const cnTextWithBranches = generateDPFormat(false);
const pgnTextCh = generatePGNFormat(0);
const pgnTextICCS = generatePGNFormat(1);
const pgnTextWXF = generatePGNFormat(2);
const urlText = generateUrlHash(false);
const urlTextToggled = generateUrlHash(true);
// ... (Engine Text Generation 保留原樣) ...
recordContainer.innerHTML = `<textarea class="ejceescomment-edit" id="export-textarea" style="width:100%; height:100%; resize:none; border:none; outline:none; background:#2a2a2a; color:#fff; padding:8px; font-family:monospace; font-size:14px; white-space:pre-wrap; overflow:auto;"></textarea>`;
const textarea = document.getElementById('export-textarea');
// 更新介面選單
commentDiv.innerHTML = `<div class="exp-fmt-outer">
<div class="exp-fmt-btn ${lastExportFormat === 'cn' ? 'active' : ''}" data-fmt="cn" data-odd="${
formatOddStates['cn']
}">DP Format</div>
<div class="exp-fmt-btn ${lastExportFormat === 'pgn' ? 'active' : ''}" data-fmt="pgn" data-odd="${
formatOddStates['pgn']
}">PGN Format</div>
<div class="exp-fmt-btn ${lastExportFormat === 'engine' ? 'active' : ''}" data-fmt="engine" data-odd="${
formatOddStates['engine']
}">Engine Format</div>
<div class="exp-fmt-btn ${lastExportFormat === 'url' ? 'active' : ''}" data-fmt="url" data-odd="${
formatOddStates['url']
}">URL Parameter</div>
</div>`;
const formatBtns = commentDiv.querySelectorAll('.exp-fmt-btn');
const updateTextarea = (fmt) => {
if (fmt === 'cn') {
if (formatOddStates[fmt] === 2) {
textarea.value = cnTextWithBranches;
} else if (formatOddStates[fmt] === 1) {
textarea.value = cnTextWidthComment;
} else {
textarea.value = cnText;
}
} else if (fmt === 'pgn') {
if (formatOddStates[fmt] === 2) {
textarea.value = pgnTextWXF;
} else if (formatOddStates[fmt] === 1) {
textarea.value = pgnTextICCS;
} else {
textarea.value = pgnTextCh;
}
} else if (fmt === 'engine') {
// ... 原來的 Engine 邏輯
} else if (fmt === 'url') {
textarea.value = formatOddStates[fmt] === 1 ? urlTextToggled : urlText;
}
setTimeout(() => textarea.select(), 12);
};
formatBtns.forEach((btn) => {
btn.addEventListener('click', () => {
const wasActive = btn.classList.contains('active');
const fmt = btn.getAttribute('data-fmt');
lastExportFormat = fmt;
formatBtns.forEach((b) => b.classList.remove('active'));
btn.classList.add('active');
if (wasActive) {
if (fmt === 'cn' || fmt === 'pgn') {
formatOddStates[fmt] = (formatOddStates[fmt] + 1) % 3;
} else {
formatOddStates[fmt] = formatOddStates[fmt] === 1 ? 0 : 1;
}
}
btn.setAttribute('data-odd', formatOddStates[fmt]);
updateTextarea(fmt);
textarea.blur();
setTimeout(() => { textarea.focus(); textarea.select(); }, 12);
});
});
updateTextarea(lastExportFormat);
// ... btnDiv 設置 (原有 btn-confirm, btn-copy 等保留)
```
並對應修改全域 `performCopy` 的邏輯以適應 `pgn`:
```javascript
// Global functions for copy and paste
function performCopy() {
let textToCopy = '';
const fmt = lastExportFormat || 'cn';
const odd = formatOddStates[fmt] || 0;
if (fmt === 'cn') {
if (odd === 2) textToCopy = generateDPFormat(false);
else if (odd === 1) textToCopy = generateTextWithComment(false);
else textToCopy = generateExportText(false);
} else if (fmt === 'pgn') {
textToCopy = generatePGNFormat(odd);
} else if (fmt === 'engine') {
// ... 原有 engine copy 邏輯 ...
```