677 lines
15 KiB
Vue
677 lines
15 KiB
Vue
<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() {
|
||
// 优先级:switchTab(tabBar)> 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> |