Last active
May 30, 2026 05:56
-
-
Save wangshuai-007/1dcdc5c09ba7d7e2fb3a0e2442800cbd to your computer and use it in GitHub Desktop.
微信小程序跳转导航页,使用前配置miniProgramGroups对象即可;每个appid 支持设置 name(名称,必须), icon(图标) testUrl(体验环境地址)和prodUrl(正式环境地址)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="zh-CN"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>小程序跳转页</title> | |
| <style> | |
| :root { | |
| --bg: linear-gradient(135deg, #f6fbff 0%, #eef6f2 100%); | |
| --card: rgba(255, 255, 255, 0.92); | |
| --text: #173042; | |
| --muted: #5d7382; | |
| --border: rgba(23, 48, 66, 0.1); | |
| --test-start: #ffd36f; | |
| --test-end: #ff9b57; | |
| --prod-start: #1fbe7a; | |
| --prod-end: #0d8f5a; | |
| --shadow: 0 18px 45px rgba(18, 48, 68, 0.12); | |
| } | |
| * { | |
| box-sizing: border-box; | |
| } | |
| body { | |
| margin: 0; | |
| min-height: 100vh; | |
| font-family: "Microsoft YaHei", "PingFang SC", sans-serif; | |
| color: var(--text); | |
| background: var(--bg); | |
| } | |
| .page { | |
| max-width: 960px; | |
| margin: 0 auto; | |
| padding: 40px 20px 56px; | |
| } | |
| .hero { | |
| margin-bottom: 28px; | |
| padding: 28px; | |
| border: 1px solid var(--border); | |
| border-radius: 24px; | |
| background: var(--card); | |
| box-shadow: var(--shadow); | |
| backdrop-filter: blur(12px); | |
| } | |
| .hero h1 { | |
| margin: 0 0 10px; | |
| font-size: clamp(28px, 4vw, 42px); | |
| line-height: 1.1; | |
| } | |
| .hero-qr-btn { | |
| display: inline-flex; | |
| align-items: center; | |
| justify-content: center; | |
| margin-left: 10px; | |
| padding: 6px; | |
| width: 42px; | |
| height: 42px; | |
| border: none; | |
| border-radius: 12px; | |
| background: linear-gradient(135deg, #07c160, #06ad56); | |
| color: #fff; | |
| font-size: 22px; | |
| cursor: pointer; | |
| vertical-align: middle; | |
| transition: transform 0.18s ease, box-shadow 0.18s ease; | |
| box-shadow: 0 4px 12px rgba(7, 193, 96, 0.3); | |
| } | |
| .hero-qr-btn:hover { | |
| transform: translateY(-2px) scale(1.05); | |
| box-shadow: 0 8px 20px rgba(7, 193, 96, 0.4); | |
| } | |
| .hero-qr-btn:active { | |
| transform: translateY(0) scale(0.96); | |
| } | |
| .hero p { | |
| margin: 0; | |
| color: var(--muted); | |
| line-height: 1.7; | |
| font-size: 15px; | |
| } | |
| .legend { | |
| display: grid; | |
| grid-template-columns: repeat(2, minmax(0, 1fr)); | |
| gap: 14px; | |
| margin: 24px 0; | |
| } | |
| .legend-item { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| padding: 14px 16px; | |
| border-radius: 18px; | |
| background: rgba(255, 255, 255, 0.72); | |
| border: 1px solid var(--border); | |
| } | |
| .legend-badge { | |
| width: 14px; | |
| height: 14px; | |
| border-radius: 50%; | |
| flex: 0 0 auto; | |
| } | |
| .legend-badge.test { | |
| background: linear-gradient(135deg, var(--test-start), var(--test-end)); | |
| } | |
| .legend-badge.prod { | |
| background: linear-gradient(135deg, var(--prod-start), var(--prod-end)); | |
| } | |
| .board { | |
| padding: 24px; | |
| border-radius: 24px; | |
| background: var(--card); | |
| border: 1px solid var(--border); | |
| box-shadow: var(--shadow); | |
| } | |
| .toolbar { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| gap: 16px; | |
| margin-bottom: 18px; | |
| flex-wrap: wrap; | |
| } | |
| .control-panel { | |
| display: grid; | |
| grid-template-columns: repeat(2, minmax(220px, 1fr)); | |
| gap: 14px; | |
| width: 100%; | |
| margin-bottom: 18px; | |
| } | |
| .control-card { | |
| padding: 16px 18px; | |
| border: 1px solid rgba(23, 48, 66, 0.1); | |
| border-radius: 18px; | |
| background: rgba(255, 255, 255, 0.82); | |
| } | |
| .control-card strong { | |
| display: block; | |
| margin-bottom: 10px; | |
| font-size: 14px; | |
| } | |
| .control-options { | |
| display: flex; | |
| gap: 14px; | |
| flex-wrap: wrap; | |
| } | |
| .control-hint { | |
| display: none; | |
| margin-top: 10px; | |
| color: var(--muted); | |
| font-size: 12px; | |
| line-height: 1.6; | |
| } | |
| .control-hint.is-visible { | |
| display: block; | |
| } | |
| .control-option { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 8px; | |
| color: var(--muted); | |
| font-size: 13px; | |
| cursor: pointer; | |
| } | |
| .inline-help-btn { | |
| width: 20px; | |
| height: 20px; | |
| border: 0; | |
| border-radius: 50%; | |
| background: linear-gradient(135deg, var(--prod-start), var(--prod-end)); | |
| color: #fff; | |
| font-size: 12px; | |
| font-weight: 700; | |
| line-height: 1; | |
| cursor: pointer; | |
| display: inline-flex; | |
| align-items: center; | |
| justify-content: center; | |
| padding: 0; | |
| } | |
| .control-option input { | |
| margin: 0; | |
| accent-color: #0d8f5a; | |
| } | |
| .search-wrap { | |
| position: relative; | |
| flex: 1 1 420px; | |
| } | |
| .search-input { | |
| width: 100%; | |
| height: 50px; | |
| padding: 0 120px 0 18px; | |
| border: 1px solid rgba(23, 48, 66, 0.14); | |
| border-radius: 16px; | |
| outline: none; | |
| background: rgba(255, 255, 255, 0.92); | |
| color: var(--text); | |
| font-size: 14px; | |
| box-shadow: inset 0 1px 2px rgba(23, 48, 66, 0.04); | |
| } | |
| .search-input:focus { | |
| border-color: rgba(13, 143, 90, 0.45); | |
| box-shadow: 0 0 0 4px rgba(31, 190, 122, 0.12); | |
| } | |
| .search-shortcut { | |
| position: absolute; | |
| top: 50%; | |
| right: 14px; | |
| transform: translateY(-50%); | |
| padding: 6px 10px; | |
| border-radius: 10px; | |
| background: rgba(23, 48, 66, 0.08); | |
| color: var(--muted); | |
| font-size: 12px; | |
| line-height: 1; | |
| white-space: nowrap; | |
| } | |
| .toolbar-meta { | |
| color: var(--muted); | |
| font-size: 13px; | |
| white-space: nowrap; | |
| } | |
| .groups { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 16px; | |
| } | |
| .group { | |
| border: 1px solid var(--border); | |
| border-radius: 22px; | |
| background: rgba(255, 255, 255, 0.76); | |
| overflow: hidden; | |
| } | |
| .group[open] { | |
| box-shadow: 0 14px 32px rgba(18, 48, 68, 0.08); | |
| } | |
| .group-summary { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| gap: 16px; | |
| padding: 18px 20px; | |
| cursor: pointer; | |
| list-style: none; | |
| } | |
| .group-summary::-webkit-details-marker { | |
| display: none; | |
| } | |
| .group-title { | |
| min-width: 0; | |
| } | |
| .group-title strong, | |
| .group-title span { | |
| display: block; | |
| min-width: 0; | |
| white-space: nowrap; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| } | |
| .group-title strong { | |
| font-size: 16px; | |
| } | |
| .group-title span { | |
| margin-top: 4px; | |
| color: var(--muted); | |
| font-size: 12px; | |
| } | |
| .group-badge { | |
| flex: 0 0 auto; | |
| padding: 8px 12px; | |
| border-radius: 999px; | |
| background: rgba(31, 190, 122, 0.12); | |
| color: #0d8f5a; | |
| font-size: 12px; | |
| font-weight: 700; | |
| } | |
| .group-meta { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| flex: 0 0 auto; | |
| } | |
| .group-actions { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| flex-wrap: wrap; | |
| justify-content: flex-end; | |
| } | |
| .group-action { | |
| min-width: 72px; | |
| padding: 8px 12px; | |
| border: 1px solid rgba(23, 48, 66, 0.12); | |
| border-radius: 999px; | |
| background: rgba(255, 255, 255, 0.9); | |
| color: var(--text); | |
| font-size: 12px; | |
| font-weight: 700; | |
| cursor: pointer; | |
| transition: transform 0.16s ease, border-color 0.16s ease, background 0.16s ease; | |
| } | |
| .group-action:hover { | |
| transform: translateY(-1px); | |
| border-color: rgba(13, 143, 90, 0.32); | |
| } | |
| .group-action.is-active { | |
| color: #0d8f5a; | |
| background: rgba(31, 190, 122, 0.12); | |
| border-color: rgba(13, 143, 90, 0.24); | |
| } | |
| .group-action:disabled { | |
| cursor: not-allowed; | |
| opacity: 0.45; | |
| transform: none; | |
| } | |
| .group-body { | |
| padding: 0 18px 18px; | |
| } | |
| .board-header, | |
| .row { | |
| display: grid; | |
| grid-template-columns: minmax(180px, 1.3fr) minmax(140px, 1fr) minmax(140px, 1fr); | |
| gap: 14px; | |
| align-items: center; | |
| } | |
| .board-header { | |
| padding: 0 0 12px; | |
| color: var(--muted); | |
| font-size: 13px; | |
| letter-spacing: 0.04em; | |
| } | |
| .rows { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 12px; | |
| } | |
| .row { | |
| padding: 16px; | |
| border: 1px solid var(--border); | |
| border-radius: 20px; | |
| background: rgba(255, 255, 255, 0.78); | |
| } | |
| .appid { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| min-width: 0; | |
| } | |
| .appid-avatar { | |
| width: 42px; | |
| height: 42px; | |
| border-radius: 12px; | |
| object-fit: cover; | |
| flex: 0 0 auto; | |
| background: linear-gradient(135deg, #d7efe4, #eef7ff); | |
| border: 1px solid rgba(23, 48, 66, 0.08); | |
| } | |
| .appid-text { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 6px; | |
| min-width: 0; | |
| flex: 1 1 auto; | |
| } | |
| .appid-text strong, | |
| .appid-text span { | |
| min-width: 0; | |
| } | |
| .appid-text strong { | |
| font-size: 15px; | |
| white-space: nowrap; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| } | |
| .appid-text span { | |
| color: var(--muted); | |
| font-size: 12px; | |
| white-space: nowrap; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| } | |
| .jump-btn { | |
| display: inline-flex; | |
| justify-content: center; | |
| align-items: center; | |
| width: 100%; | |
| min-height: 46px; | |
| padding: 12px 16px; | |
| border: 0; | |
| border-radius: 14px; | |
| color: #fff; | |
| font-size: 14px; | |
| font-weight: 700; | |
| text-decoration: none; | |
| white-space: nowrap; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| transition: transform 0.18s ease, box-shadow 0.18s ease, opacity 0.18s ease; | |
| } | |
| .jump-btn:hover { | |
| transform: translateY(-2px); | |
| opacity: 0.96; | |
| } | |
| .jump-btn.test { | |
| background: linear-gradient(135deg, var(--test-start), var(--test-end)); | |
| box-shadow: 0 12px 24px rgba(255, 155, 87, 0.28); | |
| } | |
| .jump-btn.prod { | |
| background: linear-gradient(135deg, var(--prod-start), var(--prod-end)); | |
| box-shadow: 0 12px 24px rgba(13, 143, 90, 0.28); | |
| } | |
| .empty { | |
| padding: 22px; | |
| border-radius: 18px; | |
| text-align: center; | |
| color: var(--muted); | |
| background: rgba(255, 255, 255, 0.72); | |
| border: 1px dashed rgba(23, 48, 66, 0.18); | |
| } | |
| .tips { | |
| margin-top: 18px; | |
| color: var(--muted); | |
| font-size: 13px; | |
| line-height: 1.7; | |
| } | |
| .qr-modal { | |
| position: fixed; | |
| inset: 0; | |
| display: none; | |
| align-items: center; | |
| justify-content: center; | |
| padding: 20px; | |
| background: rgba(8, 20, 30, 0.56); | |
| z-index: 999; | |
| } | |
| .qr-modal.is-open { | |
| display: flex; | |
| } | |
| .qr-dialog { | |
| width: min(92vw, 420px); | |
| padding: 22px; | |
| border-radius: 24px; | |
| background: rgba(255, 255, 255, 0.98); | |
| box-shadow: 0 24px 60px rgba(12, 28, 42, 0.28); | |
| } | |
| .qr-dialog-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| gap: 12px; | |
| margin-bottom: 14px; | |
| } | |
| .qr-dialog-header h3 { | |
| margin: 0; | |
| font-size: 18px; | |
| } | |
| .qr-header-actions { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| } | |
| .qr-help-btn { | |
| width: 36px; | |
| height: 36px; | |
| border: 0; | |
| border-radius: 50%; | |
| background: rgba(7, 193, 96, 0.12); | |
| color: #07c160; | |
| font-size: 18px; | |
| font-weight: 700; | |
| cursor: pointer; | |
| transition: transform 0.18s ease, background 0.18s ease; | |
| } | |
| .qr-help-btn:hover { | |
| transform: scale(1.12); | |
| background: rgba(7, 193, 96, 0.22); | |
| } | |
| .qr-close { | |
| width: 36px; | |
| height: 36px; | |
| border: 0; | |
| border-radius: 50%; | |
| background: rgba(23, 48, 66, 0.08); | |
| color: var(--text); | |
| font-size: 18px; | |
| cursor: pointer; | |
| } | |
| .qr-dialog p { | |
| margin: 0 0 14px; | |
| color: var(--muted); | |
| line-height: 1.7; | |
| font-size: 13px; | |
| } | |
| .qr-canvas-wrap { | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| min-height: 280px; | |
| margin-bottom: 14px; | |
| padding: 14px; | |
| border-radius: 20px; | |
| background: linear-gradient(135deg, #f7fbff, #eef8f1); | |
| } | |
| .qr-url { | |
| padding: 12px 14px; | |
| border-radius: 16px; | |
| background: rgba(23, 48, 66, 0.06); | |
| color: var(--muted); | |
| font-size: 12px; | |
| line-height: 1.6; | |
| word-break: break-all; | |
| } | |
| .guide-modal { | |
| position: fixed; | |
| inset: 0; | |
| display: none; | |
| align-items: center; | |
| justify-content: center; | |
| padding: 20px; | |
| background: rgba(8, 20, 30, 0.56); | |
| z-index: 1000; | |
| } | |
| .guide-modal.is-open { | |
| display: flex; | |
| } | |
| .guide-dialog { | |
| width: min(92vw, 460px); | |
| padding: 20px; | |
| border-radius: 24px; | |
| background: #fff; | |
| } | |
| .guide-indicator { | |
| display: flex; | |
| gap: 8px; | |
| margin-bottom: 12px; | |
| } | |
| .guide-dot { | |
| width: 8px; | |
| height: 8px; | |
| border-radius: 50%; | |
| background: #c8d5de; | |
| } | |
| .guide-dot.is-active { | |
| background: #0d8f5a; | |
| } | |
| .phone-mock { | |
| position: relative; | |
| height: 280px; | |
| border-radius: 24px; | |
| border: 1px solid var(--border); | |
| background: linear-gradient(160deg, #f9fcff, #edf4fb); | |
| overflow: hidden; | |
| margin-bottom: 12px; | |
| } | |
| .mock-center-qr { | |
| position: absolute; | |
| left: 50%; | |
| top: 46%; | |
| width: 88px; | |
| height: 88px; | |
| transform: translate(-50%, -50%); | |
| border-radius: 6px; | |
| border: 4px solid #fff; | |
| background: #fff; | |
| box-shadow: 0 0 0 1px #d8e3ea, 0 8px 20px rgba(12, 28, 42, 0.2); | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| overflow: hidden; | |
| } | |
| .mock-center-qr svg { | |
| width: 80px; | |
| height: 80px; | |
| fill: #000; | |
| display: block; | |
| } | |
| .mock-crop { | |
| position: absolute; | |
| left: 0; | |
| top: 0; | |
| width: 0; | |
| height: 0; | |
| border: 2px dashed rgba(13, 143, 90, 0.9); | |
| border-radius: 10px; | |
| opacity: 0; | |
| transition: opacity 0.2s ease; | |
| } | |
| .phone-mock.step-1 .mock-crop { | |
| opacity: 1; | |
| animation: cropGrow 1.6s ease-out both; | |
| } | |
| .phone-mock.step-2 .mock-crop { | |
| left: 17%; | |
| top: 13%; | |
| width: 66%; | |
| height: 62%; | |
| opacity: 1; | |
| } | |
| @keyframes cropGrow { | |
| 0% { left: 17%; top: 13%; width: 18px; height: 18px; opacity: 0.45; } | |
| 72% { left: 17%; top: 13%; width: 66%; height: 62%; opacity: 1; } | |
| 100% { left: 17%; top: 13%; width: 66%; height: 62%; opacity: 1; } | |
| } | |
| .mock-toolbar { | |
| position: absolute; | |
| left: 50%; | |
| bottom: 10px; | |
| display: flex; | |
| gap: 10px; | |
| justify-content: center; | |
| padding: 12px; | |
| background: rgba(255, 255, 255, 0.94); | |
| border-top: 1px solid var(--border); | |
| border-radius: 14px; | |
| box-shadow: 0 8px 18px rgba(12, 28, 42, 0.12); | |
| opacity: 0; | |
| transform: translate(-50%, 10px); | |
| transition: opacity 0.35s ease, transform 0.35s ease; | |
| width: max-content; | |
| } | |
| .phone-mock.step-2 .mock-toolbar { | |
| opacity: 1; | |
| transform: translate(-50%, 0); | |
| } | |
| .mock-cursor { | |
| position: absolute; | |
| left: 74%; | |
| top: 74%; | |
| width: 22px; | |
| height: 30px; | |
| background-repeat: no-repeat; | |
| background-size: contain; | |
| background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 22 30'%3E%3Cpath d='M2 1 L2 24 L8.4 18.6 L12.7 28 L16.8 26.1 L12.6 16.8 L21 16.8 Z' fill='white' stroke='%231d3140' stroke-width='1.5' stroke-linejoin='round'/%3E%3C/svg%3E"); | |
| transform: translate(-50%, -50%); | |
| filter: drop-shadow(0 2px 3px rgba(0, 0, 0, 0.25)); | |
| opacity: 0; | |
| z-index: 2; | |
| pointer-events: none; | |
| } | |
| .phone-mock.step-2 .mock-cursor { | |
| opacity: 1; | |
| animation: cursorMoveToQr 1.1s ease-out both; | |
| } | |
| @keyframes cursorMoveToQr { | |
| 0% { left: 74%; top: 74%; opacity: 0; } | |
| 18% { opacity: 1; } | |
| 100% { left: calc(53% + 20px); top: calc(88% + 20px); opacity: 1; } | |
| } | |
| .mock-tool { | |
| width: 26px; | |
| height: 26px; | |
| border-radius: 6px; | |
| border: 1px solid #c9d6df; | |
| display: inline-flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 13px; | |
| color: #415766; | |
| background: #fff; | |
| } | |
| .mock-tool.qr { | |
| background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNSIgaGVpZ2h0PSIzMSIgdmlld0JveD0iMCAwIDI1IDMxIiByb2xlPSJpbWciIGFyaWEtbGFiZWw9IlFSIGNvZGUgaWNvbiI+CiAgPHBhdGggZmlsbD0iIzFBMUExQSIgZD0iTTUgNGg4djFINVogTTE3IDRoNHYxSDE3WiBNNCA1aDF2MUg0WiBNMTMgNWgxdjFIMTNaIE0xNiA1aDF2MUgxNlogTTIxIDVoMXYxSDIxWiBNNCA2aDF2MUg0WiBNMTMgNmgxdjFIMTNaIE0xNiA2aDF2MUgxNlogTTIxIDZoMXYxSDIxWiBNNCA3aDF2MUg0WiBNMTMgN2gxdjFIMTNaIE0xNiA3aDF2MUgxNlogTTIxIDdoMXYxSDIxWiBNNCA4aDF2MUg0WiBNOCA4aDJ2MUg4WiBNMTMgOGgxdjFIMTNaIE0xNiA4aDF2MUgxNlogTTIxIDhoMXYxSDIxWiBNNCA5aDF2MUg0WiBNOCA5aDJ2MUg4WiBNMTMgOWgxdjFIMTNaIE0xNyA5aDR2MUgxN1ogTTQgMTBoMXYxSDRaIE0xMyAxMGgxdjFIMTNaIE00IDExaDF2MUg0WiBNMTMgMTFoMXYxSDEzWiBNNCAxMmgxdjFINFogTTEzIDEyaDF2MUgxM1ogTTE2IDEyaDJ2MUgxNlogTTIwIDEyaDJ2MUgyMFogTTUgMTNoOHYxSDVaIE0xNiAxM2gydjFIMTZaIE0yMCAxM2gydjFIMjBaIE01IDE2aDR2MUg1WiBNMTIgMTZoMnYxSDEyWiBNMTcgMTZoNHYxSDE3WiBNNCAxN2gxdjFINFogTTkgMTdoMXYxSDlaIE0xMiAxN2gydjFIMTJaIE0xNiAxN2gxdjFIMTZaIE0yMSAxN2gxdjFIMjFaIE00IDE4aDF2MUg0WiBNOSAxOGgxdjFIOVogTTE2IDE4aDF2MUgxNlogTTIxIDE4aDF2MUgyMVogTTQgMTloMXYxSDRaIE05IDE5aDF2MUg5WiBNMTYgMTloMXYxSDE2WiBNMjEgMTloMXYxSDIxWiBNNCAyMGgxdjFINFogTTkgMjBoMXYxSDlaIE0xMiAyMGgydjFIMTJaIE0xNiAyMGgxdjFIMTZaIE0yMSAyMGgxdjFIMjFaIE01IDIxaDR2MUg1WiBNMTIgMjFoMnYxSDEyWiBNMTcgMjFoNHYxSDE3WiIvPgogIDxwYXRoIGZpbGw9IiM0MTQxNDEiIGQ9Ik0xNiA5aDF2MUgxNlogTTQgMTNoMXYxSDRaIE00IDIxaDF2MUg0WiBNMTYgMjFoMXYxSDE2WiIvPgogIDxwYXRoIGZpbGw9IiM0MjQyNDIiIGQ9Ik00IDRoMXYxSDRaIE0xNiA0aDF2MUgxNlogTTQgMTZoMXYxSDRaIE0xNiAxNmgxdjFIMTZaIi8+CiAgPHBhdGggZmlsbD0iIzQ1NDU0NSIgZD0iTTEzIDRoMXYxSDEzWiBNMjEgNGgxdjFIMjFaIE0yMSA5aDF2MUgyMVogTTEzIDEzaDF2MUgxM1ogTTkgMTZoMXYxSDlaIE0yMSAxNmgxdjFIMjFaIE05IDIxaDF2MUg5WiBNMjEgMjFoMXYxSDIxWiIvPgogIDxwYXRoIGZpbGw9IiM5RjlGOUYiIGQ9Ik0xMiA1aDF2MUgxMlogTTIwIDVoMXYxSDIwWiBNMjAgOGgxdjFIMjBaIE0xMiAxMmgxdjFIMTJaIE04IDE3aDF2MUg4WiBNMjAgMTdoMXYxSDIwWiBNOCAyMGgxdjFIOFogTTIwIDIwaDF2MUgyMFoiLz4KICA8cGF0aCBmaWxsPSIjQTFBMUExIiBkPSJNNSA1aDF2MUg1WiBNMTcgNWgxdjFIMTdaIE0xNyA4aDF2MUgxN1ogTTUgMTJoMXYxSDVaIE01IDE3aDF2MUg1WiBNMTcgMTdoMXYxSDE3WiBNNSAyMGgxdjFINVogTTE3IDIwaDF2MUgxN1oiLz4KICA8cGF0aCBmaWxsPSIjQzFDMUMxIiBkPSJNNiA1aDZ2MUg2WiBNMTggNWgydjFIMThaIE0xOCA4aDJ2MUgxOFogTTYgMTJoNnYxSDZaIE02IDE3aDJ2MUg2WiBNMTggMTdoMnYxSDE4WiBNNiAyMGgydjFINlogTTE4IDIwaDJ2MUgxOFoiLz4KICA8cGF0aCBmaWxsPSIjQ0NDQ0NDIiBkPSJNMTIgNmgxdjFIMTJaIE0yMCA2aDF2MUgyMFogTTEyIDdoMXYxSDEyWiBNMjAgN2gxdjFIMjBaIE0xMiA4aDF2MUgxMlogTTEyIDloMXYxSDEyWiBNMTIgMTBoMXYxSDEyWiBNMTIgMTFoMXYxSDEyWiBNOCAxOGgxdjFIOFogTTIwIDE4aDF2MUgyMFogTTggMTloMXYxSDhaIE0yMCAxOWgxdjFIMjBaIi8+CiAgPHBhdGggZmlsbD0iI0NEQ0RDRCIgZD0iTTUgNmgxdjFINVogTTE3IDZoMXYxSDE3WiBNNSA3aDF2MUg1WiBNMTcgN2gxdjFIMTdaIE01IDhoMXYxSDVaIE01IDloMXYxSDVaIE01IDEwaDF2MUg1WiBNNSAxMWgxdjFINVogTTUgMThoMXYxSDVaIE0xNyAxOGgxdjFIMTdaIE01IDE5aDF2MUg1WiBNMTcgMTloMXYxSDE3WiIvPgo8L3N2Zz4="); | |
| background-repeat: no-repeat; | |
| background-position: center calc(50% + 1px); | |
| background-size: 18px 22px; | |
| color: transparent; | |
| font-size: 0; | |
| } | |
| .phone-mock.step-2 .mock-tool.qr { | |
| border-color: #0d8f5a; | |
| box-shadow: 0 0 0 4px rgba(31, 190, 122, 0.2); | |
| } | |
| .guide-step-text { | |
| margin: 0 0 14px; | |
| color: var(--muted); | |
| line-height: 1.7; | |
| font-size: 13px; | |
| } | |
| .kbd-hot { | |
| display: inline-block; | |
| padding: 2px 8px; | |
| margin: 0 3px; | |
| border-radius: 8px; | |
| background: rgba(13, 143, 90, 0.14); | |
| border: 1px solid rgba(13, 143, 90, 0.34); | |
| color: #0b7a4e; | |
| font-weight: 700; | |
| } | |
| .guide-confirm { | |
| width: 100%; | |
| height: 42px; | |
| border: 0; | |
| border-radius: 12px; | |
| background: linear-gradient(135deg, var(--prod-start), var(--prod-end)); | |
| color: #fff; | |
| cursor: pointer; | |
| font-size: 14px; | |
| } | |
| @media (max-width: 720px) { | |
| .legend, | |
| .board-header, | |
| .row { | |
| grid-template-columns: 1fr; | |
| } | |
| .toolbar { | |
| align-items: stretch; | |
| } | |
| .control-panel { | |
| grid-template-columns: 1fr; | |
| } | |
| .group-summary { | |
| align-items: flex-start; | |
| } | |
| .group-meta, | |
| .group-actions { | |
| width: 100%; | |
| } | |
| .group-meta { | |
| flex-direction: column; | |
| align-items: stretch; | |
| } | |
| .group-actions { | |
| justify-content: flex-start; | |
| } | |
| .toolbar-meta, | |
| .search-shortcut { | |
| white-space: normal; | |
| } | |
| .board-header { | |
| display: none; | |
| } | |
| .row { | |
| gap: 10px; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <main class="page"> | |
| <section class="hero"> | |
| <h1>微信小程序跳转页 | |
| <button id="hero-qr-btn" class="hero-qr-btn" type="button" aria-label="显示当前页面二维码" title="在微信中打开">📱</button> | |
| </h1> | |
| </section> | |
| <section class="board"> | |
| <div class="toolbar"> | |
| <label class="search-wrap" for="search-input"> | |
| <input | |
| id="search-input" | |
| class="search-input" | |
| type="search" | |
| placeholder="搜索分组、小程序名称或 AppID" | |
| autocomplete="off" | |
| > | |
| <span class="search-shortcut">/ 或 Ctrl/⌘ + K</span> | |
| </label> | |
| <div id="result-meta" class="toolbar-meta">准备就绪</div> | |
| </div> | |
| <div class="control-panel"> | |
| <div class="control-card"> | |
| <strong>正式环境链接类型</strong> | |
| <div class="control-options"> | |
| <label class="control-option"> | |
| <input type="radio" name="prod-link-type" value="scheme" checked> | |
| <span>URL Scheme</span> | |
| </label> | |
| <label class="control-option"> | |
| <input type="radio" name="prod-link-type" value="pc-shortcut"> | |
| <span>PC快捷方式</span> | |
| </label> | |
| </div> | |
| <div id="prod-link-hint" class="control-hint">使用前需在PC上打开对应小程序 → 点击右上角 → 添加到电脑桌面</div> | |
| </div> | |
| <div class="control-card"> | |
| <strong>正式环境打开方式</strong> | |
| <div class="control-options"> | |
| <label class="control-option"> | |
| <input type="radio" name="prod-open-mode" value="qr"> | |
| <span>扫码识别</span> | |
| <button id="qr-mode-guide-btn" class="inline-help-btn" type="button" aria-label="扫码识别指引">?</button> | |
| </label> | |
| <label class="control-option"> | |
| <input type="radio" name="prod-open-mode" value="direct" checked> | |
| <span>直接打开</span> | |
| </label> | |
| </div> | |
| <div id="prod-open-hint" class="control-hint">PC扫码方法:使用微信截图框选二维码→识别图中二维码</div> | |
| </div> | |
| </div> | |
| <div id="app-list" class="groups"></div> | |
| <div class="tips"> | |
| 提示:正式环境支持切换 URL Scheme 和 PC 快捷方式,并可选择直接打开或扫码识别。测试环境支持自定义地址;在手机微信浏览器中点击测试环境时,会弹出该目标地址的二维码。按 <code>/</code> 或 <code>Ctrl/⌘ + K</code> 可以直接聚焦搜索框。 | |
| </div> | |
| </section> | |
| </main> | |
| <div id="qr-modal" class="qr-modal" aria-hidden="true"> | |
| <div class="qr-dialog" role="dialog" aria-modal="true" aria-labelledby="qr-title"> | |
| <div class="qr-dialog-header"> | |
| <h3 id="qr-title">二维码</h3> | |
| <div class="qr-header-actions"> | |
| <button id="qr-help-btn" class="qr-help-btn" type="button" aria-label="使用指引" title="查看使用指引">?</button> | |
| <button id="qr-close" class="qr-close" type="button" aria-label="关闭">×</button> | |
| </div> | |
| </div> | |
| <p id="qr-tip">使用微信截图框选二维码 → 长按识别</p> | |
| <div id="qr-canvas-wrap" class="qr-canvas-wrap"></div> | |
| <a id="qr-url" class="qr-url" href="#" target="_blank" rel="noopener noreferrer"></a> | |
| </div> | |
| </div> | |
| <div id="guide-modal" class="guide-modal" aria-hidden="true"> | |
| <div class="guide-dialog" role="dialog" aria-modal="true" aria-labelledby="guide-title"> | |
| <h3 id="guide-title">微信截图识别指引</h3> | |
| <div class="guide-indicator"> | |
| <span id="guide-dot-1" class="guide-dot is-active"></span> | |
| <span id="guide-dot-2" class="guide-dot"></span> | |
| </div> | |
| <div id="phone-mock" class="phone-mock step-1"> | |
| <div class="mock-center-qr" aria-hidden="true"> | |
| <svg class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2012"><path d="M320 64 64 64 64 320 320 320 320 64ZM384 0 384 0 384 384 0 384 0 0 384 0ZM128 128 256 128 256 256 128 256zM960 64 704 64 704 320 960 320 960 64ZM1024 0 1024 0 1024 384 640 384 640 0 1024 0ZM768 128 896 128 896 256 768 256zM320 704 64 704 64 960 320 960 320 704ZM384 640 384 640 384 1024 0 1024 0 640 384 640ZM128 768 256 768 256 896 128 896zM448 0 512 0 512 64 448 64zM512 64 576 64 576 128 512 128zM448 128 512 128 512 192 448 192zM512 192 576 192 576 256 512 256zM448 256 512 256 512 320 448 320zM512 320 576 320 576 384 512 384zM448 384 512 384 512 448 448 448zM448 512 512 512 512 576 448 576zM512 576 576 576 576 640 512 640zM448 640 512 640 512 704 448 704zM512 704 576 704 576 768 512 768zM448 768 512 768 512 832 448 832zM512 832 576 832 576 896 512 896zM448 896 512 896 512 960 448 960zM512 960 576 960 576 1024 512 1024zM960 512 1024 512 1024 576 960 576zM64 512 128 512 128 576 64 576zM128 448 192 448 192 512 128 512zM0 448 64 448 64 512 0 512zM256 448 320 448 320 512 256 512zM320 512 384 512 384 576 320 576zM384 448 448 448 448 512 384 512zM576 512 640 512 640 576 576 576zM640 448 704 448 704 512 640 512zM704 512 768 512 768 576 704 576zM768 448 832 448 832 512 768 512zM832 512 896 512 896 576 832 576zM896 448 960 448 960 512 896 512zM960 640 1024 640 1024 704 960 704zM576 640 640 640 640 704 576 704zM640 576 704 576 704 640 640 640zM704 640 768 640 768 704 704 704zM832 640 896 640 896 704 832 704zM896 576 960 576 960 640 896 640zM960 768 1024 768 1024 832 960 832zM576 768 640 768 640 832 576 832zM640 704 704 704 704 768 640 768zM768 704 832 704 832 768 768 768zM832 768 896 768 896 832 832 832zM896 704 960 704 960 768 896 768zM960 896 1024 896 1024 960 960 960zM640 832 704 832 704 896 640 896zM704 896 768 896 768 960 704 960zM768 832 832 832 832 896 768 896zM832 896 896 896 896 960 832 960zM640 960 704 960 704 1024 640 1024zM768 960 832 960 832 1024 768 1024zM896 960 960 960 960 1024 896 1024z" fill="#000000" p-id="2013"></path></svg> | |
| </div> | |
| <div class="mock-crop"></div> | |
| <div class="mock-cursor" aria-hidden="true"></div> | |
| <div class="mock-toolbar"> | |
| <span class="mock-tool">□</span> | |
| <span class="mock-tool">✎</span> | |
| <span class="mock-tool qr">▦</span> | |
| <span class="mock-tool">T</span> | |
| </div> | |
| </div> | |
| <p id="guide-step-text" class="guide-step-text">步骤 1:使用微信截图快捷键(一般为 <span class="kbd-hot">Ctrl + Alt + A</span>)并框选二维码区域。</p> | |
| <button id="guide-confirm" class="guide-confirm" type="button">知道了</button> | |
| </div> | |
| </div> | |
| <script src="https://cdn.bootcdn.net/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"></script> | |
| <script> | |
| const STORAGE_KEYS = { | |
| order: "mini-program-group-order", | |
| pinned: "mini-program-group-pinned", | |
| prodLinkType: "mini-program-prod-link-type", | |
| prodOpenMode: "mini-program-prod-open-mode" | |
| }; | |
| const DEFAULT_ICON_BASE_URL = "https://developers.weixin.qq.com/community/ngi/personal/suncode"; | |
| const DEFAULT_OFFICIAL_MINI_PROGRAM = { | |
| appid: "wxcff7381e631cf54e", | |
| name: "微信开发者助手" | |
| }; | |
| const miniProgramGroups = { | |
| "常用": { | |
| "wxcff7381e631cf54e": { name: "微信开发者助手", icon: "https://developers.weixin.qq.com/community/ngi/personal/suncode/wxcff7381e631cf54e",prodUrl:"weixin://dl/business/?appid=wxcff7381e631cf54e&path=pages/index/index" , testUrl: "https://open.weixin.qq.com/sns/getexpappinfo?appid=wxcff7381e631cf54e"}, | |
| }, | |
| "极简":{ | |
| "wxcff7381e631cf54e":{ | |
| name:"极简小程序", | |
| } | |
| } | |
| }; | |
| const list = document.getElementById("app-list"); | |
| const searchInput = document.getElementById("search-input"); | |
| const resultMeta = document.getElementById("result-meta"); | |
| const qrModal = document.getElementById("qr-modal"); | |
| const qrCanvasWrap = document.getElementById("qr-canvas-wrap"); | |
| const qrUrl = document.getElementById("qr-url"); | |
| const qrClose = document.getElementById("qr-close"); | |
| const qrTitle = document.getElementById("qr-title"); | |
| const qrTip = document.getElementById("qr-tip"); | |
| const heroQrBtn = document.getElementById("hero-qr-btn"); | |
| const qrHelpBtn = document.getElementById("qr-help-btn"); | |
| const qrModeGuideBtn = document.getElementById("qr-mode-guide-btn"); | |
| const prodLinkTypeInputs = document.querySelectorAll('input[name="prod-link-type"]'); | |
| const prodOpenModeInputs = document.querySelectorAll('input[name="prod-open-mode"]'); | |
| const prodLinkHint = document.getElementById("prod-link-hint"); | |
| const prodOpenHint = document.getElementById("prod-open-hint"); | |
| const guideModal = document.getElementById("guide-modal"); | |
| const guideConfirm = document.getElementById("guide-confirm"); | |
| const guideStepText = document.getElementById("guide-step-text"); | |
| const phoneMock = document.getElementById("phone-mock"); | |
| const guideDot1 = document.getElementById("guide-dot-1"); | |
| const guideDot2 = document.getElementById("guide-dot-2"); | |
| const GUIDE_SEEN_KEY = "mini-program-page-qr-guide-seen"; | |
| let guideTimer = 0; | |
| let pendingQrRequest = null; | |
| let lastQrRequest = null; | |
| function readStorageArray(key) { | |
| try { | |
| const value = JSON.parse(localStorage.getItem(key) || "[]"); | |
| return Array.isArray(value) ? value : []; | |
| } catch (error) { | |
| return []; | |
| } | |
| } | |
| function writeStorageArray(key, value) { | |
| localStorage.setItem(key, JSON.stringify(value)); | |
| } | |
| function readStorageValue(key, fallbackValue) { | |
| return localStorage.getItem(key) || fallbackValue; | |
| } | |
| function writeStorageValue(key, value) { | |
| localStorage.setItem(key, value); | |
| } | |
| function getGroupNames() { | |
| return Object.keys(miniProgramGroups); | |
| } | |
| function getPinnedGroups() { | |
| return readStorageArray(STORAGE_KEYS.pinned).filter((name) => getGroupNames().includes(name)); | |
| } | |
| function setPinnedGroups(groups) { | |
| writeStorageArray(STORAGE_KEYS.pinned, groups); | |
| } | |
| function getStoredOrder() { | |
| return readStorageArray(STORAGE_KEYS.order).filter((name) => getGroupNames().includes(name)); | |
| } | |
| function setStoredOrder(groups) { | |
| writeStorageArray(STORAGE_KEYS.order, groups); | |
| } | |
| function getProdLinkType() { | |
| const value = readStorageValue(STORAGE_KEYS.prodLinkType, "scheme"); | |
| return value === "pc-shortcut" ? value : "scheme"; | |
| } | |
| function setProdLinkType(value) { | |
| writeStorageValue(STORAGE_KEYS.prodLinkType, value); | |
| } | |
| function getProdOpenMode() { | |
| const value = readStorageValue(STORAGE_KEYS.prodOpenMode, "direct"); | |
| return value === "qr" ? value : "direct"; | |
| } | |
| function setProdOpenMode(value) { | |
| writeStorageValue(STORAGE_KEYS.prodOpenMode, value); | |
| } | |
| function syncRadioState() { | |
| const prodLinkType = getProdLinkType(); | |
| const prodOpenMode = getProdOpenMode(); | |
| prodLinkTypeInputs.forEach((input) => { | |
| input.checked = input.value === prodLinkType; | |
| }); | |
| prodOpenModeInputs.forEach((input) => { | |
| input.checked = input.value === prodOpenMode; | |
| }); | |
| prodLinkHint.classList.toggle("is-visible", prodLinkType === "pc-shortcut"); | |
| prodOpenHint.classList.toggle("is-visible", prodOpenMode === "qr"); | |
| } | |
| function getOrderedGroupNames() { | |
| const allGroupNames = getGroupNames(); | |
| const storedOrder = getStoredOrder(); | |
| const pinnedGroups = new Set(getPinnedGroups()); | |
| const remaining = allGroupNames.filter((name) => !storedOrder.includes(name)); | |
| const mergedOrder = [...storedOrder, ...remaining]; | |
| const pinned = mergedOrder.filter((name) => pinnedGroups.has(name)); | |
| const unpinned = mergedOrder.filter((name) => !pinnedGroups.has(name)); | |
| return [...pinned, ...unpinned]; | |
| } | |
| function moveGroup(groupName, direction) { | |
| const ordered = getOrderedGroupNames(); | |
| const currentIndex = ordered.indexOf(groupName); | |
| const targetIndex = currentIndex + direction; | |
| if (currentIndex === -1 || targetIndex < 0 || targetIndex >= ordered.length) { | |
| return; | |
| } | |
| [ordered[currentIndex], ordered[targetIndex]] = [ordered[targetIndex], ordered[currentIndex]]; | |
| setStoredOrder(ordered); | |
| } | |
| function togglePinnedGroup(groupName) { | |
| const pinnedGroups = getPinnedGroups(); | |
| const nextPinnedGroups = pinnedGroups.includes(groupName) | |
| ? pinnedGroups.filter((name) => name !== groupName) | |
| : [...pinnedGroups, groupName]; | |
| setPinnedGroups(nextPinnedGroups); | |
| } | |
| function createButton(text, href, className) { | |
| const link = document.createElement("a"); | |
| link.className = `jump-btn ${className}`; | |
| link.href = href; | |
| link.target = "_blank"; | |
| link.rel = "noopener noreferrer"; | |
| link.textContent = text; | |
| return link; | |
| } | |
| function isWeixinBrowser() { | |
| const ua = navigator.userAgent.toLowerCase(); | |
| return /micromessenger/.test(ua) ? true : false; | |
| } | |
| function isMobileUA() { | |
| return /Android|iPhone|iPad|iPod|Mobile/i.test(navigator.userAgent); | |
| } | |
| function getQrTipText() { | |
| return isMobileUA() ? "长按识别" : "使用微信截图框选二维码 → 长按识别"; | |
| } | |
| function syncMobileGuideEntryVisibility() { | |
| const isMobile = isMobileUA(); | |
| heroQrBtn.style.display = isMobile ? "none" : ""; | |
| qrModeGuideBtn.style.display = isMobile ? "none" : "inline-flex"; | |
| qrHelpBtn.style.display = isMobile ? "none" : ""; | |
| } | |
| function getDefaultIconUrl(appid) { | |
| return `${DEFAULT_ICON_BASE_URL}/${encodeURIComponent(appid)}`; | |
| } | |
| function createPlaceholder(text = "仅正式环境") { | |
| const placeholder = document.createElement("div"); | |
| placeholder.className = "jump-placeholder"; | |
| placeholder.textContent = text; | |
| return placeholder; | |
| } | |
| function createTestButton(text, href, hasConfiguredTestUrl) { | |
| const button = createButton(text, href, "test"); | |
| button.addEventListener("click", (event) => { | |
| if (!isWeixinBrowser()) { | |
| event.preventDefault(); | |
| openQrModal(href, "体验版地址二维码"); | |
| return; | |
| } | |
| const isMobile = /Android|iPhone|iPad|iPod|Mobile/i.test(navigator.userAgent); | |
| if (!isMobile) { | |
| return; | |
| } | |
| event.preventDefault(); | |
| openQrModal(href); | |
| }); | |
| return button; | |
| } | |
| function createProdButton(text, href, qrHref, qrMode = "qrcode") { | |
| const button = document.createElement("a"); | |
| button.className = "jump-btn prod"; | |
| button.href = href; | |
| button.textContent = text; | |
| button.addEventListener("click", (event) => { | |
| event.preventDefault(); | |
| if (getProdOpenMode() === "qr") { | |
| openQrModal(qrHref || href, "正式环境二维码", getQrTipText(), qrMode); | |
| return; | |
| } | |
| window.location.href = href; | |
| }); | |
| return button; | |
| } | |
| function renderQrCode(text) { | |
| if (typeof QRCode === "undefined") { | |
| throw new Error("二维码库加载失败"); | |
| } | |
| qrCanvasWrap.innerHTML = ""; | |
| const tempWrap = document.createElement("div"); | |
| tempWrap.style.display = "none"; | |
| qrCanvasWrap.appendChild(tempWrap); | |
| new QRCode(tempWrap, { | |
| text, | |
| width: 280, | |
| height: 280, | |
| colorDark: "#111111", | |
| colorLight: "#ffffff", | |
| correctLevel: QRCode.CorrectLevel.H | |
| }); | |
| const sourceCanvas = tempWrap.querySelector("canvas"); | |
| const sourceImage = tempWrap.querySelector("img"); | |
| const qrImage = document.createElement("img"); | |
| if (sourceCanvas) { | |
| qrImage.src = sourceCanvas.toDataURL("image/png"); | |
| } else if (sourceImage) { | |
| qrImage.src = sourceImage.src; | |
| } else { | |
| throw new Error("二维码生成失败"); | |
| } | |
| qrImage.alt = "测试环境二维码"; | |
| qrImage.style.maxWidth = "100%"; | |
| qrImage.style.height = "auto"; | |
| qrImage.style.borderRadius = "16px"; | |
| qrImage.style.background = "#fff"; | |
| qrImage.style.webkitUserSelect = "none"; | |
| qrImage.style.userSelect = "none"; | |
| qrCanvasWrap.innerHTML = ""; | |
| qrCanvasWrap.appendChild(qrImage); | |
| } | |
| function renderRemoteImage(url, alt = "图片") { | |
| qrCanvasWrap.innerHTML = ""; | |
| const image = document.createElement("img"); | |
| image.src = url; | |
| image.alt = alt; | |
| image.style.maxWidth = "100%"; | |
| image.style.height = "auto"; | |
| image.style.borderRadius = "16px"; | |
| image.style.background = "#fff"; | |
| image.style.display = "block"; | |
| image.referrerPolicy = "no-referrer"; | |
| image.addEventListener("error", () => { | |
| qrCanvasWrap.innerHTML = ""; | |
| const fallback = document.createElement("div"); | |
| fallback.className = "empty"; | |
| fallback.textContent = "图片加载失败,请检查网络后重试。"; | |
| qrCanvasWrap.appendChild(fallback); | |
| }, { once: true }); | |
| qrCanvasWrap.appendChild(image); | |
| } | |
| function openQrModal(url, title = "二维码", tip = getQrTipText(), mode = "qrcode") { | |
| lastQrRequest = { url, title, tip, mode }; | |
| try { | |
| if (mode === "image") { | |
| renderRemoteImage(url, title); | |
| } else { | |
| renderQrCode(url); | |
| } | |
| } catch (error) { | |
| qrCanvasWrap.innerHTML = ""; | |
| const fallback = document.createElement("div"); | |
| fallback.className = "empty"; | |
| fallback.textContent = mode === "image" | |
| ? "图片加载失败,请检查网络后重试。" | |
| : "二维码库加载失败或生成异常,请检查网络后重试。"; | |
| qrCanvasWrap.appendChild(fallback); | |
| } | |
| qrTitle.textContent = title; | |
| qrTip.textContent = tip; | |
| qrUrl.textContent = mode === "image" ? "打开图片原地址" : url; | |
| qrUrl.href = url; | |
| qrModal.classList.add("is-open"); | |
| qrModal.setAttribute("aria-hidden", "false"); | |
| } | |
| function closeQrModal() { | |
| qrModal.classList.remove("is-open"); | |
| qrModal.setAttribute("aria-hidden", "true"); | |
| } | |
| function createInfo(name, appid, icon) { | |
| const info = document.createElement("div"); | |
| info.className = "appid"; | |
| const avatar = document.createElement("img"); | |
| avatar.className = "appid-avatar"; | |
| avatar.src = icon || getDefaultIconUrl(appid); | |
| avatar.alt = name; | |
| avatar.loading = "lazy"; | |
| avatar.referrerPolicy = "no-referrer"; | |
| avatar.addEventListener("error", () => { | |
| avatar.src = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='96' height='96' viewBox='0 0 96 96'%3E%3Crect width='96' height='96' rx='18' fill='%23dceef7'/%3E%3Ctext x='48' y='57' text-anchor='middle' font-size='34' fill='%23285f78' font-family='Microsoft YaHei, Arial'%3EM%3C/text%3E%3C/svg%3E"; | |
| }, { once: true }); | |
| const text = document.createElement("div"); | |
| text.className = "appid-text"; | |
| text.innerHTML = `<strong title="${name}">${name}</strong><span title="${appid}">${appid}</span>`; | |
| info.appendChild(avatar); | |
| info.appendChild(text); | |
| return info; | |
| } | |
| function createRow(name, appid, icon, prodUrlOverride, testUrlOverride, options = {}) { | |
| const row = document.createElement("div"); | |
| row.className = "row"; | |
| const hasConfiguredTestUrl = typeof testUrlOverride === "string" && testUrlOverride.trim().length > 0; | |
| const testUrl = testUrlOverride || `https://open.weixin.qq.com/sns/getexpappinfo?appid=${encodeURIComponent(appid)}&path=pages/index/index.html#wechat-redirect`; | |
| const prodUrl = prodUrlOverride || `weixin://dl/business/?appid=${encodeURIComponent(appid)}&path=pages/index/index`; | |
| const prodPcShortcutUrl = `weixin://launchapplet?app_id=${encodeURIComponent(appid)}`; | |
| const finalProdUrl = getProdLinkType() === "pc-shortcut" ? prodPcShortcutUrl : prodUrl; | |
| const prodQrUrl = prodUrlOverride ? prodUrl : getDefaultIconUrl(appid); | |
| const prodQrMode = prodUrlOverride ? "qrcode" : "image"; | |
| row.appendChild(createInfo(name, appid, icon)); | |
| if (options.prodOnly) { | |
| row.appendChild(createPlaceholder()); | |
| } else { | |
| row.appendChild(createTestButton(`${name} - 测试`, testUrl, hasConfiguredTestUrl)); | |
| } | |
| row.appendChild(createProdButton(`${name} - 正式`, finalProdUrl, prodQrUrl, prodQrMode)); | |
| row.querySelectorAll(".jump-btn").forEach((button) => { | |
| button.title = button.textContent; | |
| }); | |
| return row; | |
| } | |
| function normalizeGroups() { | |
| const pinnedGroups = new Set(getPinnedGroups()); | |
| return getOrderedGroupNames().map((groupName) => ({ | |
| groupName, | |
| isPinned: pinnedGroups.has(groupName), | |
| entries: Object.entries(miniProgramGroups[groupName]).map(([appid, config]) => ({ | |
| appid, | |
| name: config.name, | |
| icon: config.icon || getDefaultIconUrl(appid), | |
| testUrl: config.testUrl || "", | |
| prodUrl: config.prodUrl || "" | |
| })) | |
| })); | |
| } | |
| function getOfficialMiniProgram() { | |
| return { | |
| ...DEFAULT_OFFICIAL_MINI_PROGRAM, | |
| icon: getDefaultIconUrl(DEFAULT_OFFICIAL_MINI_PROGRAM.appid), | |
| prodUrl: "" | |
| }; | |
| } | |
| function matchesOfficialMiniProgram(keyword) { | |
| if (!keyword) { | |
| return true; | |
| } | |
| const official = getOfficialMiniProgram(); | |
| return matchesKeyword(official.name, keyword) || matchesKeyword(official.appid, keyword); | |
| } | |
| function matchesKeyword(text, keyword) { | |
| return text.toLowerCase().includes(keyword); | |
| } | |
| function getFilteredGroups(keyword) { | |
| const normalizedGroups = normalizeGroups(); | |
| if (!keyword) { | |
| return normalizedGroups; | |
| } | |
| return normalizedGroups | |
| .map((group) => { | |
| const groupMatched = matchesKeyword(group.groupName, keyword); | |
| const entries = groupMatched | |
| ? group.entries | |
| : group.entries.filter(({ appid, name }) => | |
| matchesKeyword(name, keyword) || matchesKeyword(appid, keyword) | |
| ); | |
| return { ...group, entries }; | |
| }) | |
| .filter((group) => group.entries.length > 0); | |
| } | |
| function updateMeta(groupCount, itemCount, keyword) { | |
| if (!itemCount) { | |
| resultMeta.textContent = keyword | |
| ? `没有找到“${keyword}”相关结果` | |
| : "当前没有可展示的小程序"; | |
| return; | |
| } | |
| resultMeta.textContent = keyword | |
| ? `找到 ${groupCount} 个分组,共 ${itemCount} 个结果` | |
| : `共 ${groupCount} 个分组,${itemCount} 个小程序`; | |
| } | |
| function renderGroups(keyword = "") { | |
| const normalizedKeyword = keyword.trim().toLowerCase(); | |
| const filteredGroups = getFilteredGroups(normalizedKeyword); | |
| const orderedNames = getOrderedGroupNames(); | |
| const officialMiniProgram = getOfficialMiniProgram(); | |
| const showOfficialMiniProgram = matchesOfficialMiniProgram(normalizedKeyword); | |
| list.innerHTML = ""; | |
| if (!filteredGroups.length && !showOfficialMiniProgram) { | |
| const empty = document.createElement("div"); | |
| empty.className = "empty"; | |
| empty.textContent = normalizedKeyword | |
| ? `没有搜索到与“${keyword}”相关的分组或小程序。` | |
| : "当前没有可展示的小程序,请先在页面脚本中填写 miniProgramGroups 分组字典。"; | |
| list.appendChild(empty); | |
| updateMeta(0, 0, keyword); | |
| return; | |
| } | |
| let itemCount = 0; | |
| if (showOfficialMiniProgram) { | |
| const officialCard = document.createElement("div"); | |
| officialCard.className = "group"; | |
| const officialBody = document.createElement("div"); | |
| officialBody.className = "group-body"; | |
| officialBody.style.paddingTop = "18px"; | |
| const header = document.createElement("div"); | |
| header.className = "board-header"; | |
| header.innerHTML = "<div>小程序名称 / AppID</div><div>测试环境</div><div>正式环境</div>"; | |
| officialBody.appendChild(header); | |
| officialBody.appendChild( | |
| createRow( | |
| officialMiniProgram.name, | |
| officialMiniProgram.appid, | |
| officialMiniProgram.icon, | |
| officialMiniProgram.prodUrl, | |
| "", | |
| { prodOnly: true } | |
| ) | |
| ); | |
| officialCard.appendChild(officialBody); | |
| list.appendChild(officialCard); | |
| itemCount += 1; | |
| } | |
| filteredGroups.forEach((group) => { | |
| const details = document.createElement("details"); | |
| details.className = "group"; | |
| details.open = true; | |
| const summary = document.createElement("summary"); | |
| summary.className = "group-summary"; | |
| const title = document.createElement("div"); | |
| title.className = "group-title"; | |
| title.innerHTML = `<strong title="${group.groupName}">${group.groupName}</strong><span>支持搜索组名、名称、AppID</span>`; | |
| const meta = document.createElement("div"); | |
| meta.className = "group-meta"; | |
| const badge = document.createElement("div"); | |
| badge.className = "group-badge"; | |
| badge.textContent = group.isPinned ? `已置顶 · ${group.entries.length} 个小程序` : `${group.entries.length} 个小程序`; | |
| const actions = document.createElement("div"); | |
| actions.className = "group-actions"; | |
| const pinButton = document.createElement("button"); | |
| pinButton.type = "button"; | |
| pinButton.className = `group-action${group.isPinned ? " is-active" : ""}`; | |
| pinButton.textContent = group.isPinned ? "取消置顶" : "置顶"; | |
| pinButton.addEventListener("click", (event) => { | |
| event.preventDefault(); | |
| event.stopPropagation(); | |
| togglePinnedGroup(group.groupName); | |
| renderGroups(searchInput.value); | |
| }); | |
| const currentIndex = orderedNames.indexOf(group.groupName); | |
| const upButton = document.createElement("button"); | |
| upButton.type = "button"; | |
| upButton.className = "group-action"; | |
| upButton.textContent = "上移"; | |
| upButton.disabled = currentIndex <= 0; | |
| upButton.addEventListener("click", (event) => { | |
| event.preventDefault(); | |
| event.stopPropagation(); | |
| moveGroup(group.groupName, -1); | |
| renderGroups(searchInput.value); | |
| }); | |
| const downButton = document.createElement("button"); | |
| downButton.type = "button"; | |
| downButton.className = "group-action"; | |
| downButton.textContent = "下移"; | |
| downButton.disabled = currentIndex === -1 || currentIndex >= orderedNames.length - 1; | |
| downButton.addEventListener("click", (event) => { | |
| event.preventDefault(); | |
| event.stopPropagation(); | |
| moveGroup(group.groupName, 1); | |
| renderGroups(searchInput.value); | |
| }); | |
| actions.appendChild(pinButton); | |
| actions.appendChild(upButton); | |
| actions.appendChild(downButton); | |
| meta.appendChild(badge); | |
| meta.appendChild(actions); | |
| summary.appendChild(title); | |
| summary.appendChild(meta); | |
| const body = document.createElement("div"); | |
| body.className = "group-body"; | |
| const header = document.createElement("div"); | |
| header.className = "board-header"; | |
| header.innerHTML = "<div>小程序名称 / AppID</div><div>测试环境</div><div>正式环境</div>"; | |
| body.appendChild(header); | |
| const rows = document.createElement("div"); | |
| rows.className = "rows"; | |
| group.entries.forEach(({ name, appid, icon, prodUrl, testUrl }) => { | |
| rows.appendChild(createRow(name, appid, icon, prodUrl, testUrl)); | |
| itemCount += 1; | |
| }); | |
| body.appendChild(rows); | |
| details.appendChild(summary); | |
| details.appendChild(body); | |
| list.appendChild(details); | |
| }); | |
| updateMeta(filteredGroups.length + (showOfficialMiniProgram ? 1 : 0), itemCount, keyword); | |
| } | |
| function focusSearch() { | |
| searchInput.focus(); | |
| searchInput.select(); | |
| } | |
| function bindSearchShortcut() { | |
| document.addEventListener("keydown", (event) => { | |
| const isShortcut = (event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "k"; | |
| const isSlash = event.key === "/" && document.activeElement !== searchInput; | |
| if (!isShortcut && !isSlash) { | |
| return; | |
| } | |
| event.preventDefault(); | |
| focusSearch(); | |
| }); | |
| } | |
| searchInput.addEventListener("input", () => { | |
| renderGroups(searchInput.value); | |
| }); | |
| prodLinkTypeInputs.forEach((input) => { | |
| input.addEventListener("change", () => { | |
| setProdLinkType(input.value); | |
| syncRadioState(); | |
| renderGroups(searchInput.value); | |
| }); | |
| }); | |
| prodOpenModeInputs.forEach((input) => { | |
| input.addEventListener("change", () => { | |
| setProdOpenMode(input.value); | |
| syncRadioState(); | |
| }); | |
| }); | |
| function markGuideStep(step) { | |
| const step1 = step === 1; | |
| phoneMock.classList.toggle("step-1", step1); | |
| phoneMock.classList.toggle("step-2", !step1); | |
| guideDot1.classList.toggle("is-active", step1); | |
| guideDot2.classList.toggle("is-active", !step1); | |
| guideStepText.innerHTML = step1 | |
| ? "步骤 1:使用微信截图快捷键(一般为 <span class=\"kbd-hot\">Ctrl + Alt + A</span>)并框选二维码区域。" | |
| : "步骤 2:点击截图工具栏中的二维码按钮。"; | |
| } | |
| function closeGuideModal() { | |
| window.clearInterval(guideTimer); | |
| guideTimer = 0; | |
| guideModal.classList.remove("is-open"); | |
| guideModal.setAttribute("aria-hidden", "true"); | |
| } | |
| function openGuideModal(qrRequest) { | |
| pendingQrRequest = qrRequest; | |
| markGuideStep(1); | |
| closeQrModal(); | |
| guideModal.classList.add("is-open"); | |
| guideModal.setAttribute("aria-hidden", "false"); | |
| window.clearInterval(guideTimer); | |
| guideTimer = window.setInterval(() => { | |
| const isStep1 = phoneMock.classList.contains("step-1"); | |
| markGuideStep(isStep1 ? 2 : 1); | |
| }, 2400); | |
| } | |
| function requestPageQr(showGuide = false) { | |
| const pageRequest = { | |
| url: window.location.href, | |
| title: "在微信中查看", | |
| tip: getQrTipText() | |
| }; | |
| if (!isMobileUA() && (showGuide || localStorage.getItem(GUIDE_SEEN_KEY) !== "1")) { | |
| openGuideModal(pageRequest); | |
| return; | |
| } | |
| lastQrRequest = pageRequest; | |
| openQrModal(pageRequest.url, pageRequest.title, pageRequest.tip); | |
| } | |
| heroQrBtn.addEventListener("click", () => requestPageQr(false)); | |
| qrModeGuideBtn.addEventListener("click", (event) => { | |
| event.preventDefault(); | |
| event.stopPropagation(); | |
| requestPageQr(true); | |
| }); | |
| qrHelpBtn.addEventListener("click", () => { | |
| if (isMobileUA()) { | |
| return; | |
| } | |
| openGuideModal(lastQrRequest || { | |
| url: window.location.href, | |
| title: "当前页面链接二维码", | |
| tip: getQrTipText() | |
| }); | |
| }); | |
| guideConfirm.addEventListener("click", () => { | |
| localStorage.setItem(GUIDE_SEEN_KEY, "1"); | |
| closeGuideModal(); | |
| if (pendingQrRequest) { | |
| lastQrRequest = pendingQrRequest; | |
| openQrModal(pendingQrRequest.url, pendingQrRequest.title, pendingQrRequest.tip); | |
| } | |
| }); | |
| guideModal.addEventListener("click", (event) => { | |
| if (event.target === guideModal) { | |
| closeGuideModal(); | |
| } | |
| }); | |
| qrClose.addEventListener("click", closeQrModal); | |
| qrModal.addEventListener("click", (event) => { | |
| if (event.target === qrModal) { | |
| closeQrModal(); | |
| } | |
| }); | |
| document.addEventListener("keydown", (event) => { | |
| if (event.key === "Escape" && qrModal.classList.contains("is-open")) { | |
| closeQrModal(); | |
| } | |
| if (event.key === "Escape" && guideModal.classList.contains("is-open")) { | |
| closeGuideModal(); | |
| } | |
| }); | |
| syncMobileGuideEntryVisibility(); | |
| syncRadioState(); | |
| bindSearchShortcut(); | |
| renderGroups(); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment