ui优化
This commit is contained in:
parent
38cd670902
commit
0f772c4a64
2
client/node_modules/.cache/.eslintcache
generated
vendored
2
client/node_modules/.cache/.eslintcache
generated
vendored
File diff suppressed because one or more lines are too long
1
client/node_modules/.cache/babel-loader/014cccc67358b647f3759a02e8a9c1e6bcbe4be7585ca999da4fa4230892a92c.json
generated
vendored
Normal file
1
client/node_modules/.cache/babel-loader/014cccc67358b647f3759a02e8a9c1e6bcbe4be7585ca999da4fa4230892a92c.json
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
1
client/node_modules/.cache/babel-loader/0300b30013d15f14abe42de07b449562a4f0280bcdc54b047ac5c9c7521a0e9d.json
generated
vendored
Normal file
1
client/node_modules/.cache/babel-loader/0300b30013d15f14abe42de07b449562a4f0280bcdc54b047ac5c9c7521a0e9d.json
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
1
client/node_modules/.cache/babel-loader/0cd906f553e1ef5e294ac472729e70437f21530386f7a28421428b2e7ae5ac6b.json
generated
vendored
Normal file
1
client/node_modules/.cache/babel-loader/0cd906f553e1ef5e294ac472729e70437f21530386f7a28421428b2e7ae5ac6b.json
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
1
client/node_modules/.cache/babel-loader/539c524edeb39ecda6df0d75e795e90e68bb710e4a677ceac24b289d07b56ff4.json
generated
vendored
Normal file
1
client/node_modules/.cache/babel-loader/539c524edeb39ecda6df0d75e795e90e68bb710e4a677ceac24b289d07b56ff4.json
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
1
client/node_modules/.cache/babel-loader/6890932def348f25727baf9b2d0a4ffea4bea464ed514dfec5f02b70780dd763.json
generated
vendored
Normal file
1
client/node_modules/.cache/babel-loader/6890932def348f25727baf9b2d0a4ffea4bea464ed514dfec5f02b70780dd763.json
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
1
client/node_modules/.cache/babel-loader/6968f7e77a72be2d50f69f6d6f57948c98340e066c0e12d0be2efe0b1469524d.json
generated
vendored
Normal file
1
client/node_modules/.cache/babel-loader/6968f7e77a72be2d50f69f6d6f57948c98340e066c0e12d0be2efe0b1469524d.json
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
1
client/node_modules/.cache/babel-loader/78f620a59586d8198533a07930695eeb28c536cbaca0eae3dacabca298afa0d8.json
generated
vendored
Normal file
1
client/node_modules/.cache/babel-loader/78f620a59586d8198533a07930695eeb28c536cbaca0eae3dacabca298afa0d8.json
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
1
client/node_modules/.cache/babel-loader/99232906c17a6e16207b89bca5511463db9bcf01dd90abbeff4c7966080e76ff.json
generated
vendored
Normal file
1
client/node_modules/.cache/babel-loader/99232906c17a6e16207b89bca5511463db9bcf01dd90abbeff4c7966080e76ff.json
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
1
client/node_modules/.cache/babel-loader/a3f616c9017e66dcf51c2603c701552917a6c080312cac72dffbf5afa616af2e.json
generated
vendored
Normal file
1
client/node_modules/.cache/babel-loader/a3f616c9017e66dcf51c2603c701552917a6c080312cac72dffbf5afa616af2e.json
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
1
client/node_modules/.cache/babel-loader/a616666ea4a177469b0fd76aedb319694a281e5ee46980909379cee874007e72.json
generated
vendored
Normal file
1
client/node_modules/.cache/babel-loader/a616666ea4a177469b0fd76aedb319694a281e5ee46980909379cee874007e72.json
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
1
client/node_modules/.cache/babel-loader/b3f0c93f3dc4a017964e62dd7dabc688b11ae4f7d6df8f0271444e698ee040b6.json
generated
vendored
Normal file
1
client/node_modules/.cache/babel-loader/b3f0c93f3dc4a017964e62dd7dabc688b11ae4f7d6df8f0271444e698ee040b6.json
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
1
client/node_modules/.cache/babel-loader/e1296f11f283003da76a583f4a6ff072af7457d9d6890059ed652f5e06044ba1.json
generated
vendored
Normal file
1
client/node_modules/.cache/babel-loader/e1296f11f283003da76a583f4a6ff072af7457d9d6890059ed652f5e06044ba1.json
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
client/node_modules/.cache/default-development/0.pack
generated
vendored
BIN
client/node_modules/.cache/default-development/0.pack
generated
vendored
Binary file not shown.
BIN
client/node_modules/.cache/default-development/1.pack
generated
vendored
BIN
client/node_modules/.cache/default-development/1.pack
generated
vendored
Binary file not shown.
BIN
client/node_modules/.cache/default-development/10.pack
generated
vendored
BIN
client/node_modules/.cache/default-development/10.pack
generated
vendored
Binary file not shown.
BIN
client/node_modules/.cache/default-development/11.pack
generated
vendored
BIN
client/node_modules/.cache/default-development/11.pack
generated
vendored
Binary file not shown.
BIN
client/node_modules/.cache/default-development/13.pack
generated
vendored
Normal file
BIN
client/node_modules/.cache/default-development/13.pack
generated
vendored
Normal file
Binary file not shown.
BIN
client/node_modules/.cache/default-development/2.pack
generated
vendored
BIN
client/node_modules/.cache/default-development/2.pack
generated
vendored
Binary file not shown.
BIN
client/node_modules/.cache/default-development/3.pack
generated
vendored
BIN
client/node_modules/.cache/default-development/3.pack
generated
vendored
Binary file not shown.
BIN
client/node_modules/.cache/default-development/4.pack
generated
vendored
BIN
client/node_modules/.cache/default-development/4.pack
generated
vendored
Binary file not shown.
BIN
client/node_modules/.cache/default-development/5.pack
generated
vendored
BIN
client/node_modules/.cache/default-development/5.pack
generated
vendored
Binary file not shown.
BIN
client/node_modules/.cache/default-development/6.pack
generated
vendored
BIN
client/node_modules/.cache/default-development/6.pack
generated
vendored
Binary file not shown.
BIN
client/node_modules/.cache/default-development/7.pack
generated
vendored
BIN
client/node_modules/.cache/default-development/7.pack
generated
vendored
Binary file not shown.
BIN
client/node_modules/.cache/default-development/8.pack
generated
vendored
BIN
client/node_modules/.cache/default-development/8.pack
generated
vendored
Binary file not shown.
BIN
client/node_modules/.cache/default-development/9.pack
generated
vendored
BIN
client/node_modules/.cache/default-development/9.pack
generated
vendored
Binary file not shown.
BIN
client/node_modules/.cache/default-development/index.pack
generated
vendored
BIN
client/node_modules/.cache/default-development/index.pack
generated
vendored
Binary file not shown.
BIN
client/node_modules/.cache/default-development/index.pack.old
generated
vendored
BIN
client/node_modules/.cache/default-development/index.pack.old
generated
vendored
Binary file not shown.
|
|
@ -1,65 +1,107 @@
|
|||
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 TodoApp from './components/TodoApp';
|
||||
import { checkAuth } from './services/api';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const AppContainer = styled.div`
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: 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() {
|
||||
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [currentUser, setCurrentUser] = useState(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
checkAuth(token)
|
||||
.then(() => {
|
||||
setIsAuthenticated(true);
|
||||
})
|
||||
.catch(() => {
|
||||
localStorage.removeItem('token');
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
} else {
|
||||
setLoading(false);
|
||||
const user = localStorage.getItem('user');
|
||||
|
||||
if (token && user) {
|
||||
try {
|
||||
const parsedUser = JSON.parse(user);
|
||||
setCurrentUser(parsedUser);
|
||||
setIsAuthenticated(true);
|
||||
} catch (error) {
|
||||
console.error('解析用户信息失败:', error);
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('user');
|
||||
}
|
||||
}
|
||||
setIsLoading(false);
|
||||
}, []);
|
||||
|
||||
const handleLogin = (token) => {
|
||||
const handleLogin = (token, user) => {
|
||||
localStorage.setItem('token', token);
|
||||
localStorage.setItem('user', JSON.stringify(user));
|
||||
setCurrentUser(user);
|
||||
setIsAuthenticated(true);
|
||||
};
|
||||
|
||||
const handleLogout = () => {
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('user');
|
||||
setCurrentUser(null);
|
||||
setIsAuthenticated(false);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<AppContainer>
|
||||
<div>加载中...</div>
|
||||
</AppContainer>
|
||||
<ThemeProvider theme={theme}>
|
||||
<GlobalStyle />
|
||||
<AppContainer>
|
||||
<AppContent>
|
||||
<div style={{
|
||||
textAlign: 'center',
|
||||
color: theme.colors.text.secondary,
|
||||
fontSize: theme.typography.fontSize.lg
|
||||
}}>
|
||||
加载中...
|
||||
</div>
|
||||
</AppContent>
|
||||
</AppContainer>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<AppContainer>
|
||||
{isAuthenticated ? (
|
||||
<TodoApp onLogout={handleLogout} />
|
||||
) : (
|
||||
<LoginForm onLogin={handleLogin} />
|
||||
)}
|
||||
</AppContainer>
|
||||
<ThemeProvider theme={theme}>
|
||||
<GlobalStyle />
|
||||
<AppContainer>
|
||||
<AppContent>
|
||||
{isAuthenticated ? (
|
||||
<TodoApp
|
||||
onLogout={handleLogout}
|
||||
currentUser={currentUser}
|
||||
/>
|
||||
) : (
|
||||
<LoginForm onLogin={handleLogin} />
|
||||
)}
|
||||
</AppContent>
|
||||
</AppContainer>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,64 +1,121 @@
|
|||
import React, { useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import styled, { keyframes } from 'styled-components';
|
||||
import { login } from '../services/api';
|
||||
|
||||
const LoginContainer = styled.div`
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 20px;
|
||||
padding: 40px;
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
text-align: center;
|
||||
const fadeIn = keyframes`
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
`;
|
||||
|
||||
const Title = styled.h1`
|
||||
color: #333;
|
||||
margin-bottom: 30px;
|
||||
font-size: 28px;
|
||||
font-weight: 300;
|
||||
const LoginContainer = styled.div`
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
margin: 0 auto;
|
||||
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`
|
||||
display: flex;
|
||||
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`
|
||||
padding: 15px 20px;
|
||||
border: 2px solid #e1e5e9;
|
||||
border-radius: 12px;
|
||||
font-size: 16px;
|
||||
transition: all 0.3s ease;
|
||||
outline: none;
|
||||
width: 100%;
|
||||
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.lg};
|
||||
background: ${({ theme }) => theme.colors.glass.light};
|
||||
color: ${({ theme }) => theme.colors.text.primary};
|
||||
transition: all ${({ theme }) => theme.transitions.default};
|
||||
|
||||
&:focus {
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
||||
outline: none;
|
||||
border-color: ${({ theme }) => theme.colors.primary};
|
||||
box-shadow: 0 0 0 3px ${({ theme }) => theme.colors.primary}40;
|
||||
}
|
||||
|
||||
&::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`
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
const SubmitButton = styled.button`
|
||||
width: 100%;
|
||||
padding: ${({ theme }) => theme.spacing.md};
|
||||
font-size: ${({ theme }) => theme.typography.fontSize.base};
|
||||
font-weight: ${({ theme }) => theme.typography.fontWeight.semibold};
|
||||
color: white;
|
||||
background: ${({ theme }) => theme.colors.primary};
|
||||
border: none;
|
||||
padding: 15px 20px;
|
||||
border-radius: 12px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
border-radius: ${({ theme }) => theme.borderRadius.lg};
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
margin-top: 10px;
|
||||
transition: all ${({ theme }) => theme.transitions.default};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: ${({ theme }) => theme.spacing.sm};
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 20px rgba(102, 126, 234, 0.3);
|
||||
background: ${({ theme }) => theme.colors.primary}dd;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
|
|
@ -66,66 +123,102 @@ const Button = styled.button`
|
|||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '→';
|
||||
font-size: 1.2em;
|
||||
transition: transform ${({ theme }) => theme.transitions.default};
|
||||
}
|
||||
|
||||
&:hover::after {
|
||||
transform: translateX(4px);
|
||||
}
|
||||
`;
|
||||
|
||||
const ErrorMessage = styled.div`
|
||||
color: #e74c3c;
|
||||
font-size: 14px;
|
||||
margin-top: 10px;
|
||||
padding: 10px;
|
||||
background: rgba(231, 76, 60, 0.1);
|
||||
border-radius: 8px;
|
||||
color: ${({ theme }) => theme.colors.status.error};
|
||||
font-size: ${({ theme }) => theme.typography.fontSize.sm};
|
||||
text-align: center;
|
||||
margin-top: ${({ theme }) => theme.spacing.sm};
|
||||
padding: ${({ theme }) => theme.spacing.sm};
|
||||
background: ${({ theme }) => theme.colors.status.error}20;
|
||||
border-radius: ${({ theme }) => theme.borderRadius.md};
|
||||
animation: ${fadeIn} 0.3s ease-out;
|
||||
`;
|
||||
|
||||
const Hint = styled.div`
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
margin-top: 20px;
|
||||
padding: 15px;
|
||||
background: rgba(102, 126, 234, 0.1);
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid #667eea;
|
||||
const LoginHint = styled.div`
|
||||
color: ${({ theme }) => theme.colors.text.secondary};
|
||||
font-size: ${({ theme }) => theme.typography.fontSize.sm};
|
||||
text-align: center;
|
||||
margin-top: ${({ theme }) => theme.spacing.md};
|
||||
padding: ${({ theme }) => theme.spacing.sm};
|
||||
background: ${({ theme }) => theme.colors.secondary}20;
|
||||
border-radius: ${({ theme }) => theme.borderRadius.md};
|
||||
|
||||
strong {
|
||||
color: ${({ theme }) => theme.colors.secondary};
|
||||
}
|
||||
`;
|
||||
|
||||
function LoginForm({ onLogin }) {
|
||||
const [username, setUsername] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
setError('');
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const response = await login(password);
|
||||
onLogin(response.token);
|
||||
const result = await login(username, password);
|
||||
// 保存token和用户信息
|
||||
localStorage.setItem('token', result.token);
|
||||
localStorage.setItem('user', JSON.stringify(result.user));
|
||||
onLogin(result.token, result.user);
|
||||
} catch (err) {
|
||||
setError(err.response?.data?.error || '登录失败,请重试');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<LoginContainer>
|
||||
<Title>工作待办</Title>
|
||||
<LoginTitle>工作待办</LoginTitle>
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="请输入访问密码"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
/>
|
||||
<Button type="submit" disabled={loading}>
|
||||
{loading ? '验证中...' : '进入系统'}
|
||||
</Button>
|
||||
<InputGroup>
|
||||
<Label>用户名</Label>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="请输入用户名"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
disabled={isLoading}
|
||||
required
|
||||
/>
|
||||
</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>
|
||||
{error && <ErrorMessage>{error}</ErrorMessage>}
|
||||
<Hint>
|
||||
💡 提示:首次使用需要输入访问密码
|
||||
</Hint>
|
||||
<LoginHint>
|
||||
{/* <strong>管理员:</strong> 用户名 admin,密码 weiMonkey2024<br /> */}
|
||||
<strong>普通用户:</strong> 请使用管理员分配的账号密码
|
||||
</LoginHint>
|
||||
</LoginContainer>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import TodoForm from './TodoForm';
|
|||
import TodoList from './TodoList';
|
||||
import HistoryTodos from './HistoryTodos';
|
||||
import SuspendedTodos from './SuspendedTodos';
|
||||
import UserManagement from './UserManagement';
|
||||
|
||||
const AppContainer = styled.div`
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
|
|
@ -29,6 +30,12 @@ const Header = styled.div`
|
|||
border-bottom: 2px solid #f0f0f0;
|
||||
`;
|
||||
|
||||
const TitleSection = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
`;
|
||||
|
||||
const Title = styled.h1`
|
||||
color: #333;
|
||||
font-size: 32px;
|
||||
|
|
@ -36,16 +43,61 @@ const Title = styled.h1`
|
|||
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`
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 8px;
|
||||
}
|
||||
`;
|
||||
|
||||
const MigratePendingButton = styled.button`
|
||||
background: rgba(255, 152, 0, 0.1);
|
||||
color: #ff9800;
|
||||
border: 2px solid rgba(255, 152, 0, 0.2);
|
||||
const ActionButton = styled.button`
|
||||
background: ${({ variant, theme }) => {
|
||||
switch (variant) {
|
||||
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;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
|
|
@ -54,85 +106,27 @@ const MigratePendingButton = styled.button`
|
|||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 152, 0, 0.15);
|
||||
border-color: #ff9800;
|
||||
transform: translateY(-1px);
|
||||
${({ variant }) => {
|
||||
if (variant === 'default') {
|
||||
return `
|
||||
border-color: #667eea;
|
||||
color: #667eea;
|
||||
`;
|
||||
}
|
||||
return 'opacity: 0.9;';
|
||||
}}
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: '📦';
|
||||
content: ${({ icon }) => icon ? `'${icon}'` : 'none'};
|
||||
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`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
@ -156,12 +150,13 @@ const EmptyMessage = styled.div`
|
|||
border: 2px dashed #e1e5e9;
|
||||
`;
|
||||
|
||||
function TodoApp({ onLogout }) {
|
||||
function TodoApp({ onLogout, currentUser }) {
|
||||
const [todos, setTodos] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
const [showHistory, setShowHistory] = useState(false);
|
||||
const [showSuspended, setShowSuspended] = useState(false);
|
||||
const [showUserManagement, setShowUserManagement] = useState(false);
|
||||
const [successMessage, setSuccessMessage] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -292,6 +287,15 @@ function TodoApp({ onLogout }) {
|
|||
|
||||
const groupedTodos = groupTodosByDate(todos);
|
||||
|
||||
// 如果正在显示用户管理视图
|
||||
if (showUserManagement) {
|
||||
return (
|
||||
<AppContainer>
|
||||
<UserManagement onBack={() => setShowUserManagement(false)} />
|
||||
</AppContainer>
|
||||
);
|
||||
}
|
||||
|
||||
// 如果正在显示历史视图
|
||||
if (showHistory) {
|
||||
return (
|
||||
|
|
@ -321,18 +325,54 @@ function TodoApp({ onLogout }) {
|
|||
return (
|
||||
<AppContainer>
|
||||
<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>
|
||||
<MigratePendingButton onClick={handleMigratePendingTodos}>
|
||||
<ActionButton
|
||||
variant="warning"
|
||||
icon="📦"
|
||||
onClick={handleMigratePendingTodos}
|
||||
>
|
||||
迁移未完成
|
||||
</MigratePendingButton>
|
||||
<SuspendedButton onClick={() => setShowSuspended(true)}>
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
variant="success"
|
||||
icon="⏸️"
|
||||
onClick={() => setShowSuspended(true)}
|
||||
>
|
||||
挂起待办
|
||||
</SuspendedButton>
|
||||
<HistoryButton onClick={() => setShowHistory(true)}>
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
variant="primary"
|
||||
icon="📚"
|
||||
onClick={() => setShowHistory(true)}
|
||||
>
|
||||
历史记录
|
||||
</HistoryButton>
|
||||
<LogoutButton onClick={onLogout}>退出</LogoutButton>
|
||||
</ActionButton>
|
||||
{currentUser?.is_admin && (
|
||||
<ActionButton
|
||||
variant="primary"
|
||||
icon="👥"
|
||||
onClick={() => setShowUserManagement(true)}
|
||||
>
|
||||
用户管理
|
||||
</ActionButton>
|
||||
)}
|
||||
<ActionButton
|
||||
variant="default"
|
||||
onClick={onLogout}
|
||||
>
|
||||
退出
|
||||
</ActionButton>
|
||||
</HeaderActions>
|
||||
</Header>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,219 +1,344 @@
|
|||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import React, { useState } from 'react';
|
||||
import styled, { keyframes } from 'styled-components';
|
||||
|
||||
const ItemContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
background: ${props => props.completed ? '#f8f9fa' : 'white'};
|
||||
border: 2px solid ${props => props.completed ? '#e9ecef' : '#f0f0f0'};
|
||||
border-radius: 16px;
|
||||
transition: all 0.3s ease;
|
||||
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 slideIn = keyframes`
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
`;
|
||||
|
||||
const PriorityIndicator = styled.div`
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
background-color: ${props => props.color};
|
||||
margin-right: 20px;
|
||||
flex-shrink: 0;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
|
||||
const fadeIn = keyframes`
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
`;
|
||||
|
||||
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;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
gap: ${({ theme }) => theme.spacing.xs};
|
||||
min-width: 0; // 防止内容溢出
|
||||
`;
|
||||
|
||||
const TodoTitle = styled.span`
|
||||
font-size: 17px;
|
||||
color: ${props => props.completed ? '#6c757d' : '#333'};
|
||||
text-decoration: ${props => props.completed ? 'line-through' : 'none'};
|
||||
font-weight: 500;
|
||||
const Title = styled.span`
|
||||
color: ${({ theme, completed }) => completed ? theme.colors.text.secondary : theme.colors.text.primary};
|
||||
font-size: ${({ theme }) => theme.typography.fontSize.base};
|
||||
font-weight: ${({ theme }) => theme.typography.fontWeight.medium};
|
||||
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;
|
||||
`;
|
||||
|
||||
const PriorityLabel = styled.span`
|
||||
font-size: 13px;
|
||||
color: ${props => props.color};
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
`;
|
||||
|
||||
const ActionButtons = styled.div`
|
||||
display: flex;
|
||||
const PriorityBadge = styled.span`
|
||||
padding: ${({ theme }) => `${theme.spacing.xs} ${theme.spacing.sm}`};
|
||||
border-radius: ${({ theme }) => theme.borderRadius.full};
|
||||
font-size: ${({ theme }) => theme.typography.fontSize.xs};
|
||||
font-weight: ${({ theme }) => theme.typography.fontWeight.medium};
|
||||
background: ${({ theme, priority }) => theme.colors.priority[priority]}20;
|
||||
color: ${({ theme, priority }) => theme.colors.priority[priority]};
|
||||
transition: all ${({ theme }) => theme.transitions.default};
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
`;
|
||||
gap: ${({ theme }) => theme.spacing.xs};
|
||||
|
||||
const CheckButton = styled.button`
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid ${props => props.completed ? '#28a745' : '#dee2e6'};
|
||||
background: ${props => props.completed ? '#28a745' : 'white'};
|
||||
cursor: ${props => props.isHistory ? 'default' : 'pointer'};
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
opacity: ${props => props.isHistory ? 0.6 : 1};
|
||||
&::before {
|
||||
content: ${({ priority }) => {
|
||||
switch (priority) {
|
||||
case 'urgent': return '"🚨"';
|
||||
case 'high': return '"🔥"';
|
||||
case 'medium': return '"⚡"';
|
||||
case 'low': return '"🐢"';
|
||||
default: return '"📌"';
|
||||
}
|
||||
}};
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: ${props => props.isHistory ? (props.completed ? '#28a745' : '#dee2e6') : '#28a745'};
|
||||
background: ${props => props.completed ? '#28a745' : (props.isHistory ? 'white' : 'rgba(40, 167, 69, 0.1)')};
|
||||
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;
|
||||
transform: translateY(-1px);
|
||||
background: ${({ theme, priority }) => theme.colors.priority[priority]}30;
|
||||
}
|
||||
`;
|
||||
|
||||
const DeleteButton = styled.button`
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid #e74c3c;
|
||||
background: white;
|
||||
const ButtonGroup = styled.div`
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing.sm};
|
||||
opacity: 0;
|
||||
transition: all ${({ theme }) => theme.transitions.default};
|
||||
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;
|
||||
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;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
font-size: 1.2em;
|
||||
|
||||
&:hover {
|
||||
background: #e74c3c;
|
||||
transform: scale(1.1);
|
||||
background: ${({ theme }) => theme.colors.glass.light};
|
||||
color: ${({ theme }) => theme.colors.primary};
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '×';
|
||||
color: #e74c3c;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
transition: color 0.3s ease;
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
&:hover::after {
|
||||
color: white;
|
||||
&:focus {
|
||||
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`
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid #ff9800;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
const StatusBadge = styled.span`
|
||||
position: absolute;
|
||||
top: ${({ theme }) => theme.spacing.sm};
|
||||
right: ${({ theme }) => theme.spacing.sm};
|
||||
padding: ${({ theme }) => `${theme.spacing.xs} ${theme.spacing.sm}`};
|
||||
border-radius: ${({ theme }) => theme.borderRadius.full};
|
||||
font-size: ${({ theme }) => theme.typography.fontSize.xs};
|
||||
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;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
gap: ${({ theme }) => theme.spacing.xs};
|
||||
|
||||
&:hover {
|
||||
background: #ff9800;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '⏸';
|
||||
color: #ff9800;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
&:hover::after {
|
||||
color: white;
|
||||
&::before {
|
||||
content: ${({ status }) => {
|
||||
switch (status) {
|
||||
case 'warning': return '"⏸️"';
|
||||
case 'success': return '"✅"';
|
||||
case 'error': return '"❌"';
|
||||
default: return '"📌"';
|
||||
}
|
||||
}};
|
||||
font-size: 1.1em;
|
||||
}
|
||||
`;
|
||||
|
||||
const priorityConfig = {
|
||||
low: { color: '#95a5a6', label: '低' },
|
||||
medium: { color: '#3498db', label: '中' },
|
||||
high: { color: '#e67e22', label: '高' },
|
||||
urgent: { color: '#e74c3c', label: '急' }
|
||||
};
|
||||
const Tooltip = styled.div`
|
||||
position: absolute;
|
||||
bottom: 100%;
|
||||
left: 50%;
|
||||
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 }) {
|
||||
const priority = priorityConfig[todo.priority] || priorityConfig.medium;
|
||||
${ActionButton}:hover & {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transform: translateX(-50%) translateY(-4px);
|
||||
}
|
||||
`;
|
||||
|
||||
const handleClick = (e) => {
|
||||
// 如果点击的是删除或挂起按钮,不触发toggle
|
||||
if (e.target.closest('button[data-action="delete"]') || e.target.closest('button[data-action="suspend"]')) {
|
||||
return;
|
||||
}
|
||||
if (!isHistory) {
|
||||
onToggle(todo.id, !todo.completed);
|
||||
}
|
||||
function TodoItem({ todo, onToggle, onDelete, onSuspend, isHistory, isToday }) {
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
|
||||
const getPriorityLabel = (priority) => {
|
||||
const labels = {
|
||||
low: '低优先级',
|
||||
medium: '中优先级',
|
||||
high: '高优先级',
|
||||
urgent: '紧急'
|
||||
};
|
||||
return labels[priority] || priority;
|
||||
};
|
||||
|
||||
const handleToggle = (e) => {
|
||||
e.stopPropagation();
|
||||
onToggle(todo.id, !todo.completed);
|
||||
};
|
||||
|
||||
const handleDelete = (e) => {
|
||||
e.stopPropagation();
|
||||
if (onDelete) {
|
||||
onDelete(todo.id);
|
||||
}
|
||||
onDelete(todo.id);
|
||||
};
|
||||
|
||||
const handleSuspend = (e) => {
|
||||
e.stopPropagation();
|
||||
if (onSuspend) {
|
||||
onSuspend(todo.id);
|
||||
}
|
||||
onSuspend(todo.id);
|
||||
};
|
||||
|
||||
return (
|
||||
<ItemContainer completed={todo.completed} onClick={handleClick} isHistory={isHistory}>
|
||||
<PriorityIndicator color={priority.color} />
|
||||
|
||||
<TodoContent>
|
||||
<TodoTitle completed={todo.completed}>
|
||||
<ItemContainer
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
onClick={handleToggle}
|
||||
>
|
||||
<Checkbox
|
||||
type="checkbox"
|
||||
checked={todo.completed}
|
||||
onChange={handleToggle}
|
||||
/>
|
||||
<Content>
|
||||
<Title completed={todo.completed}>
|
||||
{todo.title}
|
||||
</TodoTitle>
|
||||
<PriorityLabel color={priority.color}>
|
||||
{priority.label}优先级
|
||||
</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="删除待办事项"
|
||||
/>
|
||||
</>
|
||||
</Title>
|
||||
{todo.description && (
|
||||
<Description>{todo.description}</Description>
|
||||
)}
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,44 +1,130 @@
|
|||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import styled, { keyframes } from 'styled-components';
|
||||
import TodoItem from './TodoItem';
|
||||
|
||||
const fadeIn = keyframes`
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
`;
|
||||
|
||||
const ListContainer = styled.div`
|
||||
background: white;
|
||||
border-radius: 20px;
|
||||
padding: 30px;
|
||||
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.08);
|
||||
border: 1px solid #f0f0f0;
|
||||
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;
|
||||
|
||||
@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`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 25px;
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 2px solid #f8f9fa;
|
||||
justify-content: space-between;
|
||||
margin-bottom: ${({ theme }) => theme.spacing.xl};
|
||||
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`
|
||||
color: #333;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: ${({ theme }) => theme.colors.text.primary};
|
||||
font-size: ${({ theme }) => theme.typography.fontSize.xl};
|
||||
font-weight: ${({ theme }) => theme.typography.fontWeight.semibold};
|
||||
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`
|
||||
background: rgba(102, 126, 234, 0.1);
|
||||
color: #667eea;
|
||||
padding: 6px 16px;
|
||||
border-radius: 20px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
const TodoCount = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: ${({ theme }) => theme.spacing.xs};
|
||||
padding: ${({ theme }) => `${theme.spacing.xs} ${theme.spacing.md}`};
|
||||
background: ${({ theme }) => theme.colors.primary}20;
|
||||
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`
|
||||
display: flex;
|
||||
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 }) {
|
||||
|
|
@ -55,17 +141,23 @@ function TodoList({ dateLabel, todos, onToggleTodo, onDeleteTodo, onSuspendTodo,
|
|||
</DateHeader>
|
||||
|
||||
<TodoItems>
|
||||
{todos.map(todo => (
|
||||
<TodoItem
|
||||
key={todo.id}
|
||||
todo={todo}
|
||||
onToggle={onToggleTodo}
|
||||
onDelete={onDeleteTodo}
|
||||
onSuspend={onSuspendTodo}
|
||||
isHistory={isHistory}
|
||||
isToday={isToday}
|
||||
/>
|
||||
))}
|
||||
{todos.length === 0 ? (
|
||||
<EmptyMessage>
|
||||
暂无待办事项
|
||||
</EmptyMessage>
|
||||
) : (
|
||||
todos.map(todo => (
|
||||
<TodoItem
|
||||
key={todo.id}
|
||||
todo={todo}
|
||||
onToggle={onToggleTodo}
|
||||
onDelete={onDeleteTodo}
|
||||
onSuspend={onSuspendTodo}
|
||||
isHistory={isHistory}
|
||||
isToday={isToday}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</TodoItems>
|
||||
</ListContainer>
|
||||
);
|
||||
|
|
|
|||
369
client/src/components/UserManagement.js
Normal file
369
client/src/components/UserManagement.js
Normal 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;
|
||||
|
|
@ -1,13 +1,13 @@
|
|||
import axios from 'axios';
|
||||
|
||||
const API_BASE_URL = '/api';
|
||||
const API_URL = 'http://localhost:5000/api';
|
||||
|
||||
// 创建axios实例
|
||||
const api = axios.create({
|
||||
baseURL: API_BASE_URL,
|
||||
baseURL: API_URL,
|
||||
});
|
||||
|
||||
// 添加请求拦截器,自动添加token
|
||||
// 请求拦截器,自动添加token
|
||||
api.interceptors.request.use((config) => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
|
|
@ -16,17 +16,21 @@ api.interceptors.request.use((config) => {
|
|||
return config;
|
||||
});
|
||||
|
||||
// 登录验证
|
||||
export const login = async (password) => {
|
||||
const response = await api.post('/auth', { password });
|
||||
// 用户登录
|
||||
export const login = async (username, password) => {
|
||||
const response = await api.post('/auth/login', { username, password });
|
||||
return response.data;
|
||||
};
|
||||
|
||||
// 检查认证状态
|
||||
export const checkAuth = async (token) => {
|
||||
const response = await api.get('/todos', {
|
||||
headers: { Authorization: `Bearer ${token}` }
|
||||
});
|
||||
// 管理员注册新用户
|
||||
export const registerUser = async (username, password) => {
|
||||
const response = await api.post('/auth/register', { username, password });
|
||||
return response.data;
|
||||
};
|
||||
|
||||
// 获取所有用户(仅管理员)
|
||||
export const getUsers = async () => {
|
||||
const response = await api.get('/users');
|
||||
return response.data;
|
||||
};
|
||||
|
||||
|
|
@ -42,6 +46,12 @@ export const getHistoryTodos = async () => {
|
|||
return response.data;
|
||||
};
|
||||
|
||||
// 获取挂起的待办事项
|
||||
export const getSuspendedTodos = async () => {
|
||||
const response = await api.get('/todos/suspended');
|
||||
return response.data;
|
||||
};
|
||||
|
||||
// 迁移历史未完成待办到今天
|
||||
export const migratePendingTodos = async () => {
|
||||
const response = await api.post('/todos/migrate-pending');
|
||||
|
|
@ -54,12 +64,6 @@ export const suspendTodo = async (id) => {
|
|||
return response.data;
|
||||
};
|
||||
|
||||
// 获取挂起的待办事项
|
||||
export const getSuspendedTodos = async () => {
|
||||
const response = await api.get('/todos/suspended');
|
||||
return response.data;
|
||||
};
|
||||
|
||||
// 恢复挂起的待办事项
|
||||
export const resumeTodo = async (id) => {
|
||||
const response = await api.put(`/todos/${id}/resume`);
|
||||
|
|
|
|||
149
client/src/styles/theme.js
Normal file
149
client/src/styles/theme.js
Normal 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;
|
||||
}
|
||||
`;
|
||||
341
server/index.js
341
server/index.js
|
|
@ -30,21 +30,47 @@ async function initDatabase() {
|
|||
try {
|
||||
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(`
|
||||
CREATE TABLE IF NOT EXISTS todos (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
priority ENUM('low', 'medium', 'high', 'urgent') DEFAULT 'medium',
|
||||
completed BOOLEAN DEFAULT FALSE,
|
||||
suspended BOOLEAN DEFAULT FALSE,
|
||||
date DATE NOT NULL,
|
||||
suspended_date DATE NULL,
|
||||
user_id INT NOT NULL,
|
||||
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字段(如果表已存在但没有该字段)
|
||||
try {
|
||||
await connection.execute(`
|
||||
|
|
@ -57,6 +83,30 @@ async function initDatabase() {
|
|||
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();
|
||||
console.log('数据库表初始化成功');
|
||||
} catch (error) {
|
||||
|
|
@ -76,14 +126,6 @@ const authenticatePassword = (req, res, 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的中间件
|
||||
const authenticateToken = (req, res, next) => {
|
||||
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) => {
|
||||
try {
|
||||
|
|
@ -111,10 +283,13 @@ app.get('/api/todos/history', authenticateToken, async (req, res) => {
|
|||
const today = new Date();
|
||||
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',
|
||||
[oneWeekAgo.toISOString().split('T')[0]]
|
||||
);
|
||||
// 管理员可以查看所有用户的数据,普通用户只能查看自己的
|
||||
let query = 'SELECT * FROM todos WHERE date < ? AND user_id = ? ORDER BY date DESC, priority DESC, created_at DESC';
|
||||
let params = [oneWeekAgo.toISOString().split('T')[0]];
|
||||
params.push(req.user.userId);
|
||||
|
||||
|
||||
const [rows] = await connection.execute(query, params);
|
||||
|
||||
connection.release();
|
||||
res.json(rows);
|
||||
|
|
@ -133,10 +308,12 @@ app.get('/api/todos', authenticateToken, async (req, res) => {
|
|||
const today = new Date();
|
||||
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',
|
||||
[oneWeekAgo.toISOString().split('T')[0], today.toISOString().split('T')[0]]
|
||||
);
|
||||
// 管理员可以查看所有用户的数据,普通用户只能查看自己的
|
||||
let query = 'SELECT * FROM todos WHERE date >= ? AND date <= ? AND suspended = FALSE AND user_id = ? ORDER BY date DESC, priority DESC, created_at DESC';
|
||||
|
||||
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();
|
||||
res.json(rows);
|
||||
|
|
@ -149,7 +326,7 @@ app.get('/api/todos', authenticateToken, async (req, res) => {
|
|||
// 创建新的待办事项
|
||||
app.post('/api/todos', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { title, priority, date } = req.body;
|
||||
const { title, priority, date, description } = req.body;
|
||||
|
||||
if (!title || !date) {
|
||||
return res.status(400).json({ error: '标题和日期是必需的' });
|
||||
|
|
@ -158,8 +335,8 @@ app.post('/api/todos', authenticateToken, async (req, res) => {
|
|||
const connection = await pool.getConnection();
|
||||
|
||||
const [result] = await connection.execute(
|
||||
'INSERT INTO todos (title, priority, date) VALUES (?, ?, ?)',
|
||||
[title, priority || 'medium', date]
|
||||
'INSERT INTO todos (title, description, priority, date, user_id) VALUES (?, ?, ?, ?, ?)',
|
||||
[title, description || null, priority || 'medium', date, req.user.userId]
|
||||
);
|
||||
|
||||
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) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { completed } = req.body;
|
||||
const { completed, title, priority, date, description } = req.body;
|
||||
|
||||
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(
|
||||
'UPDATE todos SET completed = ? WHERE id = ?',
|
||||
[completed, id]
|
||||
`UPDATE todos SET ${updates.join(', ')} WHERE id = ?`,
|
||||
values
|
||||
);
|
||||
|
||||
// 获取更新后的待办事项
|
||||
const [updatedTodo] = await connection.execute(
|
||||
'SELECT * FROM todos WHERE id = ?',
|
||||
[id]
|
||||
|
|
@ -208,6 +440,22 @@ app.delete('/api/todos/:id', authenticateToken, async (req, res) => {
|
|||
|
||||
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]);
|
||||
|
||||
connection.release();
|
||||
|
|
@ -226,15 +474,17 @@ app.put('/api/todos/:id/suspend', authenticateToken, async (req, res) => {
|
|||
|
||||
const connection = await pool.getConnection();
|
||||
|
||||
// 检查是否是今天的待办事项
|
||||
const [todoCheck] = await connection.execute(
|
||||
'SELECT * FROM todos WHERE id = ? AND date = ?',
|
||||
[id, today]
|
||||
);
|
||||
// 检查是否是今天的待办事项且属于当前用户
|
||||
let checkQuery = 'SELECT * FROM todos WHERE id = ? AND date = ? AND user_id = ?';
|
||||
let checkParams = [id, today];
|
||||
checkParams.push(req.user.userId);
|
||||
|
||||
|
||||
const [todoCheck] = await connection.execute(checkQuery, checkParams);
|
||||
|
||||
if (todoCheck.length === 0) {
|
||||
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 {
|
||||
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();
|
||||
res.json(rows);
|
||||
|
|
@ -281,15 +535,17 @@ app.put('/api/todos/:id/resume', authenticateToken, async (req, res) => {
|
|||
|
||||
const connection = await pool.getConnection();
|
||||
|
||||
// 检查是否是挂起的待办事项
|
||||
const [todoCheck] = await connection.execute(
|
||||
'SELECT * FROM todos WHERE id = ? AND suspended = TRUE',
|
||||
[id]
|
||||
);
|
||||
// 检查是否是挂起的待办事项且属于当前用户
|
||||
let checkQuery = 'SELECT * FROM todos WHERE id = ? AND suspended = TRUE AND user_id = ?';
|
||||
let checkParams = [id];
|
||||
checkParams.push(req.user.userId);
|
||||
|
||||
|
||||
const [todoCheck] = await connection.execute(checkQuery, checkParams);
|
||||
|
||||
if (todoCheck.length === 0) {
|
||||
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 [pendingTodos] = await connection.execute(
|
||||
'SELECT * FROM todos WHERE date < ? AND completed = FALSE AND suspended = FALSE ORDER BY date DESC, priority DESC',
|
||||
[today]
|
||||
);
|
||||
let query = 'SELECT * FROM todos WHERE date < ? AND completed = FALSE AND suspended = FALSE AND user_id = ? ORDER BY date DESC, priority DESC';
|
||||
let params = [today];
|
||||
params.push(req.user.userId);
|
||||
|
||||
const [pendingTodos] = await connection.execute(query, params);
|
||||
|
||||
let migratedCount = 0;
|
||||
|
||||
|
|
|
|||
Reference in New Issue
Block a user