neo-cmp-cli 1.13.16 → 1.13.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/README.md +2 -1
  2. package/dist/index2.js +1 -1
  3. package/dist/main2.js +1 -1
  4. package/dist/neo/neoLogin.js +1 -1
  5. package/dist/package.json.js +1 -1
  6. package/package.json +1 -1
  7. package/template/antd-custom-cmp-template/package.json +1 -1
  8. package/template/asset-manage-template/package.json +2 -2
  9. package/template/echarts-custom-cmp-template/package.json +1 -1
  10. package/template/empty-custom-cmp-template/package.json +2 -2
  11. package/template/map-custom-cmp-template/package.json +1 -1
  12. package/template/neo-bi-cmps/neo.config.js +7 -1
  13. package/template/neo-bi-cmps/package.json +8 -7
  14. package/template/neo-bi-cmps/public/403.html +77 -0
  15. package/template/neo-bi-cmps/public/demo.html +2453 -0
  16. package/template/neo-bi-cmps/src/assets/icon/barChart.svg +1 -0
  17. package/template/neo-bi-cmps/src/assets/icon/card.svg +1 -0
  18. package/template/neo-bi-cmps/src/assets/icon/filter.svg +1 -0
  19. package/template/neo-bi-cmps/src/assets/icon/funnel.svg +1 -0
  20. package/template/neo-bi-cmps/src/assets/icon/tab.svg +1 -0
  21. package/template/neo-bi-cmps/src/components/filterBar__c/README.md +3 -14
  22. package/template/neo-bi-cmps/src/components/filterBar__c/common.scss +29 -0
  23. package/template/neo-bi-cmps/src/components/filterBar__c/index.tsx +668 -146
  24. package/template/neo-bi-cmps/src/components/filterBar__c/model.ts +26 -48
  25. package/template/neo-bi-cmps/src/components/filterBar__c/style.scss +46 -139
  26. package/template/neo-bi-cmps/src/components/targetNumber__c/customStyleConfig/index.tsx +11 -10
  27. package/template/neo-bi-cmps/src/components/targetNumber__c/index.tsx +9 -16
  28. package/template/neo-bi-cmps/src/utils/common.ts +231 -0
  29. package/template/neo-bi-cmps/src/utils/filter2chartFilter.ts +268 -0
  30. package/template/neo-bi-cmps/src/utils/filterBar.ts +140 -0
  31. package/template/neo-bi-cmps/src/utils/pipelineFunnel.ts +341 -0
  32. package/template/neo-bi-cmps/src/utils/queryByCustomSQL.ts +117 -0
  33. package/template/neo-bi-cmps/src/utils/requestDebounce.ts +22 -0
  34. package/template/neo-bi-cmps/src/utils/simpleTable.tsx +344 -0
  35. package/template/neo-bi-cmps/src/utils/stageSwitch.ts +15 -0
  36. package/template/neo-bi-cmps/src/utils/stageTimeChart.ts +90 -0
  37. package/template/neo-bi-cmps/src/utils/targetNumber.ts +12 -0
  38. package/template/neo-custom-cmp-template/package.json +2 -2
  39. package/template/neo-h5-cmps/package.json +2 -2
  40. package/template/neo-order-cmps/package.json +2 -2
  41. package/template/neo-pipeline-cmps/.prettierrc.js +12 -0
  42. package/template/neo-pipeline-cmps/@types/neo-ui-common.d.ts +36 -0
  43. package/template/neo-pipeline-cmps/README.md +99 -0
  44. package/template/neo-pipeline-cmps/commitlint.config.js +59 -0
  45. package/template/neo-pipeline-cmps/neo.config.js +124 -0
  46. package/template/neo-pipeline-cmps/package.json +66 -0
  47. package/template/neo-pipeline-cmps/public/403.html +77 -0
  48. package/template/neo-pipeline-cmps/public/css/base.css +283 -0
  49. package/template/neo-pipeline-cmps/public/demo.html +2453 -0
  50. package/template/neo-pipeline-cmps/public/scripts/app/bluebird.js +6679 -0
  51. package/template/neo-pipeline-cmps/public/template.html +13 -0
  52. package/template/neo-pipeline-cmps/src/assets/css/common.scss +127 -0
  53. package/template/neo-pipeline-cmps/src/assets/css/mixin.scss +47 -0
  54. package/template/neo-pipeline-cmps/src/assets/icon/barChart.svg +1 -0
  55. package/template/neo-pipeline-cmps/src/assets/icon/card.svg +1 -0
  56. package/template/neo-pipeline-cmps/src/assets/icon/filter.svg +1 -0
  57. package/template/neo-pipeline-cmps/src/assets/icon/funnel.svg +1 -0
  58. package/template/neo-pipeline-cmps/src/assets/icon/tab.svg +1 -0
  59. package/template/neo-pipeline-cmps/src/assets/img/AIBtn.gif +0 -0
  60. package/template/neo-pipeline-cmps/src/assets/img/NeoCRM.jpg +0 -0
  61. package/template/neo-pipeline-cmps/src/assets/img/aiLogo.png +0 -0
  62. package/template/neo-pipeline-cmps/src/assets/img/card-list.svg +1 -0
  63. package/template/neo-pipeline-cmps/src/assets/img/contact-form.svg +1 -0
  64. package/template/neo-pipeline-cmps/src/assets/img/custom-form.svg +1 -0
  65. package/template/neo-pipeline-cmps/src/assets/img/custom-widget.svg +1 -0
  66. package/template/neo-pipeline-cmps/src/assets/img/data-list.svg +1 -0
  67. package/template/neo-pipeline-cmps/src/assets/img/detail.svg +1 -0
  68. package/template/neo-pipeline-cmps/src/assets/img/favicon.png +0 -0
  69. package/template/neo-pipeline-cmps/src/assets/img/map.svg +1 -0
  70. package/template/neo-pipeline-cmps/src/assets/img/search.svg +1 -0
  71. package/template/neo-pipeline-cmps/src/assets/img/table.svg +1 -0
  72. package/template/neo-pipeline-cmps/src/components/filterBar__c/README.md +24 -0
  73. package/template/neo-pipeline-cmps/src/components/filterBar__c/common.scss +29 -0
  74. package/template/neo-pipeline-cmps/src/components/filterBar__c/index.tsx +730 -0
  75. package/template/neo-pipeline-cmps/src/components/filterBar__c/model.ts +50 -0
  76. package/template/neo-pipeline-cmps/src/components/filterBar__c/style.scss +119 -0
  77. package/template/neo-pipeline-cmps/src/components/pipelineFunnel__c/index.tsx +415 -0
  78. package/template/neo-pipeline-cmps/src/components/pipelineFunnel__c/model.ts +79 -0
  79. package/template/neo-pipeline-cmps/src/components/pipelineFunnel__c/style.scss +83 -0
  80. package/template/neo-pipeline-cmps/src/components/showHealthResult__c/index.tsx +463 -0
  81. package/template/neo-pipeline-cmps/src/components/showHealthResult__c/model.ts +45 -0
  82. package/template/neo-pipeline-cmps/src/components/showHealthResult__c/style.scss +137 -0
  83. package/template/neo-pipeline-cmps/src/components/simpleTable__c/README.md +90 -0
  84. package/template/neo-pipeline-cmps/src/components/simpleTable__c/common.scss +195 -0
  85. package/template/neo-pipeline-cmps/src/components/simpleTable__c/index.tsx +665 -0
  86. package/template/neo-pipeline-cmps/src/components/simpleTable__c/model.ts +124 -0
  87. package/template/neo-pipeline-cmps/src/components/simpleTable__c/style.scss +193 -0
  88. package/template/neo-pipeline-cmps/src/components/stageSwitch__c/index.tsx +511 -0
  89. package/template/neo-pipeline-cmps/src/components/stageSwitch__c/model.ts +70 -0
  90. package/template/{neo-bi-cmps → neo-pipeline-cmps}/src/components/stageSwitch__c/style.scss +4 -2
  91. package/template/neo-pipeline-cmps/src/components/stageTimeChart__c/index.tsx +455 -0
  92. package/template/neo-pipeline-cmps/src/components/stageTimeChart__c/model.ts +103 -0
  93. package/template/{neo-bi-cmps → neo-pipeline-cmps}/src/components/stageTimeChart__c/style.scss +3 -2
  94. package/template/neo-pipeline-cmps/src/utils/common.ts +229 -0
  95. package/template/neo-pipeline-cmps/src/utils/filter2chartFilter.ts +268 -0
  96. package/template/neo-pipeline-cmps/src/utils/filterBar.ts +140 -0
  97. package/template/neo-pipeline-cmps/src/utils/pipelineFunnel.ts +343 -0
  98. package/template/neo-pipeline-cmps/src/utils/queryByCustomSQL.ts +117 -0
  99. package/template/neo-pipeline-cmps/src/utils/requestDebounce.ts +22 -0
  100. package/template/neo-pipeline-cmps/src/utils/simpleTable.tsx +344 -0
  101. package/template/neo-pipeline-cmps/src/utils/stageSwitch.ts +15 -0
  102. package/template/neo-pipeline-cmps/src/utils/stageTimeChart.ts +90 -0
  103. package/template/neo-pipeline-cmps/src/utils/targetNumber.ts +12 -0
  104. package/template/neo-pipeline-cmps/tsconfig.json +40 -0
  105. package/template/neo-web-entity-grid/package.json +2 -2
  106. package/template/neo-web-form/package.json +2 -2
  107. package/template/react-custom-cmp-template/package.json +1 -1
  108. package/template/react-ts-custom-cmp-template/package.json +1 -1
  109. package/template/vue2-custom-cmp-template/package.json +1 -1
  110. package/template/neo-bi-cmps/.npmrc copy +0 -1
  111. package/template/neo-bi-cmps/src/components/aiCommitDrawer__c/README.md +0 -52
  112. package/template/neo-bi-cmps/src/components/aiCommitDrawer__c/index.tsx +0 -183
  113. package/template/neo-bi-cmps/src/components/aiCommitDrawer__c/model.ts +0 -90
  114. package/template/neo-bi-cmps/src/components/aiCommitDrawer__c/style.scss +0 -218
  115. package/template/neo-bi-cmps/src/components/forecastChart__c/README.md +0 -31
  116. package/template/neo-bi-cmps/src/components/forecastChart__c/index.tsx +0 -158
  117. package/template/neo-bi-cmps/src/components/forecastChart__c/model.ts +0 -40
  118. package/template/neo-bi-cmps/src/components/forecastChart__c/style.scss +0 -154
  119. package/template/neo-bi-cmps/src/components/forecastGrid__c/README.md +0 -36
  120. package/template/neo-bi-cmps/src/components/forecastGrid__c/index.tsx +0 -86
  121. package/template/neo-bi-cmps/src/components/forecastGrid__c/model.ts +0 -62
  122. package/template/neo-bi-cmps/src/components/forecastGrid__c/style.scss +0 -48
  123. package/template/neo-bi-cmps/src/components/gapCloser__c/README.md +0 -24
  124. package/template/neo-bi-cmps/src/components/gapCloser__c/index.tsx +0 -100
  125. package/template/neo-bi-cmps/src/components/gapCloser__c/model.ts +0 -46
  126. package/template/neo-bi-cmps/src/components/gapCloser__c/style.scss +0 -60
  127. package/template/neo-bi-cmps/src/components/kpiCards__c/README.md +0 -35
  128. package/template/neo-bi-cmps/src/components/kpiCards__c/index.tsx +0 -70
  129. package/template/neo-bi-cmps/src/components/kpiCards__c/model.ts +0 -50
  130. package/template/neo-bi-cmps/src/components/kpiCards__c/style.scss +0 -33
  131. package/template/neo-bi-cmps/src/components/oppList__c/README.md +0 -52
  132. package/template/neo-bi-cmps/src/components/oppList__c/index.tsx +0 -285
  133. package/template/neo-bi-cmps/src/components/oppList__c/model.ts +0 -86
  134. package/template/neo-bi-cmps/src/components/oppList__c/style.scss +0 -133
  135. package/template/neo-bi-cmps/src/components/pipelineFunnel__c/index.tsx +0 -130
  136. package/template/neo-bi-cmps/src/components/pipelineFunnel__c/model.ts +0 -66
  137. package/template/neo-bi-cmps/src/components/pipelineFunnel__c/style.scss +0 -133
  138. package/template/neo-bi-cmps/src/components/stageSwitch__c/index.tsx +0 -118
  139. package/template/neo-bi-cmps/src/components/stageSwitch__c/model.ts +0 -92
  140. package/template/neo-bi-cmps/src/components/stageTimeChart__c/index.tsx +0 -126
  141. package/template/neo-bi-cmps/src/components/stageTimeChart__c/model.ts +0 -57
  142. package/template/neo-bi-cmps/src/components/tabSwitch__c/README.md +0 -37
  143. package/template/neo-bi-cmps/src/components/tabSwitch__c/index.tsx +0 -80
  144. package/template/neo-bi-cmps/src/components/tabSwitch__c/model.ts +0 -45
  145. package/template/neo-bi-cmps/src/components/tabSwitch__c/style.scss +0 -37
  146. package/template/neo-bi-cmps/src/utils/axiosFetcher.ts +0 -37
  147. package/template/neo-bi-cmps/src/utils/queryObjectData.ts +0 -76
  148. package/template/neo-bi-cmps/src/utils/xobjects.ts +0 -162
  149. /package/template/{neo-bi-cmps → neo-pipeline-cmps}/src/components/pipelineFunnel__c/README.md +0 -0
  150. /package/template/{neo-bi-cmps → neo-pipeline-cmps}/src/components/stageSwitch__c/README.md +0 -0
  151. /package/template/{neo-bi-cmps → neo-pipeline-cmps}/src/components/stageTimeChart__c/README.md +0 -0
