feat: 增加主题色功能

This commit is contained in:
2026-09-01 11:15:00 +08:00
parent bb5a9b3c0f
commit bc6512f668
12 changed files with 386 additions and 229 deletions

View File

@@ -6,8 +6,11 @@ import {
getPlan, getPlan,
savePlan, savePlan,
getDefaultRecordValue, getDefaultRecordValue,
saveDefaultRecordValue, getStats, saveStats saveDefaultRecordValue, getStats, saveStats, getThemeColor, saveThemeColor
} from "@/utils/profile"; } from "@/utils/profile";
import { useAppStore } from "@/store/app";
const appStore = useAppStore();
onLaunch(() => { onLaunch(() => {
if (!getDefaultRecordValue()) { if (!getDefaultRecordValue()) {
@@ -30,6 +33,10 @@ onLaunch(() => {
if (getStats().length === 0) { if (getStats().length === 0) {
saveStats(['weight', 'sleep', 'sport', 'bloodPressure', 'bloodSugar', 'temperature']) saveStats(['weight', 'sleep', 'sport', 'bloodPressure', 'bloodSugar', 'temperature'])
} }
if (!getThemeColor()) {
saveThemeColor('purple')
}
appStore.themeColor = getThemeColor();
console.log("App Launch"); console.log("App Launch");
}); });
onShow(() => { onShow(() => {
@@ -40,11 +47,3 @@ onHide(() => {
}); });
</script> </script>
<style lang="scss">
page,
.wd-root-portal {
--wot-primary-5: #8b5cf6;
--wot-primary-6: #7c3aed;
--wot-primary-7: #6d28d9;
}
</style>

View File

@@ -31,8 +31,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useProfileStore } from '@/store/profile' import { useProfileStore } from '@/store/profile'
import { countRecordsByMonth, getTotalCount } from "@/utils/record"; import { saveOverview } from "@/utils/profile";
import {saveOverview} from "@/utils/profile";
const profileStore = useProfileStore() const profileStore = useProfileStore()

View File

@@ -0,0 +1,49 @@
<template>
<wd-popup
v-model="profileStore.showThemePopup"
position="bottom"
:safe-area-inset-bottom="true"
custom-style="border-radius: 32rpx 32rpx 0 0; padding: 40rpx 32rpx"
>
<view class="mb-4 text-center text-base text-gray-700 font-semibold">
主题色
</view>
<wd-radio-group v-model="profileStore.themeColor" type="dot" direction="horizontal"
:checked-color="getThemePrimaryColor[profileStore.themeColor]">
<wd-radio value="purple">暮夜紫</wd-radio>
<wd-radio value="green">薄荷绿</wd-radio>
<wd-radio value="blue">极客蓝</wd-radio>
<wd-radio value="amber">落日橙</wd-radio>
</wd-radio-group>
<view class="mt-5 flex w-full items-center justify-evenly">
<wd-button type="info" @click="cancelClick">
取消
</wd-button>
<wd-button type="primary" @click="saveClick">
保存
</wd-button>
</view>
</wd-popup>
</template>
<script lang="ts" setup>
import { useProfileStore } from '@/store/profile'
import { useAppStore } from '@/store/app'
import { getThemeColor, saveThemeColor } from "@/utils/profile";
import { getThemePrimaryColor } from "@/utils/themes";
const profileStore = useProfileStore()
const appStore = useAppStore()
function cancelClick() {
profileStore.showThemePopup = false
}
function saveClick() {
saveThemeColor(profileStore.themeColor)
appStore.themeColor = getThemeColor()
profileStore.showThemePopup = false
}
</script>

View File

@@ -1,111 +1,114 @@
<template> <template>
<view class="min-h-screen flex flex-col bg-gradient-to-b from-[#a78bfa] to-[#f3e8ff]"> <wd-config-provider :theme-vars="getTheme(appStore.themeColor)">
<view class="self-center py-3 flex items-center gap-2 text-gray-800"> <view class="min-h-screen flex flex-col" :style="getThemeStyle[appStore.themeColor]">
<wd-img :width="40" :height="40" :src="imgURL" /> <view class="self-center py-3 flex items-center gap-2 text-gray-800">
<wd-img :width="40" :height="40" :src="imgURL" />
<view class="flex min-w-0 flex-col"> <view class="flex min-w-0 flex-col">
<text class="truncate text-lg text-white font-semibold"> <text class="truncate text-lg text-white font-semibold">
{{ greeting }} {{ greeting }}
</text> </text>
<text class="mt-0.5 text-xs text-white/70"> <text class="mt-0.5 text-xs text-white/70">
{{ today }} · 记录每一次改变 {{ today }} · 记录每一次改变
</text> </text>
</view>
<wd-button type="info" size="mini" :disabled="hasSignedToday" @click="handleSign">
{{ hasSignedToday ? '已签到' : '签到'}}
</wd-button>
</view> </view>
<wd-button type="info" size="mini" :disabled="hasSignedToday" @click="handleSign"> <wd-card title="📊 今日概览">
{{ hasSignedToday ? '已签到' : '签到'}} <view class="grid grid-cols-2 gap-3">
</wd-button> <view
</view> v-for="item in overviewItems"
:key="item.label"
<wd-card title="📊 今日概览"> class="min-h-[80px] relative rounded-2xl bg-[#f8faf7] px-3.5 py-3 shadow-sm border border-[#eef0ef] flex flex-col justify-between"
<view class="grid grid-cols-2 gap-3"> >
<view <view class="flex items-center justify-between">
v-for="item in overviewItems" <view class="flex items-center gap-1.5">
:key="item.label" <text class="text-lg">{{ item.icon }}</text>
class="min-h-[80px] relative rounded-2xl bg-[#f8faf7] px-3.5 py-3 shadow-sm border border-[#eef0ef] flex flex-col justify-between" <text class="text-sm text-gray-400 font-medium">{{ item.label }}</text>
> </view>
<view class="flex items-center justify-between"> <wd-button v-if="item.value === '未记录'"
<view class="flex items-center gap-1.5"> size="mini" round icon="plus"
<text class="text-lg">{{ item.icon }}</text> :plain="true" type="primary"
<text class="text-sm text-gray-400 font-medium">{{ item.label }}</text> @click="addClick(item.type)"/>
</view> </view>
<wd-button v-if="item.value === '未记录'"
size="mini" round icon="plus" <text class="mt-2 text-base text-gray-800 font-bold tracking-tight">
:plain="true" type="primary" {{ item.value }}
@click="addClick(item.type)"/> </text>
<view class="mt-auto pt-1.5">
<text class="text-sm text-gray-400">{{ item.sub }}</text>
</view>
</view>
</view>
</wd-card>
<!-- 今日计划 -->
<wd-card title="🎯 今日计划">
<wd-empty v-if="plans.length === 0" icon="no-content" tip="暂无计划" />
<view v-for="(plan, idx) in plans" :key="idx">
<view class="flex items-center justify-between py-2.5">
<view class="flex items-center gap-2.5">
<text class="text-lg">{{ plan.icon }}</text>
<text class="text-sm">{{ plan.label }}</text>
</view>
<wd-tag v-if="plan.done" type="primary" round>
已完成
</wd-tag>
<wd-tag v-else type="danger" round>
未完成
</wd-tag>
</view>
<view v-if="idx < plans.length - 1" class="h-1px w-full bg-[#f2f4f3]" />
</view>
</wd-card>
<wd-card title="🔥 今日资讯">
<wd-empty v-if="tipList.length === 0" icon="no-content"
:tip="errorMessage || '获取资讯失败,请检查网络连接或联系管理员'" />
<view
v-for="(item, index) in tipList"
:key="index"
class="content-text flex flex-col py-[20rpx] border-b border-[#f5f5f5] last:border-b-0"
@click="viewTipClick(item)"
>
<view class="flex items-center gap-[12rpx]">
<wd-tag type="primary" round>{{ item.category }}</wd-tag>
<text class="text-[30rpx] font-semibold text-[#333] truncate">
{{ item.title }}
</text>
<wd-icon name="caret-right" size="24px"></wd-icon>
</view> </view>
<text class="mt-2 text-base text-gray-800 font-bold tracking-tight"> <text class="mt-[8rpx] text-[26rpx] text-[#999] leading-normal line-clamp-2">
{{ item.value }} {{ item.summary }}
</text> </text>
<view class="mt-auto pt-1.5">
<text class="text-sm text-gray-400">{{ item.sub }}</text>
</view>
</view> </view>
</view> </wd-card>
</wd-card>
<!-- 今日计划 --> <record-popup />
<wd-card title="🎯 今日计划">
<wd-empty v-if="plans.length === 0" icon="no-content" tip="暂无计划" />
<view v-for="(plan, idx) in plans" :key="idx"> <weight-popup />
<view class="flex items-center justify-between py-2.5"> <sleep-popup />
<view class="flex items-center gap-2.5"> <sport-popup />
<text class="text-lg">{{ plan.icon }}</text> <blood-pressure-popup />
<text class="text-sm">{{ plan.label }}</text> <blood-sugar-popup />
</view> <temperature-popup />
<tip-popup />
<wd-tag v-if="plan.done" type="primary" round> <liu-drag-button v-if="recordStore.showFab" :background="getThemePrimaryColor[appStore.themeColor]"
已完成 @clickBtn="handelFabClick">
</wd-tag> <wd-icon name="plus" size="24px"></wd-icon>
<wd-tag v-else type="danger" round> </liu-drag-button>
未完成 </view>
</wd-tag> </wd-config-provider>
</view>
<view v-if="idx < plans.length - 1" class="h-1px w-full bg-[#f2f4f3]" />
</view>
</wd-card>
<wd-card title="🔥 今日资讯">
<wd-empty v-if="tipList.length === 0" icon="no-content"
:tip="errorMessage || '获取资讯失败,请检查网络连接或联系管理员'" />
<view
v-for="(item, index) in tipList"
:key="index"
class="content-text flex flex-col py-[20rpx] border-b border-[#f5f5f5] last:border-b-0"
@click="viewTipClick(item)"
>
<view class="flex items-center gap-[12rpx]">
<wd-tag type="primary" round>{{ item.category }}</wd-tag>
<text class="text-[30rpx] font-semibold text-[#333] truncate">
{{ item.title }}
</text>
<wd-icon name="caret-right" size="24px"></wd-icon>
</view>
<text class="mt-[8rpx] text-[26rpx] text-[#999] leading-normal line-clamp-2">
{{ item.summary }}
</text>
</view>
</wd-card>
<record-popup />
<weight-popup />
<sleep-popup />
<sport-popup />
<blood-pressure-popup />
<blood-sugar-popup />
<temperature-popup />
<tip-popup />
<liu-drag-button v-if="recordStore.showFab" @clickBtn="handelFabClick">
<wd-icon name="plus" size="24px"></wd-icon>
</liu-drag-button>
</view>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@@ -128,6 +131,7 @@ import type { IOverview, IPlan } from "@/type/profile";
import { addSignInPoint } from "@/utils/points"; import { addSignInPoint } from "@/utils/points";
import type { ITip } from "@/type"; import type { ITip } from "@/type";
import TipPopup from "@/components/Home/TipPopup.vue"; import TipPopup from "@/components/Home/TipPopup.vue";
import { getTheme, getThemePrimaryColor, getThemeStyle } from "@/utils/themes";
defineOptions({ defineOptions({
name: 'Home', name: 'Home',

View File

@@ -1,85 +1,88 @@
<template> <template>
<view class="min-h-screen flex flex-col bg-gradient-to-b from-[#a78bfa] to-[#f3e8ff]"> <wd-config-provider :theme-vars="getTheme(appStore.themeColor)">
<wd-segmented v-model:value="appStore.viewRecordType" :options="viewTypeSegmentList" <view class="min-h-screen flex flex-col" :style="getThemeStyle[appStore.themeColor]">
@change="handleChangeSegment" <wd-segmented v-model:value="appStore.viewRecordType" :options="viewTypeSegmentList"
custom-style="align-self: center; width: 60%; margin: 10px;"> @change="handleChangeSegment"
<template #label="{ option }"> custom-style="align-self: center; width: 60%; margin: 10px;">
<view class="name">{{ option.payload }}</view> <template #label="{ option }">
</template> <view class="name">{{ option.payload }}</view>
</wd-segmented> </template>
</wd-segmented>
<wd-card v-if="appStore.viewRecordType === 'calendar'" custom-class="!mx-2 !mb-0"> <wd-card v-if="appStore.viewRecordType === 'calendar'" custom-class="!mx-2 !mb-0">
<wu-calendar :date="selectDate" :selected="calendarSelect" :itemHeight="45" :insert="true" color="#8b5cf6" <wu-calendar :date="selectDate" :selected="calendarSelect" :itemHeight="45" :insert="true"
@change="handleChangeDate" @monthSwitch="monthSwitch"/> :color="getThemePrimaryColor[appStore.themeColor]"
</wd-card> @change="handleChangeDate" @monthSwitch="monthSwitch"/>
</wd-card>
<wd-card v-if="appStore.viewRecordType === 'list'"> <wd-card v-if="appStore.viewRecordType === 'list'">
<view class="sticky top-0 z-10 flex flex-col gap-2 p-2"> <view class="sticky top-0 z-10 flex flex-col gap-2 p-2">
<view class="flex items-center justify-evenly"> <view class="flex items-center justify-evenly">
<wd-button icon="caret-left" type="primary" :round="true" size="small" @click="changeMonth(-1)" /> <wd-button icon="caret-left" type="primary" :round="true" size="small" @click="changeMonth(-1)" />
<view class="flex flex-col items-center" @click="openDatePicker"> <view class="flex flex-col items-center" @click="openDatePicker">
<text class="text-base text-gray-800 font-semibold">{{ displayDate }}</text> <text class="text-base text-gray-800 font-semibold">{{ displayDate }}</text>
</view> </view>
<wd-button icon="caret-right" type="primary" :round="true" size="small" @click="changeMonth(1)" /> <wd-button icon="caret-right" type="primary" :round="true" size="small" @click="changeMonth(1)" />
</view>
<wd-segmented v-model:value="currentRecordTypeSegment"
:options="recordTypeSegmentList"
@change="handleChangeSegment">
<template #label="{ option }">
<view class="name">{{ option.payload }}</view>
</template>
</wd-segmented>
</view>
</wd-card>
<!-- 时间轴区域 -->
<wd-card>
<wd-empty v-if="grouped.length === 0" icon="no-content" tip="暂无记录" />
<view v-for="group in grouped" :key="group.date"
class="mb-2 p-1 rounded odd:bg-gray-100 even:bg-white">
<view class="mb-2 flex items-center gap-2">
<text class="text-sm text-gray-500 font-bold">{{ group.date }}</text>
<text class="text-sm text-gray-400">{{ group.weekday }}</text>
</view>
<view
v-for="(item, idx) in group.items"
:key="item.id || idx"
class="mb-2 last:mb-0"
@longpress="deleteClick(item)"
>
<view class="flex items-center justify-between gap-1">
<text class="text-sm text-gray-400 font-mono">{{ item.time }}</text>
<wd-tag :type="getRecordStatus(item).type" round>
{{ getRecordStatus(item).label }}
</wd-tag>
</view> </view>
<view class="mt-1.5 flex items-center gap-2"> <wd-segmented v-model:value="currentRecordTypeSegment"
<text>{{ getRecordMeta(item.type).icon }}</text> :options="recordTypeSegmentList"
<text class="text-gray-700 font-medium">{{ getRecordMeta(item.type).label }}</text> @change="handleChangeSegment">
<template #label="{ option }">
<view class="name">{{ option.payload }}</view>
</template>
</wd-segmented>
</view>
</wd-card>
<!-- 时间轴区域 -->
<wd-card>
<wd-empty v-if="grouped.length === 0" icon="no-content" tip="暂无记录" />
<view v-for="group in grouped" :key="group.date"
class="mb-2 p-1 rounded odd:bg-gray-100 even:bg-white">
<view class="mb-2 flex items-center gap-2">
<text class="text-sm text-gray-500 font-bold">{{ group.date }}</text>
<text class="text-sm text-gray-400">{{ group.weekday }}</text>
</view> </view>
<text class="mt-1.5 block text-l text-gray-800 font-bold">{{ getRecordValue(item) }}</text> <view
v-for="(item, idx) in group.items"
:key="item.id || idx"
class="mb-2 last:mb-0"
@longpress="deleteClick(item)"
>
<view class="flex items-center justify-between gap-1">
<text class="text-sm text-gray-400 font-mono">{{ item.time }}</text>
<wd-tag :type="getRecordStatus(item).type" round>
{{ getRecordStatus(item).label }}
</wd-tag>
</view>
<text class="mt-1.5 block text-sm text-gray-400 leading-relaxed">{{ item.note }}</text> <view class="mt-1.5 flex items-center gap-2">
<text>{{ getRecordMeta(item.type).icon }}</text>
<text class="text-gray-700 font-medium">{{ getRecordMeta(item.type).label }}</text>
</view>
<view v-if="idx !== group.items.length - 1" class="mt-1.5 h-[1px] flex-1 bg-gray-200" /> <text class="mt-1.5 block text-l text-gray-800 font-bold">{{ getRecordValue(item) }}</text>
<text class="mt-1.5 block text-sm text-gray-400 leading-relaxed">{{ item.note }}</text>
<view v-if="idx !== group.items.length - 1" class="mt-1.5 h-[1px] flex-1 bg-gray-200" />
</view>
</view> </view>
</view> </wd-card>
</wd-card>
<!-- 日期选择器 --> <!-- 日期选择器 -->
<wd-datetime-picker <wd-datetime-picker
v-model="currentDate" v-model="currentDate"
v-model:visible="showDatePicker" v-model:visible="showDatePicker"
type="year-month" type="year-month"
:max-date="Date.now()" :max-date="Date.now()"
@confirm="onDateConfirm" @confirm="onDateConfirm"
/> />
</view> </view>
</wd-config-provider>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@@ -96,6 +99,7 @@ import {
import { onShow } from "@dcloudio/uni-app"; import { onShow } from "@dcloudio/uni-app";
import type { ICalendarSelect, IGroupHealthRecord, IHealthRecord } from "@/type/record"; import type { ICalendarSelect, IGroupHealthRecord, IHealthRecord } from "@/type/record";
import { useAppStore } from "@/store/app"; import { useAppStore } from "@/store/app";
import { getTheme, getThemePrimaryColor, getThemeStyle } from "@/utils/themes";
const currentDate = ref(Date.now()) const currentDate = ref(Date.now())
const currentRecordTypeSegment = ref('') const currentRecordTypeSegment = ref('')

View File

@@ -1,5 +1,5 @@
<template> <template>
<view class="min-h-screen flex flex-col bg-gradient-to-b from-[#a78bfa] to-[#f3e8ff]"> <view class="min-h-screen flex flex-col" :style="getThemeStyle[appStore.themeColor]">
<view class="self-center py-3 flex items-center gap-2 text-gray-800"> <view class="self-center py-3 flex items-center gap-2 text-gray-800">
<view class="flex min-w-0 flex-col"> <view class="flex min-w-0 flex-col">
<text class="truncate text-lg text-white font-semibold"> <text class="truncate text-lg text-white font-semibold">
@@ -92,9 +92,12 @@ import { ref } from 'vue'
import { getRecentRecords } from '@/utils/record' import { getRecentRecords } from '@/utils/record'
import { onShow } from "@dcloudio/uni-app"; import { onShow } from "@dcloudio/uni-app";
import { useProfileStore } from '@/store/profile' import { useProfileStore } from '@/store/profile'
import { useAppStore } from '@/store/app'
import { getStats } from "@/utils/profile"; import { getStats } from "@/utils/profile";
import { getThemeStyle } from "@/utils/themes";
const profileStore = useProfileStore() const profileStore = useProfileStore()
const appStore = useAppStore()
const weightChartData = ref({ const weightChartData = ref({
categories: [] as string[], categories: [] as string[],

View File

@@ -1,67 +1,73 @@
<template> <template>
<view class="min-h-screen bg-[#f2f4f3] pb-10"> <wd-config-provider :theme-vars="getTheme(appStore.themeColor)">
<wd-card title="个人信息" type="rectangle"> <view class="min-h-screen bg-[#f2f4f3] pb-10">
<view class="flex items-center gap-4"> <wd-card title="个人信息" type="rectangle">
<wd-avatar :text="profile.name ? profile.name.charAt(0).toUpperCase() : '我'" bg-color="#8b5cf6" /> <view class="flex items-center gap-4">
<wd-avatar :text="profile.name ? profile.name.charAt(0).toUpperCase() : '我'"
:bg-color="getThemePrimaryColor[appStore.themeColor]" />
<view class="flex flex-col"> <view class="flex flex-col">
<text class="text-lg text-gray-800 font-semibold"> <text class="text-lg text-gray-800 font-semibold">
{{ profile.name || '未设置姓名' }} {{ profile.name || '未设置姓名' }}
</text> </text>
<text class="mt-0.5 text-xs text-gray-400"> <text class="mt-0.5 text-xs text-gray-400">
{{ genderText }} · {{ profile.height ? `${profile.height} cm` : '身高未填' }} {{ genderText }} · {{ profile.height ? `${profile.height} cm` : '身高未填' }}
</text> </text>
</view>
<view class="ml-auto">
<wd-button size="small" plain type="primary" @click="openProfile">
编辑
</wd-button>
</view>
</view> </view>
<view class="ml-auto"> </wd-card>
<wd-button size="small" plain type="primary" @click="openProfile">
编辑 <wd-card title="积分成就" type="rectangle">
<view class="flex items-center justify-between">
<view class="mt-2 space-y-1 text-gray-600">
<view> 当前积分{{ appStore.points }}</view>
<view>📊 记录次数{{ appStore.recordCount }}</view>
<view>🏆 当前成就{{ appStore.achievement }}</view>
</view>
<wd-button size="small" plain type="primary" @click="openPointPopup">
查看说明
</wd-button> </wd-button>
</view> </view>
</view>
</wd-card>
<wd-card title="积分成就" type="rectangle"> <view class="mt-2 text-xs text-gray-400">
<view class="flex items-center justify-between"> 坚持记录健康数据解锁更多成就
<view class="mt-2 space-y-1 text-gray-600">
<view> 当前积分{{ appStore.points }}</view>
<view>📊 记录次数{{ appStore.recordCount }}</view>
<view>🏆 当前成就{{ appStore.achievement }}</view>
</view> </view>
</wd-card>
<wd-button size="small" plain type="primary" @click="openPointPopup"> <wd-cell-group>
查看说明 <wd-cell size="large" title="记录默认值" is-link @click="openDefaultPopup"/>
</wd-button> <wd-cell size="large" title="今日概览" is-link @click="openOverviewPopup"/>
</view> <wd-cell size="large" title="今日计划" is-link @click="openPlanPopup"/>
<wd-cell size="large" title="数据统计" is-link @click="openStatsPopup"/>
<wd-cell size="large" title="主题色" is-link @click="openThemePopup"/>
<wd-cell size="large" title="存储详情" is-link @click="openStoragePopup"/>
</wd-cell-group>
<view class="mt-2 text-xs text-gray-400"> <wd-text :text="`版本号:${appStore.version}`" custom-style="margin-top: 5px;display:block; text-align:center;"></wd-text>
坚持记录健康数据解锁更多成就
</view>
</wd-card>
<wd-cell-group> <profile-popup />
<wd-cell size="large" title="记录默认值" is-link @click="openDefaultPopup"/>
<wd-cell size="large" title="今日概览" is-link @click="openOverviewPopup"/>
<wd-cell size="large" title="今日计划" is-link @click="openPlanPopup"/>
<wd-cell size="large" title="数据统计" is-link @click="openStatsPopup"/>
<wd-cell size="large" title="存储详情" is-link @click="openStoragePopup"/>
</wd-cell-group>
<wd-text :text="`版本号:${appStore.version}`" custom-style="margin-top: 5px;display:block; text-align:center;"></wd-text> <storage-popup />
<profile-popup /> <overview-popup />
<storage-popup /> <plan-popup />
<overview-popup /> <stats-popup />
<plan-popup /> <default-popup />
<stats-popup /> <point-popup />
<default-popup /> <theme-popup />
</view>
<point-popup /> </wd-config-provider>
</view>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@@ -74,12 +80,13 @@ import { getDefaultRecordValue, getOverview, getPlan, getProfile, getStats } fro
import { useProfileStore } from '@/store/profile' import { useProfileStore } from '@/store/profile'
import { useAppStore } from "@/store/app"; import { useAppStore } from "@/store/app";
import type { IProfile } from "@/type/profile"; import type { IProfile } from "@/type/profile";
import { countRecordsByMonth, getMaxRecord, getTotalCount } from "@/utils/record";
import DefaultPopup from "@/components/Profile/DefaultPopup.vue"; import DefaultPopup from "@/components/Profile/DefaultPopup.vue";
import { getAchievement, getPoints, getRecordCount } from "@/utils/points"; import { getAchievement, getPoints, getRecordCount } from "@/utils/points";
import { onShow } from "@dcloudio/uni-app"; import { onShow } from "@dcloudio/uni-app";
import PointPopup from "@/components/Profile/PointPopup.vue"; import PointPopup from "@/components/Profile/PointPopup.vue";
import StatsPopup from "@/components/Profile/StatsPopup.vue"; import StatsPopup from "@/components/Profile/StatsPopup.vue";
import { getTheme, getThemePrimaryColor } from "@/utils/themes";
import ThemePopup from "@/components/Profile/ThemePopup.vue";
const profileStore = useProfileStore() const profileStore = useProfileStore()
const appStore = useAppStore() const appStore = useAppStore()
@@ -131,6 +138,10 @@ function openStatsPopup() {
profileStore.showStatsPopup = true profileStore.showStatsPopup = true
} }
function openThemePopup() {
profileStore.showThemePopup = true
}
function openStoragePopup() { function openStoragePopup() {
profileStore.refreshStorage() profileStore.refreshStorage()
profileStore.showStoragePopup = true profileStore.showStoragePopup = true

View File

@@ -9,6 +9,7 @@ export const useAppStore = defineStore('app', {
recordCount: 0, recordCount: 0,
achievement: '', achievement: '',
showTipPopup: false, showTipPopup: false,
themeColor: 'purple',
currentTip: { currentTip: {
title: '', title: '',
summary: '', summary: '',

View File

@@ -12,11 +12,13 @@ export const useProfileStore = defineStore('profile', {
showStatsPopup: false, showStatsPopup: false,
showDefaultRecordValuePopup: false, showDefaultRecordValuePopup: false,
showPointPopup: false, showPointPopup: false,
showThemePopup: false,
profileForm: { profileForm: {
gender: '', gender: '',
height: 0, height: 0,
name: '', name: '',
} as IProfile, } as IProfile,
themeColor: 'purple',
defaultRecordValueForm: { defaultRecordValueForm: {
weightValue: 70, weightValue: 70,
sportDuration: 30, sportDuration: 30,

View File

@@ -2,6 +2,7 @@
<view> <view>
<movable-area class="movable-area" :scale-area="false"> <movable-area class="movable-area" :scale-area="false">
<movable-view class="movable-view" :class="!isRemove?'animation-info':''" style="pointer-events: auto;" <movable-view class="movable-view" :class="!isRemove?'animation-info':''" style="pointer-events: auto;"
:style="{ background: background }"
@click="clickBtn" @touchstart="touchstart" @touchend="touchend" @change="onChange" direction="all" @click="clickBtn" @touchstart="touchstart" @touchend="touchend" @change="onChange" direction="all"
inertia="true" :x="x" :y="y" :disabled="disabled" :out-of-bounds="true" :damping="200" :friction="100"> inertia="true" :x="x" :y="y" :disabled="disabled" :out-of-bounds="true" :damping="200" :friction="100">
<slot></slot> <slot></slot>
@@ -13,6 +14,10 @@
<script> <script>
export default { export default {
props: { props: {
background: {
type: String,
default: "#8b5cf6"
},
//是否禁用拖动 //是否禁用拖动
disabled: { disabled: {
type: Boolean, type: Boolean,
@@ -110,8 +115,7 @@
.movable-view { .movable-view {
width: 100rpx; width: 100rpx;
height: 100rpx; height: 100rpx;
background: #8b5cf6; box-shadow: 0px 4rpx 12rpx 0px rgba(15, 23, 42, 0.08);
box-shadow: 0px 4rpx 12rpx 0px rgba(139, 92, 246, 0.4);
border-radius: 50rpx; border-radius: 50rpx;
color: #FFFFFF; color: #FFFFFF;
font-size: 26rpx; font-size: 26rpx;

View File

@@ -4,6 +4,7 @@ const PROFILE_KEY = 'user_profile'
const OVERVIEW_KEY = 'user_overview' const OVERVIEW_KEY = 'user_overview'
const PLAN_KEY = 'user_plan' const PLAN_KEY = 'user_plan'
const STATS_KEY = 'user_stats' const STATS_KEY = 'user_stats'
const THEME_KEY = 'user_theme'
const DEFAULT_RECORD_VALUE_KEY = 'user_default_record_value' const DEFAULT_RECORD_VALUE_KEY = 'user_default_record_value'
function getEmptyProfile(): IProfile { function getEmptyProfile(): IProfile {
@@ -54,3 +55,11 @@ export function saveStats(data: string[]) {
wx.setStorageSync(STATS_KEY, data) wx.setStorageSync(STATS_KEY, data)
} }
export function getThemeColor(): string {
return wx.getStorageSync(THEME_KEY) || ''
}
export function saveThemeColor(data: string) {
wx.setStorageSync(THEME_KEY, data)
}

72
src/utils/themes.ts Normal file
View File

@@ -0,0 +1,72 @@
import type { ConfigProviderThemeVars } from "@wot-ui/ui";
export type ThemeKey = 'purple' | 'green' | 'blue' | 'amber'
interface ThemeMeta {
label: string;
vars: ConfigProviderThemeVars
}
const THEMES: Record<ThemeKey, ThemeMeta> = {
purple: {
label: '暮夜紫',
vars: {
primary1: '#f5f3ff', primary2: '#ede9fe', primary3: '#ddd6fe', primary4: '#c4b5fd',
primary5: '#8b5cf6', primary6: '#7c3aed', primary7: '#6d28d9',
primary8: '#5b21b6', primary9: '#4c1d95', primary10: '#3b1779'
}
},
green: {
label: '薄荷绿',
vars: {
primary1: '#ecfdf5', primary2: '#d1fae5', primary3: '#a7f3d0', primary4: '#6ee7b7',
primary5: '#34d399', primary6: '#059669', primary7: '#047857',
primary8: '#065f46', primary9: '#04472f', primary10: '#03301f'
}
},
blue: {
label: '极客蓝',
vars: {
primary1: '#eff6ff', primary2: '#dbeafe', primary3: '#bfdbfe', primary4: '#93c5fd',
primary5: '#60a5fa', primary6: '#2563eb', primary7: '#1d4ed8',
primary8: '#1e40af', primary9: '#1e3a8a', primary10: '#172554'
}
},
amber: {
label: '落日橙',
vars: {
primary1: '#fff7ed', primary2: '#ffedd5', primary3: '#fed7aa', primary4: '#fdba74',
primary5: '#fb923c', primary6: '#ea580c', primary7: '#c2410c',
primary8: '#9a3412', primary9: '#7c2d12', primary10: '#4a1d0b'
}
}
}
export const DEFAULT_THEME: ThemeKey = 'purple'
export function getTheme(key?: string | ThemeKey | null): ConfigProviderThemeVars {
const safeKey = (key ?? DEFAULT_THEME) as ThemeKey
return THEMES[safeKey].vars ?? THEMES[DEFAULT_THEME].vars
}
export const getThemeStyle: Record<ThemeKey, { backgroundImage: string }> = {
purple: {
backgroundImage: 'linear-gradient(180deg, #7c3aed 0%, #a78bfa 42%, #ede9fe 100%)'
},
green: {
backgroundImage: 'linear-gradient(180deg, #059669 0%, #34d399 42%, #d1fae5 100%)'
},
blue: {
backgroundImage: 'linear-gradient(180deg, #2563eb 0%, #60a5fa 42%, #dbeafe 100%)'
},
amber: {
backgroundImage: 'linear-gradient(180deg, #c2410c 0%, #fb923c 42%, #ffedd5 100%)'
}
}
export const getThemePrimaryColor: Record<ThemeKey, string> = {
purple: '#6d28d9',
green: '#047857',
blue: '#1d4ed8',
amber: '#b45309'
}