feat(platform): filter vehicles by archive status
This commit is contained in:
@@ -102,6 +102,15 @@ func buildVehicleCoverageSQL(query url.Values) SQLQuery {
|
|||||||
case "unbound":
|
case "unbound":
|
||||||
having = append(having, "MAX(CASE WHEN b.vin IS NOT NULL AND b.vin <> '' THEN 1 ELSE 0 END) = 0")
|
having = append(having, "MAX(CASE WHEN b.vin IS NOT NULL AND b.vin <> '' THEN 1 ELSE 0 END) = 0")
|
||||||
}
|
}
|
||||||
|
switch strings.TrimSpace(query.Get("archiveStatus")) {
|
||||||
|
case "complete":
|
||||||
|
having = append(having, "v.vin IS NOT NULL AND v.vin <> ''")
|
||||||
|
having = append(having, "COALESCE(NULLIF(MAX(NULLIF(s.plate, '')), ''), NULLIF(MAX(NULLIF(b.plate, '')), '')) IS NOT NULL")
|
||||||
|
having = append(having, "NULLIF(MAX(NULLIF(b.phone, '')), '') IS NOT NULL")
|
||||||
|
having = append(having, "NULLIF(MAX(NULLIF(b.oem, '')), '') IS NOT NULL")
|
||||||
|
case "incomplete":
|
||||||
|
having = append(having, "(v.vin IS NULL OR v.vin = '' OR COALESCE(NULLIF(MAX(NULLIF(s.plate, '')), ''), NULLIF(MAX(NULLIF(b.plate, '')), '')) IS NULL OR NULLIF(MAX(NULLIF(b.phone, '')), '') IS NULL OR NULLIF(MAX(NULLIF(b.oem, '')), '') IS NULL)")
|
||||||
|
}
|
||||||
switch strings.TrimSpace(query.Get("serviceStatus")) {
|
switch strings.TrimSpace(query.Get("serviceStatus")) {
|
||||||
case "identity_required":
|
case "identity_required":
|
||||||
having = append(having, "MAX(CASE WHEN b.vin IS NOT NULL AND b.vin <> '' THEN 1 ELSE 0 END) = 0")
|
having = append(having, "MAX(CASE WHEN b.vin IS NOT NULL AND b.vin <> '' THEN 1 ELSE 0 END) = 0")
|
||||||
@@ -191,6 +200,15 @@ func buildVehicleCoverageSummarySQL(query url.Values) SQLQuery {
|
|||||||
case "unbound":
|
case "unbound":
|
||||||
having = append(having, "MAX(CASE WHEN b.vin IS NOT NULL AND b.vin <> '' THEN 1 ELSE 0 END) = 0")
|
having = append(having, "MAX(CASE WHEN b.vin IS NOT NULL AND b.vin <> '' THEN 1 ELSE 0 END) = 0")
|
||||||
}
|
}
|
||||||
|
switch strings.TrimSpace(query.Get("archiveStatus")) {
|
||||||
|
case "complete":
|
||||||
|
having = append(having, "v.vin IS NOT NULL AND v.vin <> ''")
|
||||||
|
having = append(having, "COALESCE(NULLIF(MAX(NULLIF(s.plate, '')), ''), NULLIF(MAX(NULLIF(b.plate, '')), '')) IS NOT NULL")
|
||||||
|
having = append(having, "NULLIF(MAX(NULLIF(b.phone, '')), '') IS NOT NULL")
|
||||||
|
having = append(having, "NULLIF(MAX(NULLIF(b.oem, '')), '') IS NOT NULL")
|
||||||
|
case "incomplete":
|
||||||
|
having = append(having, "(v.vin IS NULL OR v.vin = '' OR COALESCE(NULLIF(MAX(NULLIF(s.plate, '')), ''), NULLIF(MAX(NULLIF(b.plate, '')), '')) IS NULL OR NULLIF(MAX(NULLIF(b.phone, '')), '') IS NULL OR NULLIF(MAX(NULLIF(b.oem, '')), '') IS NULL)")
|
||||||
|
}
|
||||||
switch strings.TrimSpace(query.Get("serviceStatus")) {
|
switch strings.TrimSpace(query.Get("serviceStatus")) {
|
||||||
case "identity_required":
|
case "identity_required":
|
||||||
having = append(having, "MAX(CASE WHEN b.vin IS NOT NULL AND b.vin <> '' THEN 1 ELSE 0 END) = 0")
|
having = append(having, "MAX(CASE WHEN b.vin IS NOT NULL AND b.vin <> '' THEN 1 ELSE 0 END) = 0")
|
||||||
|
|||||||
@@ -117,6 +117,22 @@ func TestBuildVehicleCoverageSQLFiltersMissingProtocol(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBuildVehicleCoverageSQLFiltersArchiveStatus(t *testing.T) {
|
||||||
|
query := url.Values{"archiveStatus": {"incomplete"}, "limit": {"8"}}
|
||||||
|
built := buildVehicleCoverageSQL(query)
|
||||||
|
for _, want := range []string{
|
||||||
|
"HAVING",
|
||||||
|
"COALESCE(NULLIF(MAX(NULLIF(s.plate, '')), ''), NULLIF(MAX(NULLIF(b.plate, '')), '')) IS NULL",
|
||||||
|
"NULLIF(MAX(NULLIF(b.phone, '')), '') IS NULL",
|
||||||
|
"NULLIF(MAX(NULLIF(b.oem, '')), '') IS NULL",
|
||||||
|
"vehicle_coverage_count",
|
||||||
|
} {
|
||||||
|
if !strings.Contains(built.Text+built.CountText, want) {
|
||||||
|
t.Fatalf("archive status SQL missing %q: %s / %s", want, built.Text, built.CountText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestBuildVehicleCoverageSummarySQL(t *testing.T) {
|
func TestBuildVehicleCoverageSummarySQL(t *testing.T) {
|
||||||
query := url.Values{"keyword": {"粤A"}, "coverage": {"multi"}, "online": {"online"}, "bindingStatus": {"bound"}, "serviceStatus": {"healthy"}}
|
query := url.Values{"keyword": {"粤A"}, "coverage": {"multi"}, "online": {"online"}, "bindingStatus": {"bound"}, "serviceStatus": {"healthy"}}
|
||||||
built := buildVehicleCoverageSummarySQL(query)
|
built := buildVehicleCoverageSummarySQL(query)
|
||||||
|
|||||||
@@ -23,13 +23,14 @@ describe('parseAppHash', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('parses vehicle list filters from hash query', () => {
|
test('parses vehicle list filters from hash query', () => {
|
||||||
expect(parseAppHash('#/vehicles?coverage=multi&serviceStatus=degraded&online=online&bindingStatus=bound&missingProtocol=YUTONG_MQTT')).toEqual({
|
expect(parseAppHash('#/vehicles?coverage=multi&serviceStatus=degraded&online=online&bindingStatus=bound&archiveStatus=incomplete&missingProtocol=YUTONG_MQTT')).toEqual({
|
||||||
page: 'vehicles',
|
page: 'vehicles',
|
||||||
filters: {
|
filters: {
|
||||||
coverage: 'multi',
|
coverage: 'multi',
|
||||||
serviceStatus: 'degraded',
|
serviceStatus: 'degraded',
|
||||||
online: 'online',
|
online: 'online',
|
||||||
bindingStatus: 'bound',
|
bindingStatus: 'bound',
|
||||||
|
archiveStatus: 'incomplete',
|
||||||
missingProtocol: 'YUTONG_MQTT'
|
missingProtocol: 'YUTONG_MQTT'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -61,7 +62,7 @@ describe('buildAppHash', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('builds shareable vehicle list hash with filters', () => {
|
test('builds shareable vehicle list hash with filters', () => {
|
||||||
expect(buildAppHash({ page: 'vehicles', filters: { coverage: 'multi', serviceStatus: 'degraded', missingProtocol: 'YUTONG_MQTT' } })).toBe('#/vehicles?coverage=multi&serviceStatus=degraded&missingProtocol=YUTONG_MQTT');
|
expect(buildAppHash({ page: 'vehicles', filters: { coverage: 'multi', serviceStatus: 'degraded', archiveStatus: 'incomplete', missingProtocol: 'YUTONG_MQTT' } })).toBe('#/vehicles?coverage=multi&serviceStatus=degraded&archiveStatus=incomplete&missingProtocol=YUTONG_MQTT');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('builds shareable quality hash with filters', () => {
|
test('builds shareable quality hash with filters', () => {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ const filterKeys = [
|
|||||||
'serviceStatus',
|
'serviceStatus',
|
||||||
'online',
|
'online',
|
||||||
'bindingStatus',
|
'bindingStatus',
|
||||||
|
'archiveStatus',
|
||||||
'missingProtocol',
|
'missingProtocol',
|
||||||
'issueType',
|
'issueType',
|
||||||
'tab',
|
'tab',
|
||||||
|
|||||||
@@ -63,6 +63,11 @@ const bindingStatusLabel: Record<string, string> = {
|
|||||||
unbound: '未绑定'
|
unbound: '未绑定'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const archiveStatusLabel: Record<string, string> = {
|
||||||
|
complete: '完整',
|
||||||
|
incomplete: '不完整'
|
||||||
|
};
|
||||||
|
|
||||||
function sourceDiagnosisText(row: VehicleCoverageRow) {
|
function sourceDiagnosisText(row: VehicleCoverageRow) {
|
||||||
const parts = [sourceEvidenceText(row)];
|
const parts = [sourceEvidenceText(row)];
|
||||||
if ((row.missingProtocols ?? []).length > 0) {
|
if ((row.missingProtocols ?? []).length > 0) {
|
||||||
@@ -189,7 +194,8 @@ export function Vehicles({
|
|||||||
filters.missingProtocol ? `缺失来源:${filters.missingProtocol}` : '',
|
filters.missingProtocol ? `缺失来源:${filters.missingProtocol}` : '',
|
||||||
filters.serviceStatus ? `服务状态:${serviceStatusLabel[filters.serviceStatus] ?? filters.serviceStatus}` : '',
|
filters.serviceStatus ? `服务状态:${serviceStatusLabel[filters.serviceStatus] ?? filters.serviceStatus}` : '',
|
||||||
filters.online ? `在线:${onlineLabel[filters.online] ?? filters.online}` : '',
|
filters.online ? `在线:${onlineLabel[filters.online] ?? filters.online}` : '',
|
||||||
filters.bindingStatus ? `绑定:${bindingStatusLabel[filters.bindingStatus] ?? filters.bindingStatus}` : ''
|
filters.bindingStatus ? `绑定:${bindingStatusLabel[filters.bindingStatus] ?? filters.bindingStatus}` : '',
|
||||||
|
filters.archiveStatus ? `档案:${archiveStatusLabel[filters.archiveStatus] ?? filters.archiveStatus}` : ''
|
||||||
].filter(Boolean);
|
].filter(Boolean);
|
||||||
|
|
||||||
const load = (values: Record<string, string> = filters, page = pagination.currentPage, pageSize = pagination.pageSize) => {
|
const load = (values: Record<string, string> = filters, page = pagination.currentPage, pageSize = pagination.pageSize) => {
|
||||||
@@ -201,6 +207,7 @@ export function Vehicles({
|
|||||||
if (values?.missingProtocol) params.set('missingProtocol', values.missingProtocol);
|
if (values?.missingProtocol) params.set('missingProtocol', values.missingProtocol);
|
||||||
if (values?.online) params.set('online', values.online);
|
if (values?.online) params.set('online', values.online);
|
||||||
if (values?.bindingStatus) params.set('bindingStatus', values.bindingStatus);
|
if (values?.bindingStatus) params.set('bindingStatus', values.bindingStatus);
|
||||||
|
if (values?.archiveStatus) params.set('archiveStatus', values.archiveStatus);
|
||||||
if (values?.serviceStatus) params.set('serviceStatus', values.serviceStatus);
|
if (values?.serviceStatus) params.set('serviceStatus', values.serviceStatus);
|
||||||
const summaryParams = new URLSearchParams(params);
|
const summaryParams = new URLSearchParams(params);
|
||||||
summaryParams.delete('limit');
|
summaryParams.delete('limit');
|
||||||
@@ -344,6 +351,10 @@ export function Vehicles({
|
|||||||
<Select.Option value="bound">已绑定</Select.Option>
|
<Select.Option value="bound">已绑定</Select.Option>
|
||||||
<Select.Option value="unbound">未绑定</Select.Option>
|
<Select.Option value="unbound">未绑定</Select.Option>
|
||||||
</Form.Select>
|
</Form.Select>
|
||||||
|
<Form.Select field="archiveStatus" label="档案" placeholder="全部" style={{ width: 130 }}>
|
||||||
|
<Select.Option value="complete">完整</Select.Option>
|
||||||
|
<Select.Option value="incomplete">不完整</Select.Option>
|
||||||
|
</Form.Select>
|
||||||
<Space>
|
<Space>
|
||||||
<Button htmlType="submit" theme="solid" type="primary">查询</Button>
|
<Button htmlType="submit" theme="solid" type="primary">查询</Button>
|
||||||
<Button icon={<IconCopy />} onClick={copyVehicleShareURL}>复制筛选链接</Button>
|
<Button icon={<IconCopy />} onClick={copyVehicleShareURL}>复制筛选链接</Button>
|
||||||
|
|||||||
@@ -806,7 +806,7 @@ test('filters vehicle list from action queue', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('shows and clears current vehicle service filters', async () => {
|
test('shows and clears current vehicle service filters', async () => {
|
||||||
window.history.replaceState(null, '', '/#/vehicles?keyword=%E7%B2%A4A&protocol=JT808&coverage=multi&missingProtocol=YUTONG_MQTT&serviceStatus=degraded&online=online&bindingStatus=bound');
|
window.history.replaceState(null, '', '/#/vehicles?keyword=%E7%B2%A4A&protocol=JT808&coverage=multi&missingProtocol=YUTONG_MQTT&serviceStatus=degraded&online=online&bindingStatus=bound&archiveStatus=incomplete');
|
||||||
const fetchMock = vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => {
|
const fetchMock = vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => {
|
||||||
const path = String(input);
|
const path = String(input);
|
||||||
if (path.includes('/api/ops/health')) {
|
if (path.includes('/api/ops/health')) {
|
||||||
@@ -866,6 +866,10 @@ test('shows and clears current vehicle service filters', async () => {
|
|||||||
expect(screen.getByText('服务状态:来源不完整')).toBeInTheDocument();
|
expect(screen.getByText('服务状态:来源不完整')).toBeInTheDocument();
|
||||||
expect(screen.getByText('在线:在线')).toBeInTheDocument();
|
expect(screen.getByText('在线:在线')).toBeInTheDocument();
|
||||||
expect(screen.getByText('绑定:已绑定')).toBeInTheDocument();
|
expect(screen.getByText('绑定:已绑定')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('档案:不完整')).toBeInTheDocument();
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('archiveStatus=incomplete'), undefined);
|
||||||
|
});
|
||||||
fireEvent.click(screen.getByRole('button', { name: '清空筛选' }));
|
fireEvent.click(screen.getByRole('button', { name: '清空筛选' }));
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user