Initial commit
Some checks failed
docker-cicd / build-and-push (push) Failing after 16s

This commit is contained in:
Logic
2026-03-21 18:47:00 +08:00
commit 29d1ad71ab
29 changed files with 4050 additions and 0 deletions

134
static/app.js Normal file
View File

@@ -0,0 +1,134 @@
const searchInput = document.getElementById("searchInput");
const memberSelect = document.getElementById("memberSelect");
const generateBtn = document.getElementById("generateBtn");
const statusText = document.getElementById("statusText");
const resultCard = document.getElementById("resultCard");
const qrImage = document.getElementById("qrImage");
const downloadLink = document.getElementById("downloadLink");
const rawJson = document.getElementById("rawJson");
const metaMemberId = document.getElementById("metaMemberId");
const metaVillage = document.getElementById("metaVillage");
const metaUuid = document.getElementById("metaUuid");
const metaStatus = document.getElementById("metaStatus");
const metaMsg = document.getElementById("metaMsg");
const metaQrPrefix = document.getElementById("metaQrPrefix");
const metaPayloadLen = document.getElementById("metaPayloadLen");
const metaPayloadPrefix = document.getElementById("metaPayloadPrefix");
let allItems = [];
function setStatus(message, isError = false) {
statusText.textContent = message;
statusText.classList.toggle("error", isError);
}
function renderOptions(items) {
memberSelect.innerHTML = "";
if (!items.length) {
const option = document.createElement("option");
option.value = "";
option.textContent = "没有可选 ID";
memberSelect.appendChild(option);
return;
}
for (const item of items) {
const option = document.createElement("option");
option.value = String(item.member_id);
option.textContent = item.label;
memberSelect.appendChild(option);
}
if (!memberSelect.value) {
memberSelect.selectedIndex = 0;
}
}
function filterItems() {
const keyword = searchInput.value.trim().toLowerCase();
if (!keyword) {
renderOptions(allItems);
setStatus(`已加载 ${allItems.length} 个 ID`);
return;
}
const filtered = allItems.filter((item) => {
const idText = String(item.member_id);
const village = String(item.village_name || "").toLowerCase();
return idText.includes(keyword) || village.includes(keyword);
});
renderOptions(filtered);
setStatus(`筛选后剩余 ${filtered.length} 个 ID`);
}
async function loadFoundIds() {
setStatus("正在加载 ID 列表...");
try {
const response = await fetch("/api/found-ids");
const data = await response.json();
if (!response.ok || !data.ok) {
throw new Error(data.error || "加载 ID 列表失败");
}
allItems = data.items || [];
renderOptions(allItems);
setStatus(`已加载 ${allItems.length} 个 ID`);
} catch (error) {
setStatus(error.message || "加载失败", true);
}
}
function showResult(data) {
resultCard.classList.remove("hidden");
qrImage.src = data.data_url;
downloadLink.href = data.data_url;
downloadLink.download = `member_${data.member_id}.gif`;
metaMemberId.textContent = data.member_id ?? "-";
metaVillage.textContent = data.village_name ?? "-";
metaUuid.textContent = data.uuid ?? "-";
metaStatus.textContent = data.status ?? "-";
metaMsg.textContent = data.msg ?? "-";
metaQrPrefix.textContent = data.qr_prefix ?? "-";
metaPayloadLen.textContent = data.payload_len ?? "-";
metaPayloadPrefix.textContent = data.payload_hex_prefix ?? "-";
rawJson.textContent = JSON.stringify(data, null, 2);
}
async function generateQr() {
const memberId = memberSelect.value;
if (!memberId) {
setStatus("请先选择一个 member_id", true);
return;
}
generateBtn.disabled = true;
setStatus(`正在为 ${memberId} 生成二维码...`);
try {
const response = await fetch("/api/generate", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ member_id: Number(memberId) }),
});
const data = await response.json();
if (!response.ok || !data.ok) {
throw new Error(data.error || "生成二维码失败");
}
showResult(data);
setStatus(`member_id=${memberId} 生成成功`);
} catch (error) {
setStatus(error.message || "生成失败", true);
} finally {
generateBtn.disabled = false;
}
}
searchInput.addEventListener("input", filterItems);
generateBtn.addEventListener("click", generateQr);
memberSelect.addEventListener("dblclick", generateQr);
loadFoundIds();

161
static/styles.css Normal file
View File

@@ -0,0 +1,161 @@
:root {
color-scheme: light dark;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
body {
margin: 0;
background: #0f172a;
color: #e2e8f0;
}
.container {
max-width: 1080px;
margin: 0 auto;
padding: 24px;
}
.card {
background: #111827;
border: 1px solid #334155;
border-radius: 16px;
padding: 20px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.25);
margin-bottom: 20px;
}
h1, h2 {
margin-top: 0;
}
.subtitle {
color: #94a3b8;
margin-bottom: 20px;
}
label {
display: block;
margin: 12px 0 8px;
font-weight: 600;
}
input,
select,
button {
width: 100%;
box-sizing: border-box;
border-radius: 10px;
border: 1px solid #475569;
background: #0f172a;
color: #e2e8f0;
padding: 10px 12px;
font-size: 14px;
}
select {
min-height: 320px;
}
button {
cursor: pointer;
background: #2563eb;
border: none;
font-weight: 700;
}
button:disabled {
cursor: not-allowed;
opacity: 0.7;
}
.actions {
margin-top: 16px;
display: flex;
gap: 12px;
align-items: center;
}
.actions button {
width: auto;
min-width: 160px;
}
.status-text {
color: #93c5fd;
}
.status-text.error {
color: #fca5a5;
}
.hidden {
display: none;
}
.result-layout {
display: grid;
grid-template-columns: minmax(260px, 340px) 1fr;
gap: 20px;
}
.qr-panel {
display: flex;
flex-direction: column;
gap: 12px;
}
.qr-panel img {
width: 100%;
background: #fff;
border-radius: 12px;
}
.qr-panel a {
color: #93c5fd;
text-decoration: none;
}
.meta-list {
margin: 0 0 16px;
}
.meta-list > div {
display: grid;
grid-template-columns: 120px 1fr;
gap: 8px;
padding: 8px 0;
border-bottom: 1px solid #334155;
}
.meta-list dt {
color: #94a3b8;
}
.meta-list dd {
margin: 0;
word-break: break-all;
}
pre {
white-space: pre-wrap;
word-break: break-word;
background: #020617;
border-radius: 12px;
padding: 12px;
overflow-x: auto;
}
@media (max-width: 900px) {
.result-layout {
grid-template-columns: 1fr;
}
.actions {
flex-direction: column;
align-items: stretch;
}
.actions button {
width: 100%;
}
}