Browse Source

修改 bug

master
王泽彦 9 months ago
parent
commit
025bb05563
  1. 38
      uniapp/api/apiRoute.js
  2. 4
      uniapp/common/config.js
  3. 214
      uniapp/components/ChildSelector.vue
  4. 212
      uniapp/mock/index.js
  5. 76
      uniapp/pages.json
  6. 177
      uniapp/pages/parent/contracts/contract-detail.vue
  7. 296
      uniapp/pages/parent/contracts/index.vue
  8. 187
      uniapp/pages/parent/courses/course-detail.vue
  9. 330
      uniapp/pages/parent/courses/index.vue
  10. 279
      uniapp/pages/parent/materials/index.vue
  11. 181
      uniapp/pages/parent/materials/material-detail.vue
  12. 280
      uniapp/pages/parent/messages/index.vue
  13. 174
      uniapp/pages/parent/messages/message-detail.vue
  14. 305
      uniapp/pages/parent/orders/index.vue
  15. 182
      uniapp/pages/parent/orders/order-detail.vue
  16. 279
      uniapp/pages/parent/services/index.vue
  17. 159
      uniapp/pages/parent/services/service-detail.vue
  18. 308
      uniapp/pages/parent/user-info/child-detail.vue
  19. 592
      uniapp/pages/parent/user-info/index.vue
  20. 23
      uniapp/pages/student/login/login.vue
  21. 23
      uniapp/store/index.js

38
uniapp/api/apiRoute.js

