需要修复商城顶部筛选左右滑动问题

This commit is contained in:
cbb
2025-12-24 17:53:13 +08:00
parent 6f418fae8a
commit b67f9611c7
48 changed files with 1067 additions and 221 deletions

View File

@@ -2,8 +2,13 @@
"version" : "1.0",
"configurations" : [
{
"customPlaygroundType" : "device",
"playground" : "standard",
"type" : "uni-app:app-ios"
},
{
"playground" : "standard",
"type" : "uni-app:app-android"
}
]
}

View File

@@ -1,4 +1,3 @@
// @ts-check
module.exports = {
printWidth: 74,

View File

@@ -1 +1,27 @@
// 各模块api
import http from '@/utils/request'
/** 注册 */
export const userRegister = data => {
return http({
url: '/api/register',
method: 'post',
data
})
}
/** 登录 */
export const userLogin = data => {
return http({
url: '/api/login',
method: 'post',
data
})
}
/** 获取用户信息 */
export const getUserData = () => {
return http({
url: '/api/userInfo',
method: 'get'
})
}

View File

@@ -0,0 +1,67 @@
<script setup>
const props = defineProps()
const placeholderStyle = `font-family: PingFang SC, PingFang SC; font-weight: 500; color: #666666; font-size: 24rpx; font-style: normal; text-transform: none;`
const name = defineModel({
type: String,
default: ''
})
</script>
<template>
<view class="cb-search">
<image
src="/static/images/public/search.png"
mode="heightFix"
class="left-icon"
></image>
<input
v-model="name"
:placeholder-style="placeholderStyle"
placeholder="请输入内容"
class="search-box"
/>
<button class="search-btn">搜索</button>
</view>
</template>
<style lang="scss" scoped>
.cb-search {
height: 64rpx;
display: flex;
align-items: center;
background: #f9f9f9;
border-radius: 64rpx;
padding: 0 0 0 32rpx;
.left-icon {
height: 48rpx;
flex-shrink: 0;
margin-right: 8rpx;
}
.search-box {
width: 100%;
font-size: 24rpx;
color: #333333;
}
.search-btn {
margin: 0 8rpx;
flex-shrink: 0;
width: 120rpx;
height: 56rpx;
line-height: 56rpx;
background: linear-gradient(180deg, #00d993 0%, #00d9c5 100%);
border-radius: 64rpx;
font-family: PingFang SC, PingFang SC;
font-weight: 500;
font-size: 28rpx;
color: #ffffff;
text-align: center;
font-style: normal;
text-transform: none;
&::after {
border: none;
}
}
}
</style>

View File

@@ -1,6 +1,16 @@
<script setup>
import { reactive, computed } from 'vue'
import { reLaunch } from '@/utils/router'
import {
validatePhone,
validateEmail,
validatePassword,
validateConfirmPassword
} from '@/utils/validate'
import { useUI } from '@/utils/use-ui'
import { userRegister } from '@/api'
const { showToast } = useUI()
const props = defineProps({
/**
@@ -17,32 +27,67 @@
const isPhone = computed(() => props.type === 'phone')
const formData = reactive({
// 手机号
// 手机号、邮箱
name: '',
// 验证码
code: '',
// 密码
password: '',
// 确认密码
confirmPassword: '',
// 邀请码
inviteCode: '',
agreement: false
invitationCode: '54321',
agreement: true
})
const isBtn = computed(() => {
return (
formData.name &&
formData.code &&
formData.password &&
formData.confirmPassword &&
formData.inviteCode &&
formData.invitationCode &&
!formData.agreement
)
})
const onRegister = () => {
console.log('注册')
/** 注册 */
const onRegister = async () => {
if (isPhone.value) {
const phoneValue = validatePhone(formData.name)
if (!phoneValue.valid) {
showToast(phoneValue.message)
return
}
} else {
const emailValue = validateEmail(formData.name)
if (!emailValue.valid) {
showToast(emailValue.message)
return
}
}
const passwordValue = validatePassword(formData.password)
if (!passwordValue.valid) {
showToast(passwordValue.message)
return
}
const confirmPasswordValue = validateConfirmPassword(
formData.password,
formData.confirmPassword
)
if (!confirmPasswordValue.valid) {
showToast(confirmPasswordValue.message)
return
}
const data = {
type: isPhone.value ? 2 : 1,
mobile: formData.name,
password: formData.password,
invitationCode: formData.invitationCode
}
await userRegister(data)
await showToast('注册成功', 'success')
onLogin()
}
const onLogin = () => {
@@ -73,12 +118,12 @@
icon="3"
:placeholder="`请输入${isPhone ? '手机号' : '邮箱'}`"
></cb-input>
<cb-input
<!-- <cb-input
v-model="formData.code"
type="number"
icon="6"
placeholder="请输入验证码"
></cb-input>
></cb-input> -->
<cb-input
v-model="formData.password"
type="password"
@@ -92,7 +137,7 @@
placeholder="请输入确认密码"
></cb-input>
<cb-input
v-model="formData.inviteCode"
v-model="formData.invitationCode"
type="number"
icon="4"
placeholder="请输入邀请码"

View File

@@ -1,3 +1,4 @@
export const STORAGE_KEYS = {
TOKEN: 'token',
};
USER: 'userInfo'
}

View File

@@ -33,6 +33,49 @@
"navigationBarTitleText": "忘记密码",
"navigationStyle": "custom"
}
},
{
"path": "pages/news-list/news-list",
"style": {
"navigationBarTitleText": "消息"
}
},
{
"path": "pages/contacts/contacts",
"style": {
"navigationBarTitleText": "通讯录"
}
},
{
"path": "pages/discover/discover",
"style": {
"navigationBarTitleText": "发现"
}
},
{
"path": "pages/my-index/my-index",
"style": {
"navigationBarTitleText": "我的",
"backgroundColor": "#f7f7f7"
}
},
{
"path": "pages/mall/list",
"style": {
"navigationBarTitleText": "商城"
}
},
{
"path": "pages/mall/detail",
"style": {
"navigationBarTitleText": "商品详情"
}
},
{
"path": "pages/mall/confirm-order",
"style": {
"navigationBarTitleText": "确认订单"
}
}
],
"globalStyle": {
@@ -41,5 +84,37 @@
"navigationBarBackgroundColor": "#F8F8F8",
"backgroundColor": "#F8F8F8"
},
"uniIdRouter": {}
"uniIdRouter": {},
"tabBar": {
"color": "#333333",
"selectedColor": "#00D993",
"borderStyle": "black",
"backgroundColor": "#ffffff",
"list": [
{
"pagePath": "pages/news-list/news-list",
"iconPath": "static/images/tabBar/news.png",
"selectedIconPath": "static/images/tabBar/newsHL.png",
"text": "消息"
},
{
"pagePath": "pages/contacts/contacts",
"iconPath": "static/images/tabBar/contacts.png",
"selectedIconPath": "static/images/tabBar/contactsHL.png",
"text": "通讯录"
},
{
"pagePath": "pages/discover/discover",
"iconPath": "static/images/tabBar/discover.png",
"selectedIconPath": "static/images/tabBar/discoverHL.png",
"text": "发现"
},
{
"pagePath": "pages/my-index/my-index",
"iconPath": "static/images/tabBar/my.png",
"selectedIconPath": "static/images/tabBar/myHL.png",
"text": "我的"
}
]
}
}

