ishop/public/js/admin.js
2025-08-11 13:40:59 +08:00

782 lines
27 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 订单管理页面脚本
let currentOrders = [];
let currentCoupons = [];
let currentOrderId = null;
// DOM 元素
const ordersTableBody = document.getElementById('ordersTableBody');
const couponsTableBody = document.getElementById('couponsTableBody');
const searchInput = document.getElementById('searchOrder');
const statusFilter = document.getElementById('statusFilter');
const orderDetailModal = document.getElementById('orderDetailModal');
const shippingModal = document.getElementById('shippingModal');
const createCouponModal = document.getElementById('createCouponModal');
// 页面加载时初始化
document.addEventListener('DOMContentLoaded', function() {
loadOrders();
loadCoupons();
});
// 选项卡切换功能
function showTab(tabName) {
// 隐藏所有选项卡内容
const tabs = document.querySelectorAll('.tab-content');
tabs.forEach(tab => tab.classList.remove('active'));
// 移除所有按钮的活动状态
const buttons = document.querySelectorAll('.tab-btn');
buttons.forEach(btn => btn.classList.remove('active'));
// 显示选中的选项卡内容
document.getElementById(tabName + 'Tab').classList.add('active');
// 激活相应按钮
event.target.classList.add('active');
// 根据选项卡加载数据
if (tabName === 'coupons') {
loadCoupons();
} else if (tabName === 'orders') {
loadOrders();
}
}
// 加载订单列表
async function loadOrders(search = '', status = '') {
try {
showLoading();
const params = new URLSearchParams();
if (search) params.append('search', search);
if (status) params.append('status', status);
const response = await fetch(`/api/admin/orders?${params}`);
const data = await response.json();
if (response.ok) {
currentOrders = data.orders;
updateOrdersTable(data.orders);
updateStats(data.stats);
} else {
throw new Error(data.error || '加载订单失败');
}
} catch (error) {
console.error('Load orders error:', error);
showError('加载订单失败: ' + error.message);
}
}
// 更新订单表格
function updateOrdersTable(orders) {
if (!orders || orders.length === 0) {
ordersTableBody.innerHTML = '<tr><td colspan="8" class="loading">暂无订单数据</td></tr>';
return;
}
ordersTableBody.innerHTML = orders.map(order => `
<tr>
<td>
<strong>${order.order_id}</strong>
</td>
<td>
<div style="line-height: 1.4;">
<strong>${order.customer_name}</strong>
<button class="copy-btn" onclick="copyOrderInfo('${order.order_id}')" title="复制收货信息" style="margin-left: 5px; font-size: 12px;">📋</button><br>
<small style="color: #666;">${order.customer_phone || '未提供电话'}</small><br>
<small style="color: #666; font-size: 11px; max-width: 200px; display: inline-block; word-wrap: break-word;">${order.shipping_address ? (order.shipping_address.length > 30 ? order.shipping_address.substring(0, 30) + '...' : order.shipping_address) : '未提供地址'}</small>
</div>
</td>
<td>
<div>
${order.product_name}<br>
<small style="color: #888;">数量: ${order.quantity}</small>
</div>
</td>
<td>
<strong style="color: #ffd700;">$${order.total_amount.toFixed(2)}</strong>
</td>
<td>
<span class="status-badge status-${order.payment_status}">
${getStatusText(order.payment_status, 'payment')}
</span>
</td>
<td>
<span class="status-badge status-${order.shipping_status}">
${getStatusText(order.shipping_status, 'shipping')}
</span>
</td>
<td>
${formatDate(order.created_at)}
</td>
<td>
<button class="action-btn btn-ship" onclick="showShippingModal('${order.order_id}')">
📦 发货
</button>
<button class="action-btn btn-delete" onclick="confirmDeleteOrder('${order.order_id}')" title="删除订单">
🗑️ 删除
</button>
</td>
</tr>
`).join('');
}
// 更新统计数据
function updateStats(stats) {
if (stats) {
const totalElement = document.getElementById('totalOrders');
const pendingElement = document.getElementById('pendingOrders');
const shippedElement = document.getElementById('shippedOrders');
if (totalElement) totalElement.textContent = stats.total || 0;
if (pendingElement) pendingElement.textContent = stats.pending_ship || 0;
if (shippedElement) shippedElement.textContent = stats.shipped || 0;
}
}
// 搜索订单
function searchOrders() {
const search = searchInput.value.trim();
const status = statusFilter.value;
loadOrders(search, status);
}
// 筛选订单
function filterOrders() {
const search = searchInput.value.trim();
const status = statusFilter.value;
loadOrders(search, status);
}
// 显示订单详情
async function showOrderDetail(orderId) {
try {
const response = await fetch(`/api/orders/${orderId}`);
const order = await response.json();
if (response.ok) {
const detailContent = document.getElementById('orderDetailContent');
detailContent.innerHTML = `
<div class="detail-section">
<h4>基本信息</h4>
<div class="detail-row">
<span class="detail-label">订单号:</span>
<span class="detail-value">${order.order_id}</span>
</div>
<div class="detail-row">
<span class="detail-label">支付ID:</span>
<span class="detail-value">${order.payment_id || '未设置'}</span>
</div>
<div class="detail-row">
<span class="detail-label">创建时间:</span>
<span class="detail-value">${formatDate(order.created_at)}</span>
</div>
<div class="detail-row">
<span class="detail-label">更新时间:</span>
<span class="detail-value">${formatDate(order.updated_at)}</span>
</div>
</div>
<div class="detail-section">
<h4>客户信息</h4>
<div class="detail-row">
<span class="detail-label">姓名:</span>
<span class="detail-value">${order.customer_name}</span>
</div>
<div class="detail-row">
<span class="detail-label">邮箱:</span>
<span class="detail-value">${order.customer_email || '未提供'}</span>
</div>
<div class="detail-row">
<span class="detail-label">电话:</span>
<span class="detail-value">${order.customer_phone || '未提供'}</span>
</div>
<div class="detail-row">
<span class="detail-label">收货地址:</span>
<span class="detail-value">
${order.shipping_address}
<button class="copy-btn" onclick="copyText('${order.shipping_address.replace(/'/g, "\\'")}')">📋</button>
</span>
</div>
</div>
<div class="detail-section">
<h4>商品信息</h4>
<div class="detail-row">
<span class="detail-label">产品名称:</span>
<span class="detail-value">${order.product_name}</span>
</div>
<div class="detail-row">
<span class="detail-label">单价:</span>
<span class="detail-value">$${order.unit_price.toFixed(2)}</span>
</div>
<div class="detail-row">
<span class="detail-label">数量:</span>
<span class="detail-value">${order.quantity}</span>
</div>
<div class="detail-row">
<span class="detail-label">总金额:</span>
<span class="detail-value" style="color: #ffd700; font-weight: bold;">$${order.total_amount.toFixed(2)}</span>
</div>
</div>
<div class="detail-section">
<h4>状态信息</h4>
<div class="detail-row">
<span class="detail-label">支付状态:</span>
<span class="detail-value">
<span class="status-badge status-${order.payment_status}">
${getStatusText(order.payment_status, 'payment')}
</span>
</span>
</div>
<div class="detail-row">
<span class="detail-label">发货状态:</span>
<span class="detail-value">
<span class="status-badge status-${order.shipping_status}">
${getStatusText(order.shipping_status, 'shipping')}
</span>
</span>
</div>
${order.tracking_number ? `
<div class="detail-row">
<span class="detail-label">快递单号:</span>
<span class="detail-value">${order.tracking_number}</span>
</div>` : ''}
${order.shipping_notes ? `
<div class="detail-row">
<span class="detail-label">发货备注:</span>
<span class="detail-value">${order.shipping_notes}</span>
</div>` : ''}
</div>
`;
orderDetailModal.style.display = 'flex';
} else {
throw new Error(order.error || '获取订单详情失败');
}
} catch (error) {
console.error('Show order detail error:', error);
alert('获取订单详情失败: ' + error.message);
}
}
// 复制订单收货信息
function copyOrderInfo(orderId) {
const order = currentOrders.find(o => o.order_id === orderId);
if (!order) return;
// 组合收货信息:姓名, 电话, 地址
let infoText = order.customer_name;
if (order.customer_phone) {
infoText += ', ' + order.customer_phone;
}
infoText += ', ' + order.shipping_address;
// 复制到剪贴板
copyText(infoText);
}
// 显示发货模态框
function showShippingModal(orderId) {
currentOrderId = orderId;
const order = currentOrders.find(o => o.order_id === orderId);
if (!order) return;
// 清空表单
document.getElementById('trackingNumber').value = '';
document.getElementById('shippingNotes').value = '';
// 显示模态框
shippingModal.style.display = 'flex';
document.getElementById('trackingNumber').focus();
}
// 关闭发货模态框
function closeShippingModal() {
shippingModal.style.display = 'none';
currentOrderId = null;
}
// 确认发货
async function confirmShipping() {
const trackingNumber = document.getElementById('trackingNumber').value.trim();
const shippingNotes = document.getElementById('shippingNotes').value.trim();
if (!trackingNumber) {
alert('请输入运单号');
document.getElementById('trackingNumber').focus();
return;
}
if (!currentOrderId) {
alert('订单ID错误');
return;
}
try {
const response = await fetch(`/api/admin/orders/${currentOrderId}/shipping`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
shipping_status: 'shipped',
tracking_number: trackingNumber,
shipping_notes: shippingNotes
})
});
const result = await response.json();
if (response.ok) {
alert('订单已成功发货!');
closeShippingModal();
loadOrders(); // 重新加载订单列表
} else {
throw new Error(result.error || '发货失败');
}
} catch (error) {
console.error('Shipping error:', error);
alert('发货失败: ' + error.message);
}
}
// 确认并标记为已发货
function confirmMarkAsShipped(orderId) {
if (confirm('确认标记此订单为已发货吗?')) {
markAsShipped(orderId);
}
}
// 一键标记为已发货
async function markAsShipped(orderId) {
try {
const response = await fetch(`/api/admin/orders/${orderId}/shipping`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
shipping_status: 'shipped'
})
});
const result = await response.json();
if (response.ok) {
alert('订单已标记为发货!');
loadOrders(); // 重新加载订单列表
} else {
throw new Error(result.error || '标记发货失败');
}
} catch (error) {
console.error('Mark as shipped error:', error);
alert('标记发货失败: ' + error.message);
}
}
// 复制文本到剪贴板
async function copyText(text) {
try {
await navigator.clipboard.writeText(text);
// 显示复制成功提示
showCopySuccess();
} catch (err) {
// 降级方案:使用传统方法
const textArea = document.createElement('textarea');
textArea.value = text;
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
showCopySuccess();
}
}
// 显示复制成功提示
function showCopySuccess() {
// 创建提示元素
const toast = document.createElement('div');
toast.className = 'copy-toast';
toast.textContent = '已复制到剪贴板';
document.body.appendChild(toast);
// 3秒后移除提示
setTimeout(() => {
if (toast.parentNode) {
toast.parentNode.removeChild(toast);
}
}, 3000);
}
// 关闭订单详情模态框
function closeOrderDetail() {
orderDetailModal.style.display = 'none';
}
// 获取状态文本
function getStatusText(status, type) {
if (type === 'payment') {
switch(status) {
case 'pending': return '未支付';
case 'finished': return '已支付';
case 'failed': return '支付失败';
case 'confirming': return '确认中';
default: return status || '未知';
}
} else if (type === 'shipping') {
switch(status) {
case 'pending': return '未发货';
case 'shipped': return '已发货';
default: return status || '未知';
}
}
return status || '未知';
}
// 显示加载状态
function showLoading() {
ordersTableBody.innerHTML = '<tr><td colspan="8" class="loading">加载中...</td></tr>';
}
// 显示错误信息
function showError(message) {
ordersTableBody.innerHTML = `<tr><td colspan="8" class="loading" style="color: #ff4757;">错误: ${message}</td></tr>`;
}
// 格式化日期
function formatDate(dateString) {
if (!dateString) return '未设置';
const date = new Date(dateString);
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
}
// 加载优惠码列表
async function loadCoupons() {
try {
showCouponsLoading();
const response = await fetch('/api/admin/coupons');
const data = await response.json();
if (response.ok) {
currentCoupons = data.coupons;
updateCouponsTable(data.coupons);
} else {
throw new Error(data.error || '加载优惠码失败');
}
} catch (error) {
console.error('Load coupons error:', error);
showCouponsError('加载优惠码失败: ' + error.message);
}
}
// 更新优惠码表格
function updateCouponsTable(coupons) {
if (!coupons || coupons.length === 0) {
couponsTableBody.innerHTML = '<tr><td colspan="8" class="loading">暂无优惠码数据</td></tr>';
return;
}
couponsTableBody.innerHTML = coupons.map(coupon => `
<tr>
<td>
<strong style="font-family: monospace; color: #ffd700;">${coupon.code}</strong>
</td>
<td>${coupon.name}</td>
<td>
<strong style="color: #28a745;">
${coupon.discount_type === 'percentage' ? coupon.discount_value + '%' : '$' + coupon.discount_value.toFixed(2)}
</strong>
</td>
<td>
<span class="coupon-type coupon-${coupon.discount_type}">
${coupon.discount_type === 'percentage' ? '百分比' : '固定金额'}
</span>
</td>
<td>
<div style="font-size: 13px;">
已使用: <strong>${coupon.used_count}</strong> / ${coupon.max_uses === 999999 ? '无限' : coupon.max_uses}<br>
<small style="color: #888;">${coupon.is_reusable ? '可重复使用' : '一次性使用'}</small>
</div>
</td>
<td>
<span class="status-badge ${coupon.is_active ? 'coupon-active' : 'coupon-inactive'}">
${coupon.is_active ? '启用' : '禁用'}
</span>
</td>
<td>
${formatDate(coupon.created_at)}
</td>
<td>
<button class="action-btn ${coupon.is_active ? 'btn-disable' : 'btn-enable'}"
onclick="toggleCouponStatus(${coupon.id}, ${!coupon.is_active})"
title="${coupon.is_active ? '禁用' : '启用'}优惠码">
${coupon.is_active ? '禁用' : '启用'}
</button>
<button class="action-btn btn-delete" onclick="confirmDeleteCoupon(${coupon.id})" title="删除优惠码">
删除
</button>
</td>
</tr>
`).join('');
}
// 显示创建优惠码模态框
function showCreateCouponModal() {
// 清空表单
document.getElementById('couponCode').value = '';
document.getElementById('couponName').value = '';
document.getElementById('discountType').value = 'percentage';
document.getElementById('discountValue').value = '';
document.getElementById('isReusable').checked = false;
document.getElementById('maxUses').value = '1';
document.getElementById('maxUsesGroup').style.display = 'none';
onDiscountTypeChange(); // 更新提示文本
createCouponModal.style.display = 'flex';
document.getElementById('couponCode').focus();
}
// 关闭创建优惠码模态框
function closeCreateCouponModal() {
createCouponModal.style.display = 'none';
}
// 折扣类型变化处理
function onDiscountTypeChange() {
const discountType = document.getElementById('discountType').value;
const hint = document.getElementById('discountHint');
if (discountType === 'percentage') {
hint.textContent = '输入百分比数值10 表示10%折扣';
} else {
hint.textContent = '输入固定金额5 表示减5美元';
}
}
// 可重复使用变化处理
function onReusableChange() {
const isReusable = document.getElementById('isReusable').checked;
const maxUsesGroup = document.getElementById('maxUsesGroup');
if (isReusable) {
maxUsesGroup.style.display = 'block';
document.getElementById('maxUses').value = '10'; // 默认10次
} else {
maxUsesGroup.style.display = 'none';
document.getElementById('maxUses').value = '1';
}
}
// 创建优惠码
async function createCoupon() {
const code = document.getElementById('couponCode').value.trim();
const name = document.getElementById('couponName').value.trim();
const discountType = document.getElementById('discountType').value;
const discountValue = parseFloat(document.getElementById('discountValue').value);
const isReusable = document.getElementById('isReusable').checked;
const maxUses = parseInt(document.getElementById('maxUses').value);
// 验证表单
if (!code || !name || !discountValue) {
alert('请填写所有必填字段');
return;
}
if (discountValue <= 0) {
alert('折扣值必须大于0');
return;
}
if (discountType === 'percentage' && discountValue > 100) {
alert('百分比折扣不能超过100%');
return;
}
if (isReusable && (!maxUses || maxUses < 1)) {
alert('最大使用次数必须大于0');
return;
}
try {
console.log('Creating coupon with data:', {
code, name, discount_type: discountType, discount_value: discountValue, is_reusable: isReusable, max_uses: maxUses
});
const response = await fetch('/api/admin/coupons', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
code: code,
name: name,
discount_type: discountType,
discount_value: discountValue,
is_reusable: isReusable,
max_uses: maxUses
})
});
console.log('Response status:', response.status);
console.log('Response headers:', response.headers);
const responseText = await response.text();
console.log('Response text:', responseText);
let result;
try {
result = JSON.parse(responseText);
} catch (parseError) {
console.error('Failed to parse JSON:', parseError);
console.error('Response was:', responseText.substring(0, 200));
throw new Error('服务器返回了无效的响应格式');
}
if (response.ok) {
alert('优惠码创建成功!');
closeCreateCouponModal();
loadCoupons(); // 重新加载优惠码列表
} else {
throw new Error(result.error || '创建优惠码失败');
}
} catch (error) {
console.error('Create coupon error:', error);
alert('创建优惠码失败: ' + error.message);
}
}
// 切换优惠码状态
async function toggleCouponStatus(couponId, isActive) {
try {
const response = await fetch(`/api/admin/coupons/${couponId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
is_active: isActive
})
});
const result = await response.json();
if (response.ok) {
alert(`优惠码已${isActive ? '启用' : '禁用'}`);
loadCoupons(); // 重新加载优惠码列表
} else {
throw new Error(result.error || '更新优惠码状态失败');
}
} catch (error) {
console.error('Toggle coupon status error:', error);
alert('更新优惠码状态失败: ' + error.message);
}
}
// 确认删除优惠码
function confirmDeleteCoupon(couponId) {
const coupon = currentCoupons.find(c => c.id === couponId);
if (!coupon) {
alert('优惠码未找到');
return;
}
const confirmMessage = `确认删除以下优惠码吗?\n\n优惠码:${coupon.code}\n名称:${coupon.name}\n已使用次数:${coupon.used_count}\n\n⚠️ 此操作不可撤销!`;
if (confirm(confirmMessage)) {
deleteCoupon(couponId);
}
}
// 删除优惠码
async function deleteCoupon(couponId) {
try {
const response = await fetch(`/api/admin/coupons/${couponId}`, {
method: 'DELETE'
});
const result = await response.json();
if (response.ok) {
alert('优惠码已成功删除!');
loadCoupons(); // 重新加载优惠码列表
} else {
throw new Error(result.error || '删除优惠码失败');
}
} catch (error) {
console.error('Delete coupon error:', error);
alert('删除优惠码失败: ' + error.message);
}
}
// 显示优惠码加载状态
function showCouponsLoading() {
couponsTableBody.innerHTML = '<tr><td colspan="8" class="loading">加载中...</td></tr>';
}
// 显示优惠码错误信息
function showCouponsError(message) {
couponsTableBody.innerHTML = `<tr><td colspan="8" class="loading" style="color: #ff4757;">错误: ${message}</td></tr>`;
}
// 确认删除订单
function confirmDeleteOrder(orderId) {
const order = currentOrders.find(o => o.order_id === orderId);
if (!order) {
alert('订单未找到');
return;
}
const confirmMessage = `确认删除以下订单吗?\n\n订单号:${orderId}\n客户:${order.customer_name}\n金额:$${order.total_amount.toFixed(2)} USDT\n\n⚠️ 此操作不可撤销!`;
if (confirm(confirmMessage)) {
deleteOrder(orderId);
}
}
// 删除订单
async function deleteOrder(orderId) {
try {
const response = await fetch(`/api/admin/orders/${orderId}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
}
});
const result = await response.json();
if (response.ok) {
alert('订单已成功删除!');
loadOrders(); // 重新加载订单列表
} else {
throw new Error(result.error || '删除订单失败');
}
} catch (error) {
console.error('Delete order error:', error);
alert('删除订单失败: ' + error.message);
}
}
// 键盘事件监听
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
closeOrderDetail();
closeShippingModal();
}
});
// 搜索框回车事件
searchInput.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
searchOrders();
}
});