baa424c841
- Add used-car pages: list, detail, create, edit, mine - Add CarCard and PriceModal components - Fix API response interceptor to accept both code 200 and 0 - Fix detail page: was incorrectly using edit page template - Fix edit page: remove res.code check after API call - Add image URL processing (localhost → localhost:8787) - Add i18n translations for used-car module in 5 languages Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
245 lines
8.9 KiB
TypeScript
245 lines
8.9 KiB
TypeScript
import { useNavigate } from 'react-router-dom'
|
||
import { useUserStore } from '@/stores/user'
|
||
import { useTranslation } from 'react-i18next'
|
||
import { Toast } from 'antd-mobile'
|
||
import { showLanguageSwitcher } from '@/components/LanguageSwitcher'
|
||
import { useEffect, useState } from 'react'
|
||
import styles from './dashboard.module.css'
|
||
|
||
interface QuickAction {
|
||
id: string
|
||
icon: string
|
||
label: string
|
||
path: string
|
||
color: string
|
||
description?: string
|
||
disabled?: boolean // 新增:标记是否禁用(开发中)
|
||
}
|
||
|
||
interface TodoItem {
|
||
id: number
|
||
title: string
|
||
type: string
|
||
time: string
|
||
}
|
||
|
||
interface ActivityItem {
|
||
id: number
|
||
content: string
|
||
time: string
|
||
type: 'approval' | 'submit' | 'other'
|
||
}
|
||
|
||
// 从应用页面迁移的完整应用列表
|
||
const quickActions: QuickAction[] = [
|
||
{ id: '1', icon: '✈️', label: '出差申请', path: '/business-trip/apply', color: '#1677ff', description: '提交出差申请' },
|
||
{ id: '2', icon: '📝', label: '采购申请', path: '/purchase/apply', color: '#52c41a', description: '物品采购审批', disabled: true },
|
||
{ id: '3', icon: '📦', label: '备货录入', path: '/stock/entry', color: '#faad14', description: '库存备货录入', disabled: true },
|
||
{ id: '4', icon: '🚗', label: '发布车辆', path: '/used-car/create', color: '#722ed1', description: '发布二手车' },
|
||
{ id: '5', icon: '📋', label: '我的发布', path: '/used-car/my-list', color: '#722ed1', description: '我发布的车辆' },
|
||
]
|
||
|
||
const allApps: QuickAction[] = [
|
||
{ id: '1', icon: '✍️', label: '审批', path: '/approval', color: '#52c41a', description: '待办事项审批' },
|
||
{ id: '2', icon: '📊', label: '数据统计', path: '/reports', color: '#1890ff', description: '业务数据统计', disabled: true },
|
||
{ id: '3', icon: '⚙️', label: '系统设置', path: '/settings', color: '#8c8c8c', description: '系统偏好设置' },
|
||
{ id: '4', icon: '🚗', label: '二手车', path: '/used-car', color: '#722ed1', description: '二手车管理' },
|
||
]
|
||
|
||
export default function Dashboard() {
|
||
const navigate = useNavigate()
|
||
const { user } = useUserStore()
|
||
const { t } = useTranslation()
|
||
const [todos, setTodos] = useState<TodoItem[]>([])
|
||
const [activities, setActivities] = useState<ActivityItem[]>([])
|
||
const [pendingCount, setPendingCount] = useState(0)
|
||
|
||
// 获取待办事项
|
||
useEffect(() => {
|
||
const fetchTodos = async () => {
|
||
try {
|
||
const response = await fetch('/api/v1/approval/list?type=pending&page=1&page_size=5')
|
||
const result = await response.json()
|
||
if (result.code === 0 && result.data?.list) {
|
||
const list = result.data.list.map((item: any) => ({
|
||
id: item.id,
|
||
title: item.title,
|
||
type: item.type || 'business_trip',
|
||
time: item.created_at
|
||
}))
|
||
setTodos(list)
|
||
setPendingCount(result.data.total || 0)
|
||
}
|
||
} catch (error) {
|
||
console.error('获取待办事项失败:', error)
|
||
}
|
||
}
|
||
|
||
const fetchActivities = async () => {
|
||
try {
|
||
// 获取我发起的和已审批的
|
||
const response = await fetch('/api/v1/approval/list?type=approved&page=1&page_size=5')
|
||
const result = await response.json()
|
||
if (result.code === 0 && result.data?.list) {
|
||
const list = result.data.list.map((item: any) => ({
|
||
id: item.id,
|
||
content: `${item.applicant_name || '您'} ${item.status === 'approved' ? '通过了' : '拒绝了'} ${item.title}`,
|
||
time: item.updated_at,
|
||
type: 'approval' as const
|
||
}))
|
||
setActivities(list)
|
||
}
|
||
} catch (error) {
|
||
console.error('获取动态失败:', error)
|
||
}
|
||
}
|
||
|
||
fetchTodos()
|
||
fetchActivities()
|
||
}, [])
|
||
|
||
const handleQuickAction = (action: QuickAction) => {
|
||
if (action.disabled) {
|
||
Toast.show('系统开发中')
|
||
return
|
||
}
|
||
navigate(action.path)
|
||
}
|
||
|
||
const getCurrentLanguage = () => {
|
||
const lang = localStorage.getItem('i18nextLng') || 'zh'
|
||
const langMap: Record<string, string> = {
|
||
zh: '🇨🇳',
|
||
en: '🇬🇧',
|
||
ru: '🇷🇺',
|
||
es: '🇪🇸',
|
||
fr: '🇫🇷',
|
||
}
|
||
return langMap[lang] || '🇨🇳'
|
||
}
|
||
|
||
const formatTime = (time: string) => {
|
||
const date = new Date(time)
|
||
const now = new Date()
|
||
const diff = now.getTime() - date.getTime()
|
||
const minutes = Math.floor(diff / 60000)
|
||
const hours = Math.floor(diff / 3600000)
|
||
const days = Math.floor(diff / 86400000)
|
||
|
||
if (minutes < 1) return '刚刚'
|
||
if (minutes < 60) return `${minutes}分钟前`
|
||
if (hours < 24) return `${hours}小时前`
|
||
if (days < 7) return `${days}天前`
|
||
return date.toLocaleDateString('zh-CN')
|
||
}
|
||
|
||
return (
|
||
<div className={styles.dashboard}>
|
||
{/* 顶部欢迎区 */}
|
||
<div className={styles.welcome}>
|
||
<div className={styles.userInfo}>
|
||
<div className={styles.avatar}>{user?.name?.[0] || 'U'}</div>
|
||
<div className={styles.greetingText}>
|
||
<h2 className={styles.greeting}>{t('dashboard.greeting', { name: user?.name || t('login.username') })}</h2>
|
||
<p className={styles.date}>{new Date().toLocaleDateString('zh-CN', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })}</p>
|
||
</div>
|
||
</div>
|
||
<button className={styles.langBtn} onClick={() => showLanguageSwitcher()}>
|
||
{getCurrentLanguage()}
|
||
</button>
|
||
</div>
|
||
|
||
{/* 待办事项 */}
|
||
{pendingCount > 0 && (
|
||
<div className={styles.section}>
|
||
<div className={styles.sectionHeader}>
|
||
<h3 className={styles.sectionTitle}>待办事项</h3>
|
||
<span className={styles.badge}>{pendingCount}</span>
|
||
</div>
|
||
<div className={styles.todoList}>
|
||
{todos.slice(0, 3).map((todo) => (
|
||
<div key={todo.id} className={styles.todoItem} onClick={() => navigate(`/approval/${todo.id}`)}>
|
||
<div className={styles.todoIcon}>
|
||
{todo.type === 'business_trip' ? '✈️' : '📋'}
|
||
</div>
|
||
<div className={styles.todoContent}>
|
||
<div className={styles.todoTitle}>{todo.title}</div>
|
||
<div className={styles.todoTime}>{formatTime(todo.time)}</div>
|
||
</div>
|
||
<div className={styles.todoArrow}>›</div>
|
||
</div>
|
||
))}
|
||
{pendingCount > 3 && (
|
||
<div className={styles.todoMore} onClick={() => navigate('/approval')}>
|
||
查看全部 {pendingCount} 个待办 ›
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* 最新动态 */}
|
||
{activities.length > 0 && (
|
||
<div className={styles.section}>
|
||
<h3 className={styles.sectionTitle}>最新动态</h3>
|
||
<div className={styles.activityList}>
|
||
{activities.slice(0, 3).map((activity) => (
|
||
<div key={activity.id} className={styles.activityItem}>
|
||
<div className={styles.activityIcon}>{activity.type === 'approval' ? '✅' : '📝'}</div>
|
||
<div className={styles.activityContent}>
|
||
<div className={styles.activityText}>{activity.content}</div>
|
||
<div className={styles.activityTime}>{formatTime(activity.time)}</div>
|
||
</div>
|
||
</div>
|
||
))}
|
||
{activities.length === 0 && (
|
||
<div className={styles.empty}>暂无动态</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* 快捷入口 */}
|
||
<div className={styles.section}>
|
||
<h3 className={styles.sectionTitle}>快捷入口</h3>
|
||
<div className={styles.actionGrid}>
|
||
{quickActions.map((action) => (
|
||
<div
|
||
key={action.id}
|
||
className={styles.actionCard}
|
||
onClick={() => handleQuickAction(action)}
|
||
>
|
||
<div className={styles.actionIcon} style={{ backgroundColor: `${action.color}15` }}>
|
||
<span style={{ fontSize: 32 }}>{action.icon}</span>
|
||
</div>
|
||
<span className={styles.actionLabel}>{action.label}</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{/* 所有应用 */}
|
||
<div className={styles.section}>
|
||
<h3 className={styles.sectionTitle}>所有应用</h3>
|
||
<div className={styles.actionGrid}>
|
||
{allApps.map((action) => (
|
||
<div
|
||
key={action.id}
|
||
className={styles.actionCard}
|
||
onClick={() => handleQuickAction(action)}
|
||
>
|
||
<div className={styles.actionIcon} style={{ backgroundColor: `${action.color}15` }}>
|
||
<span style={{ fontSize: 32 }}>{action.icon}</span>
|
||
</div>
|
||
<span className={styles.actionLabel}>{action.label}</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{/* 底部安全区域 */}
|
||
<div className={styles.safeArea} />
|
||
</div>
|
||
)
|
||
}
|