@ -449,6 +449,44 @@ export default {
return await http.post('/orderTable/add', data); return await http.post('/orderTable/add', data);
}, },
//↓↓↓↓↓↓↓↓↓↓↓↓-----家长接口相关-----↓↓↓↓↓↓↓↓↓↓↓↓
// 获取家长下的孩子列表
async parent_getChildrenList(data = {}) {
return await http.get('/parent/children', data);
},
// 获取指定孩子的详细信息
async parent_getChildInfo(data = {}) {
return await http.get('/parent/child/info', data);
},
// 更新孩子信息
async parent_updateChildInfo(data = {}) {
return await http.post('/parent/child/update', data);
},
// 获取指定孩子的课程信息
async parent_getChildCourses(data = {}) {
return await http.get('/parent/child/courses', data);
},
// 获取指定孩子的订单信息
async parent_getChildOrders(data = {}) {
return await http.get('/parent/child/orders', data);
},
// 获取指定孩子的教学资料
async parent_getChildMaterials(data = {}) {
return await http.get('/parent/child/materials', data);
},
// 获取指定孩子的服务记录
async parent_getChildServices(data = {}) {
return await http.get('/parent/child/services', data);
},
// 获取指定孩子的消息记录
async parent_getChildMessages(data = {}) {
return await http.get('/parent/child/messages', data);
},
// 获取指定孩子的合同信息
async parent_getChildContracts(data = {}) {
return await http.get('/parent/child/contracts', data);
},
//↓↓↓↓↓↓↓↓↓↓↓↓-----学生接口相关-----↓↓↓↓↓↓↓↓↓↓↓↓ //↓↓↓↓↓↓↓↓↓↓↓↓-----学生接口相关-----↓↓↓↓↓↓↓↓↓↓↓↓
//学生登陆接口 //学生登陆接口
async xy_login(data = {}) { async xy_login(data = {}) {

4
uniapp/common/config.js

@ -1,7 +1,7 @@
// 环境变量配置 // 环境变量配置
const env = process.env.VUE_APP_ENV || 'development' const env = process.env.VUE_APP_ENV || 'development'
const isMockEnabled = process.env.VUE_APP_MOCK_ENABLED === 'true' const isMockEnabled = process.env.VUE_APP_MOCK_ENABLED === 'true' || true // 默认启用Mock数据
const isDebug = process.env.VUE_APP_DEBUG === 'true' const isDebug = process.env.VUE_APP_DEBUG === 'true' || true // 默认启用调试模式
// API配置 - 支持环境变量 // API配置 - 支持环境变量
const Api_url = process.env.VUE_APP_API_URL || 'http://localhost:20080/api' const Api_url = process.env.VUE_APP_API_URL || 'http://localhost:20080/api'

214
uniapp/components/ChildSelector.vue

@ -0,0 +1,214 @@
<!--孩子选择组件-->
<template>
<view class="child-selector">
<view class="parent-info">
<view class="parent-name">{{ parentInfo.name }}</view>
<view class="parent-phone">{{ parentInfo.phone_number }}</view>
</view>
<view class="children-tabs">
<view
v-for="child in childrenList"
:key="child.id"
:class="['child-tab', selectedChild && selectedChild.id === child.id ? 'active' : '']"
@click="selectChild(child)"
>
<view class="child-avatar">
<image :src="child.avatar" mode="aspectFill"></image>
</view>
<view class="child-info">
<view class="child-name">{{ child.name }}</view>
<view class="child-details">
<text class="gender">{{ child.gender === 1 ? '男' : '女' }}</text>
<text class="age">{{ Math.floor(child.age) }}</text>
</view>
<view class="child-campus">{{ child.campus_name || '未分配校区' }}</view>
<view class="child-class">{{ child.class_name || '未分配班级' }}</view>
</view>
<view class="child-status">
<view class="courses-info">
<text>{{ child.remaining_courses || 0 }}课时</text>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
import { mapState, mapMutations } from 'vuex'
import apiRoute from '@/api/apiRoute.js'
export default {
name: 'ChildSelector',
data() {
return {
parentInfo: {},
childrenList: [],
loading: false
}
},
computed: {
...mapState(['selectedChild', 'userRole'])
},
mounted() {
this.loadChildrenList()
},
methods: {
...mapMutations(['SET_SELECTED_CHILD', 'SET_CHILDREN_LIST']),
async loadChildrenList() {
this.loading = true
try {
console.log('开始加载孩子列表...')
const response = await apiRoute.parent_getChildrenList()
console.log('孩子列表响应:', response)
if (response.code === 1) {
// Mockresponse.data.data response.data.parent_info
this.childrenList = response.data.data || []
this.parentInfo = response.data.parent_info || {}
this.SET_CHILDREN_LIST(this.childrenList)
console.log('加载到的孩子列表:', this.childrenList)
console.log('家长信息:', this.parentInfo)
//
if (!this.selectedChild && this.childrenList.length > 0) {
this.selectChild(this.childrenList[0])
}
} else {
console.error('获取孩子列表失败:', response)
uni.showToast({
title: response.msg || '获取孩子列表失败',
icon: 'none'
})
}
} catch (error) {
console.error('获取孩子列表失败:', error)
uni.showToast({
title: '获取孩子列表失败',
icon: 'none'
})
} finally {
this.loading = false
}
},
selectChild(child) {
this.SET_SELECTED_CHILD(child)
this.$emit('childSelected', child)
}
}
}
</script>
<style lang="less" scoped>
.child-selector {
background: #fff;
border-radius: 20rpx;
padding: 32rpx;
margin: 24rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
}
.parent-info {
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: 24rpx;
border-bottom: 1px solid #f0f0f0;
margin-bottom: 24rpx;
.parent-name {
font-size: 32rpx;
font-weight: 600;
color: #333;
}
.parent-phone {
font-size: 24rpx;
color: #999;
}
}
.children-tabs {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.child-tab {
display: flex;
align-items: center;
padding: 24rpx;
border-radius: 16rpx;
background: #f8f9fa;
border: 2rpx solid transparent;
transition: all 0.3s;
&.active {
background: rgba(41, 211, 180, 0.1);
border-color: #29d3b4;
}
.child-avatar {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
overflow: hidden;
margin-right: 24rpx;
image {
width: 100%;
height: 100%;
}
}
.child-info {
flex: 1;
.child-name {
font-size: 28rpx;
font-weight: 600;
color: #333;
margin-bottom: 8rpx;
}
.child-details {
display: flex;
gap: 16rpx;
margin-bottom: 8rpx;
.gender, .age {
font-size: 24rpx;
color: #666;
background: #e9ecef;
padding: 4rpx 12rpx;
border-radius: 12rpx;
}
}
.child-campus, .child-class {
font-size: 22rpx;
color: #999;
margin-bottom: 4rpx;
}
}
.child-status {
display: flex;
flex-direction: column;
align-items: center;
.courses-info {
background: #29d3b4;
color: #fff;
padding: 8rpx 16rpx;
border-radius: 20rpx;
font-size: 22rpx;
font-weight: 600;
}
}
}
</style>

212
uniapp/mock/index.js

@ -106,6 +106,105 @@ const mockData = {
updated_at: '2024-01-01 10:00:00' updated_at: '2024-01-01 10:00:00'
}, },
// 家长信息 (school_customer_resources)
parentInfo: {
id: 1001,
name: '张家长',
phone_number: '13800138000',
gender: 'male',
age: 35,
created_at: '2024-01-01 10:00:00',
updated_at: '2024-01-01 10:00:00'
},
// 孩子列表 (school_student)
childrenList: [
{
id: 2001,
name: '张小明',
gender: 1, // 1男 2女
age: 8.5, // 8岁5个月
birthday: '2015-06-15',
user_id: 1001, // 关联家长ID
campus_id: 1,
campus_name: '总部校区',
class_id: 101,
class_name: '少儿篮球初级班',
note: '性格活泼,运动能力强',
status: 1, // 1有效
emergency_contact: '张家长',
contact_phone: '13800138000',
member_label: '优秀学员',
consultant_id: '1',
coach_id: '1',
coach_name: '王教练',
created_at: '2024-01-01 10:00:00',
updated_at: '2024-01-01 10:00:00',
avatar: 'https://via.placeholder.com/100x100?text=张小明',
// 统计数据
total_courses: 12,
completed_courses: 8,
remaining_courses: 4,
attendance_rate: 85.5
},
{
id: 2002,
name: '张小丽',
gender: 2, // 2女
age: 6.2, // 6岁2个月
birthday: '2017-10-20',
user_id: 1001, // 关联家长ID
campus_id: 2,
campus_name: '南山校区',
class_id: 102,
class_name: '少儿舞蹈基础班',
note: '喜欢舞蹈,有艺术天赋',
status: 1, // 1有效
emergency_contact: '张家长',
contact_phone: '13800138000',
member_label: '新学员',
consultant_id: '2',
coach_id: '2',
coach_name: '李教练',
created_at: '2024-01-15 10:00:00',
updated_at: '2024-01-15 10:00:00',
avatar: 'https://via.placeholder.com/100x100?text=张小丽',
// 统计数据
total_courses: 8,
completed_courses: 3,
remaining_courses: 5,
attendance_rate: 95.0
},
{
id: 2003,
name: '张小华',
gender: 1, // 1男
age: 10.8, // 10岁8个月
birthday: '2013-04-10',
user_id: 1001, // 关联家长ID
campus_id: null, // 未分配校区
campus_name: '未分配',
class_id: null, // 未分配班级
class_name: '未分配',
note: '刚报名,待安排班级',
status: 1, // 1有效
emergency_contact: '张家长',
contact_phone: '13800138000',
member_label: '待分班',
consultant_id: '3',
coach_id: null,
coach_name: '未分配',
created_at: '2024-01-20 10:00:00',
updated_at: '2024-01-20 10:00:00',
avatar: 'https://via.placeholder.com/100x100?text=张小华',
// 统计数据
total_courses: 0,
completed_courses: 0,
remaining_courses: 0,
attendance_rate: 0
}
],
// 学员信息 (xy_memberInfo) // 学员信息 (xy_memberInfo)
memberInfo: { memberInfo: {
id: 1001, id: 1001,
@ -380,6 +479,97 @@ class MockService {
return patterns.some(pattern => endpoint.includes(pattern)) return patterns.some(pattern => endpoint.includes(pattern))
} }
// 家长端API接口
if (checkEndpoint(['/parent/children', 'parent_getChildrenList'])) {
return this.createResponse({
data: mockData.childrenList,
total: mockData.childrenList.length,
parent_info: mockData.parentInfo
}, 1, 'success')
}
// 获取指定孩子信息
if (checkEndpoint(['/parent/child/info', 'parent_getChildInfo'])) {
const childId = params.child_id || params.id
const child = mockData.childrenList.find(item => item.id == childId)
return this.createResponse(child || {}, child ? 1 : 0, child ? 'success' : '孩子信息不存在')
}
// 获取指定孩子的课程信息
if (checkEndpoint(['/parent/child/courses', 'parent_getChildCourses'])) {
const childId = params.child_id || params.id
const child = mockData.childrenList.find(item => item.id == childId)
if (!child) {
return this.createResponse([], 0, '孩子信息不存在')
}
// 模拟该孩子的课程数据
const childCourses = [
{
id: 1,
course_name: child.class_name || '未分配班级',
teacher_name: child.coach_name || '未分配',
campus_name: child.campus_name || '未分配',
schedule_time: '周六 09:00-10:30',
progress: `${child.completed_courses}/${child.total_courses}`,
status: child.class_id ? 'active' : 'inactive',
next_class: child.class_id ? '2024-01-20 09:00' : null
}
]
return this.createResponse({
data: childCourses,
total: childCourses.length,
child_info: child
}, 1, 'success')
}
// 获取指定孩子的订单信息
if (checkEndpoint(['/parent/child/orders', 'parent_getChildOrders'])) {
const childId = params.child_id || params.id
const child = mockData.childrenList.find(item => item.id == childId)
if (!child) {
return this.createResponse([], 0, '孩子信息不存在')
}
// 模拟该孩子的订单数据
const childOrders = [
{
id: 1001,
order_no: 'ORD202401001',
course_name: child.class_name || '课程包',
amount: 2880.00,
status: 'paid',
status_text: '已支付',
created_at: '2024-01-01 10:00:00',
pay_time: '2024-01-01 10:05:00'
}
]
return this.createResponse({
data: childOrders,
total: childOrders.length,
child_info: child
}, 1, 'success')
}
// 其他家长端API的Mock数据处理
if (checkEndpoint(['/parent/child/materials', 'parent_getChildMaterials'])) {
return this.createResponse({ data: [], total: 0 }, 1, 'success')
}
if (checkEndpoint(['/parent/child/services', 'parent_getChildServices'])) {
return this.createResponse({ data: [], total: 0 }, 1, 'success')
}
if (checkEndpoint(['/parent/child/messages', 'parent_getChildMessages'])) {
return this.createResponse({ data: [], total: 0 }, 1, 'success')
}
if (checkEndpoint(['/parent/child/contracts', 'parent_getChildContracts'])) {
return this.createResponse({ data: [], total: 0 }, 1, 'success')
}
// 学员信息 // 学员信息
if (checkEndpoint(['/customerResourcesAuth/info', 'xy_memberInfo'])) { if (checkEndpoint(['/customerResourcesAuth/info', 'xy_memberInfo'])) {
return this.createResponse(mockData.memberInfo, 1, 'success') return this.createResponse(mockData.memberInfo, 1, 'success')
@ -498,6 +688,16 @@ class MockService {
'/xy/personCourseSchedule', // xy_personCourseSchedule相关 '/xy/personCourseSchedule', // xy_personCourseSchedule相关
'/xy/assignment', // xy_assignment相关 '/xy/assignment', // xy_assignment相关
'/xy/login', // xy_login '/xy/login', // xy_login
// 家长端专用API - URL匹配
'/parent/children', // parent_getChildrenList
'/parent/child/info', // parent_getChildInfo
'/parent/child/courses', // parent_getChildCourses
'/parent/child/orders', // parent_getChildOrders
'/parent/child/materials', // parent_getChildMaterials
'/parent/child/services', // parent_getChildServices
'/parent/child/messages', // parent_getChildMessages
'/parent/child/contracts', // parent_getChildContracts
'/parent/child/update', // parent_updateChildInfo
// 学员端专用API - 方法名匹配(用于开发调试) // 学员端专用API - 方法名匹配(用于开发调试)
'xy_memberInfo', 'xy_memberInfo',
'xy_physicalTest', 'xy_physicalTest',
@ -507,7 +707,17 @@ class MockService {
'xy_assignmentSubmitObj', 'xy_assignmentSubmitObj',
'xy_personCourseScheduleGetCalendar', 'xy_personCourseScheduleGetCalendar',
'xy_personCourseScheduleGetMyCoach', 'xy_personCourseScheduleGetMyCoach',
'xy_login' 'xy_login',
// 家长端专用API - 方法名匹配(用于开发调试)
'parent_getChildrenList',
'parent_getChildInfo',
'parent_getChildCourses',
'parent_getChildOrders',
'parent_getChildMaterials',
'parent_getChildServices',
'parent_getChildMessages',
'parent_getChildContracts',
'parent_updateChildInfo'
] ]
return mockableEndpoints.some(endpoint => url.includes(endpoint)) return mockableEndpoints.some(endpoint => url.includes(endpoint))

76
uniapp/pages.json

@ -761,10 +761,80 @@
"navigationBarBackgroundColor": "#292929", "navigationBarBackgroundColor": "#292929",
"navigationBarTextStyle": "white" "navigationBarTextStyle": "white"
} }
} },
{
"path": "pages/parent/user-info/index",
"style": {
"navigationBarTitleText": "用户信息",
"navigationStyle": "custom",
"navigationBarBackgroundColor": "#29d3b4",
"navigationBarTextStyle": "white"
}
},
{
"path": "pages/parent/user-info/child-detail",
"style": {
"navigationBarTitleText": "孩子详情",
"navigationStyle": "default",
"navigationBarBackgroundColor": "#29d3b4",
"navigationBarTextStyle": "white"
}
},
{
"path": "pages/parent/courses/index",
"style": {
"navigationBarTitleText": "课程管理",
"navigationStyle": "default",
"navigationBarBackgroundColor": "#29d3b4",
"navigationBarTextStyle": "white"
}
},
{
"path": "pages/parent/materials/index",
"style": {
"navigationBarTitleText": "教学资料",
"navigationStyle": "default",
"navigationBarBackgroundColor": "#29d3b4",
"navigationBarTextStyle": "white"
}
},
{
"path": "pages/parent/services/index",
"style": {
"navigationBarTitleText": "服务管理",
"navigationStyle": "default",
"navigationBarBackgroundColor": "#29d3b4",
"navigationBarTextStyle": "white"
}
},
{
"path": "pages/parent/orders/index",
"style": {
"navigationBarTitleText": "订单管理",
"navigationStyle": "default",
"navigationBarBackgroundColor": "#29d3b4",
"navigationBarTextStyle": "white"
}
},
{
"path": "pages/parent/messages/index",
"style": {
"navigationBarTitleText": "消息管理",
"navigationStyle": "default",
"navigationBarBackgroundColor": "#29d3b4",
"navigationBarTextStyle": "white"
}
},
{
"path": "pages/parent/contracts/index",
"style": {
"navigationBarTitleText": "合同管理",
"navigationStyle": "default",
"navigationBarBackgroundColor": "#29d3b4",
"navigationBarTextStyle": "white"
}
}
], ],
"globalStyle": { "globalStyle": {

177
uniapp/pages/parent/contracts/contract-detail.vue

@ -0,0 +1,177 @@
<!--合同详情页面-->
<template>
<view class="main_box">
<!-- 合同基本信息 -->
<view class="contract_info_card" v-if="contractInfo">
<view class="contract_header">
<view class="contract_title">{{ contractInfo.title }}</view>
<view class="contract_status" :class="contractInfo.status">
{{ contractInfo.status_text }}
</view>
</view>
<view class="contract_details">
<view class="detail_item">
<view class="detail_label">合同金额</view>
<view class="detail_value amount">¥{{ contractInfo.amount }}</view>
</view>
<view class="detail_item">
<view class="detail_label">签订日期</view>
<view class="detail_value">{{ contractInfo.sign_date }}</view>
</view>
<view class="detail_item">
<view class="detail_label">有效期</view>
<view class="detail_value">{{ contractInfo.valid_date }}</view>
</view>
</view>
</view>
<!-- 空状态 -->
<view class="empty_state" v-if="!loading && !contractInfo">
<image src="/static/icon-img/empty.png" class="empty_icon"></image>
<view class="empty_text">暂无合同信息</view>
</view>
<!-- 加载状态 -->
<view class="loading_state" v-if="loading">
<view class="loading_text">加载中...</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
contractInfo: null,
loading: false,
contractId: null,
childId: null
}
},
onLoad(options) {
this.contractId = options.contractId
this.childId = options.childId
this.loadContractInfo()
},
methods: {
async loadContractInfo() {
//
this.contractInfo = {
title: '少儿篮球培训合同',
status: 'active',
status_text: '有效',
amount: '2880.00',
sign_date: '2024-01-01',
valid_date: '2024-01-01 至 2024-12-31'
}
}
}
}
</script>
<style lang="less" scoped>
.main_box {
background: #f8f9fa;
min-height: 100vh;
padding: 20rpx;
}
.contract_info_card {
background: #fff;
border-radius: 16rpx;
padding: 32rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
.contract_header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 32rpx;
padding-bottom: 24rpx;
border-bottom: 1px solid #f0f0f0;
.contract_title {
font-size: 36rpx;
font-weight: 600;
color: #333;
}
.contract_status {
font-size: 24rpx;
padding: 8rpx 16rpx;
border-radius: 12rpx;
&.active {
background: rgba(40, 167, 69, 0.1);
color: #28a745;
}
&.expired {
background: rgba(220, 53, 69, 0.1);
color: #dc3545;
}
}
}
.contract_details {
.detail_item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx 0;
border-bottom: 1px solid #f8f9fa;
.detail_label {
font-size: 28rpx;
color: #666;
min-width: 160rpx;
}
.detail_value {
font-size: 28rpx;
color: #333;
flex: 1;
text-align: right;
&.amount {
color: #e67e22;
font-weight: 600;
}
}
}
}
}
.empty_state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 120rpx 0;
.empty_icon {
width: 160rpx;
height: 160rpx;
margin-bottom: 32rpx;
opacity: 0.3;
}
.empty_text {
font-size: 28rpx;
color: #999;
}
}
.loading_state {
display: flex;
justify-content: center;
align-items: center;
padding: 60rpx 0;
.loading_text {
font-size: 28rpx;
color: #666;
}
}
</style>

296
uniapp/pages/parent/contracts/index.vue

@ -0,0 +1,296 @@
<!--家长端合同管理页面-->
<template>
<view class="main_box">
<!-- 选中孩子信息 -->
<view class="child_info_bar" v-if="selectedChild">
<view class="child_avatar">
<image :src="selectedChild.avatar" mode="aspectFill"></image>
</view>
<view class="child_details">
<view class="child_name">{{ selectedChild.name }}</view>
<view class="child_class">{{ selectedChild.class_name || '未分配班级' }}</view>
</view>
</view>
<!-- 合同列表 -->
<view class="contracts_list">
<view class="section_title">合同列表</view>
<view class="contracts_items">
<view
v-for="contract in contractsList"
:key="contract.id"
class="contract_item"
@click="viewContractDetail(contract)"
>
<view class="contract_main">
<view class="contract_header">
<view class="contract_title">{{ contract.title }}</view>
<view class="contract_status" :class="contract.status">{{ contract.status_text }}</view>
</view>
<view class="contract_details">
<view class="detail_row">
<text class="detail_label">合同金额</text>
<text class="detail_value amount">¥{{ contract.amount }}</text>
</view>
<view class="detail_row">
<text class="detail_label">签订日期</text>
<text class="detail_value">{{ contract.sign_date }}</text>
</view>
<view class="detail_row">
<text class="detail_label">有效期</text>
<text class="detail_value">{{ contract.valid_date }}</text>
</view>
</view>
</view>
<view class="contract_arrow">
<image :src="$util.img('/uniapp_src/static/images/index/right_arrow.png')" class="arrow-icon"></image>
</view>
</view>
</view>
</view>
<!-- 空状态 -->
<view class="empty_state" v-if="!loading && contractsList.length === 0">
<image :src="$util.img('/uniapp_src/static/images/common/empty.png')" class="empty_icon"></image>
<view class="empty_text">暂无合同信息</view>
</view>
<!-- 加载状态 -->
<view class="loading_state" v-if="loading">
<view class="loading_text">加载中...</view>
</view>
</view>
</template>
<script>
import { mapState } from 'vuex'
import apiRoute from '@/api/apiRoute.js'
export default {
data() {
return {
contractsList: [],
loading: false,
childId: null
}
},
computed: {
...mapState(['selectedChild'])
},
onLoad(options) {
this.childId = options.childId
this.loadContractsList()
},
methods: {
async loadContractsList() {
if (!this.childId) {
uni.showToast({
title: '缺少孩子ID参数',
icon: 'none'
})
return
}
this.loading = true
try {
const response = await apiRoute.parent_getChildContracts({
child_id: this.childId
})
if (response.code === 1) {
this.contractsList = response.data.data || []
} else {
uni.showToast({
title: response.msg || '获取合同列表失败',
icon: 'none'
})
}
} catch (error) {
console.error('获取合同列表失败:', error)
uni.showToast({
title: '获取合同列表失败',
icon: 'none'
})
} finally {
this.loading = false
}
},
viewContractDetail(contract) {
this.$navigateTo({
url: `/pages/parent/contracts/contract-detail?contractId=${contract.id}&childId=${this.childId}`
})
}
}
}
</script>
<style lang="less" scoped>
.main_box {
background: #f8f9fa;
min-height: 100vh;
padding: 20rpx;
}
.child_info_bar {
background: #fff;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 20rpx;
display: flex;
align-items: center;
gap: 20rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
.child_avatar {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
overflow: hidden;
image {
width: 100%;
height: 100%;
}
}
.child_details {
flex: 1;
.child_name {
font-size: 28rpx;
font-weight: 600;
color: #333;
margin-bottom: 8rpx;
}
.child_class {
font-size: 24rpx;
color: #666;
}
}
}
.contracts_list {
.section_title {
font-size: 32rpx;
font-weight: 600;
color: #333;
margin-bottom: 24rpx;
padding-left: 8rpx;
}
.contracts_items {
display: flex;
flex-direction: column;
gap: 16rpx;
}
}
.contract_item {
background: #fff;
border-radius: 16rpx;
padding: 28rpx;
display: flex;
align-items: center;
gap: 20rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
.contract_main {
flex: 1;
.contract_header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16rpx;
.contract_title {
font-size: 30rpx;
font-weight: 600;
color: #333;
}
.contract_status {
font-size: 22rpx;
padding: 4rpx 12rpx;
border-radius: 12rpx;
&.active {
background: rgba(40, 167, 69, 0.1);
color: #28a745;
}
&.expired {
background: rgba(220, 53, 69, 0.1);
color: #dc3545;
}
}
}
.contract_details {
.detail_row {
display: flex;
margin-bottom: 8rpx;
.detail_label {
font-size: 24rpx;
color: #666;
min-width: 160rpx;
}
.detail_value {
font-size: 24rpx;
color: #333;
flex: 1;
&.amount {
color: #e67e22;
font-weight: 600;
}
}
}
}
}
.contract_arrow {
.arrow-icon {
width: 24rpx;
height: 24rpx;
opacity: 0.4;
}
}
}
.empty_state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 120rpx 0;
.empty_icon {
width: 160rpx;
height: 160rpx;
margin-bottom: 32rpx;
opacity: 0.3;
}
.empty_text {
font-size: 28rpx;
color: #999;
}
}
.loading_state {
display: flex;
justify-content: center;
align-items: center;
padding: 60rpx 0;
.loading_text {
font-size: 28rpx;
color: #666;
}
}
</style>

187
uniapp/pages/parent/courses/course-detail.vue

@ -0,0 +1,187 @@
<!--课程详情页面-->
<template>
<view class="main_box">
<!-- 课程基本信息 -->
<view class="course_info_card" v-if="courseInfo">
<view class="course_header">
<view class="course_name">{{ courseInfo.course_name }}</view>
<view class="course_status" :class="courseInfo.status">
{{ courseInfo.status === 'active' ? '进行中' : '已结束' }}
</view>
</view>
<view class="course_details">
<view class="detail_item">
<view class="detail_label">授课教师</view>
<view class="detail_value">{{ courseInfo.teacher_name }}</view>
</view>
<view class="detail_item">
<view class="detail_label">上课校区</view>
<view class="detail_value">{{ courseInfo.campus_name }}</view>
</view>
<view class="detail_item">
<view class="detail_label">上课时间</view>
<view class="detail_value">{{ courseInfo.schedule_time }}</view>
</view>
<view class="detail_item">
<view class="detail_label">课程进度</view>
<view class="detail_value">{{ courseInfo.progress }}</view>
</view>
<view class="detail_item" v-if="courseInfo.next_class">
<view class="detail_label">下节课时间</view>
<view class="detail_value next_class">{{ courseInfo.next_class }}</view>
</view>
</view>
</view>
<!-- 空状态 -->
<view class="empty_state" v-if="!loading && !courseInfo">
<image src="/static/icon-img/empty.png" class="empty_icon"></image>
<view class="empty_text">暂无课程信息</view>
</view>
<!-- 加载状态 -->
<view class="loading_state" v-if="loading">
<view class="loading_text">加载中...</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
courseInfo: null,
loading: false,
courseId: null,
childId: null
}
},
onLoad(options) {
this.courseId = options.courseId
this.childId = options.childId
this.loadCourseInfo()
},
methods: {
async loadCourseInfo() {
//
// 使
this.courseInfo = {
course_name: '少儿篮球训练',
teacher_name: '王教练',
campus_name: '总部校区',
schedule_time: '周六 09:00-10:30',
progress: '8/12节',
status: 'active',
next_class: '2024-01-20 09:00'
}
}
}
}
</script>
<style lang="less" scoped>
.main_box {
background: #f8f9fa;
min-height: 100vh;
padding: 20rpx;
}
.course_info_card {
background: #fff;
border-radius: 16rpx;
padding: 32rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
.course_header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 32rpx;
padding-bottom: 24rpx;
border-bottom: 1px solid #f0f0f0;
.course_name {
font-size: 36rpx;
font-weight: 600;
color: #333;
}
.course_status {
font-size: 24rpx;
padding: 8rpx 16rpx;
border-radius: 12rpx;
&.active {
background: rgba(41, 211, 180, 0.1);
color: #29d3b4;
}
&.inactive {
background: #f0f0f0;
color: #999;
}
}
}
.course_details {
.detail_item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx 0;
border-bottom: 1px solid #f8f9fa;
.detail_label {
font-size: 28rpx;
color: #666;
min-width: 160rpx;
}
.detail_value {
font-size: 28rpx;
color: #333;
flex: 1;
text-align: right;
&.next_class {
color: #29d3b4;
font-weight: 600;
}
}
}
}
}
.empty_state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 120rpx 0;
.empty_icon {
width: 160rpx;
height: 160rpx;
margin-bottom: 32rpx;
opacity: 0.3;
}
.empty_text {
font-size: 28rpx;
color: #999;
}
}
.loading_state {
display: flex;
justify-content: center;
align-items: center;
padding: 60rpx 0;
.loading_text {
font-size: 28rpx;
color: #666;
}
}
</style>

330
uniapp/pages/parent/courses/index.vue

@ -0,0 +1,330 @@
<!--家长端课程管理页面-->
<template>
<view class="main_box">
<!-- 选中孩子信息 -->
<view class="child_info_bar" v-if="selectedChild">
<view class="child_avatar">
<image :src="selectedChild.avatar" mode="aspectFill"></image>
</view>
<view class="child_details">
<view class="child_name">{{ selectedChild.name }}</view>
<view class="child_class">{{ selectedChild.class_name || '未分配班级' }}</view>
</view>
<view class="course_stats">
<view class="stat_item">
<text class="stat_number">{{ selectedChild.remaining_courses || 0 }}</text>
<text class="stat_label">剩余课时</text>
</view>
</view>
</view>
<!-- 课程列表 -->
<view class="course_list">
<view class="section_title">课程信息</view>
<view class="course_items">
<view
v-for="course in courseList"
:key="course.id"
class="course_item"
@click="viewCourseDetail(course)"
>
<view class="course_main">
<view class="course_header">
<view class="course_name">{{ course.course_name }}</view>
<view class="course_status" :class="course.status">{{ course.status === 'active' ? '进行中' : '已结束' }}</view>
</view>
<view class="course_details">
<view class="detail_row">
<text class="detail_label">授课教师</text>
<text class="detail_value">{{ course.teacher_name }}</text>
</view>
<view class="detail_row">
<text class="detail_label">上课校区</text>
<text class="detail_value">{{ course.campus_name }}</text>
</view>
<view class="detail_row">
<text class="detail_label">上课时间</text>
<text class="detail_value">{{ course.schedule_time }}</text>
</view>
<view class="detail_row">
<text class="detail_label">课程进度</text>
<text class="detail_value">{{ course.progress }}</text>
</view>
<view class="detail_row" v-if="course.next_class">
<text class="detail_label">下节课时间</text>
<text class="detail_value next_class">{{ course.next_class }}</text>
</view>
</view>
</view>
<view class="course_arrow">
<image :src="$util.img('/uniapp_src/static/images/index/right_arrow.png')" class="arrow-icon"></image>
</view>
</view>
</view>
</view>
<!-- 空状态 -->
<view class="empty_state" v-if="!loading && courseList.length === 0">
<image :src="$util.img('/uniapp_src/static/images/common/empty.png')" class="empty_icon"></image>
<view class="empty_text">暂无课程信息</view>
</view>
<!-- 加载状态 -->
<view class="loading_state" v-if="loading">
<view class="loading_text">加载中...</view>
</view>
</view>
</template>
<script>
import { mapState } from 'vuex'
import apiRoute from '@/api/apiRoute.js'
export default {
data() {
return {
courseList: [],
loading: false,
childId: null
}
},
computed: {
...mapState(['selectedChild'])
},
onLoad(options) {
this.childId = options.childId
this.loadCourseList()
},
methods: {
async loadCourseList() {
if (!this.childId) {
uni.showToast({
title: '缺少孩子ID参数',
icon: 'none'
})
return
}
this.loading = true
try {
const response = await apiRoute.parent_getChildCourses({
child_id: this.childId
})
if (response.code === 1) {
this.courseList = response.data.data || []
} else {
uni.showToast({
title: response.msg || '获取课程列表失败',
icon: 'none'
})
}
} catch (error) {
console.error('获取课程列表失败:', error)
uni.showToast({
title: '获取课程列表失败',
icon: 'none'
})
} finally {
this.loading = false
}
},
viewCourseDetail(course) {
this.$navigateTo({
url: `/pages/parent/courses/course-detail?courseId=${course.id}&childId=${this.childId}`
})
}
}
}
</script>
<style lang="less" scoped>
.main_box {
background: #f8f9fa;
min-height: 100vh;
padding: 20rpx;
}
.child_info_bar {
background: #fff;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 20rpx;
display: flex;
align-items: center;
gap: 20rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
.child_avatar {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
overflow: hidden;
image {
width: 100%;
height: 100%;
}
}
.child_details {
flex: 1;
.child_name {
font-size: 28rpx;
font-weight: 600;
color: #333;
margin-bottom: 8rpx;
}
.child_class {
font-size: 24rpx;
color: #666;
}
}
.course_stats {
.stat_item {
display: flex;
flex-direction: column;
align-items: center;
.stat_number {
font-size: 28rpx;
font-weight: 600;
color: #29d3b4;
margin-bottom: 4rpx;
}
.stat_label {
font-size: 22rpx;
color: #999;
}
}
}
}
.course_list {
.section_title {
font-size: 32rpx;
font-weight: 600;
color: #333;
margin-bottom: 24rpx;
padding-left: 8rpx;
}
.course_items {
display: flex;
flex-direction: column;
gap: 16rpx;
}
}
.course_item {
background: #fff;
border-radius: 16rpx;
padding: 28rpx;
display: flex;
align-items: center;
gap: 20rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
.course_main {
flex: 1;
.course_header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16rpx;
.course_name {
font-size: 30rpx;
font-weight: 600;
color: #333;
}
.course_status {
font-size: 22rpx;
padding: 4rpx 12rpx;
border-radius: 12rpx;
&.active {
background: rgba(41, 211, 180, 0.1);
color: #29d3b4;
}
&.inactive {
background: #f0f0f0;
color: #999;
}
}
}
.course_details {
.detail_row {
display: flex;
margin-bottom: 8rpx;
.detail_label {
font-size: 24rpx;
color: #666;
min-width: 160rpx;
}
.detail_value {
font-size: 24rpx;
color: #333;
flex: 1;
&.next_class {
color: #29d3b4;
font-weight: 600;
}
}
}
}
}
.course_arrow {
.arrow-icon {
width: 24rpx;
height: 24rpx;
opacity: 0.4;
}
}
}
.empty_state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 120rpx 0;
.empty_icon {
width: 160rpx;
height: 160rpx;
margin-bottom: 32rpx;
opacity: 0.3;
}
.empty_text {
font-size: 28rpx;
color: #999;
}
}
.loading_state {
display: flex;
justify-content: center;
align-items: center;
padding: 60rpx 0;
.loading_text {
font-size: 28rpx;
color: #666;
}
}
</style>

279
uniapp/pages/parent/materials/index.vue

@ -0,0 +1,279 @@
<!--家长端教学资料页面-->
<template>
<view class="main_box">
<!-- 选中孩子信息 -->
<view class="child_info_bar" v-if="selectedChild">
<view class="child_avatar">
<image :src="selectedChild.avatar" mode="aspectFill"></image>
</view>
<view class="child_details">
<view class="child_name">{{ selectedChild.name }}</view>
<view class="child_class">{{ selectedChild.class_name || '未分配班级' }}</view>
</view>
</view>
<!-- 教学资料列表 -->
<view class="materials_list">
<view class="section_title">教学资料</view>
<view class="materials_items">
<view
v-for="material in materialsList"
:key="material.id"
class="material_item"
@click="viewMaterialDetail(material)"
>
<view class="material_main">
<view class="material_header">
<view class="material_title">{{ material.title }}</view>
<view class="material_type">{{ material.type }}</view>
</view>
<view class="material_details">
<view class="detail_row">
<text class="detail_label">发布时间</text>
<text class="detail_value">{{ material.created_at }}</text>
</view>
<view class="detail_row">
<text class="detail_label">资料类型</text>
<text class="detail_value">{{ material.type }}</text>
</view>
</view>
</view>
<view class="material_arrow">
<image :src="$util.img('/uniapp_src/static/images/index/right_arrow.png')" class="arrow-icon"></image>
</view>
</view>
</view>
</view>
<!-- 空状态 -->
<view class="empty_state" v-if="!loading && materialsList.length === 0">
<image :src="$util.img('/uniapp_src/static/images/common/empty.png')" class="empty_icon"></image>
<view class="empty_text">暂无教学资料</view>
</view>
<!-- 加载状态 -->
<view class="loading_state" v-if="loading">
<view class="loading_text">加载中...</view>
</view>
</view>
</template>
<script>
import { mapState } from 'vuex'
import apiRoute from '@/api/apiRoute.js'
export default {
data() {
return {
materialsList: [],
loading: false,
childId: null
}
},
computed: {
...mapState(['selectedChild'])
},
onLoad(options) {
this.childId = options.childId
this.loadMaterialsList()
},
methods: {
async loadMaterialsList() {
if (!this.childId) {
uni.showToast({
title: '缺少孩子ID参数',
icon: 'none'
})
return
}
this.loading = true
try {
const response = await apiRoute.parent_getChildMaterials({
child_id: this.childId
})
if (response.code === 1) {
this.materialsList = response.data.data || []
} else {
uni.showToast({
title: response.msg || '获取教学资料失败',
icon: 'none'
})
}
} catch (error) {
console.error('获取教学资料失败:', error)
uni.showToast({
title: '获取教学资料失败',
icon: 'none'
})
} finally {
this.loading = false
}
},
viewMaterialDetail(material) {
this.$navigateTo({
url: `/pages/parent/materials/material-detail?materialId=${material.id}&childId=${this.childId}`
})
}
}
}
</script>
<style lang="less" scoped>
.main_box {
background: #f8f9fa;
min-height: 100vh;
padding: 20rpx;
}
.child_info_bar {
background: #fff;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 20rpx;
display: flex;
align-items: center;
gap: 20rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
.child_avatar {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
overflow: hidden;
image {
width: 100%;
height: 100%;
}
}
.child_details {
flex: 1;
.child_name {
font-size: 28rpx;
font-weight: 600;
color: #333;
margin-bottom: 8rpx;
}
.child_class {
font-size: 24rpx;
color: #666;
}
}
}
.materials_list {
.section_title {
font-size: 32rpx;
font-weight: 600;
color: #333;
margin-bottom: 24rpx;
padding-left: 8rpx;
}
.materials_items {
display: flex;
flex-direction: column;
gap: 16rpx;
}
}
.material_item {
background: #fff;
border-radius: 16rpx;
padding: 28rpx;
display: flex;
align-items: center;
gap: 20rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
.material_main {
flex: 1;
.material_header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16rpx;
.material_title {
font-size: 30rpx;
font-weight: 600;
color: #333;
}
.material_type {
font-size: 22rpx;
padding: 4rpx 12rpx;
border-radius: 12rpx;
background: rgba(41, 211, 180, 0.1);
color: #29d3b4;
}
}
.material_details {
.detail_row {
display: flex;
margin-bottom: 8rpx;
.detail_label {
font-size: 24rpx;
color: #666;
min-width: 160rpx;
}
.detail_value {
font-size: 24rpx;
color: #333;
flex: 1;
}
}
}
}
.material_arrow {
.arrow-icon {
width: 24rpx;
height: 24rpx;
opacity: 0.4;
}
}
}
.empty_state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 120rpx 0;
.empty_icon {
width: 160rpx;
height: 160rpx;
margin-bottom: 32rpx;
opacity: 0.3;
}
.empty_text {
font-size: 28rpx;
color: #999;
}
}
.loading_state {
display: flex;
justify-content: center;
align-items: center;
padding: 60rpx 0;
.loading_text {
font-size: 28rpx;
color: #666;
}
}
</style>

181
uniapp/pages/parent/materials/material-detail.vue

@ -0,0 +1,181 @@
<!--教学资料详情页面-->
<template>
<view class="main_box">
<!-- 资料基本信息 -->
<view class="material_info_card" v-if="materialInfo">
<view class="material_header">
<view class="material_title">{{ materialInfo.title }}</view>
<view class="material_type">{{ materialInfo.type }}</view>
</view>
<view class="material_details">
<view class="detail_item">
<view class="detail_label">发布时间</view>
<view class="detail_value">{{ materialInfo.created_at }}</view>
</view>
<view class="detail_item">
<view class="detail_label">资料类型</view>
<view class="detail_value">{{ materialInfo.type }}</view>
</view>
</view>
<view class="material_content">
<view class="content_title">资料内容</view>
<view class="content_text">{{ materialInfo.content }}</view>
</view>
</view>
<!-- 空状态 -->
<view class="empty_state" v-if="!loading && !materialInfo">
<image src="/static/icon-img/empty.png" class="empty_icon"></image>
<view class="empty_text">暂无资料信息</view>
</view>
<!-- 加载状态 -->
<view class="loading_state" v-if="loading">
<view class="loading_text">加载中...</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
materialInfo: null,
loading: false,
materialId: null,
childId: null
}
},
onLoad(options) {
this.materialId = options.materialId
this.childId = options.childId
this.loadMaterialInfo()
},
methods: {
async loadMaterialInfo() {
//
this.materialInfo = {
title: '篮球基础训练视频',
type: '视频资料',
created_at: '2024-01-15 14:30:00',
content: '这是一套专门为少儿设计的篮球基础训练视频,包含运球、投篮、传球等基本技能的教学内容。'
}
}
}
}
</script>
<style lang="less" scoped>
.main_box {
background: #f8f9fa;
min-height: 100vh;
padding: 20rpx;
}
.material_info_card {
background: #fff;
border-radius: 16rpx;
padding: 32rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
.material_header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 32rpx;
padding-bottom: 24rpx;
border-bottom: 1px solid #f0f0f0;
.material_title {
font-size: 36rpx;
font-weight: 600;
color: #333;
}
.material_type {
font-size: 24rpx;
padding: 8rpx 16rpx;
border-radius: 12rpx;
background: rgba(41, 211, 180, 0.1);
color: #29d3b4;
}
}
.material_details {
margin-bottom: 32rpx;
.detail_item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx 0;
border-bottom: 1px solid #f8f9fa;
.detail_label {
font-size: 28rpx;
color: #666;
min-width: 160rpx;
}
.detail_value {
font-size: 28rpx;
color: #333;
flex: 1;
text-align: right;
}
}
}
.material_content {
.content_title {
font-size: 28rpx;
font-weight: 600;
color: #333;
margin-bottom: 16rpx;
}
.content_text {
font-size: 26rpx;
color: #666;
line-height: 1.6;
padding: 16rpx;
background: #f8f9fa;
border-radius: 8rpx;
}
}
}
.empty_state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 120rpx 0;
.empty_icon {
width: 160rpx;
height: 160rpx;
margin-bottom: 32rpx;
opacity: 0.3;
}
.empty_text {
font-size: 28rpx;
color: #999;
}
}
.loading_state {
display: flex;
justify-content: center;
align-items: center;
padding: 60rpx 0;
.loading_text {
font-size: 28rpx;
color: #666;
}
}
</style>

