Merge branch 'base' into tabbar

This commit is contained in:
feige996 2025-06-17 22:39:11 +08:00
commit c1b12eab84
9 changed files with 11 additions and 1726 deletions

View File

@ -1,7 +1,7 @@
{ {
"name": "unibest", "name": "unibest",
"type": "commonjs", "type": "commonjs",
"version": "2.12.2", "version": "2.13.0",
"description": "unibest - 最好的 uniapp 开发模板", "description": "unibest - 最好的 uniapp 开发模板",
"update-time": "2025-06-17", "update-time": "2025-06-17",
"author": { "author": {
@ -11,14 +11,15 @@
"github": "https://github.com/feige996", "github": "https://github.com/feige996",
"gitee": "https://gitee.com/feige996" "gitee": "https://gitee.com/feige996"
}, },
"homepage": "https://unibest.tech",
"license": "MIT", "license": "MIT",
"repository": "https://github.com/feige996/unibest", "repository": "https://github.com/feige996/unibest",
"repository-gitee": "https://gitee.com/feige996/unibest", "repository-gitee": "https://gitee.com/feige996/unibest",
"repository-deprecated": "https://github.com/codercup/unibest", "repository-old": "https://github.com/codercup/unibest",
"bugs": { "bugs": {
"url": "https://github.com/feige996/unibest/issues" "url": "https://github.com/feige996/unibest/issues",
"url-old": "https://github.com/codercup/unibest/issues"
}, },
"homepage": "https://feige996.github.io/unibest/",
"engines": { "engines": {
"node": ">=18", "node": ">=18",
"pnpm": ">=7.30" "pnpm": ">=7.30"

View File

@ -48,12 +48,12 @@ export default defineUniPages({
// icon: '/static/logo.svg', // icon: '/static/logo.svg',
// iconType: 'local', // iconType: 'local',
// }, // },
{ // {
pagePath: 'pages/mine/index', // pagePath: 'pages/mine/index',
text: '我的', // text: '我的',
icon: 'iconfont icon-my', // icon: 'iconfont icon-my',
iconType: 'iconfont', // iconType: 'iconfont',
}, // },
], ],
}, },
}) })

View File

@ -1,47 +0,0 @@
@import 'wot-design-uni/components/wd-button/index.scss';
:deep(.wd-privacy-popup) {
width: 600rpx;
padding: 0 24rpx;
box-sizing: border-box;
border-radius: 32rpx;
overflow: hidden;
}
.wd-privacy-popup {
&__header {
width: 100%;
height: 128rpx;
line-height: 128rpx;
color: rgba(0, 0, 0, 0.85);
font-size: 30rpx;
padding: 0 12rpx;
box-sizing: border-box;
}
&__container {
width: 100%;
box-sizing: border-box;
padding: 0 12rpx;
margin-bottom: 32rpx;
font-size: 28rpx;
line-height: 1.8;
color: #3e3e3e;
text-align: left;
font-weight: 550;
&-protocol {
color: #4d80f0;
}
}
&__footer {
display: flex;
justify-content: space-between;
padding-bottom: 36rpx;
button {
border: none;
outline: none;
}
}
}

View File

@ -1,144 +0,0 @@
<template>
<view>
<wd-popup
v-model="showPopup"
:close-on-click-modal="false"
custom-class="wd-privacy-popup"
@close="handleClose"
>
<view class="wd-privacy-popup__header">
<!--标题-->
<view class="wd-picker__title">{{ title }}</view>
</view>
<view class="wd-privacy-popup__container">
<text>{{ desc }}</text>
<text class="wd-privacy-popup__container-protocol" @click="openPrivacyContract">
{{ protocol }}
</text>
<text>{{ subDesc }}</text>
</view>
<view class="wd-privacy-popup__footer">
<button
class="wd-privacy-popup__footer-disagree wd-button is-block is-round is-medium is-plain"
id="disagree-btn"
@click="handleDisagree"
>
拒绝
</button>
<button
class="wd-privacy-popup__footer-agree wd-button is-primary is-block is-round is-medium"
id="agree-btn"
open-type="agreePrivacyAuthorization"
@agreeprivacyauthorization="handleAgree"
>
同意
</button>
</view>
</wd-popup>
</view>
</template>
<script lang="ts">
export default {
name: 'privacy-popup',
options: {
virtualHost: true,
addGlobalClass: true,
styleIsolation: 'shared',
},
}
</script>
<script lang="ts" setup>
import { onBeforeMount, ref } from 'vue'
type Props = {
title?: string //
desc?: string //
subDesc?: string //
protocol?: string //
}
const props = withDefaults(defineProps<Props>(), {
title: '用户隐私保护提示',
desc: '感谢您使用本应用,您使用本应用的服务之前请仔细阅读并同意',
subDesc:
'。当您点击同意并开始时用产品服务时,即表示你已理解并同息该条款内容,该条款将对您产生法律约束力。如您拒绝,将无法使用相应服务。',
protocol: '《用户隐私保护指引》',
})
const showPopup = ref<boolean>(false) // popup
const privacyResolves = ref(new Set()) // onNeedPrivacyAuthorizationreslove
const privacyHandler = (resolve: any) => {
showPopup.value = true
privacyResolves.value.add(resolve)
}
const emit = defineEmits(['agree', 'disagree'])
onBeforeMount(() => {
//
if ((wx as any).onNeedPrivacyAuthorization) {
;(wx as any).onNeedPrivacyAuthorization((resolve: any) => {
if (typeof privacyHandler === 'function') {
privacyHandler(resolve)
}
})
}
})
/**
* 同意隐私协议
*/
function handleAgree() {
showPopup.value = false
privacyResolves.value.forEach((resolve: any) => {
resolve({
event: 'agree',
buttonId: 'agree-btn',
})
})
privacyResolves.value.clear()
emit('agree')
}
/**
* 拒绝隐私协议
*/
function handleDisagree() {
showPopup.value = false
privacyResolves.value.forEach((resolve: any) => {
resolve({
event: 'disagree',
})
})
privacyResolves.value.clear()
}
/**
* 打开隐私协议
*/
function openPrivacyContract() {
;(wx as any).openPrivacyContract({
success: (res) => {
console.log('openPrivacyContract success')
},
fail: (res) => {
console.error('openPrivacyContract fail', res)
},
})
}
/**
* 弹出框关闭时清空
*/
function handleClose() {
privacyResolves.value.clear()
}
</script>
<style lang="scss" scoped>
@import './index.scss';
</style>

