28 changed files with 8275 additions and 3062 deletions
@ -0,0 +1,71 @@ |
|||
{ |
|||
"name": "zhjwxt", |
|||
"lockfileVersion": 3, |
|||
"requires": true, |
|||
"packages": { |
|||
"": { |
|||
"dependencies": { |
|||
"@playwright/test": "^1.54.1" |
|||
} |
|||
}, |
|||
"node_modules/@playwright/test": { |
|||
"version": "1.54.1", |
|||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.54.1.tgz", |
|||
"integrity": "sha512-FS8hQ12acieG2dYSksmLOF7BNxnVf2afRJdCuM1eMSxj6QTSE6G4InGF7oApGgDb65MX7AwMVlIkpru0yZA4Xw==", |
|||
"license": "Apache-2.0", |
|||
"dependencies": { |
|||
"playwright": "1.54.1" |
|||
}, |
|||
"bin": { |
|||
"playwright": "cli.js" |
|||
}, |
|||
"engines": { |
|||
"node": ">=18" |
|||
} |
|||
}, |
|||
"node_modules/fsevents": { |
|||
"version": "2.3.2", |
|||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", |
|||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", |
|||
"hasInstallScript": true, |
|||
"license": "MIT", |
|||
"optional": true, |
|||
"os": [ |
|||
"darwin" |
|||
], |
|||
"engines": { |
|||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0" |
|||
} |
|||
}, |
|||
"node_modules/playwright": { |
|||
"version": "1.54.1", |
|||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.54.1.tgz", |
|||
"integrity": "sha512-peWpSwIBmSLi6aW2auvrUtf2DqY16YYcCMO8rTVx486jKmDTJg7UAhyrraP98GB8BoPURZP8+nxO7TSd4cPr5g==", |
|||
"license": "Apache-2.0", |
|||
"dependencies": { |
|||
"playwright-core": "1.54.1" |
|||
}, |
|||
"bin": { |
|||
"playwright": "cli.js" |
|||
}, |
|||
"engines": { |
|||
"node": ">=18" |
|||
}, |
|||
"optionalDependencies": { |
|||
"fsevents": "2.3.2" |
|||
} |
|||
}, |
|||
"node_modules/playwright-core": { |
|||
"version": "1.54.1", |
|||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.54.1.tgz", |
|||
"integrity": "sha512-Nbjs2zjj0htNhzgiy5wu+3w09YetDx5pkrpI/kZotDlDUaYk0HVA5xrBVPdow4SAUIlhgKcJeJg4GRKW6xHusA==", |
|||
"license": "Apache-2.0", |
|||
"bin": { |
|||
"playwright-core": "cli.js" |
|||
}, |
|||
"engines": { |
|||
"node": ">=18" |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,5 @@ |
|||
{ |
|||
"dependencies": { |
|||
"@playwright/test": "^1.54.1" |
|||
} |
|||
} |
|||
@ -0,0 +1,155 @@ |
|||
/** |
|||
* 工具函数索引文件 |
|||
* 统一管理项目中的可复用工具函数 |
|||
*/ |
|||
|
|||
import util from './util.js' |
|||
|
|||
// 导出所有工具函数
|
|||
export const { |
|||
// 登录退出相关
|
|||
loginOut, |
|||
openHomeView, |
|||
|
|||
// 时间格式化
|
|||
formatTime, |
|||
formatDateTime, |
|||
formatToDateTime, |
|||
dateUtils, |
|||
getCurrentDate, |
|||
|
|||
// 通用格式化
|
|||
formatLocation, |
|||
formatFileSize, |
|||
formatAge, |
|||
formatGender, |
|||
|
|||
// 对象处理
|
|||
safeGet, |
|||
|
|||
// 样式相关
|
|||
hexToRgba, |
|||
img, |
|||
getResourceUrl, |
|||
|
|||
// 数据获取
|
|||
getDict, |
|||
|
|||
// 文件上传
|
|||
uploadFile, |
|||
|
|||
// 通信相关
|
|||
makePhoneCall, |
|||
|
|||
// 页面跳转
|
|||
navigateToPage |
|||
} = util |
|||
|
|||
// 工具函数使用说明
|
|||
export const UtilsUsage = { |
|||
// 对象安全访问
|
|||
safeGet: { |
|||
description: '安全访问对象属性,避免undefined错误', |
|||
params: { |
|||
obj: 'Object - 目标对象', |
|||
path: 'String - 属性路径,如 "a.b.c"', |
|||
defaultValue: 'Any - 默认值' |
|||
}, |
|||
returns: 'Any - 属性值或默认值', |
|||
example: ` |
|||
const name = safeGet(clientInfo, 'customerResource.name', '未知客户') |
|||
` |
|||
}, |
|||
|
|||
// 文件大小格式化
|
|||
formatFileSize: { |
|||
description: '格式化文件大小为可读格式', |
|||
params: { |
|||
bytes: 'Number - 字节数' |
|||
}, |
|||
returns: 'String - 格式化后的大小', |
|||
example: ` |
|||
formatFileSize(1024000) // "1 MB"
|
|||
` |
|||
}, |
|||
|
|||
// 年龄格式化
|
|||
formatAge: { |
|||
description: '格式化年龄显示', |
|||
params: { |
|||
age: 'Number - 年龄(小数格式,如9.05表示9岁5个月)' |
|||
}, |
|||
returns: 'String - 格式化后的年龄', |
|||
example: ` |
|||
formatAge(9.05) // "9岁5个月"
|
|||
` |
|||
}, |
|||
|
|||
// 性别格式化
|
|||
formatGender: { |
|||
description: '格式化性别显示', |
|||
params: { |
|||
gender: 'Number - 性别值(1男,2女)' |
|||
}, |
|||
returns: 'String - 性别字符串', |
|||
example: ` |
|||
formatGender(1) // "男"
|
|||
` |
|||
}, |
|||
|
|||
// 拨打电话
|
|||
makePhoneCall: { |
|||
description: '拨打电话的通用方法', |
|||
params: { |
|||
phoneNumber: 'String - 电话号码', |
|||
successCallback: 'Function - 成功回调(可选)', |
|||
failCallback: 'Function - 失败回调(可选)' |
|||
}, |
|||
example: ` |
|||
makePhoneCall('13800138000', |
|||
() => console.log('拨打成功'), |
|||
(err) => console.error('拨打失败', err) |
|||
) |
|||
` |
|||
}, |
|||
|
|||
// 页面跳转
|
|||
navigateToPage: { |
|||
description: '页面跳转的通用方法', |
|||
params: { |
|||
url: 'String - 跳转路径', |
|||
params: 'Object - 跳转参数对象(可选)' |
|||
}, |
|||
example: ` |
|||
navigateToPage('/pages/detail/index', { |
|||
id: 123, |
|||
name: '测试' |
|||
}) |
|||
// 结果: /pages/detail/index?id=123&name=测试
|
|||
` |
|||
}, |
|||
|
|||
// 获取当前日期
|
|||
getCurrentDate: { |
|||
description: '获取当前日期 YYYY-MM-DD 格式', |
|||
returns: 'String - 当前日期字符串', |
|||
example: ` |
|||
getCurrentDate() // "2024-01-15"
|
|||
` |
|||
}, |
|||
|
|||
// 时间格式化
|
|||
formatToDateTime: { |
|||
description: '时间格式转换', |
|||
params: { |
|||
dateTime: 'String - 时间字符串,如 "2024-05-01 01:10:21"', |
|||
fmt: 'String - 格式模板,默认 "Y-m-d H:i:s"' |
|||
}, |
|||
returns: 'String - 格式化后的时间', |
|||
example: ` |
|||
formatToDateTime('2024-05-01 01:10:21', 'Y-m-d H:i') // "2024-05-01 01:10"
|
|||
` |
|||
} |
|||
} |
|||
|
|||
export default util |
|||
@ -0,0 +1,266 @@ |
|||
# 组件和工具函数库 |
|||
|
|||
这是从 `clue_info.vue` 页面重构提取出来的可复用组件和工具函数集合。 |
|||
|
|||
## 🧩 组件列表 |
|||
|
|||
### 1. ClientInfoCard - 客户信息卡片 |
|||
显示客户基本信息的卡片组件,包含客户头像、姓名、电话等信息。 |
|||
|
|||
**位置**: `components/client-info-card/client-info-card.vue` |
|||
|
|||
**Props**: |
|||
- `clientInfo`: Object - 客户信息对象 |
|||
|
|||
**Events**: |
|||
- `call`: 拨打电话事件,参数: phoneNumber |
|||
|
|||
**使用示例**: |
|||
```vue |
|||
<ClientInfoCard |
|||
:client-info="clientInfo" |
|||
@call="handleCall" |
|||
/> |
|||
``` |
|||
|
|||
### 2. StudentInfoCard - 学生信息卡片 |
|||
显示学生信息的卡片组件,支持展开/收起操作按钮。 |
|||
|
|||
**位置**: `components/student-info-card/student-info-card.vue` |
|||
|
|||
**Props**: |
|||
- `student`: Object - 学生信息对象 |
|||
- `actions`: Array - 操作按钮配置(可选) |
|||
- `showDetails`: Boolean - 是否显示详细信息(默认true) |
|||
|
|||
**Events**: |
|||
- `toggle-actions`: 切换操作面板事件 |
|||
- `action`: 操作按钮点击事件,参数: { action, student } |
|||
|
|||
**使用示例**: |
|||
```vue |
|||
<StudentInfoCard |
|||
:student="student" |
|||
:actions="[ |
|||
{ key: 'edit', text: '编辑学生' }, |
|||
{ key: 'order', text: '查看订单' } |
|||
]" |
|||
@toggle-actions="toggleActions" |
|||
@action="handleAction" |
|||
/> |
|||
``` |
|||
|
|||
### 3. TabSwitcher - 标签切换组件 |
|||
通用的标签页切换组件。 |
|||
|
|||
**位置**: `components/tab-switcher/tab-switcher.vue` |
|||
|
|||
**Props**: |
|||
- `tabs`: Array - 标签配置数组 [{ id, name }] |
|||
- `activeTabId`: String|Number - 当前激活标签ID |
|||
|
|||
**Events**: |
|||
- `tab-change`: 标签切换事件,参数: { tabId, index, tab } |
|||
|
|||
**使用示例**: |
|||
```vue |
|||
<TabSwitcher |
|||
:tabs="[ |
|||
{ id: 1, name: '基本资料' }, |
|||
{ id: 2, name: '课程信息' }, |
|||
{ id: 3, name: '通话记录' } |
|||
]" |
|||
:active-tab-id="activeTabId" |
|||
@tab-change="handleTabChange" |
|||
/> |
|||
``` |
|||
|
|||
### 4. FitnessRecordCard - 体测记录卡片 |
|||
显示体测记录数据和相关PDF报告的卡片组件。 |
|||
|
|||
**位置**: `components/fitness-record-card/fitness-record-card.vue` |
|||
|
|||
**Props**: |
|||
- `record`: Object - 体测记录对象 |
|||
|
|||
**Events**: |
|||
- `file-click`: 文件点击事件,参数: { file, record } |
|||
|
|||
**使用示例**: |
|||
```vue |
|||
<FitnessRecordCard |
|||
:record="fitnessRecord" |
|||
@file-click="handleFileClick" |
|||
/> |
|||
``` |
|||
|
|||
### 5. CallRecordCard - 通话记录卡片 |
|||
显示通话记录详情的卡片组件。 |
|||
|
|||
**位置**: `components/call-record-card/call-record-card.vue` |
|||
|
|||
**Props**: |
|||
- `record`: Object - 通话记录对象 |
|||
|
|||
**使用示例**: |
|||
```vue |
|||
<CallRecordCard :record="callRecord" /> |
|||
``` |
|||
|
|||
## 🛠️ 工具函数库 |
|||
|
|||
### 核心工具函数 |
|||
|
|||
以下函数已添加到 `common/util.js` 中: |
|||
|
|||
#### 1. safeGet(obj, path, defaultValue) |
|||
安全访问对象属性,避免undefined错误。 |
|||
|
|||
```javascript |
|||
const name = this.$util.safeGet(clientInfo, 'customerResource.name', '未知客户') |
|||
``` |
|||
|
|||
#### 2. formatFileSize(bytes) |
|||
格式化文件大小为可读格式。 |
|||
|
|||
```javascript |
|||
this.$util.formatFileSize(1024000) // "1 MB" |
|||
``` |
|||
|
|||
#### 3. formatAge(age) |
|||
格式化年龄显示(小数转年龄+月份)。 |
|||
|
|||
```javascript |
|||
this.$util.formatAge(9.05) // "9岁5个月" |
|||
``` |
|||
|
|||
#### 4. formatGender(gender) |
|||
格式化性别显示。 |
|||
|
|||
```javascript |
|||
this.$util.formatGender(1) // "男" |
|||
this.$util.formatGender(2) // "女" |
|||
``` |
|||
|
|||
#### 5. makePhoneCall(phoneNumber, successCallback, failCallback) |
|||
拨打电话的通用方法。 |
|||
|
|||
```javascript |
|||
this.$util.makePhoneCall('13800138000') |
|||
``` |
|||
|
|||
#### 6. navigateToPage(url, params) |
|||
页面跳转的通用方法,自动处理参数拼接。 |
|||
|
|||
```javascript |
|||
this.$util.navigateToPage('/pages/detail/index', { |
|||
id: 123, |
|||
name: '测试' |
|||
}) |
|||
// 结果: /pages/detail/index?id=123&name=测试 |
|||
``` |
|||
|
|||
#### 7. getCurrentDate() |
|||
获取当前日期 YYYY-MM-DD 格式。 |
|||
|
|||
```javascript |
|||
const today = this.$util.getCurrentDate() // "2024-01-15" |
|||
``` |
|||
|
|||
## 📁 文件结构 |
|||
|
|||
``` |
|||
components/ |
|||
├── client-info-card/ |
|||
│ └── client-info-card.vue |
|||
├── student-info-card/ |
|||
│ └── student-info-card.vue |
|||
├── tab-switcher/ |
|||
│ └── tab-switcher.vue |
|||
├── fitness-record-card/ |
|||
│ └── fitness-record-card.vue |
|||
├── call-record-card/ |
|||
│ └── call-record-card.vue |
|||
├── index.js # 组件导出索引 |
|||
└── README.md # 本文档 |
|||
|
|||
common/ |
|||
├── util.js # 扩展后的工具函数库 |
|||
└── utils-index.js # 工具函数导出索引 |
|||
``` |
|||
|
|||
## 🚀 使用方式 |
|||
|
|||
### 在页面中使用组件 |
|||
|
|||
1. **直接导入使用**: |
|||
```vue |
|||
<template> |
|||
<ClientInfoCard :client-info="clientInfo" @call="handleCall" /> |
|||
</template> |
|||
|
|||
<script> |
|||
import ClientInfoCard from '@/components/client-info-card/client-info-card.vue' |
|||
|
|||
export default { |
|||
components: { |
|||
ClientInfoCard |
|||
} |
|||
} |
|||
</script> |
|||
``` |
|||
|
|||
2. **批量导入**: |
|||
```javascript |
|||
// 从索引文件导入 |
|||
import { ClientInfoCard, StudentInfoCard } from '@/components/index.js' |
|||
``` |
|||
|
|||
### 使用工具函数 |
|||
|
|||
工具函数已集成到全局 `$util` 对象中,可直接使用: |
|||
|
|||
```vue |
|||
<template> |
|||
<view>{{ $util.safeGet(data, 'user.name', '未知') }}</view> |
|||
</template> |
|||
|
|||
<script> |
|||
export default { |
|||
methods: { |
|||
handleCall() { |
|||
const phone = this.$util.safeGet(this.clientInfo, 'phone', '') |
|||
this.$util.makePhoneCall(phone) |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
``` |
|||
|
|||
## 💡 重构优势 |
|||
|
|||
1. **代码复用**: 组件和工具函数可在多个页面中复用 |
|||
2. **维护性**: 集中管理,修改一处影响全局 |
|||
3. **可测试性**: 独立组件易于单元测试 |
|||
4. **性能优化**: 减少重复代码,提升应用性能 |
|||
5. **开发效率**: 标准化组件提升开发速度 |
|||
|
|||
## 📋 迁移指南 |
|||
|
|||
原 `clue_info.vue` 中的代码可按以下方式迁移: |
|||
|
|||
1. 替换工具函数调用: |
|||
- `this.safeGet()` → `this.$util.safeGet()` |
|||
- `this.formatAge()` → `this.$util.formatAge()` |
|||
- `this.makeCall()` → `this.$util.makePhoneCall()` |
|||
|
|||
2. 替换组件代码: |
|||
- 客户信息展示区域 → `<ClientInfoCard>` |
|||
- 学生信息展示区域 → `<StudentInfoCard>` |
|||
- 标签切换区域 → `<TabSwitcher>` |
|||
- 体测记录展示 → `<FitnessRecordCard>` |
|||
- 通话记录展示 → `<CallRecordCard>` |
|||
|
|||
3. 事件处理: |
|||
- 保持原有事件处理逻辑 |
|||
- 通过组件events获取用户操作 |
|||
@ -0,0 +1,140 @@ |
|||
<!--通话记录卡片组件--> |
|||
<template> |
|||
<view class="call-record-card"> |
|||
<view class="call-header"> |
|||
<view class="call-time">{{ formatCallTime(record.call_time) }}</view> |
|||
<view class="call-duration">{{ formatDuration(record.duration) }}</view> |
|||
</view> |
|||
|
|||
<view class="call-info"> |
|||
<view class="info-row"> |
|||
<text class="info-label">通话类型:</text> |
|||
<text class="info-value">{{ getCallType(record.call_type) }}</text> |
|||
</view> |
|||
<view class="info-row" v-if="record.caller_name"> |
|||
<text class="info-label">拨打人员:</text> |
|||
<text class="info-value">{{ record.caller_name }}</text> |
|||
</view> |
|||
<view class="info-row" v-if="record.phone_number"> |
|||
<text class="info-label">通话号码:</text> |
|||
<text class="info-value">{{ record.phone_number }}</text> |
|||
</view> |
|||
</view> |
|||
|
|||
<view class="call-notes" v-if="record.notes"> |
|||
<text class="notes-label">通话备注:</text> |
|||
<text class="notes-content">{{ record.notes }}</text> |
|||
</view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
export default { |
|||
name: 'CallRecordCard', |
|||
props: { |
|||
record: { |
|||
type: Object, |
|||
required: true |
|||
} |
|||
}, |
|||
methods: { |
|||
formatCallTime(time) { |
|||
if (!time) return '未知时间' |
|||
return this.$util.formatToDateTime(time, 'Y-m-d H:i') |
|||
}, |
|||
|
|||
formatDuration(duration) { |
|||
if (!duration || duration <= 0) return '0秒' |
|||
|
|||
const minutes = Math.floor(duration / 60) |
|||
const seconds = duration % 60 |
|||
|
|||
if (minutes > 0) { |
|||
return `${minutes}分${seconds}秒` |
|||
} |
|||
return `${seconds}秒` |
|||
}, |
|||
|
|||
getCallType(type) { |
|||
const typeMap = { |
|||
1: '呼出', |
|||
2: '呼入', |
|||
3: '未接' |
|||
} |
|||
return typeMap[type] || '未知' |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style lang="less" scoped> |
|||
.call-record-card { |
|||
background-color: #1a1a1a; |
|||
border-radius: 15rpx; |
|||
padding: 25rpx; |
|||
margin-bottom: 20rpx; |
|||
border: 1rpx solid #333; |
|||
} |
|||
|
|||
.call-header { |
|||
display: flex; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
margin-bottom: 20rpx; |
|||
|
|||
.call-time { |
|||
color: white; |
|||
font-size: 26rpx; |
|||
font-weight: bold; |
|||
} |
|||
|
|||
.call-duration { |
|||
color: #29d3b4; |
|||
font-size: 22rpx; |
|||
background-color: rgba(41, 211, 180, 0.2); |
|||
padding: 6rpx 15rpx; |
|||
border-radius: 15rpx; |
|||
} |
|||
} |
|||
|
|||
.call-info { |
|||
.info-row { |
|||
display: flex; |
|||
align-items: center; |
|||
margin-bottom: 15rpx; |
|||
|
|||
.info-label { |
|||
color: #999; |
|||
font-size: 22rpx; |
|||
width: 150rpx; |
|||
flex-shrink: 0; |
|||
} |
|||
|
|||
.info-value { |
|||
color: white; |
|||
font-size: 22rpx; |
|||
flex: 1; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.call-notes { |
|||
margin-top: 15rpx; |
|||
padding-top: 15rpx; |
|||
border-top: 1rpx solid #333; |
|||
|
|||
.notes-label { |
|||
color: #999; |
|||
font-size: 22rpx; |
|||
display: block; |
|||
margin-bottom: 8rpx; |
|||
} |
|||
|
|||
.notes-content { |
|||
color: white; |
|||
font-size: 22rpx; |
|||
line-height: 1.5; |
|||
word-break: break-all; |
|||
} |
|||
} |
|||
</style> |
|||
@ -0,0 +1,217 @@ |
|||
<!--客户信息卡片组件--> |
|||
<template> |
|||
<view class="client-info-card"> |
|||
<!-- 客户基本信息区域 --> |
|||
<view class="basic-info-section"> |
|||
<view class="section-header"> |
|||
<view class="customer-avatar"> |
|||
<text>{{ $util.safeGet(clientInfo, 'customerResource.name', '客').charAt(0) }}</text> |
|||
</view> |
|||
<view class="customer-info"> |
|||
<view class="customer-name">{{ $util.safeGet(clientInfo, 'customerResource.name', '未知客户') }}</view> |
|||
<view class="customer-meta"> |
|||
<text class="customer-phone">{{ $util.safeGet(clientInfo, 'customerResource.phone_number', '') }}</text> |
|||
</view> |
|||
</view> |
|||
<view class="contact-actions"> |
|||
<view class="contact-btn phone-btn" @click="handleMakeCall"> |
|||
<text class="contact-icon">📞</text> |
|||
</view> |
|||
<view class="action-toggle" @click="toggleActions" v-if="actions && actions.length > 0"> |
|||
<text class="toggle-icon">{{ actionsExpanded ? '▲' : '▼' }}</text> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
|
|||
<!-- 客户详细信息 --> |
|||
<view class="customer-details"> |
|||
<view class="detail-row"> |
|||
<text class="info-label">渠道来源:</text> |
|||
<text class="info-value">{{ $util.safeGet(clientInfo, 'customerResource.source_channel_name', '未知渠道') }}</text> |
|||
</view> |
|||
<view class="detail-row"> |
|||
<text class="info-label">来源类型:</text> |
|||
<text class="info-value">{{ $util.safeGet(clientInfo, 'customerResource.source_name', '未知来源') }}</text> |
|||
</view> |
|||
<view class="detail-row"> |
|||
<text class="info-label">分配顾问:</text> |
|||
<text class="info-value">{{ $util.safeGet(clientInfo, 'customerResource.consultant_name', '未知顾问') }}</text> |
|||
</view> |
|||
<view class="detail-row"> |
|||
<text class="info-label">性别:</text> |
|||
<text class="info-value">{{ $util.safeGet(clientInfo, 'customerResource.gender_name', '未知性别') }}</text> |
|||
</view> |
|||
</view> |
|||
|
|||
<!-- 操作按钮区域 --> |
|||
<view class="action-panel" v-if="actionsExpanded && actions && actions.length > 0"> |
|||
<view |
|||
class="action-btn" |
|||
v-for="action in actions" |
|||
:key="action.key" |
|||
@click="handleAction(action)" |
|||
> |
|||
<text class="action-text">{{ action.text }}</text> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
export default { |
|||
name: 'ClientInfoCard', |
|||
props: { |
|||
clientInfo: { |
|||
type: Object, |
|||
default: () => ({}) |
|||
}, |
|||
actions: { |
|||
type: Array, |
|||
default: () => ([]) |
|||
} |
|||
}, |
|||
data() { |
|||
return { |
|||
actionsExpanded: false |
|||
} |
|||
}, |
|||
methods: { |
|||
handleMakeCall() { |
|||
const phoneNumber = this.$util.safeGet(this.clientInfo, 'customerResource.phone_number', '') |
|||
this.$util.makePhoneCall(phoneNumber) |
|||
this.$emit('call', phoneNumber) |
|||
}, |
|||
|
|||
toggleActions() { |
|||
this.actionsExpanded = !this.actionsExpanded |
|||
}, |
|||
|
|||
handleAction(action) { |
|||
this.$emit('action', { action, client: this.clientInfo }) |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style lang="less" scoped> |
|||
.client-info-card { |
|||
background-color: #1a1a1a; |
|||
border-radius: 20rpx; |
|||
padding: 30rpx; |
|||
margin: 20rpx; |
|||
} |
|||
|
|||
.basic-info-section { |
|||
.section-header { |
|||
display: flex; |
|||
align-items: center; |
|||
margin-bottom: 30rpx; |
|||
|
|||
.customer-avatar { |
|||
width: 80rpx; |
|||
height: 80rpx; |
|||
border-radius: 50%; |
|||
background-color: #29d3b4; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
margin-right: 20rpx; |
|||
|
|||
text { |
|||
color: white; |
|||
font-size: 32rpx; |
|||
font-weight: bold; |
|||
} |
|||
} |
|||
|
|||
.customer-info { |
|||
flex: 1; |
|||
|
|||
.customer-name { |
|||
color: white; |
|||
font-size: 32rpx; |
|||
font-weight: bold; |
|||
margin-bottom: 8rpx; |
|||
} |
|||
|
|||
.customer-meta { |
|||
.customer-phone { |
|||
color: #999; |
|||
font-size: 24rpx; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.contact-actions { |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 15rpx; |
|||
|
|||
.contact-btn { |
|||
width: 60rpx; |
|||
height: 60rpx; |
|||
border-radius: 50%; |
|||
background-color: #29d3b4; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
|
|||
.contact-icon { |
|||
font-size: 28rpx; |
|||
} |
|||
} |
|||
|
|||
.action-toggle { |
|||
padding: 10rpx; |
|||
|
|||
.toggle-icon { |
|||
color: #29d3b4; |
|||
font-size: 24rpx; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
.customer-details { |
|||
.detail-row { |
|||
display: flex; |
|||
align-items: center; |
|||
margin-bottom: 20rpx; |
|||
|
|||
.info-label { |
|||
color: #999; |
|||
font-size: 24rpx; |
|||
width: 150rpx; |
|||
} |
|||
|
|||
.info-value { |
|||
color: white; |
|||
font-size: 24rpx; |
|||
flex: 1; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.action-panel { |
|||
margin-top: 20rpx; |
|||
padding-top: 20rpx; |
|||
border-top: 1rpx solid #333; |
|||
display: flex; |
|||
flex-wrap: wrap; |
|||
gap: 15rpx; |
|||
|
|||
.action-btn { |
|||
padding: 15rpx 25rpx; |
|||
background-color: #29d3b4; |
|||
border-radius: 25rpx; |
|||
|
|||
.action-text { |
|||
color: white; |
|||
font-size: 22rpx; |
|||
font-weight: bold; |
|||
} |
|||
} |
|||
} |
|||
</style> |
|||
@ -0,0 +1,153 @@ |
|||
// 弹窗容器样式 |
|||
.popup-container { |
|||
background: #fff; |
|||
border-radius: 15rpx; |
|||
padding: 30rpx; |
|||
margin: 20rpx; |
|||
max-height: 80vh; |
|||
overflow-y: auto; |
|||
width: 90%; |
|||
max-width: 600rpx; |
|||
} |
|||
|
|||
// 弹窗头部 |
|||
.popup-header { |
|||
display: flex; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
margin-bottom: 30rpx; |
|||
padding-bottom: 20rpx; |
|||
border-bottom: 1px solid #eee; |
|||
} |
|||
|
|||
.popup-title { |
|||
font-size: 32rpx; |
|||
font-weight: bold; |
|||
color: #333; |
|||
} |
|||
|
|||
.popup-close { |
|||
width: 60rpx; |
|||
height: 60rpx; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
font-size: 36rpx; |
|||
color: #999; |
|||
cursor: pointer; |
|||
|
|||
&:hover { |
|||
color: #666; |
|||
} |
|||
} |
|||
|
|||
// 课程编辑容器 |
|||
.course-edit-container { |
|||
.edit-section { |
|||
margin-bottom: 30rpx; |
|||
|
|||
.section-title { |
|||
font-size: 28rpx; |
|||
font-weight: bold; |
|||
color: #333; |
|||
margin-bottom: 20rpx; |
|||
} |
|||
|
|||
.empty-tip { |
|||
color: #999; |
|||
font-size: 26rpx; |
|||
text-align: center; |
|||
padding: 40rpx 0; |
|||
} |
|||
|
|||
.coach-list { |
|||
display: flex; |
|||
flex-wrap: wrap; |
|||
gap: 15rpx; |
|||
|
|||
.coach-item { |
|||
position: relative; |
|||
background: #f5f5f5; |
|||
border: 2rpx solid #f5f5f5; |
|||
border-radius: 10rpx; |
|||
padding: 20rpx 30rpx; |
|||
min-width: 120rpx; |
|||
text-align: center; |
|||
cursor: pointer; |
|||
transition: all 0.3s ease; |
|||
|
|||
&.selected { |
|||
background: #e8f5ff; |
|||
border-color: #29d3b4; |
|||
color: #29d3b4; |
|||
} |
|||
|
|||
&:hover { |
|||
background: #e8f5ff; |
|||
border-color: #29d3b4; |
|||
} |
|||
|
|||
.coach-name { |
|||
font-size: 26rpx; |
|||
color: inherit; |
|||
} |
|||
|
|||
.coach-check { |
|||
position: absolute; |
|||
top: -5rpx; |
|||
right: -5rpx; |
|||
width: 30rpx; |
|||
height: 30rpx; |
|||
background: #29d3b4; |
|||
color: #fff; |
|||
border-radius: 50%; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
font-size: 20rpx; |
|||
font-weight: bold; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
// 弹窗底部 |
|||
.popup-footer { |
|||
display: flex; |
|||
justify-content: space-between; |
|||
gap: 20rpx; |
|||
margin-top: 40rpx; |
|||
padding-top: 30rpx; |
|||
border-top: 1px solid #eee; |
|||
} |
|||
|
|||
.popup-btn { |
|||
flex: 1; |
|||
height: 80rpx; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
border-radius: 10rpx; |
|||
font-size: 28rpx; |
|||
cursor: pointer; |
|||
transition: all 0.3s ease; |
|||
|
|||
&.cancel-btn { |
|||
background: #f5f5f5; |
|||
color: #666; |
|||
|
|||
&:hover { |
|||
background: #e8e8e8; |
|||
} |
|||
} |
|||
|
|||
&.confirm-btn { |
|||
background: #29d3b4; |
|||
color: #fff; |
|||
|
|||
&:hover { |
|||
background: #25c4a8; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,159 @@ |
|||
<template> |
|||
<uni-popup ref="popup" type="center"> |
|||
<view class="popup-container"> |
|||
<view class="popup-header"> |
|||
<view class="popup-title">修改教练配置</view> |
|||
<view class="popup-close" @click="close">✕</view> |
|||
</view> |
|||
<view class="course-edit-container"> |
|||
<view class="edit-section"> |
|||
<view class="section-title">主教练(单选)</view> |
|||
<view v-if="coachList.length === 0" class="empty-tip">暂无主教练数据</view> |
|||
<view class="coach-list"> |
|||
<view v-for="coach in coachList" :key="coach.person_id" class="coach-item" |
|||
:class="{selected: selectedMainCoach === coach.person_id}" |
|||
@click="selectMainCoach(coach.person_id)"> |
|||
<view class="coach-name">{{ coach.name }}</view> |
|||
<view class="coach-check" v-if="selectedMainCoach === coach.person_id">✓</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
|
|||
<view class="edit-section"> |
|||
<view class="section-title">教务(单选)</view> |
|||
<view v-if="educationList.length === 0" class="empty-tip">暂无教务数据</view> |
|||
<view class="coach-list"> |
|||
<view v-for="education in educationList" :key="education.person_id" class="coach-item" |
|||
:class="{selected: selectedEducation === education.person_id}" |
|||
@click="selectEducation(education.person_id)"> |
|||
<view class="coach-name">{{ education.name }}</view> |
|||
<view class="coach-check" v-if="selectedEducation === education.person_id">✓</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
|
|||
<view class="edit-section"> |
|||
<view class="section-title">助教(多选)</view> |
|||
<view v-if="assistantList.length === 0" class="empty-tip">暂无助教数据</view> |
|||
<view class="coach-list"> |
|||
<view v-for="assistant in assistantList" :key="assistant.person_id" class="coach-item" |
|||
:class="{selected: selectedAssistants.includes(assistant.person_id)}" |
|||
@click="toggleAssistant(assistant.person_id)"> |
|||
<view class="coach-name">{{ assistant.name }}</view> |
|||
<view class="coach-check" v-if="selectedAssistants.includes(assistant.person_id)">✓</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
<view class="popup-footer"> |
|||
<view class="popup-btn cancel-btn" @click="close">取消</view> |
|||
<view class="popup-btn confirm-btn" @click="confirm">确认</view> |
|||
</view> |
|||
</view> |
|||
</uni-popup> |
|||
</template> |
|||
|
|||
<script> |
|||
export default { |
|||
name: 'CourseEditPopup', |
|||
components: { |
|||
uniPopup: () => import('@/components/uni-popup/uni-popup.vue') |
|||
}, |
|||
props: { |
|||
// 教练列表 |
|||
coachList: { |
|||
type: Array, |
|||
default: () => [] |
|||
}, |
|||
// 教务列表 |
|||
educationList: { |
|||
type: Array, |
|||
default: () => [] |
|||
}, |
|||
// 助教列表 |
|||
assistantList: { |
|||
type: Array, |
|||
default: () => [] |
|||
}, |
|||
// 当前课程信息 |
|||
currentCourse: { |
|||
type: Object, |
|||
default: () => null |
|||
} |
|||
}, |
|||
data() { |
|||
return { |
|||
selectedMainCoach: null, // 选中的主教练ID |
|||
selectedEducation: null, // 选中的教务ID |
|||
selectedAssistants: [], // 选中的助教ID数组 |
|||
} |
|||
}, |
|||
methods: { |
|||
// 打开弹窗 |
|||
open(courseData = {}) { |
|||
console.log('CourseEditPopup - 打开弹窗,课程数据:', courseData) |
|||
|
|||
// 初始化选中状态 |
|||
this.selectedMainCoach = courseData.main_coach_id || null |
|||
this.selectedEducation = courseData.education_id || null |
|||
this.selectedAssistants = courseData.assistant_ids ? |
|||
courseData.assistant_ids.split(',').map(Number) : [] |
|||
|
|||
console.log('CourseEditPopup - 初始化选中状态:', { |
|||
selectedMainCoach: this.selectedMainCoach, |
|||
selectedEducation: this.selectedEducation, |
|||
selectedAssistants: this.selectedAssistants, |
|||
}) |
|||
|
|||
this.$refs.popup.open() |
|||
}, |
|||
|
|||
// 关闭弹窗 |
|||
close() { |
|||
this.$refs.popup.close() |
|||
this.$emit('close') |
|||
}, |
|||
|
|||
// 选择主教练 |
|||
selectMainCoach(personId) { |
|||
this.selectedMainCoach = personId |
|||
console.log('CourseEditPopup - 选择主教练:', personId) |
|||
}, |
|||
|
|||
// 选择教务 |
|||
selectEducation(personId) { |
|||
this.selectedEducation = personId |
|||
console.log('CourseEditPopup - 选择教务:', personId) |
|||
}, |
|||
|
|||
// 切换助教选择 |
|||
toggleAssistant(personId) { |
|||
const index = this.selectedAssistants.indexOf(personId) |
|||
if (index > -1) { |
|||
this.selectedAssistants.splice(index, 1) |
|||
} else { |
|||
this.selectedAssistants.push(personId) |
|||
} |
|||
console.log('CourseEditPopup - 切换助教选择:', personId, '当前选中:', this.selectedAssistants) |
|||
}, |
|||
|
|||
// 确认修改 |
|||
confirm() { |
|||
const result = { |
|||
main_coach_id: this.selectedMainCoach, |
|||
education_id: this.selectedEducation, |
|||
assistant_ids: this.selectedAssistants.join(','), |
|||
selectedAssistants: this.selectedAssistants |
|||
} |
|||
|
|||
console.log('CourseEditPopup - 确认修改,返回数据:', result) |
|||
this.$emit('confirm', result) |
|||
this.close() |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style lang="less" scoped> |
|||
@import './course-edit-popup.less'; |
|||
</style> |
|||
@ -0,0 +1,163 @@ |
|||
<!--体测记录卡片组件--> |
|||
<template> |
|||
<view class="fitness-record-card"> |
|||
<view class="record-header"> |
|||
<view class="record-date">{{ record.test_date }}</view> |
|||
<view class="record-status">已完成</view> |
|||
</view> |
|||
|
|||
<view class="record-data"> |
|||
<view class="data-item"> |
|||
<view class="data-label">身高</view> |
|||
<view class="data-value">{{ record.height }}cm</view> |
|||
</view> |
|||
<view class="data-item"> |
|||
<view class="data-label">体重</view> |
|||
<view class="data-value">{{ record.weight }}kg</view> |
|||
</view> |
|||
</view> |
|||
|
|||
<view class="record-files" v-if="record.pdf_files && record.pdf_files.length > 0"> |
|||
<view class="files-title">体测报告</view> |
|||
<view class="file-list"> |
|||
<view |
|||
class="file-item" |
|||
v-for="pdf in record.pdf_files" |
|||
:key="pdf.id" |
|||
@click="handleFileClick(pdf)" |
|||
> |
|||
<view class="file-icon">📄</view> |
|||
<view class="file-info"> |
|||
<view class="file-name">{{ pdf.name }}</view> |
|||
<view class="file-size">{{ $util.formatFileSize(pdf.size) }}</view> |
|||
</view> |
|||
<view class="file-action"> |
|||
<text class="action-text">查看</text> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
export default { |
|||
name: 'FitnessRecordCard', |
|||
props: { |
|||
record: { |
|||
type: Object, |
|||
required: true |
|||
} |
|||
}, |
|||
methods: { |
|||
handleFileClick(file) { |
|||
this.$emit('file-click', { file, record: this.record }) |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style lang="less" scoped> |
|||
.fitness-record-card { |
|||
background-color: #1a1a1a; |
|||
border-radius: 15rpx; |
|||
padding: 25rpx; |
|||
margin-bottom: 20rpx; |
|||
border: 1rpx solid #333; |
|||
} |
|||
|
|||
.record-header { |
|||
display: flex; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
margin-bottom: 20rpx; |
|||
|
|||
.record-date { |
|||
color: white; |
|||
font-size: 28rpx; |
|||
font-weight: bold; |
|||
} |
|||
|
|||
.record-status { |
|||
color: #29d3b4; |
|||
font-size: 22rpx; |
|||
background-color: rgba(41, 211, 180, 0.2); |
|||
padding: 6rpx 15rpx; |
|||
border-radius: 15rpx; |
|||
} |
|||
} |
|||
|
|||
.record-data { |
|||
display: flex; |
|||
gap: 40rpx; |
|||
margin-bottom: 25rpx; |
|||
|
|||
.data-item { |
|||
flex: 1; |
|||
text-align: center; |
|||
|
|||
.data-label { |
|||
color: #999; |
|||
font-size: 22rpx; |
|||
margin-bottom: 8rpx; |
|||
} |
|||
|
|||
.data-value { |
|||
color: white; |
|||
font-size: 32rpx; |
|||
font-weight: bold; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.record-files { |
|||
.files-title { |
|||
color: white; |
|||
font-size: 24rpx; |
|||
margin-bottom: 15rpx; |
|||
font-weight: bold; |
|||
} |
|||
|
|||
.file-list { |
|||
.file-item { |
|||
display: flex; |
|||
align-items: center; |
|||
padding: 15rpx; |
|||
background-color: rgba(255, 255, 255, 0.05); |
|||
border-radius: 10rpx; |
|||
margin-bottom: 10rpx; |
|||
|
|||
.file-icon { |
|||
font-size: 32rpx; |
|||
margin-right: 15rpx; |
|||
} |
|||
|
|||
.file-info { |
|||
flex: 1; |
|||
|
|||
.file-name { |
|||
color: white; |
|||
font-size: 24rpx; |
|||
margin-bottom: 5rpx; |
|||
} |
|||
|
|||
.file-size { |
|||
color: #999; |
|||
font-size: 20rpx; |
|||
} |
|||
} |
|||
|
|||
.file-action { |
|||
padding: 8rpx 15rpx; |
|||
background-color: #29d3b4; |
|||
border-radius: 15rpx; |
|||
|
|||
.action-text { |
|||
color: white; |
|||
font-size: 20rpx; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
</style> |
|||
@ -0,0 +1,215 @@ |
|||
// 体测记录弹窗样式 |
|||
.popup-container { |
|||
width: 90vw; |
|||
max-width: 600rpx; |
|||
background: #fff; |
|||
border-radius: 20rpx; |
|||
overflow: hidden; |
|||
box-shadow: 0 10rpx 30rpx rgba(0, 0, 0, 0.2); |
|||
} |
|||
|
|||
.popup-header { |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: space-between; |
|||
padding: 30rpx 40rpx; |
|||
background: #29d3b4; |
|||
color: #fff; |
|||
} |
|||
|
|||
.popup-title { |
|||
font-size: 32rpx; |
|||
font-weight: bold; |
|||
} |
|||
|
|||
.popup-close { |
|||
width: 50rpx; |
|||
height: 50rpx; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
background: rgba(255, 255, 255, 0.2); |
|||
border-radius: 50%; |
|||
font-size: 28rpx; |
|||
font-weight: bold; |
|||
cursor: pointer; |
|||
transition: all 0.3s ease; |
|||
} |
|||
|
|||
.popup-close:active { |
|||
background: rgba(255, 255, 255, 0.3); |
|||
transform: scale(0.9); |
|||
} |
|||
|
|||
.popup-footer { |
|||
display: flex; |
|||
padding: 30rpx 40rpx; |
|||
background: #f8f9fa; |
|||
gap: 20rpx; |
|||
} |
|||
|
|||
.popup-btn { |
|||
flex: 1; |
|||
height: 80rpx; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
border-radius: 12rpx; |
|||
font-size: 30rpx; |
|||
font-weight: bold; |
|||
transition: all 0.3s ease; |
|||
} |
|||
|
|||
.cancel-btn { |
|||
background: #e9ecef; |
|||
color: #666; |
|||
} |
|||
|
|||
.cancel-btn:active { |
|||
background: #dee2e6; |
|||
transform: scale(0.95); |
|||
} |
|||
|
|||
.confirm-btn { |
|||
background: #29d3b4; |
|||
color: #fff; |
|||
} |
|||
|
|||
.confirm-btn:active { |
|||
background: #1ea08e; |
|||
transform: scale(0.95); |
|||
} |
|||
|
|||
// 体测记录表单样式 |
|||
.fitness-record-form { |
|||
width: 100%; |
|||
max-height: 60vh; |
|||
overflow-y: auto; |
|||
padding: 20rpx; |
|||
box-sizing: border-box; |
|||
} |
|||
|
|||
.form-section { |
|||
width: 100%; |
|||
} |
|||
|
|||
.form-item { |
|||
margin-bottom: 30rpx; |
|||
} |
|||
|
|||
.form-label { |
|||
font-size: 30rpx; |
|||
color: #333; |
|||
margin-bottom: 15rpx; |
|||
font-weight: bold; |
|||
} |
|||
|
|||
.form-input { |
|||
border: 2px solid #eee; |
|||
border-radius: 12rpx; |
|||
padding: 0 20rpx; |
|||
background: #fff; |
|||
transition: border-color 0.3s ease; |
|||
} |
|||
|
|||
.form-input:focus-within { |
|||
border-color: #29d3b4; |
|||
} |
|||
|
|||
.form-input input { |
|||
width: 100%; |
|||
height: 80rpx; |
|||
font-size: 28rpx; |
|||
color: #333; |
|||
border: none; |
|||
outline: none; |
|||
background: transparent; |
|||
} |
|||
|
|||
.file-upload-area { |
|||
width: 100%; |
|||
} |
|||
|
|||
.upload-btn { |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
background: #f8f9fa; |
|||
border: 2px dashed #ddd; |
|||
border-radius: 12rpx; |
|||
padding: 40rpx; |
|||
margin-bottom: 20rpx; |
|||
color: #666; |
|||
transition: all 0.3s ease; |
|||
} |
|||
|
|||
.upload-btn:active { |
|||
background: #e9ecef; |
|||
border-color: #29d3b4; |
|||
} |
|||
|
|||
.upload-icon { |
|||
font-size: 40rpx; |
|||
margin-right: 15rpx; |
|||
} |
|||
|
|||
.upload-text { |
|||
font-size: 28rpx; |
|||
} |
|||
|
|||
.selected-files { |
|||
width: 100%; |
|||
} |
|||
|
|||
.selected-file-item { |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: space-between; |
|||
padding: 15rpx 20rpx; |
|||
background: #f8f9fa; |
|||
border-radius: 12rpx; |
|||
margin-bottom: 10rpx; |
|||
border: 1px solid #eee; |
|||
} |
|||
|
|||
.file-info { |
|||
display: flex; |
|||
align-items: center; |
|||
flex: 1; |
|||
} |
|||
|
|||
.file-icon { |
|||
font-size: 28rpx; |
|||
margin-right: 12rpx; |
|||
color: #666; |
|||
} |
|||
|
|||
.file-name { |
|||
flex: 1; |
|||
font-size: 26rpx; |
|||
color: #333; |
|||
margin-right: 10rpx; |
|||
} |
|||
|
|||
.file-size { |
|||
font-size: 22rpx; |
|||
color: #999; |
|||
} |
|||
|
|||
.file-remove { |
|||
width: 40rpx; |
|||
height: 40rpx; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
background: #ff4757; |
|||
color: #fff; |
|||
border-radius: 50%; |
|||
font-size: 24rpx; |
|||
font-weight: bold; |
|||
margin-left: 15rpx; |
|||
} |
|||
|
|||
.file-remove:active { |
|||
background: #ff3838; |
|||
} |
|||
@ -0,0 +1,276 @@ |
|||
<template> |
|||
<uni-popup ref="popup" type="center"> |
|||
<view class="popup-container"> |
|||
<view class="popup-header"> |
|||
<view class="popup-title">{{ isEditing ? '编辑体测记录' : '新增体测记录' }}</view> |
|||
<view class="popup-close" @click="close">✕</view> |
|||
</view> |
|||
<view class="fitness-record-form"> |
|||
<view class="form-section"> |
|||
<view class="form-item"> |
|||
<view class="form-label">测试日期</view> |
|||
<view class="form-input"> |
|||
<input type="date" v-model="recordData.test_date" placeholder="请选择测试日期" /> |
|||
</view> |
|||
</view> |
|||
|
|||
<view class="form-item"> |
|||
<view class="form-label">身高(cm)</view> |
|||
<view class="form-input"> |
|||
<input type="number" v-model="recordData.height" placeholder="请输入身高" /> |
|||
</view> |
|||
</view> |
|||
|
|||
<view class="form-item"> |
|||
<view class="form-label">体重(kg)</view> |
|||
<view class="form-input"> |
|||
<input type="number" v-model="recordData.weight" placeholder="请输入体重" /> |
|||
</view> |
|||
</view> |
|||
|
|||
<view class="form-item"> |
|||
<view class="form-label">体测报告</view> |
|||
<view class="file-upload-area"> |
|||
<view class="upload-btn" @click="selectPDFFiles"> |
|||
<view class="upload-icon">📁</view> |
|||
<view class="upload-text">选择PDF文件</view> |
|||
</view> |
|||
|
|||
<!-- 已选择的PDF文件列表 --> |
|||
<view v-if="recordData.pdf_files && recordData.pdf_files.length > 0" |
|||
class="selected-files"> |
|||
<view v-for="(pdf, index) in recordData.pdf_files" :key="index" |
|||
class="selected-file-item"> |
|||
<view class="file-info" @click="previewPDF(pdf)"> |
|||
<view class="file-icon">📄</view> |
|||
<view class="file-name">{{ pdf.name }}</view> |
|||
<view class="file-size">{{ formatFileSize(pdf.size) }}</view> |
|||
</view> |
|||
<view class="file-remove" @click="removePDFFile(index)">✕</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
<view class="popup-footer"> |
|||
<view class="popup-btn cancel-btn" @click="close">取消</view> |
|||
<view class="popup-btn confirm-btn" @click="confirm">确认</view> |
|||
</view> |
|||
</view> |
|||
</uni-popup> |
|||
</template> |
|||
|
|||
<script> |
|||
export default { |
|||
name: 'FitnessRecordPopup', |
|||
props: { |
|||
resourceId: { |
|||
type: String, |
|||
default: '' |
|||
} |
|||
}, |
|||
data() { |
|||
return { |
|||
isVisible: false, |
|||
isEditing: false, |
|||
recordData: { |
|||
id: null, |
|||
test_date: '', |
|||
height: '', |
|||
weight: '', |
|||
pdf_files: [] |
|||
} |
|||
} |
|||
}, |
|||
methods: { |
|||
// 打开新增弹窗 |
|||
openAdd() { |
|||
this.isEditing = false |
|||
this.resetData() |
|||
this.recordData.test_date = this.getCurrentDate() |
|||
this.isVisible = true |
|||
this.$refs.popup.open() |
|||
}, |
|||
|
|||
// 打开编辑弹窗 |
|||
openEdit(record) { |
|||
this.isEditing = true |
|||
this.recordData = { |
|||
id: record.id, |
|||
test_date: record.test_date, |
|||
height: record.height, |
|||
weight: record.weight, |
|||
pdf_files: [...(record.pdf_files || [])] |
|||
} |
|||
this.isVisible = true |
|||
this.$refs.popup.open() |
|||
}, |
|||
|
|||
// 关闭弹窗 |
|||
close() { |
|||
this.isVisible = false |
|||
this.$refs.popup.close() |
|||
this.resetData() |
|||
this.$emit('close') |
|||
}, |
|||
|
|||
// 重置数据 |
|||
resetData() { |
|||
this.recordData = { |
|||
id: null, |
|||
test_date: '', |
|||
height: '', |
|||
weight: '', |
|||
pdf_files: [] |
|||
} |
|||
}, |
|||
|
|||
// 确认保存 |
|||
async confirm() { |
|||
try { |
|||
// 表单验证 |
|||
if (!this.recordData.test_date) { |
|||
uni.showToast({ |
|||
title: '请选择测试日期', |
|||
icon: 'none' |
|||
}) |
|||
return |
|||
} |
|||
|
|||
if (!this.recordData.height) { |
|||
uni.showToast({ |
|||
title: '请输入身高', |
|||
icon: 'none' |
|||
}) |
|||
return |
|||
} |
|||
|
|||
if (!this.recordData.weight) { |
|||
uni.showToast({ |
|||
title: '请输入体重', |
|||
icon: 'none' |
|||
}) |
|||
return |
|||
} |
|||
|
|||
uni.showLoading({ |
|||
title: '保存中...', |
|||
mask: true |
|||
}) |
|||
|
|||
const params = { |
|||
resource_id: this.resourceId, |
|||
test_date: this.recordData.test_date, |
|||
height: this.recordData.height, |
|||
weight: this.recordData.weight, |
|||
pdf_files: this.recordData.pdf_files |
|||
} |
|||
|
|||
if (this.isEditing) { |
|||
params.id = this.recordData.id |
|||
} |
|||
|
|||
console.log('保存体测记录参数:', params) |
|||
|
|||
// 触发确认事件,将数据传递给父组件 |
|||
this.$emit('confirm', { |
|||
isEditing: this.isEditing, |
|||
data: params |
|||
}) |
|||
|
|||
uni.hideLoading() |
|||
this.close() |
|||
|
|||
} catch (error) { |
|||
console.error('保存体测记录失败:', error) |
|||
uni.showToast({ |
|||
title: '保存失败,请重试', |
|||
icon: 'none' |
|||
}) |
|||
uni.hideLoading() |
|||
} |
|||
}, |
|||
|
|||
// 选择PDF文件 |
|||
selectPDFFiles() { |
|||
uni.chooseFile({ |
|||
count: 5, |
|||
type: 'file', |
|||
extension: ['pdf'], |
|||
success: (res) => { |
|||
console.log('选择的文件:', res.tempFiles) |
|||
|
|||
res.tempFiles.forEach(file => { |
|||
if (file.type === 'application/pdf') { |
|||
const pdfFile = { |
|||
id: Date.now() + Math.random(), |
|||
name: file.name, |
|||
size: file.size, |
|||
url: file.path |
|||
} |
|||
this.recordData.pdf_files.push(pdfFile) |
|||
} |
|||
}) |
|||
}, |
|||
fail: (err) => { |
|||
console.error('选择文件失败:', err) |
|||
uni.showToast({ |
|||
title: '选择文件失败', |
|||
icon: 'none' |
|||
}) |
|||
} |
|||
}) |
|||
}, |
|||
|
|||
// 移除PDF文件 |
|||
removePDFFile(index) { |
|||
this.recordData.pdf_files.splice(index, 1) |
|||
}, |
|||
|
|||
// 预览PDF文件 |
|||
previewPDF(pdf) { |
|||
console.log('预览PDF:', pdf) |
|||
|
|||
// 使用uni.openDocument预览PDF |
|||
uni.openDocument({ |
|||
filePath: pdf.url, |
|||
fileType: 'pdf', |
|||
showMenu: true, |
|||
success: (res) => { |
|||
console.log('PDF预览成功:', res) |
|||
}, |
|||
fail: (err) => { |
|||
console.error('PDF预览失败:', err) |
|||
uni.showToast({ |
|||
title: '预览失败', |
|||
icon: 'none' |
|||
}) |
|||
} |
|||
}) |
|||
}, |
|||
|
|||
// 获取当前日期 |
|||
getCurrentDate() { |
|||
const now = new Date() |
|||
const year = now.getFullYear() |
|||
const month = String(now.getMonth() + 1).padStart(2, '0') |
|||
const day = String(now.getDate()).padStart(2, '0') |
|||
return `${year}-${month}-${day}` |
|||
}, |
|||
|
|||
// 格式化文件大小 |
|||
formatFileSize(bytes) { |
|||
if (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] |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style lang="less" scoped> |
|||
@import './fitness-record-popup.less'; |
|||
</style> |
|||
@ -0,0 +1,105 @@ |
|||
/** |
|||
* 组件索引文件 |
|||
* 统一管理项目中的可复用组件 |
|||
*/ |
|||
|
|||
// 客户信息相关组件
|
|||
export { default as ClientInfoCard } from './client-info-card/client-info-card.vue' |
|||
export { default as StudentInfoCard } from './student-info-card/student-info-card.vue' |
|||
|
|||
// 通用UI组件
|
|||
export { default as TabSwitcher } from './tab-switcher/tab-switcher.vue' |
|||
|
|||
// 记录卡片组件
|
|||
export { default as FitnessRecordCard } from './fitness-record-card/fitness-record-card.vue' |
|||
export { default as CallRecordCard } from './call-record-card/call-record-card.vue' |
|||
|
|||
// 组件使用说明
|
|||
export const ComponentUsage = { |
|||
// 客户信息卡片
|
|||
ClientInfoCard: { |
|||
description: '显示客户基本信息的卡片组件', |
|||
props: { |
|||
clientInfo: 'Object - 客户信息对象' |
|||
}, |
|||
events: { |
|||
call: '拨打电话事件,参数: phoneNumber' |
|||
}, |
|||
example: ` |
|||
<ClientInfoCard |
|||
:client-info="clientInfo" |
|||
@call="handleCall" |
|||
/> |
|||
` |
|||
}, |
|||
|
|||
// 学生信息卡片
|
|||
StudentInfoCard: { |
|||
description: '显示学生信息的卡片组件,支持操作按钮', |
|||
props: { |
|||
student: 'Object - 学生信息对象', |
|||
actions: 'Array - 操作按钮配置', |
|||
showDetails: 'Boolean - 是否显示详细信息' |
|||
}, |
|||
events: { |
|||
'toggle-actions': '切换操作面板事件', |
|||
'action': '操作按钮点击事件,参数: { action, student }' |
|||
}, |
|||
example: ` |
|||
<StudentInfoCard |
|||
:student="student" |
|||
:actions="actions" |
|||
:show-details="true" |
|||
@toggle-actions="toggleActions" |
|||
@action="handleAction" |
|||
/> |
|||
` |
|||
}, |
|||
|
|||
// 标签切换组件
|
|||
TabSwitcher: { |
|||
description: '标签切换组件,支持多标签页面切换', |
|||
props: { |
|||
tabs: 'Array - 标签配置数组 [{ id, name }]', |
|||
activeTabId: 'String|Number - 当前激活标签ID' |
|||
}, |
|||
events: { |
|||
'tab-change': '标签切换事件,参数: { tabId, index, tab }' |
|||
}, |
|||
example: ` |
|||
<TabSwitcher |
|||
:tabs="tabs" |
|||
:active-tab-id="activeTabId" |
|||
@tab-change="handleTabChange" |
|||
/> |
|||
` |
|||
}, |
|||
|
|||
// 体测记录卡片
|
|||
FitnessRecordCard: { |
|||
description: '体测记录卡片组件,显示体测数据和报告', |
|||
props: { |
|||
record: 'Object - 体测记录对象' |
|||
}, |
|||
events: { |
|||
'file-click': '文件点击事件,参数: { file, record }' |
|||
}, |
|||
example: ` |
|||
<FitnessRecordCard |
|||
:record="record" |
|||
@file-click="handleFileClick" |
|||
/> |
|||
` |
|||
}, |
|||
|
|||
// 通话记录卡片
|
|||
CallRecordCard: { |
|||
description: '通话记录卡片组件,显示通话详情', |
|||
props: { |
|||
record: 'Object - 通话记录对象' |
|||
}, |
|||
example: ` |
|||
<CallRecordCard :record="record" /> |
|||
` |
|||
} |
|||
} |
|||
@ -0,0 +1,228 @@ |
|||
// 学生信息编辑弹窗样式 |
|||
.student-edit-popup { |
|||
width: 90vw; |
|||
max-width: 600rpx; |
|||
max-height: 80vh; |
|||
background: #fff; |
|||
border-radius: 20rpx; |
|||
overflow: hidden; |
|||
|
|||
.popup-header { |
|||
display: flex; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
padding: 30rpx 40rpx; |
|||
background: #f8f9fa; |
|||
border-bottom: 1rpx solid #eee; |
|||
|
|||
.popup-title { |
|||
font-size: 32rpx; |
|||
font-weight: 600; |
|||
color: #333; |
|||
} |
|||
|
|||
.popup-close { |
|||
width: 60rpx; |
|||
height: 60rpx; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
font-size: 36rpx; |
|||
color: #999; |
|||
cursor: pointer; |
|||
|
|||
&:hover { |
|||
color: #666; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.student-form-container { |
|||
max-height: 60vh; |
|||
padding: 0 40rpx; |
|||
|
|||
.form-section { |
|||
padding: 20rpx 0; |
|||
|
|||
.form-group { |
|||
margin-bottom: 40rpx; |
|||
|
|||
.form-group-title { |
|||
font-size: 28rpx; |
|||
font-weight: 600; |
|||
color: #333; |
|||
margin-bottom: 20rpx; |
|||
padding-bottom: 10rpx; |
|||
border-bottom: 2rpx solid #29d3b4; |
|||
} |
|||
|
|||
.form-item { |
|||
display: flex; |
|||
align-items: center; |
|||
margin-bottom: 30rpx; |
|||
|
|||
.form-label { |
|||
width: 160rpx; |
|||
font-size: 28rpx; |
|||
color: #666; |
|||
flex-shrink: 0; |
|||
|
|||
&.required::after { |
|||
content: '*'; |
|||
color: #ff4757; |
|||
margin-left: 4rpx; |
|||
} |
|||
} |
|||
|
|||
.form-input { |
|||
flex: 1; |
|||
|
|||
input { |
|||
width: 100%; |
|||
height: 80rpx; |
|||
padding: 0 20rpx; |
|||
border: 1rpx solid #ddd; |
|||
border-radius: 8rpx; |
|||
font-size: 28rpx; |
|||
background: #fff; |
|||
|
|||
&:focus { |
|||
border-color: #29d3b4; |
|||
outline: none; |
|||
} |
|||
|
|||
&::placeholder { |
|||
color: #ccc; |
|||
} |
|||
} |
|||
|
|||
.picker-display { |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: space-between; |
|||
height: 80rpx; |
|||
padding: 0 20rpx; |
|||
border: 1rpx solid #ddd; |
|||
border-radius: 8rpx; |
|||
font-size: 28rpx; |
|||
background: #fff; |
|||
color: #333; |
|||
|
|||
.picker-arrow { |
|||
font-size: 32rpx; |
|||
color: #999; |
|||
transform: rotate(90deg); |
|||
} |
|||
} |
|||
} |
|||
|
|||
.form-textarea { |
|||
flex: 1; |
|||
|
|||
textarea { |
|||
width: 100%; |
|||
min-height: 120rpx; |
|||
padding: 20rpx; |
|||
border: 1rpx solid #ddd; |
|||
border-radius: 8rpx; |
|||
font-size: 28rpx; |
|||
background: #fff; |
|||
resize: none; |
|||
|
|||
&:focus { |
|||
border-color: #29d3b4; |
|||
outline: none; |
|||
} |
|||
|
|||
&::placeholder { |
|||
color: #ccc; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
.popup-footer { |
|||
display: flex; |
|||
padding: 30rpx 40rpx; |
|||
background: #f8f9fa; |
|||
border-top: 1rpx solid #eee; |
|||
gap: 20rpx; |
|||
|
|||
.popup-btn { |
|||
flex: 1; |
|||
height: 80rpx; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
border-radius: 8rpx; |
|||
font-size: 28rpx; |
|||
cursor: pointer; |
|||
transition: all 0.3s ease; |
|||
|
|||
&.cancel-btn { |
|||
background: #f5f5f5; |
|||
color: #666; |
|||
border: 1rpx solid #ddd; |
|||
|
|||
&:hover { |
|||
background: #e9e9e9; |
|||
} |
|||
} |
|||
|
|||
&.confirm-btn { |
|||
background: #29d3b4; |
|||
color: #fff; |
|||
border: 1rpx solid #29d3b4; |
|||
|
|||
&:hover { |
|||
background: #26c4a6; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
// 响应式适配 |
|||
@media (max-width: 750rpx) { |
|||
.student-edit-popup { |
|||
width: 95vw; |
|||
|
|||
.popup-header { |
|||
padding: 25rpx 30rpx; |
|||
|
|||
.popup-title { |
|||
font-size: 30rpx; |
|||
} |
|||
} |
|||
|
|||
.student-form-container { |
|||
padding: 0 30rpx; |
|||
|
|||
.form-section { |
|||
.form-group { |
|||
.form-item { |
|||
flex-direction: column; |
|||
align-items: flex-start; |
|||
|
|||
.form-label { |
|||
width: 100%; |
|||
margin-bottom: 10rpx; |
|||
} |
|||
|
|||
.form-input, |
|||
.form-textarea { |
|||
width: 100%; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
.popup-footer { |
|||
padding: 25rpx 30rpx; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,324 @@ |
|||
<!--学生信息编辑弹窗组件--> |
|||
<template> |
|||
<uni-popup ref="popup" type="center"> |
|||
<view class="popup-container student-edit-popup"> |
|||
<view class="popup-header"> |
|||
<view class="popup-title">{{ isEditing ? '编辑学生信息' : '添加学生信息' }}</view> |
|||
<view class="popup-close" @click="close">✕</view> |
|||
</view> |
|||
<scroll-view class="student-form-container" scroll-y="true"> |
|||
<view class="form-section"> |
|||
<!-- 基本信息 --> |
|||
<view class="form-group"> |
|||
<view class="form-group-title">基本信息</view> |
|||
|
|||
<view class="form-item"> |
|||
<view class="form-label required">姓名</view> |
|||
<view class="form-input"> |
|||
<input type="text" v-model="studentData.name" placeholder="请输入学生姓名" maxlength="20" /> |
|||
</view> |
|||
</view> |
|||
|
|||
<view class="form-item"> |
|||
<view class="form-label required">性别</view> |
|||
<view class="form-input"> |
|||
<picker :range="genderOptions" range-key="label" @change="onGenderChange"> |
|||
<view class="picker-display"> |
|||
{{ $util.formatGender(studentData.gender) || '请选择性别' }} |
|||
<text class="picker-arrow">›</text> |
|||
</view> |
|||
</picker> |
|||
</view> |
|||
</view> |
|||
|
|||
<view class="form-item"> |
|||
<view class="form-label required">出生日期</view> |
|||
<view class="form-input"> |
|||
<picker mode="date" :value="studentData.birthday" @change="onBirthdayChange"> |
|||
<view class="picker-display"> |
|||
{{ studentData.birthday || '请选择出生日期' }} |
|||
<text class="picker-arrow">›</text> |
|||
</view> |
|||
</picker> |
|||
</view> |
|||
</view> |
|||
|
|||
<view class="form-item"> |
|||
<view class="form-label">联系电话</view> |
|||
<view class="form-input"> |
|||
<input type="number" v-model="studentData.contact_phone" placeholder="请输入联系电话" maxlength="20" /> |
|||
</view> |
|||
</view> |
|||
|
|||
<view class="form-item"> |
|||
<view class="form-label">紧急联系人</view> |
|||
<view class="form-input"> |
|||
<input type="text" v-model="studentData.emergency_contact" placeholder="请输入紧急联系人" maxlength="255" /> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
|
|||
<!-- 学员信息 --> |
|||
<view class="form-group"> |
|||
<view class="form-group-title">学员信息</view> |
|||
|
|||
<view class="form-item"> |
|||
<view class="form-label">会员标签</view> |
|||
<view class="form-input"> |
|||
<input type="text" v-model="studentData.member_label" placeholder="请输入会员标签" maxlength="255" /> |
|||
</view> |
|||
</view> |
|||
|
|||
<view class="form-item"> |
|||
<view class="form-label">体验课次数</view> |
|||
<view class="form-input"> |
|||
<input type="number" v-model="studentData.trial_class_count" placeholder="体验课次数" /> |
|||
</view> |
|||
</view> |
|||
|
|||
<view class="form-item"> |
|||
<view class="form-label">学员状态</view> |
|||
<view class="form-input"> |
|||
<picker :range="statusOptions" range-key="label" @change="onStatusChange"> |
|||
<view class="picker-display"> |
|||
{{ studentData.status === 1 ? '有效' : '无效' }} |
|||
<text class="picker-arrow">›</text> |
|||
</view> |
|||
</picker> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
|
|||
<!-- 其他信息 --> |
|||
<view class="form-group"> |
|||
<view class="form-group-title">其他信息</view> |
|||
|
|||
<view class="form-item"> |
|||
<view class="form-label">备注</view> |
|||
<view class="form-textarea"> |
|||
<textarea v-model="studentData.note" placeholder="请输入备注信息" maxlength="200"></textarea> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</scroll-view> |
|||
<view class="popup-footer"> |
|||
<view class="popup-btn cancel-btn" @click="close">取消</view> |
|||
<view class="popup-btn confirm-btn" @click="confirm">保存</view> |
|||
</view> |
|||
</view> |
|||
</uni-popup> |
|||
</template> |
|||
|
|||
<script> |
|||
export default { |
|||
name: 'StudentEditPopup', |
|||
props: { |
|||
// 客户资源ID,用于关联学生 |
|||
resourceId: { |
|||
type: [String, Number], |
|||
default: 0 |
|||
} |
|||
}, |
|||
data() { |
|||
return { |
|||
isEditing: false, // 是否为编辑模式 |
|||
|
|||
// 学生数据 |
|||
studentData: { |
|||
id: null, |
|||
name: '', |
|||
gender: 0, // 性别: 0未知, 1男, 2女 |
|||
age: 0.00, // 年龄,支持小数表示,例如3.11表示3岁11个月 |
|||
birthday: null, |
|||
user_id: 0, // 资源ID |
|||
campus_id: null, |
|||
class_id: null, |
|||
note: '', // 备注信息 |
|||
status: 1, // 学员状态: 0无效, 1有效 |
|||
emergency_contact: '', // 紧急联系人 |
|||
contact_phone: '', // 联系人电话 |
|||
member_label: '', // 会员标签 |
|||
consultant_id: null, |
|||
coach_id: null, |
|||
trial_class_count: 2, // 体验课次数|默认2(新增学员赠送) |
|||
actionsExpanded: false // 操作面板展开状态 |
|||
}, |
|||
|
|||
// 选项数据 |
|||
genderOptions: [ |
|||
{ label: '未知', value: 0 }, |
|||
{ label: '男', value: 1 }, |
|||
{ label: '女', value: 2 } |
|||
], |
|||
|
|||
statusOptions: [ |
|||
{ label: '无效', value: 0 }, |
|||
{ label: '有效', value: 1 } |
|||
] |
|||
} |
|||
}, |
|||
methods: { |
|||
// 打开弹窗 - 新增模式 |
|||
openAdd() { |
|||
this.isEditing = false |
|||
this.resetStudentData() |
|||
this.studentData.user_id = this.resourceId |
|||
this.$refs.popup.open() |
|||
}, |
|||
|
|||
// 打开弹窗 - 编辑模式 |
|||
openEdit(student) { |
|||
this.isEditing = true |
|||
this.studentData = { |
|||
id: student.id, |
|||
name: student.name, |
|||
gender: student.gender, |
|||
age: student.age, |
|||
birthday: student.birthday, |
|||
user_id: student.user_id, |
|||
campus_id: student.campus_id, |
|||
class_id: student.class_id, |
|||
note: student.note, |
|||
status: student.status, |
|||
emergency_contact: student.emergency_contact, |
|||
contact_phone: student.contact_phone, |
|||
member_label: student.member_label, |
|||
consultant_id: student.consultant_id, |
|||
coach_id: student.coach_id, |
|||
trial_class_count: student.trial_class_count, |
|||
actionsExpanded: student.actionsExpanded || false |
|||
} |
|||
this.$refs.popup.open() |
|||
}, |
|||
|
|||
// 关闭弹窗 |
|||
close() { |
|||
this.$refs.popup.close() |
|||
this.resetStudentData() |
|||
this.$emit('close') |
|||
}, |
|||
|
|||
// 重置学生数据 |
|||
resetStudentData() { |
|||
this.studentData = { |
|||
id: null, |
|||
name: '', |
|||
gender: 0, |
|||
age: 0.00, |
|||
birthday: null, |
|||
user_id: this.resourceId || 0, |
|||
campus_id: null, |
|||
class_id: null, |
|||
note: '', |
|||
status: 1, |
|||
emergency_contact: '', |
|||
contact_phone: '', |
|||
member_label: '', |
|||
consultant_id: null, |
|||
coach_id: null, |
|||
trial_class_count: 2, |
|||
actionsExpanded: false |
|||
} |
|||
}, |
|||
|
|||
// 性别选择器变化事件 |
|||
onGenderChange(e) { |
|||
const selectedGender = this.genderOptions[e.detail.value] |
|||
this.studentData.gender = selectedGender.value |
|||
}, |
|||
|
|||
// 出生日期选择器变化事件 |
|||
onBirthdayChange(e) { |
|||
this.studentData.birthday = e.detail.value |
|||
// 计算年龄(精确到月) |
|||
if (this.studentData.birthday) { |
|||
const today = new Date() |
|||
const birthday = new Date(this.studentData.birthday) |
|||
|
|||
let years = today.getFullYear() - birthday.getFullYear() |
|||
let months = today.getMonth() - birthday.getMonth() |
|||
|
|||
if (today.getDate() < birthday.getDate()) { |
|||
months-- |
|||
} |
|||
|
|||
if (months < 0) { |
|||
years-- |
|||
months += 12 |
|||
} |
|||
|
|||
// 转换为小数表示:例如3岁11个月 = 3.11 |
|||
const ageDecimal = years + (months / 100) |
|||
this.studentData.age = ageDecimal > 0 ? parseFloat(ageDecimal.toFixed(2)) : 0 |
|||
} |
|||
}, |
|||
|
|||
// 学员状态选择器变化事件 |
|||
onStatusChange(e) { |
|||
this.studentData.status = this.statusOptions[e.detail.value].value |
|||
}, |
|||
|
|||
// 确认保存 |
|||
confirm() { |
|||
// 表单验证 |
|||
if (!this.studentData.name.trim()) { |
|||
uni.showToast({ |
|||
title: '请输入学生姓名', |
|||
icon: 'none' |
|||
}) |
|||
return |
|||
} |
|||
|
|||
if (this.studentData.gender === 0) { |
|||
uni.showToast({ |
|||
title: '请选择性别', |
|||
icon: 'none' |
|||
}) |
|||
return |
|||
} |
|||
|
|||
if (!this.studentData.birthday) { |
|||
uni.showToast({ |
|||
title: '请选择出生日期', |
|||
icon: 'none' |
|||
}) |
|||
return |
|||
} |
|||
|
|||
// 构建保存参数 |
|||
const params = { |
|||
name: this.studentData.name, |
|||
gender: this.studentData.gender, |
|||
age: this.studentData.age, |
|||
birthday: this.studentData.birthday, |
|||
user_id: this.resourceId, // 关联到当前客户资源ID |
|||
campus_id: this.studentData.campus_id, |
|||
class_id: this.studentData.class_id, |
|||
note: this.studentData.note, |
|||
status: this.studentData.status, |
|||
emergency_contact: this.studentData.emergency_contact, |
|||
contact_phone: this.studentData.contact_phone, |
|||
member_label: this.studentData.member_label, |
|||
consultant_id: this.studentData.consultant_id, |
|||
coach_id: this.studentData.coach_id, |
|||
trial_class_count: this.studentData.trial_class_count |
|||
} |
|||
|
|||
if (this.isEditing) { |
|||
params.id = this.studentData.id |
|||
} |
|||
|
|||
// 触发确认事件,将数据传递给父组件处理 |
|||
this.$emit('confirm', { |
|||
isEditing: this.isEditing, |
|||
studentData: params |
|||
}) |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style lang="less" scoped> |
|||
@import './student-edit-popup.less'; |
|||
</style> |
|||
@ -0,0 +1,229 @@ |
|||
<!--学生信息卡片组件--> |
|||
<template> |
|||
<view class="student-info-card"> |
|||
<!-- 学生基本信息 --> |
|||
<view class="student-header"> |
|||
<view class="student-avatar"> |
|||
<text>{{ student.name ? student.name.charAt(0) : '学' }}</text> |
|||
</view> |
|||
<view class="student-details"> |
|||
<view class="student-name">{{ student.name || '未知学生' }}</view> |
|||
<view class="student-meta"> |
|||
<text class="student-age">{{ formatAge(student.age) }}</text> |
|||
<text class="student-gender">{{ formatGender(student.gender) }}</text> |
|||
</view> |
|||
<view class="student-label" v-if="student.member_label">{{ student.member_label }}</view> |
|||
</view> |
|||
<view class="action-toggle" @click="toggleActions"> |
|||
<text class="toggle-icon">{{ student.actionsExpanded ? '▲' : '▼' }}</text> |
|||
</view> |
|||
</view> |
|||
|
|||
<!-- 学生详细信息 --> |
|||
<view class="student-info" v-if="showDetails"> |
|||
<view class="info-row"> |
|||
<text class="info-label">生日:</text> |
|||
<text class="info-value">{{ student.birthday || '未知' }}</text> |
|||
</view> |
|||
<view class="info-row" v-if="student.emergency_contact"> |
|||
<text class="info-label">紧急联系人:</text> |
|||
<text class="info-value">{{ student.emergency_contact }}</text> |
|||
</view> |
|||
<view class="info-row" v-if="student.contact_phone"> |
|||
<text class="info-label">联系电话:</text> |
|||
<text class="info-value">{{ student.contact_phone }}</text> |
|||
</view> |
|||
<view class="info-row" v-if="student.note"> |
|||
<text class="info-label">备注:</text> |
|||
<text class="info-value">{{ student.note }}</text> |
|||
</view> |
|||
</view> |
|||
|
|||
<!-- 操作按钮区域 --> |
|||
<view class="action-panel"> |
|||
<view |
|||
class="action-btn" |
|||
v-for="action in actions" |
|||
:key="action.key" |
|||
@click="handleAction(action)" |
|||
> |
|||
<text class="action-text">{{ action.text }}</text> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
export default { |
|||
name: 'StudentInfoCard', |
|||
props: { |
|||
student: { |
|||
type: Object, |
|||
default: () => ({}) |
|||
}, |
|||
actions: { |
|||
type: Array, |
|||
default: () => ([ |
|||
{ key: 'edit', text: '编辑学生' }, |
|||
{ key: 'order', text: '查看订单' }, |
|||
{ key: 'course', text: '课程安排' }, |
|||
{ key: 'fitness', text: '体测记录' } |
|||
]) |
|||
}, |
|||
showDetails: { |
|||
type: Boolean, |
|||
default: true |
|||
} |
|||
}, |
|||
methods: { |
|||
toggleActions() { |
|||
this.$emit('toggle-actions', this.student) |
|||
}, |
|||
|
|||
handleAction(action) { |
|||
this.$emit('action', { action, student: this.student }) |
|||
}, |
|||
|
|||
// 格式化年龄显示 |
|||
formatAge(age) { |
|||
if (!age) return '未知年龄' |
|||
const years = Math.floor(age) |
|||
const months = Math.round((age - years) * 12) |
|||
if (months === 0) { |
|||
return `${years}岁` |
|||
} |
|||
return `${years}岁${months}个月` |
|||
}, |
|||
|
|||
// 格式化性别显示 |
|||
formatGender(gender) { |
|||
switch (gender) { |
|||
case 1: return '男' |
|||
case 2: return '女' |
|||
default: return '未知' |
|||
} |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style lang="less" scoped> |
|||
.student-info-card { |
|||
background-color: #1a1a1a; |
|||
border-radius: 20rpx; |
|||
padding: 30rpx; |
|||
margin-bottom: 20rpx; |
|||
border: 2rpx solid #333; |
|||
} |
|||
|
|||
.student-header { |
|||
display: flex; |
|||
align-items: center; |
|||
|
|||
.student-avatar { |
|||
width: 60rpx; |
|||
height: 60rpx; |
|||
border-radius: 50%; |
|||
background-color: #29d3b4; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
margin-right: 20rpx; |
|||
|
|||
text { |
|||
color: white; |
|||
font-size: 24rpx; |
|||
font-weight: bold; |
|||
} |
|||
} |
|||
|
|||
.student-details { |
|||
flex: 1; |
|||
|
|||
.student-name { |
|||
color: white; |
|||
font-size: 28rpx; |
|||
font-weight: bold; |
|||
margin-bottom: 8rpx; |
|||
} |
|||
|
|||
.student-meta { |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 20rpx; |
|||
margin-bottom: 8rpx; |
|||
|
|||
.student-age, |
|||
.student-gender { |
|||
color: #999; |
|||
font-size: 22rpx; |
|||
} |
|||
} |
|||
|
|||
.student-label { |
|||
color: #29d3b4; |
|||
font-size: 20rpx; |
|||
background-color: rgba(41, 211, 180, 0.2); |
|||
padding: 4rpx 12rpx; |
|||
border-radius: 10rpx; |
|||
display: inline-block; |
|||
} |
|||
} |
|||
|
|||
.action-toggle { |
|||
padding: 10rpx; |
|||
|
|||
.toggle-icon { |
|||
color: #29d3b4; |
|||
font-size: 24rpx; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.student-info { |
|||
margin-top: 20rpx; |
|||
padding-top: 20rpx; |
|||
border-top: 1rpx solid #333; |
|||
|
|||
.info-row { |
|||
display: flex; |
|||
align-items: flex-start; |
|||
margin-bottom: 15rpx; |
|||
|
|||
.info-label { |
|||
color: #999; |
|||
font-size: 22rpx; |
|||
width: 150rpx; |
|||
flex-shrink: 0; |
|||
} |
|||
|
|||
.info-value { |
|||
color: white; |
|||
font-size: 22rpx; |
|||
flex: 1; |
|||
word-break: break-all; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.action-panel { |
|||
margin-top: 20rpx; |
|||
padding-top: 20rpx; |
|||
border-top: 1rpx solid #333; |
|||
display: flex; |
|||
flex-wrap: wrap; |
|||
gap: 15rpx; |
|||
|
|||
.action-btn { |
|||
padding: 15rpx 25rpx; |
|||
background-color: #29d3b4; |
|||
border-radius: 25rpx; |
|||
|
|||
.action-text { |
|||
color: white; |
|||
font-size: 22rpx; |
|||
font-weight: bold; |
|||
} |
|||
} |
|||
} |
|||
</style> |
|||
@ -0,0 +1,70 @@ |
|||
<!--标签切换组件--> |
|||
<template> |
|||
<view class="tab-switcher"> |
|||
<view |
|||
v-for="(tab, index) in tabs" |
|||
:key="tab.id" |
|||
:class="['tab-item', { 'selected-tab': activeTabId === tab.id }]" |
|||
@click="switchTab(tab.id, index)" |
|||
> |
|||
<text :class="['tab-text', { 'selected-text': activeTabId === tab.id }]">{{ tab.name }}</text> |
|||
</view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
export default { |
|||
name: 'TabSwitcher', |
|||
props: { |
|||
tabs: { |
|||
type: Array, |
|||
required: true, |
|||
default: () => [] |
|||
}, |
|||
activeTabId: { |
|||
type: [String, Number], |
|||
required: true |
|||
} |
|||
}, |
|||
methods: { |
|||
switchTab(tabId, index) { |
|||
if (tabId !== this.activeTabId) { |
|||
this.$emit('tab-change', { tabId, index, tab: this.tabs[index] }) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style lang="less" scoped> |
|||
.tab-switcher { |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: space-around; |
|||
padding: 20rpx 0; |
|||
background-color: white; |
|||
border-radius: 0 0 20rpx 20rpx; |
|||
} |
|||
|
|||
.tab-item { |
|||
padding: 15rpx 20rpx; |
|||
border-radius: 10rpx; |
|||
transition: all 0.3s ease; |
|||
|
|||
&.selected-tab { |
|||
background-color: rgba(41, 211, 180, 0.1); |
|||
} |
|||
} |
|||
|
|||
.tab-text { |
|||
font-size: 26rpx; |
|||
color: #666; |
|||
font-weight: normal; |
|||
transition: all 0.3s ease; |
|||
|
|||
&.selected-text { |
|||
color: #29d3b4; |
|||
font-weight: bold; |
|||
} |
|||
} |
|||
</style> |
|||
File diff suppressed because it is too large
File diff suppressed because it is too large
File diff suppressed because it is too large
@ -0,0 +1,549 @@ |
|||
<!--重构后的客户详情页面 - 使用组件化方式--> |
|||
<template> |
|||
<view class="assemble"> |
|||
<view class="main_box"> |
|||
<view style="height: 20rpx;background: #29D3B4;"></view> |
|||
|
|||
<!-- 头部信息区域 --> |
|||
<view class="count_section"> |
|||
<view class="main"> |
|||
<view class="course_box"> |
|||
<view class="course_box_top"> |
|||
<view class="course_box_top_top"> |
|||
<image class="pic" :src="$util.img('/uniapp_src/static/images/index/myk.png')"></image> |
|||
<view class="name">{{ $util.safeGet(clientInfo, 'customerResource.name', '未知客户') }}</view> |
|||
</view> |
|||
<view class="course_box_top_below"> |
|||
<view class="course_box_top_below-right"> |
|||
<!-- 操作按钮 --> |
|||
<view class="action-buttons"> |
|||
<view class="btn-item" @click="handleMakeCall"> |
|||
<image class="btn-icon" :src="$util.img('/uniapp_src/static/images/index/phone.png')"></image> |
|||
</view> |
|||
<view class="btn-item" @click="handleSendMessage" v-if="$util.safeGet(clientInfo, 'customerResource.member_id')"> |
|||
<image class="btn-icon" :src="$util.img('/uniapp_src/static/images/index/message.png')"></image> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
|
|||
<!-- 标签切换组件 --> |
|||
<TabSwitcher |
|||
:tabs="tabs" |
|||
:active-tab-id="switch_tags_type" |
|||
@tab-change="handleTabChange" |
|||
/> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
<view class="bg_box bg_top"></view> |
|||
<view class="bg_box bg_bottom"></view> |
|||
</view> |
|||
|
|||
<!-- 基本资料 --> |
|||
<view class="content-section" v-if="switch_tags_type == 1"> |
|||
<view class="integrated-info-section"> |
|||
<view class="basic-message"> |
|||
<view>客户和学生信息</view> |
|||
<view class="add-student-btn" @click="openAddStudentDialog"> |
|||
<view class="add-icon">+</view> |
|||
<view class="add-text">添加学生</view> |
|||
</view> |
|||
</view> |
|||
|
|||
<!-- 使用客户信息卡片组件 --> |
|||
<ClientInfoCard |
|||
:client-info="clientInfo" |
|||
@call="handleMakeCall" |
|||
/> |
|||
|
|||
<!-- 学生信息列表 --> |
|||
<view class="student-list" v-if="studentList.length > 0"> |
|||
<StudentInfoCard |
|||
v-for="student in studentList" |
|||
:key="student.id" |
|||
:student="student" |
|||
:actions="studentActions" |
|||
@toggle-actions="toggleStudentActions" |
|||
@action="handleStudentAction" |
|||
/> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
|
|||
<!-- 课程信息 --> |
|||
<view class="content-section" v-else-if="switch_tags_type == 2"> |
|||
<view class="course-info-section"> |
|||
<text>课程信息内容...</text> |
|||
</view> |
|||
</view> |
|||
|
|||
<!-- 通话记录 --> |
|||
<view class="content-section" v-else-if="switch_tags_type == 3"> |
|||
<view class="call-records-section"> |
|||
<CallRecordCard |
|||
v-for="record in listCallUp" |
|||
:key="record.id" |
|||
:record="record" |
|||
/> |
|||
</view> |
|||
</view> |
|||
|
|||
<!-- 体测记录 --> |
|||
<view class="content-section" v-else-if="switch_tags_type == 4"> |
|||
<view class="fitness-records-section"> |
|||
<FitnessRecordCard |
|||
v-for="record in fitnessRecords" |
|||
:key="record.id" |
|||
:record="record" |
|||
@file-click="handleFitnessFileClick" |
|||
/> |
|||
</view> |
|||
</view> |
|||
|
|||
<!-- 学习计划 --> |
|||
<view class="content-section" v-else-if="switch_tags_type == 5"> |
|||
<view class="study-plan-section"> |
|||
<text>学习计划内容...</text> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
import apiRoute from '@/api/apiRoute.js' |
|||
import marketApi from '@/api/marketApi.js' |
|||
// 导入组件 |
|||
import ClientInfoCard from '@/components/client-info-card/client-info-card.vue' |
|||
import StudentInfoCard from '@/components/student-info-card/student-info-card.vue' |
|||
import TabSwitcher from '@/components/tab-switcher/tab-switcher.vue' |
|||
import FitnessRecordCard from '@/components/fitness-record-card/fitness-record-card.vue' |
|||
import CallRecordCard from '@/components/call-record-card/call-record-card.vue' |
|||
|
|||
export default { |
|||
name: 'ClueInfoRefactored', |
|||
components: { |
|||
ClientInfoCard, |
|||
StudentInfoCard, |
|||
TabSwitcher, |
|||
FitnessRecordCard, |
|||
CallRecordCard |
|||
}, |
|||
|
|||
data() { |
|||
return { |
|||
resource_sharing_id: '', |
|||
switch_tags_type: 1, // 默认显示基本资料 |
|||
|
|||
// 标签配置 |
|||
tabs: [ |
|||
{ id: 1, name: '基本资料' }, |
|||
{ id: 2, name: '课程信息' }, |
|||
{ id: 3, name: '通话记录' }, |
|||
{ id: 4, name: '体测记录' }, |
|||
{ id: 5, name: '学习计划' } |
|||
], |
|||
|
|||
// 学生操作按钮配置 |
|||
studentActions: [ |
|||
{ key: 'edit', text: '编辑学生' }, |
|||
{ key: 'order', text: '查看订单' }, |
|||
{ key: 'course', text: '课程安排' }, |
|||
{ key: 'fitness', text: '体测记录' } |
|||
], |
|||
|
|||
// 数据 |
|||
clientInfo: {}, |
|||
userInfo: {}, |
|||
studentList: [], |
|||
listCallUp: [], |
|||
fitnessRecords: [], |
|||
followList: [], |
|||
courseInfo: [], |
|||
coachList: [], |
|||
educationList: [], |
|||
assistantList: [] |
|||
} |
|||
}, |
|||
|
|||
onLoad(option) { |
|||
console.log('页面加载参数:', option) |
|||
this.resource_sharing_id = option.resource_sharing_id || '' |
|||
this.init() |
|||
}, |
|||
|
|||
methods: { |
|||
async init() { |
|||
console.log('开始初始化数据...') |
|||
try { |
|||
await this.getInfo() |
|||
await Promise.all([ |
|||
this.getUserInfo(), |
|||
this.getListCallUp(), |
|||
this.getStudentList(), |
|||
this.getFitnessRecords() |
|||
]) |
|||
console.log('数据初始化完成') |
|||
} catch (error) { |
|||
console.error('初始化失败:', error) |
|||
} |
|||
}, |
|||
|
|||
// 获取客户详情 |
|||
async getInfo() { |
|||
if (!this.resource_sharing_id) { |
|||
uni.showToast({ title: '缺少必要参数', icon: 'none' }) |
|||
return false |
|||
} |
|||
|
|||
try { |
|||
const res = await apiRoute.xs_resourceSharingInfo({ |
|||
resource_sharing_id: this.resource_sharing_id |
|||
}) |
|||
|
|||
if (res.code === 1) { |
|||
this.clientInfo = res.data |
|||
return true |
|||
} else { |
|||
uni.showToast({ title: res.msg, icon: 'none' }) |
|||
return false |
|||
} |
|||
} catch (error) { |
|||
console.error('获取客户详情失败:', error) |
|||
return false |
|||
} |
|||
}, |
|||
|
|||
// 获取员工信息 |
|||
async getUserInfo() { |
|||
try { |
|||
const res = await apiRoute.getPersonnelInfo({}) |
|||
if (res.code === 1) { |
|||
this.userInfo = res.data |
|||
return true |
|||
} |
|||
return false |
|||
} catch (error) { |
|||
console.error('获取员工信息失败:', error) |
|||
return false |
|||
} |
|||
}, |
|||
|
|||
// 获取通话记录 |
|||
async getListCallUp() { |
|||
if (!this.clientInfo.resource_id) return false |
|||
|
|||
try { |
|||
const res = await apiRoute.listCallUp({ |
|||
resource_id: this.clientInfo.resource_id |
|||
}) |
|||
|
|||
if (res.code === 1) { |
|||
this.listCallUp = res.data || [] |
|||
return true |
|||
} |
|||
return false |
|||
} catch (error) { |
|||
console.error('获取通话记录失败:', error) |
|||
return false |
|||
} |
|||
}, |
|||
|
|||
// 获取学生列表 |
|||
async getStudentList() { |
|||
try { |
|||
if (!this.clientInfo.resource_id) { |
|||
// 使用Mock数据 |
|||
this.studentList = this.getMockStudentList() |
|||
return true |
|||
} |
|||
|
|||
const res = await apiRoute.xs_getStudentList({ |
|||
parent_resource_id: this.clientInfo.resource_id |
|||
}) |
|||
|
|||
if (res.code === 1) { |
|||
this.studentList = res.data || [] |
|||
} else { |
|||
this.studentList = this.getMockStudentList() |
|||
} |
|||
return true |
|||
} catch (error) { |
|||
console.error('获取学生列表失败:', error) |
|||
this.studentList = this.getMockStudentList() |
|||
return true |
|||
} |
|||
}, |
|||
|
|||
// 获取体测记录 |
|||
async getFitnessRecords() { |
|||
try { |
|||
// 使用Mock数据 |
|||
this.fitnessRecords = this.getMockFitnessRecords() |
|||
return true |
|||
} catch (error) { |
|||
console.error('获取体测记录失败:', error) |
|||
return false |
|||
} |
|||
}, |
|||
|
|||
// Mock数据方法 |
|||
getMockStudentList() { |
|||
return [ |
|||
{ |
|||
id: 1, |
|||
name: '张小明', |
|||
gender: 1, |
|||
age: 9.05, |
|||
birthday: '2015-05-10', |
|||
emergency_contact: '张妈妈', |
|||
contact_phone: '13800138001', |
|||
member_label: '新学员', |
|||
note: '活泼好动,喜欢运动', |
|||
actionsExpanded: false |
|||
} |
|||
] |
|||
}, |
|||
|
|||
getMockFitnessRecords() { |
|||
return [ |
|||
{ |
|||
id: 1, |
|||
test_date: '2024-01-15', |
|||
height: '165', |
|||
weight: '55', |
|||
pdf_files: [ |
|||
{ |
|||
id: 1, |
|||
name: '体测报告_2024-01-15.pdf', |
|||
size: 1024000, |
|||
url: '/static/mock/fitness_report_1.pdf' |
|||
} |
|||
] |
|||
} |
|||
] |
|||
}, |
|||
|
|||
// 事件处理方法 |
|||
handleTabChange({ tabId }) { |
|||
this.switch_tags_type = tabId |
|||
}, |
|||
|
|||
handleMakeCall() { |
|||
const phoneNumber = this.$util.safeGet(this.clientInfo, 'customerResource.phone_number', '') |
|||
this.$util.makePhoneCall(phoneNumber) |
|||
}, |
|||
|
|||
handleSendMessage() { |
|||
uni.showToast({ |
|||
title: '发送消息功能待实现', |
|||
icon: 'none' |
|||
}) |
|||
}, |
|||
|
|||
toggleStudentActions(student) { |
|||
const index = this.studentList.findIndex(s => s.id === student.id) |
|||
if (index !== -1) { |
|||
this.$set(this.studentList[index], 'actionsExpanded', !student.actionsExpanded) |
|||
} |
|||
}, |
|||
|
|||
handleStudentAction({ action, student }) { |
|||
console.log('学生操作:', action, student) |
|||
|
|||
switch (action.key) { |
|||
case 'edit': |
|||
this.editStudent(student) |
|||
break |
|||
case 'order': |
|||
this.viewStudentOrders(student) |
|||
break |
|||
case 'course': |
|||
this.viewStudentCourse(student) |
|||
break |
|||
case 'fitness': |
|||
this.viewStudentFitness(student) |
|||
break |
|||
} |
|||
}, |
|||
|
|||
handleFitnessFileClick({ file, record }) { |
|||
console.log('点击体测文件:', file, record) |
|||
// 实现文件查看逻辑 |
|||
}, |
|||
|
|||
openAddStudentDialog() { |
|||
console.log('打开添加学生对话框') |
|||
// 实现添加学生逻辑 |
|||
}, |
|||
|
|||
// 学生操作相关方法 |
|||
editStudent(student) { |
|||
this.$util.navigateToPage('/pages/student/edit', { |
|||
student_id: student.id |
|||
}) |
|||
}, |
|||
|
|||
viewStudentOrders(student) { |
|||
this.$util.navigateToPage('/pages/market/clue/order_list', { |
|||
resource_id: this.clientInfo.resource_id, |
|||
student_id: student.id, |
|||
student_name: student.name |
|||
}) |
|||
}, |
|||
|
|||
viewStudentCourse(student) { |
|||
this.$util.navigateToPage('/pages/market/clue/course_arrange', { |
|||
resource_id: this.clientInfo.resource_id, |
|||
student_id: student.id, |
|||
student_name: student.name |
|||
}) |
|||
}, |
|||
|
|||
viewStudentFitness(student) { |
|||
this.$util.navigateToPage('/pages/fitness/records', { |
|||
student_id: student.id |
|||
}) |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style lang="less" scoped> |
|||
.assemble { |
|||
width: 100%; |
|||
height: 100vh; |
|||
overflow: auto; |
|||
background-color: #292929; |
|||
} |
|||
|
|||
.main_box { |
|||
background: #292929; |
|||
min-height: 20vh; |
|||
} |
|||
|
|||
.action-buttons { |
|||
display: flex; |
|||
align-items: center; |
|||
|
|||
.btn-item { |
|||
width: 60rpx; |
|||
height: 60rpx; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
margin-left: 20rpx; |
|||
|
|||
.btn-icon { |
|||
width: 40rpx; |
|||
height: 40rpx; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.count_section { |
|||
width: 100%; |
|||
position: relative; |
|||
|
|||
.main { |
|||
width: 100%; |
|||
position: absolute; |
|||
z-index: 2; |
|||
padding: 0rpx 24rpx; |
|||
display: flex; |
|||
justify-content: center; |
|||
|
|||
.course_box { |
|||
padding: 26rpx 22rpx 0 22rpx; |
|||
width: 95%; |
|||
height: 250rpx; |
|||
border-radius: 20rpx; |
|||
background-color: #fff; |
|||
} |
|||
} |
|||
|
|||
.bg_top { |
|||
height: 180rpx; |
|||
background-color: #29D3B4; |
|||
} |
|||
|
|||
.bg_bottom { |
|||
height: 80rpx; |
|||
background-color: #292929; |
|||
} |
|||
} |
|||
|
|||
.course_box_top { |
|||
display: flex; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
|
|||
.course_box_top_top { |
|||
display: flex; |
|||
align-items: center; |
|||
|
|||
.pic { |
|||
width: 60rpx; |
|||
height: 60rpx; |
|||
margin-right: 20rpx; |
|||
} |
|||
|
|||
.name { |
|||
font-size: 32rpx; |
|||
font-weight: bold; |
|||
color: #333; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.content-section { |
|||
background-color: #292929; |
|||
min-height: 60vh; |
|||
padding: 20rpx; |
|||
} |
|||
|
|||
.integrated-info-section { |
|||
.basic-message { |
|||
display: flex; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
padding: 30rpx 20rpx; |
|||
color: white; |
|||
font-size: 28rpx; |
|||
font-weight: bold; |
|||
|
|||
.add-student-btn { |
|||
display: flex; |
|||
align-items: center; |
|||
background-color: #29d3b4; |
|||
padding: 15rpx 25rpx; |
|||
border-radius: 25rpx; |
|||
|
|||
.add-icon { |
|||
color: white; |
|||
font-size: 24rpx; |
|||
margin-right: 10rpx; |
|||
} |
|||
|
|||
.add-text { |
|||
color: white; |
|||
font-size: 22rpx; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
.student-list, |
|||
.call-records-section, |
|||
.fitness-records-section { |
|||
padding: 20rpx 0; |
|||
} |
|||
|
|||
.course-info-section, |
|||
.study-plan-section { |
|||
padding: 40rpx 20rpx; |
|||
color: white; |
|||
text-align: center; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,280 @@ |
|||
# UniApp 权限管理配置文档 |
|||
|
|||
## 📋 概述 |
|||
|
|||
本文档详细列出了UniApp教育培训管理系统中所有页面的权限配置建议,用于实现精细化的菜单权限管理。系统支持**员工端**(市场、教练、销售)和**会员端**(学员、家长)两种登录方式。 |
|||
|
|||
## 🔐 权限等级说明 |
|||
|
|||
| 权限等级 | 说明 | 适用场景 | |
|||
|---------|------|---------| |
|||
| **公开访问** | 无需登录即可访问 | 登录页、隐私协议等 | |
|||
| **登录验证** | 需要有效Token | 大部分功能页面 | |
|||
| **角色验证** | 需要特定角色权限 | 业务功能页面 | |
|||
| **管理员权限** | 需要管理员级别权限 | 人员管理、系统配置 | |
|||
| **开发权限** | 仅开发环境可访问 | 测试页面、Demo页面 | |
|||
|
|||
## 👥 用户角色定义 |
|||
|
|||
### 员工端角色 |
|||
- **市场人员** (role_type=1): 负责客户线索管理、市场推广 |
|||
- **教练** (role_type=2): 负责课程教学、学员管理 |
|||
- **销售人员** (role_type=3): 负责客户转化、订单管理 |
|||
|
|||
### 会员端角色 |
|||
- **学员** (user_type=member): 查看课程、作业、个人信息 |
|||
- **家长** (user_type=parent): 管理孩子信息、课程、服务等 |
|||
|
|||
## 📊 页面权限配置表 |
|||
|
|||
### 🚪 登录认证模块 |
|||
|
|||
| 页面路径 | 页面名称 | 权限等级 | 市场 | 教练 | 销售 | 学员 | 家长 | 建议纳入权限管理 | |
|||
|---------|---------|---------|-----|-----|-----|-----|-----|-----------------| |
|||
| `/pages/student/login/login` | 登录页面 | 公开访问 | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | |
|||
| `/pages/student/login/forgot` | 找回密码 | 公开访问 | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | |
|||
|
|||
### 📚 学员模块 |
|||
|
|||
| 页面路径 | 页面名称 | 权限等级 | 市场 | 教练 | 销售 | 学员 | 家长 | 建议纳入权限管理 | |
|||
|---------|---------|---------|-----|-----|-----|-----|-----|-----------------| |
|||
| `/pages/student/index/index` | 学员首页 | 登录验证 | ❌ | ✅ | ❌ | ✅ | ✅ | ✅ | |
|||
| `/pages/student/index/job_list` | 作业列表 | 登录验证 | ❌ | ✅ | ❌ | ✅ | ✅ | ✅ | |
|||
| `/pages/student/index/work_details` | 作业详情 | 登录验证 | ❌ | ✅ | ❌ | ✅ | ✅ | ✅ | |
|||
| `/pages/student/index/physical_examination` | 体测数据 | 登录验证 | ❌ | ✅ | ❌ | ✅ | ✅ | ✅ | |
|||
| `/pages/student/timetable/index` | 课表主页 | 登录验证 | ❌ | ✅ | ❌ | ✅ | ✅ | ✅ | |
|||
| `/pages/student/timetable/info` | 课表详情 | 登录验证 | ❌ | ✅ | ❌ | ✅ | ✅ | ✅ | |
|||
| `/pages/student/timetable/list` | 场馆列表 | 登录验证 | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ | |
|||
| `/pages/student/my/my` | 个人中心 | 登录验证 | ❌ | ❌ | ❌ | ✅ | ✅ | ❌ | |
|||
| `/pages/student/my/my_coach` | 我的教练 | 登录验证 | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ | |
|||
| `/pages/student/my/my_members` | 我的成员 | 登录验证 | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ | |
|||
| `/pages/student/my/lesson_consumption` | 课时消耗 | 登录验证 | ❌ | ✅ | ❌ | ✅ | ✅ | ✅ | |
|||
| `/pages/student/my/personal_data` | 个人资料 | 登录验证 | ❌ | ❌ | ❌ | ✅ | ✅ | ❌ | |
|||
| `/pages/student/my/set_up` | 设置 | 登录验证 | ❌ | ❌ | ❌ | ✅ | ✅ | ❌ | |
|||
| `/pages/student/my/update_pass` | 修改密码 | 登录验证 | ❌ | ❌ | ❌ | ✅ | ✅ | ❌ | |
|||
|
|||
### 👨🏫 教练模块 |
|||
|
|||
| 页面路径 | 页面名称 | 权限等级 | 市场 | 教练 | 销售 | 学员 | 家长 | 建议纳入权限管理 | |
|||
|---------|---------|---------|-----|-----|-----|-----|-----|-----------------| |
|||
| `/pages/coach/home/index` | 教练首页 | 角色验证 | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ | |
|||
| `/pages/coach/job/add` | 发布作业 | 角色验证 | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ | |
|||
| `/pages/coach/job/list` | 全部作业 | 角色验证 | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ | |
|||
| `/pages/coach/course/list` | 课表管理 | 角色验证 | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ | |
|||
| `/pages/coach/course/info` | 课时详情 | 角色验证 | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ | |
|||
| `/pages/coach/course/info_list` | 课时详情列表 | 角色验证 | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ | |
|||
| `/pages/coach/class/list` | 班级管理 | 角色验证 | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ | |
|||
| `/pages/coach/class/info` | 班级详情 | 角色验证 | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ | |
|||
| `/pages/coach/student/student_list` | 我的学员 | 角色验证 | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ | |
|||
| `/pages/coach/student/student_detail` | 学员详情 | 角色验证 | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ | |
|||
| `/pages/coach/student/info` | 学员信息 | 角色验证 | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ | |
|||
| `/pages/coach/student/work_details` | 作业任务 | 角色验证 | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ | |
|||
| `/pages/coach/student/physical_examination` | 体测数据 | 角色验证 | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ | |
|||
| `/pages/coach/schedule/schedule_table` | 课程安排 | 角色验证 | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ | |
|||
| `/pages/coach/schedule/add_schedule` | 添加课程安排 | 角色验证 | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ | |
|||
| `/pages/coach/schedule/adjust_course` | 调整课程安排 | 角色验证 | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ | |
|||
| `/pages/coach/schedule/sign_in` | 课程点名 | 角色验证 | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ | |
|||
| `/pages/coach/schedule/schedule_detail` | 课程详情 | 角色验证 | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ | |
|||
| `/pages/coach/my/index` | 个人中心 | 角色验证 | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | |
|||
| `/pages/coach/my/info` | 个人资料 | 角色验证 | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | |
|||
| `/pages/coach/my/set_up` | 设置 | 角色验证 | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | |
|||
| `/pages/coach/my/update_pass` | 修改密码 | 角色验证 | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | |
|||
| `/pages/coach/my/arrival_statistics` | 到课统计 | 角色验证 | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ | |
|||
| `/pages/coach/my/due_soon` | 即将到期 | 角色验证 | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ | |
|||
| `/pages/coach/my/schooling_statistics` | 授课统计 | 角色验证 | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ | |
|||
| `/pages/coach/my/teaching_management` | 教研管理列表 | 角色验证 | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ | |
|||
| `/pages/coach/my/teaching_management_info` | 文章详情 | 角色验证 | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ | |
|||
| `/pages/coach/my/gotake_exam` | 考试 | 角色验证 | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ | |
|||
| `/pages/coach/my/exam_results` | 考试结果 | 角色验证 | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ | |
|||
| `/pages/coach/my/my_attendance` | 我的考勤 | 角色验证 | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ | |
|||
| `/pages/coach/my/service_list` | 服务列表 | 角色验证 | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ | |
|||
| `/pages/coach/my/service_detail` | 服务详情 | 角色验证 | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ | |
|||
|
|||
### 💼 市场模块 |
|||
|
|||
| 页面路径 | 页面名称 | 权限等级 | 市场 | 教练 | 销售 | 学员 | 家长 | 建议纳入权限管理 | |
|||
|---------|---------|---------|-----|-----|-----|-----|-----|-----------------| |
|||
| `/pages/market/home/index` | 市场首页 | 角色验证 | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ | |
|||
| `/pages/market/index/index` | 销售首页 | 角色验证 | ❌ | ❌ | ✅ | ❌ | ❌ | ✅ | |
|||
| `/pages/market/clue/index` | 线索管理 | 角色验证 | ✅ | ❌ | ✅ | ❌ | ❌ | ✅ | |
|||
| `/pages/market/clue/clue_info` | 客户详情 | 角色验证 | ✅ | ❌ | ✅ | ❌ | ❌ | ✅ | |
|||
| `/pages/market/clue/order_list` | 订单列表 | 角色验证 | ✅ | ❌ | ✅ | ❌ | ❌ | ✅ | |
|||
| `/pages/market/clue/add_clues` | 添加客户 | 角色验证 | ✅ | ❌ | ✅ | ❌ | ❌ | ✅ | |
|||
| `/pages/market/clue/edit_clues` | 编辑客户 | 角色验证 | ✅ | ❌ | ✅ | ❌ | ❌ | ✅ | |
|||
| `/pages/market/clue/edit_clues_log` | 修改记录 | 角色验证 | ✅ | ❌ | ✅ | ❌ | ❌ | ✅ | |
|||
| `/pages/market/clue/new_task` | 新增任务 | 角色验证 | ✅ | ❌ | ✅ | ❌ | ❌ | ✅ | |
|||
| `/pages/market/clue/writing_followUp` | 添加跟进 | 角色验证 | ✅ | ❌ | ✅ | ❌ | ❌ | ✅ | |
|||
| `/pages/market/clue/class_arrangement` | 课程安排 | 角色验证 | ✅ | ❌ | ✅ | ❌ | ❌ | ✅ | |
|||
| `/pages/market/clue/class_arrangement_detail` | 课程安排详情 | 角色验证 | ✅ | ❌ | ✅ | ❌ | ❌ | ✅ | |
|||
| `/pages/market/clue/clue_table` | 数据统计 | 角色验证 | ❌ | ❌ | ✅ | ❌ | ❌ | ✅ | |
|||
| `/pages/market/course/course_detail` | 课程详情 | 角色验证 | ✅ | ❌ | ✅ | ❌ | ❌ | ✅ | |
|||
| `/pages/market/data/index` | 数据中心 | 角色验证 | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ | |
|||
| `/pages/market/data/statistics` | 市场数据统计 | 角色验证 | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ | |
|||
| `/pages/market/reimbursement/list` | 报销列表 | 角色验证 | ✅ | ❌ | ✅ | ❌ | ❌ | ✅ | |
|||
| `/pages/market/reimbursement/add` | 新增报销 | 角色验证 | ✅ | ❌ | ✅ | ❌ | ❌ | ✅ | |
|||
| `/pages/market/reimbursement/detail` | 报销详情 | 角色验证 | ✅ | ❌ | ✅ | ❌ | ❌ | ✅ | |
|||
| `/pages/market/my/index` | 个人中心 | 角色验证 | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | |
|||
| `/pages/market/my/info` | 个人资料 | 角色验证 | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | |
|||
| `/pages/market/my/set_up` | 设置 | 角色验证 | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | |
|||
| `/pages/market/my/update_pass` | 修改密码 | 角色验证 | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | |
|||
| `/pages/market/my/signed_client_list` | 已签客户 | 角色验证 | ✅ | ❌ | ✅ | ❌ | ❌ | ✅ | |
|||
| `/pages/market/my/my_data` | 我的数据 | 角色验证 | ✅ | ❌ | ✅ | ❌ | ❌ | ✅ | |
|||
| `/pages/market/my/dept_data` | 部门数据 | 角色验证 | ✅ | ❌ | ✅ | ❌ | ❌ | ✅ | |
|||
| `/pages/market/my/campus_data` | 校区数据 | 角色验证 | ✅ | ❌ | ✅ | ❌ | ❌ | ✅ | |
|||
| `/pages/market/my/firm_info` | 企业信息 | 角色验证 | ✅ | ❌ | ✅ | ❌ | ❌ | ✅ | |
|||
|
|||
### 👥 家长模块 |
|||
|
|||
| 页面路径 | 页面名称 | 权限等级 | 市场 | 教练 | 销售 | 学员 | 家长 | 建议纳入权限管理 | |
|||
|---------|---------|---------|-----|-----|-----|-----|-----|-----------------| |
|||
| `/pages/parent/user-info/index` | 用户信息 | 角色验证 | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ | |
|||
| `/pages/parent/user-info/child-detail` | 孩子详情 | 角色验证 | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ | |
|||
| `/pages/parent/courses/index` | 课程管理 | 角色验证 | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ | |
|||
| `/pages/parent/courses/course-detail` | 课程详情 | 角色验证 | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ | |
|||
| `/pages/parent/materials/index` | 教学资料 | 角色验证 | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ | |
|||
| `/pages/parent/materials/material-detail` | 资料详情 | 角色验证 | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ | |
|||
| `/pages/parent/services/index` | 服务管理 | 角色验证 | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ | |
|||
| `/pages/parent/services/service-detail` | 服务详情 | 角色验证 | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ | |
|||
| `/pages/parent/orders/index` | 订单管理 | 角色验证 | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ | |
|||
| `/pages/parent/orders/order-detail` | 订单详情 | 角色验证 | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ | |
|||
| `/pages/parent/messages/index` | 消息管理 | 角色验证 | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ | |
|||
| `/pages/parent/messages/message-detail` | 消息详情 | 角色验证 | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ | |
|||
| `/pages/parent/contracts/index` | 合同管理 | 角色验证 | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ | |
|||
| `/pages/parent/contracts/contract-detail` | 合同详情 | 角色验证 | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ | |
|||
|
|||
### 🛠️ 公共模块 |
|||
|
|||
| 页面路径 | 页面名称 | 权限等级 | 市场 | 教练 | 销售 | 学员 | 家长 | 建议纳入权限管理 | |
|||
|---------|---------|---------|-----|-----|-----|-----|-----|-----------------| |
|||
| `/pages/common/privacy_agreement` | 隐私协议 | 公开访问 | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | |
|||
| `/pages/common/article_info` | 文章详情 | 登录验证 | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | |
|||
| `/pages/common/feedback` | 意见反馈 | 登录验证 | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | |
|||
| `/pages/common/my_message` | 我的消息 | 登录验证 | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | |
|||
| `/pages/common/im_chat_info` | 聊天详情 | 登录验证 | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | |
|||
| `/pages/common/sys_msg_list` | 系统消息 | 登录验证 | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | |
|||
| `/pages/common/contract_list` | 订单列表 | 登录验证 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | |
|||
| `/pages/common/my_attendance` | 我的考勤 | 登录验证 | ✅ | ✅ | ✅ | ❌ | ❌ | ✅ | |
|||
| `/pages/common/personnel/add_personnel` | 新员工信息填写 | 管理员权限 | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | |
|||
| `/pages/common/contract/my_contract` | 我的合同 | 登录验证 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | |
|||
| `/pages/common/contract/contract_detail` | 合同详情 | 登录验证 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | |
|||
| `/pages/common/contract/contract_sign` | 合同签订 | 登录验证 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | |
|||
|
|||
### 📋 教务模块 |
|||
|
|||
| 页面路径 | 页面名称 | 权限等级 | 市场 | 教练 | 销售 | 学员 | 家长 | 建议纳入权限管理 | |
|||
|---------|---------|---------|-----|-----|-----|-----|-----|-----------------| |
|||
| `/pages/academic/home/index` | 教务首页 | 管理员权限 | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | |
|||
|
|||
### 🧪 测试模块 |
|||
|
|||
| 页面路径 | 页面名称 | 权限等级 | 市场 | 教练 | 销售 | 学员 | 家长 | 建议纳入权限管理 | |
|||
|---------|---------|---------|-----|-----|-----|-----|-----|-----------------| |
|||
| `/pages/demo/mock-demo` | Mock数据演示 | 开发权限 | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | |
|||
| `/pages/demo/dict_optimization` | 字典优化 | 开发权限 | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | |
|||
|
|||
## 🎯 权限管理建议 |
|||
|
|||
### 🔥 高优先级页面(建议优先纳入权限管理) |
|||
|
|||
#### 核心业务功能 |
|||
- **线索管理模块**: 客户信息、跟进记录、订单管理 |
|||
- **课程教学模块**: 课表管理、作业管理、学员管理 |
|||
- **数据统计模块**: 各类业务数据和报表 |
|||
- **财务模块**: 报销管理、合同管理 |
|||
|
|||
#### 敏感数据页面 |
|||
- **个人信息查看**: 学员详情、教练信息、客户资料 |
|||
- **体测数据**: 学员体能测试结果 |
|||
- **考勤统计**: 教练和学员的考勤记录 |
|||
- **教研管理**: 教学资料和考试内容 |
|||
|
|||
### 🔶 中优先级页面(建议选择性纳入) |
|||
|
|||
#### 查看类功能 |
|||
- **课表查看**: 学员课程表、教练课程安排 |
|||
- **消息通知**: 系统消息、聊天功能 |
|||
- **服务管理**: 各类服务项目 |
|||
|
|||
### 🔷 低优先级页面(可不纳入权限管理) |
|||
|
|||
#### 通用功能 |
|||
- **个人设置**: 个人资料、密码修改、系统设置 |
|||
- **公共信息**: 隐私协议、文章详情、意见反馈 |
|||
|
|||
## ⚙️ 技术实现建议 |
|||
|
|||
### 1. 路由守卫实现 |
|||
|
|||
```javascript |
|||
// router/permission.js |
|||
const routePermissions = { |
|||
'/pages/market/clue/index': ['market', 'sales'], |
|||
'/pages/coach/home/index': ['coach'], |
|||
'/pages/student/index/index': ['member', 'parent'], |
|||
// ...更多路由权限配置 |
|||
} |
|||
|
|||
function checkPermission(route, userRole) { |
|||
const requiredRoles = routePermissions[route] |
|||
return !requiredRoles || requiredRoles.includes(userRole) |
|||
} |
|||
``` |
|||
|
|||
### 2. 菜单动态生成 |
|||
|
|||
```javascript |
|||
// menu/menuConfig.js |
|||
const menuConfig = { |
|||
market: [ |
|||
{ path: '/pages/market/home/index', name: '首页', icon: 'home' }, |
|||
{ path: '/pages/market/clue/index', name: '线索管理', icon: 'clue' }, |
|||
// ...市场人员菜单 |
|||
], |
|||
coach: [ |
|||
{ path: '/pages/coach/home/index', name: '首页', icon: 'home' }, |
|||
{ path: '/pages/coach/course/list', name: '课表管理', icon: 'course' }, |
|||
// ...教练菜单 |
|||
], |
|||
// ...其他角色菜单 |
|||
} |
|||
``` |
|||
|
|||
### 3. API权限验证 |
|||
|
|||
```php |
|||
// middleware/AuthMiddleware.php |
|||
public function handle($request, $next, $role = null) { |
|||
$user = $this->getUser($request); |
|||
|
|||
if ($role && !$this->hasRole($user, $role)) { |
|||
return $this->unauthorizedResponse(); |
|||
} |
|||
|
|||
return $next($request); |
|||
} |
|||
``` |
|||
|
|||
## 📝 配置建议总结 |
|||
|
|||
### ✅ 强烈建议纳入权限管理的页面(58个) |
|||
|
|||
1. **线索管理**: 11个页面(客户信息、跟进、订单等) |
|||
2. **教练功能**: 22个页面(课程、学员、作业等) |
|||
3. **家长功能**: 14个页面(孩子管理、服务等) |
|||
4. **数据统计**: 8个页面(各类业务报表) |
|||
5. **其他业务**: 3个页面(教务、人员、考勤等) |
|||
|
|||
### 🔶 可选择纳入权限管理的页面(15个) |
|||
|
|||
1. **学员查看**: 7个页面(课表、作业、体测等) |
|||
2. **公共业务**: 8个页面(合同、消息、服务等) |
|||
|
|||
### ❌ 建议不纳入权限管理的页面(25个) |
|||
|
|||
1. **登录认证**: 2个页面 |
|||
2. **个人设置**: 12个页面 |
|||
3. **公共信息**: 6个页面 |
|||
4. **开发测试**: 2个页面 |
|||
5. **基础功能**: 3个页面 |
|||
|
|||
总计98个页面中,建议将**58个核心业务页面**纳入权限管理系统,确保数据安全和角色隔离的同时,保持系统的易用性和维护性。 |
|||
Loading…
Reference in new issue