<!-- 顶部状态栏 -->

This commit is contained in:
2026-01-12 18:24:25 +08:00
parent 725aa1819a
commit 7adfd27f3f
33 changed files with 9401 additions and 309 deletions

2
.gitignore vendored
View File

@@ -52,3 +52,5 @@ src/manifest.json
.kilocode/** .kilocode/**
.opencode .opencode
.opencode/** .opencode/**
.agent
.agent/**

View File

@@ -2,6 +2,7 @@
# OpenSpec Instructions # OpenSpec Instructions
These instructions are for AI assistants working in this project. These instructions are for AI assistants working in this project.
When communicating with me, please use Chinese.
Always open `@/openspec/AGENTS.md` when the request: Always open `@/openspec/AGENTS.md` when the request:
- Mentions planning or proposals (words like proposal, spec, change, plan) - Mentions planning or proposals (words like proposal, spec, change, plan)

View File

@@ -1,6 +1,7 @@
# OpenSpec Instructions # OpenSpec Instructions
Instructions for AI coding assistants using OpenSpec for spec-driven development. Instructions for AI coding assistants using OpenSpec for spec-driven development
When communicating with me, please use Chinese.
## TL;DR Quick Checklist ## TL;DR Quick Checklist

View File

@@ -0,0 +1,474 @@
# Design: 集成保险流程到贷款业务
## Context
本设计涉及银行端、保险端和政务端三个系统的协同,实现贷款业务中的保险购买、核保、理赔审核和不良贷款监管的完整流程。
**关键约束**
- 保险购买是银行端在贷款审核通过后的可选操作,与用户端无关
- 银行选择保险产品后,自动发送给对应的保险公司
- 银行端发起理赔申请并上传材料到保险公司
- 政务端可以看到银行端的所有贷款信息,包括不良贷款
**本次优化重点**
- 优化银行端审核详情页面的保险功能交互
- 实现投保和理赔的页面跳转
- 流程步骤条增加保险节点
- 快捷入口位置调整
## Goals / Non-Goals
**Goals**:
- 建立银行与保险公司之间的投保和核保流程
- 实现银行向保险公司提交理赔申请的功能
- 支持保险公司进行核保和理赔审核
- 为政务端提供完整的贷款业务流程视图,包括保险和审批信息
- 确保各端数据的一致性和可追溯性
- 优化银行端保险功能的用户体验
**Non-Goals**:
- 不涉及用户端直接购买保险
- 不涉及保险公司主动向银行推送产品
- 不涉及政务端对贷款的审批(仅查看)
- 不涉及保险产品的具体定价和费率计算
## Decisions
### 1. 数据模型设计
#### 1.1 保险相关数据实体
```typescript
// 保险公司
interface InsuranceCompany {
id: string;
name: string;
contactInfo: string;
status: 'active' | 'inactive';
}
// 保险产品
interface InsuranceProduct {
id: string;
companyId: string;
name: string;
type: 'housing_loan' | 'business_credit' | 'other';
description: string;
minAmount: number;
maxAmount: number;
status: 'active' | 'inactive';
}
// 投保申请
interface InsuranceApplication {
id: string;
loanId: string;
bankId: string;
companyId: string;
productId: string;
customerInfo: {
name: string;
idNumber: string;
creditScore: number;
loanAmount: number;
loanTerm: number;
};
insuranceAmount: number;
insuranceTerm: number;
status: 'pending' | 'approved' | 'rejected';
createdAt: Date;
reviewedAt?: Date;
reviewedBy?: string;
rejectionReason?: string;
}
// 保险单
interface InsurancePolicy {
id: string;
applicationId: string;
policyNumber: string;
companyId: string;
bankId: string;
loanId: string;
productId: string;
insuranceAmount: number;
insuranceTerm: number;
startDate: Date;
endDate: Date;
status: 'active' | 'expired' | 'cancelled';
issuedAt: Date;
}
// 理赔申请
interface ClaimApplication {
id: string;
policyId: string;
loanId: string;
bankId: string;
companyId: string;
claimAmount: number;
claimReason: string;
materials: string[]; // 文件URL列表
status: 'pending' | 'approved' | 'rejected';
submittedAt: Date;
reviewedAt?: Date;
reviewedBy?: string;
rejectionReason?: string;
payoutAmount?: number;
payoutDate?: Date;
}
```
#### 1.2 贷款与保险关联
```typescript
// 扩展现有贷款实体
interface Loan {
id: string;
// ... 现有字段
insuranceApplicationId?: string; // 可选
insurancePolicyId?: string; // 可选
claimApplicationIds: string[]; // 理赔申请列表
isBadLoan: boolean; // 是否为不良贷款
}
```
### 2. 业务流程设计
#### 2.1 保险购买流程
```mermaid
sequenceDiagram
participant Bank as 银行端
participant System as 系统后端
participant Insurance as 保险端
Bank->>System: 1. 贷款审核通过
Bank->>System: 2. 选择购买保险(可选)
Bank->>System: 3. 选择保险公司和产品
Bank->>System: 4. 确定保险金额和期限
System->>System: 5. 创建投保申请
System->>Insurance: 6. 自动发送投保申请
Insurance->>Insurance: 7. 核保人员审核
alt 核保通过
Insurance->>System: 8a. 返回核保通过
System->>System: 9a. 生成保险单
System->>Bank: 10a. 通知投保成功
else 核保拒绝
Insurance->>System: 8b. 返回拒绝原因
System->>Bank: 10b. 通知投保失败
end
```
#### 2.2 理赔申请流程
```mermaid
sequenceDiagram
participant Bank as 银行端
participant System as 系统后端
participant Insurance as 保险端
Bank->>System: 1. 发起理赔申请
Bank->>System: 2. 上传理赔材料
System->>System: 3. 创建理赔申请记录
System->>Insurance: 4. 发送理赔申请
Insurance->>Insurance: 5. 审核理赔材料
alt 审核通过
Insurance->>System: 6a. 返回审核通过
Insurance->>System: 7a. 执行赔付
System->>Bank: 8a. 通知理赔成功
else 审核拒绝
Insurance->>System: 6b. 返回拒绝原因
System->>Bank: 8b. 通知理赔失败
end
```
#### 2.3 政务端查看流程
```mermaid
sequenceDiagram
participant Government as 政务端
participant System as 系统后端
Government->>System: 1. 请求贷款列表
System->>Government: 2. 返回贷款信息(含保险信息)
Government->>System: 3. 请求贷款详情
System->>Government: 4. 返回完整信息:
- 用户信息
- 贷款信息
- 保险信息(投保申请、保险单)
- 理赔信息(理赔申请、审核结果)
- 审批流程记录
```
### 3. API 接口设计
#### 3.1 银行端接口
```typescript
// 获取合作保险公司列表
GET /api/bank/insurance/companies
// 获取保险产品列表
GET /api/bank/insurance/products?companyId={companyId}
// 创建投保申请
POST /api/bank/insurance/applications
{
loanId: string;
companyId: string;
productId: string;
insuranceAmount: number;
insuranceTerm: number;
}
// 获取投保申请状态
GET /api/bank/insurance/applications/{id}
// 创建理赔申请
POST /api/bank/insurance/claims
{
policyId: string;
loanId: string;
claimAmount: number;
claimReason: string;
materials: File[];
}
// 获取理赔申请状态
GET /api/bank/insurance/claims/{id}
```
#### 3.2 保险端接口
```typescript
// 获取待核保申请列表
GET /api/insurance/applications?status=pending
// 获取投保申请详情
GET /api/insurance/applications/{id}
// 核保审核
POST /api/insurance/applications/{id}/review
{
approved: boolean;
rejectionReason?: string;
}
// 获取待理赔审核列表
GET /api/insurance/claims?status=pending
// 获取理赔申请详情
GET /api/insurance/claims/{id}
// 理赔审核
POST /api/insurance/claims/{id}/review
{
approved: boolean;
rejectionReason?: string;
payoutAmount?: number;
}
```
#### 3.3 政务端接口
```typescript
// 获取贷款列表(含保险信息)
GET /api/government/loans?includeInsurance=true
// 获取贷款详情(含完整业务流程)
GET /api/government/loans/{id}?full=true
// 获取不良贷款列表
GET /api/government/loans/bad
```
### 4. 页面结构设计
#### 4.1 银行端页面
```
src/pagesBank/insurance/
├── application/
│ ├── create.vue # 创建投保申请
│ └── detail.vue # 投保申请详情
├── claim/
│ ├── create.vue # 创建理赔申请
│ └── list.vue # 理赔申请列表
└── policy/
└── detail.vue # 保险单详情
```
#### 4.2 保险端页面
```
src/pagesInsurance/underwriting/
├── list.vue # 待核保申请列表
└── detail.vue # 核保申请详情
src/pagesInsurance/claim-review/
├── list.vue # 待理赔审核列表
└── detail.vue # 理赔审核详情
```
#### 4.3 政务端页面
```
src/pagesGovernment/bank/
├── list.vue # 修改:显示保险信息标识
└── detail.vue # 修改:显示完整业务流程信息
```
### 5. 状态机设计
#### 5.1 投保申请状态机
```
pending → approved → policy_issued
pending → rejected
```
#### 5.2 理赔申请状态机
```
pending → approved → paid
pending → rejected
```
### 6. 权限控制
- **银行端**:只能查看和操作自己发起的投保和理赔申请
- **保险端**:只能查看和操作分配给自己的核保和理赔审核任务
- **政务端**:只读权限,可查看所有贷款信息
## UI/UX 优化设计
### 6.1 审核详情页面优化
#### 6.1.1 保险信息展示
根据贷款状态动态展示不同的保险信息:
- **未投保状态**status === 'DISBURSED' && !hasInsurance
- 显示"购买保险"按钮
- 不显示保单信息
- **已投保状态**hasInsurance
- 显示保单信息(保险公司、保单号、保险金额、保险期限)
- 显示"申请理赔"按钮
- **投保中状态**insuranceStatus === 'pending'
- 显示投保申请进度
- 不显示保单号(还未生成)
- **已理赔状态**hasClaim
- 显示保单信息
- 显示理赔记录
#### 6.1.2 投保申请跳转
```typescript
function handleBuyInsurance() {
const loanId = detail.value.id
const loanAmount = detail.value.amount
const loanTerm = detail.value.term * 12 // 转换为月
uni.navigateTo({
url: `/pagesBank/insurance/application/create?loanId=${loanId}&loanAmount=${loanAmount}&loanTerm=${loanTerm}`,
})
}
```
#### 6.1.3 理赔申请跳转
```typescript
function handleApplyClaim() {
const loanId = detail.value.id
const policyId = detail.value.insurancePolicy.id
const policyNumber = detail.value.insurancePolicy.policyNumber
uni.navigateTo({
url: `/pagesBank/insurance/claim/create?loanId=${loanId}&policyId=${policyId}&policyNumber=${policyNumber}`,
})
}
```
### 6.2 流程步骤条优化
#### 6.2.1 步骤定义
```typescript
const steps = [
{ key: 'SUBMITTED', label: '申请' },
{ key: 'ACCEPTED', label: '受理' },
{ key: 'INVESTIGATING', label: '调查' },
{ key: 'APPROVING', label: '审批' },
{ key: 'INSURANCE', label: '投保', condition: (detail) => detail.hasInsurance },
{ key: 'SIGNING', label: '签约' },
{ key: 'DISBURSED', label: '放款' },
]
```
#### 6.2.2 条件渲染
```vue
<view
v-for="(step, index) in visibleSteps"
:key="step.key"
class="step-item"
:class="{ active: index <= currentStepIndex, current: index === currentStepIndex }"
>
<!-- 步骤内容 -->
</view>
```
### 6.3 快捷入口迁移
#### 6.3.1 从审核列表移除
移除 `/pagesBank/audit/list.vue` 中的保险功能快捷入口代码块。
#### 6.3.2 添加到工作台首页
`/pagesBank/dashboard/index.vue``quickActions` 数组中添加:
```typescript
const quickActions = [
{ icon: 'i-carbon-task-approved', label: '待审核', path: '/pagesBank/audit/list' },
{ icon: 'i-carbon-group', label: '客户管理', path: '/pagesBank/customer/list' },
{ icon: 'i-carbon-calendar', label: '拜访计划', path: '/pagesBank/visit/list' },
{ icon: 'i-carbon-add', label: '创建拜访', path: '/pagesBank/visit/create' },
{ icon: 'i-carbon-document-download', label: '报表', path: '/pagesBank/report/list' },
{ icon: 'i-carbon-security', label: '投保管理', path: '/pagesBank/insurance/application/list' },
{ icon: 'i-carbon-money', label: '理赔管理', path: '/pagesBank/insurance/claim/list' },
{ icon: 'i-carbon-settings', label: '设置', path: '/pagesBank/me/index' },
]
```
## Risks / Trade-offs
| Risk | Mitigation |
|------|-----------|
| 保险公司核保时间过长影响贷款放款 | 设置核保超时机制,超时后允许银行取消投保 |
| 理赔材料审核标准不统一 | 在系统中提供审核标准文档和模板 |
| 跨系统数据同步延迟 | 使用消息队列确保数据最终一致性 |
| 政务端数据量过大影响性能 | 实现分页和懒加载,支持按条件筛选 |
## Migration Plan
1. **Phase 0**: 创建数据表和 API 接口
2. **Phase 2**: 实现银行端保险购买功能
3. **Phase 3**: 实现保险端核保功能
4. **Phase 4**: 实现银行端理赔申请功能
5. **Phase 5**: 实现保险端理赔审核功能
6. **Phase 6**: 扩展政务端查看功能
**Rollback**: 如果需要回滚,可以禁用保险相关功能,贷款流程将恢复到不包含保险的状态。
## Open Questions
1. 保险公司是否需要支持多家银行同时接入?
2. 理赔赔付金额的计算规则是什么?
3. 不良贷款的判定标准是什么(逾期天数、金额等)?
4. 是否需要支持保险单的转让或变更?
5. 投保节点是否需要在所有贷款流程中都显示?

View File

@@ -0,0 +1,288 @@
# Change: 集成保险流程到贷款业务(优化版)
## Why
当前贷款业务流程中缺少保险环节,无法实现风险共担机制。银行需要在贷款审核通过后选择为贷款购买保险(可选),保险公司进行核保,并在发生逾期时进行理赔。同时,政务端需要能够查看完整的贷款业务流程信息,包括保险和审批记录。这个变更将完善金融生态的风险管理体系。
## What Changes
### 本次优化内容integrate-insurance-flow
**银行端优化**
1. **审核详情页面投保流程优化**
- 点击"购买保险"跳转到保险公司选择页面
- 选择保险公司后,跳转到保险产品选择页面
- 选择产品后,填充投保申请创建页面的表单
- 实现完整的多步导航流程
2. **新增页面**
2.1 **投保申请列表页面** (`src/pagesBank/insurance/application/list.vue`)
- 显示所有投保申请列表
- 支持按状态筛选(全部、待审核、已通过、已拒绝)
- 支持搜索功能按ID/公司名称/产品名称/保单号)
- 支持分页加载每页20条
- 显示保险公司、保单号、保险金额、状态、提交时间
- 点击跳转到投保申请详情页
- 下拉刷新和上拉加载更多
2.2 **保险公司选择页面** (`src/pagesBank/insurance/company/select.vue`)
- 独立展示合作保险公司列表
- 支持搜索公司名称和联系方式
- 显示保险公司详细信息(名称、联系方式、合作状态)
- 显示已合作产品数量
- 点击选择后带参数跳转到产品选择页面
- 返回按钮返回投保申请页面
2.3 **保险产品选择页面** (`src/pagesBank/insurance/product/select.vue`)
- 独立展示保险产品列表
- 支持搜索产品名称、描述、类型
- 显示产品详细信息(名称、类型、描述、金额范围)
- 显示所属保险公司
- 支持查看产品详情弹窗
- 点击选择后返回投保申请页面并传递产品信息
3. **工作台首页快捷入口**
- 投保管理:跳转到 `/pagesBank/insurance/application/list`
- 理赔管理:跳转到 `/pagesBank/insurance/claim/list`
### 原始功能范围
- **银行端**
- 在贷款审核通过后添加可选的保险购买流程
- 支持选择合作保险公司和保险产品
- 支持确定保险类型、保险金额和保险期限
- 添加理赔申请功能,支持上传理赔材料
- 贷款信息(包括不良贷款)对政务端可见
- **保险端**
- 添加核保流程,接收银行提交的投保申请
- 支持核保人员查看投保单和银行提供的客户信息
- 支持核保通过(出具保险单)和拒绝(返回拒绝原因)
- 添加理赔审核功能,审核银行提交的理赔材料
- **政务端**
- 查看完整的贷款业务流程信息
- 查看用户信息、保险信息和相关审批记录
- 识别和查看不良贷款信息
## Impact
- **Affected specs**:
- 新增 `bank-insurance-integration` 能力
- 新增 `insurance-underwriting` 能力
- 新增 `insurance-claim-review` 能力
- 新增 `government-bad-loans` 能力
- 新增 `insurance-navigation` 能力(本次优化)
- 修改 `bank-insurance-ui` 能力(本次优化)
- **Affected code**:
- `src/pagesBank/audit/detail.vue` - 审核详情页面(本次优化投保流程)
- `src/pagesBank/audit/list.vue` - 审核列表页面(移除快捷入口)
- `src/pagesBank/dashboard/index.vue` - 工作台首页(快捷入口已存在)
- `src/pagesBank/insurance/application/list.vue` - 投保申请列表页面(新增)
- `src/pagesBank/insurance/company/select.vue` - 保险公司选择页面(新增)
- `src/pagesBank/insurance/product/select.vue` - 保险产品选择页面(新增)
- `src/pagesBank/insurance/application/create.vue` - 投保申请创建页面(支持参数)
- `src/pagesBank/` - 银行端页面(保险购买、理赔申请)
- `src/pagesInsurance/` - 保险端页面(核保、理赔审核)
- `src/pagesGovernment/` - 政务端页面(不良贷款查看)
- `src/api/insurance.ts` - API 接口定义
- 相关的 mock 数据和类型定义
## Change Impact
- **Spec**: `openspec/changes/integrate-insurance-flow/design.md`
- **Bank End**:
- `src/pagesBank/audit/detail.vue`: 优化保险信息展示,实现投保多步导航
- `src/pagesBank/audit/list.vue`: 移除保险功能快捷入口
- `src/pagesBank/dashboard/index.vue`: 添加保险功能快捷入口(已存在)
- `src/pagesBank/insurance/application/list.vue`: 新增投保申请列表页面
- `src/pagesBank/insurance/company/select.vue`: 新增保险公司选择页面
- `src/pagesBank/insurance/product/select.vue`: 新增保险产品选择页面
- **Insurance End**:
- `src/pagesInsurance/policy/list.vue`: 添加核保和理赔审核快捷入口
- `src/pagesInsurance/claim/list.vue`: 优化理赔列表展示,支持查看保单和更多状态
- `src/pagesInsurance/underwriting/list.vue`: 添加模拟数据,支持展示多种核保状态
- `src/pagesInsurance/claim-review/list.vue`: 添加模拟数据,支持展示多种理赔审核状态
- `src/api/insurance.ts`:
- 添加保险端核保和理赔审核的模拟数据
- 扩展 `mockInsurancePolicies` 数组至12条实现数据多样性
- **Government End**:
- `src/pagesGovernment/bank/detail.vue`: 扩充 Mock 数据,支持贷款列表卡片展开详细信息
- **API**:
- `src/api/insurance.ts`: 添加保险端核保和理赔审核的模拟数据
## Implementation Plan
### Phase 0: UI/UX Enhancement已完成 + 本次优化)
- [x] Bank: Update Audit List with Insurance tags and shortcuts
- [x] Bank: Update Audit Detail with Insurance card and actions
- [x] Bank: Create Insurance Application List page
- [x] Bank: Create Company Selection page
- [x] Bank: Create Product Selection page
- [x] Bank: Optimize Audit Detail navigation flow
- [x] Insurance: Update Policy List with Underwriting/Claim shortcuts
- [x] Insurance: Update Claim List with Policy view
- [ ] Insurance: Add mock data for underwriting list本次新增
- [ ] Insurance: Add mock data for claim review list本次新增
- [x] Government: Expand Mock Data and implement collapsible cards
### Phase 1: Database & API
- **Goal**: Establish data models and backend APIs.
- **Tasks**:
- Create tables for insurance companies, products, applications, policies, claims.
- Implement APIs for all roles (Bank, Insurance, Government).
### Phase 2: Frontend Integration
- **Goal**: Integrate APIs into the frontend pages.
- **Tasks**:
- Implement full flows: Purchase -> Underwrite -> Policy -> Claim -> Auditing -> Payout.
- Connect all enhanced UI components to real data.
### Phase 3: Testing & Deployment
- **Goal**: Verify end-to-end flows.
- **Tasks**:
- Integration testing.
- Deployment to staging/prod.
## Page Navigation Flow
```
## 保险端模拟数据设计
### 核保申请模拟数据
为 `src/pagesInsurance/underwriting/list.vue` 添加以下模拟数据:
1. **待审核状态** (pending):
- 投保申请号: IA20250112001
- 银行: 中国工商银行
- 保险公司: 中国人民财产保险股份有限公司
- 保险产品: 个人住房贷款保险
- 客户: 张三(信用评分 750
- 保险金额: 500,000 元
- 保险期限: 120 个月
2. **已通过状态** (approved):
- 投保申请号: IA20250111001
- 银行: 中国工商银行
- 保险公司: 中国平安财产保险股份有限公司
- 保险产品: 小微企业贷款保证保险
- 客户: 李四(信用评分 720
- 保险金额: 800,000 元
- 保险期限: 180 个月
- 核保员: 核保员001
3. **已拒绝状态** (rejected):
- 投保申请号: IA20250110001
- 银行: 中国建设银行
- 保险公司: 中国太平洋财产保险股份有限公司
- 保险产品: 个人消费贷款保险
- 客户: 王五(信用评分 680
- 保险金额: 300,000 元
- 保险期限: 90 个月
- 核保员: 核保员002
- 拒绝原因: 客户信用评分低于产品要求最低值700分
4. **待审核状态** (pending):
- 投保申请号: IA20250109001
- 银行: 中国工商银行
- 保险公司: 中国人民财产保险股份有限公司
- 保险产品: 企业信贷履约保证保险
- 客户: 赵六(信用评分 780
- 保险金额: 600,000 元
- 保险期限: 120 个月
### 理赔审核模拟数据
为 `src/pagesInsurance/claim-review/list.vue` 添加以下模拟数据:
1. **待审核状态** (pending):
- 理赔申请号: CA20250112001
- 银行: 中国工商银行
- 保险单号: POL20250111001
- 保险公司: 中国平安财产保险股份有限公司
- 理赔金额: 400,000 元
- 理赔原因: 借款人逾期超过90天无法偿还贷款本息
- 材料数量: 3 份(逾期还款记录.pdf、催收记录.docx、借款人财务状况.jpg
2. **已通过状态** (approved):
- 理赔申请号: CA20250111001
- 银行: 中国工商银行
- 保险单号: POL20250111001
- 保险公司: 中国平安财产保险股份有限公司
- 理赔金额: 200,000 元
- 赔付金额: 180,000 元
- 理赔原因: 借款人经营困难,申请部分理赔
- 材料数量: 1 份(经营困难证明.pdf
- 审核员: 理赔审核员001
- 赔付日期: 2025-01-12
3. **已拒绝状态** (rejected):
- 理赔申请号: CA20250110001
- 银行: 中国工商银行
- 保险单号: POL20250111001
- 保险公司: 中国平安财产保险股份有限公司
- 理赔金额: 600,000 元
- 理赔原因: 借款人失联,申请全额理赔
- 材料数量: 1 份(失联证明.pdf
- 审核员: 理赔审核员002
- 拒绝原因: 提供的失联证明材料不充分,需要补充公安部门出具的正式证明文件
### 保单模拟数据设计
为 `src/pagesInsurance/policy/list.vue` 添加以下12条保单模拟数据实现数据多样性
**生效中保单 (active)** - 6条
1. 保单号: POL20250111001 | 保险公司: 中国平安 | 产品: 小微企业贷款保证保险 | 保额: 800,000元 | 期限: 180个月
2. 保单号: POL20241220001 | 保险公司: 中国人保 | 产品: 个人住房贷款保险 | 保额: 500,000元 | 期限: 120个月
3. 保单号: POL20241115002 | 保险公司: 中国太保 | 产品: 个人消费贷款保险 | 保额: 300,000元 | 期限: 90个月
4. 保单号: POL20241010003 | 保险公司: 中国人保 | 产品: 企业信贷履约保证保险 | 保额: 1,000,000元 | 期限: 240个月
5. 保单号: POL20240305010 | 保险公司: 中国平安 | 产品: 小微企业贷款保证保险 | 保额: 900,000元 | 期限: 180个月
6. 保单号: POL20240206011 | 保险公司: 中国太保 | 产品: 个人消费贷款保险 | 保额: 250,000元 | 期限: 120个月
**即将到期保单 (expiring)** - 3条
1. 保单号: POL20240905004 | 保险公司: 中国平安 | 产品: 小微企业贷款保证保险 | 保额: 450,000元 | 期限: 120个月
2. 保单号: POL20240805005 | 保险公司: 中国太保 | 产品: 个人消费贷款保险 | 保额: 200,000元 | 期限: 60个月
3. 保单号: POL20240701006 | 保险公司: 中国人保 | 产品: 个人住房贷款保险 | 保额: 350,000元 | 期限: 180个月
**已失效保单 (expired)** - 3条
1. 保单号: POL20240602007 | 保险公司: 中国平安 | 产品: 小微企业贷款保证保险 | 保额: 600,000元 | 期限: 150个月
2. 保单号: POL20240503008 | 保险公司: 中国太保 | 产品: 个人消费贷款保险 | 保额: 150,000元 | 期限: 90个月
3. 保单号: POL20240404009 | 保险公司: 中国人保 | 产品: 企业信贷履约保证保险 | 保额: 750,000元 | 期限: 200个月
### Mock 数据实现位置
模拟数据将在 `src/api/insurance.ts` 中的以下数组中添加:
- `mockInsuranceApplications`: 核保申请模拟数据4条记录
- `mockClaimApplications`: 理赔审核模拟数据3条记录
- `mockInsurancePolicies`: 保险单模拟数据12条记录包含生效中、即将到期、已失效三种状态
审核详情页面 (/pagesBank/audit/detail)
├─→ "购买保险" 按钮
保险公司选择页面 (/pagesBank/insurance/company/select)
├─→ 选择保险公司
保险产品选择页面 (/pagesBank/insurance/product/select)
├─→ 选择保险产品
投保申请创建页面 (/pagesBank/insurance/application/create)
└─→ 提交投保申请
工作台首页 (/pagesBank/dashboard/index)
├─→ "投保管理" → /pagesBank/insurance/application/list
└─→ "理赔管理" → /pagesBank/insurance/claim/list
```

View File

@@ -0,0 +1,138 @@
# bank-insurance-integration Specification Delta
## ADDED Requirements
### Requirement: 保险购买入口
银行端 SHALL 在贷款审核通过后提供保险购买入口,该入口为可选操作。
#### Scenario: 显示保险购买入口
- **WHEN** 银行端用户查看已审核通过的贷款详情
- **THEN** 系统 SHALL 显示"购买保险"按钮
- **AND** 该按钮为可选操作,不强制要求购买
#### Scenario: 跳过保险购买
- **WHEN** 银行端用户选择不购买保险
- **THEN** 系统 SHALL 继续原贷款流程处理
- **AND** 不影响贷款的后续放款流程
### Requirement: 选择合作保险公司
银行端 SHALL 支持从合作保险公司列表中选择保险公司。
#### Scenario: 查看合作保险公司列表
- **WHEN** 银行端用户点击购买保险
- **THEN** 系统 SHALL 显示合作保险公司列表
- **AND** 每个保险公司显示名称、联系方式和状态
#### Scenario: 选择保险公司
- **WHEN** 银行端用户选择一个保险公司
- **THEN** 系统 SHALL 加载该保险公司提供的保险产品列表
### Requirement: 选择保险产品
银行端 SHALL 支持根据贷款类型选择相应的保险产品。
#### Scenario: 显示保险产品列表
- **WHEN** 银行端用户选择保险公司后
- **THEN** 系统 SHALL 显示该保险公司提供的保险产品列表
- **AND** 每个产品显示名称、类型、描述和保险金额范围
#### Scenario: 根据贷款类型筛选产品
- **WHEN** 银行端用户选择保险产品
- **THEN** 系统 SHALL 根据贷款类型推荐相应的保险产品
- **AND** 个人住房贷款推荐住房贷款保险
- **AND** 企业信贷推荐履约保证保险
### Requirement: 确定保险金额和期限
银行端 SHALL 支持确定保险金额和保险期限。
#### Scenario: 设置保险金额
- **WHEN** 银行端用户设置保险金额
- **THEN** 系统 SHALL 验证保险金额不低于抵押物价值
- **AND** 保险金额应在保险产品的最小和最大金额范围内
#### Scenario: 设置保险期限
- **WHEN** 银行端用户设置保险期限
- **THEN** 系统 SHALL 默认保险期限与贷款期限一致
- **AND** 允许用户根据需要调整保险期限
### Requirement: 提交投保申请
银行端 SHALL 支持提交投保申请,系统自动发送给对应的保险公司。
#### Scenario: 提交投保申请
- **WHEN** 银行端用户填写完整的投保信息并提交
- **THEN** 系统 SHALL 创建投保申请记录
- **AND** 系统 SHALL 自动发送投保申请给对应的保险公司
- **AND** 系统 SHALL 显示投保申请已提交的提示
#### Scenario: 投保申请包含银行信息
- **WHEN** 系统发送投保申请给保险公司
- **THEN** 投保申请 SHALL 包含银行相关信息
- **AND** 包含客户的基本信息和贷款信息
- **AND** 包含银行对客户的评估信息
### Requirement: 查看投保申请状态
银行端 SHALL 支持查看投保申请的审核状态。
#### Scenario: 查看待审核状态
- **WHEN** 银行端用户查看投保申请
- **AND** 保险公司尚未审核
- **THEN** 系统 SHALL 显示"待审核"状态
#### Scenario: 查看审核通过状态
- **WHEN** 保险公司核保通过
- **THEN** 系统 SHALL 显示"审核通过"状态
- **AND** 显示保险单信息
#### Scenario: 查看审核拒绝状态
- **WHEN** 保险公司核保拒绝
- **THEN** 系统 SHALL 显示"审核拒绝"状态
- **AND** 显示拒绝原因
### Requirement: 保险单管理
银行端 SHALL 支持查看和管理保险单。
#### Scenario: 查看保险单详情
- **WHEN** 银行端用户点击保险单
- **THEN** 系统 SHALL 显示保险单的完整信息
- **AND** 包括保单号、保险公司、保险金额、保险期限等
#### Scenario: 保险单与贷款关联
- **WHEN** 银行端用户查看贷款详情
- **THEN** 系统 SHALL 显示关联的保险单信息(如果存在)
### Requirement: 理赔申请功能
银行端 SHALL 支持发起理赔申请并上传理赔材料。
#### Scenario: 发起理赔申请
- **WHEN** 银行端用户选择一个保险单并发起理赔申请
- **THEN** 系统 SHALL 显示理赔申请表单
- **AND** 要求填写理赔金额和理赔原因
#### Scenario: 上传理赔材料
- **WHEN** 银行端用户提交理赔申请
- **THEN** 系统 SHALL 要求上传理赔材料
- **AND** 支持上传多个文件
- **AND** 验证文件格式和大小
#### Scenario: 提交理赔申请到保险公司
- **WHEN** 银行端用户提交理赔申请
- **THEN** 系统 SHALL 创建理赔申请记录
- **AND** 系统 SHALL 发送理赔申请给对应的保险公司
- **AND** 系统 SHALL 显示理赔申请已提交的提示
### Requirement: 查看理赔申请状态
银行端 SHALL 支持查看理赔申请的审核状态。
#### Scenario: 查看待审核状态
- **WHEN** 银行端用户查看理赔申请
- **AND** 保险公司尚未审核
- **THEN** 系统 SHALL 显示"待审核"状态
#### Scenario: 查看审核通过状态
- **WHEN** 保险公司理赔审核通过
- **THEN** 系统 SHALL 显示"审核通过"状态
- **AND** 显示赔付金额和赔付日期
#### Scenario: 查看审核拒绝状态
- **WHEN** 保险公司理赔审核拒绝
- **THEN** 系统 SHALL 显示"审核拒绝"状态
- **AND** 显示拒绝原因

View File

@@ -0,0 +1,172 @@
# Bank Insurance UI Specification
## Overview
This specification defines the UI requirements for the bank-side insurance functionality integration, including insurance information display, navigation flows, and workflow step visualization.
## ADDED Requirements
### Requirement: Audit Detail Insurance Display
The audit detail page MUST display insurance information based on the loan status.
#### Scenario: Display insurance information for insured loans
**Given** a loan application with status `DISBURSED` and insurance policy exists
**When** the user views the audit detail page
**Then** the system SHALL display:
- Insurance status as "保障中"
- Insurance company name
- Insurance product name
- Policy number
- Insurance amount matching the loan amount
- Insurance term matching the loan term
- "申请理赔" (Apply for Claim) button
#### Scenario: Display purchase insurance option for uninsured loans
**Given** a loan application with status `DISBURSED` and no insurance policy
**When** the user views the audit detail page
**Then** the system SHALL display:
- "购买保险" (Purchase Insurance) button
- No insurance policy details section
### Requirement: Insurance Navigation
The audit detail page MUST provide navigation to insurance-related pages.
#### Scenario: Navigate to insurance application creation
**Given** a loan application with status `DISBURSED`
**When** the user clicks the "购买保险" button
**Then** the system SHALL navigate to `/pagesBank/insurance/application/create` with query parameters:
- `loanId`: The loan application ID
- `loanAmount`: The loan amount in yuan
- `loanTerm`: The loan term in months
#### Scenario: Navigate to claim application creation
**Given** a loan application with an active insurance policy
**When** the user clicks the "申请理赔" button
**Then** the system SHALL navigate to `/pagesBank/insurance/claim/create` with query parameters:
- `loanId`: The loan application ID
- `policyId`: The insurance policy ID
- `policyNumber`: The insurance policy number
### Requirement: Workflow Step Insurance Node
The workflow step bar MUST conditionally display an insurance node.
#### Scenario: Display insurance node for insured loans
**Given** a loan application with status `DISBURSED` and an active insurance policy
**When** the user views the audit detail page
**Then** the system SHALL display an "投保" (Insurance) node in the workflow step bar
**And** the insurance node SHALL appear between the "审批" (Approval) and "签约" (Signing) nodes
**And** the insurance node SHALL be marked as completed
#### Scenario: Hide insurance node for uninsured loans
**Given** a loan application with status `DISBURSED` and no insurance policy
**When** the user views the audit detail page
**Then** the system SHALL NOT display an "投保" (Insurance) node in the workflow step bar
### Requirement: Dashboard Insurance Shortcuts
The bank dashboard MUST provide insurance management shortcuts.
#### Scenario: Display insurance shortcuts on dashboard
**Given** the user is on the bank dashboard page
**When** the page loads
**Then** the system SHALL display two insurance-related shortcuts in the quick actions area:
- "投保管理" (Insurance Management) with icon `i-carbon-security`, navigating to `/pagesBank/insurance/application/list`
- "理赔管理" (Claim Management) with icon `i-carbon-money`, navigating to `/pagesBank/insurance/claim/list`
### Requirement: Audit List Insurance Shortcuts Removal
The audit list page MUST NOT contain insurance shortcuts.
#### Scenario: Remove insurance shortcuts from audit list
**Given** the user is on the bank audit list page
**When** the page loads
**Then** the system SHALL NOT display the insurance actions section that was previously located above the audit list
## Modified Requirements
### Modified: Audit Detail Insurance Information Display
The insurance information section on the audit detail page is enhanced to support multiple insurance states.
**Previous Behavior**: Displayed static insurance information
**New Behavior**:
- Shows insurance policy details only when insurance exists
- Shows "购买保险" button when no insurance exists
- Shows "申请理赔" button when insurance exists and is active
- Conditionally shows insurance node in workflow steps
## Implementation Notes
### File Locations
- `src/pagesBank/audit/detail.vue`: Audit detail page with insurance display
- `src/pagesBank/audit/list.vue`: Audit list page (shortcuts removed)
- `src/pagesBank/dashboard/index.vue`: Dashboard page (shortcuts added)
### Key Functions
```typescript
// Navigation to insurance application creation
function handleBuyInsurance() {
uni.navigateTo({
url: `/pagesBank/insurance/application/create?loanId=${id.value}&loanAmount=${amount}&loanTerm=${term}`,
})
}
// Navigation to claim application creation
function handleApplyClaim() {
uni.navigateTo({
url: `/pagesBank/insurance/claim/create?loanId=${id.value}&policyId=${policyId}&policyNumber=${policyNumber}`,
})
}
```
### Workflow Steps Configuration
```typescript
const steps = [
{ key: 'SUBMITTED', label: '申请' },
{ key: 'ACCEPTED', label: '受理' },
{ key: 'INVESTIGATING', label: '调查' },
{ key: 'APPROVING', label: '审批' },
{ key: 'INSURANCE', label: '投保', condition: (detail) => detail.hasInsurance },
{ key: 'SIGNING', label: '签约' },
{ key: 'DISBURSED', label: '放款' },
]
```
## Related Capabilities
- `bank-insurance-integration`: Core insurance integration functionality
- `insurance-underwriting`: Insurance underwriting workflow
- `insurance-claim-review`: Insurance claim review workflow

View File

@@ -0,0 +1,104 @@
# government-bad-loans Specification Delta
## ADDED Requirements
### Requirement: 查看贷款信息
政务端 SHALL 支持查看银行端的所有贷款信息,包括不良贷款。
#### Scenario: 查看贷款列表
- **WHEN** 政务端用户访问贷款列表页面
- **THEN** 系统 SHALL 显示所有银行的贷款信息
- **AND** 每个贷款显示银行名称、客户姓名、贷款金额、贷款状态
#### Scenario: 识别不良贷款
- **WHEN** 政务端用户查看贷款列表
- **THEN** 系统 SHALL 标识不良贷款
- **AND** 不良贷款 SHALL 显示特殊标识或颜色区分
#### Scenario: 筛选不良贷款
- **WHEN** 政务端用户选择筛选条件
- **THEN** 系统 SHALL 支持按贷款状态筛选
- **AND** 支持单独查看不良贷款列表
### Requirement: 查看完整业务流程信息
政务端 SHALL 支持查看完整的贷款业务流程信息。
#### Scenario: 查看用户信息
- **WHEN** 政务端用户查看贷款详情
- **THEN** 系统 SHALL 显示用户的基本信息
- **AND** 包括姓名、身份证号、联系方式
#### Scenario: 查看贷款信息
- **WHEN** 政务端用户查看贷款详情
- **THEN** 系统 SHALL 显示贷款的完整信息
- **AND** 包括贷款金额、贷款期限、贷款类型、贷款状态
#### Scenario: 查看保险信息
- **WHEN** 政务端用户查看贷款详情
- **AND** 该贷款购买了保险
- **THEN** 系统 SHALL 显示保险相关信息
- **AND** 包括保险公司、保险产品、保险金额、保险期限、保险单状态
#### Scenario: 查看投保申请记录
- **WHEN** 政务端用户查看贷款详情
- **AND** 该贷款有投保申请记录
- **THEN** 系统 SHALL 显示投保申请的完整记录
- **AND** 包括投保时间、核保结果、核保时间
#### Scenario: 查看理赔信息
- **WHEN** 政务端用户查看贷款详情
- **AND** 该贷款有理赔记录
- **THEN** 系统 SHALL 显示理赔相关信息
- **AND** 包括理赔申请时间、理赔金额、理赔审核结果、赔付金额
### Requirement: 查看审批流程记录
政务端 SHALL 支持查看贷款相关的审批流程记录。
#### Scenario: 查看贷款审批记录
- **WHEN** 政务端用户查看贷款详情
- **THEN** 系统 SHALL 显示贷款审批的完整流程
- **AND** 包括审批人、审批时间、审批结果、审批意见
#### Scenario: 查看保险核保记录
- **WHEN** 政务端用户查看贷款详情
- **AND** 该贷款有保险核保记录
- **THEN** 系统 SHALL 显示保险核保的完整记录
- **AND** 包括核保人、核保时间、核保结果、核保意见
#### Scenario: 查看理赔审核记录
- **WHEN** 政务端用户查看贷款详情
- **AND** 该贷款有理赔审核记录
- **THEN** 系统 SHALL 显示理赔审核的完整记录
- **AND** 包括审核人、审核时间、审核结果、审核意见
### Requirement: 不良贷款详情查看
政务端 SHALL 支持查看不良贷款的详细信息。
#### Scenario: 查看不良贷款基本信息
- **WHEN** 政务端用户点击不良贷款
- **THEN** 系统 SHALL 显示不良贷款的基本信息
- **AND** 包括逾期天数、逾期金额、当前状态
#### Scenario: 查看不良贷款保险情况
- **WHEN** 政务端用户查看不良贷款详情
- **AND** 该不良贷款购买了保险
- **THEN** 系统 SHALL 显示保险情况
- **AND** 包括是否已发起理赔、理赔状态、赔付金额
#### Scenario: 查看不良贷款处理记录
- **WHEN** 政务端用户查看不良贷款详情
- **THEN** 系统 SHALL 显示不良贷款的处理记录
- **AND** 包括催收记录、理赔记录、处置记录
### Requirement: 按银行筛选贷款
政务端 SHALL 支持按银行筛选贷款信息。
#### Scenario: 选择银行查看贷款
- **WHEN** 政务端用户选择一个银行
- **THEN** 系统 SHALL 显示该银行的所有贷款信息
- **AND** 包括正常贷款和不良贷款
#### Scenario: 查看银行不良贷款统计
- **WHEN** 政务端用户查看银行贷款信息
- **THEN** 系统 SHALL 显示该银行的不良贷款统计
- **AND** 包括不良贷款数量、不良贷款金额、不良贷款率

View File

@@ -0,0 +1,99 @@
# insurance-claim-review Specification Delta
## ADDED Requirements
### Requirement: 接收理赔申请
保险端 SHALL 接收银行端提交的理赔申请。
#### Scenario: 查看待理赔审核列表
- **WHEN** 保险端理赔审核人员登录系统
- **THEN** 系统 SHALL 显示待审核的理赔申请列表
- **AND** 每个申请显示银行名称、保险单号、理赔金额、理赔原因
#### Scenario: 查看理赔申请详情
- **WHEN** 保险端理赔审核人员点击理赔申请
- **THEN** 系统 SHALL 显示理赔申请的完整信息
- **AND** 包括银行信息、保险单信息、理赔金额、理赔原因
### Requirement: 查看理赔材料
保险端 SHALL 支持理赔审核人员查看银行上传的理赔材料。
#### Scenario: 查看材料列表
- **WHEN** 保险端理赔审核人员查看理赔申请详情
- **THEN** 系统 SHALL 显示理赔材料列表
- **AND** 每个材料显示文件名、文件类型、上传时间
#### Scenario: 下载和预览材料
- **WHEN** 保险端理赔审核人员点击理赔材料
- **THEN** 系统 SHALL 支持下载或预览材料
- **AND** 支持常见的文件格式图片、PDF、文档等
### Requirement: 理赔材料审核
保险端 SHALL 支持理赔审核人员对材料进行审核。
#### Scenario: 审核通过
- **WHEN** 保险端理赔审核人员审核理赔申请
- **AND** 确认理赔材料完整且符合要求
- **THEN** 系统 SHALL 允许审核人员选择"通过"
- **AND** 系统 SHALL 要求填写赔付金额
#### Scenario: 审核拒绝
- **WHEN** 保险端理赔审核人员审核理赔申请
- **AND** 发现理赔材料不完整或不符合要求
- **THEN** 系统 SHALL 允许审核人员选择"拒绝"
- **AND** 系统 SHALL 要求填写拒绝原因
#### Scenario: 保存赔付金额
- **WHEN** 保险端理赔审核人员审核通过理赔申请
- **THEN** 系统 SHALL 保存赔付金额
- **AND** 赔付金额 SHALL 返回给银行端
#### Scenario: 保存拒绝原因
- **WHEN** 保险端理赔审核人员拒绝理赔申请
- **THEN** 系统 SHALL 保存拒绝原因
- **AND** 拒绝原因 SHALL 返回给银行端
### Requirement: 执行赔付
保险端 SHALL 在理赔审核通过后执行赔付。
#### Scenario: 自动执行赔付
- **WHEN** 理赔申请审核通过
- **THEN** 系统 SHALL 自动执行赔付操作
- **AND** 系统 SHALL 记录赔付日期和赔付金额
#### Scenario: 赔付结果通知
- **WHEN** 赔付执行成功
- **THEN** 系统 SHALL 将赔付结果通知银行端
- **AND** 银行端 SHALL 能够查看赔付详情
### Requirement: 理赔审核历史记录
保险端 SHALL 保存理赔审核历史记录,支持查询和追溯。
#### Scenario: 查看理赔审核历史
- **WHEN** 保险端理赔审核人员查看已处理的理赔申请
- **THEN** 系统 SHALL 显示理赔审核历史记录
- **AND** 包括审核人员、审核时间、审核结果
#### Scenario: 查看拒绝原因历史
- **WHEN** 保险端理赔审核人员查看已拒绝的理赔申请
- **THEN** 系统 SHALL 显示拒绝原因
- **AND** 支持查看详细的拒绝理由
### Requirement: 理赔审核模拟数据
系统 SHALL 提供理赔审核的模拟数据以支持开发和测试。
#### Scenario: 模拟数据包含多种状态
- **WHEN** 系统加载理赔审核模拟数据
- **THEN** 数据 SHALL 包含待审核、已通过、已拒绝三种状态的理赔申请
- **AND** 每种状态至少包含一条记录
#### Scenario: 模拟数据包含完整信息
- **WHEN** 系统加载理赔审核模拟数据
- **THEN** 每条记录 SHALL 包含理赔申请号、银行信息、保险单号、保险公司信息
- **AND** 包含理赔金额、理赔原因、材料列表(文件名、文件类型、大小、上传时间)
- **AND** 包含申请时间、审核时间、审核人员、赔付金额、赔付日期、拒绝原因
#### Scenario: 模拟数据支持筛选
- **WHEN** 保险端理赔审核人员按状态筛选理赔申请
- **THEN** 系统 SHALL 返回对应状态的模拟数据
- **AND** 支持待审核、已通过、已拒绝三种状态筛选

View File

@@ -0,0 +1,268 @@
# Insurance Navigation Specification
## Overview
This specification defines the insurance navigation functionality for the bank side, including insurance application list, company/product selection flows, and dashboard shortcuts.
## ADDED Requirements
### Requirement: Insurance Application List Page
The bank side MUST provide a dedicated page to list all insurance applications with search and pagination support.
#### Scenario: Display insurance application list
**Given** the user navigates to `/pagesBank/insurance/application/list`
**When** the page loads
**Then** the system SHALL display a list of all insurance applications with:
- Application ID
- Company name
- Product name
- Insurance amount
- Insurance term
- Status (pending/approved/rejected)
- Created timestamp
- Clickable items that navigate to application detail
#### Scenario: Filter insurance applications by status
**Given** the user is on the insurance application list page
**When** the user selects a status filter (All, Pending, Approved, Rejected)
**Then** the system SHALL display only applications matching the selected status
#### Scenario: Search insurance applications
**Given** the user is on the insurance application list page
**When** the user enters a search term in the search box
**Then** the system SHALL filter applications by:
- Application ID
- Company name
- Product name
- Policy number
**And** display only matching applications in real-time
#### Scenario: Paginate insurance applications
**Given** the user is on the insurance application list page with more than 20 applications
**When** the user scrolls to the bottom of the list
**Then** the system SHALL load the next page of applications automatically
**And** display a loading indicator while fetching
### Requirement: Company and Product Selection Flow
The audit detail page MUST support a multi-step navigation flow for selecting insurance company and product with search functionality.
#### Scenario: Navigate from audit detail to company selection
**Given** a loan application with status `DISBURSED` and no insurance
**When** the user clicks "购买保险" button
**Then** the system SHALL navigate to `/pagesBank/insurance/company/select` with parameters:
- `loanId`: The loan application ID
- `loanAmount`: The loan amount in yuan
- `loanTerm`: The loan term in months
#### Scenario: Search and select insurance company
**Given** the user is on the insurance company selection page
**When** the user enters a search term in the search box
**Then** the system SHALL filter companies by:
- Company name
- Contact information
**And** display only matching companies in real-time
#### Scenario: Select insurance company
**Given** the user selects a company from the list
**When** the selection is confirmed
**Then** the system SHALL navigate to `/pagesBank/insurance/product/select` with parameters:
- `loanId`: The loan application ID
- `companyId`: The selected company ID
- `loanAmount`: The loan amount in yuan
- `loanTerm`: The loan term in months
#### Scenario: Search and select insurance product
**Given** the user is on the insurance product selection page
**When** the user enters a search term in the search box
**Then** the system SHALL filter products by:
- Product name
- Product description
- Product type
**And** display only matching products in real-time
#### Scenario: Select insurance product
**Given** the user selects a product from the list
**When** the selection is confirmed
**Then** the system SHALL navigate to `/pagesBank/insurance/application/create` with parameters:
- `loanId`: The loan application ID
- `companyId`: The selected company ID
- `productId`: The selected product ID
- `loanAmount`: The loan amount in yuan
- `loanTerm`: The loan term in months
### Requirement: Dashboard Shortcuts Navigation
The dashboard shortcuts MUST navigate to the correct insurance management pages.
#### Scenario: Navigate to insurance application list
**Given** the user is on the bank dashboard
**When** the user clicks the "投保管理" shortcut
**Then** the system SHALL navigate to `/pagesBank/insurance/application/list`
#### Scenario: Navigate to claim application list
**Given** the user is on the bank dashboard
**When** the user clicks the "理赔管理" shortcut
**Then** the system SHALL navigate to `/pagesBank/insurance/claim/list`
## Implementation Notes
### File Locations
- `src/pagesBank/insurance/application/list.vue`: Insurance application list page with search and pagination
- `src/pagesBank/insurance/company/select.vue`: Company selection page with search
- `src/pagesBank/insurance/product/select.vue`: Product selection page with search
- `src/pagesBank/audit/detail.vue`: Updated with multi-step navigation
- `src/pagesBank/dashboard/index.vue`: Verified shortcuts configuration
### Key Functions
```typescript
// Navigate to company selection
function handleBuyInsurance() {
uni.navigateTo({
url: `/pagesBank/insurance/company/select?loanId=${id.value}&loanAmount=${amount}&loanTerm=${term}`,
})
}
// Company selection handler
function handleSelectCompany(company: InsuranceCompany) {
uni.navigateTo({
url: `/pagesBank/insurance/product/select?loanId=${loanId.value}&companyId=${company.id}&loanAmount=${loanAmount.value}&loanTerm=${loanTerm.value}`,
})
}
// Product selection handler
function handleSelectProduct(product: InsuranceProduct) {
uni.navigateTo({
url: `/pagesBank/insurance/application/create?loanId=${loanId.value}&companyId=${companyId.value}&productId=${product.id}&loanAmount=${loanAmount.value}&loanTerm=${loanTerm.value}`,
})
}
// Search handler for lists
function handleSearch(keyword: string) {
searchKeyword.value = keyword
loadData(1) // Reset to first page
}
// Pagination handler
function handleLoadMore() {
if (!loading.value && hasMore.value) {
loadData(currentPage.value + 1)
}
}
```
### Page Flow Diagram
```
/pagesBank/audit/detail
├─→ "购买保险" button
/pagesBank/insurance/company/select (NEW)
├─→ Search companies by name/contact
├─→ Select company
/pagesBank/insurance/product/select (NEW)
├─→ Search products by name/description/type
├─→ Select product
/pagesBank/insurance/application/create (existing)
└─→ Submit application
/pagesBank/dashboard/index
├─→ "投保管理" → /pagesBank/insurance/application/list (NEW)
│ ├─→ Search by ID/company/product/policy
│ ├─→ Filter by status
│ └─→ Pagination support
└─→ "理赔管理" → /pagesBank/insurance/claim/list (existing)
```
### UI Components
#### Search Bar Component
```vue
<template>
<view class="search-bar">
<input
v-model="keyword"
placeholder="搜索公司名称、联系人..."
@input="handleSearch"
/>
<text v-if="keyword" class="clear-btn" @click="clearSearch">×</text>
</view>
</template>
```
#### Status Filter Component
```vue
<template>
<view class="status-filter">
<view
v-for="status in statusOptions"
:key="status.value"
class="filter-item"
:class="{ active: currentStatus === status.value }"
@click="selectStatus(status.value)"
>
{{ status.label }}
</view>
</view>
</template>
```
## Related Capabilities
- `bank-insurance-integration`: Core insurance integration functionality
- `bank-insurance-ui`: Insurance UI requirements

View File

@@ -0,0 +1,99 @@
# insurance-underwriting Specification Delta
## ADDED Requirements
### Requirement: 接收投保申请
保险端 SHALL 接收银行端提交的投保申请。
#### Scenario: 查看待核保申请列表
- **WHEN** 保险端核保人员登录系统
- **THEN** 系统 SHALL 显示待核保的投保申请列表
- **AND** 每个申请显示银行名称、客户姓名、贷款金额、保险产品
#### Scenario: 查看投保申请详情
- **WHEN** 保险端核保人员点击投保申请
- **THEN** 系统 SHALL 显示投保申请的完整信息
- **AND** 包括银行信息、客户信息、贷款信息、保险产品信息
### Requirement: 查看银行提供的信息
保险端 SHALL 支持核保人员查看银行提供的客户情况和相关信息。
#### Scenario: 查看客户基本信息
- **WHEN** 保险端核保人员查看投保申请详情
- **THEN** 系统 SHALL 显示客户的基本信息
- **AND** 包括姓名、身份证号、信用评分
#### Scenario: 查看贷款信息
- **WHEN** 保险端核保人员查看投保申请详情
- **THEN** 系统 SHALL 显示贷款相关信息
- **AND** 包括贷款金额、贷款期限、贷款类型
#### Scenario: 查看银行评估信息
- **WHEN** 保险端核保人员查看投保申请详情
- **THEN** 系统 SHALL 显示银行对客户的评估信息
- **AND** 包括银行的风险评估结果
### Requirement: 核保审核
保险端 SHALL 支持核保人员进行承保条件审核。
#### Scenario: 核保通过
- **WHEN** 保险端核保人员审核投保申请
- **AND** 核保人员确认承保条件满足
- **THEN** 系统 SHALL 允许核保人员选择"通过"
- **AND** 系统 SHALL 更新投保申请状态为"已通过"
#### Scenario: 核保拒绝
- **WHEN** 保险端核保人员审核投保申请
- **AND** 核保人员发现承保条件不满足
- **THEN** 系统 SHALL 允许核保人员选择"拒绝"
- **AND** 系统 SHALL 要求填写拒绝原因
#### Scenario: 保存拒绝原因
- **WHEN** 保险端核保人员拒绝投保申请
- **THEN** 系统 SHALL 保存拒绝原因
- **AND** 拒绝原因 SHALL 返回给银行端
### Requirement: 出具保险单
保险端 SHALL 在核保通过后出具保险单。
#### Scenario: 自动生成保险单
- **WHEN** 投保申请核保通过
- **THEN** 系统 SHALL 自动生成保险单
- **AND** 保险单 SHALL 包含保单号、保险公司信息、被保险人信息、保险金额、保险期限
#### Scenario: 保险单发送给银行
- **WHEN** 保险单生成成功
- **THEN** 系统 SHALL 将保险单信息发送给银行端
- **AND** 银行端 SHALL 能够查看保险单详情
### Requirement: 核保历史记录
保险端 SHALL 保存核保历史记录,支持查询和追溯。
#### Scenario: 查看核保历史
- **WHEN** 保险端核保人员查看已处理的投保申请
- **THEN** 系统 SHALL 显示核保历史记录
- **AND** 包括核保人员、核保时间、核保结果
#### Scenario: 查看拒绝原因历史
- **WHEN** 保险端核保人员查看已拒绝的投保申请
- **THEN** 系统 SHALL 显示拒绝原因
- **AND** 支持查看详细的拒绝理由
### Requirement: 核保申请模拟数据
系统 SHALL 提供核保申请的模拟数据以支持开发和测试。
#### Scenario: 模拟数据包含多种状态
- **WHEN** 系统加载核保申请模拟数据
- **THEN** 数据 SHALL 包含待审核、已通过、已拒绝三种状态的投保申请
- **AND** 每种状态至少包含一条记录
#### Scenario: 模拟数据包含完整信息
- **WHEN** 系统加载核保申请模拟数据
- **THEN** 每条记录 SHALL 包含投保申请号、银行信息、保险公司信息、保险产品信息
- **AND** 包含客户信息(姓名、身份证号、信用评分、贷款金额、贷款期限)
- **AND** 包含保险金额、保险期限、申请时间、审核时间、审核人员、拒绝原因
#### Scenario: 模拟数据支持筛选
- **WHEN** 保险端核保人员按状态筛选投保申请
- **THEN** 系统 SHALL 返回对应状态的模拟数据
- **AND** 支持待审核、已通过、已拒绝三种状态筛选

View File

@@ -0,0 +1,150 @@
# Implementation Tasks
## Phase 0: UI/UX Enhancements (Base Completed + This Optimization)
### 已完成的基础工作
- [x] Bank: Update Audit List UI (`src/pagesBank/audit/list.vue`)
- [x] Bank: Update Audit Detail UI (`src/pagesBank/audit/detail.vue`)
- [x] Insurance: Update Policy List UI (`src/pagesInsurance/policy/list.vue`)
- [x] Insurance: Update Claim List UI (`src/pagesInsurance/claim/list.vue`)
- [x] Government: Update Bank Detail UI & Mock Data (`src/pagesGovernment/bank/detail.vue`)
### 本次优化任务integrate-insurance-flow
- [x] 1.1 优化审核详情页面模拟数据,增加多种保险状态展示
- [x] 1.2 实现"购买保险"按钮跳转到投保申请页面
- [x] 1.3 实现"申请理赔"按钮跳转到理赔申请页面
- [x] 1.4 在流程步骤条中增加"投保"节点(条件显示)
- [x] 1.5 从审核列表页面移除保险功能快捷入口
- [x] 1.6 在工作台首页添加保险功能快捷入口
### 本次新增任务(保险端模拟数据)
- [x] 1.7 为保险端核保列表添加模拟数据
- [x] 1.7.1 在 `src/api/insurance.ts` 中添加 4 条核保申请模拟数据
- [x] 1.7.2 包含待审核、已通过、已拒绝三种状态
- [x] 1.7.3 包含不同的保险公司、保险产品和客户信息
- [x] 1.7.4 添加对应的保险单数据1条已通过的申请
- [x] 1.8 为保险端理赔审核列表添加模拟数据
- [x] 1.8.1 在 `src/api/insurance.ts` 中添加 3 条理赔审核模拟数据
- [x] 1.8.2 包含待审核、已通过、已拒绝三种状态
- [x] 1.8.3 包含不同的理赔原因和材料数量
- [x] 1.8.4 添加赔付金额、赔付日期、拒绝原因等详细信息
- [ ] 1.9 为保险端保单列表扩展模拟数据
- [ ] 1.9.1 在 `src/api/insurance.ts` 中扩展 `mockInsurancePolicies` 数组至12条记录
- [ ] 1.9.2 包含生效中、即将到期、已失效三种状态
- [ ] 1.9.3 包含不同的保险公司、保险产品、银行和客户信息
- [ ] 1.9.4 实现数据多样性(不同的保额、期限、日期)
- [ ] 1.9.5 确保数据与核保申请和理赔申请的关联性
### 本次新增任务(保险公司/产品选择流程)
- [x] 1.10 创建投保申请列表页面 (`src/pagesBank/insurance/application/list.vue`)
- [x] 1.10.1 实现页面基础结构和布局
- [x] 1.10.2 实现状态筛选功能(全部/待审核/已通过/已拒绝)
- [x] 1.10.3 实现搜索功能按ID/公司/产品/保单号)
- [x] 1.10.4 实现分页加载功能每页20条
- [x] 1.10.5 实现列表项点击跳转详情
- [x] 1.11 创建保险公司选择页面 (`src/pagesBank/insurance/company/select.vue`)
- [x] 1.11.1 实现页面基础结构和布局
- [x] 1.11.2 实现搜索功能(按名称/联系方式)
- [x] 1.11.3 显示公司详细信息和合作产品数量
- [x] 1.11.4 实现选择后带参数跳转
- [x] 1.12 创建保险产品选择页面 (`src/pagesBank/insurance/product/select.vue`)
- [x] 1.12.1 实现页面基础结构和布局
- [x] 1.12.2 实现搜索功能(按名称/描述/类型)
- [x] 1.12.3 显示产品详细信息(名称/类型/金额范围)
- [x] 1.12.4 实现产品详情弹窗
- [x] 1.12.5 实现选择后带参数跳转
- [x] 1.13 修改审核详情页投保流程,支持多步导航选择
- [x] 1.14 修改投保申请创建页面,支持接收公司/产品ID参数
## 1. 数据模型和数据库设计
- [ ] 2.1 设计并创建保险公司数据表
- [ ] 2.2 设计并创建保险产品数据表
- [ ] 2.3 设计并创建投保申请数据表
- [ ] 2.4 设计并创建保险单数据表
- [ ] 2.5 设计并创建理赔申请数据表
- [ ] 2.6 扩展贷款数据表,添加保险关联字段
- [ ] 2.7 创建数据库迁移脚本
- [ ] 2.8 编写 TypeScript 类型定义
## 2. 后端 API 开发
- [ ] 3.1 实现获取合作保险公司列表 API
- [ ] 3.2 实现获取保险产品列表 API
- [ ] 3.3 实现创建投保申请 API
- [ ] 3.4 实现获取投保申请详情 API
- [ ] 3.5 实现获取待核保申请列表 API保险端
- [ ] 3.6 实现核保审核 API保险端
- [ ] 3.7 实现保险单生成 API
- [ ] 3.8 实现创建理赔申请 API
- [ ] 3.9 实现获取理赔申请详情 API
- [ ] 3.10 实现获取待理赔审核列表 API保险端
- [ ] 3.11 实现理赔审核 API保险端
- [ ] 3.12 实现赔付执行 API
- [ ] 3.13 实现政务端获取贷款列表 API含保险信息
- [ ] 3.14 实现政务端获取贷款详情 API含完整业务流程
- [ ] 3.15 实现政务端获取不良贷款列表 API
- [ ] 3.16 添加文件上传 API理赔材料
## 3. 银行端页面开发
- [ ] 4.1 创建投保申请列表页面 (`src/pagesBank/insurance/application/list.vue`)
- [ ] 4.2 创建投保申请详情页面 (`src/pagesBank/insurance/application/detail.vue`)
- [ ] 4.3 创建保险公司选择页面 (`src/pagesBank/insurance/company/select.vue`)
- [ ] 4.4 创建保险产品选择页面 (`src/pagesBank/insurance/product/select.vue`)
- [ ] 4.5 修改投保申请创建页面,支持参数接收
- [ ] 4.6 创建保险单详情页面 (`src/pagesBank/insurance/policy/detail.vue`)
- [ ] 4.7 创建理赔申请创建页面 (`src/pagesBank/insurance/claim/create.vue`)
- [ ] 4.8 创建理赔申请列表页面 (`src/pagesBank/insurance/claim/list.vue`)
- [ ] 4.9 在贷款审核详情页添加保险购买入口
- [ ] 4.10 在贷款详情页显示保险信息
- [ ] 4.11 实现保险公司选择器组件
- [ ] 4.12 实现保险产品选择器组件
- [ ] 4.13 实现理赔材料上传组件
## 4. 保险端页面开发
- [ ] 5.1 创建待核保申请列表页面 (`src/pagesInsurance/underwriting/list.vue`)
- [ ] 5.2 创建核保申请详情页面 (`src/pagesInsurance/underwriting/detail.vue`)
- [ ] 5.3 实现核保审核表单组件
- [ ] 5.4 创建待理赔审核列表页面 (`src/pagesInsurance/claim-review/list.vue`)
- [ ] 5.5 创建理赔审核详情页面 (`src/pagesInsurance/claim-review/detail.vue`)
- [ ] 5.6 实现理赔材料查看组件
- [ ] 5.7 实现理赔审核表单组件
## 5. 政务端页面开发
- [ ] 6.1 修改银行贷款列表页面,显示保险信息标识
- [ ] 6.2 修改银行贷款详情页面,显示完整业务流程信息
- [ ] 6.3 添加不良贷款标识显示
- [ ] 6.4 添加保险信息展示区域
- [ ] 6.5 添加投保申请记录展示
- [ ] 6.6 添加理赔信息展示
- [ ] 6.7 添加审批流程记录展示
- [ ] 6.8 实现按银行筛选贷款功能
- [ ] 6.9 实现不良贷款筛选功能
## 6. Mock 数据开发
- [x] 7.1 创建保险公司 Mock 数据
- [x] 7.2 创建保险产品 Mock 数据
- [x] 7.3 创建投保申请 Mock 数据(包含保险端核保列表数据)
- [x] 7.4 创建保险单 Mock 数据
- [x] 7.5 创建理赔申请 Mock 数据(包含保险端理赔审核列表数据)
- [ ] 7.6 创建不良贷款 Mock 数据
## 7. API 集成和测试
- [ ] 8.1 银行端 API 集成测试
- [ ] 8.2 保险端 API 集成测试
- [ ] 8.3 政务端 API 集成测试
- [ ] 8.4 文件上传功能测试
- [ ] 8.5 端到端流程测试(投保 → 核保 → 理赔)
## 8. 样式和用户体验优化
- [ ] 9.1 银行端页面样式优化
- [ ] 9.2 保险端页面样式优化
- [ ] 9.3 政务端页面样式优化
- [ ] 9.4 添加加载状态和错误提示
- [ ] 9.5 添加表单验证提示
## 9. 文档和部署
- [ ] 10.1 更新 API 文档
- [ ] 10.2 更新用户操作手册
- [ ] 10.3 准备部署配置
- [ ] 10.4 执行数据库迁移
- [ ] 10.5 部署到测试环境
- [ ] 10.6 验证测试环境功能

872
src/api/insurance.ts Normal file
View File

@@ -0,0 +1,872 @@
/**
* 保险相关 API 接口
*/
import type {
BankLoanWithInsurance,
ClaimApplication,
ClaimReviewRequest,
CreateClaimApplicationRequest,
CreateInsuranceApplicationRequest,
InsuranceApplication,
InsuranceCompany,
InsurancePolicy,
InsuranceProduct,
UnderwritingReviewRequest,
} from '@/api/types/insurance'
// Mock 数据存储
const mockInsuranceCompanies: InsuranceCompany[] = [
{
id: 'IC001',
name: '中国人民财产保险股份有限公司',
contactInfo: '400-1234567',
status: 'active',
},
{
id: 'IC002',
name: '中国平安财产保险股份有限公司',
contactInfo: '400-7654321',
status: 'active',
},
{
id: 'IC003',
name: '中国太平洋财产保险股份有限公司',
contactInfo: '400-9876543',
status: 'active',
},
]
const mockInsuranceProducts: InsuranceProduct[] = [
{
id: 'IP001',
companyId: 'IC001',
companyName: '中国人民财产保险股份有限公司',
name: '个人住房贷款保险',
type: 'housing_loan',
description: '为个人住房贷款提供保险保障,保障贷款人因意外事故导致的还款能力丧失',
minAmount: 100000,
maxAmount: 5000000,
status: 'active',
},
{
id: 'IP002',
companyId: 'IC001',
companyName: '中国人民财产保险股份有限公司',
name: '企业信贷履约保证保险',
type: 'business_credit',
description: '为企业信贷提供履约保证,降低银行信贷风险',
minAmount: 50000,
maxAmount: 10000000,
status: 'active',
},
{
id: 'IP003',
companyId: 'IC002',
companyName: '中国平安财产保险股份有限公司',
name: '小微企业贷款保证保险',
type: 'business_credit',
description: '为小微企业贷款提供保证保险,支持小微企业发展',
minAmount: 30000,
maxAmount: 5000000,
status: 'active',
},
{
id: 'IP004',
companyId: 'IC003',
companyName: '中国太平洋财产保险股份有限公司',
name: '个人消费贷款保险',
type: 'other',
description: '为个人消费贷款提供保险保障',
minAmount: 20000,
maxAmount: 2000000,
status: 'active',
},
]
const mockInsuranceApplications: InsuranceApplication[] = [
{
id: 'IA20250112001',
loanId: 'LA20251226001',
bankId: 'B001',
bankName: '中国工商银行',
companyId: 'IC001',
companyName: '中国人民财产保险股份有限公司',
productId: 'IP001',
productName: '个人住房贷款保险',
customerInfo: {
name: '张三',
idNumber: '440106199001011234',
creditScore: 750,
loanAmount: 500000,
loanTerm: 120,
loanType: 'housing_loan',
},
insuranceAmount: 500000,
insuranceTerm: 120,
status: 'pending',
createdAt: '2025-01-10 14:30:00',
},
{
id: 'IA20250111001',
loanId: 'LA20251226002',
bankId: 'B001',
bankName: '中国工商银行',
companyId: 'IC002',
companyName: '中国平安财产保险股份有限公司',
productId: 'IP003',
productName: '小微企业贷款保证保险',
customerInfo: {
name: '李四',
idNumber: '440106199202025678',
creditScore: 720,
loanAmount: 800000,
loanTerm: 180,
loanType: 'business_credit',
},
insuranceAmount: 800000,
insuranceTerm: 180,
status: 'approved',
createdAt: '2025-01-09 10:15:00',
reviewedAt: '2025-01-10 16:20:00',
reviewedBy: '核保员001',
},
{
id: 'IA20250110001',
loanId: 'LA20251226003',
bankId: 'B002',
bankName: '中国建设银行',
companyId: 'IC003',
companyName: '中国太平洋财产保险股份有限公司',
productId: 'IP004',
productName: '个人消费贷款保险',
customerInfo: {
name: '王五',
idNumber: '440106198803034567',
creditScore: 680,
loanAmount: 300000,
loanTerm: 90,
loanType: 'other',
},
insuranceAmount: 300000,
insuranceTerm: 90,
status: 'rejected',
createdAt: '2025-01-08 09:45:00',
reviewedAt: '2025-01-09 11:30:00',
reviewedBy: '核保员002',
rejectionReason: '客户信用评分低于产品要求最低值700分',
},
{
id: 'IA20250109001',
loanId: 'LA20251226004',
bankId: 'B001',
bankName: '中国工商银行',
companyId: 'IC001',
companyName: '中国人民财产保险股份有限公司',
productId: 'IP002',
productName: '企业信贷履约保证保险',
customerInfo: {
name: '赵六',
idNumber: '440106199504045678',
creditScore: 780,
loanAmount: 600000,
loanTerm: 120,
loanType: 'business_credit',
},
insuranceAmount: 600000,
insuranceTerm: 120,
status: 'pending',
createdAt: '2025-01-07 15:20:00',
},
]
const mockInsurancePolicies: InsurancePolicy[] = [
{
id: 'IP20250111001',
applicationId: 'IA20250111001',
policyNumber: 'POL20250111001',
companyId: 'IC002',
companyName: '中国平安财产保险股份有限公司',
bankId: 'B001',
bankName: '中国工商银行',
loanId: 'LA20251226002',
productId: 'IP003',
productName: '小微企业贷款保证保险',
insuranceAmount: 800000,
insuranceTerm: 180,
startDate: '2025-01-10 16:20:00',
endDate: '2026-07-09 16:20:00',
status: 'active',
issuedAt: '2025-01-10 16:20:00',
},
{
id: 'IP20241220001',
applicationId: 'IA20241220001',
policyNumber: 'POL20241220001',
companyId: 'IC001',
companyName: '中国人民财产保险股份有限公司',
bankId: 'B002',
bankName: '中国建设银行',
loanId: 'LA20241220001',
productId: 'IP001',
productName: '个人住房贷款保险',
insuranceAmount: 500000,
insuranceTerm: 120,
startDate: '2024-12-20 10:30:00',
endDate: '2025-12-20 10:30:00',
status: 'active',
issuedAt: '2024-12-20 10:30:00',
},
{
id: 'IP20241115002',
applicationId: 'IA20241115002',
policyNumber: 'POL20241115002',
companyId: 'IC003',
companyName: '中国太平洋财产保险股份有限公司',
bankId: 'B003',
bankName: '中国农业银行',
loanId: 'LA20241115002',
productId: 'IP004',
productName: '个人消费贷款保险',
insuranceAmount: 300000,
insuranceTerm: 90,
startDate: '2024-11-15 14:45:00',
endDate: '2025-02-15 14:45:00',
status: 'active',
issuedAt: '2024-11-15 14:45:00',
},
{
id: 'IP20241010003',
applicationId: 'IA20241010003',
policyNumber: 'POL20241010003',
companyId: 'IC001',
companyName: '中国人民财产保险股份有限公司',
bankId: 'B004',
bankName: '中国银行',
loanId: 'LA20241010003',
productId: 'IP002',
productName: '企业信贷履约保证保险',
insuranceAmount: 1000000,
insuranceTerm: 240,
startDate: '2024-10-10 09:00:00',
endDate: '2026-10-10 09:00:00',
status: 'active',
issuedAt: '2024-10-10 09:00:00',
},
{
id: 'IP20240905004',
applicationId: 'IA20240905004',
policyNumber: 'POL20240905004',
companyId: 'IC002',
companyName: '中国平安财产保险股份有限公司',
bankId: 'B001',
bankName: '中国工商银行',
loanId: 'LA20240905004',
productId: 'IP003',
productName: '小微企业贷款保证保险',
insuranceAmount: 450000,
insuranceTerm: 120,
startDate: '2024-09-05 16:20:00',
endDate: '2025-09-05 16:20:00',
status: 'expiring',
issuedAt: '2024-09-05 16:20:00',
},
{
id: 'IP20240805005',
applicationId: 'IA20240805005',
policyNumber: 'POL20240805005',
companyId: 'IC003',
companyName: '中国太平洋财产保险股份有限公司',
bankId: 'B002',
bankName: '中国建设银行',
loanId: 'LA20240805005',
productId: 'IP004',
productName: '个人消费贷款保险',
insuranceAmount: 200000,
insuranceTerm: 60,
startDate: '2024-08-05 11:30:00',
endDate: '2025-02-05 11:30:00',
status: 'expiring',
issuedAt: '2024-08-05 11:30:00',
},
{
id: 'IP20240701006',
applicationId: 'IA20240701006',
policyNumber: 'POL20240701006',
companyId: 'IC001',
companyName: '中国人民财产保险股份有限公司',
bankId: 'B003',
bankName: '中国农业银行',
loanId: 'LA20240701006',
productId: 'IP001',
productName: '个人住房贷款保险',
insuranceAmount: 350000,
insuranceTerm: 180,
startDate: '2024-07-01 08:00:00',
endDate: '2025-07-01 08:00:00',
status: 'expiring',
issuedAt: '2024-07-01 08:00:00',
},
{
id: 'IP20240602007',
applicationId: 'IA20240602007',
policyNumber: 'POL20240602007',
companyId: 'IC002',
companyName: '中国平安财产保险股份有限公司',
bankId: 'B004',
bankName: '中国银行',
loanId: 'LA20240602007',
productId: 'IP003',
productName: '小微企业贷款保证保险',
insuranceAmount: 600000,
insuranceTerm: 150,
startDate: '2024-06-02 13:15:00',
endDate: '2024-12-02 13:15:00',
status: 'expired',
issuedAt: '2024-06-02 13:15:00',
},
{
id: 'IP20240503008',
applicationId: 'IA20240503008',
policyNumber: 'POL20240503008',
companyId: 'IC003',
companyName: '中国太平洋财产保险股份有限公司',
bankId: 'B001',
bankName: '中国工商银行',
loanId: 'LA20240503008',
productId: 'IP004',
productName: '个人消费贷款保险',
insuranceAmount: 150000,
insuranceTerm: 90,
startDate: '2024-05-03 10:45:00',
endDate: '2024-08-03 10:45:00',
status: 'expired',
issuedAt: '2024-05-03 10:45:00',
},
{
id: 'IP20240404009',
applicationId: 'IA20240404009',
policyNumber: 'POL20240404009',
companyId: 'IC001',
companyName: '中国人民财产保险股份有限公司',
bankId: 'B002',
bankName: '中国建设银行',
loanId: 'LA20240404009',
productId: 'IP002',
productName: '企业信贷履约保证保险',
insuranceAmount: 750000,
insuranceTerm: 200,
startDate: '2024-04-04 15:30:00',
endDate: '2024-10-04 15:30:00',
status: 'expired',
issuedAt: '2024-04-04 15:30:00',
},
{
id: 'IP20240305010',
applicationId: 'IA20240305010',
policyNumber: 'POL20240305010',
companyId: 'IC002',
companyName: '中国平安财产保险股份有限公司',
bankId: 'B003',
bankName: '中国农业银行',
loanId: 'LA20240305010',
productId: 'IP003',
productName: '小微企业贷款保证保险',
insuranceAmount: 900000,
insuranceTerm: 180,
startDate: '2024-03-05 09:20:00',
endDate: '2025-03-05 09:20:00',
status: 'active',
issuedAt: '2024-03-05 09:20:00',
},
{
id: 'IP20240206011',
applicationId: 'IA20240206011',
policyNumber: 'POL20240206011',
companyId: 'IC003',
companyName: '中国太平洋财产保险股份有限公司',
bankId: 'B004',
bankName: '中国银行',
loanId: 'LA20240206011',
productId: 'IP004',
productName: '个人消费贷款保险',
insuranceAmount: 250000,
insuranceTerm: 120,
startDate: '2024-02-06 14:10:00',
endDate: '2025-02-06 14:10:00',
status: 'active',
issuedAt: '2024-02-06 14:10:00',
},
]
const mockClaimApplications: ClaimApplication[] = [
{
id: 'CA20250112001',
policyId: 'IP20250111001',
policyNumber: 'POL20250111001',
loanId: 'LA20251226002',
bankId: 'B001',
bankName: '中国工商银行',
companyId: 'IC002',
companyName: '中国平安财产保险股份有限公司',
claimAmount: 400000,
claimReason: '借款人逾期超过90天无法偿还贷款本息',
materials: [
{
id: 'CM20250112001',
name: '逾期还款记录.pdf',
url: 'https://example.com/files/overdue_record.pdf',
type: 'application/pdf',
size: 1024000,
uploadTime: '2025-01-12 10:30:00',
},
{
id: 'CM20250112002',
name: '催收记录.docx',
url: 'https://example.com/files/collection_record.docx',
type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
size: 512000,
uploadTime: '2025-01-12 10:31:00',
},
{
id: 'CM20250112003',
name: '借款人财务状况.jpg',
url: 'https://example.com/files/financial_status.jpg',
type: 'image/jpeg',
size: 2048000,
uploadTime: '2025-01-12 10:32:00',
},
],
status: 'pending',
submittedAt: '2025-01-12 10:30:00',
},
{
id: 'CA20250111001',
policyId: 'IP20250111001',
policyNumber: 'POL20250111001',
loanId: 'LA20251226002',
bankId: 'B001',
bankName: '中国工商银行',
companyId: 'IC002',
companyName: '中国平安财产保险股份有限公司',
claimAmount: 200000,
claimReason: '借款人经营困难,申请部分理赔',
materials: [
{
id: 'CM20250111001',
name: '经营困难证明.pdf',
url: 'https://example.com/files/difficulty_proof.pdf',
type: 'application/pdf',
size: 768000,
uploadTime: '2025-01-11 14:20:00',
},
],
status: 'approved',
submittedAt: '2025-01-11 14:20:00',
reviewedAt: '2025-01-12 09:15:00',
reviewedBy: '理赔审核员001',
payoutAmount: 180000,
payoutDate: '2025-01-12 09:20:00',
},
{
id: 'CA20250110001',
policyId: 'IP20250111001',
policyNumber: 'POL20250111001',
loanId: 'LA20251226002',
bankId: 'B001',
bankName: '中国工商银行',
companyId: 'IC002',
companyName: '中国平安财产保险股份有限公司',
claimAmount: 600000,
claimReason: '借款人失联,申请全额理赔',
materials: [
{
id: 'CM20250110001',
name: '失联证明.pdf',
url: 'https://example.com/files/missing_proof.pdf',
type: 'application/pdf',
size: 512000,
uploadTime: '2025-01-10 11:00:00',
},
],
status: 'rejected',
submittedAt: '2025-01-10 11:00:00',
reviewedAt: '2025-01-10 16:30:00',
reviewedBy: '理赔审核员002',
rejectionReason: '提供的失联证明材料不充分,需要补充公安部门出具的正式证明文件',
},
]
// ==================== 银行端 API ====================
/**
* [银行端] 获取合作保险公司列表
*/
export function getInsuranceCompanies() {
return new Promise<{ list: InsuranceCompany[] }>((resolve) => {
setTimeout(() => {
resolve({
list: mockInsuranceCompanies.filter(c => c.status === 'active'),
})
}, 300)
})
}
/**
* [银行端] 获取保险产品列表
*/
export function getInsuranceProducts(companyId?: string) {
return new Promise<{ list: InsuranceProduct[] }>((resolve) => {
setTimeout(() => {
let products = mockInsuranceProducts.filter(p => p.status === 'active')
if (companyId) {
products = products.filter(p => p.companyId === companyId)
}
resolve({ list: products })
}, 300)
})
}
/**
* [银行端] 创建投保申请
*/
export function createInsuranceApplication(data: CreateInsuranceApplicationRequest) {
return new Promise<{ id: string }>((resolve) => {
setTimeout(() => {
const application: InsuranceApplication = {
id: `IA${Date.now()}`,
loanId: data.loanId,
bankId: 'B001',
bankName: '中国工商银行',
companyId: data.companyId,
companyName: mockInsuranceCompanies.find(c => c.id === data.companyId)?.name || '',
productId: data.productId,
productName: mockInsuranceProducts.find(p => p.id === data.productId)?.name || '',
customerInfo: {
name: '张三',
idNumber: '440106199001011234',
creditScore: 750,
loanAmount: 500000,
loanTerm: 120,
loanType: 'business_credit',
},
insuranceAmount: data.insuranceAmount,
insuranceTerm: data.insuranceTerm,
status: 'pending',
createdAt: new Date().toLocaleString(),
}
mockInsuranceApplications.push(application)
resolve({ id: application.id })
}, 500)
})
}
/**
* [银行端] 获取投保申请详情
*/
export function getInsuranceApplicationDetail(id: string) {
return new Promise<InsuranceApplication>((resolve, reject) => {
setTimeout(() => {
const application = mockInsuranceApplications.find(a => a.id === id)
if (application) {
resolve(application)
}
else {
reject(new Error('投保申请不存在'))
}
}, 300)
})
}
/**
* [银行端] 获取保险单详情
*/
export function getInsurancePolicyDetail(id: string) {
return new Promise<InsurancePolicy>((resolve, reject) => {
setTimeout(() => {
const policy = mockInsurancePolicies.find(p => p.id === id)
if (policy) {
resolve(policy)
}
else {
reject(new Error('保险单不存在'))
}
}, 300)
})
}
/**
* [银行端] 创建理赔申请
*/
export function createClaimApplication(data: CreateClaimApplicationRequest) {
return new Promise<{ id: string }>((resolve) => {
setTimeout(() => {
const policy = mockInsurancePolicies.find(p => p.id === data.policyId)
const claim: ClaimApplication = {
id: `CA${Date.now()}`,
policyId: data.policyId,
policyNumber: policy?.policyNumber || '',
loanId: data.loanId,
bankId: 'B001',
bankName: '中国工商银行',
companyId: policy?.companyId || '',
companyName: policy?.companyName || '',
claimAmount: data.claimAmount,
claimReason: data.claimReason,
materials: data.materials.map((file, index) => ({
id: `CM${Date.now()}_${index}`,
name: file.name,
url: URL.createObjectURL(file),
type: file.type,
size: file.size,
uploadTime: new Date().toLocaleString(),
})),
status: 'pending',
submittedAt: new Date().toLocaleString(),
}
mockClaimApplications.push(claim)
resolve({ id: claim.id })
}, 500)
})
}
/**
* [银行端] 获取理赔申请详情
*/
export function getClaimApplicationDetail(id: string) {
return new Promise<ClaimApplication>((resolve, reject) => {
setTimeout(() => {
const claim = mockClaimApplications.find(c => c.id === id)
if (claim) {
resolve(claim)
}
else {
reject(new Error('理赔申请不存在'))
}
}, 300)
})
}
/**
* [银行端] 获取理赔申请列表
*/
export function getClaimApplicationList(params?: { status?: string }) {
return new Promise<{ list: ClaimApplication[] }>((resolve) => {
setTimeout(() => {
let list = [...mockClaimApplications]
if (params?.status) {
list = list.filter(c => c.status === params.status)
}
resolve({ list })
}, 300)
})
}
// ==================== 保险端 API ====================
/**
* [保险端] 获取待核保申请列表
*/
export function getUnderwritingApplications(params?: { status?: string }) {
return new Promise<{ list: InsuranceApplication[] }>((resolve) => {
setTimeout(() => {
let list = [...mockInsuranceApplications]
if (params?.status) {
list = list.filter(a => a.status === params.status)
}
resolve({ list })
}, 300)
})
}
/**
* [保险端] 核保审核
*/
export function reviewUnderwritingApplication(id: string, data: UnderwritingReviewRequest) {
return new Promise<void>((resolve) => {
setTimeout(() => {
const application = mockInsuranceApplications.find(a => a.id === id)
if (application) {
application.status = data.approved ? 'approved' : 'rejected'
application.reviewedAt = new Date().toLocaleString()
application.reviewedBy = '核保员001'
application.rejectionReason = data.rejectionReason
// 如果审核通过,生成保险单
if (data.approved) {
const policy: InsurancePolicy = {
id: `IP${Date.now()}`,
applicationId: application.id,
policyNumber: `POL${Date.now()}`,
companyId: application.companyId,
companyName: application.companyName,
bankId: application.bankId,
bankName: application.bankName,
loanId: application.loanId,
productId: application.productId,
productName: application.productName,
insuranceAmount: application.insuranceAmount,
insuranceTerm: application.insuranceTerm,
startDate: new Date().toLocaleString(),
endDate: new Date(Date.now() + application.insuranceTerm * 30 * 24 * 60 * 60 * 1000).toLocaleString(),
status: 'active',
issuedAt: new Date().toLocaleString(),
}
mockInsurancePolicies.push(policy)
}
}
resolve()
}, 500)
})
}
/**
* [保险端] 获取待理赔审核列表
*/
export function getClaimReviewApplications(params?: { status?: string }) {
return new Promise<{ list: ClaimApplication[] }>((resolve) => {
setTimeout(() => {
let list = [...mockClaimApplications]
if (params?.status) {
list = list.filter(c => c.status === params.status)
}
resolve({ list })
}, 300)
})
}
/**
* [保险端] 理赔审核
*/
export function reviewClaimApplication(id: string, data: ClaimReviewRequest) {
return new Promise<void>((resolve) => {
setTimeout(() => {
const claim = mockClaimApplications.find(c => c.id === id)
if (claim) {
claim.status = data.approved ? 'approved' : 'rejected'
claim.reviewedAt = new Date().toLocaleString()
claim.reviewedBy = '理赔审核员001'
claim.rejectionReason = data.rejectionReason
// 如果审核通过,执行赔付
if (data.approved && data.payoutAmount) {
claim.payoutAmount = data.payoutAmount
claim.payoutDate = new Date().toLocaleString()
}
}
resolve()
}, 500)
})
}
// ==================== 政务端 API ====================
/**
* [政务端] 获取贷款列表(含保险信息)
*/
export function getGovernmentLoanList(params?: { includeInsurance?: boolean, isBadLoan?: boolean }) {
return new Promise<{ list: BankLoanWithInsurance[] }>((resolve) => {
setTimeout(() => {
// Mock 数据
const loans: BankLoanWithInsurance[] = [
{
id: 'LA20251226001',
userId: 'U001',
userName: '张三',
amount: 500000,
term: 120,
status: 'approved',
isBadLoan: false,
},
{
id: 'LA20251226002',
userId: 'U002',
userName: '李四',
amount: 800000,
term: 180,
status: 'disbursed',
insuranceApplication: mockInsuranceApplications[0],
insurancePolicy: mockInsurancePolicies[0],
isBadLoan: false,
},
{
id: 'LA20251226003',
userId: 'U003',
userName: '王五',
amount: 300000,
term: 90,
status: 'disbursed',
insuranceApplication: mockInsuranceApplications[1],
insurancePolicy: mockInsurancePolicies[1],
claimApplications: mockClaimApplications.slice(0, 2),
isBadLoan: true,
badLoanDays: 45,
},
]
let list = [...loans]
if (params?.isBadLoan !== undefined) {
list = list.filter(l => l.isBadLoan === params.isBadLoan)
}
resolve({ list })
}, 500)
})
}
/**
* [政务端] 获取贷款详情(含完整业务流程)
*/
export function getGovernmentLoanDetail(id: string, params?: { full?: boolean }) {
return new Promise<BankLoanWithInsurance>((resolve, reject) => {
setTimeout(() => {
const loan: BankLoanWithInsurance = {
id,
userId: 'U001',
userName: '张三',
amount: 500000,
term: 120,
status: 'disbursed',
insuranceApplication: mockInsuranceApplications[0],
insurancePolicy: mockInsurancePolicies[0],
claimApplications: mockClaimApplications.slice(0, 1),
isBadLoan: false,
}
resolve(loan)
}, 500)
})
}
/**
* [政务端] 获取不良贷款列表
*/
export function getBadLoanList() {
return new Promise<{ list: BankLoanWithInsurance[] }>((resolve) => {
setTimeout(() => {
const loans: BankLoanWithInsurance[] = [
{
id: 'LA20251226003',
userId: 'U003',
userName: '王五',
amount: 300000,
term: 90,
status: 'disbursed',
insuranceApplication: mockInsuranceApplications[1],
insurancePolicy: mockInsurancePolicies[1],
claimApplications: mockClaimApplications.slice(0, 2),
isBadLoan: true,
badLoanDays: 45,
},
{
id: 'LA20251226004',
userId: 'U004',
userName: '赵六',
amount: 600000,
term: 120,
status: 'disbursed',
isBadLoan: true,
badLoanDays: 30,
},
]
resolve({ list: loans })
}, 500)
})
}

