From 088ff87ea34941a2b0aabc406f3b223f5e36d44b Mon Sep 17 00:00:00 2001 From: "Ray.Hao" <1490493387@qq.com> Date: Tue, 22 Jul 2025 23:21:44 +0800 Subject: [PATCH] =?UTF-8?q?wip:=20=E4=B8=B4=E6=97=B6=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README-theme.md | 190 ----------- README-wechat-login.md | 609 ----------------------------------- src/composables/useTabbar.ts | 60 ++++ src/composables/useTheme.ts | 294 +++++++++-------- src/layouts/default.vue | 40 +-- src/layouts/tabbar.vue | 138 ++------ src/main.ts | 8 - src/styles/index.scss | 2 - src/styles/theme.scss | 190 ----------- src/types/auto-imports.d.ts | 229 ++++++++++++- vite.config.ts | 16 +- 11 files changed, 501 insertions(+), 1275 deletions(-) delete mode 100644 README-theme.md delete mode 100644 README-wechat-login.md create mode 100644 src/composables/useTabbar.ts delete mode 100644 src/styles/theme.scss diff --git a/README-theme.md b/README-theme.md deleted file mode 100644 index 4493fbd..0000000 --- a/README-theme.md +++ /dev/null @@ -1,190 +0,0 @@ -# Wot Design Uni 主题系统使用指南 - -## 概述 - -本项目基于 Wot Design Uni 的 CSS 变量系统实现了完整的主题定制功能,支持: - -- 🌙 暗黑模式切换 -- 🎨 12 种预设主题色 -- 🎯 自定义主题色 -- 💾 持久化存储 -- 📱 多平台兼容(H5、小程序) - -## 核心文件 - -### 1. 主题变量文件 - -- `src/styles/wot-theme.scss` - Wot Design Uni CSS 变量定制 -- `src/uni.scss` - 引入主题变量文件 - -### 2. 主题 Composable - -- `src/composables/useTheme.ts` - 主题状态管理和工具函数 - -### 3. 主题设置页面 - -- `src/pages/mine/settings/theme/index.vue` - 主题设置界面 -- `src/pages/test-wot-theme.vue` - 主题测试页面 - -## 使用方法 - -### 1. 在组件中使用主题 - -```vue - - - - - -``` - -### 2. 可用的 CSS 变量 - -#### 主题色系 - -- `--wot-color-theme` - 主题色 -- `--wot-color-success` - 成功色 -- `--wot-color-warning` - 警告色 -- `--wot-color-danger` - 危险色 -- `--wot-color-info` - 信息色 - -#### 文本颜色 - -- `--wot-color-text` - 主要文本色 -- `--wot-color-text-secondary` - 次要文本色 -- `--wot-color-text-placeholder` - 占位符文本色 - -#### 背景颜色 - -- `--wot-color-bg` - 主背景色 -- `--wot-color-bg-light` - 浅背景色 -- `--wot-card-bg-color` - 卡片背景色 - -#### 边框颜色 - -- `--wot-color-border` - 边框色 -- `--wot-color-border-light` - 浅边框色 - -### 3. 预设主题色 - -项目提供 12 种预设主题色: - -```javascript -const colorColumns = [ - { value: "#165DFF", label: "蓝色" }, - { value: "#0FC6C2", label: "青绿色" }, - { value: "#722ED1", label: "紫色" }, - { value: "#F5222D", label: "红色" }, - { value: "#FA8C16", label: "橙色" }, - { value: "#FADB14", label: "黄色" }, - { value: "#52C41A", label: "绿色" }, - { value: "#EB2F96", label: "粉色" }, - { value: "#13C2C2", label: "青色" }, - { value: "#1890FF", label: "天蓝色" }, - { value: "#CD5C5C", label: "经典红" }, - { value: "#228B22", label: "自然绿" }, -]; -``` - -## 暗黑模式适配 - -### 自动适配 - -所有使用 Wot CSS 变量的组件都会自动适配暗黑模式。 - -### 手动适配 - -对于自定义组件,使用 CSS 变量即可: - -```scss -.my-component { - background-color: var(--wot-card-bg-color, #fff); - color: var(--wot-color-text, #333); - border: 1px solid var(--wot-color-border, #e5e6eb); -} -``` - -## 测试页面 - -访问 `/pages/test-wot-theme` 可以测试: - -- 暗黑模式切换 -- 主题色切换 -- CSS 变量效果 -- 各种 Wot 组件的主题适配 - -## 注意事项 - -1. **CSS 变量优先级**:使用 `var(--variable-name, fallback)` 格式提供回退值 -2. **暗黑模式检测**:通过 `[data-theme="dark"]` 选择器自动切换 -3. **持久化存储**:主题设置会自动保存到本地存储 -4. **平台兼容**:H5 和小程序环境都有相应的适配处理 - -## 扩展主题 - -### 添加新的主题色 - -1. 在 `colorColumns` 数组中添加新颜色 -2. 在 `wot-theme.scss` 中添加对应的 CSS 类 - -```scss -.theme-custom { - --wot-color-theme: #your-color; -} -``` - -### 自定义 CSS 变量 - -在 `wot-theme.scss` 中添加新的变量: - -```scss -:root { - --wot-custom-color: #your-color; -} - -[data-theme="dark"] { - --wot-custom-color: #your-dark-color; -} -``` - -## 故障排除 - -1. **主题色不生效**:检查是否正确引入了 `wot-theme.scss` -2. **暗黑模式样式异常**:确认使用了正确的 CSS 变量 -3. **小程序兼容问题**:检查条件编译是否正确 - -## 更新日志 - -- ✅ 修复了暗黑模式切换无效的问题 -- ✅ 完善了 Wot Design Uni CSS 变量系统 -- ✅ 优化了主题设置页面的 UI 组件 -- ✅ 添加了完整的主题测试页面 diff --git a/README-wechat-login.md b/README-wechat-login.md deleted file mode 100644 index 71f1465..0000000 --- a/README-wechat-login.md +++ /dev/null @@ -1,609 +0,0 @@ -# 微信登录实现指南 - -本文档提供基于 wx-java-sdk 的微信授权登录后端实现方案,包括会话管理和接口设计。 - -## 技术方案概述 - -微信登录流程采用基于会话管理的方式,避免每次都从微信服务端获取授权: - -1. 前端通过微信 SDK 获取登录凭证(code)和手机号加密数据 -2. 后端接收凭证和加密数据,与微信服务器交互获取用户信息 -3. 后端创建或更新用户信息,并生成会话令牌(token)返回给前端 -4. 前端存储令牌,后续请求时携带令牌进行身份验证 -5. 令牌过期时,后端自动使用刷新令牌获取新的访问令牌 - -## 后端依赖 - -```xml - - - com.github.binarywang - weixin-java-miniapp - 4.5.0 - - - - - io.jsonwebtoken - jjwt-api - 0.11.5 - - - io.jsonwebtoken - jjwt-impl - 0.11.5 - runtime - - - io.jsonwebtoken - jjwt-jackson - 0.11.5 - runtime - -``` - -## 后端配置 - -```yaml -# application.yml -wx: - miniapp: - appid: ${WX_MINIAPP_APPID} # 微信小程序 appId - secret: ${WX_MINIAPP_SECRET} # 微信小程序 appSecret - token: ${WX_MINIAPP_TOKEN} # 微信小程序消息服务器配置的 token - aesKey: ${WX_MINIAPP_AES_KEY} # 微信小程序消息服务器配置的 EncodingAESKey - msgDataFormat: JSON # 消息格式,XML 或者 JSON - -# JWT 配置 -jwt: - secret: ${JWT_SECRET_KEY} # JWT 密钥 - access-token-expiration: 86400 # 访问令牌过期时间(秒),默认1天 - refresh-token-expiration: 604800 # 刷新令牌过期时间(秒),默认7天 -``` - -## 微信服务配置类 - -```java -@Configuration -@EnableConfigurationProperties(WxMaProperties.class) -public class WxMaConfiguration { - private final WxMaProperties properties; - private static final Map maServices = new HashMap<>(); - - @Autowired - public WxMaConfiguration(WxMaProperties properties) { - this.properties = properties; - } - - @Bean - public WxMaService wxMaService() { - WxMaService service = new WxMaServiceImpl(); - WxMaDefaultConfigImpl config = new WxMaDefaultConfigImpl(); - config.setAppid(properties.getAppid()); - config.setSecret(properties.getSecret()); - config.setToken(properties.getToken()); - config.setAesKey(properties.getAesKey()); - config.setMsgDataFormat(properties.getMsgDataFormat()); - service.setWxMaConfig(config); - maServices.put(properties.getAppid(), service); - return service; - } - - public static WxMaService getMaService(String appid) { - return maServices.get(appid); - } -} -``` - -## 微信用户表设计 - -```sql -CREATE TABLE `wx_user` ( - `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID', - `open_id` varchar(128) NOT NULL COMMENT '微信开放ID', - `union_id` varchar(128) DEFAULT NULL COMMENT '微信开放平台unionid', - `session_key` varchar(128) DEFAULT NULL COMMENT '会话密钥', - `nickname` varchar(64) DEFAULT NULL COMMENT '昵称', - `avatar_url` varchar(256) DEFAULT NULL COMMENT '头像', - `phone` varchar(32) DEFAULT NULL COMMENT '手机号', - `gender` tinyint(1) DEFAULT NULL COMMENT '性别(0:未知 1:男 2:女)', - `country` varchar(64) DEFAULT NULL COMMENT '国家', - `province` varchar(64) DEFAULT NULL COMMENT '省份', - `city` varchar(64) DEFAULT NULL COMMENT '城市', - `language` varchar(64) DEFAULT NULL COMMENT '语言', - `is_new_user` tinyint(1) DEFAULT '1' COMMENT '是否新用户(0:否 1:是)', - `last_login_time` datetime DEFAULT NULL COMMENT '最后登录时间', - `created_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - `updated_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', - PRIMARY KEY (`id`), - UNIQUE KEY `idx_open_id` (`open_id`), - KEY `idx_union_id` (`union_id`), - KEY `idx_phone` (`phone`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='微信用户表'; -``` - -## 控制器层实现 - -```java -@Slf4j -@RestController -@RequestMapping("/api/v1/auth") -@RequiredArgsConstructor -public class AuthController { - - private final WxMaService wxMaService; - private final UserService userService; - private final JwtService jwtService; - - /** - * 微信登录(简单版) - */ - @PostMapping("/wechat/login") - public R wechatLogin(@RequestBody WechatLoginRequest request) { - try { - // 获取微信用户信息 - WxMaJscode2SessionResult sessionResult = wxMaService.jsCode2SessionInfo(request.getCode()); - String openid = sessionResult.getOpenid(); - String sessionKey = sessionResult.getSessionKey(); - - // 查询或创建用户 - User user = userService.getOrCreateWxUser(openid, sessionKey); - - // 生成令牌 - String accessToken = jwtService.generateAccessToken(user.getId()); - String refreshToken = jwtService.generateRefreshToken(user.getId()); - - // 构建返回结果 - LoginResult result = new LoginResult(); - result.setAccessToken(accessToken); - result.setRefreshToken(refreshToken); - result.setTokenType("Bearer"); - result.setExpiresIn(jwtService.getAccessTokenExpiration()); - result.setNewUser(user.getIsNewUser()); - result.setProfileComplete(userService.isUserProfileComplete(user)); - - return R.ok(result); - } catch (WxErrorException e) { - log.error("微信登录失败", e); - return R.fail("微信登录失败: " + e.getMessage()); - } - } - - /** - * 微信小程序手机号登录 - */ - @PostMapping("/wechat/phone-login") - public R wechatPhoneLogin(@RequestBody WechatPhoneLoginRequest request) { - try { - // 获取微信用户信息 - WxMaJscode2SessionResult sessionResult = wxMaService.jsCode2SessionInfo(request.getCode()); - String openid = sessionResult.getOpenid(); - String sessionKey = sessionResult.getSessionKey(); - - // 解密手机号 - String phoneNumber = null; - - // 新版本获取手机号 - if (request.getPhoneCode() != null) { - WxMaPhoneNumberInfo phoneInfo = wxMaService.getPhoneNoInfo(request.getPhoneCode()); - phoneNumber = phoneInfo.getPhoneNumber(); - } - // 旧版本获取手机号 - else if (request.getEncryptedData() != null && request.getIv() != null) { - WxMaPhoneNumberInfo phoneInfo = wxMaService.getUserService() - .getPhoneNoInfo(sessionKey, request.getEncryptedData(), request.getIv()); - phoneNumber = phoneInfo.getPhoneNumber(); - } - - if (phoneNumber == null) { - return R.fail("获取手机号失败"); - } - - // 查询或创建用户并更新手机号 - User user = userService.getOrCreateWxUserWithPhone(openid, sessionKey, phoneNumber); - - // 生成令牌 - String accessToken = jwtService.generateAccessToken(user.getId()); - String refreshToken = jwtService.generateRefreshToken(user.getId()); - - // 构建返回结果 - LoginResult result = new LoginResult(); - result.setAccessToken(accessToken); - result.setRefreshToken(refreshToken); - result.setTokenType("Bearer"); - result.setExpiresIn(jwtService.getAccessTokenExpiration()); - result.setNewUser(user.getIsNewUser()); - result.setProfileComplete(userService.isUserProfileComplete(user)); - - return R.ok(result); - } catch (WxErrorException e) { - log.error("微信手机号登录失败", e); - return R.fail("微信手机号登录失败: " + e.getMessage()); - } - } - - /** - * 检查会话有效性 - */ - @GetMapping("/check-session") - public R checkSession(HttpServletRequest request) { - String token = jwtService.getTokenFromRequest(request); - boolean isValid = jwtService.validateToken(token); - - SessionValidResult result = new SessionValidResult(); - result.setValid(isValid); - - return R.ok(result); - } - - /** - * 刷新令牌 - */ - @PostMapping("/refresh-token") - public R refreshToken(@RequestBody RefreshTokenRequest request) { - try { - String refreshToken = request.getRefreshToken(); - if (!jwtService.validateRefreshToken(refreshToken)) { - return R.fail("刷新令牌无效或已过期"); - } - - Long userId = jwtService.getUserIdFromToken(refreshToken); - String newAccessToken = jwtService.generateAccessToken(userId); - - RefreshTokenResult result = new RefreshTokenResult(); - result.setAccessToken(newAccessToken); - result.setExpiresIn(jwtService.getAccessTokenExpiration()); - - return R.ok(result); - } catch (Exception e) { - log.error("刷新令牌失败", e); - return R.fail("刷新令牌失败"); - } - } - - /** - * 登出 - */ - @PostMapping("/logout") - public R logout(HttpServletRequest request) { - // 此处可以将token加入黑名单 - // 在真实场景中,可以将token存入Redis黑名单,并设置过期时间 - return R.ok(); - } -} -``` - -## JWT服务实现 - -```java -@Service -@RequiredArgsConstructor -public class JwtService { - - @Value("${jwt.secret}") - private String jwtSecret; - - @Value("${jwt.access-token-expiration}") - private long accessTokenExpiration; - - @Value("${jwt.refresh-token-expiration}") - private long refreshTokenExpiration; - - /** - * 生成访问令牌 - */ - public String generateAccessToken(Long userId) { - return generateToken(userId, accessTokenExpiration, "access"); - } - - /** - * 生成刷新令牌 - */ - public String generateRefreshToken(Long userId) { - return generateToken(userId, refreshTokenExpiration, "refresh"); - } - - /** - * 生成令牌 - */ - private String generateToken(Long userId, long expiration, String type) { - Date now = new Date(); - Date expiryDate = new Date(now.getTime() + expiration * 1000); - - return Jwts.builder() - .setSubject(userId.toString()) - .setIssuedAt(now) - .setExpiration(expiryDate) - .claim("type", type) - .signWith(getSigningKey()) - .compact(); - } - - /** - * 从请求中获取令牌 - */ - public String getTokenFromRequest(HttpServletRequest request) { - String bearerToken = request.getHeader("Authorization"); - if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) { - return bearerToken.substring(7); - } - return null; - } - - /** - * 验证令牌 - */ - public boolean validateToken(String token) { - try { - Jwts.parserBuilder() - .setSigningKey(getSigningKey()) - .build() - .parseClaimsJws(token); - return true; - } catch (Exception e) { - return false; - } - } - - /** - * 验证刷新令牌 - */ - public boolean validateRefreshToken(String token) { - try { - Claims claims = Jwts.parserBuilder() - .setSigningKey(getSigningKey()) - .build() - .parseClaimsJws(token) - .getBody(); - - return "refresh".equals(claims.get("type")); - } catch (Exception e) { - return false; - } - } - - /** - * 从令牌中获取用户ID - */ - public Long getUserIdFromToken(String token) { - Claims claims = Jwts.parserBuilder() - .setSigningKey(getSigningKey()) - .build() - .parseClaimsJws(token) - .getBody(); - - return Long.parseLong(claims.getSubject()); - } - - /** - * 获取签名密钥 - */ - private Key getSigningKey() { - byte[] keyBytes = Decoders.BASE64.decode(jwtSecret); - return Keys.hmacShaKeyFor(keyBytes); - } - - /** - * 获取访问令牌过期时间(秒) - */ - public long getAccessTokenExpiration() { - return accessTokenExpiration; - } -} -``` - -## 用户服务实现 - -```java -@Service -@RequiredArgsConstructor -public class UserServiceImpl implements UserService { - - private final WxUserMapper wxUserMapper; - - /** - * 获取或创建微信用户 - */ - @Override - @Transactional(rollbackFor = Exception.class) - public User getOrCreateWxUser(String openid, String sessionKey) { - // 查询用户 - WxUser wxUser = wxUserMapper.selectByOpenId(openid); - - // 如果用户不存在,创建新用户 - if (wxUser == null) { - wxUser = new WxUser(); - wxUser.setOpenId(openid); - wxUser.setSessionKey(sessionKey); - wxUser.setIsNewUser(true); - wxUser.setLastLoginTime(new Date()); - wxUserMapper.insert(wxUser); - } else { - // 更新会话密钥和登录时间 - wxUser.setSessionKey(sessionKey); - wxUser.setLastLoginTime(new Date()); - wxUserMapper.updateById(wxUser); - } - - // 将 WxUser 转换为 User - return convertToUser(wxUser); - } - - /** - * 获取或创建带手机号的微信用户 - */ - @Override - @Transactional(rollbackFor = Exception.class) - public User getOrCreateWxUserWithPhone(String openid, String sessionKey, String phone) { - // 先检查是否有该手机号的用户 - WxUser wxUserByPhone = wxUserMapper.selectByPhone(phone); - - // 如果存在该手机号用户但openid不同,可能是用户换了微信号,更新openid - if (wxUserByPhone != null && !openid.equals(wxUserByPhone.getOpenId())) { - wxUserByPhone.setOpenId(openid); - wxUserByPhone.setSessionKey(sessionKey); - wxUserByPhone.setLastLoginTime(new Date()); - wxUserByPhone.setIsNewUser(false); - wxUserMapper.updateById(wxUserByPhone); - return convertToUser(wxUserByPhone); - } - - // 查询用户 - WxUser wxUser = wxUserMapper.selectByOpenId(openid); - - // 如果用户不存在,创建新用户 - if (wxUser == null) { - wxUser = new WxUser(); - wxUser.setOpenId(openid); - wxUser.setSessionKey(sessionKey); - wxUser.setPhone(phone); - wxUser.setIsNewUser(true); - wxUser.setLastLoginTime(new Date()); - wxUserMapper.insert(wxUser); - } else { - // 更新会话密钥、手机号和登录时间 - wxUser.setSessionKey(sessionKey); - wxUser.setPhone(phone); - wxUser.setLastLoginTime(new Date()); - wxUserMapper.updateById(wxUser); - } - - // 将 WxUser 转换为 User - return convertToUser(wxUser); - } - - /** - * 判断用户信息是否完整 - */ - @Override - public boolean isUserProfileComplete(User user) { - if (user == null) { - return false; - } - - return StringUtils.hasText(user.getNickname()) - && StringUtils.hasText(user.getAvatar()) - && StringUtils.hasText(user.getPhone()); - } - - /** - * 将 WxUser 转换为 User - */ - private User convertToUser(WxUser wxUser) { - if (wxUser == null) { - return null; - } - - User user = new User(); - user.setId(wxUser.getId()); - user.setOpenId(wxUser.getOpenId()); - user.setUnionId(wxUser.getUnionId()); - user.setNickname(wxUser.getNickname()); - user.setAvatar(wxUser.getAvatarUrl()); - user.setPhone(wxUser.getPhone()); - user.setGender(wxUser.getGender()); - user.setIsNewUser(wxUser.getIsNewUser()); - - return user; - } -} -``` - -## 网关过滤器实现 (可选) - -如果使用了Spring Cloud Gateway,可以添加JWT验证过滤器: - -```java -@Component -@RequiredArgsConstructor -public class JwtAuthenticationFilter implements GlobalFilter { - - private final JwtService jwtService; - - @Override - public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { - ServerHttpRequest request = exchange.getRequest(); - - // 白名单路径,不需要token验证 - if (isWhiteListPath(request.getPath().toString())) { - return chain.filter(exchange); - } - - // 获取token - String token = getTokenFromRequest(request); - if (token == null) { - return onError(exchange, "未授权", HttpStatus.UNAUTHORIZED); - } - - // 验证token - if (!jwtService.validateToken(token)) { - return onError(exchange, "token无效或已过期", HttpStatus.UNAUTHORIZED); - } - - // 获取用户ID并设置到请求头 - Long userId = jwtService.getUserIdFromToken(token); - ServerHttpRequest mutatedRequest = request.mutate() - .header("X-User-ID", userId.toString()) - .build(); - - return chain.filter(exchange.mutate().request(mutatedRequest).build()); - } - - private boolean isWhiteListPath(String path) { - List whiteList = Arrays.asList( - "/api/v1/auth/login", - "/api/v1/auth/wechat/login", - "/api/v1/auth/wechat/mini-login", - "/api/v1/auth/wechat/phone-login", - "/api/v1/auth/refresh-token" - ); - - return whiteList.stream().anyMatch(path::startsWith); - } - - private String getTokenFromRequest(ServerHttpRequest request) { - List authHeaders = request.getHeaders().get("Authorization"); - if (authHeaders != null && !authHeaders.isEmpty()) { - String auth = authHeaders.get(0); - if (auth.startsWith("Bearer ")) { - return auth.substring(7); - } - } - return null; - } - - private Mono onError(ServerWebExchange exchange, String message, HttpStatus status) { - ServerHttpResponse response = exchange.getResponse(); - response.setStatusCode(status); - response.getHeaders().setContentType(MediaType.APPLICATION_JSON); - - Map result = new HashMap<>(); - result.put("code", status.value()); - result.put("message", message); - - byte[] bytes = new ObjectMapper().writeValueAsBytes(result); - DataBuffer buffer = response.bufferFactory().wrap(bytes); - return response.writeWith(Mono.just(buffer)); - } -} -``` - -## 安全建议 - -1. 生产环境中使用HTTPS保护API通信 -2. 敏感信息(如SessionKey)不要存储在前端 -3. 为JWT密钥使用足够强度的随机字符串 -4. 实现令牌黑名单机制处理注销和令牌泄露情况 -5. 考虑实现令牌自动续期机制 -6. 定期清理过期的会话记录 -7. 记录关键操作的审计日志 - -## 测试 - -使用Postman或其他API测试工具测试以下接口: - -1. `/api/v1/auth/wechat/login` - 微信简单登录 -2. `/api/v1/auth/wechat/phone-login` - 微信手机号登录 -3. `/api/v1/auth/check-session` - 检查会话有效性 -4. `/api/v1/auth/refresh-token` - 刷新令牌 -5. `/api/v1/auth/logout` - 登出 diff --git a/src/composables/useTabbar.ts b/src/composables/useTabbar.ts new file mode 100644 index 0000000..f85d356 --- /dev/null +++ b/src/composables/useTabbar.ts @@ -0,0 +1,60 @@ +/* + * @Author: weisheng + * @Date: 2024-10-29 22:12:54 + * @LastEditTime: 2025-06-25 13:33:39 + * @LastEditors: weisheng + * @Description: + * @FilePath: /wot-demo/src/composables/useTabbar.ts + * 记得注释 + */ +export interface TabbarItem { + name: string; + value: number | null; + active: boolean; + title: string; + icon: string; +} + +const tabbarItems = ref([ + { name: "home", value: null, active: true, title: "首页", icon: "home" }, + { name: "mine", value: null, active: false, title: "我的", icon: "user" }, +]); + +export function useTabbar() { + const tabbarList = computed(() => tabbarItems.value); + + const activeTabbar = computed(() => { + const item = tabbarItems.value.find((item) => item.active); + return item || tabbarItems.value[0]; + }); + + const getTabbarItemValue = (name: string) => { + const item = tabbarItems.value.find((item) => item.name === name); + return item && item.value ? item.value : null; + }; + + const setTabbarItem = (name: string, value: number) => { + const tabbarItem = tabbarItems.value.find((item) => item.name === name); + if (tabbarItem) { + tabbarItem.value = value; + } + }; + + const setTabbarItemActive = (name: string) => { + tabbarItems.value.forEach((item) => { + if (item.name === name) { + item.active = true; + } else { + item.active = false; + } + }); + }; + + return { + tabbarList, + activeTabbar, + getTabbarItemValue, + setTabbarItem, + setTabbarItemActive, + }; +} diff --git a/src/composables/useTheme.ts b/src/composables/useTheme.ts index 6618cc1..739b42d 100644 --- a/src/composables/useTheme.ts +++ b/src/composables/useTheme.ts @@ -1,162 +1,178 @@ -import { ref, watch, computed } from "vue"; import type { ConfigProviderThemeVars } from "wot-design-uni"; -/* 默认的主题色列表 */ -export const colorColumns = [ - { value: "#165DFF", label: "海洋蓝" }, - { value: "#1677FF", label: "天空蓝" }, - { value: "#0081FF", label: "梦幻蓝" }, - { value: "#4080FF", label: "皇家蓝" }, - { value: "#4D74FF", label: "靛蓝" }, - { value: "#0FC6C2", label: "碧波绿" }, - { value: "#722ED1", label: "魔幻紫" }, - { value: "#F5222D", label: "热情红" }, - { value: "#FA8C16", label: "活力橙" }, - { value: "#FADB14", label: "阳光黄" }, - { value: "#52C41A", label: "生机绿" }, - { value: "#EB2F96", label: "浪漫粉" }, - { value: "#13C2C2", label: "清新青" }, - { value: "#36CFC9", label: "湖水蓝" }, - { value: "#CD5C5C", label: "复古红" }, - { value: "#228B22", label: "森林绿" }, +// 定义主题色选项 +export interface ThemeColorOption { + name: string; + value: string; + primary: string; +} + +// 预定义的主题色选项 +export const themeColorOptions: ThemeColorOption[] = [ + { name: "默认蓝", value: "blue", primary: "#4D7FFF" }, + { name: "活力橙", value: "orange", primary: "#FF7D00" }, + { name: "薄荷绿", value: "green", primary: "#07C160" }, + { name: "樱花粉", value: "pink", primary: "#FF69B4" }, + { name: "紫罗兰", value: "purple", primary: "#8A2BE2" }, + { name: "朱砂红", value: "red", primary: "#FF4757" }, ]; -/* 存储键名 */ -const THEME_STORAGE_KEY = "app_theme_mode"; -const THEME_COLOR_STORAGE_KEY = "app_theme_color"; +export function useTheme() { + // 状态定义 + const theme = ref<"light" | "dark">("light"); + const followSystem = ref(true); // 是否跟随系统主题 + const hasUserSet = ref(false); // 用户是否手动设置过主题 + const currentThemeColor = ref(themeColorOptions[0]); + const showThemeColorSheet = ref(false); -/* 从存储中获取主题模式 */ -const getStoredTheme = (): "light" | "dark" => { - try { - const stored = uni.getStorageSync(THEME_STORAGE_KEY); - return stored === "dark" ? "dark" : "light"; - } catch { - return "light"; - } -}; + const themeVars = reactive({ + darkBackground: "#0f0f0f", + darkBackground2: "#1a1a1a", + darkBackground3: "#242424", + darkBackground4: "#2f2f2f", + darkBackground5: "#3d3d3d", + darkBackground6: "#4a4a4a", + darkBackground7: "#606060", + darkColor: "#ffffff", + darkColor2: "#e0e0e0", + darkColor3: "#a0a0a0", + colorTheme: themeColorOptions[0].primary, + }); -/* 从存储中获取主题色 */ -const getStoredThemeColor = (): string => { - try { - const stored = uni.getStorageSync(THEME_COLOR_STORAGE_KEY); - return stored || colorColumns[0].value; - } catch { - return colorColumns[0].value; - } -}; + // 计算属性 + const isDark = computed(() => theme.value === "dark"); -/* 主题状态 */ -export const theme = ref<"light" | "dark">(getStoredTheme()); -export const currentThemeColor = ref(getStoredThemeColor()); - -/* 主题变量(供 ConfigProvider 使用) */ -export const themeVars = computed(() => ({ - colorTheme: currentThemeColor.value, - // 按钮颜色 - buttonPrimaryBgColor: currentThemeColor.value, - buttonPrimaryColor: "#ffffff", - // 开关颜色 - switchOnBgColor: currentThemeColor.value, - // 其他组件颜色 - cellIconColor: currentThemeColor.value, - tagPrimaryBgColor: currentThemeColor.value, - tagPrimaryColor: "#ffffff", -})); - -/* 应用主题到根元素 */ -const applyThemeToRoot = () => { - // 获取根元素 - const root = document.documentElement; - const body = document.body; - - // #ifdef H5 - // 应用暗黑模式 - if (theme.value === "dark") { - root.setAttribute("data-theme", "dark"); - body.classList.add("wot-theme-dark"); - } else { - root.removeAttribute("data-theme"); - body.classList.remove("wot-theme-dark"); + /* 手动切换主题 */ + function toggleTheme(mode?: "light" | "dark") { + theme.value = mode || (theme.value === "light" ? "dark" : "light"); + hasUserSet.value = true; // 标记用户已手动设置 + followSystem.value = false; // 不再跟随系统 + setNavigationBarColor(); } - // 应用主题色类 - // 移除所有主题色类 - root.className = root.className.replace(/theme-color-\w+/g, "").trim(); - // 添加当前主题色类 - const colorClass = `theme-color-${currentThemeColor.value.replace("#", "")}`; - root.classList.add(colorClass); - // #endif - - // #ifdef MP - // 小程序环境下通过设置页面的 data-theme 属性 - const pages = getCurrentPages(); - if (pages.length > 0) { - const currentPage = pages[pages.length - 1] as any; - if (currentPage) { - currentPage.setData?.({ - "data-theme": theme.value, - themeColor: currentThemeColor.value, - }); + /* 设置是否跟随系统主题 */ + function setFollowSystem(follow: boolean) { + followSystem.value = follow; + if (follow) { + hasUserSet.value = false; + initTheme(); // 重新获取系统主题 } } - // #endif -}; -/* 监听主题模式变化 */ -watch( - theme, - (newTheme) => { - uni.setStorageSync(THEME_STORAGE_KEY, newTheme); - applyThemeToRoot(); - }, - { immediate: true } -); + /* 设置导航栏颜色 */ + function setNavigationBarColor() { + uni.setNavigationBarColor({ + frontColor: theme.value === "light" ? "#000000" : "#ffffff", + backgroundColor: theme.value === "light" ? "#ffffff" : "#000000", + }); + } -/* 监听主题色变化 */ -watch( - currentThemeColor, - (newColor) => { - uni.setStorageSync(THEME_COLOR_STORAGE_KEY, newColor); - applyThemeToRoot(); - }, - { immediate: true } -); + /* 设置主题色 */ + function setCurrentThemeColor(color: ThemeColorOption) { + currentThemeColor.value = color; + themeVars.colorTheme = color.primary; + } -/* 切换主题模式 */ -export const toggleTheme = () => { - theme.value = theme.value === "light" ? "dark" : "light"; -}; + /* 获取系统主题 */ + function getSystemTheme(): "light" | "dark" { + try { + // #ifdef MP-WEIXIN + // 微信小程序使用 getAppBaseInfo + const appBaseInfo = uni.getAppBaseInfo(); + if (appBaseInfo && appBaseInfo.theme) { + return appBaseInfo.theme as "light" | "dark"; + } + // #endif -/* 设置主题色 */ -export const setThemeColor = (color: string) => { - currentThemeColor.value = color; -}; + // #ifndef MP-WEIXIN + // 其他平台使用 getSystemInfoSync + const systemInfo = uni.getSystemInfoSync(); + if (systemInfo && systemInfo.theme) { + return systemInfo.theme as "light" | "dark"; + } + // #endif + } catch (error) { + console.warn("获取系统主题失败:", error); + } + return "light"; // 默认返回 light + } -/* 重置主题 */ -export const resetTheme = () => { - theme.value = "light"; - currentThemeColor.value = colorColumns[0].value; -}; + /* 初始化主题 */ + function initTheme() { + // 如果用户已手动设置且不跟随系统,保持当前主题 + if (hasUserSet.value && !followSystem.value) { + console.log("使用用户设置的主题:", theme.value); + setNavigationBarColor(); + return; + } -/* 初始化主题 */ -export const initTheme = () => { - applyThemeToRoot(); - console.log("主题初始化完成:", { - mode: theme.value, - color: currentThemeColor.value, + // 获取系统主题 + const systemTheme = getSystemTheme(); + + // 如果是首次启动或跟随系统,使用系统主题 + if (!hasUserSet.value || followSystem.value) { + theme.value = systemTheme; + if (!hasUserSet.value) { + followSystem.value = true; + console.log("首次启动,使用系统主题:", theme.value); + } else { + console.log("跟随系统主题:", theme.value); + } + } + + setNavigationBarColor(); + } + + /* 打开主题色选择 */ + function openThemeColorPicker() { + showThemeColorSheet.value = true; + } + + /* 关闭主题色选择 */ + function closeThemeColorPicker() { + showThemeColorSheet.value = false; + } + + /* 选择主题色 */ + function selectThemeColor(option: ThemeColorOption) { + setCurrentThemeColor(option); + closeThemeColorPicker(); + } + + // 检查函数是否存在的工具函数 + const isFunction = (fn: any): boolean => typeof fn === "function"; + + onBeforeMount(() => { + initTheme(); + if (isFunction(uni.onThemeChange)) { + uni.onThemeChange((res) => { + toggleTheme(res.theme); + }); + } + }); + + onUnmounted(() => { + if (isFunction(uni.offThemeChange)) { + uni.offThemeChange((res) => { + toggleTheme(res.theme); + }); + } }); -}; -/* 导出主题相关的工具 */ -export const useTheme = () => { return { - theme, + theme: computed(() => theme.value), + isDark, + followSystem: computed(() => followSystem.value), + hasUserSet: computed(() => hasUserSet.value), + currentThemeColor: computed(() => currentThemeColor.value), + showThemeColorSheet, themeVars, - currentThemeColor, - toggleTheme, - setThemeColor, - resetTheme, + themeColorOptions, initTheme, - colorColumns, + toggleTheme, + setFollowSystem, + openThemeColorPicker, + closeThemeColorPicker, + selectThemeColor, }; -}; +} diff --git a/src/layouts/default.vue b/src/layouts/default.vue index b789731..a9a267a 100644 --- a/src/layouts/default.vue +++ b/src/layouts/default.vue @@ -1,22 +1,5 @@ - + diff --git a/src/layouts/tabbar.vue b/src/layouts/tabbar.vue index 8b0aff2..048fcc7 100644 --- a/src/layouts/tabbar.vue +++ b/src/layouts/tabbar.vue @@ -1,31 +1,6 @@ - - - -