diff --git a/src/pages/Tag/index.tsx b/src/pages/Tag/index.tsx index cc875af..de2ee62 100644 --- a/src/pages/Tag/index.tsx +++ b/src/pages/Tag/index.tsx @@ -1,12 +1,15 @@ import { useState, useEffect } from 'react'; import { Table, Button, Form, Input, Popconfirm, message, Card } from 'antd'; -import { getTagListAPI, addTagDataAPI, editTagDataAPI, delTagDataAPI } from '@/api/Tag'; +import { getTagListAPI, addTagDataAPI, editTagDataAPI, delTagDataAPI, getTagDataAPI } from '@/api/Tag'; import { Tag } from '@/types/app/tag'; import Title from '@/components/Title'; import { ColumnsType } from 'antd/es/table'; const TagPage = () => { const [loading, setLoading] = useState(false); + const [btnLoading, setBtnLoading] = useState(false) + + const [form] = Form.useForm(); const [tag, setTag] = useState({} as Tag); const [list, setList] = useState([]); @@ -28,49 +31,76 @@ const TagPage = () => { ]; const getTagList = async () => { - setLoading(true); - const { data } = await getTagListAPI(); - setList(data as Tag[]); + try { + const { data } = await getTagListAPI(); + setList(data as Tag[]); + } catch (error) { + setLoading(false); + } + setLoading(false); }; useEffect(() => { + setLoading(true); getTagList(); }, []); - const [form] = Form.useForm(); - const editTagData = (record: Tag) => { - setTag(record); - form.setFieldsValue(record); + const editTagData = async (record: Tag) => { + setLoading(true); + + try { + const { data } = await getTagDataAPI(record.id) + setTag(data); + form.setFieldsValue(data); + } catch (error) { + setLoading(false); + } + + setLoading(false); }; const delTagData = async (id: number) => { setLoading(true); - await delTagDataAPI(id); - message.success('🎉 删除标签成功'); - getTagList(); + + try { + await delTagDataAPI(id); + await getTagList(); + message.success('🎉 删除标签成功'); + } catch (error) { + setLoading(false); + } }; const onSubmit = async () => { setLoading(true); - form.validateFields().then(async (values: Tag) => { - if (tag.id) { - await editTagDataAPI({ ...tag, ...values }); - message.success('🎉 编辑标签成功'); - } else { - await addTagDataAPI(values); - message.success('🎉 新增标签成功'); - } + setBtnLoading(true); - getTagList(); - form.resetFields(); - form.setFieldsValue({ name: '' }) - setTag({} as Tag); - }); + try { + form.validateFields().then(async (values: Tag) => { + if (tag.id) { + await editTagDataAPI({ ...tag, ...values }); + message.success('🎉 编辑标签成功'); + } else { + await addTagDataAPI(values); + message.success('🎉 新增标签成功'); + } + + await getTagList(); + form.resetFields(); + form.setFieldsValue({ name: '' }) + setTag({} as Tag); + }); + } catch (error) { + setLoading(false); + setBtnLoading(false); + } + + setBtnLoading(false); }; return ( - <> +
<div className='flex md:justify-between flex-col md:flex-row mx-auto mt-2'> @@ -81,14 +111,13 @@ const TagPage = () => { initialValues={tag} onFinish={onSubmit} size='large' - > <Form.Item label="标签名称" name="name" rules={[{ required: true, message: '标签名称不能为空' }]}> <Input placeholder="请输入标签名称" /> </Form.Item> <Form.Item> - <Button type="primary" htmlType="submit" loading={loading} className="w-full">{tag.id ? '编辑标签' : '新增标签'}</Button> + <Button type="primary" htmlType="submit" loading={btnLoading} className="w-full">{tag.id ? '编辑标签' : '新增标签'}</Button> </Form.Item> </Form> </Card> @@ -107,7 +136,7 @@ const TagPage = () => { /> </Card> </div> - </> + </div> ); }; diff --git a/src/pages/User/index.tsx b/src/pages/User/index.tsx index 93408b2..e8e48a9 100644 --- a/src/pages/User/index.tsx +++ b/src/pages/User/index.tsx @@ -17,7 +17,9 @@ import dayjs from 'dayjs'; const UserPage = () => { const [loading, setLoading] = useState<boolean>(false); const [btnLoading, setBtnLoading] = useState(false) + const [editLoading, setEditLoading] = useState(false) + const [form] = Form.useForm(); const store = useUserStore() const [userList, setUserList] = useState<User[]>([]); @@ -107,76 +109,105 @@ const UserPage = () => { }, ]; - const [userForm] = Form.useForm(); - const getUserList = async () => { - setLoading(true); - const { data } = await getUserListAPI(); - setUserList(data as User[]); + try { + const { data } = await getUserListAPI(); + setUserList(data as User[]); + setLoading(false); + } catch (error) { + setLoading(false); + } + setLoading(false); }; const getRoleList = async () => { const { data } = await getRoleListAPI(); - setRoleList(data as Role[]); + console.log(data); + + setRoleList(data); }; useEffect(() => { + setLoading(true); getUserList(); getRoleList() }, []); const delUserData = async (id: number) => { setLoading(true); - await delUserDataAPI(id); - await getUserList(); - notification.success({ message: '🎉 删除用户成功' }); - setLoading(false); + + try { + await delUserDataAPI(id); + await getUserList(); + notification.success({ message: '🎉 删除用户成功' }); + } catch (error) { + setLoading(false); + } }; const editUserData = async (id: number) => { - const { data } = await getUserDataAPI(id) - setUser({ ...data, role: data.role.id }); + setEditLoading(true); - userForm.setFieldsValue({ ...data, roleId: data.role.id }); - setDrawerVisible(true); + try { + setDrawerVisible(true); + const { data } = await getUserDataAPI(id) + setUser(data); + form.setFieldsValue(data); + } catch (error) { + setEditLoading(false); + } + + setEditLoading(false); }; const reset = () => { setUser({} as User) - userForm.resetFields() + form.resetFields() } const onSubmit = async () => { setBtnLoading(true) - userForm.validateFields().then(async (values: User) => { - if (user.id) { - await editUserDataAPI({ ...user, ...values }); - notification.success({ message: '🎉 编辑用户成功' }); - } else { - await addUserDataAPI({ ...values, password: "123456", createTime: new Date().getTime().toString() }); - notification.success({ message: '🎉 创建用户成功' }); - } - setDrawerVisible(false); - getUserList(); - }) + try { + form.validateFields().then(async (values: User) => { + if (user.id) { + await editUserDataAPI({ ...user, ...values }); + notification.success({ message: '🎉 编辑用户成功' }); + } else { + await addUserDataAPI({ ...values, password: "123456", createTime: new Date().getTime().toString() }); + notification.success({ message: '🎉 创建用户成功' }); + } + + await getUserList(); + setDrawerVisible(false); + reset() + }) + } catch (error) { + setBtnLoading(false) + } setBtnLoading(false) }; - const [filterForm] = Form.useForm(); - const onFilterSubmit = async (values: FilterForm) => { - const query: FilterUser = { - key: values.name, - roleId: values.role, - startDate: values.createTime && values.createTime[0].valueOf() + '', - endDate: values.createTime && values.createTime[1].valueOf() + '' + setLoading(true) + + try { + const query: FilterUser = { + key: values.name, + roleId: values.role, + startDate: values.createTime && values.createTime[0].valueOf() + '', + endDate: values.createTime && values.createTime[1].valueOf() + '' + } + + const { data } = await getUserListAPI({ query }); + setUserList(data as User[]); + } catch (error) { + setLoading(false) } - const { data } = await getUserListAPI({ query }); - setUserList(data as User[]); + setLoading(false) } return ( @@ -186,7 +217,7 @@ const UserPage = () => { -
+ @@ -227,9 +258,10 @@ const UserPage = () => { setDrawerVisible(false) }} open={drawerVisible} + loading={editLoading} > { label="角色" rules={[{ required: true, message: '请选择角色' }]} > - ({ label: item.name, value: +item.id }))} placeholder="选择用户角色" /> diff --git a/src/pages/Wall/index.tsx b/src/pages/Wall/index.tsx index 0bd4de8..359ad24 100644 --- a/src/pages/Wall/index.tsx +++ b/src/pages/Wall/index.tsx @@ -9,23 +9,35 @@ import dayjs from 'dayjs'; const WallPage = () => { const [loading, setLoading] = useState(false); - const [wall, setWall] = useState(); + + const [wall, setWall] = useState({} as Wall); const [list, setList] = useState([]); const [isModalOpen, setIsModalOpen] = useState(false); const getWallList = async () => { - const { data } = await getWallListAPI(); + try { + const { data } = await getWallListAPI(); + setList(data) + } catch (error) { + setLoading(false) + } - setList(data) setLoading(false) } const delWallData = async (id: number) => { setLoading(true) - await delWallDataAPI(id); - getWallList(); - message.success('🎉 删除留言成功'); + + try { + await delWallDataAPI(id); + await getWallList(); + message.success('🎉 删除留言成功'); + } catch (error) { + setLoading(false) + } + + setLoading(false) }; // 获取留言的分类列表 @@ -105,16 +117,24 @@ const WallPage = () => { const { RangePicker } = DatePicker; - const onSubmit = async (values: FilterForm) => { - const query: FilterWall = { - key: values.content, - cateId: values.cateId, - startDate: values.createTime && values.createTime[0].valueOf() + '', - endDate: values.createTime && values.createTime[1].valueOf() + '' + const onFilterSubmit = async (values: FilterForm) => { + setLoading(true) + + try { + const query: FilterWall = { + key: values.content, + cateId: values.cateId, + startDate: values.createTime && values.createTime[0].valueOf() + '', + endDate: values.createTime && values.createTime[1].valueOf() + '' + } + + const { data } = await getWallListAPI({ query }); + setList(data) + } catch (error) { + setLoading(false) } - const { data } = await getWallListAPI({ query }); - setList(data) + setLoading(false) } return ( @@ -122,7 +142,7 @@ const WallPage = () => { <Card className='my-2 overflow-scroll'> - <Form layout="inline" onFinish={onSubmit} autoComplete="off" className='flex-nowrap'> + <Form layout="inline" onFinish={onFilterSubmit} autoComplete="off" className='flex-nowrap'> <Form.Item label="内容" name="content" className='min-w-[200px]'> <Input placeholder='请输入内容关键词' /> </Form.Item> diff --git a/src/pages/Web/index.tsx b/src/pages/Web/index.tsx index 4281636..f0c66eb 100644 --- a/src/pages/Web/index.tsx +++ b/src/pages/Web/index.tsx @@ -1,15 +1,18 @@ import { useState, useEffect } from 'react'; import { Tabs, Input, Button, Form, Spin, Empty, Card, Popconfirm, Select, message } from 'antd'; import { SearchOutlined } from '@ant-design/icons'; -import { getLinkListAPI, addLinkDataAPI, editLinkDataAPI, delLinkDataAPI, getWebTypeListAPI } from '@/api/Web'; +import { getLinkListAPI, addLinkDataAPI, editLinkDataAPI, delLinkDataAPI, getWebTypeListAPI, getLinkDataAPI } from '@/api/Web'; import { WebType, Web } from '@/types/app/web'; import Title from '@/components/Title'; import { RuleObject } from 'antd/es/form'; import './index.scss'; -const LinkPage = () => { +export default () => { const [loading, setLoading] = useState(false); const [btnLoading, setBtnLoading] = useState(false) + const [editLoading, setEditLoading] = useState(false) + + const [form] = Form.useForm(); const [tab, setTab] = useState<string>('list'); const [list, setList] = useState<Web[]>([]); @@ -23,13 +26,18 @@ const LinkPage = () => { // 获取网站列表 const getLinkList = async () => { - const { data } = await getLinkListAPI(); - data.sort((a, b) => a.order - b.order) - data.sort((a, b) => a.type.order - b.type.order) + try { + const { data } = await getLinkListAPI(); + data.sort((a, b) => a.order - b.order) + data.sort((a, b) => a.type.order - b.type.order) - setList(data as Web[]); - setListTemp(data as Web[]); - setLoading(false); + setList(data); + setListTemp(data); + } catch (error) { + setLoading(false); + } + + setLoading(false) }; // 获取网站类型列表 @@ -50,18 +58,31 @@ const LinkPage = () => { const deleteLinkData = async (id: number) => { setLoading(true); - await delLinkDataAPI(id); - message.success('🎉 删除网站成功'); - getLinkList(); + + try { + await delLinkDataAPI(id); + await getLinkList(); + message.success('🎉 删除网站成功'); + } catch (error) { + setLoading(false) + } }; - const [form] = Form.useForm(); + const editLinkData = async (record: Web) => { + setEditLoading(true) - const editLinkData = (item: Web) => { - setTab('operate'); - setIsMethod("edit"); - setLink(item); - form.setFieldsValue(item); // 回显数据 + try { + setTab('operate'); + setIsMethod("edit"); + + const { data } = await getLinkDataAPI(record.id) + setLink(data); + form.setFieldsValue(data); + } catch (error) { + setEditLoading(false) + } + + setEditLoading(false) }; // 做一些初始化的事情 @@ -79,19 +100,23 @@ const LinkPage = () => { const submit = async () => { setBtnLoading(true) - form.validateFields().then(async (values: Web) => { - if (isMethod === "edit") { - await editLinkDataAPI({ ...link, ...values }); - message.success('🎉 编辑网站成功'); - } else { - await addLinkDataAPI({ ...values, createTime: new Date().getTime().toString() }); - message.success('🎉 新增网站成功'); - } + try { + form.validateFields().then(async (values: Web) => { + if (isMethod === "edit") { + await editLinkDataAPI({ ...link, ...values }); + message.success('🎉 编辑网站成功'); + } else { + await addLinkDataAPI({ ...values, createTime: new Date().getTime().toString() }); + message.success('🎉 新增网站成功'); + } - getLinkList(); - setTab('list'); - reset() - }); + await getLinkList(); + reset() + setTab('list'); + }); + } catch (error) { + setBtnLoading(false) + } setBtnLoading(false) }; @@ -164,47 +189,49 @@ const LinkPage = () => { <> <h2 className="text-xl pb-4 text-center">{isMethod === "edit" ? '编辑网站' : '新增网站'}</h2> - <div className='w-full md:w-[500px] mx-auto'> - <Form form={form} layout="vertical" size='large' initialValues={link} onFinish={submit}> - <Form.Item label="网站标题" name="title" rules={[{ required: true, message: '网站标题不能为空' }]}> - <Input placeholder="Thrive" /> - </Form.Item> + <Spin spinning={editLoading}> + <div className='w-full md:w-[500px] mx-auto'> + <Form form={form} layout="vertical" size='large' initialValues={link} onFinish={submit}> + <Form.Item label="网站标题" name="title" rules={[{ required: true, message: '网站标题不能为空' }]}> + <Input placeholder="Thrive" /> + </Form.Item> - <Form.Item label="网站描述" name="description" rules={[{ required: true, message: '网站描述不能为空' }]}> - <Input placeholder="记录前端、Python、Java点点滴滴" /> - </Form.Item> + <Form.Item label="网站描述" name="description" rules={[{ required: true, message: '网站描述不能为空' }]}> + <Input placeholder="记录前端、Python、Java点点滴滴" /> + </Form.Item> - <Form.Item label="站长邮箱" name="email"> - <Input placeholder="3311118881@qq.com" /> - </Form.Item> + <Form.Item label="站长邮箱" name="email"> + <Input placeholder="3311118881@qq.com" /> + </Form.Item> - <Form.Item label="网站图标" name="image" rules={[{ required: true, message: '网站图标不能为空' }]}> - <Input placeholder="https://liuyuyang.net/logo.png" /> - </Form.Item> + <Form.Item label="网站图标" name="image" rules={[{ required: true, message: '网站图标不能为空' }]}> + <Input placeholder="https://liuyuyang.net/logo.png" /> + </Form.Item> - <Form.Item label="网站链接" name="url" rules={[{ required: true, message: '网站链接不能为空' }, { validator: validateURL }]}> - <Input placeholder="https://liuyuyang.net/" /> - </Form.Item> + <Form.Item label="网站链接" name="url" rules={[{ required: true, message: '网站链接不能为空' }, { validator: validateURL }]}> + <Input placeholder="https://liuyuyang.net/" /> + </Form.Item> - <Form.Item label="订阅地址" name="rss" rules={[{ validator: validateURL }]}> - <Input placeholder="https://liuyuyang.net/api/rss" /> - </Form.Item> + <Form.Item label="订阅地址" name="rss" rules={[{ validator: validateURL }]}> + <Input placeholder="https://liuyuyang.net/api/rss" /> + </Form.Item> - <Form.Item name="typeId" label="网站类型" rules={[{ required: true, message: '网站类型不能为空' }]}> - <Select placeholder="请选择网站类型" allowClear> - {typeList.map(item => <Option key={item.id} value={item.id}>{item.name}</Option>)} - </Select> - </Form.Item> + <Form.Item name="typeId" label="网站类型" rules={[{ required: true, message: '网站类型不能为空' }]}> + <Select placeholder="请选择网站类型" allowClear> + {typeList.map(item => <Option key={item.id} value={item.id}>{item.name}</Option>)} + </Select> + </Form.Item> - <Form.Item label="顺序" name="order"> - <Input placeholder="请输入网站顺序(值越小越靠前)" /> - </Form.Item> + <Form.Item label="顺序" name="order"> + <Input placeholder="请输入网站顺序(值越小越靠前)" /> + </Form.Item> - <Form.Item> - <Button type="primary" htmlType="submit" loading={btnLoading} className='w-full'>{isMethod === "edit" ? '编辑网站' : '新增网站'}</Button> - </Form.Item> - </Form> - </div> + <Form.Item> + <Button type="primary" htmlType="submit" loading={btnLoading} className='w-full'>{isMethod === "edit" ? '编辑网站' : '新增网站'}</Button> + </Form.Item> + </Form> + </div> + </Spin> </> ), }, @@ -219,6 +246,4 @@ const LinkPage = () => { </Card> </> ); -}; - -export default LinkPage; \ No newline at end of file +}; \ No newline at end of file diff --git a/src/pages/Work/components/List/index.tsx b/src/pages/Work/components/List/index.tsx new file mode 100644 index 0000000..726e873 --- /dev/null +++ b/src/pages/Work/components/List/index.tsx @@ -0,0 +1,210 @@ +import { useState } from "react"; +import { Button, Dropdown, message, Modal } from "antd"; +import { delLinkDataAPI, auditWebDataAPI } from '@/api/Web'; +import { auditCommentDataAPI, delCommentDataAPI, addCommentDataAPI } from "@/api/Comment"; +import { auditWallDataAPI, delWallDataAPI } from "@/api/Wall"; + +import dayjs from 'dayjs'; +import RandomAvatar from "@/components/RandomAvatar"; + +import { useUserStore, useWebStore } from '@/stores'; +import TextArea from "antd/es/input/TextArea"; +import { sendDismissEmailAPI } from "@/api/Email"; + +type Menu = "comment" | "link" | "wall"; + +interface ListItemProps { + item: any; + type: Menu; + fetchData: (type: Menu) => void; +} + +export default ({ item, type, fetchData }: ListItemProps) => { + const web = useWebStore(state => state.web) + const user = useUserStore(state => state.user) + + const [btnType, setBtnType] = useState<"reply" | "dismiss" | string>("") + + // 通过 + 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); + } + + btnType != "reply" && message.success('🎉 审核成功'); + fetchData(type); + }; + + // 回复 + const [isModalOpen, setIsModalOpen] = useState(false); + const [replyInfo, setReplyInfo] = useState("") + const handleReply = async () => { + // 审核通过评论 + await handleApproval() + + // 发送回复内容 + 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(), + }) + + message.success('🎉 回复成功'); + setIsModalOpen(false) + fetchData(type); + setReplyInfo("") + setBtnType("") + } + + // 驳回 + 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); + } + + // 有内容就发送驳回通知邮件,反之直接删除 + if (dismissInfo.trim().length) await sendDismissEmail() + + message.success('🎉 驳回成功'); + setIsModalOpen(false) + fetchData(type); + setDismissInfo("") + setBtnType("") + }; + + // 发送驳回通知邮件 + const sendDismissEmail = async () => { + // 类型名称 + let email_info = { + name: "", + type: "", + url: "" + } + + switch (type) { + case "link": + email_info = { + name: item.title, + type: "友链", + url: `${web.url}/friend`, + } + break; + case "comment": + email_info = { + name: item.name, + type: "评论", + url: `${web.url}/article/${item.articleId}`, + } + break; + case "wall": + email_info = { + name: item.name, + type: "留言", + url: `${web.url}/wall/all`, + } + break; + } + + // 有邮箱才会邮件通知 + item.email != null && await sendDismissEmailAPI({ + to: item.email, + content: dismissInfo, + recipient: email_info.name, + subject: `${email_info.type}驳回通知`, + time: dayjs(Date.now()).format('YYYY年MM月DD日 HH:mm'), + type: email_info.type, + url: email_info.url + }) + } + + + return ( + <div key={item.id}> + <div className="text-center text-xs text-[#e0e0e0] mb-4"> + {dayjs(+item.createTime!).format('YYYY-MM-DD HH:mm:ss')} + </div> + + <div className="flex justify-between md:p-7 rounded-md transition-colors"> + <div className="flex mr-10"> + {type !== "wall" ? ( + <img src={item.avatar || item.image} alt="" className="w-13 h-13 border border-[#eee] rounded-full" /> + ) : <RandomAvatar className="w-13 h-13 border border-[#eee] rounded-full" />} + + <div className="flex flex-col justify-center ml-4 px-4 py-2 min-w-[210px] text-xs md:text-sm bg-[#F9F9FD] dark:bg-[#4e5969] rounded-md"> + {type === "link" ? ( + <> + <div>名称:{item.title}</div> + <div>介绍:{item.description}</div> + <div>类型:{item.type.name}</div> + <div>网站:{item?.url ? <a href={item?.url} target='_blank' className="hover:text-primary font-bold">{item?.url}</a> : '无网站'}</div> + </> + ) : type === "comment" ? ( + <> + <div>名称:{item.name}</div> + <div>内容:{item.content}</div> + <div>网站:{item?.url ? <a href={item?.url} target='_blank' className="hover:text-primary font-bold">{item?.url}</a> : '无网站'}</div> + <div>所属文章:<a href={`${web.url}/article/${item.articleId}`} target='_blank' className="hover:text-primary">{item.articleTitle || '暂无'}</a></div> + </> + ) : ( + <> + <div>名称:{item.name}</div> + <div>内容:{item.content}</div> + </> + )} + + <div>邮箱:{item.email || '暂无'}</div> + </div> + </div> + + <div className="flex items-end"> + <Dropdown menu={{ + items: type === "comment" + ? [ + { key: 'ok', label: "通过", onClick: handleApproval }, + { key: 'reply', label: "回复", onClick: () => [setIsModalOpen(true), setBtnType("reply")] }, + { key: 'dismiss', label: "驳回", onClick: () => [setIsModalOpen(true), , setBtnType("dismiss")] } + ] + : [ + { key: 'ok', label: "通过", onClick: handleApproval }, + { key: 'dismiss', label: "驳回", onClick: () => [setIsModalOpen(true), , setBtnType("dismiss")] } + ] + }}> + <div className="flex justify-evenly items-center bg-[#F9F9FD] dark:bg-[#4e5969] w-11 h-5 rounded-md cursor-pointer"> + <span className="inline-block w-2 h-2 bg-[#b5c2d3] rounded-full"></span> + <span className="inline-block w-2 h-2 bg-[#b5c2d3] rounded-full"></span> + </div> + </Dropdown> + </div> + </div> + + <Modal title={btnType === "reply" ? "回复内容" : "驳回原因"} open={isModalOpen} footer={null} onCancel={() => setIsModalOpen(false)} onClose={() => setIsModalOpen(false)}> + <TextArea + value={btnType === "reply" ? replyInfo : dismissInfo} + onChange={(e) => (btnType === "reply" ? setReplyInfo(e.target.value) : setDismissInfo(e.target.value))} + placeholder={btnType === "reply" ? "请输入回复内容" : "请输入驳回原因"} + autoSize={{ minRows: 3, maxRows: 5 }} + /> + + <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> + </div> + </Modal> + </div> + ) +} \ No newline at end of file diff --git a/src/pages/Work/index.tsx b/src/pages/Work/index.tsx index 8968857..e11f3b0 100644 --- a/src/pages/Work/index.tsx +++ b/src/pages/Work/index.tsx @@ -1,8 +1,8 @@ import { useEffect, useState } from "react"; -import { Button, Card, Dropdown, message, Modal } from "antd"; -import { getLinkListAPI, delLinkDataAPI, auditWebDataAPI } from '@/api/Web'; -import { getCommentListAPI, auditCommentDataAPI, delCommentDataAPI, addCommentDataAPI } from "@/api/Comment"; -import { getWallListAPI, auditWallDataAPI, delWallDataAPI } from "@/api/Wall"; +import { Card, Spin } from "antd"; +import { getLinkListAPI } from '@/api/Web'; +import { getCommentListAPI } from "@/api/Comment"; +import { getWallListAPI } from "@/api/Wall"; import Title from "@/components/Title"; @@ -10,213 +10,14 @@ import comment from './image/comment.svg'; import info from './image/message.svg'; import link from './image/link.svg'; -import dayjs from 'dayjs'; -import RandomAvatar from "@/components/RandomAvatar"; import Empty from "@/components/Empty"; - -import { useUserStore, useWebStore } from '@/stores'; -import TextArea from "antd/es/input/TextArea"; -import { sendDismissEmailAPI } from "@/api/Email"; +import List from "./components/List"; type Menu = "comment" | "link" | "wall"; -interface ListItemProps { - item: any; - type: Menu; - fetchData: (type: Menu) => void; -} +export default () => { + const [loading, setLoading] = useState(false) -const ListItem = ({ item, type, fetchData }: ListItemProps) => { - const web = useWebStore(state => state.web) - const user = useUserStore(state => state.user) - - const [btnType, setBtnType] = useState<"reply" | "dismiss" | string>("") - - // 通过 - 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); - } - - btnType != "reply" && message.success('🎉 审核成功'); - fetchData(type); - }; - - // 回复 - const [isModalOpen, setIsModalOpen] = useState(false); - const [replyInfo, setReplyInfo] = useState("") - const handleReply = async () => { - // 审核通过评论 - await handleApproval() - - // 发送回复内容 - 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(), - }) - - message.success('🎉 回复成功'); - setIsModalOpen(false) - fetchData(type); - setReplyInfo("") - setBtnType("") - } - - // 驳回 - 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); - } - - // 有内容就发送驳回通知邮件,反之直接删除 - if (dismissInfo.trim().length) await sendDismissEmail() - - message.success('🎉 驳回成功'); - setIsModalOpen(false) - fetchData(type); - setDismissInfo("") - setBtnType("") - }; - - // 发送驳回通知邮件 - const sendDismissEmail = async () => { - // 类型名称 - let email_info = { - name: "", - type: "", - url: "" - } - - switch (type) { - case "link": - email_info = { - name: item.title, - type: "友链", - url: `${web.url}/friend`, - } - break; - case "comment": - email_info = { - name: item.name, - type: "评论", - url: `${web.url}/article/${item.articleId}`, - } - break; - case "wall": - email_info = { - name: item.name, - type: "留言", - url: `${web.url}/wall/all`, - } - break; - } - - // 有邮箱才会邮件通知 - item.email != null && await sendDismissEmailAPI({ - to: item.email, - content: dismissInfo, - recipient: email_info.name, - subject: `${email_info.type}驳回通知`, - time: dayjs(Date.now()).format('YYYY年MM月DD日 HH:mm'), - type: email_info.type, - url: email_info.url - }) - } - - - return ( - <div key={item.id}> - <div className="text-center text-xs text-[#e0e0e0] mb-4"> - {dayjs(+item.createTime!).format('YYYY-MM-DD HH:mm:ss')} - </div> - - <div className="flex justify-between md:p-7 rounded-md transition-colors"> - <div className="flex mr-10"> - {type !== "wall" ? ( - <img src={item.avatar || item.image} alt="" className="w-13 h-13 border border-[#eee] rounded-full" /> - ) : <RandomAvatar className="w-13 h-13 border border-[#eee] rounded-full" />} - - <div className="flex flex-col justify-center ml-4 px-4 py-2 min-w-[210px] text-xs md:text-sm bg-[#F9F9FD] dark:bg-[#4e5969] rounded-md"> - {type === "link" ? ( - <> - <div>名称:{item.title}</div> - <div>介绍:{item.description}</div> - <div>类型:{item.type.name}</div> - <div>网站:{item?.url ? <a href={item?.url} target='_blank' className="hover:text-primary font-bold">{item?.url}</a> : '无网站'}</div> - </> - ) : type === "comment" ? ( - <> - <div>名称:{item.name}</div> - <div>内容:{item.content}</div> - <div>网站:{item?.url ? <a href={item?.url} target='_blank' className="hover:text-primary font-bold">{item?.url}</a> : '无网站'}</div> - <div>所属文章:<a href={`${web.url}/article/${item.articleId}`} target='_blank' className="hover:text-primary">{item.articleTitle || '暂无'}</a></div> - </> - ) : ( - <> - <div>名称:{item.name}</div> - <div>内容:{item.content}</div> - </> - )} - - <div>邮箱:{item.email || '暂无'}</div> - </div> - </div> - - <div className="flex items-end"> - <Dropdown menu={{ - items: type === "comment" - ? [ - { key: 'ok', label: "通过", onClick: handleApproval }, - { key: 'reply', label: "回复", onClick: () => [setIsModalOpen(true), setBtnType("reply")] }, - { key: 'dismiss', label: "驳回", onClick: () => [setIsModalOpen(true), , setBtnType("dismiss")] } - ] - : [ - { key: 'ok', label: "通过", onClick: handleApproval }, - { key: 'dismiss', label: "驳回", onClick: () => [setIsModalOpen(true), , setBtnType("dismiss")] } - ] - }}> - <div className="flex justify-evenly items-center bg-[#F9F9FD] dark:bg-[#4e5969] w-11 h-5 rounded-md cursor-pointer"> - <span className="inline-block w-2 h-2 bg-[#b5c2d3] rounded-full"></span> - <span className="inline-block w-2 h-2 bg-[#b5c2d3] rounded-full"></span> - </div> - </Dropdown> - </div> - </div> - - <Modal title={btnType === "reply" ? "回复内容" : "驳回原因"} open={isModalOpen} footer={null} onCancel={() => setIsModalOpen(false)} onClose={() => setIsModalOpen(false)}> - <TextArea - value={btnType === "reply" ? replyInfo : dismissInfo} - onChange={(e) => (btnType === "reply" ? setReplyInfo(e.target.value) : setDismissInfo(e.target.value))} - placeholder={btnType === "reply" ? "请输入回复内容" : "请输入驳回原因"} - autoSize={{ minRows: 3, maxRows: 5 }} - /> - - <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> - </div> - </Modal> - </div> - ) -} - -const WorkPage = () => { const activeSty = "bg-[#f9f9ff] dark:bg-[#3c5370] text-primary"; const [active, setActive] = useState<Menu>("comment"); const [commentList, setCommentList] = useState<any[]>([]); @@ -225,19 +26,26 @@ const WorkPage = () => { // 重新获取最新数据 const fetchData = async (type: Menu) => { - if (type === "comment") { - const { data } = await getCommentListAPI({ query: { status: 0 }, pattern: "list" }); - setCommentList(data); - } else if (type === "link") { - const { data } = await getLinkListAPI({ query: { status: 0 } }); - setLinkList(data); - } else if (type === "wall") { - const { data } = await getWallListAPI({ query: { status: 0 } }); - setWallList(data); + try { + if (type === "comment") { + const { data } = await getCommentListAPI({ query: { status: 0 }, pattern: "list" }); + setCommentList(data); + } else if (type === "link") { + const { data } = await getLinkListAPI({ query: { status: 0 } }); + setLinkList(data); + } else if (type === "wall") { + const { data } = await getWallListAPI({ query: { status: 0 } }); + setWallList(data); + } + } catch (error) { + setLoading(false) } + + setLoading(false) }; useEffect(() => { + setLoading(true) fetchData(active); }, [active]); @@ -246,39 +54,40 @@ const WorkPage = () => { return <Empty />; } return list.map(item => ( - <ListItem key={item.id} item={item} type={type} fetchData={(type) => fetchData(type)} /> + <List key={item.id} item={item} type={type} fetchData={(type) => fetchData(type)} /> )); }; return ( <> <Title value="工作台" /> - <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> - <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")} + <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> + + <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> - </div> - </Card> + </Card> + </Spin> </> ); -} - -export default WorkPage; \ No newline at end of file +} \ No newline at end of file