Compare commits

...
15 Commits
Author SHA1 Message Date
xrain 88af4a2d96 fix config
Publish to npm / publish (push) Failing after 10s
2026-08-10 21:03:44 +08:00
xrain 9e44c6fcaf fix config
Publish to npm / publish (push) Failing after 10s
2026-08-10 21:01:21 +08:00
xrain 592607f1e2 fix config
Publish to npm / publish (push) Failing after 8s
2026-08-10 20:46:40 +08:00
xrain 7aee4b708d fix config
Publish to npm / publish (push) Failing after 8s
2026-08-10 20:44:33 +08:00
xrain 1af5900593 fix config
Publish to npm / publish (push) Failing after 8s
2026-08-10 20:37:23 +08:00
xrain 78ef6d9ca0 fix cookie
Publish to npm / publish (push) Failing after 7s
2026-05-05 07:16:29 +08:00
xrain cee0bc4cfe fix cookie
Publish to npm / publish (push) Failing after 7s
2026-05-05 07:06:34 +08:00
xrain 9a8510d833 fix cookie
Publish to npm / publish (push) Failing after 5s
2026-05-05 07:01:21 +08:00
xrain ee034a7252 cookie过期时间设置
Publish to npm / publish (push) Failing after 19s
2026-05-05 06:51:07 +08:00
xrain 65bb076384 checklogin现在返回用户信息
Publish to npm / publish (push) Failing after 19s
2026-05-04 23:28:35 +08:00
xrain 3ece81fc06 使用JSON文件初始化
Publish to npm / publish (push) Failing after 11s
2026-05-04 19:56:48 +08:00
xrain fa4032f239 token存储转移到cookie
Publish to npm / publish (push) Failing after 9s
2026-05-04 16:57:08 +08:00
xrain 713b8ae31e 取消了一些乱七八糟不同步的方法
Publish to npm / publish (push) Failing after 10s
2026-05-04 03:32:05 +08:00
xrain e0754ad635 query 增加了自处理异常
Publish to npm / publish (push) Failing after 8s
2026-05-04 02:27:43 +08:00
xrain 39626de073 修复了退出登录 TOKEN没清空的问题
Publish to npm / publish (push) Failing after 14s
2026-05-04 00:15:23 +08:00
3 changed files with 211 additions and 385 deletions
+74 -246
View File
@@ -32,8 +32,9 @@ simapi-net 所有接口的响应格式固定如下(HTTP 状态码始终 200)
### 1.2 Token 认证 ### 1.2 Token 认证
- 前端通过请求头 `Token: <value>` 传递认证令牌 - 前端通过请求头 `Token: <value>` 传递认证令牌
- Token 存储在 localStoragekey 默认为 `simapi-auth-token` - Token 存储在 Cookie 中key 默认为 `simapi-auth-token`
- **不使用 Cookie / 不使用 Authorization: Bearer** - Cookie 属性: `path=/; secure; samesite=none`
- **不使用 Authorization: Bearer**
### 1.3 请求方式 ### 1.3 请求方式
@@ -74,57 +75,33 @@ npm install @simcu/simapi pinia
> `pinia` 是 peerDependencyVue3 项目必须安装。 > `pinia` 是 peerDependencyVue3 项目必须安装。
### 3.2 第一步:public/config.js — 外部配置文件 ### 3.2 第一步:public/config.json — 外部配置文件
在项目的 `public/` 目录下创建 `config.js`,定义 API 地址: 在项目的 `public/` 目录下创建 `config.json`,定义 API 地址:
```javascript ```json
// public/config.js {
window.simapi = { "debug": true,
debug: true, "endpoints": {
endpoints: { "default": "http://127.0.0.1:5210"
default: "http://127.0.0.1:5210" // 后端 simapi-net 服务地址
// default: "https://api.example.com" // 生产环境
} }
} }
``` ```
**为什么用 config.js 而不是硬编码?** **为什么用 config.json 而不是硬编码?**
- 前后端分离部署时,API 地址可能变化 - 前后端分离部署时,API 地址可能变化
- `public/` 下的文件 Vite 会直接复制到输出目录,不经过构建 - `public/` 下的文件 Vite 会直接复制到输出目录,不经过构建
- 打包后运维人员可以直接修改 `config.js` 切换环境,无需重新构建 - 打包后运维人员可以直接修改 `config.json` 切换环境,无需重新构建
**config.js 支持的字段:** **config.json 支持的字段:**
| 字段 | 类型 | 说明 | 默认值 | | 字段 | 类型 | 说明 | 默认值 |
|------|------|------|--------| |------|------|------|--------|
| `debug` | boolean | 是否打印请求/响应日志 | `false` | | `debug` | boolean | 是否打印请求/响应日志 | `false` |
| `endpoints` | object | 多端点地址映射 | `{ default: '' }` | | `endpoints` | object | 多端点地址映射 | `{ "default": "" }` |
| `defaultEndpoint` | string | 默认使用的端点名称 | `'default'` | | `defaultEndpoint` | string | 默认使用的端点名称 | `"default"` |
### 3.3 第二步:index.html — 引入 config.js ### 3.3 第二步:main.ts — 创建 Pinia 实例
```html
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My App</title>
<!-- ⬇️ 在这里引入 config.js,必须在 main.ts 之前加载 -->
<script src="/config.js"></script>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
```
> `<script src="/config.js">` 不加 `type="module"`,确保它在所有模块之前同步执行,设置好 `window.simapi`。
### 3.4 第三步:main.ts — 创建 Pinia 实例
```typescript ```typescript
// src/main.ts // src/main.ts
@@ -136,13 +113,12 @@ import router from './router'
const app = createApp(App) const app = createApp(App)
app.use(createPinia()) // ← useSimApi 依赖 Pinia,必须先注册 app.use(createPinia()) // ← useSimApi 依赖 Pinia,必须先注册
app.use(router) app.use(router)
// app.use(其他插件)
app.mount('#app') app.mount('#app')
``` ```
**顺序很重要**: `createPinia()` 必须在 `useSimApi()` 调用之前完成注册。 **顺序很重要**: `createPinia()` 必须在 `useSimApi()` 调用之前完成注册。
### 3.5步:App.vue — 初始化 SimApi 并设置回调 ### 3.4步:App.vue — 初始化 SimApi 并设置回调
```vue ```vue
<!-- src/App.vue --> <!-- src/App.vue -->
@@ -155,13 +131,12 @@ import { useSimApi } from '@simcu/simapi/pinia' // ← 注意 /pinia 子路径
import { onMounted } from 'vue' import { onMounted } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
// 获取 api 实例(单例,全局共享同一状态)
const api = useSimApi() const api = useSimApi()
const router = useRouter() const router = useRouter()
onMounted(() => { onMounted(async () => {
// ① 从 window.simapi 读取配置(endpoints、debug 等) // ① 从 config.json 加载配置(endpoints、debug 等)
api.autoInit() await api.loadFromFile()
// ② 注册业务错误码回调 —— 401 时跳转登录页 // ② 注册业务错误码回调 —— 401 时跳转登录页
api.setBusinessCallback(401, () => { api.setBusinessCallback(401, () => {
@@ -172,8 +147,6 @@ onMounted(() => {
// ③ 注册通用兜底回调 —— 其他所有非 200 错误统一提示 // ③ 注册通用兜底回调 —— 其他所有非 200 错误统一提示
api.setBusinessCallback('common', (data: any) => { api.setBusinessCallback('common', (data: any) => {
console.error('[SimApi]', data.code, data.message) console.error('[SimApi]', data.code, data.message)
// 如果用了 UI 组件库可以在这里弹提示:
// MessagePlugin.error({ content: data.message })
}) })
}) })
</script> </script>
@@ -184,12 +157,12 @@ onMounted(() => {
| 要点 | 说明 | | 要点 | 说明 |
|------|------| |------|------|
| `import from '@simcu/simapi/pinia'` | Vue3 项目**必须**用 `/pinia` 子路径导入 | | `import from '@simcu/simapi/pinia'` | Vue3 项目**必须**用 `/pinia` 子路径导入 |
| `onMounted` 中初始化 | 确保 DOM 已加载、config.js 已执行、Pinia 已就绪 | | `onMounted` 中初始化 | 确保 DOM 已加载、Pinia 已就绪 |
| `autoInit()` | 读取 `window.simapi``endpoints``defaultEndpoint``debug` | | `await api.loadFromFile()` | `config.json` 加载 endpointsdefaultEndpointdebug |
| `setBusinessCallback(401, fn)` | 当后端返回 code=401 时自动执行 | | `setBusinessCallback(401, fn)` | 当后端返回 code=401 时自动执行 |
| `setBusinessCallback('common', fn)` | 兜底回调,任何非 200 且未匹配其他回调时触发 | | `setBusinessCallback('common', fn)` | 兜底回调,任何非 200 且未匹配其他回调时触发 |
### 3.6步:在组件中使用 ### 3.5步:在组件中使用
```vue ```vue
<!-- src/views/UserList.vue --> <!-- src/views/UserList.vue -->
@@ -213,7 +186,6 @@ const api = useSimApi()
const users = ref<User[]>([]) const users = ref<User[]>([])
async function loadUsers() { async function loadUsers() {
// query 返回 Promise<SimApiBaseResponse<T>>code !== 200 时会抛出异常
const res = await api.query<User[]>('/user/list', { page: 1 }) const res = await api.query<User[]>('/user/list', { page: 1 })
users.value = res.data ?? [] users.value = res.data ?? []
} }
@@ -224,55 +196,17 @@ async function loadUsers() {
## 4. 完整项目模板(可直接复制使用) ## 4. 完整项目模板(可直接复制使用)
以下是一个完整的 Vue3 + simapi-vue 项目初始化清单: ### public/config.json
### 文件清单 ```json
{
``` "debug": true,
my-project/ "endpoints": {
├── public/ "default": "http://localhost:5000"
│ └── config.js # ← API 配置(第 1 步)
├── index.html # ← 引入 config.js(第 2 步)
├── src/
│ ├── main.ts # ← 注册 Pinia(第 3 步)
│ ├── App.vue # ← 初始化 SimApi(第 4 步)
│ ├── router/
│ │ └── index.ts
│ └── views/
│ └── Login.vue # ← 使用示例
├── package.json # ← 依赖
└── vite.config.ts
```
### public/config.js
```javascript
window.simapi = {
debug: true,
endpoints: {
default: "http://localhost:5000"
} }
} }
``` ```
### index.html
```html
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My App</title>
<script src="/config.js"></script>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
```
### src/main.ts ### src/main.ts
```typescript ```typescript
@@ -300,8 +234,8 @@ import { useRouter } from 'vue-router'
const api = useSimApi() const api = useSimApi()
const router = useRouter() const router = useRouter()
onMounted(() => { onMounted(async () => {
api.autoInit() await api.loadFromFile()
api.setBusinessCallback(401, () => { api.setBusinessCallback(401, () => {
api.logout() api.logout()
router.replace('/login') router.replace('/login')
@@ -333,7 +267,7 @@ const code = ref('')
async function handleLogin() { async function handleLogin() {
await api.login({ phone: phone.value, code: code.value }) await api.login({ phone: phone.value, code: code.value })
router.push('/') // 登录成功自动存 Token,然后跳转首页 router.push('/')
} }
</script> </script>
``` ```
@@ -374,7 +308,7 @@ const api = useSimApi() // 单例模式,全局状态共享
| 方法 | 参数 | 返回值 | 说明 | | 方法 | 参数 | 返回值 | 说明 |
|------|------|--------|------| |------|------|--------|------|
| `autoInit()` | 无 | `void` | `window.simapi` 读取 endpoints/debug 配置 | | `loadFromFile(file?)` | string (默认 `'config.json'`) | `Promise<void>` | 从 JSON 文件加载 endpoints/debug 配置 |
| `configure(options)` | `SimApiOptions` | `void` | 手动配置(深合并) | | `configure(options)` | `SimApiOptions` | `void` | 手动配置(深合并) |
| `setDebug(bool)` | boolean | `void` | 设置调试模式 | | `setDebug(bool)` | boolean | `void` | 设置调试模式 |
| `setEndpoints(map)` | `{[name]: url}` | `void` | 设置多端点映射 | | `setEndpoints(map)` | `{[name]: url}` | `void` | 设置多端点映射 |
@@ -409,15 +343,6 @@ async function query<T = any>(
const res = await api.query<User[]>('/user/list', { page: 1, count: 20 }) const res = await api.query<User[]>('/user/list', { page: 1, count: 20 })
console.log(res.data) // User[] 数组 console.log(res.data) // User[] 数组
// 带参数
const res = await api.query<User>('/user/detail', { id: 'xxx' })
// 指定端点
await api.query('/admin/stats', {}, 'admin')
// 自定义请求头
await api.query('/file/upload', formData, undefined, { 'Content-Type': 'multipart/form-data' })
// 错误处理 // 错误处理
try { try {
const res = await api.query('/user/list') const res = await api.query('/user/list')
@@ -425,7 +350,6 @@ try {
// err 是 SimApiBaseResponse 类型 // err 是 SimApiBaseResponse 类型
console.log(err.code) // 业务错误码,如 400/401/403/500 console.log(err.code) // 业务错误码,如 400/401/403/500
console.log(err.message) // 错误消息 console.log(err.message) // 错误消息
console.log(err.data) // 可能携带的错误详情
} }
``` ```
@@ -442,8 +366,6 @@ try {
```typescript ```typescript
// loginPOST 到 auth.login_url(默认 /auth/login),自动保存返回的 Token // loginPOST 到 auth.login_url(默认 /auth/login),自动保存返回的 Token
await api.login({ phone: '13800138000', code: '123456' }) await api.login({ phone: '13800138000', code: '123456' })
// 后端返回 { code: 200, data: "token-string" }
// 前端自动将 data 存入 localStorage
// logout:清除本地 Token,可选调后端登出接口 // logout:清除本地 Token,可选调后端登出接口
await api.logout() // 调 /auth/logout await api.logout() // 调 /auth/logout
@@ -453,9 +375,7 @@ await api.logout(null) // 只清本地 Token,不调后端
### 5.5 setBusinessCallback — 业务错误处理 ### 5.5 setBusinessCallback — 业务错误处理
```typescript ```typescript
// 针对特定错误码
api.setBusinessCallback(401, (data) => { api.setBusinessCallback(401, (data) => {
console.log('未授权', data.message)
router.replace('/login') router.replace('/login')
}) })
@@ -478,36 +398,36 @@ api.setBusinessCallback('common', (data) => {
```typescript ```typescript
/** 标准响应 */ /** 标准响应 */
interface SimApiBaseResponse<T = any> { interface SimApiBaseResponse<T = any> {
code: number // 200=成功,其他=业务错误码 code: number
message: string // 提示信息 message: string
data?: T // 业务数据 data?: T
} }
/** 版本信息 */ /** 版本信息 */
interface SimApiVersions { interface SimApiVersions {
uiApp: string // 前端应用版本 uiApp: string
uiSimApi: string // 前端 SimApi 版本 uiSimApi: string
apiApp: string // 后端应用版本(简化版) apiApp: string
apiSimApi: string // 后端 SimApi 版本(简化版) apiSimApi: string
apiAppFull: string // 后端应用版本(完整版) apiAppFull: string
apiSimApiFull: string // 后端 SimApi 版本(完整版) apiSimApiFull: string
} }
/** 认证配置 */ /** 认证配置 */
interface SimApiAuthConfig { interface SimApiAuthConfig {
token_name: string // localStorage key,默认 'simapi-auth-token' token_name: string
check_url: string // 登录检查接口,默认 '/auth/check' check_url: string
logout_url: string // 登出接口,默认 '/auth/logout' logout_url: string
login_url: string // 登录接口,默认 '/auth/login' login_url: string
} }
/** API 配置 */ /** API 配置 */
interface SimApiApiConfig { interface SimApiApiConfig {
endpoints: { [name]: string } // 多端点映射 endpoints: { [name]: string }
defaultEndpoint: string // 默认端点 defaultEndpoint: string
businessCallback: SimApiBusinessCallback // 错误码回调 businessCallback: SimApiBusinessCallback
responseCallback: SimApiResponseCallback // 响应拦截器 responseCallback: SimApiResponseCallback
timeout?: number // 超时毫秒数,默认 10000 timeout?: number
} }
/** 完整选项 */ /** 完整选项 */
@@ -522,27 +442,24 @@ interface SimApiOptions {
## 7. 多端点支持 ## 7. 多端点支持
适用于需要连接多个后端服务的场景: ```json
{
```javascript "debug": true,
// config.js "endpoints": {
window.simapi = { "default": "https://api.example.com",
debug: true, "admin": "https://admin.example.com",
endpoints: { "cdn": "https://cdn.example.com"
default: 'https://api.example.com', // 主服务
admin: 'https://admin.example.com', // 管理后台服务
cdn: 'https://cdn.example.com', // CDN/文件服务
}, },
defaultEndpoint: 'default' "defaultEndpoint": "default"
} }
``` ```
```typescript ```typescript
// 使用默认端点 // 使用默认端点
await api.query('/user/list') // → https://api.example.com/user/list await api.query('/user/list')
// 指定端点 // 指定端点
await api.query('/system/stats', {}, 'admin') // → https://admin.example.com/system/stats await api.query('/system/stats', {}, 'admin')
``` ```
也可以运行时动态添加: 也可以运行时动态添加:
@@ -555,45 +472,29 @@ api.setEndpoints({ backup: 'https://backup-api.example.com' })
## 8. configure — 手动完整配置 ## 8. configure — 手动完整配置
除了 autoInit 从 `window.simapi` 读取外,也可以手动配置一切: 除了 `loadFromFile``config.json` 读取外,也可以手动配置一切:
```typescript ```typescript
api.configure({ api.configure({
debug: false, debug: false,
auth: { token_name: 'my-app-token' },
auth: {
token_name: 'my-app-token',
check_url: '/auth/check',
logout_url: '/auth/logout',
login_url: '/auth/login',
},
api: { api: {
endpoints: { endpoints: { default: 'https://api.example.com' },
default: 'https://api.example.com',
},
defaultEndpoint: 'default', defaultEndpoint: 'default',
timeout: 15000, timeout: 15000,
businessCallback: { businessCallback: {
401: () => router.replace('/login'), 401: () => router.replace('/login'),
403: (data) => alert('无权限'), 403: (data) => alert('无权限'),
500: (data) => console.error('服务器错误', data),
'common': (data) => MessagePlugin.error(data.message), 'common': (data) => MessagePlugin.error(data.message),
}, },
responseCallback: {
success: (res) => res, // 成功响应拦截(可做数据转换)
error: (err) => console.error('网络错误', err),
},
}, },
}) })
``` ```
**configure vs autoInit 的关系:** **loadFromFile vs configure 的关系:**
- `autoInit()` 只读 `window.simapi``endpoints``defaultEndpoint``debug` - `loadFromFile()` 只读 `config.json``endpoints``defaultEndpoint``debug`
- `configure()` 可以覆盖所有字段,包括 auth 和 callbacks - `configure()` 可以覆盖所有字段,包括 auth 和 callbacks
- 通常做法是:`autoInit()` 读基础配置 + `setBusinessCallback()` 补充回调 - 通常做法是:`loadFromFile()` 读基础配置 + `setBusinessCallback()` 补充回调
--- ---
@@ -603,13 +504,11 @@ api.configure({
|---------|---------| |---------|---------|
| `import { useSimApi } from '@simcu/simapi'` Vue3 | `from '@simcu/simapi/pinia'`(必须带 `/pinia` | | `import { useSimApi } from '@simcu/simapi'` Vue3 | `from '@simcu/simapi/pinia'`(必须带 `/pinia` |
| 忘记 `app.use(createPinia())` | **必须在 `useSimApi()` 之前**注册 Pinia | | 忘记 `app.use(createPinia())` | **必须在 `useSimApi()` 之前**注册 Pinia |
| `config.js` 放在 `src/` 下 | 必须放在 `public/` 下,Vite 才能原样复制 |
| `index.html` 中不引 config.js 或放在 main.ts 之后 | config.js 必须**同步加载**且在模块代码之前执行 |
| 用 `Authorization: Bearer xxx` | 用 `Token` 请求头(这是 simapi-net 约定) | | 用 `Authorization: Bearer xxx` | 用 `Token` 请求头(这是 simapi-net 约定) |
| 期望 HTTP 4xx/5xx 表示错误 | 所有错误都是 **HTTP 200 + JSON `code` 字段** | | 期望 HTTP 4xx/5xx 表示错误 | 所有错误都是 **HTTP 200 + JSON `code` 字段** |
| `res.data` 直接用而不判空 | `res.data` 可能是 `undefined`,用 `res.data ?? []``res.data!` | | `res.data` 直接用而不判空 | `res.data` 可能是 `undefined`,用 `res.data ?? []` |
| 在 setup 外部调用 `useSimApi()` | `useSimApi()` 只能在 **setup 上下文**(或 pinia active中调用 | | 在 setup 外部调用 `useSimApi()` | `useSimApi()` 只能在 **setup 上下文**中调用 |
| 用 Cookie 传 Token | Token 通过 **请求头 `Token`** + **localStorage** 存储 | | 用 Authorization Bearer 传 Token | Token 通过 **请求头 `Token`** + **Cookie** 存储 |
| `new SimApiCore()` 在 Vue 项目里用 | Vue 项目统一用 `useSimApi()` Pinia Store | | `new SimApiCore()` 在 Vue 项目里用 | Vue 项目统一用 `useSimApi()` Pinia Store |
--- ---
@@ -629,8 +528,6 @@ api.configure({
### 10.2 内置路由对照表 ### 10.2 内置路由对照表
simapi-net 启用认证后,自动生成以下路由,simapi-vue 已内置对应方法:
| simapi-net 路由 | simapi-vue 方法 | 触发条件 | | simapi-net 路由 | simapi-vue 方法 | 触发条件 |
|-----------------|-----------------|----------| |-----------------|-----------------|----------|
| `POST /auth/login` | `api.login(request)` | `EnableSimApiAuth = true` | | `POST /auth/login` | `api.login(request)` | `EnableSimApiAuth = true` |
@@ -639,38 +536,13 @@ simapi-net 启用认证后,自动生成以下路由,simapi-vue 已内置对
| `POST /user/info` | `api.query('/user/info')` | `EnableSimApiAuth = true`(需登录) | | `POST /user/info` | `api.query('/user/info')` | `EnableSimApiAuth = true`(需登录) |
| `GET /versions` | `api.getVersion()` | `EnableVersionUrl`(默认开启) | | `GET /versions` | `api.getVersion()` | `EnableVersionUrl`(默认开启) |
### 10.3 登录流程示例
```
用户输入手机号+验证码
前端: api.login({ phone: '138xx', code: '123456' })
↓ POST /auth/login { phone, code }
后端: 验证通过 → 返回 { code: 200, data: "token-string" }
前端: 自动将 token 存入 localStorage['simapi-auth-token']
后续请求: api.query('/user/info')
↓ 自动带上 Header: Token: token-string
后端: SimApiAuthMiddleware 解析 Token → HttpContext.Items["LoginInfo"]
↓ Controller 可通过 LoginInfo 获取当前用户
```
--- ---
## 11. 构建 ## 11. 构建
```bash ```bash
# 开发模式(监听文件变化) npm run dev # 开发模式
npm run dev npm run build # 生产构建
# 生产构建
npm run build
# 本地 link 调试
npm link
cd ../your-project
npm link @simcu/simapi
``` ```
构建产物位于 `dist/` 目录: 构建产物位于 `dist/` 目录:
@@ -682,67 +554,23 @@ npm link @simcu/simapi
库使用 `declare const` 声明版本常量,构建时通过 Vite 的 `define` 注入。 库使用 `declare const` 声明版本常量,构建时通过 Vite 的 `define` 注入。
**注意:** **SimApiVersion** 由 simapi 库自身构建时从 `package.json` 注入。未配置时默认为 `0.0.0-develop`
- **SimApiVersion**:由 simapi 库自身构建时注入
- **AppVersion**:由**调用方项目**在自己的 `vite.config.ts` 中注入
#### simapi 库自身构建 — 注入 SimApiVersion **AppVersion** 由调用方项目在自己的 `vite.config.ts` 中注入。
在 simapi-vue 的 `vite.config.ts` 中,从自身 `package.json` 读取版本号:
```typescript
import { readFileSync } from 'node:fs'
const pkg = JSON.parse(readFileSync('./package.json', 'utf-8'))
export default defineConfig({
// ... 其他配置
define: {
SimApiVersion: JSON.stringify(pkg.version), // ← 取 simapi-vue 自身的 version
},
})
```
未配置时默认为 `0.0.0-develop`
#### 调用方项目 — 注入 AppVersion(推荐方式:从 package.json 自动读取)
在调用方项目的 `vite.config.ts` 中添加:
```typescript
import { readFileSync } from 'node:fs'
const pkg = JSON.parse(readFileSync('./package.json', 'utf-8'))
export default defineConfig({
// ... 其他配置
define: {
AppVersion: JSON.stringify(pkg.version), // ← 自动取 package.json 的 version 字段
},
})
```
这样每次发版只需改 `package.json``version`,无需同步修改其他地方。
--- ---
## 12. 从 axios 迁移 ## 12. 从 axios 迁移
如果你之前用的是 axios
```diff ```diff
- import axios from 'axios' - import axios from 'axios'
- const res = await axios.post('/user/list', { page: 1 }) - const res = await axios.post('/user/list', { page: 1 })
- console.log(res.data)
+ import { useSimApi } from '@simcu/simapi/pinia' + import { useSimApi } from '@simcu/simapi/pinia'
+ const api = useSimApi() + const api = useSimApi()
+ const res = await api.query('/user/list', { page: 1 }) + const res = await api.query('/user/list', { page: 1 })
+ console.log(res.data) // res.data 就是业务数据
``` ```
主要区别:
| axios | simapi-vue | | axios | simapi-vue |
|-------|-----------| |-------|-----------|
| `axios.post()` | `api.query()` | | `axios.post()` | `api.query()` |
+75 -69
View File
@@ -77,19 +77,20 @@ async function fetchPost<T = any>(
// ── SimApiCore ──────────────────────────────────────── // ── SimApiCore ────────────────────────────────────────
export class SimApiCore { export class SimApiCore {
debug: boolean = true private debug: boolean = true
auth: SimApiAuthConfig = { private auth: SimApiAuthConfig = {
token_name: 'simapi-auth-token', token_name: 'simapi-auth-token',
check_url: '/auth/check', check_url: '/user/info',
logout_url: '/auth/logout', logout_url: '/auth/logout',
login_url: '/auth/login', login_url: '/auth/login',
} }
private webConfig: Map<string, Record<string, object>> = new Map();
api: SimApiApiConfig = { private api: SimApiApiConfig = {
endpoints: {default: ''}, endpoints: {default: ''},
defaultEndpoint: 'default', defaultEndpoint: 'default',
businessCallback: { businessCallback: {
401: () => localStorage.removeItem(this.auth.token_name), 401: () => this.removeToken(),
common: () => { common: () => {
}, },
}, },
@@ -108,23 +109,34 @@ export class SimApiCore {
} }
/** /**
* 从 window.simapi 读取配置并初始化 * 从 JSON 文件加载配置
* *
* 支持字段:endpoints, defaultEndpoint, debug * 部署时替换 config.json 即可切换环境,无需重新构建。
* 业务回调(businessCallback / responseCallback)需在代码中处理 *
* @param file - 配置文件路径,默认 'config.json'
* @example
* await api.loadFromFile()
* await api.loadFromFile('/env/prod.json')
*/ */
autoInit(): void { async loadFromFile(file: string = 'config.json'): Promise<void> {
const config = (window as any).simapi try {
if (!config) return const resp = await fetch(file)
if (!resp.ok) {
if (config.debug !== undefined) { this.logDebug(`配置文件 ${file} 加载失败: HTTP ${resp.status}`)
this.debug = config.debug return
} }
if (config.endpoints) { const config = await resp.json()
this.api.endpoints = {...this.api.endpoints, ...config.endpoints} if (config.debug !== undefined) {
} this.debug = config.debug
if (config.defaultEndpoint) { }
this.api.defaultEndpoint = config.defaultEndpoint 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}`)
} }
} }
@@ -146,6 +158,14 @@ export class SimApiCore {
} }
} }
get isDebug() {
return this.debug
}
setDebug(debug: boolean) {
this.debug = debug;
}
setEndpoints(endpoints: { [name: string]: string }): void { setEndpoints(endpoints: { [name: string]: string }): void {
this.api.endpoints = {...this.api.endpoints, ...endpoints} this.api.endpoints = {...this.api.endpoints, ...endpoints}
} }
@@ -159,20 +179,21 @@ export class SimApiCore {
} }
getToken(): string { getToken(): string {
return localStorage.getItem(this.auth.token_name) ?? '' const name = this.auth.token_name
const match = document.cookie.match(new RegExp(`(?:^|;)\\s?${name}=([^;]+)`))
return match ? match[1] : ''
} }
setToken(token: string): void { setToken(token: string): void {
localStorage.setItem(this.auth.token_name, token) const name = this.auth.token_name
document.cookie = `${name}=${token}; path=/; max-age=315360000; secure; samesite=none`
} }
removeToken(): void { removeToken(): void {
localStorage.removeItem(this.auth.token_name) const name = this.auth.token_name
document.cookie = `${name}=; path=/; max-age=0; secure; samesite=none`
} }
get isLoggedIn(): boolean {
return !!localStorage.getItem(this.auth.token_name)
}
genS4(): string { genS4(): string {
return (((1 + Math.random()) * 0x10000 * Date.parse(new Date().toString())) | 0) return (((1 + Math.random()) * 0x10000 * Date.parse(new Date().toString())) | 0)
@@ -192,32 +213,19 @@ export class SimApiCore {
console.log('[DEBUG]', ...args) 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 = {
* @param endpointName - 指定从哪个 endpoint 获取版本,默认使用 default endpoint uiApp: typeof AppVersion === 'undefined' ? "0.0.0-develop" : AppVersion,
* @returns 版本信息对象 uiSimApi: typeof SimApiVersion === 'undefined' ? "0.0.0-develop" : SimApiVersion,
* apiApp: '0.0.0',
* @example apiSimApi: '0.0.0',
* // 从默认 endpoint 获取 apiAppFull: '0.0.0',
* const versions = await api.getVersion() apiSimApiFull: '0.0.0',
* };
* // 从指定 endpoint 获取 const resp = await this.query<any>('/config', {}, endpointName)
* const versions = await api.getVersion('backup')
*/
async getVersion(endpointName?: string): Promise<SimApiVersions> {
const versions: SimApiVersions = {
uiApp: typeof AppVersion === 'undefined' ? "0.0.0-develop" : AppVersion,
uiSimApi: typeof SimApiVersion === 'undefined' ? "0.0.0-develop" : SimApiVersion,
apiApp: '0.0.0',
apiSimApi: '0.0.0',
apiAppFull: '0.0.0',
apiSimApiFull: '0.0.0',
};
try {
const resp = await this.query<any>('/versions', {}, endpointName)
if (resp?.data) { if (resp?.data) {
const d = resp.data const d = resp.data.Versions;
versions.apiApp = d.App?.split('+')[0] ?? '0.0.0'; versions.apiApp = d.App?.split('+')[0] ?? '0.0.0';
versions.apiSimApi = d.SimApi?.split('+')[0] ?? '0.0.0'; versions.apiSimApi = d.SimApi?.split('+')[0] ?? '0.0.0';
versions.apiAppFull = d.App ?? '0.0.0'; versions.apiAppFull = d.App ?? '0.0.0';
@@ -225,19 +233,20 @@ export class SimApiCore {
if (this.debug) { if (this.debug) {
console.log(`UI主应用版本: ${versions.uiApp}\nUISimApi版本: ${versions.uiSimApi}\nAPI主应用版本: ${versions.apiApp}\nAPISimApi版本: ${versions.apiSimApi}`) console.log(`UI主应用版本: ${versions.uiApp}\nUISimApi版本: ${versions.uiSimApi}\nAPI主应用版本: ${versions.apiApp}\nAPISimApi版本: ${versions.apiSimApi}`)
} }
return versions resp.data.Versions = versions;
this.webConfig.set(endpointName, resp.data);
} }
} catch {
// 版本获取失败返回默认值
} }
return versions; return this.webConfig.get(endpointName)!;
} }
async query<T = any>( async query<T = any>(
uri: string, uri: string,
params: any = {}, params: any = {},
endpointKey?: string, endpointKey?: string,
extraHeaders?: Record<string, string> extraHeaders?: Record<string, string>,
selfHandleError: boolean = false
): Promise<SimApiBaseResponse<T>> { ): Promise<SimApiBaseResponse<T>> {
const headers: Record<string, string> = {...extraHeaders, ...{}} const headers: Record<string, string> = {...extraHeaders, ...{}}
const queryId = this.genS4() const queryId = this.genS4()
@@ -253,7 +262,7 @@ export class SimApiCore {
if (this.debug) { if (this.debug) {
headers['Query-Id'] = queryId headers['Query-Id'] = queryId
console.log('[REQUEST*]', queryId, '->', uri, 'AUTH:', localStorage.getItem(this.auth.token_name)) console.log('[REQUEST*]', queryId, '->', uri, 'AUTH:', this.getToken())
} }
const url = this.getEndpoint(endpointKey) + uri const url = this.getEndpoint(endpointKey) + uri
@@ -271,10 +280,12 @@ export class SimApiCore {
const processedData = this.api.responseCallback.success(respData) as SimApiBaseResponse<T> const processedData = this.api.responseCallback.success(respData) as SimApiBaseResponse<T>
// 业务回调处理 // 业务回调处理
if (this.api.businessCallback.hasOwnProperty(processedData.code)) { if (!selfHandleError) {
this.api.businessCallback[processedData.code](processedData) if (this.api.businessCallback.hasOwnProperty(processedData.code)) {
} else if (this.api.businessCallback['common'] && processedData.code !== 200) { this.api.businessCallback[processedData.code](processedData)
this.api.businessCallback['common'](processedData) } else if (this.api.businessCallback['common'] && processedData.code !== 200) {
this.api.businessCallback['common'](processedData)
}
} }
// code != 200 时抛出业务错误 // code != 200 时抛出业务错误
@@ -311,18 +322,13 @@ export class SimApiCore {
async logout(url?: string | null): Promise<any> { async logout(url?: string | null): Promise<any> {
if (url !== null) { if (url !== null) {
return this.query(url ?? this.auth.logout_url).catch(() => true) this.query(url ?? this.auth.logout_url).catch(() => true)
} }
this.removeToken() this.removeToken()
return true return true
} }
async checkLogin(url?: string | null): Promise<void> { async checkLogin(url?: string | null): Promise<any> {
if (url !== null) { return this.query(url ?? this.auth.check_url);
await this.query(url ?? this.auth.check_url).catch(() => {
})
} else if (this.getToken()) {
this.api.businessCallback[401]?.(null)
}
} }
} }
+62 -70
View File
@@ -1,91 +1,83 @@
import { defineStore } from 'pinia' import {defineStore} from 'pinia'
import { SimApiCore } from './simapi.core' import {SimApiCore} from './simapi.core'
import type { SimApiBaseResponse, SimApiOptions, SimApiVersions } from './types' import type {SimApiBaseResponse, SimApiOptions, SimApiVersions} from './types'
// ============ Pinia Store ============ // ============ Pinia Store ============
// 仅作为 core 的代理映射,不维护任何独立状态 // 仅作为 core 的代理映射,不维护任何独立状态
export const useSimApi = defineStore('simapi', { export const useSimApi = defineStore('simapi', {
state: () => ({ state: () => ({
// 在 state 中实例化 core // 在 state 中实例化 core
_core: new SimApiCore(), _core: new SimApiCore(),
}), }),
getters: {
getters: { IsDebug: state => state._core.isDebug
// 直接映射 core 的属性和方法
debug: (state) => state._core.debug,
token: (state) => state._core.getToken(),
isLoggedIn: (state) => state._core.isLoggedIn,
api: (state) => state._core.api,
auth: (state) => state._core.auth,
},
actions: {
// 所有方法直接代理到 core
autoInit(): void {
this._core.autoInit()
}, },
actions: {
// 所有方法直接代理到 core
async loadFromFile(file: string = '/config.json'): Promise<void> {
return this._core.loadFromFile(file)
},
configure(options: SimApiOptions): void { configure(options: SimApiOptions): void {
this._core.configure(options) this._core.configure(options)
}, },
setDebug(debug: boolean): void { setDebug(debug: boolean): void {
this._core.debug = debug this._core.setDebug(debug);
}, },
setEndpoints(endpoints: { [name: string]: string }): void { setEndpoints(endpoints: { [name: string]: string }): void {
this._core.setEndpoints(endpoints) this._core.setEndpoints(endpoints)
}, },
setBusinessCallback( setBusinessCallback(
code: number | string, code: number | string,
callback: (data: SimApiBaseResponse) => void callback: (data: SimApiBaseResponse) => void
): void { ): void {
this._core.setBusinessCallback(code, callback) this._core.setBusinessCallback(code, callback)
}, },
getToken(): string { getToken(): string {
return this._core.getToken() return this._core.getToken()
}, },
setToken(token: string): void { setToken(token: string): void {
this._core.setToken(token) this._core.setToken(token)
}, },
removeToken(): void { removeToken(): void {
this._core.removeToken() this._core.removeToken()
}, },
async login(request: Record<string, any>): Promise<SimApiBaseResponse<string>> { async login(request: Record<string, any>): Promise<SimApiBaseResponse<string>> {
return this._core.login(request) return this._core.login(request)
}, },
async logout(url?: string | null): Promise<any> { async logout(url?: string | null): Promise<any> {
return this._core.logout(url) return this._core.logout(url)
}, },
async checkLogin(url?: string | null): Promise<void> { async checkLogin(url?: string | null): Promise<void> {
return this._core.checkLogin(url) return this._core.checkLogin(url)
}, },
async query<T = any>( async query<T = any>(
uri: string, uri: string,
params?: any, params?: any,
endpointKey?: string, endpointKey?: string,
extraHeaders?: Record<string, string> extraHeaders?: Record<string, string>,
): Promise<SimApiBaseResponse<T>> { selfHandleError: boolean = false
return this._core.query<T>(uri, params, endpointKey, extraHeaders) ): Promise<SimApiBaseResponse<T>> {
}, return this._core.query<T>(uri, params, endpointKey, extraHeaders, selfHandleError)
},
getEndpoint(name?: string): string { getEndpoint(name?: string): string {
return this._core.getEndpoint(name) return this._core.getEndpoint(name)
}, },
async getVersion(endpointName?: string): Promise<SimApiVersions> { async getConfig(reload = false, endpointName?: string): Promise<Record<string, object>> {
return this._core.getVersion(endpointName) return this._core.getConfig(reload, endpointName)
},
}, },
},
}) })