保险前后端,养殖端和保险端小程序

This commit is contained in:
xuqiuyun
2025-09-17 19:01:52 +08:00
parent e4287b83fe
commit 473891163c
218 changed files with 109331 additions and 14103 deletions

View File

@@ -0,0 +1,226 @@
<template>
<view id="app">
<!-- 全局加载组件 -->
<view v-if="appStore.globalLoading" class="global-loading">
<view class="loading-spinner"></view>
<text class="loading-text">加载中...</text>
</view>
<!-- 网络状态提示 -->
<view v-if="!appStore.isOnline" class="network-offline">
<text>网络连接已断开</text>
</view>
</view>
</template>
<script setup>
import { onLaunch, onShow, onHide } from '@dcloudio/uni-app'
import { useAppStore } from '@/stores/app'
import { useUserStore } from '@/stores/user'
// 获取store实例
const appStore = useAppStore()
const userStore = useUserStore()
onLaunch(() => {
console.log('App Launch')
// 初始化应用
initApp()
})
onShow(() => {
console.log('App Show')
// 检查登录状态
userStore.checkAuth()
})
onHide(() => {
console.log('App Hide')
})
/**
* 初始化应用
*/
async function initApp() {
try {
// 初始化应用store
await appStore.initApp()
// 监听网络状态变化
appStore.watchNetworkStatus()
// 检查登录状态
userStore.checkAuth()
console.log('应用初始化完成')
} catch (error) {
console.error('应用初始化失败:', error)
appStore.addError(error)
}
}
</script>
<style lang="scss">
/* 全局样式 */
@import '@/styles/variables.scss';
@import '@/styles/base.scss';
#app {
height: 100vh;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif;
}
/* 全局加载样式 */
.global-loading {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.9);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 9999;
.loading-spinner {
width: 40rpx;
height: 40rpx;
border: 4rpx solid #f3f3f3;
border-top: 4rpx solid $primary-color;
border-radius: 50%;
animation: spin 1s linear infinite;
}
.loading-text {
margin-top: 20rpx;
font-size: 28rpx;
color: $text-secondary;
}
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* 网络离线提示 */
.network-offline {
position: fixed;
top: 0;
left: 0;
right: 0;
background: #ff4d4f;
color: white;
text-align: center;
padding: 20rpx;
font-size: 24rpx;
z-index: 9998;
}
/* 通用样式类 */
.container {
padding: 30rpx;
}
.section {
margin-bottom: 40rpx;
}
.section-title {
font-size: 32rpx;
font-weight: bold;
margin-bottom: 20rpx;
color: $text-primary;
}
.card {
background: white;
border-radius: 12rpx;
padding: 30rpx;
box-shadow: 0 2rpx 12rpx 0 rgba(0, 0, 0, 0.1);
}
.btn {
display: inline-block;
padding: 20rpx 40rpx;
border-radius: 8rpx;
text-align: center;
font-size: 28rpx;
transition: all 0.3s;
&.btn-primary {
background: $primary-color;
color: white;
&:active {
background: darken($primary-color, 10%);
}
}
&.btn-secondary {
background: $background-light;
color: $text-primary;
border: 1rpx solid $border-color;
&:active {
background: darken($background-light, 5%);
}
}
}
.text-primary {
color: $text-primary;
}
.text-secondary {
color: $text-secondary;
}
.text-success {
color: $success-color;
}
.text-warning {
color: $warning-color;
}
.text-error {
color: $error-color;
}
.flex {
display: flex;
}
.flex-center {
display: flex;
align-items: center;
justify-content: center;
}
.flex-between {
display: flex;
align-items: center;
justify-content: space-between;
}
.mt-20 {
margin-top: 20rpx;
}
.mb-20 {
margin-bottom: 20rpx;
}
.p-20 {
padding: 20rpx;
}
.p-30 {
padding: 30rpx;
}
</style>

View File

