Browse Source

修改 bug

master
王泽彦 9 months ago
parent
commit
b075e5bd1e
  1. 274
      uniapp/pages/coach/my/service_detail.vue
  2. 512
      uniapp/pages/coach/my/service_list.vue
  3. 499
      uniapp/pages/coach/student/student_detail.vue
  4. 216
      uniapp/pages/coach/student/student_list.vue
  5. 239
      uniapp/pages/market/clue/class_arrangement.vue
  6. 490
      uniapp/pages/market/clue/class_arrangement_detail.vue
  7. 343
      uniapp/pages/market/data/statistics.vue
  8. 390
      uniapp/pages/market/home/index.vue
  9. 272
      uniapp/pages/market/reimbursement/add.vue
  10. 136
      uniapp/pages/market/reimbursement/detail.vue
  11. 144
      uniapp/pages/market/reimbursement/list.vue

274
uniapp/pages/coach/my/service_detail.vue

@ -0,0 +1,274 @@
<!--服务详情页面-->
<template>
<view class="container">
<view class="main-content">
<view class="service-header">
<view class="service-title">我的服务详情</view>
<view class="service-subtitle">查看当前服务状态和详细信息</view>
</view>
<view class="service-cards">
<view class="service-card" v-for="(service, index) in serviceList" :key="index">
<view class="card-header">
<view class="service-name">{{ service.name }}</view>
<view class="service-status" :class="service.status === '正常' ? 'status-active' : 'status-inactive'">
{{ service.status }}
</view>
</view>
<view class="card-content">
<view class="service-info">
<view class="info-item">
<text class="label">服务类型</text>
<text class="value">{{ service.type }}</text>
</view>
<view class="info-item">
<text class="label">开始时间</text>
<text class="value">{{ service.startTime }}</text>
</view>
<view class="info-item">
<text class="label">结束时间</text>
<text class="value">{{ service.endTime }}</text>
</view>
<view class="info-item">
<text class="label">服务描述</text>
<text class="value">{{ service.description }}</text>
</view>
</view>
</view>
<view class="card-actions">
<view class="btn btn-detail" @click="viewDetail(service)">查看详情</view>
<view class="btn btn-contact" @click="contactService(service)">联系客服</view>
</view>
</view>
</view>
<view class="empty-state" v-if="serviceList.length === 0">
<image class="empty-icon" src="/static/icon-img/empty.png"></image>
<view class="empty-text">暂无服务记录</view>
</view>
</view>
</view>
</template>
<script>
import apiRoute from '@/api/apiRoute.js';
export default {
data() {
return {
serviceList: []
}
},
onLoad() {
this.init();
},
methods: {
async init() {
this.getServiceList();
},
//
async getServiceList() {
try {
// API
this.serviceList = [
{
id: 1,
name: '教练服务套餐A',
type: '专业训练',
status: '正常',
startTime: '2024-01-01',
endTime: '2024-12-31',
description: '专业体能训练指导服务'
},
{
id: 2,
name: '教练服务套餐B',
type: '基础指导',
status: '即将到期',
startTime: '2024-06-01',
endTime: '2024-06-30',
description: '基础动作指导和纠正'
}
];
// API
// let res = await apiRoute.getServiceList({});
// if (res.code === 1) {
// this.serviceList = res.data;
// }
} catch (error) {
console.error('获取服务列表失败:', error);
uni.showToast({
title: '获取数据失败',
icon: 'none'
});
}
},
//
viewDetail(service) {
uni.showModal({
title: '服务详情',
content: `服务名称:${service.name}\n服务类型:${service.type}\n状态:${service.status}\n描述:${service.description}`,
showCancel: false
});
},
//
contactService(service) {
uni.showActionSheet({
itemList: ['电话客服', '在线客服', '邮件客服'],
success: (res) => {
const actions = ['拨打客服电话', '打开在线客服', '发送邮件'];
uni.showToast({
title: actions[res.tapIndex],
icon: 'none'
});
}
});
}
}
}
</script>
<style lang="less" scoped>
.container {
background: #f5f5f5;
min-height: 100vh;
}
.main-content {
padding: 20rpx;
}
.service-header {
background: #29d3b4;
border-radius: 16rpx;
padding: 40rpx;
margin-bottom: 30rpx;
color: white;
.service-title {
font-size: 36rpx;
font-weight: bold;
margin-bottom: 10rpx;
}
.service-subtitle {
font-size: 26rpx;
opacity: 0.8;
}
}
.service-cards {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.service-card {
background: white;
border-radius: 16rpx;
padding: 30rpx;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
.service-name {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.service-status {
padding: 8rpx 16rpx;
border-radius: 20rpx;
font-size: 24rpx;
&.status-active {
background: #e8f5e8;
color: #52c41a;
}
&.status-inactive {
background: #fff2e8;
color: #fa8c16;
}
}
}
.card-content {
margin-bottom: 25rpx;
}
.service-info {
display: flex;
flex-direction: column;
gap: 15rpx;
}
.info-item {
display: flex;
.label {
color: #666;
font-size: 28rpx;
min-width: 160rpx;
}
.value {
color: #333;
font-size: 28rpx;
flex: 1;
}
}
.card-actions {
display: flex;
gap: 20rpx;
}
.btn {
flex: 1;
padding: 20rpx;
border-radius: 12rpx;
text-align: center;
font-size: 28rpx;
&.btn-detail {
background: #29d3b4;
color: white;
}
&.btn-contact {
background: #f0f0f0;
color: #333;
}
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 0;
.empty-icon {
width: 120rpx;
height: 120rpx;
margin-bottom: 30rpx;
opacity: 0.3;
}
.empty-text {
color: #999;
font-size: 28rpx;
}
}
</style>

512
uniapp/pages/coach/my/service_list.vue

@ -0,0 +1,512 @@
<!--服务列表页面-->
<template>
<view class="container">
<view class="header">
<view class="search-bar">
<view class="search-input">
<input
placeholder="搜索服务..."
v-model="searchText"
@input="handleSearch"
/>
</view>
<view class="filter-btn" @click="showFilter">
<text>筛选</text>
</view>
</view>
</view>
<view class="service-list">
<view class="service-item"
v-for="(service, index) in filteredServiceList"
:key="index"
@click="goToDetail(service)">
<view class="service-header">
<view class="service-title">{{ service.name }}</view>
<view class="service-badge" :class="[
service.status === '正常' ? 'badge-success' : '',
service.status === '即将到期' ? 'badge-warning' : '',
service.status === '已过期' ? 'badge-danger' : '',
!['正常', '即将到期', '已过期'].includes(service.status) ? 'badge-default' : ''
]">
{{ service.status }}
</view>
</view>
<view class="service-content">
<view class="service-meta">
<view class="meta-item">
<text class="meta-label">类型</text>
<text class="meta-value">{{ service.type }}</text>
</view>
<view class="meta-item">
<text class="meta-label">时长</text>
<text class="meta-value">{{ service.duration }}</text>
</view>
</view>
<view class="service-desc">{{ service.description }}</view>
<view class="service-footer">
<view class="service-time">
{{ service.startTime }} - {{ service.endTime }}
</view>
<view class="service-action">
<text class="action-text">查看详情</text>
<text class="action-arrow">></text>
</view>
</view>
</view>
</view>
</view>
<view class="empty-state" v-if="filteredServiceList.length === 0">
<image class="empty-icon" src="/static/icon-img/empty.png"></image>
<view class="empty-text">{{ searchText ? '未找到相关服务' : '暂无服务记录' }}</view>
</view>
<!-- 筛选弹窗 -->
<fui-bottom-popup v-model="showFilterPopup" :zIndex="9999">
<view class="filter-popup">
<view class="popup-header">
<view class="popup-title">筛选条件</view>
<view class="popup-close" @click="showFilterPopup = false"></view>
</view>
<view class="filter-content">
<view class="filter-group">
<view class="filter-title">服务状态</view>
<view class="filter-options">
<view
class="filter-option"
:class="{ active: selectedStatus === '' }"
@click="selectedStatus = ''"
>
全部
</view>
<view
class="filter-option"
:class="{ active: selectedStatus === '正常' }"
@click="selectedStatus = '正常'"
>
正常
</view>
<view
class="filter-option"
:class="{ active: selectedStatus === '即将到期' }"
@click="selectedStatus = '即将到期'"
>
即将到期
</view>
<view
class="filter-option"
:class="{ active: selectedStatus === '已过期' }"
@click="selectedStatus = '已过期'"
>
已过期
</view>
</view>
</view>
</view>
<view class="popup-actions">
<view class="btn btn-reset" @click="resetFilter">重置</view>
<view class="btn btn-confirm" @click="applyFilter">确定</view>
</view>
</view>
</fui-bottom-popup>
</view>
</template>
<script>
import apiRoute from '@/api/apiRoute.js';
export default {
data() {
return {
serviceList: [],
filteredServiceList: [],
searchText: '',
showFilterPopup: false,
selectedStatus: ''
}
},
onLoad() {
this.init();
},
methods: {
async init() {
this.getServiceList();
},
//
async getServiceList() {
try {
// API
this.serviceList = [
{
id: 1,
name: '专业体能训练服务',
type: '体能训练',
status: '正常',
duration: '3个月',
startTime: '2024-01-01',
endTime: '2024-03-31',
description: '专业的体能训练指导,包含有氧运动、力量训练等综合项目'
},
{
id: 2,
name: '基础动作指导服务',
type: '技术指导',
status: '即将到期',
duration: '1个月',
startTime: '2024-06-01',
endTime: '2024-06-30',
description: '针对基础动作的专业指导和纠正'
},
{
id: 3,
name: '营养咨询服务',
type: '营养指导',
status: '正常',
duration: '6个月',
startTime: '2024-01-15',
endTime: '2024-07-15',
description: '专业营养师提供个性化营养方案和饮食建议'
},
{
id: 4,
name: '康复训练服务',
type: '康复指导',
status: '已过期',
duration: '2个月',
startTime: '2023-10-01',
endTime: '2023-11-30',
description: '运动损伤康复和预防性训练指导'
}
];
this.filteredServiceList = [...this.serviceList];
// API
// let res = await apiRoute.getServiceList({});
// if (res.code === 1) {
// this.serviceList = res.data;
// this.filteredServiceList = [...this.serviceList];
// }
} catch (error) {
console.error('获取服务列表失败:', error);
uni.showToast({
title: '获取数据失败',
icon: 'none'
});
}
},
//
handleSearch() {
this.filterServices();
},
//
filterServices() {
let filtered = [...this.serviceList];
//
if (this.searchText) {
filtered = filtered.filter(service =>
service.name.includes(this.searchText) ||
service.type.includes(this.searchText) ||
service.description.includes(this.searchText)
);
}
//
if (this.selectedStatus) {
filtered = filtered.filter(service => service.status === this.selectedStatus);
}
this.filteredServiceList = filtered;
},
//
showFilter() {
this.showFilterPopup = true;
},
//
resetFilter() {
this.selectedStatus = '';
this.filterServices();
},
//
applyFilter() {
this.filterServices();
this.showFilterPopup = false;
},
//
goToDetail(service) {
this.$navigateTo({
url: `/pages/coach/my/service_detail?id=${service.id}`
});
}
}
}
</script>
<style lang="less" scoped>
.container {
background: #f5f5f5;
min-height: 100vh;
}
.header {
background: white;
padding: 20rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
}
.search-bar {
display: flex;
gap: 20rpx;
align-items: center;
}
.search-input {
flex: 1;
background: #f5f5f5;
border-radius: 25rpx;
padding: 15rpx 30rpx;
input {
width: 100%;
font-size: 28rpx;
}
}
.filter-btn {
background: #29d3b4;
color: white;
padding: 15rpx 30rpx;
border-radius: 25rpx;
font-size: 28rpx;
}
.service-list {
padding: 20rpx;
display: flex;
flex-direction: column;
gap: 20rpx;
}
.service-item {
background: white;
border-radius: 16rpx;
padding: 30rpx;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
}
.service-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
}
.service-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
flex: 1;
margin-right: 20rpx;
}
.service-badge {
padding: 8rpx 16rpx;
border-radius: 20rpx;
font-size: 24rpx;
&.badge-success {
background: #e8f5e8;
color: #52c41a;
}
&.badge-warning {
background: #fff2e8;
color: #fa8c16;
}
&.badge-danger {
background: #fff1f0;
color: #ff4d4f;
}
&.badge-default {
background: #f0f0f0;
color: #666;
}
}
.service-content {
display: flex;
flex-direction: column;
gap: 15rpx;
}
.service-meta {
display: flex;
gap: 30rpx;
}
.meta-item {
display: flex;
align-items: center;
.meta-label {
color: #666;
font-size: 26rpx;
}
.meta-value {
color: #333;
font-size: 26rpx;
}
}
.service-desc {
color: #666;
font-size: 26rpx;
line-height: 1.5;
}
.service-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 15rpx;
border-top: 1px solid #f0f0f0;
}
.service-time {
color: #999;
font-size: 24rpx;
}
.service-action {
display: flex;
align-items: center;
color: #29d3b4;
font-size: 26rpx;
.action-arrow {
margin-left: 10rpx;
}
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 0;
.empty-icon {
width: 120rpx;
height: 120rpx;
margin-bottom: 30rpx;
opacity: 0.3;
}
.empty-text {
color: #999;
font-size: 28rpx;
}
}
//
.filter-popup {
background: white;
border-radius: 20rpx 20rpx 0 0;
max-height: 80vh;
}
.popup-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 30rpx;
border-bottom: 1px solid #f0f0f0;
}
.popup-title {
font-size: 32rpx;
font-weight: bold;
}
.popup-close {
font-size: 36rpx;
color: #999;
}
.filter-content {
padding: 30rpx;
}
.filter-group {
margin-bottom: 40rpx;
}
.filter-title {
font-size: 28rpx;
font-weight: bold;
margin-bottom: 20rpx;
color: #333;
}
.filter-options {
display: flex;
flex-wrap: wrap;
gap: 20rpx;
}
.filter-option {
padding: 15rpx 30rpx;
background: #f5f5f5;
border-radius: 25rpx;
font-size: 26rpx;
color: #666;
&.active {
background: #29d3b4;
color: white;
}
}
.popup-actions {
display: flex;
gap: 20rpx;
padding: 30rpx;
border-top: 1px solid #f0f0f0;
}
.btn {
flex: 1;
padding: 20rpx;
border-radius: 12rpx;
text-align: center;
font-size: 28rpx;
&.btn-reset {
background: #f0f0f0;
color: #333;
}
&.btn-confirm {
background: #29d3b4;
color: white;
}
}
</style>

