Merge branch 'base' into spa
This commit is contained in:
commit
938ca363e5
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "unibest",
|
"name": "unibest",
|
||||||
"type": "commonjs",
|
"type": "commonjs",
|
||||||
"version": "2.9.1",
|
"version": "2.10.0",
|
||||||
"description": "unibest - 最好的 uniapp 开发模板",
|
"description": "unibest - 最好的 uniapp 开发模板",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "feige996",
|
"name": "feige996",
|
||||||
@ -101,11 +101,12 @@
|
|||||||
"@tanstack/vue-query": "^5.62.16",
|
"@tanstack/vue-query": "^5.62.16",
|
||||||
"abortcontroller-polyfill": "^1.7.8",
|
"abortcontroller-polyfill": "^1.7.8",
|
||||||
"dayjs": "1.11.10",
|
"dayjs": "1.11.10",
|
||||||
|
"js-cookie": "^3.0.5",
|
||||||
"pinia": "2.0.36",
|
"pinia": "2.0.36",
|
||||||
"pinia-plugin-persistedstate": "3.2.1",
|
"pinia-plugin-persistedstate": "3.2.1",
|
||||||
"qs": "6.5.3",
|
"qs": "6.5.3",
|
||||||
"vue": "^3.5.15",
|
"vue": "^3.5.15",
|
||||||
"wot-design-uni": "^1.4.0",
|
"wot-design-uni": "^1.9.1",
|
||||||
"z-paging": "^2.8.4"
|
"z-paging": "^2.8.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@ -144,8 +145,8 @@
|
|||||||
"typescript": "^5.7.2",
|
"typescript": "^5.7.2",
|
||||||
"unocss": "^66.0.0",
|
"unocss": "^66.0.0",
|
||||||
"unplugin-auto-import": "^0.17.8",
|
"unplugin-auto-import": "^0.17.8",
|
||||||
"vite": "5.2.8",
|
"vite": "6.3.5",
|
||||||
"vite-plugin-restart": "^0.4.2",
|
"vite-plugin-restart": "^0.4.2",
|
||||||
"vue-tsc": "^1.8.27"
|
"vue-tsc": "^2.2.10"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
7623
pnpm-lock.yaml
generated
7623
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
83
src/api/login.ts
Normal file
83
src/api/login.ts
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
import { ICaptcha, IUpdateInfo, IUpdatePassword, IUserInfoVo, IUserLogin } from './login.typings'
|
||||||
|
import { http } from '@/utils/http'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登录表单
|
||||||
|
*/
|
||||||
|
export interface ILoginForm {
|
||||||
|
username: string
|
||||||
|
password: string
|
||||||
|
code: string
|
||||||
|
uuid: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取验证码
|
||||||
|
* @returns ICaptcha 验证码
|
||||||
|
*/
|
||||||
|
export const getCode = () => {
|
||||||
|
return http.get<ICaptcha>('/user/getCode')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户登录
|
||||||
|
* @param loginForm 登录表单
|
||||||
|
*/
|
||||||
|
export const login = (loginForm: ILoginForm) => {
|
||||||
|
return http.post<IUserLogin>('/user/login', loginForm)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户信息
|
||||||
|
*/
|
||||||
|
export const getUserInfo = () => {
|
||||||
|
return http.get<IUserInfoVo>('/user/info')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 退出登录
|
||||||
|
*/
|
||||||
|
export const logout = () => {
|
||||||
|
return http.get<void>('/user/logout')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改用户信息
|
||||||
|
*/
|
||||||
|
export const updateInfo = (data: IUpdateInfo) => {
|
||||||
|
return http.post('/user/updateInfo', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改用户密码
|
||||||
|
*/
|
||||||
|
export const updateUserPassword = (data: IUpdatePassword) => {
|
||||||
|
return http.post('/user/updatePassword', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取微信登录凭证
|
||||||
|
* @returns Promise 包含微信登录凭证(code)
|
||||||
|
*/
|
||||||
|
export const getWxCode = () => {
|
||||||
|
return new Promise<UniApp.LoginRes>((resolve, reject) => {
|
||||||
|
uni.login({
|
||||||
|
provider: 'weixin',
|
||||||
|
success: (res) => resolve(res),
|
||||||
|
fail: (err) => reject(new Error(err)),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信登录参数
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信登录
|
||||||
|
* @param params 微信登录参数,包含code
|
||||||
|
* @returns Promise 包含登录结果
|
||||||
|
*/
|
||||||
|
export const wxLogin = (data: { code: string }) => {
|
||||||
|
return http.post<IUserLogin>('/user/wxLogin', data)
|
||||||
|
}
|
57
src/api/login.typings.ts
Normal file
57
src/api/login.typings.ts
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
/**
|
||||||
|
* 用户信息
|
||||||
|
*/
|
||||||
|
export type IUserInfoVo = {
|
||||||
|
id: number
|
||||||
|
username: string
|
||||||
|
avatar: string
|
||||||
|
token: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登录返回的信息
|
||||||
|
*/
|
||||||
|
export type IUserLogin = {
|
||||||
|
id: string
|
||||||
|
username: string
|
||||||
|
token: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取验证码
|
||||||
|
*/
|
||||||
|
export type ICaptcha = {
|
||||||
|
captchaEnabled: boolean
|
||||||
|
uuid: string
|
||||||
|
image: string
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 上传成功的信息
|
||||||
|
*/
|
||||||
|
export type IUploadSuccessInfo = {
|
||||||
|
fileId: number
|
||||||
|
originalName: string
|
||||||
|
fileName: string
|
||||||
|
storagePath: string
|
||||||
|
fileHash: string
|
||||||
|
fileType: string
|
||||||
|
fileBusinessType: string
|
||||||
|
fileSize: number
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 更新用户信息
|
||||||
|
*/
|
||||||
|
export type IUpdateInfo = {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
sex: string
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 更新用户信息
|
||||||
|
*/
|
||||||
|
export type IUpdatePassword = {
|
||||||
|
id: number
|
||||||
|
oldPassword: string
|
||||||
|
newPassword: string
|
||||||
|
confirmPassword: string
|
||||||
|
}
|
@ -1,44 +1,54 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
withDefaults(defineProps<{
|
withDefaults(
|
||||||
leftText?: string;
|
defineProps<{
|
||||||
rightText?: string;
|
leftText?: string
|
||||||
leftArrow?: boolean;
|
rightText?: string
|
||||||
bordered?: boolean;
|
leftArrow?: boolean
|
||||||
fixed?: boolean;
|
bordered?: boolean
|
||||||
placeholder?: boolean;
|
fixed?: boolean
|
||||||
zIndex?: number;
|
placeholder?: boolean
|
||||||
safeAreaInsetTop?: boolean;
|
zIndex?: number
|
||||||
leftDisabled?: boolean;
|
safeAreaInsetTop?: boolean
|
||||||
rightDisabled?: boolean;
|
leftDisabled?: boolean
|
||||||
}>(), {
|
rightDisabled?: boolean
|
||||||
leftText: '返回',
|
}>(),
|
||||||
rightText: '',
|
{
|
||||||
leftArrow: true,
|
leftText: '返回',
|
||||||
bordered: true,
|
rightText: '',
|
||||||
fixed: false,
|
leftArrow: true,
|
||||||
placeholder: true,
|
bordered: true,
|
||||||
zIndex: 1,
|
fixed: false,
|
||||||
safeAreaInsetTop: true,
|
placeholder: true,
|
||||||
leftDisabled: false,
|
zIndex: 1,
|
||||||
rightDisabled: false,
|
safeAreaInsetTop: true,
|
||||||
});
|
leftDisabled: false,
|
||||||
|
rightDisabled: false,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
function handleClickLeft() {
|
function handleClickLeft() {
|
||||||
uni.navigateBack({
|
uni.navigateBack({
|
||||||
fail() {
|
fail() {
|
||||||
uni.reLaunch({
|
uni.reLaunch({
|
||||||
url: '/pages/index/index',
|
url: '/pages/index/index',
|
||||||
});
|
})
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<wd-navbar
|
<wd-navbar
|
||||||
:left-text="leftText" :right-text="rightText" :left-arrow="leftArrow"
|
:left-text="leftText"
|
||||||
:bordered="bordered" :fixed="fixed" :placeholder="placeholder" :z-index="zIndex"
|
:right-text="rightText"
|
||||||
:safe-area-inset-top="safeAreaInsetTop" :left-disabled="leftDisabled" :right-disabled="rightDisabled"
|
:left-arrow="leftArrow"
|
||||||
|
:bordered="bordered"
|
||||||
|
:fixed="fixed"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
:z-index="zIndex"
|
||||||
|
:safe-area-inset-top="safeAreaInsetTop"
|
||||||
|
:left-disabled="leftDisabled"
|
||||||
|
:right-disabled="rightDisabled"
|
||||||
@click-left="handleClickLeft"
|
@click-left="handleClickLeft"
|
||||||
>
|
>
|
||||||
<template #title>
|
<template #title>
|
||||||
|
@ -12,7 +12,7 @@ const loginRoute = '/pages/login/index'
|
|||||||
|
|
||||||
const isLogined = () => {
|
const isLogined = () => {
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
return userStore.isLogined
|
return !!userStore.userInfo.username
|
||||||
}
|
}
|
||||||
|
|
||||||
const isDev = import.meta.env.DEV
|
const isDev = import.meta.env.DEV
|
||||||
|
@ -32,6 +32,42 @@
|
|||||||
"navigationBarTitleText": "关于",
|
"navigationBarTitleText": "关于",
|
||||||
"navigationStyle": "custom"
|
"navigationStyle": "custom"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/login/index",
|
||||||
|
"type": "page",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "登录",
|
||||||
|
"navigationStyle": "custom"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/mine/index",
|
||||||
|
"type": "page",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "我的"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/mine/about/index",
|
||||||
|
"type": "page",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "关于我们"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/mine/info/index",
|
||||||
|
"type": "page",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "个人资料"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/mine/password/index",
|
||||||
|
"type": "page",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "修改密码"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"subPackages": []
|
"subPackages": []
|
||||||
|
@ -42,7 +42,7 @@ defineOptions({
|
|||||||
const { safeAreaInsets } = uni.getSystemInfoSync()
|
const { safeAreaInsets } = uni.getSystemInfoSync()
|
||||||
const author = ref('菲鸽')
|
const author = ref('菲鸽')
|
||||||
const description = ref(
|
const description = ref(
|
||||||
'unibest 是一个集成了多种工具和技术的 uniapp 开发模板,由 uniapp + Vue3 + Ts + Vite4 + UnoCss + UniUI + VSCode 构建,模板具有代码提示、自动格式化、统一配置、代码片段等功能,并内置了许多常用的基本组件和基本功能,让你编写 uniapp 拥有 best 体验。',
|
'unibest 是一个集成了多种工具和技术的 uniapp 开发模板,由 uniapp + Vue3 + Ts + Vite6 + UnoCss + VSCode 构建,模板具有代码提示、自动格式化、统一配置、代码片段等功能,并内置了许多常用的基本组件和基本功能,让你编写 uniapp 拥有 best 体验。',
|
||||||
)
|
)
|
||||||
// 测试 uni API 自动引入
|
// 测试 uni API 自动引入
|
||||||
onLoad(() => {
|
onLoad(() => {
|
||||||
|
584
src/pages/login/index.vue
Normal file
584
src/pages/login/index.vue
Normal file
@ -0,0 +1,584 @@
|
|||||||
|
<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>
|
173
src/pages/mine/about/index.vue
Normal file
173
src/pages/mine/about/index.vue
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
<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>
|
367
src/pages/mine/index.vue
Normal file
367
src/pages/mine/index.vue
Normal file
@ -0,0 +1,367 @@
|
|||||||
|
<route lang="json5">
|
||||||
|
{
|
||||||
|
style: {
|
||||||
|
navigationBarTitleText: '我的',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</route>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<view class="profile-container">
|
||||||
|
{{ JSON.stringify(userStore.userInfo) }}
|
||||||
|
<!-- 用户信息区域 -->
|
||||||
|
<view class="user-info-section">
|
||||||
|
<!-- #ifdef MP-WEIXIN -->
|
||||||
|
<button class="avatar-button" open-type="chooseAvatar" @chooseavatar="onChooseAvatar">
|
||||||
|
<wd-img :src="userStore.userInfo.avatar" width="80px" height="80px" radius="50%"></wd-img>
|
||||||
|
</button>
|
||||||
|
<!-- #endif -->
|
||||||
|
<!-- #ifndef MP-WEIXIN -->
|
||||||
|
<view class="avatar-wrapper" @click="run">
|
||||||
|
<wd-img :src="userStore.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="userStore.userInfo.username"
|
||||||
|
/>
|
||||||
|
<!-- #endif -->
|
||||||
|
<!-- #ifndef MP-WEIXIN -->
|
||||||
|
<view class="username">{{ userStore.userInfo.username }}</view>
|
||||||
|
<!-- #endif -->
|
||||||
|
<view class="user-id">ID: {{ userStore.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 { uploadFileUrl, useUpload } from '@/utils/uploadFile'
|
||||||
|
import { storeToRefs } from 'pinia'
|
||||||
|
import { IUploadSuccessInfo } from '@/api/login.typings'
|
||||||
|
|
||||||
|
const userStore = useUserStore()
|
||||||
|
|
||||||
|
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>(
|
||||||
|
uploadFileUrl.USER_AVATAR,
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
onSuccess: (res) => useUserStore().getUserInfo(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
// #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>(
|
||||||
|
uploadFileUrl.USER_AVATAR,
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
onSuccess: (res) => useUserStore().getUserInfo(),
|
||||||
|
},
|
||||||
|
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.success('功能开发中')
|
||||||
|
}
|
||||||
|
// 应用更新
|
||||||
|
const handleAppUpdate = () => {
|
||||||
|
// #ifdef MP
|
||||||
|
// #ifndef MP-HARMONY
|
||||||
|
const updateManager = uni.getUpdateManager()
|
||||||
|
updateManager.onCheckForUpdate(function (res) {
|
||||||
|
// 请求完新版本信息的回调
|
||||||
|
// console.log(res.hasUpdate)
|
||||||
|
if (res.hasUpdate) {
|
||||||
|
toast.success('检测到新版本,正在下载中...')
|
||||||
|
} else {
|
||||||
|
toast.success('已是最新版本')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
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.success('功能开发中')
|
||||||
|
// #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.success('清除缓存成功')
|
||||||
|
} catch (err) {
|
||||||
|
console.error('清除缓存失败:', err)
|
||||||
|
toast.error('清除缓存失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// 退出登录
|
||||||
|
const handleLogout = () => {
|
||||||
|
uni.showModal({
|
||||||
|
title: '提示',
|
||||||
|
content: '确定要退出登录吗?',
|
||||||
|
success: (res) => {
|
||||||
|
if (res.confirm) {
|
||||||
|
// 清空用户信息
|
||||||
|
useUserStore().logout()
|
||||||
|
hasLogin.value = false
|
||||||
|
// 执行退出登录逻辑
|
||||||
|
toast.success('退出登录成功')
|
||||||
|
// #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>
|
190
src/pages/mine/info/index.vue
Normal file
190
src/pages/mine/info/index.vue
Normal file
@ -0,0 +1,190 @@
|
|||||||
|
<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>
|
203
src/pages/mine/password/index.vue
Normal file
203
src/pages/mine/password/index.vue
Normal file
@ -0,0 +1,203 @@
|
|||||||
|
<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>
|
BIN
src/static/images/avatar.jpg
Normal file
BIN
src/static/images/avatar.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 57 KiB |
BIN
src/static/images/default-avatar.png
Normal file
BIN
src/static/images/default-avatar.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 560 B |
@ -1,32 +1,100 @@
|
|||||||
|
import {
|
||||||
|
login as _login,
|
||||||
|
getUserInfo as _getUserInfo,
|
||||||
|
wxLogin as _wxLogin,
|
||||||
|
logout as _logout,
|
||||||
|
getWxCode,
|
||||||
|
} from '@/api/login'
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
import { toast } from '@/utils/toast'
|
||||||
|
import { IUserInfoVo } from '@/api/login.typings'
|
||||||
|
|
||||||
const initState = { nickname: '', avatar: '' }
|
// 初始化状态
|
||||||
|
const userInfoState: IUserInfoVo = {
|
||||||
|
id: 0,
|
||||||
|
username: '',
|
||||||
|
avatar: '/static/images/default-avatar.png',
|
||||||
|
token: '',
|
||||||
|
}
|
||||||
|
|
||||||
export const useUserStore = defineStore(
|
export const useUserStore = defineStore(
|
||||||
'user',
|
'user',
|
||||||
() => {
|
() => {
|
||||||
const userInfo = ref<IUserInfo>({ ...initState })
|
// 定义用户信息
|
||||||
|
const userInfo = ref<IUserInfoVo>({ ...userInfoState })
|
||||||
const setUserInfo = (val: IUserInfo) => {
|
// 设置用户信息
|
||||||
|
const setUserInfo = (val: IUserInfoVo) => {
|
||||||
|
console.log('设置用户信息', val)
|
||||||
|
// 若头像为空 则使用默认头像
|
||||||
|
if (!val.avatar) {
|
||||||
|
val.avatar = userInfoState.avatar
|
||||||
|
} else {
|
||||||
|
val.avatar = 'https://oss.laf.run/ukw0y1-site/avatar.jpg?feige'
|
||||||
|
}
|
||||||
userInfo.value = val
|
userInfo.value = val
|
||||||
}
|
}
|
||||||
|
// 删除用户信息
|
||||||
|
const removeUserInfo = () => {
|
||||||
|
userInfo.value = { ...userInfoState }
|
||||||
|
uni.removeStorageSync('userInfo')
|
||||||
|
uni.removeStorageSync('token')
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 用户登录
|
||||||
|
* @param credentials 登录参数
|
||||||
|
* @returns R<IUserLogin>
|
||||||
|
*/
|
||||||
|
const login = async (credentials: {
|
||||||
|
username: string
|
||||||
|
password: string
|
||||||
|
code: string
|
||||||
|
uuid: string
|
||||||
|
}) => {
|
||||||
|
const res = await _login(credentials)
|
||||||
|
console.log('登录信息', res)
|
||||||
|
toast.success('登录成功')
|
||||||
|
getUserInfo()
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 获取用户信息
|
||||||
|
*/
|
||||||
|
const getUserInfo = async () => {
|
||||||
|
const res = await _getUserInfo()
|
||||||
|
const userInfo = res.data
|
||||||
|
setUserInfo(userInfo)
|
||||||
|
uni.setStorageSync('userInfo', userInfo)
|
||||||
|
uni.setStorageSync('token', userInfo.token)
|
||||||
|
// TODO 这里可以增加获取用户路由的方法 根据用户的角色动态生成路由
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 退出登录 并 删除用户信息
|
||||||
|
*/
|
||||||
|
const logout = async () => {
|
||||||
|
_logout()
|
||||||
|
removeUserInfo()
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 微信登录
|
||||||
|
*/
|
||||||
|
const wxLogin = async () => {
|
||||||
|
// 获取微信小程序登录的code
|
||||||
|
const data = await getWxCode()
|
||||||
|
console.log('微信登录code', data)
|
||||||
|
|
||||||
const clearUserInfo = () => {
|
const res = await _wxLogin(data)
|
||||||
userInfo.value = { ...initState }
|
getUserInfo()
|
||||||
|
return res
|
||||||
}
|
}
|
||||||
// 一般没有reset需求,不需要的可以删除
|
|
||||||
const reset = () => {
|
|
||||||
userInfo.value = { ...initState }
|
|
||||||
}
|
|
||||||
const isLogined = computed(() => !!userInfo.value.token)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
userInfo,
|
userInfo,
|
||||||
setUserInfo,
|
login,
|
||||||
clearUserInfo,
|
wxLogin,
|
||||||
isLogined,
|
getUserInfo,
|
||||||
reset,
|
logout,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
2
src/types/components.d.ts
vendored
2
src/types/components.d.ts
vendored
@ -11,5 +11,7 @@ declare module 'vue' {
|
|||||||
FgTabbar: (typeof import('./../components/fg-tabbar/fg-tabbar.vue'))['default']
|
FgTabbar: (typeof import('./../components/fg-tabbar/fg-tabbar.vue'))['default']
|
||||||
PrivacyPopup: (typeof import('./../components/privacy-popup/privacy-popup.vue'))['default']
|
PrivacyPopup: (typeof import('./../components/privacy-popup/privacy-popup.vue'))['default']
|
||||||
Tabbar: (typeof import('./../components/tabbar/tabbar.vue'))['default']
|
Tabbar: (typeof import('./../components/tabbar/tabbar.vue'))['default']
|
||||||
|
FgNavbar: (typeof import('./../components/fg-navbar/fg-navbar.vue'))['default']
|
||||||
|
PrivacyPopup: (typeof import('./../components/privacy-popup/privacy-popup.vue'))['default']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
7
src/types/uni-pages.d.ts
vendored
7
src/types/uni-pages.d.ts
vendored
@ -5,7 +5,12 @@
|
|||||||
|
|
||||||
interface NavigateToOptions {
|
interface NavigateToOptions {
|
||||||
url: "/pages/index/index" |
|
url: "/pages/index/index" |
|
||||||
"/pages/about/about";
|
"/pages/about/about" |
|
||||||
|
"/pages/login/index" |
|
||||||
|
"/pages/mine/index" |
|
||||||
|
"/pages/mine/about/index" |
|
||||||
|
"/pages/mine/info/index" |
|
||||||
|
"/pages/mine/password/index";
|
||||||
}
|
}
|
||||||
interface RedirectToOptions extends NavigateToOptions {}
|
interface RedirectToOptions extends NavigateToOptions {}
|
||||||
|
|
||||||
|
@ -4,3 +4,12 @@ export enum TestEnum {
|
|||||||
A = '1',
|
A = '1',
|
||||||
B = '2',
|
B = '2',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// uni.uploadFile文件上传参数
|
||||||
|
export type IUniUploadFileOptions = {
|
||||||
|
file?: File
|
||||||
|
files?: UniApp.UploadFileOptionFiles[]
|
||||||
|
filePath?: string
|
||||||
|
name?: string
|
||||||
|
formData?: any
|
||||||
|
}
|
||||||
|
@ -23,6 +23,26 @@ export const getIsTabbar = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断指定页面是否是 tabbar 页
|
||||||
|
* @param path 页面路径
|
||||||
|
* @returns true: 是 tabbar 页 false: 不是 tabbar 页
|
||||||
|
*/
|
||||||
|
export const isTableBar = (path: string) => {
|
||||||
|
if (!tabBar) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (!tabBar.list.length) {
|
||||||
|
// 通常有 tabBar 的话,list 不能有空,且至少有2个元素,这里其实不用处理
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// 这里需要处理一下 path,因为 tabBar 中的 pagePath 是不带 /pages 前缀的
|
||||||
|
if (path.startsWith('/')) {
|
||||||
|
path = path.substring(1)
|
||||||
|
}
|
||||||
|
return !!tabBar.list.find((e) => e.pagePath === path)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取当前页面路由的 path 路径和 redirectPath 路径
|
* 获取当前页面路由的 path 路径和 redirectPath 路径
|
||||||
* path 如 '/pages/login/index'
|
* path 如 '/pages/login/index'
|
||||||
|
65
src/utils/toast.ts
Normal file
65
src/utils/toast.ts
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
/**
|
||||||
|
* toast 弹窗组件
|
||||||
|
* 支持 success/error/warning/info 四种状态
|
||||||
|
* 可配置 duration, position 等参数
|
||||||
|
*/
|
||||||
|
|
||||||
|
type ToastType = 'success' | 'error' | 'warning' | 'info'
|
||||||
|
|
||||||
|
interface ToastOptions {
|
||||||
|
type?: ToastType
|
||||||
|
duration?: number
|
||||||
|
position?: 'top' | 'middle' | 'bottom'
|
||||||
|
icon?: 'success' | 'error' | 'none' | 'loading' | 'fail' | 'exception'
|
||||||
|
message: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function showToast(options: ToastOptions | string) {
|
||||||
|
const defaultOptions: ToastOptions = {
|
||||||
|
type: 'info',
|
||||||
|
duration: 2000,
|
||||||
|
position: 'middle',
|
||||||
|
message: '',
|
||||||
|
}
|
||||||
|
const mergedOptions =
|
||||||
|
typeof options === 'string'
|
||||||
|
? { ...defaultOptions, message: options }
|
||||||
|
: { ...defaultOptions, ...options }
|
||||||
|
// 映射position到uniapp支持的格式
|
||||||
|
const positionMap: Record<ToastOptions['position'], 'top' | 'bottom' | 'center'> = {
|
||||||
|
top: 'top',
|
||||||
|
middle: 'center',
|
||||||
|
bottom: 'bottom',
|
||||||
|
}
|
||||||
|
|
||||||
|
// 映射图标类型
|
||||||
|
const iconMap: Record<
|
||||||
|
ToastType,
|
||||||
|
'success' | 'error' | 'none' | 'loading' | 'fail' | 'exception'
|
||||||
|
> = {
|
||||||
|
success: 'success',
|
||||||
|
error: 'error',
|
||||||
|
warning: 'fail',
|
||||||
|
info: 'none',
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用uni.showToast显示提示
|
||||||
|
uni.showToast({
|
||||||
|
title: mergedOptions.message,
|
||||||
|
duration: mergedOptions.duration,
|
||||||
|
position: positionMap[mergedOptions.position],
|
||||||
|
icon: mergedOptions.icon || iconMap[mergedOptions.type],
|
||||||
|
mask: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const toast = {
|
||||||
|
success: (message: string, options?: Omit<ToastOptions, 'type'>) =>
|
||||||
|
showToast({ ...options, type: 'success', message }),
|
||||||
|
error: (message: string, options?: Omit<ToastOptions, 'type'>) =>
|
||||||
|
showToast({ ...options, type: 'error', message }),
|
||||||
|
warning: (message: string, options?: Omit<ToastOptions, 'type'>) =>
|
||||||
|
showToast({ ...options, type: 'warning', message }),
|
||||||
|
info: (message: string, options?: Omit<ToastOptions, 'type'>) =>
|
||||||
|
showToast({ ...options, type: 'info', message }),
|
||||||
|
}
|
336
src/utils/uploadFile.ts
Normal file
336
src/utils/uploadFile.ts
Normal file
@ -0,0 +1,336 @@
|
|||||||
|
import { toast } from './toast'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件上传钩子函数使用示例
|
||||||
|
* @example
|
||||||
|
* const { loading, error, data, progress, run } = useUpload<IUploadResult>(
|
||||||
|
* uploadUrl,
|
||||||
|
* {},
|
||||||
|
* {
|
||||||
|
* maxSize: 5, // 最大5MB
|
||||||
|
* sourceType: ['album'], // 仅支持从相册选择
|
||||||
|
* onProgress: (p) => console.log(`上传进度:${p}%`),
|
||||||
|
* onSuccess: (res) => console.log('上传成功', res),
|
||||||
|
* onError: (err) => console.error('上传失败', err),
|
||||||
|
* },
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传文件的URL配置
|
||||||
|
*/
|
||||||
|
export const uploadFileUrl = {
|
||||||
|
/** 用户头像上传地址 */
|
||||||
|
USER_AVATAR: import.meta.env.VITE_SERVER_BASEURL + '/user/avatar',
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通用文件上传函数(支持直接传入文件路径)
|
||||||
|
* @param url 上传地址
|
||||||
|
* @param filePath 本地文件路径
|
||||||
|
* @param formData 额外表单数据
|
||||||
|
* @param options 上传选项
|
||||||
|
*/
|
||||||
|
export const useFileUpload = <T = string>(
|
||||||
|
url: string,
|
||||||
|
filePath: string,
|
||||||
|
formData: Record<string, any> = {},
|
||||||
|
options: Omit<UploadOptions, 'sourceType' | 'sizeType' | 'count'> = {},
|
||||||
|
) => {
|
||||||
|
return useUpload<T>(
|
||||||
|
url,
|
||||||
|
formData,
|
||||||
|
{
|
||||||
|
...options,
|
||||||
|
sourceType: ['album'],
|
||||||
|
sizeType: ['original'],
|
||||||
|
},
|
||||||
|
filePath,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UploadOptions {
|
||||||
|
/** 最大可选择的图片数量,默认为1 */
|
||||||
|
count?: number
|
||||||
|
/** 所选的图片的尺寸,original-原图,compressed-压缩图 */
|
||||||
|
sizeType?: Array<'original' | 'compressed'>
|
||||||
|
/** 选择图片的来源,album-相册,camera-相机 */
|
||||||
|
sourceType?: Array<'album' | 'camera'>
|
||||||
|
/** 文件大小限制,单位:MB */
|
||||||
|
maxSize?: number //
|
||||||
|
/** 上传进度回调函数 */
|
||||||
|
onProgress?: (progress: number) => void
|
||||||
|
/** 上传成功回调函数 */
|
||||||
|
onSuccess?: (res: UniApp.UploadFileSuccessCallbackResult) => void
|
||||||
|
/** 上传失败回调函数 */
|
||||||
|
onError?: (err: Error | UniApp.GeneralCallbackResult) => void
|
||||||
|
/** 上传完成回调函数(无论成功失败) */
|
||||||
|
onComplete?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件上传钩子函数
|
||||||
|
* @template T 上传成功后返回的数据类型
|
||||||
|
* @param url 上传地址
|
||||||
|
* @param formData 额外的表单数据
|
||||||
|
* @param options 上传选项
|
||||||
|
* @returns 上传状态和控制对象
|
||||||
|
*/
|
||||||
|
export const useUpload = <T = string>(
|
||||||
|
url: string,
|
||||||
|
formData: Record<string, any> = {},
|
||||||
|
options: UploadOptions = {},
|
||||||
|
/** 直接传入文件路径,跳过选择器 */
|
||||||
|
directFilePath?: string,
|
||||||
|
) => {
|
||||||
|
/** 上传中状态 */
|
||||||
|
const loading = ref(false)
|
||||||
|
/** 上传错误状态 */
|
||||||
|
const error = ref(false)
|
||||||
|
/** 上传成功后的响应数据 */
|
||||||
|
const data = ref<T>()
|
||||||
|
/** 上传进度(0-100) */
|
||||||
|
const progress = ref(0)
|
||||||
|
|
||||||
|
/** 解构上传选项,设置默认值 */
|
||||||
|
const {
|
||||||
|
/** 最大可选择的图片数量 */
|
||||||
|
count = 1,
|
||||||
|
/** 所选的图片的尺寸 */
|
||||||
|
sizeType = ['original', 'compressed'],
|
||||||
|
/** 选择图片的来源 */
|
||||||
|
sourceType = ['album', 'camera'],
|
||||||
|
/** 文件大小限制(MB) */
|
||||||
|
maxSize = 10,
|
||||||
|
/** 进度回调 */
|
||||||
|
onProgress,
|
||||||
|
/** 成功回调 */
|
||||||
|
onSuccess,
|
||||||
|
/** 失败回调 */
|
||||||
|
onError,
|
||||||
|
/** 完成回调 */
|
||||||
|
onComplete,
|
||||||
|
} = options
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查文件大小是否超过限制
|
||||||
|
* @param size 文件大小(字节)
|
||||||
|
* @returns 是否通过检查
|
||||||
|
*/
|
||||||
|
const checkFileSize = (size: number) => {
|
||||||
|
const sizeInMB = size / 1024 / 1024
|
||||||
|
if (sizeInMB > maxSize) {
|
||||||
|
toast.warning(`文件大小不能超过${maxSize}MB`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 触发文件选择和上传
|
||||||
|
* 根据平台使用不同的选择器:
|
||||||
|
* - 微信小程序使用 chooseMedia
|
||||||
|
* - 其他平台使用 chooseImage
|
||||||
|
*/
|
||||||
|
const run = () => {
|
||||||
|
if (directFilePath) {
|
||||||
|
// 直接使用传入的文件路径
|
||||||
|
loading.value = true
|
||||||
|
progress.value = 0
|
||||||
|
uploadFile<T>({
|
||||||
|
url,
|
||||||
|
tempFilePath: directFilePath,
|
||||||
|
formData,
|
||||||
|
data,
|
||||||
|
error,
|
||||||
|
loading,
|
||||||
|
progress,
|
||||||
|
onProgress,
|
||||||
|
onSuccess,
|
||||||
|
onError,
|
||||||
|
onComplete,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// #ifdef MP-WEIXIN
|
||||||
|
// 微信小程序环境下使用 chooseMedia API
|
||||||
|
uni.chooseMedia({
|
||||||
|
count,
|
||||||
|
mediaType: ['image'], // 仅支持图片类型
|
||||||
|
sourceType,
|
||||||
|
success: (res) => {
|
||||||
|
const file = res.tempFiles[0]
|
||||||
|
// 检查文件大小是否符合限制
|
||||||
|
if (!checkFileSize(file.size)) return
|
||||||
|
|
||||||
|
// 开始上传
|
||||||
|
loading.value = true
|
||||||
|
progress.value = 0
|
||||||
|
uploadFile<T>({
|
||||||
|
url,
|
||||||
|
tempFilePath: file.tempFilePath,
|
||||||
|
formData,
|
||||||
|
data,
|
||||||
|
error,
|
||||||
|
loading,
|
||||||
|
progress,
|
||||||
|
onProgress,
|
||||||
|
onSuccess,
|
||||||
|
onError,
|
||||||
|
onComplete,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
fail: (err) => {
|
||||||
|
console.error('选择媒体文件失败:', err)
|
||||||
|
error.value = true
|
||||||
|
onError?.(err)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
// #ifndef MP-WEIXIN
|
||||||
|
// 非微信小程序环境下使用 chooseImage API
|
||||||
|
uni.chooseImage({
|
||||||
|
count,
|
||||||
|
sizeType,
|
||||||
|
sourceType,
|
||||||
|
success: (res) => {
|
||||||
|
console.log('选择图片成功:', res)
|
||||||
|
|
||||||
|
// 开始上传
|
||||||
|
loading.value = true
|
||||||
|
progress.value = 0
|
||||||
|
uploadFile<T>({
|
||||||
|
url,
|
||||||
|
tempFilePath: res.tempFilePaths[0],
|
||||||
|
formData,
|
||||||
|
data,
|
||||||
|
error,
|
||||||
|
loading,
|
||||||
|
progress,
|
||||||
|
onProgress,
|
||||||
|
onSuccess,
|
||||||
|
onError,
|
||||||
|
onComplete,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
fail: (err) => {
|
||||||
|
console.error('选择图片失败:', err)
|
||||||
|
error.value = true
|
||||||
|
onError?.(err)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
// #endif
|
||||||
|
}
|
||||||
|
|
||||||
|
return { loading, error, data, progress, run }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件上传选项接口
|
||||||
|
* @template T 上传成功后返回的数据类型
|
||||||
|
*/
|
||||||
|
interface UploadFileOptions<T> {
|
||||||
|
/** 上传地址 */
|
||||||
|
url: string
|
||||||
|
/** 临时文件路径 */
|
||||||
|
tempFilePath: string
|
||||||
|
/** 额外的表单数据 */
|
||||||
|
formData: Record<string, any>
|
||||||
|
/** 上传成功后的响应数据 */
|
||||||
|
data: Ref<T | undefined>
|
||||||
|
/** 上传错误状态 */
|
||||||
|
error: Ref<boolean>
|
||||||
|
/** 上传中状态 */
|
||||||
|
loading: Ref<boolean>
|
||||||
|
/** 上传进度(0-100) */
|
||||||
|
progress: Ref<number>
|
||||||
|
/** 上传进度回调 */
|
||||||
|
onProgress?: (progress: number) => void
|
||||||
|
/** 上传成功回调 */
|
||||||
|
onSuccess?: (res: UniApp.UploadFileSuccessCallbackResult) => void
|
||||||
|
/** 上传失败回调 */
|
||||||
|
onError?: (err: Error | UniApp.GeneralCallbackResult) => void
|
||||||
|
/** 上传完成回调 */
|
||||||
|
onComplete?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行文件上传
|
||||||
|
* @template T 上传成功后返回的数据类型
|
||||||
|
* @param options 上传选项
|
||||||
|
*/
|
||||||
|
function uploadFile<T>({
|
||||||
|
url,
|
||||||
|
tempFilePath,
|
||||||
|
formData,
|
||||||
|
data,
|
||||||
|
error,
|
||||||
|
loading,
|
||||||
|
progress,
|
||||||
|
onProgress,
|
||||||
|
onSuccess,
|
||||||
|
onError,
|
||||||
|
onComplete,
|
||||||
|
}: UploadFileOptions<T>) {
|
||||||
|
try {
|
||||||
|
// 创建上传任务
|
||||||
|
const uploadTask = uni.uploadFile({
|
||||||
|
url,
|
||||||
|
filePath: tempFilePath,
|
||||||
|
name: 'file', // 文件对应的 key
|
||||||
|
formData,
|
||||||
|
header: {
|
||||||
|
// H5环境下不需要手动设置Content-Type,让浏览器自动处理multipart格式
|
||||||
|
// #ifndef H5
|
||||||
|
'Content-Type': 'multipart/form-data',
|
||||||
|
// #endif
|
||||||
|
},
|
||||||
|
// 确保文件名称合法
|
||||||
|
success: (uploadFileRes) => {
|
||||||
|
try {
|
||||||
|
// 解析响应数据
|
||||||
|
const result = JSON.parse(uploadFileRes.data)
|
||||||
|
if (result.code === 1) {
|
||||||
|
// 上传成功
|
||||||
|
data.value = result.data as T
|
||||||
|
onSuccess?.(uploadFileRes)
|
||||||
|
} else {
|
||||||
|
// 业务错误
|
||||||
|
const err = new Error(result.message || '上传失败')
|
||||||
|
error.value = true
|
||||||
|
onError?.(err)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// 响应解析错误
|
||||||
|
console.error('解析上传响应失败:', err)
|
||||||
|
error.value = true
|
||||||
|
onError?.(new Error('上传响应解析失败'))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fail: (err) => {
|
||||||
|
// 上传请求失败
|
||||||
|
console.error('上传文件失败:', err)
|
||||||
|
error.value = true
|
||||||
|
onError?.(err)
|
||||||
|
},
|
||||||
|
complete: () => {
|
||||||
|
// 无论成功失败都执行
|
||||||
|
loading.value = false
|
||||||
|
onComplete?.()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// 监听上传进度
|
||||||
|
uploadTask.onProgressUpdate((res) => {
|
||||||
|
progress.value = res.progress
|
||||||
|
onProgress?.(res.progress)
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
// 创建上传任务失败
|
||||||
|
console.error('创建上传任务失败:', err)
|
||||||
|
error.value = true
|
||||||
|
loading.value = false
|
||||||
|
onError?.(new Error('创建上传任务失败'))
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user