256 lines
11 KiB
JavaScript
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, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''');
|
|
|
|
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>';
|
|
});
|
|
})();
|
|
|