Files
x-server-mobile/frontend/src/pages/dashboard/index.tsx
T
浅念 baa424c841 feat: add used-car module and fix API response handling
- 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>
2026-03-11 16:11:49 +08:00

245 lines
8.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
)
}