13 KiB
13 KiB
品牌方案 — 小程序端更新说明
概述
后台管理系统配置的品牌方案(Logo、主色调、辅助色、背景图、品牌名、口号)通过 REST API 实时同步至会员端和教练端 UniApp 小程序。小程序冷启动、页面显示、下拉刷新时自动拉取最新配置。
架构总览
┌──────────────────────┐ ┌──────────────────────┐
│ 后台管理端 (web) │ │ 小程序端 (uniapp) │
│ │ │ │
│ BrandManagement.vue │ │ App.vue onLaunch │
│ 保存品牌配置 ──────────┐ │ │ │
│ │ │ │ brandStore │
│ POST /api/brand/logo│ │ │ .fetchConfig() │
│ PUT /api/brand/color │ │ │ │
│ PUT /api/brand/info│ │ │ GET /api/brand │
│ ↓ │ │ │ /config (JWT) │
│ ┌─────────────┐ │ │ │ │ │
│ │ PostgreSQL │◄────┼──────┼────┼────┘ │
│ │ brand_config│ │ │ │ brandStore.config │
│ └─────────────┘ │ │ │ → 各页面动态绑定 │
│ │ │ │ │
└──────────────────────┘ │ └──────────────────────┘
API 接口
GET /api/brand/config
获取当前租户的品牌配置。
| 项目 | 说明 |
|---|---|
| 方法 | GET |
| 路径 | /api/brand/config |
| 认证 | 需要 JWT Token(Authorization: Bearer <token>) |
| 租户识别 | 从 JWT Claims 中的 tenantId 自动提取 |
| 签名 | 需要签名头(项目统一签名机制) |
响应数据结构
{
"id": 1,
"tenantId": "default",
"logoUrl": "http://localhost:8084/api/files/preview/brand/logo/logo_xxx.jpeg",
"backgroundImageUrl": null,
"primaryColor": "#00E676",
"primaryColorRgb": "0,230,118",
"secondaryColor": "#1A1A1A",
"secondaryColorRgb": "26,26,26",
"fontFamily": null,
"brandName": "Novalon健身房",
"slogan": "专业健身,从这里开始",
"createdAt": "2026-07-23T17:37:36",
"updatedAt": "2026-07-23T17:37:59"
}
字段说明
| 字段 | 类型 | 说明 |
|---|---|---|
id |
Long | 记录 ID |
tenantId |
String | 租户 ID |
logoUrl |
String | Logo 图片完整 URL |
backgroundImageUrl |
String | 背景图完整 URL(可为 null) |
primaryColor |
String | 主色调 HEX 值,如 #00E676 |
primaryColorRgb |
String | 主色调 RGB 值,如 0,230,118 |
secondaryColor |
String | 辅助色 HEX 值,如 #1A1A1A |
secondaryColorRgb |
String | 辅助色 RGB 值,如 26,26,26 |
fontFamily |
String | 字体族(预留) |
brandName |
String | 品牌名称,最长 100 字符 |
slogan |
String | 品牌口号,最长 200 字符 |
createdAt |
String | 创建时间(ISO 格式) |
updatedAt |
String | 最后更新时间(ISO 格式),用作版本号 |
小程序端接入指南
文件结构
两个小程序项目结构相同:
项目根目录/
├── api/
│ └── brand.js ← 品牌配置 API 层
├── store/
│ └── brand.js ← 品牌配置 Store(含缓存逻辑)
├── App.vue ← 入口,冷启动/前台切换时拉取配置
└── pages/
├── index/index.vue ← 首页
├── login/login.vue ← 登录页
├── profile/profile.vue← 个人中心
└── ...
1. api/brand.js — API 调用层
const request = require('../utils/request')
module.exports = {
getBrandConfig() {
return request('/brand/config', { method: 'GET' })
}
}
说明:
- 复用项目现有的
luch-request封装,自动注入 JWT Token 和签名头 - 返回
Promise<Object>,即BrandConfig对象
2. store/brand.js — 品牌配置 Store
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: ''
}
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) {}
return { ...DEFAULT_CONFIG }
},
async fetchConfig() {
let cached = null
try { cached = uni.getStorageSync(BRAND_CACHE_KEY) } catch (e) {}
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
uni.setStorageSync(BRAND_CACHE_KEY, newConfig)
this._loaded = true
return true
}
}
} catch (e) {
console.error('[BrandStore] 获取品牌配置失败:', e)
}
this._loaded = true
return false
},
applyTabBar() {
const cfg = this.config
uni.setTabBarStyle({
color: '#7A7E84',
selectedColor: cfg.primaryColor,
backgroundColor: cfg.secondaryColor,
borderStyle: 'black'
})
}
}
module.exports = brandStore
3. App.vue — 应用入口
<script>
const brandStore = require('./store/brand')
export default {
onLaunch: function() {
console.log('App Launch')
brandStore.fetchConfig().then(changed => {
if (changed) brandStore.applyTabBar()
})
setTimeout(() => { brandStore.applyTabBar() }, 300)
},
onShow: function() {
console.log('App Show')
brandStore.fetchConfig().then(changed => {
if (changed) brandStore.applyTabBar()
})
}
}
</script>
4. 页面模板接入示例
以首页为例,将硬编码颜色替换为动态绑定:
<!-- 导航栏:辅助色背景 -->
<view class="header-wrap" :style="{ backgroundColor: brandConfig.secondaryColor }">
<view class="nav-bar" :style="{ backgroundColor: brandConfig.secondaryColor }">
<text class="nav-title">✦ 今日训练</text>
</view>
</view>
<!-- 品牌展示:动态名称/Slogan/Logo -->
<view class="brand-section">
<image class="brand-logo" :src="brandConfig.logoUrl || '/static/logo.png'" mode="aspectFit" />
<view class="brand-text">
<text class="brand-name">{{ brandConfig.brandName }}</text>
<text class="brand-slogan">{{ brandConfig.slogan }}</text>
</view>
</view>
<!-- 问候卡片:辅助色背景 + 背景图 -->
<view class="greeting-card" :style="{
backgroundColor: brandConfig.secondaryColor,
backgroundImage: brandConfig.backgroundImageUrl ? 'url(' + brandConfig.backgroundImageUrl + ')' : 'none',
backgroundSize: 'cover',
backgroundPosition: 'center'
}">
<!-- ... -->
</view>
<!-- 主色调元素 -->
<view class="checkin-badge" :style="{
backgroundColor: 'rgba(' + brandConfig.primaryColorRgb + ',0.2)',
color: brandConfig.primaryColor
}">
✓ 今日已签到
</view>
<text class="stat-number" :style="{ color: brandConfig.primaryColor }">128</text>
对应的 script 部分:
const brandStore = require('../../store/brand')
export default {
computed: {
brandConfig() { return brandStore.config }
},
onShow() {
brandStore.fetchConfig()
// ... other data loading
},
onPullDownRefresh() {
brandStore.fetchConfig().finally(() => { uni.stopPullDownRefresh() })
}
}
品牌字段与页面元素映射表
| 品牌字段 | 应用的元素 |
|---|---|
primaryColor |
按钮背景色、选中标签背景色、价格文字、难度星星高亮、签到成功标记、功能入口图标 |
primaryColorRgb |
半透明遮罩背景(如签到徽章 rgba(xxx, 0.2))、功能入口圆形容器背景 |
secondaryColor |
自定义导航栏背景、问候卡片背景、统计卡片背景 |
secondaryColorRgb |
半透明副色遮罩 |
logoUrl |
首页品牌展示 Logo 图片 src 属性 |
backgroundImageUrl |
问候卡片背景图(CSS background-image) |
brandName |
首页品牌名称文本、登录页应用名称 |
slogan |
首页品牌口号文本、登录页口号文本 |
更新时机
| 时机 | 触发方式 | 说明 |
|---|---|---|
| 冷启动 | App.vue → onLaunch |
先读本地缓存立即渲染,后台异步拉取,对比 updatedAt 决定是否更新 |
| 切回前台 | App.vue → onShow |
用户从后台切回时拉取最新配置 |
| 下拉刷新 | 各页面 onPullDownRefresh |
手动刷新时同步拉取品牌配置 |
| TabBar 切换 | App.vue → onShow + 各页面 onShow |
Tab 页切换时所在页触发 |
缓存策略
本地缓存 Key: brand_config_cache
存储数据: { brandName, slogan, primaryColor, primaryColorRgb,
secondaryColor, secondaryColorRgb, logoUrl,
backgroundImageUrl, updatedAt }
更新策略:
1. 启动时读取缓存 → 立即渲染
2. 后台调 API GET /api/brand/config
3. 对比 API 返回的 updatedAt 与缓存的 updatedAt
- 相同 → 不更新,节省渲染开销
- 不同 → 更新内存 + 本地缓存,触发 UI 刷新
4. 如果 API 调用失败 → 保持缓存值不变,不影响用户体验
涉及的页面清单
会员端 (gym-manage-uniapp)
| 页面 | 文件 | 动态元素 |
|---|---|---|
| 首页 | pages/index/index.vue |
品牌展示(名称/口号/Logo)、导航栏背景、问候卡背景/背景图、签到徽章、统计数据、课程卡片价格、查看全部链接 |
| 团课搜索 | pages/search/search.vue |
导航栏背景、搜索区背景、时间段筛选、类型筛选、排序激活态、预约按钮、课程价格 |
| 我的课程 | pages/my-courses/my-courses.vue |
导航栏背景、空状态跳转链接 |
| 课程详情 | pages/course-detail/course-detail.vue |
导航栏背景、封面渐变、预约按钮 |
| 个人中心 | pages/profile/profile.vue |
导航栏背景、统计卡片背景、签到数字、会员标签、签到标记、保存按钮 |
| 登录 | pages/login/login.vue |
品牌名称/口号、Logo 图标、微信登录按钮、协议链接 |
教练端 (gym-manage-coach-uniapp)
| 页面 | 文件 | 动态元素 |
|---|---|---|
| 首页 | pages/index/index.vue |
导航栏背景、欢迎卡片、教练名、统计数字、功能入口图标 |
| 课程列表 | pages/course-list/course-list.vue |
导航栏背景、Tab 筛选、状态标签 |
| 课程详情 | pages/course-detail/course-detail.vue |
导航栏背景、封面渐变、开课按钮、价格文字 |
| 个人中心 | pages/profile/profile.vue |
导航栏背景、头像容器、标签颜色、统计卡片、评分徽章、公式按钮 |
| 登录 | pages/login/login.vue |
品牌名称/口号、Logo 图标、登录按钮 |
Q&A
Q: 后端不可用时怎么办?
A: brandStore 内置默认配置(与原硬编码值完全一致),API 失败时自动回退到默认值,用户无感知。
Q: 如何确认品牌配置已生效?
A: 后台管理端修改配色/Logo/名称并保存后,小程序重新打开或下拉刷新即可看到变化。冷启动首次渲染使用缓存值(上次成功拉取的),后台静默更新。
Q: 不同租户会互相影响吗?
A: 不会。GET /api/brand/config 从 JWT Token 中提取 tenantId,只返回当前租户的配置。数据库层面也有唯一索引 UNIQUE(tenant_id) WHERE deleted_at IS NULL 保证隔离。