499
uniapp/pages/coach/student/student_detail.vue

@ -0,0 +1,499 @@
<template>
<view class="container">
<uni-nav-bar fixed status-bar left-icon="left" title="学员详情" @clickLeft="navigateBack"></uni-nav-bar>
<view class="content" v-if="studentInfo">
<view class="student-header">
<view class="avatar-box">
<image :src="studentInfo.avatar || '/static/icon-img/avatar.png'" mode="aspectFill" class="avatar-img"></image>
</view>
<view class="name-box">
<text class="name">{{studentInfo.name}}</text>
<text class="id">ID: {{studentInfo.id}}</text>
</view>
</view>
<view class="card-section">
<view class="section-title">基本信息</view>
<view class="info-card">
<view class="info-item">
<text class="item-label">所属校区</text>
<text class="item-value">{{studentInfo.campus}}</text>
</view>
<view class="info-item">
<text class="item-label">联系电话</text>
<text class="item-value">{{studentInfo.phone}}</text>
</view>
<view class="info-item">
<text class="item-label">年龄</text>
<text class="item-value">{{studentInfo.age}}</text>
</view>
<view class="info-item">
<text class="item-label">入学时间</text>
<text class="item-value">{{studentInfo.enrollmentDate}}</text>
</view>
</view>
</view>
<view class="card-section">
<view class="section-title">课程信息</view>
<view class="info-card">
<view class="info-item">
<text class="item-label">剩余课程</text>
<text class="item-value highlight">{{studentInfo.remainingCourses}}</text>
</view>
<view class="info-item">
<text class="item-label">课程到期时间</text>
<text class="item-value highlight">{{studentInfo.expiryDate}}</text>
</view>
<view class="info-item">
<text class="item-label">已消课程</text>
<text class="item-value">{{studentInfo.completedCourses}}</text>
</view>
<view class="info-item">
<text class="item-label">报名课程</text>
<text class="item-value">{{studentInfo.courseName}}</text>
</view>
</view>
</view>
<view class="card-section">
<view class="section-title">最近上课记录</view>
<view class="info-card" v-if="studentInfo.recentClasses && studentInfo.recentClasses.length > 0">
<view class="class-item" v-for="(item, index) in studentInfo.recentClasses" :key="index">
<view class="class-date">{{item.date}}</view>
<view class="class-info">
<text class="class-name">{{item.courseName}}</text>
<text class="class-time">{{item.timeSlot}}</text>
</view>
</view>
</view>
<view class="empty-class" v-else>
<text>暂无上课记录</text>
</view>
</view>
<view class="btn-group">
<button class="btn primary" @click="contactStudent">联系学员</button>
<button class="btn secondary" @click="viewSchedule">查看课表</button>
</view>
</view>
<view v-else class="loading-box">
<text>加载中...</text>
</view>
</view>
</template>
<script>
export default {
data() {
return {
id: null,
studentInfo: null
}
},
onLoad(options) {
if (options.id) {
this.id = options.id;
this.getStudentDetail();
} else {
uni.showToast({
title: '参数错误',
icon: 'none'
});
setTimeout(() => {
this.navigateBack();
}, 1500);
}
},
methods: {
navigateBack() {
uni.navigateBack();
},
getStudentDetail() {
// API
// try {
// const res = await memberApi.getStudentDetail({id: this.id});
// if(res.code == 1) {
// this.studentInfo = res.data || null;
// } else {
// uni.showToast({
// title: res.msg || '',
// icon: 'none'
// });
// }
// } catch(error) {
// console.error(':', error);
// uni.showToast({
// title: '',
// icon: 'none'
// });
// }
// 使
setTimeout(() => {
//
const studentData = {
'1': {
id: '1',
name: '张三',
avatar: '/static/icon-img/avatar.png',
campus: '总部校区',
phone: '13812341000',
age: 11,
enrollmentDate: '2023-01-11',
remainingCourses: 10,
expiryDate: '2023-12-31',
completedCourses: 20,
courseName: '少儿英语基础班',
recentClasses: [
{
date: '2023-09-15',
courseName: '少儿英语基础班',
timeSlot: '15:30-17:00'
},
{
date: '2023-09-08',
courseName: '少儿英语基础班',
timeSlot: '15:30-17:00'
},
{
date: '2023-09-01',
courseName: '少儿英语基础班',
timeSlot: '15:30-17:00'
}
]
},
'2': {
id: '2',
name: '李四',
avatar: '/static/icon-img/avatar.png',
campus: '西区校区',
phone: '13812341001',
age: 12,
enrollmentDate: '2023-01-12',
remainingCourses: 5,
expiryDate: '2023-11-15',
completedCourses: 25,
courseName: '少儿英语进阶班',
recentClasses: [
{
date: '2023-09-14',
courseName: '少儿英语进阶班',
timeSlot: '14:00-15:30'
},
{
date: '2023-09-07',
courseName: '少儿英语进阶班',
timeSlot: '14:00-15:30'
}
]
},
'3': {
id: '3',
name: '王五',
avatar: '/static/icon-img/avatar.png',
campus: '东区校区',
phone: '13812341002',
age: 13,
enrollmentDate: '2023-01-13',
remainingCourses: 15,
expiryDate: '2024-01-20',
completedCourses: 15,
courseName: '少儿英语口语班',
recentClasses: [
{
date: '2023-09-16',
courseName: '少儿英语口语班',
timeSlot: '10:00-11:30'
},
{
date: '2023-09-09',
courseName: '少儿英语口语班',
timeSlot: '10:00-11:30'
},
{
date: '2023-09-02',
courseName: '少儿英语口语班',
timeSlot: '10:00-11:30'
}
]
},
'4': {
id: '4',
name: '赵六',
avatar: '/static/icon-img/avatar.png',
campus: '南区校区',
phone: '13812341003',
age: 10,
enrollmentDate: '2023-02-15',
remainingCourses: 8,
expiryDate: '2023-11-30',
completedCourses: 12,
courseName: '少儿英语基础班',
recentClasses: [
{
date: '2023-09-13',
courseName: '少儿英语基础班',
timeSlot: '16:00-17:30'
},
{
date: '2023-09-06',
courseName: '少儿英语基础班',
timeSlot: '16:00-17:30'
}
]
},
'5': {
id: '5',
name: '刘七',
avatar: '/static/icon-img/avatar.png',
campus: '北区校区',
phone: '13812341004',
age: 14,
enrollmentDate: '2023-03-20',
remainingCourses: 20,
expiryDate: '2024-02-15',
completedCourses: 10,
courseName: '少儿英语进阶班',
recentClasses: [
{
date: '2023-09-12',
courseName: '少儿英语进阶班',
timeSlot: '17:00-18:30'
},
{
date: '2023-09-05',
courseName: '少儿英语进阶班',
timeSlot: '17:00-18:30'
}
]
},
'6': {
id: '6',
name: '陈八',
avatar: '/static/icon-img/avatar.png',
campus: '总部校区',
phone: '13812341005',
age: 9,
enrollmentDate: '2023-04-05',
remainingCourses: 3,
expiryDate: '2023-10-30',
completedCourses: 27,
courseName: '少儿英语口语班',
recentClasses: [
{
date: '2023-09-11',
courseName: '少儿英语口语班',
timeSlot: '14:30-16:00'
},
{
date: '2023-09-04',
courseName: '少儿英语口语班',
timeSlot: '14:30-16:00'
}
]
}
};
// ID
this.studentInfo = studentData[this.id] || {
id: this.id,
name: '未知学员',
avatar: '/static/icon-img/avatar.png',
campus: '未知校区',
phone: '暂无',
age: '暂无',
enrollmentDate: '暂无',
remainingCourses: 0,
expiryDate: '暂无',
completedCourses: 0,
courseName: '暂无',
recentClasses: []
};
}, 500);
},
contactStudent() {
if (this.studentInfo && this.studentInfo.phone) {
uni.makePhoneCall({
phoneNumber: this.studentInfo.phone.replace(/\*/g, '0')
});
}
},
viewSchedule() {
uni.navigateTo({
url: `/pages/coach/student/timetable?id=${this.id}`
});
}
}
}
</script>
<style lang="scss">
.container {
min-height: 100vh;
background-color: #F5F5F5;
}
.content {
padding: 20rpx;
}
.loading-box {
display: flex;
justify-content: center;
align-items: center;
height: 80vh;
color: #999;
font-size: 28rpx;
}
.student-header {
display: flex;
align-items: center;
background-color: #FFFFFF;
border-radius: 12rpx;
padding: 40rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
.avatar-box {
width: 150rpx;
height: 150rpx;
border-radius: 75rpx;
overflow: hidden;
margin-right: 30rpx;
.avatar-img {
width: 100%;
height: 100%;
}
}
.name-box {
display: flex;
flex-direction: column;
.name {
font-size: 36rpx;
font-weight: bold;
color: #333;
margin-bottom: 10rpx;
}
.id {
font-size: 24rpx;
color: #999;
}
}
}
.card-section {
margin-bottom: 20rpx;
.section-title {
font-size: 30rpx;
font-weight: bold;
color: #333;
margin: 30rpx 10rpx 20rpx;
}
.info-card {
background-color: #FFFFFF;
border-radius: 12rpx;
padding: 20rpx 30rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
}
.info-item {
display: flex;
justify-content: space-between;
padding: 20rpx 0;
border-bottom: 1px solid #F5F5F5;
&:last-child {
border-bottom: none;
}
.item-label {
color: #666;
font-size: 28rpx;
}
.item-value {
color: #333;
font-size: 28rpx;
&.highlight {
color: #FF6600;
font-weight: bold;
}
}
}
.class-item {
display: flex;
padding: 20rpx 0;
border-bottom: 1px solid #F5F5F5;
&:last-child {
border-bottom: none;
}
.class-date {
width: 180rpx;
font-size: 28rpx;
color: #666;
}
.class-info {
flex: 1;
display: flex;
flex-direction: column;
.class-name {
font-size: 28rpx;
color: #333;
margin-bottom: 10rpx;
}
.class-time {
font-size: 24rpx;
color: #999;
}
}
}
.empty-class {
padding: 40rpx 0;
text-align: center;
color: #999;
font-size: 28rpx;
}
}
.btn-group {
display: flex;
justify-content: space-between;
margin: 40rpx 0;
.btn {
width: 48%;
height: 80rpx;
line-height: 80rpx;
border-radius: 40rpx;
font-size: 30rpx;
&.primary {
background-color: #3B7CF9;
color: #FFFFFF;
}
&.secondary {
background-color: #FFFFFF;
color: #3B7CF9;
border: 1px solid #3B7CF9;
}
}
}
</style>

