Files
lunar-mini/server/web/static/js/pages/Orders.js
T
gouki deb32f8fab
Build and Publish Server / build (push) Successful in 4m30s
feat(server): 商业化支付与订单系统 + Harness 改进
- 新增商业化模块:Stripe/Binance Pay 支付、邮箱登录、订单管理
- 新增数据模型:ReportProduct, ReportEntitlement, ReportSnapshot, PaymentEvent, EmailLoginCode
- 新增 API:/api/commerce/checkout, /api/commerce/webhooks/*, /api/commerce/orders/*
- 新增管理后台:订单列表、统计图表、统一 AdminHeader 组件
- 新增中间件测试:RateLimit, Auth, CORS 共 7 个测试用例
- 新增 AGENTS.md 项目代理指令文件
- 新增 REMINDER.md 商业化功能开发规范
- 修复 JWT 解析:添加 WithValidMethods 限制 HS256
- 修复 GetPayStatus:增加用户权限校验
- 更新 .gitignore 和 README.md
2026-09-19 18:57:23 +00:00

95 lines
7.6 KiB
JavaScript

import { onMounted, ref } from 'vue';
import { adminHeader } from './AdminHeader.js';
export default {
setup() {
const orders = ref([]);
const stats = ref({ totalOrders: 0, paidOrders: 0, pendingOrders: 0, refundedOrders: 0, revenueUsdCents: 0 });
const total = ref(0);
const loading = ref(true);
const error = ref('');
const filters = ref({ status: '', provider: '', search: '' });
const load = async () => {
loading.value = true;
error.value = '';
const params = new URLSearchParams({ page: '1', pageSize: '100' });
Object.entries(filters.value).forEach(([key, value]) => value && params.set(key, value));
try {
const [orderResponse, statsResponse] = await Promise.all([
fetch('/admin/api/orders?' + params.toString(), { credentials: 'same-origin' }),
fetch('/admin/api/orders/stats', { credentials: 'same-origin' }),
]);
if (!orderResponse.ok || !statsResponse.ok) throw new Error('加载订单失败');
const orderPayload = await orderResponse.json();
const statsPayload = await statsResponse.json();
orders.value = orderPayload.data.list || [];
total.value = orderPayload.data.total || 0;
stats.value = statsPayload.data;
} catch (reason) {
error.value = reason instanceof Error ? reason.message : '加载订单失败';
} finally {
loading.value = false;
}
};
const money = order => order.currency === 'USD'
? '$' + (order.amount / 100).toFixed(2)
: order.providerAmount + ' ' + order.currency;
const dateTime = value => value ? new Date(value).toLocaleString('zh-CN', { hour12: false }) : '-';
const statusText = status => ({ pending: '待支付', paid: '已支付', cancelled: '已关闭', refunded: '已退款' }[status] || status);
const statusClass = status => ({
paid: 'bg-green-100 text-green-800', pending: 'bg-amber-100 text-amber-800',
cancelled: 'bg-gray-100 text-gray-700', refunded: 'bg-blue-100 text-blue-800',
}[status] || 'bg-gray-100 text-gray-700');
const providerText = provider => ({ stripe: 'Stripe', binance_pay: 'Binance Pay' }[provider] || provider || '-');
onMounted(load);
return { orders, stats, total, loading, error, filters, load, money, dateTime, statusText, statusClass, providerText };
},
template: `
<div class="min-h-screen bg-gray-100">
${adminHeader('orders')}
<main class="max-w-7xl mx-auto py-8 px-4 sm:px-6 lg:px-8">
<div class="grid grid-cols-2 lg:grid-cols-5 gap-4 mb-6">
<div class="bg-white p-4 rounded-lg shadow-sm"><p class="text-xs text-gray-500">报告订单</p><b class="text-2xl">{{ stats.totalOrders }}</b></div>
<div class="bg-white p-4 rounded-lg shadow-sm"><p class="text-xs text-gray-500">已支付</p><b class="text-2xl text-green-700">{{ stats.paidOrders }}</b></div>
<div class="bg-white p-4 rounded-lg shadow-sm"><p class="text-xs text-gray-500">待支付</p><b class="text-2xl text-amber-700">{{ stats.pendingOrders }}</b></div>
<div class="bg-white p-4 rounded-lg shadow-sm"><p class="text-xs text-gray-500">已退款</p><b class="text-2xl text-blue-700">{{ stats.refundedOrders }}</b></div>
<div class="bg-white p-4 rounded-lg shadow-sm"><p class="text-xs text-gray-500">Stripe 收入</p><b class="text-2xl">\${{ (stats.revenueUsdCents / 100).toFixed(2) }}</b></div>
</div>
<div class="bg-white shadow rounded-lg">
<div class="p-5 border-b flex flex-col lg:flex-row gap-3 lg:items-center lg:justify-between">
<div><h2 class="text-lg font-medium">PC 八字报告订单</h2><p class="text-xs text-gray-500 mt-1">仅显示 PC 商业版八字报告订单,共 {{ total }} 笔</p></div>
<div class="flex flex-wrap gap-2">
<input v-model="filters.search" @keyup.enter="load" class="border rounded px-3 py-2 text-sm" placeholder="订单号 / 邮箱 / 报告号" />
<select v-model="filters.provider" @change="load" class="border rounded px-3 py-2 text-sm"><option value="">全部渠道</option><option value="stripe">Stripe</option><option value="binance_pay">Binance Pay</option></select>
<select v-model="filters.status" @change="load" class="border rounded px-3 py-2 text-sm"><option value="">全部状态</option><option value="pending">待支付</option><option value="paid">已支付</option><option value="cancelled">已关闭</option><option value="refunded">已退款</option></select>
<button @click="load" class="bg-red-700 text-white rounded px-4 py-2 text-sm">查询</button>
</div>
</div>
<div v-if="error" class="m-5 p-3 bg-red-50 text-red-700 text-sm">{{ error }}</div>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50"><tr><th class="px-4 py-3 text-left text-xs text-gray-500">订单 / 报告</th><th class="px-4 py-3 text-left text-xs text-gray-500">客户</th><th class="px-4 py-3 text-left text-xs text-gray-500">渠道</th><th class="px-4 py-3 text-left text-xs text-gray-500">金额</th><th class="px-4 py-3 text-left text-xs text-gray-500">状态</th><th class="px-4 py-3 text-left text-xs text-gray-500">渠道交易号</th><th class="px-4 py-3 text-left text-xs text-gray-500">时间</th></tr></thead>
<tbody class="divide-y divide-gray-100">
<tr v-if="loading"><td colspan="7" class="p-10 text-center text-sm text-gray-500">正在加载订单...</td></tr>
<tr v-else-if="orders.length === 0"><td colspan="7" class="p-10 text-center text-sm text-gray-500">暂无符合条件的订单</td></tr>
<tr v-for="order in orders" :key="order.id" class="hover:bg-gray-50">
<td class="px-4 py-4 text-xs"><b class="block text-gray-900">{{ order.orderNo }}</b><span class="block text-gray-500">{{ order.reportId || '-' }}</span><code v-if="order.accessCode" class="block mt-1 text-red-700">{{ order.accessCode }}</code></td>
<td class="px-4 py-4 text-sm">{{ order.customerEmail || '-' }}</td>
<td class="px-4 py-4 text-sm">{{ providerText(order.provider) }}</td>
<td class="px-4 py-4 text-sm font-medium">{{ money(order) }}</td>
<td class="px-4 py-4"><span :class="statusClass(order.status)" class="px-2 py-1 rounded-full text-xs font-medium">{{ statusText(order.status) }}</span></td>
<td class="px-4 py-4 text-xs text-gray-500 max-w-xs truncate" :title="order.providerTransactionId">{{ order.providerTransactionId || order.providerOrderId || '-' }}</td>
<td class="px-4 py-4 text-xs text-gray-500"><span class="block">创建 {{ dateTime(order.createdAt) }}</span><span class="block">支付 {{ dateTime(order.payTime) }}</span></td>
</tr>
</tbody>
</table>
</div>
</div>
</main>
</div>
`,
};