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

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", "version" : "1.0",
"configurations" : [ "configurations" : [
{ {
"customPlaygroundType" : "device",
"playground" : "standard", "playground" : "standard",
"type" : "uni-app:app-ios" "type" : "uni-app:app-ios"
},
{
"playground" : "standard",
"type" : "uni-app:app-android"
} }
] ]
} }

View File

@@ -1,4 +1,3 @@
// @ts-check
module.exports = { module.exports = {
printWidth: 74, 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> <script setup>
import { reactive, computed } from 'vue' import { reactive, computed } from 'vue'
import { reLaunch } from '@/utils/router' 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({ const props = defineProps({
/** /**
@@ -17,32 +27,67 @@
const isPhone = computed(() => props.type === 'phone') const isPhone = computed(() => props.type === 'phone')
const formData = reactive({ const formData = reactive({
// 手机号 // 手机号、邮箱
name: '', name: '',
// 验证码
code: '',
// 密码 // 密码
password: '', password: '',
// 确认密码 // 确认密码
confirmPassword: '', confirmPassword: '',
// 邀请码 // 邀请码
inviteCode: '', invitationCode: '54321',
agreement: false agreement: true
}) })
const isBtn = computed(() => { const isBtn = computed(() => {
return ( return (
formData.name && formData.name &&
formData.code &&
formData.password && formData.password &&
formData.confirmPassword && formData.confirmPassword &&
formData.inviteCode && formData.invitationCode &&
!formData.agreement !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 = () => { const onLogin = () => {
@@ -73,12 +118,12 @@
icon="3" icon="3"
:placeholder="`请输入${isPhone ? '手机号' : '邮箱'}`" :placeholder="`请输入${isPhone ? '手机号' : '邮箱'}`"
></cb-input> ></cb-input>
<cb-input <!-- <cb-input
v-model="formData.code" v-model="formData.code"
type="number" type="number"
icon="6" icon="6"
placeholder="请输入验证码" placeholder="请输入验证码"
></cb-input> ></cb-input> -->
<cb-input <cb-input
v-model="formData.password" v-model="formData.password"
type="password" type="password"
@@ -92,7 +137,7 @@
placeholder="请输入确认密码" placeholder="请输入确认密码"
></cb-input> ></cb-input>
<cb-input <cb-input
v-model="formData.inviteCode" v-model="formData.invitationCode"
type="number" type="number"
icon="4" icon="4"
placeholder="请输入邀请码" placeholder="请输入邀请码"

View File

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

View File

@@ -33,6 +33,49 @@
"navigationBarTitleText": "忘记密码", "navigationBarTitleText": "忘记密码",
"navigationStyle": "custom" "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": { "globalStyle": {
@@ -41,5 +84,37 @@
"navigationBarBackgroundColor": "#F8F8F8", "navigationBarBackgroundColor": "#F8F8F8",
"backgroundColor": "#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> <script setup>
import { onMounted, ref } from 'vue' import { onLoad } from '@dcloudio/uni-app'
const cb = v => { import { onMounted, ref } from 'vue'
console.log(v) import { reLaunch } from '@/utils/router'
} const cb = v => {
console.log(v)
}
// export default { // export default {
// data() { // data() {
// return { // return {
// title: 'Hello' // title: 'Hello'
// } // }
// }, // },
// onLoad() { // onLoad() {
// }, // },
// methods: { // methods: {
// } // }
// } // }
const title = ref('你112好')
onLoad(() => {
// 3秒后跳转
setTimeout(() => {
reLaunch('/pages/news-list/news-list')
}, 3000)
})
const title = ref('这个是启动页')
</script> </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> <style>
.content { .content {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
} }
.logo { .logo {
height: 200rpx; height: 200rpx;
width: 200rpx; width: 200rpx;
margin-top: 200rpx; margin-top: 200rpx;
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
margin-bottom: 50rpx; margin-bottom: 50rpx;
} }
.text-area { .text-area {
display: flex; display: flex;
justify-content: center; justify-content: center;
} }
.title { .title {
font-size: 36rpx; font-size: 36rpx;
color: #8f8f94; color: #8f8f94;
} }
</style> </style>

View File

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

View File

@@ -3,8 +3,13 @@
import { onLoad } from '@dcloudio/uni-app' import { onLoad } from '@dcloudio/uni-app'
import { useUI } from '@/utils/use-ui' import { useUI } from '@/utils/use-ui'
import { reLaunch, navigateTo } from '@/utils/router' import { reLaunch, navigateTo } from '@/utils/router'
import { userLogin } from '@/api'
import { useTokenStore } from '@/stores/token'
import { useUserStore } from '@/stores/user'
const { showToast } = useUI() const { showToast } = useUI()
const { setToken } = useTokenStore()
const { fetchUserInfo } = useUserStore()
const formData = reactive({ const formData = reactive({
username: '', username: '',
@@ -12,9 +17,18 @@
agreement: false agreement: false
}) })
const onLogin = () => { const onLogin = async () => {
showToast('登录成功') if (!formData.agreement) {
console.log('登录:', formData) 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 = () => { const onRegister = () => {
@@ -26,7 +40,7 @@
} }
onLoad(e => { onLoad(e => {
console.log('接收==', e.id) console.log('接收参数,返回对应页面的时候使用', e)
}) })
</script> </script>
@@ -50,9 +64,7 @@
<agreement-checkbox v-model="formData.agreement" /> <agreement-checkbox v-model="formData.agreement" />
<cb-button <cb-button
class="bottom-btn" class="bottom-btn"
:disabled=" :disabled="!formData.username || !formData.password"
!formData.username || !formData.password || !formData.agreement
"
@click="onLogin" @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 { defineStore } from 'pinia'
import { ref } from 'vue'; import { ref } from 'vue'
import { STORAGE_KEYS } from '@/constants/storage-keys'; import { STORAGE_KEYS } from '@/constants/storage-keys'
import { getToken, removeToken } from '@/utils/storage'; import { setTokenData, getToken, removeToken } from '@/utils/storage'
/** 登录状态 */ /** 登录状态 */
export const useTokenStore = defineStore(STORAGE_KEYS.TOKEN, () => { export const useTokenStore = defineStore(STORAGE_KEYS.TOKEN, () => {
// 从本地存储获取token
const token = ref(getToken() || null)
const isLogin = ref(!!token.value)
// 从本地存储获取token /** 设置token并保存到本地 */
const token = ref(getToken() || null); const setToken = newToken => {
const isLogin = ref(!!token.value); token.value = newToken
isLogin.value = true
setTokenData(newToken)
}
/** 设置token并保存到本地 */ /** 清除token */
const setToken = (newToken) => { const clearToken = () => {
token.value = newToken; token.value = null
isLogin.value = true; isLogin.value = false
setToken(newToken); removeToken()
}; }
/** 清除token */ /** 检查token是否有效(可扩展过期时间判断) */
const clearToken = () => { const checkToken = () => {
token.value = null; // 简单判断token存在且不为空
isLogin.value = false; return !!token.value
removeToken() }
};
/** 检查token是否有效(可扩展过期时间判断) */ /** 验证token是否过期(实际项目中可添加过期时间判断) */
const checkToken = () => { const isTokenExpired = () => {
// 简单判断token存在且不为空 // 示例如果token中包含过期时间这里可以判断
return !!token.value; // 例如const expireTime = parseInt(token.value.split('.')[1]);
}; // return Date.now() > expireTime * 1000;
return false // 默认不过期
}
/** 验证token是否过期实际项目中可添加过期时间判断 */ return {
const isTokenExpired = () => { token,
// 示例如果token中包含过期时间这里可以判断 isLogin,
// 例如const expireTime = parseInt(token.value.split('.')[1]); setToken,
// return Date.now() > expireTime * 1000; clearToken,
return false; // 默认不过期 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 请求参数 * @param {Object} options 请求参数
* @returns {Promise} * @returns {Promise}
*/ */
const request = (options) => { const request = options => {
// 默认配置 // 默认配置
const defaultOptions = { const defaultOptions = {
url: '', url: '',
method: 'GET', method: 'GET',
data: {}, data: {},
header: { header: {
'Content-Type': 'application/json' // 默认请求内容类型 'Content-Type': 'application/json' // 默认请求内容类型
} }
} }
// 合并配置 // 合并配置
const config = { ...defaultOptions, ...options } const config = { ...defaultOptions, ...options }
// 请求拦截添加token等通用header // 请求拦截添加token等通用header
if (getToken()) { if (getToken()) {
config.header['Authorization'] = 'Bearer ' + getToken() config.header['Authorization'] = 'Bearer ' + getToken()
} }
// 显示加载状态(可选) // 显示加载状态(可选)
if (options.loading !== false) { if (options.loading !== false) {
uni.showLoading({ uni.showLoading({
title: '加载中...', title: '加载中...',
mask: true mask: true
}) })
}; }
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
uni.request({ uni.request({
url: BASE_URL + config.url, url: BASE_URL + config.url,
method: config.method, method: config.method,
data: config.data, data: config.data,
timeout: 10000, // 请求超时时间 timeout: 10000, // 请求超时时间
header: config.header, header: config.header,
success: (response) => { success: response => {
// 响应拦截:根据状态码处理 console.log(response)
if (response.statusCode === 200) { // 响应拦截:根据状态码处理
// 这里可以根据后端数据格式调整 if (response.statusCode === 200) {
// 例如if (response.data.code === 0) {...} // 这里可以根据后端数据格式调整
resolve(response.data) // 例如if (response.data.code === 0) {...}
} else { if (response.data.code === 200) {
// 状态码错误处理 resolve(response.data)
handleError(response.statusCode, response.data) } else {
reject(response) handleError(response.data.code, response.data)
} }
}, } else {
fail: (error) => { // 状态码错误处理
// 网络错误处理 handleError(response.statusCode, response.data)
uni.showToast({ reject(response)
title: '网络异常,请检查网络连接', }
icon: 'none', },
duration: 2000 fail: error => {
}) // 网络错误处理
reject(error) uni.showToast({
}, title: '网络异常,请检查网络连接',
complete: () => { icon: 'none',
// 隐藏加载状态 duration: 2000,
if (options.loading !== false) { mask: true
uni.hideLoading() })
} reject(error)
} },
}) complete: () => {
}) // 隐藏加载状态
if (options.loading !== false) {
uni.hideLoading()
}
}
})
})
} }
/** /**
@@ -78,49 +84,53 @@ const request = (options) => {
* @param {Object} data 响应数据 * @param {Object} data 响应数据
*/ */
const handleError = (statusCode, data) => { const handleError = (statusCode, data) => {
switch (statusCode) { switch (statusCode) {
case 401: case 401:
uni.showModal({ uni.showModal({
title: '提示', title: '提示',
content: '登录已过期,请重新登录', content: '登录已过期,请重新登录',
showCancel: false, showCancel: false,
success: () => { success: () => {
// 清除本地存储的token并跳转到登录页 // 清除本地存储的token并跳转到登录页
removeToken() removeToken()
uni.navigateTo({ uni.navigateTo({
url: '/pages/login/index' url: '/pages/login/index'
}) })
} }
}) })
break break
case 403: case 403:
uni.showToast({ uni.showToast({
title: '没有权限访问', title: '没有权限访问',
icon: 'none', icon: 'none',
duration: 2000 duration: 2000,
}) mask: true
break })
case 404: break
uni.showToast({ case 404:
title: '请求资源不存在', uni.showToast({
icon: 'none', title: '请求资源不存在',
duration: 2000 icon: 'none',
}) duration: 2000,
break mask: true
case 500: })
uni.showToast({ break
title: '服务器内部错误', case 500:
icon: 'none', uni.showToast({
duration: 2000 title: data.msg || '服务器内部错误',
}) icon: 'none',
break duration: 2000,
default: mask: true
uni.showToast({ })
title: data.message || '请求失败,请重试', break
icon: 'none', default:
duration: 2000 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' import { STORAGE_KEYS } from '@/constants/storage-keys'
/** 保存 token */ /** 保存 token */
export const setToken = (v) => { export const setTokenData = v => {
return uni.setStorageSync(STORAGE_KEYS.TOKEN, v) return uni.setStorageSync(STORAGE_KEYS.TOKEN, v)
} }
/** 获取 token */ /** 获取 token */
export const getToken = () => { export const getToken = () => {
return uni.getStorageSync(STORAGE_KEYS.TOKEN) || '' return uni.getStorageSync(STORAGE_KEYS.TOKEN) || ''
} }
/** 清楚 token */ /** 清楚 token */
export const removeToken = () => { 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 === 'error') icon = 'error'
if (type === 'warning') icon = 'none' if (type === 'warning') icon = 'none'
uni.showToast({ return new Promise(resolve => {
title: message, uni.showToast({
icon, title: message,
duration, icon,
mask: true 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: '' }
}