This commit is contained in:
公司git 2026-02-27 10:36:11 +08:00
commit 6c3731097a
11 changed files with 431 additions and 56 deletions

View File

@ -28,11 +28,23 @@ pip install -r requirements.txt
``` ```
### 2. 启动服务 ### 2. 启动服务
**开发环境**
```bash ```bash
python app.py python app.py
``` ```
服务默认运行在 `http://127.0.0.1:5000` 服务默认运行在 `http://127.0.0.1:5000`
**生产环境 (Gunicorn)**
在 Linux 生产环境中,建议使用 Gunicorn 作为 WSGI 服务器以获得更好的性能和稳定性:
```bash
pip install gunicorn # 如果尚未安装
gunicorn -w 4 -b 0.0.0.0:5000 app:app
```
* `-w 4`: 使用 4 个工作进程(通常设为 CPU 核心数 * 2 + 1
* `-b 0.0.0.0:5000`: 绑定所有 IP 且端口为 5000。
* `app:app`: 加载 `app.py` 中的 `app` 实例。
--- ---
## 🛠️ 常用维护命令 ## 🛠️ 常用维护命令

5
app.py
View File

@ -156,6 +156,11 @@ def create_app():
def video_page(): def video_page():
return render_template('video.html') return render_template('video.html')
@app.route('/admin/orders/<int:order_id>')
def order_detail(order_id):
# 权限检查可以在这里做,或者让模板里的 JS 向 API 请求时做 (API 已有权限检查)
return render_template('order_detail.html')
@app.route('/files/<path:filename>') @app.route('/files/<path:filename>')
def get_file(filename): def get_file(filename):
"""Proxy route to serve files from MinIO via the backend""" """Proxy route to serve files from MinIO via the backend"""

View File

@ -1,8 +1,8 @@
from flask import Blueprint, request, jsonify from flask import Blueprint, request, jsonify, g
from datetime import timedelta from datetime import timedelta
from extensions import db from extensions import db
from models import User, Role, Permission, SystemDict, SystemNotification, Order, PointsGrant, InviteReward, to_bj_time, get_bj_now from models import User, Role, Permission, SystemDict, SystemNotification, Order, PointsGrant, InviteReward, to_bj_time, get_bj_now
from middlewares.auth import permission_required from middlewares.auth import permission_required, login_required
from services.logger import system_logger from services.logger import system_logger
admin_bp = Blueprint('admin', __name__, url_prefix='/api/admin') admin_bp = Blueprint('admin', __name__, url_prefix='/api/admin')
@ -303,6 +303,40 @@ def get_orders():
} for o in orders] } for o in orders]
}) })
@admin_bp.route('/orders/<int:order_id>', methods=['GET'])
@login_required
def get_order_detail(order_id):
order = db.session.get(Order, order_id)
if not order:
return jsonify({"error": "订单不存在"}), 404
# 权限检查:必须是管理员或者订单所有者
if not g.user.has_permission('manage_system') and order.user_id != g.user.id:
return jsonify({"error": "无权访问此订单"}), 403
user = order.user
return jsonify({
"order": {
"id": order.id,
"out_trade_no": order.out_trade_no,
"trade_no": order.trade_no,
"amount": float(order.amount),
"points": order.points,
"status": order.status,
"created_at": order.created_at_bj.strftime('%Y-%m-%d %H:%M:%S'),
"paid_at": order.paid_at_bj.strftime('%Y-%m-%d %H:%M:%S') if order.paid_at else None
},
"buyer": {
"id": user.id,
"phone": user.phone,
"current_points": user.points,
"created_at": user.created_at_bj.strftime('%Y-%m-%d %H:%M:%S'),
"is_banned": user.is_banned,
"role": user.role.name if user.role else "普通用户"
},
"current_is_admin": g.user.has_permission('manage_system')
})
# --- 积分发放管理 --- # --- 积分发放管理 ---
@admin_bp.route('/points/grant', methods=['POST']) @admin_bp.route('/points/grant', methods=['POST'])
@permission_required('manage_system') @permission_required('manage_system')

View File