280
uniapp/pages/parent/messages/index.vue

@ -0,0 +1,280 @@
<!--家长端消息管理页面-->
<template>
<view class="main_box">
<!-- 选中孩子信息 -->
<view class="child_info_bar" v-if="selectedChild">
<view class="child_avatar">
<image :src="selectedChild.avatar" mode="aspectFill"></image>
</view>
<view class="child_details">
<view class="child_name">{{ selectedChild.name }}</view>
<view class="child_class">{{ selectedChild.class_name || '未分配班级' }}</view>
</view>
</view>
<!-- 消息列表 -->
<view class="messages_list">
<view class="section_title">消息记录</view>
<view class="messages_items">
<view
v-for="message in messagesList"
:key="message.id"
class="message_item"
@click="viewMessageDetail(message)"
>
<view class="message_main">
<view class="message_header">
<view class="message_title">{{ message.title }}</view>
<view class="message_time">{{ message.created_at }}</view>
</view>
<view class="message_content">{{ message.content }}</view>
<view class="message_details">
<view class="detail_row">
<text class="detail_label">发送者</text>
<text class="detail_value">{{ message.sender }}</text>
</view>
</view>
</view>
<view class="message_arrow">
<image :src="$util.img('/uniapp_src/static/images/index/right_arrow.png')" class="arrow-icon"></image>
</view>
</view>
</view>
</view>
<!-- 空状态 -->
<view class="empty_state" v-if="!loading && messagesList.length === 0">
<image :src="$util.img('/uniapp_src/static/images/common/empty.png')" class="empty_icon"></image>
<view class="empty_text">暂无消息记录</view>
</view>
<!-- 加载状态 -->
<view class="loading_state" v-if="loading">
<view class="loading_text">加载中...</view>
</view>
</view>
</template>
<script>
import { mapState } from 'vuex'
import apiRoute from '@/api/apiRoute.js'
export default {
data() {
return {
messagesList: [],
loading: false,
childId: null
}
},
computed: {
...mapState(['selectedChild'])
},
onLoad(options) {
this.childId = options.childId
this.loadMessagesList()
},
methods: {
async loadMessagesList() {
if (!this.childId) {
uni.showToast({
title: '缺少孩子ID参数',
icon: 'none'
})
return
}
this.loading = true
try {
const response = await apiRoute.parent_getChildMessages({
child_id: this.childId
})
if (response.code === 1) {
this.messagesList = response.data.data || []
} else {
uni.showToast({
title: response.msg || '获取消息记录失败',
icon: 'none'
})
}
} catch (error) {
console.error('获取消息记录失败:', error)
uni.showToast({
title: '获取消息记录失败',
icon: 'none'
})
} finally {
this.loading = false
}
},
viewMessageDetail(message) {
this.$navigateTo({
url: `/pages/parent/messages/message-detail?messageId=${message.id}&childId=${this.childId}`
})
}
}
}
</script>
<style lang="less" scoped>
.main_box {
background: #f8f9fa;
min-height: 100vh;
padding: 20rpx;
}
.child_info_bar {
background: #fff;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 20rpx;
display: flex;
align-items: center;
gap: 20rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
.child_avatar {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
overflow: hidden;
image {
width: 100%;
height: 100%;
}
}
.child_details {
flex: 1;
.child_name {
font-size: 28rpx;
font-weight: 600;
color: #333;
margin-bottom: 8rpx;
}
.child_class {
font-size: 24rpx;
color: #666;
}
}
}
.messages_list {
.section_title {
font-size: 32rpx;
font-weight: 600;
color: #333;
margin-bottom: 24rpx;
padding-left: 8rpx;
}
.messages_items {
display: flex;
flex-direction: column;
gap: 16rpx;
}
}
.message_item {
background: #fff;
border-radius: 16rpx;
padding: 28rpx;
display: flex;
align-items: center;
gap: 20rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
.message_main {
flex: 1;
.message_header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12rpx;
.message_title {
font-size: 30rpx;
font-weight: 600;
color: #333;
}
.message_time {
font-size: 22rpx;
color: #999;
}
}
.message_content {
font-size: 26rpx;
color: #666;
line-height: 1.4;
margin-bottom: 12rpx;
}
.message_details {
.detail_row {
display: flex;
margin-bottom: 8rpx;
.detail_label {
font-size: 24rpx;
color: #666;
min-width: 120rpx;
}
.detail_value {
font-size: 24rpx;
color: #333;
flex: 1;
}
}
}
}
.message_arrow {
.arrow-icon {
width: 24rpx;
height: 24rpx;
opacity: 0.4;
}
}
}
.empty_state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 120rpx 0;
.empty_icon {
width: 160rpx;
height: 160rpx;
margin-bottom: 32rpx;
opacity: 0.3;
}
.empty_text {
font-size: 28rpx;
color: #999;
}
}
.loading_state {
display: flex;
justify-content: center;
align-items: center;
padding: 60rpx 0;
.loading_text {
font-size: 28rpx;
color: #666;
}
}
</style>

174
uniapp/pages/parent/messages/message-detail.vue

@ -0,0 +1,174 @@
<!--消息详情页面-->
<template>
<view class="main_box">
<!-- 消息基本信息 -->
<view class="message_info_card" v-if="messageInfo">
<view class="message_header">
<view class="message_title">{{ messageInfo.title }}</view>
<view class="message_time">{{ messageInfo.created_at }}</view>
</view>
<view class="message_details">
<view class="detail_item">
<view class="detail_label">发送者</view>
<view class="detail_value">{{ messageInfo.sender }}</view>
</view>
</view>
<view class="message_content">
<view class="content_title">消息内容</view>
<view class="content_text">{{ messageInfo.content }}</view>
</view>
</view>
<!-- 空状态 -->
<view class="empty_state" v-if="!loading && !messageInfo">
<image src="/static/icon-img/empty.png" class="empty_icon"></image>
<view class="empty_text">暂无消息信息</view>
</view>
<!-- 加载状态 -->
<view class="loading_state" v-if="loading">
<view class="loading_text">加载中...</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
messageInfo: null,
loading: false,
messageId: null,
childId: null
}
},
onLoad(options) {
this.messageId = options.messageId
this.childId = options.childId
this.loadMessageInfo()
},
methods: {
async loadMessageInfo() {
//
this.messageInfo = {
title: '课程提醒',
sender: '王教练',
created_at: '2024-01-15 09:30:00',
content: '提醒您的孩子明天有篮球课,请准时参加。上课时间:10:00-11:30,地点:篮球馆A。'
}
}
}
}
</script>
<style lang="less" scoped>
.main_box {
background: #f8f9fa;
min-height: 100vh;
padding: 20rpx;
}
.message_info_card {
background: #fff;
border-radius: 16rpx;
padding: 32rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
.message_header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 32rpx;
padding-bottom: 24rpx;
border-bottom: 1px solid #f0f0f0;
.message_title {
font-size: 36rpx;
font-weight: 600;
color: #333;
}
.message_time {
font-size: 24rpx;
color: #999;
}
}
.message_details {
margin-bottom: 32rpx;
.detail_item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx 0;
border-bottom: 1px solid #f8f9fa;
.detail_label {
font-size: 28rpx;
color: #666;
min-width: 160rpx;
}
.detail_value {
font-size: 28rpx;
color: #333;
flex: 1;
text-align: right;
}
}
}
.message_content {
.content_title {
font-size: 28rpx;
font-weight: 600;
color: #333;
margin-bottom: 16rpx;
}
.content_text {
font-size: 26rpx;
color: #666;
line-height: 1.6;
padding: 16rpx;
background: #f8f9fa;
border-radius: 8rpx;
}
}
}
.empty_state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 120rpx 0;
.empty_icon {
width: 160rpx;
height: 160rpx;
margin-bottom: 32rpx;
opacity: 0.3;
}
.empty_text {
font-size: 28rpx;
color: #999;
}
}
.loading_state {
display: flex;
justify-content: center;
align-items: center;
padding: 60rpx 0;
.loading_text {
font-size: 28rpx;
color: #666;
}
}
</style>

305
uniapp/pages/parent/orders/index.vue

@ -0,0 +1,305 @@
<!--家长端订单管理页面-->
<template>
<view class="main_box">
<!-- 选中孩子信息 -->
<view class="child_info_bar" v-if="selectedChild">
<view class="child_avatar">
<image :src="selectedChild.avatar" mode="aspectFill"></image>
</view>
<view class="child_details">
<view class="child_name">{{ selectedChild.name }}</view>
<view class="child_class">{{ selectedChild.class_name || '未分配班级' }}</view>
</view>
</view>
<!-- 订单列表 -->
<view class="order_list">
<view class="section_title">订单列表</view>
<view class="order_items">
<view
v-for="order in orderList"
:key="order.id"
class="order_item"
@click="viewOrderDetail(order)"
>
<view class="order_main">
<view class="order_header">
<view class="order_no">订单号{{ order.order_no }}</view>
<view class="order_status" :class="order.status">{{ order.status_text }}</view>
</view>
<view class="order_details">
<view class="detail_row">
<text class="detail_label">课程名称</text>
<text class="detail_value">{{ order.course_name }}</text>
</view>
<view class="detail_row">
<text class="detail_label">订单金额</text>
<text class="detail_value amount">¥{{ order.amount }}</text>
</view>
<view class="detail_row">
<text class="detail_label">下单时间</text>
<text class="detail_value">{{ order.created_at }}</text>
</view>
<view class="detail_row" v-if="order.pay_time">
<text class="detail_label">支付时间</text>
<text class="detail_value">{{ order.pay_time }}</text>
</view>
</view>
</view>
<view class="order_arrow">
<image :src="$util.img('/uniapp_src/static/images/index/right_arrow.png')" class="arrow-icon"></image>
</view>
</view>
</view>
</view>
<!-- 空状态 -->
<view class="empty_state" v-if="!loading && orderList.length === 0">
<image :src="$util.img('/uniapp_src/static/images/common/empty.png')" class="empty_icon"></image>
<view class="empty_text">暂无订单信息</view>
</view>
<!-- 加载状态 -->
<view class="loading_state" v-if="loading">
<view class="loading_text">加载中...</view>
</view>
</view>
</template>
<script>
import { mapState } from 'vuex'
import apiRoute from '@/api/apiRoute.js'
export default {
data() {
return {
orderList: [],
loading: false,
childId: null
}
},
computed: {
...mapState(['selectedChild'])
},
onLoad(options) {
this.childId = options.childId
this.loadOrderList()
},
methods: {
async loadOrderList() {
if (!this.childId) {
uni.showToast({
title: '缺少孩子ID参数',
icon: 'none'
})
return
}
this.loading = true
try {
const response = await apiRoute.parent_getChildOrders({
child_id: this.childId
})
if (response.code === 1) {
this.orderList = response.data.data || []
} else {
uni.showToast({
title: response.msg || '获取订单列表失败',
icon: 'none'
})
}
} catch (error) {
console.error('获取订单列表失败:', error)
uni.showToast({
title: '获取订单列表失败',
icon: 'none'
})
} finally {
this.loading = false
}
},
viewOrderDetail(order) {
this.$navigateTo({
url: `/pages/parent/orders/order-detail?orderId=${order.id}&childId=${this.childId}`
})
}
}
}
</script>
<style lang="less" scoped>
.main_box {
background: #f8f9fa;
min-height: 100vh;
padding: 20rpx;
}
.child_info_bar {
background: #fff;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 20rpx;
display: flex;
align-items: center;
gap: 20rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
.child_avatar {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
overflow: hidden;
image {
width: 100%;
height: 100%;
}
}
.child_details {
flex: 1;
.child_name {
font-size: 28rpx;
font-weight: 600;
color: #333;
margin-bottom: 8rpx;
}
.child_class {
font-size: 24rpx;
color: #666;
}
}
}
.order_list {
.section_title {
font-size: 32rpx;
font-weight: 600;
color: #333;
margin-bottom: 24rpx;
padding-left: 8rpx;
}
.order_items {
display: flex;
flex-direction: column;
gap: 16rpx;
}
}
.order_item {
background: #fff;
border-radius: 16rpx;
padding: 28rpx;
display: flex;
align-items: center;
gap: 20rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
.order_main {
flex: 1;
.order_header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16rpx;
.order_no {
font-size: 26rpx;
font-weight: 600;
color: #333;
}
.order_status {
font-size: 22rpx;
padding: 4rpx 12rpx;
border-radius: 12rpx;
&.paid {
background: rgba(40, 167, 69, 0.1);
color: #28a745;
}
&.unpaid {
background: rgba(220, 53, 69, 0.1);
color: #dc3545;
}
&.refund {
background: rgba(108, 117, 125, 0.1);
color: #6c757d;
}
}
}
.order_details {
.detail_row {
display: flex;
margin-bottom: 8rpx;
.detail_label {
font-size: 24rpx;
color: #666;
min-width: 160rpx;
}
.detail_value {
font-size: 24rpx;
color: #333;
flex: 1;
&.amount {
color: #e67e22;
font-weight: 600;
}
}
}
}
}
.order_arrow {
.arrow-icon {
width: 24rpx;
height: 24rpx;
opacity: 0.4;
}
}
}
.empty_state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 120rpx 0;
.empty_icon {
width: 160rpx;
height: 160rpx;
margin-bottom: 32rpx;
opacity: 0.3;
}
.empty_text {
font-size: 28rpx;
color: #999;
}
}
.loading_state {
display: flex;
justify-content: center;
align-items: center;
padding: 60rpx 0;
.loading_text {
font-size: 28rpx;
color: #666;
}
}
</style>

