This commit is contained in:
houwq 2025-06-13 15:31:12 +08:00
parent 38cd670902
commit 0f772c4a64
37 changed files with 1617 additions and 434 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
client/node_modules/.cache/default-development/13.pack generated vendored Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,65 +1,107 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import styled from 'styled-components'; import { ThemeProvider } from 'styled-components';
import { theme, GlobalStyle } from './styles/theme';
import LoginForm from './components/LoginForm'; import LoginForm from './components/LoginForm';
import TodoApp from './components/TodoApp'; import TodoApp from './components/TodoApp';
import { checkAuth } from './services/api'; import styled from 'styled-components';
const AppContainer = styled.div` const AppContainer = styled.div`
min-height: 100vh; min-height: 100vh;
display: flex; display: flex;
align-items: center;
justify-content: center; justify-content: center;
padding: 20px; align-items: center;
padding: ${({ theme }) => theme.spacing.md};
background: linear-gradient(135deg, ${({ theme }) => theme.colors.primary}20, ${({ theme }) => theme.colors.secondary}20);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
@media (max-width: ${({ theme }) => theme.breakpoints.md}) {
padding: ${({ theme }) => theme.spacing.sm};
}
`;
const AppContent = styled.div`
width: 100%;
max-width: 1200px;
height: 100%;
min-height: 600px;
display: flex;
flex-direction: column;
position: relative;
`; `;
function App() { function App() {
const [isAuthenticated, setIsAuthenticated] = useState(false); const [isAuthenticated, setIsAuthenticated] = useState(false);
const [loading, setLoading] = useState(true); const [currentUser, setCurrentUser] = useState(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => { useEffect(() => {
const token = localStorage.getItem('token'); const token = localStorage.getItem('token');
if (token) { const user = localStorage.getItem('user');
checkAuth(token)
.then(() => { if (token && user) {
setIsAuthenticated(true); try {
}) const parsedUser = JSON.parse(user);
.catch(() => { setCurrentUser(parsedUser);
localStorage.removeItem('token'); setIsAuthenticated(true);
}) } catch (error) {
.finally(() => { console.error('解析用户信息失败:', error);
setLoading(false); localStorage.removeItem('token');
}); localStorage.removeItem('user');
} else { }
setLoading(false);
} }
setIsLoading(false);
}, []); }, []);
const handleLogin = (token) => { const handleLogin = (token, user) => {
localStorage.setItem('token', token); localStorage.setItem('token', token);
localStorage.setItem('user', JSON.stringify(user));
setCurrentUser(user);
setIsAuthenticated(true); setIsAuthenticated(true);
}; };
const handleLogout = () => { const handleLogout = () => {
localStorage.removeItem('token'); localStorage.removeItem('token');
localStorage.removeItem('user');
setCurrentUser(null);
setIsAuthenticated(false); setIsAuthenticated(false);
}; };
if (loading) { if (isLoading) {
return ( return (
<AppContainer> <ThemeProvider theme={theme}>
<div>加载中...</div> <GlobalStyle />
</AppContainer> <AppContainer>
<AppContent>
<div style={{
textAlign: 'center',
color: theme.colors.text.secondary,
fontSize: theme.typography.fontSize.lg
}}>
加载中...
</div>
</AppContent>
</AppContainer>
</ThemeProvider>
); );
} }
return ( return (
<AppContainer> <ThemeProvider theme={theme}>
{isAuthenticated ? ( <GlobalStyle />
<TodoApp onLogout={handleLogout} /> <AppContainer>
) : ( <AppContent>
<LoginForm onLogin={handleLogin} /> {isAuthenticated ? (
)} <TodoApp
</AppContainer> onLogout={handleLogout}
currentUser={currentUser}
/>
) : (
<LoginForm onLogin={handleLogin} />
)}
</AppContent>
</AppContainer>
</ThemeProvider>
); );
} }

View File

