Files
2026-11-07 10:43:06 +08:00

460 lines
13 KiB
HTML

<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<meta name="robots" content="noindex, nofollow" />
<meta name="theme-color" content="#F5F8EF" />
<meta name="format-detection" content="telephone=no,email=no,address=no" />
<title>邀请好友|来秒AI</title>
<link
rel="icon"
href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='9' fill='%2308482D'/%3E%3Cpath d='M18.6 3 7.5 18h6.7l-1.7 11L24.5 13h-6.6l.7-10Z' fill='%23DAE7C7'/%3E%3C/svg%3E"
/>
<link rel="preconnect" href="https://fonts.googleapis.cn" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.cn/css2?family=Inter:wght@400;500;600;700;800;900&family=Noto+Sans+SC:wght@400;500;700;900&display=swap"
rel="stylesheet"
/>
<script>
window.__TEAM_API_BASE__ = "%VITE_TEAM_API_URL%";
</script>
<style>
* {
box-sizing: border-box;
}
html,
body {
min-height: 100%;
}
body {
margin: 0;
background: #f5f8ef;
color: #101410;
font-family: 'Inter', 'Noto Sans SC', -apple-system, BlinkMacSystemFont, 'PingFang SC',
'Microsoft YaHei', sans-serif;
-webkit-font-smoothing: antialiased;
}
.page {
min-height: 100svh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 28px;
padding: 32px 20px 40px;
}
.brand {
display: inline-flex;
align-items: center;
gap: 8px;
color: #101410;
font-size: 20px;
font-weight: 700;
letter-spacing: -0.02em;
text-decoration: none;
}
.brand svg {
width: 24px;
height: 24px;
display: block;
}
.card {
width: 100%;
max-width: 440px;
padding: 36px 28px 28px;
border-radius: 28px;
background: #fff;
border: 1px solid rgba(8, 72, 45, 0.08);
box-shadow: 0 18px 48px -32px rgba(8, 72, 45, 0.45);
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
}
#content {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
}
.avatar {
width: 76px;
height: 76px;
border-radius: 999px;
overflow: hidden;
background: #eef2e6;
display: flex;
align-items: center;
justify-content: center;
color: #08482d;
font-size: 28px;
font-weight: 700;
margin: 0 auto 18px;
flex: none;
}
.avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
.name {
margin: 0;
font-size: 18px;
font-weight: 700;
}
.desc {
margin: 8px 0 22px;
color: #6b7268;
font-size: 14px;
line-height: 1.7;
}
.reward {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
padding: 10px 14px;
border-radius: 14px;
background: #fffbf6;
border: 1px solid #ffedd5;
color: #c44714;
font-size: 13px;
margin-bottom: 24px;
}
.reward strong {
font-weight: 700;
}
.actions {
width: 100%;
display: flex;
flex-direction: column;
gap: 10px;
}
.btn {
width: 100%;
height: 48px;
border-radius: 14px;
border: 1px solid rgba(8, 72, 45, 0.16);
background: #fff;
color: #08482d;
font-family: inherit;
font-size: 15px;
font-weight: 600;
cursor: pointer;
transition: background 0.2s ease, border-color 0.2s ease, transform 0.2s ease;
}
.btn:hover {
border-color: rgba(8, 72, 45, 0.4);
transform: translateY(-1px);
}
.btn--primary {
background: #08482d;
border-color: #08482d;
color: #fff;
}
.btn--primary:hover {
background: #063a24;
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
.btn:focus-visible {
outline: 2px solid #08482d;
outline-offset: 2px;
}
.state {
padding: 18px 6px 4px;
}
.state h2 {
margin: 0 0 8px;
font-size: 18px;
font-weight: 700;
}
.state p {
margin: 0;
color: #6b7268;
font-size: 13px;
line-height: 1.7;
}
.spinner {
width: 34px;
height: 34px;
margin: 24px auto;
border-radius: 999px;
border: 3px solid #dae7c7;
border-top-color: #08482d;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.toast {
position: fixed;
left: 50%;
bottom: 36px;
transform: translateX(-50%) translateY(10px);
padding: 10px 18px;
border-radius: 999px;
background: rgba(16, 20, 16, 0.88);
color: #fff;
font-size: 13px;
opacity: 0;
pointer-events: none;
transition: opacity 0.2s ease, transform 0.2s ease;
z-index: 50;
}
.toast.is-show {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
[hidden] {
display: none !important;
}
</style>
</head>
<body>
<main class="page">
<a class="brand" href="/">
<svg viewBox="0 0 32 32" fill="none" aria-hidden="true">
<path d="M19 2 5 19h8l-2 11L27 12h-9l1-10Z" fill="#08482D" />
<path d="M19 2 11 12" fill="none" stroke="#DAE7C7" stroke-width="1.4" stroke-linecap="round" />
</svg>
<span>来秒AI</span>
</a>
<section class="card">
<div id="loading" class="spinner"></div>
<div id="invalid" class="state" hidden>
<h2>邀请链接无效</h2>
<p id="invalidText">邀请码不存在或已失效</p>
<div class="actions" style="margin-top: 20px">
<a class="btn btn--primary" href="/" style="display: flex; align-items: center; justify-content: center; text-decoration: none">返回首页</a>
</div>
</div>
<div id="content" hidden>
<div class="avatar" id="avatar"></div>
<h1 class="name" id="nickname">好友</h1>
<p class="desc" id="desc">邀请你一起用 来秒AI 把灵感变成作品</p>
<div class="reward">
<span>加入即可获得</span>
<strong>200</strong>
<span>算力积分奖励</span>
</div>
<div class="actions">
<button class="btn btn--primary" id="acceptBtn" type="button" hidden>开始使用来秒AI</button>
<button class="btn btn--primary" id="loginBtn" type="button" hidden>微信一键登录</button>
<a class="btn" href="/" style="display: flex; align-items: center; justify-content: center; text-decoration: none">了解来秒AI</a>
</div>
</div>
</section>
</main>
<div class="toast" id="toast"></div>
<script>
(function () {
"use strict";
const API = (function () {
const base = String(window.__TEAM_API_BASE__ || "");
return base && base.charAt(0) !== "%" ? base.replace(/\/+$/, "") : "";
})();
const PENDING_KEY = "invite_pending_code";
const loadingEl = document.getElementById("loading");
const invalidEl = document.getElementById("invalid");
const invalidTextEl = document.getElementById("invalidText");
const contentEl = document.getElementById("content");
const avatarEl = document.getElementById("avatar");
const nicknameEl = document.getElementById("nickname");
const descEl = document.getElementById("desc");
const acceptBtn = document.getElementById("acceptBtn");
const loginBtn = document.getElementById("loginBtn");
const toastEl = document.getElementById("toast");
let toastTimer = 0;
function showToast(message) {
toastEl.textContent = message;
toastEl.classList.add("is-show");
window.clearTimeout(toastTimer);
toastTimer = window.setTimeout(function () {
toastEl.classList.remove("is-show");
}, 2600);
}
function readToken() {
return localStorage.getItem("access_token") || "";
}
function inviteCode() {
const fromQuery = new URLSearchParams(window.location.search).get("code");
if (fromQuery) return fromQuery.trim();
const match = window.location.pathname.match(/\/invite\/([A-Za-z0-9]+)/);
return match ? match[1] : "";
}
function currentInviteUrl(code) {
return window.location.origin + "/invite/?code=" + encodeURIComponent(code);
}
async function parseJson(response) {
const text = await response.text();
try {
return JSON.parse(text.replace(/([\[:,]\s*)(\d{16,})(\s*[,\]}])/g, '$1"$2"$3'));
} catch (error) {
return {};
}
}
function showInvalid(message) {
loadingEl.hidden = true;
contentEl.hidden = true;
invalidEl.hidden = false;
if (message) invalidTextEl.textContent = message;
}
function renderUser(info) {
const nickname = info.nickname || "好友";
nicknameEl.textContent = nickname;
descEl.textContent = "邀请你一起用 来秒AI 把灵感快速变成可发布的图片";
avatarEl.textContent = "";
if (info.avatar) {
const img = document.createElement("img");
img.src = info.avatar;
img.alt = nickname;
img.referrerPolicy = "no-referrer";
avatarEl.appendChild(img);
} else {
avatarEl.textContent = nickname.slice(0, 1);
}
}
async function acceptInvite(code) {
const token = readToken();
if (!token) return false;
acceptBtn.disabled = true;
try {
const response = await fetch(API + "/api/user/invitation/invite", {
method: "POST",
headers: { "Content-Type": "application/json", Authorization: token },
body: JSON.stringify({ code: code }),
});
const data = await parseJson(response);
if (data.ret === 101010) {
localStorage.removeItem("access_token");
return false;
}
if (data.ret !== undefined && data.ret !== 0) {
const message = String(data.msg || "邀请失败,请重试");
showToast(message.includes("重复绑定") ? "你已绑定过邀请人,直接开始创作吧" : message);
} else {
showToast(data.msg || "邀请成功,奖励已发放");
}
sessionStorage.removeItem(PENDING_KEY);
window.location.href = "/generate";
return true;
} catch (error) {
showToast("邀请失败,请重试");
return false;
} finally {
acceptBtn.disabled = false;
}
}
function goLogin(code) {
sessionStorage.setItem(PENDING_KEY, code);
const target = currentInviteUrl(code);
window.location.href = "/login/?redirect=" + encodeURIComponent(target);
}
async function init() {
const code = inviteCode();
if (!code) {
showInvalid("缺少邀请码,请确认链接是否完整");
return;
}
try {
const response = await fetch(API + "/api/user/invite/role?code=" + encodeURIComponent(code));
const data = await parseJson(response);
if (data.ret !== undefined && data.ret !== 0) {
showInvalid(data.msg || "邀请码不存在或已失效");
return;
}
if (data.role === "team") {
showInvalid("该邀请链接不是好友邀请");
return;
}
loadingEl.hidden = true;
contentEl.hidden = false;
renderUser({ nickname: data.nickname, avatar: data.avatar });
const loggedIn = !!readToken();
if (loggedIn) {
acceptBtn.hidden = false;
acceptBtn.addEventListener("click", function () {
void acceptInvite(code);
});
if (sessionStorage.getItem(PENDING_KEY) === code) {
void acceptInvite(code);
}
} else {
loginBtn.hidden = false;
loginBtn.addEventListener("click", function () {
goLogin(code);
});
}
} catch (error) {
showInvalid("无法获取邀请信息,请稍后重试");
}
}
void init();
})();
</script>
</body>
</html>