feat(admin): v3.5.12 大屏BGM、中文展示、分类删除修复与侧栏顺序

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-26 10:54:19 +08:00
parent 3afbdfe6b9
commit 58a8b47532
23 changed files with 773 additions and 111 deletions
@@ -1,5 +1,6 @@
import { useCallback, useEffect, useMemo, useRef, useState, type CSSProperties } from 'react';
import { request } from '../lib/api';
import bgmSrc from '../assets/big-screen/dukang-bgm.mp3';
import sfxTier1 from '../assets/big-screen/celebrate-t1.mp3';
import sfxTier2 from '../assets/big-screen/celebrate-t2.mp3';
import sfxTier3 from '../assets/big-screen/celebrate-t3.mp3';
@@ -33,6 +34,50 @@ const CELEBRATE_SFX: Record<AmountTier, string> = {
/** 浏览器自动播放策略:需用户手势后才能出声 */
let audioUnlocked = false;
const sharedAudio = typeof Audio !== 'undefined' ? new Audio() : null;
const BGM_VOLUME = 0.32;
const BGM_DUCK_VOLUME = 0.08;
let bgmAudio: HTMLAudioElement | null = null;
let bgmDucked = false;
function getBgmAudio(): HTMLAudioElement | null {
if (typeof Audio === 'undefined') return null;
if (!bgmAudio) {
bgmAudio = new Audio(bgmSrc);
bgmAudio.loop = true;
bgmAudio.preload = 'auto';
bgmAudio.volume = BGM_VOLUME;
}
return bgmAudio;
}
function applyBgmVolume() {
if (!bgmAudio) return;
bgmAudio.volume = bgmDucked ? BGM_DUCK_VOLUME : BGM_VOLUME;
}
function duckBgm() {
bgmDucked = true;
applyBgmVolume();
}
function unduckBgm() {
bgmDucked = false;
applyBgmVolume();
}
function setBgmPlaying(on: boolean): Promise<boolean> {
const audio = getBgmAudio();
if (!audio) return Promise.resolve(false);
applyBgmVolume();
if (!on) {
audio.pause();
return Promise.resolve(false);
}
return audio
.play()
.then(() => true)
.catch(() => false);
}
function unlockCelebrateAudio() {
if (audioUnlocked || !sharedAudio) return;
@@ -53,6 +98,7 @@ function unlockCelebrateAudio() {
function playCelebrateSfx(tier: AmountTier): () => void {
if (!sharedAudio) return () => undefined;
duckBgm();
try {
sharedAudio.pause();
sharedAudio.currentTime = 0;
@@ -72,6 +118,7 @@ function playCelebrateSfx(tier: AmountTier): () => void {
} catch {
/* ignore */
}
unduckBgm();
};
}
@@ -341,10 +388,40 @@ function CelebrateFx({ order, onDone }: { order: BigScreenOrder; onDone: () => v
);
}
function BgmToggleButton({ on, onToggle }: { on: boolean; onToggle: () => void }) {
return (
<button
type="button"
className={`big-screen-bgm${on ? ' is-on' : ''}`}
onClick={onToggle}
aria-pressed={on}
aria-label={on ? '暂停背景音乐' : '播放背景音乐'}
>
<span className="big-screen-bgm-icon" aria-hidden>
{on ? (
<svg viewBox="0 0 24 24" width="16" height="16">
<path
fill="currentColor"
d="M9 18V6h2v12H9zm4 0V6h2v12h-2z"
/>
</svg>
) : (
<svg viewBox="0 0 24 24" width="16" height="16">
<path fill="currentColor" d="M8 5.14v13.72L19.06 12 8 5.14z" />
</svg>
)}
</span>
{on ? 'BGM 播放中' : '播放 BGM'}
</button>
);
}
export default function BigScreenPage() {
const [items, setItems] = useState<BigScreenOrder[]>([]);
const [loadError, setLoadError] = useState('');
const [paused, setPaused] = useState(false);
const [bgmOn, setBgmOn] = useState(false);
const bgmWantedRef = useRef(false);
const [celebrate, setCelebrate] = useState<BigScreenOrder | null>(null);
const seenIdsRef = useRef<Set<string> | null>(null);
const celebratedIdsRef = useRef<Set<string>>(new Set());
@@ -416,6 +493,22 @@ export default function BigScreenPage() {
return () => clearInterval(id);
}, [fetchData]);
const toggleBgm = useCallback(() => {
unlockCelebrateAudio();
const next = !bgmWantedRef.current;
bgmWantedRef.current = next;
if (!next) {
void setBgmPlaying(false);
setBgmOn(false);
return;
}
setBgmOn(true);
void setBgmPlaying(true).then((playing) => {
if (!bgmWantedRef.current) return;
setBgmOn(playing);
});
}, []);
useEffect(() => {
unlockCelebrateAudio();
const unlock = () => unlockCelebrateAudio();
@@ -424,6 +517,8 @@ export default function BigScreenPage() {
return () => {
window.removeEventListener('pointerdown', unlock);
window.removeEventListener('keydown', unlock);
bgmWantedRef.current = false;
void setBgmPlaying(false);
};
}, []);
@@ -480,6 +575,7 @@ export default function BigScreenPage() {
<span className="big-screen-subtitle">· </span>
</div>
<div className="big-screen-live-wrap">
<BgmToggleButton on={bgmOn} onToggle={toggleBgm} />
<span className="big-screen-live-label"></span>
<span className="big-screen-live">
<span className="big-screen-live-dot" />
+32 -9
View File
@@ -4,6 +4,14 @@ import { Card, Descriptions, Drawer, Select, Space, Table, Tag } from 'antd';
import type { ColumnsType } from 'antd/es/table';
import { request } from '../lib/api';
import { fmtTime } from '../lib/constants';
import {
DOMAIN_EVENT_TYPE_LABELS,
domainParamFieldLabel,
domainParamValue,
formatRef,
zhCode,
zhLabel,
} from '../lib/display-labels';
import { useAdminListColumns } from '../lib/useAdminListColumns';
import { AdminPrimaryLink } from '../components/AdminPrimaryLink';
@@ -15,8 +23,11 @@ type Row = {
refId: string;
status?: string | null;
param1?: string | null;
param1Desc?: string | null;
param2?: string | null;
param2Desc?: string | null;
param3?: string | null;
param3Desc?: string | null;
remark?: string | null;
createdAt: string;
};
@@ -70,13 +81,18 @@ export default function DomainEventsPage() {
setDetail(await request<Row>(`/admin/logs/domain-events/${r.id}`));
}}
>
<Tag>{v}</Tag>
<Tag>{zhLabel(DOMAIN_EVENT_TYPE_LABELS, v)}</Tag>
</AdminPrimaryLink>
),
},
{ title: '关联', render: (_, r) => `${r.refType} #${r.refId}` },
{ title: '状态', dataIndex: 'status', width: 100 },
{ title: '摘要', render: (_, r) => r.param1 || r.remark || '—' },
{ title: '关联', render: (_, r) => formatRef(r.refType, r.refId) },
{ title: '状态', dataIndex: 'status', width: 100, render: (v) => zhCode(v) },
{
title: '摘要',
render: (_, r) => domainParamValue(r.param1Desc, r.param1) !== '—'
? domainParamValue(r.param1Desc, r.param1)
: (r.remark || '—'),
},
{
title: '操作',
width: 80,
@@ -133,11 +149,18 @@ export default function DomainEventsPage() {
{detail && (
<Descriptions column={1} size="small">
<Descriptions.Item label="ID">{detail.id}</Descriptions.Item>
<Descriptions.Item label="类型">{detail.eventType}</Descriptions.Item>
<Descriptions.Item label="关联">{`${detail.refType} #${detail.refId}`}</Descriptions.Item>
<Descriptions.Item label="param1">{detail.param1}</Descriptions.Item>
<Descriptions.Item label="param2">{detail.param2}</Descriptions.Item>
<Descriptions.Item label="param3">{detail.param3}</Descriptions.Item>
<Descriptions.Item label="类型">{zhLabel(DOMAIN_EVENT_TYPE_LABELS, detail.eventType)}</Descriptions.Item>
<Descriptions.Item label="关联">{formatRef(detail.refType, detail.refId)}</Descriptions.Item>
<Descriptions.Item label="状态">{zhCode(detail.status)}</Descriptions.Item>
<Descriptions.Item label={domainParamFieldLabel(detail.param1Desc, 1)}>
{domainParamValue(detail.param1Desc, detail.param1)}
</Descriptions.Item>
<Descriptions.Item label={domainParamFieldLabel(detail.param2Desc, 2)}>
{domainParamValue(detail.param2Desc, detail.param2)}
</Descriptions.Item>
<Descriptions.Item label={domainParamFieldLabel(detail.param3Desc, 3)}>
{domainParamValue(detail.param3Desc, detail.param3)}
</Descriptions.Item>
<Descriptions.Item label="备注">{detail.remark}</Descriptions.Item>
<Descriptions.Item label="时间">{fmtTime(detail.createdAt)}</Descriptions.Item>
</Descriptions>
+6 -5
View File
@@ -7,6 +7,7 @@ import { useSearchParams } from 'react-router-dom';
import { request } from '../lib/api';
import { AdminCellLine } from '../components/AdminCellLine';
import { fmtTime } from '../lib/constants';
import { formatRef, zhCode } from '../lib/display-labels';
import { HQ_OPERATION_ACTION_OPTIONS, resolveHqOperationLabel } from '../lib/hq-log';
import { useAdminList } from '../lib/useAdminList';
import { useAdminListColumns } from '../lib/useAdminListColumns';
@@ -84,7 +85,7 @@ export default function HqLogsPage() {
</AdminPrimaryLink>
),
},
{ title: '对象类型', dataIndex: 'refType', width: 120, render: (v) => v || '—' },
{ title: '对象类型', dataIndex: 'refType', width: 120, render: (v) => zhCode(v) },
{ title: '对象 ID', dataIndex: 'refId', width: 100, render: (v) => v || '—' },
{
title: '操作',
@@ -143,7 +144,7 @@ export default function HqLogsPage() {
/>
</Form.Item>
<Form.Item name="refType" label="对象类型">
<Input allowClear placeholder="ORDER / USER / CITY..." style={{ width: 140 }} />
<Input allowClear placeholder="对象编码" style={{ width: 140 }} />
</Form.Item>
<Form.Item>
<Space>
@@ -192,12 +193,12 @@ export default function HqLogsPage() {
secondary={[detail.hqPhone, detail.hqAccountId ? `ID:${detail.hqAccountId}` : ''].filter(Boolean).join(' ') || null}
/>
</Descriptions.Item>
<Descriptions.Item label="角色">{detail.hqRole || '—'}</Descriptions.Item>
<Descriptions.Item label="角色">{zhCode(detail.hqRole)}</Descriptions.Item>
<Descriptions.Item label="行为">
{detail.actionLabel || resolveHqOperationLabel(detail.action)}
</Descriptions.Item>
<Descriptions.Item label="对象">{detail.refType} / {detail.refId}</Descriptions.Item>
<Descriptions.Item label="状态">{detail.status || '—'}</Descriptions.Item>
<Descriptions.Item label="对象">{formatRef(detail.refType, detail.refId)}</Descriptions.Item>
<Descriptions.Item label="状态">{zhCode(detail.status)}</Descriptions.Item>
<Descriptions.Item label="备注">{detail.remark || '—'}</Descriptions.Item>
</Descriptions>
<Typography.Title level={5} style={{ marginTop: 16 }}>/</Typography.Title>
+2 -1
View File
@@ -39,6 +39,7 @@ import {
ORDER_STATUS_OPERATOR_LABELS,
fmtTime,
} from '../lib/constants';
import { zhRemark } from '../lib/display-labels';
import { FULFILLMENT_HOLD_REASON_LABELS } from '@dukang/shared-types';
import ProxyOrderModal from '../components/ProxyOrderModal';
import OrderTrackDrawer from '../components/OrderTrackDrawer';
@@ -1210,7 +1211,7 @@ export default function OrdersPage() {
{
title: '备注',
dataIndex: 'remark',
render: (v) => v || '—',
render: (v) => zhRemark(v),
},
{
title: '时间',
+21 -14
View File
@@ -4,15 +4,14 @@ import type { ColumnsType } from 'antd/es/table';
import { useSearchParams } from 'react-router-dom';
import {
PARTNER_LOG_CATEGORY_OPTIONS,
PARTNER_LOG_CATEGORY_LABELS,
PARTNER_STAFF_ROLE_LABELS,
resolvePartnerLogCategory,
type PartnerLogCategory,
type PartnerStaffRole,
} from '@dukang/shared-types';
import { request } from '../lib/api';
import { AdminCellLine } from '../components/AdminCellLine';
import { fmtTime } from '../lib/constants';
import { clientAppLabel, eventNameLabel, formatRef, partnerEventCategoryLabel } from '../lib/display-labels';
import { useAdminList } from '../lib/useAdminList';
import { useAdminListColumns } from '../lib/useAdminListColumns';
import { AdminListHeader } from '../components/AdminListHeader';
@@ -107,7 +106,7 @@ export default function PartnerLogsPage() {
dataIndex: 'category',
width: 100,
render: (v: PartnerLogCategory | null, r) => (
<Tag>{PARTNER_LOG_CATEGORY_LABELS[v ?? ''] || resolvePartnerLogCategory(r.eventName) || '其他'}</Tag>
<Tag>{partnerEventCategoryLabel(r.eventName, v)}</Tag>
),
},
{
@@ -121,14 +120,14 @@ export default function PartnerLogsPage() {
setDrawerOpen(true);
}}
>
{v}
{eventNameLabel(v)}
</AdminPrimaryLink>
),
},
{
title: '关联',
width: 120,
render: (_, r) => (r.refType && r.refId ? `${r.refType}#${r.refId}` : '—'),
render: (_, r) => formatRef(r.refType, r.refId),
},
{
title: '摘要',
@@ -188,11 +187,11 @@ export default function PartnerLogsPage() {
<Form.Item name="companyName" label="公司">
<Input placeholder="合伙人公司" allowClear style={{ width: 140 }} />
</Form.Item>
<Form.Item name="partnerId" label="合伙人ID">
<Input placeholder="partnerId" allowClear style={{ width: 120 }} />
<Form.Item name="partnerId" label="合伙人 ID">
<Input placeholder="合伙人 ID" allowClear style={{ width: 120 }} />
</Form.Item>
<Form.Item name="eventName" label="事件">
<Input placeholder="partner_sms_login" allowClear style={{ width: 160 }} />
<Form.Item name="eventName" label="事件编码">
<Input placeholder="事件编码" allowClear style={{ width: 160 }} />
</Form.Item>
<Form.Item>
<Space>
@@ -237,11 +236,19 @@ export default function PartnerLogsPage() {
<Drawer title="日志详情" open={drawerOpen} onClose={() => setDrawerOpen(false)} width={520}>
{detail && (
<Descriptions column={1} bordered size="small">
{Object.entries(detail).map(([k, v]) => (
<Descriptions.Item key={k} label={k}>
{typeof v === 'object' ? JSON.stringify(v, null, 2) : String(v ?? '—')}
</Descriptions.Item>
))}
<Descriptions.Item label="ID">{String(detail.id ?? '—')}</Descriptions.Item>
<Descriptions.Item label="合伙人">{String(detail.companyName || detail.partnerId || '—')}</Descriptions.Item>
<Descriptions.Item label="账号">{String(detail.accountName || detail.accountPhone || '—')}</Descriptions.Item>
<Descriptions.Item label="分类">{partnerEventCategoryLabel(String(detail.eventName ?? ''), detail.category ? String(detail.category) : null)}</Descriptions.Item>
<Descriptions.Item label="事件">{eventNameLabel(String(detail.eventName ?? ''))}</Descriptions.Item>
<Descriptions.Item label="端">{clientAppLabel(detail.clientApp ? String(detail.clientApp) : null)}</Descriptions.Item>
<Descriptions.Item label="关联">{formatRef(detail.refType ? String(detail.refType) : null, detail.refId ? String(detail.refId) : null)}</Descriptions.Item>
<Descriptions.Item label="时间">{fmtTime(detail.createdAt ? String(detail.createdAt) : null)}</Descriptions.Item>
<Descriptions.Item label="参数">
<pre style={{ margin: 0, whiteSpace: 'pre-wrap', wordBreak: 'break-all' }}>
{JSON.stringify(detail.extraJson ?? {}, null, 2)}
</pre>
</Descriptions.Item>
</Descriptions>
)}
</Drawer>
@@ -159,9 +159,20 @@ export default function StoreCategoriesPage() {
<Popconfirm
title={row.level === 1 ? '删除一级分类?若有门店占用将改为停用' : '删除该分类?若有门店占用将改为停用'}
onConfirm={async () => {
await request(`/admin/store-categories/${row.id}`, { method: 'DELETE' });
message.success('已处理');
void reload();
try {
const res = await request<{ deleted?: boolean; softDisabled?: boolean }>(
`/admin/store-categories/${row.id}`,
{ method: 'DELETE' },
);
if (res.softDisabled) {
message.success('该分类已有门店占用,已改为停用');
} else {
message.success('已删除');
}
void reload();
} catch (e) {
message.error(e instanceof Error ? e.message : '删除失败');
}
}}
>
<Button type="link" size="small" danger></Button>
+9 -10
View File
@@ -4,13 +4,12 @@ import type { ColumnsType } from 'antd/es/table';
import { useSearchParams } from 'react-router-dom';
import {
STORE_LOG_CATEGORY_OPTIONS,
STORE_LOG_CATEGORY_LABELS,
resolveStoreLogCategory,
type StoreLogCategory,
} from '../lib/store-log';
import { request } from '../lib/api';
import { AdminCellLine } from '../components/AdminCellLine';
import { fmtTime } from '../lib/constants';
import { clientAppLabel, eventNameLabel, formatRef, storeEventCategoryLabel } from '../lib/display-labels';
import { useAdminList } from '../lib/useAdminList';
import { useAdminListColumns } from '../lib/useAdminListColumns';
import { AdminListHeader } from '../components/AdminListHeader';
@@ -102,7 +101,7 @@ export default function StoreLogsPage() {
{
title: '分类', dataIndex: 'category', width: 100,
render: (v: StoreLogCategory | null, r) => (
<Tag>{STORE_LOG_CATEGORY_LABELS[v ?? ''] || resolveStoreLogCategory(r.eventName) || '其他'}</Tag>
<Tag>{storeEventCategoryLabel(r.eventName, v)}</Tag>
),
},
{
@@ -118,7 +117,7 @@ export default function StoreLogsPage() {
setDrawerOpen(true);
}}
>
{v}
{eventNameLabel(v)}
</AdminPrimaryLink>
),
},
@@ -128,7 +127,7 @@ export default function StoreLogsPage() {
},
{
title: '关联', width: 120,
render: (_, r) => (r.refType && r.refId ? `${r.refType}#${r.refId}` : '—'),
render: (_, r) => formatRef(r.refType, r.refId),
},
{
title: '摘要',
@@ -184,7 +183,7 @@ export default function StoreLogsPage() {
<Form.Item name="storeName" label="门店名称"><Input allowClear style={{ width: 140 }} /></Form.Item>
<Form.Item name="storeAccountId" label="账号ID"><Input allowClear style={{ width: 120 }} /></Form.Item>
<Form.Item name="phone" label="手机号"><Input allowClear style={{ width: 130 }} /></Form.Item>
<Form.Item name="eventName" label="事件"><Input allowClear style={{ width: 160 }} placeholder="store_login_success" /></Form.Item>
<Form.Item name="eventName" label="事件编码"><Input allowClear style={{ width: 160 }} placeholder="事件编码" /></Form.Item>
<Form.Item><Button type="primary" htmlType="submit"></Button></Form.Item>
<Form.Item><Button onClick={() => { form.resetFields(); setFilters({}); setCategory(''); setSearchParams({}); setPage(1); }}></Button></Form.Item>
</Form>
@@ -198,12 +197,12 @@ export default function StoreLogsPage() {
<Descriptions.Item label="账号">{String(detail.accountName || detail.storeAccountId || '系统/HQ')}</Descriptions.Item>
<Descriptions.Item label="手机">{String(detail.accountPhone || '—')}</Descriptions.Item>
<Descriptions.Item label="分类">
{(STORE_LOG_CATEGORY_LABELS as Record<string, string>)[String(detail.category ?? '')] || String(detail.category || '—')}
{storeEventCategoryLabel(String(detail.eventName ?? ''), detail.category ? String(detail.category) : null)}
</Descriptions.Item>
<Descriptions.Item label="事件">{String(detail.eventName)}</Descriptions.Item>
<Descriptions.Item label="事件">{eventNameLabel(String(detail.eventName ?? ''))}</Descriptions.Item>
<Descriptions.Item label="来源">{SOURCE_LABELS[String(detail.source) as Row['source']] || String(detail.source || '—')}</Descriptions.Item>
<Descriptions.Item label="客户端">{String(detail.clientApp || '—')}</Descriptions.Item>
<Descriptions.Item label="关联">{detail.refType ? `${String(detail.refType)}#${String(detail.refId)}` : '—'}</Descriptions.Item>
<Descriptions.Item label="端">{clientAppLabel(detail.clientApp ? String(detail.clientApp) : null)}</Descriptions.Item>
<Descriptions.Item label="关联">{formatRef(detail.refType ? String(detail.refType) : null, detail.refId ? String(detail.refId) : null)}</Descriptions.Item>
<Descriptions.Item label="时间">{fmtTime(String(detail.createdAt))}</Descriptions.Item>
<Descriptions.Item label="参数">
<pre style={{ margin: 0, whiteSpace: 'pre-wrap', wordBreak: 'break-all' }}>
+27 -18
View File
@@ -3,6 +3,13 @@ import { Button, Descriptions, Drawer, Form, Input, Select, Table, Tag, Typograp
import type { ColumnsType } from 'antd/es/table';
import { request } from '../lib/api';
import { fmtTime } from '../lib/constants';
import {
THIRD_PARTY_PROVIDER_LABELS,
THIRD_PARTY_SCENE_LABELS,
THIRD_PARTY_STATUS_LABELS,
formatRef,
zhLabel,
} from '../lib/display-labels';
import { useAdminList } from '../lib/useAdminList';
import { useAdminListColumns } from '../lib/useAdminListColumns';
import { AdminListHeader } from '../components/AdminListHeader';
@@ -26,14 +33,14 @@ type Row = {
};
const PROVIDER_OPTIONS = [
{ value: 'WECHAT_AUTH', label: 'WECHAT_AUTH' },
{ value: 'WECHAT_PAY', label: 'WECHAT_PAY' },
{ value: 'WECHAT_MAP', label: 'WECHAT_MAP' },
{ value: 'ALIYUN_OSS', label: 'ALIYUN_OSS' },
{ value: 'ALIYUN_SMS', label: 'ALIYUN_SMS' },
{ value: 'MOCK_SMS', label: 'MOCK_SMS' },
{ value: 'XFX', label: '小飞侠 (XFX)' },
{ value: 'LOGISTICS', label: 'LOGISTICS' },
{ value: 'WECHAT_AUTH', label: '微信授权' },
{ value: 'WECHAT_PAY', label: '微信支付' },
{ value: 'WECHAT_MAP', label: '微信/腾讯地图' },
{ value: 'ALIYUN_OSS', label: '阿里云 OSS' },
{ value: 'ALIYUN_SMS', label: '阿里云短信' },
{ value: 'MOCK_SMS', label: 'Mock 短信' },
{ value: 'XFX', label: '小飞侠' },
{ value: 'LOGISTICS', label: '物流快递' },
];
const STATUS_COLOR: Record<string, string> = {
@@ -75,18 +82,18 @@ export default function ThirdPartyLogsPage() {
const baseColumns: ColumnsType<Row> = [
{ title: '时间', dataIndex: 'createdAt', width: 160, render: fmtTime },
{ title: 'Provider', dataIndex: 'provider', width: 120 },
{ title: '场景', dataIndex: 'scene', width: 120 },
{ title: '渠道', dataIndex: 'provider', width: 120, render: (v: string) => zhLabel(THIRD_PARTY_PROVIDER_LABELS, v) },
{ title: '场景', dataIndex: 'scene', width: 140, render: (v: string) => zhLabel(THIRD_PARTY_SCENE_LABELS, v) },
{
title: '关联',
width: 120,
render: (_, r) => (r.refType && r.refId ? `${r.refType}#${r.refId}` : '—'),
render: (_, r) => formatRef(r.refType, r.refId),
},
{
title: '状态',
dataIndex: 'status',
width: 90,
render: (v: string) => <Tag color={STATUS_COLOR[v] ?? 'default'}>{v}</Tag>,
render: (v: string) => <Tag color={STATUS_COLOR[v] ?? 'default'}>{zhLabel(THIRD_PARTY_STATUS_LABELS, v)}</Tag>,
},
{
title: '错误信息',
@@ -147,11 +154,11 @@ export default function ThirdPartyLogsPage() {
setPage(1);
}}
>
<Form.Item name="provider" label="Provider">
<Form.Item name="provider" label="渠道">
<Select allowClear placeholder="全部" style={{ width: 160 }} options={PROVIDER_OPTIONS} />
</Form.Item>
<Form.Item name="scene" label="场景">
<Input allowClear placeholder="JSSDK_CONFIG / LOGIN" style={{ width: 160 }} />
<Input allowClear placeholder="场景编码" style={{ width: 160 }} />
</Form.Item>
<Form.Item name="refId" label="关联ID">
<Input allowClear style={{ width: 120 }} />
@@ -181,13 +188,15 @@ export default function ThirdPartyLogsPage() {
{detail && (
<Descriptions column={1} bordered size="small">
<Descriptions.Item label="ID">{String(detail.id)}</Descriptions.Item>
<Descriptions.Item label="Provider">{String(detail.provider)}</Descriptions.Item>
<Descriptions.Item label="场景">{String(detail.scene)}</Descriptions.Item>
<Descriptions.Item label="渠道">{zhLabel(THIRD_PARTY_PROVIDER_LABELS, String(detail.provider))}</Descriptions.Item>
<Descriptions.Item label="场景">{zhLabel(THIRD_PARTY_SCENE_LABELS, String(detail.scene))}</Descriptions.Item>
<Descriptions.Item label="状态">
<Tag color={STATUS_COLOR[String(detail.status)] ?? 'default'}>{String(detail.status)}</Tag>
<Tag color={STATUS_COLOR[String(detail.status)] ?? 'default'}>
{zhLabel(THIRD_PARTY_STATUS_LABELS, String(detail.status))}
</Tag>
</Descriptions.Item>
<Descriptions.Item label="关联">
{detail.refType ? `${String(detail.refType)}#${String(detail.refId)}` : '—'}
{formatRef(detail.refType ? String(detail.refType) : null, detail.refId ? String(detail.refId) : null)}
</Descriptions.Item>
<Descriptions.Item label="外部单号">{String(detail.externalNo ?? '—')}</Descriptions.Item>
<Descriptions.Item label="金额">{detail.amount != null ? String(detail.amount) : '—'}</Descriptions.Item>
+13 -11
View File
@@ -1,11 +1,12 @@
import { useEffect, useState } from 'react';
import { Button, Descriptions, Drawer, Form, Input, Segmented, Space, Table, Tag, Typography } from 'antd';
import type { ColumnsType } from 'antd/es/table';
import { USER_LOG_CATEGORY_OPTIONS, resolveUserLogCategory, USER_LOG_CATEGORY_LABELS, type UserLogCategory } from '../lib/user-log';
import { USER_LOG_CATEGORY_OPTIONS, type UserLogCategory } from '../lib/user-log';
import { useSearchParams } from 'react-router-dom';
import { request } from '../lib/api';
import { AdminCellLine } from '../components/AdminCellLine';
import { fmtTime } from '../lib/constants';
import { clientAppLabel, eventNameLabel, formatRef, userEventCategoryLabel } from '../lib/display-labels';
import { useAdminList } from '../lib/useAdminList';
import { useAdminListColumns } from '../lib/useAdminListColumns';
import { AdminListHeader } from '../components/AdminListHeader';
@@ -27,8 +28,6 @@ type Row = {
createdAt: string;
};
const CATEGORY_LABELS = USER_LOG_CATEGORY_LABELS;
function summarizeExtra(json: Record<string, unknown> | null) {
if (!json) return '—';
const text = JSON.stringify(json);
@@ -79,7 +78,7 @@ export default function UserLogsPage() {
{
title: '分类', dataIndex: 'category', width: 100,
render: (v: UserLogCategory | null, r) => (
<Tag>{CATEGORY_LABELS[v ?? ''] || resolveUserLogCategory(r.eventName) || '其他'}</Tag>
<Tag>{userEventCategoryLabel(r.eventName, v)}</Tag>
),
},
{
@@ -93,13 +92,16 @@ export default function UserLogsPage() {
setDrawerOpen(true);
}}
>
{v}
{eventNameLabel(v)}
</AdminPrimaryLink>
),
},
{
title: '端', dataIndex: 'clientApp', width: 110, render: (v) => clientAppLabel(v),
},
{
title: '关联', width: 120,
render: (_, r) => (r.refType && r.refId ? `${r.refType}#${r.refId}` : '—'),
render: (_, r) => formatRef(r.refType, r.refId),
},
{
title: '摘要',
@@ -148,7 +150,7 @@ export default function UserLogsPage() {
<Form.Item name="userId" label="用户ID"><Input allowClear style={{ width: 120 }} /></Form.Item>
<Form.Item name="phone" label="手机号"><Input allowClear style={{ width: 130 }} /></Form.Item>
<Form.Item name="userNo" label="用户编号"><Input allowClear style={{ width: 120 }} /></Form.Item>
<Form.Item name="eventName" label="事件"><Input allowClear style={{ width: 160 }} placeholder="pay_success" /></Form.Item>
<Form.Item name="eventName" label="事件编码"><Input allowClear style={{ width: 160 }} placeholder="事件编码" /></Form.Item>
<Form.Item><Button type="primary" htmlType="submit"></Button></Form.Item>
<Form.Item><Button onClick={() => { form.resetFields(); setFilters({}); setCategory(''); setSearchParams({}); setPage(1); }}></Button></Form.Item>
</Form>
@@ -160,10 +162,10 @@ export default function UserLogsPage() {
<Descriptions.Item label="ID">{String(detail.id)}</Descriptions.Item>
<Descriptions.Item label="用户">{String(detail.nickname || detail.userNo || detail.userId || '—')}</Descriptions.Item>
<Descriptions.Item label="手机">{String(detail.phone || '—')}</Descriptions.Item>
<Descriptions.Item label="分类">{CATEGORY_LABELS[String(detail.category ?? '')] || String(detail.category || '—')}</Descriptions.Item>
<Descriptions.Item label="事件">{String(detail.eventName)}</Descriptions.Item>
<Descriptions.Item label="客户端">{String(detail.clientApp || '—')}</Descriptions.Item>
<Descriptions.Item label="关联">{detail.refType ? `${String(detail.refType)}#${String(detail.refId)}` : '—'}</Descriptions.Item>
<Descriptions.Item label="分类">{userEventCategoryLabel(String(detail.eventName ?? ''), detail.category ? String(detail.category) : null)}</Descriptions.Item>
<Descriptions.Item label="事件">{eventNameLabel(String(detail.eventName ?? ''))}</Descriptions.Item>
<Descriptions.Item label="端">{clientAppLabel(detail.clientApp ? String(detail.clientApp) : null)}</Descriptions.Item>
<Descriptions.Item label="关联">{formatRef(detail.refType ? String(detail.refType) : null, detail.refId ? String(detail.refId) : null)}</Descriptions.Item>
<Descriptions.Item label="时间">{fmtTime(String(detail.createdAt))}</Descriptions.Item>
<Descriptions.Item label="参数">
<pre style={{ margin: 0, whiteSpace: 'pre-wrap', wordBreak: 'break-all' }}>
+5 -4
View File
@@ -17,12 +17,13 @@ import {
message,
} from 'antd';
import type { ColumnsType } from 'antd/es/table';
import { USER_SOURCE_TYPE_LABELS, resolveUserLogCategory, type UserSourceType } from '@dukang/shared-types';
import { USER_SOURCE_TYPE_LABELS, type UserSourceType } from '@dukang/shared-types';
import { request, type AdminUserRow, type HqProfile, type Paginated } from '../lib/api';
import { useAdminListColumns } from '../lib/useAdminListColumns';
import { AdminListHeader } from '../components/AdminListHeader';
import { AdminPrimaryLink } from '../components/AdminPrimaryLink';
import { ORDER_STATUS_LABELS, fmtTime, maskPhone } from '../lib/constants';
import { clientAppLabel, eventNameLabel, userEventCategoryLabel } from '../lib/display-labels';
type UserOrderRow = {
id: string;
@@ -640,10 +641,10 @@ export default function UsersPage() {
{
title: '分类',
width: 100,
render: (_, r) => resolveUserLogCategory(r.eventName) ?? '—',
render: (_, r) => userEventCategoryLabel(r.eventName),
},
{ title: '事件', dataIndex: 'eventName' },
{ title: '端', dataIndex: 'clientApp', width: 90 },
{ title: '事件', dataIndex: 'eventName', render: (v: string) => eventNameLabel(v) },
{ title: '端', dataIndex: 'clientApp', width: 110, render: (v: string | null) => clientAppLabel(v) },
]}
/>
@@ -4,6 +4,7 @@ import type { ColumnsType } from 'antd/es/table';
import { useAdminList } from '../lib/useAdminList';
import { request } from '../lib/api';
import { fmtTime } from '../lib/constants';
import { WECOM_BOT_ACTION_LABELS, zhLabel } from '../lib/display-labels';
import type { WecomBotLogDto } from '@dukang/shared-types';
import { useAdminListColumns } from '../lib/useAdminListColumns';
import { AdminListHeader } from '../components/AdminListHeader';
@@ -39,8 +40,8 @@ export default function WecomBotLogsPage() {
),
},
{ title: '企微用户', dataIndex: 'wecomUserId', width: 120 },
{ title: '动作', dataIndex: 'action', width: 160 },
{ title: '权限', dataIndex: 'permission', width: 140, render: (v) => v || '—' },
{ title: '动作', dataIndex: 'action', width: 180, render: (v: string) => zhLabel(WECOM_BOT_ACTION_LABELS, v) },
{ title: '权限', dataIndex: 'permission', width: 160, render: (v) => zhLabel(WECOM_BOT_ACTION_LABELS, v) },
{
title: '结果',
dataIndex: 'success',
@@ -81,13 +82,13 @@ export default function WecomBotLogsPage() {
}}
>
<Form.Item name="botId" label="机器人 ID">
<Input allowClear placeholder="wecom_bot.id" style={{ width: 140 }} />
<Input allowClear placeholder="机器人 ID" style={{ width: 140 }} />
</Form.Item>
<Form.Item name="wecomUserId" label="企微 userid">
<Input allowClear placeholder="userid" style={{ width: 140 }} />
<Form.Item name="wecomUserId" label="企微用户">
<Input allowClear placeholder="企微用户 ID" style={{ width: 140 }} />
</Form.Item>
<Form.Item name="action" label="动作">
<Input allowClear placeholder="order.read" style={{ width: 140 }} />
<Input allowClear placeholder="动作编码" style={{ width: 140 }} />
</Form.Item>
<Form.Item name="success" label="结果">
<Select
@@ -130,8 +131,8 @@ export default function WecomBotLogsPage() {
<Descriptions.Item label="时间">{fmtTime(detail.createdAt)}</Descriptions.Item>
<Descriptions.Item label="机器人">{detail.botName || detail.botKey || '—'}</Descriptions.Item>
<Descriptions.Item label="企微用户">{detail.wecomUserId}</Descriptions.Item>
<Descriptions.Item label="动作">{detail.action}</Descriptions.Item>
<Descriptions.Item label="权限">{detail.permission || '—'}</Descriptions.Item>
<Descriptions.Item label="动作">{zhLabel(WECOM_BOT_ACTION_LABELS, detail.action)}</Descriptions.Item>
<Descriptions.Item label="权限">{zhLabel(WECOM_BOT_ACTION_LABELS, detail.permission)}</Descriptions.Item>
<Descriptions.Item label="输入摘要">{detail.inputSummary || '—'}</Descriptions.Item>
<Descriptions.Item label="结果">{detail.success ? '成功' : '失败'}</Descriptions.Item>
<Descriptions.Item label="错误">{detail.errorMessage || '—'}</Descriptions.Item>