完善初始化更新

This commit is contained in:
2026-03-20 20:42:33 +08:00
parent 568ccb08fa
commit e6866feb29
39 changed files with 6986 additions and 2379 deletions

View File

@@ -0,0 +1,187 @@
import { marked } from "marked";
const DEFAULT_DEV_API_BASE = "http://localhost:8080";
const DEFAULT_PROD_API_BASE = "https://auth.api.shumengya.top";
export const API_BASE = (() => {
const configuredBase = import.meta.env.VITE_API_BASE?.trim();
const fallbackBase = import.meta.env.DEV ? DEFAULT_DEV_API_BASE : DEFAULT_PROD_API_BASE;
return (configuredBase || fallbackBase).replace(/\/+$/, "");
})();
/** `public/logo192.png`,含 Vite `base` 前缀,避免子路径部署时顶栏/开屏裂图 */
export const LOGO_192_SRC = `${import.meta.env.BASE_URL}logo192.png`;
/** 浏览器侧 IP/地理(与后端写入「最后访问」字段配合使用) */
export const CLIENT_GEO_LOOKUP_URL = "https://cf-ip-geo.smyhub.com/api";
export function formatVisitDisplayLocation(payload) {
const g = payload?.geo;
if (!g || typeof g !== "object") return "";
const parts = [g.countryName, g.regionName, g.cityName].filter(
(x) => typeof x === "string" && x.trim()
);
return parts.join(" ");
}
export async function fetchClientVisitMeta(timeoutMs = 2800) {
const ctrl = new AbortController();
const id = setTimeout(() => ctrl.abort(), timeoutMs);
try {
const res = await fetch(CLIENT_GEO_LOOKUP_URL, {
credentials: "omit",
signal: ctrl.signal
});
clearTimeout(id);
if (!res.ok) return { ip: "", displayLocation: "" };
const data = await res.json();
const ip = typeof data.ip === "string" ? data.ip.trim() : "";
const displayLocation = formatVisitDisplayLocation(data);
return { ip, displayLocation };
} catch {
clearTimeout(id);
return { ip: "", displayLocation: "" };
}
}
marked.setOptions({ breaks: true });
export { marked };
export const emptyForm = {
account: "",
password: "",
username: "",
email: "",
level: 0,
sproutCoins: 0,
secondaryEmails: "",
phone: "",
avatarUrl: "",
websiteUrl: "",
bio: "",
banned: false,
banReason: ""
};
/** 后端封禁响应:`error` + 可选 `banReason`(登录/me 等 403 */
export function formatAuthBanMessage(data) {
const base = (data && data.error && String(data.error).trim()) || "account is banned";
const reason = data && data.banReason != null && String(data.banReason).trim();
return reason ? `${base}${reason}` : base;
}
/** 展示用:从完整 URL 得到「主机 + 路径」短文案 */
export function formatWebsiteLabel(url) {
if (!url || typeof url !== "string") return "";
try {
const u = new URL(url);
const path = u.pathname === "/" ? "" : u.pathname;
return u.host + path;
} catch {
return url;
}
}
/** 用户 `createdAt`RFC3339→ 本地可读注册时间 */
export function formatUserRegisteredAt(iso) {
if (!iso || typeof iso !== "string") return "未知";
const d = new Date(iso);
if (Number.isNaN(d.getTime())) return iso;
return new Intl.DateTimeFormat("zh-CN", {
dateStyle: "long",
timeStyle: "short"
}).format(d);
}
/** RFC3339 / 后端存储时间 → 本地可读(用于应用接入记录等) */
export function formatIsoDateTimeReadable(iso) {
if (!iso || typeof iso !== "string") return "—";
const d = new Date(iso);
if (Number.isNaN(d.getTime())) return iso;
return new Intl.DateTimeFormat("zh-CN", {
dateStyle: "medium",
timeStyle: "medium"
}).format(d);
}
export const parseEmailList = (value) =>
value.split(",").map((item) => item.trim()).filter(Boolean);
/** 可点击发信:`mailto:`,排除明显非法地址 */
export function mailtoHref(address) {
const a = typeof address === "string" ? address.trim() : "";
if (!a || /\s/.test(a) || a.includes("<") || a.includes(">")) return null;
const at = a.indexOf("@");
if (at < 1 || at === a.length - 1) return null;
return `mailto:${a}`;
}
export function getPublicAccountFromPathname(pathname) {
if (pathname !== "/user" && !pathname.startsWith("/user/")) return "";
const raw = pathname === "/user" ? "" : pathname.slice("/user/".length).split("/")[0];
if (!raw) return "";
try {
return decodeURIComponent(raw).trim();
} catch {
return raw.trim();
}
}
export function getAuthFlowFromSearch(search) {
const params = new URLSearchParams(search);
const redirectUri = (params.get("redirect_uri") || params.get("return_url") || "").trim();
return {
redirectUri,
state: (params.get("state") || "").trim(),
prompt: (params.get("prompt") || "").trim(),
clientId: (params.get("client_id") || "").trim(),
clientName: (params.get("client_name") || "").trim()
};
}
const AUTH_CLIENT_ID_KEY = "sproutgate_auth_client_id";
const AUTH_CLIENT_NAME_KEY = "sproutgate_auth_client_name";
/** 从统一登录 URL 上的 `client_id` / `client_name` 写入 session后续请求带 `X-Auth-Client` 头以便记录接入应用 */
export function persistAuthClientFromFlow(authFlow) {
if (!authFlow?.clientId) return;
try {
sessionStorage.setItem(AUTH_CLIENT_ID_KEY, authFlow.clientId);
if (authFlow.clientName) sessionStorage.setItem(AUTH_CLIENT_NAME_KEY, authFlow.clientName);
} catch {
/* ignore */
}
}
export function authClientFetchHeaders() {
try {
const id = (sessionStorage.getItem(AUTH_CLIENT_ID_KEY) || "").trim();
const name = (sessionStorage.getItem(AUTH_CLIENT_NAME_KEY) || "").trim();
const h = {};
if (id) h["X-Auth-Client"] = id;
if (name) h["X-Auth-Client-Name"] = name;
return h;
} catch {
return {};
}
}
export function clearAuthClientContext() {
try {
sessionStorage.removeItem(AUTH_CLIENT_ID_KEY);
sessionStorage.removeItem(AUTH_CLIENT_NAME_KEY);
} catch {
/* ignore */
}
}
export function buildAuthCallbackUrl(redirectUri, payload) {
const url = new URL(redirectUri, window.location.href);
const hashParams = new URLSearchParams(url.hash ? url.hash.slice(1) : "");
Object.entries(payload).forEach(([key, value]) => {
if (value === undefined || value === null || value === "") return;
hashParams.set(key, String(value));
});
url.hash = hashParams.toString();
return url.toString();
}