View File

@ -1,584 +0,0 @@
<route lang="json5" type="page">
{
style: {
navigationBarTitleText: '登录',
navigationStyle: 'custom',
},
}
</route>
<template>
<view class="login-container">
<!-- 背景装饰元素 -->
<view class="bg-decoration bg-circle-1"></view>
<view class="bg-decoration bg-circle-2"></view>
<view class="bg-decoration bg-circle-3"></view>
<view class="login-header">
<image class="login-logo" :src="appLogo" mode="aspectFit"></image>
<view class="login-title">{{ appTitle }}</view>
</view>
<view class="login-form">
<view class="welcome-text">欢迎登录</view>
<view class="login-desc">请输入您的账号和密码</view>
<view class="login-input-group">
<view class="input-wrapper">
<wd-input
v-model="loginForm.username"
prefix-icon="user"
placeholder="请输入用户名"
clearable
class="login-input"
:border="false"
required
></wd-input>
<view class="input-bottom-line"></view>
</view>
<view class="input-wrapper">
<wd-input
v-model="loginForm.password"
prefix-icon="lock-on"
placeholder="请输入密码"
clearable
show-password
class="login-input"
:border="false"
required
></wd-input>
<view class="input-bottom-line"></view>
</view>
<!-- 验证码区域 -->
<view class="input-wrapper captcha-wrapper">
<wd-input
v-if="captcha.captchaEnabled"
v-model="loginForm.code"
prefix-icon="secured"
placeholder="请输入验证码"
clearable
class="login-input captcha-input"
:border="false"
required
>
<template #suffix>
<image
class="captcha-image"
:src="'data:image/gif;base64,' + captcha.image"
mode="aspectFit"
@click="refreshCaptcha"
></image>
</template>
</wd-input>
<view class="input-bottom-line"></view>
</view>
</view>
<!-- 登录按钮组 -->
<view class="login-buttons">
<!-- 账号密码登录按钮 -->
<wd-button
type="primary"
size="large"
block
@click="handleAccountLogin"
class="account-login-btn"
>
<wd-icon name="right" size="18px" class="login-icon"></wd-icon>
登录
</wd-button>
<!-- 微信小程序一键登录按钮 -->
<!-- #ifdef MP-WEIXIN -->
<view class="divider">
<view class="divider-line"></view>
<view class="divider-text"></view>
<view class="divider-line"></view>
</view>
<wd-button
type="info"
size="large"
block
plain
@click="handleWechatLogin"
class="wechat-login-btn"
>
微信一键登录
</wd-button>
<!-- #endif -->
</view>
</view>
<!-- 隐私协议勾选 -->
<view class="privacy-agreement">
<wd-checkbox
v-model="agreePrivacy"
shape="square"
class="privacy-checkbox"
active-color="var(--wot-color-theme, #1989fa)"
>
<view class="agreement-text">
我已阅读并同意
<text class="agreement-link" @click.stop="handleAgreement('user')">用户协议</text>
<text class="agreement-link" @click.stop="handleAgreement('privacy')">隐私政策</text>
</view>
</wd-checkbox>
</view>
<view class="login-footer"></view>
</view>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useUserStore } from '@/store/user'
import { isMpWeixin } from '@/utils/platform'
import { getCode, ILoginForm } from '@/api/login'
import { toast } from '@/utils/toast'
import { isTableBar } from '@/utils/index'
import { ICaptcha } from '@/api/login.typings'
const redirectRoute = ref('')
//
const appTitle = ref(import.meta.env.VITE_APP_TITLE || 'Unibest Login')
const appLogo = ref(import.meta.env.VITE_APP_LOGO || '/static/logo.svg')
// store
const userStore = useUserStore()
//
//
const captcha = ref<ICaptcha>({
captchaEnabled: false,
uuid: '',
image: '',
})
//
const loginForm = ref<ILoginForm>({
username: 'admin',
password: '123456',
code: '',
uuid: '',
})
//
const agreePrivacy = ref(true)
//
onLoad((option) => {
//
captcha.value.captchaEnabled && refreshCaptcha()
//
if (option.redirect) {
redirectRoute.value = option.redirect
}
})
//
const handleAccountLogin = async () => {
if (!agreePrivacy.value) {
toast.error('请阅读同意协议')
return
}
//
if (!loginForm.value.username) {
toast.error('请输入用户名')
return
}
if (!loginForm.value.password) {
toast.error('请输入密码')
return
}
if (captcha.value.captchaEnabled && !loginForm.value.code) {
toast.error('请输入验证码')
return
}
//
await userStore.login(loginForm.value)
//
const targetUrl = redirectRoute.value || '/pages/index/index'
if (isTableBar(targetUrl)) {
uni.switchTab({ url: targetUrl })
} else {
uni.redirectTo({ url: targetUrl })
}
}
//
const handleWechatLogin = async () => {
if (!isMpWeixin) {
toast.info('请在微信小程序中使用此功能')
return
}
//
if (!agreePrivacy.value) {
toast.error('请先阅读并同意用户协议和隐私政策')
return
}
//
await userStore.wxLogin()
//
const targetUrl = redirectRoute.value || '/pages/index/index'
if (isTableBar(targetUrl)) {
uni.switchTab({ url: targetUrl })
} else {
uni.redirectTo({ url: targetUrl })
}
}
//
const refreshCaptcha = () => {
//
getCode().then((res) => {
const { data } = res
loginForm.value.uuid = data.uuid
captcha.value = data
})
}
//
const handleAgreement = (type: 'user' | 'privacy') => {
const title = type === 'user' ? '用户协议' : '隐私政策'
// showToast(`${title}`)
//
// uni.navigateTo({
// url: `/pages/agreement/${type}`
// })
}
</script>
<style lang="scss" scoped>
/* 验证码输入框样式 */
.captcha-wrapper {
.captcha-input {
:deep(.wd-input__suffix) {
margin-right: 0;
padding-right: 0;
}
}
.captcha-image {
width: 100px;
height: 36px;
margin-left: 10px;
border-radius: 8px;
cursor: pointer;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
position: relative;
overflow: hidden;
&::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(to bottom, rgba(255, 255, 255, 0.1), transparent);
pointer-events: none;
}
&:active {
opacity: 0.8;
transform: scale(0.96);
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
}
}
}
.login-container {
box-sizing: border-box;
display: flex;
flex-direction: column;
min-height: 100vh;
padding: 0 70rpx;
background-color: #ffffff;
background-image: linear-gradient(
135deg,
rgba(25, 137, 250, 0.05) 0%,
rgba(255, 255, 255, 0) 100%
);
position: relative;
overflow: hidden;
}
/* 背景装饰元素 */
.bg-decoration {
position: absolute;
border-radius: 50%;
background: linear-gradient(135deg, rgba(25, 137, 250, 0.05), rgba(25, 137, 250, 0.1));
z-index: 0;
pointer-events: none;
}
.bg-circle-1 {
width: 500rpx;
height: 500rpx;
top: -200rpx;
right: -200rpx;
opacity: 0.6;
}
.bg-circle-2 {
width: 400rpx;
height: 400rpx;
bottom: 10%;
left: -200rpx;
opacity: 0.4;
}
.bg-circle-3 {
width: 300rpx;
height: 300rpx;
bottom: -100rpx;
right: 10%;
opacity: 0.3;
background: linear-gradient(135deg, rgba(7, 193, 96, 0.05), rgba(7, 193, 96, 0.1));
}
.login-header {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin-top: 120rpx;
animation: fadeInDown 0.8s ease-out;
.login-logo {
width: 200rpx;
height: 200rpx;
border-radius: 36rpx;
box-shadow: 0 10rpx 30rpx rgba(0, 0, 0, 0.12);
transition: all 0.3s ease;
&:active {
transform: scale(0.95);
box-shadow: 0 6rpx 15rpx rgba(0, 0, 0, 0.1);
}
}
.login-title {
margin-top: 30rpx;
font-size: 46rpx;
font-weight: bold;
color: #333333;
letter-spacing: 3rpx;
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.05);
}
}
.login-form {
flex: 1;
margin-top: 70rpx;
animation: fadeIn 0.8s ease-out 0.2s both;
.welcome-text {
margin-bottom: 16rpx;
font-size: 48rpx;
font-weight: bold;
color: #333333;
text-align: center;
letter-spacing: 1rpx;
}
.login-desc {
margin-bottom: 70rpx;
font-size: 28rpx;
color: #888888;
text-align: center;
}
.login-input-group {
margin-bottom: 60rpx;
position: relative;
z-index: 1;
.input-wrapper {
position: relative;
margin-bottom: 50rpx;
transition: all 0.3s ease;
border-radius: 16rpx;
overflow: hidden;
&:last-child {
margin-bottom: 0;
}
.login-input {
padding: 12rpx 20rpx;
background-color: rgba(245, 247, 250, 0.7);
border-radius: 16rpx;
transition: all 0.3s ease;
:deep(.wd-input__inner) {
font-size: 30rpx;
color: #333333;
}
:deep(.wd-input__placeholder) {
font-size: 28rpx;
color: #aaaaaa;
}
&:focus-within {
background-color: rgba(245, 247, 250, 0.95);
box-shadow: 0 6rpx 16rpx rgba(0, 0, 0, 0.06);
transform: translateY(-3rpx);
}
}
.input-bottom-line {
position: absolute;
bottom: -2rpx;
left: 5%;
width: 90%;
height: 2rpx;
background: linear-gradient(
to right,
transparent,
var(--wot-color-theme, #1989fa),
transparent
);
transition: transform 0.4s ease;
transform: scaleX(0);
opacity: 0.8;
}
&:focus-within .input-bottom-line {
transform: scaleX(1);
}
.input-icon {
margin-right: 16rpx;
color: #666666;
transition: color 0.3s ease;
}
&:focus-within .input-icon {
color: var(--wot-color-theme, #1989fa);
}
}
}
.login-buttons {
display: flex;
flex-direction: column;
gap: 36rpx;
.account-login-btn {
height: 96rpx;
margin-top: 20rpx;
font-size: 32rpx;
font-weight: 500;
letter-spacing: 2rpx;
border-radius: 48rpx;
box-shadow: 0 10rpx 20rpx rgba(25, 137, 250, 0.25);
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
.login-icon {
margin-right: 8rpx;
opacity: 0.8;
transition: all 0.3s ease;
}
&:active {
box-shadow: 0 5rpx 10rpx rgba(25, 137, 250, 0.2);
transform: scale(0.98);
.login-icon {
transform: translateX(3rpx);
}
}
}
.divider {
display: flex;
align-items: center;
margin: 24rpx 0;
.divider-line {
flex: 1;
height: 1px;
background-color: #eeeeee;
}
.divider-text {
padding: 0 24rpx;
font-size: 24rpx;
color: #999999;
}
}
.wechat-login-btn {
height: 96rpx;
font-size: 32rpx;
color: #07c160;
border-color: #07c160;
border-radius: 48rpx;
transition: all 0.3s ease;
.wechat-icon {
margin-right: 12rpx;
}
&:active {
background-color: rgba(7, 193, 96, 0.08);
transform: scale(0.98);
}
}
}
}
.privacy-agreement {
display: flex;
justify-content: center;
margin: 30rpx 0 40rpx;
animation: fadeIn 0.8s ease-out 0.4s both;
.privacy-checkbox {
display: flex;
align-items: center;
}
.agreement-text {
font-size: 26rpx;
line-height: 1.6;
color: #666666;
.agreement-link {
padding: 0 4rpx;
font-weight: 500;
color: var(--wot-color-theme, #1989fa);
transition: all 0.3s ease;
&:active {
opacity: 0.8;
transform: scale(0.98);
}
}
}
}
.login-footer {
padding: 50rpx 0;
margin-top: auto;
}
/* 添加动画效果 */
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes fadeInDown {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style>

View File

@ -1,173 +0,0 @@
<route lang="json5">
{
style: {
navigationBarTitleText: '关于我们',
},
}
</route>
<template>
<view class="about-container">
<view class="about-card">
<!-- 应用信息 -->
<view class="app-info">
<view class="logo-wrapper">
<wd-img :src="appLogo" width="120px" height="120px" radius="24rpx"></wd-img>
</view>
<view class="app-name">{{ appTitle }}</view>
<view class="app-version">版本 {{ packageJson.version }}</view>
</view>
<!-- 联系方式 -->
<view class="info-section">
<view class="section-title">联系我们</view>
<view class="section-content">
<view class="contact-item">
<wd-icon name="phone" size="20px" class="contact-icon"></wd-icon>
<text class="contact-text">客服电话400-XXX-XXXX</text>
</view>
<view class="contact-item">
<wd-icon name="mail" size="20px" class="contact-icon"></wd-icon>
<text class="contact-text">邮箱support@unibest.tech</text>
</view>
<view class="contact-item">
<wd-icon name="location" size="20px" class="contact-icon"></wd-icon>
<text class="contact-text">地址中国·深圳</text>
</view>
</view>
</view>
<!-- 版权信息 -->
<view class="copyright">
<text>Copyright © 2025-{{ currentYear }} {{ appTitle }}</text>
<text>All Rights Reserved</text>
</view>
</view>
</view>
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue'
import packageJson from '@/../package.json'
const appTitle = ref(import.meta.env.VITE_APP_TITLE || 'unibest')
const appLogo = ref(import.meta.env.VITE_APP_LOGO || '/static/logo.svg')
//
const currentYear = computed(() => new Date().getFullYear())
</script>
<style lang="scss" scoped>
.about-container {
background-color: #f5f7fa;
padding: 30rpx;
}
.about-card {
background-color: #ffffff;
border-radius: 24rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
overflow: hidden;
padding: 40rpx 30rpx;
}
/* 应用信息 */
.app-info {
display: flex;
flex-direction: column;
align-items: center;
padding: 30rpx 0 50rpx;
border-bottom: 2rpx solid #f0f0f0;
}
.logo-wrapper {
margin-bottom: 20rpx;
box-shadow: 0 8rpx 16rpx rgba(0, 0, 0, 0.08);
border-radius: 24rpx;
}
.app-name {
font-size: 40rpx;
font-weight: 600;
color: #333;
margin-bottom: 10rpx;
}
.app-version {
font-size: 28rpx;
color: #999;
}
/* 信息区块 */
.info-section {
padding: 40rpx 0;
border-bottom: 2rpx solid #f0f0f0;
}
.section-title {
font-size: 34rpx;
font-weight: 600;
color: #333;
margin-bottom: 20rpx;
position: relative;
padding-left: 24rpx;
&::before {
content: '';
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 8rpx;
height: 32rpx;
background: linear-gradient(135deg, #4a7bff, #6a5acd);
border-radius: 4rpx;
}
}
.section-content {
padding: 0 10rpx;
}
.content-text {
font-size: 30rpx;
color: #666;
line-height: 1.6;
text-align: justify;
}
/* 联系方式 */
.contact-item {
display: flex;
align-items: center;
margin-bottom: 20rpx;
&:last-child {
margin-bottom: 0;
}
}
.contact-icon {
margin-right: 20rpx;
color: #4a7bff;
}
.contact-text {
font-size: 30rpx;
color: #666;
}
/* 版权信息 */
.copyright {
padding-top: 40rpx;
display: flex;
flex-direction: column;
align-items: center;
text {
font-size: 26rpx;
color: #999;
line-height: 1.6;
}
}
</style>

View File

@ -1,375 +0,0 @@
<route lang="json5">
{
layout: 'tabbar',
style: {
navigationBarTitleText: '我的',
},
}
</route>
<template>
<view class="profile-container">
{{ JSON.stringify(userInfo) }}
<!-- 用户信息区域 -->
<view class="user-info-section">
<!-- #ifdef MP-WEIXIN -->
<button class="avatar-button" open-type="chooseAvatar" @chooseavatar="onChooseAvatar">
<wd-img :src="userInfo.avatar" width="80px" height="80px" radius="50%"></wd-img>
</button>
<!-- #endif -->
<!-- #ifndef MP-WEIXIN -->
<view class="avatar-wrapper" @click="run">
<wd-img :src="userInfo.avatar" width="100%" height="100%" radius="50%"></wd-img>
</view>
<!-- #endif -->
<view class="user-details">
<!-- #ifdef MP-WEIXIN -->
<input
type="nickname"
class="weui-input"
placeholder="请输入昵称"
v-model="userInfo.username"
/>
<!-- #endif -->
<!-- #ifndef MP-WEIXIN -->
<view class="username">{{ userInfo.username }}</view>
<!-- #endif -->
<view class="user-id">ID: {{ userInfo.id }}</view>
</view>
</view>
<!-- 功能区块 -->
<view class="function-section">
<view class="cell-group">
<view class="group-title">账号管理</view>
<wd-cell title="个人资料" is-link @click="handleProfileInfo">
<template #icon>
<wd-icon name="user" size="20px"></wd-icon>
</template>
</wd-cell>
<wd-cell title="账号安全" is-link @click="handlePassword">
<template #icon>
<wd-icon name="lock-on" size="20px"></wd-icon>
</template>
</wd-cell>
</view>
<view class="cell-group">
<view class="group-title">通用设置</view>
<wd-cell title="消息通知" is-link @click="handleInform">
<template #icon>
<wd-icon name="notification" size="20px"></wd-icon>
</template>
</wd-cell>
<wd-cell title="清理缓存" is-link @click="handleClearCache">
<template #icon>
<wd-icon name="clear" size="20px"></wd-icon>
</template>
</wd-cell>
<wd-cell title="应用更新" is-link @click="handleAppUpdate">
<template #icon>
<wd-icon name="refresh1" size="20px"></wd-icon>
</template>
</wd-cell>
<wd-cell title="关于我们" is-link @click="handleAbout">
<template #icon>
<wd-icon name="info-circle" size="20px"></wd-icon>
</template>
</wd-cell>
</view>
<view class="logout-button-wrapper">
<wd-button type="error" v-if="hasLogin" block @click="handleLogout">退出登录</wd-button>
<wd-button type="primary" v-else block @click="handleLogin">登录</wd-button>
</view>
</view>
</view>
</template>
<script lang="ts" setup>
import { useUserStore } from '@/store'
import { useToast } from 'wot-design-uni'
import { useUpload } from '@/utils/uploadFile'
import { storeToRefs } from 'pinia'
import { IUploadSuccessInfo } from '@/api/login.typings'
const userStore = useUserStore()
// 使storeToRefsuserInfo
const { userInfo } = storeToRefs(userStore)
const toast = useToast()
const hasLogin = ref(false)
onShow((options) => {
hasLogin.value = !!uni.getStorageSync('token')
console.log('个人中心onShow', hasLogin.value, options)
hasLogin.value && useUserStore().getUserInfo()
})
// #ifndef MP-WEIXIN
//
const { run } = useUpload<IUploadSuccessInfo>(
import.meta.env.VITE_UPLOAD_BASEURL,
{},
{
onSuccess: (res) => {
console.log('h5头像上传成功', res)
useUserStore().setUserAvatar(res.url)
},
},
)
// #endif
//
const handleLogin = async () => {
// #ifdef MP-WEIXIN
//
await userStore.wxLogin()
hasLogin.value = true
// #endif
// #ifndef MP-WEIXIN
uni.navigateTo({ url: '/pages/login/index' })
// #endif
}
// #ifdef MP-WEIXIN
//
const onChooseAvatar = (e: any) => {
console.log('选择头像', e.detail)
const { avatarUrl } = e.detail
const { run } = useUpload<IUploadSuccessInfo>(
import.meta.env.VITE_UPLOAD_BASEURL,
{},
{
onSuccess: (res) => {
console.log('wx头像上传成功', res)
useUserStore().setUserAvatar(res.url)
},
},
avatarUrl,
)
run()
}
// #endif
// #ifdef MP-WEIXIN
//
const getUserInfo = (e: any) => {
console.log(e.detail)
}
// #endif
//
const handleProfileInfo = () => {
uni.navigateTo({ url: `/pages/mine/info/index` })
}
//
const handlePassword = () => {
uni.navigateTo({ url: `/pages/mine/password/index` })
}
//
const handleInform = () => {
// uni.navigateTo({ url: `/pages/mine/inform/index` })
toast.show('功能开发中')
}
//
const handleAppUpdate = () => {
// #ifdef MP
// #ifndef MP-HARMONY
const updateManager = uni.getUpdateManager()
updateManager.onCheckForUpdate(function (res) {
//
// console.log(res.hasUpdate)
if (res.hasUpdate) {
toast.show('检测到新版本,正在下载中...')
} else {
toast.show('已是最新版本')
}
})
updateManager.onUpdateReady(function (res) {
uni.showModal({
title: '更新提示',
content: '新版本已经准备好,是否重启应用?',
success(res) {
if (res.confirm) {
// applyUpdate
updateManager.applyUpdate()
}
},
})
})
updateManager.onUpdateFailed(function (res) {
//
toast.error('新版本下载失败')
})
// #endif
// #endif
// #ifndef MP
toast.show('功能开发中')
// #endif
}
//
const handleAbout = () => {
uni.navigateTo({ url: `/pages/mine/about/index` })
}
//
const handleClearCache = () => {
uni.showModal({
title: '清除缓存',
content: '确定要清除所有缓存吗?\n清除后需要重新登录',
success: (res) => {
if (res.confirm) {
try {
//
uni.clearStorageSync()
//
useUserStore().logout()
toast.show('清除缓存成功')
} catch (err) {
console.error('清除缓存失败:', err)
toast.error('清除缓存失败')
}
}
},
})
}
// 退
const handleLogout = () => {
uni.showModal({
title: '提示',
content: '确定要退出登录吗?',
success: (res) => {
if (res.confirm) {
//
useUserStore().logout()
hasLogin.value = false
// 退
toast.show('退出登录成功')
// #ifdef MP-WEIXIN
//
// uni.reLaunch({ url: '/pages/index/index' })
// #endif
// #ifndef MP-WEIXIN
//
// uni.reLaunch({ url: '/pages/login/index' })
// #endif
}
},
})
}
</script>
<style lang="scss" scoped>
/* 基础样式 */
.profile-container {
overflow: hidden;
font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', sans-serif;
background-color: #f7f8fa;
}
/* 用户信息区域 */
.user-info-section {
display: flex;
align-items: center;
padding: 40rpx;
margin: 30rpx 30rpx 20rpx;
background-color: #fff;
border-radius: 24rpx;
box-shadow: 0 6rpx 20rpx rgba(0, 0, 0, 0.08);
transition: all 0.3s ease;
}
.avatar-wrapper {
width: 160rpx;
height: 160rpx;
margin-right: 40rpx;
overflow: hidden;
border: 4rpx solid #f5f5f5;
border-radius: 50%;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08);
}
.avatar-button {
height: 160rpx;
padding: 0;
margin-right: 40rpx;
overflow: hidden;
border: 4rpx solid #f5f5f5;
border-radius: 50%;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08);
}
.user-details {
flex: 1;
}
.username {
margin-bottom: 12rpx;
font-size: 38rpx;
font-weight: 600;
color: #333;
letter-spacing: 0.5rpx;
}
.user-id {
font-size: 28rpx;
color: #666;
}
.user-created {
margin-top: 8rpx;
font-size: 24rpx;
color: #999;
}
/* 功能区块 */
.function-section {
padding: 0 20rpx;
margin-top: 20rpx;
}
.cell-group {
margin-bottom: 20rpx;
overflow: hidden;
background-color: #fff;
border-radius: 16rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
}
.group-title {
padding: 24rpx 30rpx 16rpx;
font-size: 30rpx;
font-weight: 500;
color: #999;
background-color: #fafafa;
}
:deep(.wd-cell) {
border-bottom: 1rpx solid #f5f5f5;
&:last-child {
border-bottom: none;
}
.wd-cell__title {
margin-left: 5px;
font-size: 32rpx;
color: #333;
}
.cell-icon {
margin-right: 20rpx;
font-size: 36rpx;
}
}
/* 退出登录按钮 */
.logout-button-wrapper {
padding: 40rpx 30rpx;
}
:deep(.wd-button--danger) {
height: 88rpx;
font-size: 32rpx;
line-height: 88rpx;
color: #fff;
background-color: #f53f3f;
border-radius: 44rpx;
}
</style>

View File

@ -1,190 +0,0 @@
<route lang="json5">
{
style: {
navigationBarTitleText: '个人资料',
},
}
</route>
<template>
<view class="profile-info-container">
<view class="profile-card">
<view class="form-wrapper">
<wd-form ref="formRef" :model="formData" label-width="160rpx" class="profile-form">
<wd-cell-group class="form-group">
<!-- 昵称 -->
<view class="sex-field">
<text class="field-label">昵称</text>
<wd-input
prop="name"
clearable
v-model="formData.name"
placeholder="请输入昵称"
:rules="[{ required: true, message: '请填写昵称' }]"
class="form-input"
/>
</view>
<!-- 性别 -->
<view class="sex-field">
<text class="field-label">性别</text>
<wd-radio-group
v-model="formData.sex"
shape="button"
:rules="[{ required: true, message: '请选择性别' }]"
>
<wd-radio :value="'1'"></wd-radio>
<wd-radio :value="'0'"></wd-radio>
</wd-radio-group>
</view>
</wd-cell-group>
</wd-form>
<!-- 操作按钮 -->
<view class="form-actions">
<wd-button type="primary" size="large" @click="handleSubmit">保存修改</wd-button>
</view>
</view>
</view>
</view>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import { useUserStore } from '@/store'
import { storeToRefs } from 'pinia'
import { toast } from '@/utils/toast'
import { updateInfo } from '@/api/login'
//
const formRef = ref()
//
const userStore = useUserStore()
const { userInfo } = storeToRefs(userStore)
//
const formData = ref({
id: userInfo.value.id,
name: userInfo.value.name,
sex: userInfo.value.sex,
})
//
const handleSubmit = async () => {
//
const valid = await formRef.value.validate()
if (!valid) return
const { message } = await updateInfo(formData.value)
await useUserStore().getUserInfo()
toast.success(message)
}
</script>
<style lang="scss" scoped>
.profile-info-container {
min-height: 100vh;
background-color: #f5f7fa;
padding: 30rpx;
}
.profile-card {
background-color: #ffffff;
border-radius: 24rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
overflow: hidden;
}
.card-header {
padding: 40rpx 30rpx 20rpx;
border-bottom: 2rpx solid #f0f0f0;
}
.card-title {
font-size: 36rpx;
font-weight: 600;
color: #333;
position: relative;
display: inline-block;
padding-bottom: 16rpx;
&::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 60rpx;
height: 6rpx;
background: linear-gradient(90deg, #4a7bff, #6a5acd);
border-radius: 6rpx;
}
}
.form-wrapper {
padding: 30rpx;
}
.form-group {
border-radius: 16rpx;
overflow: hidden;
margin-bottom: 40rpx;
}
.form-input {
font-size: 30rpx;
}
.sex-field {
display: flex;
align-items: center;
padding: 24rpx 30rpx;
background-color: #ffffff;
}
.field-label {
width: 160rpx;
font-size: 30rpx;
color: #333;
}
.radio-group {
flex: 1;
display: flex;
gap: 20rpx;
}
.radio-btn {
flex: 1;
height: 80rpx;
line-height: 80rpx;
text-align: center;
font-size: 30rpx;
border-radius: 12rpx;
background-color: #f5f7fa;
&:active {
opacity: 0.8;
}
}
.form-actions {
display: flex;
flex-direction: row;
gap: 20rpx;
}
.submit-btn {
height: 90rpx;
border-radius: 45rpx;
font-size: 32rpx;
font-weight: 500;
background: linear-gradient(135deg, #4a7bff, #6a5acd);
box-shadow: 0 8rpx 16rpx rgba(74, 123, 255, 0.2);
transition: all 0.3s ease;
&:active {
transform: translateY(2rpx);
box-shadow: 0 4rpx 8rpx rgba(74, 123, 255, 0.15);
}
}
</style>

View File

@ -1,203 +0,0 @@
<route lang="json5">
{
style: {
navigationBarTitleText: '修改密码',
},
}
</route>
<template>
<view class="profile-info-container">
<view class="profile-card">
<view class="form-wrapper">
<wd-form ref="formRef" :model="formData" label-width="160rpx" class="profile-form">
<wd-cell-group class="form-group">
<!-- 昵称 -->
<view class="sex-field">
<text class="field-label">旧密码</text>
<wd-input
prop="oldPassword"
clearable
v-model="formData.oldPassword"
placeholder="请输入旧密码"
show-password
:rules="[{ required: true, message: '请填写旧密码' }]"
class="form-input"
/>
</view>
<view class="sex-field">
<text class="field-label">新密码</text>
<wd-input
prop="newPassword"
clearable
v-model="formData.newPassword"
placeholder="请输入新密码"
show-password
:rules="[{ required: true, message: '请填写新密码' }]"
class="form-input"
/>
</view>
<view class="sex-field">
<text class="field-label">确认密码</text>
<wd-input
prop="confirmPassword"
clearable
v-model="formData.confirmPassword"
placeholder="请输入新密码"
show-password
:rules="[{ required: true, message: '请填写新密码' }]"
class="form-input"
/>
</view>
</wd-cell-group>
</wd-form>
<!-- 操作按钮 -->
<view class="form-actions">
<wd-button type="primary" size="large" @click="handleSubmit">保存修改</wd-button>
</view>
</view>
</view>
</view>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import { useUserStore } from '@/store'
import { storeToRefs } from 'pinia'
import { toast } from '@/utils/toast'
import { updateInfo, updateUserPassword } from '@/api/login'
//
const formRef = ref()
//
const userStore = useUserStore()
const { userInfo } = storeToRefs(userStore)
//
const formData = ref({
id: userInfo.value.id,
oldPassword: '',
newPassword: '',
confirmPassword: '',
})
//
const handleSubmit = async () => {
//
const valid = await formRef.value.validate()
if (!valid) return
const { message } = await updateUserPassword(formData.value)
await useUserStore().logout()
toast.success('修改成功,请重新登录')
}
</script>
<style lang="scss" scoped>
.profile-info-container {
min-height: 100vh;
background-color: #f5f7fa;
padding: 30rpx;
}
.profile-card {
background-color: #ffffff;
border-radius: 24rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
overflow: hidden;
}
.card-header {
padding: 40rpx 30rpx 20rpx;
border-bottom: 2rpx solid #f0f0f0;
}
.card-title {
font-size: 36rpx;
font-weight: 600;
color: #333;
position: relative;
display: inline-block;
padding-bottom: 16rpx;
&::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 60rpx;
height: 6rpx;
background: linear-gradient(90deg, #4a7bff, #6a5acd);
border-radius: 6rpx;
}
}
.form-wrapper {
padding: 30rpx;
}
.form-group {
border-radius: 16rpx;
overflow: hidden;
margin-bottom: 40rpx;
}
.form-input {
font-size: 30rpx;
}
.sex-field {
display: flex;
align-items: center;
padding: 24rpx 30rpx;
background-color: #ffffff;
}
.field-label {
width: 160rpx;
font-size: 30rpx;
color: #333;
}
.radio-group {
flex: 1;
display: flex;
gap: 20rpx;
}
.radio-btn {
flex: 1;
height: 80rpx;
line-height: 80rpx;
text-align: center;
font-size: 30rpx;
border-radius: 12rpx;
background-color: #f5f7fa;
&:active {
opacity: 0.8;
}
}
.form-actions {
display: flex;
flex-direction: row;
gap: 20rpx;
}
.submit-btn {
height: 90rpx;
border-radius: 45rpx;
font-size: 32rpx;
font-weight: 500;
background: linear-gradient(135deg, #4a7bff, #6a5acd);
box-shadow: 0 8rpx 16rpx rgba(74, 123, 255, 0.2);
transition: all 0.3s ease;
&:active {
transform: translateY(2rpx);
box-shadow: 0 4rpx 8rpx rgba(74, 123, 255, 0.15);
}
}
</style>