216
uniapp/pages/coach/student/student_list.vue

@ -0,0 +1,216 @@
<template>
<view class="container">
<view class="content">
<view v-if="studentList.length === 0" class="empty-box">
<image src="/static/icon-img/empty.png" mode="aspectFit" class="empty-img"></image>
<text class="empty-text">暂无学员数据</text>
</view>
<view v-else class="student-list">
<view v-for="(item, index) in studentList" :key="index" class="student-item" @click="goToDetail(item)">
<view class="student-card">
<view class="student-avatar">
<image :src="item.avatar || '/static/icon-img/avatar.png'" mode="aspectFill" class="avatar-img"></image>
</view>
<view class="student-info">
<view class="student-name">{{item.name}}</view>
<view class="info-row">
<text class="info-label">所属校区</text>
<text class="info-value">{{item.campus}}</text>
</view>
<view class="info-row">
<text class="info-label">剩余课程</text>
<text class="info-value">{{item.remainingCourses}}</text>
</view>
<view class="info-row">
<text class="info-label">到期时间</text>
<text class="info-value">{{item.expiryDate}}</text>
</view>
</view>
<view class="arrow-right">
<uni-icons type="right" size="16" color="#CCCCCC"></uni-icons>
</view>
</view>
</view>
</view>
</view>
<AQTabber />
</view>
</template>
<script>
import AQTabber from "@/components/AQ/AQTabber.vue"
export default {
components: {
AQTabber,
},
data() {
return {
studentList: []
}
},
onLoad() {
this.getStudentList();
},
methods: {
navigateBack() {
uni.navigateBack();
},
getStudentList() {
// API
// const res = await memberApi.getStudentList({});
// if(res.code == 1) {
// this.studentList = res.data || [];
// } else {
// uni.showToast({
// title: res.msg || '',
// icon: 'none'
// });
// }
// 使
this.studentList = [
{
id: 1,
name: '张三',
avatar: '/static/icon-img/avatar.png',
campus: '总部校区',
remainingCourses: 10,
expiryDate: '2023-12-31'
},
{
id: 2,
name: '李四',
avatar: '/static/icon-img/avatar.png',
campus: '西区校区',
remainingCourses: 5,
expiryDate: '2023-11-15'
},
{
id: 3,
name: '王五',
avatar: '/static/icon-img/avatar.png',
campus: '东区校区',
remainingCourses: 15,
expiryDate: '2024-01-20'
},
{
id: 4,
name: '赵六',
avatar: '/static/icon-img/avatar.png',
campus: '南区校区',
remainingCourses: 8,
expiryDate: '2023-11-30'
},
{
id: 5,
name: '刘七',
avatar: '/static/icon-img/avatar.png',
campus: '北区校区',
remainingCourses: 20,
expiryDate: '2024-02-15'
},
{
id: 6,
name: '陈八',
avatar: '/static/icon-img/avatar.png',
campus: '总部校区',
remainingCourses: 3,
expiryDate: '2023-10-30'
}
];
},
goToDetail(student) {
uni.navigateTo({
url: `/pages/market/clue/clue_info?resource_sharing_id=25`
});
}
}
}
</script>
<style lang="scss">
.container {
min-height: 100vh;
background-color: #F5F5F5;
}
.content {
padding: 20rpx;
}
.empty-box {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding-top: 200rpx;
.empty-img {
width: 200rpx;
height: 200rpx;
}
.empty-text {
margin-top: 20rpx;
font-size: 28rpx;
color: #999;
}
}
.student-list {
.student-item {
margin-bottom: 20rpx;
}
.student-card {
display: flex;
align-items: center;
background-color: #FFFFFF;
border-radius: 12rpx;
padding: 30rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
}
.student-avatar {
width: 120rpx;
height: 120rpx;
border-radius: 60rpx;
overflow: hidden;
margin-right: 30rpx;
.avatar-img {
width: 100%;
height: 100%;
}
}
.student-info {
flex: 1;
}
.student-name {
font-size: 32rpx;
font-weight: bold;
margin-bottom: 10rpx;
color: #333;
}
.info-row {
display: flex;
font-size: 26rpx;
margin-top: 8rpx;
.info-label {
color: #666;
}
.info-value {
color: #333;
}
}
.arrow-right {
padding-left: 20rpx;
}
}
</style>

