优化详情页和发布界面等
This commit is contained in:
Vendored
+2
@@ -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']
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user