Merge branch 'main' of http://331002.xyz:8418/240241002/ai_v
This commit is contained in:
commit
6c3731097a
12
README.md
12
README.md
@ -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
5
app.py
@ -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"""
|
||||||
|
|||||||
@ -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')
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
BIN
requirements.txt
BIN
requirements.txt
Binary file not shown.
@ -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. 持久化记录
|
||||||
|
|||||||
@ -149,20 +149,59 @@ 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">
|
||||||
@ -172,9 +211,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
</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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
238
templates/order_detail.html
Normal 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 %}
|
||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -123,17 +123,37 @@
|
|||||||
: '<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 =>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user