feat(worker-platform): 抢单大厅首页新增联系客服胶囊入口并抽象通用客服弹窗组件

1. 抽离打手端通用联系客服弹窗组件 WorkerContactModal,统一维护微信二维码、打手账号/状态/余额回显、一键复制及沟通提示;
2. 抢单大厅移动端顶部状态栏(余额上限右侧首屏黄金位)新增精致微动效【联系客服】胶囊按钮,大幅缩短找客服沟通链路;
3. 抢单大厅桌面端头部操作栏同步补齐【联系客服】按钮,并在个人中心账号弹窗中统一复用 WorkerContactModal;
4. 适配移动端与超窄屏自适应响应式样式,完成前端59项测试与全量打包构建验证。
This commit is contained in:
yml2213
2026-09-21 12:37:38 +08:00
parent a4f6f5ed9b
commit 0df5c9f34e
5 changed files with 210 additions and 54 deletions
@@ -0,0 +1,79 @@
import { Alert, Button, Descriptions, Image, Modal, Space, Typography } from 'antd'
import { formatMoney, formatWorkerStatus } from '@/pages/worker/worker-profile-view-utils'
import type { WorkerFinanceConfig, WorkerUser } from '@/types/worker-platform'
export type WorkerContactModalProps = {
open: boolean
onClose: () => void
isMobile?: boolean
worker?: WorkerUser | null
financeConfig?: WorkerFinanceConfig | null
title?: string
}
export function WorkerContactModal({
open,
onClose,
isMobile = false,
worker,
financeConfig,
title = '联系客服',
}: WorkerContactModalProps) {
return (
<Modal
title={title}
open={open}
width={isMobile ? '92%' : 500}
destroyOnHidden
footer={
<Button type="primary" onClick={onClose}>
我知道了
</Button>
}
onCancel={onClose}
>
<Space direction="vertical" size={16} className="full-width">
<Alert
showIcon
type="info"
message="如需取消已接订单,请扫码联系客服处理。充值、提现与审核问题也由后台人工客服处理。"
/>
{financeConfig?.adminContact?.wechatQrCodeImage ? (
<div style={{ textAlign: 'center' }}>
<Image
width={200}
src={
financeConfig.adminContact.wechatQrCodeImage.mediumUrl ||
financeConfig.adminContact.wechatQrCodeImage.url
}
alt="客服微信二维码"
/>
</div>
) : (
<Typography.Text type="secondary">
客服暂未配置微信二维码,请按下方账号信息联系。
</Typography.Text>
)}
<Descriptions column={1} bordered size="small">
<Descriptions.Item label="账号">
<Typography.Text copyable>{worker?.username || '-'}</Typography.Text>
</Descriptions.Item>
<Descriptions.Item label="昵称">{worker?.displayName || '-'}</Descriptions.Item>
<Descriptions.Item label="手机号">
<Typography.Text copyable>{worker?.phone || '-'}</Typography.Text>
</Descriptions.Item>
<Descriptions.Item label="账号状态">
{formatWorkerStatus(worker?.status || '')}
</Descriptions.Item>
<Descriptions.Item label="当前余额">
{formatMoney(worker?.wallet?.availableAmount)}
</Descriptions.Item>
</Descriptions>
<Typography.Paragraph className="worker-profile-help-text">
建议沟通时一起提供:申请类型、金额、订单号或转账备注、收款方式,以及当前账号信息。
</Typography.Paragraph>
</Space>
</Modal>
)
}
@@ -3,6 +3,7 @@ import {
ArrowRightOutlined,
CheckCircleOutlined,
ClockCircleOutlined,
CustomerServiceOutlined,
FireOutlined,
PushpinOutlined,
QuestionCircleOutlined,
@@ -35,6 +36,8 @@ import {
import { useEffect, useRef, useState } from 'react'
import { Link } from 'react-router'
import { WorkerContactModal } from '@/components/worker/WorkerContactModal'
import {
fetchWorkerHallCategories,
fetchWorkerHallOrders,
@@ -83,6 +86,7 @@ export default function WorkerHallPage() {
const [grabbingId, setGrabbingId] = useState(0)
const [sharingOrder, setSharingOrder] = useState<WorkOrder | null>(null)
const [leaderboardOpen, setLeaderboardOpen] = useState(false)
const [contactOpen, setContactOpen] = useState(false)
const [leaderboardPeriod, setLeaderboardPeriod] = useState<WorkerLeaderboardPeriod>('all')
const [sharingQuantity, setSharingQuantity] = useState(1)
const [submittingSharing, setSubmittingSharing] = useState(false)
@@ -236,6 +240,7 @@ export default function WorkerHallPage() {
setFailedImageKeys({})
}, [allCategories.length, allIconUrl])
const worker = profileQuery.data?.data.worker
const financeConfig = profileQuery.data?.data.financeConfig
const isInternalWorker = worker?.workerType === 'internal'
const activeCount = Number(profileQuery.data?.data.summary.activeOrderCount || 0)
const pendingAcceptanceCount = hallSummaryQuery.data?.pendingAcceptanceCount || 0
@@ -411,6 +416,16 @@ export default function WorkerHallPage() {
</span>
</div>
<button
type="button"
className="worker-hall-mobile-contact-pill"
onClick={() => setContactOpen(true)}
aria-label="联系客服"
>
<CustomerServiceOutlined className="worker-hall-mobile-contact-icon" />
<span>联系客服</span>
</button>
<div className="worker-hall-mobile-top-actions">
<Tooltip title={`抢单确认已${grabConfirmEnabled ? '开启' : '关闭'}`}>
<Button
@@ -784,6 +799,9 @@ export default function WorkerHallPage() {
<Button icon={<TrophyOutlined />} onClick={() => setLeaderboardOpen(true)}>
接单排行榜
</Button>
<Button icon={<CustomerServiceOutlined />} onClick={() => setContactOpen(true)}>
联系客服
</Button>
</div>
</div>
@@ -1298,6 +1316,14 @@ export default function WorkerHallPage() {
) : null}
</div>
</Drawer>
<WorkerContactModal
open={contactOpen}
onClose={() => setContactOpen(false)}
isMobile={isMobile}
worker={worker}
financeConfig={financeConfig}
/>
</>
)
}
@@ -17,7 +17,8 @@ import type { WorkerFinanceConfig, WorkerSessionDevice, WorkerUser } from '@/typ
import { formatDateTime } from '@/utils/date-time'
import { isWorkerPasswordStrong, WORKER_PASSWORD_RULE_MESSAGE } from '@/utils/worker-password'
import { formatMoney, formatWorkerStatus } from './worker-profile-view-utils'
import { WorkerContactModal } from '@/components/worker/WorkerContactModal'
import { formatWorkerStatus } from './worker-profile-view-utils'
export type PasswordFormValues = {
currentPassword?: string
@@ -184,60 +185,13 @@ export function WorkerProfileAccountModals({
</Form>
</Modal>
<Modal
title="联系管理员"
<WorkerContactModal
open={contactOpen}
width={isMobile ? '92%' : 500}
destroyOnHidden
footer={
<Button type="primary" onClick={() => setContactOpen(false)}>
我知道了
</Button>
}
onCancel={() => setContactOpen(false)}
>
<Space direction="vertical" size={16} className="full-width">
<Alert
showIcon
type="info"
message="如需取消已接订单,请扫码联系管理员处理。充值、提现与审核问题也由后台管理员人工处理。"
/>
{financeConfig?.adminContact?.wechatQrCodeImage ? (
<div style={{ textAlign: 'center' }}>
<Image
width={200}
src={
financeConfig.adminContact.wechatQrCodeImage.mediumUrl ||
financeConfig.adminContact.wechatQrCodeImage.url
}
alt="管理员微信二维码"
/>
</div>
) : (
<Typography.Text type="secondary">
管理员暂未配置微信二维码,请按下方账号信息联系。
</Typography.Text>
)}
<Descriptions column={1} bordered size="small">
<Descriptions.Item label="账号">
<Typography.Text copyable>{worker?.username || '-'}</Typography.Text>
</Descriptions.Item>
<Descriptions.Item label="昵称">{worker?.displayName || '-'}</Descriptions.Item>
<Descriptions.Item label="手机号">
<Typography.Text copyable>{worker?.phone || '-'}</Typography.Text>
</Descriptions.Item>
<Descriptions.Item label="账号状态">
{formatWorkerStatus(worker?.status || '')}
</Descriptions.Item>
<Descriptions.Item label="当前余额">
{formatMoney(worker?.wallet.availableAmount)}
</Descriptions.Item>
</Descriptions>
<Typography.Paragraph className="worker-profile-help-text">
建议沟通时一起提供:申请类型、金额、订单号或转账备注、收款方式,以及当前账号信息。
</Typography.Paragraph>
</Space>
</Modal>
onClose={() => setContactOpen(false)}
isMobile={isMobile}
worker={worker}
financeConfig={financeConfig}
/>
<Modal
title="修改昵称"
+56
View File
@@ -2308,6 +2308,62 @@ body {
display: flex;
align-items: center;
gap: 8px;
flex-shrink: 0;
}
.worker-hall-mobile-contact-pill {
background: #fff7ed;
border: 1px solid #fed7aa;
border-radius: 999px;
padding: 4px 10px;
font-size: 12px;
font-weight: 600;
color: #c2410c;
display: inline-flex;
align-items: center;
gap: 4px;
cursor: pointer;
box-shadow: 0 1px 2px rgba(234, 88, 12, 0.08);
white-space: nowrap;
flex-shrink: 0;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
outline: none;
}
.worker-hall-mobile-contact-pill:hover,
.worker-hall-mobile-contact-pill:focus-visible {
background: #ffedd5;
border-color: #fdba74;
color: #9a3412;
}
.worker-hall-mobile-contact-pill:active {
transform: scale(0.96);
background: #fed7aa;
}
.worker-hall-mobile-contact-icon {
font-size: 13px;
color: #ea580c;
}
@media (max-width: 360px) {
.worker-hall-mobile-stat-pill {
padding: 4px 8px;
gap: 4px;
font-size: 11px;
}
.worker-hall-mobile-contact-pill {
padding: 4px 8px;
font-size: 11px;
}
}
@media (max-width: 330px) {
.worker-hall-mobile-contact-pill span {
display: none;
}
}
/* 游戏 / 一级分类横向卡片条 */
@@ -200,3 +200,44 @@
4. **自动化测试与全量构建**:
- 前端 59 个自动化测试全绿(**59 pass / 0 fail**);
- `format:check`、`lint:check`、`typecheck`、`build` 全部通过。
---
## 八、 专项优化:打手抢单大厅首页新增「联系客服」入口与通用客服弹窗组件(2026-09-21 已闭环)
### 1. 现状与用户诉求(User Feedback & Target)
- **用户反馈**:“首页 增加联系客服 现在联系客服藏的太深了”,并附带手机端首页截图,用红圈标注了移动端顶部状态栏(`余额 / 上限` 胶囊右侧、操作按钮区左侧的空白区域)。
- **目标**:
1. 在抢单大厅首页移动端顶部状态栏(红圈标注的核心首屏黄金位置)增加醒目高质感的【联系客服】胶囊按钮;
2. 在桌面端抢单大厅头部操作栏同步补齐【联系客服】入口,打通双端闭环;
3. 将原本深藏在个人中心底层的“联系客服/管理员”弹窗抽象为全站通用的 `WorkerContactModal` 组件,统一维护二维码、账号、余额等信息,提供一键复制与标准沟通提示。
### 2. 证据分类与代码级根因(Evidence & Root Cause)
- **已知信息 [确信度: 100%]**:
1. 官方公告栏循环播放:“☆接单请先个人中心联系客服☆方便客服处理”,证明新打手或接单打手频繁需要找客服沟通充值、提现或报备;
2. 原联系管理员弹窗代码内联在 `apps/frontend/src/pages/worker/WorkerProfileAccountModals.tsx:187-240`,打手必须切换至“个人中心”页面并向下滑动到底部才能找到入口,链路过长、深度太深;
3. `WorkerHallPage.tsx` 中的 `profileQuery.data?.data` 原生就包含了 `worker` 实体与 `financeConfig`(含管理员微信二维码配置 `adminContact.wechatQrCodeImage`),数据已就绪,仅缺前端呈现入口。
- **未知/风险预判 [确信度: 100%]**:
- 移动端 375px 或超窄屏(如 320px)下顶部栏元素横向空间是否会挤压拥挤;
- 应对对策:采用弹性缩放与媒体查询控制,超小屏下保持单行不折叠。
### 3. 解决对策与落地成果(Solution & Milestones)
1. **抽离通用联系客服弹窗组件**:
- 新建 `apps/frontend/src/components/worker/WorkerContactModal.tsx`,集中管理微信二维码大图、当前打手账号、昵称、手机号、账号状态、可用余额,以及一键复制和温馨沟通提示;
- 在 `WorkerProfileAccountModals.tsx` 与 `WorkerHallPage.tsx` 中全面复用该组件,消灭重复代码。
2. **移动端黄金位置新增【联系客服】胶囊按钮**:
- 在 `WorkerHallPage.tsx` 移动端顶部状态条(用户红圈位置)插入专属胶囊:
```html
<button class="worker-hall-mobile-contact-pill" onClick={() => setContactOpen(true)}>
<CustomerServiceOutlined />
<span>联系客服</span>
</button>
```
- 采用浅暖橙背景(`#fff7ed`)+ 优雅橙边框(`#fed7aa`)+ 微动效,与打手端整体暖橙调性浑然一体,点击即弹出客服微信与账号卡片。
3. **桌面端同步补齐入口**:
- 在桌面端头部操作按钮栏(刷新与排行榜旁)新增 `<Button icon={<CustomerServiceOutlined />} onClick={() => setContactOpen(true)}>联系客服</Button>`。
4. **验证门禁全绿通过 [确信度: 100%]**:
- 前端 59 个自动化测试全绿(**59 pass / 0 fail**);
- 前端 `format:check`、`lint:check`、`typecheck`、`build` 均零错误零警告通过;
- 后端 504 个自动化测试全绿(**501 pass / 3 skipped / 0 fail**)。