From b101cc8e3bdd5f1039e3c99ce741b7f5ebf63e5e Mon Sep 17 00:00:00 2001 From: ditiqi Date: Sun, 26 Jan 2025 20:26:12 +0800 Subject: [PATCH] add --- apps/server/src/auth/utils.ts | 16 +++--- .../common/uploader/AvatarUploader.tsx | 18 ++++-- .../common/uploader/TusUploader.tsx | 2 +- .../layout/element/usermenu/user-form.tsx | 4 ++ .../layout/element/usermenu/usermenu.tsx | 5 +- .../models/post/detail/PostCommentCard.tsx | 6 +- .../models/post/detail/PostCommentEditor.tsx | 9 +-- .../post/editor/form/LetterBasicForm.tsx | 4 +- apps/web/src/hooks/useTusUpload.ts | 6 +- packages/common/src/select.ts | 1 + packages/common/src/types.ts | 56 ++++++++++--------- packages/utils/src/index.ts | 47 +++++++++------- 12 files changed, 101 insertions(+), 73 deletions(-) diff --git a/apps/server/src/auth/utils.ts b/apps/server/src/auth/utils.ts index 5e8cd9a..2e3ab7c 100644 --- a/apps/server/src/auth/utils.ts +++ b/apps/server/src/auth/utils.ts @@ -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, diff --git a/apps/web/src/components/common/uploader/AvatarUploader.tsx b/apps/web/src/components/common/uploader/AvatarUploader.tsx index ae831a3..8cc6161 100644 --- a/apps/web/src/components/common/uploader/AvatarUploader.tsx +++ b/apps/web/src/components/common/uploader/AvatarUploader.tsx @@ -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 = ({ value, onChange, + compressed = true, className, placeholder = "点击上传", style, // 解构style属性 @@ -32,6 +35,7 @@ const AvatarUploader: React.FC = ({ const [file, setFile] = useState(null); const [previewUrl, setPreviewUrl] = useState(value || ""); + const [url, setUrl] = useState(value || ""); const [uploading, setUploading] = useState(false); const inputRef = useRef(null); @@ -50,7 +54,7 @@ const AvatarUploader: React.FC = ({ setUploading(true); try { - const fileId = await new Promise((resolve, reject) => { + const uploadedUrl = await new Promise((resolve, reject) => { handleFileUpload( selectedFile, (result) => { @@ -59,10 +63,13 @@ const AvatarUploader: React.FC = ({ 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 = ({ file?.fileKey ); }); - onChange?.(fileId); + // 使用 resolved 的最新值调用 onChange + onChange?.(uploadedUrl); message.success("头像上传成功"); } catch (error) { console.error("上传错误:", error); diff --git a/apps/web/src/components/common/uploader/TusUploader.tsx b/apps/web/src/components/common/uploader/TusUploader.tsx index 3aa7ad0..a7e0244 100644 --- a/apps/web/src/components/common/uploader/TusUploader.tsx +++ b/apps/web/src/components/common/uploader/TusUploader.tsx @@ -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; diff --git a/apps/web/src/components/layout/element/usermenu/user-form.tsx b/apps/web/src/components/layout/element/usermenu/user-form.tsx index d4c508f..61eacc8 100644 --- a/apps/web/src/components/layout/element/usermenu/user-form.tsx +++ b/apps/web/src/components/layout/element/usermenu/user-form.tsx @@ -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() { { + console.log(value); + }} style={{ width: "120px", height: "150px", diff --git a/apps/web/src/components/layout/element/usermenu/usermenu.tsx b/apps/web/src/components/layout/element/usermenu/usermenu.tsx index 9893014..c7c9cdd 100644 --- a/apps/web/src/components/layout/element/usermenu/usermenu.tsx +++ b/apps/web/src/components/layout/element/usermenu/usermenu.tsx @@ -142,10 +142,9 @@ export function UserMenu() { onClick={toggleMenu} className="flex items-center rounded-full transition-all duration-200 ease-in-out"> {/* Avatar 容器,相对定位 */} -
diff --git a/apps/web/src/components/models/post/detail/PostCommentEditor.tsx b/apps/web/src/components/models/post/detail/PostCommentEditor.tsx index 7a65b1d..bf782b0 100644 --- a/apps/web/src/components/models/post/detail/PostCommentEditor.tsx +++ b/apps/web/src/components/models/post/detail/PostCommentEditor.tsx @@ -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(undefined); const [fileIds, setFileIds] = useState([]); - 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() {
{ setSignature(e.target.value); }} - showCount + // showCount placeholder="签名" />
diff --git a/apps/web/src/components/models/post/editor/form/LetterBasicForm.tsx b/apps/web/src/components/models/post/editor/form/LetterBasicForm.tsx index 5b3312b..f088c91 100644 --- a/apps/web/src/components/models/post/editor/form/LetterBasicForm.tsx +++ b/apps/web/src/components/models/post/editor/form/LetterBasicForm.tsx @@ -102,11 +102,11 @@ export function LetterBasicForm() {
diff --git a/apps/web/src/hooks/useTusUpload.ts b/apps/web/src/hooks/useTusUpload.ts index a1ddf1d..38ff95d 100644 --- a/apps/web/src/hooks/useTusUpload.ts +++ b/apps/web/src/hooks/useTusUpload.ts @@ -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, }); diff --git a/packages/common/src/select.ts b/packages/common/src/select.ts index 32271f6..4b3526a 100644 --- a/packages/common/src/select.ts +++ b/packages/common/src/select.ts @@ -29,6 +29,7 @@ export const postDetailSelect: Prisma.PostSelect = { name: true, }, }, + meta: true, }, }, receivers: { diff --git a/packages/common/src/types.ts b/packages/common/src/types.ts index de67869..ca8a3a9 100755 --- a/packages/common/src/types.ts +++ b/packages/common/src/types.ts @@ -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; diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index b08b0b5..77aa06e 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -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" \ No newline at end of file + 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";