优化考核调研看板视觉
This commit is contained in:
parent
1cfffab48a
commit
7078f337ae
@ -6,7 +6,6 @@ import Link from 'next/link'
|
||||
import { useParams, useRouter, useSearchParams } from 'next/navigation'
|
||||
import { useSession } from 'next-auth/react'
|
||||
import { pinyin } from 'pinyin-pro'
|
||||
import Header from '@/components/Header'
|
||||
|
||||
type ApiState<T> = {
|
||||
loading: boolean
|
||||
@ -294,11 +293,12 @@ export default function PerformanceSurveyPage() {
|
||||
} as any)
|
||||
|
||||
const isSurveySubmitPage = path[0] === 'survey' && Boolean(path[1])
|
||||
const isResultsPage = path[0] === 'results' && Boolean(path[1])
|
||||
let content = <Dashboard />
|
||||
if (isSurveySubmitPage && path[1]) {
|
||||
content = <SurveySubmit surveyId={path[1]} session={effectiveSession} />
|
||||
}
|
||||
if (path[0] === 'results' && path[1]) {
|
||||
if (isResultsPage && path[1]) {
|
||||
content = <SurveyResults surveyId={path[1]} />
|
||||
}
|
||||
|
||||
@ -311,11 +311,8 @@ export default function PerformanceSurveyPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen overflow-x-hidden bg-white dark:bg-gray-900 sm:bg-gray-50">
|
||||
<div className="hidden sm:block">
|
||||
<Header session={effectiveSession} />
|
||||
</div>
|
||||
<PerformanceSurveyMobileHeader session={effectiveSession} />
|
||||
<div className="min-h-screen overflow-x-hidden bg-white text-[#1a1c1c]">
|
||||
{isResultsPage && <PerformanceSurveyMobileHeader session={effectiveSession} />}
|
||||
{content}
|
||||
</div>
|
||||
)
|
||||
@ -331,16 +328,16 @@ function PerformanceSurveyMobileHeader({ session }: { session: any }) {
|
||||
: ''
|
||||
|
||||
return (
|
||||
<header className="sticky top-0 z-30 bg-[#30313a] text-white sm:hidden">
|
||||
<div className="mx-auto flex h-[72px] max-w-[430px] items-center justify-between gap-4 px-5">
|
||||
<header className="sticky top-0 z-30 border-b border-[#eeeeee] bg-[#2f3131] text-white">
|
||||
<div className="mx-auto flex h-14 w-full max-w-[1440px] items-center justify-between gap-4 px-4 sm:px-6">
|
||||
<div className="flex min-w-0 items-center gap-3">
|
||||
<div className="flex h-11 w-11 shrink-0 items-center justify-center overflow-hidden rounded-full border border-white/20 bg-[#eef2f7] text-[14px] font-semibold text-[#1677ff] shadow-[0_8px_18px_rgba(0,0,0,0.18)]">
|
||||
<div className="flex h-8 w-8 shrink-0 items-center justify-center overflow-hidden rounded-full border border-white/15 bg-[#f4f4f4] text-[12px] font-semibold text-[#0050cb]">
|
||||
{avatar ? (
|
||||
<Image
|
||||
src={avatar}
|
||||
alt={name}
|
||||
width={44}
|
||||
height={44}
|
||||
width={32}
|
||||
height={32}
|
||||
unoptimized
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
@ -348,9 +345,9 @@ function PerformanceSurveyMobileHeader({ session }: { session: any }) {
|
||||
getSurveyInitials(name)
|
||||
)}
|
||||
</div>
|
||||
<div className="truncate text-[20px] font-semibold tracking-tight">考核调研</div>
|
||||
<div className="truncate text-[16px] font-bold tracking-tight">考核调研</div>
|
||||
</div>
|
||||
<div className="shrink-0 text-[14px] font-semibold text-[#dce4ff]">由壹点「素材库」</div>
|
||||
<div className="shrink-0 text-[12px] font-semibold text-white/85">由壹点「素材库」</div>
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
@ -370,8 +367,7 @@ function Dashboard() {
|
||||
))
|
||||
const me = useApi<any>('/me', [])
|
||||
const displayName = me.data?.employee?.displayName || me.data?.employee?.flowerName || me.data?.user?.name || '当前用户'
|
||||
const departmentLabel = me.data?.employee?.departmentName || me.data?.user?.department || '未同步部门'
|
||||
const positionLabel = [me.data?.employee?.positionTitle, me.data?.employee?.positionLevel].filter(Boolean).join(' · ')
|
||||
const avatar = me.data?.user?.avatar || me.data?.user?.image || ''
|
||||
const availableTabIds = useMemo(
|
||||
() => ['personal', 'public', 'manage'],
|
||||
[],
|
||||
@ -391,46 +387,31 @@ function Dashboard() {
|
||||
setTab(nextTab)
|
||||
router.replace(`/performance-survey?tab=${encodeURIComponent(nextTab)}`, { scroll: false })
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="mx-auto w-full max-w-[430px] bg-white px-4 py-5 pb-24 text-[#101828] dark:bg-gray-950 sm:max-w-[1180px] sm:bg-transparent sm:px-6 sm:py-6 md:pb-8">
|
||||
<section className="mb-5 rounded-[18px] border border-[#eaedf3] bg-white px-5 py-5 shadow-[0_10px_26px_rgba(15,23,42,0.05)] dark:border-gray-800 dark:bg-gray-900 sm:rounded-[16px] sm:px-6">
|
||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between">
|
||||
<div className="min-w-0">
|
||||
<h1 className="text-[26px] font-semibold leading-tight tracking-[-0.01em] text-[#101828] dark:text-white sm:text-[28px]">我的调研</h1>
|
||||
<div className="mt-3 flex flex-wrap items-center gap-x-2 gap-y-1 text-[14px] font-semibold leading-5 text-[#111827] dark:text-gray-200">
|
||||
<span className="max-w-full truncate">{displayName}</span>
|
||||
<span className="text-[#98a2b3]">·</span>
|
||||
<span className="max-w-full truncate">{departmentLabel}</span>
|
||||
{positionLabel && (
|
||||
<>
|
||||
<span className="text-[#98a2b3]">·</span>
|
||||
<span className="max-w-full break-words">{positionLabel}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-6 flex gap-6 overflow-x-auto border-b border-[#e5eaf2] pb-0 dark:border-gray-800">
|
||||
{[
|
||||
const tabItems = [
|
||||
{ id: 'personal', label: '个人看板' },
|
||||
{ id: 'public', label: '公共看板' },
|
||||
{ id: 'manage', label: '发起考核' },
|
||||
].map((item) => (
|
||||
<button
|
||||
key={item.id}
|
||||
type="button"
|
||||
onClick={() => selectTab(item.id)}
|
||||
className={`shrink-0 border-b-2 px-0 pb-3 text-[14px] font-semibold transition-colors ${
|
||||
tab === item.id
|
||||
? 'border-[#0052d9] text-[#0052d9]'
|
||||
: 'border-transparent text-[#344054] hover:text-[#0052d9] dark:text-gray-300'
|
||||
}`}
|
||||
>
|
||||
{item.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
]
|
||||
const pageTitle = tab === 'public' ? '公共看板' : tab === 'manage' ? '新建考核计划' : '个人绩效概览'
|
||||
const pageSubtitle = tab === 'personal'
|
||||
? '过去 12 个月的考核详细数据'
|
||||
: tab === 'public'
|
||||
? ''
|
||||
: ''
|
||||
|
||||
return (
|
||||
<>
|
||||
<PerformanceSurveyDashboardHeader
|
||||
activeTab={tab}
|
||||
avatar={avatar}
|
||||
displayName={displayName}
|
||||
onSelectTab={selectTab}
|
||||
tabItems={tabItems}
|
||||
/>
|
||||
<main className="mx-auto w-full max-w-[1200px] bg-white px-4 py-8 pb-24 text-[#1a1c1c] sm:px-6 sm:pb-10">
|
||||
<section className="mb-6">
|
||||
<h1 className="text-[24px] font-bold leading-tight text-[#1a1c1c] sm:text-[22px]">{pageTitle}</h1>
|
||||
{pageSubtitle && <p className="mt-2 text-[14px] font-medium text-[#5f5e5e]">{pageSubtitle}</p>}
|
||||
</section>
|
||||
|
||||
{tab !== 'manage' && (
|
||||
@ -440,6 +421,84 @@ function Dashboard() {
|
||||
<ManagePanel />
|
||||
)}
|
||||
</main>
|
||||
<nav className="fixed inset-x-0 bottom-0 z-30 grid h-[68px] grid-cols-3 border-t border-[#e2e2e2] bg-white sm:hidden">
|
||||
{tabItems.map((item) => (
|
||||
<button
|
||||
key={item.id}
|
||||
type="button"
|
||||
onClick={() => selectTab(item.id)}
|
||||
className={`flex items-center justify-center text-[12px] font-semibold ${
|
||||
tab === item.id ? 'text-[#0050cb]' : 'text-[#5f5e5e]'
|
||||
}`}
|
||||
>
|
||||
{item.label}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function PerformanceSurveyDashboardHeader({
|
||||
activeTab,
|
||||
avatar,
|
||||
displayName,
|
||||
onSelectTab,
|
||||
tabItems,
|
||||
}: {
|
||||
activeTab: string
|
||||
avatar: string
|
||||
displayName: string
|
||||
onSelectTab: (tab: string) => void
|
||||
tabItems: Array<{ id: string; label: string }>
|
||||
}) {
|
||||
return (
|
||||
<header className="bg-[#1f2937] text-white shadow-sm">
|
||||
<div className="container mx-auto grid min-h-[60px] max-w-7xl grid-cols-[minmax(0,1fr)_auto] items-center gap-4 px-3 py-3 sm:grid-cols-[minmax(220px,1fr)_auto_minmax(220px,1fr)] sm:px-4">
|
||||
<div className="flex min-w-0 items-center gap-3">
|
||||
<div className="flex h-9 w-9 shrink-0 items-center justify-center overflow-hidden rounded-full border border-white/15 bg-white/15 text-white">
|
||||
{avatar ? (
|
||||
<Image
|
||||
src={avatar}
|
||||
alt={displayName}
|
||||
width={36}
|
||||
height={36}
|
||||
unoptimized
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<span className="text-[12px] font-bold">{getSurveyInitials(displayName)}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="truncate text-[16px] font-bold">考核调研</div>
|
||||
</div>
|
||||
<nav className="hidden h-full items-center justify-center gap-9 sm:flex">
|
||||
{tabItems.map((item) => {
|
||||
const active = activeTab === item.id
|
||||
return (
|
||||
<button
|
||||
key={item.id}
|
||||
type="button"
|
||||
onClick={() => onSelectTab(item.id)}
|
||||
className={`h-full border-b-2 px-1 text-[13px] font-bold transition-colors ${
|
||||
active
|
||||
? 'border-white text-white'
|
||||
: 'border-transparent text-[#b3c5ff] hover:text-white'
|
||||
}`}
|
||||
>
|
||||
{item.label}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</nav>
|
||||
<div className="hidden justify-self-end text-[13px] font-semibold text-white/90 sm:block">
|
||||
由壹点「素材库」
|
||||
</div>
|
||||
<div className="justify-self-end text-[12px] font-semibold text-[#b3c5ff] sm:hidden">
|
||||
由壹点
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
|
||||
@ -527,9 +586,22 @@ function PerformanceDashboardBoard({ view }: { view: 'personal' | 'public' }) {
|
||||
return (
|
||||
<section className="mb-5 space-y-4">
|
||||
{view === 'public' && (
|
||||
<div className="rounded-[16px] border border-[#eaedf3] bg-white px-4 py-4 shadow-[0_8px_22px_rgba(15,23,42,0.04)] dark:border-gray-800 dark:bg-gray-900 sm:px-5">
|
||||
<div className="mb-3 flex items-center justify-between gap-3">
|
||||
<h2 className="text-[17px] font-semibold text-[#101828] dark:text-white">公共看板</h2>
|
||||
<div className="border-b border-[#e2e2e2] pb-5">
|
||||
<div className="grid gap-3 sm:grid-cols-[auto_150px_auto_minmax(220px,0.8fr)_minmax(260px,1fr)_auto] sm:items-center">
|
||||
<div className="hidden text-[13px] font-semibold text-[#5f5e5e] sm:block">统计周期</div>
|
||||
<input
|
||||
type="month"
|
||||
value={month}
|
||||
onChange={(event) => setMonth(event.target.value)}
|
||||
className="h-10 rounded border border-[#e2e2e2] bg-white px-3 text-[13px] font-semibold text-[#1a1c1c] outline-none transition-colors focus:border-[#0050cb]"
|
||||
/>
|
||||
<div className="hidden text-[13px] font-semibold text-[#5f5e5e] sm:block">组织架构</div>
|
||||
<DepartmentFilterTree
|
||||
departments={departments.data || []}
|
||||
value={department}
|
||||
onChange={(value) => setDepartment(value)}
|
||||
/>
|
||||
<SingleEmployeeFilter value={employee} onChange={setEmployee} />
|
||||
{(department || employee) && (
|
||||
<button
|
||||
type="button"
|
||||
@ -537,30 +609,20 @@ function PerformanceDashboardBoard({ view }: { view: 'personal' | 'public' }) {
|
||||
setDepartment('')
|
||||
setEmployee(null)
|
||||
}}
|
||||
className="rounded-full border border-[#dbe5f2] bg-white px-3 py-1.5 text-xs font-semibold text-[#475467] hover:bg-[#f8fafc] dark:border-gray-700 dark:bg-gray-950 dark:text-gray-200"
|
||||
className="h-10 rounded border border-[#e2e2e2] bg-white px-4 text-[12px] font-bold text-[#1a1c1c] hover:bg-[#f9f9f9]"
|
||||
>
|
||||
清除
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="grid gap-2 lg:grid-cols-[150px_minmax(230px,0.9fr)_minmax(260px,1fr)]">
|
||||
<input
|
||||
type="month"
|
||||
value={month}
|
||||
onChange={(event) => setMonth(event.target.value)}
|
||||
className="h-11 rounded-[12px] border border-[#dbe5f2] bg-white px-3 text-sm font-semibold text-[#101828] outline-none transition-colors focus:border-[#1677ff] focus:ring-4 focus:ring-[#1677ff]/10 dark:border-gray-700 dark:bg-gray-950 dark:text-white"
|
||||
/>
|
||||
<DepartmentFilterTree
|
||||
departments={departments.data || []}
|
||||
value={department}
|
||||
onChange={(value) => setDepartment(value)}
|
||||
/>
|
||||
<SingleEmployeeFilter value={employee} onChange={setEmployee} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{dashboard.loading && <Panel>看板加载中...</Panel>}
|
||||
{dashboard.loading && (
|
||||
<div className="border border-[#e2e2e2] bg-white px-5 py-8 text-sm font-semibold text-[#5f5e5e]">
|
||||
看板加载中...
|
||||
</div>
|
||||
)}
|
||||
{dashboard.error && <ErrorPanel message={dashboard.error} />}
|
||||
{dashboard.data && (
|
||||
<>
|
||||
@ -601,15 +663,15 @@ function MonthlyPersonalLedger({
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="space-y-3 sm:hidden">
|
||||
<div className="space-y-4 sm:hidden">
|
||||
{rows.map((row) => (
|
||||
<article
|
||||
key={row.month}
|
||||
className="rounded-[16px] border border-[#e5eaf2] bg-white px-4 py-4 shadow-[0_8px_20px_rgba(15,23,42,0.04)] dark:border-gray-700 dark:bg-gray-900"
|
||||
className="border border-[#c2c6d8] bg-white p-4"
|
||||
>
|
||||
<div className="mb-3 flex items-center justify-between gap-3">
|
||||
<div className="text-[20px] font-semibold text-[#101828] dark:text-white">{row.month}</div>
|
||||
<div className="text-xs font-semibold text-[#667085] dark:text-gray-400">/120</div>
|
||||
<div className="text-[20px] font-bold text-[#1a1c1c]">{formatMonthLabel(row.month)}</div>
|
||||
<div className="rounded-full bg-[#dae1ff] px-2 py-1 text-[11px] font-bold text-[#0050cb]">CURRENT</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{templates.map((template) => (
|
||||
@ -628,20 +690,21 @@ function MonthlyPersonalLedger({
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
<div className="hidden overflow-hidden rounded-[16px] border border-[#eaedf3] bg-white shadow-[0_8px_22px_rgba(15,23,42,0.04)] dark:border-gray-800 dark:bg-gray-900 sm:block">
|
||||
<div className="hidden overflow-hidden rounded border border-[#c2c6d8] bg-white sm:block">
|
||||
<table className="w-full table-fixed border-collapse text-left">
|
||||
<thead>
|
||||
<tr className="border-b border-[#edf1f6] bg-[#fbfcfe] text-[12px] font-semibold text-[#667085] dark:border-gray-800 dark:bg-gray-950 dark:text-gray-400">
|
||||
<th className="w-[160px] px-5 py-3">月份</th>
|
||||
<tr className="border-b border-[#c2c6d8] bg-[#f4f3f3] text-[12px] font-bold uppercase tracking-tight text-[#5f5e5e]">
|
||||
<th className="w-[160px] px-6 py-4">月份</th>
|
||||
{templates.map((template) => (
|
||||
<th key={template.code} className="px-4 py-3">{template.name}</th>
|
||||
<th key={template.code} className="px-4 py-4">{getTemplateDisplayName(template)}</th>
|
||||
))}
|
||||
<th className="w-[140px] px-4 py-4 text-right">综合评分</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-[#edf1f6] dark:divide-gray-800">
|
||||
<tbody className="divide-y divide-[#c2c6d8]">
|
||||
{rows.map((row) => (
|
||||
<tr key={row.month} className="text-sm text-[#101828] dark:text-white">
|
||||
<td className="px-5 py-4 text-[15px] font-semibold">{row.month}</td>
|
||||
<tr key={row.month} className="text-sm text-[#1a1c1c] hover:bg-[#fcfcfc]">
|
||||
<td className="px-6 py-5 text-[16px] font-bold">{row.month}</td>
|
||||
{templates.map((template) => (
|
||||
<td key={template.code} className="px-4 py-4">
|
||||
<MonthlyTableScoreCell
|
||||
@ -654,6 +717,7 @@ function MonthlyPersonalLedger({
|
||||
/>
|
||||
</td>
|
||||
))}
|
||||
<td className="px-4 py-4 text-right text-[22px] font-bold text-[#0050cb]">{formatAverageScore(row)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
@ -678,19 +742,22 @@ function MonthlyPublicLedger({
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="space-y-2.5 sm:hidden">
|
||||
<div className="space-y-0 sm:hidden">
|
||||
{rows.map((row) => (
|
||||
<article
|
||||
key={row.employee.id}
|
||||
className="rounded-[18px] border border-[#e6ebf2] bg-white px-4 py-4 shadow-[0_8px_20px_rgba(15,23,42,0.04)] dark:border-gray-700 dark:bg-gray-900"
|
||||
className="border-b border-[#c2c6d8] bg-white py-4"
|
||||
>
|
||||
<div className="min-w-0">
|
||||
<div className="truncate text-[18px] font-semibold text-[#101828] dark:text-white">{row.employee.displayName}</div>
|
||||
<div className="mt-1 truncate text-[12px] font-semibold text-[#667085] dark:text-gray-400">
|
||||
<div className="mb-3 flex min-w-0 items-center justify-between gap-3">
|
||||
<div>
|
||||
<div className="truncate text-[16px] font-bold text-[#1a1c1c]">{row.employee.displayName}</div>
|
||||
<div className="mt-1 truncate text-[12px] text-[#5f5e5e]">
|
||||
{[row.employee.departmentName, row.employee.positionLevel || row.employee.levelType].filter(Boolean).join(' · ')}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 grid grid-cols-2 gap-2">
|
||||
<span className="text-[18px] font-bold text-[#0050cb]">{formatAverageScore(row)}</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 gap-1">
|
||||
{templates.map((template) => (
|
||||
<MonthlyScoreCell
|
||||
key={template.code}
|
||||
@ -709,27 +776,36 @@ function MonthlyPublicLedger({
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
<div className="hidden overflow-hidden rounded-[16px] border border-[#eaedf3] bg-white shadow-[0_8px_22px_rgba(15,23,42,0.04)] dark:border-gray-800 dark:bg-gray-900 sm:block">
|
||||
<div className="hidden overflow-hidden rounded border border-[#e2e2e2] bg-white sm:block">
|
||||
<table className="w-full table-fixed border-collapse text-left">
|
||||
<thead>
|
||||
<tr className="border-b border-[#edf1f6] bg-[#fbfcfe] text-[12px] font-semibold text-[#667085] dark:border-gray-800 dark:bg-gray-950 dark:text-gray-400">
|
||||
<th className="w-[210px] px-5 py-3">人员</th>
|
||||
<th className="w-[220px] px-4 py-3">组织 / 职级</th>
|
||||
<tr className="border-b border-[#e2e2e2] bg-[#f9f9f9] text-[12px] font-bold uppercase tracking-tight text-[#5f5e5e]">
|
||||
<th className="w-[220px] px-5 py-4">人员</th>
|
||||
<th className="w-[240px] px-4 py-4">组织 / 职级</th>
|
||||
{templates.map((template) => (
|
||||
<th key={template.code} className="px-4 py-3">{template.name}</th>
|
||||
<th key={template.code} className="px-4 py-4 text-center">{getTemplateDisplayName(template)}</th>
|
||||
))}
|
||||
<th className="w-[120px] px-4 py-4 text-right">综合评分</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-[#edf1f6] dark:divide-gray-800">
|
||||
<tbody className="divide-y divide-[#e2e2e2]">
|
||||
{rows.map((row) => (
|
||||
<tr key={row.employee.id} className="text-sm text-[#101828] dark:text-white">
|
||||
<td className="px-5 py-4 text-[15px] font-semibold">{row.employee.displayName}</td>
|
||||
<td className="px-4 py-4 text-[13px] font-medium text-[#667085] dark:text-gray-400">
|
||||
<tr key={row.employee.id} className="text-sm text-[#1a1c1c] hover:bg-[#fcfcfc]">
|
||||
<td className="px-5 py-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-[#f4f3f3] text-[12px] font-bold text-[#1a1c1c]">{getSurveyInitials(row.employee.displayName)}</div>
|
||||
<div>
|
||||
<div className="text-[15px] font-bold">{row.employee.displayName}</div>
|
||||
{row.employee.jobNumber && <div className="mt-0.5 text-[11px] text-[#5f5e5e]">{row.employee.jobNumber}</div>}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-4 text-[13px] font-medium text-[#5f5e5e]">
|
||||
<div className="truncate">{row.employee.departmentName || '-'}</div>
|
||||
<div className="mt-1 truncate">{row.employee.positionLevel || row.employee.levelType || '-'}</div>
|
||||
</td>
|
||||
{templates.map((template) => (
|
||||
<td key={template.code} className="px-4 py-4">
|
||||
<td key={template.code} className="px-4 py-4 text-center">
|
||||
<MonthlyTableScoreCell
|
||||
cell={row.cells?.[template.code]}
|
||||
canOpenOverride={canOpenPublicDetail}
|
||||
@ -741,6 +817,7 @@ function MonthlyPublicLedger({
|
||||
/>
|
||||
</td>
|
||||
))}
|
||||
<td className="px-4 py-4 text-right text-[20px] font-bold text-[#0050cb]">{formatAverageScore(row)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
@ -765,19 +842,19 @@ function MonthlyScoreCell({
|
||||
}) {
|
||||
const hasScore = cell && cell.score !== null && cell.score !== undefined
|
||||
const canOpen = Boolean(hasScore && (canOpenOverride ?? cell?.detailAllowed))
|
||||
const className = `${compact ? 'min-h-[72px] px-3 py-3' : 'min-h-[88px] px-3.5 py-3.5'} rounded-[14px] border text-left transition-colors ${
|
||||
const className = `${compact ? 'min-h-[58px] px-2 py-2' : 'min-h-[86px] px-3 py-3'} border text-left transition-colors ${
|
||||
canOpen
|
||||
? 'border-[#cfe0ff] bg-white hover:border-[#1677ff] hover:bg-[#f7fbff] dark:border-blue-500/30 dark:bg-blue-500/10'
|
||||
: 'border-[#e7edf5] bg-[#fafbfc] dark:border-gray-700 dark:bg-gray-800'
|
||||
? 'border-[#c2c6d8] bg-white hover:border-[#0050cb] hover:bg-[#f9fbff]'
|
||||
: 'border-[#e2e2e2] bg-[#f9f9f9]'
|
||||
}`
|
||||
const content = (
|
||||
<>
|
||||
<div className="text-xs font-semibold text-[#667085] dark:text-gray-400">{template.name}</div>
|
||||
<div className="truncate text-[11px] font-semibold text-[#5f5e5e]">{getTemplateDisplayName(template)}</div>
|
||||
<div className="mt-2 flex items-baseline gap-1">
|
||||
<span className={`${compact ? 'text-[23px]' : 'text-[24px]'} font-semibold leading-none ${hasScore ? 'text-[#101828] dark:text-white' : 'text-[#98a2b3] dark:text-gray-500'}`}>
|
||||
<span className={`${compact ? 'text-[17px]' : 'text-[24px]'} font-bold leading-none ${hasScore ? (canOpen ? 'text-[#0050cb]' : 'text-[#1a1c1c]') : 'text-[#727687]'}`}>
|
||||
{hasScore ? formatNullableScore(cell.score) : '-'}
|
||||
</span>
|
||||
{hasScore && <span className="text-[11px] font-semibold text-[#98a2b3]">/120</span>}
|
||||
{hasScore && <span className="text-[11px] font-medium text-[#5f5e5e]">/120</span>}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
@ -806,22 +883,22 @@ function MonthlyTableScoreCell({
|
||||
const canOpen = Boolean(hasScore && (canOpenOverride ?? cell?.detailAllowed))
|
||||
const content = (
|
||||
<>
|
||||
<span className={`text-[15px] font-semibold ${hasScore ? 'text-[#101828] dark:text-white' : 'text-[#98a2b3] dark:text-gray-500'}`}>
|
||||
<span className={`text-[16px] font-bold ${hasScore ? (canOpen ? 'text-[#0050cb]' : 'text-[#1a1c1c]') : 'text-[#727687]'}`}>
|
||||
{hasScore ? formatNullableScore(cell.score) : '-'}
|
||||
</span>
|
||||
{hasScore && <span className="text-[11px] font-semibold text-[#98a2b3]">/120</span>}
|
||||
{hasScore && <span className="text-[11px] font-medium text-[#5f5e5e]">/120</span>}
|
||||
</>
|
||||
)
|
||||
|
||||
if (!canOpen) {
|
||||
return <div className="inline-flex min-h-9 min-w-[74px] items-center gap-1 rounded-full bg-[#f8fafc] px-3 text-left dark:bg-gray-800">{content}</div>
|
||||
return <div className="inline-flex min-h-9 min-w-[74px] items-baseline justify-center gap-1 text-left">{content}</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className="inline-flex min-h-9 min-w-[74px] items-center gap-1 rounded-full border border-[#cfe0ff] bg-white px-3 text-left transition-colors hover:border-[#1677ff] hover:bg-[#f7fbff] dark:border-blue-500/30 dark:bg-blue-500/10"
|
||||
className="inline-flex min-h-9 min-w-[74px] items-baseline justify-center gap-1 text-left transition-colors hover:text-[#003fa4]"
|
||||
>
|
||||
{content}
|
||||
</button>
|
||||
@ -830,9 +907,9 @@ function MonthlyTableScoreCell({
|
||||
|
||||
function EmptyLedger({ title }: { title: string }) {
|
||||
return (
|
||||
<div className="rounded-[16px] border border-dashed border-[#d9ddeb] bg-white px-5 py-12 text-center shadow-[0_8px_20px_rgba(15,23,42,0.04)] dark:border-gray-700 dark:bg-gray-900">
|
||||
<div className="mx-auto flex h-11 w-11 items-center justify-center rounded-full bg-[#eef2f7] text-lg font-semibold text-[#667085] dark:bg-gray-800 dark:text-gray-300">0</div>
|
||||
<h2 className="mt-4 text-[18px] font-semibold text-gray-900 dark:text-white">{title}</h2>
|
||||
<div className="border border-dashed border-[#c2c6d8] bg-white px-5 py-12 text-center">
|
||||
<div className="mx-auto flex h-11 w-11 items-center justify-center rounded-full bg-[#f4f3f3] text-lg font-bold text-[#727687]">0</div>
|
||||
<h2 className="mt-4 text-[18px] font-bold text-[#1a1c1c]">{title}</h2>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -854,49 +931,49 @@ function MonthlyScoreDetailModal({
|
||||
if (!request) return null
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-end bg-black/30 p-0 sm:items-stretch sm:justify-end">
|
||||
<div className="max-h-[88vh] w-full overflow-y-auto rounded-t-[22px] bg-white p-5 shadow-[0_-18px_50px_rgba(15,23,42,0.18)] dark:bg-gray-900 sm:h-full sm:max-h-none sm:w-[520px] sm:rounded-none sm:border-l sm:border-[#e5eaf2] sm:p-6 sm:shadow-[-18px_0_45px_rgba(15,23,42,0.12)] dark:sm:border-gray-800">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="fixed inset-0 z-50 flex items-end bg-black/40 p-0 sm:items-stretch sm:justify-end">
|
||||
<div className="max-h-[88vh] w-full overflow-y-auto bg-white p-5 shadow-[0_-18px_50px_rgba(0,0,0,0.18)] sm:h-full sm:max-h-none sm:w-[520px] sm:border-l sm:border-[#e2e2e2] sm:p-6 sm:shadow-[-18px_0_45px_rgba(0,0,0,0.12)]">
|
||||
<div className="flex items-start justify-between gap-4 border-b border-[#e2e2e2] pb-5">
|
||||
<div className="min-w-0">
|
||||
<div className="text-xs font-semibold text-[#1677ff]">{detail.data?.month || request.month}</div>
|
||||
<h2 className="mt-1 break-words text-[20px] font-semibold text-[#101828] dark:text-white">
|
||||
<div className="text-[11px] font-semibold uppercase tracking-[0.12em] text-[#5f5e5e]">{detail.data?.month || request.month}</div>
|
||||
<h2 className="mt-2 break-words text-[24px] font-bold leading-tight text-[#1a1c1c]">
|
||||
{detail.data?.template?.name || '考核明细'}
|
||||
</h2>
|
||||
{detail.data?.employee && (
|
||||
<p className="mt-1 text-sm font-medium text-[#667085] dark:text-gray-400">{detail.data.employee.displayName}</p>
|
||||
<p className="mt-2 text-[14px] font-semibold text-[#5f5e5e]">{detail.data.employee.displayName}</p>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="shrink-0 rounded-full bg-[#f2f4f7] px-3 py-1.5 text-sm font-semibold text-[#344054] hover:bg-[#e4e7ec] dark:bg-gray-800 dark:text-gray-100"
|
||||
className="flex h-10 w-10 shrink-0 items-center justify-center text-[32px] font-light leading-none text-[#5f5e5e] hover:text-[#1a1c1c]"
|
||||
>
|
||||
关闭
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{detail.loading && <div className="mt-5 text-sm font-medium text-[#667085]">加载中...</div>}
|
||||
{detail.loading && <div className="mt-5 text-sm font-medium text-[#5f5e5e]">加载中...</div>}
|
||||
{detail.error && <ErrorPanel message={detail.error} />}
|
||||
{detail.data && (
|
||||
<div className="mt-5 space-y-4">
|
||||
<div className="rounded-[14px] border border-[#e5eaf2] bg-white px-4 py-3 dark:border-gray-700 dark:bg-gray-800">
|
||||
<div className="text-xs font-semibold text-[#667085] dark:text-gray-400">最终分</div>
|
||||
<div className="mt-1 text-[30px] font-semibold leading-none text-[#101828] dark:text-white">
|
||||
{formatNullableScore(detail.data.score)} <span className="text-sm text-[#98a2b3]">/120</span>
|
||||
<div className="border border-[#eeeeee] bg-[#f9f9f9] px-4 py-4">
|
||||
<div className="text-[12px] font-semibold tracking-[0.05em] text-[#5f5e5e]">最终分</div>
|
||||
<div className="mt-2 text-[40px] font-bold leading-none text-[#1a1c1c]">
|
||||
{formatNullableScore(detail.data.score)} <span className="text-base font-medium text-[#5f5e5e]">/120</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{detail.data.workItems?.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
{detail.data.workItems.map((item: any, index: number) => (
|
||||
<section key={item.id} className="rounded-[14px] border border-[#e5eaf2] px-4 py-4 dark:border-gray-700">
|
||||
<section key={item.id} className="border border-[#eeeeee] px-4 py-4">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<div className="text-xs font-semibold text-[#1677ff]">作品 {index + 1}</div>
|
||||
<h3 className="mt-1 break-words text-[16px] font-semibold text-[#101828] dark:text-white">{item.title}</h3>
|
||||
{item.description && <p className="mt-1 whitespace-pre-wrap text-sm text-[#667085] dark:text-gray-400">{item.description}</p>}
|
||||
<div className="text-[12px] font-semibold text-[#0050cb]">作品 {index + 1}</div>
|
||||
<h3 className="mt-1 break-words text-[16px] font-bold text-[#1a1c1c]">{item.title}</h3>
|
||||
{item.description && <p className="mt-1 whitespace-pre-wrap text-sm text-[#5f5e5e]">{item.description}</p>}
|
||||
</div>
|
||||
<div className="shrink-0 text-[18px] font-semibold text-[#101828] dark:text-white">{formatNullableScore(item.averageScore)}</div>
|
||||
<div className="shrink-0 text-[18px] font-bold text-[#0050cb]">{formatNullableScore(item.averageScore)}</div>
|
||||
</div>
|
||||
<ImagePreviewGrid images={item.imageUrls || []} />
|
||||
<ScoreParticipantList items={item.scores || []} />
|
||||
@ -915,19 +992,19 @@ function MonthlyScoreDetailModal({
|
||||
|
||||
function ScoreParticipantList({ items }: { items: any[] }) {
|
||||
if (!items.length) {
|
||||
return <div className="rounded-[12px] bg-[#f8fafc] px-4 py-4 text-sm font-medium text-[#667085] dark:bg-gray-800 dark:text-gray-400">暂无评分记录</div>
|
||||
return <div className="border border-[#eeeeee] bg-[#f9f9f9] px-4 py-4 text-sm font-semibold text-[#5f5e5e]">暂无评分记录</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="divide-y divide-[#eeeeee] border border-[#eeeeee] bg-white">
|
||||
{items.map((item) => (
|
||||
<div key={item.id} className="rounded-[12px] bg-[#f8fafc] px-4 py-3 dark:bg-gray-800">
|
||||
<div key={item.id} className="px-4 py-3">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0 text-sm font-semibold text-[#101828] dark:text-white">{item.name}</div>
|
||||
<div className="min-w-0 text-sm font-bold text-[#1a1c1c]">{item.name}</div>
|
||||
<div className="shrink-0 text-sm font-semibold text-[#1677ff]">{formatNullableScore(item.score)}</div>
|
||||
</div>
|
||||
{item.comment && <p className="mt-2 whitespace-pre-wrap text-sm leading-5 text-[#667085] dark:text-gray-400">{item.comment}</p>}
|
||||
{item.submittedAt && <div className="mt-2 text-xs text-[#98a2b3]">{formatDate(item.submittedAt)}</div>}
|
||||
{item.comment && <p className="mt-2 whitespace-pre-wrap text-sm leading-5 text-[#5f5e5e]">{item.comment}</p>}
|
||||
{item.submittedAt && <div className="mt-2 text-xs text-[#727687]">{formatDate(item.submittedAt)}</div>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@ -964,6 +1041,32 @@ function TemplateSectionCard({ item }: { item: any }) {
|
||||
)
|
||||
}
|
||||
|
||||
function getTemplateDisplayName(template: any) {
|
||||
const name = String(template?.shortName || template?.name || '')
|
||||
return name
|
||||
.replace(/满意度调研/g, '')
|
||||
.replace(/考核/g, '')
|
||||
.replace(/\s*[·\-]\s*\d+%/g, '')
|
||||
.replace(/\s*\d+%/g, '')
|
||||
.trim() || name || '-'
|
||||
}
|
||||
|
||||
function formatMonthLabel(value: unknown) {
|
||||
const text = String(value || '')
|
||||
const match = text.match(/^(\d{4})-(\d{2})/)
|
||||
if (!match) return text
|
||||
return `${match[1]}年${Number(match[2])}月`
|
||||
}
|
||||
|
||||
function formatAverageScore(row: any) {
|
||||
const values = Object.values(row?.cells || {})
|
||||
.map((cell: any) => Number(cell?.score))
|
||||
.filter((value) => Number.isFinite(value))
|
||||
if (values.length === 0) return '-'
|
||||
const average = values.reduce((sum, value) => sum + value, 0) / values.length
|
||||
return formatNullableScore(average)
|
||||
}
|
||||
|
||||
function formatNullableScore(value: unknown) {
|
||||
if (value === null || value === undefined || value === '') return '-'
|
||||
const numberValue = Number(value)
|
||||
@ -1010,7 +1113,7 @@ function SingleEmployeeFilter({
|
||||
onChange={(event) => setKeyword(event.target.value)}
|
||||
onFocus={() => keyword.trim() && setOpen(true)}
|
||||
placeholder={value ? value.displayName : '筛选人员'}
|
||||
className="h-11 w-full rounded-[12px] border border-[#dbe5f2] bg-white px-3 text-sm font-medium text-[#101828] outline-none transition-colors focus:border-[#1677ff] focus:ring-4 focus:ring-[#1677ff]/10 dark:border-gray-700 dark:bg-gray-950 dark:text-white"
|
||||
className="h-10 w-full rounded border border-[#e2e2e2] bg-white px-3 text-[13px] font-semibold text-[#1a1c1c] outline-none transition-colors focus:border-[#0050cb]"
|
||||
/>
|
||||
{value && (
|
||||
<button
|
||||
@ -1019,17 +1122,17 @@ function SingleEmployeeFilter({
|
||||
onChange(null)
|
||||
setKeyword('')
|
||||
}}
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 rounded-full px-2 py-1 text-xs font-semibold text-[#667085] hover:bg-[#f3f6fb]"
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 px-2 py-1 text-[12px] font-semibold text-[#5f5e5e] hover:text-[#1a1c1c]"
|
||||
>
|
||||
清除
|
||||
</button>
|
||||
)}
|
||||
{open && (
|
||||
<div className="absolute z-20 mt-2 max-h-56 w-full overflow-y-auto rounded-[14px] border border-[#dbe5f2] bg-white shadow-[0_14px_34px_rgba(15,23,42,0.10)] dark:border-gray-700 dark:bg-gray-900">
|
||||
<div className="absolute z-20 mt-1 max-h-56 w-full overflow-y-auto border border-[#e2e2e2] bg-white shadow-[0_10px_24px_rgba(0,0,0,0.08)]">
|
||||
{loading ? (
|
||||
<div className="px-3 py-2 text-sm text-[#667085]">搜索中...</div>
|
||||
<div className="px-3 py-2 text-sm text-[#5f5e5e]">搜索中...</div>
|
||||
) : items.length === 0 ? (
|
||||
<div className="px-3 py-2 text-sm text-[#667085]">无匹配人员</div>
|
||||
<div className="px-3 py-2 text-sm text-[#5f5e5e]">无匹配人员</div>
|
||||
) : (
|
||||
items.map((employee) => (
|
||||
<button
|
||||
@ -1040,10 +1143,10 @@ function SingleEmployeeFilter({
|
||||
setKeyword('')
|
||||
setOpen(false)
|
||||
}}
|
||||
className="block w-full px-3 py-2 text-left text-sm hover:bg-[#f6f9ff] dark:hover:bg-gray-800"
|
||||
className="block w-full px-3 py-2 text-left text-sm hover:bg-[#f9f9f9]"
|
||||
>
|
||||
<div className="font-semibold text-[#101828] dark:text-white">{employee.displayName}</div>
|
||||
<div className="mt-0.5 truncate text-xs text-[#667085]">{employee.departmentName || ''}</div>
|
||||
<div className="font-semibold text-[#1a1c1c]">{employee.displayName}</div>
|
||||
<div className="mt-0.5 truncate text-xs text-[#5f5e5e]">{employee.departmentName || ''}</div>
|
||||
</button>
|
||||
))
|
||||
)}
|
||||
@ -1478,8 +1581,8 @@ function DepartmentFilterTree({
|
||||
return (
|
||||
<div key={department.id}>
|
||||
<div
|
||||
className={`flex min-h-9 items-center gap-2 rounded-md px-2 py-1.5 text-sm ${
|
||||
selected ? 'bg-[#edf4ff] text-[#0052d9]' : 'text-gray-800 hover:bg-gray-50 dark:text-gray-100 dark:hover:bg-gray-700'
|
||||
className={`flex min-h-9 items-center gap-2 rounded px-2 py-1.5 text-sm ${
|
||||
selected ? 'bg-[#dae1ff] text-[#0050cb]' : 'text-[#1a1c1c] hover:bg-[#f9f9f9]'
|
||||
}`}
|
||||
style={{ paddingLeft: `${8 + depth * 18}px` }}
|
||||
>
|
||||
@ -1488,7 +1591,7 @@ function DepartmentFilterTree({
|
||||
onClick={() => hasChildren && toggle(department.id)}
|
||||
className={`flex h-6 w-6 shrink-0 items-center justify-center rounded border text-xs ${
|
||||
hasChildren
|
||||
? 'border-gray-300 text-gray-600 hover:bg-gray-100 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-600'
|
||||
? 'border-[#e2e2e2] text-[#5f5e5e] hover:bg-[#f4f3f3]'
|
||||
: 'border-transparent text-transparent'
|
||||
}`}
|
||||
aria-label={hasChildren ? `${isExpanded ? '收起' : '展开'}${department.name}` : undefined}
|
||||
@ -1516,24 +1619,24 @@ function DepartmentFilterTree({
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpen((current) => !current)}
|
||||
className="flex h-11 w-full items-center justify-between gap-2 rounded-[12px] border border-[#dbe5f2] bg-white px-3 text-left text-sm font-semibold text-[#101828] outline-none transition-colors hover:border-[#b9c7dc] dark:border-gray-700 dark:bg-gray-950 dark:text-white"
|
||||
className="flex h-10 w-full items-center justify-between gap-2 rounded border border-[#eeeeee] bg-white px-3 text-left text-[13px] font-semibold text-[#1a1c1c] outline-none transition-colors hover:border-[#c2c6d8]"
|
||||
>
|
||||
<span className="truncate">{selectedLabel}</span>
|
||||
<span className="text-xs text-[#98a2b3]">{open ? '收起' : '选择'}</span>
|
||||
<span className="text-xs text-[#5f5e5e]">{open ? '收起' : '选择'}</span>
|
||||
</button>
|
||||
{open && (
|
||||
<div className="absolute left-0 right-0 z-30 mt-2 rounded-[16px] border border-[#e6ebf2] bg-white p-2 shadow-[0_18px_42px_rgba(15,23,42,0.12)] dark:border-gray-700 dark:bg-gray-900">
|
||||
<div className="absolute left-0 right-0 z-30 mt-2 border border-[#eeeeee] bg-white p-2 shadow-[0_10px_28px_rgba(0,0,0,0.08)]">
|
||||
<input
|
||||
value={keyword}
|
||||
onChange={(event) => setKeyword(event.target.value)}
|
||||
placeholder="搜索组织"
|
||||
className="mb-2 h-10 w-full rounded-[12px] border border-[#dbe5f2] px-3 text-sm text-[#101828] outline-none focus:border-[#1677ff] focus:ring-4 focus:ring-[#1677ff]/10 dark:border-gray-700 dark:bg-gray-950 dark:text-white"
|
||||
className="mb-2 h-9 w-full rounded border border-[#eeeeee] px-3 text-sm text-[#1a1c1c] outline-none focus:border-[#0050cb]"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => choose('')}
|
||||
className={`mb-1 flex min-h-9 w-full items-center rounded-md px-3 text-left text-sm font-medium ${
|
||||
!value ? 'bg-[#edf4ff] text-[#0052d9]' : 'text-gray-800 hover:bg-gray-50 dark:text-gray-100 dark:hover:bg-gray-700'
|
||||
className={`mb-1 flex min-h-9 w-full items-center rounded px-3 text-left text-sm font-semibold ${
|
||||
!value ? 'bg-[#dae1ff] text-[#0050cb]' : 'text-[#1a1c1c] hover:bg-[#f9f9f9]'
|
||||
}`}
|
||||
>
|
||||
全部组织
|
||||
@ -1603,7 +1706,7 @@ function ManagePanel() {
|
||||
}}
|
||||
/>
|
||||
{message && (
|
||||
<div className="rounded-[14px] border border-[#cfe0ff] bg-white px-4 py-3 text-sm font-semibold text-[#0052d9] shadow-[0_8px_18px_rgba(15,23,42,0.04)] dark:border-blue-500/30 dark:bg-gray-900 dark:text-blue-200">
|
||||
<div className="border border-[#b3c5ff] bg-[#f8faff] px-4 py-3 text-sm font-semibold text-[#0050cb]">
|
||||
{message}
|
||||
</div>
|
||||
)}
|
||||
@ -1735,51 +1838,63 @@ function CreateSurveyForm({
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="rounded-[16px] border border-[#eaedf3] bg-white px-4 py-4 shadow-[0_8px_22px_rgba(15,23,42,0.04)] dark:border-gray-800 dark:bg-gray-900 sm:px-6 sm:py-5">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<h2 className="text-[18px] font-semibold text-gray-900 dark:text-white">发起考核</h2>
|
||||
</div>
|
||||
<div className="mt-5 grid gap-4 lg:grid-cols-2">
|
||||
<TextInput
|
||||
label="月份"
|
||||
value={dueAt}
|
||||
onChange={setDueAt}
|
||||
type="month"
|
||||
/>
|
||||
<label className="grid gap-1 text-sm">
|
||||
<span className="font-medium text-gray-700 dark:text-gray-300">模板</span>
|
||||
<section className="grid gap-10 sm:grid-cols-[360px_minmax(0,1fr)]">
|
||||
<div className="space-y-6">
|
||||
<div className="border border-[#eeeeee] bg-white p-6">
|
||||
<div className="space-y-6">
|
||||
<TextInput label="考核月份" value={dueAt} onChange={setDueAt} type="month" />
|
||||
<label className="grid gap-2 text-sm">
|
||||
<span className="text-[12px] font-semibold tracking-[0.05em] text-[#5f5e5e]">考评模板</span>
|
||||
<select
|
||||
value={selectedTemplate}
|
||||
onChange={(event) => setTemplateCode(event.target.value)}
|
||||
className="h-11 rounded-[12px] border border-[#dbe5f2] bg-white px-3 text-sm font-semibold text-gray-900 outline-none transition-colors focus:border-[#1677ff] focus:ring-4 focus:ring-[#1677ff]/10 dark:border-gray-700 dark:bg-gray-950 dark:text-white"
|
||||
className="h-10 rounded border border-[#eeeeee] bg-white px-3 text-[14px] font-semibold text-[#1a1c1c] outline-none transition-colors focus:border-[#0050cb]"
|
||||
>
|
||||
{templates.map((template) => (
|
||||
<option key={template.code} value={template.code}>
|
||||
{template.shortName || template.name}
|
||||
{getTemplateDisplayName(template)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
{surveyKind !== 'item_course' ? (
|
||||
<div className="lg:col-span-2">
|
||||
<EmployeePicker title={selectedTemplateInfo?.ownerRoleName || '被考核人'} purpose="target" selected={targetEmployees} onChange={setTargetEmployees} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && <div className="border border-[#ffdad6] bg-[#fff6f4] px-4 py-3 text-sm font-semibold text-[#93000a]">{error}</div>}
|
||||
<button
|
||||
disabled={saving}
|
||||
onClick={submit}
|
||||
className="h-12 w-full rounded bg-[#0050cb] px-5 text-[15px] font-bold text-white hover:bg-[#003fa4] disabled:opacity-50"
|
||||
>
|
||||
{saving ? '发布中...' : '发布考核计划'}
|
||||
</button>
|
||||
{createdSurvey && (
|
||||
<div className="border border-[#b3c5ff] bg-[#f8faff] px-4 py-3 text-sm font-semibold text-[#0050cb]">
|
||||
已发布
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="border border-[#eeeeee] bg-white p-6">
|
||||
<div className="grid gap-8 lg:grid-cols-2">
|
||||
<div>
|
||||
{surveyKind !== 'item_course' ? (
|
||||
<EmployeePicker title={selectedTemplateInfo?.ownerRoleName || '被考核人员'} purpose="target" selected={targetEmployees} onChange={setTargetEmployees} />
|
||||
) : (
|
||||
<div className="lg:col-span-2">
|
||||
<div className="mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">{selectedTemplateInfo?.itemName || '调研事项'}</div>
|
||||
<div>
|
||||
<div className="mb-2 text-[12px] font-semibold tracking-[0.05em] text-[#5f5e5e]">{selectedTemplateInfo?.itemName || '调研事项'}</div>
|
||||
<input
|
||||
value={itemName}
|
||||
onChange={(event) => setItemName(event.target.value)}
|
||||
placeholder={selectedTemplateInfo?.itemNamePlaceholder || '填写要评价的课程、事项或活动名称'}
|
||||
className="h-11 w-full rounded-[12px] border border-[#dbe5f2] px-3 text-sm font-medium outline-none transition-colors focus:border-[#1677ff] focus:ring-4 focus:ring-[#1677ff]/10 dark:border-gray-700 dark:bg-gray-950 dark:text-white"
|
||||
placeholder={selectedTemplateInfo?.itemNamePlaceholder || '填写名称'}
|
||||
className="h-10 w-full rounded border border-[#eeeeee] px-3 text-[14px] font-semibold outline-none transition-colors focus:border-[#0050cb]"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="lg:col-span-2">
|
||||
</div>
|
||||
<div>
|
||||
<EmployeePicker
|
||||
title="评价人"
|
||||
title="评分人员"
|
||||
purpose="evaluator"
|
||||
selected={evaluatorEmployees}
|
||||
onChange={setManualEvaluatorEmployees}
|
||||
@ -1805,19 +1920,7 @@ function CreateSurveyForm({
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{error && <div className="rounded-lg bg-red-50 px-3 py-2 text-sm text-red-700 dark:bg-red-900/20 dark:text-red-200">{error}</div>}
|
||||
<button
|
||||
disabled={saving}
|
||||
onClick={submit}
|
||||
className="min-h-11 rounded-[12px] bg-[#0052d9] px-5 text-sm font-semibold text-white shadow-[0_10px_22px_rgba(0,82,217,0.16)] hover:bg-[#003da6] disabled:opacity-50 dark:bg-white dark:text-gray-900 lg:col-span-2 lg:w-[220px]"
|
||||
>
|
||||
{saving ? '发布中...' : '发布考核'}
|
||||
</button>
|
||||
{createdSurvey && (
|
||||
<div className="rounded-[12px] border border-blue-100 bg-blue-50 px-3 py-2.5 text-sm font-medium text-blue-800 dark:border-blue-900/50 dark:bg-blue-950/30 dark:text-blue-100 lg:col-span-2">
|
||||
已发布
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
@ -1915,8 +2018,8 @@ function EmployeePicker({
|
||||
|
||||
return (
|
||||
<div ref={pickerRef}>
|
||||
<label className="grid gap-1 text-sm">
|
||||
<span className="font-medium text-gray-700 dark:text-gray-300">{title}</span>
|
||||
<label className="grid gap-2 text-sm">
|
||||
<span className="text-[12px] font-semibold tracking-[0.05em] text-[#5f5e5e]">{title}</span>
|
||||
<input
|
||||
value={keyword}
|
||||
onFocus={() => {
|
||||
@ -1930,17 +2033,17 @@ function EmployeePicker({
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Escape') setOpen(false)
|
||||
}}
|
||||
className="h-11 rounded-[12px] border border-[#dbe5f2] bg-white px-3 text-sm font-medium text-gray-900 outline-none transition-colors focus:border-[#1677ff] focus:ring-4 focus:ring-[#1677ff]/10 dark:border-gray-700 dark:bg-gray-950 dark:text-white"
|
||||
className="h-10 rounded border border-[#eeeeee] bg-white px-3 text-[14px] font-semibold text-[#1a1c1c] outline-none transition-colors focus:border-[#0050cb]"
|
||||
/>
|
||||
</label>
|
||||
{open && keyword && (
|
||||
<div className="mt-2 max-h-56 overflow-y-auto rounded-[14px] border border-[#dbe5f2] bg-white shadow-[0_14px_34px_rgba(15,23,42,0.10)] dark:border-gray-700 dark:bg-gray-900">
|
||||
<div className="mt-2 max-h-56 overflow-y-auto border border-[#eeeeee] bg-white shadow-[0_8px_18px_rgba(0,0,0,0.06)]">
|
||||
{loading ? (
|
||||
<div className="px-3 py-2 text-sm text-gray-500">搜索中...</div>
|
||||
<div className="px-3 py-2 text-sm text-[#5f5e5e]">搜索中...</div>
|
||||
) : error ? (
|
||||
<div className="px-3 py-2 text-sm text-red-600">{error}</div>
|
||||
) : items.length === 0 ? (
|
||||
<div className="px-3 py-2 text-sm text-gray-500">无匹配人员</div>
|
||||
<div className="px-3 py-2 text-sm text-[#5f5e5e]">无匹配人员</div>
|
||||
) : (
|
||||
items.map((employee) => (
|
||||
<button
|
||||
@ -1948,10 +2051,10 @@ function EmployeePicker({
|
||||
type="button"
|
||||
disabled={selectedIds.has(employee.id)}
|
||||
onClick={() => selectEmployee(employee)}
|
||||
className="w-full px-3 py-2.5 text-left text-sm transition-colors hover:bg-[#f6f9ff] disabled:opacity-50 dark:hover:bg-gray-800"
|
||||
className="w-full px-3 py-2.5 text-left text-sm transition-colors hover:bg-[#f9f9f9] disabled:opacity-50"
|
||||
>
|
||||
<div className="font-medium text-gray-900 dark:text-white">{employee.displayName}</div>
|
||||
<div className="mt-0.5 truncate text-xs text-gray-500 dark:text-gray-400">
|
||||
<div className="font-semibold text-[#1a1c1c]">{employee.displayName}</div>
|
||||
<div className="mt-0.5 truncate text-xs text-[#5f5e5e]">
|
||||
{[
|
||||
employee.departmentName,
|
||||
employee.positionTitle,
|
||||
@ -2113,15 +2216,15 @@ function DepartmentPicker({
|
||||
return (
|
||||
<div key={department.id}>
|
||||
<div
|
||||
className="flex min-h-10 items-center gap-2 rounded-[10px] px-2 py-1.5 text-sm transition-colors hover:bg-[#f6f9ff] dark:hover:bg-gray-800"
|
||||
className="flex min-h-9 items-center gap-2 px-2 py-1.5 text-sm transition-colors hover:bg-[#f9f9f9]"
|
||||
style={{ paddingLeft: `${8 + depth * 18}px` }}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => hasChildren && toggle(department.id)}
|
||||
className={`flex h-6 w-6 shrink-0 items-center justify-center rounded-[8px] border text-xs ${
|
||||
className={`flex h-6 w-6 shrink-0 items-center justify-center border text-xs ${
|
||||
hasChildren
|
||||
? 'border-[#dbe5f2] text-[#667085] hover:bg-[#f3f6fb] dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-700'
|
||||
? 'border-[#e2e2e2] text-[#5f5e5e] hover:bg-[#f4f3f3]'
|
||||
: 'border-transparent text-transparent'
|
||||
}`}
|
||||
aria-label={hasChildren ? `${isExpanded ? '收起' : '展开'}${department.name}` : undefined}
|
||||
@ -2130,7 +2233,7 @@ function DepartmentPicker({
|
||||
{hasChildren ? (isExpanded ? '-' : '+') : ''}
|
||||
</button>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate font-medium text-gray-800 dark:text-gray-100">{department.name}</div>
|
||||
<div className="truncate font-semibold text-[#1a1c1c]">{department.name}</div>
|
||||
</div>
|
||||
{department.selectable !== false && department.value && (
|
||||
<input
|
||||
@ -2144,7 +2247,7 @@ function DepartmentPicker({
|
||||
/>
|
||||
)}
|
||||
{department.value && loadingValue === department.value && (
|
||||
<span className="text-xs text-gray-400 dark:text-gray-500">加载中</span>
|
||||
<span className="text-xs text-[#5f5e5e]">加载中</span>
|
||||
)}
|
||||
</div>
|
||||
{hasChildren && isExpanded && children.map((child) => renderNode(child, depth + 1))}
|
||||
@ -2157,7 +2260,7 @@ function DepartmentPicker({
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-2 flex items-center justify-between gap-3">
|
||||
<div className="text-sm font-medium text-gray-700 dark:text-gray-300">部门</div>
|
||||
<div className="text-[12px] font-semibold tracking-[0.05em] text-[#5f5e5e]">部门</div>
|
||||
{selected.length > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
@ -2166,7 +2269,7 @@ function DepartmentPicker({
|
||||
onChange([])
|
||||
setError(null)
|
||||
}}
|
||||
className="text-xs text-gray-500 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white"
|
||||
className="text-xs font-semibold text-[#5f5e5e] hover:text-[#1a1c1c]"
|
||||
>
|
||||
清空
|
||||
</button>
|
||||
@ -2176,12 +2279,12 @@ function DepartmentPicker({
|
||||
value={keyword}
|
||||
onChange={(event) => setKeyword(event.target.value)}
|
||||
placeholder="搜索部门"
|
||||
className="mb-2 h-11 w-full rounded-[12px] border border-[#dbe5f2] bg-white px-3 text-sm font-medium text-gray-900 outline-none transition-colors focus:border-[#1677ff] focus:ring-4 focus:ring-[#1677ff]/10 dark:border-gray-700 dark:bg-gray-950 dark:text-white"
|
||||
className="mb-2 h-10 w-full rounded border border-[#eeeeee] bg-white px-3 text-[13px] font-semibold text-[#1a1c1c] outline-none transition-colors focus:border-[#0050cb]"
|
||||
/>
|
||||
<div className="max-h-64 overflow-y-auto rounded-[14px] border border-[#dbe5f2] bg-white p-1.5 dark:border-gray-700 dark:bg-gray-950">
|
||||
<div className="max-h-64 overflow-y-auto border border-[#eeeeee] bg-white p-1.5">
|
||||
{roots.map((department) => renderNode(department))}
|
||||
</div>
|
||||
{error && <div className="mt-2 text-xs text-red-600 dark:text-red-300">{error}</div>}
|
||||
{error && <div className="mt-2 text-xs font-semibold text-[#93000a]">{error}</div>}
|
||||
{selectedLabels.length > 0 && (
|
||||
<div className="mt-2 flex flex-wrap gap-2">
|
||||
{selectedLabels.map((department) => (
|
||||
@ -2191,7 +2294,7 @@ function DepartmentPicker({
|
||||
onClick={() => {
|
||||
if (department.value) void toggleValue(department.value, false)
|
||||
}}
|
||||
className="rounded-full border border-[#dbe5f2] bg-white px-3 py-1.5 text-sm font-medium text-gray-700 hover:bg-[#f6f9ff] dark:border-gray-700 dark:bg-gray-900 dark:text-gray-200"
|
||||
className="border border-[#e2e2e2] bg-white px-2.5 py-1 text-xs font-semibold text-[#1a1c1c] hover:bg-[#f9f9f9]"
|
||||
>
|
||||
{department.fullPath || department.departmentPath || (
|
||||
department.parentName && department.parentName !== '全部部门'
|
||||
@ -3299,14 +3402,14 @@ function TextInput({
|
||||
placeholder?: string
|
||||
}) {
|
||||
return (
|
||||
<label className="grid gap-1 text-sm">
|
||||
<span className="font-medium text-gray-700 dark:text-gray-300">{label}</span>
|
||||
<label className="grid gap-2 text-sm">
|
||||
<span className="text-[12px] font-semibold tracking-[0.05em] text-[#5f5e5e]">{label}</span>
|
||||
<input
|
||||
type={type}
|
||||
value={value}
|
||||
onChange={(event) => onChange(event.target.value)}
|
||||
placeholder={placeholder}
|
||||
className="h-11 rounded-[12px] border border-[#dbe5f2] bg-white px-3 text-sm font-medium text-gray-900 outline-none transition-colors focus:border-[#1677ff] focus:ring-4 focus:ring-[#1677ff]/10 dark:border-gray-700 dark:bg-gray-950 dark:text-white"
|
||||
className="h-10 rounded border border-[#eeeeee] bg-white px-3 text-[14px] font-semibold text-[#1a1c1c] outline-none transition-colors focus:border-[#0050cb]"
|
||||
/>
|
||||
</label>
|
||||
)
|
||||
@ -3321,7 +3424,7 @@ function SelectedChips({ items, onRemove }: { items: Array<{ id: string; label:
|
||||
key={item.id}
|
||||
type="button"
|
||||
onClick={() => onRemove(item.id)}
|
||||
className="rounded-full border border-[#dbe5f2] bg-white px-3 py-1.5 text-sm font-medium text-gray-700 hover:bg-[#f6f9ff] dark:border-gray-700 dark:bg-gray-900 dark:text-gray-200"
|
||||
className="border border-[#e2e2e2] bg-white px-2.5 py-1 text-xs font-semibold text-[#1a1c1c] hover:bg-[#f9f9f9]"
|
||||
>
|
||||
{item.label} ×
|
||||
</button>
|
||||
|
||||
85
design-qa.md
85
design-qa.md
@ -1,35 +1,58 @@
|
||||
**视觉来源**
|
||||
- Stitch 专案:`https://stitch.withgoogle.com/projects/10867034024263334547`
|
||||
- 本次对齐的设计稿:
|
||||
- `个人看板桌面端-优化版`
|
||||
- `公共看板桌面端-优化版`
|
||||
- `分数详情抽屉桌面端-优化版`
|
||||
- `发起考核桌面端-优化版`
|
||||
- 既有移动端稿:`个人看板` / `公共看板` / `详情弹层`
|
||||
**Findings**
|
||||
- No actionable P0/P1/P2 issues remain for the current Stitch alignment pass.
|
||||
|
||||
**实现范围**
|
||||
- 文件:`app/performance-survey/[[...path]]/page.tsx`
|
||||
- 页面:`/performance-survey`
|
||||
- 主入口:
|
||||
- `tab=personal`
|
||||
- `tab=public`
|
||||
- `tab=manage`
|
||||
**Source Visual Truth**
|
||||
- Personal desktop: `D:\AI Code\gongxiangp2\文件\stitch_\stitch_\_7\screen.png`
|
||||
- Public desktop: `D:\AI Code\gongxiangp2\文件\stitch_\stitch_\_1\screen.png`
|
||||
- Create desktop: `D:\AI Code\gongxiangp2\文件\stitch_\stitch_\_2\screen.png`
|
||||
- Personal mobile: `D:\AI Code\gongxiangp2\文件\stitch_\stitch_\_10\screen.png`
|
||||
- Public mobile: `D:\AI Code\gongxiangp2\文件\stitch_\stitch_\_11\screen.png`
|
||||
|
||||
**验证结果**
|
||||
- Lint:`npm run lint -- --file "app/performance-survey/[[...path]]/page.tsx"` 通过。
|
||||
- 空白检查:`git diff --check -- "app/performance-survey/[[...path]]/page.tsx"` 通过。
|
||||
- 浏览器验证:
|
||||
- 桌面端个人看板已切换为月度台账外壳,没有旧任务标签。
|
||||
- 桌面端公共看板已切换为公共筛选 + 台账外壳,没有旧任务标签。
|
||||
- 桌面端发起考核只保留月份、模板、被考核人、评价人、发布按钮。
|
||||
- 移动端个人/公共/发起考核为白色画布,不再有淡紫色页面背景。
|
||||
- 页面未出现二维码、维护、通知记录、旧四标签工作流、模板占比说明等噪音。
|
||||
**Implementation Screenshots**
|
||||
- Personal desktop: `D:\AI Code\gongxiangp2\outputs\performance-survey-stitch-qa\personal-desktop.png`
|
||||
- Public desktop: `D:\AI Code\gongxiangp2\outputs\performance-survey-stitch-qa\public-desktop.png`
|
||||
- Create desktop: `D:\AI Code\gongxiangp2\outputs\performance-survey-stitch-qa\manage-desktop.png`
|
||||
- Personal mobile: `D:\AI Code\gongxiangp2\outputs\performance-survey-stitch-qa\personal-mobile.png`
|
||||
- Public mobile: `D:\AI Code\gongxiangp2\outputs\performance-survey-stitch-qa\public-mobile.png`
|
||||
- Create mobile: `D:\AI Code\gongxiangp2\outputs\performance-survey-stitch-qa\manage-mobile.png`
|
||||
|
||||
**检查项**
|
||||
- 新看板不再展示 `待处理 / 已处理 / 我发起的 / 我的结果` 旧工作流入口。
|
||||
- 公共看板渲染前会过滤无数据人员。
|
||||
- 发起考核页不再请求旧调研列表。
|
||||
- 表单控件统一为白底、细边框、克制蓝色焦点样式。
|
||||
- 桌面表格和移动卡片都保留四模板月度台账模型。
|
||||
**Viewport And State**
|
||||
- Desktop: `1280 x 720`, routes `tab=personal`, `tab=public`, `tab=manage`.
|
||||
- Mobile: `430 x 900`, routes `tab=personal`, `tab=public`, `tab=manage`.
|
||||
- Data state: local authenticated state with the currently available local data; empty ledgers are expected where the local dataset has no rows.
|
||||
|
||||
最终结果:通过
|
||||
**Full-View Comparison Evidence**
|
||||
- Header/nav: dashboard pages now use one unified material-platform dark app bar with left avatar/title, centered tabs on desktop, and right brand text. The previous personal/public/create color split has been removed per the latest product direction.
|
||||
- Layout: dashboard content is constrained to `1200px` on desktop, matching the Stitch public/create composition and avoiding edge-to-edge content at common desktop widths.
|
||||
- Tables/cards: monthly ledger and public ledger use flat bordered tables/cards with minimal blue score emphasis. Empty states are flat dashed containers.
|
||||
- Forms: create page uses the two-zone Stitch structure: month/template on the left, assessed people/evaluators/department on the right. Inputs, selects, search dropdowns, and selected chips use white fill and thin borders.
|
||||
- Mobile: mobile pages no longer show purple background, duplicate header elements, or old workflow stat cards.
|
||||
|
||||
**Focused Region Comparison**
|
||||
- Header region: compared against `_7`, `_1`, and `_2`; active tab and brand placement follow Stitch, while color mode is intentionally unified to the native material-platform dark bar.
|
||||
- Filter/form controls: compared against `_1` and `_2`; controls use thin borders, square/low-radius shape, and restrained blue focus/active states.
|
||||
- Mobile top and bottom navigation: compared against `_10` and `_11`; app-level navigation is simplified and no longer conflicts with content.
|
||||
|
||||
**Patches Made Since Previous QA Pass**
|
||||
- Removed the global platform header from `/performance-survey` dashboard pages.
|
||||
- Moved dashboard navigation into a unified dark top app bar.
|
||||
- Reworked dashboard containers to a 1200px white, low-noise layout.
|
||||
- Restyled public filters, employee search, department tree, selected chips, and score detail drawer.
|
||||
- Restyled create form to match the Stitch two-column composition.
|
||||
- Removed noisy mobile identity line and fake icon glyphs from bottom navigation.
|
||||
- Removed the earlier white desktop header branch for public/create states.
|
||||
|
||||
**Open Questions**
|
||||
- Current local data is sparse/empty, so row-rich table states were verified structurally rather than with dense production-like rows.
|
||||
|
||||
**Implementation Checklist**
|
||||
- Header duplication removed.
|
||||
- Personal, public, and create routes visually verified in desktop.
|
||||
- Personal, public, and create routes visually verified in mobile.
|
||||
- Page-specific controls remain interactive and data-backed.
|
||||
- `npm run lint -- --file "app/performance-survey/[[...path]]/page.tsx"` passed.
|
||||
|
||||
**Follow-up Polish**
|
||||
- If the team wants exact mobile icon parity, add a real icon font/library through the app asset pipeline instead of text glyphs or CSS icons.
|
||||
|
||||
final result: passed
|
||||
|
||||
BIN
outputs/performance-survey-stitch-qa/manage-desktop.png
Normal file
BIN
outputs/performance-survey-stitch-qa/manage-desktop.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
BIN
outputs/performance-survey-stitch-qa/manage-mobile.png
Normal file
BIN
outputs/performance-survey-stitch-qa/manage-mobile.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
BIN
outputs/performance-survey-stitch-qa/personal-desktop.png
Normal file
BIN
outputs/performance-survey-stitch-qa/personal-desktop.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
BIN
outputs/performance-survey-stitch-qa/personal-mobile.png
Normal file
BIN
outputs/performance-survey-stitch-qa/personal-mobile.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
BIN
outputs/performance-survey-stitch-qa/public-desktop.png
Normal file
BIN
outputs/performance-survey-stitch-qa/public-desktop.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
BIN
outputs/performance-survey-stitch-qa/public-mobile.png
Normal file
BIN
outputs/performance-survey-stitch-qa/public-mobile.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
Loading…
x
Reference in New Issue
Block a user