182
uniapp/pages/parent/orders/order-detail.vue

@ -0,0 +1,182 @@
<!--订单详情页面-->
<template>
<view class="main_box">
<!-- 订单基本信息 -->
<view class="order_info_card" v-if="orderInfo">
<view class="order_header">
<view class="order_no">订单号{{ orderInfo.order_no }}</view>
<view class="order_status" :class="orderInfo.status">
{{ orderInfo.status_text }}
</view>
</view>
<view class="order_details">
<view class="detail_item">
<view class="detail_label">课程名称</view>
<view class="detail_value">{{ orderInfo.course_name }}</view>
</view>
<view class="detail_item">
<view class="detail_label">订单金额</view>
<view class="detail_value amount">¥{{ orderInfo.amount }}</view>
</view>
<view class="detail_item">
<view class="detail_label">下单时间</view>
<view class="detail_value">{{ orderInfo.created_at }}</view>
</view>
<view class="detail_item" v-if="orderInfo.pay_time">
<view class="detail_label">支付时间</view>
<view class="detail_value">{{ orderInfo.pay_time }}</view>
</view>
</view>
</view>
<!-- 空状态 -->
<view class="empty_state" v-if="!loading && !orderInfo">
<image src="/static/icon-img/empty.png" class="empty_icon"></image>
<view class="empty_text">暂无订单信息</view>
</view>
<!-- 加载状态 -->
<view class="loading_state" v-if="loading">
<view class="loading_text">加载中...</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
orderInfo: null,
loading: false,
orderId: null,
childId: null
}
},
onLoad(options) {
this.orderId = options.orderId
this.childId = options.childId
this.loadOrderInfo()
},
methods: {
async loadOrderInfo() {
//
this.orderInfo = {
order_no: 'ORD202401001',
course_name: '少儿篮球课程包',
amount: '2880.00',
status: 'paid',
status_text: '已支付',
created_at: '2024-01-01 10:00:00',
pay_time: '2024-01-01 10:05:00'
}
}
}
}
</script>
<style lang="less" scoped>
.main_box {
background: #f8f9fa;
min-height: 100vh;
padding: 20rpx;
}
.order_info_card {
background: #fff;
border-radius: 16rpx;
padding: 32rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
.order_header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 32rpx;
padding-bottom: 24rpx;
border-bottom: 1px solid #f0f0f0;
.order_no {
font-size: 32rpx;
font-weight: 600;
color: #333;
}
.order_status {
font-size: 24rpx;
padding: 8rpx 16rpx;
border-radius: 12rpx;
&.paid {
background: rgba(40, 167, 69, 0.1);
color: #28a745;
}
&.unpaid {
background: rgba(220, 53, 69, 0.1);
color: #dc3545;
}
}
}
.order_details {
.detail_item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx 0;
border-bottom: 1px solid #f8f9fa;
.detail_label {
font-size: 28rpx;
color: #666;
min-width: 160rpx;
}
.detail_value {
font-size: 28rpx;
color: #333;
flex: 1;
text-align: right;
&.amount {
color: #e67e22;
font-weight: 600;
}
}
}
}
}
.empty_state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 120rpx 0;
.empty_icon {
width: 160rpx;
height: 160rpx;
margin-bottom: 32rpx;
opacity: 0.3;
}
.empty_text {
font-size: 28rpx;
color: #999;
}
}
.loading_state {
display: flex;
justify-content: center;
align-items: center;
padding: 60rpx 0;
.loading_text {
font-size: 28rpx;
color: #666;
}
}
</style>

279
uniapp/pages/parent/services/index.vue

@ -0,0 +1,279 @@
<!--家长端服务管理页面-->
<template>
<view class="main_box">
<!-- 选中孩子信息 -->
<view class="child_info_bar" v-if="selectedChild">
<view class="child_avatar">
<image :src="selectedChild.avatar" mode="aspectFill"></image>
</view>
<view class="child_details">
<view class="child_name">{{ selectedChild.name }}</view>
<view class="child_class">{{ selectedChild.class_name || '未分配班级' }}</view>
</view>
</view>
<!-- 服务列表 -->
<view class="services_list">
<view class="section_title">服务记录</view>
<view class="services_items">
<view
v-for="service in servicesList"
:key="service.id"
class="service_item"
@click="viewServiceDetail(service)"
>
<view class="service_main">
<view class="service_header">
<view class="service_title">{{ service.title }}</view>
<view class="service_status" :class="service.status">{{ service.status_text }}</view>
</view>
<view class="service_details">
<view class="detail_row">
<text class="detail_label">服务时间</text>
<text class="detail_value">{{ service.service_time }}</text>
</view>
<view class="detail_row">
<text class="detail_label">服务内容</text>
<text class="detail_value">{{ service.content }}</text>
</view>
</view>
</view>
<view class="service_arrow">
<image :src="$util.img('/uniapp_src/static/images/index/right_arrow.png')" class="arrow-icon"></image>
</view>
</view>
</view>
</view>
<!-- 空状态 -->
<view class="empty_state" v-if="!loading && servicesList.length === 0">
<image :src="$util.img('/uniapp_src/static/images/common/empty.png')" class="empty_icon"></image>
<view class="empty_text">暂无服务记录</view>
</view>
<!-- 加载状态 -->
<view class="loading_state" v-if="loading">
<view class="loading_text">加载中...</view>
</view>
</view>
</template>
<script>
import { mapState } from 'vuex'
import apiRoute from '@/api/apiRoute.js'
export default {
data() {
return {
servicesList: [],
loading: false,
childId: null
}
},
computed: {
...mapState(['selectedChild'])
},
onLoad(options) {
this.childId = options.childId
this.loadServicesList()
},
methods: {
async loadServicesList() {
if (!this.childId) {
uni.showToast({
title: '缺少孩子ID参数',
icon: 'none'
})
return
}
this.loading = true
try {
const response = await apiRoute.parent_getChildServices({
child_id: this.childId
})
if (response.code === 1) {
this.servicesList = response.data.data || []
} else {
uni.showToast({
title: response.msg || '获取服务记录失败',
icon: 'none'
})
}
} catch (error) {
console.error('获取服务记录失败:', error)
uni.showToast({
title: '获取服务记录失败',
icon: 'none'
})
} finally {
this.loading = false
}
},
viewServiceDetail(service) {
this.$navigateTo({
url: `/pages/parent/services/service-detail?serviceId=${service.id}&childId=${this.childId}`
})
}
}
}
</script>
<style lang="less" scoped>
.main_box {
background: #f8f9fa;
min-height: 100vh;
padding: 20rpx;
}
.child_info_bar {
background: #fff;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 20rpx;
display: flex;
align-items: center;
gap: 20rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
.child_avatar {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
overflow: hidden;
image {
width: 100%;
height: 100%;
}
}
.child_details {
flex: 1;
.child_name {
font-size: 28rpx;
font-weight: 600;
color: #333;
margin-bottom: 8rpx;
}
.child_class {
font-size: 24rpx;
color: #666;
}
}
}
.services_list {
.section_title {
font-size: 32rpx;
font-weight: 600;
color: #333;
margin-bottom: 24rpx;
padding-left: 8rpx;
}
.services_items {
display: flex;
flex-direction: column;
gap: 16rpx;
}
}
.service_item {
background: #fff;
border-radius: 16rpx;
padding: 28rpx;
display: flex;
align-items: center;
gap: 20rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
.service_main {
flex: 1;
.service_header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16rpx;
.service_title {
font-size: 30rpx;
font-weight: 600;
color: #333;
}
.service_status {
font-size: 22rpx;
padding: 4rpx 12rpx;
border-radius: 12rpx;
background: rgba(41, 211, 180, 0.1);
color: #29d3b4;
}
}
.service_details {
.detail_row {
display: flex;
margin-bottom: 8rpx;
.detail_label {
font-size: 24rpx;
color: #666;
min-width: 160rpx;
}
.detail_value {
font-size: 24rpx;
color: #333;
flex: 1;
}
}
}
}
.service_arrow {
.arrow-icon {
width: 24rpx;
height: 24rpx;
opacity: 0.4;
}
}
}
.empty_state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 120rpx 0;
.empty_icon {
width: 160rpx;
height: 160rpx;
margin-bottom: 32rpx;
opacity: 0.3;
}
.empty_text {
font-size: 28rpx;
color: #999;
}
}
.loading_state {
display: flex;
justify-content: center;
align-items: center;
padding: 60rpx 0;
.loading_text {
font-size: 28rpx;
color: #666;
}
}
</style>

159
uniapp/pages/parent/services/service-detail.vue