191
src/api/types/insurance.ts Normal file
View File

@@ -0,0 +1,191 @@
/**
* 保险相关类型定义
*/
// 保险公司状态
export type InsuranceCompanyStatus = 'active' | 'inactive'
// 保险产品类型
export type InsuranceProductType = 'housing_loan' | 'business_credit' | 'other'
// 投保申请状态
export type InsuranceApplicationStatus = 'pending' | 'approved' | 'rejected'
// 保险单状态
export type InsurancePolicyStatus = 'active' | 'expiring' | 'expired' | 'cancelled'
// 理赔申请状态
export type ClaimApplicationStatus = 'pending' | 'approved' | 'rejected'
/**
* 保险公司
*/
export interface InsuranceCompany {
id: string
name: string
contactInfo: string
status: InsuranceCompanyStatus
}
/**
* 保险产品
*/
export interface InsuranceProduct {
id: string
companyId: string
companyName: string
name: string
type: InsuranceProductType
description: string
minAmount: number
maxAmount: number
status: InsuranceCompanyStatus
}
/**
* 客户信息(用于投保申请)
*/
export interface CustomerInfo {
name: string
idNumber: string
creditScore: number
loanAmount: number
loanTerm: number
loanType: string
}
/**
* 投保申请
*/
export interface InsuranceApplication {
id: string
loanId: string
bankId: string
bankName: string
companyId: string
companyName: string
productId: string
productName: string
customerInfo: CustomerInfo
insuranceAmount: number
insuranceTerm: number
status: InsuranceApplicationStatus
createdAt: string
reviewedAt?: string
reviewedBy?: string
rejectionReason?: string
}
/**
* 保险单
*/
export interface InsurancePolicy {
id: string
applicationId: string
policyNumber: string
companyId: string
companyName: string
bankId: string
bankName: string
loanId: string
productId: string
productName: string
insuranceAmount: number
insuranceTerm: number
startDate: string
endDate: string
status: InsurancePolicyStatus
issuedAt: string
}
/**
* 理赔材料
*/
export interface ClaimMaterial {
id: string
name: string
url: string
type: string
size: number
uploadTime: string
}
/**
* 理赔申请
*/
export interface ClaimApplication {
id: string
policyId: string
policyNumber: string
loanId: string
bankId: string
bankName: string
companyId: string
companyName: string
claimAmount: number
claimReason: string
materials: ClaimMaterial[]
status: ClaimApplicationStatus
submittedAt: string
reviewedAt?: string
reviewedBy?: string
rejectionReason?: string
payoutAmount?: number
payoutDate?: string
}
/**
* 创建投保申请请求
*/
export interface CreateInsuranceApplicationRequest {
loanId: string
companyId: string
productId: string
insuranceAmount: number
insuranceTerm: number
}
/**
* 核保审核请求
*/
export interface UnderwritingReviewRequest {
approved: boolean
rejectionReason?: string
}
/**
* 创建理赔申请请求
*/
export interface CreateClaimApplicationRequest {
policyId: string
loanId: string
claimAmount: number
claimReason: string
materials: File[]
}
/**
* 理赔审核请求
*/
export interface ClaimReviewRequest {
approved: boolean
rejectionReason?: string
payoutAmount?: number
}
/**
* 银行贷款信息(扩展)
*/
export interface BankLoanWithInsurance {
id: string
userId: string
userName: string
amount: number
term: number
status: string
insuranceApplication?: InsuranceApplication
insurancePolicy?: InsurancePolicy
claimApplications?: ClaimApplication[]
isBadLoan: boolean
badLoanDays?: number
}

