Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa4032f239 | ||
|
|
713b8ae31e | ||
|
|
e0754ad635 | ||
|
|
39626de073 | ||
|
|
13c836b4ab | ||
|
|
88be4210aa | ||
|
|
4257f72799 | ||
|
|
46257ecf4a | ||
|
|
a2f57bc419 | ||
|
|
fe77c0ee1a | ||
|
|
98c46f4a7b | ||
|
|
ae742d452d | ||
|
|
29d3705073 | ||
|
|
df0d7a4fe3 | ||
|
|
d7aa0f171d | ||
|
|
b6d5a5dbe1 | ||
|
|
e91ddd36bf | ||
|
|
903f70b0ba |
@@ -31,7 +31,7 @@ jobs:
|
||||
run: npm ci
|
||||
|
||||
- name: Build
|
||||
run: npm run build -- --define=$VERSION
|
||||
run: npm run build
|
||||
|
||||
- name: Publish to npm
|
||||
run: npm publish --access public
|
||||
-138
@@ -1,138 +0,0 @@
|
||||
# @simcu/simapi — AI 开发指南
|
||||
|
||||
> 面向 AI Agent 的代码结构说明,帮助理解、修改和扩展本库。
|
||||
|
||||
---
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
simapi-vue/
|
||||
├── src/
|
||||
│ ├── types.ts # 类型定义,全部导出
|
||||
│ ├── simapi.core.ts # 核心类 SimApiCore,零框架依赖
|
||||
│ └── simapi.pinia.ts # Pinia Store,Vue3 适配层
|
||||
├── dist/ # 构建产物
|
||||
├── package.json # exports: "/" → core, "/pinia" → vue
|
||||
├── vite.core.config.ts # 构建 core → dist/index.mjs/cjs
|
||||
├── vite.pinia.config.ts # 构建 pinia → dist/pinia.mjs
|
||||
└── 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'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## SimApiCore(simapi.core.ts)
|
||||
|
||||
**职责**:纯 TS HTTP 客户端,基于原生 fetch API,不依赖任何框架。
|
||||
|
||||
**关键设计**:
|
||||
|
||||
- **零依赖**:使用原生 fetch,无 axios 或其他 HTTP 库
|
||||
- **无 Cookie**:所有请求使用 `credentials: 'omit'`,避免 CORS 问题
|
||||
- **Token 传递**:通过请求头 `Token` 传递认证信息
|
||||
- **超时控制**:通过 `fetchWithTimeout` 辅助函数实现
|
||||
- `setEndpoints()` 会自动调用 `fetchVersions()` 查询后端版本(`/versions`),打印到控制台
|
||||
- `handleResponse()` 在响应非 200 时触发 `businessCallback`,不抛出异常
|
||||
- `query()` 抛出异常的只有网络/HTTP 错误,业务错误码通过回调处理
|
||||
- `isLoggedIn` 是 getter,基于 localStorage 中的 token 判断
|
||||
|
||||
**修改建议**:
|
||||
|
||||
- 改请求方法(GET/PUT/DELETE):在 `fetchPost()` 内新增 `method` 参数分支,或新增 `fetchGet()`/`fetchPut()` 方法
|
||||
- 改 Token 存储:替换 `localStorage` 为 `sessionStorage` 或内存变量,修改 `getToken()`/`setToken()`/`removeToken()`
|
||||
- 改登录/登出逻辑:修改 `login()`/`logout()` 方法
|
||||
- 改超时处理:修改 `fetchWithTimeout()` 函数
|
||||
|
||||
**autoInit 设计约束**:
|
||||
|
||||
- 仅读取 `window.simapi` 的三个顶级字段:`endpoints`、`defaultEndpoint`、`debug`
|
||||
- 业务回调(`businessCallback`/`responseCallback`)不支持从 window 读取,必须在代码中通过 `setBusinessCallback` 注册
|
||||
|
||||
---
|
||||
|
||||
## Pinia Store(simapi.pinia.ts)
|
||||
|
||||
**职责**:Vue3 适配层,SimApiCore 的纯代理,不维护任何独立状态。
|
||||
|
||||
**关键设计**:
|
||||
|
||||
- **无独立状态**:state 中只有一个 `_core` 实例,不维护 `debug`、`versions`、`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 vite.core.config.ts 输出 dist/index.mjs / index.cjs
|
||||
→ vite build vite.pinia.config.ts 输出 dist/pinia.mjs
|
||||
→ tsc -p tsconfig.build.json 输出 *.d.ts 类型声明
|
||||
```
|
||||
|
||||
**dist 输出是平铺的**,core 和 pinia 的编译产物全部在同一目录:
|
||||
|
||||
```
|
||||
dist/
|
||||
├── index.mjs # core ESM (5.59 KB)
|
||||
├── index.cjs # core CJS (4.15 KB)
|
||||
├── pinia.mjs # pinia ESM (6.81 KB)
|
||||
├── simapi.core.d.ts
|
||||
├── simapi.pinia.d.ts
|
||||
└── types.d.ts
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## package.json exports
|
||||
|
||||
```json
|
||||
".": { "import": "./dist/index.mjs", "require": "./dist/index.cjs", "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**:所有请求不发送 Cookie,Token 通过请求头传递
|
||||
- **零依赖**:不需要安装 axios,使用原生 fetch
|
||||
- 删除了 Angular 支持,如需恢复参考 git 历史
|
||||
@@ -1,267 +1,561 @@
|
||||
# @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 存储在 localStorage,key 默认为 `simapi-auth-token`
|
||||
- **不使用 Cookie / 不使用 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
|
||||
npm install @simcu/simapi
|
||||
npm install @simcu/simapi pinia
|
||||
```
|
||||
|
||||
## 架构
|
||||
> `pinia` 是 peerDependency,Vue3 项目必须安装。
|
||||
|
||||
```
|
||||
src/
|
||||
├── types.ts # 类型定义(SimApiBaseResponse、SimApiOptions 等)
|
||||
├── simapi.core.ts # 纯 TS 核心,无框架依赖
|
||||
└── simapi.pinia.ts # Vue3 Pinia Store 封装
|
||||
```
|
||||
### 3.2 第一步:public/config.js — 外部配置文件
|
||||
|
||||
## 核心层(框架无关)
|
||||
在项目的 `public/` 目录下创建 `config.js`,定义 API 地址:
|
||||
|
||||
适用于浏览器、Node.js、小程序等任意环境。
|
||||
|
||||
```typescript
|
||||
import { SimApiCore } from '@simcu/simapi'
|
||||
|
||||
const api = new SimApiCore()
|
||||
|
||||
// 从 window.simapi 读取配置(可选)
|
||||
api.autoInit()
|
||||
|
||||
// 或手动配置
|
||||
api.configure({
|
||||
api: { endpoints: { default: 'https://api.example.com' } },
|
||||
```javascript
|
||||
// public/config.js
|
||||
window.simapi = {
|
||||
debug: true,
|
||||
})
|
||||
|
||||
// 发起请求
|
||||
const res = await api.query('/users/list', { page: 1 })
|
||||
// res.code === 200,res.data 为业务数据
|
||||
|
||||
// 登录
|
||||
await api.login({ phone: '13800138000', code: '123456' })
|
||||
|
||||
// 登出
|
||||
await api.logout()
|
||||
|
||||
// 注册业务错误码回调
|
||||
api.setBusinessCallback(401, () => router.push('/login'))
|
||||
api.setBusinessCallback('common', (data) => alert(data.message))
|
||||
endpoints: {
|
||||
default: "http://127.0.0.1:5210" // 后端 simapi-net 服务地址
|
||||
// default: "https://api.example.com" // 生产环境
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Vue3
|
||||
**为什么用 config.js 而不是硬编码?**
|
||||
- 前后端分离部署时,API 地址可能变化
|
||||
- `public/` 下的文件 Vite 会直接复制到输出目录,不经过构建
|
||||
- 打包后运维人员可以直接修改 `config.js` 切换环境,无需重新构建
|
||||
|
||||
安装 Pinia:
|
||||
**config.js 支持的字段:**
|
||||
|
||||
```bash
|
||||
npm install pinia
|
||||
| 字段 | 类型 | 说明 | 默认值 |
|
||||
|------|------|------|--------|
|
||||
| `debug` | boolean | 是否打印请求/响应日志 | `false` |
|
||||
| `endpoints` | object | 多端点地址映射 | `{ default: '' }` |
|
||||
| `defaultEndpoint` | string | 默认使用的端点名称 | `'default'` |
|
||||
|
||||
### 3.3 第二步:index.html — 引入 config.js
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>My App</title>
|
||||
<!-- ⬇️ 在这里引入 config.js,必须在 main.ts 之前加载 -->
|
||||
<script src="/config.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
> `<script src="/config.js">` 不加 `type="module"`,确保它在所有模块之前同步执行,设置好 `window.simapi`。
|
||||
|
||||
### 3.4 第三步:main.ts — 创建 Pinia 实例
|
||||
|
||||
```typescript
|
||||
// main.ts
|
||||
// src/main.ts
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import { createPinia } from 'pinia' // ← 必须安装并注册 Pinia
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
|
||||
const app = createApp(App)
|
||||
app.use(createPinia())
|
||||
app.use(createPinia()) // ← useSimApi 依赖 Pinia,必须先注册
|
||||
app.use(router)
|
||||
// app.use(其他插件)
|
||||
app.mount('#app')
|
||||
```
|
||||
|
||||
```typescript
|
||||
// 任意组件
|
||||
import { useSimApi } from '@simcu/simapi'
|
||||
**顺序很重要**: `createPinia()` 必须在 `useSimApi()` 调用之前完成注册。
|
||||
|
||||
### 3.5 第四步: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'
|
||||
|
||||
// 获取 api 实例(单例,全局共享同一状态)
|
||||
const api = useSimApi()
|
||||
const router = useRouter()
|
||||
|
||||
// 响应式
|
||||
console.log(api.token) // 当前 Token
|
||||
console.log(api.isLoggedIn) // 是否已登录
|
||||
onMounted(() => {
|
||||
// ① 从 window.simapi 读取配置(endpoints、debug 等)
|
||||
api.autoInit()
|
||||
|
||||
// 发起请求
|
||||
const res = await api.query('/users/list', { page: 1 })
|
||||
console.log(res.data)
|
||||
|
||||
// 登录 / 登出
|
||||
await api.login({ phone: '13800138000', code: '123456' })
|
||||
await api.logout()
|
||||
|
||||
// 调试模式
|
||||
api.setDebug(true)
|
||||
```
|
||||
|
||||
### 多端点
|
||||
|
||||
```typescript
|
||||
api.setEndpoints({
|
||||
default: 'https://api.example.com',
|
||||
admin: 'https://admin.example.com',
|
||||
// ② 注册业务错误码回调 —— 401 时跳转登录页
|
||||
api.setBusinessCallback(401, () => {
|
||||
api.logout()
|
||||
router.replace({ path: '/login' })
|
||||
})
|
||||
|
||||
// 指定端点发请求
|
||||
await api.query('/stats', {}, 'admin')
|
||||
// ③ 注册通用兜底回调 —— 其他所有非 200 错误统一提示
|
||||
api.setBusinessCallback('common', (data: any) => {
|
||||
console.error('[SimApi]', data.code, data.message)
|
||||
// 如果用了 UI 组件库可以在这里弹提示:
|
||||
// MessagePlugin.error({ content: data.message })
|
||||
})
|
||||
})
|
||||
</script>
|
||||
```
|
||||
|
||||
## autoInit — 从 window 读取配置
|
||||
**关键点说明:**
|
||||
|
||||
仅支持三个字段:`endpoints`、`defaultEndpoint`、`debug`。业务回调需在代码中通过 `setBusinessCallback` 处理。
|
||||
| 要点 | 说明 |
|
||||
|------|------|
|
||||
| `import from '@simcu/simapi/pinia'` | Vue3 项目**必须**用 `/pinia` 子路径导入 |
|
||||
| `onMounted` 中初始化 | 确保 DOM 已加载、config.js 已执行、Pinia 已就绪 |
|
||||
| `autoInit()` | 读取 `window.simapi` 的 `endpoints`、`defaultEndpoint`、`debug` |
|
||||
| `setBusinessCallback(401, fn)` | 当后端返回 code=401 时自动执行 |
|
||||
| `setBusinessCallback('common', fn)` | 兜底回调,任何非 200 且未匹配其他回调时触发 |
|
||||
|
||||
```html
|
||||
<script>
|
||||
window.simapi = {
|
||||
endpoints: {
|
||||
default: 'https://api.example.com',
|
||||
admin: 'https://admin.example.com',
|
||||
},
|
||||
defaultEndpoint: 'default',
|
||||
debug: false,
|
||||
### 3.6 第五步:在组件中使用
|
||||
|
||||
```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
|
||||
}
|
||||
|
||||
const api = useSimApi()
|
||||
const users = ref<User[]>([])
|
||||
|
||||
async function loadUsers() {
|
||||
// query 返回 Promise<SimApiBaseResponse<T>>,code !== 200 时会抛出异常
|
||||
const res = await api.query<User[]>('/user/list', { page: 1 })
|
||||
users.value = res.data ?? []
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
初始化时手动调用:
|
||||
---
|
||||
|
||||
```typescript
|
||||
// 方式一:从 window.simapi 读取
|
||||
const api = useSimApi()
|
||||
api.autoInit()
|
||||
## 4. 完整项目模板(可直接复制使用)
|
||||
|
||||
// 方式二:直接传入配置
|
||||
const api = useSimApi()
|
||||
api.configure({
|
||||
api: { endpoints: { default: 'https://api.example.com' } },
|
||||
})
|
||||
以下是一个完整的 Vue3 + simapi-vue 项目初始化清单:
|
||||
|
||||
### 文件清单
|
||||
|
||||
```
|
||||
my-project/
|
||||
├── public/
|
||||
│ └── config.js # ← API 配置(第 1 步)
|
||||
├── index.html # ← 引入 config.js(第 2 步)
|
||||
├── src/
|
||||
│ ├── main.ts # ← 注册 Pinia(第 3 步)
|
||||
│ ├── App.vue # ← 初始化 SimApi(第 4 步)
|
||||
│ ├── router/
|
||||
│ │ └── index.ts
|
||||
│ └── views/
|
||||
│ └── Login.vue # ← 使用示例
|
||||
├── package.json # ← 依赖
|
||||
└── vite.config.ts
|
||||
```
|
||||
|
||||
## SimApiBaseResponse 响应格式
|
||||
### public/config.js
|
||||
|
||||
```typescript
|
||||
interface SimApiBaseResponse<T = any> {
|
||||
code: number // 200 = 成功,其他为业务错误码
|
||||
message: string // 提示信息
|
||||
data?: T // 业务数据
|
||||
```javascript
|
||||
window.simapi = {
|
||||
debug: true,
|
||||
endpoints: {
|
||||
default: "http://localhost:5000"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 完整配置参考
|
||||
### index.html
|
||||
|
||||
### SimApiOptions — 完整配置结构
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>My App</title>
|
||||
<script src="/config.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
### src/main.ts
|
||||
|
||||
```typescript
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
|
||||
createApp(App)
|
||||
.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(() => {
|
||||
api.autoInit()
|
||||
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('/') // 登录成功自动存 Token,然后跳转首页
|
||||
}
|
||||
</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)
|
||||
|
||||
| 方法 | 参数 | 返回值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| `autoInit()` | 无 | `void` | 从 `window.simapi` 读取 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[] 数组
|
||||
|
||||
// 带参数
|
||||
const res = await api.query<User>('/user/detail', { id: 'xxx' })
|
||||
|
||||
// 指定端点
|
||||
await api.query('/admin/stats', {}, 'admin')
|
||||
|
||||
// 自定义请求头
|
||||
await api.query('/file/upload', formData, undefined, { 'Content-Type': 'multipart/form-data' })
|
||||
|
||||
// 错误处理
|
||||
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) // 错误消息
|
||||
console.log(err.data) // 可能携带的错误详情
|
||||
}
|
||||
```
|
||||
|
||||
**⚠️ query 的行为要点:**
|
||||
|
||||
1. 自动在请求头添加 `Token`(如果存在)
|
||||
2. `code === 200` → 正常返回 `SimApiBaseResponse<T>`
|
||||
3. `code !== 200` → 先执行对应的 businessCallback,然后 **throw 异常**
|
||||
4. 网络错误 → 执行 responseCallback.error,throw 包装后的 `{code: -1}` 异常
|
||||
5. 所以调用方只需 `try/catch` 处理异常即可
|
||||
|
||||
### 5.4 login / logout 方法
|
||||
|
||||
```typescript
|
||||
// login:POST 到 auth.login_url(默认 /auth/login),自动保存返回的 Token
|
||||
await api.login({ phone: '13800138000', code: '123456' })
|
||||
// 后端返回 { code: 200, data: "token-string" }
|
||||
// 前端自动将 data 存入 localStorage
|
||||
|
||||
// logout:清除本地 Token,可选调后端登出接口
|
||||
await api.logout() // 调 /auth/logout
|
||||
await api.logout(null) // 只清本地 Token,不调后端
|
||||
```
|
||||
|
||||
### 5.5 setBusinessCallback — 业务错误处理
|
||||
|
||||
```typescript
|
||||
// 针对特定错误码
|
||||
api.setBusinessCallback(401, (data) => {
|
||||
console.log('未授权', data.message)
|
||||
router.replace('/login')
|
||||
})
|
||||
|
||||
api.setBusinessCallback(403, (data) => {
|
||||
alert('无权限:' + data.message)
|
||||
})
|
||||
|
||||
// 兜底:任何未单独处理的非 200 错误都会走 common
|
||||
api.setBusinessCallback('common', (data) => {
|
||||
console.error('请求失败:', data.code, data.message)
|
||||
})
|
||||
```
|
||||
|
||||
**回调执行顺序:** 匹配具体错误码 → 未匹配则走 `'common'` → 然后 throw
|
||||
|
||||
---
|
||||
|
||||
## 6. 类型定义速查
|
||||
|
||||
```typescript
|
||||
/** 标准响应 */
|
||||
interface SimApiBaseResponse<T = any> {
|
||||
code: number // 200=成功,其他=业务错误码
|
||||
message: string // 提示信息
|
||||
data?: T // 业务数据
|
||||
}
|
||||
|
||||
/** 版本信息 */
|
||||
interface SimApiVersions {
|
||||
uiApp: string // 前端应用版本
|
||||
uiSimApi: string // 前端 SimApi 版本
|
||||
apiApp: string // 后端应用版本(简化版)
|
||||
apiSimApi: string // 后端 SimApi 版本(简化版)
|
||||
apiAppFull: string // 后端应用版本(完整版)
|
||||
apiSimApiFull: string // 后端 SimApi 版本(完整版)
|
||||
}
|
||||
|
||||
/** 认证配置 */
|
||||
interface SimApiAuthConfig {
|
||||
token_name: string // localStorage key,默认 'simapi-auth-token'
|
||||
check_url: string // 登录检查接口,默认 '/auth/check'
|
||||
logout_url: string // 登出接口,默认 '/auth/logout'
|
||||
login_url: string // 登录接口,默认 '/auth/login'
|
||||
}
|
||||
|
||||
/** API 配置 */
|
||||
interface SimApiApiConfig {
|
||||
endpoints: { [name]: string } // 多端点映射
|
||||
defaultEndpoint: string // 默认端点
|
||||
businessCallback: SimApiBusinessCallback // 错误码回调
|
||||
responseCallback: SimApiResponseCallback // 响应拦截器
|
||||
timeout?: number // 超时毫秒数,默认 10000
|
||||
}
|
||||
|
||||
/** 完整选项 */
|
||||
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'
|
||||
## 7. 多端点支持
|
||||
|
||||
/** 检查登录状态的接口路径 */
|
||||
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'), // 未授权
|
||||
403: (data) => ElMessage.error('无权限'), // 无权限
|
||||
500: (data) => console.error(data), // 服务器错误
|
||||
'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
|
||||
```javascript
|
||||
// config.js
|
||||
window.simapi = {
|
||||
debug: true,
|
||||
endpoints: {
|
||||
default: 'https://api.example.com', // 主服务
|
||||
admin: 'https://admin.example.com', // 管理后台服务
|
||||
cdn: 'https://cdn.example.com', // CDN/文件服务
|
||||
},
|
||||
error: (err) => {
|
||||
console.error('请求失败', err)
|
||||
throw err
|
||||
}
|
||||
defaultEndpoint: 'default'
|
||||
}
|
||||
```
|
||||
|
||||
### SimApiVersions — 版本信息
|
||||
|
||||
```typescript
|
||||
interface SimApiVersions {
|
||||
uiApp: string // 前端应用版本
|
||||
uiSimApi: string // 前端 SimApi 版本
|
||||
apiApp: string // 后端应用版本(简化版,如 "1.2.3")
|
||||
apiSimApi: string // 后端 SimApi 版本(简化版)
|
||||
apiAppFull: string // 后端应用版本(完整版,如 "1.2.3+20240101")
|
||||
apiSimApiFull: string // 后端 SimApi 版本(完整版)
|
||||
}
|
||||
// 使用默认端点
|
||||
await api.query('/user/list') // → https://api.example.com/user/list
|
||||
|
||||
// 指定端点
|
||||
await api.query('/system/stats', {}, 'admin') // → https://admin.example.com/system/stats
|
||||
```
|
||||
|
||||
### 完整配置示例
|
||||
也可以运行时动态添加:
|
||||
|
||||
```typescript
|
||||
api.setEndpoints({ backup: 'https://backup-api.example.com' })
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. configure — 手动完整配置
|
||||
|
||||
除了 autoInit 从 `window.simapi` 读取外,也可以手动配置一切:
|
||||
|
||||
```typescript
|
||||
api.configure({
|
||||
@@ -269,83 +563,190 @@ api.configure({
|
||||
|
||||
auth: {
|
||||
token_name: 'my-app-token',
|
||||
check_url: '/api/auth/check',
|
||||
logout_url: '/api/auth/logout',
|
||||
login_url: '/api/auth/login',
|
||||
check_url: '/auth/check',
|
||||
logout_url: '/auth/logout',
|
||||
login_url: '/auth/login',
|
||||
},
|
||||
|
||||
api: {
|
||||
endpoints: {
|
||||
default: 'https://api.example.com',
|
||||
admin: 'https://admin.example.com',
|
||||
},
|
||||
defaultEndpoint: 'default',
|
||||
timeout: 15000,
|
||||
|
||||
businessCallback: {
|
||||
401: () => router.push('/login'),
|
||||
403: () => ElMessage.error('无权限访问'),
|
||||
500: (data) => console.error('服务器错误:', data.message),
|
||||
'common': (data) => ElMessage.error(data.message || '请求失败'),
|
||||
401: () => router.replace('/login'),
|
||||
403: (data) => alert('无权限'),
|
||||
500: (data) => console.error('服务器错误', data),
|
||||
'common': (data) => MessagePlugin.error(data.message),
|
||||
},
|
||||
|
||||
responseCallback: {
|
||||
success: (res) => res.data ?? res,
|
||||
error: (err) => {
|
||||
console.error('网络错误', err)
|
||||
},
|
||||
success: (res) => res, // 成功响应拦截(可做数据转换)
|
||||
error: (err) => console.error('网络错误', err),
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## API 参考
|
||||
**configure vs autoInit 的关系:**
|
||||
- `autoInit()` 只读 `window.simapi` 的 `endpoints`、`defaultEndpoint`、`debug`
|
||||
- `configure()` 可以覆盖所有字段,包括 auth 和 callbacks
|
||||
- 通常做法是:`autoInit()` 读基础配置 + `setBusinessCallback()` 补充回调
|
||||
|
||||
### SimApiCore(核心类)
|
||||
---
|
||||
|
||||
| 方法/属性 | 说明 |
|
||||
|-----------|------|
|
||||
| `configure(options)` | 批量配置(深合并) |
|
||||
| `autoInit()` | 从 `window.simapi` 读取配置(endpoints、defaultEndpoint、debug) |
|
||||
| `setEndpoints(map)` | 设置端点,自动触发版本检查 |
|
||||
| `setBusinessCallback(code, fn)` | 注册业务错误码回调 |
|
||||
| `setDebug(debug)` | 设置调试模式 |
|
||||
| `query(uri, params?, endpointKey?, headers?)` | POST 请求,返回 `Promise<SimApiBaseResponse<T>>` |
|
||||
| `login(request)` | 登录,自动存 Token |
|
||||
| `logout(url?)` | 登出,清除 Token |
|
||||
| `checkLogin(url?)` | 主动检查登录状态 |
|
||||
| `getToken()` | 获取 Token |
|
||||
| `setToken(token)` | 手动设置 Token |
|
||||
| `removeToken()` | 清除 Token |
|
||||
| `isLoggedIn` | getter,是否已登录 |
|
||||
| `token` | getter,获取当前 Token |
|
||||
| `debug` | boolean,调试模式 |
|
||||
| `versions` | 版本信息对象 |
|
||||
## 9. GOTCHAS(AI 最容易犯的错)
|
||||
|
||||
## 构建
|
||||
| ❌ 错误 | ✅ 正确 |
|
||||
|---------|---------|
|
||||
| `import { useSimApi } from '@simcu/simapi'` (Vue3) | `from '@simcu/simapi/pinia'`(必须带 `/pinia`) |
|
||||
| 忘记 `app.use(createPinia())` | **必须在 `useSimApi()` 之前**注册 Pinia |
|
||||
| `config.js` 放在 `src/` 下 | 必须放在 `public/` 下,Vite 才能原样复制 |
|
||||
| `index.html` 中不引 config.js 或放在 main.ts 之后 | config.js 必须**同步加载**且在模块代码之前执行 |
|
||||
| 用 `Authorization: Bearer xxx` | 用 `Token` 请求头(这是 simapi-net 约定) |
|
||||
| 期望 HTTP 4xx/5xx 表示错误 | 所有错误都是 **HTTP 200 + JSON `code` 字段** |
|
||||
| `res.data` 直接用而不判空 | `res.data` 可能是 `undefined`,用 `res.data ?? []` 或 `res.data!` |
|
||||
| 在 setup 外部调用 `useSimApi()` | `useSimApi()` 只能在 **setup 上下文**(或 pinia active)中调用 |
|
||||
| 用 Cookie 传 Token | Token 通过 **请求头 `Token`** + **localStorage** 存储 |
|
||||
| `new SimApiCore()` 在 Vue 项目里用 | Vue 项目统一用 `useSimApi()` Pinia Store |
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run build
|
||||
npm link # 本地调试
|
||||
---
|
||||
|
||||
## 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 已内置对应方法:
|
||||
|
||||
| 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`(默认开启) |
|
||||
|
||||
### 10.3 登录流程示例
|
||||
|
||||
```
|
||||
用户输入手机号+验证码
|
||||
↓
|
||||
前端: api.login({ phone: '138xx', code: '123456' })
|
||||
↓ POST /auth/login { phone, code }
|
||||
后端: 验证通过 → 返回 { code: 200, data: "token-string" }
|
||||
↓
|
||||
前端: 自动将 token 存入 localStorage['simapi-auth-token']
|
||||
↓
|
||||
后续请求: api.query('/user/info')
|
||||
↓ 自动带上 Header: Token: token-string
|
||||
后端: SimApiAuthMiddleware 解析 Token → HttpContext.Items["LoginInfo"]
|
||||
↓ Controller 可通过 LoginInfo 获取当前用户
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 从旧版迁移
|
||||
## 11. 构建
|
||||
|
||||
如果你之前使用的是带 axios 的版本,迁移非常简单:
|
||||
```bash
|
||||
# 开发模式(监听文件变化)
|
||||
npm run dev
|
||||
|
||||
# 生产构建
|
||||
npm run build
|
||||
|
||||
# 本地 link 调试
|
||||
npm link
|
||||
cd ../your-project
|
||||
npm link @simcu/simapi
|
||||
```
|
||||
|
||||
构建产物位于 `dist/` 目录:
|
||||
- `dist/index.mjs` — 核心(SimApiCore)ESM 入口
|
||||
- `dist/pinia.mjs` — Pinia Store ESM 入口
|
||||
- `dist/*.d.ts` — TypeScript 类型声明
|
||||
|
||||
### 版本号注入
|
||||
|
||||
库使用 `declare const` 声明版本常量,构建时通过 Vite 的 `define` 注入。
|
||||
|
||||
**注意:**
|
||||
- **SimApiVersion**:由 simapi 库自身构建时注入
|
||||
- **AppVersion**:由**调用方项目**在自己的 `vite.config.ts` 中注入
|
||||
|
||||
#### simapi 库自身构建 — 注入 SimApiVersion
|
||||
|
||||
在 simapi-vue 的 `vite.config.ts` 中,从自身 `package.json` 读取版本号:
|
||||
|
||||
```typescript
|
||||
import { readFileSync } from 'node:fs'
|
||||
|
||||
const pkg = JSON.parse(readFileSync('./package.json', 'utf-8'))
|
||||
|
||||
export default defineConfig({
|
||||
// ... 其他配置
|
||||
define: {
|
||||
SimApiVersion: JSON.stringify(pkg.version), // ← 取 simapi-vue 自身的 version
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
未配置时默认为 `0.0.0-develop`。
|
||||
|
||||
#### 调用方项目 — 注入 AppVersion(推荐方式:从 package.json 自动读取)
|
||||
|
||||
在调用方项目的 `vite.config.ts` 中添加:
|
||||
|
||||
```typescript
|
||||
import { readFileSync } from 'node:fs'
|
||||
|
||||
const pkg = JSON.parse(readFileSync('./package.json', 'utf-8'))
|
||||
|
||||
export default defineConfig({
|
||||
// ... 其他配置
|
||||
define: {
|
||||
AppVersion: JSON.stringify(pkg.version), // ← 自动取 package.json 的 version 字段
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
这样每次发版只需改 `package.json` 的 `version`,无需同步修改其他地方。
|
||||
|
||||
---
|
||||
|
||||
## 12. 从 axios 迁移
|
||||
|
||||
如果你之前用的是 axios:
|
||||
|
||||
```diff
|
||||
- import axios from 'axios'
|
||||
+ import { SimApiCore } from '@simcu/simapi'
|
||||
- // ... 你的 axios 配置
|
||||
- const res = await axios.post('/user/list', { page: 1 })
|
||||
- console.log(res.data)
|
||||
|
||||
+ const api = new SimApiCore()
|
||||
+ api.setEndpoints({ default: 'https://api.example.com' })
|
||||
+ const res = await api.query('/users/list', { page: 1 })
|
||||
+ import { useSimApi } from '@simcu/simapi/pinia'
|
||||
+ const api = useSimApi()
|
||||
+ const res = await api.query('/user/list', { page: 1 })
|
||||
+ console.log(res.data) // res.data 就是业务数据
|
||||
```
|
||||
|
||||
API 完全兼容,无需其他改动。主要变化:
|
||||
- 使用原生 fetch 替代 axios
|
||||
- Token 通过请求头 `Token` 传递(不是 `Authorization: Bearer`)
|
||||
- 默认不发送 Cookie,避免 CORS 问题
|
||||
主要区别:
|
||||
|
||||
| axios | simapi-vue |
|
||||
|-------|-----------|
|
||||
| `axios.post()` | `api.query()` |
|
||||
| `response.data` 直接是业务数据 | `SimApiBaseResponse<T>.data` 是业务数据 |
|
||||
| HTTP 4xx/5xx 表示错误 | HTTP 200 + `code` 字段表示错误 |
|
||||
| `interceptors.response` | `businessCallback` + `responseCallback` |
|
||||
| Authorization Bearer | Token header |
|
||||
|
||||
Generated
+324
-13
@@ -1,18 +1,19 @@
|
||||
{
|
||||
"name": "@simcu/simapi",
|
||||
"version": "0.0.0-version-placeholder",
|
||||
"version": "0.0.0-develop",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@simcu/simapi",
|
||||
"version": "0.0.0-version-placeholder",
|
||||
"version": "0.0.0-develop",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.0.0",
|
||||
"@types/node": "^25.6.0",
|
||||
"concurrently": "^9.2.1",
|
||||
"pinia": "^2.2.0",
|
||||
"typescript": "~5.9.3",
|
||||
"vite": "^5.0.0",
|
||||
@@ -433,18 +434,14 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@vitejs/plugin-vue": {
|
||||
"version": "5.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz",
|
||||
"integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==",
|
||||
"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",
|
||||
"engines": {
|
||||
"node": "^18.0.0 || >=20.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vite": "^5.0.0 || ^6.0.0",
|
||||
"vue": "^3.2.25"
|
||||
"dependencies": {
|
||||
"undici-types": "~7.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-core": {
|
||||
@@ -563,6 +560,122 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/chalk/node_modules/supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-flag": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
|
||||
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"string-width": "^4.2.0",
|
||||
"strip-ansi": "^6.0.1",
|
||||
"wrap-ansi": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-name": "~1.1.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/concurrently": {
|
||||
"version": "9.2.1",
|
||||
"resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz",
|
||||
"integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chalk": "4.1.2",
|
||||
"rxjs": "7.8.2",
|
||||
"shell-quote": "1.8.3",
|
||||
"supports-color": "8.1.1",
|
||||
"tree-kill": "1.2.2",
|
||||
"yargs": "17.7.2"
|
||||
},
|
||||
"bin": {
|
||||
"conc": "dist/bin/concurrently.js",
|
||||
"concurrently": "dist/bin/concurrently.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/open-cli-tools/concurrently?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||
@@ -570,6 +683,13 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/entities": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
|
||||
@@ -583,6 +703,16 @@
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/escalade": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
||||
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/estree-walker": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
|
||||
@@ -605,6 +735,36 @@
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/get-caller-file": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "6.* || 8.* || >= 10.*"
|
||||
}
|
||||
},
|
||||
"node_modules/has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/is-fullwidth-code-point": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/magic-string": {
|
||||
"version": "0.30.21",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
||||
@@ -693,6 +853,16 @@
|
||||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/require-directory": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "4.60.1",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz",
|
||||
@@ -738,6 +908,29 @@
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/rxjs": {
|
||||
"version": "7.8.2",
|
||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
|
||||
"integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/shell-quote": {
|
||||
"version": "1.8.3",
|
||||
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz",
|
||||
"integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
@@ -748,6 +941,60 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/supports-color": {
|
||||
"version": "8.1.1",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
|
||||
"integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-flag": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/supports-color?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/tree-kill": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
|
||||
"integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"tree-kill": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
@@ -768,6 +1015,13 @@
|
||||
"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": {
|
||||
"version": "5.4.21",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
|
||||
@@ -1306,6 +1560,63 @@
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"string-width": "^4.1.0",
|
||||
"strip-ansi": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/y18n": {
|
||||
"version": "5.0.8",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
||||
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs": {
|
||||
"version": "17.7.2",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
|
||||
"integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cliui": "^8.0.1",
|
||||
"escalade": "^3.1.1",
|
||||
"get-caller-file": "^2.0.5",
|
||||
"require-directory": "^2.1.1",
|
||||
"string-width": "^4.2.3",
|
||||
"y18n": "^5.0.5",
|
||||
"yargs-parser": "^21.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs-parser": {
|
||||
"version": "21.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
|
||||
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+6
-12
@@ -1,32 +1,28 @@
|
||||
{
|
||||
"name": "@simcu/simapi",
|
||||
"version": "0.0.0-version-placeholder",
|
||||
"version": "0.0.0-develop",
|
||||
"description": "SimApi 统一前端 HTTP 客户端库,支持 Vue3 和常规 JS/TS 项目(基于原生 fetch)",
|
||||
"author": "simcu",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git@github.com:simcu/simapi-ts.git"
|
||||
"url": "git@github.com:simcu/simapi-vue.git"
|
||||
},
|
||||
"keywords": [
|
||||
"simapi",
|
||||
"vue3",
|
||||
"pinia",
|
||||
"rxjs",
|
||||
"http",
|
||||
"fetch"
|
||||
],
|
||||
"files": [
|
||||
"dist",
|
||||
"simapi.core.ts",
|
||||
"simapi.pinia.ts",
|
||||
"README.md",
|
||||
"package.json"
|
||||
],
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/index.mjs",
|
||||
"require": "./dist/index.cjs",
|
||||
"types": "./dist/simapi.core.d.ts"
|
||||
},
|
||||
"./pinia": {
|
||||
@@ -34,14 +30,11 @@
|
||||
"types": "./dist/simapi.pinia.d.ts"
|
||||
}
|
||||
},
|
||||
"main": "./dist/index.cjs",
|
||||
"module": "./dist/index.mjs",
|
||||
"types": "./dist/simapi.core.d.ts",
|
||||
"scripts": {
|
||||
"build": "npm run build:core && npm run build:pinia && npm run types",
|
||||
"build:core": "vite build --config vite.core.config.ts",
|
||||
"build:pinia": "vite build --config vite.pinia.config.ts",
|
||||
"dev": "vite build --config vite.core.config.ts --watch",
|
||||
"build": "vite build && npm run types",
|
||||
"dev": "vite build --watch",
|
||||
"types": "tsc --declaration --emitDeclarationOnly --project tsconfig.build.json",
|
||||
"lint": "tsc --noEmit"
|
||||
},
|
||||
@@ -49,7 +42,8 @@
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.0.0",
|
||||
"@types/node": "^25.6.0",
|
||||
"concurrently": "^9.2.1",
|
||||
"pinia": "^2.2.0",
|
||||
"typescript": "~5.9.3",
|
||||
"vite": "^5.0.0",
|
||||
|
||||
+64
-47
@@ -11,8 +11,6 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
AppVersion,
|
||||
SimApiVersion,
|
||||
type SimApiVersions,
|
||||
type SimApiAuthConfig,
|
||||
type SimApiApiConfig,
|
||||
@@ -20,7 +18,6 @@ import {
|
||||
type SimApiBaseResponse,
|
||||
} from './types'
|
||||
|
||||
export { SimApiVersion, AppVersion } from './types'
|
||||
export type {
|
||||
SimApiVersions,
|
||||
SimApiAuthConfig,
|
||||
@@ -29,6 +26,9 @@ export type {
|
||||
SimApiBaseResponse,
|
||||
} from './types'
|
||||
|
||||
declare const SimApiVersion: string;
|
||||
declare const AppVersion: string;
|
||||
|
||||
// ── Helper: Fetch with Timeout ────────────────────────────────────────
|
||||
|
||||
function fetchWithTimeout(
|
||||
@@ -77,26 +77,26 @@ async function fetchPost<T = any>(
|
||||
// ── SimApiCore ────────────────────────────────────────
|
||||
|
||||
export class SimApiCore {
|
||||
debug: boolean = true
|
||||
uiAppVersion?: string
|
||||
|
||||
auth: SimApiAuthConfig = {
|
||||
private debug: boolean = true
|
||||
private auth: SimApiAuthConfig = {
|
||||
token_name: 'simapi-auth-token',
|
||||
check_url: '/auth/check',
|
||||
logout_url: '/auth/logout',
|
||||
login_url: '/auth/login',
|
||||
}
|
||||
|
||||
api: SimApiApiConfig = {
|
||||
private api: SimApiApiConfig = {
|
||||
endpoints: {default: ''},
|
||||
defaultEndpoint: 'default',
|
||||
businessCallback: {
|
||||
401: () => localStorage.removeItem(this.auth.token_name),
|
||||
common: () => {},
|
||||
401: () => this.removeToken(),
|
||||
common: () => {
|
||||
},
|
||||
},
|
||||
responseCallback: {
|
||||
success: (response: any) => response,
|
||||
error: (_err: any) => {},
|
||||
error: (_err: any) => {
|
||||
},
|
||||
},
|
||||
timeout: 10000,
|
||||
}
|
||||
@@ -110,7 +110,7 @@ export class SimApiCore {
|
||||
/**
|
||||
* 从 window.simapi 读取配置并初始化
|
||||
*
|
||||
* 支持字段:endpoints, defaultEndpoint, debug, uiAppVersion
|
||||
* 支持字段:endpoints, defaultEndpoint, debug
|
||||
* 业务回调(businessCallback / responseCallback)需在代码中处理
|
||||
*/
|
||||
autoInit(): void {
|
||||
@@ -120,9 +120,6 @@ export class SimApiCore {
|
||||
if (config.debug !== undefined) {
|
||||
this.debug = config.debug
|
||||
}
|
||||
if (config.uiAppVersion !== undefined) {
|
||||
this.uiAppVersion = config.uiAppVersion
|
||||
}
|
||||
if (config.endpoints) {
|
||||
this.api.endpoints = {...this.api.endpoints, ...config.endpoints}
|
||||
}
|
||||
@@ -135,9 +132,6 @@ export class SimApiCore {
|
||||
if (options.debug !== undefined) {
|
||||
this.debug = options.debug
|
||||
}
|
||||
if (options.uiAppVersion !== undefined) {
|
||||
this.uiAppVersion = options.uiAppVersion
|
||||
}
|
||||
if (options.auth) {
|
||||
this.auth = {...this.auth, ...options.auth}
|
||||
}
|
||||
@@ -152,6 +146,14 @@ export class SimApiCore {
|
||||
}
|
||||
}
|
||||
|
||||
get isDebug() {
|
||||
return this.debug
|
||||
}
|
||||
|
||||
setDebug(debug: boolean) {
|
||||
this.debug = debug;
|
||||
}
|
||||
|
||||
setEndpoints(endpoints: { [name: string]: string }): void {
|
||||
this.api.endpoints = {...this.api.endpoints, ...endpoints}
|
||||
}
|
||||
@@ -165,20 +167,21 @@ export class SimApiCore {
|
||||
}
|
||||
|
||||
getToken(): string {
|
||||
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 {
|
||||
localStorage.setItem(this.auth.token_name, token)
|
||||
const name = this.auth.token_name
|
||||
document.cookie = `${name}=${token}; path=/; secure; samesite=none`
|
||||
}
|
||||
|
||||
removeToken(): void {
|
||||
localStorage.removeItem(this.auth.token_name)
|
||||
const name = this.auth.token_name
|
||||
document.cookie = `${name}=; path=/; max-age=0; secure; samesite=none`
|
||||
}
|
||||
|
||||
get isLoggedIn(): boolean {
|
||||
return !!localStorage.getItem(this.auth.token_name)
|
||||
}
|
||||
|
||||
genS4(): string {
|
||||
return (((1 + Math.random()) * 0x10000 * Date.parse(new Date().toString())) | 0)
|
||||
@@ -212,18 +215,22 @@ export class SimApiCore {
|
||||
* const versions = await api.getVersion('backup')
|
||||
*/
|
||||
async getVersion(endpointName?: string): Promise<SimApiVersions> {
|
||||
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',
|
||||
};
|
||||
try {
|
||||
const resp = await this.query<any>('/versions', {}, endpointName)
|
||||
if (resp?.data) {
|
||||
const d = resp.data
|
||||
const versions: SimApiVersions = {
|
||||
uiApp: this.uiAppVersion ?? AppVersion,
|
||||
uiSimApi: SimApiVersion,
|
||||
apiApp: d.App?.split('+')[0] ?? '0.0.0',
|
||||
apiSimApi: d.SimApi?.split('+')[0] ?? '0.0.0',
|
||||
apiAppFull: d.App ?? '0.0.0',
|
||||
apiSimApiFull: d.SimApi ?? '0.0.0',
|
||||
}
|
||||
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}`)
|
||||
}
|
||||
@@ -232,21 +239,15 @@ export class SimApiCore {
|
||||
} catch {
|
||||
// 版本获取失败返回默认值
|
||||
}
|
||||
return {
|
||||
uiApp: AppVersion,
|
||||
uiSimApi: SimApiVersion,
|
||||
apiApp: '0.0.0',
|
||||
apiSimApi: '0.0.0',
|
||||
apiAppFull: '0.0.0',
|
||||
apiSimApiFull: '0.0.0',
|
||||
}
|
||||
return versions;
|
||||
}
|
||||
|
||||
async query<T = any>(
|
||||
uri: string,
|
||||
params: any = {},
|
||||
endpointKey?: string,
|
||||
extraHeaders?: Record<string, string>
|
||||
extraHeaders?: Record<string, string>,
|
||||
selfHandleError: boolean = false
|
||||
): Promise<SimApiBaseResponse<T>> {
|
||||
const headers: Record<string, string> = {...extraHeaders, ...{}}
|
||||
const queryId = this.genS4()
|
||||
@@ -262,7 +263,7 @@ export class SimApiCore {
|
||||
|
||||
if (this.debug) {
|
||||
headers['Query-Id'] = queryId
|
||||
console.log('[REQUEST*]', queryId, '->', uri, 'AUTH:', localStorage.getItem(this.auth.token_name))
|
||||
console.log('[REQUEST*]', queryId, '->', uri, 'AUTH:', this.getToken())
|
||||
}
|
||||
|
||||
const url = this.getEndpoint(endpointKey) + uri
|
||||
@@ -280,19 +281,34 @@ export class SimApiCore {
|
||||
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 抛出错误
|
||||
// code != 200 时抛出业务错误
|
||||
if (processedData.code !== 200) {
|
||||
throw processedData
|
||||
}
|
||||
return processedData
|
||||
} catch (error) {
|
||||
|
||||
} 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
|
||||
}
|
||||
}
|
||||
@@ -306,16 +322,17 @@ export class SimApiCore {
|
||||
}
|
||||
|
||||
async logout(url?: string | null): Promise<any> {
|
||||
this.removeToken()
|
||||
if (url !== null) {
|
||||
return this.query(url ?? this.auth.logout_url).catch(() => true)
|
||||
this.query(url ?? this.auth.logout_url).catch(() => true)
|
||||
}
|
||||
this.removeToken()
|
||||
return true
|
||||
}
|
||||
|
||||
async checkLogin(url?: string | null): Promise<void> {
|
||||
if (url !== null) {
|
||||
await this.query(url ?? this.auth.check_url).catch(() => {})
|
||||
await this.query(url ?? this.auth.check_url).catch(() => {
|
||||
})
|
||||
} else if (this.getToken()) {
|
||||
this.api.businessCallback[401]?.(null)
|
||||
}
|
||||
|
||||
+5
-11
@@ -10,16 +10,9 @@ export const useSimApi = defineStore('simapi', {
|
||||
// 在 state 中实例化 core
|
||||
_core: new SimApiCore(),
|
||||
}),
|
||||
|
||||
getters: {
|
||||
// 直接映射 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,
|
||||
IsDebug: state => state._core.isDebug
|
||||
},
|
||||
|
||||
actions: {
|
||||
// 所有方法直接代理到 core
|
||||
autoInit(): void {
|
||||
@@ -31,7 +24,7 @@ export const useSimApi = defineStore('simapi', {
|
||||
},
|
||||
|
||||
setDebug(debug: boolean): void {
|
||||
this._core.debug = debug
|
||||
this._core.setDebug(debug);
|
||||
},
|
||||
|
||||
setEndpoints(endpoints: { [name: string]: string }): void {
|
||||
@@ -73,9 +66,10 @@ export const useSimApi = defineStore('simapi', {
|
||||
uri: string,
|
||||
params?: any,
|
||||
endpointKey?: string,
|
||||
extraHeaders?: Record<string, string>
|
||||
extraHeaders?: Record<string, string>,
|
||||
selfHandleError: boolean = false
|
||||
): Promise<SimApiBaseResponse<T>> {
|
||||
return this._core.query<T>(uri, params, endpointKey, extraHeaders)
|
||||
return this._core.query<T>(uri, params, endpointKey, extraHeaders, selfHandleError)
|
||||
},
|
||||
|
||||
getEndpoint(name?: string): string {
|
||||
|
||||
@@ -2,10 +2,6 @@
|
||||
* SimApi 类型定义
|
||||
*/
|
||||
|
||||
// 版本号占位符,构建时由 GitHub Action 替换
|
||||
export const SimApiVersion = '0.0.0-version-placeholder'
|
||||
export const AppVersion = '0.0.0-version-placeholder'
|
||||
|
||||
/** 版本信息 */
|
||||
export interface SimApiVersions {
|
||||
uiApp: string
|
||||
@@ -56,8 +52,6 @@ export interface SimApiApiConfig {
|
||||
/** SimApi 完整配置 */
|
||||
export interface SimApiOptions {
|
||||
debug?: boolean
|
||||
/** UI 应用版本,如果不指定则使用库内置的占位符版本 */
|
||||
uiAppVersion?: string
|
||||
auth?: Partial<SimApiAuthConfig>
|
||||
api?: Partial<SimApiApiConfig>
|
||||
}
|
||||
|
||||
+2
-1
@@ -17,5 +17,6 @@
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true
|
||||
},
|
||||
"include": ["src/types.ts", "src/simapi.core.ts", "src/simapi.pinia.ts"]
|
||||
"include": ["src/types.ts", "src/simapi.core.ts", "src/simapi.pinia.ts"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"allowJs": true,
|
||||
"checkJs": false,
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true
|
||||
},
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import { readFileSync } from 'node:fs'
|
||||
const pkg = JSON.parse(readFileSync('./package.json', 'utf-8'))
|
||||
export default defineConfig(({ mode }) => {
|
||||
return {
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
emptyOutDir: true,
|
||||
lib: {
|
||||
entry: ['src/simapi.core.ts', 'src/simapi.pinia.ts'],
|
||||
name: 'SimApi',
|
||||
formats: ['es', 'cjs'],
|
||||
fileName: (format, entryName) => {
|
||||
if (entryName === 'simapi.core') {
|
||||
return `index.${format === 'es' ? 'mjs' : 'cjs'}`
|
||||
} else if (entryName === 'simapi.pinia') {
|
||||
return `pinia.${format === 'es' ? 'mjs' : 'cjs'}`
|
||||
}
|
||||
return `${entryName}.${format === 'es' ? 'mjs' : 'cjs'}`
|
||||
}
|
||||
},
|
||||
rollupOptions: {
|
||||
external: ['vue', 'pinia'],
|
||||
output: {
|
||||
globals: {
|
||||
vue: 'Vue',
|
||||
pinia: 'Pinia'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
define: {
|
||||
SimApiVersion: JSON.stringify(pkg.version)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -1,17 +0,0 @@
|
||||
import { defineConfig } from 'vite'
|
||||
|
||||
export default defineConfig({
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
emptyOutDir: true,
|
||||
lib: {
|
||||
entry: 'src/simapi.core.ts',
|
||||
name: 'SimApiCore',
|
||||
formats: ['es', 'cjs'],
|
||||
fileName: (format) => `index.${format === 'es' ? 'mjs' : 'cjs'}`
|
||||
},
|
||||
rollupOptions: {
|
||||
// 不再需要 external,使用原生 fetch
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -1,23 +0,0 @@
|
||||
import { defineConfig } from 'vite'
|
||||
|
||||
export default defineConfig({
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
emptyOutDir: false,
|
||||
lib: {
|
||||
entry: 'src/simapi.pinia.ts',
|
||||
name: 'SimApiPinia',
|
||||
formats: ['es'],
|
||||
fileName: () => 'pinia.mjs'
|
||||
},
|
||||
rollupOptions: {
|
||||
external: ['vue', 'pinia'],
|
||||
output: {
|
||||
globals: {
|
||||
vue: 'Vue',
|
||||
pinia: 'Pinia'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user