Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c136375100 | ||
|
|
d44938e456 | ||
|
|
c100973590 | ||
|
|
957656ad5b | ||
|
|
02c9c7af3c | ||
|
|
0b4b4b0c03 | ||
|
|
f89b14ea7d |
@@ -3,60 +3,35 @@ name: Publish to npm
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- 'v*.*.*'
|
- "*"
|
||||||
workflow_dispatch:
|
permissions:
|
||||||
|
id-token: write # Required for OIDC
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
publish:
|
publish:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
environment: release # npm Trusted Publisher 环境
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
id-token: write # OIDC 认证
|
|
||||||
contents: read # 读取仓库
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- uses: actions/checkout@v6
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
- uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0 # 完整 history 用于版本分析
|
node-version: "24"
|
||||||
|
registry-url: "https://registry.npmjs.org"
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Replace version placeholder
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: '20'
|
|
||||||
registry-url: 'https://registry.npmjs.org'
|
|
||||||
|
|
||||||
# 从 tag 提取版本号
|
|
||||||
- name: Get version from tag
|
|
||||||
id: version
|
|
||||||
run: |
|
run: |
|
||||||
VERSION=${GITHUB_REF#refs/tags/v}
|
VERSION="${GITHUB_REF#refs/tags/}"
|
||||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
echo "Publishing version: $VERSION"
|
||||||
echo "Package version: $VERSION"
|
# Update package.json
|
||||||
|
npm pkg set version=$VERSION
|
||||||
# 修改 package.json 版本号
|
|
||||||
- name: Update package.json version
|
|
||||||
run: |
|
|
||||||
npm pkg set version=${{ steps.version.outputs.version }}
|
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: npm run build
|
run: npm run build -- --define=$VERSION
|
||||||
|
|
||||||
# Trusted Publisher 发布(无需 token)
|
|
||||||
- name: Publish to npm
|
- name: Publish to npm
|
||||||
run: npm publish --access public
|
run: npm publish --access public
|
||||||
|
|
||||||
- name: Create GitHub Release
|
|
||||||
uses: softprops/action-gh-release@v1
|
|
||||||
with:
|
|
||||||
body: |
|
|
||||||
## @simcu/simapi v${{ steps.version.outputs.version }}
|
|
||||||
|
|
||||||
See [changelog](https://github.com/simcu/simapi-vue/releases) for details.
|
|
||||||
draft: false
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
-24
@@ -1,24 +0,0 @@
|
|||||||
# Dependencies
|
|
||||||
node_modules/
|
|
||||||
|
|
||||||
# Build output
|
|
||||||
dist/
|
|
||||||
|
|
||||||
# IDE
|
|
||||||
.vscode/
|
|
||||||
.idea/
|
|
||||||
|
|
||||||
# Logs
|
|
||||||
*.log
|
|
||||||
npm-debug.log*
|
|
||||||
|
|
||||||
# OS
|
|
||||||
.DS_Store
|
|
||||||
Thumbs.db
|
|
||||||
|
|
||||||
# Test
|
|
||||||
coverage/
|
|
||||||
|
|
||||||
# Environment
|
|
||||||
.env
|
|
||||||
.env.local
|
|
||||||
-25
@@ -1,25 +0,0 @@
|
|||||||
# 源码不发布
|
|
||||||
src/
|
|
||||||
|
|
||||||
# 开发文件
|
|
||||||
tsconfig.json
|
|
||||||
vite.config.ts
|
|
||||||
vue.config.js
|
|
||||||
|
|
||||||
# 测试
|
|
||||||
**/*.test.ts
|
|
||||||
**/*.spec.ts
|
|
||||||
__tests__/
|
|
||||||
|
|
||||||
# IDE
|
|
||||||
.vscode/
|
|
||||||
.idea/
|
|
||||||
|
|
||||||
# Git
|
|
||||||
.git
|
|
||||||
.gitignore
|
|
||||||
|
|
||||||
# 其他
|
|
||||||
*.log
|
|
||||||
.DS_Store
|
|
||||||
node_modules/
|
|
||||||
+138
@@ -0,0 +1,138 @@
|
|||||||
|
# @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,6 +1,6 @@
|
|||||||
# SimApi - 轻量 API 请求库
|
# @simcu/simapi
|
||||||
|
|
||||||
基于 Axios 的 Vue 3 + Pinia HTTP 客户端库。
|
> 轻量级 HTTP 请求库,基于原生 fetch,支持任意 JS/TS 环境及 Vue3。
|
||||||
|
|
||||||
## 安装
|
## 安装
|
||||||
|
|
||||||
@@ -8,130 +8,344 @@
|
|||||||
npm install @simcu/simapi
|
npm install @simcu/simapi
|
||||||
```
|
```
|
||||||
|
|
||||||
## 快速开始
|
## 架构
|
||||||
|
|
||||||
### 1. 安装依赖
|
```
|
||||||
|
src/
|
||||||
```bash
|
├── types.ts # 类型定义(SimApiBaseResponse、SimApiOptions 等)
|
||||||
npm install @simcu/simapi pinia
|
├── simapi.core.ts # 纯 TS 核心,无框架依赖
|
||||||
|
└── simapi.pinia.ts # Vue3 Pinia Store 封装
|
||||||
```
|
```
|
||||||
|
|
||||||
## 快速开始
|
## 核心层(框架无关)
|
||||||
|
|
||||||
### 1. 初始化配置
|
适用于浏览器、Node.js、小程序等任意环境。
|
||||||
|
|
||||||
在 Vue 应用入口(如 `main.ts` 或 `App.vue`)中配置端点和调试模式:
|
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { useSimApi } from 'simapi'
|
import { SimApiCore } from '@simcu/simapi'
|
||||||
|
|
||||||
const api = useSimApi()
|
const api = new SimApiCore()
|
||||||
|
|
||||||
// 设置 API 端点
|
// 从 window.simapi 读取配置(可选)
|
||||||
api.setEndpoints({
|
api.autoInit()
|
||||||
default: 'https://api.example.com'
|
|
||||||
|
// 或手动配置
|
||||||
|
api.configure({
|
||||||
|
api: { endpoints: { default: 'https://api.example.com' } },
|
||||||
|
debug: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
// 设置调试模式(默认 true)
|
// 发起请求
|
||||||
api.setDebug(true)
|
const res = await api.query('/users/list', { page: 1 })
|
||||||
```
|
// res.code === 200,res.data 为业务数据
|
||||||
|
|
||||||
### 2. 发起请求
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { useSimApi } from 'simapi'
|
|
||||||
|
|
||||||
const api = useSimApi()
|
|
||||||
|
|
||||||
// 简单请求
|
|
||||||
const result = await api.query('/users/list', { page: 1 })
|
|
||||||
|
|
||||||
// 带 Token 认证的请求
|
|
||||||
const result = await api.query('/protected/resource', { id: 123 })
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 登录/登出
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// 登录
|
// 登录
|
||||||
const result = await api.login({
|
await api.login({ phone: '13800138000', code: '123456' })
|
||||||
phone: '13800138000',
|
|
||||||
code: '123456'
|
|
||||||
})
|
|
||||||
|
|
||||||
// 登出
|
// 登出
|
||||||
await api.logout()
|
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 { createPinia } from 'pinia'
|
||||||
|
import App from './App.vue'
|
||||||
|
|
||||||
|
const app = createApp(App)
|
||||||
|
app.use(createPinia())
|
||||||
|
app.mount('#app')
|
||||||
|
```
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 任意组件
|
||||||
|
import { useSimApi } from '@simcu/simapi'
|
||||||
|
|
||||||
|
const api = useSimApi()
|
||||||
|
|
||||||
|
// 响应式
|
||||||
|
console.log(api.token) // 当前 Token
|
||||||
|
console.log(api.isLoggedIn) // 是否已登录
|
||||||
|
|
||||||
|
// 发起请求
|
||||||
|
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',
|
||||||
|
})
|
||||||
|
|
||||||
|
// 指定端点发请求
|
||||||
|
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>
|
||||||
|
```
|
||||||
|
|
||||||
|
初始化时手动调用:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 方式一:从 window.simapi 读取
|
||||||
|
const api = useSimApi()
|
||||||
|
api.autoInit()
|
||||||
|
|
||||||
|
// 方式二:直接传入配置
|
||||||
|
const api = useSimApi()
|
||||||
|
api.configure({
|
||||||
|
api: { endpoints: { default: 'https://api.example.com' } },
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## SimApiBaseResponse 响应格式
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface SimApiBaseResponse<T = any> {
|
||||||
|
code: number // 200 = 成功,其他为业务错误码
|
||||||
|
message: string // 提示信息
|
||||||
|
data?: T // 业务数据
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 完整配置参考
|
||||||
|
|
||||||
|
### SimApiOptions — 完整配置结构
|
||||||
|
|
||||||
|
```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'), // 未授权
|
||||||
|
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
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
console.error('请求失败', err)
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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 版本(完整版)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 完整配置示例
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
api.configure({
|
||||||
|
debug: false,
|
||||||
|
|
||||||
|
auth: {
|
||||||
|
token_name: 'my-app-token',
|
||||||
|
check_url: '/api/auth/check',
|
||||||
|
logout_url: '/api/auth/logout',
|
||||||
|
login_url: '/api/auth/login',
|
||||||
|
},
|
||||||
|
|
||||||
|
api: {
|
||||||
|
endpoints: {
|
||||||
|
default: 'https://api.example.com',
|
||||||
|
admin: 'https://admin.example.com',
|
||||||
|
},
|
||||||
|
defaultEndpoint: 'default',
|
||||||
|
|
||||||
|
businessCallback: {
|
||||||
|
401: () => router.push('/login'),
|
||||||
|
403: () => ElMessage.error('无权限访问'),
|
||||||
|
500: (data) => console.error('服务器错误:', data.message),
|
||||||
|
'common': (data) => ElMessage.error(data.message || '请求失败'),
|
||||||
|
},
|
||||||
|
|
||||||
|
responseCallback: {
|
||||||
|
success: (res) => res.data ?? res,
|
||||||
|
error: (err) => {
|
||||||
|
console.error('网络错误', err)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
## API 参考
|
## API 参考
|
||||||
|
|
||||||
### 配置方法
|
### SimApiCore(核心类)
|
||||||
|
|
||||||
| 方法 | 说明 |
|
| 方法/属性 | 说明 |
|
||||||
| ------------------------------------- | ---------------------------------------------------------- |
|
|-----------|------|
|
||||||
| `setEndpoints(endpoints)` | 设置 API 端点,如 `{ default: 'https://api.example.com' }` |
|
| `configure(options)` | 批量配置(深合并) |
|
||||||
| `setDebug(debug: boolean)` | 开启/关闭调试模式 |
|
| `autoInit()` | 从 `window.simapi` 读取配置(endpoints、defaultEndpoint、debug) |
|
||||||
| `setBusinessCallback(code, callback)` | 设置业务错误码回调 |
|
| `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` | 版本信息对象 |
|
||||||
|
|
||||||
### 请求方法
|
## 构建
|
||||||
|
|
||||||
| 方法 | 说明 |
|
```bash
|
||||||
| ------------------------ | -------------- |
|
npm install
|
||||||
| `api.query(uri, params)` | 发起 POST 请求 |
|
npm run build
|
||||||
| `api.login(data)` | 登录 |
|
npm link # 本地调试
|
||||||
| `api.logout()` | 登出 |
|
|
||||||
|
|
||||||
### 工具方法
|
|
||||||
|
|
||||||
| 方法 | 说明 |
|
|
||||||
| ------------------------ | -------------- |
|
|
||||||
| `api.getEndpoint(name?)` | 获取端点地址 |
|
|
||||||
| `api.getToken()` | 获取当前 Token |
|
|
||||||
|
|
||||||
### Getters
|
|
||||||
|
|
||||||
| 属性 | 说明 |
|
|
||||||
| ---------------- | -------------------- |
|
|
||||||
| `api.token` | 当前 Token(响应式) |
|
|
||||||
| `api.isLoggedIn` | 是否已登录(响应式) |
|
|
||||||
|
|
||||||
## 调试日志
|
|
||||||
|
|
||||||
启用调试模式后,控制台会输出:
|
|
||||||
|
|
||||||
```
|
|
||||||
[REQUEST*] queryId -> /uri AUTH: token
|
|
||||||
[RESPONSE] queryId -> {data: {...}, code: 200}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
设置端点后会自动打印版本信息:
|
---
|
||||||
|
|
||||||
```
|
## 从旧版迁移
|
||||||
UI主应用版本: 1.0.0
|
|
||||||
UISimApi版本: 1.0.0
|
如果你之前使用的是带 axios 的版本,迁移非常简单:
|
||||||
API主应用版本: x.x.x
|
|
||||||
APISimApi版本: x.x.x
|
```diff
|
||||||
|
- import axios from 'axios'
|
||||||
|
+ import { SimApiCore } from '@simcu/simapi'
|
||||||
|
- // ... 你的 axios 配置
|
||||||
|
|
||||||
|
+ const api = new SimApiCore()
|
||||||
|
+ api.setEndpoints({ default: 'https://api.example.com' })
|
||||||
|
+ const res = await api.query('/users/list', { page: 1 })
|
||||||
```
|
```
|
||||||
|
|
||||||
## 业务错误处理
|
API 完全兼容,无需其他改动。主要变化:
|
||||||
|
- 使用原生 fetch 替代 axios
|
||||||
```typescript
|
- Token 通过请求头 `Token` 传递(不是 `Authorization: Bearer`)
|
||||||
const api = useSimApi()
|
- 默认不发送 Cookie,避免 CORS 问题
|
||||||
|
|
||||||
// 处理 401 未授权
|
|
||||||
api.setBusinessCallback(401, (data) => {
|
|
||||||
console.log('Token 过期', data)
|
|
||||||
localStorage.removeItem('token')
|
|
||||||
router.push('/login')
|
|
||||||
})
|
|
||||||
|
|
||||||
// 处理 403 禁止访问
|
|
||||||
api.setBusinessCallback(403, (data) => {
|
|
||||||
ElMessage.error('没有权限')
|
|
||||||
})
|
|
||||||
|
|
||||||
// 处理所有非 200 错误
|
|
||||||
api.setBusinessCallback('common', (data) => {
|
|
||||||
ElMessage.error(data.message)
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|||||||
Generated
+518
-935
File diff suppressed because it is too large
Load Diff
+44
-32
@@ -1,46 +1,58 @@
|
|||||||
{
|
{
|
||||||
"name": "@simcu/simapi",
|
"name": "@simcu/simapi",
|
||||||
"version": "1.0.1",
|
"version": "0.0.0-version-placeholder",
|
||||||
"description": "轻量 Vue3 HTTP 客户端库,基于 Axios + Pinia",
|
"description": "SimApi 统一前端 HTTP 客户端库,支持 Vue3 和常规 JS/TS 项目(基于原生 fetch)",
|
||||||
"main": "dist/simapi.umd.cjs",
|
|
||||||
"module": "dist/simapi.js",
|
|
||||||
"types": "dist/index.d.ts",
|
|
||||||
"files": [
|
|
||||||
"dist",
|
|
||||||
"README.md"
|
|
||||||
],
|
|
||||||
"scripts": {
|
|
||||||
"build": "vite build",
|
|
||||||
"dev": "vite build --watch",
|
|
||||||
"types": "vue-tsc --declaration --emitDeclarationOnly",
|
|
||||||
"lint": "vue-tsc --noEmit"
|
|
||||||
},
|
|
||||||
"keywords": [
|
|
||||||
"vue3",
|
|
||||||
"pinia",
|
|
||||||
"http",
|
|
||||||
"axios",
|
|
||||||
"api",
|
|
||||||
"rest"
|
|
||||||
],
|
|
||||||
"author": "simcu",
|
"author": "simcu",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git@github.com:simcu/simapi-vue.git"
|
"url": "git@github.com:simcu/simapi-ts.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": {
|
||||||
|
"import": "./dist/pinia.mjs",
|
||||||
|
"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",
|
||||||
|
"types": "tsc --declaration --emitDeclarationOnly --project tsconfig.build.json",
|
||||||
|
"lint": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.6.0"
|
"tslib": "^2.3.0"
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"vue": "^3.0.0",
|
|
||||||
"pinia": "^2.0.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitejs/plugin-vue": "^5.0.0",
|
"@vitejs/plugin-vue": "^5.0.0",
|
||||||
"typescript": "^5.0.0",
|
"pinia": "^2.2.0",
|
||||||
|
"typescript": "~5.9.3",
|
||||||
"vite": "^5.0.0",
|
"vite": "^5.0.0",
|
||||||
"vue": "^3.4.0",
|
"vue": "^3.4.0"
|
||||||
"vue-tsc": "^2.0.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
/**
|
|
||||||
* SimApi - 轻量 API 请求库
|
|
||||||
*/
|
|
||||||
|
|
||||||
export { useSimApi } from './simapi'
|
|
||||||
export type { SimApiAuthConfig, SimApiConfig, Versions } from './simapi'
|
|
||||||
export { SimApiVersion, AppVersion } from './simapi'
|
|
||||||
@@ -0,0 +1,323 @@
|
|||||||
|
/**
|
||||||
|
* SimApi Core — 纯 TypeScript 核心,无框架依赖
|
||||||
|
*
|
||||||
|
* 支持所有 JS/TS 环境(Node.js、浏览器、小程序等)。
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* import { SimApiCore } from '@simcu/simapi'
|
||||||
|
* const api = new SimApiCore()
|
||||||
|
* api.setEndpoints({ default: 'https://api.example.com' })
|
||||||
|
* const res = await api.query('/users/list', { page: 1 })
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
AppVersion,
|
||||||
|
SimApiVersion,
|
||||||
|
type SimApiVersions,
|
||||||
|
type SimApiAuthConfig,
|
||||||
|
type SimApiApiConfig,
|
||||||
|
type SimApiOptions,
|
||||||
|
type SimApiBaseResponse,
|
||||||
|
} from './types'
|
||||||
|
|
||||||
|
export { SimApiVersion, AppVersion } from './types'
|
||||||
|
export type {
|
||||||
|
SimApiVersions,
|
||||||
|
SimApiAuthConfig,
|
||||||
|
SimApiApiConfig,
|
||||||
|
SimApiOptions,
|
||||||
|
SimApiBaseResponse,
|
||||||
|
} from './types'
|
||||||
|
|
||||||
|
// ── Helper: Fetch with Timeout ────────────────────────────────────────
|
||||||
|
|
||||||
|
function fetchWithTimeout(
|
||||||
|
url: string,
|
||||||
|
options: RequestInit,
|
||||||
|
timeout: number = 10000
|
||||||
|
): Promise<Response> {
|
||||||
|
return Promise.race([
|
||||||
|
fetch(url, options),
|
||||||
|
new Promise<never>((_, reject) =>
|
||||||
|
setTimeout(() => reject(new Error(`Request timeout after ${timeout}ms`)), timeout)
|
||||||
|
),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Helper: Fetch POST with JSON body ────────────────────────────────────
|
||||||
|
|
||||||
|
async function fetchPost<T = any>(
|
||||||
|
url: string,
|
||||||
|
body: any,
|
||||||
|
headers: Record<string, string>,
|
||||||
|
timeout: number
|
||||||
|
): Promise<SimApiBaseResponse<T>> {
|
||||||
|
const options: RequestInit = {
|
||||||
|
method: 'POST',
|
||||||
|
headers: headers as HeadersInit,
|
||||||
|
body: body instanceof FormData ? body : JSON.stringify(body),
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── SimApiCore ────────────────────────────────────────
|
||||||
|
|
||||||
|
export class SimApiCore {
|
||||||
|
debug: boolean = true
|
||||||
|
uiAppVersion?: string
|
||||||
|
|
||||||
|
auth: SimApiAuthConfig = {
|
||||||
|
token_name: 'simapi-auth-token',
|
||||||
|
check_url: '/auth/check',
|
||||||
|
logout_url: '/auth/logout',
|
||||||
|
login_url: '/auth/login',
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 window.simapi 读取配置并初始化
|
||||||
|
*
|
||||||
|
* 支持字段:endpoints, defaultEndpoint, debug, uiAppVersion
|
||||||
|
* 业务回调(businessCallback / responseCallback)需在代码中处理
|
||||||
|
*/
|
||||||
|
autoInit(): void {
|
||||||
|
const config = (window as any).simapi
|
||||||
|
if (!config) return
|
||||||
|
|
||||||
|
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 }
|
||||||
|
}
|
||||||
|
if (config.defaultEndpoint) {
|
||||||
|
this.api.defaultEndpoint = config.defaultEndpoint
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
configure(options: SimApiOptions): void {
|
||||||
|
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 }
|
||||||
|
}
|
||||||
|
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> {
|
||||||
|
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',
|
||||||
|
}
|
||||||
|
if (this.debug) {
|
||||||
|
console.log(`UI主应用版本: ${versions.uiApp}\nUISimApi版本: ${versions.uiSimApi}\nAPI主应用版本: ${versions.apiApp}\nAPISimApi版本: ${versions.apiSimApi}`)
|
||||||
|
}
|
||||||
|
return versions
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// 版本获取失败返回默认值
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
uiApp: AppVersion,
|
||||||
|
uiSimApi: SimApiVersion,
|
||||||
|
apiApp: '0.0.0',
|
||||||
|
apiSimApi: '0.0.0',
|
||||||
|
apiAppFull: '0.0.0',
|
||||||
|
apiSimApiFull: '0.0.0',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
headers['Token'] = token
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.debug) {
|
||||||
|
headers['Query-Id'] = queryId
|
||||||
|
console.log('[REQUEST*]', queryId, '->', uri, 'AUTH:', localStorage.getItem(this.auth.token_name))
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (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>> {
|
||||||
|
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> {
|
||||||
|
this.removeToken()
|
||||||
|
if (url !== null) {
|
||||||
|
return this.query(url ?? this.auth.logout_url).catch(() => true)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkLogin(url?: string | null): Promise<void> {
|
||||||
|
if (url !== null) {
|
||||||
|
await this.query(url ?? this.auth.check_url).catch(() => {})
|
||||||
|
} else if (this.getToken()) {
|
||||||
|
this.api.businessCallback[401]?.(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { SimApiCore } from './simapi.core'
|
||||||
|
import type { SimApiBaseResponse, SimApiOptions, SimApiVersions } from './types'
|
||||||
|
|
||||||
|
// ============ Pinia Store ============
|
||||||
|
// 仅作为 core 的代理映射,不维护任何独立状态
|
||||||
|
|
||||||
|
export const useSimApi = defineStore('simapi', {
|
||||||
|
state: () => ({
|
||||||
|
// 在 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,
|
||||||
|
},
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
// 所有方法直接代理到 core
|
||||||
|
autoInit(): void {
|
||||||
|
this._core.autoInit()
|
||||||
|
},
|
||||||
|
|
||||||
|
configure(options: SimApiOptions): void {
|
||||||
|
this._core.configure(options)
|
||||||
|
},
|
||||||
|
|
||||||
|
setDebug(debug: boolean): void {
|
||||||
|
this._core.debug = debug
|
||||||
|
},
|
||||||
|
|
||||||
|
setEndpoints(endpoints: { [name: string]: string }): void {
|
||||||
|
this._core.setEndpoints(endpoints)
|
||||||
|
},
|
||||||
|
|
||||||
|
setBusinessCallback(
|
||||||
|
code: number | string,
|
||||||
|
callback: (data: SimApiBaseResponse) => void
|
||||||
|
): void {
|
||||||
|
this._core.setBusinessCallback(code, callback)
|
||||||
|
},
|
||||||
|
|
||||||
|
getToken(): string {
|
||||||
|
return this._core.getToken()
|
||||||
|
},
|
||||||
|
|
||||||
|
setToken(token: string): void {
|
||||||
|
this._core.setToken(token)
|
||||||
|
},
|
||||||
|
|
||||||
|
removeToken(): void {
|
||||||
|
this._core.removeToken()
|
||||||
|
},
|
||||||
|
|
||||||
|
async login(request: Record<string, any>): Promise<SimApiBaseResponse<string>> {
|
||||||
|
return this._core.login(request)
|
||||||
|
},
|
||||||
|
|
||||||
|
async logout(url?: string | null): Promise<any> {
|
||||||
|
return this._core.logout(url)
|
||||||
|
},
|
||||||
|
|
||||||
|
async checkLogin(url?: string | null): Promise<void> {
|
||||||
|
return this._core.checkLogin(url)
|
||||||
|
},
|
||||||
|
|
||||||
|
async query<T = any>(
|
||||||
|
uri: string,
|
||||||
|
params?: any,
|
||||||
|
endpointKey?: string,
|
||||||
|
extraHeaders?: Record<string, string>
|
||||||
|
): Promise<SimApiBaseResponse<T>> {
|
||||||
|
return this._core.query<T>(uri, params, endpointKey, extraHeaders)
|
||||||
|
},
|
||||||
|
|
||||||
|
getEndpoint(name?: string): string {
|
||||||
|
return this._core.getEndpoint(name)
|
||||||
|
},
|
||||||
|
|
||||||
|
async getVersion(endpointName?: string): Promise<SimApiVersions> {
|
||||||
|
return this._core.getVersion(endpointName)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
-207
@@ -1,207 +0,0 @@
|
|||||||
/**
|
|
||||||
* SimApi Pinia Store
|
|
||||||
*
|
|
||||||
* 使用方法:
|
|
||||||
* import { useSimApi } from 'simapi'
|
|
||||||
* const api = useSimApi()
|
|
||||||
* await api.query('/api/xxx', data)
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { defineStore } from 'pinia'
|
|
||||||
import axios, { AxiosRequestHeaders } from 'axios'
|
|
||||||
|
|
||||||
// ============ 类型定义 ============
|
|
||||||
|
|
||||||
export const SimApiVersion = '1.0.0'
|
|
||||||
export const AppVersion = '1.0.0'
|
|
||||||
|
|
||||||
export interface Versions {
|
|
||||||
uiApp: string
|
|
||||||
uiSimApi: string
|
|
||||||
apiApp: string
|
|
||||||
apiSimApi: string
|
|
||||||
apiAppFull: string
|
|
||||||
apiSimApiFull: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BusinessCallback {
|
|
||||||
[key: number | string]: (data: any) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SimApiAuthConfig {
|
|
||||||
token_name: string
|
|
||||||
check_url: string
|
|
||||||
logout_url: string
|
|
||||||
login_url: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SimApiConfig {
|
|
||||||
endpoints: { [name: string]: string }
|
|
||||||
defaultEndpoint: string
|
|
||||||
businessCallback: BusinessCallback
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============ Store 定义 ============
|
|
||||||
|
|
||||||
export const useSimApi = defineStore('simapi', {
|
|
||||||
// ============ State ============
|
|
||||||
state: () => ({
|
|
||||||
debug: true,
|
|
||||||
|
|
||||||
auth: {
|
|
||||||
token_name: 'simapi-auth-token',
|
|
||||||
check_url: '/auth/check',
|
|
||||||
logout_url: '/auth/logout',
|
|
||||||
login_url: '/auth/login'
|
|
||||||
} as SimApiAuthConfig,
|
|
||||||
|
|
||||||
api: {
|
|
||||||
endpoints: { default: '' },
|
|
||||||
defaultEndpoint: 'default',
|
|
||||||
businessCallback: {
|
|
||||||
401: () => localStorage.removeItem('simapi-auth-token'),
|
|
||||||
common: () => {}
|
|
||||||
}
|
|
||||||
} as SimApiConfig,
|
|
||||||
|
|
||||||
versions: {
|
|
||||||
uiApp: AppVersion,
|
|
||||||
uiSimApi: SimApiVersion,
|
|
||||||
apiApp: '0.0.0',
|
|
||||||
apiSimApi: '0.0.0',
|
|
||||||
apiAppFull: '0.0.0',
|
|
||||||
apiSimApiFull: '0.0.0'
|
|
||||||
} as Versions
|
|
||||||
}),
|
|
||||||
|
|
||||||
// ============ Getters ============
|
|
||||||
getters: {
|
|
||||||
token: (state) => localStorage.getItem(state.auth.token_name) || '',
|
|
||||||
isLoggedIn: (state) => !!localStorage.getItem(state.auth.token_name)
|
|
||||||
},
|
|
||||||
|
|
||||||
// ============ Actions ============
|
|
||||||
actions: {
|
|
||||||
/** 设置端点配置 */
|
|
||||||
setEndpoints(endpoints: { [name: string]: string }): void {
|
|
||||||
this.api.endpoints = { ...this.api.endpoints, ...endpoints }
|
|
||||||
// 获取并打印版本信息
|
|
||||||
this.getVersions()
|
|
||||||
},
|
|
||||||
|
|
||||||
/** 设置业务错误回调 */
|
|
||||||
setBusinessCallback(code: number | string, callback: (data: any) => void): void {
|
|
||||||
this.api.businessCallback[code] = callback
|
|
||||||
},
|
|
||||||
|
|
||||||
/** 设置调试模式 */
|
|
||||||
setDebug(debug: boolean): void {
|
|
||||||
this.debug = debug
|
|
||||||
},
|
|
||||||
|
|
||||||
/** 打印调试日志 */
|
|
||||||
debug(title: string, data: any): void {
|
|
||||||
if (this.debug) {
|
|
||||||
console.log('[DEBUG]', title, data)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/** 生成随机字符串 */
|
|
||||||
genS4(): string {
|
|
||||||
return (((1 + Math.random()) * 0x10000 * Date.parse(new Date())) | 0).toString(16).substring(1)
|
|
||||||
},
|
|
||||||
|
|
||||||
/** 获取端点地址 */
|
|
||||||
getEndpoint(name?: string): string {
|
|
||||||
return this.api.endpoints[name || this.api.defaultEndpoint] || ''
|
|
||||||
},
|
|
||||||
|
|
||||||
/** 获取并打印版本信息 */
|
|
||||||
async getVersions(): Promise<void> {
|
|
||||||
try {
|
|
||||||
const resp = await axios.post(this.getEndpoint() + '/versions', {}, { timeout: 5000 })
|
|
||||||
if (resp.data?.data) {
|
|
||||||
this.versions = {
|
|
||||||
uiApp: AppVersion,
|
|
||||||
uiSimApi: SimApiVersion,
|
|
||||||
apiApp: resp.data.data.App?.split('+')[0] || '0.0.0',
|
|
||||||
apiSimApi: resp.data.data.SimApi?.split('+')[0] || '0.0.0',
|
|
||||||
apiAppFull: resp.data.data.App || '0.0.0',
|
|
||||||
apiSimApiFull: resp.data.data.SimApi || '0.0.0'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// 版本获取失败不影响主流程
|
|
||||||
}
|
|
||||||
// 打印版本信息,格式与 Angular 一致
|
|
||||||
console.log(`UI主应用版本: ${this.versions.uiApp}\nUISimApi版本: ${this.versions.uiSimApi}\nAPI主应用版本: ${this.versions.apiApp}\nAPISimApi版本: ${this.versions.apiSimApi}`)
|
|
||||||
},
|
|
||||||
|
|
||||||
/** 发起请求 */
|
|
||||||
async query(uri: string, params: any = {}): Promise<any> {
|
|
||||||
const headers: Record<string, string> = {}
|
|
||||||
const queryId = this.genS4()
|
|
||||||
|
|
||||||
if (!(params instanceof FormData)) {
|
|
||||||
headers['Content-Type'] = 'application/json'
|
|
||||||
}
|
|
||||||
|
|
||||||
const token = this.token
|
|
||||||
if (token) {
|
|
||||||
headers['Token'] = token
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.debug) {
|
|
||||||
headers['Query-Id'] = queryId
|
|
||||||
console.log('[REQUEST*]', queryId, '->', uri, 'AUTH:', token)
|
|
||||||
}
|
|
||||||
|
|
||||||
const url = this.getEndpoint() + uri
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await axios.post(url, params, { headers: headers as AxiosRequestHeaders })
|
|
||||||
if (this.debug) {
|
|
||||||
console.log('[RESPONSE]', queryId, '->', response.data)
|
|
||||||
}
|
|
||||||
return this.handleResponse(response.data)
|
|
||||||
} catch (error) {
|
|
||||||
if (this.debug) {
|
|
||||||
console.log('[RESPONSE]', queryId, '->', error)
|
|
||||||
}
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/** 处理响应 */
|
|
||||||
handleResponse(data: any): any {
|
|
||||||
if (data.code !== 200) {
|
|
||||||
const callback = this.api.businessCallback[data.code] || this.api.businessCallback['common']
|
|
||||||
callback?.(data)
|
|
||||||
}
|
|
||||||
return data
|
|
||||||
},
|
|
||||||
|
|
||||||
/** 登录 */
|
|
||||||
async login(request: Record<string, any>): Promise<any> {
|
|
||||||
const result = await this.query(this.auth.login_url, request)
|
|
||||||
if (result.data) {
|
|
||||||
localStorage.setItem(this.auth.token_name, result.data)
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
},
|
|
||||||
|
|
||||||
/** 登出 */
|
|
||||||
async logout(url?: string | null): Promise<any> {
|
|
||||||
localStorage.removeItem(this.auth.token_name)
|
|
||||||
if (url !== null) {
|
|
||||||
return this.query(url ?? this.auth.logout_url).catch(() => true)
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
},
|
|
||||||
|
|
||||||
/** 获取 Token */
|
|
||||||
getToken(): string {
|
|
||||||
return localStorage.getItem(this.auth.token_name) || ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
/**
|
||||||
|
* SimApi 类型定义
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 版本号占位符,构建时由 GitHub Action 替换
|
||||||
|
export const SimApiVersion = '0.0.0-version-placeholder'
|
||||||
|
export const AppVersion = '0.0.0-version-placeholder'
|
||||||
|
|
||||||
|
/** 版本信息 */
|
||||||
|
export interface SimApiVersions {
|
||||||
|
uiApp: string
|
||||||
|
uiSimApi: string
|
||||||
|
apiApp: string
|
||||||
|
apiSimApi: string
|
||||||
|
apiAppFull: string
|
||||||
|
apiSimApiFull: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 认证配置 */
|
||||||
|
export interface SimApiAuthConfig {
|
||||||
|
/** localStorage key,默认 'simapi-auth-token' */
|
||||||
|
token_name: string
|
||||||
|
/** 检查登录接口,默认 '/auth/check' */
|
||||||
|
check_url: string
|
||||||
|
/** 登出接口,默认 '/auth/logout' */
|
||||||
|
logout_url: string
|
||||||
|
/** 登录接口,默认 '/auth/login' */
|
||||||
|
login_url: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 业务错误码回调 */
|
||||||
|
export interface SimApiBusinessCallback {
|
||||||
|
[key: number | string]: (data: any) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 响应拦截回调 */
|
||||||
|
export interface SimApiResponseCallback {
|
||||||
|
success: (response: any) => any
|
||||||
|
error: (err: any) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
/** API 配置 */
|
||||||
|
export interface SimApiApiConfig {
|
||||||
|
/** 多端点映射 */
|
||||||
|
endpoints: { [name: string]: string }
|
||||||
|
/** 默认端点名称 */
|
||||||
|
defaultEndpoint: string
|
||||||
|
/** 业务错误码回调 */
|
||||||
|
businessCallback: SimApiBusinessCallback
|
||||||
|
/** 响应拦截器 */
|
||||||
|
responseCallback: SimApiResponseCallback
|
||||||
|
/** 请求超时时间(毫秒),默认 10000 */
|
||||||
|
timeout?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/** SimApi 完整配置 */
|
||||||
|
export interface SimApiOptions {
|
||||||
|
debug?: boolean
|
||||||
|
/** UI 应用版本,如果不指定则使用库内置的占位符版本 */
|
||||||
|
uiAppVersion?: string
|
||||||
|
auth?: Partial<SimApiAuthConfig>
|
||||||
|
api?: Partial<SimApiApiConfig>
|
||||||
|
}
|
||||||
|
|
||||||
|
/** SimApi 标准响应格式 */
|
||||||
|
export interface SimApiBaseResponse<T = any> {
|
||||||
|
code: number
|
||||||
|
message: string
|
||||||
|
data?: T
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"declaration": true,
|
||||||
|
"emitDeclarationOnly": true,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": false,
|
||||||
|
"noUnusedParameters": false,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"emitDecoratorMetadata": true
|
||||||
|
},
|
||||||
|
"include": ["src/types.ts", "src/simapi.core.ts", "src/simapi.pinia.ts"]
|
||||||
|
}
|
||||||
+6
-5
@@ -10,11 +10,12 @@
|
|||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"jsx": "preserve",
|
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"noUnusedLocals": true,
|
"noUnusedLocals": false,
|
||||||
"noUnusedParameters": true,
|
"noUnusedParameters": false,
|
||||||
"noFallthroughCasesInSwitch": true
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"emitDecoratorMetadata": true
|
||||||
},
|
},
|
||||||
"include": ["src/**/*.ts", "src/**/*.d.ts"]
|
"include": ["src/**/*.ts"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
import { defineConfig } from 'vite'
|
|
||||||
import vue from '@vitejs/plugin-vue'
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
plugins: [vue()],
|
|
||||||
build: {
|
|
||||||
lib: {
|
|
||||||
entry: 'src/index.ts',
|
|
||||||
name: 'SimApi',
|
|
||||||
formats: ['es', 'umd'],
|
|
||||||
fileName: (format) => `simapi.${format}.${format === 'es' ? 'js' : 'cjs'}`
|
|
||||||
},
|
|
||||||
rollupOptions: {
|
|
||||||
external: ['vue'],
|
|
||||||
output: {
|
|
||||||
globals: {
|
|
||||||
vue: 'Vue'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
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