View File

@@ -296,6 +296,54 @@
"navigationBarTitleText": "拜访详情" "navigationBarTitleText": "拜访详情"
} }
}, },
{
"path": "insurance/application/list",
"style": {
"navigationBarTitleText": "投保申请列表"
}
},
{
"path": "insurance/application/create",
"style": {
"navigationBarTitleText": "创建投保申请"
}
},
{
"path": "insurance/application/detail",
"style": {
"navigationBarTitleText": "投保申请详情"
}
},
{
"path": "insurance/company/select",
"style": {
"navigationBarTitleText": "选择保险公司"
}
},
{
"path": "insurance/product/select",
"style": {
"navigationBarTitleText": "选择保险产品"
}
},
{
"path": "insurance/claim/list",
"style": {
"navigationBarTitleText": "理赔申请列表"
}
},
{
"path": "insurance/claim/create",
"style": {
"navigationBarTitleText": "创建理赔申请"
}
},
{
"path": "insurance/policy/detail",
"style": {
"navigationBarTitleText": "保险单详情"
}
},
{ {
"path": "me/index", "path": "me/index",
"style": { "style": {
@@ -380,6 +428,30 @@
"navigationBarTitleText": "理赔详情" "navigationBarTitleText": "理赔详情"
} }
}, },
{
"path": "underwriting/list",
"style": {
"navigationBarTitleText": "待核保列表"
}
},
{
"path": "underwriting/detail",
"style": {
"navigationBarTitleText": "核保详情"
}
},
{
"path": "claim-review/list",
"style": {
"navigationBarTitleText": "理赔审核列表"
}
},
{
"path": "claim-review/detail",
"style": {
"navigationBarTitleText": "理赔审核详情"
}
},
{ {
"path": "bank/list", "path": "bank/list",
"style": { "style": {

View File

@@ -1,8 +1,8 @@
<script lang="ts" setup> <script lang="ts" setup>
import { getLoanApplicationDetail, operateLoanApplication, queryCreditInfo, getCreditAssessment } from '@/api/loan'
import { LoanStatus } from '@/typings/loan'
import type { LoanApplication, RelatedMerchant } from '@/typings/loan' import type { LoanApplication, RelatedMerchant } from '@/typings/loan'
import { getCreditAssessment, getLoanApplicationDetail, operateLoanApplication, queryCreditInfo } from '@/api/loan'
import { mockTransactions } from '@/pagesBank/mock' import { mockTransactions } from '@/pagesBank/mock'
import { LoanStatus } from '@/typings/loan'
definePage({ definePage({
style: { style: {
@@ -23,15 +23,28 @@ const creditAssessment = ref<any>(null)
const assessmentLoading = ref(false) const assessmentLoading = ref(false)
// 流程步骤定义 // 流程步骤定义
const steps = [ const stepConfig = [
{ key: LoanStatus.SUBMITTED, label: '申请' }, { key: LoanStatus.SUBMITTED, label: '申请' },
{ key: LoanStatus.ACCEPTED, label: '受理' }, { key: LoanStatus.ACCEPTED, label: '受理' },
{ key: LoanStatus.INVESTIGATING, label: '调查' }, { key: LoanStatus.INVESTIGATING, label: '调查' },
{ key: LoanStatus.APPROVING, label: '审批' }, { key: LoanStatus.APPROVING, label: '审批' },
{ key: 'INSURANCE', label: '投保', isInsurance: true },
{ key: LoanStatus.SIGNING, label: '签约' }, { key: LoanStatus.SIGNING, label: '签约' },
{ key: LoanStatus.DISBURSED, label: '放款' } { key: LoanStatus.DISBURSED, label: '放款' },
] ]
const visibleSteps = computed(() => {
if (!detail.value)
return stepConfig
const hasInsurance = detail.value.insuranceInfo?.hasInsurance
if (hasInsurance) {
return stepConfig
}
return stepConfig.filter(step => !step.isInsurance)
})
const steps = computed(() => visibleSteps.value)
// 状态中文映射 // 状态中文映射
const statusTextMap: Record<string, string> = { const statusTextMap: Record<string, string> = {
[LoanStatus.DRAFT]: '草稿', [LoanStatus.DRAFT]: '草稿',
@@ -45,25 +58,31 @@ const statusTextMap: Record<string, string> = {
[LoanStatus.PENDING_SUPPLEMENT]: '驳回要求补充资料', [LoanStatus.PENDING_SUPPLEMENT]: '驳回要求补充资料',
[LoanStatus.SIGNING]: '待签约', [LoanStatus.SIGNING]: '待签约',
[LoanStatus.SIGNED]: '已签约', [LoanStatus.SIGNED]: '已签约',
[LoanStatus.DISBURSED]: '已放款' [LoanStatus.DISBURSED]: '已放款',
} }
// 获取状态中文文本 // 获取状态中文文本
const statusText = computed(() => { const statusText = computed(() => {
if (!detail.value) return '' if (!detail.value)
return ''
return statusTextMap[detail.value.status] || detail.value.status return statusTextMap[detail.value.status] || detail.value.status
}) })
// 获取当前步骤索引 // 获取当前步骤索引
const currentStepIndex = computed(() => { const currentStepIndex = computed(() => {
if (!detail.value) return 0 if (!detail.value)
return 0
const status = detail.value.status const status = detail.value.status
const index = steps.findIndex(s => s.key === status) const index = steps.value.findIndex(s => s.key === status)
// 特殊处理中间状态映射 // 特殊处理中间状态映射
if (status === LoanStatus.REPORTED) return 3 // 上报后进入审批阶段 if (status === LoanStatus.REPORTED)
if (status === LoanStatus.PENDING_SUPPLEMENT) return 2 // 待补充资料,留在中间阶段 return 3 // 上报后进入审批阶段
if (status === LoanStatus.APPROVED) return 4 // 审批通过等待签约 if (status === LoanStatus.PENDING_SUPPLEMENT)
if (status === LoanStatus.SIGNED) return 5 // 签约完成等待放款 return 2 // 待补充资料,留在中间阶段
if (status === LoanStatus.APPROVED)
return 4 // 审批通过等待签约
if (status === LoanStatus.SIGNED)
return 5 // 签约完成等待放款
return index > -1 ? index : 0 return index > -1 ? index : 0
}) })
@@ -72,7 +91,8 @@ async function loadDetail() {
try { try {
const res = await getLoanApplicationDetail(id.value) const res = await getLoanApplicationDetail(id.value)
detail.value = res detail.value = res
} finally { }
finally {
loading.value = false loading.value = false
} }
} }
@@ -86,32 +106,39 @@ async function handleAction(action: string) {
const res = await uni.showModal({ const res = await uni.showModal({
title: '录入调查报告', title: '录入调查报告',
editable: true, editable: true,
placeholderText: '请输入实地调查情况...' placeholderText: '请输入实地调查情况...',
}) })
if (!res.confirm) return if (!res.confirm)
return
data = { report: res.content } data = { report: res.content }
} else if (action === 'request_supplement') { }
else if (action === 'request_supplement') {
const res = await uni.showModal({ const res = await uni.showModal({
title: '要求补充资料', title: '要求补充资料',
editable: true, editable: true,
placeholderText: '请输入需补充的资料及原因...' placeholderText: '请输入需补充的资料及原因...',
}) })
if (!res.confirm) return if (!res.confirm)
return
data = { reason: res.content } data = { reason: res.content }
} else if (action === 'approve' || action === 'reject') { }
else if (action === 'approve' || action === 'reject') {
const res = await uni.showModal({ const res = await uni.showModal({
title: action === 'approve' ? '确认通过' : '确认拒绝', title: action === 'approve' ? '确认通过' : '确认拒绝',
editable: true, editable: true,
placeholderText: '请输入审批意见...' placeholderText: '请输入审批意见...',
}) })
if (!res.confirm) return if (!res.confirm)
return
data = { opinion: res.content } data = { opinion: res.content }
} else if (action === 'disburse') { }
else if (action === 'disburse') {
const res = await uni.showModal({ const res = await uni.showModal({
title: '确认放款', title: '确认放款',
content: `确认发放贷款 ${detail.value?.amount} 万元?` content: `确认发放贷款 ${detail.value?.amount} 万元?`,
}) })
if (!res.confirm) return if (!res.confirm)
return
} }
uni.showLoading({ title: '处理中...' }) uni.showLoading({ title: '处理中...' })
@@ -119,7 +146,8 @@ async function handleAction(action: string) {
await operateLoanApplication(id.value, action, data) await operateLoanApplication(id.value, action, data)
uni.showToast({ title: '操作成功', icon: 'success' }) uni.showToast({ title: '操作成功', icon: 'success' })
loadDetail() // 刷新详情 loadDetail() // 刷新详情
} finally { }
finally {
uni.hideLoading() uni.hideLoading()
} }
} }
@@ -130,26 +158,51 @@ function previewImage(url: string) {
// 查询征信信息 // 查询征信信息
async function handleQueryCredit() { async function handleQueryCredit() {
if (!detail.value) return if (!detail.value)
return
creditLoading.value = true creditLoading.value = true
try { try {
creditInfo.value = await queryCreditInfo(detail.value.userId) creditInfo.value = await queryCreditInfo(detail.value.userId)
} finally { }
finally {
creditLoading.value = false creditLoading.value = false
} }
} }
// 获取信用评估 // 获取信用评估
async function handleGetAssessment() { async function handleGetAssessment() {
if (!detail.value) return if (!detail.value)
return
assessmentLoading.value = true assessmentLoading.value = true
try { try {
creditAssessment.value = await getCreditAssessment(detail.value.userId) creditAssessment.value = await getCreditAssessment(detail.value.userId)
} finally { }
finally {
assessmentLoading.value = false assessmentLoading.value = false
} }
} }
// 购买保险
function handleBuyInsurance() {
if (!detail.value)
return
const loanAmount = detail.value.amount * 10000
const loanTerm = detail.value.term * 12
uni.navigateTo({
url: `/pagesBank/insurance/company/select?loanId=${id.value}&loanAmount=${loanAmount}&loanTerm=${loanTerm}`,
})
}
// 申请理赔
function handleApplyClaim() {
if (!detail.value?.insuranceInfo?.policy)
return
const policy = detail.value.insuranceInfo.policy
uni.navigateTo({
url: `/pagesBank/insurance/claim/create?loanId=${id.value}&policyId=${policy.id}&policyNumber=${policy.policyNumber}`,
})
}
onLoad((options) => { onLoad((options) => {
if (options?.id) { if (options?.id) {
id.value = options.id id.value = options.id
@@ -160,7 +213,9 @@ onLoad((options) => {
<template> <template>
<view class="audit-detail-page"> <view class="audit-detail-page">
<view v-if="loading" class="loading-state">加载中...</view> <view v-if="loading" class="loading-state">
加载中...
</view>
<template v-else-if="detail"> <template v-else-if="detail">
<!-- 流程步骤条 --> <!-- 流程步骤条 -->
@@ -173,11 +228,11 @@ onLoad((options) => {
:class="{ active: index <= currentStepIndex, current: index === currentStepIndex }" :class="{ active: index <= currentStepIndex, current: index === currentStepIndex }"
> >
<view class="step-icon"> <view class="step-icon">
<text class="num" v-if="index > currentStepIndex">{{ index + 1 }}</text> <text v-if="index > currentStepIndex" class="num">{{ index + 1 }}</text>
<text class="i-carbon-checkmark" v-else></text> <text v-else class="i-carbon-checkmark" />
</view> </view>
<text class="step-label">{{ step.label }}</text> <text class="step-label">{{ step.label }}</text>
<view class="step-line" v-if="index < steps.length - 1"></view> <view v-if="index < steps.length - 1" class="step-line" />
</view> </view>
</view> </view>
</view> </view>
@@ -207,13 +262,13 @@ onLoad((options) => {
<view class="section-card"> <view class="section-card">
<view class="card-header"> <view class="card-header">
<text class="title">征信信息</text> <text class="title">征信信息</text>
<button v-if="!creditInfo" class="credit-btn" @click="handleQueryCredit" :disabled="creditLoading"> <button v-if="!creditInfo" class="credit-btn" :disabled="creditLoading" @click="handleQueryCredit">
<text v-if="creditLoading">查询中...</text> <text v-if="creditLoading">查询中...</text>
<text v-else>查询征信</text> <text v-else>查询征信</text>
</button> </button>
<button v-else class="refresh-btn" @click="handleQueryCredit" :disabled="creditLoading"> <button v-else class="refresh-btn" :disabled="creditLoading" @click="handleQueryCredit">
<text v-if="creditLoading">加载中...</text> <text v-if="creditLoading">加载中...</text>
<text v-else class="i-carbon-reset"></text> <text v-else class="i-carbon-reset" />
</button> </button>
</view> </view>
<view v-if="creditInfo" class="credit-content"> <view v-if="creditInfo" class="credit-content">
@@ -226,7 +281,9 @@ onLoad((options) => {
<text class="query-time">查询时间{{ creditInfo.queryTime }}</text> <text class="query-time">查询时间{{ creditInfo.queryTime }}</text>
</view> </view>
<view class="section-title">贷款记录</view> <view class="section-title">
贷款记录
</view>
<view class="loan-records"> <view class="loan-records">
<view v-for="(record, index) in creditInfo.loanRecords" :key="index" class="loan-record-item"> <view v-for="(record, index) in creditInfo.loanRecords" :key="index" class="loan-record-item">
<view class="record-info"> <view class="record-info">
@@ -234,16 +291,18 @@ onLoad((options) => {
<text class="record-amount">{{ record.amount }}</text> <text class="record-amount">{{ record.amount }}</text>
</view> </view>
<view class="record-status" :class="record.status"> <view class="record-status" :class="record.status">
<text v-if="record.status === 'normal'" class="i-carbon-checkmark"></text> <text v-if="record.status === 'normal'" class="i-carbon-checkmark" />
<text v-else-if="record.status === 'overdue'" class="i-carbon-warning"></text> <text v-else-if="record.status === 'overdue'" class="i-carbon-warning" />
<text v-else class="i-carbon-time"></text> <text v-else class="i-carbon-time" />
<text class="status-text">{{ record.status === 'normal' ? '正常' : record.status === 'overdue' ? '逾期' : '已还' }}</text> <text class="status-text">{{ record.status === 'normal' ? '正常' : record.status === 'overdue' ? '逾期' : '已还' }}</text>
</view> </view>
<text class="record-date">{{ record.date }}</text> <text class="record-date">{{ record.date }}</text>
</view> </view>
</view> </view>
<view v-if="creditInfo.overdueRecords && creditInfo.overdueRecords.length" class="section-title">逾期记录</view> <view v-if="creditInfo.overdueRecords && creditInfo.overdueRecords.length" class="section-title">
逾期记录
</view>
<view v-if="creditInfo.overdueRecords && creditInfo.overdueRecords.length" class="overdue-records"> <view v-if="creditInfo.overdueRecords && creditInfo.overdueRecords.length" class="overdue-records">
<view v-for="(record, index) in creditInfo.overdueRecords" :key="index" class="overdue-item"> <view v-for="(record, index) in creditInfo.overdueRecords" :key="index" class="overdue-item">
<view class="overdue-info"> <view class="overdue-info">
@@ -255,7 +314,7 @@ onLoad((options) => {
</view> </view>
<view class="disclaimer"> <view class="disclaimer">
<text class="i-carbon-information disclaimer-icon"></text> <text class="i-carbon-information disclaimer-icon" />
<text class="disclaimer-text">征信信息仅供参考实际应用请以权威数据为准</text> <text class="disclaimer-text">征信信息仅供参考实际应用请以权威数据为准</text>
</view> </view>
</view> </view>
@@ -289,7 +348,7 @@ onLoad((options) => {
</view> </view>
</view> </view>
<view v-if="!detail.userOrders || detail.userOrders.length === 0" class="empty-orders"> <view v-if="!detail.userOrders || detail.userOrders.length === 0" class="empty-orders">
<text class="i-carbon-document"></text> <text class="i-carbon-document" />
<text>暂无消费订单</text> <text>暂无消费订单</text>
</view> </view>
</view> </view>
@@ -317,7 +376,7 @@ onLoad((options) => {
</view> </view>
<!-- 材料展示 --> <!-- 材料展示 -->
<view class="materials" v-if="merchant.materials?.materials.length"> <view v-if="merchant.materials?.materials.length" class="materials">
<view <view
v-for="(img, idx) in merchant.materials.materials" v-for="(img, idx) in merchant.materials.materials"
:key="idx" :key="idx"
@@ -334,7 +393,9 @@ onLoad((options) => {
<!-- 经营信息 --> <!-- 经营信息 -->
<view class="section-card"> <view class="section-card">
<view class="card-header">经营信息</view> <view class="card-header">
经营信息
</view>
<view class="cell-group"> <view class="cell-group">
<view class="cell"> <view class="cell">
<text class="label">经营项目</text> <text class="label">经营项目</text>
@@ -355,9 +416,9 @@ onLoad((options) => {
<view class="section-card"> <view class="section-card">
<view class="card-header"> <view class="card-header">
<text class="title">平台信用评估</text> <text class="title">平台信用评估</text>
<button v-if="!creditAssessment" class="refresh-btn" @click="handleGetAssessment" :disabled="assessmentLoading"> <button v-if="!creditAssessment" class="refresh-btn" :disabled="assessmentLoading" @click="handleGetAssessment">
<text v-if="assessmentLoading">加载中...</text> <text v-if="assessmentLoading">加载中...</text>
<text v-else class="i-carbon-reset"></text> <text v-else class="i-carbon-reset" />
</button> </button>
</view> </view>
<view v-if="creditAssessment" class="assessment-content"> <view v-if="creditAssessment" class="assessment-content">
@@ -376,38 +437,105 @@ onLoad((options) => {
</view> </view>
</view> </view>
<view class="disclaimer"> <view class="disclaimer">
<text class="i-carbon-information disclaimer-icon"></text> <text class="i-carbon-information disclaimer-icon" />
<text class="disclaimer-text">仅供参考不构成专业建议实际应用请以权威数据为准</text> <text class="disclaimer-text">仅供参考不构成专业建议实际应用请以权威数据为准</text>
</view> </view>
</view> </view>
</view> </view>
<!-- 保险信息 -->
<view class="section-card">
<view class="card-header">
<text class="title">保险信息</text>
</view>
<view v-if="detail.insuranceInfo?.hasInsurance" class="insurance-content">
<view class="info-row status-row">
<text class="label">投保状态</text>
<text class="value status-active">{{ detail.insuranceInfo.statusText }}</text>
</view>
<view class="insurance-detail">
<view class="cell">
<text class="label">保险公司</text>
<text class="value">{{ detail.insuranceInfo.policy.companyName }}</text>
</view>
<view class="cell">
<text class="label">保险产品</text>
<text class="value">{{ detail.insuranceInfo.policy.productName }}</text>
</view>
<view class="cell">
<text class="label">保单号</text>
<text class="value">{{ detail.insuranceInfo.policy.policyNumber }}</text>
</view>
<view class="cell">
<text class="label">保险金额</text>
<text class="value highlight">{{ (detail.insuranceInfo.policy.insuranceAmount / 10000).toFixed(2) }}</text>
</view>
<view class="cell">
<text class="label">保险期限</text>
<text class="value">{{ detail.insuranceInfo.policy.insuranceTerm / 12 }}</text>
</view>
</view>
<view class="claim-action">
<button class="btn warning plain" @click="handleApplyClaim">
申请理赔
</button>
</view>
</view>
<view v-else class="insurance-empty">
<text class="i-carbon-security empty-icon" />
<text class="empty-text">暂未购买保险</text>
<text class="empty-hint">购买保险可降低贷款风险</text>
<button class="buy-insurance-btn" @click="handleBuyInsurance">
<text class="i-carbon-add" />
<text>购买保险</text>
</button>
</view>
</view>
<!-- 底部操作栏 --> <!-- 底部操作栏 -->
<view class="action-bar safe-area-bottom"> <view class="action-bar safe-area-bottom">
<template v-if="detail.status === LoanStatus.SUBMITTED"> <template v-if="detail.status === LoanStatus.SUBMITTED">
<button class="btn primary" @click="handleAction('accept')">受理申请</button> <button class="btn primary" @click="handleAction('accept')">
受理申请
</button>
</template> </template>
<template v-else-if="detail.status === LoanStatus.ACCEPTED"> <template v-else-if="detail.status === LoanStatus.ACCEPTED">
<button class="btn primary" @click="handleAction('investigate')">开始上门调查</button> <button class="btn primary" @click="handleAction('investigate')">
开始上门调查
</button>
</template> </template>
<template v-else-if="detail.status === LoanStatus.INVESTIGATING"> <template v-else-if="detail.status === LoanStatus.INVESTIGATING">
<button class="btn primary" @click="handleAction('report')">提交调查报告</button> <button class="btn primary" @click="handleAction('report')">
提交调查报告
</button>
</template> </template>
<template v-else-if="[LoanStatus.REPORTED, LoanStatus.APPROVING].includes(detail.status)"> <template v-else-if="[LoanStatus.REPORTED, LoanStatus.APPROVING].includes(detail.status)">
<button class="btn danger" @click="handleAction('reject')">拒绝</button> <button class="btn danger" @click="handleAction('reject')">
<button class="btn warning" @click="handleAction('request_supplement')">要求补充</button> 拒绝
<button class="btn primary" @click="handleAction('approve')">通过审批</button> </button>
<button class="btn warning" @click="handleAction('request_supplement')">
要求补充
</button>
<button class="btn primary" @click="handleAction('approve')">
通过审批
</button>
</template> </template>
<template v-else-if="detail.status === LoanStatus.APPROVED"> <template v-else-if="detail.status === LoanStatus.APPROVED">
<button class="btn primary" @click="handleAction('sign')">完成签约</button> <button class="btn primary" @click="handleAction('sign')">
完成签约
</button>
</template> </template>
<template v-else-if="detail.status === LoanStatus.SIGNED"> <template v-else-if="detail.status === LoanStatus.SIGNED">
<button class="btn success" @click="handleAction('disburse')">确认放款</button> <button class="btn success" @click="handleAction('disburse')">
确认放款
</button>
</template> </template>
</view> </view>
</template> </template>
@@ -509,8 +637,15 @@ onLoad((options) => {
margin-bottom: 30rpx; margin-bottom: 30rpx;
.user { .user {
.name { font-size: 32rpx; font-weight: bold; margin-right: 16rpx; } .name {
.phone { font-size: 26rpx; opacity: 0.9; } font-size: 32rpx;
font-weight: bold;
margin-right: 16rpx;
}
.phone {
font-size: 26rpx;
opacity: 0.9;
}
} }
.status-tag { .status-tag {
@@ -527,9 +662,20 @@ onLoad((options) => {
.item { .item {
flex: 1; flex: 1;
.label { display: block; font-size: 24rpx; opacity: 0.8; margin-bottom: 8rpx; } .label {
.value { font-size: 40rpx; font-weight: bold; display: block;
.unit { font-size: 24rpx; font-weight: normal; margin-left: 4rpx; } font-size: 24rpx;
opacity: 0.8;
margin-bottom: 8rpx;
}
.value {
font-size: 40rpx;
font-weight: bold;
.unit {
font-size: 24rpx;
font-weight: normal;
margin-left: 4rpx;
}
} }
} }
} }
@@ -555,7 +701,7 @@ onLoad((options) => {
.credit-btn { .credit-btn {
padding: 8rpx 20rpx; padding: 8rpx 20rpx;
border-radius: 8rpx; border-radius: 8rpx;
background: #3B82F6; background: #3b82f6;
color: #fff; color: #fff;
font-size: 24rpx; font-size: 24rpx;
border: none; border: none;
@@ -685,14 +831,23 @@ onLoad((options) => {
padding: 20rpx 0; padding: 20rpx 0;
border-bottom: 1rpx solid #f9f9f9; border-bottom: 1rpx solid #f9f9f9;
&:last-child { border-bottom: none; } &:last-child {
border-bottom: none;
}
.t-main { .t-main {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
.t-title { font-size: 26rpx; color: #333; margin-bottom: 6rpx; } .t-title {
.t-time { font-size: 22rpx; color: #999; } font-size: 26rpx;
color: #333;
margin-bottom: 6rpx;
}
.t-time {
font-size: 22rpx;
color: #999;
}
} }
.t-amount { .t-amount {
@@ -700,8 +855,12 @@ onLoad((options) => {
font-weight: bold; font-weight: bold;
color: #333; color: #333;
&.income { color: #fe2c55; } &.income {
&.expend { color: #333; } color: #fe2c55;
}
&.expend {
color: #333;
}
} }
} }
} }
@@ -720,9 +879,17 @@ onLoad((options) => {
justify-content: space-between; justify-content: space-between;
margin-bottom: 16rpx; margin-bottom: 16rpx;
.m-name { font-size: 28rpx; font-weight: bold; color: #333; } .m-name {
.m-status { font-size: 24rpx; color: #999; font-size: 28rpx;
&.submitted { color: #00c05a; } font-weight: bold;
color: #333;
}
.m-status {
font-size: 24rpx;
color: #999;
&.submitted {
color: #00c05a;
}
} }
} }
@@ -736,7 +903,11 @@ onLoad((options) => {
width: 100rpx; width: 100rpx;
height: 100rpx; height: 100rpx;
image { width: 100%; height: 100%; border-radius: 8rpx; } image {
width: 100%;
height: 100%;
border-radius: 8rpx;
}
.type-tag { .type-tag {
position: absolute; position: absolute;
bottom: 0; bottom: 0;
@@ -762,8 +933,12 @@ onLoad((options) => {
padding: 16rpx 0; padding: 16rpx 0;
font-size: 26rpx; font-size: 26rpx;
.label { color: #666; } .label {
.value { color: #333; } color: #666;
}
.value {
color: #333;
}
} }
} }
@@ -771,20 +946,23 @@ onLoad((options) => {
padding: 20rpx 0; padding: 20rpx 0;
.score-box { .score-box {
background: linear-gradient(135deg, #00c05a 0%, #34d19d 100%);
border-radius: 12rpx;
padding: 24rpx;
display: flex; display: flex;
align-items: baseline; align-items: baseline;
gap: 12rpx; gap: 12rpx;
margin-bottom: 24rpx; margin-bottom: 24rpx;
.score { .score {
font-size: 56rpx; font-size: 48rpx;
font-weight: bold; font-weight: bold;
color: #00c05a; color: #fff;
} }
.unit { .unit {
font-size: 28rpx; font-size: 24rpx;
color: #666; color: rgba(255, 255, 255, 0.8);
} }
.level { .level {
@@ -792,33 +970,30 @@ onLoad((options) => {
border-radius: 8rpx; border-radius: 8rpx;
font-size: 24rpx; font-size: 24rpx;
font-weight: 600; font-weight: 600;
background: rgba(255, 255, 255, 0.2);
color: #fff;
&.优秀 { &.优秀 {
background: rgba(0, 192, 90, 0.1); background: rgba(255, 255, 255, 0.3);
color: #00c05a; color: #fff;
} }
&.良好 { &.良好 {
background: rgba(59, 130, 246, 0.1); background: rgba(255, 255, 255, 0.25);
color: #3B82F6; color: #fff;
} }
&.一般 { &.一般 {
background: rgba(245, 158, 11, 0.1); background: rgba(255, 255, 255, 0.2);
color: #F59E0B; color: #fff;
} }
&.较差 { &.较差 {
background: rgba(250, 67, 80, 0.1); background: rgba(255, 255, 255, 0.15);
color: #fa4350; color: #fff;
} }
} }
} }
.factors-list { .factors-list {
.factor-item { .factor-item {
display: flex;
flex-direction: column;
padding: 16rpx 0; padding: 16rpx 0;
border-bottom: 1rpx solid #f5f5f5; border-bottom: 1rpx solid #f5f5f5;
@@ -835,10 +1010,11 @@ onLoad((options) => {
.factor-name { .factor-name {
font-size: 26rpx; font-size: 26rpx;
color: #333; color: #333;
font-weight: 500;
} }
.factor-score { .factor-score {
font-size: 24rpx; font-size: 26rpx;
font-weight: 600; font-weight: 600;
color: #00c05a; color: #00c05a;
} }
@@ -846,7 +1022,7 @@ onLoad((options) => {
.factor-desc { .factor-desc {
font-size: 24rpx; font-size: 24rpx;
color: #666; color: #999;
line-height: 1.5; line-height: 1.5;
} }
} }
@@ -863,7 +1039,7 @@ onLoad((options) => {
.disclaimer-icon { .disclaimer-icon {
font-size: 24rpx; font-size: 24rpx;
color: #F59E0B; color: #f59e0b;
} }
.disclaimer-text { .disclaimer-text {
@@ -873,6 +1049,127 @@ onLoad((options) => {
} }
} }
.insurance-content {
padding: 20rpx 0;
.status-row {
display: flex;
justify-content: space-between;
margin-bottom: 20rpx;
.label {
color: #666;
font-size: 26rpx;
}
.status-active {
color: #00c05a;
font-weight: 600;
font-size: 26rpx;
}
.status-pending {
color: #ff8f0d;
font-weight: 600;
font-size: 26rpx;
}
.status-claim {
color: #3b82f6;
font-weight: 600;
font-size: 26rpx;
}
}
.insurance-detail {
background: #f8f9fa;
border-radius: 12rpx;
padding: 16rpx 24rpx;
margin-bottom: 20rpx;
.cell {
display: flex;
justify-content: space-between;
padding: 12rpx 0;
font-size: 26rpx;
.label {
color: #999;
}
.value {
color: #333;
font-weight: 500;
}
.highlight {
color: #ff8f0d;
font-weight: 600;
}
}
}
.claim-action {
display: flex;
justify-content: flex-end;
.btn {
font-size: 24rpx;
padding: 12rpx 32rpx;
margin: 0;
line-height: 1.5;
&.warning.plain {
color: #fa4350;
border: 1rpx solid #fa4350;
background: transparent;
}
}
}
}
.insurance-empty {
display: flex;
flex-direction: column;
align-items: center;
padding: 60rpx 0;
color: #999;
.empty-icon {
font-size: 64rpx;
color: #ddd;
margin-bottom: 16rpx;
}
.empty-text {
font-size: 28rpx;
color: #666;
margin-bottom: 8rpx;
}
.empty-hint {
font-size: 24rpx;
color: #999;
margin-bottom: 24rpx;
}
.buy-insurance-btn {
display: flex;
align-items: center;
gap: 8rpx;
padding: 16rpx 32rpx;
background: linear-gradient(135deg, #00c05a 0%, #34d19d 100%);
color: #fff;
font-size: 28rpx;
border-radius: 40rpx;
border: none;
margin: 0;
text:first-child {
font-size: 24rpx;
}
text:last-child {
font-size: 28rpx;
}
}
}
.action-bar { .action-bar {
position: fixed; position: fixed;
bottom: 0; bottom: 0;
@@ -892,9 +1189,18 @@ onLoad((options) => {
line-height: 80rpx; line-height: 80rpx;
border-radius: 40rpx; border-radius: 40rpx;
&.primary { background: #00c05a; color: #fff; } &.primary {
&.danger { background: #fa4350; color: #fff; } background: #00c05a;
&.success { background: #00c05a; color: #fff; } color: #fff;
}
&.danger {
background: #fa4350;
color: #fff;
}
&.success {
background: #00c05a;
color: #fff;
}
} }
} }
@@ -903,7 +1209,7 @@ onLoad((options) => {
padding: 20rpx 0; padding: 20rpx 0;
.credit-score-box { .credit-score-box {
background: linear-gradient(135deg, #3B82F6 0%, #60A5FA 100%); background: linear-gradient(135deg, #3b82f6 0%, #60a5fa 100%);
border-radius: 12rpx; border-radius: 12rpx;
padding: 24rpx; padding: 24rpx;
display: flex; display: flex;
@@ -1037,7 +1343,7 @@ onLoad((options) => {
.disclaimer-icon { .disclaimer-icon {
font-size: 24rpx; font-size: 24rpx;
color: #F59E0B; color: #f59e0b;
} }
.disclaimer-text { .disclaimer-text {

View File

@@ -135,12 +135,21 @@ onPullDownRefresh(() => {
<text class="merchant-name">{{ item.userName }}的贷款申请</text> <text class="merchant-name">{{ item.userName }}的贷款申请</text>
<text class="time">{{ item.createTime }}</text> <text class="time">{{ item.createTime }}</text>
</view> </view>
<view class="tags-row">
<text <text
class="status-tag" class="status-tag"
:style="{ color: statusMap[item.status]?.color, backgroundColor: statusMap[item.status]?.bgColor }" :style="{ color: statusMap[item.status]?.color, backgroundColor: statusMap[item.status]?.bgColor }"
> >
{{ statusMap[item.status]?.text || item.status }} {{ statusMap[item.status]?.text || item.status }}
</text> </text>
<!-- 保险状态标识 -->
<text
v-if="item.status === LoanStatus.DISBURSED"
class="insurance-tag"
>
{{ Math.random() > 0.5 ? '已投保' : '未投保' }}
</text>
</view>
</view> </view>
<view class="card-content"> <view class="card-content">
@@ -273,12 +282,27 @@ onPullDownRefresh(() => {
} }
} }
.tags-row {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 8rpx;
.status-tag { .status-tag {
font-size: 22rpx; font-size: 22rpx;
padding: 6rpx 16rpx; padding: 6rpx 16rpx;
border-radius: 8rpx; border-radius: 8rpx;
font-weight: 600; font-weight: 600;
} }
.insurance-tag {
font-size: 20rpx;
color: #00c05a;
background: rgba(0, 192, 90, 0.1);
padding: 4rpx 12rpx;
border-radius: 6rpx;
}
}
} }
.card-content { .card-content {

View File

@@ -16,8 +16,8 @@ const quickActions = [
{ icon: 'i-carbon-task-approved', label: '待审核', path: '/pagesBank/audit/list' }, { icon: 'i-carbon-task-approved', label: '待审核', path: '/pagesBank/audit/list' },
{ icon: 'i-carbon-group', label: '客户管理', path: '/pagesBank/customer/list' }, { icon: 'i-carbon-group', label: '客户管理', path: '/pagesBank/customer/list' },
{ icon: 'i-carbon-calendar', label: '拜访计划', path: '/pagesBank/visit/list' }, { icon: 'i-carbon-calendar', label: '拜访计划', path: '/pagesBank/visit/list' },
{ icon: 'i-carbon-add', label: '创建拜访', path: '/pagesBank/visit/create' }, { icon: 'i-carbon-security', label: '投保管理', path: '/pagesBank/insurance/application/list' },
{ icon: 'i-carbon-document-download', label: '报表', path: '/pagesBank/report/list' }, { icon: 'i-carbon-money', label: '理赔管理', path: '/pagesBank/insurance/claim/list' },
{ icon: 'i-carbon-settings', label: '设置', path: '/pagesBank/me/index' }, { icon: 'i-carbon-settings', label: '设置', path: '/pagesBank/me/index' },
] ]

View File

@@ -0,0 +1,521 @@
<script lang="ts" setup>
import type { InsuranceCompany, InsuranceProduct } from '@/api/types/insurance'
import { createInsuranceApplication, getInsuranceCompanies, getInsuranceProducts } from '@/api/insurance'
definePage({
style: {
navigationBarTitleText: '购买保险',
},
})
const loanId = ref('')
const companyId = ref('')
const productId = ref('')
const loanAmount = ref(0)
const loanTerm = ref(0)
const loading = ref(false)
const submitting = ref(false)
// 选中的数据
const selectedCompany = ref<InsuranceCompany | null>(null)
const selectedProduct = ref<InsuranceProduct | null>(null)
const insuranceAmount = ref('')
const insuranceTerm = ref('')
// 列表数据
const companies = ref<InsuranceCompany[]>([])
const products = ref<InsuranceProduct[]>([])
// 状态
const showCompanyPicker = ref(false)
const showProductPicker = ref(false)
// 加载保险公司列表
async function loadCompanies() {
loading.value = true
try {
const res = await getInsuranceCompanies()
companies.value = res.list
}
finally {
loading.value = false
}
}
// 根据ID预选保险公司
function preSelectCompany() {
if (!companyId.value)
return
const company = companies.value.find(c => c.id === companyId.value)
if (company) {
selectedCompany.value = company
loadProducts(company.id)
}
}
// 选择保险公司
function handleSelectCompany(company: InsuranceCompany) {
selectedCompany.value = company
selectedProduct.value = null
insuranceAmount.value = ''
insuranceTerm.value = ''
loadProducts(company.id)
showCompanyPicker.value = false
}
// 加载保险产品列表
async function loadProducts(companyId: string) {
const res = await getInsuranceProducts(companyId)
products.value = res.list
}
// 选择保险产品
function handleSelectProduct(product: InsuranceProduct) {
selectedProduct.value = product
// 默认保险金额等于贷款金额
insuranceAmount.value = loanAmount.value.toString()
// 默认保险期限等于贷款期限(月)
insuranceTerm.value = loanTerm.value.toString()
showProductPicker.value = false
}
// 根据ID预选保险产品
function preSelectProduct() {
if (!productId.value || !selectedCompany.value)
return
loadProducts(selectedCompany.value.id).then(() => {
const product = products.value.find(p => p.id === productId.value)
if (product) {
handleSelectProduct(product)
}
})
}
// 提交投保申请
async function handleSubmit() {
if (!selectedCompany.value || !selectedProduct.value) {
uni.showToast({ title: '请选择保险公司和保险产品', icon: 'none' })
return
}
if (!insuranceAmount.value || !insuranceTerm.value) {
uni.showToast({ title: '请填写保险金额和期限', icon: 'none' })
return
}
const amount = Number(insuranceAmount.value)
const term = Number(insuranceTerm.value)
// 验证保险金额
if (amount < selectedProduct.value.minAmount) {
uni.showToast({ title: `保险金额不能低于${selectedProduct.value.minAmount}`, icon: 'none' })
return
}
if (amount > selectedProduct.value.maxAmount) {
uni.showToast({ title: `保险金额不能高于${selectedProduct.value.maxAmount}`, icon: 'none' })
return
}
submitting.value = true
try {
await createInsuranceApplication({
loanId: loanId.value,
companyId: selectedCompany.value.id,
productId: selectedProduct.value.id,
insuranceAmount: amount,
insuranceTerm: term,
})
uni.showToast({ title: '投保申请已提交', icon: 'success' })
setTimeout(() => {
uni.navigateBack()
}, 1500)
}
finally {
submitting.value = false
}
}
onLoad((options) => {
if (options?.loanId) {
loanId.value = options.loanId
}
if (options?.loanAmount) {
loanAmount.value = Number(options.loanAmount)
}
if (options?.loanTerm) {
loanTerm.value = Number(options.loanTerm)
}
if (options?.companyId) {
companyId.value = options.companyId
}
if (options?.productId) {
productId.value = options.productId
}
loadCompanies().then(() => {
preSelectCompany()
if (productId.value) {
preSelectProduct()
}
})
})
</script>
<template>
<view class="insurance-create-page">
<view v-if="loading" class="loading-state">
加载中...
</view>
<template v-else>
<!-- 保险公司选择 -->
<view class="section-card">
<view class="card-header">
<text class="title">选择保险公司</text>
<text v-if="selectedCompany" class="selected-text">{{ selectedCompany.name }}</text>
</view>
<view class="company-list">
<view
v-for="company in companies"
:key="company.id"
class="company-item"
:class="{ active: selectedCompany?.id === company.id }"
@click="handleSelectCompany(company)"
>
<view class="company-info">
<text class="company-name">{{ company.name }}</text>
<text class="contact-info">联系方式: {{ company.contactInfo }}</text>
</view>
<text v-if="selectedCompany?.id === company.id" class="i-carbon-checkmark selected-icon" />
</view>
</view>
</view>
<!-- 保险产品选择 -->
<view v-if="selectedCompany" class="section-card">
<view class="card-header">
<text class="title">选择保险产品</text>
<text v-if="selectedProduct" class="selected-text">{{ selectedProduct.name }}</text>
</view>
<view class="product-list">
<view
v-for="product in products"
:key="product.id"
class="product-item"
:class="{ active: selectedProduct?.id === product.id }"
@click="handleSelectProduct(product)"
>
<view class="product-info">
<text class="product-name">{{ product.name }}</text>
<text class="product-desc">{{ product.description }}</text>
<view class="product-meta">
<text class="meta-item">类型: {{ product.type === 'housing_loan' ? '住房贷款' : product.type === 'business_credit' ? '企业信贷' : '其他' }}</text>
<text class="meta-item">金额范围: {{ product.minAmount }}-{{ product.maxAmount }}</text>
</view>
</view>
<text v-if="selectedProduct?.id === product.id" class="i-carbon-checkmark selected-icon" />
</view>
</view>
</view>
<!-- 保险信息 -->
<view v-if="selectedProduct" class="section-card">
<view class="card-header">
保险信息
</view>
<view class="form-group">
<view class="form-item">
<text class="label">保险金额</text>
<input
v-model="insuranceAmount"
type="digit"
class="input"
placeholder="请输入保险金额"
>
</view>
<view class="form-item">
<text class="label">保险期限</text>
<input
v-model="insuranceTerm"
type="number"
class="input"
placeholder="请输入保险期限"
>
</view>
<view class="form-item">
<text class="label">贷款金额参考</text>
<text class="value">{{ loanAmount }}</text>
</view>
<view class="form-item">
<text class="label">贷款期限参考</text>
<text class="value">{{ loanTerm }}</text>
</view>
</view>
</view>
<!-- 提示信息 -->
<view class="tips-card">
<view class="tips-header">
<text class="i-carbon-information tips-icon" />
<text class="tips-title">温馨提示</text>
</view>
<view class="tips-content">
<text class="tips-item"> 保险金额一般不低于抵押物价值</text>
<text class="tips-item"> 保险期限应与贷款期限一致</text>
<text class="tips-item"> 投保申请提交后将由保险公司进行核保</text>
<text class="tips-item"> 核保通过后将自动生成保险单</text>
</view>
</view>
<!-- 底部操作栏 -->
<view class="action-bar safe-area-bottom">
<button
class="btn primary"
:disabled="submitting"
@click="handleSubmit"
>
<text v-if="submitting">提交中...</text>
<text v-else>提交投保申请</text>
</button>
</view>
</template>
</view>
</template>
<style lang="scss" scoped>
.insurance-create-page {
min-height: 100vh;
background: #f5f7fa;
padding: 20rpx;
padding-bottom: 120rpx;
}
.loading-state {
display: flex;
align-items: center;
justify-content: center;
padding: 100rpx 0;
color: #999;
}
.section-card {
background: #fff;
border-radius: 16rpx;
padding: 0 30rpx;
margin-bottom: 20rpx;
.card-header {
padding: 30rpx 0;
border-bottom: 1rpx solid #f5f5f5;
font-size: 28rpx;
font-weight: bold;
color: #333;
display: flex;
justify-content: space-between;
align-items: center;
.selected-text {
font-size: 24rpx;
color: #00c05a;
font-weight: normal;
}
}
}
.company-list {
padding: 20rpx 0;
.company-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24rpx;
background: #f8f9fa;
border-radius: 12rpx;
margin-bottom: 16rpx;
border: 2rpx solid transparent;
&:last-child {
margin-bottom: 0;
}
&.active {
border-color: #00c05a;
background: #e6f7eb;
}
.company-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 8rpx;
.company-name {
font-size: 28rpx;
font-weight: 600;
color: #333;
}
.contact-info {
font-size: 24rpx;
color: #666;
}
}
.selected-icon {
font-size: 40rpx;
color: #00c05a;
}
}
}
.product-list {
padding: 20rpx 0;
.product-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24rpx;
background: #f8f9fa;
border-radius: 12rpx;
margin-bottom: 16rpx;
border: 2rpx solid transparent;
&:last-child {
margin-bottom: 0;
}
&.active {
border-color: #00c05a;
background: #e6f7eb;
}
.product-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 8rpx;
.product-name {
font-size: 28rpx;
font-weight: 600;
color: #333;
}
.product-desc {
font-size: 24rpx;
color: #666;
line-height: 1.5;
}
.product-meta {
display: flex;
gap: 16rpx;
flex-wrap: wrap;
.meta-item {
font-size: 22rpx;
color: #999;
}
}
}
.selected-icon {
font-size: 40rpx;
color: #00c05a;
}
}
}
.form-group {
padding: 20rpx 0;
.form-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16rpx 0;
font-size: 26rpx;
.label {
color: #666;
flex-shrink: 0;
}
.input {
flex: 1;
text-align: right;
font-size: 26rpx;
color: #333;
}
.value {
color: #333;
}
}
}
.tips-card {
background: #fff;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 20rpx;
.tips-header {
display: flex;
align-items: center;
gap: 8rpx;
margin-bottom: 16rpx;
.tips-icon {
font-size: 28rpx;
color: #f59e0b;
}
.tips-title {
font-size: 28rpx;
font-weight: 600;
color: #333;
}
}
.tips-content {
display: flex;
flex-direction: column;
gap: 12rpx;
.tips-item {
font-size: 24rpx;
color: #666;
line-height: 1.6;
}
}
}
.action-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: #fff;
padding: 20rpx 30rpx;
box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.05);
z-index: 100;
.btn {
width: 100%;
font-size: 28rpx;
height: 80rpx;
line-height: 80rpx;
border-radius: 40rpx;
&.primary {
background: #00c05a;
color: #fff;
}
&:disabled {
opacity: 0.6;
}
}
}
</style>

View File

@@ -0,0 +1,322 @@
<script lang="ts" setup>
import type { InsuranceApplication, InsurancePolicy } from '@/api/types/insurance'
import { getInsuranceApplicationDetail, getInsurancePolicyDetail } from '@/api/insurance'
definePage({
style: {
navigationBarTitleText: '投保申请详情',
},
})
const applicationId = ref('')
const application = ref<InsuranceApplication | null>(null)
const policy = ref<InsurancePolicy | null>(null)
const loading = ref(false)
// 状态映射
const statusMap: Record<string, { text: string, color: string }> = {
pending: { text: '待审核', color: '#F59E0B' },
approved: { text: '已通过', color: '#00c05a' },
rejected: { text: '已拒绝', color: '#fa4350' },
}
const statusInfo = computed(() => {
if (!application.value)
return null
return statusMap[application.value.status] || { text: application.value.status, color: '#666' }
})
async function loadDetail() {
loading.value = true
try {
const res = await getInsuranceApplicationDetail(applicationId.value)
application.value = res
// 如果已通过,加载保险单
if (res.status === 'approved') {
// 查找对应的保险单
const policies = await getInsurancePolicyDetail(res.id)
policy.value = policies
}
}
finally {
loading.value = false
}
}
onLoad((options) => {
if (options?.id) {
applicationId.value = options.id
loadDetail()
}
})
</script>
<template>
<view class="application-detail-page">
<view v-if="loading" class="loading-state">
加载中...
</view>
<template v-else-if="application">
<!-- 状态卡片 -->
<view class="status-card" :style="{ background: statusInfo?.color }">
<text class="status-text">{{ statusInfo?.text }}</text>
<text v-if="application.status === 'pending'" class="status-desc">保险公司正在审核您的投保申请</text>
<text v-else-if="application.status === 'approved'" class="status-desc">投保申请已通过保险单已生成</text>
<text v-else-if="application.status === 'rejected'" class="status-desc">投保申请已被拒绝</text>
</view>
<!-- 基本信息 -->
<view class="section-card">
<view class="card-header">
基本信息
</view>
<view class="info-list">
<view class="info-item">
<text class="label">投保申请号</text>
<text class="value">{{ application.id }}</text>
</view>
<view class="info-item">
<text class="label">贷款编号</text>
<text class="value">{{ application.loanId }}</text>
</view>
<view class="info-item">
<text class="label">申请时间</text>
<text class="value">{{ application.createdAt }}</text>
</view>
</view>
</view>
<!-- 保险公司信息 -->
<view class="section-card">
<view class="card-header">
保险公司信息
</view>
<view class="info-list">
<view class="info-item">
<text class="label">保险公司</text>
<text class="value">{{ application.companyName }}</text>
</view>
<view class="info-item">
<text class="label">保险产品</text>
<text class="value">{{ application.productName }}</text>
</view>
</view>
</view>
<!-- 客户信息 -->
<view class="section-card">
<view class="card-header">
客户信息
</view>
<view class="info-list">
<view class="info-item">
<text class="label">客户姓名</text>
<text class="value">{{ application.customerInfo.name }}</text>
</view>
<view class="info-item">
<text class="label">身份证号</text>
<text class="value">{{ application.customerInfo.idNumber }}</text>
</view>
<view class="info-item">
<text class="label">信用评分</text>
<text class="value score">{{ application.customerInfo.creditScore }}</text>
</view>
<view class="info-item">
<text class="label">贷款金额</text>
<text class="value">{{ application.customerInfo.loanAmount }}</text>
</view>
<view class="info-item">
<text class="label">贷款期限</text>
<text class="value">{{ application.customerInfo.loanTerm }}</text>
</view>
</view>
</view>
<!-- 保险信息 -->
<view class="section-card">
<view class="card-header">
保险信息
</view>
<view class="info-list">
<view class="info-item">
<text class="label">保险金额</text>
<text class="value amount">{{ application.insuranceAmount }}</text>
</view>
<view class="info-item">
<text class="label">保险期限</text>
<text class="value">{{ application.insuranceTerm }}</text>
</view>
</view>
</view>
<!-- 审核信息 -->
<view v-if="application.status !== 'pending'" class="section-card">
<view class="card-header">
审核信息
</view>
<view class="info-list">
<view class="info-item">
<text class="label">审核时间</text>
<text class="value">{{ application.reviewedAt }}</text>
</view>
<view class="info-item">
<text class="label">审核人员</text>
<text class="value">{{ application.reviewedBy }}</text>
</view>
<view v-if="application.rejectionReason" class="info-item full">
<text class="label">拒绝原因</text>
<text class="value reason">{{ application.rejectionReason }}</text>
</view>
</view>
</view>
<!-- 保险单信息 -->
<view v-if="policy" class="section-card">
<view class="card-header">
<text>保险单信息</text>
<text class="i-carbon-checkmark success-icon" />
</view>
<view class="info-list">
<view class="info-item">
<text class="label">保险单号</text>
<text class="value">{{ policy.policyNumber }}</text>
</view>
<view class="info-item">
<text class="label">保险金额</text>
<text class="value amount">{{ policy.insuranceAmount }}</text>
</view>
<view class="info-item">
<text class="label">保险期限</text>
<text class="value">{{ policy.insuranceTerm }}</text>
</view>
<view class="info-item">
<text class="label">开始日期</text>
<text class="value">{{ policy.startDate }}</text>
</view>
<view class="info-item">
<text class="label">结束日期</text>
<text class="value">{{ policy.endDate }}</text>
</view>
<view class="info-item">
<text class="label">出单时间</text>
<text class="value">{{ policy.issuedAt }}</text>
</view>
</view>
</view>
</template>
</view>
</template>
<style lang="scss" scoped>
.application-detail-page {
min-height: 100vh;
background: #f5f7fa;
padding: 20rpx;
}
.loading-state {
display: flex;
align-items: center;
justify-content: center;
padding: 100rpx 0;
color: #999;
}
.status-card {
background: linear-gradient(135deg, #00c05a 0%, #34d19d 100%);
border-radius: 16rpx;
padding: 40rpx 30rpx;
color: #fff;
margin-bottom: 20rpx;
text-align: center;
.status-text {
font-size: 36rpx;
font-weight: bold;
display: block;
margin-bottom: 12rpx;
}
.status-desc {
font-size: 24rpx;
opacity: 0.9;
display: block;
}
}
.section-card {
background: #fff;
border-radius: 16rpx;
padding: 0 30rpx;
margin-bottom: 20rpx;
.card-header {
padding: 30rpx 0;
border-bottom: 1rpx solid #f5f5f5;
font-size: 28rpx;
font-weight: bold;
color: #333;
display: flex;
align-items: center;
gap: 8rpx;
.success-icon {
font-size: 28rpx;
color: #00c05a;
}
}
}
.info-list {
padding: 20rpx 0;
.info-item {
display: flex;
justify-content: space-between;
padding: 16rpx 0;
font-size: 26rpx;
border-bottom: 1rpx solid #f9f9f9;
&:last-child {
border-bottom: none;
}
.label {
color: #666;
}
.value {
color: #333;
text-align: right;
&.score {
color: #00c05a;
font-weight: 600;
}
&.amount {
color: #ff8f0d;
font-weight: 600;
}
&.reason {
color: #fa4350;
text-align: left;
line-height: 1.5;
}
}
&.full {
flex-direction: column;
align-items: flex-start;
.value {
margin-top: 8rpx;
width: 100%;
}
}
}
}
</style>

View File

@@ -0,0 +1,439 @@
<script lang="ts" setup>
import type { InsuranceApplication } from '@/api/types/insurance'
import { getInsuranceApplicationList } from '@/api/insurance'
definePage({
style: {
navigationBarTitleText: '投保申请',
},
})
const loading = ref(false)
const applications = ref<InsuranceApplication[]>([])
const filteredApplications = ref<InsuranceApplication[]>([])
const searchKeyword = ref('')
const currentTab = ref('all')
const pageSize = 20
const currentPage = ref(1)
const hasMore = ref(true)
const statusMap: Record<string, { text: string, color: string }> = {
pending: { text: '待审核', color: '#F59E0B' },
approved: { text: '已通过', color: '#00c05a' },
rejected: { text: '已拒绝', color: '#fa4350' },
}
const tabs = [
{ key: 'all', label: '全部' },
{ key: 'pending', label: '待审核' },
{ key: 'approved', label: '已通过' },
{ key: 'rejected', label: '已拒绝' },
]
function getStatusInfo(status: string) {
return statusMap[status] || { text: status, color: '#999' }
}
function getProductTypeText(type: string) {
const typeMap: Record<string, string> = {
housing_loan: '住房贷款',
business_credit: '企业信贷',
other: '其他',
}
return typeMap[type] || type
}
function filterBySearch(list: InsuranceApplication[]) {
if (!searchKeyword.value.trim())
return list
const keyword = searchKeyword.value.toLowerCase()
return list.filter(app =>
app.id.toLowerCase().includes(keyword)
|| app.companyName.toLowerCase().includes(keyword)
|| app.productName.toLowerCase().includes(keyword)
|| (app as any).policyNumber?.toLowerCase().includes(keyword),
)
}
function filterByStatus(list: InsuranceApplication[]) {
if (currentTab.value === 'all')
return list
return list.filter(app => app.status === currentTab.value)
}
function updateFilteredList() {
let list = filterBySearch(applications.value)
list = filterByStatus(list)
filteredApplications.value = list
}
function handleTabChange(tab: string) {
currentTab.value = tab
updateFilteredList()
}
function handleSearch() {
currentPage.value = 1
updateFilteredList()
}
function handleClearSearch() {
searchKeyword.value = ''
handleSearch()
}
function handleViewDetail(id: string) {
uni.navigateTo({
url: `/pagesBank/insurance/application/detail?id=${id}`,
})
}
async function loadList(reset = false) {
if (reset) {
currentPage.value = 1
applications.value = []
hasMore.value = true
}
if (!hasMore.value)
return
loading.value = true
try {
const res = await getInsuranceApplicationList()
applications.value = res.list
updateFilteredList()
hasMore.value = res.list.length >= pageSize
}
finally {
loading.value = false
}
}
function handleLoadMore() {
if (!loading.value && hasMore.value) {
currentPage.value++
loadList()
}
}
function handleRefresh() {
loadList(true)
}
onShow(() => {
loadList(true)
})
</script>
<template>
<view class="application-list-page">
<view class="search-bar">
<view class="search-input-wrap">
<text class="search-icon i-carbon-search" />
<input
v-model="searchKeyword"
class="search-input"
placeholder="搜索保单号/公司/产品"
@confirm="handleSearch"
>
<text v-if="searchKeyword" class="clear-icon" @click="handleClearSearch">×</text>
</view>
</view>
<view class="tabs">
<view
v-for="tab in tabs"
:key="tab.key"
class="tab-item"
:class="{ active: currentTab === tab.key }"
@click="handleTabChange(tab.key)"
>
<text class="tab-text">{{ tab.label }}</text>
<view v-if="currentTab === tab.key" class="tab-indicator" />
</view>
</view>
<view v-if="loading && filteredApplications.length === 0" class="loading-state">
加载中...
</view>
<view v-else-if="filteredApplications.length > 0" class="application-list">
<view
v-for="app in filteredApplications"
:key="app.id"
class="application-item"
@click="handleViewDetail(app.id)"
>
<view class="app-header">
<view class="app-info">
<text class="app-id">{{ app.id }}</text>
<text class="company-name">{{ app.companyName }}</text>
</view>
<view
class="status-tag"
:style="{ background: getStatusInfo(app.status).color }"
>
{{ getStatusInfo(app.status).text }}
</view>
</view>
<view class="app-body">
<view class="app-row">
<text class="label">保险产品</text>
<text class="value">{{ app.productName }}</text>
</view>
<view class="app-row">
<text class="label">产品类型</text>
<text class="value">{{ getProductTypeText(app.customerInfo?.loanType || 'other') }}</text>
</view>
<view class="app-row">
<text class="label">保险金额</text>
<text class="value amount">{{ (app.insuranceAmount / 10000).toFixed(2) }}</text>
</view>
<view class="app-row">
<text class="label">保险期限</text>
<text class="value">{{ app.insuranceTerm / 12 }}</text>
</view>
<view class="app-row">
<text class="label">提交时间</text>
<text class="value">{{ app.createdAt }}</text>
</view>
<view v-if="app.rejectionReason" class="app-row reject">
<text class="label">拒绝原因</text>
<text class="value">{{ app.rejectionReason }}</text>
</view>
</view>
<view class="app-footer">
<text class="view-detail">查看详情</text>
<text class="arrow-icon i-carbon-chevron-right" />
</view>
</view>
</view>
<view v-if="!loading && filteredApplications.length > 0 && hasMore" class="load-more" @click="handleLoadMore">
<text>加载更多</text>
</view>
<view v-if="!loading && filteredApplications.length === 0" class="empty-state">
<text class="empty-icon i-carbon-document" />
<text class="empty-text">暂无投保申请</text>
</view>
</view>
</template>
<style lang="scss" scoped>
.application-list-page {
min-height: 100vh;
background: #f5f7fa;
}
.search-bar {
background: #fff;
padding: 20rpx 30rpx;
border-bottom: 1rpx solid #f5f5f5;
.search-input-wrap {
display: flex;
align-items: center;
background: #f5f7fa;
border-radius: 36rpx;
padding: 16rpx 24rpx;
.search-icon {
font-size: 32rpx;
color: #999;
margin-right: 12rpx;
}
.search-input {
flex: 1;
font-size: 28rpx;
}
.clear-icon {
font-size: 36rpx;
color: #999;
padding: 10rpx;
}
}
}
.tabs {
background: #fff;
display: flex;
padding: 0 30rpx;
border-bottom: 1rpx solid #f5f5f5;
.tab-item {
flex: 1;
position: relative;
padding: 30rpx 0;
text-align: center;
font-size: 28rpx;
color: #666;
&.active {
color: #00c05a;
font-weight: 600;
}
.tab-text {
position: relative;
z-index: 1;
}
.tab-indicator {
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 60rpx;
height: 4rpx;
background: #00c05a;
border-radius: 2rpx;
}
}
}
.loading-state {
display: flex;
align-items: center;
justify-content: center;
padding: 100rpx 0;
color: #999;
}
.application-list {
padding: 20rpx;
.application-item {
background: #fff;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 16rpx;
&:last-child {
margin-bottom: 0;
}
.app-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
.app-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 8rpx;
.app-id {
font-size: 28rpx;
font-weight: 600;
color: #333;
}
.company-name {
font-size: 24rpx;
color: #666;
}
}
.status-tag {
padding: 8rpx 20rpx;
border-radius: 8rpx;
font-size: 24rpx;
color: #fff;
}
}
.app-body {
background: #f8f9fa;
border-radius: 12rpx;
padding: 20rpx;
margin-bottom: 16rpx;
.app-row {
display: flex;
justify-content: space-between;
padding: 12rpx 0;
font-size: 26rpx;
border-bottom: 1rpx solid #f5f5f5;
&:last-child {
border-bottom: none;
}
.label {
color: #666;
}
.value {
color: #333;
text-align: right;
&.amount {
color: #ff8f0d;
font-weight: 600;
}
&.reject {
color: #fa4350;
text-align: left;
line-height: 1.5;
}
}
}
}
.app-footer {
display: flex;
justify-content: flex-end;
align-items: center;
padding-top: 12rpx;
.view-detail {
font-size: 24rpx;
color: #00c05a;
margin-right: 8rpx;
}
.arrow-icon {
font-size: 28rpx;
color: #00c05a;
}
}
}
}
.load-more {
display: flex;
align-items: center;
justify-content: center;
padding: 30rpx;
color: #666;
font-size: 26rpx;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 0;
color: #999;
.empty-icon {
font-size: 80rpx;
margin-bottom: 20rpx;
color: #ccc;
}
.empty-text {
font-size: 26rpx;
}
}
</style>

View File

@@ -0,0 +1,421 @@
<script lang="ts" setup>
import type { InsurancePolicy } from '@/api/types/insurance'
import { createClaimApplication } from '@/api/insurance'
definePage({
style: {
navigationBarTitleText: '发起理赔申请',
},
})
const policyId = ref('')
const policyNumber = ref('')
const loanId = ref('')
const claimAmount = ref('')
const claimReason = ref('')
const materials = ref<any[]>([])
const submitting = ref(false)
// 上传材料
function handleUploadMaterial() {
uni.chooseImage({
count: 9,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: (res) => {
const tempFilePaths = res.tempFilePaths as string[]
tempFilePaths.forEach((filePath, index) => {
materials.value.push({
id: `material_${Date.now()}_${index}`,
url: filePath,
name: `理赔材料${materials.value.length + 1}.jpg`,
type: 'image/jpeg',
size: 0,
})
})
},
})
}
// 删除材料
function handleRemoveMaterial(index: number) {
materials.value.splice(index, 1)
}
// 预览图片
function handlePreviewImage(url: string) {
uni.previewImage({
urls: [url],
})
}
// 提交理赔申请
async function handleSubmit() {
if (!claimAmount.value || !claimReason.value) {
uni.showToast({ title: '请填写理赔金额和原因', icon: 'none' })
return
}
if (materials.value.length === 0) {
uni.showToast({ title: '请上传理赔材料', icon: 'none' })
return
}
submitting.value = true
try {
const files = materials.value.map((m) => {
// 模拟 File 对象
return new File([], m.name, { type: m.type })
})
await createClaimApplication({
policyId: policyId.value,
loanId: loanId.value,
claimAmount: Number(claimAmount.value),
claimReason: claimReason.value,
materials: files,
})
uni.showToast({ title: '理赔申请已提交', icon: 'success' })
setTimeout(() => {
uni.navigateBack()
}, 1500)
}
finally {
submitting.value = false
}
}
onLoad((options) => {
if (options?.policyId) {
policyId.value = options.policyId
}
if (options?.policyNumber) {
policyNumber.value = options.policyNumber
}
if (options?.loanId) {
loanId.value = options.loanId
}
})
</script>
<template>
<view class="claim-create-page">
<!-- 保险单信息 -->
<view class="section-card">
<view class="card-header">
保险单信息
</view>
<view class="info-list">
<view class="info-item">
<text class="label">保险单号</text>
<text class="value">{{ policyNumber }}</text>
</view>
</view>
</view>
<!-- 理赔信息 -->
<view class="section-card">
<view class="card-header">
理赔信息
</view>
<view class="form-group">
<view class="form-item">
<text class="label">理赔金额</text>
<input
v-model="claimAmount"
type="digit"
class="input"
placeholder="请输入理赔金额"
>
</view>
<view class="form-item vertical">
<text class="label">理赔原因</text>
<textarea
v-model="claimReason"
class="textarea"
placeholder="请详细描述理赔原因..."
:maxlength="500"
/>
<text class="char-count">{{ claimReason.length }}/500</text>
</view>
</view>
</view>
<!-- 理赔材料 -->
<view class="section-card">
<view class="card-header">
<text>理赔材料</text>
<text class="required">至少上传1份</text>
</view>
<view class="materials-area">
<view class="upload-btn" @click="handleUploadMaterial">
<text class="i-carbon-add upload-icon" />
<text class="upload-text">上传材料</text>
</view>
<view
v-for="(material, index) in materials"
:key="material.id"
class="material-item"
>
<image :src="material.url" mode="aspectFill" @click="handlePreviewImage(material.url)" />
<view class="material-actions">
<text class="delete-btn" @click="handleRemoveMaterial(index)">
<text class="i-carbon-close" />
</text>
</view>
</view>
</view>
</view>
<!-- 提示信息 -->
<view class="tips-card">
<view class="tips-header">
<text class="i-carbon-information tips-icon" />
<text class="tips-title">温馨提示</text>
</view>
<view class="tips-content">
<text class="tips-item"> 请上传清晰的理赔材料包括但不限于事故证明损失清单相关票据等</text>
<text class="tips-item"> 理赔申请提交后将由保险公司进行审核</text>
<text class="tips-item"> 审核通过后将执行赔付审核失败将返回拒绝原因</text>
<text class="tips-item"> 理赔金额不得超过保险金额</text>
</view>
</view>
<!-- 底部操作栏 -->
<view class="action-bar safe-area-bottom">
<button
class="btn primary"
:disabled="submitting"
@click="handleSubmit"
>
<text v-if="submitting">提交中...</text>
<text v-else>提交理赔申请</text>
</button>
</view>
</view>
</template>
<style lang="scss" scoped>
.claim-create-page {
min-height: 100vh;
background: #f5f7fa;
padding: 20rpx;
padding-bottom: 120rpx;
}
.section-card {
background: #fff;
border-radius: 16rpx;
padding: 0 30rpx;
margin-bottom: 20rpx;
.card-header {
padding: 30rpx 0;
border-bottom: 1rpx solid #f5f5f5;
font-size: 28rpx;
font-weight: bold;
color: #333;
display: flex;
justify-content: space-between;
align-items: center;
.required {
font-size: 22rpx;
color: #fa4350;
font-weight: normal;
}
}
}
.info-list {
padding: 20rpx 0;
.info-item {
display: flex;
justify-content: space-between;
padding: 16rpx 0;
font-size: 26rpx;
.label {
color: #666;
}
.value {
color: #333;
}
}
}
.form-group {
padding: 20rpx 0;
.form-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16rpx 0;
font-size: 26rpx;
&.vertical {
flex-direction: column;
align-items: flex-start;
}
.label {
color: #666;
flex-shrink: 0;
margin-bottom: 12rpx;
}
.input {
flex: 1;
text-align: right;
font-size: 26rpx;
color: #333;
}
.textarea {
width: 100%;
min-height: 150rpx;
padding: 16rpx;
background: #f8f9fa;
border-radius: 8rpx;
font-size: 26rpx;
color: #333;
line-height: 1.5;
}
.char-count {
font-size: 22rpx;
color: #999;
text-align: right;
margin-top: 8rpx;
}
}
}
.materials-area {
padding: 20rpx 0;
display: flex;
gap: 16rpx;
flex-wrap: wrap;
.upload-btn {
width: 160rpx;
height: 160rpx;
border: 2rpx dashed #d9d9d9;
border-radius: 12rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8rpx;
background: #f8f9fa;
.upload-icon {
font-size: 48rpx;
color: #999;
}
.upload-text {
font-size: 22rpx;
color: #999;
}
}
.material-item {
position: relative;
width: 160rpx;
height: 160rpx;
border-radius: 12rpx;
overflow: hidden;
image {
width: 100%;
height: 100%;
}
.material-actions {
position: absolute;
top: 8rpx;
right: 8rpx;
.delete-btn {
width: 40rpx;
height: 40rpx;
background: rgba(0, 0, 0, 0.6);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 24rpx;
}
}
}
}
.tips-card {
background: #fff;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 20rpx;
.tips-header {
display: flex;
align-items: center;
gap: 8rpx;
margin-bottom: 16rpx;
.tips-icon {
font-size: 28rpx;
color: #f59e0b;
}
.tips-title {
font-size: 28rpx;
font-weight: 600;
color: #333;
}
}
.tips-content {
display: flex;
flex-direction: column;
gap: 12rpx;
.tips-item {
font-size: 24rpx;
color: #666;
line-height: 1.6;
}
}
}
.action-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: #fff;
padding: 20rpx 30rpx;
box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.05);
z-index: 100;
.btn {
width: 100%;
font-size: 28rpx;
height: 80rpx;
line-height: 80rpx;
border-radius: 40rpx;
&.primary {
background: #00c05a;
color: #fff;
}
&:disabled {
opacity: 0.6;
}
}
}
</style>

View File

@@ -0,0 +1,332 @@
<script lang="ts" setup>
import type { ClaimApplication } from '@/api/types/insurance'
import { getClaimApplicationList } from '@/api/insurance'
definePage({
style: {
navigationBarTitleText: '理赔申请',
},
})
const loading = ref(false)
const claims = ref<ClaimApplication[]>([])
const currentTab = ref('all')
// 状态映射
const statusMap: Record<string, { text: string, color: string }> = {
pending: { text: '待审核', color: '#F59E0B' },
approved: { text: '已通过', color: '#00c05a' },
rejected: { text: '已拒绝', color: '#fa4350' },
}
const tabs = [
{ key: 'all', label: '全部' },
{ key: 'pending', label: '待审核' },
{ key: 'approved', label: '已通过' },
{ key: 'rejected', label: '已拒绝' },
]
// 筛选后的列表
const filteredClaims = computed(() => {
if (currentTab.value === 'all') {
return claims.value
}
return claims.value.filter(c => c.status === currentTab.value)
})
// 切换标签
function handleTabChange(tab: string) {
currentTab.value = tab
}
// 查看详情
function handleViewDetail(id: string) {
uni.navigateTo({
url: `/pagesBank/insurance/claim/detail?id=${id}`,
})
}
// 加载列表
async function loadList() {
loading.value = true
try {
const res = await getClaimApplicationList()
claims.value = res.list
}
finally {
loading.value = false
}
}
onShow(() => {
loadList()
})
</script>
<template>
<view class="claim-list-page">
<!-- 标签页 -->
<view class="tabs">
<view
v-for="tab in tabs"
:key="tab.key"
class="tab-item"
:class="{ active: currentTab === tab.key }"
@click="handleTabChange(tab.key)"
>
<text class="tab-text">{{ tab.label }}</text>
<view v-if="currentTab === tab.key" class="tab-indicator" />
</view>
</view>
<!-- 列表 -->
<view v-if="loading" class="loading-state">
加载中...
</view>
<view v-else-if="filteredClaims.length > 0" class="claim-list">
<view
v-for="claim in filteredClaims"
:key="claim.id"
class="claim-item"
@click="handleViewDetail(claim.id)"
>
<view class="claim-header">
<view class="claim-info">
<text class="policy-no">保险单号: {{ claim.policyNumber }}</text>
<text class="company-name">{{ claim.companyName }}</text>
</view>
<view
class="status-tag"
:style="{ background: statusMap[claim.status]?.color }"
>
{{ statusMap[claim.status]?.text }}
</view>
</view>
<view class="claim-body">
<view class="claim-row">
<text class="label">理赔金额</text>
<text class="value amount">{{ claim.claimAmount }}</text>
</view>
<view class="claim-row">
<text class="label">理赔原因</text>
<text class="value reason">{{ claim.claimReason }}</text>
</view>
<view class="claim-row">
<text class="label">提交时间</text>
<text class="value">{{ claim.submittedAt }}</text>
</view>
<view v-if="claim.payoutAmount" class="claim-row">
<text class="label">赔付金额</text>
<text class="value payout">{{ claim.payoutAmount }}</text>
</view>
<view v-if="claim.payoutDate" class="claim-row">
<text class="label">赔付日期</text>
<text class="value">{{ claim.payoutDate }}</text>
</view>
<view v-if="claim.rejectionReason" class="claim-row">
<text class="label">拒绝原因</text>
<text class="value reject">{{ claim.rejectionReason }}</text>
</view>
</view>
<view class="claim-footer">
<text class="material-count">已上传 {{ claim.materials.length }} 份材料</text>
<text class="arrow-icon i-carbon-chevron-right" />
</view>
</view>
</view>
<!-- 空状态 -->
<view v-else class="empty-state">
<text class="empty-icon i-carbon-document" />
<text class="empty-text">暂无理赔申请</text>
</view>
</view>
</template>
<style lang="scss" scoped>
.claim-list-page {
min-height: 100vh;
background: #f5f7fa;
}
.tabs {
background: #fff;
display: flex;
padding: 0 30rpx;
border-bottom: 1rpx solid #f5f5f5;
.tab-item {
flex: 1;
position: relative;
padding: 30rpx 0;
text-align: center;
font-size: 28rpx;
color: #666;
&.active {
color: #00c05a;
font-weight: 600;
}
.tab-text {
position: relative;
z-index: 1;
}
.tab-indicator {
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 60rpx;
height: 4rpx;
background: #00c05a;
border-radius: 2rpx;
}
}
}
.loading-state {
display: flex;
align-items: center;
justify-content: center;
padding: 100rpx 0;
color: #999;
}
.claim-list {
padding: 20rpx;
.claim-item {
background: #fff;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 16rpx;
&:last-child {
margin-bottom: 0;
}
.claim-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
.claim-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 8rpx;
.policy-no {
font-size: 28rpx;
font-weight: 600;
color: #333;
}
.company-name {
font-size: 24rpx;
color: #666;
}
}
.status-tag {
padding: 8rpx 20rpx;
border-radius: 8rpx;
font-size: 24rpx;
color: #fff;
}
}
.claim-body {
background: #f8f9fa;
border-radius: 12rpx;
padding: 20rpx;
margin-bottom: 16rpx;
.claim-row {
display: flex;
justify-content: space-between;
padding: 12rpx 0;
font-size: 26rpx;
border-bottom: 1rpx solid #f5f5f5;
&:last-child {
border-bottom: none;
}
.label {
color: #666;
}
.value {
color: #333;
text-align: right;
flex: 1;
&.amount {
color: #ff8f0d;
font-weight: 600;
}
&.reason {
text-align: left;
line-height: 1.5;
}
&.payout {
color: #00c05a;
font-weight: 600;
}
&.reject {
color: #fa4350;
text-align: left;
line-height: 1.5;
}
}
}
}
.claim-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 12rpx;
.material-count {
font-size: 24rpx;
color: #999;
}
.arrow-icon {
font-size: 28rpx;
color: #ccc;
}
}
}
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 0;
color: #999;
.empty-icon {
font-size: 80rpx;
margin-bottom: 20rpx;
color: #ccc;
}
.empty-text {
font-size: 26rpx;
}
}
</style>

View File

@@ -0,0 +1,380 @@
<script lang="ts" setup>
import type { InsuranceCompany, InsuranceProduct } from '@/api/types/insurance'
import { getInsuranceCompanies, getInsuranceProducts } from '@/api/insurance'
definePage({
style: {
navigationBarTitleText: '选择保险公司',
},
})
const loanId = ref('')
const loanAmount = ref(0)
const loanTerm = ref(0)
const loading = ref(false)
const searchKeyword = ref('')
const companies = ref<InsuranceCompany[]>([])
const productsCount = ref<Record<string, number>>({})
const filteredCompanies = computed(() => {
if (!searchKeyword.value.trim())
return companies.value
const keyword = searchKeyword.value.toLowerCase()
return companies.value.filter(company =>
company.name.toLowerCase().includes(keyword)
|| company.contactInfo.toLowerCase().includes(keyword),
)
})
function handleSearch() {
}
function handleClearSearch() {
searchKeyword.value = ''
}
async function loadCompanies() {
loading.value = true
try {
const res = await getInsuranceCompanies()
companies.value = res.list
for (const company of companies.value) {
const productsRes = await getInsuranceProducts(company.id)
productsCount.value[company.id] = productsRes.list.length
}
}
finally {
loading.value = false
}
}
function handleSelectCompany(company: InsuranceCompany) {
uni.navigateTo({
url: `/pagesBank/insurance/product/select?loanId=${loanId.value}&companyId=${company.id}&loanAmount=${loanAmount.value}&loanTerm=${loanTerm.value}`,
})
}
function handleGoBack() {
uni.navigateBack()
}
onLoad((options) => {
if (options?.loanId)
loanId.value = options.loanId
if (options?.loanAmount)
loanAmount.value = Number(options.loanAmount)
if (options?.loanTerm)
loanTerm.value = Number(options.loanTerm)
loadCompanies()
})
</script>
<template>
<view class="company-select-page">
<view class="header">
<view class="back-btn" @click="handleGoBack">
<text class="i-carbon-arrow-left" />
</view>
<text class="header-title">选择保险公司</text>
</view>
<view class="search-bar">
<view class="search-input-wrap">
<text class="search-icon i-carbon-search" />
<input
v-model="searchKeyword"
class="search-input"
placeholder="搜索公司名称或联系方式"
@input="handleSearch"
>
<text v-if="searchKeyword" class="clear-icon" @click="handleClearSearch">×</text>
</view>
</view>
<view v-if="loading" class="loading-state">
加载中...
</view>
<view v-else-if="filteredCompanies.length > 0" class="company-list">
<view
v-for="company in filteredCompanies"
:key="company.id"
class="company-item"
@click="handleSelectCompany(company)"
>
<view class="company-main">
<view class="company-avatar">
<text class="i-carbon-building" />
</view>
<view class="company-info">
<text class="company-name">{{ company.name }}</text>
<view class="company-meta">
<text class="contact">
<text class="i-carbon-phone" />
{{ company.contactInfo }}
</text>
<text class="status active">
<text class="i-carbon-checkmark" />
合作中
</text>
</view>
</view>
</view>
<view class="company-action">
<text class="product-count">{{ productsCount[company.id] || 0 }}个产品</text>
<text class="select-icon i-carbon-chevron-right" />
</view>
</view>
</view>
<view v-else class="empty-state">
<text class="empty-icon i-carbon-search" />
<text class="empty-text">未找到相关保险公司</text>
</view>
<view v-if="!loading && filteredCompanies.length > 0" class="tips-card">
<view class="tips-header">
<text class="tips-icon i-carbon-information" />
<text class="tips-title">温馨提示</text>
</view>
<view class="tips-content">
<text class="tips-item">请选择为您发放贷款的保险公司</text>
<text class="tips-item">选择后将进入产品选择页面</text>
</view>
</view>
</view>
</template>
<style lang="scss" scoped>
.company-select-page {
min-height: 100vh;
background: #f5f7fa;
}
.header {
display: flex;
align-items: center;
background: #fff;
padding: 20rpx 30rpx;
border-bottom: 1rpx solid #f5f5f5;
.back-btn {
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
margin-right: 20rpx;
text {
font-size: 36rpx;
color: #333;
}
}
.header-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
}
}
.search-bar {
background: #fff;
padding: 20rpx 30rpx;
border-bottom: 1rpx solid #f5f5f5;
.search-input-wrap {
display: flex;
align-items: center;
background: #f5f7fa;
border-radius: 36rpx;
padding: 16rpx 24rpx;
.search-icon {
font-size: 32rpx;
color: #999;
margin-right: 12rpx;
}
.search-input {
flex: 1;
font-size: 28rpx;
}
.clear-icon {
font-size: 36rpx;
color: #999;
padding: 10rpx;
}
}
}
.loading-state {
display: flex;
align-items: center;
justify-content: center;
padding: 100rpx 0;
color: #999;
}
.company-list {
padding: 20rpx;
.company-item {
display: flex;
justify-content: space-between;
align-items: center;
background: #fff;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 16rpx;
&:last-child {
margin-bottom: 0;
}
.company-main {
display: flex;
align-items: center;
flex: 1;
.company-avatar {
width: 80rpx;
height: 80rpx;
background: linear-gradient(135deg, #00c05a 0%, #34d19d 100%);
border-radius: 16rpx;
display: flex;
align-items: center;
justify-content: center;
margin-right: 20rpx;
text {
font-size: 40rpx;
color: #fff;
}
}
.company-info {
flex: 1;
.company-name {
font-size: 30rpx;
font-weight: 600;
color: #333;
margin-bottom: 12rpx;
display: block;
}
.company-meta {
display: flex;
align-items: center;
gap: 20rpx;
.contact {
display: flex;
align-items: center;
gap: 8rpx;
font-size: 24rpx;
color: #666;
text:first-child {
font-size: 24rpx;
}
}
.status {
display: flex;
align-items: center;
gap: 6rpx;
font-size: 24rpx;
&.active {
color: #00c05a;
}
text:first-child {
font-size: 20rpx;
}
}
}
}
}
.company-action {
display: flex;
align-items: center;
gap: 12rpx;
.product-count {
font-size: 24rpx;
color: #999;
}
.select-icon {
font-size: 28rpx;
color: #ccc;
}
}
}
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 0;
color: #999;
.empty-icon {
font-size: 80rpx;
margin-bottom: 20rpx;
color: #ccc;
}
.empty-text {
font-size: 26rpx;
}
}
.tips-card {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: #fff;
padding: 24rpx 30rpx;
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.05);
.tips-header {
display: flex;
align-items: center;
gap: 8rpx;
margin-bottom: 12rpx;
.tips-icon {
font-size: 28rpx;
color: #f59e0b;
}
.tips-title {
font-size: 28rpx;
font-weight: 600;
color: #333;
}
}
.tips-content {
display: flex;
flex-direction: column;
gap: 8rpx;
.tips-item {
font-size: 24rpx;
color: #666;
line-height: 1.5;
}
}
}
</style>

View File

@@ -0,0 +1,270 @@
<script lang="ts" setup>
import type { InsurancePolicy } from '@/api/types/insurance'
import { getInsurancePolicyDetail } from '@/api/insurance'
definePage({
style: {
navigationBarTitleText: '保险单详情',
},
})
const policyId = ref('')
const policy = ref<InsurancePolicy | null>(null)
const loading = ref(false)
// 状态映射
const statusMap: Record<string, { text: string, color: string }> = {
active: { text: '生效中', color: '#00c05a' },
expired: { text: '已过期', color: '#999' },
cancelled: { text: '已取消', color: '#fa4350' },
}
const statusInfo = computed(() => {
if (!policy.value)
return null
return statusMap[policy.value.status] || { text: policy.value.status, color: '#666' }
})
async function loadDetail() {
loading.value = true
try {
const res = await getInsurancePolicyDetail(policyId.value)
policy.value = res
}
finally {
loading.value = false
}
}
onLoad((options) => {
if (options?.id) {
policyId.value = options.id
loadDetail()
}
})
</script>
<template>
<view class="policy-detail-page">
<view v-if="loading" class="loading-state">
加载中...
</view>
<template v-else-if="policy">
<!-- 状态卡片 -->
<view class="status-card" :style="{ background: statusInfo?.color }">
<text class="status-text">{{ statusInfo?.text }}</text>
<text class="policy-no">保险单号: {{ policy.policyNumber }}</text>
</view>
<!-- 基本信息 -->
<view class="section-card">
<view class="card-header">
基本信息
</view>
<view class="info-list">
<view class="info-item">
<text class="label">保险单号</text>
<text class="value">{{ policy.policyNumber }}</text>
</view>
<view class="info-item">
<text class="label">出单时间</text>
<text class="value">{{ policy.issuedAt }}</text>
</view>
</view>
</view>
<!-- 保险公司信息 -->
<view class="section-card">
<view class="card-header">
保险公司信息
</view>
<view class="info-list">
<view class="info-item">
<text class="label">保险公司</text>
<text class="value">{{ policy.companyName }}</text>
</view>
<view class="info-item">
<text class="label">保险产品</text>
<text class="value">{{ policy.productName }}</text>
</view>
</view>
</view>
<!-- 保险信息 -->
<view class="section-card">
<view class="card-header">
保险信息
</view>
<view class="info-list">
<view class="info-item">
<text class="label">保险金额</text>
<text class="value amount">{{ policy.insuranceAmount }}</text>
</view>
<view class="info-item">
<text class="label">保险期限</text>
<text class="value">{{ policy.insuranceTerm }}</text>
</view>
<view class="info-item">
<text class="label">开始日期</text>
<text class="value">{{ policy.startDate }}</text>
</view>
<view class="info-item">
<text class="label">结束日期</text>
<text class="value">{{ policy.endDate }}</text>
</view>
</view>
</view>
<!-- 关联信息 -->
<view class="section-card">
<view class="card-header">
关联信息
</view>
<view class="info-list">
<view class="info-item">
<text class="label">投保申请号</text>
<text class="value">{{ policy.applicationId }}</text>
</view>
<view class="info-item">
<text class="label">贷款编号</text>
<text class="value">{{ policy.loanId }}</text>
</view>
</view>
</view>
<!-- 操作提示 -->
<view v-if="policy.status === 'active'" class="tips-card">
<view class="tips-header">
<text class="i-carbon-information tips-icon" />
<text class="tips-title">温馨提示</text>
</view>
<view class="tips-content">
<text class="tips-item"> 保险单生效期间如发生保险事故可发起理赔申请</text>
<text class="tips-item"> 理赔申请需提供相关证明材料</text>
<text class="tips-item"> 保险单到期后自动失效需重新购买保险</text>
</view>
</view>
</template>
</view>
</template>
<style lang="scss" scoped>
.policy-detail-page {
min-height: 100vh;
background: #f5f7fa;
padding: 20rpx;
}
.loading-state {
display: flex;
align-items: center;
justify-content: center;
padding: 100rpx 0;
color: #999;
}
.status-card {
background: linear-gradient(135deg, #00c05a 0%, #34d19d 100%);
border-radius: 16rpx;
padding: 40rpx 30rpx;
color: #fff;
margin-bottom: 20rpx;
text-align: center;
.status-text {
font-size: 36rpx;
font-weight: bold;
display: block;
margin-bottom: 12rpx;
}
.policy-no {
font-size: 24rpx;
opacity: 0.9;
display: block;
}
}
.section-card {
background: #fff;
border-radius: 16rpx;
padding: 0 30rpx;
margin-bottom: 20rpx;
.card-header {
padding: 30rpx 0;
border-bottom: 1rpx solid #f5f5f5;
font-size: 28rpx;
font-weight: bold;
color: #333;
}
}
.info-list {
padding: 20rpx 0;
.info-item {
display: flex;
justify-content: space-between;
padding: 16rpx 0;
font-size: 26rpx;
border-bottom: 1rpx solid #f9f9f9;
&:last-child {
border-bottom: none;
}
.label {
color: #666;
}
.value {
color: #333;
text-align: right;
&.amount {
color: #ff8f0d;
font-weight: 600;
}
}
}
}
.tips-card {
background: #fff;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 20rpx;
.tips-header {
display: flex;
align-items: center;
gap: 8rpx;
margin-bottom: 16rpx;
.tips-icon {
font-size: 28rpx;
color: #f59e0b;
}
.tips-title {
font-size: 28rpx;
font-weight: 600;
color: #333;
}
}
.tips-content {
display: flex;
flex-direction: column;
gap: 12rpx;
.tips-item {
font-size: 24rpx;
color: #666;
line-height: 1.6;
}
}
}
</style>

View File

@@ -0,0 +1,582 @@
<script lang="ts" setup>
import type { InsuranceCompany, InsuranceProduct } from '@/api/types/insurance'
import { getInsuranceCompanies, getInsuranceProducts } from '@/api/insurance'
definePage({
style: {
navigationBarTitleText: '选择保险产品',
},
})
const loanId = ref('')
const companyId = ref('')
const companyName = ref('')
const loanAmount = ref(0)
const loanTerm = ref(0)
const loading = ref(false)
const searchKeyword = ref('')
const products = ref<InsuranceProduct[]>([])
const selectedProduct = ref<InsuranceProduct | null>(null)
const showDetailModal = ref(false)
const filteredProducts = computed(() => {
if (!searchKeyword.value.trim())
return products.value
const keyword = searchKeyword.value.toLowerCase()
return products.value.filter(product =>
product.name.toLowerCase().includes(keyword)
|| product.description.toLowerCase().includes(keyword)
|| product.type.toLowerCase().includes(keyword),
)
})
function getProductTypeText(type: string) {
const typeMap: Record<string, string> = {
housing_loan: '住房贷款',
business_credit: '企业信贷',
other: '其他',
}
return typeMap[type] || type
}
function handleSearch() {
}
function handleClearSearch() {
searchKeyword.value = ''
}
async function loadProducts() {
loading.value = true
try {
const res = await getInsuranceProducts(companyId.value)
products.value = res.list
}
finally {
loading.value = false
}
}
function handleShowDetail(product: InsuranceProduct) {
selectedProduct.value = product
showDetailModal.value = true
}
function handleCloseDetail() {
showDetailModal.value = false
selectedProduct.value = null
}
function handleSelectProduct(product: InsuranceProduct) {
uni.navigateTo({
url: `/pagesBank/insurance/application/create?loanId=${loanId.value}&companyId=${companyId.value}&productId=${product.id}&loanAmount=${loanAmount.value}&loanTerm=${loanTerm.value}`,
})
}
function handleGoBack() {
uni.navigateBack()
}
onLoad(async (options) => {
if (options?.loanId)
loanId.value = options.loanId
if (options?.companyId)
companyId.value = options.companyId
if (options?.loanAmount)
loanAmount.value = Number(options.loanAmount)
if (options?.loanTerm)
loanTerm.value = Number(options.loanTerm)
if (companyId.value) {
const companyRes = await getInsuranceCompanies()
const company = companyRes.list.find(c => c.id === companyId.value)
if (company)
companyName.value = company.name
loadProducts()
}
})
</script>
<template>
<view class="product-select-page">
<view class="header">
<view class="back-btn" @click="handleGoBack">
<text class="i-carbon-arrow-left" />
</view>
<text class="header-title">{{ companyName }} - 选择产品</text>
</view>
<view class="search-bar">
<view class="search-input-wrap">
<text class="search-icon i-carbon-search" />
<input
v-model="searchKeyword"
class="search-input"
placeholder="搜索产品名称/类型/描述"
@input="handleSearch"
>
<text v-if="searchKeyword" class="clear-icon" @click="handleClearSearch">×</text>
</view>
</view>
<view v-if="loading" class="loading-state">
加载中...
</view>
<view v-else-if="filteredProducts.length > 0" class="product-list">
<view
v-for="product in filteredProducts"
:key="product.id"
class="product-item"
@click="handleSelectProduct(product)"
>
<view class="product-main">
<view class="product-icon">
<text class="i-carbon-security" />
</view>
<view class="product-info">
<text class="product-name">{{ product.name }}</text>
<view class="product-tags">
<view class="tag type">
{{ getProductTypeText(product.type) }}
</view>
</view>
<text class="product-desc">{{ product.description }}</text>
<view class="product-range">
<text class="range-label">保险金额范围</text>
<text class="range-value">{{ (product.minAmount / 10000).toFixed(0) }} - {{ (product.maxAmount / 10000).toFixed(0) }}</text>
</view>
</view>
</view>
<view class="product-actions">
<view class="info-btn" @click.stop="handleShowDetail(product)">
<text class="i-carbon-information" />
<text>详情</text>
</view>
<text class="select-icon i-carbon-chevron-right" />
</view>
</view>
</view>
<view v-else class="empty-state">
<text class="empty-icon i-carbon-search" />
<text class="empty-text">未找到相关产品</text>
</view>
<view v-if="!loading && filteredProducts.length > 0" class="tips-card">
<view class="tips-header">
<text class="tips-icon i-carbon-information" />
<text class="tips-title">温馨提示</text>
</view>
<view class="tips-content">
<text class="tips-item">请选择适合的保险产品</text>
<text class="tips-item">保险金额建议与贷款金额一致</text>
<text class="tips-item">保险期限建议与贷款期限一致</text>
</view>
</view>
<view v-if="showDetailModal && selectedProduct" class="modal-mask" @click="handleCloseDetail">
<view class="modal-content" @click.stop>
<view class="modal-header">
<text class="modal-title">产品详情</text>
<view class="close-btn" @click="handleCloseDetail">
<text class="i-carbon-close" />
</view>
</view>
<view class="modal-body">
<view class="detail-row">
<text class="detail-label">产品名称</text>
<text class="detail-value">{{ selectedProduct.name }}</text>
</view>
<view class="detail-row">
<text class="detail-label">所属公司</text>
<text class="detail-value">{{ companyName }}</text>
</view>
<view class="detail-row">
<text class="detail-label">产品类型</text>
<text class="detail-value">{{ getProductTypeText(selectedProduct.type) }}</text>
</view>
<view class="detail-row">
<text class="detail-label">产品描述</text>
<text class="detail-value desc">{{ selectedProduct.description }}</text>
</view>
<view class="detail-row">
<text class="detail-label">最低金额</text>
<text class="detail-value amount">{{ (selectedProduct.minAmount / 10000).toFixed(2) }}</text>
</view>
<view class="detail-row">
<text class="detail-label">最高金额</text>
<text class="detail-value amount">{{ (selectedProduct.maxAmount / 10000).toFixed(2) }}</text>
</view>
</view>
<view class="modal-footer">
<button class="btn" @click="handleSelectProduct(selectedProduct)">
选择此产品
</button>
</view>
</view>
</view>
</view>
</template>
<style lang="scss" scoped>
.product-select-page {
min-height: 100vh;
background: #f5f7fa;
padding-bottom: 200rpx;
}
.header {
display: flex;
align-items: center;
background: #fff;
padding: 20rpx 30rpx;
border-bottom: 1rpx solid #f5f5f5;
.back-btn {
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
margin-right: 20rpx;
text {
font-size: 36rpx;
color: #333;
}
}
.header-title {
font-size: 30rpx;
font-weight: 600;
color: #333;
flex: 1;
text-align: center;
margin-right: 60rpx;
}
}
.search-bar {
background: #fff;
padding: 20rpx 30rpx;
border-bottom: 1rpx solid #f5f5f5;
.search-input-wrap {
display: flex;
align-items: center;
background: #f5f7fa;
border-radius: 36rpx;
padding: 16rpx 24rpx;
.search-icon {
font-size: 32rpx;
color: #999;
margin-right: 12rpx;
}
.search-input {
flex: 1;
font-size: 28rpx;
}
.clear-icon {
font-size: 36rpx;
color: #999;
padding: 10rpx;
}
}
}
.loading-state {
display: flex;
align-items: center;
justify-content: center;
padding: 100rpx 0;
color: #999;
}
.product-list {
padding: 20rpx;
.product-item {
display: flex;
justify-content: space-between;
align-items: center;
background: #fff;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 16rpx;
&:last-child {
margin-bottom: 0;
}
.product-main {
display: flex;
align-items: center;
flex: 1;
.product-icon {
width: 80rpx;
height: 80rpx;
background: linear-gradient(135deg, #00c05a 0%, #34d19d 100%);
border-radius: 16rpx;
display: flex;
align-items: center;
justify-content: center;
margin-right: 20rpx;
text {
font-size: 40rpx;
color: #fff;
}
}
.product-info {
flex: 1;
.product-name {
font-size: 30rpx;
font-weight: 600;
color: #333;
margin-bottom: 8rpx;
display: block;
}
.product-tags {
display: flex;
gap: 12rpx;
margin-bottom: 8rpx;
.tag {
display: inline-block;
padding: 4rpx 12rpx;
border-radius: 6rpx;
font-size: 22rpx;
&.type {
background: #e6f7eb;
color: #00c05a;
}
}
}
.product-desc {
font-size: 24rpx;
color: #666;
line-height: 1.5;
margin-bottom: 12rpx;
display: block;
}
.product-range {
display: flex;
align-items: center;
gap: 8rpx;
.range-label {
font-size: 24rpx;
color: #999;
}
.range-value {
font-size: 24rpx;
color: #ff8f0d;
font-weight: 600;
}
}
}
}
.product-actions {
display: flex;
align-items: center;
gap: 16rpx;
.info-btn {
display: flex;
align-items: center;
gap: 6rpx;
padding: 8rpx 16rpx;
background: #f5f7fa;
border-radius: 8rpx;
font-size: 24rpx;
color: #666;
text:first-child {
font-size: 24rpx;
}
text:last-child {
font-size: 24rpx;
}
}
.select-icon {
font-size: 28rpx;
color: #ccc;
}
}
}
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 0;
color: #999;
.empty-icon {
font-size: 80rpx;
margin-bottom: 20rpx;
color: #ccc;
}
.empty-text {
font-size: 26rpx;
}
}
.tips-card {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: #fff;
padding: 24rpx 30rpx;
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.05);
.tips-header {
display: flex;
align-items: center;
gap: 8rpx;
margin-bottom: 12rpx;
.tips-icon {
font-size: 28rpx;
color: #f59e0b;
}
.tips-title {
font-size: 28rpx;
font-weight: 600;
color: #333;
}
}
.tips-content {
display: flex;
flex-direction: column;
gap: 8rpx;
.tips-item {
font-size: 24rpx;
color: #666;
line-height: 1.5;
}
}
}
.modal-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-content {
background: #fff;
border-radius: 24rpx;
width: 600rpx;
max-height: 80vh;
overflow-y: auto;
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 30rpx;
.modal-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
}
.close-btn {
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
text {
font-size: 36rpx;
color: #999;
}
}
}
.modal-body {
padding: 0 30rpx 30rpx;
.detail-row {
display: flex;
justify-content: space-between;
padding: 16rpx 0;
border-bottom: 1rpx solid #f5f5f5;
&:last-child {
border-bottom: none;
}
.detail-label {
font-size: 26rpx;
color: #666;
flex-shrink: 0;
margin-right: 20rpx;
}
.detail-value {
flex: 1;
text-align: right;
font-size: 26rpx;
color: #333;
&.desc {
text-align: left;
line-height: 1.5;
}
&.amount {
color: #ff8f0d;
font-weight: 600;
}
}
}
}
.modal-footer {
padding: 20rpx 30rpx 30rpx;
.btn {
width: 100%;
height: 80rpx;
line-height: 80rpx;
border-radius: 40rpx;
background: #00c05a;
color: #fff;
font-size: 28rpx;
}
}
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,491 @@
<script lang="ts" setup>
import type { ClaimApplication } from '@/api/types/insurance'
import { getClaimApplicationDetail, reviewClaimApplication } from '@/api/insurance'
definePage({
style: {
navigationBarTitleText: '理赔审核详情',
},
})
const claimId = ref('')
const claim = ref<ClaimApplication | null>(null)
const loading = ref(false)
const reviewing = ref(false)
const rejectionReason = ref('')
const payoutAmount = ref('')
// 状态映射
const statusMap: Record<string, { text: string, color: string }> = {
pending: { text: '待审核', color: '#F59E0B' },
approved: { text: '已通过', color: '#00c05a' },
rejected: { text: '已拒绝', color: '#fa4350' },
}
const statusInfo = computed(() => {
if (!claim.value)
return null
return statusMap[claim.value.status] || { text: claim.value.status, color: '#666' }
})
async function loadDetail() {
loading.value = true
try {
const res = await getClaimApplicationDetail(claimId.value)
claim.value = res
if (res.payoutAmount) {
payoutAmount.value = res.payoutAmount.toString()
}
}
finally {
loading.value = false
}
}
// 预览图片
function handlePreviewImage(url: string) {
uni.previewImage({
urls: [url],
})
}
// 理赔审核通过
async function handleApprove() {
if (!payoutAmount.value || Number(payoutAmount.value) <= 0) {
uni.showToast({ title: '请输入赔付金额', icon: 'none' })
return
}
reviewing.value = true
try {
await reviewClaimApplication(claimId.value, {
approved: true,
payoutAmount: Number(payoutAmount.value),
})
uni.showToast({ title: '审核通过', icon: 'success' })
setTimeout(() => {
uni.navigateBack()
}, 1500)
}
finally {
reviewing.value = false
}
}
// 理赔审核拒绝
async function handleReject() {
if (!rejectionReason.value) {
uni.showToast({ title: '请填写拒绝原因', icon: 'none' })
return
}
reviewing.value = true
try {
await reviewClaimApplication(claimId.value, {
approved: false,
rejectionReason: rejectionReason.value,
})
uni.showToast({ title: '已拒绝', icon: 'success' })
setTimeout(() => {
uni.navigateBack()
}, 1500)
}
finally {
reviewing.value = false
}
}
onLoad((options) => {
if (options?.id) {
claimId.value = options.id
loadDetail()
}
})
</script>
<template>
<view class="claim-review-detail-page">
<view v-if="loading" class="loading-state">
加载中...
</view>
<template v-else-if="claim">
<!-- 状态卡片 -->
<view class="status-card" :style="{ background: statusInfo?.color }">
<text class="status-text">{{ statusInfo?.text }}</text>
</view>
<!-- 银行信息 -->
<view class="section-card">
<view class="card-header">
银行信息
</view>
<view class="info-list">
<view class="info-item">
<text class="label">银行名称</text>
<text class="value">{{ claim.bankName }}</text>
</view>
<view class="info-item">
<text class="label">保险单号</text>
<text class="value">{{ claim.policyNumber }}</text>
</view>
</view>
</view>
<!-- 保险公司信息 -->
<view class="section-card">
<view class="card-header">
保险公司信息
</view>
<view class="info-list">
<view class="info-item">
<text class="label">保险公司</text>
<text class="value">{{ claim.companyName }}</text>
</view>
</view>
</view>
<!-- 理赔信息 -->
<view class="section-card">
<view class="card-header">
理赔信息
</view>
<view class="info-list">
<view class="info-item">
<text class="label">理赔金额</text>
<text class="value amount">{{ claim.claimAmount }}</text>
</view>
<view class="info-item">
<text class="label">理赔原因</text>
<text class="value reason">{{ claim.claimReason }}</text>
</view>
<view class="info-item">
<text class="label">提交时间</text>
<text class="value">{{ claim.submittedAt }}</text>
</view>
</view>
</view>
<!-- 理赔材料 -->
<view class="section-card">
<view class="card-header">
理赔材料
</view>
<view class="materials-list">
<view
v-for="material in claim.materials"
:key="material.id"
class="material-item"
@click="handlePreviewImage(material.url)"
>
<image :src="material.url" mode="aspectFill" />
<view class="material-info">
<text class="material-name">{{ material.name }}</text>
<text class="upload-time">{{ material.uploadTime }}</text>
</view>
</view>
</view>
</view>
<!-- 审核操作 -->
<view v-if="claim.status === 'pending'" class="action-card">
<view class="card-header">
理赔审核
</view>
<view class="action-buttons">
<button
class="btn approve"
:disabled="reviewing"
@click="handleApprove"
>
<text v-if="reviewing">处理中...</text>
<text v-else>通过</text>
</button>
<button
class="btn reject"
:disabled="reviewing"
@click="handleReject"
>
<text v-if="reviewing">处理中...</text>
<text v-else>拒绝</text>
</button>
</view>
<view class="payout-input">
<text class="label">赔付金额</text>
<input
v-model="payoutAmount"
type="digit"
class="input"
placeholder="请输入赔付金额"
>
</view>
<view v-if="!reviewing" class="reject-reason">
<textarea
v-model="rejectionReason"
class="textarea"
placeholder="请输入拒绝原因..."
:maxlength="500"
/>
<text class="char-count">{{ rejectionReason.length }}/500</text>
</view>
</view>
<!-- 已审核信息 -->
<view v-if="claim.status !== 'pending'" class="section-card">
<view class="card-header">
审核信息
</view>
<view class="info-list">
<view class="info-item">
<text class="label">审核时间</text>
<text class="value">{{ claim.reviewedAt }}</text>
</view>
<view class="info-item">
<text class="label">审核人员</text>
<text class="value">{{ claim.reviewedBy }}</text>
</view>
<view v-if="claim.payoutAmount" class="info-item">
<text class="label">赔付金额</text>
<text class="value payout">{{ claim.payoutAmount }}</text>
</view>
<view v-if="claim.payoutDate" class="info-item">
<text class="label">赔付日期</text>
<text class="value">{{ claim.payoutDate }}</text>
</view>
<view v-if="claim.rejectionReason" class="info-item full">
<text class="label">拒绝原因</text>
<text class="value reason">{{ claim.rejectionReason }}</text>
</view>
</view>
</view>
</template>
</view>
</template>
<style lang="scss" scoped>
.claim-review-detail-page {
min-height: 100vh;
background: #f5f7fa;
padding: 20rpx;
padding-bottom: 120rpx;
}
.loading-state {
display: flex;
align-items: center;
justify-content: center;
padding: 100rpx 0;
color: #999;
}
.status-card {
background: linear-gradient(135deg, #00c05a 0%, #34d19d 100%);
border-radius: 16rpx;
padding: 40rpx 30rpx;
color: #fff;
margin-bottom: 20rpx;
text-align: center;
.status-text {
font-size: 36rpx;
font-weight: bold;
display: block;
}
}
.section-card {
background: #fff;
border-radius: 16rpx;
padding: 0 30rpx;
margin-bottom: 20rpx;
.card-header {
padding: 30rpx 0;
border-bottom: 1rpx solid #f5f5f5;
font-size: 28rpx;
font-weight: bold;
color: #333;
}
}
.info-list {
padding: 20rpx 0;
.info-item {
display: flex;
justify-content: space-between;
padding: 16rpx 0;
font-size: 26rpx;
border-bottom: 1rpx solid #f9f9f9;
&:last-child {
border-bottom: none;
}
.label {
color: #666;
}
.value {
color: #333;
text-align: right;
&.amount {
color: #ff8f0d;
font-weight: 600;
}
&.reason {
text-align: left;
line-height: 1.5;
}
&.payout {
color: #00c05a;
font-weight: 600;
}
}
&.full {
flex-direction: column;
align-items: flex-start;
.value {
margin-top: 8rpx;
width: 100%;
}
}
}
}
.materials-list {
padding: 20rpx 0;
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16rpx;
.material-item {
position: relative;
border-radius: 12rpx;
overflow: hidden;
background: #f8f9fa;
image {
width: 100%;
height: 200rpx;
display: block;
}
.material-info {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: rgba(0, 0, 0, 0.7);
padding: 12rpx;
display: flex;
flex-direction: column;
gap: 4rpx;
.material-name {
font-size: 22rpx;
color: #fff;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.upload-time {
font-size: 20rpx;
color: rgba(255, 255, 255, 0.8);
}
}
}
}
.action-card {
background: #fff;
border-radius: 16rpx;
padding: 0 30rpx;
margin-bottom: 20rpx;
.card-header {
padding: 30rpx 0;
border-bottom: 1rpx solid #f5f5f5;
font-size: 28rpx;
font-weight: bold;
color: #333;
}
.action-buttons {
display: flex;
gap: 20rpx;
padding: 20rpx 0;
.btn {
flex: 1;
font-size: 28rpx;
height: 80rpx;
line-height: 80rpx;
border-radius: 40rpx;
&.approve {
background: #00c05a;
color: #fff;
}
&.reject {
background: #fa4350;
color: #fff;
}
&:disabled {
opacity: 0.6;
}
}
}
.payout-input {
padding: 0 0 20rpx 0;
.label {
display: block;
font-size: 26rpx;
color: #666;
margin-bottom: 12rpx;
}
.input {
width: 100%;
padding: 16rpx;
background: #f8f9fa;
border-radius: 8rpx;
font-size: 26rpx;
color: #333;
}
}
.reject-reason {
padding: 0 0 20rpx 0;
.textarea {
width: 100%;
min-height: 150rpx;
padding: 16rpx;
background: #f8f9fa;
border-radius: 8rpx;
font-size: 26rpx;
color: #333;
line-height: 1.5;
}
.char-count {
font-size: 22rpx;
color: #999;
text-align: right;
margin-top: 8rpx;
display: block;
}
}
}
</style>

View File

@@ -0,0 +1,339 @@
<script lang="ts" setup>
import type { ClaimApplication } from '@/api/types/insurance'
import { getClaimReviewApplications } from '@/api/insurance'
definePage({
style: {
navigationBarTitleText: '理赔审核',
},
})
const loading = ref(false)
const claims = ref<ClaimApplication[]>([])
const currentTab = ref('pending')
// 状态映射
const statusMap: Record<string, { text: string, color: string }> = {
pending: { text: '待审核', color: '#F59E0B' },
approved: { text: '已通过', color: '#00c05a' },
rejected: { text: '已拒绝', color: '#fa4350' },
}
const tabs = [
{ key: 'pending', label: '待审核' },
{ key: 'approved', label: '已通过' },
{ key: 'rejected', label: '已拒绝' },
]
// 筛选后的列表
const filteredClaims = computed(() => {
if (currentTab.value === 'pending') {
return claims.value.filter(c => c.status === 'pending')
}
return claims.value.filter(c => c.status === currentTab.value)
})
// 切换标签
function handleTabChange(tab: string) {
currentTab.value = tab
}
// 查看详情
function handleViewDetail(id: string) {
uni.navigateTo({
url: `/pagesInsurance/claim-review/detail?id=${id}`,
})
}
// 加载列表
async function loadList() {
loading.value = true
try {
const res = await getClaimReviewApplications()
claims.value = res.list
}
finally {
loading.value = false
}
}
onShow(() => {
loadList()
})
</script>
<template>
<view class="claim-review-list-page">
<!-- 标签页 -->
<view class="tabs">
<view
v-for="tab in tabs"
:key="tab.key"
class="tab-item"
:class="{ active: currentTab === tab.key }"
@click="handleTabChange(tab.key)"
>
<text class="tab-text">{{ tab.label }}</text>
<view v-if="currentTab === tab.key" class="tab-indicator" />
</view>
</view>
<!-- 列表 -->
<view v-if="loading" class="loading-state">
加载中...
</view>
<view v-else-if="filteredClaims.length > 0" class="claim-list">
<view
v-for="claim in filteredClaims"
:key="claim.id"
class="claim-item"
@click="handleViewDetail(claim.id)"
>
<view class="claim-header">
<view class="claim-info">
<text class="claim-id">理赔申请号: {{ claim.id }}</text>
<text class="bank-name">{{ claim.bankName }}</text>
</view>
<view
class="status-tag"
:style="{ background: statusMap[claim.status]?.color }"
>
{{ statusMap[claim.status]?.text }}
</view>
</view>
<view class="claim-body">
<view class="claim-row">
<text class="label">保险单号</text>
<text class="value">{{ claim.policyNumber }}</text>
</view>
<view class="claim-row">
<text class="label">保险公司</text>
<text class="value">{{ claim.companyName }}</text>
</view>
<view class="claim-row">
<text class="label">理赔金额</text>
<text class="value amount">{{ claim.claimAmount }}</text>
</view>
<view class="claim-row">
<text class="label">理赔原因</text>
<text class="value reason">{{ claim.claimReason }}</text>
</view>
<view class="claim-row">
<text class="label">提交时间</text>
<text class="value">{{ claim.submittedAt }}</text>
</view>
<view v-if="claim.payoutAmount" class="claim-row">
<text class="label">赔付金额</text>
<text class="value payout">{{ claim.payoutAmount }}</text>
</view>
<view v-if="claim.payoutDate" class="claim-row">
<text class="label">赔付日期</text>
<text class="value">{{ claim.payoutDate }}</text>
</view>
<view v-if="claim.rejectionReason" class="claim-row">
<text class="label">拒绝原因</text>
<text class="value reject">{{ claim.rejectionReason }}</text>
</view>
</view>
<view class="claim-footer">
<text class="material-count">已上传 {{ claim.materials.length }} 份材料</text>
<text class="arrow-icon i-carbon-chevron-right" />
</view>
</view>
</view>
<!-- 空状态 -->
<view v-else class="empty-state">
<text class="empty-icon i-carbon-document" />
<text class="empty-text">暂无理赔申请</text>
</view>
</view>
</template>
<style lang="scss" scoped>
.claim-review-list-page {
min-height: 100vh;
background: #f5f7fa;
}
.tabs {
background: #fff;
display: flex;
padding: 0 30rpx;
border-bottom: 1rpx solid #f5f5f5;
.tab-item {
flex: 1;
position: relative;
padding: 30rpx 0;
text-align: center;
font-size: 28rpx;
color: #666;
&.active {
color: #00c05a;
font-weight: 600;
}
.tab-text {
position: relative;
z-index: 1;
}
.tab-indicator {
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 60rpx;
height: 4rpx;
background: #00c05a;
border-radius: 2rpx;
}
}
}
.loading-state {
display: flex;
align-items: center;
justify-content: center;
padding: 100rpx 0;
color: #999;
}
.claim-list {
padding: 20rpx;
.claim-item {
background: #fff;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 16rpx;
&:last-child {
margin-bottom: 0;
}
.claim-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
.claim-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 8rpx;
.claim-id {
font-size: 28rpx;
font-weight: 600;
color: #333;
}
.bank-name {
font-size: 24rpx;
color: #666;
}
}
.status-tag {
padding: 8rpx 20rpx;
border-radius: 8rpx;
font-size: 24rpx;
color: #fff;
}
}
.claim-body {
background: #f8f9fa;
border-radius: 12rpx;
padding: 20rpx;
margin-bottom: 16rpx;
.claim-row {
display: flex;
justify-content: space-between;
padding: 12rpx 0;
font-size: 26rpx;
border-bottom: 1rpx solid #f5f5f5;
&:last-child {
border-bottom: none;
}
.label {
color: #666;
}
.value {
color: #333;
text-align: right;
flex: 1;
&.amount {
color: #ff8f0d;
font-weight: 600;
}
&.reason {
text-align: left;
line-height: 1.5;
}
&.payout {
color: #00c05a;
font-weight: 600;
}
&.reject {
color: #fa4350;
text-align: left;
line-height: 1.5;
}
}
}
}
.claim-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 12rpx;
.material-count {
font-size: 24rpx;
color: #999;
}
.arrow-icon {
font-size: 28rpx;
color: #ccc;
}
}
}
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 0;
color: #999;
.empty-icon {
font-size: 80rpx;
margin-bottom: 20rpx;
color: #ccc;
}
.empty-text {
font-size: 26rpx;
}
}
</style>

View File

@@ -58,11 +58,14 @@
<view class="card-status-bar"> <view class="card-status-bar">
<text class="status-btn" :class="item.status">{{ item.statusText }}</text> <text class="status-btn" :class="item.status">{{ item.statusText }}</text>
<view class="actions">
<view class="view-policy-btn" @click="goPolicyDetail">查看保单</view>
<text class="i-carbon-chevron-right text-gray-400" /> <text class="i-carbon-chevron-right text-gray-400" />
</view> </view>
</view> </view>
</view> </view>
</view> </view>
</view>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@@ -109,16 +112,58 @@ const claimList = ref([
reason: '意外事故导致还款能力丧失', reason: '意外事故导致还款能力丧失',
status: 'processing', status: 'processing',
statusText: '复核中' statusText: '复核中'
},
{
id: '4',
claimNo: 'C20251228005',
time: '2025-12-28 11:00',
amount: 80000,
bankName: '某某商业银行',
customerName: '赵某某',
reason: '企业破产清算',
status: 'processing',
statusText: '审核中'
},
{
id: '5',
claimNo: 'C20251215001',
time: '2025-12-15 14:00',
amount: 200000,
bankName: '某某商业银行',
customerName: '钱某某',
reason: '贷款逾期超过180天',
status: 'completed',
statusText: '已赔付'
},
{
id: '6',
claimNo: 'C20251120003',
time: '2025-11-20 09:30',
amount: 60000,
bankName: '某某村镇银行',
customerName: '孙某某',
reason: '资料不全,无法证明损失',
status: 'rejected',
statusText: '已拒绝'
} }
]) ])
const filteredList = computed(() => { const filteredList = computed(() => {
if (currentTab.value === 'completed') {
return claimList.value.filter(item => ['completed', 'rejected'].includes(item.status))
}
return claimList.value.filter(item => item.status === currentTab.value) return claimList.value.filter(item => item.status === currentTab.value)
}) })
function goDetail(id: string) { function goDetail(id: string) {
uni.navigateTo({ url: `/pagesInsurance/claim/detail?id=${id}` }) uni.navigateTo({ url: `/pagesInsurance/claim/detail?id=${id}` })
} }
function goPolicyDetail(e: Event) {
e.stopPropagation()
uni.showToast({ title: '查看保单详情', icon: 'none' })
// uni.navigateTo({ url: `/pagesInsurance/policy/detail?id=1` })
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@@ -249,6 +294,26 @@ function goDetail(id: string) {
&.pending { color: #f57c00; } &.pending { color: #f57c00; }
&.processing { color: #0957DE; } &.processing { color: #0957DE; }
&.completed { color: #38a169; } &.completed { color: #38a169; }
&.rejected { color: #fa4350; }
}
.actions {
display: flex;
align-items: center;
gap: 16rpx;
.view-policy-btn {
font-size: 24rpx;
color: #666;
padding: 8rpx 20rpx;
background: #f5f5f5;
border-radius: 24rpx;
&:active {
opacity: 0.8;
background: #eee;
}
}
} }
} }
} }

View File

@@ -10,6 +10,18 @@
<view class="policy-list"> <view class="policy-list">
<!-- 搜索和筛选 --> <!-- 搜索和筛选 -->
<view class="header-search"> <view class="header-search">
<view class="action-buttons">
<view class="action-btn" @click="goToUnderwriting">
<text class="i-carbon-task icon"></text>
<text>待核保</text>
<view class="badge">3</view>
</view>
<view class="action-btn" @click="goToClaimReview">
<text class="i-carbon-review icon"></text>
<text>理赔审核</text>
<view class="badge warning">2</view>
</view>
</view>
<wd-search v-model="keyword" placeholder="搜索保单号/客户名" @search="onSearch" /> <wd-search v-model="keyword" placeholder="搜索保单号/客户名" @search="onSearch" />
<view class="filter-tabs"> <view class="filter-tabs">
<view <view
@@ -147,6 +159,14 @@ function goDetail(id: string) {
function handleRenew(item: any) { function handleRenew(item: any) {
uni.showToast({ title: '发起续保', icon: 'none' }) uni.showToast({ title: '发起续保', icon: 'none' })
} }
function goToUnderwriting() {
uni.navigateTo({ url: '/pagesInsurance/underwriting/list' })
}
function goToClaimReview() {
uni.navigateTo({ url: '/pagesInsurance/claim-review/list' })
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@@ -162,6 +182,48 @@ function handleRenew(item: any) {
position: sticky; position: sticky;
top: 0; top: 0;
z-index: 10; z-index: 10;
.action-buttons {
display: flex;
gap: 20rpx;
margin-bottom: 24rpx;
.action-btn {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 12rpx;
padding: 16rpx;
background: #f8f9fa;
border-radius: 12rpx;
position: relative;
font-size: 28rpx;
color: #333;
font-weight: 500;
.icon {
font-size: 32rpx;
color: #0957DE;
}
.badge {
position: absolute;
top: -6rpx;
right: -6rpx;
background: #fa4350;
color: #fff;
font-size: 20rpx;
padding: 2rpx 10rpx;
border-radius: 20rpx;
transform: scale(0.9);
&.warning {
background: #ff8f0d;
}
}
}
}
} }
.filter-tabs { .filter-tabs {

View File

@@ -0,0 +1,405 @@
<script lang="ts" setup>
import type { InsuranceApplication } from '@/api/types/insurance'
import { getInsuranceApplicationDetail, reviewUnderwritingApplication } from '@/api/insurance'
definePage({
style: {
navigationBarTitleText: '核保申请详情',
},
})
const applicationId = ref('')
const application = ref<InsuranceApplication | null>(null)
const loading = ref(false)
const reviewing = ref(false)
const rejectionReason = ref('')
// 状态映射
const statusMap: Record<string, { text: string, color: string }> = {
pending: { text: '待审核', color: '#F59E0B' },
approved: { text: '已通过', color: '#00c05a' },
rejected: { text: '已拒绝', color: '#fa4350' },
}
const statusInfo = computed(() => {
if (!application.value)
return null
return statusMap[application.value.status] || { text: application.value.status, color: '#666' }
})
async function loadDetail() {
loading.value = true
try {
const res = await getInsuranceApplicationDetail(applicationId.value)
application.value = res
}
finally {
loading.value = false
}
}
// 核保通过
async function handleApprove() {
reviewing.value = true
try {
await reviewUnderwritingApplication(applicationId.value, {
approved: true,
})
uni.showToast({ title: '核保通过', icon: 'success' })
setTimeout(() => {
uni.navigateBack()
}, 1500)
}
finally {
reviewing.value = false
}
}
// 核保拒绝
async function handleReject() {
if (!rejectionReason.value) {
uni.showToast({ title: '请填写拒绝原因', icon: 'none' })
return
}
reviewing.value = true
try {
await reviewUnderwritingApplication(applicationId.value, {
approved: false,
rejectionReason: rejectionReason.value,
})
uni.showToast({ title: '已拒绝', icon: 'success' })
setTimeout(() => {
uni.navigateBack()
}, 1500)
}
finally {
reviewing.value = false
}
}
onLoad((options) => {
if (options?.id) {
applicationId.value = options.id
loadDetail()
}
})
</script>
<template>
<view class="underwriting-detail-page">
<view v-if="loading" class="loading-state">
加载中...
</view>
<template v-else-if="application">
<!-- 状态卡片 -->
<view class="status-card" :style="{ background: statusInfo?.color }">
<text class="status-text">{{ statusInfo?.text }}</text>
</view>
<!-- 银行信息 -->
<view class="section-card">
<view class="card-header">
银行信息
</view>
<view class="info-list">
<view class="info-item">
<text class="label">银行名称</text>
<text class="value">{{ application.bankName }}</text>
</view>
<view class="info-item">
<text class="label">贷款编号</text>
<text class="value">{{ application.loanId }}</text>
</view>
</view>
</view>
<!-- 保险公司信息 -->
<view class="section-card">
<view class="card-header">
保险公司信息
</view>
<view class="info-list">
<view class="info-item">
<text class="label">保险公司</text>
<text class="value">{{ application.companyName }}</text>
</view>
<view class="info-item">
<text class="label">保险产品</text>
<text class="value">{{ application.productName }}</text>
</view>
</view>
</view>
<!-- 客户信息 -->
<view class="section-card">
<view class="card-header">
客户信息
</view>
<view class="info-list">
<view class="info-item">
<text class="label">客户姓名</text>
<text class="value">{{ application.customerInfo.name }}</text>
</view>
<view class="info-item">
<text class="label">身份证号</text>
<text class="value">{{ application.customerInfo.idNumber }}</text>
</view>
<view class="info-item">
<text class="label">信用评分</text>
<text class="value score">{{ application.customerInfo.creditScore }}</text>
</view>
<view class="info-item">
<text class="label">贷款金额</text>
<text class="value">{{ application.customerInfo.loanAmount }}</text>
</view>
<view class="info-item">
<text class="label">贷款期限</text>
<text class="value">{{ application.customerInfo.loanTerm }}</text>
</view>
</view>
</view>
<!-- 保险信息 -->
<view class="section-card">
<view class="card-header">
保险信息
</view>
<view class="info-list">
<view class="info-item">
<text class="label">保险金额</text>
<text class="value amount">{{ application.insuranceAmount }}</text>
</view>
<view class="info-item">
<text class="label">保险期限</text>
<text class="value">{{ application.insuranceTerm }}</text>
</view>
<view class="info-item">
<text class="label">申请时间</text>
<text class="value">{{ application.createdAt }}</text>
</view>
</view>
</view>
<!-- 审核操作 -->
<view v-if="application.status === 'pending'" class="action-card">
<view class="card-header">
核保审核
</view>
<view class="action-buttons">
<button
class="btn approve"
:disabled="reviewing"
@click="handleApprove"
>
<text v-if="reviewing">处理中...</text>
<text v-else>通过</text>
</button>
<button
class="btn reject"
:disabled="reviewing"
@click="handleReject"
>
<text v-if="reviewing">处理中...</text>
<text v-else>拒绝</text>
</button>
</view>
<view v-if="!reviewing" class="reject-reason">
<textarea
v-model="rejectionReason"
class="textarea"
placeholder="请输入拒绝原因..."
:maxlength="500"
/>
<text class="char-count">{{ rejectionReason.length }}/500</text>
</view>
</view>
<!-- 已审核信息 -->
<view v-if="application.status !== 'pending'" class="section-card">
<view class="card-header">
审核信息
</view>
<view class="info-list">
<view class="info-item">
<text class="label">审核时间</text>
<text class="value">{{ application.reviewedAt }}</text>
</view>
<view class="info-item">
<text class="label">审核人员</text>
<text class="value">{{ application.reviewedBy }}</text>
</view>
<view v-if="application.rejectionReason" class="info-item full">
<text class="label">拒绝原因</text>
<text class="value reason">{{ application.rejectionReason }}</text>
</view>
</view>
</view>
</template>
</view>
</template>
<style lang="scss" scoped>
.underwriting-detail-page {
min-height: 100vh;
background: #f5f7fa;
padding: 20rpx;
padding-bottom: 120rpx;
}
.loading-state {
display: flex;
align-items: center;
justify-content: center;
padding: 100rpx 0;
color: #999;
}
.status-card {
background: linear-gradient(135deg, #00c05a 0%, #34d19d 100%);
border-radius: 16rpx;
padding: 40rpx 30rpx;
color: #fff;
margin-bottom: 20rpx;
text-align: center;
.status-text {
font-size: 36rpx;
font-weight: bold;
display: block;
}
}
.section-card {
background: #fff;
border-radius: 16rpx;
padding: 0 30rpx;
margin-bottom: 20rpx;
.card-header {
padding: 30rpx 0;
border-bottom: 1rpx solid #f5f5f5;
font-size: 28rpx;
font-weight: bold;
color: #333;
}
}
.action-card {
background: #fff;
border-radius: 16rpx;
padding: 0 30rpx;
margin-bottom: 20rpx;
.card-header {
padding: 30rpx 0;
border-bottom: 1rpx solid #f5f5f5;
font-size: 28rpx;
font-weight: bold;
color: #333;
}
.action-buttons {
display: flex;
gap: 20rpx;
padding: 20rpx 0;
.btn {
flex: 1;
font-size: 28rpx;
height: 80rpx;
line-height: 80rpx;
border-radius: 40rpx;
&.approve {
background: #00c05a;
color: #fff;
}
&.reject {
background: #fa4350;
color: #fff;
}
&:disabled {
opacity: 0.6;
}
}
}
.reject-reason {
padding: 0 0 20rpx 0;
.textarea {
width: 100%;
min-height: 150rpx;
padding: 16rpx;
background: #f8f9fa;
border-radius: 8rpx;
font-size: 26rpx;
color: #333;
line-height: 1.5;
}
.char-count {
font-size: 22rpx;
color: #999;
text-align: right;
margin-top: 8rpx;
display: block;
}
}
}
.info-list {
padding: 20rpx 0;
.info-item {
display: flex;
justify-content: space-between;
padding: 16rpx 0;
font-size: 26rpx;
border-bottom: 1rpx solid #f9f9f9;
&:last-child {
border-bottom: none;
}
.label {
color: #666;
}
.value {
color: #333;
text-align: right;
&.score {
color: #00c05a;
font-weight: 600;
}
&.amount {
color: #ff8f0d;
font-weight: 600;
}
&.reason {
color: #fa4350;
text-align: left;
line-height: 1.5;
}
}
&.full {
flex-direction: column;
align-items: flex-start;
.value {
margin-top: 8rpx;
width: 100%;
}
}
}
}
</style>

View File

@@ -0,0 +1,321 @@
<script lang="ts" setup>
import type { InsuranceApplication } from '@/api/types/insurance'
import { getUnderwritingApplications } from '@/api/insurance'
definePage({
style: {
navigationBarTitleText: '待核保申请',
},
})
const loading = ref(false)
const applications = ref<InsuranceApplication[]>([])
const currentTab = ref('pending')
// 状态映射
const statusMap: Record<string, { text: string, color: string }> = {
pending: { text: '待审核', color: '#F59E0B' },
approved: { text: '已通过', color: '#00c05a' },
rejected: { text: '已拒绝', color: '#fa4350' },
}
const tabs = [
{ key: 'pending', label: '待审核' },
{ key: 'approved', label: '已通过' },
{ key: 'rejected', label: '已拒绝' },
]
// 筛选后的列表
const filteredApplications = computed(() => {
if (currentTab.value === 'pending') {
return applications.value.filter(a => a.status === 'pending')
}
return applications.value.filter(a => a.status === currentTab.value)
})
// 切换标签
function handleTabChange(tab: string) {
currentTab.value = tab
}
// 查看详情
function handleViewDetail(id: string) {
uni.navigateTo({
url: `/pagesInsurance/underwriting/detail?id=${id}`,
})
}
// 加载列表
async function loadList() {
loading.value = true
try {
const res = await getUnderwritingApplications()
applications.value = res.list
}
finally {
loading.value = false
}
}
onShow(() => {
loadList()
})
</script>
<template>
<view class="underwriting-list-page">
<!-- 标签页 -->
<view class="tabs">
<view
v-for="tab in tabs"
:key="tab.key"
class="tab-item"
:class="{ active: currentTab === tab.key }"
@click="handleTabChange(tab.key)"
>
<text class="tab-text">{{ tab.label }}</text>
<view v-if="currentTab === tab.key" class="tab-indicator" />
</view>
</view>
<!-- 列表 -->
<view v-if="loading" class="loading-state">
加载中...
</view>
<view v-else-if="filteredApplications.length > 0" class="application-list">
<view
v-for="app in filteredApplications"
:key="app.id"
class="application-item"
@click="handleViewDetail(app.id)"
>
<view class="app-header">
<view class="app-info">
<text class="app-id">投保申请号: {{ app.id }}</text>
<text class="bank-name">{{ app.bankName }}</text>
</view>
<view
class="status-tag"
:style="{ background: statusMap[app.status]?.color }"
>
{{ statusMap[app.status]?.text }}
</view>
</view>
<view class="app-body">
<view class="app-row">
<text class="label">保险公司</text>
<text class="value">{{ app.companyName }}</text>
</view>
<view class="app-row">
<text class="label">保险产品</text>
<text class="value">{{ app.productName }}</text>
</view>
<view class="app-row">
<text class="label">保险金额</text>
<text class="value amount">{{ app.insuranceAmount }}</text>
</view>
<view class="app-row">
<text class="label">保险期限</text>
<text class="value">{{ app.insuranceTerm }}</text>
</view>
<view class="app-row">
<text class="label">客户姓名</text>
<text class="value">{{ app.customerInfo.name }}</text>
</view>
<view class="app-row">
<text class="label">信用评分</text>
<text class="value score">{{ app.customerInfo.creditScore }}</text>
</view>
<view class="app-row">
<text class="label">贷款金额</text>
<text class="value">{{ app.customerInfo.loanAmount }}</text>
</view>
<view class="app-row">
<text class="label">申请时间</text>
<text class="value">{{ app.createdAt }}</text>
</view>
</view>
<view class="app-footer">
<text class="arrow-icon i-carbon-chevron-right" />
</view>
</view>
</view>
<!-- 空状态 -->
<view v-else class="empty-state">
<text class="empty-icon i-carbon-document" />
<text class="empty-text">暂无投保申请</text>
</view>
</view>
</template>
<style lang="scss" scoped>
.underwriting-list-page {
min-height: 100vh;
background: #f5f7fa;
}
.tabs {
background: #fff;
display: flex;
padding: 0 30rpx;
border-bottom: 1rpx solid #f5f5f5;
.tab-item {
flex: 1;
position: relative;
padding: 30rpx 0;
text-align: center;
font-size: 28rpx;
color: #666;
&.active {
color: #00c05a;
font-weight: 600;
}
.tab-text {
position: relative;
z-index: 1;
}
.tab-indicator {
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 60rpx;
height: 4rpx;
background: #00c05a;
border-radius: 2rpx;
}
}
}
.loading-state {
display: flex;
align-items: center;
justify-content: center;
padding: 100rpx 0;
color: #999;
}
.application-list {
padding: 20rpx;
.application-item {
background: #fff;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 16rpx;
&:last-child {
margin-bottom: 0;
}
.app-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
.app-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 8rpx;
.app-id {
font-size: 28rpx;
font-weight: 600;
color: #333;
}
.bank-name {
font-size: 24rpx;
color: #666;
}
}
.status-tag {
padding: 8rpx 20rpx;
border-radius: 8rpx;
font-size: 24rpx;
color: #fff;
}
}
.app-body {
background: #f8f9fa;
border-radius: 12rpx;
padding: 20rpx;
margin-bottom: 16rpx;
.app-row {
display: flex;
justify-content: space-between;
padding: 12rpx 0;
font-size: 26rpx;
border-bottom: 1rpx solid #f5f5f5;
&:last-child {
border-bottom: none;
}
.label {
color: #666;
}
.value {
color: #333;
text-align: right;
flex: 1;
&.amount {
color: #ff8f0d;
font-weight: 600;
}
&.score {
color: #00c05a;
font-weight: 600;
}
}
}
}
.app-footer {
display: flex;
justify-content: flex-end;
padding-top: 12rpx;
.arrow-icon {
font-size: 28rpx;
color: #ccc;
}
}
}
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 0;
color: #999;
.empty-icon {
font-size: 80rpx;
margin-bottom: 20rpx;
color: #ccc;
}
.empty-text {
font-size: 26rpx;
}
}
</style>