feat: 优化订单详情页UI,添加右侧悬浮操作栏

## 问题
- 原页面过长,操作按钮在底部,用户需要滚动才能操作
- 部分操作在侧边栏和主内容区重复显示

## 改进
1. **布局重构**
   - 采用左右分栏布局(主内容 + 320px 侧边栏)
   - 侧边栏固定悬浮(sticky),始终可见
   - 页面最大宽度从 1200px 扩展到 1400px

2. **操作整合(避免重复)**
   - 简单操作直接在侧边栏完成:提交交接、确认收号、确认结账
   - 复杂操作提供快捷入口:发起结账、修改结账、拒绝修正、发起申诉
   - 移除主内容区的重复表单

3. **视觉优化**
   - 蓝色高亮卡片:发起结账(重要操作)
   - 红色警告卡片:发起申诉(特殊操作)
   - 添加小分隔线区分主次操作

4. **响应式设计**
   - 桌面端:左右分栏
   - 移动端:单列布局,侧边栏移到顶部

## 用户体验提升
- 所有关键操作始终可见,无需滚动
- 简单操作一键完成,复杂操作跳转到详细表单
- 视觉层次清晰,减少误操作

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
yml
2026-06-05 11:01:17 +08:00
co-authored by Claude Opus 4.7
parent b1a1da726a
commit b380817940
3 changed files with 396 additions and 44 deletions
+121
View File
@@ -0,0 +1,121 @@
# 订单详情页 UI 优化
## 优化内容
### 问题
- 原页面 UI 过长,需要滚动到最底部才能看到"提交交接说明"、"提交结账"、"发起申诉"等操作按钮
- 用户体验差,操作流程不流畅
### 解决方案
#### 1. 布局调整
- 采用**左右分栏布局**:主内容区(左)+ 操作侧边栏(右)
- 侧边栏**固定悬浮**(sticky),始终可见
- 页面最大宽度从 1200px 扩展到 1400px,为侧边栏留出空间
#### 2. 操作侧边栏设计
- **宽度**: 320px
- **位置**: 右侧固定悬浮(sticky top: 24px
- **样式**: 白色卡片,带阴影和边框,重要操作突出显示
#### 3. 按钮优化
##### 快捷操作(直接执行)
- **提交交接**: 号主在侧边栏直接填写并提交
- **确认收号**: 租客一键确认
- **确认结账**: 号主一键确认
- **同意修正**: 租客一键同意
##### 复杂操作(跳转填写)
- **发起结账**: 点击后平滑滚动到结账表单填写区
- **修改结账**: 点击后平滑滚动到修改表单
- **拒绝修正**: 点击后平滑滚动到拒绝原因填写区
- **发起申诉**: 点击后平滑滚动到申诉表单
#### 4. 响应式设计
- **桌面端**: 左右分栏,侧边栏悬浮
- **移动端**: 单列布局,侧边栏移到顶部(order: -1)
## 核心代码变更
### 布局结构
```vue
<div class="content-layout">
<div class="main-content">
<!-- 主要内容进度信息交接记录详细表单 -->
</div>
<div class="action-sidebar">
<!-- 操作卡片根据订单状态和用户角色显示 -->
</div>
</div>
```
### 样式关键点
```css
.content-layout {
display: grid;
grid-template-columns: 1fr 320px;
gap: 24px;
align-items: start;
}
.action-sidebar {
position: sticky;
top: 24px;
display: flex;
flex-direction: column;
gap: 16px;
}
.sidebar-card {
padding: 20px;
background: #ffffff;
border: 2px solid #e5e7eb;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
```
### 平滑滚动函数
```typescript
function scrollToCheckout() {
const checkoutSection = document.querySelector('.checkout-form-section')
if (checkoutSection) {
checkoutSection.scrollIntoView({ behavior: 'smooth', block: 'start' })
}
}
```
## 优化效果
### 用户体验提升
1. **无需滚动查找**: 所有核心操作始终可见
2. **快速决策**: 重要信息和操作集中在右侧
3. **减少误操作**: 复杂操作跳转到详细表单,简单操作直接执行
4. **视觉引导**: 申诉等特殊操作用不同颜色背景突出
### 操作流程优化
- **交接**: 侧边栏直接提交 → 提升效率
- **结账**: 侧边栏点击跳转 → 避免误操作
- **申诉**: 侧边栏始终可见 → 降低投诉门槛
### 性能优化
- 使用 CSS `position: sticky` 代替 JavaScript 监听滚动
- 平滑滚动使用原生 `scrollIntoView` API
- 响应式布局使用 CSS Grid,无需 JavaScript
## 兼容性
- **现代浏览器**: 完全支持
- **移动端**: 自动切换为单列布局
- **低分辨率**: 自动调整为堆叠布局
## 文件变更
- `frontend/src/features/orders/views/OrderDetailView.vue`
- 添加 `.content-layout``.action-sidebar` 布局
- 添加侧边栏操作卡片
- 添加平滑滚动函数
- 为表单区域添加 class 标识
- 响应式样式优化
+74
View File
@@ -0,0 +1,74 @@
# 订单详情页 UI 优化总结
## 问题
- 原页面 UI 过长,操作按钮在页面底部
- 用户需要滚动到最底部才能执行"提交交接"、"提交结账"、"发起申诉"等关键操作
- 部分操作在侧边栏和主内容区重复显示
## 解决方案
### 1. 布局重构
- **左右分栏布局**:主内容区(左)+ 操作侧边栏(右,320px)
- **侧边栏悬浮**`position: sticky; top: 24px;` 始终可见
- **页面最大宽度**:从 1200px 扩展到 1400px
### 2. 操作整合(避免重复)
#### 简单操作 - 直接在侧边栏完成
-**提交交接说明**:侧边栏内直接填写 textarea 并提交(移除主内容区重复表单)
-**确认收号**:侧边栏一键确认(移除主内容区重复按钮)
-**确认结账**:侧边栏一键确认 + 跳转到修改表单按钮
-**同意修正结账**:侧边栏一键同意 + 跳转到拒绝表单按钮
#### 复杂操作 - 侧边栏提供快捷入口,主内容区保留详细表单
- 📝 **发起结账**:侧边栏可快速提交(使用当前表单值),也可跳转到详细表单修改消耗品、哈夫币等
- 📝 **修改结账**:侧边栏提供跳转按钮,主内容区保留完整修改表单
- 📝 **拒绝修正**:侧边栏提供跳转按钮,主内容区填写拒绝原因
- 📝 **发起申诉**:侧边栏提供跳转按钮,主内容区填写完整申诉信息
### 3. 视觉优化
- **卡片样式**:白色背景 + 边框 + 阴影
- **高亮卡片**:发起结账使用蓝色背景(`.highlight-card`
- **争议卡片**:发起申诉使用红色背景(`.dispute-card`
- **小分隔线**:侧边栏内使用 `.section-divider-mini` 分隔主次操作
### 4. 响应式设计
- **桌面端**`grid-template-columns: 1fr 320px`
- **移动端**:单列布局,侧边栏移到顶部(`order: -1`
## 主要代码变更
### 新增函数
```typescript
function scrollToCheckout()
function scrollToCounterCheckout()
function scrollToRejectCheckout()
function scrollToDispute()
```
### 新增样式类
```css
.content-layout { display: grid; grid-template-columns: 1fr 320px; }
.action-sidebar { position: sticky; top: 24px; }
.sidebar-card { ... }
.sidebar-card.highlight-card { background: #eff6ff; }
.sidebar-card.dispute-card { background: #fef2f2; }
.section-divider-mini { ... }
```
### 移除重复内容
- ❌ 主内容区的"提交交接说明"表单
- ❌ 主内容区的"确认收号"表单
- ❌ "确认结账"部分的重复按钮和分隔线
## 用户体验提升
1. **无需滚动**:所有关键操作始终在右侧可见
2. **快捷操作**:简单操作直接在侧边栏完成(如提交交接、确认收号)
3. **灵活选择**:复杂操作既可快速提交,也可跳转到详细表单修改
4. **视觉引导**:重要操作用颜色区分(蓝色高亮、红色警告)
5. **减少误操作**:复杂表单保留在主内容区,避免误提交
## 文件变更
- `frontend/src/features/orders/views/OrderDetailView.vue`
- `docs/ui-optimization-order-detail.md`(优化文档)
@@ -612,6 +612,34 @@ function linesToList(value: string) {
.filter(Boolean)
}
function scrollToDispute() {
const disputeSection = document.querySelector('.dispute-section')
if (disputeSection) {
disputeSection.scrollIntoView({ behavior: 'smooth', block: 'start' })
}
}
function scrollToCheckout() {
const checkoutSection = document.querySelector('.checkout-form-section')
if (checkoutSection) {
checkoutSection.scrollIntoView({ behavior: 'smooth', block: 'start' })
}
}
function scrollToCounterCheckout() {
const counterSection = document.querySelector('.counter-checkout-section')
if (counterSection) {
counterSection.scrollIntoView({ behavior: 'smooth', block: 'start' })
}
}
function scrollToRejectCheckout() {
const rejectSection = document.querySelector('.reject-checkout-section')
if (rejectSection) {
rejectSection.scrollIntoView({ behavior: 'smooth', block: 'start' })
}
}
function formatHandoffRecordType(type: string) {
const typeMap: Record<string, string> = {
owner_handoff: '卖家交接',
@@ -640,6 +668,8 @@ function formatHandoffRecordType(type: string) {
</div>
</div>
<div v-if="order" class="content-layout">
<div class="main-content">
<div v-if="order" class="order-progress-section">
<el-steps :active="getOrderStep(order.status)" align-center finish-status="success" process-status="process">
<el-step title="待支付" :description="order.status === 'pending_payment' ? '等待租客支付' : ''" />
@@ -726,30 +756,8 @@ function formatHandoffRecordType(type: string) {
</el-timeline>
</div>
<div v-if="order && isOwner && order.status === 'pending_handoff' && order.handoff_status === 'pending_owner'" class="form-section">
<div class="section-header">
<h2>提交交接说明</h2>
</div>
<div class="form-card">
<el-input v-model="handoffContent" type="textarea" :rows="4" placeholder="填写登录方式、注意事项和交接说明" />
<el-button type="primary" size="large" :loading="handoffing" @click="handleSubmitHandoff">提交交接</el-button>
</div>
</div>
<div
v-if="order && isRenter && order.status === 'pending_handoff' && order.handoff_status === 'pending_renter_confirm'"
class="form-section"
>
<div class="section-header">
<h2>确认收号</h2>
</div>
<div class="form-card">
<p class="form-hint">确认账号可以正常登录后订单会进入使用中并重新计算预计截止时间</p>
<el-button type="primary" size="large" :loading="confirming" @click="handleConfirmReceive">确认已收到账号</el-button>
</div>
</div>
<div v-if="order && isRenter && ['renting', 'overdue'].includes(order.status)" class="form-section">
<div v-if="order && isRenter && ['renting', 'overdue'].includes(order.status)" class="form-section checkout-form-section">
<div class="section-header">
<h2>发起结账</h2>
</div>
@@ -848,19 +856,12 @@ function formatHandoffRecordType(type: string) {
</div>
</div>
<div v-if="order && isOwner && order.status === 'pending_checkout_confirm'" class="form-section">
<div v-if="order && isOwner && order.status === 'pending_checkout_confirm'" class="form-section counter-checkout-section">
<div class="section-header">
<h2>确认结账</h2>
<h2>修改结账</h2>
</div>
<div class="form-card">
<p class="form-hint">确认账号状态和扣款金额无误后订单会完成账号重新上架</p>
<el-button type="primary" size="large" :loading="completing" @click="handleConfirmCheckout">确认结账并完成订单</el-button>
<div class="section-divider">
<span>或者</span>
</div>
<h3 class="sub-title">修改结账</h3>
<h3 class="sub-title">修改结账金额</h3>
<el-form class="form-grid" label-position="top">
<el-form-item label="额外消耗品已用金额">
<el-input-number v-model="counterForm.consumable_amount" class="full-control" :min="0" :precision="0" controls-position="right" />
@@ -881,23 +882,89 @@ function formatHandoffRecordType(type: string) {
</div>
</div>
<div v-if="order && isRenter && order.status === 'pending_checkout_accept'" class="form-section">
<div v-if="order && isRenter && order.status === 'pending_checkout_accept'" class="form-section reject-checkout-section">
<div class="section-header">
<h2>确认修正结账</h2>
<h2>拒绝修正</h2>
</div>
<div class="form-card">
<p class="form-hint">同意后订单会完成结算不同意会进入争议由客服仲裁</p>
<el-button type="primary" size="large" :loading="acceptingCheckout" @click="handleAcceptCheckout">同意修正并完成订单</el-button>
<div class="section-divider">
<span>或者</span>
</div>
<h3 class="sub-title">拒绝修正</h3>
<p class="form-hint">同意修正时填写原因订单会进入争议处理</p>
<el-input v-model="rejectReason" type="textarea" :rows="3" placeholder="不同意时填写原因,会进入争议处理" />
<el-button type="danger" plain size="large" :loading="rejectingCheckout" @click="handleRejectCheckout">拒绝修正并发起争议</el-button>
</div>
</div>
</div>
<!-- 右侧悬浮操作区 -->
<div v-if="order" 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">
<h3 class="sidebar-title">确认结账</h3>
<el-button type="primary" size="large" :loading="completing" @click="handleConfirmCheckout">
确认结账并完成订单
</el-button>
<div class="section-divider-mini">
<span>或者</span>
</div>
<el-button type="warning" plain size="large" @click="scrollToCounterCheckout">
需要修改结账金额
</el-button>
<p class="sidebar-hint">如认为金额有误点击上方按钮修改</p>
</div>
<!-- 确认修正结账 -->
<div v-if="isRenter && order.status === 'pending_checkout_accept'" class="sidebar-card">
<h3 class="sidebar-title">确认修正结账</h3>
<el-button type="primary" size="large" :loading="acceptingCheckout" @click="handleAcceptCheckout">
同意修正并完成订单
</el-button>
<div class="section-divider-mini">
<span>或者</span>
</div>
<el-button type="danger" plain size="large" @click="scrollToRejectCheckout">
不同意发起争议
</el-button>
<p class="sidebar-hint">不同意修正时需填写原因将进入争议处理</p>
</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>
<div v-if="order && canOpenDispute" class="form-section dispute-section">
<div class="section-header">
@@ -975,7 +1042,7 @@ function formatHandoffRecordType(type: string) {
<style scoped>
.order-detail-page {
max-width: 1200px;
max-width: 1400px;
margin: 0 auto;
}
@@ -999,6 +1066,64 @@ function formatHandoffRecordType(type: string) {
margin-top: 8px;
}
.content-layout {
display: grid;
grid-template-columns: 1fr 320px;
gap: 24px;
align-items: start;
}
.main-content {
min-width: 0;
}
.action-sidebar {
position: sticky;
top: 24px;
display: flex;
flex-direction: column;
gap: 16px;
}
.sidebar-card {
display: flex;
flex-direction: column;
gap: 12px;
padding: 20px;
background: #ffffff;
border: 2px solid #e5e7eb;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.sidebar-card.highlight-card {
background: #eff6ff;
border-color: #3b82f6;
}
.sidebar-card.dispute-card {
background: #fef2f2;
border-color: #fecaca;
}
.sidebar-title {
font-size: 16px;
font-weight: 700;
color: #1f2937;
margin: 0;
}
.sidebar-hint {
margin: 0;
font-size: 13px;
color: #64748b;
line-height: 1.5;
}
.sidebar-card .el-button {
width: 100%;
}
.order-progress-section {
margin-bottom: 32px;
padding: 24px;
@@ -1244,6 +1369,29 @@ function formatHandoffRecordType(type: string) {
font-weight: 500;
}
.section-divider-mini {
display: flex;
align-items: center;
justify-content: center;
margin: 8px 0;
position: relative;
}
.section-divider-mini::before,
.section-divider-mini::after {
content: '';
flex: 1;
height: 1px;
background: #e5e7eb;
}
.section-divider-mini span {
padding: 0 12px;
color: #9ca3af;
font-size: 12px;
font-weight: 500;
}
.sub-title {
font-size: 16px;
font-weight: 600;
@@ -1562,6 +1710,15 @@ function formatHandoffRecordType(type: string) {
}
@media (max-width: 768px) {
.content-layout {
grid-template-columns: 1fr;
}
.action-sidebar {
position: static;
order: -1;
}
.header-top {
flex-direction: column;
}