Signed-off-by: 吴文峰 <kevin@lmve.net>
This commit is contained in:
2026-04-14 21:42:39 +08:00
parent 9ecab34b15
commit bb6c9fbecb
11 changed files with 1183 additions and 24 deletions
@@ -13,5 +13,5 @@
}
]
},
"lastUpdated": 1776172462417
"lastUpdated": 1776173744465
}
+13
View File
@@ -2,6 +2,7 @@ export default {
// tabBar
tabBar: {
home: 'Home',
user: 'Profile',
settings: 'Settings',
},
// 首页
@@ -109,5 +110,17 @@ export default {
failed: 'Operation failed',
back: 'Back',
networkError: 'Network request failed, please check your connection',
},
// 用户页
user: {
profile: 'Profile',
username: 'Username',
firstName: 'Name',
birthday: 'Birthday',
gender: 'Gender',
region: 'Region',
male: 'Male',
female: 'Female',
unknown: 'Unknown',
}
}
+4 -1
View File
@@ -1,7 +1,10 @@
import { createI18n } from 'vue-i18n'
import { createI18n, useI18n as _useI18n } from 'vue-i18n'
import zh from './zh.js'
import en from './en.js'
// 兼容 uni-app 的 useI18n 导出
export const useI18n = _useI18n
// 获取本地语言设置,默认中文
function getLocale() {
// 从本地存储读取
+13
View File
@@ -2,6 +2,7 @@ export default {
// tabBar
tabBar: {
home: '首页',
user: '我的',
settings: '设置',
},
// 首页
@@ -109,5 +110,17 @@ export default {
failed: '操作失败',
back: '返回',
networkError: '网络请求失败,请检查网络',
},
// 用户页
user: {
profile: '个人资料',
username: '用户名',
firstName: '姓名',
birthday: '生日',
gender: '性别',
region: '地区',
male: '男',
female: '女',
unknown: '未知',
}
}
+12
View File
@@ -12,6 +12,12 @@
"navigationBarTitleText": "首页"
}
},
{
"path": "pages/user/user",
"style": {
"navigationBarTitleText": "我的"
}
},
{
"path": "pages/settings/apiConfig",
"style": {
@@ -50,6 +56,12 @@
"iconPath": "static/tabbar/home.png",
"selectedIconPath": "static/tabbar/home-active.png"
},
{
"pagePath": "pages/user/user",
"text": "我的",
"iconPath": "static/tabbar/settings.png",
"selectedIconPath": "static/tabbar/settings-active.png"
},
{
"pagePath": "pages/settings/apiConfig",
"text": "设置",
+22 -12
View File
@@ -135,18 +135,26 @@ const handleLogin = () => {
url: getApp().globalData.BASE_URL + '/users/login',
method: 'POST',
data: {
username: form.username,
password: form.password,
remember: form.remember
userCookieValue: '',
data: {
username: form.username,
password: form.password,
remember: form.remember
}
},
header: {
'Content-Type': 'application/json'
},
success: (res) => {
if (res.data.code === 0 && res.data.data && res.data.data.cookie) {
const cookie = res.data.data.cookie
uni.setStorageSync('sessionCookie', cookie.Value)
uni.setStorageSync('userInfo', res.data.data)
if (res.data.err_code === 0 && res.data.return && res.data.return.cookie) {
const cookieData = res.data.return.cookie
// 存储 cookie Value 作为 session 标识
uni.setStorageSync('sessionCookie', cookieData.Value)
uni.setStorageSync('cookieExpires', cookieData.ExpiresAt)
uni.setStorageSync('userInfo', {
userId: cookieData.ID,
username: form.username
})
if (form.remember) {
uni.setStorageSync('savedUsername', form.username)
@@ -168,13 +176,15 @@ const handleLogin = () => {
})
}, 1500)
} else {
// 根据 err_code 显示错误信息
const errCode = res.data.err_code
const msgMap = {
userNameNoFund: t('login.usernameNotFound'),
userPassIncorrect: t('login.passwordIncorrect'),
jsonErr: t('login.paramError'),
postErr: t('login.requestFailed')
'-41': t('login.usernameNotFound'),
'-42': t('login.passwordIncorrect'),
'-3': t('login.paramError'),
'-2': t('login.requestFailed')
}
errorMsg.value = msgMap[res.data.code] || t('login.loginFailed')
errorMsg.value = msgMap[errCode] || res.data.err_msg || t('login.loginFailed')
}
},
fail: (err) => {
@@ -161,15 +161,18 @@ const handleRegister = () => {
url: getApp().globalData.BASE_URL + '/users/register',
method: 'POST',
data: {
username: form.username,
useremail: form.email,
userpass: form.password
userCookieValue: '',
data: {
username: form.username,
useremail: form.email,
userpass: form.password
}
},
header: {
'Content-Type': 'application/json'
},
success: (res) => {
if (res.data.code === 0) {
if (res.data.err_code === 0) {
uni.showToast({
title: t('register.registerSuccess'),
icon: 'success',
@@ -179,13 +182,15 @@ const handleRegister = () => {
uni.navigateBack()
}, 1500)
} else {
// 根据 err_code 显示错误信息
const errCode = res.data.err_code
const msgMap = {
userNameDup: t('register.usernameExists'),
userEmailDup: t('register.emailUsed'),
jsonErr: t('register.paramError'),
postErr: t('register.requestFailed')
'-4': t('register.usernameExists'),
'-43': t('register.emailInvalid'),
'-3': t('register.paramError'),
'-2': t('register.requestFailed')
}
errorMsg.value = msgMap[res.data.code] || t('register.registerFailed')
errorMsg.value = msgMap[errCode] || res.data.err_msg || t('register.registerFailed')
}
},
fail: (err) => {
+319
View File
@@ -0,0 +1,319 @@
<template>
<view class="container">
<!-- 未登录状态 -->
<view v-if="!isLoggedIn" class="login-prompt">
<view class="prompt-icon">👤</view>
<text class="prompt-text">{{ t('index.pleaseLogin') }}</text>
<button class="login-btn" @click="goLogin">{{ t('index.login') }}</button>
</view>
<!-- 已登录状态 -->
<view v-else class="user-info">
<!-- 用户卡片 -->
<view class="user-card">
<view class="avatar-box">
<view class="avatar-placeholder">👤</view>
</view>
<view class="user-details">
<text class="username">{{ userInfo.username || userInfo.Name || 'User' }}</text>
<text class="email">{{ userInfo.email || userInfo.Email || '' }}</text>
</view>
</view>
<!-- 加载状态 -->
<view v-if="loading" class="loading-box">
<text>{{ t('common.loading') }}</text>
</view>
<!-- 用户详细信息 -->
<view v-else-if="userDetail" class="detail-section">
<view class="section-title">{{ t('user.profile') }}</view>
<view class="info-list">
<view class="info-item">
<text class="info-label">{{ t('user.username') }}</text>
<text class="info-value">{{ userDetail.userInfo.Username || '-' }}</text>
</view>
<view class="info-item">
<text class="info-label">{{ t('user.firstName') }}</text>
<text class="info-value">{{ userDetail.userInfo.FirstName || '-' }}</text>
</view>
<view class="info-item">
<text class="info-label">{{ t('user.birthday') }}</text>
<text class="info-value">{{ userDetail.userInfo.Birthdate || '-' }}</text>
</view>
<view class="info-item">
<text class="info-label">{{ t('user.gender') }}</text>
<text class="info-value">{{ genderText }}</text>
</view>
<view class="info-item">
<text class="info-label">{{ t('user.region') }}</text>
<text class="info-value">{{ userDetail.userInfo.Region || '-' }}</text>
</view>
</view>
</view>
<!-- 操作按钮 -->
<view class="action-section">
<button class="action-btn logout" @click="handleLogout">
{{ t('index.logout') }}
</button>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { onShow } from '@dcloudio/uni-app'
const { t, locale } = useI18n()
const isLoggedIn = ref(false)
const loading = ref(false)
const userInfo = ref({})
const userDetail = ref(null)
// 性别显示文本
const genderText = computed(() => {
if (!userDetail.value?.userInfo?.Gender) return '-'
const map = { M: t('user.male'), F: t('user.female'), U: t('user.unknown') }
return map[userDetail.value.userInfo.Gender] || '-'
})
// 检查登录状态
const checkLogin = () => {
const cookie = uni.getStorageSync('sessionCookie')
const info = uni.getStorageSync('userInfo')
isLoggedIn.value = !!cookie && !!info
if (info) userInfo.value = info
}
// 获取用户详细信息
const fetchUserDetail = () => {
if (!isLoggedIn.value) return
loading.value = true
const cookie = uni.getStorageSync('sessionCookie')
uni.request({
url: getApp().globalData.BASE_URL + '/users/getinfo',
method: 'POST',
data: {
userCookieValue: cookie,
data: {}
},
header: {
'Content-Type': 'application/json'
},
success: (res) => {
if (res.data.err_code === 0 && res.data.return) {
userDetail.value = res.data.return
// 更新本地存储的用户信息
const newInfo = {
userId: res.data.return.user?.ID,
username: res.data.return.user?.Name,
email: res.data.return.user?.Email,
...res.data.return.userInfo
}
uni.setStorageSync('userInfo', newInfo)
userInfo.value = newInfo
} else if (res.data.err_code === -44) {
// Cookie 过期,清除登录状态
handleLogout()
}
},
fail: (err) => {
console.error('Get user info error:', err)
},
complete: () => {
loading.value = false
}
})
}
// 跳转到登录页
const goLogin = () => {
uni.navigateTo({
url: '/pages/login/login'
})
}
// 退出登录
const handleLogout = () => {
uni.showModal({
title: t('index.logoutConfirm'),
content: t('index.logoutMessage'),
success: (res) => {
if (res.confirm) {
uni.removeStorageSync('sessionCookie')
uni.removeStorageSync('cookieExpires')
uni.removeStorageSync('userInfo')
isLoggedIn.value = false
userInfo.value = {}
userDetail.value = null
uni.showToast({
title: t('index.logout'),
icon: 'success'
})
}
}
})
}
// 页面显示时刷新数据
onShow(() => {
checkLogin()
if (isLoggedIn.value) {
fetchUserDetail()
}
})
</script>
<style>
.container {
min-height: 100vh;
background-color: #f5f5f5;
padding: 20rpx;
}
.login-prompt {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding-top: 200rpx;
}
.prompt-icon {
font-size: 120rpx;
margin-bottom: 30rpx;
}
.prompt-text {
font-size: 32rpx;
color: #666;
margin-bottom: 40rpx;
}
.login-btn {
width: 300rpx;
height: 88rpx;
line-height: 88rpx;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #fff;
border-radius: 44rpx;
font-size: 32rpx;
border: none;
}
.user-card {
display: flex;
align-items: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 20rpx;
padding: 40rpx;
margin-bottom: 30rpx;
}
.avatar-box {
width: 120rpx;
height: 120rpx;
border-radius: 60rpx;
background: rgba(255, 255, 255, 0.3);
display: flex;
align-items: center;
justify-content: center;
margin-right: 30rpx;
}
.avatar-placeholder {
font-size: 60rpx;
}
.user-details {
flex: 1;
}
.username {
display: block;
font-size: 36rpx;
font-weight: bold;
color: #fff;
margin-bottom: 10rpx;
}
.email {
display: block;
font-size: 26rpx;
color: rgba(255, 255, 255, 0.8);
}
.loading-box {
text-align: center;
padding: 60rpx;
color: #999;
}
.detail-section {
background: #fff;
border-radius: 16rpx;
padding: 30rpx;
margin-bottom: 30rpx;
}
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 20rpx;
padding-bottom: 20rpx;
border-bottom: 1rpx solid #eee;
}
.info-list {
display: flex;
flex-direction: column;
}
.info-item {
display: flex;
justify-content: space-between;
padding: 24rpx 0;
border-bottom: 1rpx solid #f5f5f5;
}
.info-item:last-child {
border-bottom: none;
}
.info-label {
font-size: 28rpx;
color: #666;
}
.info-value {
font-size: 28rpx;
color: #333;
}
.action-section {
margin-top: 40rpx;
}
.action-btn {
width: 100%;
height: 88rpx;
line-height: 88rpx;
border-radius: 44rpx;
font-size: 32rpx;
border: none;
margin-bottom: 20rpx;
}
.action-btn.logout {
background: #fff;
color: #ff4d4f;
border: 2rpx solid #ff4d4f;
}
</style>
@@ -0,0 +1,5 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="20" cy="20" r="20" fill="#F0F0F0"/>
<circle cx="20" cy="15" r="6" fill="#999"/>
<path d="M8 36C8 28.268 13.372 22 20 22C26.628 22 32 28.268 32 36" stroke="#999" stroke-width="4" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 323 B