- fix(pricing): 后置消耗/损耗金额 100% 划归号主收益,超出预收部分平台不抽成,避免被平台费吸收 - feat(checkout): 前端新增结账实时试算预览组件 (CheckoutTrialPreview + useCheckoutSimulator),角色隔离展示(仅见自身收入/退款,屏蔽平台服务费) - refactor(admin): 重构交易全流程时间线 (ProcessTimeline.vue),将平铺挤压指标重构成4大分类仪表盘微型卡片矩阵,升级可视化业务状态流转徽章与阶段色彩主题 - style(admin): 优化支付与交接流水元信息网格布局,防止超宽屏下长单行拉伸
1139 lines
25 KiB
Vue
1139 lines
25 KiB
Vue
<script setup lang="ts">
|
||
import { formatCentWithSymbol } from '@/shared/utils/money'
|
||
import { formatDateTime } from '@/shared/utils/time'
|
||
|
||
export interface ProcessTimelineEvent {
|
||
id: number
|
||
stage: string
|
||
action: string
|
||
actor_type: string
|
||
actor_id: number
|
||
actor_name: string
|
||
target_type?: string
|
||
target_id?: number
|
||
target_name?: string
|
||
content?: string
|
||
reason?: string
|
||
payload?: Record<string, unknown>
|
||
attachment_urls?: string[]
|
||
state_before?: Record<string, unknown>
|
||
state_after?: Record<string, unknown>
|
||
created_at: string
|
||
}
|
||
|
||
const props = defineProps<{ events: ProcessTimelineEvent[]; emptyText?: string }>()
|
||
|
||
const actionLabels: Record<string, string> = {
|
||
owner_handoff_submitted: '号主提交交接',
|
||
platform_handoff_submitted: '客服代交接',
|
||
admin_force_handoff: '客服确认交接',
|
||
renter_received_confirmed: '租客确认收号',
|
||
checkout_submitted: '租客发起结账',
|
||
checkout_countered: '修改结账方案',
|
||
checkout_confirmed: '号主确认结账',
|
||
checkout_accepted: '租客接受结账方案',
|
||
platform_checkout_countered: '客服修改结账方案',
|
||
platform_checkout_confirmed: '客服确认结账',
|
||
platform_checkout_dispute_opened: '客服发起结账争议',
|
||
offline_settlement_confirmed: '确认线下结算',
|
||
deposit_held: '暂扣押金',
|
||
deposit_released: '归还暂扣押金',
|
||
pickup_created: '创建提号',
|
||
pickup_completed: '完成提号',
|
||
pickup_profit_updated: '修改提号利润',
|
||
pickup_financial_adjusted: '创建财务调整',
|
||
pickup_adjustment_settled: '确认财务调整',
|
||
pickup_offline_settlement_confirmed: '确认提号线下结算',
|
||
pickup_cancelled: '取消提号',
|
||
}
|
||
|
||
const stageLabels: Record<string, string> = {
|
||
handoff: '交接',
|
||
checkout: '结账',
|
||
settlement: '结算',
|
||
dispute: '争议',
|
||
pickup: '提号',
|
||
}
|
||
|
||
const payloadLabels: Record<string, string> = {
|
||
checkout_id: '结账单ID',
|
||
round: '协商轮次',
|
||
turn: '等待确认方',
|
||
proposed_by: '方案提出人',
|
||
rent_amount_cent: '实际结算租金',
|
||
owner_rent_amount_cent: '号主基础租金',
|
||
platform_fee_cent: '平台服务费',
|
||
deposit_amount_cent: '订单押金',
|
||
consumable_amount_cent: '消耗品金额',
|
||
coin_consumed_m: '哈夫币消耗',
|
||
deposit_deduct_amount_cent: '押金赔付扣除',
|
||
renter_refund_amount_cent: '退还租客',
|
||
owner_income_amount_cent: '号主最终收入',
|
||
shortfall_cent: '押金不足差额',
|
||
overshoot_amount_cent: '打超金额',
|
||
offline_settlement_amount_cent: '线下待结算',
|
||
deposit_hold_amount_cent: '暂扣金额',
|
||
profit_amount_cent: '利润金额',
|
||
settle_amount_cent: '结算给号主',
|
||
profit_delta_cent: '利润调整',
|
||
settle_delta_cent: '结算调整',
|
||
settlement_mode: '结算方式',
|
||
account_source: '账号来源',
|
||
source_channel: '来源渠道',
|
||
}
|
||
|
||
const primaryPayloadKeys = new Set([
|
||
'round',
|
||
'turn',
|
||
'rent_amount_cent',
|
||
'renter_refund_amount_cent',
|
||
'owner_income_amount_cent',
|
||
'offline_settlement_amount_cent',
|
||
'profit_amount_cent',
|
||
'settle_amount_cent',
|
||
])
|
||
|
||
const stateLabels: Record<string, Record<string, string>> = {
|
||
order_status: {
|
||
pending_payment: '待支付',
|
||
pending_handoff: '待交接',
|
||
renting: '租用中',
|
||
overdue: '已逾期',
|
||
pending_checkout_confirm: '待号主确认结账',
|
||
pending_checkout_accept: '待租客确认结账',
|
||
completed: '已完成',
|
||
cancelled: '已取消',
|
||
closed: '已关闭',
|
||
abnormal: '异常',
|
||
},
|
||
handoff_status: {
|
||
none: '未开始',
|
||
pending_owner: '待号主交接',
|
||
pending_renter_confirm: '待租客确认收号',
|
||
received: '租客已收号',
|
||
pending_owner_checkout: '待号主确认结账',
|
||
pending_renter_checkout: '待租客确认结账',
|
||
returned: '已归还',
|
||
},
|
||
settlement_status: {
|
||
unsettled: '未结算',
|
||
pending: '结算待确认',
|
||
settled: '已结算',
|
||
arbitrated: '仲裁结算',
|
||
},
|
||
offline_settlement_status: { none: '无需线下结算', pending: '待线下结算', settled: '已线下结算' },
|
||
pickup_status: {
|
||
pending: '待处理',
|
||
processing: '处理中',
|
||
completed: '已完成',
|
||
cancelled: '已取消',
|
||
},
|
||
}
|
||
|
||
const stateFieldLabels: Record<string, string> = {
|
||
order_status: '订单状态',
|
||
handoff_status: '交接状态',
|
||
settlement_status: '结算状态',
|
||
offline_settlement_status: '线下结算',
|
||
pickup_status: '提号状态',
|
||
}
|
||
|
||
interface PayloadItem {
|
||
key: string
|
||
label: string
|
||
value: string
|
||
variant: 'income' | 'refund' | 'fee' | 'deduct' | 'primary' | 'neutral'
|
||
}
|
||
|
||
interface PayloadCategory {
|
||
id: string
|
||
title: string
|
||
icon: string
|
||
items: PayloadItem[]
|
||
}
|
||
|
||
const CATEGORY_DEFINITIONS: {
|
||
id: string
|
||
title: string
|
||
icon: string
|
||
keys: string[]
|
||
}[] = [
|
||
{
|
||
id: 'financial',
|
||
title: '资金结算明细',
|
||
icon: '💰',
|
||
keys: [
|
||
'owner_rent_amount_cent',
|
||
'platform_fee_cent',
|
||
'owner_income_amount_cent',
|
||
'renter_refund_amount_cent',
|
||
'rent_amount_cent',
|
||
'offline_settlement_amount_cent',
|
||
'profit_amount_cent',
|
||
'settle_amount_cent',
|
||
'profit_delta_cent',
|
||
'settle_delta_cent',
|
||
],
|
||
},
|
||
{
|
||
id: 'deposit',
|
||
title: '押金与赔付扣除',
|
||
icon: '🛡️',
|
||
keys: [
|
||
'deposit_amount_cent',
|
||
'deposit_deduct_amount_cent',
|
||
'overshoot_amount_cent',
|
||
'shortfall_cent',
|
||
'deposit_hold_amount_cent',
|
||
],
|
||
},
|
||
{
|
||
id: 'gameplay',
|
||
title: '消耗与游戏指标',
|
||
icon: '🎮',
|
||
keys: ['coin_consumed_m', 'consumable_amount_cent'],
|
||
},
|
||
{
|
||
id: 'meta',
|
||
title: '流程单据与属性',
|
||
icon: '📋',
|
||
keys: [
|
||
'checkout_id',
|
||
'round',
|
||
'turn',
|
||
'proposed_by',
|
||
'settlement_mode',
|
||
'account_source',
|
||
'source_channel',
|
||
],
|
||
},
|
||
]
|
||
|
||
function actionLabel(action: string) {
|
||
return actionLabels[action] || action
|
||
}
|
||
|
||
function stageLabel(stage: string) {
|
||
return stageLabels[stage] || '流程'
|
||
}
|
||
|
||
function actorLabel(item: ProcessTimelineEvent) {
|
||
if (item.actor_name) return item.actor_name
|
||
if (item.actor_type === 'system') return '系统'
|
||
return `${item.actor_type === 'admin' ? '客服' : '用户'} ID ${item.actor_id}`
|
||
}
|
||
|
||
function actorRole(item: ProcessTimelineEvent) {
|
||
if (item.actor_type === 'admin') return '客服操作'
|
||
if (item.actor_type === 'system') return '系统处理'
|
||
return '用户操作'
|
||
}
|
||
|
||
function payloadRows(payload?: Record<string, unknown>) {
|
||
if (!payload || typeof payload !== 'object') return []
|
||
return Object.entries(payload).filter(
|
||
([, value]) => value !== null && value !== undefined && value !== ''
|
||
)
|
||
}
|
||
|
||
function primaryPayloadRows(item: ProcessTimelineEvent) {
|
||
return payloadRows(item.payload).filter(([key]) => primaryPayloadKeys.has(key))
|
||
}
|
||
|
||
function detailPayloadRows(item: ProcessTimelineEvent) {
|
||
return payloadRows(item.payload).filter(([key]) => !primaryPayloadKeys.has(key))
|
||
}
|
||
|
||
function payloadLabel(key: string) {
|
||
return payloadLabels[key] || key
|
||
}
|
||
|
||
function payloadValue(key: string, value: unknown) {
|
||
if (key.endsWith('_cent')) return formatCentWithSymbol(Number(value || 0))
|
||
if (key === 'coin_consumed_m') return `${value} M`
|
||
if (key === 'round') return `第 ${value} 轮`
|
||
if (key === 'turn')
|
||
return value === 'owner' ? '号主' : value === 'renter' ? '租客' : String(value)
|
||
if (key === 'account_source') return value === 'external' ? '外部上传' : '站内上传'
|
||
if (key === 'settlement_mode')
|
||
return value === 'platform_managed' ? '平台线下结算' : '号主钱包结算'
|
||
if (typeof value === 'object') return JSON.stringify(value)
|
||
return String(value)
|
||
}
|
||
|
||
function itemVariant(key: string): PayloadItem['variant'] {
|
||
if (
|
||
key === 'owner_income_amount_cent' ||
|
||
key === 'profit_amount_cent' ||
|
||
key === 'settle_amount_cent'
|
||
) {
|
||
return 'income'
|
||
}
|
||
if (key === 'renter_refund_amount_cent') {
|
||
return 'refund'
|
||
}
|
||
if (key === 'platform_fee_cent') {
|
||
return 'fee'
|
||
}
|
||
if (
|
||
key === 'deposit_deduct_amount_cent' ||
|
||
key === 'overshoot_amount_cent' ||
|
||
key === 'shortfall_cent' ||
|
||
key === 'deposit_hold_amount_cent'
|
||
) {
|
||
return 'deduct'
|
||
}
|
||
if (key === 'rent_amount_cent' || key === 'deposit_amount_cent') {
|
||
return 'primary'
|
||
}
|
||
return 'neutral'
|
||
}
|
||
|
||
function categorizedPayload(item: ProcessTimelineEvent): PayloadCategory[] {
|
||
const detailRows = detailPayloadRows(item)
|
||
if (detailRows.length === 0) return []
|
||
|
||
const rowMap = new Map<string, unknown>(detailRows)
|
||
const usedKeys = new Set<string>()
|
||
const result: PayloadCategory[] = []
|
||
|
||
for (const cat of CATEGORY_DEFINITIONS) {
|
||
const items: PayloadItem[] = []
|
||
for (const k of cat.keys) {
|
||
if (rowMap.has(k)) {
|
||
usedKeys.add(k)
|
||
items.push({
|
||
key: k,
|
||
label: payloadLabel(k),
|
||
value: payloadValue(k, rowMap.get(k)),
|
||
variant: itemVariant(k),
|
||
})
|
||
}
|
||
}
|
||
if (items.length > 0) {
|
||
result.push({
|
||
id: cat.id,
|
||
title: cat.title,
|
||
icon: cat.icon,
|
||
items,
|
||
})
|
||
}
|
||
}
|
||
|
||
const remainingKeys = detailRows.filter(([k]) => !usedKeys.has(k))
|
||
if (remainingKeys.length > 0) {
|
||
const gameplayItems: PayloadItem[] = []
|
||
const metaItems: PayloadItem[] = []
|
||
|
||
for (const [k, v] of remainingKeys) {
|
||
const isMoney = k.endsWith('_cent')
|
||
const pItem: PayloadItem = {
|
||
key: k,
|
||
label: payloadLabel(k),
|
||
value: payloadValue(k, v),
|
||
variant: itemVariant(k),
|
||
}
|
||
if (isMoney) {
|
||
const finCat = result.find(c => c.id === 'financial')
|
||
if (finCat) {
|
||
finCat.items.push(pItem)
|
||
continue
|
||
}
|
||
}
|
||
if (
|
||
typeof v === 'number' ||
|
||
k.includes('kill') ||
|
||
k.includes('count') ||
|
||
k.includes('coin')
|
||
) {
|
||
gameplayItems.push(pItem)
|
||
} else {
|
||
metaItems.push(pItem)
|
||
}
|
||
}
|
||
|
||
if (gameplayItems.length > 0) {
|
||
const existing = result.find(c => c.id === 'gameplay')
|
||
if (existing) {
|
||
existing.items.push(...gameplayItems)
|
||
} else {
|
||
result.push({
|
||
id: 'gameplay',
|
||
title: '消耗与游戏指标',
|
||
icon: '🎮',
|
||
items: gameplayItems,
|
||
})
|
||
}
|
||
}
|
||
|
||
if (metaItems.length > 0) {
|
||
const existing = result.find(c => c.id === 'meta')
|
||
if (existing) {
|
||
existing.items.push(...metaItems)
|
||
} else {
|
||
result.push({
|
||
id: 'meta',
|
||
title: '其他属性与单据',
|
||
icon: '📋',
|
||
items: metaItems,
|
||
})
|
||
}
|
||
}
|
||
}
|
||
|
||
return result
|
||
}
|
||
|
||
function stateValue(key: string, value: unknown) {
|
||
const raw = String(value || '')
|
||
return stateLabels[key]?.[raw] || raw || '-'
|
||
}
|
||
|
||
function stateChanges(item: ProcessTimelineEvent) {
|
||
const before = item.state_before || {}
|
||
const after = item.state_after || {}
|
||
return Object.keys(stateFieldLabels)
|
||
.filter(key => before[key] !== undefined && before[key] !== after[key])
|
||
.map(key => ({
|
||
label: stateFieldLabels[key],
|
||
before: stateValue(key, before[key]),
|
||
after: stateValue(key, after[key]),
|
||
}))
|
||
}
|
||
|
||
function stateToneClass(val: string) {
|
||
if (
|
||
['已完成', '已结算', '租客已收号', '已归还', '处理完成', '已线下结算'].some(s =>
|
||
val.includes(s)
|
||
)
|
||
) {
|
||
return 'tone-success'
|
||
}
|
||
if (['租用中', '处理中'].some(s => val.includes(s))) {
|
||
return 'tone-primary'
|
||
}
|
||
if (['待', '等待', '未结算', '未开始'].some(s => val.includes(s))) {
|
||
return 'tone-warning'
|
||
}
|
||
if (['已取消', '已关闭', '异常', '争议'].some(s => val.includes(s))) {
|
||
return 'tone-danger'
|
||
}
|
||
return 'tone-neutral'
|
||
}
|
||
|
||
function hasMore(item: ProcessTimelineEvent) {
|
||
return detailPayloadRows(item).length > 0 || stateChanges(item).length > 0
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<el-empty v-if="props.events.length === 0" :description="props.emptyText || '暂无过程记录'" />
|
||
<div v-else class="process-timeline">
|
||
<article
|
||
v-for="item in props.events"
|
||
:key="item.id"
|
||
class="process-item"
|
||
:class="`process-item-${item.stage}`"
|
||
>
|
||
<span class="process-dot" :class="`dot-${item.stage}`"></span>
|
||
<header class="process-heading">
|
||
<div class="heading-main">
|
||
<span class="stage-badge" :class="`stage-${item.stage}`">
|
||
{{ stageLabel(item.stage) }}
|
||
</span>
|
||
<strong class="action-title">{{ actionLabel(item.action) }}</strong>
|
||
</div>
|
||
<time class="process-time">{{ formatDateTime(item.created_at) }}</time>
|
||
</header>
|
||
|
||
<div class="process-people">
|
||
<span class="actor-badge" :class="`actor-${item.actor_type}`">
|
||
{{ actorRole(item) }}
|
||
</span>
|
||
<span class="actor-name">{{ actorLabel(item) }}</span>
|
||
<span v-if="item.target_name" class="target-text">
|
||
<span class="target-arrow">➔</span> 关联对象:{{ item.target_name }}
|
||
</span>
|
||
</div>
|
||
|
||
<div v-if="item.content" class="process-content-card">
|
||
<div class="content-text">{{ item.content }}</div>
|
||
</div>
|
||
|
||
<div v-if="item.reason" class="process-reason-card">
|
||
<strong class="reason-tag">备注说明</strong>
|
||
<span class="reason-text">{{ item.reason }}</span>
|
||
</div>
|
||
|
||
<!-- High-visibility Summary Badges -->
|
||
<div v-if="primaryPayloadRows(item).length" class="process-summary-grid">
|
||
<div
|
||
v-for="[key, value] in primaryPayloadRows(item)"
|
||
:key="key"
|
||
class="summary-pill"
|
||
:class="`summary-pill-${itemVariant(key)}`"
|
||
>
|
||
<span class="summary-label">{{ payloadLabel(key) }}</span>
|
||
<strong class="summary-val">{{ payloadValue(key, value) }}</strong>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Expandable Structured Details -->
|
||
<details v-if="hasMore(item)" class="process-details">
|
||
<summary class="process-details-summary">
|
||
<span class="toggle-left">
|
||
<span class="toggle-arrow">▾</span>
|
||
<span class="toggle-title">查看完整明细与状态变化</span>
|
||
</span>
|
||
<span class="toggle-badge">
|
||
{{ detailPayloadRows(item).length }} 项明细指标<span v-if="stateChanges(item).length">
|
||
· {{ stateChanges(item).length }} 项状态变化</span
|
||
>
|
||
</span>
|
||
</summary>
|
||
|
||
<div class="process-details-body">
|
||
<!-- Categorized Detail Groups -->
|
||
<div v-for="cat in categorizedPayload(item)" :key="cat.id" class="detail-category-block">
|
||
<div class="category-header">
|
||
<span class="category-icon">{{ cat.icon }}</span>
|
||
<span class="category-title">{{ cat.title }}</span>
|
||
<span class="category-count">({{ cat.items.length }})</span>
|
||
</div>
|
||
<div class="category-grid">
|
||
<div
|
||
v-for="sub in cat.items"
|
||
:key="sub.key"
|
||
class="metric-box"
|
||
:class="`metric-box-${sub.variant}`"
|
||
>
|
||
<div class="metric-label">{{ sub.label }}</div>
|
||
<div class="metric-value">{{ sub.value }}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Business State Changes Flow -->
|
||
<div v-if="stateChanges(item).length" class="state-changes-block">
|
||
<div class="category-header">
|
||
<span class="category-icon">🔄</span>
|
||
<span class="category-title">业务状态流转</span>
|
||
<span class="category-count">({{ stateChanges(item).length }})</span>
|
||
</div>
|
||
<div class="state-flow-list">
|
||
<div v-for="change in stateChanges(item)" :key="change.label" class="state-flow-item">
|
||
<span class="state-field-badge">{{ change.label }}</span>
|
||
<span class="state-node state-node-before">{{ change.before }}</span>
|
||
<span class="state-arrow">➔</span>
|
||
<span class="state-node state-node-after" :class="stateToneClass(change.after)">
|
||
{{ change.after }}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</details>
|
||
|
||
<div v-if="item.attachment_urls?.length" class="process-attachments">
|
||
<el-image
|
||
v-for="url in item.attachment_urls"
|
||
:key="url"
|
||
:src="url"
|
||
:preview-src-list="item.attachment_urls"
|
||
preview-teleported
|
||
fit="cover"
|
||
/>
|
||
</div>
|
||
</article>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.process-timeline {
|
||
display: grid;
|
||
gap: 14px;
|
||
padding-left: 6px;
|
||
position: relative;
|
||
}
|
||
|
||
.process-item {
|
||
position: relative;
|
||
padding: 16px 20px;
|
||
border: 1px solid #e2e8f0;
|
||
border-radius: 12px;
|
||
background: #ffffff;
|
||
box-shadow: 0 1px 3px 0 rgba(15, 23, 42, 0.04);
|
||
transition:
|
||
box-shadow 0.2s ease,
|
||
border-color 0.2s ease;
|
||
}
|
||
|
||
.process-item:hover {
|
||
border-color: #cbd5e1;
|
||
box-shadow: 0 4px 6px -1px rgba(15, 23, 42, 0.06);
|
||
}
|
||
|
||
.process-item::before {
|
||
content: '';
|
||
position: absolute;
|
||
top: -15px;
|
||
bottom: calc(100% - 1px);
|
||
left: -2px;
|
||
width: 2px;
|
||
background: #e2e8f0;
|
||
}
|
||
|
||
.process-item:first-child::before {
|
||
display: none;
|
||
}
|
||
|
||
.process-dot {
|
||
position: absolute;
|
||
left: -7px;
|
||
top: 22px;
|
||
width: 12px;
|
||
height: 12px;
|
||
border: 2px solid #ffffff;
|
||
border-radius: 50%;
|
||
background: #ff6a00;
|
||
box-shadow: 0 0 0 3px #fed7aa;
|
||
}
|
||
|
||
.dot-checkout {
|
||
background: #10b981;
|
||
box-shadow: 0 0 0 3px #d1fae5;
|
||
}
|
||
|
||
.dot-handoff {
|
||
background: #3b82f6;
|
||
box-shadow: 0 0 0 3px #dbeafe;
|
||
}
|
||
|
||
.dot-settlement {
|
||
background: #8b5cf6;
|
||
box-shadow: 0 0 0 3px #ede9fe;
|
||
}
|
||
|
||
.dot-dispute {
|
||
background: #f43f5e;
|
||
box-shadow: 0 0 0 3px #ffe4e6;
|
||
}
|
||
|
||
.dot-pickup {
|
||
background: #f59e0b;
|
||
box-shadow: 0 0 0 3px #fef3c7;
|
||
}
|
||
|
||
.process-heading {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 16px;
|
||
}
|
||
|
||
.heading-main {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
min-width: 0;
|
||
}
|
||
|
||
.action-title {
|
||
color: #0f172a;
|
||
font-size: 15px;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.stage-badge {
|
||
flex: none;
|
||
padding: 3px 8px;
|
||
border-radius: 6px;
|
||
font-size: 12px;
|
||
font-weight: 700;
|
||
border: 1px solid transparent;
|
||
}
|
||
|
||
.stage-checkout {
|
||
background: #ecfdf5;
|
||
color: #047857;
|
||
border-color: #a7f3d0;
|
||
}
|
||
|
||
.stage-handoff {
|
||
background: #eff6ff;
|
||
color: #1d4ed8;
|
||
border-color: #bfdbfe;
|
||
}
|
||
|
||
.stage-settlement {
|
||
background: #f5f3ff;
|
||
color: #6d28d9;
|
||
border-color: #ddd6fe;
|
||
}
|
||
|
||
.stage-dispute {
|
||
background: #fff1f2;
|
||
color: #be123c;
|
||
border-color: #fecaca;
|
||
}
|
||
|
||
.stage-pickup {
|
||
background: #fffbeb;
|
||
color: #b45309;
|
||
border-color: #fde68a;
|
||
}
|
||
|
||
.process-time {
|
||
flex: none;
|
||
color: #94a3b8;
|
||
font-size: 12px;
|
||
white-space: nowrap;
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
|
||
.process-people {
|
||
display: flex;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
gap: 8px 12px;
|
||
margin-top: 8px;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.actor-badge {
|
||
padding: 2px 7px;
|
||
border-radius: 4px;
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.actor-admin {
|
||
background: #fffbeb;
|
||
color: #92400e;
|
||
border: 1px solid #fde68a;
|
||
}
|
||
|
||
.actor-system {
|
||
background: #e0f2fe;
|
||
color: #0369a1;
|
||
border: 1px solid #bae6fd;
|
||
}
|
||
|
||
.actor-user {
|
||
background: #f1f5f9;
|
||
color: #475569;
|
||
border: 1px solid #e2e8f0;
|
||
}
|
||
|
||
.actor-name {
|
||
font-weight: 600;
|
||
color: #334155;
|
||
}
|
||
|
||
.target-text {
|
||
color: #64748b;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
}
|
||
|
||
.target-arrow {
|
||
color: #94a3b8;
|
||
}
|
||
|
||
.process-content-card {
|
||
margin-top: 10px;
|
||
padding: 10px 14px;
|
||
border-radius: 8px;
|
||
background: #f8fafc;
|
||
border: 1px solid #e2e8f0;
|
||
border-left: 3px solid #3b82f6;
|
||
}
|
||
|
||
.content-text {
|
||
color: #334155;
|
||
font-size: 13px;
|
||
line-height: 1.65;
|
||
white-space: pre-wrap;
|
||
}
|
||
|
||
.process-reason-card {
|
||
margin-top: 8px;
|
||
padding: 8px 12px;
|
||
border-radius: 8px;
|
||
background: #fffbeb;
|
||
border: 1px solid #fef3c7;
|
||
border-left: 3px solid #f59e0b;
|
||
color: #92400e;
|
||
font-size: 12px;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.reason-tag {
|
||
font-weight: 700;
|
||
margin-right: 8px;
|
||
}
|
||
|
||
.process-summary-grid {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
margin-top: 12px;
|
||
}
|
||
|
||
.summary-pill {
|
||
display: inline-flex;
|
||
align-items: baseline;
|
||
gap: 6px;
|
||
padding: 6px 12px;
|
||
border-radius: 8px;
|
||
font-size: 12px;
|
||
border: 1px solid transparent;
|
||
}
|
||
|
||
.summary-pill-income {
|
||
background: #ecfdf5;
|
||
border-color: #a7f3d0;
|
||
color: #065f46;
|
||
}
|
||
|
||
.summary-pill-income .summary-val {
|
||
color: #047857;
|
||
font-weight: 700;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.summary-pill-refund {
|
||
background: #eff6ff;
|
||
border-color: #bfdbfe;
|
||
color: #1e40af;
|
||
}
|
||
|
||
.summary-pill-refund .summary-val {
|
||
color: #1d4ed8;
|
||
font-weight: 700;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.summary-pill-fee {
|
||
background: #f5f3ff;
|
||
border-color: #ddd6fe;
|
||
color: #5b21b6;
|
||
}
|
||
|
||
.summary-pill-fee .summary-val {
|
||
color: #6d28d9;
|
||
font-weight: 700;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.summary-pill-deduct {
|
||
background: #fff7ed;
|
||
border-color: #fed7aa;
|
||
color: #9a3412;
|
||
}
|
||
|
||
.summary-pill-deduct .summary-val {
|
||
color: #c2410c;
|
||
font-weight: 700;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.summary-pill-primary {
|
||
background: #f1f5f9;
|
||
border-color: #cbd5e1;
|
||
color: #334155;
|
||
}
|
||
|
||
.summary-pill-primary .summary-val {
|
||
color: #0f172a;
|
||
font-weight: 700;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.summary-pill-neutral {
|
||
background: #f8fafc;
|
||
border-color: #e2e8f0;
|
||
color: #64748b;
|
||
}
|
||
|
||
.summary-pill-neutral .summary-val {
|
||
color: #1e293b;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.summary-label {
|
||
font-weight: 500;
|
||
}
|
||
|
||
.summary-val {
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
|
||
.process-details {
|
||
margin-top: 12px;
|
||
}
|
||
|
||
.process-details-summary {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 8px 12px;
|
||
border: 1px solid #e2e8f0;
|
||
border-radius: 8px;
|
||
background: #f8fafc;
|
||
cursor: pointer;
|
||
user-select: none;
|
||
transition:
|
||
background-color 0.2s ease,
|
||
border-color 0.2s ease;
|
||
list-style: none;
|
||
}
|
||
|
||
.process-details-summary::-webkit-details-marker {
|
||
display: none;
|
||
}
|
||
|
||
.process-details-summary:hover {
|
||
background: #f1f5f9;
|
||
border-color: #cbd5e1;
|
||
}
|
||
|
||
.toggle-left {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.toggle-arrow {
|
||
display: inline-block;
|
||
font-size: 12px;
|
||
color: #2563eb;
|
||
transition: transform 0.2s ease;
|
||
transform: rotate(-90deg);
|
||
}
|
||
|
||
.process-details[open] .toggle-arrow {
|
||
transform: rotate(0deg);
|
||
}
|
||
|
||
.toggle-title {
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
color: #2563eb;
|
||
}
|
||
|
||
.toggle-badge {
|
||
font-size: 12px;
|
||
color: #64748b;
|
||
background: #ffffff;
|
||
border: 1px solid #e2e8f0;
|
||
border-radius: 12px;
|
||
padding: 2px 8px;
|
||
}
|
||
|
||
.process-details-body {
|
||
margin-top: 10px;
|
||
padding: 14px;
|
||
border: 1px solid #e2e8f0;
|
||
border-radius: 10px;
|
||
background: #fafbfc;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 14px;
|
||
}
|
||
|
||
.detail-category-block,
|
||
.state-changes-block {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
}
|
||
|
||
.category-header {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
font-size: 12px;
|
||
font-weight: 700;
|
||
color: #334155;
|
||
padding-bottom: 4px;
|
||
border-bottom: 1px dashed #e2e8f0;
|
||
}
|
||
|
||
.category-icon {
|
||
font-size: 13px;
|
||
}
|
||
|
||
.category-count {
|
||
font-size: 11px;
|
||
color: #94a3b8;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.category-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(170px, 1fr));
|
||
gap: 8px 10px;
|
||
}
|
||
|
||
.metric-box {
|
||
background: #ffffff;
|
||
border: 1px solid #e2e8f0;
|
||
border-radius: 6px;
|
||
padding: 8px 12px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 3px;
|
||
transition:
|
||
border-color 0.15s ease,
|
||
box-shadow 0.15s ease;
|
||
}
|
||
|
||
.metric-box:hover {
|
||
border-color: #cbd5e1;
|
||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
|
||
}
|
||
|
||
.metric-label {
|
||
font-size: 12px;
|
||
color: #64748b;
|
||
}
|
||
|
||
.metric-value {
|
||
font-size: 14px;
|
||
font-weight: 700;
|
||
color: #0f172a;
|
||
font-variant-numeric: tabular-nums;
|
||
word-break: break-word;
|
||
}
|
||
|
||
.metric-box-income .metric-value {
|
||
color: #059669;
|
||
}
|
||
|
||
.metric-box-refund .metric-value {
|
||
color: #2563eb;
|
||
}
|
||
|
||
.metric-box-fee .metric-value {
|
||
color: #7c3aed;
|
||
}
|
||
|
||
.metric-box-deduct .metric-value {
|
||
color: #c2410c;
|
||
}
|
||
|
||
.metric-box-primary .metric-value {
|
||
color: #0f172a;
|
||
}
|
||
|
||
.state-flow-list {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px 12px;
|
||
}
|
||
|
||
.state-flow-item {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
padding: 5px 10px;
|
||
background: #ffffff;
|
||
border: 1px solid #e2e8f0;
|
||
border-radius: 6px;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.state-field-badge {
|
||
font-weight: 600;
|
||
color: #475569;
|
||
background: #f1f5f9;
|
||
padding: 1px 6px;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.state-node {
|
||
padding: 1px 7px;
|
||
border-radius: 4px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.state-node-before {
|
||
background: #f8fafc;
|
||
color: #64748b;
|
||
border: 1px solid #e2e8f0;
|
||
}
|
||
|
||
.state-arrow {
|
||
color: #3b82f6;
|
||
font-size: 10px;
|
||
}
|
||
|
||
.state-node-after {
|
||
font-weight: 600;
|
||
}
|
||
|
||
.tone-success {
|
||
background: #ecfdf5;
|
||
color: #047857;
|
||
border: 1px solid #a7f3d0;
|
||
}
|
||
|
||
.tone-primary {
|
||
background: #eff6ff;
|
||
color: #1d4ed8;
|
||
border: 1px solid #bfdbfe;
|
||
}
|
||
|
||
.tone-warning {
|
||
background: #fffbeb;
|
||
color: #b45309;
|
||
border: 1px solid #fde68a;
|
||
}
|
||
|
||
.tone-danger {
|
||
background: #fef2f2;
|
||
color: #b91c1c;
|
||
border: 1px solid #fecaca;
|
||
}
|
||
|
||
.tone-neutral {
|
||
background: #f8fafc;
|
||
color: #475569;
|
||
border: 1px solid #e2e8f0;
|
||
}
|
||
|
||
.process-attachments {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
margin-top: 10px;
|
||
}
|
||
|
||
.process-attachments :deep(.el-image) {
|
||
width: 76px;
|
||
height: 76px;
|
||
border: 1px solid #e2e8f0;
|
||
border-radius: 6px;
|
||
}
|
||
|
||
@media (max-width: 700px) {
|
||
.process-item {
|
||
padding: 14px 14px 14px 16px;
|
||
}
|
||
.process-heading {
|
||
align-items: flex-start;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
}
|
||
.process-summary-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
}
|
||
.category-grid {
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
}
|
||
}
|
||
</style>
|