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

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
+575
View File
@@ -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 形如 #RRGGBBpercent 负数表示变暗
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
View File
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
View File
File diff suppressed because it is too large Load Diff
+1128
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+235
View File
@@ -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>
+626
View File
@@ -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>
+602
View File
@@ -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>
+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>
+1410
View File
File diff suppressed because it is too large Load Diff
+210
View File
@@ -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>