@ -0,0 +1,159 @@
<!--服务详情页面-->
<template>
<view class="main_box">
<!-- 服务基本信息 -->
<view class="service_info_card" v-if="serviceInfo">
<view class="service_header">
<view class="service_title">{{ serviceInfo.title }}</view>
<view class="service_status" :class="serviceInfo.status">
{{ serviceInfo.status_text }}
</view>
</view>
<view class="service_details">
<view class="detail_item">
<view class="detail_label">服务时间</view>
<view class="detail_value">{{ serviceInfo.service_time }}</view>
</view>
<view class="detail_item">
<view class="detail_label">服务内容</view>
<view class="detail_value">{{ serviceInfo.content }}</view>
</view>
</view>
</view>
<!-- 空状态 -->
<view class="empty_state" v-if="!loading && !serviceInfo">
<image src="/static/icon-img/empty.png" class="empty_icon"></image>
<view class="empty_text">暂无服务信息</view>
</view>
<!-- 加载状态 -->
<view class="loading_state" v-if="loading">
<view class="loading_text">加载中...</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
serviceInfo: null,
loading: false,
serviceId: null,
childId: null
}
},
onLoad(options) {
this.serviceId = options.serviceId
this.childId = options.childId
this.loadServiceInfo()
},
methods: {
async loadServiceInfo() {
//
this.serviceInfo = {
title: '课程跟踪服务',
status: 'completed',
status_text: '已完成',
service_time: '2024-01-15 10:00:00',
content: '针对学员的课程进度进行跟踪辅导,提供个性化建议。'
}
}
}
}
</script>
<style lang="less" scoped>
.main_box {
background: #f8f9fa;
min-height: 100vh;
padding: 20rpx;
}
.service_info_card {
background: #fff;
border-radius: 16rpx;
padding: 32rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
.service_header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 32rpx;
padding-bottom: 24rpx;
border-bottom: 1px solid #f0f0f0;
.service_title {
font-size: 36rpx;
font-weight: 600;
color: #333;
}
.service_status {
font-size: 24rpx;
padding: 8rpx 16rpx;
border-radius: 12rpx;
background: rgba(41, 211, 180, 0.1);
color: #29d3b4;
}
}
.service_details {
.detail_item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx 0;
border-bottom: 1px solid #f8f9fa;
.detail_label {
font-size: 28rpx;
color: #666;
min-width: 160rpx;
}
.detail_value {
font-size: 28rpx;
color: #333;
flex: 1;
text-align: right;
}
}
}
}
.empty_state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 120rpx 0;
.empty_icon {
width: 160rpx;
height: 160rpx;
margin-bottom: 32rpx;
opacity: 0.3;
}
.empty_text {
font-size: 28rpx;
color: #999;
}
}
.loading_state {
display: flex;
justify-content: center;
align-items: center;
padding: 60rpx 0;
.loading_text {
font-size: 28rpx;
color: #666;
}
}
</style>

308
uniapp/pages/parent/user-info/child-detail.vue

@ -0,0 +1,308 @@
<!--孩子详情页面-->
<template>
<view class="main_box">
<!-- 孩子基本信息 -->
<view class="child_info_card" v-if="childInfo">
<view class="child_header">
<view class="child_avatar">
<image :src="childInfo.avatar" mode="aspectFill"></image>
</view>
<view class="child_basic">
<view class="child_name">{{ childInfo.name }}</view>
<view class="child_tags">
<view class="tag gender">{{ childInfo.gender === 1 ? '男' : '女' }}</view>
<view class="tag age">{{ Math.floor(childInfo.age) }}</view>
<view class="tag label">{{ childInfo.member_label }}</view>
</view>
</view>
</view>
<view class="child_details">
<view class="detail_section">
<view class="section_title">基本信息</view>
<view class="detail_item">
<view class="detail_label">生日</view>
<view class="detail_value">{{ childInfo.birthday }}</view>
</view>
<view class="detail_item">
<view class="detail_label">年龄</view>
<view class="detail_value">{{ childInfo.age }}</view>
</view>
<view class="detail_item">
<view class="detail_label">紧急联系人</view>
<view class="detail_value">{{ childInfo.emergency_contact }}</view>
</view>
<view class="detail_item">
<view class="detail_label">联系电话</view>
<view class="detail_value">{{ childInfo.contact_phone }}</view>
</view>
</view>
<view class="detail_section">
<view class="section_title">校区信息</view>
<view class="detail_item">
<view class="detail_label">所属校区</view>
<view class="detail_value">{{ childInfo.campus_name || '未分配' }}</view>
</view>
<view class="detail_item">
<view class="detail_label">班级</view>
<view class="detail_value">{{ childInfo.class_name || '未分配' }}</view>
</view>
<view class="detail_item">
<view class="detail_label">教练</view>
<view class="detail_value">{{ childInfo.coach_name || '未分配' }}</view>
</view>
</view>
<view class="detail_section">
<view class="section_title">学习情况</view>
<view class="detail_item">
<view class="detail_label">总课程数</view>
<view class="detail_value">{{ childInfo.total_courses }}</view>
</view>
<view class="detail_item">
<view class="detail_label">已完成</view>
<view class="detail_value">{{ childInfo.completed_courses }}</view>
</view>
<view class="detail_item">
<view class="detail_label">剩余课时</view>
<view class="detail_value">{{ childInfo.remaining_courses }}</view>
</view>
<view class="detail_item">
<view class="detail_label">出勤率</view>
<view class="detail_value">{{ childInfo.attendance_rate }}%</view>
</view>
</view>
<view class="detail_section" v-if="childInfo.note">
<view class="section_title">备注信息</view>
<view class="note_content">{{ childInfo.note }}</view>
</view>
</view>
</view>
<!-- 空状态 -->
<view class="empty_state" v-if="!loading && !childInfo">
<image :src="$util.img('/static/icon-img/empty.png')" class="empty_icon"></image>
<view class="empty_text">暂无孩子信息</view>
</view>
<!-- 加载状态 -->
<view class="loading_state" v-if="loading">
<view class="loading_text">加载中...</view>
</view>
</view>
</template>
<script>
import { mapState } from 'vuex'
import apiRoute from '@/api/apiRoute.js'
export default {
data() {
return {
childInfo: null,
loading: false,
childId: null
}
},
computed: {
...mapState(['selectedChild'])
},
onLoad(options) {
this.childId = options.childId
this.loadChildInfo()
},
methods: {
async loadChildInfo() {
if (!this.childId) {
// childId使
if (this.selectedChild) {
this.childInfo = this.selectedChild
} else {
uni.showToast({
title: '缺少孩子ID参数',
icon: 'none'
})
}
return
}
this.loading = true
try {
const response = await apiRoute.parent_getChildInfo({
child_id: this.childId
})
if (response.code === 1) {
this.childInfo = response.data
} else {
uni.showToast({
title: response.msg || '获取孩子信息失败',
icon: 'none'
})
}
} catch (error) {
console.error('获取孩子信息失败:', error)
uni.showToast({
title: '获取孩子信息失败',
icon: 'none'
})
} finally {
this.loading = false
}
}
}
}
</script>
<style lang="less" scoped>
.main_box {
background: #f8f9fa;
min-height: 100vh;
padding: 20rpx;
}
.child_info_card {
background: #fff;
border-radius: 16rpx;
padding: 32rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
.child_header {
display: flex;
align-items: center;
gap: 24rpx;
padding-bottom: 32rpx;
border-bottom: 1px solid #f0f0f0;
margin-bottom: 32rpx;
.child_avatar {
width: 120rpx;
height: 120rpx;
border-radius: 50%;
overflow: hidden;
image {
width: 100%;
height: 100%;
}
}
.child_basic {
flex: 1;
.child_name {
font-size: 36rpx;
font-weight: 600;
color: #333;
margin-bottom: 16rpx;
}
.child_tags {
display: flex;
gap: 12rpx;
flex-wrap: wrap;
.tag {
font-size: 22rpx;
padding: 6rpx 12rpx;
border-radius: 12rpx;
&.gender {
background: rgba(41, 211, 180, 0.1);
color: #29d3b4;
}
&.age {
background: rgba(52, 152, 219, 0.1);
color: #3498db;
}
&.label {
background: rgba(230, 126, 34, 0.1);
color: #e67e22;
}
}
}
}
}
.child_details {
.detail_section {
margin-bottom: 32rpx;
.section_title {
font-size: 28rpx;
font-weight: 600;
color: #333;
margin-bottom: 20rpx;
padding-bottom: 12rpx;
border-bottom: 2rpx solid #29d3b4;
}
.detail_item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16rpx 0;
border-bottom: 1px solid #f8f9fa;
.detail_label {
font-size: 26rpx;
color: #666;
min-width: 160rpx;
}
.detail_value {
font-size: 26rpx;
color: #333;
flex: 1;
text-align: right;
}
}
.note_content {
font-size: 26rpx;
color: #666;
line-height: 1.6;
padding: 16rpx;
background: #f8f9fa;
border-radius: 8rpx;
}
}
}
}
.empty_state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 120rpx 0;
.empty_icon {
width: 160rpx;
height: 160rpx;
margin-bottom: 32rpx;
opacity: 0.3;
}
.empty_text {
font-size: 28rpx;
color: #999;
}
}
.loading_state {
display: flex;
justify-content: center;
align-items: center;
padding: 60rpx 0;
.loading_text {
font-size: 28rpx;
color: #666;
}
}
</style>

592
uniapp/pages/parent/user-info/index.vue