@@ -0,0 +1,232 @@
# 保险端微信小程序 - 开发指南
## 项目概述
这是一个基于 Vue.js 3.x + uni-app 开发的保险服务微信小程序,后端完全动态调用现有的 insurance_backend 接口,实现了用户登录、产品浏览、在线投保、保单管理、理赔申请等核心功能。
## 技术栈
- **前端框架**: Vue.js 3.x (Composition API)
- **小程序框架**: uni-app
- **状态管理**: Pinia
- **UI组件**: 自定义组件 + uni-app内置组件
- **网络请求**: 基于uni.request()封装
- **后端API**: 动态调用 c:\nxxmdata\insurance_backend 的所有接口
## 环境要求
- Node.js: **严格要求 16.20.2**
- npm: >= 8.0.0
- 微信开发者工具: 最新稳定版
## 快速开始
### 1. 安装依赖
```bash
# 进入项目目录
cd c:\nxxmdata\insurance_mini_program
# 检查Node.js版本必须是16.20.2
node -v
# 安装依赖
npm install
```
### 2. 启动后端服务
```bash
# 启动保险端后端服务
cd c:\nxxmdata\insurance_backend
npm run dev
```
确保后端服务运行在 http://localhost:3000
### 3. 配置小程序
1. 修改 `utils/request.js` 中的 BASE_URL确保指向正确的后端地址
2. 在微信开发者工具中配置合法域名(开发时可关闭域名校验)
### 4. 编译运行
```bash
# 开发模式编译到微信小程序
npm run dev:mp-weixin
# 或使用
npm run serve
```
### 5. 在微信开发者工具中预览
1. 打开微信开发者工具
2. 导入项目,选择 `dist/dev/mp-weixin` 目录
3. 配置AppID或使用测试号
4. 预览调试
## 项目结构
```
insurance_mini_program/
├── components/ # 公共组件
│ ├── ProductCard.vue # 产品卡片组件
│ ├── StatusBadge.vue # 状态标签组件
│ ├── LoadingSpinner.vue # 加载组件
│ └── PolicyCard.vue # 保单卡片组件
├── pages/ # 页面
│ ├── index/ # 首页
│ ├── login/ # 登录页
│ ├── products/ # 产品列表
│ ├── application/ # 投保申请
│ └── my/ # 个人中心
├── store/ # Pinia状态管理
│ ├── index.js # Store入口
│ ├── user.js # 用户状态
│ └── insurance.js # 保险业务状态
├── utils/ # 工具类
│ ├── api.js # API接口封装
│ ├── auth.js # 认证工具
│ ├── request.js # 请求封装
│ └── constants.js # 常量定义
├── static/ # 静态资源
├── App.vue # 应用入口组件
├── main.js # 应用入口文件
├── manifest.json # 应用配置
├── pages.json # 页面配置
├── uni.scss # 全局样式
└── package.json # 项目配置
```
## 核心功能模块
### 1. 用户认证模块
- **微信一键登录**: 集成微信小程序授权登录
- **账号密码登录**: 支持传统用户名密码登录
- **登录状态管理**: 基于Pinia的用户状态管理
- **Token管理**: JWT token自动管理和刷新
### 2. 产品浏览模块
- **产品列表**: 动态加载保险产品,支持分类筛选
- **产品搜索**: 实时搜索,防抖优化
- **产品详情**: 展示详细的产品信息和条款
### 3. 投保申请模块
- **在线申请**: 完整的投保表单,支持表单验证
- **实时保费计算**: 根据保额动态计算保费
- **材料上传**: 支持身份证等材料上传
### 4. 保单管理模块
- **保单列表**: 查看个人所有保单
- **保单详情**: 详细的保单信息展示
- **保单状态**: 实时同步保单状态
### 5. 理赔申请模块
- **理赔申请**: 在线提交理赔申请
- **材料上传**: 支持理赔材料上传
- **进度查询**: 实时查看理赔进度
## API接口说明
本项目所有API接口都动态调用 `c:\nxxmdata\insurance_backend` 的接口:
### 认证接口
- `POST /api/auth/wx-login` - 微信登录(需后端扩展)
- `POST /api/auth/login` - 账号密码登录
- `GET /api/auth/profile` - 获取用户信息
### 产品接口
- `GET /api/insurance-types` - 获取产品列表
- `GET /api/insurance-types/:id` - 获取产品详情
### 申请接口
- `POST /api/insurance/applications` - 提交投保申请
- `GET /api/miniprogram/my-applications` - 获取我的申请(需后端扩展)
### 保单接口
- `GET /api/miniprogram/my-policies` - 获取我的保单(需后端扩展)
- `GET /api/policies/:id` - 获取保单详情
### 理赔接口
- `POST /api/claims` - 提交理赔申请
- `GET /api/miniprogram/my-claims` - 获取我的理赔(需后端扩展)
## 后端扩展要求
为了支持小程序的完整功能,需要在 `insurance_backend` 中扩展以下接口:
1. **微信登录接口**: `POST /api/auth/wx-login`
2. **小程序专用路由**: `GET /api/miniprogram/*`
3. **用户关联**: 在相关模型中添加 `user_id` 字段
详细的后端扩展代码已在开发文档中提供。
## 开发规范
### 1. 代码规范
- 使用 Vue 3 Composition API
- 组件使用 PascalCase 命名
- 文件使用 kebab-case 命名
- 遵循 ESLint 规则
### 2. 状态管理
- 使用 Pinia 进行状态管理
- 按业务模块划分 Store
- 异步操作统一在 actions 中处理
### 3. API调用
- 统一使用封装的 request 方法
- 错误处理统一在请求拦截器中
- 支持请求缓存和防抖
### 4. 组件开发
- 组件职责单一,高内聚低耦合
- 合理使用 props 和 emit
- 样式使用 scoped避免污染
## 调试指南
### 1. 网络调试
- 在微信开发者工具中查看网络请求
- 检查后端服务是否正常运行
- 确认API接口返回格式正确
### 2. 状态调试
- 使用 Vue DevTools 调试状态
- 在控制台查看 Pinia store 数据
- 检查本地存储数据
### 3. 常见问题
- **登录失败**: 检查后端微信登录接口是否实现
- **API调用失败**: 确认后端服务运行状态
- **页面跳转失败**: 检查页面路径配置
## 部署上线
### 1. 小程序发布
1. 执行 `npm run build:mp-weixin` 构建生产版本
2. 在微信开发者工具中上传代码
3. 在微信公众平台提交审核
4. 审核通过后发布上线
### 2. 后端配置
- 配置生产环境的数据库
- 设置正确的微信小程序配置
- 配置HTTPS域名和SSL证书
## 注意事项
1. **版本兼容**: 严格使用 Node.js 16.20.2,避免版本冲突
2. **接口调用**: 所有数据都从后端动态获取,禁止硬编码
3. **错误处理**: 完善的错误提示和用户反馈
4. **性能优化**: 合理使用分页加载和图片懒加载
5. **安全考虑**: 敏感信息加密传输Token安全管理
## 技术支持
如有问题,请参考:
1. [uni-app官方文档](https://uniapp.dcloud.net.cn/)
2. [Vue.js官方文档](https://vuejs.org/)
3. [Pinia官方文档](https://pinia.vuejs.org/)
4. [微信小程序官方文档](https://developers.weixin.qq.com/miniprogram/dev/framework/)

View File

@@ -0,0 +1,71 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>保险服务 - H5版本</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif;
margin: 0;
padding: 0;
background-color: #f5f5f5;
color: #333;
}
.container {
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
.header {
text-align: center;
padding: 20px 0;
background-color: #1890ff;
color: white;
margin-bottom: 20px;
}
.content {
background-color: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.message {
text-align: center;
padding: 40px 20px;
}
.button {
display: inline-block;
padding: 10px 20px;
background-color: #1890ff;
color: white;
border-radius: 4px;
text-decoration: none;
margin-top: 20px;
}
</style>
</head>
<body>
<div class="header">
<h1>保险服务</h1>
</div>
<div class="container">
<div class="content">
<div class="message">
<h2>欢迎使用保险服务H5版本</h2>
<p>这是一个临时的入口页面用于测试H5版本的访问。</p>
<p>完整的H5应用正在加载中请稍候...</p>
<a href="/insurance/" class="button">进入应用</a>
</div>
</div>
</div>
<script>
// 自动重定向到实际的应用入口
setTimeout(() => {
window.location.href = '/insurance/';
}, 3000);
</script>
</body>
</html>

View File

@@ -0,0 +1,14 @@
import { createSSRApp } from 'vue'
import store from './store'
import App from './App.vue'
export function createApp() {
const app = createSSRApp(App)
// 使用Pinia状态管理
app.use(store)
return {
app
}
}

View File

@@ -0,0 +1,44 @@
{
"name": "保险服务小程序",
"appid": "your-miniprogram-appid",
"description": "专业的保险服务平台,提供产品浏览、在线投保、理赔申请等服务",
"versionName": "1.0.0",
"versionCode": "100",
"transformPx": false,
"app-plus": {
"usingComponents": true
},
"h5": {
"title": "保险服务",
"router": {
"mode": "hash",
"base": "/insurance/"
}
},
"mp-weixin": {
"appid": "your-miniprogram-appid",
"setting": {
"urlCheck": false,
"es6": true,
"minified": true,
"postcss": true
},
"usingComponents": true,
"permission": {
"scope.userLocation": {
"desc": "获取位置信息用于投保地址定位"
}
},
"requiredBackgroundModes": ["location"],
"plugins": {
"WechatSI": {
"version": "0.3.3",
"provider": "wx069ba97219f66d99"
}
}
},
"mp-alipay": {
"usingComponents": true
},
"quickapp": {}
}

17756
insurance_mini_program/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,45 @@
{
"name": "insurance-miniprogram",
"version": "1.0.0",
"description": "保险端微信小程序 - 基于Vue.js 3.x + uni-app",
"scripts": {
"serve": "npm run dev:mp-weixin",
"build": "npm run build:mp-weixin",
"dev:mp-weixin": "cross-env NODE_ENV=development UNI_PLATFORM=mp-weixin uni",
"build:mp-weixin": "cross-env NODE_ENV=production UNI_PLATFORM=mp-weixin uni build",
"dev:h5": "cross-env NODE_ENV=development UNI_PLATFORM=h5 uni",
"build:h5": "cross-env NODE_ENV=production UNI_PLATFORM=h5 uni build",
"lint": "eslint --ext .js,.vue .",
"lint:fix": "eslint --ext .js,.vue . --fix"
},
"dependencies": {
"@dcloudio/uni-app": "3.0.0-3081220230817001",
"@dcloudio/uni-mp-weixin": "3.0.0-3081220230817001",
"@dcloudio/uni-components": "3.0.0-3081220230817001",
"vue": "^3.3.4",
"pinia": "^2.1.6",
"@vant/weapp": "^1.11.6"
},
"devDependencies": {
"@dcloudio/uni-cli-shared": "3.0.0-3081220230817001",
"@dcloudio/vite-plugin-uni": "3.0.0-3081220230817001",
"@dcloudio/uni-automator": "3.0.0-3081220230817001",
"@vue/runtime-core": "^3.3.4",
"vite": "^4.4.9",
"sass": "^1.64.1",
"cross-env": "^7.0.3"
},
"engines": {
"node": "16.20.2",
"npm": ">=8.0.0"
},
"author": "Insurance Team",
"license": "MIT",
"keywords": [
"insurance",
"miniprogram",
"vue3",
"uni-app",
"weixin"
]
}

View File

@@ -0,0 +1,132 @@
{
"pages": [
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "保险服务",
"enablePullDownRefresh": true,
"backgroundTextStyle": "dark"
}
},
{
"path": "pages/login/login",
"style": {
"navigationBarTitleText": "登录",
"navigationStyle": "custom"
}
},
{
"path": "pages/products/products",
"style": {
"navigationBarTitleText": "保险产品",
"enablePullDownRefresh": true
}
},
{
"path": "pages/product-detail/product-detail",
"style": {
"navigationBarTitleText": "产品详情"
}
},
{
"path": "pages/application/application",
"style": {
"navigationBarTitleText": "投保申请"
}
},
{
"path": "pages/application-result/application-result",
"style": {
"navigationBarTitleText": "申请结果"
}
},
{
"path": "pages/my/my",
"style": {
"navigationBarTitleText": "个人中心",
"enablePullDownRefresh": true
}
},
{
"path": "pages/policies/policies",
"style": {
"navigationBarTitleText": "我的保单",
"enablePullDownRefresh": true
}
},
{
"path": "pages/policy-detail/policy-detail",
"style": {
"navigationBarTitleText": "保单详情"
}
},
{
"path": "pages/claims/claims",
"style": {
"navigationBarTitleText": "理赔申请",
"enablePullDownRefresh": true
}
},
{
"path": "pages/claim-detail/claim-detail",
"style": {
"navigationBarTitleText": "理赔详情"
}
}
],
"globalStyle": {
"navigationBarTextStyle": "white",
"navigationBarTitleText": "保险服务",
"navigationBarBackgroundColor": "#1890ff",
"backgroundColor": "#f5f5f5",
"backgroundTextStyle": "dark",
"app-plus": {
"background": "#f5f5f5"
}
},
"tabBar": {
"color": "#666666",
"selectedColor": "#1890ff",
"backgroundColor": "#ffffff",
"borderStyle": "white",
"height": "50px",
"fontSize": "12px",
"iconWidth": "24px",
"spacing": "3px",
"list": [
{
"pagePath": "pages/index/index",
"text": "首页",
"iconPath": "static/images/tab-home.png",
"selectedIconPath": "static/images/tab-home-active.png"
},
{
"pagePath": "pages/products/products",
"text": "产品",
"iconPath": "static/images/tab-products.png",
"selectedIconPath": "static/images/tab-products-active.png"
},
{
"pagePath": "pages/my/my",
"text": "我的",
"iconPath": "static/images/tab-my.png",
"selectedIconPath": "static/images/tab-my-active.png"
}
]
},
"condition": {
"current": 0,
"list": [
{
"name": "首页",
"path": "pages/index/index",
"query": ""
},
{
"name": "产品详情",
"path": "pages/product-detail/product-detail",
"query": "id=1"
}
]
}
}

View File

@@ -0,0 +1,25 @@
{
"setting": {
"es6": true,
"postcss": true,
"minified": true,
"uglifyFileName": false,
"enhance": true,
"packNpmRelationList": [],
"babelSetting": {
"ignore": [],
"disablePlugins": [],
"outputPath": ""
},
"useCompilerPlugins": false,
"minifyWXML": true
},
"compileType": "miniprogram",
"simulatorPluginLibVersion": {},
"packOptions": {
"ignore": [],
"include": []
},
"appid": "wx363d2520963f1853",
"editorSetting": {}
}

View File

@@ -0,0 +1,14 @@
{
"libVersion": "3.10.0",
"projectname": "insurance_mini_program",
"setting": {
"urlCheck": true,
"coverView": true,
"lazyloadPlaceholderEnable": false,
"skylineRenderEnable": false,
"preloadBackgroundData": false,
"autoAudits": false,
"showShadowRootInWxmlPanel": true,
"compileHotReLoad": true
}
}

View File

@@ -0,0 +1,226 @@
<template>
<view id="app">
<!-- 全局加载组件 -->
<view v-if="appStore.globalLoading" class="global-loading">
<view class="loading-spinner"></view>
<text class="loading-text">加载中...</text>
</view>
<!-- 网络状态提示 -->
<view v-if="!appStore.isOnline" class="network-offline">
<text>网络连接已断开</text>
</view>
</view>
</template>
<script setup>
import { onLaunch, onShow, onHide } from '@dcloudio/uni-app'
import { useAppStore } from '@/stores/app'
import { useUserStore } from '@/stores/user'
// 获取store实例
const appStore = useAppStore()
const userStore = useUserStore()
onLaunch(() => {
console.log('App Launch')
// 初始化应用
initApp()
})
onShow(() => {
console.log('App Show')
// 检查登录状态
userStore.checkAuth()
})
onHide(() => {
console.log('App Hide')
})
/**
* 初始化应用
*/
async function initApp() {
try {
// 初始化应用store
await appStore.initApp()
// 监听网络状态变化
appStore.watchNetworkStatus()
// 检查登录状态
userStore.checkAuth()
console.log('应用初始化完成')
} catch (error) {
console.error('应用初始化失败:', error)
appStore.addError(error)
}
}
</script>
<style lang="scss">
/* 全局样式 */
@import '@/styles/variables.scss';
@import '@/styles/base.scss';
#app {
height: 100vh;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif;
}
/* 全局加载样式 */
.global-loading {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.9);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 9999;
.loading-spinner {
width: 40rpx;
height: 40rpx;
border: 4rpx solid #f3f3f3;
border-top: 4rpx solid $primary-color;
border-radius: 50%;
animation: spin 1s linear infinite;
}
.loading-text {
margin-top: 20rpx;
font-size: 28rpx;
color: $text-secondary;
}
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* 网络离线提示 */
.network-offline {
position: fixed;
top: 0;
left: 0;
right: 0;
background: #ff4d4f;
color: white;
text-align: center;
padding: 20rpx;
font-size: 24rpx;
z-index: 9998;
}
/* 通用样式类 */
.container {
padding: 30rpx;
}
.section {
margin-bottom: 40rpx;
}
.section-title {
font-size: 32rpx;
font-weight: bold;
margin-bottom: 20rpx;
color: $text-primary;
}
.card {
background: white;
border-radius: 12rpx;
padding: 30rpx;
box-shadow: 0 2rpx 12rpx 0 rgba(0, 0, 0, 0.1);
}
.btn {
display: inline-block;
padding: 20rpx 40rpx;
border-radius: 8rpx;
text-align: center;
font-size: 28rpx;
transition: all 0.3s;
&.btn-primary {
background: $primary-color;
color: white;
&:active {
background: darken($primary-color, 10%);
}
}
&.btn-secondary {
background: $background-light;
color: $text-primary;
border: 1rpx solid $border-color;
&:active {
background: darken($background-light, 5%);
}
}
}
.text-primary {
color: $text-primary;
}
.text-secondary {
color: $text-secondary;
}
.text-success {
color: $success-color;
}
.text-warning {
color: $warning-color;
}
.text-error {
color: $error-color;
}
.flex {
display: flex;
}
.flex-center {
display: flex;
align-items: center;
justify-content: center;
}
.flex-between {
display: flex;
align-items: center;
justify-content: space-between;
}
.mt-20 {
margin-top: 20rpx;
}
.mb-20 {
margin-bottom: 20rpx;
}
.p-20 {
padding: 20rpx;
}
.p-30 {
padding: 30rpx;
}
</style>

View File

@@ -0,0 +1,94 @@
<template>
<view class="loading-spinner" v-if="show">
<view class="spinner-container">
<view class="spinner" :class="{ spinning: spinning }"></view>
<text v-if="text" class="loading-text">{{ text }}</text>
</view>
</view>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
// 定义属性
const props = defineProps({
show: {
type: Boolean,
default: true
},
text: {
type: String,
default: '加载中...'
},
size: {
type: String,
default: 'normal',
validator: (value) => ['small', 'normal', 'large'].includes(value)
}
})
// 响应式数据
const spinning = ref(false)
// 启动动画
onMounted(() => {
spinning.value = true
})
// 清理动画
onUnmounted(() => {
spinning.value = false
})
</script>
<style scoped>
.loading-spinner {
display: flex;
justify-content: center;
align-items: center;
padding: 60rpx;
}
.spinner-container {
display: flex;
flex-direction: column;
align-items: center;
}
.spinner {
width: 60rpx;
height: 60rpx;
border: 4rpx solid #f0f0f0;
border-top: 4rpx solid #1890ff;
border-radius: 50%;
}
.spinner.spinning {
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loading-text {
margin-top: 20rpx;
font-size: 28rpx;
color: #999;
text-align: center;
}
/* 尺寸变体 */
.spinner.size-small {
width: 40rpx;
height: 40rpx;
border-width: 3rpx;
}
.spinner.size-large {
width: 80rpx;
height: 80rpx;
border-width: 5rpx;
}
</style>

View File

@@ -0,0 +1,155 @@
<template>
<view class="policy-card" @tap="handleCardClick">
<view class="policy-header">
<view class="policy-info">
<text class="policy-name">{{ policy.application?.insurance_type?.name }}</text>
<StatusBadge
:status="policy.status"
type="policy"
size="small"
/>
</view>
<text class="policy-number">{{ policy.policy_number }}</text>
</view>
<view class="policy-content">
<view class="info-row">
<text class="info-label">保险金额</text>
<text class="info-value">{{ formatAmount(policy.coverage_amount) }}</text>
</view>
<view class="info-row">
<text class="info-label">年保费</text>
<text class="info-value primary">¥{{ policy.premium_amount }}</text>
</view>
<view class="info-row">
<text class="info-label">保险期间</text>
<text class="info-value">{{ formatDateRange(policy.start_date, policy.end_date) }}</text>
</view>
</view>
<view class="policy-footer">
<text class="create-date">投保时间{{ formatDate(policy.created_at) }}</text>
</view>
</view>
</template>
<script setup>
import StatusBadge from './StatusBadge.vue'
// 定义属性
const props = defineProps({
policy: {
type: Object,
required: true,
default: () => ({})
}
})
// 定义事件
const emit = defineEmits(['click'])
// 格式化金额
const formatAmount = (amount) => {
if (!amount) return '0万'
if (amount >= 10000) {
return (amount / 10000).toFixed(0) + '万'
}
return amount.toString()
}
// 格式化日期
const formatDate = (dateString) => {
if (!dateString) return ''
const date = new Date(dateString)
return date.toLocaleDateString('zh-CN')
}
// 格式化日期范围
const formatDateRange = (startDate, endDate) => {
if (!startDate || !endDate) return ''
const start = new Date(startDate).toLocaleDateString('zh-CN')
const end = new Date(endDate).toLocaleDateString('zh-CN')
return `${start} - ${end}`
}
// 处理卡片点击
const handleCardClick = () => {
emit('click', props.policy)
}
</script>
<style scoped>
.policy-card {
background: #fff;
border-radius: 12rpx;
padding: 30rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
}
.policy-header {
margin-bottom: 20rpx;
}
.policy-info {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10rpx;
}
.policy-name {
font-size: 32rpx;
font-weight: bold;
color: #333;
flex: 1;
}
.policy-number {
font-size: 24rpx;
color: #999;
}
.policy-content {
border-top: 1px solid #f0f0f0;
border-bottom: 1px solid #f0f0f0;
padding: 20rpx 0;
margin-bottom: 20rpx;
}
.info-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15rpx;
}
.info-row:last-child {
margin-bottom: 0;
}
.info-label {
font-size: 28rpx;
color: #666;
}
.info-value {
font-size: 28rpx;
color: #333;
font-weight: 500;
}
.info-value.primary {
color: #1890ff;
font-weight: bold;
}
.policy-footer {
text-align: right;
}
.create-date {
font-size: 24rpx;
color: #999;
}
</style>

View File

@@ -0,0 +1,193 @@
<template>
<view class="product-card" @tap="handleCardClick">
<image
:src="product.icon || '/static/images/default-product.png'"
class="product-image"
mode="aspectFill"
/>
<view class="product-content">
<view class="product-header">
<text class="product-name">{{ product.name }}</text>
<text v-if="product.is_hot" class="hot-tag">热门</text>
</view>
<text class="product-desc">{{ product.description }}</text>
<view class="product-features" v-if="features.length > 0">
<text
v-for="feature in features"
:key="feature"
class="feature-tag"
>
{{ feature }}
</text>
</view>
<view class="product-footer">
<view class="price-info">
<text class="price-label">起保费</text>
<text class="price-value">¥{{ product.min_premium }}</text>
<text class="price-unit">/</text>
</view>
<view class="coverage-info">
<text class="coverage-label">最高保额</text>
<text class="coverage-value">{{ formatAmount(product.max_amount) }}</text>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { computed } from 'vue'
// 定义属性
const props = defineProps({
product: {
type: Object,
required: true,
default: () => ({})
},
showFeatures: {
type: Boolean,
default: true
}
})
// 定义事件
const emit = defineEmits(['click'])
// 计算属性
const features = computed(() => {
if (!props.showFeatures) return []
const featureList = []
if (props.product.is_hot) featureList.push('热门推荐')
if (props.product.min_premium <= 100) featureList.push('低保费')
if (props.product.max_amount >= 1000000) featureList.push('高保额')
return featureList.slice(0, 2)
})
// 格式化金额
const formatAmount = (amount) => {
if (!amount) return '0'
if (amount >= 10000) {
return (amount / 10000).toFixed(0) + '万'
}
return amount.toString()
}
// 处理卡片点击
const handleCardClick = () => {
emit('click', props.product)
}
</script>
<style scoped>
.product-card {
display: flex;
background: #fff;
border-radius: 12rpx;
padding: 30rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
}
.product-image {
width: 120rpx;
height: 120rpx;
border-radius: 8rpx;
margin-right: 30rpx;
flex-shrink: 0;
}
.product-content {
flex: 1;
}
.product-header {
display: flex;
align-items: center;
margin-bottom: 10rpx;
}
.product-name {
font-size: 32rpx;
font-weight: bold;
color: #333;
flex: 1;
}
.hot-tag {
background: #ff4d4f;
color: #fff;
font-size: 20rpx;
padding: 4rpx 12rpx;
border-radius: 20rpx;
margin-left: 20rpx;
}
.product-desc {
font-size: 26rpx;
color: #666;
line-height: 1.4;
margin-bottom: 15rpx;
}
.product-features {
display: flex;
gap: 10rpx;
margin-bottom: 20rpx;
}
.feature-tag {
background: #f0f9ff;
color: #1890ff;
font-size: 22rpx;
padding: 6rpx 12rpx;
border-radius: 20rpx;
}
.product-footer {
display: flex;
justify-content: space-between;
align-items: center;
}
.price-info {
display: flex;
align-items: baseline;
}
.price-label {
font-size: 24rpx;
color: #999;
margin-right: 10rpx;
}
.price-value {
font-size: 36rpx;
font-weight: bold;
color: #ff6b35;
}
.price-unit {
font-size: 24rpx;
color: #999;
margin-left: 4rpx;
}
.coverage-info {
text-align: right;
}
.coverage-label {
font-size: 24rpx;
color: #999;
display: block;
}
.coverage-value {
font-size: 28rpx;
color: #333;
font-weight: bold;
}
</style>

View File

@@ -0,0 +1,136 @@
<template>
<text
class="status-badge"
:class="[`status-${type}`, size && `size-${size}`]"
>
{{ text }}
</text>
</template>
<script setup>
import { computed } from 'vue'
import {
APPLICATION_STATUS_TEXT,
POLICY_STATUS_TEXT,
CLAIM_STATUS_TEXT
} from '@/utils/constants'
// 定义属性
const props = defineProps({
status: {
type: String,
required: true
},
type: {
type: String,
default: 'default',
validator: (value) => ['default', 'application', 'policy', 'claim'].includes(value)
},
size: {
type: String,
default: 'normal',
validator: (value) => ['small', 'normal', 'large'].includes(value)
}
})
// 计算显示文本
const text = computed(() => {
switch (props.type) {
case 'application':
return APPLICATION_STATUS_TEXT[props.status] || props.status
case 'policy':
return POLICY_STATUS_TEXT[props.status] || props.status
case 'claim':
return CLAIM_STATUS_TEXT[props.status] || props.status
default:
return props.status
}
})
</script>
<style scoped>
.status-badge {
display: inline-block;
padding: 6rpx 16rpx;
border-radius: 20rpx;
font-size: 22rpx;
font-weight: 500;
text-align: center;
white-space: nowrap;
}
/* 尺寸变体 */
.size-small {
padding: 4rpx 12rpx;
font-size: 20rpx;
}
.size-normal {
padding: 6rpx 16rpx;
font-size: 22rpx;
}
.size-large {
padding: 8rpx 20rpx;
font-size: 24rpx;
}
/* 默认状态样式 */
.status-default {
background: #f5f5f5;
color: #666;
}
/* 申请状态样式 */
.status-pending {
background: #fff7e6;
color: #faad14;
}
.status-under_review {
background: #e6f7ff;
color: #1890ff;
}
.status-approved {
background: #f6ffed;
color: #52c41a;
}
.status-rejected {
background: #fff2f0;
color: #f5222d;
}
/* 保单状态样式 */
.status-active {
background: #f6ffed;
color: #52c41a;
}
.status-expired {
background: #f5f5f5;
color: #999;
}
.status-cancelled {
background: #fff2f0;
color: #f5222d;
}
.status-suspended {
background: #fff7e6;
color: #faad14;
}
/* 理赔状态样式 */
.status-reviewing {
background: #e6f7ff;
color: #1890ff;
}
.status-paid {
background: #f6ffed;
color: #52c41a;
}
</style>

View File

@@ -0,0 +1,14 @@
import { createSSRApp } from 'vue'
import store from './store'
import App from './App.vue'
export function createApp() {
const app = createSSRApp(App)
// 使用Pinia状态管理
app.use(store)
return {
app
}
}

View File

@@ -0,0 +1,44 @@
{
"name": "保险服务小程序",
"appid": "your-miniprogram-appid",
"description": "专业的保险服务平台,提供产品浏览、在线投保、理赔申请等服务",
"versionName": "1.0.0",
"versionCode": "100",
"transformPx": false,
"app-plus": {
"usingComponents": true
},
"h5": {
"title": "保险服务",
"router": {
"mode": "hash",
"base": "/insurance/"
}
},
"mp-weixin": {
"appid": "your-miniprogram-appid",
"setting": {
"urlCheck": false,
"es6": true,
"minified": true,
"postcss": true
},
"usingComponents": true,
"permission": {
"scope.userLocation": {
"desc": "获取位置信息用于投保地址定位"
}
},
"requiredBackgroundModes": ["location"],
"plugins": {
"WechatSI": {
"version": "0.3.3",
"provider": "wx069ba97219f66d99"
}
}
},
"mp-alipay": {
"usingComponents": true
},
"quickapp": {}
}

View File

@@ -0,0 +1,132 @@
{
"pages": [
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "保险服务",
"enablePullDownRefresh": true,
"backgroundTextStyle": "dark"
}
},
{
"path": "pages/login/login",
"style": {
"navigationBarTitleText": "登录",
"navigationStyle": "custom"
}
},
{
"path": "pages/products/products",
"style": {
"navigationBarTitleText": "保险产品",
"enablePullDownRefresh": true
}
},
{
"path": "pages/product-detail/product-detail",
"style": {
"navigationBarTitleText": "产品详情"
}
},
{
"path": "pages/application/application",
"style": {
"navigationBarTitleText": "投保申请"
}
},
{
"path": "pages/application-result/application-result",
"style": {
"navigationBarTitleText": "申请结果"
}
},
{
"path": "pages/my/my",
"style": {
"navigationBarTitleText": "个人中心",
"enablePullDownRefresh": true
}
},
{
"path": "pages/policies/policies",
"style": {
"navigationBarTitleText": "我的保单",
"enablePullDownRefresh": true
}
},
{
"path": "pages/policy-detail/policy-detail",
"style": {
"navigationBarTitleText": "保单详情"
}
},
{
"path": "pages/claims/claims",
"style": {
"navigationBarTitleText": "理赔申请",
"enablePullDownRefresh": true
}
},
{
"path": "pages/claim-detail/claim-detail",
"style": {
"navigationBarTitleText": "理赔详情"
}
}
],
"globalStyle": {
"navigationBarTextStyle": "white",
"navigationBarTitleText": "保险服务",
"navigationBarBackgroundColor": "#1890ff",
"backgroundColor": "#f5f5f5",
"backgroundTextStyle": "dark",
"app-plus": {
"background": "#f5f5f5"
}
},
"tabBar": {
"color": "#666666",
"selectedColor": "#1890ff",
"backgroundColor": "#ffffff",
"borderStyle": "white",
"height": "50px",
"fontSize": "12px",
"iconWidth": "24px",
"spacing": "3px",
"list": [
{
"pagePath": "pages/index/index",
"text": "首页",
"iconPath": "static/images/tab-home.png",
"selectedIconPath": "static/images/tab-home-active.png"
},
{
"pagePath": "pages/products/products",
"text": "产品",
"iconPath": "static/images/tab-products.png",
"selectedIconPath": "static/images/tab-products-active.png"
},
{
"pagePath": "pages/my/my",
"text": "我的",
"iconPath": "static/images/tab-my.png",
"selectedIconPath": "static/images/tab-my-active.png"
}
]
},
"condition": {
"current": 0,
"list": [
{
"name": "首页",
"path": "pages/index/index",
"query": ""
},
{
"name": "产品详情",
"path": "pages/product-detail/product-detail",
"query": "id=1"
}
]
}
}

