add
This commit is contained in:
parent
3bb7b74695
commit
7ec9f7b97b
|
|
@ -82,6 +82,9 @@ export class PostRouter {
|
|||
.mutation(async ({ input }) => {
|
||||
return await this.postService.deleteMany(input);
|
||||
}),
|
||||
getPostCount: this.trpc.procedure.query(async ({ ctx, input }) => {
|
||||
return await this.postService.getPostCount();
|
||||
}),
|
||||
findManyWithCursor: this.trpc.protectProcedure
|
||||
.input(
|
||||
z.object({
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {
|
|||
ResPerm,
|
||||
ObjectType,
|
||||
PostType,
|
||||
PostState,
|
||||
} from '@nice/common';
|
||||
import { MessageService } from '../message/message.service';
|
||||
import { BaseService } from '../base/base.service';
|
||||
|
|
@ -205,4 +206,23 @@ export class PostService extends BaseService<Prisma.PostDelegate> {
|
|||
data: {}, // 空对象会自动更新 updatedAt 时间戳
|
||||
});
|
||||
}
|
||||
async getPostCount() {
|
||||
const receiveCount = await db.post.count({
|
||||
where: {
|
||||
type: PostType.POST,
|
||||
deletedAt: null,
|
||||
},
|
||||
});
|
||||
const resolvedCount = await db.post.count({
|
||||
where: {
|
||||
type: PostType.POST,
|
||||
deletedAt: null,
|
||||
state: PostState.RESOLVED,
|
||||
},
|
||||
});
|
||||
return {
|
||||
receiveCount,
|
||||
resolvedCount,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import {
|
|||
ObjectType,
|
||||
UserProfile,
|
||||
Prisma,
|
||||
PostType,
|
||||
} from '@nice/common';
|
||||
import { DepartmentService } from '../department/department.service';
|
||||
import { z } from 'zod';
|
||||
|
|
@ -12,6 +13,7 @@ import { BaseService } from '../base/base.service';
|
|||
import * as argon2 from 'argon2';
|
||||
import EventBus, { CrudOperation } from '@server/utils/event-bus';
|
||||
import { DefaultArgs } from '@prisma/client/runtime/library';
|
||||
import { setPostRelation } from './utils';
|
||||
|
||||
@Injectable()
|
||||
export class StaffService extends BaseService<Prisma.StaffDelegate> {
|
||||
|
|
@ -137,7 +139,17 @@ export class StaffService extends BaseService<Prisma.StaffDelegate> {
|
|||
in: childDepts,
|
||||
};
|
||||
}
|
||||
|
||||
return super.findManyWithPagination(args as any);
|
||||
return this.wrapResult(
|
||||
super.findManyWithPagination(args as any),
|
||||
async (result) => {
|
||||
const { items } = result;
|
||||
await Promise.all(
|
||||
items.map(async (item) => {
|
||||
await setPostRelation({ data: item });
|
||||
}),
|
||||
);
|
||||
return { ...result, items };
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,37 @@
|
|||
import { db, PostState, PostType, Staff, UserProfile } from '@nice/common';
|
||||
|
||||
export async function setPostRelation(params: {
|
||||
data: Staff;
|
||||
staff?: UserProfile;
|
||||
}) {
|
||||
const { data, staff } = params;
|
||||
// 在函数开始时计算一次时间
|
||||
const replyCount = await db.post.count({
|
||||
where: {
|
||||
type: PostType.POST,
|
||||
receivers: {
|
||||
some: {
|
||||
id: data?.id,
|
||||
},
|
||||
},
|
||||
state: PostState.RESOLVED,
|
||||
},
|
||||
});
|
||||
const receiveCount = await db.post.count({
|
||||
where: {
|
||||
type: PostType.POST,
|
||||
receivers: {
|
||||
some: {
|
||||
id: data?.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Object.assign(data, {
|
||||
replyCount,
|
||||
receiveCount,
|
||||
});
|
||||
// console.log('data', data);
|
||||
return data; // 明确返回修改后的数据
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
VITE_APP_SERVER_IP=192.168.112.239
|
||||
VITE_APP_UOLOAD_PORT=80
|
||||
VITE_APP_UPLOAD_PORT=80
|
||||
VITE_APP_SERVER_PORT=3000
|
||||
VITE_APP_VERSION=0.3.0
|
||||
VITE_APP_APP_NAME=信箱
|
||||
|
|
@ -8,7 +8,7 @@
|
|||
window.env = {
|
||||
VITE_APP_SERVER_IP: "$VITE_APP_SERVER_IP",
|
||||
VITE_APP_SERVER_PORT: "$VITE_APP_SERVER_PORT",
|
||||
VITE_APP_UOLOAD_PORT: "$VITE_APP_UOLOAD_PORT",
|
||||
VITE_APP_UPLOAD_PORT: "$VITE_APP_UPLOAD_PORT",
|
||||
VITE_APP_APP_NAME: "$VITE_APP_APP_NAME",
|
||||
VITE_APP_VERSION: "$VITE_APP_VERSION",
|
||||
};
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 71 KiB |
|
|
@ -114,6 +114,9 @@ export default function BaseSettingPage() {
|
|||
<Input></Input>
|
||||
</Form.Item>
|
||||
</div>
|
||||
<Form.Item label="写信须知" name={["appConfig", "notice"]}>
|
||||
<Input.TextArea></Input.TextArea>
|
||||
</Form.Item>
|
||||
<div className="p-2 grid grid-cols-8 gap-2 border-b">
|
||||
<Form.Item
|
||||
label="网站logo"
|
||||
|
|
|
|||
|
|
@ -68,13 +68,13 @@ export const RegisterForm = ({ onSubmit, isLoading }: RegisterFormProps) => {
|
|||
</Form.Item>
|
||||
<Form.Item
|
||||
name="showname"
|
||||
label="姓名"
|
||||
label="名称"
|
||||
noStyle
|
||||
rules={[
|
||||
{ required: true, message: "请输入姓名" },
|
||||
{ min: 2, message: "姓名至少需要2个字符" },
|
||||
{ required: true, message: "请输入名称" },
|
||||
{ min: 2, message: "名称至少需要2个字符" },
|
||||
]}>
|
||||
<Input placeholder="姓名" />
|
||||
<Input placeholder="名称" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
noStyle
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
export function Header() {
|
||||
return (
|
||||
<header className="bg-gradient-to-r from-primary to-primary-400 p-6 rounded-t-xl">
|
||||
<header className="bg-gradient-to-r from-primary-500/80 to-primary-400 p-6 rounded-t-xl">
|
||||
<div className="flex flex-col space-y-6">
|
||||
{/* 主标题区域 */}
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-wider text-white">
|
||||
我收到的信件
|
||||
我的信件
|
||||
</h1>
|
||||
<p className="mt-2 text-blue-100 text-lg">
|
||||
{/* <p className="mt-2 text-blue-100 text-lg">
|
||||
及时查看 • 快速处理 • 高效反馈
|
||||
</p>
|
||||
</p> */}
|
||||
</div>
|
||||
|
||||
{/* 服务特点说明 */}
|
||||
|
|
@ -60,14 +60,6 @@ export function Header() {
|
|||
<span>高效反馈处理结果</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 服务宗旨说明 */}
|
||||
<div className="text-sm text-blue-100 border-t border-blue-400/30 pt-4">
|
||||
<p className="leading-relaxed">
|
||||
我们确保您能够及时查看、快速处理收到的信件,
|
||||
并通过高效反馈机制,提升沟通效率,助力工作顺利开展。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,8 +1,13 @@
|
|||
import LetterList from "@web/src/components/models/post/list/LetterList";
|
||||
import { Header } from "./Header";
|
||||
import { useAuth } from "@web/src/providers/auth-provider";
|
||||
import { useMemo } from "react";
|
||||
import { PostType, RolePerms } from "@nice/common";
|
||||
export default function InboxPage() {
|
||||
const { user } = useAuth();
|
||||
const { user, hasSomePermissions } = useAuth();
|
||||
const readAnyPost = useMemo(() => {
|
||||
return hasSomePermissions(RolePerms.READ_ANY_POST);
|
||||
}, []);
|
||||
return (
|
||||
// 添加 flex flex-col 使其成为弹性布局容器
|
||||
<div className="min-h-screen shadow-elegant border-2 border-white rounded-xl overflow-hidden bg-slate-200">
|
||||
|
|
@ -12,11 +17,15 @@ export default function InboxPage() {
|
|||
<LetterList
|
||||
params={{
|
||||
where: {
|
||||
receivers: {
|
||||
some: {
|
||||
id: user?.id,
|
||||
},
|
||||
},
|
||||
type: PostType.POST,
|
||||
deletedAt: null,
|
||||
receivers: !readAnyPost
|
||||
? {
|
||||
some: {
|
||||
id: user?.id,
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
}}></LetterList>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,8 +4,6 @@ import LetterListPage from "../list/page";
|
|||
|
||||
export default function IndexPage() {
|
||||
const { user } = useAuth();
|
||||
if (user) {
|
||||
return <InboxPage></InboxPage>;
|
||||
}
|
||||
|
||||
return <LetterListPage></LetterListPage>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,71 +1,22 @@
|
|||
import { useAppConfig } from "@nice/client";
|
||||
|
||||
export function Header() {
|
||||
return (
|
||||
<header className="bg-gradient-to-r from-primary to-primary-400 p-6 rounded-t-xl">
|
||||
<div className="flex flex-col space-y-6">
|
||||
{/* 主标题区域 */}
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-wider text-white">公开信件</h1>
|
||||
<p className="mt-2 text-blue-100 text-lg">
|
||||
透明公开 • 及时反馈 • 有效解决
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 服务特点说明 */}
|
||||
<div className="flex flex-wrap gap-6 text-sm text-white">
|
||||
<div className="flex items-center gap-2">
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
||||
/>
|
||||
</svg>
|
||||
<span>公开透明处理流程</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
<span>及时跟进反馈进度</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"
|
||||
/>
|
||||
</svg>
|
||||
<span>高效解决实际问题</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 服务宗旨说明 */}
|
||||
<div className="text-sm text-blue-100 border-t border-blue-400/30 pt-4">
|
||||
<p className="leading-relaxed">
|
||||
我们致力于构建开放透明的信件处理平台,通过公开信件处理过程,广泛吸取意见建议,促进各项工作有序开展,切实解决实际问题,提升单位整体战斗力。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
const { notice } = useAppConfig();
|
||||
return (
|
||||
<header className="bg-gradient-to-r from-primary-500/80 to-primary-400 p-6 rounded-t-xl">
|
||||
<div className="flex flex-col space-y-3">
|
||||
{/* 主标题区域 */}
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-wider text-white ">
|
||||
写信须知
|
||||
</h1>
|
||||
</div>
|
||||
<div className="text-xl text-blue-100 border-t border-blue-400/30 pt-4">
|
||||
<p className="leading-relaxed text-blue-100 whitespace-pre-wrap">
|
||||
{notice}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,62 @@
|
|||
import React from "react";
|
||||
import { UserOutlined } from "@ant-design/icons";
|
||||
import { Button } from "antd";
|
||||
|
||||
export interface StaffCardProps {
|
||||
staff: {
|
||||
id?: string;
|
||||
showname?: string;
|
||||
username?: string;
|
||||
meta?: {
|
||||
photoUrl?: string;
|
||||
rank?: string;
|
||||
};
|
||||
replyCount?: number;
|
||||
receiveCount?: number;
|
||||
};
|
||||
}
|
||||
|
||||
export function StaffCard({ staff }: StaffCardProps) {
|
||||
const handleSendLetter = () => {
|
||||
window.open(`/editor?&receiverId=${staff.id}`, "_blank");
|
||||
};
|
||||
return (
|
||||
<div className="bg-slate-100 border border-white rounded-lg overflow-hidden flex transition-transform duration-300 hover:translate-y-[-5px] hover:shadow-lg">
|
||||
{/* Image Container */}
|
||||
<div className="w-24 h-32 flex-shrink-0 flex items-center justify-center relative">
|
||||
{staff.meta?.photoUrl ? (
|
||||
<img
|
||||
src={staff.meta.photoUrl}
|
||||
alt={staff?.showname || staff?.username}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center text-gray-400 w-full h-full bg-gray-200">
|
||||
<UserOutlined style={{ fontSize: "32px" }} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Content Container */}
|
||||
<div className="flex-1 p-4 flex flex-col justify-center gap-2">
|
||||
<h3 className="text-2xl font-semibold text-primary">
|
||||
{staff?.showname || staff?.username || "未知"}
|
||||
</h3>
|
||||
<div className=" flex justify-start gap-2 items-center text-base text-primary">
|
||||
<div className="">
|
||||
答复 {staff?.replyCount} / 收信 {staff?.receiveCount}
|
||||
</div>
|
||||
<Button
|
||||
size="small"
|
||||
onClick={handleSendLetter}
|
||||
type="primary"
|
||||
style={{
|
||||
boxShadow: "none",
|
||||
}}>
|
||||
我要写信
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
import React from "react";
|
||||
import { Typography } from "antd";
|
||||
import { useAppConfig } from "@nice/client";
|
||||
|
||||
const { Paragraph } = Typography;
|
||||
|
||||
const TopNotificationInput = () => {
|
||||
const { notice } = useAppConfig();
|
||||
|
||||
return (
|
||||
<div className="top-notification-wrapper">
|
||||
<Paragraph
|
||||
className="top-notification-text "
|
||||
style={{
|
||||
whiteSpace: "pre-wrap",
|
||||
padding: 40,
|
||||
fontSize: 22,
|
||||
fontWeight: "bold",
|
||||
}}>
|
||||
{notice}
|
||||
</Paragraph>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TopNotificationInput;
|
||||
|
|
@ -1,21 +1,54 @@
|
|||
import LetterList from "@web/src/components/models/post/list/LetterList";
|
||||
import { Header } from "./Header";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { SendCard } from "../write/SendCard";
|
||||
import { StaffCard } from "./StaffCard";
|
||||
import StaffList from "@web/src/components/models/staff/staff-list";
|
||||
import SendStaffList from "@web/src/components/models/post/list/SendStaffList";
|
||||
import { PostType } from "@nice/common";
|
||||
import TopNotificationInput from "./TopNotification";
|
||||
|
||||
export default function LetterListPage() {
|
||||
const [params] = useSearchParams()
|
||||
const keyword = params.get("keyword")
|
||||
return (
|
||||
// 添加 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 />
|
||||
{/* 添加 flex-grow 使内容区域自动填充剩余空间 */}
|
||||
const [params] = useSearchParams();
|
||||
const keyword = params.get("keyword");
|
||||
const mockStaff = {
|
||||
id: "12345",
|
||||
showname: "张三",
|
||||
username: "zhangsan",
|
||||
phoneNumber: "123-4567-8901",
|
||||
department: {
|
||||
name: "工程部",
|
||||
},
|
||||
meta: {
|
||||
photoUrl: "https://example.com/zhangsan-photo.jpg",
|
||||
rank: "高级工程师",
|
||||
email: "zhangsan@example.com",
|
||||
office: "A栋101室",
|
||||
},
|
||||
};
|
||||
|
||||
<LetterList search={keyword} params={{
|
||||
where: {
|
||||
isPublic: true
|
||||
}
|
||||
}}></LetterList>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className="min-h-screen ">
|
||||
{/* Left side - 3/4 width */}
|
||||
<TopNotificationInput></TopNotificationInput>
|
||||
<div className="flex gap-4 mt-4">
|
||||
<div className="w-4/5 shadow-elegant border-2 border-white rounded-xl overflow-hidden bg-slate-200 flex flex-col">
|
||||
<LetterList
|
||||
search={keyword}
|
||||
params={{
|
||||
where: {
|
||||
deletedAt: null,
|
||||
type: PostType.POST,
|
||||
},
|
||||
}}></LetterList>
|
||||
</div>
|
||||
{/* Right side - 1/4 width */}
|
||||
<div className="w-1/5">
|
||||
{/* <Staff */}
|
||||
<SendStaffList></SendStaffList>
|
||||
{/* Add your content for the right side here */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ export default function OutboxPage() {
|
|||
<LetterList
|
||||
params={{
|
||||
where: {
|
||||
deletedAt: null,
|
||||
authorId: user?.id,
|
||||
},
|
||||
}}></LetterList>
|
||||
|
|
|
|||
|
|
@ -1,70 +1,66 @@
|
|||
export default function ProgressHeader() {
|
||||
return <header className=" rounded-t-xl bg-gradient-to-r from-primary to-primary-400 text-white p-6">
|
||||
<div className="flex flex-col space-y-6">
|
||||
{/* 主标题 */}
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-wider">
|
||||
查询处理进度
|
||||
</h1>
|
||||
<p className="mt-2 text-blue-100 text-lg">
|
||||
实时跟踪您的信件处理状态
|
||||
</p>
|
||||
</div>
|
||||
return (
|
||||
<header className=" rounded-t-xl bg-gradient-to-r from-primary-500/80 to-primary-400 text-white p-6">
|
||||
<div className="flex flex-col space-y-6">
|
||||
{/* 主标题 */}
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-wider">
|
||||
查询处理进度
|
||||
</h1>
|
||||
{/* <p className="mt-2 text-blue-100 text-lg">
|
||||
实时跟踪您的信件处理状态
|
||||
</p> */}
|
||||
</div>
|
||||
|
||||
{/* 处理状态说明 */}
|
||||
<div className="flex flex-wrap gap-6 text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
|
||||
/>
|
||||
</svg>
|
||||
<span>身份信息已加密</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
||||
/>
|
||||
</svg>
|
||||
<span>全程跟踪处理状态</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M8 11V7a4 4 0 118 0m-4 8v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
<span>处理结果及时反馈</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 处理说明 */}
|
||||
<div className="text-sm text-blue-100 border-t border-blue-400/30 pt-4">
|
||||
<p>您可以随时查看问题的处理进度,我们会及时更新处理状态并通知您最新进展。所有信息都经过加密保护,确保处理过程的安全性。</p>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
}
|
||||
{/* 处理状态说明 */}
|
||||
<div className="flex flex-wrap gap-6 text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
|
||||
/>
|
||||
</svg>
|
||||
<span>身份信息已加密</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
||||
/>
|
||||
</svg>
|
||||
<span>全程跟踪处理状态</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M8 11V7a4 4 0 118 0m-4 8v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
<span>处理结果及时反馈</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,72 +1,68 @@
|
|||
import { TermDto } from "@nice/common";
|
||||
|
||||
export default function WriteHeader({ term }: { term?: TermDto }) {
|
||||
return <header className=" rounded-t-xl bg-gradient-to-r from-primary to-primary-400 text-white p-6">
|
||||
<div className="flex flex-col space-y-6">
|
||||
{/* 主标题 */}
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-wider">
|
||||
{term?.name}信件投递入口
|
||||
</h1>
|
||||
<p className="mt-2 text-blue-100 text-lg">
|
||||
保护您隐私的信件传输平台
|
||||
</p>
|
||||
</div>
|
||||
return (
|
||||
<header className=" rounded-t-xl bg-gradient-to-r from-primary-500/80 to-primary-400 text-white p-6">
|
||||
<div className="flex flex-col space-y-6">
|
||||
{/* 主标题 */}
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-wider">
|
||||
{term?.name}信件投递入口
|
||||
</h1>
|
||||
{/* <p className="mt-2 text-blue-100 text-lg">
|
||||
保护您隐私的信件传输平台
|
||||
</p> */}
|
||||
</div>
|
||||
|
||||
{/* 隐私保护说明 */}
|
||||
<div className="flex flex-wrap gap-6 text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
|
||||
/>
|
||||
</svg>
|
||||
<span>个人信息严格保密</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
||||
/>
|
||||
</svg>
|
||||
<span>支持匿名反映问题</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M8 11V7a4 4 0 118 0m-4 8v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
<span>网络数据加密存储</span>
|
||||
{/* 隐私保护说明 */}
|
||||
<div className="flex flex-wrap gap-6 text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
|
||||
/>
|
||||
</svg>
|
||||
<span>个人信息严格保密</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
||||
/>
|
||||
</svg>
|
||||
<span>支持匿名反映问题</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M8 11V7a4 4 0 118 0m-4 8v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
<span>网络数据加密存储</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 隐私承诺 */}
|
||||
<div className="text-sm text-blue-100 border-t border-blue-400/30 pt-4">
|
||||
<p>您的个人信息将被严格保密,不向任何第三方透露。您可以选择匿名反映问题,平台会自动过滤可能暴露身份的信息。</p>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useState, useCallback, useEffect} from "react";
|
||||
import { useState, useCallback, useEffect } from "react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { SendCard } from "./SendCard";
|
||||
|
|
@ -8,7 +8,7 @@ import DepartmentSelect from "@web/src/components/models/department/department-s
|
|||
import debounce from "lodash/debounce";
|
||||
import { SearchOutlined } from "@ant-design/icons";
|
||||
import WriteHeader from "./WriteHeader";
|
||||
import { RoleName, staffDetailSelect } from "@nice/common";
|
||||
import { RoleName, staffDetailSelect } from "@nice/common";
|
||||
|
||||
export default function WriteLetterPage() {
|
||||
const [searchParams] = useSearchParams();
|
||||
|
|
@ -20,7 +20,11 @@ export default function WriteLetterPage() {
|
|||
const { getTerm } = useTerm();
|
||||
const { data: enabledStaffIds, isLoading: roleMapIsLoading } =
|
||||
api.rolemap.getStaffIdsByRoleNames.useQuery({
|
||||
roleNames: [RoleName.Leader, RoleName.Organization],
|
||||
roleNames: [
|
||||
RoleName.Leader,
|
||||
RoleName.Organization,
|
||||
RoleName.RootAdmin,
|
||||
],
|
||||
});
|
||||
const { data, isLoading, error } =
|
||||
api.staff.findManyWithPagination.useQuery(
|
||||
|
|
@ -29,11 +33,12 @@ export default function WriteLetterPage() {
|
|||
pageSize,
|
||||
select: staffDetailSelect,
|
||||
where: {
|
||||
id: enabledStaffIds
|
||||
? {
|
||||
in: enabledStaffIds,
|
||||
}
|
||||
: undefined,
|
||||
id:
|
||||
enabledStaffIds?.filter(Boolean)?.length > 0
|
||||
? {
|
||||
in: enabledStaffIds,
|
||||
}
|
||||
: undefined,
|
||||
deptId: selectedDept,
|
||||
OR: [
|
||||
{
|
||||
|
|
@ -91,7 +96,7 @@ export default function WriteLetterPage() {
|
|||
prefix={
|
||||
<SearchOutlined className="text-gray-400" />
|
||||
}
|
||||
placeholder="搜索领导姓名或职级..."
|
||||
placeholder="搜索领导名称或职级..."
|
||||
onChange={debounce(
|
||||
(e) => setSearchQuery(e.target.value),
|
||||
300
|
||||
|
|
|
|||
|
|
@ -1,61 +1,58 @@
|
|||
import { useState, useMemo } from 'react';
|
||||
import { NavLink, matchPath, useLocation, useMatches } from 'react-router-dom';
|
||||
import { Layout, Menu, theme } from 'antd';
|
||||
import type { MenuProps } from 'antd';
|
||||
import { CustomRouteObject } from '@web/src/routes/types';
|
||||
import { useState, useMemo } from "react";
|
||||
import { NavLink, matchPath, useLocation, useMatches } from "react-router-dom";
|
||||
import { Layout, Menu, theme } from "antd";
|
||||
import type { MenuProps } from "antd";
|
||||
import { CustomRouteObject } from "@web/src/routes/types";
|
||||
|
||||
const { Sider } = Layout;
|
||||
const { useToken } = theme;
|
||||
|
||||
type SidebarProps = {
|
||||
routes: CustomRouteObject[];
|
||||
routes: CustomRouteObject[];
|
||||
};
|
||||
|
||||
export default function AdminSidebar({ routes }: SidebarProps) {
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
const { token } = useToken();
|
||||
let matches = useMatches();
|
||||
console.log(matches)
|
||||
const menuItems: MenuProps['items'] = useMemo(() =>
|
||||
routes.map(route => ({
|
||||
key: route.path,
|
||||
icon: route.icon,
|
||||
label: (
|
||||
<NavLink
|
||||
to={route.path}
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
const { token } = useToken();
|
||||
const matches = useMatches();
|
||||
console.log(matches);
|
||||
const menuItems: MenuProps["items"] = useMemo(
|
||||
() =>
|
||||
routes.map((route) => ({
|
||||
key: route.path,
|
||||
icon: route.icon,
|
||||
label: <NavLink to={route.path}>{route.name}</NavLink>,
|
||||
})),
|
||||
[routes]
|
||||
);
|
||||
|
||||
>
|
||||
{route.name}
|
||||
</NavLink>
|
||||
),
|
||||
}))
|
||||
, [routes]);
|
||||
|
||||
return (
|
||||
<Sider
|
||||
collapsible
|
||||
collapsed={collapsed}
|
||||
onCollapse={(value) => setCollapsed(value)}
|
||||
width={150}
|
||||
className="h-screen sticky top-0"
|
||||
style={{
|
||||
backgroundColor: token.colorBgContainer,
|
||||
borderRight: `1px solid ${token.colorBorderSecondary}`
|
||||
}}
|
||||
>
|
||||
<Menu
|
||||
theme="light"
|
||||
mode="inline"
|
||||
selectedKeys={routes
|
||||
.filter(route => matches.some(match => match.pathname.includes(route.path)))
|
||||
.map(route => route.path)}
|
||||
|
||||
items={menuItems}
|
||||
className="border-r-0"
|
||||
style={{
|
||||
borderRight: 0
|
||||
}}
|
||||
/>
|
||||
</Sider>
|
||||
);
|
||||
return (
|
||||
<Sider
|
||||
collapsible
|
||||
collapsed={collapsed}
|
||||
onCollapse={(value) => setCollapsed(value)}
|
||||
width={150}
|
||||
className="h-screen sticky top-0"
|
||||
style={{
|
||||
backgroundColor: token.colorBgContainer,
|
||||
borderRight: `1px solid ${token.colorBorderSecondary}`,
|
||||
}}>
|
||||
<Menu
|
||||
theme="light"
|
||||
mode="inline"
|
||||
selectedKeys={routes
|
||||
.filter((route) =>
|
||||
matches.some((match) =>
|
||||
match.pathname.includes(route.path)
|
||||
)
|
||||
)
|
||||
.map((route) => route.path)}
|
||||
items={menuItems}
|
||||
className="border-r-0"
|
||||
style={{
|
||||
borderRight: 0,
|
||||
}}
|
||||
/>
|
||||
</Sider>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -126,27 +126,15 @@ export default function StaffForm() {
|
|||
}}></AvatarUploader>
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="grid grid-cols-1 gap-2 flex-1">
|
||||
<Form.Item
|
||||
noStyle
|
||||
rules={[{ required: true }]}
|
||||
name={"username"}
|
||||
label="帐号">
|
||||
<Input
|
||||
placeholder="请输入用户名"
|
||||
allowClear
|
||||
autoComplete="new-username" // 使用非标准的自动完成值
|
||||
spellCheck={false}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
noStyle
|
||||
rules={[{ required: true }]}
|
||||
name={"showname"}
|
||||
label="姓名">
|
||||
label="名称">
|
||||
<Input
|
||||
placeholder="请输入姓名"
|
||||
placeholder="请输入名称"
|
||||
allowClear
|
||||
autoComplete="new-name" // 使用非标准的自动完成值
|
||||
spellCheck={false}
|
||||
|
|
@ -172,77 +160,17 @@ export default function StaffForm() {
|
|||
rules={[{ required: true }]}>
|
||||
<DepartmentSelect rootId={domainId} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item noStyle label="密码" name={"password"}>
|
||||
<Input.Password
|
||||
placeholder="修改密码"
|
||||
spellCheck={false}
|
||||
visibilityToggle
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-2 flex-1">
|
||||
<Form.Item noStyle name={"rank"}>
|
||||
<Input
|
||||
placeholder="请输入职级(可选)"
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
allowClear
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
// rules={[
|
||||
// {
|
||||
// required: false,
|
||||
// pattern: /^\d{5,18}$/,
|
||||
// message: "请输入正确的证件号(数字)",
|
||||
// },
|
||||
// ]}
|
||||
noStyle
|
||||
name={"officerId"}>
|
||||
<Input
|
||||
placeholder="请输入证件号(可选)"
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
allowClear
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
rules={[
|
||||
{
|
||||
required: false,
|
||||
pattern: /^\d{6,11}$/,
|
||||
message: "请输入正确的手机号(数字)",
|
||||
},
|
||||
]}
|
||||
noStyle
|
||||
name={"phoneNumber"}
|
||||
label="手机号">
|
||||
<Input
|
||||
placeholder="请输入手机号(可选)"
|
||||
autoComplete="new-phone" // 使用非标准的自动完成值
|
||||
spellCheck={false}
|
||||
allowClear
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item noStyle name={"email"}>
|
||||
<Input
|
||||
placeholder="请输入邮箱(可选)"
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
allowClear
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item noStyle name={"office"}>
|
||||
<Input
|
||||
placeholder="请输入办公地点(可选)"
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
allowClear
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item noStyle label="密码" name={"password"}>
|
||||
<Input.Password
|
||||
placeholder="修改密码"
|
||||
spellCheck={false}
|
||||
visibilityToggle
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -165,12 +165,9 @@ export function UserMenu() {
|
|||
|
||||
{/* 用户信息,显示在 Avatar 右侧 */}
|
||||
<div className="flex flex-col space-y-0.5 ml-3 items-start">
|
||||
<span className="text-sm font-semibold text-white">
|
||||
<span className="text-base text-primary flex items-center gap-1.5">
|
||||
{user?.showname || user?.username}
|
||||
</span>
|
||||
<span className="text-xs text-white flex items-center gap-1.5">
|
||||
{user?.department?.name}
|
||||
</span>
|
||||
</div>
|
||||
</motion.button>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,69 +1,77 @@
|
|||
import { PhoneOutlined, MailOutlined, CloudOutlined, HomeOutlined, FileSearchOutlined, FireTwoTone, FireOutlined } from '@ant-design/icons';
|
||||
import Logo from '../../common/element/Logo';
|
||||
import {
|
||||
PhoneOutlined,
|
||||
MailOutlined,
|
||||
CloudOutlined,
|
||||
HomeOutlined,
|
||||
FileSearchOutlined,
|
||||
FireTwoTone,
|
||||
FireOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import Logo from "../../common/element/Logo";
|
||||
|
||||
export function Footer() {
|
||||
return (
|
||||
<footer className="bg-gradient-to-b from-primary-600 to-primary-800 text-secondary-200">
|
||||
<div className="container mx-auto px-4 py-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
{/* 开发组织信息 */}
|
||||
<div className="text-center md:text-left space-y-2">
|
||||
<h3 className="text-white font-semibold text-sm flex items-center justify-center md:justify-start">
|
||||
创新高地 软件小组
|
||||
</h3>
|
||||
<p className="text-gray-400 text-xs italic">
|
||||
提供技术支持
|
||||
</p>
|
||||
</div>
|
||||
return (
|
||||
<footer className="bg-gradient-to-b from-primary-500 to-primary-500 text-white ">
|
||||
<div className="container mx-auto px-4 py-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
{/* 开发组织信息 */}
|
||||
<div className="text-center md:text-left space-y-2">
|
||||
<h3 className="text-white font-semibold text-sm flex items-center justify-center md:justify-start">
|
||||
创新高地 软件小组
|
||||
</h3>
|
||||
<p className="text-white text-xs italic">
|
||||
提供技术支持
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 联系方式 */}
|
||||
<div className="text-center space-y-2">
|
||||
<div className="flex items-center justify-center space-x-2">
|
||||
<PhoneOutlined className="text-gray-400" />
|
||||
<span className="text-gray-300 text-xs">628118</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-center space-x-2">
|
||||
<MailOutlined className="text-gray-400" />
|
||||
<span className="text-gray-300 text-xs">gcsjs6@tx3l.nb.kj</span>
|
||||
</div>
|
||||
</div>
|
||||
{/* 联系方式 */}
|
||||
<div className="text-center space-y-2">
|
||||
<div className="flex items-center justify-center space-x-2">
|
||||
<PhoneOutlined className="text-gray-400" />
|
||||
<span className="text-white text-xs">628118</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-center space-x-2">
|
||||
<MailOutlined className="text-gray-400" />
|
||||
<span className="text-white text-xs">
|
||||
gcsjs6@tx3l.nb.kj
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 系统链接 */}
|
||||
<div className="text-center md:text-right space-y-2">
|
||||
<div className="flex items-center justify-center md:justify-end space-x-4">
|
||||
<a
|
||||
href="https://27.57.72.21"
|
||||
className="text-gray-400 hover:text-white transition-colors"
|
||||
title="访问门户网站"
|
||||
>
|
||||
<HomeOutlined className="text-lg" />
|
||||
</a>
|
||||
<a
|
||||
href="https://27.57.72.14"
|
||||
className="text-gray-400 hover:text-white transition-colors"
|
||||
title="访问烽火青云"
|
||||
>
|
||||
<CloudOutlined className="text-lg" />
|
||||
</a>
|
||||
{/* 系统链接 */}
|
||||
<div className="text-center md:text-right space-y-2">
|
||||
<div className="flex items-center justify-center md:justify-end space-x-4">
|
||||
<a
|
||||
href="https://27.57.72.21"
|
||||
className="text-white hover:text-white transition-colors"
|
||||
title="访问门户网站">
|
||||
<HomeOutlined className="text-lg" />
|
||||
</a>
|
||||
<a
|
||||
href="https://27.57.72.14"
|
||||
className="text-white hover:text-white transition-colors"
|
||||
title="访问烽火青云">
|
||||
<CloudOutlined className="text-lg" />
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="http://27.57.72.38"
|
||||
className="text-gray-400 hover:text-white transition-colors"
|
||||
title="访问烽火律询"
|
||||
>
|
||||
<FileSearchOutlined className="text-lg" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<a
|
||||
href="http://27.57.72.38"
|
||||
className="text-white hover:text-white transition-colors"
|
||||
title="访问烽火律询">
|
||||
<FileSearchOutlined className="text-lg" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 版权信息 */}
|
||||
<div className="border-t border-gray-700/50 mt-4 pt-4 text-center">
|
||||
<p className="text-gray-400 text-xs">
|
||||
© {new Date().getFullYear()} 南天烽火. All rights reserved.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
{/* 版权信息 */}
|
||||
<div className="border-t border-gray-700/50 mt-4 pt-4 text-center">
|
||||
<p className="text-white text-xs">
|
||||
© {new Date().getFullYear()} 南天烽火. All rights
|
||||
reserved.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,34 +5,47 @@ import Navigation from "./navigation";
|
|||
import { useAuth } from "@web/src/providers/auth-provider";
|
||||
import { UserOutlined } from "@ant-design/icons";
|
||||
import { UserMenu } from "../element/usermenu/usermenu";
|
||||
import logo from "@web/src/assets/logo.png"
|
||||
import logo from "@web/src/assets/logo.png";
|
||||
import { env } from "@web/src/env";
|
||||
import { Button } from "antd";
|
||||
import usePublicImage from "@web/src/hooks/usePublicImage";
|
||||
export const Header = memo(function Header() {
|
||||
const { isAuthenticated } = useAuth();
|
||||
const navigate = useNavigate()
|
||||
const navigate = useNavigate();
|
||||
const { imageUrl } = usePublicImage("logo.png");
|
||||
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 shadow-lg bg-slate-50">
|
||||
<div className="mx-auto px-4">
|
||||
<div className="py-2 relative"> {/* 添加relative定位上下文 */}
|
||||
<div className="py-1 relative">
|
||||
{" "}
|
||||
{/* 添加relative定位上下文 */}
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
{/* 左侧logo部分 */}
|
||||
<div className="flex items-center flex-shrink-0"> {/* 防止压缩 */}
|
||||
<img className="w-24" src={logo} alt="logo" />
|
||||
<div>
|
||||
<span className="text-xl font-bold"> {env.APP_NAME || '信箱'}</span>
|
||||
<p className="text-sm text-secondary-50">
|
||||
聆怀若水,应语如风;纾难化困,践诺成春
|
||||
</p>
|
||||
<div className="flex items-center flex-shrink-0">
|
||||
{" "}
|
||||
{/* 防止压缩 */}
|
||||
<div className="text-xl font-bold text-primary-500/80 whitespace-nowrap">
|
||||
{env.APP_NAME}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* <SearchBar /> */}
|
||||
{/* 中间标题部分 */}
|
||||
{/* <div className="absolute left-1/2 top-1/2 transform -translate-x-1/2 -translate-y-1/2"></div> */}
|
||||
{/* 右侧登录部分 */}
|
||||
<div className="flex items-center flex-shrink-0"> {/* 防止压缩 */}
|
||||
<div className="flex items-center flex-shrink-0">
|
||||
{" "}
|
||||
{/* 防止压缩 */}
|
||||
{!isAuthenticated ? (
|
||||
<Button size="large" onClick={() => {
|
||||
navigate("/auth")
|
||||
}} type="primary" icon={<UserOutlined></UserOutlined>}>
|
||||
<Button
|
||||
className=" text-lg bg-primary-500/80 "
|
||||
style={{
|
||||
boxShadow: "none",
|
||||
}}
|
||||
onClick={() => {
|
||||
navigate("/auth");
|
||||
}}
|
||||
type="primary"
|
||||
icon={<UserOutlined></UserOutlined>}>
|
||||
登录
|
||||
</Button>
|
||||
) : (
|
||||
|
|
@ -41,13 +54,13 @@ export const Header = memo(function Header() {
|
|||
</div>
|
||||
|
||||
{/* 独立定位的搜索栏 */}
|
||||
<div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-full max-w-2xl px-4">
|
||||
<SearchBar />
|
||||
</div>
|
||||
{/* <div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-full max-w-2xl px-4">
|
||||
|
||||
</div> */}
|
||||
</div>
|
||||
</div>
|
||||
<Navigation />
|
||||
{/* <Navigation /> */}
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,32 +1,36 @@
|
|||
import { motion } from 'framer-motion'
|
||||
import { Outlet } from 'react-router-dom'
|
||||
import { Header } from './Header'
|
||||
import { Footer } from './Footer'
|
||||
import { useEffect } from 'react'
|
||||
import { env } from '@web/src/env'
|
||||
import { motion } from "framer-motion";
|
||||
import { Outlet } from "react-router-dom";
|
||||
import { Header } from "./Header";
|
||||
import { Footer } from "./Footer";
|
||||
import { useEffect } from "react";
|
||||
import { env } from "@web/src/env";
|
||||
import Navigation from "./navigation";
|
||||
import TopPic from "./TopPic";
|
||||
|
||||
export function MainLayout() {
|
||||
useEffect(() => {
|
||||
document.title = env.APP_NAME || '信箱'
|
||||
}, [])
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
{/* 顶部 Header */}
|
||||
<Header />
|
||||
useEffect(() => {
|
||||
document.title = env.APP_NAME || "信箱";
|
||||
}, []);
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.5 }}>
|
||||
{/* 顶部 Header */}
|
||||
<Header />
|
||||
{/* 主要内容区域 */}
|
||||
<main className="min-h-screen bg-slate-50">
|
||||
<TopPic></TopPic>
|
||||
<div className=" mx-auto px-4 pb-8 ">
|
||||
<div className="my-6">
|
||||
<Navigation />
|
||||
</div>
|
||||
<Outlet />
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* 主要内容区域 */}
|
||||
<main className="min-h-screen bg-slate-50">
|
||||
<div className=" mx-auto px-4 py-8">
|
||||
<Outlet />
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* 底部 Footer */}
|
||||
<Footer />
|
||||
</motion.div>
|
||||
)
|
||||
{/* 底部 Footer */}
|
||||
<Footer />
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
import usePublicImage from "@web/src/hooks/usePublicImage";
|
||||
|
||||
export default function TopPic() {
|
||||
const { imageUrl } = usePublicImage("logo.png");
|
||||
|
||||
return (
|
||||
<div className="w-full overflow-hidden">
|
||||
<img
|
||||
src={imageUrl}
|
||||
alt="Banner"
|
||||
className="w-full h-auto object-cover"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -26,81 +26,38 @@ export default function Navigation({ className }: NavigationProps) {
|
|||
};
|
||||
|
||||
return (
|
||||
<nav
|
||||
className={twMerge(
|
||||
"mt-4 rounded-t-xl bg-gradient-to-r from-primary-500 to-primary-600 shadow-lg",
|
||||
className
|
||||
)}>
|
||||
<div className="flex flex-col md:flex-row items-stretch">
|
||||
{/* Desktop Navigation */}
|
||||
<div className="hidden md:flex items-center px-6 py-2 w-full overflow-x-auto">
|
||||
<div className="flex space-x-6 min-w-max">
|
||||
{navItems.map((item) => (
|
||||
<NavLink
|
||||
key={item.to}
|
||||
to={item.to}
|
||||
className={({ isActive: active }) =>
|
||||
twMerge(
|
||||
"relative px-4 py-2.5 text-sm font-medium",
|
||||
"text-gray-300 hover:text-white",
|
||||
"transition-all duration-200 ease-out group",
|
||||
active && "text-white"
|
||||
)
|
||||
}>
|
||||
<span className="relative z-10 flex items-center gap-2 transition-transform group-hover:translate-y-[-1px]">
|
||||
{item.icon}
|
||||
<span className="tracking-wide">
|
||||
{item.label}
|
||||
</span>
|
||||
<nav className={twMerge("mt-4 w-full", className)}>
|
||||
<div className="flex justify-between">
|
||||
<div className="flex w-full space-x-4 overflow-x-auto scrollbar-none">
|
||||
{navItems.map((item) => (
|
||||
<NavLink
|
||||
key={item.to}
|
||||
to={item.to}
|
||||
className={({ isActive: active }) =>
|
||||
twMerge(
|
||||
"flex-1 px-6 py-4 text-2xl font-bold rounded-xl", // 调整字体大小和粗细
|
||||
"transition-all duration-200 ease-out",
|
||||
"bg-gradient-to-r from-primary-500/80 to-primary-400 text-white",
|
||||
"hover:from-primary-500 hover:to-primary-500 ",
|
||||
active &&
|
||||
"from-primary-600 to-primary-500 *:"
|
||||
)
|
||||
}>
|
||||
<span className="flex items-center justify-center gap-4">
|
||||
{" "}
|
||||
{/* 增加图标和文字间距 */}
|
||||
{item.icon &&
|
||||
React.cloneElement(
|
||||
item.icon as React.ReactElement,
|
||||
{ style: { fontSize: "28px" } }
|
||||
)}{" "}
|
||||
{/* 调整图标大小 */}
|
||||
<span className="tracking-wide">
|
||||
{item.label}
|
||||
</span>
|
||||
|
||||
{/* Active Indicator */}
|
||||
<span
|
||||
className={twMerge(
|
||||
"absolute bottom-0 left-1/2 h-[2px] bg-blue-400",
|
||||
"transition-all duration-300 ease-out",
|
||||
"transform -translate-x-1/2",
|
||||
isActive(item.to)
|
||||
? "w-full opacity-100"
|
||||
: "w-0 opacity-0 group-hover:w-1/2 group-hover:opacity-40"
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Hover Glow Effect */}
|
||||
<span
|
||||
className={twMerge(
|
||||
"absolute inset-0 rounded-lg bg-blue-400/0",
|
||||
"transition-all duration-300",
|
||||
"group-hover:bg-blue-400/5"
|
||||
)}
|
||||
/>
|
||||
</NavLink>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile Navigation */}
|
||||
<div className="md:hidden flex overflow-x-auto scrollbar-none px-4 py-2">
|
||||
<div className="flex space-x-4 min-w-max">
|
||||
{navItems.map((item) => (
|
||||
<NavLink
|
||||
key={item.to}
|
||||
to={item.to}
|
||||
className={({ isActive: active }) =>
|
||||
twMerge(
|
||||
"px-3 py-1.5 text-sm font-medium rounded-full",
|
||||
"transition-colors duration-200",
|
||||
"text-gray-300 hover:text-white",
|
||||
active && "bg-blue-500/20 text-white"
|
||||
)
|
||||
}>
|
||||
<span className="flex items-center gap-1.5">
|
||||
{item.icon}
|
||||
<span>{item.label}</span>
|
||||
</span>
|
||||
</NavLink>
|
||||
))}
|
||||
</div>
|
||||
</span>
|
||||
</NavLink>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,13 @@
|
|||
import { api } from "@nice/client";
|
||||
import { TaxonomySlug } from "@nice/common";
|
||||
import React, { useMemo } from "react";
|
||||
import { MailOutlined, SendOutlined } from "@ant-design/icons";
|
||||
import {
|
||||
FileSearchOutlined,
|
||||
FormOutlined,
|
||||
InboxOutlined,
|
||||
MailOutlined,
|
||||
SendOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import {
|
||||
FileTextOutlined,
|
||||
ScheduleOutlined,
|
||||
|
|
@ -10,6 +16,7 @@ import {
|
|||
TagsOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { useAuth } from "@web/src/providers/auth-provider";
|
||||
import { env } from "@web/src/env";
|
||||
|
||||
export interface NavItem {
|
||||
to: string;
|
||||
|
|
@ -28,58 +35,40 @@ export function useNavItem() {
|
|||
const navItems = useMemo(() => {
|
||||
// 定义固定的导航项
|
||||
const staticItems = {
|
||||
inbox: {
|
||||
to: user ? "/" : "/inbox",
|
||||
label: "我收到的",
|
||||
letterList: {
|
||||
to: "/",
|
||||
label: "全部来信",
|
||||
icon: <MailOutlined className="text-base" />,
|
||||
},
|
||||
outbox: {
|
||||
to: "/outbox",
|
||||
label: "我发出的",
|
||||
icon: <SendOutlined className="text-base" />,
|
||||
},
|
||||
letterList: {
|
||||
to: !user ? "/" : "/letter-list",
|
||||
label: "公开信件",
|
||||
icon: <FileTextOutlined className="text-base" />,
|
||||
editor: {
|
||||
to: "/editor",
|
||||
label: "我要写信",
|
||||
icon: <FormOutlined className="text-base" />,
|
||||
},
|
||||
letterProgress: {
|
||||
to: "/letter-progress",
|
||||
label: "进度查询",
|
||||
icon: <ScheduleOutlined className="text-base" />,
|
||||
label: "信件查询",
|
||||
icon: <FileSearchOutlined className="text-base" />,
|
||||
},
|
||||
inbox: {
|
||||
to: user ? "/inbox" : "auth",
|
||||
label: "我的信件",
|
||||
icon: <InboxOutlined className="text-base" />,
|
||||
},
|
||||
help: {
|
||||
to: env.LIB_URL || "27.57.72.38",
|
||||
label: "法规查询",
|
||||
icon: <QuestionCircleOutlined className="text-base" />,
|
||||
},
|
||||
// help: {
|
||||
// to: "/help",
|
||||
// label: "使用帮助",
|
||||
// icon: <QuestionCircleOutlined className="text-base" />
|
||||
// }
|
||||
};
|
||||
|
||||
if (!data) {
|
||||
return [
|
||||
user && staticItems.inbox,
|
||||
user && staticItems.outbox,
|
||||
staticItems.letterList,
|
||||
staticItems.letterProgress,
|
||||
// staticItems.help,
|
||||
].filter(Boolean);
|
||||
}
|
||||
|
||||
// 构建分类导航项
|
||||
const categoryItems = data.map((term) => ({
|
||||
to: `/write-letter?termId=${term.id}`,
|
||||
label: term.name,
|
||||
icon: <TagsOutlined className="text-base"></TagsOutlined>,
|
||||
}));
|
||||
|
||||
// 按照指定顺序返回导航项
|
||||
return [
|
||||
user && staticItems.inbox,
|
||||
user && staticItems.outbox,
|
||||
staticItems.letterList,
|
||||
staticItems.editor,
|
||||
staticItems.letterProgress,
|
||||
...categoryItems,
|
||||
// staticItems.help,
|
||||
staticItems.inbox,
|
||||
staticItems.help,
|
||||
].filter(Boolean);
|
||||
}, [data, user]);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,128 +1,139 @@
|
|||
import { PostState, PostStateLabels } from '@nice/common';
|
||||
import { PostState, PostStateLabels } from "@nice/common";
|
||||
import {
|
||||
ExclamationCircleOutlined,
|
||||
BulbOutlined,
|
||||
QuestionCircleOutlined,
|
||||
CommentOutlined,
|
||||
ClockCircleOutlined,
|
||||
SyncOutlined,
|
||||
CheckCircleOutlined,
|
||||
TagOutlined,
|
||||
} from '@ant-design/icons';
|
||||
ExclamationCircleOutlined,
|
||||
BulbOutlined,
|
||||
QuestionCircleOutlined,
|
||||
CommentOutlined,
|
||||
ClockCircleOutlined,
|
||||
SyncOutlined,
|
||||
CheckCircleOutlined,
|
||||
TagOutlined,
|
||||
CalendarOutlined,
|
||||
} from "@ant-design/icons";
|
||||
|
||||
export const BADGE_STYLES = {
|
||||
category: {
|
||||
complaint: {
|
||||
bg: "bg-orange-50",
|
||||
text: "text-orange-700",
|
||||
border: "border border-orange-100",
|
||||
category: {
|
||||
complaint: {
|
||||
bg: "bg-orange-50",
|
||||
text: "text-orange-700",
|
||||
border: "border border-orange-100",
|
||||
|
||||
icon: <ExclamationCircleOutlined className="text-orange-400 text-sm" />,
|
||||
icon: (
|
||||
<ExclamationCircleOutlined className="text-orange-400 text-sm" />
|
||||
),
|
||||
},
|
||||
suggestion: {
|
||||
bg: "bg-blue-50",
|
||||
text: "text-blue-700",
|
||||
border: "border border-blue-100",
|
||||
|
||||
},
|
||||
suggestion: {
|
||||
bg: "bg-blue-50",
|
||||
text: "text-blue-700",
|
||||
border: "border border-blue-100",
|
||||
icon: <BulbOutlined className="text-blue-400 text-sm" />,
|
||||
},
|
||||
request: {
|
||||
bg: "bg-purple-50",
|
||||
text: "text-purple-700",
|
||||
border: "border border-purple-100",
|
||||
|
||||
icon: <BulbOutlined className="text-blue-400 text-sm" />,
|
||||
icon: (
|
||||
<QuestionCircleOutlined className="text-purple-400 text-sm" />
|
||||
),
|
||||
},
|
||||
feedback: {
|
||||
bg: "bg-teal-50",
|
||||
text: "text-teal-700",
|
||||
border: "border border-teal-100",
|
||||
|
||||
},
|
||||
request: {
|
||||
bg: "bg-purple-50",
|
||||
text: "text-purple-700",
|
||||
border: "border border-purple-100",
|
||||
icon: <CommentOutlined className="text-teal-400 text-sm" />,
|
||||
},
|
||||
},
|
||||
state: {
|
||||
[PostState.PENDING]: {
|
||||
bg: "bg-yellow-50",
|
||||
text: "text-yellow-700",
|
||||
border: "border border-yellow-100",
|
||||
|
||||
icon: <QuestionCircleOutlined className="text-purple-400 text-sm" />,
|
||||
icon: <ClockCircleOutlined className="text-yellow-400 text-sm" />,
|
||||
},
|
||||
[PostState.PROCESSING]: {
|
||||
bg: "bg-blue-50",
|
||||
text: "text-blue-700",
|
||||
border: "border border-blue-100",
|
||||
|
||||
},
|
||||
feedback: {
|
||||
bg: "bg-teal-50",
|
||||
text: "text-teal-700",
|
||||
border: "border border-teal-100",
|
||||
icon: <SyncOutlined className="text-blue-400 text-sm" spin />,
|
||||
},
|
||||
[PostState.RESOLVED]: {
|
||||
bg: "bg-green-50",
|
||||
text: "text-green-700",
|
||||
border: "border border-green-100",
|
||||
|
||||
icon: <CommentOutlined className="text-teal-400 text-sm" />,
|
||||
icon: <CheckCircleOutlined className="text-green-400 text-sm" />,
|
||||
},
|
||||
},
|
||||
tag: {
|
||||
default: {
|
||||
bg: "bg-gray-50",
|
||||
text: "text-gray-700",
|
||||
border: "border border-gray-100",
|
||||
|
||||
},
|
||||
},
|
||||
state: {
|
||||
[PostState.PENDING]: {
|
||||
bg: "bg-yellow-50",
|
||||
text: "text-yellow-700",
|
||||
border: "border border-yellow-100",
|
||||
icon: <TagOutlined className="text-gray-400 text-sm" />,
|
||||
},
|
||||
},
|
||||
date: {
|
||||
default: {
|
||||
bg: "bg-gray-50",
|
||||
text: "text-gray-700",
|
||||
border: "border border-gray-100",
|
||||
|
||||
icon: <ClockCircleOutlined className="text-yellow-400 text-sm" />,
|
||||
|
||||
},
|
||||
[PostState.PROCESSING]: {
|
||||
bg: "bg-blue-50",
|
||||
text: "text-blue-700",
|
||||
border: "border border-blue-100",
|
||||
|
||||
icon: <SyncOutlined className="text-blue-400 text-sm" spin />,
|
||||
|
||||
},
|
||||
[PostState.RESOLVED]: {
|
||||
bg: "bg-green-50",
|
||||
text: "text-green-700",
|
||||
border: "border border-green-100",
|
||||
|
||||
icon: <CheckCircleOutlined className="text-green-400 text-sm" />,
|
||||
|
||||
},
|
||||
},
|
||||
tag: {
|
||||
default: {
|
||||
bg: "bg-gray-50",
|
||||
text: "text-gray-700",
|
||||
border: "border border-gray-100",
|
||||
|
||||
icon: <TagOutlined className="text-gray-400 text-sm" />,
|
||||
|
||||
}
|
||||
},
|
||||
icon: <CalendarOutlined className="text-gray-400 text-sm" />,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const getBadgeStyle = (
|
||||
type: keyof typeof BADGE_STYLES,
|
||||
value: string
|
||||
type: keyof typeof BADGE_STYLES,
|
||||
value: string
|
||||
) => {
|
||||
const style = type === 'tag'
|
||||
? BADGE_STYLES.tag.default
|
||||
: BADGE_STYLES[type][value as keyof (typeof BADGE_STYLES)[typeof type]] || {
|
||||
bg: "bg-gray-50",
|
||||
text: "text-gray-700",
|
||||
icon: <TagOutlined className="text-gray-400 text-sm" />,
|
||||
};
|
||||
const style =
|
||||
type === "tag"
|
||||
? BADGE_STYLES.tag.default
|
||||
: type === "date"
|
||||
? BADGE_STYLES.date.default
|
||||
: BADGE_STYLES[type][
|
||||
value as keyof (typeof BADGE_STYLES)[typeof type]
|
||||
] || {
|
||||
bg: "bg-gray-50",
|
||||
text: "text-gray-700",
|
||||
icon: <TagOutlined className="text-gray-400 text-sm" />,
|
||||
};
|
||||
|
||||
return style;
|
||||
return style;
|
||||
};
|
||||
|
||||
export function LetterBadge({
|
||||
type,
|
||||
value,
|
||||
className = "",
|
||||
type,
|
||||
value,
|
||||
className = "",
|
||||
}: {
|
||||
type: "category" | "state" | "tag";
|
||||
value: string;
|
||||
className?: string;
|
||||
type: "category" | "state" | "tag" | "date";
|
||||
value: string;
|
||||
className?: string;
|
||||
}) {
|
||||
if (!value) return null;
|
||||
if (!value) return null;
|
||||
|
||||
const style = getBadgeStyle(type, value);
|
||||
const style = getBadgeStyle(type, value);
|
||||
|
||||
return (
|
||||
<span
|
||||
className={`
|
||||
inline-flex items-center gap-2 px-2 py-1 rounded-full
|
||||
text-xs transition-all
|
||||
return (
|
||||
<span
|
||||
className={`
|
||||
inline-flex items-center gap-2 px-2 py-1 rounded-md
|
||||
text-base transition-all
|
||||
${style.bg} ${style.text} ${style.border}
|
||||
${className}
|
||||
`}>
|
||||
{style.icon}
|
||||
<span className="tracking-wide">
|
||||
{type === 'state' ? PostStateLabels[value] : value}
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
{style.icon}
|
||||
<span className="tracking-wide">
|
||||
{type === "state" ? PostStateLabels[value] : value}
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,82 +26,55 @@ export function LetterCard({ letter }: LetterCardProps) {
|
|||
<div
|
||||
onClick={() => {
|
||||
window.open(`/${letter.id}/detail`);
|
||||
// console.log(letter);
|
||||
}}
|
||||
className="cursor-pointer p-6 bg-slate-100/80 rounded-xl hover:ring-white hover:ring-1 transition-all
|
||||
duration-300 ease-in-out hover:-translate-y-0.5
|
||||
active:scale-[0.98] border border-white
|
||||
group relative overflow-hidden">
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className=" text-xl text-primary font-bold">
|
||||
{letter.title}
|
||||
</div>
|
||||
{/* Meta Info */}
|
||||
<div className="flex justify-between items-center text-sm text-gray-600 gap-4 flex-wrap">
|
||||
<div className="flex items-center gap-4 flex-1 min-w-[300px]">
|
||||
{letter.author?.department?.name && (
|
||||
<div className="flex items-center gap-2">
|
||||
<BankOutlined className="text-secondary-400 text-base" />
|
||||
<Text className="text-gray-600 font-medium">
|
||||
{letter.author?.department?.name}
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
<UserOutlined className="text-primary text-base" />
|
||||
<Text className="text-primary font-medium">
|
||||
{letter?.meta?.signature ||
|
||||
letter.author?.showname ||
|
||||
"匿名用户"}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
{letter.receivers.some((item) => item.showname) && (
|
||||
<div className="flex items-center gap-2">
|
||||
<MailOutlined className="text-primary-400 text-base" />
|
||||
<div className=" text-2xl text-primary font-bold flex justify-between gap-2">
|
||||
<div className="flex items-center gap-2 ">
|
||||
<MailOutlined className="" />
|
||||
{letter.receivers.some((item) => item?.showname) && (
|
||||
<div className="flex items-center gap-2 text-primary-400 ">
|
||||
<Tooltip
|
||||
title={letter?.receivers
|
||||
?.map((item) => item.showname)
|
||||
?.map((item) => `${item?.showname}信箱`)
|
||||
.filter(Boolean)
|
||||
.join(", ")}>
|
||||
<Text className="text-primary-400">
|
||||
<Text className="text-primary-400 text-2xl flex">
|
||||
<span>{"[ "}</span>
|
||||
{letter.receivers
|
||||
.map((item) => item.showname)
|
||||
.map(
|
||||
(item) =>
|
||||
`${item?.showname}信箱`
|
||||
)
|
||||
.filter(Boolean)
|
||||
.slice(0, 2)
|
||||
.join("、")}
|
||||
{letter.receivers.filter(
|
||||
(item) => item.showname
|
||||
(item) => `${item?.showname}信箱`
|
||||
).length > 2 && " 等"}
|
||||
<span>{" ]"}</span>
|
||||
</Text>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<CalendarOutlined className="text-secondary-400 text-base" />
|
||||
<Text className="text-gray-500">
|
||||
{dayjs(letter.createdAt).format("YYYY-MM-DD")}
|
||||
</Text>
|
||||
{letter.title}
|
||||
{/* 印章样式的"密"字 */}
|
||||
{!letter?.isPublic && (
|
||||
<div className=" bg-red-600 text-white px-2 py-1 flex justify-center items-center rounded-md text-base font-bold border-2 border-white shadow-md">
|
||||
密
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content Preview */}
|
||||
{letter.content && (
|
||||
<div className="flex-1 leading-relaxed text-sm">
|
||||
<div
|
||||
dangerouslySetInnerHTML={{ __html: letter.content }}
|
||||
className="line-clamp-2"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Badges & Interactions */}
|
||||
<div className="flex justify-between items-center ">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<LetterBadge type="state" value={letter.state} />
|
||||
{letter?.meta?.tags?.map((tag) => (
|
||||
<LetterBadge key={tag} type="tag" value={tag} />
|
||||
))}
|
||||
{letter.terms.map((term) => (
|
||||
<LetterBadge
|
||||
key={term.name}
|
||||
|
|
@ -109,16 +82,24 @@ export function LetterCard({ letter }: LetterCardProps) {
|
|||
value={term.name}
|
||||
/>
|
||||
))}
|
||||
<LetterBadge
|
||||
type="date"
|
||||
value={dayjs(letter.createdAt).format("YYYY-MM-DD")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
type="default"
|
||||
type="text"
|
||||
shape="round"
|
||||
style={{
|
||||
color: "#4b5563",
|
||||
}}
|
||||
icon={<EyeOutlined />}>
|
||||
<span className="mr-1">浏览量</span>
|
||||
{letter.views}
|
||||
</Button>
|
||||
|
||||
<PostHateButton post={letter as any}></PostHateButton>
|
||||
<PostLikeButton post={letter as any}></PostLikeButton>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -36,9 +36,16 @@ export default function PostCommentList() {
|
|||
where: {
|
||||
parentId: post?.id,
|
||||
type: PostType.POST_COMMENT,
|
||||
OR: [
|
||||
{ authorId: null }, // 允许 authorId 为 null
|
||||
{ authorId: { notIn: receiverIds } }, // 排除 receiverIds 中的 authorId
|
||||
AND: [
|
||||
{
|
||||
OR: [{ authorId: null }, { authorId: { not: null } }],
|
||||
},
|
||||
{
|
||||
OR: [
|
||||
{ authorId: null },
|
||||
{ authorId: { notIn: receiverIds } },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
select: postDetailSelect,
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import PostDetailLayout from "./layout/PostDetailLayout";
|
|||
export default function PostDetail({ id }: { id?: string }) {
|
||||
|
||||
return <PostDetailProvider editId={id}>
|
||||
|
||||
<PostDetailLayout></PostDetailLayout>
|
||||
</PostDetailProvider>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,11 +33,11 @@ export default function PostHateButton({ post }: { post: PostDto }) {
|
|||
return (
|
||||
<Button
|
||||
title={post?.hated ? "取消点踩" : "点踩"}
|
||||
type={post?.hated ? "primary" : "default"}
|
||||
type={"text"}
|
||||
style={{
|
||||
backgroundColor: post?.hated ? "#ff4d4f" : "#fff",
|
||||
borderColor: post?.hated ? "transparent" : "",
|
||||
color: post?.hated ? "#fff" : "#000",
|
||||
// backgroundColor: post?.hated ? "#ff4d4f" : "transparent",
|
||||
// borderColor: post?.hated ? "transparent" : "transparent",
|
||||
color: post?.hated ? "#ff4d4f" : "#4b5563",
|
||||
boxShadow: "none", // 去除阴影
|
||||
}}
|
||||
shape="round"
|
||||
|
|
|
|||
|
|
@ -33,10 +33,10 @@ export default function PostLikeButton({ post }: { post: PostDto }) {
|
|||
return (
|
||||
<Button
|
||||
title={post?.liked ? "取消点赞" : "点赞"}
|
||||
type={post?.liked ? "primary" : "default"}
|
||||
type={"text"}
|
||||
shape="round"
|
||||
style={{
|
||||
boxShadow: "none", // 去除阴影
|
||||
color: post?.liked ? "#1677ff" : "#4b5563",
|
||||
}}
|
||||
icon={post?.liked ? <LikeFilled /> : <LikeOutlined />}
|
||||
onClick={(e) => {
|
||||
|
|
|
|||
|
|
@ -1,35 +1,181 @@
|
|||
import React, { useContext } from "react";
|
||||
import React, { useContext, useMemo, useState } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import {
|
||||
LikeFilled,
|
||||
LikeOutlined,
|
||||
EyeOutlined,
|
||||
CommentOutlined,
|
||||
SendOutlined,
|
||||
DeleteFilled,
|
||||
DeleteOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { Button, Tooltip } from "antd/lib";
|
||||
import { PostDetailContext } from "../context/PostDetailContext";
|
||||
import PostLikeButton from "./PostLikeButton";
|
||||
import { Modal } from "antd";
|
||||
import { ExclamationCircleOutlined } from "@ant-design/icons";
|
||||
|
||||
const { confirm } = Modal;
|
||||
import PostResources from "../PostResources";
|
||||
import PostHateButton from "./PostHateButton";
|
||||
import { useAuth } from "@web/src/providers/auth-provider";
|
||||
import { RolePerms } from "@nice/common";
|
||||
import { usePost } from "@nice/client";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import toast from "react-hot-toast";
|
||||
import StaffSelect from "../../../staff/staff-select";
|
||||
import LimitedStaffSelect from "../../../staff/limited-staff-select";
|
||||
|
||||
export function StatsSection() {
|
||||
const { post } = useContext(PostDetailContext);
|
||||
const { hasSomePermissions } = useAuth();
|
||||
const [isResending, setIsResending] = useState(false);
|
||||
const [receiverIds, setReceiverIds] = useState(
|
||||
post?.receivers?.map((receiver) => receiver?.id)?.filter(Boolean)
|
||||
);
|
||||
const showResend = () => {
|
||||
setIsResending(true);
|
||||
};
|
||||
|
||||
async function handleResend() {
|
||||
try {
|
||||
await update.mutateAsync({
|
||||
where: {
|
||||
id: post?.id,
|
||||
},
|
||||
data: {
|
||||
receivers: {
|
||||
connect: receiverIds
|
||||
?.filter(Boolean)
|
||||
?.map((id) => ({ id })),
|
||||
},
|
||||
},
|
||||
});
|
||||
toast.success("分发成功");
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
setIsResending(false);
|
||||
}
|
||||
|
||||
const handleCancelResend = () => {
|
||||
setIsResending(false);
|
||||
};
|
||||
const navigate = useNavigate();
|
||||
const canEdit = useMemo(() => {
|
||||
return hasSomePermissions(RolePerms.MANAGE_ANY_POST);
|
||||
}, [hasSomePermissions, post]);
|
||||
const { update } = usePost();
|
||||
|
||||
const showDeleteConfirm = () => {
|
||||
confirm({
|
||||
title: "确定要删除这条记录吗?",
|
||||
icon: <ExclamationCircleOutlined />,
|
||||
content: "删除后将无法恢复",
|
||||
okText: "确定",
|
||||
okType: "danger",
|
||||
cancelText: "取消",
|
||||
centered: true, // 添加这一行
|
||||
onOk() {
|
||||
deleteById();
|
||||
},
|
||||
onCancel() {
|
||||
console.log("取消删除");
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
async function deleteById() {
|
||||
try {
|
||||
await update.mutateAsync({
|
||||
where: {
|
||||
id: post.id,
|
||||
},
|
||||
data: {
|
||||
deletedAt: new Date(),
|
||||
},
|
||||
});
|
||||
navigate(-1);
|
||||
toast.success("删除成功");
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
toast.error("删除失败");
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div
|
||||
|
||||
className="mt-6 flex flex-wrap gap-4 justify-end items-center">
|
||||
|
||||
<div className="mt-6 flex flex-wrap gap-4 justify-end items-center">
|
||||
<div className=" flex gap-2">
|
||||
<Button title="浏览量" type="default" shape="round" icon={<EyeOutlined />}>
|
||||
<span className="mr-1">浏览量</span>{post?.views}
|
||||
<Button
|
||||
title="浏览量"
|
||||
type="text"
|
||||
style={{
|
||||
color: "#4b5563",
|
||||
}}
|
||||
shape="round"
|
||||
icon={<EyeOutlined />}>
|
||||
<span className="mr-1">浏览量</span>
|
||||
{post?.views}
|
||||
</Button>
|
||||
<Button type="default" title="回复数" shape="round" icon={<CommentOutlined />}>
|
||||
<span className="mr-1">回复数</span>{post?.commentsCount}
|
||||
<Button
|
||||
type="text"
|
||||
style={{
|
||||
color: "#4b5563",
|
||||
}}
|
||||
title="回复数"
|
||||
shape="round"
|
||||
icon={<CommentOutlined />}>
|
||||
<span className="mr-1">回复数</span>
|
||||
{post?.commentsCount}
|
||||
</Button>
|
||||
|
||||
<PostHateButton post={post}></PostHateButton>
|
||||
<PostLikeButton post={post}></PostLikeButton>
|
||||
{canEdit && (
|
||||
<>
|
||||
<Button
|
||||
type="text"
|
||||
style={{
|
||||
color: "#00308a",
|
||||
}}
|
||||
shape="round"
|
||||
icon={<SendOutlined />}
|
||||
onClick={() => showResend()}>
|
||||
<span className="mr-1">分发</span>
|
||||
</Button>
|
||||
<Modal
|
||||
centered
|
||||
title="选择分发领导"
|
||||
visible={isResending}
|
||||
onOk={handleResend}
|
||||
onCancel={handleCancelResend}>
|
||||
<div className="flex flex-1 justify-center">
|
||||
<LimitedStaffSelect
|
||||
value={receiverIds}
|
||||
multiple
|
||||
onChange={(value) => {
|
||||
setReceiverIds(value as string[]);
|
||||
}}
|
||||
style={{
|
||||
width: "90%",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
)}
|
||||
{canEdit && (
|
||||
<Button
|
||||
type="text"
|
||||
style={{
|
||||
color: "#ff0000",
|
||||
}}
|
||||
onClick={showDeleteConfirm}
|
||||
shape="round"
|
||||
icon={<DeleteOutlined />}>
|
||||
<span className="mr-1">删除</span>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ export default function PostResources({ post }: { post: PostDto }) {
|
|||
|
||||
const sortedResources = post.resources
|
||||
.map((resource) => {
|
||||
const original = `http://${env.SERVER_IP}:${env.UOLOAD_PORT}/uploads/${resource.url}`;
|
||||
const original = `http://${env.SERVER_IP}:${env.UPLOAD_PORT}/uploads/${resource.url}`;
|
||||
const isImg = isImage(resource.url);
|
||||
return {
|
||||
...resource,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,19 @@
|
|||
import { api, usePost } from "@nice/client";
|
||||
import { Post, postDetailSelect, PostDto, UserProfile } from "@nice/common";
|
||||
import {
|
||||
Post,
|
||||
postDetailSelect,
|
||||
PostDto,
|
||||
RolePerms,
|
||||
UserProfile,
|
||||
} from "@nice/common";
|
||||
import { useAuth } from "@web/src/providers/auth-provider";
|
||||
import React, { createContext, ReactNode, useEffect, useState } from "react";
|
||||
import React, {
|
||||
createContext,
|
||||
ReactNode,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from "react";
|
||||
import { PostParams } from "@nice/client/src/singleton/DataHolder";
|
||||
|
||||
interface PostDetailContextType {
|
||||
|
|
@ -9,6 +21,8 @@ interface PostDetailContextType {
|
|||
post?: PostDto;
|
||||
isLoading?: boolean;
|
||||
user?: UserProfile;
|
||||
setKeyCode?: React.Dispatch<React.SetStateAction<string>>;
|
||||
canSee?: boolean;
|
||||
}
|
||||
interface PostFormProviderProps {
|
||||
children: ReactNode;
|
||||
|
|
@ -21,13 +35,14 @@ export function PostDetailProvider({
|
|||
children,
|
||||
editId,
|
||||
}: PostFormProviderProps) {
|
||||
const { user } = useAuth();
|
||||
const { user, hasSomePermissions } = useAuth();
|
||||
|
||||
const postParams = PostParams.getInstance();
|
||||
const queryParams = {
|
||||
where: { id: editId },
|
||||
select: postDetailSelect,
|
||||
};
|
||||
|
||||
const [keyCode, setKeyCode] = useState<string>("");
|
||||
useEffect(() => {
|
||||
if (editId) {
|
||||
postParams.addDetailItem(queryParams);
|
||||
|
|
@ -42,7 +57,23 @@ export function PostDetailProvider({
|
|||
const { data: post, isLoading }: { data: PostDto; isLoading: boolean } = (
|
||||
api.post.findFirst as any
|
||||
).useQuery(queryParams, { enabled: Boolean(editId) });
|
||||
|
||||
const canSee = useMemo(() => {
|
||||
if (hasSomePermissions(RolePerms.READ_ANY_POST)) {
|
||||
return true;
|
||||
} else if (
|
||||
post?.receivers?.map((receiver) => receiver.id)?.includes(user?.id)
|
||||
) {
|
||||
return true;
|
||||
} else if (!post?.isPublic) {
|
||||
if (keyCode === post?.id) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}, [keyCode, post]);
|
||||
return (
|
||||
<PostDetailContext.Provider
|
||||
value={{
|
||||
|
|
@ -50,6 +81,8 @@ export function PostDetailProvider({
|
|||
post,
|
||||
user,
|
||||
isLoading,
|
||||
canSee,
|
||||
setKeyCode,
|
||||
}}>
|
||||
{children}
|
||||
</PostDetailContext.Provider>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useContext, useEffect } from "react";
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { PostDetailContext } from "../context/PostDetailContext";
|
||||
import PostCommentEditor from "../PostCommentEditor";
|
||||
import PostCommentList from "../PostCommentList";
|
||||
|
|
@ -6,9 +6,11 @@ import { useVisitor } from "@nice/client";
|
|||
import { VisitType } from "@nice/common";
|
||||
import Header from "../PostHeader/Header";
|
||||
import Content from "../PostHeader/Content";
|
||||
import { Button, Input, Skeleton } from "antd";
|
||||
|
||||
export default function PostDetailLayout() {
|
||||
const { post, user } = useContext(PostDetailContext);
|
||||
const { post, user, canSee, setKeyCode, isLoading } =
|
||||
useContext(PostDetailContext);
|
||||
const { read } = useVisitor();
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -22,10 +24,48 @@ export default function PostDetailLayout() {
|
|||
});
|
||||
}
|
||||
}, [post]);
|
||||
return <>
|
||||
<Header></Header>
|
||||
<Content></Content>
|
||||
<PostCommentEditor></PostCommentEditor>
|
||||
<PostCommentList></PostCommentList>
|
||||
</>
|
||||
const [password, setPassword] = useState("");
|
||||
const handlePasswordSubmit = () => {
|
||||
setKeyCode(password);
|
||||
};
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div style={{ padding: "20px" }}>
|
||||
<Skeleton active avatar paragraph={{ rows: 4 }} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (!canSee) {
|
||||
return (
|
||||
<div
|
||||
className=" justify-center gap-2"
|
||||
style={{
|
||||
height: 400,
|
||||
}}>
|
||||
<div className=" mb-2 mt-8 flex gap-1 justify-center text-primary text-2xl font-bold flex-1">
|
||||
该信件受到密码保护!
|
||||
</div>
|
||||
<div className="flex gap-1 justify-center items-center flex-1 ">
|
||||
<Input
|
||||
placeholder="请输入密码"
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
style={{
|
||||
width: 400,
|
||||
}}
|
||||
/>
|
||||
<Button type="primary" onClick={handlePasswordSubmit}>
|
||||
确认
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Header></Header>
|
||||
<Content></Content>
|
||||
<PostCommentEditor></PostCommentEditor>
|
||||
<PostCommentList></PostCommentList>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ export interface LetterFormData {
|
|||
content: string;
|
||||
resources?: string[];
|
||||
receivers?: string[];
|
||||
terms?: string[];
|
||||
term?: string;
|
||||
isPublic?: boolean;
|
||||
signature?: string;
|
||||
meta: {
|
||||
|
|
@ -47,17 +47,18 @@ export function LetterFormProvider({
|
|||
try {
|
||||
console.log(data);
|
||||
const receivers = data?.receivers;
|
||||
const terms = data?.terms;
|
||||
const term = data?.term;
|
||||
delete data.receivers;
|
||||
delete data.terms;
|
||||
delete data.term;
|
||||
console.log("term", term);
|
||||
const result = await create.mutateAsync({
|
||||
data: {
|
||||
...data,
|
||||
type: PostType.POST,
|
||||
terms: {
|
||||
connect: (terms || [])?.filter(Boolean).map((id) => ({
|
||||
id,
|
||||
})),
|
||||
connect: {
|
||||
id: term,
|
||||
},
|
||||
},
|
||||
receivers: {
|
||||
connect: (receivers || [])
|
||||
|
|
@ -70,12 +71,12 @@ export function LetterFormProvider({
|
|||
isPublic: data?.isPublic,
|
||||
resources: data.resources?.length
|
||||
? {
|
||||
connect: (
|
||||
data.resources?.filter(Boolean) || []
|
||||
).map((fileId) => ({
|
||||
fileId,
|
||||
})),
|
||||
}
|
||||
connect: (
|
||||
data.resources?.filter(Boolean) || []
|
||||
).map((fileId) => ({
|
||||
fileId,
|
||||
})),
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
});
|
||||
|
|
@ -100,6 +101,8 @@ export function LetterFormProvider({
|
|||
duration: 5000, // 10秒
|
||||
}
|
||||
);
|
||||
|
||||
navigate(-1);
|
||||
// navigate(`/${result.id}/detail`, {
|
||||
// replace: true,
|
||||
// state: { scrollToTop: true },
|
||||
|
|
|
|||
|
|
@ -7,16 +7,29 @@ import StaffSelect from "../../../staff/staff-select";
|
|||
import TermSelect from "../../../term/term-select";
|
||||
import TabPane from "antd/es/tabs/TabPane";
|
||||
import toast from "react-hot-toast";
|
||||
import { api } from "@nice/client";
|
||||
import { RoleName } from "@nice/common";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
export function LetterBasicForm() {
|
||||
const { onSubmit, receiverId, termId, form } = useLetterEditor();
|
||||
const handleFinish = async (values: any) => {
|
||||
await onSubmit(values);
|
||||
};
|
||||
|
||||
const { data: enabledStaffIds, isLoading: roleMapIsLoading } =
|
||||
api.rolemap.getStaffIdsByRoleNames.useQuery({
|
||||
roleNames: [
|
||||
RoleName.Leader,
|
||||
RoleName.Organization,
|
||||
RoleName.RootAdmin,
|
||||
],
|
||||
});
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
await form.validateFields();
|
||||
form.submit();
|
||||
|
||||
// toast.success("提交成功!");
|
||||
} catch (error) {
|
||||
// 提取所有错误信息
|
||||
|
|
@ -47,15 +60,19 @@ export function LetterBasicForm() {
|
|||
onFinish={handleFinish}
|
||||
initialValues={{
|
||||
meta: { tags: [] },
|
||||
receivers: [receiverId],
|
||||
terms: [termId],
|
||||
receivers: [receiverId].filter(Boolean),
|
||||
// terms: [termId],
|
||||
isPublic: true,
|
||||
}}>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<Form.Item label="收件人" name={"receivers"}>
|
||||
<StaffSelect multiple placeholder="选择收信人员" />
|
||||
<StaffSelect
|
||||
multiple
|
||||
limitedIds={enabledStaffIds?.filter(Boolean)}
|
||||
placeholder="选择收信人员"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="分类" name={"terms"}>
|
||||
<Form.Item label="分类" name={"term"}>
|
||||
<TermSelect placeholder="选择信件分类" />
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
|
@ -69,7 +86,7 @@ export function LetterBasicForm() {
|
|||
/>
|
||||
</Form.Item>
|
||||
{/* Tags Input */}
|
||||
<Form.Item name={["meta", "tags"]} className="mb-6">
|
||||
{/* <Form.Item name={["meta", "tags"]} className="mb-6">
|
||||
<Select
|
||||
mode="tags"
|
||||
showSearch={false}
|
||||
|
|
@ -83,7 +100,7 @@ export function LetterBasicForm() {
|
|||
className="w-full"
|
||||
dropdownStyle={{ display: "none" }}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form.Item> */}
|
||||
|
||||
<Tabs defaultActiveKey="1">
|
||||
{/* Content Editor */}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,109 @@
|
|||
import { useState, useCallback, useEffect } from "react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
|
||||
import { Spin, Empty, Input, Alert, Pagination } from "antd";
|
||||
import { api, useTerm } from "@nice/client";
|
||||
import DepartmentSelect from "@web/src/components/models/department/department-select";
|
||||
import debounce from "lodash/debounce";
|
||||
import { SearchOutlined } from "@ant-design/icons";
|
||||
// import WriteHeader from "./WriteHeader";
|
||||
import { RoleName, staffDetailSelect } from "@nice/common";
|
||||
import { StaffCard } from "@web/src/app/main/letter/list/StaffCard";
|
||||
|
||||
export default function SendStaffList() {
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const pageSize = 15;
|
||||
const { data: enabledStaffIds, isLoading: roleMapIsLoading } =
|
||||
api.rolemap.getStaffIdsByRoleNames.useQuery({
|
||||
roleNames: [
|
||||
RoleName.Leader,
|
||||
RoleName.Organization,
|
||||
RoleName.RootAdmin,
|
||||
],
|
||||
});
|
||||
const { data, isLoading, error } =
|
||||
api.staff.findManyWithPagination.useQuery(
|
||||
{
|
||||
page: currentPage,
|
||||
pageSize,
|
||||
select: staffDetailSelect,
|
||||
where: {
|
||||
id: {
|
||||
in: enabledStaffIds?.filter(Boolean),
|
||||
},
|
||||
deletedAt: null,
|
||||
},
|
||||
orderBy: {
|
||||
order: "asc",
|
||||
},
|
||||
},
|
||||
{
|
||||
enabled: !roleMapIsLoading,
|
||||
}
|
||||
);
|
||||
|
||||
// Reset page when search or department changes
|
||||
|
||||
return (
|
||||
<div className="min-h-screen shadow-elegant rounded-xl ">
|
||||
{error && (
|
||||
<div className="mb-4 space-y-4">
|
||||
{
|
||||
<Alert
|
||||
message="加载失败"
|
||||
description="获取数据时出现错误,请刷新页面重试。"
|
||||
type="error"
|
||||
showIcon
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
<AnimatePresence>
|
||||
{isLoading ? (
|
||||
<div className="flex justify-center items-center py-12">
|
||||
<Spin size="large" tip="加载中..." />
|
||||
</div>
|
||||
) : data?.items.length > 0 ? (
|
||||
<motion.div
|
||||
className="grid grid-cols-1 gap-2"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}>
|
||||
{data?.items.map((item: any) => (
|
||||
<StaffCard key={item.id} staff={item} />
|
||||
))}
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.div
|
||||
className="text-center py-12"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}>
|
||||
<Empty
|
||||
description="没有找到匹配的收信人"
|
||||
className="py-12"
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Pagination */}
|
||||
{data?.items.length > pageSize && (
|
||||
<div className="flex justify-center mt-8">
|
||||
<Pagination
|
||||
current={currentPage}
|
||||
total={data?.totalPages || 0}
|
||||
pageSize={pageSize}
|
||||
onChange={(page) => {
|
||||
setCurrentPage(page);
|
||||
window.scrollTo(0, 0);
|
||||
}}
|
||||
showSizeChanger={false}
|
||||
showTotal={(total) => `共 ${total} 条记录`}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,118 +1,134 @@
|
|||
import AgServerTable from "@web/src/components/presentation/ag-server-table";
|
||||
import { ObjectType } from "@nice/common"
|
||||
import { ObjectType } from "@nice/common";
|
||||
import { ICellRendererParams } from "@ag-grid-community/core";
|
||||
import { Menu, MenuItem } from "@web/src/components/presentation/dropdown-menu";
|
||||
import { DeleteOutlined, EllipsisOutlined, PlusOutlined } from "@ant-design/icons";
|
||||
import {
|
||||
DeleteOutlined,
|
||||
EllipsisOutlined,
|
||||
PlusOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { ColDef, ValueGetterParams } from "@ag-grid-community/core";
|
||||
import { Button } from "antd";
|
||||
import DepartmentSelect from "../../department/department-select";
|
||||
import { useContext, useEffect } from "react";
|
||||
import { RoleEditorContext } from "./role-editor";
|
||||
import { useAuth } from "@web/src/providers/auth-provider";
|
||||
import { useRoleMap } from "@nice/client"
|
||||
import { useRoleMap } from "@nice/client";
|
||||
const OpreationRenderer = ({ props }: { props: ICellRendererParams }) => {
|
||||
const { deleteMany } = useRoleMap()
|
||||
return (
|
||||
<div>
|
||||
<Menu
|
||||
node={
|
||||
<EllipsisOutlined className=" hover:bg-textHover p-1 rounded" />
|
||||
}>
|
||||
<MenuItem
|
||||
label="移除"
|
||||
onClick={() => {
|
||||
deleteMany.mutateAsync({
|
||||
ids: [props?.data?.id],
|
||||
});
|
||||
}}
|
||||
icon={<DeleteOutlined></DeleteOutlined>}></MenuItem>
|
||||
</Menu>
|
||||
</div>
|
||||
);
|
||||
|
||||
const { deleteMany } = useRoleMap();
|
||||
return (
|
||||
<div>
|
||||
<Menu
|
||||
node={
|
||||
<EllipsisOutlined className=" hover:bg-textHover p-1 rounded" />
|
||||
}>
|
||||
<MenuItem
|
||||
label="移除"
|
||||
onClick={() => {
|
||||
deleteMany.mutateAsync({
|
||||
ids: [props?.data?.id],
|
||||
});
|
||||
}}
|
||||
icon={<DeleteOutlined></DeleteOutlined>}></MenuItem>
|
||||
</Menu>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default function AssignList() {
|
||||
const { user, hasSomePermissions } = useAuth();
|
||||
const { domainId, setModalOpen, role, setDomainId, canManageRole } =
|
||||
useContext(RoleEditorContext);
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
setDomainId?.(user.domainId);
|
||||
}
|
||||
}, [user]);
|
||||
const columnDefs: ColDef[] = [
|
||||
{
|
||||
headerName: "帐号",
|
||||
field: "staff.username",
|
||||
sort: "desc",
|
||||
valueGetter: (params: ValueGetterParams) => {
|
||||
return params.data?.staff_username;
|
||||
},
|
||||
filter: "agTextColumnFilter",
|
||||
maxWidth: 300,
|
||||
},
|
||||
{
|
||||
headerName: "姓名",
|
||||
field: "staff.showname",
|
||||
sort: "desc",
|
||||
valueGetter: (params: ValueGetterParams) => {
|
||||
return params.data?.staff_showname;
|
||||
},
|
||||
filter: "agTextColumnFilter",
|
||||
maxWidth: 300,
|
||||
},
|
||||
{
|
||||
headerName: "证件号",
|
||||
field: "staff.officer_id",
|
||||
sort: "desc",
|
||||
valueGetter: (params: ValueGetterParams) => {
|
||||
return params.data?.staff_officer_id;
|
||||
},
|
||||
filter: "agTextColumnFilter",
|
||||
},
|
||||
{
|
||||
headerName: "所在单位",
|
||||
field: "department.name",
|
||||
sort: "desc",
|
||||
valueGetter: (params: ValueGetterParams) => {
|
||||
return params.data?.department_name;
|
||||
},
|
||||
filter: "agTextColumnFilter",
|
||||
maxWidth: 300,
|
||||
},
|
||||
const { user, hasSomePermissions } = useAuth();
|
||||
const { domainId, setModalOpen, role, setDomainId, canManageRole } =
|
||||
useContext(RoleEditorContext);
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
setDomainId?.(user.domainId);
|
||||
}
|
||||
}, [user]);
|
||||
const columnDefs: ColDef[] = [
|
||||
{
|
||||
headerName: "帐号",
|
||||
field: "staff.username",
|
||||
sort: "desc",
|
||||
valueGetter: (params: ValueGetterParams) => {
|
||||
return params.data?.staff_username;
|
||||
},
|
||||
filter: "agTextColumnFilter",
|
||||
maxWidth: 300,
|
||||
},
|
||||
{
|
||||
headerName: "名称",
|
||||
field: "staff.showname",
|
||||
sort: "desc",
|
||||
valueGetter: (params: ValueGetterParams) => {
|
||||
return params.data?.staff_showname;
|
||||
},
|
||||
filter: "agTextColumnFilter",
|
||||
maxWidth: 300,
|
||||
},
|
||||
{
|
||||
headerName: "证件号",
|
||||
field: "staff.officer_id",
|
||||
sort: "desc",
|
||||
valueGetter: (params: ValueGetterParams) => {
|
||||
return params.data?.staff_officer_id;
|
||||
},
|
||||
filter: "agTextColumnFilter",
|
||||
},
|
||||
{
|
||||
headerName: "所在单位",
|
||||
field: "department.name",
|
||||
sort: "desc",
|
||||
valueGetter: (params: ValueGetterParams) => {
|
||||
return params.data?.department_name;
|
||||
},
|
||||
filter: "agTextColumnFilter",
|
||||
maxWidth: 300,
|
||||
},
|
||||
|
||||
{
|
||||
headerName: "操作",
|
||||
sortable: true,
|
||||
{
|
||||
headerName: "操作",
|
||||
sortable: true,
|
||||
|
||||
cellRenderer: (props) => <OpreationRenderer props={props}></OpreationRenderer>, // 指定 cellRenderer
|
||||
maxWidth: 100,
|
||||
},
|
||||
];
|
||||
return <div className=" flex-grow">
|
||||
<div className="p-2 border-b flex items-center justify-between">
|
||||
<div className="flex items-center gap-2 ">
|
||||
<span className=""> {role?.name}</span>
|
||||
<span className=" text-tertiary-300 "> 角色成员列表</span>
|
||||
|
||||
</div>
|
||||
<div className=" flex items-center gap-4">
|
||||
<DepartmentSelect onChange={(value) => setDomainId(value as string)} rootId={user?.domainId} value={domainId} disabled={!canManageRole} domain={true} className=" w-48"></DepartmentSelect>
|
||||
{canManageRole && <Button
|
||||
onClick={() => {
|
||||
setModalOpen(true)
|
||||
}}
|
||||
type="primary" icon={<PlusOutlined></PlusOutlined>}>添加成员</Button>}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<AgServerTable
|
||||
rowGroupPanelShow="onlyWhenGrouping"
|
||||
height={"calc(100vh - 48px - 49px - 49px)"}
|
||||
columnDefs={columnDefs}
|
||||
rowHeight={50}
|
||||
params={{ domainId, roleId: role?.id }}
|
||||
objectType={ObjectType.ROLE_MAP}
|
||||
/>
|
||||
</div>
|
||||
cellRenderer: (props) => (
|
||||
<OpreationRenderer props={props}></OpreationRenderer>
|
||||
), // 指定 cellRenderer
|
||||
maxWidth: 100,
|
||||
},
|
||||
];
|
||||
return (
|
||||
<div className=" flex-grow">
|
||||
<div className="p-2 border-b flex items-center justify-between">
|
||||
<div className="flex items-center gap-2 ">
|
||||
<span className=""> {role?.name}</span>
|
||||
<span className=" text-tertiary-300 "> 角色成员列表</span>
|
||||
</div>
|
||||
<div className=" flex items-center gap-4">
|
||||
<DepartmentSelect
|
||||
onChange={(value) => setDomainId(value as string)}
|
||||
rootId={user?.domainId}
|
||||
value={domainId}
|
||||
disabled={!canManageRole}
|
||||
domain={true}
|
||||
className=" w-48"></DepartmentSelect>
|
||||
{canManageRole && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
setModalOpen(true);
|
||||
}}
|
||||
type="primary"
|
||||
icon={<PlusOutlined></PlusOutlined>}>
|
||||
添加成员
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<AgServerTable
|
||||
rowGroupPanelShow="onlyWhenGrouping"
|
||||
height={"calc(100vh - 48px - 49px - 49px)"}
|
||||
columnDefs={columnDefs}
|
||||
rowHeight={50}
|
||||
params={{ domainId, roleId: role?.id }}
|
||||
objectType={ObjectType.ROLE_MAP}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,101 @@
|
|||
import { useMemo, useState } from "react";
|
||||
import { Button, Select, Spin } from "antd";
|
||||
import type { SelectProps } from "antd";
|
||||
import { api } from "@nice/client";
|
||||
import React from "react";
|
||||
import { SizeType } from "antd/es/config-provider/SizeContext";
|
||||
import { RoleName } from "@nice/common";
|
||||
interface StaffSelectProps {
|
||||
value?: string | string[];
|
||||
onChange?: (value: string | string[]) => void;
|
||||
style?: React.CSSProperties;
|
||||
multiple?: boolean;
|
||||
domainId?: string;
|
||||
placeholder?: string;
|
||||
size?: SizeType;
|
||||
}
|
||||
|
||||
export default function LimitedStaffSelect({
|
||||
value,
|
||||
onChange,
|
||||
placeholder,
|
||||
style,
|
||||
multiple,
|
||||
domainId,
|
||||
size,
|
||||
}: StaffSelectProps) {
|
||||
const [keyword, setQuery] = useState<string>("");
|
||||
|
||||
// Determine ids based on whether value is an array or not
|
||||
const ids = useMemo(() => {
|
||||
return Array.isArray(value) ? value : [];
|
||||
}, [value]);
|
||||
const { data: enabledStaffIds, isLoading: roleMapIsLoading } =
|
||||
api.rolemap.getStaffIdsByRoleNames.useQuery({
|
||||
roleNames: [
|
||||
RoleName.Leader,
|
||||
RoleName.Organization,
|
||||
RoleName.RootAdmin,
|
||||
],
|
||||
});
|
||||
// Adjust the query to include ids when they are present
|
||||
const { data, isLoading } = api.staff.findMany.useQuery({
|
||||
where: {
|
||||
id:
|
||||
enabledStaffIds?.length > 0
|
||||
? { in: enabledStaffIds }
|
||||
: { in: [] },
|
||||
deletedAt: null,
|
||||
OR: [
|
||||
{
|
||||
username: {
|
||||
contains: keyword,
|
||||
},
|
||||
},
|
||||
{
|
||||
showname: {
|
||||
contains: keyword,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: {
|
||||
in: ids,
|
||||
},
|
||||
},
|
||||
],
|
||||
domainId,
|
||||
},
|
||||
select: { id: true, showname: true, username: true },
|
||||
take: 30,
|
||||
orderBy: { order: "asc" },
|
||||
});
|
||||
|
||||
const handleSearch = (value: string) => {
|
||||
setQuery(value);
|
||||
};
|
||||
|
||||
const options: SelectProps["options"] =
|
||||
data?.map((staff: any) => ({
|
||||
value: staff.id,
|
||||
label: staff?.showname || staff?.username,
|
||||
})) || [];
|
||||
|
||||
return (
|
||||
<>
|
||||
<Select
|
||||
size={size}
|
||||
allowClear
|
||||
showSearch
|
||||
mode={multiple ? "multiple" : undefined}
|
||||
placeholder={placeholder || "请选择人员"}
|
||||
notFoundContent={isLoading ? <Spin size="small" /> : null}
|
||||
filterOption={false}
|
||||
onSearch={handleSearch}
|
||||
options={options}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
style={{ minWidth: 200, ...style }}
|
||||
/>{" "}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -163,9 +163,9 @@ export default function StaffForm() {
|
|||
noStyle
|
||||
rules={[{ required: true }]}
|
||||
name={"showname"}
|
||||
label="姓名">
|
||||
label="名称">
|
||||
<Input
|
||||
placeholder="请输入姓名"
|
||||
placeholder="请输入名称"
|
||||
allowClear
|
||||
autoComplete="new-name" // 使用非标准的自动完成值
|
||||
spellCheck={false}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,11 @@
|
|||
import { Icon } from "@nice/iconer"; import {
|
||||
import { Icon } from "@nice/iconer";
|
||||
import {
|
||||
DeleteOutlined,
|
||||
EditFilled,
|
||||
EllipsisOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { ICellRendererParams } from "@ag-grid-community/core";
|
||||
import {
|
||||
ColDef,
|
||||
ValueGetterParams,
|
||||
} from "@ag-grid-community/core";
|
||||
import { ColDef, ValueGetterParams } from "@ag-grid-community/core";
|
||||
import { ObjectType, StaffRowModel } from "@nice/common";
|
||||
import { Menu, MenuItem } from "../../presentation/dropdown-menu";
|
||||
import AgServerTable from "../../presentation/ag-server-table";
|
||||
|
|
@ -21,7 +19,7 @@ import { message, Tag } from "antd";
|
|||
import { CustomCellRendererProps } from "@ag-grid-community/react";
|
||||
const OpreationRenderer = ({ props }: { props: ICellRendererParams }) => {
|
||||
const { setEditId, setModalOpen } = useContext(StaffEditorContext);
|
||||
const { softDeleteByIds } = useStaff()
|
||||
const { softDeleteByIds } = useStaff();
|
||||
if (props?.data?.id)
|
||||
return (
|
||||
<div>
|
||||
|
|
@ -39,14 +37,21 @@ const OpreationRenderer = ({ props }: { props: ICellRendererParams }) => {
|
|||
<MenuItem
|
||||
label="移除"
|
||||
onClick={() => {
|
||||
softDeleteByIds.mutateAsync({
|
||||
ids: [props?.data?.id],
|
||||
}, {
|
||||
onSettled: () => {
|
||||
message.success("删除成功");
|
||||
emitDataChange(ObjectType.STAFF, props.data as any, CrudOperation.DELETED)
|
||||
softDeleteByIds.mutateAsync(
|
||||
{
|
||||
ids: [props?.data?.id],
|
||||
},
|
||||
});
|
||||
{
|
||||
onSettled: () => {
|
||||
message.success("删除成功");
|
||||
emitDataChange(
|
||||
ObjectType.STAFF,
|
||||
props.data as any,
|
||||
CrudOperation.DELETED
|
||||
);
|
||||
},
|
||||
}
|
||||
);
|
||||
}}
|
||||
icon={<DeleteOutlined></DeleteOutlined>}></MenuItem>
|
||||
</Menu>
|
||||
|
|
@ -63,13 +68,12 @@ const StaffList = ({
|
|||
const { canManageAnyStaff } = useContext(StaffEditorContext);
|
||||
const [params, setParams] = useState({ domainId: null });
|
||||
useEffect(() => {
|
||||
|
||||
if (domainId) {
|
||||
setParams((prev) => ({ ...prev, domainId }))
|
||||
setParams((prev) => ({ ...prev, domainId }));
|
||||
} else {
|
||||
setParams((prev) => ({ ...prev, domainId: null }))
|
||||
setParams((prev) => ({ ...prev, domainId: null }));
|
||||
}
|
||||
}, [domainId])
|
||||
}, [domainId]);
|
||||
const columnDefs: ColDef[] = [
|
||||
canManageAnyStaff && {
|
||||
headerName: "所属域",
|
||||
|
|
@ -87,10 +91,11 @@ const StaffList = ({
|
|||
return params.data?.dept_name;
|
||||
},
|
||||
cellRenderer: (params) => {
|
||||
|
||||
return (
|
||||
params.value || (
|
||||
<span className="text-tertiary-300">未录入所属单位</span>
|
||||
<span className="text-tertiary-300">
|
||||
未录入所属单位
|
||||
</span>
|
||||
)
|
||||
);
|
||||
},
|
||||
|
|
@ -103,7 +108,7 @@ const StaffList = ({
|
|||
{
|
||||
field: "order",
|
||||
hide: true,
|
||||
sort: "asc" as SortDirection
|
||||
sort: "asc" as SortDirection,
|
||||
},
|
||||
{
|
||||
headerName: "帐号",
|
||||
|
|
@ -112,7 +117,9 @@ const StaffList = ({
|
|||
if (params?.data?.id)
|
||||
return (
|
||||
params.value || (
|
||||
<span className="text-tertiary-300">未录入帐号</span>
|
||||
<span className="text-tertiary-300">
|
||||
未录入帐号
|
||||
</span>
|
||||
)
|
||||
);
|
||||
},
|
||||
|
|
@ -122,13 +129,15 @@ const StaffList = ({
|
|||
maxWidth: 300,
|
||||
},
|
||||
{
|
||||
headerName: "姓名",
|
||||
headerName: "名称",
|
||||
field: "showname",
|
||||
cellRenderer: (params) => {
|
||||
if (params?.data?.id)
|
||||
return (
|
||||
params.value || (
|
||||
<span className="text-tertiary-300">未录入姓名</span>
|
||||
<span className="text-tertiary-300">
|
||||
未录入名称
|
||||
</span>
|
||||
)
|
||||
);
|
||||
},
|
||||
|
|
@ -156,7 +165,9 @@ const StaffList = ({
|
|||
cellRenderer: (params) => {
|
||||
const { data }: { data: StaffRowModel } = params;
|
||||
if (params?.data?.id)
|
||||
return <PhoneBook phoneNumber={data?.phone_number}></PhoneBook>;
|
||||
return (
|
||||
<PhoneBook phoneNumber={data?.phone_number}></PhoneBook>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -165,8 +176,11 @@ const StaffList = ({
|
|||
sortable: true,
|
||||
enableRowGroup: true,
|
||||
cellRenderer: (props: CustomCellRendererProps) => {
|
||||
|
||||
return <Tag color={props?.data?.enabled ? "success" : "error"}>{props?.data?.enabled ? "已启用" : "已禁用"}</Tag>
|
||||
return (
|
||||
<Tag color={props?.data?.enabled ? "success" : "error"}>
|
||||
{props?.data?.enabled ? "已启用" : "已禁用"}
|
||||
</Tag>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ interface StaffSelectProps {
|
|||
multiple?: boolean;
|
||||
domainId?: string;
|
||||
placeholder?: string;
|
||||
limitedIds?: string[];
|
||||
size?: SizeType;
|
||||
}
|
||||
|
||||
|
|
@ -20,6 +21,7 @@ export default function StaffSelect({
|
|||
placeholder,
|
||||
style,
|
||||
multiple,
|
||||
limitedIds,
|
||||
domainId,
|
||||
size,
|
||||
}: StaffSelectProps) {
|
||||
|
|
@ -33,6 +35,8 @@ export default function StaffSelect({
|
|||
// Adjust the query to include ids when they are present
|
||||
const { data, isLoading } = api.staff.findMany.useQuery({
|
||||
where: {
|
||||
id: limitedIds?.length > 0 ? { in: limitedIds } : undefined,
|
||||
deletedAt: null,
|
||||
OR: [
|
||||
{
|
||||
username: {
|
||||
|
|
|
|||
|
|
@ -2,8 +2,9 @@ export const env: {
|
|||
APP_NAME: string;
|
||||
SERVER_IP: string;
|
||||
VERSION: string;
|
||||
UOLOAD_PORT: string;
|
||||
UPLOAD_PORT: string;
|
||||
SERVER_PORT: string;
|
||||
LIB_URL: string;
|
||||
} = {
|
||||
APP_NAME: import.meta.env.PROD
|
||||
? (window as any).env.VITE_APP_APP_NAME
|
||||
|
|
@ -11,15 +12,18 @@ export const env: {
|
|||
SERVER_IP: import.meta.env.PROD
|
||||
? (window as any).env.VITE_APP_SERVER_IP
|
||||
: import.meta.env.VITE_APP_SERVER_IP,
|
||||
UOLOAD_PORT: import.meta.env.PROD
|
||||
? (window as any).env.VITE_APP_UOLOAD_PORT
|
||||
: import.meta.env.VITE_APP_UOLOAD_PORT,
|
||||
UPLOAD_PORT: import.meta.env.PROD
|
||||
? (window as any).env.VITE_APP_UPLOAD_PORT
|
||||
: import.meta.env.VITE_APP_UPLOAD_PORT,
|
||||
SERVER_PORT: import.meta.env.PROD
|
||||
? (window as any).env.VITE_APP_SERVER_PORT
|
||||
: import.meta.env.VITE_APP_SERVER_PORT,
|
||||
VERSION: import.meta.env.PROD
|
||||
? (window as any).env.VITE_APP_VERSION
|
||||
: import.meta.env.VITE_APP_VERSION,
|
||||
LIB_URL: import.meta.env.PROD
|
||||
? (window as any).env.VITE_APP_LIB_URL
|
||||
: import.meta.env.VITE_APP_LIB_URL,
|
||||
};
|
||||
|
||||
console.log(env);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
import { useState, useEffect } from "react";
|
||||
|
||||
const usePublicImage = (imageName: string) => {
|
||||
const [imageUrl, setImageUrl] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const loadImage = async () => {
|
||||
try {
|
||||
const response = await fetch(`/${imageName}`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const blob = await response.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
setImageUrl(url);
|
||||
} catch (error) {
|
||||
console.error("Error loading image:", error);
|
||||
setImageUrl(null);
|
||||
}
|
||||
};
|
||||
|
||||
loadImage();
|
||||
|
||||
return () => {
|
||||
if (imageUrl) {
|
||||
URL.revokeObjectURL(imageUrl);
|
||||
}
|
||||
};
|
||||
}, [imageName]);
|
||||
|
||||
return { imageUrl };
|
||||
};
|
||||
|
||||
export default usePublicImage;
|
||||
|
|
@ -35,7 +35,7 @@ export function useTusUpload() {
|
|||
if (uploadIndex === -1 || uploadIndex + 4 >= parts.length) {
|
||||
throw new Error("Invalid upload URL format");
|
||||
}
|
||||
const resUrl = `http://${env.SERVER_IP}:${env.UOLOAD_PORT}/uploads/${parts.slice(uploadIndex + 1, uploadIndex + 6).join("/")}`;
|
||||
const resUrl = `http://${env.SERVER_IP}:${env.UPLOAD_PORT}/uploads/${parts.slice(uploadIndex + 1, uploadIndex + 6).join("/")}`;
|
||||
|
||||
return resUrl;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -117,3 +117,29 @@
|
|||
.ql-snow .ql-picker.ql-header .ql-picker-label:not([data-value])::before {
|
||||
content: "正文" !important;
|
||||
}
|
||||
.top-notification-wrapper {
|
||||
position: relative;
|
||||
padding: 10px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.top-notification-text {
|
||||
border: 20px dashed transparent;
|
||||
background-image: linear-gradient(white, white),
|
||||
repeating-linear-gradient(
|
||||
45deg,
|
||||
#ff0000 0,
|
||||
#ff0000 40px,
|
||||
transparent 40px,
|
||||
transparent 60px,
|
||||
#0000ff 60px,
|
||||
#0000ff 100px,
|
||||
transparent 100px,
|
||||
transparent 120px
|
||||
);
|
||||
background-origin: border-box;
|
||||
background-clip: padding-box, border-box;
|
||||
padding: 15px;
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,6 +32,9 @@ export function useAppConfig() {
|
|||
const splashScreen = useMemo(() => {
|
||||
return baseSetting?.appConfig?.splashScreen;
|
||||
}, [baseSetting]);
|
||||
const notice = useMemo(() => {
|
||||
return baseSetting?.appConfig?.notice;
|
||||
}, [baseSetting]);
|
||||
const devDept = useMemo(() => {
|
||||
return baseSetting?.appConfig?.devDept;
|
||||
}, [baseSetting]);
|
||||
|
|
@ -46,6 +49,7 @@ export function useAppConfig() {
|
|||
splashScreen,
|
||||
devDept,
|
||||
isLoading,
|
||||
notice,
|
||||
logo,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@ export const staffDetailSelect: Prisma.StaffSelect = {
|
|||
department: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true
|
||||
}
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
showname: true,
|
||||
phoneNumber: true,
|
||||
|
|
@ -14,12 +14,12 @@ export const staffDetailSelect: Prisma.StaffSelect = {
|
|||
domain: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true
|
||||
}
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
domainId: true,
|
||||
meta: true
|
||||
}
|
||||
meta: true,
|
||||
};
|
||||
export const postDetailSelect: Prisma.PostSelect = {
|
||||
id: true,
|
||||
type: true,
|
||||
|
|
@ -63,6 +63,7 @@ export const postDetailSelect: Prisma.PostSelect = {
|
|||
name: true,
|
||||
},
|
||||
},
|
||||
meta: true,
|
||||
},
|
||||
},
|
||||
meta: true,
|
||||
|
|
|
|||
|
|
@ -45,6 +45,8 @@ export type StaffDto = Staff & {
|
|||
domain?: Department;
|
||||
department?: Department;
|
||||
meta?: StaffMeta;
|
||||
replyCount?: number;
|
||||
receiveCount?: number;
|
||||
};
|
||||
export interface AuthDto {
|
||||
token: string;
|
||||
|
|
@ -222,6 +224,7 @@ export interface BaseSetting {
|
|||
splashScreen?: string;
|
||||
devDept?: string;
|
||||
logo?: string;
|
||||
notice?: string;
|
||||
};
|
||||
}
|
||||
export interface PostMeta {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { ThemeSeed } from "./types";
|
|||
// 添加默认的主题配置
|
||||
export const USAFSeed: ThemeSeed = {
|
||||
colors: {
|
||||
primary: '#003087', // 深蓝色
|
||||
primary: '#3498DB', // 深蓝色
|
||||
secondary: '#71767C', // 灰色
|
||||
neutral: '#4A4A4A', // 中性灰色
|
||||
success: '#287233', // 绿色
|
||||
|
|
|
|||
|
|
@ -7,13 +7,13 @@ export const NiceTailwindConfig: Config = {
|
|||
colors: {
|
||||
// 主色调 - 空军蓝
|
||||
primary: {
|
||||
50: "#e8f2ff",
|
||||
50: "#e8f2ff",
|
||||
100: "#c5d9f7",
|
||||
150: "#EDF0F8", // via color
|
||||
150: "#EDF0F8", // via color
|
||||
200: "#9fc0ef",
|
||||
250: "#E6E9F0", // from color
|
||||
250: "#E6E9F0", // from color
|
||||
300: "#78a7e7",
|
||||
350: "#D8E2EF", // to color
|
||||
350: "#D8E2EF", // to color
|
||||
400: "#528edf",
|
||||
500: "#00308a",
|
||||
600: "#00256b",
|
||||
|
|
@ -28,7 +28,7 @@ export const NiceTailwindConfig: Config = {
|
|||
100: "#e0e0e0",
|
||||
200: "#c2c2c2",
|
||||
300: "#a3a3a3",
|
||||
350: "#97A9C4", // New color inserted
|
||||
350: "#97A9C4", // New color inserted
|
||||
400: "#858585",
|
||||
500: "#666666",
|
||||
600: "#4d4d4d",
|
||||
|
|
@ -77,21 +77,30 @@ export const NiceTailwindConfig: Config = {
|
|||
solid: "2px 2px 0 0 rgba(0, 0, 0, 0.2)",
|
||||
glow: "0 0 8px rgba(230, 180, 0, 0.8)",
|
||||
inset: "inset 0 2px 4px 0 rgba(0, 0, 0, 0.15)",
|
||||
"elevation-1": "0 1px 2px rgba(0, 48, 138, 0.05), 0 1px 1px rgba(0, 0, 0, 0.05)",
|
||||
"elevation-2": "0 2px 4px rgba(0, 48, 138, 0.1), 0 2px 2px rgba(0, 0, 0, 0.1)",
|
||||
"elevation-3": "0 4px 8px rgba(0, 48, 138, 0.15), 0 4px 4px rgba(0, 0, 0, 0.15)",
|
||||
"elevation-4": "0 8px 16px rgba(0, 48, 138, 0.2), 0 8px 8px rgba(0, 0, 0, 0.2)",
|
||||
"elevation-5": "0 16px 32px rgba(0, 48, 138, 0.25), 0 16px 16px rgba(0, 0, 0, 0.25)",
|
||||
"elevation-1":
|
||||
"0 1px 2px rgba(0, 48, 138, 0.05), 0 1px 1px rgba(0, 0, 0, 0.05)",
|
||||
"elevation-2":
|
||||
"0 2px 4px rgba(0, 48, 138, 0.1), 0 2px 2px rgba(0, 0, 0, 0.1)",
|
||||
"elevation-3":
|
||||
"0 4px 8px rgba(0, 48, 138, 0.15), 0 4px 4px rgba(0, 0, 0, 0.15)",
|
||||
"elevation-4":
|
||||
"0 8px 16px rgba(0, 48, 138, 0.2), 0 8px 8px rgba(0, 0, 0, 0.2)",
|
||||
"elevation-5":
|
||||
"0 16px 32px rgba(0, 48, 138, 0.25), 0 16px 16px rgba(0, 0, 0, 0.25)",
|
||||
panel: "0 4px 6px -1px rgba(0, 48, 138, 0.1), 0 2px 4px -2px rgba(0, 48, 138, 0.1), inset 0 -1px 2px rgba(255, 255, 255, 0.05)",
|
||||
button: "0 2px 4px rgba(0, 48, 138, 0.2), inset 0 1px 2px rgba(255, 255, 255, 0.1), 0 1px 1px rgba(0, 0, 0, 0.05)",
|
||||
card: "0 4px 6px rgba(0, 48, 138, 0.1), 0 1px 3px rgba(0, 0, 0, 0.08), inset 0 -1px 2px rgba(255, 255, 255, 0.05)",
|
||||
modal: "0 8px 32px rgba(0, 48, 138, 0.2), 0 4px 16px rgba(0, 0, 0, 0.15), inset 0 -2px 4px rgba(255, 255, 255, 0.05)",
|
||||
"soft-primary": "0 2px 4px rgba(0, 48, 138, 0.1), 0 4px 8px rgba(0, 48, 138, 0.05)",
|
||||
"soft-secondary": "0 2px 4px rgba(77, 77, 77, 0.1), 0 4px 8px rgba(77, 77, 77, 0.05)",
|
||||
"soft-accent": "0 2px 4px rgba(230, 180, 0, 0.1), 0 4px 8px rgba(230, 180, 0, 0.05)",
|
||||
"soft-primary":
|
||||
"0 2px 4px rgba(0, 48, 138, 0.1), 0 4px 8px rgba(0, 48, 138, 0.05)",
|
||||
"soft-secondary":
|
||||
"0 2px 4px rgba(77, 77, 77, 0.1), 0 4px 8px rgba(77, 77, 77, 0.05)",
|
||||
"soft-accent":
|
||||
"0 2px 4px rgba(230, 180, 0, 0.1), 0 4px 8px rgba(230, 180, 0, 0.05)",
|
||||
"inner-glow": "inset 0 0 8px rgba(0, 48, 138, 0.1)",
|
||||
"outer-glow": "0 0 16px rgba(0, 48, 138, 0.1)",
|
||||
"elegant": "0 4px 24px rgba(0, 48, 138, 0.15), 0 2px 12px rgba(0, 48, 138, 0.1), 0 1px 6px rgba(0, 48, 138, 0.05), inset 0 -1px 2px rgba(255, 255, 255, 0.1)",
|
||||
elegant:
|
||||
"0 4px 24px rgba(0, 48, 138, 0.15), 0 2px 12px rgba(0, 48, 138, 0.1), 0 1px 6px rgba(0, 48, 138, 0.05), inset 0 -1px 2px rgba(255, 255, 255, 0.1)",
|
||||
},
|
||||
animation: {
|
||||
"pulse-slow": "pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite",
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -8,7 +8,7 @@
|
|||
window.env = {
|
||||
VITE_APP_SERVER_IP: "192.168.112.239",
|
||||
VITE_APP_SERVER_PORT: "3002",
|
||||
VITE_APP_UOLOAD_PORT: "8090",
|
||||
VITE_APP_UPLOAD_PORT: "8090",
|
||||
VITE_APP_APP_NAME: "领导信箱",
|
||||
VITE_APP_VERSION: "0.1.0",
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue