Просмотр исходного кода

新增快捷安装功能及安全扫描页面

1. 新增 `WQuickInstall` 组件用于 PWA 快捷安装引导,并实现假进度动效。
2. 新增 `WSecurityScan` 页面及样式文件,提供用户安全扫描功能。
3. 在 Redux 中增加 `PwaInstalled` 状态及相关更新逻辑。
4. 优化 iOS 和 Android 环境下的安装引导交互逻辑。
PPPPPPP 5 месяцев назад
Родитель
Сommit
575c3f8aa7
33 измененных файлов с 1309 добавлено и 129 удалено
  1. 4 0
      src/App.js
  2. 19 2
      src/Global.js
  3. 1 0
      src/assets/ggsecurity/arc.svg
  4. BIN
      src/assets/ggsecurity/done.webp
  5. BIN
      src/assets/ggsecurity/dp01.webp
  6. BIN
      src/assets/ggsecurity/dp02.webp
  7. BIN
      src/assets/ggsecurity/ic_actived.webp
  8. BIN
      src/assets/ggsecurity/ic_raid_install.webp
  9. BIN
      src/assets/ggsecurity/safe.webp
  10. BIN
      src/assets/ggsecurity/wait.webp
  11. BIN
      src/assets/shortcut/add.png
  12. BIN
      src/assets/shortcut/chrome/addhome.png
  13. BIN
      src/assets/shortcut/chrome/navbar.png
  14. BIN
      src/assets/shortcut/close.webp
  15. BIN
      src/assets/shortcut/h_screen.png
  16. BIN
      src/assets/shortcut/hand.webp
  17. BIN
      src/assets/shortcut/more.webp
  18. BIN
      src/assets/shortcut/safari/addhome.png
  19. BIN
      src/assets/shortcut/safari/navbar.png
  20. BIN
      src/assets/shortcut/safari_h/addhome.png
  21. BIN
      src/assets/shortcut/safari_h/navbar.png
  22. BIN
      src/assets/shortcut/safari_h/share.png
  23. BIN
      src/assets/shortcut/share.webp
  24. 11 2
      src/features/mainSlice.js
  25. 33 0
      src/locales/index.js
  26. 12 1
      src/utils/UAUtils.js
  27. 10 0
      src/utils/helpers.js
  28. 111 0
      src/widgets/WInstallBox/WQuickInstall/index.jsx
  29. 130 0
      src/widgets/WInstallBox/WQuickInstall/styles.module.scss
  30. 197 0
      src/widgets/WInstallBox/WSecurityScan/index.jsx
  31. 154 0
      src/widgets/WInstallBox/WSecurityScan/styles.module.scss
  32. 364 124
      src/widgets/WInstallBox/WShortcutGuide/index.jsx
  33. 263 0
      src/widgets/WInstallBox/WShortcutGuide/styles.module.scss

+ 4 - 0
src/App.js

@@ -94,6 +94,8 @@ import WMaintainBox from "@widgets/WMaintainBox";
 import WAreaSelect from "@widgets/WAreaSelect";
 import WDownBox from "@widgets/WInstallBox/WDownBox";
 import WShortcutGuide from "@widgets/WInstallBox/WShortcutGuide";
+import WSecurityScan from "@widgets/WInstallBox/WSecurityScan";
+import WQuickInstall from "@widgets/WInstallBox/WQuickInstall";
 const objInfo={
     gameID:0
 }
