增加品牌方案管理
This commit is contained in:
@@ -1,10 +1,14 @@
|
||||
<script>
|
||||
const brandStore = require('./store/brand')
|
||||
|
||||
export default {
|
||||
onLaunch: function() {
|
||||
console.log('Coach App Launch')
|
||||
brandStore.fetchConfig()
|
||||
},
|
||||
onShow: function() {
|
||||
console.log('Coach App Show')
|
||||
brandStore.fetchConfig()
|
||||
},
|
||||
onHide: function() {
|
||||
console.log('Coach App Hide')
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
const http = require('../utils/request')
|
||||
|
||||
module.exports = {
|
||||
/**
|
||||
* 获取品牌配置(需 JWT 登录态,自动按 tenantId 返回对应租户配置)
|
||||
* @returns {Promise<Object>} 品牌配置对象
|
||||
*/
|
||||
getBrandConfig() {
|
||||
return http.get('/brand')
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<view class="detail-page">
|
||||
<view class="header-wrap" :style="{ paddingTop: statusBarHeight + 'px' }">
|
||||
<view class="header-wrap" :style="headerStyle">
|
||||
<view class="nav-bar" :style="{ height: navBarHeight + 'px' }">
|
||||
<text class="back-btn" @click="goBack">←</text>
|
||||
<text class="nav-title">课程详情</text>
|
||||
@@ -11,7 +11,7 @@
|
||||
<view class="cover-area">
|
||||
<image v-if="course.coverImage && !loading" class="cover-img" :src="course.coverImage" mode="aspectFill"
|
||||
@error="course.coverError = true" />
|
||||
<view v-if="!course.coverImage || course.coverError || loading" class="cover-bg">
|
||||
<view v-if="!course.coverImage || course.coverError || loading" class="cover-bg" :style="{ background: coverGradient }">
|
||||
<text class="cover-text">{{ (course.typeName ? course.typeName.slice(0, 2) : '团课') }}</text>
|
||||
</view>
|
||||
<view class="cover-status" :class="courseStatusClass" v-if="!loading">
|
||||
@@ -47,7 +47,7 @@
|
||||
</view>
|
||||
<view class="info-item" v-if="course.storedValueAmount">
|
||||
<text class="info-label">消耗</text>
|
||||
<text class="info-value price-text">¥{{ course.storedValueAmount }}</text>
|
||||
<text class="info-value price-text" :style="{ color: brandConfig.primaryColor }">¥{{ course.storedValueAmount }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -63,6 +63,7 @@
|
||||
v-if="course.status == 0"
|
||||
class="action-btn start-btn"
|
||||
:class="{ disabled: !canStartCourse }"
|
||||
:style="startBtnStyle"
|
||||
:loading="actionLoading"
|
||||
:disabled="actionLoading || !canStartCourse"
|
||||
@click="handleStartCourse"
|
||||
@@ -100,6 +101,7 @@
|
||||
<script>
|
||||
const coachApi = require('../../api/coach')
|
||||
const store = require('../../store/index')
|
||||
const brandStore = require('../../store/brand')
|
||||
const { resolveCoverUrl } = require('../../utils/request')
|
||||
|
||||
export default {
|
||||
@@ -113,11 +115,18 @@
|
||||
actionLoading: false,
|
||||
course: {},
|
||||
now: Date.now(),
|
||||
realMemberCount: 0
|
||||
realMemberCount: 0,
|
||||
brandConfig: brandStore.config
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
duration() {
|
||||
headerStyle() { return 'padding-top:' + this.statusBarHeight + 'px;background-color:' + this.brandConfig.secondaryColor },
|
||||
startBtnStyle() {
|
||||
if (!this.canStartCourse) return ''
|
||||
return 'background-color:' + this.brandConfig.primaryColor + ';color:' + this.brandConfig.secondaryColor
|
||||
},
|
||||
coverGradient() { return 'linear-gradient(135deg, ' + this.brandConfig.primaryColor + ', #00BFA5)' },
|
||||
duration() {
|
||||
if (!this.course.startTime || !this.course.endTime) return '--'
|
||||
const start = new Date(this.course.startTime).getTime()
|
||||
const end = new Date(this.course.endTime).getTime()
|
||||
@@ -158,6 +167,11 @@
|
||||
this.courseId = options.id
|
||||
this.loadDetail()
|
||||
}
|
||||
|
||||
uni.$on('brand:updated', (config) => { this.brandConfig = config })
|
||||
},
|
||||
onUnload() {
|
||||
uni.$off('brand:updated')
|
||||
},
|
||||
onShow() {
|
||||
if (!store.isLoggedIn) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<view class="course-list-page">
|
||||
<view class="header-wrap" :style="{ paddingTop: statusBarHeight + 'px' }">
|
||||
<view class="header-wrap" :style="headerStyle">
|
||||
<view class="nav-bar" :style="{ height: navBarHeight + 'px' }">
|
||||
<text class="back-btn" @click="goBack">←</text>
|
||||
<text class="nav-title">我的团课</text>
|
||||
@@ -12,6 +12,7 @@
|
||||
:key="idx"
|
||||
class="tab-item"
|
||||
:class="{ active: currentTab === tab.value }"
|
||||
:style="currentTab === tab.value ? tabActiveStyle : ''"
|
||||
@click="switchTab(tab.value)"
|
||||
>
|
||||
<text>{{ tab.label }}</text>
|
||||
@@ -33,7 +34,7 @@
|
||||
<view v-for="course in filteredCourses" :key="course.id" class="course-card" :class="{ grayed: !course._isToday }" @click="goToDetail(course.id)">
|
||||
<view class="card-header">
|
||||
<text class="course-name">{{ course.courseName }}</text>
|
||||
<view class="status-tag" :class="course._statusClass">
|
||||
<view class="status-tag" :class="course._statusClass" :style="course._statusClass === 'normal' ? statusNormalStyle : ''">
|
||||
<text>{{ course._statusLabel }}</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -62,6 +63,7 @@
|
||||
<script>
|
||||
const coachApi = require('../../api/coach')
|
||||
const store = require('../../store/index')
|
||||
const brandStore = require('../../store/brand')
|
||||
|
||||
export default {
|
||||
data() {
|
||||
@@ -78,10 +80,14 @@
|
||||
{ label: '待开课', value: 'pending' },
|
||||
{ label: '进行中', value: 'in_progress' },
|
||||
{ label: '已结束', value: 'ended' }
|
||||
]
|
||||
],
|
||||
brandConfig: brandStore.config
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
headerStyle() { return 'padding-top:' + this.statusBarHeight + 'px;background-color:' + this.brandConfig.secondaryColor },
|
||||
tabActiveStyle() { return 'background-color:' + this.brandConfig.primaryColor + ';color:' + this.brandConfig.secondaryColor },
|
||||
statusNormalStyle() { return 'background-color:rgba(' + this.brandConfig.primaryColorRgb + ',0.15);color:' + this.brandConfig.primaryColor },
|
||||
filteredCourses() {
|
||||
switch (this.currentTab) {
|
||||
case 'pending':
|
||||
@@ -109,14 +115,24 @@
|
||||
this.navBarHeight = navBarHeight
|
||||
// 状态栏 + 导航栏 + tab行高度
|
||||
this.totalHeaderHeight = statusBarHeight + navBarHeight + 44
|
||||
|
||||
uni.$on('brand:updated', (config) => { this.brandConfig = config })
|
||||
},
|
||||
onUnload() {
|
||||
uni.$off('brand:updated')
|
||||
},
|
||||
onShow() {
|
||||
brandStore.fetchConfig()
|
||||
if (!store.isLoggedIn) {
|
||||
uni.reLaunch({ url: '/pages/login/login' })
|
||||
return
|
||||
}
|
||||
this.loadCourses()
|
||||
},
|
||||
onPullDownRefresh() {
|
||||
brandStore.fetchConfig()
|
||||
this.loadCourses().finally(() => { uni.stopPullDownRefresh() })
|
||||
},
|
||||
methods: {
|
||||
goBack() { uni.navigateBack() },
|
||||
goToDetail(id) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<view class="home-page">
|
||||
<view class="header-wrap" :style="{ paddingTop: statusBarHeight + 'px' }">
|
||||
<view class="nav-bar" :style="{ height: navBarHeight + 'px' }">
|
||||
<view class="header-wrap" :style="headerStyle">
|
||||
<view class="nav-bar" :style="navStyle">
|
||||
<text class="nav-title">◆ Novalon 教练端</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -9,10 +9,10 @@
|
||||
<scroll-view class="content" scroll-y :style="{ height: 'calc(100vh - ' + totalHeaderHeight + 'px)' }">
|
||||
<view class="content-inner">
|
||||
<!-- 教练欢迎卡片 -->
|
||||
<view class="greeting-card">
|
||||
<view class="greeting-card" :style="{ backgroundColor: brandConfig.secondaryColor }">
|
||||
<view class="greeting-text">
|
||||
<text class="greeting-label">{{ greetingLabel }}</text>
|
||||
<text class="coach-name">{{ coachName }} 教练</text>
|
||||
<text class="coach-name" :style="{ color: brandConfig.primaryColor }">{{ coachName }} 教练</text>
|
||||
</view>
|
||||
<view class="greeting-desc">
|
||||
<text>祝您今日授课顺利</text>
|
||||
@@ -22,17 +22,17 @@
|
||||
<!-- 今日统计 -->
|
||||
<view class="stats-card">
|
||||
<view class="stat-item">
|
||||
<text class="stat-number">{{ todayStats.pending }}</text>
|
||||
<text class="stat-number" :style="{ color: brandConfig.primaryColor }">{{ todayStats.pending }}</text>
|
||||
<text class="stat-label">待开课</text>
|
||||
</view>
|
||||
<view class="stat-divider"></view>
|
||||
<view class="stat-item">
|
||||
<text class="stat-number">{{ todayStats.inProgress }}</text>
|
||||
<text class="stat-number" :style="{ color: brandConfig.primaryColor }">{{ todayStats.inProgress }}</text>
|
||||
<text class="stat-label">进行中</text>
|
||||
</view>
|
||||
<view class="stat-divider"></view>
|
||||
<view class="stat-item">
|
||||
<text class="stat-number">{{ todayStats.ended }}</text>
|
||||
<text class="stat-number" :style="{ color: brandConfig.primaryColor }">{{ todayStats.ended }}</text>
|
||||
<text class="stat-label">已结束</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -43,15 +43,15 @@
|
||||
</view>
|
||||
<view class="entry-grid">
|
||||
<view class="entry-card" @click="goToCourseList">
|
||||
<view class="entry-icon-wrap green-bg">
|
||||
<text class="entry-icon">☰</text>
|
||||
<view class="entry-icon-wrap green-bg" :style="{ backgroundColor: 'rgba(' + brandConfig.primaryColorRgb + ',0.15)' }">
|
||||
<text class="entry-icon" :style="{ color: brandConfig.primaryColor }">☰</text>
|
||||
</view>
|
||||
<text class="entry-name">我的团课</text>
|
||||
<text class="entry-desc">查看和管理课程</text>
|
||||
</view>
|
||||
<view class="entry-card" @click="goToProfile">
|
||||
<view class="entry-icon-wrap dark-bg">
|
||||
<text class="entry-icon">☺</text>
|
||||
<view class="entry-icon-wrap dark-bg" :style="{ backgroundColor: 'rgba(' + brandConfig.secondaryColorRgb + ',0.1)' }">
|
||||
<text class="entry-icon" :style="{ color: brandConfig.primaryColor }">☺</text>
|
||||
</view>
|
||||
<text class="entry-name">个人中心</text>
|
||||
<text class="entry-desc">信息与违规记录</text>
|
||||
@@ -67,6 +67,7 @@
|
||||
<script>
|
||||
const store = require('../../store/index')
|
||||
const coachApi = require('../../api/coach')
|
||||
const brandStore = require('../../store/brand')
|
||||
|
||||
export default {
|
||||
data() {
|
||||
@@ -79,10 +80,13 @@
|
||||
pending: 0,
|
||||
inProgress: 0,
|
||||
ended: 0
|
||||
}
|
||||
},
|
||||
brandConfig: brandStore.config
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
headerStyle() { return 'padding-top:' + this.statusBarHeight + 'px;background-color:' + this.brandConfig.secondaryColor },
|
||||
navStyle() { return 'height:' + this.navBarHeight + 'px;background-color:' + this.brandConfig.secondaryColor },
|
||||
greetingLabel() {
|
||||
const hour = new Date().getHours()
|
||||
if (hour < 6) return '夜深了,'
|
||||
@@ -104,6 +108,11 @@
|
||||
this.statusBarHeight = statusBarHeight
|
||||
this.navBarHeight = navBarHeight
|
||||
this.totalHeaderHeight = statusBarHeight + navBarHeight
|
||||
|
||||
uni.$on('brand:updated', (config) => { this.brandConfig = config })
|
||||
},
|
||||
onUnload() {
|
||||
uni.$off('brand:updated')
|
||||
},
|
||||
onShow() {
|
||||
if (!store.isLoggedIn) {
|
||||
@@ -114,7 +123,8 @@
|
||||
this.loadStats()
|
||||
},
|
||||
onPullDownRefresh() {
|
||||
this.loadStats().finally(() => { uni.stopPullDownRefresh() })
|
||||
Promise.all([this.loadStats(), brandStore.fetchConfig()])
|
||||
.finally(() => { uni.stopPullDownRefresh() })
|
||||
},
|
||||
methods: {
|
||||
goToCourseList() {
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
<template>
|
||||
<view class="login-page" :style="{ paddingTop: statusBarHeight + 'px' }">
|
||||
<view class="login-page" :style="{ paddingTop: statusBarHeight + 'px', backgroundColor: brandConfig.secondaryColor }">
|
||||
<view class="brand-area">
|
||||
<view class="logo-icon">
|
||||
<text class="logo-symbol">◆</text>
|
||||
<view class="logo-wrap" :style="{ backgroundColor: brandConfig.logoUrl ? 'transparent' : 'rgba(' + brandConfig.primaryColorRgb + ',0.15)' }">
|
||||
<image v-if="brandConfig.logoUrl" class="logo-img" :src="brandConfig.logoUrl" mode="aspectFit" />
|
||||
<text v-else class="logo-symbol" :style="{ color: brandConfig.primaryColor }">◆</text>
|
||||
</view>
|
||||
<text class="app-name">Novalon 教练端</text>
|
||||
<text class="app-slogan">高效管理 · 轻松授课</text>
|
||||
<text class="app-name">{{ brandConfig.brandName }}</text>
|
||||
<text class="app-slogan" v-if="brandConfig.slogan">{{ brandConfig.slogan }}</text>
|
||||
</view>
|
||||
|
||||
<view class="login-form">
|
||||
@@ -33,13 +34,13 @@
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<button class="login-btn" :loading="loading" :disabled="loading || !username || !password" @click="handleLogin">
|
||||
<button class="login-btn" :style="loginBtnStyle" :loading="loading" :disabled="loading || !username || !password" @click="handleLogin">
|
||||
{{ loading ? '登录中...' : '登 录' }}
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<view class="footer-tip">
|
||||
<text>Novalon 健身房管理系统</text>
|
||||
<text>{{ brandConfig.brandName }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
@@ -47,6 +48,7 @@
|
||||
<script>
|
||||
const coachApi = require('../../api/coach')
|
||||
const store = require('../../store/index')
|
||||
const brandStore = require('../../store/brand')
|
||||
|
||||
export default {
|
||||
data() {
|
||||
@@ -55,9 +57,13 @@
|
||||
username: '',
|
||||
password: '',
|
||||
showPassword: false,
|
||||
loading: false
|
||||
loading: false,
|
||||
brandConfig: brandStore.config
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
loginBtnStyle() { return 'background-color:' + this.brandConfig.primaryColor + ';color:' + this.brandConfig.secondaryColor }
|
||||
},
|
||||
onLoad() {
|
||||
const systemInfo = uni.getSystemInfoSync()
|
||||
this.statusBarHeight = systemInfo.statusBarHeight || 20
|
||||
@@ -65,6 +71,11 @@
|
||||
if (store.isLoggedIn) {
|
||||
uni.reLaunch({ url: '/pages/index/index' })
|
||||
}
|
||||
|
||||
uni.$on('brand:updated', (config) => { this.brandConfig = config })
|
||||
},
|
||||
onUnload() {
|
||||
uni.$off('brand:updated')
|
||||
},
|
||||
methods: {
|
||||
async handleLogin() {
|
||||
@@ -113,7 +124,7 @@
|
||||
<style scoped>
|
||||
.login-page {
|
||||
min-height: 100vh;
|
||||
background: #F5F7FA;
|
||||
background: #1A1A1A;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
@@ -128,29 +139,18 @@
|
||||
margin-top: 80px;
|
||||
margin-bottom: 60px;
|
||||
}
|
||||
.logo-icon {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
background: rgba(0,230,118,0.15);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.logo-symbol {
|
||||
font-size: 36px;
|
||||
color: #00C853;
|
||||
}
|
||||
.logo-wrap { width: 120px; height: 120px; border-radius: 50%; display: flex; align-items: center; justify-content: center; margin-bottom: 24px; overflow: hidden; }
|
||||
.logo-img { width: 100%; height: 100%; }
|
||||
.logo-symbol { font-size: 52px; color: #00C853; }
|
||||
.app-name {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: #1E1E1E;
|
||||
color: #FFFFFF;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
.app-slogan {
|
||||
font-size: 14px;
|
||||
color: #7A7E84;
|
||||
color: rgba(255,255,255,0.6);
|
||||
margin-top: 8px;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
@@ -164,28 +164,27 @@
|
||||
.input-group {
|
||||
width: 100%;
|
||||
height: 52px;
|
||||
background: #FFFFFF;
|
||||
background: rgba(255,255,255,0.1);
|
||||
border-radius: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 18px;
|
||||
margin-bottom: 14px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
|
||||
}
|
||||
.input-icon {
|
||||
font-size: 18px;
|
||||
margin-right: 10px;
|
||||
color: #7A7E84;
|
||||
color: rgba(255,255,255,0.5);
|
||||
}
|
||||
.login-input {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
font-size: 15px;
|
||||
color: #1E1E1E;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
.toggle-pwd {
|
||||
font-size: 18px;
|
||||
color: #7A7E84;
|
||||
color: rgba(255,255,255,0.5);
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
@@ -212,6 +211,6 @@
|
||||
position: absolute;
|
||||
bottom: 60px;
|
||||
font-size: 12px;
|
||||
color: #BDBDBD;
|
||||
color: rgba(255,255,255,0.3);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<view class="profile-page">
|
||||
<view class="header-wrap" :style="{ paddingTop: statusBarHeight + 'px' }">
|
||||
<view class="header-wrap" :style="headerStyle">
|
||||
<view class="nav-bar" :style="{ height: navBarHeight + 'px' }">
|
||||
<text class="back-btn" @click="goBack">←</text>
|
||||
<text class="nav-title">个人中心</text>
|
||||
@@ -12,13 +12,13 @@
|
||||
<!-- 教练信息卡片 -->
|
||||
<view class="user-card">
|
||||
<view class="user-header">
|
||||
<view class="avatar">
|
||||
<text class="avatar-text">CC</text>
|
||||
<view class="avatar" :style="{ backgroundColor: 'rgba(' + brandConfig.primaryColorRgb + ',0.15)' }">
|
||||
<text class="avatar-text" :style="{ color: brandConfig.primaryColor }">CC</text>
|
||||
</view>
|
||||
<view class="user-info">
|
||||
<text class="user-name">{{ coachInfo.username }}</text>
|
||||
<view class="user-tag">
|
||||
<text>教练</text>
|
||||
<text :style="{ color: brandConfig.primaryColor }">教练</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -49,26 +49,26 @@
|
||||
<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 class="perf-bar" :style="attendanceBarStyle"></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 class="perf-bar" :style="fullnessBarStyle"></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 class="score-badge" :class="scoreClass" :style="scoreClass === 'score-great' ? scoreGreatStyle : ''">{{ scoreText }}</text>
|
||||
<text v-if="scoreText !== '--'" class="score-icon">★</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 违规统计 -->
|
||||
<view v-if="violations.length > 0" class="stats-card">
|
||||
<view v-if="violations.length > 0" class="stats-card" :style="{ backgroundColor: brandConfig.secondaryColor }">
|
||||
<view class="stat-item">
|
||||
<text class="stat-number">{{ violations.length }}</text>
|
||||
<text class="stat-label">违规次数</text>
|
||||
@@ -105,7 +105,7 @@
|
||||
<text class="formula-example">{{ formulaExample }}</text>
|
||||
</view>
|
||||
<view class="formula-footer">
|
||||
<button class="formula-btn" @click="closeFormula">知道了</button>
|
||||
<button class="formula-btn" :style="{ backgroundColor: brandConfig.primaryColor }" @click="closeFormula">知道了</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -124,6 +124,7 @@
|
||||
<script>
|
||||
const store = require('../../store/index')
|
||||
const coachApi = require('../../api/coach')
|
||||
const brandStore = require('../../store/brand')
|
||||
|
||||
export default {
|
||||
data() {
|
||||
@@ -150,9 +151,16 @@
|
||||
formulaVisible: false,
|
||||
formulaTitle: '',
|
||||
formulaContent: '',
|
||||
formulaExample: ''
|
||||
formulaExample: '',
|
||||
brandConfig: brandStore.config
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
headerStyle() { return 'padding-top:' + this.statusBarHeight + 'px;background-color:' + this.brandConfig.secondaryColor },
|
||||
scoreGreatStyle() { return 'color:' + this.brandConfig.primaryColor + ';background-color:rgba(' + this.brandConfig.primaryColorRgb + ',0.15)' },
|
||||
attendanceBarStyle() { return { width: this.performance.attendanceRate + '%', background: this.getRateColor(this.performance.attendanceRate) } },
|
||||
fullnessBarStyle() { return { width: this.performance.fullnessRate + '%', background: this.getRateColor(this.performance.fullnessRate) } }
|
||||
},
|
||||
onLoad() {
|
||||
const systemInfo = uni.getSystemInfoSync()
|
||||
const statusBarHeight = systemInfo.statusBarHeight || 20
|
||||
@@ -166,8 +174,14 @@
|
||||
this.statusBarHeight = statusBarHeight
|
||||
this.navBarHeight = navBarHeight
|
||||
this.totalHeaderHeight = statusBarHeight + navBarHeight
|
||||
|
||||
uni.$on('brand:updated', (config) => { this.brandConfig = config })
|
||||
},
|
||||
onUnload() {
|
||||
uni.$off('brand:updated')
|
||||
},
|
||||
onShow() {
|
||||
brandStore.fetchConfig()
|
||||
if (!store.isLoggedIn) {
|
||||
uni.reLaunch({ url: '/pages/login/login' })
|
||||
return
|
||||
@@ -178,6 +192,12 @@
|
||||
},
|
||||
methods: {
|
||||
goBack() { uni.navigateBack() },
|
||||
getRateColor(rate) {
|
||||
if (rate == null) return '#CCCCCC'
|
||||
if (rate >= 80) return this.brandConfig.primaryColor
|
||||
if (rate >= 60) return '#FF9800'
|
||||
return '#F44336'
|
||||
},
|
||||
|
||||
async loadPerformance() {
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
const brandApi = require('../api/brand')
|
||||
|
||||
const BRAND_CACHE_KEY = 'brand_config_cache'
|
||||
|
||||
const DEFAULT_CONFIG = {
|
||||
brandName: 'Novalon教练端',
|
||||
slogan: '高效管理 · 轻松授课',
|
||||
primaryColor: '#00E676',
|
||||
primaryColorRgb: '0,230,118',
|
||||
secondaryColor: '#1A1A1A',
|
||||
secondaryColorRgb: '26,26,26',
|
||||
logoUrl: '',
|
||||
backgroundImageUrl: '',
|
||||
updatedAt: ''
|
||||
}
|
||||
|
||||
/**
|
||||
* 品牌方案 Store
|
||||
*
|
||||
* 响应式策略:fetchConfig 更新后通过 uni.$emit('brand:updated', config) 广播,
|
||||
* 页面通过 uni.$on 监听并在 data 中维护副本,确保 UI 自动刷新。
|
||||
*/
|
||||
const brandStore = {
|
||||
_config: null,
|
||||
_loaded: false,
|
||||
|
||||
/** 获取当前品牌配置(优先内存,其次缓存,最后默认值) */
|
||||
get config() {
|
||||
if (this._config) return this._config
|
||||
try {
|
||||
const cached = uni.getStorageSync(BRAND_CACHE_KEY)
|
||||
if (cached && cached.primaryColor) {
|
||||
this._config = cached
|
||||
return cached
|
||||
}
|
||||
} catch (e) { /* ignore */ }
|
||||
return { ...DEFAULT_CONFIG }
|
||||
},
|
||||
|
||||
/** 是否已从后端加载完成 */
|
||||
get isLoaded() {
|
||||
return this._loaded
|
||||
},
|
||||
|
||||
/**
|
||||
* 从后端拉取品牌配置
|
||||
* 策略:对比 updatedAt,有变化时更新缓存并广播事件通知全部页面
|
||||
* @returns {Promise<boolean>} 是否有变化
|
||||
*/
|
||||
async fetchConfig() {
|
||||
try {
|
||||
const res = await brandApi.getBrandConfig()
|
||||
if (res) {
|
||||
const newConfig = {
|
||||
brandName: res.brandName || DEFAULT_CONFIG.brandName,
|
||||
slogan: res.slogan || DEFAULT_CONFIG.slogan,
|
||||
primaryColor: res.primaryColor || DEFAULT_CONFIG.primaryColor,
|
||||
primaryColorRgb: res.primaryColorRgb || DEFAULT_CONFIG.primaryColorRgb,
|
||||
secondaryColor: res.secondaryColor || DEFAULT_CONFIG.secondaryColor,
|
||||
secondaryColorRgb: res.secondaryColorRgb || DEFAULT_CONFIG.secondaryColorRgb,
|
||||
logoUrl: res.logoUrl || '',
|
||||
backgroundImageUrl: res.backgroundImageUrl || '',
|
||||
updatedAt: res.updatedAt || ''
|
||||
}
|
||||
|
||||
if (!this._config || this._config.updatedAt !== newConfig.updatedAt) {
|
||||
this._config = newConfig
|
||||
try {
|
||||
uni.setStorageSync(BRAND_CACHE_KEY, newConfig)
|
||||
} catch (e) { /* ignore */ }
|
||||
this._loaded = true
|
||||
// 广播事件通知所有页面刷新
|
||||
uni.$emit('brand:updated', newConfig)
|
||||
return true
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[BrandStore] 获取品牌配置失败:', e)
|
||||
}
|
||||
|
||||
this._loaded = true
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = brandStore
|
||||
Reference in New Issue
Block a user