239
uniapp/pages/market/clue/class_arrangement.vue

@ -0,0 +1,239 @@
<template>
<view class="class-arrange-root">
<!-- 顶部日期选择 -->
<view class="date-bar">
<view class="date-item" v-for="(item, idx) in weekList" :key="idx" :class="{active: item.status}" @click="getDate(item.date,item.day)">
<view class="week">{{ item.week }}</view>
<view class="day">{{ item.day }}</view>
</view>
</view>
<!-- "查看更多"按钮移到日期条下方 -->
<view class="more-bar-wrapper">
<view class="more-bar" @click="openCalendar">
<text>查看更多</text>
<uni-icons type="arrowdown" size="18" color="#bdbdbd" />
</view>
</view>
<!-- 日历底部弹窗 -->
<uni-popup ref="calendarPopup" type="bottom">
<uni-calendar @change="onCalendarChange" />
</uni-popup>
<!-- 课程卡片列表 -->
<view class="course-list">
<view class="course-card" v-for="(course, idx) in courseList" :key="idx">
<view class="card-header">
<view class="status-end">{{ getStatusText(course.status) }}</view>
</view>
<view class="card-body">
<view class="row">时间{{ course.course_date }}</view>
<view class="row">校区{{ course.campus_name }}</view>
<view class="row">教室{{ course.venue.venue_name }}</view>
<view class="row">课程{{ course.course.course_name }}</view>
<view class="row">人数{{ course.available_capacity }}</view>
</view>
<view class="card-footer">
<view class="sign-info"></view>
<button class="detail-btn" @click="viewDetail(course)">详情</button>
</view>
</view>
</view>
</view>
</template>
<script>
import apiRoute from '@/api/apiRoute.js';
export default {
data() {
return {
weekList: [],
selectedDayIndex: 4,
date: '',
courseList: [],
resource_id:''
};
},
onLoad(options) {
this.resource_id = options.resource_id
this.getDate();
},
methods: {
async getDate(date = '', day = '') {
try {
let res = await apiRoute.getDate({
'date': date,
'day': day
})
this.weekList = res.data.dates
this.date = res.data.date
let data = await apiRoute.courseAllList({
'schedule_date': this.date
})
this.courseList = data.data
} catch (error) {
console.error('获取信息失败:', error);
}
},
openCalendar() {
this.$refs.calendarPopup.open();
},
closeCalendar() {
console.log(123123)
this.$refs.calendarPopup.close();
},
viewDetail(course) {
//
this.$navigateTo({
url: '/pages/market/clue/class_arrangement_detail?id=' + course.id+'&resource_id='+this.resource_id
});
},
onCalendarConfirm(e) {
// e.fulldate
uni.showToast({
title: '选择日期:' + e.fulldate,
icon: 'none'
});
this.closeCalendar();
},
onCalendarChange(e) {
this.getDate(e.fulldate, e.date);
this.closeCalendar();
},
getStatusText(status) {
const statusMap = {
pending: '待开始',
upcoming: '即将开始',
ongoing: '进行中',
completed: '已结束'
};
return statusMap[status] || status;
}
},
};
</script>
<style lang="less" scoped>
.class-arrange-root {
background: #232323;
min-height: 100vh;
padding-bottom: 30rpx;
}
.date-bar {
display: flex;
align-items: center;
background: #232323;
padding: 0 0 10rpx 0;
overflow-x: auto;
border-bottom: 1px solid #333;
.date-item {
flex: 1;
text-align: center;
color: #bdbdbd;
padding: 16rpx 0 0 0;
.week {
font-size: 22rpx;
}
.day {
font-size: 28rpx;
margin-top: 4rpx;
}
&.active {
color: #29d3b4;
.day {
border-radius: 50%;
background: #333;
color: #29d3b4;
padding: 2rpx 10rpx;
}
}
}
}
.more-bar-wrapper {
width: 100%;
display: flex;
justify-content: center;
margin: 10rpx 0 0 0;
}
.more-bar {
display: flex;
align-items: center;
color: #bdbdbd;
font-size: 22rpx;
cursor: pointer;
background: #333;
border-radius: 20rpx;
padding: 8rpx 24rpx;
}
.course-list {
margin-top: 20rpx;
.course-card {
background: #434544;
border-radius: 10rpx;
margin: 0 0 20rpx 0;
padding: 0 0 20rpx 0;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.08);
.card-header {
display: flex;
justify-content: flex-end;
padding: 10rpx 20rpx 0 0;
.status-end {
background: #e95c6b;
color: #fff;
border-radius: 10rpx;
padding: 4rpx 18rpx;
font-size: 22rpx;
}
}
.card-body {
padding: 0 20rpx;
.row {
color: #fff;
font-size: 24rpx;
margin: 8rpx 0;
}
}
.card-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 20rpx;
.sign-info {
color: #bdbdbd;
font-size: 22rpx;
}
.detail-btn {
background: transparent;
border: 2rpx solid #ffd86b;
color: #ffd86b;
border-radius: 8rpx;
padding: 6rpx 24rpx;
font-size: 24rpx;
margin-left: 10rpx;
}
}
}
}
</style>

490
uniapp/pages/market/clue/class_arrangement_detail.vue

