add
This commit is contained in:
parent
fe4a7cfae3
commit
b101cc8e3b
|
@ -11,7 +11,7 @@ import { env } from '@server/env';
|
|||
import { redis } from '@server/utils/redis/redis.service';
|
||||
import EventBus from '@server/utils/event-bus';
|
||||
import { RoleMapService } from '@server/models/rbac/rolemap.service';
|
||||
import { Request } from "express"
|
||||
import { Request } from 'express';
|
||||
interface ProfileResult {
|
||||
staff: UserProfile | undefined;
|
||||
error?: string;
|
||||
|
@ -22,9 +22,11 @@ interface TokenVerifyResult {
|
|||
error?: string;
|
||||
}
|
||||
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(' ') ?? [];
|
||||
return type === 'Bearer' ? token : undefined;
|
||||
}
|
||||
|
@ -40,7 +42,7 @@ export class UserProfileService {
|
|||
this.jwtService = new JwtService();
|
||||
this.departmentService = new DepartmentService();
|
||||
this.roleMapService = new RoleMapService(this.departmentService);
|
||||
EventBus.on("dataChanged", ({ type, data }) => {
|
||||
EventBus.on('dataChanged', ({ type, data }) => {
|
||||
if (type === ObjectType.STAFF) {
|
||||
// 确保 data 是数组,如果不是则转换为数组
|
||||
const dataArray = Array.isArray(data) ? data : [data];
|
||||
|
@ -51,7 +53,6 @@ export class UserProfileService {
|
|||
}
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
public getProfileCacheKey(id: string) {
|
||||
return `user-profile-${id}`;
|
||||
|
@ -162,6 +163,7 @@ export class UserProfileService {
|
|||
showname: true,
|
||||
username: true,
|
||||
phoneNumber: true,
|
||||
meta: true,
|
||||
},
|
||||
})) as unknown as UserProfile;
|
||||
}
|
||||
|
@ -174,9 +176,7 @@ export class UserProfileService {
|
|||
staff.deptId
|
||||
? this.departmentService.getDescendantIdsInDomain(staff.deptId)
|
||||
: [],
|
||||
staff.deptId
|
||||
? this.departmentService.getAncestorIds([staff.deptId])
|
||||
: [],
|
||||
staff.deptId ? this.departmentService.getAncestorIds([staff.deptId]) : [],
|
||||
this.roleMapService.getPermsForObject({
|
||||
domainId: staff.domainId,
|
||||
staffId: staff.id,
|
||||
|
|
|
@ -9,6 +9,7 @@ export interface AvatarUploaderProps {
|
|||
placeholder?: string;
|
||||
className?: string;
|
||||
onChange?: (value: string) => void;
|
||||
compressed?: boolean;
|
||||
style?: React.CSSProperties; // 添加style属性
|
||||
}
|
||||
|
||||
|
@ -18,12 +19,14 @@ interface UploadingFile {
|
|||
status: "uploading" | "done" | "error";
|
||||
fileId?: string;
|
||||
url?: string;
|
||||
compressedUrl?: string;
|
||||
fileKey?: string;
|
||||
}
|
||||
|
||||
const AvatarUploader: React.FC<AvatarUploaderProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
compressed = true,
|
||||
className,
|
||||
placeholder = "点击上传",
|
||||
style, // 解构style属性
|
||||
|
@ -32,6 +35,7 @@ const AvatarUploader: React.FC<AvatarUploaderProps> = ({
|
|||
const [file, setFile] = useState<UploadingFile | null>(null);
|
||||
|
||||
const [previewUrl, setPreviewUrl] = useState<string>(value || "");
|
||||
const [url, setUrl] = useState<string>(value || "");
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
|
@ -50,7 +54,7 @@ const AvatarUploader: React.FC<AvatarUploaderProps> = ({
|
|||
setUploading(true);
|
||||
|
||||
try {
|
||||
const fileId = await new Promise<string>((resolve, reject) => {
|
||||
const uploadedUrl = await new Promise<string>((resolve, reject) => {
|
||||
handleFileUpload(
|
||||
selectedFile,
|
||||
(result) => {
|
||||
|
@ -59,10 +63,13 @@ const AvatarUploader: React.FC<AvatarUploaderProps> = ({
|
|||
progress: 100,
|
||||
status: "done",
|
||||
fileId: result.fileId,
|
||||
url: result?.url,
|
||||
url: result.url,
|
||||
compressedUrl: result.compressedUrl,
|
||||
}));
|
||||
setPreviewUrl(result?.url);
|
||||
resolve(result.fileId);
|
||||
setUrl(result.url);
|
||||
setPreviewUrl(result.compressedUrl);
|
||||
// 直接使用 result 中的最新值
|
||||
resolve(compressed ? result.compressedUrl : result.url);
|
||||
},
|
||||
(error) => {
|
||||
reject(error);
|
||||
|
@ -70,7 +77,8 @@ const AvatarUploader: React.FC<AvatarUploaderProps> = ({
|
|||
file?.fileKey
|
||||
);
|
||||
});
|
||||
onChange?.(fileId);
|
||||
// 使用 resolved 的最新值调用 onChange
|
||||
onChange?.(uploadedUrl);
|
||||
message.success("头像上传成功");
|
||||
} catch (error) {
|
||||
console.error("上传错误:", error);
|
||||
|
|
|
@ -8,7 +8,7 @@ import { Upload, Progress, Button } from "antd";
|
|||
import type { UploadFile } from "antd";
|
||||
import { useTusUpload } from "@web/src/hooks/useTusUpload";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
import { getCompressedImageUrl } from "@nice/utils";
|
||||
export interface TusUploaderProps {
|
||||
value?: string[];
|
||||
onChange?: (value: string[]) => void;
|
||||
|
|
|
@ -47,6 +47,7 @@ export default function StaffForm() {
|
|||
rank,
|
||||
office,
|
||||
} = values;
|
||||
console.log("photoUrl", photoUrl);
|
||||
setFormLoading(true);
|
||||
try {
|
||||
if (data && user?.id) {
|
||||
|
@ -120,6 +121,9 @@ export default function StaffForm() {
|
|||
<AvatarUploader
|
||||
placeholder="点击上传头像"
|
||||
className="rounded-lg"
|
||||
onChange={(value) => {
|
||||
console.log(value);
|
||||
}}
|
||||
style={{
|
||||
width: "120px",
|
||||
height: "150px",
|
||||
|
|
|
@ -142,10 +142,9 @@ export function UserMenu() {
|
|||
onClick={toggleMenu}
|
||||
className="flex items-center rounded-full transition-all duration-200 ease-in-out">
|
||||
{/* Avatar 容器,相对定位 */}
|
||||
|
||||
<div className="relative">
|
||||
<Avatar
|
||||
src={(user?.meta as any)?.photoUrl}
|
||||
src={user?.meta?.photoUrl}
|
||||
name={user?.showname || user?.username}
|
||||
size={40}
|
||||
className="ring-2 ring-white hover:ring-[#00538E]/90
|
||||
|
@ -196,7 +195,7 @@ export function UserMenu() {
|
|||
border-b border-[#E5EDF5] ">
|
||||
<div className="flex items-center space-x-4">
|
||||
<Avatar
|
||||
src={user?.avatar}
|
||||
src={user?.meta?.photoUrl}
|
||||
name={user?.showname || user?.username}
|
||||
size={40}
|
||||
className="ring-2 ring-white shadow-sm"
|
||||
|
|
|
@ -17,6 +17,7 @@ import { CustomAvatar } from "@web/src/components/presentation/CustomAvatar";
|
|||
import PostResources from "./PostResources";
|
||||
import PostHateButton from "./PostHeader/PostHateButton";
|
||||
import PostSendButton from "./PostHeader/PostSendButton";
|
||||
import { getCompressedImageUrl } from "@nice/utils";
|
||||
|
||||
export default function PostCommentCard({
|
||||
post,
|
||||
|
@ -36,7 +37,10 @@ export default function PostCommentCard({
|
|||
<CustomAvatar
|
||||
src={post.author?.meta?.photoUrl}
|
||||
size={50}
|
||||
name={!post.author?.meta?.photoUrl && post.author?.showname}
|
||||
name={
|
||||
!post.author?.meta?.photoUrl &&
|
||||
post.author?.showname
|
||||
}
|
||||
randomString={
|
||||
post?.meta?.signature || post?.meta?.ip
|
||||
}></CustomAvatar>
|
||||
|
|
|
@ -3,7 +3,7 @@ import { motion } from "framer-motion";
|
|||
import { Button, Input, Tabs } from "antd";
|
||||
import QuillEditor from "@web/src/components/common/editor/quill/QuillEditor";
|
||||
import { PostDetailContext } from "./context/PostDetailContext";
|
||||
import { useEntity } from "@nice/client";
|
||||
import { usePost } from "@nice/client";
|
||||
import { PostType } from "@nice/common";
|
||||
import toast from "react-hot-toast";
|
||||
import { isContentEmpty } from "./utils";
|
||||
|
@ -18,7 +18,8 @@ export default function PostCommentEditor() {
|
|||
const [content, setContent] = useState("");
|
||||
const [signature, setSignature] = useState<string | undefined>(undefined);
|
||||
const [fileIds, setFileIds] = useState<string[]>([]);
|
||||
const { create } = useEntity("post")
|
||||
const { create } = usePost();
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (isContentEmpty(content)) {
|
||||
|
@ -98,14 +99,14 @@ export default function PostCommentEditor() {
|
|||
<div className="flex items-center justify-end gap-2">
|
||||
<CustomAvatar randomString={signature}></CustomAvatar>
|
||||
<Input
|
||||
maxLength={10}
|
||||
maxLength={15}
|
||||
style={{
|
||||
width: 150,
|
||||
}}
|
||||
onChange={(e) => {
|
||||
setSignature(e.target.value);
|
||||
}}
|
||||
showCount
|
||||
// showCount
|
||||
placeholder="签名"
|
||||
/>
|
||||
<div>
|
||||
|
|
|
@ -102,11 +102,11 @@ export function LetterBasicForm() {
|
|||
<div className="flex gap-2 ">
|
||||
<Form.Item name={["meta", "signature"]}>
|
||||
<Input
|
||||
maxLength={10}
|
||||
maxLength={15}
|
||||
style={{
|
||||
width: 150,
|
||||
}}
|
||||
showCount
|
||||
// showCount
|
||||
placeholder="签名"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { useState } from "react";
|
||||
import * as tus from "tus-js-client";
|
||||
import { env } from "../env";
|
||||
|
||||
import { getCompressedImageUrl } from "@nice/utils";
|
||||
// useTusUpload.ts
|
||||
interface UploadProgress {
|
||||
fileId: string;
|
||||
|
@ -9,6 +9,7 @@ interface UploadProgress {
|
|||
}
|
||||
|
||||
interface UploadResult {
|
||||
compressedUrl: string;
|
||||
url: string;
|
||||
fileId: string;
|
||||
}
|
||||
|
@ -35,7 +36,7 @@ export function useTusUpload() {
|
|||
throw new Error("Invalid upload URL format");
|
||||
}
|
||||
const resUrl = `http://${env.SERVER_IP}/uploads/${parts.slice(uploadIndex + 1, uploadIndex + 6).join("/")}`;
|
||||
console.log(resUrl);
|
||||
|
||||
return resUrl;
|
||||
};
|
||||
const handleFileUpload = async (
|
||||
|
@ -84,6 +85,7 @@ export function useTusUpload() {
|
|||
[fileKey]: 100,
|
||||
}));
|
||||
onSuccess({
|
||||
compressedUrl: getCompressedImageUrl(url),
|
||||
url,
|
||||
fileId,
|
||||
});
|
||||
|
|
|
@ -29,6 +29,7 @@ export const postDetailSelect: Prisma.PostSelect = {
|
|||
name: true,
|
||||
},
|
||||
},
|
||||
meta: true,
|
||||
},
|
||||
},
|
||||
receivers: {
|
||||
|
|
|
@ -35,15 +35,16 @@ export type AppLocalSettings = {
|
|||
important?: number;
|
||||
exploreTime?: Date;
|
||||
};
|
||||
export type StaffMeta = {
|
||||
photoUrl?: string;
|
||||
office?: string;
|
||||
email?: string;
|
||||
rank?: string;
|
||||
};
|
||||
export type StaffDto = Staff & {
|
||||
domain?: Department;
|
||||
department?: Department;
|
||||
meta?: {
|
||||
photoUrl?: string;
|
||||
office?: string;
|
||||
email?: string;
|
||||
rank?: string;
|
||||
};
|
||||
meta?: StaffMeta;
|
||||
};
|
||||
export interface AuthDto {
|
||||
token: string;
|
||||
|
@ -57,6 +58,7 @@ export type UserProfile = Staff & {
|
|||
parentDeptIds: string[];
|
||||
domain: Department;
|
||||
department: Department;
|
||||
meta?: StaffMeta;
|
||||
};
|
||||
|
||||
export interface DataNode {
|
||||
|
@ -132,22 +134,22 @@ export type PostComment = {
|
|||
};
|
||||
|
||||
export interface BaseMetadata {
|
||||
size: number
|
||||
filetype: string
|
||||
filename: string
|
||||
extension: string
|
||||
modifiedAt: Date
|
||||
size: number;
|
||||
filetype: string;
|
||||
filename: string;
|
||||
extension: string;
|
||||
modifiedAt: Date;
|
||||
}
|
||||
/**
|
||||
* 图片特有元数据接口
|
||||
*/
|
||||
export interface ImageMetadata {
|
||||
width: number; // 图片宽度(px)
|
||||
height: number; // 图片高度(px)
|
||||
width: number; // 图片宽度(px)
|
||||
height: number; // 图片高度(px)
|
||||
compressedUrl?: string;
|
||||
orientation?: number; // EXIF方向信息
|
||||
space?: string; // 色彩空间 (如: RGB, CMYK)
|
||||
hasAlpha?: boolean; // 是否包含透明通道
|
||||
orientation?: number; // EXIF方向信息
|
||||
space?: string; // 色彩空间 (如: RGB, CMYK)
|
||||
hasAlpha?: boolean; // 是否包含透明通道
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -159,26 +161,28 @@ export interface VideoMetadata {
|
|||
duration?: number;
|
||||
videoCodec?: string;
|
||||
audioCodec?: string;
|
||||
coverUrl?: string
|
||||
coverUrl?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 音频特有元数据接口
|
||||
*/
|
||||
export interface AudioMetadata {
|
||||
duration: number; // 音频时长(秒)
|
||||
bitrate?: number; // 比特率(bps)
|
||||
sampleRate?: number; // 采样率(Hz)
|
||||
channels?: number; // 声道数
|
||||
codec?: string; // 音频编码格式
|
||||
duration: number; // 音频时长(秒)
|
||||
bitrate?: number; // 比特率(bps)
|
||||
sampleRate?: number; // 采样率(Hz)
|
||||
channels?: number; // 声道数
|
||||
codec?: string; // 音频编码格式
|
||||
}
|
||||
|
||||
|
||||
export type FileMetadata = ImageMetadata & VideoMetadata & AudioMetadata & BaseMetadata
|
||||
export type FileMetadata = ImageMetadata &
|
||||
VideoMetadata &
|
||||
AudioMetadata &
|
||||
BaseMetadata;
|
||||
|
||||
export type ResourceDto = Resource & {
|
||||
meta: FileMetadata
|
||||
}
|
||||
meta: FileMetadata;
|
||||
};
|
||||
export type PostDto = Post & {
|
||||
readed: boolean;
|
||||
liked: boolean;
|
||||
|
|
|
@ -4,33 +4,38 @@
|
|||
* @returns 唯一ID字符串
|
||||
*/
|
||||
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'
|
||||
? window.crypto.getRandomValues(new Uint32Array(1))[0].toString(36)
|
||||
: require('crypto').randomBytes(4).toString('hex');
|
||||
// 获取环境特定的额外随机性
|
||||
const environmentPart =
|
||||
typeof window !== "undefined"
|
||||
? 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) => {
|
||||
if (bytes < 1024) return `${bytes} B`;
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
||||
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
|
||||
if (bytes < 1024) return `${bytes} B`;
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
||||
if (bytes < 1024 * 1024 * 1024)
|
||||
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
|
||||
};
|
||||
// 压缩图片路径生成函数
|
||||
export const getCompressedImageUrl = (originalUrl: string): string => {
|
||||
const cleanUrl = originalUrl.split(/[?#]/)[0] // 移除查询参数和哈希
|
||||
const lastSlashIndex = cleanUrl.lastIndexOf('/')
|
||||
return `${cleanUrl.slice(0, lastSlashIndex)}/compressed/${cleanUrl.slice(lastSlashIndex + 1).replace(/\.[^.]+$/, '.webp')}`
|
||||
}
|
||||
export * from "./types"
|
||||
if (!originalUrl) {
|
||||
return originalUrl;
|
||||
}
|
||||
const cleanUrl = originalUrl.split(/[?#]/)[0]; // 移除查询参数和哈希
|
||||
const lastSlashIndex = cleanUrl.lastIndexOf("/");
|
||||
return `${cleanUrl.slice(0, lastSlashIndex)}/compressed/${cleanUrl.slice(lastSlashIndex + 1).replace(/\.[^.]+$/, ".webp")}`;
|
||||
};
|
||||
export * from "./types";
|
||||
|
|
Loading…
Reference in New Issue