接单平台:新增店铺筛选条件
This commit is contained in:
@@ -453,6 +453,12 @@ export type ListInput = {
|
||||
/** 后台待办跳转时按工单主键精确定位 */
|
||||
workOrderId?: number
|
||||
keyword?: string
|
||||
/**
|
||||
* 后台按店铺筛选(精确匹配,可多选)。
|
||||
* 来源是 `orders.shop_name`:工单只存 `order_id`,`material.source.shopName` 只是货源快照,
|
||||
* 两者并不等价,所以这里统一按订单归属店铺过滤。
|
||||
*/
|
||||
shopNames?: string[]
|
||||
/** 打手类型筛选:internal 内部打手 / external 外部打手 */
|
||||
workerType?: string
|
||||
/** 后台打手列表按主键精确定位。 */
|
||||
|
||||
@@ -347,6 +347,7 @@ export async function listWorkOrders({
|
||||
status = '',
|
||||
workOrderId = 0,
|
||||
keyword = '',
|
||||
shopNames = [],
|
||||
workerId = 0,
|
||||
workerLevelIds = [],
|
||||
categoryId = 0,
|
||||
@@ -370,6 +371,7 @@ export async function listWorkOrders({
|
||||
statuses: effectiveStatuses,
|
||||
workOrderId,
|
||||
keyword,
|
||||
shopNames,
|
||||
workerId,
|
||||
workerLevelIds,
|
||||
...(resolvedWorkerIds ? { workerIds: resolvedWorkerIds } : {}),
|
||||
@@ -455,6 +457,26 @@ export async function listWorkOrders({
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 后台「店铺」筛选的候选值。
|
||||
*
|
||||
* 口径是「订单归属店铺」(orders.shop_name),只列真正挂在工单上的店铺——
|
||||
* 订单里存在但没同步出工单的店铺如果出现在下拉里,选了必然是空列表。
|
||||
* 固定全量去重、不随其它筛选变化,避免用户选完店铺后候选自己缩水。
|
||||
*/
|
||||
export async function listWorkOrderShopNames(): Promise<string[]> {
|
||||
const result = await query<{ shop_name: string }>(
|
||||
`SELECT DISTINCT BTRIM(o.shop_name) AS shop_name
|
||||
FROM work_orders wo
|
||||
INNER JOIN orders o ON o.id = wo.order_id
|
||||
WHERE BTRIM(o.shop_name) <> ''
|
||||
ORDER BY shop_name ASC`,
|
||||
)
|
||||
return result.rows
|
||||
.map((row) => String(row.shop_name || '').trim())
|
||||
.filter((shopName) => shopName.length > 0)
|
||||
}
|
||||
|
||||
/** 只返回筛选范围总数,不加载工单 JSON、取消事件和当前页关联字段。 */
|
||||
export async function countWorkOrders(input: ListInput = {}): Promise<number> {
|
||||
const resolvedWorkerIds = await resolveWorkerLevelFilterIds(input.workerLevelIds)
|
||||
@@ -1077,6 +1099,7 @@ export function buildWorkOrderWhere({
|
||||
statuses = [],
|
||||
workOrderId = 0,
|
||||
keyword = '',
|
||||
shopNames = [],
|
||||
workerId = 0,
|
||||
workerLevelIds = [],
|
||||
workerIds,
|
||||
@@ -1275,6 +1298,22 @@ export function buildWorkOrderWhere({
|
||||
filters.push(`${workOrderSearchExpression()} LIKE LOWER($${params.length})`)
|
||||
}
|
||||
}
|
||||
|
||||
const normalizedShopNames = [
|
||||
...new Set(shopNames.map((item) => String(item || '').trim()).filter(Boolean)),
|
||||
]
|
||||
if (normalizedShopNames.length > 0) {
|
||||
// 店铺口径是「订单归属店铺」:工单只存 order_id,必须回查 orders.shop_name。
|
||||
// 下拉选项本身就是真实值,这里做精确匹配,并用 BTRIM 兼容历史脏数据的前后空格
|
||||
// (表达式索引见 103_work_order_shop_filter_index.sql)。
|
||||
params.push(normalizedShopNames)
|
||||
filters.push(`wo.order_id IN (
|
||||
SELECT shop_order.id
|
||||
FROM orders shop_order
|
||||
WHERE BTRIM(shop_order.shop_name) = ANY($${params.length}::text[])
|
||||
)`)
|
||||
}
|
||||
|
||||
if (workerId) {
|
||||
params.push(workerId)
|
||||
filters.push(`(
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
getAdminWorkOrderEvents,
|
||||
getAdminWorkOrderSharing,
|
||||
listAdminMockCustomShops,
|
||||
listAdminWorkOrderShopNames,
|
||||
listAdminWorkOrders,
|
||||
pinAdminWorkOrder,
|
||||
publishAdminWorkOrder,
|
||||
@@ -47,6 +48,17 @@ router.get(
|
||||
}),
|
||||
)
|
||||
|
||||
// 「店铺」筛选下拉:固定全量候选,不随列表筛选变化(前端 Select 自带搜索过滤)。
|
||||
router.get(
|
||||
'/worker-platform/orders/shop-options',
|
||||
requireAdminPermission('worker_order.view'),
|
||||
createJsonHandler(() => listAdminWorkOrderShopNames(), {
|
||||
successMessage: 'ok',
|
||||
errorMessage: '读取店铺筛选选项失败',
|
||||
scope: '[admin/worker-platform/orders/shop-options]',
|
||||
}),
|
||||
)
|
||||
|
||||
router.get(
|
||||
'/worker-platform/orders/mock/custom-shops',
|
||||
requireAdminRoles(['admin', 'operator', 'support']),
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
listConsignmentInventoriesByIds,
|
||||
listWorkOrderEventsByOrderId,
|
||||
listWorkOrderSharesByOrderIds,
|
||||
listWorkOrderShopNames,
|
||||
listWorkOrders,
|
||||
sumPendingUnfreezeByOrderIds,
|
||||
type ConsignmentInventoryRow,
|
||||
@@ -49,6 +50,7 @@ function buildWorkOrderStatisticsCacheKey(filters: JsonObject): string {
|
||||
filters.status || '',
|
||||
filters.workOrderId || 0,
|
||||
filters.keyword || '',
|
||||
filters.shopNames || [],
|
||||
filters.workerSharingId || 0,
|
||||
filters.workerId || 0,
|
||||
filters.workerLevelIds || [],
|
||||
@@ -92,6 +94,7 @@ export function resolveAdminWorkOrderListFilters(query: JsonObject = {}): ListIn
|
||||
workOrderId: 0,
|
||||
categoryId: 0,
|
||||
keyword,
|
||||
shopNames: [],
|
||||
workerSharingId: 0,
|
||||
workerLevelIds: [],
|
||||
vipEvidencePending: false,
|
||||
@@ -106,6 +109,8 @@ export function resolveAdminWorkOrderListFilters(query: JsonObject = {}): ListIn
|
||||
.map(normalizeOptionalId)
|
||||
.filter((levelId): levelId is number => levelId !== null)
|
||||
const statuses = normalizeStatuses(query.status)
|
||||
// 店铺来自 orders.shop_name,前端按逗号拼串传参,复用同一套数组解析(去重 + trim)。
|
||||
const shopNames = normalizeStatuses(query.shopNames ?? query.shop_names)
|
||||
const vipEvidencePending = normalizeBoolean(query.vipEvidencePending, false)
|
||||
const createdFrom = normalizeDateString(query.createdFrom)
|
||||
const createdTo = normalizeDateString(query.createdTo)
|
||||
@@ -116,6 +121,7 @@ export function resolveAdminWorkOrderListFilters(query: JsonObject = {}): ListIn
|
||||
workOrderId,
|
||||
categoryId,
|
||||
keyword,
|
||||
shopNames,
|
||||
workerSharingId: workerId,
|
||||
workerLevelIds,
|
||||
vipEvidencePending,
|
||||
@@ -182,3 +188,13 @@ export async function listAdminWorkOrders(query: JsonObject = {}) {
|
||||
statistics,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 后台工单「店铺」筛选的下拉候选。
|
||||
*
|
||||
* 固定全量去重(不随状态/日期等筛选变化),下拉里带搜索框由前端过滤即可;
|
||||
* 数据口径见 repository 的 listWorkOrderShopNames(orders.shop_name,精确匹配)。
|
||||
*/
|
||||
export async function listAdminWorkOrderShopNames(): Promise<{ items: string[] }> {
|
||||
return { items: await listWorkOrderShopNames() }
|
||||
}
|
||||
|
||||
@@ -60,6 +60,7 @@ import {
|
||||
fetchAdminWorkCategories,
|
||||
fetchAdminSession,
|
||||
fetchAdminWorkOrderSharing,
|
||||
fetchAdminWorkOrderShopOptions,
|
||||
fetchAdminWorkerLevels,
|
||||
fetchAdminWorkerUsers,
|
||||
fetchAdminWorkOrders,
|
||||
@@ -266,6 +267,7 @@ export default function WorkOrdersPanel() {
|
||||
const [createdRange, setCreatedRange] = useState<[string, string] | null>(null)
|
||||
const [keywordInput, setKeywordInput] = useState('')
|
||||
const [workerId, setWorkerId] = useState<number | undefined>()
|
||||
const [shopNames, setShopNames] = useState<string[]>([])
|
||||
const [categoryId, setCategoryId] = useState<number | undefined>()
|
||||
const [workerLevelIds, setWorkerLevelIds] = useState<number[]>([])
|
||||
const [acceptanceMode, setAcceptanceMode] = useState<string | undefined>()
|
||||
@@ -369,6 +371,7 @@ export default function WorkOrdersPanel() {
|
||||
setKeyword('')
|
||||
setKeywordInput('')
|
||||
setWorkerId(undefined)
|
||||
setShopNames([])
|
||||
setWorkerLevelIds([])
|
||||
// 定位工单是**排他**条件(wo.id = X):只留它一个,其它筛选一并清掉,
|
||||
// 否则之后按单号搜索时会被残留的接单人员/分类/日期/验收模式再次挡掉。
|
||||
@@ -395,6 +398,7 @@ export default function WorkOrdersPanel() {
|
||||
{
|
||||
statuses: statuses.join(','),
|
||||
keyword,
|
||||
shopNames: shopNames.join(','),
|
||||
workerId,
|
||||
categoryId,
|
||||
workerLevelIds: workerLevelIds.join(','),
|
||||
@@ -417,6 +421,7 @@ export default function WorkOrdersPanel() {
|
||||
workOrderId: targetWorkOrderId,
|
||||
categoryId,
|
||||
keyword,
|
||||
shopNames: shopNames.length > 0 ? shopNames.join(',') : undefined,
|
||||
workerId,
|
||||
workerLevelIds:
|
||||
showWorkerLevelFilter && workerLevelIds.length > 0 ? workerLevelIds.join(',') : undefined,
|
||||
@@ -490,6 +495,12 @@ export default function WorkOrdersPanel() {
|
||||
queryFn: () => fetchAdminWorkerLevels(),
|
||||
enabled: showWorkerLevelFilter,
|
||||
})
|
||||
// 店铺候选固定全量(不随其它筛选变化),下拉本身带搜索框做本地过滤。
|
||||
const shopOptionsQuery = useQuery({
|
||||
queryKey: ['admin-worker-platform-order-shop-options'],
|
||||
queryFn: fetchAdminWorkOrderShopOptions,
|
||||
staleTime: 60_000,
|
||||
})
|
||||
const filterWorkersQuery = useQuery({
|
||||
queryKey: ['admin-worker-platform-workers', 'order-filter', filterWorkerKeyword],
|
||||
queryFn: () =>
|
||||
@@ -713,6 +724,9 @@ export default function WorkOrdersPanel() {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ['admin-worker-platform-orders'],
|
||||
}),
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ['admin-worker-platform-order-shop-options'],
|
||||
}),
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ['admin-worker-platform-summary'],
|
||||
}),
|
||||
@@ -1095,6 +1109,10 @@ export default function WorkOrdersPanel() {
|
||||
setFilterWorkerKeyword('')
|
||||
cleared.push('接单人员')
|
||||
}
|
||||
if (shopNames.length > 0) {
|
||||
setShopNames([])
|
||||
cleared.push('店铺')
|
||||
}
|
||||
if (categoryId) {
|
||||
setCategoryId(undefined)
|
||||
cleared.push('分类')
|
||||
@@ -1130,6 +1148,7 @@ export default function WorkOrdersPanel() {
|
||||
setKeyword('')
|
||||
setStatuses([])
|
||||
setWorkerId(undefined)
|
||||
setShopNames([])
|
||||
setCategoryId(undefined)
|
||||
setWorkerLevelIds([])
|
||||
setAcceptanceMode(undefined)
|
||||
@@ -1841,12 +1860,12 @@ export default function WorkOrdersPanel() {
|
||||
) : null}
|
||||
<Card title="订单搜索与筛选" bordered={false}>
|
||||
<div className="worker-orders-toolbar">
|
||||
<Space wrap className="filter-form worker-orders-search-controls">
|
||||
<div className="worker-orders-filter-bar">
|
||||
<Input
|
||||
allowClear
|
||||
value={keywordInput}
|
||||
placeholder="工单号 / 平台单号 / 寄卖单号 / 商品名 / 游戏ID / 游戏昵称"
|
||||
style={{ width: 280 }}
|
||||
placeholder="工单号 / 平台单号 / 商品名 / 游戏ID / 昵称"
|
||||
className="worker-orders-search-input"
|
||||
onChange={(event) => {
|
||||
const nextVal = event.target.value
|
||||
setKeywordInput(nextVal)
|
||||
@@ -1857,12 +1876,31 @@ export default function WorkOrdersPanel() {
|
||||
}}
|
||||
onPressEnter={searchOrders}
|
||||
/>
|
||||
<Select
|
||||
mode="multiple"
|
||||
allowClear
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
value={shopNames}
|
||||
placeholder="全部店铺"
|
||||
className="worker-orders-filter-select worker-orders-filter-select--wide"
|
||||
maxTagCount="responsive"
|
||||
loading={shopOptionsQuery.isLoading}
|
||||
options={(shopOptionsQuery.data?.data.items || []).map((shopName) => ({
|
||||
value: shopName,
|
||||
label: shopName,
|
||||
}))}
|
||||
onChange={(nextShopNames: string[]) => {
|
||||
setShopNames(nextShopNames)
|
||||
setPage(1)
|
||||
}}
|
||||
/>
|
||||
<Select
|
||||
mode="multiple"
|
||||
allowClear
|
||||
value={statuses}
|
||||
placeholder="全部状态(可多选)"
|
||||
style={{ minWidth: 220, maxWidth: 420 }}
|
||||
placeholder="全部状态"
|
||||
className="worker-orders-filter-select worker-orders-filter-select--compact"
|
||||
maxTagCount="responsive"
|
||||
listHeight={320}
|
||||
options={FILTERABLE_WORK_ORDER_STATUSES.map((item) => ({
|
||||
@@ -1879,8 +1917,8 @@ export default function WorkOrdersPanel() {
|
||||
showSearch
|
||||
filterOption={false}
|
||||
value={workerId}
|
||||
placeholder="全部接单人员"
|
||||
style={{ width: 180 }}
|
||||
placeholder="接单人员"
|
||||
className="worker-orders-filter-select"
|
||||
loading={filterWorkersQuery.isLoading}
|
||||
onSearch={setFilterWorkerKeyword}
|
||||
options={(filterWorkersQuery.data?.data.items || []).map((worker) => ({
|
||||
@@ -1901,8 +1939,8 @@ export default function WorkOrdersPanel() {
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
value={categoryId}
|
||||
placeholder="全部接单分类"
|
||||
style={{ width: 180 }}
|
||||
placeholder="接单分类"
|
||||
className="worker-orders-filter-select"
|
||||
loading={categoriesQuery.isLoading}
|
||||
options={categoryOptions}
|
||||
onChange={(nextCategoryId) => {
|
||||
@@ -1915,8 +1953,8 @@ export default function WorkOrdersPanel() {
|
||||
mode="multiple"
|
||||
allowClear
|
||||
value={workerLevelIds}
|
||||
placeholder="全部打手 VIP 等级"
|
||||
style={{ minWidth: 180, maxWidth: 320 }}
|
||||
placeholder="VIP 等级"
|
||||
className="worker-orders-filter-select"
|
||||
maxTagCount="responsive"
|
||||
loading={workerLevelsQuery.isLoading}
|
||||
options={(workerLevelsQuery.data?.data.items || []).map((level) => ({
|
||||
@@ -1932,8 +1970,8 @@ export default function WorkOrdersPanel() {
|
||||
<Select
|
||||
allowClear
|
||||
value={acceptanceMode}
|
||||
placeholder="全部验收模式"
|
||||
style={{ width: 160 }}
|
||||
placeholder="验收模式"
|
||||
className="worker-orders-filter-select"
|
||||
options={[
|
||||
{ value: 'standard', label: '标准验收' },
|
||||
{ value: 'friend_gift', label: '打手好友赠送' },
|
||||
@@ -1948,8 +1986,8 @@ export default function WorkOrdersPanel() {
|
||||
<Select
|
||||
allowClear
|
||||
value={giftPhase}
|
||||
placeholder="全部赠送阶段"
|
||||
style={{ width: 170 }}
|
||||
placeholder="赠送阶段"
|
||||
className="worker-orders-filter-select"
|
||||
options={GIFT_PHASE_FILTER_OPTIONS.map((phase) => ({
|
||||
value: phase,
|
||||
label: (
|
||||
@@ -1965,6 +2003,7 @@ export default function WorkOrdersPanel() {
|
||||
/>
|
||||
) : null}
|
||||
<AdminRangePicker
|
||||
className="worker-orders-date-range"
|
||||
value={createdRange ? [dayjs(createdRange[0]), dayjs(createdRange[1])] : null}
|
||||
onChange={(dates) => {
|
||||
const [start, end] = dates || []
|
||||
@@ -1980,7 +2019,7 @@ export default function WorkOrdersPanel() {
|
||||
查询
|
||||
</Button>
|
||||
<Button onClick={clearOrderFilters}>清空筛选</Button>
|
||||
</Space>
|
||||
</div>
|
||||
<div className="worker-orders-mock">
|
||||
<Typography.Text type="secondary">本地 Mock</Typography.Text>
|
||||
<MockWorkOrderForm onCreated={refreshAll} />
|
||||
|
||||
@@ -638,6 +638,11 @@ export function fetchAdminWorkOrders(params?: Record<string, unknown>) {
|
||||
>('/api/v1/admin/worker-platform/orders', params)
|
||||
}
|
||||
|
||||
/** 工单「店铺」筛选候选:订单归属店铺(orders.shop_name)全量去重。 */
|
||||
export function fetchAdminWorkOrderShopOptions() {
|
||||
return apiGet<{ items: string[] }>('/api/v1/admin/worker-platform/orders/shop-options')
|
||||
}
|
||||
|
||||
export function fetchAdminAfterSalesCases(params?: Record<string, unknown>) {
|
||||
return apiGet<WorkerListResponse<WorkerAfterSalesCase>>(
|
||||
'/api/v1/admin/worker-platform/after-sales-cases',
|
||||
|
||||
@@ -121,8 +121,49 @@
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.worker-orders-search-controls {
|
||||
/* 单行筛选栏:搜索框、筛选条件、清空按钮同一行且不换行;空间不足时行内横向滚动 */
|
||||
.worker-orders-filter-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: nowrap;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
scrollbar-width: thin;
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
|
||||
.worker-orders-search-input {
|
||||
flex: 1.4 1 240px;
|
||||
min-width: 180px;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.worker-orders-filter-select {
|
||||
flex: 1 1 110px;
|
||||
min-width: 88px;
|
||||
}
|
||||
|
||||
.worker-orders-filter-select--wide {
|
||||
flex: 1.6 1 140px;
|
||||
min-width: 112px;
|
||||
}
|
||||
|
||||
/* 状态:多选标签可截断,宽度让给时间区间 */
|
||||
.worker-orders-filter-select--compact {
|
||||
flex: 0.9 1 96px;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
/* 时间区间:留足两个完整日期(YYYY-MM-DD)与「开始日期/结束日期」占位文案的宽度 */
|
||||
.worker-orders-date-range {
|
||||
flex: 0 0 230px;
|
||||
width: 230px;
|
||||
min-width: 230px;
|
||||
}
|
||||
|
||||
.worker-orders-filter-bar > .ant-btn {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.worker-orders-mock {
|
||||
|
||||
@@ -23,6 +23,8 @@ import dayjs from 'dayjs'
|
||||
export type AdminWorkOrderListFilters = {
|
||||
statuses: string[]
|
||||
keyword: string
|
||||
/** 店铺筛选(订单归属店铺,可多选精确匹配)。 */
|
||||
shopNames: string[]
|
||||
workerId: number
|
||||
categoryId: number
|
||||
workerLevelIds: number[]
|
||||
@@ -69,6 +71,10 @@ export function parseAdminWorkOrderListFilters(
|
||||
.map((value) => value.trim())
|
||||
.filter(Boolean),
|
||||
keyword: String(raw.keyword || '').trim(),
|
||||
shopNames: String(raw.shopNames || '')
|
||||
.split(',')
|
||||
.map((value) => value.trim())
|
||||
.filter(Boolean),
|
||||
workerId: toNumber(raw.workerId),
|
||||
categoryId: toNumber(raw.categoryId),
|
||||
workerLevelIds: String(raw.workerLevelIds || '')
|
||||
@@ -211,10 +217,13 @@ export function evaluateAdminWorkOrderRealtimeMatch(
|
||||
// 还有平台单号、工单号的特殊匹配分支;
|
||||
// - 赠送阶段:gift_ready 是"冷却到点"的实时阶段,含 NOW() 判定;
|
||||
// - 打手等级:只在服务端按接单人与拼单参与者计算;
|
||||
// - 店铺:口径是订单归属店铺(orders.shop_name),快照里只有工单 material 的货源店铺名,
|
||||
// 两者不可互换,无法证伪;
|
||||
// - VIP 待补图:混在状态列表里的虚拟值,判定依赖 acceptance_json 多个字段与 NOW()。
|
||||
// 注:显式单号检索已在函数开头短路,走不到这里。
|
||||
if (filters.acceptanceMode) return 'unknown'
|
||||
if (vipEvidencePendingActive) return 'unknown'
|
||||
if (filters.shopNames.length > 0) return 'unknown'
|
||||
if (filters.keyword) return 'unknown'
|
||||
if (filters.giftPhase) return 'unknown'
|
||||
if (filters.workerLevelIds.length > 0) return 'unknown'
|
||||
|
||||
@@ -16,6 +16,7 @@ function buildQueryKey(overrides: Partial<AdminWorkOrderListFilters> = {}) {
|
||||
const filters: AdminWorkOrderListFilters = {
|
||||
statuses: '',
|
||||
keyword: '',
|
||||
shopNames: '',
|
||||
workerId: 0,
|
||||
categoryId: 0,
|
||||
workerLevelIds: '',
|
||||
@@ -32,6 +33,7 @@ function buildQueryKey(overrides: Partial<AdminWorkOrderListFilters> = {}) {
|
||||
{
|
||||
statuses: filters.statuses,
|
||||
keyword: filters.keyword,
|
||||
shopNames: filters.shopNames,
|
||||
workerId: filters.workerId,
|
||||
categoryId: filters.categoryId,
|
||||
workerLevelIds: filters.workerLevelIds,
|
||||
|
||||
Reference in New Issue
Block a user