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