View File

@@ -0,0 +1,663 @@
<template>
<view class="container">
<!-- 产品信息卡片 -->
<view v-if="product" class="product-card">
<view class="product-header">
<image :src="product.icon || '/static/images/default-product.png'" class="product-icon" />
<view class="product-info">
<text class="product-name">{{ product.name }}</text>
<text class="product-desc">{{ product.description }}</text>
<view class="product-price">
<text class="price-range">保费¥{{ product.min_premium }} - ¥{{ product.max_premium }}/</text>
</view>
</view>
</view>
</view>
<!-- 投保表单 -->
<view class="form-section">
<view class="section-title">
<text>投保信息</text>
<text class="required-tip">* 为必填项</text>
</view>
<!-- 投保人信息 -->
<view class="form-group">
<text class="form-label">投保人姓名 *</text>
<input
v-model="formData.customer_name"
class="form-input"
placeholder="请输入真实姓名"
@input="onInputChange('customer_name', $event)"
/>
</view>
<view class="form-group">
<text class="form-label">身份证号 *</text>
<input
v-model="formData.customer_id_card"
class="form-input"
placeholder="请输入18位身份证号码"
maxlength="18"
@input="onInputChange('customer_id_card', $event)"
/>
</view>
<view class="form-group">
<text class="form-label">手机号码 *</text>
<input
v-model="formData.customer_phone"
class="form-input"
placeholder="请输入11位手机号码"
type="number"
maxlength="11"
@input="onInputChange('customer_phone', $event)"
/>
</view>
<view class="form-group">
<text class="form-label">联系地址 *</text>
<textarea
v-model="formData.customer_address"
class="form-textarea"
placeholder="请输入详细联系地址"
maxlength="200"
@input="onInputChange('customer_address', $event)"
/>
</view>
<!-- 保险信息 -->
<view class="form-group">
<text class="form-label">保险金额 *</text>
<view class="amount-input-wrapper">
<input
v-model="formData.application_amount"
class="form-input amount-input"
placeholder="请输入保险金额"
type="digit"
@input="onInputChange('application_amount', $event)"
/>
<text class="amount-unit">万元</text>
</view>
<view class="amount-tips">
<text>建议保额{{ product?.min_amount }} - {{ product?.max_amount }}</text>
</view>
</view>
<!-- 受益人信息 -->
<view class="beneficiary-section">
<text class="section-subtitle">受益人信息可选</text>
<view class="form-group">
<text class="form-label">受益人姓名</text>
<input
v-model="formData.beneficiary_name"
class="form-input"
placeholder="请输入受益人姓名"
@input="onInputChange('beneficiary_name', $event)"
/>
</view>
<view class="form-group">
<text class="form-label">与投保人关系</text>
<picker
:value="relationIndex"
:range="relationOptions"
@change="onRelationChange"
>
<view class="picker-input">
{{ formData.beneficiary_relation || '请选择与受益人关系' }}
<text class="picker-arrow">></text>
</view>
</picker>
</view>
</view>
<!-- 附加信息 -->
<view class="form-group">
<text class="form-label">备注信息</text>
<textarea
v-model="formData.remarks"
class="form-textarea"
placeholder="请输入其他需要说明的信息(可选)"
maxlength="500"
@input="onInputChange('remarks', $event)"
/>
</view>
</view>
<!-- 保费预览 -->
<view class="premium-preview" v-if="estimatedPremium">
<view class="preview-header">
<text>保费预览</text>
</view>
<view class="preview-content">
<view class="preview-item">
<text class="preview-label">保险金额</text>
<text class="preview-value">{{ formData.application_amount }}万元</text>
</view>
<view class="preview-item">
<text class="preview-label">预估年保费</text>
<text class="preview-value primary">¥{{ estimatedPremium }}</text>
</view>
</view>
</view>
<!-- 条款确认 -->
<view class="agreement-section">
<view class="agreement-item" @tap="toggleAgreement">
<image
:src="agreedToTerms ? '/static/images/checkbox-checked.png' : '/static/images/checkbox-unchecked.png'"
class="checkbox"
/>
<text class="agreement-text">
我已阅读并同意
<text class="agreement-link" @tap.stop="showTerms">保险条款</text>
<text class="agreement-link" @tap.stop="showPrivacy">隐私政策</text>
</text>
</view>
</view>
<!-- 提交按钮 -->
<view class="submit-section">
<button
class="submit-btn"
:class="{ disabled: !canSubmit }"
:disabled="!canSubmit || submitting"
@tap="submitApplication"
>
{{ submitting ? '提交中...' : '提交申请' }}
</button>
</view>
</view>
</template>
<script setup>
import { ref, reactive, computed, onMounted } from 'vue'
import { useInsuranceStore } from '@/store/insurance'
import { useUserStore } from '@/store/user'
import { auth } from '@/utils/auth'
import { BENEFICIARY_RELATIONS } from '@/utils/constants'
// 状态管理
const insuranceStore = useInsuranceStore()
const userStore = useUserStore()
// 获取路由参数
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
const productId = ref(currentPage.options.productId)
// 响应式数据
const product = ref(null)
const submitting = ref(false)
const agreedToTerms = ref(false)
const relationIndex = ref(-1)
// 关系选项
const relationOptions = ref(BENEFICIARY_RELATIONS)
// 表单数据使用reactive实现响应式
const formData = reactive({
customer_name: '',
customer_id_card: '',
customer_phone: '',
customer_address: '',
application_amount: '',
beneficiary_name: '',
beneficiary_relation: '',
remarks: ''
})
// 计算属性
const canSubmit = computed(() => {
return formData.customer_name &&
formData.customer_id_card &&
formData.customer_phone &&
formData.customer_address &&
formData.application_amount &&
agreedToTerms.value &&
!submitting.value
})
// 预估保费计算
const estimatedPremium = computed(() => {
if (!formData.application_amount || !product.value) return 0
const amount = parseFloat(formData.application_amount)
const rate = product.value.premium_rate || 0.005 // 默认费率0.5%
return Math.round(amount * 10000 * rate)
})
// 加载产品信息(动态调用保险端后端)
const loadProductInfo = async () => {
try {
if (!productId.value) {
uni.showToast({
title: '产品信息缺失',
icon: 'none'
})
return
}
// 动态调用 /api/insurance-types/:id
const productData = await insuranceStore.fetchProductDetail(productId.value)
product.value = productData
console.log('产品信息加载成功:', productData)
} catch (error) {
console.error('加载产品信息失败:', error)
uni.showToast({
title: '加载产品信息失败',
icon: 'none'
})
}
}
// 表单输入处理手动更新避免v-model问题
const onInputChange = (field, event) => {
const value = event.detail ? event.detail.value : event.target.value
formData[field] = value
// 特殊处理身份证号格式
if (field === 'customer_id_card') {
formData[field] = value.toUpperCase()
}
}
// 关系选择处理
const onRelationChange = (event) => {
const index = event.detail.value
relationIndex.value = index
formData.beneficiary_relation = relationOptions.value[index]
}
// 切换条款同意状态
const toggleAgreement = () => {
agreedToTerms.value = !agreedToTerms.value
}
// 显示保险条款
const showTerms = () => {
uni.navigateTo({
url: '/pages/terms/terms?type=insurance'
})
}
// 显示隐私政策
const showPrivacy = () => {
uni.navigateTo({
url: '/pages/terms/terms?type=privacy'
})
}
// 表单验证
const validateForm = () => {
if (!formData.customer_name.trim()) {
uni.showToast({ title: '请输入姓名', icon: 'none' })
return false
}
// 身份证号验证
const idCardRegex = /^[1-9]\d{5}(19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[0-9Xx]$/
if (!idCardRegex.test(formData.customer_id_card)) {
uni.showToast({ title: '请输入正确的身份证号', icon: 'none' })
return false
}
// 手机号验证
const phoneRegex = /^1[3-9]\d{9}$/
if (!phoneRegex.test(formData.customer_phone)) {
uni.showToast({ title: '请输入正确的手机号', icon: 'none' })
return false
}
if (!formData.customer_address.trim()) {
uni.showToast({ title: '请输入联系地址', icon: 'none' })
return false
}
const amount = parseFloat(formData.application_amount)
if (!amount || amount <= 0) {
uni.showToast({ title: '请输入正确的保险金额', icon: 'none' })
return false
}
// 检查保额范围
if (product.value) {
if (amount < product.value.min_amount || amount > product.value.max_amount) {
uni.showToast({
title: `保险金额应在${product.value.min_amount}-${product.value.max_amount}万元之间`,
icon: 'none'
})
return false
}
}
if (!agreedToTerms.value) {
uni.showToast({ title: '请先同意保险条款', icon: 'none' })
return false
}
return true
}
// 提交申请(动态调用保险端后端)
const submitApplication = async () => {
// 检查登录状态
if (!auth.requireAuth()) {
return
}
if (!validateForm()) return
if (submitting.value) return
submitting.value = true
try {
const applicationData = {
...formData,
insurance_type_id: productId.value,
application_amount: parseFloat(formData.application_amount) * 10000 // 转换为元
}
console.log('提交申请数据:', applicationData)
// 动态调用保险端 /api/insurance/applications 接口
const result = await insuranceStore.submitApplication(applicationData)
uni.showToast({
title: '申请提交成功',
icon: 'success'
})
// 跳转到申请结果页面
setTimeout(() => {
uni.redirectTo({
url: `/pages/application-result/application-result?id=${result.id}`
})
}, 1500)
} catch (error) {
console.error('提交申请失败:', error)
let errorMessage = '提交失败,请重试'
if (error.message) {
errorMessage = error.message
}
uni.showToast({
title: errorMessage,
icon: 'none'
})
} finally {
submitting.value = false
}
}
// 生命周期
onMounted(() => {
loadProductInfo()
})
</script>
<style scoped>
.container {
background-color: #f5f5f5;
min-height: 100vh;
padding-bottom: 120rpx;
}
.product-card {
background: #fff;
margin: 20rpx;
border-radius: 12rpx;
padding: 30rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
}
.product-header {
display: flex;
}
.product-icon {
width: 100rpx;
height: 100rpx;
border-radius: 8rpx;
margin-right: 30rpx;
}
.product-info {
flex: 1;
}
.product-name {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 10rpx;
}
.product-desc {
font-size: 26rpx;
color: #666;
line-height: 1.4;
margin-bottom: 15rpx;
}
.product-price {
font-size: 28rpx;
color: #1890ff;
font-weight: 500;
}
.form-section {
background: #fff;
margin: 20rpx;
border-radius: 12rpx;
padding: 30rpx;
}
.section-title {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 40rpx;
padding-bottom: 20rpx;
border-bottom: 1px solid #f0f0f0;
font-size: 36rpx;
font-weight: bold;
color: #333;
}
.required-tip {
font-size: 24rpx;
color: #999;
font-weight: normal;
}
.section-subtitle {
font-size: 30rpx;
font-weight: bold;
color: #333;
margin-bottom: 30rpx;
padding-top: 20rpx;
}
.form-group {
margin-bottom: 40rpx;
}
.form-label {
display: block;
font-size: 30rpx;
color: #333;
margin-bottom: 20rpx;
font-weight: 500;
}
.form-input {
width: 100%;
height: 88rpx;
padding: 0 30rpx;
border: 1px solid #d9d9d9;
border-radius: 8rpx;
font-size: 30rpx;
background: #fff;
box-sizing: border-box;
}
.form-input:focus {
border-color: #1890ff;
}
.form-textarea {
width: 100%;
min-height: 120rpx;
padding: 20rpx 30rpx;
border: 1px solid #d9d9d9;
border-radius: 8rpx;
font-size: 30rpx;
background: #fff;
box-sizing: border-box;
resize: none;
}
.amount-input-wrapper {
display: flex;
align-items: center;
}
.amount-input {
flex: 1;
margin-right: 20rpx;
}
.amount-unit {
font-size: 30rpx;
color: #666;
}
.amount-tips {
margin-top: 15rpx;
font-size: 24rpx;
color: #999;
}
.beneficiary-section {
margin-top: 40rpx;
padding-top: 40rpx;
border-top: 1px solid #f0f0f0;
}
.picker-input {
display: flex;
justify-content: space-between;
align-items: center;
height: 88rpx;
padding: 0 30rpx;
border: 1px solid #d9d9d9;
border-radius: 8rpx;
font-size: 30rpx;
background: #fff;
color: #333;
}
.picker-arrow {
color: #999;
font-size: 24rpx;
}
.premium-preview {
background: #fff;
margin: 20rpx;
border-radius: 12rpx;
padding: 30rpx;
}
.preview-header {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 30rpx;
}
.preview-item {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
}
.preview-label {
font-size: 28rpx;
color: #666;
}
.preview-value {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.preview-value.primary {
color: #1890ff;
font-size: 36rpx;
}
.agreement-section {
padding: 30rpx;
}
.agreement-item {
display: flex;
align-items: flex-start;
}
.checkbox {
width: 32rpx;
height: 32rpx;
margin-right: 20rpx;
margin-top: 4rpx;
}
.agreement-text {
flex: 1;
font-size: 26rpx;
color: #666;
line-height: 1.5;
}
.agreement-link {
color: #1890ff;
text-decoration: underline;
}
.submit-section {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: #fff;
padding: 20rpx 30rpx;
border-top: 1px solid #f0f0f0;
}
.submit-btn {
width: 100%;
height: 88rpx;
background: #1890ff;
color: #fff;
border: none;
border-radius: 44rpx;
font-size: 32rpx;
font-weight: bold;
}
.submit-btn.disabled {
background: #d9d9d9;
color: #999;
}
</style>

View File

@@ -0,0 +1,372 @@
<template>
<view class="container">
<!-- 轮播图 -->
<swiper class="banner-swiper" indicator-dots="true" autoplay="true" circular="true">
<swiper-item v-for="banner in banners" :key="banner.id">
<image :src="banner.image" class="banner-image" mode="aspectFill" />
</swiper-item>
</swiper>
<!-- 快捷入口 -->
<view class="quick-actions">
<view class="action-item" @tap="goToProducts">
<image src="/static/images/icon-products.png" class="action-icon" />
<text class="action-text">保险产品</text>
</view>
<view class="action-item" @tap="goToMy">
<image src="/static/images/icon-policy.png" class="action-icon" />
<text class="action-text">我的保单</text>
</view>
<view class="action-item" @tap="goToClaims">
<image src="/static/images/icon-claim.png" class="action-icon" />
<text class="action-text">理赔申请</text>
</view>
<view class="action-item" @tap="goToService">
<image src="/static/images/icon-service.png" class="action-icon" />
<text class="action-text">客服服务</text>
</view>
</view>
<!-- 热门产品 -->
<view class="section">
<view class="section-header">
<text class="section-title">热门产品</text>
<text class="section-more" @tap="goToProducts">更多 ></text>
</view>
<view class="product-grid" v-if="!loading">
<view
v-for="item in hotProducts"
:key="item.id"
class="product-card"
@tap="goToProductDetail(item.id)"
>
<image :src="item.icon || '/static/images/default-product.png'" class="product-icon" />
<view class="product-info">
<text class="product-name">{{ item.name }}</text>
<text class="product-desc">{{ item.description }}</text>
<view class="product-price">
<text class="price-label">起保费</text>
<text class="price-value">¥{{ item.min_premium }}</text>
</view>
</view>
</view>
</view>
<!-- 加载状态 -->
<view v-if="loading" class="loading">
<text>加载中...</text>
</view>
<!-- 空状态 -->
<view v-if="!loading && hotProducts.length === 0" class="empty-state">
<image src="/static/images/empty-products.png" class="empty-icon" />
<text>暂无热门产品</text>
</view>
</view>
<!-- 服务优势 -->
<view class="section">
<view class="section-header">
<text class="section-title">服务优势</text>
</view>
<view class="advantage-grid">
<view class="advantage-item">
<image src="/static/images/advantage-1.png" class="advantage-icon" />
<text class="advantage-title">专业保障</text>
<text class="advantage-desc">专业团队提供全方位保险咨询</text>
</view>
<view class="advantage-item">
<image src="/static/images/advantage-2.png" class="advantage-icon" />
<text class="advantage-title">快速理赔</text>
<text class="advantage-desc">7x24小时快速理赔服务</text>
</view>
<view class="advantage-item">
<image src="/static/images/advantage-3.png" class="advantage-icon" />
<text class="advantage-title">安全可靠</text>
<text class="advantage-desc">银行级安全保障体系</text>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useInsuranceStore } from '@/store/insurance'
// 状态管理
const insuranceStore = useInsuranceStore()
// 响应式数据
const loading = ref(true)
const banners = ref([
{
id: 1,
image: '/static/images/banner-1.jpg',
title: '专业保险服务'
},
{
id: 2,
image: '/static/images/banner-2.jpg',
title: '安心保障'
},
{
id: 3,
image: '/static/images/banner-3.jpg',
title: '贴心理赔'
}
])
// 计算属性
const hotProducts = computed(() => insuranceStore.getHotProducts)
// 加载页面数据(动态调用保险端后端)
const loadPageData = async () => {
try {
loading.value = true
// 动态调用保险端 /api/insurance-types 接口获取热门产品
await insuranceStore.fetchHotProducts()
console.log('热门产品加载成功:', hotProducts.value)
} catch (error) {
console.error('加载首页数据失败:', error)
uni.showToast({
title: '加载失败',
icon: 'none'
})
} finally {
loading.value = false
}
}
// 页面跳转方法
const goToProductDetail = (id) => {
uni.navigateTo({
url: `/pages/product-detail/product-detail?id=${id}`
})
}
const goToProducts = () => {
uni.switchTab({
url: '/pages/products/products'
})
}
const goToMy = () => {
uni.switchTab({
url: '/pages/my/my'
})
}
const goToClaims = () => {
uni.navigateTo({
url: '/pages/claims/claims'
})
}
const goToService = () => {
uni.showModal({
title: '客服服务',
content: '客服电话400-888-8888\n服务时间周一至周日 9:00-18:00',
showCancel: false
})
}
// 下拉刷新
const onPullDownRefresh = async () => {
try {
await loadPageData()
} finally {
uni.stopPullDownRefresh()
}
}
// 生命周期
onMounted(() => {
loadPageData()
})
// 页面显示时刷新数据
onShow(() => {
// 如果数据为空,重新加载
if (hotProducts.value.length === 0) {
loadPageData()
}
})
</script>
<style scoped>
.container {
background-color: #f5f5f5;
}
.banner-swiper {
height: 400rpx;
width: 100%;
}
.banner-image {
width: 100%;
height: 100%;
}
.quick-actions {
display: flex;
justify-content: space-around;
padding: 40rpx 20rpx;
background: #fff;
margin-bottom: 20rpx;
}
.action-item {
display: flex;
flex-direction: column;
align-items: center;
}
.action-icon {
width: 80rpx;
height: 80rpx;
margin-bottom: 20rpx;
}
.action-text {
font-size: 24rpx;
color: #666;
}
.section {
margin-bottom: 20rpx;
background: #fff;
padding: 30rpx;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30rpx;
}
.section-title {
font-size: 36rpx;
font-weight: bold;
color: #333;
}
.section-more {
color: #1890ff;
font-size: 28rpx;
}
.product-grid {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.product-card {
display: flex;
padding: 30rpx;
border: 1px solid #eee;
border-radius: 12rpx;
background: #fafafa;
}
.product-icon {
width: 120rpx;
height: 120rpx;
margin-right: 30rpx;
border-radius: 8rpx;
}
.product-info {
flex: 1;
}
.product-name {
font-size: 32rpx;
font-weight: bold;
margin-bottom: 10rpx;
color: #333;
}
.product-desc {
font-size: 28rpx;
color: #666;
margin-bottom: 15rpx;
}
.product-price {
display: flex;
align-items: center;
}
.price-label {
font-size: 24rpx;
color: #999;
}
.price-value {
font-size: 32rpx;
color: #ff6b35;
font-weight: bold;
}
.advantage-grid {
display: flex;
justify-content: space-between;
}
.advantage-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
padding: 0 20rpx;
}
.advantage-icon {
width: 100rpx;
height: 100rpx;
margin-bottom: 20rpx;
}
.advantage-title {
font-size: 28rpx;
font-weight: bold;
margin-bottom: 10rpx;
color: #333;
}
.advantage-desc {
font-size: 24rpx;
color: #666;
line-height: 1.4;
}
.loading {
text-align: center;
padding: 60rpx;
color: #999;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
padding: 80rpx 20rpx;
color: #999;
}
.empty-icon {
width: 120rpx;
height: 120rpx;
margin-bottom: 30rpx;
opacity: 0.5;
}
</style>

View File

@@ -0,0 +1,300 @@
<template>
<view class="login-page">
<!-- 自定义导航栏 -->
<view class="custom-navbar" :style="{ paddingTop: statusBarHeight + 'px' }">
<view class="navbar-content">
<text class="navbar-title">登录</text>
</view>
</view>
<!-- 登录内容 -->
<view class="login-content">
<!-- Logo区域 -->
<view class="logo-section">
<image src="/static/images/logo.png" class="logo" />
<text class="app-name">保险服务</text>
<text class="app-desc">专业的保险服务平台</text>
</view>
<!-- 微信登录按钮 -->
<view class="login-section">
<button
class="wx-login-btn"
:disabled="isLogging"
@tap="handleWxLogin"
>
<image src="/static/images/wechat-icon.png" class="wx-icon" />
<text class="btn-text">{{ isLogging ? '登录中...' : '微信登录' }}</text>
</button>
<!-- 登录说明 -->
<view class="login-tips">
<text class="tip-text">登录即表示同意</text>
<text class="link-text" @tap="showPrivacyPolicy">隐私政策</text>
<text class="tip-text"></text>
<text class="link-text" @tap="showUserAgreement">用户协议</text>
</view>
</view>
</view>
<!-- 底部信息 -->
<view class="footer">
<text class="footer-text">安全可靠 · 专业服务</text>
</view>
</view>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useUserStore } from '@/stores/user'
import { useAppStore } from '@/stores/app'
// 状态管理
const userStore = useUserStore()
const appStore = useAppStore()
// 响应式数据
const isLogging = ref(false)
const statusBarHeight = ref(0)
// 生命周期
onMounted(() => {
// 获取状态栏高度
const systemInfo = uni.getSystemInfoSync()
statusBarHeight.value = systemInfo.statusBarHeight || 0
// 检查是否已登录
if (userStore.isAuthenticated) {
redirectToHome()
}
})
/**
* 微信登录处理
* 动态调用保险端后端微信认证接口
*/
async function handleWxLogin() {
if (isLogging.value) return
isLogging.value = true
try {
console.log('开始微信登录...')
// 显示登录提示
uni.showLoading({
title: '登录中...',
mask: true
})
// 调用store中的微信登录方法动态调用保险端后端
const result = await userStore.wxLogin()
console.log('微信登录成功:', result)
// 登录成功提示
uni.showToast({
title: '登录成功',
icon: 'success',
duration: 1500
})
// 延迟跳转,让用户看到成功提示
setTimeout(() => {
redirectToHome()
}, 1500)
} catch (error) {
console.error('微信登录失败:', error)
// 处理不同类型的错误
let errorMsg = '登录失败,请稍后重试'
if (error.message) {
if (error.message.includes('用户拒绝')) {
errorMsg = '需要您的授权才能登录'
} else if (error.message.includes('网络')) {
errorMsg = '网络连接失败,请检查网络设置'
} else {
errorMsg = error.message
}
}
uni.showModal({
title: '登录失败',
content: errorMsg,
showCancel: false,
confirmText: '我知道了'
})
// 记录错误到应用store
appStore.addError({
type: 'login_error',
message: errorMsg,
detail: error
})
} finally {
isLogging.value = false
uni.hideLoading()
}
}
/**
* 跳转到首页
*/
function redirectToHome() {
// 使用 reLaunch 重新启动到首页,清除页面栈
uni.reLaunch({
url: '/pages/index/index'
})
}
/**
* 显示隐私政策
*/
function showPrivacyPolicy() {
uni.showModal({
title: '隐私政策',
content: '我们重视您的隐私,详细的隐私政策请访问官网查看。',
showCancel: false
})
}
/**
* 显示用户协议
*/
function showUserAgreement() {
uni.showModal({
title: '用户协议',
content: '请仔细阅读用户协议条款,详细协议请访问官网查看。',
showCancel: false
})
}
</script>
<style lang="scss" scoped>
.login-page {
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
flex-direction: column;
}
/* 自定义导航栏 */
.custom-navbar {
position: relative;
.navbar-content {
height: 88rpx;
display: flex;
align-items: center;
justify-content: center;
position: relative;
.navbar-title {
font-size: $font-size-lg;
font-weight: bold;
color: white;
}
}
}
/* 登录内容 */
.login-content {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
padding: 60rpx 60rpx 120rpx;
}
/* Logo区域 */
.logo-section {
text-align: center;
margin-bottom: 120rpx;
.logo {
width: 160rpx;
height: 160rpx;
border-radius: 32rpx;
margin-bottom: 40rpx;
}
.app-name {
display: block;
font-size: 48rpx;
font-weight: bold;
color: white;
margin-bottom: 20rpx;
}
.app-desc {
font-size: $font-size-md;
color: rgba(255, 255, 255, 0.8);
}
}
/* 登录区域 */
.login-section {
.wx-login-btn {
width: 100%;
height: 88rpx;
background: white;
border-radius: 44rpx;
display: flex;
align-items: center;
justify-content: center;
box-shadow: $shadow-normal;
border: none;
margin-bottom: 40rpx;
&:active {
transform: scale(0.98);
transition: transform 0.1s;
}
&:disabled {
opacity: 0.6;
}
.wx-icon {
width: 48rpx;
height: 48rpx;
margin-right: 20rpx;
}
.btn-text {
font-size: $font-size-lg;
color: $text-primary;
font-weight: bold;
}
}
.login-tips {
text-align: center;
.tip-text {
font-size: $font-size-xs;
color: rgba(255, 255, 255, 0.7);
}
.link-text {
font-size: $font-size-xs;
color: white;
text-decoration: underline;
}
}
}
/* 底部信息 */
.footer {
text-align: center;
padding: 40rpx;
.footer-text {
font-size: $font-size-sm;
color: rgba(255, 255, 255, 0.6);
}
}
</style>

