首次提交:初始化项目代码
This commit is contained in:
@@ -0,0 +1,575 @@
|
||||
<template>
|
||||
<view class="about-page theme-transition" :style="{ background: themeConfig.bgColor }">
|
||||
<!-- 顶部品牌区 -->
|
||||
<view class="header-card" :style="{ background: headerGradient }">
|
||||
<view class="header-decoration">
|
||||
<view class="decoration-circle c1"></view>
|
||||
<view class="decoration-circle c2"></view>
|
||||
<view class="decoration-circle c3"></view>
|
||||
<view class="decoration-circle c4"></view>
|
||||
</view>
|
||||
<view class="header-content">
|
||||
<view class="logo-wrapper">
|
||||
<view class="logo-box" v-if="!logoLoaded" :style="{ background: 'rgba(255,255,255,0.95)' }">
|
||||
<text class="logo-text-fallback" :style="{ color: themeConfig.primaryColor }">M</text>
|
||||
</view>
|
||||
<image
|
||||
class="app-logo"
|
||||
:src="logoConfig.path"
|
||||
mode="aspectFit"
|
||||
@load="logoLoaded = true"
|
||||
@error="logoLoaded = false"
|
||||
v-show="logoLoaded"
|
||||
/>
|
||||
</view>
|
||||
<text class="app-name">{{ logoConfig.name }}</text>
|
||||
<text class="app-subtitle">{{ logoConfig.subtitle }}</text>
|
||||
<view class="version-tag">
|
||||
<text class="version-text">{{ logoConfig.version }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="content-area">
|
||||
<!-- 联系与支持 -->
|
||||
<view class="info-card theme-transition" :style="{ background: themeConfig.cardBgColor }">
|
||||
<view class="card-header">
|
||||
<view class="card-icon" :style="{ background: themeConfig.primaryColor + '18' }">
|
||||
<FaIcon icon="headphones" :size="16" :color="themeConfig.primaryColor" />
|
||||
</view>
|
||||
<text class="card-title" :style="{ color: themeConfig.textPrimary }">联系与支持</text>
|
||||
</view>
|
||||
<view class="info-list">
|
||||
<view class="info-row">
|
||||
<view class="info-icon-box" :style="{ background: themeConfig.primaryColor + '12' }">
|
||||
<FaIcon icon="mobile" :size="16" :color="themeConfig.primaryColor" />
|
||||
</view>
|
||||
<view class="info-body">
|
||||
<text class="info-label" :style="{ color: themeConfig.textSecondary }">版本号</text>
|
||||
<text class="info-value" :style="{ color: themeConfig.textPrimary }">{{ logoConfig.version }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="info-row divider" @click="copyEmail">
|
||||
<view class="info-icon-box" :style="{ background: (themeConfig.warningColor || '#FFAA33') + '15' }">
|
||||
<FaIcon icon="envelope" :size="16" :color="themeConfig.warningColor || '#FFAA33'" />
|
||||
</view>
|
||||
<view class="info-body">
|
||||
<text class="info-label" :style="{ color: themeConfig.textSecondary }">反馈邮箱</text>
|
||||
<text class="info-value" :style="{ color: themeConfig.textPrimary }">{{ logoConfig.email }}</text>
|
||||
</view>
|
||||
<view class="copy-btn" :style="{ color: themeConfig.primaryColor, borderColor: themeConfig.primaryColor + '40', background: themeConfig.primaryColor + '10' }">
|
||||
<FaIcon icon="copy" :size="12" :color="themeConfig.primaryColor" />
|
||||
<text class="copy-btn-text" :style="{ color: themeConfig.primaryColor }">复制</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="info-row" @click="copyWechat">
|
||||
<view class="info-icon-box" :style="{ background: (themeConfig.successColor || '#22C55E') + '15' }">
|
||||
<FaIcon icon="comment" :size="16" :color="themeConfig.successColor || '#22C55E'" />
|
||||
</view>
|
||||
<view class="info-body">
|
||||
<text class="info-label" :style="{ color: themeConfig.textSecondary }">微信公众号</text>
|
||||
<text class="info-value" :style="{ color: themeConfig.textPrimary }">{{ logoConfig.wechat.name }}</text>
|
||||
</view>
|
||||
<view class="copy-btn" :style="{ color: themeConfig.primaryColor, borderColor: themeConfig.primaryColor + '40', background: themeConfig.primaryColor + '10' }">
|
||||
<FaIcon icon="copy" :size="12" :color="themeConfig.primaryColor" />
|
||||
<text class="copy-btn-text" :style="{ color: themeConfig.primaryColor }">复制</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 法律与协议 -->
|
||||
<view class="menu-card theme-transition" :style="{ background: themeConfig.cardBgColor }">
|
||||
<view class="menu-item" @click="gotoUserAgreement">
|
||||
<view class="menu-icon-box" :style="{ background: themeConfig.primaryColor + '15' }">
|
||||
<FaIcon icon="file-lines" :size="16" :color="themeConfig.primaryColor" />
|
||||
</view>
|
||||
<text class="menu-text" :style="{ color: themeConfig.textPrimary }">用户协议</text>
|
||||
<FaIcon icon="chevron-right" :size="18" :color="themeConfig.textDisabled" />
|
||||
</view>
|
||||
<view class="menu-item" @click="gotoPrivacyPolicy">
|
||||
<view class="menu-icon-box" :style="{ background: themeConfig.secondaryColor + '15' }">
|
||||
<FaIcon icon="shield-halved" :size="16" :color="themeConfig.secondaryColor" />
|
||||
</view>
|
||||
<text class="menu-text" :style="{ color: themeConfig.textPrimary }">隐私政策</text>
|
||||
<FaIcon icon="chevron-right" :size="18" :color="themeConfig.textDisabled" />
|
||||
</view>
|
||||
<view class="menu-item logout-item" @click="handleLogout">
|
||||
<view class="menu-icon-box" :style="{ background: (themeConfig.dangerColor || '#F53F3F') + '15' }">
|
||||
<FaIcon icon="right-from-bracket" :size="16" :color="themeConfig.dangerColor || '#F53F3F'" />
|
||||
</view>
|
||||
<text class="menu-text" :style="{ color: themeConfig.dangerColor || '#F53F3F' }">退出登录</text>
|
||||
<FaIcon icon="chevron-right" :size="18" :color="themeConfig.textDisabled" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 品牌理念 -->
|
||||
<view class="brand-card theme-transition" :style="{ background: themeConfig.cardBgColor }">
|
||||
<view class="brand-inner" :style="{ background: brandBgGradient }">
|
||||
<view class="brand-icon-wrap" :style="{ background: themeConfig.primaryColor + '20' }">
|
||||
<FaIcon icon="sparkles" :size="22" :color="themeConfig.primaryColor" />
|
||||
</view>
|
||||
<text class="brand-slogan" :style="{ color: themeConfig.textPrimary }">{{ logoConfig.slogan }}</text>
|
||||
<text class="brand-tagline" :style="{ color: themeConfig.primaryColor }">用心记录每一刻</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="footer-area">
|
||||
<text class="copyright" :style="{ color: themeConfig.textSecondary }">{{ logoConfig.copyright }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getThemeConfigFromStorage, THEME_PRESETS } from '@/utils/theme'
|
||||
import { refreshNavBarTheme } from '@/utils/themeGlobal'
|
||||
import { LOGO_CONFIG } from '@/utils/constants'
|
||||
import { logout as logoutApi } from '@/api'
|
||||
import { logout as authLogout } from '@/utils/auth'
|
||||
import FaIcon from '@/components/FaIcon.vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
FaIcon
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
themeConfig: getThemeConfigFromStorage() || THEME_PRESETS['default'],
|
||||
logoConfig: LOGO_CONFIG,
|
||||
logoLoaded: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
headerGradient() {
|
||||
const c = this.themeConfig.primaryColor || '#9B8AFB'
|
||||
const c2 = this.themeConfig.secondaryColor || this.lightenColor(c, -15)
|
||||
return `linear-gradient(135deg, ${c} 0%, ${c2} 60%, ${this.lightenColor(c, -25)} 100%)`
|
||||
},
|
||||
brandBgGradient() {
|
||||
const c = this.themeConfig.primaryColor || '#9B8AFB'
|
||||
return `linear-gradient(135deg, ${c}12 0%, ${c}08 100%)`
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
refreshNavBarTheme()
|
||||
this.themeChangeHandler = (themeData) => {
|
||||
let newTheme = null
|
||||
if (themeData?.key && THEME_PRESETS[themeData.key]) {
|
||||
newTheme = THEME_PRESETS[themeData.key]
|
||||
} else if (themeData?.config) {
|
||||
newTheme = themeData.config
|
||||
}
|
||||
if (newTheme) {
|
||||
this.themeConfig = newTheme
|
||||
refreshNavBarTheme()
|
||||
}
|
||||
}
|
||||
uni.$on('themeChanged', this.themeChangeHandler)
|
||||
},
|
||||
onShow() {
|
||||
const latestTheme = getThemeConfigFromStorage() || THEME_PRESETS['default']
|
||||
if (JSON.stringify(latestTheme) !== JSON.stringify(this.themeConfig)) {
|
||||
this.themeConfig = latestTheme
|
||||
}
|
||||
refreshNavBarTheme()
|
||||
},
|
||||
onUnload() {
|
||||
uni.$off('themeChanged', this.themeChangeHandler)
|
||||
},
|
||||
methods: {
|
||||
lightenColor(hex, percent) {
|
||||
// 简单变暗:hex 形如 #RRGGBB,percent 负数表示变暗
|
||||
const num = parseInt(hex.replace('#', ''), 16)
|
||||
const r = Math.max(0, Math.min(255, (num >> 16) + Math.round(255 * percent / 100)))
|
||||
const g = Math.max(0, Math.min(255, ((num >> 8) & 0xff) + Math.round(255 * percent / 100)))
|
||||
const b = Math.max(0, Math.min(255, (num & 0xff) + Math.round(255 * percent / 100)))
|
||||
return '#' + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)
|
||||
},
|
||||
gotoUserAgreement() {
|
||||
uni.navigateTo({ url: '/pages/userAgreement/userAgreement' })
|
||||
},
|
||||
gotoPrivacyPolicy() {
|
||||
uni.navigateTo({ url: '/pages/privacyPolicy/privacyPolicy' })
|
||||
},
|
||||
copyEmail() {
|
||||
uni.vibrateShort && uni.vibrateShort({ type: 'light' })
|
||||
uni.setClipboardData({
|
||||
data: this.logoConfig.email,
|
||||
success: () => {
|
||||
uni.showToast({ title: '邮箱已复制', icon: 'success' })
|
||||
}
|
||||
})
|
||||
},
|
||||
copyWechat() {
|
||||
uni.vibrateShort && uni.vibrateShort({ type: 'light' })
|
||||
uni.setClipboardData({
|
||||
data: this.logoConfig.wechat.account,
|
||||
success: () => {
|
||||
uni.showToast({ title: '公众号已复制', icon: 'success' })
|
||||
}
|
||||
})
|
||||
},
|
||||
async handleLogout() {
|
||||
uni.showModal({
|
||||
title: '确认退出',
|
||||
content: '确定要退出登录吗?',
|
||||
confirmColor: this.themeConfig.dangerColor || '#F53F3F',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
await logoutApi()
|
||||
} catch (e) {
|
||||
console.error('退出登录失败:', e)
|
||||
} finally {
|
||||
authLogout()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '@/uni.scss';
|
||||
|
||||
.about-page {
|
||||
min-height: 100vh;
|
||||
padding: 20rpx 24rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.theme-transition {
|
||||
transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease;
|
||||
}
|
||||
|
||||
.header-card {
|
||||
border-radius: 28rpx;
|
||||
padding: 72rpx 32rpx 60rpx;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
margin-bottom: 24rpx;
|
||||
box-shadow: 0 12rpx 40rpx rgba(155, 138, 251, 0.28);
|
||||
}
|
||||
|
||||
.header-decoration {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.decoration-circle {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
|
||||
.decoration-circle.c1 {
|
||||
width: 220rpx;
|
||||
height: 220rpx;
|
||||
top: -80rpx;
|
||||
right: -60rpx;
|
||||
}
|
||||
.decoration-circle.c2 {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
bottom: -20rpx;
|
||||
left: -30rpx;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
.decoration-circle.c3 {
|
||||
width: 70rpx;
|
||||
height: 70rpx;
|
||||
top: 50%;
|
||||
right: 20%;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
.decoration-circle.c4 {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
top: 20%;
|
||||
left: 15%;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.header-content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.logo-wrapper {
|
||||
margin-bottom: 24rpx;
|
||||
position: relative;
|
||||
width: 128rpx;
|
||||
height: 128rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.logo-box {
|
||||
position: absolute;
|
||||
width: 128rpx;
|
||||
height: 128rpx;
|
||||
border-radius: 32rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 10rpx 32rpx rgba(0, 0, 0, 0.18);
|
||||
}
|
||||
|
||||
.logo-text-fallback {
|
||||
font-size: 60rpx;
|
||||
font-weight: 800;
|
||||
letter-spacing: -2rpx;
|
||||
}
|
||||
|
||||
.app-logo {
|
||||
position: absolute;
|
||||
width: 128rpx;
|
||||
height: 128rpx;
|
||||
border-radius: 32rpx;
|
||||
box-shadow: 0 10rpx 32rpx rgba(0, 0, 0, 0.18);
|
||||
}
|
||||
|
||||
.app-name {
|
||||
font-size: 42rpx;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
margin-bottom: 8rpx;
|
||||
letter-spacing: 1.5rpx;
|
||||
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.18);
|
||||
}
|
||||
|
||||
.app-subtitle {
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.88);
|
||||
letter-spacing: 1rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.version-tag {
|
||||
padding: 6rpx 18rpx;
|
||||
background: rgba(255, 255, 255, 0.22);
|
||||
border-radius: 24rpx;
|
||||
backdrop-filter: blur(8rpx);
|
||||
-webkit-backdrop-filter: blur(8rpx);
|
||||
}
|
||||
|
||||
.version-text {
|
||||
font-size: 22rpx;
|
||||
color: #fff;
|
||||
letter-spacing: 1rpx;
|
||||
}
|
||||
|
||||
.content-area {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.info-card,
|
||||
.menu-card,
|
||||
.brand-card {
|
||||
border-radius: 20rpx;
|
||||
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
|
||||
transition: background-color 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
.info-card { padding: 28rpx; }
|
||||
.menu-card { overflow: hidden; }
|
||||
.brand-card { padding: 12rpx; }
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
border-radius: 14rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.5rpx;
|
||||
}
|
||||
|
||||
.info-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 22rpx 0;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.info-row.divider {
|
||||
border-top: 1rpx solid rgba(0, 0, 0, 0.06);
|
||||
border-bottom: 1rpx solid rgba(0, 0, 0, 0.06);
|
||||
margin: 4rpx -28rpx;
|
||||
padding-left: 28rpx;
|
||||
padding-right: 28rpx;
|
||||
}
|
||||
|
||||
.info-row:active {
|
||||
background: rgba(155, 138, 251, 0.05);
|
||||
}
|
||||
|
||||
.info-icon-box {
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
border-radius: 16rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 18rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.info-body {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4rpx;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-size: 24rpx;
|
||||
opacity: 0.55;
|
||||
letter-spacing: 0.5rpx;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.copy-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4rpx;
|
||||
padding: 8rpx 16rpx;
|
||||
border: 1rpx solid;
|
||||
border-radius: 24rpx;
|
||||
font-size: 22rpx;
|
||||
font-weight: 500;
|
||||
flex-shrink: 0;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.copy-btn:active {
|
||||
transform: scale(0.94);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.copy-btn-text {
|
||||
font-size: 22rpx;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.5rpx;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 28rpx 28rpx;
|
||||
transition: all 0.2s ease;
|
||||
border-bottom: 1rpx solid rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.menu-item:last-child { border-bottom: none; }
|
||||
.menu-item:active { background: rgba(155, 138, 251, 0.06); }
|
||||
|
||||
.menu-icon-box {
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
border-radius: 16rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 18rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.menu-text {
|
||||
flex: 1;
|
||||
font-size: 28rpx;
|
||||
letter-spacing: 0.5rpx;
|
||||
}
|
||||
|
||||
.brand-inner {
|
||||
border-radius: 16rpx;
|
||||
padding: 36rpx 28rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.brand-icon-wrap {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
border-radius: 20rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
animation: floatBrand 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes floatBrand {
|
||||
0%, 100% { transform: translateY(0) scale(1); }
|
||||
50% { transform: translateY(-4rpx) scale(1.04); }
|
||||
}
|
||||
|
||||
.brand-slogan {
|
||||
font-size: 26rpx;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.5rpx;
|
||||
text-align: center;
|
||||
line-height: 1.7;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
.brand-tagline {
|
||||
font-size: 22rpx;
|
||||
font-weight: 600;
|
||||
letter-spacing: 2rpx;
|
||||
padding: 6rpx 18rpx;
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
|
||||
.footer-area {
|
||||
padding: 36rpx 24rpx 60rpx;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.copyright {
|
||||
font-size: 22rpx;
|
||||
opacity: 0.45;
|
||||
letter-spacing: 0.5rpx;
|
||||
}
|
||||
</style>
|
||||
+2661
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+1363
File diff suppressed because it is too large
Load Diff
+1128
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,235 @@
|
||||
<template>
|
||||
<view class="agreement-page" :style="{ background: themeConfig.bgColor }">
|
||||
<!-- 协议内容滚动区 -->
|
||||
<scroll-view class="content-scroll" scroll-y>
|
||||
<view class="agreement-content" :style="{ background: themeConfig.cardBgColor }">
|
||||
<view class="title" :style="{ color: themeConfig.textPrimary }">简记memo隐私政策</view>
|
||||
<view class="effective-date" :style="{ color: themeConfig.textSecondary }">生效日期:2025年04月24日</view>
|
||||
|
||||
<view class="section">
|
||||
<view class="section-title" :style="{
|
||||
color: themeConfig.primaryColor,
|
||||
background: themeConfig.primaryColor + '10',
|
||||
borderLeftColor: themeConfig.primaryColor
|
||||
}">1. 总则</view>
|
||||
<view class="section-text" :style="{ color: themeConfig.textSecondary }">1.1 为保护用户隐私,规范本小程序的个人信息处理行为,本小程序运营者(个人开发者,以下简称“运营者”)依据《中华人民共和国个人信息保护法》《中华人民共和国网络安全法》《微信小程序平台运营规范》等法律法规及规范,制定本隐私政策。</view>
|
||||
<view class="section-text" :style="{ color: themeConfig.textSecondary }">1.2 本隐私政策适用于您使用本小程序过程中涉及的个人信息处理活动,您使用本小程序即视为同意本政策;若您不同意本政策,请勿使用本小程序。</view>
|
||||
<view class="section-text" :style="{ color: themeConfig.textSecondary }">1.3 本小程序由个人独立开发及运营,无所属企业/公司主体,所有信息处理行为均由运营者个人负责。</view>
|
||||
</view>
|
||||
|
||||
<view class="section">
|
||||
<view class="section-title" :style="{
|
||||
color: themeConfig.primaryColor,
|
||||
background: themeConfig.primaryColor + '10',
|
||||
borderLeftColor: themeConfig.primaryColor
|
||||
}">2. 个人信息的收集</view>
|
||||
<view class="section-text" :style="{ color: themeConfig.textSecondary }">2.1 登录相关信息:您使用微信授权登录本小程序时,运营者仅获取微信开放平台提供的唯一标识(OpenID),该信息仅用于识别您的用户身份,不会收集您的微信昵称、头像、手机号等其他微信账号信息。</view>
|
||||
<view class="section-text" :style="{ color: themeConfig.textSecondary }">2.2 业务数据:您在本小程序内录入的备忘录、待办事项、账单内容、编辑记录、时间戳等数据,将存储于运营者的服务器中(而非本地设备),仅用于为您提供数据同步、查看、编辑等核心功能。</view>
|
||||
<view class="section-text" :style="{ color: themeConfig.textSecondary }">2.3 设备与日志信息:为保障小程序正常运行,可能被动获取微信平台提供的基础设备信息(如设备型号、系统版本)、操作日志(如页面访问记录、功能使用记录),该类信息仅用于故障排查、功能优化,且仅与OpenID关联(不关联其他个人身份信息)。</view>
|
||||
<view class="section-text" :style="{ color: themeConfig.textSecondary }">2.4 运营者不会主动收集您的姓名、身份证号、手机号、地理位置等其他个人敏感信息。</view>
|
||||
</view>
|
||||
|
||||
<view class="section">
|
||||
<view class="section-title" :style="{
|
||||
color: themeConfig.primaryColor,
|
||||
background: themeConfig.primaryColor + '10',
|
||||
borderLeftColor: themeConfig.primaryColor
|
||||
}">3. 个人信息的使用</view>
|
||||
<view class="section-text" :style="{ color: themeConfig.textSecondary }">3.1 OpenID仅用于识别您的用户身份,保障您能正常访问、管理自己存储在服务器的业务数据。</view>
|
||||
<view class="section-text" :style="{ color: themeConfig.textSecondary }">3.2 您的业务数据仅用于为您本人提供查看、编辑、删除、同步等核心功能,运营者不会利用该数据进行商业推广、数据分析、第三方共享等其他用途。</view>
|
||||
<view class="section-text" :style="{ color: themeConfig.textSecondary }">3.3 设备与日志信息仅用于定位和解决小程序运行故障、优化产品体验,不会用于识别个人身份或向第三方披露。</view>
|
||||
</view>
|
||||
|
||||
<view class="section">
|
||||
<view class="section-title" :style="{
|
||||
color: themeConfig.primaryColor,
|
||||
background: themeConfig.primaryColor + '10',
|
||||
borderLeftColor: themeConfig.primaryColor
|
||||
}">4. 个人信息的存储与保护</view>
|
||||
<view class="section-text" :style="{ color: themeConfig.textSecondary }">4.1 存储方式:OpenID及您的业务数据存储于运营者的服务器中;设备与日志信息仅在故障排查期间临时存储,排查完成后即时删除。</view>
|
||||
<view class="section-text" :style="{ color: themeConfig.textSecondary }">4.2 存储期限:OpenID与业务数据将持续存储,直至您主动申请删除账号(可通过联系运营者完成);设备与日志信息存储期限不超过7日。</view>
|
||||
<view class="section-text" :style="{ color: themeConfig.textSecondary }">4.3 安全措施:运营者通过服务器访问权限控制、数据加密传输等技术手段保障数据安全,但因网络攻击、服务器故障、微信平台漏洞等非运营方可控因素导致的数据泄露,运营者不承担责任。</view>
|
||||
<view class="section-text" :style="{ color: themeConfig.textSecondary }">4.4 您可通过小程序内的删除功能清理自身业务数据,如需彻底删除账号及所有数据,可联系运营者协助处理。</view>
|
||||
</view>
|
||||
|
||||
<view class="section">
|
||||
<view class="section-title" :style="{
|
||||
color: themeConfig.primaryColor,
|
||||
background: themeConfig.primaryColor + '10',
|
||||
borderLeftColor: themeConfig.primaryColor
|
||||
}">5. 个人信息的共享、转让与公开披露</view>
|
||||
<view class="section-text" :style="{ color: themeConfig.textSecondary }">5.1 运营者不会将您的OpenID、业务数据、设备信息等任何信息共享、转让给第三方,除非获得您的明确授权,或法律法规、司法机关/行政机关的强制性要求。</view>
|
||||
<view class="section-text" :style="{ color: themeConfig.textSecondary }">5.2 未经您同意,运营者不会公开披露您的任何信息;因法律法规要求披露时,将在允许范围内尽可能提前通知您。</view>
|
||||
</view>
|
||||
|
||||
<view class="section">
|
||||
<view class="section-title" :style="{
|
||||
color: themeConfig.primaryColor,
|
||||
background: themeConfig.primaryColor + '10',
|
||||
borderLeftColor: themeConfig.primaryColor
|
||||
}">6. 您的权利</view>
|
||||
<view class="section-text" :style="{ color: themeConfig.textSecondary }">6.1 您有权随时查看、编辑、删除自己存储在本小程序内的业务数据;</view>
|
||||
<view class="section-text" :style="{ color: themeConfig.textSecondary }">6.2 您有权联系运营者申请删除账号及所有关联数据(包括OpenID、业务数据);</view>
|
||||
<view class="section-text" :style="{ color: themeConfig.textSecondary }">6.3 您有权向运营者查询自身个人信息的处理情况,运营者将在合理期限内予以答复;</view>
|
||||
<view class="section-text" :style="{ color: themeConfig.textSecondary }">6.4 若您发现本小程序存在违规处理您信息的行为,有权向运营者反馈或向微信平台投诉。</view>
|
||||
</view>
|
||||
|
||||
<view class="section">
|
||||
<view class="section-title" :style="{
|
||||
color: themeConfig.primaryColor,
|
||||
background: themeConfig.primaryColor + '10',
|
||||
borderLeftColor: themeConfig.primaryColor
|
||||
}">7. 未成年人保护</view>
|
||||
<view class="section-text" :style="{ color: themeConfig.textSecondary }">7.1 本小程序面向所有年龄段用户,若未成年人使用本小程序,应在监护人指导下进行,监护人应承担相应监护责任。</view>
|
||||
<view class="section-text" :style="{ color: themeConfig.textSecondary }">7.2 运营者不会主动收集未成年人的任何额外个人信息,若监护人发现未成年人的信息被不当处理,可联系运营者协助处理。</view>
|
||||
</view>
|
||||
|
||||
<view class="section">
|
||||
<view class="section-title" :style="{
|
||||
color: themeConfig.primaryColor,
|
||||
background: themeConfig.primaryColor + '10',
|
||||
borderLeftColor: themeConfig.primaryColor
|
||||
}">8. 隐私政策的变更</view>
|
||||
<view class="section-text" :style="{ color: themeConfig.textSecondary }">8.1 运营者可根据法律法规调整或业务优化,修改本隐私政策,修改后的政策将在小程序内公示,公示满7日后生效。</view>
|
||||
<view class="section-text" :style="{ color: themeConfig.textSecondary }">8.2 若您不同意修改后的政策,应停止使用本小程序并可申请删除账号数据;继续使用的,视为同意修改后的政策。</view>
|
||||
</view>
|
||||
|
||||
<view class="section">
|
||||
<view class="section-title" :style="{
|
||||
color: themeConfig.primaryColor,
|
||||
background: themeConfig.primaryColor + '10',
|
||||
borderLeftColor: themeConfig.primaryColor
|
||||
}">9. 联系我们</view>
|
||||
<view class="section-text" :style="{ color: themeConfig.textSecondary }">9.1 若您对本隐私政策有任何疑问、意见或投诉,可通过以下方式联系运营者:</view>
|
||||
<view class="section-text" :style="{ color: themeConfig.textSecondary }">联系邮箱:memo@miaoall.cn</view>
|
||||
<view class="section-text" :style="{ color: themeConfig.textSecondary }">微信公众号:孙三苗(sunsanmiao)</view>
|
||||
<view class="section-text" :style="{ color: themeConfig.textSecondary }">9.2 运营者将在15个工作日内回复您的咨询或处理请求。</view>
|
||||
</view>
|
||||
|
||||
<view class="section">
|
||||
<view class="section-title" :style="{
|
||||
color: themeConfig.primaryColor,
|
||||
background: themeConfig.primaryColor + '10',
|
||||
borderLeftColor: themeConfig.primaryColor
|
||||
}">10. 其他</view>
|
||||
<view class="section-text" :style="{ color: themeConfig.textSecondary }">10.1 本隐私政策的解释权归运营者个人所有;</view>
|
||||
<view class="section-text" :style="{ color: themeConfig.textSecondary }">10.2 若本政策与法律法规冲突,以法律法规为准;</view>
|
||||
<view class="section-text" :style="{ color: themeConfig.textSecondary }">10.3 您使用本小程序的行为,同时受微信平台的隐私政策及运营规范约束。</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getThemeConfigFromStorage, THEME_PRESETS } from '@/utils/theme'
|
||||
import { refreshNavBarTheme } from '@/utils/themeGlobal'
|
||||
|
||||
export default {
|
||||
name: "privacyPolicy",
|
||||
data() {
|
||||
return {
|
||||
themeConfig: getThemeConfigFromStorage() || THEME_PRESETS['default']
|
||||
};
|
||||
},
|
||||
onLoad() {
|
||||
refreshNavBarTheme()
|
||||
this.themeChangeHandler = (themeData) => {
|
||||
let newTheme = null
|
||||
if (themeData?.key && THEME_PRESETS[themeData.key]) {
|
||||
newTheme = THEME_PRESETS[themeData.key]
|
||||
} else if (themeData?.config) {
|
||||
newTheme = themeData.config
|
||||
}
|
||||
if (newTheme) {
|
||||
this.themeConfig = newTheme
|
||||
refreshNavBarTheme()
|
||||
}
|
||||
}
|
||||
uni.$on('themeChanged', this.themeChangeHandler)
|
||||
},
|
||||
onShow() {
|
||||
const latestTheme = getThemeConfigFromStorage() || THEME_PRESETS['default']
|
||||
if (JSON.stringify(latestTheme) !== JSON.stringify(this.themeConfig)) {
|
||||
this.themeConfig = latestTheme
|
||||
}
|
||||
refreshNavBarTheme()
|
||||
},
|
||||
onUnload() {
|
||||
uni.$off('themeChanged', this.themeChangeHandler)
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.agreement-page {
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
padding: 20rpx 24rpx;
|
||||
padding-bottom: 40px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.content-scroll {
|
||||
height: calc(100vh - 40rpx);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.agreement-content {
|
||||
padding: 40rpx;
|
||||
border-radius: 12rpx;
|
||||
min-height: calc(100% - 48rpx);
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
margin-bottom: 16rpx;
|
||||
letter-spacing: 1rpx;
|
||||
}
|
||||
|
||||
.effective-date {
|
||||
text-align: center;
|
||||
font-size: 24rpx;
|
||||
margin-bottom: 40rpx;
|
||||
padding-bottom: 24rpx;
|
||||
border-bottom: 1px dashed rgba(132, 179, 217, 0.2);
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-bottom: 36rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
margin-bottom: 18rpx;
|
||||
padding: 12rpx 16rpx;
|
||||
border-radius: 12rpx;
|
||||
border-left: 4rpx solid;
|
||||
}
|
||||
|
||||
.section-text {
|
||||
font-size: 28rpx;
|
||||
line-height: 2;
|
||||
margin-bottom: 14rpx;
|
||||
text-indent: 56rpx;
|
||||
}
|
||||
|
||||
.footer-info {
|
||||
margin-top: 48rpx;
|
||||
padding-top: 24rpx;
|
||||
border-top: 1px dashed rgba(132, 179, 217, 0.2);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.footer-text {
|
||||
font-size: 24rpx;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.contact-link {
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,626 @@
|
||||
<template>
|
||||
<view class="review-page" :style="{ background: themeConfig.bgColor }">
|
||||
<PageHeader
|
||||
title="今日复盘"
|
||||
:subtitle="formatDisplayDate(currentDate)"
|
||||
:show-right="true"
|
||||
:show-back="false"
|
||||
>
|
||||
<template #right>
|
||||
<view class="header-actions">
|
||||
<view class="action-btn" :style="{ background: themeConfig.cardBgColor }" @click="toggleCalendar">
|
||||
<FaIcon icon="calendar-days" :size="20" :color="themeConfig.textPrimary" />
|
||||
</view>
|
||||
<view class="action-btn" :style="{ background: themeConfig.cardBgColor }" @click="goHistory">
|
||||
<FaIcon icon="list" :size="20" :color="themeConfig.textPrimary" />
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</PageHeader>
|
||||
|
||||
<view class="calendar-wrap" v-if="showCalendar">
|
||||
<InlineCalendar
|
||||
:value="currentDate"
|
||||
@change="handleDateChange"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<scroll-view class="main-content" scroll-y :show-scrollbar="false">
|
||||
<view class="content-wrap">
|
||||
<view class="loading-state" v-if="loading">
|
||||
<view class="loading-spinner" :style="{ borderTopColor: themeConfig.primaryColor }"></view>
|
||||
<text class="loading-text" :style="{ color: themeConfig.textSecondary }">加载中...</text>
|
||||
</view>
|
||||
|
||||
<template v-else>
|
||||
<AppCard
|
||||
:padding="26"
|
||||
:radius="22"
|
||||
:marginBottom="20"
|
||||
:showShadow="true"
|
||||
>
|
||||
<view class="method-intro">
|
||||
<view class="intro-icon" :style="{ background: themeConfig.bgColor }">
|
||||
<FaIcon :icon="currentIntro.iconName" :size="22" :color="themeConfig.primaryColor" />
|
||||
</view>
|
||||
<view class="intro-texts">
|
||||
<text class="intro-title" :style="{ color: themeConfig.textPrimary }">{{ currentIntro.title }}</text>
|
||||
<text class="intro-desc" :style="{ color: themeConfig.textSecondary }">{{ currentIntro.desc }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</AppCard>
|
||||
|
||||
<view class="review-content">
|
||||
<AppCard
|
||||
v-for="(field, idx) in currentFields"
|
||||
:key="field.key"
|
||||
:padding="26"
|
||||
:radius="20"
|
||||
:marginBottom="idx < currentFields.length - 1 ? 20 : 0"
|
||||
:showShadow="true"
|
||||
>
|
||||
<view class="input-card">
|
||||
<view class="card-header">
|
||||
<view class="card-icon-wrap" :style="{ background: getFieldBg(field.colorKey) }">
|
||||
<FaIcon :icon="field.iconName" :size="22" :color="getProgressColor(field.colorKey)" />
|
||||
</view>
|
||||
<view class="card-title-wrap">
|
||||
<text class="card-title" :style="{ color: themeConfig.textPrimary }">{{ field.title }}</text>
|
||||
<text class="card-subtitle" :style="{ color: themeConfig.textSecondary }">{{ field.subtitle }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="card-body">
|
||||
<textarea
|
||||
class="card-input"
|
||||
v-model="currentData[field.key]"
|
||||
:placeholder="field.placeholder"
|
||||
:style="{ color: themeConfig.textPrimary }"
|
||||
auto-height
|
||||
:maxlength="500"
|
||||
show-confirm-bar="{{ false }}"
|
||||
></textarea>
|
||||
</view>
|
||||
<view class="card-footer">
|
||||
<view class="progress-wrap">
|
||||
<ProgressBar
|
||||
:percent="getProgressPercent(currentData[field.key])"
|
||||
:height="8"
|
||||
:color="getProgressColor(field.colorKey)"
|
||||
:show-text="false"
|
||||
/>
|
||||
</view>
|
||||
<text class="word-count" :style="{ color: themeConfig.textSecondary }">
|
||||
{{ currentData[field.key].length }}/500
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</AppCard>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<view class="bottom-spacer"></view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<view class="footer-section">
|
||||
<view
|
||||
class="submit-button"
|
||||
:class="{ disabled: !canSave }"
|
||||
@click="handleSave"
|
||||
:style="{
|
||||
background: themeConfig.gradientVibrant,
|
||||
boxShadow: `0 12rpx 36rpx ${themeConfig.shadowColor}`
|
||||
}"
|
||||
>
|
||||
<view class="btn-content">
|
||||
<text class="btn-text">{{ isEdit ? '更新复盘' : '保存复盘' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<GrowthModal
|
||||
:visible="growthModalVisible"
|
||||
:title="growthModalTitle"
|
||||
:message="growthModalMessage"
|
||||
:icon="growthModalIcon"
|
||||
:confirm-text="growthModalConfirmText"
|
||||
@close="onGrowthModalClose"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { saveReview, getReviewByDate } from '@/api'
|
||||
import growthModalMixin from '@/mixins/growthModalMixin.js'
|
||||
import themeMixin from '@/mixins/themeMixin.js'
|
||||
import PageHeader from '@/components/PageHeader.vue'
|
||||
import InlineCalendar from '@/components/InlineCalendar.vue'
|
||||
import ProgressBar from '@/components/ProgressBar.vue'
|
||||
import AppCard from '@/components/AppCard.vue'
|
||||
import EmptyState from '@/components/EmptyState.vue'
|
||||
import GrowthModal from '@/components/GrowthModal.vue'
|
||||
import FaIcon from '@/components/FaIcon.vue'
|
||||
import { isLogin } from '@/utils/auth'
|
||||
import { refreshNavBarTheme } from '@/utils/themeGlobal'
|
||||
|
||||
const REVIEW_STORAGE_PREFIX = 'review_'
|
||||
|
||||
export default {
|
||||
name: 'ReviewPage',
|
||||
components: {
|
||||
PageHeader,
|
||||
InlineCalendar,
|
||||
ProgressBar,
|
||||
AppCard,
|
||||
EmptyState,
|
||||
GrowthModal,
|
||||
FaIcon
|
||||
},
|
||||
mixins: [growthModalMixin, themeMixin],
|
||||
data() {
|
||||
return {
|
||||
currentDate: '',
|
||||
kptData: {
|
||||
keep: '',
|
||||
problem: '',
|
||||
try: ''
|
||||
},
|
||||
showCalendar: false,
|
||||
loading: false,
|
||||
isEdit: false,
|
||||
kptFields: [
|
||||
{ key: 'keep', title: 'Keep(保持)', subtitle: '做得好的地方', iconName: 'lightbulb', colorKey: 'success', placeholder: '今天哪些做得好的地方值得保持?' },
|
||||
{ key: 'problem', title: 'Problem(问题)', subtitle: '遇到的挑战', iconName: 'triangle-exclamation', colorKey: 'danger', placeholder: '今天遇到了什么问题和挑战?' },
|
||||
{ key: 'try', title: 'Try(尝试)', subtitle: '改进方向', iconName: 'rocket', colorKey: 'secondary', placeholder: '明天尝试什么改进方法?' }
|
||||
]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
currentFields() {
|
||||
return this.kptFields
|
||||
},
|
||||
currentData() {
|
||||
return this.kptData
|
||||
},
|
||||
canSave() {
|
||||
return Object.values(this.kptData).some(v => v && v.trim())
|
||||
},
|
||||
storageKey() {
|
||||
return `${REVIEW_STORAGE_PREFIX}${this.currentDate}`
|
||||
},
|
||||
currentIntro() {
|
||||
return {
|
||||
iconName: 'lightbulb',
|
||||
title: 'KPT 复盘法',
|
||||
desc: '通过记录做得好的、遇到的问题、改进方向,快速复盘成长'
|
||||
}
|
||||
}
|
||||
},
|
||||
onLoad(options) {
|
||||
if (!isLogin()) {
|
||||
uni.redirectTo({ url: '/pages/login/login' })
|
||||
return
|
||||
}
|
||||
const now = new Date()
|
||||
this.currentDate = options.date || this.formatDate(now)
|
||||
refreshNavBarTheme()
|
||||
this.loadReview()
|
||||
},
|
||||
onShow() {
|
||||
refreshNavBarTheme()
|
||||
},
|
||||
onUnload() {
|
||||
this.autoSaveDraft()
|
||||
},
|
||||
methods: {
|
||||
formatDate(date) {
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
return `${year}-${month}-${day}`
|
||||
},
|
||||
formatDisplayDate(dateStr) {
|
||||
if (!dateStr) return ''
|
||||
const date = new Date(dateStr)
|
||||
const today = new Date()
|
||||
const yesterday = new Date(today)
|
||||
yesterday.setDate(yesterday.getDate() - 1)
|
||||
const tomorrow = new Date(today)
|
||||
tomorrow.setDate(tomorrow.getDate() + 1)
|
||||
|
||||
if (date.toDateString() === today.toDateString()) {
|
||||
return '今天'
|
||||
} else if (date.toDateString() === yesterday.toDateString()) {
|
||||
return '昨天'
|
||||
} else if (date.toDateString() === tomorrow.toDateString()) {
|
||||
return '明天'
|
||||
} else {
|
||||
const weekDays = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六']
|
||||
const month = date.getMonth() + 1
|
||||
const day = date.getDate()
|
||||
const weekDay = weekDays[date.getDay()]
|
||||
return `${month}月${day}日 ${weekDay}`
|
||||
}
|
||||
},
|
||||
getProgressPercent(text) {
|
||||
if (!text) return 0
|
||||
return Math.min(100, (text.length / 500) * 100)
|
||||
},
|
||||
getProgressColor(colorKey) {
|
||||
const colorMap = {
|
||||
success: this.themeConfig.successColor,
|
||||
danger: this.themeConfig.dangerColor,
|
||||
secondary: this.themeConfig.secondaryColor,
|
||||
primary: this.themeConfig.primaryColor,
|
||||
warning: this.themeConfig.warningColor,
|
||||
accent: this.themeConfig.accentColor
|
||||
}
|
||||
return colorMap[colorKey] || this.themeConfig.primaryColor
|
||||
},
|
||||
getFieldBg(colorKey) {
|
||||
const color = this.getProgressColor(colorKey)
|
||||
return color + '18'
|
||||
},
|
||||
toggleCalendar() {
|
||||
this.showCalendar = !this.showCalendar
|
||||
},
|
||||
handleDateChange(dateStr) {
|
||||
this.currentDate = dateStr
|
||||
this.showCalendar = false
|
||||
this.loadReview()
|
||||
},
|
||||
goHistory() {
|
||||
uni.navigateTo({ url: '/pages/review/reviewList' })
|
||||
},
|
||||
async loadReview() {
|
||||
this.loading = true
|
||||
|
||||
try {
|
||||
const result = await getReviewByDate({ date: this.currentDate })
|
||||
|
||||
if (result) {
|
||||
this.isEdit = true
|
||||
// Support both nested {kpt: {keep, problem, try}} and flat {keep, problem, try}
|
||||
const kptData = result.kpt || result
|
||||
this.kptData = {
|
||||
keep: kptData.keep || '',
|
||||
problem: kptData.problem || '',
|
||||
try: kptData.try || ''
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('API加载复盘失败,尝试本地存储:', e)
|
||||
this.loadReviewFromLocal()
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
loadReviewFromLocal() {
|
||||
try {
|
||||
const storedData = uni.getStorageSync(this.storageKey)
|
||||
if (storedData) {
|
||||
const data = JSON.parse(storedData)
|
||||
const kptData = data.kpt || data
|
||||
this.kptData = {
|
||||
keep: kptData.keep || '',
|
||||
problem: kptData.problem || '',
|
||||
try: kptData.try || ''
|
||||
}
|
||||
this.isEdit = !data.isDraft
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('本地存储加载失败:', e)
|
||||
}
|
||||
},
|
||||
autoSaveDraft() {
|
||||
try {
|
||||
const dataToSave = {
|
||||
date: this.currentDate,
|
||||
method: 'kpt',
|
||||
kpt: this.kptData,
|
||||
isDraft: true,
|
||||
saveTime: new Date().toISOString()
|
||||
}
|
||||
uni.setStorageSync(this.storageKey, JSON.stringify(dataToSave))
|
||||
} catch (e) {
|
||||
console.error('保存草稿失败:', e)
|
||||
}
|
||||
},
|
||||
async handleSave() {
|
||||
if (!this.canSave) {
|
||||
uni.showToast({ title: '请填写至少一项内容', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
uni.showLoading({ title: '保存中...', mask: true })
|
||||
|
||||
try {
|
||||
const dataToSave = {
|
||||
date: this.currentDate,
|
||||
kpt: this.kptData
|
||||
}
|
||||
|
||||
const result = await saveReview(dataToSave)
|
||||
|
||||
uni.hideLoading()
|
||||
uni.showToast({ title: '保存成功', icon: 'success' })
|
||||
|
||||
this.isEdit = true
|
||||
|
||||
if (result && (result.levelUp || (result.newAchievements && result.newAchievements.length > 0))) {
|
||||
this.showGrowthModalWithCallback(result, () => {
|
||||
setTimeout(() => uni.navigateBack(), 1500)
|
||||
})
|
||||
} else {
|
||||
this.showGrowthModal({
|
||||
type: 'review',
|
||||
exp: result && result.expGained ? result.expGained : 10,
|
||||
level: result && result.currentLevel ? result.currentLevel : null
|
||||
})
|
||||
setTimeout(() => {
|
||||
uni.navigateBack()
|
||||
}, 1500)
|
||||
}
|
||||
} catch (e) {
|
||||
uni.hideLoading()
|
||||
console.error('保存复盘失败:', e)
|
||||
|
||||
try {
|
||||
const dataToSave = {
|
||||
date: this.currentDate,
|
||||
method: 'kpt',
|
||||
kpt: this.kptData,
|
||||
isDraft: false,
|
||||
saveTime: new Date().toISOString()
|
||||
}
|
||||
uni.setStorageSync(this.storageKey, JSON.stringify(dataToSave))
|
||||
this.isEdit = true
|
||||
uni.showToast({ title: '已本地保存', icon: 'success' })
|
||||
setTimeout(() => uni.navigateBack(), 1500)
|
||||
} catch (fallbackErr) {
|
||||
console.error('本地保存也失败:', fallbackErr)
|
||||
uni.showToast({ title: '保存失败,请重试', icon: 'none' })
|
||||
}
|
||||
}
|
||||
},
|
||||
onGrowthModalClose() {
|
||||
this.growthModalVisible = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.review-page {
|
||||
min-height: 100vh;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 14rpx;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
border-radius: 18rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.25s ease;
|
||||
}
|
||||
|
||||
.action-btn:active {
|
||||
transform: scale(0.93);
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
font-size: 32rpx;
|
||||
}
|
||||
|
||||
.calendar-wrap {
|
||||
margin: 0 36rpx 16rpx;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
height: calc(100vh - 200rpx);
|
||||
position: relative;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.content-wrap {
|
||||
padding: 0 32rpx;
|
||||
}
|
||||
|
||||
.loading-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 120rpx 0;
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
border: 4rpx solid #f0f0f0;
|
||||
border-top-color: #333;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.method-intro {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 18rpx;
|
||||
}
|
||||
|
||||
.intro-icon {
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
border-radius: 14rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.intro-texts {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4rpx;
|
||||
}
|
||||
|
||||
.intro-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.intro-desc {
|
||||
font-size: 24rpx;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.review-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.input-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 18rpx;
|
||||
}
|
||||
|
||||
.card-icon-wrap {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
border-radius: 14rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
font-size: 30rpx;
|
||||
}
|
||||
|
||||
.card-title-wrap {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4rpx;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.card-subtitle {
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
margin-bottom: 4rpx;
|
||||
}
|
||||
|
||||
.card-input {
|
||||
width: 100%;
|
||||
min-height: 160rpx;
|
||||
font-size: 28rpx;
|
||||
line-height: 1.8;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
padding-top: 12rpx;
|
||||
border-top: 1rpx solid rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.progress-wrap {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.word-count {
|
||||
font-size: 22rpx;
|
||||
font-weight: 500;
|
||||
min-width: 100rpx;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.footer-section {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 26rpx 36rpx;
|
||||
padding-bottom: calc(26rpx + constant(safe-area-inset-bottom));
|
||||
padding-bottom: calc(26rpx + env(safe-area-inset-bottom));
|
||||
background: linear-gradient(to top, rgba(248, 250, 252, 0.99) 0%, rgba(248, 250, 252, 0.9) 55%, transparent 100%);
|
||||
backdrop-filter: blur(14rpx);
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.submit-button {
|
||||
width: 100%;
|
||||
height: 108rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 18rpx;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.submit-button.disabled {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.btn-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14rpx;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.btn-text {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-emoji {
|
||||
margin-left: 8rpx;
|
||||
}
|
||||
|
||||
.bottom-spacer {
|
||||
height: 60rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,602 @@
|
||||
<template>
|
||||
<view class="review-list-page" :style="{ backgroundColor: themeConfig.bgColor }">
|
||||
<PageHeader
|
||||
title="复盘历史"
|
||||
subtitle="回顾与成长"
|
||||
:showBack="false"
|
||||
:showRight="true"
|
||||
>
|
||||
<template #right>
|
||||
<view class="header-actions">
|
||||
<view
|
||||
class="action-btn"
|
||||
:class="{ 'action-btn--active': searchVisible }"
|
||||
:style="{ backgroundColor: themeConfig.cardBgColor }"
|
||||
@click="toggleSearch"
|
||||
>
|
||||
<FaIcon :icon="searchVisible ? 'times' : 'search'" :size="18" :color="searchVisible ? themeConfig.primaryColor : themeConfig.textPrimary" />
|
||||
</view>
|
||||
<view
|
||||
class="action-btn"
|
||||
:class="{ 'action-btn--active': calendarVisible }"
|
||||
:style="{ backgroundColor: themeConfig.cardBgColor }"
|
||||
@click="toggleCalendar"
|
||||
>
|
||||
<FaIcon :icon="calendarVisible ? 'times' : 'calendar'" :size="18" :color="calendarVisible ? themeConfig.primaryColor : themeConfig.textPrimary" />
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</PageHeader>
|
||||
|
||||
<!-- 筛选区域 -->
|
||||
<view class="filter-section" v-if="searchVisible || calendarVisible" :style="{ backgroundColor: themeConfig.bgColor }">
|
||||
<!-- 搜索栏 -->
|
||||
<view v-if="searchVisible" class="search-bar" :style="{ backgroundColor: themeConfig.cardBgColor }">
|
||||
<FaIcon icon="search" :size="16" :color="themeConfig.textSecondary" />
|
||||
<input
|
||||
class="search-input"
|
||||
:style="{ color: themeConfig.textPrimary }"
|
||||
v-model="searchKeyword"
|
||||
placeholder="搜索复盘内容..."
|
||||
:placeholder-style="{ color: themeConfig.textDisabled }"
|
||||
confirm-type="search"
|
||||
@confirm="onSearch"
|
||||
/>
|
||||
<view v-if="searchKeyword" class="clear-btn" @click="clearSearch">
|
||||
<FaIcon icon="times-circle" :size="16" :color="themeConfig.textSecondary" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 日历范围选择 -->
|
||||
<view v-if="calendarVisible" class="calendar-section">
|
||||
<InlineCalendar
|
||||
rangeSelect
|
||||
:startDate="dateRange.start"
|
||||
:endDate="dateRange.end"
|
||||
@rangeChange="onDateRangeChange"
|
||||
/>
|
||||
<view v-if="dateRange.start" class="date-range-info">
|
||||
<text class="range-text" :style="{ color: themeConfig.textSecondary }">
|
||||
{{ dateRange.start }} ~ {{ dateRange.end || '至今' }}
|
||||
</text>
|
||||
<view class="clear-range-btn" @click="clearDateRange">
|
||||
<text class="clear-range-text" :style="{ color: themeConfig.primaryColor }">清除</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 列表 -->
|
||||
<scroll-view
|
||||
class="list-scroll"
|
||||
scroll-y
|
||||
refresher-enabled
|
||||
:refresher-triggered="refresherTriggered"
|
||||
@refresherrefresh="onRefresh"
|
||||
:show-scrollbar="false"
|
||||
@scrolltolower="loadMore"
|
||||
>
|
||||
<view class="list-content" v-if="reviewList.length > 0">
|
||||
<SwipeableItem
|
||||
v-for="item in reviewList"
|
||||
:key="item.id"
|
||||
:actions="swipeActions"
|
||||
:index="item.id"
|
||||
@action="(key) => onSwipeAction(key, item)"
|
||||
@open="onSwipeOpen"
|
||||
>
|
||||
<AppCard
|
||||
:padding="24"
|
||||
:radius="20"
|
||||
:clickable="true"
|
||||
:marginBottom="16"
|
||||
@click="goToDetail(item)"
|
||||
>
|
||||
<view class="review-item">
|
||||
<!-- 头部:日期 + 时间 -->
|
||||
<view class="item-header">
|
||||
<view class="date-wrap">
|
||||
<text class="item-date" :style="{ color: themeConfig.textPrimary }">{{ formatDate(item.date) }}</text>
|
||||
<text class="item-weekday" :style="{ color: themeConfig.textSecondary }">{{ getWeekday(item.date) }}</text>
|
||||
</view>
|
||||
<text class="item-time" :style="{ color: themeConfig.textDisabled }">{{ formatTime(item.created_at || item.date) }}</text>
|
||||
</view>
|
||||
|
||||
<!-- KPT 三字段 -->
|
||||
<view class="kpt-fields">
|
||||
<view
|
||||
class="kpt-field"
|
||||
:style="{ backgroundColor: themeConfig.successColor + '10' }"
|
||||
v-if="item.kpt && item.kpt.keep"
|
||||
>
|
||||
<view class="kpt-label" :style="{ backgroundColor: themeConfig.successColor, color: '#fff' }">
|
||||
<text>Keep</text>
|
||||
</view>
|
||||
<text class="kpt-text" :style="{ color: themeConfig.textSecondary }">{{ item.kpt.keep }}</text>
|
||||
</view>
|
||||
|
||||
<view
|
||||
class="kpt-field"
|
||||
:style="{ backgroundColor: themeConfig.warningColor + '10' }"
|
||||
v-if="item.kpt && item.kpt.problem"
|
||||
>
|
||||
<view class="kpt-label" :style="{ backgroundColor: themeConfig.warningColor, color: '#fff' }">
|
||||
<text>Problem</text>
|
||||
</view>
|
||||
<text class="kpt-text" :style="{ color: themeConfig.textSecondary }">{{ item.kpt.problem }}</text>
|
||||
</view>
|
||||
|
||||
<view
|
||||
class="kpt-field"
|
||||
:style="{ backgroundColor: (themeConfig.infoColor || themeConfig.primaryColor) + '10' }"
|
||||
v-if="item.kpt && item.kpt.try"
|
||||
>
|
||||
<view class="kpt-label" :style="{ backgroundColor: (themeConfig.infoColor || themeConfig.primaryColor), color: '#fff' }">
|
||||
<text>Try</text>
|
||||
</view>
|
||||
<text class="kpt-text" :style="{ color: themeConfig.textSecondary }">{{ item.kpt.try }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</AppCard>
|
||||
</SwipeableItem>
|
||||
|
||||
<!-- 底部状态 -->
|
||||
<view class="list-footer" v-if="loading && hasMore">
|
||||
<view class="loading-dot" :style="{ backgroundColor: themeConfig.primaryColor }"></view>
|
||||
<text class="footer-text" :style="{ color: themeConfig.textSecondary }">加载中...</text>
|
||||
</view>
|
||||
<view class="list-footer" v-else-if="!hasMore">
|
||||
<text class="footer-text" :style="{ color: themeConfig.textDisabled }">— 没有更多了 —</text>
|
||||
</view>
|
||||
<view class="bottom-spacer"></view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view class="empty-wrap" v-else-if="!loading">
|
||||
<EmptyState
|
||||
type="review"
|
||||
title="还没有复盘记录"
|
||||
desc="坚持每日复盘,见证成长轨迹"
|
||||
actionText="开始复盘"
|
||||
@action="goToTodayReview"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 首次加载中 -->
|
||||
<view class="initial-loading" v-if="loading && reviewList.length === 0">
|
||||
<view class="loading-spinner" :style="{ borderColor: themeConfig.primaryColor }"></view>
|
||||
<text :style="{ color: themeConfig.textSecondary }">加载中...</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getReviewList, deleteReview } from '@/api'
|
||||
import themeMixin from '@/mixins/themeMixin.js'
|
||||
import PageHeader from '@/components/PageHeader.vue'
|
||||
import AppCard from '@/components/AppCard.vue'
|
||||
import EmptyState from '@/components/EmptyState.vue'
|
||||
import InlineCalendar from '@/components/InlineCalendar.vue'
|
||||
import SwipeableItem from '@/components/SwipeableItem.vue'
|
||||
import FaIcon from '@/components/FaIcon.vue'
|
||||
|
||||
export default {
|
||||
name: 'ReviewList',
|
||||
mixins: [themeMixin],
|
||||
components: {
|
||||
PageHeader,
|
||||
AppCard,
|
||||
EmptyState,
|
||||
InlineCalendar,
|
||||
SwipeableItem,
|
||||
FaIcon
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
reviewList: [],
|
||||
searchKeyword: '',
|
||||
searchVisible: false,
|
||||
calendarVisible: false,
|
||||
dateRange: {
|
||||
start: '',
|
||||
end: ''
|
||||
},
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
hasMore: true,
|
||||
loading: false,
|
||||
loaded: false,
|
||||
refresherTriggered: false,
|
||||
swipeActions: [
|
||||
{ key: 'delete', label: '删除' }
|
||||
]
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
this.loadList(true)
|
||||
},
|
||||
onShow() {
|
||||
this.syncThemeConfig()
|
||||
// 从编辑页返回时刷新列表
|
||||
if (this.loaded) {
|
||||
this.loadList(true)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async loadList(reset = false) {
|
||||
if (this.loading) return
|
||||
this.loading = true
|
||||
|
||||
if (reset) {
|
||||
this.page = 1
|
||||
this.hasMore = true
|
||||
this.reviewList = []
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await getReviewList({
|
||||
keyword: this.searchKeyword,
|
||||
startDate: this.dateRange.start,
|
||||
endDate: this.dateRange.end,
|
||||
page: this.page,
|
||||
pageSize: this.pageSize
|
||||
})
|
||||
|
||||
const data = res
|
||||
const list = (data && data.list) ? data.list : (Array.isArray(data) ? data : [])
|
||||
const total = (data && data.total) != null ? data.total : list.length
|
||||
|
||||
if (reset) {
|
||||
this.reviewList = list
|
||||
} else {
|
||||
this.reviewList = [...this.reviewList, ...list]
|
||||
}
|
||||
|
||||
this.hasMore = this.reviewList.length < total
|
||||
this.page++
|
||||
this.loaded = true
|
||||
} catch (e) {
|
||||
console.error('加载复盘列表失败:', e)
|
||||
if (!this.loaded) {
|
||||
uni.showToast({ title: '加载失败,下拉刷新重试', icon: 'none' })
|
||||
}
|
||||
} finally {
|
||||
this.loading = false
|
||||
this.refresherTriggered = false
|
||||
}
|
||||
},
|
||||
|
||||
loadMore() {
|
||||
if (!this.hasMore || this.loading) return
|
||||
this.loadList(false)
|
||||
},
|
||||
|
||||
async onRefresh() {
|
||||
this.refresherTriggered = true
|
||||
await this.loadList(true)
|
||||
},
|
||||
|
||||
onSearch() {
|
||||
this.loadList(true)
|
||||
},
|
||||
|
||||
clearSearch() {
|
||||
this.searchKeyword = ''
|
||||
this.loadList(true)
|
||||
},
|
||||
|
||||
toggleSearch() {
|
||||
this.searchVisible = !this.searchVisible
|
||||
if (!this.searchVisible && this.searchKeyword) {
|
||||
this.searchKeyword = ''
|
||||
this.loadList(true)
|
||||
}
|
||||
},
|
||||
|
||||
toggleCalendar() {
|
||||
this.calendarVisible = !this.calendarVisible
|
||||
},
|
||||
|
||||
onDateRangeChange(range) {
|
||||
this.dateRange.start = range.start || ''
|
||||
this.dateRange.end = range.end || ''
|
||||
this.searchVisible = false
|
||||
this.calendarVisible = false
|
||||
this.loadList(true)
|
||||
},
|
||||
|
||||
clearDateRange() {
|
||||
this.dateRange.start = ''
|
||||
this.dateRange.end = ''
|
||||
this.loadList(true)
|
||||
},
|
||||
|
||||
onSwipeOpen() {},
|
||||
|
||||
async onSwipeAction(actionKey, item) {
|
||||
if (actionKey === 'delete') {
|
||||
uni.showModal({
|
||||
title: '确认删除',
|
||||
content: '确定要删除这条复盘记录吗?删除后不可恢复。',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
await this.handleDelete(item.id)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
async handleDelete(itemId) {
|
||||
try {
|
||||
await deleteReview({ id: itemId })
|
||||
this.reviewList = this.reviewList.filter(item => item.id !== itemId)
|
||||
uni.showToast({ title: '删除成功', icon: 'success' })
|
||||
} catch (e) {
|
||||
console.error('删除失败:', e)
|
||||
uni.showToast({ title: '删除失败,请重试', icon: 'none' })
|
||||
}
|
||||
},
|
||||
|
||||
goToDetail(item) {
|
||||
uni.navigateTo({
|
||||
url: `/pages/review/review?date=${item.date}`
|
||||
})
|
||||
},
|
||||
|
||||
goToTodayReview() {
|
||||
const now = new Date()
|
||||
const y = now.getFullYear()
|
||||
const m = String(now.getMonth() + 1).padStart(2, '0')
|
||||
const d = String(now.getDate()).padStart(2, '0')
|
||||
const today = `${y}-${m}-${d}`
|
||||
uni.navigateTo({
|
||||
url: `/pages/review/review?date=${today}`
|
||||
})
|
||||
},
|
||||
|
||||
formatDate(dateStr) {
|
||||
if (!dateStr) return ''
|
||||
const date = new Date(dateStr)
|
||||
const m = date.getMonth() + 1
|
||||
const d = date.getDate()
|
||||
return `${m}月${d}日`
|
||||
},
|
||||
|
||||
getWeekday(dateStr) {
|
||||
if (!dateStr) return ''
|
||||
const date = new Date(dateStr)
|
||||
const weekdays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
|
||||
return weekdays[date.getDay()]
|
||||
},
|
||||
|
||||
formatTime(dateStr) {
|
||||
if (!dateStr) return ''
|
||||
// 处理 '2026-07-28 20:30' 或 '2026-07-28' 格式
|
||||
const parts = dateStr.split(' ')
|
||||
if (parts.length > 1) {
|
||||
const timeParts = parts[1].split(':')
|
||||
return `${timeParts[0]}:${timeParts[1]}`
|
||||
}
|
||||
return ''
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.review-list-page {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
// 头部操作按钮
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
border-radius: 16rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.action-btn--active {
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.action-btn:active {
|
||||
transform: scale(0.92);
|
||||
}
|
||||
|
||||
// 筛选区域
|
||||
.filter-section {
|
||||
padding: 16rpx 32rpx 24rpx;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
padding: 16rpx 24rpx;
|
||||
border-radius: 16rpx;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
font-size: 28rpx;
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.clear-btn {
|
||||
flex-shrink: 0;
|
||||
padding: 4rpx;
|
||||
}
|
||||
|
||||
.calendar-section {
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
.date-range-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16rpx 24rpx 0;
|
||||
}
|
||||
|
||||
.range-text {
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.clear-range-btn {
|
||||
padding: 8rpx 16rpx;
|
||||
}
|
||||
|
||||
.clear-range-text {
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
// 列表
|
||||
.list-scroll {
|
||||
flex: 1;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.list-content {
|
||||
padding: 8rpx 32rpx 0;
|
||||
}
|
||||
|
||||
// 复盘卡片
|
||||
.review-item {
|
||||
padding: 4rpx 0;
|
||||
}
|
||||
|
||||
.item-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.date-wrap {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.item-date {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.item-weekday {
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.item-time {
|
||||
font-size: 22rpx;
|
||||
}
|
||||
|
||||
// KPT 字段
|
||||
.kpt-fields {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10rpx;
|
||||
}
|
||||
|
||||
.kpt-field {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12rpx;
|
||||
padding: 14rpx 16rpx;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
|
||||
.kpt-label {
|
||||
flex-shrink: 0;
|
||||
padding: 4rpx 14rpx;
|
||||
border-radius: 8rpx;
|
||||
font-size: 20rpx;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
min-width: 80rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.kpt-text {
|
||||
flex: 1;
|
||||
font-size: 26rpx;
|
||||
line-height: 1.6;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
// 底部状态
|
||||
.list-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12rpx;
|
||||
padding: 36rpx 0;
|
||||
}
|
||||
|
||||
.loading-dot {
|
||||
width: 12rpx;
|
||||
height: 12rpx;
|
||||
border-radius: 50%;
|
||||
animation: loadingPulse 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes loadingPulse {
|
||||
0%, 100% { opacity: 0.3; transform: scale(1); }
|
||||
50% { opacity: 1; transform: scale(1.3); }
|
||||
}
|
||||
|
||||
.footer-text {
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.bottom-spacer {
|
||||
height: 60rpx;
|
||||
}
|
||||
|
||||
// 空状态
|
||||
.empty-wrap {
|
||||
padding: 120rpx 32rpx;
|
||||
}
|
||||
|
||||
// 首次加载
|
||||
.initial-loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 200rpx 0;
|
||||
gap: 20rpx;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
border: 4rpx solid;
|
||||
border-top-color: transparent !important;
|
||||
border-right-color: transparent !important;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
@@ -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() {
|
||||
// 优先级: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>
|
||||
+1410
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,210 @@
|
||||
<template>
|
||||
<view class="agreement-page" :style="{ background: themeConfig.bgColor }">
|
||||
<!-- 协议内容滚动区 -->
|
||||
<scroll-view class="content-scroll" scroll-y>
|
||||
<view class="agreement-content" :style="{ background: themeConfig.cardBgColor }">
|
||||
<view class="title" :style="{ color: themeConfig.textPrimary }">简记memo用户协议</view>
|
||||
<view class="effective-date" :style="{ color: themeConfig.textSecondary }">生效日期:2025年04月24日</view>
|
||||
|
||||
<view class="section">
|
||||
<view class="section-title" :style="{
|
||||
color: themeConfig.primaryColor,
|
||||
background: themeConfig.primaryColor + '10',
|
||||
borderLeftColor: themeConfig.primaryColor
|
||||
}">1. 协议主体与范围</view>
|
||||
<view class="section-text" :style="{ color: themeConfig.textSecondary }">1.1 本协议是您(以下简称“用户”)与简记memo小程序(以下简称“本小程序”)个人运营者(以下简称“运营者”)之间关于使用本小程序所订立的契约,运营者为个人独立开发者,无所属企业/公司主体。</view>
|
||||
<view class="section-text" :style="{ color: themeConfig.textSecondary }">1.2 您通过微信小程序入口访问、使用本小程序的行为,即视为您已阅读、理解并同意本协议全部条款及《隐私政策》;若您不同意本协议或《隐私政策》,应立即停止使用本小程序。</view>
|
||||
<view class="section-text" :style="{ color: themeConfig.textSecondary }">1.3 本协议适用于您使用本小程序的所有行为,包括但不限于微信授权登录、数据录入、存储、编辑等核心功能。</view>
|
||||
</view>
|
||||
|
||||
<view class="section">
|
||||
<view class="section-title" :style="{
|
||||
color: themeConfig.primaryColor,
|
||||
background: themeConfig.primaryColor + '10',
|
||||
borderLeftColor: themeConfig.primaryColor
|
||||
}">2. 服务内容与范围</view>
|
||||
<view class="section-text" :style="{ color: themeConfig.textSecondary }">2.1 本小程序为用户提供免费的备忘录、待办事项、账单记录服务,核心功能包括文字内容录入、服务器存储、编辑、删除、数据同步等,所有功能仅基于微信OpenID识别用户身份提供。</view>
|
||||
<view class="section-text" :style="{ color: themeConfig.textSecondary }">2.2 运营者有权根据技术发展、用户需求、法律法规调整等因素,不定期优化、调整或暂停部分/全部服务,重大调整将通过小程序内公告提前7日告知用户。</view>
|
||||
<view class="section-text" :style="{ color: themeConfig.textSecondary }">2.3 本小程序为免费服务,运营者不承诺提供商业级别的服务稳定性,仅保障基础功能的正常可用。</view>
|
||||
</view>
|
||||
|
||||
<view class="section">
|
||||
<view class="section-title" :style="{
|
||||
color: themeConfig.primaryColor,
|
||||
background: themeConfig.primaryColor + '10',
|
||||
borderLeftColor: themeConfig.primaryColor
|
||||
}">3. 用户权利与义务</view>
|
||||
<view class="section-text" :style="{ color: themeConfig.textSecondary }">3.1 权利:您有权按照本协议约定使用本小程序核心功能;有权查看、编辑、删除自身存储在服务器的所有数据;有权向运营者反馈功能问题或优化建议;在遵守本协议的前提下,运营者保障您正常使用本小程序的基础权益。</view>
|
||||
<view class="section-text" :style="{ color: themeConfig.textSecondary }">3.2 义务:您保证使用本小程序的行为符合《中华人民共和国网络安全法》《互联网信息服务管理办法》等法律法规及公序良俗;不得利用本小程序存储、传播违法、违规、侵权、色情、暴力等内容;应对自身录入的内容及使用行为承担全部法律责任;不得对本小程序进行反向工程、篡改、破解、爬虫抓取等损害小程序正常运行的操作。</view>
|
||||
<view class="section-text" :style="{ color: themeConfig.textSecondary }">3.3 您理解并同意,微信OpenID仅用于身份识别,运营者不会通过该信息获取您的其他微信账号信息。</view>
|
||||
</view>
|
||||
|
||||
<view class="section">
|
||||
<view class="section-title" :style="{
|
||||
color: themeConfig.primaryColor,
|
||||
background: themeConfig.primaryColor + '10',
|
||||
borderLeftColor: themeConfig.primaryColor
|
||||
}">4. 服务的暂停与终止</view>
|
||||
<view class="section-text" :style="{ color: themeConfig.textSecondary }">4.1 运营者有权在以下情形暂停/终止服务:(1)您违反本协议约定且未在通知期限内纠正;(2)法律法规、微信平台规则要求暂停/终止;(3)不可抗力(如服务器故障、网络中断);(4)运营者自主停止运营(提前30日在小程序内公告)。</view>
|
||||
<view class="section-text" :style="{ color: themeConfig.textSecondary }">4.2 服务暂停期间,运营者将尽力恢复服务,但不承诺恢复时限;服务终止后,您存储在服务器的所有数据将保留7日,逾期将永久删除,运营者不承担数据找回、备份或赔偿责任。</view>
|
||||
<view class="section-text" :style="{ color: themeConfig.textSecondary }">4.3 您可主动申请终止服务(联系运营者删除账号及所有数据),申请生效后,运营者将在15个工作日内完成数据清理。</view>
|
||||
</view>
|
||||
|
||||
<view class="section">
|
||||
<view class="section-title" :style="{
|
||||
color: themeConfig.primaryColor,
|
||||
background: themeConfig.primaryColor + '10',
|
||||
borderLeftColor: themeConfig.primaryColor
|
||||
}">5. 免责声明</view>
|
||||
<view class="section-text" :style="{ color: themeConfig.textSecondary }">5.1 本小程序仅提供基础数据存储与管理功能,运营者不对您存储内容的完整性、准确性、安全性做绝对担保;因网络故障、服务器维护、微信平台规则调整、您的设备问题等非运营方可控因素导致的数据丢失、功能异常,运营者不承担赔偿责任。</view>
|
||||
<view class="section-text" :style="{ color: themeConfig.textSecondary }">5.2 本小程序为个人开发的免费服务,运营者无义务提供7×24小时技术支持,仅根据实际情况在合理期限内解答用户咨询(通过预留邮箱/公众号)。</view>
|
||||
<view class="section-text" :style="{ color: themeConfig.textSecondary }">5.3 因您违反法律法规或本协议约定,导致第三方索赔、行政机关处罚的,由您自行承担全部责任,运营者不承担连带责任。</view>
|
||||
</view>
|
||||
|
||||
<view class="section">
|
||||
<view class="section-title" :style="{
|
||||
color: themeConfig.primaryColor,
|
||||
background: themeConfig.primaryColor + '10',
|
||||
borderLeftColor: themeConfig.primaryColor
|
||||
}">6. 协议的变更</view>
|
||||
<view class="section-text" :style="{ color: themeConfig.textSecondary }">6.1 运营者可根据法律法规调整、业务优化需要修改本协议,修改后的协议将在小程序内公示,公示满7日后自动生效。</view>
|
||||
<view class="section-text" :style="{ color: themeConfig.textSecondary }">6.2 若您不同意修改后的协议,应立即停止使用本小程序并可申请删除账号数据;继续使用本小程序的,视为您同意修改后的协议。</view>
|
||||
</view>
|
||||
|
||||
<view class="section">
|
||||
<view class="section-title" :style="{
|
||||
color: themeConfig.primaryColor,
|
||||
background: themeConfig.primaryColor + '10',
|
||||
borderLeftColor: themeConfig.primaryColor
|
||||
}">7. 法律适用与争议解决</view>
|
||||
<view class="section-text" :style="{ color: themeConfig.textSecondary }">7.1 本协议适用中华人民共和国法律(不含港澳台地区法律)。</view>
|
||||
<view class="section-text" :style="{ color: themeConfig.textSecondary }">7.2 因本协议产生的争议,双方应友好协商解决;协商不成的,任何一方可向运营者所在地有管辖权的人民法院提起诉讼。</view>
|
||||
</view>
|
||||
|
||||
<view class="section">
|
||||
<view class="section-title" :style="{
|
||||
color: themeConfig.primaryColor,
|
||||
background: themeConfig.primaryColor + '10',
|
||||
borderLeftColor: themeConfig.primaryColor
|
||||
}">8. 联系与其他</view>
|
||||
<view class="section-text" :style="{ color: themeConfig.textSecondary }">8.1 若您对本协议有任何疑问,可通过以下方式联系运营者:联系邮箱:memo@miaoall.cn;微信公众号:孙三苗(sunsanmiao)。</view>
|
||||
<view class="section-text" :style="{ color: themeConfig.textSecondary }">8.2 本协议的条款如被有权机关认定为无效或不可执行,不影响其余条款的效力。</view>
|
||||
<view class="section-text" :style="{ color: themeConfig.textSecondary }">8.3 本协议与《隐私政策》共同构成您与运营者之间的完整约定,两者冲突时,以《隐私政策》中关于个人信息保护的内容为准。</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getThemeConfigFromStorage, THEME_PRESETS } from '@/utils/theme'
|
||||
import { refreshNavBarTheme } from '@/utils/themeGlobal'
|
||||
|
||||
export default {
|
||||
name: "userAgreement",
|
||||
data() {
|
||||
return {
|
||||
themeConfig: getThemeConfigFromStorage() || THEME_PRESETS['default']
|
||||
};
|
||||
},
|
||||
onLoad() {
|
||||
refreshNavBarTheme()
|
||||
this.themeChangeHandler = (themeData) => {
|
||||
let newTheme = null
|
||||
if (themeData?.key && THEME_PRESETS[themeData.key]) {
|
||||
newTheme = THEME_PRESETS[themeData.key]
|
||||
} else if (themeData?.config) {
|
||||
newTheme = themeData.config
|
||||
}
|
||||
if (newTheme) {
|
||||
this.themeConfig = newTheme
|
||||
refreshNavBarTheme()
|
||||
}
|
||||
}
|
||||
uni.$on('themeChanged', this.themeChangeHandler)
|
||||
},
|
||||
onShow() {
|
||||
const latestTheme = getThemeConfigFromStorage() || THEME_PRESETS['default']
|
||||
if (JSON.stringify(latestTheme) !== JSON.stringify(this.themeConfig)) {
|
||||
this.themeConfig = latestTheme
|
||||
}
|
||||
refreshNavBarTheme()
|
||||
},
|
||||
onUnload() {
|
||||
uni.$off('themeChanged', this.themeChangeHandler)
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.agreement-page {
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
padding: 20rpx 24rpx;
|
||||
padding-bottom: 40px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.content-scroll {
|
||||
height: calc(100vh - 40rpx);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.agreement-content {
|
||||
padding: 40rpx;
|
||||
border-radius: 12rpx;
|
||||
min-height: calc(100% - 48rpx);
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
margin-bottom: 16rpx;
|
||||
letter-spacing: 1rpx;
|
||||
}
|
||||
|
||||
.effective-date {
|
||||
text-align: center;
|
||||
font-size: 24rpx;
|
||||
margin-bottom: 40rpx;
|
||||
padding-bottom: 24rpx;
|
||||
border-bottom: 1px dashed rgba(132, 179, 217, 0.2);
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-bottom: 36rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
margin-bottom: 18rpx;
|
||||
padding: 12rpx 16rpx;
|
||||
border-radius: 12rpx;
|
||||
border-left: 4rpx solid;
|
||||
}
|
||||
|
||||
.section-text {
|
||||
font-size: 28rpx;
|
||||
line-height: 2;
|
||||
margin-bottom: 14rpx;
|
||||
text-indent: 56rpx;
|
||||
}
|
||||
|
||||
.footer-info {
|
||||
margin-top: 48rpx;
|
||||
padding-top: 24rpx;
|
||||
border-top: 1px dashed rgba(132, 179, 217, 0.2);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.footer-text {
|
||||
font-size: 24rpx;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.contact-link {
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user