Files
gym-manage/gym-manage-coach-uniapp/pages/profile/profile.vue
T

624 lines
16 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="profile-page">
<view class="header-wrap" :style="{ paddingTop: statusBarHeight + 'px' }">
<view class="nav-bar" :style="{ height: navBarHeight + 'px' }">
<text class="back-btn" @click="goBack">&#8592;</text>
<text class="nav-title">个人中心</text>
</view>
</view>
<scroll-view class="content" scroll-y :style="{ height: 'calc(100vh - ' + totalHeaderHeight + 'px)' }">
<view class="content-inner">
<!-- 教练信息卡片 -->
<view class="user-card">
<view class="user-header">
<view class="avatar">
<text class="avatar-text">CC</text>
</view>
<view class="user-info">
<text class="user-name">{{ coachInfo.username }}</text>
<view class="user-tag">
<text>教练</text>
</view>
</view>
</view>
<view class="user-details">
<view class="detail-row">
<text class="detail-label">教练 ID</text>
<text class="detail-value">{{ coachInfo.coachId }}</text>
</view>
</view>
</view>
<!-- 业绩数据 -->
<view class="performance-card">
<view class="section-header">
<text class="section-title">本月业绩</text>
<text class="period-text">{{ performance.periodLabel }}</text>
</view>
<view class="perf-grid">
<view class="perf-item">
<text class="perf-number">{{ performance.totalLessons }}</text>
<text class="perf-label">授课量</text>
</view>
<view class="perf-item">
<text class="perf-number">{{ performance.totalAttendees }}</text>
<text class="perf-label">学员人次</text>
</view>
<view class="perf-item" @click="showFormula('attendance')">
<text class="perf-number">{{ performance.attendanceRate != null ? performance.attendanceRate + '%' : 'N/A' }}</text>
<text class="perf-label">出勤率 </text>
<view v-if="performance.attendanceRate != null" class="perf-bar-wrap">
<view class="perf-bar" :style="{ width: performance.attendanceRate + '%', background: getRateColor(performance.attendanceRate) }"></view>
</view>
</view>
<view class="perf-item" @click="showFormula('fullness')">
<text class="perf-number">{{ performance.fullnessRate != null ? performance.fullnessRate + '%' : 'N/A' }}</text>
<text class="perf-label">满员率 </text>
<view v-if="performance.fullnessRate != null" class="perf-bar-wrap">
<view class="perf-bar" :style="{ width: performance.fullnessRate + '%', background: getRateColor(performance.fullnessRate) }"></view>
</view>
</view>
</view>
<view class="score-row" @click="showFormula('composite')">
<text class="score-label">综合评分 </text>
<text class="score-badge" :class="scoreClass">{{ scoreText }}</text>
<text v-if="scoreText !== '--'" class="score-icon"></text>
</view>
</view>
<!-- 违规统计 -->
<view v-if="violations.length > 0" class="stats-card">
<view class="stat-item">
<text class="stat-number">{{ violations.length }}</text>
<text class="stat-label">违规次数</text>
</view>
</view>
<!-- 违规记录列表 -->
<view v-if="violations.length > 0" class="section-header">
<text class="section-title">违规记录</text>
</view>
<view v-if="violations.length > 0" class="violation-list">
<view v-for="(v, idx) in violations" :key="idx" class="violation-card">
<view class="violation-header">
<text class="violation-course">{{ v.course_name || '未知课程' }}</text>
<view class="violation-tag" :class="v._violationClass">
<text>{{ v._violationLabel }}</text>
</view>
</view>
<text class="violation-time">{{ formatViolationTime(v.violation_time) }}</text>
</view>
</view>
<!-- 计算方式弹窗 -->
<view v-if="formulaVisible" class="formula-overlay" @click="closeFormula">
<view class="formula-dialog" @click.stop>
<view class="formula-header">
<text class="formula-title">{{ formulaTitle }}</text>
<text class="formula-close" @click="closeFormula"></text>
</view>
<view class="formula-body">
<text class="formula-text">{{ formulaContent }}</text>
<view class="formula-divider"></view>
<text class="formula-example">{{ formulaExample }}</text>
</view>
<view class="formula-footer">
<button class="formula-btn" @click="closeFormula">知道了</button>
</view>
</view>
</view>
<!-- 退出登录 -->
<view class="logout-area">
<button class="logout-btn" @click="handleLogout">退出登录</button>
</view>
<view class="bottom-safe"></view>
</view>
</scroll-view>
</view>
</template>
<script>
const store = require('../../store/index')
const coachApi = require('../../api/coach')
export default {
data() {
return {
statusBarHeight: 0,
navBarHeight: 44,
totalHeaderHeight: 44,
coachInfo: {
username: '加载中...',
coachId: ''
},
violations: [],
loadingViolations: true,
performance: {
totalLessons: 0,
totalAttendees: 0,
attendanceRate: null,
fullnessRate: null,
periodLabel: ''
},
compositeScore: null,
scoreText: '--',
scoreClass: '',
formulaVisible: false,
formulaTitle: '',
formulaContent: '',
formulaExample: ''
}
},
onLoad() {
const systemInfo = uni.getSystemInfoSync()
const statusBarHeight = systemInfo.statusBarHeight || 20
let navBarHeight = 44
// #ifdef MP-WEIXIN
const menuButton = uni.getMenuButtonBoundingClientRect()
if (menuButton) {
navBarHeight = (menuButton.top - statusBarHeight) * 2 + menuButton.height
}
// #endif
this.statusBarHeight = statusBarHeight
this.navBarHeight = navBarHeight
this.totalHeaderHeight = statusBarHeight + navBarHeight
},
onShow() {
if (!store.isLoggedIn) {
uni.reLaunch({ url: '/pages/login/login' })
return
}
this.coachInfo = store.getCoachInfo() || { username: '教练', coachId: '' }
this.loadViolations()
this.loadPerformance()
},
methods: {
goBack() { uni.navigateBack() },
async loadPerformance() {
try {
const coachId = store.getCoachId()
if (!coachId) return
const data = await coachApi.getCoachPerformance(coachId, 'MONTH')
if (data) {
this.$set(this.performance, 'totalLessons', data.completedCourses != null ? data.completedCourses : 0)
this.$set(this.performance, 'totalAttendees', data.attendedStudents != null ? data.attendedStudents : 0)
this.$set(this.performance, 'attendanceRate', data.attendanceRate != null ? Math.round(data.attendanceRate) : null)
this.$set(this.performance, 'fullnessRate', data.fillRate != null ? Math.round(data.fillRate) : null)
const score = data.compositeScore != null ? Math.round(data.compositeScore * 10) / 10 : null
this.compositeScore = score
this.scoreText = score != null ? score.toFixed(1) : '--'
if (score == null) {
this.scoreClass = ''
} else if (score >= 80) {
this.scoreClass = 'score-great'
} else if (score >= 60) {
this.scoreClass = 'score-good'
} else {
this.scoreClass = 'score-low'
}
this.$set(this.performance, 'periodLabel', '本月')
}
} catch (e) {
console.error('加载业绩数据失败:', e)
}
},
showFormula(type) {
if (type === 'attendance') {
this.formulaTitle = '出勤率计算方式'
this.formulaContent = '出勤率 = 出席人次 ÷ 非取消预约总数 × 100%'
this.formulaExample = '示例:本月有 10 人预约了你的课程,其中 2 人取消预约,8 人中实际出席 7 人\n\n出勤率 = 7 ÷ (10 - 2) × 100% = 87.5%'
} else if (type === 'fullness') {
this.formulaTitle = '满员率计算方式'
this.formulaContent = '满员率 = 各课程(出席人数 ÷ 课程最大容量)的平均值 × 100%'
this.formulaExample = '示例:本月你完成了 2 节课\n- 课程 A:容量 20 人,实际出席 12 人 → 60%\n- 课程 B:容量 15 人,实际出席 9 人 → 60%\n\n满员率 = (60% + 60%) ÷ 2 = 60%'
} else if (type === 'composite') {
this.formulaTitle = '综合评分计算方式'
this.formulaContent = '综合评分 = 授课量评分 × 40% + 出勤率评分 × 30% + 满员率评分 × 30%'
this.formulaExample = '计算步骤:\n1. 授课量归一化:你的授课量 ÷ 全部教练最高授课量 × 100\n2. 出勤率与满员率各取实际百分值\n3. 加权求和\n\n示例:\n授课量评分 80 × 0.4 = 32\n出勤率 85 × 0.3 = 25.5\n满员率 60 × 0.3 = 18\n\n综合评分 = 32 + 25.5 + 18 = 75.5'
}
this.formulaVisible = true
},
closeFormula() {
this.formulaVisible = false
},
async loadViolations() {
this.loadingViolations = true
try {
const coachId = store.getCoachId()
if (!coachId) return
const result = await coachApi.getCoachViolations(coachId)
const list = Array.isArray(result) ? result : (result && result.content ? result.content : [])
// 预计算违规 class 和 labeluni-app 模板不支持 :class 中调用方法
const classMap = { COACH_LATE: 'late', COACH_ABSENT: 'absent', NOT_MANUAL_END: 'no-manual-end' }
const labelMap = { COACH_LATE: '迟到', COACH_ABSENT: '缺席', NOT_MANUAL_END: '未手动结课' }
this.violations = list.map(v => ({
...v,
_violationClass: classMap[v.violation_reason] || '',
_violationLabel: labelMap[v.violation_reason] || v.violation_reason || '违规'
}))
} catch (e) {
console.error('加载违规记录失败:', e)
} finally {
this.loadingViolations = false
}
},
handleLogout() {
uni.showModal({
title: '提示',
content: '确定要退出登录吗?',
success: (res) => {
if (res.confirm) {
store.clearLogin()
uni.reLaunch({ url: '/pages/login/login' })
}
}
})
},
formatViolationTime(timeStr) {
if (!timeStr) return ''
return timeStr.replace('T', ' ').substring(0, 19)
}
}
}
</script>
<style scoped>
.profile-page {
min-height: 100vh;
background: #F5F7FA;
overflow-x: hidden;
}
.header-wrap {
background: #1A1A1A;
}
.nav-bar {
padding: 0 16px;
display: flex;
align-items: center;
}
.back-btn {
font-size: 20px;
color: #FFFFFF;
margin-right: 12px;
}
.nav-title {
font-size: 18px;
font-weight: 700;
color: #FFFFFF;
}
.content-inner {
padding: 16px 14px 0;
}
/* 用户卡片 */
.user-card {
background: #FFFFFF;
border-radius: 20px;
padding: 20px;
margin-bottom: 16px;
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
}
.user-header {
display: flex;
align-items: center;
margin-bottom: 16px;
}
.avatar {
width: 56px;
height: 56px;
border-radius: 50%;
background: rgba(0,230,118,0.15);
display: flex;
align-items: center;
justify-content: center;
margin-right: 14px;
}
.avatar-text {
font-size: 20px;
font-weight: 700;
color: #00C853;
}
.user-info {
flex: 1;
}
.user-name {
font-size: 18px;
font-weight: 700;
color: #1E1E1E;
}
.user-tag {
font-size: 13px;
color: #00C853;
font-weight: 500;
}
.user-details {
background: #F5F7FA;
border-radius: 12px;
padding: 14px 16px;
}
.detail-row {
display: flex;
justify-content: space-between;
}
.detail-label {
font-size: 13px;
color: #7A7E84;
}
.detail-value {
font-size: 14px;
font-weight: 600;
color: #1E1E1E;
}
/* 业绩卡片 */
.performance-card {
background: #FFFFFF;
border-radius: 20px;
padding: 20px;
margin-bottom: 16px;
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
}
.period-text {
font-size: 12px;
color: #7A7E84;
}
.perf-grid {
display: flex;
justify-content: space-between;
margin-top: 14px;
margin-bottom: 16px;
}
.perf-item {
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
}
.perf-number {
font-size: 22px;
font-weight: 700;
color: #1E1E1E;
}
.perf-label {
font-size: 11px;
color: #7A7E84;
margin-top: 2px;
margin-bottom: 6px;
}
.perf-bar-wrap {
width: 40px;
height: 3px;
background: #EEEEEE;
border-radius: 2px;
overflow: hidden;
}
.perf-bar {
height: 100%;
border-radius: 2px;
}
.score-row {
display: flex;
justify-content: space-between;
align-items: center;
background: #F5F7FA;
border-radius: 16px;
padding: 16px 20px;
margin-top: 12px;
border-top: none;
}
.score-label {
font-size: 15px;
color: #7A7E84;
font-weight: 500;
}
.score-badge {
font-size: 38px;
font-weight: 800;
background: #FFFFFF;
border-radius: 12px;
padding: 4px 18px;
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
}
.score-badge.score-great {
color: #00C853;
background: #E8F5E9;
}
.score-badge.score-good {
color: #FF9800;
background: #FFF3E0;
}
.score-badge.score-low {
color: #F44336;
background: #FFEBEE;
}
.score-icon {
font-size: 34px;
}
/* 违规统计 */
.stats-card {
background: #1A1A1A;
border-radius: 20px;
padding: 18px 20px;
margin-bottom: 16px;
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
}
.stat-item {
display: flex;
flex-direction: column;
align-items: center;
}
.stat-number {
font-size: 28px;
font-weight: 700;
color: #F44336;
}
.stat-label {
font-size: 12px;
color: rgba(255,255,255,0.6);
margin-top: 4px;
}
/* 违规记录 */
.section-header {
margin-bottom: 12px;
}
.section-title {
font-size: 16px;
font-weight: 700;
color: #1E1E1E;
}
.empty-wrap {
display: flex;
justify-content: center;
padding: 30px 0;
}
.empty-text {
font-size: 14px;
color: #7A7E84;
}
.violation-list {
margin-bottom: 16px;
}
.violation-card {
background: #FFFFFF;
border-radius: 12px;
padding: 12px 16px;
margin-bottom: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
}
.violation-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 4px;
}
.violation-course {
font-size: 14px;
font-weight: 600;
color: #1E1E1E;
}
.violation-tag {
padding: 3px 10px;
border-radius: 12px;
font-size: 11px;
font-weight: 600;
}
.violation-tag.late { background: rgba(255,152,0,0.15); color: #FF9800; }
.violation-tag.absent { background: rgba(244,67,54,0.15); color: #F44336; }
.violation-tag.no-manual-end { background: rgba(33,150,243,0.15); color: #2196F3; }
.violation-time {
font-size: 12px;
color: #7A7E84;
}
/* 计算方式弹窗 */
.formula-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 999;
}
.formula-dialog {
width: 85%;
background: #FFFFFF;
border-radius: 20px;
padding: 24px 20px 20px;
box-shadow: 0 8px 24px rgba(0,0,0,0.15);
}
.formula-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 18px;
}
.formula-title {
font-size: 18px;
font-weight: 700;
color: #1E1E1E;
}
.formula-close {
font-size: 20px;
color: #7A7E84;
padding: 4px;
}
.formula-body {
margin-bottom: 20px;
}
.formula-text {
font-size: 15px;
font-weight: 600;
color: #1E1E1E;
line-height: 1.6;
}
.formula-divider {
height: 1px;
background: #EEEEEE;
margin: 14px 0;
}
.formula-example {
font-size: 13px;
color: #7A7E84;
line-height: 1.7;
white-space: pre-line;
}
.formula-footer {
display: flex;
justify-content: center;
}
.formula-btn {
width: 100%;
height: 44px;
background: #00C853;
color: #FFFFFF;
border-radius: 40px;
font-size: 15px;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
border: none;
}
.formula-btn::after { border: none; }
/* 退出登录 */
.logout-area {
padding: 20px 0;
}
.logout-btn {
width: 100%;
height: 50px;
background: #FFFFFF;
color: #F44336;
border-radius: 40px;
font-size: 16px;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
border: 2px solid #F44336;
}
.logout-btn::after { border: none; }
.bottom-safe {
height: 30px;
}
</style>