View File

@@ -0,0 +1,463 @@
<template>
<view class="container">
<!-- 用户信息卡片 -->
<view class="user-card">
<view class="user-info" v-if="userStore.isAuthenticated">
<image :src="userStore.avatar" class="avatar" />
<view class="user-details">
<text class="username">{{ userStore.nickname }}</text>
<text class="user-desc">{{ userStore.userInfo?.phone || '未绑定手机号' }}</text>
</view>
<text class="settings-btn" @tap="goToSettings">设置</text>
</view>
<!-- 未登录状态 -->
<view class="login-prompt" v-else @tap="goToLogin">
<image src="/static/images/default-avatar.png" class="avatar" />
<view class="user-details">
<text class="username">点击登录</text>
<text class="user-desc">登录后享受更多服务</text>
</view>
<text class="login-arrow">></text>
</view>
</view>
<!-- 统计数据 -->
<view class="stats-section" v-if="userStore.isAuthenticated">
<view class="stats-grid">
<view class="stats-item" @tap="goToPolicies">
<text class="stats-number">{{ stats.policies }}</text>
<text class="stats-label">我的保单</text>
</view>
<view class="stats-item" @tap="goToClaims">
<text class="stats-number">{{ stats.claims }}</text>
<text class="stats-label">理赔申请</text>
</view>
<view class="stats-item" @tap="goToApplications">
<text class="stats-number">{{ stats.applications }}</text>
<text class="stats-label">投保申请</text>
</view>
</view>
</view>
<!-- 功能菜单 -->
<view class="menu-section">
<!-- 我的服务 -->
<view class="menu-group">
<text class="group-title">我的服务</text>
<view class="menu-item" @tap="goToPolicies">
<image src="/static/images/menu-policy.png" class="menu-icon" />
<text class="menu-text">我的保单</text>
<text class="menu-arrow">></text>
</view>
<view class="menu-item" @tap="goToClaims">
<image src="/static/images/menu-claim.png" class="menu-icon" />
<text class="menu-text">理赔申请</text>
<text class="menu-arrow">></text>
</view>
<view class="menu-item" @tap="goToApplications">
<image src="/static/images/menu-application.png" class="menu-icon" />
<text class="menu-text">投保申请</text>
<text class="menu-arrow">></text>
</view>
<view class="menu-item" @tap="goToProfile">
<image src="/static/images/menu-profile.png" class="menu-icon" />
<text class="menu-text">个人资料</text>
<text class="menu-arrow">></text>
</view>
</view>
<!-- 帮助中心 -->
<view class="menu-group">
<text class="group-title">帮助中心</text>
<view class="menu-item" @tap="goToHelp">
<image src="/static/images/menu-help.png" class="menu-icon" />
<text class="menu-text">常见问题</text>
<text class="menu-arrow">></text>
</view>
<view class="menu-item" @tap="contactService">
<image src="/static/images/menu-service.png" class="menu-icon" />
<text class="menu-text">联系客服</text>
<text class="menu-arrow">></text>
</view>
<view class="menu-item" @tap="goToFeedback">
<image src="/static/images/menu-feedback.png" class="menu-icon" />
<text class="menu-text">意见反馈</text>
<text class="menu-arrow">></text>
</view>
</view>
<!-- 其他 -->
<view class="menu-group">
<text class="group-title">其他</text>
<view class="menu-item" @tap="goToAbout">
<image src="/static/images/menu-about.png" class="menu-icon" />
<text class="menu-text">关于我们</text>
<text class="menu-arrow">></text>
</view>
<view class="menu-item" @tap="checkUpdate">
<image src="/static/images/menu-update.png" class="menu-icon" />
<text class="menu-text">检查更新</text>
<text class="menu-arrow">></text>
</view>
</view>
</view>
<!-- 退出登录按钮 -->
<view class="logout-section" v-if="userStore.isAuthenticated">
<button class="logout-btn" @tap="handleLogout">退出登录</button>
</view>
</view>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { useUserStore } from '@/store/user'
import { useInsuranceStore } from '@/store/insurance'
// 状态管理
const userStore = useUserStore()
const insuranceStore = useInsuranceStore()
// 响应式数据
const stats = reactive({
policies: 0,
claims: 0,
applications: 0
})
// 加载用户统计数据(动态调用保险端后端)
const loadUserStats = async () => {
if (!userStore.isAuthenticated) return
try {
// 这里可以添加专门的统计接口调用
// 暂时使用模拟数据
stats.policies = 3
stats.claims = 1
stats.applications = 2
console.log('用户统计数据加载成功')
} catch (error) {
console.error('加载用户统计数据失败:', error)
}
}
// 页面跳转方法
const goToLogin = () => {
uni.navigateTo({
url: '/pages/login/login'
})
}
const goToSettings = () => {
uni.navigateTo({
url: '/pages/settings/settings'
})
}
const goToPolicies = () => {
if (!userStore.isAuthenticated) {
goToLogin()
return
}
uni.navigateTo({
url: '/pages/policies/policies'
})
}
const goToClaims = () => {
if (!userStore.isAuthenticated) {
goToLogin()
return
}
uni.navigateTo({
url: '/pages/claims/claims'
})
}
const goToApplications = () => {
if (!userStore.isAuthenticated) {
goToLogin()
return
}
uni.navigateTo({
url: '/pages/applications/applications'
})
}
const goToProfile = () => {
if (!userStore.isAuthenticated) {
goToLogin()
return
}
uni.navigateTo({
url: '/pages/profile/profile'
})
}
const goToHelp = () => {
uni.navigateTo({
url: '/pages/help/help'
})
}
const contactService = () => {
uni.showActionSheet({
itemList: ['拨打客服电话', '在线客服'],
success: (res) => {
if (res.tapIndex === 0) {
uni.makePhoneCall({
phoneNumber: '400-888-8888'
})
} else if (res.tapIndex === 1) {
uni.showToast({
title: '在线客服功能开发中',
icon: 'none'
})
}
}
})
}
const goToFeedback = () => {
uni.navigateTo({
url: '/pages/feedback/feedback'
})
}
const goToAbout = () => {
uni.navigateTo({
url: '/pages/about/about'
})
}
const checkUpdate = () => {
uni.showLoading({
title: '检查中...'
})
setTimeout(() => {
uni.hideLoading()
uni.showToast({
title: '当前已是最新版本',
icon: 'success'
})
}, 1500)
}
// 退出登录
const handleLogout = () => {
uni.showModal({
title: '确认退出',
content: '确定要退出登录吗?',
success: async (res) => {
if (res.confirm) {
try {
// 调用用户状态管理的退出方法
await userStore.logout()
// 清理统计数据
stats.policies = 0
stats.claims = 0
stats.applications = 0
uni.showToast({
title: '已退出登录',
icon: 'success'
})
} catch (error) {
console.error('退出登录失败:', error)
uni.showToast({
title: '退出失败',
icon: 'none'
})
}
}
}
})
}
// 下拉刷新
const onPullDownRefresh = async () => {
try {
if (userStore.isAuthenticated) {
await Promise.all([
userStore.getUserProfile(),
loadUserStats()
])
}
} catch (error) {
console.error('刷新失败:', error)
} finally {
uni.stopPullDownRefresh()
}
}
// 生命周期
onMounted(() => {
loadUserStats()
})
// 页面显示时刷新数据
onShow(() => {
if (userStore.isAuthenticated) {
loadUserStats()
}
})
</script>
<style scoped>
.container {
background-color: #f5f5f5;
min-height: 100vh;
}
.user-card {
background: linear-gradient(135deg, #1890ff 0%, #40a9ff 100%);
padding: 40rpx 30rpx;
margin-bottom: 20rpx;
}
.user-info,
.login-prompt {
display: flex;
align-items: center;
}
.avatar {
width: 120rpx;
height: 120rpx;
border-radius: 60rpx;
margin-right: 30rpx;
border: 3px solid rgba(255, 255, 255, 0.3);
}
.user-details {
flex: 1;
}
.username {
font-size: 36rpx;
font-weight: bold;
color: #fff;
margin-bottom: 10rpx;
display: block;
}
.user-desc {
font-size: 26rpx;
color: rgba(255, 255, 255, 0.8);
}
.settings-btn,
.login-arrow {
font-size: 28rpx;
color: rgba(255, 255, 255, 0.9);
padding: 10rpx 20rpx;
}
.stats-section {
background: #fff;
margin-bottom: 20rpx;
padding: 30rpx;
}
.stats-grid {
display: flex;
justify-content: space-around;
}
.stats-item {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
}
.stats-number {
font-size: 48rpx;
font-weight: bold;
color: #1890ff;
margin-bottom: 10rpx;
}
.stats-label {
font-size: 26rpx;
color: #666;
}
.menu-section {
background: #fff;
margin-bottom: 20rpx;
}
.menu-group {
margin-bottom: 20rpx;
}
.group-title {
display: block;
font-size: 28rpx;
color: #999;
padding: 30rpx 30rpx 20rpx;
background: #fafafa;
border-bottom: 1px solid #f0f0f0;
}
.menu-item {
display: flex;
align-items: center;
padding: 30rpx;
border-bottom: 1px solid #f8f8f8;
}
.menu-item:last-child {
border-bottom: none;
}
.menu-icon {
width: 48rpx;
height: 48rpx;
margin-right: 30rpx;
}
.menu-text {
flex: 1;
font-size: 30rpx;
color: #333;
}
.menu-arrow {
font-size: 28rpx;
color: #999;
}
.logout-section {
padding: 30rpx;
}
.logout-btn {
width: 100%;
height: 88rpx;
background: #fff;
color: #f5222d;
border: 1px solid #f5222d;
border-radius: 44rpx;
font-size: 32rpx;
font-weight: bold;
}
.logout-btn:active {
background: #fff2f0;
}
</style>

View File

@@ -0,0 +1,498 @@
<template>
<view class="container">
<!-- 搜索栏 -->
<view class="search-bar">
<view class="search-input-wrapper">
<image src="/static/images/search-icon.png" class="search-icon" />
<input
v-model="searchKeyword"
class="search-input"
placeholder="搜索保险产品"
@input="onSearchInput"
@confirm="handleSearch"
/>
<text v-if="searchKeyword" class="clear-btn" @tap="clearSearch">×</text>
</view>
</view>
<!-- 筛选标签 -->
<view class="filter-tabs">
<scroll-view class="tabs-scroll" scroll-x="true">
<view class="tab-list">
<text
v-for="category in categories"
:key="category.value"
class="tab-item"
:class="{ active: selectedCategory === category.value }"
@tap="selectCategory(category.value)"
>
{{ category.label }}
</text>
</view>
</scroll-view>
</view>
<!-- 产品列表 -->
<view class="product-list">
<view
v-for="product in products"
:key="product.id"
class="product-item"
@tap="goToProductDetail(product.id)"
>
<image
:src="product.icon || '/static/images/default-product.png'"
class="product-image"
mode="aspectFill"
/>
<view class="product-content">
<view class="product-header">
<text class="product-name">{{ product.name }}</text>
<text v-if="product.is_hot" class="hot-tag">热门</text>
</view>
<text class="product-desc">{{ product.description }}</text>
<view class="product-features">
<text
v-for="feature in getProductFeatures(product)"
:key="feature"
class="feature-tag"
>
{{ feature }}
</text>
</view>
<view class="product-footer">
<view class="price-info">
<text class="price-label">起保费</text>
<text class="price-value">¥{{ product.min_premium }}</text>
<text class="price-unit">/</text>
</view>
<view class="coverage-info">
<text class="coverage-label">最高保额</text>
<text class="coverage-value">{{ formatAmount(product.max_amount) }}</text>
</view>
</view>
</view>
</view>
</view>
<!-- 加载状态 -->
<view v-if="loading" class="loading">
<text>加载中...</text>
</view>
<!-- 空状态 -->
<view v-if="!loading && products.length === 0" class="empty-state">
<image src="/static/images/empty-products.png" class="empty-icon" />
<text class="empty-text">{{ searchKeyword ? '未找到相关产品' : '暂无产品' }}</text>
<button v-if="searchKeyword" class="empty-btn" @tap="clearSearch">清除搜索</button>
</view>
<!-- 加载更多 -->
<view v-if="hasMore && !loading" class="load-more" @tap="loadMore">
<text>加载更多</text>
</view>
<!-- 没有更多数据 -->
<view v-if="!hasMore && products.length > 0" class="no-more">
<text>没有更多数据了</text>
</view>
</view>
</template>
<script setup>
import { ref, reactive, computed, onMounted } from 'vue'
import { useInsuranceStore } from '@/store/insurance'
import { INSURANCE_TYPES_TEXT } from '@/utils/constants'
// 状态管理
const insuranceStore = useInsuranceStore()
// 响应式数据
const loading = ref(false)
const searchKeyword = ref('')
const selectedCategory = ref('')
const searchTimer = ref(null)
// 产品分类
const categories = ref([
{ label: '全部', value: '' },
{ label: '人寿保险', value: 'life' },
{ label: '健康保险', value: 'health' },
{ label: '车险', value: 'car' },
{ label: '财产保险', value: 'property' },
{ label: '旅行保险', value: 'travel' }
])
// 计算属性
const products = computed(() => insuranceStore.insuranceTypes)
const hasMore = computed(() => {
const pagination = insuranceStore.pagination.products
return pagination.page * pagination.limit < pagination.total
})
// 获取产品特色标签
const getProductFeatures = (product) => {
const features = []
if (product.is_hot) features.push('热门推荐')
if (product.min_premium <= 100) features.push('低保费')
if (product.max_amount >= 1000000) features.push('高保额')
return features.slice(0, 2)
}
// 格式化金额
const formatAmount = (amount) => {
if (amount >= 10000) {
return (amount / 10000).toFixed(0) + '万'
}
return amount.toString()
}
// 加载产品列表(动态调用保险端后端)
const loadProducts = async (isRefresh = true) => {
try {
loading.value = true
if (isRefresh) {
insuranceStore.resetPagination('products')
}
// 构建查询参数
const params = {
page: isRefresh ? 1 : insuranceStore.pagination.products.page + 1
}
if (searchKeyword.value.trim()) {
params.name = searchKeyword.value.trim()
}
if (selectedCategory.value) {
params.category = selectedCategory.value
}
// 动态调用保险端 /api/insurance-types 接口
await insuranceStore.fetchInsuranceTypes(params)
console.log('产品列表加载成功')
} catch (error) {
console.error('加载产品列表失败:', error)
uni.showToast({
title: '加载失败',
icon: 'none'
})
} finally {
loading.value = false
}
}
// 搜索输入处理(使用防抖优化)
const onSearchInput = (event) => {
const value = event.detail.value
searchKeyword.value = value
// 清除之前的定时器
if (searchTimer.value) {
clearTimeout(searchTimer.value)
}
// 设置新的定时器300ms 防抖)
searchTimer.value = setTimeout(() => {
handleSearch()
}, 300)
}
// 执行搜索
const handleSearch = () => {
console.log('搜索关键词:', searchKeyword.value)
loadProducts(true)
}
// 清除搜索
const clearSearch = () => {
searchKeyword.value = ''
if (searchTimer.value) {
clearTimeout(searchTimer.value)
}
loadProducts(true)
}
// 选择分类
const selectCategory = (category) => {
selectedCategory.value = category
console.log('选择分类:', category)
loadProducts(true)
}
// 加载更多
const loadMore = () => {
if (loading.value || !hasMore.value) return
loadProducts(false)
}
// 跳转到产品详情
const goToProductDetail = (id) => {
uni.navigateTo({
url: `/pages/product-detail/product-detail?id=${id}`
})
}
// 下拉刷新
const onPullDownRefresh = async () => {
try {
await loadProducts(true)
} finally {
uni.stopPullDownRefresh()
}
}
// 页面触底加载更多
const onReachBottom = () => {
loadMore()
}
// 生命周期
onMounted(() => {
loadProducts(true)
})
// 页面显示时刷新
onShow(() => {
// 如果列表为空,重新加载
if (products.value.length === 0) {
loadProducts(true)
}
})
</script>
<style scoped>
.container {
background-color: #f5f5f5;
min-height: 100vh;
}
.search-bar {
background: #fff;
padding: 20rpx 30rpx;
border-bottom: 1px solid #f0f0f0;
}
.search-input-wrapper {
display: flex;
align-items: center;
background: #f5f5f5;
border-radius: 50rpx;
padding: 0 30rpx;
height: 70rpx;
}
.search-icon {
width: 32rpx;
height: 32rpx;
margin-right: 20rpx;
opacity: 0.6;
}
.search-input {
flex: 1;
font-size: 28rpx;
height: 70rpx;
line-height: 70rpx;
}
.clear-btn {
font-size: 40rpx;
color: #999;
margin-left: 10rpx;
padding: 0 10rpx;
}
.filter-tabs {
background: #fff;
border-bottom: 1px solid #f0f0f0;
}
.tabs-scroll {
white-space: nowrap;
}
.tab-list {
display: flex;
padding: 0 20rpx;
}
.tab-item {
display: inline-block;
padding: 20rpx 30rpx;
font-size: 28rpx;
color: #666;
white-space: nowrap;
border-bottom: 3rpx solid transparent;
}
.tab-item.active {
color: #1890ff;
border-bottom-color: #1890ff;
font-weight: bold;
}
.product-list {
padding: 20rpx;
}
.product-item {
display: flex;
background: #fff;
border-radius: 12rpx;
padding: 30rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
}
.product-image {
width: 120rpx;
height: 120rpx;
border-radius: 8rpx;
margin-right: 30rpx;
flex-shrink: 0;
}
.product-content {
flex: 1;
}
.product-header {
display: flex;
align-items: center;
margin-bottom: 10rpx;
}
.product-name {
font-size: 32rpx;
font-weight: bold;
color: #333;
flex: 1;
}
.hot-tag {
background: #ff4d4f;
color: #fff;
font-size: 20rpx;
padding: 4rpx 12rpx;
border-radius: 20rpx;
margin-left: 20rpx;
}
.product-desc {
font-size: 26rpx;
color: #666;
line-height: 1.4;
margin-bottom: 15rpx;
}
.product-features {
display: flex;
gap: 10rpx;
margin-bottom: 20rpx;
}
.feature-tag {
background: #f0f9ff;
color: #1890ff;
font-size: 22rpx;
padding: 6rpx 12rpx;
border-radius: 20rpx;
}
.product-footer {
display: flex;
justify-content: space-between;
align-items: center;
}
.price-info {
display: flex;
align-items: baseline;
}
.price-label {
font-size: 24rpx;
color: #999;
margin-right: 10rpx;
}
.price-value {
font-size: 36rpx;
font-weight: bold;
color: #ff6b35;
}
.price-unit {
font-size: 24rpx;
color: #999;
margin-left: 4rpx;
}
.coverage-info {
text-align: right;
}
.coverage-label {
font-size: 24rpx;
color: #999;
display: block;
}
.coverage-value {
font-size: 28rpx;
color: #333;
font-weight: bold;
}
.loading {
text-align: center;
padding: 60rpx;
color: #999;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
padding: 120rpx 40rpx;
}
.empty-icon {
width: 200rpx;
height: 200rpx;
margin-bottom: 40rpx;
opacity: 0.5;
}
.empty-text {
font-size: 28rpx;
color: #999;
margin-bottom: 40rpx;
}
.empty-btn {
background: #1890ff;
color: #fff;
border: none;
border-radius: 44rpx;
padding: 20rpx 40rpx;
font-size: 28rpx;
}
.load-more {
text-align: center;
padding: 40rpx;
color: #1890ff;
font-size: 28rpx;
}
.no-more {
text-align: center;
padding: 40rpx;
color: #999;
font-size: 26rpx;
}
</style>

View File

@@ -0,0 +1,66 @@
# 静态资源说明
## 图片资源目录结构
```
static/images/
├── icons/ # 图标类
│ ├── tab-home.png # 首页tab图标
│ ├── tab-home-active.png # 首页tab选中图标
│ ├── tab-products.png # 产品tab图标
│ ├── tab-products-active.png
│ ├── tab-my.png # 我的tab图标
│ └── tab-my-active.png
├── banners/ # 轮播图
│ ├── banner-1.jpg
│ ├── banner-2.jpg
│ └── banner-3.jpg
├── products/ # 产品相关
│ ├── default-product.png # 默认产品图标
│ ├── life-insurance.png # 人寿保险图标
│ ├── health-insurance.png # 健康保险图标
│ └── car-insurance.png # 车险图标
├── avatars/ # 头像类
│ └── default-avatar.png # 默认头像
├── empty/ # 空状态图
│ ├── empty-products.png
│ ├── empty-policies.png
│ └── empty-claims.png
└── common/ # 通用图标
├── logo.png # 应用Logo
├── search-icon.png # 搜索图标
├── wx-icon.png # 微信图标
├── checkbox-checked.png # 选中状态
└── checkbox-unchecked.png # 未选中状态
```
## 资源使用规范
1. **图标规格**
- Tab图标60px × 60px
- 普通图标48px × 48px
- 产品图标120px × 120px
2. **图片格式**
- 图标类PNG格式支持透明
- 照片类JPG格式
- 需要透明PNG格式
3. **命名规范**
- 使用小写字母和连字符
- 语义化命名
- 状态类图标加后缀(如:-active、-disabled
4. **大小优化**
- 图标文件大小控制在20KB以内
- 轮播图控制在100KB以内
- 使用适当的压缩率
## 注意事项
由于这是演示项目,实际开发中需要:
1. 准备真实的图片资源
2. 根据设计稿确定图标样式
3. 进行图片压缩优化
4. 考虑不同分辨率的适配

View File

@@ -0,0 +1,6 @@
// store/index.js - Pinia Store 入口
import { createPinia } from 'pinia'
const store = createPinia()
export default store

View File

@@ -0,0 +1,375 @@
// store/insurance.js - 保险业务状态管理
import { defineStore } from 'pinia'
import { insuranceAPI, policyAPI, claimAPI } from '@/utils/api'
import { APPLICATION_STATUS, POLICY_STATUS, CLAIM_STATUS } from '@/utils/constants'
export const useInsuranceStore = defineStore('insurance', {
state: () => ({
// 保险产品
insuranceTypes: [],
hotProducts: [],
currentProduct: null,
productsLoading: false,
// 保险申请
applications: [],
currentApplication: null,
applicationsLoading: false,
applicationStats: {
total: 0,
pending: 0,
approved: 0,
rejected: 0
},
// 保单
policies: [],
currentPolicy: null,
policiesLoading: false,
// 理赔
claims: [],
currentClaim: null,
claimsLoading: false,
// 分页信息
pagination: {
products: { page: 1, limit: 10, total: 0 },
applications: { page: 1, limit: 10, total: 0 },
policies: { page: 1, limit: 10, total: 0 },
claims: { page: 1, limit: 10, total: 0 }
}
}),
getters: {
// 获取热门产品
getHotProducts: (state) => state.hotProducts.slice(0, 6),
// 根据状态获取申请
getApplicationsByStatus: (state) => (status) => {
return state.applications.filter(app => app.status === status)
},
// 根据状态获取保单
getPoliciesByStatus: (state) => (status) => {
return state.policies.filter(policy => policy.status === status)
},
// 根据状态获取理赔
getClaimsByStatus: (state) => (status) => {
return state.claims.filter(claim => claim.status === status)
},
// 有效保单数量
activePoliciesCount: (state) => {
return state.policies.filter(policy => policy.status === POLICY_STATUS.ACTIVE).length
},
// 待处理理赔数量
pendingClaimsCount: (state) => {
return state.claims.filter(claim => claim.status === CLAIM_STATUS.PENDING).length
}
},
actions: {
// 获取保险产品列表(动态调用保险端后端)
async fetchInsuranceTypes(params = {}) {
try {
this.productsLoading = true
// 动态调用 /api/insurance-types
const res = await insuranceAPI.getTypes({
page: this.pagination.products.page,
limit: this.pagination.products.limit,
...params
})
if (params.page === 1) {
this.insuranceTypes = res.data
} else {
this.insuranceTypes.push(...res.data)
}
// 更新分页信息
if (res.pagination) {
this.pagination.products = {
...this.pagination.products,
...res.pagination
}
}
return res.data
} catch (error) {
console.error('获取保险产品失败:', error)
throw error
} finally {
this.productsLoading = false
}
},
// 获取热门产品(动态调用)
async fetchHotProducts() {
try {
// 动态调用保险端接口获取热门产品
const res = await insuranceAPI.getTypes({
is_hot: true,
status: 'active',
limit: 8
})
this.hotProducts = res.data || []
return this.hotProducts
} catch (error) {
console.error('获取热门产品失败:', error)
throw error
}
},
// 获取产品详情(动态调用)
async fetchProductDetail(id) {
try {
// 动态调用 /api/insurance-types/:id
const res = await insuranceAPI.getTypeDetail(id)
this.currentProduct = res.data
return res.data
} catch (error) {
console.error('获取产品详情失败:', error)
throw error
}
},
// 提交保险申请(动态调用保险端后端)
async submitApplication(applicationData) {
try {
// 动态调用 /api/insurance/applications
const res = await insuranceAPI.submitApplication(applicationData)
// 添加到本地状态(如果需要)
if (res.data) {
this.applications.unshift(res.data)
this.applicationStats.total += 1
this.applicationStats.pending += 1
}
return res.data
} catch (error) {
console.error('提交保险申请失败:', error)
throw error
}
},
// 获取我的申请列表(动态调用)
async fetchMyApplications(params = {}) {
try {
this.applicationsLoading = true
// 动态调用后端扩展接口
const res = await insuranceAPI.getMyApplications({
page: this.pagination.applications.page,
limit: this.pagination.applications.limit,
...params
})
if (params.page === 1) {
this.applications = res.data
} else {
this.applications.push(...res.data)
}
// 更新分页信息
if (res.pagination) {
this.pagination.applications = {
...this.pagination.applications,
...res.pagination
}
}
return res.data
} catch (error) {
console.error('获取我的申请失败:', error)
throw error
} finally {
this.applicationsLoading = false
}
},
// 获取申请详情(动态调用)
async fetchApplicationDetail(id) {
try {
// 动态调用 /api/insurance/applications/:id
const res = await insuranceAPI.getApplicationDetail(id)
this.currentApplication = res.data
return res.data
} catch (error) {
console.error('获取申请详情失败:', error)
throw error
}
},
// 获取我的保单列表(动态调用)
async fetchMyPolicies(params = {}) {
try {
this.policiesLoading = true
// 动态调用后端扩展接口
const res = await policyAPI.getMyPolicies({
page: this.pagination.policies.page,
limit: this.pagination.policies.limit,
...params
})
if (params.page === 1) {
this.policies = res.data
} else {
this.policies.push(...res.data)
}
// 更新分页信息
if (res.pagination) {
this.pagination.policies = {
...this.pagination.policies,
...res.pagination
}
}
return res.data
} catch (error) {
console.error('获取我的保单失败:', error)
throw error
} finally {
this.policiesLoading = false
}
},
// 获取保单详情(动态调用)
async fetchPolicyDetail(id) {
try {
// 动态调用 /api/policies/:id
const res = await policyAPI.getPolicyDetail(id)
this.currentPolicy = res.data
return res.data
} catch (error) {
console.error('获取保单详情失败:', error)
throw error
}
},
// 提交理赔申请(动态调用)
async submitClaim(claimData) {
try {
// 动态调用 /api/claims
const res = await claimAPI.submitClaim(claimData)
// 添加到本地状态
if (res.data) {
this.claims.unshift(res.data)
}
return res.data
} catch (error) {
console.error('提交理赔申请失败:', error)
throw error
}
},
// 获取我的理赔列表(动态调用)
async fetchMyClaims(params = {}) {
try {
this.claimsLoading = true
// 动态调用后端扩展接口
const res = await claimAPI.getMyClaims({
page: this.pagination.claims.page,
limit: this.pagination.claims.limit,
...params
})
if (params.page === 1) {
this.claims = res.data
} else {
this.claims.push(...res.data)
}
// 更新分页信息
if (res.pagination) {
this.pagination.claims = {
...this.pagination.claims,
...res.pagination
}
}
return res.data
} catch (error) {
console.error('获取我的理赔失败:', error)
throw error
} finally {
this.claimsLoading = false
}
},
// 获取理赔详情(动态调用)
async fetchClaimDetail(id) {
try {
// 动态调用 /api/claims/:id
const res = await claimAPI.getClaimDetail(id)
this.currentClaim = res.data
return res.data
} catch (error) {
console.error('获取理赔详情失败:', error)
throw error
}
},
// 上传理赔材料(动态调用)
async uploadClaimDocument(filePath, claimId) {
try {
// 动态调用上传接口
const res = await claimAPI.uploadClaimDocument(filePath, claimId)
return res.data
} catch (error) {
console.error('上传理赔材料失败:', error)
throw error
}
},
// 获取申请统计(动态调用)
async fetchApplicationStats() {
try {
const res = await insuranceAPI.getApplicationStats()
this.applicationStats = {
...this.applicationStats,
...res.data
}
return res.data
} catch (error) {
console.error('获取申请统计失败:', error)
throw error
}
},
// 重置分页
resetPagination(type) {
if (this.pagination[type]) {
this.pagination[type].page = 1
}
},
// 清理数据
clearData() {
this.insuranceTypes = []
this.hotProducts = []
this.currentProduct = null
this.applications = []
this.currentApplication = null
this.policies = []
this.currentPolicy = null
this.claims = []
this.currentClaim = null
// 重置分页
Object.keys(this.pagination).forEach(key => {
this.pagination[key].page = 1
})
}
}
})

View File

@@ -0,0 +1,227 @@
// store/user.js - 基于Pinia的用户状态管理
import { defineStore } from 'pinia'
import { authAPI } from '@/utils/api'
import { STORAGE_KEYS } from '@/utils/constants'
export const useUserStore = defineStore('user', {
state: () => ({
token: '',
userInfo: null,
isLoggedIn: false,
loginTime: null
}),
getters: {
// 是否已认证
isAuthenticated: (state) => !!state.token && !!state.userInfo,
// 用户昵称
nickname: (state) => state.userInfo?.nickname || '未登录',
// 用户头像
avatar: (state) => state.userInfo?.avatar || '/static/images/default-avatar.png',
// 用户角色
roleId: (state) => state.userInfo?.role_id || null,
// 是否为管理员
isAdmin: (state) => {
const roleId = state.userInfo?.role_id
return roleId === 1 || roleId === 2
},
// 是否为普通用户
isUser: (state) => state.userInfo?.role_id === 3
},
actions: {
// 微信登录(动态调用保险端后端认证接口)
async wxLogin() {
try {
console.log('开始微信登录...')
// 1. 获取微信登录码
const loginRes = await uni.login({
provider: 'weixin'
})
if (!loginRes[1]?.code) {
throw new Error('获取登录码失败')
}
console.log('获取到微信登录码:', loginRes[1].code)
// 2. 获取用户信息(微信小程序需要用户授权)
const userInfoRes = await uni.getUserInfo({
provider: 'weixin'
})
console.log('获取到用户信息:', userInfoRes[1])
// 3. 发送到保险端后端验证(动态调用)
const authRes = await authAPI.wxLogin({
code: loginRes[1].code,
userInfo: userInfoRes[1].userInfo,
signature: userInfoRes[1].signature,
rawData: userInfoRes[1].rawData
})
console.log('后端认证成功:', authRes)
// 4. 保存 token 和用户信息
this.token = authRes.data.token
this.userInfo = authRes.data.userInfo
this.isLoggedIn = true
this.loginTime = new Date().toISOString()
// 持久化存储
uni.setStorageSync(STORAGE_KEYS.TOKEN, this.token)
uni.setStorageSync(STORAGE_KEYS.USER_INFO, this.userInfo)
uni.setStorageSync(STORAGE_KEYS.LAST_LOGIN_TIME, this.loginTime)
console.log('登录状态保存成功')
return authRes.data
} catch (error) {
console.error('微信登录失败:', error)
// 登录失败时清理状态
this.clearLoginState()
throw error
}
},
// 普通账号密码登录(复用现有接口)
async login(credentials) {
try {
console.log('开始账号密码登录...')
// 动态调用保险端登录接口
const authRes = await authAPI.login(credentials)
// 保存登录状态
this.token = authRes.data.token
this.userInfo = authRes.data.userInfo
this.isLoggedIn = true
this.loginTime = new Date().toISOString()
// 持久化存储
uni.setStorageSync(STORAGE_KEYS.TOKEN, this.token)
uni.setStorageSync(STORAGE_KEYS.USER_INFO, this.userInfo)
uni.setStorageSync(STORAGE_KEYS.LAST_LOGIN_TIME, this.loginTime)
return authRes.data
} catch (error) {
console.error('登录失败:', error)
this.clearLoginState()
throw error
}
},
// 检查登录状态
checkAuth() {
try {
const token = uni.getStorageSync(STORAGE_KEYS.TOKEN)
const userInfo = uni.getStorageSync(STORAGE_KEYS.USER_INFO)
const loginTime = uni.getStorageSync(STORAGE_KEYS.LAST_LOGIN_TIME)
if (token && userInfo) {
this.token = token
this.userInfo = userInfo
this.isLoggedIn = true
this.loginTime = loginTime
console.log('恢复登录状态成功:', userInfo)
return true
}
return false
} catch (error) {
console.error('检查登录状态失败:', error)
return false
}
},
// 获取用户信息(动态调用保险端接口)
async getUserProfile() {
try {
const res = await authAPI.getProfile()
this.userInfo = res.data
// 更新本地存储
uni.setStorageSync(STORAGE_KEYS.USER_INFO, this.userInfo)
return res.data
} catch (error) {
console.error('获取用户信息失败:', error)
throw error
}
},
// 更新用户信息(动态调用保险端接口)
async updateProfile(data) {
try {
const res = await authAPI.updateProfile(data)
this.userInfo = { ...this.userInfo, ...res.data }
// 更新本地存储
uni.setStorageSync(STORAGE_KEYS.USER_INFO, this.userInfo)
return res.data
} catch (error) {
console.error('更新用户信息失败:', error)
throw error
}
},
// 退出登录
async logout() {
try {
// 调用后端退出接口
await authAPI.logout()
} catch (error) {
console.warn('后端退出接口调用失败:', error)
} finally {
// 无论后端接口是否成功,都清理本地状态
this.clearLoginState()
// 跳转到登录页
uni.reLaunch({
url: '/pages/login/login'
})
}
},
// 清理登录状态
clearLoginState() {
this.token = ''
this.userInfo = null
this.isLoggedIn = false
this.loginTime = null
// 清理本地存储
uni.removeStorageSync(STORAGE_KEYS.TOKEN)
uni.removeStorageSync(STORAGE_KEYS.USER_INFO)
uni.removeStorageSync(STORAGE_KEYS.LAST_LOGIN_TIME)
console.log('登录状态已清理')
},
// 刷新token
async refreshToken() {
try {
// 如果后端支持token刷新
const res = await authAPI.refreshToken()
this.token = res.data.token
uni.setStorageSync(STORAGE_KEYS.TOKEN, this.token)
return res.data.token
} catch (error) {
console.error('刷新token失败:', error)
// token刷新失败需要重新登录
this.logout()
throw error
}
}
}
})

View File

@@ -0,0 +1,271 @@
// stores/app.js - 应用全局状态管理
import { defineStore } from 'pinia'
export const useAppStore = defineStore('app', {
state: () => ({
// 系统信息
systemInfo: {},
// 网络状态
networkType: 'unknown',
isOnline: true,
// 应用配置
config: {
theme: 'light',
language: 'zh-CN'
},
// 加载状态
globalLoading: false,
// 错误信息
errors: [],
// 通知消息
notifications: [],
// 搜索历史
searchHistory: []
}),
getters: {
// 是否为暗黑主题
isDarkTheme: (state) => state.config.theme === 'dark',
// 未读通知数量
unreadNotificationCount: (state) => state.notifications.filter(n => !n.read).length,
// 最近搜索记录前10条
recentSearches: (state) => state.searchHistory.slice(0, 10)
},
actions: {
/**
* 初始化应用
*/
async initApp() {
try {
// 获取系统信息
const systemInfo = uni.getSystemInfoSync()
this.systemInfo = systemInfo
// 获取网络类型
const networkInfo = await uni.getNetworkType()
this.networkType = networkInfo.networkType
this.isOnline = networkInfo.networkType !== 'none'
// 加载本地配置
this.loadConfig()
// 加载搜索历史
this.loadSearchHistory()
console.log('应用初始化完成:', {
systemInfo: this.systemInfo,
networkType: this.networkType,
config: this.config
})
} catch (error) {
console.error('应用初始化失败:', error)
}
},
/**
* 加载配置
*/
loadConfig() {
try {
const config = uni.getStorageSync('app_config')
if (config) {
this.config = { ...this.config, ...config }
}
} catch (error) {
console.error('加载配置失败:', error)
}
},
/**
* 保存配置
*/
saveConfig() {
try {
uni.setStorageSync('app_config', this.config)
} catch (error) {
console.error('保存配置失败:', error)
}
},
/**
* 设置主题
*/
setTheme(theme) {
this.config.theme = theme
this.saveConfig()
},
/**
* 设置语言
*/
setLanguage(language) {
this.config.language = language
this.saveConfig()
},
/**
* 设置全局加载状态
*/
setGlobalLoading(loading) {
this.globalLoading = loading
},
/**
* 添加错误信息
*/
addError(error) {
const errorItem = {
id: Date.now(),
message: error.message || error,
timestamp: new Date(),
type: error.type || 'error'
}
this.errors.unshift(errorItem)
// 只保留最近50条错误
if (this.errors.length > 50) {
this.errors = this.errors.slice(0, 50)
}
},
/**
* 清除错误信息
*/
clearErrors() {
this.errors = []
},
/**
* 添加通知
*/
addNotification(notification) {
const notificationItem = {
id: Date.now(),
title: notification.title,
message: notification.message,
type: notification.type || 'info',
read: false,
timestamp: new Date()
}
this.notifications.unshift(notificationItem)
// 只保留最近100条通知
if (this.notifications.length > 100) {
this.notifications = this.notifications.slice(0, 100)
}
},
/**
* 标记通知为已读
*/
markNotificationAsRead(id) {
const notification = this.notifications.find(n => n.id === id)
if (notification) {
notification.read = true
}
},
/**
* 标记所有通知为已读
*/
markAllNotificationsAsRead() {
this.notifications.forEach(n => n.read = true)
},
/**
* 清除通知
*/
clearNotifications() {
this.notifications = []
},
/**
* 加载搜索历史
*/
loadSearchHistory() {
try {
const history = uni.getStorageSync('search_history')
if (history) {
this.searchHistory = history
}
} catch (error) {
console.error('加载搜索历史失败:', error)
}
},
/**
* 保存搜索历史
*/
saveSearchHistory() {
try {
uni.setStorageSync('search_history', this.searchHistory)
} catch (error) {
console.error('保存搜索历史失败:', error)
}
},
/**
* 添加搜索记录
*/
addSearchHistory(keyword) {
if (!keyword || !keyword.trim()) return
keyword = keyword.trim()
// 移除已存在的相同关键词
this.searchHistory = this.searchHistory.filter(item => item !== keyword)
// 添加到开头
this.searchHistory.unshift(keyword)
// 只保留最近20条
if (this.searchHistory.length > 20) {
this.searchHistory = this.searchHistory.slice(0, 20)
}
this.saveSearchHistory()
},
/**
* 清除搜索历史
*/
clearSearchHistory() {
this.searchHistory = []
this.saveSearchHistory()
},
/**
* 更新网络状态
*/
updateNetworkStatus(networkType) {
this.networkType = networkType
this.isOnline = networkType !== 'none'
if (!this.isOnline) {
this.addNotification({
title: '网络连接',
message: '网络连接已断开,请检查网络设置',
type: 'warning'
})
}
},
/**
* 监听网络状态变化
*/
watchNetworkStatus() {
uni.onNetworkStatusChange((res) => {
this.updateNetworkStatus(res.networkType)
})
}
}
})

View File

@@ -0,0 +1,12 @@
// stores/index.js - Pinia Store入口文件
import { createPinia } from 'pinia'
// 创建Pinia实例
const pinia = createPinia()
export default pinia
// 导出所有store
export { useUserStore } from './user'
export { useInsuranceStore } from './insurance'
export { useAppStore } from './app'

View File

@@ -0,0 +1,346 @@
/* styles/base.scss - 基础样式 */
/* 重置样式 */
* {
box-sizing: border-box;
}
page {
background-color: $background-color;
font-size: $font-size-md;
line-height: $line-height-normal;
color: $text-primary;
}
/* 通用布局 */
.container {
padding: $spacing-md;
}
.page {
min-height: 100vh;
background-color: $background-color;
}
.page-content {
padding: $spacing-md;
}
/* 卡片样式 */
.card {
background: $background-white;
border-radius: $border-radius-large;
padding: $spacing-md;
margin-bottom: $spacing-md;
box-shadow: $shadow-light;
}
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: $spacing-sm;
.card-title {
font-size: $font-size-lg;
font-weight: bold;
color: $text-primary;
}
}
.card-body {
color: $text-secondary;
line-height: $line-height-large;
}
/* 按钮样式 */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 24rpx 32rpx;
border-radius: $border-radius-normal;
font-size: $font-size-md;
text-align: center;
transition: all $animation-duration-normal;
border: none;
cursor: pointer;
&.btn-block {
width: 100%;
}
&.btn-large {
padding: 32rpx 48rpx;
font-size: $font-size-lg;
}
&.btn-small {
padding: 16rpx 24rpx;
font-size: $font-size-sm;
}
&.btn-primary {
background-color: $primary-color;
color: white;
&:active {
background-color: darken($primary-color, 10%);
}
&:disabled {
background-color: $border-color;
color: $text-disabled;
}
}
&.btn-secondary {
background-color: $background-light;
color: $text-primary;
border: 1rpx solid $border-color;
&:active {
background-color: $border-light;
}
}
&.btn-success {
background-color: $success-color;
color: white;
&:active {
background-color: darken($success-color, 10%);
}
}
&.btn-warning {
background-color: $warning-color;
color: white;
&:active {
background-color: darken($warning-color, 10%);
}
}
&.btn-error {
background-color: $error-color;
color: white;
&:active {
background-color: darken($error-color, 10%);
}
}
}
/* 表单样式 */
.form-group {
margin-bottom: $spacing-md;
}
.form-label {
display: block;
margin-bottom: $spacing-xs;
font-size: $font-size-md;
color: $text-primary;
&.required::after {
content: ' *';
color: $error-color;
}
}
.form-input {
width: 100%;
padding: 24rpx;
border: 1rpx solid $border-color;
border-radius: $border-radius-normal;
font-size: $font-size-md;
background-color: $background-white;
&:focus {
border-color: $primary-color;
outline: none;
}
&:disabled {
background-color: $background-light;
color: $text-disabled;
}
}
.form-textarea {
@extend .form-input;
height: 120rpx;
resize: none;
}
.form-select {
@extend .form-input;
position: relative;
&::after {
content: '';
position: absolute;
right: 20rpx;
top: 50%;
transform: translateY(-50%);
width: 0;
height: 0;
border-left: 10rpx solid transparent;
border-right: 10rpx solid transparent;
border-top: 10rpx solid $text-secondary;
}
}
/* 列表样式 */
.list {
background: $background-white;
border-radius: $border-radius-large;
overflow: hidden;
}
.list-item {
padding: $spacing-md;
border-bottom: 1rpx solid $border-light;
display: flex;
align-items: center;
&:last-child {
border-bottom: none;
}
&:active {
background-color: $background-light;
}
}
.list-item-content {
flex: 1;
}
.list-item-title {
font-size: $font-size-md;
color: $text-primary;
margin-bottom: 8rpx;
}
.list-item-subtitle {
font-size: $font-size-sm;
color: $text-secondary;
}
.list-item-extra {
margin-left: $spacing-sm;
color: $text-secondary;
}
/* 状态标签 */
.status-badge {
display: inline-block;
padding: 8rpx 16rpx;
border-radius: 24rpx;
font-size: $font-size-xs;
font-weight: bold;
&.status-pending {
background-color: lighten($warning-color, 35%);
color: $warning-color;
}
&.status-approved {
background-color: lighten($success-color, 35%);
color: $success-color;
}
&.status-rejected {
background-color: lighten($error-color, 35%);
color: $error-color;
}
&.status-reviewing {
background-color: lighten($primary-color, 35%);
color: $primary-color;
}
}
/* 工具类 */
.text-center {
text-align: center;
}
.text-left {
text-align: left;
}
.text-right {
text-align: right;
}
.text-primary {
color: $text-primary;
}
.text-secondary {
color: $text-secondary;
}
.text-success {
color: $success-color;
}
.text-warning {
color: $warning-color;
}
.text-error {
color: $error-color;
}
.flex {
display: flex;
}
.flex-center {
display: flex;
align-items: center;
justify-content: center;
}
.flex-between {
display: flex;
align-items: center;
justify-content: space-between;
}
.flex-column {
display: flex;
flex-direction: column;
}
.flex-1 {
flex: 1;
}
/* 间距工具类 */
.mt-10 { margin-top: 10rpx; }
.mt-20 { margin-top: 20rpx; }
.mt-30 { margin-top: 30rpx; }
.mb-10 { margin-bottom: 10rpx; }
.mb-20 { margin-bottom: 20rpx; }
.mb-30 { margin-bottom: 30rpx; }
.ml-10 { margin-left: 10rpx; }
.ml-20 { margin-left: 20rpx; }
.mr-10 { margin-right: 10rpx; }
.mr-20 { margin-right: 20rpx; }
.p-10 { padding: 10rpx; }
.p-20 { padding: 20rpx; }
.p-30 { padding: 30rpx; }
.pt-10 { padding-top: 10rpx; }
.pt-20 { padding-top: 20rpx; }
.pb-10 { padding-bottom: 10rpx; }
.pb-20 { padding-bottom: 20rpx; }
.pl-10 { padding-left: 10rpx; }
.pl-20 { padding-left: 20rpx; }
.pr-10 { padding-right: 10rpx; }
.pr-20 { padding-right: 20rpx; }