@ -1,64 +1,121 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import styled from 'styled-components'; import styled, { keyframes } from 'styled-components';
import { login } from '../services/api'; import { login } from '../services/api';
const LoginContainer = styled.div` const fadeIn = keyframes`
background: rgba(255, 255, 255, 0.95); from {
backdrop-filter: blur(10px); opacity: 0;
border-radius: 20px; transform: translateY(20px);
padding: 40px; }
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1); to {
width: 100%; opacity: 1;
max-width: 400px; transform: translateY(0);
text-align: center; }
`; `;
const Title = styled.h1` const LoginContainer = styled.div`
color: #333; width: 100%;
margin-bottom: 30px; max-width: 400px;
font-size: 28px; margin: 0 auto;
font-weight: 300; padding: ${({ theme }) => theme.spacing.xl};
background: ${({ theme }) => theme.colors.glass.light};
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border-radius: ${({ theme }) => theme.borderRadius.xl};
box-shadow: ${({ theme }) => theme.shadows.lg};
animation: ${fadeIn} 0.5s ease-out;
border: 1px solid ${({ theme }) => theme.colors.glass.light};
@media (prefers-color-scheme: dark) {
background: ${({ theme }) => theme.colors.glass.dark};
border-color: ${({ theme }) => theme.colors.glass.dark};
}
`;
const LoginTitle = styled.h1`
color: ${({ theme }) => theme.colors.text.primary};
font-size: ${({ theme }) => theme.typography.fontSize['3xl']};
font-weight: ${({ theme }) => theme.typography.fontWeight.bold};
text-align: center;
margin-bottom: ${({ theme }) => theme.spacing.xl};
display: flex;
align-items: center;
justify-content: center;
gap: ${({ theme }) => theme.spacing.sm};
&::before {
content: '🔒';
font-size: 1.2em;
}
`; `;
const Form = styled.form` const Form = styled.form`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 20px; gap: ${({ theme }) => theme.spacing.lg};
`;
const InputGroup = styled.div`
position: relative;
`;
const Label = styled.label`
display: block;
margin-bottom: ${({ theme }) => theme.spacing.xs};
color: ${({ theme }) => theme.colors.text.primary};
font-size: ${({ theme }) => theme.typography.fontSize.sm};
font-weight: ${({ theme }) => theme.typography.fontWeight.medium};
`; `;
const Input = styled.input` const Input = styled.input`
padding: 15px 20px; width: 100%;
border: 2px solid #e1e5e9; padding: ${({ theme }) => theme.spacing.md};
border-radius: 12px; font-size: ${({ theme }) => theme.typography.fontSize.base};
font-size: 16px; border: 2px solid ${({ theme }) => theme.colors.glass.light};
transition: all 0.3s ease; border-radius: ${({ theme }) => theme.borderRadius.lg};
outline: none; background: ${({ theme }) => theme.colors.glass.light};
color: ${({ theme }) => theme.colors.text.primary};
transition: all ${({ theme }) => theme.transitions.default};
&:focus { &:focus {
border-color: #667eea; outline: none;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); border-color: ${({ theme }) => theme.colors.primary};
box-shadow: 0 0 0 3px ${({ theme }) => theme.colors.primary}40;
} }
&::placeholder { &::placeholder {
color: #a0a0a0; color: ${({ theme }) => theme.colors.text.secondary};
}
@media (prefers-color-scheme: dark) {
background: ${({ theme }) => theme.colors.glass.dark};
border-color: ${({ theme }) => theme.colors.glass.dark};
} }
`; `;
const Button = styled.button` const SubmitButton = styled.button`
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); width: 100%;
padding: ${({ theme }) => theme.spacing.md};
font-size: ${({ theme }) => theme.typography.fontSize.base};
font-weight: ${({ theme }) => theme.typography.fontWeight.semibold};
color: white; color: white;
background: ${({ theme }) => theme.colors.primary};
border: none; border: none;
padding: 15px 20px; border-radius: ${({ theme }) => theme.borderRadius.lg};
border-radius: 12px;
font-size: 16px;
font-weight: 500;
cursor: pointer; cursor: pointer;
transition: all 0.3s ease; transition: all ${({ theme }) => theme.transitions.default};
margin-top: 10px; display: flex;
align-items: center;
justify-content: center;
gap: ${({ theme }) => theme.spacing.sm};
&:hover { &:hover {
transform: translateY(-2px); background: ${({ theme }) => theme.colors.primary}dd;
box-shadow: 0 10px 20px rgba(102, 126, 234, 0.3); transform: translateY(-1px);
}
&:active {
transform: translateY(0);
} }
&:disabled { &:disabled {
@ -66,66 +123,102 @@ const Button = styled.button`
cursor: not-allowed; cursor: not-allowed;
transform: none; transform: none;
} }
&::after {
content: '→';
font-size: 1.2em;
transition: transform ${({ theme }) => theme.transitions.default};
}
&:hover::after {
transform: translateX(4px);
}
`; `;
const ErrorMessage = styled.div` const ErrorMessage = styled.div`
color: #e74c3c; color: ${({ theme }) => theme.colors.status.error};
font-size: 14px; font-size: ${({ theme }) => theme.typography.fontSize.sm};
margin-top: 10px; text-align: center;
padding: 10px; margin-top: ${({ theme }) => theme.spacing.sm};
background: rgba(231, 76, 60, 0.1); padding: ${({ theme }) => theme.spacing.sm};
border-radius: 8px; background: ${({ theme }) => theme.colors.status.error}20;
border-radius: ${({ theme }) => theme.borderRadius.md};
animation: ${fadeIn} 0.3s ease-out;
`; `;
const Hint = styled.div` const LoginHint = styled.div`
color: #666; color: ${({ theme }) => theme.colors.text.secondary};
font-size: 14px; font-size: ${({ theme }) => theme.typography.fontSize.sm};
margin-top: 20px; text-align: center;
padding: 15px; margin-top: ${({ theme }) => theme.spacing.md};
background: rgba(102, 126, 234, 0.1); padding: ${({ theme }) => theme.spacing.sm};
border-radius: 8px; background: ${({ theme }) => theme.colors.secondary}20;
border-left: 4px solid #667eea; border-radius: ${({ theme }) => theme.borderRadius.md};
strong {
color: ${({ theme }) => theme.colors.secondary};
}
`; `;
function LoginForm({ onLogin }) { function LoginForm({ onLogin }) {
const [username, setUsername] = useState('');
const [password, setPassword] = useState(''); const [password, setPassword] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState(''); const [error, setError] = useState('');
const [isLoading, setIsLoading] = useState(false);
const handleSubmit = async (e) => { const handleSubmit = async (e) => {
e.preventDefault(); e.preventDefault();
setLoading(true);
setError(''); setError('');
setIsLoading(true);
try { try {
const response = await login(password); const result = await login(username, password);
onLogin(response.token); // 保存token和用户信息
localStorage.setItem('token', result.token);
localStorage.setItem('user', JSON.stringify(result.user));
onLogin(result.token, result.user);
} catch (err) { } catch (err) {
setError(err.response?.data?.error || '登录失败,请重试'); setError(err.response?.data?.error || '登录失败,请重试');
} finally { } finally {
setLoading(false); setIsLoading(false);
} }
}; };
return ( return (
<LoginContainer> <LoginContainer>
<Title>工作待办</Title> <LoginTitle>工作待办</LoginTitle>
<Form onSubmit={handleSubmit}> <Form onSubmit={handleSubmit}>
<Input <InputGroup>
type="password" <Label>用户名</Label>
placeholder="请输入访问密码" <Input
value={password} type="text"
onChange={(e) => setPassword(e.target.value)} placeholder="请输入用户名"
required value={username}
/> onChange={(e) => setUsername(e.target.value)}
<Button type="submit" disabled={loading}> disabled={isLoading}
{loading ? '验证中...' : '进入系统'} required
</Button> />
</InputGroup>
<InputGroup>
<Label>密码</Label>
<Input
type="password"
placeholder="请输入密码"
value={password}
onChange={(e) => setPassword(e.target.value)}
disabled={isLoading}
required
/>
</InputGroup>
<SubmitButton type="submit" disabled={isLoading}>
{isLoading ? '登录中...' : '登录'}
</SubmitButton>
{error && <ErrorMessage>{error}</ErrorMessage>}
</Form> </Form>
{error && <ErrorMessage>{error}</ErrorMessage>} <LoginHint>
<Hint> {/* <strong>管理员:</strong> 用户名 admin密码 weiMonkey2024<br /> */}
💡 提示首次使用需要输入访问密码 <strong>普通用户:</strong> 使
</Hint> </LoginHint>
</LoginContainer> </LoginContainer>
); );
} }

View File

@ -6,6 +6,7 @@ import TodoForm from './TodoForm';
import TodoList from './TodoList'; import TodoList from './TodoList';
import HistoryTodos from './HistoryTodos'; import HistoryTodos from './HistoryTodos';
import SuspendedTodos from './SuspendedTodos'; import SuspendedTodos from './SuspendedTodos';
import UserManagement from './UserManagement';
const AppContainer = styled.div` const AppContainer = styled.div`
background: rgba(255, 255, 255, 0.95); background: rgba(255, 255, 255, 0.95);
@ -29,6 +30,12 @@ const Header = styled.div`
border-bottom: 2px solid #f0f0f0; border-bottom: 2px solid #f0f0f0;
`; `;
const TitleSection = styled.div`
display: flex;
flex-direction: column;
gap: 8px;
`;
const Title = styled.h1` const Title = styled.h1`
color: #333; color: #333;
font-size: 32px; font-size: 32px;
@ -36,16 +43,61 @@ const Title = styled.h1`
margin: 0; margin: 0;
`; `;
const UserInfo = styled.div`
color: #666;
font-size: 14px;
display: flex;
align-items: center;
gap: 8px;
.username {
font-weight: 500;
color: #667eea;
}
.role {
padding: 2px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
}
.admin {
background: rgba(255, 152, 0, 0.1);
color: #ff9800;
}
.user {
background: rgba(102, 126, 234, 0.1);
color: #667eea;
}
`;
const HeaderActions = styled.div` const HeaderActions = styled.div`
display: flex; display: flex;
gap: 12px; gap: 12px;
align-items: center; align-items: center;
flex-wrap: wrap;
@media (max-width: 768px) {
flex-direction: column;
align-items: stretch;
gap: 8px;
}
`; `;
const MigratePendingButton = styled.button` const ActionButton = styled.button`
background: rgba(255, 152, 0, 0.1); background: ${({ variant, theme }) => {
color: #ff9800; switch (variant) {
border: 2px solid rgba(255, 152, 0, 0.2); case 'primary': return theme?.colors?.primary || '#667eea';
case 'warning': return '#ff9800';
case 'success': return '#00B894';
case 'danger': return '#e74c3c';
default: return 'transparent';
}
}};
color: ${({ variant }) => variant === 'default' ? '#666' : 'white'};
border: ${({ variant }) => variant === 'default' ? '2px solid #e1e5e9' : 'none'};
padding: 10px 20px; padding: 10px 20px;
border-radius: 8px; border-radius: 8px;
font-size: 14px; font-size: 14px;
@ -54,85 +106,27 @@ const MigratePendingButton = styled.button`
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
white-space: nowrap;
&:hover { &:hover {
background: rgba(255, 152, 0, 0.15);
border-color: #ff9800;
transform: translateY(-1px); transform: translateY(-1px);
${({ variant }) => {
if (variant === 'default') {
return `
border-color: #667eea;
color: #667eea;
`;
}
return 'opacity: 0.9;';
}}
} }
&::before { &::before {
content: '📦'; content: ${({ icon }) => icon ? `'${icon}'` : 'none'};
font-size: 16px; font-size: 16px;
} }
`; `;
const SuspendedButton = styled.button`
background: rgba(255, 152, 0, 0.1);
color: #ff9800;
border: 2px solid rgba(255, 152, 0, 0.2);
padding: 10px 20px;
border-radius: 8px;
font-size: 14px;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 8px;
&:hover {
background: rgba(255, 152, 0, 0.15);
border-color: #ff9800;
transform: translateY(-1px);
}
&::before {
content: '⏸️';
font-size: 16px;
}
`;
const HistoryButton = styled.button`
background: rgba(102, 126, 234, 0.1);
color: #667eea;
border: 2px solid rgba(102, 126, 234, 0.2);
padding: 10px 20px;
border-radius: 8px;
font-size: 14px;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 8px;
&:hover {
background: rgba(102, 126, 234, 0.15);
border-color: #667eea;
transform: translateY(-1px);
}
&::before {
content: '📚';
font-size: 16px;
}
`;
const LogoutButton = styled.button`
background: transparent;
color: #666;
border: 2px solid #e1e5e9;
padding: 10px 20px;
border-radius: 8px;
font-size: 14px;
cursor: pointer;
transition: all 0.3s ease;
&:hover {
border-color: #667eea;
color: #667eea;
}
`;
const Content = styled.div` const Content = styled.div`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -156,12 +150,13 @@ const EmptyMessage = styled.div`
border: 2px dashed #e1e5e9; border: 2px dashed #e1e5e9;
`; `;
function TodoApp({ onLogout }) { function TodoApp({ onLogout, currentUser }) {
const [todos, setTodos] = useState([]); const [todos, setTodos] = useState([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState(''); const [error, setError] = useState('');
const [showHistory, setShowHistory] = useState(false); const [showHistory, setShowHistory] = useState(false);
const [showSuspended, setShowSuspended] = useState(false); const [showSuspended, setShowSuspended] = useState(false);
const [showUserManagement, setShowUserManagement] = useState(false);
const [successMessage, setSuccessMessage] = useState(''); const [successMessage, setSuccessMessage] = useState('');
useEffect(() => { useEffect(() => {
@ -292,6 +287,15 @@ function TodoApp({ onLogout }) {
const groupedTodos = groupTodosByDate(todos); const groupedTodos = groupTodosByDate(todos);
// 如果正在显示用户管理视图
if (showUserManagement) {
return (
<AppContainer>
<UserManagement onBack={() => setShowUserManagement(false)} />
</AppContainer>
);
}
// 如果正在显示历史视图 // 如果正在显示历史视图
if (showHistory) { if (showHistory) {
return ( return (
@ -321,18 +325,54 @@ function TodoApp({ onLogout }) {
return ( return (
<AppContainer> <AppContainer>
<Header> <Header>
<Title>工作待办</Title> <TitleSection>
<Title>工作待办</Title>
{currentUser && (
<UserInfo>
欢迎<span className="username">{currentUser.username}</span>
<span className={`role ${currentUser.is_admin ? 'admin' : 'user'}`}>
{currentUser.is_admin ? '管理员' : '用户'}
</span>
</UserInfo>
)}
</TitleSection>
<HeaderActions> <HeaderActions>
<MigratePendingButton onClick={handleMigratePendingTodos}> <ActionButton
variant="warning"
icon="📦"
onClick={handleMigratePendingTodos}
>
迁移未完成 迁移未完成
</MigratePendingButton> </ActionButton>
<SuspendedButton onClick={() => setShowSuspended(true)}> <ActionButton
variant="success"
icon="⏸️"
onClick={() => setShowSuspended(true)}
>
挂起待办 挂起待办
</SuspendedButton> </ActionButton>
<HistoryButton onClick={() => setShowHistory(true)}> <ActionButton
variant="primary"
icon="📚"
onClick={() => setShowHistory(true)}
>
历史记录 历史记录
</HistoryButton> </ActionButton>
<LogoutButton onClick={onLogout}>退出</LogoutButton> {currentUser?.is_admin && (
<ActionButton
variant="primary"
icon="👥"
onClick={() => setShowUserManagement(true)}
>
用户管理
</ActionButton>
)}
<ActionButton
variant="default"
onClick={onLogout}
>
退出
</ActionButton>
</HeaderActions> </HeaderActions>
</Header> </Header>

View File

@ -1,219 +1,344 @@
import React from 'react'; import React, { useState } from 'react';
import styled from 'styled-components'; import styled, { keyframes } from 'styled-components';
const ItemContainer = styled.div` const slideIn = keyframes`
display: flex; from {
align-items: center; opacity: 0;
padding: 20px; transform: translateX(-10px);
background: ${props => props.completed ? '#f8f9fa' : 'white'}; }
border: 2px solid ${props => props.completed ? '#e9ecef' : '#f0f0f0'}; to {
border-radius: 16px; opacity: 1;
transition: all 0.3s ease; transform: translateX(0);
cursor: ${props => props.isHistory ? 'default' : 'pointer'};
opacity: ${props => props.completed ? 0.7 : 1};
&:hover {
border-color: ${props => props.completed ? '#e9ecef' : (props.isHistory ? '#f0f0f0' : '#667eea')};
box-shadow: ${props => (props.completed || props.isHistory) ? 'none' : '0 6px 16px rgba(102, 126, 234, 0.15)'};
transform: ${props => (props.completed || props.isHistory) ? 'none' : 'translateY(-2px)'};
} }
`; `;
const PriorityIndicator = styled.div` const fadeIn = keyframes`
width: 16px; from {
height: 16px; opacity: 0;
border-radius: 50%; }
background-color: ${props => props.color}; to {
margin-right: 20px; opacity: 1;
flex-shrink: 0; }
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
`; `;
const TodoContent = styled.div` const scaleIn = keyframes`
from {
transform: scale(0.95);
}
to {
transform: scale(1);
}
`;
const ItemContainer = styled.div`
background: ${({ theme }) => theme.colors.glass.light};
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border-radius: ${({ theme }) => theme.borderRadius.lg};
padding: ${({ theme }) => theme.spacing.lg};
display: flex;
align-items: center;
gap: ${({ theme }) => theme.spacing.md};
transition: all ${({ theme }) => theme.transitions.default};
border: 1px solid ${({ theme }) => theme.colors.glass.light};
animation: ${slideIn} 0.3s ease-out;
position: relative;
overflow: hidden;
cursor: pointer;
&:hover {
transform: translateY(-2px);
box-shadow: ${({ theme }) => theme.shadows.md};
}
&:active {
transform: translateY(0);
box-shadow: ${({ theme }) => theme.shadows.sm};
}
@media (prefers-color-scheme: dark) {
background: ${({ theme }) => theme.colors.glass.dark};
border-color: ${({ theme }) => theme.colors.glass.dark};
}
@media (max-width: ${({ theme }) => theme.breakpoints.md}) {
padding: ${({ theme }) => theme.spacing.md};
}
`;
const Checkbox = styled.input`
appearance: none;
width: 24px;
height: 24px;
border: 2px solid ${({ theme }) => theme.colors.primary};
border-radius: ${({ theme }) => theme.borderRadius.md};
cursor: pointer;
position: relative;
transition: all ${({ theme }) => theme.transitions.default};
flex-shrink: 0;
&:checked {
background: ${({ theme }) => theme.colors.primary};
border-color: ${({ theme }) => theme.colors.primary};
}
&:checked::after {
content: '✓';
position: absolute;
color: white;
font-size: 16px;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
animation: ${fadeIn} 0.2s ease-out;
}
&:hover {
transform: scale(1.1);
}
&:focus {
outline: none;
box-shadow: 0 0 0 3px ${({ theme }) => theme.colors.primary}40;
}
`;
const Content = styled.div`
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 6px; gap: ${({ theme }) => theme.spacing.xs};
min-width: 0; // 防止内容溢出
`; `;
const TodoTitle = styled.span` const Title = styled.span`
font-size: 17px; color: ${({ theme, completed }) => completed ? theme.colors.text.secondary : theme.colors.text.primary};
color: ${props => props.completed ? '#6c757d' : '#333'}; font-size: ${({ theme }) => theme.typography.fontSize.base};
text-decoration: ${props => props.completed ? 'line-through' : 'none'}; font-weight: ${({ theme }) => theme.typography.fontWeight.medium};
font-weight: 500; text-decoration: ${({ completed }) => completed ? 'line-through' : 'none'};
transition: all ${({ theme }) => theme.transitions.default};
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
`;
const Description = styled.p`
color: ${({ theme }) => theme.colors.text.secondary};
font-size: ${({ theme }) => theme.typography.fontSize.sm};
margin: 0;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
line-height: 1.4; line-height: 1.4;
`; `;
const PriorityLabel = styled.span` const PriorityBadge = styled.span`
font-size: 13px; padding: ${({ theme }) => `${theme.spacing.xs} ${theme.spacing.sm}`};
color: ${props => props.color}; border-radius: ${({ theme }) => theme.borderRadius.full};
font-weight: 600; font-size: ${({ theme }) => theme.typography.fontSize.xs};
text-transform: uppercase; font-weight: ${({ theme }) => theme.typography.fontWeight.medium};
letter-spacing: 0.5px; background: ${({ theme, priority }) => theme.colors.priority[priority]}20;
`; color: ${({ theme, priority }) => theme.colors.priority[priority]};
transition: all ${({ theme }) => theme.transitions.default};
const ActionButtons = styled.div` display: inline-flex;
display: flex;
align-items: center; align-items: center;
gap: 8px; gap: ${({ theme }) => theme.spacing.xs};
`;
const CheckButton = styled.button` &::before {
width: 28px; content: ${({ priority }) => {
height: 28px; switch (priority) {
border-radius: 50%; case 'urgent': return '"🚨"';
border: 2px solid ${props => props.completed ? '#28a745' : '#dee2e6'}; case 'high': return '"🔥"';
background: ${props => props.completed ? '#28a745' : 'white'}; case 'medium': return '"⚡"';
cursor: ${props => props.isHistory ? 'default' : 'pointer'}; case 'low': return '"🐢"';
transition: all 0.3s ease; default: return '"📌"';
display: flex; }
align-items: center; }};
justify-content: center; font-size: 1.1em;
flex-shrink: 0; }
opacity: ${props => props.isHistory ? 0.6 : 1};
&:hover { &:hover {
border-color: ${props => props.isHistory ? (props.completed ? '#28a745' : '#dee2e6') : '#28a745'}; transform: translateY(-1px);
background: ${props => props.completed ? '#28a745' : (props.isHistory ? 'white' : 'rgba(40, 167, 69, 0.1)')}; background: ${({ theme, priority }) => theme.colors.priority[priority]}30;
transform: ${props => props.isHistory ? 'none' : 'scale(1.1)'};
}
&::after {
content: '✓';
color: white;
font-size: 16px;
font-weight: bold;
opacity: ${props => props.completed ? 1 : 0};
transition: opacity 0.3s ease;
} }
`; `;
const DeleteButton = styled.button` const ButtonGroup = styled.div`
width: 28px; display: flex;
height: 28px; gap: ${({ theme }) => theme.spacing.sm};
border-radius: 50%; opacity: 0;
border: 2px solid #e74c3c; transition: all ${({ theme }) => theme.transitions.default};
background: white; position: absolute;
right: ${({ theme }) => theme.spacing.lg};
top: 50%;
transform: translateY(-50%);
${ItemContainer}:hover & {
opacity: 1;
}
@media (max-width: ${({ theme }) => theme.breakpoints.md}) {
opacity: 1;
position: static;
transform: none;
}
`;
const ActionButton = styled.button`
background: none;
border: none;
padding: ${({ theme }) => theme.spacing.sm};
cursor: pointer; cursor: pointer;
transition: all 0.3s ease; color: ${({ theme }) => theme.colors.text.secondary};
border-radius: ${({ theme }) => theme.borderRadius.md};
transition: all ${({ theme }) => theme.transitions.default};
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
flex-shrink: 0; font-size: 1.2em;
&:hover { &:hover {
background: #e74c3c; background: ${({ theme }) => theme.colors.glass.light};
transform: scale(1.1); color: ${({ theme }) => theme.colors.primary};
transform: translateY(-1px);
} }
&::after { &:active {
content: '×'; transform: translateY(0);
color: #e74c3c;
font-size: 18px;
font-weight: bold;
transition: color 0.3s ease;
} }
&:hover::after { &:focus {
color: white; outline: none;
box-shadow: 0 0 0 3px ${({ theme }) => theme.colors.primary}40;
}
@media (prefers-color-scheme: dark) {
&:hover {
background: ${({ theme }) => theme.colors.glass.dark};
}
} }
`; `;
const SuspendButton = styled.button` const StatusBadge = styled.span`
width: 28px; position: absolute;
height: 28px; top: ${({ theme }) => theme.spacing.sm};
border-radius: 50%; right: ${({ theme }) => theme.spacing.sm};
border: 2px solid #ff9800; padding: ${({ theme }) => `${theme.spacing.xs} ${theme.spacing.sm}`};
background: white; border-radius: ${({ theme }) => theme.borderRadius.full};
cursor: pointer; font-size: ${({ theme }) => theme.typography.fontSize.xs};
transition: all 0.3s ease; font-weight: ${({ theme }) => theme.typography.fontWeight.medium};
background: ${({ theme, status }) => theme.colors.status[status]}20;
color: ${({ theme, status }) => theme.colors.status[status]};
animation: ${fadeIn} 0.3s ease-out;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; gap: ${({ theme }) => theme.spacing.xs};
flex-shrink: 0;
&:hover { &::before {
background: #ff9800; content: ${({ status }) => {
transform: scale(1.1); switch (status) {
} case 'warning': return '"⏸️"';
case 'success': return '"✅"';
&::after { case 'error': return '"❌"';
content: '⏸'; default: return '"📌"';
color: #ff9800; }
font-size: 12px; }};
font-weight: bold; font-size: 1.1em;
transition: color 0.3s ease;
}
&:hover::after {
color: white;
} }
`; `;
const priorityConfig = { const Tooltip = styled.div`
low: { color: '#95a5a6', label: '低' }, position: absolute;
medium: { color: '#3498db', label: '中' }, bottom: 100%;
high: { color: '#e67e22', label: '高' }, left: 50%;
urgent: { color: '#e74c3c', label: '急' } transform: translateX(-50%);
}; padding: ${({ theme }) => `${theme.spacing.xs} ${theme.spacing.sm}`};
background: ${({ theme }) => theme.colors.glass.dark};
color: ${({ theme }) => theme.colors.text.light};
border-radius: ${({ theme }) => theme.borderRadius.md};
font-size: ${({ theme }) => theme.typography.fontSize.xs};
white-space: nowrap;
opacity: 0;
visibility: hidden;
transition: all ${({ theme }) => theme.transitions.default};
pointer-events: none;
z-index: 1000;
function TodoItem({ todo, onToggle, onDelete, onSuspend, isHistory = false, isToday = false }) { ${ActionButton}:hover & {
const priority = priorityConfig[todo.priority] || priorityConfig.medium; opacity: 1;
visibility: visible;
transform: translateX(-50%) translateY(-4px);
}
`;
const handleClick = (e) => { function TodoItem({ todo, onToggle, onDelete, onSuspend, isHistory, isToday }) {
// 如果点击的是删除或挂起按钮不触发toggle const [isHovered, setIsHovered] = useState(false);
if (e.target.closest('button[data-action="delete"]') || e.target.closest('button[data-action="suspend"]')) {
return; const getPriorityLabel = (priority) => {
} const labels = {
if (!isHistory) { low: '低优先级',
onToggle(todo.id, !todo.completed); medium: '中优先级',
} high: '高优先级',
urgent: '紧急'
};
return labels[priority] || priority;
};
const handleToggle = (e) => {
e.stopPropagation();
onToggle(todo.id, !todo.completed);
}; };
const handleDelete = (e) => { const handleDelete = (e) => {
e.stopPropagation(); e.stopPropagation();
if (onDelete) { onDelete(todo.id);
onDelete(todo.id);
}
}; };
const handleSuspend = (e) => { const handleSuspend = (e) => {
e.stopPropagation(); e.stopPropagation();
if (onSuspend) { onSuspend(todo.id);
onSuspend(todo.id);
}
}; };
return ( return (
<ItemContainer completed={todo.completed} onClick={handleClick} isHistory={isHistory}> <ItemContainer
<PriorityIndicator color={priority.color} /> onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
<TodoContent> onClick={handleToggle}
<TodoTitle completed={todo.completed}> >
<Checkbox
type="checkbox"
checked={todo.completed}
onChange={handleToggle}
/>
<Content>
<Title completed={todo.completed}>
{todo.title} {todo.title}
</TodoTitle> </Title>
<PriorityLabel color={priority.color}> {todo.description && (
{priority.label}优先级 <Description>{todo.description}</Description>
</PriorityLabel>
</TodoContent>
<ActionButtons>
<CheckButton completed={todo.completed} isHistory={isHistory} />
{isToday && !isHistory && (
<>
<SuspendButton
onClick={handleSuspend}
data-action="suspend"
title="挂起待办事项"
/>
<DeleteButton
onClick={handleDelete}
data-action="delete"
title="删除待办事项"
/>
</>
)} )}
</ActionButtons> <PriorityBadge priority={todo.priority}>
{getPriorityLabel(todo.priority)}
</PriorityBadge>
</Content>
<ButtonGroup>
{isToday && !todo.completed && !todo.suspended && (
<ActionButton onClick={handleSuspend} title="挂起">
<Tooltip>挂起任务</Tooltip>
</ActionButton>
)}
{!isHistory && (
<ActionButton onClick={handleDelete} title="删除">
🗑
<Tooltip>删除任务</Tooltip>
</ActionButton>
)}
</ButtonGroup>
{todo.suspended && (
<StatusBadge status="warning">已挂起</StatusBadge>
)}
</ItemContainer> </ItemContainer>
); );
} }

View File

@ -1,44 +1,130 @@
import React from 'react'; import React from 'react';
import styled from 'styled-components'; import styled, { keyframes } from 'styled-components';
import TodoItem from './TodoItem'; import TodoItem from './TodoItem';
const fadeIn = keyframes`
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
`;
const ListContainer = styled.div` const ListContainer = styled.div`
background: white; background: ${({ theme }) => theme.colors.glass.light};
border-radius: 20px; backdrop-filter: blur(10px);
padding: 30px; -webkit-backdrop-filter: blur(10px);
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.08); border-radius: ${({ theme }) => theme.borderRadius.xl};
border: 1px solid #f0f0f0; padding: ${({ theme }) => theme.spacing.xl};
box-shadow: ${({ theme }) => theme.shadows.lg};
border: 1px solid ${({ theme }) => theme.colors.glass.light};
animation: ${fadeIn} 0.5s ease-out;
@media (prefers-color-scheme: dark) {
background: ${({ theme }) => theme.colors.glass.dark};
border-color: ${({ theme }) => theme.colors.glass.dark};
}
@media (max-width: ${({ theme }) => theme.breakpoints.md}) {
padding: ${({ theme }) => theme.spacing.lg};
border-radius: ${({ theme }) => theme.borderRadius.lg};
}
`; `;
const DateHeader = styled.div` const DateHeader = styled.div`
display: flex; display: flex;
align-items: center; align-items: center;
margin-bottom: 25px; justify-content: space-between;
padding-bottom: 15px; margin-bottom: ${({ theme }) => theme.spacing.xl};
border-bottom: 2px solid #f8f9fa; padding-bottom: ${({ theme }) => theme.spacing.md};
border-bottom: 2px solid ${({ theme }) => theme.colors.glass.light};
@media (prefers-color-scheme: dark) {
border-bottom-color: ${({ theme }) => theme.colors.glass.dark};
}
@media (max-width: ${({ theme }) => theme.breakpoints.md}) {
margin-bottom: ${({ theme }) => theme.spacing.lg};
padding-bottom: ${({ theme }) => theme.spacing.sm};
}
`; `;
const DateLabel = styled.h3` const DateLabel = styled.h3`
color: #333; color: ${({ theme }) => theme.colors.text.primary};
font-size: 20px; font-size: ${({ theme }) => theme.typography.fontSize.xl};
font-weight: 600; font-weight: ${({ theme }) => theme.typography.fontWeight.semibold};
margin: 0; margin: 0;
margin-right: 16px; display: flex;
align-items: center;
gap: ${({ theme }) => theme.spacing.sm};
&::before {
content: '📅';
font-size: 1.2em;
}
@media (max-width: ${({ theme }) => theme.breakpoints.md}) {
font-size: ${({ theme }) => theme.typography.fontSize.lg};
}
`; `;
const TodoCount = styled.span` const TodoCount = styled.div`
background: rgba(102, 126, 234, 0.1); display: flex;
color: #667eea; align-items: center;
padding: 6px 16px; gap: ${({ theme }) => theme.spacing.xs};
border-radius: 20px; padding: ${({ theme }) => `${theme.spacing.xs} ${theme.spacing.md}`};
font-size: 14px; background: ${({ theme }) => theme.colors.primary}20;
font-weight: 600; color: ${({ theme }) => theme.colors.primary};
border-radius: ${({ theme }) => theme.borderRadius.full};
font-size: ${({ theme }) => theme.typography.fontSize.sm};
font-weight: ${({ theme }) => theme.typography.fontWeight.medium};
transition: all ${({ theme }) => theme.transitions.default};
&:hover {
background: ${({ theme }) => theme.colors.primary}30;
transform: translateY(-1px);
}
&::before {
content: '📊';
font-size: 1.1em;
}
`; `;
const TodoItems = styled.div` const TodoItems = styled.div`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 16px; gap: ${({ theme }) => theme.spacing.md};
@media (max-width: ${({ theme }) => theme.breakpoints.md}) {
gap: ${({ theme }) => theme.spacing.sm};
}
`;
const EmptyMessage = styled.div`
text-align: center;
padding: ${({ theme }) => theme.spacing.xl};
color: ${({ theme }) => theme.colors.text.secondary};
font-size: ${({ theme }) => theme.typography.fontSize.lg};
background: ${({ theme }) => theme.colors.glass.light};
border-radius: ${({ theme }) => theme.borderRadius.lg};
border: 2px dashed ${({ theme }) => theme.colors.glass.light};
animation: ${fadeIn} 0.5s ease-out;
@media (prefers-color-scheme: dark) {
background: ${({ theme }) => theme.colors.glass.dark};
border-color: ${({ theme }) => theme.colors.glass.dark};
}
&::before {
content: '📝';
font-size: 2em;
display: block;
margin-bottom: ${({ theme }) => theme.spacing.sm};
}
`; `;
function TodoList({ dateLabel, todos, onToggleTodo, onDeleteTodo, onSuspendTodo, isHistory = false, isToday = false }) { function TodoList({ dateLabel, todos, onToggleTodo, onDeleteTodo, onSuspendTodo, isHistory = false, isToday = false }) {
@ -55,17 +141,23 @@ function TodoList({ dateLabel, todos, onToggleTodo, onDeleteTodo, onSuspendTodo,
</DateHeader> </DateHeader>
<TodoItems> <TodoItems>
{todos.map(todo => ( {todos.length === 0 ? (
<TodoItem <EmptyMessage>
key={todo.id} 暂无待办事项
todo={todo} </EmptyMessage>
onToggle={onToggleTodo} ) : (
onDelete={onDeleteTodo} todos.map(todo => (
onSuspend={onSuspendTodo} <TodoItem
isHistory={isHistory} key={todo.id}
isToday={isToday} todo={todo}
/> onToggle={onToggleTodo}
))} onDelete={onDeleteTodo}
onSuspend={onSuspendTodo}
isHistory={isHistory}
isToday={isToday}
/>
))
)}
</TodoItems> </TodoItems>
</ListContainer> </ListContainer>
); );

View File

@ -0,0 +1,369 @@
import React, { useState, useEffect } from 'react';
import styled, { keyframes } from 'styled-components';
import { getUsers, registerUser } from '../services/api';
const fadeIn = keyframes`
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
`;
const UserManagementContainer = styled.div`
background: ${({ theme }) => theme.colors.glass.light};
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border-radius: ${({ theme }) => theme.borderRadius.xl};
padding: ${({ theme }) => theme.spacing.xl};
box-shadow: ${({ theme }) => theme.shadows.lg};
border: 1px solid ${({ theme }) => theme.colors.glass.light};
animation: ${fadeIn} 0.5s ease-out;
max-width: 800px;
margin: 0 auto;
@media (prefers-color-scheme: dark) {
background: ${({ theme }) => theme.colors.glass.dark};
border-color: ${({ theme }) => theme.colors.glass.dark};
}
`;
const Header = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: ${({ theme }) => theme.spacing.xl};
padding-bottom: ${({ theme }) => theme.spacing.lg};
border-bottom: 2px solid ${({ theme }) => theme.colors.glass.light};
@media (prefers-color-scheme: dark) {
border-bottom-color: ${({ theme }) => theme.colors.glass.dark};
}
`;
const Title = styled.h2`
color: ${({ theme }) => theme.colors.text.primary};
font-size: ${({ theme }) => theme.typography.fontSize['2xl']};
font-weight: ${({ theme }) => theme.typography.fontWeight.semibold};
margin: 0;
display: flex;
align-items: center;
gap: ${({ theme }) => theme.spacing.sm};
&::before {
content: '👥';
font-size: 1.2em;
}
`;
const BackButton = styled.button`
background: ${({ theme }) => theme.colors.glass.light};
color: ${({ theme }) => theme.colors.text.primary};
border: 2px solid ${({ theme }) => theme.colors.glass.light};
padding: ${({ theme }) => `${theme.spacing.sm} ${theme.spacing.lg}`};
border-radius: ${({ theme }) => theme.borderRadius.lg};
font-size: ${({ theme }) => theme.typography.fontSize.sm};
cursor: pointer;
transition: all ${({ theme }) => theme.transitions.default};
&:hover {
background: ${({ theme }) => theme.colors.primary};
color: white;
border-color: ${({ theme }) => theme.colors.primary};
transform: translateY(-1px);
}
@media (prefers-color-scheme: dark) {
background: ${({ theme }) => theme.colors.glass.dark};
border-color: ${({ theme }) => theme.colors.glass.dark};
}
`;
const AddUserForm = styled.form`
display: flex;
gap: ${({ theme }) => theme.spacing.md};
margin-bottom: ${({ theme }) => theme.spacing.xl};
padding: ${({ theme }) => theme.spacing.lg};
background: ${({ theme }) => theme.colors.glass.light};
border-radius: ${({ theme }) => theme.borderRadius.lg};
border: 2px dashed ${({ theme }) => theme.colors.primary}40;
@media (max-width: ${({ theme }) => theme.breakpoints.md}) {
flex-direction: column;
}
@media (prefers-color-scheme: dark) {
background: ${({ theme }) => theme.colors.glass.dark};
}
`;
const InputGroup = styled.div`
flex: 1;
display: flex;
flex-direction: column;
gap: ${({ theme }) => theme.spacing.xs};
`;
const Label = styled.label`
color: ${({ theme }) => theme.colors.text.primary};
font-size: ${({ theme }) => theme.typography.fontSize.sm};
font-weight: ${({ theme }) => theme.typography.fontWeight.medium};
`;
const Input = styled.input`
padding: ${({ theme }) => theme.spacing.md};
font-size: ${({ theme }) => theme.typography.fontSize.base};
border: 2px solid ${({ theme }) => theme.colors.glass.light};
border-radius: ${({ theme }) => theme.borderRadius.md};
background: ${({ theme }) => theme.colors.glass.light};
color: ${({ theme }) => theme.colors.text.primary};
transition: all ${({ theme }) => theme.transitions.default};
&:focus {
outline: none;
border-color: ${({ theme }) => theme.colors.primary};
box-shadow: 0 0 0 3px ${({ theme }) => theme.colors.primary}40;
}
&::placeholder {
color: ${({ theme }) => theme.colors.text.secondary};
}
@media (prefers-color-scheme: dark) {
background: ${({ theme }) => theme.colors.glass.dark};
border-color: ${({ theme }) => theme.colors.glass.dark};
}
`;
const AddButton = styled.button`
background: ${({ theme }) => theme.colors.primary};
color: white;
border: none;
padding: ${({ theme }) => `${theme.spacing.md} ${theme.spacing.lg}`};
border-radius: ${({ theme }) => theme.borderRadius.md};
font-size: ${({ theme }) => theme.typography.fontSize.base};
font-weight: ${({ theme }) => theme.typography.fontWeight.medium};
cursor: pointer;
transition: all ${({ theme }) => theme.transitions.default};
align-self: end;
&:hover {
background: ${({ theme }) => theme.colors.primary}dd;
transform: translateY(-1px);
}
&:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
`;
const UserList = styled.div`
display: flex;
flex-direction: column;
gap: ${({ theme }) => theme.spacing.md};
`;
const UserItem = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
padding: ${({ theme }) => theme.spacing.lg};
background: ${({ theme }) => theme.colors.glass.light};
border-radius: ${({ theme }) => theme.borderRadius.lg};
border: 1px solid ${({ theme }) => theme.colors.glass.light};
transition: all ${({ theme }) => theme.transitions.default};
&:hover {
transform: translateY(-1px);
box-shadow: ${({ theme }) => theme.shadows.md};
}
@media (prefers-color-scheme: dark) {
background: ${({ theme }) => theme.colors.glass.dark};
border-color: ${({ theme }) => theme.colors.glass.dark};
}
`;
const UserInfo = styled.div`
display: flex;
flex-direction: column;
gap: ${({ theme }) => theme.spacing.xs};
`;
const Username = styled.span`
color: ${({ theme }) => theme.colors.text.primary};
font-size: ${({ theme }) => theme.typography.fontSize.lg};
font-weight: ${({ theme }) => theme.typography.fontWeight.medium};
`;
const UserRole = styled.span`
color: ${({ theme, isAdmin }) => isAdmin ? theme.colors.status.warning : theme.colors.text.secondary};
font-size: ${({ theme }) => theme.typography.fontSize.sm};
font-weight: ${({ theme }) => theme.typography.fontWeight.medium};
`;
const UserDate = styled.span`
color: ${({ theme }) => theme.colors.text.secondary};
font-size: ${({ theme }) => theme.typography.fontSize.sm};
`;
const Message = styled.div`
padding: ${({ theme }) => theme.spacing.md};
border-radius: ${({ theme }) => theme.borderRadius.md};
margin-bottom: ${({ theme }) => theme.spacing.lg};
text-align: center;
font-size: ${({ theme }) => theme.typography.fontSize.sm};
animation: ${fadeIn} 0.3s ease-out;
${({ type, theme }) => {
if (type === 'success') {
return `
background: ${theme.colors.status.success}20;
color: ${theme.colors.status.success};
border: 1px solid ${theme.colors.status.success}40;
`;
}
if (type === 'error') {
return `
background: ${theme.colors.status.error}20;
color: ${theme.colors.status.error};
border: 1px solid ${theme.colors.status.error}40;
`;
}
return '';
}}
`;
function UserManagement({ onBack }) {
const [users, setUsers] = useState([]);
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [loading, setLoading] = useState(true);
const [submitting, setSubmitting] = useState(false);
const [message, setMessage] = useState({ text: '', type: '' });
useEffect(() => {
loadUsers();
}, []);
const loadUsers = async () => {
try {
setLoading(true);
const userData = await getUsers();
setUsers(userData);
} catch (error) {
setMessage({ text: '加载用户列表失败', type: 'error' });
} finally {
setLoading(false);
}
};
const handleSubmit = async (e) => {
e.preventDefault();
if (!username.trim() || !password.trim()) {
setMessage({ text: '用户名和密码不能为空', type: 'error' });
return;
}
setSubmitting(true);
try {
await registerUser(username.trim(), password);
setMessage({ text: '用户添加成功', type: 'success' });
setUsername('');
setPassword('');
await loadUsers();
} catch (error) {
setMessage({
text: error.response?.data?.error || '添加用户失败',
type: 'error'
});
} finally {
setSubmitting(false);
}
// 清除消息
setTimeout(() => setMessage({ text: '', type: '' }), 5000);
};
const formatDate = (dateString) => {
return new Date(dateString).toLocaleDateString('zh-CN', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
};
return (
<UserManagementContainer>
<Header>
<Title>用户管理</Title>
<BackButton onClick={onBack}>返回</BackButton>
</Header>
{message.text && (
<Message type={message.type}>{message.text}</Message>
)}
<AddUserForm onSubmit={handleSubmit}>
<InputGroup>
<Label>用户名</Label>
<Input
type="text"
placeholder="请输入用户名"
value={username}
onChange={(e) => setUsername(e.target.value)}
disabled={submitting}
required
/>
</InputGroup>
<InputGroup>
<Label>密码</Label>
<Input
type="password"
placeholder="请输入密码"
value={password}
onChange={(e) => setPassword(e.target.value)}
disabled={submitting}
required
/>
</InputGroup>
<AddButton type="submit" disabled={submitting}>
{submitting ? '添加中...' : '添加用户'}
</AddButton>
</AddUserForm>
<UserList>
{loading ? (
<div style={{ textAlign: 'center', padding: '2rem' }}>
加载中...
</div>
) : users.length === 0 ? (
<div style={{ textAlign: 'center', padding: '2rem', color: '#999' }}>
暂无用户
</div>
) : (
users.map(user => (
<UserItem key={user.id}>
<UserInfo>
<Username>{user.username}</Username>
<UserRole isAdmin={user.is_admin}>
{user.is_admin ? '管理员' : '普通用户'}
</UserRole>
<UserDate>创建于 {formatDate(user.created_at)}</UserDate>
</UserInfo>
</UserItem>
))
)}
</UserList>
</UserManagementContainer>
);
}
export default UserManagement;

View File

@ -1,13 +1,13 @@
import axios from 'axios'; import axios from 'axios';
const API_BASE_URL = '/api'; const API_URL = 'http://localhost:5000/api';
// 创建axios实例 // 创建axios实例
const api = axios.create({ const api = axios.create({
baseURL: API_BASE_URL, baseURL: API_URL,
}); });
// 添加请求拦截器自动添加token // 请求拦截器自动添加token
api.interceptors.request.use((config) => { api.interceptors.request.use((config) => {
const token = localStorage.getItem('token'); const token = localStorage.getItem('token');
if (token) { if (token) {
@ -16,17 +16,21 @@ api.interceptors.request.use((config) => {
return config; return config;
}); });
// 登录验证 // 用户登录
export const login = async (password) => { export const login = async (username, password) => {
const response = await api.post('/auth', { password }); const response = await api.post('/auth/login', { username, password });
return response.data; return response.data;
}; };
// 检查认证状态 // 管理员注册新用户
export const checkAuth = async (token) => { export const registerUser = async (username, password) => {
const response = await api.get('/todos', { const response = await api.post('/auth/register', { username, password });
headers: { Authorization: `Bearer ${token}` } return response.data;
}); };
// 获取所有用户(仅管理员)
export const getUsers = async () => {
const response = await api.get('/users');
return response.data; return response.data;
}; };
@ -42,6 +46,12 @@ export const getHistoryTodos = async () => {
return response.data; return response.data;
}; };
// 获取挂起的待办事项
export const getSuspendedTodos = async () => {
const response = await api.get('/todos/suspended');
return response.data;
};
// 迁移历史未完成待办到今天 // 迁移历史未完成待办到今天
export const migratePendingTodos = async () => { export const migratePendingTodos = async () => {
const response = await api.post('/todos/migrate-pending'); const response = await api.post('/todos/migrate-pending');
@ -54,12 +64,6 @@ export const suspendTodo = async (id) => {
return response.data; return response.data;
}; };
// 获取挂起的待办事项
export const getSuspendedTodos = async () => {
const response = await api.get('/todos/suspended');
return response.data;
};
// 恢复挂起的待办事项 // 恢复挂起的待办事项
export const resumeTodo = async (id) => { export const resumeTodo = async (id) => {
const response = await api.put(`/todos/${id}/resume`); const response = await api.put(`/todos/${id}/resume`);

149
client/src/styles/theme.js Normal file
View File

@ -0,0 +1,149 @@
import { createGlobalStyle } from 'styled-components';
export const theme = {
colors: {
primary: '#6C5CE7', // 主色:柔和紫
secondary: '#00B894', // 辅助色:生态绿
background: {
light: 'rgba(255, 255, 255, 0.95)',
dark: 'rgba(30, 30, 30, 0.95)',
},
text: {
primary: '#2D3436',
secondary: '#636E72',
light: '#FFFFFF',
},
priority: {
low: '#95A5A6',
medium: '#3498DB',
high: '#E67E22',
urgent: '#E74C3C',
},
status: {
success: '#00B894',
warning: '#FDCB6E',
error: '#E74C3C',
},
glass: {
light: 'rgba(255, 255, 255, 0.7)',
dark: 'rgba(30, 30, 30, 0.7)',
}
},
typography: {
fontFamily: {
pc: 'Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
mobile: 'SF Pro Rounded, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
},
fontSize: {
xs: '0.75rem',
sm: '0.875rem',
base: '1rem',
lg: '1.125rem',
xl: '1.25rem',
'2xl': '1.5rem',
'3xl': '1.875rem',
'4xl': '2.25rem',
},
fontWeight: {
normal: 400,
medium: 500,
semibold: 600,
bold: 700,
},
},
spacing: {
xs: '0.25rem',
sm: '0.5rem',
md: '1rem',
lg: '1.5rem',
xl: '2rem',
'2xl': '3rem',
},
borderRadius: {
sm: '0.375rem',
md: '0.5rem',
lg: '0.75rem',
xl: '1rem',
full: '9999px',
},
shadows: {
sm: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
md: '0 4px 6px -1px rgba(0, 0, 0, 0.1)',
lg: '0 10px 15px -3px rgba(0, 0, 0, 0.1)',
xl: '0 20px 25px -5px rgba(0, 0, 0, 0.1)',
},
transitions: {
default: '0.3s ease',
fast: '0.15s ease',
slow: '0.5s ease',
},
breakpoints: {
sm: '640px',
md: '768px',
lg: '1024px',
xl: '1280px',
'2xl': '1536px',
},
};
export const GlobalStyle = createGlobalStyle`
:root {
--primary: ${theme.colors.primary};
--secondary: ${theme.colors.secondary};
--background: ${theme.colors.background.light};
--text-primary: ${theme.colors.text.primary};
--text-secondary: ${theme.colors.text.secondary};
}
@media (prefers-color-scheme: dark) {
:root {
--background: ${theme.colors.background.dark};
--text-primary: ${theme.colors.text.light};
--text-secondary: ${theme.colors.text.light};
}
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: ${theme.typography.fontFamily.pc};
background: var(--background);
color: var(--text-primary);
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
@media (max-width: ${theme.breakpoints.md}) {
body {
font-family: ${theme.typography.fontFamily.mobile};
}
}
button {
font-family: inherit;
}
/* 滚动条样式 */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: ${theme.colors.primary}40;
border-radius: ${theme.borderRadius.full};
}
::-webkit-scrollbar-thumb:hover {
background: ${theme.colors.primary}60;
}
`;

View File

@ -30,21 +30,47 @@ async function initDatabase() {
try { try {
const connection = await pool.getConnection(); const connection = await pool.getConnection();
// 创建用户表
await connection.execute(`
CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(64) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL,
is_admin BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
// 创建待办事项表 // 创建待办事项表
await connection.execute(` await connection.execute(`
CREATE TABLE IF NOT EXISTS todos ( CREATE TABLE IF NOT EXISTS todos (
id INT AUTO_INCREMENT PRIMARY KEY, id INT AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(255) NOT NULL, title VARCHAR(255) NOT NULL,
description TEXT,
priority ENUM('low', 'medium', 'high', 'urgent') DEFAULT 'medium', priority ENUM('low', 'medium', 'high', 'urgent') DEFAULT 'medium',
completed BOOLEAN DEFAULT FALSE, completed BOOLEAN DEFAULT FALSE,
suspended BOOLEAN DEFAULT FALSE, suspended BOOLEAN DEFAULT FALSE,
date DATE NOT NULL, date DATE NOT NULL,
suspended_date DATE NULL, suspended_date DATE NULL,
user_id INT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
) )
`); `);
// 添加user_id字段如果表已存在但没有该字段
try {
await connection.execute(`
ALTER TABLE todos
ADD COLUMN user_id INT NOT NULL,
ADD CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
`);
} catch (error) {
// 字段可能已存在,忽略错误
console.log('User_id column may already exist');
}
// 添加suspended字段如果表已存在但没有该字段 // 添加suspended字段如果表已存在但没有该字段
try { try {
await connection.execute(` await connection.execute(`
@ -56,6 +82,30 @@ async function initDatabase() {
// 字段可能已存在,忽略错误 // 字段可能已存在,忽略错误
console.log('Suspended columns may already exist'); console.log('Suspended columns may already exist');
} }
// 添加description字段如果表已存在但没有该字段
try {
await connection.execute(`
ALTER TABLE todos
ADD COLUMN description TEXT
`);
} catch (error) {
// 字段可能已存在,忽略错误
console.log('Description column may already exist');
}
// 创建默认管理员用户
const adminPassword = await bcrypt.hash('weiMonkey2024', 10);
try {
await connection.execute(`
INSERT INTO users (username, password, is_admin)
VALUES ('admin', ?, TRUE)
`, [adminPassword]);
console.log('默认管理员用户创建成功');
} catch (error) {
// 用户可能已存在
console.log('默认管理员用户可能已存在');
}
connection.release(); connection.release();
console.log('数据库表初始化成功'); console.log('数据库表初始化成功');
@ -76,14 +126,6 @@ const authenticatePassword = (req, res, next) => {
next(); next();
}; };
// 路由
// 验证密码
app.post('/api/auth', authenticatePassword, (req, res) => {
const token = jwt.sign({ authenticated: true }, JWT_SECRET, { expiresIn: '24h' });
res.json({ success: true, token });
});
// 验证token的中间件 // 验证token的中间件
const authenticateToken = (req, res, next) => { const authenticateToken = (req, res, next) => {
const authHeader = req.headers['authorization']; const authHeader = req.headers['authorization'];
@ -102,6 +144,136 @@ const authenticateToken = (req, res, next) => {
}); });
}; };
// 验证管理员权限的中间件
const authenticateAdmin = (req, res, next) => {
if (!req.user.is_admin) {
return res.status(403).json({ error: '需要管理员权限' });
}
next();
};
// 路由
// 用户登录
app.post('/api/auth/login', async (req, res) => {
try {
const { username, password } = req.body;
if (!username || !password) {
return res.status(400).json({ error: '用户名和密码是必需的' });
}
const connection = await pool.getConnection();
// 查找用户
const [users] = await connection.execute(
'SELECT * FROM users WHERE username = ?',
[username]
);
connection.release();
if (users.length === 0) {
return res.status(401).json({ error: '用户名或密码错误' });
}
const user = users[0];
// 验证密码
const isValidPassword = await bcrypt.compare(password, user.password);
if (!isValidPassword) {
return res.status(401).json({ error: '用户名或密码错误' });
}
// 生成token
const token = jwt.sign(
{
userId: user.id,
username: user.username,
is_admin: user.is_admin
},
JWT_SECRET,
{ expiresIn: '24h' }
);
res.json({
success: true,
token,
user: {
id: user.id,
username: user.username,
is_admin: user.is_admin
}
});
} catch (error) {
console.error('登录失败:', error);
res.status(500).json({ error: '服务器错误' });
}
});
// 管理员添加用户
app.post('/api/auth/register', authenticateToken, authenticateAdmin, async (req, res) => {
try {
const { username, password } = req.body;
if (!username || !password) {
return res.status(400).json({ error: '用户名和密码是必需的' });
}
if (password.length < 6) {
return res.status(400).json({ error: '密码长度至少6位' });
}
const connection = await pool.getConnection();
// 检查用户名是否已存在
const [existingUsers] = await connection.execute(
'SELECT id FROM users WHERE username = ?',
[username]
);
if (existingUsers.length > 0) {
connection.release();
return res.status(400).json({ error: '用户名已存在' });
}
// 创建新用户
const hashedPassword = await bcrypt.hash(password, 10);
const [result] = await connection.execute(
'INSERT INTO users (username, password, is_admin) VALUES (?, ?, FALSE)',
[username, hashedPassword]
);
connection.release();
res.json({
success: true,
message: '用户创建成功',
userId: result.insertId
});
} catch (error) {
console.error('注册用户失败:', error);
res.status(500).json({ error: '服务器错误' });
}
});
// 获取所有用户(仅管理员)
app.get('/api/users', authenticateToken, authenticateAdmin, async (req, res) => {
try {
const connection = await pool.getConnection();
const [users] = await connection.execute(
'SELECT id, username, is_admin, created_at FROM users ORDER BY created_at DESC'
);
connection.release();
res.json(users);
} catch (error) {
console.error('获取用户列表失败:', error);
res.status(500).json({ error: '服务器错误' });
}
});
// 获取历史待办事项(一周之前的) // 获取历史待办事项(一周之前的)
app.get('/api/todos/history', authenticateToken, async (req, res) => { app.get('/api/todos/history', authenticateToken, async (req, res) => {
try { try {
@ -111,10 +283,13 @@ app.get('/api/todos/history', authenticateToken, async (req, res) => {
const today = new Date(); const today = new Date();
const oneWeekAgo = new Date(today.getTime() - 7 * 24 * 60 * 60 * 1000); const oneWeekAgo = new Date(today.getTime() - 7 * 24 * 60 * 60 * 1000);
const [rows] = await connection.execute( // 管理员可以查看所有用户的数据,普通用户只能查看自己的
'SELECT * FROM todos WHERE date < ? ORDER BY date DESC, priority DESC, created_at DESC', let query = 'SELECT * FROM todos WHERE date < ? AND user_id = ? ORDER BY date DESC, priority DESC, created_at DESC';
[oneWeekAgo.toISOString().split('T')[0]] let params = [oneWeekAgo.toISOString().split('T')[0]];
); params.push(req.user.userId);
const [rows] = await connection.execute(query, params);
connection.release(); connection.release();
res.json(rows); res.json(rows);
@ -133,10 +308,12 @@ app.get('/api/todos', authenticateToken, async (req, res) => {
const today = new Date(); const today = new Date();
const oneWeekAgo = new Date(today.getTime() - 7 * 24 * 60 * 60 * 1000); const oneWeekAgo = new Date(today.getTime() - 7 * 24 * 60 * 60 * 1000);
const [rows] = await connection.execute( // 管理员可以查看所有用户的数据,普通用户只能查看自己的
'SELECT * FROM todos WHERE date >= ? AND date <= ? AND suspended = FALSE ORDER BY date DESC, priority DESC, created_at DESC', let query = 'SELECT * FROM todos WHERE date >= ? AND date <= ? AND suspended = FALSE AND user_id = ? ORDER BY date DESC, priority DESC, created_at DESC';
[oneWeekAgo.toISOString().split('T')[0], today.toISOString().split('T')[0]]
); let params = [oneWeekAgo.toISOString().split('T')[0], today.toISOString().split('T')[0]];
params.push(req.user.userId);
const [rows] = await connection.execute(query, params);
connection.release(); connection.release();
res.json(rows); res.json(rows);
@ -149,7 +326,7 @@ app.get('/api/todos', authenticateToken, async (req, res) => {
// 创建新的待办事项 // 创建新的待办事项
app.post('/api/todos', authenticateToken, async (req, res) => { app.post('/api/todos', authenticateToken, async (req, res) => {
try { try {
const { title, priority, date } = req.body; const { title, priority, date, description } = req.body;
if (!title || !date) { if (!title || !date) {
return res.status(400).json({ error: '标题和日期是必需的' }); return res.status(400).json({ error: '标题和日期是必需的' });
@ -158,8 +335,8 @@ app.post('/api/todos', authenticateToken, async (req, res) => {
const connection = await pool.getConnection(); const connection = await pool.getConnection();
const [result] = await connection.execute( const [result] = await connection.execute(
'INSERT INTO todos (title, priority, date) VALUES (?, ?, ?)', 'INSERT INTO todos (title, description, priority, date, user_id) VALUES (?, ?, ?, ?, ?)',
[title, priority || 'medium', date] [title, description || null, priority || 'medium', date, req.user.userId]
); );
const [newTodo] = await connection.execute( const [newTodo] = await connection.execute(
@ -179,15 +356,70 @@ app.post('/api/todos', authenticateToken, async (req, res) => {
app.put('/api/todos/:id', authenticateToken, async (req, res) => { app.put('/api/todos/:id', authenticateToken, async (req, res) => {
try { try {
const { id } = req.params; const { id } = req.params;
const { completed } = req.body; const { completed, title, priority, date, description } = req.body;
const connection = await pool.getConnection(); const connection = await pool.getConnection();
// 检查待办事项是否存在且属于当前用户(管理员可以修改所有)
let checkQuery = 'SELECT * FROM todos WHERE id = ?';
let checkParams = [id];
if (!req.user.is_admin) {
checkQuery = 'SELECT * FROM todos WHERE id = ? AND user_id = ?';
checkParams.push(req.user.userId);
}
const [existingTodos] = await connection.execute(checkQuery, checkParams);
if (existingTodos.length === 0) {
connection.release();
return res.status(404).json({ error: '待办事项不存在或无权限访问' });
}
// 构建更新字段和值
const updates = [];
const values = [];
if (completed !== undefined) {
updates.push('completed = ?');
values.push(completed);
}
if (title !== undefined) {
updates.push('title = ?');
values.push(title);
}
if (priority !== undefined) {
updates.push('priority = ?');
values.push(priority);
}
if (date !== undefined) {
updates.push('date = ?');
values.push(date);
}
if (description !== undefined) {
updates.push('description = ?');
values.push(description || null);
}
if (updates.length === 0) {
connection.release();
return res.status(400).json({ error: '没有提供要更新的字段' });
}
// 添加 id 到值数组
values.push(id);
// 执行更新
await connection.execute( await connection.execute(
'UPDATE todos SET completed = ? WHERE id = ?', `UPDATE todos SET ${updates.join(', ')} WHERE id = ?`,
[completed, id] values
); );
// 获取更新后的待办事项
const [updatedTodo] = await connection.execute( const [updatedTodo] = await connection.execute(
'SELECT * FROM todos WHERE id = ?', 'SELECT * FROM todos WHERE id = ?',
[id] [id]
@ -208,6 +440,22 @@ app.delete('/api/todos/:id', authenticateToken, async (req, res) => {
const connection = await pool.getConnection(); const connection = await pool.getConnection();
// 检查待办事项是否存在且属于当前用户(管理员可以删除所有)
let checkQuery = 'SELECT * FROM todos WHERE id = ?';
let checkParams = [id];
if (!req.user.is_admin) {
checkQuery = 'SELECT * FROM todos WHERE id = ? AND user_id = ?';
checkParams.push(req.user.userId);
}
const [existingTodos] = await connection.execute(checkQuery, checkParams);
if (existingTodos.length === 0) {
connection.release();
return res.status(404).json({ error: '待办事项不存在或无权限访问' });
}
await connection.execute('DELETE FROM todos WHERE id = ?', [id]); await connection.execute('DELETE FROM todos WHERE id = ?', [id]);
connection.release(); connection.release();
@ -226,15 +474,17 @@ app.put('/api/todos/:id/suspend', authenticateToken, async (req, res) => {
const connection = await pool.getConnection(); const connection = await pool.getConnection();
// 检查是否是今天的待办事项 // 检查是否是今天的待办事项且属于当前用户
const [todoCheck] = await connection.execute( let checkQuery = 'SELECT * FROM todos WHERE id = ? AND date = ? AND user_id = ?';
'SELECT * FROM todos WHERE id = ? AND date = ?', let checkParams = [id, today];
[id, today] checkParams.push(req.user.userId);
);
const [todoCheck] = await connection.execute(checkQuery, checkParams);
if (todoCheck.length === 0) { if (todoCheck.length === 0) {
connection.release(); connection.release();
return res.status(400).json({ error: '只能挂起今天的待办事项' }); return res.status(400).json({ error: '只能挂起今天的待办事项或无权限访问' });
} }
// 更新挂起状态 // 更新挂起状态
@ -261,9 +511,13 @@ app.get('/api/todos/suspended', authenticateToken, async (req, res) => {
try { try {
const connection = await pool.getConnection(); const connection = await pool.getConnection();
const [rows] = await connection.execute( // 管理员可以查看所有用户的挂起待办,普通用户只能查看自己的
'SELECT * FROM todos WHERE suspended = TRUE ORDER BY suspended_date DESC, priority DESC, created_at DESC' let query = 'SELECT * FROM todos WHERE suspended = TRUE AND user_id = ? ORDER BY suspended_date DESC, priority DESC, created_at DESC';
); let params = [];
params.push(req.user.userId);
const [rows] = await connection.execute(query, params);
connection.release(); connection.release();
res.json(rows); res.json(rows);
@ -281,15 +535,17 @@ app.put('/api/todos/:id/resume', authenticateToken, async (req, res) => {
const connection = await pool.getConnection(); const connection = await pool.getConnection();
// 检查是否是挂起的待办事项 // 检查是否是挂起的待办事项且属于当前用户
const [todoCheck] = await connection.execute( let checkQuery = 'SELECT * FROM todos WHERE id = ? AND suspended = TRUE AND user_id = ?';
'SELECT * FROM todos WHERE id = ? AND suspended = TRUE', let checkParams = [id];
[id] checkParams.push(req.user.userId);
);
const [todoCheck] = await connection.execute(checkQuery, checkParams);
if (todoCheck.length === 0) { if (todoCheck.length === 0) {
connection.release(); connection.release();
return res.status(400).json({ error: '待办事项未处于挂起状态' }); return res.status(400).json({ error: '待办事项未处于挂起状态或无权限访问' });
} }
// 恢复待办事项到今天 // 恢复待办事项到今天
@ -320,10 +576,11 @@ app.post('/api/todos/migrate-pending', authenticateToken, async (req, res) => {
const today = new Date().toISOString().split('T')[0]; const today = new Date().toISOString().split('T')[0];
// 查找今天之前所有未完成且未挂起的待办事项 // 查找今天之前所有未完成且未挂起的待办事项
const [pendingTodos] = await connection.execute( let query = 'SELECT * FROM todos WHERE date < ? AND completed = FALSE AND suspended = FALSE AND user_id = ? ORDER BY date DESC, priority DESC';
'SELECT * FROM todos WHERE date < ? AND completed = FALSE AND suspended = FALSE ORDER BY date DESC, priority DESC', let params = [today];
[today] params.push(req.user.userId);
);
const [pendingTodos] = await connection.execute(query, params);
let migratedCount = 0; let migratedCount = 0;