feat(admin): v3.5.12 大屏BGM、中文展示、分类删除修复与侧栏顺序
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -308,6 +308,40 @@ body:has(.big-screen-page),
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.big-screen-bgm {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-right: 8px;
|
||||
padding: 6px 14px 6px 12px;
|
||||
border: 1px solid rgba(105, 192, 255, 0.45);
|
||||
border-radius: 6px;
|
||||
background: rgba(8, 28, 56, 0.7);
|
||||
color: rgba(230, 244, 255, 0.92);
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 1px;
|
||||
cursor: pointer;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.big-screen-bgm:hover {
|
||||
border-color: rgba(105, 192, 255, 0.85);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.big-screen-bgm.is-on {
|
||||
border-color: rgba(64, 169, 255, 0.85);
|
||||
box-shadow: 0 0 12px rgba(64, 169, 255, 0.28);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.big-screen-bgm-icon {
|
||||
display: inline-flex;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.big-screen-live-label {
|
||||
font-size: 18px;
|
||||
color: rgba(230, 244, 255, 0.88);
|
||||
|
||||
Binary file not shown.
@@ -41,7 +41,6 @@ type MenuItem = NonNullable<MenuProps['items']>[number];
|
||||
const MENU_ITEMS: MenuProps['items'] = [
|
||||
{ key: '/', icon: <DashboardOutlined />, label: '概览' },
|
||||
{ key: '/users', icon: <UserOutlined />, label: '用户' },
|
||||
{ key: '/wechat-bindings', icon: <UserOutlined />, label: '微信绑定' },
|
||||
{
|
||||
key: 'products-group',
|
||||
icon: <ShoppingOutlined />,
|
||||
@@ -52,18 +51,17 @@ const MENU_ITEMS: MenuProps['items'] = [
|
||||
],
|
||||
},
|
||||
{ key: '/orders', icon: <ShoppingOutlined />, label: '订单' },
|
||||
{ key: '/promo-codes', icon: <GiftOutlined />, label: '推广码' },
|
||||
{
|
||||
key: 'wecom-group',
|
||||
key: 'partners-group',
|
||||
icon: <TeamOutlined />,
|
||||
label: '企微机器人',
|
||||
label: '开城',
|
||||
children: [
|
||||
{ key: '/wecom/bots', label: '智能机器人' },
|
||||
{ key: '/wecom/pushes', label: '消息推送' },
|
||||
{ key: '/cities', label: '城市' },
|
||||
{ key: '/city-partners', label: '城市合伙人' },
|
||||
{ key: '/city-warehouses', label: '仓库' },
|
||||
{ key: '/fulfillment-providers', label: '仓配管理' },
|
||||
],
|
||||
},
|
||||
{ key: '/llm-configs', icon: <RobotOutlined />, label: '语言模型' },
|
||||
{ key: '/knowledge-bases', icon: <FileTextOutlined />, label: '知识库' },
|
||||
{
|
||||
key: 'stores-group',
|
||||
icon: <ShopOutlined />,
|
||||
@@ -77,17 +75,6 @@ const MENU_ITEMS: MenuProps['items'] = [
|
||||
{ key: '/store-media', label: '门店资源' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'partners-group',
|
||||
icon: <TeamOutlined />,
|
||||
label: '开城',
|
||||
children: [
|
||||
{ key: '/cities', label: '城市' },
|
||||
{ key: '/city-partners', label: '城市合伙人' },
|
||||
{ key: '/city-warehouses', label: '仓库' },
|
||||
{ key: '/fulfillment-providers', label: '仓配管理' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'finance-group',
|
||||
icon: <AccountBookOutlined />,
|
||||
@@ -129,6 +116,20 @@ const MENU_ITEMS: MenuProps['items'] = [
|
||||
{ key: '/tickets/support', label: '技术支持' },
|
||||
],
|
||||
},
|
||||
{ key: '/invoices', icon: <FileTextOutlined />, label: '发票' },
|
||||
{ key: '/promo-codes', icon: <GiftOutlined />, label: '推广码' },
|
||||
{ key: '/wechat-bindings', icon: <UserOutlined />, label: '微信绑定' },
|
||||
{
|
||||
key: 'wecom-group',
|
||||
icon: <TeamOutlined />,
|
||||
label: '企微机器人',
|
||||
children: [
|
||||
{ key: '/wecom/bots', label: '智能机器人' },
|
||||
{ key: '/wecom/pushes', label: '消息推送' },
|
||||
],
|
||||
},
|
||||
{ key: '/llm-configs', icon: <RobotOutlined />, label: '语言模型' },
|
||||
{ key: '/knowledge-bases', icon: <FileTextOutlined />, label: '知识库' },
|
||||
{
|
||||
key: 'dev-plan-group',
|
||||
icon: <ProjectOutlined />,
|
||||
@@ -139,7 +140,6 @@ const MENU_ITEMS: MenuProps['items'] = [
|
||||
{ key: '/dev-plan/settings', label: '开发设置' },
|
||||
],
|
||||
},
|
||||
{ key: '/invoices', icon: <FileTextOutlined />, label: '发票管理' },
|
||||
{ key: '/resources', icon: <CloudUploadOutlined />, label: 'OSS 资源库' },
|
||||
{
|
||||
key: 'logs-group',
|
||||
@@ -156,9 +156,9 @@ const MENU_ITEMS: MenuProps['items'] = [
|
||||
],
|
||||
},
|
||||
{ key: '/hq-permissions', icon: <LockOutlined />, label: '权限分配' },
|
||||
{ key: '/hq-accounts', icon: <SafetyOutlined />, label: 'HQ账户' },
|
||||
{ key: '/test-whitelist', icon: <SafetyOutlined />, label: '白名单管理' },
|
||||
{ key: '/system-settings', icon: <SettingOutlined />, label: '系统设置' },
|
||||
{ key: '/hq-accounts', icon: <SafetyOutlined />, label: 'HQ账户' },
|
||||
];
|
||||
|
||||
function menuAllowed(key: string, permissionKeys: string[]): boolean {
|
||||
|
||||
@@ -31,9 +31,18 @@ export const ORDER_STATUS_OPERATOR_LABELS: Record<string, string> = {
|
||||
USER_ON_SITE: '用户·现场取货',
|
||||
USER_WECHAT_CONFIRM: '用户·微信确认收货',
|
||||
WECHAT_TRADE_MANAGE: '微信结算同步',
|
||||
WECHAT_PAY: '微信支付',
|
||||
MOCK: 'Mock',
|
||||
MOCK_PAY: 'Mock 支付',
|
||||
SYSTEM: '系统',
|
||||
SYSTEM_AUTO_EXPIRE: '系统·超时取消',
|
||||
PARTNER_PROXY: '合伙人代下单',
|
||||
HQ_PROXY: 'HQ 代操作',
|
||||
HQ_LOGISTICS: 'HQ 物流',
|
||||
WAREHOUSE_MANUAL: '仓库手工',
|
||||
HQ: '总部',
|
||||
STORE: '门店',
|
||||
PARTNER: '合伙人',
|
||||
};
|
||||
|
||||
/** 订单状态 Tag 颜色(Ant Design preset) */
|
||||
|
||||
@@ -0,0 +1,358 @@
|
||||
import {
|
||||
LEDGER_TYPE_LABELS,
|
||||
ORDER_STATUS_LABELS,
|
||||
ORDER_STATUS_OPERATOR_LABELS,
|
||||
} from './constants';
|
||||
import { HQ_OPERATION_ACTION_LABELS } from './hq-log';
|
||||
import { USER_LOG_CATEGORY_LABELS } from './user-log';
|
||||
import {
|
||||
PARTNER_LOG_CATEGORY_LABELS,
|
||||
HQ_ADMIN_ROLES,
|
||||
resolvePartnerLogCategory,
|
||||
resolveStoreLogCategory,
|
||||
resolveUserLogCategory,
|
||||
STORE_LOG_CATEGORY_LABELS,
|
||||
WECOM_BOT_PERMISSION_LABELS,
|
||||
} from '@dukang/shared-types';
|
||||
|
||||
export function zhLabel(dict: Record<string, string>, value?: string | null, empty = '—'): string {
|
||||
if (value == null || value === '') return empty;
|
||||
return dict[value] ?? value;
|
||||
}
|
||||
|
||||
export const CLIENT_APP_LABELS: Record<string, string> = {
|
||||
USER_MINI: 'C 端小程序',
|
||||
USER_H5: 'C 端 H5',
|
||||
PARTNER_MINI: '合伙人小程序',
|
||||
PARTNER_H5: '合伙人 H5',
|
||||
HQ_MINI: 'HQ 小程序',
|
||||
HQ_WEB: 'HQ 后台',
|
||||
SHOP_H5: '门店 H5',
|
||||
};
|
||||
|
||||
export const REF_TYPE_LABELS: Record<string, string> = {
|
||||
ORDER: '订单',
|
||||
USER: '用户',
|
||||
STORE: '门店',
|
||||
PARTNER: '合伙人',
|
||||
CITY: '城市',
|
||||
PRODUCT: '商品',
|
||||
COUPON: '权益券',
|
||||
TICKET: '工单',
|
||||
SUPPORT_TICKET: '技术支持工单',
|
||||
INVOICE: '发票',
|
||||
PROMO: '推广码',
|
||||
PROMO_CODE: '推广码',
|
||||
HQ: '总部',
|
||||
HQ_ACCOUNT: 'HQ 账号',
|
||||
STORE_ACCOUNT: '门店账号',
|
||||
PARTNER_ACCOUNT: '合伙人账号',
|
||||
PAYOUT: '打款',
|
||||
WITHDRAWAL: '提现',
|
||||
BILL: '账单',
|
||||
DELIVERY: '配送单',
|
||||
REDEEM: '核销',
|
||||
WAREHOUSE: '仓库',
|
||||
PACKAGE: '套餐',
|
||||
};
|
||||
|
||||
export const DOMAIN_EVENT_TYPE_LABELS: Record<string, string> = {
|
||||
ORDER_STATUS: '订单状态',
|
||||
BENEFIT_LEDGER: '权益流水',
|
||||
STORE_AUDIT: '门店审核',
|
||||
TICKET_COLLAB: '工单协作',
|
||||
PROMO_TOUCH: '推广触达',
|
||||
HQ_OPERATION: 'HQ 操作',
|
||||
};
|
||||
|
||||
export const PARAM_DESC_LABELS: Record<string, string> = {
|
||||
from_status: '原状态',
|
||||
to_status: '目标状态',
|
||||
operator: '操作人',
|
||||
ledger_type: '流水类型',
|
||||
coupon_id: '权益券 ID',
|
||||
action: '行为',
|
||||
target_type: '对象类型',
|
||||
target_id: '对象 ID',
|
||||
};
|
||||
|
||||
export const USER_LOG_EVENT_LABELS: Record<string, string> = {
|
||||
login_success: '登录成功',
|
||||
sms_login: '短信登录',
|
||||
sms_send: '发送验证码',
|
||||
sms_verify_fail: '验证码校验失败',
|
||||
session_start: '会话开始',
|
||||
home_view: '浏览首页',
|
||||
product_list_view: '浏览商品列表',
|
||||
product_click: '点击商品',
|
||||
product_detail_view: '浏览商品详情',
|
||||
order_confirm_view: '浏览确认订单',
|
||||
order_submit: '提交订单',
|
||||
order_list_view: '浏览订单列表',
|
||||
order_detail_view: '浏览订单详情',
|
||||
order_cancel: '取消订单',
|
||||
pay_page_view: '进入支付页',
|
||||
pay_success: '支付成功',
|
||||
pay_fail: '支付失败',
|
||||
wechat_login: '微信登录',
|
||||
wechat_phone: '微信授权手机号',
|
||||
wechat_phone_login: '微信手机号登录',
|
||||
wechat_location: '微信定位',
|
||||
wechat_album: '微信选图',
|
||||
wechat_bind: '绑定微信',
|
||||
store_list_view: '浏览门店列表',
|
||||
store_detail_view: '浏览门店详情',
|
||||
benefit_page_view: '浏览权益页',
|
||||
benefit_detail_view: '浏览权益详情',
|
||||
benefit_redeem_start: '开始核销',
|
||||
benefit_redeem_success: '核销成功',
|
||||
redeem_code_view: '查看核销码',
|
||||
redeem_success_view: '核销成功页',
|
||||
profile_update: '修改资料',
|
||||
bind_phone: '绑定手机号',
|
||||
address_list_view: '浏览地址列表',
|
||||
address_edit: '编辑地址',
|
||||
promo_touch: '推广触达',
|
||||
promo_share: '分享推广',
|
||||
cs_contact: '联系客服',
|
||||
after_sale_list_view: '浏览售后列表',
|
||||
after_sale_apply: '申请售后',
|
||||
client_error: '前端异常',
|
||||
};
|
||||
|
||||
export const STORE_LOG_EVENT_LABELS: Record<string, string> = {
|
||||
store_sms_send: '发送验证码',
|
||||
store_sms_login: '短信登录',
|
||||
store_sms_verify_fail: '验证码校验失败',
|
||||
store_login_success: '登录成功',
|
||||
store_select: '选择门店',
|
||||
store_wechat_login: '微信登录',
|
||||
store_wechat_bind: '绑定微信',
|
||||
store_home_view: '浏览首页',
|
||||
store_redeem_entry_view: '进入核销页',
|
||||
store_redeem_confirm_view: '核销确认页',
|
||||
store_phone_redeem_view: '手机号核销页',
|
||||
store_records_view: '浏览核销记录',
|
||||
store_withdraw_view: '浏览提现页',
|
||||
store_packages_view: '浏览套餐页',
|
||||
store_mine_view: '浏览我的',
|
||||
store_staff_view: '浏览子账号',
|
||||
store_redeem_preview: '核销预览',
|
||||
store_redeem_confirm: '确认核销',
|
||||
store_redeem_confirm_fail: '核销失败',
|
||||
store_redeem_weaknet_threshold: '弱网阈值',
|
||||
store_redeem_pending_submit: '提交挂起核销',
|
||||
store_redeem_pending_complete: '完成挂起核销',
|
||||
store_redeem_pending_reject: '驳回挂起核销',
|
||||
store_redeem_phone_lookup_sms: '手机号核销发码',
|
||||
store_redeem_phone_balance: '查询用户权益余额',
|
||||
store_redeem_phone_prepare: '手机号核销准备',
|
||||
store_redeem_scan_start: '扫码核销开始',
|
||||
store_payout_created: '生成打款单',
|
||||
store_payout_paid: '打款完成',
|
||||
store_withdraw_applied: '申请提现',
|
||||
store_withdraw_paid: '提现已打款',
|
||||
store_status_change: '变更营业状态',
|
||||
store_staff_create: '新增子账号',
|
||||
store_staff_update: '编辑子账号',
|
||||
store_staff_delete: '删除子账号',
|
||||
store_staff_permission_update: '更新子账号权限',
|
||||
store_package_apply: '申请套餐',
|
||||
store_package_audit_view: '查看套餐审核',
|
||||
};
|
||||
|
||||
export const PARTNER_LOG_EVENT_LABELS: Record<string, string> = {
|
||||
partner_sms_send: '发送验证码',
|
||||
partner_sms_login: '短信登录',
|
||||
partner_sms_verify_fail: '验证码校验失败',
|
||||
partner_login_success: '登录成功',
|
||||
partner_wechat_login: '微信登录',
|
||||
partner_wechat_bind: '绑定微信',
|
||||
partner_home_view: '浏览首页',
|
||||
partner_store_list_view: '浏览门店列表',
|
||||
partner_store_detail_view: '浏览门店详情',
|
||||
partner_store_create_view: '进入录店页',
|
||||
partner_order_list_view: '浏览订单列表',
|
||||
partner_order_detail_view: '浏览订单详情',
|
||||
partner_proxy_order_view: '进入代下单页',
|
||||
partner_bills_view: '浏览账单列表',
|
||||
partner_bill_detail_view: '浏览账单详情',
|
||||
partner_staff_list_view: '浏览子账号',
|
||||
partner_leaderboard_view: '浏览排行榜',
|
||||
partner_weekly_report_view: '浏览周报',
|
||||
partner_staff_create: '新增子账号',
|
||||
partner_staff_sms_send: '子账号发验证码',
|
||||
partner_staff_sms_verify_fail: '子账号验证码失败',
|
||||
partner_staff_update: '编辑子账号',
|
||||
partner_staff_delete: '删除子账号',
|
||||
partner_staff_permission_update: '更新子账号权限',
|
||||
partner_store_create: '录入门店',
|
||||
partner_store_status_change: '变更门店状态',
|
||||
partner_store_audit_approved: '门店审核通过',
|
||||
partner_store_audit_rejected: '门店审核驳回',
|
||||
partner_order_ship: '订单发货',
|
||||
partner_delivery_advance: '推进配送',
|
||||
partner_proxy_order_create: '代下单',
|
||||
partner_bill_view: '查看账单',
|
||||
partner_bill_confirm: '确认账单',
|
||||
partner_warehouse_view: '浏览仓库',
|
||||
partner_warehouse_update: '更新仓库',
|
||||
};
|
||||
|
||||
export const EVENT_NAME_LABELS: Record<string, string> = {
|
||||
...USER_LOG_EVENT_LABELS,
|
||||
...STORE_LOG_EVENT_LABELS,
|
||||
...PARTNER_LOG_EVENT_LABELS,
|
||||
};
|
||||
|
||||
export const THIRD_PARTY_PROVIDER_LABELS: Record<string, string> = {
|
||||
WECHAT_AUTH: '微信授权',
|
||||
WECHAT_PAY: '微信支付',
|
||||
WECHAT_REFUND: '微信退款',
|
||||
WECHAT_MAP: '微信/腾讯地图',
|
||||
ALIYUN_OSS: '阿里云 OSS',
|
||||
ALIYUN_SMS: '阿里云短信',
|
||||
MOCK_SMS: 'Mock 短信',
|
||||
SMS: '短信',
|
||||
XFX: '小飞侠',
|
||||
LOGISTICS: '物流快递',
|
||||
MANUAL: '手动',
|
||||
};
|
||||
|
||||
export const THIRD_PARTY_STATUS_LABELS: Record<string, string> = {
|
||||
SUCCESS: '成功',
|
||||
FAILED: '失败',
|
||||
PENDING: '处理中',
|
||||
};
|
||||
|
||||
export const THIRD_PARTY_SCENE_LABELS: Record<string, string> = {
|
||||
LOGIN: '登录',
|
||||
USERINFO: '用户信息',
|
||||
JSSDK_CONFIG: 'JSSDK 配置',
|
||||
BIND_PHONE: '绑定手机号',
|
||||
GET_LOCATION: '获取定位',
|
||||
CHOOSE_IMAGE: '选择图片',
|
||||
ORDER_PAY: '订单支付',
|
||||
ORDER_REFUND: '订单退款',
|
||||
UPLOAD_SHIPPING: '上传发货信息',
|
||||
TRADE_MANAGE_PUSH: '交易结算推送',
|
||||
ORDER_DISPATCH: '配送下单',
|
||||
ORDER_DISPATCH_HOLD: '配送暂不下发',
|
||||
TRACK_CALLBACK: '物流回调',
|
||||
UPLOAD_TOKEN: '上传凭证',
|
||||
UPLOAD_PUT_OBJECT: '上传文件',
|
||||
GEOCODE: '地理编码',
|
||||
REVERSE_GEOCODE: '逆地理编码',
|
||||
};
|
||||
|
||||
export const WECOM_BOT_ACTION_LABELS: Record<string, string> = {
|
||||
...WECOM_BOT_PERMISSION_LABELS,
|
||||
'user.read_sms.start': '查询用户·发送验证码',
|
||||
'user.read_sms.verify': '查询用户·校验验证码',
|
||||
};
|
||||
|
||||
export const GENERIC_STATUS_LABELS: Record<string, string> = {
|
||||
...ORDER_STATUS_LABELS,
|
||||
...LEDGER_TYPE_LABELS,
|
||||
...THIRD_PARTY_STATUS_LABELS,
|
||||
PENDING: '待处理',
|
||||
APPROVED: '已通过',
|
||||
REJECTED: '已驳回',
|
||||
ACTIVE: '正常',
|
||||
DISABLED: '停用',
|
||||
SUCCESS: '成功',
|
||||
FAILED: '失败',
|
||||
OPEN: '营业中',
|
||||
PAUSED: '已暂停',
|
||||
CLOSED: '已关闭',
|
||||
};
|
||||
|
||||
export const HQ_ROLE_LABELS: Record<string, string> = Object.fromEntries(
|
||||
HQ_ADMIN_ROLES.map((r) => [r.value, r.label]),
|
||||
);
|
||||
|
||||
const CODE_ZH: Record<string, string> = {
|
||||
...CLIENT_APP_LABELS,
|
||||
...REF_TYPE_LABELS,
|
||||
...DOMAIN_EVENT_TYPE_LABELS,
|
||||
...EVENT_NAME_LABELS,
|
||||
...ORDER_STATUS_LABELS,
|
||||
...ORDER_STATUS_OPERATOR_LABELS,
|
||||
...LEDGER_TYPE_LABELS,
|
||||
...HQ_OPERATION_ACTION_LABELS,
|
||||
...HQ_ROLE_LABELS,
|
||||
...THIRD_PARTY_PROVIDER_LABELS,
|
||||
...THIRD_PARTY_STATUS_LABELS,
|
||||
...THIRD_PARTY_SCENE_LABELS,
|
||||
...WECOM_BOT_ACTION_LABELS,
|
||||
...GENERIC_STATUS_LABELS,
|
||||
...PARAM_DESC_LABELS,
|
||||
};
|
||||
|
||||
export function zhCode(value?: string | null, empty = '—'): string {
|
||||
if (value == null || value === '') return empty;
|
||||
return CODE_ZH[value] ?? value;
|
||||
}
|
||||
|
||||
export function eventNameLabel(value?: string | null): string {
|
||||
return zhLabel(EVENT_NAME_LABELS, value);
|
||||
}
|
||||
|
||||
export function clientAppLabel(value?: string | null): string {
|
||||
return zhLabel(CLIENT_APP_LABELS, value);
|
||||
}
|
||||
|
||||
export function formatRef(refType?: string | null, refId?: string | null): string {
|
||||
if (!refType && !refId) return '—';
|
||||
const type = zhLabel(REF_TYPE_LABELS, refType, refType || '');
|
||||
return refId ? `${type} #${refId}` : type;
|
||||
}
|
||||
|
||||
export function userEventCategoryLabel(eventName: string, category?: string | null): string {
|
||||
const key = category || resolveUserLogCategory(eventName);
|
||||
if (key && USER_LOG_CATEGORY_LABELS[key]) return USER_LOG_CATEGORY_LABELS[key];
|
||||
return '其他';
|
||||
}
|
||||
|
||||
export function storeEventCategoryLabel(eventName: string, category?: string | null): string {
|
||||
const key = category || resolveStoreLogCategory(eventName);
|
||||
const labels = STORE_LOG_CATEGORY_LABELS as Record<string, string>;
|
||||
if (key && labels[key]) return labels[key];
|
||||
return '其他';
|
||||
}
|
||||
|
||||
export function partnerEventCategoryLabel(eventName: string, category?: string | null): string {
|
||||
const key = category || resolvePartnerLogCategory(eventName);
|
||||
const labels = PARTNER_LOG_CATEGORY_LABELS as Record<string, string>;
|
||||
if (key && labels[key]) return labels[key];
|
||||
return '其他';
|
||||
}
|
||||
|
||||
export function domainParamFieldLabel(desc?: string | null, fallbackIndex?: 1 | 2 | 3): string {
|
||||
if (desc && PARAM_DESC_LABELS[desc]) return PARAM_DESC_LABELS[desc];
|
||||
if (fallbackIndex) return `参数${fallbackIndex}`;
|
||||
return '参数';
|
||||
}
|
||||
|
||||
export function zhRemark(value?: string | null): string {
|
||||
if (!value) return '—';
|
||||
return value
|
||||
.replace(/partnerAccountId=/g, '合伙人账号=')
|
||||
.replace(/hqAccountId=/g, 'HQ账号=')
|
||||
.replace(/deliveryMode=/g, '履约方式=')
|
||||
.replace(/\bmode=/g, '履约方式=')
|
||||
.replace(/customer=/g, '客户=')
|
||||
.replace(/\bON_SITE_PICKUP\b/g, '现场提货')
|
||||
.replace(/\bCROSS_CITY\b/g, '跨城')
|
||||
.replace(/\bLOCAL\b/g, '同城');
|
||||
}
|
||||
|
||||
export function domainParamValue(desc?: string | null, value?: string | null): string {
|
||||
if (value == null || value === '') return '—';
|
||||
if (desc === 'from_status' || desc === 'to_status') return zhLabel(ORDER_STATUS_LABELS, value);
|
||||
if (desc === 'operator') return zhLabel(ORDER_STATUS_OPERATOR_LABELS, value);
|
||||
if (desc === 'ledger_type') return zhLabel(LEDGER_TYPE_LABELS, value);
|
||||
if (desc === 'action') return zhLabel(HQ_OPERATION_ACTION_LABELS, value);
|
||||
if (desc === 'target_type') return zhLabel(REF_TYPE_LABELS, value);
|
||||
return zhCode(value);
|
||||
}
|
||||
@@ -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: '摘要',
|
||||
@@ -189,10 +188,10 @@ export default function PartnerLogsPage() {
|
||||
<Input placeholder="合伙人公司" allowClear style={{ width: 140 }} />
|
||||
</Form.Item>
|
||||
<Form.Item name="partnerId" label="合伙人 ID">
|
||||
<Input placeholder="partnerId" allowClear style={{ width: 120 }} />
|
||||
<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 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('已处理');
|
||||
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>
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
| 3.5.9 | 08-25 | 门店累计核销好客权益;HQ 表格去省略号;序号列 + 列设置 + 拖表头改列宽;用户备注 / 手机号不脱敏 | [`v3.5.9`](./杜康好客-v3.5.9-开发文档.md) |
|
||||
| 3.5.10 | 08-25 | C 端门店详情去掉分享按钮;同城送提示改承运商 HTML;小程序客服接通企微微信客服 | [`v3.5.10`](./杜康好客-v3.5.10-开发文档.md) |
|
||||
| 3.5.11 | 08-26 | 城市三种履约起购;企微通知补提交人/规格物流/核销用户;结算任务失败通知 + 提现通过/四账单 | [`v3.5.11`](./杜康好客-v3.5.11-开发文档.md) |
|
||||
| 3.5.12 | 08-26 | 发布会订单大屏循环 BGM;HQ 日志/订单状态流转/用户行为时间线展示中文;修复删除门店分类被默认树回种;HQ 侧栏顺序(业务前 11 项,系统设置置底) | [`v3.5.12`](./杜康好客-v3.5.12-开发文档.md) |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -66,9 +66,11 @@
|
||||
| 3.5.9 | [`门店累计核销 + HQ 去截断 + 列设置`](./杜康好客-v3.5.9-开发文档.md) | 🔶 开发完成 |
|
||||
| 3.5.10 | [`门店详情去分享按钮 + 同城提示改承运商 HTML + 小程序客服接企微`](./杜康好客-v3.5.10-开发文档.md) | 🔶 开发完成 |
|
||||
| 3.5.11 | [`城市履约起购 + 企微通知字段与结算通知`](./杜康好客-v3.5.11-开发文档.md) | 🔶 开发完成 |
|
||||
| 3.5.12 | [`订单大屏 BGM + HQ 中文展示 + 修复删除门店分类 + HQ 侧栏顺序`](./杜康好客-v3.5.12-开发文档.md) | 🔶 开发完成 |
|
||||
|
||||
| 日期 | 说明 |
|
||||
|------|------|
|
||||
| 2026-08-26 | v3.5.12:订单大屏循环 BGM;HQ 日志/订单状态流转/用户行为时间线英文码改中文;修复删除门店分类后被 `ensureDefaults` 回种;HQ 侧栏按业务前 11 项重排、系统设置置底 |
|
||||
| 2026-08-26 | v3.5.11:城市三种履约起购;企微补提交人/订单规格物流收货/核销用户;`alert.settlement` 改名「结算任务失败通知」归入系统监控;补提现通过与四账单 |
|
||||
| 2026-08-25 | v3.5.10:门店详情去掉分享按钮;同城送提示改承运商 HTML(`GET /catalog/local-deliveries`);小程序客服接通企微(需填 CorpID) |
|
||||
| 2026-08-25 | v3.5.9:门店列表累计核销好客权益;HQ 表格去省略号;主列表序号 + 列设置/列宽存 HQ 账号;用户列表备注(不改昵称)、手机号不脱敏;订单列表状态可多选 |
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
# 杜康好客 · v3.5.12 开发文档
|
||||
|
||||
> **2026-08-26** · admin-web / API
|
||||
> **主题**:发布会订单大屏循环 BGM;HQ 日志 / 订单状态流转 / 用户行为时间线展示中文;修复删除门店分类被默认树回种;HQ 侧栏菜单顺序
|
||||
|
||||
---
|
||||
|
||||
## 1. 版本目标
|
||||
|
||||
1. 订单大屏内置杜康 BGM,循环播放。
|
||||
2. 右上角提供「播放 BGM / BGM 播放中」开关(浏览器禁止自动出声,需用户点击)。
|
||||
3. 新成交撒花音效响起时,BGM 自动压低,特效结束后恢复。
|
||||
4. HQ 日志、订单详情「状态流转」、用户详情「行为时间线」中的英文码全部改为中文展示。
|
||||
5. 修复 HQ 删除门店分类:接口成功后列表不再自动补种默认树,分类真正消失(有门店占用则停用并明确提示)。
|
||||
6. HQ 左侧导航按业务优先重排:前 11 项固定,系统设置置底。
|
||||
|
||||
**不做**:C 端 / 门店 / 合伙人音频;OSS 签名直链(会过期);成交音效替换;改库内存储码(筛选仍可用事件编码);改权限 key。
|
||||
|
||||
---
|
||||
|
||||
## 2. 规则
|
||||
|
||||
### 2.1 大屏 BGM
|
||||
|
||||
- 音频文件随 `admin-web` 打包:`apps/admin-web/src/assets/big-screen/dukang-bgm.mp3`(约 2MB)。
|
||||
- BGM 与成交音效分两个 `Audio` 实例,互不打断。
|
||||
- 离开大屏页立即暂停 BGM。
|
||||
|
||||
### 2.2 英文码展示中文
|
||||
|
||||
只改 HQ 展示,不改 API 字段值。对照表在 `apps/admin-web/src/lib/display-labels.ts`。
|
||||
|
||||
| 页面 | 原英文 | 展示 |
|
||||
|------|--------|------|
|
||||
| 日志(用户/门店/合伙人/HQ/第三方/领域事件/企微机器人) | `pay_success`、`USER_MINI`、`ORDER`、`WECHAT_PAY`、`SUCCESS`、`order.read` 等 | 支付成功、C 端小程序、订单、微信支付、成功、查询订单 |
|
||||
| 订单详情 · 状态流转 | `WECHAT_PAY`、`PARTNER_PROXY`、`SYSTEM_AUTO_EXPIRE`;备注里的 `deliveryMode=LOCAL` | 微信支付、合伙人代下单、系统·超时取消;履约方式=同城 |
|
||||
| 用户详情 · 行为时间线 | 分类 `pay`、事件 `pay_success`、端 `USER_H5` | 支付、支付成功、C 端 H5 |
|
||||
|
||||
未知码仍原文回退,避免丢信息。列表筛选项「事件编码 / 对象编码」仍按存储码查询。
|
||||
|
||||
### 2.3 删除门店分类
|
||||
|
||||
根因:`GET /admin/store-categories`(以及 C 端/合伙人分类列表)默认 `ensureDefaults()`。删除无门店占用的默认分类(如「火锅」)后刷新列表会按编码把行再 insert 回来,前端却提示「已处理」。
|
||||
|
||||
规则:
|
||||
|
||||
- 列表接口 **不再自动补种**;缺省分类只通过 seed 或 HQ「同步默认分类」补齐。
|
||||
- 无门店占用:硬删除,提示「已删除」。
|
||||
- 有门店占用:改为停用,提示「该分类已有门店占用,已改为停用」。
|
||||
- 仍有下级:拒绝删除(「请先删除或停用下级分类」)。
|
||||
|
||||
### 2.4 HQ 侧栏顺序
|
||||
|
||||
仅改 `apps/admin-web/src/layouts/AdminLayout.tsx` 的 `MENU_ITEMS` 展示顺序与「发票」文案;路由、权限 key 不变。无权限项仍按原规则隐藏。
|
||||
|
||||
固定前 11 项:概览 → 用户 → 商品 → 订单 → 开城 → 门店 → 财务 → 好客权益 → 配送单 → 工单 → 发票。
|
||||
其余:推广码 → 微信绑定 → 企微机器人 → 语言模型 → 知识库 → 开发计划 → OSS 资源库 → 日志 → 权限分配 → HQ账户 → 白名单管理。
|
||||
最后一项:系统设置。
|
||||
|
||||
侧栏「发票」对应原「发票管理」入口(`/invoices`),页内标题仍为发票管理。
|
||||
|
||||
---
|
||||
|
||||
## 3. 关键路径
|
||||
|
||||
| 域 | 路径 |
|
||||
|----|------|
|
||||
| 大屏页 | `apps/admin-web/src/pages/BigScreenPage.tsx` |
|
||||
| 样式 | `apps/admin-web/src/admin.css` |
|
||||
| 音频 | `apps/admin-web/src/assets/big-screen/dukang-bgm.mp3` |
|
||||
| 中文对照 | `apps/admin-web/src/lib/display-labels.ts` |
|
||||
| 日志页 | `UserLogsPage` / `StoreLogsPage` / `PartnerLogsPage` / `HqLogsPage` / `ThirdPartyLogsPage` / `DomainEventsPage` / `WecomBotLogsPage` |
|
||||
| 订单详情 | `OrdersPage.tsx` 状态流转表 |
|
||||
| 用户详情 | `UsersPage.tsx` 行为时间线 |
|
||||
| 门店分类删除 | `store-category.service.ts` · `admin-store-categories.controller.ts` · `StoreCategoriesPage.tsx` |
|
||||
| HQ 侧栏顺序 | `apps/admin-web/src/layouts/AdminLayout.tsx` |
|
||||
|
||||
---
|
||||
|
||||
## 4. 发版步骤
|
||||
|
||||
| 步骤 | 动作 |
|
||||
|------|------|
|
||||
| 1 | `pnpm --filter @dukang/admin-web build` |
|
||||
| 2 | 发 `admin-web` 静态资源 + `dukang-api`(分类删除修复) |
|
||||
|
||||
无 Prisma 迁移。
|
||||
|
||||
---
|
||||
|
||||
## 5. 验收清单
|
||||
|
||||
- [ ] 打开 `/orders/big-screen`,右上角可见「播放 BGM」
|
||||
- [ ] 点击后循环播放背景音乐,按钮变为「BGM 播放中」;再点暂停
|
||||
- [ ] 新成交撒花时成交音效清晰,BGM 变轻;特效结束后 BGM 恢复
|
||||
- [ ] 离开大屏后背景音乐停止
|
||||
- [ ] 日志列表「事件 / 端 / 关联 / 渠道 / 状态」为中文,详情抽屉字段名不为 `param1` / `eventName`
|
||||
- [ ] 订单详情「状态流转」操作人为中文,备注中的履约方式为中文
|
||||
- [ ] 用户详情「行为时间线」分类、事件、端为中文
|
||||
- [ ] 删除无门店占用的分类后列表不再出现该行,提示「已删除」(再点「同步默认分类」才会补回默认项)
|
||||
- [ ] 删除有门店占用的分类:行仍在但状态为停用,提示「已有门店占用,已改为停用」
|
||||
- [ ] 有下级的一级分类删除被拒绝,提示先处理下级
|
||||
- [ ] HQ 侧栏前 11 项为:概览、用户、商品、订单、开城、门店、财务、好客权益、配送单、工单、发票
|
||||
- [ ] 系统设置在侧栏最后一项
|
||||
@@ -49,6 +49,8 @@ C 端购酒核销 · 门店扫码核销+打款 · 合伙人拓店履约 · WebAd
|
||||
|
||||
**起购(v3.5.11)**:现场提货 / 同城 / 跨城阈值在城市 `pickup_min_qty` / `local_min_qty` / `cross_min_qty`(瓶当量)。承运商「起送瓶数」只用于物流计价,不拦下单。
|
||||
|
||||
**订单大屏(v3.5.12)**:循环 BGM;右上角「播放 BGM」开关(需点击才出声);成交撒花时压低背景音。HQ 日志 / 订单状态流转 / 用户行为时间线展示中文(存储码不变)。删除门店分类后列表不再自动补种默认树(「同步默认分类」才补)。HQ 侧栏:概览→用户→商品→订单→开城→门店→财务→好客权益→配送单→工单→发票,其余运营/工具项随后,**系统设置最后**。
|
||||
|
||||
## 5. 验收用例(必过)
|
||||
|
||||
**主链路 15 项**:登录、4 SKU、起购、支付+权益、双通道核销、payout、关店不可见、拓店审核、配送完成、退款、T+1/T+30…
|
||||
|
||||
@@ -20,12 +20,12 @@ export class AdminStoreCategoriesController {
|
||||
|
||||
@Get()
|
||||
list() {
|
||||
return this.categories.listTree({ includeDisabled: true, ensure: true });
|
||||
return this.categories.listTree({ includeDisabled: true, ensure: false });
|
||||
}
|
||||
|
||||
@Get('flat')
|
||||
listFlat() {
|
||||
return this.categories.listFlat({ includeDisabled: true, ensure: true });
|
||||
return this.categories.listFlat({ includeDisabled: true, ensure: false });
|
||||
}
|
||||
|
||||
@Post('ensure-defaults')
|
||||
|
||||
@@ -20,7 +20,7 @@ export class StoreCategoryService {
|
||||
|
||||
/**
|
||||
* 仅补齐缺失的默认分类,绝不 update 已有行(避免改动线上历史数据)。
|
||||
* 已存在的 HOTPOT/LOCAL 等保持原样;缺的一级/二级才 create。
|
||||
* 只应由「同步默认分类」或 seed 调用;列表接口不得自动补种,否则 HQ 删除会被立刻重建。
|
||||
*/
|
||||
async ensureDefaults() {
|
||||
for (const root of DEFAULT_STORE_CATEGORY_TREE) {
|
||||
@@ -91,7 +91,8 @@ export class StoreCategoryService {
|
||||
}
|
||||
|
||||
async listTree(options?: { includeDisabled?: boolean; ensure?: boolean }) {
|
||||
if (options?.ensure !== false) {
|
||||
/** 列表默认不补种:否则 HQ 刚删的默认分类会被立刻重建,表现为「已处理但没删掉」 */
|
||||
if (options?.ensure === true) {
|
||||
await this.ensureDefaults();
|
||||
}
|
||||
const includeDisabled = options?.includeDisabled === true;
|
||||
@@ -111,7 +112,7 @@ export class StoreCategoryService {
|
||||
}
|
||||
|
||||
async listFlat(options?: { includeDisabled?: boolean; ensure?: boolean }) {
|
||||
if (options?.ensure !== false) {
|
||||
if (options?.ensure === true) {
|
||||
await this.ensureDefaults();
|
||||
}
|
||||
const includeDisabled = options?.includeDisabled === true;
|
||||
|
||||
@@ -68,7 +68,7 @@ export class PublicStoreCategoriesController {
|
||||
|
||||
@Get()
|
||||
list() {
|
||||
return this.categories.listTree({ includeDisabled: false, ensure: true });
|
||||
return this.categories.listTree({ includeDisabled: false, ensure: false });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ export class PartnerStoreCategoriesController {
|
||||
|
||||
@Get()
|
||||
list() {
|
||||
return this.categories.listTree({ includeDisabled: false, ensure: true });
|
||||
return this.categories.listTree({ includeDisabled: false, ensure: false });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user