api.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421
  1. import axios from "axios";
  2. import { useUserStore } from "@/store/user";
  3. const API_URL = import.meta.env.VITE_API_URL;
  4. const VIDEO_API_URL = import.meta.env.VITE_VIDEO_API_URL;
  5. const plat_id = import.meta.env.VITE_VIDEO_PLAT_ID;
  6. const channel_id = import.meta.env.VITE_VIDEO_CHANNEL_ID;
  7. const user_id = import.meta.env.VITE_VIDEO_USER_ID;
  8. const token = import.meta.env.VITE_VIDEO_TOKEN;
  9. const api = axios.create({
  10. baseURL: API_URL,
  11. headers: { "Content-Type": "application/json" },
  12. });
  13. const videoApi = axios.create({
  14. baseURL: VIDEO_API_URL,
  15. headers: { "Content-Type": "multipart/form-data" },
  16. });
  17. // api请求拦截器
  18. api.interceptors.request.use(
  19. (config) => {
  20. const userStore = useUserStore();
  21. if (userStore.token) {
  22. config.headers.Authorization = `Bearer ${userStore.token}`;
  23. }
  24. return config;
  25. },
  26. (error) => Promise.reject(error)
  27. );
  28. // api响应拦截器
  29. api.interceptors.response.use(
  30. (response) => response,
  31. (error) => {
  32. if (error.response?.status === 401) {
  33. const userStore = useUserStore();
  34. userStore.logout();
  35. }
  36. console.error("API Error:", error.response?.data || error);
  37. return Promise.reject(error.response?.data || error);
  38. }
  39. );
  40. // videoApi响应拦截器
  41. videoApi.interceptors.response.use(
  42. (response) => response,
  43. (error) => {
  44. console.error("Video API Error:", error.response?.data || error);
  45. return Promise.reject(error.response?.data || error);
  46. }
  47. );
  48. // videoApi共用参数
  49. function createVideoFormData(
  50. device: string,
  51. page_count = 1,
  52. page_size = 20,
  53. extraParams: Record<string, string> = {}
  54. ): FormData {
  55. const formData = new FormData();
  56. const commonParams: Record<string, string> = {
  57. plat_id,
  58. channel_id,
  59. user_id,
  60. app_type: "4",
  61. token,
  62. device,
  63. device_type: "3",
  64. page_count: String(page_count),
  65. page_size: String(page_size),
  66. };
  67. Object.entries({ ...commonParams, ...extraParams }).forEach(([k, v]) =>
  68. formData.append(k, v)
  69. );
  70. return formData;
  71. }
  72. // 统一视频请求
  73. async function videoRequest(
  74. endpoint: string,
  75. formData: FormData
  76. ): Promise<any> {
  77. const res = await videoApi.post(endpoint, formData);
  78. return res.data;
  79. }
  80. /**
  81. * ===================== 用户相关接口 =====================
  82. */
  83. export const login = async (name: string, password: string): Promise<any> => {
  84. const res = await api.post("/member/login", { name, password });
  85. return res.data;
  86. };
  87. export const register = async (
  88. name: string,
  89. password: string,
  90. email?: string,
  91. phone?: string,
  92. code?: string
  93. ): Promise<any> => {
  94. const res = await api.post("/member/register", {
  95. name,
  96. password,
  97. email: email || null,
  98. phone: phone || null,
  99. code: code || null,
  100. });
  101. return res.data;
  102. };
  103. export const profile = async (): Promise<any> => {
  104. const res = await api.get("/member/profile");
  105. return res.data;
  106. };
  107. export const newGuest = async (code?: string, ref?: string): Promise<any> => {
  108. const params: any = {};
  109. if (code) params.code = code;
  110. if (ref) params.ref = ref;
  111. const res = await api.get("/member/guest", { params });
  112. return res.data;
  113. };
  114. export const upgradeGuest = async (
  115. userId: number,
  116. name: string,
  117. password: string,
  118. email?: string,
  119. phone?: string
  120. ): Promise<any> => {
  121. const res = await api.post("/member/guestUpgrade", {
  122. userId,
  123. name,
  124. password,
  125. email: email || null,
  126. phone: phone || null,
  127. });
  128. return res.data;
  129. };
  130. // 会员购买
  131. export const purchaseMember = async (
  132. userId: number,
  133. type: string
  134. ): Promise<any> => {
  135. const res = await api.post("/payment/vip/create", { userId, type });
  136. return res.data;
  137. };
  138. // 单片购买
  139. export const purchaseSingle = async (
  140. userId: number,
  141. resourceId: string
  142. ): Promise<any> => {
  143. const res = await api.post("/payment/single/create", { userId, resourceId });
  144. return res.data;
  145. };
  146. // 用户支付订单查询
  147. export const userQueryOrder = async (
  148. orderNo?: string,
  149. resourceId?: string
  150. ): Promise<any> => {
  151. const params = {
  152. orderNo: orderNo || undefined,
  153. resourceId: resourceId || undefined,
  154. };
  155. const res = await api.get(`/payment/user/query`, {
  156. params,
  157. });
  158. return res.data;
  159. };
  160. // 查询用户是否已购买特定单片资源
  161. export const checkSinglePurchase = async (resourceId: string): Promise<any> => {
  162. const res = await api.get("/payment/single/query", {
  163. params: { resourceId },
  164. });
  165. return res.data;
  166. };
  167. // 获取用户的单片机购买记录列表(分页)
  168. export const getSinglePurchaseList = async (
  169. page = 0,
  170. size = 20
  171. ): Promise<any> => {
  172. const res = await api.get("/payment/single/list", {
  173. params: { page, size },
  174. });
  175. return res.data;
  176. };
  177. // 获取价格配置
  178. export const getPriceConfig = async (): Promise<any> => {
  179. const res = await api.get("/config/team/user");
  180. return res.data;
  181. };
  182. // 更新用户信息(用户名和邮箱)
  183. export const updateProfile = async (
  184. name: string,
  185. email?: string
  186. ): Promise<any> => {
  187. const res = await api.put("/member/profile", {
  188. name,
  189. email: email || null,
  190. });
  191. return res.data;
  192. };
  193. // 重置密码
  194. export const resetPassword = async (password: string): Promise<any> => {
  195. const res = await api.post("/member/reset-password", {
  196. password,
  197. });
  198. return res.data;
  199. };
  200. // 获取团队主题颜色
  201. export const getTeamTheme = async (): Promise<{ themeColor: string }> => {
  202. const res = await api.get("/teams/my-theme");
  203. return res.data;
  204. };
  205. // 封禁当前用户
  206. export const banUser = async (): Promise<any> => {
  207. const res = await api.post("/member/ban");
  208. return res.data;
  209. };
  210. // 检查IP是否被封禁
  211. export const checkBanStatus = async (): Promise<{ ip: string; isBanned: boolean }> => {
  212. const res = await api.get("/member/check-ban");
  213. return res.data;
  214. };
  215. /**
  216. * ===================== 广告栏相关接口 =====================
  217. */
  218. // 根据位置获取广告栏列表(公开接口)
  219. export const getBannersByPosition = async (
  220. position: "top" | "middle" | "bottom"
  221. ): Promise<any[]> => {
  222. const res = await api.get(`/banners/position/${position}`);
  223. return res.data;
  224. };
  225. // 记录广告栏点击
  226. export const recordBannerClick = async (bannerId: number): Promise<any> => {
  227. const res = await api.post(`/banners/${bannerId}/click`);
  228. return res.data;
  229. };
  230. /**
  231. * ===================== 视频相关接口 =====================
  232. */
  233. // 视频关键字 查询接口
  234. export const searchVideoByKeyword = async (
  235. device: string,
  236. keyword: string,
  237. page_count = 1,
  238. page_size = 20,
  239. resource_type?: "long" | "short",
  240. lang?: string
  241. ): Promise<any> => {
  242. const formData = createVideoFormData(device, page_count, page_size, {
  243. keyword,
  244. ...(resource_type && { resource_type }),
  245. ...(lang && { lang }),
  246. });
  247. return videoRequest(`/media/${plat_id}/keyword`, formData);
  248. };
  249. // 视频查询接口(支持标签分类和类型查询)
  250. export const searchVideoByTags = async (
  251. device: string,
  252. page_count = 1,
  253. page_size = 20,
  254. tag?: string,
  255. resource_type?: "long" | "short",
  256. sort?: "view" | "like" | "time"
  257. ): Promise<any> => {
  258. const formData = createVideoFormData(device, page_count, page_size, {
  259. ...(tag && { tag }),
  260. ...(resource_type && { resource_type }),
  261. ...(sort && { sort }),
  262. });
  263. return videoRequest(`/media/${plat_id}/search`, formData);
  264. };
  265. // 视频详情接口
  266. export const getVideoDetail = async (
  267. device: string,
  268. resource_id: string
  269. ): Promise<any> => {
  270. const formData = createVideoFormData(device, 1, 20, { resource_id });
  271. return videoRequest(`/media/${user_id}/movie/play`, formData);
  272. };
  273. // 视频顶部标签 查询接口 - 已废弃,现在使用固定的菜单数据
  274. // export const getVideoMenu = async (
  275. // device: string,
  276. // type: "1" | "2",
  277. // page_count = 1,
  278. // page_size = 20
  279. // ): Promise<any> => {
  280. // const formData = createVideoFormData(device, page_count, page_size, { type });
  281. // return videoRequest(`/media/${plat_id}/menu`, formData);
  282. // };
  283. // 点播集 查询接口
  284. export const getVodList = async (
  285. device: string,
  286. hash: string,
  287. page_count = 1,
  288. page_size = 20
  289. ): Promise<any> => {
  290. const formData = createVideoFormData(device, page_count, page_size, { hash });
  291. return videoRequest(`/media/${plat_id}/menu/vods`, formData);
  292. };
  293. // ===================== 全局请求拦截器:移除 HLS 视频相关文件的 Range 请求头 =====================
  294. let originalXHROpen: any = null;
  295. let originalXHRSetRequestHeader: any = null;
  296. let originalFetch: any = null;
  297. let hlsInterceptorActive = false;
  298. const shouldRemoveRangeHeader = (url: string): boolean => {
  299. if (!url) return false;
  300. if (/\.m3u8(\?|$)/i.test(url)) {
  301. return true;
  302. }
  303. if (
  304. /\/dx\d+/i.test(url) || // 匹配 dx002, dx003 等
  305. /\/[^\/]+\.ts(\?|$)/i.test(url) || // 匹配 .ts 分片文件
  306. /\/segment\d+/i.test(url) || // 匹配 segment1, segment2 等
  307. /\/chunk\d+/i.test(url)
  308. ) {
  309. return true;
  310. }
  311. return false;
  312. };
  313. /**
  314. * 设置全局拦截器以移除 HLS 视频相关文件的 Range 请求头
  315. */
  316. const setupHLSInterceptor = (): void => {
  317. if (hlsInterceptorActive) return;
  318. originalXHROpen = XMLHttpRequest.prototype.open;
  319. originalXHRSetRequestHeader = XMLHttpRequest.prototype.setRequestHeader;
  320. originalFetch = window.fetch;
  321. XMLHttpRequest.prototype.open = function (
  322. method: string,
  323. url: string | URL,
  324. ...rest: any[]
  325. ) {
  326. (this as any)._url = url.toString();
  327. return originalXHROpen.apply(this, [method, url, ...rest]);
  328. };
  329. XMLHttpRequest.prototype.setRequestHeader = function (
  330. header: string,
  331. value: string
  332. ) {
  333. const url = (this as any)._url || "";
  334. if (shouldRemoveRangeHeader(url) && header.toLowerCase() === "range") {
  335. return;
  336. }
  337. return originalXHRSetRequestHeader.apply(this, [header, value]);
  338. };
  339. window.fetch = function (
  340. input: RequestInfo | URL,
  341. init?: RequestInit
  342. ): Promise<Response> {
  343. const url =
  344. typeof input === "string"
  345. ? input
  346. : input instanceof URL
  347. ? input.toString()
  348. : (input as Request).url;
  349. if (shouldRemoveRangeHeader(url)) {
  350. const modifiedInit = { ...init };
  351. if (modifiedInit.headers) {
  352. const headers = new Headers(modifiedInit.headers);
  353. headers.delete("Range");
  354. headers.delete("range");
  355. modifiedInit.headers = headers;
  356. } else {
  357. modifiedInit.headers = new Headers();
  358. }
  359. return originalFetch.apply(this, [input, modifiedInit]);
  360. }
  361. return originalFetch.apply(this, [input, init]);
  362. };
  363. hlsInterceptorActive = true;
  364. console.log("HLS Range Header Interceptor Activated");
  365. };
  366. if (typeof window !== "undefined") {
  367. setupHLSInterceptor();
  368. }
  369. export default api;