20260809223428
This commit is contained in:
1 parent
d72ca4193d
commit
c4891538a7
466 files changed
+21000
-3339
No files matched your search
@@ -0,0 +1,117 @@
|
||||
import { post } from "./http";
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @typedef {object} ApiResult
|
||||
* @property {boolean} success 前端根据 code 补充的成功标记。
|
||||
* @property {number} code 后端业务状态码,0 表示成功。
|
||||
* @property {string} message 响应消息。
|
||||
* @property {T} data 响应数据。
|
||||
*/
|
||||
|
||||
/**
|
||||
* 机构用户登录。
|
||||
* @param {string} orgId 机构码。
|
||||
* @param {string} userId 用户 ID。
|
||||
* @param {string} password 密码。
|
||||
* @returns {Promise<ApiResult<{token: string, orgid: string, user: {id: string, name: string}}>>} 登录响应。
|
||||
*/
|
||||
export function loginApi(orgId, userId, password) {
|
||||
return post("/auth/login", {
|
||||
orgid: orgId,
|
||||
userid: userId,
|
||||
password,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 按表或视图加载数据。
|
||||
* @param {string} viewName 表名或视图名,必填。
|
||||
* @param {string} [searchCondition] 查询条件,不包含 WHERE。
|
||||
* @param {string} [orderBy] 排序字段,可带 ASC 或 DESC。
|
||||
* @param {string[]} [searchColumns] 需要返回的字段列表,省略时返回全部字段。
|
||||
* @returns {Promise<ApiResult<object[]>>} 查询响应。
|
||||
*/
|
||||
export function loadDataApi(viewName, searchCondition, orderBy, searchColumns) {
|
||||
return post("/data/loaddata", {
|
||||
view_name: viewName,
|
||||
search_condition: searchCondition,
|
||||
order_by: orderBy,
|
||||
search_columns: searchColumns,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行只读 SQL 查询。
|
||||
* @param {string} sql 完整的 SELECT 或 WITH 查询语句。
|
||||
* @returns {Promise<ApiResult<object[]>>} 查询响应。
|
||||
*/
|
||||
export function loadDataBySqlApi(sql) {
|
||||
return post("/data/loaddatabysql", { sql });
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页加载表或视图数据。
|
||||
* @param {object} options 分页查询参数。
|
||||
* @param {string} options.viewName 表名或视图名,必填。
|
||||
* @param {string} options.orderBy 排序字段,必填,可用逗号分隔多个字段。
|
||||
* @param {string[]} [options.searchColumns] 需要返回的字段列表。
|
||||
* @param {string} [options.searchCondition] 查询条件,不包含 WHERE。
|
||||
* @param {number} [options.pageNo=1] 页码,从 1 开始。
|
||||
* @param {number} [options.pageSize=20] 每页记录数。
|
||||
* @returns {Promise<ApiResult<{rows: object[], row_count: number, total: number, page_no: number, page_size: number}>>} 分页响应。
|
||||
*/
|
||||
export function pageDataApi({
|
||||
viewName,
|
||||
orderBy,
|
||||
searchColumns,
|
||||
searchCondition,
|
||||
pageNo = 1,
|
||||
pageSize = 20,
|
||||
}) {
|
||||
return post("/data/page", {
|
||||
view_name: viewName,
|
||||
search_columns: searchColumns,
|
||||
search_condition: searchCondition,
|
||||
order_by: orderBy,
|
||||
page_no: pageNo,
|
||||
page_size: pageSize,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 在同一事务中保存一个或多个表的数据变更。
|
||||
* @param {{table: string, key_field: string, inserts?: object[], updates?: object[], deletes?: object[]}[]} requests 保存配置,不能为空数组。
|
||||
* @returns {Promise<ApiResult<null>>} 保存响应。
|
||||
*/
|
||||
export function saveObjectApi(requests) {
|
||||
return post("/data/saveobjt", requests);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取下一个雪花 ID;传入 count 可批量获取指定数量的 ID(返回数组)。
|
||||
* @param {number} [count=1] 需要获取的 ID 数量,缺省 1。
|
||||
* @returns {Promise<ApiResult<string|string[]>>} count 为 1 时 data 为单个 ID 字符串,大于 1 时为 ID 字符串数组。
|
||||
*/
|
||||
export function nextIdApi(count = 1) {
|
||||
return post("/data/nextid", { count });
|
||||
}
|
||||
|
||||
/**
|
||||
* 按模块规则原子获取业务编号;count 大于 1 时一次返回多个编号。
|
||||
* @param {string} moduleId 数据模块 ID。
|
||||
* @param {number} [count=1] 需要获取的编号数量。
|
||||
* @returns {Promise<ApiResult<string|string[]>>} 业务编号或编号数组。
|
||||
*/
|
||||
export function nextCodeApi(moduleId, count = 1) {
|
||||
return post("/data/nextcode", { moduleId: String(moduleId), count });
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取表或视图的列结构,用于同步字段等场景。
|
||||
* @param {string} table 表名或视图名,必填,后端会校验标识符。
|
||||
* @returns {Promise<ApiResult<{name: string, type: string, nullable: boolean, size: number}[]>} 列结构响应。
|
||||
*/
|
||||
export function describeApi(table) {
|
||||
return post("/data/describe", { table });
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
import { post } from "./http";
|
||||
|
||||
let storageType = null;
|
||||
let ossCredCache = null;
|
||||
const CRED_BUFFER_MS = 5 * 60 * 1000; // 到期前 5 分钟刷新
|
||||
|
||||
/** 初始化存储模式,返回 "local" | "aliyun-oss" */
|
||||
export async function initFileConfig() {
|
||||
if (storageType) return storageType;
|
||||
const res = await post("/file/config");
|
||||
storageType = res.data.storageType;
|
||||
return storageType;
|
||||
}
|
||||
|
||||
export function getStorageType() {
|
||||
return storageType;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量上传文件(自动匹配模式,含 DB 写入)
|
||||
* @param {{ files: File[], father: string, moduleId: string, cateId?: string }} opts
|
||||
* @returns {Promise<Object[]>} 文件记录列表
|
||||
*/
|
||||
export async function upload({ files, father, moduleId, cateId }) {
|
||||
if (!storageType) await initFileConfig();
|
||||
|
||||
if (storageType === "aliyun-oss") {
|
||||
return uploadToOss({ files, father, moduleId, cateId });
|
||||
}
|
||||
return uploadToLocal({ files, father, moduleId, cateId });
|
||||
}
|
||||
|
||||
/** Local: 一次请求打包所有文件,后端逐个写磁盘 + INSERT */
|
||||
async function uploadToLocal({ files, father, moduleId, cateId }) {
|
||||
const fd = new FormData();
|
||||
for (const f of files) {
|
||||
fd.append("files", f);
|
||||
}
|
||||
fd.append("father", father);
|
||||
fd.append("moduleId", moduleId);
|
||||
if (cateId) fd.append("cateId", cateId);
|
||||
|
||||
const res = await post("/file/upload", fd, {
|
||||
headers: { "Content-Type": undefined },
|
||||
});
|
||||
return res.data; // List<Map>
|
||||
}
|
||||
|
||||
/** OSS: 共享凭证 → 逐个 XHR 直传 → 逐个回调 save-record */
|
||||
async function uploadToOss({ files, father, moduleId, cateId }) {
|
||||
const cred = await getOssCredential();
|
||||
const { accessid, policy, signature, dir, host } = cred;
|
||||
|
||||
const results = [];
|
||||
for (const file of files) {
|
||||
const idRes = await post("/data/nextid");
|
||||
const subid = String(idRes.data);
|
||||
|
||||
const ext = extname(file.name);
|
||||
const objectName = crypto.randomUUID().replace(/-/g, "") + (ext ? "." + ext : "");
|
||||
const url = await xhrUpload(host, {
|
||||
key: dir + objectName,
|
||||
OSSAccessKeyId: accessid,
|
||||
policy,
|
||||
Signature: signature,
|
||||
file,
|
||||
filename: objectName,
|
||||
});
|
||||
|
||||
const record = {
|
||||
subid,
|
||||
father,
|
||||
mx_moduleid: moduleId,
|
||||
mx_cate_id: cateId || "",
|
||||
mx_filename: file.name,
|
||||
mx_filesize: String(file.size),
|
||||
mx_fileext: ext,
|
||||
mx_mapfilename: url,
|
||||
};
|
||||
await post("/file/save-record", record);
|
||||
results.push(record);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
/* ───── OSS 凭证缓存 ───── */
|
||||
|
||||
async function getOssCredential() {
|
||||
if (ossCredCache && !credExpiring(ossCredCache)) {
|
||||
return ossCredCache;
|
||||
}
|
||||
const res = await post("/file/upload-credential");
|
||||
ossCredCache = { ...res.data, _fetchedAt: Date.now() };
|
||||
return ossCredCache;
|
||||
}
|
||||
|
||||
function credExpiring(cache) {
|
||||
// 如果 g3oss 返回了 UNIX 时间戳过期时间,优先用
|
||||
if (cache.expire) {
|
||||
return Date.now() + CRED_BUFFER_MS > Number(cache.expire) * 1000;
|
||||
}
|
||||
if (cache.Expiration) {
|
||||
return Date.now() + CRED_BUFFER_MS > new Date(cache.Expiration).getTime();
|
||||
}
|
||||
// 兜底:默认有效期 55 分钟
|
||||
return Date.now() - cache._fetchedAt > 55 * 60 * 1000;
|
||||
}
|
||||
|
||||
/* ───── 底层工具 ───── */
|
||||
|
||||
function xhrUpload(host, fields) {
|
||||
const fd = new FormData();
|
||||
fd.append("key", fields.key);
|
||||
fd.append("OSSAccessKeyId", fields.OSSAccessKeyId);
|
||||
fd.append("policy", fields.policy);
|
||||
fd.append("Signature", fields.Signature);
|
||||
fd.append("file", fields.file, fields.filename);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open("POST", host);
|
||||
xhr.onload = () => {
|
||||
if (xhr.status === 200 || xhr.status === 204) {
|
||||
resolve(host + "/" + fields.key);
|
||||
} else {
|
||||
reject(new Error("OSS 上传失败: " + xhr.status));
|
||||
}
|
||||
};
|
||||
xhr.onerror = () => reject(new Error("OSS 上传异常"));
|
||||
xhr.send(fd);
|
||||
});
|
||||
}
|
||||
|
||||
/** 获取预览/下载地址 */
|
||||
export function getPreviewUrl(fileRow) {
|
||||
return fileRow.mx_mapfilename;
|
||||
}
|
||||
|
||||
/** 删除文件 */
|
||||
export async function removeFile(subid) {
|
||||
return post("/file/delete", { subid });
|
||||
}
|
||||
|
||||
function extname(name) {
|
||||
if (!name || !name.includes(".")) return "";
|
||||
return name.substring(name.lastIndexOf(".") + 1).toLowerCase();
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import axios from "axios";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
|
||||
const AUTH_ERROR_CODE = 401;
|
||||
const SESSION_REPLACED_CODE = 40101;
|
||||
|
||||
const http = axios.create({
|
||||
baseURL: import.meta.env.VITE_API_BASE_URL || "/api",
|
||||
timeout: 15000,
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
let redirectingToLogin = false;
|
||||
|
||||
function redirectToLogin(reason) {
|
||||
const currentPath = window.location.pathname.replace(/\/+$/, "") || "/";
|
||||
if (currentPath === "/login" || redirectingToLogin) return;
|
||||
|
||||
redirectingToLogin = true;
|
||||
const redirect = `${window.location.pathname}${window.location.search}${window.location.hash}`;
|
||||
const query = new URLSearchParams({ redirect });
|
||||
if (reason) query.set("reason", reason);
|
||||
window.location.assign(`/login?${query.toString()}`);
|
||||
}
|
||||
|
||||
function isAuthenticationError(code) {
|
||||
return code === AUTH_ERROR_CODE || code === SESSION_REPLACED_CODE;
|
||||
}
|
||||
|
||||
function handleUnauthorized(code) {
|
||||
const authStore = useAuthStore();
|
||||
authStore.clearSession();
|
||||
redirectToLogin(code === SESSION_REPLACED_CODE ? "session-replaced" : "expired");
|
||||
}
|
||||
|
||||
http.interceptors.request.use((config) => {
|
||||
const authStore = useAuthStore();
|
||||
|
||||
if (authStore.token) {
|
||||
config.headers.Authorization = `Bearer ${authStore.token}`;
|
||||
}
|
||||
|
||||
return config;
|
||||
});
|
||||
|
||||
http.interceptors.response.use(
|
||||
(response) => {
|
||||
const payload = response.data;
|
||||
|
||||
if (payload?.code === undefined) return payload;
|
||||
const result = {
|
||||
...payload,
|
||||
success: payload.code === 0,
|
||||
};
|
||||
if (result.success) return result;
|
||||
|
||||
if (isAuthenticationError(result.code)) handleUnauthorized(result.code);
|
||||
|
||||
const error = new Error(result.message || "请求失败");
|
||||
error.code = result.code;
|
||||
error.data = result.data;
|
||||
error.success = result.success;
|
||||
throw error;
|
||||
},
|
||||
(error) => {
|
||||
const status = error.response?.status;
|
||||
const payload = error.response?.data;
|
||||
|
||||
const authenticationCode = payload?.code ?? status;
|
||||
if (isAuthenticationError(authenticationCode)) handleUnauthorized(authenticationCode);
|
||||
|
||||
const requestError = new Error(
|
||||
payload?.message ||
|
||||
(error.code === "ECONNABORTED" ? "请求超时,请稍后重试" : "网络异常,请检查服务是否正常"),
|
||||
);
|
||||
requestError.code = payload?.code ?? status;
|
||||
requestError.data = payload?.data;
|
||||
requestError.success = false;
|
||||
throw requestError;
|
||||
},
|
||||
);
|
||||
|
||||
export function post(path, data, config) {
|
||||
return http.post(path, data, config);
|
||||
}
|
||||
|
||||
export default http;
|
||||
Reference in new issue
Block a user