View File

@@ -0,0 +1,56 @@
/* styles/variables.scss - SCSS变量定义 */
/* 主题颜色 */
$primary-color: #1890ff;
$success-color: #52c41a;
$warning-color: #faad14;
$error-color: #f5222d;
$info-color: #722ed1;
/* 文本颜色 */
$text-primary: #262626;
$text-secondary: #8c8c8c;
$text-disabled: #bfbfbf;
/* 背景颜色 */
$background-color: #f5f5f5;
$background-light: #fafafa;
$background-white: #ffffff;
/* 边框颜色 */
$border-color: #e8e8e8;
$border-light: #f0f0f0;
/* 阴影 */
$shadow-light: 0 2rpx 8rpx 0 rgba(0, 0, 0, 0.06);
$shadow-normal: 0 2rpx 12rpx 0 rgba(0, 0, 0, 0.1);
$shadow-deep: 0 4rpx 16rpx 0 rgba(0, 0, 0, 0.12);
/* 圆角 */
$border-radius-small: 4rpx;
$border-radius-normal: 8rpx;
$border-radius-large: 12rpx;
/* 间距 */
$spacing-xs: 10rpx;
$spacing-sm: 20rpx;
$spacing-md: 30rpx;
$spacing-lg: 40rpx;
$spacing-xl: 60rpx;
/* 字体大小 */
$font-size-xs: 20rpx;
$font-size-sm: 24rpx;
$font-size-md: 28rpx;
$font-size-lg: 32rpx;
$font-size-xl: 36rpx;
$font-size-xxl: 40rpx;
/* 行高 */
$line-height-normal: 1.5;
$line-height-large: 1.8;
/* 动画时间 */
$animation-duration-fast: 0.2s;
$animation-duration-normal: 0.3s;
$animation-duration-slow: 0.5s;