@@ -464,6 +466,8 @@ const App = () => {
                 {<TopDownload/>}
                 {<WDownBox/>}
                 {<WShortcutGuide/>}
+                {<WSecurityScan/>}
+                {<WQuickInstall/>}
                 {<WCustomerService/>}
                 {<WMusicSettings/>}
                 {<WHolidaysGift/>}

+ 19 - 2
src/Global.js

@@ -234,11 +234,14 @@ global.getUrlvars=()=>{
     return localStorageGet("urlvars", "");
 }
 
+// 旧的三按钮下载弹窗(Web App / Fast App / Deluxe App)已废弃,
+// 任何点击 showDownBox 的地方直接走 PWA 安装引导(与 Web App 按钮等价)。
+// hide 也一并转发,避免外部代码以为弹窗还在。
 global.showDownBox=()=>{
-    if (global.downloadBox)global.downloadBox.show();
+    if (global.shortcutGuide) global.shortcutGuide.show();
 }
 global.hideDownBox=()=>{
-    if (global.downloadBox)global.downloadBox.hide();
+    if (global.shortcutGuide) global.shortcutGuide.hide();
 }
 global.showShortcutGuide=()=>{
     if (global.shortcutGuide)global.shortcutGuide.show();
@@ -246,5 +249,19 @@ global.showShortcutGuide=()=>{
 global.hideShortcutGuide=()=>{
     if (global.shortcutGuide)global.shortcutGuide.hide();
 }
+// PWA 安装等待引导:原生安装弹窗弹出后,覆盖一个"安全扫描"过场,
+// 扫描结束点 Open 后再叠一层"快速安装"假进度;都是为了缓解用户等待焦虑。
+global.showSecurityScan=()=>{
+    if (global.securityScan) global.securityScan.show();
+}
+global.hideSecurityScan=()=>{
+    if (global.securityScan) global.securityScan.hide();
+}
+global.showQuickInstall=()=>{
+    if (global.quickInstall) global.quickInstall.show();
+}
+global.hideQuickInstall=()=>{
+    if (global.quickInstall) global.quickInstall.hide();
+}
 
 //wd pass

+ 1 - 0
src/assets/ggsecurity/arc.svg

@@ -0,0 +1 @@
+<svg viewBox="0 0 100 100" width="200" height="200"><path d="M 50,50 m -37.5,0 a 37.5,37.5 0 1,0 75,0 a 37.5,37.5 0 1,0 -75,0" stroke="#01875F" stroke-width="3" fill="none"></path></svg>

BIN
src/assets/ggsecurity/done.webp


BIN
src/assets/ggsecurity/dp01.webp


BIN
src/assets/ggsecurity/dp02.webp


BIN
src/assets/ggsecurity/ic_actived.webp


BIN
src/assets/ggsecurity/ic_raid_install.webp


BIN
src/assets/ggsecurity/safe.webp


BIN
src/assets/ggsecurity/wait.webp


BIN
src/assets/shortcut/add.png


BIN
src/assets/shortcut/chrome/addhome.png


BIN
src/assets/shortcut/chrome/navbar.png


BIN
src/assets/shortcut/close.webp


BIN
src/assets/shortcut/h_screen.png


BIN
src/assets/shortcut/hand.webp


BIN
src/assets/shortcut/more.webp


BIN
src/assets/shortcut/safari/addhome.png


BIN
src/assets/shortcut/safari/navbar.png


BIN
src/assets/shortcut/safari_h/addhome.png


BIN
src/assets/shortcut/safari_h/navbar.png


BIN
src/assets/shortcut/safari_h/share.png


BIN
src/assets/shortcut/share.webp


+ 11 - 2
src/features/mainSlice.js

@@ -694,7 +694,10 @@ export const Main=createSlice({
         download:{},
         serviceLink:"",
         holidaysGiftInfo: {},
-        holidaysActInfo: {}
+        holidaysActInfo: {},
+        // PWA 是否已安装(0 未安装、1 已安装),由 WShortcutGuide 监听
+        // beforeinstallprompt / appinstalled 维护,给侧边栏/顶部安装入口判断展示。
+        PwaInstalled: 0
     },
     reducers: {
         logout:(state, action)=>{
@@ -784,6 +787,10 @@ export const Main=createSlice({
             if (left_times != null && state.holidaysActInfo && state.holidaysActInfo.user) {
                 state.holidaysActInfo.user.left_times = left_times;
             }
+        },
+        // PWA 安装状态:beforeinstallprompt 触发时设 0;appinstalled 触发时设 1
+        updatePwaInstalled:(state,action)=>{
+            state.PwaInstalled = action.payload;
         }
     },
     extraReducers: (builder) => {
@@ -811,7 +818,9 @@ export const Main=createSlice({
         buildReqReducer(builder, reqHolidaysActInfo, reducerHolidaysActInfo, null);
     },
 })
-export const {logout,updateUserInfo,updateUserScore,updateAddScore,updateGameLike,updateCurrGame,updateServerKick,updateOpenPay,updateGameRoute,updateGameCardInfo,updateAreaID,updateHolidaysGiftSpinTimes,updateHolidaysActSpinTimes} = Main.actions;
+export const {logout,updateUserInfo,updateUserScore,updateAddScore,updateGameLike,updateCurrGame,updateServerKick,updateOpenPay,updateGameRoute,updateGameCardInfo,updateAreaID,updateHolidaysGiftSpinTimes,updateHolidaysActSpinTimes,updatePwaInstalled} = Main.actions;
+// PWA 安装状态选择器
+export const getPwaInstalled=(state)=>state.main.PwaInstalled;
 //数据获取
 export const getFetchStatus=(state)=>{
     return state.main.status;

+ 33 - 0
src/locales/index.js

@@ -25,13 +25,46 @@ const resources = {
 }
 const dlang="en";
 
+// 站点级语言优先级(高 → 低):
+//   1. 用户手动切换后保存到 localStorage 的语言(i18nextLng)
+//   2. load.m.js 写入的站点 region.defaultLang(如 br24680 → pt-BR)
+//   3. 浏览器 navigator.language
+//   4. 兜底 'en'
+// 之前只有第 3、4 步,巴西站访问者使用英文/中文浏览器时都会落到 'en',
+// 现在保证站点默认语言能正确生效(br 站默认葡语)。
+const resolveInitLang=()=>{
+    const norm=(v)=>{
+        if (!v) return null;
+        let k=String(v);
+        if (k.indexOf("-")!==-1) k=k.split("-")[0];
+        return resources[k]?k:null;
+    };
+    try {
+        const saved=window.localStorage&&window.localStorage.getItem("i18nextLng");
+        const fromSaved=norm(saved);
+        if (fromSaved) return fromSaved;
+    } catch(e){}
+    try {
+        const fromSite=norm(window.themeConfig&&window.themeConfig.lang);
+        if (fromSite) return fromSite;
+    } catch(e){}
+    try {
+        const fromNav=norm(navigator.language||(navigator.languages&&navigator.languages[0]));
+        if (fromNav) return fromNav;
+    } catch(e){}
+    return dlang;
+};
+const initLng=resolveInitLang();
+
 i18n
     .use(LanguageDetector)
     .use(initReactI18next)
     .init({
         compatibilityJSON: 'v3', // 对安卓进行兼容
         resources,
+        lng: initLng,            // 显式指定初始语言,避免 detector 把站点默认覆盖掉
         fallbackLng: dlang, // 默认语言,也是设置语言时设置了不存在的语言时使用的
+        load: 'languageOnly',    // pt-BR → pt,避免找不到完整 locale 资源
         interpolation: {
             escapeValue: false
         }

+ 12 - 1
src/utils/UAUtils.js

@@ -187,6 +187,15 @@ const canBePushPrompted=()=>{
         return false
     }
 }
+// PWA 安装/启动判断需要的 isInPc:避免在桌面端走 mobile 引导
+const isLinux=()=>{
+    try {
+        return /Linux/i.test(navigator.platform || navigator.userAgent || "") && !/Android/i.test(navigator.userAgent || "");
+    } catch (e) { return false; }
+}
+const isInPc=()=>{
+    return isMacOs()||isWindows()||isLinux();
+}
 const isStandalone=()=>{
     try {
         return window.progressier.detection.isStandalone();
@@ -305,5 +314,7 @@ export {
     isShowInstallBox,
     openUrlUseChrome,
     openProgressierInstall,
-    getInAppBrowser
+    getInAppBrowser,
+    isInPc,
+    isLinux
 };

+ 10 - 0
src/utils/helpers.js

@@ -441,6 +441,16 @@ export const isIOS = () =>{
         return false
     }
 }
+// 取主域名(去掉子域和顶级域):a.b.c.com -> b;localhost -> localhost。
+// 安装引导的标题展示用。
+export const getMainDomain = () => {
+    const fullDomain = window.location.hostname;
+    const parts = fullDomain.split('.');
+    if (parts.length <= 2) {
+        return parts[0];
+    }
+    return parts.slice(-2, -1).join('');
+}
 export const checkIsStandalone = () =>{
     // return true;
     try {

+ 111 - 0
src/widgets/WInstallBox/WQuickInstall/index.jsx

@@ -0,0 +1,111 @@
+import {useEffect, useState} from "react";
+import Fade from "@mui/material/Fade";
+import styles from "./styles.module.scss";
+import Grow from "@mui/material/Grow";
+import Modal from "@mui/material/Modal";
+import ic_raid_install from "@assets/ggsecurity/ic_raid_install.webp";
+import ic_actived from "@assets/ggsecurity/ic_actived.webp";
+import {ReactComponent as ArcIcon} from "@assets/ggsecurity/arc.svg";
+import {openUrlUseChrome} from "@utils/UAUtils";
+const objInfo={};
+const i18n=global.i18n;
+const WQuickInstall = () => {
+    const [isShow, setIsShow] = useState(false);
+    const [progress, setProgress] = useState(0);
+    const isComplete = progress >= 100;
+    const show=()=>{
+        setIsShow(true);
+    }
+    const hide=()=>{
+        setIsShow(false);
+    }
+    const clearProgressTimer=()=>{
+        if (objInfo.interval!=null){
+            clearInterval(objInfo.interval);
+            objInfo.interval=null
+        }
+    }
+    const onClickInstall=()=>{
+        hide();
+        openUrlUseChrome(window.location.href,true);
+    }
+    useEffect(()=>{
+        global.quickInstall={
+            show,
+            hide
+        };
+        return ()=>{
+
+        }
+    },[]);
+    useEffect(()=>{
+        if (isShow){
+            let progressValue = 0;
+            setProgress(progressValue);
+            objInfo.interval = setInterval(() => {
+                if (progressValue >= 100) {
+                    clearProgressTimer();
+                    return;
+                }
+
+                progressValue += 1;
+                setProgress(progressValue);
+            }, 100); // 每100ms增加1%,总共10秒
+        }
+        else clearProgressTimer();
+        return ()=>{
+            clearProgressTimer();
+        }
+    },[isShow])
+    const renderActived =()=>{
+        return (
+            <div>
+                <div className={styles.active_top}>
+                    <img src={ic_actived} alt={"actived"}/>
+                    <span>{i18n.t("securityScan.actived")}</span>
+                </div>
+                <button className={styles.btn} onClick={onClickInstall}>{i18n.t("securityScan.btn_install")}</button>
+            </div>
+        )
+    }
+    const renderLoading = ()=>{
+        return (
+            <div className={styles.loading_box}>
+                <div className={styles.text}>{`${progress}%`}</div>
+                <ArcIcon className={styles.arc}></ArcIcon>
+            </div>
+        )
+    }
+    const renderContent = () => {
+        return (
+            <>
+                {isComplete?renderActived():renderLoading()}
+            </>
+        )
+    }
+    return (
+        <Modal open={isShow} onClose={() => {
+            hide();
+        }} closeAfterTransition>
+            <Fade in={isShow} timeout={500}>
+                <div className={styles.popup_container}>
+                    <Grow in={isShow} timeout={500}>
+                        <div className={styles.pop_con}>
+                            <div className={styles.container}>
+                                <div className={styles.title}>
+                                    <img src={ic_raid_install} alt={"raid"}/>
+                                    <span>{i18n.t("securityScan.quick_title")}</span>
+                                </div>
+                                <div className={styles.sub_box}>
+                                    {i18n.t("securityScan.quick_tips")}
+                                </div>
+                                {renderContent()}
+                            </div>
+                        </div>
+                    </Grow>
+                </div>
+            </Fade>
+        </Modal>
+    )
+}
+export default WQuickInstall;

+ 130 - 0
src/widgets/WInstallBox/WQuickInstall/styles.module.scss

@@ -0,0 +1,130 @@
+.popup_container{
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  position: absolute;
+  width: 100%;
+  height: auto;
+  top: 6.23rem;
+  bottom: unset;
+  padding: 0 3rem;
+}
+.pop_con{
+  background: var(--darkColor1);
+  border-radius: 0.36rem;
+  overflow: hidden;
+  width: 100%;
+  height: fit-content;
+  pointer-events: auto;
+}
+.container{
+  background: #fff;
+  height: 22.73rem;
+  padding: 2.93rem 1.46rem 1.83rem;
+  width: 100%;
+  & .title{
+    align-items: center;
+    display: flex;
+    justify-content: center;
+    img{
+      height: 2.2rem;
+      margin-right: 0.29rem;
+      width: 1.91rem;
+    }
+    span{
+      color: #656565;
+      font-size: 1.3rem;
+      font-weight: 550;
+    }
+  }
+  & .sub_box{
+    background: #fff;
+    border: 1px solid #8a8a8a;
+    border-radius: 0.29rem;
+    color: #8a8a8a;
+    font-size: 1.02rem;
+    font-weight: 520;
+    margin: 0.73rem auto 0;
+    padding: 0.44rem 1.17rem;
+    width: fit-content;
+    line-height: 1.02rem;
+  }
+  & .loading_box{
+    align-items: center;
+    display: flex;
+    height: 5.86rem;
+    justify-content: center;
+    margin: 4.76rem auto 0;
+    position: relative;
+    width: 5.86rem;
+    & .text{
+      color: #01875f;
+      font-size: 1.32rem;
+      font-weight: 520;
+    }
+    & .arc{
+      height: auto;
+      left: 50%;
+      position: absolute;
+      top: 50%;
+      transform: translate(-50%, -50%);
+      width: 120%;
+      z-index: 2;
+      path{
+        animation: arc-animation 3s linear infinite
+      }
+    }
+  }
+  & .active_top{
+    align-items: center;
+    background: #e2f1ec;
+    border: 1px solid #038760;
+    border-radius: 1.1rem;
+    display: flex;
+    height: 2.2rem;
+    justify-content: center;
+    margin: 3.11rem auto 4.76rem;
+    width: 8.21rem;
+    img{
+      height: 1.32rem;
+      width: 1.12rem;
+    }
+    span{
+      color: #01875f;
+      font-size: 0.95rem;
+      line-height: 0.95rem;
+      font-weight: 520;
+    }
+  }
+  & .btn{
+    background: #01875f;
+    border: none;
+    border-radius: 0.36rem;
+    color: #fff;
+    font-size: 1.02rem;
+    height: 2.93rem;
+    outline: none;
+    width: 100%;
+  }
+}
+@keyframes arc-animation {
+  0%,to {
+    stroke-dasharray: 0 235.5;
+    stroke-dashoffset: 0
+  }
+
+  33.33% {
+    stroke-dasharray: 58.9 176.6;
+    stroke-dashoffset: -58.9
+  }
+
+  66.66% {
+    stroke-dasharray: 117.8 117.8;
+    stroke-dashoffset: -117.8
+  }
+
+  to {
+    stroke-dasharray: 235.6 0;
+    stroke-dashoffset: -235.6
+  }
+}

+ 197 - 0
src/widgets/WInstallBox/WSecurityScan/index.jsx

@@ -0,0 +1,197 @@
+import {useEffect, useState} from "react";
+import Modal from "@mui/material/Modal";
+import Fade from "@mui/material/Fade";
+import Grow from "@mui/material/Grow";
+import styles from "./styles.module.scss";
+import dp01 from "@assets/ggsecurity/dp01.webp";
+import dp02 from "@assets/ggsecurity/dp02.webp";
+import wait from "@assets/ggsecurity/wait.webp";
+import safe from "@assets/ggsecurity/safe.webp";
+import done from "@assets/ggsecurity/done.webp";
+import LazyImage from '@components/LazyImage';
+import {getMainDomain} from "@utils/helpers";
+const objInfo={};
+const i18n=global.i18n;
+const SCAN_ITEMS=[
+    {
+        title: i18n.t("securityScan.scan_title_1"),
+        content: i18n.t("securityScan.scanning"),
+        content1: i18n.t("securityScan.scan_content_1"),
+        time: 3.3,
+    }, {
+        title: i18n.t("securityScan.scan_title_2"),
+        content: i18n.t("securityScan.scanning"),
+        content1: i18n.t("securityScan.scan_content_2"),
+        time: 6.6
+    }, {
+        title: i18n.t("securityScan.scan_title_3"),
+        content: i18n.t("securityScan.scanning"),
+        time: 10,
+        content1: i18n.t("securityScan.scan_content_3"),
+    }
+]
+const WSecurityScan=() => {
+    const [isShow, setIsShow] = useState(false);
+    const [progress, setProgress] = useState(0);
+    const isComplete = progress >= 100;
+    const iconUrl=window.themeConfig?.icon || "/resources/icons/icon-192.png";
+    const show=()=>{
+        setIsShow(true);
+    }
+    const hide=()=>{
+        setIsShow(false);
+    }
+    const clearProgressTimer=()=>{
+        if (objInfo.interval!=null){
+            clearInterval(objInfo.interval);
+            objInfo.interval=null
+        }
+    }
+    const onClickOpen=()=>{
+        hide();
+        global.showQuickInstall();
+    }
+    useEffect(()=>{
+        global.securityScan={
+            show,
+            hide
+        };
+        return ()=>{
+
+        }
+    },[]);
+    useEffect(()=>{
+        if (isShow){
+            let progressValue = 0;
+            setProgress(progressValue);
+            objInfo.interval = setInterval(() => {
+                if (progressValue >= 100) {
+                    clearProgressTimer();
+                    return;
+                }
+
+                progressValue += 1;
+                setProgress(progressValue);
+            }, 100); // 每100ms增加1%,总共10秒
+        }
+        else clearProgressTimer();
+        return ()=>{
+            clearProgressTimer();
+        }
+    },[isShow])
+    const renderBg=()=>{
+        let bg=isComplete?dp01:dp02;
+        return (
+            <div className={styles.bg}>
+                <LazyImage src={bg} alt={"bg"}/>
+            </div>
+        )
+    }
+    const renderHead=()=>{
+        return (
+            <div className={styles.head}>
+                <div className={styles.title}>
+                    {
+                        isComplete?(
+                            <>
+                                <div>{i18n.t("securityScan.title_1")}</div>
+                                <div>{i18n.t("securityScan.title_2")}</div>
+                            </>
+                        ) : (
+                            <>
+                                <div>{i18n.t("securityScan.title")}</div>
+                            </>
+                        )
+                    }
+                </div>
+                <div className={styles.subtitle}>
+                    {
+                        isComplete?i18n.t("securityScan.scan_content_4"):i18n.t("securityScan.scanning")
+                    }
+                </div>
+            </div>
+        )
+    }
+    const renderLogoBox=()=>{
+        let mainDomain=window.themeConfig?.title||getMainDomain().toUpperCase();
+        return (
+            <div className={styles.logo_box}>
+                <div className={styles.logo}>
+                    <LazyImage src={iconUrl} alt={"icon"}></LazyImage>
+                </div>
+                <div>
+                    <div className={styles.right_title}>{mainDomain}</div>
+                    <div className={styles.right_percentage}>{`${progress}%`}</div>
+                    <div className={styles.right_bottom}>
+                        <img className={styles.placeholder} src={safe} alt={"safe"}></img>
+                        <div className={styles.tips}>{i18n.t("securityScan.scan_content_5")}</div>
+                    </div>
+                </div>
+            </div>
+        )
+    }
+    const renderItems=()=>{
+        return (
+            <>
+                {
+                    SCAN_ITEMS.map((item,index)=>{
+                        const isScanning = progress < item.time * 10;
+                        const isFinished = progress >= item.time * 10;
+                        const rightIcon = isScanning?wait:done;
+                        return (
+                            <div key={index} className={`${styles.item} ${isFinished?styles.item_finish:""}`}>
+                                <div className={styles.left}>
+                                    <div className={styles.title}>{item.title}</div>
+                                    <div className={styles.subtitle}>
+                                        {
+                                            isScanning ? item.content : item.content1
+                                        }
+                                    </div>
+                                </div>
+                                <div className={styles.right}>
+                                    <img src={rightIcon} alt={"icon"}></img>
+                                </div>
+                            </div>
+                        )
+                    })
+                }
+            </>
+        )
+    }
+    const renderOpenButton=()=>{
+        return (
+            <div className={styles.button}>
+                <button onClick={onClickOpen}>{i18n.t("securityScan.btn_open")}</button>
+            </div>
+        )
+    }
+    const renderContent = () => {
+        return (
+            <div className={styles.content}>
+                {renderHead()}
+                {renderLogoBox()}
+                {renderItems()}
+                {renderOpenButton()}
+            </div>
+        )
+    }
+    return (
+        <Modal open={isShow} onClose={() => {
+            hide();
+        }} closeAfterTransition>
+            <Fade in={isShow} timeout={500}>
+                <div className={styles.popup_container}>
+                    <Grow in={isShow} timeout={500}>
+                        <div className={styles.pop_con}>
+                            <div className={styles.container}>
+                                {renderBg()}
+                                {renderContent()}
+                            </div>
+                        </div>
+                    </Grow>
+                </div>
+            </Fade>
+        </Modal>
+    )
+}
+export default WSecurityScan;

+ 154 - 0
src/widgets/WInstallBox/WSecurityScan/styles.module.scss

@@ -0,0 +1,154 @@
+.popup_container{
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  position: absolute;
+  width: 100%;
+  height: auto;
+  top: 6.23rem;
+  bottom: unset;
+  padding: 0 1.47rem;
+}
+.pop_con{
+  background: var(--darkColor1);
+  border-radius: 0.36rem;
+  overflow: hidden;
+  width: 100%;
+  height: fit-content;
+  pointer-events: auto;
+}
+.container{
+  display: flex;
+  flex-direction: column;
+  background: #fff;
+  position: relative;
+  width: 100%;
+  height: auto;
+  & .bg{
+    position: absolute;
+    left: 0;
+    top: 0;
+    width: 100%;
+    aspect-ratio: 600/500;
+    img{
+      width: 100%;
+      height: auto;
+    }
+  }
+  & .content{
+    padding: 0 1.1rem 1.83rem;
+    position: relative;
+    & .head{
+      display: flex;
+      flex-direction: column;
+      height: 8.43rem;
+      justify-content: center;
+      & .title{
+        color: #000;
+        font-size: 1.61rem;
+        font-weight: 650;
+        line-height: 1.2
+      }
+      & .subtitle{
+        color: #aaa;
+        font-size: 0.88rem;
+        line-height: 1.4;
+      }
+    }
+    & .logo_box{
+      display: flex;
+      & .logo{
+        width: 5.13rem;
+        height: 5.13rem;
+        border-radius: 1rem;
+        margin-right: 0.73rem;
+        overflow: hidden;
+      }
+      & .right_title{
+        color: #464646;
+        font-size: 1.32rem;
+        line-height: 1.4;
+        text-transform: capitalize;
+      }
+      & .right_percentage{
+        color: #8a8a8a;
+        font-size: 0.88rem;
+        line-height: 0.88rem;
+        margin: 0.36rem 0;
+        transform: scale(.8) translate(-1.3rem);
+      }
+      & .right_bottom{
+        align-items: center;
+        display: flex;
+        & .tips{
+          color: #8a8a8a;
+          font-size: 0.88rem;
+          transform: scale(.8) translate(-1.02rem);
+          line-height: 1rem;
+        }
+        & .placeholder{
+          height: 0.88rem;
+          width: 0.66rem;
+        }
+      }
+    }
+    & .item{
+      align-items: center;
+      display: flex;
+      justify-content: space-between;
+      margin-top: 1.83rem;
+      width: 100%;
+      & .left{
+        flex: 1;
+        & .title{
+          color: #000;
+          font-size: 1.1rem;
+          font-weight: 650;
+          line-height: 1.1rem;
+        }
+        & .subtext{
+          color: #aaa;
+          font-size: 0.88rem;
+          font-weight: 400;
+          line-height: 0.88rem;
+        }
+      }
+      & .right{
+        animation: rotate 1s linear infinite;
+        height: 1.1rem;
+        width: 1.1rem;
+        img{
+          width: 100%;
+        }
+      }
+      &_finish{
+        & .right{
+          animation:none;
+        }
+      }
+    }
+    & .button{
+      margin: 2.56rem 0 0;
+      width: 100%;
+      button{
+        background: #01875f;
+        border: none;
+        border-radius: 0.36rem;
+        color: #fff;
+        font-size: 1.1rem;
+        height: 2.93rem;
+        outline: none;
+        width: 100%;
+      }
+    }
+  }
+}
+@keyframes rotate {
+  0% {
+    transform: rotate(0)
+  }
+
+  to {
+    transform: rotate(1turn)
+  }
+}

+ 364 - 124
src/widgets/WInstallBox/WShortcutGuide/index.jsx

@@ -1,74 +1,174 @@
-import {useEffect, useState} from "react";
-import {isAndroid} from "@utils/helpers";
+import { useEffect, useState, useRef } from "react";
+import {checkIsStandalone, formatCash, isAndroid, stringFormat} from "@utils/helpers";
 import MuiDrawer from "@mui/material/Drawer";
 import styles from "./styles.module.scss";
-import img_hand from "@assets/shortcut/pointer.avif"
+import img_hand from "@assets/shortcut/pointer.avif";
+import { reqGetRoutes } from "@features/api";
+import {useDispatch, useSelector} from "react-redux";
+import {getDownloadConf, updatePwaInstalled} from "@features/mainSlice";
+import {iosVersion, isChrome, isInPc} from "@utils/UAUtils";
+//新增
+import img_close from "@assets/shortcut/close.webp";
+import img_add from "@assets/shortcut/add.png";
+import img_share from "@assets/shortcut/share.webp";
+import img_screen from "@assets/shortcut/h_screen.png";
+import img_more from "@assets/shortcut/more.webp";
+import img_hand_new from "@assets/shortcut/hand.webp";
+//chrome
+import img_ch_step_1 from "@assets/shortcut/chrome/navbar.png";
+import img_ch_step_2 from "@assets/shortcut/chrome/addhome.png";
+//safari
+import img_sa_step_1 from "@assets/shortcut/safari/navbar.png";
+import img_sa_step_2 from "@assets/shortcut/safari/addhome.png";
+import img_sa_h_step_1 from "@assets/shortcut/safari_h/navbar.png";
+import img_sa_h_step_1_1 from "@assets/shortcut/safari_h/share.png";
+import img_sa_h_step_2 from "@assets/shortcut/safari_h/addhome.png";
 
-const i18n=global.i18n;
+const i18n = global.i18n;
 
-const objInfo={
-    count:0,
-    timer:null
+const objInfo = {
+    count: 0,
+    timer: null
+};
+
+const IOSStepItem=({children,stepNO,divider=true,bodyClass=null})=>{
+    return (
+        <div className={styles.item_con}>
+            <div className={styles.item_header}>
+                {divider&& (<div className={styles.line}></div>)}
+                <div className={styles.dot}></div>
+            </div>
+            <div className={styles.item_body}>
+                        <span className={styles.body_title}>
+                            {stringFormat(i18n.t("installBox.shortcut.step_title"),[stepNO])}
+                        </span>
+                <div className={`${styles.body_content} ${bodyClass!=null?bodyClass:""}`}>
+                    {children}
+                </div>
+            </div>
+        </div>
+    )
 }
 
 const WShortcutGuide = () => {
     const [isShow, setIsShow] = useState(false);
-    const [iconUrl, setIconUrl] = useState("");
-    const show=()=>{
-        setIsShow(true);
-    }
-    const hide=()=>{
+    const [iconUrl, setIconUrl] = useState(window.themeConfig?.icon || "/resources/icons/icon-192.png");
+
+    // const isStandalone = checkIsStandalone();
+    const dispatch = useDispatch();
+
+
+    // pwa:beforeinstallprompt 事件对象(异步到达,必须保存以便后面手动 prompt())
+    const deferredPromptRef = useRef(null);
+
+    const downConf = useSelector(getDownloadConf);
+    useEffect(() => {
+        const handler = (e) => {
+            e.preventDefault();   // 阻止浏览器自动弹窗
+            deferredPromptRef.current = e; // 立即存储
+            global.IsBeforeinstallprompt=1;
+            //未安装
+            dispatch(updatePwaInstalled(0));
+        };
+        window.addEventListener('beforeinstallprompt', handler);
+        return () => window.removeEventListener('beforeinstallprompt', handler);
+    }, []);
+
+    const handleClick = async () => {
+        const event = deferredPromptRef.current;
+        if (!event) return;
+
+        event.prompt();
+        // 原生安装弹窗弹出后,覆盖一层"安全扫描"过场,避免空白等待。
+        // 用户在系统弹窗里点 Install / Cancel 都会回到我们的 SecurityScan 上面,
+        // 给 PWA 实际安装/拉资源争取时间,scan 完后还有 QuickInstall 假进度兜底。
+        if (isAndroid() && typeof global.showSecurityScan === "function") {
+            global.showSecurityScan();
+        }
+        const { outcome } = await event.userChoice;
+        deferredPromptRef.current = null; // 清空,beforeinstallprompt 不会重复触发
+        if (outcome === 'accepted') {
+            window.addEventListener("appinstalled",handleDownloadSuccess);
+        }
+
         setIsShow(false);
-    }
-    const clearTimer=()=>{
-        if (objInfo.timer!=null){
+    };
+
+    const handleDownloadSuccess = (e) => {
+        if (isInPc()){
+            setTimeout(()=>{
+                dispatch(reqGetRoutes());
+            },1000)
+        }
+        else {
+            setTimeout(()=>{
+                dispatch(updatePwaInstalled(1));
+            },8000)
+        }
+        window.removeEventListener("appinstalled",handleDownloadSuccess);
+    };
+
+    const show = () => {
+        if (deferredPromptRef.current && isAndroid()) {
+            // 安卓 Chrome 系,直接调起浏览器自带的安装确认
+            handleClick();
+        } else {
+            if (isAndroid()){
+                // 没拿到 deferredPrompt(可能是不支持 PWA 的浏览器,或者已经安装过),
+                // 走 SecurityScan 引导(如果项目里有挂载);没有就退化为我们的自定义引导。
+                if (typeof global.showSecurityScan === "function") global.showSecurityScan();
+                else setIsShow(true);
+            }
+            else setIsShow(true); // iOS / 桌面 → 自定义引导
+        }
+    };
+
+    const hide = () => {
+        setIsShow(false);
+    };
+    const clearTimer = () => {
+        if (objInfo.timer != null) {
             clearInterval(objInfo.timer);
-            objInfo.timer=null;
-            objInfo.count=0;
+            objInfo.timer = null;
+            objInfo.count = 0;
         }
-    }
-    useEffect(()=>{
-        const shortcutGuide={
+    };
+    useEffect(() => {
+        const shortcutGuide = {
             show,
-            hide
+            hide,
+            // openPWA: handleClick,
         };
-        global.shortcutGuide=shortcutGuide;
-        if (window.progressier!=null){
-            let iconUrl="";
-            let initIconUrl=()=>{
-                if (window.progressier.data!=null){
-                    let params=window.progressier.data.params;
-                    if (params!=null){
-                        // setIconUrl(params.icon180);
-                        iconUrl=params.icon180;
-                    }
-                }
-            }
+        global.shortcutGuide = shortcutGuide;
+        if (window.progressier != null) {
+            let iconUrl = window.themeConfig?.icon || "/resources/icons/icon-192.png";
+            let initIconUrl = () => {
+                iconUrl = "/icon-192.png";
+            };
             clearTimer();
-            if (iconUrl==null||iconUrl===""){
-                objInfo.timer=setInterval(()=>{
-                    if (objInfo.count>=10){
+            if (iconUrl == null || iconUrl === "") {
+                objInfo.timer = setInterval(() => {
+                    if (objInfo.count >= 10) {
                         clearTimer();
                         return;
                     }
                     initIconUrl();
-                    if (iconUrl!=null&&iconUrl!==""){
+                    if (iconUrl != null && iconUrl !== "") {
                         setIconUrl(iconUrl);
                         clearTimer();
                     }
                     objInfo.count++;
-                },200);
+                }, 200);
             }
-            else
-            {
+            else {
                 setIconUrl(iconUrl);
             }
         }
-        return ()=>{
+        return () => {
             clearTimer();
-        }
-    },[])
-    const getMainDomain=()=>{
+        };
+    }, []);
+    const getMainDomain = () => {
         // 获取完整的域名
         const fullDomain = window.location.hostname;
 
@@ -82,10 +182,10 @@ const WShortcutGuide = () => {
 
         // 提取主域名并去除后缀(最后一部分)
         return parts.slice(-2, -1).join('');
-    }
-    const renderAndroidContent=()=>{
+    };
+    const renderAndroidContent = () => {
         let mainDomain = getMainDomain();
-        if(mainDomain!=null)mainDomain=mainDomain.toUpperCase();
+        if (mainDomain != null) mainDomain = mainDomain.toUpperCase();
         return (
             <div className={styles.container}>
                 <div className={styles.header}>
@@ -105,7 +205,7 @@ const WShortcutGuide = () => {
                     <div className={styles.android_op_2}>
                         <span className={styles.header}>{i18n.t("installBox.shortcut.title")}</span>
                         <div className={styles.app_info_con}>
-                            <img className={styles.icon} src={iconUrl}/>
+                            <img className={styles.icon} src={iconUrl} />
                             <span>{mainDomain}</span>
                         </div>
                         <div className={styles.app_btns}>
@@ -115,118 +215,258 @@ const WShortcutGuide = () => {
                     </div>
                 </div>
                 <div className={styles.t_guide_con}>
-                    <img className={styles.t_hand} src={img_hand}/>
+                    <img className={styles.t_hand} src={img_hand} />
                 </div>
             </div>
-        )
-    }
+        );
+    };
     const renderAndroid = () => {
+        if (deferredPromptRef.current != null && isShow) {
+            return <></>;
+        } else {
+            return (
+                <MuiDrawer open={isShow}
+                    anchor={"top"}
+                    sx={{
+                        pointerEvents: 'auto',
+                        zIndex: 'var(--zIndex-popup)',
+                        "& .MuiDrawer-paper": {
+                            boxSizing: "border-box",
+                            overflow: 'visible !important',
+                            backgroundColor: 'var(--standard);',
+                            boxShadow: 'var(--widget-shadow);',
+                            borderBottomLeftRadius: '0.71rem',
+                            borderBottomRightRadius: '0.71rem',
+                            padding: '1.42rem'
+                        },
+                    }}
+                    variant="temporary"
+                    onClose={() => {
+                        setIsShow(false);
+                    }}>
+                    {renderAndroidContent()}
+                </MuiDrawer>
+            );
+        };
+    };
+    const getDownBonus=()=>{
+        let bonus=Number(downConf?.bonus);
+        if (Number.isNaN(bonus)) bonus=0;
+        return global.DOLLAR+formatCash(bonus,0);
+    }
+    const checkIsChrome=()=>{
+        return isChrome();
+    }
+    const checkIsHighSafari=()=>{
+        let highVer=26;
+        try {
+            return iosVersion()>=highVer;
+        }
+        catch(e){
+            return false;
+        }
+    }
+    const renderStepScreen=()=>{
         return (
-            <MuiDrawer open={isShow}
-                       anchor={"top"}
-                       sx={{
-                           pointerEvents: 'auto',
-                           zIndex: 'var(--zindex-install)',
-                           "& .MuiDrawer-paper": {
-                               boxSizing: "border-box",
-                               overflow: 'visible !important',
-                               backgroundColor:'var(--standard);',
-                               boxShadow: 'var(--widget-shadow);',
-                               borderBottomLeftRadius: '0.71rem',
-                               borderBottomRightRadius: '0.71rem',
-                               padding: '1.42rem'
-                           },
-                       }}
-                       variant="temporary"
-                       onClose={()=>{
-                           setIsShow(false)
-                       }}>
-                {renderAndroidContent()}
-            </MuiDrawer>
+            <div className={styles.step_content_h}>
+                <div className={styles.screen_con}>
+                    <img className={styles.phone} src={img_screen} alt={""}/>
+                    <img className={styles.s_icon} src={iconUrl} alt={""}/>
+                </div>
+                <div className={styles.tips}>
+                    {i18n.t("installBox.shortcut.op_tip_5")}
+                    <span className={styles.money}>{getDownBonus()}</span>
+                </div>
+            </div>
         )
     }
-    const renderIOSContent=()=>{
-        let mainDomain = getMainDomain();
-        if(mainDomain!=null)mainDomain=mainDomain.toUpperCase();
-        const origin=window.location.origin;
+    const renderChromeStepItems = () => {
         return (
-            <div className={styles.container}>
-                <div className={styles.header}>
-                    <span className={styles.title}>
-                        {i18n.t("installBox.shortcut.title")}
-                    </span>
-                </div>
-                <div className={styles.content}>
-                    <span className={styles.sub_title}>{i18n.t("installBox.shortcut.sub_1")}</span>
-                    <div className={styles.ios_op_1}>
-                        <span>{i18n.t("installBox.shortcut.ios_op_1")}</span>
-                        <span>{i18n.t("installBox.shortcut.ios_op_2")}</span>
-                        <span>{i18n.t("installBox.shortcut.ios_op_3")}</span>
-                        <span className={styles.bottom}>{i18n.t("installBox.shortcut.ios_op_4")}</span>
+            <>
+                <IOSStepItem key={1} stepNO={1}>
+                    <div className={styles.step_content_v}>
+                        <img className={styles.chrome_img_1} src={img_ch_step_1} alt={""}/>
+                        <div className={styles.op_tip_con}>
+                            {i18n.t("installBox.shortcut.op_tip_1")}
+                            <img src={img_share} alt={"up"}/>
+                            {i18n.t("installBox.shortcut.op_tip_2")}
+                        </div>
                     </div>
-                    <span className={styles.sub_title}>{i18n.t("installBox.shortcut.sub_2")}</span>
-                    <div className={styles.ios_op_2}>
-                        <div className={styles.header}>
-                            <span className={styles.btn}>{i18n.t("installBox.shortcut.cancel")}</span>
-                            <span className={styles.title_1}>{i18n.t("installBox.shortcut.title")}</span>
-                            <span className={styles.btn}>{i18n.t("installBox.shortcut.add")}</span>
+                </IOSStepItem>
+                <IOSStepItem key={2} stepNO={2}>
+                    <div className={styles.step_content_v}>
+                        <img className={styles.chrome_img_2} src={img_ch_step_2} alt={""}/>
+                        <div className={styles.op_tip_con}>
+                            {i18n.t("installBox.shortcut.op_tip_3")}
+                            <img src={img_add} alt={"add"}/>
+                            {i18n.t("installBox.shortcut.op_tip_4")}
                         </div>
-                        <div className={styles.appinfo_con}>
-                            <img className={styles.icon} src={iconUrl}/>
-                            <div className={styles.info_con}>
-                                <span className={styles.host}>{mainDomain}</span>
-                                <span className={styles.origin}>{origin}</span>
-                            </div>
+                    </div>
+                </IOSStepItem>
+                <IOSStepItem key={3} stepNO={3} divider={false}>
+                    {renderStepScreen()}
+                </IOSStepItem>
+            </>
+        )
+    }
+    const renderSafariStepItems = () => {
+        let isHigh=checkIsHighSafari();
+        return (
+            <>
+                <IOSStepItem key={1} stepNO={1} bodyClass={isHigh?styles.body_gray:""}>
+                    <div className={styles.step_content_v}>
+                        {
+                            isHigh?(
+                                <>
+                                    <img className={styles.safari_h_img_1} src={img_sa_h_step_1} alt={""}/>
+                                    <img className={styles.safari_h_img_1_1} src={img_sa_h_step_1_1} alt={""}/>
+                                </>
+                            ) : (
+                                <img className={styles.safari_img_1} src={img_sa_step_1} alt={""}/>
+                            )
+                        }
+                        <div className={styles.op_tip_con}>
+                            {i18n.t("installBox.shortcut.op_tip_1")}
+                            <img src={img_share} alt={"up"}/>
+                            {i18n.t("installBox.shortcut.op_tip_2")}
                         </div>
-                        <div className={styles.tips_con}>
-                            <span>{i18n.t("installBox.shortcut.tips_1")}</span>
+                    </div>
+                </IOSStepItem>
+                <IOSStepItem key={2} stepNO={2}>
+                    <div className={styles.step_content_v}>
+                        {
+                            isHigh?(
+                                <img className={styles.safari_h_img_2} src={img_sa_h_step_2} alt={""}/>
+                            ): (
+                                <img className={styles.safari_img_2} src={img_sa_step_2} alt={""}/>
+                            )
+                        }
+                        <div className={styles.op_tip_con}>
+                            {i18n.t("installBox.shortcut.op_tip_3")}
+                            <img src={img_add} alt={"add"}/>
+                            {i18n.t("installBox.shortcut.op_tip_4")}
                         </div>
                     </div>
+                </IOSStepItem>
+                <IOSStepItem key={3} stepNO={3} divider={false}>
+                    {renderStepScreen()}
+                </IOSStepItem>
+            </>
+        )
+    }
+    const renderIOSStepItems = () => {
+        if (checkIsChrome()) return renderChromeStepItems();
+        else return renderSafariStepItems();
+    }
+    const renderIOSSteps = () => {
+        return (
+            <div className={styles.steps_con}>
+                {
+                    renderIOSStepItems()
+                }
+            </div>
+        )
+    }
+    const renderIOSTitle = () => {
+        let bonus = getDownBonus();
+        return (
+            <div className={styles.title_con}>
+                <div className={styles.title}>
+                    {i18n.t("installBox.shortcut.title_1")}
+                    <span>{" "+bonus}</span>
                 </div>
-                <div className={styles.b_guide_con}>
-                    <img className={styles.b_hand} src={img_hand}/>
+                <div className={styles.sub_title}>
+                    {stringFormat(i18n.t("installBox.shortcut.sub_title"),[bonus])}
+                </div>
+            </div>
+        )
+    }
+    const renderIOSClose=()=>{
+        return (
+            <button className={`${styles.btn_close} ${checkIsChrome()?styles.bottom:""}`} onClick={() => {
+                setIsShow(false)
+            }}>
+                <img src={img_close} alt={"close"}/>
+            </button>
+        )
+    }
+    const renderIOSHandGuide=()=>{
+        if (checkIsChrome()){
+            return (
+                <div className={`${styles.guide_hand_con} ${styles.t_right}`}>
+                    <img className={styles.up_hand} src={img_hand_new}/>
                 </div>
+            )
+        }
+        else {
+            let isHigh=checkIsHighSafari();
+            if (isHigh){
+                return (
+                    <div className={`${styles.guide_hand_con} ${styles.b_right}`}>
+                        <img className={styles.hand} src={img_hand_new}/>
+                        <img className={styles.more} src={img_more}/>
+                    </div>
+                )
+            } else {
+                return (
+                    <div className={`${styles.guide_hand_con} ${styles.b_center}`}>
+                        <img className={styles.hand} src={img_hand_new}/>
+                        <img className={styles.share} src={img_share}/>
+                    </div>
+                )
+            }
+        }
+    }
+    const renderIOSNewContent = () => {
+        return (
+            <div className={styles.ios_container}>
+                {renderIOSTitle()}
+                {renderIOSSteps()}
+                {renderIOSClose()}
+                {renderIOSHandGuide()}
             </div>
         )
     }
-    const renderIOS = () => {
+    const renderIOSNew= () => {
+        let is_chrome=checkIsChrome();
         return (
             <MuiDrawer open={isShow}
-                       anchor={"bottom"}
+                       anchor={is_chrome?"top":"bottom"}
                        sx={{
                            pointerEvents: 'auto',
-                           zIndex: 'var(--zindex-install)',
+                           zIndex: 'var(--zIndex-popup)',
                            "& .MuiDrawer-paper": {
                                boxSizing: "border-box",
                                overflow: 'visible !important',
-                               backgroundColor:'var(--standard);',
+                               backgroundColor: 'var(--darkColor2);',
                                boxShadow: 'var(--widget-shadow);',
-                               borderTopLeftRadius: '0.71rem',
-                               borderTopRightRadius: '0.71rem',
-                               padding: '1.42rem'
+                               borderTopLeftRadius: is_chrome?'0rem':'0.71rem',
+                               borderTopRightRadius: is_chrome?'0rem':'0.71rem',
+                               borderBottomLeftRadius:is_chrome?'0.71rem':'0rem',
+                               borderBottomRightRadius:is_chrome?'0.71rem':'0rem',
+                               // padding: '1.42rem'
                            },
                        }}
                        variant="temporary"
-                       onClose={()=>{
-                           setIsShow(false)
+                       onClose={() => {
+                           setIsShow(false);
                        }}>
-                {renderIOSContent()}
+                {renderIOSNewContent()}
             </MuiDrawer>
-        )
+        );
     }
-    const renderGuide=()=>{
-        if (isAndroid()){
+    const renderGuide = () => {
+        if (isAndroid()) {
             return renderAndroid();
         }
-        else
-        {
-            return renderIOS();
+        else {
+            return renderIOSNew();
         }
-    }
+    };
     return (
         <>
             {renderGuide()}
         </>
-    )
-}
+    );
+};
 export default WShortcutGuide;

+ 263 - 0
src/widgets/WInstallBox/WShortcutGuide/styles.module.scss

@@ -222,3 +222,266 @@
     animation: top_guide .9s ease-in-out infinite;
   }
 }
+
+.ios_container{
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  width: 100%;
+  position: relative;
+  padding: 0rem 2.1rem;
+  min-height: 50vh;
+  & .title_con{
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    margin-top: 0.91rem;
+    margin-bottom: 2rem;
+    gap: 0.5rem;
+    text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
+    & .title{
+      font-size: 1.3rem;
+      font-weight: 600;
+      color: #ffffff;
+      span{
+        color: #ffcb20;
+      }
+    }
+    & .sub_title{
+      color: #d9dde2;
+    }
+  }
+  & .steps_con{
+    display: flex;
+    flex-direction: column;
+    width: 100%;
+    height: auto;
+    margin-bottom: 1.5rem;
+    & .item_con{
+      display: flex;
+      flex-direction: row;
+      position: relative;
+      gap: 1rem;
+      width: 100%;
+      height: auto;
+      //height: 10rem;
+      & .item_header{
+        display: flex;
+        justify-content: center;
+        height: 100%;
+        width: 0.5rem;
+        & .line{
+          position: absolute;
+          height: 100%;
+          z-index: 0;
+          border-left: 1.5px solid #5a636d;
+          top: 0.75rem;
+        }
+        & .dot{
+          display: flex;
+          align-items: center;
+          justify-content: center;
+          position: absolute;
+          z-index: 1;
+          padding: 0.5rem;
+          &:after{
+            background-color: #fed200;
+            border-radius: 50%;
+            content: "";
+            display: block;
+            flex: none;
+            height: 0.8rem;
+            width: 0.8rem;
+          }
+        }
+      }
+      & .item_body{
+        display: flex;
+        flex-direction: column;
+        flex: 1;
+        gap: 0.3rem;
+        margin-bottom: 2rem;
+        & .body_title{
+          font-size: 1.1rem;
+          font-weight: 600;
+          color: #f5d543;
+        }
+        & .body_content{
+          display: flex;
+          align-items: center;
+          justify-content: center;
+          width: 100%;
+          padding: 0.75rem;
+          background-color: #1c1c27;
+          border-radius: 0.8rem;
+          &.body_gray{
+            background-color: #343b48;
+          }
+        }
+      }
+    }
+  }
+  & .btn_close{
+    cursor: pointer;
+    position: absolute;
+    right: 1.02rem;
+    top: 1.02rem;
+    width: 1.32rem;
+    img{
+      width: 100%;
+      height: auto;
+    }
+    &.bottom{
+      top: unset;
+      bottom: 1.02rem;
+    }
+  }
+}
+.step_content_v{
+  display: flex;
+  flex-direction: column;
+  gap: 0.5rem;
+  width: 100%;
+  height: auto;
+  & .op_tip_con{
+    display: flex;
+    flex-direction: row;
+    align-items: center;
+    gap: 0.75rem;
+    font-size: 1rem;
+    color: #c9cad3;
+    margin-left: 0.5rem;
+    img{
+      width: 1rem;
+      height: auto;
+    }
+  }
+  & .chrome_img_1{
+    width: 100%;
+    aspect-ratio: 521/63;
+    //height: auto;
+  }
+  & .chrome_img_2{
+    width: 87%;
+    aspect-ratio: 444/163;
+    //height: auto;
+  }
+  & .safari_img_1{
+    width: 100%;
+    aspect-ratio: 513/62;
+  }
+  & .safari_img_2{
+    width: 87%;
+    aspect-ratio: 451/165;
+  }
+  & .safari_h_img_1{
+    width: 85%;
+    aspect-ratio: 438/65;
+  }
+  & .safari_h_img_1_1{
+    width: 43.5%;
+    aspect-ratio: 224/63;
+  }
+  & .safari_h_img_2{
+    width: 87%;
+    aspect-ratio: 450/117;
+  }
+}
+.step_content_h{
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: space-between;
+  width: 100%;
+  gap: 1.3rem;
+  & .screen_con{
+    width: 10rem;
+    aspect-ratio: 251/108;
+    position: relative;
+    & .phone{
+      width: 100%;
+      height: 100%;
+      z-index: 0;
+    }
+    & .s_icon{
+      position: absolute;
+      width: 2.2rem;
+      height: 2.2rem;
+      left: 0.6rem;
+      bottom: 0.2rem;
+      z-index: 1;
+      border-radius: 0.35rem;
+    }
+  }
+  & .tips{
+    flex: 1;
+    font-size: 1rem;
+    color: #c9cad3;
+    & .money{
+      color: #c2b348;
+    }
+  }
+}
+.guide_hand_con{
+  display: flex;
+  align-items: center;
+  position: absolute;
+  gap: 0.75rem;
+  &.b_center{
+    bottom: 0.36rem;
+    left: calc(50% - 1.21rem);
+  }
+  &.b_right{
+    bottom: 0.36rem;
+    right: 2.5rem;
+  }
+  &.t_right{
+    top: 0.36rem;
+    right: 0.5rem;
+  }
+  & .hand{
+    animation: updown 1s infinite;
+    width: 2.42rem;
+  }
+  & .up_hand{
+    animation: updown1 1s infinite;
+    width: 2.42rem;
+  }
+  & .share{
+    height: 1.28rem;
+    width: 1.02rem;
+  }
+  & .more{
+    width: 1.44rem;
+    height: auto;
+  }
+}
+
+@keyframes updown1 {
+  0% {
+    transform: rotate(180deg) translateY(0)
+  }
+
+  50% {
+    transform: rotate(180deg) translateY(-0.36rem)
+  }
+
+  to {
+    transform: rotate(180deg) translateY(0)
+  }
+}
+
+@keyframes updown {
+  0% {
+    transform: translateY(0)
+  }
+
+  50% {
+    transform: translateY(-0.36rem)
+  }
+
+  to {
+    transform: translateY(0)
+  }
+}