@ -0,0 +1,490 @@
<template>
<view class="detail-root">
<view class="header">
<view class="title">课程安排详情</view>
<view class="date">日期{{ course_info.course_date }} {{course_info.time_slot}}</view>
</view>
<view class="section">
<view class="section-title">学员列表</view>
<view class="student-list" v-if="course_info && course_info.available_capacity">
<!-- 显示已安排的学员 -->
<view
v-for="(stu, idx) in students"
:key="idx"
class="student-item"
@tap="viewStudent(stu)"
>
<view class="avatar">{{ stu.name && stu.name.charAt(0) }}</view>
<view class="info">
<view class="name">{{ stu.name }}</view>
<view class="desc">{{ getStatusText(stu.status) }}</view>
</view>
</view>
<!-- 显示空位 -->
<view
v-for="index in emptySeats"
:key="index"
class="student-item empty"
@tap="addStudent($event, index)"
>
<view class="avatar empty-avatar">+</view>
<view class="info">
<view class="name">空位</view>
<view class="desc">点击添加学员</view>
</view>
</view>
</view>
<!-- 没有数据时的占位区域 -->
<view class="empty-placeholder" v-else>
<image src="/static/icon-img/empty.png" mode="aspectFit"></image>
<text>暂无课程数据</text>
</view>
</view>
<!-- 请假原因弹窗 -->
<fui-modal ref="leaveReasonModal" :buttons="[]" width="600" title="请假申请">
<view class="leave-form">
<view class="leave-label">请假原因</view>
<view class="leave-input">
<fui-textarea v-model="leaveReason" placeholder="请输入请假原因" :isCounter="true" :maxlength="200" :minHeight="200" :isAutoHeight="true"></fui-textarea>
</view>
<view class="leave-buttons">
<fui-button background="#434544" color="#fff" borderColor="#666" btnSize="medium" @tap="$refs.leaveReasonModal.close()">取消</fui-button>
<fui-button background="#29d3b4" color="#fff" btnSize="medium" @tap="submitLeaveRequest">提交</fui-button>
</view>
</view>
</fui-modal>
</view>
</template>
<script>
import apiRoute from '@/api/apiRoute.js';
export default {
data() {
return {
course_id:'',
course_info:[],
date: '',
students: [
// {
// name: '',
// desc: ''
// },
// {
// name: '',
// desc: ''
// },
],
resource_id:'',
leaveReason: '', //
currentStudent: null, //
emptySeats: [] //
};
},
onLoad(query) {
console.log('onLoad 参数:', query);
this.course_id = query.id || '';
this.resource_id = query.resource_id
console.log('初始化参数 - course_id:', this.course_id, 'resource_id:', this.resource_id);
this.courseInfo();
},
methods: {
viewStudent(stu){
console.log(stu, this.course_info);
//
if (stu.person_type === 'customer_resource') {
//
uni.showModal({
title: '取消课程',
content: `是否取消学员 ${stu.name} 的课程?`,
success: async (res) => {
if (res.confirm) {
// schedule_del
try {
uni.showLoading({
title: '处理中...'
});
const params = {
resources_id: stu.resources_id,
id: this.course_info.id
};
const result = await apiRoute.schedule_del(params);
uni.hideLoading();
if (result.code === 1) {
uni.showToast({
title: '取消课程成功',
icon: 'success'
});
//
await this.courseInfo();
} else {
uni.showToast({
title: result.msg || '取消课程失败',
icon: 'none'
});
}
} catch (error) {
uni.hideLoading();
uni.showToast({
title: '操作失败,请重试',
icon: 'none'
});
console.error('取消课程失败:', error);
}
}
}
});
} else if (stu.person_type === 'student') {
//
this.$refs.leaveReasonModal.open();
this.currentStudent = stu; //
}
},
getStatusText(status) {
const statusMap = {
0: '待上课',
1: '已上课',
2: '请假'
};
return statusMap[status] || status;
},
async scheduleList() {
try {
console.log('开始获取学员列表, schedule_id:', this.course_id);
let res = await apiRoute.scheduleList({
'schedule_id': this.course_id
})
console.log('学员列表响应:', res);
if (res.code === 1 && Array.isArray(res.data)) {
//
this.students = res.data.sort((a, b) => {
// positionposition
if (a.position !== undefined && b.position !== undefined) {
return parseInt(a.position) - parseInt(b.position);
}
// ID
return a.id - b.id;
});
} else {
this.students = [];
}
//
this.updateAvailableCapacity();
return this.students;
} catch (error) {
console.error('获取学员列表失败:', error);
this.students = [];
return [];
}
},
async courseInfo() {
try {
console.log('开始获取课程信息, id:', this.course_id);
let res = await apiRoute.courseInfo({
'id': this.course_id
})
console.log('课程信息响应:', res);
if (res.code === 1 && res.data) {
this.course_info = res.data;
//
await this.scheduleList();
//
this.updateAvailableCapacity();
} else {
uni.showToast({
title: res.msg || '获取课程信息失败',
icon: 'none'
});
}
} catch (error) {
console.error('获取课程信息失败:', error);
uni.showToast({
title: '获取课程信息失败',
icon: 'none'
});
}
},
//
updateAvailableCapacity() {
// course_infostudents
if (!this.course_info || !this.students) return;
console.log('更新可用容量 - 课程总容量:', this.course_info.available_capacity, '学员数量:', this.students.length);
//
const occupiedSeats = this.students.length;
//
const totalCapacity = parseInt(this.course_info.available_capacity) || 0;
//
this.course_info.available_capacity = Math.max(0, totalCapacity - occupiedSeats);
console.log('计算后的可用容量:', this.course_info.available_capacity);
//
this.emptySeats = [];
for (let i = 1; i <= this.course_info.available_capacity; i++) {
this.emptySeats.push(i);
}
},
async addStudent(e, index) {
console.log('添加学员到位置:', index);
const data = {
'resources_id': this.resource_id,
'person_type': 'customer_resource',
'schedule_id': this.course_id,
'course_date': this.course_info.course_date,
'time_slot': this.course_info.time_slot,
'position': index //
};
try {
uni.showLoading({
title: '添加中...'
});
let res = await apiRoute.addSchedule(data)
uni.hideLoading();
if(res.code == 1){
uni.showToast({
title: '添加成功',
icon: 'success'
});
//
await this.courseInfo();
}else{
uni.showToast({
title: res.msg || '添加失败',
icon: 'none'
});
}
} catch (error) {
uni.hideLoading();
uni.showToast({
title: '添加失败',
icon: 'none'
});
console.error('添加学员失败:', error);
}
},
//
submitLeaveRequest() {
if (!this.leaveReason.trim()) {
uni.showToast({
title: '请填写请假原因',
icon: 'none'
});
return;
}
//
uni.showLoading({
title: '提交中...'
});
//
const params = {
resources_id: this.currentStudent.resources_id,
id: this.course_info.id,
remark: this.leaveReason
};
//
apiRoute.schedule_del(params)
.then(res => {
uni.hideLoading();
if (res.code === 1) {
uni.showToast({
title: '请假申请提交成功',
icon: 'success'
});
this.$refs.leaveReasonModal.close();
this.leaveReason = ''; //
//
this.courseInfo().then(() => {
//
this.updateAvailableCapacity();
});
} else {
uni.showToast({
title: res.msg || '请假申请提交失败',
icon: 'none'
});
}
})
.catch(err => {
uni.hideLoading();
uni.showToast({
title: '请假申请提交失败,请重试',
icon: 'none'
});
console.error('请假申请提交失败:', err);
});
},
},
};
</script>
<style lang="less" scoped>
.detail-root {
background: #232323;
min-height: 100vh;
padding-bottom: 30rpx;
}
.header {
padding: 40rpx 30rpx 20rpx 30rpx;
.title {
color: #fff;
font-size: 36rpx;
font-weight: bold;
}
.date {
color: #29d3b4;
font-size: 26rpx;
margin-top: 10rpx;
}
}
.section {
margin: 30rpx;
background: #434544;
border-radius: 16rpx;
padding: 30rpx 20rpx;
}
.section-title {
color: #ffd86b;
font-size: 28rpx;
margin-bottom: 20rpx;
}
.student-list {
display: flex;
flex-wrap: wrap;
gap: 20rpx;
}
.student-item {
background: #333;
border-radius: 12rpx;
display: flex;
align-items: center;
padding: 18rpx 24rpx;
min-width: 260rpx;
.avatar {
width: 60rpx;
height: 60rpx;
border-radius: 50%;
background: #29d3b4;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 32rpx;
margin-right: 18rpx;
}
.info {
.name {
color: #fff;
font-size: 28rpx;
}
.desc {
color: #bdbdbd;
font-size: 22rpx;
margin-top: 4rpx;
}
}
&.empty {
border: 2rpx dashed #ffd86b;
background: #232323;
.avatar.empty-avatar {
background: #ffd86b;
color: #232323;
font-size: 36rpx;
}
.info .name {
color: #ffd86b;
}
.info .desc {
color: #ffd86b;
}
}
}
//
.leave-form {
padding: 20rpx;
.leave-label {
font-size: 28rpx;
color: #333;
margin-bottom: 20rpx;
}
.leave-input {
margin-bottom: 30rpx;
}
.leave-buttons {
display: flex;
justify-content: space-between;
gap: 20rpx;
.fui-button {
flex: 1;
}
}
}
/* 空数据占位区域样式 */
.empty-placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60rpx 0;
image {
width: 200rpx;
height: 200rpx;
margin-bottom: 20rpx;
}
text {
color: #999;
font-size: 28rpx;
}
}
</style>

343
uniapp/pages/market/data/statistics.vue

