refactor: ♻️ 我的和登录页面优化,添加主题设置

This commit is contained in:
Ray.Hao
2025-04-05 10:05:05 +08:00
parent 572d347bdb
commit 6a245e9d26
21 changed files with 2594 additions and 182 deletions

View File

@@ -1,16 +1,32 @@
<script setup lang="ts">
import { onLaunch, onShow, onHide } from "@dcloudio/uni-app";
import { useThemeStore } from "@/store";
// 主题初始化
const themeStore = useThemeStore();
onLaunch(() => {
console.log("App Launch");
// 初始化主题
themeStore.initTheme();
});
onShow(() => {
console.log("App Hide");
console.log("App Show");
});
onHide(() => {
console.log("App Hide");
});
</script>
<style lang="scss">
:root {
--primary-color: #165dff;
--primary-color-light: #94bfff;
--primary-color-dark: #0e3c9b;
}
page {
background: #f8f8f8;
}

View File

@@ -0,0 +1,122 @@
<template>
<view :class="['todo-item', { completed: todo.completed }]" @tap="toggleCompleted">
<view class="status-icon">
<wd-icon v-if="todo.completed" name="check" color="#67C23A" size="20" />
<view v-else class="uncompleted-circle"></view>
</view>
<view class="todo-content">
<view class="todo-title">{{ todo.title }}</view>
<view v-if="todo.description" class="todo-description">
{{ todo.description }}
</view>
<view v-if="todo.deadline" class="todo-deadline">
<wd-icon name="time" size="14" color="#999999" style="margin-right: 4rpx" />
{{ formatDate(todo.deadline) }}
</view>
</view>
<view class="todo-actions">
<wd-icon
name="delete-filling"
color="#F56C6C"
size="18"
@tap.stop="$emit('delete', todo.id)"
/>
</view>
</view>
</template>
<script lang="ts" setup>
import { defineProps, defineEmits } from "vue";
import { Todo } from "@/types/todo";
const props = defineProps({
todo: {
type: Object as () => Todo,
required: true,
},
});
const emit = defineEmits(["update", "delete"]);
// 切换完成状态
const toggleCompleted = () => {
emit("update", {
...props.todo,
completed: !props.todo.completed,
});
};
// 格式化日期
const formatDate = (date: string) => {
const d = new Date(date);
const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, "0");
const day = String(d.getDate()).padStart(2, "0");
return `${year}-${month}-${day}`;
};
</script>
<style lang="scss" scoped>
.todo-item {
display: flex;
align-items: flex-start;
padding: 24rpx;
margin-bottom: 16rpx;
background-color: #fff;
border-radius: 12rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
&.completed {
opacity: 0.7;
.todo-title {
color: #909399;
text-decoration: line-through;
}
}
.status-icon {
display: flex;
align-items: center;
justify-content: center;
width: 40rpx;
height: 40rpx;
margin-right: 20rpx;
.uncompleted-circle {
width: 36rpx;
height: 36rpx;
border: 2rpx solid #dcdfe6;
border-radius: 50%;
}
}
.todo-content {
flex: 1;
.todo-title {
margin-bottom: 8rpx;
font-size: 30rpx;
font-weight: 500;
color: #303133;
}
.todo-description {
margin-bottom: 8rpx;
font-size: 26rpx;
color: #606266;
}
.todo-deadline {
display: flex;
align-items: center;
font-size: 24rpx;
color: #909399;
}
}
.todo-actions {
padding: 6rpx;
}
}
</style>

View File

@@ -0,0 +1,43 @@
<template>
<view class="todo-list">
<wd-empty v-if="todos.length === 0" description="暂无待办事项" />
<TodoItem
v-for="todo in todos"
:key="todo.id"
:todo="todo"
@update="handleUpdate"
@delete="handleDelete"
/>
</view>
</template>
<script lang="ts" setup>
import { defineProps, defineEmits } from "vue";
import { Todo } from "@/types/todo";
import TodoItem from "@/components/todo/TodoItem.vue";
defineProps({
todos: {
type: Array as () => Todo[],
default: () => [],
},
});
const emit = defineEmits(["update", "delete"]);
// 处理更新待办事项
const handleUpdate = (todo: Todo) => {
emit("update", todo);
};
// 处理删除待办事项
const handleDelete = (id: string) => {
emit("delete", id);
};
</script>
<style lang="scss" scoped>
.todo-list {
padding: 20rpx 0;
}
</style>

View File

@@ -3,6 +3,7 @@ import App from "./App.vue";
import setupPlugins from "@/plugins";
import "uno.css";
import "@/styles/global.scss";
import { setupStore } from "@/store";

View File

@@ -68,6 +68,12 @@
"navigationBarTitleText": "隐私政策"
}
},
{
"path": "pages/mine/settings/theme/index",
"style": {
"navigationBarTitleText": "主题设置"
}
},
{
"path": "pages/mine/settings/network/index",
"style": {
@@ -80,6 +86,12 @@
"navigationBarTitleText": "登录"
}
},
{
"path": "pages/login/complete-profile",
"style": {
"navigationBarTitleText": "完善个人信息"
}
},
{
"path": "pages/mine/profile/index",
"style": {

View File

@@ -0,0 +1,487 @@
<template>
<view class="complete-profile-container">
<!-- 顶部导航栏 -->
<view class="nav-bar">
<wd-icon name="arrow-left" size="20" color="#fff" @click="goBack" />
<text class="nav-title">完善个人信息</text>
<view style="width: 20px"></view>
</view>
<!-- 蓝色区域内容 -->
<view class="top-content">
<view class="title">完善个人信息</view>
<view class="subtitle">完善您的个人信息以获得更好的使用体验</view>
</view>
<!-- 白色区域 -->
<view class="bottom-content">
<!-- 头像上传 -->
<view class="avatar-section">
<view class="avatar-wrapper" @click="chooseAvatar">
<image v-if="userForm.avatar" :src="userForm.avatar" class="avatar" />
<view v-else class="avatar-placeholder">
<wd-icon name="fill-camera" size="28" color="#999" />
</view>
<view class="avatar-upload">
<wd-icon name="camera" size="16" color="#fff" />
</view>
</view>
<view class="avatar-tip">点击更换头像</view>
</view>
<!-- 表单部分 -->
<view class="form-section">
<wd-form ref="formRef" :model="userForm">
<!-- 昵称输入框 -->
<view class="input-wrap">
<text class="input-label">昵称</text>
<input
v-model="userForm.nickname"
class="form-input"
placeholder="请输入您的昵称"
placeholder-style="color: #999; font-weight: normal;"
/>
<wd-icon
v-if="userForm.nickname"
name="close-fill"
size="18"
color="#999"
class="clear-icon"
@click="userForm.nickname = ''"
/>
</view>
<view class="divider"></view>
<!-- 性别选择 -->
<view class="gender-wrap">
<text class="input-label">性别</text>
<view class="gender-options">
<view
class="gender-option"
:class="userForm.gender === 1 ? 'gender-selected' : ''"
@click="userForm.gender = 1"
>
<text></text>
</view>
<view
class="gender-option"
:class="userForm.gender === 2 ? 'gender-selected' : ''"
@click="userForm.gender = 2"
>
<text></text>
</view>
</view>
</view>
<view class="divider"></view>
<!-- 提交按钮 -->
<view class="button-wrap">
<button
class="submit-button"
:disabled="submitting"
:style="submitting ? 'opacity: 0.7;' : ''"
@click="handleSubmit"
>
提交
</button>
</view>
<!-- 微信获取信息按钮 -->
<view v-if="isWechatEnv" class="button-wrap mt-20">
<button
class="wechat-button"
:disabled="wxAuthing"
:style="wxAuthing ? 'opacity: 0.7;' : ''"
@click="handleGetWechatInfo"
>
<image src="/static/icons/weixin.png" class="wechat-icon" />
<text>一键获取微信头像昵称</text>
</button>
</view>
</wd-form>
</view>
</view>
<!-- 头像裁剪 -->
<wd-img-cropper v-model="avatarShow" :img-src="originalSrc" @confirm="handleAvatarConfirm" />
<!-- 弹窗 -->
<wd-toast />
</view>
</template>
<script lang="ts" setup>
import { onLoad } from "@dcloudio/uni-app";
import { useToast } from "wot-design-uni";
import { useUserStore } from "@/store/modules/user";
import FileAPI, { type FileInfo } from "@/api/file";
import UserAPI, { type UserProfileForm } from "@/api/system/user";
import { ref } from "vue";
const toast = useToast();
const formRef = ref();
const userStore = useUserStore();
const submitting = ref(false);
const wxAuthing = ref(false);
const avatarShow = ref(false);
const originalSrc = ref("");
const redirectPath = ref("/pages/index/index");
// 用户表单数据
const userForm = ref<UserProfileForm>({
nickname: "",
gender: 1,
avatar: "",
});
// 判断是否微信环境
const isWechatEnv = ref(false);
// 获取参数
onLoad((options) => {
// 读取用户信息
if (userStore.userInfo) {
userForm.value.nickname = userStore.userInfo.nickname || "";
userForm.value.gender = userStore.userInfo.gender || 1;
userForm.value.avatar = userStore.userInfo.avatar || "";
}
// 保存跳转路径
if (options.redirect) {
redirectPath.value = decodeURIComponent(options.redirect);
}
// 检测环境
// #ifdef MP-WEIXIN
isWechatEnv.value = true;
// #endif
});
// 返回上一页
const goBack = () => {
uni.navigateBack();
};
// 选择头像
const chooseAvatar = () => {
uni.chooseImage({
count: 1,
success: (res) => {
originalSrc.value = res.tempFilePaths[0];
avatarShow.value = true;
},
});
};
// 头像裁剪完成
const handleAvatarConfirm = (event: any) => {
const { tempFilePath } = event;
// 显示上传中
uni.showLoading({ title: "上传中..." });
// 上传头像
FileAPI.upload(tempFilePath)
.then((fileInfo: FileInfo) => {
userForm.value.avatar = fileInfo.url;
uni.hideLoading();
})
.catch(() => {
uni.hideLoading();
toast.error("头像上传失败,请重试");
});
};
// 一键获取微信信息
const handleGetWechatInfo = () => {
wxAuthing.value = true;
// #ifdef MP-WEIXIN
// @ts-ignore
uni.getUserProfile({
desc: "用于完善用户资料",
success: (res) => {
const { userInfo } = res;
// 填充表单
userForm.value.nickname = userInfo.nickName;
userForm.value.gender = userInfo.gender;
// 如果有头像,下载并上传头像
if (userInfo.avatarUrl) {
uni.showLoading({ title: "获取头像中..." });
// 下载微信头像
uni.downloadFile({
url: userInfo.avatarUrl,
success: (res) => {
if (res.statusCode === 200) {
// 上传到服务器
FileAPI.upload(res.tempFilePath)
.then((fileInfo: FileInfo) => {
userForm.value.avatar = fileInfo.url;
uni.hideLoading();
})
.catch(() => {
uni.hideLoading();
toast.error("头像上传失败");
});
}
},
fail: () => {
uni.hideLoading();
toast.error("获取微信头像失败");
},
});
}
},
fail: () => {
toast.error("获取微信信息失败,请手动填写");
},
complete: () => {
wxAuthing.value = false;
},
});
// #endif
// 非微信环境
// #ifndef MP-WEIXIN
toast.error("当前环境不支持获取微信信息");
wxAuthing.value = false;
// #endif
};
// 提交表单
const handleSubmit = () => {
if (submitting.value) return;
// 简单验证
if (!userForm.value.nickname.trim()) {
toast.error("请填写昵称");
return;
}
submitting.value = true;
// 更新用户信息
UserAPI.updateProfile(userForm.value)
.then(async () => {
// 更新本地存储的用户信息
await userStore.getInfo();
toast.success("信息已更新");
// 延迟跳转
setTimeout(() => {
uni.reLaunch({
url: redirectPath.value,
});
}, 1500);
})
.catch(() => {
toast.error("更新失败,请重试");
})
.finally(() => {
submitting.value = false;
});
};
</script>
<style lang="scss" scoped>
.complete-profile-container {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #0063ff;
}
.nav-bar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 30rpx;
padding-top: calc(30rpx + var(--status-bar-height, 0px));
}
.nav-title {
font-size: 36rpx;
font-weight: 500;
color: #fff;
}
.top-content {
display: flex;
flex-direction: column;
align-items: center;
padding: 20rpx 0 60rpx;
}
.top-content .title {
margin-bottom: 20rpx;
font-size: 36rpx;
font-weight: 500;
color: #fff;
}
.top-content .subtitle {
padding: 0 60rpx;
font-size: 26rpx;
color: rgba(255, 255, 255, 0.9);
text-align: center;
}
.bottom-content {
flex: 1;
padding: 40rpx 30rpx;
overflow-y: auto;
background-color: #fff;
border-radius: 30rpx 30rpx 0 0;
}
.avatar-section {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 40rpx;
}
.avatar-section .avatar-wrapper {
position: relative;
width: 160rpx;
height: 160rpx;
margin-bottom: 20rpx;
}
.avatar-section .avatar-wrapper .avatar {
width: 160rpx;
height: 160rpx;
border-radius: 50%;
}
.avatar-section .avatar-wrapper .avatar-placeholder {
display: flex;
align-items: center;
justify-content: center;
width: 160rpx;
height: 160rpx;
background-color: #f5f7fa;
border-radius: 50%;
}
.avatar-section .avatar-wrapper .avatar-upload {
position: absolute;
right: 5rpx;
bottom: 5rpx;
display: flex;
align-items: center;
justify-content: center;
width: 40rpx;
height: 40rpx;
background-color: #4080ff;
border: 2rpx solid #fff;
border-radius: 50%;
}
.avatar-section .avatar-tip {
font-size: 26rpx;
color: #666;
}
.form-section .input-wrap {
position: relative;
display: flex;
align-items: center;
padding: 20rpx 0;
}
.form-section .input-wrap .input-label {
width: 100rpx;
font-size: 28rpx;
color: #333;
}
.form-section .input-wrap .form-input {
flex: 1;
height: 60rpx;
font-size: 28rpx;
line-height: 60rpx;
color: #333;
}
.form-section .input-wrap .clear-icon {
padding: 10rpx;
}
.form-section .gender-wrap {
display: flex;
align-items: center;
padding: 20rpx 0;
}
.form-section .gender-wrap .input-label {
width: 100rpx;
font-size: 28rpx;
color: #333;
}
.form-section .gender-wrap .gender-options {
display: flex;
flex: 1;
gap: 20rpx;
justify-content: flex-end;
}
.form-section .gender-wrap .gender-options .gender-option {
padding: 10rpx 30rpx;
font-size: 28rpx;
color: #666;
background-color: #f5f7fa;
border-radius: 10rpx;
}
.form-section .gender-wrap .gender-options .gender-option.gender-selected {
color: #fff;
background-color: #4080ff;
}
.form-section .divider {
height: 1px;
margin: 0 0 10rpx 0;
background-color: #e8e8e8;
}
.form-section .button-wrap {
margin-top: 60rpx;
}
.form-section .button-wrap .submit-button {
width: 100%;
height: 90rpx;
font-size: 32rpx;
line-height: 90rpx;
color: #fff;
background-color: #4080ff;
border: none;
border-radius: 45rpx;
}
.form-section .button-wrap .wechat-button {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 90rpx;
font-size: 30rpx;
color: #fff;
background-color: #07c160;
border: none;
border-radius: 45rpx;
}
.form-section .button-wrap .wechat-button .wechat-icon {
width: 36rpx;
height: 36rpx;
margin-right: 10rpx;
}
.mt-20 {
margin-top: 20rpx;
}
</style>

View File

@@ -1,107 +1,159 @@
<template>
<view class="login-container">
<view class="login-header">
<image src="/static/logo.png" class="w160rpx h160rpx" />
<view class="text-sm text-white">有来开源专注于构建高效开发的应用解决方案</view>
<!-- 背景图 -->
<image src="/static/images/auth/login-bg.svg" mode="aspectFill" class="login-bg" />
<!-- Logo和标题区域 -->
<view class="header">
<image src="/static/logo.png" class="logo" />
<text class="title">有来开源</text>
<text class="subtitle">专注于构建高效开发的应用解决方案</text>
</view>
<view class="login-form">
<wd-form ref="loginFormRef" :model="loginFormData">
<wd-cell-group border>
<wd-input
v-model="loginFormData.username"
label="用户名"
label-width="100px"
prop="username"
clearable
placeholder="请输入用户名"
:rules="[{ required: true, message: '请填写用户名' }]"
/>
<wd-input
v-model="loginFormData.password"
label="密码"
label-width="100px"
prop="password"
show-password
clearable
placeholder="请输入密码"
:rules="[{ required: true, message: '请填写密码' }]"
/>
</wd-cell-group>
<view class="mt-80rpx">
<wd-button size="large" type="primary" block @click="handleLogin">登录</wd-button>
<!-- 登录表单区域 -->
<view class="login-card">
<view class="form-wrap">
<wd-form ref="loginFormRef" :model="loginFormData">
<!-- 用户名输入框 -->
<view class="form-item">
<wd-icon name="user" size="22" color="#165DFF" class="input-icon" />
<input v-model="loginFormData.username" class="form-input" placeholder="请输入用户名" />
<wd-icon
v-if="loginFormData.username"
name="close-fill"
size="18"
color="#9ca3af"
class="clear-icon"
@click="loginFormData.username = ''"
/>
</view>
<view class="divider"></view>
<!-- 密码输入框 -->
<view class="form-item">
<wd-icon name="lock-on" size="22" color="#165DFF" class="input-icon" />
<input
v-model="loginFormData.password"
class="form-input"
type="password"
placeholder="请输入密码"
placeholder-style="color: #9ca3af; font-weight: normal;"
/>
<wd-icon name="eye-close" size="18" color="#9ca3af" class="eye-icon" />
</view>
<view class="divider"></view>
<!-- 登录按钮 -->
<button
class="login-btn"
:disabled="loading"
:style="loading ? 'opacity: 0.7;' : ''"
@click="handleLogin"
>
登录
</button>
</wd-form>
<!-- 微信登录 -->
<view class="other-login">
<view class="other-login-title">
<view class="line"></view>
<text class="text">其他登录方式</text>
<view class="line"></view>
</view>
<view class="wechat-login" @click="handleWechatLogin">
<view class="wechat-icon-wrapper">
<image src="/static/icons/weixin.png" class="wechat-icon" />
</view>
</view>
</view>
</wd-form>
<!-- 底部协议 -->
<view class="agreement">
<text class="text">登录即同意</text>
<text class="link" @click="navigateToUserAgreement">用户协议</text>
<text class="text"></text>
<text class="link" @click="navigateToPrivacy">隐私政策</text>
</view>
</view>
</view>
<view class="login-footer">
<view class="text-center">
<wd-divider>
<img
src="/static/icons/weixin.png"
class="w-[80rpx] h-[80rpx]"
@click="handleWechatLogin"
/>
</wd-divider>
</view>
<view class="text-center mt-20rpx text-sm">
<text class="text-gray">登录即同意</text>
<text @click="navigateToUserAgreement">用户协议</text>
<text class="text-gray"></text>
<text @click="navigateToPrivacy">隐私政策</text>
</view>
</view>
<wd-toast />
</view>
</template>
<script lang="ts" setup>
import { onLoad } from "@dcloudio/uni-app";
import { type LoginFormData } from "@/api/auth";
import { useUserStore } from "@/store/modules/user";
const loginFormRef = ref();
import { useToast } from "wot-design-uni";
import { ref } from "vue";
const loginFormRef = ref();
const toast = useToast();
const loading = ref(false);
const userStore = useUserStore();
// 登录表单数据
const loginFormData = ref<LoginFormData>({
username: "admin",
password: "123456",
});
const userStore = useUserStore();
// 获取重定向参数
const redirect = ref("");
onLoad((options) => {
if (options) {
redirect.value = options.redirect ? decodeURIComponent(options.redirect) : "/pages/index/index";
} else {
redirect.value = "/pages/index/index";
}
});
// 登录处理
const handleLogin = () => {
loginFormRef.value.validate().then(async ({ valid }: { valid: boolean }) => {
if (valid) {
try {
await userStore.login(loginFormData.value);
await userStore.getInfo();
uni.showToast({ title: "登录成功", icon: "success" });
if (loading.value) return;
loading.value = true;
// 检查是否有上一页
userStore
.login(loginFormData.value)
.then(() => userStore.getInfo())
.then(() => {
toast.success("登录成功");
// 检查用户信息是否完整
if (!userStore.isUserInfoComplete()) {
// 信息不完整,跳转到完善信息页面
setTimeout(() => {
uni.navigateTo({
url: `/pages/login/complete-profile?redirect=${encodeURIComponent(redirect.value)}`,
});
}, 1000);
} else {
// 否则直接跳转到重定向页面
setTimeout(() => {
uni.reLaunch({
url: "/pages/index/index",
url: redirect.value,
});
}, 1500);
} catch (error: any) {
console.log("登录失败", error.message);
}, 1000);
}
}
});
};
const navigateToUserAgreement = () => {
uni.navigateTo({
url: "/pages/mine/user-agreement/index",
});
};
const navigateToPrivacy = () => {
uni.navigateTo({
url: "/pages/mine/privacy/index",
});
})
.catch((error) => {
toast.error(error?.message || "登录失败");
})
.finally(() => {
loading.value = false;
});
};
// 微信登录处理
const handleWechatLogin = async () => {
if (loading.value) return;
loading.value = true;
try {
// #ifdef MP-WEIXIN
// 获取微信登录的临时 code
const { code } = await uni.login({
provider: "weixin",
@@ -113,55 +165,222 @@ const handleWechatLogin = async () => {
if (result) {
// 获取用户信息
await userStore.getInfo();
toast.success("登录成功");
uni.showToast({
title: "登录成功",
icon: "success",
});
const pages = getCurrentPages();
if (pages.length > 1) {
uni.navigateBack();
// 检查用户信息是否完整
if (!userStore.isUserInfoComplete()) {
// 如果信息不完整,跳转到完善信息页面
setTimeout(() => {
uni.navigateTo({
url: `/pages/login/complete-profile?redirect=${encodeURIComponent(redirect.value)}`,
});
}, 1000);
} else {
uni.reLaunch({
url: "/pages/index/index",
});
// 否则直接跳转到重定向页面
setTimeout(() => {
uni.reLaunch({
url: redirect.value,
});
}, 1000);
}
}
// #endif
// #ifndef MP-WEIXIN
toast.error("当前环境不支持微信登录");
// #endif
} catch (error: any) {
console.error("微信登录失败", error);
uni.showToast({
title: error.message || "微信登录失败",
icon: "none",
});
toast.error(error?.message || "微信登录失败");
} finally {
loading.value = false;
}
};
// 跳转到用户协议页面
const navigateToUserAgreement = () => {
uni.navigateTo({
url: "/pages/mine/user-agreement/index",
});
};
// 跳转到隐私政策页面
const navigateToPrivacy = () => {
uni.navigateTo({
url: "/pages/mine/privacy/index",
});
};
</script>
<style lang="scss" scoped>
.login-container {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
height: 100vh;
background: #fff;
.login-header {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 160rpx 0;
background: url("/static/images/login-bg.png") no-repeat center center;
background-size: 100% 100%;
}
.login-form {
width: 80%;
margin: 80rpx auto;
}
overflow: hidden;
}
.login-footer {
position: absolute;
bottom: 40rpx;
width: 100%;
}
.login-bg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.header {
z-index: 2;
display: flex;
flex-direction: column;
align-items: center;
margin-top: 120rpx;
}
.logo {
width: 140rpx;
height: 140rpx;
margin-bottom: 20rpx;
}
.title {
margin-bottom: 10rpx;
font-size: 48rpx;
font-weight: bold;
color: #ffffff;
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.1);
}
.subtitle {
font-size: 28rpx;
color: #ffffff;
text-align: center;
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.1);
}
.login-card {
z-index: 2;
display: flex;
flex-direction: column;
width: 90%;
margin-top: 80rpx;
overflow: hidden;
background-color: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(10px);
border-radius: 24rpx;
box-shadow: 0 8rpx 40rpx rgba(0, 0, 0, 0.1);
}
.form-wrap {
padding: 40rpx;
}
.form-item {
position: relative;
display: flex;
align-items: center;
padding: 24rpx 0;
}
.input-icon {
margin-right: 20rpx;
}
.form-input {
flex: 1;
height: 60rpx;
font-size: 28rpx;
line-height: 60rpx;
color: #333;
}
.clear-icon,
.eye-icon {
padding: 10rpx;
}
.divider {
height: 1px;
margin: 0;
background-color: rgba(0, 0, 0, 0.06);
}
.login-btn {
width: 100%;
height: 90rpx;
margin-top: 60rpx;
font-size: 32rpx;
line-height: 90rpx;
color: #fff;
background: linear-gradient(90deg, #165dff, #4080ff);
border: none;
border-radius: 45rpx;
box-shadow: 0 8rpx 20rpx rgba(22, 93, 255, 0.3);
transition: all 0.3s;
}
.login-btn:active {
box-shadow: 0 4rpx 10rpx rgba(22, 93, 255, 0.2);
transform: translateY(2rpx);
}
.other-login {
margin-top: 60rpx;
}
.other-login-title {
display: flex;
align-items: center;
margin-bottom: 40rpx;
}
.line {
flex: 1;
height: 1px;
background-color: rgba(0, 0, 0, 0.08);
}
.text {
padding: 0 30rpx;
font-size: 26rpx;
color: #9ca3af;
}
.wechat-login {
display: flex;
justify-content: center;
margin-bottom: 30rpx;
}
.wechat-icon-wrapper {
display: flex;
align-items: center;
justify-content: center;
width: 90rpx;
height: 90rpx;
background-color: #fff;
border-radius: 50%;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08);
}
.wechat-icon {
width: 60rpx;
height: 60rpx;
}
.agreement {
display: flex;
justify-content: center;
margin-top: 30rpx;
font-size: 24rpx;
}
.agreement .text {
padding: 0 4rpx;
color: #9ca3af;
}
.agreement .link {
color: #165dff;
}
</style>

View File

@@ -59,9 +59,15 @@
<script lang="ts" setup>
import { ref } from "vue";
import { useToast } from "wot-design-uni";
import { checkLogin } from "@/utils/auth";
const toast = useToast();
// 检查登录状态
onLoad(() => {
if (!checkLogin()) return;
});
// 问题类型选项
const feedbackTypes = [
{ label: "功能异常", value: "bug" },

View File

@@ -1,58 +1,218 @@
<template>
<view class="mine-container">
<view class="mine-header">
<view class="flex items-center">
<!-- 头像 -->
<image
class="w-100rpx h-100rpx rounded-full"
:src="isLogin ? userInfo!.avatar : defaultAvatar"
/>
<!-- 用户信息 -->
<view class="ml-20rpx">
<!-- 已登录 -->
<view v-if="isLogin">
<view class="text-32rpx">
{{ userInfo!.nickname }}
</view>
<view class="text-28rpx mt2">{{ userInfo?.username }}</view>
<!-- 用户信息卡片 -->
<view class="user-profile">
<view class="blur-bg"></view>
<view class="user-info">
<view class="avatar-container" @click="navigateToProfile">
<image
class="avatar"
:src="isLogin ? userInfo!.avatar : defaultAvatar"
mode="aspectFill"
/>
<view v-if="isLogin" class="avatar-edit">
<wd-icon name="edit-pen" size="16" color="#fff" />
</view>
<!-- 未登录 -->
<view v-else>
<view class="login-tip">您还未登录请先登录</view>
<view class="w120rpx mt-2">
<button class="login-btn" @click="navigateToLoginPage">登录</button>
</view>
</view>
<view class="user-details">
<block v-if="isLogin">
<view class="nickname">{{ userInfo!.nickname || "匿名用户" }}</view>
<view class="user-id">ID: {{ userInfo?.username || "0000000" }}</view>
</block>
<block v-else>
<view class="login-prompt">立即登录获取更多功能</view>
<wd-button class="login-btn" size="small" type="primary" @click="navigateToLoginPage">
登录/注册
</wd-button>
</block>
</view>
<view class="actions">
<view class="action-btn" @click="navigateToSettings">
<wd-icon name="setting1" size="22" color="#333" />
</view>
<view v-if="isLogin" class="action-btn" @click="navigateToSection('messages')">
<wd-icon name="message" size="22" color="#333" />
<view v-if="true" class="badge">2</view>
</view>
</view>
</view>
<!-- 编辑资料 -->
<view v-if="isLogin" class="cursor-pointer">
<wd-icon name="setting" @click="navigateToProfile" />
</view>
<!-- 数据统计 -->
<view class="stats-container">
<view class="stat-item" @click="navigateToSection('wallet')">
<view class="stat-value">0.00</view>
<view class="stat-label">我的余额</view>
</view>
<view class="divider"></view>
<view class="stat-item" @click="navigateToSection('favorites')">
<view class="stat-value">0</view>
<view class="stat-label">我的收藏</view>
</view>
<view class="divider"></view>
<view class="stat-item" @click="navigateToSection('history')">
<view class="stat-value">0</view>
<view class="stat-label">浏览历史</view>
</view>
</view>
<!-- 功能操作 -->
<wd-grid clickable custom-class="rounded-[10rpx] bg-white">
<wd-grid-item text="我的消息" icon="mail" @itemclick="handleItemclick" />
<wd-grid-item text="我的待办" icon="list" @itemclick="handleItemclick" />
<wd-grid-item text="我的收藏" icon="star" @itemclick="handleItemclick" />
<wd-grid-item text="浏览历史" icon="view" @itemclick="handleItemclick" />
</wd-grid>
<!-- 菜单列表 -->
<view class="mt40rpx">
<wd-cell-group border>
<wd-cell title="关于我们" icon="info-circle" clickable @click="navigateToAbout">
<wd-icon name="arrow-right" />
</wd-cell>
<wd-cell title="常见问题" icon="help-circle" clickable @click="navigateToFAQ">
<wd-icon name="arrow-right" />
</wd-cell>
<wd-cell title="问题反馈" icon="check-circle" clickable @click="handleQuestionFeedback">
<wd-icon name="arrow-right" />
</wd-cell>
<wd-cell title="设置" icon="setting1" clickable @click="navigateToSettings">
<wd-icon name="arrow-right" />
</wd-cell>
</wd-cell-group>
<!-- 我的订单 -->
<view class="card-container">
<view class="card-header">
<view class="card-title">
<wd-icon name="cart" size="18" :color="themeStore.primaryColor" />
<text>我的订单</text>
</view>
<view class="card-action" @click="navigateToSection('orders')">
<text>全部订单</text>
<wd-icon name="arrow-right" size="14" color="#999" />
</view>
</view>
<view class="order-status">
<view class="status-item" @click="navigateToSection('orders', 'pending')">
<view class="status-icon">
<wd-icon name="wallet-pay" size="28" :color="themeStore.primaryColor" />
<view v-if="true" class="status-badge">2</view>
</view>
<view class="status-label">待付款</view>
</view>
<view class="status-item" @click="navigateToSection('orders', 'shipping')">
<view class="status-icon">
<wd-icon name="shopping" size="28" :color="themeStore.primaryColor" />
</view>
<view class="status-label">待发货</view>
</view>
<view class="status-item" @click="navigateToSection('orders', 'receiving')">
<view class="status-icon">
<wd-icon name="car" size="28" :color="themeStore.primaryColor" />
</view>
<view class="status-label">待收货</view>
</view>
<view class="status-item" @click="navigateToSection('orders', 'comment')">
<view class="status-icon">
<wd-icon name="comment-o" size="28" :color="themeStore.primaryColor" />
</view>
<view class="status-label">待评价</view>
</view>
<view class="status-item" @click="navigateToSection('orders', 'after-sale')">
<view class="status-icon">
<wd-icon name="service" size="28" :color="themeStore.primaryColor" />
</view>
<view class="status-label">售后</view>
</view>
</view>
</view>
<!-- 常用工具 -->
<view class="card-container">
<view class="card-header">
<view class="card-title">
<wd-icon name="tools" size="18" :color="themeStore.primaryColor" />
<text>常用工具</text>
</view>
</view>
<view class="tools-grid">
<view class="tool-item" @click="navigateToProfile">
<view class="tool-icon">
<wd-icon name="person" size="24" :color="themeStore.primaryColor" />
</view>
<view class="tool-label">个人资料</view>
</view>
<view class="tool-item" @click="navigateToSection('address')">
<view class="tool-icon">
<wd-icon name="location" size="24" :color="themeStore.primaryColor" />
</view>
<view class="tool-label">收货地址</view>
</view>
<view class="tool-item" @click="navigateToSection('todos')">
<view class="tool-icon">
<wd-icon name="task" size="24" :color="themeStore.primaryColor" />
</view>
<view class="tool-label">待办事项</view>
</view>
<view class="tool-item" @click="navigateToFAQ">
<view class="tool-icon">
<wd-icon name="help-circle" size="24" :color="themeStore.primaryColor" />
</view>
<view class="tool-label">常见问题</view>
</view>
<view class="tool-item" @click="handleQuestionFeedback">
<view class="tool-icon">
<wd-icon name="check-circle" size="24" :color="themeStore.primaryColor" />
</view>
<view class="tool-label">问题反馈</view>
</view>
<view class="tool-item" @click="navigateToAbout">
<view class="tool-icon">
<wd-icon name="info-circle" size="24" :color="themeStore.primaryColor" />
</view>
<view class="tool-label">关于我们</view>
</view>
<view class="tool-item" @click="navigateToSettings">
<view class="tool-icon">
<wd-icon name="setting1" size="24" :color="themeStore.primaryColor" />
</view>
<view class="tool-label">设置</view>
</view>
<view class="tool-item" @click="navigateToSection('wallet')">
<view class="tool-icon">
<wd-icon name="wallet" size="24" :color="themeStore.primaryColor" />
</view>
<view class="tool-label">我的钱包</view>
</view>
</view>
</view>
<!-- 推荐服务 -->
<view class="card-container">
<view class="card-header">
<view class="card-title">
<wd-icon name="star" size="18" :color="themeStore.primaryColor" />
<text>推荐服务</text>
</view>
</view>
<view class="services-list">
<view class="service-item" @click="navigateToSection('services', 'vip')">
<view class="service-left">
<view class="service-icon vip-icon">
<wd-icon name="crown" size="22" color="#FFD700" />
</view>
<view class="service-info">
<view class="service-name">会员中心</view>
<view class="service-desc">解锁更多特权</view>
</view>
</view>
<wd-icon name="arrow-right" size="14" color="#999" />
</view>
<view class="service-item" @click="navigateToSection('services', 'coupon')">
<view class="service-left">
<view class="service-icon">
<wd-icon name="ticket" size="22" :color="themeStore.primaryColor" />
</view>
<view class="service-info">
<view class="service-name">优惠券</view>
<view class="service-desc">查看我的优惠券</view>
</view>
</view>
<wd-icon name="arrow-right" size="14" color="#999" />
</view>
<view class="service-item" @click="navigateToSection('services', 'invite')">
<view class="service-left">
<view class="service-icon">
<wd-icon name="share" size="22" :color="themeStore.primaryColor" />
</view>
<view class="service-info">
<view class="service-name">邀请有礼</view>
<view class="service-desc">邀请好友得奖励</view>
</view>
</view>
<wd-icon name="arrow-right" size="14" color="#999" />
</view>
</view>
</view>
<view v-if="isLogin" class="logout-btn-container">
<wd-button class="logout-btn" @click="handleLogout">退出登录</wd-button>
</view>
<wd-toast />
@@ -62,19 +222,47 @@
<script lang="ts" setup>
import { useToast } from "wot-design-uni";
import { useUserStore } from "@/store/modules/user";
import { useThemeStore } from "@/store/modules/theme";
import { computed } from "vue";
const toast = useToast();
const userStore = useUserStore();
const themeStore = useThemeStore();
const userInfo = computed(() => userStore.userInfo);
const isLogin = computed(() => !!userInfo.value);
const defaultAvatar = "/static/images/default-avatar.png";
// 登录
const navigateToLoginPage = () => {
uni.navigateTo({ url: "/pages/login/index" });
const pages = getCurrentPages();
const currentPage = pages[pages.length - 1];
const currentPagePath = `/${currentPage.route}`;
uni.navigateTo({
url: `/pages/login/index?redirect=${encodeURIComponent(currentPagePath)}`,
});
};
// 退出登录
const handleLogout = () => {
uni.showModal({
title: "提示",
content: "确认退出登录吗?",
success: function (res) {
if (res.confirm) {
userStore.logout();
toast.show("已退出登录");
}
},
});
};
// 个人信息
const navigateToProfile = () => {
if (!isLogin.value) {
navigateToLoginPage();
return;
}
uni.navigateTo({ url: "/pages/mine/profile/index" });
};
@@ -82,61 +270,394 @@ const navigateToProfile = () => {
const navigateToFAQ = () => {
uni.navigateTo({ url: "/pages/mine/faq/index" });
};
// 关于我们
const navigateToAbout = () => {
uni.navigateTo({ url: "/pages/mine/about/index" });
};
// 设置
const navigateToSettings = () => {
uni.navigateTo({ url: "/pages/mine/settings/index" });
};
// 问题反馈
const handleQuestionFeedback = () => {
uni.navigateTo({ url: "/pages/mine/feedback/index" });
};
// 建设中
const handleItemclick = () => {
toast.show("建设中...");
// 导航到各个板块
const navigateToSection = (section: string, subSection?: string) => {
if (!isLogin.value && section !== "services") {
navigateToLoginPage();
return;
}
const sections: Record<string, string> = {
messages: "消息中心",
todos: "待办事项",
favorites: "我的收藏",
history: "浏览历史",
wallet: "我的钱包",
orders: "我的订单",
address: "收货地址",
services: "增值服务",
};
let message = sections[section];
if (subSection) {
message += ` - ${subSection}`;
}
toast.show(`${message}功能开发中...`);
};
</script>
<style lang="scss" scoped>
.mine-container {
.mine-header {
min-height: 100vh;
padding-bottom: 100rpx;
background-color: #f5f7fa;
}
// 用户信息卡片
.user-profile {
position: relative;
padding: 30rpx;
overflow: hidden;
.blur-bg {
position: absolute;
top: 0;
right: 0;
left: 0;
z-index: 0;
height: 240rpx;
background: linear-gradient(to bottom, var(--primary-color), var(--primary-color-light));
}
.user-info {
position: relative;
z-index: 1;
display: flex;
align-items: start;
align-items: center;
.avatar-container {
position: relative;
.avatar {
width: 120rpx;
height: 120rpx;
border: 4rpx solid rgba(255, 255, 255, 0.8);
border-radius: 50%;
box-shadow: 0 6rpx 16rpx rgba(0, 0, 0, 0.1);
}
.avatar-edit {
position: absolute;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
width: 36rpx;
height: 36rpx;
background-color: var(--primary-color);
border-radius: 50%;
box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.2);
}
}
.user-details {
flex: 1;
margin-left: 24rpx;
.nickname {
margin-bottom: 8rpx;
font-size: 34rpx;
font-weight: bold;
color: #fff;
}
.user-id {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.8);
}
.login-prompt {
margin-bottom: 16rpx;
font-size: 28rpx;
color: #fff;
}
.login-btn {
width: 160rpx;
height: 60rpx;
font-size: 26rpx;
color: var(--primary-color);
background-color: #fff;
border: none;
border-radius: 30rpx;
}
}
.actions {
display: flex;
.action-btn {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 70rpx;
height: 70rpx;
margin-left: 16rpx;
background-color: rgba(255, 255, 255, 0.9);
border-radius: 50%;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);
.badge {
position: absolute;
top: -6rpx;
right: -6rpx;
z-index: 2;
min-width: 32rpx;
height: 32rpx;
padding: 0 6rpx;
font-size: 20rpx;
line-height: 32rpx;
color: #fff;
text-align: center;
background-color: #ff4d4f;
border: 2rpx solid #fff;
border-radius: 16rpx;
}
}
}
}
}
// 数据统计
.stats-container {
display: flex;
padding: 30rpx 20rpx;
margin: 20rpx 30rpx;
background: #fff;
border-radius: 16rpx;
box-shadow: 0 6rpx 16rpx rgba(0, 0, 0, 0.04);
.stat-item {
display: flex;
flex: 1;
flex-direction: column;
align-items: center;
.stat-value {
margin-bottom: 8rpx;
font-size: 36rpx;
font-weight: 600;
color: #333;
}
.stat-label {
font-size: 26rpx;
color: #666;
}
}
.divider {
width: 1px;
margin: 0 20rpx;
background-color: #eee;
}
}
// 卡片容器通用样式
.card-container {
margin: 24rpx 30rpx;
overflow: hidden;
background: #fff;
border-radius: 16rpx;
box-shadow: 0 6rpx 16rpx rgba(0, 0, 0, 0.04);
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20rpx 24rpx;
border-bottom: 1rpx solid #f5f5f5;
padding: 60rpx 20rpx;
color: #fff;
background: linear-gradient(60deg, #517cf0, #769ef5);
.card-title {
display: flex;
align-items: center;
.login-tip {
font-size: 28rpx;
color: #fff;
text {
margin-left: 12rpx;
font-size: 28rpx;
font-weight: 600;
color: #333;
}
}
.login-btn {
font-size: 28rpx;
line-height: 50rpx;
color: #4d80f0;
border-radius: 10rpx;
.card-action {
display: flex;
align-items: center;
text {
margin-right: 8rpx;
font-size: 24rpx;
color: #999;
}
}
.cursor-pointer {
}
}
// 订单状态
.order-status {
display: flex;
padding: 30rpx 0 20rpx;
.status-item {
display: flex;
flex: 1;
flex-direction: column;
align-items: center;
.status-icon {
position: relative;
display: flex;
align-items: center;
justify-content: center;
margin: auto;
margin-right: 10rpx;
width: 80rpx;
height: 80rpx;
margin-bottom: 12rpx;
.status-badge {
position: absolute;
top: -10rpx;
right: -10rpx;
z-index: 2;
min-width: 32rpx;
height: 32rpx;
padding: 0 6rpx;
font-size: 20rpx;
line-height: 32rpx;
color: #fff;
text-align: center;
background-color: #ff4d4f;
border-radius: 16rpx;
}
}
.status-label {
font-size: 24rpx;
color: #666;
}
}
}
@keyframes spin {
0% {
transform: rotate(0deg);
// 工具网格
.tools-grid {
display: flex;
flex-wrap: wrap;
padding: 20rpx 0 10rpx;
.tool-item {
display: flex;
flex-direction: column;
align-items: center;
width: 25%;
margin-bottom: 30rpx;
.tool-icon {
display: flex;
align-items: center;
justify-content: center;
width: 90rpx;
height: 90rpx;
margin-bottom: 12rpx;
background-color: rgba(var(--primary-color-rgb), 0.08);
border-radius: 18rpx;
transition: transform 0.2s;
&:active {
transform: scale(0.95);
}
}
.tool-label {
font-size: 24rpx;
color: #555;
}
}
100% {
transform: rotate(360deg);
}
// 服务列表
.services-list {
.service-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 30rpx 24rpx;
border-bottom: 1rpx solid #f5f5f5;
&:last-child {
border-bottom: none;
}
.service-left {
display: flex;
align-items: center;
.service-icon {
display: flex;
align-items: center;
justify-content: center;
width: 70rpx;
height: 70rpx;
background-color: rgba(var(--primary-color-rgb), 0.08);
border-radius: 16rpx;
&.vip-icon {
background: linear-gradient(135deg, #ffd700, #ffa500);
}
}
.service-info {
margin-left: 20rpx;
.service-name {
margin-bottom: 6rpx;
font-size: 28rpx;
font-weight: 500;
color: #333;
}
.service-desc {
font-size: 24rpx;
color: #999;
}
}
}
}
}
// 退出登录按钮
.logout-btn-container {
margin: 60rpx 30rpx;
.logout-btn {
width: 100%;
height: 80rpx;
font-size: 28rpx;
color: #666;
background-color: #f5f5f5;
border: none;
border-radius: 40rpx;
&:active {
opacity: 0.8;
}
}
}
</style>

View File

@@ -65,6 +65,7 @@
<script setup lang="ts">
import UserAPI, { type UserProfileVO, UserProfileForm } from "@/api/system/user";
import FileAPI, { type FileInfo } from "@/api/file";
import { checkLogin } from "@/utils/auth";
const originalSrc = ref<string>(""); //选取的原图路径
const avatarShow = ref<boolean>(false); //显示头像裁剪
@@ -137,7 +138,10 @@ function handleSubmit() {
});
}
onMounted(() => {
// 检查登录状态
onLoad(() => {
if (!checkLogin()) return;
// #ifdef H5
document.addEventListener("touchstart", touchstartListener, { passive: false });
document.addEventListener("touchmove", touchmoveListener, { passive: false });
@@ -145,6 +149,11 @@ onMounted(() => {
loadUserProfile();
});
onMounted(() => {
// 在onMounted中不再重复检查登录状态和加载用户信息
// 如果需要检查登录状态和加载用户信息请使用onLoad中的逻辑
});
// 页面销毁前移除事件监听
onBeforeUnmount(() => {
// #ifdef H5

View File

@@ -2,6 +2,7 @@
<view class="settings-container">
<wd-cell-group>
<wd-cell title="账号和安全" icon="secured" is-link @click="navigateToAccount" />
<wd-cell title="主题设置" icon="brush" is-link @click="navigateToTheme" />
<wd-cell title="用户协议" icon="user" is-link @click="navigateToUserAgreement" />
<wd-cell title="隐私政策" icon="folder" is-link @click="navigateToPrivacy" />
</wd-cell-group>
@@ -33,6 +34,8 @@
<script lang="ts" setup>
import { useUserStore } from "@/store/modules/user";
import { checkLogin } from "@/utils/auth";
const userStore = useUserStore();
const isLogin = computed(() => !!userStore.userInfo);
@@ -50,6 +53,10 @@ const navigateToUserAgreement = () => {
const navigateToPrivacy = () => {
uni.navigateTo({ url: "/pages/mine/settings/privacy/index" });
};
// 主题设置
const navigateToTheme = () => {
uni.navigateTo({ url: "/pages/mine/settings/theme/index" });
};
// 网络测试
const navigateToNetworkTest = () => {
uni.navigateTo({ url: "/pages/mine/settings/network/index" });
@@ -145,7 +152,10 @@ const handleLogout = () => {
});
};
// 检查登录状态
onLoad(() => {
if (!checkLogin()) return;
getCacheSize();
});
</script>

View File

@@ -0,0 +1,288 @@
<template>
<view class="theme-settings-container">
<view class="title">主题色设置</view>
<view class="color-palette">
<view class="subtitle">选择主题色</view>
<view class="color-grid">
<view
v-for="(color, index) in themeColors"
:key="index"
class="color-item"
:style="{ backgroundColor: color }"
@click="handleSelectColor(color)"
>
<wd-icon v-if="themeStore.primaryColor === color" name="check" color="#fff" size="16" />
</view>
</view>
</view>
<view class="custom-color">
<view class="subtitle">自定义主题色</view>
<view class="color-picker">
<view class="color-preview" :style="{ backgroundColor: customColor }"></view>
<input
v-model="customColor"
type="text"
placeholder="请输入十六进制颜色值,如 #165DFF"
class="color-input"
maxlength="7"
/>
</view>
<button class="apply-btn" @click="applyCustomColor">应用</button>
</view>
<view class="preview-section">
<view class="subtitle">预览效果</view>
<view class="preview-container">
<view class="preview-item">
<view class="preview-button" :style="{ backgroundColor: themeStore.primaryColor }">
按钮
</view>
</view>
<view class="preview-item">
<view class="preview-text" :style="{ color: themeStore.primaryColor }">文本颜色</view>
</view>
<view class="preview-item">
<view class="preview-border" :style="{ borderColor: themeStore.primaryColor }">边框</view>
</view>
</view>
</view>
<button class="reset-btn" @click="resetTheme">恢复默认主题色</button>
</view>
</template>
<script lang="ts" setup>
import { onLoad } from "@dcloudio/uni-app";
import { useThemeStore } from "@/store";
const themeStore = useThemeStore();
// 预设主题色
const themeColors = [
"#165DFF", // Arco蓝色
"#0FC6C2", // 青绿色
"#722ED1", // 紫色
"#F5222D", // 红色
"#FA8C16", // 橙色
"#FADB14", // 黄色
"#52C41A", // 绿色
"#EB2F96", // 粉色
];
// 自定义颜色
const customColor = ref("#165DFF");
// 选择预设颜色
const handleSelectColor = (color: string) => {
themeStore.setPrimaryColor(color);
customColor.value = color;
// 保存设置
saveThemeSettings();
// 提示
uni.showToast({
title: "主题色已更新",
icon: "success",
});
};
// 应用自定义颜色
const applyCustomColor = () => {
// 验证颜色格式
const colorRegex = /^#([0-9A-F]{6})$/i;
if (!colorRegex.test(customColor.value)) {
uni.showToast({
title: "请输入有效的颜色值",
icon: "none",
});
return;
}
themeStore.setPrimaryColor(customColor.value);
// 保存设置
saveThemeSettings();
// 提示
uni.showToast({
title: "自定义主题色已应用",
icon: "success",
});
};
// 重置为默认主题色
const resetTheme = () => {
const defaultColor = "#165DFF"; // Arco蓝色
themeStore.setPrimaryColor(defaultColor);
customColor.value = defaultColor;
// 保存设置
saveThemeSettings();
// 提示
uni.showToast({
title: "已恢复默认主题色",
icon: "success",
});
};
// 保存主题设置
const saveThemeSettings = () => {
// 主题store已经处理了持久化这里不需要额外操作
};
onLoad(() => {
// 初始化自定义颜色输入框
customColor.value = themeStore.primaryColor;
});
</script>
<style lang="scss" scoped>
.theme-settings-container {
padding: 30rpx;
.title {
margin-bottom: 30rpx;
font-size: 36rpx;
font-weight: bold;
color: #333;
}
.subtitle {
margin-bottom: 20rpx;
font-size: 30rpx;
font-weight: 500;
color: #333;
}
.color-palette {
padding: 30rpx;
margin-bottom: 40rpx;
background-color: #fff;
border-radius: 12rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
.color-grid {
display: flex;
flex-wrap: wrap;
gap: 20rpx;
.color-item {
display: flex;
align-items: center;
justify-content: center;
width: 80rpx;
height: 80rpx;
border-radius: 12rpx;
box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.1);
transition: transform 0.2s;
&:active {
transform: scale(0.95);
}
}
}
}
.custom-color {
padding: 30rpx;
margin-bottom: 40rpx;
background-color: #fff;
border-radius: 12rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
.color-picker {
display: flex;
align-items: center;
margin-bottom: 30rpx;
.color-preview {
width: 60rpx;
height: 60rpx;
margin-right: 20rpx;
border-radius: 8rpx;
box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.1);
}
.color-input {
flex: 1;
height: 80rpx;
padding: 0 20rpx;
font-size: 28rpx;
border: 1px solid #e0e0e0;
border-radius: 8rpx;
}
}
.apply-btn {
width: 100%;
height: 80rpx;
font-size: 28rpx;
line-height: 80rpx;
color: #fff;
background-color: var(--primary-color);
border: none;
border-radius: 8rpx;
}
}
.preview-section {
padding: 30rpx;
margin-bottom: 40rpx;
background-color: #fff;
border-radius: 12rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
.preview-container {
display: flex;
flex-direction: column;
gap: 20rpx;
.preview-item {
display: flex;
align-items: center;
justify-content: center;
.preview-button {
width: 200rpx;
height: 80rpx;
font-size: 28rpx;
line-height: 80rpx;
color: #fff;
text-align: center;
border-radius: 8rpx;
}
.preview-text {
font-size: 32rpx;
font-weight: bold;
}
.preview-border {
width: 200rpx;
height: 80rpx;
line-height: 80rpx;
text-align: center;
border: 2px solid;
border-radius: 8rpx;
}
}
}
}
.reset-btn {
width: 100%;
height: 80rpx;
margin-bottom: 30rpx;
font-size: 28rpx;
line-height: 80rpx;
color: #666;
background-color: #f5f5f5;
border: none;
border-radius: 8rpx;
}
}
</style>

273
src/pages/todo/index.vue Normal file
View File

@@ -0,0 +1,273 @@
<template>
<view class="todo-container">
<view class="page-header">
<text class="page-title">待办事项</text>
<wd-button size="small" type="primary" icon="add" @click="showAddTodoPopup = true">
新建待办
</wd-button>
</view>
<view class="filter-tabs">
<wd-tabs v-model="activeTab" sticky>
<wd-tab title="全部" name="all" />
<wd-tab title="未完成" name="active" />
<wd-tab title="已完成" name="completed" />
</wd-tabs>
</view>
<TodoList :todos="filteredTodos" @update="handleUpdateTodo" @delete="handleDeleteTodo" />
<!-- 新增待办弹窗 -->
<wd-popup
v-model="showAddTodoPopup"
position="bottom"
close-on-click-modal
:style="{ height: '65%' }"
>
<view class="popup-header">
<text class="popup-title">新建待办</text>
<wd-icon name="close" @click="showAddTodoPopup = false" />
</view>
<view class="popup-form">
<wd-input
v-model="newTodo.title"
placeholder="请输入待办标题"
:rules="[{ required: true, message: '请输入标题' }]"
/>
<wd-textarea
v-model="newTodo.description"
placeholder="请输入详细描述"
rows="3"
autosize
class="mt-20"
/>
<wd-cell title="截止日期" is-link @click="showDatePicker = true">
<text v-if="newTodo.deadline">{{ formatDate(newTodo.deadline) }}</text>
<text v-else class="text-placeholder">请选择</text>
</wd-cell>
<wd-cell title="优先级">
<wd-radio-group v-model="newTodo.priority" shape="button">
<wd-radio value="low"></wd-radio>
<wd-radio value="medium"></wd-radio>
<wd-radio value="high"></wd-radio>
</wd-radio-group>
</wd-cell>
<view class="form-actions">
<wd-button
block
type="primary"
:loading="loading"
:disabled="!newTodo.title"
@click="handleAddTodo"
>
保存
</wd-button>
</view>
</view>
</wd-popup>
<!-- 日期选择器 -->
<wd-datetime-picker
v-model="showDatePicker"
v-model:value="newTodo.deadline"
label="截止日期"
type="date"
confirm-button-text="确认"
cancel-button-text="取消"
title="选择截止日期"
/>
<wd-toast />
</view>
</template>
<script lang="ts" setup>
import { ref, computed } from "vue";
import { useToast } from "wot-design-uni";
import { Todo } from "@/types/todo";
import TodoList from "@/components/todo/TodoList.vue";
const toast = useToast();
const loading = ref(false);
const showAddTodoPopup = ref(false);
const showDatePicker = ref(false);
const activeTab = ref("all");
// 待办列表
const todos = ref<Todo[]>([
{
id: "1",
title: "完成个人资料填写",
description: "包括上传头像、填写基本信息等",
completed: false,
priority: "high",
createdAt: new Date().toISOString(),
},
{
id: "2",
title: "阅读使用指南",
description: "熟悉系统功能和操作流程",
completed: true,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
]);
// 新增待办表单
const newTodo = ref<Partial<Todo>>({
title: "",
description: "",
priority: "medium",
deadline: "",
});
// 根据标签筛选待办
const filteredTodos = computed(() => {
switch (activeTab.value) {
case "active":
return todos.value.filter((todo) => !todo.completed);
case "completed":
return todos.value.filter((todo) => todo.completed);
default:
return todos.value;
}
});
// 格式化日期
const formatDate = (date: string) => {
const d = new Date(date);
const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, "0");
const day = String(d.getDate()).padStart(2, "0");
return `${year}-${month}-${day}`;
};
// 生成唯一ID
const generateId = () => {
return Date.now().toString(36) + Math.random().toString(36).substring(2);
};
// 添加待办
const handleAddTodo = () => {
if (!newTodo.value.title) {
toast.error("请输入待办标题");
return;
}
loading.value = true;
// 模拟API调用
setTimeout(() => {
const now = new Date().toISOString();
const todo: Todo = {
id: generateId(),
title: newTodo.value.title!,
description: newTodo.value.description,
completed: false,
deadline: newTodo.value.deadline,
priority: newTodo.value.priority as "low" | "medium" | "high",
createdAt: now,
};
todos.value.unshift(todo);
resetForm();
loading.value = false;
showAddTodoPopup.value = false;
toast.success("待办创建成功");
}, 500);
};
// 更新待办
const handleUpdateTodo = (todo: Todo) => {
const index = todos.value.findIndex((item) => item.id === todo.id);
if (index !== -1) {
todos.value[index] = {
...todo,
updatedAt: new Date().toISOString(),
};
toast.success(todo.completed ? "已完成" : "已取消完成");
}
};
// 删除待办
const handleDeleteTodo = (id: string) => {
uni.showModal({
title: "提示",
content: "确认删除该待办事项?",
success: (res) => {
if (res.confirm) {
todos.value = todos.value.filter((todo) => todo.id !== id);
toast.success("删除成功");
}
},
});
};
// 重置表单
const resetForm = () => {
newTodo.value = {
title: "",
description: "",
priority: "medium",
deadline: "",
};
};
</script>
<style lang="scss" scoped>
.todo-container {
min-height: 100vh;
padding: 30rpx;
background-color: #f5f7fa;
.page-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 30rpx;
.page-title {
font-size: 36rpx;
font-weight: 600;
color: #333;
}
}
.filter-tabs {
margin-bottom: 20rpx;
overflow: hidden;
background-color: #fff;
border-radius: 12rpx;
}
.popup-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 30rpx;
border-bottom: 1rpx solid #eee;
.popup-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
}
}
.popup-form {
padding: 30rpx;
.form-actions {
margin-top: 40rpx;
}
}
.text-placeholder {
color: #999;
}
.mt-20 {
margin-top: 20rpx;
}
}
</style>

View File

@@ -0,0 +1,31 @@
<svg width="100%" height="100%" viewBox="0 0 1000 1000" xmlns="http://www.w3.org/2000/svg">
<!-- 背景渐变 -->
<defs>
<linearGradient id="bgGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#94BFFF" />
<stop offset="100%" stop-color="#165DFF" />
</linearGradient>
<!-- 图形渐变 -->
<linearGradient id="shapeGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#ffffff" stop-opacity="0.3" />
<stop offset="100%" stop-color="#ffffff" stop-opacity="0.15" />
</linearGradient>
</defs>
<!-- 透明背景,不完全填充 -->
<rect width="100%" height="50%" fill="url(#bgGradient)" />
<!-- 左侧方块装饰 -->
<rect x="100" y="150" width="120" height="120" rx="15" fill="url(#shapeGradient)" transform="rotate(-10, 160, 210)" opacity="0.7" />
<rect x="190" y="90" width="80" height="80" rx="10" fill="url(#shapeGradient)" transform="rotate(15, 230, 130)" opacity="0.6" />
<rect x="60" y="250" width="100" height="100" rx="10" fill="url(#shapeGradient)" transform="rotate(-5, 110, 300)" opacity="0.5" />
<!-- 右侧圆形装饰 -->
<circle cx="750" cy="150" r="60" fill="url(#shapeGradient)" opacity="0.7" />
<circle cx="820" cy="230" r="90" fill="url(#shapeGradient)" opacity="0.5" />
<circle cx="690" cy="250" r="40" fill="url(#shapeGradient)" opacity="0.6" />
<!-- 底部波浪 -->
<path d="M0,900 C200,800 350,950 550,870 C750,790 850,900 1000,850 L1000,1000 L0,1000 Z" fill="#ffffff" />
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

View File

@@ -9,4 +9,6 @@ export function setupStore(app: App<Element>) {
}
export * from "./modules/user";
export * from "./modules/dict";
export * from "./modules/theme";
export { store };

View File

@@ -0,0 +1,82 @@
import { defineStore } from "pinia";
import { ref } from "vue";
// 从缓存获取主题色
const getThemeColor = (): string => {
const savedColor = uni.getStorageSync("themeColor");
return savedColor || "#165DFF"; // 默认Arco蓝色
};
// 保存主题色到缓存
const setThemeColorCache = (color: string) => {
uni.setStorageSync("themeColor", color);
};
export const useThemeStore = defineStore("theme", () => {
// 主题色
const primaryColor = ref<string>(getThemeColor());
// 设置主题色
const setPrimaryColor = (color: string) => {
primaryColor.value = color;
setThemeColorCache(color);
// 设置CSS变量方便全局使用
document.documentElement.style.setProperty("--primary-color", color);
// 计算衍生色
const lighterColor = getLighterColor(color, 0.8);
const darkerColor = getDarkerColor(color, 0.8);
document.documentElement.style.setProperty("--primary-color-light", lighterColor);
document.documentElement.style.setProperty("--primary-color-dark", darkerColor);
};
// 获取浅色版本主题色
const getLighterColor = (hexColor: string, factor: number): string => {
// 去掉#前缀
const hex = hexColor.replace("#", "");
// 解析RGB值
let r = parseInt(hex.substring(0, 2), 16);
let g = parseInt(hex.substring(2, 4), 16);
let b = parseInt(hex.substring(4, 6), 16);
// 调亮颜色
r = Math.min(255, Math.floor(r + (255 - r) * factor));
g = Math.min(255, Math.floor(g + (255 - g) * factor));
b = Math.min(255, Math.floor(b + (255 - b) * factor));
// 转回16进制
return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
};
// 获取深色版本主题色
const getDarkerColor = (hexColor: string, factor: number): string => {
// 去掉#前缀
const hex = hexColor.replace("#", "");
// 解析RGB值
let r = parseInt(hex.substring(0, 2), 16);
let g = parseInt(hex.substring(2, 4), 16);
let b = parseInt(hex.substring(4, 6), 16);
// 调暗颜色
r = Math.max(0, Math.floor(r * factor));
g = Math.max(0, Math.floor(g * factor));
b = Math.max(0, Math.floor(b * factor));
// 转回16进制
return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
};
// 初始化,应用主题色
const initTheme = () => {
setPrimaryColor(primaryColor.value);
};
return {
primaryColor,
setPrimaryColor,
initTheme,
};
});

View File

@@ -64,11 +64,19 @@ export const useUserStore = defineStore("user", () => {
}
};
// 判断用户信息是否完整
const isUserInfoComplete = (): boolean => {
if (!userInfo.value) return false;
return !!(userInfo.value.nickname && userInfo.value.avatar);
};
return {
userInfo,
login,
loginByWechat,
logout,
getInfo,
isUserInfoComplete,
};
});

244
src/styles/global.scss Normal file
View File

@@ -0,0 +1,244 @@
/**
* 全局样式变量
*/
/*
* 主题颜色 - 会被theme.ts中的动态设置覆盖
* 这里作为默认值和IDE提示
*/
:root {
/* 主色 */
--primary-color: #165dff;
--primary-color-light: #94bfff;
--primary-color-dark: #0e3c9b;
/* 功能色 */
--success-color: #0fc6c2;
--warning-color: #ff7d00;
--danger-color: #f5222d;
--info-color: #86909c;
/* 文字颜色 */
--text-primary: #1d2129;
--text-regular: #4e5969;
--text-secondary: #86909c;
--text-placeholder: #c9cdd4;
--text-inverse: #ffffff;
/* 边框颜色 */
--border-color: #e5e6eb;
--border-light: #f2f3f5;
/* 背景颜色 */
--bg-white: #ffffff;
--bg-light: #f2f3f5;
--bg-gray: #f7f8fa;
}
/**
* 主题相关的通用类
*/
/* 主色文本 */
.text-primary {
color: var(--primary-color) !important;
}
/* 主色背景 */
.bg-primary {
color: #fff;
background-color: var(--primary-color) !important;
}
/* 主色边框 */
.border-primary {
border-color: var(--primary-color) !important;
}
/* 主色按钮样式 */
.btn-primary {
color: #fff;
background-color: var(--primary-color);
border: none;
border-radius: 8rpx;
transition: opacity 0.3s;
&:active {
opacity: 0.8;
}
}
/* 圆角按钮 */
.btn-rounded {
border-radius: 45rpx !important;
}
/* 次级按钮 */
.btn-secondary {
color: var(--primary-color);
background-color: #fff;
border: 1px solid var(--primary-color);
border-radius: 8rpx;
transition: background-color 0.3s;
&:active {
background-color: rgba(22, 93, 255, 0.05);
}
}
/**
* 字体大小
*/
.font-xs {
font-size: 24rpx;
}
.font-sm {
font-size: 28rpx;
}
.font-md {
font-size: 32rpx;
}
.font-lg {
font-size: 36rpx;
}
.font-xl {
font-size: 40rpx;
}
/**
* 边距辅助类
*/
.mt-10 {
margin-top: 10rpx;
}
.mt-20 {
margin-top: 20rpx;
}
.mt-30 {
margin-top: 30rpx;
}
.mt-40 {
margin-top: 40rpx;
}
.mb-10 {
margin-bottom: 10rpx;
}
.mb-20 {
margin-bottom: 20rpx;
}
.mb-30 {
margin-bottom: 30rpx;
}
.mb-40 {
margin-bottom: 40rpx;
}
.ml-10 {
margin-left: 10rpx;
}
.ml-20 {
margin-left: 20rpx;
}
.ml-30 {
margin-left: 30rpx;
}
.ml-40 {
margin-left: 40rpx;
}
.mr-10 {
margin-right: 10rpx;
}
.mr-20 {
margin-right: 20rpx;
}
.mr-30 {
margin-right: 30rpx;
}
.mr-40 {
margin-right: 40rpx;
}
.p-10 {
padding: 10rpx;
}
.p-20 {
padding: 20rpx;
}
.p-30 {
padding: 30rpx;
}
.p-40 {
padding: 40rpx;
}
/**
* 布局辅助类
*/
.flex {
display: flex;
}
.flex-wrap {
flex-wrap: wrap;
}
.flex-column {
flex-direction: column;
}
.items-center {
align-items: center;
}
.items-start {
align-items: flex-start;
}
.items-end {
align-items: flex-end;
}
.justify-center {
justify-content: center;
}
.justify-between {
justify-content: space-between;
}
.justify-around {
justify-content: space-around;
}
.justify-start {
justify-content: flex-start;
}
.justify-end {
justify-content: flex-end;
}
/**
* 其他辅助类
*/
.text-center {
text-align: center;
}
.text-left {
text-align: left;
}
.text-right {
text-align: right;
}
.rounded-sm {
border-radius: 4rpx;
}
.rounded {
border-radius: 8rpx;
}
.rounded-lg {
border-radius: 16rpx;
}
.rounded-xl {
border-radius: 24rpx;
}
.rounded-full {
border-radius: 9999rpx;
}

16
src/types/todo.ts Normal file
View File

@@ -0,0 +1,16 @@
export interface Todo {
id: string;
title: string;
description?: string;
completed: boolean;
deadline?: string;
priority?: "low" | "medium" | "high";
createdAt: string;
updatedAt?: string;
}
export interface TodoState {
todos: Todo[];
loading: boolean;
error: string | null;
}

22
src/utils/auth.ts Normal file
View File

@@ -0,0 +1,22 @@
import { useUserStore } from "@/store/modules/user";
/**
* 检查用户登录状态,未登录则跳转到登录页面
* @returns 返回用户是否已登录
*/
export function checkLogin(): boolean {
const userStore = useUserStore();
if (!userStore.userInfo) {
const pages = getCurrentPages();
const currentPage = pages[pages.length - 1];
const currentPagePath = `/${currentPage.route}`;
uni.navigateTo({
url: `/pages/login/index?redirect=${encodeURIComponent(currentPagePath)}`,
});
return false;
}
return true;
}