Merge branch 'main' of http://113.45.157.195:3003/insiinc/leader-mail
This commit is contained in:
commit
d411935430
|
@ -24,7 +24,7 @@ export class AuthService {
|
||||||
private readonly staffService: StaffService,
|
private readonly staffService: StaffService,
|
||||||
private readonly jwtService: JwtService,
|
private readonly jwtService: JwtService,
|
||||||
private readonly sessionService: SessionService,
|
private readonly sessionService: SessionService,
|
||||||
) {}
|
) { }
|
||||||
async validateFileRequest(params: FileRequest): Promise<FileAuthResult> {
|
async validateFileRequest(params: FileRequest): Promise<FileAuthResult> {
|
||||||
try {
|
try {
|
||||||
// 基础参数验证
|
// 基础参数验证
|
||||||
|
|
|
@ -51,7 +51,7 @@ export class ResourceProcessingPipeline {
|
||||||
ResourceStatus.PROCESSED,
|
ResourceStatus.PROCESSED,
|
||||||
);
|
);
|
||||||
this.logger.log(
|
this.logger.log(
|
||||||
`资源 ${resource.id} 处理成功 ${JSON.stringify(currentResource.metadata)}`,
|
`资源 ${resource.id} 处理成功 ${JSON.stringify(currentResource.meta)}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import path, { dirname } from "path";
|
import path, { dirname } from "path";
|
||||||
import { FileMetadata, VideoMetadata, ResourceProcessor } from "../types";
|
import { ResourceProcessor } from "../types";
|
||||||
import { Resource, ResourceStatus, db } from "@nice/common";
|
import { Resource, ResourceStatus, db } from "@nice/common";
|
||||||
import { Logger } from "@nestjs/common";
|
import { Logger } from "@nestjs/common";
|
||||||
import fs from 'fs/promises';
|
import fs from 'fs/promises';
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import sharp from 'sharp';
|
import sharp from 'sharp';
|
||||||
import { FileMetadata, ImageMetadata, ResourceProcessor } from '../types';
|
import { FileMetadata, ImageMetadata, ResourceDto } from '@nice/common';
|
||||||
import { Resource, ResourceStatus, db } from '@nice/common';
|
import { Resource, ResourceStatus, db } from '@nice/common';
|
||||||
import { getUploadFilePath } from '@server/utils/file';
|
import { getUploadFilePath } from '@server/utils/file';
|
||||||
import { BaseProcessor } from './BaseProcessor';
|
import { BaseProcessor } from './BaseProcessor';
|
||||||
|
@ -10,10 +10,10 @@ export class ImageProcessor extends BaseProcessor {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
async process(resource: Resource): Promise<Resource> {
|
async process(resource: ResourceDto): Promise<Resource> {
|
||||||
const { url } = resource;
|
const { url } = resource;
|
||||||
const filepath = getUploadFilePath(url);
|
const filepath = getUploadFilePath(url);
|
||||||
const originMeta = resource.metadata as unknown as FileMetadata;
|
const originMeta = resource.meta as unknown as FileMetadata;
|
||||||
if (!originMeta.filetype?.startsWith('image/')) {
|
if (!originMeta.filetype?.startsWith('image/')) {
|
||||||
this.logger.log(`Skipping non-image resource: ${resource.id}`);
|
this.logger.log(`Skipping non-image resource: ${resource.id}`);
|
||||||
return resource;
|
return resource;
|
||||||
|
@ -47,7 +47,7 @@ export class ImageProcessor extends BaseProcessor {
|
||||||
const updatedResource = await db.resource.update({
|
const updatedResource = await db.resource.update({
|
||||||
where: { id: resource.id },
|
where: { id: resource.id },
|
||||||
data: {
|
data: {
|
||||||
metadata: {
|
meta: {
|
||||||
...originMeta,
|
...originMeta,
|
||||||
...imageMeta,
|
...imageMeta,
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import path, { dirname } from 'path';
|
import path, { dirname } from 'path';
|
||||||
import ffmpeg from 'fluent-ffmpeg';
|
import ffmpeg from 'fluent-ffmpeg';
|
||||||
import { FileMetadata, VideoMetadata, ResourceProcessor } from '../types';
|
import { Resource, ResourceStatus, db, FileMetadata, VideoMetadata, ResourceDto } from '@nice/common';
|
||||||
import { Resource, ResourceStatus, db } from '@nice/common';
|
|
||||||
import { getUploadFilePath } from '@server/utils/file';
|
import { getUploadFilePath } from '@server/utils/file';
|
||||||
import fs from 'fs/promises';
|
import fs from 'fs/promises';
|
||||||
import sharp from 'sharp';
|
import sharp from 'sharp';
|
||||||
|
@ -11,14 +10,14 @@ export class VideoProcessor extends BaseProcessor {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
async process(resource: Resource): Promise<Resource> {
|
async process(resource: ResourceDto): Promise<Resource> {
|
||||||
const { url } = resource;
|
const { url } = resource;
|
||||||
const filepath = getUploadFilePath(url);
|
const filepath = getUploadFilePath(url);
|
||||||
this.logger.log(
|
this.logger.log(
|
||||||
`Processing video for resource ID: ${resource.id}, File ID: ${url}`,
|
`Processing video for resource ID: ${resource.id}, File ID: ${url}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
const originMeta = resource.metadata as unknown as FileMetadata;
|
const originMeta = resource.meta as unknown as FileMetadata;
|
||||||
if (!originMeta.filetype?.startsWith('video/')) {
|
if (!originMeta.filetype?.startsWith('video/')) {
|
||||||
this.logger.log(`Skipping non-video resource: ${resource.id}`);
|
this.logger.log(`Skipping non-video resource: ${resource.id}`);
|
||||||
return resource;
|
return resource;
|
||||||
|
@ -39,7 +38,7 @@ export class VideoProcessor extends BaseProcessor {
|
||||||
const updatedResource = await db.resource.update({
|
const updatedResource = await db.resource.update({
|
||||||
where: { id: resource.id },
|
where: { id: resource.id },
|
||||||
data: {
|
data: {
|
||||||
metadata: {
|
meta: {
|
||||||
...originMeta,
|
...originMeta,
|
||||||
...videoMeta,
|
...videoMeta,
|
||||||
},
|
},
|
||||||
|
|
|
@ -9,47 +9,3 @@ export interface ProcessResult {
|
||||||
error?: Error
|
error?: Error
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BaseMetadata {
|
|
||||||
size: number
|
|
||||||
filetype: string
|
|
||||||
filename: string
|
|
||||||
extension: string
|
|
||||||
modifiedAt: Date
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* 图片特有元数据接口
|
|
||||||
*/
|
|
||||||
export interface ImageMetadata {
|
|
||||||
width: number; // 图片宽度(px)
|
|
||||||
height: number; // 图片高度(px)
|
|
||||||
compressedUrl?: string;
|
|
||||||
orientation?: number; // EXIF方向信息
|
|
||||||
space?: string; // 色彩空间 (如: RGB, CMYK)
|
|
||||||
hasAlpha?: boolean; // 是否包含透明通道
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 视频特有元数据接口
|
|
||||||
*/
|
|
||||||
export interface VideoMetadata {
|
|
||||||
width?: number;
|
|
||||||
height?: number;
|
|
||||||
duration?: number;
|
|
||||||
videoCodec?: string;
|
|
||||||
audioCodec?: string;
|
|
||||||
coverUrl?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 音频特有元数据接口
|
|
||||||
*/
|
|
||||||
export interface AudioMetadata {
|
|
||||||
duration: number; // 音频时长(秒)
|
|
||||||
bitrate?: number; // 比特率(bps)
|
|
||||||
sampleRate?: number; // 采样率(Hz)
|
|
||||||
channels?: number; // 声道数
|
|
||||||
codec?: string; // 音频编码格式
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export type FileMetadata = ImageMetadata & VideoMetadata & AudioMetadata & BaseMetadata
|
|
|
@ -44,6 +44,7 @@ export class StaffService extends BaseService<Prisma.StaffDelegate> {
|
||||||
...data,
|
...data,
|
||||||
password: await argon2.hash((data.password || '123456') as string),
|
password: await argon2.hash((data.password || '123456') as string),
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = await super.create({ ...args, data: createData });
|
const result = await super.create({ ...args, data: createData });
|
||||||
this.emitDataChangedEvent(result, CrudOperation.CREATED);
|
this.emitDataChangedEvent(result, CrudOperation.CREATED);
|
||||||
return result;
|
return result;
|
||||||
|
|
|
@ -68,13 +68,12 @@ export class TusService implements OnModuleInit {
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const fileId = this.getFileId(upload.id);
|
const fileId = this.getFileId(upload.id);
|
||||||
|
|
||||||
await this.resourceService.create({
|
await this.resourceService.create({
|
||||||
data: {
|
data: {
|
||||||
title: getFilenameWithoutExt(upload.metadata.filename),
|
title: getFilenameWithoutExt(upload.metadata.filename),
|
||||||
fileId, // 移除最后的文件名
|
fileId, // 移除最后的文件名
|
||||||
url: upload.id,
|
url: upload.id,
|
||||||
metadata: upload.metadata,
|
meta: upload.metadata,
|
||||||
status: ResourceStatus.UPLOADING,
|
status: ResourceStatus.UPLOADING,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
VITE_APP_VERSION: "$VITE_APP_VERSION",
|
VITE_APP_VERSION: "$VITE_APP_VERSION",
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<title>fhmooc</title>
|
<title>首长机关信箱</title>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|
|
@ -48,10 +48,10 @@ const AuthPage: React.FC = () => {
|
||||||
transition={{ delay: 0.2, duration: 0.5 }}
|
transition={{ delay: 0.2, duration: 0.5 }}
|
||||||
>
|
>
|
||||||
<div className="text-4xl text-white mb-4 font-serif">
|
<div className="text-4xl text-white mb-4 font-serif">
|
||||||
烽火建言
|
首长机关信箱
|
||||||
</div>
|
</div>
|
||||||
<Paragraph className="text-lg mb-8 text-blue-100 leading-relaxed text-justify">
|
<Paragraph className="text-lg mb-8 text-blue-100 leading-relaxed text-justify">
|
||||||
用真心聆听每一位官兵的心声,第一时间回应大家的关切,把战友们的烦心事当作自己的心头事,用心用情解决每一个难题,全心全意办好每一件实事,让贴心服务成为军营最温暖的底色
|
聆音于微,润心以答,纾难化雨,解忧惟勤
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
{showLogin && (
|
{showLogin && (
|
||||||
<Button
|
<Button
|
||||||
|
|
|
@ -1,15 +1,17 @@
|
||||||
import LetterList from "@web/src/components/models/post/list/LetterList";
|
import LetterList from "@web/src/components/models/post/list/LetterList";
|
||||||
import { Header } from "./Header";
|
import { Header } from "./Header";
|
||||||
|
import { useSearchParams } from "react-router-dom";
|
||||||
|
|
||||||
export default function LetterListPage() {
|
export default function LetterListPage() {
|
||||||
|
const [params] = useSearchParams()
|
||||||
|
const keyword = params.get("keyword")
|
||||||
return (
|
return (
|
||||||
// 添加 flex flex-col 使其成为弹性布局容器
|
// 添加 flex flex-col 使其成为弹性布局容器
|
||||||
<div className="min-h-screen shadow-elegant border-2 border-white rounded-xl overflow-hidden bg-slate-200 flex flex-col">
|
<div className="min-h-screen shadow-elegant border-2 border-white rounded-xl overflow-hidden bg-slate-200 flex flex-col">
|
||||||
<Header />
|
<Header />
|
||||||
{/* 添加 flex-grow 使内容区域自动填充剩余空间 */}
|
{/* 添加 flex-grow 使内容区域自动填充剩余空间 */}
|
||||||
|
|
||||||
<LetterList params={{
|
<LetterList search={keyword} params={{
|
||||||
where: {
|
where: {
|
||||||
isPublic: true
|
isPublic: true
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,7 +30,7 @@ export default function LetterProgressPage() {
|
||||||
<div
|
<div
|
||||||
className=" shadow-elegant border-2 border-white rounded-xl bg-slate-200">
|
className=" shadow-elegant border-2 border-white rounded-xl bg-slate-200">
|
||||||
<ProgressHeader></ProgressHeader>
|
<ProgressHeader></ProgressHeader>
|
||||||
<main className="container mx-auto p-6 flex flex-col gap-4">
|
<main className=" mx-auto p-6 flex flex-col gap-4">
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
<Input
|
<Input
|
||||||
variant="filled"
|
variant="filled"
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
export default function LetterSearchPage() {
|
|
||||||
return <>Search</>
|
|
||||||
}
|
|
|
@ -36,6 +36,7 @@ export default function WriteLetterPage() {
|
||||||
contains: searchQuery,
|
contains: searchQuery,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
orderBy: {
|
orderBy: {
|
||||||
|
@ -55,9 +56,8 @@ export default function WriteLetterPage() {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen shadow-elegant border-2 border-white rounded-xl bg-slate-200">
|
<div className="min-h-screen shadow-elegant border-2 border-white rounded-xl bg-slate-200">
|
||||||
<WriteHeader term={getTerm(termId)} />
|
<WriteHeader term={getTerm(termId)} />
|
||||||
<div className="container mx-auto px-4 py-8">
|
<div className=" mx-auto p-6">
|
||||||
<div className="mb-8 space-y-4">
|
<div className="mb-4 space-y-4">
|
||||||
{/* Search and Filter Section */}
|
|
||||||
<div className="flex flex-col md:flex-row gap-4 items-center">
|
<div className="flex flex-col md:flex-row gap-4 items-center">
|
||||||
<DepartmentSelect
|
<DepartmentSelect
|
||||||
variant="filled"
|
variant="filled"
|
||||||
|
@ -80,7 +80,6 @@ export default function WriteLetterPage() {
|
||||||
size="large"
|
size="large"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<Alert
|
<Alert
|
||||||
message="加载失败"
|
message="加载失败"
|
||||||
|
@ -90,7 +89,6 @@ export default function WriteLetterPage() {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="flex justify-center items-center py-12">
|
<div className="flex justify-center items-center py-12">
|
||||||
|
|
|
@ -12,7 +12,7 @@ export function Footer() {
|
||||||
创新高地 软件小组
|
创新高地 软件小组
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-gray-400 text-xs italic">
|
<p className="text-gray-400 text-xs italic">
|
||||||
提供全天候技术支持
|
提供技术支持
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -1,26 +1,27 @@
|
||||||
import { Link, NavLink } from "react-router-dom";
|
import { Link, NavLink, useNavigate } from "react-router-dom";
|
||||||
import { memo } from "react";
|
import { memo } from "react";
|
||||||
import { SearchBar } from "./SearchBar";
|
import { SearchBar } from "./SearchBar";
|
||||||
import Navigation from "./navigation";
|
import Navigation from "./navigation";
|
||||||
import { useAuth } from "@web/src/providers/auth-provider";
|
import { useAuth } from "@web/src/providers/auth-provider";
|
||||||
import { UserOutlined } from "@ant-design/icons";
|
import { UserOutlined } from "@ant-design/icons";
|
||||||
import { UserMenu } from "../element/usermenu/usermenu";
|
import { UserMenu } from "../element/usermenu/usermenu";
|
||||||
import SineWavesCanvas from "../../animation/sine-wave";
|
|
||||||
interface HeaderProps {
|
export const Header = memo(function Header() {
|
||||||
onSearch?: (query: string) => void;
|
|
||||||
}
|
|
||||||
export const Header = memo(function Header({ onSearch }: HeaderProps) {
|
|
||||||
const { isAuthenticated } = useAuth();
|
const { isAuthenticated } = useAuth();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="sticky top-0 z-50 bg-gradient-to-br from-primary-500 to-primary-800 text-white shadow-lg">
|
<header className="sticky top-0 z-50 bg-gradient-to-br from-primary-500 to-primary-800 text-white shadow-lg">
|
||||||
<div className="container mx-auto px-4">
|
<div className=" mx-auto px-4">
|
||||||
<div className="py-3">
|
<div className="py-4">
|
||||||
<div className="flex items-center justify-between gap-4">
|
<div className="flex items-center justify-between gap-4">
|
||||||
<div className=" text-xl font-bold">烽火建言</div>
|
<div className="">
|
||||||
<div className="flex-grow max-w-2xl">
|
<span className="text-xl font-bold">首长机关信箱</span>
|
||||||
<SearchBar onSearch={onSearch} />
|
<p className=" text-sm text-secondary-50">聆怀若水,应语如风;纾难化困,践诺成春</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-6">
|
<div className="flex-grow max-w-2xl">
|
||||||
|
<SearchBar />
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center ">
|
||||||
{!isAuthenticated ? (
|
{!isAuthenticated ? (
|
||||||
<Link
|
<Link
|
||||||
to="/auth"
|
to="/auth"
|
||||||
|
@ -33,6 +34,7 @@ focus:outline-none focus:ring-2
|
||||||
focus:ring-[#8EADD4] focus:ring-offset-2
|
focus:ring-[#8EADD4] focus:ring-offset-2
|
||||||
focus:ring-offset-[#13294B]"
|
focus:ring-offset-[#13294B]"
|
||||||
aria-label="Login">
|
aria-label="Login">
|
||||||
|
|
||||||
<UserOutlined
|
<UserOutlined
|
||||||
className="h-5 w-5 transition-transform
|
className="h-5 w-5 transition-transform
|
||||||
group-hover:scale-110 group-hover:rotate-12"></UserOutlined>
|
group-hover:scale-110 group-hover:rotate-12"></UserOutlined>
|
||||||
|
|
|
@ -15,7 +15,7 @@ export function MainLayout() {
|
||||||
|
|
||||||
{/* 主要内容区域 */}
|
{/* 主要内容区域 */}
|
||||||
<main className="min-h-screen bg-slate-50">
|
<main className="min-h-screen bg-slate-50">
|
||||||
<div className="container mx-auto px-4 py-8">
|
<div className=" mx-auto px-4 py-8">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
|
@ -1,23 +1,21 @@
|
||||||
import { MagnifyingGlassIcon } from "@heroicons/react/24/outline";
|
import { MagnifyingGlassIcon } from "@heroicons/react/24/outline";
|
||||||
import { useState, useCallback } from "react";
|
import { useState, useCallback } from "react";
|
||||||
import debounce from "lodash/debounce";
|
import debounce from "lodash/debounce";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
interface SearchBarProps {
|
|
||||||
onSearch?: (query: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SearchBar = ({ onSearch }: SearchBarProps) => {
|
export const SearchBar = () => {
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
|
const navigate = useNavigate()
|
||||||
const debouncedSearch = useCallback(
|
const debouncedSearch = useCallback(
|
||||||
debounce((value: string) => {
|
debounce((value: string) => {
|
||||||
onSearch?.(value);
|
navigate(`/letter-list?keyword=${value}`, { replace: true })
|
||||||
}, 300),
|
}, 300),
|
||||||
[onSearch]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex-1 max-w-xl mx-12">
|
<div className="flex-1 max-w-xl ">
|
||||||
<div className="group relative">
|
<div className="group relative">
|
||||||
<input
|
<input
|
||||||
type="search"
|
type="search"
|
||||||
|
@ -26,12 +24,12 @@ export const SearchBar = ({ onSearch }: SearchBarProps) => {
|
||||||
setQuery(e.target.value);
|
setQuery(e.target.value);
|
||||||
debouncedSearch(e.target.value);
|
debouncedSearch(e.target.value);
|
||||||
}}
|
}}
|
||||||
placeholder="Search letters, announcements, policies..."
|
placeholder="搜索信件..."
|
||||||
className="w-full rounded-lg border border-transparent bg-[#0B1A32]
|
className="w-full rounded-lg border border-transparent bg-[#0B1A32]
|
||||||
px-4 py-2.5 pl-10 text-white placeholder-[#8EADD4]
|
px-4 py-2.5 pl-10 text-white placeholder-[#8EADD4]
|
||||||
transition-all duration-300 focus:border-[#8EADD4]
|
transition-all duration-300 focus:border-[#8EADD4]
|
||||||
focus:outline-none focus:ring-1 focus:ring-[#8EADD4]"
|
focus:outline-none focus:ring-1 focus:ring-[#8EADD4]"
|
||||||
aria-label="Search"
|
aria-label="搜索"
|
||||||
/>
|
/>
|
||||||
<MagnifyingGlassIcon
|
<MagnifyingGlassIcon
|
||||||
className="absolute left-3 top-3 h-5 w-5 text-[#8EADD4]
|
className="absolute left-3 top-3 h-5 w-5 text-[#8EADD4]
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { DownloadOutlined } from "@ant-design/icons";
|
||||||
import { PostDto } from "@nice/common";
|
import { PostDto } from "@nice/common";
|
||||||
import { env } from "@web/src/env";
|
import { env } from "@web/src/env";
|
||||||
import { getFileIcon } from "./utils";
|
import { getFileIcon } from "./utils";
|
||||||
|
import { formatFileSize } from '@nice/utils';
|
||||||
export default function PostResources({ post }: { post: PostDto }) {
|
export default function PostResources({ post }: { post: PostDto }) {
|
||||||
const { resources } = useMemo(() => {
|
const { resources } = useMemo(() => {
|
||||||
if (!post?.resources) return { resources: [] };
|
if (!post?.resources) return { resources: [] };
|
||||||
|
@ -98,9 +98,8 @@ export default function PostResources({ post }: { post: PostDto }) {
|
||||||
文件
|
文件
|
||||||
</span>
|
</span>
|
||||||
<span className="text-xs text-gray-400">
|
<span className="text-xs text-gray-400">
|
||||||
{(resource.metadata as any)
|
{resource.meta.size &&
|
||||||
.size &&
|
formatFileSize(resource.meta.size)}
|
||||||
`${((resource.metadata as any).size / 1024 / 1024).toFixed(1)}MB`}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -8,12 +8,17 @@ import debounce from "lodash/debounce";
|
||||||
import { postDetailSelect } from "@nice/common";
|
import { postDetailSelect } from "@nice/common";
|
||||||
export default function LetterList({
|
export default function LetterList({
|
||||||
params,
|
params,
|
||||||
|
search = ''
|
||||||
}: {
|
}: {
|
||||||
|
search?: string,
|
||||||
params: NonVoid<RouterInputs["post"]["findManyWithPagination"]>;
|
params: NonVoid<RouterInputs["post"]["findManyWithPagination"]>;
|
||||||
}) {
|
}) {
|
||||||
const [searchText, setSearchText] = useState("");
|
const [keyword, setKeyword] = useState<string | undefined>('');
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
useEffect(() => {
|
||||||
|
|
||||||
|
setKeyword(search || '')
|
||||||
|
}, [search])
|
||||||
const { data, isLoading } = api.post.findManyWithPagination.useQuery({
|
const { data, isLoading } = api.post.findManyWithPagination.useQuery({
|
||||||
page: currentPage,
|
page: currentPage,
|
||||||
pageSize: params.pageSize,
|
pageSize: params.pageSize,
|
||||||
|
@ -21,7 +26,7 @@ export default function LetterList({
|
||||||
OR: [
|
OR: [
|
||||||
{
|
{
|
||||||
title: {
|
title: {
|
||||||
contains: searchText,
|
contains: keyword,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -39,7 +44,7 @@ export default function LetterList({
|
||||||
const debouncedSearch = useMemo(
|
const debouncedSearch = useMemo(
|
||||||
() =>
|
() =>
|
||||||
debounce((value: string) => {
|
debounce((value: string) => {
|
||||||
setSearchText(value);
|
setKeyword(value);
|
||||||
setCurrentPage(1);
|
setCurrentPage(1);
|
||||||
}, 300),
|
}, 300),
|
||||||
[]
|
[]
|
||||||
|
@ -65,6 +70,7 @@ export default function LetterList({
|
||||||
{/* Search Bar */}
|
{/* Search Bar */}
|
||||||
<div className="p-6 transition-all ">
|
<div className="p-6 transition-all ">
|
||||||
<Input
|
<Input
|
||||||
|
value={keyword}
|
||||||
variant="filled"
|
variant="filled"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
placeholder="搜索信件标题..."
|
placeholder="搜索信件标题..."
|
||||||
|
@ -103,7 +109,7 @@ export default function LetterList({
|
||||||
<div className="flex flex-col justify-center items-center pt-6">
|
<div className="flex flex-col justify-center items-center pt-6">
|
||||||
<Empty
|
<Empty
|
||||||
description={
|
description={
|
||||||
searchText ? "未找到相关信件" : "暂无信件"
|
keyword ? "未找到相关信件" : "暂无信件"
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -52,6 +52,7 @@ export function useTusUpload() {
|
||||||
metadata: {
|
metadata: {
|
||||||
filename: file.name,
|
filename: file.name,
|
||||||
filetype: file.type,
|
filetype: file.type,
|
||||||
|
size: file.size as any
|
||||||
},
|
},
|
||||||
onProgress: (bytesUploaded, bytesTotal) => {
|
onProgress: (bytesUploaded, bytesTotal) => {
|
||||||
const progress = Number(
|
const progress = Number(
|
||||||
|
|
|
@ -78,7 +78,6 @@ model Staff {
|
||||||
|
|
||||||
phoneNumber String? @unique @map("phone_number")
|
phoneNumber String? @unique @map("phone_number")
|
||||||
|
|
||||||
|
|
||||||
domainId String? @map("domain_id")
|
domainId String? @map("domain_id")
|
||||||
deptId String? @map("dept_id")
|
deptId String? @map("dept_id")
|
||||||
|
|
||||||
|
@ -271,9 +270,7 @@ model Resource {
|
||||||
type String? @map("type")
|
type String? @map("type")
|
||||||
fileId String? @unique
|
fileId String? @unique
|
||||||
url String?
|
url String?
|
||||||
// 元数据
|
meta Json? @map("meta")
|
||||||
metadata Json? @map("metadata")
|
|
||||||
// 处理状态控制
|
|
||||||
status String?
|
status String?
|
||||||
createdAt DateTime? @default(now()) @map("created_at")
|
createdAt DateTime? @default(now()) @map("created_at")
|
||||||
updatedAt DateTime? @updatedAt @map("updated_at")
|
updatedAt DateTime? @updatedAt @map("updated_at")
|
||||||
|
|
|
@ -130,6 +130,55 @@ export type PostComment = {
|
||||||
avatar: string;
|
avatar: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface BaseMetadata {
|
||||||
|
size: number
|
||||||
|
filetype: string
|
||||||
|
filename: string
|
||||||
|
extension: string
|
||||||
|
modifiedAt: Date
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 图片特有元数据接口
|
||||||
|
*/
|
||||||
|
export interface ImageMetadata {
|
||||||
|
width: number; // 图片宽度(px)
|
||||||
|
height: number; // 图片高度(px)
|
||||||
|
compressedUrl?: string;
|
||||||
|
orientation?: number; // EXIF方向信息
|
||||||
|
space?: string; // 色彩空间 (如: RGB, CMYK)
|
||||||
|
hasAlpha?: boolean; // 是否包含透明通道
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 视频特有元数据接口
|
||||||
|
*/
|
||||||
|
export interface VideoMetadata {
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
duration?: number;
|
||||||
|
videoCodec?: string;
|
||||||
|
audioCodec?: string;
|
||||||
|
coverUrl?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 音频特有元数据接口
|
||||||
|
*/
|
||||||
|
export interface AudioMetadata {
|
||||||
|
duration: number; // 音频时长(秒)
|
||||||
|
bitrate?: number; // 比特率(bps)
|
||||||
|
sampleRate?: number; // 采样率(Hz)
|
||||||
|
channels?: number; // 声道数
|
||||||
|
codec?: string; // 音频编码格式
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export type FileMetadata = ImageMetadata & VideoMetadata & AudioMetadata & BaseMetadata
|
||||||
|
|
||||||
|
export type ResourceDto = Resource & {
|
||||||
|
meta: FileMetadata
|
||||||
|
}
|
||||||
export type PostDto = Post & {
|
export type PostDto = Post & {
|
||||||
readed: boolean;
|
readed: boolean;
|
||||||
liked: boolean;
|
liked: boolean;
|
||||||
|
@ -139,7 +188,7 @@ export type PostDto = Post & {
|
||||||
terms: TermDto[];
|
terms: TermDto[];
|
||||||
author: StaffDto | undefined;
|
author: StaffDto | undefined;
|
||||||
receivers: StaffDto[];
|
receivers: StaffDto[];
|
||||||
resources: Resource[];
|
resources: ResourceDto[];
|
||||||
perms?: {
|
perms?: {
|
||||||
delete: boolean;
|
delete: boolean;
|
||||||
// edit: boolean;
|
// edit: boolean;
|
||||||
|
|
|
@ -21,5 +21,11 @@ export function generateUniqueId(prefix?: string): string {
|
||||||
// 如果提供了前缀,则添加前缀
|
// 如果提供了前缀,则添加前缀
|
||||||
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`;
|
||||||
|
};
|
||||||
|
|
||||||
export * from "./types"
|
export * from "./types"
|
Loading…
Reference in New Issue