@ -131,6 +131,13 @@ def video_generate():
"aspect_ratio": data.get('aspect_ratio', '9:16') "aspect_ratio": data.get('aspect_ratio', '9:16')
} }
# 处理 Base64 图片 (去掉 header)
if payload.get("images"):
payload["images"] = [
img.split(',', 1)[1] if ',' in img else img
for img in payload["images"]
]
# 4. 启动异步视频任务 # 4. 启动异步视频任务
app = current_app._get_current_object() app = current_app._get_current_object()
task_id = start_async_video_task(app, user_id, payload, cost, model_value) task_id = start_async_video_task(app, user_id, payload, cost, model_value)

Binary file not shown.

View File

@ -359,10 +359,14 @@ def process_video_generation(app, user_id, internal_task_id, payload, api_key, c
elif 'url' in poll_result: elif 'url' in poll_result:
video_url = poll_result['url'] video_url = poll_result['url']
break break
elif status in ['FAILED', 'ERROR']: elif status in ['FAILURE', 'FAILED', 'ERROR']:
raise Exception(f"视频生成失败: {poll_result.get('fail_reason') or poll_result.get('message') or '未知错误'}") reason = poll_result.get('fail_reason') or poll_result.get('message') or '未知错误'
raise Exception(f"视频生成失败: {reason}")
if not video_url: if not video_url:
if status in ['FAILURE', 'FAILED', 'ERROR']: # 防止循环结束时正好是失败状态但未抛出的极端情况
reason = poll_result.get('fail_reason') or poll_result.get('message') or '未知错误'
raise Exception(f"视频生成失败: {reason}")
raise Exception("超时未获取到视频地址") raise Exception("超时未获取到视频地址")
# 3. 持久化记录 # 3. 持久化记录

View File

@ -149,32 +149,72 @@ document.addEventListener('DOMContentLoaded', () => {
showToast('已应用提示词模板', 'success'); showToast('已应用提示词模板', 'success');
}; };
// 上传图片逻辑 // 图片处理辅助函数
const processImageFile = (file) => new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => {
const img = new Image();
img.onload = () => {
const maxDim = 2048;
let w = img.width;
let h = img.height;
if (w <= maxDim && h <= maxDim) {
resolve(e.target.result);
return;
}
if (w > h) {
if (w > maxDim) {
h = Math.round(h * (maxDim / w));
w = maxDim;
}
} else {
if (h > maxDim) {
w = Math.round(w * (maxDim / h));
h = maxDim;
}
}
const canvas = document.createElement('canvas');
canvas.width = w;
canvas.height = h;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0, w, h);
// 保持原格式,如果不是 png 则默认 jpeg (0.9 质量)
const outType = file.type === 'image/png' ? 'image/png' : 'image/jpeg';
resolve(canvas.toDataURL(outType, 0.9));
};
img.onerror = reject;
img.src = e.target.result;
};
reader.onerror = reject;
reader.readAsDataURL(file);
});
// 上传图片逻辑 (改为纯前端 Base64 处理)
fileInput.onchange = async (e) => { fileInput.onchange = async (e) => {
const files = e.target.files; const files = e.target.files;
if (files.length === 0) return; if (files.length === 0) return;
const formData = new FormData();
formData.append('images', files[0]);
try { try {
submitBtn.disabled = true; submitBtn.disabled = true;
const r = await fetch('/api/upload', { method: 'POST', body: formData }); // 压缩并转换为 Base64
const d = await r.json(); uploadedImageUrl = await processImageFile(files[0]);
if (d.urls && d.urls.length > 0) {
uploadedImageUrl = d.urls[0]; imagePreview.innerHTML = `
imagePreview.innerHTML = ` <div class="relative w-20 h-20 rounded-xl overflow-hidden border border-indigo-200 shadow-sm group">
<div class="relative w-20 h-20 rounded-xl overflow-hidden border border-indigo-200 shadow-sm group"> <img src="${uploadedImageUrl}" class="w-full h-full object-cover">
<img src="${uploadedImageUrl}" class="w-full h-full object-cover"> <div class="absolute inset-0 bg-black/40 hidden group-hover:flex items-center justify-center cursor-pointer" onclick="removeImage()">
<div class="absolute inset-0 bg-black/40 hidden group-hover:flex items-center justify-center cursor-pointer" onclick="removeImage()"> <i data-lucide="x" class="w-5 h-5 text-white"></i>
<i data-lucide="x" class="w-5 h-5 text-white"></i>
</div>
</div> </div>
`; </div>
lucide.createIcons(); `;
} lucide.createIcons();
showToast('图片已就绪', 'success');
} catch (err) { } catch (err) {
showToast('图片上传失败', 'error'); console.error(err);
showToast('图片处理失败', 'error');
} finally { } finally {
submitBtn.disabled = false; submitBtn.disabled = false;
} }

View File

@ -181,7 +181,8 @@
<th class="px-8 py-5">订单信息</th> <th class="px-8 py-5">订单信息</th>
<th class="px-8 py-5">积分/金额</th> <th class="px-8 py-5">积分/金额</th>
<th class="px-8 py-5">状态</th> <th class="px-8 py-5">状态</th>
<th class="px-8 py-5 text-right">时间</th> <th class="px-8 py-5">时间</th>
<th class="px-8 py-5">操作</th>
</tr> </tr>
</thead> </thead>
<tbody class="divide-y divide-slate-50 text-sm font-bold text-slate-600"> <tbody class="divide-y divide-slate-50 text-sm font-bold text-slate-600">
@ -216,15 +217,21 @@
<span class="text-slate-400 text-[10px]">已取消</span> <span class="text-slate-400 text-[10px]">已取消</span>
{% endif %} {% endif %}
</td> </td>
<td class="px-8 py-5 text-right text-slate-400 text-xs"> <td class="px-8 py-5 text-slate-400 text-xs">
{{ order.paid_at_bj.strftime('%Y-%m-%d %H:%M') if order.paid_at_bj else {{ order.paid_at_bj.strftime('%Y-%m-%d %H:%M') if order.paid_at_bj else
order.created_at_bj.strftime('%Y-%m-%d %H:%M') }} order.created_at_bj.strftime('%Y-%m-%d %H:%M') }}
</td> </td>
<td class="px-8 py-5">
<a href="/admin/orders/{{ order.id }}"
class="w-7 h-7 bg-slate-50 text-slate-400 hover:bg-indigo-50 hover:text-indigo-600 rounded-lg flex items-center justify-center transition-all">
<i data-lucide="eye" class="w-3.5 h-3.5"></i>
</a>
</td>
</tr> </tr>
{% endfor %} {% endfor %}
{% else %} {% else %}
<tr> <tr>
<td colspan="4" class="px-8 py-16 text-center text-slate-300 italic"> <td colspan="5" class="px-8 py-16 text-center text-slate-300 italic">
暂无充值记录 暂无充值记录
</td> </td>
</tr> </tr>

238
templates/order_detail.html Normal file
View File

@ -0,0 +1,238 @@
{% extends "base.html" %}
{% block title %}订单详情 - 管理后台{% endblock %}
{% block content %}
<div class="flex-1 overflow-y-auto p-8 relative">
<div class="max-w-4xl mx-auto space-y-8">
<!-- 头部导航 -->
<div class="flex items-center justify-between">
<div class="flex items-center gap-4">
<a href="/admin/orders" onclick="history.back(); return false;"
class="w-10 h-10 bg-white border border-slate-200 rounded-xl flex items-center justify-center text-slate-500 hover:text-indigo-600 transition-colors">
<i data-lucide="arrow-left" class="w-5 h-5"></i>
</a>
<div class="space-y-1">
<h1 class="text-3xl font-black text-slate-900 tracking-tight">订单详情</h1>
<p class="text-slate-500 font-bold text-sm flex items-center gap-2">
<i data-lucide="file-text" class="w-4 h-4"></i>
订单号: <span id="outTradeNo">...</span>
</p>
</div>
</div>
<div id="statusBadgeContainer">
<!-- Status badge will be injected here -->
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
<!-- 左侧:主要信息 -->
<div class="md:col-span-2 space-y-8">
<!-- 订单商品信息 -->
<div
class="bg-white/70 backdrop-blur-xl rounded-[2.5rem] border border-slate-200/50 shadow-2xl p-8 space-y-6">
<div class="flex items-center justify-between">
<h2 class="text-lg font-black text-slate-900 flex items-center gap-2">
<i data-lucide="package" class="w-5 h-5 text-indigo-500"></i>
商品信息
</h2>
</div>
<div class="space-y-4">
<div
class="flex items-center justify-between p-6 bg-slate-50 rounded-3xl border border-slate-100">
<div class="flex items-center gap-4">
<div
class="w-14 h-14 bg-amber-100 rounded-2xl flex items-center justify-center text-amber-600 shadow-sm">
<i data-lucide="zap" class="w-7 h-7"></i>
</div>
<div>
<h3 class="font-black text-slate-900">账户充值积分</h3>
<p class="text-xs text-slate-400 font-bold">虚拟商品 · 即时到账</p>
</div>
</div>
<div class="text-right">
<div class="text-xl font-black text-slate-900">+<span id="orderPoints">0</span> Pts
</div>
<div class="text-sm font-bold text-slate-400">¥<span id="orderAmount">0.00</span></div>
</div>
</div>
<div class="pt-4 border-t border-slate-100 space-y-3">
<div class="flex justify-between text-sm font-bold">
<span class="text-slate-400">商品总额</span>
<span class="text-slate-600">¥<span id="orderAmount2">0.00</span></span>
</div>
<div class="flex justify-between text-sm font-bold">
<span class="text-slate-400">优惠金额</span>
<span class="text-emerald-500">-¥0.00</span>
</div>
<div class="flex justify-between items-center pt-2">
<span class="text-lg font-black text-slate-900">实付款</span>
<span class="text-2xl font-black text-indigo-600">¥<span
id="orderTotal">0.00</span></span>
</div>
</div>
</div>
</div>
<!-- 支付信息 -->
<div
class="bg-white/70 backdrop-blur-xl rounded-[2.5rem] border border-slate-200/50 shadow-2xl p-8 space-y-6">
<h2 class="text-lg font-black text-slate-900 flex items-center gap-2">
<i data-lucide="credit-card" class="w-5 h-5 text-indigo-500"></i>
支付信息
</h2>
<div class="grid grid-cols-2 gap-8">
<div class="space-y-1">
<label class="text-[10px] font-black text-slate-400 uppercase tracking-widest">支付渠道</label>
<div class="flex items-center gap-2 font-bold text-slate-700">
支付宝 (Alipay)
</div>
</div>
<div class="space-y-1">
<label
class="text-[10px] font-black text-slate-400 uppercase tracking-widest">支付宝流水号</label>
<p id="tradeNo" class="font-mono text-xs font-bold text-slate-700">--</p>
</div>
<div class="space-y-1">
<label class="text-[10px] font-black text-slate-400 uppercase tracking-widest">创建时间</label>
<p id="createdAt" class="text-sm font-bold text-slate-700">--</p>
</div>
<div class="space-y-1">
<label class="text-[10px] font-black text-slate-400 uppercase tracking-widest">支付时间</label>
<p id="paidAt" class="text-sm font-bold text-slate-700">--</p>
</div>
</div>
</div>
</div>
<!-- 右侧:买家信息 -->
<div class="space-y-8">
<div
class="bg-white/70 backdrop-blur-xl rounded-[2.5rem] border border-slate-200/50 shadow-2xl p-8 space-y-6">
<h2 class="text-lg font-black text-slate-900 flex items-center gap-2">
<i data-lucide="user" class="w-5 h-5 text-indigo-500"></i>
买家信息
</h2>
<div class="flex flex-col items-center text-center space-y-4 pb-4">
<div
class="w-20 h-20 bg-indigo-50 rounded-[2rem] flex items-center justify-center text-indigo-600 shadow-inner">
<i data-lucide="user" class="w-10 h-10"></i>
</div>
<div>
<h3 id="userPhone" class="text-xl font-black text-slate-900">--</h3>
<div class="flex items-center justify-center gap-2 mt-1">
<span id="userRole"
class="px-2 py-0.5 bg-slate-100 text-slate-500 text-[10px] font-black rounded-md border border-slate-200 uppercase">--</span>
<span id="userBanned"
class="hidden px-2 py-0.5 bg-rose-50 text-rose-500 text-[10px] font-black rounded-md border border-rose-100 uppercase">已封禁</span>
</div>
</div>
</div>
<div class="pt-6 border-t border-slate-100 space-y-4">
<div class="flex justify-between items-center">
<span class="text-xs font-bold text-slate-400">用户 ID</span>
<span id="userId" class="text-sm font-black text-slate-700">#0</span>
</div>
<div class="flex justify-between items-center">
<span class="text-xs font-bold text-slate-400">注册时间</span>
<span id="userCreatedAt" class="text-sm font-black text-slate-700 text-right">--</span>
</div>
<div class="flex justify-between items-center">
<span class="text-xs font-bold text-slate-400">当前余额</span>
<div class="flex items-center gap-1">
<i data-lucide="zap" class="w-3 h-3 text-amber-500"></i>
<span id="userCurrentPoints" class="text-sm font-black text-slate-900">0</span>
</div>
</div>
</div>
<a id="viewUserBtn" href="#"
class="hidden w-full py-3 bg-slate-50 hover:bg-slate-100 text-slate-500 hover:text-indigo-600 transition-all rounded-2xl text-center text-xs font-black border border-slate-100 mt-4">
查看用户详细档案
</a>
</div>
<!-- 订单备注 (示例) -->
<div class="bg-indigo-600 rounded-[2.5rem] p-8 text-white space-y-4 shadow-2xl shadow-indigo-200">
<div class="flex items-center gap-2">
<i data-lucide="info" class="w-5 h-5 text-indigo-200"></i>
<h3 class="font-black">系统提示</h3>
</div>
<p class="text-xs text-indigo-100 font-medium leading-relaxed">
该订单由系统自动处理并完成积分发放。如有异常,请及时联系技术人员查看服务器日志。
</p>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
const orderId = window.location.pathname.split('/').pop();
async function loadOrderDetail() {
try {
const r = await fetch(`/api/admin/orders/${orderId}`);
if (!r.ok) throw new Error('订单未找到');
const data = await r.json();
renderDetail(data);
} catch (e) {
showToast('加载详情失败: ' + e.message, 'error');
setTimeout(() => history.back(), 2000);
}
}
function renderDetail(data) {
const { order, buyer, current_is_admin } = data;
// 订单基本信息
document.getElementById('outTradeNo').textContent = order.out_trade_no;
document.getElementById('orderPoints').textContent = order.points;
document.getElementById('orderAmount').textContent = order.amount.toFixed(2);
document.getElementById('orderAmount2').textContent = order.amount.toFixed(2);
document.getElementById('orderTotal').textContent = order.amount.toFixed(2);
document.getElementById('tradeNo').textContent = order.trade_no || '--';
document.getElementById('createdAt').textContent = order.created_at;
document.getElementById('paidAt').textContent = order.paid_at || '--';
// 状态徽章
const badgeContainer = document.getElementById('statusBadgeContainer');
if (order.status === 'PAID') {
badgeContainer.innerHTML = '<span class="px-5 py-2 bg-emerald-50 text-emerald-600 text-sm font-black rounded-xl border border-emerald-100 shadow-sm shadow-emerald-50 flex items-center gap-2"><i data-lucide="check-circle" class="w-4 h-4"></i>已完成</span>';
} else if (order.status === 'PENDING') {
badgeContainer.innerHTML = '<span class="px-5 py-2 bg-amber-50 text-amber-600 text-sm font-black rounded-xl border border-amber-100 shadow-sm shadow-amber-50 flex items-center gap-2"><i data-lucide="clock" class="w-4 h-4"></i>待支付</span>';
} else {
badgeContainer.innerHTML = '<span class="px-5 py-2 bg-slate-50 text-slate-400 text-sm font-black rounded-xl border border-slate-100 shadow-sm shadow-slate-50 flex items-center gap-2"><i data-lucide="x-circle" class="w-4 h-4"></i>已取消</span>';
}
// 买家信息
document.getElementById('userPhone').textContent = buyer.phone;
document.getElementById('userRole').textContent = buyer.role;
document.getElementById('userId').textContent = '#' + buyer.id;
document.getElementById('userCreatedAt').textContent = buyer.created_at;
document.getElementById('userCurrentPoints').textContent = buyer.current_points;
if (buyer.is_banned) {
document.getElementById('userBanned').classList.remove('hidden');
}
// 权限相关
const viewBtn = document.getElementById('viewUserBtn');
if (current_is_admin) {
viewBtn.classList.remove('hidden');
viewBtn.href = `/api/admin/users?q=${buyer.phone}`;
}
lucide.createIcons();
}
loadOrderDetail();
</script>
{% endblock %}

View File

@ -16,31 +16,41 @@
</div> </div>
<div class="flex gap-4"> <div class="flex gap-4">
<div class="relative"> <div class="relative">
<input type="text" id="orderSearch" placeholder="搜索手机号/订单号..." class="pl-10 pr-4 py-2 bg-white border border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-indigo-500 outline-none transition-all w-64"> <input type="text" id="orderSearch" placeholder="搜索手机号/订单号..."
class="pl-10 pr-4 py-2 bg-white border border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-indigo-500 outline-none transition-all w-64">
<i data-lucide="search" class="w-4 h-4 absolute left-3 top-1/2 -translate-y-1/2 text-slate-400"></i> <i data-lucide="search" class="w-4 h-4 absolute left-3 top-1/2 -translate-y-1/2 text-slate-400"></i>
</div> </div>
<button onclick="loadOrders()" class="w-10 h-10 bg-white border border-slate-200 rounded-xl flex items-center justify-center text-slate-500 hover:text-indigo-600 transition-colors"> <button onclick="loadOrders()"
class="w-10 h-10 bg-white border border-slate-200 rounded-xl flex items-center justify-center text-slate-500 hover:text-indigo-600 transition-colors">
<i data-lucide="refresh-cw" class="w-4 h-4"></i> <i data-lucide="refresh-cw" class="w-4 h-4"></i>
</button> </button>
</div> </div>
</div> </div>
<!-- 记录列表 --> <!-- 记录列表 -->
<div class="bg-white/70 backdrop-blur-xl rounded-[2.5rem] border border-slate-200/50 shadow-2xl overflow-hidden"> <div
class="bg-white/70 backdrop-blur-xl rounded-[2.5rem] border border-slate-200/50 shadow-2xl overflow-hidden">
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<table class="w-full text-left border-collapse"> <table class="w-full text-left border-collapse">
<thead> <thead>
<tr class="border-b border-slate-100 bg-slate-50/50"> <tr class="border-b border-slate-100 bg-slate-50/50">
<th class="px-8 py-5 text-[10px] font-black text-slate-400 uppercase tracking-widest">用户信息</th> <th class="px-8 py-5 text-[10px] font-black text-slate-400 uppercase tracking-widest">用户信息
<th class="px-8 py-5 text-[10px] font-black text-slate-400 uppercase tracking-widest">订单详情</th> </th>
<th class="px-8 py-5 text-[10px] font-black text-slate-400 uppercase tracking-widest">积分/金额</th> <th class="px-8 py-5 text-[10px] font-black text-slate-400 uppercase tracking-widest">订单详情
<th class="px-8 py-5 text-[10px] font-black text-slate-400 uppercase tracking-widest">状态</th> </th>
<th class="px-8 py-5 text-[10px] font-black text-slate-400 uppercase tracking-widest">时间</th> <th class="px-8 py-5 text-[10px] font-black text-slate-400 uppercase tracking-widest">积分/金额
</th>
<th class="px-8 py-5 text-[10px] font-black text-slate-400 uppercase tracking-widest">状态
</th>
<th class="px-8 py-5 text-[10px] font-black text-slate-400 uppercase tracking-widest">时间
</th>
<th class="px-8 py-5 text-[10px] font-black text-slate-400 uppercase tracking-widest">操作
</th>
</tr> </tr>
</thead> </thead>
<tbody id="orderList" class="divide-y divide-slate-100"> <tbody id="orderList" class="divide-y divide-slate-100">
<tr> <tr>
<td colspan="5" class="px-8 py-20 text-center"> <td colspan="6" class="px-8 py-20 text-center">
<div class="flex flex-col items-center gap-4 animate-pulse"> <div class="flex flex-col items-center gap-4 animate-pulse">
<i data-lucide="loader-2" class="w-8 h-8 text-indigo-500 animate-spin"></i> <i data-lucide="loader-2" class="w-8 h-8 text-indigo-500 animate-spin"></i>
<p class="text-slate-400 font-bold">正在获取记录...</p> <p class="text-slate-400 font-bold">正在获取记录...</p>
@ -73,17 +83,7 @@
function renderOrders(orders) { function renderOrders(orders) {
const list = document.getElementById('orderList'); const list = document.getElementById('orderList');
if (orders.length === 0) { if (orders.length === 0) {
list.innerHTML = ` renderEmptyState();
<tr>
<td colspan="5" class="px-8 py-20 text-center">
<div class="flex flex-col items-center gap-4 opacity-20">
<i data-lucide="inbox" class="w-16 h-16"></i>
<p class="font-black text-xl">暂无记录</p>
</div>
</td>
</tr>
`;
lucide.createIcons();
return; return;
} }
@ -117,23 +117,43 @@
</td> </td>
<td class="px-8 py-5"> <td class="px-8 py-5">
${o.status === 'PAID' ${o.status === 'PAID'
? '<span class="px-3 py-1 bg-emerald-50 text-emerald-600 text-[10px] font-black rounded-lg border border-emerald-100">已完成</span>' ? '<span class="px-3 py-1 bg-emerald-50 text-emerald-600 text-[10px] font-black rounded-lg border border-emerald-100">已完成</span>'
: o.status === 'PENDING' : o.status === 'PENDING'
? '<span class="px-3 py-1 bg-amber-50 text-amber-600 text-[10px] font-black rounded-lg border border-amber-100">待支付</span>' ? '<span class="px-3 py-1 bg-amber-50 text-amber-600 text-[10px] font-black rounded-lg border border-amber-100">待支付</span>'
: '<span class="px-3 py-1 bg-slate-50 text-slate-400 text-[10px] font-black rounded-lg border border-slate-100">已取消</span>' : '<span class="px-3 py-1 bg-slate-50 text-slate-400 text-[10px] font-black rounded-lg border border-slate-100">已取消</span>'
} }
</td> </td>
<td class="px-8 py-5"> <td class="px-8 py-5 text-right">
<div class="flex flex-col"> <div class="flex flex-col">
<span class="text-[10px] font-bold text-slate-500">创建: ${o.created_at}</span> <span class="text-[10px] font-bold text-slate-500">创建: ${o.created_at}</span>
<span class="text-[10px] font-bold text-emerald-500">${o.paid_at ? '完成: ' + o.paid_at : ''}</span> <span class="text-[10px] font-bold text-emerald-500">${o.paid_at ? '完成: ' + o.paid_at : ''}</span>
</div> </div>
</td> </td>
<td class="px-8 py-5">
<a href="/admin/orders/${o.id}" class="w-8 h-8 bg-slate-50 text-slate-400 hover:bg-indigo-50 hover:text-indigo-600 rounded-lg flex items-center justify-center transition-all">
<i data-lucide="eye" class="w-4 h-4"></i>
</a>
</td>
</tr> </tr>
`).join(''); `).join('');
lucide.createIcons(); lucide.createIcons();
} }
function renderEmptyState() {
const list = document.getElementById('orderList');
list.innerHTML = `
<tr>
<td colspan="6" class="px-8 py-20 text-center">
<div class="flex flex-col items-center gap-4 opacity-20">
<i data-lucide="inbox" class="w-16 h-16"></i>
<p class="font-black text-xl">暂无记录</p>
</div>
</td>
</tr>
`;
lucide.createIcons();
}
document.getElementById('orderSearch').oninput = (e) => { document.getElementById('orderSearch').oninput = (e) => {
const val = e.target.value.toLowerCase(); const val = e.target.value.toLowerCase();
const filtered = allOrders.filter(o => const filtered = allOrders.filter(o =>

View File

@ -38,6 +38,8 @@
</th> </th>
<th class="px-8 py-5 text-[10px] font-black text-slate-400 uppercase tracking-widest">支付时间 <th class="px-8 py-5 text-[10px] font-black text-slate-400 uppercase tracking-widest">支付时间
</th> </th>
<th class="px-8 py-5 text-[10px] font-black text-slate-400 uppercase tracking-widest">操作
</th>
</tr> </tr>
</thead> </thead>
<tbody class="divide-y divide-slate-100"> <tbody class="divide-y divide-slate-100">
@ -84,11 +86,17 @@
{% endif %} {% endif %}
</span> </span>
</td> </td>
<td class="px-8 py-5">
<a href="/admin/orders/{{ order.id }}"
class="w-8 h-8 bg-slate-50 text-slate-400 hover:bg-indigo-50 hover:text-indigo-600 rounded-lg flex items-center justify-center transition-all">
<i data-lucide="eye" class="w-4 h-4"></i>
</a>
</td>
</tr> </tr>
{% endfor %} {% endfor %}
{% else %} {% else %}
<tr> <tr>
<td colspan="5" class="px-8 py-20 text-center"> <td colspan="6" class="px-8 py-20 text-center">
<div class="flex flex-col items-center gap-4 opacity-20"> <div class="flex flex-col items-center gap-4 opacity-20">
<i data-lucide="inbox" class="w-16 h-16"></i> <i data-lucide="inbox" class="w-16 h-16"></i>
<p class="font-black text-xl">暂无充值记录</p> <p class="font-black text-xl">暂无充值记录</p>