This commit is contained in:
longdayi 2025-01-26 21:01:46 +08:00
commit 713bd5f74b
12 changed files with 292 additions and 232 deletions

View File

@ -11,7 +11,7 @@ import { env } from '@server/env';
import { redis } from '@server/utils/redis/redis.service'; import { redis } from '@server/utils/redis/redis.service';
import EventBus from '@server/utils/event-bus'; import EventBus from '@server/utils/event-bus';
import { RoleMapService } from '@server/models/rbac/rolemap.service'; import { RoleMapService } from '@server/models/rbac/rolemap.service';
import { Request } from "express" import { Request } from 'express';
interface ProfileResult { interface ProfileResult {
staff: UserProfile | undefined; staff: UserProfile | undefined;
error?: string; error?: string;
@ -22,9 +22,11 @@ interface TokenVerifyResult {
error?: string; error?: string;
} }
export function extractTokenFromHeader(request: Request): string | undefined { export function extractTokenFromHeader(request: Request): string | undefined {
return extractTokenFromAuthorization(request.headers.authorization) return extractTokenFromAuthorization(request.headers.authorization);
} }
export function extractTokenFromAuthorization(authorization: string): string | undefined { export function extractTokenFromAuthorization(
authorization: string,
): string | undefined {
const [type, token] = authorization?.split(' ') ?? []; const [type, token] = authorization?.split(' ') ?? [];
return type === 'Bearer' ? token : undefined; return type === 'Bearer' ? token : undefined;
} }
@ -40,7 +42,7 @@ export class UserProfileService {
this.jwtService = new JwtService(); this.jwtService = new JwtService();
this.departmentService = new DepartmentService(); this.departmentService = new DepartmentService();
this.roleMapService = new RoleMapService(this.departmentService); this.roleMapService = new RoleMapService(this.departmentService);
EventBus.on("dataChanged", ({ type, data }) => { EventBus.on('dataChanged', ({ type, data }) => {
if (type === ObjectType.STAFF) { if (type === ObjectType.STAFF) {
// 确保 data 是数组,如果不是则转换为数组 // 确保 data 是数组,如果不是则转换为数组
const dataArray = Array.isArray(data) ? data : [data]; const dataArray = Array.isArray(data) ? data : [data];
@ -51,7 +53,6 @@ export class UserProfileService {
} }
} }
}); });
} }
public getProfileCacheKey(id: string) { public getProfileCacheKey(id: string) {
return `user-profile-${id}`; return `user-profile-${id}`;
@ -162,6 +163,7 @@ export class UserProfileService {
showname: true, showname: true,
username: true, username: true,
phoneNumber: true, phoneNumber: true,
meta: true,
}, },
})) as unknown as UserProfile; })) as unknown as UserProfile;
} }
@ -174,9 +176,7 @@ export class UserProfileService {
staff.deptId staff.deptId
? this.departmentService.getDescendantIdsInDomain(staff.deptId) ? this.departmentService.getDescendantIdsInDomain(staff.deptId)
: [], : [],
staff.deptId staff.deptId ? this.departmentService.getAncestorIds([staff.deptId]) : [],
? this.departmentService.getAncestorIds([staff.deptId])
: [],
this.roleMapService.getPermsForObject({ this.roleMapService.getPermsForObject({
domainId: staff.domainId, domainId: staff.domainId,
staffId: staff.id, staffId: staff.id,

View File

@ -9,6 +9,7 @@ export interface AvatarUploaderProps {
placeholder?: string; placeholder?: string;
className?: string; className?: string;
onChange?: (value: string) => void; onChange?: (value: string) => void;
compressed?: boolean;
style?: React.CSSProperties; // 添加style属性 style?: React.CSSProperties; // 添加style属性
} }
@ -18,12 +19,14 @@ interface UploadingFile {
status: "uploading" | "done" | "error"; status: "uploading" | "done" | "error";
fileId?: string; fileId?: string;
url?: string; url?: string;
compressedUrl?: string;
fileKey?: string; fileKey?: string;
} }
const AvatarUploader: React.FC<AvatarUploaderProps> = ({ const AvatarUploader: React.FC<AvatarUploaderProps> = ({
value, value,
onChange, onChange,
compressed = true,
className, className,
placeholder = "点击上传", placeholder = "点击上传",
style, // 解构style属性 style, // 解构style属性
@ -32,6 +35,7 @@ const AvatarUploader: React.FC<AvatarUploaderProps> = ({
const [file, setFile] = useState<UploadingFile | null>(null); const [file, setFile] = useState<UploadingFile | null>(null);
const [previewUrl, setPreviewUrl] = useState<string>(value || ""); const [previewUrl, setPreviewUrl] = useState<string>(value || "");
const [url, setUrl] = useState<string>(value || "");
const [uploading, setUploading] = useState(false); const [uploading, setUploading] = useState(false);
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
@ -50,7 +54,7 @@ const AvatarUploader: React.FC<AvatarUploaderProps> = ({
setUploading(true); setUploading(true);
try { try {
const fileId = await new Promise<string>((resolve, reject) => { const uploadedUrl = await new Promise<string>((resolve, reject) => {
handleFileUpload( handleFileUpload(
selectedFile, selectedFile,
(result) => { (result) => {
@ -59,10 +63,13 @@ const AvatarUploader: React.FC<AvatarUploaderProps> = ({
progress: 100, progress: 100,
status: "done", status: "done",
fileId: result.fileId, fileId: result.fileId,
url: result?.url, url: result.url,
compressedUrl: result.compressedUrl,
})); }));
setPreviewUrl(result?.url); setUrl(result.url);
resolve(result.fileId); setPreviewUrl(result.compressedUrl);
// 直接使用 result 中的最新值
resolve(compressed ? result.compressedUrl : result.url);
}, },
(error) => { (error) => {
reject(error); reject(error);
@ -70,7 +77,8 @@ const AvatarUploader: React.FC<AvatarUploaderProps> = ({
file?.fileKey file?.fileKey
); );
}); });
onChange?.(fileId); // 使用 resolved 的最新值调用 onChange
onChange?.(uploadedUrl);
message.success("头像上传成功"); message.success("头像上传成功");
} catch (error) { } catch (error) {
console.error("上传错误:", error); console.error("上传错误:", error);

View File

@ -1,11 +1,17 @@
import { useCallback, useState } from "react"; // TusUploader.tsx
import {
useCallback,
useState,
useEffect,
forwardRef,
useImperativeHandle,
} from "react";
import { import {
UploadOutlined, UploadOutlined,
CheckCircleOutlined, CheckCircleOutlined,
DeleteOutlined, DeleteOutlined,
} from "@ant-design/icons"; } from "@ant-design/icons";
import { Upload, Progress, Button } from "antd"; import { Upload, Progress, Button } from "antd";
import type { UploadFile } from "antd";
import { useTusUpload } from "@web/src/hooks/useTusUpload"; import { useTusUpload } from "@web/src/hooks/useTusUpload";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
@ -14,6 +20,10 @@ export interface TusUploaderProps {
onChange?: (value: string[]) => void; onChange?: (value: string[]) => void;
} }
export interface TusUploaderRef {
reset: () => void;
}
interface UploadingFile { interface UploadingFile {
name: string; name: string;
progress: number; progress: number;
@ -22,164 +32,179 @@ interface UploadingFile {
fileKey?: string; fileKey?: string;
} }
export const TusUploader = ({ value = [], onChange }: TusUploaderProps) => { export const TusUploader = forwardRef<TusUploaderRef, TusUploaderProps>(
const { handleFileUpload, uploadProgress } = useTusUpload(); ({ value = [], onChange }, ref) => {
const [uploadingFiles, setUploadingFiles] = useState<UploadingFile[]>([]); const { handleFileUpload, uploadProgress } = useTusUpload();
const [completedFiles, setCompletedFiles] = useState<UploadingFile[]>( const [uploadingFiles, setUploadingFiles] = useState<UploadingFile[]>(
() => []
value?.map((fileId) => ({ );
name: `文件 ${fileId}`, const [completedFiles, setCompletedFiles] = useState<UploadingFile[]>(
progress: 100, []
status: "done" as const, );
fileId,
})) || [] // 同步父组件value到completedFiles
); useEffect(() => {
// 恢复使用 uploadResults 状态跟踪最新结果 setCompletedFiles(
const [uploadResults, setUploadResults] = useState<string[]>(value || []); value.map((fileId) => ({
const handleRemoveFile = useCallback( name: `文件 ${fileId}`,
(fileId: string) => { progress: 100,
setCompletedFiles((prev) => status: "done" as const,
prev.filter((f) => f.fileId !== fileId) fileId,
}))
); );
// 使用函数式更新保证获取最新状态 }, [value]);
setUploadResults((prev) => {
const newValue = prev.filter((id) => id !== fileId);
onChange?.(newValue); // 同步更新父组件
return newValue;
});
},
[onChange]
);
const handleBeforeUpload = useCallback( // 暴露重置方法
(file: File) => { useImperativeHandle(ref, () => ({
const fileKey = `${file.name}-${Date.now()}`; reset: () => {
setCompletedFiles([]);
setUploadingFiles([]);
},
}));
setUploadingFiles((prev) => [ const handleRemoveFile = useCallback(
...prev, (fileId: string) => {
{ setCompletedFiles((prev) =>
name: file.name, prev.filter((f) => f.fileId !== fileId)
progress: 0, );
status: "uploading", onChange?.(value.filter((id) => id !== fileId));
fileKey, },
}, [onChange, value]
]); );
handleFileUpload( const handleRemoveUploadingFile = useCallback((fileKey: string) => {
file, setUploadingFiles((prev) =>
(result) => { prev.filter((f) => f.fileKey !== fileKey)
setCompletedFiles((prev) => [
...prev,
{
name: file.name,
progress: 100,
status: "done",
fileId: result.fileId,
},
]);
setUploadingFiles((prev) =>
prev.filter((f) => f.fileKey !== fileKey)
);
// 正确的状态更新方式
setUploadResults((prev) => {
const newValue = [...prev, result.fileId];
onChange?.(newValue); // 传递值而非函数
return newValue;
});
},
(error) => {
console.error("上传错误:", error);
toast.error(
`上传失败: ${
error instanceof Error ? error.message : "未知错误"
}`
);
setUploadingFiles((prev) =>
prev.map((f) =>
f.fileKey === fileKey
? { ...f, status: "error" }
: f
)
);
},
fileKey
); );
}, []);
return false; const handleBeforeUpload = useCallback(
}, (file: File) => {
[handleFileUpload, onChange] const fileKey = `${file.name}-${Date.now()}`;
);
return ( setUploadingFiles((prev) => [
<div className="space-y-1"> ...prev,
<Upload.Dragger {
name="files" name: file.name,
multiple progress: 0,
showUploadList={false} status: "uploading",
style={{ background: "transparent", borderStyle: "none" }} fileKey,
beforeUpload={handleBeforeUpload}> },
<p className="ant-upload-drag-icon"> ]);
<UploadOutlined />
</p>
<p className="ant-upload-text">
</p>
<p className="ant-upload-hint"></p>
{/* 上传状态展示 */} handleFileUpload(
<div className="px-2 py-0 rounded mt-1"> file,
{/* 上传中的文件 */} (result) => {
{uploadingFiles.map((file) => ( const newValue = [...value, result.fileId];
<div onChange?.(newValue);
key={file.fileKey} setUploadingFiles((prev) =>
className="flex flex-col gap-1 mb-2"> prev.filter((f) => f.fileKey !== fileKey)
<div className="flex items-center gap-2"> );
<span className="text-sm">{file.name}</span> },
(error) => {
console.error("上传错误:", error);
toast.error(
`上传失败: ${error instanceof Error ? error.message : "未知错误"}`
);
setUploadingFiles((prev) =>
prev.map((f) =>
f.fileKey === fileKey
? { ...f, status: "error" }
: f
)
);
},
fileKey
);
return false;
},
[handleFileUpload, onChange, value]
);
return (
<div className="space-y-1">
<Upload.Dragger
name="files"
multiple
showUploadList={false}
style={{ background: "transparent", borderStyle: "none" }}
beforeUpload={handleBeforeUpload}>
<p className="ant-upload-drag-icon">
<UploadOutlined />
</p>
<p className="ant-upload-text">
</p>
<p className="ant-upload-hint"></p>
<div className="px-2 py-0 rounded mt-1">
{uploadingFiles.map((file) => (
<div
key={file.fileKey}
className="flex flex-col gap-1 mb-2">
<div className="flex items-center gap-2">
<span className="text-sm">{file.name}</span>
</div>
<div className="flex items-center gap-2">
<Progress
percent={
file.status === "done"
? 100
: Math.round(
uploadProgress?.[
file.fileKey!
] || 0
)
}
status={
file.status === "error"
? "exception"
: "active"
}
className="flex-1"
/>
{file.status === "error" && (
<Button
type="text"
danger
icon={<DeleteOutlined />}
onClick={(e) => {
e.stopPropagation();
if (file.fileKey)
handleRemoveUploadingFile(
file.fileKey
);
}}
/>
)}
</div>
</div> </div>
<Progress ))}
percent={
file.status === "done"
? 100
: Math.round(
uploadProgress?.[
file.fileKey!
] || 0
)
}
status={
file.status === "error"
? "exception"
: "active"
}
/>
</div>
))}
{/* 已完成的文件 */} {completedFiles.map((file) => (
{completedFiles.map((file) => ( <div
<div key={file.fileId}
key={file.fileId} className="flex items-center justify-between gap-2 mb-2">
className="flex items-center justify-between gap-2 mb-2"> <div className="flex items-center gap-2">
<div className="flex items-center gap-2"> <CheckCircleOutlined className="text-green-500" />
<CheckCircleOutlined className="text-green-500" /> <span className="text-sm">{file.name}</span>
<span className="text-sm">{file.name}</span> </div>
<Button
type="text"
danger
icon={<DeleteOutlined />}
onClick={(e) => {
e.stopPropagation();
if (file.fileId)
handleRemoveFile(file.fileId);
}}
/>
</div> </div>
<Button ))}
type="text" </div>
danger </Upload.Dragger>
icon={<DeleteOutlined />} </div>
onClick={(e) => { );
e.stopPropagation(); }
if (file.fileId) );
handleRemoveFile(file.fileId);
}}
/>
</div>
))}
</div>
</Upload.Dragger>
</div>
);
};

View File

@ -47,6 +47,7 @@ export default function StaffForm() {
rank, rank,
office, office,
} = values; } = values;
console.log("photoUrl", photoUrl);
setFormLoading(true); setFormLoading(true);
try { try {
if (data && user?.id) { if (data && user?.id) {
@ -120,6 +121,9 @@ export default function StaffForm() {
<AvatarUploader <AvatarUploader
placeholder="点击上传头像" placeholder="点击上传头像"
className="rounded-lg" className="rounded-lg"
onChange={(value) => {
console.log(value);
}}
style={{ style={{
width: "120px", width: "120px",
height: "150px", height: "150px",

View File

@ -144,7 +144,7 @@ export function UserMenu() {
{/* Avatar 容器,相对定位 */} {/* Avatar 容器,相对定位 */}
<div className="relative"> <div className="relative">
<Avatar <Avatar
src={user?.avatar} src={user?.meta?.photoUrl}
name={user?.showname || user?.username} name={user?.showname || user?.username}
size={40} size={40}
className="ring-2 ring-white hover:ring-[#00538E]/90 className="ring-2 ring-white hover:ring-[#00538E]/90
@ -195,7 +195,7 @@ export function UserMenu() {
border-b border-[#E5EDF5] "> border-b border-[#E5EDF5] ">
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">
<Avatar <Avatar
src={user?.avatar} src={user?.meta?.photoUrl}
name={user?.showname || user?.username} name={user?.showname || user?.username}
size={40} size={40}
className="ring-2 ring-white shadow-sm" className="ring-2 ring-white shadow-sm"

View File

@ -17,6 +17,7 @@ import { CustomAvatar } from "@web/src/components/presentation/CustomAvatar";
import PostResources from "./PostResources"; import PostResources from "./PostResources";
import PostHateButton from "./PostHeader/PostHateButton"; import PostHateButton from "./PostHeader/PostHateButton";
import PostSendButton from "./PostHeader/PostSendButton"; import PostSendButton from "./PostHeader/PostSendButton";
import { getCompressedImageUrl } from "@nice/utils";
export default function PostCommentCard({ export default function PostCommentCard({
post, post,
@ -34,9 +35,12 @@ export default function PostCommentCard({
<div className="flex items-start space-x-2 gap-2"> <div className="flex items-start space-x-2 gap-2">
<div className="flex-shrink-0"> <div className="flex-shrink-0">
<CustomAvatar <CustomAvatar
src={post.author?.avatar} src={post.author?.meta?.photoUrl}
size={50} size={50}
name={!post.author?.avatar && post.author?.showname} name={
!post.author?.meta?.photoUrl &&
post.author?.showname
}
randomString={ randomString={
post?.meta?.signature || post?.meta?.ip post?.meta?.signature || post?.meta?.ip
}></CustomAvatar> }></CustomAvatar>

View File

@ -1,9 +1,9 @@
import React, { useContext, useState } from "react"; import React, { useContext, useRef, useState } from "react";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { Button, Input, Tabs } from "antd"; import { Button, Input, Tabs } from "antd";
import QuillEditor from "@web/src/components/common/editor/quill/QuillEditor"; import QuillEditor from "@web/src/components/common/editor/quill/QuillEditor";
import { PostDetailContext } from "./context/PostDetailContext"; import { PostDetailContext } from "./context/PostDetailContext";
import { useEntity } from "@nice/client"; import { usePost } from "@nice/client";
import { PostType } from "@nice/common"; import { PostType } from "@nice/common";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import { isContentEmpty } from "./utils"; import { isContentEmpty } from "./utils";
@ -18,7 +18,9 @@ export default function PostCommentEditor() {
const [content, setContent] = useState(""); const [content, setContent] = useState("");
const [signature, setSignature] = useState<string | undefined>(undefined); const [signature, setSignature] = useState<string | undefined>(undefined);
const [fileIds, setFileIds] = useState<string[]>([]); const [fileIds, setFileIds] = useState<string[]>([]);
const { create } = useEntity("post") const { create } = usePost();
const uploaderRef = useRef<{ reset: () => void }>(null);
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
if (isContentEmpty(content)) { if (isContentEmpty(content)) {
@ -45,7 +47,10 @@ export default function PostCommentEditor() {
}); });
toast.success("发布成功!"); toast.success("发布成功!");
setContent(""); setContent("");
setFileIds([]); setFileIds([]); // 重置上传组件状态
// if (uploaderRef.current) {
// uploaderRef.current.reset();
// }
} catch (error) { } catch (error) {
toast.error("发布失败,请稍后重试"); toast.error("发布失败,请稍后重试");
console.error("Error posting comment:", error); console.error("Error posting comment:", error);
@ -85,6 +90,8 @@ export default function PostCommentEditor() {
<TabPane tab="附件" key="2"> <TabPane tab="附件" key="2">
<div className="relative rounded-xl border border-white hover:ring-1 ring-white transition-all duration-300 ease-in-out bg-slate-100"> <div className="relative rounded-xl border border-white hover:ring-1 ring-white transition-all duration-300 ease-in-out bg-slate-100">
<TusUploader <TusUploader
ref={uploaderRef}
value={fileIds}
onChange={(value) => { onChange={(value) => {
console.log("ids", value); console.log("ids", value);
setFileIds(value); setFileIds(value);
@ -98,14 +105,14 @@ export default function PostCommentEditor() {
<div className="flex items-center justify-end gap-2"> <div className="flex items-center justify-end gap-2">
<CustomAvatar randomString={signature}></CustomAvatar> <CustomAvatar randomString={signature}></CustomAvatar>
<Input <Input
maxLength={10} maxLength={15}
style={{ style={{
width: 150, width: 150,
}} }}
onChange={(e) => { onChange={(e) => {
setSignature(e.target.value); setSignature(e.target.value);
}} }}
showCount // showCount
placeholder="签名" placeholder="签名"
/> />
<div> <div>

View File

@ -102,11 +102,11 @@ export function LetterBasicForm() {
<div className="flex gap-2 "> <div className="flex gap-2 ">
<Form.Item name={["meta", "signature"]}> <Form.Item name={["meta", "signature"]}>
<Input <Input
maxLength={10} maxLength={15}
style={{ style={{
width: 150, width: 150,
}} }}
showCount // showCount
placeholder="签名" placeholder="签名"
/> />
</Form.Item> </Form.Item>

View File

@ -1,7 +1,7 @@
import { useState } from "react"; import { useState } from "react";
import * as tus from "tus-js-client"; import * as tus from "tus-js-client";
import { env } from "../env"; import { env } from "../env";
import { getCompressedImageUrl } from "@nice/utils";
// useTusUpload.ts // useTusUpload.ts
interface UploadProgress { interface UploadProgress {
fileId: string; fileId: string;
@ -9,6 +9,7 @@ interface UploadProgress {
} }
interface UploadResult { interface UploadResult {
compressedUrl: string;
url: string; url: string;
fileId: string; fileId: string;
} }
@ -35,7 +36,7 @@ export function useTusUpload() {
throw new Error("Invalid upload URL format"); throw new Error("Invalid upload URL format");
} }
const resUrl = `http://${env.SERVER_IP}/uploads/${parts.slice(uploadIndex + 1, uploadIndex + 6).join("/")}`; const resUrl = `http://${env.SERVER_IP}/uploads/${parts.slice(uploadIndex + 1, uploadIndex + 6).join("/")}`;
console.log(resUrl);
return resUrl; return resUrl;
}; };
const handleFileUpload = async ( const handleFileUpload = async (
@ -44,12 +45,12 @@ export function useTusUpload() {
onError: (error: Error) => void, onError: (error: Error) => void,
fileKey: string // 添加文件唯一标识 fileKey: string // 添加文件唯一标识
) => { ) => {
if (!file || !file.name || !file.type) { // if (!file || !file.name || !file.type) {
const error = new Error("不可上传该类型文件"); // const error = new Error("不可上传该类型文件");
setUploadError(error.message); // setUploadError(error.message);
onError(error); // onError(error);
return; // return;
} // }
setIsUploading(true); setIsUploading(true);
setUploadProgress((prev) => ({ ...prev, [fileKey]: 0 })); setUploadProgress((prev) => ({ ...prev, [fileKey]: 0 }));
@ -84,6 +85,7 @@ export function useTusUpload() {
[fileKey]: 100, [fileKey]: 100,
})); }));
onSuccess({ onSuccess({
compressedUrl: getCompressedImageUrl(url),
url, url,
fileId, fileId,
}); });

View File

@ -29,6 +29,7 @@ export const postDetailSelect: Prisma.PostSelect = {
name: true, name: true,
}, },
}, },
meta: true,
}, },
}, },
receivers: { receivers: {

View File

@ -35,15 +35,16 @@ export type AppLocalSettings = {
important?: number; important?: number;
exploreTime?: Date; exploreTime?: Date;
}; };
export type StaffMeta = {
photoUrl?: string;
office?: string;
email?: string;
rank?: string;
};
export type StaffDto = Staff & { export type StaffDto = Staff & {
domain?: Department; domain?: Department;
department?: Department; department?: Department;
meta?: { meta?: StaffMeta;
photoUrl?: string;
office?: string;
email?: string;
rank?: string;
};
}; };
export interface AuthDto { export interface AuthDto {
token: string; token: string;
@ -57,6 +58,7 @@ export type UserProfile = Staff & {
parentDeptIds: string[]; parentDeptIds: string[];
domain: Department; domain: Department;
department: Department; department: Department;
meta?: StaffMeta;
}; };
export interface DataNode { export interface DataNode {
@ -132,22 +134,22 @@ export type PostComment = {
}; };
export interface BaseMetadata { export interface BaseMetadata {
size: number size: number;
filetype: string filetype: string;
filename: string filename: string;
extension: string extension: string;
modifiedAt: Date modifiedAt: Date;
} }
/** /**
* *
*/ */
export interface ImageMetadata { export interface ImageMetadata {
width: number; // 图片宽度(px) width: number; // 图片宽度(px)
height: number; // 图片高度(px) height: number; // 图片高度(px)
compressedUrl?: string; compressedUrl?: string;
orientation?: number; // EXIF方向信息 orientation?: number; // EXIF方向信息
space?: string; // 色彩空间 (如: RGB, CMYK) space?: string; // 色彩空间 (如: RGB, CMYK)
hasAlpha?: boolean; // 是否包含透明通道 hasAlpha?: boolean; // 是否包含透明通道
} }
/** /**
@ -159,26 +161,28 @@ export interface VideoMetadata {
duration?: number; duration?: number;
videoCodec?: string; videoCodec?: string;
audioCodec?: string; audioCodec?: string;
coverUrl?: string coverUrl?: string;
} }
/** /**
* *
*/ */
export interface AudioMetadata { export interface AudioMetadata {
duration: number; // 音频时长(秒) duration: number; // 音频时长(秒)
bitrate?: number; // 比特率(bps) bitrate?: number; // 比特率(bps)
sampleRate?: number; // 采样率(Hz) sampleRate?: number; // 采样率(Hz)
channels?: number; // 声道数 channels?: number; // 声道数
codec?: string; // 音频编码格式 codec?: string; // 音频编码格式
} }
export type FileMetadata = ImageMetadata &
export type FileMetadata = ImageMetadata & VideoMetadata & AudioMetadata & BaseMetadata VideoMetadata &
AudioMetadata &
BaseMetadata;
export type ResourceDto = Resource & { export type ResourceDto = Resource & {
meta: FileMetadata meta: FileMetadata;
} };
export type PostDto = Post & { export type PostDto = Post & {
readed: boolean; readed: boolean;
liked: boolean; liked: boolean;

View File

@ -4,33 +4,38 @@
* @returns ID字符串 * @returns ID字符串
*/ */
export function generateUniqueId(prefix?: string): string { export function generateUniqueId(prefix?: string): string {
// 获取当前时间戳 // 获取当前时间戳
const timestamp = Date.now(); const timestamp = Date.now();
// 生成随机数部分 // 生成随机数部分
const randomPart = Math.random().toString(36).substring(2, 8); const randomPart = Math.random().toString(36).substring(2, 8);
// 获取环境特定的额外随机性 // 获取环境特定的额外随机性
const environmentPart = typeof window !== 'undefined' const environmentPart =
? window.crypto.getRandomValues(new Uint32Array(1))[0].toString(36) typeof window !== "undefined"
: require('crypto').randomBytes(4).toString('hex'); ? window.crypto.getRandomValues(new Uint32Array(1))[0].toString(36)
: require("crypto").randomBytes(4).toString("hex");
// 组合所有部分 // 组合所有部分
const uniquePart = `${timestamp}${randomPart}${environmentPart}`; const uniquePart = `${timestamp}${randomPart}${environmentPart}`;
// 如果提供了前缀,则添加前缀 // 如果提供了前缀,则添加前缀
return prefix ? `${prefix}_${uniquePart}` : uniquePart; return prefix ? `${prefix}_${uniquePart}` : uniquePart;
} }
export const formatFileSize = (bytes: number) => { export const formatFileSize = (bytes: number) => {
if (bytes < 1024) return `${bytes} B`; if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; if (bytes < 1024 * 1024 * 1024)
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`; return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
}; };
// 压缩图片路径生成函数 // 压缩图片路径生成函数
export const getCompressedImageUrl = (originalUrl: string): string => { export const getCompressedImageUrl = (originalUrl: string): string => {
const cleanUrl = originalUrl.split(/[?#]/)[0] // 移除查询参数和哈希 if (!originalUrl) {
const lastSlashIndex = cleanUrl.lastIndexOf('/') return originalUrl;
return `${cleanUrl.slice(0, lastSlashIndex)}/compressed/${cleanUrl.slice(lastSlashIndex + 1).replace(/\.[^.]+$/, '.webp')}` }
} const cleanUrl = originalUrl.split(/[?#]/)[0]; // 移除查询参数和哈希
export * from "./types" const lastSlashIndex = cleanUrl.lastIndexOf("/");
return `${cleanUrl.slice(0, lastSlashIndex)}/compressed/${cleanUrl.slice(lastSlashIndex + 1).replace(/\.[^.]+$/, ".webp")}`;
};
export * from "./types";