v3.5.8和v3.5.9版本更新
CI / verify (pull_request) Waiting to run

This commit is contained in:
2026-08-25 09:20:32 +08:00
parent 7304c7a8e1
commit 5935024ea8
101 changed files with 7640 additions and 5364 deletions
+104 -8
View File
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useState } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react';
import { Link, useLocation, useNavigate } from 'react-router-dom';
import {
Alert,
@@ -19,6 +19,7 @@ import {
import type { ColumnsType } from 'antd/es/table';
import { USER_SOURCE_TYPE_LABELS, resolveUserLogCategory, type UserSourceType } from '@dukang/shared-types';
import { request, type AdminUserRow, type HqProfile, type Paginated } from '../lib/api';
import { useAdminListColumns } from '../lib/useAdminListColumns';
import { ORDER_STATUS_LABELS, fmtTime, maskPhone } from '../lib/constants';
type UserOrderRow = {
@@ -30,6 +31,77 @@ type UserOrderRow = {
createdAt: string;
};
function NicknameCell({
id,
nickname,
onSaved,
}: {
id: string;
nickname: string | null;
onSaved: (id: string, nickname: string | null) => void;
}) {
const [editing, setEditing] = useState(false);
const [value, setValue] = useState(nickname ?? '');
const savingRef = useRef(false);
useEffect(() => {
if (!editing) setValue(nickname ?? '');
}, [nickname, editing]);
async function commit() {
if (savingRef.current) return;
const next = value.trim() || null;
const prev = nickname?.trim() || null;
setEditing(false);
if (next === prev) return;
savingRef.current = true;
try {
const res = await request<{ id: string; nickname: string | null }>(`/admin/users/${id}`, {
method: 'PUT',
body: JSON.stringify({ nickname: next ?? '' }),
});
onSaved(id, res.nickname);
message.success('昵称已保存');
} catch (e) {
message.error(e instanceof Error ? e.message : '保存失败');
setValue(nickname ?? '');
} finally {
savingRef.current = false;
}
}
if (editing) {
return (
<Input
size="small"
autoFocus
maxLength={64}
value={value}
onChange={(e) => setValue(e.target.value)}
onBlur={() => void commit()}
onPressEnter={() => void commit()}
onKeyDown={(e) => {
if (e.key === 'Escape') void commit();
}}
onClick={(e) => e.stopPropagation()}
/>
);
}
return (
<span
title="双击修改,离开后保存"
onDoubleClick={(e) => {
e.stopPropagation();
setEditing(true);
}}
style={{ cursor: 'text', display: 'inline-block', minWidth: 48 }}
>
{nickname || '—'}
</span>
);
}
/** 好客权益金额展示:0 与空值统一显示占位,避免整列都是 ¥0.00 干扰 */
function fmtBenefit(v: number | null | undefined) {
const n = Number(v ?? 0);
@@ -244,7 +316,7 @@ export default function UsersPage() {
{ title: '下单时间', dataIndex: 'createdAt', width: 160, render: fmtTime },
];
const columns: ColumnsType<AdminUserRow> = [
const baseColumns: ColumnsType<AdminUserRow> = [
{
title: '用户编号',
dataIndex: 'userNo',
@@ -256,12 +328,30 @@ export default function UsersPage() {
</Space>
),
},
{ title: '昵称', dataIndex: 'nickname', width: 100 },
{
title: '昵称',
dataIndex: 'nickname',
width: 140,
render: (v: string | null, row) => (
<NicknameCell
id={row.id}
nickname={v}
onSaved={(id, nickname) => {
setData((prev) =>
prev
? { ...prev, items: prev.items.map((u) => (u.id === id ? { ...u, nickname } : u)) }
: prev,
);
setDetail((d) => (d && d.id === id ? { ...d, nickname } : d));
}}
/>
),
},
{
title: '手机',
dataIndex: 'phone',
width: 120,
render: (v) => maskPhone(v),
render: (v) => v || '—',
},
{
title: '验手机',
@@ -289,7 +379,6 @@ export default function UsersPage() {
title: '来源 ID',
dataIndex: 'sourceRefId',
width: 100,
ellipsis: true,
render: (v, row) => {
if (!v) return '—';
if (row.sourceType === 'PROMO_CODE') {
@@ -306,13 +395,11 @@ export default function UsersPage() {
title: '来源标签',
dataIndex: 'sourceLabel',
width: 120,
ellipsis: true,
render: (v) => v || '—',
},
{
title: 'deviceKey',
dataIndex: 'deviceKey',
ellipsis: true,
render: (v) => v || '—',
},
{
@@ -377,10 +464,18 @@ export default function UsersPage() {
},
];
const { columns, settingsButton, settingsModal } = useAdminListColumns('users', baseColumns, {
page,
pageSize,
});
return (
<div>
{settingsModal}
<Space style={{ marginBottom: 16, width: '100%', justifyContent: 'space-between' }}>
<Typography.Title level={4} style={{ margin: 0 }}></Typography.Title>
<Space>
{settingsButton}
{canDeleteUsers ? (
<Button
danger
@@ -390,6 +485,7 @@ export default function UsersPage() {
{selectedRowKeys.length ? ` (${selectedRowKeys.length})` : ''}
</Button>
) : null}
</Space>
</Space>
<Form form={form} layout="inline" style={{ marginBottom: 16 }} onFinish={() => { setPage(1); void load(); }}>
<Form.Item name="phone" label="手机号">
@@ -429,7 +525,7 @@ export default function UsersPage() {
loading={loading}
columns={columns}
dataSource={data?.items ?? []}
scroll={{ x: 1830 }}
scroll={{ x: 'max-content' }}
rowSelection={canDeleteUsers ? {
selectedRowKeys,
preserveSelectedRowKeys: true,