refactor: ♻️ 优化请求接口

This commit is contained in:
tsl
2026-04-16 09:50:30 +08:00
parent cc30c088c4
commit 82c2b0f1bd
24 changed files with 8535 additions and 78 deletions
+165
View File
@@ -0,0 +1,165 @@
import { fetch } from "@tauri-apps/plugin-http";
import type { ClientOptions } from "@tauri-apps/plugin-http";
/** CRM API 根地址,从环境变量 VITE_API_BASE 读取 */
export const API_BASE = import.meta.env.VITE_API_BASE;
/**
* 请求选项类型,支持请求数据
* @template D 请求数据类型(可选)
*/
export type RequestOptions<D = unknown> = RequestInit &
ClientOptions & { data?: D };
/**
* 增强的响应类型,继承 Response 并添加泛型支持
* @template T 响应数据类型
*/
export interface ApiResponse<T = unknown> extends Response {
/**
* 解析 JSON 响应并返回指定类型的数据
* @returns 解析后的响应数据
*/
json(): Promise<T>;
}
/**
* 基础请求函数,支持请求类型和响应类型泛型
* @template T 响应数据类型
* @template D 请求数据类型(可选)
* @param path API 路径(相对于 API_BASE,如 `/SystemUser/Login`
* @param options 请求配置,可包含请求数据
* @returns 返回指定泛型类型的响应数据
*/
/**
* 原始请求函数,返回增强的响应对象
* @template T 响应数据类型
* @template D 请求数据类型(可选)
* @param path API 路径(相对于 API_BASE,如 `/SystemUser/Login`
* @param options 请求配置,可包含请求数据
* @returns 返回增强的 ApiResponse 对象
*/
export async function requestRaw<T = unknown, D = unknown>(
path: string,
options: RequestOptions<D> = {},
): Promise<ApiResponse<T>> {
// 如果有 data 参数,将其序列化为 JSON 并设置到 body 中
const { data, ...restOptions } = options;
const requestOptions: RequestInit & ClientOptions = { ...restOptions };
if (data !== undefined) {
requestOptions.body = JSON.stringify(data);
requestOptions.headers ??= {};
(requestOptions.headers as Record<string, string>)["Content-Type"] =
"application/json";
}
const response = await fetch(`${API_BASE}${path}`, requestOptions);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
// 创建增强的响应对象
const enhancedResponse = response as ApiResponse<T>;
// 重写 json 方法,添加泛型支持
const originalJson = response.json.bind(response);
enhancedResponse.json = () => originalJson() as Promise<T>;
return enhancedResponse;
}
/**
* 基础请求函数,支持请求类型和响应类型泛型
* @template T 响应数据类型
* @template D 请求数据类型(可选)
* @param path API 路径(相对于 API_BASE,如 `/SystemUser/Login`
* @param options 请求配置,可包含请求数据
* @returns 返回指定泛型类型的响应数据
*/
export async function request<T = unknown, D = unknown>(
path: string,
options: RequestOptions<D> = {},
): Promise<T> {
const response = await requestRaw<T, D>(path, options);
return await response.json();
}
/**
* GET 请求辅助函数
* @template T 响应数据类型
* @param path API 路径
* @param options 可选的请求配置
* @returns 返回指定泛型类型的响应数据
*/
export async function get<T = unknown>(
path: string,
options: Omit<RequestOptions<never>, "data"> = {},
): Promise<T> {
return request<T>(path, { ...options, method: "GET" });
}
/**
* POST 请求辅助函数
* @template T 响应数据类型
* @template D 请求数据类型
* @param path API 路径
* @param data 请求数据
* @param options 可选的请求配置
* @returns 返回指定泛型类型的响应数据
*/
export async function post<T = unknown, D = unknown>(
path: string,
data?: D,
options: Omit<RequestOptions<D>, "data"> = {},
): Promise<T> {
return request<T, D>(path, { ...options, method: "POST", data });
}
/**
* PUT 请求辅助函数
* @template T 响应数据类型
* @template D 请求数据类型
* @param path API 路径
* @param data 请求数据
* @param options 可选的请求配置
* @returns 返回指定泛型类型的响应数据
*/
export async function put<T = unknown, D = unknown>(
path: string,
data?: D,
options: Omit<RequestOptions<D>, "data"> = {},
): Promise<T> {
return request<T, D>(path, { ...options, method: "PUT", data });
}
/**
* DELETE 请求辅助函数
* @template T 响应数据类型
* @param path API 路径
* @param options 可选的请求配置
* @returns 返回指定泛型类型的响应数据
*/
export async function del<T = unknown>(
path: string,
options: Omit<RequestOptions<never>, "data"> = {},
): Promise<T> {
return request<T>(path, { ...options, method: "DELETE" });
}
/**
* PATCH 请求辅助函数
* @template T 响应数据类型
* @template D 请求数据类型
* @param path API 路径
* @param data 请求数据
* @param options 可选的请求配置
* @returns 返回指定泛型类型的响应数据
*/
export async function patch<T = unknown, D = unknown>(
path: string,
data?: D,
options: Omit<RequestOptions<D>, "data"> = {},
): Promise<T> {
return request<T, D>(path, { ...options, method: "PATCH", data });
}
+101
View File
@@ -0,0 +1,101 @@
import { post, requestRaw } from "@/api/client";
import type {
LoginResponseData,
WorkOrderResponseData,
CheckStatusResponse,
ApiResponse,
} from "@/api/types";
/**
* 登录请求
* @param account 账号
* @param password 密码
* @returns 登录响应数据和会话ID
*/
export async function login(
account: string,
password: string,
): Promise<{ data: ApiResponse<LoginResponseData>; sessionId: string }> {
// 登录接口需要特殊处理,因为需要从响应头中获取 sessionId
const loginData = {
Account: account,
PassWord: password,
};
const resp = await requestRaw<
ApiResponse<LoginResponseData>,
typeof loginData
>("/SystemUser/Login", {
method: "POST",
headers: {
"Content-Length": String(
new TextEncoder().encode(JSON.stringify(loginData)).length,
),
Host: "crm.yunvip123.com",
"Cache-Control": "no-cache",
Cookie: "",
},
credentials: "omit",
data: loginData,
});
const setCookie = resp.headers.get("set-cookie") || "";
const match = /ASP\.NET_SessionId=([^;]+)/.exec(setCookie);
const sessionId = match ? match[1] : "";
const data = await resp.json();
return { data, sessionId };
}
/**
* 查询待审核工单数量
* @param sessionId 会话ID
* @returns 工单数据响应
*/
export async function checkWorkOrders(
sessionId: string,
): Promise<ApiResponse<WorkOrderResponseData>> {
// 请求数据类型
const requestData = {
isSelect: 8,
isShow: 3,
IsExport: 0,
PageIndex: 1,
PageSize: 20,
};
return post<ApiResponse<WorkOrderResponseData>, typeof requestData>(
"/DemandManage/GetWorkOrderListPage",
requestData,
{
headers: {
Cookie: `ASP.NET_SessionId=${sessionId}`,
},
},
);
}
/**
* 发送工单状态检查请求
* @param sessionId 会话ID
* @param userGid 用户GID
* @returns 状态检查响应
*/
export async function checkStatus(
sessionId: string,
userGid?: string,
): Promise<ApiResponse<CheckStatusResponse>> {
// 请求数据类型
const requestData: Record<string, string> = {};
if (userGid) requestData.UserGID = userGid;
return post<ApiResponse<CheckStatusResponse>, typeof requestData>(
"/DemandManage/QueryIndexCount",
requestData,
{
headers: {
Cookie: `ASP.NET_SessionId=${sessionId}`,
},
},
);
}
+6
View File
@@ -0,0 +1,6 @@
/** 通用 API 响应 */
export interface ApiResponse<T = unknown> {
success: boolean;
msg: string;
data: T;
}
+2
View File
@@ -0,0 +1,2 @@
export * from "./common";
export * from "./monitor";
+111
View File
@@ -0,0 +1,111 @@
/** 菜单信息 */
export interface MenuInfo {
/** 唯一标识 */
GID: string;
/** 菜单名称 */
MM_Name: string;
/** 链接地址 */
MM_LinkUrl: string;
/** 父级 ID */
MM_ParentID: string;
/** 排序 */
MM_Sort: number;
/** 备注/标记 */
MM_Mark: string | null;
/** 图标 */
MM_Ico: string;
/** 资源列表 */
ResourceList: unknown;
}
/** 角色资源权限 */
export interface SRRoleItem {
/** 唯一标识 */
GID: string;
/** 菜单 GID */
MM_GID: string;
/** 资源名称 */
RS_Name: string;
/** 资源链接 */
RS_LinkUrl: string;
/** 是否显示 */
RS_IsShow: number;
/** 排序 */
RS_Sort: number;
/** 创建时间 */
RS_CreateTime: string;
/** 资源编码 */
RS_Code: string | null;
}
/** 登录 API 响应中的 data 部分 */
export interface LoginResponseData {
/** 用户唯一标识 GID */
GID: string;
/** 用户名 */
SU_UserName: string;
/** 账号/工号 */
SU_Account: string;
/** 角色名称 */
SR_Name: string;
/** 菜单列表 */
MenuInfoList?: MenuInfo[];
/** 角色权限列表 */
SRRole?: SRRoleItem[];
}
/** 检查状态接口返回的计数数据 */
export interface CheckStatusResponse {
PendingCount?: number;
StaycloseCount?: number;
ConfirmCount?: number;
}
/** 工单列表项 */
export interface WorkOrderItem {
/** 工单 ID */
ID: number;
/** 需求名称/标题 */
DL_DemandName: string;
/** 需求内容 */
DL_DemandContent: string;
/** 创建人 */
DL_Creator: string;
/** 创建时间 */
DL_CreateTime: string;
/** 状态码 (如 350) */
DL_Status: number;
/** 优先级 */
DL_Priority: string;
/** 产品名称 */
DL_ProductName: string;
/** 指派人 */
DL_AssignName: string | null;
/** 关联项目 ID */
DL_ID: number;
/** 关联项目名称 */
DL_ItemID: string;
}
/** 工单查询接口返回的 data 部分 */
export interface WorkOrderResponseData {
/** 总页数 */
PageTotal: number;
/** 每页数量 */
PageSize: number;
/** 总数 */
DataCount: number;
/** 当前页码 */
PageIndex: number;
/** 工单列表 */
DataList: WorkOrderItem[];
}
/** 监测状态摘要 */
export interface TicketCounts {
pending: number;
stayclose: number;
confirm: number;
workOrderCount: number;
lastCheck: string;
}
+1 -1
View File
@@ -14,9 +14,9 @@ import { TrayIcon } from "@tauri-apps/api/tray";
import { getCurrentWindow } from "@tauri-apps/api/window";
import { openUrl } from "@tauri-apps/plugin-opener";
import { type Ref } from "vue";
import { type TicketCounts } from "@/api/types";
import { CRM_URL } from "@/constants/app";
import { useLogStore } from "@/stores/log";
import { type TicketCounts } from "@/stores/monitor";
import { getErrorMessage } from "@/utils/common";
/**
-11
View File
@@ -1,11 +0,0 @@
/** CRM API 根地址 */
export const API_BASE = "https://crm.yunvip123.com/api";
/** 用户登录接口 */
export const LOGIN_URL = `${API_BASE}/SystemUser/Login`;
/** 工单状态计数查询接口 */
export const CHECK_URL = `${API_BASE}/DemandManage/QueryIndexCount`;
/** 工单列表分页查询接口 */
export const WORK_ORDER_URL = `${API_BASE}/DemandManage/GetWorkOrderListPage`;
+14 -59
View File
@@ -1,20 +1,12 @@
import { fetch } from "@tauri-apps/plugin-http";
import { defineStore, acceptHMRUpdate, storeToRefs } from "pinia";
import { ref, watch } from "vue";
import { login, checkWorkOrders, checkStatus } from "@/api/monitor";
import { useNotification } from "@/composables/useNotification";
import { useTray } from "@/composables/useTray";
import { LOGIN_URL, CHECK_URL, WORK_ORDER_URL } from "@/constants/api";
import { useAppStore } from "@/stores/app";
import { useLogStore } from "@/stores/log";
import { formatTime, getErrorMessage } from "@/utils/common";
export interface TicketCounts {
pending: number;
stayclose: number;
confirm: number;
workOrderCount: number;
lastCheck: string;
}
import type { TicketCounts, CheckStatusResponse } from "@/api/types";
export const useMonitorStore = defineStore("monitor", () => {
// ===== 响应式数据 =====
@@ -55,29 +47,13 @@ export const useMonitorStore = defineStore("monitor", () => {
/** 使用用户名/密码登录,成功后提取并存储 SessionId,返回是否成功 */
async function apiLogin(): Promise<boolean> {
try {
const body = JSON.stringify({
Account: username.value,
PassWord: password.value,
});
const resp = await fetch(LOGIN_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Content-Length": String(new TextEncoder().encode(body).length),
Host: "crm.yunvip123.com",
"Cache-Control": "no-cache",
Cookie: "",
},
body,
credentials: "omit",
});
const setCookie = resp.headers.get("set-cookie") || "";
const match = /ASP\.NET_SessionId=([^;]+)/.exec(setCookie);
if (match) sessionId = match[1];
const { data, sessionId: newSessionId } = await login(
username.value,
password.value,
);
if (newSessionId) sessionId = newSessionId;
addLog("DEBUG", `SessionId: ${sessionId || "(未提取)"}`, "LOGIN");
const data = await resp.json();
if (data.success && data.data) {
userGid = data.data.GID;
userName = data.data.SU_UserName || "用户";
@@ -100,21 +76,7 @@ export const useMonitorStore = defineStore("monitor", () => {
/** 查询待审核工单数量(分页接口,取 DataCount 字段) */
async function apiCheckWorkOrders(): Promise<number> {
try {
const resp = await fetch(WORK_ORDER_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
Cookie: `ASP.NET_SessionId=${sessionId}`,
},
body: JSON.stringify({
isSelect: 8,
isShow: 3,
IsExport: 0,
PageIndex: 1,
PageSize: 20,
}),
});
const result = await resp.json();
const result = await checkWorkOrders(sessionId);
if (result.success && result.data) {
const count = result.data.DataCount || 0;
addLog("INFO", `待审核检查完成 - 数据条数: ${count}`, "SCHEDULER");
@@ -135,21 +97,14 @@ export const useMonitorStore = defineStore("monitor", () => {
/** 发送工单状态检查请求 */
async function performStatusCheckRequest() {
const body: Record<string, string> = {};
if (userGid) body.UserGID = userGid;
const resp = await fetch(CHECK_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
Cookie: `ASP.NET_SessionId=${sessionId}`,
},
body: JSON.stringify(body),
});
return await resp.json();
return await checkStatus(sessionId, userGid);
}
/** 处理检查到的工单数据并更新状态 */
async function updateTicketCountsAndLog(data: any) {
/**
* 处理检查到的工单数据并更新状态
* @param data 接口返回的原始计数数据
*/
async function updateTicketCountsAndLog(data: CheckStatusResponse) {
const pending = data.PendingCount || 0;
const stayclose = data.StaycloseCount || 0;
const confirm = data.ConfirmCount || 0;
+2 -2
View File
@@ -1,7 +1,7 @@
import { fetch } from "@tauri-apps/plugin-http";
import { defineStore, acceptHMRUpdate } from "pinia";
import { ref } from "vue";
import { LOGIN_URL } from "@/constants/api";
import { API_BASE } from "@/api/client";
import { useAppStore } from "@/stores/app";
import { useLogStore } from "@/stores/log";
import { formatTime } from "@/utils/common";
@@ -49,7 +49,7 @@ export const useNetworkStore = defineStore("network", () => {
if (connected) {
try {
const apiStart = Date.now();
const resp = await fetch(LOGIN_URL, {
const resp = await fetch(`${API_BASE}/SystemUser/Login`, {
method: "HEAD",
connectTimeout: appStore.config.network_timeout * 1000,
});
+1
View File
@@ -2,6 +2,7 @@
interface ImportMetaEnv {
readonly VITE_APP_NAME: string;
readonly VITE_API_BASE: string;
}
interface ImportMeta {