From 6d118287dbbe8d0f13cb021f14eb5676eb6a9420 Mon Sep 17 00:00:00 2001 From: molong Date: Sat, 21 Feb 2026 14:57:05 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- resources/mobile/api/index.js | 135 ++++++++++++++++ resources/mobile/config/index.js | 37 +++++ resources/mobile/main.js | 62 ++++++++ resources/mobile/store/app.js | 147 +++++++++++++++++ resources/mobile/store/index.js | 11 ++ resources/mobile/store/user.js | 142 +++++++++++++++++ resources/mobile/utils/request.js | 256 ++++++++++++++++++++++++++++++ 7 files changed, 790 insertions(+) create mode 100644 resources/mobile/api/index.js create mode 100644 resources/mobile/config/index.js create mode 100644 resources/mobile/store/app.js create mode 100644 resources/mobile/store/index.js create mode 100644 resources/mobile/store/user.js create mode 100644 resources/mobile/utils/request.js diff --git a/resources/mobile/api/index.js b/resources/mobile/api/index.js new file mode 100644 index 0000000..8ba0fb1 --- /dev/null +++ b/resources/mobile/api/index.js @@ -0,0 +1,135 @@ +import request from '@/utils/request' + +export default { + // 用户相关 + user: { + // 登录 + login: (data) => { + return request.post('/api/auth/login', data, { + custom: { auth: false } // 登录不需要 Token + }) + }, + + // 注册 + register: (data) => { + return request.post('/api/auth/register', data, { + custom: { auth: false } + }) + }, + + // 获取用户信息 + getInfo: () => { + return request.get('/api/user/info') + }, + + // 更新用户信息 + updateInfo: (data) => { + return request.put('/api/user/info', data) + }, + + // 修改密码 + changePassword: (data) => { + return request.post('/api/user/password', data) + } + }, + + // 首页相关 + home: { + // 获取轮播图 + getBanner: () => { + return request.get('/api/home/banner') + }, + + // 获取推荐列表 + getRecommend: (params) => { + return request.get('/api/home/recommend', params) + } + }, + + // 文章相关 + article: { + // 文章列表 + getList: (params) => { + return request.get('/api/article/list', params) + }, + + // 文章详情 + getDetail: (id) => { + return request.get(`/api/article/detail/${id}`) + }, + + // 文章点赞 + like: (id) => { + return request.post(`/api/article/like/${id}`) + }, + + // 收藏文章 + collect: (id) => { + return request.post(`/api/article/collect/${id}`) + } + }, + + // 消息相关 + message: { + // 消息列表 + getList: (params) => { + return request.get('/api/message/list', params) + }, + + // 未读数量 + getUnreadCount: () => { + return request.get('/api/message/unread') + }, + + // 标记已读 + markRead: (id) => { + return request.put(`/api/message/read/${id}`) + }, + + // 全部已读 + markAllRead: () => { + return request.put('/api/message/read-all') + } + }, + + // 上传相关 + upload: { + // 上传图片 + image: (filePath, formData = {}) => { + return request.upload('/api/upload/image', filePath, formData, { + custom: { + loading: true, + loadingText: '上传中...' + } + }) + }, + + // 上传文件 + file: (filePath, formData = {}) => { + return request.upload('/api/upload/file', filePath, formData, { + custom: { + loading: true, + loadingText: '上传中...' + } + }) + } + }, + + // 通用接口 + common: { + // 获取字典 + getDict: (type) => { + return request.get('/api/common/dict', { type }) + }, + + // 获取配置 + getConfig: () => { + return request.get('/api/common/config') + }, + + // 意见反馈 + feedback: (data) => { + return request.post('/api/common/feedback', data) + } + } +} diff --git a/resources/mobile/config/index.js b/resources/mobile/config/index.js new file mode 100644 index 0000000..175be58 --- /dev/null +++ b/resources/mobile/config/index.js @@ -0,0 +1,37 @@ +// 环境配置 +export const config = { + // API 基础 URL + apiBaseUrl: import.meta.env.VITE_API_BASE_URL || 'https://api.example.com', + + // 应用配置 + appName: 'UniApp 移动应用', + appVersion: '1.0.0', + + // 分页配置 + pageSize: 20, + + // 存储键名 + storage: { + token: 'app_token', + userInfo: 'app_user_info', + settings: 'app_settings' + }, + + // 请求超时时间(毫秒) + timeout: 60000, + + // 上传配置 + upload: { + maxFileSize: 10 * 1024 * 1024, // 10MB + allowedTypes: ['jpg', 'jpeg', 'png', 'gif', 'pdf', 'doc', 'docx', 'xls', 'xlsx'] + } +} + +// 环境判断 +export const isDev = import.meta.env.DEV +export const isProd = import.meta.env.PROD + +// 当前环境 +export const env = import.meta.env.MODE || 'development' + +export default config diff --git a/resources/mobile/main.js b/resources/mobile/main.js index 782f8c2..97ebffc 100644 --- a/resources/mobile/main.js +++ b/resources/mobile/main.js @@ -1,9 +1,71 @@ import App from './App' +import pinia from './store' +import { useUserStore, useAppStore } from './store' import { createSSRApp } from 'vue' + export function createApp() { const app = createSSRApp(App) + + // 使用 pinia + app.use(pinia) + + // 初始化 store + const userStore = useUserStore() + const appStore = useAppStore() + + // 从本地存储初始化数据 + userStore.initFromStorage() + appStore.initFromStorage() + + // 设置设备类型 + appStore.setDevice(getDeviceType()) + return { app } } + +/** + * 获取设备类型 + * @returns {string} + */ +function getDeviceType() { + // #ifdef H5 + return 'h5' + // #endif + + // #ifdef APP-PLUS + return 'app' + // #endif + + // #ifdef MP-WEIXIN + return 'mp-weixin' + // #endif + + // #ifdef MP-ALIPAY + return 'mp-alipay' + // #endif + + // #ifdef MP-BAIDU + return 'mp-baidu' + // #endif + + // #ifdef MP-TOUTIAO + return 'mp-toutiao' + // #endif + + // #ifdef MP-QQ + return 'mp-qq' + // #endif + + // #ifdef MP-KS + return 'mp-ks' + // #endif + + // #ifdef MP-JD + return 'mp-jd' + // #endif + + return 'unknown' +} diff --git a/resources/mobile/store/app.js b/resources/mobile/store/app.js new file mode 100644 index 0000000..6a5283b --- /dev/null +++ b/resources/mobile/store/app.js @@ -0,0 +1,147 @@ +import { defineStore } from 'pinia' +import { ref } from 'vue' +import config from '@/config' + +export const useAppStore = defineStore('app', () => { + // State + const sidebarCollapsed = ref(false) + const device = ref('unknown') + const language = ref('zh-CN') + const theme = ref('light') + const settings = ref(null) + + // Actions + + /** + * 设置侧边栏折叠状态 + * @param {boolean} collapsed + */ + function setSidebarCollapsed(collapsed) { + sidebarCollapsed.value = collapsed + } + + /** + * 切换侧边栏 + */ + function toggleSidebar() { + sidebarCollapsed.value = !sidebarCollapsed.value + } + + /** + * 设置设备类型 + * @param {string} type - 'h5' | 'app' | 'mp-weixin' | 'mp-alipay' | 'mp-baidu' | 'mp-toutiao' | 'mp-qq' | 'mp-ks' | 'mp-jd' + */ + function setDevice(type) { + device.value = type + } + + /** + * 设置语言 + * @param {string} lang + */ + function setLanguage(lang) { + language.value = lang + uni.setStorageSync('app_language', lang) + } + + /** + * 设置主题 + * @param {string} mode - 'light' | 'dark' + */ + function setTheme(mode) { + theme.value = mode + uni.setStorageSync('app_theme', mode) + } + + /** + * 切换主题 + */ + function toggleTheme() { + const newTheme = theme.value === 'light' ? 'dark' : 'light' + setTheme(newTheme) + } + + /** + * 设置应用配置 + * @param {object} data + */ + function setSettings(data) { + settings.value = data + uni.setStorageSync(config.storage.settings, JSON.stringify(data)) + } + + /** + * 更新设置 + * @param {object} data + */ + function updateSettings(data) { + settings.value = { + ...settings.value, + ...data + } + uni.setStorageSync(config.storage.settings, JSON.stringify(settings.value)) + } + + /** + * 从本地存储初始化 + */ + function initFromStorage() { + // 获取语言设置 + const savedLanguage = uni.getStorageSync('app_language') + if (savedLanguage) { + language.value = savedLanguage + } + + // 获取主题设置 + const savedTheme = uni.getStorageSync('app_theme') + if (savedTheme) { + theme.value = savedTheme + } + + // 获取应用设置 + const savedSettings = uni.getStorageSync(config.storage.settings) + if (savedSettings) { + try { + settings.value = JSON.parse(savedSettings) + } catch (e) { + console.error('解析应用设置失败:', e) + settings.value = null + } + } + } + + /** + * 清除设置 + */ + function clearSettings() { + language.value = 'zh-CN' + theme.value = 'light' + settings.value = null + + // 清除本地存储 + uni.removeStorageSync('app_language') + uni.removeStorageSync('app_theme') + uni.removeStorageSync(config.storage.settings) + } + + return { + // State + sidebarCollapsed, + device, + language, + theme, + settings, + + // Actions + setSidebarCollapsed, + toggleSidebar, + setDevice, + setLanguage, + setTheme, + toggleTheme, + setSettings, + updateSettings, + initFromStorage, + clearSettings + } +}) diff --git a/resources/mobile/store/index.js b/resources/mobile/store/index.js new file mode 100644 index 0000000..93383e0 --- /dev/null +++ b/resources/mobile/store/index.js @@ -0,0 +1,11 @@ +// Store 入口文件 +import { createPinia } from 'pinia' + +// 导出所有 store +export { useUserStore } from './user' +export { useAppStore } from './app' + +// 创建 pinia 实例 +const pinia = createPinia() + +export default pinia diff --git a/resources/mobile/store/user.js b/resources/mobile/store/user.js new file mode 100644 index 0000000..407c3be --- /dev/null +++ b/resources/mobile/store/user.js @@ -0,0 +1,142 @@ +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' +import config from '@/config' + +export const useUserStore = defineStore('user', () => { + // State + const token = ref('') + const refreshToken = ref('') + const userInfo = ref(null) + + // Getters + const isLoggedIn = computed(() => !!token.value) + const userName = computed(() => userInfo.value?.name || '') + const userAvatar = computed(() => userInfo.value?.avatar || '') + const userId = computed(() => userInfo.value?.id || '') + + // Actions + + /** + * 设置 Token + * @param {string} newToken + */ + function setToken(newToken) { + token.value = newToken + uni.setStorageSync(config.storage.token, newToken) + } + + /** + * 设置刷新 Token + * @param {string} newRefreshToken + */ + function setRefreshToken(newRefreshToken) { + refreshToken.value = newRefreshToken + uni.setStorageSync('app_refresh_token', newRefreshToken) + } + + /** + * 设置用户信息 + * @param {object} info + */ + function setUserInfo(info) { + userInfo.value = info + uni.setStorageSync(config.storage.userInfo, JSON.stringify(info)) + } + + /** + * 获取用户信息 + * @returns {object|null} + */ + function getUserInfo() { + return userInfo.value + } + + /** + * 从本地存储初始化 + */ + function initFromStorage() { + // 获取 Token + const savedToken = uni.getStorageSync(config.storage.token) + if (savedToken) { + token.value = savedToken + } + + // 获取刷新 Token + const savedRefreshToken = uni.getStorageSync('app_refresh_token') + if (savedRefreshToken) { + refreshToken.value = savedRefreshToken + } + + // 获取用户信息 + const savedUserInfo = uni.getStorageSync(config.storage.userInfo) + if (savedUserInfo) { + try { + userInfo.value = JSON.parse(savedUserInfo) + } catch (e) { + console.error('解析用户信息失败:', e) + userInfo.value = null + } + } + } + + /** + * 清除用户数据 + */ + function clearUserData() { + token.value = '' + refreshToken.value = '' + userInfo.value = null + + // 清除本地存储 + uni.removeStorageSync(config.storage.token) + uni.removeStorageSync('app_refresh_token') + uni.removeStorageSync(config.storage.userInfo) + } + + /** + * 登出 + */ + function logout() { + clearUserData() + + // 跳转到登录页 + uni.reLaunch({ + url: '/pages/login/index' + }) + } + + /** + * 更新用户信息 + * @param {object} data + */ + function updateUserInfo(data) { + userInfo.value = { + ...userInfo.value, + ...data + } + uni.setStorageSync(config.storage.userInfo, JSON.stringify(userInfo.value)) + } + + return { + // State + token, + refreshToken, + userInfo, + + // Getters + isLoggedIn, + userName, + userAvatar, + userId, + + // Actions + setToken, + setRefreshToken, + setUserInfo, + getUserInfo, + initFromStorage, + clearUserData, + logout, + updateUserInfo + } +}) diff --git a/resources/mobile/utils/request.js b/resources/mobile/utils/request.js new file mode 100644 index 0000000..d20a72c --- /dev/null +++ b/resources/mobile/utils/request.js @@ -0,0 +1,256 @@ +import Request from 'luch-request' +import { useUserStore } from '@/store/user' + +// 请求配置 +const http = new Request({ + // 基础 URL + baseURL: import.meta.env.VITE_API_BASE_URL || 'https://api.example.com', + // 超时时间 + timeout: 60000, + // 请求头 + header: { + 'Content-Type': 'application/json;charset=UTF-8' + }, + // 自定义配置 + custom: { + auth: true, // 是否需要认证 + loading: true, // 是否显示加载提示 + loadingText: '加载中...', + showError: true, // 是否显示错误提示 + showErrorType: 'toast' // 错误提示方式:toast、modal、none + } +}) + +// 请求拦截器 +http.interceptors.request.use( + (config) => { + const userStore = useUserStore() + + // 添加 Token + if (config.custom?.auth !== false && userStore.token) { + config.header['Authorization'] = `Bearer ${userStore.token}` + } + + // 添加时间戳防止缓存 + if (config.method === 'GET' && config.params) { + config.params._t = Date.now() + } + + // 显示加载提示 + if (config.custom?.loading !== false) { + uni.showLoading({ + title: config.custom?.loadingText || '加载中...', + mask: true + }) + } + + return config + }, + (config) => { + // 请求错误 + uni.hideLoading() + return Promise.reject(config) + } +) + +// 响应拦截器 +http.interceptors.response.use( + (response) => { + // 隐藏加载提示 + uni.hideLoading() + + const { statusCode, data } = response + + // HTTP 状态码判断 + if (statusCode !== 200) { + handleHttpError(statusCode) + return Promise.reject(response) + } + + // 业务状态码判断 + if (data.code !== 200) { + handleBusinessError(data, response.config) + return Promise.reject(data) + } + + return data + }, + (error) => { + // 隐藏加载提示 + uni.hideLoading() + + // 网络错误处理 + handleNetworkError(error) + + return Promise.reject(error) + } +) + +// 处理 HTTP 错误 +const handleHttpError = (statusCode) => { + let message = '网络错误' + + switch (statusCode) { + case 400: + message = '请求参数错误' + break + case 401: + message = '未授权,请重新登录' + handleUnauthorized() + break + case 403: + message = '拒绝访问' + break + case 404: + message = '请求的资源不存在' + break + case 405: + message = '请求方法不允许' + break + case 408: + message = '请求超时' + break + case 500: + message = '服务器内部错误' + break + case 502: + message = '网关错误' + break + case 503: + message = '服务不可用' + break + case 504: + message = '网关超时' + break + default: + message = `连接错误 ${statusCode}` + } + + uni.showToast({ + title: message, + icon: 'none' + }) +} + +// 处理业务错误 +const handleBusinessError = (data, config) => { + const { code, message } = data + + // 是否显示错误提示 + const showError = config?.custom?.showError !== false + + if (!showError) { + return + } + + // 错误提示方式 + const errorType = config?.custom?.showErrorType || 'toast' + + switch (code) { + case 401: + // 未授权 + handleUnauthorized() + break + case 403: + // 无权限 + if (errorType === 'modal') { + uni.showModal({ + title: '提示', + content: message || '没有权限访问', + showCancel: false + }) + } else { + uni.showToast({ + title: message || '没有权限', + icon: 'none' + }) + } + break + default: + // 其他错误 + if (errorType === 'modal') { + uni.showModal({ + title: '提示', + content: message || '操作失败', + showCancel: false + }) + } else if (errorType !== 'none') { + uni.showToast({ + title: message || '操作失败', + icon: 'none' + }) + } + } +} + +// 处理网络错误 +const handleNetworkError = (error) => { + let message = '网络连接失败' + + if (error.errMsg) { + if (error.errMsg.includes('timeout')) { + message = '请求超时,请稍后重试' + } else if (error.errMsg.includes('request:fail')) { + message = '网络连接失败,请检查网络' + } else if (error.errMsg.includes('abort')) { + message = '请求已取消' + } + } + + uni.showToast({ + title: message, + icon: 'none' + }) +} + +// 处理未授权 +const handleUnauthorized = () => { + const userStore = useUserStore() + + // 清除用户信息 + userStore.logout() + + // 跳转到登录页 + uni.reLaunch({ + url: '/pages/login/index' + }) +} + +// 请求方法封装 +const request = { + // GET 请求 + get(url, params = {}, config = {}) { + return http.get(url, { params, ...config }) + }, + + // POST 请求 + post(url, data = {}, config = {}) { + return http.post(url, data, config) + }, + + // PUT 请求 + put(url, data = {}, config = {}) { + return http.put(url, data, config) + }, + + // DELETE 请求 + delete(url, data = {}, config = {}) { + return http.delete(url, data, config) + }, + + // 上传文件 + upload(url, filePath, formData = {}, config = {}) { + return http.upload(url, { + filePath, + formData, + ...config + }) + }, + + // 下载文件 + download(url, config = {}) { + return http.download(url, config) + } +} + +export default request