优化订单交接和结账界面
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
import { computed, nextTick, onMounted, ref, watch } from 'vue'
|
import { computed, nextTick, onMounted, ref, watch } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { ArrowLeft, Close, Loading, Picture } from '@element-plus/icons-vue'
|
import { ArrowLeft, Close, DocumentChecked, Loading, Picture } from '@element-plus/icons-vue'
|
||||||
import {
|
import {
|
||||||
fetchChat,
|
fetchChat,
|
||||||
fetchChatMessages,
|
fetchChatMessages,
|
||||||
@@ -33,6 +33,9 @@ const fileInputRef = ref<HTMLInputElement | null>(null)
|
|||||||
|
|
||||||
const conversationID = computed(() => Number(route.params.id || 0))
|
const conversationID = computed(() => Number(route.params.id || 0))
|
||||||
const canSend = computed(() => content.value.trim() !== '' || attachments.value.length > 0)
|
const canSend = computed(() => content.value.trim() !== '' || attachments.value.length > 0)
|
||||||
|
const activeOrderId = computed(
|
||||||
|
() => conversation.value?.order_id || conversation.value?.latest_order_id || null
|
||||||
|
)
|
||||||
const memberText = computed(() => {
|
const memberText = computed(() => {
|
||||||
const participants = conversation.value?.participants || []
|
const participants = conversation.value?.participants || []
|
||||||
if (participants.length === 0)
|
if (participants.length === 0)
|
||||||
@@ -252,6 +255,11 @@ function handleKeydown(e: KeyboardEvent) {
|
|||||||
handleSend()
|
handleSend()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function goOrderHandoff() {
|
||||||
|
if (!activeOrderId.value) return
|
||||||
|
router.push({ path: `/orders/${activeOrderId.value}`, query: { focus: 'handoff' } })
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -267,15 +275,12 @@ function handleKeydown(e: KeyboardEvent) {
|
|||||||
<h2>{{ conversation?.title || '客服会话' }}</h2>
|
<h2>{{ conversation?.title || '客服会话' }}</h2>
|
||||||
<p>{{ memberText }}</p>
|
<p>{{ memberText }}</p>
|
||||||
</div>
|
</div>
|
||||||
<el-button
|
<div v-if="activeOrderId" class="chat-header-actions">
|
||||||
v-if="conversation?.order_id"
|
<el-button type="primary" :icon="DocumentChecked" @click="goOrderHandoff">
|
||||||
type="primary"
|
订单交接
|
||||||
link
|
|
||||||
@click="router.push(`/orders/${conversation.order_id}`)"
|
|
||||||
>
|
|
||||||
查看订单
|
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Messages -->
|
<!-- Messages -->
|
||||||
<div ref="listRef" v-loading="loading" class="message-list">
|
<div ref="listRef" v-loading="loading" class="message-list">
|
||||||
@@ -450,6 +455,14 @@ function handleKeydown(e: KeyboardEvent) {
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chat-header-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-left: auto;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.message-list {
|
.message-list {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
|||||||
@@ -34,65 +34,100 @@ const props = withDefaults(
|
|||||||
<h2>交接记录</h2>
|
<h2>交接记录</h2>
|
||||||
<span>商品编号 {{ props.listingCode }}</span>
|
<span>商品编号 {{ props.listingCode }}</span>
|
||||||
</div>
|
</div>
|
||||||
<el-empty v-if="props.records.length === 0" description="暂无交接记录" />
|
<div class="timeline-card">
|
||||||
<el-timeline v-else>
|
<div v-if="props.records.length === 0" class="empty-log">暂无交接记录</div>
|
||||||
<el-timeline-item
|
<div v-else class="compact-timeline">
|
||||||
v-for="record in props.records"
|
<div v-for="record in props.records" :key="record.id" class="timeline-row">
|
||||||
:key="record.id"
|
<span class="timeline-dot"></span>
|
||||||
:timestamp="formatDateTime(record.created_at)"
|
<span class="timeline-time">{{ formatDateTime(record.created_at) }}</span>
|
||||||
placement="top"
|
<strong class="timeline-title">{{ formatHandoffRecordType(record.type) }}</strong>
|
||||||
>
|
<p class="timeline-body">{{ record.content }}</p>
|
||||||
<div class="timeline-content">
|
</div>
|
||||||
<div class="timeline-title">{{ formatHandoffRecordType(record.type) }}</div>
|
</div>
|
||||||
<div class="timeline-body">{{ record.content }}</div>
|
|
||||||
</div>
|
</div>
|
||||||
</el-timeline-item>
|
|
||||||
</el-timeline>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.timeline-section {
|
.timeline-section {
|
||||||
margin-bottom: 32px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-header {
|
.section-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-header h2 {
|
.section-header h2 {
|
||||||
font-size: 20px;
|
font-size: 18px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #1f2937;
|
color: #1f2937;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.timeline-section :deep(.el-timeline) {
|
.timeline-card {
|
||||||
padding: 24px;
|
padding: 12px;
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
border: 1px solid #e5e7eb;
|
border: 1px solid #e5e7eb;
|
||||||
border-radius: 12px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.timeline-content {
|
.empty-log {
|
||||||
|
padding: 14px;
|
||||||
|
color: #8a94a6;
|
||||||
|
font-size: 13px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.compact-timeline {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.timeline-row {
|
||||||
|
position: relative;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 150px 92px minmax(0, 1fr);
|
||||||
|
gap: 10px;
|
||||||
|
align-items: start;
|
||||||
|
padding: 10px 12px 10px 22px;
|
||||||
|
border: 1px solid #eef1f5;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #fbfcfe;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-dot {
|
||||||
|
position: absolute;
|
||||||
|
left: 10px;
|
||||||
|
top: 15px;
|
||||||
|
width: 7px;
|
||||||
|
height: 7px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #ff6a00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-time {
|
||||||
|
color: #8a94a6;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
.timeline-title {
|
.timeline-title {
|
||||||
font-size: 15px;
|
font-size: 13px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #1f2937;
|
color: #1f2937;
|
||||||
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.timeline-body {
|
.timeline-body {
|
||||||
font-size: 14px;
|
margin: 0;
|
||||||
|
font-size: 13px;
|
||||||
color: #64748b;
|
color: #64748b;
|
||||||
line-height: 1.6;
|
line-height: 1.55;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-section {
|
.card-section {
|
||||||
|
|||||||
@@ -161,14 +161,15 @@ function updateNumberField(field: 'coin_consumed_m' | 'otherAmountYuan', event:
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div v-else class="form-section checkout-form-section">
|
<div v-else class="form-section checkout-form-section">
|
||||||
<div class="section-header">
|
<div class="section-header compact">
|
||||||
<h2>发起结账</h2>
|
<h2>当前待办:发起结账</h2>
|
||||||
|
<span>使用完毕后填写租后状态和消耗明细</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-card">
|
<div class="form-card checkout-task-card">
|
||||||
<el-input
|
<el-input
|
||||||
:model-value="props.checkoutForm.content"
|
:model-value="props.checkoutForm.content"
|
||||||
type="textarea"
|
type="textarea"
|
||||||
:rows="4"
|
:rows="3"
|
||||||
placeholder="填写结账说明、租后资产状态或注意事项"
|
placeholder="填写结账说明、租后资产状态或注意事项"
|
||||||
@update:model-value="updateCheckoutForm({ content: String($event) })"
|
@update:model-value="updateCheckoutForm({ content: String($event) })"
|
||||||
/>
|
/>
|
||||||
@@ -177,7 +178,9 @@ function updateNumberField(field: 'coin_consumed_m' | 'otherAmountYuan', event:
|
|||||||
<strong>额外消耗品</strong>
|
<strong>额外消耗品</strong>
|
||||||
<span class="amount-highlight">已用金额:¥{{ money(props.resourceChargeAmount) }}</span>
|
<span class="amount-highlight">已用金额:¥{{ money(props.resourceChargeAmount) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<el-empty v-if="props.resources.length === 0" description="订单快照中暂无额外消耗品" />
|
<div v-if="props.resources.length === 0" class="checkout-resource-empty">
|
||||||
|
订单快照中暂无额外消耗品
|
||||||
|
</div>
|
||||||
<div v-for="item in props.resources" v-else :key="item.key" class="checkout-resource-row">
|
<div v-for="item in props.resources" v-else :key="item.key" class="checkout-resource-row">
|
||||||
<div class="checkout-resource-meta">
|
<div class="checkout-resource-meta">
|
||||||
<strong>{{ item.label }}</strong>
|
<strong>{{ item.label }}</strong>
|
||||||
@@ -194,36 +197,52 @@ function updateNumberField(field: 'coin_consumed_m' | 'otherAmountYuan', event:
|
|||||||
<span class="checkout-resource-amount">¥{{ money(props.resourceLineAmount(item)) }}</span>
|
<span class="checkout-resource-amount">¥{{ money(props.resourceLineAmount(item)) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<el-form class="form-grid" label-position="top">
|
<div class="checkout-number-grid">
|
||||||
<el-form-item label="消耗哈夫币(M)">
|
<div class="checkout-number-card coin">
|
||||||
|
<div class="checkout-number-copy">
|
||||||
|
<div class="checkout-number-title">
|
||||||
|
<strong>哈夫币消耗</strong>
|
||||||
|
<span>默认全部使用完成</span>
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
快照 {{ quantity(props.snapshotHafCoinM) }}M,预计剩余
|
||||||
|
{{ quantity(props.remainingHafCoinM) }}M
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="checkout-number-control">
|
||||||
<el-input-number
|
<el-input-number
|
||||||
:model-value="props.checkoutForm.coin_consumed_m"
|
:model-value="props.checkoutForm.coin_consumed_m"
|
||||||
class="full-control"
|
class="checkout-number-input"
|
||||||
:min="0"
|
:min="0"
|
||||||
:max="props.snapshotHafCoinM"
|
:max="props.snapshotHafCoinM"
|
||||||
:precision="2"
|
:precision="2"
|
||||||
controls-position="right"
|
controls-position="right"
|
||||||
@update:model-value="updateCheckoutForm({ coin_consumed_m: Number($event) || 0 })"
|
@update:model-value="updateCheckoutForm({ coin_consumed_m: Number($event) || 0 })"
|
||||||
/>
|
/>
|
||||||
<span class="field-hint">
|
<span>M</span>
|
||||||
订单快照 {{ quantity(props.snapshotHafCoinM) }}M,预计剩余
|
</div>
|
||||||
{{ quantity(props.remainingHafCoinM) }}M
|
</div>
|
||||||
</span>
|
<div class="checkout-number-card deposit">
|
||||||
</el-form-item>
|
<div class="checkout-number-copy">
|
||||||
<el-form-item label="其他押金赔付(元)">
|
<div class="checkout-number-title">
|
||||||
|
<strong>其他押金赔付</strong>
|
||||||
|
<span>仅押金扣除项</span>
|
||||||
|
</div>
|
||||||
|
<p>封禁、违规、资产损坏等需要从押金赔付的费用,不含额外消耗品。</p>
|
||||||
|
</div>
|
||||||
|
<div class="checkout-number-control">
|
||||||
<el-input-number
|
<el-input-number
|
||||||
:model-value="props.checkoutForm.otherAmountYuan"
|
:model-value="props.checkoutForm.otherAmountYuan"
|
||||||
class="full-control"
|
class="checkout-number-input"
|
||||||
:min="0"
|
:min="0"
|
||||||
:precision="0"
|
:precision="0"
|
||||||
controls-position="right"
|
controls-position="right"
|
||||||
@update:model-value="updateCheckoutForm({ otherAmountYuan: Number($event) || 0 })"
|
@update:model-value="updateCheckoutForm({ otherAmountYuan: Number($event) || 0 })"
|
||||||
/>
|
/>
|
||||||
<span class="field-hint">
|
<span>元</span>
|
||||||
不含上方额外消耗品;仅填写封禁、违规、资产损坏等需要从押金赔付的费用。
|
</div>
|
||||||
</span>
|
</div>
|
||||||
</el-form-item>
|
</div>
|
||||||
</el-form>
|
|
||||||
<div class="checkout-deduct-preview">
|
<div class="checkout-deduct-preview">
|
||||||
<div>
|
<div>
|
||||||
<span>额外消耗品已用</span>
|
<span>额外消耗品已用</span>
|
||||||
@@ -239,12 +258,12 @@ function updateNumberField(field: 'coin_consumed_m' | 'otherAmountYuan', event:
|
|||||||
<el-input
|
<el-input
|
||||||
:model-value="props.checkoutForm.evidenceText"
|
:model-value="props.checkoutForm.evidenceText"
|
||||||
type="textarea"
|
type="textarea"
|
||||||
:rows="3"
|
:rows="2"
|
||||||
placeholder="结账证据链接,一行一个。可填写截图地址或备注链接"
|
placeholder="结账证据链接,一行一个。可填写截图地址或备注链接"
|
||||||
@update:model-value="updateCheckoutForm({ evidenceText: String($event) })"
|
@update:model-value="updateCheckoutForm({ evidenceText: String($event) })"
|
||||||
/>
|
/>
|
||||||
<el-button type="primary" size="large" :loading="props.returning" @click="emit('submit')">
|
<el-button type="primary" size="large" :loading="props.returning" @click="emit('submit')">
|
||||||
发起结账
|
提交结账
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -252,18 +271,23 @@ function updateNumberField(field: 'coin_consumed_m' | 'otherAmountYuan', event:
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.form-section {
|
.form-section {
|
||||||
margin-bottom: 32px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-header {
|
.section-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header.compact span {
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-header h2 {
|
.section-header h2 {
|
||||||
font-size: 20px;
|
font-size: 18px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #1f2937;
|
color: #1f2937;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -271,17 +295,22 @@ function updateNumberField(field: 'coin_consumed_m' | 'otherAmountYuan', event:
|
|||||||
|
|
||||||
.form-card {
|
.form-card {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 16px;
|
gap: 12px;
|
||||||
padding: 24px;
|
padding: 16px;
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
border: 1px solid #e5e7eb;
|
border: 1px solid #e5e7eb;
|
||||||
border-radius: 12px;
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkout-task-card {
|
||||||
|
border-color: #bfdbfe;
|
||||||
|
background: linear-gradient(180deg, #f8fbff 0%, #eff6ff 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.checkout-resource-panel {
|
.checkout-resource-panel {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 12px;
|
gap: 10px;
|
||||||
padding: 16px;
|
padding: 12px;
|
||||||
background: #f9fafb;
|
background: #f9fafb;
|
||||||
border: 1px solid #e5e7eb;
|
border: 1px solid #e5e7eb;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
@@ -290,10 +319,10 @@ function updateNumberField(field: 'coin_consumed_m' | 'otherAmountYuan', event:
|
|||||||
.checkout-resource-head {
|
.checkout-resource-head {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr auto;
|
grid-template-columns: 1fr auto;
|
||||||
gap: 12px;
|
gap: 10px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding-bottom: 12px;
|
padding-bottom: 10px;
|
||||||
border-bottom: 2px solid #e5e7eb;
|
border-bottom: 1px solid #e5e7eb;
|
||||||
}
|
}
|
||||||
|
|
||||||
.checkout-resource-head strong {
|
.checkout-resource-head strong {
|
||||||
@@ -304,15 +333,25 @@ function updateNumberField(field: 'coin_consumed_m' | 'otherAmountYuan', event:
|
|||||||
.amount-highlight {
|
.amount-highlight {
|
||||||
color: #ff6a00;
|
color: #ff6a00;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
font-size: 14px;
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkout-resource-empty {
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px dashed #d8dee8;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #ffffff;
|
||||||
|
color: #8a94a6;
|
||||||
|
font-size: 13px;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.checkout-resource-row {
|
.checkout-resource-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr auto auto;
|
grid-template-columns: 1fr auto auto;
|
||||||
gap: 12px;
|
gap: 10px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 12px;
|
padding: 10px;
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
border: 1px solid #e5e7eb;
|
border: 1px solid #e5e7eb;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
@@ -334,17 +373,102 @@ function updateNumberField(field: 'coin_consumed_m' | 'otherAmountYuan', event:
|
|||||||
}
|
}
|
||||||
|
|
||||||
.checkout-resource-amount {
|
.checkout-resource-amount {
|
||||||
min-width: 80px;
|
min-width: 72px;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
color: #ff6a00;
|
color: #ff6a00;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
font-size: 16px;
|
font-size: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-grid {
|
.form-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
|
||||||
gap: 16px;
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkout-number-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkout-number-card {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 106px;
|
||||||
|
padding: 14px;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkout-number-card.coin {
|
||||||
|
border-color: #bfdbfe;
|
||||||
|
background: #f8fbff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkout-number-card.deposit {
|
||||||
|
border-color: #fed7aa;
|
||||||
|
background: #fffaf3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkout-number-copy {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkout-number-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkout-number-title strong {
|
||||||
|
color: #1f2937;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkout-number-title span {
|
||||||
|
padding: 2px 8px;
|
||||||
|
color: #2563eb;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
background: #dbeafe;
|
||||||
|
border-radius: 999px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkout-number-card.deposit .checkout-number-title span {
|
||||||
|
color: #c2410c;
|
||||||
|
background: #ffedd5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkout-number-copy p {
|
||||||
|
margin: 0;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkout-number-control {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 170px 24px;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkout-number-input {
|
||||||
|
width: 170px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkout-number-control span {
|
||||||
|
color: #475569;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.field-hint {
|
.field-hint {
|
||||||
@@ -357,7 +481,7 @@ function updateNumberField(field: 'coin_consumed_m' | 'otherAmountYuan', event:
|
|||||||
.checkout-deduct-preview,
|
.checkout-deduct-preview,
|
||||||
.checkout-fee-preview {
|
.checkout-fee-preview {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 12px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.checkout-deduct-preview {
|
.checkout-deduct-preview {
|
||||||
@@ -367,8 +491,8 @@ function updateNumberField(field: 'coin_consumed_m' | 'otherAmountYuan', event:
|
|||||||
.checkout-deduct-preview div,
|
.checkout-deduct-preview div,
|
||||||
.checkout-fee-preview div {
|
.checkout-fee-preview div {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 6px;
|
gap: 4px;
|
||||||
padding: 12px;
|
padding: 10px 12px;
|
||||||
border: 1px solid #e5e7eb;
|
border: 1px solid #e5e7eb;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background: #f9fafb;
|
background: #f9fafb;
|
||||||
@@ -377,14 +501,14 @@ function updateNumberField(field: 'coin_consumed_m' | 'otherAmountYuan', event:
|
|||||||
.checkout-deduct-preview span,
|
.checkout-deduct-preview span,
|
||||||
.checkout-fee-preview span {
|
.checkout-fee-preview span {
|
||||||
color: #4b5563;
|
color: #4b5563;
|
||||||
font-size: 13px;
|
font-size: 12px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.checkout-deduct-preview strong,
|
.checkout-deduct-preview strong,
|
||||||
.checkout-fee-preview strong {
|
.checkout-fee-preview strong {
|
||||||
color: #ff6a00;
|
color: #ff6a00;
|
||||||
font-size: 18px;
|
font-size: 16px;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -543,5 +667,18 @@ function updateNumberField(field: 'coin_consumed_m' | 'otherAmountYuan', event:
|
|||||||
.checkout-deduct-preview {
|
.checkout-deduct-preview {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.checkout-number-grid,
|
||||||
|
.checkout-number-card {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkout-number-control {
|
||||||
|
grid-template-columns: minmax(0, 1fr) 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkout-number-input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -48,9 +48,9 @@ export interface UseOrderActionsOptions {
|
|||||||
notifyError: (message: string) => void
|
notifyError: (message: string) => void
|
||||||
/** 警告提示,用于表单校验未通过等非致命场景。 */
|
/** 警告提示,用于表单校验未通过等非致命场景。 */
|
||||||
notifyWarning: (message: string) => void
|
notifyWarning: (message: string) => void
|
||||||
/** 二次确认。返回 true 表示用户确认继续,false 表示取消。PC 注入直接 resolve(true),Mobile 注入 showDialog。 */
|
/** 二次确认。返回 true 表示用户确认继续,false 表示取消。 */
|
||||||
confirm: (opts: { title: string; message: string }) => Promise<boolean>
|
confirm: (opts: { title: string; message: string }) => Promise<boolean>
|
||||||
/** 需要走二次确认的 handler 名集合。Mobile 传 ['cancel','confirmCheckout','acceptCheckout','cancelDispute'],PC 传 []。 */
|
/** 需要走二次确认的 handler 名集合。 */
|
||||||
actionsNeedingConfirm: readonly string[]
|
actionsNeedingConfirm: readonly string[]
|
||||||
/** 支付前选择支付方式。返回 null 表示用户取消支付。 */
|
/** 支付前选择支付方式。返回 null 表示用户取消支付。 */
|
||||||
selectPayWay: () => Promise<PaymentPayWay | null>
|
selectPayWay: () => Promise<PaymentPayWay | null>
|
||||||
@@ -78,6 +78,7 @@ export const CONFIRMABLE_ACTIONS = [
|
|||||||
'confirmCheckout',
|
'confirmCheckout',
|
||||||
'acceptCheckout',
|
'acceptCheckout',
|
||||||
'cancelDispute',
|
'cancelDispute',
|
||||||
|
'submitCheckout',
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
export interface CheckoutFormState {
|
export interface CheckoutFormState {
|
||||||
@@ -152,6 +153,7 @@ export function useOrderActions(options: UseOrderActionsOptions) {
|
|||||||
const disputeType = ref('cannot_login')
|
const disputeType = ref('cannot_login')
|
||||||
const disputeDescription = ref('')
|
const disputeDescription = ref('')
|
||||||
const disputeEvidenceText = ref('')
|
const disputeEvidenceText = ref('')
|
||||||
|
let checkoutDefaultsHydratedOrderID: number | null = null
|
||||||
|
|
||||||
// 结账快照能力(资源金额、内容摘要、资源使用量规范化)
|
// 结账快照能力(资源金额、内容摘要、资源使用量规范化)
|
||||||
const {
|
const {
|
||||||
@@ -217,12 +219,28 @@ export function useOrderActions(options: UseOrderActionsOptions) {
|
|||||||
counterForm.value = hydrateCounterFormFromCheckout(order.value.checkout)
|
counterForm.value = hydrateCounterFormFromCheckout(order.value.checkout)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hydrateCheckoutDefaults() {
|
||||||
|
if (!order.value || !isRenter.value || !['renting', 'overdue'].includes(order.value.status)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (checkoutDefaultsHydratedOrderID === order.value.id) return
|
||||||
|
const defaultCoinConsumedM = snapshotHafCoinM.value
|
||||||
|
if (defaultCoinConsumedM > 0 && Number(checkoutForm.value.coin_consumed_m || 0) <= 0) {
|
||||||
|
checkoutForm.value = {
|
||||||
|
...checkoutForm.value,
|
||||||
|
coin_consumed_m: defaultCoinConsumedM,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
checkoutDefaultsHydratedOrderID = order.value.id
|
||||||
|
}
|
||||||
|
|
||||||
async function loadOrder() {
|
async function loadOrder() {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
order.value = await fetchOrder(String(route.params.id))
|
order.value = await fetchOrder(String(route.params.id))
|
||||||
handoffRecords.value = await fetchHandoffRecords(String(route.params.id))
|
handoffRecords.value = await fetchHandoffRecords(String(route.params.id))
|
||||||
hydrateResourceUsage()
|
hydrateResourceUsage()
|
||||||
|
hydrateCheckoutDefaults()
|
||||||
hydrateCounterForm()
|
hydrateCounterForm()
|
||||||
if (
|
if (
|
||||||
!autoPayHandled &&
|
!autoPayHandled &&
|
||||||
@@ -317,6 +335,14 @@ export function useOrderActions(options: UseOrderActionsOptions) {
|
|||||||
|
|
||||||
async function handleSubmitCheckout() {
|
async function handleSubmitCheckout() {
|
||||||
if (!order.value) return
|
if (!order.value) return
|
||||||
|
if (needsConfirm('submitCheckout')) {
|
||||||
|
const ok = await options.confirm({
|
||||||
|
title: '确认提交结账',
|
||||||
|
message:
|
||||||
|
'结账数据会影响租金、押金赔付和双方结算金额。请确认哈夫币消耗、额外消耗品、押金赔付和证据链接已认真核对,提交后将发送给号主确认。',
|
||||||
|
})
|
||||||
|
if (!ok) return
|
||||||
|
}
|
||||||
returning.value = true
|
returning.value = true
|
||||||
try {
|
try {
|
||||||
const consumableAmount = resourceChargeAmount.value
|
const consumableAmount = resourceChargeAmount.value
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref } from 'vue'
|
import { computed, nextTick, onMounted, ref, watch } from 'vue'
|
||||||
import { ElMessage } from 'element-plus'
|
import { useRoute } from 'vue-router'
|
||||||
import { ChatDotRound, CopyDocument, Loading } from '@element-plus/icons-vue'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { ChatDotRound, CopyDocument, Loading, Service } from '@element-plus/icons-vue'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
amountYuan,
|
amountYuan,
|
||||||
@@ -18,6 +19,8 @@ import { formatDateTime } from '@/shared/utils/time'
|
|||||||
import type { PaymentPayWay } from '@/features/orders/api/orders'
|
import type { PaymentPayWay } from '@/features/orders/api/orders'
|
||||||
|
|
||||||
// 支付收银台:二维码渲染 + 轮询查询。PC 不做 App 浏览器跳转,直接渲染二维码弹窗。
|
// 支付收银台:二维码渲染 + 轮询查询。PC 不做 App 浏览器跳转,直接渲染二维码弹窗。
|
||||||
|
const route = useRoute()
|
||||||
|
const disputeDialogVisible = ref(false)
|
||||||
const cashier = useOrderPaymentCashier({
|
const cashier = useOrderPaymentCashier({
|
||||||
notifySuccess: message => ElMessage.success(message),
|
notifySuccess: message => ElMessage.success(message),
|
||||||
notifyError: message => ElMessage.error(message),
|
notifyError: message => ElMessage.error(message),
|
||||||
@@ -88,12 +91,25 @@ const {
|
|||||||
notifySuccess: message => ElMessage.success(message),
|
notifySuccess: message => ElMessage.success(message),
|
||||||
notifyError: message => ElMessage.error(message),
|
notifyError: message => ElMessage.error(message),
|
||||||
notifyWarning: message => ElMessage.warning(message),
|
notifyWarning: message => ElMessage.warning(message),
|
||||||
confirm: async () => true, // PC 不做二次确认,直接放行。
|
confirm: async ({ title, message }) => {
|
||||||
actionsNeedingConfirm: [],
|
try {
|
||||||
|
await ElMessageBox.confirm(message, title, {
|
||||||
|
confirmButtonText: '确认提交',
|
||||||
|
cancelButtonText: '返回检查',
|
||||||
|
type: 'warning',
|
||||||
|
dangerouslyUseHTMLString: false,
|
||||||
|
})
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actionsNeedingConfirm: ['submitCheckout'],
|
||||||
selectPayWay,
|
selectPayWay,
|
||||||
ordersPath: '/orders',
|
ordersPath: '/orders',
|
||||||
chatPathPrefix: '/messages/',
|
chatPathPrefix: '/messages/',
|
||||||
startCashier: (payment, onPaid) => cashier.openPaymentCashier(payment, onPaid),
|
startCashier: (payment, onPaid) => cashier.openPaymentCashier(payment, onPaid),
|
||||||
|
onCreateDisputeSuccess: closeDisputeDialog,
|
||||||
})
|
})
|
||||||
|
|
||||||
// 支付弹窗别名:保持 template 字段名不变。
|
// 支付弹窗别名:保持 template 字段名不变。
|
||||||
@@ -104,6 +120,34 @@ const qrGenerating = cashier.qrGenerating
|
|||||||
const checkingPayment = cashier.checkingPayment
|
const checkingPayment = cashier.checkingPayment
|
||||||
const paymentPayURL = cashier.payURL
|
const paymentPayURL = cashier.payURL
|
||||||
const activePayWayLabel = computedPayWayLabel(activePayment)
|
const activePayWayLabel = computedPayWayLabel(activePayment)
|
||||||
|
const handoffFocused = ref(false)
|
||||||
|
const hasSidebarActions = computed(() => {
|
||||||
|
if (!order.value) return false
|
||||||
|
return (
|
||||||
|
(isOwner.value && order.value.status === 'pending_checkout_confirm') ||
|
||||||
|
(isRenter.value && order.value.status === 'pending_checkout_accept')
|
||||||
|
)
|
||||||
|
})
|
||||||
|
const disputeSupportTitle = computed(() => {
|
||||||
|
if (canCancelDispute.value) {
|
||||||
|
return order.value?.status === 'checkout_disputing' ? '结账争议处理中' : '申诉处理中'
|
||||||
|
}
|
||||||
|
return isCheckoutDisputeStage.value ? '结账金额有异议?' : '遇到交接问题?'
|
||||||
|
})
|
||||||
|
const disputeSupportMessage = computed(() => {
|
||||||
|
if (canCancelDispute.value) {
|
||||||
|
return '客服正在处理当前争议,如问题已解决可取消申诉。'
|
||||||
|
}
|
||||||
|
return isCheckoutDisputeStage.value
|
||||||
|
? '可提交结账争议,客服会核查双方证据。'
|
||||||
|
: '无法登录、超时交接等情况可申请客服介入。'
|
||||||
|
})
|
||||||
|
const disputeSupportActionText = computed(() => {
|
||||||
|
if (canCancelDispute.value) {
|
||||||
|
return order.value?.status === 'checkout_disputing' ? '取消结账争议' : '取消申诉'
|
||||||
|
}
|
||||||
|
return isCheckoutDisputeStage.value ? '发起结账争议' : '发起申诉'
|
||||||
|
})
|
||||||
|
|
||||||
// selectPayWay 在创建支付单前让用户选择支付宝或微信。
|
// selectPayWay 在创建支付单前让用户选择支付宝或微信。
|
||||||
// 通过自定义对话框展示两个平级渠道卡片,返回 null 表示用户取消。
|
// 通过自定义对话框展示两个平级渠道卡片,返回 null 表示用户取消。
|
||||||
@@ -161,30 +205,35 @@ function getOrderStep(status: string) {
|
|||||||
return stepMap[status] ?? 0
|
return stepMap[status] ?? 0
|
||||||
}
|
}
|
||||||
|
|
||||||
function scrollToDispute() {
|
function openDisputeDialog() {
|
||||||
const disputeSection = document.querySelector('.dispute-section')
|
disputeDialogVisible.value = true
|
||||||
if (disputeSection) {
|
|
||||||
disputeSection.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
function scrollToCheckout() {
|
function closeDisputeDialog() {
|
||||||
const checkoutSection = document.querySelector('.checkout-form-section')
|
disputeDialogVisible.value = false
|
||||||
if (checkoutSection) {
|
|
||||||
checkoutSection.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
function scrollToCounterCheckout() {
|
function scrollToCounterCheckout() {
|
||||||
const counterSection = document.querySelector('.counter-checkout-section')
|
scrollToSection('.counter-checkout-section')
|
||||||
if (counterSection) {
|
|
||||||
counterSection.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
function scrollToRejectCheckout() {
|
function scrollToRejectCheckout() {
|
||||||
const rejectSection = document.querySelector('.reject-checkout-section')
|
scrollToSection('.reject-checkout-section')
|
||||||
if (rejectSection) {
|
}
|
||||||
rejectSection.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
function scrollToHandoff() {
|
||||||
|
scrollToSection('.handoff-workspace')
|
||||||
|
}
|
||||||
|
function scrollToSection(selector: string) {
|
||||||
|
const section = document.querySelector(selector)
|
||||||
|
if (section) {
|
||||||
|
section.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
async function focusHandoffFromQuery() {
|
||||||
|
if (handoffFocused.value || loading.value || !order.value || route.query.focus !== 'handoff') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
handoffFocused.value = true
|
||||||
|
await nextTick()
|
||||||
|
scrollToHandoff()
|
||||||
|
}
|
||||||
|
|
||||||
async function copyListingCode() {
|
async function copyListingCode() {
|
||||||
if (!order.value) return
|
if (!order.value) return
|
||||||
@@ -195,11 +244,19 @@ async function copyListingCode() {
|
|||||||
ElMessage.error('复制失败')
|
ElMessage.error('复制失败')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
void focusHandoffFromQuery()
|
||||||
|
})
|
||||||
|
|
||||||
|
watch([() => route.query.focus, order, loading], () => {
|
||||||
|
void focusHandoffFromQuery()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="page order-detail-page" v-loading="loading">
|
<section class="page order-detail-page" v-loading="loading">
|
||||||
<div v-if="order" class="page-header">
|
<div v-if="order" class="page-header" :class="{ centered: !hasSidebarActions }">
|
||||||
<div class="header-top">
|
<div class="header-top">
|
||||||
<div class="header-info">
|
<div class="header-info">
|
||||||
<p class="eyebrow">订单号:{{ order.order_no }}</p>
|
<p class="eyebrow">订单号:{{ order.order_no }}</p>
|
||||||
@@ -216,7 +273,7 @@ async function copyListingCode() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="order" class="content-layout">
|
<div v-if="order" class="content-layout" :class="{ centered: !hasSidebarActions }">
|
||||||
<div class="main-content">
|
<div class="main-content">
|
||||||
<div v-if="order" class="order-progress-section">
|
<div v-if="order" class="order-progress-section">
|
||||||
<el-steps
|
<el-steps
|
||||||
@@ -251,6 +308,61 @@ async function copyListingCode() {
|
|||||||
</el-steps>
|
</el-steps>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="
|
||||||
|
isOwner &&
|
||||||
|
order.status === 'pending_handoff' &&
|
||||||
|
order.handoff_status === 'pending_owner'
|
||||||
|
"
|
||||||
|
class="form-section handoff-action-section"
|
||||||
|
>
|
||||||
|
<div class="section-header compact">
|
||||||
|
<h2>当前待办:提交交接说明</h2>
|
||||||
|
<span>填写后租客即可确认收号</span>
|
||||||
|
</div>
|
||||||
|
<div class="handoff-action-card">
|
||||||
|
<el-input
|
||||||
|
v-model="handoffContent"
|
||||||
|
type="textarea"
|
||||||
|
:rows="4"
|
||||||
|
placeholder="填写登录方式、注意事项和交接说明"
|
||||||
|
/>
|
||||||
|
<el-button type="primary" :loading="handoffing" @click="handleSubmitHandoff">
|
||||||
|
提交交接
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="
|
||||||
|
isRenter &&
|
||||||
|
order.status === 'pending_handoff' &&
|
||||||
|
order.handoff_status === 'pending_renter_confirm'
|
||||||
|
"
|
||||||
|
class="handoff-confirm-strip"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<strong>当前待办:确认收号</strong>
|
||||||
|
<span>确认账号可以正常登录后,订单会进入使用中并重新计算预计截止时间。</span>
|
||||||
|
</div>
|
||||||
|
<el-button type="primary" :loading="confirming" @click="handleConfirmReceive">
|
||||||
|
确认已收到账号
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<OrderResourceUsageEditor
|
||||||
|
v-if="order && isRenter && ['renting', 'overdue'].includes(order.status)"
|
||||||
|
v-model:checkout-form="checkoutForm"
|
||||||
|
v-model:resource-usage="resourceUsage"
|
||||||
|
:resources="checkoutResources"
|
||||||
|
:resource-charge-amount="resourceChargeAmount"
|
||||||
|
:snapshot-haf-coin-m="snapshotHafCoinM"
|
||||||
|
:remaining-haf-coin-m="remainingHafCoinM"
|
||||||
|
:returning="returning"
|
||||||
|
:resource-line-amount="resourceLineAmount"
|
||||||
|
@submit="handleSubmitCheckout"
|
||||||
|
/>
|
||||||
|
|
||||||
<div v-if="order" class="detail-grid">
|
<div v-if="order" class="detail-grid">
|
||||||
<div class="metric-card primary">
|
<div class="metric-card primary">
|
||||||
<span class="metric-label">订单状态</span>
|
<span class="metric-label">订单状态</span>
|
||||||
@@ -309,29 +421,43 @@ async function copyListingCode() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="order.status === 'pending_handoff'" class="action-card info">
|
<div
|
||||||
<div class="action-buttons">
|
v-if="order.status === 'pending_handoff' || canOpenDispute || canCancelDispute"
|
||||||
|
class="secondary-actions"
|
||||||
|
>
|
||||||
|
<div v-if="order.status === 'pending_handoff'" class="secondary-action-card danger">
|
||||||
|
<div>
|
||||||
|
<strong>不继续交接?</strong>
|
||||||
|
<span>取消后将释放账号,订单会回到可租状态。</span>
|
||||||
|
</div>
|
||||||
<el-button type="danger" plain :loading="cancelling" @click="handleCancel">
|
<el-button type="danger" plain :loading="cancelling" @click="handleCancel">
|
||||||
取消订单并释放账号
|
取消订单
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="canOpenDispute || canCancelDispute" class="secondary-action-card support">
|
||||||
|
<span class="support-dispute-icon">
|
||||||
|
<el-icon><Service /></el-icon>
|
||||||
|
</span>
|
||||||
|
<div class="support-dispute-copy">
|
||||||
|
<strong>{{ disputeSupportTitle }}</strong>
|
||||||
|
<span>{{ disputeSupportMessage }}</span>
|
||||||
|
</div>
|
||||||
|
<el-button
|
||||||
|
type="warning"
|
||||||
|
plain
|
||||||
|
:loading="cancellingDispute"
|
||||||
|
@click="canCancelDispute ? handleCancelDispute() : openDisputeDialog()"
|
||||||
|
>
|
||||||
|
{{ disputeSupportActionText }}
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="handoff-workspace">
|
||||||
<OrderHandoffTimeline v-if="order" :records="handoffRecords" :listing-code="listingCode" />
|
<OrderHandoffTimeline v-if="order" :records="handoffRecords" :listing-code="listingCode" />
|
||||||
|
</div>
|
||||||
<OrderResourceUsageEditor
|
|
||||||
v-if="order && isRenter && ['renting', 'overdue'].includes(order.status)"
|
|
||||||
v-model:checkout-form="checkoutForm"
|
|
||||||
v-model:resource-usage="resourceUsage"
|
|
||||||
:resources="checkoutResources"
|
|
||||||
:resource-charge-amount="resourceChargeAmount"
|
|
||||||
:snapshot-haf-coin-m="snapshotHafCoinM"
|
|
||||||
:remaining-haf-coin-m="remainingHafCoinM"
|
|
||||||
:returning="returning"
|
|
||||||
:resource-line-amount="resourceLineAmount"
|
|
||||||
@submit="handleSubmitCheckout"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<OrderCheckoutSummary
|
<OrderCheckoutSummary
|
||||||
v-if="
|
v-if="
|
||||||
@@ -442,63 +568,7 @@ async function copyListingCode() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 右侧悬浮操作区 -->
|
<!-- 右侧悬浮操作区 -->
|
||||||
<div v-if="order" class="action-sidebar">
|
<div v-if="order && hasSidebarActions" class="action-sidebar">
|
||||||
<!-- 交接操作 -->
|
|
||||||
<div
|
|
||||||
v-if="
|
|
||||||
isOwner &&
|
|
||||||
order.status === 'pending_handoff' &&
|
|
||||||
order.handoff_status === 'pending_owner'
|
|
||||||
"
|
|
||||||
class="sidebar-card"
|
|
||||||
>
|
|
||||||
<h3 class="sidebar-title">提交交接说明</h3>
|
|
||||||
<el-input
|
|
||||||
v-model="handoffContent"
|
|
||||||
type="textarea"
|
|
||||||
:rows="5"
|
|
||||||
placeholder="填写登录方式、注意事项和交接说明"
|
|
||||||
/>
|
|
||||||
<el-button type="primary" size="large" :loading="handoffing" @click="handleSubmitHandoff"
|
|
||||||
>提交交接</el-button
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 确认收号 -->
|
|
||||||
<div
|
|
||||||
v-if="
|
|
||||||
isRenter &&
|
|
||||||
order.status === 'pending_handoff' &&
|
|
||||||
order.handoff_status === 'pending_renter_confirm'
|
|
||||||
"
|
|
||||||
class="sidebar-card"
|
|
||||||
>
|
|
||||||
<h3 class="sidebar-title">确认收号</h3>
|
|
||||||
<p class="sidebar-hint">
|
|
||||||
确认账号可以正常登录后,订单会进入使用中并重新计算预计截止时间。
|
|
||||||
</p>
|
|
||||||
<el-button type="primary" size="large" :loading="confirming" @click="handleConfirmReceive"
|
|
||||||
>确认已收到账号</el-button
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 发起结账 -->
|
|
||||||
<div
|
|
||||||
v-if="isRenter && ['renting', 'overdue'].includes(order.status)"
|
|
||||||
class="sidebar-card highlight-card"
|
|
||||||
>
|
|
||||||
<h3 class="sidebar-title">快捷结账</h3>
|
|
||||||
<el-button type="primary" size="large" :loading="returning" @click="handleSubmitCheckout">
|
|
||||||
立即提交结账
|
|
||||||
</el-button>
|
|
||||||
<p class="sidebar-hint">
|
|
||||||
如需修改消耗品、哈夫币等详细信息,请先向下滚动填写完整表单后再提交
|
|
||||||
</p>
|
|
||||||
<el-button type="default" size="small" @click="scrollToCheckout">
|
|
||||||
查看详细结账表单
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 确认结账 -->
|
<!-- 确认结账 -->
|
||||||
<div v-if="isOwner && order.status === 'pending_checkout_confirm'" class="sidebar-card">
|
<div v-if="isOwner && order.status === 'pending_checkout_confirm'" class="sidebar-card">
|
||||||
<h3 class="sidebar-title">确认结账</h3>
|
<h3 class="sidebar-title">确认结账</h3>
|
||||||
@@ -539,38 +609,25 @@ async function copyListingCode() {
|
|||||||
<p class="sidebar-hint">不同意修正时需填写原因,将进入争议处理</p>
|
<p class="sidebar-hint">不同意修正时需填写原因,将进入争议处理</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 发起申诉 -->
|
</div>
|
||||||
<div v-if="canOpenDispute" class="sidebar-card dispute-card">
|
|
||||||
<h3 class="sidebar-title">{{ isCheckoutDisputeStage ? '发起结账争议' : '发起申诉' }}</h3>
|
|
||||||
<el-button type="warning" size="large" @click="scrollToDispute">
|
|
||||||
{{ isCheckoutDisputeStage ? '提交结账争议' : '提交申诉' }}
|
|
||||||
</el-button>
|
|
||||||
<p class="sidebar-hint">点击后滚动到申诉表单填写详细信息</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="canCancelDispute" class="sidebar-card dispute-card">
|
<el-dialog
|
||||||
<h3 class="sidebar-title">
|
v-model="disputeDialogVisible"
|
||||||
{{ order.status === 'checkout_disputing' ? '取消结账争议' : '取消申诉' }}
|
:title="isCheckoutDisputeStage ? '发起结账争议' : '发起申诉'"
|
||||||
</h3>
|
width="720px"
|
||||||
<el-button
|
append-to-body
|
||||||
type="warning"
|
class="dispute-dialog"
|
||||||
plain
|
|
||||||
size="large"
|
|
||||||
:loading="cancellingDispute"
|
|
||||||
@click="handleCancelDispute"
|
|
||||||
>
|
>
|
||||||
{{ order.status === 'checkout_disputing' ? '取消结账争议' : '取消申诉' }}
|
<div v-if="order && canOpenDispute" class="dispute-dialog-body">
|
||||||
</el-button>
|
<div class="dispute-dialog-tip">
|
||||||
<p class="sidebar-hint">取消后订单将恢复到发起申诉前的流程</p>
|
<span class="support-dispute-icon">
|
||||||
|
<el-icon><Service /></el-icon>
|
||||||
|
</span>
|
||||||
|
<p>
|
||||||
|
{{ isCheckoutDisputeStage ? '提交后客服会核查结账金额和双方证据。' : '请尽量写清楚时间点、问题经过和证据,便于客服快速处理。' }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="order && canOpenDispute" class="form-section dispute-section">
|
|
||||||
<div class="section-header">
|
|
||||||
<h2>{{ isCheckoutDisputeStage ? '发起结账争议' : '发起申诉' }}</h2>
|
|
||||||
</div>
|
|
||||||
<div class="form-card">
|
|
||||||
<el-select
|
<el-select
|
||||||
v-if="!isCheckoutDisputeStage"
|
v-if="!isCheckoutDisputeStage"
|
||||||
v-model="disputeType"
|
v-model="disputeType"
|
||||||
@@ -605,11 +662,16 @@ async function copyListingCode() {
|
|||||||
@change="handleEvidenceUpload"
|
@change="handleEvidenceUpload"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<el-button type="warning" size="large" :loading="disputing" @click="handleCreateDispute">
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<div class="dispute-dialog-footer">
|
||||||
|
<el-button @click="closeDisputeDialog">取消</el-button>
|
||||||
|
<el-button type="warning" :loading="disputing" @click="handleCreateDispute">
|
||||||
{{ isCheckoutDisputeStage ? '提交结账争议' : '提交申诉' }}
|
{{ isCheckoutDisputeStage ? '提交结账争议' : '提交申诉' }}
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
<el-dialog
|
<el-dialog
|
||||||
v-model="payWayDialogVisible"
|
v-model="payWayDialogVisible"
|
||||||
@@ -684,72 +746,85 @@ async function copyListingCode() {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.order-detail-page {
|
.order-detail-page {
|
||||||
max-width: 1400px;
|
max-width: 1280px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-header {
|
.page-header {
|
||||||
margin-bottom: 32px;
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header.centered {
|
||||||
|
max-width: 940px;
|
||||||
|
margin-right: auto;
|
||||||
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-top {
|
.header-top {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 24px;
|
gap: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-info {
|
.header-info {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.header-info h1 {
|
||||||
|
margin: 4px 0 0;
|
||||||
|
font-size: 26px;
|
||||||
|
line-height: 1.25;
|
||||||
|
}
|
||||||
|
|
||||||
.order-meta {
|
.order-meta {
|
||||||
color: #64748b;
|
color: #64748b;
|
||||||
margin-top: 8px;
|
margin: 6px 0 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content-layout {
|
.content-layout {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 320px;
|
grid-template-columns: minmax(0, 1fr) 280px;
|
||||||
gap: 24px;
|
gap: 16px;
|
||||||
align-items: start;
|
align-items: start;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.content-layout.centered {
|
||||||
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
max-width: 940px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
.main-content {
|
.main-content {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-sidebar {
|
.action-sidebar {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 24px;
|
top: 16px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 16px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-card {
|
.sidebar-card {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 12px;
|
gap: 10px;
|
||||||
padding: 20px;
|
padding: 14px;
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
border: 2px solid #e5e7eb;
|
border: 1px solid #e5e7eb;
|
||||||
border-radius: 12px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
box-shadow: 0 1px 4px rgba(15, 23, 42, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-card.highlight-card {
|
.sidebar-card.highlight-card {
|
||||||
background: #eff6ff;
|
background: #f8fbff;
|
||||||
border-color: #3b82f6;
|
border-color: #3b82f6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-card.dispute-card {
|
|
||||||
background: #fef2f2;
|
|
||||||
border-color: #fecaca;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-title {
|
.sidebar-title {
|
||||||
font-size: 16px;
|
font-size: 14px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #1f2937;
|
color: #1f2937;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -757,37 +832,69 @@ async function copyListingCode() {
|
|||||||
|
|
||||||
.sidebar-hint {
|
.sidebar-hint {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 13px;
|
font-size: 12px;
|
||||||
color: #64748b;
|
color: #64748b;
|
||||||
line-height: 1.5;
|
line-height: 1.45;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-card .el-button {
|
.sidebar-card .el-button {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.support-dispute-icon {
|
||||||
|
display: grid;
|
||||||
|
flex: none;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
place-items: center;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #fff1d6;
|
||||||
|
color: #d97706;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.support-dispute-copy {
|
||||||
|
display: grid;
|
||||||
|
gap: 2px;
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.support-dispute-copy strong {
|
||||||
|
color: #1f2937;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.support-dispute-copy span {
|
||||||
|
color: #7c5a16;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
.order-progress-section {
|
.order-progress-section {
|
||||||
margin-bottom: 32px;
|
margin-bottom: 12px;
|
||||||
padding: 24px;
|
padding: 16px;
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
border: 1px solid #e5e7eb;
|
border: 1px solid #e5e7eb;
|
||||||
border-radius: 12px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-grid {
|
.detail-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||||
gap: 16px;
|
gap: 10px;
|
||||||
margin-bottom: 32px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.metric-card {
|
.metric-card {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 8px;
|
gap: 6px;
|
||||||
padding: 20px;
|
min-height: 86px;
|
||||||
|
padding: 14px;
|
||||||
border: 1px solid #e5e7eb;
|
border: 1px solid #e5e7eb;
|
||||||
border-radius: 12px;
|
border-radius: 8px;
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
}
|
}
|
||||||
@@ -839,15 +946,16 @@ async function copyListingCode() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.metric-label {
|
.metric-label {
|
||||||
font-size: 13px;
|
font-size: 12px;
|
||||||
color: #64748b;
|
color: #64748b;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.metric-value {
|
.metric-value {
|
||||||
font-size: 24px;
|
font-size: 20px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #1f2937;
|
color: #1f2937;
|
||||||
|
line-height: 1.2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.metric-value.amount {
|
.metric-value.amount {
|
||||||
@@ -860,48 +968,46 @@ async function copyListingCode() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.info-section {
|
.info-section {
|
||||||
margin-bottom: 32px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-card {
|
.info-card {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 12px;
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
padding: 20px;
|
gap: 16px;
|
||||||
|
padding: 14px 16px;
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
border: 1px solid #e5e7eb;
|
border: 1px solid #e5e7eb;
|
||||||
border-radius: 12px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-row {
|
.info-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 12px 0;
|
gap: 12px;
|
||||||
border-bottom: 1px solid #f3f4f6;
|
padding: 0;
|
||||||
}
|
|
||||||
|
|
||||||
.info-row:last-child {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-label {
|
.info-label {
|
||||||
font-size: 14px;
|
font-size: 13px;
|
||||||
color: #64748b;
|
color: #64748b;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-value {
|
.info-value {
|
||||||
font-size: 14px;
|
font-size: 13px;
|
||||||
color: #1f2937;
|
color: #1f2937;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-card {
|
.action-card {
|
||||||
margin-top: 16px;
|
margin-top: 12px;
|
||||||
padding: 20px;
|
padding: 14px;
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
border: 1px solid #e5e7eb;
|
border: 1px solid #e5e7eb;
|
||||||
border-radius: 12px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-card.warning {
|
.action-card.warning {
|
||||||
@@ -909,13 +1015,8 @@ async function copyListingCode() {
|
|||||||
border-color: #fde68a;
|
border-color: #fde68a;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-card.info {
|
|
||||||
background: #eff6ff;
|
|
||||||
border-color: #bfdbfe;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-message {
|
.action-message {
|
||||||
margin: 0 0 16px 0;
|
margin: 0 0 12px 0;
|
||||||
color: #1f2937;
|
color: #1f2937;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
@@ -930,37 +1031,135 @@ async function copyListingCode() {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header.compact span {
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-header h2 {
|
.section-header h2 {
|
||||||
font-size: 20px;
|
font-size: 18px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #1f2937;
|
color: #1f2937;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-section {
|
.form-section {
|
||||||
margin-bottom: 32px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-card {
|
.form-card {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 16px;
|
gap: 12px;
|
||||||
padding: 24px;
|
padding: 16px;
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
border: 1px solid #e5e7eb;
|
border: 1px solid #e5e7eb;
|
||||||
border-radius: 12px;
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.handoff-action-section {
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.handoff-action-card {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) 160px;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: stretch;
|
||||||
|
padding: 16px;
|
||||||
|
border: 1px solid #bfdbfe;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: linear-gradient(180deg, #f8fbff 0%, #eff6ff 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.handoff-action-card .el-button {
|
||||||
|
height: 100%;
|
||||||
|
min-height: 96px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.handoff-confirm-strip {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
padding: 16px;
|
||||||
|
border: 1px solid #bfdbfe;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: linear-gradient(180deg, #f8fbff 0%, #eff6ff 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.handoff-confirm-strip div {
|
||||||
|
display: grid;
|
||||||
|
gap: 4px;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.handoff-confirm-strip strong {
|
||||||
|
color: #1f2937;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.handoff-confirm-strip span {
|
||||||
|
color: #475569;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secondary-actions {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secondary-action-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secondary-action-card > div {
|
||||||
|
display: grid;
|
||||||
|
gap: 2px;
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secondary-action-card strong {
|
||||||
|
color: #1f2937;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secondary-action-card span {
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secondary-action-card.danger {
|
||||||
|
border: 1px solid #fecaca;
|
||||||
|
background: #fff7f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secondary-action-card.support {
|
||||||
|
border: 1px solid #f8d99b;
|
||||||
|
background: #fffaf0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-hint {
|
.form-hint {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 12px 16px;
|
padding: 10px 12px;
|
||||||
background: #f8fafc;
|
background: #f8fafc;
|
||||||
border-left: 3px solid #3b82f6;
|
border-left: 3px solid #3b82f6;
|
||||||
color: #475569;
|
color: #475569;
|
||||||
font-size: 14px;
|
font-size: 13px;
|
||||||
line-height: 1.6;
|
line-height: 1.5;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1011,16 +1210,16 @@ async function copyListingCode() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.sub-title {
|
.sub-title {
|
||||||
font-size: 16px;
|
font-size: 15px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #374151;
|
color: #374151;
|
||||||
margin: 0 0 16px 0;
|
margin: 0 0 6px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-grid {
|
.form-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
|
||||||
gap: 16px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.field-hint {
|
.field-hint {
|
||||||
@@ -1030,9 +1229,32 @@ async function copyListingCode() {
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dispute-section .form-card {
|
.dispute-dialog-body {
|
||||||
background: #fef2f2;
|
display: grid;
|
||||||
border-color: #fecaca;
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dispute-dialog-tip {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border: 1px solid #f8d99b;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #fffaf0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dispute-dialog-tip p {
|
||||||
|
margin: 0;
|
||||||
|
color: #7c5a16;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dispute-dialog-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.upload-line {
|
.upload-line {
|
||||||
@@ -1047,6 +1269,18 @@ async function copyListingCode() {
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:global(.dispute-dialog) {
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dispute-dialog .el-dialog__body) {
|
||||||
|
padding: 14px 20px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dispute-dialog .el-dialog__footer) {
|
||||||
|
padding: 12px 20px 18px;
|
||||||
|
}
|
||||||
|
|
||||||
.pay-dialog-body {
|
.pay-dialog-body {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
@@ -1260,6 +1494,18 @@ async function copyListingCode() {
|
|||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.handoff-action-card,
|
||||||
|
.handoff-confirm-strip,
|
||||||
|
.secondary-action-card {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.handoff-action-card .el-button {
|
||||||
|
min-height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
.action-buttons {
|
.action-buttons {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user