Files
2026-06-21 10:00:13 +08:00

256 lines
11 KiB
JavaScript

(() => {
const $ = (sel) => document.querySelector(sel);
const rechargeOptionListEl = $('#rechargeOptionList') || $('#planList');
const estimateBoxEl = $('#estimateBox');
const resultBoxEl = $('#orderResultBox');
const holderNameEl = $('#holderName');
const customInitialBalanceEl = $('#customInitialBalance') || $('#initialBalance');
const customRechargeBoxEl = $('#customRechargeBox');
const submitBtn = $('#submitOrderBtn');
const HOLDER_NAME_PATTERN = /^[A-Za-z][A-Za-z .,'()&@/_\-+]*$/;
const state = {
rechargeOptions: [5, 10, 15, 20],
selectedAmount: 5,
customMode: false
};
if (!rechargeOptionListEl || !estimateBoxEl || !resultBoxEl || !holderNameEl || !customInitialBalanceEl || !submitBtn) {
console.error('[ic-card-order] Missing required DOM nodes', {
rechargeOptionListEl: !!rechargeOptionListEl,
estimateBoxEl: !!estimateBoxEl,
resultBoxEl: !!resultBoxEl,
holderNameEl: !!holderNameEl,
customInitialBalanceEl: !!customInitialBalanceEl,
customRechargeBoxEl: !!customRechargeBoxEl,
submitBtn: !!submitBtn
});
return;
}
const api = {
async request(url, opts = {}) {
const res = await fetch(url, opts);
const data = await res.json().catch(() => ({}));
if (!res.ok || data.ok === false) {
throw new Error(data.error || res.statusText || '请求失败');
}
return data;
},
fetchConfig() {
return api.request('/api/public/ic-cards/config');
},
createOrder(payload) {
return api.request('/api/public/ic-cards/orders', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
}
};
const escapeHtml = (value) => String(value == null ? '' : value)
.replace(/&/g, '&')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
const getInitialBalance = () => {
if (state.customMode) {
return Math.max(1, Number(customInitialBalanceEl.value || 0) || 0);
}
return Math.max(1, Number(state.selectedAmount || 0) || 0);
};
const renderRechargeOptions = () => {
const options = Array.isArray(state.rechargeOptions) && state.rechargeOptions.length
? state.rechargeOptions
: [5, 10, 15, 20];
rechargeOptionListEl.innerHTML = options.map((amount) => `
<button type="button" class="jr-card-plan ${!state.customMode && Number(state.selectedAmount) === Number(amount) ? 'is-active' : ''}" data-amount="${escapeHtml(amount)}">
<span class="jr-card-plan-title">${escapeHtml(amount)}</span>
<span class="jr-card-plan-desc">首次充值 ${escapeHtml(amount)}</span>
</button>
`).join('') + `
<button type="button" class="jr-card-plan jr-card-plan-compact ${state.customMode ? 'is-active' : ''}" data-custom="true">
<span class="jr-card-plan-title">自定义</span>
<span class="jr-card-plan-desc">手动输入首次充值金额</span>
</button>
`;
if (customRechargeBoxEl) {
customRechargeBoxEl.classList.toggle('is-active', !!state.customMode);
}
rechargeOptionListEl.querySelectorAll('[data-amount]').forEach((button) => {
button.addEventListener('click', () => {
state.customMode = false;
state.selectedAmount = Number(button.getAttribute('data-amount')) || 5;
customInitialBalanceEl.disabled = true;
renderRechargeOptions();
renderEstimate();
});
});
const customBtn = rechargeOptionListEl.querySelector('[data-custom="true"]');
if (customBtn) {
customBtn.addEventListener('click', () => {
state.customMode = true;
customInitialBalanceEl.disabled = false;
customInitialBalanceEl.focus();
if (!customInitialBalanceEl.value) customInitialBalanceEl.value = '25';
renderRechargeOptions();
renderEstimate();
});
}
};
const syncLegacyDom = () => {
const holderPhoneEl = $('#holderPhone');
const orderNoteEl = $('#orderNote');
if (holderPhoneEl) {
holderPhoneEl.disabled = true;
holderPhoneEl.value = '';
holderPhoneEl.placeholder = '已停用';
holderPhoneEl.style.display = 'none';
}
if (orderNoteEl) {
orderNoteEl.disabled = true;
orderNoteEl.value = '';
orderNoteEl.placeholder = '已停用';
orderNoteEl.style.display = 'none';
}
const createTypeSelect = $('#createType');
if (createTypeSelect) {
createTypeSelect.disabled = true;
createTypeSelect.style.display = 'none';
}
};
const validateHolderName = (value) => {
const holderName = String(value || '').trim();
if (!holderName) return '请输入持卡人姓名';
if (holderName.length > 24) return '持卡人姓名不能超过 24 个字符';
if (!HOLDER_NAME_PATTERN.test(holderName)) return '持卡人姓名仅支持英文与常用符号';
return '';
};
const validateInitialBalance = () => {
const initialBalance = getInitialBalance();
if (!Number.isFinite(initialBalance) || initialBalance < 1) {
return '首次充值金额必须大于 0';
}
return '';
};
const renderEstimate = () => {
const initialBalance = getInitialBalance();
if (!Number.isFinite(initialBalance) || initialBalance < 1) {
estimateBoxEl.innerHTML = '<div class="jr-center-empty"><p>请选择有效的首次充值金额。</p></div>';
return;
}
estimateBoxEl.innerHTML = `
<div class="list-item"><span class="k">卡片类型</span><span class="v">IC 储值卡</span></div>
<div class="list-item"><span class="k">首次充值</span><span class="v">${escapeHtml(initialBalance)}</span></div>
<div class="list-item"><span class="k">持卡人限制</span><span class="v">仅英文与常用符号</span></div>
<div class="list-item jr-total-row"><span class="k">预计金额</span><span class="v jr-total-amount">${escapeHtml(initialBalance)}</span></div>
`;
};
const renderResult = (data) => {
if (!data) {
resultBoxEl.className = 'jr-center-empty';
resultBoxEl.innerHTML = '<p>提交后将在此显示卡号、凭证码和领卡提示。</p>';
return;
}
const isDomain = location.hostname.includes('fse-media.group');
const voucherCode = data.code || data.voucher_code || data.order_code || '---';
const cardStatus = String(data.card?.status || data.status || '').trim().toLowerCase();
const lookupKey = (cardStatus === 'pending_pickup')
? voucherCode
: (data.card?.card_id || data.card_id || voucherCode);
const searchHref = isDomain
? `https://ticket.fse-media.group/ic-card/search?q=${encodeURIComponent(lookupKey)}`
: `/ic-card-search.html?q=${encodeURIComponent(lookupKey)}`;
const detailHref = isDomain
? `https://ticket.fse-media.group/ic/${encodeURIComponent(lookupKey)}`
: `/ic/${encodeURIComponent(lookupKey)}`;
const shownCardId = data.display_card_id || data.card?.display_card_id || data.card_id || '---';
const amount = Number(data.amount ?? data.card?.purchase_amount ?? data.card?.balance ?? getInitialBalance()) || getInitialBalance();
resultBoxEl.className = '';
resultBoxEl.innerHTML = `
<div class="jr-redeem-summary jr-card-order-result">
<div class="jr-kicker">ORDER CREATED</div>
<div class="jr-redeem-code-row">
<span class="jr-redeem-code-label">凭证码</span>
<strong class="jr-redeem-code-value">${escapeHtml(voucherCode)}</strong>
</div>
<div class="jr-redeem-code-row" style="margin-top:12px;">
<span class="jr-redeem-code-label">卡号</span>
<strong class="jr-redeem-code-value jr-code-accent-secondary">${escapeHtml(shownCardId)}</strong>
</div>
<div class="jr-order-info-grid">
<div class="jr-order-info-item">
<span>首次充值</span>
<strong>${escapeHtml(amount)}</strong>
</div>
<div class="jr-order-info-item">
<span>当前状态</span>
<strong class="jr-code-accent-status">待领卡</strong>
</div>
</div>
<p class="jr-redeem-copy">购卡信息已生成。请保存卡号与凭证码,前往站内办理领卡或后续状态查询。</p>
<div class="jr-action-row">
<a class="btn jr-secondary-btn" href="${detailHref}"><i class="fas fa-id-card"></i> 卡片详情</a>
<a class="btn jr-secondary-btn" href="${searchHref}"><i class="fas fa-search"></i> 查询此卡</a>
</div>
</div>
`;
};
const submitOrder = async () => {
const holderNameError = validateHolderName(holderNameEl.value);
if (holderNameError) {
alert(holderNameError);
return;
}
const balanceError = validateInitialBalance();
if (balanceError) {
alert(balanceError);
return;
}
const payload = {
holder_name: holderNameEl.value.trim(),
initial_balance: getInitialBalance()
};
submitBtn.disabled = true;
try {
const data = await api.createOrder(payload);
renderResult(data);
alert(`购卡成功,凭证码:${data.code}`);
} finally {
submitBtn.disabled = false;
}
};
customInitialBalanceEl.addEventListener('input', renderEstimate);
submitBtn.addEventListener('click', () => submitOrder().catch((error) => alert(error.message || String(error))));
(async () => {
syncLegacyDom();
const data = await api.fetchConfig();
state.rechargeOptions = (data.recharge_options || data.initial_balance_options || [5, 10, 15, 20])
.map((value) => Number(value) || 0)
.filter((value) => value > 0);
state.selectedAmount = state.rechargeOptions[0] || 5;
customInitialBalanceEl.disabled = true;
if (customRechargeBoxEl) customRechargeBoxEl.classList.remove('is-active');
renderRechargeOptions();
renderEstimate();
renderResult(null);
})().catch((error) => {
rechargeOptionListEl.innerHTML = `<div class="jr-center-empty"><p>${escapeHtml(error.message || String(error))}</p></div>`;
estimateBoxEl.innerHTML = '<div class="jr-center-empty"><p>配置加载失败。</p></div>';
});
})();