From efd33705197f087aae1c8bc95becb0bfee07c0cb Mon Sep 17 00:00:00 2001 From: kangwenjing <1138819403@qq.com> Date: Thu, 2 Apr 2026 10:07:21 +0800 Subject: [PATCH] 0402 --- .../dto/opportunity/OpportunityMetaDTO.java | 13 +- .../service/impl/OpportunityServiceImpl.java | 20 +- .../src/main/resources/application-prod.yml | 2 +- frontend/src/lib/auth.ts | 1 + frontend/src/pages/Dashboard.tsx | 2 +- frontend/src/pages/Expansion.tsx | 22 +- frontend/src/pages/Login.tsx | 202 +++++++++--------- frontend/src/pages/Opportunities.tsx | 46 +++- frontend/src/pages/login.css | 156 +++++++------- ...pportunity_construction_type_dict_pg17.sql | 21 ++ sql/init_full_pg17.sql | 16 ++ 11 files changed, 312 insertions(+), 189 deletions(-) create mode 100644 sql/archive/alter_opportunity_construction_type_dict_pg17.sql diff --git a/backend/src/main/java/com/unis/crm/dto/opportunity/OpportunityMetaDTO.java b/backend/src/main/java/com/unis/crm/dto/opportunity/OpportunityMetaDTO.java index 1a31aba1..093b2ee1 100644 --- a/backend/src/main/java/com/unis/crm/dto/opportunity/OpportunityMetaDTO.java +++ b/backend/src/main/java/com/unis/crm/dto/opportunity/OpportunityMetaDTO.java @@ -7,6 +7,7 @@ public class OpportunityMetaDTO { private List stageOptions; private List operatorOptions; private List projectLocationOptions; + private List opportunityTypeOptions; public OpportunityMetaDTO() { } @@ -14,10 +15,12 @@ public class OpportunityMetaDTO { public OpportunityMetaDTO( List stageOptions, List operatorOptions, - List projectLocationOptions) { + List projectLocationOptions, + List opportunityTypeOptions) { this.stageOptions = stageOptions; this.operatorOptions = operatorOptions; this.projectLocationOptions = projectLocationOptions; + this.opportunityTypeOptions = opportunityTypeOptions; } public List getStageOptions() { @@ -43,4 +46,12 @@ public class OpportunityMetaDTO { public void setProjectLocationOptions(List projectLocationOptions) { this.projectLocationOptions = projectLocationOptions; } + + public List getOpportunityTypeOptions() { + return opportunityTypeOptions; + } + + public void setOpportunityTypeOptions(List opportunityTypeOptions) { + this.opportunityTypeOptions = opportunityTypeOptions; + } } diff --git a/backend/src/main/java/com/unis/crm/service/impl/OpportunityServiceImpl.java b/backend/src/main/java/com/unis/crm/service/impl/OpportunityServiceImpl.java index 244217b9..a1b8a692 100644 --- a/backend/src/main/java/com/unis/crm/service/impl/OpportunityServiceImpl.java +++ b/backend/src/main/java/com/unis/crm/service/impl/OpportunityServiceImpl.java @@ -6,6 +6,7 @@ import com.unis.crm.dto.opportunity.CurrentUserAccountDTO; import com.unis.crm.dto.opportunity.CreateOpportunityFollowUpRequest; import com.unis.crm.dto.opportunity.CreateOpportunityRequest; import com.unis.crm.dto.opportunity.OmsPreSalesOptionDTO; +import com.unis.crm.dto.opportunity.OpportunityDictOptionDTO; import com.unis.crm.dto.opportunity.OpportunityMetaDTO; import com.unis.crm.dto.opportunity.OpportunityFollowUpDTO; import com.unis.crm.dto.opportunity.OpportunityIntegrationTargetDTO; @@ -33,6 +34,7 @@ public class OpportunityServiceImpl implements OpportunityService { private static final String STAGE_TYPE_CODE = "sj_xmjd"; private static final String OPERATOR_TYPE_CODE = "sj_yzf"; + private static final String OPPORTUNITY_TYPE_CODE = "sj_jslx"; private static final Logger log = LoggerFactory.getLogger(OpportunityServiceImpl.class); private final OpportunityMapper opportunityMapper; @@ -45,10 +47,26 @@ public class OpportunityServiceImpl implements OpportunityService { @Override public OpportunityMetaDTO getMeta() { + List opportunityTypeOptions = opportunityMapper.selectDictItems(OPPORTUNITY_TYPE_CODE); return new OpportunityMetaDTO( opportunityMapper.selectDictItems(STAGE_TYPE_CODE), opportunityMapper.selectDictItems(OPERATOR_TYPE_CODE), - opportunityMapper.selectProvinceAreaOptions()); + opportunityMapper.selectProvinceAreaOptions(), + opportunityTypeOptions.isEmpty() ? buildDefaultOpportunityTypeOptions() : opportunityTypeOptions); + } + + private List buildDefaultOpportunityTypeOptions() { + return List.of( + buildDictOption("新建", "新建"), + buildDictOption("扩容", "扩容"), + buildDictOption("替换", "替换")); + } + + private OpportunityDictOptionDTO buildDictOption(String label, String value) { + OpportunityDictOptionDTO option = new OpportunityDictOptionDTO(); + option.setLabel(label); + option.setValue(value); + return option; } @Override diff --git a/backend/src/main/resources/application-prod.yml b/backend/src/main/resources/application-prod.yml index 9fd2bd97..c434414b 100644 --- a/backend/src/main/resources/application-prod.yml +++ b/backend/src/main/resources/application-prod.yml @@ -69,7 +69,7 @@ unisbase: access-token-safety-seconds: 120 oms: enabled: ${OMS_ENABLED:true} - base-url: ${OMS_BASE_URL:http://10.100.52.135:28080} + base-url: ${OMS_BASE_URL:http://121.41.238.146:28080} api-key: ${OMS_API_KEY:c7f858d0-30b8-4b7f-9ea1-0ccf5ceb1c54} api-key-header: ${OMS_API_KEY_HEADER:apiKey} user-info-path: ${OMS_USER_INFO_PATH:/api/v1/user/info} diff --git a/frontend/src/lib/auth.ts b/frontend/src/lib/auth.ts index 520ff766..383eec1e 100644 --- a/frontend/src/lib/auth.ts +++ b/frontend/src/lib/auth.ts @@ -284,6 +284,7 @@ export interface OpportunityMeta { stageOptions?: OpportunityDictOption[]; operatorOptions?: OpportunityDictOption[]; projectLocationOptions?: OpportunityDictOption[]; + opportunityTypeOptions?: OpportunityDictOption[]; } export interface OmsPreSalesOption { diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index 06a59973..e28de00f 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -125,7 +125,7 @@ export default function Dashboard() {

工作台

- 欢迎回来,{home.realName || "无"}。今天是你入职的第 {home.onboardingDays ?? 0} 天。 + 欢迎回来,{home.realName || "无"}。

diff --git a/frontend/src/pages/Expansion.tsx b/frontend/src/pages/Expansion.tsx index 8c6264b3..bf1e7164 100644 --- a/frontend/src/pages/Expansion.tsx +++ b/frontend/src/pages/Expansion.tsx @@ -102,6 +102,20 @@ function normalizeOptionalText(value?: string) { return trimmed ? trimmed : undefined; } +function dedupeExpansionItemsById(items: T[]) { + const seenIds = new Set(); + return items.filter((item) => { + if (item.id === null || item.id === undefined) { + return true; + } + if (seenIds.has(item.id)) { + return false; + } + seenIds.add(item.id); + return true; + }); +} + function getFieldInputClass(hasError: boolean) { return cn( "crm-input-box crm-input-text w-full border bg-white outline-none focus:border-violet-500 focus:ring-1 focus:ring-violet-500 dark:bg-slate-900/50", @@ -477,8 +491,8 @@ export default function Expansion() { return; } - setSalesData(data.salesItems ?? []); - setChannelData(data.channelItems ?? []); + setSalesData(dedupeExpansionItemsById(data.salesItems ?? [])); + setChannelData(dedupeExpansionItemsById(data.channelItems ?? [])); setSelectedItem(null); } catch { if (!cancelled) { @@ -848,6 +862,8 @@ export default function Expansion() { value={form.officeName || ""} placeholder="请选择" sheetTitle="代表处 / 办事处" + searchable + searchPlaceholder="搜索代表处 / 办事处" options={[ { value: "", label: "请选择" }, ...officeOptions.map((option) => ({ @@ -877,7 +893,7 @@ export default function Expansion() { onChange("targetDept", e.target.value)} - placeholder="办事处/行业系统部/地市" + placeholder="办事处/地市" className={getFieldInputClass(Boolean(fieldErrors?.targetDept))} /> {fieldErrors?.targetDept ?

{fieldErrors.targetDept}

: null} diff --git a/frontend/src/pages/Login.tsx b/frontend/src/pages/Login.tsx index 0c6d654c..e08aae9b 100644 --- a/frontend/src/pages/Login.tsx +++ b/frontend/src/pages/Login.tsx @@ -249,116 +249,112 @@ export default function LoginPage() {
-
-
-
- {platformConfig?.logoUrl ? {appName} : } -
-
-

智慧销售协同平台

-

{appName}

-
-
- -
-

Sales Workspace

-

- 统一客户管理 -
- 高效推进业务增长 -

-

{systemDescription}

-
- -
- 首页 - 拓展 - 商机 - 工作 - 我的 -
-
-
-
-

欢迎回来

-

紫光汇智CRM系统

-
- -
- - - - - - - {captchaEnabled ? ( -
- 验证码 -
- handleChange("captchaCode", event.target.value)} - placeholder="请输入验证码" - /> - +
+
+
+
+ {platformConfig?.logoUrl ? {appName} : } +
+
+

智慧销售协同平台

+

{appName}

- ) : null} - -
- +
+

欢迎回来

+

账号登录

+

{systemDescription}

+
+
+ 客户拓展 + 商机推进 + 销售协同 +
- {error ?
{error}
: null} +
+
+

欢迎回来

+

{appName}

+
- - +
+ + + + + + + {captchaEnabled ? ( +
+ 验证码 +
+ handleChange("captchaCode", event.target.value)} + placeholder="请输入验证码" + /> + +
+
+ ) : null} + +
+ +
+ + {error ?
{error}
: null} + + +
+
+
diff --git a/frontend/src/pages/Opportunities.tsx b/frontend/src/pages/Opportunities.tsx index 70ff0f5b..1165764d 100644 --- a/frontend/src/pages/Opportunities.tsx +++ b/frontend/src/pages/Opportunities.tsx @@ -14,6 +14,12 @@ const CONFIDENCE_OPTIONS = [ { value: "C", label: "C" }, ] as const; +const FALLBACK_OPPORTUNITY_TYPE_OPTIONS = [ + { value: "新建", label: "新建" }, + { value: "扩容", label: "扩容" }, + { value: "替换", label: "替换" }, +] as const; + type ConfidenceGrade = (typeof CONFIDENCE_OPTIONS)[number]["value"]; const COMPETITOR_OPTIONS = [ @@ -52,7 +58,7 @@ const defaultForm: CreateOpportunityPayload = { expectedCloseDate: "", confidencePct: "C", stage: "", - opportunityType: "新建", + opportunityType: "", productType: "VDI云桌面", source: "主动开发", salesExpansionId: undefined, @@ -77,7 +83,7 @@ function toFormFromItem(item: OpportunityItem): CreateOpportunityPayload { expectedCloseDate: item.date || "", confidencePct: normalizeConfidenceGrade(item.confidence), stage: item.stageCode || item.stage || "", - opportunityType: item.type || "新建", + opportunityType: item.type || "", productType: item.product || "VDI云桌面", source: item.source || "主动开发", salesExpansionId: item.salesExpansionId, @@ -859,6 +865,7 @@ export default function Opportunities() { const [stageOptions, setStageOptions] = useState([]); const [operatorOptions, setOperatorOptions] = useState([]); const [projectLocationOptions, setProjectLocationOptions] = useState([]); + const [opportunityTypeOptions, setOpportunityTypeOptions] = useState([]); const [form, setForm] = useState(defaultForm); const [pushPreSalesId, setPushPreSalesId] = useState(undefined); const [pushPreSalesName, setPushPreSalesName] = useState(""); @@ -927,12 +934,14 @@ export default function Opportunities() { setStageOptions((data.stageOptions ?? []).filter((item) => item.value)); setOperatorOptions((data.operatorOptions ?? []).filter((item) => item.value)); setProjectLocationOptions((data.projectLocationOptions ?? []).filter((item) => item.value)); + setOpportunityTypeOptions((data.opportunityTypeOptions ?? []).filter((item) => item.value)); } } catch { if (!cancelled) { setStageOptions([]); setOperatorOptions([]); setProjectLocationOptions([]); + setOpportunityTypeOptions([]); } } } @@ -952,6 +961,15 @@ export default function Opportunities() { setForm((current) => (current.stage ? current : { ...current, stage: defaultStage })); }, [stageOptions]); + useEffect(() => { + if (!opportunityTypeOptions.length) { + return; + } + + const defaultOpportunityType = opportunityTypeOptions[0]?.value || ""; + setForm((current) => (current.opportunityType ? current : { ...current, opportunityType: defaultOpportunityType })); + }, [opportunityTypeOptions]); + const followUpRecords: OpportunityFollowUp[] = selectedItem?.followUps ?? []; const visibleItems = items.filter((item) => (archiveTab === "active" ? !item.archived : Boolean(item.archived))); const stageFilterOptions = [ @@ -972,6 +990,23 @@ export default function Opportunities() { : [] ), ]; + const normalizedOpportunityType = form.opportunityType?.trim() || ""; + const effectiveOpportunityTypeOptions = opportunityTypeOptions.length > 0 + ? opportunityTypeOptions + : FALLBACK_OPPORTUNITY_TYPE_OPTIONS; + const opportunityTypeSelectOptions = [ + { value: "", label: "请选择" }, + ...effectiveOpportunityTypeOptions.map((item) => ({ + label: item.label || item.value || "", + value: item.value || "", + })), + ...( + normalizedOpportunityType + && !effectiveOpportunityTypeOptions.some((item) => (item.value || "").trim() === normalizedOpportunityType) + ? [{ value: normalizedOpportunityType, label: normalizedOpportunityType }] + : [] + ), + ]; const activeStageFilterLabel = stageFilterOptions.find((item) => item.value === filter)?.label || "全部"; const selectedSalesExpansion = selectedItem?.salesExpansionId ? salesExpansionOptions.find((item) => item.id === selectedItem.salesExpansionId) ?? null @@ -1578,12 +1613,9 @@ export default function Opportunities() { 建设类型