# 品牌方案 — 小程序端更新说明 ## 概述 后台管理系统配置的品牌方案(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 `) | | **租户识别** | 从 JWT Claims 中的 `tenantId` 自动提取 | | **签名** | 需要签名头(项目统一签名机制) | ### 响应数据结构 ```json { "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 调用层 ```js const request = require('../utils/request') module.exports = { getBrandConfig() { return request('/brand/config', { method: 'GET' }) } } ``` **说明**: - 复用项目现有的 `luch-request` 封装,自动注入 JWT Token 和签名头 - 返回 `Promise`,即 `BrandConfig` 对象 ### 2. store/brand.js — 品牌配置 Store ```js 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 — 应用入口 ```html ``` ### 4. 页面模板接入示例 以首页为例,将硬编码颜色替换为动态绑定: ```html ✦ 今日训练 {{ brandConfig.brandName }} {{ brandConfig.slogan }} ✓ 今日已签到 128 ``` 对应的 script 部分: ```js 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` 保证隔离。