feat(admin): link city-partner store count; fix benefitUsageRule null string
CI / verify (pull_request) Has been cancelled
CI / verify (pull_request) Has been cancelled
City partner storeCount jumps to /stores?partnerId=; normalize empty/null text so usage rule is not saved as literal "null. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -301,7 +301,16 @@ export default function CityPartnersPage() {
|
||||
ellipsis: true,
|
||||
render: (v) => v || '—',
|
||||
},
|
||||
{ title: '门店', dataIndex: 'storeCount', width: 60 },
|
||||
{
|
||||
title: '门店',
|
||||
dataIndex: 'storeCount',
|
||||
width: 60,
|
||||
render: (n: number, row) => (
|
||||
<Link to={`/stores?partnerId=${row.id}`} title={`查看「${row.companyName || row.phone}」门店`}>
|
||||
{n ?? 0}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{ title: '子账号', dataIndex: 'accountCount', width: 70, render: (n) => Math.max(0, n - 1) },
|
||||
{ title: '创建', dataIndex: 'createdAt', width: 150, render: fmtTime },
|
||||
{
|
||||
@@ -444,7 +453,11 @@ export default function CityPartnersPage() {
|
||||
<>
|
||||
<Descriptions column={2} size="small" bordered style={{ marginBottom: 16 }}>
|
||||
<Descriptions.Item label="城市">{detail.cityName ?? '—'}</Descriptions.Item>
|
||||
<Descriptions.Item label="门店数">{detail.storeCount ?? 0}</Descriptions.Item>
|
||||
<Descriptions.Item label="门店数">
|
||||
<Link to={`/stores?partnerId=${detail.id}`} title="查看该城市合伙人门店">
|
||||
{detail.storeCount ?? 0}
|
||||
</Link>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="管仓仓库" span={2}>
|
||||
{detail.managedWarehouseName ?? (
|
||||
<Typography.Text type="secondary">
|
||||
|
||||
@@ -324,12 +324,16 @@ export default function StoresPage() {
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const initialCityId = searchParams.get('cityId') ?? '';
|
||||
const initialPartnerId = searchParams.get('partnerId') ?? '';
|
||||
const [form] = Form.useForm();
|
||||
const [editForm] = Form.useForm();
|
||||
const [createForm] = Form.useForm<StoreCreateForm>();
|
||||
const [filters, setFilters] = useState<Record<string, string>>(() =>
|
||||
initialCityId ? { cityId: initialCityId } : {},
|
||||
);
|
||||
const [filters, setFilters] = useState<Record<string, string>>(() => {
|
||||
const init: Record<string, string> = {};
|
||||
if (initialCityId) init.cityId = initialCityId;
|
||||
if (initialPartnerId) init.partnerId = initialPartnerId;
|
||||
return init;
|
||||
});
|
||||
const { data, loading, page, pageSize, setPage, setPageSize, reload } = useAdminList<StoreRow>(
|
||||
'/admin/stores',
|
||||
() => {
|
||||
@@ -339,11 +343,13 @@ export default function StoresPage() {
|
||||
if (filters.auditStatus) qs.set('auditStatus', filters.auditStatus);
|
||||
if (filters.phone) qs.set('phone', filters.phone);
|
||||
if (filters.cityId) qs.set('cityId', filters.cityId);
|
||||
if (filters.partnerId) qs.set('partnerId', filters.partnerId);
|
||||
return qs;
|
||||
},
|
||||
[filters],
|
||||
);
|
||||
const [filterCities, setFilterCities] = useState<CityOption[]>([]);
|
||||
const [filterPartners, setFilterPartners] = useState<PartnerOption[]>([]);
|
||||
const [detail, setDetail] = useState<Record<string, unknown> | null>(null);
|
||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||
const [rejectOpen, setRejectOpen] = useState(false);
|
||||
@@ -366,19 +372,34 @@ export default function StoresPage() {
|
||||
void request<Paginated<CityOption>>(`/admin/cities?pageSize=${ADMIN_OPTIONS_PAGE_SIZE}`)
|
||||
.then((res) => setFilterCities(res.items))
|
||||
.catch(() => {});
|
||||
void request<Paginated<PartnerOption>>(`/admin/partners?pageSize=${ADMIN_OPTIONS_PAGE_SIZE}`)
|
||||
.then((res) => setFilterPartners(res.items))
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const cityId = searchParams.get('cityId') ?? '';
|
||||
const partnerId = searchParams.get('partnerId') ?? '';
|
||||
let changed = false;
|
||||
setFilters((prev) => {
|
||||
if ((prev.cityId ?? '') === cityId) return prev;
|
||||
const next = { ...prev };
|
||||
if (cityId) next.cityId = cityId;
|
||||
else delete next.cityId;
|
||||
return next;
|
||||
if ((prev.cityId ?? '') !== cityId) {
|
||||
changed = true;
|
||||
if (cityId) next.cityId = cityId;
|
||||
else delete next.cityId;
|
||||
}
|
||||
if ((prev.partnerId ?? '') !== partnerId) {
|
||||
changed = true;
|
||||
if (partnerId) next.partnerId = partnerId;
|
||||
else delete next.partnerId;
|
||||
}
|
||||
return changed ? next : prev;
|
||||
});
|
||||
form.setFieldsValue({ cityId: cityId || undefined });
|
||||
if (cityId) setPage(1);
|
||||
form.setFieldsValue({
|
||||
cityId: cityId || undefined,
|
||||
partnerId: partnerId || undefined,
|
||||
});
|
||||
if (cityId || partnerId) setPage(1);
|
||||
}, [searchParams, form, setPage]);
|
||||
|
||||
const selectedPartnerId = Form.useWatch('partnerAccountId', createForm);
|
||||
@@ -452,7 +473,12 @@ export default function StoresPage() {
|
||||
// 以门店手机号为准保存;若与账号登录号不一致,保存时会强制同步到登录账号
|
||||
phone: storePhone || loginPhone,
|
||||
intro: d.intro,
|
||||
benefitUsageRule: d.benefitUsageRule,
|
||||
benefitUsageRule:
|
||||
d.benefitUsageRule != null &&
|
||||
String(d.benefitUsageRule).trim() &&
|
||||
!/^null$/i.test(String(d.benefitUsageRule).trim())
|
||||
? String(d.benefitUsageRule)
|
||||
: '',
|
||||
coverUrl: d.coverUrl,
|
||||
province: d.province,
|
||||
city: d.cityName,
|
||||
@@ -490,7 +516,12 @@ export default function StoresPage() {
|
||||
phone: v.phone,
|
||||
coverUrl: v.coverUrl,
|
||||
intro: v.intro,
|
||||
benefitUsageRule: v.benefitUsageRule ?? null,
|
||||
benefitUsageRule:
|
||||
typeof v.benefitUsageRule === 'string' &&
|
||||
v.benefitUsageRule.trim() &&
|
||||
!/^null$/i.test(v.benefitUsageRule.trim())
|
||||
? v.benefitUsageRule.trim()
|
||||
: null,
|
||||
categoryId: v.categoryId,
|
||||
province: v.province,
|
||||
city: v.city,
|
||||
@@ -761,6 +792,16 @@ export default function StoresPage() {
|
||||
options={filterCities.map((c) => ({ value: c.id, label: c.name }))}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="partnerId" label="城市合伙人">
|
||||
<Select
|
||||
allowClear
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
style={{ width: 180 }}
|
||||
placeholder="全部"
|
||||
options={filterPartners.map((p) => ({ value: p.id, label: p.companyName || p.id }))}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="status" label="营业状态">
|
||||
<Select allowClear style={{ width: 100 }} placeholder="全部" options={Object.entries(STORE_STATUS_LABELS).map(([value, label]) => ({ value, label }))} />
|
||||
</Form.Item>
|
||||
@@ -779,7 +820,9 @@ export default function StoresPage() {
|
||||
form.resetFields();
|
||||
setFilters({});
|
||||
setPage(1);
|
||||
if (searchParams.has('cityId')) navigate('/stores', { replace: true });
|
||||
if (searchParams.has('cityId') || searchParams.has('partnerId')) {
|
||||
navigate('/stores', { replace: true });
|
||||
}
|
||||
}}
|
||||
>
|
||||
重置
|
||||
|
||||
@@ -67,7 +67,10 @@ export default function StoreDetailPage() {
|
||||
phone: String(data.phone || ''),
|
||||
address: String(data.address || ''),
|
||||
intro: String(data.intro || ''),
|
||||
benefitUsageRule: String(data.benefitUsageRule || ''),
|
||||
benefitUsageRule: (() => {
|
||||
const raw = String(data.benefitUsageRule || '').trim();
|
||||
return raw && !/^null$/i.test(raw) ? raw : '';
|
||||
})(),
|
||||
latitude: data.latitude != null && data.latitude !== '' ? String(data.latitude) : '',
|
||||
longitude: data.longitude != null && data.longitude !== '' ? String(data.longitude) : '',
|
||||
});
|
||||
|
||||
@@ -174,7 +174,9 @@ export default function StoreDetailPage() {
|
||||
]);
|
||||
|
||||
const intro = store.intro?.trim() || '';
|
||||
const benefitRule = store.benefitUsageRule?.trim() || '';
|
||||
const benefitRuleRaw = store.benefitUsageRule?.trim() || '';
|
||||
const benefitRule =
|
||||
benefitRuleRaw && !/^null$/i.test(benefitRuleRaw) ? benefitRuleRaw : '';
|
||||
|
||||
function previewEnv(index: number) {
|
||||
if (!envPhotos.length) return;
|
||||
|
||||
@@ -17,6 +17,14 @@ import type {
|
||||
UpdateStoreStatusDto,
|
||||
} from './dto/admin-mutate.dto';
|
||||
|
||||
/** 选填文案:空 / null / "null" 一律存库为 null,避免 String(null)==="null" */
|
||||
function normalizeStoreOptionalText(value: unknown): string | null {
|
||||
if (value == null) return null;
|
||||
const s = String(value).trim();
|
||||
if (!s || /^null$/i.test(s) || /^undefined$/i.test(s)) return null;
|
||||
return s;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class AdminStoresService {
|
||||
constructor(
|
||||
@@ -286,7 +294,7 @@ export class AdminStoresService {
|
||||
...(normalizedPhone !== undefined ? { phone: normalizedPhone } : {}),
|
||||
...(dto.intro !== undefined ? { intro: dto.intro } : {}),
|
||||
...(dto.benefitUsageRule !== undefined
|
||||
? { benefitUsageRule: String(dto.benefitUsageRule).trim() || null }
|
||||
? { benefitUsageRule: normalizeStoreOptionalText(dto.benefitUsageRule) }
|
||||
: {}),
|
||||
...(dto.address !== undefined ? { address: dto.address.trim() } : {}),
|
||||
...(dto.province !== undefined ? { province: dto.province.trim() } : {}),
|
||||
@@ -417,7 +425,7 @@ export class AdminStoresService {
|
||||
if (intro && (intro.length < 2 || intro.length > 500)) {
|
||||
throw new BadRequestException('门店简介须为 2~500 字');
|
||||
}
|
||||
const benefitUsageRule = dto.benefitUsageRule?.trim() || null;
|
||||
const benefitUsageRule = normalizeStoreOptionalText(dto.benefitUsageRule);
|
||||
if (benefitUsageRule && benefitUsageRule.length > 1000) {
|
||||
throw new BadRequestException('好客权益券使用规则最多 1000 字');
|
||||
}
|
||||
|
||||
@@ -28,6 +28,14 @@ function haversineMeters(lat1: number, lng1: number, lat2: number, lng2: number)
|
||||
return 2 * R * Math.asin(Math.min(1, Math.sqrt(a)));
|
||||
}
|
||||
|
||||
/** 选填文案:空 / null / "null" 存库为 null,避免 String(null)==="null" */
|
||||
function normalizeOptionalTextField(value: unknown): string | null {
|
||||
if (value == null) return null;
|
||||
const s = String(value).trim();
|
||||
if (!s || /^null$/i.test(s) || /^undefined$/i.test(s)) return null;
|
||||
return s;
|
||||
}
|
||||
|
||||
function parseOptionalCoord(value: unknown, kind: 'lat' | 'lng' = 'lng'): number | null {
|
||||
if (value == null || value === '') return null;
|
||||
const n = typeof value === 'number' ? value : Number(value);
|
||||
@@ -301,9 +309,8 @@ export class StoreService {
|
||||
if (introRaw && (introRaw.length < 2 || introRaw.length > 500)) {
|
||||
throw new BadRequestException('门店简介须为 2~500 字');
|
||||
}
|
||||
const benefitUsageRuleRaw =
|
||||
body.benefitUsageRule != null ? String(body.benefitUsageRule).trim() : '';
|
||||
if (benefitUsageRuleRaw.length > 1000) {
|
||||
const benefitUsageRuleRaw = normalizeOptionalTextField(body.benefitUsageRule);
|
||||
if (benefitUsageRuleRaw && benefitUsageRuleRaw.length > 1000) {
|
||||
throw new BadRequestException('好客权益券使用规则最多 1000 字');
|
||||
}
|
||||
|
||||
@@ -325,7 +332,7 @@ export class StoreService {
|
||||
district: String(body.district ?? ''),
|
||||
address: String(body.address),
|
||||
intro: introRaw || null,
|
||||
benefitUsageRule: benefitUsageRuleRaw || null,
|
||||
benefitUsageRule: benefitUsageRuleRaw,
|
||||
avgPrice: avgPriceRaw,
|
||||
openTime,
|
||||
closeTime,
|
||||
@@ -522,7 +529,9 @@ export class StoreService {
|
||||
const address = body.address !== undefined ? String(body.address).trim() : undefined;
|
||||
const introRaw = body.intro !== undefined ? String(body.intro).trim() : undefined;
|
||||
const benefitUsageRuleRaw =
|
||||
body.benefitUsageRule !== undefined ? String(body.benefitUsageRule).trim() : undefined;
|
||||
body.benefitUsageRule !== undefined
|
||||
? normalizeOptionalTextField(body.benefitUsageRule)
|
||||
: undefined;
|
||||
const latitude =
|
||||
body.latitude !== undefined ? parseOptionalCoord(body.latitude, 'lat') : undefined;
|
||||
const longitude =
|
||||
@@ -536,7 +545,7 @@ export class StoreService {
|
||||
if (introRaw && (introRaw.length < 2 || introRaw.length > 500)) {
|
||||
throw new BadRequestException('门店简介须为 2~500 字');
|
||||
}
|
||||
if (benefitUsageRuleRaw !== undefined && benefitUsageRuleRaw.length > 1000) {
|
||||
if (benefitUsageRuleRaw != null && benefitUsageRuleRaw.length > 1000) {
|
||||
throw new BadRequestException('好客权益券使用规则最多 1000 字');
|
||||
}
|
||||
if (
|
||||
@@ -561,7 +570,7 @@ export class StoreService {
|
||||
...(hasCoordsUpdate ? { latitude, longitude } : {}),
|
||||
...(introRaw !== undefined ? { intro: introRaw || null } : {}),
|
||||
...(benefitUsageRuleRaw !== undefined
|
||||
? { benefitUsageRule: benefitUsageRuleRaw || null }
|
||||
? { benefitUsageRule: benefitUsageRuleRaw }
|
||||
: {}),
|
||||
...(resubmitAudit
|
||||
? {
|
||||
|
||||
Reference in New Issue
Block a user