From 04e7422438db328c5130569f73159ac485e0e380 Mon Sep 17 00:00:00 2001 From: kangwenjing <1138819403@qq.com> Date: Thu, 16 Apr 2026 10:55:37 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8E=A7=E5=88=B6=E9=A6=96=E9=A1=B5=E5=B1=95?= =?UTF-8?q?=E7=A4=BA=E7=9A=84=E6=9D=83=E9=99=90=E6=B7=BB=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/dashboard/DashboardActivityDTO.java | 9 + .../crm/dto/dashboard/DashboardHomeDTO.java | 21 ++ .../com/unis/crm/mapper/DashboardMapper.java | 9 +- .../service/impl/DashboardServiceImpl.java | 55 +++- .../mapper/dashboard/DashboardMapper.xml | 279 +++++++++++------- frontend/src/lib/auth.ts | 8 + frontend/src/pages/Dashboard.tsx | 58 +++- frontend/src/pages/Expansion.tsx | 33 ++- frontend/src/pages/Opportunities.tsx | 24 ++ 9 files changed, 376 insertions(+), 120 deletions(-) diff --git a/backend/src/main/java/com/unis/crm/dto/dashboard/DashboardActivityDTO.java b/backend/src/main/java/com/unis/crm/dto/dashboard/DashboardActivityDTO.java index 3d251118..f0504e36 100644 --- a/backend/src/main/java/com/unis/crm/dto/dashboard/DashboardActivityDTO.java +++ b/backend/src/main/java/com/unis/crm/dto/dashboard/DashboardActivityDTO.java @@ -12,6 +12,7 @@ public class DashboardActivityDTO { private String content; private Long operatorUserId; private String operatorName; + private String targetTab; private OffsetDateTime createdAt; private String timeText; @@ -79,6 +80,14 @@ public class DashboardActivityDTO { this.operatorName = operatorName; } + public String getTargetTab() { + return targetTab; + } + + public void setTargetTab(String targetTab) { + this.targetTab = targetTab; + } + public OffsetDateTime getCreatedAt() { return createdAt; } diff --git a/backend/src/main/java/com/unis/crm/dto/dashboard/DashboardHomeDTO.java b/backend/src/main/java/com/unis/crm/dto/dashboard/DashboardHomeDTO.java index 15a0f780..53f19651 100644 --- a/backend/src/main/java/com/unis/crm/dto/dashboard/DashboardHomeDTO.java +++ b/backend/src/main/java/com/unis/crm/dto/dashboard/DashboardHomeDTO.java @@ -9,6 +9,8 @@ public class DashboardHomeDTO { private String jobTitle; private String deptName; private Long onboardingDays; + private Boolean todoCardVisible; + private Boolean activityCardVisible; private List stats; private List todos; private List activities; @@ -17,6 +19,7 @@ public class DashboardHomeDTO { } public DashboardHomeDTO(Long userId, String realName, String jobTitle, String deptName, Long onboardingDays, + Boolean todoCardVisible, Boolean activityCardVisible, List stats, List todos, List activities) { this.userId = userId; @@ -24,6 +27,8 @@ public class DashboardHomeDTO { this.jobTitle = jobTitle; this.deptName = deptName; this.onboardingDays = onboardingDays; + this.todoCardVisible = todoCardVisible; + this.activityCardVisible = activityCardVisible; this.stats = stats; this.todos = todos; this.activities = activities; @@ -69,6 +74,22 @@ public class DashboardHomeDTO { this.onboardingDays = onboardingDays; } + public Boolean getTodoCardVisible() { + return todoCardVisible; + } + + public void setTodoCardVisible(Boolean todoCardVisible) { + this.todoCardVisible = todoCardVisible; + } + + public Boolean getActivityCardVisible() { + return activityCardVisible; + } + + public void setActivityCardVisible(Boolean activityCardVisible) { + this.activityCardVisible = activityCardVisible; + } + public List getStats() { return stats; } diff --git a/backend/src/main/java/com/unis/crm/mapper/DashboardMapper.java b/backend/src/main/java/com/unis/crm/mapper/DashboardMapper.java index 468e2b4c..6a00e1c3 100644 --- a/backend/src/main/java/com/unis/crm/mapper/DashboardMapper.java +++ b/backend/src/main/java/com/unis/crm/mapper/DashboardMapper.java @@ -26,5 +26,12 @@ public interface DashboardMapper { int markTodoDone(@Param("userId") Long userId, @Param("todoId") Long todoId); - List selectLatestActivities(@Param("userId") Long userId); + @DataScope(tableAlias = "a", ownerColumn = "owner_user_id") + List selectLatestOpportunityActivities(@Param("userId") Long userId); + + @DataScope(tableAlias = "a", ownerColumn = "owner_user_id") + List selectLatestSalesActivities(@Param("userId") Long userId); + + @DataScope(tableAlias = "a", ownerColumn = "owner_user_id") + List selectLatestChannelActivities(@Param("userId") Long userId); } diff --git a/backend/src/main/java/com/unis/crm/service/impl/DashboardServiceImpl.java b/backend/src/main/java/com/unis/crm/service/impl/DashboardServiceImpl.java index ae633cde..65383a52 100644 --- a/backend/src/main/java/com/unis/crm/service/impl/DashboardServiceImpl.java +++ b/backend/src/main/java/com/unis/crm/service/impl/DashboardServiceImpl.java @@ -8,21 +8,34 @@ import com.unis.crm.dto.dashboard.DashboardTodoDTO; import com.unis.crm.dto.dashboard.UserWelcomeDTO; import com.unis.crm.mapper.DashboardMapper; import com.unis.crm.service.DashboardService; +import com.unisbase.service.SysPermissionService; +import com.unisbase.spi.UnisBaseTenantProvider; import java.time.LocalDate; import java.time.OffsetDateTime; import java.time.ZoneId; import java.time.temporal.ChronoUnit; import java.util.ArrayList; +import java.util.Comparator; import java.util.List; +import java.util.Set; import org.springframework.stereotype.Service; @Service public class DashboardServiceImpl implements DashboardService { - private final DashboardMapper dashboardMapper; + private static final String DASHBOARD_TODO_CARD_VIEW_PERMISSION = "dashboard_todo_card:view"; + private static final String DASHBOARD_ACTIVITY_CARD_VIEW_PERMISSION = "dashboard_activity_card:view"; - public DashboardServiceImpl(DashboardMapper dashboardMapper) { + private final DashboardMapper dashboardMapper; + private final SysPermissionService sysPermissionService; + private final UnisBaseTenantProvider tenantProvider; + + public DashboardServiceImpl(DashboardMapper dashboardMapper, + SysPermissionService sysPermissionService, + UnisBaseTenantProvider tenantProvider) { this.dashboardMapper = dashboardMapper; + this.sysPermissionService = sysPermissionService; + this.tenantProvider = tenantProvider; } @Override @@ -41,8 +54,11 @@ public class DashboardServiceImpl implements DashboardService { if (monthlyChannelStat != null) { stats.add(monthlyChannelStat); } - List todos = dashboardMapper.selectTodos(userId); - List activities = dashboardMapper.selectLatestActivities(userId); + Set permissionCodes = loadPermissionCodes(userId); + boolean todoCardVisible = permissionCodes.contains(DASHBOARD_TODO_CARD_VIEW_PERMISSION); + boolean activityCardVisible = permissionCodes.contains(DASHBOARD_ACTIVITY_CARD_VIEW_PERMISSION); + List todos = todoCardVisible ? dashboardMapper.selectTodos(userId) : List.of(); + List activities = activityCardVisible ? loadLatestActivities(userId) : List.of(); enrichActivityTimeText(activities); long onboardingDays = 0; @@ -57,6 +73,8 @@ public class DashboardServiceImpl implements DashboardService { user.getJobTitle(), user.getDeptName(), onboardingDays, + todoCardVisible, + activityCardVisible, stats, todos, activities @@ -77,6 +95,35 @@ public class DashboardServiceImpl implements DashboardService { } } + private Set loadPermissionCodes(Long userId) { + try { + Long tenantId = tenantProvider.getCurrentTenantId(); + if (tenantId == null) { + return Set.of(); + } + Set permissionCodes = sysPermissionService.listPermissionCodesByUserId(userId, tenantId); + if (permissionCodes == null) { + return Set.of(); + } + return permissionCodes; + } catch (Exception ignored) { + return Set.of(); + } + } + + private List loadLatestActivities(Long userId) { + List activities = new ArrayList<>(); + activities.addAll(dashboardMapper.selectLatestOpportunityActivities(userId)); + activities.addAll(dashboardMapper.selectLatestSalesActivities(userId)); + activities.addAll(dashboardMapper.selectLatestChannelActivities(userId)); + activities.sort(Comparator.comparing(DashboardActivityDTO::getCreatedAt, + Comparator.nullsLast(Comparator.reverseOrder()))); + if (activities.size() > 8) { + return new ArrayList<>(activities.subList(0, 8)); + } + return activities; + } + private void enrichActivityTimeText(List activities) { if (activities == null || activities.isEmpty()) { return; diff --git a/backend/src/main/resources/mapper/dashboard/DashboardMapper.xml b/backend/src/main/resources/mapper/dashboard/DashboardMapper.xml index 83191b99..060c294b 100644 --- a/backend/src/main/resources/mapper/dashboard/DashboardMapper.xml +++ b/backend/src/main/resources/mapper/dashboard/DashboardMapper.xml @@ -116,110 +116,7 @@ and status in ('todo', 'done') - select a.id, a.bizType, @@ -229,10 +126,180 @@ a.content, a.operatorUserId, u.display_name as operatorName, + a.targetTab, a.createdAt - from activity_union a + from ( + select + (1000000000 + o.id) as id, + 'opportunity' as bizType, + o.id as bizId, + 'status_update' as actionType, + '商机状态更新' as title, + o.opportunity_name || ' · 当前阶段:' || + case o.stage + when 'initial_contact' then '初步沟通' + when 'solution_discussion' then '方案交流' + when 'bidding' then '招投标' + when 'business_negotiation' then '商务谈判' + when 'won' then '已成交' + when 'lost' then '已放弃' + else coalesce(o.stage, '未知') + end || + case when coalesce(o.archived, false) then ' · 已签单' else '' end || + case when coalesce(o.pushed_to_oms, false) then ' · 已推送OMS' else '' end as content, + o.owner_user_id as operatorUserId, + case when coalesce(o.archived, false) then 'archived' else 'active' end as targetTab, + o.owner_user_id as owner_user_id, + o.updated_at as createdAt + from crm_opportunity o + where o.updated_at > o.created_at + + union all + + select + (1100000000 + f.id) as id, + 'opportunity' as bizType, + f.opportunity_id as bizId, + 'followup_update' as actionType, + '商机跟进更新' as title, + o.opportunity_name || ' · ' || coalesce(f.followup_type, '跟进') || ' · ' || + left(coalesce(nullif(btrim(f.next_action), ''), nullif(btrim(f.content), ''), '已新增跟进记录'), 80) as content, + f.followup_user_id as operatorUserId, + case when coalesce(o.archived, false) then 'archived' else 'active' end as targetTab, + o.owner_user_id as owner_user_id, + f.followup_time as createdAt + from crm_opportunity_followup f + join crm_opportunity o on o.id = f.opportunity_id + ) a left join sys_user u on u.user_id = a.operatorUserId + where 1 = 1 order by a.createdAt desc nulls last - limit 8 + limit 12 + + + + + diff --git a/frontend/src/lib/auth.ts b/frontend/src/lib/auth.ts index d8532f46..cd415acb 100644 --- a/frontend/src/lib/auth.ts +++ b/frontend/src/lib/auth.ts @@ -94,6 +94,7 @@ export interface DashboardActivity { content?: string; operatorUserId?: number; operatorName?: string; + targetTab?: string; createdAt?: string; timeText?: string; } @@ -104,6 +105,8 @@ export interface DashboardHome { jobTitle?: string; deptName?: string; onboardingDays?: number; + todoCardVisible?: boolean; + activityCardVisible?: boolean; stats?: DashboardStat[]; todos?: DashboardTodo[]; activities?: DashboardActivity[]; @@ -650,6 +653,11 @@ function applyAuthHeaders(headers: Headers) { headers.set("X-User-Id", String(userId)); } + const activeTenantId = localStorage.getItem("activeTenantId"); + if (activeTenantId?.trim()) { + headers.set("X-Tenant-Id", activeTenantId.trim()); + } + return headers; } diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index f6b5209d..62923e12 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -120,6 +120,9 @@ export default function Dashboard() { const visibleActivities = showAllActivities ? activities : activities.slice(0, DASHBOARD_PREVIEW_COUNT); const hasMoreActivities = activities.length > DASHBOARD_PREVIEW_COUNT && activities[0]?.id !== 0; const hasMoreHistoryTodos = historyTodos.length > DASHBOARD_HISTORY_PREVIEW_COUNT; + const showTodoCard = home.todoCardVisible !== false; + const showActivityCard = home.activityCardVisible !== false; + const dashboardPanelCount = Number(showTodoCard) + Number(showActivityCard); const handleCompleteTodo = async (todoId: string) => { if (!todoId || completingTodoId === todoId) { @@ -151,6 +154,23 @@ export default function Dashboard() { navigate(target.pathname, target.state ? { state: target.state } : undefined); }; + const handleActivityClick = (activity: DashboardActivity) => { + if (!activity?.bizId || !activity.bizType) { + return; + } + + if (activity.bizType === "opportunity") { + const archiveTab = activity.targetTab === "archived" ? "archived" : "active"; + navigate("/opportunities", { state: { selectedId: activity.bizId, archiveTab } }); + return; + } + + if (activity.bizType === "sales" || activity.bizType === "channel") { + const tab = activity.targetTab === "channel" ? "channel" : "sales"; + navigate("/expansion", { state: { tab, selectedId: activity.bizId } }); + } + }; + return (
@@ -223,7 +243,8 @@ export default function Dashboard() { })}
-
+
1 ? "md:grid-cols-2" : "md:grid-cols-1"} md:gap-6`}> + {showTodoCard ? (
+ ) : null} + {showActivityCard ? ( 最新动态
{visibleActivities.map((news: DashboardActivity, i: number) => ( -
-
+ ))}
{hasMoreActivities && !showAllActivities ? ( @@ -353,6 +394,7 @@ export default function Dashboard() { ) : null} + ) : null}
)} diff --git a/frontend/src/pages/Expansion.tsx b/frontend/src/pages/Expansion.tsx index 39b701eb..ba1ac2d2 100644 --- a/frontend/src/pages/Expansion.tsx +++ b/frontend/src/pages/Expansion.tsx @@ -29,6 +29,7 @@ import { cn } from "@/lib/utils"; type ExpansionItem = SalesExpansionItem | ChannelExpansionItem; type ExpansionTab = "sales" | "channel"; +type ExpansionLocationState = { tab?: ExpansionTab; selectedId?: number } | null; type ExpansionExportFilters = { keyword?: string; intent?: string; @@ -1038,12 +1039,42 @@ export default function Expansion() { }, []); useEffect(() => { - const requestedTab = (location.state as { tab?: ExpansionTab } | null)?.tab; + const requestedTab = (location.state as ExpansionLocationState)?.tab; if (requestedTab === "sales" || requestedTab === "channel") { setActiveTab(requestedTab); } }, [location.state]); + useEffect(() => { + const requestedState = location.state as ExpansionLocationState; + const requestedTab = requestedState?.tab; + const requestedId = requestedState?.selectedId; + if (!requestedId) { + return; + } + + if (requestedTab === "sales") { + const matched = salesData.find((item) => item.id === requestedId); + if (matched) { + setSelectedItem(matched); + } + return; + } + + if (requestedTab === "channel") { + const matched = channelData.find((item) => item.id === requestedId); + if (matched) { + setSelectedItem(matched); + } + return; + } + + const matched = salesData.find((item) => item.id === requestedId) ?? channelData.find((item) => item.id === requestedId) ?? null; + if (matched) { + setSelectedItem(matched); + } + }, [location.state, salesData, channelData]); + useEffect(() => { let cancelled = false; diff --git a/frontend/src/pages/Opportunities.tsx b/frontend/src/pages/Opportunities.tsx index 8301535f..3ebdcb29 100644 --- a/frontend/src/pages/Opportunities.tsx +++ b/frontend/src/pages/Opportunities.tsx @@ -2,6 +2,7 @@ import { useEffect, useRef, useState, type ReactNode } from "react"; import { Search, Plus, Download, ChevronDown, Check, Building, Calendar, DollarSign, Activity, X, Clock, FileText, User, Tag, AlertTriangle, ListFilter } from "lucide-react"; import { motion, AnimatePresence } from "motion/react"; import { createPortal } from "react-dom"; +import { useLocation } from "react-router-dom"; import { createOpportunity, getExpansionOverview, getOpportunityMeta, getOpportunityOmsPreSalesOptions, getOpportunityOverview, getStoredCurrentUserId, pushOpportunityToOms, updateOpportunity, type ChannelExpansionItem, type CreateOpportunityPayload, type OmsPreSalesOption, type OpportunityDictOption, type OpportunityFollowUp, type OpportunityItem, type PushOpportunityToOmsPayload, type SalesExpansionItem } from "@/lib/auth"; import { AdaptiveSelect } from "@/components/AdaptiveSelect"; import { useIsWecomBrowser } from "@/hooks/useIsWecomBrowser"; @@ -32,6 +33,7 @@ const COMPETITOR_OPTIONS = [ type CompetitorOption = (typeof COMPETITOR_OPTIONS)[number]; type OperatorMode = "none" | "h3c" | "channel" | "both"; type OpportunityArchiveTab = "active" | "archived"; +type OpportunityLocationState = { selectedId?: number; archiveTab?: OpportunityArchiveTab } | null; type OpportunityExportFilters = { keyword?: string; expectedStartDate?: string; @@ -1342,6 +1344,7 @@ function CompetitorMultiSelect({ } export default function Opportunities() { + const location = useLocation(); const currentUserId = getStoredCurrentUserId(); const isMobileViewport = useIsMobileViewport(); const isWecomBrowser = useIsWecomBrowser(); @@ -1404,6 +1407,27 @@ export default function Opportunities() { }; }, [keyword, filter]); + useEffect(() => { + const requestedState = location.state as OpportunityLocationState; + const requestedArchiveTab = requestedState?.archiveTab; + if (requestedArchiveTab === "active" || requestedArchiveTab === "archived") { + setArchiveTab(requestedArchiveTab); + } + }, [location.state]); + + useEffect(() => { + const requestedState = location.state as OpportunityLocationState; + const requestedId = requestedState?.selectedId; + if (!requestedId) { + return; + } + + const matched = items.find((item) => item.id === requestedId); + if (matched) { + setSelectedItem(matched); + } + }, [location.state, items]); + useEffect(() => { let cancelled = false;