460 lines
13 KiB
HTML
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>
|