View File

@@ -0,0 +1,216 @@
// utils/api.js - 全部动态调用现有保险端insurance_backend的API
import request from './request'
// 认证相关 API扩展支持微信登录
export const authAPI = {
// 微信登录(新增)
wxLogin: (data) => request({
url: '/auth/wx-login',
method: 'POST',
data
}),
// 复用现有登录接口
login: (data) => request({
url: '/auth/login',
method: 'POST',
data
}),
// 获取用户信息(动态调用)
getProfile: () => request({
url: '/auth/profile'
}),
// 更新用户信息(动态调用)
updateProfile: (data) => request({
url: '/auth/profile',
method: 'PUT',
data
}),
// 退出登录
logout: () => request({
url: '/auth/logout',
method: 'POST'
})
}
// 保险产品相关 API全部动态调用现有接口
export const insuranceAPI = {
// 获取保险产品列表(动态调用 /api/insurance-types
getTypes: (params) => request({
url: '/insurance-types',
data: params
}),
// 获取产品详情(动态调用)
getTypeDetail: (id) => request({
url: `/insurance-types/${id}`
}),
// 提交保险申请(动态调用 /api/insurance/applications
submitApplication: (data) => request({
url: '/insurance/applications',
method: 'POST',
data
}),
// 获取我的申请列表(动态调用,需后端扩展)
getMyApplications: (params) => request({
url: '/miniprogram/my-applications',
data: params
}),
// 获取申请详情(动态调用)
getApplicationDetail: (id) => request({
url: `/insurance/applications/${id}`
}),
// 获取申请统计(动态调用)
getApplicationStats: () => request({
url: '/insurance/applications/stats'
})
}
// 保单相关 API全部动态调用现有接口
export const policyAPI = {
// 获取我的保单列表(动态调用,需后端扩展)
getMyPolicies: (params) => request({
url: '/miniprogram/my-policies',
data: params
}),
// 获取保单详情(动态调用 /api/policies/:id
getPolicyDetail: (id) => request({
url: `/policies/${id}`
}),
// 获取保单列表(管理端接口)
getList: (params) => request({
url: '/policies',
data: params
})
}
// 理赔相关 API全部动态调用现有接口
export const claimAPI = {
// 提交理赔申请(动态调用 /api/claims
submitClaim: (data) => request({
url: '/claims',
method: 'POST',
data
}),
// 获取我的理赔列表(动态调用,需后端扩展)
getMyClaims: (params) => request({
url: '/miniprogram/my-claims',
data: params
}),
// 获取理赔详情(动态调用 /api/claims/:id
getClaimDetail: (id) => request({
url: `/claims/${id}`
}),
// 更新理赔状态(动态调用)
updateClaimStatus: (id, data) => request({
url: `/claims/${id}/status`,
method: 'PUT',
data
}),
// 上传理赔材料(动态调用)
uploadClaimDocument: (filePath, claimId) => {
return new Promise((resolve, reject) => {
const token = uni.getStorageSync('token')
uni.uploadFile({
url: `${BASE_URL}/claims/upload-document`,
filePath,
name: 'file',
formData: {
claim_id: claimId
},
header: {
'Authorization': token ? `Bearer ${token}` : ''
},
success: (res) => {
try {
const data = JSON.parse(res.data)
if (data.status === 'success') {
resolve(data)
} else {
reject(data)
}
} catch (error) {
reject({ message: '响应解析失败' })
}
},
fail: reject
})
})
}
}
// 小程序专用API动态调用后端扩展接口
export const miniprogramAPI = {
// 获取小程序首页数据
getHomeData: () => request({
url: '/miniprogram/home'
}),
// 获取产品列表(小程序版本)
getProducts: (params) => request({
url: '/miniprogram/products',
data: params
}),
// 获取用户统计信息
getUserStats: () => request({
url: '/miniprogram/user-stats'
})
}
// 统计数据 API动态调用 /api/system
export const dashboardAPI = {
getStats: () => request({
url: '/system/stats'
}),
getRecentActivities: () => request({
url: '/system/logs',
data: { limit: 10 }
})
}
// 用户管理 API动态调用现有接口
export const userAPI = {
getList: (params) => request({
url: '/users',
data: params
}),
create: (data) => request({
url: '/users',
method: 'POST',
data
}),
update: (id, data) => request({
url: `/users/${id}`,
method: 'PUT',
data
}),
delete: (id) => request({
url: `/users/${id}`,
method: 'DELETE'
})
}
export default {
authAPI,
insuranceAPI,
policyAPI,
claimAPI,
miniprogramAPI,
dashboardAPI,
userAPI
}

