ALIEZ 1 месяц назад
Родитель
Сommit
49d42e0fc4

+ 4 - 3
.env.development

@@ -3,10 +3,11 @@ NEXT_PUBLIC_APP_ENV=local
 
 # 主业务 :8005 | 汇款/出款/存款支付 :8504(与 src/lib/remittance-client、withdrawal-api、checkout-api 一致)
 NEXT_PUBLIC_API_BASE_URL_LOCAL=/api-backend
-API_PROXY_TARGET=http://192.168.0.33:8005
-# API_PROXY_TARGET=http://103.158.191.66:4000
+# API_PROXY_TARGET=http://192.168.0.33:8005
+API_PROXY_TARGET=http://103.158.191.66:8005
 NEXT_PUBLIC_REMITTANCE_API_BASE_URL=/api-backend-remittance
-API_PROXY_TARGET_REMITTANCE=http://192.168.0.33:8504
+# API_PROXY_TARGET_REMITTANCE=http://192.168.0.33:8504
+API_PROXY_TARGET_REMITTANCE=http://103.158.191.66:8504
 
 # __ORIGIN__:浏览器 → 当前域 + /api-backend;SSR → NEXT_PUBLIC_SITE_URL + /api-backend。端口仍由上面 API_PROXY_* 决定。
 NEXT_PUBLIC_API_BASE_URL_TEST=__ORIGIN__

+ 1 - 1
messages/zh.json

@@ -226,7 +226,7 @@
     "resultScholarship": "演示奖学金金额:{amount} USD",
     "resultContact": "请截图并联系客服完成后续流程(演示文案)。",
     "resultFail": "有题目未通过,可重新作答。",
