This commit is contained in:
beabigegg
2025-09-01 16:42:41 +08:00
parent 22a231d78c
commit 00061adeb7
23 changed files with 858 additions and 3584 deletions

View File

@@ -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>

View File

@@ -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);
}
};

View File

@@ -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>
);
};

View File

@@ -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>

View File

@@ -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={

View File

@@ -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