| import { |
| Avatar, |
| Button, |
| Divider, |
| Listbox, |
| ListboxItem, |
| ListboxSection, |
| Modal, |
| ModalBody, |
| ModalContent, |
| ModalFooter, |
| ModalHeader, |
| Switch, |
| Textarea, |
| Tooltip, |
| useDisclosure, |
| Link, |
| } from '@nextui-org/react'; |
| import { PlusIcon } from '@web/components/PlusIcon'; |
| import { trpc } from '@web/utils/trpc'; |
| import { useMemo, useState } from 'react'; |
| import { useNavigate, useParams } from 'react-router-dom'; |
| import { toast } from 'sonner'; |
| import dayjs from 'dayjs'; |
| import { serverOriginUrl } from '@web/utils/env'; |
| import ArticleList from './list'; |
|
|
| const Feeds = () => { |
| const { id } = useParams(); |
|
|
| const { isOpen, onOpen, onOpenChange, onClose } = useDisclosure(); |
| const { refetch: refetchFeedList, data: feedData } = trpc.feed.list.useQuery( |
| { |
| limit: 100, |
| }, |
| { |
| refetchOnWindowFocus: true, |
| }, |
| ); |
|
|
| const navigate = useNavigate(); |
|
|
| const queryUtils = trpc.useUtils(); |
|
|
| const { mutateAsync: getMpInfo, isLoading: isGetMpInfoLoading } = |
| trpc.platform.getMpInfo.useMutation({}); |
| const { mutateAsync: updateMpInfo } = trpc.feed.edit.useMutation({}); |
|
|
| const { mutateAsync: addFeed, isLoading: isAddFeedLoading } = |
| trpc.feed.add.useMutation({}); |
| const { mutateAsync: refreshMpArticles, isLoading: isGetArticlesLoading } = |
| trpc.feed.refreshArticles.useMutation(); |
|
|
| const { mutateAsync: deleteFeed, isLoading: isDeleteFeedLoading } = |
| trpc.feed.delete.useMutation({}); |
|
|
| const [wxsLink, setWxsLink] = useState(''); |
|
|
| const [currentMpId, setCurrentMpId] = useState(id || ''); |
|
|
| const handleConfirm = async () => { |
| |
| const res = await getMpInfo({ wxsLink: wxsLink }); |
| if (res[0]) { |
| const item = res[0]; |
| await addFeed({ |
| id: item.id, |
| mpName: item.name, |
| mpCover: item.cover, |
| mpIntro: item.intro, |
| updateTime: item.updateTime, |
| status: 1, |
| }); |
| await refreshMpArticles(item.id); |
|
|
| toast.success('添加成功', { |
| description: `公众号 ${item.name}`, |
| }); |
| refetchFeedList(); |
| setWxsLink(''); |
| onClose(); |
| await queryUtils.article.list.reset(); |
| } else { |
| toast.error('添加失败', { description: '请检查链接是否正确' }); |
| } |
| }; |
|
|
| const isActive = (key: string) => { |
| return currentMpId === key; |
| }; |
|
|
| const currentMpInfo = useMemo(() => { |
| return feedData?.items.find((item) => item.id === currentMpId); |
| }, [currentMpId, feedData?.items]); |
|
|
| return ( |
| <> |
| <div className="h-full flex justify-between"> |
| <div className="w-64 p-4 h-full"> |
| <div className="pb-4 flex justify-between align-middle items-center"> |
| <Button |
| color="primary" |
| size="sm" |
| onPress={onOpen} |
| endContent={<PlusIcon />} |
| > |
| 添加 |
| </Button> |
| <div className="font-normal text-sm"> |
| 共{feedData?.items.length || 0}个订阅 |
| </div> |
| </div> |
| |
| {feedData?.items ? ( |
| <Listbox |
| aria-label="订阅源" |
| emptyContent="暂无订阅" |
| onAction={(key) => setCurrentMpId(key as string)} |
| > |
| <ListboxSection showDivider> |
| <ListboxItem |
| key={''} |
| href={`/feeds`} |
| className={isActive('') ? 'bg-primary-50 text-primary' : ''} |
| startContent={<Avatar name="ALL"></Avatar>} |
| > |
| 全部 |
| </ListboxItem> |
| </ListboxSection> |
| |
| <ListboxSection className="overflow-y-auto h-[calc(100vh-260px)]"> |
| {feedData?.items.map((item) => { |
| return ( |
| <ListboxItem |
| href={`/feeds/${item.id}`} |
| className={ |
| isActive(item.id) ? 'bg-primary-50 text-primary' : '' |
| } |
| key={item.id} |
| startContent={<Avatar src={item.mpCover}></Avatar>} |
| > |
| {item.mpName} |
| </ListboxItem> |
| ); |
| }) || []} |
| </ListboxSection> |
| </Listbox> |
| ) : ( |
| '' |
| )} |
| </div> |
| <div className="flex-1 h-full flex flex-col"> |
| <div className="p-4 pb-0 flex justify-between"> |
| <h3 className="text-medium font-mono flex-1 overflow-hidden text-ellipsis break-keep text-nowrap pr-1"> |
| {currentMpInfo?.mpName || '全部'} |
| </h3> |
| {currentMpInfo ? ( |
| <div className="flex h-5 items-center space-x-4 text-small"> |
| <div className="font-light"> |
| 最后更新时间: |
| {dayjs(currentMpInfo.syncTime * 1e3).format( |
| 'YYYY-MM-DD HH:mm:ss', |
| )} |
| </div> |
| <Divider orientation="vertical" /> |
| <Tooltip |
| content="频繁调用会导致一段时间内不可用!" |
| color="danger" |
| > |
| <Link |
| size="sm" |
| href="#" |
| isDisabled={isGetArticlesLoading} |
| onClick={async (ev) => { |
| ev.preventDefault(); |
| ev.stopPropagation(); |
| await refreshMpArticles(currentMpInfo.id); |
| await refetchFeedList(); |
| await queryUtils.article.list.reset(); |
| }} |
| > |
| {isGetArticlesLoading ? '更新中...' : '立即更新'} |
| </Link> |
| </Tooltip> |
| <Divider orientation="vertical" /> |
| |
| <Tooltip content="启用服务端定时更新"> |
| <div> |
| <Switch |
| size="sm" |
| onValueChange={async (value) => { |
| await updateMpInfo({ |
| id: currentMpInfo.id, |
| data: { |
| status: value ? 1 : 0, |
| }, |
| }); |
| |
| await refetchFeedList(); |
| }} |
| isSelected={currentMpInfo?.status === 1} |
| ></Switch> |
| </div> |
| </Tooltip> |
| <Divider orientation="vertical" /> |
| <Tooltip content="仅删除订阅源,已获取的文章不会被删除"> |
| <Link |
| href="#" |
| color="danger" |
| size="sm" |
| isDisabled={isDeleteFeedLoading} |
| onClick={async (ev) => { |
| ev.preventDefault(); |
| ev.stopPropagation(); |
| |
| if (window.confirm('确定删除吗?')) { |
| await deleteFeed(currentMpInfo.id); |
| navigate('/feeds'); |
| await refetchFeedList(); |
| } |
| }} |
| > |
| 删除 |
| </Link> |
| </Tooltip> |
| |
| <Divider orientation="vertical" /> |
| <Tooltip content={<div>可添加.atom/.rss/.json格式输出</div>}> |
| <Link |
| size="sm" |
| showAnchorIcon |
| target="_blank" |
| href={`${serverOriginUrl}/feeds/${currentMpInfo.id}.atom`} |
| color="foreground" |
| > |
| RSS |
| </Link> |
| </Tooltip> |
| </div> |
| ) : ( |
| <Link |
| size="sm" |
| showAnchorIcon |
| target="_blank" |
| href={`${serverOriginUrl}/feeds/all.atom`} |
| color="foreground" |
| > |
| RSS |
| </Link> |
| )} |
| </div> |
| <div className="p-2 overflow-y-auto"> |
| <ArticleList></ArticleList> |
| </div> |
| </div> |
| </div> |
| <Modal isOpen={isOpen} onOpenChange={onOpenChange}> |
| <ModalContent> |
| {(onClose) => ( |
| <> |
| <ModalHeader className="flex flex-col gap-1"> |
| 添加公众号源 |
| </ModalHeader> |
| <ModalBody> |
| <Textarea |
| value={wxsLink} |
| onValueChange={setWxsLink} |
| autoFocus |
| label="分享链接" |
| placeholder="输入公众号文章分享链接,如 https://mp.weixin.qq.com/s/xxxxxx" |
| variant="bordered" |
| /> |
| </ModalBody> |
| <ModalFooter> |
| <Button color="danger" variant="flat" onPress={onClose}> |
| 取消 |
| </Button> |
| <Button |
| color="primary" |
| isDisabled={ |
| !wxsLink.startsWith('https://mp.weixin.qq.com/s/') |
| } |
| onPress={handleConfirm} |
| isLoading={ |
| isAddFeedLoading || |
| isGetMpInfoLoading || |
| isGetArticlesLoading |
| } |
| > |
| 确定 |
| </Button> |
| </ModalFooter> |
| </> |
| )} |
| </ModalContent> |
| </Modal> |
| </> |
| ); |
| }; |
|
|
| export default Feeds; |
|
|