View File

@@ -0,0 +1,7 @@
<script setup></script>
<template>
<view class="contacts">通讯录</view>
</template>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,75 @@
<script setup>
import { navigateTo } from '@/utils/router'
const btnList = [
{ name: '等级排行榜', icon: 'grade' },
{ name: '签到', icon: 'sign' },
{ name: '公司介绍', icon: 'company' },
{ name: '朋友圈', icon: 'circle' },
{ name: '线上商城', icon: 'mall' },
{ name: '我的拼团', icon: 'team' },
{ name: '项目入口', icon: 'project' }
]
const onGo = item => {
if (item === 'mall') {
navigateTo('/pages/mall/list')
}
}
</script>
<template>
<view class="discover-box">
<view
v-for="(item, index) in btnList"
:key="index"
class="card-box"
@click="onGo(item.icon)"
>
<view class="left-box">
<image
:src="`/static/images/discover/${item.icon}.png`"
mode="heightFix"
class="icon"
></image>
<text>{{ item.name }}</text>
</view>
<image
src="/static/images/public/right-arrow.png"
mode="heightFix"
class="right-box"
></image>
</view>
</view>
</template>
<style lang="scss" scoped>
.discover-box {
padding: 32rpx 24rpx;
.card-box {
padding: 20rpx 32rpx;
display: flex;
justify-content: space-between;
align-items: center;
.left-box {
display: flex;
align-items: center;
.icon {
height: 80rpx;
margin-right: 16rpx;
}
text {
font-family: PingFang SC, PingFang SC;
font-weight: 500;
font-size: 32rpx;
color: #333333;
font-style: normal;
text-transform: none;
}
}
.right-box {
height: 32rpx;
}
}
}
</style>

