Compare commits

...
23 Commits
Author SHA1 Message Date
xrain 1597c98f39 支持了localstorage存储token
Publish to npm / publish (push) Failing after 8s
2026-08-10 22:58:28 +08:00
xrain 7dbb48ad90 fix config
Publish to npm / publish (push) Failing after 10s
2026-08-10 21:15:53 +08:00
xrain 88af4a2d96 fix config
Publish to npm / publish (push) Failing after 10s
2026-08-10 21:03:44 +08:00
xrain 9e44c6fcaf fix config
Publish to npm / publish (push) Failing after 10s
2026-08-10 21:01:21 +08:00
xrain 592607f1e2 fix config
Publish to npm / publish (push) Failing after 8s
2026-08-10 20:46:40 +08:00
xrain 7aee4b708d fix config
Publish to npm / publish (push) Failing after 8s
2026-08-10 20:44:33 +08:00
xrain 1af5900593 fix config
Publish to npm / publish (push) Failing after 8s
2026-08-10 20:37:23 +08:00
xrain 78ef6d9ca0 fix cookie
Publish to npm / publish (push) Failing after 7s
2026-05-05 07:16:29 +08:00
xrain cee0bc4cfe fix cookie
Publish to npm / publish (push) Failing after 7s
2026-05-05 07:06:34 +08:00
xrain 9a8510d833 fix cookie
Publish to npm / publish (push) Failing after 5s
2026-05-05 07:01:21 +08:00
xrain ee034a7252 cookie过期时间设置
Publish to npm / publish (push) Failing after 19s
2026-05-05 06:51:07 +08:00
xrain 65bb076384 checklogin现在返回用户信息
Publish to npm / publish (push) Failing after 19s
2026-05-04 23:28:35 +08:00
xrain 3ece81fc06 使用JSON文件初始化
Publish to npm / publish (push) Failing after 11s
2026-05-04 19:56:48 +08:00
xrain fa4032f239 token存储转移到cookie
Publish to npm / publish (push) Failing after 9s
2026-05-04 16:57:08 +08:00
xrain 713b8ae31e 取消了一些乱七八糟不同步的方法
Publish to npm / publish (push) Failing after 10s
2026-05-04 03:32:05 +08:00
xrain e0754ad635 query 增加了自处理异常
Publish to npm / publish (push) Failing after 8s
2026-05-04 02:27:43 +08:00
xrain 39626de073 修复了退出登录 TOKEN没清空的问题
Publish to npm / publish (push) Failing after 14s
2026-05-04 00:15:23 +08:00
xrain 13c836b4ab 修改退出登录没带TOKEn
Publish to npm / publish (push) Failing after 14s
2026-05-03 23:11:55 +08:00
xrain 88be4210aa 修正文档 2026-04-26 10:44:40 +08:00
xrain 4257f72799 fix ci
Publish to npm / publish (push) Failing after 8s
2026-04-23 21:39:57 +08:00
xrain 46257ecf4a fix version 2026-04-23 21:39:38 +08:00
xrain a2f57bc419 增加了AppVersion
Publish to npm / publish (push) Failing after 12s
2026-04-23 20:16:38 +08:00
xrain fe77c0ee1a 修正了业务错误报错
Publish to npm / publish (push) Failing after 12s
2026-04-22 22:08:35 +08:00
10 changed files with 922 additions and 867 deletions
-2
View File
@@ -31,8 +31,6 @@ jobs:
run: npm ci run: npm ci
- name: Build - name: Build
env:
SimApiVersion: ${{ github.ref_name }}
run: npm run build run: npm run build
- name: Publish to npm - name: Publish to npm
-166
View File
@@ -1,166 +0,0 @@
# @simcu/simapi — AI 开发指南
> 面向 AI Agent 的代码结构说明,帮助理解、修改和扩展本库。
---
## 项目结构
```
simapi-vue/
├── src/
│ ├── types.ts # 类型定义,全部导出
│ ├── simapi.core.ts # 核心类 SimApiCore,零框架依赖
│ └── simapi.pinia.ts # Pinia StoreVue3 适配层
├── dist/ # 构建产物
├── package.json # exports: "." → core, "/pinia" → vue
├── vite.config.ts # 统一构建配置,生成 .mjs 和 .cjs
└── tsconfig.build.json # tsc 生成类型声明
```
---
## 核心类型(types.ts
```typescript
SimApiBaseResponse<T> // 标准响应 { code, message, data? }
SimApiOptions // configure() 入参 { debug?, auth?, api? }
SimApiAuthConfig // auth: { token_name, check_url, logout_url, login_url }
SimApiApiConfig // api: { endpoints, defaultEndpoint, businessCallback, responseCallback, timeout? }
SimApiBusinessCallback // { [code]: (data) => void },支持数字码或 'common'
```
---
## SimApiCoresimapi.core.ts
**职责**:纯 TS HTTP 客户端,基于原生 fetch API,不依赖任何框架。
**关键设计**
- **零依赖**:使用原生 fetch,无 axios 或其他 HTTP 库
- **无 Cookie**:所有请求使用 `credentials: 'omit'`,避免 CORS 问题
- **Token 传递**:通过请求头 `Token` 传递认证信息
- **超时控制**:通过 `fetchWithTimeout` 辅助函数实现
- `query()` 抛出异常的只有网络/HTTP 错误,业务错误码通过回调处理
- `isLoggedIn` 是 getter,基于 localStorage 中的 token 判断
- **版本号**`getVersion()` 返回的版本信息中,`uiSimApi` 由构建时注入的 `SimApiVersion` 填充,`uiApp` 由调用方的 `AppVersion` 填充(未注入时为 `0.0.0-develop`
**修改建议**
- 改请求方法(GET/PUT/DELETE):在 `fetchPost()` 内新增 `method` 参数分支,或新增 `fetchGet()`/`fetchPut()` 方法
- 改 Token 存储:替换 `localStorage``sessionStorage` 或内存变量,修改 `getToken()`/`setToken()`/`removeToken()`
- 改登录/登出逻辑:修改 `login()`/`logout()` 方法
- 改超时处理:修改 `fetchWithTimeout()` 函数
- **版本管理**:版本号通过 `declare const` 声明常量,构建时通过 Vite 的 `define` 注入。未指定时默认为 `0.0.0-develop`
**autoInit 设计约束**
- 仅读取 `window.simapi` 的顶级字段:`endpoints``defaultEndpoint``debug`
- 业务回调(`businessCallback`/`responseCallback`)不支持从 window 读取,必须在代码中通过 `setBusinessCallback` 注册
---
## Pinia Storesimapi.pinia.ts
**职责**Vue3 适配层,SimApiCore 的纯代理,不维护任何独立状态。
**关键设计**
- **无独立状态**:state 中只有一个 `_core` 实例,不维护 `debug``token` 等独立数据
- **单例 Core**:在 store state 中实例化 `SimApiCore`,整个应用共享一个实例
- **纯代理映射**:所有 getters 直接映射到 `this._core` 的属性,所有 actions 直接调用 `this._core` 的方法
- **响应式**:通过 Pinia 的响应式系统,当 core 状态变化时自动更新
**使用方式**
```typescript
// 任意组件
import { useSimApi } from '@simcu/simapi'
const api = useSimApi()
// 初始化(二选一)
// 方式一:从 window.simapi 读取
api.autoInit()
// 方式二:直接传入配置
api.configure({
api: { endpoints: { default: 'https://api.example.com' } },
})
// 所有方法与 SimApiCore 完全一致
await api.query('/users/list', { page: 1 })
```
---
## 构建流程
```
npm run build
→ vite build 输出 dist/index.mjs / index.cjs / pinia.mjs / pinia.cjs
→ tsc -p tsconfig.build.json 输出 *.d.ts 类型声明
```
**版本号注入:**
版本号通过 `vite.config.ts``define` 配置注入,从 npm config 读取:
```typescript
define: {
'SimApiVersion': JSON.stringify(process.env.npm_config_SimApiVersion || '0.0.0-develop'),
}
```
**本地构建示例:**
```bash
# 通过环境变量
# Windows PowerShell
$env:npm_config_SimApiVersion='1.0.0'; npm run build
# Linux/Mac
npm_config_SimApiVersion=1.0.0 npm run build
```
**GitHub Actions 自动发布:**
```yaml
env:
SimApiVersion: ${{ github.ref_name }}
run: npm run build
```
**dist 输出是平铺的**core 和 pinia 的编译产物全部在同一目录:
```
dist/
├── index.mjs # core ESM
├── index.cjs # core CJS
├── pinia.mjs # pinia ESM
├── pinia.cjs # pinia CJS
├── simapi.core.d.ts
├── simapi.pinia.d.ts
└── types.d.ts
```
---
## package.json exports
```json
".": { "import": "./dist/index.mjs", "types": "./dist/simapi.core.d.ts" }
"./pinia": { "import": "./dist/pinia.mjs", "types": "./dist/simapi.pinia.d.ts" }
```
---
## 注意事项
- Core 默认 `debug: true`,生产环境需手动 `configure({ debug: false })`
- `query()` 返回 `Promise<SimApiBaseResponse<T>>`code !== 200 时不 reject,通过 `businessCallback` 处理
- `login()` 成功后将 `result.data` 存入 localStorage
- **无 Cookie**:所有请求不发送 CookieToken 通过请求头传递
- **零依赖**:不需要安装 axios,使用原生 fetch
- 删除了 Angular 支持,如需恢复参考 git 历史
- **版本号管理**:使用 `declare const` + Vite `define` 注入
- **AppVersion** 不注入,留给调用方自己管理
+509 -327
View File
@@ -1,398 +1,580 @@
# @simcu/simapi # @simcu/simapi — SimApi Vue 前端库(AI 编码参考)
> 轻量级 HTTP 请求库,基于原生 fetch,支持任意 JS/TS 环境及 Vue3。 > **包名**: `@simcu/simapi` | **技术栈**: TypeScript + Vue3 + Pinia + 原生 fetch
> **后端对应**: [simapi-net](../simapi-net)`Simcu.SimApi` NuGet 包)
> **性质**: simapi-net 的官方前端 HTTP 客户端,**专为其统一响应格式设计**
>
> **使用方式**: 将本文档作为上下文提供给 AI,或粘贴到对话开头。AI 阅读本文档后应能正确编写调用 simapi-net 接口的前端代码。
## 安装 ---
## 0. 一句话定位
simapi-vue 是 **simapi-net 的前端搭档**。后端用 `Simcu.SimApi` 写接口,前端用 `@simcu/simapi` 调接口。两者共享同一套响应格式、认证方式和错误处理约定。
---
## 1. 核心概念(必读)
### 1.1 统一响应格式
simapi-net 所有接口的响应格式固定如下(HTTP 状态码始终 200):
```json
{ "code": 200, "message": "成功", "data": { ... } }
```
| code 含义 |
|-----------|
| 200 成功 | 204 无数据 | 400 参数错误 |
| 401 需要登录 | 403 无权访问 | 404 不存在 | 500 服务器错误 |
### 1.2 Token 认证
- 前端通过请求头 `Token: <value>` 传递认证令牌
- Token 存储在 Cookie 中,key 默认为 `simapi-auth-token`
- Cookie 属性: `path=/; secure; samesite=none`
- **不使用 Authorization: Bearer**
### 1.3 请求方式
- **默认全部 POST**body 为 JSON
- 使用原生 fetch,不依赖 axios
---
## 2. 项目结构
```
simapi-vue/
├── src/
│ ├── types.ts # 所有类型定义
│ ├── simapi.core.ts # 纯 TS 核心(SimApiCore 类)
│ └── simapi.pinia.ts # Vue3 Pinia Store 封装(useSimApi
├── dist/ # 构建产物(ESM)
├── package.json # 包名 @simcu/simapi
└── vite.config.ts # Vite 构建配置
```
### 导出路径(Subpath Exports
| 导入路径 | 内容 | 适用场景 |
|---------|------|---------|
| `@simcu/simapi` | SimApiCore + 类型 | 纯 TS / Node.js / 任意 JS 环境 |
| `@simcu/simapi/pinia` | useSimApi Pinia Store | **Vue3 项目(推荐)** |
---
## 3. 快速开始(Vue3 项目标准用法)
### 3.1 安装
```bash ```bash
npm install @simcu/simapi npm install @simcu/simapi pinia
``` ```
## 架构 > `pinia` 是 peerDependencyVue3 项目必须安装。
``` ### 3.2 第一步:public/config.json — 外部配置文件
src/
├── types.ts # 类型定义(SimApiBaseResponse、SimApiOptions 等) 在项目的 `public/` 目录下创建 `config.json`,定义 API 地址:
├── simapi.core.ts # 纯 TS 核心,无框架依赖
└── simapi.pinia.ts # Vue3 Pinia Store 封装 ```json
{
"debug": true,
"endpoints": {
"default": "http://127.0.0.1:5210"
}
}
``` ```
## 核心层(框架无关) **为什么用 config.json 而不是硬编码?**
- 前后端分离部署时,API 地址可能变化
- `public/` 下的文件 Vite 会直接复制到输出目录,不经过构建
- 打包后运维人员可以直接修改 `config.json` 切换环境,无需重新构建
适用于浏览器、Node.js、小程序等任意环境。 **config.json 支持的字段:**
| 字段 | 类型 | 说明 | 默认值 |
|------|------|------|--------|
| `debug` | boolean | 是否打印请求/响应日志 | `false` |
| `endpoints` | object | 多端点地址映射 | `{ "default": "" }` |
| `defaultEndpoint` | string | 默认使用的端点名称 | `"default"` |
### 3.3 第二步:main.ts — 创建 Pinia 实例
```typescript ```typescript
import { SimApiCore } from '@simcu/simapi' // src/main.ts
const api = new SimApiCore()
// 从 window.simapi 读取配置(可选)
api.autoInit()
// 或手动配置
api.configure({
api: { endpoints: { default: 'https://api.example.com' } },
debug: true,
})
// 发起请求
const res = await api.query('/users/list', { page: 1 })
// res.code === 200res.data 为业务数据
// 登录
await api.login({ phone: '13800138000', code: '123456' })
// 登出
await api.logout()
// 注册业务错误码回调
api.setBusinessCallback(401, () => router.push('/login'))
api.setBusinessCallback('common', (data) => alert(data.message))
```
## Vue3
安装 Pinia
```bash
npm install pinia
```
```typescript
// main.ts
import { createApp } from 'vue' import { createApp } from 'vue'
import { createPinia } from 'pinia' import { createPinia } from 'pinia' // ← 必须安装并注册 Pinia
import App from './App.vue' import App from './App.vue'
import router from './router'
const app = createApp(App) const app = createApp(App)
app.use(createPinia()) app.use(createPinia()) // ← useSimApi 依赖 Pinia,必须先注册
app.use(router)
app.mount('#app') app.mount('#app')
``` ```
```typescript **顺序很重要**: `createPinia()` 必须在 `useSimApi()` 调用之前完成注册。
// 任意组件
import { useSimApi } from '@simcu/simapi' ### 3.4 第三步:App.vue — 初始化 SimApi 并设置回调
```vue
<!-- src/App.vue -->
<template>
<router-view></router-view>
</template>
<script setup lang="ts">
import { useSimApi } from '@simcu/simapi/pinia' // ← 注意 /pinia 子路径
import { onMounted } from 'vue'
import { useRouter } from 'vue-router'
const api = useSimApi() const api = useSimApi()
const router = useRouter()
// 响应式 onMounted(async () => {
console.log(api.token) // 当前 Token // ① 从 config.json 加载配置(endpoints、debug 等)
console.log(api.isLoggedIn) // 是否已登录 await api.loadFromFile()
// 发起请求 // ② 注册业务错误码回调 —— 401 时跳转登录页
const res = await api.query('/users/list', { page: 1 }) api.setBusinessCallback(401, () => {
console.log(res.data) api.logout()
router.replace({ path: '/login' })
})
// 登录 / 登出 // ③ 注册通用兜底回调 —— 其他所有非 200 错误统一提示
await api.login({ phone: '13800138000', code: '123456' }) api.setBusinessCallback('common', (data: any) => {
await api.logout() console.error('[SimApi]', data.code, data.message)
})
// 调试模式
api.setDebug(true)
```
### 多端点
```typescript
api.setEndpoints({
default: 'https://api.example.com',
admin: 'https://admin.example.com',
}) })
// 指定端点发请求
await api.query('/stats', {}, 'admin')
```
## autoInit — 从 window 读取配置
支持字段:`endpoints``defaultEndpoint``debug`。业务回调需在代码中通过 `setBusinessCallback` 处理。
```html
<script>
window.simapi = {
endpoints: {
default: 'https://api.example.com',
admin: 'https://admin.example.com',
},
defaultEndpoint: 'default',
debug: false,
}
</script> </script>
``` ```
初始化时手动调用: **关键点说明:**
| 要点 | 说明 |
|------|------|
| `import from '@simcu/simapi/pinia'` | Vue3 项目**必须**用 `/pinia` 子路径导入 |
| `onMounted` 中初始化 | 确保 DOM 已加载、Pinia 已就绪 |
| `await api.loadFromFile()` | 从 `config.json` 加载 endpoints、defaultEndpoint、debug |
| `setBusinessCallback(401, fn)` | 当后端返回 code=401 时自动执行 |
| `setBusinessCallback('common', fn)` | 兜底回调,任何非 200 且未匹配其他回调时触发 |
### 3.5 第四步:在组件中使用
```vue
<!-- src/views/UserList.vue -->
<template>
<div>
<button @click="loadUsers">加载用户</button>
<ul v-for="user in users" :key="user.id">{{ user.name }}</ul>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useSimApi } from '@simcu/simapi/pinia'
interface User {
id: string
name: string
}
```typescript
// 方式一:从 window.simapi 读取
const api = useSimApi() const api = useSimApi()
api.autoInit() const users = ref<User[]>([])
// 方式二:直接传入配置 async function loadUsers() {
const api = useSimApi() const res = await api.query<User[]>('/user/list', { page: 1 })
api.configure({ users.value = res.data ?? []
api: { endpoints: { default: 'https://api.example.com' } }, }
}) </script>
``` ```
## SimApiBaseResponse 响应格式 ---
```typescript ## 4. 完整项目模板(可直接复制使用)
interface SimApiBaseResponse<T = any> {
code: number // 200 = 成功,其他为业务错误码
message: string // 提示信息
data?: T // 业务数据
}
```
## 完整配置参考 ### public/config.json
### SimApiOptions — 完整配置结构 ```json
```typescript
interface SimApiOptions {
/** 调试模式,默认 true */
debug?: boolean
/** 认证相关配置 */
auth?: Partial<SimApiAuthConfig>
/** API 相关配置 */
api?: Partial<SimApiApiConfig>
}
```
### SimApiAuthConfig — 认证配置
```typescript
interface SimApiAuthConfig {
/** localStorage 中存储 Token 的 key */
token_name: string // 默认 'simapi-auth-token'
/** 检查登录状态的接口路径 */
check_url: string // 默认 '/auth/check'
/** 登出接口路径 */
logout_url: string // 默认 '/auth/logout'
/** 登录接口路径 */
login_url: string // 默认 '/auth/login'
}
```
### SimApiApiConfig — API 配置
```typescript
interface SimApiApiConfig {
/** 多端点映射,key 为端点名,value 为 baseURL */
endpoints: { [name: string]: string }
/** 默认端点名,默认 'default' */
defaultEndpoint: string
/** 业务错误码回调,key 为错误码或 'common' */
businessCallback: SimApiBusinessCallback
/** 响应拦截回调 */
responseCallback: SimApiResponseCallback
/** 请求超时时间(毫秒),默认 10000 */
timeout?: number
}
```
**⚠️ CORS 说明**
- 本库使用原生 fetch API,默认不发送 Cookie`credentials: 'omit'`
- Token 通过请求头 `Token` 传递,无需依赖 Cookie
- 服务器返回 `Access-Control-Allow-Origin: *` 不会有问题
### SimApiBusinessCallback — 业务回调
key 支持数字错误码或 `'common'`(通用兜底回调):
```typescript
type SimApiBusinessCallback = {
[code: number | string]: (data: SimApiBaseResponse) => void
}
// 示例
{ {
401: (data) => router.push('/login'), // 未授权 "debug": true,
403: (data) => ElMessage.error('无权限'), // 无权限 "endpoints": {
500: (data) => console.error(data), // 服务器错误 "default": "http://localhost:5000"
'common': (data) => ElMessage.error(data.message) // 其他错误码兜底
}
```
### SimApiResponseCallback — 响应拦截
```typescript
interface SimApiResponseCallback {
/** 成功响应拦截,可在此统一处理数据结构 */
success: (response: any) => any
/** 网络/HTTP 错误拦截 */
error: (err: any) => void
}
// 示例:统一脱敏处理
{
success: (res) => {
// fetch 返回 { code, message, data }
return res
},
error: (err) => {
console.error('请求失败', err)
throw err
} }
} }
``` ```
### SimApiVersions — 版本信息 ### src/main.ts
```typescript ```typescript
interface SimApiVersions { import { createApp } from 'vue'
uiApp: string // 前端应用版本 import { createPinia } from 'pinia'
uiSimApi: string // 前端 SimApi 版本 import App from './App.vue'
apiApp: string // 后端应用版本(简化版,如 "1.2.3" import router from './router'
apiSimApi: string // 后端 SimApi 版本(简化版)
apiAppFull: string // 后端应用版本(完整版,如 "1.2.3+20240101" createApp(App)
apiSimApiFull: string // 后端 SimApi 版本(完整版) .use(createPinia())
.use(router)
.mount('#app')
```
### src/App.vue
```vue
<template><router-view /></template>
<script setup lang="ts">
import { useSimApi } from '@simcu/simapi/pinia'
import { onMounted } from 'vue'
import { useRouter } from 'vue-router'
const api = useSimApi()
const router = useRouter()
onMounted(async () => {
await api.loadFromFile()
api.setBusinessCallback(401, () => {
api.logout()
router.replace('/login')
})
})
</script>
```
### src/views/Login.vue
```vue
<template>
<form @submit.prevent="handleLogin">
<input v-model="phone" placeholder="手机号" />
<input v-model="code" placeholder="验证码" />
<button type="submit">登录</button>
</form>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { useSimApi } from '@simcu/simapi/pinia'
const api = useSimApi()
const router = useRouter()
const phone = ref('')
const code = ref('')
async function handleLogin() {
await api.login({ phone: phone.value, code: code.value })
router.push('/')
}
</script>
```
---
## 5. API 参考
### 5.1 import 方式
```typescript
// ✅ Vue3 项目 — 用 Pinia Store(推荐)
import { useSimApi } from '@simcu/simapi/pinia'
// ✅ 非 Vue 项目 / 纯 TS — 用 Core 类
import { SimApiCore } from '@simcu/simapi'
```
### 5.2 useSimApi Store — 方法与属性一览
获取实例:
```typescript
const api = useSimApi() // 单例模式,全局状态共享
```
#### 属性(Getters
| 属性 | 类型 | 说明 |
|------|------|------|
| `api.token` | `string` | 当前存储的 Token(只读) |
| `api.isLoggedIn` | `boolean` | 是否已登录(Token 是否存在) |
| `api.debug` | `boolean` | 调试开关 |
| `api.api` | `SimApiApiConfig` | API 配置对象(一般不直接操作) |
| `api.auth` | `SimApiAuthConfig` | 认证配置对象(一般不直接操作) |
#### 方法(Actions
| 方法 | 参数 | 返回值 | 说明 |
|------|------|--------|------|
| `loadFromFile(file?)` | string (默认 `'config.json'`) | `Promise<void>` | 从 JSON 文件加载 endpoints/debug 配置 |
| `configure(options)` | `SimApiOptions` | `void` | 手动配置(深合并) |
| `setDebug(bool)` | boolean | `void` | 设置调试模式 |
| `setEndpoints(map)` | `{[name]: url}` | `void` | 设置多端点映射 |
| `query(uri, params?, endpoint?, headers?)` | 见下方详解 | `Promise<SimApiBaseResponse<T>>` | **核心方法:发送 POST 请求** |
| `login(request)` | `{[key]: any}` | `Promise<SimApiBaseResponse<string>>` | 登录,成功后自动存 Token |
| `logout(url?)` | string? | `Promise<any>` | 登出,清除 Token 并调后端接口 |
| `checkLogin(url?)` | string? | `Promise<void>` | 检查登录态,过期则触发 401 回调 |
| `setBusinessCallback(code, fn)` | number\|string, callback | `void` | 注册业务错误码回调 |
| `getToken()` | 无 | `string` | 获取当前 Token |
| `setToken(token)` | string | `void` | 手动设置 Token |
| `removeToken()` | 无 | `void` | 清除 Token |
| `getVersion(endpoint?)` | string? | `Promise<SimApiVersions>` | 获取前后端版本信息 |
| `getEndpoint(name?)` | string? | `string` | 获取某端点的 baseURL |
### 5.3 query 方法详解
这是最核心的方法——几乎所有数据交互都通过它:
```typescript
async function query<T = any>(
uri: string, // 接口路径,如 '/user/list'
params?: any = {}, // 请求体(POST body),JSON 对象
endpointKey?: string, // 可选:指定端点名(默认用 default)
extraHeaders?: Record<string, string> // 可选:额外请求头
): Promise<SimApiBaseResponse<T>>
```
**使用示例:**
```typescript
// 基本查询
const res = await api.query<User[]>('/user/list', { page: 1, count: 20 })
console.log(res.data) // User[] 数组
// 错误处理
try {
const res = await api.query('/user/list')
} catch (err: any) {
// err 是 SimApiBaseResponse 类型
console.log(err.code) // 业务错误码,如 400/401/403/500
console.log(err.message) // 错误消息
} }
``` ```
### 完整配置示例 **⚠️ query 的行为要点:**
1. 自动在请求头添加 `Token`(如果存在)
2. `code === 200` → 正常返回 `SimApiBaseResponse<T>`
3. `code !== 200` → 先执行对应的 businessCallback,然后 **throw 异常**
4. 网络错误 → 执行 responseCallback.errorthrow 包装后的 `{code: -1}` 异常
5. 所以调用方只需 `try/catch` 处理异常即可
### 5.4 login / logout 方法
```typescript ```typescript
api.configure({ // loginPOST 到 auth.login_url(默认 /auth/login),自动保存返回的 Token
debug: false, await api.login({ phone: '13800138000', code: '123456' })
auth: { // logout:清除本地 Token,可选调后端登出接口
token_name: 'my-app-token', await api.logout() // 调 /auth/logout
check_url: '/api/auth/check', await api.logout(null) // 只清本地 Token,不调后端
logout_url: '/api/auth/logout', ```
login_url: '/api/auth/login',
},
api: { ### 5.5 setBusinessCallback — 业务错误处理
endpoints: {
default: 'https://api.example.com',
admin: 'https://admin.example.com',
},
defaultEndpoint: 'default',
businessCallback: { ```typescript
401: () => router.push('/login'), api.setBusinessCallback(401, (data) => {
403: () => ElMessage.error('无权限访问'), router.replace('/login')
500: (data) => console.error('服务器错误:', data.message), })
'common': (data) => ElMessage.error(data.message || '请求失败'),
},
responseCallback: { api.setBusinessCallback(403, (data) => {
success: (res) => res.data ?? res, alert('无权限:' + data.message)
error: (err) => { })
console.error('网络错误', err)
}, // 兜底:任何未单独处理的非 200 错误都会走 common
}, api.setBusinessCallback('common', (data) => {
}, console.error('请求失败:', data.code, data.message)
}) })
``` ```
## API 参考 **回调执行顺序:** 匹配具体错误码 → 未匹配则走 `'common'` → 然后 throw
### SimApiCore(核心类) ---
| 方法/属性 | 说明 | ## 6. 类型定义速查
|-----------|------|
| `configure(options)` | 批量配置(深合并) |
| `autoInit()` | 从 `window.simapi` 读取配置(endpoints、defaultEndpoint、debug |
| `setEndpoints(map)` | 设置端点 |
| `setBusinessCallback(code, fn)` | 注册业务错误码回调 |
| `query(uri, params?, endpointKey?, headers?)` | POST 请求,返回 `Promise<SimApiBaseResponse<T>>` |
| `login(request)` | 登录,自动存 Token |
| `logout(url?)` | 登出,清除 Token |
| `checkLogin(url?)` | 主动检查登录状态 |
| `getVersion(endpointName?)` | 获取版本信息,返回 `Promise<SimApiVersions>` |
| `getToken()` | 获取 Token |
| `setToken(token)` | 手动设置 Token |
| `removeToken()` | 清除 Token |
| `isLoggedIn` | getter,是否已登录 |
| `debug` | boolean,调试模式 |
| `logDebug(...args)` | 日志工具(仅在 debug 模式输出) |
## 构建
```bash
npm install
npm run build
npm link # 本地调试
```
### 版本号注入
库使用 `declare const` 声明版本常量,构建时通过 Vite 的 `define` 注入版本号。
**注意:**
- **SimApiVersion**:由 simapi 库构建时注入
- **AppVersion**:不注入,留给调用方 APP 注入
**支持的版本号环境变量(按优先级):**
1. `VITE_SimApiVersion` — Vite .env 文件
2. `npm_config_SimApiVersion` — npm 构建时通过 `npm run build` 前设置
3. `SimApiVersion` — 直接环境变量(如 GitHub Actions
未指定时默认为 `0.0.0-develop`
**本地构建:**
```bash
# Windows PowerShell
$env:npm_config_SimApiVersion='1.0.0'; npm run build
# Windows CMD
set npm_config_SimApiVersion=1.0.0 && npm run build
# Linux/Mac
npm_config_SimApiVersion=1.0.0 npm run build
```
**GitHub Actions / CI/CD**
```yaml
- name: Build
env:
SimApiVersion: ${{ github.ref_name }} # 或其他版本号
run: npm run build
```
**调用方注入 AppVersion**
调用方在自己的 `vite.config.ts` 中添加:
```typescript ```typescript
define: { /** 标准响应 */
'AppVersion': JSON.stringify('1.0.0') interface SimApiBaseResponse<T = any> {
code: number
message: string
data?: T
}
/** 版本信息 */
interface SimApiVersions {
uiApp: string
uiSimApi: string
apiApp: string
apiSimApi: string
apiAppFull: string
apiSimApiFull: string
}
/** 认证配置 */
interface SimApiAuthConfig {
token_name: string
check_url: string
logout_url: string
login_url: string
}
/** API 配置 */
interface SimApiApiConfig {
endpoints: { [name]: string }
defaultEndpoint: string
businessCallback: SimApiBusinessCallback
responseCallback: SimApiResponseCallback
timeout?: number
}
/** 完整选项 */
interface SimApiOptions {
debug?: boolean
auth?: Partial<SimApiAuthConfig>
api?: Partial<SimApiApiConfig>
} }
``` ```
--- ---
## 从旧版迁移 ## 7. 多端点支持
如果你之前使用的是带 axios 的版本,迁移非常简单: ```json
{
"debug": true,
"endpoints": {
"default": "https://api.example.com",
"admin": "https://admin.example.com",
"cdn": "https://cdn.example.com"
},
"defaultEndpoint": "default"
}
```
```typescript
// 使用默认端点
await api.query('/user/list')
// 指定端点
await api.query('/system/stats', {}, 'admin')
```
也可以运行时动态添加:
```typescript
api.setEndpoints({ backup: 'https://backup-api.example.com' })
```
---
## 8. configure — 手动完整配置
除了 `loadFromFile``config.json` 读取外,也可以手动配置一切:
```typescript
api.configure({
debug: false,
auth: { token_name: 'my-app-token' },
api: {
endpoints: { default: 'https://api.example.com' },
defaultEndpoint: 'default',
timeout: 15000,
businessCallback: {
401: () => router.replace('/login'),
403: (data) => alert('无权限'),
'common': (data) => MessagePlugin.error(data.message),
},
},
})
```
**loadFromFile vs configure 的关系:**
- `loadFromFile()` 只读 `config.json``endpoints``defaultEndpoint``debug`
- `configure()` 可以覆盖所有字段,包括 auth 和 callbacks
- 通常做法是:`loadFromFile()` 读基础配置 + `setBusinessCallback()` 补充回调
---
## 9. GOTCHASAI 最容易犯的错)
| ❌ 错误 | ✅ 正确 |
|---------|---------|
| `import { useSimApi } from '@simcu/simapi'` Vue3 | `from '@simcu/simapi/pinia'`(必须带 `/pinia` |
| 忘记 `app.use(createPinia())` | **必须在 `useSimApi()` 之前**注册 Pinia |
| 用 `Authorization: Bearer xxx` | 用 `Token` 请求头(这是 simapi-net 约定) |
| 期望 HTTP 4xx/5xx 表示错误 | 所有错误都是 **HTTP 200 + JSON `code` 字段** |
| `res.data` 直接用而不判空 | `res.data` 可能是 `undefined`,用 `res.data ?? []` |
| 在 setup 外部调用 `useSimApi()` | `useSimApi()` 只能在 **setup 上下文**中调用 |
| 用 Authorization Bearer 传 Token | Token 通过 **请求头 `Token`** + **Cookie** 存储 |
| `new SimApiCore()` 在 Vue 项目里用 | Vue 项目统一用 `useSimApi()` Pinia Store |
---
## 10. 与 simapi-net 后端的对接约定
### 10.1 通信协议
```
[Vue 前端] -- POST(JSON) --> [simapi-net 后端]
Header: Token: <value>
Body: { key: value }
[Vue 前端] <-- JSON {code, message, data} -- [simapi-net 后端]
(HTTP Status 始终 200)
```
### 10.2 内置路由对照表
| simapi-net 路由 | simapi-vue 方法 | 触发条件 |
|-----------------|-----------------|----------|
| `POST /auth/login` | `api.login(request)` | `EnableSimApiAuth = true` |
| `POST /auth/check` | `api.checkLogin()` | `EnableSimApiAuth = true` |
| `POST /auth/logout` | `api.logout()` | `EnableSimApiAuth = true` |
| `POST /user/info` | `api.query('/user/info')` | `EnableSimApiAuth = true`(需登录) |
| `GET /versions` | `api.getVersion()` | `EnableVersionUrl`(默认开启) |
---
## 11. 构建
```bash
npm run dev # 开发模式
npm run build # 生产构建
```
构建产物位于 `dist/` 目录:
- `dist/index.mjs` — 核心(SimApiCoreESM 入口
- `dist/pinia.mjs` — Pinia Store ESM 入口
- `dist/*.d.ts` — TypeScript 类型声明
### 版本号注入
库使用 `declare const` 声明版本常量,构建时通过 Vite 的 `define` 注入。
**SimApiVersion** 由 simapi 库自身构建时从 `package.json` 注入。未配置时默认为 `0.0.0-develop`
**AppVersion** 由调用方项目在自己的 `vite.config.ts` 中注入。
---
## 12. 从 axios 迁移
```diff ```diff
- import axios from 'axios' - import axios from 'axios'
+ import { SimApiCore } from '@simcu/simapi' - const res = await axios.post('/user/list', { page: 1 })
- // ... 你的 axios 配置
+ const api = new SimApiCore() + import { useSimApi } from '@simcu/simapi/pinia'
+ api.setEndpoints({ default: 'https://api.example.com' }) + const api = useSimApi()
+ const res = await api.query('/users/list', { page: 1 }) + const res = await api.query('/user/list', { page: 1 })
``` ```
API 完全兼容,无需其他改动。主要变化: | axios | simapi-vue |
- 使用原生 fetch 替代 axios |-------|-----------|
- Token 通过请求头 `Token` 传递(不是 `Authorization: Bearer` | `axios.post()` | `api.query()` |
- 默认不发送 Cookie,避免 CORS 问题 | `response.data` 直接是业务数据 | `SimApiBaseResponse<T>.data` 是业务数据 |
| HTTP 4xx/5xx 表示错误 | HTTP 200 + `code` 字段表示错误 |
| `interceptors.response` | `businessCallback` + `responseCallback` |
| Authorization Bearer | Token header |
+18
View File
@@ -12,6 +12,7 @@
"tslib": "^2.3.0" "tslib": "^2.3.0"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^25.6.0",
"concurrently": "^9.2.1", "concurrently": "^9.2.1",
"pinia": "^2.2.0", "pinia": "^2.2.0",
"typescript": "~5.9.3", "typescript": "~5.9.3",
@@ -433,6 +434,16 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/node": {
"version": "25.6.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz",
"integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~7.19.0"
}
},
"node_modules/@vue/compiler-core": { "node_modules/@vue/compiler-core": {
"version": "3.5.32", "version": "3.5.32",
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.32.tgz", "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.32.tgz",
@@ -1004,6 +1015,13 @@
"node": ">=14.17" "node": ">=14.17"
} }
}, },
"node_modules/undici-types": {
"version": "7.19.2",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz",
"integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==",
"dev": true,
"license": "MIT"
},
"node_modules/vite": { "node_modules/vite": {
"version": "5.4.21", "version": "5.4.21",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
+1
View File
@@ -42,6 +42,7 @@
"tslib": "^2.3.0" "tslib": "^2.3.0"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^25.6.0",
"concurrently": "^9.2.1", "concurrently": "^9.2.1",
"pinia": "^2.2.0", "pinia": "^2.2.0",
"typescript": "~5.9.3", "typescript": "~5.9.3",
+291 -255
View File
@@ -11,300 +11,336 @@
*/ */
import { import {
type SimApiVersions, type SimApiVersions,
type SimApiAuthConfig, type SimApiAuthConfig,
type SimApiApiConfig, type SimApiApiConfig,
type SimApiOptions, type SimApiOptions,
type SimApiBaseResponse, type SimApiBaseResponse,
} from './types' } from './types'
export type { export type {
SimApiVersions, SimApiVersions,
SimApiAuthConfig, SimApiAuthConfig,
SimApiApiConfig, SimApiApiConfig,
SimApiOptions, SimApiOptions,
SimApiBaseResponse, SimApiBaseResponse,
} from './types' } from './types'
declare const SimApiVersion: string; declare const SimApiVersion: string;
declare const AppVersion: string;
// ── Helper: Fetch with Timeout ──────────────────────────────────────── // ── Helper: Fetch with Timeout ────────────────────────────────────────
function fetchWithTimeout( function fetchWithTimeout(
url: string, url: string,
options: RequestInit, options: RequestInit,
timeout: number = 10000 timeout: number = 10000
): Promise<Response> { ): Promise<Response> {
return Promise.race([ return Promise.race([
fetch(url, options), fetch(url, options),
new Promise<never>((_, reject) => new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error(`Request timeout after ${timeout}ms`)), timeout) setTimeout(() => reject(new Error(`Request timeout after ${timeout}ms`)), timeout)
), ),
]) ])
} }
// ── Helper: Fetch POST with JSON body ──────────────────────────────────── // ── Helper: Fetch POST with JSON body ────────────────────────────────────
async function fetchPost<T = any>( async function fetchPost<T = any>(
url: string, url: string,
body: any, body: any,
headers: Record<string, string>, headers: Record<string, string>,
timeout: number timeout: number
): Promise<SimApiBaseResponse<T>> { ): Promise<SimApiBaseResponse<T>> {
const options: RequestInit = { const options: RequestInit = {
method: 'POST', method: 'POST',
headers: headers as HeadersInit, headers: headers as HeadersInit,
body: body instanceof FormData ? body : JSON.stringify(body), body: body instanceof FormData ? body : JSON.stringify(body),
credentials: 'omit', // 从不发送 Cookie credentials: 'omit', // 从不发送 Cookie
}
const response = await fetchWithTimeout(url, options, timeout)
if (!response.ok) {
const errorData = await response.json().catch(() => ({}))
throw {
status: response.status,
statusText: response.statusText,
data: errorData,
message: `HTTP ${response.status}: ${response.statusText}`,
} }
}
return response.json() const response = await fetchWithTimeout(url, options, timeout)
if (!response.ok) {
const errorData = await response.json().catch(() => ({}))
throw {
status: response.status,
statusText: response.statusText,
data: errorData,
message: `HTTP ${response.status}: ${response.statusText}`,
}
}
return response.json()
} }
// ── SimApiCore ──────────────────────────────────────── // ── SimApiCore ────────────────────────────────────────
export class SimApiCore { export class SimApiCore {
debug: boolean = true private debug: boolean = true
auth: SimApiAuthConfig = { private auth: SimApiAuthConfig = {
token_name: 'simapi-auth-token', token_name: 'simapi-auth-token',
check_url: '/auth/check', check_url: '/user/info',
logout_url: '/auth/logout', logout_url: '/auth/logout',
login_url: '/auth/login', login_url: '/auth/login',
} tokenStore: "localstorage"
api: SimApiApiConfig = {
endpoints: { default: '' },
defaultEndpoint: 'default',
businessCallback: {
401: () => localStorage.removeItem(this.auth.token_name),
common: () => {},
},
responseCallback: {
success: (response: any) => response,
error: (_err: any) => {},
},
timeout: 10000,
}
constructor(options?: SimApiOptions) {
if (options) {
this.configure(options)
} }
} private webConfig: Map<string, Record<string, object>> = new Map();
/** private api: SimApiApiConfig = {
* 从 window.simapi 读取配置并初始化 endpoints: {default: ''},
* defaultEndpoint: 'default',
* 支持字段:endpoints, defaultEndpoint, debug businessCallback: {
* 业务回调(businessCallback / responseCallback)需在代码中处理 401: () => this.removeToken(),
*/ common: () => {
autoInit(): void { },
const config = (window as any).simapi },
if (!config) return responseCallback: {
success: (response: any) => response,
if (config.debug !== undefined) { error: (_err: any) => {
this.debug = config.debug },
},
timeout: 10000
} }
if (config.endpoints) {
this.api.endpoints = { ...this.api.endpoints, ...config.endpoints }
}
if (config.defaultEndpoint) {
this.api.defaultEndpoint = config.defaultEndpoint
}
}
configure(options: SimApiOptions): void { constructor(options?: SimApiOptions) {
if (options.debug !== undefined) { if (options) {
this.debug = options.debug this.configure(options)
}
if (options.auth) {
this.auth = { ...this.auth, ...options.auth }
}
if (options.api) {
this.api = {
...this.api,
...options.api,
endpoints: { ...this.api.endpoints, ...(options.api.endpoints ?? {}) },
businessCallback: { ...this.api.businessCallback, ...(options.api.businessCallback ?? {}) },
responseCallback: { ...this.api.responseCallback, ...(options.api.responseCallback ?? {}) },
}
}
}
setEndpoints(endpoints: { [name: string]: string }): void {
this.api.endpoints = { ...this.api.endpoints, ...endpoints }
}
getEndpoint(name?: string): string {
return this.api.endpoints[name ?? this.api.defaultEndpoint] ?? ''
}
setBusinessCallback(code: number | string, callback: (data: any) => void): void {
this.api.businessCallback[code] = callback
}
getToken(): string {
return localStorage.getItem(this.auth.token_name) ?? ''
}
setToken(token: string): void {
localStorage.setItem(this.auth.token_name, token)
}
removeToken(): void {
localStorage.removeItem(this.auth.token_name)
}
get isLoggedIn(): boolean {
return !!localStorage.getItem(this.auth.token_name)
}
genS4(): string {
return (((1 + Math.random()) * 0x10000 * Date.parse(new Date().toString())) | 0)
.toString(16)
.substring(1)
}
/**
* 日志工具(仅在 debug 模式下输出)
*
* @example
* api.logDebug('用户登录', { id: 1, name: 'test' })
* api.logDebug('请求开始', uri, params)
*/
logDebug(...args: any[]): void {
if (!this.debug) return
console.log('[DEBUG]', ...args)
}
/**
* 获取版本信息
*
* @param endpointName - 指定从哪个 endpoint 获取版本,默认使用 default endpoint
* @returns 版本信息对象
*
* @example
* // 从默认 endpoint 获取
* const versions = await api.getVersion()
*
* // 从指定 endpoint 获取
* const versions = await api.getVersion('backup')
*/
async getVersion(endpointName?: string): Promise<SimApiVersions> {
const versions: SimApiVersions = {
uiApp: '0.0.0-develop',
uiSimApi: typeof SimApiVersion === 'undefined' ? "0.0.0-develop" : SimApiVersion,
apiApp: '0.0.0',
apiSimApi: '0.0.0',
apiAppFull: '0.0.0',
apiSimApiFull: '0.0.0',
};
try {
const resp = await this.query<any>('/versions', {}, endpointName)
if (resp?.data) {
const d = resp.data
versions.apiApp= d.App?.split('+')[0] ?? '0.0.0';
versions.apiSimApi= d.SimApi?.split('+')[0] ?? '0.0.0';
versions.apiAppFull= d.App ?? '0.0.0';
versions.apiSimApiFull= d.SimApi ?? '0.0.0';
if (this.debug) {
console.log(`UI主应用版本: ${versions.uiApp}\nUISimApi版本: ${versions.uiSimApi}\nAPI主应用版本: ${versions.apiApp}\nAPISimApi版本: ${versions.apiSimApi}`)
} }
return versions
}
} catch {
// 版本获取失败返回默认值
}
return versions;
}
async query<T = any>(
uri: string,
params: any = {},
endpointKey?: string,
extraHeaders?: Record<string, string>
): Promise<SimApiBaseResponse<T>> {
const headers: Record<string, string> = { ...extraHeaders, ...{} }
const queryId = this.genS4()
if (!(params instanceof FormData)) {
headers['Content-Type'] = 'application/json'
} }
const token = this.getToken() /**
if (token) { * 从 JSON 文件加载配置
headers['Token'] = token *
* 部署时替换 config.json 即可切换环境,无需重新构建。
*
* @param file - 配置文件路径,默认 'config.json'
* @example
* await api.loadFromFile()
* await api.loadFromFile('/env/prod.json')
*/
async loadFromFile(file: string = 'config.json'): Promise<void> {
try {
const resp = await fetch(file)
if (!resp.ok) {
this.logDebug(`配置文件 ${file} 加载失败: HTTP ${resp.status}`)
return
}
const config = await resp.json()
if (config.debug !== undefined) {
this.debug = config.debug
}
if (config.endpoints) {
this.api.endpoints = {...this.api.endpoints, ...config.endpoints}
}
if (config.defaultEndpoint) {
this.api.defaultEndpoint = config.defaultEndpoint
}
} catch (err: any) {
this.logDebug(`配置文件 ${file} 加载失败: ${err.message}`)
}
} }
if (this.debug) { configure(options: SimApiOptions): void {
headers['Query-Id'] = queryId if (options.debug !== undefined) {
console.log('[REQUEST*]', queryId, '->', uri, 'AUTH:', localStorage.getItem(this.auth.token_name)) this.debug = options.debug
}
if (options.auth) {
this.auth = {...this.auth, ...options.auth}
}
if (options.api) {
this.api = {
...this.api,
...options.api,
endpoints: {...this.api.endpoints, ...(options.api.endpoints ?? {})},
businessCallback: {...this.api.businessCallback, ...(options.api.businessCallback ?? {})},
responseCallback: {...this.api.responseCallback, ...(options.api.responseCallback ?? {})},
}
}
} }
const url = this.getEndpoint(endpointKey) + uri get isDebug() {
return this.debug
try {
const respData = await fetchPost<T>(
url,
params,
headers,
this.api.timeout ?? 10000
)
if (this.debug) {
console.log('[RESPONSE]', queryId, '->', respData)
}
const processedData = this.api.responseCallback.success(respData) as SimApiBaseResponse<T>
// 业务回调处理
if (this.api.businessCallback.hasOwnProperty(processedData.code)) {
this.api.businessCallback[processedData.code](processedData)
} else if (this.api.businessCallback['common'] && processedData.code !== 200) {
this.api.businessCallback['common'](processedData)
}
// 直接返回,不再根据 code 抛出错误
return processedData
} catch (error) {
if (this.debug) {
console.log('[RESPONSE]', queryId, '->', error)
}
this.api.responseCallback.error(error)
throw error
} }
}
async login(request: Record<string, any>): Promise<SimApiBaseResponse<string>> { setDebug(debug: boolean) {
const result = await this.query<string>(this.auth.login_url, request) this.debug = debug;
if (result?.data) {
this.setToken(result.data)
} }
return result
}
async logout(url?: string | null): Promise<any> { setEndpoints(endpoints: { [name: string]: string }): void {
this.removeToken() this.api.endpoints = {...this.api.endpoints, ...endpoints}
if (url !== null) {
return this.query(url ?? this.auth.logout_url).catch(() => true)
} }
return true
}
async checkLogin(url?: string | null): Promise<void> { getEndpoint(name?: string): string {
if (url !== null) { return this.api.endpoints[name ?? this.api.defaultEndpoint] ?? ''
await this.query(url ?? this.auth.check_url).catch(() => {}) }
} else if (this.getToken()) {
this.api.businessCallback[401]?.(null) setBusinessCallback(code: number | string, callback: (data: any) => void): void {
this.api.businessCallback[code] = callback
}
getToken(): string {
if (this.auth.tokenStore === 'localstorage') {
return localStorage.getItem(this.auth.token_name) ?? ''
}
const name = this.auth.token_name
const match = document.cookie.match(new RegExp(`(?:^|;)\\s?${name}=([^;]+)`))
return match ? match[1] : ''
}
setToken(token: string): void {
if (this.auth.tokenStore === 'localstorage') {
localStorage.setItem(this.auth.token_name, token)
} else {
const name = this.auth.token_name
document.cookie = `${name}=${token}; path=/; max-age=315360000; secure; samesite=none`
}
}
removeToken(): void {
if (this.auth.tokenStore === 'localstorage') {
localStorage.removeItem(this.auth.token_name)
} else {
const name = this.auth.token_name
document.cookie = `${name}=; path=/; max-age=0; secure; samesite=none`
}
}
genS4(): string {
return (((1 + Math.random()) * 0x10000 * Date.parse(new Date().toString())) | 0)
.toString(16)
.substring(1)
}
/**
* 日志工具(仅在 debug 模式下输出)
*
* @example
* api.logDebug('用户登录', { id: 1, name: 'test' })
* api.logDebug('请求开始', uri, params)
*/
logDebug(...args: any[]): void {
if (!this.debug) return
console.log('[DEBUG]', ...args)
}
async getConfig(reload: boolean = false, endpointName: string = 'default'): Promise<Record<string, object>> {
if (!this.webConfig.has(endpointName) || reload) {
const versions: SimApiVersions = {
uiApp: typeof AppVersion === 'undefined' ? "0.0.0-develop" : AppVersion,
uiSimApi: typeof SimApiVersion === 'undefined' ? "0.0.0-develop" : SimApiVersion,
apiApp: '0.0.0',
apiSimApi: '0.0.0',
apiAppFull: '0.0.0',
apiSimApiFull: '0.0.0',
};
const resp = await this.query<any>('/config', {}, endpointName)
if (resp?.data) {
const d = resp.data.Versions;
versions.apiApp = d.App?.split('+')[0] ?? '0.0.0';
versions.apiSimApi = d.SimApi?.split('+')[0] ?? '0.0.0';
versions.apiAppFull = d.App ?? '0.0.0';
versions.apiSimApiFull = d.SimApi ?? '0.0.0';
if (this.debug) {
console.log(`UI主应用版本: ${versions.uiApp}\nUISimApi版本: ${versions.uiSimApi}\nAPI主应用版本: ${versions.apiApp}\nAPISimApi版本: ${versions.apiSimApi}`)
}
const conf = JSON.parse(JSON.stringify(resp.data));
conf.Versions = versions;
this.webConfig.set(endpointName, conf);
}
}
return this.webConfig.get(endpointName)!;
}
async query<T = any>(
uri: string,
params: any = {},
endpointKey?: string,
extraHeaders?: Record<string, string>,
selfHandleError: boolean = false
): Promise<SimApiBaseResponse<T>> {
const headers: Record<string, string> = {...extraHeaders, ...{}}
const queryId = this.genS4()
if (!(params instanceof FormData)) {
headers['Content-Type'] = 'application/json'
}
const token = this.getToken()
if (token) {
headers['Token'] = token
}
if (this.debug) {
headers['Query-Id'] = queryId
console.log('[REQUEST*]', queryId, '->', uri, 'AUTH:', this.getToken())
}
const url = this.getEndpoint(endpointKey) + uri
try {
const respData = await fetchPost<T>(
url,
params,
headers,
this.api.timeout ?? 10000
)
if (this.debug) {
console.log('[RESPONSE]', queryId, '->', respData)
}
const processedData = this.api.responseCallback.success(respData) as SimApiBaseResponse<T>
// 业务回调处理
if (!selfHandleError) {
if (this.api.businessCallback.hasOwnProperty(processedData.code)) {
this.api.businessCallback[processedData.code](processedData)
} else if (this.api.businessCallback['common'] && processedData.code !== 200) {
this.api.businessCallback['common'](processedData)
}
}
// code != 200 时抛出业务错误
if (processedData.code !== 200) {
throw processedData
}
return processedData
} catch (error: any) {
if (this.debug) {
console.log('[RESPONSE]', queryId, '->', error)
}
// 网络/HTTP 错误:包装成标准响应格式抛出
if (!error?.code) {
this.api.responseCallback.error(error)
throw {
code: -1,
message: error?.message || '网络错误',
data: error,
} as SimApiBaseResponse<T>
}
// 业务错误直接抛出
throw error
}
}
async login(request: Record<string, any>): Promise<SimApiBaseResponse<string>> {
const result = await this.query<string>(this.auth.login_url, request)
if (result?.data) {
this.setToken(result.data)
}
return result
}
async logout(url?: string | null): Promise<any> {
if (url !== null) {
this.query(url ?? this.auth.logout_url).catch(() => true)
}
this.removeToken()
return true
}
async checkLogin(url?: string | null): Promise<any> {
return this.query(url ?? this.auth.check_url);
} }
}
} }
+62 -70
View File
@@ -1,91 +1,83 @@
import { defineStore } from 'pinia' import {defineStore} from 'pinia'
import { SimApiCore } from './simapi.core' import {SimApiCore} from './simapi.core'
import type { SimApiBaseResponse, SimApiOptions, SimApiVersions } from './types' import type {SimApiBaseResponse, SimApiOptions, SimApiVersions} from './types'
// ============ Pinia Store ============ // ============ Pinia Store ============
// 仅作为 core 的代理映射,不维护任何独立状态 // 仅作为 core 的代理映射,不维护任何独立状态
export const useSimApi = defineStore('simapi', { export const useSimApi = defineStore('simapi', {
state: () => ({ state: () => ({
// 在 state 中实例化 core // 在 state 中实例化 core
_core: new SimApiCore(), _core: new SimApiCore(),
}), }),
getters: {
getters: { IsDebug: state => state._core.isDebug
// 直接映射 core 的属性和方法
debug: (state) => state._core.debug,
token: (state) => state._core.getToken(),
isLoggedIn: (state) => state._core.isLoggedIn,
api: (state) => state._core.api,
auth: (state) => state._core.auth,
},
actions: {
// 所有方法直接代理到 core
autoInit(): void {
this._core.autoInit()
}, },
actions: {
// 所有方法直接代理到 core
async loadFromFile(file: string = '/config.json'): Promise<void> {
return this._core.loadFromFile(file)
},
configure(options: SimApiOptions): void { configure(options: SimApiOptions): void {
this._core.configure(options) this._core.configure(options)
}, },
setDebug(debug: boolean): void { setDebug(debug: boolean): void {
this._core.debug = debug this._core.setDebug(debug);
}, },
setEndpoints(endpoints: { [name: string]: string }): void { setEndpoints(endpoints: { [name: string]: string }): void {
this._core.setEndpoints(endpoints) this._core.setEndpoints(endpoints)
}, },
setBusinessCallback( setBusinessCallback(
code: number | string, code: number | string,
callback: (data: SimApiBaseResponse) => void callback: (data: SimApiBaseResponse) => void
): void { ): void {
this._core.setBusinessCallback(code, callback) this._core.setBusinessCallback(code, callback)
}, },
getToken(): string { getToken(): string {
return this._core.getToken() return this._core.getToken()
}, },
setToken(token: string): void { setToken(token: string): void {
this._core.setToken(token) this._core.setToken(token)
}, },
removeToken(): void { removeToken(): void {
this._core.removeToken() this._core.removeToken()
}, },
async login(request: Record<string, any>): Promise<SimApiBaseResponse<string>> { async login(request: Record<string, any>): Promise<SimApiBaseResponse<string>> {
return this._core.login(request) return this._core.login(request)
}, },
async logout(url?: string | null): Promise<any> { async logout(url?: string | null): Promise<any> {
return this._core.logout(url) return this._core.logout(url)
}, },
async checkLogin(url?: string | null): Promise<void> { async checkLogin(url?: string | null): Promise<void> {
return this._core.checkLogin(url) return this._core.checkLogin(url)
}, },
async query<T = any>( async query<T = any>(
uri: string, uri: string,
params?: any, params?: any,
endpointKey?: string, endpointKey?: string,
extraHeaders?: Record<string, string> extraHeaders?: Record<string, string>,
): Promise<SimApiBaseResponse<T>> { selfHandleError: boolean = false
return this._core.query<T>(uri, params, endpointKey, extraHeaders) ): Promise<SimApiBaseResponse<T>> {
}, return this._core.query<T>(uri, params, endpointKey, extraHeaders, selfHandleError)
},
getEndpoint(name?: string): string { getEndpoint(name?: string): string {
return this._core.getEndpoint(name) return this._core.getEndpoint(name)
}, },
async getVersion(endpointName?: string): Promise<SimApiVersions> { async getConfig(reload = false, endpointName?: string): Promise<Record<string, object>> {
return this._core.getVersion(endpointName) return this._core.getConfig(reload, endpointName)
},
}, },
},
}) })
+35 -33
View File
@@ -4,61 +4,63 @@
/** 版本信息 */ /** 版本信息 */
export interface SimApiVersions { export interface SimApiVersions {
uiApp: string uiApp: string
uiSimApi: string uiSimApi: string
apiApp: string apiApp: string
apiSimApi: string apiSimApi: string
apiAppFull: string apiAppFull: string
apiSimApiFull: string apiSimApiFull: string
} }
/** 认证配置 */ /** 认证配置 */
export interface SimApiAuthConfig { export interface SimApiAuthConfig {
/** localStorage key,默认 'simapi-auth-token' */ /** localStorage key,默认 'simapi-auth-token' */
token_name: string token_name: string
/** 检查登录接口,默认 '/auth/check' */ /** 检查登录接口,默认 '/auth/check' */
check_url: string check_url: string
/** 登出接口,默认 '/auth/logout' */ /** 登出接口,默认 '/auth/logout' */
logout_url: string logout_url: string
/** 登录接口,默认 '/auth/login' */ /** 登录接口,默认 '/auth/login' */
login_url: string login_url: string
/** Token 存储位置 */
tokenStore: string
} }
/** 业务错误码回调 */ /** 业务错误码回调 */
export interface SimApiBusinessCallback { export interface SimApiBusinessCallback {
[key: number | string]: (data: any) => void [key: number | string]: (data: any) => void
} }
/** 响应拦截回调 */ /** 响应拦截回调 */
export interface SimApiResponseCallback { export interface SimApiResponseCallback {
success: (response: any) => any success: (response: any) => any
error: (err: any) => void error: (err: any) => void
} }
/** API 配置 */ /** API 配置 */
export interface SimApiApiConfig { export interface SimApiApiConfig {
/** 多端点映射 */ /** 多端点映射 */
endpoints: { [name: string]: string } endpoints: { [name: string]: string }
/** 默认端点名称 */ /** 默认端点名称 */
defaultEndpoint: string defaultEndpoint: string
/** 业务错误码回调 */ /** 业务错误码回调 */
businessCallback: SimApiBusinessCallback businessCallback: SimApiBusinessCallback
/** 响应拦截器 */ /** 响应拦截器 */
responseCallback: SimApiResponseCallback responseCallback: SimApiResponseCallback
/** 请求超时时间(毫秒),默认 10000 */ /** 请求超时时间(毫秒),默认 10000 */
timeout?: number timeout?: number
} }
/** SimApi 完整配置 */ /** SimApi 完整配置 */
export interface SimApiOptions { export interface SimApiOptions {
debug?: boolean debug?: boolean
auth?: Partial<SimApiAuthConfig> auth?: Partial<SimApiAuthConfig>
api?: Partial<SimApiApiConfig> api?: Partial<SimApiApiConfig>
} }
/** SimApi 标准响应格式 */ /** SimApi 标准响应格式 */
export interface SimApiBaseResponse<T = any> { export interface SimApiBaseResponse<T = any> {
code: number code: number
message: string message: string
data?: T data?: T
} }
+2
View File
@@ -14,6 +14,8 @@
"noUnusedLocals": false, "noUnusedLocals": false,
"noUnusedParameters": false, "noUnusedParameters": false,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"allowJs": true,
"checkJs": false,
"experimentalDecorators": true, "experimentalDecorators": true,
"emitDecoratorMetadata": true "emitDecoratorMetadata": true
}, },
+4 -14
View File
@@ -1,8 +1,7 @@
import { defineConfig, loadEnv } from 'vite' import { defineConfig } from 'vite'
import { readFileSync } from 'node:fs'
const pkg = JSON.parse(readFileSync('./package.json', 'utf-8'))
export default defineConfig(({ mode }) => { export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd())
return { return {
build: { build: {
outDir: 'dist', outDir: 'dist',
@@ -31,16 +30,7 @@ export default defineConfig(({ mode }) => {
} }
}, },
define: { define: {
// SimApiVersion 由环境变量注入,支持: SimApiVersion: JSON.stringify(pkg.version)
// - VITE_SimApiVersion (Vite .env 文件)
// - npm_config_SimApiVersion (npm 构建时)
// - SimApiVersion (直接环境变量,如 GitHub Actions)
'SimApiVersion': JSON.stringify(
env.VITE_SimApiVersion ||
process.env.npm_config_SimApiVersion ||
process.env.SimApiVersion ||
'0.0.0-develop'
),
} }
} }
}) })