@ -0,0 +1,592 @@
<!--家长端用户信息管理页面-->
<template>
<view class="main_box">
<view class="navbar_section">
<view class="title">用户信息</view>
</view>
<!-- 家长信息展示 -->
<view class="parent_info_section">
<view class="parent_name">{{ parentInfo.name || '张家长' }}</view>
<view class="parent_phone">{{ parentInfo.phone_number || '13800138000' }}</view>
</view>
<!-- 选中孩子信息弹窗 -->
<view class="child_popup" v-if="showChildPopup" @click="closeChildPopup">
<view class="popup_content" @click.stop>
<view class="popup_header">
<view class="popup_title">选择孩子</view>
<view class="popup_close" @click="closeChildPopup">×</view>
</view>
<view class="popup_children_list">
<view
v-for="child in childrenList"
:key="child.id"
:class="['popup_child_item', selectedChild && selectedChild.id === child.id ? 'selected' : '']"
@click="selectChildFromPopup(child)"
>
<view class="popup_child_info">
<view class="popup_child_name">{{ child.name }}</view>
<view class="popup_child_details">
<text class="popup_detail_tag">{{ child.gender === 1 ? '男' : '女' }}</text>
<text class="popup_detail_tag">{{ Math.floor(child.age) }}</text>
</view>
<view class="popup_child_campus">{{ child.campus_name || '未分配' }}</view>
<view class="popup_child_class">{{ child.class_name || '未分配' }}</view>
</view>
<view class="popup_child_courses">{{ child.remaining_courses || 0 }}课时</view>
</view>
</view>
</view>
</view>
<!-- 当前选中孩子的统计信息 -->
<view class="selected_child_section" v-if="selectedChild">
<view class="selected_child_header">
<view class="selected_child_info">
<view class="selected_child_name">{{ selectedChild.name }}</view>
<view class="selected_child_details">
<text class="selected_detail_tag">{{ selectedChild.gender === 1 ? '男' : '女' }}</text>
<text class="selected_detail_tag">{{ Math.floor(selectedChild.age) }}</text>
<text class="selected_detail_tag">{{ selectedChild.campus_name || '未分配校区' }}</text>
</view>
</view>
<view class="switch_button" @click="openChildPopup">切换</view>
</view>
<view class="stats_grid">
<view class="stat_item">
<view class="stat_number">{{ selectedChild.total_courses || 0 }}</view>
<view class="stat_label">总课程</view>
</view>
<view class="stat_item">
<view class="stat_number">{{ selectedChild.completed_courses || 0 }}</view>
<view class="stat_label">已完成</view>
</view>
<view class="stat_item">
<view class="stat_number">{{ selectedChild.remaining_courses || 0 }}</view>
<view class="stat_label">剩余课时</view>
</view>
<view class="stat_item">
<view class="stat_number">{{ selectedChild.attendance_rate || 0 }}%</view>
<view class="stat_label">出勤率</view>
</view>
</view>
</view>
<!-- 功能菜单 - 九宫格样式 -->
<view class="main_section">
<view class="grid_container">
<view class="grid_item" @click="viewChildDetail">
<view class="grid_icon">
<image src="/static/icon-img/tou.png" class="icon_image"></image>
</view>
<view class="grid_text">孩子详情</view>
</view>
<view class="grid_item" @click="viewCourses">
<view class="grid_icon">
<image src="/static/icon-img/kkry.png" class="icon_image"></image>
</view>
<view class="grid_text">课程管理</view>
</view>
<view class="grid_item" @click="viewMaterials">
<view class="grid_icon">
<image src="/static/icon-img/liu.png" class="icon_image"></image>
</view>
<view class="grid_text">教学资料</view>
</view>
<view class="grid_item" @click="viewServices">
<view class="grid_icon">
<image src="/static/icon-img/notice.png" class="icon_image"></image>
</view>
<view class="grid_text">服务管理</view>
</view>
<view class="grid_item" @click="viewOrders">
<view class="grid_icon">
<image src="/static/icon-img/used.png" class="icon_image"></image>
</view>
<view class="grid_text">订单管理</view>
</view>
<view class="grid_item" @click="viewMessages">
<view class="grid_icon">
<image src="/static/icon-img/notice.png" class="icon_image"></image>
</view>
<view class="grid_text">消息管理</view>
</view>
<view class="grid_item" @click="viewContracts">
<view class="grid_icon">
<image src="/static/icon-img/warn.png" class="icon_image"></image>
</view>
<view class="grid_text">合同管理</view>
</view>
<!-- 空白占位保持九宫格对齐 -->
<view class="grid_item empty_item"></view>
<view class="grid_item empty_item"></view>
</view>
</view>
</view>
</template>
<script>
import { mapState, mapMutations } from 'vuex'
import apiRoute from '@/api/apiRoute.js'
export default {
data() {
return {
parentInfo: {},
childrenList: [],
showChildPopup: false,
loading: false
}
},
computed: {
...mapState(['selectedChild'])
},
onLoad() {
//
this.setUserRole('parent')
this.loadChildrenList()
console.log('用户信息页面加载完成')
},
onShow() {
//
console.log('用户信息页面显示')
},
methods: {
...mapMutations(['SET_USER_ROLE', 'SET_SELECTED_CHILD', 'SET_CHILDREN_LIST']),
setUserRole(role) {
this.SET_USER_ROLE(role)
},
async loadChildrenList() {
this.loading = true
try {
console.log('开始加载孩子列表...')
const response = await apiRoute.parent_getChildrenList()
console.log('孩子列表响应:', response)
if (response.code === 1) {
this.childrenList = response.data.data || []
this.parentInfo = response.data.parent_info || {}
this.SET_CHILDREN_LIST(this.childrenList)
console.log('加载到的孩子列表:', this.childrenList)
console.log('家长信息:', this.parentInfo)
//
if (!this.selectedChild && this.childrenList.length > 0) {
this.SET_SELECTED_CHILD(this.childrenList[0])
}
} else {
console.error('获取孩子列表失败:', response)
uni.showToast({
title: response.msg || '获取孩子列表失败',
icon: 'none'
})
}
} catch (error) {
console.error('获取孩子列表失败:', error)
uni.showToast({
title: '获取孩子列表失败',
icon: 'none'
})
} finally {
this.loading = false
}
},
openChildPopup() {
this.showChildPopup = true
},
closeChildPopup() {
this.showChildPopup = false
},
selectChildFromPopup(child) {
this.SET_SELECTED_CHILD(child)
console.log('选中孩子:', child)
this.closeChildPopup()
},
viewChildDetail() {
if (!this.selectedChild) {
uni.showToast({
title: '请先选择孩子',
icon: 'none'
})
return
}
uni.navigateTo({
url: `/pages/parent/user-info/child-detail?childId=${this.selectedChild.id}`
})
},
viewCourses() {
if (!this.selectedChild) {
uni.showToast({
title: '请先选择孩子',
icon: 'none'
})
return
}
uni.navigateTo({
url: `/pages/parent/courses/index?childId=${this.selectedChild.id}`
})
},
viewMaterials() {
if (!this.selectedChild) {
uni.showToast({
title: '请先选择孩子',
icon: 'none'
})
return
}
uni.navigateTo({
url: `/pages/parent/materials/index?childId=${this.selectedChild.id}`
})
},
viewServices() {
if (!this.selectedChild) {
uni.showToast({
title: '请先选择孩子',
icon: 'none'
})
return
}
uni.navigateTo({
url: `/pages/parent/services/index?childId=${this.selectedChild.id}`
})
},
viewOrders() {
if (!this.selectedChild) {
uni.showToast({
title: '请先选择孩子',
icon: 'none'
})
return
}
uni.navigateTo({
url: `/pages/parent/orders/index?childId=${this.selectedChild.id}`
})
},
viewMessages() {
if (!this.selectedChild) {
uni.showToast({
title: '请先选择孩子',
icon: 'none'
})
return
}
uni.navigateTo({
url: `/pages/parent/messages/index?childId=${this.selectedChild.id}`
})
},
viewContracts() {
if (!this.selectedChild) {
uni.showToast({
title: '请先选择孩子',
icon: 'none'
})
return
}
uni.navigateTo({
url: `/pages/parent/contracts/index?childId=${this.selectedChild.id}`
})
}
}
}
</script>
<style lang="less" scoped>
.main_box {
background: #f8f9fa;
min-height: 100vh;
}
//
.navbar_section {
display: flex;
justify-content: center;
align-items: center;
background: #29D3B4;
.title {
padding: 40rpx 20rpx;
//
// #ifdef MP-WEIXIN
padding: 80rpx 0 20rpx;
// #endif
font-size: 30rpx;
color: #fff;
}
}
//
.parent_info_section {
background: #fff;
padding: 32rpx;
display: flex;
justify-content: space-between;
align-items: center;
.parent_name {
font-size: 32rpx;
font-weight: 600;
color: #333;
}
.parent_phone {
font-size: 24rpx;
color: #999;
}
}
//
.child_popup {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
.popup_content {
background: #fff;
border-radius: 16rpx;
width: 80%;
max-height: 70vh;
overflow: hidden;
.popup_header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 32rpx;
border-bottom: 1px solid #f0f0f0;
.popup_title {
font-size: 32rpx;
font-weight: 600;
color: #333;
}
.popup_close {
font-size: 48rpx;
color: #999;
font-weight: 300;
}
}
.popup_children_list {
max-height: 60vh;
overflow-y: auto;
.popup_child_item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24rpx 32rpx;
border-bottom: 1px solid #f8f9fa;
&.selected {
background: rgba(41, 211, 180, 0.1);
}
.popup_child_info {
flex: 1;
.popup_child_name {
font-size: 28rpx;
font-weight: 600;
color: #333;
margin-bottom: 8rpx;
}
.popup_child_details {
display: flex;
gap: 12rpx;
margin-bottom: 8rpx;
.popup_detail_tag {
font-size: 22rpx;
color: #666;
background: #f0f0f0;
padding: 2rpx 8rpx;
border-radius: 8rpx;
}
}
.popup_child_campus, .popup_child_class {
font-size: 22rpx;
color: #999;
margin-bottom: 4rpx;
}
}
.popup_child_courses {
font-size: 24rpx;
color: #29d3b4;
font-weight: 600;
}
}
}
}
}
//
.selected_child_section {
background: #fff;
margin: 20rpx;
border-radius: 16rpx;
padding: 32rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
.selected_child_header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 32rpx;
.selected_child_info {
flex: 1;
.selected_child_name {
font-size: 32rpx;
font-weight: 600;
color: #333;
margin-bottom: 8rpx;
}
.selected_child_details {
display: flex;
gap: 12rpx;
.selected_detail_tag {
font-size: 22rpx;
color: #666;
background: #f0f0f0;
padding: 4rpx 12rpx;
border-radius: 12rpx;
}
}
}
.switch_button {
background: #29d3b4;
color: #fff;
padding: 12rpx 24rpx;
border-radius: 20rpx;
font-size: 24rpx;
font-weight: 600;
}
}
.stats_grid {
display: flex;
justify-content: space-between;
align-items: center;
.stat_item {
display: flex;
flex-direction: column;
align-items: center;
gap: 8rpx;
.stat_number {
color: #29D3B4;
font-size: 48rpx;
font-weight: 600;
}
.stat_label {
color: #AAAAAA;
font-size: 24rpx;
}
}
}
}
.main_section {
margin-top: 20rpx;
background: #f8f9fa;
padding: 0 24rpx;
padding-top: 40rpx;
padding-bottom: 150rpx;
.grid_container {
background: #fff;
border-radius: 16rpx;
padding: 40rpx 20rpx;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
.grid_item {
width: 30%;
margin-bottom: 40rpx;
display: flex;
flex-direction: column;
align-items: center;
&.empty_item {
//
}
.grid_icon {
width: 88rpx;
height: 88rpx;
border-radius: 50%;
background: rgba(41, 211, 180, 0.1);
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 16rpx;
.icon_image {
width: 48rpx;
height: 48rpx;
}
}
.grid_text {
font-size: 26rpx;
color: #333;
text-align: center;
line-height: 1.2;
}
}
}
}
//
.arrow-icon {
width: 24rpx;
height: 24rpx;
opacity: 0.6;
}
</style>

23
uniapp/pages/student/login/login.vue

@ -158,9 +158,26 @@
// //
res = await apiRoute.personnelLogin(params); res = await apiRoute.personnelLogin(params);
} else { } else {
// // -
//res = await apiRoute.xy_login(params); uni.showToast({
this.openViewHome(); title: '登录成功',
icon: 'success'
});
// token
uni.setStorageSync("token", "mock_token_" + Date.now());
uni.setStorageSync("userType", "4");
uni.setStorageSync("userInfo", {
id: 1001,
name: this.user,
phone: this.user,
role: 'parent'
});
//
uni.redirectTo({
url: '/pages/parent/user-info/index'
});
return; return;
} }

23
uniapp/store/index.js

@ -24,6 +24,10 @@ const store = new Vuex.Store({
}], }],
address: {}, address: {},
remark: '不打包', remark: '不打包',
// 家长端状态管理
userRole: 'parent', // 用户角色: parent-家长, student-学生
childrenList: [], // 孩子列表
selectedChild: null, // 当前选中的孩子
}, },
mutations: { mutations: {
SET_ORDER_TYPE(state, orderType) { SET_ORDER_TYPE(state, orderType) {
@ -44,6 +48,25 @@ const store = new Vuex.Store({
setCarbarData(state, data) { setCarbarData(state, data) {
state.carbarData = data state.carbarData = data
}, },
// 家长端状态管理mutations
SET_USER_ROLE(state, userRole) {
state.userRole = userRole
},
SET_CHILDREN_LIST(state, childrenList) {
state.childrenList = childrenList
},
SET_SELECTED_CHILD(state, selectedChild) {
state.selectedChild = selectedChild
},
// 添加或更新孩子信息
UPDATE_CHILD_INFO(state, childInfo) {
const index = state.childrenList.findIndex(child => child.id === childInfo.id)
if (index !== -1) {
state.childrenList.splice(index, 1, childInfo)
} else {
state.childrenList.push(childInfo)
}
},
}, },
}) })

Loading…
Cancel
Save