Merge Profile0928 asset analysis and permission previews into main
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
shishengliang
2026-09-28 12:34:01 +08:00
74 changed files with 3922 additions and 545 deletions
+17 -78
View File
@@ -24,16 +24,14 @@ import {
Cell,
LabelList,
} from 'recharts';
import type { SummaryData, TypeSummary, VehicleListItem, DeptGroup, RegionGroup, CustomerStats, RegionalInventoryStats } from './types';
import { fetchSummary, fetchByType, fetchVehicleList, fetchWeeklyDetail, fetchDeptStats, fetchRegionStats, fetchCustomerStats, fetchInventoryStats, fetchRegionChart, fetchSubjects, fetchFlowStats, type SubjectOption } from './api';
import type { FlowDetailItem, FlowStatsResponse, FlowType, WeeklyDetailItem } from './api';
import type { SummaryData, TypeSummary, DeptGroup, RegionGroup, CustomerStats, RegionalInventoryStats } from './types';
import { fetchSummary, fetchByType, fetchDeptStats, fetchRegionStats, fetchCustomerStats, fetchInventoryStats, fetchRegionChart, fetchSubjects, fetchFlowStats, type SubjectOption } from './api';
import type { FlowDetailItem, FlowStatsResponse, FlowType } from './api';
import {
buildCustomerPieData,
buildVehicleModalRequest,
deriveCustomerView,
deriveDepartmentView,
deriveInventoryView,
deriveModalVehicleView,
formatLocalDateTime,
getRecentFlowRange,
selectFlowDetails,
@@ -44,8 +42,8 @@ import { SearchSelect } from '../../components/SearchSelect';
import { MultiSearchSelect } from '../../components/MultiSearchSelect';
import { ErrorState, LoadingState, PageFrame, SkeletonBlock, SurfaceCard } from '../../components/ui/surface';
import { AssetsHeader } from './components/AssetsHeader';
import { FlowDetailModal } from './components/FlowDetailModal';
import { VehicleDetailModal } from './components/VehicleDetailModal';
import { FlowAnalysisModal } from './components/FlowAnalysisModal';
import { AssetAnalysisModal } from './components/analysis/AssetAnalysisModal';
import { OverviewView } from './components/OverviewView';
@@ -67,7 +65,7 @@ export default function AssetsModule() {
useEffect(() => {
if (!assistantOpen) return;
const previousTitle = document.title;
document.title = 'AI 问数 - 资产BI';
document.title = 'AI问数 - 资产BI';
return () => { document.title = previousTitle; };
}, [assistantOpen]);
const openAssistant = () => {
@@ -109,16 +107,14 @@ export default function AssetsModule() {
const [expandedModels, setExpandedModels] = useState<Set<string>>(new Set());
const [expandedAssetTypes, setExpandedAssetTypes] = useState<Set<string>>(new Set());
const [showPlateNumbers, setShowPlateNumbers] = useState<VehicleModalSelection | null>(null);
const closeAssetAnalysis = useCallback(() => setShowPlateNumbers(null), []);
// Data state
const [summary, setSummary] = useState<SummaryData | null>(null);
const [processedData, setProcessedData] = useState<TypeSummary[]>([]);
const [modalVehicles, setModalVehicles] = useState<VehicleListItem[]>([]);
const [modalWeeklyDetail, setModalWeeklyDetail] = useState<WeeklyDetailItem[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [lastUpdate, setLastUpdate] = useState<string>(() => formatLocalDateTime(new Date()));
const [modalLoading, setModalLoading] = useState(false);
const [flowRange, setFlowRange] = useState(() => getRecentFlowRange());
const [flowStats, setFlowStats] = useState<FlowStatsResponse | null>(null);
const [flowLoading, setFlowLoading] = useState(false);
@@ -163,17 +159,6 @@ export default function AssetsModule() {
const [regionChartView, setRegionChartView] = useState<'region' | 'province'>('region');
const [regionChartData, setRegionChartData] = useState<{ name: string; value: number }[]>([]);
// Modal filter state
const [modalFilters, setModalFilters] = useState({ plateNumber: '', model: '', brand: '', location: '' });
const [isModalFilterExpanded, setIsModalFilterExpanded] = useState(false);
// Reset modal filters when modal opens
useEffect(() => {
if (showPlateNumbers) {
setModalFilters({ plateNumber: '', model: '', brand: '', location: '' });
}
}, [showPlateNumbers]);
const loadData = useCallback(async () => {
try {
setLoading(true);
@@ -262,30 +247,7 @@ export default function AssetsModule() {
.catch(() => setRegionChartData([]));
}, [regionChartView, selectedSubject]);
// Load modal vehicles
useEffect(() => {
if (!showPlateNumbers) {
setModalVehicles([]);
setModalWeeklyDetail([]);
return;
}
setModalLoading(true);
const request = buildVehicleModalRequest(showPlateNumbers, selectedSubject);
if (request.kind === 'weekly') {
setModalVehicles([]);
fetchWeeklyDetail(request.type, { ...request.filters, subject: selectedSubject })
.then(setModalWeeklyDetail)
.catch(() => setModalWeeklyDetail([]))
.finally(() => setModalLoading(false));
return;
}
setModalWeeklyDetail([]);
fetchVehicleList(request.params)
.then(setModalVehicles)
.catch(() => setModalVehicles([]))
.finally(() => setModalLoading(false));
}, [showPlateNumbers, selectedSubject]);
// Vehicle records are requested only at the final level of AssetAnalysisModal.
const allTypesExpanded = processedData.length > 0 && processedData.every((t) => expandedAssetTypes.has(t.type));
@@ -403,18 +365,6 @@ export default function AssetsModule() {
[customerData, deptData, customerFilters],
);
const {
plates: uniqueModalPlates,
models: uniqueModalModels,
brands: uniqueModalBrands,
locations: uniqueModalLocations,
filteredVehicles: filteredModalVehicles,
filteredWeeklyDetails: filteredModalWeeklyDetail,
} = useMemo(
() => deriveModalVehicleView(modalVehicles, modalWeeklyDetail, modalFilters),
[modalVehicles, modalWeeklyDetail, modalFilters],
);
const selectedFlowDetails = useMemo(
() => selectFlowDetails(flowStats, selectedFlow),
[flowStats, selectedFlow],
@@ -461,7 +411,7 @@ export default function AssetsModule() {
if (assistantOpen) {
return (
<Suspense fallback={<div role="status" className="min-h-screen bg-[var(--app-bg)] p-6 text-slate-700">正在加载 AI 问数页面…</div>}>
<Suspense fallback={<div role="status" className="min-h-screen bg-[var(--app-bg)] p-6 text-slate-700">正在加载AI问数页面…</div>}>
<AssetAssistant key={selectedSubject || 'all'} subject={selectedSubject} appearance="light" close={closeAssistant} />
</Suspense>
);
@@ -1445,7 +1395,7 @@ export default function AssetsModule() {
<div className="space-y-1.5">
<label className="text-[10px] font-bold text-gray-400 uppercase tracking-wider">业务负责人</label>
<select className="w-full bg-white border border-gray-200 rounded-lg py-2 px-2 text-xs focus:ring-2 focus:ring-emerald-500/20 focus:border-emerald-500 outline-none transition-all cursor-pointer shadow-sm" value={draftCustomerFilters.manager} onChange={(e) => setDraftCustomerFilters(prev => ({ ...prev, manager: e.target.value }))}>
<option value="">所有负责人</option>
<option value="">所有业务负责人</option>
{customerManagersGroupedByDept
.filter(g => g.department !== '公务车')
.filter(g => !draftCustomerFilters.department || g.department === draftCustomerFilters.department)
@@ -1499,7 +1449,7 @@ export default function AssetsModule() {
)}
{customerFilters.manager && (
<span className="px-2 py-0.5 bg-emerald-50 text-emerald-700 rounded text-[10px] flex items-center gap-1">
负责人: {customerFilters.manager}
业务负责人: {customerFilters.manager}
<button onClick={() => setCustomerFilters(prev => ({...prev, manager: ''}))} className="hover:text-red-500 ml-0.5">×</button>
</span>
)}
@@ -1717,28 +1667,17 @@ export default function AssetsModule() {
)}
<FlowDetailModal
<FlowAnalysisModal
selectedFlow={selectedFlow}
selectedFlowDetails={selectedFlowDetails}
setSelectedFlow={setSelectedFlow}
exportFlowDetails={exportFlowDetails}
/>
<VehicleDetailModal
<AssetAnalysisModal
selection={showPlateNumbers}
setSelection={setShowPlateNumbers}
isFilterExpanded={isModalFilterExpanded}
setIsFilterExpanded={setIsModalFilterExpanded}
filters={modalFilters}
setFilters={setModalFilters}
plates={uniqueModalPlates}
models={uniqueModalModels}
brands={uniqueModalBrands}
locations={uniqueModalLocations}
loading={modalLoading}
weeklyDetails={modalWeeklyDetail}
filteredWeeklyDetails={filteredModalWeeklyDetail}
filteredVehicles={filteredModalVehicles}
subject={selectedSubject}
close={closeAssetAnalysis}
/>
</div>
@@ -1746,11 +1685,11 @@ export default function AssetsModule() {
ref={assistantTrigger}
type="button"
onClick={openAssistant}
aria-label="进入 AI 问数页面"
aria-label="进入AI问数页面"
className="fixed bottom-[calc(5rem+env(safe-area-inset-bottom))] right-4 z-[60] inline-flex min-h-12 items-center gap-2 rounded-full bg-gradient-to-br from-blue-600 to-cyan-500 px-4 text-sm font-semibold text-white shadow-lg shadow-blue-200 transition hover:from-blue-700 hover:to-cyan-600 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 md:bottom-6 md:right-6"
>
<Sparkles size={20} aria-hidden="true" />
AI 问数
AI问数
</button>
</div>
);