diff --git a/.gitignore b/.gitignore index 715f96f..7f5020f 100644 --- a/.gitignore +++ b/.gitignore @@ -42,4 +42,5 @@ logs .gocache artifacts/ -frontend/coverage/ \ No newline at end of file +frontend/coverage/ +.zcode/ \ No newline at end of file diff --git a/frontend/src/features/listings/views/ListingDetailView.css b/frontend/src/features/listings/views/ListingDetailView.css index acae137..274523e 100644 --- a/frontend/src/features/listings/views/ListingDetailView.css +++ b/frontend/src/features/listings/views/ListingDetailView.css @@ -1,594 +1,1101 @@ -.detail-back-bar { - /* 吸顶固定:始终显示在顶部导航栏(64px)下方,滚动时不跟随页面移走 */ - position: sticky; - top: 78px; - z-index: 20; - display: flex; - align-items: center; - margin-bottom: 14px; +/* ========================================================================== + PC 商品详情页全新重构样式 (对齐首页设计规范) + ========================================================================== */ + +.pc-detail-page { + width: 100%; + max-width: 1720px; + margin: 0 auto; + min-width: 0; + box-sizing: border-box; } -.detail-back-btn { +/* ---------- 顶部导航与安全保障条 ---------- */ +.top-nav-bar { + position: sticky; + top: 76px; + z-index: 30; + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + margin-bottom: 16px; + padding: 2px 0; +} + +.back-pill-btn { display: inline-flex; align-items: center; gap: 6px; height: 36px; - padding: 0 14px; - border: 1px solid #eef1f5; + padding: 0 16px; + border: 1px solid #e2e8f0; + border-radius: 999px; + background: #ffffff; + color: #475569; + font-size: 13px; + font-weight: 700; + cursor: pointer; + box-shadow: 0 2px 8px rgba(15, 23, 42, 0.04); + transition: all 0.2s ease; +} + +.back-pill-btn:hover { + border-color: #ff8533; + background: #fff7ed; + color: #ea580c; + box-shadow: 0 4px 14px rgba(255, 106, 0, 0.12); + transform: translateX(-2px); +} + +.safe-notice-strip { + display: inline-flex; + align-items: center; + gap: 8px; + height: 36px; + padding: 0 16px; + border-radius: 999px; + background: #fffcf5; + border: 1px solid #fef3c7; + color: #92400e; + font-size: 12px; + box-shadow: 0 1px 4px rgba(245, 158, 11, 0.06); +} + +.safe-icon { + color: #f59e0b; + font-size: 16px; +} + +.safe-text { + font-weight: 500; + white-space: nowrap; +} + +/* ---------- 页面主双栏网格布局 ---------- */ +.pc-detail-layout { + display: grid; + grid-template-columns: minmax(0, 1fr) 380px; + gap: 20px; + align-items: start; +} + +.pc-detail-main { + display: flex; + flex-direction: column; + gap: 16px; + min-width: 0; +} + +/* 通用详情卡片 */ +.detail-card { + border: 1px solid #e8edf5; + border-radius: 16px; + background: #ffffff; + padding: 22px 24px; + box-shadow: 0 4px 20px rgba(15, 23, 42, 0.025); +} + +/* 卡片头部通栏标题 */ +.section-title-bar { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + margin-bottom: 16px; +} + +.title-left { + display: flex; + align-items: baseline; + gap: 10px; +} + +.title-left h2 { + margin: 0; + font-size: 17px; + font-weight: 800; + color: #0f172a; + letter-spacing: -0.01em; +} + +.title-tip { + font-size: 13px; + color: #64748b; +} + +.gallery-count { + font-size: 12px; + font-weight: 600; + color: #94a3b8; +} + +/* ---------- 1. 抬头卡片 (Header Card) ---------- */ +.header-meta-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + flex-wrap: wrap; +} + +.meta-tags-wrap { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 8px; +} + +.code-badge-btn { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 3px 10px; + border-radius: 6px; + background: #eff6ff; + border: 1px solid #bfdbfe; + color: #2563eb; + font-size: 12px; + font-weight: 700; + cursor: pointer; + transition: all 0.15s ease; +} + +.code-badge-btn:hover { + background: #dbeafe; + border-color: #93c5fd; +} + +.platform-pill { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 3px 9px; + border-radius: 6px; + font-size: 12px; + font-weight: 600; +} + +.platform-pill.wechat { + background: #ecfdf5; + color: #059669; +} + +.platform-pill.qq { + background: #eff6ff; + color: #0284c7; +} + +.platform-pill.scan { + background: #fff7ed; + color: #ea580c; +} + +.platform-pill.password, +.platform-pill.default { + background: #f1f5f9; + color: #475569; +} + +.platform-pill.steam { + background: #eef2ff; + color: #4f46e5; +} + +.platform-pill.rank { + background: #f5f3ff; + color: #7c3aed; +} + +.status-pill { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 3px 10px; + border-radius: 6px; + background: #f0fdf4; + border: 1px solid #dcfce7; + color: #15803d; + font-size: 12px; + font-weight: 700; +} + +.status-dot { + width: 6px; + height: 6px; + border-radius: 999px; + background: #16a34a; +} + +.status-pill.is-busy { + background: #fff7ed; + border-color: #fed7aa; + color: #ea580c; +} + +.status-pill.is-busy .status-dot { + background: #ea580c; +} + +.favorite-action-btn { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 6px 14px; + border: 1px solid #e2e8f0; + border-radius: 999px; + background: #ffffff; + color: #64748b; + font-size: 13px; + font-weight: 600; + cursor: pointer; + transition: all 0.2s ease; +} + +.favorite-action-btn:hover { + border-color: #ff8533; + color: #ea580c; + background: #fff7ed; +} + +.favorite-action-btn.is-active { + border-color: #fed7aa; + background: #fff7ed; + color: #ea580c; +} + +.header-title-block { + margin-top: 14px; +} + +.listing-main-title { + margin: 0; + font-size: 22px; + font-weight: 800; + line-height: 1.4; + color: #0f172a; + letter-spacing: -0.015em; +} + +.listing-subtitle { + margin: 6px 0 0; + font-size: 14px; + font-weight: 500; + color: #64748b; +} + +/* 核心资产四大微卡 */ +.core-assets-bar { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 12px; + margin-top: 18px; +} + +.asset-tile { + display: flex; + flex-direction: column; + gap: 6px; + padding: 14px 18px; + border-radius: 12px; + background: #f8fafc; + border: 1px solid #e2e8f0; +} + +.coin-tile { + background: #fff9f5; + border-color: #fed7aa; +} + +.tile-label { + font-size: 12px; + font-weight: 500; + color: #64748b; +} + +.tile-value { + font-size: 24px; + font-weight: 800; + line-height: 1.1; + letter-spacing: -0.02em; +} + +.text-orange { + color: #ea580c; +} + +.text-slate { + color: #0f172a; +} + +.text-amber { + color: #475569; +} + +.tile-subvalues { + display: flex; + flex-wrap: wrap; + gap: 6px; + align-items: center; +} + +.val-pill { + display: inline-flex; + align-items: center; + padding: 3px 8px; + border-radius: 6px; + font-size: 12px; + font-weight: 700; +} + +.val-pill.green { + background: #f0fdf4; + border: 1px solid #dcfce7; + color: #15803d; +} + +.val-pill.warm { + background: #fffbeb; + border: 1px solid #fef3c7; + color: #b45309; +} + +/* ---------- 2. 验号图片展台 (Gallery Card) ---------- */ +.gallery-stage { + position: relative; + width: 100%; + height: 480px; + border-radius: 12px; + overflow: hidden; + background: #0f172a; +} + +.stage-viewer { + width: 100%; + height: 100%; + position: relative; + display: flex; + align-items: center; + justify-content: center; + cursor: zoom-in; +} + +.stage-image { + width: 100%; + height: 100%; + object-fit: contain; +} + +.stage-viewer :deep(.el-image) { + width: 100%; + height: 100%; +} + +.stage-viewer :deep(.el-image__inner) { + width: 100%; + height: 100%; + object-fit: contain; +} + +.stage-tag { + position: absolute; + top: 14px; + left: 14px; + padding: 4px 12px; + border-radius: 6px; + background: rgba(15, 23, 42, 0.75); + backdrop-filter: blur(6px); + color: #ffffff; + font-size: 12px; + font-weight: 600; + z-index: 2; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); +} + +.stage-zoom-hint { + position: absolute; + bottom: 14px; + right: 14px; + display: inline-flex; + align-items: center; + gap: 6px; + padding: 4px 12px; + border-radius: 999px; + background: rgba(15, 23, 42, 0.65); + backdrop-filter: blur(6px); + color: #f1f5f9; + font-size: 12px; + z-index: 2; +} + +.stage-empty { + display: flex; + align-items: center; + justify-content: center; + height: 100%; + color: #64748b; + font-size: 14px; +} + +/* 底部胶卷导轨 */ +.thumbnail-filmstrip { + display: flex; + gap: 10px; + margin-top: 12px; + overflow-x: auto; + padding-bottom: 4px; +} + +.thumbnail-item { + flex: 0 0 116px; + height: 76px; + position: relative; + border-radius: 8px; + overflow: hidden; + border: 2px solid transparent; + background: #0f172a; + cursor: pointer; + padding: 0; + transition: all 0.18s ease; +} + +.thumbnail-item:hover { + border-color: #cbd5e1; + opacity: 0.95; +} + +.thumbnail-item.is-active { + border-color: #ff6a00; + box-shadow: 0 0 0 2px rgba(255, 106, 0, 0.25); +} + +.thumb-img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.thumbnail-item :deep(.el-image) { + width: 100%; + height: 100%; +} + +.thumbnail-item :deep(.el-image__inner) { + width: 100%; + height: 100%; + object-fit: cover; +} + +.thumb-label { + position: absolute; + bottom: 0; + left: 0; + right: 0; + background: rgba(15, 23, 42, 0.78); + color: #ffffff; + font-size: 11px; + font-weight: 500; + text-align: center; + padding: 2px 4px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* ---------- 3. 账号档案卡片 (Specs Card) ---------- */ +.secondary-chips-flow { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-bottom: 14px; +} + +.secondary-spec-pill { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 4px 10px; + border-radius: 6px; + background: #f8fafc; + border: 1px solid #e2e8f0; + font-size: 12px; +} + +.pill-k { + color: #64748b; +} + +.pill-v { + color: #0f172a; + font-weight: 700; +} + +.specs-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 10px; +} + +.spec-cell { + display: flex; + flex-direction: column; + gap: 4px; + padding: 12px 14px; + border-radius: 10px; + background: #f8fafc; + border: 1px solid #edf2f7; +} + +.cell-label { + font-size: 12px; + font-weight: 500; + color: #64748b; +} + +.cell-val { + font-size: 14px; + font-weight: 700; + color: #0f172a; + overflow-wrap: anywhere; +} + +/* ---------- 4. 精选皮肤展台 (Skin Card) ---------- */ +.skin-groups-container { + display: flex; + flex-direction: column; + gap: 14px; +} + +.skin-group-block { + display: flex; + flex-direction: column; + gap: 8px; +} + +.skin-group-header { + display: flex; + align-items: center; + gap: 8px; +} + +.skin-group-title { + font-size: 13px; + font-weight: 700; + color: #334155; +} + +.skin-group-count { + padding: 1px 7px; + border-radius: 999px; + background: #f1f5f9; + color: #64748b; + font-size: 11px; + font-weight: 600; +} + +.skin-pill-wrap { + display: flex; + flex-wrap: wrap; + gap: 6px; +} + +.skin-pill { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 4px 11px; + border-radius: 6px; + font-size: 12px; + font-weight: 600; + background: #f8fafc; + color: #334155; + border: 1px solid transparent; +} + +.quality-dot { + width: 6px; + height: 6px; + border-radius: 999px; +} + +.skin-type-melee { + background: #faf5ff; + color: #7e22ce; + border-color: #f3e8ff; +} +.skin-type-melee .quality-dot { + background: #a855f7; +} + +.skin-type-operatorGold, +.skin-type-weaponGold { + background: #fffbeb; + color: #b45309; + border-color: #fef3c7; +} +.skin-type-operatorGold .quality-dot, +.skin-type-weaponGold .quality-dot { + background: #f59e0b; +} + +.skin-type-operatorRed { + background: #fff1f2; + color: #be123c; + border-color: #ffe4e6; +} +.skin-type-operatorRed .quality-dot { + background: #f43f5e; +} + +.skin-type-weapon, +.skin-type-operator, +.skin-type-other { + background: #f8fafc; + color: #334155; + border-color: #e2e8f0; +} +.skin-type-weapon .quality-dot, +.skin-type-operator .quality-dot, +.skin-type-other .quality-dot { + background: #94a3b8; +} + +/* ---------- 5. 仓库物资专区 (Resources Card) ---------- */ +.resources-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 10px; +} + +.resource-tile { + display: flex; + flex-direction: column; + justify-content: space-between; + gap: 10px; + padding: 14px; + border-radius: 10px; + background: #f8fafc; + border: 1px solid #edf2f7; +} + +.res-top { + display: flex; + justify-content: space-between; + align-items: center; + gap: 8px; +} + +.res-name { + font-size: 13px; + font-weight: 600; + color: #334155; +} + +.res-qty { + font-size: 16px; + font-weight: 800; + color: #ea580c; +} + +.res-bottom { + display: flex; + align-items: center; + justify-content: space-between; +} + +.res-badge { + padding: 2px 8px; + border-radius: 4px; + font-size: 11px; + font-weight: 700; +} + +.res-badge.gift { + background: #ecfdf5; + color: #059669; +} + +.res-badge.charge { + background: #fff7ed; + color: #ea580c; +} + +.res-badge.standard { + background: #f1f5f9; + color: #64748b; +} + +/* ---------- 6. 号主说明卡片 (Note Card) ---------- */ +.seller-note-box { + display: flex; + gap: 12px; + align-items: flex-start; + padding: 16px 18px; + border-radius: 10px; + background: #f8fafc; + border-left: 4px solid #ff8533; +} + +.note-quote-icon { + color: #ff8533; + flex-shrink: 0; + margin-top: 2px; +} + +.note-content { + margin: 0; + font-size: 14px; + line-height: 1.7; + color: #334155; +} + +/* ---------- 7. 平台安全保障卡片 (Trust Card) ---------- */ +.trust-features-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 12px; +} + +.trust-item { + display: flex; + gap: 10px; + align-items: flex-start; + padding: 12px; + border-radius: 10px; + background: #f8fafc; + border: 1px solid #f1f5f9; +} + +.trust-icon-box { + width: 38px; + height: 38px; + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.trust-icon-box.orange { + background: #fff7ed; + color: #ea580c; +} + +.trust-icon-box.blue { + background: #eff6ff; + color: #2563eb; +} + +.trust-icon-box.green { + background: #ecfdf5; + color: #059669; +} + +.trust-icon-box.amber { + background: #fffbeb; + color: #d97706; +} + +.trust-text strong { + display: block; + font-size: 13px; + font-weight: 700; + color: #0f172a; + margin-bottom: 2px; +} + +.trust-text p { + margin: 0; + font-size: 11px; + color: #64748b; + line-height: 1.45; +} + +/* ========================================================================== + 右侧:常驻粘性结算下单面板 (Sticky Order Panel) + ========================================================================== */ +.pc-order-panel { + position: sticky; + top: 76px; + align-self: start; + z-index: 10; +} + +.order-sticky-box { + display: flex; + flex-direction: column; + gap: 16px; + background: #ffffff; + border: 1px solid #e8edf5; + border-radius: 18px; + padding: 22px; + box-shadow: 0 10px 30px rgba(15, 23, 42, 0.06); +} + +.panel-header { + display: flex; + justify-content: space-between; + align-items: flex-start; +} + +.panel-title-wrap { + display: flex; + flex-direction: column; + gap: 2px; +} + +.panel-eyebrow { + display: inline-block; + font-size: 11px; + font-weight: 600; + color: #ff6a00; + letter-spacing: 0.5px; +} + +.panel-title { + margin: 0; + font-size: 20px; + font-weight: 800; + color: #0f172a; + letter-spacing: -0.01em; +} + +.order-status-pill { + display: inline-flex; + align-items: center; + gap: 5px; + padding: 3px 9px; + border-radius: 999px; + background: #ecfdf5; + color: #059669; + font-size: 11px; + font-weight: 700; +} + +.order-status-pill .dot { + width: 6px; + height: 6px; + border-radius: 999px; + background: #10b981; +} + +.order-status-pill.is-busy { + background: #fff7ed; + color: #ea580c; +} + +.order-status-pill.is-busy .dot { + background: #ea580c; +} + +/* 结算总价卡片(高质感暖色渐变卡) */ +.checkout-price-card { + display: flex; + flex-direction: column; + gap: 12px; + padding: 16px; + border-radius: 14px; + background: linear-gradient(135deg, #fffcf5 0%, #fff7ed 100%); + border: 1px solid #fed7aa; +} + +.price-header-row { + display: flex; + justify-content: space-between; + align-items: baseline; +} + +.price-caption { + font-size: 13px; + font-weight: 700; + color: #9a3412; +} + +.price-total-display { + display: flex; + align-items: baseline; + gap: 2px; + color: #ff5500; +} + +.price-total-display small { + font-size: 18px; + font-weight: 700; +} + +.price-total-display strong { + font-size: 34px; + font-weight: 900; + line-height: 1; + letter-spacing: -0.02em; +} + +.price-breakdown-list { + display: flex; + flex-direction: column; + gap: 6px; + padding: 8px 0; + border-top: 1px dashed rgba(234, 88, 12, 0.2); + border-bottom: 1px dashed rgba(234, 88, 12, 0.2); +} + +.breakdown-row { + display: flex; + justify-content: space-between; + font-size: 12px; +} + +.b-label { + color: #7c2d12; +} + +.b-val { + font-weight: 700; + color: #1e293b; +} + +.deposit-guarantee-row { + display: flex; + justify-content: space-between; + align-items: center; +} + +.dep-left { + display: flex; + flex-direction: column; +} + +.dep-title { + font-size: 13px; + font-weight: 700; + color: #9a3412; +} + +.dep-sub { + font-size: 10px; + color: #b45309; +} + +.dep-val { + font-size: 17px; + font-weight: 800; + color: #9a3412; +} + +/* 下单速览核对清单 */ +.order-quick-summary { + display: flex; + flex-direction: column; + gap: 8px; +} + +.summary-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 9px 12px; + border-radius: 8px; + background: #f8fafc; + border: 1px solid #f1f5f9; +} + +.s-label { + font-size: 12px; + color: #64748b; +} + +.s-val { + font-size: 12px; + font-weight: 700; + color: #0f172a; +} + +.s-val.text-green { + color: #15803d; +} + +/* 按钮组 */ +.order-cta-block { + display: flex; + flex-direction: column; + gap: 10px; +} + +.primary-order-btn { + width: 100%; + height: 46px; + border: none; + border-radius: 12px; + background: linear-gradient(135deg, #ff6a00 0%, #ff8533 100%); + color: #ffffff; + font-size: 15px; + font-weight: 800; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + box-shadow: 0 6px 20px rgba(255, 106, 0, 0.28); + transition: all 0.2s ease; +} + +.primary-order-btn:hover:not(:disabled) { + background: linear-gradient(135deg, #ff7a1a 0%, #ff9544 100%); + box-shadow: 0 8px 24px rgba(255, 106, 0, 0.38); + transform: translateY(-2px); +} + +.primary-order-btn:disabled { + background: #cbd5e1; + box-shadow: none; + cursor: not-allowed; +} + +.secondary-favorite-btn { + width: 100%; + height: 40px; + border: 1px solid #e2e8f0; border-radius: 10px; background: #ffffff; color: #475569; font-size: 13px; font-weight: 700; cursor: pointer; - box-shadow: 0 2px 8px rgba(23, 35, 61, 0.08); - transition: - border-color 0.2s ease, - color 0.2s ease, - background 0.2s ease, - box-shadow 0.2s ease; -} - -.detail-back-btn:hover { - border-color: #ff6a00; - background: #fff7ed; - color: #ff6a00; - box-shadow: 0 4px 12px rgba(255, 106, 0, 0.12); -} - -.pc-detail-layout { - grid-template-columns: minmax(0, 1fr) 420px; - align-items: start; -} - -.pc-detail-main { - overflow: hidden; -} - -.detail-hero-card { - position: relative; - display: grid; - height: 340px; - overflow: hidden; - background: #111827; - color: #ffffff; -} - -.detail-hero-card img { - width: 100%; - height: 100%; - object-fit: cover; -} - -.detail-hero-card > span { - display: grid; - height: 340px; - place-items: center; - font-size: 32px; - font-weight: 900; -} - -.detail-hero-card::after { - position: absolute; - inset: 0; - content: ''; - background: - linear-gradient(180deg, rgba(15, 23, 42, 0.06), rgba(15, 23, 42, 0.58)), - linear-gradient(90deg, rgba(15, 23, 42, 0.76), rgba(15, 23, 42, 0.08) 62%); -} - -.detail-hero-overlay { - position: absolute; - inset: auto 0 0; - z-index: 1; - max-width: 860px; - padding: 28px; -} - -.detail-tags { display: flex; - flex-wrap: wrap; - gap: 8px; - margin-bottom: 14px; -} - -.detail-tags span { - display: inline-flex; - align-items: center; - min-height: 30px; - padding: 0 10px; - border: 1px solid rgba(255, 255, 255, 0.34); - border-radius: 999px; - background: rgba(255, 255, 255, 0.14); - color: #ffffff; - font-size: 13px; - font-weight: 900; -} - -.detail-code-chip { - width: fit-content; - margin-top: 10px; - padding: 5px 10px; - border: 1px solid rgba(147, 197, 253, 0.6); - border-radius: 999px; - background: rgba(239, 246, 255, 0.92); - color: #2563eb; - font-size: 12px; - font-weight: 900; - cursor: pointer; -} - -.detail-favorite-chip { - display: inline-flex; - align-items: center; - gap: 6px; - width: fit-content; - margin-top: 8px; - padding: 7px 12px; - border: 1px solid rgba(255, 106, 0, 0.42); - border-radius: 999px; - background: rgba(255, 106, 0, 0.18); - color: #fff7ed; - font-size: 12px; - font-weight: 900; - cursor: pointer; -} - -.detail-hero-overlay h1 { - margin: 0; - color: #ffffff; - font-size: 30px; - line-height: 1.25; -} - -.detail-hero-overlay p { - margin: 8px 0 0; - color: rgba(255, 255, 255, 0.82); - font-size: 16px; - font-weight: 800; -} - -.detail-body { - display: grid; - gap: 18px; - padding: 24px; -} - -.detail-summary-row { - display: grid; - grid-template-columns: repeat(5, minmax(0, 1fr)); - gap: 14px; -} - -.detail-metric, -.detail-section { - border: 1px solid #edf0f4; - border-radius: 16px; - background: #ffffff; -} - -.detail-metric { - padding: 18px; - background: #f8fafc; -} - -.detail-metric span { - display: block; - color: #8b9cb5; - font-size: 13px; - font-weight: 900; -} - -.detail-metric strong { - display: block; - margin-top: 8px; - color: #17233d; - font-size: 28px; - line-height: 1.1; -} - -.detail-metric.is-coin strong { - color: #1477ff; -} - -.detail-metric.is-price strong { - color: #ef4444; -} - -.detail-section { - padding: 22px; -} - -.detail-section-head { - display: flex; - align-items: center; - justify-content: space-between; - gap: 16px; - margin-bottom: 16px; -} - -.detail-section-head h2 { - margin: 0; - color: #17233d; - font-size: 20px; -} - -.detail-section-head span { - color: #8b9cb5; - font-size: 13px; - font-weight: 900; -} - -.detail-chip-row { - display: flex; - flex-wrap: wrap; - gap: 8px; -} - -.detail-chip-row span { - display: inline-flex; - align-items: center; - min-height: 32px; - padding: 0 10px; - border: 1px solid rgba(255, 106, 0, 0.28); - border-radius: 8px; - background: #fff7ed; - color: #ea580c; - font-size: 13px; - font-weight: 900; -} - -.detail-info-grid { - display: grid; - grid-template-columns: repeat(4, minmax(0, 1fr)); - gap: 12px; - margin: 18px 0 0; -} - -.detail-info-grid div { - min-width: 0; - border-radius: 12px; - background: #f8fafc; - padding: 14px; -} - -.detail-info-grid dt { - color: #8b9cb5; - font-size: 12px; - font-weight: 900; -} - -.detail-info-grid dd { - margin: 8px 0 0; - overflow-wrap: anywhere; - color: #17233d; - font-size: 15px; - font-weight: 900; -} - -.detail-resource-grid { - display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 12px; -} - -.detail-resource-card { - display: grid; - grid-template-columns: minmax(0, 1fr) auto; - gap: 8px 12px; - border-radius: 12px; - background: #f8fafc; - padding: 14px; -} - -.detail-resource-card span { - min-width: 0; - color: #475569; - font-size: 14px; - font-weight: 900; -} - -.detail-resource-card strong { - color: #17233d; - font-size: 18px; -} - -.detail-resource-card em { - display: flex; - align-items: center; - justify-content: space-between; - grid-column: 1 / -1; - font-size: 12px; - font-style: normal; - font-weight: 900; -} - -.detail-resource-card em b { - color: #ff6a00; -} - -.detail-resource-card em small { - color: #17233d; - font-size: 13px; - font-weight: 900; -} - -.skin-groups { - display: grid; - gap: 16px; -} - -.skin-group h3 { - margin: 0 0 10px; - color: #475569; - font-size: 15px; -} - -.detail-description { - margin: 0; - color: #52616f; - font-size: 15px; - line-height: 1.8; -} - -.detail-screenshot-grid { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 14px; -} - -.detail-screenshot { - margin: 0; -} - -.detail-screenshot-image { - width: 100%; - aspect-ratio: 16 / 10; - overflow: hidden; - border-radius: 12px; - border: 1px solid #edf0f4; - background: #f8fafc; - cursor: zoom-in; -} - -.detail-screenshot-image :deep(.el-image__inner) { - width: 100%; - height: 100%; - object-fit: cover; -} - -.detail-screenshot figcaption { - margin-top: 8px; - color: #64748b; - font-size: 13px; - font-weight: 800; - text-align: center; -} - -.pc-order-card { - max-width: none; - border-radius: 20px; - padding: 24px; - overflow: hidden; -} - -.order-card-head { - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: 18px; - margin-bottom: 10px; -} - -.order-card-head h2 { - margin: 4px 0 0; - color: #17233d; - font-size: 22px; - line-height: 1.2; -} - -.order-eyebrow { - display: inline-flex; - align-items: center; - min-height: 24px; - border-radius: 999px; - background: #f1f5f9; - padding: 0 10px; - color: #64748b; - font-size: 12px; - font-weight: 900; -} - -.order-state { - display: inline-flex; align-items: center; justify-content: center; - min-width: 58px; - min-height: 28px; - border-radius: 999px; - background: #ecfdf5; - color: #16a34a; - font-size: 12px; - font-weight: 900; - white-space: nowrap; + gap: 6px; + transition: all 0.2s; } -.order-state.is-busy { - background: #fff7ed; +.secondary-favorite-btn:hover { + border-color: #ff8533; color: #ea580c; -} - -.order-form { - display: grid; - gap: 14px; -} - -.order-safe-text { - margin-bottom: 18px; - color: #7a89a5; - font-size: 15px; - font-weight: 700; -} - -.order-total-box { - position: relative; - display: grid; - gap: 12px; - margin-bottom: 0; - border: 1px solid rgba(245, 158, 11, 0.2); - background: - linear-gradient(135deg, rgba(255, 251, 235, 0.96), rgba(255, 247, 237, 0.92)), #fffbeb; - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.86); -} - -.order-total-head { - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: 16px; - padding-bottom: 14px; - border-bottom: 1px solid rgba(217, 119, 6, 0.14); -} - -.order-total-head span { - color: #9a5a16; - font-size: 14px; - font-weight: 900; -} - -.order-total-head strong { - margin: 0; - color: #ef4444; - font-size: 34px; - line-height: 1; - text-align: right; -} - -.order-price-breakdown { - display: grid; - gap: 10px; - padding: 0; -} - -.order-price-breakdown div { - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; -} - -.order-price-breakdown span, -.order-deposit-row span { - color: #8a5a12; - font-size: 13px; - font-weight: 900; -} - -.order-price-breakdown strong, -.order-deposit-row strong { - margin: 0; - color: #17233d; - font-size: 16px; - line-height: 1.2; -} - -.order-deposit-row { - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; - padding-top: 12px; - border-top: 1px solid rgba(217, 119, 6, 0.14); -} - -.order-deposit-row strong { - color: #92400e; - font-size: 18px; -} - -.order-check-list { - display: grid; - gap: 10px; - margin: 0; -} - -.order-check-list div { - display: grid; - grid-template-columns: minmax(0, 1fr) auto; - align-items: center; - gap: 16px; - min-height: 44px; - border: 1px solid #eef2f7; - border-radius: 12px; - background: #f8fafc; - padding: 0 14px; -} - -.order-check-list dt { - color: #8b9cb5; - font-size: 13px; - font-weight: 900; -} - -.order-check-list dd { - margin: 0; - color: #17233d; - font-size: 14px; - font-weight: 900; - text-align: right; - overflow-wrap: anywhere; -} - -.full-control { - width: 100%; -} - -.order-action-group { - display: grid; - gap: 10px; - padding-top: 2px; -} - -.order-primary-btn { - min-height: 48px; - border: 0; - background: #ff8a00; - color: #ffffff; - font-size: 15px; - font-weight: 900; - box-shadow: 0 12px 24px rgba(255, 106, 0, 0.24); -} - -.order-primary-btn:hover, -.order-primary-btn:focus { - background: #ff7a00; - color: #ffffff; -} - -.order-primary-btn.is-disabled, -.order-primary-btn.is-disabled:hover { - background: #f3a94a; - color: rgba(255, 255, 255, 0.86); - box-shadow: none; -} - -.favorite-order-btn { - min-height: 46px; - margin-left: 0; - border-color: #ff6a00; - background: #ffffff; - color: #ff6a00; - font-size: 15px; - font-weight: 900; -} - -.favorite-order-btn:hover, -.favorite-order-btn:focus { - border-color: #ff6a00; background: #fff7ed; - color: #ea580c; } +.secondary-favorite-btn.is-active { + border-color: #fed7aa; + color: #ea580c; + background: #fff7ed; +} + +/* 信任底栏 */ +.order-panel-trust-footer { + padding-top: 4px; +} + +.trust-badge-row { + display: flex; + justify-content: space-around; + font-size: 11px; + color: #94a3b8; + font-weight: 600; +} + +/* ========================================================================== + 协议弹窗样式 + ========================================================================== */ .agreement-dialog-body { display: grid; gap: 14px; @@ -598,7 +1105,7 @@ margin: 0; color: #64748b; font-size: 14px; - font-weight: 700; + font-weight: 600; } .agreement-grid { @@ -615,62 +1122,71 @@ .agreement-panel h3 { margin: 0; - color: #17233d; - font-size: 16px; - font-weight: 900; + color: #0f172a; + font-size: 15px; + font-weight: 800; } .agreement-content { height: 260px; overflow: auto; - border: 1px solid #d8dee9; + border: 1px solid #e2e8f0; border-radius: 8px; background: #f8fafc; padding: 14px; - color: #1f2937; + color: #334155; font-size: 13px; - font-weight: 700; + font-weight: 500; line-height: 1.75; white-space: pre-wrap; } .agreement-read-hint { - color: #f97316; + color: #ea580c; font-size: 12px; - font-weight: 800; + font-weight: 700; } -@media (max-width: 1180px) { - .pc-detail-layout, - .detail-screenshot-grid { +/* ========================================================================== + 响应式断点 + ========================================================================== */ +@media (max-width: 1200px) { + .pc-detail-layout { grid-template-columns: 1fr; } - .detail-summary-row, - .detail-info-grid, - .detail-resource-grid { - grid-template-columns: repeat(2, minmax(0, 1fr)); + .pc-order-panel { + position: static; } - .detail-hero-card, - .detail-hero-card img, - .detail-hero-card > span { - height: 300px; + .gallery-stage { + height: 380px; } - .detail-hero-overlay { - padding: 24px; - } - - .detail-hero-overlay h1 { - font-size: 26px; + .trust-features-grid { + grid-template-columns: repeat(2, 1fr); } } -@media (max-width: 720px) { - .detail-summary-row, - .detail-info-grid, - .detail-resource-grid { +@media (max-width: 768px) { + .top-nav-bar { + flex-direction: column; + align-items: flex-start; + } + + .core-assets-bar { + grid-template-columns: repeat(2, 1fr); + } + + .specs-grid { + grid-template-columns: repeat(2, 1fr); + } + + .resources-grid { + grid-template-columns: 1fr; + } + + .trust-features-grid { grid-template-columns: 1fr; } diff --git a/frontend/src/features/listings/views/ListingDetailView.vue b/frontend/src/features/listings/views/ListingDetailView.vue index 5b4edf6..9b1f2fc 100644 --- a/frontend/src/features/listings/views/ListingDetailView.vue +++ b/frontend/src/features/listings/views/ListingDetailView.vue @@ -3,7 +3,7 @@ import { readError } from '@/shared/utils/error' import { ElMessage, ElMessageBox } from 'element-plus' import { computed, nextTick, onMounted, ref, watch } from 'vue' import { useRoute, useRouter } from 'vue-router' -import { ShoppingCart, Star, StarFilled, ArrowLeft } from '@element-plus/icons-vue' +import { ShoppingCart, Star, StarFilled, ArrowLeft, DocumentCopy } from '@element-plus/icons-vue' import { goBackOrHome } from '@/shared/utils/routerBack' import { fetchListing, type Listing } from '@/features/listings/api/listings' @@ -38,6 +38,8 @@ import { getServerRegion, readAssetNumber, readAssetString, + getRatioValue, + type ListingDisplayResource, } from '@/shared/utils/listingDisplay' const route = useRoute() @@ -56,6 +58,8 @@ const virtualAgreementRef = ref(null) const renterAgreementRef = ref(null) const { recordListingView, isFavorite, toggleFavorite } = useListingCollections() +const activeImageIndex = ref(0) + const canCreateOrderAfterAgreement = computed( () => virtualAgreementRead.value && @@ -97,32 +101,57 @@ const orderPriceBreakdown = computed(() => { } }) -const coverURL = computed(() => { +// 核心资产微卡计算(同首页设计标准) +const hafCoinDisplay = computed(() => { + if (!listing.value) return '--' + return formatHafCoinM(getCoinWan(listing.value)) +}) + +const insuranceSlot = computed(() => { + if (!listing.value) return '--' + return readAssetString(listing.value, 'season_insurance') || '--' +}) + +const ratioValue = computed(() => { + if (!listing.value) return 0 + return getRatioValue(listing.value) +}) + +const ratioDisplay = computed(() => { + if (ratioValue.value <= 0) return null + return `1元 = ${ratioValue.value.toFixed(1)}w` +}) + +const rentalDuration = computed(() => { + if (!listing.value) return '--' + return formatEstimatedRentalDuration(listing.value) +}) + +const dailyLoss = computed(() => { if (!listing.value) return '' - return listing.value.cover_url || listing.value.screenshot_urls?.[0] || '' + return getDailyLoss(listing.value) }) -const detailMetrics = computed(() => { +// 平台/区服特性徽标 +const accessTags = computed(() => { if (!listing.value) return [] - const dailyLoss = getDailyLoss(listing.value) - const secretKD = readAssetNumber(listing.value, 'secret_kd') - return [ - { label: '纯币', value: formatHafCoinM(getCoinWan(listing.value)), tone: 'coin' }, - { - label: '绝密KD', - value: secretKD > 0 ? formatAssetNumber(secretKD) : '--', - tone: 'coin', - }, - { - label: '日损耗', - value: dailyLoss ? `${dailyLoss}/天` : '--', - tone: 'coin', - }, - { label: '价格', value: `¥${orderTotal.value}`, tone: 'price' }, - { label: '押金', value: `¥${formatCent(listing.value.deposit_amount_cent)}`, tone: '' }, - ] + return [getServerRegion(listing.value), getLoginMethod(listing.value)] + .filter(Boolean) + .map(item => accessBadgeMeta(item)) }) +function accessBadgeMeta(value: string) { + const text = value.trim() + if (text.includes('微信')) return { label: text, icon: 'wechat', tone: 'wechat' } + if (text.toUpperCase().includes('QQ')) return { label: text, icon: 'qq', tone: 'qq' } + if (text.includes('扫码')) return { label: text, icon: 'scan', tone: 'scan' } + if (text.includes('账') || text.includes('密码')) + return { label: text, icon: 'lock', tone: 'password' } + if (text.includes('Steam')) return { label: text, icon: 'desktop-o', tone: 'steam' } + return { label: text, icon: 'bookmark-o', tone: 'default' } +} + +// 截图多图展台 const detailScreenshots = computed(() => { if (!listing.value) return [] const groupedScreenshots = readGroupedScreenshots(listing.value) @@ -135,15 +164,20 @@ const detailScreenshots = computed(() => { }) const detailScreenshotUrls = computed(() => detailScreenshots.value.map(shot => shot.url)) +const activeScreenshot = computed(() => { + if (detailScreenshots.value.length === 0) return null + return detailScreenshots.value[activeImageIndex.value] || detailScreenshots.value[0] +}) + function readGroupedScreenshots(item: Listing) { const groups = item.asset_summary?.screenshot_groups if (typeof groups !== 'object' || groups === null) return [] const slots = [ { key: 'coin', label: '纯币截图' }, - { key: 'gameId', label: '游戏ID截图' }, - { key: 'totalAsset', label: '总资产截图' }, { key: 'tencentSecurity', label: '腾讯安全中心截图' }, { key: 'skin', label: '皮肤截图' }, + { key: 'gameId', label: '游戏ID截图' }, + { key: 'totalAsset', label: '总资产截图' }, ] return slots.flatMap(slot => { const urls = (groups as Record)[slot.key] @@ -156,30 +190,51 @@ function readGroupedScreenshots(item: Listing) { }) } +// 皮肤清单(带高光品级) const detailSkinGroups = computed(() => { if (!listing.value) return [] const groups = listing.value.asset_summary?.skin_groups if (typeof groups !== 'object' || groups === null) return [] - const titles: Record = { - melee: '近战皮肤', - operator: '干员皮肤', - operatorGold: '干员金皮', - operatorRed: '干员红皮', - weapon: '武器皮肤', - other: '其他皮肤', - imported: '其他皮肤', + const titles: Record = { + melee: { title: '近战皮肤', type: 'melee' }, + operatorRed: { title: '干员红皮', type: 'operatorRed' }, + operatorGold: { title: '干员金皮', type: 'operatorGold' }, + operator: { title: '干员皮肤', type: 'operator' }, + weapon: { title: '武器皮肤', type: 'weapon' }, + other: { title: '其他皮肤', type: 'other' }, + imported: { title: '其他皮肤', type: 'other' }, } return Object.entries(groups as Record) - .map(([key, value]) => ({ - key, - title: titles[key] || key, - options: Array.isArray(value) - ? value.filter((skin): skin is string => typeof skin === 'string') - : [], - })) + .map(([key, value]) => { + const meta = titles[key] || { title: key, type: 'other' } + return { + key, + type: meta.type, + title: meta.title, + options: Array.isArray(value) + ? value.filter((skin): skin is string => typeof skin === 'string') + : [], + } + }) .filter(group => group.options.length) }) +const totalSkinCount = computed(() => { + return detailSkinGroups.value.reduce((acc, g) => acc + g.options.length, 0) +}) + +// 规格胶囊(过滤已展示的哈夫币、保险、AWM) +const secondaryChips = computed(() => { + if (!listing.value) return [] + return getListingChips(listing.value).filter( + chip => + chip.label !== '哈夫币' && + chip.label !== '保险格数' && + chip.label !== 'AWM' + ) +}) + +// 账号基础档案 12 宫格 const accountRows = computed(() => { if (!listing.value) return [] const regions = assetRegions(listing.value) @@ -197,10 +252,22 @@ const accountRows = computed(() => { { label: '方便上号', value: getOnlineTimeText(listing.value) || '--' }, { label: '预计可租', value: formatEstimatedRentalDuration(listing.value) }, { label: '常用登录地', value: regions.length ? regions.join('、') : '--' }, - { label: '封禁记录', value: readAssetString(listing.value, 'ban_record') || '无' }, + { label: '封禁记录', value: readAssetString(listing.value, 'ban_record') || '无封禁记录' }, ] }) +// 仓库物资 +const resourceItems = computed(() => { + if (!listing.value) return [] + return getListingResources(listing.value) +}) + +function formatResourceQuantity(resource: ListingDisplayResource) { + const suffix = resource.key === 'awmAmmo' ? '发' : '' + const val = Number.isInteger(resource.quantity) ? String(resource.quantity) : resource.quantity.toFixed(1) + return `${val}${suffix}` +} + async function handleCreateOrder() { if (!listing.value) return if (!session.token) { @@ -279,7 +346,7 @@ async function copyListingCode() { if (!listing.value) return try { await navigator.clipboard.writeText(formatListingCode(listing.value)) - ElMessage.success('商品编号已复制') + ElMessage.success('商品编号已复制到剪贴板') } catch { ElMessage.error('复制失败') } @@ -300,47 +367,33 @@ function updateAgreementReadState(type: 'virtual' | 'renter') { } } -// 手动锁定/解锁背景滚动 +// 锁定/解锁背景滚动 watch(agreementVisible, visible => { - // 只锁定 .pc-main 容器,不影响 body 和 sticky 导航栏 const pcMain = document.querySelector('.pc-main') as HTMLElement if (!pcMain) return if (visible) { - // 保存当前滚动位置 const scrollTop = pcMain.scrollTop || 0 - - // 锁定主容器滚动 pcMain.style.overflow = 'hidden' pcMain.style.position = 'fixed' pcMain.style.top = `-${scrollTop}px` pcMain.style.left = '0' pcMain.style.right = '0' pcMain.style.width = '100%' - - // 保存滚动位置供恢复使用 pcMain.dataset.scrollTop = String(scrollTop) } else { - // 恢复主容器滚动 const scrollTop = parseInt(pcMain.dataset.scrollTop || '0', 10) - pcMain.style.overflow = '' pcMain.style.position = '' pcMain.style.top = '' pcMain.style.left = '' pcMain.style.right = '' pcMain.style.width = '' - - // 恢复滚动位置 pcMain.scrollTop = scrollTop delete pcMain.dataset.scrollTop } }) -function listingPrice(item: Listing) { - return formatMoney(getListingDisplayPrice(item)) -} - function handleToggleFavorite() { if (!listing.value) return const added = toggleFavorite(listing.value) @@ -353,228 +406,412 @@ function goBack() {