Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1597c98f39 | ||
|
|
7dbb48ad90 | ||
|
|
88af4a2d96 | ||
|
|
9e44c6fcaf | ||
|
|
592607f1e2 | ||
|
|
7aee4b708d | ||
|
|
1af5900593 | ||
|
|
78ef6d9ca0 | ||
|
|
cee0bc4cfe | ||
|
|
9a8510d833 | ||
|
|
ee034a7252 | ||
|
|
65bb076384 | ||
|
|
3ece81fc06 | ||
|
|
fa4032f239 | ||
|
|
713b8ae31e | ||
|
|
e0754ad635 | ||
|
|
39626de073 | ||
|
|
13c836b4ab | ||
|
|
88be4210aa | ||
|
|
4257f72799 | ||
|
|
46257ecf4a | ||
|
|
a2f57bc419 | ||
|
|
fe77c0ee1a | ||
|
|
98c46f4a7b | ||
|
|
ae742d452d | ||
|
|
29d3705073 | ||
|
|
df0d7a4fe3 | ||
|
|
d7aa0f171d | ||
|
|
b6d5a5dbe1 | ||
|
|
e91ddd36bf | ||
|
|
903f70b0ba | ||
|
|
c136375100 | ||
|
|
d44938e456 | ||
|
|
c100973590 | ||
|
|
957656ad5b |
@@ -3,31 +3,29 @@ name: Publish to npm
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
workflow_dispatch:
|
||||
- "*"
|
||||
permissions:
|
||||
id-token: write # Required for OIDC
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '20'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
node-version: "24"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
- name: Get version from tag
|
||||
id: version
|
||||
- name: Replace version placeholder
|
||||
run: |
|
||||
VERSION="${GITHUB_REF#refs/tags/}"
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Update package.json version
|
||||
run: npm pkg set version=${{ steps.version.outputs.version }}
|
||||
echo "Publishing version: $VERSION"
|
||||
# Update package.json
|
||||
npm pkg set version=$VERSION
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
@@ -37,11 +35,3 @@ jobs:
|
||||
|
||||
- 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 }}
|
||||
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/
|
||||
@@ -1,137 +1,580 @@
|
||||
# SimApi - 轻量 API 请求库
|
||||
# @simcu/simapi — SimApi Vue 前端库(AI 编码参考)
|
||||
|
||||
基于 Axios 的 Vue 3 + Pinia HTTP 客户端库。
|
||||
> **包名**: `@simcu/simapi` | **技术栈**: TypeScript + Vue3 + Pinia + 原生 fetch
|
||||
> **后端对应**: [simapi-net](../simapi-net)(`Simcu.SimApi` NuGet 包)
|
||||
> **性质**: simapi-net 的官方前端 HTTP 客户端,**专为其统一响应格式设计**
|
||||
>
|
||||
> **使用方式**: 将本文档作为上下文提供给 AI,或粘贴到对话开头。AI 阅读本文档后应能正确编写调用 simapi-net 接口的前端代码。
|
||||
|
||||
## 安装
|
||||
---
|
||||
|
||||
```bash
|
||||
npm install @simcu/simapi
|
||||
## 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. 安装依赖
|
||||
### 1.2 Token 认证
|
||||
|
||||
- 前端通过请求头 `Token: <value>` 传递认证令牌
|
||||
- Token 存储在 Cookie 中,key 默认为 `simapi-auth-token`
|
||||
- Cookie 属性: `path=/; secure; samesite=none`
|
||||
- **不使用 Authorization: Bearer**
|
||||
|
||||
### 1.3 请求方式
|
||||
|
||||
- **默认全部 POST**,body 为 JSON
|
||||
- 使用原生 fetch,不依赖 axios
|
||||
|
||||
---
|
||||
|
||||
## 2. 项目结构
|
||||
|
||||
```
|
||||
simapi-vue/
|
||||
├── src/
|
||||
│ ├── types.ts # 所有类型定义
|
||||
│ ├── simapi.core.ts # 纯 TS 核心(SimApiCore 类)
|
||||
│ └── simapi.pinia.ts # Vue3 Pinia Store 封装(useSimApi)
|
||||
├── dist/ # 构建产物(ESM)
|
||||
├── package.json # 包名 @simcu/simapi
|
||||
└── vite.config.ts # Vite 构建配置
|
||||
```
|
||||
|
||||
### 导出路径(Subpath Exports)
|
||||
|
||||
| 导入路径 | 内容 | 适用场景 |
|
||||
|---------|------|---------|
|
||||
| `@simcu/simapi` | SimApiCore + 类型 | 纯 TS / Node.js / 任意 JS 环境 |
|
||||
| `@simcu/simapi/pinia` | useSimApi Pinia Store | **Vue3 项目(推荐)** |
|
||||
|
||||
---
|
||||
|
||||
## 3. 快速开始(Vue3 项目标准用法)
|
||||
|
||||
### 3.1 安装
|
||||
|
||||
```bash
|
||||
npm install @simcu/simapi pinia
|
||||
```
|
||||
|
||||
## 快速开始
|
||||
> `pinia` 是 peerDependency,Vue3 项目必须安装。
|
||||
|
||||
### 1. 初始化配置
|
||||
### 3.2 第一步:public/config.json — 外部配置文件
|
||||
|
||||
在 Vue 应用入口(如 `main.ts` 或 `App.vue`)中配置端点和调试模式:
|
||||
在项目的 `public/` 目录下创建 `config.json`,定义 API 地址:
|
||||
|
||||
```json
|
||||
{
|
||||
"debug": true,
|
||||
"endpoints": {
|
||||
"default": "http://127.0.0.1:5210"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**为什么用 config.json 而不是硬编码?**
|
||||
- 前后端分离部署时,API 地址可能变化
|
||||
- `public/` 下的文件 Vite 会直接复制到输出目录,不经过构建
|
||||
- 打包后运维人员可以直接修改 `config.json` 切换环境,无需重新构建
|
||||
|
||||
**config.json 支持的字段:**
|
||||
|
||||
| 字段 | 类型 | 说明 | 默认值 |
|
||||
|------|------|------|--------|
|
||||
| `debug` | boolean | 是否打印请求/响应日志 | `false` |
|
||||
| `endpoints` | object | 多端点地址映射 | `{ "default": "" }` |
|
||||
| `defaultEndpoint` | string | 默认使用的端点名称 | `"default"` |
|
||||
|
||||
### 3.3 第二步:main.ts — 创建 Pinia 实例
|
||||
|
||||
```typescript
|
||||
import { useSimApi } from 'simapi'
|
||||
// src/main.ts
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia' // ← 必须安装并注册 Pinia
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
|
||||
const app = createApp(App)
|
||||
app.use(createPinia()) // ← useSimApi 依赖 Pinia,必须先注册
|
||||
app.use(router)
|
||||
app.mount('#app')
|
||||
```
|
||||
|
||||
**顺序很重要**: `createPinia()` 必须在 `useSimApi()` 调用之前完成注册。
|
||||
|
||||
### 3.4 第三步:App.vue — 初始化 SimApi 并设置回调
|
||||
|
||||
```vue
|
||||
<!-- src/App.vue -->
|
||||
<template>
|
||||
<router-view></router-view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useSimApi } from '@simcu/simapi/pinia' // ← 注意 /pinia 子路径
|
||||
import { onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const api = useSimApi()
|
||||
const router = useRouter()
|
||||
|
||||
// 设置 API 端点
|
||||
api.setEndpoints({
|
||||
default: 'https://api.example.com'
|
||||
onMounted(async () => {
|
||||
// ① 从 config.json 加载配置(endpoints、debug 等)
|
||||
await api.loadFromFile()
|
||||
|
||||
// ② 注册业务错误码回调 —— 401 时跳转登录页
|
||||
api.setBusinessCallback(401, () => {
|
||||
api.logout()
|
||||
router.replace({ path: '/login' })
|
||||
})
|
||||
|
||||
// 设置调试模式(默认 true)
|
||||
api.setDebug(true)
|
||||
```
|
||||
|
||||
### 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'
|
||||
// ③ 注册通用兜底回调 —— 其他所有非 200 错误统一提示
|
||||
api.setBusinessCallback('common', (data: any) => {
|
||||
console.error('[SimApi]', data.code, data.message)
|
||||
})
|
||||
|
||||
// 登出
|
||||
await api.logout()
|
||||
})
|
||||
</script>
|
||||
```
|
||||
|
||||
## API 参考
|
||||
**关键点说明:**
|
||||
|
||||
### 配置方法
|
||||
| 要点 | 说明 |
|
||||
|------|------|
|
||||
| `import from '@simcu/simapi/pinia'` | Vue3 项目**必须**用 `/pinia` 子路径导入 |
|
||||
| `onMounted` 中初始化 | 确保 DOM 已加载、Pinia 已就绪 |
|
||||
| `await api.loadFromFile()` | 从 `config.json` 加载 endpoints、defaultEndpoint、debug |
|
||||
| `setBusinessCallback(401, fn)` | 当后端返回 code=401 时自动执行 |
|
||||
| `setBusinessCallback('common', fn)` | 兜底回调,任何非 200 且未匹配其他回调时触发 |
|
||||
|
||||
| 方法 | 说明 |
|
||||
| ------------------------------------- | ---------------------------------------------------------- |
|
||||
| `setEndpoints(endpoints)` | 设置 API 端点,如 `{ default: 'https://api.example.com' }` |
|
||||
| `setDebug(debug: boolean)` | 开启/关闭调试模式 |
|
||||
| `setBusinessCallback(code, callback)` | 设置业务错误码回调 |
|
||||
### 3.5 第四步:在组件中使用
|
||||
|
||||
### 请求方法
|
||||
```vue
|
||||
<!-- src/views/UserList.vue -->
|
||||
<template>
|
||||
<div>
|
||||
<button @click="loadUsers">加载用户</button>
|
||||
<ul v-for="user in users" :key="user.id">{{ user.name }}</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
| 方法 | 说明 |
|
||||
| ------------------------ | -------------- |
|
||||
| `api.query(uri, params)` | 发起 POST 请求 |
|
||||
| `api.login(data)` | 登录 |
|
||||
| `api.logout()` | 登出 |
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useSimApi } from '@simcu/simapi/pinia'
|
||||
|
||||
### 工具方法
|
||||
interface User {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
|
||||
| 方法 | 说明 |
|
||||
| ------------------------ | -------------- |
|
||||
| `api.getEndpoint(name?)` | 获取端点地址 |
|
||||
| `api.getToken()` | 获取当前 Token |
|
||||
const api = useSimApi()
|
||||
const users = ref<User[]>([])
|
||||
|
||||
### Getters
|
||||
|
||||
| 属性 | 说明 |
|
||||
| ---------------- | -------------------- |
|
||||
| `api.token` | 当前 Token(响应式) |
|
||||
| `api.isLoggedIn` | 是否已登录(响应式) |
|
||||
|
||||
## 调试日志
|
||||
|
||||
启用调试模式后,控制台会输出:
|
||||
|
||||
```
|
||||
[REQUEST*] queryId -> /uri AUTH: token
|
||||
[RESPONSE] queryId -> {data: {...}, code: 200}
|
||||
async function loadUsers() {
|
||||
const res = await api.query<User[]>('/user/list', { page: 1 })
|
||||
users.value = res.data ?? []
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
设置端点后会自动打印版本信息:
|
||||
---
|
||||
|
||||
```
|
||||
UI主应用版本: 1.0.0
|
||||
UISimApi版本: 1.0.0
|
||||
API主应用版本: x.x.x
|
||||
APISimApi版本: x.x.x
|
||||
## 4. 完整项目模板(可直接复制使用)
|
||||
|
||||
### public/config.json
|
||||
|
||||
```json
|
||||
{
|
||||
"debug": true,
|
||||
"endpoints": {
|
||||
"default": "http://localhost:5000"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 业务错误处理
|
||||
### src/main.ts
|
||||
|
||||
```typescript
|
||||
const api = useSimApi()
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
|
||||
// 处理 401 未授权
|
||||
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(async () => {
|
||||
await api.loadFromFile()
|
||||
api.setBusinessCallback(401, () => {
|
||||
api.logout()
|
||||
router.replace('/login')
|
||||
})
|
||||
})
|
||||
</script>
|
||||
```
|
||||
|
||||
### src/views/Login.vue
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<form @submit.prevent="handleLogin">
|
||||
<input v-model="phone" placeholder="手机号" />
|
||||
<input v-model="code" placeholder="验证码" />
|
||||
<button type="submit">登录</button>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useSimApi } from '@simcu/simapi/pinia'
|
||||
|
||||
const api = useSimApi()
|
||||
const router = useRouter()
|
||||
const phone = ref('')
|
||||
const code = ref('')
|
||||
|
||||
async function handleLogin() {
|
||||
await api.login({ phone: phone.value, code: code.value })
|
||||
router.push('/')
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. API 参考
|
||||
|
||||
### 5.1 import 方式
|
||||
|
||||
```typescript
|
||||
// ✅ Vue3 项目 — 用 Pinia Store(推荐)
|
||||
import { useSimApi } from '@simcu/simapi/pinia'
|
||||
|
||||
// ✅ 非 Vue 项目 / 纯 TS — 用 Core 类
|
||||
import { SimApiCore } from '@simcu/simapi'
|
||||
```
|
||||
|
||||
### 5.2 useSimApi Store — 方法与属性一览
|
||||
|
||||
获取实例:
|
||||
|
||||
```typescript
|
||||
const api = useSimApi() // 单例模式,全局状态共享
|
||||
```
|
||||
|
||||
#### 属性(Getters)
|
||||
|
||||
| 属性 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `api.token` | `string` | 当前存储的 Token(只读) |
|
||||
| `api.isLoggedIn` | `boolean` | 是否已登录(Token 是否存在) |
|
||||
| `api.debug` | `boolean` | 调试开关 |
|
||||
| `api.api` | `SimApiApiConfig` | API 配置对象(一般不直接操作) |
|
||||
| `api.auth` | `SimApiAuthConfig` | 认证配置对象(一般不直接操作) |
|
||||
|
||||
#### 方法(Actions)
|
||||
|
||||
| 方法 | 参数 | 返回值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| `loadFromFile(file?)` | string (默认 `'config.json'`) | `Promise<void>` | 从 JSON 文件加载 endpoints/debug 配置 |
|
||||
| `configure(options)` | `SimApiOptions` | `void` | 手动配置(深合并) |
|
||||
| `setDebug(bool)` | boolean | `void` | 设置调试模式 |
|
||||
| `setEndpoints(map)` | `{[name]: url}` | `void` | 设置多端点映射 |
|
||||
| `query(uri, params?, endpoint?, headers?)` | 见下方详解 | `Promise<SimApiBaseResponse<T>>` | **核心方法:发送 POST 请求** |
|
||||
| `login(request)` | `{[key]: any}` | `Promise<SimApiBaseResponse<string>>` | 登录,成功后自动存 Token |
|
||||
| `logout(url?)` | string? | `Promise<any>` | 登出,清除 Token 并调后端接口 |
|
||||
| `checkLogin(url?)` | string? | `Promise<void>` | 检查登录态,过期则触发 401 回调 |
|
||||
| `setBusinessCallback(code, fn)` | number\|string, callback | `void` | 注册业务错误码回调 |
|
||||
| `getToken()` | 无 | `string` | 获取当前 Token |
|
||||
| `setToken(token)` | string | `void` | 手动设置 Token |
|
||||
| `removeToken()` | 无 | `void` | 清除 Token |
|
||||
| `getVersion(endpoint?)` | string? | `Promise<SimApiVersions>` | 获取前后端版本信息 |
|
||||
| `getEndpoint(name?)` | string? | `string` | 获取某端点的 baseURL |
|
||||
|
||||
### 5.3 query 方法详解
|
||||
|
||||
这是最核心的方法——几乎所有数据交互都通过它:
|
||||
|
||||
```typescript
|
||||
async function query<T = any>(
|
||||
uri: string, // 接口路径,如 '/user/list'
|
||||
params?: any = {}, // 请求体(POST body),JSON 对象
|
||||
endpointKey?: string, // 可选:指定端点名(默认用 default)
|
||||
extraHeaders?: Record<string, string> // 可选:额外请求头
|
||||
): Promise<SimApiBaseResponse<T>>
|
||||
```
|
||||
|
||||
**使用示例:**
|
||||
|
||||
```typescript
|
||||
// 基本查询
|
||||
const res = await api.query<User[]>('/user/list', { page: 1, count: 20 })
|
||||
console.log(res.data) // User[] 数组
|
||||
|
||||
// 错误处理
|
||||
try {
|
||||
const res = await api.query('/user/list')
|
||||
} catch (err: any) {
|
||||
// err 是 SimApiBaseResponse 类型
|
||||
console.log(err.code) // 业务错误码,如 400/401/403/500
|
||||
console.log(err.message) // 错误消息
|
||||
}
|
||||
```
|
||||
|
||||
**⚠️ query 的行为要点:**
|
||||
|
||||
1. 自动在请求头添加 `Token`(如果存在)
|
||||
2. `code === 200` → 正常返回 `SimApiBaseResponse<T>`
|
||||
3. `code !== 200` → 先执行对应的 businessCallback,然后 **throw 异常**
|
||||
4. 网络错误 → 执行 responseCallback.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' })
|
||||
|
||||
// logout:清除本地 Token,可选调后端登出接口
|
||||
await api.logout() // 调 /auth/logout
|
||||
await api.logout(null) // 只清本地 Token,不调后端
|
||||
```
|
||||
|
||||
### 5.5 setBusinessCallback — 业务错误处理
|
||||
|
||||
```typescript
|
||||
api.setBusinessCallback(401, (data) => {
|
||||
console.log('Token 过期', data)
|
||||
localStorage.removeItem('token')
|
||||
router.push('/login')
|
||||
router.replace('/login')
|
||||
})
|
||||
|
||||
// 处理 403 禁止访问
|
||||
api.setBusinessCallback(403, (data) => {
|
||||
ElMessage.error('没有权限')
|
||||
alert('无权限:' + data.message)
|
||||
})
|
||||
|
||||
// 处理所有非 200 错误
|
||||
// 兜底:任何未单独处理的非 200 错误都会走 common
|
||||
api.setBusinessCallback('common', (data) => {
|
||||
ElMessage.error(data.message)
|
||||
console.error('请求失败:', data.code, data.message)
|
||||
})
|
||||
```
|
||||
|
||||
**回调执行顺序:** 匹配具体错误码 → 未匹配则走 `'common'` → 然后 throw
|
||||
|
||||
---
|
||||
|
||||
## 6. 类型定义速查
|
||||
|
||||
```typescript
|
||||
/** 标准响应 */
|
||||
interface SimApiBaseResponse<T = any> {
|
||||
code: number
|
||||
message: string
|
||||
data?: T
|
||||
}
|
||||
|
||||
/** 版本信息 */
|
||||
interface SimApiVersions {
|
||||
uiApp: string
|
||||
uiSimApi: string
|
||||
apiApp: string
|
||||
apiSimApi: string
|
||||
apiAppFull: string
|
||||
apiSimApiFull: string
|
||||
}
|
||||
|
||||
/** 认证配置 */
|
||||
interface SimApiAuthConfig {
|
||||
token_name: string
|
||||
check_url: string
|
||||
logout_url: string
|
||||
login_url: string
|
||||
}
|
||||
|
||||
/** API 配置 */
|
||||
interface SimApiApiConfig {
|
||||
endpoints: { [name]: string }
|
||||
defaultEndpoint: string
|
||||
businessCallback: SimApiBusinessCallback
|
||||
responseCallback: SimApiResponseCallback
|
||||
timeout?: number
|
||||
}
|
||||
|
||||
/** 完整选项 */
|
||||
interface SimApiOptions {
|
||||
debug?: boolean
|
||||
auth?: Partial<SimApiAuthConfig>
|
||||
api?: Partial<SimApiApiConfig>
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 多端点支持
|
||||
|
||||
```json
|
||||
{
|
||||
"debug": true,
|
||||
"endpoints": {
|
||||
"default": "https://api.example.com",
|
||||
"admin": "https://admin.example.com",
|
||||
"cdn": "https://cdn.example.com"
|
||||
},
|
||||
"defaultEndpoint": "default"
|
||||
}
|
||||
```
|
||||
|
||||
```typescript
|
||||
// 使用默认端点
|
||||
await api.query('/user/list')
|
||||
|
||||
// 指定端点
|
||||
await api.query('/system/stats', {}, 'admin')
|
||||
```
|
||||
|
||||
也可以运行时动态添加:
|
||||
|
||||
```typescript
|
||||
api.setEndpoints({ backup: 'https://backup-api.example.com' })
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. configure — 手动完整配置
|
||||
|
||||
除了 `loadFromFile` 从 `config.json` 读取外,也可以手动配置一切:
|
||||
|
||||
```typescript
|
||||
api.configure({
|
||||
debug: false,
|
||||
auth: { token_name: 'my-app-token' },
|
||||
api: {
|
||||
endpoints: { default: 'https://api.example.com' },
|
||||
defaultEndpoint: 'default',
|
||||
timeout: 15000,
|
||||
businessCallback: {
|
||||
401: () => router.replace('/login'),
|
||||
403: (data) => alert('无权限'),
|
||||
'common': (data) => MessagePlugin.error(data.message),
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
**loadFromFile vs configure 的关系:**
|
||||
- `loadFromFile()` 只读 `config.json` 的 `endpoints`、`defaultEndpoint`、`debug`
|
||||
- `configure()` 可以覆盖所有字段,包括 auth 和 callbacks
|
||||
- 通常做法是:`loadFromFile()` 读基础配置 + `setBusinessCallback()` 补充回调
|
||||
|
||||
---
|
||||
|
||||
## 9. GOTCHAS(AI 最容易犯的错)
|
||||
|
||||
| ❌ 错误 | ✅ 正确 |
|
||||
|---------|---------|
|
||||
| `import { useSimApi } from '@simcu/simapi'` (Vue3) | `from '@simcu/simapi/pinia'`(必须带 `/pinia`) |
|
||||
| 忘记 `app.use(createPinia())` | **必须在 `useSimApi()` 之前**注册 Pinia |
|
||||
| 用 `Authorization: Bearer xxx` | 用 `Token` 请求头(这是 simapi-net 约定) |
|
||||
| 期望 HTTP 4xx/5xx 表示错误 | 所有错误都是 **HTTP 200 + JSON `code` 字段** |
|
||||
| `res.data` 直接用而不判空 | `res.data` 可能是 `undefined`,用 `res.data ?? []` |
|
||||
| 在 setup 外部调用 `useSimApi()` | `useSimApi()` 只能在 **setup 上下文**中调用 |
|
||||
| 用 Authorization Bearer 传 Token | Token 通过 **请求头 `Token`** + **Cookie** 存储 |
|
||||
| `new SimApiCore()` 在 Vue 项目里用 | Vue 项目统一用 `useSimApi()` Pinia Store |
|
||||
|
||||
---
|
||||
|
||||
## 10. 与 simapi-net 后端的对接约定
|
||||
|
||||
### 10.1 通信协议
|
||||
|
||||
```
|
||||
[Vue 前端] -- POST(JSON) --> [simapi-net 后端]
|
||||
Header: Token: <value>
|
||||
Body: { key: value }
|
||||
|
||||
[Vue 前端] <-- JSON {code, message, data} -- [simapi-net 后端]
|
||||
(HTTP Status 始终 200)
|
||||
```
|
||||
|
||||
### 10.2 内置路由对照表
|
||||
|
||||
| simapi-net 路由 | simapi-vue 方法 | 触发条件 |
|
||||
|-----------------|-----------------|----------|
|
||||
| `POST /auth/login` | `api.login(request)` | `EnableSimApiAuth = true` |
|
||||
| `POST /auth/check` | `api.checkLogin()` | `EnableSimApiAuth = true` |
|
||||
| `POST /auth/logout` | `api.logout()` | `EnableSimApiAuth = true` |
|
||||
| `POST /user/info` | `api.query('/user/info')` | `EnableSimApiAuth = true`(需登录) |
|
||||
| `GET /versions` | `api.getVersion()` | `EnableVersionUrl`(默认开启) |
|
||||
|
||||
---
|
||||
|
||||
## 11. 构建
|
||||
|
||||
```bash
|
||||
npm run dev # 开发模式
|
||||
npm run build # 生产构建
|
||||
```
|
||||
|
||||
构建产物位于 `dist/` 目录:
|
||||
- `dist/index.mjs` — 核心(SimApiCore)ESM 入口
|
||||
- `dist/pinia.mjs` — Pinia Store ESM 入口
|
||||
- `dist/*.d.ts` — TypeScript 类型声明
|
||||
|
||||
### 版本号注入
|
||||
|
||||
库使用 `declare const` 声明版本常量,构建时通过 Vite 的 `define` 注入。
|
||||
|
||||
**SimApiVersion** 由 simapi 库自身构建时从 `package.json` 注入。未配置时默认为 `0.0.0-develop`。
|
||||
|
||||
**AppVersion** 由调用方项目在自己的 `vite.config.ts` 中注入。
|
||||
|
||||
---
|
||||
|
||||
## 12. 从 axios 迁移
|
||||
|
||||
```diff
|
||||
- import axios from 'axios'
|
||||
- const res = await axios.post('/user/list', { page: 1 })
|
||||
|
||||
+ import { useSimApi } from '@simcu/simapi/pinia'
|
||||
+ const api = useSimApi()
|
||||
+ const res = await api.query('/user/list', { page: 1 })
|
||||
```
|
||||
|
||||
| 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
+799
-905
File diff suppressed because it is too large
Load Diff
+38
-32
@@ -1,46 +1,52 @@
|
||||
{
|
||||
"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"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^3.0.0",
|
||||
"pinia": "^2.0.0"
|
||||
"./pinia": {
|
||||
"import": "./dist/pinia.mjs",
|
||||
"types": "./dist/simapi.pinia.d.ts"
|
||||
}
|
||||
},
|
||||
"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",
|
||||
"@types/node": "^25.6.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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,346 @@
|
||||
/**
|
||||
* 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;
|
||||
declare const AppVersion: 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 {
|
||||
private debug: boolean = true
|
||||
private auth: SimApiAuthConfig = {
|
||||
token_name: 'simapi-auth-token',
|
||||
check_url: '/user/info',
|
||||
logout_url: '/auth/logout',
|
||||
login_url: '/auth/login',
|
||||
tokenStore: "localstorage"
|
||||
}
|
||||
private webConfig: Map<string, Record<string, object>> = new Map();
|
||||
|
||||
private api: SimApiApiConfig = {
|
||||
endpoints: {default: ''},
|
||||
defaultEndpoint: 'default',
|
||||
businessCallback: {
|
||||
401: () => this.removeToken(),
|
||||
common: () => {
|
||||
},
|
||||
},
|
||||
responseCallback: {
|
||||
success: (response: any) => response,
|
||||
error: (_err: any) => {
|
||||
},
|
||||
},
|
||||
timeout: 10000
|
||||
}
|
||||
|
||||
constructor(options?: SimApiOptions) {
|
||||
if (options) {
|
||||
this.configure(options)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 JSON 文件加载配置
|
||||
*
|
||||
* 部署时替换 config.json 即可切换环境,无需重新构建。
|
||||
*
|
||||
* @param file - 配置文件路径,默认 'config.json'
|
||||
* @example
|
||||
* await api.loadFromFile()
|
||||
* await api.loadFromFile('/env/prod.json')
|
||||
*/
|
||||
async loadFromFile(file: string = 'config.json'): Promise<void> {
|
||||
try {
|
||||
const resp = await fetch(file)
|
||||
if (!resp.ok) {
|
||||
this.logDebug(`配置文件 ${file} 加载失败: HTTP ${resp.status}`)
|
||||
return
|
||||
}
|
||||
const config = await resp.json()
|
||||
if (config.debug !== undefined) {
|
||||
this.debug = config.debug
|
||||
}
|
||||
if (config.endpoints) {
|
||||
this.api.endpoints = {...this.api.endpoints, ...config.endpoints}
|
||||
}
|
||||
if (config.defaultEndpoint) {
|
||||
this.api.defaultEndpoint = config.defaultEndpoint
|
||||
}
|
||||
} catch (err: any) {
|
||||
this.logDebug(`配置文件 ${file} 加载失败: ${err.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
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 ?? {})},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get isDebug() {
|
||||
return this.debug
|
||||
}
|
||||
|
||||
setDebug(debug: boolean) {
|
||||
this.debug = debug;
|
||||
}
|
||||
|
||||
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 {
|
||||
if (this.auth.tokenStore === 'localstorage') {
|
||||
return localStorage.getItem(this.auth.token_name) ?? ''
|
||||
}
|
||||
const name = this.auth.token_name
|
||||
const match = document.cookie.match(new RegExp(`(?:^|;)\\s?${name}=([^;]+)`))
|
||||
return match ? match[1] : ''
|
||||
}
|
||||
|
||||
setToken(token: string): void {
|
||||
if (this.auth.tokenStore === 'localstorage') {
|
||||
localStorage.setItem(this.auth.token_name, token)
|
||||
} else {
|
||||
const name = this.auth.token_name
|
||||
document.cookie = `${name}=${token}; path=/; max-age=315360000; secure; samesite=none`
|
||||
}
|
||||
}
|
||||
|
||||
removeToken(): void {
|
||||
if (this.auth.tokenStore === 'localstorage') {
|
||||
localStorage.removeItem(this.auth.token_name)
|
||||
} else {
|
||||
const name = this.auth.token_name
|
||||
document.cookie = `${name}=; path=/; max-age=0; secure; samesite=none`
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
genS4(): string {
|
||||
return (((1 + Math.random()) * 0x10000 * Date.parse(new Date().toString())) | 0)
|
||||
.toString(16)
|
||||
.substring(1)
|
||||
}
|
||||
|
||||
/**
|
||||
* 日志工具(仅在 debug 模式下输出)
|
||||
*
|
||||
* @example
|
||||
* api.logDebug('用户登录', { id: 1, name: 'test' })
|
||||
* api.logDebug('请求开始', uri, params)
|
||||
*/
|
||||
logDebug(...args: any[]): void {
|
||||
if (!this.debug) return
|
||||
console.log('[DEBUG]', ...args)
|
||||
}
|
||||
|
||||
async getConfig(reload: boolean = false, endpointName: string = 'default'): Promise<Record<string, object>> {
|
||||
if (!this.webConfig.has(endpointName) || reload) {
|
||||
const versions: SimApiVersions = {
|
||||
uiApp: typeof AppVersion === 'undefined' ? "0.0.0-develop" : AppVersion,
|
||||
uiSimApi: typeof SimApiVersion === 'undefined' ? "0.0.0-develop" : SimApiVersion,
|
||||
apiApp: '0.0.0',
|
||||
apiSimApi: '0.0.0',
|
||||
apiAppFull: '0.0.0',
|
||||
apiSimApiFull: '0.0.0',
|
||||
};
|
||||
const resp = await this.query<any>('/config', {}, endpointName)
|
||||
if (resp?.data) {
|
||||
const d = resp.data.Versions;
|
||||
versions.apiApp = d.App?.split('+')[0] ?? '0.0.0';
|
||||
versions.apiSimApi = d.SimApi?.split('+')[0] ?? '0.0.0';
|
||||
versions.apiAppFull = d.App ?? '0.0.0';
|
||||
versions.apiSimApiFull = d.SimApi ?? '0.0.0';
|
||||
if (this.debug) {
|
||||
console.log(`UI主应用版本: ${versions.uiApp}\nUISimApi版本: ${versions.uiSimApi}\nAPI主应用版本: ${versions.apiApp}\nAPISimApi版本: ${versions.apiSimApi}`)
|
||||
}
|
||||
const conf = JSON.parse(JSON.stringify(resp.data));
|
||||
conf.Versions = versions;
|
||||
this.webConfig.set(endpointName, conf);
|
||||
}
|
||||
}
|
||||
return this.webConfig.get(endpointName)!;
|
||||
}
|
||||
|
||||
async query<T = any>(
|
||||
uri: string,
|
||||
params: any = {},
|
||||
endpointKey?: string,
|
||||
extraHeaders?: Record<string, string>,
|
||||
selfHandleError: boolean = false
|
||||
): Promise<SimApiBaseResponse<T>> {
|
||||
const headers: Record<string, string> = {...extraHeaders, ...{}}
|
||||
const queryId = this.genS4()
|
||||
|
||||
if (!(params instanceof FormData)) {
|
||||
headers['Content-Type'] = 'application/json'
|
||||
}
|
||||
|
||||
const token = this.getToken()
|
||||
if (token) {
|
||||
headers['Token'] = token
|
||||
}
|
||||
|
||||
if (this.debug) {
|
||||
headers['Query-Id'] = queryId
|
||||
console.log('[REQUEST*]', queryId, '->', uri, 'AUTH:', this.getToken())
|
||||
}
|
||||
|
||||
const url = this.getEndpoint(endpointKey) + uri
|
||||
|
||||
try {
|
||||
const respData = await fetchPost<T>(
|
||||
url,
|
||||
params,
|
||||
headers,
|
||||
this.api.timeout ?? 10000
|
||||
)
|
||||
if (this.debug) {
|
||||
console.log('[RESPONSE]', queryId, '->', respData)
|
||||
}
|
||||
const processedData = this.api.responseCallback.success(respData) as SimApiBaseResponse<T>
|
||||
|
||||
// 业务回调处理
|
||||
if (!selfHandleError) {
|
||||
if (this.api.businessCallback.hasOwnProperty(processedData.code)) {
|
||||
this.api.businessCallback[processedData.code](processedData)
|
||||
} else if (this.api.businessCallback['common'] && processedData.code !== 200) {
|
||||
this.api.businessCallback['common'](processedData)
|
||||
}
|
||||
}
|
||||
|
||||
// code != 200 时抛出业务错误
|
||||
if (processedData.code !== 200) {
|
||||
throw processedData
|
||||
}
|
||||
return processedData
|
||||
|
||||
} catch (error: any) {
|
||||
if (this.debug) {
|
||||
console.log('[RESPONSE]', queryId, '->', error)
|
||||
}
|
||||
// 网络/HTTP 错误:包装成标准响应格式抛出
|
||||
if (!error?.code) {
|
||||
this.api.responseCallback.error(error)
|
||||
throw {
|
||||
code: -1,
|
||||
message: error?.message || '网络错误',
|
||||
data: error,
|
||||
} as SimApiBaseResponse<T>
|
||||
}
|
||||
// 业务错误直接抛出
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async login(request: Record<string, any>): Promise<SimApiBaseResponse<string>> {
|
||||
const result = await this.query<string>(this.auth.login_url, request)
|
||||
if (result?.data) {
|
||||
this.setToken(result.data)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
async logout(url?: string | null): Promise<any> {
|
||||
if (url !== null) {
|
||||
this.query(url ?? this.auth.logout_url).catch(() => true)
|
||||
}
|
||||
this.removeToken()
|
||||
return true
|
||||
}
|
||||
|
||||
async checkLogin(url?: string | null): Promise<any> {
|
||||
return this.query(url ?? this.auth.check_url);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
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: {
|
||||
IsDebug: state => state._core.isDebug
|
||||
},
|
||||
actions: {
|
||||
// 所有方法直接代理到 core
|
||||
async loadFromFile(file: string = '/config.json'): Promise<void> {
|
||||
return this._core.loadFromFile(file)
|
||||
},
|
||||
|
||||
configure(options: SimApiOptions): void {
|
||||
this._core.configure(options)
|
||||
},
|
||||
|
||||
setDebug(debug: boolean): void {
|
||||
this._core.setDebug(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>,
|
||||
selfHandleError: boolean = false
|
||||
): Promise<SimApiBaseResponse<T>> {
|
||||
return this._core.query<T>(uri, params, endpointKey, extraHeaders, selfHandleError)
|
||||
},
|
||||
|
||||
getEndpoint(name?: string): string {
|
||||
return this._core.getEndpoint(name)
|
||||
},
|
||||
|
||||
async getConfig(reload = false, endpointName?: string): Promise<Record<string, object>> {
|
||||
return this._core.getConfig(reload, 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,66 @@
|
||||
/**
|
||||
* 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
|
||||
/** Token 存储位置 */
|
||||
tokenStore: 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
|
||||
}
|
||||
@@ -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
@@ -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"]
|
||||
}
|
||||
|
||||
+23
-9
@@ -1,22 +1,36 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
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/index.ts',
|
||||
entry: ['src/simapi.core.ts', 'src/simapi.pinia.ts'],
|
||||
name: 'SimApi',
|
||||
formats: ['es', 'umd'],
|
||||
fileName: (format) => `simapi.${format}.${format === 'es' ? 'js' : 'cjs'}`
|
||||
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'],
|
||||
external: ['vue', 'pinia'],
|
||||
output: {
|
||||
globals: {
|
||||
vue: 'Vue'
|
||||
vue: 'Vue',
|
||||
pinia: 'Pinia'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
define: {
|
||||
SimApiVersion: JSON.stringify(pkg.version)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user