375 lines
11 KiB
TypeScript
375 lines
11 KiB
TypeScript
import { useEffect, useCallback, useState } from 'react';
|
||
import type { CSSProperties } from 'react';
|
||
import {
|
||
Layout,
|
||
Nav,
|
||
Button,
|
||
Breadcrumb,
|
||
BackTop,
|
||
Avatar,
|
||
Toast,
|
||
SideSheet,
|
||
Row,
|
||
List,
|
||
ButtonGroup,
|
||
Typography,
|
||
} from '@douyinfe/semi-ui';
|
||
import {
|
||
IconBell,
|
||
IconExit,
|
||
IconArrowUp,
|
||
IconMenu,
|
||
IconCode,
|
||
} from '@douyinfe/semi-icons';
|
||
import { IconDescriptions } from '@douyinfe/semi-icons-lab';
|
||
|
||
import { useAuthStore } from '../stores/useAuthStore';
|
||
import { useNoteStore } from '../stores/useNoteStore';
|
||
import { useUIStore } from '../stores/useUIStore';
|
||
import noteService from '../services/note.service';
|
||
import authService from '../services/auth.service';
|
||
|
||
import NoteTree from '../components/sidebar/NoteTree';
|
||
import NoteEditor from '../components/note/NoteEditor';
|
||
import HomePage from './home/HomePage';
|
||
import DeleteModal from '../components/modals/DeleteModal';
|
||
import PasswordModal from '../components/modals/PasswordModal';
|
||
import TrashListModal from '../components/modals/TrashListModal';
|
||
|
||
import type { INote } from '../types';
|
||
|
||
import './styles.css';
|
||
|
||
const topStyle: CSSProperties = {
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
height: 30,
|
||
width: 30,
|
||
borderRadius: '100%',
|
||
backgroundColor: 'var(--semi-color-primary)',
|
||
color: '#fff',
|
||
bottom: 65,
|
||
right: 20,
|
||
};
|
||
|
||
export default function NotePage() {
|
||
const user = useAuthStore((s) => s.user);
|
||
const { setTreeData, setHomeData, setViewData } = useNoteStore();
|
||
const {
|
||
activeView,
|
||
siderFlag,
|
||
sideVisible,
|
||
setActiveView,
|
||
setSiderFlag,
|
||
setSideVisible,
|
||
setDeleteModalVisible,
|
||
setUpdatePasswordModalVisible,
|
||
} = useUIStore();
|
||
|
||
const [trashList, setTrashList] = useState<INote[]>([]);
|
||
|
||
const loadTree = useCallback(() => {
|
||
noteService.getAllContent().then(
|
||
(res) => setTreeData(res.data),
|
||
() => Toast.error('加载笔记树失败'),
|
||
);
|
||
}, [setTreeData]);
|
||
|
||
useEffect(() => {
|
||
loadTree();
|
||
noteService.getHomeData('1', '0').then(
|
||
(res) => setHomeData(res.data),
|
||
() => {},
|
||
);
|
||
noteService.getHomeData('3', '0').then(
|
||
(res) => setViewData(res.data.slice(0, 10)),
|
||
() => {},
|
||
);
|
||
}, [loadTree, setHomeData, setViewData]);
|
||
|
||
const handleLogout = useCallback(() => {
|
||
void authService.logout();
|
||
}, []);
|
||
|
||
const handleNavItemClick = useCallback(
|
||
(item: string) => {
|
||
if (item === '首页') {
|
||
setActiveView('home');
|
||
} else if (item === '设置') {
|
||
setActiveView('settings');
|
||
} else if (item === '已删除') {
|
||
noteService.getHomeData('4', '0').then(
|
||
(res) => {
|
||
setTrashList(res.data);
|
||
setDeleteModalVisible(true);
|
||
},
|
||
() => Toast.error('加载回收站失败'),
|
||
);
|
||
}
|
||
},
|
||
[setActiveView, setDeleteModalVisible],
|
||
);
|
||
|
||
const handleSearch = useCallback(() => {
|
||
const query = useUIStore.getState().searchQuery;
|
||
setActiveView('search');
|
||
noteService.getHomeData('0', query).then(
|
||
(res) => setHomeData(res.data),
|
||
() => Toast.error('搜索失败'),
|
||
);
|
||
}, [setActiveView, setHomeData]);
|
||
|
||
const handleBreakpoint = useCallback(
|
||
(_screen: string, matched: boolean) => {
|
||
setSiderFlag(matched);
|
||
},
|
||
[setSiderFlag],
|
||
);
|
||
|
||
const { Header, Footer, Sider, Content } = Layout;
|
||
const siderWidth = siderFlag ? '265px' : '0px';
|
||
|
||
return (
|
||
<Layout style={{ border: '1px solid var(--semi-color-border)', height: '100vh', overflow: 'hidden' }}>
|
||
<Header style={{ backgroundColor: 'var(--semi-color-bg-1)', flexShrink: 0 }}>
|
||
<Nav mode="horizontal" defaultSelectedKeys={['Home']}>
|
||
<Nav.Header>
|
||
<IconDescriptions style={{ height: '36px', fontSize: 36 }} />
|
||
</Nav.Header>
|
||
<span style={{ color: 'var(--semi-color-text-0)', fontWeight: 600, marginRight: 24 }}>
|
||
首页
|
||
</span>
|
||
<Nav.Footer>
|
||
{!siderFlag && (
|
||
<>
|
||
<Button
|
||
theme="borderless"
|
||
icon={<IconMenu size="large" />}
|
||
style={{ color: 'var(--semi-color-text-2)', marginRight: 12 }}
|
||
onClick={() => setSideVisible(!sideVisible)}
|
||
/>
|
||
<SideSheet
|
||
title="菜单"
|
||
width={335}
|
||
visible={sideVisible}
|
||
onCancel={() => setSideVisible(false)}
|
||
>
|
||
<NoteTree
|
||
onRefreshTree={loadTree}
|
||
onNavItemClick={handleNavItemClick}
|
||
onSearch={handleSearch}
|
||
/>
|
||
</SideSheet>
|
||
</>
|
||
)}
|
||
<Button
|
||
theme="borderless"
|
||
icon={<IconBell size="large" />}
|
||
style={{ color: 'var(--semi-color-text-2)', marginRight: 12 }}
|
||
/>
|
||
<Button
|
||
theme="borderless"
|
||
icon={<IconExit size="large" />}
|
||
style={{ color: 'var(--semi-color-text-2)', marginRight: 12 }}
|
||
onClick={handleLogout}
|
||
/>
|
||
<Avatar color="orange" size="small">
|
||
{user?.username ?? ''}
|
||
</Avatar>
|
||
</Nav.Footer>
|
||
</Nav>
|
||
</Header>
|
||
|
||
<Layout style={{ flex: 1, minHeight: 0 }}>
|
||
<Sider
|
||
style={{ width: siderWidth, backgroundColor: 'var(--semi-color-bg-1)', overflowY: 'auto' }}
|
||
breakpoint={['md']}
|
||
onBreakpoint={handleBreakpoint}
|
||
>
|
||
{siderFlag && (
|
||
<NoteTree
|
||
onRefreshTree={loadTree}
|
||
onNavItemClick={handleNavItemClick}
|
||
onSearch={handleSearch}
|
||
/>
|
||
)}
|
||
</Sider>
|
||
|
||
{/* Delete confirmation modal */}
|
||
<DeleteModal onDeleted={loadTree} />
|
||
|
||
{/* Change password modal */}
|
||
<PasswordModal />
|
||
|
||
{/* Trash / deleted notes modal */}
|
||
<TrashListModal trashList={trashList} onRestored={loadTree} />
|
||
|
||
{/* Main content area */}
|
||
{activeView === 'note' && (
|
||
<Content style={{ padding: '0 10px 10px', backgroundColor: 'var(--semi-color-bg-0)', overflowY: 'auto' }}>
|
||
<NoteEditor />
|
||
</Content>
|
||
)}
|
||
|
||
{activeView === 'home' && (
|
||
<Content style={{ padding: '10px', backgroundColor: 'var(--semi-color-bg-0)', overflowY: 'auto' }}>
|
||
<Breadcrumb style={{ marginBottom: 24 }} routes={['首页']} />
|
||
<div
|
||
style={{
|
||
borderRadius: 10,
|
||
border: '1px solid var(--semi-color-border)',
|
||
padding: 10,
|
||
}}
|
||
>
|
||
<HomePage />
|
||
</div>
|
||
</Content>
|
||
)}
|
||
|
||
{activeView === 'settings' && (
|
||
<Content style={{ padding: '10px', backgroundColor: 'var(--semi-color-bg-0)', overflowY: 'auto' }}>
|
||
<Breadcrumb style={{ marginBottom: 24 }} routes={['设置']} />
|
||
<div
|
||
style={{
|
||
borderRadius: 10,
|
||
border: '1px solid var(--semi-color-border)',
|
||
padding: 15,
|
||
}}
|
||
>
|
||
Settings
|
||
<hr />
|
||
<Row>
|
||
<Button
|
||
theme="solid"
|
||
type="secondary"
|
||
style={{ marginRight: 8 }}
|
||
onClick={() => setUpdatePasswordModalVisible(true)}
|
||
>
|
||
修改密码
|
||
</Button>
|
||
</Row>
|
||
<br />
|
||
<Row>
|
||
<Button
|
||
theme="solid"
|
||
type="tertiary"
|
||
style={{ marginRight: 8 }}
|
||
onClick={() => {
|
||
void noteService
|
||
.historyQueryOrDelete(0)
|
||
.then((res) => Toast.info(`历史记录数:${String(res.data)}`));
|
||
}}
|
||
>
|
||
查看历史记录Count
|
||
</Button>
|
||
</Row>
|
||
<br />
|
||
<Row>
|
||
<Button
|
||
theme="solid"
|
||
type="danger"
|
||
style={{ marginRight: 8 }}
|
||
onClick={() => {
|
||
void noteService
|
||
.historyQueryOrDelete(1)
|
||
.then(() => Toast.success('历史记录已清空'));
|
||
}}
|
||
>
|
||
删除全部历史记录
|
||
</Button>
|
||
</Row>
|
||
</div>
|
||
</Content>
|
||
)}
|
||
|
||
{activeView === 'search' && (
|
||
<Content style={{ padding: 24, backgroundColor: 'var(--semi-color-bg-0)', overflowY: 'auto' }}>
|
||
<Breadcrumb style={{ marginBottom: 24 }} routes={['搜索结果']} />
|
||
<div
|
||
style={{
|
||
borderRadius: 10,
|
||
border: '1px solid var(--semi-color-border)',
|
||
padding: 32,
|
||
}}
|
||
>
|
||
<SearchResults onViewNote={(id) => {
|
||
setActiveView('note');
|
||
noteService.get(id).then(
|
||
(res) => {
|
||
const note = res.data;
|
||
useNoteStore.getState().setCurrentNote(note.id, note.title, note.context);
|
||
},
|
||
() => Toast.error('加载失败'),
|
||
);
|
||
}} />
|
||
</div>
|
||
</Content>
|
||
)}
|
||
</Layout>
|
||
|
||
<Footer
|
||
style={{
|
||
display: 'flex',
|
||
justifyContent: 'space-between',
|
||
padding: '20px',
|
||
color: 'var(--semi-color-text-2)',
|
||
backgroundColor: 'rgba(var(--semi-grey-0), 1)',
|
||
}}
|
||
>
|
||
<span style={{ display: 'flex', alignItems: 'center' }}>
|
||
<IconCode size="large" style={{ marginRight: 8 }} />
|
||
<span>Copyright © 2023 Chuyaoyuan. CYY.IM.</span>
|
||
</span>
|
||
<span>
|
||
<span style={{ marginRight: 24 }}>客服</span>
|
||
<span>反馈建议</span>
|
||
</span>
|
||
</Footer>
|
||
|
||
<BackTop style={topStyle}>
|
||
<IconArrowUp />
|
||
</BackTop>
|
||
</Layout>
|
||
);
|
||
}
|
||
|
||
/** Inline search results — reads homeData (set by search) from store */
|
||
function SearchResults({ onViewNote }: { onViewNote: (id: number) => void }) {
|
||
const homeData = useNoteStore((s) => s.homeData);
|
||
const searchQuery = useUIStore((s) => s.searchQuery);
|
||
return (
|
||
<>
|
||
<p>搜索:{searchQuery}</p>
|
||
<div style={{ padding: 12, border: '1px solid var(--semi-color-border)', margin: 12 }}>
|
||
<List
|
||
dataSource={homeData as INote[]}
|
||
renderItem={(item: INote) => (
|
||
<List.Item
|
||
header={<Avatar color="blue">NOTE</Avatar>}
|
||
main={
|
||
<div>
|
||
<span style={{ color: 'var(--semi-color-text-0)', fontWeight: 500 }}>
|
||
{item.title}
|
||
</span>
|
||
<Typography.Text
|
||
ellipsis={{ showTooltip: true }}
|
||
style={{ width: 'calc(100% - 68px)' }}
|
||
>
|
||
创建时间:{item.createtime},更新时间:{item.updatetime}。
|
||
</Typography.Text>
|
||
</div>
|
||
}
|
||
extra={
|
||
<ButtonGroup theme="borderless">
|
||
<Button onClick={() => onViewNote(item.id)}>查看</Button>
|
||
</ButtonGroup>
|
||
}
|
||
/>
|
||
)}
|
||
/>
|
||
</div>
|
||
</>
|
||
);
|
||
}
|