@ -0,0 +1,343 @@
<!--市场数据统计页面-->
<template>
<view class="main_box" :class="{'has-safe-area': hasSafeArea}">
<!--自定义导航栏-->
<view class="navbar_section">
<view class="title">市场数据统计</view>
</view>
<!-- 顶部筛选 -->
<view class="filter-section">
<view class="filter-item">
<view class="label">统计时间</view>
<picker mode="date" fields="month" :value="currentDate" @change="dateChange">
<view class="picker-value">{{currentDate}}</view>
</picker>
</view>
</view>
<!-- 市场人员资源量统计 -->
<view class="table-section">
<view class="table-title">市场人员资源量统计</view>
<view class="table-container">
<view class="table-header">
<view class="th th-person">人员</view>
<view class="th th-week">周数量</view>
<view class="th th-month">月数量</view>
<view class="th th-year">年数量</view>
</view>
<view class="table-body">
<view class="table-row" v-for="(item, index) in staffData" :key="index">
<view class="td td-person">{{item.name}}</view>
<view class="td td-week">{{item.weekCount}}</view>
<view class="td td-month">{{item.monthCount}}</view>
<view class="td td-year">{{item.yearCount}}</view>
</view>
</view>
</view>
</view>
<!-- 校区人员资源渠道量 -->
<view class="table-section">
<view class="table-title">{{currentMonth}} - 年度校区人员资源渠道量</view>
<view class="table-container">
<view class="table-header">
<view class="th th-channel" style="width: 100px;">渠道</view>
<view class="th" v-for="(school, index) in schoolList" :key="index">{{school.campus_name}}</view>
<view class="th th-total">渠道合计</view>
</view>
<view class="table-body">
<view class="table-row" v-for="(channel, index) in channelList" :key="index">
<view class="td td-channel" style="width: 100px;">{{channel.name}}</view>
<view class="td" v-for="(school, sIndex) in schoolList" :key="sIndex">
{{channel[school.id] || 0}}
</view>
<view class="td td-total">{{channel.total}}</view>
</view>
</view>
</view>
</view>
<!-- 市场人员资源渠道统计 -->
<view class="table-section">
<view class="table-title">{{currentMonth}} - 年度市场人员资源渠道统计</view>
<view class="table-container">
<view class="table-header">
<view class="th th-channel" style="width: 100px;">渠道</view>
<view class="th" v-for="(staff, index) in staffData" :key="index">市场{{staff.name}}</view>
<view class="th th-total">资源合计</view>
</view>
<view class="table-body">
<view class="table-row" v-for="(channel, index) in channelList" :key="index">
<view class="td td-channel" style="width: 100px;">{{channel.name}}</view>
<view class="td" v-for="(staff, sIndex) in staffData" :key="sIndex">
{{staff['channel'][channel.value] || 0}}
</view>
<view class="td td-total">{{channel.total}}</view>
</view>
</view>
</view>
</view>
<AQTabber />
</view>
</template>
<script>
import apiRoute from '@/api/apiRoute.js';
import AQTabber from "@/components/AQ/AQTabber.vue"
export default {
components: {
AQTabber,
},
data() {
return {
currentDate: this.formatDate(new Date()),
currentMonth: new Date().getMonth() + 1,
//
staffData: [],
//
schoolList: [],
//
hasSafeArea: false,
//
channelList: [],
//
marketStaffList: [
{ id: 'A', name: 'A' },
{ id: 'B', name: 'B' },
{ id: 'C', name: 'C' },
{ id: 'D', name: 'D' }
],
//
channelSchoolData: [],
//
channelStaffData: []
}
},
onLoad() {
this.initData();
this.checkSafeArea();
},
methods: {
// YYYY-MM
formatDate(date) {
const year = date.getFullYear();
const month = (date.getMonth() + 1).toString().padStart(2, '0');
return `${year}-${month}`;
},
//
dateChange(e) {
this.currentDate = e.detail.value;
const [year, month] = this.currentDate.split('-');
this.currentMonth = parseInt(month);
this.initData();
},
//
async initData() {
// API
await this.getStaffStatistics();
// await this.getChannelSchoolStatistics();
// this.getChannelStaffStatistics();
},
//
async getStaffStatistics() {
// API
let res = await apiRoute.getStaffStatistics({date: this.currentDate})
console.log(res.data.staffData,"================");
this.staffData = res.data.staffData
this.schoolList = res.data.schoolList
this.channelList = res.data.channelList
},
//
getChannelSchoolStatistics() {
// API
this.channelSchoolData = this.channelList.map(channel => {
const schools = {};
let total = 0;
this.schoolList.forEach(school => {
const count = Math.floor(Math.random() * 50);
schools[school.id] = count;
total += count;
});
return {
id: channel.id,
name: channel.name,
schools,
total
}
});
},
//
getChannelStaffStatistics() {
// API
this.channelStaffData = this.channelList.map(channel => {
const staffs = {};
let total = 0;
this.marketStaffList.forEach(staff => {
const count = Math.floor(Math.random() * 40);
staffs[staff.id] = count;
total += count;
});
return {
id: channel.id,
name: channel.name,
staffs,
total
}
});
},
//
checkSafeArea() {
try {
const systemInfo = uni.getSystemInfoSync();
// iPhone X
if (systemInfo.safeAreaInsets && systemInfo.safeAreaInsets.bottom > 0) {
this.hasSafeArea = true;
}
} catch (e) {
console.error('获取系统信息失败', e);
}
}
}
}
</script>
<style lang="scss" scoped>
.main_box {
min-height: 100vh;
background-color: #1a1a1a;
color: #ffffff;
padding-bottom: 150rpx; /* 增加底部内边距,防止内容被底部导航栏遮挡 */
}
/* 适配有安全区域的设备(如iPhone X及以上机型) */
.has-safe-area {
padding-bottom: calc(150rpx + constant(safe-area-inset-bottom)); /* iOS 11.0-11.2 */
padding-bottom: calc(150rpx + env(safe-area-inset-bottom)); /* iOS 11.2+ */
}
.navbar_section {
background-color: #1a1a1a;
height: 88rpx;
display: flex;
align-items: center;
justify-content: center;
position: relative;
.title {
font-size: 36rpx;
font-weight: bold;
}
}
.filter-section {
padding: 20rpx;
background-color: #222222;
margin: 20rpx;
border-radius: 10rpx;
.filter-item {
display: flex;
align-items: center;
.label {
font-size: 28rpx;
margin-right: 10rpx;
}
.picker-value {
padding: 10rpx 20rpx;
background-color: #333333;
border-radius: 6rpx;
font-size: 28rpx;
}
}
}
.table-section {
margin: 30rpx 20rpx;
background-color: #222222;
border-radius: 10rpx;
overflow: hidden;
.table-title {
font-size: 32rpx;
font-weight: bold;
padding: 20rpx;
border-bottom: 1px solid #333333;
}
.table-container {
width: 100%;
overflow-x: auto;
}
.table-header {
display: flex;
background-color: #333333;
.th {
padding: 15rpx 10rpx;
font-size: 26rpx;
text-align: center;
flex: 1;
min-width: 100rpx;
font-weight: bold;
}
}
.table-body {
.table-row {
display: flex;
border-bottom: 1px solid #333333;
&:last-child {
border-bottom: none;
}
.td {
padding: 15rpx 10rpx;
font-size: 26rpx;
text-align: center;
flex: 1;
min-width: 100rpx;
}
}
}
.th-person, .td-person {
min-width: 80rpx;
flex: 0.8;
}
.th-week, .td-week,
.th-month, .td-month,
.th-year, .td-year {
flex: 1;
}
.th-channel, .td-channel {
flex: 1.2;
text-align: left;
padding-left: 20rpx;
}
.th-total, .td-total {
flex: 1.2;
font-weight: bold;
}
}
</style>

390
uniapp/pages/market/home/index.vue

