feat:加一个网站发布公告会弹出来点确定我已知晓的弹窗;mock数据自动匹配接单模板;本地访问模式修改
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
import { SoundOutlined } from '@ant-design/icons'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { Button, Modal, Typography } from 'antd'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
import { fetchWorkerAnnouncement } from '@/services/worker'
|
||||
import {
|
||||
readAnnouncementAck,
|
||||
resolveAnnouncementVersion,
|
||||
shouldShowAnnouncementPopup,
|
||||
writeAnnouncementAck,
|
||||
} from '@/utils/announcement-ack'
|
||||
|
||||
/**
|
||||
* 打手端公告弹窗。
|
||||
*
|
||||
* 后台「打手端滚动公告」一改动正文,就会在打手端下次进入时弹出;必须点「我已知晓」
|
||||
* 才能关闭(遮罩/ESC 不生效)。确认结果按公告内容指纹记在 localStorage,
|
||||
* 同一份公告只弹一次,改了正文会重新弹。
|
||||
*/
|
||||
export default function WorkerAnnouncementPopup() {
|
||||
const [acknowledgedVersion, setAcknowledgedVersion] = useState(() => readAnnouncementAck())
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
const announcementQuery = useQuery({
|
||||
queryKey: ['worker-announcement'],
|
||||
queryFn: fetchWorkerAnnouncement,
|
||||
retry: false,
|
||||
// 全局默认 staleTime: Infinity,公告需要在下一次进入页面时重新拉取
|
||||
staleTime: 5 * 60_000,
|
||||
})
|
||||
const announcement = announcementQuery.data?.data
|
||||
const content = String(announcement?.content || '').trim()
|
||||
|
||||
useEffect(() => {
|
||||
const shouldShow = shouldShowAnnouncementPopup({
|
||||
enabled: announcement?.enabled,
|
||||
content,
|
||||
acknowledgedVersion,
|
||||
})
|
||||
if (shouldShow) setOpen(true)
|
||||
}, [announcement?.enabled, content, acknowledgedVersion])
|
||||
|
||||
function acknowledge() {
|
||||
const version = resolveAnnouncementVersion(content)
|
||||
writeAnnouncementAck(version)
|
||||
setAcknowledgedVersion(version)
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
title={
|
||||
<span className="worker-announcement-popup-title">
|
||||
<SoundOutlined /> 公告
|
||||
</span>
|
||||
}
|
||||
closable={false}
|
||||
maskClosable={false}
|
||||
keyboard={false}
|
||||
centered
|
||||
width={460}
|
||||
className="worker-announcement-popup"
|
||||
footer={
|
||||
<Button type="primary" block onClick={acknowledge}>
|
||||
我已知晓
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Typography.Paragraph className="worker-announcement-popup-content">
|
||||
{content}
|
||||
</Typography.Paragraph>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
} from '@/utils/worker-auth'
|
||||
import { useRealtimeEvents, type RealtimeEvent } from '@/hooks/useRealtimeEvents'
|
||||
import WorkerAnnouncementBanner from '@/components/worker/WorkerAnnouncementBanner'
|
||||
import WorkerAnnouncementPopup from '@/components/worker/WorkerAnnouncementPopup'
|
||||
import WorkerNewOrderSpeech from '@/components/worker/WorkerNewOrderSpeech'
|
||||
|
||||
const menuItems: MenuProps['items'] = [
|
||||
@@ -191,6 +192,7 @@ export default function WorkerLayout() {
|
||||
|
||||
<main className="worker-mobile-content">
|
||||
<WorkerAnnouncementBanner />
|
||||
<WorkerAnnouncementPopup />
|
||||
<Outlet />
|
||||
</main>
|
||||
|
||||
@@ -285,6 +287,7 @@ export default function WorkerLayout() {
|
||||
<Layout className="admin-main-shell">
|
||||
<Layout.Content className="admin-content">
|
||||
<WorkerAnnouncementBanner />
|
||||
<WorkerAnnouncementPopup />
|
||||
<Outlet />
|
||||
</Layout.Content>
|
||||
</Layout>
|
||||
|
||||
@@ -226,7 +226,7 @@ function AnnouncementCard() {
|
||||
name="content"
|
||||
label="公告内容"
|
||||
rules={[{ required: true, whitespace: true, message: '请填写公告内容' }]}
|
||||
extra="公告会显示在打手端各页面顶部并横向滚动。"
|
||||
extra="公告会显示在打手端各页面顶部并横向滚动;正文一改动,打手端下次进入会弹出公告弹窗,需点「我已知晓」才关闭。"
|
||||
>
|
||||
<Input.TextArea rows={4} maxLength={300} showCount placeholder="请输入打手端公告内容" />
|
||||
</Form.Item>
|
||||
|
||||
@@ -73,6 +73,24 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* 公告弹窗:正文保留换行,长公告可滚动 */
|
||||
.worker-announcement-popup-title {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.worker-announcement-popup-content {
|
||||
margin: 0 !important;
|
||||
max-height: 50vh;
|
||||
overflow-y: auto;
|
||||
color: #1f2937;
|
||||
font-size: 14px;
|
||||
line-height: 1.75;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.worker-page-head h3,
|
||||
.worker-page-head > .ant-typography {
|
||||
margin: 0 !important;
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* 打手端公告弹窗的「已确认」记忆。
|
||||
*
|
||||
* 后台公告只有一条全局配置(`worker_announcement = { enabled, content }`),没有公告 ID,
|
||||
* 因此这里用**内容指纹**充当版本号:只要运营改了公告正文,指纹就变,打手端会重新弹一次。
|
||||
* 确认记录存在 localStorage(方案 B:服务端不落库),换设备或清缓存后会再弹一次。
|
||||
*/
|
||||
|
||||
const ANNOUNCEMENT_ACK_STORAGE_KEY = 'order-site-worker-announcement-ack'
|
||||
|
||||
type AnnouncementPopupState = {
|
||||
enabled?: boolean
|
||||
content?: string
|
||||
/** 本地已确认过的公告版本(来自 localStorage) */
|
||||
acknowledgedVersion?: string
|
||||
}
|
||||
|
||||
/** 公告正文的内容指纹(FNV-1a 32 位),空内容返回空串表示「没有可确认的公告」。 */
|
||||
export function resolveAnnouncementVersion(content: string): string {
|
||||
const normalized = String(content || '').trim()
|
||||
if (!normalized) return ''
|
||||
let hash = 0x811c9dc5
|
||||
for (let index = 0; index < normalized.length; index += 1) {
|
||||
hash ^= normalized.charCodeAt(index)
|
||||
hash = Math.imul(hash, 0x01000193) >>> 0
|
||||
}
|
||||
return `v1-${hash.toString(16).padStart(8, '0')}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否需要弹窗:公告开启 + 有正文 + 当前版本还没被这台设备确认过。
|
||||
* 已确认过的版本不再弹;运营改了正文(版本变化)后会重新弹。
|
||||
*/
|
||||
export function shouldShowAnnouncementPopup(state: AnnouncementPopupState): boolean {
|
||||
if (state.enabled !== true) return false
|
||||
const version = resolveAnnouncementVersion(state.content || '')
|
||||
if (!version) return false
|
||||
return state.acknowledgedVersion !== version
|
||||
}
|
||||
|
||||
/** 读取本机已确认的公告版本(隐私模式/禁用存储时返回空串,页面照常可用)。 */
|
||||
export function readAnnouncementAck(): string {
|
||||
try {
|
||||
if (typeof window === 'undefined') return ''
|
||||
return window.localStorage.getItem(ANNOUNCEMENT_ACK_STORAGE_KEY) || ''
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
/** 记录本机已确认的公告版本。 */
|
||||
export function writeAnnouncementAck(version: string): void {
|
||||
if (!version) return
|
||||
try {
|
||||
if (typeof window === 'undefined') return
|
||||
window.localStorage.setItem(ANNOUNCEMENT_ACK_STORAGE_KEY, version)
|
||||
} catch {
|
||||
// 存储不可用(隐私模式/配额满)时忽略,下一次打开会再弹一次。
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,7 @@ type ResolveCameraEntryOptions = {
|
||||
* 桌面端(宽度 > 768)隐藏,避免出现点了只会打开文件选择的无效入口。
|
||||
*
|
||||
* 说明:这里用 `<input type="file" capture="environment">` 调起系统相机,
|
||||
* 不依赖 getUserMedia,因此在局域网 HTTP(如 http://192.168.10.8:8080)下同样可用。
|
||||
* 不依赖 getUserMedia,因此在纯 HTTP(如 http://worker.localhost/)下同样可用。
|
||||
*/
|
||||
export function resolveCameraEntryEnabled(options: ResolveCameraEntryOptions): boolean {
|
||||
if (typeof options.allowCamera === 'boolean') return options.allowCamera
|
||||
|
||||
Reference in New Issue
Block a user