-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathproject-codes.html
More file actions
169 lines (169 loc) · 23.5 KB
/
project-codes.html
File metadata and controls
169 lines (169 loc) · 23.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>访问码管理 - 创页 创作台</title>
<style>
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Noto+Serif+SC:wght@600;700;900&display=swap");
:root{--bg:#f7f7f5;--surface-strong:#fff;--brand-light:#f0f0ec;--text-primary:#111;--text-secondary:#686868;--border:rgba(17,17,17,.08);--radius:20px;--radius-sm:12px;--success:#10b981;--danger:#ef4444;--font-sans:"Inter",system-ui,-apple-system,sans-serif;--font-serif:"Noto Serif SC",serif;--ease:cubic-bezier(.16,1,.3,1);--ease-out:cubic-bezier(.33,1,.68,1)}
*{box-sizing:border-box;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;margin:0;padding:0}
body{background:var(--bg);color:var(--text-primary);font-family:var(--font-sans);overflow-x:hidden;min-height:100vh}
#ambient-glow{position:fixed;width:600px;height:600px;background:radial-gradient(circle,rgba(17,17,17,.03) 0,transparent 70%);border-radius:50%;pointer-events:none;z-index:-1;transform:translate(-50%,-50%);filter:blur(72px);transition:transform .8s var(--ease-out)}
nav{display:flex;justify-content:space-between;align-items:center;padding:18px 6%;position:sticky;top:0;z-index:100;background:rgba(247,247,245,.78);backdrop-filter:blur(18px);-webkit-backdrop-filter:blur(18px);border-bottom:1px solid rgba(17,17,17,.05)}
.logo{font-family:var(--font-serif);font-size:24px;font-weight:700;color:var(--text-primary);letter-spacing:-.03em;text-decoration:none;transition:opacity .3s ease}
.logo:hover{opacity:.8}.nav-center{font-size:14px;font-weight:600;display:flex;align-items:center;gap:12px}
.status-tag{font-size:11px;background:rgba(16,185,129,.1);color:var(--success);padding:2px 8px;border-radius:6px;font-weight:600;border:1px solid rgba(16,185,129,.2)}
.nav-actions{display:flex;gap:10px;align-items:center}
.btn-nav-ghost,.btn-nav-primary,.btn,.tab{display:inline-flex;align-items:center;justify-content:center}
.btn-nav-ghost{border:1px solid var(--border);background:rgba(255,255,255,.6);color:var(--text-primary);height:36px;padding:0 16px;border-radius:999px;font-size:13px;font-weight:500;cursor:pointer;text-decoration:none;transition:background .2s,border-color .2s,transform .2s}
.btn-nav-ghost:hover{background:rgba(255,255,255,.92);border-color:rgba(17,17,17,.16);transform:translateY(-1px)}
.btn-nav-primary{background:var(--text-primary);border:1px solid var(--text-primary);color:#fff;height:36px;padding:0 20px;border-radius:999px;font-size:13px;font-weight:600;text-decoration:none;transition:all .3s var(--ease);box-shadow:0 4px 12px rgba(0,0,0,.08)}
.btn-nav-primary:hover,.btn-primary:hover{background:#27272a;transform:translateY(-1px);box-shadow:0 6px 16px rgba(0,0,0,.12)}
.codes-container{max-width:1200px;margin:40px auto 100px;padding:0 6%}
.page-header{margin-bottom:40px;display:flex;justify-content:space-between;align-items:flex-end;gap:20px}
.page-header h1{font-family:var(--font-serif);font-size:32px;margin:0 0 10px;letter-spacing:-.02em}
.page-header p{color:var(--text-secondary);font-size:15px}
.stats-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:20px;margin-bottom:40px}
.stat-card,.tool-panel,.list-panel{background:var(--surface-strong);border:1px solid var(--border);box-shadow:0 4px 20px rgba(0,0,0,.015)}
.stat-card{padding:24px;border-radius:var(--radius);transition:transform .3s var(--ease)}
.stat-card:hover{transform:translateY(-2px);box-shadow:0 8px 24px rgba(0,0,0,.03)}
.stat-card span{font-size:12px;color:var(--text-secondary);font-weight:500;display:block;margin-bottom:8px}
.stat-card h2{font-size:32px;font-weight:700;letter-spacing:-.02em}
.tool-panel{padding:32px;border-radius:var(--radius);margin-bottom:32px;display:grid;grid-template-columns:1fr 1fr 200px;gap:24px;align-items:flex-end}
.input-group{display:flex;flex-direction:column;gap:8px}.input-group label{font-size:13px;font-weight:600}
.form-input{width:100%;padding:12px 16px;border-radius:var(--radius-sm);border:1px solid var(--border);background:var(--bg);font:inherit;font-size:14px;outline:none;transition:.2s;color:var(--text-primary)}
.form-input:focus{border-color:var(--text-primary);background:#fff}.form-input::placeholder{color:#a1a1aa}
.filter-tabs{display:flex;gap:8px;margin-bottom:20px}
.tab{padding:8px 20px;border-radius:999px;border:1px solid transparent;background:transparent;cursor:pointer;font-size:13px;font-weight:600;color:var(--text-secondary);transition:.2s}
.tab.active{background:var(--text-primary);color:#fff}.tab:not(.active):hover{background:var(--border);color:var(--text-primary)}
.list-panel{border-radius:var(--radius);padding:20px;overflow:hidden}
.list-table{width:100%;border-collapse:collapse}.list-table th{text-align:left;padding:16px 20px;font-size:12px;font-weight:600;color:var(--text-secondary);border-bottom:1px solid var(--border)}
.list-table td{padding:16px 20px;font-size:14px;border-bottom:1px solid rgba(17,17,17,.04);color:var(--text-primary)}.list-table tr:last-child td{border-bottom:none}
.code-text{font-family:"Courier New",Courier,monospace;font-weight:600;color:var(--text-primary);background:var(--brand-light);padding:4px 8px;border-radius:6px;border:1px solid var(--border)}
.status-pill{padding:4px 10px;border-radius:999px;font-size:12px;font-weight:500}.unused{background:rgba(16,185,129,.1);color:var(--success);border:1px solid rgba(16,185,129,.2)}.used{background:rgba(17,17,17,.05);color:var(--text-secondary);border:1px solid var(--border)}.invalid{background:rgba(239,68,68,.1);color:var(--danger);border:1px solid rgba(239,68,68,.2)}
.btn{padding:10px 20px;border-radius:12px;font-size:13px;font-weight:600;cursor:pointer;transition:.2s ease;text-decoration:none;gap:8px;border:none;outline:none}
.btn-primary{background:var(--text-primary);color:#fff;box-shadow:0 4px 12px rgba(0,0,0,.08);border-radius:999px}
.btn-outline{border:1px solid var(--border);background:var(--surface-strong);color:var(--text-primary);border-radius:999px}.btn-outline:hover{background:var(--brand-light);border-color:rgba(17,17,17,.15)}
.btn-sm{padding:6px 14px;font-size:12px;border-radius:8px}.btn-ghost-sm{background:transparent;color:var(--text-secondary);border:1px solid transparent;padding:6px 14px;font-size:12px;font-weight:500;border-radius:8px;cursor:pointer;transition:.2s}.btn-ghost-sm:hover{background:rgba(17,17,17,.05);color:var(--text-primary)}
#toast{position:fixed;bottom:40px;left:50%;transform:translateX(-50%) translateY(100px);background:var(--text-primary);color:#fff;padding:12px 24px;border-radius:999px;font-size:13px;font-weight:500;transition:.4s var(--ease-out);z-index:1000;box-shadow:0 10px 30px rgba(0,0,0,.15);opacity:0}#toast.show{transform:translateX(-50%) translateY(0);opacity:1}
.hidden{display:none!important}
@media (max-width:1024px){.tool-panel{grid-template-columns:1fr;gap:16px}.list-table th:nth-child(4),.list-table td:nth-child(4){display:none}}
@media (max-width:768px){.stats-grid{grid-template-columns:repeat(2,1fr)}.page-header{flex-direction:column;align-items:flex-start}}
@media (max-width:720px){nav{padding:16px 20px;flex-wrap:wrap}.nav-actions{width:100%;justify-content:space-between;margin-top:12px}.codes-container{padding:0 20px;margin-top:24px}.tool-panel{padding:24px}.list-table,.list-table thead,.list-table tbody,.list-table th,.list-table td,.list-table tr{display:block;width:100%}.list-table thead{display:none}.list-table tr{border:1px solid var(--border);border-radius:16px;padding:16px;margin-bottom:12px;background:var(--bg)}.list-table td{padding:8px 0;border:none;display:flex;justify-content:space-between;align-items:center;gap:12px}.list-table td::before{font-size:12px;font-weight:600;color:var(--text-secondary);content:attr(data-label);flex:0 0 auto}}
@media (max-width:600px){nav{padding:16px 20px;flex-wrap:wrap}.nav-actions{width:100%;justify-content:space-between;margin-top:12px}.stats-grid{grid-template-columns:repeat(2,1fr)}.codes-container{padding:0 20px;margin-top:24px}.tool-panel{padding:24px}}
</style>
</head>
<body>
<div id="ambient-glow"></div>
<nav>
<div style="display:flex;align-items:center;gap:24px;">
<a href="index.html" class="logo">创页</a>
<div class="nav-center">
<span id="nav-project-title">项目名称加载中</span>
<span class="status-tag" id="nav-project-status">--</span>
</div>
</div>
<div class="nav-actions">
<button class="btn-nav-ghost" type="button" data-action="logout">退出账号</button>
<a href="project-editor.html" class="btn-nav-ghost" id="nav-editor-link">返回编辑器</a>
<a href="dashboard.html" class="btn-nav-primary">返回工坊</a>
</div>
</nav>
<main class="codes-container">
<header class="page-header">
<div>
<h1>兑换码与交付管理</h1>
<p>为你的项目生成、筛选并交付用于访问、分发或变现的访问码。</p>
</div>
<div style="display:flex;gap:12px;">
<button class="btn btn-outline" type="button" id="export-csv-btn">导出 CSV</button>
<button class="btn btn-outline" type="button" id="batch-copy-btn">批量复制</button>
</div>
</header>
<section class="stats-grid">
<div class="stat-card"><span>累计生成兑换码</span><h2 id="stat-total">--</h2></div>
<div class="stat-card"><span>未使用</span><h2 id="stat-unused" style="color:var(--success);">--</h2></div>
<div class="stat-card"><span>已使用</span><h2 id="stat-used">--</h2></div>
<div class="stat-card"><span>今日新增</span><h2 id="stat-today">--</h2></div>
</section>
<section class="tool-panel" id="code-tool-panel">
<div class="input-group">
<label>生成数量</label>
<input type="number" id="gen-count" class="form-input" placeholder="输入生成数量,例如 50" value="50" />
</div>
<div class="input-group">
<label>批次名称 / 备注</label>
<input type="text" id="gen-batch" class="form-input" placeholder="例如:十一月常规批次" value="默认生成批次" />
</div>
<button class="btn btn-primary" style="width:100%;height:44px;" type="button" id="generate-btn">生成兑换码</button>
</section>
<section class="tool-panel hidden" id="direct-share-panel" style="grid-template-columns:1fr 180px 180px;">
<div class="input-group">
<label id="share-link-label">访问链接</label>
<input type="text" id="share-link" class="form-input" readonly placeholder="项目发布后将生成访问链接" />
</div>
<button class="btn btn-outline" type="button" id="copy-share-btn" style="width:100%;height:44px;">复制链接</button>
<button class="btn btn-primary" type="button" id="open-share-btn" style="width:100%;height:44px;">打开预览</button>
</section>
<div class="filter-tabs" id="filter-tabs">
<button class="tab active" type="button" data-filter="all">全部</button>
<button class="tab" type="button" data-filter="unused">未使用</button>
<button class="tab" type="button" data-filter="used">已使用</button>
<button class="tab" type="button" data-filter="invalid">已失效</button>
</div>
<section class="list-panel" id="code-list-panel">
<table class="list-table">
<thead>
<tr>
<th>兑换码</th>
<th>状态</th>
<th>生成时间</th>
<th>批次名称</th>
<th>使用记录</th>
<th>操作</th>
</tr>
</thead>
<tbody id="code-list">
<tr>
<td colspan="6" style="text-align:center;color:var(--text-secondary);padding:40px 20px;">正在加载兑换码数据...</td>
</tr>
</tbody>
</table>
</section>
<footer style="margin-top:40px;text-align:center;color:var(--text-secondary);font-size:13px;">
<p id="footer-tip">交付提示:请先发布项目,发布后将生成对应的兑换入口与可用码。</p>
</footer>
</main>
<div id="toast"></div>
<script src="seller-session.js"></script>
<script>
const glow=document.getElementById("ambient-glow");let targetX=window.innerWidth/2,targetY=window.innerHeight/2,currentX=targetX,currentY=targetY;window.addEventListener("mousemove",(e)=>{targetX=e.clientX;targetY=e.clientY;});function animateGlow(){currentX+=(targetX-currentX)*.05;currentY+=(targetY-currentY)*.05;glow.style.transform=`translate(calc(-50% + ${currentX}px), calc(-50% + ${currentY}px))`;requestAnimationFrame(animateGlow)}animateGlow();
const params=new URLSearchParams(window.location.search),state={projectId:params.get("projectId")||localStorage.getItem("tester:lastProjectId"),proProjectId:params.get("proProjectId")||localStorage.getItem("tester:lastProProjectId"),projectType:params.get("proProjectId")||(!params.get("projectId")&&localStorage.getItem("tester:lastProProjectId"))?"pro":"standard",project:null,codes:[],filter:"all",shareLink:""};
const codeList=document.getElementById("code-list"),statTotal=document.getElementById("stat-total"),statUnused=document.getElementById("stat-unused"),statUsed=document.getElementById("stat-used"),statToday=document.getElementById("stat-today"),projectTitleNode=document.getElementById("nav-project-title"),projectStatusNode=document.getElementById("nav-project-status"),editorLink=document.getElementById("nav-editor-link"),genCountInput=document.getElementById("gen-count"),genBatchInput=document.getElementById("gen-batch"),codeToolPanel=document.getElementById("code-tool-panel"),directSharePanel=document.getElementById("direct-share-panel"),filterTabs=document.getElementById("filter-tabs"),codeListPanel=document.getElementById("code-list-panel"),shareLinkLabel=document.getElementById("share-link-label"),shareLinkInput=document.getElementById("share-link"),copyShareBtn=document.getElementById("copy-share-btn"),openShareBtn=document.getElementById("open-share-btn"),footerTip=document.getElementById("footer-tip"),toast=document.getElementById("toast"),exportCsvBtn=document.getElementById("export-csv-btn"),batchCopyBtn=document.getElementById("batch-copy-btn"),generateBtn=document.getElementById("generate-btn");
function showToast(message){toast.textContent=message;toast.classList.add("show");window.clearTimeout(showToast.timer);showToast.timer=window.setTimeout(()=>toast.classList.remove("show"),2000)}
async function fetchJson(url,options){return window.SellerSession.fetchJson(url,options)}
function getProjectApiBase(){return state.projectType==="pro"?`/api/pro-projects/${state.proProjectId}`:`/api/projects/${state.projectId}`}
function getProjectCodesApiBase(){return`${getProjectApiBase()}/codes`}
function getEditorLink(project){return project?.projectType==="pro"||state.projectType==="pro"?`pro-project-editor.html?projectId=${project.id}`:`project-editor.html?projectId=${project.id}`}
function formatDate(value){return value?new Date(value).toLocaleString("zh-CN",{year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit"}):"-"}
function getFilteredCodes(){return state.filter==="all"?state.codes:state.codes.filter((item)=>item.status===state.filter)}
function buildShareLink(project){if(!project?.id)return"";if(project.deliveryMode==="direct")return state.projectType==="pro"?`${window.location.origin}/quiz.html?proProjectId=${project.id}`:`${window.location.origin}/quiz.html?projectId=${project.id}`;return state.projectType==="pro"?`${window.location.origin}/redeem.html?proProjectId=${project.id}`:`${window.location.origin}/redeem.html?projectId=${project.id}`}
function applyProjectMode(){const isDirect=state.project?.deliveryMode==="direct",isPublished=state.project?.status==="published";codeToolPanel.classList.toggle("hidden",isDirect);directSharePanel.classList.remove("hidden");filterTabs.classList.toggle("hidden",isDirect);codeListPanel.classList.toggle("hidden",isDirect);exportCsvBtn.classList.toggle("hidden",isDirect);batchCopyBtn.classList.toggle("hidden",isDirect);state.shareLink=isPublished?buildShareLink(state.project):"";shareLinkInput.value=state.shareLink;shareLinkLabel.textContent=isDirect?"直接访问链接":"兑换入口链接";copyShareBtn.disabled=!isPublished;openShareBtn.disabled=!isPublished;footerTip.textContent=isDirect?(isPublished?"交付提示:当前项目未开启兑换码功能,买家可通过直接访问链接体验。":"交付提示:请先发布项目,发布后将生成可直接访问的链接。"):isPublished?"交付提示:您可以将兑换码发给受众,并通过入口链接引导其核销体验。":"交付提示:请先发布项目,发布后会生成对应的兑换入口与可用码。"}
function renderHeader(){if(!state.project)return;projectTitleNode.textContent=state.project.title||"未命名项目";projectStatusNode.textContent=state.project.status==="published"?"已发布":"草稿";projectStatusNode.style.color=state.project.status==="published"?"var(--success)":"var(--text-secondary)";projectStatusNode.style.borderColor=state.project.status==="published"?"rgba(16,185,129,0.2)":"var(--border)";projectStatusNode.style.background=state.project.status==="published"?"rgba(16,185,129,0.1)":"var(--surface-strong)";editorLink.href=getEditorLink(state.project)}
function renderStats(){const total=state.codes.length,unused=state.codes.filter((item)=>item.status==="unused").length,used=state.codes.filter((item)=>item.status==="used").length,today=state.codes.filter((item)=>{if(!item.createdAt)return false;const createdAt=new Date(item.createdAt),now=new Date;return createdAt.getFullYear()===now.getFullYear()&&createdAt.getMonth()===now.getMonth()&&createdAt.getDate()===now.getDate()}).length;statTotal.textContent=String(total);statUnused.textContent=String(unused);statUsed.textContent=String(used);statToday.textContent=`+${today}`}
function renderTable(){const filteredCodes=getFilteredCodes();if(!filteredCodes.length){codeList.innerHTML=`<tr><td colspan="6" style="text-align:center;color:var(--text-secondary);padding:40px 20px;">当前暂无兑换码记录,可在此处生成。</td></tr>`;return}codeList.innerHTML=filteredCodes.map((item)=>{const usageText=item.status==="used"?[item.note?`备注:${item.note}`:"已核销",formatDate(item.redeemedAt)].filter(Boolean).join(" · "):item.note||"-";const copyButton=item.status!=="used"?`<button class="btn-outline btn-sm" data-action="copy" data-code="${item.code}">复制</button>`:"";const invalidateButton=item.status==="unused"?`<button class="btn-ghost-sm" style="color: var(--danger);" data-action="invalidate" data-id="${item.id}">作废</button>`:"";return`<tr><td data-label="兑换码"><span class="code-text">${item.code}</span></td><td data-label="状态"><span class="status-pill ${item.status}">${item.status==="unused"?"未使用":item.status==="used"?"已使用":"已失效"}</span></td><td data-label="时间" style="color: var(--text-secondary);">${formatDate(item.createdAt)}</td><td data-label="批次">${item.batchName||"-"}</td><td data-label="记录" style="color: var(--text-secondary);">${usageText}</td><td data-label="操作" style="display:flex;gap:8px;">${copyButton}${invalidateButton}</td></tr>`}).join("")}
async function copyText(value){await navigator.clipboard.writeText(value);showToast("兑换码已复制")}
async function loadProjectAndCodes(){window.SellerSession.requireUser();if(!state.projectId&&!state.proProjectId){alert("缺少项目参数,将返回创建页。");window.location.href=state.projectType==="pro"?"pro-project-new.html":"project-new.html";return}const[projectData,codeData]=await Promise.all([fetchJson(getProjectApiBase()),fetchJson(getProjectCodesApiBase())]);state.project=projectData.project;state.codes=codeData.codes||[];if(state.projectType==="pro")localStorage.setItem("tester:lastProProjectId",String(state.project.id));else localStorage.setItem("tester:lastProjectId",String(state.project.id));localStorage.setItem("tester:lastProject",JSON.stringify(state.project));renderHeader();applyProjectMode();renderStats();renderTable()}
async function generateCodes(){const count=Number(genCountInput.value||0),batchName=genBatchInput.value.trim();if(!count||count<1){showToast("请输入正确的生成数量");return}generateBtn.disabled=true;try{await fetchJson(`${getProjectCodesApiBase()}/generate`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({count,batchName})});await loadProjectAndCodes();showToast(`成功生成 ${count} 个兑换码`)}finally{generateBtn.disabled=false}}
async function invalidateCode(codeId){await fetchJson(`${getProjectCodesApiBase()}/${codeId}/invalidate`,{method:"PATCH"});await loadProjectAndCodes();showToast("兑换码已被作废")}
async function exportCsv(){if(!state.codes.length){showToast("当前无记录可供导出");return}const lines=[["code","status","createdAt","batchName","redeemedAt"].join(","),...state.codes.map((item)=>[item.code,item.status,item.createdAt||"",item.batchName||"",item.redeemedAt||""].map((value)=>`\"${String(value).replace(/\"/g,'\"\"')}\"`).join(","))],blob=new Blob([`\uFEFF${lines.join("\n")}`],{type:"text/csv;charset=utf-8;"}),url=URL.createObjectURL(blob),link=document.createElement("a");link.href=url;link.download=`${state.project?.title||"tester"}-codes.csv`;link.click();URL.revokeObjectURL(url);showToast("CSV 文件已导出")}
async function batchCopyCodes(){const values=getFilteredCodes().map((item)=>item.code);if(!values.length){showToast("当前筛选下没有可复制的内容");return}await copyText(values.join("\n"))}
async function copyShareLink(){if(!state.shareLink){showToast("项目需发布后方可获取链接");return}await copyText(state.shareLink)}
function openSharePreview(){if(!state.shareLink){showToast("项目需发布后方可预览");return}window.open(state.shareLink,"_blank","noopener")}
function filterList(status,button){state.filter=status;document.querySelectorAll(".tab").forEach((item)=>item.classList.remove("active"));if(button)button.classList.add("active");renderTable()}
codeList.addEventListener("click",async(event)=>{const action=event.target.dataset.action;if(!action)return;try{if(action==="copy")await copyText(event.target.dataset.code);if(action==="invalidate")await invalidateCode(event.target.dataset.id)}catch(error){showToast(error.message||"操作未响应")}});
document.querySelectorAll("[data-filter]").forEach((button)=>button.addEventListener("click",()=>filterList(button.dataset.filter,button)));exportCsvBtn.addEventListener("click",()=>{exportCsv().catch((error)=>showToast(error.message||"导出中断"))});batchCopyBtn.addEventListener("click",()=>{batchCopyCodes().catch((error)=>showToast(error.message||"复制中断"))});copyShareBtn.addEventListener("click",()=>{copyShareLink().catch((error)=>showToast(error.message||"复制中断"))});openShareBtn.addEventListener("click",openSharePreview);generateBtn.addEventListener("click",()=>{generateCodes().catch((error)=>showToast(error.message||"生成终端异常"))});window.showToast=showToast;window.filterList=filterList;window.generateCodes=generateCodes;loadProjectAndCodes().catch((error)=>{console.error(error);alert(error.message||"数据拉取失败,请检查网络后重试")});
</script>
</body>
</html>