修复app登录和接口问题

This commit is contained in:
扫地僧 2026-02-07 00:04:58 +08:00
parent f9d7f980bb
commit a8a7435721
17 changed files with 694 additions and 945 deletions

12
.env
View File

@ -1,16 +1,16 @@
# =========================================== # ===========================================
# API配置 - 根据你的后端接口修改 # API配置 - 根据你的后端接口修改
# =========================================== # ===========================================
# 开发环境 # 开发环境Vite 只能识别 VITE_ 前缀)
VUE_APP_API_BASE_URL=https://localhost:8000 VITE_APP_API_BASE_URL=http://localhost:8000
VUE_APP_API_TIMEOUT=10000 VITE_APP_API_TIMEOUT=10000
# =========================================== # ===========================================
# 应用配置 # 应用配置
# =========================================== # ===========================================
VUE_APP_APP_NAME=babyhealth VITE_APP_APP_NAME=babyhealth
VUE_APP_APP_VERSION=1.0.0 VITE_APP_APP_VERSION=1.0.0
VUE_APP_DEBUG=true VITE_APP_DEBUG=true
# =========================================== # ===========================================
# 其他配置 (可选) # 其他配置 (可选)

10
env.d.ts vendored Normal file
View File

@ -0,0 +1,10 @@
/// <reference types="vite/client" />
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}
// uni-app 的全局对象(让 TS 不再提示 uni 未定义)
declare const uni: any

19
main.js
View File

@ -1,29 +1,12 @@
import { createSSRApp } from 'vue' import { createSSRApp } from 'vue'
// 引入 createPinia 方法(命名导出)
import { createPinia } from 'pinia' import { createPinia } from 'pinia'
import App from './App.vue' import App from './App.vue'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
// 引入 lime-echart 组件
import LEchart from './uni_modules/lime-echart/components/l-echart/l-echart.vue'
// FontAwesome CSS 将通过 App.vue 中的全局样式引入
export function createApp() { export function createApp() {
const app = createSSRApp(App) const app = createSSRApp(App)
// 创建 Pinia 实例
const pinia = createPinia() const pinia = createPinia()
// 注册 Pinia 持久化
pinia.use(piniaPluginPersistedstate) pinia.use(piniaPluginPersistedstate)
// 注册 Pinia
app.use(pinia) app.use(pinia)
return { app }
// 暂时注释掉 uView因为与 Vue 3 有兼容性问题
// 后续可以使用 uView Plus 或其他 Vue 3 兼容的 UI 库
// app.use(uView)
// 全局注册 lime-echart 组件
app.component('l-echart', LEchart)
return {
app
}
} }

View File

