大改动

This commit is contained in:
宇阳
2025-01-12 00:00:38 +08:00
parent 34198b212b
commit 7f01194c61
8 changed files with 259 additions and 197 deletions

View File

@@ -15,27 +15,29 @@ import { useWebStore } from '@/stores';
import dayjs from 'dayjs';
const ArticlePage = () => {
export default () => {
const [loading, setLoading] = useState<boolean>(false);
const [form] = Form.useForm();
const web = useWebStore(state => state.web)
const [current, setCurrent] = useState<number>(1);
const [loading, setLoading] = useState<boolean>(false);
const [articleList, setArticleList] = useState<Article[]>([]);
const [form] = Form.useForm();
const { RangePicker } = DatePicker;
const getArticleList = async () => {
setLoading(true);
const { data } = await getArticleListAPI();
setArticleList(data as Article[]);
try {
const { data } = await getArticleListAPI();
setArticleList(data);
setLoading(false);
} catch (error) {
setLoading(false);
}
setLoading(false);
};
useEffect(() => {
getArticleList()
}, []);
const delArticleData = async (id: number) => {
setLoading(true);
@@ -46,7 +48,6 @@ const ArticlePage = () => {
form.resetFields()
setCurrent(1)
notification.success({ message: '🎉 删除文章成功' })
setLoading(false);
} catch (error) {
setLoading(false);
}
@@ -149,19 +150,27 @@ const ArticlePage = () => {
},
];
const onSubmit = async (values: FilterForm) => {
const query: FilterArticle = {
key: values.title,
cateIds: values.cateIds,
tagId: values.tagId,
isDraft: 0,
isDel: 0,
startDate: values.createTime && values.createTime[0].valueOf() + '',
endDate: values.createTime && values.createTime[1].valueOf() + ''
const onFilterSubmit = async (values: FilterForm) => {
setLoading(true)
try {
const query: FilterArticle = {
key: values.title,
cateIds: values.cateIds,
tagId: values.tagId,
isDraft: 0,
isDel: 0,
startDate: values.createTime && values.createTime[0].valueOf() + '',
endDate: values.createTime && values.createTime[1].valueOf() + ''
}
const { data } = await getArticleListAPI({ query });
setArticleList(data);
} catch (error) {
setLoading(false)
}
const { data } = await getArticleListAPI({ query });
setArticleList(data as Article[]);
setLoading(false)
}
const [cateList, setCateList] = useState<Cate[]>([])
@@ -178,16 +187,18 @@ const ArticlePage = () => {
}
useEffect(() => {
setLoading(true);
getArticleList()
getCateList()
getTagList()
}, [])
return (
<>
<div>
<Title value="文章管理" />
<Card className='my-2 overflow-scroll'>
<Form form={form} layout="inline" onFinish={onSubmit} autoComplete="off" className='flex-nowrap'>
<Form form={form} layout="inline" onFinish={onFilterSubmit} autoComplete="off" className='flex-nowrap'>
<Form.Item label="标题" name="title" className='min-w-[200px]'>
<Input placeholder='请输入关键词' />
</Form.Item>
@@ -225,7 +236,6 @@ const ArticlePage = () => {
rowKey="id"
dataSource={articleList}
columns={columns as any}
loading={loading}
scroll={{ x: 'max-content' }}
pagination={{
position: ['bottomCenter'],
@@ -235,10 +245,9 @@ const ArticlePage = () => {
setCurrent(current)
}
}}
loading={loading}
/>
</Card>
</>
</div>
);
};
export default ArticlePage;
};

View File

@@ -6,9 +6,10 @@ import { Form, Input, Button, Tree, Modal, Spin, Dropdown, Card, MenuProps, Popc
import Title from '@/components/Title';
import "./index.scss"
const CatePage = () => {
export default () => {
const [loading, setLoading] = useState(false);
const [btnLoading, setBtnLoading] = useState(false)
const [editLoading, setEditLoading] = useState(false)
const [isModelOpen, setIsModelOpen] = useState(false);
const [cate, setCate] = useState<Cate>({} as Cate);
@@ -41,19 +42,22 @@ const CatePage = () => {
};
const editCateData = async (id: number) => {
setLoading(true);
setIsMethod("edit")
setIsModelOpen(true);
setEditLoading(true);
try {
setIsMethod("edit")
setIsModelOpen(true);
const { data } = await getCateDataAPI(id);
setIsCateShow(data.type === "cate" ? false : true)
setCate(data);
form.setFieldsValue(data);
// 判断是分类还是导航
setIsCateShow(data.type === "cate" ? false : true)
} catch (error) {
setLoading(false);
setEditLoading(false);
}
setEditLoading(false);
};
const delCateData = async (id: number) => {
@@ -61,8 +65,8 @@ const CatePage = () => {
try {
await delCateDataAPI(id);
await getCateList();
message.success('🎉 删除分类成功');
getCateList();
} catch (error) {
setLoading(false);
}
@@ -83,17 +87,19 @@ const CatePage = () => {
message.success('🎉 新增分类成功');
}
await getCateList();
// 初始化表单状态
form.resetFields();
setCate({} as Cate);
setIsModelOpen(false);
getCateList();
setIsMethod("create")
})
} catch (error) {
setBtnLoading(false)
}
setBtnLoading(false)
};
const closeModel = () => {
@@ -153,7 +159,7 @@ const CatePage = () => {
<Tree defaultExpandAll={true} treeData={treeData(list)} />
</Spin>
<Modal title={isMethod === "edit" ? "编辑分类" : "新增分类"} open={isModelOpen} onCancel={closeModel} destroyOnClose footer={null}>
<Modal loading={editLoading} title={isMethod === "edit" ? "编辑分类" : "新增分类"} open={isModelOpen} onCancel={closeModel} destroyOnClose footer={null}>
<Form form={form} layout="vertical" initialValues={cate} size='large' preserve={false} className='mt-6'>
<Form.Item label="名称" name="name" rules={[{ required: true, message: '分类名称不能为空' }]}>
<Input placeholder="请输入分类名称" />
@@ -195,6 +201,4 @@ const CatePage = () => {
</Card>
</>
);
};
export default CatePage;
};

View File

@@ -15,11 +15,12 @@ import { useWebStore, useUserStore } from '@/stores'
import dayjs from 'dayjs';
const CommentPage = () => {
export default () => {
const [loading, setLoading] = useState(false);
const web = useWebStore(state => state.web)
const user = useUserStore(state => state.user)
const [loading, setLoading] = useState(false);
const [btnLoading, setBtnLoading] = useState(false);
const [comment, setComment] = useState<Comment>({} as Comment);
@@ -28,21 +29,15 @@ const CommentPage = () => {
const [isCommentModalOpen, setIsCommentModalOpen] = useState(false);
const getCommentList = async () => {
const { data } = await getCommentListAPI();
setList(data)
setLoading(false)
}
const delCommentData = async (id: number) => {
setLoading(true)
try {
await delCommentDataAPI(id);
getCommentList();
message.success('🎉 删除评论成功');
const { data } = await getCommentListAPI();
setList(data)
} catch (error) {
setLoading(false)
}
};
setLoading(false)
}
useEffect(() => {
setLoading(true)
@@ -119,6 +114,18 @@ const CommentPage = () => {
const { RangePicker } = DatePicker;
const delCommentData = async (id: number) => {
setLoading(true)
try {
await delCommentDataAPI(id);
await getCommentList();
message.success('🎉 删除评论成功');
} catch (error) {
setLoading(false)
}
};
const onSubmit = async (values: FilterForm) => {
setLoading(true)
@@ -135,6 +142,8 @@ const CommentPage = () => {
} catch (error) {
setLoading(false)
}
setLoading(false)
}
// 回复内容
@@ -156,18 +165,17 @@ const CommentPage = () => {
createTime: new Date().getTime().toString(),
})
await getCommentList()
message.success('🎉 回复评论成功');
setIsReplyModalOpen(false)
setReplyInfo("")
getCommentList()
} catch (error) {
setBtnLoading(false)
}
}
return (
<>
<div>
<Title value='评论管理' />
<Card className='my-2 overflow-scroll'>
@@ -195,13 +203,13 @@ const CommentPage = () => {
rowKey="id"
dataSource={list}
columns={columns}
loading={loading}
expandable={{ defaultExpandAllRows: true }}
scroll={{ x: 'max-content' }}
pagination={{
position: ['bottomCenter'],
defaultPageSize: 8,
}}
loading={loading}
/>
</Card>
@@ -231,9 +239,6 @@ const CommentPage = () => {
<Button type="primary" loading={btnLoading} onClick={handleReply} className="w-full mt-2"></Button>
</div>
</Modal>
</>
</div>
);
};
export default CommentPage;
};

View File

@@ -15,6 +15,11 @@ import { Article, Status } from "@/types/app/article";
import dayjs from 'dayjs';
interface Props {
data: Article,
closeModel: () => void
}
interface FieldType {
title: string,
createTime: number;
@@ -28,7 +33,7 @@ interface FieldType {
password: string
}
const PublishForm = ({ data, closeModel }: { data: Article, closeModel: () => void }) => {
const PublishForm = ({ data, closeModel }: Props) => {
const [params] = useSearchParams()
const id = +params.get('id')!
const isDraftParams = Boolean(params.get('draft'))
@@ -167,18 +172,19 @@ const PublishForm = ({ data, closeModel }: { data: Article, closeModel: () => vo
} as any)
}
}
// 关闭弹框
closeModel()
// 清除本地持久化的数据
localStorage.removeItem('article_content')
// 如果是草稿就跳转到草稿页,否则文章页
isDraft ? navigate("/draft") : navigate("/article")
// 初始化表单
form.resetFields()
} catch (error) {
setBtnLoading(false)
}
// 关闭弹框
closeModel()
// 清除本地持久化的数据
localStorage.removeItem('article_content')
// 如果是草稿就跳转到草稿页,否则文章页
isDraft ? navigate("/draft") : navigate("/article")
// 初始化表单
form.resetFields()
setBtnLoading(false)
}
@@ -191,7 +197,7 @@ const PublishForm = ({ data, closeModel }: { data: Article, closeModel: () => vo
}
return (
<>
<div>
<Form
form={form}
name="basic"
@@ -277,7 +283,7 @@ const PublishForm = ({ data, closeModel }: { data: Article, closeModel: () => vo
</Form.Item>
)}
</Form>
</>
</div>
);
};

View File

@@ -13,13 +13,13 @@ import { BiSave } from "react-icons/bi";
import { AiOutlineEdit, AiOutlineSend } from 'react-icons/ai';
import { titleSty } from '@/styles/sty';
const CreatePage = () => {
export default () => {
const [loading, setLoading] = useState(false)
const [params] = useSearchParams()
const id = +params.get('id')!
const isDraftParams = Boolean(params.get('draft'))
const [loading, setLoading] = useState(false)
const [data, setData] = useState<Article>({} as Article)
const [content, setContent] = useState('');
const [publishOpen, setPublishOpen] = useState(false)
@@ -38,6 +38,8 @@ const CreatePage = () => {
} catch (error) {
setLoading(false)
}
setLoading(false)
}
// 回显数据
@@ -91,53 +93,61 @@ const CreatePage = () => {
// 解析接口数据
const parsingData = async (command: string) => {
const res = await fetch(`/ai/v1/chat/completions`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${import.meta.env.VITE_AI_APIPassword}`
},
body: JSON.stringify({
model: import.meta.env.VITE_AI_MODEL,
messages: [{
role: "user",
content: `${command}${content}`
}],
stream: true
})
});
setLoading(true)
const reader = res.body.getReader();
const decoder = new TextDecoder("utf-8");
try {
const res = await fetch(`/ai/v1/chat/completions`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${import.meta.env.VITE_AI_APIPassword}`
},
body: JSON.stringify({
model: import.meta.env.VITE_AI_MODEL,
messages: [{
role: "user",
content: `${command}${content}`
}],
stream: true
})
});
let receivedText = "";
const reader = res.body.getReader();
const decoder = new TextDecoder("utf-8");
while (true) {
const { done, value } = await reader.read();
if (done) break;
receivedText += decoder.decode(value, { stream: true });
let receivedText = "";
// 处理每一块数据
const lines = receivedText.split("\n");
for (let i = 0; i < lines.length - 1; i++) {
const line = lines[i].trim();
if (line.startsWith("data:")) {
const jsonString = line.substring(5).trim();
if (jsonString !== "[DONE]") {
const data = JSON.parse(jsonString);
console.log("Received chunk:", data.choices[0].delta.content);
setContent((content) => content + data.choices[0].delta.content);
// 在这里处理每一块数据
} else {
console.log("Stream finished.");
return;
while (true) {
const { done, value } = await reader.read();
if (done) break;
receivedText += decoder.decode(value, { stream: true });
// 处理每一块数据
const lines = receivedText.split("\n");
for (let i = 0; i < lines.length - 1; i++) {
const line = lines[i].trim();
if (line.startsWith("data:")) {
const jsonString = line.substring(5).trim();
if (jsonString !== "[DONE]") {
const data = JSON.parse(jsonString);
console.log("Received chunk:", data.choices[0].delta.content);
setContent((content) => content + data.choices[0].delta.content);
// 在这里处理每一块数据
} else {
console.log("Stream finished.");
return;
}
}
}
}
// 保留最后一行未处理的数据
receivedText = lines[lines.length - 1];
// 保留最后一行未处理的数据
receivedText = lines[lines.length - 1];
}
} catch (error) {
setLoading(false)
}
setLoading(false)
}
// AI功能
@@ -166,7 +176,7 @@ const CreatePage = () => {
];
return (
<>
<div>
<Title value="创作">
<div className='flex items-center space-x-4 w-[360px]'>
<Dropdown.Button menu={{ items }}>
@@ -196,8 +206,6 @@ const CreatePage = () => {
<PublishForm data={data} closeModel={() => setPublishOpen(false)} />
</Drawer>
</Card >
</>
</div>
);
};
export default CreatePage;
};

View File

@@ -15,12 +15,12 @@ import { LuImagePlus } from "react-icons/lu";
import { RiDeleteBinLine } from "react-icons/ri";
export default () => {
const [loading, setLoading] = useState(false)
const [params] = useSearchParams()
const id = +params.get('id')!
const navigate = useNavigate()
const [loading, setLoading] = useState(false)
const [content, setContent] = useState("")
const [imageList, setImageList] = useState<string[]>([])
@@ -61,17 +61,20 @@ export default () => {
}
const getRecordData = async () => {
setLoading(true)
const { data } = await getRecordDataAPI(id)
setContent(data.content)
setImageList(JSON.parse(data.images as string))
try {
const { data } = await getRecordDataAPI(id)
setContent(data.content)
setImageList(JSON.parse(data.images as string))
} catch (error) {
setLoading(false)
}
setLoading(false)
}
// 回显数据
useEffect(() => {
setLoading(true)
// 有Id就回显指定的数据
if (id) getRecordData()
}, [id])
@@ -130,7 +133,7 @@ export default () => {
};
return (
<>
<div>
<Title value="闪念" />
<Spin spinning={loading}>
@@ -172,6 +175,7 @@ export default () => {
type="primary"
size="large"
icon={<BiLogoTelegram className="text-xl" />}
loading={loading}
className="absolute bottom-4 right-4"
onClick={onSubmit}
/>
@@ -188,6 +192,6 @@ export default () => {
}}
onCancel={() => setIsModalOpen(false)}
/>
</>
</div>
)
}

View File

@@ -17,9 +17,12 @@ interface ListItemProps {
item: any;
type: Menu;
fetchData: (type: Menu) => void;
setLoading: (loading: boolean) => void;
}
export default ({ item, type, fetchData }: ListItemProps) => {
export default ({ item, type, fetchData, setLoading }: ListItemProps) => {
const [btnLoading, setBtnLoading] = useState<boolean>(false)
const web = useWebStore(state => state.web)
const user = useUserStore(state => state.user)
@@ -27,64 +30,86 @@ export default ({ item, type, fetchData }: ListItemProps) => {
// 通过
const handleApproval = async () => {
if (type === "link") {
await auditWebDataAPI(item.id);
} else if (type === "comment") {
await auditCommentDataAPI(item.id);
} else if (type === "wall") {
await auditWallDataAPI(item.id);
}
setLoading(true)
btnType != "reply" && message.success('🎉 审核成功');
fetchData(type);
try {
if (type === "link") {
await auditWebDataAPI(item.id);
} else if (type === "comment") {
await auditCommentDataAPI(item.id);
} else if (type === "wall") {
await auditWallDataAPI(item.id);
}
await fetchData(type);
btnType != "reply" && message.success('🎉 审核成功');
} catch (error) {
setLoading(false)
}
};
// 回复
const [isModalOpen, setIsModalOpen] = useState(false);
const [replyInfo, setReplyInfo] = useState("")
const handleReply = async () => {
// 审核通过评论
await handleApproval()
setBtnLoading(true)
// 发送回复内容
await addCommentDataAPI({
avatar: user.avatar,
url: web.url,
content: replyInfo,
commentId: item?.id!,
auditStatus: 1,
email: user.email ? user.email : null,
name: user.name,
articleId: item?.articleId!,
createTime: new Date().getTime().toString(),
})
try {
// 审核通过评论
await handleApproval()
message.success('🎉 回复成功');
setIsModalOpen(false)
fetchData(type);
setReplyInfo("")
setBtnType("")
// 发送回复内容
await addCommentDataAPI({
avatar: user.avatar,
url: web.url,
content: replyInfo,
commentId: item?.id!,
auditStatus: 1,
email: user.email ? user.email : null,
name: user.name,
articleId: item?.articleId!,
createTime: new Date().getTime().toString(),
})
await fetchData(type);
message.success('🎉 回复成功');
setReplyInfo("")
setBtnType("")
setIsModalOpen(false)
} catch (error) {
setBtnLoading(false)
}
setBtnLoading(false)
}
// 驳回
const [dismissInfo, setDismissInfo] = useState("")
const handleDismiss = async () => {
if (type === "link") {
await delLinkDataAPI(item.id);
} else if (type === "comment") {
await delCommentDataAPI(item.id);
} else if (type === "wall") {
await delWallDataAPI(item.id);
setBtnLoading(true)
try {
if (type === "link") {
await delLinkDataAPI(item.id);
} else if (type === "comment") {
await delCommentDataAPI(item.id);
} else if (type === "wall") {
await delWallDataAPI(item.id);
}
// 有内容就发送驳回通知邮件,反之直接删除
if (dismissInfo.trim().length) await sendDismissEmail()
await fetchData(type);
message.success('🎉 驳回成功');
setDismissInfo("")
setBtnType("")
setIsModalOpen(false)
} catch (error) {
setBtnLoading(false)
}
// 有内容就发送驳回通知邮件,反之直接删除
if (dismissInfo.trim().length) await sendDismissEmail()
message.success('🎉 驳回成功');
setIsModalOpen(false)
fetchData(type);
setDismissInfo("")
setBtnType("")
setBtnLoading(false)
};
// 发送驳回通知邮件
@@ -202,7 +227,7 @@ export default ({ item, type, fetchData }: ListItemProps) => {
<div className="flex space-x-4">
<Button className="w-full mt-2" onClick={() => setIsModalOpen(false)}></Button>
<Button type="primary" className="w-full mt-2" onClick={btnType === "reply" ? handleReply : handleDismiss}></Button>
<Button type="primary" onClick={btnType === "reply" ? handleReply : handleDismiss} loading={btnLoading} className="w-full mt-2"></Button>
</div>
</Modal>
</div>

View File

@@ -54,7 +54,7 @@ export default () => {
return <Empty />;
}
return list.map(item => (
<List key={item.id} item={item} type={type} fetchData={(type) => fetchData(type)} />
<List key={item.id} item={item} type={type} fetchData={(type) => fetchData(type)} setLoading={setLoading} />
));
};
@@ -62,32 +62,33 @@ export default () => {
<>
<Title value="工作台" />
<Spin spinning={loading}>
<Card className="mt-2 min-h-[calc(100vh-180px)]">
<div className="flex flex-col md:flex-row w-full">
<div className="w-full min-w-[200px] md:w-2/12 md:min-h-96 mb-5 md:mb-0 pr-4 md:border-b-transparent md:border-r border-[#eee] dark:border-strokedark">
<ul className="space-y-1">
{(["comment", "link", "wall"] as Menu[]).map((menu) => (
<li
key={menu}
className={`flex items-center w-full py-3 px-4 hover:bg-[#f9f9ff] dark:hover:bg-[#3c5370] hover:text-primary ${active === menu ? activeSty : ''} rounded-md text-base cursor-pointer transition-colors`}
onClick={() => setActive(menu)}
>
<img src={menu === "comment" ? comment : menu === "link" ? link : info} alt="" className="w-8 mr-4" />
<span>{menu === "comment" ? "评论" : menu === "link" ? "友联" : "留言"}</span>
</li>
))}
</ul>
</div>
<Card className="mt-2 min-h-[calc(100vh-180px)]">
<div className="flex flex-col md:flex-row w-full">
<div className="w-full min-w-[200px] md:w-2/12 md:min-h-96 mb-5 md:mb-0 pr-4 md:border-b-transparent md:border-r border-[#eee] dark:border-strokedark">
<ul className="space-y-1">
{(["comment", "link", "wall"] as Menu[]).map((menu) => (
<li
key={menu}
className={`flex items-center w-full py-3 px-4 hover:bg-[#f9f9ff] dark:hover:bg-[#3c5370] hover:text-primary ${active === menu ? activeSty : ''} rounded-md text-base cursor-pointer transition-colors`}
onClick={() => setActive(menu)}
>
<img src={menu === "comment" ? comment : menu === "link" ? link : info} alt="" className="w-8 mr-4" />
<span>{menu === "comment" ? "评论" : menu === "link" ? "友联" : "留言"}</span>
</li>
))}
</ul>
</div>
<Spin spinning={loading}>
<div className="w-full md:w-10/12 md:pl-6 py-4 space-y-10">
{active === "link" && renderList(linkList, "link")}
{active === "comment" && renderList(commentList, "comment")}
{active === "wall" && renderList(wallList, "wall")}
</div>
</div>
</Card>
</Spin>
</Spin>
</div>
</Card>
</>
);
}