diff --git a/client/src/components/common/PageHeader.tsx b/client/src/components/common/PageHeader.tsx index d70f9c30..f9e388cb 100644 --- a/client/src/components/common/PageHeader.tsx +++ b/client/src/components/common/PageHeader.tsx @@ -18,8 +18,8 @@ export function PageHeader({ title, subtitle, actions, eyebrow, meta, className {eyebrow ? (
{eyebrow}
) : null} -

{title}

- {subtitle ?

{subtitle}

: null} +

{title}

+ {subtitle ?

{subtitle}

: null} {meta ?
{meta}
: null} {actions ?
{actions}
: null} diff --git a/client/src/components/navigation/Topbar.tsx b/client/src/components/navigation/Topbar.tsx index ff7c3bb3..381e2998 100644 --- a/client/src/components/navigation/Topbar.tsx +++ b/client/src/components/navigation/Topbar.tsx @@ -166,7 +166,7 @@ export function Topbar() {
{pageMeta.group}
-

{pageMeta.title}

+

{pageMeta.title}

{wecomEntry ? ( {title} {subtitle ?

{subtitle}

: null}
- {actions ?
{actions}
: null} + {actions ?
{actions}
: null} ); } diff --git a/client/src/components/shell/AppBrand.tsx b/client/src/components/shell/AppBrand.tsx index 69705719..d6695885 100644 --- a/client/src/components/shell/AppBrand.tsx +++ b/client/src/components/shell/AppBrand.tsx @@ -7,8 +7,8 @@ export function AppBrand({ compact = false }: { compact?: boolean }) {
-
海音达工作台
-
Hyinda OA · 小团队运营中枢
+
海音达工作台
+
审批 报销 团队
); diff --git a/client/src/layouts/AppShellLayout.tsx b/client/src/layouts/AppShellLayout.tsx index 87ca44e2..d1adf7a6 100644 --- a/client/src/layouts/AppShellLayout.tsx +++ b/client/src/layouts/AppShellLayout.tsx @@ -26,7 +26,6 @@ export function AppShellLayout() { return (
-
diff --git a/client/src/layouts/AuthLayout.tsx b/client/src/layouts/AuthLayout.tsx index 014261f8..0fdac154 100644 --- a/client/src/layouts/AuthLayout.tsx +++ b/client/src/layouts/AuthLayout.tsx @@ -4,17 +4,11 @@ import { AppBrand } from "../components/shell/AppBrand"; export function AuthLayout() { return (
-
-
+
+
-
-
统一入口
-

海音达 OA

-

打开后直接处理今天的审批、报销和团队事项。

-
-
- -
+
+
diff --git a/client/src/pages/CutoverConsolePage.tsx b/client/src/pages/CutoverConsolePage.tsx index 071dab1f..91cc6c0d 100644 --- a/client/src/pages/CutoverConsolePage.tsx +++ b/client/src/pages/CutoverConsolePage.tsx @@ -242,7 +242,7 @@ export function CutoverConsolePage() { onSuccess: async () => { setDryRunFile(null); await refresh(); - toast.success("dry-run 已完成"); + toast.success("演练已完成"); }, onError: (error) => { toast.error(resolveErrorMessage(error)); @@ -319,32 +319,32 @@ export function CutoverConsolePage() { return (
- +
已启用
{summary.active}
-
enabled=true 且已到生效时间
+
已到生效时间
已禁用
{summary.disabled}
-
enabled=false
+
手动关闭
待生效
{summary.pending}
-
enabled=true 且 startsAt > now
+
等待生效时间
- 新增 scope + 新增范围 } > @@ -368,9 +368,9 @@ export function CutoverConsolePage() { 类型 - 范围 Key + 范围标识 状态 - startsAt + 生效时间 倒计时 备注 操作 @@ -453,10 +453,10 @@ export function CutoverConsolePage() {
- +
- +
- + - 启用该 scope + 启用该范围
diff --git a/client/src/pages/ErpIntegrationPage.tsx b/client/src/pages/ErpIntegrationPage.tsx index 1f6b0517..aa736d3b 100644 --- a/client/src/pages/ErpIntegrationPage.tsx +++ b/client/src/pages/ErpIntegrationPage.tsx @@ -238,7 +238,7 @@ export function ErpIntegrationPage() { queryClient.invalidateQueries({ queryKey: ["integration-outbox-stats"] }), queryClient.invalidateQueries({ queryKey: ["integration-outbox-detail"] }), ]); - toast.success("Outbox 事件已重置为待处理"); + toast.success("同步任务已重置为待处理"); }, onError: (mutationError) => { const message = resolveError(mutationError); @@ -262,7 +262,7 @@ export function ErpIntegrationPage() {
syncMutation.mutate(undefined)} disabled={syncMutation.isPending}> @@ -279,7 +279,7 @@ export function ErpIntegrationPage() { setTab(value as ErpTab)}> 主数据同步 - Outbox 队列 + 同步队列 ID 映射 @@ -303,14 +303,14 @@ export function ErpIntegrationPage() {
{formatGateReason(gate.effectiveReason)}
-
Provider / 主数据端点
+
同步服务 / 主数据端点
{gate.provider}
{gate.masterDataEndpointConfigured ? "主数据端点已配置" : "主数据端点未配置"}
-
凭证端点 / Cron
+
凭证端点 / 自动同步
{gate.journalEndpointConfigured ? "凭证端点已配置" : "凭证端点未配置"}
@@ -372,9 +372,9 @@ export function ErpIntegrationPage() {

说明

-

- 本页只负责 ERP 主数据同步、Outbox 队列和 ID 映射,不承担业务单据本身的录入。

-

- 报销、采购和资产的 ERP 同步动作请前往对应业务详情区执行。

-

- 当前阶段不接入真实 ERP Provider,仍以 mock Provider 为准。

+

- 本页只负责主数据同步、业务同步队列和 ID 映射。

+

- 报销、采购和资产的同步动作在对应业务详情区执行。

+

- 当前仍使用演示同步服务。

@@ -410,8 +410,8 @@ export function ErpIntegrationPage() {
-

Outbox 事件列表

-

支持按状态和事件类型筛选,失败和死信事件可在此重试。

+

同步任务列表

+

失败任务可重试。

@@ -587,7 +587,7 @@ export function ErpIntegrationPage() { - Provider + 同步服务 实体类型 本地 ID 外部 ID @@ -636,8 +636,8 @@ export function ErpIntegrationPage() { > - Outbox 事件详情 - 查看完整 payload、错误摘要和当前队列状态。 + 同步任务详情 + 查看数据内容、错误摘要和当前队列状态。 {outboxDetailQuery.isLoading ?

详情加载中...

: null} @@ -650,7 +650,7 @@ export function ErpIntegrationPage() { {outboxDetailQuery.data ? (
-
Outbox ID:{outboxDetailQuery.data.id}
+
队列 ID:{outboxDetailQuery.data.id}
事件类型:{outboxDetailQuery.data.eventType}
状态:{outboxStatusLabel(outboxDetailQuery.data.status)}
尝试次数:{outboxDetailQuery.data.attempts} / {outboxDetailQuery.data.maxAttempts}
@@ -668,7 +668,7 @@ export function ErpIntegrationPage() {
-
Payload
+
数据内容
                   {JSON.stringify(outboxDetailQuery.data.payload ?? null, null, 2)}
                 
diff --git a/client/src/pages/InvoicePoolPage.tsx b/client/src/pages/InvoicePoolPage.tsx index 1cf6b19c..58be4906 100644 --- a/client/src/pages/InvoicePoolPage.tsx +++ b/client/src/pages/InvoicePoolPage.tsx @@ -445,7 +445,7 @@ export function InvoicePoolPage() {
diff --git a/client/src/pages/LoginPage.tsx b/client/src/pages/LoginPage.tsx index 47d60304..c6659c29 100644 --- a/client/src/pages/LoginPage.tsx +++ b/client/src/pages/LoginPage.tsx @@ -145,12 +145,11 @@ export function LoginPage() { } return ( -
-
-
统一入口
-

海音达 OA

-

- {showWecomPrimary ? "从企业微信进入,直接打开移动工作台。" : "使用账号密码进入工作台。"} +

+
+

登录工作台

+

+ {showWecomPrimary ? "企业微信进入优先。" : "账号密码登录。"}

@@ -163,13 +162,13 @@ export function LoginPage() { ) : null} {showWecomPrimary ? ( -
+
{wecomConfig && !wecomEntry ? (
- 企业微信入口暂不可用,请使用账号密码登录。 + 企业微信入口不可用,请用账号密码。
) : null} {!showPasswordPanel ? ( diff --git a/client/src/pages/MobileApprovalsPage.tsx b/client/src/pages/MobileApprovalsPage.tsx index e626bf82..6fcece69 100644 --- a/client/src/pages/MobileApprovalsPage.tsx +++ b/client/src/pages/MobileApprovalsPage.tsx @@ -731,13 +731,6 @@ export function MobileApprovalsPage() { } const listTitle = taskTab === "pending" ? "待办任务" : "已处理任务"; - const listSubtitle = canUseRemote - ? taskTab === "pending" - ? "在线串行审批,支持进入批量处理模式。" - : "展示最近 30 天已处理任务。" - : taskTab === "pending" - ? "离线状态下仅展示已缓存待办,并支持批量入队。" - : "离线模式下暂不提供已处理历史。"; const allVisibleSelected = filteredTaskCards.length > 0 && filteredTaskCards.every((task) => selectedBatchTaskIds.includes(task.id)); @@ -745,7 +738,7 @@ export function MobileApprovalsPage() { {!isOnline ? ( @@ -766,7 +759,7 @@ export function MobileApprovalsPage() {

{listTitle}

-

{listSubtitle}

+

{filteredTaskCards.length} 条

{isOnline ? "在线" : "离线"}
@@ -865,7 +858,7 @@ export function MobileApprovalsPage() { rows={3} value={batchRejectComment} onChange={(event) => setBatchRejectComment(event.target.value)} - placeholder="批量驳回共享意见,所有选中任务将复用这条说明。" + placeholder="驳回意见。" />
) : null} @@ -927,7 +920,7 @@ export function MobileApprovalsPage() {
{task.title}
-
任务ID: {task.id}
+
任务:{task.id}
{task.subtitle}
{task.isCached ?
来源:离线缓存
: null} @@ -949,7 +942,7 @@ export function MobileApprovalsPage() {
{task.title}
-
任务ID: {task.id}
+
任务:{task.id}
{task.subtitle}
{task.reference && task.reference !== task.id ? (
业务参考:{task.reference}
@@ -962,7 +955,7 @@ export function MobileApprovalsPage() {
-

任务详情与动作

+

详情与处理

{!selectedTaskId ?

请先选择任务。

: null} diff --git a/client/src/pages/MobileCapturePage.tsx b/client/src/pages/MobileCapturePage.tsx index 21a14e3d..4b85ebfc 100644 --- a/client/src/pages/MobileCapturePage.tsx +++ b/client/src/pages/MobileCapturePage.tsx @@ -173,7 +173,7 @@ export function MobileCapturePage() { {error ? ( @@ -188,7 +188,7 @@ export function MobileCapturePage() {

选择票据

-

支持图片和 PDF,手机会优先打开相机。

+

图片或 PDF。

{isOnline ? "在线" : "离线"} diff --git a/client/src/pages/MobilePortalPage.tsx b/client/src/pages/MobilePortalPage.tsx index 75d2d354..9ae27fa7 100644 --- a/client/src/pages/MobilePortalPage.tsx +++ b/client/src/pages/MobilePortalPage.tsx @@ -1,4 +1,3 @@ -import { useEffect, useState } from "react"; import { Link } from "react-router-dom"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { toast } from "sonner"; @@ -6,33 +5,33 @@ import { clockInAttendance, clockOutAttendance, fetchMyTodayAttendance, + type AttendanceTodayResponse, } from "../api/attendance"; -import { fetchNotifications } from "../api/notifications"; -import { fetchPortalTeamStatus } from "../api/portal"; import { mapApiErrorMessage } from "../api/errorMessages"; +import { fetchNotifications } from "../api/notifications"; import { isApiError } from "../api/types"; -import { fetchMyPresence, upsertMyPresence, type PresenceItem } from "../api/presence"; import { fetchWorkflowTasks } from "../api/workflow"; import { useAuth } from "../auth/AuthContext"; import { hasAnyPermission } from "../auth/permissions"; -import { EmployeeAvatar } from "../components/common/EmployeeAvatar"; import { PageHeader } from "../components/common/PageHeader"; import { PageScaffold } from "../components/patterns/PageScaffold"; import { Button } from "../components/ui/button"; import { MOBILE_APPROVAL_PERMISSIONS, - MOBILE_ASSET_SCAN_PERMISSIONS, MOBILE_CAPTURE_PERMISSIONS, MOBILE_LEAVE_PERMISSIONS, MOBILE_REIMBURSEMENT_PERMISSIONS, } from "../features/mobile/navigation"; import { buildMobileApprovalsPath } from "../pwa"; -const QUICK_PRESENCE_OPTIONS = [ - { label: "在岗", value: "office" as const }, - { label: "居家", value: "remote" as const }, - { label: "外出", value: "out" as const }, -]; +type MobileAction = { + key: string; + label: string; + meta: string; + icon: string; + to: string; + tone?: "primary" | "warning"; +}; function resolveErrorMessage(error: unknown) { if (isApiError(error)) { @@ -44,12 +43,22 @@ function resolveErrorMessage(error: unknown) { return "请求失败,请稍后重试。"; } +function isAttendanceFeatureDisabled(error: unknown) { + return isApiError(error) && error.code === "VALID_001" && error.details?.featureFlag === "attendance_v1_disabled"; +} + function formatDateTime(value: string | null) { if (!value) { return "-"; } try { - return new Date(value).toLocaleString("zh-CN"); + return new Date(value).toLocaleString("zh-CN", { + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + hour12: false, + }); } catch { return value; } @@ -70,44 +79,6 @@ function formatShortTime(value: string | null) { } } -function presenceStatusLabel(item: PresenceItem | null) { - if (!item) { - return "未更新"; - } - - switch (item.status) { - case "office": - return "在岗"; - case "remote": - return "居家"; - case "out": - return "外出"; - case "travel": - return "出差"; - case "leave": - return "请假"; - default: - return item.status; - } -} - -function canQuickUpdatePresence(item: PresenceItem | null) { - return !(item?.status === "leave" && item.source === "leave_sync"); -} - -function isAttendanceFeatureDisabled(error: unknown) { - return isApiError(error) && error.code === "VALID_001" && error.details?.featureFlag === "attendance_v1_disabled"; -} - -function attendanceExemptionLabel(reason: unknown) { - if (reason === "leave") return "今日请假,无需打卡"; - if (reason === "travel") return "今日差旅,无需打卡"; - if (reason === "holiday") return "今日节假日,无需打卡"; - if (reason === "weekend") return "今日周末,无需打卡"; - if (reason === "policy_disabled") return "当前考勤策略未启用"; - return "今日无需打卡"; -} - function formatWorkMinutes(value: number | null | undefined) { if (value == null) { return "-"; @@ -117,27 +88,13 @@ function formatWorkMinutes(value: number | null | undefined) { return `${hours}h ${String(minutes).padStart(2, "0")}m`; } -function resolveNotificationTargetPath( - item: { targetType: string | null; targetId: string | null }, - canAccessApprovals: boolean, -) { - if (!item.targetType || !item.targetId) { - return "/notifications"; - } - - if (item.targetType === "workflow_task" && canAccessApprovals) { - return buildMobileApprovalsPath(item.targetId); - } - - if (item.targetType === "employee") { - return "/mobile/me"; - } - - if (item.targetType === "reimbursement_form") { - return `/reimbursements/${item.targetId}`; - } - - return "/notifications"; +function attendanceExemptionLabel(reason: unknown) { + if (reason === "leave") return "今日请假"; + if (reason === "travel") return "今日差旅"; + if (reason === "holiday") return "节假日"; + if (reason === "weekend") return "周末"; + if (reason === "policy_disabled") return "无需打卡"; + return "无需打卡"; } function workflowEntityLabel(entityType: string | null | undefined) { @@ -145,69 +102,46 @@ function workflowEntityLabel(entityType: string | null | undefined) { if (entityType === "reimbursement") return "报销"; if (entityType === "purchase_request") return "采购"; if (entityType === "travel_request") return "差旅"; - return entityType || "审批"; + return "审批"; } -function resolveTeamStatusBadge(item: { - onLeave: boolean; - leaveType: string | null; - presenceStatus: string | null; - attendanceClockIn: string | null; -}) { - if (item.onLeave) { - return { label: item.leaveType ? `请假 · ${item.leaveType}` : "请假", className: "badge-warning" }; +function resolveAttendanceAction(attendanceToday: AttendanceTodayResponse | null) { + if (!attendanceToday) { + return null; } - if (item.presenceStatus === "office") { - return { label: "在岗", className: "badge-success" }; + if (!attendanceToday.needClockIn) { + return null; } - if (item.presenceStatus === "out") { - return { - label: "外出", - className: "inline-flex items-center rounded-full bg-sky-50 px-2 py-0.5 text-[11px] font-semibold text-sky-700 ring-1 ring-inset ring-sky-600/10", - }; + if (!attendanceToday.item?.clockInAt) { + return "clock-in" as const; } - if (item.presenceStatus === "travel") { - return { - label: "差旅", - className: "inline-flex items-center rounded-full bg-sky-50 px-2 py-0.5 text-[11px] font-semibold text-sky-700 ring-1 ring-inset ring-sky-600/10", - }; + if (!attendanceToday.item.clockOutAt) { + return "clock-out" as const; } - if (item.presenceStatus === "remote") { - return { label: "居家", className: "badge-neutral" }; - } - if (!item.attendanceClockIn) { - return { label: "未打卡", className: "badge-neutral" }; - } - return { - label: "已打卡", - className: "inline-flex items-center rounded-full bg-slate-100 px-2 py-0.5 text-[11px] font-semibold text-slate-700 ring-1 ring-inset ring-slate-600/10", - }; + return null; } -function QuickLinkCard({ - label, - description, - to, - disabled = false, -}: { - label: string; - description: string; - to: string; - disabled?: boolean; -}) { - if (disabled) { - return ( -
-
{label}
-
{description}
-
- ); - } +function ActionTile({ action }: { action: MobileAction }) { + const isPrimary = action.tone === "primary"; + const isWarning = action.tone === "warning"; return ( - -
{label}
-
{description}
+ +
+ {action.label} + {action.icon} +
+
{action.meta}
); } @@ -215,27 +149,11 @@ function QuickLinkCard({ export function MobilePortalPage() { const queryClient = useQueryClient(); const { permissions } = useAuth(); - const [nowLabel, setNowLabel] = useState(() => - new Date().toLocaleTimeString("zh-CN", { hour12: false }), - ); const canAccessApprovals = hasAnyPermission(permissions, [...MOBILE_APPROVAL_PERMISSIONS]); const canAccessCapture = hasAnyPermission(permissions, [...MOBILE_CAPTURE_PERMISSIONS]); - const canAccessAssetScan = hasAnyPermission(permissions, [...MOBILE_ASSET_SCAN_PERMISSIONS]); const canAccessLeave = hasAnyPermission(permissions, [...MOBILE_LEAVE_PERMISSIONS]); const canAccessReimbursements = hasAnyPermission(permissions, [...MOBILE_REIMBURSEMENT_PERMISSIONS]); - useEffect(() => { - const timer = window.setInterval(() => { - setNowLabel(new Date().toLocaleTimeString("zh-CN", { hour12: false })); - }, 1000); - return () => window.clearInterval(timer); - }, []); - - const presenceQuery = useQuery({ - queryKey: ["mobile", "portal", "presence"], - queryFn: () => fetchMyPresence(), - }); - const attendanceTodayQuery = useQuery({ queryKey: ["mobile", "portal", "attendance", "today"], queryFn: () => fetchMyTodayAttendance(), @@ -245,34 +163,12 @@ export function MobilePortalPage() { const pendingCountQuery = useQuery({ enabled: canAccessApprovals, queryKey: ["mobile", "workflow", "pending-items"], - queryFn: () => fetchWorkflowTasks({ status: "pending", page: 1, pageSize: 5 }), - }); - - const teamStatusQuery = useQuery({ - queryKey: ["mobile", "portal", "team-status"], - queryFn: () => fetchPortalTeamStatus(), + queryFn: () => fetchWorkflowTasks({ status: "pending", page: 1, pageSize: 3 }), }); const notificationsQuery = useQuery({ queryKey: ["mobile", "portal", "notifications"], - queryFn: () => fetchNotifications({ page: 1, pageSize: 5 }), - }); - - const presenceMutation = useMutation({ - mutationFn: (status: "office" | "remote" | "out") => upsertMyPresence({ status }), - onSuccess: (response) => { - queryClient.setQueryData( - ["mobile", "portal", "presence"], - (current: { item: PresenceItem | null; date: string } | undefined) => ({ - item: response.item, - date: current?.date ?? response.item.date, - }), - ); - toast.success("今日考勤状态已更新"); - }, - onError: (error) => { - toast.error(resolveErrorMessage(error)); - }, + queryFn: () => fetchNotifications({ page: 1, pageSize: 3 }), }); const attendanceClockInMutation = useMutation({ @@ -283,11 +179,7 @@ export function MobilePortalPage() { queryClient.invalidateQueries({ queryKey: ["attendance", "me", "today"] }), queryClient.invalidateQueries({ queryKey: ["attendance", "me", "month"] }), ]); - toast.success( - response.item.isLate - ? `上班打卡成功,已迟到 ${response.item.lateMinutes} 分钟` - : "上班打卡成功", - ); + toast.success(response.item.isLate ? `上班打卡成功,迟到 ${response.item.lateMinutes} 分钟` : "上班打卡成功"); }, onError: (error) => { toast.error(resolveErrorMessage(error)); @@ -309,273 +201,170 @@ export function MobilePortalPage() { }, }); - const presenceItem = presenceQuery.data?.item ?? null; const attendanceToday = attendanceTodayQuery.data ?? null; + const attendanceAction = resolveAttendanceAction(attendanceToday); const attendanceFeatureDisabled = isAttendanceFeatureDisabled(attendanceTodayQuery.error); - const teamStatusItems = teamStatusQuery.data ?? []; + const pendingApprovalCount = canAccessApprovals ? pendingCountQuery.data?.total ?? 0 : 0; const pendingTasks = pendingCountQuery.data?.items ?? []; const notifications = notificationsQuery.data?.items ?? []; - const pendingApprovalCount = canAccessApprovals ? pendingCountQuery.data?.total ?? 0 : 0; const unreadNotificationCount = notifications.filter((item) => !item.readAt).length; + const actions: MobileAction[] = [ + canAccessApprovals + ? { + key: "approvals", + label: "审批", + meta: pendingApprovalCount > 0 ? `${pendingApprovalCount} 条待处理` : "暂无待办", + icon: "fact_check", + to: "/mobile/approvals", + tone: pendingApprovalCount > 0 ? "warning" : "primary", + } + : null, + canAccessCapture + ? { + key: "capture", + label: "传票据", + meta: "拍照或选文件", + icon: "receipt_long", + to: "/mobile/capture", + tone: "primary", + } + : null, + canAccessReimbursements + ? { + key: "reimbursements", + label: "报销", + meta: "新建和查进度", + icon: "request_quote", + to: "/reimbursements", + } + : null, + canAccessLeave + ? { + key: "leave", + label: "请假", + meta: "提交申请", + icon: "event_available", + to: "/leave-requests", + } + : null, + { + key: "me", + label: "我的", + meta: unreadNotificationCount > 0 ? `${unreadNotificationCount} 条新通知` : "资料和通知", + icon: "person", + to: "/mobile/me", + }, + ].filter((item): item is MobileAction => Boolean(item)).slice(0, 4); + return ( - + -
-
-
-
今日重点
-
移动端只保留高频动作,低频模块放到“我的”。
-
+
+
+
先处理这几件事
0 ? "badge-warning" : "badge-success"}> - {pendingApprovalCount > 0 ? `${pendingApprovalCount} 条待审批` : "暂无审批待办"} + {pendingApprovalCount > 0 ? `${pendingApprovalCount} 待批` : "清爽"}
- -
-
-
{pendingApprovalCount}
-
审批待办
-
-
-
{unreadNotificationCount}
-
未读通知
-
-
- -
- - - -
-
- - {!attendanceFeatureDisabled ? ( -
-
-
-
今日考勤
-
- {attendanceToday?.workDate ?? "今日"} · 当前时间 {nowLabel} -
-
- {attendanceToday?.workDate ?? "今日"} -
- - {attendanceTodayQuery.isLoading ? ( -

加载今日考勤中...

- ) : attendanceTodayQuery.isError ? ( -

- {resolveErrorMessage(attendanceTodayQuery.error)} -

- ) : attendanceToday && !attendanceToday.needClockIn ? ( -
-
{attendanceExemptionLabel(attendanceToday.exemptionReason)}
-
- {attendanceToday.item?.clockInAt || attendanceToday.item?.clockOutAt - ? `已有补录记录:${formatDateTime(attendanceToday.item.clockInAt)} / ${formatDateTime(attendanceToday.item.clockOutAt)}` - : "系统已自动排除缺卡统计。"} -
-
- ) : attendanceToday?.item?.clockInAt && !attendanceToday.item.clockOutAt ? ( -
-
- 已打上班卡:{formatDateTime(attendanceToday.item.clockInAt)} - {attendanceToday.item.isLate ? `,迟到 ${attendanceToday.item.lateMinutes} 分钟` : ""} -
- -
- ) : attendanceToday?.item?.clockInAt && attendanceToday.item.clockOutAt ? ( -
-
今日已完成
-
- 上班:{formatDateTime(attendanceToday.item.clockInAt)} -
-
- 下班:{formatDateTime(attendanceToday.item.clockOutAt)} -
-
- 工时:{formatWorkMinutes(attendanceToday.item.workMinutes)} -
-
- ) : ( -
-
- 今日尚未打上班卡。 -
- -
- )} -
- ) : null} - -
-
-
-
团队状态
-
查看全员今日在岗、请假和打卡状态。
-
- {teamStatusItems.length} 人 -
- - {teamStatusQuery.isLoading ?

加载团队状态中...

: null} - {teamStatusQuery.isError ? ( -

- {resolveErrorMessage(teamStatusQuery.error)} -

- ) : null} - {!teamStatusQuery.isLoading && !teamStatusQuery.isError && !teamStatusItems.length ? ( -

暂无团队状态。

- ) : null} - -
- {teamStatusItems.map((item) => { - const badge = resolveTeamStatusBadge(item); - return ( -
-
- -
-
{item.name}
-
- {item.attendanceClockIn ? `${formatShortTime(item.attendanceClockIn)} 已打卡` : "暂无打卡"} -
-
-
-
- {badge.label} -
-
- ); - })} -
-
- -
-
-
-
今日考勤状态
-
- {presenceQuery.isError ? "当前无法读取考勤状态" : `当前状态:${presenceStatusLabel(presenceItem)}`} -
-
- {presenceQuery.data?.date ?? "今日"} -
- - {presenceItem?.status === "leave" && presenceItem.source === "leave_sync" ? ( -

- 当前日期已被审批通过的请假覆盖,不能在移动首页手动修改。 -

- ) : null} - -
- {QUICK_PRESENCE_OPTIONS.map((option) => ( - +
+ {actions.map((action) => ( + ))}
-
-
-
-
待办聚合
-
- {canAccessApprovals ? "展示前 5 条审批待办,可直接进入移动审批。" : "当前账号没有移动审批权限"} -
+ {!attendanceFeatureDisabled ? ( +
+
+
打卡
+ {attendanceToday?.workDate ?? "今日"}
- - {canAccessApprovals ? `${pendingCountQuery.data?.total ?? 0} 条` : "不可用"} - -
- {canAccessApprovals ? ( - <> - + ) : null} +
+ ) : null} - {pendingCountQuery.isLoading ?

加载待办中...

: null} - {pendingCountQuery.isError ? ( -

- {resolveErrorMessage(pendingCountQuery.error)} -

- ) : null} - {!pendingCountQuery.isLoading && !pendingTasks.length ? ( -

当前没有审批待办。

- ) : null} - -
- {pendingTasks.map((task) => ( - -
{task.stepName}
-
- 业务:{workflowEntityLabel(task.workflowInstance?.entityType)} · 实体:{task.workflowInstance?.entityId ?? "-"} -
- - ))} -
- - ) : ( -
- 你当前没有审批权限,审批页入口已禁用。 + {canAccessApprovals ? ( +
+
+
待审批
+
- )} -
- {(canAccessLeave || canAccessAssetScan) ? ( -
-
其他快捷入口
-
- - + {pendingCountQuery.isLoading ?

读取中...

: null} + {pendingCountQuery.isError ? ( +

+ {resolveErrorMessage(pendingCountQuery.error)} +

+ ) : null} + {!pendingCountQuery.isLoading && !pendingTasks.length ? ( +

没有待处理审批。

+ ) : null} + +
+ {pendingTasks.map((task) => ( + +
{task.stepName}
+
{workflowEntityLabel(task.workflowInstance?.entityType)}
+ + ))}
) : null} -
-
-
最近通知
+
+
+
通知
- {notificationsQuery.isLoading ?

加载通知中...

: null} + {notificationsQuery.isLoading ?

读取中...

: null} {notificationsQuery.isError ? ( -

+

{resolveErrorMessage(notificationsQuery.error)}

) : null} @@ -587,15 +376,14 @@ export function MobilePortalPage() { {notifications.map((item) => ( -
-
{item.title}
- {!item.readAt ? 未读 : null} +
+
{item.title}
+ {!item.readAt ? 新 : null}
- {item.body ?
{item.body}
: null} -
{formatDateTime(item.createdAt)}
+
{formatDateTime(item.createdAt)}
))}
diff --git a/client/src/pages/PortalHomePage.tsx b/client/src/pages/PortalHomePage.tsx index d23de2e9..c8c702de 100644 --- a/client/src/pages/PortalHomePage.tsx +++ b/client/src/pages/PortalHomePage.tsx @@ -352,11 +352,11 @@ function resolveClockAction( function SummaryPanel({ title, items }: { title: string; items: SummaryItem[] }) { if (!items.length) { - return ; + return ; } return ( - +
{items.map((item) => ( ) : null}
- )) : } + )) : }
); } @@ -400,14 +400,14 @@ function RecentVisitsPanel() { const recentVisits = readRecentVisits(); return ( - +
{recentVisits.length ? recentVisits.map((item) => (
{item.title}
{item.path}
- )) : } + )) : }
); @@ -417,13 +417,13 @@ function QuickLinksPanel({ items }: { items: PortalHomePayload["quickLinks"] }) const safeItems = Array.isArray(items) ? items : []; return ( - +
{safeItems.length ? safeItems.map((item) => ( {item.title} - )) : } + )) : }
); @@ -431,7 +431,7 @@ function QuickLinksPanel({ items }: { items: PortalHomePayload["quickLinks"] }) function SecondaryCardsPanel({ title, description, cards }: { title: string; description: string; cards: PortalHomePayload["cards"] }) { if (!cards.length) { - return ; + return ; } return ( @@ -443,7 +443,7 @@ function SecondaryCardsPanel({ title, description, cards }: { title: string; des title={card.title} value={card.value} href={card.href} - note="进入详情页处理" + note="查看" tone={severityTone(card.severity)} /> ))} @@ -462,26 +462,26 @@ function AdoptionSummaryPanel({ adoptionSummary }: { adoptionSummary: NonNullabl
试点观测摘要
-
管理员按需展开,用于观察习惯是否已形成。
+
管理员按需查看。
展开 / 收起
近 7 天登录覆盖率
-
{adoptionSummary.login7dRate}%
+
{adoptionSummary.login7dRate}%
周报提交率
-
{adoptionSummary.weeklySubmissionRate}%
+
{adoptionSummary.weeklySubmissionRate}%
团队状态覆盖率
-
{adoptionSummary.presenceCoverageRate}%
+
{adoptionSummary.presenceCoverageRate}%
当前公告数
-
{adoptionSummary.announcementCount}
+
{adoptionSummary.announcementCount}
@@ -510,7 +510,7 @@ function SecondaryWorkspacePanel({
{title}
-
{description}
+
{description}
展开 / 收起
@@ -549,7 +549,7 @@ function GettingStartedCard({ return (
@@ -680,7 +680,7 @@ function TodoAggregatePanel({ items }: { items: TodoAggregateItem[] }) { return (
@@ -706,11 +706,11 @@ function TodoAggregatePanel({ items }: { items: TodoAggregateItem[] }) {
- )) : } + )) : }
{hiddenCount > 0 ? (
- 还有 {hiddenCount} 项已收起,避免首页变成待办清单。 + 还有 {hiddenCount} 项已收起。 查看全部待办
) : null} @@ -726,7 +726,7 @@ function TeamStatusBoardPanel({ items }: { items: PortalTeamStatusItem[] }) { return ( 查看团队页} className="portal-panel-fill" > @@ -743,11 +743,11 @@ function TeamStatusBoardPanel({ items }: { items: PortalTeamStatusItem[] }) { {badge.label}
); - }) : } + }) : }
{hiddenCount > 0 ? (
- 已收起 {hiddenCount} 位成员,避免首页纵向过长。 + 已收起 {hiddenCount} 位成员。 查看全部
) : null} @@ -785,7 +785,7 @@ function ActivityAnnouncementsPanel({ return ( 公告页} className="portal-panel-fill" > @@ -959,7 +959,7 @@ export function PortalHomePage() { href: payload.todayPresence.href, label: "今日状态", value: payload.todayPresence.label, - note: payload.todayPresence.note ?? "可在团队页手动更新在岗、居家、外出或出差状态。", + note: payload.todayPresence.note ?? "可在团队页更新。", tone: payload.todayPresence.status === "leave" ? "warning" as const : "normal" as const, } : null, @@ -971,7 +971,7 @@ export function PortalHomePage() { href: payload.teamPresence?.href ?? "/team", label: "今日团队状态", value: payload.teamPresence ? `在岗 ${payload.teamPresence.office}/${payload.teamPresence.total}` : "待启用", - note: payload.teamPresence ? `请假 ${payload.teamPresence.leave},未更新 ${payload.teamPresence.unspecified}` : "团队状态板可展示在岗、居家、外出、出差与请假。", + note: payload.teamPresence ? `请假 ${payload.teamPresence.leave},未更新 ${payload.teamPresence.unspecified}` : "团队状态待启用。", tone: payload.teamPresence && (payload.teamPresence.leave > 0 || payload.teamPresence.unspecified > 0) ? "warning" as const : "normal" as const, }, payload.weeklySummary @@ -1039,9 +1039,7 @@ export function PortalHomePage() { eyebrow={isManagerView ? "管理工作台" : "个人工作台"} title={`你好,${payload.greeting.name}`} className="workbench-hero--compact" - subtitle={isManagerView - ? "先处理审批、团队状态和周报缺口,再进入项目与组织协同。" - : "先处理今天必须完成的事,再继续查看项目、周报和公告。"} + subtitle={isManagerView ? "审批、团队、周报。" : "待办、打卡、申请。"} actions={ !payload.workspace.persisted && canSetupWorkspace ? (
-
Outbox
+
同步队列
{erpPreviewQuery.data?.exportState.lastOutboxId ?? "-"}
@@ -1247,7 +1247,7 @@ export function ReimbursementDetailPage() {
- +
报销编号:{detail.formNumber ?? detail.id}
报销人:{detail.owner?.name ?? "-"}
提交人:{detail.submitter?.name ?? "-"}
@@ -1461,7 +1461,7 @@ export function ReimbursementDetailPage() { {pendingOcrInvoiceIds.length ? (

- {pendingOcrInvoiceIds.length} 张发票正在 OCR 识别中,系统会在可绑定后自动刷新列表。 + {pendingOcrInvoiceIds.length} 张发票正在识别,完成后自动刷新。

) : null}
@@ -1473,7 +1473,7 @@ export function ReimbursementDetailPage() { ERP 凭证预览 - 展示当前报销单将发送到 ERP 的凭证切片与最近一次导出状态。 + 查看凭证内容和最近同步状态。 {erpPreviewQuery.data ? ( @@ -1492,7 +1492,7 @@ export function ReimbursementDetailPage() {

导出状态

同步状态:{erpSyncStatus}
外部 ID:{erpPreviewQuery.data.exportState.externalId ?? "-"}
-
Outbox:{erpPreviewQuery.data.exportState.lastOutboxId ?? "-"}
+
队列 ID:{erpPreviewQuery.data.exportState.lastOutboxId ?? "-"}
队列状态:{erpPreviewQuery.data.exportState.lastOutboxStatus ?? "-"}
最近导出:{formatDateTime(erpPreviewQuery.data.exportState.exportedAt)}
最近错误:{erpPreviewQuery.data.exportState.lastError ?? "-"}
diff --git a/client/src/pages/WorkflowTaskDetailPage.tsx b/client/src/pages/WorkflowTaskDetailPage.tsx index ff3afe97..e50f0b1a 100644 --- a/client/src/pages/WorkflowTaskDetailPage.tsx +++ b/client/src/pages/WorkflowTaskDetailPage.tsx @@ -185,7 +185,7 @@ export function WorkflowTaskDetailPage() { @@ -203,7 +203,7 @@ export function WorkflowTaskDetailPage() {
- +
@@ -291,7 +291,7 @@ export function WorkflowTaskDetailPage() { {detail.workflowInstance?.entityType === "reimbursement" && detail.workflowInstance.contextData.standardViolations.length ? ( - +
{detail.workflowInstance.contextData.standardViolations.map((item) => (
) : null} - +
{actionError ? (

- +

{detail.history.length ? ( detail.history.map((item) => ( @@ -435,7 +435,7 @@ export function WorkflowTaskDetailPage() {
- +
节点:{detail.task.stepName}
实例状态:{detail.workflowInstance?.currentStatus ?? "-"}
当前审批角色:{detail.task.assigneeRole ?? "-"}
diff --git a/client/src/pages/invoice-pool/InvoiceUploadCard.tsx b/client/src/pages/invoice-pool/InvoiceUploadCard.tsx index 5a78ec7c..085eb478 100644 --- a/client/src/pages/invoice-pool/InvoiceUploadCard.tsx +++ b/client/src/pages/invoice-pool/InvoiceUploadCard.tsx @@ -33,7 +33,7 @@ export function InvoiceUploadCard({ return (
-

上传票据(VAT OCR)

+

添加发票

{showProxyOwnerSelector ? (