Compare commits

...
16 Commits
Author SHA1 Message Date
xrain fe77c0ee1a 修正了业务错误报错
Publish to npm / publish (push) Failing after 12s
2026-04-22 22:08:35 +08:00
xrain 98c46f4a7b 修改了环境变量
Publish to npm / publish (push) Failing after 11s
2026-04-08 02:58:50 +08:00
xrain ae742d452d 修正无用的内容
Publish to npm / publish (push) Failing after 10s
2026-04-08 02:44:04 +08:00
xrain 29d3705073 fix ci
Publish to npm / publish (push) Failing after 11s
2026-04-08 01:52:19 +08:00
xrain df0d7a4fe3 fix ci
Publish to npm / publish (push) Failing after 10s
2026-04-08 01:51:14 +08:00
xrain d7aa0f171d fix ci
Publish to npm / publish (push) Failing after 12s
2026-04-08 01:45:53 +08:00
xrain b6d5a5dbe1 fix ci
Publish to npm / publish (push) Failing after 12s
2026-04-08 01:33:23 +08:00
xrain e91ddd36bf fix ci
Publish to npm / publish (push) Failing after 12s
2026-04-08 01:21:45 +08:00
xrain 903f70b0ba fix ci
Publish to npm / publish (push) Failing after 29s
2026-04-08 01:20:36 +08:00
xrain c136375100 fix ci
Publish to npm / publish (push) Failing after 28s
2026-04-08 01:15:33 +08:00
xrain d44938e456 3.0
Publish to npm / publish (push) Failing after 29s
2026-04-08 01:07:10 +08:00
xrain c100973590 fix ci
Publish to npm / publish (push) Failing after 2m22s
2026-03-31 06:51:46 +08:00
xrain 957656ad5b fix ci
Publish to npm / publish (push) Failing after 2m22s
2026-03-31 06:50:18 +08:00
xrain 02c9c7af3c fix ci
Publish to npm / publish (push) Failing after 2m27s
2026-03-31 06:47:38 +08:00
xrain 0b4b4b0c03 fix ci 2026-03-31 06:46:34 +08:00
xrain f89b14ea7d fix: remove environment 2026-03-31 06:44:36 +08:00
15 changed files with 1914 additions and 1365 deletions
+17 -40
View File
@@ -3,60 +3,37 @@ name: Publish to npm
on:
push:
tags:
- 'v*.*.*'
workflow_dispatch:
- "*"
permissions:
id-token: write # Required for OIDC
contents: read
jobs:
publish:
runs-on: ubuntu-latest
environment: release # npm Trusted Publisher 环境
permissions:
id-token: write # OIDC 认证
contents: read # 读取仓库
steps:
- name: Checkout
uses: actions/checkout@v4
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
fetch-depth: 0 # 完整 history 用于版本分析
node-version: "24"
registry-url: "https://registry.npmjs.org"
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
registry-url: 'https://registry.npmjs.org'
# 从 tag 提取版本号
- name: Get version from tag
id: version
- name: Replace version placeholder
run: |
VERSION=${GITHUB_REF#refs/tags/v}
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Package version: $VERSION"
# 修改 package.json 版本号
- name: Update package.json version
run: |
npm pkg set version=${{ steps.version.outputs.version }}
VERSION="${GITHUB_REF#refs/tags/}"
echo "Publishing version: $VERSION"
# Update package.json
npm pkg set version=$VERSION
- name: Install dependencies
run: npm ci
- name: Build
env:
SimApiVersion: ${{ github.ref_name }}
run: npm run build
# Trusted Publisher 发布(无需 token
- name: Publish to npm
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 }}
run: npm publish --access public
-24
View File
@@ -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
View File
@@ -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/
+166
View File
@@ -0,0 +1,166 @@
# @simcu/simapi — AI 开发指南
> 面向 AI Agent 的代码结构说明,帮助理解、修改和扩展本库。
---
## 项目结构
```
simapi-vue/
├── src/
│ ├── types.ts # 类型定义,全部导出
│ ├── simapi.core.ts # 核心类 SimApiCore,零框架依赖
│ └── simapi.pinia.ts # Pinia StoreVue3 适配层
├── dist/ # 构建产物
├── package.json # exports: "." → core, "/pinia" → vue
├── vite.config.ts # 统一构建配置,生成 .mjs 和 .cjs
└── tsconfig.build.json # tsc 生成类型声明
```
---
## 核心类型(types.ts
```typescript
SimApiBaseResponse<T> // 标准响应 { code, message, data? }
SimApiOptions // configure() 入参 { debug?, auth?, api? }
SimApiAuthConfig // auth: { token_name, check_url, logout_url, login_url }
SimApiApiConfig // api: { endpoints, defaultEndpoint, businessCallback, responseCallback, timeout? }
SimApiBusinessCallback // { [code]: (data) => void },支持数字码或 'common'
```
---
## SimApiCoresimapi.core.ts
**职责**:纯 TS HTTP 客户端,基于原生 fetch API,不依赖任何框架。
**关键设计**
- **零依赖**:使用原生 fetch,无 axios 或其他 HTTP 库
- **无 Cookie**:所有请求使用 `credentials: 'omit'`,避免 CORS 问题
- **Token 传递**:通过请求头 `Token` 传递认证信息
- **超时控制**:通过 `fetchWithTimeout` 辅助函数实现
- `query()` 抛出异常的只有网络/HTTP 错误,业务错误码通过回调处理
- `isLoggedIn` 是 getter,基于 localStorage 中的 token 判断
- **版本号**`getVersion()` 返回的版本信息中,`uiSimApi` 由构建时注入的 `SimApiVersion` 填充,`uiApp` 由调用方的 `AppVersion` 填充(未注入时为 `0.0.0-develop`
**修改建议**
- 改请求方法(GET/PUT/DELETE):在 `fetchPost()` 内新增 `method` 参数分支,或新增 `fetchGet()`/`fetchPut()` 方法
- 改 Token 存储:替换 `localStorage``sessionStorage` 或内存变量,修改 `getToken()`/`setToken()`/`removeToken()`
- 改登录/登出逻辑:修改 `login()`/`logout()` 方法
- 改超时处理:修改 `fetchWithTimeout()` 函数
- **版本管理**:版本号通过 `declare const` 声明常量,构建时通过 Vite 的 `define` 注入。未指定时默认为 `0.0.0-develop`
**autoInit 设计约束**
- 仅读取 `window.simapi` 的顶级字段:`endpoints``defaultEndpoint``debug`
- 业务回调(`businessCallback`/`responseCallback`)不支持从 window 读取,必须在代码中通过 `setBusinessCallback` 注册
---
## Pinia Storesimapi.pinia.ts
**职责**Vue3 适配层,SimApiCore 的纯代理,不维护任何独立状态。
**关键设计**
- **无独立状态**:state 中只有一个 `_core` 实例,不维护 `debug``token` 等独立数据
- **单例 Core**:在 store state 中实例化 `SimApiCore`,整个应用共享一个实例
- **纯代理映射**:所有 getters 直接映射到 `this._core` 的属性,所有 actions 直接调用 `this._core` 的方法
- **响应式**:通过 Pinia 的响应式系统,当 core 状态变化时自动更新
**使用方式**
```typescript
// 任意组件
import { useSimApi } from '@simcu/simapi'
const api = useSimApi()
// 初始化(二选一)
// 方式一:从 window.simapi 读取
api.autoInit()
// 方式二:直接传入配置
api.configure({
api: { endpoints: { default: 'https://api.example.com' } },
})
// 所有方法与 SimApiCore 完全一致
await api.query('/users/list', { page: 1 })
```
---
## 构建流程
```
npm run build
→ vite build 输出 dist/index.mjs / index.cjs / pinia.mjs / pinia.cjs
→ tsc -p tsconfig.build.json 输出 *.d.ts 类型声明
```
**版本号注入:**
版本号通过 `vite.config.ts``define` 配置注入,从 npm config 读取:
```typescript
define: {
'SimApiVersion': JSON.stringify(process.env.npm_config_SimApiVersion || '0.0.0-develop'),
}
```
**本地构建示例:**
```bash
# 通过环境变量
# Windows PowerShell
$env:npm_config_SimApiVersion='1.0.0'; npm run build
# Linux/Mac
npm_config_SimApiVersion=1.0.0 npm run build
```
**GitHub Actions 自动发布:**
```yaml
env:
SimApiVersion: ${{ github.ref_name }}
run: npm run build
```
**dist 输出是平铺的**core 和 pinia 的编译产物全部在同一目录:
```
dist/
├── index.mjs # core ESM
├── index.cjs # core CJS
├── pinia.mjs # pinia ESM
├── pinia.cjs # pinia CJS
├── simapi.core.d.ts
├── simapi.pinia.d.ts
└── types.d.ts
```
---
## package.json exports
```json
".": { "import": "./dist/index.mjs", "types": "./dist/simapi.core.d.ts" }
"./pinia": { "import": "./dist/pinia.mjs", "types": "./dist/simapi.pinia.d.ts" }
```
---
## 注意事项
- Core 默认 `debug: true`,生产环境需手动 `configure({ debug: false })`
- `query()` 返回 `Promise<SimApiBaseResponse<T>>`code !== 200 时不 reject,通过 `businessCallback` 处理
- `login()` 成功后将 `result.data` 存入 localStorage
- **无 Cookie**:所有请求不发送 CookieToken 通过请求头传递
- **零依赖**:不需要安装 axios,使用原生 fetch
- 删除了 Angular 支持,如需恢复参考 git 历史
- **版本号管理**:使用 `declare const` + Vite `define` 注入
- **AppVersion** 不注入,留给调用方自己管理
+360 -99
View File
@@ -1,6 +1,6 @@
# SimApi - 轻量 API 请求库
# @simcu/simapi
基于 Axios 的 Vue 3 + Pinia HTTP 客户端库
> 轻量级 HTTP 请求库,基于原生 fetch,支持任意 JS/TS 环境及 Vue3
## 安装
@@ -8,130 +8,391 @@
npm install @simcu/simapi
```
## 快速开始
## 架构
### 1. 安装依赖
```bash
npm install @simcu/simapi pinia
```
src/
├── types.ts # 类型定义(SimApiBaseResponse、SimApiOptions 等)
├── simapi.core.ts # 纯 TS 核心,无框架依赖
└── simapi.pinia.ts # Vue3 Pinia Store 封装
```
## 快速开始
## 核心层(框架无关)
### 1. 初始化配置
在 Vue 应用入口(如 `main.ts``App.vue`)中配置端点和调试模式:
适用于浏览器、Node.js、小程序等任意环境。
```typescript
import { useSimApi } from 'simapi'
import { SimApiCore } from '@simcu/simapi'
const api = useSimApi()
const api = new SimApiCore()
// 设置 API 端点
api.setEndpoints({
default: 'https://api.example.com'
// 从 window.simapi 读取配置(可选)
api.autoInit()
// 或手动配置
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 === 200res.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({
phone: '13800138000',
code: '123456'
})
await api.login({ phone: '13800138000', code: '123456' })
// 登出
await api.logout()
// 注册业务错误码回调
api.setBusinessCallback(401, () => router.push('/login'))
api.setBusinessCallback('common', (data) => alert(data.message))
```
## Vue3
安装 Pinia
```bash
npm install pinia
```
```typescript
// main.ts
import { createApp } from 'vue'
import { 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 参考
### 配置方法
### SimApiCore(核心类)
| 方法 | 说明 |
| ------------------------------------- | ---------------------------------------------------------- |
| `setEndpoints(endpoints)` | 设置 API 端点,如 `{ default: 'https://api.example.com' }` |
| `setDebug(debug: boolean)` | 开启/关闭调试模式 |
| `setBusinessCallback(code, callback)` | 设置业务错误码回调 |
| 方法/属性 | 说明 |
|-----------|------|
| `configure(options)` | 批量配置(深合并) |
| `autoInit()` | 从 `window.simapi` 读取配置(endpoints、defaultEndpoint、debug |
| `setEndpoints(map)` | 设置端点 |
| `setBusinessCallback(code, fn)` | 注册业务错误码回调 |
| `query(uri, params?, endpointKey?, headers?)` | POST 请求,返回 `Promise<SimApiBaseResponse<T>>` |
| `login(request)` | 登录,自动存 Token |
| `logout(url?)` | 登出,清除 Token |
| `checkLogin(url?)` | 主动检查登录状态 |
| `getVersion(endpointName?)` | 获取版本信息,返回 `Promise<SimApiVersions>` |
| `getToken()` | 获取 Token |
| `setToken(token)` | 手动设置 Token |
| `removeToken()` | 清除 Token |
| `isLoggedIn` | getter,是否已登录 |
| `debug` | boolean,调试模式 |
| `logDebug(...args)` | 日志工具(仅在 debug 模式输出) |
### 请求方法
## 构建
| 方法 | 说明 |
| ------------------------ | -------------- |
| `api.query(uri, params)` | 发起 POST 请求 |
| `api.login(data)` | 登录 |
| `api.logout()` | 登出 |
### 工具方法
| 方法 | 说明 |
| ------------------------ | -------------- |
| `api.getEndpoint(name?)` | 获取端点地址 |
| `api.getToken()` | 获取当前 Token |
### Getters
| 属性 | 说明 |
| ---------------- | -------------------- |
| `api.token` | 当前 Token(响应式) |
| `api.isLoggedIn` | 是否已登录(响应式) |
## 调试日志
启用调试模式后,控制台会输出:
```
[REQUEST*] queryId -> /uri AUTH: token
[RESPONSE] queryId -> {data: {...}, code: 200}
```bash
npm install
npm run build
npm link # 本地调试
```
设置端点后会自动打印版本信息:
### 版本号注入
```
UI主应用版本: 1.0.0
UISimApi版本: 1.0.0
API主应用版本: x.x.x
APISimApi版本: x.x.x
库使用 `declare const` 声明版本常量,构建时通过 Vite 的 `define` 注入版本号。
**注意:**
- **SimApiVersion**:由 simapi 库构建时注入
- **AppVersion**:不注入,留给调用方 APP 注入
**支持的版本号环境变量(按优先级):**
1. `VITE_SimApiVersion` — Vite .env 文件
2. `npm_config_SimApiVersion` — npm 构建时通过 `npm run build` 前设置
3. `SimApiVersion` — 直接环境变量(如 GitHub Actions
未指定时默认为 `0.0.0-develop`
**本地构建:**
```bash
# Windows PowerShell
$env:npm_config_SimApiVersion='1.0.0'; npm run build
# Windows CMD
set npm_config_SimApiVersion=1.0.0 && npm run build
# Linux/Mac
npm_config_SimApiVersion=1.0.0 npm run build
```
## 业务错误处理
**GitHub Actions / CI/CD**
```yaml
- name: Build
env:
SimApiVersion: ${{ github.ref_name }} # 或其他版本号
run: npm run build
```
**调用方注入 AppVersion**
调用方在自己的 `vite.config.ts` 中添加:
```typescript
const api = useSimApi()
// 处理 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)
})
define: {
'AppVersion': JSON.stringify('1.0.0')
}
```
---
## 从旧版迁移
如果你之前使用的是带 axios 的版本,迁移非常简单:
```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
- Token 通过请求头 `Token` 传递(不是 `Authorization: Bearer`
- 默认不发送 Cookie,避免 CORS 问题
+786 -910
View File
File diff suppressed because it is too large Load Diff
+37 -32
View File
@@ -1,46 +1,51 @@
{
"name": "@simcu/simapi",
"version": "1.0.1",
"description": "轻量 Vue3 HTTP 客户端库,基于 Axios + Pinia",
"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"
],
"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-vue.git"
},
"dependencies": {
"axios": "^1.6.0"
"keywords": [
"simapi",
"vue3",
"pinia",
"http",
"fetch"
],
"files": [
"dist",
"README.md",
"package.json"
],
"exports": {
".": {
"import": "./dist/index.mjs",
"types": "./dist/simapi.core.d.ts"
},
"./pinia": {
"import": "./dist/pinia.mjs",
"types": "./dist/simapi.pinia.d.ts"
}
},
"peerDependencies": {
"vue": "^3.0.0",
"pinia": "^2.0.0"
"module": "./dist/index.mjs",
"types": "./dist/simapi.core.d.ts",
"scripts": {
"build": "vite build && npm run types",
"dev": "vite build --watch",
"types": "tsc --declaration --emitDeclarationOnly --project tsconfig.build.json",
"lint": "tsc --noEmit"
},
"dependencies": {
"tslib": "^2.3.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.0.0",
"typescript": "^5.0.0",
"concurrently": "^9.2.1",
"pinia": "^2.2.0",
"typescript": "~5.9.3",
"vite": "^5.0.0",
"vue": "^3.4.0",
"vue-tsc": "^2.0.0"
"vue": "^3.4.0"
}
}
-7
View File
@@ -1,7 +0,0 @@
/**
* SimApi - 轻量 API 请求库
*/
export { useSimApi } from './simapi'
export type { SimApiAuthConfig, SimApiConfig, Versions } from './simapi'
export { SimApiVersion, AppVersion } from './simapi'
+323
View File
@@ -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 {
type SimApiVersions,
type SimApiAuthConfig,
type SimApiApiConfig,
type SimApiOptions,
type SimApiBaseResponse,
} from './types'
export type {
SimApiVersions,
SimApiAuthConfig,
SimApiApiConfig,
SimApiOptions,
SimApiBaseResponse,
} from './types'
declare const SimApiVersion: string;
// ── 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
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
* 业务回调(businessCallback / responseCallback)需在代码中处理
*/
autoInit(): void {
const config = (window as any).simapi
if (!config) return
if (config.debug !== undefined) {
this.debug = config.debug
}
if (config.endpoints) {
this.api.endpoints = { ...this.api.endpoints, ...config.endpoints }
}
if (config.defaultEndpoint) {
this.api.defaultEndpoint = config.defaultEndpoint
}
}
configure(options: SimApiOptions): void {
if (options.debug !== undefined) {
this.debug = options.debug
}
if (options.auth) {
this.auth = { ...this.auth, ...options.auth }
}
if (options.api) {
this.api = {
...this.api,
...options.api,
endpoints: { ...this.api.endpoints, ...(options.api.endpoints ?? {}) },
businessCallback: { ...this.api.businessCallback, ...(options.api.businessCallback ?? {}) },
responseCallback: { ...this.api.responseCallback, ...(options.api.responseCallback ?? {}) },
}
}
}
setEndpoints(endpoints: { [name: string]: string }): void {
this.api.endpoints = { ...this.api.endpoints, ...endpoints }
}
getEndpoint(name?: string): string {
return this.api.endpoints[name ?? this.api.defaultEndpoint] ?? ''
}
setBusinessCallback(code: number | string, callback: (data: any) => void): void {
this.api.businessCallback[code] = callback
}
getToken(): string {
return localStorage.getItem(this.auth.token_name) ?? ''
}
setToken(token: string): void {
localStorage.setItem(this.auth.token_name, token)
}
removeToken(): void {
localStorage.removeItem(this.auth.token_name)
}
get isLoggedIn(): boolean {
return !!localStorage.getItem(this.auth.token_name)
}
genS4(): string {
return (((1 + Math.random()) * 0x10000 * Date.parse(new Date().toString())) | 0)
.toString(16)
.substring(1)
}
/**
* 日志工具(仅在 debug 模式下输出)
*
* @example
* api.logDebug('用户登录', { id: 1, name: 'test' })
* api.logDebug('请求开始', uri, params)
*/
logDebug(...args: any[]): void {
if (!this.debug) return
console.log('[DEBUG]', ...args)
}
/**
* 获取版本信息
*
* @param endpointName - 指定从哪个 endpoint 获取版本,默认使用 default endpoint
* @returns 版本信息对象
*
* @example
* // 从默认 endpoint 获取
* const versions = await api.getVersion()
*
* // 从指定 endpoint 获取
* const versions = await api.getVersion('backup')
*/
async getVersion(endpointName?: string): Promise<SimApiVersions> {
const versions: SimApiVersions = {
uiApp: '0.0.0-develop',
uiSimApi: typeof SimApiVersion === 'undefined' ? "0.0.0-develop" : SimApiVersion,
apiApp: '0.0.0',
apiSimApi: '0.0.0',
apiAppFull: '0.0.0',
apiSimApiFull: '0.0.0',
};
try {
const resp = await this.query<any>('/versions', {}, endpointName)
if (resp?.data) {
const d = resp.data
versions.apiApp= d.App?.split('+')[0] ?? '0.0.0';
versions.apiSimApi= d.SimApi?.split('+')[0] ?? '0.0.0';
versions.apiAppFull= d.App ?? '0.0.0';
versions.apiSimApiFull= d.SimApi ?? '0.0.0';
if (this.debug) {
console.log(`UI主应用版本: ${versions.uiApp}\nUISimApi版本: ${versions.uiSimApi}\nAPI主应用版本: ${versions.apiApp}\nAPISimApi版本: ${versions.apiSimApi}`)
}
return versions
}
} catch {
// 版本获取失败返回默认值
}
return versions;
}
async query<T = any>(
uri: string,
params: any = {},
endpointKey?: string,
extraHeaders?: Record<string, string>
): Promise<SimApiBaseResponse<T>> {
const headers: Record<string, string> = { ...extraHeaders, ...{} }
const queryId = this.genS4()
if (!(params instanceof FormData)) {
headers['Content-Type'] = 'application/json'
}
const token = this.getToken()
if (token) {
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 != 200 时抛出业务错误
if (processedData.code !== 200) {
throw processedData
}
return processedData
} catch (error: any) {
if (this.debug) {
console.log('[RESPONSE]', queryId, '->', error)
}
// 网络/HTTP 错误:包装成标准响应格式抛出
if (!error?.code) {
this.api.responseCallback.error(error)
throw {
code: -1,
message: error?.message || '网络错误',
data: error,
} as SimApiBaseResponse<T>
}
// 业务错误直接抛出
throw error
}
}
async login(request: Record<string, any>): Promise<SimApiBaseResponse<string>> {
const result = await this.query<string>(this.auth.login_url, request)
if (result?.data) {
this.setToken(result.data)
}
return result
}
async logout(url?: string | null): Promise<any> {
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)
}
}
}
+91
View File
@@ -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
View File
@@ -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) || ''
}
}
})
+64
View File
@@ -0,0 +1,64 @@
/**
* SimApi 类型定义
*/
/** 版本信息 */
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
auth?: Partial<SimApiAuthConfig>
api?: Partial<SimApiApiConfig>
}
/** SimApi 标准响应格式 */
export interface SimApiBaseResponse<T = any> {
code: number
message: string
data?: T
}
+22
View File
@@ -0,0 +1,22 @@
{
"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"],
"exclude": ["node_modules", "dist"]
}
+8 -5
View File
@@ -10,11 +10,14 @@
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
"noUnusedLocals": false,
"noUnusedParameters": false,
"noFallthroughCasesInSwitch": true,
"allowJs": true,
"checkJs": false,
"experimentalDecorators": true,
"emitDecoratorMetadata": true
},
"include": ["src/**/*.ts", "src/**/*.d.ts"]
"include": ["src/**/*.ts"]
}
+40 -16
View File
@@ -1,22 +1,46 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { defineConfig, loadEnv } from 'vite'
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'
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd())
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 由环境变量注入,支持:
// - VITE_SimApiVersion (Vite .env 文件)
// - npm_config_SimApiVersion (npm 构建时)
// - SimApiVersion (直接环境变量,如 GitHub Actions)
'SimApiVersion': JSON.stringify(
env.VITE_SimApiVersion ||
process.env.npm_config_SimApiVersion ||
process.env.SimApiVersion ||
'0.0.0-develop'
),
}
}
})