6th
This commit is contained in:
@@ -10,93 +10,31 @@ import {
|
||||
FormControlLabel,
|
||||
Button,
|
||||
Divider,
|
||||
Avatar,
|
||||
TextField,
|
||||
IconButton,
|
||||
Chip,
|
||||
Grid,
|
||||
Paper,
|
||||
Alert,
|
||||
Snackbar,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
Select,
|
||||
MenuItem,
|
||||
Slider,
|
||||
} from '@mui/material';
|
||||
import {
|
||||
Person,
|
||||
Palette,
|
||||
Notifications,
|
||||
Security,
|
||||
Language,
|
||||
Save,
|
||||
Edit,
|
||||
PhotoCamera,
|
||||
DarkMode,
|
||||
LightMode,
|
||||
SettingsBrightness,
|
||||
VolumeUp,
|
||||
Email,
|
||||
Sms,
|
||||
Phone,
|
||||
Schedule,
|
||||
Visibility,
|
||||
Lock,
|
||||
Key,
|
||||
Shield,
|
||||
Refresh,
|
||||
} from '@mui/icons-material';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { motion } from 'framer-motion';
|
||||
import { useTheme } from '@/providers/ThemeProvider';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import DashboardLayout from '@/components/layout/DashboardLayout';
|
||||
|
||||
const SettingsPage = () => {
|
||||
const { themeMode, setThemeMode, actualTheme } = useTheme();
|
||||
const searchParams = useSearchParams();
|
||||
const { actualTheme } = useTheme();
|
||||
const [showSuccess, setShowSuccess] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState(() => {
|
||||
const tabParam = searchParams.get('tab');
|
||||
return tabParam || 'profile';
|
||||
});
|
||||
|
||||
// 用戶設定
|
||||
const [userSettings, setUserSettings] = useState(() => {
|
||||
try {
|
||||
const userStr = localStorage.getItem('user');
|
||||
if (userStr) {
|
||||
const user = JSON.parse(userStr);
|
||||
return {
|
||||
name: user.display_name || user.ad_account || '',
|
||||
email: user.email || '',
|
||||
department: '資訊部',
|
||||
position: '員工',
|
||||
phone: '',
|
||||
bio: '',
|
||||
avatar: (user.display_name || user.ad_account || 'U').charAt(0).toUpperCase(),
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to parse user from localStorage:', error);
|
||||
}
|
||||
|
||||
return {
|
||||
name: '',
|
||||
email: '',
|
||||
department: '資訊部',
|
||||
position: '員工',
|
||||
phone: '',
|
||||
bio: '',
|
||||
avatar: 'U',
|
||||
};
|
||||
});
|
||||
|
||||
// 通知設定
|
||||
// 郵件通知設定
|
||||
const [notificationSettings, setNotificationSettings] = useState({
|
||||
emailNotifications: true,
|
||||
pushNotifications: true,
|
||||
smsNotifications: false,
|
||||
todoReminders: true,
|
||||
deadlineAlerts: true,
|
||||
weeklyReports: true,
|
||||
@@ -104,639 +42,201 @@ const SettingsPage = () => {
|
||||
soundVolume: 70,
|
||||
});
|
||||
|
||||
// 隱私設定
|
||||
const [privacySettings, setPrivacySettings] = useState({
|
||||
profileVisibility: 'team',
|
||||
todoVisibility: 'responsible',
|
||||
showOnlineStatus: true,
|
||||
allowDirectMessages: true,
|
||||
dataSharing: false,
|
||||
});
|
||||
|
||||
// 工作設定
|
||||
const [workSettings, setWorkSettings] = useState({
|
||||
timeZone: 'Asia/Taipei',
|
||||
dateFormat: 'YYYY-MM-DD',
|
||||
timeFormat: '24h',
|
||||
workingHours: {
|
||||
start: '09:00',
|
||||
end: '18:00',
|
||||
},
|
||||
autoRefresh: 30,
|
||||
defaultView: 'list',
|
||||
});
|
||||
|
||||
const containerVariants = {
|
||||
hidden: { opacity: 0 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
transition: {
|
||||
staggerChildren: 0.1,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const itemVariants = {
|
||||
hidden: { opacity: 0, y: 20 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
transition: { duration: 0.5 },
|
||||
},
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
console.log('Settings saved:', {
|
||||
user: userSettings,
|
||||
notifications: notificationSettings,
|
||||
privacy: privacySettings,
|
||||
work: workSettings,
|
||||
});
|
||||
console.log('郵件通知設定已儲存:', notificationSettings);
|
||||
setShowSuccess(true);
|
||||
};
|
||||
|
||||
const themeOptions = [
|
||||
{ value: 'light', label: '亮色模式', icon: <LightMode /> },
|
||||
{ value: 'dark', label: '深色模式', icon: <DarkMode /> },
|
||||
{ value: 'system', label: '跟隨系統', icon: <SettingsBrightness /> },
|
||||
];
|
||||
|
||||
const tabs = [
|
||||
{ id: 'profile', label: '個人資料', icon: <Person /> },
|
||||
{ id: 'appearance', label: '外觀主題', icon: <Palette /> },
|
||||
{ id: 'notifications', label: '通知設定', icon: <Notifications /> },
|
||||
{ id: 'privacy', label: '隱私安全', icon: <Security /> },
|
||||
{ id: 'work', label: '工作偏好', icon: <Schedule /> },
|
||||
];
|
||||
|
||||
const renderProfileSettings = () => (
|
||||
<motion.div variants={itemVariants}>
|
||||
<Card sx={{
|
||||
backgroundColor: actualTheme === 'dark' ? '#1f2937' : '#ffffff',
|
||||
border: `1px solid ${actualTheme === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'}`,
|
||||
}}>
|
||||
<CardContent sx={{ p: 3 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 3 }}>
|
||||
<Person sx={{ color: 'primary.main', fontSize: 24 }} />
|
||||
<Typography variant="h6" sx={{ fontWeight: 600 }}>
|
||||
個人資料設定
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* 頭像區域 */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 3, mb: 4 }}>
|
||||
<Box sx={{ position: 'relative' }}>
|
||||
<Avatar
|
||||
sx={{
|
||||
width: 80,
|
||||
height: 80,
|
||||
fontSize: '2rem',
|
||||
fontWeight: 700,
|
||||
background: 'linear-gradient(45deg, #3b82f6 30%, #8b5cf6 90%)',
|
||||
}}
|
||||
>
|
||||
{userSettings.avatar}
|
||||
</Avatar>
|
||||
<IconButton
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
bottom: -5,
|
||||
right: -5,
|
||||
backgroundColor: 'primary.main',
|
||||
color: 'white',
|
||||
width: 32,
|
||||
height: 32,
|
||||
'&:hover': {
|
||||
backgroundColor: 'primary.dark',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<PhotoCamera sx={{ fontSize: 16 }} />
|
||||
</IconButton>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography variant="h6" sx={{ fontWeight: 600, mb: 0.5 }}>
|
||||
{userSettings.name}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
|
||||
{userSettings.position} · {userSettings.department}
|
||||
</Typography>
|
||||
<Chip
|
||||
label="已驗證"
|
||||
color="success"
|
||||
size="small"
|
||||
icon={<Shield sx={{ fontSize: 14 }} />}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="姓名"
|
||||
value={userSettings.name}
|
||||
onChange={(e) => setUserSettings(prev => ({ ...prev, name: e.target.value }))}
|
||||
sx={{
|
||||
'& .MuiOutlinedInput-root': {
|
||||
borderRadius: 2,
|
||||
backgroundColor: actualTheme === 'dark'
|
||||
? 'rgba(255, 255, 255, 0.05)'
|
||||
: 'rgba(0, 0, 0, 0.02)',
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="電子信箱"
|
||||
value={userSettings.email}
|
||||
onChange={(e) => setUserSettings(prev => ({ ...prev, email: e.target.value }))}
|
||||
sx={{
|
||||
'& .MuiOutlinedInput-root': {
|
||||
borderRadius: 2,
|
||||
backgroundColor: actualTheme === 'dark'
|
||||
? 'rgba(255, 255, 255, 0.05)'
|
||||
: 'rgba(0, 0, 0, 0.02)',
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="部門"
|
||||
value={userSettings.department}
|
||||
onChange={(e) => setUserSettings(prev => ({ ...prev, department: e.target.value }))}
|
||||
sx={{
|
||||
'& .MuiOutlinedInput-root': {
|
||||
borderRadius: 2,
|
||||
backgroundColor: actualTheme === 'dark'
|
||||
? 'rgba(255, 255, 255, 0.05)'
|
||||
: 'rgba(0, 0, 0, 0.02)',
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="職位"
|
||||
value={userSettings.position}
|
||||
onChange={(e) => setUserSettings(prev => ({ ...prev, position: e.target.value }))}
|
||||
sx={{
|
||||
'& .MuiOutlinedInput-root': {
|
||||
borderRadius: 2,
|
||||
backgroundColor: actualTheme === 'dark'
|
||||
? 'rgba(255, 255, 255, 0.05)'
|
||||
: 'rgba(0, 0, 0, 0.02)',
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="電話號碼"
|
||||
value={userSettings.phone}
|
||||
onChange={(e) => setUserSettings(prev => ({ ...prev, phone: e.target.value }))}
|
||||
sx={{
|
||||
'& .MuiOutlinedInput-root': {
|
||||
borderRadius: 2,
|
||||
backgroundColor: actualTheme === 'dark'
|
||||
? 'rgba(255, 255, 255, 0.05)'
|
||||
: 'rgba(0, 0, 0, 0.02)',
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<TextField
|
||||
fullWidth
|
||||
multiline
|
||||
rows={3}
|
||||
label="個人簡介"
|
||||
placeholder="簡單介紹一下自己..."
|
||||
value={userSettings.bio}
|
||||
onChange={(e) => setUserSettings(prev => ({ ...prev, bio: e.target.value }))}
|
||||
sx={{
|
||||
'& .MuiOutlinedInput-root': {
|
||||
borderRadius: 2,
|
||||
backgroundColor: actualTheme === 'dark'
|
||||
? 'rgba(255, 255, 255, 0.05)'
|
||||
: 'rgba(0, 0, 0, 0.02)',
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
);
|
||||
|
||||
const renderAppearanceSettings = () => (
|
||||
<motion.div variants={itemVariants}>
|
||||
<Card sx={{
|
||||
backgroundColor: actualTheme === 'dark' ? '#1f2937' : '#ffffff',
|
||||
border: `1px solid ${actualTheme === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'}`,
|
||||
}}>
|
||||
<CardContent sx={{ p: 3 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 3 }}>
|
||||
<Palette sx={{ color: 'primary.main', fontSize: 24 }} />
|
||||
<Typography variant="h6" sx={{ fontWeight: 600 }}>
|
||||
外觀主題設定
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Typography variant="body1" color="text.secondary" sx={{ mb: 3 }}>
|
||||
選擇您喜歡的介面主題,讓工作更舒適
|
||||
</Typography>
|
||||
|
||||
<Grid container spacing={3}>
|
||||
{themeOptions.map((option) => (
|
||||
<Grid item xs={12} sm={4} key={option.value}>
|
||||
<motion.div
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
>
|
||||
<Paper
|
||||
onClick={() => setThemeMode(option.value as 'light' | 'dark' | 'auto')}
|
||||
sx={{
|
||||
p: 3,
|
||||
cursor: 'pointer',
|
||||
textAlign: 'center',
|
||||
backgroundColor: themeMode === option.value
|
||||
? (actualTheme === 'dark' ? 'rgba(59, 130, 246, 0.2)' : 'rgba(59, 130, 246, 0.1)')
|
||||
: (actualTheme === 'dark' ? '#374151' : '#f9fafb'),
|
||||
border: themeMode === option.value
|
||||
? `2px solid ${actualTheme === 'dark' ? '#60a5fa' : '#3b82f6'}`
|
||||
: `1px solid ${actualTheme === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'}`,
|
||||
borderRadius: 3,
|
||||
transition: 'all 0.3s ease',
|
||||
'&:hover': {
|
||||
backgroundColor: actualTheme === 'dark'
|
||||
? 'rgba(59, 130, 246, 0.1)'
|
||||
: 'rgba(59, 130, 246, 0.05)',
|
||||
transform: 'translateY(-2px)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
mb: 2,
|
||||
color: themeMode === option.value ? 'primary.main' : 'text.secondary',
|
||||
'& svg': {
|
||||
fontSize: 40,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{option.icon}
|
||||
</Box>
|
||||
<Typography
|
||||
variant="h6"
|
||||
sx={{
|
||||
fontWeight: 600,
|
||||
color: themeMode === option.value ? 'primary.main' : 'text.primary',
|
||||
mb: 1,
|
||||
}}
|
||||
>
|
||||
{option.label}
|
||||
</Typography>
|
||||
{themeMode === option.value && (
|
||||
<Chip
|
||||
label="已選擇"
|
||||
color="primary"
|
||||
size="small"
|
||||
sx={{ fontWeight: 600 }}
|
||||
/>
|
||||
)}
|
||||
</Paper>
|
||||
</motion.div>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
|
||||
{/* 預覽區域 */}
|
||||
<Box sx={{ mt: 4 }}>
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 600, mb: 2 }}>
|
||||
主題預覽
|
||||
</Typography>
|
||||
<Paper
|
||||
sx={{
|
||||
p: 3,
|
||||
backgroundColor: actualTheme === 'dark' ? '#374151' : '#f8fafc',
|
||||
border: `1px solid ${actualTheme === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'}`,
|
||||
borderRadius: 2,
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6" sx={{ mb: 2, fontWeight: 600 }}>
|
||||
待辦事項預覽
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 2 }}>
|
||||
<Chip label="進行中" color="primary" size="small" />
|
||||
<Chip label="高優先級" color="error" size="small" />
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
2024-01-15 到期
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
這是在 {themeMode === 'light' ? '亮色' : themeMode === 'dark' ? '深色' : '系統'} 模式下的預覽效果
|
||||
</Typography>
|
||||
</Paper>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
);
|
||||
|
||||
const renderNotificationSettings = () => (
|
||||
<motion.div variants={itemVariants}>
|
||||
<Card sx={{
|
||||
backgroundColor: actualTheme === 'dark' ? '#1f2937' : '#ffffff',
|
||||
border: `1px solid ${actualTheme === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'}`,
|
||||
}}>
|
||||
<CardContent sx={{ p: 3 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 3 }}>
|
||||
<Notifications sx={{ color: 'primary.main', fontSize: 24 }} />
|
||||
<Typography variant="h6" sx={{ fontWeight: 600 }}>
|
||||
通知設定
|
||||
<Card sx={{
|
||||
backgroundColor: actualTheme === 'dark' ? '#1f2937' : '#ffffff',
|
||||
border: `1px solid ${actualTheme === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'}`,
|
||||
}}>
|
||||
<CardContent sx={{ p: 3 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 3 }}>
|
||||
<Notifications sx={{ color: 'primary.main', fontSize: 24 }} />
|
||||
<Typography variant="h6" sx={{ fontWeight: 600 }}>
|
||||
郵件通知設定
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12}>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 2, color: 'text.secondary' }}>
|
||||
通知方式
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12}>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 2, color: 'text.secondary' }}>
|
||||
通知方式
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={notificationSettings.emailNotifications}
|
||||
onChange={(e) => setNotificationSettings(prev => ({ ...prev, emailNotifications: e.target.checked }))}
|
||||
color="primary"
|
||||
/>
|
||||
}
|
||||
label={
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Email sx={{ fontSize: 18, color: 'text.secondary' }} />
|
||||
電子信箱通知
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={notificationSettings.pushNotifications}
|
||||
onChange={(e) => setNotificationSettings(prev => ({ ...prev, pushNotifications: e.target.checked }))}
|
||||
color="primary"
|
||||
/>
|
||||
}
|
||||
label={
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Notifications sx={{ fontSize: 18, color: 'text.secondary' }} />
|
||||
推送通知
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={notificationSettings.smsNotifications}
|
||||
onChange={(e) => setNotificationSettings(prev => ({ ...prev, smsNotifications: e.target.checked }))}
|
||||
color="primary"
|
||||
/>
|
||||
}
|
||||
label={
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Sms sx={{ fontSize: 18, color: 'text.secondary' }} />
|
||||
簡訊通知
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12}>
|
||||
<Divider sx={{ my: 2 }} />
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 2, color: 'text.secondary' }}>
|
||||
通知內容
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={notificationSettings.todoReminders}
|
||||
onChange={(e) => setNotificationSettings(prev => ({ ...prev, todoReminders: e.target.checked }))}
|
||||
color="primary"
|
||||
/>
|
||||
}
|
||||
label="待辦事項提醒"
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={notificationSettings.deadlineAlerts}
|
||||
onChange={(e) => setNotificationSettings(prev => ({ ...prev, deadlineAlerts: e.target.checked }))}
|
||||
color="primary"
|
||||
/>
|
||||
}
|
||||
label="截止日期警告"
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={notificationSettings.weeklyReports}
|
||||
onChange={(e) => setNotificationSettings(prev => ({ ...prev, weeklyReports: e.target.checked }))}
|
||||
color="primary"
|
||||
/>
|
||||
}
|
||||
label="每週報告"
|
||||
/>
|
||||
</Box>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12}>
|
||||
<Divider sx={{ my: 2 }} />
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 2, color: 'text.secondary' }}>
|
||||
聲音設定
|
||||
</Typography>
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={notificationSettings.soundEnabled}
|
||||
onChange={(e) => setNotificationSettings(prev => ({ ...prev, soundEnabled: e.target.checked }))}
|
||||
color="primary"
|
||||
/>
|
||||
}
|
||||
label={
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<VolumeUp sx={{ fontSize: 18, color: 'text.secondary' }} />
|
||||
啟用通知聲音
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
{notificationSettings.soundEnabled && (
|
||||
<Box sx={{ px: 2, mb: 2 }}>
|
||||
<Typography variant="caption" color="text.secondary" gutterBottom>
|
||||
音量大小: {notificationSettings.soundVolume}%
|
||||
</Typography>
|
||||
<Slider
|
||||
value={notificationSettings.soundVolume}
|
||||
onChange={(_, value) => setNotificationSettings(prev => ({ ...prev, soundVolume: value as number }))}
|
||||
min={0}
|
||||
max={100}
|
||||
step={10}
|
||||
marks
|
||||
sx={{ color: 'primary.main' }}
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={notificationSettings.emailNotifications}
|
||||
onChange={(e) => setNotificationSettings(prev => ({ ...prev, emailNotifications: e.target.checked }))}
|
||||
color="primary"
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Grid>
|
||||
}
|
||||
label={
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Email sx={{ fontSize: 18, color: 'text.secondary' }} />
|
||||
電子信箱通知
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
</Grid>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
|
||||
<Grid item xs={12}>
|
||||
<Divider sx={{ my: 2 }} />
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 2, color: 'text.secondary' }}>
|
||||
通知內容
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={notificationSettings.todoReminders}
|
||||
onChange={(e) => setNotificationSettings(prev => ({ ...prev, todoReminders: e.target.checked }))}
|
||||
color="primary"
|
||||
/>
|
||||
}
|
||||
label="待辦事項提醒"
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={notificationSettings.deadlineAlerts}
|
||||
onChange={(e) => setNotificationSettings(prev => ({ ...prev, deadlineAlerts: e.target.checked }))}
|
||||
color="primary"
|
||||
/>
|
||||
}
|
||||
label="截止日期警告"
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={notificationSettings.weeklyReports}
|
||||
onChange={(e) => setNotificationSettings(prev => ({ ...prev, weeklyReports: e.target.checked }))}
|
||||
color="primary"
|
||||
/>
|
||||
}
|
||||
label="每週報告"
|
||||
/>
|
||||
</Box>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12}>
|
||||
<Divider sx={{ my: 2 }} />
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 2, color: 'text.secondary' }}>
|
||||
聲音設定
|
||||
</Typography>
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={notificationSettings.soundEnabled}
|
||||
onChange={(e) => setNotificationSettings(prev => ({ ...prev, soundEnabled: e.target.checked }))}
|
||||
color="primary"
|
||||
/>
|
||||
}
|
||||
label={
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<VolumeUp sx={{ fontSize: 18, color: 'text.secondary' }} />
|
||||
啟用通知聲音
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
{notificationSettings.soundEnabled && (
|
||||
<Box sx={{ px: 2, mb: 2 }}>
|
||||
<Typography variant="caption" color="text.secondary" gutterBottom>
|
||||
音量大小: {notificationSettings.soundVolume}%
|
||||
</Typography>
|
||||
<Slider
|
||||
value={notificationSettings.soundVolume}
|
||||
onChange={(_, value) => setNotificationSettings(prev => ({ ...prev, soundVolume: value as number }))}
|
||||
min={0}
|
||||
max={100}
|
||||
step={10}
|
||||
marks
|
||||
sx={{ color: 'primary.main' }}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
||||
return (
|
||||
<DashboardLayout>
|
||||
<motion.div
|
||||
variants={containerVariants}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
{/* 標題區域 */}
|
||||
<Box sx={{ mb: 3 }}>
|
||||
<motion.div variants={itemVariants}>
|
||||
<Typography
|
||||
variant="h4"
|
||||
sx={{
|
||||
fontWeight: 700,
|
||||
mb: 0.5,
|
||||
background: actualTheme === 'dark'
|
||||
? 'linear-gradient(45deg, #f3f4f6 30%, #d1d5db 90%)'
|
||||
: 'linear-gradient(45deg, #111827 30%, #374151 90%)',
|
||||
WebkitBackgroundClip: 'text',
|
||||
WebkitTextFillColor: 'transparent',
|
||||
backgroundClip: 'text',
|
||||
}}
|
||||
>
|
||||
設定
|
||||
</Typography>
|
||||
<Typography variant="body1" color="text.secondary">
|
||||
管理您的個人資料和應用程式偏好設定
|
||||
</Typography>
|
||||
</motion.div>
|
||||
<Typography
|
||||
variant="h4"
|
||||
sx={{
|
||||
fontWeight: 700,
|
||||
mb: 0.5,
|
||||
background: actualTheme === 'dark'
|
||||
? 'linear-gradient(45deg, #f3f4f6 30%, #d1d5db 90%)'
|
||||
: 'linear-gradient(45deg, #111827 30%, #374151 90%)',
|
||||
WebkitBackgroundClip: 'text',
|
||||
WebkitTextFillColor: 'transparent',
|
||||
backgroundClip: 'text',
|
||||
}}
|
||||
>
|
||||
郵件通知設定
|
||||
</Typography>
|
||||
<Typography variant="body1" color="text.secondary">
|
||||
管理您的郵件通知偏好設定
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Grid container spacing={3}>
|
||||
{/* 側邊欄 */}
|
||||
<Grid item xs={12} md={3}>
|
||||
<motion.div variants={itemVariants}>
|
||||
<Card sx={{
|
||||
backgroundColor: actualTheme === 'dark' ? '#1f2937' : '#ffffff',
|
||||
border: `1px solid ${actualTheme === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'}`,
|
||||
}}>
|
||||
<Box sx={{ p: 2 }}>
|
||||
{tabs.map((tab) => (
|
||||
<motion.div
|
||||
key={tab.id}
|
||||
whileHover={{ x: 4 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
>
|
||||
<Button
|
||||
fullWidth
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
startIcon={tab.icon}
|
||||
sx={{
|
||||
justifyContent: 'flex-start',
|
||||
textTransform: 'none',
|
||||
fontWeight: activeTab === tab.id ? 600 : 400,
|
||||
color: activeTab === tab.id ? 'primary.main' : 'text.primary',
|
||||
backgroundColor: activeTab === tab.id
|
||||
? (actualTheme === 'dark' ? 'rgba(59, 130, 246, 0.1)' : 'rgba(59, 130, 246, 0.1)')
|
||||
: 'transparent',
|
||||
borderRadius: 2,
|
||||
mb: 1,
|
||||
py: 1.5,
|
||||
px: 2,
|
||||
'&:hover': {
|
||||
backgroundColor: actualTheme === 'dark'
|
||||
? 'rgba(59, 130, 246, 0.1)'
|
||||
: 'rgba(59, 130, 246, 0.05)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{tab.label}
|
||||
</Button>
|
||||
</motion.div>
|
||||
))}
|
||||
</Box>
|
||||
</Card>
|
||||
</motion.div>
|
||||
</Grid>
|
||||
{/* 設定內容 */}
|
||||
{renderNotificationSettings()}
|
||||
|
||||
{/* 主要內容 */}
|
||||
<Grid item xs={12} md={9}>
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key={activeTab}
|
||||
initial={{ opacity: 0, x: 20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: -20 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
{activeTab === 'profile' && renderProfileSettings()}
|
||||
{activeTab === 'appearance' && renderAppearanceSettings()}
|
||||
{activeTab === 'notifications' && renderNotificationSettings()}
|
||||
{/* 其他 tab 內容可以在這裡添加 */}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
|
||||
{/* 儲存按鈕 */}
|
||||
<motion.div variants={itemVariants} style={{ marginTop: 24 }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'flex-end', gap: 2 }}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={<Refresh />}
|
||||
sx={{
|
||||
borderRadius: 2,
|
||||
textTransform: 'none',
|
||||
fontWeight: 600,
|
||||
px: 3,
|
||||
}}
|
||||
>
|
||||
重置
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<Save />}
|
||||
onClick={handleSave}
|
||||
sx={{
|
||||
background: 'linear-gradient(45deg, #3b82f6 30%, #8b5cf6 90%)',
|
||||
borderRadius: 2,
|
||||
textTransform: 'none',
|
||||
fontWeight: 600,
|
||||
px: 3,
|
||||
'&:hover': {
|
||||
background: 'linear-gradient(45deg, #2563eb 30%, #7c3aed 90%)',
|
||||
boxShadow: '0 4px 12px rgba(59, 130, 246, 0.3)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
儲存變更
|
||||
</Button>
|
||||
</Box>
|
||||
</motion.div>
|
||||
</Grid>
|
||||
</Grid>
|
||||
{/* 儲存按鈕 */}
|
||||
<Box sx={{ display: 'flex', justifyContent: 'flex-end', gap: 2, mt: 3 }}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={<Refresh />}
|
||||
sx={{
|
||||
borderRadius: 2,
|
||||
textTransform: 'none',
|
||||
fontWeight: 600,
|
||||
px: 3,
|
||||
}}
|
||||
>
|
||||
重置
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<Save />}
|
||||
onClick={handleSave}
|
||||
sx={{
|
||||
background: 'linear-gradient(45deg, #3b82f6 30%, #8b5cf6 90%)',
|
||||
borderRadius: 2,
|
||||
textTransform: 'none',
|
||||
fontWeight: 600,
|
||||
px: 3,
|
||||
'&:hover': {
|
||||
background: 'linear-gradient(45deg, #2563eb 30%, #7c3aed 90%)',
|
||||
boxShadow: '0 4px 12px rgba(59, 130, 246, 0.3)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
儲存變更
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
{/* 成功通知 */}
|
||||
<Snackbar
|
||||
@@ -753,7 +253,7 @@ const SettingsPage = () => {
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
設定已成功儲存!
|
||||
郵件通知設定已成功儲存!
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
</motion.div>
|
||||
|
@@ -51,22 +51,22 @@ const DashboardLayout: React.FC<DashboardLayoutProps> = ({ children }) => {
|
||||
const { user, logout } = useAuth();
|
||||
const { themeMode, actualTheme, setThemeMode } = useTheme();
|
||||
const muiTheme = useMuiTheme();
|
||||
const isMobile = useMediaQuery('(max-width: 1200px)'); // 降低斷點確保覆蓋所有小螢幕
|
||||
const isMobile = useMediaQuery('(max-width: 768px)'); // 調整為平板以下尺寸才隱藏側邊欄
|
||||
const isTablet = useMediaQuery('(max-width: 1024px) and (min-width: 769px)'); // 平板尺寸自動收合
|
||||
|
||||
// 響應式處理
|
||||
useEffect(() => {
|
||||
console.log('Responsive handling:', {
|
||||
isMobile,
|
||||
windowWidth: window.innerWidth,
|
||||
currentSidebarOpen: sidebarOpen
|
||||
});
|
||||
if (isMobile) {
|
||||
setSidebarOpen(false);
|
||||
setSidebarCollapsed(false);
|
||||
} else if (isTablet) {
|
||||
setSidebarOpen(true);
|
||||
setSidebarCollapsed(true); // 平板尺寸自動收合側邊欄
|
||||
} else {
|
||||
setSidebarOpen(true);
|
||||
setSidebarCollapsed(false); // 桌面尺寸完全展開
|
||||
}
|
||||
}, [isMobile]);
|
||||
}, [isMobile, isTablet]);
|
||||
|
||||
// 保持 sidebar 狀態穩定
|
||||
useEffect(() => {
|
||||
@@ -142,15 +142,6 @@ const DashboardLayout: React.FC<DashboardLayoutProps> = ({ children }) => {
|
||||
|
||||
|
||||
const toggleSidebar = (event?: React.MouseEvent) => {
|
||||
console.log('🔧 Toggle sidebar clicked:', {
|
||||
isMobile,
|
||||
sidebarOpen,
|
||||
sidebarCollapsed,
|
||||
windowWidth: window.innerWidth,
|
||||
eventTarget: event?.target,
|
||||
eventCurrentTarget: event?.currentTarget
|
||||
});
|
||||
|
||||
// 防止事件冒泡
|
||||
if (event) {
|
||||
event.preventDefault();
|
||||
@@ -158,10 +149,8 @@ const DashboardLayout: React.FC<DashboardLayoutProps> = ({ children }) => {
|
||||
}
|
||||
|
||||
if (isMobile) {
|
||||
console.log('📱 Mobile: Setting sidebar open to:', !sidebarOpen);
|
||||
setSidebarOpen(!sidebarOpen);
|
||||
} else {
|
||||
console.log('🖥️ Desktop: Toggling collapsed to:', !sidebarCollapsed);
|
||||
setSidebarCollapsed(!sidebarCollapsed);
|
||||
}
|
||||
};
|
||||
|
@@ -35,10 +35,17 @@ import dayjs, { Dayjs } from 'dayjs';
|
||||
import isoWeek from 'dayjs/plugin/isoWeek';
|
||||
import isSameOrAfter from 'dayjs/plugin/isSameOrAfter';
|
||||
import isSameOrBefore from 'dayjs/plugin/isSameOrBefore';
|
||||
import weekday from 'dayjs/plugin/weekday';
|
||||
import localeData from 'dayjs/plugin/localeData';
|
||||
|
||||
dayjs.extend(isoWeek);
|
||||
dayjs.extend(isSameOrAfter);
|
||||
dayjs.extend(isSameOrBefore);
|
||||
dayjs.extend(weekday);
|
||||
dayjs.extend(localeData);
|
||||
|
||||
// 設定週的開始為週日
|
||||
dayjs.Ls.en.weekStart = 0;
|
||||
|
||||
interface CalendarViewProps {
|
||||
todos: Todo[];
|
||||
@@ -85,8 +92,16 @@ const CalendarView: React.FC<CalendarViewProps> = ({
|
||||
case 'month': {
|
||||
const startOfMonth = currentDate.startOf('month');
|
||||
const endOfMonth = currentDate.endOf('month');
|
||||
const startOfWeek = startOfMonth.startOf('week');
|
||||
const endOfWeek = endOfMonth.endOf('week');
|
||||
|
||||
// 獲取月份第一天是星期幾 (0=週日, 1=週一, ..., 6=週六)
|
||||
const firstDayWeekday = startOfMonth.day();
|
||||
// 獲取月份最後一天是星期幾
|
||||
const lastDayWeekday = endOfMonth.day();
|
||||
|
||||
// 計算需要顯示的第一天(從包含本月第一天的那週的週日開始)
|
||||
const startOfWeek = startOfMonth.subtract(firstDayWeekday, 'day');
|
||||
// 計算需要顯示的最後一天(到包含本月最後一天的那週的週六結束)
|
||||
const endOfWeek = endOfMonth.add(6 - lastDayWeekday, 'day');
|
||||
|
||||
const dates = [];
|
||||
let current = startOfWeek;
|
||||
@@ -211,65 +226,95 @@ const CalendarView: React.FC<CalendarViewProps> = ({
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
>
|
||||
<Grid container spacing={1}>
|
||||
{/* 星期標題 */}
|
||||
{['日', '一', '二', '三', '四', '五', '六'].map((day, index) => (
|
||||
<Grid item xs key={index}>
|
||||
<Paper
|
||||
<Box
|
||||
sx={{
|
||||
backgroundColor: actualTheme === 'dark' ? '#1f2937' : '#ffffff',
|
||||
border: `1px solid ${actualTheme === 'dark'
|
||||
? 'rgba(255, 255, 255, 0.1)'
|
||||
: 'rgba(0, 0, 0, 0.1)'}`,
|
||||
borderRadius: 2,
|
||||
overflow: 'hidden'
|
||||
}}
|
||||
>
|
||||
{/* 星期標題行 */}
|
||||
<Box sx={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(7, 1fr)',
|
||||
backgroundColor: actualTheme === 'dark' ? '#374151' : '#f3f4f6',
|
||||
borderBottom: `1px solid ${actualTheme === 'dark'
|
||||
? 'rgba(255, 255, 255, 0.1)'
|
||||
: 'rgba(0, 0, 0, 0.1)'}`,
|
||||
}}>
|
||||
{['日', '一', '二', '三', '四', '五', '六'].map((day, index) => (
|
||||
<Box
|
||||
key={index}
|
||||
sx={{
|
||||
p: 1,
|
||||
p: 2,
|
||||
textAlign: 'center',
|
||||
backgroundColor: actualTheme === 'dark' ? '#374151' : '#f3f4f6',
|
||||
border: 'none',
|
||||
borderRight: index < 6 ? `1px solid ${actualTheme === 'dark'
|
||||
? 'rgba(255, 255, 255, 0.1)'
|
||||
: 'rgba(0, 0, 0, 0.1)'}` : 'none',
|
||||
}}
|
||||
>
|
||||
<Typography variant="caption" sx={{ fontWeight: 600, color: 'text.secondary' }}>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600, color: 'text.secondary' }}>
|
||||
{day}
|
||||
</Typography>
|
||||
</Paper>
|
||||
</Grid>
|
||||
))}
|
||||
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
|
||||
{/* 日期網格 */}
|
||||
{weeks.map((week, weekIndex) =>
|
||||
week.map((date, dayIndex) => {
|
||||
const todosForDate = getTodosForDate(date);
|
||||
const isCurrentMonth = date.month() === currentDate.month();
|
||||
const isToday = date.isSame(dayjs(), 'day');
|
||||
|
||||
return (
|
||||
<Grid item xs key={`${weekIndex}-${dayIndex}`}>
|
||||
<motion.div variants={itemVariants}>
|
||||
<Card
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
|
||||
{weeks.map((week, weekIndex) => (
|
||||
<Box
|
||||
key={weekIndex}
|
||||
sx={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(7, 1fr)',
|
||||
borderBottom: weekIndex < weeks.length - 1 ? `1px solid ${actualTheme === 'dark'
|
||||
? 'rgba(255, 255, 255, 0.1)'
|
||||
: 'rgba(0, 0, 0, 0.1)'}` : 'none',
|
||||
}}
|
||||
>
|
||||
{week.map((date, dayIndex) => {
|
||||
const todosForDate = getTodosForDate(date);
|
||||
const isCurrentMonth = date.month() === currentDate.month();
|
||||
const isToday = date.isSame(dayjs(), 'day');
|
||||
|
||||
return (
|
||||
<Box
|
||||
key={`${weekIndex}-${dayIndex}`}
|
||||
sx={{
|
||||
minHeight: 120,
|
||||
p: 1,
|
||||
backgroundColor: actualTheme === 'dark'
|
||||
? (isCurrentMonth ? '#1f2937' : '#111827')
|
||||
: (isCurrentMonth ? '#ffffff' : '#f9fafb'),
|
||||
border: isToday
|
||||
? `2px solid ${actualTheme === 'dark' ? '#60a5fa' : '#3b82f6'}`
|
||||
: `1px solid ${actualTheme === 'dark'
|
||||
? 'rgba(255, 255, 255, 0.1)'
|
||||
: 'rgba(0, 0, 0, 0.1)'}`,
|
||||
p: 1.5,
|
||||
borderRight: dayIndex < 6 ? `1px solid ${actualTheme === 'dark'
|
||||
? 'rgba(255, 255, 255, 0.1)'
|
||||
: 'rgba(0, 0, 0, 0.1)'}` : 'none',
|
||||
backgroundColor: isToday
|
||||
? (actualTheme === 'dark' ? 'rgba(59, 130, 246, 0.1)' : 'rgba(59, 130, 246, 0.05)')
|
||||
: (isCurrentMonth
|
||||
? 'transparent'
|
||||
: (actualTheme === 'dark' ? 'rgba(0, 0, 0, 0.2)' : 'rgba(0, 0, 0, 0.02)')),
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s ease',
|
||||
position: 'relative',
|
||||
'&:hover': {
|
||||
backgroundColor: actualTheme === 'dark'
|
||||
? 'rgba(59, 130, 246, 0.1)'
|
||||
: 'rgba(59, 130, 246, 0.05)',
|
||||
transform: 'translateY(-1px)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 0.5 }}>
|
||||
{/* 日期數字和徽章 */}
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 1 }}>
|
||||
<Typography
|
||||
variant="caption"
|
||||
variant="body2"
|
||||
sx={{
|
||||
fontWeight: isToday ? 700 : 500,
|
||||
fontWeight: isToday ? 700 : (isCurrentMonth ? 500 : 400),
|
||||
color: isCurrentMonth
|
||||
? (isToday ? 'primary.main' : 'text.primary')
|
||||
: 'text.disabled',
|
||||
fontSize: '0.875rem',
|
||||
}}
|
||||
>
|
||||
{date.date()}
|
||||
@@ -280,23 +325,24 @@ const CalendarView: React.FC<CalendarViewProps> = ({
|
||||
color="primary"
|
||||
sx={{
|
||||
'& .MuiBadge-badge': {
|
||||
fontSize: '0.6rem',
|
||||
minWidth: 16,
|
||||
height: 16,
|
||||
fontSize: '0.7rem',
|
||||
minWidth: 18,
|
||||
height: 18,
|
||||
right: -6,
|
||||
top: -6,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Circle sx={{ fontSize: 8, color: 'primary.main' }} />
|
||||
</Badge>
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* 待辦事項列表 */}
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
|
||||
{todosForDate.slice(0, 3).map((todo) => (
|
||||
{todosForDate.slice(0, 2).map((todo) => (
|
||||
<motion.div
|
||||
key={todo.id}
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
whileHover={{ scale: 1.01 }}
|
||||
whileTap={{ scale: 0.99 }}
|
||||
>
|
||||
<Box
|
||||
onClick={(e) => handleTodoClick(todo, e)}
|
||||
@@ -306,12 +352,11 @@ const CalendarView: React.FC<CalendarViewProps> = ({
|
||||
backgroundColor: selectedTodos.includes(todo.id)
|
||||
? 'rgba(59, 130, 246, 0.2)'
|
||||
: `${getPriorityColor(todo.priority)}15`,
|
||||
borderLeft: `3px solid ${getPriorityColor(todo.priority)}`,
|
||||
borderLeft: `2px solid ${getPriorityColor(todo.priority)}`,
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s ease',
|
||||
'&:hover': {
|
||||
backgroundColor: `${getPriorityColor(todo.priority)}25`,
|
||||
transform: 'translateX(2px)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
@@ -320,62 +365,42 @@ const CalendarView: React.FC<CalendarViewProps> = ({
|
||||
sx={{
|
||||
display: 'block',
|
||||
fontWeight: 600,
|
||||
fontSize: '0.65rem',
|
||||
fontSize: '0.7rem',
|
||||
color: 'text.primary',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
lineHeight: 1.2,
|
||||
}}
|
||||
>
|
||||
{todo.starred && <Star sx={{ fontSize: 10, color: '#fbbf24', mr: 0.25 }} />}
|
||||
{todo.title}
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.25, mt: 0.25 }}>
|
||||
<Circle
|
||||
sx={{
|
||||
fontSize: 6,
|
||||
color: getStatusColor(todo.status),
|
||||
}}
|
||||
/>
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{
|
||||
fontSize: '0.6rem',
|
||||
color: 'text.secondary',
|
||||
}}
|
||||
>
|
||||
{(() => {
|
||||
const firstUser = todo.responsible_users_details?.[0] ||
|
||||
(todo.responsible_users?.[0] ? {ad_account: todo.responsible_users[0], display_name: todo.responsible_users[0]} : null);
|
||||
return firstUser ? (firstUser.display_name || firstUser.ad_account) : '未指派';
|
||||
})()}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</motion.div>
|
||||
))}
|
||||
|
||||
{todosForDate.length > 3 && (
|
||||
{todosForDate.length > 2 && (
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{
|
||||
fontSize: '0.6rem',
|
||||
fontSize: '0.65rem',
|
||||
color: 'text.secondary',
|
||||
textAlign: 'center',
|
||||
mt: 0.5,
|
||||
fontStyle: 'italic',
|
||||
}}
|
||||
>
|
||||
+{todosForDate.length - 3} 更多
|
||||
+{todosForDate.length - 2} 項
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</Card>
|
||||
</motion.div>
|
||||
</Grid>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</Grid>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
@@ -288,7 +288,7 @@ const ExcelImport: React.FC<ExcelImportProps> = ({ open, onClose, onImportComple
|
||||
<TableCell>優先級</TableCell>
|
||||
<TableCell>到期日</TableCell>
|
||||
<TableCell>負責人</TableCell>
|
||||
<TableCell>追蹤人</TableCell>
|
||||
<TableCell>公開設定</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
@@ -332,12 +332,12 @@ const ExcelImport: React.FC<ExcelImportProps> = ({ open, onClose, onImportComple
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2">
|
||||
{todo.responsible_users.join(', ') || '-'}
|
||||
{(todo.responsible_users && todo.responsible_users.length > 0) ? todo.responsible_users.join(', ') : '-'}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2">
|
||||
{todo.followers.join(', ') || '-'}
|
||||
{todo.is_public ? '是' : '否'}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
@@ -66,7 +66,6 @@ interface LocalTodo {
|
||||
starred: boolean;
|
||||
creator?: User;
|
||||
responsible: User[];
|
||||
tags: string[];
|
||||
isPublic: boolean;
|
||||
}
|
||||
|
||||
@@ -101,11 +100,9 @@ const TodoDialog: React.FC<TodoDialogProps> = ({
|
||||
dueDate: null,
|
||||
starred: false,
|
||||
responsible: [],
|
||||
tags: [],
|
||||
isPublic: true,
|
||||
isPublic: false, // 預設為非公開
|
||||
});
|
||||
|
||||
const [tagInput, setTagInput] = useState('');
|
||||
const [assignToMyself, setAssignToMyself] = useState(false);
|
||||
|
||||
// 用戶資料
|
||||
@@ -178,8 +175,7 @@ const TodoDialog: React.FC<TodoDialogProps> = ({
|
||||
avatar: adAccount.charAt(0).toUpperCase(),
|
||||
department: '員工'
|
||||
})),
|
||||
tags: apiTodo.tags || [],
|
||||
isPublic: true, // 預設值
|
||||
isPublic: false, // 預設值
|
||||
};
|
||||
setFormData(editTodo);
|
||||
} else {
|
||||
@@ -191,8 +187,7 @@ const TodoDialog: React.FC<TodoDialogProps> = ({
|
||||
dueDate: null,
|
||||
starred: false,
|
||||
responsible: [],
|
||||
tags: [],
|
||||
isPublic: true,
|
||||
isPublic: false,
|
||||
});
|
||||
}
|
||||
setAssignToMyself(false);
|
||||
@@ -215,20 +210,6 @@ const TodoDialog: React.FC<TodoDialogProps> = ({
|
||||
}));
|
||||
};
|
||||
|
||||
const handleAddTag = (event: React.KeyboardEvent) => {
|
||||
if (event.key === 'Enter' && tagInput.trim()) {
|
||||
event.preventDefault();
|
||||
const newTag = tagInput.trim();
|
||||
if (!(formData.tags || []).includes(newTag)) {
|
||||
handleInputChange('tags', [...(formData.tags || []), newTag]);
|
||||
}
|
||||
setTagInput('');
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveTag = (tagToRemove: string) => {
|
||||
handleInputChange('tags', (formData.tags || []).filter(tag => tag !== tagToRemove));
|
||||
};
|
||||
|
||||
const validateForm = (): boolean => {
|
||||
if (!formData.title.trim()) {
|
||||
@@ -265,7 +246,6 @@ const TodoDialog: React.FC<TodoDialogProps> = ({
|
||||
responsible_users: responsibleUsers,
|
||||
starred: formData.starred,
|
||||
is_public: formData.isPublic,
|
||||
tags: formData.tags
|
||||
};
|
||||
|
||||
let savedTodo;
|
||||
@@ -653,62 +633,14 @@ const TodoDialog: React.FC<TodoDialogProps> = ({
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
{/* 標籤和設定 */}
|
||||
{/* 設定 */}
|
||||
<Grid item xs={12}>
|
||||
<Divider sx={{ my: 2 }} />
|
||||
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2 }}>
|
||||
標籤和設定
|
||||
設定
|
||||
</Typography>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="新增標籤"
|
||||
placeholder="輸入標籤並按 Enter..."
|
||||
value={tagInput}
|
||||
onChange={(e) => setTagInput(e.target.value)}
|
||||
onKeyDown={handleAddTag}
|
||||
sx={{
|
||||
'& .MuiOutlinedInput-root': {
|
||||
borderRadius: 2,
|
||||
backgroundColor: actualTheme === 'dark'
|
||||
? 'rgba(255, 255, 255, 0.05)'
|
||||
: 'rgba(0, 0, 0, 0.02)',
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1, mt: 2 }}>
|
||||
{(formData.tags || []).map((tag, index) => (
|
||||
<motion.div
|
||||
key={tag}
|
||||
initial={{ opacity: 0, scale: 0.8 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ delay: index * 0.1 }}
|
||||
>
|
||||
<Chip
|
||||
label={tag}
|
||||
onDelete={() => handleRemoveTag(tag)}
|
||||
deleteIcon={<Delete sx={{ fontSize: 16 }} />}
|
||||
sx={{
|
||||
borderRadius: 2,
|
||||
backgroundColor: actualTheme === 'dark'
|
||||
? 'rgba(139, 92, 246, 0.2)'
|
||||
: 'rgba(139, 92, 246, 0.1)',
|
||||
color: '#8b5cf6',
|
||||
'&:hover': {
|
||||
backgroundColor: actualTheme === 'dark'
|
||||
? 'rgba(139, 92, 246, 0.3)'
|
||||
: 'rgba(139, 92, 246, 0.15)',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</motion.div>
|
||||
))}
|
||||
</Box>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
|
@@ -20,7 +20,6 @@ export interface Todo {
|
||||
creator_email?: string;
|
||||
starred: boolean;
|
||||
is_public: boolean;
|
||||
tags: string[];
|
||||
responsible_users: string[];
|
||||
followers: string[];
|
||||
responsible_users_details?: UserDetail[];
|
||||
@@ -35,7 +34,6 @@ export interface TodoCreate {
|
||||
due_date?: string;
|
||||
starred?: boolean;
|
||||
is_public?: boolean;
|
||||
tags?: string[];
|
||||
responsible_users?: string[];
|
||||
followers?: string[];
|
||||
}
|
||||
@@ -52,7 +50,6 @@ export interface TodoFilter {
|
||||
due_to?: string;
|
||||
search?: string;
|
||||
view?: 'all' | 'created' | 'responsible' | 'following' | 'public';
|
||||
tags?: string[];
|
||||
}
|
||||
|
||||
// User Types
|
||||
|
Reference in New Issue
Block a user