View File

@@ -1,59 +1,67 @@
<template>
<view class="content">
<image class="logo" src="/static/logo.png"></image>
<view class="text-area">
<text class="title">{{ title }}</text>
</view>
</view>
</template>
<script setup>
import { onMounted, ref } from 'vue'
const cb = v => {
console.log(v)
}
import { onLoad } from '@dcloudio/uni-app'
import { onMounted, ref } from 'vue'
import { reLaunch } from '@/utils/router'
const cb = v => {
console.log(v)
}
// export default {
// data() {
// return {
// title: 'Hello'
// }
// },
// onLoad() {
// export default {
// data() {
// return {
// title: 'Hello'
// }
// },
// onLoad() {
// },
// methods: {
// },
// methods: {
// }
// }
const title = ref('你112好')
// }
// }
onLoad(() => {
// 3秒后跳转
setTimeout(() => {
reLaunch('/pages/news-list/news-list')
}, 3000)
})
const title = ref('这个是启动页')
</script>
<template>
<view class="content">
<image class="logo" src="/static/logo.png"></image>
<view class="text-area">
<text class="title">{{ title }}</text>
</view>
</view>
</template>
<style>
.content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.logo {
height: 200rpx;
width: 200rpx;
margin-top: 200rpx;
margin-left: auto;
margin-right: auto;
margin-bottom: 50rpx;
}
.logo {
height: 200rpx;
width: 200rpx;
margin-top: 200rpx;
margin-left: auto;
margin-right: auto;
margin-bottom: 50rpx;
}
.text-area {
display: flex;
justify-content: center;
}
.text-area {
display: flex;
justify-content: center;
}
.title {
font-size: 36rpx;
color: #8f8f94;
}
.title {
font-size: 36rpx;
color: #8f8f94;
}
</style>

View File