-    "backAccount": "返回会员中心"
+    "backAccount": "返回用户中心"
   },
   "legal": {
     "termsTitle": "服务条款",





BIN
public/about/analysis/1.png


BIN
public/about/analysis/2.png


+ 1 - 1
src/app/[locale]/account/change-password/page.tsx

@@ -96,7 +96,7 @@ export default function ChangePasswordPage() {
       {ok ? <p className="mt-4 text-sm text-emerald-700">密码修改成功。</p> : null}
       <p className="mt-8 text-center text-sm">
         <Link href="/account" className="text-[var(--accent)] hover:underline">
-          返回会员中心
+          返回用户中心
         </Link>
       </p>
     </div>

+ 60 - 43
src/app/[locale]/account/withdraw-apply/page.tsx

@@ -159,33 +159,43 @@ export default function WithdrawApplyPage() {
   const isBankTelegraphic = selectedChannel?.type === "BANK_TELEGRAPHIC";
   const needCpf = selectedChannel?.code === "PAY_RETAILER_REMIT_PAY_KEY_BRW";
 
+  function resetApplyForm() {
+    setSelectedSavedId("");
+    setSelectedBankCode("");
+    setAddress("");
+    setAmount("");
+    setAgree(false);
+    setAgreeExtra(false);
+    setAgencyNo("");
+    setCpf("");
+    setBankUnameInput("");
+    setBankCardNumInput("");
+    setBankNameInput("");
+    setBankBranchNameInput("");
+    setSwiftCodeInput("");
+    setCustomBankCodeInput("");
+    setBankAddrInput("");
+    setTelegraphicCurrency("USD");
+    setCardUnameInput("");
+    setCardNumInput("");
+    setCardCvvInput("");
+    setCardExpiryInput("");
+  }
+
   useEffect(() => {
     let cancelled = false;
     async function loadBase() {
       setChannelsLoading(true);
       setChannelsError(null);
-      setSavedAccountsError(null);
       try {
-        const [channelsResult, savedResult] = await Promise.allSettled([
-          fetchWithdrawChannels(),
-          fetchSavedWithdrawAccounts(),
-        ]);
+        const channelsResult = await fetchWithdrawChannels();
         if (cancelled) return;
-        if (channelsResult.status === "fulfilled") {
-          setChannels(channelsResult.value);
-        } else {
-          const err = channelsResult.reason as Error;
-          setChannelsError(err?.message || "提款通道加载失败");
-          setChannels([]);
-        }
-
-        if (savedResult.status === "fulfilled") {
-          setSavedAccounts(savedResult.value);
-        } else {
-          const err = savedResult.reason as Error;
-          setSavedAccountsError(err?.message || "收款信息加载失败");
-          setSavedAccounts([]);
-        }
+        setChannels(channelsResult);
+      } catch (e) {
+        if (cancelled) return;
+        const err = e as Error;
+        setChannelsError(err?.message || "提款通道加载失败");
+        setChannels([]);
       } finally {
         if (!cancelled) {
           setChannelsLoading(false);
@@ -199,7 +209,29 @@ export default function WithdrawApplyPage() {
   }, []);
 
   useEffect(() => {
-    if (!selectedChannel) {
+    if (!applyDialogOpen) return;
+    let cancelled = false;
+    async function loadSavedAccounts() {
+      setSavedAccountsError(null);
+      try {
+        const list = await fetchSavedWithdrawAccounts();
+        if (cancelled) return;
+        setSavedAccounts(list);
+      } catch (e) {
+        if (cancelled) return;
+        const err = e as Error;
+        setSavedAccountsError(err?.message || "收款信息加载失败");
+        setSavedAccounts([]);
+      }
+    }
+    void loadSavedAccounts();
+    return () => {
+      cancelled = true;
+    };
+  }, [applyDialogOpen]);
+
+  useEffect(() => {
+    if (!selectedChannel || !applyDialogOpen) {
       setBankOptions([]);
       setSelectedBankCode("");
       return;
@@ -243,7 +275,7 @@ export default function WithdrawApplyPage() {
     return () => {
       cancelled = true;
     };
-  }, [selectedChannel]);
+  }, [selectedChannel, applyDialogOpen]);
 
   useEffect(() => {
     if (!selectedSavedAccount) return;
@@ -360,25 +392,7 @@ export default function WithdrawApplyPage() {
         title: "提交成功",
         message: "提款申请已提交,请等待平台审核。",
       });
-      setAmount("");
-      setAddress("");
-      setAgree(false);
-      setAgreeExtra(false);
-      setSelectedSavedId("");
-      setAgencyNo("");
-      setCpf("");
-      setBankUnameInput("");
-      setBankCardNumInput("");
-      setBankNameInput("");
-      setBankBranchNameInput("");
-      setSwiftCodeInput("");
-      setCustomBankCodeInput("");
-      setBankAddrInput("");
-      setTelegraphicCurrency("USD");
-      setCardUnameInput("");
-      setCardNumInput("");
-      setCardCvvInput("");
-      setCardExpiryInput("");
+      resetApplyForm();
     } catch (e) {
       const err = e as Error;
       setResultDialog({
@@ -510,7 +524,7 @@ export default function WithdrawApplyPage() {
 
       <p className="mt-8 text-center text-sm">
         <Link href="/account" className="text-[var(--accent)] hover:underline">
-          返回会员中心
+          返回用户中心
         </Link>
       </p>
 
@@ -553,7 +567,10 @@ export default function WithdrawApplyPage() {
               <h2 className="text-sm font-semibold text-[var(--navy)]">填写提款信息</h2>
               <button
                 type="button"
-                onClick={() => setApplyDialogOpen(false)}
+                onClick={() => {
+                  resetApplyForm();
+                  setApplyDialogOpen(false);
+                }}
                 className="ui-interactive-btn rounded border border-[var(--border)] px-2 py-1 text-xs text-[var(--muted)]"
               >
                 关闭

+ 68 - 5
src/app/[locale]/courses/[slug]/files-list-client.tsx

@@ -43,9 +43,20 @@ export function CourseFilesListClient({ goodsId, allHref, allLabel }: Props) {
   const canPrev = page > 1 && !loading;
   const canNext = !loading && page * pageSize < total;
   const previewDurationMs = 220;
+  const [iframeLoadFailed, setIframeLoadFailed] = useState(false);
+
+  function getPreviewKind(url: string): "image" | "video" | "audio" | "pdf" | "iframe" {
+    const normalized = url.toLowerCase().split("?")[0].split("#")[0];
+    if (/\.(png|jpe?g|gif|webp|bmp|svg)$/.test(normalized)) return "image";
+    if (/\.(mp4|webm|ogg|mov|m3u8)$/.test(normalized)) return "video";
+    if (/\.(mp3|wav|aac|m4a|flac|oga)$/.test(normalized)) return "audio";
+    if (/\.pdf$/.test(normalized)) return "pdf";
+    return "iframe";
+  }
 
   function openPreview(file: CourseFile) {
     setActiveFile(file);
+    setIframeLoadFailed(false);
     requestAnimationFrame(() => setIsPreviewVisible(true));
   }
 
@@ -230,11 +241,63 @@ export function CourseFilesListClient({ goodsId, allHref, allLabel }: Props) {
                 </button>
               </div>
             </div>
-            <iframe
-              title={activeFile.title}
-              src={activeFile.downloadUrl}
-              className="h-[65vh] w-full bg-white"
-            />
+            {(() => {
+              const kind = getPreviewKind(activeFile.downloadUrl);
+              if (kind === "image") {
+                return (
+                  // eslint-disable-next-line @next/next/no-img-element
+                  <img
+                    alt={activeFile.title}
+                    src={activeFile.downloadUrl}
+                    className="max-h-[65vh] w-full bg-white object-contain"
+                  />
+                );
+              }
+              if (kind === "video") {
+                return (
+                  <video
+                    src={activeFile.downloadUrl}
+                    controls
+                    preload="metadata"
+                    className="h-[65vh] w-full bg-black"
+                  />
+                );
+              }
+              if (kind === "audio") {
+                return (
+                  <div className="flex h-[40vh] items-center justify-center bg-white px-6">
+                    <audio src={activeFile.downloadUrl} controls className="w-full max-w-xl" />
+                  </div>
+                );
+              }
+              if (kind === "pdf" || kind === "iframe") {
+                return (
+                  <div className="relative">
+                    {!iframeLoadFailed ? (
+                      <iframe
+                        title={activeFile.title}
+                        src={activeFile.downloadUrl}
+                        className="h-[65vh] w-full bg-white"
+                        onError={() => setIframeLoadFailed(true)}
+                      />
+                    ) : (
+                      <div className="flex h-[40vh] flex-col items-center justify-center gap-3 bg-white px-6 text-center">
+                        <p className="text-sm text-slate-600">该文件不支持站内嵌入预览,请在新窗口打开查看。</p>
+                        <a
+                          href={activeFile.downloadUrl}
+                          target="_blank"
+                          rel="noopener noreferrer"
+                          className="inline-flex rounded-full border border-slate-300 px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-100"
+                        >
+                          {t("fileOpenNewWindow")}
+                        </a>
+                      </div>
+                    )}
+                  </div>
+                );
+              }
+              return null;
+            })()}
           </div>
         </div>
       ) : null}

+ 55 - 18
src/data/courses.ts

@@ -1,4 +1,5 @@
 import { apiPost } from "@/lib/api";
+import { getApiBaseUrl } from "@/lib/env";
 
 export type GoodsType = 1 | 2 | 3 | 4 | 5;
 export type CustomType = 0 | 1 | 2;
@@ -373,7 +374,7 @@ function normalizeVideo(item: GoodsVideoListItem): CourseVideo | null {
     title: title || videoName || "未命名视频",
     introduction,
     frontUrl,
-    playUrl: linkUrl ?? fileUrl ?? playUrl,
+    playUrl: toAbsoluteFileUrl(linkUrl ?? fileUrl ?? playUrl ?? ""),
     payType,
   };
 }
@@ -451,15 +452,52 @@ function parseDownloadUrls(rawDownload: unknown): string[] {
     .filter(Boolean);
 }
 
-function normalizeFile(item: GoodsSearchListItem): CourseFile[] {
-  const id = String(item.goodsId ?? item.id ?? "").trim();
+function toAbsoluteFileUrl(input: string): string {
+  const value = input.trim();
+  if (!value) return "";
+  const apiBaseUrl = getApiBaseUrl().replace(/\/$/, "");
+  const preferredOrigin = (() => {
+    try {
+      if (/^https?:\/\//i.test(apiBaseUrl)) return new URL(apiBaseUrl).origin;
+      return "";
+    } catch {
+      return "";
+    }
+  })();
+
+  if (/^https?:\/\//i.test(value)) {
+    try {
+      const current = new URL(value);
+      const isLocalHost = current.hostname === "localhost" || current.hostname === "127.0.0.1";
+      if (isLocalHost && preferredOrigin) {
+        return `${preferredOrigin}${current.pathname}${current.search}${current.hash}`;
+      }
+      return value;
+    } catch {
+      return value;
+    }
+  }
+  if (value.startsWith("//")) {
+    if (typeof window !== "undefined" && window.location?.protocol) {
+      return `${window.location.protocol}${value}`;
+    }
+    return `https:${value}`;
+  }
+
+  const path = value.startsWith("/") ? value : `/${value}`;
+  // 优先走同源反向代理,避免本地/线上域名不一致导致资源不可达。
+  return `/api-backend${path}`;
+}
+
+function normalizeFileFromVideo(item: GoodsVideoListItem): CourseFile[] {
+  const id = String(item.videoId ?? item.id ?? "").trim();
   if (!id) return [];
-  const title = String(item.title ?? item.goodsName ?? item.name ?? "").trim();
+  const title = String(item.title ?? item.videoName ?? item.videoTitle ?? item.name ?? "").trim();
   if (!title) return [];
-  const downloadUrls = parseDownloadUrls(item.download);
+  const downloadUrls = parseDownloadUrls(item.fileUrl);
   if (downloadUrls.length === 0) return [];
   const introduction = String(
-    item.introduction ?? item.subtitle ?? item.subTitle ?? item.intro ?? "",
+    item.introduction ?? item.description ?? item.intro ?? "",
   ).trim();
   const frontUrl =
     typeof item.frontUrl === "string" && item.frontUrl.trim()
@@ -470,7 +508,7 @@ function normalizeFile(item: GoodsSearchListItem): CourseFile[] {
     title,
     introduction,
     frontUrl,
-    downloadUrl,
+    downloadUrl: toAbsoluteFileUrl(downloadUrl),
   }));
 }
 
@@ -543,26 +581,25 @@ export async function fetchCourseFiles(
   const normalizedGoodsId = goodsId.trim();
   const current = Number.isFinite(page.current) ? Math.max(1, page.current) : 1;
   const row = Number.isFinite(page.row) ? Math.max(1, page.row) : 10;
+  if (!normalizedGoodsId) {
+    return { list: [], page: { current, row, total: 0 } };
+  }
   try {
     const payload = await apiPost<
-      GoodsSearchListResponse,
-      { goodsType: GoodsType; page: { current: number; row: number } }
-    >("/goods/search/list", {
-      goodsType: 5,
+      GoodsVideoListResponse,
+      { goodsId: string; page: { current: number; row: number } }
+    >("/goods/video/search/list", {
+      goodsId: normalizedGoodsId,
       page: { current, row },
     });
-    const list = extractList(payload).flatMap(normalizeFile);
-    const matched = normalizedGoodsId
-      ? list.filter((f) => f.id.startsWith(`${normalizedGoodsId}-`))
-      : [];
-    const finalList = matched.length > 0 ? matched : list;
-    const serverPage = extractSearchPage(payload);
+    const finalList = extractVideoList(payload).flatMap(normalizeFileFromVideo);
+    const serverPage = extractVideoPage(payload);
     return {
       list: finalList,
       page: {
         current: serverPage.current || current,
         row: serverPage.row || row,
-        total: matched.length > 0 ? matched.length : serverPage.total || finalList.length,
+        total: serverPage.total || finalList.length,
       },
     };
   } catch {