feat(admin): v3.5.12 大屏BGM、中文展示、分类删除修复与侧栏顺序
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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: '时间',
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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' }}>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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' }}>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user