|
|
@@ -495,12 +495,169 @@ body::before {
|
|
|
}
|
|
|
.error-bar.show { display: block; }
|
|
|
|
|
|
+/* ─── Order History ─── */
|
|
|
+.order-history {
|
|
|
+ margin-top: 36px;
|
|
|
+ padding-top: 28px;
|
|
|
+ border-top: 1px solid rgba(0,212,255,0.08);
|
|
|
+}
|
|
|
+.order-history .section-header {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ margin-bottom: 16px;
|
|
|
+}
|
|
|
+.order-history .section-header .section-title {
|
|
|
+ margin-bottom: 0;
|
|
|
+}
|
|
|
+.order-history .refresh-btn {
|
|
|
+ background: none;
|
|
|
+ border: 1px solid rgba(0,212,255,0.2);
|
|
|
+ color: var(--neon-dim);
|
|
|
+ font-size: 12px;
|
|
|
+ padding: 4px 12px;
|
|
|
+ border-radius: 6px;
|
|
|
+ cursor: pointer;
|
|
|
+ transition: all 0.2s;
|
|
|
+}
|
|
|
+.order-history .refresh-btn:hover {
|
|
|
+ border-color: var(--neon);
|
|
|
+ color: var(--neon);
|
|
|
+ background: var(--neon-glow);
|
|
|
+}
|
|
|
+.order-history .refresh-btn:disabled {
|
|
|
+ opacity: 0.4;
|
|
|
+ cursor: not-allowed;
|
|
|
+}
|
|
|
+
|
|
|
+.order-table {
|
|
|
+ width: 100%;
|
|
|
+ border-collapse: separate;
|
|
|
+ border-spacing: 0;
|
|
|
+ font-size: 13px;
|
|
|
+}
|
|
|
+.order-table thead th {
|
|
|
+ text-align: left;
|
|
|
+ padding: 10px 12px;
|
|
|
+ color: var(--text-dim);
|
|
|
+ font-weight: 500;
|
|
|
+ font-size: 12px;
|
|
|
+ letter-spacing: 0.5px;
|
|
|
+ border-bottom: 1px solid rgba(255,255,255,0.06);
|
|
|
+ white-space: nowrap;
|
|
|
+}
|
|
|
+.order-table tbody tr {
|
|
|
+ transition: background 0.15s;
|
|
|
+}
|
|
|
+.order-table tbody tr:hover {
|
|
|
+ background: rgba(0,212,255,0.03);
|
|
|
+}
|
|
|
+.order-table tbody td {
|
|
|
+ padding: 12px 12px;
|
|
|
+ border-bottom: 1px solid rgba(255,255,255,0.03);
|
|
|
+ color: var(--text);
|
|
|
+ vertical-align: middle;
|
|
|
+}
|
|
|
+.order-table tbody tr:last-child td {
|
|
|
+ border-bottom: none;
|
|
|
+}
|
|
|
+.order-table .col-time {
|
|
|
+ color: var(--text-dim);
|
|
|
+ font-size: 12px;
|
|
|
+ white-space: nowrap;
|
|
|
+}
|
|
|
+.order-table .col-amount {
|
|
|
+ font-weight: 600;
|
|
|
+ color: var(--neon);
|
|
|
+ white-space: nowrap;
|
|
|
+}
|
|
|
+.order-table .col-count {
|
|
|
+ white-space: nowrap;
|
|
|
+}
|
|
|
+
|
|
|
+.order-status {
|
|
|
+ display: inline-block;
|
|
|
+ font-size: 11px;
|
|
|
+ font-weight: 500;
|
|
|
+ padding: 2px 8px;
|
|
|
+ border-radius: 4px;
|
|
|
+ white-space: nowrap;
|
|
|
+}
|
|
|
+.order-status.status-paid {
|
|
|
+ color: var(--success);
|
|
|
+ background: rgba(0,255,136,0.08);
|
|
|
+ border: 1px solid rgba(0,255,136,0.2);
|
|
|
+}
|
|
|
+.order-status.status-pending {
|
|
|
+ color: #FFB800;
|
|
|
+ background: rgba(255,184,0,0.08);
|
|
|
+ border: 1px solid rgba(255,184,0,0.2);
|
|
|
+}
|
|
|
+.order-status.status-failed {
|
|
|
+ color: var(--danger);
|
|
|
+ background: rgba(255,77,106,0.08);
|
|
|
+ border: 1px solid rgba(255,77,106,0.2);
|
|
|
+}
|
|
|
+.order-status.status-recharged {
|
|
|
+ color: var(--neon);
|
|
|
+ background: var(--neon-glow);
|
|
|
+ border: 1px solid rgba(0,212,255,0.2);
|
|
|
+}
|
|
|
+
|
|
|
+.order-empty {
|
|
|
+ text-align: center;
|
|
|
+ padding: 32px 16px;
|
|
|
+ color: var(--text-dim);
|
|
|
+ font-size: 13px;
|
|
|
+}
|
|
|
+.order-empty svg {
|
|
|
+ display: block;
|
|
|
+ margin: 0 auto 12px;
|
|
|
+ opacity: 0.3;
|
|
|
+}
|
|
|
+.order-loading {
|
|
|
+ text-align: center;
|
|
|
+ padding: 24px;
|
|
|
+ color: var(--text-dim);
|
|
|
+ font-size: 13px;
|
|
|
+}
|
|
|
+.order-loading .polling-dot {
|
|
|
+ margin-right: 6px;
|
|
|
+}
|
|
|
+.order-error {
|
|
|
+ text-align: center;
|
|
|
+ padding: 16px;
|
|
|
+ color: var(--text-dim);
|
|
|
+ font-size: 12px;
|
|
|
+}
|
|
|
+.order-more-btn {
|
|
|
+ display: block;
|
|
|
+ width: 100%;
|
|
|
+ padding: 10px;
|
|
|
+ margin-top: 8px;
|
|
|
+ background: none;
|
|
|
+ border: 1px solid rgba(255,255,255,0.06);
|
|
|
+ border-radius: 8px;
|
|
|
+ color: var(--text-dim);
|
|
|
+ font-size: 12px;
|
|
|
+ cursor: pointer;
|
|
|
+ transition: all 0.2s;
|
|
|
+}
|
|
|
+.order-more-btn:hover {
|
|
|
+ border-color: rgba(0,212,255,0.2);
|
|
|
+ color: var(--neon-dim);
|
|
|
+ background: rgba(0,212,255,0.03);
|
|
|
+}
|
|
|
+
|
|
|
/* ─── Responsive ─── */
|
|
|
@media (max-width: 480px) {
|
|
|
.main-container { padding: 20px 16px; }
|
|
|
.api-info-card { padding: 20px; }
|
|
|
.tier-grid { grid-template-columns: 1fr; }
|
|
|
.modal-box { padding: 24px 20px; }
|
|
|
+ .order-table { font-size: 12px; }
|
|
|
+ .order-table thead th,
|
|
|
+ .order-table tbody td { padding: 8px 6px; }
|
|
|
}
|
|
|
</style>
|
|
|
</head>
|
|
|
@@ -564,6 +721,19 @@ body::before {
|
|
|
<span class="btn-icon">💳</span> 微信支付
|
|
|
</button>
|
|
|
|
|
|
+ <!-- Order History -->
|
|
|
+ <div class="order-history" id="orderHistory" style="display:none;">
|
|
|
+ <div class="section-header">
|
|
|
+ <div class="section-title">历史订单</div>
|
|
|
+ <button class="refresh-btn" id="refreshOrdersBtn" onclick="loadOrderHistory()">刷新</button>
|
|
|
+ </div>
|
|
|
+ <div class="neu-groove" style="padding:2px 16px;overflow-x:auto;">
|
|
|
+ <div id="orderListBody">
|
|
|
+ <div class="order-loading"><span class="polling-dot"></span> 加载中...</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
<!-- Success Screen -->
|
|
|
<div class="success-screen" id="successScreen">
|
|
|
<div class="success-icon">
|
|
|
@@ -640,6 +810,7 @@ async function loadApig() {
|
|
|
applyTestTier();
|
|
|
renderApigInfo();
|
|
|
renderTiers();
|
|
|
+ loadOrderHistory();
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
@@ -659,6 +830,7 @@ async function loadApig() {
|
|
|
applyTestTier();
|
|
|
renderApigInfo();
|
|
|
renderTiers();
|
|
|
+ loadOrderHistory();
|
|
|
return;
|
|
|
}
|
|
|
}
|
|
|
@@ -681,6 +853,7 @@ async function loadApig() {
|
|
|
applyTestTier();
|
|
|
renderApigInfo();
|
|
|
renderTiers();
|
|
|
+ loadOrderHistory();
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
@@ -905,6 +1078,8 @@ async function doRecharge() {
|
|
|
} catch (e) {
|
|
|
console.warn('Recharge call failed (non-critical):', e.message);
|
|
|
}
|
|
|
+ // Refresh order history to show the new order
|
|
|
+ loadOrderHistory();
|
|
|
}
|
|
|
|
|
|
async function refreshBalance() {
|
|
|
@@ -999,6 +1174,153 @@ function cancelPayment() {
|
|
|
document.getElementById('qrModal').classList.remove('show');
|
|
|
}
|
|
|
|
|
|
+// ─── Order History ───
|
|
|
+let orderSkip = 0;
|
|
|
+const ORDER_PAGE_SIZE = 10;
|
|
|
+
|
|
|
+async function loadOrderHistory(append) {
|
|
|
+ const container = document.getElementById('orderListBody');
|
|
|
+ const section = document.getElementById('orderHistory');
|
|
|
+ const refreshBtn = document.getElementById('refreshOrdersBtn');
|
|
|
+ section.style.display = '';
|
|
|
+ if (refreshBtn) refreshBtn.disabled = true;
|
|
|
+
|
|
|
+ if (!append) {
|
|
|
+ orderSkip = 0;
|
|
|
+ container.innerHTML = '<div class="order-loading"><span class="polling-dot"></span> 加载中...</div>';
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ // Query APIGOrder class via Parse REST API
|
|
|
+ // Orders link to user via fromCompany/fromUser Pointer whose objectId = userId or authId
|
|
|
+ const userVal = userId || authId;
|
|
|
+ const where = JSON.stringify({
|
|
|
+ '$or': [
|
|
|
+ { fromCompany: { __type: 'Pointer', className: 'Company', objectId: userVal } },
|
|
|
+ { fromUser: { __type: 'Pointer', className: '_User', objectId: userVal } }
|
|
|
+ ]
|
|
|
+ });
|
|
|
+ const qs = new URLSearchParams({
|
|
|
+ where: where,
|
|
|
+ order: '-createdAt',
|
|
|
+ limit: String(ORDER_PAGE_SIZE),
|
|
|
+ skip: String(orderSkip)
|
|
|
+ });
|
|
|
+ const resp = await fetch(API_BASE + '/parse/classes/APIGOrder?' + qs.toString(), {
|
|
|
+ headers: {
|
|
|
+ 'X-Parse-Application-Id': APP_ID
|
|
|
+ }
|
|
|
+ });
|
|
|
+ if (!resp.ok) throw new Error('HTTP ' + resp.status);
|
|
|
+ const data = await resp.json();
|
|
|
+ const orders = data.results || [];
|
|
|
+ console.log('Order history loaded:', orders.length, 'skip:', orderSkip);
|
|
|
+
|
|
|
+ if (!append) {
|
|
|
+ if (orders.length === 0) {
|
|
|
+ container.innerHTML =
|
|
|
+ '<div class="order-empty">' +
|
|
|
+ '<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M9 5H7a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2"/><rect x="9" y="3" width="6" height="4" rx="1"/></svg>' +
|
|
|
+ '暂无订单记录</div>';
|
|
|
+ if (refreshBtn) refreshBtn.disabled = false;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ container.innerHTML = '';
|
|
|
+ // Build table
|
|
|
+ const table = document.createElement('table');
|
|
|
+ table.className = 'order-table';
|
|
|
+ table.innerHTML =
|
|
|
+ '<thead><tr>' +
|
|
|
+ '<th>时间</th>' +
|
|
|
+ '<th>套餐</th>' +
|
|
|
+ '<th>金额</th>' +
|
|
|
+ '<th>状态</th>' +
|
|
|
+ '</tr></thead>';
|
|
|
+ const tbody = document.createElement('tbody');
|
|
|
+ tbody.id = 'orderTbody';
|
|
|
+ table.appendChild(tbody);
|
|
|
+ container.appendChild(table);
|
|
|
+ }
|
|
|
+
|
|
|
+ const tbody = document.getElementById('orderTbody');
|
|
|
+ orders.forEach(o => {
|
|
|
+ const tr = document.createElement('tr');
|
|
|
+ tr.innerHTML = formatOrderRow(o);
|
|
|
+ tbody.appendChild(tr);
|
|
|
+ });
|
|
|
+
|
|
|
+ // Remove old "load more" button
|
|
|
+ const oldMore = container.querySelector('.order-more-btn');
|
|
|
+ if (oldMore) oldMore.remove();
|
|
|
+
|
|
|
+ // Show "load more" if there might be more records
|
|
|
+ if (orders.length >= ORDER_PAGE_SIZE) {
|
|
|
+ orderSkip += ORDER_PAGE_SIZE;
|
|
|
+ const moreBtn = document.createElement('button');
|
|
|
+ moreBtn.className = 'order-more-btn';
|
|
|
+ moreBtn.textContent = '加载更多...';
|
|
|
+ moreBtn.onclick = () => loadOrderHistory(true);
|
|
|
+ container.appendChild(moreBtn);
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ console.warn('Order history error:', e);
|
|
|
+ if (!append) {
|
|
|
+ container.innerHTML =
|
|
|
+ '<div class="order-error">订单加载失败: ' + e.message + '</div>';
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (refreshBtn) refreshBtn.disabled = false;
|
|
|
+}
|
|
|
+
|
|
|
+function formatOrderRow(o) {
|
|
|
+ // Time
|
|
|
+ const dt = o.createdAt ? new Date(o.createdAt) : null;
|
|
|
+ const timeStr = dt
|
|
|
+ ? dt.getFullYear() + '-' +
|
|
|
+ String(dt.getMonth() + 1).padStart(2, '0') + '-' +
|
|
|
+ String(dt.getDate()).padStart(2, '0') + ' ' +
|
|
|
+ String(dt.getHours()).padStart(2, '0') + ':' +
|
|
|
+ String(dt.getMinutes()).padStart(2, '0')
|
|
|
+ : '—';
|
|
|
+
|
|
|
+ // Count — from detail.addCount or count field
|
|
|
+ const count = (o.detail && o.detail.addCount) || o.count || '—';
|
|
|
+ const unitLabel = (apig && (apig.objectId === 'MYM5zJBKgw' || apig.objectId === 'FQtTgjcqIZ')) ? 'token' : '次';
|
|
|
+ const countStr = count !== '—' ? count + ' ' + unitLabel : '—';
|
|
|
+
|
|
|
+ // Amount — from price field
|
|
|
+ const fee = o.price != null ? o.price : null;
|
|
|
+ const amountStr = fee != null ? '¥' + fee : '—';
|
|
|
+
|
|
|
+ // Status
|
|
|
+ const status = resolveOrderStatus(o);
|
|
|
+
|
|
|
+ return '<td class="col-time">' + timeStr + '</td>' +
|
|
|
+ '<td class="col-count">' + countStr + '</td>' +
|
|
|
+ '<td class="col-amount">' + amountStr + '</td>' +
|
|
|
+ '<td>' + status + '</td>';
|
|
|
+}
|
|
|
+
|
|
|
+function resolveOrderStatus(o) {
|
|
|
+ // isPay is the primary payment status flag in APIGOrder
|
|
|
+ if (o.isPay === true) {
|
|
|
+ // Check if recharge was applied (detail.oldCount exists and addCount exists)
|
|
|
+ if (o.detail && o.detail.addCount && o.detail.oldCount != null) {
|
|
|
+ return '<span class="order-status status-recharged">已充值</span>';
|
|
|
+ }
|
|
|
+ return '<span class="order-status status-paid">已支付</span>';
|
|
|
+ }
|
|
|
+ if (o.isPay === false) {
|
|
|
+ // Unpaid — check if expired (>30 min old)
|
|
|
+ const age = Date.now() - new Date(o.createdAt).getTime();
|
|
|
+ if (age > 30 * 60 * 1000) {
|
|
|
+ return '<span class="order-status status-failed">已过期</span>';
|
|
|
+ }
|
|
|
+ return '<span class="order-status status-pending">待支付</span>';
|
|
|
+ }
|
|
|
+ return '<span class="order-status status-pending">未知</span>';
|
|
|
+}
|
|
|
+
|
|
|
// ─── Helpers ───
|
|
|
function showError(msg) {
|
|
|
const el = document.getElementById('errorBar');
|