menus.ts 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296
  1. import { api8000 } from "@/utils/request"; // 确保这是您项目中正确的请求函数
  2. import { ElMessage } from 'element-plus';
  3. import 'element-plus/es/components/message/style/css';
  4. // 🔹 定义查询参数类型
  5. interface QueryTableParams {
  6. table: string;
  7. filters?: string; // JSON 字符串格式的过滤条件
  8. page?: number;
  9. page_size?: number;
  10. sort_by?: string;
  11. sort_order?: 'asc' | 'desc';
  12. }
  13. // 🔹 定义查询返回结果类型
  14. interface QueryTableResponse {
  15. data: Record<string, any>[];
  16. pagination: {
  17. total: number;
  18. page: number;
  19. page_size: number;
  20. total_pages: number;
  21. };
  22. }
  23. /// 🔹 通用查询表格数据 (增强版)
  24. export const queryTable = (params: QueryTableParams): Promise<QueryTableResponse> => {
  25. // 设置默认分页参数
  26. const requestParams = {
  27. page: 1,
  28. page_size: 10,
  29. sort_order: 'asc' as const, // 默认升序
  30. ...params, // 覆盖默认值
  31. };
  32. return api8000({
  33. url: "/admin/query_table", // 确保后端有此对应路由
  34. method: "GET",
  35. params: requestParams,
  36. })
  37. .then((res) => {
  38. // 假设后端严格按照定义返回结构
  39. return res.data as QueryTableResponse;
  40. })
  41. .catch((err) => {
  42. console.error("查询数据失败:", err);
  43. if (err.response?.status === 401) throw err;
  44. throw new Error(err.message || "获取数据失败");
  45. });
  46. };
  47. // 🔹 获取表格数据 (保持兼容性,仅获取所有数据)
  48. export const table = (params: { table: string }) => {
  49. return api8000({
  50. url: "/admin/table",
  51. method: "GET",
  52. params: params,
  53. })
  54. .then((res) => {
  55. // 适配后端返回的直接数组格式
  56. return { data: Array.isArray(res.data) ? res.data : [] };
  57. })
  58. .catch((err) => {
  59. if (err.response?.status === 401) throw err;
  60. throw new Error(err.message || "获取数据失败");
  61. });
  62. };
  63. // 🔹 新增数据
  64. export const addItem = (data: {
  65. table: string;
  66. item: Record<string, any>;
  67. }) => {
  68. return api8000({
  69. url: "/admin/add_item",
  70. method: "POST",
  71. params: { table: data.table },
  72. data: data.item,
  73. }).catch((error) => {
  74. if (error.response?.status === 400 && error.response.data.detail === "重复数据") {
  75. ElMessage.error("数据重复,请重新添加");
  76. }
  77. throw error;
  78. });
  79. };
  80. // 🔹 更新数据
  81. export const updateItem = (data: {
  82. table: string;
  83. id: number;
  84. update_data: Record<string, any>;
  85. }) => {
  86. return api8000({
  87. url: "/admin/update_item",
  88. method: "PUT",
  89. params: { table: data.table, id: data.id },
  90. data: data.update_data,
  91. }).catch((error) => {
  92. if (error.response?.status === 400 && error.response.data.detail === "重复数据") {
  93. ElMessage.error("数据重复,无法更新");
  94. }
  95. throw error;
  96. });
  97. };
  98. // 🔹 删除数据
  99. export const deleteItemApi = (params: {
  100. table: string;
  101. id: number;
  102. }) => {
  103. return api8000({
  104. url: "/admin/delete_item",
  105. method: "DELETE",
  106. params: params,
  107. });
  108. };
  109. // 🔹 导出数据 (改进版)
  110. export const exportData = (table: string, format: string = "xlsx") => {
  111. return api8000({
  112. url: "/admin/export_data",
  113. method: "GET",
  114. params: { table, fmt: format }, // 注意后端参数是 fmt
  115. responseType: "blob",
  116. })
  117. .then((response) => {
  118. // --- 修改从这里开始 ---
  119. let filename = `${table}_data.${format === "xlsx" ? "xlsx" : "csv"}`; // 默认文件名
  120. const contentDisposition = response.headers["content-disposition"];
  121. if (contentDisposition) {
  122. const filenameMatch = contentDisposition.match(/filename[^;]*=([^;]+)/);
  123. // 使用 filename*= 优先匹配 UTF-8 编码的文件名
  124. const utf8FilenameMatch = contentDisposition.match(/filename\*=UTF-8''(.+)/);
  125. if (utf8FilenameMatch) {
  126. try {
  127. // 解码 UTF-8 文件名
  128. filename = decodeURIComponent(utf8FilenameMatch[1]);
  129. } catch (e) {
  130. console.warn("解码 UTF-8 文件名失败:", e);
  131. }
  132. } else if (filenameMatch) {
  133. try {
  134. // 解码普通文件名 (可能包含引号)
  135. filename = decodeURIComponent(filenameMatch[1].trim().replace(/['"]/g, ''));
  136. } catch (e) {
  137. console.warn("解码文件名失败:", e);
  138. }
  139. }
  140. } else {
  141. console.warn("响应头中未找到 Content-Disposition,使用默认文件名");
  142. }
  143. // --- 修改到这里结束 ---
  144. const blob = new Blob([response.data], {
  145. type: format === "xlsx"
  146. ? "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
  147. : "text/csv",
  148. });
  149. const url = window.URL.createObjectURL(blob);
  150. const link = document.createElement("a");
  151. link.href = url;
  152. link.download = filename; // 使用解析或默认的文件名
  153. document.body.appendChild(link); // 兼容性建议
  154. link.click();
  155. document.body.removeChild(link); // 兼容性建议
  156. window.URL.revokeObjectURL(url);
  157. })
  158. .catch((error) => {
  159. console.error("❌ 导出数据失败:", error);
  160. throw error;
  161. });
  162. };
  163. // 🔹 导入数据 (修正:使用 api8000 替代 customRequest)
  164. export const importData = (table: string, file: File) => { // <--- 添加类型注解 here
  165. const formData = new FormData();
  166. formData.append("file", file);
  167. formData.append("table", table);
  168. // 返回 api8000 的 Promise 链
  169. return api8000({
  170. url: "/admin/import_data",
  171. method: "POST",
  172. data: formData,
  173. headers: {
  174. "Content-Type": "multipart/form-data",
  175. },
  176. }).then(response => {
  177. // 检查响应数据
  178. if (response.data && response.data.total_data === 0) {
  179. // 如果 total_data 为 0,认为是空数据导入,抛出一个错误
  180. // 可以自定义错误信息或使用 Error 对象
  181. throw new Error('导入的数据为空,请检查上传的文件是否包含有效数据。');
  182. // 或者使用后端可能返回的特定错误码/信息
  183. // throw { code: 'EMPTY_IMPORT_DATA', message: '导入的数据为空...', details: response.data };
  184. }
  185. // 如果数据不为空,正常返回响应
  186. return response;
  187. });
  188. // 调用者可以通过 .catch() 或 async/await 的 try...catch 捕获上面抛出的错误
  189. };
  190. // 🔹 下载模板
  191. // 辅助函数:解析 Content-Disposition 头以获取文件名
  192. function getFileNameFromContentDisposition(contentDispositionHeader: string): string | null {
  193. if (!contentDispositionHeader) {
  194. return null;
  195. }
  196. let fileName: string | null = null;
  197. // 尝试匹配 filename* (支持编码)
  198. // 格式: filename*=UTF-8''%E4%B8%AD%E6%96%87%E6%96%87%E4%BB%B6%E5%90%8D.xlsx
  199. const fileNameStarRegex = /filename\*=(?:UTF-8'')?([^;]+)/i;
  200. const fileNameStarMatch = contentDispositionHeader.match(fileNameStarRegex);
  201. if (fileNameStarMatch && fileNameStarMatch[1]) {
  202. try {
  203. // filename* 的值通常是百分号编码的 (Percent-encoding)
  204. fileName = decodeURIComponent(fileNameStarMatch[1]);
  205. return fileName;
  206. } catch (e) {
  207. console.warn("解码 filename* 失败:", e);
  208. // 如果解码失败,继续尝试 filename
  209. }
  210. }
  211. // 如果 filename* 不存在或解码失败,则尝试匹配 filename
  212. const fileNameRegex = /filename=([^;]+)/i; // 移除了开头的 ^,以防前面有空格或其他内容
  213. const fileNameMatch = contentDispositionHeader.match(fileNameRegex);
  214. if (fileNameMatch && fileNameMatch[1]) {
  215. fileName = fileNameMatch[1].trim(); // 去除首尾空格
  216. // 检查是否被双引号包围
  217. if (fileName.startsWith('"') && fileName.endsWith('"')) {
  218. fileName = fileName.substring(1, fileName.length - 1); // 移除双引号
  219. }
  220. // 尝试解码,以防它是 URL 编码的 (虽然不如 filename* 标准)
  221. try {
  222. fileName = decodeURIComponent(fileName);
  223. } catch (e) {
  224. console.warn("解码 filename 失败:", e);
  225. // 如果解码失败,就使用原始提取的字符串(可能包含引号或编码字符)
  226. // 或者可以在这里决定是返回 null 还是原始字符串
  227. }
  228. return fileName;
  229. }
  230. // 如果都未匹配到,则返回 null
  231. return null;
  232. }
  233. // 🔹 下载模板 (已修复文件名解析)
  234. export const downloadTemplate = (table: string, format: string = "excel") => {
  235. return api8000({
  236. url: "/admin/download_template",
  237. method: "GET",
  238. params: { table, format },
  239. responseType: "blob",
  240. })
  241. .then((response) => {
  242. const contentDisposition = response.headers["content-disposition"];
  243. let filename: string | null = null;
  244. if (contentDisposition) {
  245. filename = getFileNameFromContentDisposition(contentDisposition);
  246. }
  247. // 如果未能从响应头解析出文件名,则使用默认文件名
  248. if (!filename) {
  249. console.warn("无法从 Content-Disposition 头解析文件名,使用默认文件名。");
  250. filename = `${table}_template.${format === "excel" ? "xlsx" : "csv"}`;
  251. }
  252. const blob = new Blob([response.data], {
  253. type:
  254. format === "excel"
  255. ? "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
  256. : "text/csv",
  257. });
  258. const url = window.URL.createObjectURL(blob);
  259. const link = document.createElement("a");
  260. link.href = url;
  261. link.download = filename; // 使用解析或默认的文件名
  262. document.body.appendChild(link); // 推荐添加到 DOM 再点击
  263. link.click();
  264. document.body.removeChild(link); // 点击后移除
  265. window.URL.revokeObjectURL(url);
  266. })
  267. .catch((error) => {
  268. console.error("❌ 下载模板失败:", error);
  269. // 可以在这里添加用户提示,例如 ElMessage.error("模板下载失败");
  270. throw error;
  271. });
  272. };