Files

677 lines
15 KiB
Vue
Raw Permalink 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="theme-page theme-transition" :style="{ background: themeConfig.bgColor }">
<!-- 背景装饰 -->
<view class="page-decoration deco-1" :style="{ background: themeConfig.primaryColor + '10' }"></view>
<view class="page-decoration deco-2" :style="{ background: themeConfig.secondaryColor + '10' }"></view>
<!-- 页面头部 -->
<view class="page-header">
<view class="header-decoration">
<view class="header-glow" :style="{ background: `radial-gradient(circle, ${themeConfig.primaryColor}20 0%, transparent 70%)` }"></view>
</view>
<view class="header-title-row">
<view class="header-icon-wrap" :style="{ background: themeConfig.primaryColor + '15' }">
<text class="header-icon">🎨</text>
</view>
<view class="header-text">
<text class="page-title theme-transition" :style="{ color: themeConfig.textPrimary }">主题皮肤</text>
<text class="page-subtitle theme-transition" :style="{ color: themeConfig.textSecondary }">选择你喜欢的配色方案</text>
</view>
</view>
</view>
<view class="theme-list">
<!-- 遍历主题列表 -->
<view
class="theme-item card theme-transition"
v-for="(theme, key) in validThemeList"
:key="`theme-${key}`"
@click="selectTheme(key)"
:style="getThemeItemStyle(theme, key)"
>
<!-- 主题预览区域 -->
<view class="theme-preview"
:style="{ background: theme.bgColor }">
<view class="preview-shine"></view>
<view class="preview-circle-wrap">
<view class="preview-circle" :style="{ background: `linear-gradient(135deg, ${theme.primaryColor} 0%, ${theme.secondaryColor} 100%)` }">
<view class="preview-circle-inner" :style="{ background: theme.cardBgColor }"></view>
</view>
</view>
<view class="preview-colors">
<view class="preview-color-item primary" :style="{ background: theme.primaryColor }"></view>
<view class="preview-color-item secondary" :style="{ background: theme.secondaryColor }"></view>
<view class="preview-color-item accent" :style="{ background: theme.warningColor }"></view>
<view class="preview-color-item text" :style="{ background: theme.textPrimary }"></view>
</view>
</view>
<!-- 主题信息区 -->
<view class="theme-info">
<view class="theme-name-row">
<text class="theme-name theme-transition" :style="{ color: theme.textPrimary }">
{{ theme.name }}
</text>
<view class="theme-badge" v-if="currentThemeKey === key" :style="{ background: theme.primaryColor }">
<text class="badge-text">当前</text>
</view>
</view>
<text class="theme-desc theme-transition" :style="{ color: theme.textSecondary }">
{{ getThemeDesc(key) }}
</text>
</view>
<!-- 选中标识 -->
<view class="active-indicator theme-transition" v-if="currentThemeKey === key">
<view class="indicator-ring" :style="{ borderColor: theme.primaryColor }"></view>
<view class="indicator-inner" :style="{ background: themeConfig.primaryColor }">
<FaIcon icon="check" :size="12" color="#fff" />
</view>
</view>
</view>
</view>
<!-- 底部提示 -->
<view class="bottom-area">
<view class="tip-card theme-transition" :style="{ background: themeConfig.cardBgColor }">
<view class="tip-icon-wrap" :style="{ background: themeConfig.primaryColor + '15' }">
<text class="tip-icon">💡</text>
</view>
<view class="tip-content">
<text class="tip-title theme-transition" :style="{ color: themeConfig.textPrimary }">提示</text>
<text class="tip-text theme-transition" :style="{ color: themeConfig.textSecondary }">主题变更将同步到所有页面</text>
</view>
</view>
</view>
</view>
</template>
<script>
import {
THEME_PRESETS,
getThemeConfigFromStorage,
getThemeKeyFromStorage,
switchTheme,
DEFAULT_THEME_KEY,
THEME_CHANGE_EVENT
} from '@/utils/theme.js'
import { refreshNavBarTheme, forceApplyTabBarTheme } from '@/utils/themeGlobal'
import FaIcon from '@/components/FaIcon.vue'
export default {
components: { FaIcon },
data() {
return {
currentThemeKey: '',
themeConfig: getThemeConfigFromStorage() || THEME_PRESETS[DEFAULT_THEME_KEY],
THEME_PRESETS,
isSwitching: false
}
},
// 计算属性:过滤有效主题 + 实时获取配置
computed: {
// 过滤掉无效的主题配置,避免循环异常
validThemeList() {
const validThemes = {}
Object.keys(THEME_PRESETS).forEach(key => {
if (THEME_PRESETS[key]?.name && THEME_PRESETS[key]?.primaryColor) {
validThemes[key] = THEME_PRESETS[key]
}
})
return validThemes
},
// 实时读取最新主题配置(优先级:全局 > 本地 > 默认)
currentThemeConfig() {
const appInstance = getApp({ allowDefault: true })
return appInstance?.globalData?.themeConfig
|| getThemeConfigFromStorage()
|| THEME_PRESETS[DEFAULT_THEME_KEY]
}
},
onLoad() {
this.initPage()
},
onShow() {
this.syncThemeState()
refreshNavBarTheme()
},
onUnload() {
uni.$off(THEME_CHANGE_EVENT, this.themeChangeHandler)
},
methods: {
initPage() {
this.syncThemeState()
this.watchThemeChange()
},
syncThemeState() {
const appInstance = getApp({ allowDefault: true })
this.currentThemeKey = appInstance?.globalData?.themeKey
|| getThemeKeyFromStorage()
|| DEFAULT_THEME_KEY
this.themeConfig = appInstance?.globalData?.themeConfig
|| getThemeConfigFromStorage()
|| THEME_PRESETS[DEFAULT_THEME_KEY]
},
watchThemeChange() {
this.themeChangeHandler = ({ key, config }) => {
if (!key || !config) return
if (this.currentThemeKey !== key) {
this.currentThemeKey = key
this.themeConfig = config
}
}
uni.$on(THEME_CHANGE_EVENT, this.themeChangeHandler)
},
/**
* 获取主题项样式(简化逻辑 + 性能优化)
*/
getThemeItemStyle(theme, key) {
const isActive = this.currentThemeKey === key
return {
backgroundColor: theme.cardBgColor,
color: theme.textPrimary,
borderRadius: '28rpx',
boxShadow: `0 6rpx 20rpx ${theme.shadowColor}`,
border: `2rpx solid ${isActive ? theme.primaryColor : theme.borderColor}`,
marginBottom: isActive ? '0' : '20rpx' // 选中项减少下边距,突出显示
}
},
/**
* 获取主题描述(增强用户认知)
*/
getThemeDesc(key) {
const descMap = {
default: '暖阳黄 · 明亮活泼,适合日常',
mint: '清新青 · 自然舒适,清爽怡人',
cherry: '樱花粉 · 梦幻浪漫,少女心满满',
sky: '晴空蓝 · 清爽明亮,干净利落',
dark: '深邃夜 · 护眼深色,夜间更舒适',
cream: '薰衣草 · 温柔治愈,浪漫梦幻',
ocean: '柑橘橙 · 元气满满,活力四射',
matcha: '薄荷青 · 清凉冰爽,沁心舒爽'
}
return descMap[key] || '自定义主题风格'
},
/**
* 选择主题(防重复点击 + 友好交互)
*/
async selectTheme(themeKey) {
// 防重复点击 + 合法性校验
if (this.isSwitching || this.currentThemeKey === themeKey || !this.validThemeList[themeKey]) {
return
}
this.isSwitching = true // 上锁
uni.showLoading({ title: '切换中...', mask: true }) // 遮罩防止误操作
try {
// 调用核心切换方法
const success = await switchTheme(themeKey)
if (success) {
// 延迟返回,等待提示展示(提升体验)
setTimeout(() => {
this.jumpToMyPage()
}, 800)
} else {
uni.showToast({
title: '主题已生效,云端同步失败',
icon: 'none',
duration: 2000
})
// 同步失败也返回页面
setTimeout(() => this.jumpToMyPage(), 2000)
}
} catch (error) {
console.error('切换主题异常:', error)
uni.showToast({
title: '切换失败,请重试',
icon: 'none',
duration: 2000
})
} finally {
uni.hideLoading()
this.isSwitching = false // 解锁
}
},
/**
* 跳转回我的页面(多层兜底,适配所有场景)
*/
jumpToMyPage() {
const pages = getCurrentPages()
// 场景1:从我的页面跳转 → 回退
if (pages.length > 1) {
uni.navigateBack({
delta: 1,
fail: () => this.forceJumpToMyPage(),
success: () => this.postJumpRefresh()
})
} else {
// 场景2:直接打开 → 强制跳转
this.forceJumpToMyPage()
}
},
/**
* 强制跳转我的页面(终极兜底)
*/
forceJumpToMyPage() {
// 优先级:switchTabtabBar> redirectTo > navigateTo
const jumpMethods = [
() => uni.switchTab({
url: '/pages/mine/mine',
success: () => this.postJumpRefresh(),
fail: () => {}
}),
() => uni.redirectTo({
url: '/pages/mine/mine',
success: () => this.postJumpRefresh(),
fail: () => {}
}),
() => uni.navigateTo({
url: '/pages/mine/mine',
success: () => this.postJumpRefresh(),
fail: () => {}
})
]
// 依次尝试跳转,直到成功
const tryJump = (index = 0) => {
if (index >= jumpMethods.length) return
jumpMethods[index]()
setTimeout(() => tryJump(index + 1), 300)
}
tryJump()
},
/**
* 跳转后刷新TabBar样式(关键:确保TabBar颜色立即更新)
*/
postJumpRefresh() {
setTimeout(() => {
const appInstance = getApp({ allowDefault: true })
if (appInstance?.globalData?.themeConfig) {
forceApplyTabBarTheme(appInstance.globalData.themeConfig)
}
}, 100)
setTimeout(() => {
const appInstance = getApp({ allowDefault: true })
if (appInstance?.globalData?.themeConfig) {
forceApplyTabBarTheme(appInstance.globalData.themeConfig)
}
}, 300)
}
}
}
</script>
<style scoped>
/* 页面容器 */
.theme-page {
padding: 20rpx 24rpx;
min-height: 100vh;
box-sizing: border-box;
position: relative;
overflow: hidden;
}
/* 页面装饰 */
.page-decoration {
position: absolute;
border-radius: 50%;
pointer-events: none;
}
.deco-1 {
width: 300rpx;
height: 300rpx;
top: -100rpx;
right: -100rpx;
animation: float1 8s ease-in-out infinite;
}
.deco-2 {
width: 200rpx;
height: 200rpx;
bottom: 100rpx;
left: -60rpx;
animation: float2 6s ease-in-out infinite;
}
@keyframes float1 {
0%, 100% { transform: translate(0, 0); }
50% { transform: translate(20rpx, 20rpx); }
}
@keyframes float2 {
0%, 100% { transform: translate(0, 0); }
50% { transform: translate(-15rpx, -15rpx); }
}
/* 页面头部 */
.page-header {
margin-bottom: 40rpx;
padding: 40rpx 0;
position: relative;
}
.header-decoration {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
}
.header-glow {
position: absolute;
top: -50rpx;
left: -50rpx;
width: 300rpx;
height: 300rpx;
}
.header-title-row {
display: flex;
align-items: center;
gap: 20rpx;
position: relative;
z-index: 1;
}
.header-icon-wrap {
width: 88rpx;
height: 88rpx;
border-radius: 12rpx;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 8rpx 24rpx rgba(0,0,0,0.06);
}
.header-icon {
font-size: 44rpx;
}
.header-text {
display: flex;
flex-direction: column;
gap: 8rpx;
}
.page-title {
font-size: 42rpx;
font-weight: 700;
letter-spacing: 2rpx;
}
.page-subtitle {
font-size: 28rpx;
opacity: 0.75;
}
/* 主题列表 */
.theme-list {
margin-bottom: 40rpx;
}
/* 主题项 */
.theme-item {
display: flex;
align-items: center;
padding: 32rpx;
border-radius: 12rpx;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
position: relative;
overflow: hidden;
}
.theme-item:active {
transform: scale(0.98);
}
.theme-item::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4rpx;
background: linear-gradient(90deg, transparent, var(--primaryColor), transparent);
opacity: 0;
transition: opacity 0.3s ease;
}
.theme-item.active::before {
opacity: 1;
}
/* 主题预览区 */
.theme-preview {
width: 150rpx;
height: 110rpx;
border-radius: 12rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 14rpx;
margin-right: 28rpx;
border: 2rpx solid rgba(0,0,0,0.04);
box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.06);
position: relative;
overflow: hidden;
}
.preview-shine {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 50%;
background: linear-gradient(180deg, rgba(255,255,255,0.3) 0%, transparent 100%);
pointer-events: none;
}
.preview-circle-wrap {
width: 64rpx;
height: 64rpx;
display: flex;
align-items: center;
justify-content: center;
position: relative;
z-index: 1;
}
.preview-circle {
width: 56rpx;
height: 56rpx;
border-radius: 50%;
box-shadow: 0 6rpx 20rpx rgba(0,0,0,0.12);
display: flex;
align-items: center;
justify-content: center;
}
.preview-circle-inner {
width: 24rpx;
height: 24rpx;
border-radius: 50%;
}
.preview-colors {
display: flex;
gap: 8rpx;
position: relative;
z-index: 1;
}
.preview-color-item {
width: 18rpx;
height: 18rpx;
border-radius: 6rpx;
box-shadow: 0 2rpx 6rpx rgba(0,0,0,0.1);
}
.preview-color-item.primary {
border-radius: 8rpx 8rpx 2rpx 8rpx;
}
.preview-color-item.secondary {
border-radius: 8rpx 8rpx 8rpx 2rpx;
}
.preview-color-item.accent {
border-radius: 8rpx 2rpx 8rpx 8rpx;
}
.preview-color-item.text {
border-radius: 2rpx 8rpx 8rpx 8rpx;
}
/* 主题信息区 */
.theme-info {
flex: 1;
}
.theme-name-row {
display: flex;
align-items: center;
gap: 16rpx;
margin-bottom: 8rpx;
}
.theme-name {
font-size: 34rpx;
font-weight: 600;
}
.theme-badge {
padding: 6rpx 16rpx;
border-radius: 16rpx;
}
.badge-text {
font-size: 20rpx;
color: #fff;
font-weight: 500;
}
.theme-desc {
font-size: 26rpx;
opacity: 0.7;
line-height: 1.4;
}
/* 选中标识 */
.active-indicator {
width: 56rpx;
height: 56rpx;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.indicator-ring {
position: absolute;
width: 52rpx;
height: 52rpx;
border-radius: 50%;
border: 3rpx solid;
display: flex;
align-items: center;
justify-content: center;
animation: ringPulse 2s ease-in-out infinite;
}
.indicator-inner {
width: 36rpx;
height: 36rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.2);
}
.check-icon {
color: #fff;
font-size: 20rpx;
font-weight: 700;
}
@keyframes ringPulse {
0%, 100% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(1.15);
opacity: 0.5;
}
}
/* 底部区域 */
.bottom-area {
padding-bottom: 60rpx;
}
.tip-card {
display: flex;
align-items: flex-start;
gap: 20rpx;
padding: 28rpx 32rpx;
border-radius: 12rpx;
box-shadow: 0 6rpx 24rpx rgba(0,0,0,0.06);
}
.tip-icon-wrap {
width: 56rpx;
height: 56rpx;
border-radius: 16rpx;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.tip-icon {
font-size: 28rpx;
}
.tip-content {
display: flex;
flex-direction: column;
gap: 6rpx;
}
.tip-title {
font-size: 28rpx;
font-weight: 600;
}
.tip-text {
font-size: 26rpx;
opacity: 0.75;
line-height: 1.5;
}
/* 全局过渡类 */
.theme-transition {
transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
}
/* 覆盖card基础样式 */
.card {
margin-bottom: 24rpx;
}
</style>