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 ( } > 验收后的报酬与押金进入待解冻队列,到期自动转入打手可用余额; 解冻天数与报酬延迟在「财务」面板配置,调小后存量待解冻也会按新天数提前到期。 {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), '自定义店铺已删除') } > ))} )}
) }