Files

1688 lines
91 KiB
HTML
Raw Permalink 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.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>枢机协律商店</title>
<script src="/assets/gerp/GERPThemeSync.js"></script>
<link rel="stylesheet" href="/assets/css/GERP.css">
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://cdn.bootcdn.net/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200" rel="stylesheet">
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: 'var(--PrimaryColor, #ff4400)',
primaryDark: 'var(--PrimaryHover, #e63a00)',
secondary: 'var(--SecondaryColor, #ff7800)',
accent: 'var(--PrimaryColor, #ff5000)',
success: 'var(--SuccessColor, #10b981)',
warning: 'var(--WarningColor, #f59e0b)',
danger: 'var(--DangerColor, #ef4444)',
card: 'var(--CardBackground, #ffffff)',
bg: 'var(--BackgroundColor, #f1f5f9)',
border: 'var(--BorderColor, #e2e8f0)',
},
boxShadow: {
card: 'var(--ShadowSm, 0 2px 8px rgba(0,0,0,0.06))',
cardHover: 'var(--ShadowLg, 0 12px 24px rgba(0,0,0,0.15))',
}
}
}
}
</script>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif; background: var(--BackgroundColor, #f5f5f5); color: var(--TextPrimary, #333); }
.card-hover { transition: transform .25s cubic-bezier(.4,0,.2,1), box-shadow .25s ease; }
.card-hover:hover { transform: translateY(-3px); box-shadow: 0 8px 24px rgba(0,0,0,0.08); }
/* Google Material Symbols 图标样式 */
.material-symbols-outlined {
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
vertical-align: middle;
user-select: none;
}
.ms-fill { font-variation-settings: 'FILL' 1, 'wght' 400, 'GRAD' 0, 'opsz' 24; }
.material-symbols-outlined.spinning { animation: spin 1s linear infinite; }
@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
.toast-item { animation: toastIn .3s ease forwards; border-radius: 12px; backdrop-filter: blur(8px); }
@keyframes toastIn { from { opacity:0; transform: translate(-50%, 20px); } to { opacity:1; transform: translate(-50%, 0); } }
.modal-mask { transition: opacity .25s ease; }
.modal-box { transition: transform .25s ease, opacity .25s ease; }
.fade-in { animation: fadeIn .35s ease forwards; }
@keyframes fadeIn { from { opacity:0; transform: translateY(8px); } to { opacity:1; transform: translateY(0); } }
.placeholder-img { background: linear-gradient(135deg, var(--HoverBackground, #f5f5f5), var(--BorderColor, #eee)); }
::-webkit-scrollbar { width:6px; height:6px; }
::-webkit-scrollbar-thumb { background: var(--BorderColor, #ddd); border-radius:6px; }
::-webkit-scrollbar-thumb:hover { background: var(--TextSecondary, #bbb); }
input:focus, textarea:focus, select:focus { outline: none; box-shadow: 0 0 0 3px color-mix(in srgb, var(--PrimaryColor, #ff4400) 18%, transparent); }
/* 淘宝风格搜索框 - 跟随主题 */
.tb-search-wrap { border: 2px solid var(--PrimaryColor, #ff4400); border-radius: 24px; overflow: hidden; background: var(--InputBackground, var(--CardBackground, #fff)); transition: box-shadow .2s; }
.tb-search-wrap:focus-within { box-shadow: 0 0 0 4px color-mix(in srgb, var(--PrimaryColor, #ff4400) 12%, transparent); }
.tb-search { border-radius: 22px 0 0 22px; border: none; }
.tb-search-btn { background: var(--PrimaryColor, #ff4400); border-radius: 0 22px 22px 0; transition: background .2s; }
.tb-search-btn:hover { background: var(--PrimaryHover, #e63a00); }
/* 骨架屏 */
.skeleton { position: relative; overflow: hidden; background: var(--HoverBackground, #f0f0f0); border-radius: 8px; }
.skeleton::after { content: ''; position: absolute; inset: 0; transform: translateX(-100%);
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.6), transparent);
animation: skeletonPulse 1.4s ease-in-out infinite; }
@keyframes skeletonPulse { 100% { transform: translateX(100%); } }
/* 购物车飞入动画 */
.fly-clone { position: fixed; z-index: 9999; pointer-events: none;
transition: transform .8s cubic-bezier(.5,-0.3,.7,1), opacity .8s ease;
border-radius: 8px; overflow: hidden; background-size: cover; background-position: center;
box-shadow: 0 8px 24px rgba(0,0,0,0.2); }
.fly-clone.go { transform: translate(var(--dx,0), var(--dy,0)) scale(0.1); opacity: 0; }
/* 移动端底部固定导航 - 跟随主题 */
.mobile-bottom-nav { display: none; background: var(--CardBackground, #fff); border-color: var(--BorderColor, #eee); box-shadow: 0 -2px 12px rgba(0,0,0,0.04); padding-bottom: env(safe-area-inset-bottom, 0); }
.mobile-bottom-nav .material-symbols-outlined { font-size: 24px; }
.mobile-nav-link { transition: color .2s ease; color: var(--TextSecondary, #999); }
.badge-bg { background: var(--PrimaryColor, #ff4400); }
@media (max-width: 767px) {
.mobile-bottom-nav { display: flex; }
main#app { padding-bottom: 70px; }
.mobile-nav-link.active { color: var(--PrimaryColor, #ff4400); }
.mobile-nav-link.active .material-symbols-outlined { font-variation-settings: 'FILL' 1, 'wght' 400, 'GRAD' 0, 'opsz' 24; }
}
/* 订单状态进度条 */
.order-progress { display: flex; align-items: flex-start; gap: 0; }
.order-progress-step { flex: 1; text-align: center; position: relative; }
.order-progress-step .dot { width: 28px; height: 28px; border-radius: 50%;
background: var(--BorderColor, #e0e0e0); color: #fff; display: flex;
align-items: center; justify-content: center; font-size: 12px; margin: 0 auto;
position: relative; z-index: 2; transition: all .3s ease; }
.order-progress-step .label { font-size: 12px; margin-top: 6px; color: var(--TextSecondary, #999); }
.order-progress-step::before { content: ''; position: absolute; top: 14px; left: -50%;
width: 100%; height: 3px; background: var(--BorderColor, #e0e0e0); z-index: 1; border-radius: 2px; }
.order-progress-step:first-child::before { display: none; }
.order-progress-step.done .dot { background: var(--SuccessColor, #10b981); }
.order-progress-step.done::before { background: var(--SuccessColor, #10b981); }
.order-progress-step.current .dot { background: var(--PrimaryColor, #ff4400);
transform: scale(1.15); box-shadow: 0 0 0 6px color-mix(in srgb, var(--PrimaryColor, #ff4400) 12%, transparent); }
.order-progress-step.current .label { color: var(--PrimaryColor, #ff4400); font-weight: 600; }
/* 触摸优化 */
@media (max-width: 767px) {
.touchable { min-height: 44px; min-width: 44px; }
}
/* 左侧分类栏 - 圆润版 */
.cat-sidebar { width: 200px; background: var(--CardBackground, #fff); border-radius: 12px; padding: 8px 0; box-shadow: 0 2px 12px rgba(0,0,0,0.04); }
.cat-sidebar-item { display: flex; align-items: center; gap: 10px; padding: 10px 18px; font-size: 13px; color: var(--TextPrimary, #333); cursor: pointer; border-left: 3px solid transparent; transition: all .2s cubic-bezier(.4,0,.2,1); border-radius: 0 8px 8px 0; margin-right: 8px; }
.cat-sidebar-item:hover { background: color-mix(in srgb, var(--PrimaryColor, #ff4400) 6%, transparent); color: var(--PrimaryColor, #ff4400); }
.cat-sidebar-item.active { background: color-mix(in srgb, var(--PrimaryColor, #ff4400) 10%, transparent); color: var(--PrimaryColor, #ff4400); border-left-color: var(--PrimaryColor, #ff4400); font-weight: 600; }
.cat-sidebar-item .material-symbols-outlined { font-size: 20px; }
.cat-sidebar-title { font-weight: 700; font-size: 15px; padding: 10px 18px 10px; color: var(--PrimaryColor, #ff4400); display: flex; align-items: center; gap: 8px; }
/* Banner 轮播 - 圆润版 */
.banner-container { position: relative; border-radius: 16px; overflow: hidden; box-shadow: 0 4px 20px rgba(0,0,0,0.08); }
.banner-slide { position: absolute; inset: 0; display: flex; align-items: center; padding: 0 48px; opacity: 0; transition: opacity .6s ease; border-radius: 16px; }
.banner-slide.active { opacity: 1; }
.banner-dots { position: absolute; bottom: 16px; left: 50%; transform: translateX(-50%); display: flex; gap: 8px; z-index: 2; }
.banner-dot { width: 8px; height: 8px; border-radius: 50%; background: rgba(255,255,255,0.45); cursor: pointer; transition: all .3s; backdrop-filter: blur(4px); }
.banner-dot.active { background: #fff; width: 24px; border-radius: 6px; }
.banner-arrow { position: absolute; top: 50%; transform: translateY(-50%); width: 40px; height: 48px; background: rgba(0,0,0,0.12); color: #fff; display: flex; align-items: center; justify-content: center; cursor: pointer; z-index: 2; transition: background .2s, opacity .2s; border: none; opacity: 0; backdrop-filter: blur(4px); }
.banner-container:hover .banner-arrow { opacity: 1; }
.banner-arrow:hover { background: rgba(0,0,0,0.3); }
.banner-arrow.prev { left: 0; border-radius: 0 12px 12px 0; }
.banner-arrow.next { right: 0; border-radius: 12px 0 0 12px; }
/* 右侧促销卡片 - 圆润版 */
.promo-card { border-radius: 12px; padding: 14px 16px; display: flex; align-items: center; gap: 12px; cursor: pointer; transition: all .25s cubic-bezier(.4,0,.2,1); box-shadow: 0 2px 8px rgba(0,0,0,0.04); }
.promo-card:hover { transform: translateY(-2px); box-shadow: 0 6px 16px rgba(0,0,0,0.1); }
.promo-card-icon { font-size: 32px; line-height: 1; }
/* 淘宝风格商品卡片 - 圆润版 */
.tb-product { background: var(--CardBackground, #fff); border-radius: 12px; overflow: hidden; cursor: pointer; transition: all .25s cubic-bezier(.4,0,.2,1); box-shadow: 0 1px 4px rgba(0,0,0,0.04); }
.tb-product:hover { box-shadow: 0 8px 24px rgba(0,0,0,0.1); transform: translateY(-4px); z-index: 1; }
.tb-product-img { aspect-ratio: 1; overflow: hidden; background: var(--HoverBackground, #f5f5f5); position: relative; }
.tb-product-img img { width: 100%; height: 100%; object-fit: cover; transition: transform .4s cubic-bezier(.4,0,.2,1); }
.tb-product:hover .tb-product-img img { transform: scale(1.06); }
.tb-product-info { padding: 10px 12px 12px; }
.tb-product-title { font-size: 13px; line-height: 1.5; color: var(--TextPrimary, #333); display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; min-height: 39px; }
.tb-product-price { color: var(--PrimaryColor, #ff4400); font-weight: 700; margin-top: 8px; display: flex; align-items: baseline; gap: 2px; }
.tb-product-price .sym { font-size: 12px; font-weight: 600; }
.tb-product-price .val { font-size: 22px; letter-spacing: -0.5px; }
.tb-product-meta { display: flex; align-items: center; justify-content: space-between; margin-top: 6px; font-size: 11px; color: var(--TextSecondary, #999); }
.tb-product-tag { position: absolute; top: 8px; left: 8px; background: var(--PrimaryColor, #ff4400); color: #fff; font-size: 10px; padding: 3px 8px; font-weight: 600; z-index: 1; border-radius: 6px; backdrop-filter: blur(4px); }
.tb-product-tag.low-stock { background: var(--DangerColor, #e53935); }
/* 猜你喜欢标题 */
.section-title { display: flex; align-items: center; gap: 8px; margin-bottom: 16px; font-size: 20px; font-weight: 700; color: var(--TextPrimary, #333); }
.section-title .heart { color: var(--PrimaryColor, #ff4400); font-size: 24px !important; }
.section-title-sub { font-size: 13px; color: var(--TextSecondary, #999); font-weight: 400; margin-left: 2px; }
/* 价格装饰渐变(详情页) */
.price-decor { background:
radial-gradient(circle at 0% 0%, color-mix(in srgb, var(--PrimaryColor, #ff4400) 8%, transparent), transparent 55%),
linear-gradient(135deg, color-mix(in srgb, var(--PrimaryColor, #ff4400) 3%, transparent), color-mix(in srgb, var(--PrimaryColor, #ff8800) 6%, transparent));
border-radius: 14px; }
/* 全局圆润圆角覆盖 */
.rounded { border-radius: 8px !important; }
.rounded-lg { border-radius: 10px !important; }
.rounded-xl { border-radius: 14px !important; }
.rounded-2xl { border-radius: 18px !important; }
.rounded-full { border-radius: 999px !important; }
/* 按钮圆润 */
button, .btn, a.btn { border-radius: 10px !important; }
</style>
</head>
<body class="min-h-screen flex flex-col">
<!-- 顶部导航栏(跟随主题,淘宝橙色作为强调色) -->
<header class="sticky top-0 z-40 shadow-sm border-b" style="background: var(--CardBackground, #fff); border-color: var(--BorderColor, #f0f0f0);">
<div class="max-w-7xl mx-auto px-3 sm:px-4">
<div class="flex items-center gap-3 h-16 sm:h-[68px]">
<!-- Logo -->
<a href="#home" class="flex items-center gap-2 shrink-0">
<span class="material-symbols-outlined text-3xl" style="color: var(--PrimaryColor, #ff4400)">storefront</span>
<span class="text-xl sm:text-2xl font-bold hidden sm:inline" style="color: var(--PrimaryColor, #ff4400)">枢机协律</span>
</a>
<!-- 搜索框 -->
<div class="flex-1 max-w-2xl mx-auto">
<div class="tb-search-wrap flex h-10 sm:h-11">
<input id="searchInput" type="text" placeholder="搜索宝贝..." class="flex-1 px-5 text-sm tb-search bg-transparent focus:ring-0" style="color: var(--TextPrimary, #333)">
<button id="searchBtn" class="tb-search-btn px-6 sm:px-8 text-white font-medium transition-colors flex items-center justify-center gap-1.5">
<span class="material-symbols-outlined text-[20px]">search</span>
<span class="hidden sm:inline">搜索</span>
</button>
</div>
</div>
<!-- 钱包余额 -->
<div class="hidden lg:flex items-center gap-1.5 text-sm px-3 py-2 rounded-full cursor-pointer transition-colors" style="color: var(--TextSecondary, #666)" onclick="window.location.hash='home'" onmouseover="this.style.background='var(--HoverBackground, #f5f5f5)'" onmouseout="this.style.background='transparent'">
<span class="material-symbols-outlined text-[22px]" style="color: var(--PrimaryColor, #ff4400)">account_balance_wallet</span>
<span id="walletBalance">GC: --</span>
</div>
<!-- 购物车 -->
<a href="#cart" class="relative p-2 rounded-full transition-colors touchable" title="购物车" style="color: var(--TextPrimary, #333)" onmouseover="this.style.background='var(--HoverBackground, #f5f5f5)'" onmouseout="this.style.background='transparent'">
<span class="material-symbols-outlined text-[26px]">shopping_cart</span>
<span id="cartBadge" class="absolute -top-0.5 -right-0.5 text-white text-[10px] min-w-[18px] h-[18px] flex items-center justify-center rounded-full font-bold px-0.5 hidden" style="background: var(--PrimaryColor, #ff4400)">0</span>
</a>
<!-- 我的订单 -->
<a href="#orders" class="hidden sm:block p-2 rounded-full transition-colors touchable" title="我的订单" style="color: var(--TextPrimary, #333)" onmouseover="this.style.background='var(--HoverBackground, #f5f5f5)'" onmouseout="this.style.background='transparent'">
<span class="material-symbols-outlined text-[26px]">receipt_long</span>
</a>
<!-- 卖家管理 -->
<a href="#seller" class="hidden sm:block p-2 rounded-full transition-colors touchable" title="卖家中心" style="color: var(--TextPrimary, #333)" onmouseover="this.style.background='var(--HoverBackground, #f5f5f5)'" onmouseout="this.style.background='transparent'">
<span class="material-symbols-outlined text-[26px]">store</span>
</a>
</div>
</div>
</header>
<!-- 主视图容器 -->
<main id="app" class="flex-1 max-w-7xl mx-auto px-3 sm:px-4 py-3 sm:py-4 min-h-[60vh] w-full"></main>
<!-- 移动端底部固定导航栏(跟随主题) -->
<nav class="mobile-bottom-nav sm:hidden fixed bottom-0 left-0 right-0 z-40 border-t">
<a href="#home" class="mobile-nav-link flex-1 flex flex-col items-center justify-center py-2 text-xs" data-route="home">
<span class="material-symbols-outlined">home</span><span class="mt-0.5">首页</span>
</a>
<a href="#orders" class="mobile-nav-link flex-1 flex flex-col items-center justify-center py-2 text-xs" data-route="orders">
<span class="material-symbols-outlined">receipt_long</span><span class="mt-0.5">订单</span>
</a>
<a href="#cart" class="mobile-nav-link flex-1 flex flex-col items-center justify-center py-2 text-xs relative" data-route="cart">
<span class="material-symbols-outlined">shopping_cart</span><span class="mt-0.5">购物车</span>
<span id="mobileCartBadge" class="absolute top-1 right-1/4 text-white text-[10px] min-w-[16px] h-[16px] flex items-center justify-center rounded-full px-1 hidden badge-bg">0</span>
</a>
<a href="#seller" class="mobile-nav-link flex-1 flex flex-col items-center justify-center py-2 text-xs" data-route="seller">
<span class="material-symbols-outlined">store</span><span class="mt-0.5">卖家</span>
</a>
</nav>
<!-- 通用模态框容器 -->
<div id="modalRoot"></div>
<!-- Toast 容器 -->
<div id="toastRoot" class="fixed bottom-8 left-1/2 -translate-x-1/2 z-[100] flex flex-col gap-2 items-center pointer-events-none"></div>
<script>
// ==================== 全局状态 ====================
// 用 IIFE 包裹避免与 GERP 主框架的全局标识符(如 Toast)冲突
(function() {
const App = {
State: {
walletGC: null,
cartCount: 0,
currentPage: 1,
pageSize: 20,
totalPages: 1,
keyword: '',
currentProductUUID: null,
orderTab: '',
lastView: 'home',
currentBanner: 0,
bannerTimer: null,
currentCategory: '',
categories: [
{ name: '推荐商品', icon: 'recommend', cat: '' },
{ name: '装备道具', icon: 'shield', cat: '装备' },
{ name: '建筑材料', icon: 'grid_on', cat: '材料' },
{ name: '消耗品', icon: 'science', cat: '消耗品' },
{ name: '食物', icon: 'restaurant', cat: '食物' },
{ name: '红石科技', icon: 'memory', cat: '红石' },
{ name: '装饰方块', icon: 'brush', cat: '装饰' },
{ name: '附魔书', icon: 'menu_book', cat: '附魔' },
{ name: '稀有物品', icon: 'workspace_premium', cat: '稀有' },
],
banners: [
{ title: '枢机协律商店', subtitle: '正版Minecraft游戏内商城', bg: 'linear-gradient(135deg, #ff5000 0%, #ff8800 100%)', icon: 'auto_awesome' },
{ title: '新品上架', subtitle: '限时优惠,先到先得', bg: 'linear-gradient(135deg, #e53935 0%, #ff7043 100%)', icon: 'bolt' },
{ title: '热卖爆款', subtitle: '玩家最爱的好物都在这', bg: 'linear-gradient(135deg, #7b1fa2 0%, #e040fb 100%)', icon: 'local_fire_department' },
{ title: 'GC充值', subtitle: '使用GC虚拟币畅快购物', bg: 'linear-gradient(135deg, #00897b 0%, #4db6ac 100%)', icon: 'credit_card' },
]
}
};
// ==================== Toast ====================
const Toast = {
show(message, type = 'info') {
const root = document.getElementById('toastRoot');
const colors = {
success: 'bg-green-500',
error: 'bg-red-500',
info: 'bg-gray-800',
warning: 'bg-orange-500',
};
const icons = { success: 'check_circle', error: 'cancel', info: 'info', warning: 'warning' };
const el = document.createElement('div');
el.className = `toast-item ${colors[type] || colors.info} text-white px-4 py-2 rounded-lg shadow-lg text-sm flex items-center gap-2 pointer-events-auto max-w-xs`;
el.innerHTML = `<span class="material-symbols-outlined">${icons[type] || icons.info}</span><span>${escapeHtml(message)}</span>`;
root.appendChild(el);
setTimeout(() => {
el.style.transition = 'opacity .3s';
el.style.opacity = '0';
setTimeout(() => el.remove(), 300);
}, 2500);
},
success(m) { this.show(m, 'success'); },
error(m) { this.show(m, 'error'); },
info(m) { this.show(m, 'info'); },
warning(m) { this.show(m, 'warning'); }
};
// ==================== API 封装 ====================
const Api = {
async request(url, options = {}) {
try {
const res = await fetch(url, {
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
...options,
});
const text = await res.text();
let data;
try { data = text ? JSON.parse(text) : {}; } catch { data = { status: false, msg: '返回格式错误' }; }
return data;
} catch (e) {
return { status: false, msg: '网络错误: ' + e.message };
}
},
get(url) { return this.request(url, { method: 'GET' }); },
post(url, body) { return this.request(url, { method: 'POST', body: JSON.stringify(body || {}) }); },
// 商品
getProductList(params = {}) {
const q = new URLSearchParams({ Keyword: params.Keyword || '', Category: params.Category || '', Tag: params.Tag || '', Page: params.Page || 1, PageSize: params.PageSize || 20 });
return this.get('/API/Shop/ProductList?' + q.toString());
},
getProductDetail(uuid) { return this.get('/API/Shop/ProductDetail?ProductUUID=' + encodeURIComponent(uuid)); },
getMyProducts() { return this.get('/API/Shop/MyProducts'); },
createProduct(data) { return this.post('/API/Shop/CreateProduct', data); },
modifyProduct(data) { return this.post('/API/Shop/ModifyProduct', data); },
deleteProduct(uuid) { return this.post('/API/Shop/DeleteProduct', { ProductUUID: uuid }); },
setProductStatus(uuid, status) { return this.post('/API/Shop/SetProductStatus', { ProductUUID: uuid, Status: status }); },
// 购物车
getCartList() { return this.get('/API/Shop/CartList'); },
cartAdd(productUUID, quantity) { return this.post('/API/Shop/CartAdd', { ProductUUID: productUUID, Quantity: quantity }); },
cartUpdate(cartItemUUID, quantity) { return this.post('/API/Shop/CartUpdate', { CartItemUUID: cartItemUUID, Quantity: quantity }); },
cartRemove(items) {
if (Array.isArray(items)) return this.post('/API/Shop/CartRemove', { CartItemUUIDs: items });
return this.post('/API/Shop/CartRemove', { CartItemUUID: items });
},
cartClear() { return this.post('/API/Shop/CartClear', {}); },
// 订单
getOrderList(status = '', asSeller = 0) { return this.get('/API/Shop/OrderList?Status=' + status + '&AsSeller=' + asSeller); },
getOrderDetail(uuid) { return this.get('/API/Shop/OrderDetail?OrderUUID=' + encodeURIComponent(uuid)); },
createOrder(data) { return this.post('/API/Shop/CreateOrder', data); },
payOrder(uuid) { return this.post('/API/Shop/PayOrder', { OrderUUID: uuid }); },
cancelOrder(uuid) { return this.post('/API/Shop/CancelOrder', { OrderUUID: uuid }); },
shipOrder(uuid) { return this.post('/API/Shop/ShipOrder', { OrderUUID: uuid }); },
confirmOrder(uuid) { return this.post('/API/Shop/ConfirmOrder', { OrderUUID: uuid }); },
refundOrder(uuid, reason) { return this.post('/API/Shop/RefundOrder', { OrderUUID: uuid, Reason: reason }); },
refundApprove(uuid, approve) { return this.post('/API/Shop/RefundApprove', { OrderUUID: uuid, Approve: approve }); },
// 钱包
getWallet() { return this.get('/API/wallet'); },
};
// ==================== 工具函数 ====================
function escapeHtml(s) {
if (s === null || s === undefined) return '';
return String(s).replace(/[&<>"']/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
}
function fmtPrice(n) {
const v = Number(n || 0);
return v.toFixed(2);
}
function imageUrl(sha1) {
if (!sha1) return '';
return '/API/Asset/Download/' + sha1;
}
function placeholderHtml(name) {
const ch = (name || '?').toString().charAt(0).toUpperCase();
return `<div class="placeholder-img w-full h-full flex items-center justify-center text-4xl font-bold text-gray-400">${escapeHtml(ch)}</div>`;
}
function productImgHtml(images, name, cls = 'w-full h-full object-cover') {
if (Array.isArray(images) && images.length > 0) {
return `<img src="${imageUrl(images[0])}" alt="${escapeHtml(name)}" class="${cls}" onerror="this.outerHTML='${placeholderHtml(name).replace(/'/g, '&#39;').replace(/"/g, '&quot;')}'">`;
}
return placeholderHtml(name);
}
// 订单状态映射
const OrderStatus = {
0: { text: '待付款', color: 'bg-orange-100 text-orange-600', dot: 'bg-orange-500' },
1: { text: '已付款', color: 'bg-blue-100 text-blue-600', dot: 'bg-blue-500' },
2: { text: '已发货', color: 'bg-cyan-100 text-cyan-600', dot: 'bg-cyan-500' },
3: { text: '已完成', color: 'bg-green-100 text-green-600', dot: 'bg-green-500' },
4: { text: '已取消', color: 'bg-gray-100 text-gray-500', dot: 'bg-gray-400' },
5: { text: '退款中', color: 'bg-yellow-100 text-yellow-700', dot: 'bg-yellow-500' },
6: { text: '已退款', color: 'bg-red-100 text-red-600', dot: 'bg-red-500' },
};
// 商品状态映射
const ProductStatus = {
0: { text: '已下架', color: 'bg-gray-100 text-gray-600' },
1: { text: '在售', color: 'bg-green-100 text-green-600' },
2: { text: '缺货', color: 'bg-orange-100 text-orange-600' },
};
// ==================== 模态框 ====================
const Modal = {
show(html) {
const root = document.getElementById('modalRoot');
root.innerHTML = `
<div class="modal-mask fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4" id="modalMask" style="opacity:0;">
<div class="modal-box bg-card rounded-xl shadow-2xl w-full max-w-md max-h-[90vh] overflow-y-auto" id="modalBox" style="transform:scale(0.95);opacity:0;">
${html}
</div>
</div>`;
requestAnimationFrame(() => {
document.getElementById('modalMask').style.opacity = '1';
document.getElementById('modalBox').style.transform = 'scale(1)';
document.getElementById('modalBox').style.opacity = '1';
});
document.getElementById('modalMask').addEventListener('click', (e) => {
if (e.target.id === 'modalMask') Modal.close();
});
},
close() {
const mask = document.getElementById('modalMask');
if (mask) {
mask.style.opacity = '0';
const box = document.getElementById('modalBox');
if (box) { box.style.transform = 'scale(0.95)'; box.style.opacity = '0'; }
setTimeout(() => { document.getElementById('modalRoot').innerHTML = ''; }, 250);
}
},
confirm(opts) {
const { title = '确认', content = '', confirmText = '确认', cancelText = '取消', confirmClass = 'bg-primary hover:bg-primaryDark', onConfirm } = opts;
this.show(`
<div class="p-5">
<h3 class="text-lg font-bold mb-3">${escapeHtml(title)}</h3>
<div class="text-gray-600 text-sm mb-5">${content}</div>
<div class="flex gap-3">
<button id="modalCancel" class="flex-1 border border-border hover:bg-gray-100 py-2 rounded-lg transition-colors">${cancelText}</button>
<button id="modalConfirm" class="flex-1 ${confirmClass} text-white py-2 rounded-lg transition-colors">${confirmText}</button>
</div>
</div>`);
document.getElementById('modalCancel').onclick = () => Modal.close();
document.getElementById('modalConfirm').onclick = async () => {
const r = await onConfirm();
if (r !== false) Modal.close();
};
},
prompt(opts) {
const { title = '输入', placeholder = '', confirmText = '确认', onConfirm, multiline = false } = opts;
const inputHtml = multiline
? `<textarea id="modalInput" rows="4" class="w-full border border-border rounded-lg p-2 text-sm" placeholder="${escapeHtml(placeholder)}"></textarea>`
: `<input id="modalInput" type="text" class="w-full border border-border rounded-lg p-2 text-sm" placeholder="${escapeHtml(placeholder)}">`;
this.show(`
<div class="p-5">
<h3 class="text-lg font-bold mb-3">${escapeHtml(title)}</h3>
${inputHtml}
<div class="flex gap-3 mt-4">
<button id="modalCancel" class="flex-1 border border-border hover:bg-gray-100 py-2 rounded-lg transition-colors">取消</button>
<button id="modalConfirm" class="flex-1 bg-primary hover:bg-primaryDark text-white py-2 rounded-lg transition-colors">${confirmText}</button>
</div>
</div>`);
document.getElementById('modalCancel').onclick = () => Modal.close();
document.getElementById('modalConfirm').onclick = async () => {
const v = document.getElementById('modalInput').value.trim();
const r = await onConfirm(v);
if (r !== false) Modal.close();
};
setTimeout(() => document.getElementById('modalInput').focus(), 100);
}
};
// ==================== 路由 ====================
const Router = {
init() {
window.addEventListener('hashchange', () => Router.render());
this.render();
},
navigate(hash) {
window.location.hash = hash;
},
parse() {
const raw = window.location.hash.replace(/^#/, '') || 'home';
const [path, qs] = raw.split('?');
const params = {};
if (qs) {
qs.split('&').forEach(kv => {
const [k, v] = kv.split('=');
params[decodeURIComponent(k)] = decodeURIComponent(v || '');
});
}
return { path: path || 'home', params };
},
render() {
const { path, params } = this.parse();
const app = document.getElementById('app');
app.classList.remove('fade-in');
void app.offsetWidth;
app.classList.add('fade-in');
// 离开首页时停止轮播定时器
if (path !== 'home') {
stopBannerAutoPlay();
}
// 高亮移动端底部导航
const activeRoute = (path === 'detail') ? 'home' : path;
document.querySelectorAll('.mobile-nav-link').forEach(a => {
a.classList.toggle('active', a.dataset.route === activeRoute);
});
App.State.lastView = path;
switch (path) {
case 'home': Views.home(); break;
case 'detail': Views.detail(params); break;
case 'cart': Views.cart(); break;
case 'orders': Views.orders(params); break;
case 'seller': Views.seller(params); break;
default: Views.home();
}
window.scrollTo(0, 0);
}
};
// ==================== 视图 ====================
const Views = {
// -------- 首页:淘宝风格布局 --------
async home() {
const app = document.getElementById('app');
const cats = App.State.categories;
const banners = App.State.banners;
app.innerHTML = `
<!-- 移动端横向分类滚动 -->
<div class="cat-mobile-scroll flex lg:hidden gap-1.5 mb-3 overflow-x-auto pb-2 -mx-3 sm:-mx-4 px-3 sm:px-4">
${cats.map((c, i) => `<button class="cat-m-item shrink-0 px-3.5 py-1.5 text-xs rounded-full whitespace-nowrap transition-all touchable flex items-center gap-1 ${(i === 0 && !App.State.currentCategory) ? 'text-white shadow-md' : 'bg-white border'}" data-cat="${escapeHtml(c.cat)}" style="${(i === 0 && !App.State.currentCategory) ? 'background: var(--PrimaryColor, #ff4400); border-color: var(--PrimaryColor, #ff4400)' : 'border-color: var(--BorderColor, #e5e5e5); color: var(--TextPrimary, #333)'}"><span class="material-symbols-outlined text-[16px]">${c.icon}</span>${escapeHtml(c.name)}</button>`).join('')}
</div>
<!-- 主区域:左侧分类 + 中间Banner + 右侧促销卡(桌面端) -->
<div class="flex gap-3 mb-4">
<!-- 左侧分类栏(桌面端) -->
<div class="cat-sidebar cat-sidebar-desktop hidden lg:block shrink-0">
<div class="cat-sidebar-title"><span class="material-symbols-outlined text-[20px]">grid_view</span>商品分类</div>
${cats.map((c, i) => `<div class="cat-sidebar-item ${(i === 0 && !App.State.currentCategory) ? 'active' : ''}" data-cat="${escapeHtml(c.cat)}"><span class="material-symbols-outlined">${c.icon}</span><span>${escapeHtml(c.name)}</span></div>`).join('')}
</div>
<!-- 中间轮播Banner -->
<div class="flex-1 min-w-0">
<div class="flex gap-3">
<div class="banner-container flex-1 h-[220px] sm:h-[280px] lg:h-[320px] xl:h-[340px] relative" id="bannerContainer" style="background: var(--CardBackground, #fff)">
${banners.map((b, i) => `
<div class="banner-slide ${i === 0 ? 'active' : ''}" style="background: ${b.bg}">
<div class="text-white">
<span class="material-symbols-outlined" style="font-size:64px;opacity:0.9;margin-bottom:12px;display:block">${b.icon}</span>
<div class="text-xl sm:text-2xl lg:text-3xl font-bold mb-1">${escapeHtml(b.title)}</div>
<div class="text-sm sm:text-base opacity-80">${escapeHtml(b.subtitle)}</div>
<div class="mt-4 inline-flex items-center gap-1 bg-white/20 hover:bg-white/30 backdrop-blur px-5 py-2 rounded-full text-sm cursor-pointer transition-colors">
立即查看 <span class="material-symbols-outlined text-[18px]">arrow_forward</span>
</div>
</div>
</div>
`).join('')}
<button class="banner-arrow prev" id="bannerPrev"><span class="material-symbols-outlined text-2xl">chevron_left</span></button>
<button class="banner-arrow next" id="bannerNext"><span class="material-symbols-outlined text-2xl">chevron_right</span></button>
<div class="banner-dots">
${banners.map((_, i) => `<div class="banner-dot ${i === 0 ? 'active' : ''}" data-idx="${i}"></div>`).join('')}
</div>
</div>
<!-- 右侧促销小卡片(桌面端) -->
<div class="hidden xl:flex flex-col gap-3 w-48 shrink-0">
<div class="promo-card flex-1" style="background: linear-gradient(135deg, #fff3e0, #ffe0b2);">
<div class="promo-card-icon">💰</div>
<div>
<div class="font-bold text-sm" style="color:#e65100">充值中心</div>
<div class="text-xs text-orange-700/70">GC快速充值</div>
</div>
</div>
<div class="promo-card flex-1" style="background: linear-gradient(135deg, #fce4ec, #f8bbd0);">
<div class="promo-card-icon">🎁</div>
<div>
<div class="font-bold text-sm" style="color:#c62828">新人福利</div>
<div class="text-xs text-red-700/70">首单特惠</div>
</div>
</div>
<div class="promo-card flex-1" style="background: linear-gradient(135deg, #e8f5e9, #c8e6c9);">
<div class="promo-card-icon">⚡</div>
<div>
<div class="font-bold text-sm" style="color:#2e7d32">限时秒杀</div>
<div class="text-xs text-green-700/70">每日更新</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 猜你喜欢 -->
<div class="section-title">
<span class="material-symbols-outlined heart ms-fill">favorite</span>
<span>猜你喜欢</span>
<span class="section-title-sub">—— 为你精选好物</span>
<span id="totalInfo" class="ml-auto text-xs font-normal" style="color:var(--TextSecondary, #999)"></span>
</div>
<!-- 商品网格 -->
<div id="productGrid" class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-2 sm:gap-3">
${tbSkeletonGridHtml(10)}
</div>
<div id="pager" class="flex justify-center items-center gap-2 mt-8"></div>`;
// 启动轮播
startBannerAutoPlay();
bindBannerControls();
bindCategoryClicks();
const page = (Router.parse().params.page) ? parseInt(Router.parse().params.page) : App.State.currentPage;
App.State.currentPage = page;
const r = await Api.getProductList({ Keyword: App.State.keyword, Category: App.State.currentCategory, Page: page, PageSize: App.State.pageSize });
const grid = document.getElementById('productGrid');
const totalInfo = document.getElementById('totalInfo');
if (!r || r.status === false) {
grid.innerHTML = emptyState('error', '加载失败', r && r.msg ? r.msg : '请稍后重试');
return;
}
const list = r.Data || [];
const total = r.Total || 0;
App.State.totalPages = Math.max(1, Math.ceil(total / (r.PageSize || App.State.pageSize)));
totalInfo.textContent = total > 0 ? `共 ${total} 件宝贝` : '';
if (list.length === 0) {
grid.innerHTML = emptyState('shopping_bag', '暂无商品', '还没有商品上架,快去看看别的吧');
document.getElementById('pager').innerHTML = '';
return;
}
grid.innerHTML = list.map(p => tbProductCardHtml(p)).join('');
bindProductCards(grid);
renderPager(document.getElementById('pager'), App.State.currentPage, App.State.totalPages, (p) => {
App.State.currentPage = p;
Router.navigate('home?page=' + p);
});
},
// -------- 商品详情 --------
async detail(params) {
const app = document.getElementById('app');
const uuid = params.uuid;
if (!uuid) { Router.navigate('home'); return; }
App.State.currentProductUUID = uuid;
app.innerHTML = `<div class="text-center py-20 text-gray-400"><span class="material-symbols-outlined fa-spinner spinning text-3xl">autorenew</span><p class="mt-2">加载中...</p></div>`;
const r = await Api.getProductDetail(uuid);
if (!r || r.status === false || !r.Data) {
app.innerHTML = emptyState('error', '加载失败', (r && r.msg) || '商品不存在');
return;
}
const p = r.Data;
const images = Array.isArray(p.ProductImages) ? p.ProductImages : [];
const tags = Array.isArray(p.ProductTags) ? p.ProductTags : [];
const stock = p.Stock !== undefined ? p.Stock : 0;
const status = p.Status !== undefined ? p.Status : 1;
app.innerHTML = `
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 bg-card rounded-xl p-5 shadow-card">
<div>
<div id="detailMainImg" class="aspect-square rounded-lg overflow-hidden bg-[var(--HoverBackground)] border-border">
${images.length > 0
? `<img src="${imageUrl(images[0])}" class="w-full h-full object-cover" onerror="this.outerHTML='${placeholderHtml(p.ProductName).replace(/'/g, '&#39;').replace(/"/g, '&quot;')}'">`
: placeholderHtml(p.ProductName)}
</div>
${images.length > 1 ? `
<div class="flex gap-2 mt-3 overflow-x-auto pb-2">
${images.map((sha, i) => `<img src="${imageUrl(sha)}" class="w-16 h-16 object-cover rounded border-2 cursor-pointer ${i === 0 ? 'border-primary' : 'border-transparent'}" data-idx="${i}" onerror="this.style.display='none'">`).join('')}
</div>` : ''}
</div>
<div>
<h1 class="text-2xl font-bold mb-2">${escapeHtml(p.ProductName)}</h1>
<div class="flex flex-wrap gap-1 mb-3">
${tags.map(t => `<span class="text-xs bg-orange-50 text-primary px-2 py-0.5 rounded">${escapeHtml(t)}</span>`).join('')}
${p.ProductCategory ? `<span class="text-xs bg-blue-50 text-blue-600 px-2 py-0.5 rounded">${escapeHtml(p.ProductCategory)}</span>` : ''}
</div>
<div class="price-decor relative rounded-xl p-5 mb-4 overflow-hidden border border-orange-100/40">
<span class="absolute top-3 right-3 text-[10px] bg-primary text-white px-2 py-0.5 rounded-full font-bold shadow"><span class="material-symbols-outlined mr-0.5" style="font-size:12px">bolt</span>限时特价</span>
<div class="flex items-center gap-2 mb-1">
<span class="material-symbols-outlined text-primary">local_fire_department</span>
<span class="text-xs font-semibold text-primary">抢购价</span>
</div>
<div class="text-4xl font-bold text-primary leading-none"><span class="text-lg align-top mr-0.5">GC</span>${fmtPrice(p.ProductPrice)}</div>
<div class="text-xs mt-2" style="color: var(--TextSecondary, #94a3b8)"><span class="material-symbols-outlined mr-1" style="font-size:14px;vertical-align:middle">schedule</span>活动限时进行中</div>
</div>
<div class="flex items-center gap-4 text-sm mb-4" style="color: var(--TextSecondary, #64748b)">
<span>库存: <b class="${stock > 0 ? 'text-success' : 'text-danger'}">${stock}</b></span>
<span>状态: <b>${ProductStatus[status] ? ProductStatus[status].text : '-'}</b></span>
</div>
${p.ProductDesc ? `<div class="border-t pt-4 mb-4" style="border-color: var(--BorderColor, #e2e8f0)"><h3 class="font-semibold mb-2">商品描述</h3><p class="text-sm whitespace-pre-wrap" style="color: var(--TextSecondary, #64748b)">${escapeHtml(p.ProductDesc)}</p></div>` : ''}
<div class="flex items-center gap-2 mb-4">
<span class="text-sm" style="color: var(--TextSecondary, #64748b)">数量:</span>
<div class="flex items-center border rounded-lg overflow-hidden" style="border-color: var(--BorderColor, #e2e8f0)">
<button id="qtyMinus" class="px-3 py-1 hover:bg-gray-100 touchable"><span class="material-symbols-outlined">remove</span></button>
<input id="qtyInput" type="number" value="1" min="1" max="${stock}" class="w-16 text-center border-0">
<button id="qtyPlus" class="px-3 py-1 hover:bg-gray-100 touchable"><span class="material-symbols-outlined">add</span></button>
</div>
</div>
<div class="flex gap-3">
<button id="addCartBtn" class="flex-1 border-2 border-primary text-primary hover:bg-orange-50 py-3 rounded-lg font-semibold transition-colors touchable" ${stock <= 0 || status !== 1 ? 'disabled' : ''}><span class="material-symbols-outlined mr-1" style="font-size:18px;vertical-align:middle">add_shopping_cart</span>加入购物车</button>
<button id="buyNowBtn" class="flex-1 bg-primary hover:bg-primaryDark text-white py-3 rounded-lg font-semibold transition-colors touchable" ${stock <= 0 || status !== 1 ? 'disabled' : ''}><span class="material-symbols-outlined mr-1" style="font-size:18px;vertical-align:middle">bolt</span>立即购买</button>
</div>
${stock <= 0 || status !== 1 ? '<p class="text-danger text-sm mt-2">该商品暂不可购买</p>' : ''}
</div>
</div>`;
// 图片切换
const thumbs = app.querySelectorAll('[data-idx]');
thumbs.forEach(th => {
th.addEventListener('click', () => {
const idx = th.dataset.idx;
const main = document.getElementById('detailMainImg');
main.innerHTML = `<img src="${imageUrl(images[idx])}" class="w-full h-full object-cover" onerror="this.outerHTML='${placeholderHtml(p.ProductName).replace(/'/g, '&#39;').replace(/"/g, '&quot;')}'">`;
thumbs.forEach(t => t.classList.replace('border-primary', 'border-transparent'));
th.classList.replace('border-transparent', 'border-primary');
});
});
// 数量控制
const qtyInput = document.getElementById('qtyInput');
document.getElementById('qtyMinus').onclick = () => { qtyInput.value = Math.max(1, parseInt(qtyInput.value) - 1); };
document.getElementById('qtyPlus').onclick = () => { qtyInput.value = Math.min(stock, parseInt(qtyInput.value) + 1); };
// 加入购物车
document.getElementById('addCartBtn').onclick = async () => {
const qty = parseInt(qtyInput.value) || 1;
// 飞入购物车动画
const imgEl = document.querySelector('#detailMainImg img');
flyToCart(imgEl);
const r = await Api.cartAdd(uuid, qty);
if (r && r.status !== false) {
Toast.success('已加入购物车');
refreshCartBadge();
} else {
Toast.error((r && r.msg) || '加入购物车失败');
}
};
// 立即购买
document.getElementById('buyNowBtn').onclick = async () => {
const qty = parseInt(qtyInput.value) || 1;
Modal.confirm({
title: '确认购买',
content: `<div class="space-y-1">
<p>商品: ${escapeHtml(p.ProductName)}</p>
<p>数量: ${qty}</p>
<p>金额: <b class="text-primary text-lg">GC ${fmtPrice(Number(p.ProductPrice) * qty)}</b></p>
<p>钱包余额: <b class="${(App.State.walletGC === null || App.State.walletGC < 0) ? 'text-red-500' : 'text-green-600'}">GC ${App.State.walletGC === null ? '未获取' : fmtPrice(App.State.walletGC)}</b></p>
</div>`,
confirmText: '确认下单',
onConfirm: async () => {
const r = await Api.createOrder({ FromCart: false, ProductUUID: uuid, Quantity: qty, Remark: '' });
if (r && r.status !== false) {
Toast.success('订单创建成功');
const orderUUID = r.OrderUUID || (r.Data && r.Data.OrderUUID) || '';
if (orderUUID) Router.navigate('orders?tab=');
else Router.navigate('orders');
} else {
Toast.error((r && r.msg) || '下单失败');
return false;
}
}
});
};
},
// -------- 购物车 --------
async cart() {
const app = document.getElementById('app');
app.innerHTML = `
<h2 class="text-xl font-bold mb-4" style="color: var(--TextPrimary, #1e293b)"><span class="material-symbols-outlined text-primary mr-2">shopping_cart</span>我的购物车</h2>
<div id="cartBox" class="bg-card rounded-xl shadow-card p-4">
${skeletonOrderHtml(3)}
</div>`;
const r = await Api.getCartList();
const box = document.getElementById('cartBox');
if (!r || r.status === false) {
box.innerHTML = emptyState('error', '加载失败', (r && r.msg) || '请稍后重试');
return;
}
const items = r.Items || [];
const totalAmount = r.TotalAmount || 0;
if (items.length === 0) {
box.innerHTML = emptyState('shopping_cart', '购物车空空如也', '快去挑选商品吧', '去逛逛', '#home');
return;
}
App.State.cartItems = items;
box.innerHTML = `
<div class="flex items-center gap-3 pb-3 border-b mb-2 text-sm" style="color: var(--TextSecondary, #64748b); border-color: var(--BorderColor, #e2e8f0)">
<label class="flex items-center gap-1 cursor-pointer"><input type="checkbox" id="selectAll" class="cart-check"> 全选</label>
<span class="ml-auto hidden sm:block">商品信息</span>
<span class="w-20 text-center hidden sm:block">单价</span>
<span class="w-28 text-center">数量</span>
<span class="w-24 text-center hidden sm:block">小计</span>
<span class="w-16 text-center">操作</span>
</div>
<div id="cartList" class="divide-y" style="border-color: var(--BorderColor, #e2e8f0)">
${items.map(it => cartItemHtml(it)).join('')}
</div>
<div class="flex items-center justify-between pt-4 mt-2 border-t flex-wrap gap-3" style="border-color: var(--BorderColor, #e2e8f0)">
<div class="text-sm" style="color: var(--TextSecondary, #64748b)">已选 <b id="selCount" class="text-primary">0</b> 件</div>
<div class="flex items-center gap-4">
<div class="text-right">
<div class="text-xs" style="color: var(--TextSecondary, #94a3b8)">合计:</div>
<div class="text-2xl font-bold text-primary"><span class="text-sm">GC</span> <span id="totalAmount">0.00</span></div>
</div>
<button id="checkoutBtn" class="bg-primary hover:bg-primaryDark text-white px-8 py-3 rounded-lg font-semibold transition-colors disabled:opacity-50 touchable" disabled>去结算</button>
</div>
</div>`;
bindCartEvents(box, totalAmount);
},
// -------- 我的订单 --------
async orders(params) {
const app = document.getElementById('app');
const tab = params.tab || '';
App.State.orderTab = tab;
const tabs = [
{ v: '', t: '全部' }, { v: '0', t: '待付款' }, { v: '1', t: '已付款' },
{ v: '2', t: '已发货' }, { v: '3', t: '已完成' }, { v: '5', t: '退款中' },
];
app.innerHTML = `
<h2 class="text-xl font-bold mb-4" style="color: var(--TextPrimary, #1e293b)"><span class="material-symbols-outlined text-primary mr-2">list_alt</span>我的订单</h2>
<div class="bg-card rounded-xl shadow-card overflow-hidden">
<div class="flex gap-2 p-3 overflow-x-auto border-b" style="border-color: var(--BorderColor, #e2e8f0)">
${tabs.map(t => `<a href="#orders?tab=${t.v}" class="px-4 py-1.5 rounded-full text-sm whitespace-nowrap transition-colors touchable ${tab === t.v ? 'bg-primary text-white font-semibold' : 'border border-border hover:bg-gray-50'}" style="${tab === t.v ? '' : 'color: var(--TextPrimary, #1e293b)'}">${t.t}</a>`).join('')}
</div>
<div id="orderList" class="p-4">
${skeletonOrderHtml(3)}
</div>
</div>`;
const r = await Api.getOrderList(tab, 0);
const list = document.getElementById('orderList');
if (!r || r.status === false) {
list.innerHTML = emptyState('error', '加载失败', (r && r.msg) || '请稍后重试');
return;
}
const orders = r.Data || [];
if (orders.length === 0) {
list.innerHTML = emptyState('inbox', '暂无订单', '快去挑选商品吧', '去逛逛', '#home');
return;
}
list.innerHTML = orders.map(o => orderCardHtml(o, false)).join('');
bindOrderEvents(list, false);
},
// -------- 卖家管理 --------
async seller(params) {
const app = document.getElementById('app');
const edit = params.edit;
if (edit === 'new') { sellerForm(null); return; }
if (edit && edit !== 'new') { sellerForm(edit); return; }
app.innerHTML = `
<div class="flex items-center justify-between mb-4">
<h2 class="text-xl font-bold" style="color: var(--TextPrimary, #1e293b)"><span class="material-symbols-outlined text-primary mr-2">work</span>卖家管理</h2>
<a href="#seller?edit=new" class="bg-primary hover:bg-primaryDark text-white px-4 py-2 rounded-lg text-sm font-semibold transition-colors touchable"><span class="material-symbols-outlined mr-1" style="font-size:16px;vertical-align:middle">add</span>新建商品</a>
</div>
<div id="sellerStats" class="grid grid-cols-2 lg:grid-cols-4 gap-3 mb-5">
${skeletonStatHtml(4)}
</div>
<div id="sellerList" class="bg-card rounded-xl shadow-card p-4">
${skeletonGridHtml(6)}
</div>`;
// 并行拉取商品列表与待发货订单
const [r, ordersR] = await Promise.all([
Api.getMyProducts(),
Api.getOrderList('1', 1)
]);
const box = document.getElementById('sellerList');
const statsEl = document.getElementById('sellerStats');
if (!r || r.status === false) {
box.innerHTML = emptyState('error', '加载失败', (r && r.msg) || '请稍后重试');
statsEl.innerHTML = '';
return;
}
const list = r.Data || [];
// 统计计算
const onSaleCount = list.filter(p => p.Status === 1).length;
const totalSold = list.reduce((s, p) => s + (p.SoldCount !== undefined ? p.SoldCount : (p.Sold || 0)), 0);
const totalRevenue = list.reduce((s, p) => s + (p.SoldCount !== undefined ? p.SoldCount : (p.Sold || 0)) * (p.ProductPrice || 0), 0);
const pendingShipOrders = (ordersR && ordersR.status !== false) ? (ordersR.Data || []).length : 0;
statsEl.innerHTML = `
${statCardHtml('inventory_2', '在售商品', onSaleCount, 'text-primary')}
${statCardHtml('trending_up', '总销量', totalSold, 'text-cyan-500')}
${statCardHtml('local_shipping', '待发货订单', pendingShipOrders, 'text-warning')}
${statCardHtml('payments', '总收入', 'GC ' + fmtPrice(totalRevenue), 'text-success')}
`;
if (list.length === 0) {
box.innerHTML = emptyState('inventory_2', '还没有商品', '点击右上角新建你的第一件商品', '新建商品', '#seller?edit=new');
return;
}
box.innerHTML = `<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">${list.map(p => sellerCardHtml(p)).join('')}</div>`;
bindSellerCards(box);
}
};
// ==================== HTML 片段函数 ====================
function emptyState(icon, title, desc, btnText, btnHref) {
let btn = '';
if (btnText && btnHref) btn = `<a href="${btnHref}" class="inline-block mt-4 bg-primary hover:bg-primaryDark text-white px-6 py-2.5 rounded-lg text-sm font-semibold transition-colors shadow-card"><span class="material-symbols-outlined mr-1" style="font-size:16px;vertical-align:middle">arrow_forward</span>${btnText}</a>`;
return `<div class="col-span-full text-center py-20">
<div class="inline-flex items-center justify-center w-20 h-20 rounded-full mb-4" style="background: var(--HoverBackground, #f0f0f0)">
<span class="material-symbols-outlined text-4xl" style="color: var(--TextSecondary, #94a3b8); opacity: 0.6">${icon}</span>
</div>
<p class="text-base font-medium" style="color: var(--TextPrimary, #475569)">${escapeHtml(title)}</p>
<p class="text-sm mt-1.5" style="color: var(--TextSecondary, #94a3b8)">${escapeHtml(desc)}</p>
${btn}
</div>`;
}
// ==================== 骨架屏(淘宝风格无圆角) ====================
function tbSkeletonGridHtml(count = 10) {
const item = `<div class="bg-white overflow-hidden" style="border-radius:4px">
<div class="skeleton aspect-square" style="border-radius:0"></div>
<div class="p-2.5">
<div class="skeleton h-3 w-full mb-1.5"></div>
<div class="skeleton h-3 w-3/4 mb-2"></div>
<div class="flex justify-between items-end">
<div class="skeleton h-5 w-14"></div>
<div class="skeleton h-3 w-8"></div>
</div>
</div>
</div>`;
return item.repeat(count);
}
function skeletonGridHtml(count = 10) {
return tbSkeletonGridHtml(count);
}
function skeletonRowHtml(count = 6) {
const item = `<div class="bg-white overflow-hidden shrink-0 w-32 sm:w-40" style="border-radius:4px">
<div class="skeleton aspect-square" style="border-radius:0"></div>
<div class="p-2">
<div class="skeleton h-3 w-full mb-1"></div>
<div class="skeleton h-4 w-12 mt-1"></div>
</div>
</div>`;
return item.repeat(count);
}
function skeletonOrderHtml(count = 3) {
const item = `<div class="border rounded-lg p-4 mb-3" style="border-color: var(--BorderColor, #e2e8f0)">
<div class="flex justify-between mb-3">
<div class="skeleton h-4 w-40"></div>
<div class="skeleton h-5 w-16 rounded-full"></div>
</div>
<div class="flex gap-3 mb-3">
<div class="skeleton w-12 h-12 rounded"></div>
<div class="flex-1">
<div class="skeleton h-3 w-32 mb-2"></div>
<div class="skeleton h-3 w-20"></div>
</div>
</div>
<div class="flex justify-between">
<div class="skeleton h-4 w-24"></div>
<div class="skeleton h-7 w-16 rounded"></div>
</div>
</div>`;
return item.repeat(count);
}
function skeletonStatHtml(count = 4) {
const item = `<div class="bg-card rounded-xl shadow-card p-4 flex items-center gap-3">
<div class="skeleton w-11 h-11 rounded-full"></div>
<div class="flex-1">
<div class="skeleton h-3 w-12 mb-2"></div>
<div class="skeleton h-4 w-20"></div>
</div>
</div>`;
return item.repeat(count);
}
// ==================== Banner 轮播控制 ====================
function switchBanner(idx) {
const slides = document.querySelectorAll('.banner-slide');
const dots = document.querySelectorAll('.banner-dot');
const total = slides.length;
if (total === 0) return;
let target = idx;
if (target < 0) target = total - 1;
if (target >= total) target = 0;
App.State.currentBanner = target;
slides.forEach((s, i) => s.classList.toggle('active', i === target));
dots.forEach((d, i) => d.classList.toggle('active', i === target));
}
function startBannerAutoPlay() {
stopBannerAutoPlay();
App.State.bannerTimer = setInterval(() => {
switchBanner(App.State.currentBanner + 1);
}, 4000);
}
function stopBannerAutoPlay() {
if (App.State.bannerTimer) {
clearInterval(App.State.bannerTimer);
App.State.bannerTimer = null;
}
}
function bindBannerControls() {
const prev = document.getElementById('bannerPrev');
const next = document.getElementById('bannerNext');
if (prev) prev.onclick = () => { switchBanner(App.State.currentBanner - 1); startBannerAutoPlay(); };
if (next) next.onclick = () => { switchBanner(App.State.currentBanner + 1); startBannerAutoPlay(); };
document.querySelectorAll('.banner-dot').forEach(d => {
d.onclick = () => { switchBanner(parseInt(d.dataset.idx)); startBannerAutoPlay(); };
});
const container = document.getElementById('bannerContainer');
if (container) {
container.onmouseenter = () => stopBannerAutoPlay();
container.onmouseleave = () => startBannerAutoPlay();
}
}
// ==================== 分类点击 ====================
function bindCategoryClicks() {
const handler = (e) => {
const btn = e.target.closest('[data-cat]');
if (!btn) return;
const cat = btn.dataset.cat;
App.State.currentCategory = cat;
App.State.currentPage = 1;
App.State.keyword = '';
const searchInput = document.getElementById('searchInput');
if (searchInput) searchInput.value = '';
// 更新桌面端分类高亮
document.querySelectorAll('.cat-sidebar-item').forEach(item => {
item.classList.toggle('active', item.dataset.cat === cat);
});
// 更新移动端分类高亮
document.querySelectorAll('.cat-m-item').forEach(item => {
const isActive = item.dataset.cat === cat;
item.style.background = isActive ? 'var(--PrimaryColor, #ff4400)' : 'var(--CardBackground, #fff)';
item.style.borderColor = isActive ? 'var(--PrimaryColor, #ff4400)' : 'var(--BorderColor, #e5e5e5)';
item.style.color = isActive ? '#fff' : 'var(--TextPrimary, #333)';
});
Views.home();
};
document.querySelectorAll('.cat-sidebar-item').forEach(el => el.onclick = handler);
document.querySelectorAll('.cat-m-item').forEach(el => el.onclick = handler);
}
// ==================== 购物车飞入动画 ====================
function flyToCart(imgEl) {
if (!imgEl) return;
const cartIcon = document.querySelector('header a[href="#cart"] .material-symbols-outlined');
if (!cartIcon) return;
const imgRect = imgEl.getBoundingClientRect();
const cartRect = cartIcon.getBoundingClientRect();
const clone = document.createElement('div');
clone.className = 'fly-clone';
clone.style.left = imgRect.left + 'px';
clone.style.top = imgRect.top + 'px';
clone.style.width = Math.max(40, imgRect.width) + 'px';
clone.style.height = Math.max(40, imgRect.height) + 'px';
if (imgEl.src) {
clone.style.backgroundImage = `url(${imgEl.src})`;
} else {
clone.style.background = 'var(--PrimaryColor, #ff4400)';
}
document.body.appendChild(clone);
const dx = (cartRect.left + cartRect.width / 2) - (imgRect.left + imgRect.width / 2);
const dy = (cartRect.top + cartRect.height / 2) - (imgRect.top + imgRect.height / 2);
requestAnimationFrame(() => {
clone.style.setProperty('--dx', dx + 'px');
clone.style.setProperty('--dy', dy + 'px');
clone.classList.add('go');
});
setTimeout(() => { if (clone.parentNode) clone.remove(); }, 850);
}
// ==================== 订单状态进度条 ====================
function orderProgressHtml(status) {
const steps = [
{ key: 0, label: '待付款' },
{ key: 1, label: '已付款' },
{ key: 2, label: '已发货' },
{ key: 3, label: '已完成' },
];
if (status === 4 || status === 5 || status === 6) return '';
return `<div class="order-progress mt-3 pt-3 border-t" style="border-color: var(--BorderColor, #e2e8f0)">
${steps.map(s => {
const done = (status > s.key) || (status === 3 && s.key === 3);
const current = status === s.key && status !== 3;
const cls = done ? 'done' : (current ? 'current' : '');
const icon = (done && !current) ? '<span class="material-symbols-outlined" style="font-size:16px">check</span>' : (s.key + 1);
return `<div class="order-progress-step ${cls}"><div class="dot">${icon}</div><div class="label">${s.label}</div></div>`;
}).join('')}
</div>`;
}
// ==================== 卖家统计卡片 ====================
function statCardHtml(icon, label, value, color) {
return `<div class="bg-card rounded-xl shadow-card p-4 flex items-center gap-3 card-hover">
<div class="w-11 h-11 rounded-full flex items-center justify-center ${color} shrink-0" style="background: var(--HoverBackground, #f0f0f0)">
<span class="material-symbols-outlined text-xl">${icon}</span>
</div>
<div class="min-w-0 flex-1">
<div class="text-xs truncate" style="color: var(--TextSecondary, #64748b)">${escapeHtml(label)}</div>
<div class="text-lg font-bold truncate ${color}">${escapeHtml(String(value))}</div>
</div>
</div>`;
}
// ==================== 淘宝风格商品卡片 ====================
function tbProductCardHtml(p) {
const images = Array.isArray(p.ProductImages) ? p.ProductImages : [];
const price = fmtPrice(p.ProductPrice);
const sold = (p.SoldCount !== undefined ? p.SoldCount : (p.Sold !== undefined ? p.Sold : 0)) || 0;
const stock = p.Stock !== undefined ? p.Stock : 0;
const lowStock = stock > 0 && stock < 10;
const tags = Array.isArray(p.ProductTags) ? p.ProductTags : [];
const isHot = tags.some(t => t.indexOf('热') >= 0 || t.indexOf('爆') >= 0) || sold > 50;
return `
<div class="tb-product product-card" data-uuid="${escapeHtml(p.ProductUUID)}">
<div class="tb-product-img">
${images.length > 0
? `<img src="${imageUrl(images[0])}" alt="${escapeHtml(p.ProductName)}" loading="lazy" onerror="this.outerHTML='${placeholderHtml(p.ProductName).replace(/'/g, '&#39;').replace(/"/g, '&quot;')}'">`
: placeholderHtml(p.ProductName)}
${isHot ? `<span class="tb-product-tag"><span class="material-symbols-outlined mr-0.5" style="font-size:12px">bolt</span>热卖</span>` : ''}
${lowStock ? `<span class="tb-product-tag low-stock" style="${isHot ? 'left:auto;right:0' : ''}"><span class="material-symbols-outlined mr-0.5" style="font-size:12px">error</span>仅剩${stock}</span>` : ''}
</div>
<div class="tb-product-info">
<div class="tb-product-title">${escapeHtml(p.ProductName)}</div>
<div class="tb-product-price">
<span class="sym">GC</span>
<span class="val">${price}</span>
</div>
<div class="tb-product-meta">
<span>${p.ProductCategory || ''}</span>
<span>${sold > 0 ? sold + '人付款' : '新品'}</span>
</div>
</div>
</div>`;
}
function productCardHtml(p) {
return tbProductCardHtml(p);
}
function hotCardHtml(p) {
return tbProductCardHtml(p);
}
function bindProductCards(grid) {
grid.querySelectorAll('.product-card').forEach(card => {
card.addEventListener('click', () => {
const uuid = card.dataset.uuid;
Router.navigate('detail?uuid=' + encodeURIComponent(uuid));
});
});
}
function renderPager(container, current, total, onGo) {
if (total <= 1) { container.innerHTML = ''; return; }
let html = '';
const btn = (label, page, disabled, active) => `<button class="px-3 py-1.5 text-sm rounded border transition-colors ${active ? 'bg-primary text-white border-primary' : 'bg-card hover:bg-gray-50 ' + (disabled ? 'opacity-40 cursor-not-allowed' : '')}" ${disabled ? 'disabled' : ''} data-page="${page}">${label}</button>`;
html += btn('<span class="material-symbols-outlined">chevron_left</span>', current - 1, current <= 1, false);
const start = Math.max(1, current - 2);
const end = Math.min(total, current + 2);
if (start > 1) html += btn('1', 1, false, current === 1);
if (start > 2) html += '<span class="px-1">...</span>';
for (let i = start; i <= end; i++) html += btn(i, i, false, i === current);
if (end < total - 1) html += '<span class="px-1">...</span>';
if (end < total) html += btn(total, total, false, current === total);
html += btn('<span class="material-symbols-outlined">chevron_right</span>', current + 1, current >= total, false);
container.innerHTML = html;
container.querySelectorAll('button[data-page]').forEach(b => {
b.addEventListener('click', () => {
if (!b.disabled) onGo(parseInt(b.dataset.page));
});
});
}
function cartItemHtml(it) {
const images = Array.isArray(it.ProductImages) ? it.ProductImages : (it.Images || []);
const name = it.ProductName || it.Name || '';
const price = it.ProductPrice !== undefined ? it.ProductPrice : (it.Price || 0);
const qty = it.Quantity !== undefined ? it.Quantity : (it.Qty || 1);
const subtotal = Number(price) * qty;
const uuid = it.CartItemUUID || it.UUID || '';
const productUUID = it.ProductUUID || '';
return `
<div class="cart-item flex items-center gap-3 py-3 flex-wrap" data-uuid="${escapeHtml(uuid)}" data-price="${price}">
<input type="checkbox" class="cart-check-item w-4 h-4">
<div class="w-16 h-16 rounded overflow-hidden bg-[var(--HoverBackground)] shrink-0 cursor-pointer" onclick="window.location.hash='detail?uuid=${encodeURIComponent(productUUID)}'">
${images.length > 0
? `<img src="${imageUrl(images[0])}" class="w-full h-full object-cover" onerror="this.outerHTML='${placeholderHtml(name).replace(/'/g, '&#39;').replace(/"/g, '&quot;')}'">`
: placeholderHtml(name)}
</div>
<div class="flex-1 min-w-0">
<p class="text-sm font-medium truncate cursor-pointer" onclick="window.location.hash='detail?uuid=${encodeURIComponent(productUUID)}'">${escapeHtml(name)}</p>
<p class="text-xs text-gray-400 sm:hidden">GC ${fmtPrice(price)}</p>
</div>
<div class="text-sm text-primary w-20 text-center hidden sm:block">GC ${fmtPrice(price)}</div>
<div class="flex items-center border rounded w-28">
<button class="cart-qty-minus px-2 py-1 hover:bg-gray-100"><span class="material-symbols-outlined text-xs">remove</span></button>
<input type="number" value="${qty}" min="1" class="cart-qty-input w-12 text-center text-sm border-0 py-1">
<button class="cart-qty-plus px-2 py-1 hover:bg-gray-100"><span class="material-symbols-outlined text-xs">add</span></button>
</div>
<div class="text-sm font-bold text-primary w-24 text-center cart-subtotal">GC ${fmtPrice(subtotal)}</div>
<button class="cart-remove w-16 text-center text-gray-400 hover:text-red-500 text-sm"><span class="material-symbols-outlined">delete</span></button>
</div>`;
}
function bindCartEvents(box, totalAmount) {
const selCountEl = document.getElementById('selCount');
const totalEl = document.getElementById('totalAmount');
const checkoutBtn = document.getElementById('checkoutBtn');
const selectAll = document.getElementById('selectAll');
function recalcTotal() {
let total = 0, count = 0;
box.querySelectorAll('.cart-item').forEach(item => {
const cb = item.querySelector('.cart-check-item');
if (cb.checked) {
total += Number(item.querySelector('.cart-subtotal').textContent.replace(/GC\s*/, ''));
count++;
}
});
totalEl.textContent = fmtPrice(total);
selCountEl.textContent = count;
checkoutBtn.disabled = count === 0;
const allCbs = box.querySelectorAll('.cart-check-item');
selectAll.checked = allCbs.length > 0 && Array.from(allCbs).every(c => c.checked);
}
selectAll.onchange = () => {
box.querySelectorAll('.cart-check-item').forEach(cb => { cb.checked = selectAll.checked; });
recalcTotal();
};
box.querySelectorAll('.cart-check-item').forEach(cb => { cb.onchange = recalcTotal; });
box.querySelectorAll('.cart-item').forEach(item => {
const uuid = item.dataset.uuid;
const qtyInput = item.querySelector('.cart-qty-input');
const subtotalEl = item.querySelector('.cart-subtotal');
const price = Number(item.dataset.price);
item.querySelector('.cart-qty-minus').onclick = async () => {
let v = parseInt(qtyInput.value) || 1;
if (v <= 1) return;
v--;
const r = await Api.cartUpdate(uuid, v);
if (r && r.status !== false) {
qtyInput.value = v;
subtotalEl.textContent = 'GC ' + fmtPrice(price * v);
recalcTotal();
} else { Toast.error((r && r.msg) || '更新失败'); }
};
item.querySelector('.cart-qty-plus').onclick = async () => {
let v = parseInt(qtyInput.value) || 1;
v++;
const r = await Api.cartUpdate(uuid, v);
if (r && r.status !== false) {
qtyInput.value = v;
subtotalEl.textContent = 'GC ' + fmtPrice(price * v);
recalcTotal();
} else { Toast.error((r && r.msg) || '更新失败'); }
};
qtyInput.onchange = async () => {
let v = parseInt(qtyInput.value) || 1;
if (v < 1) v = 1;
const r = await Api.cartUpdate(uuid, v);
if (r && r.status !== false) {
qtyInput.value = v;
subtotalEl.textContent = 'GC ' + fmtPrice(price * v);
recalcTotal();
} else {
Toast.error((r && r.msg) || '更新失败');
Views.cart();
}
};
item.querySelector('.cart-remove').onclick = async () => {
Modal.confirm({
title: '移除商品',
content: '确定要从购物车中移除该商品吗?',
confirmText: '移除',
confirmClass: 'bg-red-500 hover:bg-red-600',
onConfirm: async () => {
const r = await Api.cartRemove(uuid);
if (r && r.status !== false) {
Toast.success('已移除');
refreshCartBadge();
Views.cart();
} else { Toast.error((r && r.msg) || '移除失败'); return false; }
}
});
};
});
checkoutBtn.onclick = async () => {
const selected = Array.from(box.querySelectorAll('.cart-check-item:checked')).map(cb => cb.closest('.cart-item').dataset.uuid);
if (selected.length === 0) return;
Modal.confirm({
title: '确认结算',
content: `<div class="space-y-1">
<p>已选 ${selected.length} 件商品</p>
<p>合计: <b class="text-primary text-lg">GC ${totalEl.textContent}</b></p>
<p>钱包余额: <b class="${(App.State.walletGC === null || App.State.walletGC < 0) ? 'text-red-500' : 'text-green-600'}">GC ${App.State.walletGC === null ? '未获取' : fmtPrice(App.State.walletGC)}</b></p>
</div>`,
confirmText: '去结算',
onConfirm: async () => {
const r = await Api.createOrder({ FromCart: true, CartItemUUIDs: selected, Remark: '' });
if (r && r.status !== false) {
Toast.success('订单创建成功');
refreshCartBadge();
Router.navigate('orders?tab=0');
} else {
Toast.error((r && r.msg) || '结算失败');
return false;
}
}
});
};
}
function orderCardHtml(o, asSeller) {
const items = o.Items || [];
const status = o.OrderStatus;
const st = OrderStatus[status] || { text: '未知', color: 'bg-gray-100 text-gray-500', dot: 'bg-gray-400' };
let actions = '';
if (!asSeller) {
if (status === 0) actions = `
<button class="pay-btn bg-primary hover:bg-primaryDark text-white px-4 py-1.5 rounded text-sm touchable" data-uuid="${escapeHtml(o.OrderUUID)}">付款</button>
<button class="cancel-btn border border-border hover:bg-gray-100 px-4 py-1.5 rounded text-sm touchable" data-uuid="${escapeHtml(o.OrderUUID)}">取消</button>`;
else if (status === 2) actions = `
<button class="confirm-btn bg-green-500 hover:bg-green-600 text-white px-4 py-1.5 rounded text-sm touchable" data-uuid="${escapeHtml(o.OrderUUID)}">确认收货</button>
<button class="refund-btn border border-yellow-400 text-yellow-600 hover:bg-yellow-50 px-4 py-1.5 rounded text-sm touchable" data-uuid="${escapeHtml(o.OrderUUID)}">申请退款</button>`;
else if (status === 3) actions = `<span class="text-sm" style="color: var(--TextSecondary, #94a3b8)">订单已完成</span>`;
else if (status === 4) actions = `<span class="text-sm" style="color: var(--TextSecondary, #94a3b8)">订单已取消</span>`;
else if (status === 5) actions = `<span class="text-yellow-600 text-sm">退款处理中</span>`;
else if (status === 6) actions = `<span class="text-danger text-sm">已退款</span>`;
else if (status === 1) actions = `<span class="text-blue-500 text-sm">等待卖家发货</span>`;
} else {
if (status === 1) actions = `<button class="ship-btn bg-cyan-500 hover:bg-cyan-600 text-white px-4 py-1.5 rounded text-sm touchable" data-uuid="${escapeHtml(o.OrderUUID)}">发货</button>`;
else if (status === 5) actions = `
<button class="approve-btn bg-green-500 hover:bg-green-600 text-white px-4 py-1.5 rounded text-sm touchable" data-uuid="${escapeHtml(o.OrderUUID)}" data-approve="true">通过退款</button>
<button class="reject-btn bg-red-500 hover:bg-red-600 text-white px-4 py-1.5 rounded text-sm touchable" data-uuid="${escapeHtml(o.OrderUUID)}" data-approve="false">拒绝退款</button>`;
}
return `
<div class="border rounded-lg p-4 mb-3 hover:shadow-md transition-shadow" style="border-color: var(--BorderColor, #e2e8f0)">
<div class="flex items-center justify-between mb-3 pb-3 border-b flex-wrap gap-2" style="border-color: var(--BorderColor, #e2e8f0)">
<div class="text-sm" style="color: var(--TextSecondary, #64748b)">订单号: ${escapeHtml(o.OrderNO || o.OrderUUID)}</div>
<span class="text-xs px-2 py-1 rounded-full ${st.color}">${st.text}</span>
</div>
<div class="space-y-2">
${items.map(it => {
const imgs = Array.isArray(it.ProductImages) ? it.ProductImages : (it.Images || []);
const name = it.ProductName || it.Name || '';
return `
<div class="flex items-center gap-3">
<div class="w-12 h-12 rounded bg-[var(--HoverBackground)] overflow-hidden shrink-0">
${imgs.length > 0 ? `<img src="${imageUrl(imgs[0])}" class="w-full h-full object-cover" onerror="this.outerHTML='${placeholderHtml(name).replace(/'/g, '&#39;').replace(/"/g, '&quot;')}'">` : placeholderHtml(name)}
</div>
<div class="flex-1 min-w-0">
<p class="text-sm truncate" style="color: var(--TextPrimary, #1e293b)">${escapeHtml(name)}</p>
<p class="text-xs" style="color: var(--TextSecondary, #94a3b8)">GC ${fmtPrice(it.ProductPrice || it.Price || 0)} × ${it.Quantity || 1}</p>
</div>
</div>`;
}).join('')}
</div>
<div class="flex items-center justify-between mt-3 pt-3 border-t flex-wrap gap-2" style="border-color: var(--BorderColor, #e2e8f0)">
<div class="text-sm">共 ${items.length} 件 合计: <b class="text-primary text-lg">GC ${fmtPrice(o.TotalAmount)}</b></div>
<div class="flex gap-2">${actions}</div>
</div>
${orderProgressHtml(status)}
${o.RefundReason ? `<div class="mt-2 text-xs text-yellow-700 bg-yellow-50 p-2 rounded">退款原因: ${escapeHtml(o.RefundReason)}</div>` : ''}
</div>`;
}
function bindOrderEvents(container, asSeller) {
if (!asSeller) {
container.querySelectorAll('.pay-btn').forEach(b => {
b.onclick = async () => {
const uuid = b.dataset.uuid;
Modal.confirm({
title: '确认付款',
content: `<div class="space-y-1">
<p>钱包余额: <b class="${(App.State.walletGC === null || App.State.walletGC < 0) ? 'text-red-500' : 'text-green-600'}">GC ${App.State.walletGC === null ? '未获取' : fmtPrice(App.State.walletGC)}</b></p>
<p class="text-sm text-gray-500">付款后将从钱包扣除对应 GC</p>
</div>`,
confirmText: '确认付款',
onConfirm: async () => {
const r = await Api.payOrder(uuid);
if (r && r.status !== false) { Toast.success('付款成功'); refreshWallet(); Views.orders({ tab: App.State.orderTab }); }
else { Toast.error((r && r.msg) || '付款失败'); return false; }
}
});
};
});
container.querySelectorAll('.cancel-btn').forEach(b => {
b.onclick = async () => {
Modal.confirm({
title: '取消订单', content: '确定要取消该订单吗?', confirmText: '确认取消',
onConfirm: async () => {
const r = await Api.cancelOrder(b.dataset.uuid);
if (r && r.status !== false) { Toast.success('已取消'); Views.orders({ tab: App.State.orderTab }); }
else { Toast.error((r && r.msg) || '取消失败'); return false; }
}
});
};
});
container.querySelectorAll('.confirm-btn').forEach(b => {
b.onclick = async () => {
Modal.confirm({
title: '确认收货', content: '确认已收到商品吗?', confirmText: '确认收货', confirmClass: 'bg-green-500 hover:bg-green-600',
onConfirm: async () => {
const r = await Api.confirmOrder(b.dataset.uuid);
if (r && r.status !== false) { Toast.success('已确认收货'); Views.orders({ tab: App.State.orderTab }); }
else { Toast.error((r && r.msg) || '操作失败'); return false; }
}
});
};
});
container.querySelectorAll('.refund-btn').forEach(b => {
b.onclick = async () => {
Modal.prompt({
title: '申请退款', placeholder: '请输入退款原因', confirmText: '提交申请', multiline: true,
onConfirm: async (reason) => {
if (!reason) { Toast.warning('请输入退款原因'); return false; }
const r = await Api.refundOrder(b.dataset.uuid, reason);
if (r && r.status !== false) { Toast.success('退款申请已提交'); Views.orders({ tab: App.State.orderTab }); }
else { Toast.error((r && r.msg) || '提交失败'); return false; }
}
});
};
});
}
}
function sellerCardHtml(p) {
const images = Array.isArray(p.ProductImages) ? p.ProductImages : [];
const status = p.Status !== undefined ? p.Status : 1;
const st = ProductStatus[status] || { text: '-', color: 'bg-gray-100 text-gray-500' };
return `
<div class="border border-border rounded-lg overflow-hidden hover:shadow-md transition-shadow">
<div class="aspect-video bg-[var(--HoverBackground)]">
${images.length > 0
? `<img src="${imageUrl(images[0])}" class="w-full h-full object-cover" onerror="this.outerHTML='${placeholderHtml(p.ProductName).replace(/'/g, '&#39;').replace(/"/g, '&quot;')}'">`
: placeholderHtml(p.ProductName)}
</div>
<div class="p-3">
<div class="flex items-center justify-between">
<h3 class="font-medium truncate">${escapeHtml(p.ProductName)}</h3>
<span class="text-xs px-2 py-0.5 rounded-full ${st.color}">${st.text}</span>
</div>
<div class="flex items-center justify-between mt-2 text-sm">
<span class="text-primary font-bold">GC ${fmtPrice(p.ProductPrice)}</span>
<span class="text-gray-400 text-xs">库存 ${p.Stock !== undefined ? p.Stock : '-'}</span>
</div>
<div class="flex gap-2 mt-3">
<a href="#seller?edit=${encodeURIComponent(p.ProductUUID)}" class="flex-1 text-center text-sm border border-border hover:bg-gray-50 py-1.5 rounded"><span class="material-symbols-outlined mr-1" style="font-size:14px;vertical-align:middle">edit</span>编辑</a>
<button class="status-btn flex-1 text-sm ${status === 1 ? 'border border-orange-400 text-orange-600 hover:bg-orange-50' : 'border border-green-400 text-green-600 hover:bg-green-50'} py-1.5 rounded" data-uuid="${escapeHtml(p.ProductUUID)}" data-status="${status === 1 ? 0 : 1}">${status === 1 ? '下架' : '上架'}</button>
<button class="delete-btn text-sm border border-red-400 text-red-500 hover:bg-red-50 px-3 py-1.5 rounded" data-uuid="${escapeHtml(p.ProductUUID)}"><span class="material-symbols-outlined" style="font-size:16px">delete</span></button>
</div>
</div>
</div>`;
}
function bindSellerCards(container) {
container.querySelectorAll('.status-btn').forEach(b => {
b.onclick = async () => {
const uuid = b.dataset.uuid;
const status = parseInt(b.dataset.status);
const r = await Api.setProductStatus(uuid, status);
if (r && r.status !== false) { Toast.success(status === 1 ? '已上架' : '已下架'); Views.seller({}); }
else Toast.error((r && r.msg) || '操作失败');
};
});
container.querySelectorAll('.delete-btn').forEach(b => {
b.onclick = async () => {
Modal.confirm({
title: '删除商品', content: '确定要删除该商品吗?此操作不可恢复。', confirmText: '删除', confirmClass: 'bg-red-500 hover:bg-red-600',
onConfirm: async () => {
const r = await Api.deleteProduct(b.dataset.uuid);
if (r && r.status !== false) { Toast.success('已删除'); Views.seller({}); }
else { Toast.error((r && r.msg) || '删除失败'); return false; }
}
});
};
});
}
// 卖家商品编辑表单
async function sellerForm(uuid) {
const app = document.getElementById('app');
const isNew = !uuid;
app.innerHTML = `<div class="text-center py-20 text-gray-400"><span class="material-symbols-outlined fa-spinner spinning text-3xl">autorenew</span><p class="mt-2">加载中...</p></div>`;
let p = { ProductName: '', ProductPrice: 0, Stock: 0, ProductDesc: '', ProductImages: [], ProductTags: [], ProductCategory: '' };
if (!isNew) {
const r = await Api.getProductDetail(uuid);
if (r && r.status !== false && r.Data) p = r.Data;
else { app.innerHTML = emptyState('error', '加载失败', '商品不存在', '返回', '#seller'); return; }
}
const images = Array.isArray(p.ProductImages) ? p.ProductImages : [];
const tags = Array.isArray(p.ProductTags) ? p.ProductTags : [];
app.innerHTML = `
<div class="bg-card rounded-xl shadow-card p-6 max-w-2xl mx-auto">
<div class="flex items-center justify-between mb-5">
<h2 class="text-xl font-bold">${isNew ? '新建商品' : '编辑商品'}</h2>
<a href="#seller" class="text-gray-500 hover:text-primary text-sm"><span class="material-symbols-outlined mr-1" style="font-size:14px;vertical-align:middle">arrow_back</span>返回</a>
</div>
<form id="productForm" class="space-y-4">
<div>
<label class="block text-sm font-medium mb-1">商品名称 <span class="text-red-500">*</span></label>
<input name="ProductName" type="text" required value="${escapeHtml(p.ProductName)}" class="w-full border border-border rounded-lg p-2 text-sm" placeholder="请输入商品名称">
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium mb-1">价格 (GC) <span class="text-red-500">*</span></label>
<input name="ProductPrice" type="number" step="0.01" min="0" required value="${p.ProductPrice || 0}" class="w-full border border-border rounded-lg p-2 text-sm">
</div>
<div>
<label class="block text-sm font-medium mb-1">库存 <span class="text-red-500">*</span></label>
<input name="Stock" type="number" min="0" required value="${p.Stock || 0}" class="w-full border border-border rounded-lg p-2 text-sm">
</div>
</div>
<div>
<label class="block text-sm font-medium mb-1">商品分类</label>
<input name="ProductCategory" type="text" value="${escapeHtml(p.ProductCategory || '')}" class="w-full border border-border rounded-lg p-2 text-sm" placeholder="如: 装备/材料/消耗品">
</div>
<div>
<label class="block text-sm font-medium mb-1">商品标签 (逗号分隔)</label>
<input name="ProductTags" type="text" value="${escapeHtml(tags.join(','))}" class="w-full border border-border rounded-lg p-2 text-sm" placeholder="如: 热门,限时,新品">
</div>
<div>
<label class="block text-sm font-medium mb-1">商品图片 SHA1 (每行一个)</label>
<textarea name="ProductImages" rows="4" class="w-full border border-border rounded-lg p-2 text-sm font-mono" placeholder="输入图片资源的 SHA1 哈希值">${escapeHtml(images.join('\n'))}</textarea>
<p class="text-xs text-gray-400 mt-1">图片将通过 /API/Asset/Download/&lt;sha1&gt; 加载</p>
</div>
<div>
<label class="block text-sm font-medium mb-1">商品描述</label>
<textarea name="ProductDesc" rows="4" class="w-full border border-border rounded-lg p-2 text-sm" placeholder="详细描述商品">${escapeHtml(p.ProductDesc || '')}</textarea>
</div>
<div class="flex gap-3 pt-2">
<a href="#seller" class="flex-1 text-center border border-border hover:bg-gray-100 py-2 rounded-lg transition-colors">取消</a>
<button type="submit" class="flex-1 bg-primary hover:bg-primaryDark text-white py-2 rounded-lg font-semibold transition-colors">${isNew ? '创建商品' : '保存修改'}</button>
</div>
</form>
</div>`;
document.getElementById('productForm').onsubmit = async (e) => {
e.preventDefault();
const fd = new FormData(e.target);
const data = {
ProductName: fd.get('ProductName').trim(),
ProductPrice: Number(fd.get('ProductPrice')),
Stock: parseInt(fd.get('Stock')),
ProductDesc: fd.get('ProductDesc').trim(),
ProductCategory: fd.get('ProductCategory').trim(),
ProductImages: fd.get('ProductImages').split('\n').map(s => s.trim()).filter(Boolean),
ProductTags: fd.get('ProductTags').split(',').map(s => s.trim()).filter(Boolean),
};
if (!data.ProductName) { Toast.warning('请输入商品名称'); return; }
let r;
if (isNew) r = await Api.createProduct(data);
else { data.ProductUUID = uuid; r = await Api.modifyProduct(data); }
if (r && r.status !== false) {
Toast.success(isNew ? '创建成功' : '保存成功');
Router.navigate('seller');
} else {
Toast.error((r && r.msg) || '操作失败');
}
};
}
// ==================== 全局刷新 ====================
async function refreshWallet() {
const r = await Api.getWallet();
const el = document.getElementById('walletBalance');
if (!el) return;
if (r && r.status !== false) {
let gc = null;
if (r.GC !== undefined) {
gc = r.GC;
} else if (Array.isArray(r.Balances)) {
const b = r.Balances.find(x => x.Type === 'GC' || x.Symbol === 'GC');
gc = b ? b.Balance : null;
}
App.State.walletGC = gc;
if (gc === null) el.textContent = 'GC: 未绑定';
else if (gc < 0) el.textContent = 'GC: 未绑定';
else el.textContent = 'GC: ' + fmtPrice(gc);
} else {
el.textContent = 'GC: --';
}
}
async function refreshCartBadge() {
const r = await Api.getCartList();
const badge = document.getElementById('cartBadge');
const mobileBadge = document.getElementById('mobileCartBadge');
if (!badge && !mobileBadge) return;
if (r && r.status !== false) {
const items = r.Items || [];
const count = items.length;
App.State.cartCount = count;
const text = count > 99 ? '99+' : String(count);
[badge, mobileBadge].forEach(el => {
if (!el) return;
if (count > 0) {
el.textContent = text;
el.classList.remove('hidden');
} else {
el.classList.add('hidden');
}
});
}
}
// 搜索
function doSearch() {
App.State.keyword = document.getElementById('searchInput').value.trim();
App.State.currentPage = 1;
App.State.currentCategory = '';
// 清空分类高亮
document.querySelectorAll('.cat-sidebar-item').forEach(item => {
item.classList.toggle('active', item.dataset.cat === '');
});
document.querySelectorAll('.cat-m-item').forEach(item => {
const isActive = item.dataset.cat === '';
item.style.background = isActive ? 'var(--PrimaryColor, #ff4400)' : 'var(--CardBackground, #fff)';
item.style.borderColor = isActive ? 'var(--PrimaryColor, #ff4400)' : 'var(--BorderColor, #e5e5e5)';
item.style.color = isActive ? '#fff' : 'var(--TextPrimary, #333)';
});
if (App.State.lastView === 'home') {
Views.home();
} else {
Router.navigate('home?page=1');
}
}
document.getElementById('searchBtn').addEventListener('click', doSearch);
document.getElementById('searchInput').addEventListener('keydown', (e) => {
if (e.key === 'Enter') doSearch();
});
// ==================== 启动 ====================
refreshWallet();
refreshCartBadge();
Router.init();
})(); // end IIFE
// ==================== GERP 主题同步 ====================
window.addEventListener('load', function() {
const checkAndInit = () => {
if (window.GERPThemeSync) {
window.GERPThemeSync.init();
return;
}
setTimeout(checkAndInit, 100);
};
checkAndInit();
if (window.parent !== window) {
window.parent.postMessage({ type: 'GET_THEME' }, '*');
}
});
window.addEventListener('message', function(e) {
if (e.data && e.data.type === 'THEME_CHANGE') {
const colors = e.data.colors || (e.data.data && e.data.data.colors);
if (colors) {
const root = document.documentElement;
Object.entries(colors).forEach(([key, value]) => {
root.style.setProperty('--' + key, value);
});
}
}
});
</script>
</body>
</html>