255 lines
19 KiB
HTML
255 lines
19 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>L1-S2-U21-L1 寻人启事 - 组件配置</title>
|
||
<style>
|
||
*{margin:0;padding:0;box-sizing:border-box}
|
||
body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;background:#f0f2f5;color:#1a1a2e;padding:24px}
|
||
.toolbar{position:sticky;top:0;z-index:100;background:#fff;border-radius:10px;padding:16px 20px;margin-bottom:20px;box-shadow:0 2px 8px rgba(0,0,0,.1);display:flex;align-items:center;gap:16px;flex-wrap:wrap}
|
||
.toolbar h1{font-size:20px;flex:1}
|
||
.btn{padding:10px 20px;border:none;border-radius:8px;font-size:14px;font-weight:600;cursor:pointer;transition:all .2s}
|
||
.btn-push{background:#0984e3;color:#fff}
|
||
.btn-push:hover{background:#0773c5}
|
||
.btn-push:disabled{background:#b2bec3;cursor:not-allowed}
|
||
.btn-refresh{background:#00b894;color:#fff}
|
||
.btn-refresh:hover{background:#00a381}
|
||
.status{font-size:13px;padding:6px 12px;border-radius:6px}
|
||
.status-ok{background:#d4edda;color:#155724}
|
||
.status-err{background:#f8d7da;color:#721c24}
|
||
.status-info{background:#d1ecf1;color:#0c5460}
|
||
.summary{background:#fff;border-radius:10px;padding:16px 20px;margin-bottom:20px;box-shadow:0 1px 3px rgba(0,0,0,.06);display:flex;gap:24px;flex-wrap:wrap}
|
||
.summary-item{text-align:center}
|
||
.summary-num{font-size:28px;font-weight:700;color:#0984e3}
|
||
.summary-label{font-size:12px;color:#888}
|
||
.card{background:#fff;border-radius:10px;padding:20px;margin-bottom:14px;box-shadow:0 1px 3px rgba(0,0,0,.06);border-left:4px solid #0984e3;transition:border-color .3s}
|
||
.card.modified{border-left-color:#f39c12}
|
||
.card-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;flex-wrap:wrap;gap:8px}
|
||
.card-type{font-weight:700;font-size:15px;color:#0984e3}
|
||
.card-meta{font-size:12px;color:#888}
|
||
.card-body{font-size:13px;line-height:1.8}
|
||
.field{margin-bottom:4px}
|
||
.field-label{font-weight:600;color:#555}
|
||
.field-value{color:#333}
|
||
.config-textarea{width:100%;min-height:120px;background:#f8f9fb;border:1px solid #e8eaed;border-radius:6px;padding:12px;font-family:"SF Mono",Menlo,Consolas,monospace;font-size:12px;line-height:1.7;resize:vertical;margin-top:8px;color:#2d3436}
|
||
.config-textarea:focus{outline:none;border-color:#0984e3;box-shadow:0 0 0 3px rgba(9,132,227,.1)}
|
||
.tag{display:inline-block;background:#e8eaed;color:#666;padding:2px 8px;border-radius:4px;font-size:11px;margin-right:4px}
|
||
.tag-k{background:#fff3cd;color:#b8860b}
|
||
.tag-id{background:#d1ecf1;color:#0c5460}
|
||
.modal{display:none;position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,.5);z-index:1000;justify-content:center;align-items:center}
|
||
.modal.show{display:flex}
|
||
.modal-content{background:#fff;border-radius:12px;padding:24px;max-width:500px;width:90%;max-height:80vh;overflow-y:auto}
|
||
.modal h3{margin-bottom:12px}
|
||
.modal pre{background:#2d3436;color:#dfe6e9;padding:12px;border-radius:8px;font-size:12px;overflow-x:auto;white-space:pre-wrap;max-height:300px;overflow-y:auto}
|
||
.modal .btn{margin-top:12px}
|
||
.progress{display:none;margin-top:8px;font-size:12px;color:#888}
|
||
.progress.show{display:block}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="toolbar">
|
||
<h1>L1-S2-U21-L1 寻人启事</h1>
|
||
<span id="status" class="status status-info">共 16 个组件</span>
|
||
<button class="btn btn-push" id="btnPush" onclick="pushToFeishu()">📤 推送到飞书</button>
|
||
<button class="btn btn-refresh" onclick="location.reload()">🔄 刷新</button>
|
||
<div class="progress" id="progress"></div>
|
||
</div>
|
||
<div class="summary">
|
||
<div class="summary-item"><div class="summary-num" id="totalCount">16</div><div class="summary-label">组件总数</div></div>
|
||
<div class="summary-item"><div class="summary-num" id="modifiedCount">0</div><div class="summary-label">已修改</div></div>
|
||
</div>
|
||
<div id="cards"></div>
|
||
|
||
<div class="modal" id="modal">
|
||
<div class="modal-content">
|
||
<h3 id="modalTitle">推送结果</h3>
|
||
<pre id="modalBody"></pre>
|
||
<button class="btn btn-push" onclick="document.getElementById('modal').classList.remove('show')">关闭</button>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
const SPREADSHEET_TOKEN = "Qv55sAjZHhzF3ttnSJQc2sfMn8f";
|
||
const SHEET_ID = "wMQVyV";
|
||
const COMPONENTS = [{"row": 31, "type": "图片单选", "id": "1221101", "config_info": "S1主线", "role": "Sally", "en_text": "Logi wears a blue baseball cap.", "config": "【任务标题】选择棒球帽\n\n【情境引入】\nSally: Logi wears a blue baseball cap.\n\n【互动内容】\n题干:Choose Logi's $baseball cap$.\n选项:\n00\n正确:00\n\n【互动反馈】\n正确:无\n错误:Sally: Look again. Is that a blue baseball cap?\n\n【后置对话】\n无", "knowledge": "baseball cap", "desc": ""}, {"row": 37, "type": "图片单选", "id": "1221102", "config_info": "S2主线", "role": "Sally", "en_text": "He wears glasses.", "config": "【任务标题】选择眼镜\n\n【情境引入】\nSally: Logi wears glasses.\n\n【互动内容】\n题干:Choose Logi's $glasses$.\n选项:\n00\n正确:00\n\n【互动反馈】\n正确:无\n错误:Sally: Hmm... Those are not his glasses.\n\n【后置对话】\n无", "knowledge": "glasses", "desc": ""}, {"row": 43, "type": "图片单选", "id": "1221103", "config_info": "S3主线", "role": "Sally", "en_text": "And white shoes.", "config": "【任务标题】选择鞋子\n\n【情境引入】\n无\n\n【互动内容】\n题干:Choose Logi's $shoes$.\n选项:\n00\n正确:00\n\n【互动反馈】\n正确:Sally: Got it?\n错误:Sally: Are those white shoes?\n\n【后置对话】\nBen: Got it!", "knowledge": "shoe", "desc": ""}, {"row": 58, "type": "对话朗读-配图", "id": "1221104", "config_info": "S4主线", "role": "User", "en_text": "No. He wears a blue cap.", "config": "【任务标题】辨认棒球帽\n【资源配置】\n图片时机:情境引入 互动内容\n\n【情境引入】\nUser: Hmm... No.\n\n【互动内容】\nUser: He wears a blue cap.(朗读)\n\n\n【后置对话】\n无", "knowledge": "wear", "desc": ""}, {"row": 71, "type": "对话朗读", "id": "1221105", "config_info": "S5主线", "role": "User", "en_text": "Is it a baseball cap?", "config": "【任务标题】确认棒球帽\n【资源配置】\n图片时机:情境引入 互动内容\n\n【情境引入】\nVicky: I found a blue hat!\n\n【互动内容】\nUser: Is it a baseball cap?(朗读)\n\n\n【后置对话】\n无", "knowledge": "baseball cap", "desc": ""}, {"row": 80, "type": "对话挖空-配图", "id": "1221106", "config_info": "S6主线", "role": "User", "en_text": "This is not Logi's cap.", "config": "【任务标题】辨认帽子\n【资源配置】\n【资源配置】\n图片时机:情境引入 互动内容 互动反馈\n\n【情境引入】\nUser: You're right. This is not Logi's cap.\n\n【互动内容】\n题目:He ___ a baseball cap.\n选项1:wears(正确)\n选项2:bears\n\n【互动反馈】\n正确:无\n错误:Vicky: No bears here!\n\n【后置对话】\n空", "knowledge": "... wear(s)...", "desc": ""}, {"row": 90, "type": "图片单选", "id": "1221107", "config_info": "S7主线", "role": "", "en_text": "", "config": "【任务标题】找到棒球帽\n\n\n【情境引入】\n无\n\n【互动内容】\n题干:Find the $baseball cap$.\n选项:\n00\n01\n02\n正确:00\n\n【互动反馈】\n正确:无\n错误:Ben: Is that really a baseball cap?\n\n【后置对话】\n无", "knowledge": "baseball cap", "desc": "【教研图】蓝色棒球帽挂在树枝上;点击图片上的Baseball cap"}, {"row": 107, "type": "对话挖空-配图", "id": "1221108", "config_info": "S8主线", "role": "User", "en_text": "Logi wears glasses, not sunglasses.", "config": "【任务标题】辨认眼镜\n【资源配置】\n图片时机:互动内容 互动反馈\n\n【情境引入】\n无\n\n【互动内容】\n题目:Logi wears ___, not sunglasses.\n选项1:glasses(正确)\n选项2:grass\n\n【互动反馈】\n正确:无\n错误:Vicky: Logi can't wear grass!\n\n【后置对话】\n无", "knowledge": "glasses", "desc": "【教研图】黑色墨镜和Logi的照片的对比图"}, {"row": 113, "type": "对话朗读", "id": "1221109", "config_info": "S9主线", "role": "User", "en_text": "These are not his glasses.", "config": "【任务标题】辨认眼镜\n【资源配置】\n图片时机:互动内容\n\n【情境引入】\n无\n\n【互动内容】\nUser: These are not his glasses.(朗读)\n\n【后置对话】\n无", "knowledge": "This is / These are not...", "desc": ""}, {"row": 132, "type": "对话朗读-配图", "id": "1221110", "config_info": "S10主线", "role": "User", "en_text": "A shoe!", "config": "【任务标题】辨认鞋子\n【资源配置】\n图片时机:互动内容\n\n【情境引入】\n无\n\n【互动内容】\nUser: A shoe!(朗读)\n\n\n【后置对话】\nVicky: But Logi wears white shoes.", "knowledge": "shoe", "desc": "【教研图】棕色鞋子和Logi的照片的对比图"}, {"row": 138, "type": "对话朗读--配图", "id": "1221111", "config_info": "S11主线", "role": "User", "en_text": "This is not a white shoe.", "config": "【任务标题】辨认白鞋\n【资源配置】\n图片时机:互动内容\n\n【情境引入】\nUser: You are right.\n\n【互动内容】\nUser: This is not a white shoe.(朗读)\n\n\n【后置对话】\n无", "knowledge": "This is / These are not...", "desc": ""}, {"row": 149, "type": "对话选择-配图", "id": "1221112", "config_info": "S12主线", "role": "", "en_text": "", "config": "【任务标题】汇报发现\n【资源配置】\n图片时机:互动内容 互动反馈\n\n【情境引入】Sally: What did you find?\n\n【互动内容】\n要求:选择正确的回复\n选项:(音频)\n选项1:We found his baseball cap.(正确)\n- 反馈 Sally: Great news!\n选项2:We found his white hat.\n- 反馈 Vicky: Wait, Are you sure?\n\n【后置对话】\n无", "knowledge": "baseball cap", "desc": "【教研图】蓝色棒球帽、墨镜、棕色的鞋"}, {"row": 157, "type": "对话选择-配图", "id": "1221113", "config_info": "S13主线", "role": "User", "en_text": "We found sunglasses.", "config": "【任务标题】汇报眼镜\n【资源配置】\n图片时机:情境引入 互动内容 互动反馈\n\n【情境引入】\nSally: Great news! And?\n\n【互动内容】\n要求:选择正确的回复\n选项:(音频)\n选项1:We found his sunglasses.\n- 反馈 Sally: Really? He doesn't have sunglasses.\n选项2:We found his glasses.(正确)\n- 反馈 Sally: Oh, I see.\n\n【后置对话】\n无", "knowledge": "This is / These are not...", "desc": ""}, {"row": 164, "type": "对话选择-配图", "id": "1221114", "config_info": "S14主线", "role": "User", "en_text": "And a brown shoe.", "config": "【任务标题】汇报鞋子\n【资源配置】\n图片时机:互动内容 互动反馈\n\n【情境引入】\n无\n\n【互动内容】\n要求:选择正确的回复\n选项:(音频)\n选项1:And a brown shoe.(正确)\n- 反馈 Sally: A brown shoe?\n选项2:And a white shoe.\n- 反馈 Sally: That must be Logi's!\n\n【后置对话】\n无", "knowledge": "shoe", "desc": ""}, {"row": 195, "type": "图片单选", "id": "1221115", "config_info": "S15主线", "role": "", "en_text": "", "config": "【任务标题】选出棒球帽\n【资源配置】\n图片时机:互动内容\n\n【情境引入】\nUser: Here you are!\n\n【互动内容】\n题干:Choose the $baseball cap$.\n选项:\n00\n01\n02\n正确:00\n\n【互动反馈】\n正确:无\n错误:Logi: That's not my baseball cap.\n\n【后置对话】\n无", "knowledge": "baseball cap", "desc": "【教研图】蓝色棒球帽、墨镜、棕色鞋子,选出图片上的baseball cap"}, {"row": 221, "type": "听力拖拽", "id": "1221116", "config_info": "S17主线", "role": "", "en_text": "", "config": "导览配置\n【任务标题】\n找回Logi的物品\n\n【任务背景】\nLogi为什么弄丢了棒球帽,鞋子和眼镜?快来听听他在树林里的遭遇吧!\n\n【通关知识】\n单词:\nwear v. 穿着\nbaseball cap n. 棒球帽\nglasses n. 眼镜\nshoe n. 鞋子\n\n互动配置\n【开场语】\nLogi: Thank you for finding my things!\nLogi: Let me tell you what happened.\n\n【听力文本】\n# 文本 1\nLogi: I lost my way in the woods.\nLogi: A bird takes my glasses! \nLogi: I cannot see clearly.\nLogi: My shoe is in the mud.\nLogi: Oh, where is my shoe? I can't find it!\nLogi: The wind blows. My baseball cap flies up, up, up. \nLogi: My baseball cap is on the tree branch!\n\n\n【结束语】\nLogi: Now I can wear my baseball cap, glasses and shoes.\nLogi: Thank you, my friends!\n\n【题目信息】\n#多空选择\n选项图片编号:00,01,02,03,04\n答案图片编号:00,01,02\n\n【学习过程】\n句子 1\nA bird takes my glasses! 【glasses】\n\n句子 2\nMy shoe is in the mud.【shoe】\n\n句子 4\nMy baseball cap is on the tree branch!【baseball cap】", "knowledge": "wear \nbaseball cap\nglasses\nshoe", "desc": "将对应的baseball cap、glasses、shoe拖到对应位置上"}];
|
||
|
||
let originalConfigs = {};
|
||
let modifiedRows = new Set();
|
||
|
||
function escapeHtml(text) {
|
||
const div = document.createElement('div');
|
||
div.textContent = text;
|
||
return div.innerHTML;
|
||
}
|
||
|
||
function renderCards() {
|
||
const container = document.getElementById('cards');
|
||
container.innerHTML = '';
|
||
|
||
COMPONENTS.forEach((c, idx) => {
|
||
originalConfigs[c.row] = c.config;
|
||
const card = document.createElement('div');
|
||
card.className = 'card';
|
||
card.id = 'card-' + c.row;
|
||
|
||
let html = '<div class="card-header">';
|
||
html += '<span class="card-type">#' + (idx+1) + ' ' + escapeHtml(c.type) + '</span>';
|
||
html += '<span class="card-meta">Row ' + c.row + ' | <span class="tag tag-id">ID: ' + escapeHtml(c.id) + '</span> | ' + escapeHtml(c.config_info) + '</span>';
|
||
html += '</div>';
|
||
html += '<div class="card-body">';
|
||
html += '<div class="field"><span class="field-label">角色:</span><span class="field-value">' + escapeHtml(c.role) + '</span></div>';
|
||
html += '<div class="field"><span class="field-label">英文台词:</span><span class="field-value">' + escapeHtml(c.en_text) + '</span></div>';
|
||
|
||
if (c.knowledge) {
|
||
html += '<div class="field"><span class="tag tag-k">知识点: ' + escapeHtml(c.knowledge) + '</span></div>';
|
||
}
|
||
if (c.desc && c.desc !== 'None') {
|
||
html += '<div class="field"><span class="field-label">剧情描述:</span><span class="field-value">' + escapeHtml(c.desc) + '</span></div>';
|
||
}
|
||
|
||
html += '<textarea class="config-textarea" id="config-' + c.row + '" data-row="' + c.row + '" oninput="onConfigChange(' + c.row + ')">' + escapeHtml(c.config) + '</textarea>';
|
||
html += '</div>';
|
||
|
||
card.innerHTML = html;
|
||
container.appendChild(card);
|
||
});
|
||
}
|
||
|
||
function onConfigChange(row) {
|
||
const textarea = document.getElementById('config-' + row);
|
||
const card = document.getElementById('card-' + row);
|
||
const current = textarea.value;
|
||
|
||
if (current !== originalConfigs[row]) {
|
||
modifiedRows.add(row);
|
||
card.classList.add('modified');
|
||
} else {
|
||
modifiedRows.delete(row);
|
||
card.classList.remove('modified');
|
||
}
|
||
|
||
document.getElementById('modifiedCount').textContent = modifiedRows.size;
|
||
}
|
||
|
||
async function pushToFeishu() {
|
||
if (modifiedRows.size === 0) {
|
||
showModal('提示', '没有检测到修改,无需推送。');
|
||
return;
|
||
}
|
||
|
||
const btn = document.getElementById('btnPush');
|
||
const progress = document.getElementById('progress');
|
||
const status = document.getElementById('status');
|
||
|
||
btn.disabled = true;
|
||
btn.textContent = '推送中...';
|
||
progress.classList.add('show');
|
||
status.className = 'status status-info';
|
||
status.textContent = '正在推送...';
|
||
|
||
const valueRanges = [];
|
||
const rows = Array.from(modifiedRows).sort((a,b) => a-b);
|
||
|
||
for (const row of rows) {
|
||
const config = document.getElementById('config-' + row).value;
|
||
valueRanges.push({
|
||
range: SHEET_ID + '!F' + row + ':F' + row,
|
||
values: [[config]]
|
||
});
|
||
}
|
||
|
||
// Get a fresh token
|
||
let token;
|
||
try {
|
||
const tokenResp = await fetch('https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({
|
||
app_id: 'cli_a931175d41799cc7',
|
||
app_secret: 'Iw2vEfbjT6GtV0GhbxbZqfQ4nAPtbR14'
|
||
})
|
||
});
|
||
const tokenData = await tokenResp.json();
|
||
token = tokenData.tenant_access_token;
|
||
} catch(e) {
|
||
showModal('推送失败', '无法获取飞书 Token,请检查网络连接。\n\n错误:' + e.message);
|
||
btn.disabled = false;
|
||
btn.textContent = '📤 推送到飞书';
|
||
progress.classList.remove('show');
|
||
status.className = 'status status-err';
|
||
status.textContent = '推送失败';
|
||
return;
|
||
}
|
||
|
||
// Batch update (max 50 per batch)
|
||
const batchSize = 50;
|
||
let successCount = 0;
|
||
let failCount = 0;
|
||
const errors = [];
|
||
|
||
for (let i = 0; i < valueRanges.length; i += batchSize) {
|
||
const batch = valueRanges.slice(i, i + batchSize);
|
||
progress.textContent = '推送第 ' + (i+1) + '-' + Math.min(i+batchSize, valueRanges.length) + ' / ' + valueRanges.length + ' 行...';
|
||
|
||
try {
|
||
const resp = await fetch('https://open.feishu.cn/open-apis/sheets/v2/spreadsheets/' + SPREADSHEET_TOKEN + '/values_batch_update', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Authorization': 'Bearer ' + token,
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({valueRanges: batch})
|
||
});
|
||
const data = await resp.json();
|
||
|
||
if (data.code === 0) {
|
||
successCount += batch.length;
|
||
// Update original configs
|
||
for (const vr of batch) {
|
||
const rowMatch = vr.range.match(/F(\d+)/);
|
||
if (rowMatch) {
|
||
const row = parseInt(rowMatch[1]);
|
||
originalConfigs[row] = vr.values[0][0];
|
||
modifiedRows.delete(row);
|
||
const card = document.getElementById('card-' + row);
|
||
if (card) card.classList.remove('modified');
|
||
}
|
||
}
|
||
} else {
|
||
failCount += batch.length;
|
||
errors.push('批次 ' + (Math.floor(i/batchSize)+1) + ': ' + (data.msg || '未知错误'));
|
||
}
|
||
} catch(e) {
|
||
failCount += batch.length;
|
||
errors.push('批次 ' + (Math.floor(i/batchSize)+1) + ': ' + e.message);
|
||
}
|
||
}
|
||
|
||
btn.disabled = false;
|
||
btn.textContent = '📤 推送到飞书';
|
||
progress.classList.remove('show');
|
||
document.getElementById('modifiedCount').textContent = modifiedRows.size;
|
||
|
||
if (failCount === 0) {
|
||
status.className = 'status status-ok';
|
||
status.textContent = '✅ 推送成功!' + successCount + ' 行已更新';
|
||
showModal('推送成功', '已成功更新 ' + successCount + ' 行组件配置到飞书文档。');
|
||
} else {
|
||
status.className = 'status status-err';
|
||
status.textContent = '⚠️ 部分失败:' + successCount + ' 成功, ' + failCount + ' 失败';
|
||
showModal('推送结果', '成功:' + successCount + ' 行\n失败:' + failCount + ' 行\n\n错误详情:\n' + errors.join('\n'));
|
||
}
|
||
}
|
||
|
||
function showModal(title, body) {
|
||
document.getElementById('modalTitle').textContent = title;
|
||
document.getElementById('modalBody').textContent = body;
|
||
document.getElementById('modal').classList.add('show');
|
||
}
|
||
|
||
// Init
|
||
renderCards();
|
||
</script>
|
||
</body>
|
||
</html> |