优化详情页和发布界面等

This commit is contained in:
yml2213
2026-06-01 10:43:16 +08:00
parent ff4ab37546
commit 2d883eb2cb
7 changed files with 993 additions and 39 deletions
+2
View File
@@ -43,6 +43,8 @@ declare module 'vue' {
ElSwitch: typeof import('element-plus/es')['ElSwitch']
ElTable: typeof import('element-plus/es')['ElTable']
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
ElTabPane: typeof import('element-plus/es')['ElTabPane']
ElTabs: typeof import('element-plus/es')['ElTabs']
ElTag: typeof import('element-plus/es')['ElTag']
ElTimePicker: typeof import('element-plus/es')['ElTimePicker']
MobileBottomNav: typeof import('./src/components/MobileBottomNav.vue')['default']
+158 -2
View File
@@ -6,14 +6,19 @@ import {
ArrowDown,
ChatDotRound,
CirclePlus,
Coin,
DocumentChecked,
EditPen,
Finished,
House,
Search,
Service,
Shop,
SwitchButton,
Tickets,
UserFilled,
Van,
VideoPlay,
Wallet,
} from "@element-plus/icons-vue";
import { ensureSupportChat } from "@/api/chats";
@@ -28,6 +33,17 @@ const navItems = [
{ label: "消息", to: "/messages", icon: ChatDotRound },
{ label: "钱包", to: "/wallet", icon: Wallet },
];
const buyerServiceItems = [
{ label: "待支付", icon: Coin, to: { path: "/orders", query: { tab: "pending_payment" } } },
{ label: "待交接", icon: Van, to: { path: "/orders", query: { tab: "pending_handoff" } } },
{ label: "使用中", icon: VideoPlay, to: { path: "/orders", query: { tab: "renting" } } },
{ label: "已完成", icon: Finished, to: { path: "/orders", query: { tab: "completed" } } },
];
const sellerServiceItems = [
{ label: "发布商品", icon: CirclePlus, to: "/seller/listings/create" },
{ label: "我的商品", icon: Shop, to: "/seller/listings" },
{ label: "提现/账单", icon: Wallet, to: "/wallet" },
];
const showUserDropdown = ref(false);
const supportLoading = ref(false);
@@ -187,6 +203,51 @@ async function handleSupportClick() {
{{ realnameBadge.text }}
</span>
</RouterLink>
<div class="dropdown-divider"></div>
<section class="dropdown-service-group">
<div class="dropdown-service-head">
<span>买家服务</span>
<RouterLink
class="dropdown-service-more"
:to="{ path: '/orders', query: { tab: 'all' } }"
@click="showUserDropdown = false"
>
全部订单
</RouterLink>
</div>
<div class="dropdown-service-grid">
<RouterLink
v-for="item in buyerServiceItems"
:key="item.label"
class="dropdown-service-card"
:to="item.to"
@click="showUserDropdown = false"
>
<el-icon><component :is="item.icon" /></el-icon>
<span>{{ item.label }}</span>
</RouterLink>
</div>
</section>
<section class="dropdown-service-group">
<div class="dropdown-service-head">
<span>卖家服务</span>
</div>
<div class="dropdown-seller-list">
<RouterLink
v-for="item in sellerServiceItems"
:key="item.label"
class="dropdown-seller-item"
:to="item.to"
@click="showUserDropdown = false"
>
<el-icon><component :is="item.icon" /></el-icon>
<span>{{ item.label }}</span>
</RouterLink>
</div>
</section>
<div class="dropdown-divider"></div>
<button class="dropdown-item logout" @click="handleLogout">
<el-icon><SwitchButton /></el-icon>
@@ -564,8 +625,8 @@ async function handleSupportClick() {
}
.pc-user-dropdown {
min-width: 200px;
padding: 8px;
width: 324px;
padding: 10px;
border-radius: 14px;
background: #fff;
border: 1px solid #eef1f5;
@@ -638,6 +699,101 @@ async function handleSupportClick() {
background: #f1f5f9;
}
.dropdown-service-group {
padding: 6px 4px 4px;
}
.dropdown-service-head {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 8px 8px;
color: #17233d;
font-size: 13px;
font-weight: 900;
}
.dropdown-service-more {
color: #64748b;
font-size: 12px;
font-weight: 800;
text-decoration: none;
}
.dropdown-service-more:hover {
color: #ff6a00;
}
.dropdown-service-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 8px;
}
.dropdown-service-card,
.dropdown-seller-item {
text-decoration: none;
transition: all 0.2s;
}
.dropdown-service-card {
display: grid;
min-width: 0;
min-height: 68px;
place-items: center;
gap: 6px;
padding: 8px 4px;
border-radius: 10px;
background: #f8fafc;
color: #475569;
font-size: 12px;
font-weight: 800;
text-align: center;
}
.dropdown-service-card .el-icon {
color: #ff6a00;
font-size: 18px;
}
.dropdown-service-card:hover {
background: #fff3e8;
color: #ff6a00;
transform: translateY(-1px);
}
.dropdown-seller-list {
display: grid;
gap: 6px;
}
.dropdown-seller-item {
display: flex;
align-items: center;
gap: 10px;
min-height: 36px;
padding: 0 10px;
border-radius: 10px;
background: #f8fafc;
color: #475569;
font-size: 13px;
font-weight: 800;
}
.dropdown-seller-item .el-icon {
color: #94a3b8;
font-size: 16px;
}
.dropdown-seller-item:hover {
background: #fff3e8;
color: #ff6a00;
}
.dropdown-seller-item:hover .el-icon {
color: #ff6a00;
}
/* 下拉动画 */
.dropdown-enter-active,
.dropdown-leave-active {
+2 -2
View File
@@ -17,12 +17,12 @@ body {
min-width: 320px;
min-height: 100vh;
max-width: 100%;
overflow-x: hidden;
overflow-x: clip;
}
#app {
max-width: 100%;
overflow-x: hidden;
overflow-x: clip;
}
a {
+59 -2
View File
@@ -1,6 +1,7 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { computed, onMounted, ref, watch } from 'vue'
import { ElMessage } from 'element-plus'
import { useRoute, useRouter } from 'vue-router'
import { fetchOrders, payOrder, type Order } from '@/api/orders'
import { useSessionStore } from '@/stores/session'
@@ -10,9 +11,43 @@ const loading = ref(false)
const orders = ref<Order[]>([])
const payingOrderId = ref<number | null>(null)
const session = useSessionStore()
const route = useRoute()
const router = useRouter()
const statusTabs = [
{ key: 'all', label: '全部' },
{ key: 'pending_payment', label: '待支付' },
{ key: 'pending_handoff', label: '待交接' },
{ key: 'renting', label: '使用中' },
{ key: 'pending_checkout_confirm', label: '待结账' },
{ key: 'completed', label: '已完成' },
]
const tabKeys = new Set(statusTabs.map((item) => item.key))
const activeTab = ref(readTab(route.query.tab))
const displayOrders = computed(() => {
if (activeTab.value === 'all') return orders.value
return orders.value.filter((order) => order.status === activeTab.value)
})
onMounted(loadOrders)
watch(
() => route.query.tab,
(tab) => {
activeTab.value = readTab(tab)
},
)
function readTab(tab: unknown) {
const value = typeof tab === 'string' ? tab : 'all'
return tabKeys.has(value) ? value : 'all'
}
function handleTabChange(tab: string | number) {
const nextTab = readTab(String(tab))
router.replace({ path: '/orders', query: nextTab === 'all' ? {} : { tab: nextTab } })
}
async function loadOrders() {
loading.value = true
try {
@@ -62,7 +97,11 @@ function money(value: unknown) {
<p>跟踪待支付待交接使用中待归还申诉中和已完成订单</p>
</div>
<el-table v-loading="loading" class="table-panel" :data="orders">
<el-tabs v-model="activeTab" class="order-tabs" @tab-change="handleTabChange">
<el-tab-pane v-for="tab in statusTabs" :key="tab.key" :label="tab.label" :name="tab.key" />
</el-tabs>
<el-table v-loading="loading" class="table-panel" :data="displayOrders">
<el-table-column prop="order_no" label="订单号" min-width="210" />
<el-table-column prop="title" label="账号" min-width="180" />
<el-table-column label="我的金额" width="100">
@@ -97,3 +136,21 @@ function money(value: unknown) {
</el-table>
</section>
</template>
<style scoped>
.order-tabs {
margin-top: 24px;
border: 1px solid #e4e7ed;
border-radius: 8px;
background: #ffffff;
padding: 0 16px;
}
.order-tabs :deep(.el-tabs__header) {
margin: 0;
}
.order-tabs :deep(.el-tabs__nav-wrap::after) {
height: 0;
}
</style>
+203 -1
View File
@@ -1,5 +1,22 @@
<script setup lang="ts">
import { Camera, CircleCheckFilled, EditPen, Postcard, RefreshRight, User, WarningFilled } from '@element-plus/icons-vue'
import {
Camera,
CircleCheckFilled,
CirclePlus,
Coin,
EditPen,
Finished,
Goods,
Postcard,
RefreshRight,
Shop,
Tickets,
User,
Van,
VideoPlay,
Wallet,
WarningFilled,
} from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import { computed, onMounted, reactive, ref } from 'vue'
import { useRouter } from 'vue-router'
@@ -17,6 +34,17 @@ const form = reactive({
nickname: '',
avatar_url: '',
})
const buyerServices = [
{ label: '待支付', icon: Coin, tone: 'warning', to: { path: '/orders', query: { tab: 'pending_payment' } } },
{ label: '待交接', icon: Van, tone: 'info', to: { path: '/orders', query: { tab: 'pending_handoff' } } },
{ label: '使用中', icon: VideoPlay, tone: 'primary', to: { path: '/orders', query: { tab: 'renting' } } },
{ label: '已完成', icon: Finished, tone: 'success', to: { path: '/orders', query: { tab: 'completed' } } },
]
const sellerServices = [
{ label: '发布商品', icon: CirclePlus, tone: 'orange', to: '/seller/listings/create' },
{ label: '我的商品', icon: Shop, tone: 'purple', to: '/seller/listings' },
{ label: '提现/账单', icon: Wallet, tone: 'teal', to: '/wallet' },
]
const displayName = computed(() => session.displayName)
const maskedPhone = computed(() => {
@@ -192,6 +220,44 @@ function readError(error: unknown, fallback: string) {
</button>
</aside>
</div>
<div class="service-panels">
<section class="service-panel">
<div class="service-head">
<h2>买家服务</h2>
<RouterLink class="service-all" :to="{ path: '/orders', query: { tab: 'all' } }">
<span>全部订单</span>
<el-icon><Tickets /></el-icon>
</RouterLink>
</div>
<div class="service-grid buyer-grid">
<RouterLink v-for="item in buyerServices" :key="item.label" class="service-item" :to="item.to">
<span class="service-icon" :class="`is-${item.tone}`">
<el-icon><component :is="item.icon" /></el-icon>
</span>
<strong>{{ item.label }}</strong>
</RouterLink>
</div>
</section>
<section class="service-panel">
<div class="service-head">
<h2>卖家服务</h2>
<RouterLink class="service-all" to="/seller/listings">
<span>管理发布</span>
<el-icon><Goods /></el-icon>
</RouterLink>
</div>
<div class="service-grid seller-grid">
<RouterLink v-for="item in sellerServices" :key="item.label" class="service-item" :to="item.to">
<span class="service-icon" :class="`is-${item.tone}`">
<el-icon><component :is="item.icon" /></el-icon>
</span>
<strong>{{ item.label }}</strong>
</RouterLink>
</div>
</section>
</div>
</section>
</template>
@@ -504,9 +570,145 @@ function readError(error: unknown, fallback: string) {
color: #ff6a00;
}
.service-panels {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 22px;
margin-top: 22px;
}
.service-panel {
padding: 24px;
border: 1px solid #eef1f5;
border-radius: 16px;
background: #ffffff;
box-shadow: 0 10px 28px rgba(23, 35, 61, 0.06);
}
.service-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 18px;
}
.service-head h2 {
margin: 0;
color: #17233d;
font-size: 22px;
}
.service-all {
display: inline-flex;
align-items: center;
gap: 6px;
color: #64748b;
font-size: 14px;
font-weight: 900;
text-decoration: none;
}
.service-all:hover {
color: #ff6a00;
}
.service-grid {
display: grid;
gap: 14px;
margin-top: 22px;
}
.buyer-grid {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.seller-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.service-item {
display: grid;
min-width: 0;
min-height: 118px;
place-items: center;
gap: 10px;
padding: 16px 10px;
border: 1px solid #edf2f7;
border-radius: 14px;
background: #fbfdff;
color: #1f2937;
text-align: center;
text-decoration: none;
transition:
border-color 0.18s ease,
box-shadow 0.18s ease,
transform 0.18s ease;
}
.service-item:hover {
border-color: rgba(255, 106, 0, 0.35);
box-shadow: 0 12px 24px rgba(23, 35, 61, 0.08);
transform: translateY(-2px);
}
.service-item strong {
color: #334155;
font-size: 15px;
font-weight: 900;
line-height: 1.35;
}
.service-icon {
display: grid;
width: 58px;
height: 58px;
place-items: center;
border-radius: 14px;
font-size: 28px;
}
.service-icon.is-warning {
background: #fff7ed;
color: #f59e0b;
}
.service-icon.is-info {
background: #eff6ff;
color: #2563eb;
}
.service-icon.is-primary {
background: #eef2ff;
color: #4f46e5;
}
.service-icon.is-success {
background: #ecfdf3;
color: #10b981;
}
.service-icon.is-orange {
background: #fff1e8;
color: #ff6a00;
}
.service-icon.is-purple {
background: #f3e8ff;
color: #8b5cf6;
}
.service-icon.is-teal {
background: #e8f7f5;
color: #0f766e;
}
@media (max-width: 1100px) {
.profile-layout {
grid-template-columns: 1fr;
}
.service-panels {
grid-template-columns: 1fr;
}
}
</style>
+556 -25
View File
@@ -1,13 +1,33 @@
<script setup lang="ts">
import { ElMessage } from "element-plus";
import { onMounted, ref } from "vue";
import { computed, onMounted, ref } from "vue";
import { useRoute, useRouter } from "vue-router";
import { fetchListing, type Listing } from "@/api/listings";
import { createOrder } from "@/api/orders";
import { useSessionStore } from "@/stores/session";
import {
assetRegions,
formatEstimatedRentalDuration,
formatHafCoinM,
formatRatio,
getCoinWan,
getDailyLoss,
getListingChips,
getListingDisplayPrice,
getListingResources,
getListingSubtitle,
getListingTitle,
getOnlineTimeText,
getLoginMethod,
getServerRegion,
readAssetNumber,
readAssetString,
} from "@/utils/listingDisplay";
const route = useRoute();
const router = useRouter();
const session = useSessionStore();
const loading = ref(false);
const ordering = ref(false);
const listing = ref<Listing | null>(null);
@@ -21,8 +41,84 @@ onMounted(async () => {
}
});
const orderTotal = computed(() => {
if (!listing.value) return "0";
return `${Math.round(getListingDisplayPrice(listing.value))}`;
});
const coverURL = computed(() => {
if (!listing.value) return "";
return listing.value.cover_url || listing.value.screenshot_urls?.[0] || "";
});
const detailMetrics = computed(() => {
if (!listing.value) return [];
const totalAssetWan = readAssetNumber(listing.value, "total_asset_wan");
return [
{ label: "纯币", value: formatHafCoinM(getCoinWan(listing.value)), tone: "coin" },
{
label: "总资产",
value: totalAssetWan > 0 ? formatHafCoinM(totalAssetWan) : "--",
tone: "coin",
},
{ label: "价格", value: `¥${orderTotal.value}`, tone: "price" },
{ label: "押金", value: `¥${listing.value.deposit_amount}`, tone: "" },
];
});
const detailScreenshots = computed(() => {
if (!listing.value) return [];
const labels = ["纯币截图", "游戏ID截图", "总资产截图", "腾讯安全中心截图", "皮肤截图"];
return (listing.value.screenshot_urls || []).map((url, index) => ({
label: labels[index] || `账号截图${index + 1}`,
url,
}));
});
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<string, string> = {
melee: "近战皮肤",
operator: "干员皮肤",
operatorGold: "干员金皮",
operatorRed: "干员红皮",
weapon: "武器皮肤",
};
return Object.entries(groups as Record<string, unknown>)
.map(([key, value]) => ({
key,
title: titles[key] || key,
options: Array.isArray(value)
? value.filter((skin): skin is string => typeof skin === "string")
: [],
}))
.filter((group) => group.options.length);
});
const accountRows = computed(() => {
if (!listing.value) return [];
const regions = assetRegions(listing.value);
return [
{ label: "所属区服", value: getServerRegion(listing.value) || "--" },
{ label: "上号方式", value: getLoginMethod(listing.value) || "--" },
{ label: "游戏段位", value: listing.value.rank_level || "--" },
{ label: "M单价", value: formatRatio(listing.value) },
{ label: "方便上号", value: getOnlineTimeText(listing.value) || "--" },
{ label: "预计可租", value: formatEstimatedRentalDuration(listing.value) },
{ label: "常用登录地", value: regions.length ? regions.join("、") : "--" },
{ label: "封禁记录", value: readAssetString(listing.value, "ban_record") || "无" },
];
});
async function handleCreateOrder() {
if (!listing.value) return;
if (!session.token) {
await router.push({ path: "/login", query: { redirect: route.fullPath } });
return;
}
ordering.value = true;
try {
const order = await createOrder(listing.value.id);
@@ -45,7 +141,7 @@ function readError(error: unknown, fallback: string) {
}
function listingPrice(item: Listing) {
return `${Math.round(Number(item.price || 0))}`;
return `${Math.round(getListingDisplayPrice(item))}`;
}
</script>
@@ -59,35 +155,99 @@ function listingPrice(item: Listing) {
<div v-if="listing" class="pc-detail-layout">
<section class="pc-detail-main">
<div class="detail-cover">
<div class="detail-hero-card">
<img
v-if="listing.screenshot_urls?.[0]"
:src="listing.screenshot_urls[0]"
:alt="listing.title"
v-if="coverURL"
:src="coverURL"
:alt="getListingTitle(listing)"
/>
<span v-else>HFB ACCOUNT</span>
</div>
<div class="page-header detail-title">
<p class="eyebrow">
{{ listing.server_region }} / {{ listing.login_platform }}
</p>
<h1>{{ listing.title }}</h1>
<p>{{ listing.description || "号主暂未填写详细说明。" }}</p>
<div class="detail-hero-overlay">
<div class="detail-tags">
<span>{{ getServerRegion(listing) }}</span>
<span v-if="getLoginMethod(listing)">{{ getLoginMethod(listing) }}</span>
<span v-if="listing.rank_level">{{ listing.rank_level }}</span>
<span v-if="getDailyLoss(listing)">损耗 {{ getDailyLoss(listing) }}</span>
</div>
<h1>{{ getListingTitle(listing) }}</h1>
<p>{{ getListingSubtitle(listing) }}</p>
</div>
</div>
<div class="detail-grid">
<div class="metric-card">
<span>哈夫币</span>
<strong>{{ listing.haf_coin_amount }}</strong>
</div>
<div class="metric-card">
<span>价格</span>
<strong>¥{{ listingPrice(listing) }}</strong>
</div>
<div class="metric-card">
<span>押金</span>
<strong>¥{{ listing.deposit_amount }}</strong>
<div class="detail-body">
<div class="detail-summary-row">
<div v-for="metric in detailMetrics" :key="metric.label" class="detail-metric" :class="`is-${metric.tone}`">
<span>{{ metric.label }}</span>
<strong>{{ metric.value }}</strong>
</div>
</div>
<section class="detail-section">
<div class="detail-section-head">
<h2>账号资料</h2>
<span>{{ listing.game_name || "三角洲行动" }}</span>
</div>
<div class="detail-chip-row">
<span v-for="chip in getListingChips(listing)" :key="chip.label">
{{ chip.label }}{{ chip.value }}
</span>
</div>
<dl class="detail-info-grid">
<div v-for="row in accountRows" :key="row.label">
<dt>{{ row.label }}</dt>
<dd>{{ row.value }}</dd>
</div>
</dl>
</section>
<section v-if="getListingResources(listing).length" class="detail-section">
<div class="detail-section-head">
<h2>额外消耗品</h2>
<span>{{ getListingResources(listing).length }} </span>
</div>
<div class="detail-resource-grid">
<div v-for="resource in getListingResources(listing)" :key="resource.key" class="detail-resource-card">
<span>{{ resource.label }}</span>
<strong>{{ resource.quantity }}</strong>
<em>{{ resource.mode }}</em>
</div>
</div>
</section>
<section v-if="detailSkinGroups.length" class="detail-section">
<div class="detail-section-head">
<h2>皮肤清单</h2>
<span>按类型展示</span>
</div>
<div class="skin-groups">
<div v-for="group in detailSkinGroups" :key="group.key" class="skin-group">
<h3>{{ group.title }}</h3>
<div class="detail-chip-row">
<span v-for="skin in group.options" :key="skin">{{ skin }}</span>
</div>
</div>
</div>
</section>
<section class="detail-section">
<div class="detail-section-head">
<h2>号主备注</h2>
</div>
<p class="detail-description">{{ listing.description || "号主暂未填写详细说明。" }}</p>
</section>
<section v-if="detailScreenshots.length" class="detail-section">
<div class="detail-section-head">
<h2>账号截图</h2>
<span>{{ detailScreenshots.length }} </span>
</div>
<div class="detail-screenshot-grid">
<figure v-for="shot in detailScreenshots" :key="shot.url" class="detail-screenshot">
<img :src="shot.url" :alt="shot.label" />
<figcaption>{{ shot.label }}</figcaption>
</figure>
</div>
</section>
</div>
</section>
@@ -100,6 +260,20 @@ function listingPrice(item: Listing) {
<strong>¥{{ listingPrice(listing) }}</strong>
<em>押金 ¥{{ listing.deposit_amount }}</em>
</div>
<dl class="order-check-list">
<div>
<dt>账号区服</dt>
<dd>{{ getServerRegion(listing) || "--" }}</dd>
</div>
<div>
<dt>上号方式</dt>
<dd>{{ getLoginMethod(listing) || "--" }}</dd>
</div>
<div>
<dt>预计可租</dt>
<dd>{{ formatEstimatedRentalDuration(listing) }}</dd>
</div>
</dl>
<el-button
type="warning"
size="large"
@@ -115,3 +289,360 @@ function listingPrice(item: Listing) {
</div>
</section>
</template>
<style scoped>
.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-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(4, 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 {
grid-column: 1 / -1;
color: #ff6a00;
font-size: 12px;
font-style: normal;
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 img {
width: 100%;
aspect-ratio: 16 / 10;
object-fit: cover;
border-radius: 12px;
border: 1px solid #edf0f4;
}
.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;
}
.order-total-box {
margin-bottom: 14px;
}
.order-check-list {
display: grid;
gap: 10px;
margin: 0 0 18px;
}
.order-check-list div {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
min-height: 40px;
padding: 0 12px;
border-radius: 10px;
background: #f8fafc;
}
.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;
}
@media (max-width: 1180px) {
.pc-detail-layout,
.detail-screenshot-grid {
grid-template-columns: 1fr;
}
.detail-summary-row,
.detail-info-grid,
.detail-resource-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.detail-hero-card,
.detail-hero-card img,
.detail-hero-card > span {
height: 300px;
}
.detail-hero-overlay {
padding: 24px;
}
.detail-hero-overlay h1 {
font-size: 26px;
}
}
@media (max-width: 720px) {
.detail-summary-row,
.detail-info-grid,
.detail-resource-grid {
grid-template-columns: 1fr;
}
}
</style>
@@ -34,13 +34,14 @@
.publish-layout {
display: grid;
grid-template-columns: minmax(760px, 920px) 300px;
grid-template-columns: minmax(760px, 1fr) 300px;
align-items: start;
gap: 18px;
max-width: 1238px;
gap: 24px;
max-width: 1580px;
margin: 0 auto;
overflow: visible;
@media (max-width: 1180px) {
@media (max-width: 1120px) {
grid-template-columns: 1fr;
max-width: 920px;
}
@@ -734,19 +735,24 @@
}
.summary-column {
--summary-sticky-top: clamp(148px, 22vh, 240px);
min-width: 0;
position: sticky;
top: 76px;
top: var(--summary-sticky-top);
align-self: start;
height: fit-content;
max-height: calc(100vh - var(--summary-sticky-top) - 24px);
@media (max-width: 1180px) {
@media (max-width: 1120px) {
display: none;
}
}
.summary-panel {
padding: 16px;
max-height: inherit;
overflow-y: auto;
overscroll-behavior: contain;
}
.summary-main {