View File

@@ -0,0 +1,109 @@
// utils/auth.js - Vue.js 认证工具
import { useUserStore } from '@/store/user'
export const auth = {
// 快速检查登录状态
checkAuth() {
const userStore = useUserStore()
return userStore.checkAuth()
},
// 获取用户信息
getUserInfo() {
const userStore = useUserStore()
return userStore.userInfo
},
// 获取Token
getToken() {
const userStore = useUserStore()
return userStore.token
},
// 登录拦截器(页面级别使用)
requireAuth() {
if (!this.checkAuth()) {
uni.showModal({
title: '提示',
content: '请先登录',
success: (res) => {
if (res.confirm) {
uni.navigateTo({ url: '/pages/login/login' })
}
}
})
return false
}
return true
},
// 退出登录
logout() {
const userStore = useUserStore()
userStore.logout()
},
// 角色权限检查
hasRole(roleId) {
const userInfo = this.getUserInfo()
return userInfo && userInfo.role_id === roleId
},
// 是否为管理员
isAdmin() {
return this.hasRole(1) || this.hasRole(2)
},
// 是否为普通用户
isUser() {
return this.hasRole(3)
}
}
// 路由拦截器工具
export const routeGuard = {
// 检查页面访问权限
checkPageAuth(pagePath) {
// 定义需要登录的页面
const authRequiredPages = [
'/pages/application/application',
'/pages/policies/policies',
'/pages/claims/claims',
'/pages/my/my'
]
if (authRequiredPages.includes(pagePath)) {
return auth.requireAuth()
}
return true
},
// 管理员页面权限检查
checkAdminAuth() {
if (!auth.checkAuth()) {
uni.showModal({
title: '提示',
content: '请先登录',
success: (res) => {
if (res.confirm) {
uni.navigateTo({ url: '/pages/login/login' })
}
}
})
return false
}
if (!auth.isAdmin()) {
uni.showToast({
title: '权限不足',
icon: 'none'
})
return false
}
return true
}
}
export default auth

View File

