瀏覽代碼

pwa功能修复

wilhelm wong 1 月之前
父節點
當前提交
17bd5ffb50

+ 2 - 2
dev-dist/sw.js

@@ -67,7 +67,7 @@ if (!self.define) {
     });
   };
 }
-define(['./workbox-f2cb1a81'], (function (workbox) { 'use strict';
+define(['./workbox-f9d29cf2'], (function (workbox) { 'use strict';
 
   self.skipWaiting();
   workbox.clientsClaim();
@@ -79,7 +79,7 @@ define(['./workbox-f2cb1a81'], (function (workbox) { 'use strict';
    */
   workbox.precacheAndRoute([{
     "url": "index.html",
-    "revision": "0.e7somuoms2g"
+    "revision": "0.s2vqo2svsso"
   }], {});
   workbox.cleanupOutdatedCaches();
   workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {

+ 24 - 0
index.html

@@ -44,6 +44,30 @@
     <meta name="twitter:image" content="/icons/icon-512x512.png" />
     
     <title>Junma Show - 在线视频播放平台</title>
+    
+    <!-- 友盟统计 CNZZ -->
+    <script>
+      var _czc = _czc || [];
+      (function () {
+        var um = document.createElement("script");
+        um.src = "https://v1.cnzz.com/z.js?id=1281444456&async=1";
+        
+        // 添加加载成功回调(用于调试)
+        um.onload = function() {
+          console.log('[CNZZ统计] 脚本加载成功');
+          console.log('[CNZZ统计] 站点ID: 1281444456');
+          console.log('[CNZZ统计] _czc对象:', window._czc);
+        };
+        
+        // 添加加载失败回调(用于调试)
+        um.onerror = function() {
+          console.error('[CNZZ统计] 脚本加载失败,请检查网络连接');
+        };
+        
+        var s = document.getElementsByTagName("script")[0];
+        s.parentNode.insertBefore(um, s);
+      })();
+    </script>
   </head>
   <body>
     <noscript>

File diff suppressed because it is too large
+ 272 - 261
package-lock.json


+ 214 - 0
src/components/DomainReminderDialog.vue

@@ -0,0 +1,214 @@
+<template>
+  <div
+    v-if="modelValue"
+    class="fixed inset-0 z-50 flex items-center justify-center p-4"
+  >
+    <!-- 背景遮罩 -->
+    <div
+      class="absolute inset-0 bg-black/70 backdrop-blur-sm"
+      @click="closeDialog"
+    ></div>
+
+    <!-- 域名提示卡片 -->
+    <div
+      class="relative w-full max-w-md rounded-2xl bg-surface border border-white/10 p-6 shadow-xl"
+    >
+      <button
+        @click="closeDialog"
+        class="absolute right-4 top-4 h-8 w-8 rounded-full grid place-items-center text-white/60 hover:bg-white/10 hover:text-white transition"
+      >
+        <svg
+          viewBox="0 0 24 24"
+          width="20"
+          height="20"
+          fill="none"
+          stroke="currentColor"
+          stroke-width="2"
+        >
+          <path d="M18 6 6 18M6 6l12 12" />
+        </svg>
+      </button>
+
+      <div class="text-center mb-6">
+        <div class="mx-auto mb-4 w-16 h-16 rounded-full bg-emerald-500/20 flex items-center justify-center">
+          <svg
+            viewBox="0 0 24 24"
+            width="32"
+            height="32"
+            fill="none"
+            stroke="currentColor"
+            stroke-width="2"
+            class="text-emerald-400"
+          >
+            <path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5" />
+          </svg>
+        </div>
+        <h2 class="text-xl font-bold text-white/90 mb-2">
+          重要提示
+        </h2>
+        <p class="text-white/60 text-sm">
+          请记住我们的永久域名,以便随时访问
+        </p>
+      </div>
+
+      <div class="space-y-4">
+        <!-- 域名列表 -->
+        <div class="space-y-3">
+          <div
+            v-for="(domain, index) in domains"
+            :key="index"
+            class="flex items-center gap-3 p-4 rounded-lg bg-white/5 border border-white/10 hover:bg-white/10 transition"
+          >
+            <div class="flex-shrink-0 w-8 h-8 rounded-full bg-emerald-500/20 flex items-center justify-center">
+              <span class="text-emerald-400 text-sm font-semibold">{{ index + 1 }}</span>
+            </div>
+            <div class="flex-1">
+              <div class="text-white/90 font-medium text-lg">{{ domain }}</div>
+              <div class="text-white/50 text-xs mt-0.5">永久域名</div>
+            </div>
+            <button
+              @click="copyDomain(domain, index)"
+              class="flex-shrink-0 px-3 py-1.5 rounded-lg bg-emerald-500/20 text-emerald-400 hover:bg-emerald-500/30 transition text-sm font-medium"
+            >
+              {{ copiedIndex === index ? "已复制" : "复制" }}
+            </button>
+          </div>
+        </div>
+
+        <!-- 提示信息 -->
+        <div class="p-3 rounded-lg bg-blue-500/10 border border-blue-500/20">
+          <p class="text-blue-400 text-xs text-center">
+            💡 建议收藏这些域名,避免因域名变更而无法访问
+          </p>
+        </div>
+
+        <!-- 确认按钮 -->
+        <button
+          @click="closeDialog"
+          class="w-full py-2.5 rounded-lg bg-brand text-slate-900 font-medium hover:bg-brand/90 transition"
+        >
+          我知道了
+        </button>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted, watch } from "vue";
+import { getRetentionDomainsByLanding } from "@/services/api";
+
+const props = defineProps<{
+  modelValue: boolean;
+}>();
+
+const emit = defineEmits<{
+  (e: "update:modelValue", value: boolean): void;
+}>();
+
+const domains = ref<string[]>([]);
+const copiedIndex = ref<number | null>(null);
+
+// 从 URL 中提取域名(处理 https://example.com 格式)
+const extractDomain = (urlOrDomain: string): string => {
+  try {
+    // 如果包含协议,使用 URL 对象解析
+    if (urlOrDomain.includes('://')) {
+      const url = new URL(urlOrDomain);
+      return url.hostname;
+    }
+    // 否则直接返回
+    return urlOrDomain;
+  } catch {
+    // 如果解析失败,返回原值
+    return urlOrDomain;
+  }
+};
+
+// 获取域名列表(通过新接口获取留存域名)
+const fetchDomains = async () => {
+  try {
+    // 获取当前访问域名作为落地域名
+    const currentDomain = window.location.hostname;
+    
+    if (!currentDomain) {
+      domains.value = [];
+      return;
+    }
+    
+    // 调用新接口获取留存域名
+    // 接口会根据当前落地域名返回对应的留存域名列表
+    // 如果没有找到留存域名,会返回落地域名本身
+    // 如果没有找到落地域名,会返回输入的域名(作为临时对象)
+    const retentionDomains = await getRetentionDomainsByLanding(currentDomain);
+    
+    // 从返回的数组中提取域名
+    if (Array.isArray(retentionDomains) && retentionDomains.length > 0) {
+      domains.value = retentionDomains.map((item: any) => {
+        // 如果返回的是对象,提取 domain 字段;如果是字符串,直接使用
+        return typeof item === 'string' ? item : (item?.domain || currentDomain);
+      });
+    } else {
+      // 如果返回为空,至少显示当前域名
+      domains.value = [currentDomain];
+    }
+  } catch (error) {
+    console.error("获取留存域名失败:", error);
+    // 如果获取失败,至少显示当前域名
+    const currentDomain = window.location.hostname;
+    if (currentDomain) {
+      domains.value = [currentDomain];
+    } else {
+      domains.value = [];
+    }
+  }
+};
+
+// 当对话框打开时获取域名
+watch(() => props.modelValue, (newVal) => {
+  if (newVal && domains.value.length === 0) {
+    fetchDomains();
+  }
+});
+
+onMounted(() => {
+  if (props.modelValue) {
+    fetchDomains();
+  }
+});
+
+const closeDialog = () => {
+  emit("update:modelValue", false);
+};
+
+const copyDomain = async (domain: string, index: number) => {
+  try {
+    await navigator.clipboard.writeText(domain);
+    copiedIndex.value = index;
+    // 2秒后恢复"复制"文字
+    setTimeout(() => {
+      copiedIndex.value = null;
+    }, 2000);
+  } catch (err) {
+    console.error("复制失败:", err);
+    // 降级方案:使用传统方法
+    const textArea = document.createElement("textarea");
+    textArea.value = domain;
+    textArea.style.position = "fixed";
+    textArea.style.opacity = "0";
+    document.body.appendChild(textArea);
+    textArea.select();
+    try {
+      document.execCommand("copy");
+      copiedIndex.value = index;
+      setTimeout(() => {
+        copiedIndex.value = null;
+      }, 2000);
+    } catch (e) {
+      console.error("复制失败:", e);
+    }
+    document.body.removeChild(textArea);
+  }
+};
+</script>
+

+ 40 - 4
src/components/LoginDialog.vue

@@ -80,7 +80,19 @@
           />
         </div>
 
-
+        <!-- 注册时显示确认密码字段 -->
+        <div v-if="isRegister">
+          <label for="confirmPassword" class="block text-sm text-white/70 mb-1.5"
+            >确认密码</label
+          >
+          <input
+            type="password"
+            id="confirmPassword"
+            v-model="confirmPassword"
+            class="w-full px-4 py-2.5 rounded-lg bg-white/5 border border-white/10 text-white/90 focus:outline-none focus:ring-2 focus:ring-brand/50"
+            placeholder="请再次输入密码"
+          />
+        </div>
 
         <div v-if="error" class="text-red-400 text-sm py-1">
           {{ error }}
@@ -142,6 +154,7 @@ const emit = defineEmits<{
 
 const username = ref("");
 const password = ref("");
+const confirmPassword = ref("");
 const phone = ref("");
 const error = ref("");
 const isLoading = ref(false);
@@ -155,6 +168,7 @@ const closeDialog = () => {
 const toggleMode = () => {
   isRegister.value = !isRegister.value;
   error.value = "";
+  confirmPassword.value = "";
 };
 
 const handleButtonClick = () => {
@@ -192,11 +206,31 @@ const handleRegister = async () => {
     return;
   }
 
+  // 验证确认密码
+  if (!confirmPassword.value) {
+    error.value = "请确认密码";
+    return;
+  }
+
+  // 验证两次密码是否一致
+  if (password.value !== confirmPassword.value) {
+    error.value = "两次输入的密码不一致,请重新输入";
+    return;
+  }
+
   // 使用手机号作为用户名
   const finalUsername = phone.value;
 
-  // 从localStorage读取memberCode作为code参数
-  const memberCode = localStorage.getItem("memberCode");
+  // 从localStorage读取memberCode作为推广码参数
+  // 优先级:URL中的code > URL中的memberCode > localStorage中的memberCode
+  const urlParams = new URLSearchParams(window.location.search);
+  const urlCode = urlParams.get("code");
+  const urlMemberCode = urlParams.get("memberCode");
+  const storedMemberCode = localStorage.getItem("memberCode");
+  const memberCode = urlCode || urlMemberCode || storedMemberCode;
+
+  // 获取当前落地域名
+  const landingDomain = window.location.hostname;
 
   try {
     error.value = "";
@@ -207,7 +241,8 @@ const handleRegister = async () => {
       password.value,
       undefined, // email
       phone.value, // phone
-      memberCode || undefined // code参数,从memberCode获取
+      memberCode || undefined, // memberCode参数,推广码
+      landingDomain || undefined // landingDomain参数,落地域名
     );
 
     // 注册成功后清除memberCode,避免重复使用
@@ -231,6 +266,7 @@ watch(
     if (!newVal) {
       username.value = "";
       password.value = "";
+      confirmPassword.value = "";
       phone.value = "";
       error.value = "";
       isLoading.value = false;

+ 0 - 4
src/components/VideoJSPlayer.vue

@@ -183,7 +183,6 @@ const processCover = async (url: string): Promise<void> => {
         processedCoverUrl.value = url;
       }
     } else {
-      console.log("使用原始封面URL");
       processedCoverUrl.value = url;
     }
 
@@ -225,7 +224,6 @@ const processVideo = async (url: string): Promise<void> => {
 const initVideoJSPlayer = async (): Promise<void> => {
   return new Promise(async (resolve, reject) => {
     if (!videoContainer.value || !processedVideoUrl.value) {
-      console.log("初始化失败:缺少容器或视频URL");
       reject(new Error("缺少容器或视频URL"));
       return;
     }
@@ -581,8 +579,6 @@ onUnmounted(() => {
 
 // 手动播放方法
 const playVideo = (): void => {
-  console.log("手动播放被调用");
-
   if (player.value) {
     player.value.play().catch((err: any) => {
       console.error("播放失败:", err);

+ 10 - 1
src/components/layout/MainLayout.vue

@@ -164,11 +164,20 @@ const createGuestAccount = async () => {
         console.warn("ref 参数解密失败,使用原始值");
         decryptedRef = refParam;
       }
+      // 保存解密后的 ref 到 localStorage 供注册时使用
+      if (decryptedRef) {
+        localStorage.setItem("ref", decryptedRef);
+        console.log("保存ref到localStorage:", decryptedRef);
+      }
     }
 
+    // 获取当前落地域名
+    const landingDomain = window.location.hostname;
+
     await userStore.createGuest(
       codeToUse || undefined,
-      decryptedRef || undefined
+      decryptedRef || undefined,
+      landingDomain || undefined
     );
     console.log("游客账号创建成功", userStore.userInfo);
   } catch (error) {

+ 24 - 24
src/data/freeVideo.ts

@@ -12,18 +12,18 @@ export interface FreeVideo {
  * 试看视频数据
  */
 export const freeVideos: FreeVideo[] = [
-  {
-    id: "free-1",
-    cover: "https://cknz.t1mg0oa.com/2209/CVELA54RLB2N/cover",
-    m3u8: "https://api.xmqspco.com//v/mMPq4Ua6YhQmDBxqIVQ73Q.m3u8?user_id=605500911&plat_id=60005",
-    name: "姐姐是情色博主.精东影业",
-  },
-  {
-    id: "free-2",
-    cover: "https://cknz.t1mg0oa.com/2209/77G8BSFETL8V/cover",
-    m3u8: "https://api.xmqspco.com//v/YccfZkGyfDqhnkAQGP-tZA.m3u8?user_id=605500911&plat_id=60005",
-    name: "常去保健室的我和保健科小花老师上课时放学后的中出SEX 小花暖   中文字幕 角色剧情 制服诱惑 ",
-  },
+//   {
+//     id: "free-1",
+//     cover: "https://cknz.t1mg0oa.com/2209/CVELA54RLB2N/cover",
+//     m3u8: "https://api.xmqspco.com//v/mMPq4Ua6YhQmDBxqIVQ73Q.m3u8?user_id=605500911&plat_id=60005",
+//     name: "姐姐是情色博主.精东影业",
+//   },
+//   {
+//     id: "free-2",
+//     cover: "https://cknz.t1mg0oa.com/2209/77G8BSFETL8V/cover",
+//     m3u8: "https://api.xmqspco.com//v/YccfZkGyfDqhnkAQGP-tZA.m3u8?user_id=605500911&plat_id=60005",
+//     name: "常去保健室的我和保健科小花老师上课时放学后的中出SEX 小花暖   中文字幕 角色剧情 制服诱惑 ",
+//   },
   {
     id: "free-3",
     cover: "https://cknz.t1mg0oa.com/2208/MDSQ00006700/cover",
@@ -36,18 +36,18 @@ export const freeVideos: FreeVideo[] = [
     m3u8: "https://api.xmqspco.com//v/gS4t6fB-EU6vS4WGeLO60Q.m3u8?user_id=605500911&plat_id=60005",
     name: "きょう肉肉引き裂き靴下撕黑丝OL秘书#黑丝#包臀裙#气质#性感#丝袜诱惑#蜜桃臀#肉感#肉肉编号24C8F8E6",
   },
-  {
-    id: "free-5",
-    cover: "https://cknz.t1mg0oa.com/2208/K1CP3FVKKS5K/cover",
-    m3u8: "https://api.xmqspco.com//v/S_tGP-yHk44OZyq70mnk-w.m3u8?user_id=605500911&plat_id=60005",
-    name: "强 スワッピング&种付け轮  辈达からの凌 の果て 孕まされた2人 巨乳若妻 お腹 子 种は友达 旦那乙爱丽丝 姬咲华  凌辱强暴 多P群交 角色剧情  ",
-  },
-  {
-    id: "free-6",
-    cover: "https://cknz.t1mg0oa.com/2208/C00IU6149KDT/cover",
-    m3u8: "https://api.xmqspco.com//v/j2tCad8_KNrE083MnrgPGQ.m3u8?user_id=605500911&plat_id=60005",
-    name: "え、ここでヤルの ダメッ すぐイっちゃうってばぁ 早漏ビンカン娘にいきなり即ズボッ大作戦 1日完全密着イクイク声我慢ドキュメント森千里   直接开啪 ",
-  },
+//   {
+//     id: "free-5",
+//     cover: "https://cknz.t1mg0oa.com/2208/K1CP3FVKKS5K/cover",
+//     m3u8: "https://api.xmqspco.com//v/S_tGP-yHk44OZyq70mnk-w.m3u8?user_id=605500911&plat_id=60005",
+//     name: "强 スワッピング&种付け轮  辈达からの凌 の果て 孕まされた2人 巨乳若妻 お腹 子 种は友达 旦那乙爱丽丝 姬咲华  凌辱强暴 多P群交 角色剧情  ",
+//   },
+//   {
+//     id: "free-6",
+//     cover: "https://cknz.t1mg0oa.com/2208/C00IU6149KDT/cover",
+//     m3u8: "https://api.xmqspco.com//v/j2tCad8_KNrE083MnrgPGQ.m3u8?user_id=605500911&plat_id=60005",
+//     name: "え、ここでヤルの ダメッ すぐイっちゃうってばぁ 早漏ビンカン娘にいきなり即ズボッ大作戦 1日完全密着イクイク声我慢ドキュメント森千里   直接开啪 ",
+//   },
 ];
 
 // 默认导出

+ 171 - 4
src/services/api.ts

@@ -102,14 +102,16 @@ export const register = async (
   password: string,
   email?: string,
   phone?: string,
-  code?: string
+  memberCode?: string,
+  landingDomain?: string
 ): Promise<any> => {
   const res = await api.post("/member/register", {
     name,
     password,
     email: email || null,
     phone: phone || null,
-    code: code || null,
+    memberCode: memberCode || null,
+    landingDomain: landingDomain || null,
   });
   return res.data;
 };
@@ -119,10 +121,15 @@ export const profile = async (): Promise<any> => {
   return res.data;
 };
 
-export const newGuest = async (code?: string, ref?: string): Promise<any> => {
+export const newGuest = async (
+  code?: string,
+  ref?: string,
+  landingDomain?: string
+): Promise<any> => {
   const params: any = {};
   if (code) params.code = code;
   if (ref) params.ref = ref;
+  if (landingDomain) params.landingDomain = landingDomain;
 
   const res = await api.get("/member/guest", { params });
   return res.data;
@@ -203,6 +210,26 @@ export const getPriceConfig = async (): Promise<any> => {
   return res.data;
 };
 
+// 获取系统配置 super_domain
+export const getSuperDomain = async (): Promise<any> => {
+  const res = await api.get("/config/super_domain");
+  return res.data;
+};
+
+/**
+ * ===================== 落地域名池相关接口 =====================
+ */
+
+// 根据当前落地域名获取留存域名
+// 接口地址: GET /api/landing-domain-pools/retention-by-landing
+// 权限要求: 无需认证,任何人都可以访问
+export const getRetentionDomainsByLanding = async (domain: string): Promise<any[]> => {
+  const res = await api.get("/landing-domain-pools/retention-by-landing", {
+    params: { domain },
+  });
+  return res.data;
+};
+
 // 更新用户信息(用户名和邮箱)
 export const updateProfile = async (
   name: string,
@@ -259,6 +286,12 @@ export const recordBannerClick = async (bannerId: number): Promise<any> => {
   return res.data;
 };
 
+// 记录页面点击
+export const recordPageClick = async (pageType: "home" | "video"): Promise<any> => {
+  const res = await api.post("/page-clicks/click", { pageType });
+  return res.data;
+};
+
 /**
  * ===================== 视频相关接口 =====================
  */
@@ -334,6 +367,16 @@ let originalXHRSetRequestHeader: any = null;
 let originalFetch: any = null;
 let hlsInterceptorActive = false;
 
+// keycode 缓存 Map,key 是 URL,value 是 ArrayBuffer
+const keycodeCache = new Map<string, ArrayBuffer>();
+
+// 判断是否是 keycode 请求
+const isKeycodeRequest = (url: string): boolean => {
+  if (!url) return false;
+  // 匹配 keycode 相关的 URL
+  return /keycode/i.test(url) || /\/key(\?|$)/i.test(url);
+};
+
 const shouldRemoveRangeHeader = (url: string): boolean => {
   if (!url) return false;
 
@@ -372,6 +415,94 @@ const setupHLSInterceptor = (): void => {
     return originalXHROpen.apply(this, [method, url, ...rest]);
   };
 
+  // 拦截 XHR 的 send 方法来处理 keycode 缓存
+  const originalXHRSend = XMLHttpRequest.prototype.send;
+  XMLHttpRequest.prototype.send = function (body?: Document | XMLHttpRequestBodyInit | null) {
+    const url = (this as any)._url || "";
+    const xhr = this;
+    
+    // 处理 keycode 缓存
+    if (isKeycodeRequest(url)) {
+      const cachedKeycode = keycodeCache.get(url);
+      if (cachedKeycode) {
+        // 使用 setTimeout 异步触发,确保事件监听器已设置
+        setTimeout(() => {
+          // 设置响应类型
+          if (xhr.responseType === "" || xhr.responseType === "text") {
+            try {
+              xhr.responseType = "arraybuffer";
+            } catch (e) {
+              // 某些浏览器可能不支持修改 responseType
+            }
+          }
+          
+          // 设置响应数据
+          Object.defineProperty(xhr, "status", { value: 200, writable: true, configurable: true });
+          Object.defineProperty(xhr, "statusText", { value: "OK", writable: true, configurable: true });
+          Object.defineProperty(xhr, "response", { value: cachedKeycode, writable: true, configurable: true });
+          Object.defineProperty(xhr, "readyState", { value: XMLHttpRequest.DONE, writable: true, configurable: true });
+          
+          // 触发事件
+          if (xhr.onreadystatechange) {
+            xhr.onreadystatechange(new Event("readystatechange") as any);
+          }
+          if (xhr.onload) {
+            xhr.onload(new Event("load") as any);
+          }
+          if (typeof xhr.addEventListener === "function") {
+            xhr.dispatchEvent(new Event("readystatechange"));
+            xhr.dispatchEvent(new Event("load"));
+          }
+        }, 0);
+        return;
+      }
+
+      // 如果没有缓存,正常请求并缓存
+      const originalOnLoad = xhr.onload;
+      const originalOnReadyStateChange = xhr.onreadystatechange;
+      
+      xhr.onload = function(event: any) {
+        // 缓存响应
+        if (xhr.response && xhr.response instanceof ArrayBuffer) {
+          keycodeCache.set(url, xhr.response);
+        }
+        
+        // 调用原始回调
+        if (originalOnLoad) {
+          originalOnLoad.call(xhr, event);
+        }
+      };
+      
+      xhr.onreadystatechange = function(event: any) {
+        // 缓存响应(在 readyState 为 DONE 时)
+        if (xhr.readyState === XMLHttpRequest.DONE && xhr.response && xhr.response instanceof ArrayBuffer) {
+          if (!keycodeCache.has(url)) {
+            keycodeCache.set(url, xhr.response);
+          }
+        }
+        
+        // 调用原始回调
+        if (originalOnReadyStateChange) {
+          originalOnReadyStateChange.call(xhr, event);
+        }
+      };
+      
+      // 也监听 addEventListener 方式注册的事件
+      if (xhr.addEventListener) {
+        const loadHandler = () => {
+          if (xhr.response && xhr.response instanceof ArrayBuffer) {
+            if (!keycodeCache.has(url)) {
+              keycodeCache.set(url, xhr.response);
+            }
+          }
+        };
+        xhr.addEventListener("load", loadHandler);
+      }
+    }
+
+    return originalXHRSend.apply(this, [body]);
+  };
+
   XMLHttpRequest.prototype.setRequestHeader = function (
     header: string,
     value: string
@@ -394,6 +525,43 @@ const setupHLSInterceptor = (): void => {
         ? input.toString()
         : (input as Request).url;
 
+    // 处理 keycode 缓存
+    if (isKeycodeRequest(url)) {
+      // 检查缓存
+      const cachedKeycode = keycodeCache.get(url);
+      if (cachedKeycode) {
+        // 返回缓存的响应
+        return Promise.resolve(
+          new Response(cachedKeycode, {
+            status: 200,
+            statusText: "OK",
+            headers: {
+              "Content-Type": "application/octet-stream",
+              "Content-Length": String(cachedKeycode.byteLength),
+            },
+          })
+        );
+      }
+
+      // 如果没有缓存,请求并缓存
+      return originalFetch.apply(this, [input, init]).then((response: Response) => {
+        // 克隆响应以便读取和缓存
+        const clonedResponse = response.clone();
+        
+        // 读取响应并缓存
+        clonedResponse
+          .arrayBuffer()
+          .then((buffer: ArrayBuffer) => {
+            keycodeCache.set(url, buffer);
+          })
+          .catch((err: Error) => {
+            console.error(`[Keycode Cache] 缓存 keycode 失败:`, err);
+          });
+
+        return response;
+      });
+    }
+
     if (shouldRemoveRangeHeader(url)) {
       const modifiedInit = { ...init };
       if (modifiedInit.headers) {
@@ -411,7 +579,6 @@ const setupHLSInterceptor = (): void => {
   };
 
   hlsInterceptorActive = true;
-  console.log("HLS Range Header Interceptor Activated");
 };
 
 if (typeof window !== "undefined") {

+ 16 - 4
src/store/user.ts

@@ -123,9 +123,17 @@ export const useUserStore = defineStore("user", () => {
     password: string,
     email?: string,
     phone?: string,
-    code?: string
+    memberCode?: string,
+    landingDomain?: string
   ) => {
-    const response = await apiRegister(name, password, email, phone, code);
+    const response = await apiRegister(
+      name,
+      password,
+      email,
+      phone,
+      memberCode,
+      landingDomain
+    );
     setToken(response.token);
     setUserInfo(response.user);
     userManuallyLoggedOut.value = false;
@@ -165,9 +173,13 @@ export const useUserStore = defineStore("user", () => {
     priceStore.resetPriceConfig();
   };
 
-  const createGuest = async (code?: string, ref?: string) => {
+  const createGuest = async (
+    code?: string,
+    ref?: string,
+    landingDomain?: string
+  ) => {
     try {
-      const response = await newGuest(code, ref);
+      const response = await newGuest(code, ref, landingDomain);
       setToken(response.token);
       setUserInfo(response.user);
       userManuallyLoggedOut.value = false;

+ 228 - 2
src/views/Account.vue

@@ -8,9 +8,11 @@ import {
   userQueryOrder,
   updateProfile,
   resetPassword,
+  getSuperDomain,
 } from "@/services/api";
 import { vipLevelToText, VipLevel } from "@/types/vip";
 import { getShareRecords, ShareRecord } from "@/services/shareApi";
+import { useStorage } from "@vueuse/core";
 
 const userStore = useUserStore();
 const priceStore = usePriceStore();
@@ -36,11 +38,20 @@ const showErrorDialog = ref(false);
 const showSuccessDialog = ref(false);
 const showEditUserDialog = ref(false);
 const showResetPasswordDialog = ref(false);
+const showAccountInfoDialog = ref(false);
 const errorMessage = ref("");
 const successMessage = ref("");
+const domains = ref<string[]>([]);
+
+// 记录用户是否已经看过账户信息提示(基于用户ID)
+const accountInfoShownKey = computed(() => `accountInfoShown_${userStore.userInfo?.id || 'guest'}`);
+const accountInfoShown = useStorage(accountInfoShownKey.value, false);
 const currentOrderNo = ref("");
+// 记录已处理过的订单号,防止重复显示成功弹窗
+const processedOrderNos = ref<Set<string>>(new Set());
 const upgradeForm = ref({
   password: null,
+  confirmPassword: null,
   phone: null,
 });
 const editUserForm = ref({
@@ -75,6 +86,77 @@ const membershipPlans = computed(() => priceStore.getMembershipPlans);
 const selectedPlan = ref("");
 const selectedPayment = ref("alipay");
 
+// 从 URL 中提取域名(处理 https://example.com 格式)
+const extractDomain = (urlOrDomain: string): string => {
+  try {
+    // 如果包含协议,使用 URL 对象解析
+    if (urlOrDomain.includes('://')) {
+      const url = new URL(urlOrDomain);
+      return url.hostname;
+    }
+    // 否则直接返回
+    return urlOrDomain;
+  } catch {
+    // 如果解析失败,返回原值
+    return urlOrDomain;
+  }
+};
+
+// 获取域名列表
+const fetchDomains = async () => {
+  try {
+    // 获取系统配置中的 super_domain
+    const config = await getSuperDomain();
+    const superDomainValue = config?.value;
+    
+    // 从 value 字段提取域名
+    let superDomain: string | undefined;
+    if (superDomainValue && typeof superDomainValue === 'string') {
+      superDomain = extractDomain(superDomainValue);
+    }
+    
+    // 获取当前访问域名
+    const currentDomain = window.location.hostname;
+    
+    // 构建域名列表:第一个是 super_domain,第二个是当前域名
+    const domainList: string[] = [];
+    
+    if (superDomain) {
+      domainList.push(superDomain);
+    }
+    
+    // 如果当前域名与 super_domain 不同,则添加当前域名
+    if (currentDomain && currentDomain !== superDomain) {
+      domainList.push(currentDomain);
+    }
+    
+    domains.value = domainList.length > 0 ? domainList : [currentDomain || ''];
+  } catch (error) {
+    console.error("获取域名配置失败:", error);
+    // 如果获取失败,至少显示当前域名
+    const currentDomain = window.location.hostname;
+    if (currentDomain) {
+      domains.value = [currentDomain];
+    } else {
+      domains.value = [];
+    }
+  }
+};
+
+// 关闭账户信息提示弹窗并记录已提示
+const closeAccountInfoDialog = () => {
+  const shownKey = `accountInfoShown_${userStore.userInfo?.id || 'guest'}`;
+  window.localStorage.setItem(shownKey, 'true');
+  showAccountInfoDialog.value = false;
+};
+
+// 当账户信息对话框打开时获取域名
+watch(() => showAccountInfoDialog.value, (newVal) => {
+  if (newVal && domains.value.length === 0) {
+    fetchDomains();
+  }
+});
+
 const handleLogout = () => {
   userStore.logout();
 };
@@ -271,6 +353,18 @@ const handleUpgrade = async () => {
     return;
   }
 
+  // 验证确认密码
+  if (!upgradeForm.value.confirmPassword) {
+    alert("请确认密码");
+    return;
+  }
+
+  // 验证两次密码是否一致
+  if (upgradeForm.value.password !== upgradeForm.value.confirmPassword) {
+    alert("两次输入的密码不一致,请重新输入");
+    return;
+  }
+
   isLoading.value = true;
   try {
     // 使用手机号作为正式用户的用户名
@@ -291,6 +385,7 @@ const handleUpgrade = async () => {
     // 重置表单
     upgradeForm.value = {
       password: null,
+      confirmPassword: null,
       phone: null,
     };
 
@@ -318,8 +413,21 @@ const checkPaymentSuccess = async () => {
       if (response.status === 1) {
         // 订单状态确认正确,同步用户信息
         await userStore.sync();
-        showSuccess(`会员购买成功`);
         currentOrderNo.value = "";
+        
+        // 检查用户是否是游客用户,如果是则显示账户信息提示弹窗
+        if (isGuestUser.value) {
+          // 检查是否已经提示过
+          const shownKey = `accountInfoShown_${userStore.userInfo?.id || 'guest'}`;
+          const hasShown = window.localStorage.getItem(shownKey) === 'true';
+          if (!hasShown) {
+            showAccountInfoDialog.value = true;
+          } else {
+            showSuccess(`会员购买成功`);
+          }
+        } else {
+          showSuccess(`会员购买成功`);
+        }
       } else {
         // 订单状态异常,仍同步用户信息
         await userStore.sync();
@@ -343,15 +451,36 @@ const handleQueryOrder = async () => {
     return;
   }
 
+  // 检查是否已经处理过这个订单
+  if (processedOrderNos.value.has(currentOrderNo.value)) {
+    return;
+  }
+
   try {
     const response = await userQueryOrder(currentOrderNo.value);
 
     if (response.status === 1) {
+      // 标记订单已处理
+      processedOrderNos.value.add(currentOrderNo.value);
+      
       // 支付成功
       await userStore.sync();
       showPaymentWaitingDialog.value = false;
-      showSuccess("会员购买成功");
       currentOrderNo.value = "";
+      
+      // 检查用户是否是游客用户,如果是则显示账户信息提示弹窗
+      if (isGuestUser.value) {
+        // 检查是否已经提示过
+        const shownKey = `accountInfoShown_${userStore.userInfo?.id || 'guest'}`;
+        const hasShown = window.localStorage.getItem(shownKey) === 'true';
+        if (!hasShown) {
+          showAccountInfoDialog.value = true;
+        } else {
+          showSuccess("会员购买成功");
+        }
+      } else {
+        showSuccess("会员购买成功");
+      }
     } else {
       showError(response.msg || "会员购买失败");
     }
@@ -706,6 +835,19 @@ onMounted(async () => {
             </p>
           </div>
 
+          <div>
+            <label class="block text-sm font-medium text-white/70 mb-1">
+              确认密码 <span class="text-red-400">*</span>
+            </label>
+            <input
+              v-model="upgradeForm.confirmPassword"
+              type="password"
+              required
+              class="w-full px-3 py-2 bg-white/5 border border-white/10 rounded-lg text-white placeholder-white/40 focus:ring-2 focus:ring-brand focus:border-transparent"
+              placeholder="请再次输入密码"
+            />
+          </div>
+
           <div class="flex gap-3 pt-4">
             <button
               type="button"
@@ -1095,6 +1237,90 @@ onMounted(async () => {
         </button>
       </div>
     </div>
+
+    <!-- 账户信息提示弹窗 -->
+    <div
+      v-if="showAccountInfoDialog"
+      class="fixed inset-0 bg-black/50 flex items-center justify-center z-[9999]"
+      @click.self="showAccountInfoDialog = false"
+    >
+      <div
+        class="bg-surface border border-white/10 rounded-2xl p-6 w-full max-w-md mx-4"
+      >
+        <!-- 提示图标 -->
+        <div class="mb-4">
+          <div
+            class="w-12 h-12 mx-auto bg-yellow-500/20 rounded-full flex items-center justify-center"
+          >
+            <svg
+              class="w-6 h-6 text-yellow-400"
+              fill="none"
+              stroke="currentColor"
+              viewBox="0 0 24 24"
+            >
+              <path
+                stroke-linecap="round"
+                stroke-linejoin="round"
+                stroke-width="2"
+                d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"
+              />
+            </svg>
+          </div>
+        </div>
+
+        <!-- 提示信息 -->
+        <h3 class="text-lg font-semibold text-white/90 mb-4 text-center">
+          重要提示
+        </h3>
+        <div
+          class="bg-yellow-500/10 border border-yellow-500/20 rounded-lg p-4 mb-6"
+        >
+          <p class="text-sm text-yellow-200 mb-3">
+            您还未成为正式用户,请务必记录以下账户信息:
+          </p>
+          <div class="space-y-2">
+            <div class="flex items-center justify-between">
+              <span class="text-sm text-white/70">用户名:</span>
+              <span class="text-sm font-mono font-semibold text-white/90">
+                {{ userStore.userInfo?.name || "未知" }}
+              </span>
+            </div>
+            <div class="flex items-center justify-between">
+              <span class="text-sm text-white/70">默认密码:</span>
+              <span class="text-sm font-mono font-semibold text-white/90">
+                password123
+              </span>
+            </div>
+            <div class="flex flex-col gap-2">
+              <span class="text-sm text-white/70">防丢网址:</span>
+              <div class="space-y-1">
+                <div
+                  v-for="(domain, index) in domains"
+                  :key="index"
+                  class="text-sm font-mono font-semibold text-white/90"
+                >
+                  {{ domain }}
+                </div>
+                <div v-if="domains.length === 0" class="text-sm font-mono font-semibold text-white/90">
+                  加载中...
+                </div>
+              </div>
+            </div>
+          </div>
+          <p class="text-xs text-yellow-200/80 mt-3">
+            请妥善保管您的账户信息,建议尽快升级为正式用户并修改密码。
+          </p>
+        </div>
+
+        <!-- 确认按钮 -->
+        <button
+          @click="closeAccountInfoDialog"
+          class="w-full px-4 py-2 bg-brand text-slate-900 rounded-lg hover:bg-brand/90 transition"
+        >
+          我已记录
+        </button>
+      </div>
+    </div>
   </section>
 </template>
 <style scoped>

+ 74 - 17
src/views/Home.vue

@@ -119,8 +119,28 @@
       <div class="text-white/50">加载中...</div>
     </div>
 
+    <!-- 错误状态 -->
+    <div v-if="!loading && errorMessage" class="text-center py-8">
+      <div class="space-y-3">
+        <div class="text-red-400 text-lg font-medium">
+          {{ isTimeout ? "资源响应超时" : "加载失败" }}
+        </div>
+        <div class="text-white/70 text-sm">{{ errorMessage }}</div>
+        <button
+          @click="
+            isSearchMode
+              ? handleSearch(currentSearchKeyword, currentPage)
+              : loadVideosByTag(selectedMenu, currentPage)
+          "
+          class="mt-4 px-4 py-2 rounded-lg bg-emerald-500/20 text-emerald-400 border border-emerald-400/30 hover:bg-emerald-500/30 transition text-sm"
+        >
+          重试
+        </button>
+      </div>
+    </div>
+
     <!-- 空状态 -->
-    <div v-if="!loading && videoList.length === 0" class="text-center py-8">
+    <div v-if="!loading && !errorMessage && videoList.length === 0" class="text-center py-8">
       <div class="text-white/50">暂无视频内容</div>
     </div>
 
@@ -381,6 +401,9 @@
 
     <!-- 登录弹窗 -->
     <!-- <LoginDialog v-model="showLoginDialog" @login-success="onLoginSuccess" /> -->
+
+    <!-- 域名提示弹窗 -->
+    <DomainReminderDialog v-model="showDomainReminder" />
   </section>
 </template>
 
@@ -396,6 +419,7 @@ import {
 import { freeVideos } from "@/data/freeVideo";
 import VideoJSPlayer from "@/components/VideoJSPlayer.vue";
 import Banner from "@/components/Banner.vue";
+import DomainReminderDialog from "@/components/DomainReminderDialog.vue";
 import { useUserStore } from "@/store/user";
 import { VipLevel } from "@/types/vip";
 
@@ -431,6 +455,10 @@ const device = generateMacAddress();
 const videoList = ref<any[]>([]);
 const loading = ref(false);
 
+// 错误状态
+const errorMessage = ref<string>("");
+const isTimeout = ref(false);
+
 // 分页数据
 const currentPage = ref(1);
 const pageSize = ref(24);
@@ -446,7 +474,6 @@ const sortOptions = computed(() => {
     { value: "like", label: "点赞" },
   ];
 
-  console.log("isVipUser", isVipUser);
   if (!isVipUser) {
     options.push({ value: "free", label: "试看" });
   }
@@ -463,7 +490,6 @@ watch(
       newVipLevel !== VipLevel.GUEST && newVipLevel !== VipLevel.FREE;
     if (isVip && selectedSort.value === "free") {
       selectedSort.value = "time";
-      console.log("用户已升级为会员,自动切换到最新排序");
     }
   }
 );
@@ -475,6 +501,9 @@ const showAllTags = ref(false);
 const isSearchMode = ref(false);
 const currentSearchKeyword = ref("");
 
+// 域名提示弹窗
+const showDomainReminder = ref(false);
+
 // 格式化时长
 const formatDuration = (duration: string | number): string => {
   const seconds = parseInt(String(duration));
@@ -498,7 +527,6 @@ const jumpToPageHandler = () => {
 
   // 检查输入是否为空
   if (!inputValue) {
-    console.log("输入框为空,不执行跳转");
     return;
   }
 
@@ -506,39 +534,29 @@ const jumpToPageHandler = () => {
 
   // 检查是否为有效数字
   if (isNaN(page)) {
-    console.log("输入的不是有效数字:", inputValue);
     jumpToPage.value = "";
     return;
   }
 
   // 检查页码范围
   if (page < 1 || page > totalPages.value) {
-    console.log(`页码超出范围: ${page}, 总页数: ${totalPages.value}`);
     jumpToPage.value = "";
     return;
   }
 
   // 检查是否与当前页相同
   if (page === currentPage.value) {
-    console.log("跳转到当前页,无需操作");
     jumpToPage.value = "";
     return;
   }
 
-  console.log(
-    `准备跳转到第 ${page} 页,当前页: ${currentPage.value}, 总页数: ${totalPages.value}`
-  );
-
   try {
     if (isSearchMode.value) {
-      console.log("搜索模式,执行搜索跳转");
       handleSearch(currentSearchKeyword.value, page);
     } else {
-      console.log("标签模式,执行标签跳转");
       loadVideosByTag(selectedMenu.value || "", page);
     }
     jumpToPage.value = "";
-    console.log("跳转完成");
   } catch (error) {
     console.error("跳转失败:", error);
     jumpToPage.value = "";
@@ -589,6 +607,9 @@ const showFreeVideos = () => {
   // 清除搜索模式
   isSearchMode.value = false;
   currentSearchKeyword.value = "";
+  // 清除错误状态
+  errorMessage.value = "";
+  isTimeout.value = false;
 };
 
 // 搜索功能
@@ -605,6 +626,9 @@ const handleSearch = async (keyword: string, page = 1) => {
   currentSearchKeyword.value = keyword.trim();
   loading.value = true;
   currentPage.value = page;
+  // 清除之前的错误状态
+  errorMessage.value = "";
+  isTimeout.value = false;
 
   try {
     const response = await searchVideoByKeyword(
@@ -623,16 +647,30 @@ const handleSearch = async (keyword: string, page = 1) => {
         totalPages.value = response.data.pageInfo.pageCount || 0;
         totalCount.value = response.data.pageInfo.pageTotal || 0;
       }
+      // 清除错误状态
+      errorMessage.value = "";
+      isTimeout.value = false;
     } else {
       videoList.value = [];
       totalPages.value = 0;
       totalCount.value = 0;
+      errorMessage.value = response.message || "获取视频列表失败";
+      isTimeout.value = false;
     }
-  } catch (error) {
+  } catch (error: any) {
     console.error("搜索视频失败:", error);
     videoList.value = [];
     totalPages.value = 0;
     totalCount.value = 0;
+    
+    // 判断是否是超时错误
+    if (error.code === "ECONNABORTED" || error.message?.includes("timeout")) {
+      isTimeout.value = true;
+      errorMessage.value = "资源响应超时,请稍后重试";
+    } else {
+      isTimeout.value = false;
+      errorMessage.value = error.message || error.response?.data?.message || "获取视频列表失败,请稍后重试";
+    }
   } finally {
     loading.value = false;
   }
@@ -682,6 +720,9 @@ const playVideo = (video: any) => {
 const loadVideosByTag = async (tagHash: string, page = 1) => {
   loading.value = true;
   currentPage.value = page;
+  // 清除之前的错误状态
+  errorMessage.value = "";
+  isTimeout.value = false;
 
   try {
     const response = await searchVideoByTags(
@@ -700,16 +741,30 @@ const loadVideosByTag = async (tagHash: string, page = 1) => {
         totalPages.value = response.data.pageInfo.pageCount || 0;
         totalCount.value = response.data.pageInfo.pageTotal || 0;
       }
+      // 清除错误状态
+      errorMessage.value = "";
+      isTimeout.value = false;
     } else {
       videoList.value = [];
       totalPages.value = 0;
       totalCount.value = 0;
+      errorMessage.value = response.message || "获取视频列表失败";
+      isTimeout.value = false;
     }
-  } catch (error) {
+  } catch (error: any) {
     console.error("获取视频列表失败:", error);
     videoList.value = [];
     totalPages.value = 0;
     totalCount.value = 0;
+    
+    // 判断是否是超时错误
+    if (error.code === "ECONNABORTED" || error.message?.includes("timeout")) {
+      isTimeout.value = true;
+      errorMessage.value = "资源响应超时,请稍后重试";
+    } else {
+      isTimeout.value = false;
+      errorMessage.value = error.message || error.response?.data?.message || "获取视频列表失败,请稍后重试";
+    }
   } finally {
     loading.value = false;
   }
@@ -768,7 +823,6 @@ const restoreSavedState = async () => {
 
     // 使用setTimeout确保DOM完全更新后再滚动
     setTimeout(() => {
-      console.log("恢复滚动位置:", savedState.scrollPosition);
       window.scrollTo({
         top: savedState.scrollPosition,
         behavior: "auto",
@@ -842,6 +896,9 @@ onMounted(() => {
   initializeVideoMenus();
   // 监听Header搜索事件
   window.addEventListener("header-search", handleHeaderSearch as EventListener);
+  
+  // 每次进入首页都显示域名提示弹窗
+  showDomainReminder.value = true;
 });
 
 onBeforeUnmount(() => {

+ 7 - 0
src/views/Purchased.vue

@@ -2,6 +2,7 @@
 import { ref, onMounted, onUnmounted } from "vue";
 import { useRouter } from "vue-router";
 import { getSinglePurchaseList, getVideoDetail } from "@/services/api";
+import DomainReminderDialog from "@/components/DomainReminderDialog.vue";
 
 interface PurchaseRecord {
   id: number;
@@ -29,6 +30,7 @@ const isLoading = ref(true);
 const currentPage = ref(0);
 const hasMore = ref(true);
 const coverUrls = ref<Record<string, string>>({}); // 存储解密后的封面URLs
+const showDomainReminder = ref(false);
 
 // 生成设备标识
 const generateMacAddress = (): string => {
@@ -272,6 +274,8 @@ const playVideo = (item: PurchaseItem) => {
 
 onMounted(() => {
   loadPurchasedItems();
+  // 页面加载后显示域名提示弹窗
+  showDomainReminder.value = true;
 });
 
 // 清理URL对象,防止内存泄漏
@@ -457,6 +461,9 @@ onUnmounted(() => {
         <p>暂无购买记录</p>
       </div>
     </div>
+
+    <!-- 域名提示弹窗 -->
+    <DomainReminderDialog v-model="showDomainReminder" />
   </section>
 </template>
 

+ 423 - 29
src/views/VideoPlayer.vue

@@ -518,6 +518,90 @@
       </div>
     </div>
 
+    <!-- 账户信息提示弹窗 -->
+    <div
+      v-if="showAccountInfoDialog"
+      class="fixed inset-0 bg-black/50 flex items-center justify-center z-[9999]"
+      @click.self="showAccountInfoDialog = false"
+    >
+      <div
+        class="bg-surface border border-white/10 rounded-2xl p-6 w-full max-w-md mx-4"
+      >
+        <!-- 提示图标 -->
+        <div class="mb-4">
+          <div
+            class="w-12 h-12 mx-auto bg-yellow-500/20 rounded-full flex items-center justify-center"
+          >
+            <svg
+              class="w-6 h-6 text-yellow-400"
+              fill="none"
+              stroke="currentColor"
+              viewBox="0 0 24 24"
+            >
+              <path
+                stroke-linecap="round"
+                stroke-linejoin="round"
+                stroke-width="2"
+                d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"
+              />
+            </svg>
+          </div>
+        </div>
+
+        <!-- 提示信息 -->
+        <h3 class="text-lg font-semibold text-white/90 mb-4 text-center">
+          重要提示
+        </h3>
+        <div
+          class="bg-yellow-500/10 border border-yellow-500/20 rounded-lg p-4 mb-6"
+        >
+          <p class="text-sm text-yellow-200 mb-3">
+            您还未成为正式用户,请务必记录以下账户信息:
+          </p>
+          <div class="space-y-2">
+            <div class="flex items-center justify-between">
+              <span class="text-sm text-white/70">用户名:</span>
+              <span class="text-sm font-mono font-semibold text-white/90">
+                {{ userStore.userInfo?.name || "未知" }}
+              </span>
+            </div>
+            <div class="flex items-center justify-between">
+              <span class="text-sm text-white/70">默认密码:</span>
+              <span class="text-sm font-mono font-semibold text-white/90">
+                password123
+              </span>
+            </div>
+            <div class="flex flex-col gap-2">
+              <span class="text-sm text-white/70">防丢网址:</span>
+              <div class="space-y-1">
+                <div
+                  v-for="(domain, index) in domains"
+                  :key="index"
+                  class="text-sm font-mono font-semibold text-white/90"
+                >
+                  {{ domain }}
+                </div>
+                <div v-if="domains.length === 0" class="text-sm font-mono font-semibold text-white/90">
+                  加载中...
+                </div>
+              </div>
+            </div>
+          </div>
+          <p class="text-xs text-yellow-200/80 mt-3">
+            请妥善保管您的账户信息,建议尽快升级为正式用户并修改密码。
+          </p>
+        </div>
+
+        <!-- 确认按钮 -->
+        <button
+          @click="closeAccountInfoDialog"
+          class="w-full px-4 py-2 bg-brand text-slate-900 rounded-lg hover:bg-brand/90 transition"
+        >
+          我已记录
+        </button>
+      </div>
+    </div>
+
     <!-- 分享链接弹窗 -->
     <ShareDialog
       v-model="showShareLinkDialog"
@@ -687,12 +771,15 @@ import {
   purchaseMember,
   userQueryOrder,
   checkSinglePurchase,
+  getSuperDomain,
+  recordPageClick,
 } from "@/services/api";
 import VideoJSPlayer from "@/components/VideoJSPlayer.vue";
 import ShareDialog from "@/components/ShareDialog.vue";
 import { useUserStore } from "@/store/user";
 import { usePriceStore } from "@/store/price";
 import { handleShareLinkAccess } from "@/services/shareApi";
+import { decryptRefParam } from "@/utils/crypto";
 
 // 路由相关
 const route = useRoute();
@@ -700,7 +787,7 @@ const router = useRouter();
 
 // 用户信息
 const userStore = useUserStore();
-const { isLoginUser, isVipUser } = storeToRefs(userStore);
+const { isLoginUser, isVipUser, isGuestUser } = storeToRefs(userStore);
 
 // 价格配置
 const priceStore = usePriceStore();
@@ -769,11 +856,71 @@ const singleCurrentOrderNo = ref("");
 const isUserSynced = ref(false);
 
 // 会员购买按钮点击
-const handleMembershipClick = () => {
+const handleMembershipClick = async () => {
   // 特殊游览器滚动
   specialBrowserScroll();
 
-  // 已登录,显示购买弹窗
+  // 如果用户未登录,先创建账号并获取价格
+  if (!isLoginUser.value) {
+    try {
+      // 获取 URL 参数
+      const urlParams = new URLSearchParams(window.location.search);
+      const inviteCode = urlParams.get("code");
+      const memberCode = urlParams.get("memberCode");
+      const refParam = urlParams.get("ref");
+      const ivParam = urlParams.get("iv");
+
+      // 优先使用URL中的code参数,如果没有则使用memberCode
+      const codeToUse = inviteCode || memberCode || localStorage.getItem("memberCode");
+
+      // 如果存在 ref 参数,尝试解密
+      let decryptedRef: string | null = null;
+      if (refParam) {
+        decryptedRef = await decryptRefParam(refParam, ivParam || undefined);
+        if (!decryptedRef) {
+          // 如果解密失败,可能 ref 参数本身就是未加密的,使用原始值
+          decryptedRef = refParam;
+        }
+        // 保存解密后的 ref 到 localStorage 供注册时使用
+        if (decryptedRef) {
+          localStorage.setItem("ref", decryptedRef);
+          console.log("保存ref到localStorage:", decryptedRef);
+        }
+      }
+
+      // 获取当前落地域名
+      const landingDomain = window.location.hostname;
+
+      // 创建游客账户(会自动获取价格配置)
+      const guestResponse = await userStore.createGuest(
+        codeToUse || undefined,
+        decryptedRef || undefined,
+        landingDomain || undefined
+      );
+      
+      if (!guestResponse) {
+        showError("创建账户失败,请重试");
+        return;
+      }
+    } catch (error) {
+      console.error("创建账户失败:", error);
+      showError("创建账户失败,请重试");
+      return;
+    }
+  }
+
+  // 确保价格配置已加载(团队相关价格可能不同)
+  if (!priceStore.isPriceConfigLoaded) {
+    try {
+      await priceStore.fetchPriceConfig();
+    } catch (error) {
+      console.error("价格配置加载失败:", error);
+      showError("价格配置加载失败,请重试");
+      return;
+    }
+  }
+
+  // 显示购买弹窗
   showMembershipPurchaseModal.value = true;
 };
 
@@ -782,8 +929,57 @@ const handleSinglePurchaseClick = async () => {
   // 特殊游览器滚动
   specialBrowserScroll();
 
+  // 如果用户未登录,先创建账号并获取价格
+  if (!isLoginUser.value) {
+    try {
+      // 获取 URL 参数
+      const urlParams = new URLSearchParams(window.location.search);
+      const inviteCode = urlParams.get("code");
+      const memberCode = urlParams.get("memberCode");
+      const refParam = urlParams.get("ref");
+      const ivParam = urlParams.get("iv");
+
+      // 优先使用URL中的code参数,如果没有则使用memberCode
+      const codeToUse = inviteCode || memberCode || localStorage.getItem("memberCode");
+
+      // 如果存在 ref 参数,尝试解密
+      let decryptedRef: string | null = null;
+      if (refParam) {
+        decryptedRef = await decryptRefParam(refParam, ivParam || undefined);
+        if (!decryptedRef) {
+          // 如果解密失败,可能 ref 参数本身就是未加密的,使用原始值
+          decryptedRef = refParam;
+        }
+        // 保存解密后的 ref 到 localStorage 供注册时使用
+        if (decryptedRef) {
+          localStorage.setItem("ref", decryptedRef);
+          console.log("保存ref到localStorage:", decryptedRef);
+        }
+      }
+
+      // 获取当前落地域名
+      const landingDomain = window.location.hostname;
+
+      // 创建游客账户(会自动获取价格配置)
+      const guestResponse = await userStore.createGuest(
+        codeToUse || undefined,
+        decryptedRef || undefined,
+        landingDomain || undefined
+      );
+      
+      if (!guestResponse) {
+        showError("创建账户失败,请重试");
+        return;
+      }
+    } catch (error) {
+      console.error("创建账户失败:", error);
+      showError("创建账户失败,请重试");
+      return;
+    }
+  }
+
   // 确保价格配置已加载(团队相关价格可能不同)
-  if (isLoginUser.value && !priceStore.isPriceConfigLoaded) {
+  if (!priceStore.isPriceConfigLoaded) {
     try {
       await priceStore.fetchPriceConfig();
     } catch (error) {
@@ -793,7 +989,7 @@ const handleSinglePurchaseClick = async () => {
     }
   }
 
-  // 已登录,显示购买弹窗
+  // 显示购买弹窗
   showSinglePurchaseModal.value = true;
 };
 
@@ -809,7 +1005,16 @@ const handleMembershipPurchase = async () => {
   // 未登录用户先创建游客账户
   if (!isLoginUser.value) {
     try {
-      const guestResponse = await userStore.createGuest();
+      // 从 localStorage 读取保存的 code 和 ref 参数
+      const codeToUse = localStorage.getItem("memberCode");
+      const refToUse = localStorage.getItem("ref");
+      // 获取当前落地域名
+      const landingDomain = window.location.hostname;
+      const guestResponse = await userStore.createGuest(
+        codeToUse || undefined,
+        refToUse || undefined,
+        landingDomain || undefined
+      );
       if (!guestResponse) {
         showError("付费时创建账户失败,请重试");
         return;
@@ -867,7 +1072,16 @@ const handleSinglePurchase = async () => {
     // 未登录用户先创建游客账户
     if (!isLoginUser.value) {
       try {
-        const guestResponse = await userStore.createGuest();
+        // 从 localStorage 读取保存的 code 和 ref 参数
+        const codeToUse = localStorage.getItem("memberCode");
+        const refToUse = localStorage.getItem("ref");
+        // 获取当前落地域名
+        const landingDomain = window.location.hostname;
+        const guestResponse = await userStore.createGuest(
+          codeToUse || undefined,
+          refToUse || undefined,
+          landingDomain || undefined
+        );
         if (!guestResponse) {
           showError("付费时创建账户失败,请重试");
           return;
@@ -940,10 +1154,35 @@ const handleMembershipQueryOrder = async () => {
     if (response.status === 1) {
       // 支付成功,停止定时查询
       stopMembershipQueryTimer();
+      
+      // 保存订单号用于检查
+      const orderNo = currentOrderNo.value;
+      
+      // 检查是否已经处理过这个订单
+      if (!orderNo || processedOrderNos.value.has(orderNo)) {
+        return;
+      }
+      
+      // 标记订单已处理
+      processedOrderNos.value.add(orderNo);
+      
       await userStore.sync();
       showPaymentWaitingDialog.value = false;
-      showSuccess("会员购买成功!");
       currentOrderNo.value = "";
+      
+      // 检查用户是否是游客用户,如果是则显示账户信息提示弹窗
+      if (isGuestUser.value) {
+        // 检查是否已经提示过
+        const shownKey = `accountInfoShown_${userStore.userInfo?.id || 'guest'}`;
+        const hasShown = window.localStorage.getItem(shownKey) === 'true';
+        if (!hasShown) {
+          showAccountInfoDialog.value = true;
+        } else {
+          showSuccess("会员购买成功!");
+        }
+      } else {
+        showSuccess("会员购买成功!");
+      }
     } else {
       showError(response.msg || "会员购买失败");
     }
@@ -969,10 +1208,36 @@ const handleSingleQueryOrder = async () => {
     if (response.status === 1) {
       // 支付成功,停止定时查询
       stopSingleQueryTimer();
+      
+      // 保存订单号用于检查
+      const orderNo = singleCurrentOrderNo.value;
+      
+      // 检查是否已经处理过这个订单
+      if (!orderNo || processedOrderNos.value.has(orderNo)) {
+        return;
+      }
+      
+      // 标记订单已处理
+      processedOrderNos.value.add(orderNo);
+      
       await userStore.sync();
       showSinglePaymentWaitingDialog.value = false;
-      showSuccess("视频购买成功!");
       singleCurrentOrderNo.value = "";
+      
+      // 检查用户是否是游客用户,如果是则显示账户信息提示弹窗
+      if (isGuestUser.value) {
+        // 检查是否已经提示过
+        const shownKey = `accountInfoShown_${userStore.userInfo?.id || 'guest'}`;
+        const hasShown = window.localStorage.getItem(shownKey) === 'true';
+        if (!hasShown) {
+          showAccountInfoDialog.value = true;
+        } else {
+          showSuccess("视频购买成功!");
+        }
+      } else {
+        showSuccess("视频购买成功!");
+      }
+      
       // 重新检查购买状态(已完成支付时的检查)
       await checkVideoPurchaseStatus();
     } else {
@@ -1005,15 +1270,41 @@ const startMembershipQueryTimer = () => {
     try {
       const response = await userQueryOrder(currentOrderNo.value);
 
-      if (response.status === 1) {
-        // 支付成功
-        stopMembershipQueryTimer();
-        await userStore.sync();
-        showPaymentWaitingDialog.value = false;
+    if (response.status === 1) {
+      // 支付成功
+      stopMembershipQueryTimer();
+      
+      // 保存订单号用于检查
+      const orderNo = currentOrderNo.value;
+      
+      // 检查是否已经处理过这个订单
+      if (!orderNo || processedOrderNos.value.has(orderNo)) {
+        return;
+      }
+      
+      // 标记订单已处理
+      processedOrderNos.value.add(orderNo);
+      
+      await userStore.sync();
+      showPaymentWaitingDialog.value = false;
+      currentOrderNo.value = "";
+      
+      // 检查用户是否是游客用户,如果是则显示账户信息提示弹窗
+      if (isGuestUser.value) {
+        // 检查是否已经提示过
+        const shownKey = `accountInfoShown_${userStore.userInfo?.id || 'guest'}`;
+        const hasShown = window.localStorage.getItem(shownKey) === 'true';
+        if (!hasShown) {
+          showAccountInfoDialog.value = true;
+        } else {
+          showSuccess("会员购买成功!");
+        }
+      } else {
         showSuccess("会员购买成功!");
-        currentOrderNo.value = "";
-        await checkVideoPurchaseStatus();
       }
+      
+      await checkVideoPurchaseStatus();
+    }
     } catch (error) {
       console.error("定时查询会员订单状态失败:", error);
     }
@@ -1054,15 +1345,41 @@ const startSingleQueryTimer = () => {
         videoInfo.value.id
       );
 
-      if (response.status === 1) {
-        // 支付成功
-        stopSingleQueryTimer();
-        await userStore.sync();
-        showSinglePaymentWaitingDialog.value = false;
+    if (response.status === 1) {
+      // 支付成功
+      stopSingleQueryTimer();
+      
+      // 保存订单号用于检查
+      const orderNo = singleCurrentOrderNo.value;
+      
+      // 检查是否已经处理过这个订单
+      if (!orderNo || processedOrderNos.value.has(orderNo)) {
+        return;
+      }
+      
+      // 标记订单已处理
+      processedOrderNos.value.add(orderNo);
+      
+      await userStore.sync();
+      showSinglePaymentWaitingDialog.value = false;
+      singleCurrentOrderNo.value = "";
+      
+      // 检查用户是否是游客用户,如果是则显示账户信息提示弹窗
+      if (isGuestUser.value) {
+        // 检查是否已经提示过
+        const shownKey = `accountInfoShown_${userStore.userInfo?.id || 'guest'}`;
+        const hasShown = window.localStorage.getItem(shownKey) === 'true';
+        if (!hasShown) {
+          showAccountInfoDialog.value = true;
+        } else {
+          showSuccess("视频购买成功!");
+        }
+      } else {
         showSuccess("视频购买成功!");
-        singleCurrentOrderNo.value = "";
-        await checkVideoPurchaseStatus();
       }
+      
+      await checkVideoPurchaseStatus();
+    }
     } catch (error) {
       console.error("定时查询单片购买订单状态失败:", error);
     }
@@ -1102,6 +1419,8 @@ const cancelSinglePayment = () => {
 // 支付等待相关
 const showPaymentWaitingDialog = ref(false);
 const currentOrderNo = ref("");
+// 记录已处理过的订单号,防止重复显示成功弹窗
+const processedOrderNos = ref<Set<string>>(new Set());
 
 // 跳转支付页面
 const openPaymentPage = (url: string) => {
@@ -1112,10 +1431,13 @@ const openPaymentPage = (url: string) => {
 const showSuccessDialog = ref(false);
 // 错误提示弹窗
 const showErrorDialog = ref(false);
+// 账户信息提示弹窗
+const showAccountInfoDialog = ref(false);
 // 成功提示消息
 const successMessage = ref("");
 // 错误提示消息
 const errorMessage = ref("");
+const domains = ref<string[]>([]);
 
 // 夸克浏览器检测函数
 const isQuarkBrowser = () => {
@@ -1185,6 +1507,77 @@ const showError = (message: string) => {
   showErrorDialog.value = true;
 };
 
+// 从 URL 中提取域名(处理 https://example.com 格式)
+const extractDomain = (urlOrDomain: string): string => {
+  try {
+    // 如果包含协议,使用 URL 对象解析
+    if (urlOrDomain.includes('://')) {
+      const url = new URL(urlOrDomain);
+      return url.hostname;
+    }
+    // 否则直接返回
+    return urlOrDomain;
+  } catch {
+    // 如果解析失败,返回原值
+    return urlOrDomain;
+  }
+};
+
+// 获取域名列表
+const fetchDomains = async () => {
+  try {
+    // 获取系统配置中的 super_domain
+    const config = await getSuperDomain();
+    const superDomainValue = config?.value;
+    
+    // 从 value 字段提取域名
+    let superDomain: string | undefined;
+    if (superDomainValue && typeof superDomainValue === 'string') {
+      superDomain = extractDomain(superDomainValue);
+    }
+    
+    // 获取当前访问域名
+    const currentDomain = window.location.hostname;
+    
+    // 构建域名列表:第一个是 super_domain,第二个是当前域名
+    const domainList: string[] = [];
+    
+    if (superDomain) {
+      domainList.push(superDomain);
+    }
+    
+    // 如果当前域名与 super_domain 不同,则添加当前域名
+    if (currentDomain && currentDomain !== superDomain) {
+      domainList.push(currentDomain);
+    }
+    
+    domains.value = domainList.length > 0 ? domainList : [currentDomain || ''];
+  } catch (error) {
+    console.error("获取域名配置失败:", error);
+    // 如果获取失败,至少显示当前域名
+    const currentDomain = window.location.hostname;
+    if (currentDomain) {
+      domains.value = [currentDomain];
+    } else {
+      domains.value = [];
+    }
+  }
+};
+
+// 关闭账户信息提示弹窗并记录已提示
+const closeAccountInfoDialog = () => {
+  const shownKey = `accountInfoShown_${userStore.userInfo?.id || 'guest'}`;
+  window.localStorage.setItem(shownKey, 'true');
+  showAccountInfoDialog.value = false;
+};
+
+// 当账户信息对话框打开时获取域名
+watch(() => showAccountInfoDialog.value, (newVal) => {
+  if (newVal && domains.value.length === 0) {
+    fetchDomains();
+  }
+});
+
 // 生成设备标识
 const generateMacAddress = (): string => {
   const hex = "0123456789ABCDEF";
@@ -1323,7 +1716,6 @@ const checkVideoPurchaseStatus = async () => {
 
   try {
     await loadVideoInfo();
-    console.log("showPurchasePrompt:", showPurchasePrompt.value);
   } catch (error) {
     console.error("检查视频购买状态失败:", error);
     isSinglePurchased.value = false;
@@ -1335,7 +1727,6 @@ const loadVideoInfo = async () => {
   const videoId = route.params.id || route.query.id;
   // 试看视频
   if (isFreeVideo.value) {
-    console.log("isFreeVideo");
     showPurchaseButtonArea.value = false;
     showPurchasePrompt.value = false;
 
@@ -1353,7 +1744,6 @@ const loadVideoInfo = async () => {
     };
   } else if (!isVipUser.value) {
     // 未登录
-    console.log("isNotLogin");
     if (!isLoginUser.value) {
       showPurchasePrompt.value = true;
       showPurchaseButtonArea.value = true;
@@ -1371,11 +1761,9 @@ const loadVideoInfo = async () => {
       };
     } else {
       // guest或free用户
-      console.log("isNotVipUser");
       const response = await checkSinglePurchase(videoId as string);
       const wasPurchased = response.status === 1 || response === true;
       isSinglePurchased.value = wasPurchased;
-      console.log("isSinglePurchased:", response);
       if (response) {
         // 已购买单片
         // 关闭弹窗
@@ -1421,7 +1809,6 @@ const loadVideoInfo = async () => {
   } else if (isVipUser.value) {
     // 付费用户,从详情接口获取视频信息
     const response = await getVideoDetail(device, String(videoId));
-    console.log("isVipUser");
     if (response.status === 0 && response.data) {
       const data = response.data;
       videoInfo.value = {
@@ -1556,6 +1943,13 @@ onMounted(async () => {
   // 标记用户信息已同步完成,允许路由监听逻辑运行
   isUserSynced.value = true;
 
+  // 记录视频页面访问
+  try {
+    await recordPageClick("video");
+  } catch (error) {
+    console.error("记录视频页面访问失败:", error);
+  }
+
   // 初次进入页面时,按顺序执行:购买判断 -> 加载视频 -> 加载相关推荐
   await checkVideoPurchaseStatus();
   await loadVideoInfo();

Some files were not shown because too many files changed in this diff