diff --git a/apps/admin-web/src/lib/hq-log.ts b/apps/admin-web/src/lib/hq-log.ts index 760c75e..198485c 100644 --- a/apps/admin-web/src/lib/hq-log.ts +++ b/apps/admin-web/src/lib/hq-log.ts @@ -27,6 +27,7 @@ export const HQ_OPERATION_ACTION_OPTIONS = [ { value: 'STORE_AUDIT', label: '门店审核' }, { value: 'STORE_ACCOUNT_CREATE', label: '新增门店账户' }, { value: 'STORE_ACCOUNT_UPDATE', label: '编辑门店账户' }, + { value: 'STORE_ACCOUNT_STAFF_DELETE', label: '删除门店子账号' }, { value: 'STORE_CATEGORY_CREATE', label: '新增门店分类' }, { value: 'STORE_CATEGORY_UPDATE', label: '编辑门店分类' }, { value: 'STORE_CATEGORY_DELETE', label: '删除门店分类' }, diff --git a/apps/admin-web/src/pages/CitiesPage.tsx b/apps/admin-web/src/pages/CitiesPage.tsx index fca7551..5c6325e 100644 --- a/apps/admin-web/src/pages/CitiesPage.tsx +++ b/apps/admin-web/src/pages/CitiesPage.tsx @@ -1,4 +1,5 @@ import { useCallback, useEffect, useState } from 'react'; +import { Link } from 'react-router-dom'; import { Button, Descriptions, @@ -228,7 +229,16 @@ export default function CitiesPage() { { title: '省份', dataIndex: 'province', width: 90 }, { title: '状态', dataIndex: 'status', width: 90, render: (s) => {CITY_STATUS_LABELS[s] || s} }, { title: '合伙人', dataIndex: 'partnerBindingCount', width: 90 }, - { title: '门店', dataIndex: 'storeCount', width: 70 }, + { + title: '门店', + dataIndex: 'storeCount', + width: 70, + render: (n: number, row) => ( + + {n ?? 0} + + ), + }, { title: '订单', dataIndex: 'orderCount', width: 70 }, { title: '创建', dataIndex: 'createdAt', width: 160, render: fmtTime }, { diff --git a/apps/admin-web/src/pages/StoreAccountsPage.tsx b/apps/admin-web/src/pages/StoreAccountsPage.tsx index 5e4c10e..f7e22fc 100644 --- a/apps/admin-web/src/pages/StoreAccountsPage.tsx +++ b/apps/admin-web/src/pages/StoreAccountsPage.tsx @@ -1,6 +1,6 @@ import { useState } from 'react'; import { - Button, Descriptions, Drawer, Form, Input, Modal, Select, Space, Table, Tag, Typography, message, + Button, Descriptions, Drawer, Form, Input, Modal, Popconfirm, Select, Space, Table, Tag, Typography, message, } from 'antd'; import type { ColumnsType } from 'antd/es/table'; import { request, type Paginated } from '../lib/api'; @@ -45,12 +45,33 @@ export default function StoreAccountsPage() { const [drawerOpen, setDrawerOpen] = useState(false); const [createOpen, setCreateOpen] = useState(false); const [stores, setStores] = useState([]); + const [deletingStaffId, setDeletingStaffId] = useState(null); async function loadStores() { const res = await request>(`/admin/stores?pageSize=${ADMIN_OPTIONS_PAGE_SIZE}`); setStores(res.items); } + async function refreshDetail(accountId: string) { + const d = await request(`/admin/store-accounts/${accountId}`); + setDetail(d); + void reload(); + } + + async function deleteStaff(staffId: string) { + if (!detail) return; + setDeletingStaffId(staffId); + try { + await request(`/admin/store-accounts/${detail.id}/staff/${staffId}`, { method: 'DELETE' }); + message.success('子账号已删除'); + await refreshDetail(detail.id); + } catch (e) { + message.error(e instanceof Error ? e.message : '删除失败'); + } finally { + setDeletingStaffId(null); + } + } + const columns: ColumnsType = [ { title: '姓名', dataIndex: 'name', width: 100 }, { title: '手机', dataIndex: 'phone', width: 120 }, @@ -218,10 +239,32 @@ export default function StoreAccountsPage() { dataIndex: 'status', render: (s) => {ACCOUNT_STATUS_LABELS[s] || s}, }, + { + title: '操作', + width: 80, + render: (_, staff) => ( + void deleteStaff(staff.id)} + > + + + ), + }, ]} /> - ) : null} + ) : ( + + 暂无子账号 + + )} )} diff --git a/apps/admin-web/src/pages/StoresPage.tsx b/apps/admin-web/src/pages/StoresPage.tsx index cca51b8..305da2a 100644 --- a/apps/admin-web/src/pages/StoresPage.tsx +++ b/apps/admin-web/src/pages/StoresPage.tsx @@ -1,5 +1,5 @@ -import { useMemo, useState } from 'react'; -import { useNavigate } from 'react-router-dom'; +import { useEffect, useMemo, useState } from 'react'; +import { useNavigate, useSearchParams } from 'react-router-dom'; import { Alert, Button, @@ -322,10 +322,14 @@ type CategoryNode = { export default function StoresPage() { const navigate = useNavigate(); + const [searchParams] = useSearchParams(); + const initialCityId = searchParams.get('cityId') ?? ''; const [form] = Form.useForm(); const [editForm] = Form.useForm(); const [createForm] = Form.useForm(); - const [filters, setFilters] = useState>({}); + const [filters, setFilters] = useState>(() => + initialCityId ? { cityId: initialCityId } : {}, + ); const { data, loading, page, pageSize, setPage, setPageSize, reload } = useAdminList( '/admin/stores', () => { @@ -334,10 +338,12 @@ export default function StoresPage() { if (filters.status) qs.set('status', filters.status); if (filters.auditStatus) qs.set('auditStatus', filters.auditStatus); if (filters.phone) qs.set('phone', filters.phone); + if (filters.cityId) qs.set('cityId', filters.cityId); return qs; }, [filters], ); + const [filterCities, setFilterCities] = useState([]); const [detail, setDetail] = useState | null>(null); const [drawerOpen, setDrawerOpen] = useState(false); const [rejectOpen, setRejectOpen] = useState(false); @@ -356,6 +362,25 @@ export default function StoresPage() { const [saving, setSaving] = useState(false); const [phoneMismatch, setPhoneMismatch] = useState(null); + useEffect(() => { + void request>(`/admin/cities?pageSize=${ADMIN_OPTIONS_PAGE_SIZE}`) + .then((res) => setFilterCities(res.items)) + .catch(() => {}); + }, []); + + useEffect(() => { + const cityId = searchParams.get('cityId') ?? ''; + setFilters((prev) => { + if ((prev.cityId ?? '') === cityId) return prev; + const next = { ...prev }; + if (cityId) next.cityId = cityId; + else delete next.cityId; + return next; + }); + form.setFieldsValue({ cityId: cityId || undefined }); + if (cityId) setPage(1); + }, [searchParams, form, setPage]); + const selectedPartnerId = Form.useWatch('partnerAccountId', createForm); const selectedRegionCodes = Form.useWatch('regionCodes', createForm); const selectedCityId = Form.useWatch('cityId', createForm); @@ -726,6 +751,16 @@ export default function StoresPage() {
{ setFilters(v); setPage(1); }}> + + ({ value, label }))} /> @@ -738,7 +773,18 @@ export default function StoresPage() { /> - + + +
{ setPage(p); setPageSize(ps); } }} /> diff --git a/server/dukang-api/src/common/hq-operation/hq-operation.constants.ts b/server/dukang-api/src/common/hq-operation/hq-operation.constants.ts index 4d6eb3c..cd84429 100644 --- a/server/dukang-api/src/common/hq-operation/hq-operation.constants.ts +++ b/server/dukang-api/src/common/hq-operation/hq-operation.constants.ts @@ -28,6 +28,7 @@ export const HqOperationAction = { STORE_AUDIT: 'STORE_AUDIT', STORE_ACCOUNT_CREATE: 'STORE_ACCOUNT_CREATE', STORE_ACCOUNT_UPDATE: 'STORE_ACCOUNT_UPDATE', + STORE_ACCOUNT_STAFF_DELETE: 'STORE_ACCOUNT_STAFF_DELETE', STORE_MEDIA_CREATE: 'STORE_MEDIA_CREATE', STORE_MEDIA_UPDATE: 'STORE_MEDIA_UPDATE', STORE_MEDIA_DELETE: 'STORE_MEDIA_DELETE', @@ -127,6 +128,7 @@ export const HQ_OPERATION_ACTION_LABELS: Record = { [HqOperationAction.STORE_AUDIT]: '门店审核', [HqOperationAction.STORE_ACCOUNT_CREATE]: '新增门店账户', [HqOperationAction.STORE_ACCOUNT_UPDATE]: '编辑门店账户', + [HqOperationAction.STORE_ACCOUNT_STAFF_DELETE]: '删除门店子账号', [HqOperationAction.STORE_MEDIA_CREATE]: '新增门店资源', [HqOperationAction.STORE_MEDIA_UPDATE]: '编辑门店资源', [HqOperationAction.STORE_MEDIA_DELETE]: '删除门店资源', diff --git a/server/dukang-api/src/modules/ops/admin-stores.controller.ts b/server/dukang-api/src/modules/ops/admin-stores.controller.ts index 9c0762d..068c874 100644 --- a/server/dukang-api/src/modules/ops/admin-stores.controller.ts +++ b/server/dukang-api/src/modules/ops/admin-stores.controller.ts @@ -94,6 +94,16 @@ export class AdminStoreAccountsController { update(@Param('id') id: string, @Body() dto: UpdateStoreAccountDto) { return this.service.updateStoreAccount(BigInt(id), dto); } + + @Delete(':id/staff/:staffId') + @HqOperation({ + action: HqOperationAction.STORE_ACCOUNT_STAFF_DELETE, + refType: 'STORE_ACCOUNT', + refIdParam: 'staffId', + }) + deleteStaff(@Param('id') id: string, @Param('staffId') staffId: string) { + return this.service.deleteStoreStaff(BigInt(id), BigInt(staffId)); + } } @Controller('admin/store-media') diff --git a/server/dukang-api/src/modules/ops/admin-stores.service.ts b/server/dukang-api/src/modules/ops/admin-stores.service.ts index 10603b3..07f0a7c 100644 --- a/server/dukang-api/src/modules/ops/admin-stores.service.ts +++ b/server/dukang-api/src/modules/ops/admin-stores.service.ts @@ -717,4 +717,26 @@ export class AdminStoresService { }); return serializeBigInt(account); } + + /** HQ 删除门店子账号(非主账号) */ + async deleteStoreStaff(parentAccountId: bigint, staffId: bigint) { + const parent = await this.prisma.storeAccount.findUnique({ where: { id: parentAccountId } }); + if (!parent || parent.isPrimary !== 1) { + throw new BadRequestException('主账号不存在'); + } + const staff = await this.prisma.storeAccount.findFirst({ + where: { id: staffId, parentAccountId, isPrimary: 0 }, + }); + if (!staff) throw new NotFoundException('子账号不存在'); + + const pending = await this.prisma.redeemPendingRecord.count({ + where: { storeAccountId: staffId }, + }); + if (pending > 0) { + throw new BadRequestException('该子账号仍有待处理核销单,无法删除'); + } + + await this.prisma.storeAccount.delete({ where: { id: staffId } }); + return { ok: true }; + } }