fix(mobile): 重构商品详情资产展示与验图
This commit is contained in:
@@ -2,9 +2,12 @@ import { describe, expect, it } from 'vitest'
|
||||
|
||||
import {
|
||||
faceOwnerText,
|
||||
listingAssetRows,
|
||||
listingGalleryItems,
|
||||
listingPriceDetail,
|
||||
listingRatioText,
|
||||
listingResources,
|
||||
listingSkinGroups,
|
||||
resourceBadgeText,
|
||||
resourceChargeText,
|
||||
} from '@/features/listings/labels'
|
||||
@@ -168,3 +171,96 @@ describe('faceOwnerText', () => {
|
||||
expect(faceOwnerText({ face_owner: '需配合人脸' })).toBe('需配合人脸')
|
||||
})
|
||||
})
|
||||
|
||||
describe('listingAssetRows', () => {
|
||||
it('把内部资产字段转换成买家可读文案,并排除截图和计价数据', () => {
|
||||
const rows = listingAssetRows({
|
||||
fire_level: 60,
|
||||
stamina_level: '7级',
|
||||
load_level: '4级',
|
||||
secret_kd: '2.5',
|
||||
daily_loss_m: 30,
|
||||
common_regions: ['内蒙古', '北京'],
|
||||
all_hero: '是',
|
||||
online_time: { start: '09:00', end: '23:00' },
|
||||
face_owner: '否',
|
||||
screenshot_groups: { coin: ['/api/listings/2/screenshots/0'] },
|
||||
publish_ratio: 34,
|
||||
resources: [{ label: '6级弹', quantity: 10 }],
|
||||
})
|
||||
const values = Object.fromEntries(rows.map(row => [row.label, row.value]))
|
||||
|
||||
expect(values).toMatchObject({
|
||||
烽火等级: '60 级',
|
||||
体力等级: '7级',
|
||||
负重等级: '4级',
|
||||
'绝密 KD': '2.5',
|
||||
每日损耗: '30M / 天',
|
||||
常用地区: '内蒙古、北京',
|
||||
全英雄: '是',
|
||||
在线时段: '09:00 - 23:00',
|
||||
人脸情况: '非本人',
|
||||
})
|
||||
expect(rows.some(row => row.label === 'screenshot_groups')).toBe(false)
|
||||
expect(rows.some(row => row.label === 'publish_ratio')).toBe(false)
|
||||
expect(rows.some(row => row.label === 'resources')).toBe(false)
|
||||
})
|
||||
|
||||
it('缺少资产摘要时返回空数组', () => {
|
||||
expect(listingAssetRows(null)).toEqual([])
|
||||
expect(listingAssetRows({})).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('listingGalleryItems', () => {
|
||||
it('优先按业务分组生成有标签的验号截图', () => {
|
||||
expect(
|
||||
listingGalleryItems({
|
||||
cover_url: '/api/listings/2/cover',
|
||||
screenshot_urls: ['/api/listings/2/screenshots/0'],
|
||||
asset_summary: {
|
||||
screenshot_groups: {
|
||||
coin: ['/api/listings/2/screenshots/0', '/api/listings/2/screenshots/1'],
|
||||
gameId: ['/api/listings/2/screenshots/2'],
|
||||
},
|
||||
},
|
||||
})
|
||||
).toEqual([
|
||||
{ url: '/api/listings/2/screenshots/0', label: '纯币截图 1' },
|
||||
{ url: '/api/listings/2/screenshots/1', label: '纯币截图 2' },
|
||||
{ url: '/api/listings/2/screenshots/2', label: '游戏 ID 截图' },
|
||||
])
|
||||
})
|
||||
|
||||
it('没有分组时使用截图数组,最后才回退到商品封面', () => {
|
||||
expect(
|
||||
listingGalleryItems({
|
||||
cover_url: '/api/listings/2/cover',
|
||||
screenshot_urls: ['/api/listings/2/screenshots/0'],
|
||||
})
|
||||
).toEqual([{ url: '/api/listings/2/screenshots/0', label: '纯币截图' }])
|
||||
expect(
|
||||
listingGalleryItems({
|
||||
cover_url: '/api/listings/2/cover',
|
||||
screenshot_urls: [],
|
||||
})
|
||||
).toEqual([{ url: '/api/listings/2/cover', label: '商品封面' }])
|
||||
})
|
||||
})
|
||||
|
||||
describe('listingSkinGroups', () => {
|
||||
it('过滤空分组并保留皮肤品级', () => {
|
||||
expect(
|
||||
listingSkinGroups({
|
||||
skin_groups: {
|
||||
melee: ['影锋', '北极星'],
|
||||
operatorRed: ['天际线'],
|
||||
weapon: [],
|
||||
},
|
||||
})
|
||||
).toEqual([
|
||||
{ key: 'melee', label: '近战皮肤', tone: 'melee', names: ['影锋', '北极星'] },
|
||||
{ key: 'operatorRed', label: '干员红皮', tone: 'operatorRed', names: ['天际线'] },
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -60,6 +60,161 @@ export function assetFieldLabel(key: string) {
|
||||
return assetFieldLabels[key] ?? key
|
||||
}
|
||||
|
||||
export type ListingAssetRow = {
|
||||
label: string
|
||||
value: string
|
||||
}
|
||||
|
||||
export type ListingGalleryItem = {
|
||||
url: string
|
||||
label: string
|
||||
}
|
||||
|
||||
export type ListingSkinTone = 'melee' | 'operatorRed' | 'operatorGold' | 'weapon' | 'other'
|
||||
|
||||
export type ListingSkinGroup = {
|
||||
key: string
|
||||
label: string
|
||||
tone: ListingSkinTone
|
||||
names: string[]
|
||||
}
|
||||
|
||||
const skinGroupMeta: Record<string, { label: string; tone: ListingSkinTone }> = {
|
||||
melee: { label: '近战皮肤', tone: 'melee' },
|
||||
operatorRed: { label: '干员红皮', tone: 'operatorRed' },
|
||||
operatorGold: { label: '干员金皮', tone: 'operatorGold' },
|
||||
operator: { label: '干员皮肤', tone: 'other' },
|
||||
weapon: { label: '武器皮肤', tone: 'weapon' },
|
||||
other: { label: '其他皮肤', tone: 'other' },
|
||||
imported: { label: '其他皮肤', tone: 'other' },
|
||||
}
|
||||
|
||||
const screenshotGroupMeta = [
|
||||
{ key: 'coin', label: '纯币截图' },
|
||||
{ key: 'tencentSecurity', label: '腾讯安全截图' },
|
||||
{ key: 'skin', label: '皮肤截图' },
|
||||
{ key: 'gameId', label: '游戏 ID 截图' },
|
||||
{ key: 'totalAsset', label: '总资产截图' },
|
||||
]
|
||||
|
||||
function asRecord(value: unknown): Record<string, unknown> {
|
||||
return value && typeof value === 'object' && !Array.isArray(value)
|
||||
? (value as Record<string, unknown>)
|
||||
: {}
|
||||
}
|
||||
|
||||
function plainText(value: unknown) {
|
||||
if (typeof value === 'string') return value.trim()
|
||||
if (typeof value === 'number' && Number.isFinite(value)) return String(value)
|
||||
if (typeof value === 'boolean') return value ? '是' : '否'
|
||||
return ''
|
||||
}
|
||||
|
||||
function yesNoText(value: unknown) {
|
||||
if (value === true) return '是'
|
||||
if (value === false) return '否'
|
||||
return plainText(value)
|
||||
}
|
||||
|
||||
function stringList(value: unknown) {
|
||||
if (!Array.isArray(value)) return []
|
||||
return value
|
||||
.filter((item): item is string => typeof item === 'string')
|
||||
.map(item => item.trim())
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
/**
|
||||
* 买家侧资产档案只展示用户能理解的字段,避免把内部截图地址和计价字段直接暴露出来。
|
||||
*/
|
||||
export function listingAssetRows(summary?: Record<string, unknown> | null): ListingAssetRow[] {
|
||||
if (!summary) return []
|
||||
const fireLevel = Number(summary.fire_level ?? 0)
|
||||
const secretKd = Number(summary.secret_kd ?? 0)
|
||||
const dailyLoss = Number(summary.daily_loss_m ?? 0)
|
||||
const regions = stringList(summary.common_regions)
|
||||
const rows: ListingAssetRow[] = [
|
||||
{
|
||||
label: '烽火等级',
|
||||
value:
|
||||
Number.isFinite(fireLevel) && fireLevel > 0
|
||||
? `${fireLevel} 级`
|
||||
: plainText(summary.fire_level),
|
||||
},
|
||||
{ label: '体力等级', value: plainText(summary.stamina_level) },
|
||||
{ label: '负重等级', value: plainText(summary.load_level) },
|
||||
{ label: '赛季保险', value: plainText(summary.season_insurance) },
|
||||
{
|
||||
label: '绝密 KD',
|
||||
value: Number.isFinite(secretKd) && secretKd > 0 ? String(secretKd) : '',
|
||||
},
|
||||
{
|
||||
label: '每日损耗',
|
||||
value: Number.isFinite(dailyLoss) && dailyLoss > 0 ? `${dailyLoss}M / 天` : '',
|
||||
},
|
||||
{ label: '全英雄', value: yesNoText(summary.all_hero ?? summary.all_heroes) },
|
||||
{ label: '解锁德穆兰', value: yesNoText(summary.unlock_demulan) },
|
||||
{ label: '在线时段', value: onlineTimeText(summary) },
|
||||
{ label: '常用地区', value: regions.join('、') },
|
||||
{ label: '凌晨响应', value: yesNoText(summary.early_morning_response) },
|
||||
{ label: '人脸情况', value: faceOwnerText(summary) },
|
||||
{ label: '封禁记录', value: banRecordText(summary) },
|
||||
{ label: '可改游戏名', value: gameNameChangeText(summary) },
|
||||
{ label: '下赛季通行证', value: yesNoText(summary.next_season_pass) },
|
||||
{ label: '号主备注', value: plainText(summary.remark) },
|
||||
]
|
||||
return rows.filter(row => row.value)
|
||||
}
|
||||
|
||||
export function listingSkinGroups(summary?: Record<string, unknown> | null): ListingSkinGroup[] {
|
||||
const groups = asRecord(summary?.skin_groups)
|
||||
return Object.entries(groups)
|
||||
.map(([key, value]) => {
|
||||
const meta = skinGroupMeta[key] ?? { label: key, tone: 'other' as const }
|
||||
return {
|
||||
key,
|
||||
label: meta.label,
|
||||
tone: meta.tone,
|
||||
names: stringList(value),
|
||||
}
|
||||
})
|
||||
.filter(group => group.names.length > 0)
|
||||
}
|
||||
|
||||
export function listingGalleryItems(item: {
|
||||
cover_url?: string
|
||||
screenshot_urls?: string[]
|
||||
asset_summary?: Record<string, unknown>
|
||||
}): ListingGalleryItem[] {
|
||||
const seen = new Set<string>()
|
||||
const rows: ListingGalleryItem[] = []
|
||||
const add = (url: unknown, label: string) => {
|
||||
if (typeof url !== 'string') return
|
||||
const value = url.trim()
|
||||
if (!value || seen.has(value)) return
|
||||
seen.add(value)
|
||||
rows.push({ url: value, label })
|
||||
}
|
||||
|
||||
const groups = asRecord(item.asset_summary?.screenshot_groups)
|
||||
for (const slot of screenshotGroupMeta) {
|
||||
const urls = stringList(groups[slot.key])
|
||||
urls.forEach((url, index) => {
|
||||
add(url, urls.length > 1 ? `${slot.label} ${index + 1}` : slot.label)
|
||||
})
|
||||
}
|
||||
|
||||
if (rows.length === 0) {
|
||||
const fallbackLabels = ['纯币截图', '游戏 ID 截图', '总资产截图', '腾讯安全截图', '皮肤截图']
|
||||
;(item.screenshot_urls ?? []).forEach((url, index) => {
|
||||
add(url, fallbackLabels[index] ?? `账号截图 ${index + 1}`)
|
||||
})
|
||||
}
|
||||
|
||||
if (rows.length === 0 && item.cover_url) add(item.cover_url, '商品封面')
|
||||
return rows
|
||||
}
|
||||
|
||||
function formatOnlineTime(value: unknown) {
|
||||
if (!value || typeof value !== 'object') return ''
|
||||
const item = value as { start?: unknown; end?: unknown }
|
||||
|
||||
@@ -1,19 +1,37 @@
|
||||
import { useState } from 'react'
|
||||
import { Button, Checkbox, Dialog, ErrorBlock, Popup, Skeleton, Tag, Toast } from 'antd-mobile'
|
||||
import { LeftOutline, PhoneFill } from 'antd-mobile-icons'
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
Dialog,
|
||||
ErrorBlock,
|
||||
ImageViewer,
|
||||
Popup,
|
||||
Skeleton,
|
||||
Tag,
|
||||
Toast,
|
||||
} from 'antd-mobile'
|
||||
import {
|
||||
CheckShieldOutline,
|
||||
LeftOutline,
|
||||
PhoneFill,
|
||||
SmileOutline,
|
||||
UserContactOutline,
|
||||
} from 'antd-mobile-icons'
|
||||
import { useNavigate, useParams } from 'react-router-dom'
|
||||
|
||||
import { BrandLogo } from '@/components/BrandLogo'
|
||||
import { useListingDetail } from '@/features/listings/api'
|
||||
import {
|
||||
banRecordText,
|
||||
faceOwnerText,
|
||||
gameNameChangeText,
|
||||
listingAssetRows,
|
||||
listingGalleryItems,
|
||||
listingPriceDetail,
|
||||
listingRatioText,
|
||||
listingRentalDays,
|
||||
listingResources,
|
||||
listingSkinGroups,
|
||||
resourceBadgeText,
|
||||
type ListingGalleryItem,
|
||||
} from '@/features/listings/labels'
|
||||
import { ensureSupportConversation } from '@/features/messages/api'
|
||||
import { createOrder, useOrderAgreements, usePostRentalNotice } from '@/features/orders/api'
|
||||
@@ -24,18 +42,111 @@ function money(cents: number) {
|
||||
return `¥${(cents / 100).toFixed(2)}`
|
||||
}
|
||||
|
||||
function assetValue(value: unknown) {
|
||||
if (value && typeof value === 'object') {
|
||||
const record = value as Record<string, unknown>
|
||||
if (typeof record.start === 'string' && typeof record.end === 'string') {
|
||||
return `${record.start} – ${record.end}`
|
||||
}
|
||||
return Object.entries(record)
|
||||
.map(([key, item]) => `${key}:${String(item)}`)
|
||||
.join(',')
|
||||
function summaryText(value: unknown) {
|
||||
if (typeof value === 'string') return value.trim()
|
||||
if (typeof value === 'number' && Number.isFinite(value)) return String(value)
|
||||
if (typeof value === 'boolean') return value ? '是' : '否'
|
||||
return ''
|
||||
}
|
||||
|
||||
function MobileListingGallery({ items }: { items: ListingGalleryItem[] }) {
|
||||
const [activeIndex, setActiveIndex] = useState(0)
|
||||
const [failedURLs, setFailedURLs] = useState<string[]>([])
|
||||
const [viewerOpen, setViewerOpen] = useState(false)
|
||||
const [viewerIndex, setViewerIndex] = useState(0)
|
||||
const safeIndex = Math.min(activeIndex, Math.max(items.length - 1, 0))
|
||||
const active = items[safeIndex]
|
||||
const availableItems = items.filter(item => !failedURLs.includes(item.url))
|
||||
|
||||
const markFailed = (url: string) => {
|
||||
setFailedURLs(current => (current.includes(url) ? current : [...current, url]))
|
||||
}
|
||||
if (Array.isArray(value)) return value.map(String).join('、')
|
||||
return value == null || value === '' ? '未填写' : String(value)
|
||||
|
||||
const openViewer = () => {
|
||||
if (!active || failedURLs.includes(active.url) || availableItems.length === 0) return
|
||||
setViewerIndex(
|
||||
Math.max(
|
||||
availableItems.findIndex(item => item.url === active.url),
|
||||
0
|
||||
)
|
||||
)
|
||||
setViewerOpen(true)
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="mobile-detail__panel mobile-detail__gallery-card">
|
||||
<div className="mobile-detail__section-head">
|
||||
<div>
|
||||
<span>验号图片展台</span>
|
||||
<h2>真实游戏截图</h2>
|
||||
</div>
|
||||
<small>共 {items.length} 张</small>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="mobile-detail__stage"
|
||||
onClick={openViewer}
|
||||
aria-label={active ? `查看${active.label}` : '暂无验号截图'}
|
||||
>
|
||||
{active && !failedURLs.includes(active.url) ? (
|
||||
<img
|
||||
src={active.url}
|
||||
alt={active.label}
|
||||
loading="eager"
|
||||
onError={() => markFailed(active.url)}
|
||||
/>
|
||||
) : (
|
||||
<span className="mobile-detail__image-fallback">
|
||||
<BrandLogo height={40} variant="seal" />
|
||||
<em>{active ? '截图暂不可访问' : '暂无验号截图'}</em>
|
||||
</span>
|
||||
)}
|
||||
{active ? <span className="mobile-detail__stage-label">{active.label}</span> : null}
|
||||
{active && !failedURLs.includes(active.url) && items.length > 0 ? (
|
||||
<span className="mobile-detail__stage-hint">点击放大</span>
|
||||
) : null}
|
||||
</button>
|
||||
|
||||
{items.length > 1 ? (
|
||||
<div className="mobile-detail__thumb-strip">
|
||||
{items.map((item, index) => (
|
||||
<button
|
||||
type="button"
|
||||
key={`${item.url}-${index}`}
|
||||
className={`mobile-detail__thumb${index === safeIndex ? ' is-active' : ''}`}
|
||||
onClick={() => setActiveIndex(index)}
|
||||
>
|
||||
{failedURLs.includes(item.url) ? (
|
||||
<span className="mobile-detail__thumb-fallback">
|
||||
<BrandLogo height={20} variant="seal" />
|
||||
</span>
|
||||
) : (
|
||||
<img
|
||||
src={item.url}
|
||||
alt={item.label}
|
||||
loading="lazy"
|
||||
onError={() => markFailed(item.url)}
|
||||
/>
|
||||
)}
|
||||
<em>{item.label}</em>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<ImageViewer.Multi
|
||||
key={`${viewerOpen}-${viewerIndex}`}
|
||||
visible={viewerOpen}
|
||||
images={availableItems.map(item => item.url)}
|
||||
defaultIndex={viewerIndex}
|
||||
onClose={() => setViewerOpen(false)}
|
||||
renderFooter={(_image, index) => (
|
||||
<span className="mobile-detail__viewer-label">{availableItems[index]?.label}</span>
|
||||
)}
|
||||
/>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export function MobileListingDetailPage() {
|
||||
@@ -51,6 +162,16 @@ export function MobileListingDetailPage() {
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
const [openingSupport, setOpeningSupport] = useState(false)
|
||||
|
||||
const copyListingNo = async () => {
|
||||
if (!data) return
|
||||
try {
|
||||
await navigator.clipboard.writeText(data.listing_no)
|
||||
Toast.show({ content: '商品编号已复制' })
|
||||
} catch {
|
||||
Toast.show({ content: '复制失败,请手动记录编号' })
|
||||
}
|
||||
}
|
||||
|
||||
const openSupport = async () => {
|
||||
if (!token) {
|
||||
navigate(`/m/login?redirect=/m/listings/${id}`)
|
||||
@@ -148,55 +269,84 @@ export function MobileListingDetailPage() {
|
||||
)
|
||||
}
|
||||
|
||||
const onlineTime =
|
||||
data.asset_summary && typeof data.asset_summary === 'object'
|
||||
? (data.asset_summary.online_time as unknown)
|
||||
: undefined
|
||||
const assetEntries = Object.entries(data.asset_summary || {}).filter(
|
||||
([key]) =>
|
||||
key !== 'online_time' &&
|
||||
key !== 'face_owner' &&
|
||||
key !== 'ban_record' &&
|
||||
key !== 'can_change_game_name' &&
|
||||
key !== 'modifiable_name' &&
|
||||
key !== 'pure_coin_price_cent' &&
|
||||
key !== 'extra_item_price_cent' &&
|
||||
key !== 'publish_ratio' &&
|
||||
key !== 'resources'
|
||||
)
|
||||
const summary = data.asset_summary ?? {}
|
||||
const rentalDays = listingRentalDays(data)
|
||||
const faceOwner = faceOwnerText(data.asset_summary)
|
||||
const banRecord = banRecordText(data.asset_summary)
|
||||
const gameNameChange = gameNameChangeText(data.asset_summary)
|
||||
const faceOwner = faceOwnerText(summary)
|
||||
const ratio = listingRatioText(data)
|
||||
const resources = listingResources(data.asset_summary)
|
||||
const galleryItems = listingGalleryItems(data)
|
||||
const assetRows = listingAssetRows(summary)
|
||||
const skinGroups = listingSkinGroups(summary)
|
||||
const skinCount = skinGroups.reduce((total, group) => total + group.names.length, 0)
|
||||
const priceDetail = listingPriceDetail(data, { compact: true })
|
||||
const insuranceSlot = summaryText(summary.season_insurance)
|
||||
const dailyLoss = Number(summary.daily_loss_m ?? 0)
|
||||
const fireLevel = Number(summary.fire_level ?? 0)
|
||||
const coreTiles = [
|
||||
{ label: '哈夫币', value: formatCoinM(data.haf_coin_amount), tone: 'coin' },
|
||||
{ label: '安全箱', value: insuranceSlot || '—', tone: insuranceSlot === '3*3' ? 'coin' : '' },
|
||||
{ label: '折算比', value: ratio ? `1元 ≈ ${ratio}` : '—', tone: '' },
|
||||
{ label: '预计可租', value: rentalDays ? `${rentalDays} 天` : '—', tone: 'success' },
|
||||
{
|
||||
label: '每日损耗',
|
||||
value: Number.isFinite(dailyLoss) && dailyLoss > 0 ? `${dailyLoss}M / 天` : '—',
|
||||
tone: 'warm',
|
||||
},
|
||||
{ label: '人脸情况', value: faceOwner || '—', tone: faceOwner === '非本人' ? 'danger' : '' },
|
||||
]
|
||||
const isOwner = user?.id === data.owner_id
|
||||
|
||||
return (
|
||||
<div className="mobile-detail">
|
||||
<button type="button" className="mobile-detail__back" onClick={() => navigate(-1)}>
|
||||
<LeftOutline /> 返回
|
||||
</button>
|
||||
<header className="mobile-detail__nav">
|
||||
<button type="button" className="mobile-detail__nav-back" onClick={() => navigate(-1)}>
|
||||
<LeftOutline />
|
||||
<span>返回</span>
|
||||
</button>
|
||||
<strong>账号详情</strong>
|
||||
<span className="mobile-detail__nav-spacer" />
|
||||
</header>
|
||||
|
||||
<div className="mobile-detail__cover">
|
||||
{data.cover_url ? (
|
||||
<img src={data.cover_url} alt={data.title} />
|
||||
) : (
|
||||
<BrandLogo height={52} variant="seal" />
|
||||
)}
|
||||
<div className="mobile-detail__fraud-tip">
|
||||
<CheckShieldOutline />
|
||||
<span>平台官方担保交易,请按平台交接流程验号,勿脱离平台私下转账。</span>
|
||||
</div>
|
||||
|
||||
<section className="mobile-detail__head">
|
||||
<div className="mobile-detail__no">{data.listing_no}</div>
|
||||
<h1>{data.title}</h1>
|
||||
<div className="mobile-detail__tags">
|
||||
<section className="mobile-detail__hero-card">
|
||||
<div className="mobile-detail__hero-meta">
|
||||
<button
|
||||
type="button"
|
||||
className="mobile-detail__code"
|
||||
onClick={() => void copyListingNo()}
|
||||
>
|
||||
<span>{data.listing_no}</span>
|
||||
<em>复制</em>
|
||||
</button>
|
||||
<Tag color={data.in_transaction ? 'default' : 'success'} fill="outline">
|
||||
{data.in_transaction ? '租赁中' : '可租'}
|
||||
</Tag>
|
||||
</div>
|
||||
<h1>{data.title}</h1>
|
||||
<div className="mobile-detail__tags">
|
||||
<Tag fill="outline">{data.server_region || '区服待定'}</Tag>
|
||||
<Tag fill="outline">{data.login_platform || '登录方式待定'}</Tag>
|
||||
<Tag fill="outline">{data.rank_level || '段位待定'}</Tag>
|
||||
{fireLevel > 0 ? <Tag fill="outline">烽火 {fireLevel} 级</Tag> : null}
|
||||
{data.is_premium ? <Tag color="warning">极品专区</Tag> : null}
|
||||
{data.owner_is_quality_owner ? <Tag color="success">优质号主</Tag> : null}
|
||||
{data.compensation_plan === 'insurance' ? <Tag color="primary">无忧保险</Tag> : null}
|
||||
</div>
|
||||
|
||||
<div className="mobile-detail__core-grid">
|
||||
{coreTiles.map(item => (
|
||||
<div
|
||||
key={item.label}
|
||||
className={`mobile-detail__core-tile${item.tone ? ` is-${item.tone}` : ''}`}
|
||||
>
|
||||
<span>{item.label}</span>
|
||||
<strong>{item.value}</strong>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -218,83 +368,135 @@ export function MobileListingDetailPage() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mobile-detail__section">
|
||||
<h2>账号说明</h2>
|
||||
<p>{data.description || '号主暂未填写补充说明。'}</p>
|
||||
</section>
|
||||
<MobileListingGallery key={data.id} items={galleryItems} />
|
||||
|
||||
<section className="mobile-detail__section">
|
||||
<h2>资产摘要</h2>
|
||||
<dl className="mobile-detail__assets">
|
||||
{ratio ? (
|
||||
<div>
|
||||
<dt>折算比</dt>
|
||||
<dd>1 元 ≈ {ratio}</dd>
|
||||
</div>
|
||||
) : null}
|
||||
{assetEntries.length ? (
|
||||
assetEntries.map(([key, value]) => (
|
||||
<div key={key}>
|
||||
<dt>{key}</dt>
|
||||
<dd>{assetValue(value)}</dd>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div>
|
||||
<dt>资产</dt>
|
||||
<dd>未填写</dd>
|
||||
</div>
|
||||
)}
|
||||
{faceOwner ? (
|
||||
<div>
|
||||
<dt>人脸情况</dt>
|
||||
<dd>{faceOwner}</dd>
|
||||
</div>
|
||||
) : null}
|
||||
{banRecord ? (
|
||||
<div>
|
||||
<dt>封禁记录</dt>
|
||||
<dd>{banRecord}</dd>
|
||||
</div>
|
||||
) : null}
|
||||
{gameNameChange ? (
|
||||
<div>
|
||||
<dt>可改游戏名</dt>
|
||||
<dd>{gameNameChange}</dd>
|
||||
</div>
|
||||
) : null}
|
||||
{rentalDays ? (
|
||||
<div>
|
||||
<dt>预计租期</dt>
|
||||
<dd>{rentalDays} 天</dd>
|
||||
</div>
|
||||
) : null}
|
||||
<section className="mobile-detail__panel">
|
||||
<div className="mobile-detail__section-head">
|
||||
<div>
|
||||
<dt>在线时间</dt>
|
||||
<dd>{assetValue(onlineTime)}</dd>
|
||||
<span>账号核心档案</span>
|
||||
<h2>资产与游戏信息</h2>
|
||||
</div>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
{resources.length ? (
|
||||
<section className="mobile-detail__section">
|
||||
<h2>仓库物资</h2>
|
||||
<dl className="mobile-detail__assets">
|
||||
{resources.map(row => (
|
||||
<div key={row.label}>
|
||||
<dt>{row.label}</dt>
|
||||
<dd>
|
||||
{row.quantity} 个
|
||||
<span className={`mobile-detail__resource-mode is-${row.mode}`}>
|
||||
{resourceBadgeText(row)}
|
||||
</span>
|
||||
</dd>
|
||||
<small>{assetRows.length} 项信息</small>
|
||||
</div>
|
||||
{assetRows.length ? (
|
||||
<div className="mobile-detail__spec-grid">
|
||||
{assetRows.map(row => (
|
||||
<div className="mobile-detail__spec-cell" key={row.label}>
|
||||
<span>{row.label}</span>
|
||||
<strong>{row.value}</strong>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
</div>
|
||||
) : (
|
||||
<p className="mobile-detail__empty">号主暂未填写更多资产信息。</p>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section className="mobile-detail__panel">
|
||||
<div className="mobile-detail__section-head">
|
||||
<div>
|
||||
<span>号主留言与说明</span>
|
||||
<h2>交接前请先阅读</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mobile-detail__note">
|
||||
{data.description || '号主暂未填写特殊交接要求,可正常直接下单租用。'}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{skinGroups.length ? (
|
||||
<section className="mobile-detail__panel">
|
||||
<div className="mobile-detail__section-head">
|
||||
<div>
|
||||
<span>精选皮肤展台</span>
|
||||
<h2>账号皮肤收藏</h2>
|
||||
</div>
|
||||
<small>共 {skinCount} 款</small>
|
||||
</div>
|
||||
<div className="mobile-detail__skin-groups">
|
||||
{skinGroups.map(group => (
|
||||
<div className="mobile-detail__skin-group" key={group.key}>
|
||||
<div>
|
||||
<strong>{group.label}</strong>
|
||||
<span>{group.names.length} 款</span>
|
||||
</div>
|
||||
<div className="mobile-detail__skin-pills">
|
||||
{group.names.map(name => (
|
||||
<span className={`is-${group.tone}`} key={name}>
|
||||
{name}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{resources.length ? (
|
||||
<section className="mobile-detail__panel">
|
||||
<div className="mobile-detail__section-head">
|
||||
<div>
|
||||
<span>仓库物资与消耗品</span>
|
||||
<h2>随号附带资源</h2>
|
||||
</div>
|
||||
<small>{resources.length} 种</small>
|
||||
</div>
|
||||
<div className="mobile-detail__resource-grid">
|
||||
{resources.map(row => (
|
||||
<div className="mobile-detail__resource-tile" key={row.label}>
|
||||
<div>
|
||||
<span>{row.label}</span>
|
||||
<strong>{row.quantity}</strong>
|
||||
</div>
|
||||
<em className={`mobile-detail__resource-mode is-${row.mode}`}>
|
||||
{resourceBadgeText(row)}
|
||||
</em>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
<section className="mobile-detail__trust">
|
||||
<div className="mobile-detail__section-head">
|
||||
<div>
|
||||
<span>平台官方交易保障</span>
|
||||
<h2>从这里开始,全程有保障</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mobile-detail__trust-grid">
|
||||
<div>
|
||||
<span className="is-green">
|
||||
<CheckShieldOutline />
|
||||
</span>
|
||||
<strong>资金托管</strong>
|
||||
<p>租金押金平台监管,确认收号后结算</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="is-blue">
|
||||
<SmileOutline />
|
||||
</span>
|
||||
<strong>找回包赔</strong>
|
||||
<p>按平台规则处理异常,减少租号风险</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="is-orange">
|
||||
<PhoneFill />
|
||||
</span>
|
||||
<strong>客服协助</strong>
|
||||
<p>交接遇到问题,平台客服随时介入</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="is-slate">
|
||||
<UserContactOutline />
|
||||
</span>
|
||||
<strong>双向实名</strong>
|
||||
<p>租客与号主完成实名后再进入交易</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mobile-detail__notice">
|
||||
<strong>先看清规则,再决定要不要租</strong>
|
||||
<p>押金、租金、额外消耗和退款规则会在下单页逐项列出;交接、归还、结账全程留痕。</p>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
.mobile-detail {
|
||||
padding: 14px 16px 108px;
|
||||
min-height: 100dvh;
|
||||
padding: 0 16px 112px;
|
||||
background: #f4f6f2;
|
||||
}
|
||||
|
||||
.mobile-detail--loading,
|
||||
@@ -15,6 +17,564 @@
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.mobile-detail__nav {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 40;
|
||||
display: grid;
|
||||
grid-template-columns: 72px minmax(0, 1fr) 72px;
|
||||
align-items: center;
|
||||
min-height: 50px;
|
||||
margin: 0 -16px 10px;
|
||||
padding: 0 12px;
|
||||
border-bottom: 1px solid rgba(217, 224, 217, 0.9);
|
||||
background: rgba(248, 250, 247, 0.94);
|
||||
backdrop-filter: blur(14px);
|
||||
}
|
||||
|
||||
.mobile-detail__nav > strong {
|
||||
overflow: hidden;
|
||||
color: var(--ink);
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mobile-detail__nav-back {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
padding: 7px 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--ink-secondary);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.mobile-detail__nav-spacer {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.mobile-detail__fraud-tip {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 7px;
|
||||
margin-bottom: 10px;
|
||||
padding: 9px 11px;
|
||||
border: 1px solid #eadfca;
|
||||
border-radius: 9px;
|
||||
background: #fffaf1;
|
||||
color: #8a6438;
|
||||
font-size: 11px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.mobile-detail__fraud-tip svg {
|
||||
flex: 0 0 auto;
|
||||
margin-top: 1px;
|
||||
color: #b7791f;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.mobile-detail__hero-card,
|
||||
.mobile-detail__panel {
|
||||
margin-top: 10px;
|
||||
padding: 16px;
|
||||
border: 1px solid #e2e7e0;
|
||||
border-radius: 12px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.mobile-detail__hero-card {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.mobile-detail__hero-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.mobile-detail__code {
|
||||
display: inline-flex;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: #7e8b84;
|
||||
font-family: 'Inter', monospace;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.mobile-detail__code span {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mobile-detail__code em {
|
||||
flex: 0 0 auto;
|
||||
padding: 2px 5px;
|
||||
border-radius: 4px;
|
||||
background: #edf3ee;
|
||||
color: #48675b;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.mobile-detail__hero-card h1 {
|
||||
margin: 10px 0 10px;
|
||||
color: var(--ink);
|
||||
font-family: 'Noto Serif SC', 'Songti SC', serif;
|
||||
font-size: 23px;
|
||||
font-weight: 600;
|
||||
line-height: 1.38;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.mobile-detail__hero-card .mobile-detail__tags {
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.mobile-detail__core-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 7px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.mobile-detail__core-tile {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
padding: 9px 8px;
|
||||
border: 1px solid #e7ebe5;
|
||||
border-radius: 8px;
|
||||
background: #f7f9f6;
|
||||
}
|
||||
|
||||
.mobile-detail__core-tile span {
|
||||
color: #849088;
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
.mobile-detail__core-tile strong {
|
||||
overflow: hidden;
|
||||
color: #26322d;
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mobile-detail__core-tile.is-coin {
|
||||
border-color: #ead8c5;
|
||||
background: #fff8f1;
|
||||
}
|
||||
|
||||
.mobile-detail__core-tile.is-coin strong,
|
||||
.mobile-detail__core-tile.is-warm strong {
|
||||
color: #a95632;
|
||||
}
|
||||
|
||||
.mobile-detail__core-tile.is-success strong {
|
||||
color: #25724e;
|
||||
}
|
||||
|
||||
.mobile-detail__core-tile.is-danger {
|
||||
border-color: #ecd4cf;
|
||||
background: #fff5f2;
|
||||
}
|
||||
|
||||
.mobile-detail__core-tile.is-danger strong {
|
||||
color: #a64332;
|
||||
}
|
||||
|
||||
.mobile-detail__panel {
|
||||
scroll-margin-top: 58px;
|
||||
}
|
||||
|
||||
.mobile-detail__section-head {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.mobile-detail__section-head > div {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.mobile-detail__section-head span,
|
||||
.mobile-detail__section-head small {
|
||||
color: #89958e;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.mobile-detail__section-head h2 {
|
||||
margin: 3px 0 0;
|
||||
color: var(--brand-ink);
|
||||
font-family: 'Noto Serif SC', 'Songti SC', serif;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.mobile-detail__section-head small {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.mobile-detail__gallery-card {
|
||||
padding-bottom: 14px;
|
||||
}
|
||||
|
||||
.mobile-detail__stage {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
aspect-ratio: 4 / 3;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
border: 0;
|
||||
border-radius: 9px;
|
||||
background: #17221e;
|
||||
}
|
||||
|
||||
.mobile-detail__stage > img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.mobile-detail__image-fallback {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
color: #aeb8b1;
|
||||
}
|
||||
|
||||
.mobile-detail__image-fallback em {
|
||||
font-size: 11px;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.mobile-detail__stage-label,
|
||||
.mobile-detail__stage-hint {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
padding: 3px 7px;
|
||||
border-radius: 5px;
|
||||
background: rgba(17, 28, 23, 0.76);
|
||||
color: #fff;
|
||||
font-size: 10px;
|
||||
backdrop-filter: blur(5px);
|
||||
}
|
||||
|
||||
.mobile-detail__stage-label {
|
||||
top: 8px;
|
||||
left: 8px;
|
||||
}
|
||||
|
||||
.mobile-detail__stage-hint {
|
||||
right: 8px;
|
||||
bottom: 8px;
|
||||
border-radius: 999px;
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
.mobile-detail__thumb-strip {
|
||||
display: flex;
|
||||
gap: 7px;
|
||||
margin-top: 9px;
|
||||
overflow-x: auto;
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
|
||||
.mobile-detail__thumb {
|
||||
position: relative;
|
||||
flex: 0 0 76px;
|
||||
height: 55px;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
border: 2px solid transparent;
|
||||
border-radius: 7px;
|
||||
background: #1b2923;
|
||||
}
|
||||
|
||||
.mobile-detail__thumb.is-active {
|
||||
border-color: #b45b3f;
|
||||
}
|
||||
|
||||
.mobile-detail__thumb img,
|
||||
.mobile-detail__thumb-fallback {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.mobile-detail__thumb img {
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.mobile-detail__thumb-fallback {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: #b5c0ba;
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
.mobile-detail__thumb em {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
padding: 2px 3px;
|
||||
overflow: hidden;
|
||||
background: rgba(17, 28, 23, 0.78);
|
||||
color: #fff;
|
||||
font-size: 8px;
|
||||
font-style: normal;
|
||||
text-align: center;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mobile-detail__viewer-label {
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.mobile-detail__spec-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.mobile-detail__spec-cell {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
padding: 9px 10px;
|
||||
border: 1px solid #edf0eb;
|
||||
border-radius: 8px;
|
||||
background: #f8faf7;
|
||||
}
|
||||
|
||||
.mobile-detail__spec-cell span {
|
||||
color: #849088;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.mobile-detail__spec-cell strong {
|
||||
color: #26322d;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
line-height: 1.45;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.mobile-detail__empty {
|
||||
margin: 0;
|
||||
color: var(--ink-muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.mobile-detail__note {
|
||||
padding: 12px 13px;
|
||||
border-left: 3px solid #b45b3f;
|
||||
border-radius: 0 8px 8px 0;
|
||||
background: #faf7f2;
|
||||
color: #48564f;
|
||||
font-size: 13px;
|
||||
line-height: 1.75;
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.mobile-detail__skin-groups {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.mobile-detail__skin-group > div:first-child {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.mobile-detail__skin-group strong {
|
||||
color: #35443d;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.mobile-detail__skin-group > div:first-child span {
|
||||
padding: 1px 6px;
|
||||
border-radius: 999px;
|
||||
background: #f0f3ef;
|
||||
color: #859189;
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
.mobile-detail__skin-pills {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.mobile-detail__skin-pills span {
|
||||
padding: 4px 8px;
|
||||
border: 1px solid #e2e7e0;
|
||||
border-radius: 6px;
|
||||
background: #f7f9f6;
|
||||
color: #48564f;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.mobile-detail__skin-pills span.is-melee {
|
||||
border-color: #ecd1bd;
|
||||
background: #fff8f1;
|
||||
color: #a95632;
|
||||
}
|
||||
|
||||
.mobile-detail__skin-pills span.is-operatorRed {
|
||||
border-color: #ecd4cf;
|
||||
background: #fff5f2;
|
||||
color: #a64332;
|
||||
}
|
||||
|
||||
.mobile-detail__skin-pills span.is-operatorGold {
|
||||
border-color: #eadca9;
|
||||
background: #fffbed;
|
||||
color: #9b6d18;
|
||||
}
|
||||
|
||||
.mobile-detail__skin-pills span.is-weapon {
|
||||
border-color: #cfe2d3;
|
||||
background: #f3faf5;
|
||||
color: #29744e;
|
||||
}
|
||||
|
||||
.mobile-detail__resource-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.mobile-detail__resource-tile {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 10px;
|
||||
border: 1px solid #e8ece6;
|
||||
border-radius: 8px;
|
||||
background: #f8faf7;
|
||||
}
|
||||
|
||||
.mobile-detail__resource-tile > div {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.mobile-detail__resource-tile > div span {
|
||||
overflow: hidden;
|
||||
color: #47544d;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mobile-detail__resource-tile > div strong {
|
||||
flex: 0 0 auto;
|
||||
color: #26322d;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.mobile-detail__resource-tile .mobile-detail__resource-mode {
|
||||
align-self: flex-start;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.mobile-detail__trust {
|
||||
margin-top: 10px;
|
||||
padding: 16px;
|
||||
border: 1px solid #e7e1d5;
|
||||
border-radius: 12px;
|
||||
background: #fcfaf5;
|
||||
}
|
||||
|
||||
.mobile-detail__trust-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.mobile-detail__trust-grid > div {
|
||||
min-width: 0;
|
||||
padding: 11px;
|
||||
border: 1px solid #eee8dc;
|
||||
border-radius: 8px;
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
}
|
||||
|
||||
.mobile-detail__trust-grid > div > span {
|
||||
display: grid;
|
||||
width: 27px;
|
||||
height: 27px;
|
||||
margin-bottom: 7px;
|
||||
place-items: center;
|
||||
border-radius: 7px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.mobile-detail__trust-grid > div > span.is-green {
|
||||
background: #eaf5ee;
|
||||
color: #2d7952;
|
||||
}
|
||||
|
||||
.mobile-detail__trust-grid > div > span.is-blue {
|
||||
background: #edf3f7;
|
||||
color: #476d82;
|
||||
}
|
||||
|
||||
.mobile-detail__trust-grid > div > span.is-orange {
|
||||
background: #faf0e8;
|
||||
color: #a85e35;
|
||||
}
|
||||
|
||||
.mobile-detail__trust-grid > div > span.is-slate {
|
||||
background: #eff1ed;
|
||||
color: #59665f;
|
||||
}
|
||||
|
||||
.mobile-detail__trust-grid strong {
|
||||
display: block;
|
||||
color: #35433c;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.mobile-detail__trust-grid p {
|
||||
margin: 4px 0 0;
|
||||
color: #7d8982;
|
||||
font-size: 10px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.mobile-detail__back {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -74,6 +634,7 @@
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 1px;
|
||||
margin-top: 10px;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
@@ -206,7 +767,7 @@
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
max-width: 560px;
|
||||
max-width: 480px;
|
||||
margin: 0 auto;
|
||||
padding: 12px 16px calc(12px + env(safe-area-inset-bottom));
|
||||
border-top: 1px solid var(--line);
|
||||
|
||||
Reference in New Issue
Block a user