收口移动端入口与文案(05-30-mobile-confirmed-entry-copy-followups)
这个提交包含在:
@@ -792,10 +792,18 @@ surfaces. They are not compressed PC management consoles.
|
|||||||
and bottom safe-area primary action.
|
and bottom safe-area primary action.
|
||||||
- Mobile home should show compact status and today's actionable items. Do not
|
- Mobile home should show compact status and today's actionable items. Do not
|
||||||
add a full module directory or dashboard.
|
add a full module directory or dashboard.
|
||||||
|
- Mobile home finance summaries are conditional action cards. Render them only
|
||||||
|
when the current finance/cashier user has pending payment, payment exception
|
||||||
|
or risk-review counts to handle; do not show disabled finance placeholders or
|
||||||
|
unconditional mobile `我的` finance anchors.
|
||||||
- Mobile lists use search/status filtering, cards and one primary action. Do not
|
- Mobile lists use search/status filtering, cards and one primary action. Do not
|
||||||
use wide tables, column configuration, complex multi-column filters,
|
use wide tables, column configuration, complex multi-column filters,
|
||||||
full-scope management, admin configuration, batch export, bulk approval or
|
full-scope management, admin configuration, batch export, bulk approval or
|
||||||
bulk payment.
|
bulk payment.
|
||||||
|
- Mobile list search that filters the currently loaded list must expose the
|
||||||
|
visible label and aria label `当前列表搜索`; do not imply global search with
|
||||||
|
labels such as `搜索报销` or `搜索采购` unless the page actually queries a
|
||||||
|
global corpus.
|
||||||
- Mobile details use a focus card, sectioned evidence and bottom actions. Do not
|
- Mobile details use a focus card, sectioned evidence and bottom actions. Do not
|
||||||
copy PC two-column layouts.
|
copy PC two-column layouts.
|
||||||
- Mobile forms keep necessary fields, material upload, field-local validation
|
- Mobile forms keep necessary fields, material upload, field-local validation
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
{"file":".trellis/spec/hyb-oa/frontend-react-vite-design.md","reason":"检查移动页面布局、文案、动作位置和验证要求。"}
|
||||||
|
{"file":".trellis/spec/hyb-oa/wecom-boundary.md","reason":"检查移动请假不越过企业微信主办边界。"}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
{"file":".trellis/spec/hyb-oa/frontend-react-vite-design.md","reason":"移动端统一 shell、轻动作、待我处理、财务/付款和列表搜索规则。"}
|
||||||
|
{"file":".trellis/spec/hyb-oa/wecom-boundary.md","reason":"请假和考勤由企业微信主办,移动请假保持只读和 OA 应急边界。"}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# Implementation Notes
|
||||||
|
|
||||||
|
## Design Decision
|
||||||
|
|
||||||
|
移动端继续走“轻动作、少空壳、状态先行”的方向:
|
||||||
|
|
||||||
|
- 请假:企业微信主办,OA 只读展示记录和额度;OA 应急入口只给 `leave.manage`。
|
||||||
|
- 财务:首页不常驻展示财务模块,只有待付款或异常/风险需要处理时才出现。
|
||||||
|
- 搜索:报销和采购的移动搜索只过滤当前已加载列表,不暗示全局搜索。
|
||||||
|
- 审批:统一叫 `待我处理`,避免 `待办` / `待我审批` / `审批` 多套叫法。
|
||||||
|
|
||||||
|
## Code Surface
|
||||||
|
|
||||||
|
- `client/src/pages/MobileLeavePage.tsx`
|
||||||
|
- `client/src/pages/MobilePortalPage.tsx`
|
||||||
|
- `client/src/pages/MobileApprovalsPage.tsx`
|
||||||
|
- `client/src/pages/MobileReimbursementsPage.tsx`
|
||||||
|
- `client/src/pages/MobilePurchaseRequestsPage.tsx`
|
||||||
|
- `client/src/pages/MobileMePage.tsx`
|
||||||
|
- `docs/产品设计规则.md`
|
||||||
|
- 相关移动端 Playwright 测试
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
# 移动端已确认入口与文案收敛
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
把前一轮已经确认但未进入 PC 导航任务的移动端规则落到代码:移动请假强化只读额度,移动财务卡只在有待处理付款/异常时出现,移动报销和采购搜索明确为当前列表搜索,移动审批统一使用 `待我处理`。
|
||||||
|
|
||||||
|
## Background
|
||||||
|
|
||||||
|
- 产品面向国内、小于 10 人的科技初创公司,移动端只承接轻动作:提交、补材料、查状态、处理待办、扫码和消息直达。
|
||||||
|
- 用户已明确:请假和考勤由企业微信主办,OA 不重复处理日常请假,只展示镜像/记录/额度和应急入口。
|
||||||
|
- 用户已确认:
|
||||||
|
- 移动请假展示只读假期余额。
|
||||||
|
- 移动财务卡只在存在待付款、付款异常或风险复核时显示。
|
||||||
|
- 移动报销和移动采购搜索标为 `当前列表搜索`。
|
||||||
|
- 移动审批统一使用 `待我处理`,不再使用 `待我审批`。
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- `MobileLeavePage`:普通员工记录视角也展示只读额度摘要和明细,不暴露 OA 日常请假提交动作。
|
||||||
|
- `MobilePortalPage`:财务摘要只在有真实待付款或异常/风险时显示;无事项时不展示空壳财务卡。
|
||||||
|
- `MobileReimbursementsPage` / `MobilePurchaseRequestsPage`:搜索控件中文标签统一为 `当前列表搜索`。
|
||||||
|
- `MobileApprovalsPage` / `MobilePortalPage`:移动端审批入口、tab 和空态文案统一为 `待我处理`。
|
||||||
|
- `MobileMePage`:不再提供无条件财务锚点入口,避免无待处理事项时跳到空财务区。
|
||||||
|
- 产品事实源中明显过时的 `待我审批` 文案同步为 `待我处理`。
|
||||||
|
|
||||||
|
## Out Of Scope
|
||||||
|
|
||||||
|
- 不新增企业微信写入、请假提交或考勤动作。
|
||||||
|
- 不新增财务付款能力,不改变付款批次、AP、审批 API 或权限模型。
|
||||||
|
- 不调整 PC 页面。
|
||||||
|
- 不做生产部署或公网检查。
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
1. 移动请假普通员工默认保持 `请假记录` + `企业微信主办` + `只读`。
|
||||||
|
2. 移动请假在记录视角展示可用、冻结、已用额度汇总,并列出最多 3 个假种额度;加载、错误、无额度状态要自然中文表达。
|
||||||
|
3. 移动财务卡只有在待付款、付款异常或风险复核数量大于 0 时展示;`paymentBatchEnabled` 只影响卡片语义,不制造空壳提示。
|
||||||
|
4. 移动财务无待处理事项时,首页不显示财务空壳卡;`#finance` 深链也不应制造空白说明区。
|
||||||
|
5. 移动报销和采购搜索控件具有明确 `当前列表搜索` 标签,不改变现有列表筛选语义。
|
||||||
|
6. 移动审批相关用户文案统一使用 `待我处理`。
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- [ ] 普通员工移动请假页能看到只读额度摘要和假种额度,不看到 OA 新建/编辑/提交日常请假。
|
||||||
|
- [ ] 财务角色在无待付款、无异常、无风险时不看到 `mobile-finance-summary`。
|
||||||
|
- [ ] 财务角色在有待付款或异常/风险时能看到财务摘要卡。
|
||||||
|
- [ ] 移动报销和移动采购搜索区域显示 `当前列表搜索`。
|
||||||
|
- [ ] 移动首页和移动审批页不再出现 `待我审批`。
|
||||||
|
- [ ] 移动我的页不展示无条件财务锚点入口,审批入口使用 `待我处理`。
|
||||||
|
- [ ] 前端 typecheck/build 和相关 Playwright 通过。
|
||||||
|
- [ ] Trellis governance 和 `git diff --check` 通过。
|
||||||
|
|
||||||
|
## Test Plan
|
||||||
|
|
||||||
|
- `cd client && npm run typecheck`
|
||||||
|
- `cd client && npm run build`
|
||||||
|
- 本地 Vite 启动后运行聚焦 Playwright:
|
||||||
|
- `cd client && npx playwright test tests/e2e/r124-mobile-entry-matrix.spec.ts tests/e2e/dg7-mobile-information-dedupe.spec.ts --workers=1 --reporter=list`
|
||||||
|
- `python3 ./.trellis/scripts/task.py validate 05-30-mobile-confirmed-entry-copy-followups`
|
||||||
|
- `node scripts/qa/check-trellis-governance.mjs`
|
||||||
|
- `git diff --check`
|
||||||
|
|
||||||
|
## Implementation Plan
|
||||||
|
|
||||||
|
1. 在移动请假记录视角复用现有余额查询,抽出只读额度渲染,避免把表单视角和普通员工路径混在一起。
|
||||||
|
2. 在移动首页计算财务待办数量,只有待付款或异常大于 0 时渲染财务卡。
|
||||||
|
3. 调整移动报销、采购、审批相关中文文案和测试断言。
|
||||||
|
4. 跑前端构建和移动端聚焦 Playwright,必要时补充断言。
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"id": "mobile-confirmed-entry-copy-followups",
|
||||||
|
"name": "mobile-confirmed-entry-copy-followups",
|
||||||
|
"title": "移动端已确认入口与文案收敛",
|
||||||
|
"description": "收口已确认的移动端入口、财务卡显示条件、请假只读额度和待我处理文案。",
|
||||||
|
"status": "in_progress",
|
||||||
|
"dev_type": null,
|
||||||
|
"scope": "frontend-mobile",
|
||||||
|
"package": null,
|
||||||
|
"priority": "P2",
|
||||||
|
"creator": "shunli",
|
||||||
|
"assignee": "shunli",
|
||||||
|
"createdAt": "2026-05-30",
|
||||||
|
"completedAt": null,
|
||||||
|
"branch": null,
|
||||||
|
"base_branch": "main",
|
||||||
|
"worktree_path": null,
|
||||||
|
"commit": null,
|
||||||
|
"pr_url": null,
|
||||||
|
"subtasks": [],
|
||||||
|
"children": [],
|
||||||
|
"parent": null,
|
||||||
|
"relatedFiles": [],
|
||||||
|
"notes": "",
|
||||||
|
"meta": {}
|
||||||
|
}
|
||||||
@@ -1006,7 +1006,7 @@ export function MobileApprovalsPage() {
|
|||||||
<div className="mobile-command-surface">
|
<div className="mobile-command-surface">
|
||||||
<Tabs value={taskTab} onValueChange={handleTaskTabChange}>
|
<Tabs value={taskTab} onValueChange={handleTaskTabChange}>
|
||||||
<TabsList className="flex w-full justify-start gap-1">
|
<TabsList className="flex w-full justify-start gap-1">
|
||||||
<TabsTrigger value="pending">待我审批</TabsTrigger>
|
<TabsTrigger value="pending">待我处理</TabsTrigger>
|
||||||
<TabsTrigger value="completed" disabled={!canUseRemote}>已处理</TabsTrigger>
|
<TabsTrigger value="completed" disabled={!canUseRemote}>已处理</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
@@ -1062,7 +1062,7 @@ export function MobileApprovalsPage() {
|
|||||||
{taskTab === "completed"
|
{taskTab === "completed"
|
||||||
? "最近 30 天暂无符合筛选条件的已处理任务。"
|
? "最近 30 天暂无符合筛选条件的已处理任务。"
|
||||||
: !canAccessRemoteApprovals && isOnline
|
: !canAccessRemoteApprovals && isOnline
|
||||||
? "暂无待我审批;收到企业微信审批消息后可从这里查看。"
|
? "暂无待我处理;收到企业微信审批消息后可从这里查看。"
|
||||||
: canUseRemote
|
: canUseRemote
|
||||||
? "暂无符合筛选条件的审批任务。"
|
? "暂无符合筛选条件的审批任务。"
|
||||||
: "暂无符合筛选条件的已缓存审批。"}
|
: "暂无符合筛选条件的已缓存审批。"}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
type LeaveRequestItem,
|
type LeaveRequestItem,
|
||||||
type LeaveRequestStatus,
|
type LeaveRequestStatus,
|
||||||
type LeaveSession,
|
type LeaveSession,
|
||||||
|
type LeaveBalanceItem,
|
||||||
} from "../api/leave";
|
} from "../api/leave";
|
||||||
import { useAuth } from "../auth/AuthContext";
|
import { useAuth } from "../auth/AuthContext";
|
||||||
import { hasPermission } from "../auth/permissions";
|
import { hasPermission } from "../auth/permissions";
|
||||||
@@ -156,6 +157,78 @@ function LeaveRecordCard({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function MobileLeaveBalances({
|
||||||
|
balances,
|
||||||
|
isLoading,
|
||||||
|
isError,
|
||||||
|
errorMessage,
|
||||||
|
summary,
|
||||||
|
}: {
|
||||||
|
balances: LeaveBalanceItem[];
|
||||||
|
isLoading: boolean;
|
||||||
|
isError: boolean;
|
||||||
|
errorMessage: string;
|
||||||
|
summary: { available: number; reserved: number; used: number };
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<section className="mobile-card mobile-page-stack" data-testid="mobile-leave-balances">
|
||||||
|
<div className="mobile-section-header">
|
||||||
|
<div>
|
||||||
|
<h3 className="mobile-section-title">假期额度</h3>
|
||||||
|
<p className="mobile-list-card__subtitle mt-1">企业微信主办,OA 只读展示同步额度</p>
|
||||||
|
</div>
|
||||||
|
<span className="badge-success">只读</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mobile-metric-grid text-sm" data-testid="mobile-leave-balance-summary">
|
||||||
|
<div>
|
||||||
|
<div className="text-xs text-zinc-500">可用</div>
|
||||||
|
<div className="mt-1 text-2xl font-semibold text-zinc-900">{summary.available.toFixed(1)}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-xs text-zinc-500">冻结</div>
|
||||||
|
<div className="mt-1 text-2xl font-semibold text-zinc-900">{summary.reserved.toFixed(1)}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-xs text-zinc-500">已用</div>
|
||||||
|
<div className="mt-1 text-2xl font-semibold text-zinc-900">{summary.used.toFixed(1)}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isLoading ? <div className="mobile-empty-state">额度加载中...</div> : null}
|
||||||
|
{isError ? <div className="mobile-empty-state text-red-600">{errorMessage}</div> : null}
|
||||||
|
{!isLoading && !isError && !balances.length ? <div className="mobile-empty-state">暂无可展示额度</div> : null}
|
||||||
|
|
||||||
|
{balances.length ? (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{balances.slice(0, 3).map((item) => (
|
||||||
|
<div key={item.leaveType.id} className="mobile-list-card">
|
||||||
|
<div className="flex items-center justify-between gap-3">
|
||||||
|
<div className="font-medium text-zinc-900">{item.leaveType.name}</div>
|
||||||
|
<span className="badge-neutral">同步</span>
|
||||||
|
</div>
|
||||||
|
<div className="mobile-metric-grid mt-3 text-sm">
|
||||||
|
<div>
|
||||||
|
<div className="text-xs text-zinc-500">可用</div>
|
||||||
|
<div className="font-semibold text-zinc-900">{item.available}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-xs text-zinc-500">冻结</div>
|
||||||
|
<div className="font-semibold text-zinc-900">{item.reserved}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-xs text-zinc-500">已用</div>
|
||||||
|
<div className="font-semibold text-zinc-900">{item.used}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function MobileLeavePage() {
|
export function MobileLeavePage() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
@@ -412,25 +485,16 @@ export function MobileLeavePage() {
|
|||||||
</Tabs>
|
</Tabs>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<MobileLeaveBalances
|
||||||
|
balances={balances}
|
||||||
|
isLoading={balancesQuery.isLoading}
|
||||||
|
isError={balancesQuery.isError}
|
||||||
|
errorMessage={balancesQuery.isError ? resolveErrorMessage(balancesQuery.error) : ""}
|
||||||
|
summary={balanceSummary}
|
||||||
|
/>
|
||||||
|
|
||||||
{effectiveView === "form" ? (
|
{effectiveView === "form" ? (
|
||||||
<>
|
<>
|
||||||
<section className="mobile-card" data-testid="mobile-leave-summary">
|
|
||||||
<div className="mobile-metric-grid text-sm">
|
|
||||||
<div>
|
|
||||||
<div className="text-xs text-zinc-500">可用</div>
|
|
||||||
<div className="mt-1 text-2xl font-semibold text-zinc-900">{balanceSummary.available.toFixed(1)}</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="text-xs text-zinc-500">冻结</div>
|
|
||||||
<div className="mt-1 text-2xl font-semibold text-zinc-900">{balanceSummary.reserved.toFixed(1)}</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="text-xs text-zinc-500">已用</div>
|
|
||||||
<div className="mt-1 text-2xl font-semibold text-zinc-900">{balanceSummary.used.toFixed(1)}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="mobile-card" data-testid="mobile-leave-form">
|
<section className="mobile-card" data-testid="mobile-leave-form">
|
||||||
<div className="mb-4 flex items-center justify-between gap-3">
|
<div className="mb-4 flex items-center justify-between gap-3">
|
||||||
<h3 className="text-base font-semibold text-zinc-900">{editingItem ? "编辑 OA 应急请假" : "新建 OA 应急请假"}</h3>
|
<h3 className="text-base font-semibold text-zinc-900">{editingItem ? "编辑 OA 应急请假" : "新建 OA 应急请假"}</h3>
|
||||||
@@ -533,35 +597,6 @@ export function MobileLeavePage() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="mobile-card" data-testid="mobile-leave-balances">
|
|
||||||
<h3 className="text-base font-semibold text-zinc-900">额度</h3>
|
|
||||||
<div className="mt-3 space-y-2">
|
|
||||||
{balancesQuery.isLoading ? <div className="text-sm text-zinc-500">加载中...</div> : null}
|
|
||||||
{!balancesQuery.isLoading && !balances.length ? <div className="text-sm text-zinc-500">暂无额度</div> : null}
|
|
||||||
{balances.slice(0, 3).map((item) => (
|
|
||||||
<div key={item.leaveType.id} className="mobile-list-card">
|
|
||||||
<div className="flex items-center justify-between gap-3">
|
|
||||||
<div className="font-medium text-zinc-900">{item.leaveType.name}</div>
|
|
||||||
<span className="badge-success">启用</span>
|
|
||||||
</div>
|
|
||||||
<div className="mobile-metric-grid mt-3 text-sm">
|
|
||||||
<div>
|
|
||||||
<div className="text-xs text-zinc-500">可用</div>
|
|
||||||
<div className="font-semibold text-zinc-900">{item.available}</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="text-xs text-zinc-500">冻结</div>
|
|
||||||
<div className="font-semibold text-zinc-900">{item.reserved}</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="text-xs text-zinc-500">已用</div>
|
|
||||||
<div className="font-semibold text-zinc-900">{item.used}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { fetchEmployeeProfile } from "../api/employees";
|
|||||||
import { mapApiErrorMessage } from "../api/errorMessages";
|
import { mapApiErrorMessage } from "../api/errorMessages";
|
||||||
import { isApiError } from "../api/types";
|
import { isApiError } from "../api/types";
|
||||||
import { useAuth } from "../auth/AuthContext";
|
import { useAuth } from "../auth/AuthContext";
|
||||||
import { hasAnyPermission, hasBusinessApproverRole, hasFinanceRole } from "../auth/permissions";
|
import { hasAnyPermission, hasBusinessApproverRole } from "../auth/permissions";
|
||||||
import { EmployeeAvatar } from "../components/common/EmployeeAvatar";
|
import { EmployeeAvatar } from "../components/common/EmployeeAvatar";
|
||||||
import { PageHeader } from "../components/common/PageHeader";
|
import { PageHeader } from "../components/common/PageHeader";
|
||||||
import { MobileIcon } from "../components/mobile/MobileIcon";
|
import { MobileIcon } from "../components/mobile/MobileIcon";
|
||||||
@@ -56,7 +56,6 @@ export function MobileMePage() {
|
|||||||
const canAccessAssets = hasAnyPermission(permissions, ["asset.view_own", "asset.view_all", "asset.manage", "asset.transfer", "asset.inventory"]);
|
const canAccessAssets = hasAnyPermission(permissions, ["asset.view_own", "asset.view_all", "asset.manage", "asset.transfer", "asset.inventory"]);
|
||||||
const canAccessPurchase = hasAnyPermission(permissions, [...MOBILE_PURCHASE_PERMISSIONS]);
|
const canAccessPurchase = hasAnyPermission(permissions, [...MOBILE_PURCHASE_PERMISSIONS]);
|
||||||
const canAccessApprovals = hasBusinessApproverRole(me?.roleCodes, permissions);
|
const canAccessApprovals = hasBusinessApproverRole(me?.roleCodes, permissions);
|
||||||
const canAccessFinance = hasFinanceRole(me?.roleCodes, permissions);
|
|
||||||
|
|
||||||
const profileQuery = useQuery({
|
const profileQuery = useQuery({
|
||||||
enabled: Boolean(me?.id),
|
enabled: Boolean(me?.id),
|
||||||
@@ -116,8 +115,7 @@ export function MobileMePage() {
|
|||||||
<MobileActionLink to="/mobile/leave" label="请假记录" icon="event_available" />
|
<MobileActionLink to="/mobile/leave" label="请假记录" icon="event_available" />
|
||||||
<MobileActionLink to="/mobile/reimbursements" label="报销" icon="request_quote" />
|
<MobileActionLink to="/mobile/reimbursements" label="报销" icon="request_quote" />
|
||||||
{canAccessPurchase ? <MobileActionLink to="/mobile/purchase-requests" label="采购" icon="shopping_cart" /> : null}
|
{canAccessPurchase ? <MobileActionLink to="/mobile/purchase-requests" label="采购" icon="shopping_cart" /> : null}
|
||||||
<MobileActionLink to="/mobile/approvals" label={canAccessApprovals ? "待办" : "进度"} icon="fact_check" />
|
<MobileActionLink to="/mobile/approvals" label={canAccessApprovals ? "待我处理" : "进度"} icon="fact_check" />
|
||||||
{canAccessFinance ? <MobileActionLink to="/mobile#finance" label="财务" description="审批与付款状态" icon="account_balance_wallet" /> : null}
|
|
||||||
{canAccessAssets ? <MobileActionLink to="/mobile/assets/scan" label="资产扫码" icon="inventory_2" /> : null}
|
{canAccessAssets ? <MobileActionLink to="/mobile/assets/scan" label="资产扫码" icon="inventory_2" /> : null}
|
||||||
</div>
|
</div>
|
||||||
</SectionPanel>
|
</SectionPanel>
|
||||||
|
|||||||
@@ -182,6 +182,10 @@ export function MobilePortalPage() {
|
|||||||
.slice(0, 3);
|
.slice(0, 3);
|
||||||
const financeSummary = portalHomeQuery.data?.financeSummary ?? null;
|
const financeSummary = portalHomeQuery.data?.financeSummary ?? null;
|
||||||
const paymentBatchEnabled = Boolean(financeSummary?.paymentBatchEnabled);
|
const paymentBatchEnabled = Boolean(financeSummary?.paymentBatchEnabled);
|
||||||
|
const financePendingCount = financeSummary?.pendingBatchCount ?? 0;
|
||||||
|
const financeExceptionCount = (financeSummary?.failedItemCount ?? 0) + (financeSummary?.riskCount ?? 0);
|
||||||
|
const shouldShowFinanceCard = canUseFinance && !portalHomeQuery.isLoading && !portalHomeQuery.isError && (financePendingCount > 0 || financeExceptionCount > 0);
|
||||||
|
const financeCardBadge = paymentBatchEnabled ? "复核与付款" : financePendingCount > 0 ? "待付款" : "异常与复核";
|
||||||
|
|
||||||
async function openHomeNotification(item: NotificationItem) {
|
async function openHomeNotification(item: NotificationItem) {
|
||||||
const targetLink = resolveMobileNotificationTargetLink(item);
|
const targetLink = resolveMobileNotificationTargetLink(item);
|
||||||
@@ -261,7 +265,7 @@ export function MobilePortalPage() {
|
|||||||
<div className="mobile-section-header mobile-section-header--top">
|
<div className="mobile-section-header mobile-section-header--top">
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<div className="mobile-section-title">待处理</div>
|
<div className="mobile-section-title">待处理</div>
|
||||||
<div className="mobile-list-card__subtitle mt-1">待我审批</div>
|
<div className="mobile-list-card__subtitle mt-1">待我处理</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-right">
|
<div className="text-right">
|
||||||
<div className="text-3xl font-semibold leading-none text-zinc-900" data-testid="mobile-pending-summary-count">
|
<div className="text-3xl font-semibold leading-none text-zinc-900" data-testid="mobile-pending-summary-count">
|
||||||
@@ -305,42 +309,26 @@ export function MobilePortalPage() {
|
|||||||
</section>
|
</section>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{canUseFinance ? (
|
{shouldShowFinanceCard ? (
|
||||||
<section id="finance" tabIndex={-1} className="mobile-card mobile-page-stack" data-testid="mobile-finance-summary">
|
<section id="finance" tabIndex={-1} className="mobile-card mobile-page-stack" data-testid="mobile-finance-summary">
|
||||||
<div className="mobile-section-header">
|
<div className="mobile-section-header">
|
||||||
<div className="mobile-section-title">财务</div>
|
<div className="mobile-section-title">财务</div>
|
||||||
<span className="badge-neutral">{paymentBatchEnabled ? "复核与付款" : "审批与台账"}</span>
|
<span className="badge-neutral">{financeCardBadge}</span>
|
||||||
</div>
|
</div>
|
||||||
{portalHomeQuery.isLoading ? <p className="text-sm text-zinc-500">读取中...</p> : null}
|
<div className="mobile-metric-grid text-sm">
|
||||||
{portalHomeQuery.isError ? (
|
<div className="rounded-lg bg-zinc-50 px-3 py-2">
|
||||||
<p className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600">
|
<div className="text-xs text-zinc-500">待付款</div>
|
||||||
{resolveErrorMessage(portalHomeQuery.error)}
|
<div className="mt-1 text-xl font-semibold text-zinc-900">{financePendingCount}</div>
|
||||||
</p>
|
|
||||||
) : null}
|
|
||||||
{!portalHomeQuery.isLoading && !portalHomeQuery.isError && paymentBatchEnabled ? (
|
|
||||||
<div className="mobile-metric-grid text-sm">
|
|
||||||
<div className="rounded-lg bg-zinc-50 px-3 py-2">
|
|
||||||
<div className="text-xs text-zinc-500">待付款</div>
|
|
||||||
<div className="mt-1 text-xl font-semibold text-zinc-900">{financeSummary?.pendingBatchCount ?? 0}</div>
|
|
||||||
</div>
|
|
||||||
<div className="rounded-lg bg-zinc-50 px-3 py-2">
|
|
||||||
<div className="text-xs text-zinc-500">异常</div>
|
|
||||||
<div className="mt-1 text-xl font-semibold text-zinc-900">
|
|
||||||
{(financeSummary?.failedItemCount ?? 0) + (financeSummary?.riskCount ?? 0)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="rounded-lg bg-zinc-50 px-3 py-2">
|
|
||||||
<div className="text-xs text-zinc-500">电脑端</div>
|
|
||||||
<div className="mt-1 text-xl font-semibold text-zinc-900">处理台账</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
<div className="rounded-lg bg-zinc-50 px-3 py-2">
|
||||||
{!portalHomeQuery.isLoading && !portalHomeQuery.isError && !paymentBatchEnabled ? (
|
<div className="text-xs text-zinc-500">异常</div>
|
||||||
<div className="mobile-subcard text-sm text-zinc-600" data-testid="mobile-finance-disabled-state">
|
<div className="mt-1 text-xl font-semibold text-zinc-900">{financeExceptionCount}</div>
|
||||||
<div className="font-medium text-zinc-900">付款批次未启用</div>
|
|
||||||
<div className="mt-1 leading-5">可查看已流转到财务审批的报销单,待付款和异常批次暂不展示。</div>
|
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
<div className="rounded-lg bg-zinc-50 px-3 py-2">
|
||||||
|
<div className="text-xs text-zinc-500">处理</div>
|
||||||
|
<div className="mt-1 text-xl font-semibold text-zinc-900">电脑端</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
|||||||
@@ -193,8 +193,12 @@ export function MobilePurchaseRequestsPage() {
|
|||||||
|
|
||||||
<section className="mobile-card" data-testid="mobile-purchase-requests-list">
|
<section className="mobile-card" data-testid="mobile-purchase-requests-list">
|
||||||
<div className="mobile-command-surface mb-4">
|
<div className="mobile-command-surface mb-4">
|
||||||
|
<label className="text-xs font-medium text-zinc-500" htmlFor="mobile-purchase-current-search">
|
||||||
|
当前列表搜索
|
||||||
|
</label>
|
||||||
<Input
|
<Input
|
||||||
aria-label="搜索采购"
|
id="mobile-purchase-current-search"
|
||||||
|
aria-label="当前列表搜索"
|
||||||
value={searchInput}
|
value={searchInput}
|
||||||
onChange={(event) => setSearchInput(event.target.value)}
|
onChange={(event) => setSearchInput(event.target.value)}
|
||||||
placeholder="搜索编号、标题、供应商"
|
placeholder="搜索编号、标题、供应商"
|
||||||
|
|||||||
@@ -293,8 +293,12 @@ export function MobileReimbursementsPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mobile-command-surface">
|
<div className="mobile-command-surface">
|
||||||
|
<label className="text-xs font-medium text-zinc-500" htmlFor="mobile-reimbursements-current-search">
|
||||||
|
当前列表搜索
|
||||||
|
</label>
|
||||||
<Input
|
<Input
|
||||||
aria-label="搜索报销"
|
id="mobile-reimbursements-current-search"
|
||||||
|
aria-label="当前列表搜索"
|
||||||
value={searchInput}
|
value={searchInput}
|
||||||
onChange={(event) => setSearchInput(event.target.value)}
|
onChange={(event) => setSearchInput(event.target.value)}
|
||||||
placeholder="搜索编号、说明、金额"
|
placeholder="搜索编号、说明、金额"
|
||||||
|
|||||||
@@ -918,10 +918,14 @@ test.describe("MIF7 移动信息架构整改", () => {
|
|||||||
await mobile.page.goto("/mobile/leave?view=records&status=approved");
|
await mobile.page.goto("/mobile/leave?view=records&status=approved");
|
||||||
await expect(mobile.page.getByTestId("mobile-leave-view-records")).toHaveAttribute("data-state", "active");
|
await expect(mobile.page.getByTestId("mobile-leave-view-records")).toHaveAttribute("data-state", "active");
|
||||||
await expect(mobile.page.getByTestId("mobile-leave-record-card")).toBeVisible();
|
await expect(mobile.page.getByTestId("mobile-leave-record-card")).toBeVisible();
|
||||||
|
await expect(mobile.page.getByTestId("mobile-leave-balances")).toBeVisible();
|
||||||
|
await expect(mobile.page.getByTestId("mobile-leave-balances")).toContainText("企业微信主办");
|
||||||
|
await expect(mobile.page.getByTestId("mobile-leave-balance-summary")).toContainText("4.0");
|
||||||
await expect(mobile.page.getByTestId("mobile-leave-form")).toHaveCount(0);
|
await expect(mobile.page.getByTestId("mobile-leave-form")).toHaveCount(0);
|
||||||
|
|
||||||
await mobile.page.goto("/mobile/purchase-requests?status=approved");
|
await mobile.page.goto("/mobile/purchase-requests?status=approved");
|
||||||
await expect(mobile.page.getByTestId("mobile-purchase-requests-list")).toBeVisible();
|
await expect(mobile.page.getByTestId("mobile-purchase-requests-list")).toBeVisible();
|
||||||
|
await expect(mobile.page.getByLabel("当前列表搜索")).toBeVisible();
|
||||||
await expect(mobile.page.getByTestId("mobile-purchase-request-card")).toBeVisible();
|
await expect(mobile.page.getByTestId("mobile-purchase-request-card")).toBeVisible();
|
||||||
await expect(mobile.page.getByTestId("mobile-purchase-tab-approved")).toHaveAttribute("data-state", "active");
|
await expect(mobile.page.getByTestId("mobile-purchase-tab-approved")).toHaveAttribute("data-state", "active");
|
||||||
} finally {
|
} finally {
|
||||||
@@ -930,7 +934,7 @@ test.describe("MIF7 移动信息架构整改", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test("审批人首页只保留轻待办摘要,审批页按待我审批和已处理分组", async ({ browser }) => {
|
test("审批人首页只保留轻待办摘要,审批页按待我处理和已处理分组", async ({ browser }) => {
|
||||||
const notifications = buildNotifications();
|
const notifications = buildNotifications();
|
||||||
const mobile = await newMobilePage(browser, 390);
|
const mobile = await newMobilePage(browser, 390);
|
||||||
try {
|
try {
|
||||||
@@ -986,7 +990,7 @@ test.describe("MIF7 移动信息架构整改", () => {
|
|||||||
await mobile.page.getByTestId("mobile-approvals-detail-header").getByRole("button", { name: "返回" }).click();
|
await mobile.page.getByTestId("mobile-approvals-detail-header").getByRole("button", { name: "返回" }).click();
|
||||||
await expect(mobile.page).toHaveURL(/\/mobile$/);
|
await expect(mobile.page).toHaveURL(/\/mobile$/);
|
||||||
await mobile.page.goto("/mobile/approvals");
|
await mobile.page.goto("/mobile/approvals");
|
||||||
await expect(mobile.page.getByRole("tab", { name: "待我审批" })).toBeVisible();
|
await expect(mobile.page.getByRole("tab", { name: "待我处理" })).toBeVisible();
|
||||||
await expect(mobile.page.getByRole("tab", { name: "已处理" })).toBeVisible();
|
await expect(mobile.page.getByRole("tab", { name: "已处理" })).toBeVisible();
|
||||||
await expect(mobile.page.getByTestId("mobile-approvals-queue")).toHaveCount(0);
|
await expect(mobile.page.getByTestId("mobile-approvals-queue")).toHaveCount(0);
|
||||||
await mobile.page.getByRole("tab", { name: "已处理" }).click();
|
await mobile.page.getByRole("tab", { name: "已处理" }).click();
|
||||||
@@ -1061,6 +1065,7 @@ test.describe("MIF7 移动信息架构整改", () => {
|
|||||||
await loginForMobile(mobile.page, "employee");
|
await loginForMobile(mobile.page, "employee");
|
||||||
|
|
||||||
await mobile.page.goto("/mobile/reimbursements");
|
await mobile.page.goto("/mobile/reimbursements");
|
||||||
|
await expect(mobile.page.getByLabel("当前列表搜索")).toBeVisible();
|
||||||
await mobile.page.getByTestId("mobile-reimbursements-tab-approved").click();
|
await mobile.page.getByTestId("mobile-reimbursements-tab-approved").click();
|
||||||
await expect(mobile.page).toHaveURL(/\/mobile\/reimbursements\?status=approved$/);
|
await expect(mobile.page).toHaveURL(/\/mobile\/reimbursements\?status=approved$/);
|
||||||
await mobile.page.evaluate(() => {
|
await mobile.page.evaluate(() => {
|
||||||
@@ -1116,16 +1121,18 @@ test.describe("MIF7 移动信息架构整改", () => {
|
|||||||
await loginForMobile(financeMobile.page, "finance");
|
await loginForMobile(financeMobile.page, "finance");
|
||||||
await financeMobile.page.goto("/mobile");
|
await financeMobile.page.goto("/mobile");
|
||||||
await expect(financeMobile.page.getByTestId("mobile-finance-summary")).toBeVisible();
|
await expect(financeMobile.page.getByTestId("mobile-finance-summary")).toBeVisible();
|
||||||
await expect(financeMobile.page.getByTestId("mobile-finance-summary")).toContainText("付款批次未启用");
|
await expect(financeMobile.page.getByTestId("mobile-finance-summary")).toContainText("待付款");
|
||||||
await expect(financeMobile.page.getByTestId("mobile-finance-disabled-state")).toBeVisible();
|
await expect(financeMobile.page.getByTestId("mobile-finance-summary")).toContainText("异常");
|
||||||
|
await expect(financeMobile.page.getByTestId("mobile-finance-summary")).toContainText("4");
|
||||||
|
await expect(financeMobile.page.getByTestId("mobile-finance-disabled-state")).toHaveCount(0);
|
||||||
await expectNoMobileShellRegression(financeMobile.page);
|
await expectNoMobileShellRegression(financeMobile.page);
|
||||||
await financeMobile.page.screenshot({ path: screenshotPath("home-finance-summary"), fullPage: true });
|
await financeMobile.page.screenshot({ path: screenshotPath("home-finance-summary"), fullPage: true });
|
||||||
await financeMobile.page.goto("/mobile/me");
|
await financeMobile.page.goto("/mobile/me");
|
||||||
await expect(financeMobile.page.getByRole("link", { name: /财务/ })).toBeVisible();
|
await expect(financeMobile.page.getByRole("link", { name: /财务/ })).toHaveCount(0);
|
||||||
await financeMobile.page.getByRole("link", { name: /财务/ }).click();
|
await financeMobile.page.goto("/mobile#finance");
|
||||||
await expect(financeMobile.page).toHaveURL(/\/mobile#finance$/);
|
await expect(financeMobile.page).toHaveURL(/\/mobile#finance$/);
|
||||||
await expect(financeMobile.page.getByTestId("mobile-finance-summary")).toBeInViewport();
|
await expect(financeMobile.page.getByTestId("mobile-finance-summary")).toBeInViewport();
|
||||||
await expect(financeMobile.page.getByTestId("mobile-finance-disabled-state")).toBeInViewport();
|
await expect(financeMobile.page.getByTestId("mobile-finance-disabled-state")).toHaveCount(0);
|
||||||
await expectNoMobileShellRegression(financeMobile.page);
|
await expectNoMobileShellRegression(financeMobile.page);
|
||||||
} finally {
|
} finally {
|
||||||
await financeMobile.context.close();
|
await financeMobile.context.close();
|
||||||
|
|||||||
@@ -493,11 +493,13 @@ test.describe("R124 企业微信移动入口矩阵", () => {
|
|||||||
|
|
||||||
await mobile.page.goto("/mobile");
|
await mobile.page.goto("/mobile");
|
||||||
await expect(mobile.page.getByTestId("mobile-nav-home")).toBeVisible();
|
await expect(mobile.page.getByTestId("mobile-nav-home")).toBeVisible();
|
||||||
|
await expect(mobile.page.getByTestId("mobile-finance-summary")).toHaveCount(0);
|
||||||
await expectMobilePageContract(mobile.page);
|
await expectMobilePageContract(mobile.page);
|
||||||
|
|
||||||
await mobile.page.getByTestId("mobile-nav-reimbursements").click();
|
await mobile.page.getByTestId("mobile-nav-reimbursements").click();
|
||||||
await expect(mobile.page).toHaveURL(/\/mobile\/reimbursements$/);
|
await expect(mobile.page).toHaveURL(/\/mobile\/reimbursements$/);
|
||||||
await expect(mobile.page.getByTestId("mobile-reimbursements-list")).toBeVisible();
|
await expect(mobile.page.getByTestId("mobile-reimbursements-list")).toBeVisible();
|
||||||
|
await expect(mobile.page.getByLabel("当前列表搜索")).toBeVisible();
|
||||||
await expectMobilePageContract(mobile.page);
|
await expectMobilePageContract(mobile.page);
|
||||||
|
|
||||||
await mobile.page.goto("/mobile/capture");
|
await mobile.page.goto("/mobile/capture");
|
||||||
@@ -524,7 +526,7 @@ test.describe("R124 企业微信移动入口矩阵", () => {
|
|||||||
|
|
||||||
await mobile.page.getByTestId("mobile-nav-approvals").click();
|
await mobile.page.getByTestId("mobile-nav-approvals").click();
|
||||||
await expect(mobile.page).toHaveURL(/\/mobile\/approvals$/);
|
await expect(mobile.page).toHaveURL(/\/mobile\/approvals$/);
|
||||||
await expect(mobile.page.getByRole("heading", { name: "待办", exact: true })).toBeVisible();
|
await expect(mobile.page.getByRole("heading", { level: 1, name: "待我处理", exact: true })).toBeVisible();
|
||||||
await expect(mobile.page.locator(".mobile-status-slot--approval").first()).toContainText("待处理");
|
await expect(mobile.page.locator(".mobile-status-slot--approval").first()).toContainText("待处理");
|
||||||
await expectMobilePageContract(mobile.page);
|
await expectMobilePageContract(mobile.page);
|
||||||
|
|
||||||
@@ -539,11 +541,9 @@ test.describe("R124 企业微信移动入口矩阵", () => {
|
|||||||
await mobile.page.screenshot({ path: screenshotPath("mobile-me") });
|
await mobile.page.screenshot({ path: screenshotPath("mobile-me") });
|
||||||
await expectMobilePageContract(mobile.page);
|
await expectMobilePageContract(mobile.page);
|
||||||
|
|
||||||
await expect(mobile.page.getByRole("link", { name: /财务/ })).toBeVisible();
|
await expect(mobile.page.getByRole("link", { name: /财务/ })).toHaveCount(0);
|
||||||
await mobile.page.getByRole("link", { name: /财务/ }).click();
|
await mobile.page.goto("/mobile#finance");
|
||||||
await expect(mobile.page).toHaveURL(/\/mobile#finance$/);
|
await expect(mobile.page.getByTestId("mobile-finance-summary")).toHaveCount(0);
|
||||||
await expect(mobile.page.getByTestId("mobile-finance-summary")).toBeInViewport();
|
|
||||||
await expect(mobile.page.getByTestId("mobile-finance-disabled-state")).toBeInViewport();
|
|
||||||
await expectMobilePageContract(mobile.page);
|
await expectMobilePageContract(mobile.page);
|
||||||
|
|
||||||
await mobile.page.goto("/mobile/me");
|
await mobile.page.goto("/mobile/me");
|
||||||
@@ -572,12 +572,16 @@ test.describe("R124 企业微信移动入口矩阵", () => {
|
|||||||
await mobile.page.getByRole("link", { name: /请假/ }).click();
|
await mobile.page.getByRole("link", { name: /请假/ }).click();
|
||||||
await expect(mobile.page).toHaveURL(/\/mobile\/leave$/);
|
await expect(mobile.page).toHaveURL(/\/mobile\/leave$/);
|
||||||
await expect(mobile.page.getByTestId("mobile-leave-records")).toBeVisible();
|
await expect(mobile.page.getByTestId("mobile-leave-records")).toBeVisible();
|
||||||
|
await expect(mobile.page.getByTestId("mobile-leave-balances")).toBeVisible();
|
||||||
|
await expect(mobile.page.getByTestId("mobile-leave-balances")).toContainText("企业微信主办");
|
||||||
|
await expect(mobile.page.getByTestId("mobile-leave-balance-summary")).toContainText("4.0");
|
||||||
await expectMobilePageContract(mobile.page);
|
await expectMobilePageContract(mobile.page);
|
||||||
|
|
||||||
await mobile.page.goto("/mobile/me");
|
await mobile.page.goto("/mobile/me");
|
||||||
await mobile.page.getByRole("link", { name: /报销/ }).click();
|
await mobile.page.getByRole("link", { name: /报销/ }).click();
|
||||||
await expect(mobile.page).toHaveURL(/\/mobile\/reimbursements$/);
|
await expect(mobile.page).toHaveURL(/\/mobile\/reimbursements$/);
|
||||||
await expect(mobile.page.getByTestId("mobile-reimbursements-list")).toBeVisible();
|
await expect(mobile.page.getByTestId("mobile-reimbursements-list")).toBeVisible();
|
||||||
|
await expect(mobile.page.getByLabel("当前列表搜索")).toBeVisible();
|
||||||
const draftCard = mobile.page.getByTestId("mobile-reimbursement-card").filter({ hasText: "R124 删除草稿" }).first();
|
const draftCard = mobile.page.getByTestId("mobile-reimbursement-card").filter({ hasText: "R124 删除草稿" }).first();
|
||||||
await expect(draftCard).toBeVisible();
|
await expect(draftCard).toBeVisible();
|
||||||
await draftCard.locator("summary").click();
|
await draftCard.locator("summary").click();
|
||||||
|
|||||||
+1
-1
@@ -119,7 +119,7 @@ A 类页面优先保证真实试运行路径顺畅;B 类页面优先保证台
|
|||||||
- 首页不使用常驻欢迎大卡片、说明型折叠区或入口集合。首屏应使用紧凑状态摘要、可行动事项列表和条件出现的 `团队卡点` / `管理提醒` / `我的进度`,没有真实数据的区域不显示。
|
- 首页不使用常驻欢迎大卡片、说明型折叠区或入口集合。首屏应使用紧凑状态摘要、可行动事项列表和条件出现的 `团队卡点` / `管理提醒` / `我的进度`,没有真实数据的区域不显示。
|
||||||
- `报销进度`、团队公告、试运行观测等不作为首页固定按钮或固定模块;只有存在可靠状态、真实阻塞或异常时才出现。
|
- `报销进度`、团队公告、试运行观测等不作为首页固定按钮或固定模块;只有存在可靠状态、真实阻塞或异常时才出现。
|
||||||
- 首页不承担常驻新手教学职责;首次欢迎提示只做一次性、可关闭的常用业务入口,最多 3 个,不放个人设置、通知中心或“了解流程”这类说明型入口。
|
- 首页不承担常驻新手教学职责;首次欢迎提示只做一次性、可关闭的常用业务入口,最多 3 个,不放个人设置、通知中心或“了解流程”这类说明型入口。
|
||||||
- 审批入口同时服务审批人和发起人:审批人处理待我审批,员工查看我发起的进度。
|
- 审批入口同时服务审批人和发起人:审批人处理待我处理,员工查看我发起的进度。
|
||||||
- 普通员工不应看到不可点或无权限的审批入口;审批、财务、负责人和管理员只能看到服务端允许的动作。
|
- 普通员工不应看到不可点或无权限的审批入口;审批、财务、负责人和管理员只能看到服务端允许的动作。
|
||||||
- 个人设置默认只放个人资料、密码/安全、通知偏好和企业微信绑定;组织归属、直属汇报和角色摘要可折叠展示,不默认暴露权限 code、role code、原始账号状态或完整员工档案维护入口。
|
- 个人设置默认只放个人资料、密码/安全、通知偏好和企业微信绑定;组织归属、直属汇报和角色摘要可折叠展示,不默认暴露权限 code、role code、原始账号状态或完整员工档案维护入口。
|
||||||
- 个人设置和管理配置彻底分离:组织、角色、审批规则、功能开关、主数据、审计日志、人员维护和系统配置统一进入 `管理`,不出现在普通员工路径或个人资料页里。
|
- 个人设置和管理配置彻底分离:组织、角色、审批规则、功能开关、主数据、审计日志、人员维护和系统配置统一进入 `管理`,不出现在普通员工路径或个人资料页里。
|
||||||
|
|||||||
在新工单中引用
屏蔽一个用户