@@ -1,5 +1,8 @@
<script setup>
import { ref, reactive } from 'vue'
import { useUI } from '@/utils/use-ui'
const { showToast } = useUI()
const formData = reactive({
username: '',
@@ -15,6 +18,16 @@
}
const onBottomBtn = () => {
if (!formData.username) {
showToast('请输入手机号/邮箱')
return
}
if (!formData.password) {
showToast('请输入密码')
return
}
console.log('确认')
}
</script>
@@ -38,14 +51,14 @@
v-model="formData.username"
placeholder="请输入手机号/邮箱"
></cb-input>
<cb-input
<!-- <cb-input
v-model="formData.code"
v-model:code="isCode"
type="number"
icon="6"
placeholder="请输入验证码"
@onGetCode="getCode"
></cb-input>
></cb-input> -->
<cb-input
v-model="formData.password"
type="password"

View File

@@ -3,8 +3,13 @@
import { onLoad } from '@dcloudio/uni-app'
import { useUI } from '@/utils/use-ui'
import { reLaunch, navigateTo } from '@/utils/router'
import { userLogin } from '@/api'
import { useTokenStore } from '@/stores/token'
import { useUserStore } from '@/stores/user'
const { showToast } = useUI()
const { setToken } = useTokenStore()
const { fetchUserInfo } = useUserStore()
const formData = reactive({
username: '',
@@ -12,9 +17,18 @@
agreement: false
})
const onLogin = () => {
showToast('登录成功')
console.log('登录:', formData)
const onLogin = async () => {
if (!formData.agreement) {
showToast('请同意协议')
return
}
const res = await userLogin({
account: formData.username,
password: formData.password
})
setToken(res.token)
await fetchUserInfo()
reLaunch('/pages/news-list/news-list')
}
const onRegister = () => {
@@ -26,7 +40,7 @@
}
onLoad(e => {
console.log('接收==', e.id)
console.log('接收参数,返回对应页面的时候使用', e)
})
</script>
@@ -50,9 +64,7 @@
<agreement-checkbox v-model="formData.agreement" />
<cb-button
class="bottom-btn"
:disabled="
!formData.username || !formData.password || !formData.agreement
"
:disabled="!formData.username || !formData.password"
@click="onLogin"
>
登录

View File

@@ -0,0 +1,7 @@
<script setup></script>
<template>
<view class="mall-confirm-order">确认订单</view>
</template>
<style lang="scss" scoped></style>

7
pages/mall/detail.vue Normal file
View File

@@ -0,0 +1,7 @@
<script setup></script>
<template>
<view class="mall-detail">商品详情</view>
</template>
<style lang="scss" scoped></style>

78
pages/mall/list.vue Normal file
View File

@@ -0,0 +1,78 @@
<script setup>
import { reactive } from 'vue'
// 顶部分类选项
const topNavOptions = [
{ name: '全部', value: '0' },
{ name: '休闲零食', value: '1' },
{ name: '中外名酒', value: '2' },
{ name: '家用洗漱', value: '3' },
{ name: '家电家具', value: '4' },
{ name: '电子产品', value: '5' },
{ name: '户外用品', value: '6' }
]
const formData = reactive({
name: '',
type: '0'
})
const onTop = value => {
formData.type = value
}
</script>
<template>
<view class="mall-list">
<cb-search v-model="formData.name"></cb-search>
<view class="top-options">
<view
v-for="item in topNavOptions"
:key="item.value"
:class="{ active: item.value === formData.type }"
class="text"
@click="onTop(item.value)"
>
{{ item.name }}
</view>
</view>
</view>
</template>
<style lang="scss" scoped>
.mall-list {
padding: 24rpx;
.top-options {
overflow: hidden;
margin-top: 32rpx;
display: flex;
flex-direction: row;
white-space: nowrap; /* 重要:防止换行 */
-webkit-overflow-scrolling: touch; /* iOS 平滑滚动 */
.text + .text {
margin-left: 16rpx;
}
.text {
flex-shrink: 0;
padding: 8rpx 16rpx;
font-family: PingFang SC, PingFang SC;
font-weight: 500;
font-size: 28rpx;
color: #999999;
text-align: center;
font-style: normal;
text-transform: none;
background: #f4f4f4;
border-radius: 64rpx;
box-sizing: border-box;
}
.active {
padding: 6rpx 14rpx;
border-radius: 64rpx;
border: 2rpx solid #00d993;
color: #00d993;
}
}
}
</style>

221
pages/my-index/my-index.vue Normal file
View File

@@ -0,0 +1,221 @@
<script setup>
import { useUserStore } from '@/stores/user'
const bottomList = [
{ name: '我的钱包', icon: 'wallet' },
{ name: '我的团队', icon: 'team' },
{ name: '会议记录', icon: 'meeting' },
{ name: '我的朋友圈', icon: 'circle' },
{ name: '我的收藏', icon: 'collection' },
{ name: '在线客服', icon: 'customer' },
{ name: '系统设置', icon: 'system' }
]
const { userInfo } = useUserStore()
console.log(userInfo)
</script>
<template>
<view class="my-index">
<view class="top-info">
<view class="left-box">
<image
src="https://wx1.sinaimg.cn/mw690/92eeb099gy1i29hl0ne80j21jk2bcash.jpg"
mode="scaleToFill"
class="avatar"
></image>
<view class="nickname">
<text class="name">{{ userInfo.userName }}</text>
<text class="name">ID:{{ userInfo.userId }}</text>
</view>
</view>
<image
src="/static/images/public/right-arrow.png"
mode="heightFix"
class="right-box"
></image>
</view>
<!-- 卡片列表 -->
<view class="card-list">
<view class="top-box">
<view class="left-name">
<text>账户积分</text>
<text>2933</text>
</view>
<view class="right-btn">
<button>充值</button>
<button>提现</button>
</view>
</view>
<!-- 入口列表 -->
<view
v-for="(item, index) in bottomList"
:key="index"
class="item-box"
>
<view class="item-name">
<image
:src="`/static/images/my-index/${item.icon}.png`"
mode="heightFix"
class="icon"
></image>
<text>{{ item.name }}</text>
</view>
<image
src="/static/images/public/right-arrow.png"
mode="heightFix"
class="right-box"
></image>
</view>
</view>
</view>
</template>
<style lang="scss" scoped>
page {
background-color: #f7f7f7;
}
.my-index {
padding: 32rpx 26rpx;
.right-box {
height: 32rpx;
}
.top-info {
padding: 32rpx 46rpx;
background: #ffffff;
height: 192rpx;
border-radius: 16rpx;
display: flex;
justify-content: space-between;
align-items: center;
.left-box {
display: flex;
align-items: center;
.avatar {
width: 128rpx;
height: 128rpx;
border-radius: 128rpx;
margin-right: 32rpx;
}
.nickname {
display: flex;
flex-direction: column;
.name {
font-family: PingFang SC, PingFang SC;
font-weight: bold;
font-size: 32rpx;
color: #333333;
text-align: left;
font-style: normal;
text-transform: none;
&:last-child {
font-weight: 500;
font-size: 24rpx;
color: #999999;
margin-top: 16rpx;
}
}
}
}
}
.card-list {
margin-top: 28rpx;
background: #ffffff;
border-radius: 16rpx;
overflow: hidden;
.top-box {
padding: 12rpx 36rpx;
display: flex;
justify-content: space-between;
align-items: center;
background: linear-gradient(180deg, #00d993 0%, #00d9c5 100%);
position: relative;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-image: url('/static/images/my-index/my-card-bg.png');
background-size: 30%;
background-position: center;
background-repeat: no-repeat;
z-index: 1;
pointer-events: none;
}
.left-name {
display: flex;
flex-direction: column;
text {
font-family: PingFang SC, PingFang SC;
font-weight: 500;
font-size: 28rpx;
color: #ffffff;
text-align: center;
font-style: normal;
text-transform: none;
&:last-child {
font-weight: bold;
font-size: 40rpx;
margin-top: 10rpx;
}
}
}
.right-btn {
display: flex;
button {
width: 128rpx;
height: 64rpx;
line-height: 64rpx;
border-radius: 100rpx 100rpx 100rpx 100rpx;
font-family: PingFang SC, PingFang SC;
font-weight: 500;
font-size: 28rpx;
color: #ffffff;
text-align: center;
font-style: normal;
text-transform: none;
background: transparent;
border: 2rpx solid #ffffff;
&::after {
border: none;
}
&:last-child {
margin-left: 16rpx;
background: #ffffff;
color: #00d993;
border: none;
}
}
}
}
.item-box {
padding: 23rpx 32rpx;
display: flex;
justify-content: space-between;
align-items: center;
.item-name {
display: flex;
justify-content: space-between;
align-items: center;
.icon {
height: 64rpx;
margin-right: 16rpx;
}
text {
font-family: PingFang SC, PingFang SC;
font-weight: 500;
font-size: 32rpx;
color: #333333;
font-style: normal;
text-transform: none;
}
}
}
}
}
</style>

View File

@@ -0,0 +1,12 @@
<script setup>
import { useUserStore } from '@/stores/user'
const { userInfo } = useUserStore()
console.log(userInfo.userId, '====userInfo===')
</script>
<template>
<view class="news-list">消息列表</view>
</template>
<style lang="scss" scoped></style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 331 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
static/images/tabBar/my.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -1,42 +1,48 @@
import { defineStore } from 'pinia';
import { ref } from 'vue';
import { STORAGE_KEYS } from '@/constants/storage-keys';
import { getToken, removeToken } from '@/utils/storage';
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { STORAGE_KEYS } from '@/constants/storage-keys'
import { setTokenData, getToken, removeToken } from '@/utils/storage'
/** 登录状态 */
export const useTokenStore = defineStore(STORAGE_KEYS.TOKEN, () => {
// 从本地存储获取token
const token = ref(getToken() || null)
const isLogin = ref(!!token.value)
// 从本地存储获取token
const token = ref(getToken() || null);
const isLogin = ref(!!token.value);
/** 设置token并保存到本地 */
const setToken = newToken => {
token.value = newToken
isLogin.value = true
setTokenData(newToken)
}
/** 设置token并保存到本地 */
const setToken = (newToken) => {
token.value = newToken;
isLogin.value = true;
setToken(newToken);
};
/** 清除token */
const clearToken = () => {
token.value = null
isLogin.value = false
removeToken()
}
/** 清除token */
const clearToken = () => {
token.value = null;
isLogin.value = false;
removeToken()
};
/** 检查token是否有效(可扩展过期时间判断) */
const checkToken = () => {
// 简单判断token存在且不为空
return !!token.value
}
/** 检查token是否有效(可扩展过期时间判断) */
const checkToken = () => {
// 简单判断token存在且不为空
return !!token.value;
};
/** 验证token是否过期(实际项目中可添加过期时间判断) */
const isTokenExpired = () => {
// 示例如果token中包含过期时间这里可以判断
// 例如const expireTime = parseInt(token.value.split('.')[1]);
// return Date.now() > expireTime * 1000;
return false // 默认不过期
}
/** 验证token是否过期实际项目中可添加过期时间判断 */
const isTokenExpired = () => {
// 示例如果token中包含过期时间这里可以判断
// 例如const expireTime = parseInt(token.value.split('.')[1]);
// return Date.now() > expireTime * 1000;
return false; // 默认不过期
};
return { token, isLogin, setToken, clearToken, checkToken, isTokenExpired };
})
return {
token,
isLogin,
setToken,
clearToken,
checkToken,
isTokenExpired
}
})