@@ -0,0 +1,511 @@
1
+ /**
2
+ * @file 阶段切换卡片组件
3
+ * @description 切换显示不同销售阶段的数据;商机来自 xObject.query,阶段来自 stage 接口
4
+ */
5
+ import * as React from 'react';
6
+ import { Spin } from 'antd';
7
+ // @ts-ignore
8
+ import { request, xObject } from 'neo-open-api';
9
+ // @ts-ignore
10
+ import isEqual from 'lodash/isEqual';
11
+ // @ts-ignore
12
+ import { BaseCmp, NeoEvent } from 'neo-ui-common';
13
+
14
+ import queryByCustomSQL from '../../utils/queryByCustomSQL';
15
+ import {
16
+ closeRangeFromFilter,
17
+ entityTypeIdForWhere,
18
+ extractStageKeyFromStageName,
19
+ formatAmountDisplay,
20
+ normalizeOwnerIdsForWhere,
21
+ parseNumberOrZero,
22
+ getDefaultFilterByProps,
23
+ } from '../../utils/common';
24
+ import { rowMoney, rowOpportunityStage } from '../../utils/stageSwitch';
25
+
26
+ import './style.scss';
27
+
28
+ const STAGE_LIST_URL =
29
+ '/rest/data/v2.0/xobjects/stage/actions/getStageListByEntityTypeApiKey';
30
+
31
+ interface StageTab {
32
+ key: string;
33
+ name: string;
34
+ amount: string;
35
+ count: number;
36
+ /** 暂无环比时可为空对象,不参与渲染 */
37
+ changes?: Record<string, unknown>;
38
+ }
39
+
40
+ interface StageSwitchProps {
41
+ stages?: StageTab[];
42
+ activeStage?: string;
43
+ data?: {
44
+ __NeoCurrentUser?: { id?: string | number };
45
+ };
46
+ onStageChange?: (stage: StageTab) => void;
47
+ className?: string;
48
+ style?: React.CSSProperties;
49
+ }
50
+
51
+ interface StageSwitchState {
52
+ activeStage: string;
53
+ opportunityList: Record<string, unknown>[];
54
+ /** 由接口 + getOpportunityList 生成的展示用阶段 */
55
+ stages: StageTab[];
56
+ loading: boolean;
57
+ error: string | null;
58
+ /** 与 FilterBar 事件 payload 一致(closeDateCustomRange / opportunityOwner / businessType 等);空对象时回退 defaultFilter */
59
+ filter: any;
60
+ defaultEntityTypeApiKey: string;
61
+ }
62
+
63
+ class StageSwitch extends BaseCmp<StageSwitchProps, StageSwitchState> {
64
+ constructor(props: StageSwitchProps) {
65
+ super(props);
66
+
67
+ const defaultFilter = getDefaultFilterByProps(props);
68
+
69
+ this.state = {
70
+ activeStage: props.activeStage || '',
71
+ opportunityList: [],
72
+ stages: props.stages?.length ? props.stages : [],
73
+ loading: false,
74
+ error: null,
75
+ filter: defaultFilter,
76
+ defaultEntityTypeApiKey:
77
+ defaultFilter.businessTypeApiKey ?? 'defaultBusiType',
78
+ };
79
+
80
+ this.getOpportunityQueryFilter = this.getOpportunityQueryFilter.bind(this);
81
+ this.fetchOpportunityList = this.fetchOpportunityList.bind(this);
82
+ this.getOpportunityStats = this.getOpportunityStats.bind(this);
83
+ this.fetchStageRows = this.fetchStageRows.bind(this);
84
+ this.buildStagesFromApi = this.buildStagesFromApi.bind(this);
85
+ this.fetchAllData = this.fetchAllData.bind(this);
86
+ this.handleStageClick = this.handleStageClick.bind(this);
87
+ this.refreshData = this.refreshData.bind(this);
88
+ this.setFilter = this.setFilter.bind(this);
89
+ this.updateActiveStage = this.updateActiveStage.bind(this);
90
+ }
91
+
92
+ componentDidMount() {
93
+ this.fetchAllData();
94
+
95
+ /*
96
+ // 监听一个广播事件
97
+ NeoEvent.listen('updateFilterData', (filterData: any) => {
98
+ console.log('StageSwitch 监听到了一个广播事件 updateFilterData: ', filterData);
99
+ this.setFilter(filterData);
100
+ });
101
+
102
+ // 广播事件:更新当前激活的销售阶段
103
+ NeoEvent.listen('updateActiveStage', (activeStage: string) => {
104
+ console.log('SimpleTable 监听到了一个广播事件 updateActiveStage: ', activeStage);
105
+ this.updateActiveStage(activeStage);
106
+ });
107
+ */
108
+ }
109
+
110
+ componentDidUpdate(prevProps: StageSwitchProps) {
111
+ if (
112
+ this.props.activeStage != null &&
113
+ this.props.activeStage !== '' &&
114
+ this.props.activeStage !== prevProps.activeStage
115
+ ) {
116
+ this.setState({ activeStage: this.props.activeStage });
117
+ }
118
+ }
119
+
120
+ /** 将 FilterBar 结构 filter 转为 xObject.query 的 where 字符串数组(SDK 按顺序 and 拼接) */
121
+ getOpportunityQueryFilter(): string[] {
122
+ const raw = this.state.filter;
123
+ const curFilter: Record<string, unknown> =
124
+ raw && typeof raw === 'object' && !Array.isArray(raw)
125
+ ? Object.keys(raw as object).length > 0
126
+ ? (raw as Record<string, unknown>)
127
+ : defaultFilter
128
+ : defaultFilter;
129
+
130
+ const where: string[] = [];
131
+
132
+ const range = closeRangeFromFilter(curFilter.closeDateCustomRange);
133
+ if (range) {
134
+ where.push(`closeDate >= ${range.start} and closeDate <= ${range.end}`);
135
+ }
136
+
137
+ const ownerIds = normalizeOwnerIdsForWhere(curFilter.opportunityOwner);
138
+ if (ownerIds.length === 1) {
139
+ where.push(`ownerId = ${ownerIds[0]}`);
140
+ } else if (ownerIds.length > 1) {
141
+ where.push(`ownerId in (${ownerIds.join(', ')})`);
142
+ }
143
+
144
+ const entityTypeId = entityTypeIdForWhere(curFilter.businessType);
145
+ if (entityTypeId != null) {
146
+ where.push(`entityType = ${entityTypeId}`);
147
+ }
148
+
149
+ return where;
150
+ }
151
+
152
+ /** 用于生成获取历史商机列表数据的查询条件 */
153
+ getHistoryOpportunityQueryFilter(): string[] {
154
+ const raw = this.state.filter;
155
+ const curFilter: Record<string, unknown> =
156
+ raw && typeof raw === 'object' && !Array.isArray(raw)
157
+ ? Object.keys(raw as object).length > 0
158
+ ? (raw as Record<string, unknown>)
159
+ : defaultFilter
160
+ : defaultFilter;
161
+
162
+ const where: string[] = [];
163
+
164
+ const range = closeRangeFromFilter(curFilter.closeDateCustomRange);
165
+ if (range) {
166
+ where.push(
167
+ `opportunity_1_closeDate >= ${range.start} and opportunity_1_closeDate <= ${range.end}`,
168
+ );
169
+ }
170
+
171
+ const ownerIds = normalizeOwnerIdsForWhere(curFilter.opportunityOwner);
172
+ if (ownerIds.length === 1) {
173
+ where.push(`opportunity_1_ownerId = ${ownerIds[0]}`);
174
+ } else if (ownerIds.length > 1) {
175
+ where.push(`opportunity_1_ownerId in (${ownerIds.join(', ')})`);
176
+ }
177
+
178
+ const entityTypeId = entityTypeIdForWhere(curFilter.businessType);
179
+ if (entityTypeId != null) {
180
+ where.push(`opportunity_1_entityType = ${entityTypeId}`);
181
+ }
182
+
183
+ if (curFilter.changesSinceCustomTime) {
184
+ where.push(`version = ${curFilter.changesSinceCustomTime}`);
185
+ }
186
+
187
+ return where;
188
+ }
189
+
190
+ /** 单次查询最多 1000 条 opportunity */
191
+ async fetchOpportunityList(): Promise<Record<string, unknown>[]> {
192
+ const result = await xObject.query({
193
+ xObjectApiKey: 'opportunity',
194
+ fields: ['id', 'money', 'customItem248__c'],
195
+ page: 1,
196
+ pageSize: 1000,
197
+ where: this.getOpportunityQueryFilter(),
198
+ });
199
+
200
+ return result?.status
201
+ ? ((result.data ?? []) as Record<string, unknown>[])
202
+ : [];
203
+ }
204
+
205
+ /** 查询历史商机列表数据 */
206
+ async fetchHistoryOpportunityList(): Promise<unknown[]> {
207
+ const result = await queryByCustomSQL({
208
+ xObjectApiKey: 'biCustomModel_397169_20260401104916618',
209
+ fields: [
210
+ 'opportunity_1_opportunityName',
211
+ 'opportunity_1_saleStageId',
212
+ 'opportunity_1_money',
213
+ ],
214
+ page: 1,
215
+ pageSize: 1000,
216
+ where: this.getHistoryOpportunityQueryFilter(),
217
+ });
218
+
219
+ return result?.status ? result.data ?? [] : [];
220
+ }
221
+
222
+ /**
223
+ * 按 stageName 过滤商机:取 stageName 第一个 `.` 之后的片段作为阶段键,与商机行上阶段文案同样规则比对。
224
+ * @returns count — 条数; amount — money(若无则 amount 字段)之和
225
+ */
226
+ getOpportunityStats(
227
+ stageName: string,
228
+ list: Record<string, unknown>[], // 商机列表
229
+ ): { amount: number; count: number } {
230
+ const key = extractStageKeyFromStageName(stageName);
231
+ let amount = 0; // 商机总金额
232
+ let count = 0; // 商机总条数
233
+
234
+ for (const row of list) {
235
+ if (!row || typeof row !== 'object') continue;
236
+ const rec = row as Record<string, unknown>;
237
+ const rowKey = extractStageKeyFromStageName(rowOpportunityStage(rec));
238
+ if (key !== rowKey) continue;
239
+ count += 1;
240
+ amount += rowMoney(rec);
241
+ }
242
+
243
+ return { amount, count };
244
+ }
245
+
246
+ /**
247
+ * 历史商机列表为自定义 SQL 返回的二维数组行:
248
+ * `[opportunityName, saleStageId, money, ...]` — 第 2 列为阶段 ID(与阶段 `key` 对应),第 3 列为销售额。
249
+ * @returns count — 条数; amount — 销售额之和
250
+ */
251
+ getHistoryOpportunityStats(
252
+ stageKey: string,
253
+ list: unknown[],
254
+ ): { amount: number; count: number } {
255
+ let amount = 0;
256
+ let count = 0;
257
+ const targetId = String(stageKey);
258
+
259
+ for (const row of list) {
260
+ if (!Array.isArray(row) || row.length < 3) continue;
261
+ if (String(row[1]) !== targetId) continue;
262
+ count += 1;
263
+ amount += parseNumberOrZero(row[2]);
264
+ }
265
+
266
+ return { amount, count };
267
+ }
268
+
269
+ async fetchStageRows(): Promise<Record<string, unknown>[]> {
270
+ const defaultEntityTypeApiKey =
271
+ this.state.defaultEntityTypeApiKey ?? 'defaultBusiType';
272
+
273
+ const res = await request({
274
+ url: STAGE_LIST_URL,
275
+ method: 'POST',
276
+ data: {
277
+ data: {
278
+ entityTypeApiKey: defaultEntityTypeApiKey,
279
+ },
280
+ },
281
+ });
282
+
283
+ return res.data || [];
284
+ }
285
+
286
+ buildStagesFromApi(
287
+ rows: any[], // 阶段列表
288
+ opportunityList: Record<string, unknown>[], // 商机列表
289
+ historyOpportunityList: unknown[], // 历史商机:SQL 二维数组行
290
+ ): StageTab[] {
291
+ console.log(
292
+ '[StageSwitch__c] buildStagesFromApi:',
293
+ rows,
294
+ opportunityList,
295
+ historyOpportunityList,
296
+ );
297
+ return rows.map((row, index) => {
298
+ const stageName = row.stageName;
299
+ const { amount, count } = this.getOpportunityStats(
300
+ stageName,
301
+ opportunityList,
302
+ );
303
+ const key = String(
304
+ row.id ?? row.apiKey ?? (stageName || `stage-${index}`),
305
+ );
306
+
307
+ // 根据商机ID,从历史商机列表中找到对应的商机
308
+ const { amount: historyAmount, count: historyCount } =
309
+ this.getHistoryOpportunityStats(key, historyOpportunityList);
310
+
311
+ const tab: StageTab = {
312
+ key,
313
+ name: stageName,
314
+ amount: formatAmountDisplay(amount),
315
+ count: count,
316
+ changes: {
317
+ amount: formatAmountDisplay(historyAmount),
318
+ count: historyCount,
319
+ amountDirection:
320
+ historyAmount < amount
321
+ ? 'up'
322
+ : historyAmount == amount
323
+ ? 'same'
324
+ : 'down',
325
+ countDirection:
326
+ historyCount < count
327
+ ? 'up'
328
+ : historyCount == count
329
+ ? 'same'
330
+ : 'down',
331
+ },
332
+ };
333
+ return tab;
334
+ });
335
+ }
336
+
337
+ async fetchAllData() {
338
+ this.setState({ loading: true, error: null });
339
+
340
+ try {
341
+ const [opportunityList, stageRows, historyOpportunityList] =
342
+ await Promise.all([
343
+ this.fetchOpportunityList(),
344
+ this.fetchStageRows(),
345
+ this.fetchHistoryOpportunityList(),
346
+ ]);
347
+
348
+ const stages = this.buildStagesFromApi(
349
+ stageRows,
350
+ opportunityList,
351
+ historyOpportunityList,
352
+ );
353
+
354
+ this.setState({
355
+ opportunityList,
356
+ stages,
357
+ loading: false,
358
+ });
359
+ } catch (e: unknown) {
360
+ console.error('StageSwitch 加载失败:', e);
361
+ const msg = e instanceof Error ? e.message : '加载失败';
362
+ this.setState({
363
+ loading: false,
364
+ error: msg,
365
+ opportunityList: [],
366
+ stages: [],
367
+ });
368
+ }
369
+ }
370
+
371
+ // 点击阶段切换时,更新当前激活的销售阶段
372
+ handleStageClick(stage: StageTab) {
373
+ const { onStageChange } = this.props;
374
+ if (onStageChange) {
375
+ onStageChange(stage);
376
+ }
377
+ // 广播事件:更新当前激活的销售阶段
378
+ const activeStage = extractStageKeyFromStageName(stage.name);
379
+ this.setState({ activeStage: activeStage });
380
+
381
+ this.onActiveStageChange({
382
+ activeStage,
383
+ });
384
+
385
+ // 触发一个广播事件
386
+ // NeoEvent.broadcast('updateActiveStage', activeStage);
387
+ }
388
+
389
+ @NeoEvent.function
390
+ async refreshData() {
391
+ await this.fetchAllData();
392
+ }
393
+
394
+ /**
395
+ * 与 pipelineFunnel 一致:传入 { relation, filter },将重新拉取商机并合并阶段统计。
396
+ */
397
+ @NeoEvent.function
398
+ setFilter(filter?: any) {
399
+ if (isEqual(filter, this.state.filter)) {
400
+ return;
401
+ }
402
+ let defaultEntityTypeApiKey = this.state.defaultEntityTypeApiKey;
403
+ if (filter.businessTypeApiKey) {
404
+ defaultEntityTypeApiKey = filter.businessTypeApiKey;
405
+ }
406
+ console.log('[StageSwitch__c] setFilter:', filter);
407
+ this.setState(
408
+ { filter: filter, defaultEntityTypeApiKey: defaultEntityTypeApiKey },
409
+ () => {
410
+ this.fetchAllData();
411
+ },
412
+ );
413
+ }
414
+
415
+ /**
416
+ * 更新当前激活的销售阶段
417
+ */
418
+ @NeoEvent.function
419
+ updateActiveStage(activeStage?: string) {
420
+ if (isEqual(activeStage, this.state.activeStage)) {
421
+ return;
422
+ }
423
+ console.log('[StageSwitch__c] updateActiveStage:', activeStage);
424
+ this.setState({ activeStage: activeStage || '' });
425
+ }
426
+
427
+ @NeoEvent.dispatch
428
+ onActiveStageChange(eventData?: any) {}
429
+
430
+ render() {
431
+ const { className, style } = this.props;
432
+ const { activeStage, stages, loading, error } = this.state;
433
+
434
+ return (
435
+ <div
436
+ className={`stageSwitch__c ${className || ''}`}
437
+ style={style}
438
+ data-time="2026.4.15 01"
439
+ >
440
+ <Spin spinning={loading}>
441
+ {error ? (
442
+ <div
443
+ className="stageSwitch__c-error"
444
+ style={{ color: '#cf1322', fontSize: 12, marginBottom: 8 }}
445
+ >
446
+ {error}
447
+ </div>
448
+ ) : null}
449
+ <div className="stage-tabs">
450
+ {stages.map((stage) => (
451
+ <div
452
+ key={stage.key}
453
+ className={`stage-tab ${
454
+ activeStage === extractStageKeyFromStageName(stage.name)
455
+ ? 'active'
456
+ : ''
457
+ }`}
458
+ onClick={() => this.handleStageClick(stage)}
459
+ >
460
+ <div className="stage-name">{stage.name}</div>
461
+ <div className="stage-amount-row">
462
+ <span className="stage-amount">{stage.amount}</span>
463
+ <span className="stage-count">({stage.count})</span>
464
+ </div>
465
+ {stage.changes && Object.keys(stage.changes).length > 0 && (
466
+ <div className="stage-sub">
467
+ <span
468
+ className={`change-amount ${
469
+ stage.changes.amountDirection === 'up'
470
+ ? 'positive'
471
+ : stage.changes.amountDirection === 'down'
472
+ ? 'negative'
473
+ : ''
474
+ }`}
475
+ >
476
+ {stage.changes.amountDirection === 'up'
477
+ ? '↑'
478
+ : stage.changes.amountDirection === 'same'
479
+ ? ''
480
+ : '↓'}{' '}
481
+ {stage.changes.amount}
482
+ </span>
483
+ <span> &nbsp; </span>
484
+ <span
485
+ className={`change-count ${
486
+ stage.changes.countDirection === 'up'
487
+ ? 'positive'
488
+ : stage.changes.amountDirection === 'down'
489
+ ? 'negative'
490
+ : ''
491
+ }`}
492
+ >
493
+ {stage.changes.countDirection === 'up'
494
+ ? '↑'
495
+ : stage.changes.countDirection === 'same'
496
+ ? ''
497
+ : '↓'}{' '}
498
+ {stage.changes.count}
499
+ </span>
500
+ </div>
501
+ )}
502
+ </div>
503
+ ))}
504
+ </div>
505
+ </Spin>
506
+ </div>
507
+ );
508
+ }
509
+ }
510
+
511
+ export default StageSwitch;
@@ -0,0 +1,70 @@
1
+ export class StageSwitchModel {
2
+ label: string = '阶段切换卡片';
3
+ description: string = '切换显示不同销售阶段的数据,包含金额、数量和变化趋势';
4
+ iconUrl: string = 'https://custom-widgets.bj.bcebos.com/card.svg';
5
+ targetPage: string[] = ['all'];
6
+ targetDevice: string = 'all';
7
+
8
+ defaultComProps = {
9
+ /** 请求阶段列表接口时传入的 defaultEntityTypeApiKey */
10
+ // defaultEntityTypeApiKey: 'defaultBusiType',
11
+ stages: [] as unknown[],
12
+ activeStage: 'Prospecting',
13
+ };
14
+
15
+ events = [
16
+ {
17
+ apiKey: 'onActiveStageChange',
18
+ label: '当前激活阶段变动后',
19
+ helpText:
20
+ '当前激活阶段变动后触发;事件参数含 activeStage(当前激活阶段)',
21
+ eventParams:
22
+ '[{"apiKey":"eventParam","children":[{"apiKey":"activeStage","label":"当前激活阶段","type":"String"}],"label":"事件入参","type":"Object"}]',
23
+ },
24
+ ];
25
+
26
+ functions = [
27
+ {
28
+ apiKey: 'updateActiveStage',
29
+ label: '更新当前激活阶段',
30
+ helpTextKey: '设置当前激活的销售阶段',
31
+ funcInParams: [
32
+ {
33
+ apiKey: 'activeStage',
34
+ label: '当前激活阶段',
35
+ type: 'String',
36
+ required: false,
37
+ },
38
+ ],
39
+ },
40
+ {
41
+ apiKey: 'setFilter',
42
+ label: '设置过滤条件',
43
+ helpTextKey:
44
+ '设置与图表一致的 filter(closeDate / ownerId / entityType),并重新拉取商机',
45
+ funcInParams: [
46
+ {
47
+ apiKey: 'filter',
48
+ label: '过滤条件(relation + filter 数组)',
49
+ type: 'Object',
50
+ required: false,
51
+ },
52
+ ],
53
+ },
54
+ {
55
+ apiKey: 'refreshData',
56
+ label: '刷新数据',
57
+ helpTextKey: '重新拉取商机列表与阶段列表',
58
+ },
59
+ ];
60
+
61
+ propsSchema = [
62
+ {
63
+ type: 'panelInput',
64
+ name: 'defaultEntityTypeApiKey',
65
+ label: '业务类型 ApiKey(阶段列表接口)',
66
+ },
67
+ ];
68
+ }
69
+
70
+ export default StageSwitchModel;
@@ -1,8 +1,10 @@
1
- .stage-switch__c {
1
+ .stageSwitch__c {
2
+ margin-top: 12px;
3
+
2
4
  .stage-tabs {
3
5
  display: flex;
4
6
  gap: 0;
5
- margin-bottom: 20px;
7
+ margin-bottom: 12px;
6
8
  background: #fff;
7
9
  border-radius: 8px;
8
10
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);