feat(auth): 用户账户 - 完善个人资料与登录日志 (#752)
Co-authored-by: wuligao <1247662379@qq.com>
This commit is contained in:
@@ -1,8 +1,17 @@
|
||||
import type { ImageCropRect } from "./image-operation-plan";
|
||||
import { assertPixelSize, type ImageCropRect, type PixelSize } from "./image-operation-plan";
|
||||
export type CropGrip = readonly [-1 | 0 | 1, -1 | 0 | 1];
|
||||
const limit = (n: number, a: number, b: number) => Math.max(a, Math.min(b, n));
|
||||
const minimum = 0.06;
|
||||
|
||||
export function centeredAspectSelection(size: PixelSize, aspect: number): ImageCropRect {
|
||||
assertPixelSize(size);
|
||||
if (!Number.isFinite(aspect) || aspect <= 0) throw new RangeError("裁剪比例无效");
|
||||
const ratio = (aspect * size.height) / size.width;
|
||||
const height = Math.min(1, 1 / ratio);
|
||||
const width = height * ratio;
|
||||
return { x: (1 - width) / 2, y: (1 - height) / 2, width, height };
|
||||
}
|
||||
|
||||
export function translateSelection(box: ImageCropRect, dx: number, dy: number): ImageCropRect {
|
||||
return { width: box.width, height: box.height, x: limit(box.x + dx, 0, 1 - box.width), y: limit(box.y + dy, 0, 1 - box.height) };
|
||||
}
|
||||
@@ -29,7 +38,8 @@ export function resizeSelection(box: ImageCropRect, grip: CropGrip, dx: number,
|
||||
const proposed = 1 + (dx * vx + dy * vy) / (vx * vx + vy * vy);
|
||||
const capacity = (anchor: number, fraction: number) => Math.min(fraction ? anchor / fraction : Infinity, fraction < 1 ? (1 - anchor) / (1 - fraction) : Infinity);
|
||||
const ceiling = Math.min(capacity(ax, fx) / box.width, capacity(ay, fy) / box.height);
|
||||
const floor = Math.min(ceiling, Math.max(minimum / box.width, minimum / box.height));
|
||||
// 以较长的归一化边限制最小尺寸,避免极宽/高原图的窄轴把缩小比例锁死。
|
||||
const floor = Math.min(ceiling, minimum / Math.max(box.width, box.height));
|
||||
const factor = limit(proposed, floor, ceiling);
|
||||
const width = box.width * factor;
|
||||
const height = box.height * factor;
|
||||
|
||||
16
web/src/lib/default-user-avatars.ts
Normal file
16
web/src/lib/default-user-avatars.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export const DEFAULT_USER_AVATARS = [
|
||||
{ id: "person", label: "默认人像" },
|
||||
{ id: "director", label: "导演" },
|
||||
{ id: "camera", label: "摄影师" },
|
||||
{ id: "cat", label: "小猫" },
|
||||
{ id: "rabbit", label: "小兔" },
|
||||
{ id: "robot", label: "机器人" },
|
||||
{ id: "rocket", label: "探索者" },
|
||||
{ id: "sparkles", label: "灵感" },
|
||||
] as const;
|
||||
|
||||
export type DefaultUserAvatarID = (typeof DEFAULT_USER_AVATARS)[number]["id"];
|
||||
|
||||
export function isDefaultUserAvatarID(value: unknown): value is DefaultUserAvatarID {
|
||||
return DEFAULT_USER_AVATARS.some((avatar) => avatar.id === value);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
export type SettingsSection = "channels" | "models" | "preferences" | "prompts" | "storage";
|
||||
export type SettingsSection = "profile" | "channels" | "models" | "preferences" | "prompts" | "storage";
|
||||
|
||||
export function settingsPath(section: SettingsSection = "channels", continueCreation = false) {
|
||||
const params = new URLSearchParams({ section });
|
||||
export function settingsPath(section?: SettingsSection, continueCreation = false) {
|
||||
const params = new URLSearchParams({ section: section ?? (continueCreation ? "channels" : "profile") });
|
||||
if (continueCreation) params.set("continue", "1");
|
||||
return `/settings?${params.toString()}`;
|
||||
}
|
||||
|
||||
8
web/src/lib/user-avatar-url.ts
Normal file
8
web/src/lib/user-avatar-url.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
// Profile responses use a canonical /api path; deployment routing belongs to
|
||||
// the configured API base, including its origin and reverse-proxy prefix.
|
||||
export function resolveUserAvatarURL(candidate: string | undefined, apiBaseURL: string): string {
|
||||
if (!candidate) return "";
|
||||
if (/^https?:\/\//i.test(candidate)) return candidate;
|
||||
if (!/^\/api\/auth\/profile\/avatar\/[a-zA-Z0-9_-]+\?v=[a-zA-Z0-9_-]+$/.test(candidate)) return "";
|
||||
return `${apiBaseURL.replace(/\/+$/, "")}${candidate.slice("/api".length)}`;
|
||||
}
|
||||
Reference in New Issue
Block a user