70
stores/user.js Normal file
View File

@@ -0,0 +1,70 @@
import { defineStore } from 'pinia'
import {
getToken,
getUserInfoData,
setUserInfoData,
removeUserInfoData
} from '@/utils/storage'
import { useTokenStore } from './token'
import { getUserData } from '@/api'
import { ref } from 'vue'
export const useUserStore = defineStore('user', () => {
const { clearToken } = useTokenStore()
/** 用户信息对象 */
const userInfo = ref(JSON.parse(getUserInfoData()) || null)
/**
* 获取用户信息(可从缓存或接口)
*/
const fetchUserInfo = async () => {
// 示例:先尝试从本地缓存读取
const cachedToken = getToken()
const cachedUserInfo = getUserInfoData()
if (cachedToken && cachedUserInfo) {
userInfo.value = JSON.parse(cachedUserInfo)
return
}
const res = await getUserData()
await setUserInfo(res.data)
return
}
/**
* 设置用户信息
*/
const setUserInfo = async data => {
console.log('存储数据到userInfo==', data)
userInfo.value = data
// 同步到本地存储
setUserInfoData(data)
}
/**
* 清除用户信息(退出登录)
*/
const clearUserInfo = () => {
userInfo.value = null
clearToken()
removeUserInfoData()
}
/**
* 更新部分用户信息(例如昵称、头像)
*/
const updateUserInfo = partialData => {
if (!userInfo.value) return
userInfo.value = { ...userInfo.value, ...partialData }
setUserInfoData(userInfo.value)
}
return {
userInfo: userInfo.value,
fetchUserInfo,
setUserInfo,
clearUserInfo,
updateUserInfo
}
})

