首次提交:初始化项目代码

This commit is contained in:
sunct
2026-07-31 14:42:05 +08:00
commit ecca407485
84 changed files with 24158 additions and 0 deletions
+677
View File
@@ -0,0 +1,677 @@
<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>