@ -38,26 +38,35 @@
<view class="form-container"> <view class="form-container">
<!-- 用户名输入 --> <!-- 用户名输入 -->
<view class="input-field-group"> <view class="input-field-group">
<view class="input-container" :class="{ 'focused': usernameFocused, 'error': usernameError }"> <view
class="input-container"
:class="{ focused: accountFocused, error: accountError }"
>
<view class="input-icon-wrapper"> <view class="input-icon-wrapper">
<i class="fas fa-user input-icon"></i> <i class="fas fa-user input-icon"></i>
</view> </view>
<input <input
v-model="form.username" v-model="form.account"
placeholder="用户名" placeholder="用户名"
class="input" class="input"
type="text" type="text"
@focus="handleUsernameFocus" @focus="handleAccountFocus"
@blur="handleUsernameBlur" @blur="handleAccountBlur"
@input="clearUsernameError"> @input="clearAccountError"
/>
<view class="input-border"></view> <view class="input-border"></view>
</view> </view>
<text class="error-text" v-if="usernameError">{{ usernameError }}</text> <text class="error-text" v-if="accountError">{{
accountError
}}</text>
</view> </view>
<!-- 密码输入 --> <!-- 密码输入 -->
<view class="input-field-group"> <view class="input-field-group">
<view class="input-container" :class="{ 'focused': passwordFocused, 'error': passwordError }"> <view
class="input-container"
:class="{ focused: passwordFocused, error: passwordError }"
>
<view class="input-icon-wrapper"> <view class="input-icon-wrapper">
<i class="fas fa-lock input-icon"></i> <i class="fas fa-lock input-icon"></i>
</view> </view>
@ -68,32 +77,40 @@
:type="showPassword ? 'text' : 'password'" :type="showPassword ? 'text' : 'password'"
@focus="handlePasswordFocus" @focus="handlePasswordFocus"
@blur="handlePasswordBlur" @blur="handlePasswordBlur"
@input="clearPasswordError"> @input="clearPasswordError"
/>
<view class="password-toggle" @click="togglePassword"> <view class="password-toggle" @click="togglePassword">
<i :class="showPassword ? 'fas fa-eye-slash' : 'fas fa-eye'"></i> <i
:class="showPassword ? 'fas fa-eye-slash' : 'fas fa-eye'"
></i>
</view> </view>
<view class="input-border"></view> <view class="input-border"></view>
</view> </view>
<text class="error-text" v-if="passwordError">{{ passwordError }}</text> <text class="error-text" v-if="passwordError">{{
passwordError
}}</text>
</view> </view>
<!-- 选项区域 --> <!-- 选项区域 -->
<view class="options-row"> <view class="options-row">
<view class="remember-section" @click="toggleRemember"> <view class="remember-section" @click="toggleRemember">
<view class="custom-checkbox" :class="{ 'checked': rememberMe }"> <view class="custom-checkbox" :class="{ checked: rememberMe }">
<i class="fas fa-check" v-if="rememberMe"></i> <i class="fas fa-check" v-if="rememberMe"></i>
</view> </view>
<text class="remember-text">记住我</text> <text class="remember-text">记住我</text>
</view> </view>
<text class="forgot-link" @click="handleForgotPassword">忘记密码</text> <text class="forgot-link" @click="handleForgotPassword"
>忘记密码</text
>
</view> </view>
<!-- 登录按钮 --> <!-- 登录按钮 -->
<button <button
:disabled="loading || !isFormValid" :disabled="loading || !isFormValid"
class="login-button" class="login-button"
:class="{ 'loading': loading, 'disabled': !isFormValid }" :class="{ loading: loading, disabled: !isFormValid }"
@click="handleLogin"> @click="handleLogin"
>
<view class="button-content"> <view class="button-content">
<view class="button-icon" v-if="!loading"> <view class="button-icon" v-if="!loading">
<i class="fas fa-arrow-right"></i> <i class="fas fa-arrow-right"></i>
@ -101,7 +118,9 @@
<view class="loading-spinner" v-if="loading"> <view class="loading-spinner" v-if="loading">
<i class="fas fa-spinner fa-spin"></i> <i class="fas fa-spinner fa-spin"></i>
</view> </view>
<text class="button-text">{{ loading ? '登录中...' : '立即登录' }}</text> <text class="button-text">{{
loading ? "登录中..." : "立即登录"
}}</text>
</view> </view>
<view class="button-shine" v-if="!loading"></view> <view class="button-shine" v-if="!loading"></view>
</button> </button>
@ -119,227 +138,160 @@
</view> </view>
</template> </template>
<script> <script lang="ts" setup>
import { userApi } from '../../src/api/index.js' import { reactive, ref, computed } from "vue";
import { useAuthStore } from '../../src/store/authStore.js' import loginApi from "@/src/api/login";
import { redirectAfterLogin } from '../../src/utils/routeGuard.js' import { useAuthStore } from "@/src/store/authStore.js";
import { redirectAfterLogin } from "@/src/utils/routeGuard.js";
export default { interface LoginForm {
data() { account: string;
return { password: string;
form: { }
username: '',
password: '' interface LoginUser {
}, id: number;
loading: false, account: string;
usernameFocused: false, name: string;
passwordFocused: false, group_id: number;
showPassword: false, }
rememberMe: false,
usernameError: '', interface LoginResponseData {
passwordError: '' token: string;
} user?: LoginUser;
}, id?: number;
computed: { }
isFormValid() {
return this.form.username.trim() && this.form.password.trim() const form = reactive<LoginForm>({
} account: "",
}, password: "",
methods: { });
async handleLogin() {
this.loading = true; const loading = ref(false);
const accountFocused = ref(false);
const passwordFocused = ref(false);
const showPassword = ref(false);
const rememberMe = ref(false);
const accountError = ref("");
const passwordError = ref("");
const authStore = useAuthStore();
const isFormValid = computed<boolean>(() => {
return form.account.trim() !== "" && form.password.trim() !== "";
});
async function handleLogin() {
loading.value = true;
try { try {
const res = await userApi.login(this.form); const res = (await loginApi.login(form)) as LoginResponseData;
console.log('登录响应:', res); console.log("登录响应:", res);
// const { token, user, id } = res || {};
const token = res.accessToken || res.token;
if (!token) { if (!token) {
throw new Error('登录失败:未获取到访问令牌'); throw new Error("登录失败:未获取到访问令牌");
} }
const authStore = useAuthStore(); const userInfo: LoginUser | { account: string; id?: number } = user || {
// account: form.account,
const userInfo = res.user || { id: id,
username: this.form.username,
id: res.id
}; };
authStore.login(userInfo, token); authStore.login(userInfo, token);
uni.showToast({ uni.showToast({
title: '登录成功', title: "登录成功",
icon: 'success' icon: "success",
}); });
//
setTimeout(() => { setTimeout(() => {
redirectAfterLogin(); redirectAfterLogin();
}, 500); }, 500);
} catch (e) { } catch (e: any) {
this.loading = false; loading.value = false;
console.error('登录错误:', e); console.error("登录错误:", e);
// const errorMessage: string =
const errorMessage = e.message || e.msg || '登录失败,请检查网络连接'; e?.message || e?.msg || "登录失败,请检查网络连接";
uni.showToast({ uni.showToast({
title: errorMessage, title: errorMessage,
icon: 'none', icon: "none",
duration: 3000 duration: 3000,
}); });
} }
}, }
// async handleLogin2() { //
// if(!this.form.username || !this.form.password) { function togglePassword() {
// uni.showToast({ showPassword.value = !showPassword.value;
// title: '', }
// icon: 'none'
// });
// return;
// }
// this.loading = true; //
function toggleRemember() {
rememberMe.value = !rememberMe.value;
}
// try { //
// // API function handleForgotPassword() {
// // API
// const mockLogin = () => {
// return new Promise((resolve) => {
// setTimeout(() => {
// //
// if (this.form.username === 'admin' && this.form.password === '123456') {
// resolve({
// code: 0,
// message: '',
// data: {
// token: 'mock_token_' + Date.now(),
// userInfo: {
// id: 1,
// username: this.form.username,
// name: '',
// avatar: '/static/logo.png',
// department: '',
// role: 'admin'
// }
// }
// });
// } else {
// resolve({
// code: 1,
// message: ''
// });
// }
// }, 1000);
// });
// };
// const result = await mockLogin();
// if (result.code === 0) {
// // 使 Pinia store
// const authStore = useAuthStore();
// authStore.login(result.data.userInfo, result.data.token);
// uni.showToast({
// title: '',
// icon: 'success'
// });
// //
// setTimeout(() => {
// redirectAfterLogin();
// }, 500);
// } else {
// uni.showToast({
// title: result.message || '',
// icon: 'none'
// });
// }
// } catch (error) {
// console.error(':', error);
// uni.showToast({
// title: '',
// icon: 'none'
// });
// }
// this.loading = false;
// },
//
togglePassword() {
this.showPassword = !this.showPassword
},
//
toggleRemember() {
this.rememberMe = !this.rememberMe
},
//
handleForgotPassword() {
uni.showToast({ uni.showToast({
title: '请联系管理员重置密码', title: "请联系管理员重置密码",
icon: 'none' icon: "none",
}) });
}, }
// //
handleUsernameFocus() { function handleAccountFocus() {
this.usernameFocused = true; accountFocused.value = true;
this.clearUsernameError(); clearAccountError();
}, }
handleUsernameBlur() { function handleAccountBlur() {
this.usernameFocused = false; accountFocused.value = false;
this.validateUsername(); validateAccount();
}, }
// //
handlePasswordFocus() { function handlePasswordFocus() {
this.passwordFocused = true; passwordFocused.value = true;
this.clearPasswordError(); clearPasswordError();
}, }
handlePasswordBlur() { function handlePasswordBlur() {
this.passwordFocused = false; passwordFocused.value = false;
this.validatePassword(); validatePassword();
}, }
// //
clearUsernameError() { function clearAccountError() {
this.usernameError = ''; accountError.value = "";
}, }
// //
clearPasswordError() { function clearPasswordError() {
this.passwordError = ''; passwordError.value = "";
}, }
// //
validateUsername() { function validateAccount(): boolean {
if (!this.form.username.trim()) { if (!form.account.trim()) {
this.usernameError = '请输入用户名'; accountError.value = "请输入用户名";
return false; return false;
} }
return true; return true;
}, }
// //
validatePassword() { function validatePassword(): boolean {
if (!this.form.password.trim()) { if (!form.password.trim()) {
this.passwordError = '请输入密码'; passwordError.value = "请输入密码";
return false; return false;
} }
if (this.form.password.length < 6) { if (form.password.length < 6) {
this.passwordError = '密码至少6位'; passwordError.value = "密码至少6位";
return false; return false;
} }
return true; return true;
}
}
} }
</script> </script>
@ -370,7 +322,11 @@ export default {
.gradient-orb { .gradient-orb {
position: absolute; position: absolute;
border-radius: 50%; border-radius: 50%;
background: linear-gradient(45deg, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.05)); background: linear-gradient(
45deg,
rgba(255, 255, 255, 0.1),
rgba(255, 255, 255, 0.05)
);
animation: float 8s ease-in-out infinite; animation: float 8s ease-in-out infinite;
filter: blur(1rpx); filter: blur(1rpx);
} }
@ -439,7 +395,8 @@ export default {
} }
@keyframes float { @keyframes float {
0%, 100% { 0%,
100% {
transform: translateY(0px) rotate(0deg) scale(1); transform: translateY(0px) rotate(0deg) scale(1);
opacity: 0.7; opacity: 0.7;
} }
@ -493,7 +450,8 @@ export default {
} }
@keyframes pulse { @keyframes pulse {
0%, 100% { 0%,
100% {
transform: scale(1); transform: scale(1);
opacity: 0.7; opacity: 0.7;
} }
@ -536,7 +494,7 @@ export default {
} }
.login-card::before { .login-card::before {
content: ''; content: "";
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
@ -811,13 +769,22 @@ export default {
left: -100%; left: -100%;
width: 100%; width: 100%;
height: 100%; height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent); background: linear-gradient(
90deg,
transparent,
rgba(255, 255, 255, 0.2),
transparent
);
animation: shine 2s infinite; animation: shine 2s infinite;
} }
@keyframes shine { @keyframes shine {
0% { left: -100%; } 0% {
100% { left: 100%; } left: -100%;
}
100% {
left: 100%;
}
} }
/* 测试提示 */ /* 测试提示 */

View File

@ -1,607 +0,0 @@
/**
* API接口配置
*/
import { apiBaseUrl, apiTimeout } from '../config/index.js'
// 基础配置 - 从配置文件获取
const BASE_URL = apiBaseUrl
const TIMEOUT = apiTimeout
/**
* 请求拦截器
*/
const requestInterceptor = (config) => {
// 添加token
const token = uni.getStorageSync('token')
if (token) {
config.header = {
...config.header,
'Authorization': `Bearer ${token}`
}
}
// 添加通用请求头
config.header = {
'Content-Type': 'application/json',
...config.header
}
return config
}
/**
* 响应拦截器
*/
const responseInterceptor = (response) => {
const { statusCode, data } = response
if (statusCode === 200) {
if (data.code === 0) {
return data.data
} else {
uni.showToast({
title: data.message || '请求失败',
icon: 'none'
})
return Promise.reject(new Error(data.message || '请求失败'))
}
} else if (statusCode === 401) {
// token过期跳转登录
uni.removeStorageSync('token')
uni.reLaunch({
url: '/pages/login/login'
})
return Promise.reject(new Error('登录已过期'))
} else {
uni.showToast({
title: '网络错误',
icon: 'none'
})
return Promise.reject(new Error('网络错误'))
}
}
/**
* 通用请求方法
*/
const request = (options) => {
return new Promise((resolve, reject) => {
// 请求拦截
const config = requestInterceptor({
url: options.url.startsWith('/') ? BASE_URL + options.url : BASE_URL + '/' + options.url,
method: options.method || 'GET',
data: options.data,
header: options.header || {},
timeout: options.timeout || TIMEOUT
})
uni.request({
...config,
success: (response) => {
try {
const result = responseInterceptor(response)
resolve(result)
} catch (error) {
reject(error)
}
},
fail: (error) => {
uni.showToast({
title: '网络连接失败',
icon: 'none'
})
reject(error)
}
})
})
}
/**
* 用户相关API
*/
export const userApi = {
// 登录
login(data) {
return request({
url: '/api/login',
method: 'POST',
data
})
},
// 登出
logout() {
return request({
url: '/api/logout',
method: 'POST'
})
},
// 获取用户信息
getUserInfo() {
return request({
url: '/api/user/info',
method: 'GET'
})
},
// 更新用户信息
updateUserInfo(data) {
return request({
url: '/api/user/info',
method: 'PUT',
data
})
},
// 修改密码
changePassword(data) {
return request({
url: '/api/user/password',
method: 'PUT',
data
})
},
// 上传头像
uploadAvatar(file) {
return request({
url: '/api/user/avatar',
method: 'POST',
data: file
})
}
}
/**
* 考勤相关API
*/
export const attendanceApi = {
// 打卡
checkIn(data) {
return request({
url: '/api/attendance/checkin',
method: 'POST',
data
})
},
// 下班打卡
checkOut(data) {
return request({
url: '/api/attendance/checkout',
method: 'POST',
data
})
},
// 获取考勤记录
getAttendanceList(params) {
return request({
url: '/api/attendance/list',
method: 'GET',
data: params
})
},
// 获取考勤统计
getAttendanceStats(params) {
return request({
url: '/api/attendance/stats',
method: 'GET',
data: params
})
},
// 获取考勤详情
getAttendanceDetail(id) {
return request({
url: '/api/attendance/detail',
method: 'GET',
data: { id }
})
}
}
/**
* 请假相关API
*/
export const leaveApi = {
// 申请请假
applyLeave(data) {
return request({
url: '/api/leave/apply',
method: 'POST',
data
})
},
// 获取请假列表
getLeaveList(params) {
return request({
url: '/api/leave/list',
method: 'GET',
data: params
})
},
// 获取请假详情
getLeaveDetail(id) {
return request({
url: '/api/leave/detail',
method: 'GET',
data: { id }
})
},
// 取消请假
cancelLeave(id) {
return request({
url: '/api/leave/cancel',
method: 'PUT',
data: { id }
})
},
// 审批请假
approveLeave(id, data) {
return request({
url: '/api/leave/approve',
method: 'PUT',
data: { id, ...data }
})
},
// 拒绝请假
rejectLeave(id, data) {
return request({
url: '/api/leave/reject',
method: 'PUT',
data: { id, ...data }
})
}
}
/**
* 报销相关API
*/
export const reimbursementApi = {
// 提交报销
submitReimbursement(data) {
return request({
url: '/api/reimbursement/submit',
method: 'POST',
data
})
},
// 获取报销列表
getReimbursementList(params) {
return request({
url: '/api/reimbursement/list',
method: 'GET',
data: params
})
},
// 获取报销详情
getReimbursementDetail(id) {
return request({
url: '/api/reimbursement/detail',
method: 'GET',
data: { id }
})
},
// 上传发票
uploadInvoice(file) {
return request({
url: '/api/reimbursement/upload',
method: 'POST',
data: file
})
},
// 审批报销
approveReimbursement(id, data) {
return request({
url: '/api/reimbursement/approve',
method: 'PUT',
data: { id, ...data }
})
},
// 拒绝报销
rejectReimbursement(id, data) {
return request({
url: '/api/reimbursement/reject',
method: 'PUT',
data: { id, ...data }
})
}
}
/**
* 任务相关API
*/
export const taskApi = {
// 获取任务列表
getTaskList(params) {
return request({
url: '/api/task/list',
method: 'GET',
data: params
})
},
// 创建任务
createTask(data) {
return request({
url: '/api/task/create',
method: 'POST',
data
})
},
// 获取任务详情
getTaskDetail(id) {
return request({
url: '/api/task/detail',
method: 'GET',
data: { id }
})
},
// 更新任务
updateTask(id, data) {
return request({
url: '/api/task/update',
method: 'PUT',
data: { id, ...data }
})
},
// 更新任务状态
updateTaskStatus(id, status) {
return request({
url: '/api/task/status',
method: 'PUT',
data: { id, status }
})
},
// 分配任务
assignTask(id, data) {
return request({
url: '/api/task/assign',
method: 'PUT',
data: { id, ...data }
})
},
// 完成任务
completeTask(id, data) {
return request({
url: '/api/task/complete',
method: 'PUT',
data: { id, ...data }
})
}
}
/**
* 消息相关API
*/
export const messageApi = {
// 获取消息列表
getMessageList(params) {
return request({
url: '/api/message/list',
method: 'GET',
data: params
})
},
// 获取消息详情
getMessageDetail(id) {
return request({
url: '/api/message/detail',
method: 'GET',
data: { id }
})
},
// 标记消息为已读
markAsRead(id) {
return request({
url: '/api/message/read',
method: 'PUT',
data: { id }
})
},
// 获取未读消息数量
getUnreadCount() {
return request({
url: '/api/message/unread-count',
method: 'GET'
})
},
// 发送消息
sendMessage(data) {
return request({
url: '/api/message/send',
method: 'POST',
data
})
}
}
/**
* 文件相关API
*/
export const fileApi = {
// 上传文件
uploadFile(file) {
return request({
url: '/api/file/upload',
method: 'POST',
data: file
})
},
// 获取文件列表
getFileList(params) {
return request({
url: '/api/file/list',
method: 'GET',
data: params
})
},
// 下载文件
downloadFile(id) {
return request({
url: '/api/file/download',
method: 'GET',
data: { id }
})
},
// 删除文件
deleteFile(id) {
return request({
url: '/api/file/delete',
method: 'DELETE',
data: { id }
})
}
}
/**
* 客户相关API
*/
export const customerApi = {
// 获取客户列表
getCustomerList(params) {
return request({
url: '/api/customer/list',
method: 'GET',
data: params
})
},
// 获取客户详情
getCustomerDetail(id) {
return request({
url: '/api/customer/detail',
method: 'GET',
data: { id }
})
},
// 添加客户
addCustomer(data) {
return request({
url: '/api/customer/add',
method: 'POST',
data
})
},
// 更新客户信息
updateCustomer(id, data) {
return request({
url: '/api/customer/update',
method: 'PUT',
data: { id, ...data }
})
},
// 删除客户
deleteCustomer(id) {
return request({
url: '/api/customer/delete',
method: 'DELETE',
data: { id }
})
}
}
/**
* 部门相关API
*/
export const departmentApi = {
// 获取部门列表
getDepartmentList(params) {
return request({
url: '/api/department/list',
method: 'GET',
data: params
})
},
// 获取部门树
getDepartmentTree() {
return request({
url: '/api/department/tree',
method: 'GET'
})
},
// 获取部门详情
getDepartmentDetail(id) {
return request({
url: '/api/department/detail',
method: 'GET',
data: { id }
})
}
}
/**
* 通知相关API
*/
export const notificationApi = {
// 获取通知列表
getNotificationList(params) {
return request({
url: '/api/notification/list',
method: 'GET',
data: params
})
},
// 标记通知为已读
markAsRead(id) {
return request({
url: '/api/notification/read',
method: 'PUT',
data: { id }
})
},
// 获取未读通知数量
getUnreadCount() {
return request({
url: '/api/notification/unread-count',
method: 'GET'
})
}
}
export default {
userApi,
attendanceApi,
leaveApi,
reimbursementApi,
taskApi,
messageApi,
fileApi,
customerApi,
departmentApi,
notificationApi
}

17
src/api/login.js Normal file
View File

@ -0,0 +1,17 @@
/**
* 登录相关 API
*/
import request from './request'
export const loginApi = {
// 登录
login(data) {
return request({
url: '/api/login',
method: 'POST',
data
})
}
}
export default loginApi

108
src/api/request.js Normal file
View File

@ -0,0 +1,108 @@
/**
* 通用请求封装拦截器等
*/
import { apiBaseUrl, apiTimeout } from '../config/index.js'
// 基础配置 - 从配置文件获取
const BASE_URL = apiBaseUrl
const TIMEOUT = apiTimeout
/**
* 请求拦截器
*/
const requestInterceptor = (config) => {
// 添加token
const token = uni.getStorageSync('token')
if (token) {
config.header = {
...config.header,
'Authorization': `Bearer ${token}`
}
}
// 添加通用请求头
config.header = {
'Content-Type': 'application/json',
...config.header
}
return config
}
/**
* 响应拦截器
*/
const responseInterceptor = (response) => {
const { statusCode, data } = response
if (statusCode === 200) {
// 兼容两种后端返回格式:
// 1) { code: 0, message, data }
// 2) { code: 200, msg, data }
const businessCode = data.code
const success =
businessCode === undefined || businessCode === 0 || businessCode === 200
if (success) {
// 优先返回 data.data其次整个 data
return data.data !== undefined ? data.data : data
}
const message = data.msg || data.message || '请求失败'
uni.showToast({
title: message,
icon: 'none'
})
return Promise.reject(new Error(message))
} else if (statusCode === 401) {
// token过期跳转登录
uni.removeStorageSync('token')
uni.reLaunch({
url: 'pages/login/index'
})
return Promise.reject(new Error('登录已过期'))
} else {
uni.showToast({
title: '网络错误',
icon: 'none'
})
return Promise.reject(new Error('网络错误'))
}
}
/**
* 通用请求方法
*/
export const request = (options) => {
return new Promise((resolve, reject) => {
// 请求拦截
const config = requestInterceptor({
url: options.url.startsWith('/') ? BASE_URL + options.url : BASE_URL + '/' + options.url,
method: options.method || 'GET',
data: options.data,
header: options.header || {},
timeout: options.timeout || TIMEOUT
})
uni.request({
...config,
success: (response) => {
try {
const result = responseInterceptor(response)
resolve(result)
} catch (error) {
reject(error)
}
},
fail: (error) => {
uni.showToast({
title: '网络连接失败',
icon: 'none'
})
reject(error)
}
})
})
}
export default request

60
src/api/user.js Normal file
View File

@ -0,0 +1,60 @@
/**
* 用户相关API
*/
import request from './request'
export const userApi = {
// 登录
login(data) {
return request({
url: '/api/login',
method: 'POST',
data
})
},
// 登出
logout() {
return request({
url: '/api/logout',
method: 'POST'
})
},
// 获取用户信息
getUserInfo() {
return request({
url: '/api/user/info',
method: 'GET'
})
},
// 更新用户信息
updateUserInfo(data) {
return request({
url: '/api/user/info',
method: 'PUT',
data
})
},
// 修改密码
changePassword(data) {
return request({
url: '/api/user/password',
method: 'PUT',
data
})
},
// 上传头像
uploadAvatar(file) {
return request({
url: '/api/user/avatar',
method: 'POST',
data: file
})
}
}
export default userApi

View File

@ -1,19 +1,26 @@
/** /**
* 配置模块统一导出 * 配置模块统一导出
* 直接定义配置简单明了 * 优先使用 .env 中的环境变量未配置时再使用默认值
*
* 说明
* - Vite / uni-app(vite) 中只能通过 import.meta.env 访问环境变量
* - 且必须以 VITE_ 前缀开头才会被注入到客户端
*/ */
// 常用配置 - 直接定义 // 从环境变量读取
export const apiBaseUrl = 'https://apigo.yunzer.cn' const env = import.meta.env || {}
export const apiTimeout = 10000
export const appName = '企业办公移动应用'
export const appVersion = '1.0.0'
export const debug = true
// 环境判断 // 常用配置(支持 VITE_APP_* 前缀)
export const isDev = true export const apiBaseUrl = env.VITE_APP_API_BASE_URL || 'https://localhost:8000/'
export const isTest = false export const apiTimeout = Number(env.VITE_APP_API_TIMEOUT || 10000)
export const isProd = false export const appName = env.VITE_APP_APP_NAME || 'babyhealth'
export const appVersion = env.VITE_APP_APP_VERSION || '1.0.0'
export const debug = String(env.VITE_APP_DEBUG || 'true').toLowerCase() === 'true'
// 环境判断(根据需要自己扩展)
export const isDev = env.MODE === 'development'
export const isProd = env.MODE === 'production'
export const isTest = !isDev && !isProd
// 默认导出 // 默认导出
export default { export default {
@ -24,5 +31,5 @@ export default {
debug, debug,
isDev, isDev,
isTest, isTest,
isProd isProd,
} }

View File

@ -1,6 +1,6 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import { userApi } from '../api' import userApi from '../api/user'
export const useAuthStore = defineStore('auth', () => { export const useAuthStore = defineStore('auth', () => {
// 状态 // 状态
@ -14,32 +14,41 @@ export const useAuthStore = defineStore('auth', () => {
}) })
// 登录 // 登录
const loginOld = (userData, authToken) => { // 兼容两种调用方式:
userInfo.value = userData // 1) login(userInfo, token) —— 推荐(你登录接口就返回 user + token
token.value = authToken // 2) login(token) —— 仅保存 token然后再拉取 userInfo
isLoggedIn.value = true const login = async (userDataOrToken, authToken) => {
let userData = null
let finalToken = null
// 保存到本地存储 if (typeof userDataOrToken === 'string' && authToken === undefined) {
uni.setStorageSync('userInfo', userData) finalToken = userDataOrToken
uni.setStorageSync('token', authToken) } else {
uni.setStorageSync('isLoggedIn', true) userData = userDataOrToken
finalToken = authToken
console.log('用户登录成功:', userData)
} }
token.value = finalToken
// 登录
const login = (authToken) => {
token.value = authToken
isLoggedIn.value = true isLoggedIn.value = true
// 保存到本地存储 // 保存到本地存储
uni.setStorageSync('token', authToken) uni.setStorageSync('token', finalToken)
uni.setStorageSync('isLoggedIn', true) uni.setStorageSync('isLoggedIn', true)
//读取用户信息 // 如果有 userInfo 就直接保存;没有就尝试拉取
getUserInfo(); if (userData) {
console.log('用户登录成功:', authToken) userInfo.value = userData
uni.setStorageSync('userInfo', userData)
} else {
try {
await getUserInfo()
} catch (e) {
// 拉取用户信息失败不阻断登录态(路由守卫会用到 token + isLoggedIn
console.warn('获取用户信息失败:', e)
}
}
console.log('用户登录成功:', finalToken)
} }
//获取用户信息 //获取用户信息

28
tsconfig.json Normal file
View File

@ -0,0 +1,28 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Node",
"allowJs": true,
"checkJs": false,
"strict": false,
"skipLibCheck": true,
"baseUrl": ".",
"paths": {
"@/*": ["*"],
"@": ["."]
},
"resolveJsonModule": true,
"isolatedModules": true
},
"include": [
"src/**/*",
"pages/**/*",
"components/**/*",
"App.vue",
"main.js",
"env.d.ts",
"**/*.vue"
],
"exclude": ["node_modules", "unpackage", "dist"]
}

View File

@ -1,13 +1,13 @@
{ {
"hash": "308de400", "hash": "d040dac9",
"configHash": "11378e7b", "configHash": "edf7a139",
"lockfileHash": "eccfc8e7", "lockfileHash": "eccfc8e7",
"browserHash": "42a8a35f", "browserHash": "dcaa9f00",
"optimized": { "optimized": {
"pinia-plugin-persistedstate": { "pinia-plugin-persistedstate": {
"src": "../../../../../node_modules/pinia-plugin-persistedstate/dist/index.js", "src": "../../../../../node_modules/pinia-plugin-persistedstate/dist/index.js",
"file": "pinia-plugin-persistedstate.js", "file": "pinia-plugin-persistedstate.js",
"fileHash": "53c30cff", "fileHash": "79ff99f1",
"needsInterop": false "needsInterop": false
} }
}, },

View File

@ -1,4 +1,4 @@
// E:/Demos/DemoOwns/PHP/official/mobile/node_modules/pinia-plugin-persistedstate/dist/index.js // E:/Demo/PHP/official_website/babyhealth/node_modules/pinia-plugin-persistedstate/dist/index.js
function get(obj, path) { function get(obj, path) {
if (obj == null) if (obj == null)
return void 0; return void 0;

View File

@ -0,0 +1,3 @@
{
"type": "module"
}

View File

@ -0,0 +1,143 @@
// E:/Demo/PHP/official_website/babyhealth/node_modules/pinia-plugin-persistedstate/dist/index.js
function get(obj, path) {
if (obj == null)
return void 0;
let value = obj;
for (let i = 0; i < path.length; i++) {
if (value === void 0 || value[path[i]] === void 0)
return void 0;
if (value === null || value[path[i]] === null)
return null;
value = value[path[i]];
}
return value;
}
function set(obj, value, path) {
if (path.length === 0)
return value;
const idx = path[0];
if (path.length > 1)
value = set(typeof obj !== "object" || obj === null || !Object.prototype.hasOwnProperty.call(obj, idx) ? Number.isInteger(Number(path[1])) ? [] : {} : obj[idx], value, Array.prototype.slice.call(path, 1));
if (Number.isInteger(Number(idx)) && Array.isArray(obj))
return obj.slice()[idx];
return Object.assign({}, obj, { [idx]: value });
}
function unset(obj, path) {
if (obj == null || path.length === 0)
return obj;
if (path.length === 1) {
if (obj == null)
return obj;
if (Number.isInteger(path[0]) && Array.isArray(obj))
return Array.prototype.slice.call(obj, 0).splice(path[0], 1);
const result = {};
for (const p in obj)
result[p] = obj[p];
delete result[path[0]];
return result;
}
if (obj[path[0]] == null) {
if (Number.isInteger(path[0]) && Array.isArray(obj))
return Array.prototype.concat.call([], obj);
const result = {};
for (const p in obj)
result[p] = obj[p];
return result;
}
return set(obj, unset(obj[path[0]], Array.prototype.slice.call(path, 1)), [path[0]]);
}
function deepPick(obj, paths) {
return paths.map((p) => p.split(".")).map((p) => [p, get(obj, p)]).filter((t) => t[1] !== void 0).reduce((acc, cur) => set(acc, cur[1], cur[0]), {});
}
function deepOmit(obj, paths) {
return paths.map((p) => p.split(".")).reduce((acc, cur) => unset(acc, cur), obj);
}
function hydrateStore(store, { storage, serializer, key, debug, pick, omit, beforeHydrate, afterHydrate }, context, runHooks = true) {
try {
if (runHooks)
beforeHydrate == null ? void 0 : beforeHydrate(context);
const fromStorage = storage.getItem(key);
if (fromStorage) {
const deserialized = serializer.deserialize(fromStorage);
const picked = pick ? deepPick(deserialized, pick) : deserialized;
const omitted = omit ? deepOmit(picked, omit) : picked;
store.$patch(omitted);
}
if (runHooks)
afterHydrate == null ? void 0 : afterHydrate(context);
} catch (error) {
if (debug)
console.error("[pinia-plugin-persistedstate]", error);
}
}
function persistState(state, { storage, serializer, key, debug, pick, omit }) {
try {
const picked = pick ? deepPick(state, pick) : state;
const omitted = omit ? deepOmit(picked, omit) : picked;
const toStorage = serializer.serialize(omitted);
storage.setItem(key, toStorage);
} catch (error) {
if (debug)
console.error("[pinia-plugin-persistedstate]", error);
}
}
function parsePersistKey(key, storeId) {
return typeof key === "function" ? key(storeId) : typeof key === "string" ? key : storeId;
}
function createPersistence(context, optionsParser, auto) {
const { pinia, store, options: { persist = auto } } = context;
if (!persist)
return;
if (!(store.$id in pinia.state.value)) {
const originalStore = pinia._s.get(store.$id.replace("__hot:", ""));
if (originalStore)
Promise.resolve().then(() => originalStore.$persist());
return;
}
const persistences = (Array.isArray(persist) ? persist : persist === true ? [{}] : [persist]).map(optionsParser);
store.$hydrate = ({ runHooks = true } = {}) => {
persistences.forEach((p) => {
hydrateStore(store, p, context, runHooks);
});
};
store.$persist = () => {
persistences.forEach((p) => {
persistState(store.$state, p);
});
};
persistences.forEach((p) => {
hydrateStore(store, p, context);
store.$subscribe((_mutation, state) => persistState(state, p), { detached: true });
});
}
function createPersistedState(options = {}) {
return function(context) {
createPersistence(context, (p) => {
const persistKey = parsePersistKey(p.key, context.store.$id);
return {
key: (options.key ? options.key : (x) => x)(persistKey),
debug: p.debug ?? options.debug ?? false,
serializer: p.serializer ?? options.serializer ?? {
serialize: (data) => JSON.stringify(data),
deserialize: (data) => JSON.parse(data)
},
storage: p.storage ?? options.storage ?? window.localStorage,
beforeHydrate: p.beforeHydrate ?? options.beforeHydrate,
afterHydrate: p.afterHydrate ?? options.afterHydrate,
pick: p.pick,
omit: p.omit
};
}, options.auto ?? false);
};
}
var src_default = createPersistedState();
export {
createPersistedState,
src_default as default
};
/*! Bundled license information:
pinia-plugin-persistedstate/dist/index.js:
(* v8 ignore if -- @preserve *)
*/
//# sourceMappingURL=pinia-plugin-persistedstate.js.map

File diff suppressed because one or more lines are too long

14
vite.config.js Normal file
View File

@ -0,0 +1,14 @@
import { defineConfig } from 'vite'
import uni from '@dcloudio/vite-plugin-uni'
import path from 'path'
// uni-app 项目的 Vite 配置
export default defineConfig({
plugins: [uni()],
resolve: {
alias: {
'@': path.resolve(__dirname, 'src')
}
}
})