@ -0,0 +1,390 @@
<template>
<view class="assemble">
<view style="height: 20rpx;"></view>
<!-- 市场人员展示-->
<view class="div-style">
<view style="height: 38vh;">
<view style="display: flex;align-items: center;padding: 20rpx 0 0 20rpx;">
<view>
<image :src="$util.img('/uniapp_src/static/images/index/danlan.png')" class="drop-image">
</image>
</view>
<view class="title">本月业绩</view>
</view>
<view class="coach-message">
<view class="left1">
<view style="padding: 20rpx 0;">
<view style="display: flex;align-items: center;">
<view style="padding: 12rpx;">
<image :src="$util.img('/uniapp_src/static/images/index/huang.png')"
class="drop-image-x"></image>
</view>
<view class="title-x">资源总数</view>
</view>
<view class="title-x1">{{infoData.month.new_total}}</view>
</view>
<view>
<view style="display: flex;align-items: center;">
<view style="padding: 12rpx;">
<image :src="$util.img('/uniapp_src/static/images/index/lvs.png')"
class="drop-image-x"></image>
</view>
<view class="title-x">已分配</view>
</view>
<view class="title-x1">{{infoData.month.new_total}}</view>
</view>
<view>
<view style="display: flex;align-items: center;">
<view style="padding: 12rpx;">
<image :src="$util.img('/uniapp_src/static/images/index/shenlan.png')"
class="drop-image-x"></image>
</view>
<view class="title-x">昨日新增</view>
</view>
<view class="title-x1">{{infoData.month.yesterday_new}}</view>
</view>
<view>
<view style="display: flex;align-items: center;">
<view style="padding: 12rpx;">
<image :src="$util.img('/uniapp_src/static/images/index/lan.png')"
class="drop-image-x"></image>
</view>
<view class="title-x">今日新增</view>
</view>
<view class="title-x1">{{infoData.month.today_new}}</view>
</view>
</view>
<!-- 统计图-->
<view class="right1">
<view style="text-align: center;">{{infoData.date_range}}</view>
<view class="statistics_box">
<view class="item">
<view class="box">
<view class="progress-bar"
:style="{ height: `${infoData.month.yesterday_new_rate}%`, background: '#f59a23' }">
</view>
<view class="ratio"
:style="{ color: infoData.month.yesterday_new_rate <= 0 ? '#333333' : '#000' }">
{{ infoData.month.yesterday_new_rate }}%
</view>
</view>
<view class="title">昨日</view>
</view>
<view class="item">
<view class="box">
<view class="progress-bar"
:style="{ height: `${infoData.month.assigned_sales_rate}%`, background: '#039f64' }">
</view>
<view class="ratio"
:style="{ color: infoData.month.assigned_sales_rate <= 0 ? '#333333' : '#000' }">
{{ infoData.month.assigned_sales_rate }}%
</view>
</view>
<view class="title">分配</view>
</view>
<view class="item">
<view class="box">
<view class="progress-bar"
:style="{ height: `${infoData.month.today_new_rate}%`, background: '#4066f2' }">
</view>
<view class="ratio"
:style="{ color: infoData.month.today_new_rate <= 0 ? '#333333' : '#000' }">
{{ infoData.month.today_new_rate }}%
</view>
</view>
<view class="title">今日</view>
</view>
</view>
</view>
</view>
</view>
<view style="width: 90%;background: #EFF3F8;height: 4rpx;margin: auto;"></view>
<view style="height: 38vh;">
<view style="display: flex;align-items: center;padding: 20rpx 0 0 20rpx;">
<view>
<image :src="$util.img('/uniapp_src/static/images/index/danlv.png')" class="drop-image"></image>
</view>
<view class="title">个人业绩</view>
</view>
<view class="coach-message">
<view class="this_month">
<view style="padding: 20rpx 0;display: flex;justify-content: space-between;">
<view style="width: 48%;">
<view style="display: flex;align-items: center;">
<view style="padding: 12rpx;">
<image :src="$util.img('/uniapp_src/static/images/index/danlv.png')"
class="drop-image-x"></image>
</view>
<view class="title-x">今日新增资源</view>
</view>
<view class="title-x1">{{infoData.last_month.xzzy}}</view>
</view>
<view style="width: 48%;">
<view style="display: flex;align-items: center;">
<view style="padding: 12rpx;">
<image :src="$util.img('/uniapp_src/static/images/index/danlv.png')"
class="drop-image-x"></image>
</view>
<view class="title-x">今日业绩收入</view>
</view>
<view class="title-x1">{{infoData.last_month.yjsr}}</view>
</view>
</view>
<view style="padding: 20rpx 0;display: flex;justify-content: space-between;">
<view style="width: 48%;">
<view style="display: flex;align-items: center;">
<view style="padding: 12rpx;">
<image :src="$util.img('/uniapp_src/static/images/index/danlv.png')"
class="drop-image-x"></image>
</view>
<view class="title-x">历史关单数量</view>
</view>
<view class="title-x1">{{infoData.last_month.gdsl}}</view>
</view>
<view style="width: 48%;">
<view style="display: flex;align-items: center;">
<view style="padding: 12rpx;">
<image :src="$util.img('/uniapp_src/static/images/index/danlv.png')"
class="drop-image-x"></image>
</view>
<view class="title-x">其他奖励</view>
</view>
<view class="title-x1">{{infoData.last_month.qtjl}}</view>
</view>
</view>
<view style="padding: 20rpx 0;display: flex;justify-content: space-between;">
<view style="width: 48%;">
<view style="display: flex;align-items: center;">
<view style="padding: 12rpx;">
<image :src="$util.img('/uniapp_src/static/images/index/danlv.png')"
class="drop-image-x"></image>
</view>
<view class="title-x">本月提成</view>
</view>
<view class="title-x1">{{infoData.last_month.bytc}}</view>
</view>
</view>
</view>
</view>
</view>
</view>
<AQTabber />
</view>
</template>
<script>
import apiRoute from '@/api/apiRoute.js';
import AQTabber from "@/components/AQ/AQTabber.vue"
export default {
components: {
AQTabber,
},
data() {
return {
infoData: {}, //
userInfo: {}, //
}
},
onShow() {
this.init()
},
methods: {
async init() {
await this.getUserInfo()
await this.getXsIndex()
},
//
async getUserInfo() {
let res = await apiRoute.getPersonnelInfo({})
if (res.code != 1) {
uni.showToast({
title: res.msg,
icon: 'none'
})
return
}
this.userInfo = res.data
},
//
async getXsIndex() {
let role_key_arr = this.userInfo.role_key_arr.join(',')
let params = {
personnel_id: this.userInfo.id, //id
role_key_arr: role_key_arr, // key
}
let res = await apiRoute.xs_statisticsMarketHome(params)
if (res.code != 1) {
uni.showToast({
title: res.msg,
icon: 'none'
})
return
}
this.infoData = res.data
console.log('统计', this.infoData)
},
}
}
</script>
<style lang="less" scoped>
//
.navbar_section {
border: 1px solid #fff;
display: flex;
justify-content: center;
align-items: center;
background: #fff;
.title {
padding: 40rpx 0rpx;
/* 小程序端样式 */
// #ifdef MP-WEIXIN
padding: 80rpx 0rpx;
// #endif
font-size: 30rpx;
color: #858585;
}
}
.assemble {
width: 100%;
height: 100vh;
background: #292929;
}
.div-style {
width: 92%;
height: 85vh;
background: #fff;
border-radius: 16rpx;
margin: auto;
}
.coach-message {
width: 92%;
margin: 10rpx auto;
display: flex;
align-items: center;
padding-top: 20rpx;
}
.drop-image {
width: 50rpx;
height: 50rpx;
}
.title {
font-size: 30rpx;
color: #7F7F7F;
padding-left: 20rpx;
}
.left1 {
width: 48%;
height: 95%;
margin: auto;
}
.right1 {
width: 48%;
height: 95%;
margin: auto;
.statistics_box {
margin: auto;
margin-top: 10rpx;
display: flex;
justify-content: space-between;
.item {
width: 90rpx;
display: flex;
flex-direction: column;
align-items: center;
.box {
width: 100%;
height: 328rpx;
border: 1px solid #ddd;
border-radius: 6rpx;
background: #f5f5f5;
position: relative;
.progress-bar {
width: 100%;
height: 0;
transition: height 0.3s ease;
position: absolute;
bottom: 0;
}
.ratio {
width: 100%;
position: absolute;
bottom: -0rpx;
font-size: 26rpx;
text-align: center;
}
}
.title {
margin-top: 5rpx;
padding: 0;
font-size: 26rpx;
color: #999999;
;
text-align: center;
}
}
}
}
.this_month {
width: 100%;
height: 95%;
margin: auto;
}
.drop-image-x {
width: 20rpx;
height: 20rpx;
}
.title-x {
font-size: 28rpx;
color: #7F7F7F;
padding-left: 20rpx;
}
.title-x1 {
font-size: 28rpx;
color: #333333;
padding-left: 60rpx;
}
</style>

272
uniapp/pages/market/reimbursement/add.vue

@ -0,0 +1,272 @@
<template>
<view class="reim-add-page">
<view class="header-bar">
<view class="title">{{ pageTitle }}</view>
</view>
<view class="form-box">
<!-- 金额 -->
<view class="form-row">
<view class="label">报销金额</view>
<input class="input-amount" type="number" v-model="form.amount" placeholder="0.00"
:disabled="disabled" />
</view>
<!-- 描述 -->
<view class="form-row-top">
<view class="label">报销描述</view>
<textarea class="textarea" v-model="form.description" placeholder="请输入报销事由" :disabled="disabled" />
</view>
<!-- 附件 -->
<view class="form-row">
<view class="label">发票/收据</view>
<view class="file-upload-wrapper">
<view v-if="form.receipt_url" class="preview-box" @click="previewImage">
<image v-if="isImage(form.receipt_url)" :src="form.receipt_url" class="preview-img"
mode="aspectFit" />
<view v-else class="file-link">{{ getFileName(form.receipt_url) }}</view>
</view>
<button class="upload-btn" @click="chooseFile" :disabled="disabled">
{{ form.receipt_url ? '重新选择' : '选择附件' }}
</button>
</view>
</view>
</view>
<view class="save-btn-box" v-if="!disabled">
<fui-button background="#434544" color="#24BA9F" borderColor="#24BA9F" @click="submit">提交</fui-button>
</view>
</view>
</template>
<script>
import apiRoute from '@/api/apiRoute.js';
import {
Api_url
} from "@/common/config.js";
export default {
data() {
return {
uploadUrl: `${Api_url}/uploadImage`,
pageTitle: '新增报销',
disabled: false,
form: {
id: null,
amount: '',
description: '',
receipt_url: '',
}
}
},
onLoad(options) {
if (options.id) {
this.form.id = options.id;
this.fetchDetail(options.id);
}
},
methods: {
async fetchDetail(id) {
uni.showLoading({
title: '加载中...'
});
let res = await apiRoute.reimbursement_info({id:id})
let mockData = res.data
if (mockData.status !== 'pending') {
this.disabled = true;
this.pageTitle = '查看报销';
} else {
this.pageTitle = '编辑报销';
}
this.form.amount = mockData.amount;
this.form.description = mockData.description;
this.form.receipt_url = mockData.receipt_url;
uni.hideLoading();
},
chooseFile() {
uni.chooseImage({
count: 1,
success: (res) => {
const tempFilePath = res.tempFilePaths[0]
//
this.uploadFilePromise(tempFilePath)
}
})
},
uploadFilePromise(url) {
let token = uni.getStorageSync('token') || ''
let a = uni.uploadFile({
url: this.uploadUrl, //
filePath: url,
name: 'file',
header: {
'token': `${token}`, //token
},
success: (e) => {
let res = JSON.parse(e.data.replace(/\ufeff/g, "") || "{}")
if (res.code == 1) {
this.form.receipt_url = res.data.url
} else {
uni.showToast({
title: res.msg,
icon: 'none'
})
}
},
});
},
isImage(url) {
if (!url) return false;
return /\.(jpg|jpeg|png|gif|bmp)$/i.test(url);
},
getFileName(path) {
if (!path) return '';
return path.split('/').pop();
},
previewImage() {
if (this.form.receipt_url && this.isImage(this.form.receipt_url)) {
uni.previewImage({
urls: [this.form.receipt_url]
});
}
},
submit() {
if (!this.form.amount || !this.form.description) {
uni.showToast({
title: '请填写完整',
icon: 'none'
});
return;
}
let res = apiRoute.reimbursement_add(this.form)
if (res['code'] == 1) {
uni.showToast({
title: '提交成功',
icon: 'success'
});
}
setTimeout(() => {
uni.navigateBack();
}, 1000);
}
}
}
</script>
<style lang="less" scoped>
.reim-add-page {
min-height: 100vh;
background: #292929;
padding-bottom: 120rpx;
}
.header-bar {
padding: 32rpx;
.title {
font-size: 44rpx;
color: #fff;
font-weight: bold;
}
}
.form-box {
margin: 0 32rpx;
background: #434544;
border-radius: 18rpx;
padding: 0 24rpx;
}
.form-row,
.form-row-top {
display: flex;
padding: 32rpx 0;
border-bottom: 1rpx solid #3a3a3a;
&:last-of-type {
border-bottom: none;
}
}
.form-row {
align-items: center;
}
.form-row-top {
align-items: flex-start;
}
.label {
color: #aaa;
font-size: 28rpx;
width: 180rpx;
flex-shrink: 0;
}
.input-amount {
flex: 1;
color: #fff;
font-size: 28rpx;
text-align: right;
}
.textarea {
flex: 1;
height: 180rpx;
color: #fff;
font-size: 28rpx;
background-color: transparent;
padding: 0;
width: 100%;
}
.file-upload-wrapper {
flex: 1;
display: flex;
justify-content: flex-end;
align-items: center;
gap: 20rpx;
}
.upload-btn {
background: #24BA9F;
color: #fff;
border: none;
border-radius: 8rpx;
padding: 0 32rpx;
line-height: 64rpx;
height: 64rpx;
font-size: 26rpx;
margin: 0;
}
.preview-box {
.preview-img {
width: 120rpx;
height: 120rpx;
border-radius: 8rpx;
background: #222;
border: 1rpx solid #333;
}
.file-link {
color: #24BA9F;
font-size: 26rpx;
word-break: break-all;
}
}
.save-btn-box {
position: fixed;
left: 0;
right: 0;
bottom: 0;
z-index: 100;
background: #292929;
padding: 20rpx 40rpx 40rpx 40rpx;
box-shadow: 0 -2rpx 8rpx rgba(0, 0, 0, 0.12);
}
</style>

