# 订单详情保险保障卡片补「退保」按钮

**日期**: 2026-04-24
**Issue**: #1384
**类型**: ui (前端改动, 后端零改动)

---

## 摘要

用户截图反馈订单详情 → 合同保险 tab → 保险保障卡片 (状态「已承保」) **只有「查看保单」按钮, 没有退保入口**。后端 `POST /admin/insurance/cancel/{id}` 历史已就位 (保游网 `/Insurance/CancelIns` 封装完整, 状态机 INSURED→CANCELLED 已跑通, 该订单历史也有 CANCELLED 记录), 独立「保险订单列表」页据说已有退保按钮, 仅是**订单详情嵌入的保险卡片组件漏了**。

## 后端接口

已有 (无需改动):

```
POST /admin/insurance/cancel/{insuranceOrderId}
  → Result<InsuranceOrderDetailVO>

业务规则:
- 仅允许 status == INSURED 的保险订单调用
- 调保游网 /Insurance/CancelIns 后, 成功或返回「正在退保中」都把 DB status 改为 CANCELLED
- 其他失败抛 BusinessException 带保游网错误文案
```

## 前端改动

订单详情页合同保险 tab → 保险保障卡片组件, 右下角「查看保单」旁加一个「退保」按钮:

```vue
<template>
  <!-- 保险保障卡片底部 action 区 -->
  <div class="card-actions">
    <n-button text type="info" @click="handleViewPolicy(insurance.insuranceOrderId)">
      查看保单
    </n-button>
    <!-- 新增:仅 INSURED 显示 -->
    <n-button
      v-if="insurance.status === 'INSURED'"
      text
      type="error"
      @click="handleRefund(insurance.insuranceOrderId)"
    >
      退保
    </n-button>
  </div>
</template>

<script setup>
import { useDialog, useMessage } from 'naive-ui'
import { adminInsuranceCancel } from '@/api/insurance'  // 复用保险订单列表页已有的 api

const dialog = useDialog()
const message = useMessage()
const emit = defineEmits(['refresh'])

async function handleRefund(insuranceOrderId) {
  dialog.warning({
    title: '确认退保',
    content: '将调保游网撤单, 成功后订单状态变为「已退保」。此操作不可逆。',
    positiveText: '确认退保',
    negativeText: '取消',
    onPositiveClick: async () => {
      try {
        await adminInsuranceCancel(insuranceOrderId)
        message.success('退保成功')
        emit('refresh')  // 刷新订单详情
      } catch (e) {
        message.error(e?.message || '退保失败')
      }
    }
  })
}
</script>
```

## 显示规则

| 保险 status | 显示「查看保单」 | 显示「退保」 |
|---|---|---|
| PENDING   | ❌ (无保单) | ❌ |
| INSURING  | ❌ | ❌ |
| INSURED   | ✅ | ✅ |
| CANCELLED | ✅ (可下载历史保单) | ❌ |
| FAILED    | ❌ | ❌ |

## 验收点

- [ ] 订单 `2047252402389598209` 详情 → 合同保险 tab → 保险保障卡片右下角出现「退保」按钮
- [ ] 点击弹二次确认, 确认后调 `POST /admin/insurance/cancel/2047591721386098690`
- [ ] 响应成功 → 卡片状态角标由「已承保」变「已退保」, 自动刷新
- [ ] 响应失败 (保游网错误) → toast 显示错误原因, 状态不变
- [ ] CANCELLED / FAILED / PENDING 状态不显示退保按钮
