优化考核调研首页移动端体验

This commit is contained in:
chenpei 2026-07-07 19:55:23 +08:00
parent ab2e31f01a
commit d428eb96a6

View File

@ -289,50 +289,89 @@ function Dashboard() {
const surveys = useApi<any>('/surveys', [])
const me = useApi<any>('/me', [])
const canManage = Boolean(me.data?.permissions?.isDepartmentAdmin)
const pendingItems = surveys.data?.pending || []
const createdItems = surveys.data?.created || []
const resultItems = surveys.data?.results || []
const openTotal = createdItems.filter((item: any) => item.status === 'open').length
const responseTotal = createdItems.reduce((sum: number, item: any) => sum + Number(item.counts?.responses || 0), 0)
const displayName = me.data?.employee?.displayName || me.data?.employee?.flowerName || me.data?.user?.name || '当前用户'
const avatarUrl = me.data?.user?.avatar || ''
const avatarInitial = String(displayName || '用').trim().charAt(0).toUpperCase() || '用'
const departmentLabel = me.data?.employee?.departmentName || me.data?.user?.department || '未同步部门'
const positionLabel = [me.data?.employee?.positionTitle, me.data?.employee?.positionLevel].filter(Boolean).join(' · ')
const tabs = [
{ id: 'pending', label: '待我评价' },
{ id: 'created', label: '我发起的' },
{ id: 'results', label: '我的结果' },
...(canManage ? [{ id: 'manage', label: '管理' }] : []),
{ id: 'pending', label: '待我评价', count: pendingItems.length },
{ id: 'created', label: '我发起的', count: createdItems.length },
{ id: 'results', label: '我的结果', count: resultItems.length },
...(canManage ? [{ id: 'manage', label: '管理', count: undefined }] : []),
]
return (
<main className="mx-auto max-w-7xl px-4 py-6">
<div className="mb-5 flex flex-col gap-3 md:flex-row md:items-end md:justify-between">
<div>
<h1 className="text-2xl font-semibold text-gray-900 dark:text-white">考核调研</h1>
<div className="mt-2 flex flex-wrap gap-2 text-sm text-gray-500 dark:text-gray-400">
<span>{me.data?.user?.name || '当前用户'}</span>
{me.data?.employee?.departmentName && <span>{me.data.employee.departmentName}</span>}
{me.data?.employee?.positionLevel && <span>{me.data.employee.positionLevel}</span>}
<main className="mx-auto max-w-7xl px-4 py-4 pb-24 sm:py-6 md:pb-6">
<section className="mb-4 overflow-hidden rounded-[28px] border border-[#dfe7f2] bg-white shadow-[0_18px_48px_rgba(15,23,42,0.08)] dark:border-gray-700 dark:bg-gray-900 sm:rounded-[20px]">
<div className="bg-[#0e1728] px-5 py-5 text-white sm:px-6">
<div className="flex items-start justify-between gap-4">
<div className="min-w-0">
<div className="text-sm font-medium text-white/65">考核调研</div>
<h1 className="mt-2 text-[2rem] font-semibold leading-tight tracking-normal sm:text-2xl">我的调研工作台</h1>
<div className="mt-3 flex flex-wrap items-center gap-2 text-sm text-white/72">
<span className="max-w-[12rem] truncate font-medium text-white">{displayName}</span>
<span className="h-1 w-1 rounded-full bg-white/35" />
<span className="max-w-[14rem] truncate">{departmentLabel}</span>
{positionLabel && (
<>
<span className="h-1 w-1 rounded-full bg-white/35" />
<span>{positionLabel}</span>
</>
)}
</div>
</div>
<div className="flex h-14 w-14 shrink-0 items-center justify-center overflow-hidden rounded-full border border-white/25 bg-white/10 text-lg font-semibold text-white shadow-inner">
{avatarUrl ? (
// eslint-disable-next-line @next/next/no-img-element
<img src={avatarUrl} alt={displayName} className="h-full w-full object-cover" />
) : (
avatarInitial
)}
</div>
</div>
<div className="mt-5 grid grid-cols-3 gap-2">
<DashboardMetric label="待评价" value={pendingItems.length} emphasis />
<DashboardMetric label="进行中" value={openTotal} />
<DashboardMetric label="已提交" value={responseTotal} />
</div>
</div>
<div className="flex rounded-lg border border-gray-200 bg-white p-1 dark:border-gray-700 dark:bg-gray-800">
<div className="flex gap-1 overflow-x-auto px-3 py-3 sm:px-4">
{tabs.map((item) => (
<button
key={item.id}
onClick={() => setTab(item.id)}
className={`px-3 py-1.5 text-sm rounded-md transition-colors ${
className={`min-w-max rounded-2xl px-4 py-2.5 text-sm font-semibold transition-colors ${
tab === item.id
? 'bg-gray-900 text-white dark:bg-white dark:text-gray-900'
: 'text-gray-600 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700'
? 'bg-[#111827] text-white shadow-sm dark:bg-white dark:text-gray-900'
: 'text-gray-500 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-800'
}`}
>
{item.label}
{typeof item.count === 'number' && item.count > 0 && (
<span className={`ml-2 rounded-full px-1.5 py-0.5 text-xs ${tab === item.id ? 'bg-white/18 text-white dark:bg-gray-900/10 dark:text-gray-900' : 'bg-gray-100 text-gray-500 dark:bg-gray-800 dark:text-gray-300'}`}>
{item.count}
</span>
)}
</button>
))}
</div>
</div>
</section>
{surveys.loading && <Panel>加载中...</Panel>}
{surveys.error && <ErrorPanel message={surveys.error} />}
{surveys.data && <DashboardStats data={surveys.data} />}
{surveys.data && tab === 'pending' && <PendingList items={surveys.data.pending || []} />}
{surveys.data && <DashboardStats data={surveys.data} activeTab={tab} />}
{surveys.data && tab === 'pending' && <PendingList items={pendingItems} />}
{surveys.data && tab === 'created' && (
<CreatedList items={surveys.data.created || []} onRefresh={surveys.refresh} />
<CreatedList items={createdItems} onRefresh={surveys.refresh} />
)}
{surveys.data && tab === 'results' && <MyResults items={surveys.data.results || []} />}
{surveys.data && tab === 'results' && <MyResults items={resultItems} />}
{canManage && tab === 'manage' && (
<ManagePanel
onChanged={async () => {
@ -345,7 +384,16 @@ function Dashboard() {
)
}
function DashboardStats({ data }: { data: any }) {
function DashboardMetric({ label, value, emphasis }: { label: string; value: number; emphasis?: boolean }) {
return (
<div className={`rounded-2xl px-3 py-3 ${emphasis ? 'bg-white text-[#0f172a]' : 'bg-white/10 text-white'}`}>
<div className={`text-xs ${emphasis ? 'text-gray-500' : 'text-white/60'}`}>{label}</div>
<div className="mt-1 text-2xl font-semibold leading-none">{value}</div>
</div>
)
}
function DashboardStats({ data, activeTab }: { data: any; activeTab: string }) {
const created = data.created || []
const templateStats = data.templateStats || []
const responseTotal = created.reduce((sum: number, item: any) => sum + Number(item.counts?.responses || 0), 0)
@ -358,7 +406,7 @@ function DashboardStats({ data }: { data: any }) {
]
return (
<div className="mb-5 grid gap-4 sm:grid-cols-2 xl:grid-cols-4">
<div className={`mb-5 grid gap-4 sm:grid-cols-2 xl:grid-cols-4 ${activeTab === 'manage' ? '' : 'hidden sm:grid'}`}>
{items.map((item) => (
<div key={item.label} className="rounded-lg border border-gray-200 bg-white p-5 dark:border-gray-700 dark:bg-gray-800">
<div className="text-sm text-gray-500 dark:text-gray-400">{item.label}</div>
@ -371,27 +419,34 @@ function DashboardStats({ data }: { data: any }) {
function PendingList({ items }: { items: any[] }) {
if (items.length === 0) {
return <EmptyState title="暂无待评价" />
return (
<div className="rounded-[24px] border border-dashed border-gray-200 bg-white px-5 py-8 text-center shadow-[0_12px_34px_rgba(15,23,42,0.05)] dark:border-gray-700 dark:bg-gray-900 sm:rounded-lg">
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-gray-100 text-lg font-semibold text-gray-500 dark:bg-gray-800 dark:text-gray-300">0</div>
<h2 className="mt-4 text-lg font-semibold text-gray-900 dark:text-white">暂无待评价</h2>
<p className="mt-2 text-sm text-gray-500 dark:text-gray-400">新的考核调研会出现在这里。</p>
</div>
)
}
return (
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-3">
{items.map((item) => (
<div key={item.assignmentId} className="rounded-lg border border-gray-200 bg-white p-4 dark:border-gray-700 dark:bg-gray-800">
<div key={item.assignmentId} className="rounded-[24px] border border-gray-200 bg-white p-5 shadow-[0_14px_36px_rgba(15,23,42,0.07)] dark:border-gray-700 dark:bg-gray-900 sm:rounded-lg sm:shadow-none">
<div className="flex items-start justify-between gap-3">
<div>
<h2 className="text-base font-semibold text-gray-900 dark:text-white">{item.title}</h2>
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">{item.targetName}</p>
<div className="min-w-0">
<div className="mb-2 text-xs font-semibold text-blue-600 dark:text-blue-300">待填写</div>
<h2 className="text-xl font-semibold leading-snug text-gray-950 dark:text-white sm:text-base">{item.title}</h2>
<p className="mt-2 truncate text-sm text-gray-500 dark:text-gray-400">评价对象:{item.targetName}</p>
</div>
<StatusBadge status={item.status} />
</div>
<div className="mt-4 flex items-center justify-between text-sm">
<div className="mt-5 flex items-center justify-between gap-3 text-sm">
<span className="text-gray-500 dark:text-gray-400">{item.dueAt ? `截止 ${formatDate(item.dueAt)}` : '无截止时间'}</span>
<Link
href={`/performance-survey/survey/${item.surveyId}?targetId=${item.targetId}`}
className="rounded-md bg-gray-900 px-3 py-1.5 text-white hover:bg-gray-700 dark:bg-white dark:text-gray-900"
className="rounded-2xl bg-[#1677ff] px-5 py-2.5 font-semibold text-white shadow-[0_10px_22px_rgba(22,119,255,0.26)] hover:bg-[#0f66dd] dark:bg-white dark:text-gray-900"
>
填写
去评价
</Link>
</div>
</div>