This commit is contained in:
那些花儿
2025-04-30 13:34:37 +08:00
parent ad9a7bc7b8
commit baa6f7284a
12 changed files with 567 additions and 284 deletions
+1 -2
View File
@@ -5,9 +5,8 @@ export default {
globalData: {
user: null
},
onLaunch: function () {
onLaunch: async function () {
console.log('App Launch')
console.log(process.env.VUE_APP_PLATFORM)
// 检查访问页面是否需要登录(不走拦截器 直接从地址栏进入)
checkAuth()
// 初始化路由拦截器
+9 -1
View File
@@ -1,4 +1,5 @@
import {createSSRApp} from "vue";
import * as Pinia from 'pinia';
import App from "./App.vue";
// 引入TDesign组件库
@@ -14,10 +15,17 @@ import 'moment/dist/locale/zh-cn'
moment.locale('zh-cn')
uni.$on('beforeRouterEnter', async (to, from, next) => {
console.log('beforeRouterEnter', to, from)
})
export function createApp() {
const app = createSSRApp(App);
app.use(TDesign); // 使用TDesign组件库
app.use(TDesign);
app.use(Pinia.createPinia())
return {
app,
Pinia
};
}
+1 -2
View File
@@ -152,11 +152,10 @@ const getListData = async (isRefresh = false) => {
hasMore.value = dataList.value.length < totalCount;
pageForm.pageNumber++;
} else {
Toast({message: '获取活动列表失败', theme: 'error'});
Toast({message: response.msg, theme: 'error'});
}
} catch (error) {
console.error('获取活动列表失败', error);
Toast({message: '获取活动列表失败', theme: 'error'});
} finally {
loading.value = false;
+69 -51
View File
@@ -3,13 +3,15 @@
<view class="title-area">
<div class="tech-animation">
<svg preserveAspectRatio="xMidYMid slice" viewBox="0 0 300 100">
<pattern id="pattern-hex" x="0" y="0" width="10" height="10" patternUnits="userSpaceOnUse" patternTransform="scale(0.8)">
<pattern id="pattern-hex" x="0" y="0" width="10" height="10" patternUnits="userSpaceOnUse"
patternTransform="scale(0.8)">
<rect x="0" y="0" width="10" height="10" fill="none"/>
<path d="M0,5 L2.5,0 L7.5,0 L10,5 L7.5,10 L2.5,10 Z" stroke-width="0.5" stroke="rgba(244, 90, 90, 0.2)" fill="rgba(244, 90, 90, 0.1)">
<animate attributeName="stroke-opacity" values="0.1;0.3;0.1" dur="3s" repeatCount="indefinite" />
<path d="M0,5 L2.5,0 L7.5,0 L10,5 L7.5,10 L2.5,10 Z" stroke-width="0.5" stroke="rgba(244, 90, 90, 0.2)"
fill="rgba(244, 90, 90, 0.1)">
<animate attributeName="stroke-opacity" values="0.1;0.3;0.1" dur="3s" repeatCount="indefinite"/>
</path>
</pattern>
<rect x="0" y="0" width="100%" height="100%" fill="url(#pattern-hex)" />
<rect x="0" y="0" width="100%" height="100%" fill="url(#pattern-hex)"/>
<g fill="none" stroke-width="0.5">
<path d="M20,50 Q60,20 100,50 T180,50 T260,50" stroke="rgba(244, 90, 90, 0.6)">
@@ -17,7 +19,7 @@
values="M20,50 Q60,20 100,50 T180,50 T260,50;
M20,50 Q60,80 100,50 T180,50 T260,50;
M20,50 Q60,20 100,50 T180,50 T260,50"
dur="10s" repeatCount="indefinite" />
dur="10s" repeatCount="indefinite"/>
</path>
<path d="M20,70 Q60,40 100,70 T180,70 T260,70" stroke="rgba(244, 90, 90, 0.4)">
@@ -25,26 +27,26 @@
values="M20,70 Q60,40 100,70 T180,70 T260,70;
M20,70 Q60,100 100,70 T180,70 T260,70;
M20,70 Q60,40 100,70 T180,70 T260,70"
dur="12s" repeatCount="indefinite" />
dur="12s" repeatCount="indefinite"/>
</path>
</g>
<circle cx="50" cy="50" r="1" fill="rgba(244, 90, 90, 0.8)">
<animate attributeName="cx" values="50;250;50" dur="20s" repeatCount="indefinite" />
<animate attributeName="cy" values="50;30;80;50" dur="16s" repeatCount="indefinite" />
<animate attributeName="r" values="1;1.5;1" dur="5s" repeatCount="indefinite" />
<animate attributeName="cx" values="50;250;50" dur="20s" repeatCount="indefinite"/>
<animate attributeName="cy" values="50;30;80;50" dur="16s" repeatCount="indefinite"/>
<animate attributeName="r" values="1;1.5;1" dur="5s" repeatCount="indefinite"/>
</circle>
<circle cx="150" cy="30" r="1" fill="rgba(244, 90, 90, 0.8)">
<animate attributeName="cx" values="150;100;200;150" dur="25s" repeatCount="indefinite" />
<animate attributeName="cy" values="30;60;20;30" dur="18s" repeatCount="indefinite" />
<animate attributeName="r" values="1;1.8;1" dur="6s" repeatCount="indefinite" />
<animate attributeName="cx" values="150;100;200;150" dur="25s" repeatCount="indefinite"/>
<animate attributeName="cy" values="30;60;20;30" dur="18s" repeatCount="indefinite"/>
<animate attributeName="r" values="1;1.8;1" dur="6s" repeatCount="indefinite"/>
</circle>
<circle cx="250" cy="70" r="1" fill="rgba(244, 90, 90, 0.8)">
<animate attributeName="cx" values="250;150;250" dur="22s" repeatCount="indefinite" />
<animate attributeName="cy" values="70;40;70" dur="15s" repeatCount="indefinite" />
<animate attributeName="r" values="1;1.2;1" dur="4s" repeatCount="indefinite" />
<animate attributeName="cx" values="250;150;250" dur="22s" repeatCount="indefinite"/>
<animate attributeName="cy" values="70;40;70" dur="15s" repeatCount="indefinite"/>
<animate attributeName="r" values="1;1.2;1" dur="4s" repeatCount="indefinite"/>
</circle>
</svg>
</div>
@@ -77,7 +79,7 @@
<view class="form-container">
<!-- 手机号码输入框 -->
<view class="input-box">
<t-icon name="mobile" size="20px" class="input-icon" />
<t-icon name="mobile" size="20px" class="input-icon"/>
<input
class="input"
type="tel"
@@ -89,7 +91,7 @@
<!-- 验证码输入框 -->
<view class="input-box">
<t-icon name="shield-error" size="20px" class="input-icon" />
<t-icon name="shield-error" size="20px" class="input-icon"/>
<input
class="input"
type="number"
@@ -107,11 +109,11 @@
立即登录
</view>
<!-- &lt;!&ndash; 注册链接 &ndash;&gt;-->
<!-- <view class="register-link">-->
<!-- <text>没有账号?</text>-->
<!-- <text class="link">去注册</text>-->
<!-- </view>-->
<!-- &lt;!&ndash; 注册链接 &ndash;&gt;-->
<!-- <view class="register-link">-->
<!-- <text>没有账号?</text>-->
<!-- <text class="link">去注册</text>-->
<!-- </view>-->
</view>
</view>
</view>
@@ -121,8 +123,10 @@
import {ref, reactive, computed, onMounted, onBeforeUnmount} from 'vue'
import {Toast} from 'tdesign-mobile-vue';
import {useRequest} from "@/utils/request";
import {useUserStore} from "@/store";
const request = useRequest()
const userStore = useUserStore()
const activeMethod = ref('phone')
const loginForm = reactive({
@@ -167,15 +171,15 @@ const saveCountdownState = (seconds, phone) => {
const restoreCountdownState = () => {
const endTime = uni.getStorageSync(SMS_COUNTDOWN_KEY)
const savedPhone = uni.getStorageSync(SMS_PHONE_KEY)
if (endTime && savedPhone) {
const now = Date.now()
const remainingSeconds = Math.floor((endTime - now) / 1000)
if (remainingSeconds > 0) {
countdown.value = remainingSeconds
startCountDownTimer() // 重新启动定时器
// 如果用户还没输入手机号,填入保存的手机号
if (!loginForm.phone) {
loginForm.phone = savedPhone
@@ -267,33 +271,47 @@ async function handleLogin() {
}
try {
const {data, code, msg} = await request.post('/client/auth/sms-login', loginForm)
if (code === 0) {
Toast({message: '登录成功', theme: 'success'})
// 保存token
uni.setStorageSync('token', data)
userStore.smsLogin(loginForm).then(() => {
userStore.getUserInfo().then(() => {
Toast({message: '登录成功', theme: 'success'})
uni.switchTab({
url: '/pages/index/index'
})
}).catch(e => {
Toast({message: e.message, theme: 'error'})
})
}).catch(e => {
console.log(e)
Toast({message: e.message, theme: 'error'})
})
// 获取之前保存的重定向URL
const redirectUrl = uni.getStorageSync('REDIRECT_URL') || '/pages/index/index'
uni.removeStorageSync('REDIRECT_URL') // 使用后删除
// 延迟跳转,让用户看到成功提示
setTimeout(() => {
if (redirectUrl.startsWith('/pages/index/') || redirectUrl.startsWith('/pages/profile/')) {
// 主页和个人中心,使用switchTab
uni.switchTab({
url: redirectUrl
})
} else {
// 其他页面使用reLaunch
uni.reLaunch({
url: redirectUrl
})
}
}, 1000)
} else {
Toast({message: msg, theme: 'error'})
}
// const {data, code, msg} = await request.post('/client/auth/sms-login', loginForm)
// if (code === 0) {
// Toast({message: '登录成功', theme: 'success'})
// // 保存token
// uni.setStorageSync('token', data)
//
// // 获取之前保存的重定向URL
// const redirectUrl = uni.getStorageSync('REDIRECT_URL') || '/pages/index/index'
// uni.removeStorageSync('REDIRECT_URL') // 使用后删除
//
// // 延迟跳转,让用户看到成功提示
// setTimeout(() => {
// if (redirectUrl.startsWith('/pages/index/') || redirectUrl.startsWith('/pages/profile/')) {
// // 主页和个人中心,使用switchTab
// uni.switchTab({
// url: redirectUrl
// })
// } else {
// // 其他页面使用reLaunch
// uni.reLaunch({
// url: redirectUrl
// })
// }
// }, 1000)
// } else {
// Toast({message: msg, theme: 'error'})
// }
} catch (e) {
console.error(e)
Toast({message: e.message, theme: 'error'})
+51 -46
View File
@@ -1,21 +1,16 @@
<script setup>
import {ref, onMounted} from 'vue';
import {ref, onMounted, computed} from 'vue';
import {Toast, Dialog} from 'tdesign-mobile-vue';
import {useRequest} from "@/utils/request";
import {useUserStore} from "@/store";
// 用户资料数据
const userInfo = ref({
id: '--',
name: '--',
avatar: '/static/avatar.png',
phone: '--',
company: '--'
});
const userStore = useUserStore()
// 我的活动数据
const activities = ref({
enrolled: 6
});
// 用户信息
const userInfo = computed(() => {
return userStore.userInfo
})
console.log('userInfo', userInfo)
// 退出登录
const handleLogout = () => {
@@ -25,18 +20,11 @@ const handleLogout = () => {
confirmBtn: '确定',
cancelBtn: '取消',
onConfirm: () => {
useRequest().post('/client/auth/logout').then(res=>{
if(res.code===0){
uni.clearStorageSync()
Toast({message: '退出登录成功', theme: 'success'});
setTimeout(() => {
uni.reLaunch({
url: '/pages/login/index'
});
}, 1000);
}else{
Toast({message: '退出登录失败', theme: 'error'});
}
userStore.logout().then(() => {
Toast({message: '退出登录成功', theme: 'success'});
uni.reLaunch({
url: '/pages/login/index'
});
})
}
});
@@ -47,22 +35,41 @@ const navigateTo = (path) => {
uni.navigateTo({url: path});
};
// 处理编辑资料
const handleEditProfile = () => {
navigateTo('/pages/profile/edit/index');
};
// 选择头像
function changeAvatar() {
uni.chooseImage({
count: 1,
sizeType: ['original', 'compressed'],
success: async (res) => {
try {
const {code, data, msg} = await useRequest().upload(res.tempFiles[0])
if (code === 0) {
updateAvatar(data)
} else {
Toast({message: msg, theme: 'error'});
}
} catch (e) {
Toast({message: e, theme: 'error'});
}
}
})
}
onMounted(() => {
// 获取用户信息
console.log('个人资料页面已加载');
userInfo.value={
id: getApp().globalData.user.id,
name: getApp().globalData.user.userName,
avatar: '/static/avatar.png',
phone: getApp().globalData.user.phone,
company: getApp().globalData.user.unitName
// 修改头像
async function updateAvatar(avatar) {
try {
await userStore.updateUserInfo({
...userStore.userInfo,
avatar: avatar
})
Toast({message: '修改头像成功', theme: 'success'});
} catch (e) {
Toast({message: e, theme: 'error'});
}
});
}
</script>
<template>
@@ -74,14 +81,14 @@ onMounted(() => {
<view class="user-profile-section">
<view class="user-info-container">
<view class="avatar-wrapper">
<t-avatar :image="userInfo.avatar" size="large"/>
<t-avatar :image="userInfo.avatar" size="large" @click="changeAvatar"/>
</view>
<view class="user-info">
<view class="user-name-row">
<text class="user-name">{{ userInfo.name }}</text>
<view class="edit-profile" @click="handleEditProfile">
<t-icon name="edit" size="20"/>
</view>
<text class="user-name">{{ userInfo.userName }}</text>
<!-- <view class="edit-profile" @click="handleEditProfile">-->
<!-- <t-icon name="edit" size="20"/>-->
<!-- </view>-->
</view>
<view class="user-company">{{ userInfo.phone }}</view>
</view>
@@ -115,12 +122,10 @@ onMounted(() => {
</t-cell-group>
</view>
<!-- 退出登录按钮 -->
<view class="logout-btn">
<t-button theme="danger" size="large" block @click="handleLogout">退出登录</t-button>
</view>
<!-- 底部版权信息 -->
<view class="footer">
<t-divider>活动报名系统 v1.0.0</t-divider>
</view>
+86
View File
@@ -0,0 +1,86 @@
import {defineStore} from "pinia";
import {useRequest} from "@/utils/request";
const request = useRequest()
export const useUserStore = defineStore("user", {
state: () => ({
userInfo: uni.getStorageSync('userInfo') || null,
token: uni.getStorageSync('token')
}),
actions: {
setToken(token) {
this.token = token
uni.setStorageSync('token', token)
},
setUserInfo(userInfo) {
this.userInfo = userInfo
uni.setStorageSync('userInfo', userInfo)
},
setUserInfoKV(key, value) {
this.userInfo[key] = value
},
async smsLogin(loginForm) {
try {
const {data, code, msg} = await request.post('/client/auth/sms-login', loginForm)
if (code === 0) {
this.setToken(data)
} else {
throw new Error(msg)
}
} catch (e) {
console.log(e)
throw e
}
},
async getUserInfo() {
try {
const {data, code, msg} = await request.get('/client/auth/user-info')
if (code === 0) {
this.setUserInfo(data)
} else {
throw new Error(msg)
}
} catch (e) {
console.log(e)
throw e
}
},
async updateUserInfo(userInfo) {
try {
const {data, code, msg} = await request.post('/client/auth/update-user-info', userInfo)
if (code === 0) {
this.setUserInfo(data)
} else {
throw new Error(msg)
}
} catch (e) {
console.log(e)
throw e
}
},
async logout() {
try {
const {code, msg} = await request.post('/client/auth/logout')
if (code === 0) {
this.setToken(null)
this.setUserInfo(null)
await uni.reLaunch({
url: '/pages/login/index'
});
} else {
throw new Error(msg)
}
} catch (e) {
console.log(e)
throw e
}
}
}
})
+185 -161
View File
@@ -1,200 +1,224 @@
import { reactive } from "vue";
import {reactive} from "vue";
/**
* 全局请求状态管理
*/
export const requestState = reactive({
// 所有正在加载的请求ID映射表
loadingRequests: {},
// 是否有任何请求正在加载中
get isLoading() {
return Object.keys(this.loadingRequests).length > 0;
},
// 当前正在加载的请求数量
get loadingCount() {
return Object.keys(this.loadingRequests).length;
}
// 所有正在加载的请求ID映射表
loadingRequests: {},
// 是否有任何请求正在加载中
get isLoading() {
return Object.keys(this.loadingRequests).length > 0;
},
// 当前正在加载的请求数量
get loadingCount() {
return Object.keys(this.loadingRequests).length;
}
});
/**
* 生成唯一的请求ID
*/
function generateRequestId() {
return Date.now() + "-" + Math.random().toString(36).substr(2, 9);
return Date.now() + "-" + Math.random().toString(36).substr(2, 9);
}
/**
* 请求开始处理
*/
function handleRequestStart(requestId) {
requestState.loadingRequests[requestId] = true;
requestState.loadingRequests[requestId] = true;
}
/**
* 请求结束处理
*/
function handleRequestEnd(requestId) {
delete requestState.loadingRequests[requestId];
delete requestState.loadingRequests[requestId];
}
/**
* 处理未登录状态
*/
function handleUnauthorized() {
// Token失效,清除存储并跳转登录
uni.removeStorageSync("token");
uni.removeStorageSync("userInfo");
uni.showToast({
title: "登录已过期,请重新登录",
icon: "none",
duration: 2000
});
setTimeout(() => {
uni.navigateTo({ url: "/pages/login/index" });
}, 2000);
// Token失效,清除存储并跳转登录
uni.removeStorageSync("token");
uni.removeStorageSync("userInfo");
uni.showToast({
title: "登录已过期,请重新登录",
icon: "none",
duration: 2000
});
setTimeout(() => {
uni.navigateTo({url: "/pages/login/index"});
}, 2000);
}
/**
* 使用简化的请求功能
*/
export function useRequest(options = {}) {
const defaultOptions = {
baseUrl: "/api",
timeout: 60000,
...options
};
/**
* 执行HTTP请求
*/
const request = (config) => {
const requestId = generateRequestId();
// 合并配置
const mergedConfig = {
...defaultOptions,
...config,
header: {
"Content-Type": "application/json",
"X-Requested-With": "XMLHttpRequest",
...defaultOptions.header,
...config.header
}
const defaultOptions = {
baseUrl: "/api",
timeout: 60000,
...options
};
// 构建完整URL
if (mergedConfig.url && !mergedConfig.url.startsWith("http")) {
mergedConfig.url = `${mergedConfig.baseUrl}${mergedConfig.url}`;
}
handleRequestStart(requestId);
return new Promise((resolve, reject) => {
uni.request({
...mergedConfig,
success: (response) => {
const { statusCode, data } = response;
// HTTP状态码成功
if (statusCode >= 200 && statusCode < 300) {
if (data.code === 600098) {
handleUnauthorized();
reject(data);
}else{
resolve(data);
}
// // 业务逻辑成功
// if (data.code === 0) {
// resolve(data);
// }
// // 未登录
// else if (data.code === 600098) {
// handleUnauthorized();
// reject(data);
// }
// // 其他业务错误,直接返回,由业务代码处理
// else {
// reject(data);
// }
}
// HTTP错误
else {
reject({
code: statusCode,
message: `HTTP错误: ${statusCode}`,
data: data
});
}
},
fail: (error) => {
reject(error);
},
complete: () => {
handleRequestEnd(requestId);
}
});
});
};
/**
* GET请求
*/
const get = (url, params = {}, config = {}) => {
return request({
method: "GET",
url,
data: params,
...config
});
};
/**
* POST请求
*/
const post = (url, data = {}, config = {}) => {
return request({
method: "POST",
url,
data,
...config
});
};
/**
* PUT请求
*/
const put = (url, data = {}, config = {}) => {
return request({
method: "PUT",
url,
data,
...config
});
};
/**
* DELETE请求
*/
const del = (url, data = {}, config = {}) => {
return request({
method: "DELETE",
url,
data,
...config
});
};
// 返回所有HTTP方法
return {
request,
get,
post,
put,
delete: del
};
/**
* 执行HTTP请求
*/
const request = (config) => {
const requestId = generateRequestId();
// 合并配置
const mergedConfig = {
...defaultOptions,
...config,
header: {
"Content-Type": "application/json",
"X-Requested-With": "XMLHttpRequest",
...defaultOptions.header,
...config.header
}
};
// 构建完整URL
if (mergedConfig.url && !mergedConfig.url.startsWith("http")) {
mergedConfig.url = `${mergedConfig.baseUrl}${mergedConfig.url}`;
}
handleRequestStart(requestId);
return new Promise((resolve, reject) => {
uni.request({
...mergedConfig,
success: (response) => {
const {statusCode, data} = response;
// HTTP状态码成功
if (statusCode >= 200 && statusCode < 300) {
if (data.code === 600098) {
handleUnauthorized();
reject(data);
} else {
resolve(data);
}
// // 业务逻辑成功
// if (data.code === 0) {
// resolve(data);
// }
// // 未登录
// else if (data.code === 600098) {
// handleUnauthorized();
// reject(data);
// }
// // 其他业务错误,直接返回,由业务代码处理
// else {
// reject(data);
// }
}
// HTTP错误
else {
reject({
code: statusCode,
message: `HTTP错误: ${statusCode}`,
data: data
});
}
},
fail: (error) => {
reject(error);
},
complete: () => {
handleRequestEnd(requestId);
}
});
});
};
/**
* GET请求
*/
const get = (url, params = {}, config = {}) => {
return request({
method: "GET",
url,
data: params,
...config
});
};
/**
* POST请求
*/
const post = (url, data = {}, config = {}) => {
return request({
method: "POST",
url,
data,
...config
});
};
/**
* PUT请求
*/
const put = (url, data = {}, config = {}) => {
return request({
method: "PUT",
url,
data,
...config
});
};
/**
* DELETE请求
*/
const del = (url, data = {}, config = {}) => {
return request({
method: "DELETE",
url,
data,
...config
});
};
/**
* 上传文件
*/
const upload = (file) => {
return new Promise((resolve, reject) => {
uni.uploadFile({
url: '/api/open/file/upload/image',
file: file,
name: 'file',
success: (res) => {
if (res.statusCode === 200) {
resolve(JSON.parse(res.data))
} else {
reject(res)
}
},
fail: (err) => {
reject(err)
}
})
})
}
// 返回所有HTTP方法
return {
request,
get,
post,
put,
delete: del,
upload
};
}
+9 -20
View File
@@ -1,5 +1,6 @@
import {Dialog} from "tdesign-mobile-vue";
import {useRequest} from "@/utils/request";
import {useUserStore} from "@/store";
/**
* 路由拦截器配置
@@ -10,24 +11,15 @@ export const setupRouterInterceptor = (whiteList = []) => {
addNavigationInterceptors(whiteList)
}
export const checkAuth = () => {
debugger
export const checkAuth = async () => {
if (process.env.VUE_APP_PLATFORM === 'h5' && window.location.pathname !== '/') {
const path = window.location.pathname.replace(/^\//, '')
if (!getDefaultWhiteList().includes(path)) {
const token = uni.getStorageSync('token')
if(token){
// 获取用户信息
const {user} = getApp().globalData
if(user == null){
return useRequest().get('/client/auth/info').then(({code, data, msg}) => {
if(code === 0){
getApp().globalData.user = data
}
})
}
}else{
uni.redirectTo({
if (token) {
await useUserStore().getUserInfo()
} else {
await uni.redirectTo({
url: '/pages/login/index'
})
}
@@ -84,17 +76,14 @@ async function routeInterceptor(args, whiteList) {
}
console.log('路由拦截检查:', {url: targetUrl, isInWhiteList, hasToken: !!token})
// 如果不在白名单且未登录,则跳转到登录页
if (!isInWhiteList) {
if (token) {
// 获取用户信息
const {user} = getApp().globalData
const user = useUserStore().userInfo
if (user == null) {
const {code, data, msg} = await useRequest().get('/client/auth/info')
if (code === 0) {
getApp().globalData.user = data
return args
}
await userStore().getUserInfo()
}
return args
} else {