feat: Phase 3 - API layer + Pinia stores + app integration
- HTTP client (axios interceptors, token mgmt, typed APIs) - Pinia stores: token, user (login/logout), app (dark mode, sidebar) - globalConfig TS types + Window augmentation - Vue Router (hash history, auth guard) - Login/Home/Mine pages (Vant UI) - Vant integration + globalConfig dev script - Build passes (vue-tsc + vite)
This commit is contained in:
@@ -1,13 +1,15 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||||
<title>yuto-water-h5</title>
|
<title>舆图智慧水务</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
<!-- 运行时全局配置 — 开发环境 -->
|
||||||
|
<script src="/config/globalConfig.dev.js"></script>
|
||||||
<script type="module" src="/src/main.ts"></script>
|
<script type="module" src="/src/main.ts"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
24
package-lock.json
generated
24
package-lock.json
generated
@@ -20,7 +20,8 @@
|
|||||||
"supercluster": "^8.0.1",
|
"supercluster": "^8.0.1",
|
||||||
"uuid": "^14.0.0",
|
"uuid": "^14.0.0",
|
||||||
"vant": "^4.9.24",
|
"vant": "^4.9.24",
|
||||||
"vue": "^3.5.34"
|
"vue": "^3.5.34",
|
||||||
|
"vue-router": "^4.6.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^24.12.3",
|
"@types/node": "^24.12.3",
|
||||||
@@ -5035,6 +5036,27 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/vue-router": {
|
||||||
|
"version": "4.6.4",
|
||||||
|
"resolved": "http://mirrors.tencentyun.com/npm/vue-router/-/vue-router-4.6.4.tgz",
|
||||||
|
"integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@vue/devtools-api": "^6.6.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/posva"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"vue": "^3.5.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/vue-router/node_modules/@vue/devtools-api": {
|
||||||
|
"version": "6.6.4",
|
||||||
|
"resolved": "http://mirrors.tencentyun.com/npm/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
|
||||||
|
"integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/vue-tsc": {
|
"node_modules/vue-tsc": {
|
||||||
"version": "3.3.5",
|
"version": "3.3.5",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
|||||||
@@ -21,7 +21,8 @@
|
|||||||
"supercluster": "^8.0.1",
|
"supercluster": "^8.0.1",
|
||||||
"uuid": "^14.0.0",
|
"uuid": "^14.0.0",
|
||||||
"vant": "^4.9.24",
|
"vant": "^4.9.24",
|
||||||
"vue": "^3.5.34"
|
"vue": "^3.5.34",
|
||||||
|
"vue-router": "^4.6.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^24.12.3",
|
"@types/node": "^24.12.3",
|
||||||
|
|||||||
148
public/config/globalConfig.dev.js
Normal file
148
public/config/globalConfig.dev.js
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
/**
|
||||||
|
* @Description: 全局配置(开发环境)
|
||||||
|
*
|
||||||
|
* 此文件在 index.html 中通过 <script> 标签加载,
|
||||||
|
* 将配置挂载到 window.globalConfig 供应用启动时读取。
|
||||||
|
*
|
||||||
|
* 生产环境部署时,由运维/后端将此文件替换为对应环境的配置。
|
||||||
|
*/
|
||||||
|
window.globalConfig = {
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
// 系统基础配置
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
system: {
|
||||||
|
/** 系统名称(登录页、导航栏展示) */
|
||||||
|
name: '舆图智慧水务',
|
||||||
|
/** 系统编码(集成管控平台中的子系统编码) */
|
||||||
|
id: '4b9ce59235794ff5b6785dd0bf4ddf9e',
|
||||||
|
/** 附件存储桶名称 */
|
||||||
|
buckName: 'swfile',
|
||||||
|
/** 符号库存储桶名称 */
|
||||||
|
buckNameMetadata: 'sw-metadata',
|
||||||
|
/** 附件存储桶地址 */
|
||||||
|
imgLoadUrl: 'http://10.10.10.189:81/icp-api',
|
||||||
|
/** 版本信息 */
|
||||||
|
version: 'v2.0.0',
|
||||||
|
/** 是否是微信小程序 */
|
||||||
|
isMiniProgram: false,
|
||||||
|
/** navbar 高度(px) */
|
||||||
|
navHeight: 76,
|
||||||
|
/** 轨迹记录时间间隔(毫秒,1分钟) */
|
||||||
|
trackTime: 1000 * 60,
|
||||||
|
/** 班组打卡轨迹记录距离间隔(毫秒,10分钟) */
|
||||||
|
groupsTrackTime: 1000 * 600,
|
||||||
|
/** 默认行政区划等级:PROVINCE | CITY | COUNTY | TOWNSHIP */
|
||||||
|
regionLevel: 'COUNTY',
|
||||||
|
/** 系统首页标题 */
|
||||||
|
homeTitle: '智慧水务',
|
||||||
|
/** 系统描述 */
|
||||||
|
description: '舆图智慧水务移动端',
|
||||||
|
},
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
// API 接口配置
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
api: {
|
||||||
|
/** API 主机地址 */
|
||||||
|
host: 'http://10.10.10.189:81/',
|
||||||
|
/** API 基础路径 */
|
||||||
|
baseURL: 'http://10.10.10.189:81/icp-api',
|
||||||
|
// baseURL: '/dev-api', // 代理模式
|
||||||
|
/** 智慧水务服务路径 */
|
||||||
|
swPath: '/wisdomwater',
|
||||||
|
// swPath: '/wisdomwatersm', // 思明环境
|
||||||
|
/** 天地图接口服务 */
|
||||||
|
tdt: 'https://api.tianditu.gov.cn',
|
||||||
|
tdtToken: '72f4500be0826ec2443f0794b05bec0f',
|
||||||
|
/** 高德地图接口服务 */
|
||||||
|
amap: 'https://restapi.amap.com',
|
||||||
|
amapKey: 'c6c64f95ef9ca6f504d4b676a0a77c7d',
|
||||||
|
/** 台风数据接口服务 */
|
||||||
|
typhoon: 'http://183.252.1.27:8082',
|
||||||
|
/** 请求超时时间(毫秒) */
|
||||||
|
timeout: 30000,
|
||||||
|
},
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
// 地图配置
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
map: {
|
||||||
|
/** 地图中心点 [经度, 纬度] */
|
||||||
|
center: [118.734575, 31.990939],
|
||||||
|
// center: [118.112785, 24.486653],
|
||||||
|
/** 地图默认缩放级别 */
|
||||||
|
zoom: 10,
|
||||||
|
/** 地图最大缩放级别 */
|
||||||
|
maxZoom: 20,
|
||||||
|
/** 地图最小缩放级别 */
|
||||||
|
minZoom: 3,
|
||||||
|
/** Token 拦截配置 */
|
||||||
|
interceptors: [
|
||||||
|
{
|
||||||
|
urls: 'https://map.zygh.xm.gov.cn:6060',
|
||||||
|
params: {
|
||||||
|
token: '11eA5fuVv89GgFd2zJRJj44OQsDuHzCf-LuiYqjfqF3D3RlrV4hGzv9ev6ckf8eZbGGW2obvw9zHKU5g0XU-6w..',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
/** 基础图层列表 */
|
||||||
|
baseLayers: [],
|
||||||
|
/** 地图类型 */
|
||||||
|
type: 'tdt',
|
||||||
|
},
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
// 排水管网配置
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
pipe: {
|
||||||
|
/** 检查井 ID */
|
||||||
|
checkId: '32',
|
||||||
|
},
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
// 巡检养护配置
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
xjyhSys: {
|
||||||
|
/** (防汛)养护单位父级 code */
|
||||||
|
fxyhdwParentCode: 'yhdw',
|
||||||
|
/** 巡检单位父级 code */
|
||||||
|
xjdwParentCode: 'swxj',
|
||||||
|
},
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
// 打卡配置
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
clockIn: {
|
||||||
|
/** 打卡距离(米) */
|
||||||
|
distance: 50,
|
||||||
|
},
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
// 系统加载服务类型
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
loadMapType: {
|
||||||
|
/** 基础服务 */
|
||||||
|
basic: 'base-map',
|
||||||
|
/** 专题服务 */
|
||||||
|
thematic: 'thematic-map',
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 基础服务缓存 Key */
|
||||||
|
CACHE_KEY: 'loadMapListCacheSearch',
|
||||||
|
/** 所有基础服务缓存 Key */
|
||||||
|
CACHE_KEY_ALL: 'loadMapListCacheSearch_ALL',
|
||||||
|
/** 专题判断标识列表 */
|
||||||
|
specialFeature: [],
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
// 设备类型配置(雨情/水情/工情)
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
sbType: {
|
||||||
|
/** 雨情设备 */
|
||||||
|
yq: ['雨量计'],
|
||||||
|
/** 水情设备 */
|
||||||
|
sq: ['水位计', '河道水位', '电子水尺'],
|
||||||
|
/** 工情设备 */
|
||||||
|
gq: ['雨水泵站', '闸门', '截流井', '拍门'],
|
||||||
|
},
|
||||||
|
}
|
||||||
19
src/App.vue
19
src/App.vue
@@ -1,7 +1,22 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import HelloWorld from './components/HelloWorld.vue'
|
/**
|
||||||
|
* 应用根组件
|
||||||
|
*
|
||||||
|
* 使用 Vant ConfigProvider 包裹路由视图,
|
||||||
|
* 提供全局主题配置和路由出口。
|
||||||
|
*/
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<HelloWorld />
|
<van-config-provider>
|
||||||
|
<router-view />
|
||||||
|
</van-config-provider>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* App-level 全局样式调整 */
|
||||||
|
#app {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
139
src/api/common.ts
Normal file
139
src/api/common.ts
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
/**
|
||||||
|
* 通用 API
|
||||||
|
*
|
||||||
|
* 包含文件上传、字典查询、验证码获取、系统配置等公共接口。
|
||||||
|
*
|
||||||
|
* @module api/common
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { post, get } from '@/utils/http'
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
// 请求参数类型
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
|
||||||
|
/** 文件上传响应 */
|
||||||
|
export interface UploadResult {
|
||||||
|
/** 文件在服务器上的存储名称 */
|
||||||
|
fileName: string
|
||||||
|
/** 文件可访问的完整 URL */
|
||||||
|
url: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 字典项 */
|
||||||
|
export interface DictItem {
|
||||||
|
/** 字典标签(显示用) */
|
||||||
|
dictLabel: string
|
||||||
|
/** 字典值(存储用) */
|
||||||
|
dictValue: string
|
||||||
|
/** 字典类型 */
|
||||||
|
dictType?: string
|
||||||
|
/** CSS 类名(用于表格回显样式) */
|
||||||
|
cssClass?: string
|
||||||
|
/** 列表样式 */
|
||||||
|
listClass?: string
|
||||||
|
/** 是否默认 */
|
||||||
|
isDefault?: string
|
||||||
|
/** 排序号 */
|
||||||
|
dictSort?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 验证码图片响应 */
|
||||||
|
export interface CaptchaResult {
|
||||||
|
/** 验证码唯一标识,登录时需要回传 */
|
||||||
|
uuid: string
|
||||||
|
/** Base64 编码的验证码图片 */
|
||||||
|
img: string
|
||||||
|
/** 是否开启验证码 */
|
||||||
|
captchaEnabled: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 系统配置项 */
|
||||||
|
export interface ConfigItem {
|
||||||
|
/** 参数键名 */
|
||||||
|
configKey: string
|
||||||
|
/** 参数值 */
|
||||||
|
configValue: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
// API 方法
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传文件
|
||||||
|
*
|
||||||
|
* 支持图片、文档等文件上传,使用 FormData 格式。
|
||||||
|
*
|
||||||
|
* @param file - 要上传的文件对象
|
||||||
|
* @returns 上传结果,包含文件名和访问 URL
|
||||||
|
*/
|
||||||
|
export function uploadFile(file: File): Promise<UploadResult> {
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('file', file)
|
||||||
|
|
||||||
|
return post<UploadResult>('/common/upload', formData, {
|
||||||
|
headers: { 'Content-Type': 'multipart/form-data' },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量上传文件
|
||||||
|
* @param files - 文件对象数组
|
||||||
|
* @returns 上传结果数组
|
||||||
|
*/
|
||||||
|
export async function uploadFiles(files: File[]): Promise<UploadResult[]> {
|
||||||
|
const results: UploadResult[] = []
|
||||||
|
for (const file of files) {
|
||||||
|
const result = await uploadFile(file)
|
||||||
|
results.push(result)
|
||||||
|
}
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取字典数据
|
||||||
|
*
|
||||||
|
* 根据字典类型查询对应的字典项列表,用于下拉框、单选框等组件的数据源。
|
||||||
|
*
|
||||||
|
* @param dictType - 字典类型标识,如 'sys_user_sex'
|
||||||
|
* @returns 字典项数组
|
||||||
|
*/
|
||||||
|
export function getDictData(dictType: string): Promise<DictItem[]> {
|
||||||
|
return get<DictItem[]>('/system/dict/data/type', { dictType })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据字典类型获取字典数据(RESTful 风格)
|
||||||
|
* @param dictType - 字典类型标识
|
||||||
|
* @returns 字典项数组
|
||||||
|
*/
|
||||||
|
export function getDictDataByType(dictType: string): Promise<DictItem[]> {
|
||||||
|
return get<DictItem[]>(`/system/dict/data/type/${dictType}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取验证码图片
|
||||||
|
* @returns 验证码信息(uuid 和 base64 图片)
|
||||||
|
*/
|
||||||
|
export function getCaptcha(): Promise<CaptchaResult> {
|
||||||
|
return get<CaptchaResult>('/captchaImage')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据配置键获取系统配置值
|
||||||
|
* @param configKey - 配置键名
|
||||||
|
* @returns 配置项的值
|
||||||
|
*/
|
||||||
|
export function getConfigValue(configKey: string): Promise<string> {
|
||||||
|
return get<string>(`/system/config/configKey/${configKey}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取系统配置项详情
|
||||||
|
* @param configKey - 配置键名
|
||||||
|
* @returns 配置项完整信息
|
||||||
|
*/
|
||||||
|
export function getConfigItem(configKey: string): Promise<ConfigItem> {
|
||||||
|
return get<ConfigItem>(`/system/config/configKey/${configKey}`)
|
||||||
|
}
|
||||||
43
src/api/index.ts
Normal file
43
src/api/index.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
/**
|
||||||
|
* API 模块聚合入口
|
||||||
|
*
|
||||||
|
* 统一导出所有 API 子模块的方法和类型,业务代码只需
|
||||||
|
* `import { login, getUserInfo } from '@/api'` 即可使用。
|
||||||
|
*
|
||||||
|
* @module api
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ── 用户模块 ──
|
||||||
|
export {
|
||||||
|
login,
|
||||||
|
getUserInfo,
|
||||||
|
logout,
|
||||||
|
resetPassword,
|
||||||
|
} from './user'
|
||||||
|
export type {
|
||||||
|
LoginParams,
|
||||||
|
LoginResult,
|
||||||
|
ResetPasswordParams,
|
||||||
|
UserInfo,
|
||||||
|
} from './user'
|
||||||
|
|
||||||
|
// ── 通用模块 ──
|
||||||
|
export {
|
||||||
|
uploadFile,
|
||||||
|
uploadFiles,
|
||||||
|
getDictData,
|
||||||
|
getDictDataByType,
|
||||||
|
getCaptcha,
|
||||||
|
getConfigValue,
|
||||||
|
getConfigItem,
|
||||||
|
} from './common'
|
||||||
|
export type {
|
||||||
|
UploadResult,
|
||||||
|
DictItem,
|
||||||
|
CaptchaResult,
|
||||||
|
ConfigItem,
|
||||||
|
} from './common'
|
||||||
|
|
||||||
|
// ── HTTP 工具(便捷导出) ──
|
||||||
|
export { getToken, setToken, removeToken } from '@/utils/http'
|
||||||
|
export type { ApiResponse } from '@/utils/http'
|
||||||
105
src/api/user.ts
Normal file
105
src/api/user.ts
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
/**
|
||||||
|
* 用户相关 API
|
||||||
|
*
|
||||||
|
* 包含登录、获取用户信息、退出登录、重置密码等用户模块接口。
|
||||||
|
*
|
||||||
|
* @module api/user
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { post, get } from '@/utils/http'
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
// 请求参数类型
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
|
||||||
|
/** 登录请求参数 */
|
||||||
|
export interface LoginParams {
|
||||||
|
/** 用户名 */
|
||||||
|
username: string
|
||||||
|
/** 密码 */
|
||||||
|
password: string
|
||||||
|
/** 验证码 */
|
||||||
|
code?: string
|
||||||
|
/** 验证码唯一标识 */
|
||||||
|
uuid?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 重置密码请求参数 */
|
||||||
|
export interface ResetPasswordParams {
|
||||||
|
/** 旧密码 */
|
||||||
|
oldPassword: string
|
||||||
|
/** 新密码 */
|
||||||
|
newPassword: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
// 响应数据类型
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
|
||||||
|
/** 登录返回结果 */
|
||||||
|
export interface LoginResult {
|
||||||
|
/** 认证 Token */
|
||||||
|
token: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 用户信息 */
|
||||||
|
export interface UserInfo {
|
||||||
|
/** 用户 ID */
|
||||||
|
userId: string | number
|
||||||
|
/** 用户名 */
|
||||||
|
userName: string
|
||||||
|
/** 昵称 */
|
||||||
|
nickName: string
|
||||||
|
/** 头像地址 */
|
||||||
|
avatar: string
|
||||||
|
/** 手机号 */
|
||||||
|
phonenumber?: string
|
||||||
|
/** 邮箱 */
|
||||||
|
email?: string
|
||||||
|
/** 性别(0-男 1-女 2-未知) */
|
||||||
|
sex?: string
|
||||||
|
/** 部门名称 */
|
||||||
|
deptName?: string
|
||||||
|
/** 角色列表 */
|
||||||
|
roles?: string[]
|
||||||
|
/** 权限列表 */
|
||||||
|
permissions?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
// API 方法
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户登录
|
||||||
|
* @param params - 登录参数(用户名、密码、验证码等)
|
||||||
|
* @returns 返回包含 Token 的登录结果
|
||||||
|
*/
|
||||||
|
export function login(params: LoginParams): Promise<LoginResult> {
|
||||||
|
return post<LoginResult>('/login', params)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前登录用户信息
|
||||||
|
* @returns 用户详细信息
|
||||||
|
*/
|
||||||
|
export function getUserInfo(): Promise<UserInfo> {
|
||||||
|
return get<UserInfo>('/getInfo')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 退出登录
|
||||||
|
* @returns Promise<void>
|
||||||
|
*/
|
||||||
|
export function logout(): Promise<void> {
|
||||||
|
return post<void>('/logout')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重置密码
|
||||||
|
* @param params - 旧密码和新密码
|
||||||
|
* @returns Promise<void>
|
||||||
|
*/
|
||||||
|
export function resetPassword(params: ResetPasswordParams): Promise<void> {
|
||||||
|
return post<void>('/system/user/profile/resetPwd', params)
|
||||||
|
}
|
||||||
167
src/config/types.ts
Normal file
167
src/config/types.ts
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
/**
|
||||||
|
* 全局配置类型声明
|
||||||
|
*
|
||||||
|
* 声明 window.globalConfig 的完整 TypeScript 类型,
|
||||||
|
* 该配置由 public/config/globalConfig.js 在页面加载时注入。
|
||||||
|
* 同时扩展 Window 接口,使得 TypeScript 能够正确识别
|
||||||
|
* window.globalConfig 及其子属性。
|
||||||
|
*
|
||||||
|
* @module config/types
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
// 全局配置子模块类型
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
|
||||||
|
/** 系统基础配置 */
|
||||||
|
export interface SystemConfig {
|
||||||
|
/** 系统名称(登录页、系统左上角展示) */
|
||||||
|
name: string
|
||||||
|
/** 系统编码(在集成管控平台中的子系统编码) */
|
||||||
|
id: string
|
||||||
|
/** Bucket 名称(用于图片等静态资源存储) */
|
||||||
|
buckName: string
|
||||||
|
/** 系统版本号 */
|
||||||
|
version: string
|
||||||
|
/** 系统 Logo 地址 */
|
||||||
|
logo?: string
|
||||||
|
/** 系统首页标题 */
|
||||||
|
homeTitle?: string
|
||||||
|
/** 系统描述 */
|
||||||
|
description?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** API 接口配置 */
|
||||||
|
export interface ApiConfig {
|
||||||
|
/** API 后端基础地址 */
|
||||||
|
baseURL: string
|
||||||
|
/** API 主机地址(图片上传下载等场景) */
|
||||||
|
host: string
|
||||||
|
/** WebSocket 连接地址 */
|
||||||
|
wsURL?: string
|
||||||
|
/** 请求超时时间(毫秒) */
|
||||||
|
timeout?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 地图服务配置 */
|
||||||
|
export interface MapConfig {
|
||||||
|
/** 地图类型(如 'tdt' 天地图、'amap' 高德、'baidu' 百度) */
|
||||||
|
type: string
|
||||||
|
/** 地图服务地址 */
|
||||||
|
url: string
|
||||||
|
/** 地图默认中心点坐标 [经度, 纬度] */
|
||||||
|
center?: [number, number]
|
||||||
|
/** 地图默认缩放级别 */
|
||||||
|
zoom?: number
|
||||||
|
/** 地图最大缩放级别 */
|
||||||
|
maxZoom?: number
|
||||||
|
/** 地图最小缩放级别 */
|
||||||
|
minZoom?: number
|
||||||
|
/** 地图瓦片地址 */
|
||||||
|
tileUrl?: string
|
||||||
|
/** 天地图 Key(type 为 tdt 时使用) */
|
||||||
|
tdtKey?: string
|
||||||
|
/** 地图样式地址 */
|
||||||
|
styleUrl?: string
|
||||||
|
/** 是否开启地图注记 */
|
||||||
|
label?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 管网配置 */
|
||||||
|
export interface PipeConfig {
|
||||||
|
/** 管网数据服务地址 */
|
||||||
|
url?: string
|
||||||
|
/** 管网默认图层 */
|
||||||
|
defaultLayer?: string
|
||||||
|
/** 管网高亮颜色 */
|
||||||
|
highlightColor?: string
|
||||||
|
/** 管网选中颜色 */
|
||||||
|
selectedColor?: string
|
||||||
|
/** 管道直径范围 */
|
||||||
|
diameterRange?: [number, number]
|
||||||
|
/** 是否展示管网标注 */
|
||||||
|
showLabel?: boolean
|
||||||
|
/** 管网数据版本 */
|
||||||
|
version?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 打卡配置 */
|
||||||
|
export interface ClockInConfig {
|
||||||
|
/** 打卡范围半径(米) */
|
||||||
|
radius?: number
|
||||||
|
/** 打卡地点列表 */
|
||||||
|
locations?: Array<{
|
||||||
|
/** 地点名称 */
|
||||||
|
name: string
|
||||||
|
/** 经度 */
|
||||||
|
lng: number
|
||||||
|
/** 纬度 */
|
||||||
|
lat: number
|
||||||
|
/** 有效范围半径(米) */
|
||||||
|
radius?: number
|
||||||
|
}>
|
||||||
|
/** 是否开启拍照打卡 */
|
||||||
|
needPhoto?: boolean
|
||||||
|
/** 是否开启定位验证 */
|
||||||
|
needLocation?: boolean
|
||||||
|
/** 打卡时间限制(如 '09:00-18:00') */
|
||||||
|
timeRange?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 新简易运维系统配置 */
|
||||||
|
export interface XjyhSysConfig {
|
||||||
|
/** 是否启用新简易运维 */
|
||||||
|
enabled?: boolean
|
||||||
|
/** 运维系统地址 */
|
||||||
|
url?: string
|
||||||
|
/** 运维系统 AppId */
|
||||||
|
appId?: string
|
||||||
|
/** 运维系统密钥 */
|
||||||
|
secret?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 全局配置对象类型
|
||||||
|
*
|
||||||
|
* 由 public/config/globalConfig.js 在页面加载时注入到
|
||||||
|
* window.globalConfig,并在应用启动时通过接口获取远程配置
|
||||||
|
* 进行合并。
|
||||||
|
*/
|
||||||
|
export interface GlobalConfig {
|
||||||
|
/** 系统基础配置 */
|
||||||
|
system: SystemConfig
|
||||||
|
/** API 接口配置 */
|
||||||
|
api: ApiConfig
|
||||||
|
/** 地图服务配置 */
|
||||||
|
map: MapConfig
|
||||||
|
/** 管网配置 */
|
||||||
|
pipe: PipeConfig
|
||||||
|
/** 打卡配置 */
|
||||||
|
clockIn: ClockInConfig
|
||||||
|
/** 新简易运维系统配置 */
|
||||||
|
xjyhSys: XjyhSysConfig
|
||||||
|
/** 设备类型配置(key 为类型编码,value 为类型名称) */
|
||||||
|
sbType: Record<string, string>
|
||||||
|
/** 地图加载类型配置 */
|
||||||
|
loadMapType: string
|
||||||
|
/** 其他扩展配置(预留字段) */
|
||||||
|
[key: string]: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
// Window 接口扩展
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
/**
|
||||||
|
* 运行时全局配置对象
|
||||||
|
*
|
||||||
|
* 由 public/config/globalConfig.js 在 index.html 中通过
|
||||||
|
* `<script>` 标签注入,前端启动时合并远程配置后使用。
|
||||||
|
*/
|
||||||
|
globalConfig: GlobalConfig
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {}
|
||||||
116
src/main.ts
116
src/main.ts
@@ -1,5 +1,119 @@
|
|||||||
|
/**
|
||||||
|
* 应用入口 — Yuto Water H5
|
||||||
|
*
|
||||||
|
* 初始化 Vue 应用,集成 Vant 4 UI、Pinia 状态管理、
|
||||||
|
* Vue Router 路由、全局样式等核心模块。
|
||||||
|
*
|
||||||
|
* @module main
|
||||||
|
*/
|
||||||
|
|
||||||
import { createApp } from 'vue'
|
import { createApp } from 'vue'
|
||||||
|
import { createPinia } from 'pinia'
|
||||||
|
|
||||||
|
// ── Vant 4 组件按需注册 ──
|
||||||
|
import {
|
||||||
|
ConfigProvider,
|
||||||
|
NavBar,
|
||||||
|
Tabbar,
|
||||||
|
TabbarItem,
|
||||||
|
Button,
|
||||||
|
Form,
|
||||||
|
Field,
|
||||||
|
Cell,
|
||||||
|
CellGroup,
|
||||||
|
Popup,
|
||||||
|
Uploader,
|
||||||
|
} from 'vant'
|
||||||
|
|
||||||
|
// ── Vant 样式(组件样式按需引入) ──
|
||||||
|
import 'vant/lib/index.css'
|
||||||
|
|
||||||
|
// ── 全局样式 ──
|
||||||
import './styles/index.scss'
|
import './styles/index.scss'
|
||||||
|
|
||||||
|
// ── 路由 ──
|
||||||
|
import router from './router'
|
||||||
|
|
||||||
|
// ── 根组件 ──
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
|
|
||||||
createApp(App).mount('#app')
|
// ══════════════════════════════════════════════
|
||||||
|
// 创建应用
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
|
||||||
|
const app = createApp(App)
|
||||||
|
|
||||||
|
// ── 注册 Pinia ──
|
||||||
|
const pinia = createPinia()
|
||||||
|
app.use(pinia)
|
||||||
|
|
||||||
|
// ── 注册 Vant 组件(组件类) ──
|
||||||
|
// Toast / Dialog / ImagePreview 为函数式 API,
|
||||||
|
// 直接从 vant 导入即可使用,无需 app.use 注册。
|
||||||
|
const vantComponents = [
|
||||||
|
ConfigProvider,
|
||||||
|
NavBar,
|
||||||
|
Tabbar,
|
||||||
|
TabbarItem,
|
||||||
|
Button,
|
||||||
|
Form,
|
||||||
|
Field,
|
||||||
|
Cell,
|
||||||
|
CellGroup,
|
||||||
|
Popup,
|
||||||
|
Uploader,
|
||||||
|
]
|
||||||
|
|
||||||
|
for (const component of vantComponents) {
|
||||||
|
app.use(component)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 注册路由 ──
|
||||||
|
app.use(router)
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
// 挂载应用(带错误处理)
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
|
||||||
|
try {
|
||||||
|
app.mount('#app')
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[Yuto Water H5] 应用挂载失败:', err)
|
||||||
|
// 在 DOM 中展示降级错误信息
|
||||||
|
const root = document.getElementById('app')
|
||||||
|
if (root) {
|
||||||
|
root.innerHTML = `
|
||||||
|
<div style="
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100vh;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
|
||||||
|
color: #323233;
|
||||||
|
text-align: center;
|
||||||
|
padding: 24px;
|
||||||
|
">
|
||||||
|
<h1 style="font-size: 20px; margin-bottom: 8px;">应用加载失败</h1>
|
||||||
|
<p style="font-size: 14px; color: #969799;">
|
||||||
|
${err instanceof Error ? err.message : '未知错误'}
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
onclick="location.reload()"
|
||||||
|
style="
|
||||||
|
margin-top: 16px;
|
||||||
|
padding: 8px 24px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #2196F3;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
重新加载
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
122
src/router/index.ts
Normal file
122
src/router/index.ts
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
/**
|
||||||
|
* 路由配置
|
||||||
|
*
|
||||||
|
* 基于 Vue Router 4 的 Hash 模式路由,
|
||||||
|
* 包含登录、首页、个人中心等核心页面路由定义。
|
||||||
|
* 通过全局前置守卫实现 Token 认证拦截。
|
||||||
|
*
|
||||||
|
* @module router
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
createRouter,
|
||||||
|
createWebHashHistory,
|
||||||
|
type RouteRecordRaw,
|
||||||
|
type NavigationGuardNext,
|
||||||
|
type RouteLocationNormalized,
|
||||||
|
} from 'vue-router'
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
// 常量
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
|
||||||
|
/** 白名单路由 — 无需 Token 即可访问 */
|
||||||
|
const WHITE_LIST: string[] = ['/login']
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
// 路由表
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 动态路由(懒加载)
|
||||||
|
*
|
||||||
|
* 使用 import() 实现路由级别的代码分割,
|
||||||
|
* 配合 Vite 自动生成独立的 chunk 文件。
|
||||||
|
*/
|
||||||
|
const routes: RouteRecordRaw[] = [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
redirect: '/home',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/login',
|
||||||
|
name: 'Login',
|
||||||
|
component: () => import('@/views/login/index.vue'),
|
||||||
|
meta: {
|
||||||
|
title: '登录',
|
||||||
|
noAuth: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/home',
|
||||||
|
name: 'Home',
|
||||||
|
component: () => import('@/views/home/index.vue'),
|
||||||
|
meta: {
|
||||||
|
title: '首页',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/mine',
|
||||||
|
name: 'Mine',
|
||||||
|
component: () => import('@/views/mine/index.vue'),
|
||||||
|
meta: {
|
||||||
|
title: '我的',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// 404 兜底
|
||||||
|
{
|
||||||
|
path: '/:pathMatch(.*)*',
|
||||||
|
name: 'NotFound',
|
||||||
|
redirect: '/home',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
// 创建路由实例
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
// Hash 模式 — 兼容移动端 WebView 和微信内嵌浏览器
|
||||||
|
history: createWebHashHistory(),
|
||||||
|
routes,
|
||||||
|
})
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
// 全局前置守卫 — 认证拦截
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
|
||||||
|
router.beforeEach(
|
||||||
|
(
|
||||||
|
to: RouteLocationNormalized,
|
||||||
|
_from: RouteLocationNormalized,
|
||||||
|
next: NavigationGuardNext,
|
||||||
|
) => {
|
||||||
|
// 更新页面标题
|
||||||
|
if (to.meta.title) {
|
||||||
|
document.title = `${to.meta.title} - 舆图智慧水务`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 白名单路由直接放行
|
||||||
|
if (WHITE_LIST.includes(to.path) || to.meta.noAuth) {
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查 Token 是否存在
|
||||||
|
const token = localStorage.getItem('Authorization')
|
||||||
|
|
||||||
|
if (token && token !== '') {
|
||||||
|
// 已登录 → 放行
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 未登录 → 重定向到登录页,保存原目标路径
|
||||||
|
next({
|
||||||
|
path: '/login',
|
||||||
|
query: {
|
||||||
|
redirect: to.fullPath,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
export default router
|
||||||
299
src/stores/app.ts
Normal file
299
src/stores/app.ts
Normal file
@@ -0,0 +1,299 @@
|
|||||||
|
/**
|
||||||
|
* 应用全局状态管理
|
||||||
|
*
|
||||||
|
* 基于 Pinia 的应用级 Store,管理全局配置、侧边栏、
|
||||||
|
* 暗黑模式、缓存页面等应用维度的状态。
|
||||||
|
*
|
||||||
|
* @module stores/app
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { ref, computed, watch } from 'vue'
|
||||||
|
import type { GlobalConfig } from '@/config/types'
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
// 常量
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
|
||||||
|
/** 暗黑模式 localStorage 键名 */
|
||||||
|
const DARK_MODE_KEY = 'app-dark-mode'
|
||||||
|
|
||||||
|
/** 侧边栏状态 localStorage 键名 */
|
||||||
|
const SIDEBAR_KEY = 'app-sidebar-collapsed'
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
// 内部工具函数
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
|
||||||
|
/** 从 localStorage 读取布尔值 */
|
||||||
|
function readBoolFromStorage(key: string, fallback: boolean): boolean {
|
||||||
|
try {
|
||||||
|
const stored = localStorage.getItem(key)
|
||||||
|
if (stored === null) return fallback
|
||||||
|
return stored === 'true'
|
||||||
|
} catch {
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 将布尔值写入 localStorage */
|
||||||
|
function writeBoolToStorage(key: string, value: boolean): void {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(key, String(value))
|
||||||
|
} catch {
|
||||||
|
// localStorage 不可用时静默失败
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
// 类型定义
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
|
||||||
|
/** 侧边栏状态 */
|
||||||
|
export interface SidebarState {
|
||||||
|
/** 是否折叠 */
|
||||||
|
collapsed: boolean
|
||||||
|
/** 是否在移动端展开(不考虑折叠状态) */
|
||||||
|
opened: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 应用尺寸类型 */
|
||||||
|
export type DeviceSize = 'mobile' | 'tablet' | 'desktop'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用全局 Store
|
||||||
|
*
|
||||||
|
* 管理应用的全局状态,包括:
|
||||||
|
* - globalConfig:运行时全局配置
|
||||||
|
* - sidebar:侧边栏折叠/展开状态(持久化)
|
||||||
|
* - darkMode:暗黑模式开关(持久化)
|
||||||
|
* - cachedViews:需要缓存的页面视图名称列表
|
||||||
|
* - device:当前设备类型
|
||||||
|
*/
|
||||||
|
export const useAppStore = defineStore('app', () => {
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
// State
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 运行时全局配置
|
||||||
|
*
|
||||||
|
* 应用启动时从 window.globalConfig 读取初始值,
|
||||||
|
* 后续可通过 loadGlobalConfig 从远程接口更新。
|
||||||
|
*/
|
||||||
|
const globalConfig = ref<GlobalConfig | null>(
|
||||||
|
typeof window !== 'undefined' ? window.globalConfig ?? null : null,
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 侧边栏状态
|
||||||
|
*
|
||||||
|
* collapsed 会持久化到 localStorage,刷新后保持。
|
||||||
|
*/
|
||||||
|
const sidebar = ref<SidebarState>({
|
||||||
|
collapsed: readBoolFromStorage(SIDEBAR_KEY, false),
|
||||||
|
opened: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 暗黑模式
|
||||||
|
*
|
||||||
|
* 切换时自动在 <html> 元素上添加/移除 'dark' 类名,
|
||||||
|
* 并持久化到 localStorage。
|
||||||
|
*/
|
||||||
|
const darkMode = ref<boolean>(readBoolFromStorage(DARK_MODE_KEY, false))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 缓存的视图组件名称列表
|
||||||
|
*
|
||||||
|
* 用于 <keep-alive> 的 include 属性,
|
||||||
|
* 存储需要保持状态不销毁的路由组件 name。
|
||||||
|
*/
|
||||||
|
const cachedViews = ref<string[]>([])
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前设备类型
|
||||||
|
*
|
||||||
|
* 根据窗口宽度自动判断。
|
||||||
|
*/
|
||||||
|
const device = ref<DeviceSize>(getDeviceSize())
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
// 暗黑模式初始化
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
|
||||||
|
// 监听 darkMode 变化,同步到 DOM 和 localStorage
|
||||||
|
watch(darkMode, (val: boolean) => {
|
||||||
|
writeBoolToStorage(DARK_MODE_KEY, val)
|
||||||
|
if (typeof document !== 'undefined') {
|
||||||
|
document.documentElement.classList.toggle('dark', val)
|
||||||
|
}
|
||||||
|
}, { immediate: true })
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
// Getters
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前是否为移动端设备
|
||||||
|
* @returns 设备宽度 < 768px 返回 true
|
||||||
|
*/
|
||||||
|
const isMobile = computed<boolean>(() => device.value === 'mobile')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前是否为桌面端设备
|
||||||
|
* @returns 设备宽度 >= 992px 返回 true
|
||||||
|
*/
|
||||||
|
const isDesktop = computed<boolean>(() => device.value === 'desktop')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前是否为平板设备
|
||||||
|
* @returns 设备宽度在 768px ~ 991px 返回 true
|
||||||
|
*/
|
||||||
|
const isTablet = computed<boolean>(() => device.value === 'tablet')
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
// Actions
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置全局配置
|
||||||
|
*
|
||||||
|
* 可用于应用启动时从远程接口加载配置后合并更新。
|
||||||
|
*
|
||||||
|
* @param config - 完整的全局配置对象或部分配置
|
||||||
|
*/
|
||||||
|
function setGlobalConfig(config: Partial<GlobalConfig>): void {
|
||||||
|
if (globalConfig.value) {
|
||||||
|
Object.assign(globalConfig.value, config)
|
||||||
|
} else {
|
||||||
|
globalConfig.value = config as GlobalConfig
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切换侧边栏折叠状态
|
||||||
|
*
|
||||||
|
* 桌面端切换 collapsed 状态,移动端切换 opened 状态。
|
||||||
|
*/
|
||||||
|
function toggleSidebar(): void {
|
||||||
|
if (isMobile.value) {
|
||||||
|
sidebar.value.opened = !sidebar.value.opened
|
||||||
|
} else {
|
||||||
|
sidebar.value.collapsed = !sidebar.value.collapsed
|
||||||
|
writeBoolToStorage(SIDEBAR_KEY, sidebar.value.collapsed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关闭侧边栏
|
||||||
|
*
|
||||||
|
* 通常用于移动端点击遮罩层或路由跳转后自动关闭。
|
||||||
|
*/
|
||||||
|
function closeSidebar(): void {
|
||||||
|
sidebar.value.opened = false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切换暗黑模式
|
||||||
|
*
|
||||||
|
* 反转当前的暗黑模式状态,变化会自动同步到
|
||||||
|
* localStorage 和 DOM。
|
||||||
|
*/
|
||||||
|
function toggleDarkMode(): void {
|
||||||
|
darkMode.value = !darkMode.value
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置暗黑模式
|
||||||
|
*
|
||||||
|
* @param value - true 开启暗黑模式,false 关闭
|
||||||
|
*/
|
||||||
|
function setDarkMode(value: boolean): void {
|
||||||
|
darkMode.value = value
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加缓存视图
|
||||||
|
*
|
||||||
|
* 将指定视图名称加入缓存列表,用于 keep-alive。
|
||||||
|
* 重复添加同名视图不会产生副作用。
|
||||||
|
*
|
||||||
|
* @param viewName - 要缓存的视图组件名称
|
||||||
|
*/
|
||||||
|
function addCachedView(viewName: string): void {
|
||||||
|
if (!cachedViews.value.includes(viewName)) {
|
||||||
|
cachedViews.value.push(viewName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除缓存视图
|
||||||
|
*
|
||||||
|
* 将指定视图名称从缓存列表中移除。
|
||||||
|
*
|
||||||
|
* @param viewName - 要移除缓存的视图组件名称
|
||||||
|
*/
|
||||||
|
function removeCachedView(viewName: string): void {
|
||||||
|
const index = cachedViews.value.indexOf(viewName)
|
||||||
|
if (index !== -1) {
|
||||||
|
cachedViews.value.splice(index, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空所有缓存视图
|
||||||
|
*/
|
||||||
|
function clearCachedViews(): void {
|
||||||
|
cachedViews.value = []
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新设备类型
|
||||||
|
*
|
||||||
|
* 根据当前窗口宽度重新计算设备类型。
|
||||||
|
* 通常在窗口 resize 事件中调用。
|
||||||
|
*/
|
||||||
|
function updateDevice(): void {
|
||||||
|
device.value = getDeviceSize()
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
// State
|
||||||
|
globalConfig,
|
||||||
|
sidebar,
|
||||||
|
darkMode,
|
||||||
|
cachedViews,
|
||||||
|
device,
|
||||||
|
// Getters
|
||||||
|
isMobile,
|
||||||
|
isDesktop,
|
||||||
|
isTablet,
|
||||||
|
// Actions
|
||||||
|
setGlobalConfig,
|
||||||
|
toggleSidebar,
|
||||||
|
closeSidebar,
|
||||||
|
toggleDarkMode,
|
||||||
|
setDarkMode,
|
||||||
|
addCachedView,
|
||||||
|
removeCachedView,
|
||||||
|
clearCachedViews,
|
||||||
|
updateDevice,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
// 辅助函数
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据窗口宽度判断当前设备类型
|
||||||
|
* @returns 设备类型标识
|
||||||
|
*/
|
||||||
|
function getDeviceSize(): DeviceSize {
|
||||||
|
if (typeof window === 'undefined') return 'desktop'
|
||||||
|
const width = window.innerWidth
|
||||||
|
if (width < 768) return 'mobile'
|
||||||
|
if (width < 992) return 'tablet'
|
||||||
|
return 'desktop'
|
||||||
|
}
|
||||||
98
src/stores/token.ts
Normal file
98
src/stores/token.ts
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
/**
|
||||||
|
* Token 状态管理
|
||||||
|
*
|
||||||
|
* 基于 Pinia 的 Token 持久化 Store,
|
||||||
|
* 封装 localStorage 中 Token 的读取、写入、清除操作。
|
||||||
|
* 配合 axios 请求拦截器实现自动附加 Token。
|
||||||
|
*
|
||||||
|
* @module stores/token
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
|
||||||
|
/** localStorage 中存储 Token 的键名 */
|
||||||
|
const TOKEN_KEY = 'Authorization'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Token Store
|
||||||
|
*
|
||||||
|
* 管理登录凭证 Token 的持久化存储。
|
||||||
|
* - token:当前 Token 值(响应式)
|
||||||
|
* - hasToken:是否已存在 Token(计算属性)
|
||||||
|
* - setToken:存储 Token 到 localStorage 并同步到 Store
|
||||||
|
* - getToken:从 localStorage 读取 Token 并同步到 Store
|
||||||
|
* - clearToken:清除 localStorage 和 Store 中的 Token
|
||||||
|
*/
|
||||||
|
export const useTokenStore = defineStore('token', () => {
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
// State
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
|
||||||
|
/** 当前存储的 Token 值 */
|
||||||
|
const token = ref<string | null>(localStorage.getItem(TOKEN_KEY))
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
// Getters
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否已存在有效的 Token
|
||||||
|
* @returns 如果 Token 存在且非空返回 true
|
||||||
|
*/
|
||||||
|
const hasToken = computed<boolean>(() => {
|
||||||
|
return token.value !== null && token.value !== ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
// Actions
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置 Token
|
||||||
|
*
|
||||||
|
* 将 Token 写入 localStorage 并同步更新 Store 中的响应式状态。
|
||||||
|
*
|
||||||
|
* @param value - Token 字符串
|
||||||
|
*/
|
||||||
|
function setToken(value: string): void {
|
||||||
|
token.value = value
|
||||||
|
localStorage.setItem(TOKEN_KEY, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 Token
|
||||||
|
*
|
||||||
|
* 从 localStorage 读取 Token 并同步到 Store 状态。
|
||||||
|
* 通常在应用初始化时调用。
|
||||||
|
*
|
||||||
|
* @returns 当前的 Token 值,不存在时返回 null
|
||||||
|
*/
|
||||||
|
function getToken(): string | null {
|
||||||
|
const stored = localStorage.getItem(TOKEN_KEY)
|
||||||
|
token.value = stored
|
||||||
|
return stored
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除 Token
|
||||||
|
*
|
||||||
|
* 移除 localStorage 中存储的 Token,并将 Store 状态置为 null。
|
||||||
|
* 通常在退出登录或 Token 过期时调用。
|
||||||
|
*/
|
||||||
|
function clearToken(): void {
|
||||||
|
token.value = null
|
||||||
|
localStorage.removeItem(TOKEN_KEY)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
// State
|
||||||
|
token,
|
||||||
|
// Getters
|
||||||
|
hasToken,
|
||||||
|
// Actions
|
||||||
|
setToken,
|
||||||
|
getToken,
|
||||||
|
clearToken,
|
||||||
|
}
|
||||||
|
})
|
||||||
193
src/stores/user.ts
Normal file
193
src/stores/user.ts
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
/**
|
||||||
|
* 用户状态管理
|
||||||
|
*
|
||||||
|
* 基于 Pinia 的用户信息 Store,管理当前登录用户的
|
||||||
|
* 个人信息、登录/退出逻辑以及权限相关数据。
|
||||||
|
*
|
||||||
|
* @module stores/user
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { useTokenStore } from './token'
|
||||||
|
import {
|
||||||
|
login as loginApi,
|
||||||
|
logout as logoutApi,
|
||||||
|
getUserInfo as getUserInfoApi,
|
||||||
|
type LoginParams,
|
||||||
|
type UserInfo,
|
||||||
|
} from '@/api'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户 Store
|
||||||
|
*
|
||||||
|
* 管理当前登录用户的完整生命周期:
|
||||||
|
* - userInfo:用户基本信息(响应式)
|
||||||
|
* - roles:角色列表
|
||||||
|
* - permissions:权限标识列表
|
||||||
|
* - hasUserInfo:是否已加载用户信息(计算属性)
|
||||||
|
* - login:调用登录接口并存储 Token
|
||||||
|
* - logout:调用退出接口并清除状态
|
||||||
|
* - getUserInfo:获取当前用户详细信息
|
||||||
|
*/
|
||||||
|
export const useUserStore = defineStore('user', () => {
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
// State
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
|
||||||
|
/** 当前登录用户信息 */
|
||||||
|
const userInfo = ref<UserInfo | null>(null)
|
||||||
|
|
||||||
|
/** 用户角色列表 */
|
||||||
|
const roles = ref<string[]>([])
|
||||||
|
|
||||||
|
/** 用户权限标识列表 */
|
||||||
|
const permissions = ref<string[]>([])
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
// Getters
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否已加载用户信息
|
||||||
|
* @returns 用户信息对象存在则返回 true
|
||||||
|
*/
|
||||||
|
const hasUserInfo = computed<boolean>(() => {
|
||||||
|
return userInfo.value !== null
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前用户名
|
||||||
|
* @returns 用户名称,未登录时返回空字符串
|
||||||
|
*/
|
||||||
|
const userName = computed<string>(() => {
|
||||||
|
return userInfo.value?.userName ?? ''
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前用户昵称
|
||||||
|
* @returns 用户昵称,未登录时返回空字符串
|
||||||
|
*/
|
||||||
|
const nickName = computed<string>(() => {
|
||||||
|
return userInfo.value?.nickName ?? ''
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前用户头像
|
||||||
|
* @returns 头像地址,未登录时返回空字符串
|
||||||
|
*/
|
||||||
|
const avatar = computed<string>(() => {
|
||||||
|
return userInfo.value?.avatar ?? ''
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前用户 ID
|
||||||
|
* @returns 用户 ID,未登录时返回空字符串
|
||||||
|
*/
|
||||||
|
const userId = computed<string | number>(() => {
|
||||||
|
return userInfo.value?.userId ?? ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
// Actions
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户登录
|
||||||
|
*
|
||||||
|
* 调用登录接口获取 Token,并写入 Token Store。
|
||||||
|
* 登录成功后自动调用 getUserInfo 获取用户详细信息。
|
||||||
|
*
|
||||||
|
* @param params - 登录参数(用户名、密码、验证码等)
|
||||||
|
* @returns 登录成功后返回的 Token
|
||||||
|
*/
|
||||||
|
async function login(params: LoginParams): Promise<string> {
|
||||||
|
const tokenStore = useTokenStore()
|
||||||
|
const res = await loginApi(params)
|
||||||
|
tokenStore.setToken(res.token)
|
||||||
|
await getUserInfo()
|
||||||
|
return res.token
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前登录用户信息
|
||||||
|
*
|
||||||
|
* 调用后端接口获取用户详细信息,同时更新
|
||||||
|
* roles 和 permissions 列表。
|
||||||
|
*/
|
||||||
|
async function getUserInfo(): Promise<void> {
|
||||||
|
const info = await getUserInfoApi()
|
||||||
|
userInfo.value = info
|
||||||
|
roles.value = info.roles ?? []
|
||||||
|
permissions.value = info.permissions ?? []
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 退出登录
|
||||||
|
*
|
||||||
|
* 调用后端退出接口,清除 Token Store 中的 Token,
|
||||||
|
* 并将当前 Store 状态重置为初始值。
|
||||||
|
*/
|
||||||
|
async function logout(): Promise<void> {
|
||||||
|
const tokenStore = useTokenStore()
|
||||||
|
try {
|
||||||
|
await logoutApi()
|
||||||
|
} finally {
|
||||||
|
// 无论接口调用成功与否,前端都必须清除登录状态
|
||||||
|
tokenStore.clearToken()
|
||||||
|
$reset()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断当前用户是否拥有指定权限
|
||||||
|
*
|
||||||
|
* @param permission - 权限标识字符串
|
||||||
|
* @returns 拥有该权限返回 true,否则返回 false
|
||||||
|
*/
|
||||||
|
function hasPermission(permission: string): boolean {
|
||||||
|
return permissions.value.includes(permission)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断当前用户是否拥有指定角色
|
||||||
|
*
|
||||||
|
* @param role - 角色标识字符串
|
||||||
|
* @returns 拥有该角色返回 true,否则返回 false
|
||||||
|
*/
|
||||||
|
function hasRole(role: string): boolean {
|
||||||
|
return roles.value.includes(role)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重置 Store 状态
|
||||||
|
*
|
||||||
|
* 将 userInfo、roles、permissions 全部恢复为初始值。
|
||||||
|
* 通过 Pinia 的 $reset 方法可调用此逻辑。
|
||||||
|
*/
|
||||||
|
function $reset(): void {
|
||||||
|
userInfo.value = null
|
||||||
|
roles.value = []
|
||||||
|
permissions.value = []
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
// State
|
||||||
|
userInfo,
|
||||||
|
roles,
|
||||||
|
permissions,
|
||||||
|
// Getters
|
||||||
|
hasUserInfo,
|
||||||
|
userName,
|
||||||
|
nickName,
|
||||||
|
avatar,
|
||||||
|
userId,
|
||||||
|
// Actions
|
||||||
|
login,
|
||||||
|
getUserInfo,
|
||||||
|
logout,
|
||||||
|
hasPermission,
|
||||||
|
hasRole,
|
||||||
|
$reset,
|
||||||
|
}
|
||||||
|
})
|
||||||
12
src/types/global.d.ts
vendored
Normal file
12
src/types/global.d.ts
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
/**
|
||||||
|
* 全局类型声明
|
||||||
|
*
|
||||||
|
* 扩展 Window 接口,声明运行时注入的全局配置对象。
|
||||||
|
* globalConfig 的完整类型定义见 @/config/types。
|
||||||
|
*
|
||||||
|
* @module types/global
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Window.globalConfig 的完整类型声明已迁移至 src/config/types.ts,
|
||||||
|
// 本文件保留作为全局类型声明的入口。
|
||||||
|
export {}
|
||||||
237
src/utils/http/index.ts
Normal file
237
src/utils/http/index.ts
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
/**
|
||||||
|
* HTTP 客户端封装
|
||||||
|
*
|
||||||
|
* 基于 Axios 封装的 HTTP 请求模块,提供统一的请求/响应拦截、
|
||||||
|
* Token 管理以及类型安全的请求方法。
|
||||||
|
*
|
||||||
|
* - baseURL 从 window.globalConfig.api.baseURL 读取,回退到 '/dev-api'
|
||||||
|
* - 请求拦截器自动附加 Authorization Token
|
||||||
|
* - 响应拦截器自动解包数据并统一错误提示
|
||||||
|
*
|
||||||
|
* @module utils/http
|
||||||
|
*/
|
||||||
|
|
||||||
|
import axios, {
|
||||||
|
type AxiosInstance,
|
||||||
|
type AxiosResponse,
|
||||||
|
type AxiosRequestConfig,
|
||||||
|
type InternalAxiosRequestConfig,
|
||||||
|
} from 'axios'
|
||||||
|
import { showFailToast } from 'vant'
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
// 类型定义
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
|
||||||
|
/** 后端统一响应结构 */
|
||||||
|
export interface ApiResponse<T = unknown> {
|
||||||
|
/** 状态码,200 表示成功 */
|
||||||
|
code: number
|
||||||
|
/** 响应数据 */
|
||||||
|
data: T
|
||||||
|
/** 响应消息 */
|
||||||
|
msg: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
// Token 管理
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
|
||||||
|
/** localStorage 中存储 Token 的键名 */
|
||||||
|
const TOKEN_KEY = 'Authorization'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取本地存储的 Token
|
||||||
|
* @returns Token 字符串,不存在时返回 null
|
||||||
|
*/
|
||||||
|
export function getToken(): string | null {
|
||||||
|
return localStorage.getItem(TOKEN_KEY)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置 Token 到本地存储
|
||||||
|
* @param token - Token 字符串
|
||||||
|
*/
|
||||||
|
export function setToken(token: string): void {
|
||||||
|
localStorage.setItem(TOKEN_KEY, token)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除本地存储的 Token
|
||||||
|
*/
|
||||||
|
export function removeToken(): void {
|
||||||
|
localStorage.removeItem(TOKEN_KEY)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
// Axios 实例
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Axios 实例
|
||||||
|
*
|
||||||
|
* - baseURL:优先读取 window.globalConfig.api.baseURL,不存在时使用 '/dev-api'
|
||||||
|
* - timeout:30 秒超时
|
||||||
|
*/
|
||||||
|
const http: AxiosInstance = axios.create({
|
||||||
|
baseURL:
|
||||||
|
(typeof window !== 'undefined' && window.globalConfig?.api?.baseURL) ||
|
||||||
|
'/dev-api',
|
||||||
|
timeout: 30_000,
|
||||||
|
})
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
// 请求拦截器
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 请求拦截器
|
||||||
|
*
|
||||||
|
* 在每次请求发出前,自动从 localStorage 读取 Token
|
||||||
|
* 并附加到请求头的 Authorization 字段中。
|
||||||
|
*/
|
||||||
|
http.interceptors.request.use(
|
||||||
|
(config: InternalAxiosRequestConfig) => {
|
||||||
|
const token = getToken()
|
||||||
|
if (token && config.headers) {
|
||||||
|
config.headers.Authorization = `Bearer ${token}`
|
||||||
|
}
|
||||||
|
return config
|
||||||
|
},
|
||||||
|
(error: unknown) => {
|
||||||
|
return Promise.reject(error)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
// 响应拦截器
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 响应拦截器
|
||||||
|
*
|
||||||
|
* - 成功(code === 200):解包返回 response.data.data
|
||||||
|
* - 业务异常:弹出错误提示并 reject
|
||||||
|
* - 网络异常:弹出错误提示并 reject
|
||||||
|
*/
|
||||||
|
http.interceptors.response.use(
|
||||||
|
(response): AxiosResponse | Promise<AxiosResponse> => {
|
||||||
|
const res = response.data as ApiResponse
|
||||||
|
|
||||||
|
// 文件下载等特殊响应(非 JSON)直接返回
|
||||||
|
if (response.config.responseType === 'blob') {
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res.code === 200) {
|
||||||
|
// 解包:将 response.data 替换为业务数据
|
||||||
|
response.data = res.data
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
// 业务错误码处理
|
||||||
|
const errMsg = res.msg || '请求失败'
|
||||||
|
showFailToast(errMsg)
|
||||||
|
|
||||||
|
// Token 过期等特殊错误码可在此扩展处理
|
||||||
|
// if (res.code === 401) { removeToken(); /* 跳转登录 */ }
|
||||||
|
|
||||||
|
return Promise.reject(new Error(errMsg))
|
||||||
|
},
|
||||||
|
(error: unknown) => {
|
||||||
|
let msg = '网络错误,请稍后重试'
|
||||||
|
|
||||||
|
if (axios.isAxiosError(error)) {
|
||||||
|
const resData = error.response?.data as ApiResponse | undefined
|
||||||
|
if (resData?.msg) {
|
||||||
|
msg = resData.msg
|
||||||
|
} else if (error.message) {
|
||||||
|
msg = error.message
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTP 状态码特殊处理
|
||||||
|
const status = error.response?.status
|
||||||
|
if (status === 401) {
|
||||||
|
msg = '登录已过期,请重新登录'
|
||||||
|
removeToken()
|
||||||
|
// TODO: 跳转登录页
|
||||||
|
} else if (status === 403) {
|
||||||
|
msg = '没有操作权限'
|
||||||
|
} else if (status === 404) {
|
||||||
|
msg = '请求的资源不存在'
|
||||||
|
} else if (status === 500) {
|
||||||
|
msg = '服务器内部错误'
|
||||||
|
}
|
||||||
|
} else if (error instanceof Error) {
|
||||||
|
msg = error.message
|
||||||
|
}
|
||||||
|
|
||||||
|
showFailToast(msg)
|
||||||
|
return Promise.reject(error)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
// 类型安全的请求方法
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET 请求
|
||||||
|
* @param url - 请求地址
|
||||||
|
* @param params - 查询参数
|
||||||
|
* @param config - Axios 请求配置
|
||||||
|
* @returns Promise<T>
|
||||||
|
*/
|
||||||
|
export async function get<T>(
|
||||||
|
url: string,
|
||||||
|
params?: Record<string, unknown>,
|
||||||
|
config?: AxiosRequestConfig,
|
||||||
|
): Promise<T> {
|
||||||
|
return http.get(url, { params, ...config }) as Promise<T>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST 请求
|
||||||
|
* @param url - 请求地址
|
||||||
|
* @param data - 请求体数据
|
||||||
|
* @param config - Axios 请求配置
|
||||||
|
* @returns Promise<T>
|
||||||
|
*/
|
||||||
|
export async function post<T>(
|
||||||
|
url: string,
|
||||||
|
data?: unknown,
|
||||||
|
config?: AxiosRequestConfig,
|
||||||
|
): Promise<T> {
|
||||||
|
return http.post(url, data, config) as Promise<T>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PUT 请求
|
||||||
|
* @param url - 请求地址
|
||||||
|
* @param data - 请求体数据
|
||||||
|
* @param config - Axios 请求配置
|
||||||
|
* @returns Promise<T>
|
||||||
|
*/
|
||||||
|
export async function put<T>(
|
||||||
|
url: string,
|
||||||
|
data?: unknown,
|
||||||
|
config?: AxiosRequestConfig,
|
||||||
|
): Promise<T> {
|
||||||
|
return http.put(url, data, config) as Promise<T>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DELETE 请求
|
||||||
|
* @param url - 请求地址
|
||||||
|
* @param config - Axios 请求配置
|
||||||
|
* @returns Promise<T>
|
||||||
|
*/
|
||||||
|
export async function del<T>(
|
||||||
|
url: string,
|
||||||
|
config?: AxiosRequestConfig,
|
||||||
|
): Promise<T> {
|
||||||
|
return http.delete(url, config) as Promise<T>
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 导出原始的 Axios 实例,供特殊场景(如文件上传进度监听)使用 */
|
||||||
|
export default http
|
||||||
125
src/views/home/index.vue
Normal file
125
src/views/home/index.vue
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* 首页 (placeholder)
|
||||||
|
*
|
||||||
|
* 使用 Vant Tabbar 实现底部导航切换,
|
||||||
|
* 当前仅展示占位内容,后续集成地图、管网等功能模块。
|
||||||
|
*/
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
/** 当前激活的 Tab */
|
||||||
|
const active = ref(0)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tab 切换处理
|
||||||
|
*/
|
||||||
|
function onTabChange(index: number): void {
|
||||||
|
active.value = index
|
||||||
|
if (index === 0) {
|
||||||
|
router.replace('/home')
|
||||||
|
} else if (index === 1) {
|
||||||
|
router.replace('/mine')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="home-page">
|
||||||
|
<!-- 顶部导航栏 -->
|
||||||
|
<van-nav-bar title="舆图智慧水务" fixed placeholder />
|
||||||
|
|
||||||
|
<!-- 页面主体区域 -->
|
||||||
|
<div class="home-content">
|
||||||
|
<div class="welcome-card">
|
||||||
|
<h2>欢迎使用智慧水务</h2>
|
||||||
|
<p>移动端管理平台</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="feature-grid">
|
||||||
|
<div class="feature-item" v-for="i in 4" :key="i">
|
||||||
|
<div class="feature-icon"></div>
|
||||||
|
<span class="feature-label">功能模块 {{ i }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 底部导航栏 -->
|
||||||
|
<van-tabbar v-model="active" :fixed="true" :placeholder="true" @change="onTabChange">
|
||||||
|
<van-tabbar-item icon="home-o" name="首页">首页</van-tabbar-item>
|
||||||
|
<van-tabbar-item icon="user-o" name="我的">我的</van-tabbar-item>
|
||||||
|
</van-tabbar>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.home-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: var(--color-bg-page);
|
||||||
|
|
||||||
|
:deep(.van-nav-bar) {
|
||||||
|
background: var(--color-primary);
|
||||||
|
--van-nav-bar-title-text-color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-content {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-card {
|
||||||
|
background: var(--color-bg-card);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 32px 24px;
|
||||||
|
text-align: center;
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
margin-bottom: 16px;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 20px;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-item {
|
||||||
|
background: var(--color-bg-card);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 24px 16px;
|
||||||
|
text-align: center;
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color var(--transition-fast);
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background: var(--color-border-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-icon {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: var(--color-primary-bg);
|
||||||
|
margin: 0 auto 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--color-text-regular);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
153
src/views/login/index.vue
Normal file
153
src/views/login/index.vue
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* 登录页面 (placeholder)
|
||||||
|
*
|
||||||
|
* 包含用户名、密码输入表单,调用 userStore.login 完成登录。
|
||||||
|
* 登录成功后跳转至首页(或 redirect 参数指定的页面)。
|
||||||
|
*/
|
||||||
|
import { ref, reactive } from 'vue'
|
||||||
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
|
import { showSuccessToast, showFailToast } from 'vant'
|
||||||
|
import { useUserStore } from '@/stores/user'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
|
const userStore = useUserStore()
|
||||||
|
|
||||||
|
/** 表单加载状态 */
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
/** 登录表单数据 */
|
||||||
|
const loginForm = reactive({
|
||||||
|
username: '',
|
||||||
|
password: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
/** 表单校验规则 */
|
||||||
|
const rules = {
|
||||||
|
username: [{ required: true, message: '请输入用户名' }],
|
||||||
|
password: [{ required: true, message: '请输入密码' }],
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理登录提交
|
||||||
|
*/
|
||||||
|
async function handleSubmit(): Promise<void> {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
await userStore.login({
|
||||||
|
username: loginForm.username,
|
||||||
|
password: loginForm.password,
|
||||||
|
})
|
||||||
|
showSuccessToast('登录成功')
|
||||||
|
// 跳转到 redirect 指定的页面或首页
|
||||||
|
const redirect = (route.query.redirect as string) || '/home'
|
||||||
|
router.replace(redirect)
|
||||||
|
} catch (err) {
|
||||||
|
const msg = err instanceof Error ? err.message : '登录失败,请重试'
|
||||||
|
showFailToast(msg)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="login-page">
|
||||||
|
<!-- 头部 Logo 区域 -->
|
||||||
|
<div class="login-header">
|
||||||
|
<h1 class="login-title">舆图智慧水务</h1>
|
||||||
|
<p class="login-subtitle">移动端管理平台</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 登录表单 -->
|
||||||
|
<div class="login-form-wrapper">
|
||||||
|
<van-form @submit="handleSubmit">
|
||||||
|
<van-field
|
||||||
|
v-model="loginForm.username"
|
||||||
|
name="username"
|
||||||
|
label="用户名"
|
||||||
|
placeholder="请输入用户名"
|
||||||
|
:rules="rules.username"
|
||||||
|
clearable
|
||||||
|
left-icon="user-o"
|
||||||
|
/>
|
||||||
|
<van-field
|
||||||
|
v-model="loginForm.password"
|
||||||
|
name="password"
|
||||||
|
label="密码"
|
||||||
|
placeholder="请输入密码"
|
||||||
|
type="password"
|
||||||
|
:rules="rules.password"
|
||||||
|
left-icon="lock"
|
||||||
|
/>
|
||||||
|
<div class="login-button-wrapper">
|
||||||
|
<van-button
|
||||||
|
round
|
||||||
|
block
|
||||||
|
type="primary"
|
||||||
|
native-type="submit"
|
||||||
|
:loading="loading"
|
||||||
|
loading-text="登录中..."
|
||||||
|
>
|
||||||
|
登 录
|
||||||
|
</van-button>
|
||||||
|
</div>
|
||||||
|
</van-form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 底部版权 -->
|
||||||
|
<div class="login-footer">
|
||||||
|
<span>v2.0.0</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.login-page {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
background: var(--color-bg-page);
|
||||||
|
padding: 0 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-title {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-primary);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-subtitle {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
background: var(--color-bg-card);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 24px 16px;
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-button-wrapper {
|
||||||
|
margin-top: 24px;
|
||||||
|
padding: 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-footer {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 32px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--color-text-placeholder);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
77
src/views/mine/index.vue
Normal file
77
src/views/mine/index.vue
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* 个人中心页面 (placeholder)
|
||||||
|
*
|
||||||
|
* 展示用户基本信息,提供退出登录等功能入口。
|
||||||
|
*/
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="mine-page">
|
||||||
|
<van-nav-bar title="我的" fixed placeholder />
|
||||||
|
|
||||||
|
<div class="mine-content">
|
||||||
|
<div class="user-card">
|
||||||
|
<div class="avatar-placeholder"></div>
|
||||||
|
<div class="user-info">
|
||||||
|
<span class="user-name">未登录</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<van-cell-group inset>
|
||||||
|
<van-cell title="个人信息" is-link />
|
||||||
|
<van-cell title="系统设置" is-link />
|
||||||
|
<van-cell title="关于我们" is-link />
|
||||||
|
</van-cell-group>
|
||||||
|
|
||||||
|
<div class="logout-wrapper">
|
||||||
|
<van-button round block type="danger" @click="router.replace('/login')">
|
||||||
|
退出登录
|
||||||
|
</van-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.mine-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: var(--color-bg-page);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mine-content {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
background: var(--color-bg-card);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 24px 16px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-placeholder {
|
||||||
|
width: 56px;
|
||||||
|
height: 56px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--color-primary-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-name {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logout-wrapper {
|
||||||
|
margin-top: 24px;
|
||||||
|
padding: 0 16px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user