Merge branch 'main' of http://113.45.157.195:3003/insiinc/leader-mail
This commit is contained in:
commit
713bd5f74b
|
@ -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,
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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,
|
||||||
});
|
});
|
||||||
|
|
|
@ -29,6 +29,7 @@ export const postDetailSelect: Prisma.PostSelect = {
|
||||||
name: true,
|
name: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
meta: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
receivers: {
|
receivers: {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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";
|
||||||
|
|
Loading…
Reference in New Issue