View File

@@ -1,75 +1,81 @@
import { getToken, removeToken } from './storage';
import { getToken, removeToken } from './storage'
const BASE_URL = 'xxxxx'
const BASE_URL = 'http://c36bd4b4.natappfree.cc'
/**
* 网络请求封装
* @param {Object} options 请求参数
* @returns {Promise}
*/
const request = (options) => {
// 默认配置
const defaultOptions = {
url: '',
method: 'GET',
data: {},
header: {
'Content-Type': 'application/json' // 默认请求内容类型
}
}
const request = options => {
// 默认配置
const defaultOptions = {
url: '',
method: 'GET',
data: {},
header: {
'Content-Type': 'application/json' // 默认请求内容类型
}
}
// 合并配置
const config = { ...defaultOptions, ...options }
// 合并配置
const config = { ...defaultOptions, ...options }
// 请求拦截添加token等通用header
if (getToken()) {
config.header['Authorization'] = 'Bearer ' + getToken()
}
// 请求拦截添加token等通用header
if (getToken()) {
config.header['Authorization'] = 'Bearer ' + getToken()
}
// 显示加载状态(可选)
if (options.loading !== false) {
uni.showLoading({
title: '加载中...',
mask: true
})
};
// 显示加载状态(可选)
if (options.loading !== false) {
uni.showLoading({
title: '加载中...',
mask: true
})
}
return new Promise((resolve, reject) => {
uni.request({
url: BASE_URL + config.url,
method: config.method,
data: config.data,
timeout: 10000, // 请求超时时间
header: config.header,
success: (response) => {
// 响应拦截:根据状态码处理
if (response.statusCode === 200) {
// 这里可以根据后端数据格式调整
// 例如if (response.data.code === 0) {...}
resolve(response.data)
} else {
// 状态码错误处理
handleError(response.statusCode, response.data)
reject(response)
}
},
fail: (error) => {
// 网络错误处理
uni.showToast({
title: '网络异常,请检查网络连接',
icon: 'none',
duration: 2000
})
reject(error)
},
complete: () => {
// 隐藏加载状态
if (options.loading !== false) {
uni.hideLoading()
}
}
})
})
return new Promise((resolve, reject) => {
uni.request({
url: BASE_URL + config.url,
method: config.method,
data: config.data,
timeout: 10000, // 请求超时时间
header: config.header,
success: response => {
console.log(response)
// 响应拦截:根据状态码处理
if (response.statusCode === 200) {
// 这里可以根据后端数据格式调整
// 例如if (response.data.code === 0) {...}
if (response.data.code === 200) {
resolve(response.data)
} else {
handleError(response.data.code, response.data)
}
} else {
// 状态码错误处理
handleError(response.statusCode, response.data)
reject(response)
}
},
fail: error => {
// 网络错误处理
uni.showToast({
title: '网络异常,请检查网络连接',
icon: 'none',
duration: 2000,
mask: true
})
reject(error)
},
complete: () => {
// 隐藏加载状态
if (options.loading !== false) {
uni.hideLoading()
}
}
})
})
}
/**
@@ -78,49 +84,53 @@ const request = (options) => {
* @param {Object} data 响应数据
*/
const handleError = (statusCode, data) => {
switch (statusCode) {
case 401:
uni.showModal({
title: '提示',
content: '登录已过期,请重新登录',
showCancel: false,
success: () => {
// 清除本地存储的token并跳转到登录页
removeToken()
uni.navigateTo({
url: '/pages/login/index'
})
}
})
break
case 403:
uni.showToast({
title: '没有权限访问',
icon: 'none',
duration: 2000
})
break
case 404:
uni.showToast({
title: '请求资源不存在',
icon: 'none',
duration: 2000
})
break
case 500:
uni.showToast({
title: '服务器内部错误',
icon: 'none',
duration: 2000
})
break
default:
uni.showToast({
title: data.message || '请求失败,请重试',
icon: 'none',
duration: 2000
})
}
switch (statusCode) {
case 401:
uni.showModal({
title: '提示',
content: '登录已过期,请重新登录',
showCancel: false,
success: () => {
// 清除本地存储的token并跳转到登录页
removeToken()
uni.navigateTo({
url: '/pages/login/index'
})
}
})
break
case 403:
uni.showToast({
title: '没有权限访问',
icon: 'none',
duration: 2000,
mask: true
})
break
case 404:
uni.showToast({
title: '请求资源不存在',
icon: 'none',
duration: 2000,
mask: true
})
break
case 500:
uni.showToast({
title: data.msg || '服务器内部错误',
icon: 'none',
duration: 2000,
mask: true
})
break
default:
uni.showToast({
title: data.msg || '请求失败,请重试',
icon: 'none',
duration: 2000,
mask: true
})
}
}
export default request
export default request

View File

@@ -1,16 +1,31 @@
import { STORAGE_KEYS } from '@/constants/storage-keys'
/** 保存 token */
export const setToken = (v) => {
return uni.setStorageSync(STORAGE_KEYS.TOKEN, v)
export const setTokenData = v => {
return uni.setStorageSync(STORAGE_KEYS.TOKEN, v)
}
/** 获取 token */
export const getToken = () => {
return uni.getStorageSync(STORAGE_KEYS.TOKEN) || ''
return uni.getStorageSync(STORAGE_KEYS.TOKEN) || ''
}
/** 清楚 token */
export const removeToken = () => {
return uni.removeStorageSync(STORAGE_KEYS.TOKEN)
}
return uni.removeStorageSync(STORAGE_KEYS.TOKEN)
}
/** 保存用户信息 */
export const setUserInfoData = v => {
return uni.setStorageSync(STORAGE_KEYS.USER, JSON.stringify(v))
}
/** 获取用户信息 */
export const getUserInfoData = () => {
return uni.getStorageSync(STORAGE_KEYS.USER) || ''
}
/** 删除用户信息 */
export const removeUserInfoData = () => {
return uni.removeStorageSync(STORAGE_KEYS.USER)
}

View File

@@ -37,11 +37,14 @@ const showToast = (message, type = 'none', duration = 2000) => {
if (type === 'error') icon = 'error'
if (type === 'warning') icon = 'none'
uni.showToast({
title: message,
icon,
duration,
mask: true
return new Promise(resolve => {
uni.showToast({
title: message,
icon,
duration,
mask: true
})
setTimeout(() => resolve(), duration)
})
}

84
utils/validate.js Normal file
View File

@@ -0,0 +1,84 @@
// 正则
/** 手机号正则(中国大陆) */
const PHONE_REGEX = /^1[3-9]\d{9}$/
/** 邮箱正则 */
const EMAIL_REGEX = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
/** 身份证号(简化版) */
const ID_CARD_REGEX = /(^\d{15}$)|(^\d{18}$)|(^\d{17}[\dXx]$)/
/** 密码强度至少8位包含数字和字母 */
const PASSWORD_REGEX = /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d@$!%*#?&]{8,}$/
/**
* 校验手机号
* @param {string} phone - 待校验的手机号
* @returns {{ valid: boolean, message: string }}
*/
export const validatePhone = phone => {
if (!phone) return { valid: false, message: '手机号不能为空' }
if (!PHONE_REGEX.test(phone)) {
return { valid: false, message: '手机号格式不正确' }
}
return { valid: true, message: '' }
}
/**
* 校验邮箱
* @param {string} email - 待校验的邮箱
* @returns {{ valid: boolean, message: string }}
*/
export const validateEmail = email => {
if (!email) return { valid: false, message: '邮箱不能为空' }
if (!EMAIL_REGEX.test(email)) {
return { valid: false, message: '邮箱格式不正确' }
}
return { valid: true, message: '' }
}
/**
* 校验密码强度
* @param {string} password - 待校验的密码
* @returns {{ valid: boolean, message: string }}
*/
export const validatePassword = password => {
if (!password) return { valid: false, message: '密码不能为空' }
if (password.length < 8) {
return { valid: false, message: '密码长度不能少于8位' }
}
if (!PASSWORD_REGEX.test(password)) {
return { valid: false, message: '密码需包含字母和数字' }
}
return { valid: true, message: '' }
}
/**
* 校验确认密码是否与原密码一致
* @param {string} password - 原始密码
* @param {string} confirmPassword - 确认密码
* @returns {{ valid: boolean, message: string }}
*/
export function validateConfirmPassword(password, confirmPassword) {
if (!confirmPassword) {
return { valid: false, message: '请再次输入密码' }
}
if (password !== confirmPassword) {
return { valid: false, message: '两次输入的密码不一致' }
}
return { valid: true, message: '' }
}
/**
* 校验身份证号(仅格式校验,不验证真实性)
* @param {string} idCard - 待校验的身份证号
* @returns {{ valid: boolean, message: string }}
*/
export const validateIdCard = idCard => {
if (!idCard) return { valid: false, message: '身份证号不能为空' }
if (!ID_CARD_REGEX.test(idCard)) {
return { valid: false, message: '身份证号格式不正确' }
}
return { valid: true, message: '' }
}