1688 lines
91 KiB
HTML
1688 lines
91 KiB
HTML
<!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 => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[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, ''').replace(/"/g, '"')}'">`;
|
||
}
|
||
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, ''').replace(/"/g, '"')}'">`
|
||
: 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, ''').replace(/"/g, '"')}'">`;
|
||
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, ''').replace(/"/g, '"')}'">`
|
||
: 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, ''').replace(/"/g, '"')}'">`
|
||
: 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, ''').replace(/"/g, '"')}'">` : 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, ''').replace(/"/g, '"')}'">`
|
||
: 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/<sha1> 加载</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>
|