import { useQuery, useQueryClient } from '@tanstack/react-query'
import { ReloadOutlined, SaveOutlined } from '@ant-design/icons'
import {
Alert,
App,
Button,
Card,
Form,
Input,
InputNumber,
Popconfirm,
Space,
Switch,
Tag,
Typography,
} from 'antd'
import { useEffect, useState } from 'react'
import {
addAdminMockCustomShop,
deleteAdminMockCustomShop,
fetchAdminMockCustomShops,
fetchAdminScheduledJobsConfig,
fetchAdminWorkerAnnouncement,
renameAdminMockCustomShop,
runAdminScheduledJob,
saveAdminWorkerAnnouncement,
saveAdminScheduledJobsConfig,
} from '@/services/admin'
import type { AdminScheduledJobItem, AdminScheduledJobRuntimeState } from '@/types/admin'
import type { WorkerAnnouncementConfig } from '@/types/worker-platform'
import { formatAdminDateTime } from '@/utils/admin-time'
import HallConfigPanel from './HallConfigPanel'
function isDepositUnfreezeJob(job: AdminScheduledJobItem) {
return job.type === 'deposit_unfreeze' || job.id === 'deposit-unfreeze'
}
export default function AnnouncementPanel() {
return (
)
}
function DepositUnfreezeCard() {
const { message } = App.useApp()
const queryClient = useQueryClient()
const [saving, setSaving] = useState(false)
const [running, setRunning] = useState(false)
const [draft, setDraft] = useState(null)
const jobsQuery = useQuery({
queryKey: ['admin-scheduled-jobs'],
queryFn: fetchAdminScheduledJobsConfig,
})
const source = jobsQuery.data?.data
const currentJob = source?.source.jobs.find(isDepositUnfreezeJob) || null
const runtime: AdminScheduledJobRuntimeState | null =
source?.runtime.find((item) => item.id === (draft?.id || currentJob?.id)) || null
useEffect(() => {
setDraft(currentJob ? { ...currentJob } : null)
}, [currentJob])
function patchJob(patch: Partial) {
setDraft((prev) => (prev ? { ...prev, ...patch } : prev))
}
async function saveJob() {
if (!draft || !source) return
setSaving(true)
try {
await saveAdminScheduledJobsConfig({
...source.source,
jobs: source.source.jobs.map((item) => (item.id === draft.id ? draft : item)),
})
await queryClient.invalidateQueries({ queryKey: ['admin-scheduled-jobs'] })
message.success('押金解冻配置已保存')
} catch (error) {
message.error(error instanceof Error ? error.message : '保存押金解冻配置失败')
} finally {
setSaving(false)
}
}
async function runNow() {
const jobId = draft?.id || currentJob?.id || ''
if (!jobId) return
setRunning(true)
try {
await runAdminScheduledJob(jobId)
await queryClient.invalidateQueries({ queryKey: ['admin-scheduled-jobs'] })
message.success('押金解冻任务已执行')
} catch (error) {
message.error(error instanceof Error ? error.message : '执行押金解冻失败')
} finally {
setRunning(false)
}
}
return (
} loading={running} onClick={() => void runNow()}>
立即解冻
}
loading={saving}
disabled={!draft}
onClick={() => void saveJob()}
>
保存
}
>
验收后的报酬与押金进入待解冻队列,到期自动转入打手可用余额;
解冻天数与报酬延迟在「财务」面板配置,调小后存量待解冻也会按新天数提前到期。
{draft ? (
patchJob({ enabled })}
/>
{draft.enabled !== false ? '已启用' : '已停用'}
执行间隔(秒)
patchJob({ intervalSeconds: Number(value || 60) })}
/>
每次解冻数量
patchJob({ config: { ...draft.config, scanLimit: Number(value || 100) } })
}
/>
) : (
加载中…
)}
{runtime ? (
) : null}
)
}
function AnnouncementCard() {
const { message } = App.useApp()
const queryClient = useQueryClient()
const [form] = Form.useForm()
const [saving, setSaving] = useState(false)
const announcementQuery = useQuery({
queryKey: ['admin-worker-announcement'],
queryFn: fetchAdminWorkerAnnouncement,
})
useEffect(() => {
if (announcementQuery.data?.data) form.setFieldsValue(announcementQuery.data.data)
}, [announcementQuery.data, form])
async function saveAnnouncement(values: WorkerAnnouncementConfig) {
setSaving(true)
try {
await saveAdminWorkerAnnouncement({
enabled: values.enabled !== false,
content: String(values.content || '').trim(),
})
await queryClient.invalidateQueries({ queryKey: ['admin-worker-announcement'] })
message.success('公告已保存')
} catch (error) {
message.error(error instanceof Error ? error.message : '保存公告失败')
} finally {
setSaving(false)
}
}
return (
关闭后打手端不显示公告栏。
)
}
function MockCustomShopsCard() {
const { message, modal } = App.useApp()
const queryClient = useQueryClient()
const [adding, setAdding] = useState(false)
const [addForm] = Form.useForm<{ name: string }>()
const customShopsQuery = useQuery({
queryKey: ['admin-mock-custom-shops'],
queryFn: fetchAdminMockCustomShops,
})
const shops = customShopsQuery.data?.data.items || []
async function refresh() {
await queryClient.invalidateQueries({ queryKey: ['admin-mock-custom-shops'] })
}
async function runShopAction(action: () => Promise, successMessage: string) {
try {
await action()
await refresh()
message.success(successMessage)
} catch (error) {
message.error(error instanceof Error ? error.message : '操作失败')
}
}
async function addShop(values: { name: string }) {
setAdding(true)
try {
await runShopAction(
() => addAdminMockCustomShop(String(values.name || '').trim()),
'自定义店铺已新增',
)
addForm.resetFields()
} finally {
setAdding(false)
}
}
function renameShop(name: string) {
let nextName = ''
modal.confirm({
title: '重命名自定义店铺',
content: (
{
nextName = String(event.target.value || '')
}}
/>
),
onOk: async () => {
const trimmed = nextName.trim()
if (!trimmed || trimmed === name) return
await runShopAction(() => renameAdminMockCustomShop(name, trimmed), '自定义店铺已重命名')
},
})
}
return (
在接单工单「本地 Mock」中录入过的外部平台店铺会自动记忆到这里;也可以直接维护,
维护后的店铺会出现在 Mock 表单的下拉选项中。
{shops.length === 0 ? (
还没有自定义店铺,先在上方新增或通过本地 Mock 录入。
) : (
{shops.map((name) => (
{name}
runShopAction(() => deleteAdminMockCustomShop(name), '自定义店铺已删除')
}
>
))}
)}
)
}