@@ -0,0 +1,288 @@
// utils/constants.js - 常量定义
/**
* API 基础配置
*/
export const API_CONFIG = {
// 动态获取保险端后端地址
BASE_URL: process.env.NODE_ENV === 'development'
? 'http://localhost:3000/api' // 本地开发环境
: 'https://your-insurance-backend.com/api', // 生产环境
TIMEOUT: 10000, // 请求超时时间
// 缓存控制避免304状态码
CACHE_HEADERS: {
'Cache-Control': 'no-cache',
'Pragma': 'no-cache'
}
}
/**
* 存储键名
*/
export const STORAGE_KEYS = {
TOKEN: 'token',
USER_INFO: 'userInfo',
SEARCH_HISTORY: 'searchHistory',
CART: 'cart',
SETTINGS: 'settings'
}
/**
* 保险产品类型
*/
export const INSURANCE_TYPES = {
LIFE: 'life', // 寿险
HEALTH: 'health', // 健康险
ACCIDENT: 'accident', // 意外险
VEHICLE: 'vehicle', // 车险
PROPERTY: 'property', // 财产险
TRAVEL: 'travel', // 旅行险
PET: 'pet' // 宠物险
}
/**
* 保险产品类型中文映射
*/
export const INSURANCE_TYPE_LABELS = {
[INSURANCE_TYPES.LIFE]: '寿险',
[INSURANCE_TYPES.HEALTH]: '健康险',
[INSURANCE_TYPES.ACCIDENT]: '意外险',
[INSURANCE_TYPES.VEHICLE]: '车险',
[INSURANCE_TYPES.PROPERTY]: '财产险',
[INSURANCE_TYPES.TRAVEL]: '旅行险',
[INSURANCE_TYPES.PET]: '宠物险'
}
/**
* 申请状态
*/
export const APPLICATION_STATUS = {
PENDING: 'pending', // 待审核
UNDER_REVIEW: 'under_review', // 审核中
APPROVED: 'approved', // 已批准
REJECTED: 'rejected' // 已拒绝
}
/**
* 申请状态中文映射
*/
export const APPLICATION_STATUS_LABELS = {
[APPLICATION_STATUS.PENDING]: '待审核',
[APPLICATION_STATUS.UNDER_REVIEW]: '审核中',
[APPLICATION_STATUS.APPROVED]: '已批准',
[APPLICATION_STATUS.REJECTED]: '已拒绝'
}
/**
* 保单状态
*/
export const POLICY_STATUS = {
ACTIVE: 'active', // 有效
EXPIRED: 'expired', // 已过期
CANCELLED: 'cancelled', // 已取消
SUSPENDED: 'suspended' // 暂停
}
/**
* 保单状态中文映射
*/
export const POLICY_STATUS_LABELS = {
[POLICY_STATUS.ACTIVE]: '有效',
[POLICY_STATUS.EXPIRED]: '已过期',
[POLICY_STATUS.CANCELLED]: '已取消',
[POLICY_STATUS.SUSPENDED]: '暂停'
}
/**
* 理赔状态
*/
export const CLAIM_STATUS = {
PENDING: 'pending', // 待处理
REVIEWING: 'reviewing', // 审核中
APPROVED: 'approved', // 已批准
REJECTED: 'rejected', // 已拒绝
PAID: 'paid' // 已赔付
}
/**
* 理赔状态中文映射
*/
export const CLAIM_STATUS_LABELS = {
[CLAIM_STATUS.PENDING]: '待处理',
[CLAIM_STATUS.REVIEWING]: '审核中',
[CLAIM_STATUS.APPROVED]: '已批准',
[CLAIM_STATUS.REJECTED]: '已拒绝',
[CLAIM_STATUS.PAID]: '已赔付'
}
/**
* 用户角色
*/
export const USER_ROLES = {
ADMIN: 1, // 管理员
REVIEWER: 2, // 审核员
CUSTOMER: 3 // 客户
}
/**
* 用户角色中文映射
*/
export const USER_ROLE_LABELS = {
[USER_ROLES.ADMIN]: '管理员',
[USER_ROLES.REVIEWER]: '审核员',
[USER_ROLES.CUSTOMER]: '客户'
}
/**
* 性别
*/
export const GENDER = {
UNKNOWN: 0, // 未知
MALE: 1, // 男
FEMALE: 2 // 女
}
/**
* 性别中文映射
*/
export const GENDER_LABELS = {
[GENDER.UNKNOWN]: '未知',
[GENDER.MALE]: '男',
[GENDER.FEMALE]: '女'
}
/**
* 与受益人关系
*/
export const BENEFICIARY_RELATIONS = [
{ value: 'spouse', label: '配偶' },
{ value: 'parent', label: '父母' },
{ value: 'child', label: '子女' },
{ value: 'sibling', label: '兄弟姐妹' },
{ value: 'other', label: '其他' }
]
/**
* 文件类型限制
*/
export const FILE_TYPES = {
IMAGE: ['jpg', 'jpeg', 'png', 'gif', 'webp'],
DOCUMENT: ['pdf', 'doc', 'docx', 'txt'],
ALL: ['jpg', 'jpeg', 'png', 'gif', 'webp', 'pdf', 'doc', 'docx', 'txt']
}
/**
* 文件大小限制(字节)
*/
export const FILE_SIZE_LIMITS = {
IMAGE: 5 * 1024 * 1024, // 5MB
DOCUMENT: 10 * 1024 * 1024, // 10MB
DEFAULT: 5 * 1024 * 1024 // 5MB
}
/**
* 分页配置
*/
export const PAGINATION = {
DEFAULT_PAGE_SIZE: 10,
DEFAULT_PAGE: 1,
MAX_PAGE_SIZE: 100
}
/**
* 表单验证规则
*/
export const VALIDATION_RULES = {
PHONE: /^1[3-9]\d{9}$/,
ID_CARD: /^[1-9]\d{5}(19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[0-9Xx]$/,
EMAIL: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
PASSWORD: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d@$!%*?&]{8,}$/
}
/**
* 错误码映射
*/
export const ERROR_CODES = {
NETWORK_ERROR: 'NETWORK_ERROR',
TIMEOUT: 'TIMEOUT',
UNAUTHORIZED: 'UNAUTHORIZED',
FORBIDDEN: 'FORBIDDEN',
NOT_FOUND: 'NOT_FOUND',
VALIDATION_ERROR: 'VALIDATION_ERROR',
SERVER_ERROR: 'SERVER_ERROR'
}
/**
* 错误信息映射
*/
export const ERROR_MESSAGES = {
[ERROR_CODES.NETWORK_ERROR]: '网络连接失败,请检查网络设置',
[ERROR_CODES.TIMEOUT]: '请求超时,请稍后重试',
[ERROR_CODES.UNAUTHORIZED]: '登录已过期,请重新登录',
[ERROR_CODES.FORBIDDEN]: '权限不足,无法访问',
[ERROR_CODES.NOT_FOUND]: '请求的资源不存在',
[ERROR_CODES.VALIDATION_ERROR]: '数据验证失败',
[ERROR_CODES.SERVER_ERROR]: '服务器内部错误,请稍后重试'
}
/**
* 主题颜色
*/
export const THEME_COLORS = {
PRIMARY: '#1890ff',
SUCCESS: '#52c41a',
WARNING: '#faad14',
ERROR: '#f5222d',
INFO: '#722ed1',
TEXT_PRIMARY: '#262626',
TEXT_SECONDARY: '#8c8c8c',
BORDER: '#e8e8e8',
BACKGROUND: '#f5f5f5'
}
/**
* 动画持续时间
*/
export const ANIMATION_DURATION = {
FAST: 200,
NORMAL: 300,
SLOW: 500
}
/**
* 微信小程序限制
*/
export const WECHAT_LIMITS = {
MAX_REQUEST_SIZE: 1048576, // 1MB
MAX_UPLOAD_SIZE: 10485760, // 10MB
MAX_CONCURRENT_REQUESTS: 10, // 最大并发请求数
MAX_WEBSOCKET_CONNECTIONS: 5 // 最大WebSocket连接数
}
export default {
API_CONFIG,
STORAGE_KEYS,
INSURANCE_TYPES,
INSURANCE_TYPE_LABELS,
APPLICATION_STATUS,
APPLICATION_STATUS_LABELS,
POLICY_STATUS,
POLICY_STATUS_LABELS,
CLAIM_STATUS,
CLAIM_STATUS_LABELS,
USER_ROLES,
USER_ROLE_LABELS,
GENDER,
GENDER_LABELS,
BENEFICIARY_RELATIONS,
FILE_TYPES,
FILE_SIZE_LIMITS,
PAGINATION,
VALIDATION_RULES,
ERROR_CODES,
ERROR_MESSAGES,
THEME_COLORS,
ANIMATION_DURATION,
WECHAT_LIMITS
}

View File

@@ -0,0 +1,73 @@
// utils/request.js - 统一请求封装兼容Vue.js和小程序的请求封装
import { useUserStore } from '@/store/user'
// 动态获取保险端后端地址
const BASE_URL = process.env.NODE_ENV === 'development'
? 'http://localhost:3000/api' // 本地开发环境,对应 c:\nxxmdata\insurance_backend
: 'https://your-insurance-backend.com/api' // 生产环境
// 统一请求封装支持Vue.js和小程序
export function request(options) {
return new Promise((resolve, reject) => {
const userStore = useUserStore()
const token = userStore.token || uni.getStorageSync('token')
// 统一请求配置
const requestConfig = {
url: BASE_URL + options.url,
method: options.method || 'GET',
data: options.data || {},
header: {
'Content-Type': 'application/json',
'Authorization': token ? `Bearer ${token}` : '',
// 根据记忆添加缓存控制头避免304问题
'Cache-Control': 'no-cache',
'Pragma': 'no-cache',
...options.header
},
timeout: 10000,
success: (res) => {
console.log('API请求成功:', options.url, res)
// 统一响应处理(兼容现有保险端格式)
if (res.statusCode === 200) {
if (res.data.status === 'success') {
resolve(res.data)
} else {
uni.showToast({
title: res.data.message || '请求失败',
icon: 'none'
})
reject(res.data)
}
} else if (res.statusCode === 401) {
// token 过期,清理登录状态
console.warn('Token过期清理登录状态')
userStore.logout()
uni.removeStorageSync('token')
uni.redirectTo({ url: '/pages/login/login' })
reject(res.data)
} else {
uni.showToast({
title: '网络请求失败',
icon: 'none'
})
reject(res.data)
}
},
fail: (error) => {
console.error('API请求失败:', options.url, error)
uni.showToast({
title: '网络连接失败',
icon: 'none'
})
reject(error)
}
}
// 发起请求兼容小程序和H5
uni.request(requestConfig)
})
}
export default request

View File

@@ -0,0 +1,243 @@
// utils/util.js - 通用工具函数
/**
* 格式化日期
* @param {Date|string|number} date 日期
* @param {string} format 格式 'YYYY-MM-DD HH:mm:ss'
*/
export function formatDate(date, format = 'YYYY-MM-DD HH:mm:ss') {
if (!date) return ''
const d = new Date(date)
if (isNaN(d.getTime())) return ''
const year = d.getFullYear()
const month = String(d.getMonth() + 1).padStart(2, '0')
const day = String(d.getDate()).padStart(2, '0')
const hours = String(d.getHours()).padStart(2, '0')
const minutes = String(d.getMinutes()).padStart(2, '0')
const seconds = String(d.getSeconds()).padStart(2, '0')
return format
.replace('YYYY', year)
.replace('MM', month)
.replace('DD', day)
.replace('HH', hours)
.replace('mm', minutes)
.replace('ss', seconds)
}
/**
* 格式化金额
* @param {number|string} amount 金额
* @param {number} decimals 小数位数
*/
export function formatMoney(amount, decimals = 2) {
if (!amount && amount !== 0) return '0.00'
const num = parseFloat(amount)
if (isNaN(num)) return '0.00'
return num.toLocaleString('zh-CN', {
minimumFractionDigits: decimals,
maximumFractionDigits: decimals
})
}
/**
* 防抖函数
* @param {Function} func 要防抖的函数
* @param {number} delay 延迟时间(毫秒)
*/
export function debounce(func, delay = 300) {
let timeoutId
return function (...args) {
const context = this
clearTimeout(timeoutId)
timeoutId = setTimeout(() => func.apply(context, args), delay)
}
}
/**
* 节流函数
* @param {Function} func 要节流的函数
* @param {number} limit 限制时间(毫秒)
*/
export function throttle(func, limit = 300) {
let inThrottle
return function (...args) {
const context = this
if (!inThrottle) {
func.apply(context, args)
inThrottle = true
setTimeout(() => inThrottle = false, limit)
}
}
}
/**
* 手机号脱敏
* @param {string} phone 手机号
*/
export function maskPhone(phone) {
if (!phone || phone.length !== 11) return phone
return phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2')
}
/**
* 身份证号脱敏
* @param {string} idCard 身份证号
*/
export function maskIdCard(idCard) {
if (!idCard || idCard.length < 6) return idCard
return idCard.replace(/(\d{4})\d+(\d{4})/, '$1**********$2')
}
/**
* 验证手机号
* @param {string} phone 手机号
*/
export function validatePhone(phone) {
const phoneRegex = /^1[3-9]\d{9}$/
return phoneRegex.test(phone)
}
/**
* 验证身份证号
* @param {string} idCard 身份证号
*/
export function validateIdCard(idCard) {
const idCardRegex = /^[1-9]\d{5}(19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[0-9Xx]$/
return idCardRegex.test(idCard)
}
/**
* 获取文件扩展名
* @param {string} filename 文件名
*/
export function getFileExtension(filename) {
if (!filename) return ''
const lastDot = filename.lastIndexOf('.')
return lastDot === -1 ? '' : filename.substring(lastDot + 1).toLowerCase()
}
/**
* 格式化文件大小
* @param {number} bytes 字节数
*/
export function formatFileSize(bytes) {
if (!bytes || bytes === 0) return '0 B'
const k = 1024
const sizes = ['B', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
}
/**
* 生成随机字符串
* @param {number} length 长度
*/
export function generateRandomString(length = 8) {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
let result = ''
for (let i = 0; i < length; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length))
}
return result
}
/**
* 深拷贝
* @param {any} obj 要拷贝的对象
*/
export function deepClone(obj) {
if (obj === null || typeof obj !== 'object') return obj
if (obj instanceof Date) return new Date(obj.getTime())
if (obj instanceof Array) return obj.map(item => deepClone(item))
if (obj instanceof Object) {
const clonedObj = {}
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
clonedObj[key] = deepClone(obj[key])
}
}
return clonedObj
}
}
/**
* 获取查询参数
* @param {string} url URL地址
*/
export function getQueryParams(url) {
const params = {}
const queryString = url.split('?')[1]
if (queryString) {
queryString.split('&').forEach(param => {
const [key, value] = param.split('=')
params[decodeURIComponent(key)] = decodeURIComponent(value || '')
})
}
return params
}
/**
* 保险状态映射
*/
export const INSURANCE_STATUS = {
pending: { text: '待审核', color: '#faad14' },
approved: { text: '已通过', color: '#52c41a' },
rejected: { text: '已拒绝', color: '#f5222d' },
under_review: { text: '审核中', color: '#1890ff' }
}
export const POLICY_STATUS = {
active: { text: '有效', color: '#52c41a' },
expired: { text: '已过期', color: '#8c8c8c' },
cancelled: { text: '已取消', color: '#f5222d' },
suspended: { text: '暂停', color: '#faad14' }
}
export const CLAIM_STATUS = {
pending: { text: '待处理', color: '#faad14' },
reviewing: { text: '审核中', color: '#1890ff' },
approved: { text: '已批准', color: '#52c41a' },
rejected: { text: '已拒绝', color: '#f5222d' },
paid: { text: '已赔付', color: '#722ed1' }
}
/**
* 获取状态显示信息
* @param {string} status 状态值
* @param {string} type 类型 'insurance' | 'policy' | 'claim'
*/
export function getStatusInfo(status, type) {
const statusMap = {
insurance: INSURANCE_STATUS,
policy: POLICY_STATUS,
claim: CLAIM_STATUS
}
return statusMap[type]?.[status] || { text: status, color: '#8c8c8c' }
}
export default {
formatDate,
formatMoney,
debounce,
throttle,
maskPhone,
maskIdCard,
validatePhone,
validateIdCard,
getFileExtension,
formatFileSize,
generateRandomString,
deepClone,
getQueryParams,
getStatusInfo,
INSURANCE_STATUS,
POLICY_STATUS,
CLAIM_STATUS
}

View File

@@ -0,0 +1,66 @@
/* uni.scss - 全局样式变量和通用样式 */
/* 颜色变量 */
$primary-color: #1890ff;
$success-color: #52c41a;
$warning-color: #faad14;
$error-color: #f5222d;
$text-color: #333333;
$text-color-light: #666666;
$text-color-lighter: #999999;
$border-color: #d9d9d9;
$background-color: #f5f5f5;
/* 尺寸变量 */
$border-radius: 8rpx;
$border-radius-large: 12rpx;
$spacing-small: 20rpx;
$spacing-medium: 30rpx;
$spacing-large: 40rpx;
/* 字体变量 */
$font-size-small: 24rpx;
$font-size-base: 28rpx;
$font-size-medium: 30rpx;
$font-size-large: 32rpx;
$font-size-xl: 36rpx;
/* 通用类样式 */
.text-center {
text-align: center;
}
.text-left {
text-align: left;
}
.text-right {
text-align: right;
}
.text-overflow {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.text-overflow-2 {
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
/* 间距类 */
.p-0 { padding: 0 !important; }
.p-1 { padding: 10rpx !important; }
.p-2 { padding: 20rpx !important; }
.p-3 { padding: 30rpx !important; }
.p-4 { padding: 40rpx !important; }
.m-0 { margin: 0 !important; }
.m-1 { margin: 10rpx !important; }
.m-2 { margin: 20rpx !important; }
.m-3 { margin: 30rpx !important; }
.m-4 { margin: 40rpx !important; }

View File

@@ -0,0 +1,37 @@
import { defineConfig } from 'vite'
import uni from '@dcloudio/vite-plugin-uni'
import { resolve } from 'path'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [uni()],
base: '/insurance/',
resolve: {
alias: {
'@': resolve(__dirname, '.'),
'~': resolve(__dirname)
}
},
css: {
preprocessorOptions: {
scss: {
additionalData: `@import "./styles/variables.scss";`
}
}
},
server: {
port: 3000,
open: false
},
build: {
target: 'es6',
cssCodeSplit: false,
rollupOptions: {
output: {
manualChunks: {
'uni-app': ['@dcloudio/uni-app']
}
}
}
}
})