Files
hfb_sys/frontend/src/features/admin/views/AdminPickupDetailView.vue
T

911 lines
28 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { computed, onMounted, reactive, ref } from 'vue'
import { EditPen } from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { useRoute } from 'vue-router'
import {
fetchAdminPickup,
fetchAdminPickupFinancialAdjustments,
fetchAdminPickupProcessEvents,
createAdminPickupFinancialAdjustment,
settleAdminPickupFinancialAdjustment,
updateAdminPickupProfit,
type AdminPickup,
type AdminPickupFinancialAdjustment,
type AdminPickupProcessEvent,
} from '@/features/admin/api/adminPickup'
import { quantity, readNumber, readUnitPrice } from '@/features/orders/composables/useOrderSnapshot'
import { adminPath } from '@/shared/utils/adminPath'
import { formatGameName, formatListingNo } from '@/shared/utils/listingDisplay'
import {
centToYuan,
formatCentWithSymbol,
formatMoneyWithSymbol,
yuanToCent,
} from '@/shared/utils/money'
import { pickupStatusLabel } from '@/shared/utils/statusLabels'
import { formatDateTime } from '@/shared/utils/time'
import ProcessTimeline from '../components/ProcessTimeline.vue'
interface SnapshotResource {
key: string
label: string
price: string
mode: string
quantity: number
amount: number
}
const route = useRoute()
const loading = ref(false)
const pickup = ref<AdminPickup | null>(null)
const financialAdjustments = ref<AdminPickupFinancialAdjustment[]>([])
const processEvents = ref<AdminPickupProcessEvent[]>([])
const profitDialogVisible = ref(false)
const financialAdjustmentDialogVisible = ref(false)
const profitSaving = ref(false)
const financialAdjustmentSaving = ref(false)
const profitForm = reactive({ profit_amount: 0, reason: '' })
const financialAdjustmentForm = reactive({ profit_amount: 0, settle_amount: 0, reason: '' })
const listingCode = computed(() =>
pickup.value ? formatListingNo(pickup.value.listing_no, pickup.value.listing_id) : '-'
)
const canEditProfit = computed(() => pickup.value?.status === 'picking_up')
const canCreateFinancialAdjustment = computed(() => pickup.value?.status === 'completed')
const snapshot = computed(() => normalizeRecord(pickup.value?.account_snapshot))
const assetSummary = computed(() => normalizeRecord(snapshot.value?.asset_summary))
const priceBreakdown = computed(() => normalizeRecord(assetSummary.value?.price_breakdown))
const snapshotText = computed(() =>
snapshot.value ? JSON.stringify(snapshot.value, null, 2) : '暂无账号快照'
)
const snapshotHafCoinM = computed(() =>
snapshot.value ? quantity(readNumber(snapshot.value.haf_coin_amount) / 1000000) : '-'
)
const snapshotSeasonTags = computed(() => {
const tags = snapshot.value?.season_tags
return Array.isArray(tags) ? tags.map(item => String(item)).filter(Boolean) : []
})
const snapshotResources = computed(() => readSnapshotResources(assetSummary.value))
const accountSnapshotItems = computed(() => {
const row = snapshot.value
if (!row) return []
return [
{ label: '账号ID', value: displayValue(row.account_id) },
{ label: '账号标题', value: displayValue(row.title) },
{ label: '游戏名称', value: formatGameName(row.game_name, '-') },
{ label: '所在区服', value: displayValue(row.server_region) },
{ label: '登录平台', value: displayValue(row.login_platform) },
{ label: '段位等级', value: displayValue(row.rank_level) },
].filter(item => item.value !== '-')
})
const moneySplitRows = computed(() => {
const row = pickup.value
if (!row) return []
const ownerCoinBasePriceCent = readBreakdownCent('seller_coin_base_price')
const ownerLossPriceCent =
readBreakdownCent('consumable_price') ?? fallbackOwnerLossCent(ownerCoinBasePriceCent)
const offlineTotalCent =
effectiveSettleCent(row) > 0 ? effectiveSettleCent(row) + effectiveProfitCent(row) : null
const rows = [
{ label: '网站售价', amountCent: row.listing_price_cent, tone: '' },
{ label: '号主纯币价格', amountCent: ownerCoinBasePriceCent, tone: '' },
{ label: '号主损耗', amountCent: ownerLossPriceCent, tone: '' },
{ label: '号主价合计', amountCent: row.owner_price_cent, tone: 'subtotal' },
{ label: '网站加价', amountCent: row.website_profit_cent, tone: '' },
{ label: '线下利润', amountCent: effectiveProfitCent(row), tone: 'profit' },
{ label: '结算给号主', amountCent: effectiveSettleCent(row), tone: '' },
]
if (offlineTotalCent !== null) {
rows.push({ label: '线下成交合计', amountCent: offlineTotalCent, tone: 'total' })
}
return rows
})
const breakdownRows = computed(() => {
const row = priceBreakdown.value
if (!row) return []
return [
{ label: '回收比例', value: ratioText(readNumber(row.seller_ratio)) },
{ label: '售卖比例', value: ratioText(readNumber(row.buyer_ratio)) },
{ label: '参考比例', value: ratioText(readNumber(row.seller_reference_ratio)) },
{ label: '加速出号比例', value: ratioText(readNumber(row.accelerated_sale_ratio)) },
{ label: '定价规则', value: displayValue(row.platform_rule_type) },
{ label: '调价原因', value: displayValue(row.admin_adjust_reason) },
{ label: '调价时间', value: displayValue(row.admin_adjusted_at) },
].filter(item => item.value !== '-')
})
onMounted(loadPickup)
async function loadPickup() {
loading.value = true
try {
const [item, adjustments, events] = await Promise.all([
fetchAdminPickup(String(route.params.id)),
fetchAdminPickupFinancialAdjustments(String(route.params.id)),
fetchAdminPickupProcessEvents(String(route.params.id)),
])
pickup.value = item
financialAdjustments.value = adjustments
processEvents.value = events
} finally {
loading.value = false
}
}
function openFinancialAdjustmentDialog() {
if (!pickup.value) return
financialAdjustmentForm.profit_amount = centToYuan(effectiveProfitCent(pickup.value))
financialAdjustmentForm.settle_amount = centToYuan(effectiveSettleCent(pickup.value))
financialAdjustmentForm.reason = ''
financialAdjustmentDialogVisible.value = true
}
async function handleFinancialAdjustment() {
if (!pickup.value) return
if (!financialAdjustmentForm.reason.trim()) {
ElMessage.warning('请输入调整原因')
return
}
if (financialAdjustmentForm.profit_amount < 0 || financialAdjustmentForm.settle_amount < 0) {
ElMessage.warning('金额不能小于 0')
return
}
financialAdjustmentSaving.value = true
try {
pickup.value = await createAdminPickupFinancialAdjustment(pickup.value.id, {
profit_amount_cent: yuanToCent(financialAdjustmentForm.profit_amount),
settle_amount_cent: yuanToCent(financialAdjustmentForm.settle_amount),
reason: financialAdjustmentForm.reason.trim(),
})
financialAdjustments.value = await fetchAdminPickupFinancialAdjustments(pickup.value.id)
financialAdjustmentDialogVisible.value = false
ElMessage.success('财务调整已创建')
} catch (e: unknown) {
ElMessage.error(errorMessage(e) || '调整失败')
} finally {
financialAdjustmentSaving.value = false
}
}
async function handleSettleFinancialAdjustment(item: AdminPickupFinancialAdjustment) {
const action = item.status === 'recovery_pending' ? '已追回' : '已线下打款'
try {
const { value } = await ElMessageBox.prompt(`确认该笔调整${action}`, '确认调整处理', {
confirmButtonText: '确认',
cancelButtonText: '取消',
inputType: 'textarea',
inputPlaceholder: '处理备注(可选)',
inputValidator: input => input.length <= 255 || '备注不能超过 255 个字符',
})
await settleAdminPickupFinancialAdjustment(item.id, { remark: value })
financialAdjustments.value = await fetchAdminPickupFinancialAdjustments(item.pickup_id)
ElMessage.success('调整已确认')
} catch {
// 用户取消确认。
}
}
function openProfitDialog() {
if (!pickup.value) return
profitForm.profit_amount = centToYuan(pickup.value.profit_amount_cent)
profitForm.reason = ''
profitDialogVisible.value = true
}
async function handleUpdateProfit() {
if (!pickup.value) return
if (profitForm.profit_amount < 0) {
ElMessage.warning('线下利润不能为负数')
return
}
profitSaving.value = true
try {
pickup.value = await updateAdminPickupProfit(pickup.value.id, {
profit_amount_cent: yuanToCent(profitForm.profit_amount),
reason: profitForm.reason,
})
ElMessage.success('线下利润已更新')
profitDialogVisible.value = false
} catch (e: unknown) {
ElMessage.error(errorMessage(e) || '修改失败')
} finally {
profitSaving.value = false
}
}
function statusType(status: string) {
if (status === 'picking_up') return 'warning'
if (status === 'completed') return 'success'
if (status === 'cancelled') return 'info'
return ''
}
function pickupSourceLabel(source: AdminPickup['account_source']) {
return source === 'external_platform_managed' ? '外部上传 · 平台代管' : '站内上传'
}
function pickupSourceTagType(source: AdminPickup['account_source']) {
return source === 'external_platform_managed' ? 'primary' : 'info'
}
function settlementModeLabel(row: AdminPickup) {
return row.settlement_mode === 'platform_managed' ? '平台线下结算' : '号主钱包结算'
}
function offlineSettlementLabel(row: AdminPickup) {
if (row.settlement_mode !== 'platform_managed') return '钱包已结算'
if (row.offline_settlement_status === 'pending') return '待线下结算'
if (row.offline_settlement_status === 'settled') return '已线下结算'
return '待完成提号'
}
function effectiveProfitCent(row: AdminPickup) {
return Number.isFinite(row.effective_profit_amount_cent)
? row.effective_profit_amount_cent
: row.profit_amount_cent
}
function effectiveSettleCent(row: AdminPickup) {
return Number.isFinite(row.effective_settle_amount_cent)
? row.effective_settle_amount_cent
: row.settle_amount_cent
}
function financialAdjustmentStatusLabel(status: AdminPickupFinancialAdjustment['status']) {
if (status === 'pending') return '待线下处理'
if (status === 'recovery_pending') return '待追回'
return '已处理'
}
function financialAdjustmentStatusType(status: AdminPickupFinancialAdjustment['status']) {
if (status === 'pending' || status === 'recovery_pending') return 'warning'
return 'success'
}
function formatSignedCent(value: number) {
const amount = formatCentWithSymbol(Math.abs(value))
return value > 0 ? `+${amount}` : value < 0 ? `-${amount}` : amount
}
function ratioText(value: number) {
const ratio = Number(value || 0)
if (ratio <= 0) return '-'
const rounded = Math.round(ratio * 100) / 100
return `1:${Number.isInteger(rounded) ? rounded : rounded.toFixed(2)}`
}
function displayValue(value: unknown) {
if (value === undefined || value === null || value === '') return '-'
return String(value)
}
function errorMessage(e: unknown) {
if (typeof e === 'object' && e !== null && 'message' in e) {
return String((e as { message?: unknown }).message)
}
return ''
}
function readBreakdownCent(key: string) {
const value = readNumber(priceBreakdown.value?.[key])
return value > 0 ? Math.round(value * 100) : null
}
function fallbackOwnerLossCent(ownerCoinBasePriceCent: number | null) {
const row = pickup.value
if (!row || ownerCoinBasePriceCent === null) return null
return Math.max(row.owner_price_cent - ownerCoinBasePriceCent, 0)
}
function formatNullableCent(value: number | null | undefined) {
if (value === null || value === undefined) return '-'
return formatCentWithSymbol(value)
}
function normalizeRecord(value: unknown): Record<string, unknown> | null {
if (typeof value === 'object' && value !== null) return value as Record<string, unknown>
if (typeof value === 'string' && value.trim()) {
try {
const parsed = JSON.parse(value)
return typeof parsed === 'object' && parsed !== null ? parsed : null
} catch {
return null
}
}
return null
}
function readSnapshotResources(summary: Record<string, unknown> | null): SnapshotResource[] {
const resources = summary?.resources
if (!Array.isArray(resources)) return []
return resources
.filter((item): item is Record<string, unknown> => typeof item === 'object' && item !== null)
.map(item => {
const key = String(item.key || item.label || '')
const label = String(item.label || key || '额外消耗品')
const price = String(item.price || '')
const mode = String(item.mode || '收费')
const count = readNumber(item.quantity)
return {
key,
label,
price,
mode,
quantity: count,
amount: mode === '收费' ? count * readUnitPrice(price) : 0,
}
})
.filter(item => item.key && item.quantity > 0)
}
</script>
<template>
<section class="page" v-loading="loading">
<div v-if="pickup" class="page-header-row">
<div class="page-header">
<p class="eyebrow">{{ pickup.pickup_no }}</p>
<h1>提号详情</h1>
<p>
商品编号 {{ listingCode }} · {{ pickup.account_title }} · {{ pickup.server_region }} /
{{ pickup.login_platform }}
</p>
</div>
<div class="toolbar-actions">
<el-button
v-if="canEditProfit"
type="primary"
plain
:icon="EditPen"
@click="openProfitDialog"
>
修改利润
</el-button>
<el-button
v-if="canCreateFinancialAdjustment"
type="primary"
plain
:icon="EditPen"
@click="openFinancialAdjustmentDialog"
>
财务调整
</el-button>
<RouterLink :to="adminPath('pickup')">
<el-button>返回列表</el-button>
</RouterLink>
</div>
</div>
<div v-if="pickup" class="metric-grid pickup-metrics">
<div class="metric-card">
<span>提号状态</span>
<strong>{{ pickupStatusLabel(pickup.status) }}</strong>
<small>创建 {{ formatDateTime(pickup.created_at) }}</small>
</div>
<div class="metric-card">
<span>线下利润</span>
<strong>{{ formatCentWithSymbol(effectiveProfitCent(pickup)) }}</strong>
<small>财务统计使用该金额</small>
</div>
<div class="metric-card">
<span>结算金额</span>
<strong>{{ formatCentWithSymbol(effectiveSettleCent(pickup)) }}</strong>
<small>{{ pickup.completed_at ? formatDateTime(pickup.completed_at) : '待结算' }}</small>
</div>
<div class="metric-card">
<span>比例</span>
<strong>{{ ratioText(pickup.seller_ratio) }} / {{ ratioText(pickup.buyer_ratio) }}</strong>
<small>回收 / 售卖</small>
</div>
</div>
<div v-if="pickup" class="pickup-detail-grid">
<section class="dashboard-panel detail-panel">
<div class="panel-heading">
<h2>提号概览</h2>
<el-tag :type="statusType(pickup.status)" effect="light">
{{ pickupStatusLabel(pickup.status) }}
</el-tag>
</div>
<dl class="detail-list">
<div>
<dt>提号编号</dt>
<dd>{{ pickup.pickup_no }}</dd>
</div>
<div>
<dt>上架编号</dt>
<dd>{{ listingCode }}</dd>
</div>
<div>
<dt>账号ID</dt>
<dd>{{ pickup.account_id }}</dd>
</div>
<div>
<dt>号主</dt>
<dd>{{ pickup.owner_phone || pickup.owner_id }}</dd>
</div>
<div>
<dt>账号来源</dt>
<dd class="source-detail">
<el-tag
:type="pickupSourceTagType(pickup.account_source)"
effect="light"
size="small"
>
{{ pickupSourceLabel(pickup.account_source) }}
</el-tag>
<span v-if="pickup.source_channel">{{ pickup.source_channel }}</span>
</dd>
</div>
<div>
<dt>结算方式</dt>
<dd>{{ settlementModeLabel(pickup) }}</dd>
</div>
<div>
<dt>结算状态</dt>
<dd>{{ offlineSettlementLabel(pickup) }}</dd>
</div>
<div v-if="pickup.settlement_mode === 'platform_managed'">
<dt>线下结算时间</dt>
<dd>{{ formatDateTime(pickup.offline_settled_at, '待确认') }}</dd>
</div>
<div>
<dt>交易渠道</dt>
<dd>{{ pickup.platform || '-' }}</dd>
</div>
<div>
<dt>店铺名称</dt>
<dd>{{ pickup.shop_name || '-' }}</dd>
</div>
<div>
<dt>管理员ID</dt>
<dd>{{ pickup.admin_id }}</dd>
</div>
<div>
<dt>创建时间</dt>
<dd>{{ formatDateTime(pickup.created_at) }}</dd>
</div>
<div>
<dt>完成时间</dt>
<dd>{{ formatDateTime(pickup.completed_at, '未完成') }}</dd>
</div>
</dl>
</section>
<section class="dashboard-panel detail-panel pickup-wide-panel">
<div class="panel-heading">
<h2>提号全流程</h2>
<span class="panel-subtitle">创建完成调整及线下结算均单独留痕</span>
</div>
<ProcessTimeline
:events="processEvents"
empty-text="暂无新版过程记录可先查看下方备注和财务调整记录"
/>
</section>
<section
v-if="financialAdjustments.length"
class="dashboard-panel detail-panel pickup-wide-panel"
>
<div class="panel-heading">
<h2>财务调整记录</h2>
<span class="panel-subtitle">原始完成金额不变调整按创建日计入财务</span>
</div>
<el-table :data="financialAdjustments" size="small" class="adjustment-table">
<el-table-column label="利润调整" width="130">
<template #default="{ row }">{{ formatSignedCent(row.profit_delta_cent) }}</template>
</el-table-column>
<el-table-column label="打款调整" width="130">
<template #default="{ row }">{{ formatSignedCent(row.settle_delta_cent) }}</template>
</el-table-column>
<el-table-column label="处理状态" width="130">
<template #default="{ row }">
<el-tag :type="financialAdjustmentStatusType(row.status)" effect="light" size="small">
{{ financialAdjustmentStatusLabel(row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="reason" label="调整原因" min-width="180" />
<el-table-column label="创建时间" width="170">
<template #default="{ row }">{{ formatDateTime(row.created_at) }}</template>
</el-table-column>
<el-table-column label="操作" width="120" fixed="right">
<template #default="{ row }">
<el-button
v-if="
row.status === 'recovery_pending' ||
(row.status === 'pending' && pickup?.offline_settlement_status !== 'pending')
"
size="small"
type="primary"
plain
@click="handleSettleFinancialAdjustment(row)"
>
{{ row.status === 'recovery_pending' ? '确认追回' : '确认打款' }}
</el-button>
<span v-else class="text-muted">{{ formatDateTime(row.settled_at, '-') }}</span>
</template>
</el-table-column>
</el-table>
</section>
<section class="dashboard-panel detail-panel">
<div class="panel-heading">
<h2>资金拆分</h2>
<span class="panel-subtitle">价格快照来自创建时</span>
</div>
<div class="money-list">
<div
v-for="row in moneySplitRows"
:key="row.label"
class="money-row"
:class="`is-${row.tone || 'normal'}`"
>
<span>{{ row.label }}</span>
<strong>{{ formatNullableCent(row.amountCent) }}</strong>
</div>
</div>
</section>
<section class="dashboard-panel detail-panel">
<div class="panel-heading">
<h2>备注</h2>
</div>
<dl class="detail-list">
<div class="wide">
<dt>创建备注</dt>
<dd>{{ pickup.remark || '-' }}</dd>
</div>
<div class="wide">
<dt>完成备注</dt>
<dd>{{ pickup.complete_remark || '-' }}</dd>
</div>
<div v-if="pickup.settlement_mode === 'platform_managed'" class="wide">
<dt>线下结算备注</dt>
<dd>{{ pickup.offline_settlement_remark || '-' }}</dd>
</div>
</dl>
</section>
<section v-if="breakdownRows.length" class="dashboard-panel detail-panel">
<div class="panel-heading">
<h2>价格拆分</h2>
<span class="panel-subtitle">来自账号快照</span>
</div>
<dl class="detail-list">
<div v-for="row in breakdownRows" :key="row.label">
<dt>{{ row.label }}</dt>
<dd>{{ row.value }}</dd>
</div>
</dl>
</section>
<section class="dashboard-panel detail-panel pickup-wide-panel">
<div class="panel-heading">
<h2>账号快照</h2>
<span class="panel-subtitle">哈夫币 {{ snapshotHafCoinM }}M</span>
</div>
<dl v-if="accountSnapshotItems.length" class="detail-list snapshot-detail-list">
<div v-for="item in accountSnapshotItems" :key="item.label">
<dt>{{ item.label }}</dt>
<dd>{{ item.value }}</dd>
</div>
</dl>
<div v-if="snapshotSeasonTags.length" class="snapshot-tags">
<el-tag v-for="tag in snapshotSeasonTags" :key="tag" size="small" effect="plain">
{{ tag }}
</el-tag>
</div>
<div v-if="snapshotResources.length" class="snapshot-resources">
<div v-for="resource in snapshotResources" :key="resource.key" class="resource-item">
<strong>{{ resource.label }}</strong>
<span>
{{ quantity(resource.quantity) }} · {{ resource.price || resource.mode }}
<template v-if="resource.amount > 0">
· {{ formatMoneyWithSymbol(resource.amount) }}</template
>
</span>
</div>
</div>
<pre class="snapshot-json">{{ snapshotText }}</pre>
</section>
</div>
<el-dialog v-model="profitDialogVisible" title="修改线下利润" width="460px">
<el-form label-width="110px">
<el-form-item label="线下利润(元)" required>
<el-input-number
v-model="profitForm.profit_amount"
:min="0"
:step="1"
:precision="2"
controls-position="right"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="修改原因">
<el-input
v-model="profitForm.reason"
type="textarea"
:rows="3"
maxlength="255"
show-word-limit
placeholder="可选"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="profitDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="profitSaving" @click="handleUpdateProfit">
保存
</el-button>
</template>
</el-dialog>
<el-dialog v-model="financialAdjustmentDialogVisible" title="完成后财务调整" width="500px">
<el-form label-width="130px">
<el-form-item label="目标利润(元)">
<el-input-number
v-model="financialAdjustmentForm.profit_amount"
:min="0"
:step="1"
:precision="2"
controls-position="right"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="目标打款金额(元)">
<el-input-number
v-model="financialAdjustmentForm.settle_amount"
:min="0"
:step="1"
:precision="2"
controls-position="right"
style="width: 100%"
/>
<div class="form-hint">
{{
pickup?.settlement_mode === 'platform_managed'
? '会生成待线下补款或待追回记录'
: '增加金额立即补入钱包,减少金额生成待追回记录'
}}
</div>
</el-form-item>
<el-form-item label="调整原因" required>
<el-input
v-model="financialAdjustmentForm.reason"
type="textarea"
:rows="3"
maxlength="255"
show-word-limit
placeholder="请输入调整原因"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="financialAdjustmentDialogVisible = false">取消</el-button>
<el-button
type="primary"
:loading="financialAdjustmentSaving"
@click="handleFinancialAdjustment"
>
确认调整
</el-button>
</template>
</el-dialog>
</section>
</template>
<style scoped>
.pickup-metrics {
margin-bottom: 20px;
}
.pickup-metrics .metric-card strong {
font-size: 20px;
line-height: 1.25;
overflow-wrap: anywhere;
}
.metric-card small {
display: block;
margin-top: 8px;
color: #8f9bba;
font-size: 12px;
line-height: 1.4;
word-break: break-word;
}
.form-hint {
margin-top: 4px;
color: #64748b;
font-size: 12px;
line-height: 1.5;
}
.adjustment-table {
width: 100%;
}
.pickup-detail-grid {
display: grid;
grid-template-columns: minmax(0, 1.1fr) minmax(360px, 0.9fr);
gap: 20px;
margin-bottom: 28px;
}
.detail-panel {
min-width: 0;
}
.pickup-wide-panel {
grid-column: 1 / -1;
}
.panel-heading {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
margin-bottom: 16px;
}
.panel-heading h2 {
margin: 0;
}
.panel-subtitle {
color: #8f9bba;
font-size: 13px;
line-height: 1.6;
text-align: right;
}
.detail-list {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px 18px;
margin: 0;
}
.detail-list div {
min-width: 0;
border-top: 1px solid #f0f2f5;
padding-top: 10px;
}
.detail-list .wide {
grid-column: 1 / -1;
}
.detail-list dt {
margin-bottom: 5px;
color: #8f9bba;
font-size: 12px;
font-weight: 600;
}
.detail-list dd {
margin: 0;
color: #1b2559;
font-size: 14px;
font-weight: 700;
line-height: 1.5;
overflow-wrap: anywhere;
white-space: pre-wrap;
}
.source-detail {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 6px 8px;
}
.money-list {
display: grid;
gap: 8px;
}
.money-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
border-top: 1px solid #f0f2f5;
padding-top: 9px;
color: #52616f;
font-size: 13px;
}
.money-row strong {
color: #1b2559;
font-size: 15px;
}
.money-row.is-subtotal {
border-top-color: #d8e2f0;
color: #1b2559;
font-weight: 700;
}
.money-row.is-profit strong {
color: #0f766e;
}
.money-row.is-total {
margin-top: 4px;
border-top-color: #bfdbfe;
color: #1b2559;
font-weight: 800;
}
.money-row.is-total strong {
font-size: 18px;
}
.snapshot-detail-list {
margin-bottom: 14px;
}
.snapshot-tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 14px;
}
.snapshot-resources {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 10px;
margin-bottom: 14px;
}
.resource-item {
display: grid;
gap: 4px;
border: 1px solid #eef2f7;
border-radius: 8px;
background: #fbfcff;
padding: 10px 12px;
}
.resource-item strong {
color: #1b2559;
font-size: 13px;
}
.resource-item span {
color: #52616f;
font-size: 12px;
}
.snapshot-json {
overflow-x: auto;
max-height: 360px;
margin: 0;
border-radius: 8px;
background: #0f172a;
color: #dbeafe;
padding: 14px;
font-size: 12px;
line-height: 1.6;
}
@media (max-width: 960px) {
.pickup-detail-grid {
grid-template-columns: 1fr;
}
.pickup-wide-panel {
grid-column: auto;
}
.detail-list {
grid-template-columns: 1fr;
}
}
</style>