136
uniapp/pages/market/reimbursement/detail.vue

@ -0,0 +1,136 @@
<template>
<view class="reim-detail-page">
<view class="header-bar">
<view class="title">报销详情</view>
</view>
<view class="detail-box">
<view class="row">
<view class="label">报销金额</view>
<view class="value">{{ detail.amount }}</view>
</view>
<view class="row">
<view class="label">报销描述</view>
<view class="value">{{ detail.description }}</view>
</view>
<view class="row">
<view class="label">发票/收据</view>
<view class="value">
<image v-if="detail.receipt_url && isImage(detail.receipt_url)" :src="detail.receipt_url" class="receipt-img" mode="aspectFit" />
<view v-else-if="detail.receipt_url" class="file-link">{{ detail.receipt_url }}</view>
<text v-else>无附件</text>
</view>
</view>
<view class="row">
<view class="label">状态</view>
<view :class="['value', 'status-' + detail.status]">{{ statusMap[detail.status] || detail.status }}</view>
</view>
<view class="row">
<view class="label">创建时间</view>
<view class="value">{{ detail.created_at }}</view>
</view>
<view class="row">
<view class="label">修改时间</view>
<view class="value">{{ detail.updated_at }}</view>
</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
detail: {},
statusMap: {
pending: '待审批',
approved: '已批准',
rejected: '已拒绝',
},
}
},
onLoad(options) {
if (options.id) {
this.fetchDetail(options.id);
} else {
uni.showToast({ title: '缺少报销ID', icon: 'none' });
uni.navigateBack();
}
},
methods: {
fetchDetail(id) {
//
uni.showLoading({ title: '加载中...' });
setTimeout(() => {
const mockData = {
id: id,
amount: 300.00,
description: '办公用品采购(已批准)',
receipt_url: 'https://cdn.uviewui.com/uview/swiper/1.jpg',
status: 'approved',
created_at: '2024-06-02 09:30',
updated_at: '2024-06-03 12:00',
};
this.detail = mockData;
uni.hideLoading();
}, 500);
},
isImage(url) {
if (!url) return false;
return /\.(jpg|jpeg|png|gif|bmp)$/i.test(url)
}
}
}
</script>
<style lang="less" scoped>
.reim-detail-page {
min-height: 100vh;
background: #292929;
}
.header-bar {
padding: 32rpx 32rpx 0 32rpx;
.title {
font-size: 36rpx;
color: #24BA9F;
font-weight: bold;
}
}
.detail-box {
margin: 32rpx;
background: #434544;
border-radius: 18rpx;
padding: 32rpx 24rpx 24rpx 24rpx;
}
.row {
display: flex;
align-items: flex-start;
margin-bottom: 32rpx;
.label {
color: #aaa;
font-size: 28rpx;
width: 180rpx;
flex-shrink: 0;
margin-top: 10rpx;
}
.value {
color: #fff;
font-size: 28rpx;
word-break: break-all;
}
.status-pending { color: #f0ad4e; }
.status-approved { color: #24BA9F; }
.status-rejected { color: #e74c3c; }
.receipt-img {
width: 160rpx;
height: 160rpx;
border-radius: 8rpx;
background: #222;
border: 1rpx solid #333;
}
.file-link {
color: #24BA9F;
font-size: 26rpx;
word-break: break-all;
}
}
</style>

144
uniapp/pages/market/reimbursement/list.vue

@ -0,0 +1,144 @@
<template>
<view class="reim-list-page">
<view class="header-bar">
<view class="title">报销列表</view>
<fui-button
background="transparent"
color="#24BA9F"
borderColor="#24BA9F"
width="180rpx"
height="64rpx"
radius="32rpx"
@click="goAdd">
新增报销
</fui-button>
</view>
<view v-if="list.length === 0" class="empty-tip">暂无报销记录</view>
<view v-for="item in list" :key="item.id" class="reim-card" @click="goDetail(item)">
<view class="row">
<view class="label">金额</view>
<view class="value">{{ item.amount }}</view>
</view>
<view class="row">
<view class="label">描述</view>
<view class="value">{{ item.description }}</view>
</view>
<view class="row">
<view class="label">发票/收据</view>
<view class="value">
<image v-if="item.receipt_url" :src="item.receipt_url" class="receipt-img" mode="aspectFit" />
<text v-else>无附件</text>
</view>
</view>
<view class="row">
<view class="label">状态</view>
<view :class="['value', 'status-' + item.status]">{{ statusMap[item.status] || item.status }}</view>
</view>
<view class="row">
<view class="label">创建时间</view>
<view class="value">{{ item.created_at }}</view>
</view>
</view>
</view>
</template>
<script>
import apiRoute from '@/api/apiRoute.js';
export default {
data() {
return {
list: [],
statusMap: {
pending: '待审批',
approved: '已批准',
rejected: '已拒绝',
},
}
},
async onShow() {
await this.reimbursementList();
},
methods: {
async reimbursementList(){
let res = await apiRoute.reimbursement_list({})
this.list = res.data;
},
goAdd() {
uni.navigateTo({
url: '/pages/market/reimbursement/add'
});
},
goDetail(item) {
//
if (item.status === 'pending') {
uni.navigateTo({
url: `/pages/market/reimbursement/add?id=${item.id}`
});
} else {
uni.navigateTo({
url: `/pages/market/reimbursement/detail?id=${item.id}`
});
}
}
}
}
</script>
<style lang="less" scoped>
.reim-list-page {
min-height: 100vh;
background: #292929;
padding-bottom: 120rpx;
}
.header-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 32rpx 32rpx 0 32rpx;
.title {
font-size: 36rpx;
color: #fff;
font-weight: bold;
}
}
.empty-tip {
color: #888;
text-align: center;
margin: 80rpx 0;
}
.reim-card {
background: #434544;
border-radius: 18rpx;
margin: 32rpx;
margin-bottom: 0;
padding: 32rpx 24rpx 24rpx 24rpx;
box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.08);
.row {
display: flex;
align-items: center;
margin-bottom: 18rpx;
.label {
color: #aaa;
font-size: 26rpx;
width: 140rpx;
flex-shrink: 0;
}
.value {
color: #fff;
font-size: 28rpx;
word-break: break-all;
}
.status-pending { color: #f0ad4e; }
.status-approved { color: #24BA9F; }
.status-rejected { color: #e74c3c; }
.receipt-img {
width: 120rpx;
height: 120rpx;
border-radius: 8rpx;
background: #222;
border: 1rpx solid #333;
}
}
}
</style>
Loading…
Cancel
Save