refactor: ♻️ 我的和登录页面优化,添加主题设置
This commit is contained in:
18
src/App.vue
18
src/App.vue
@@ -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;
|
||||
}
|
||||
|
||||
122
src/components/todo/TodoItem.vue
Normal file
122
src/components/todo/TodoItem.vue
Normal 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>
|
||||
43
src/components/todo/TodoList.vue
Normal file
43
src/components/todo/TodoList.vue
Normal 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>
|
||||
@@ -3,6 +3,7 @@ import App from "./App.vue";
|
||||
import setupPlugins from "@/plugins";
|
||||
|
||||
import "uno.css";
|
||||
import "@/styles/global.scss";
|
||||
|
||||
import { setupStore } from "@/store";
|
||||
|
||||
|
||||
@@ -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": {
|
||||
|
||||
487
src/pages/login/complete-profile.vue
Normal file
487
src/pages/login/complete-profile.vue
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
288
src/pages/mine/settings/theme/index.vue
Normal file
288
src/pages/mine/settings/theme/index.vue
Normal 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
273
src/pages/todo/index.vue
Normal 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>
|
||||
31
src/static/images/auth/login-bg.svg
Normal file
31
src/static/images/auth/login-bg.svg
Normal 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 |
|
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 6.9 KiB |
@@ -9,4 +9,6 @@ export function setupStore(app: App<Element>) {
|
||||
}
|
||||
|
||||
export * from "./modules/user";
|
||||
export * from "./modules/dict";
|
||||
export * from "./modules/theme";
|
||||
export { store };
|
||||
|
||||
82
src/store/modules/theme.ts
Normal file
82
src/store/modules/theme.ts
Normal 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,
|
||||
};
|
||||
});
|
||||
@@ -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
244
src/styles/global.scss
Normal 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
16
src/types/todo.ts
Normal 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
22
src/utils/auth.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user