neo-cmp-cli 1.13.12 → 1.13.13

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 (61) hide show
  1. package/dist/neo/neoService.js +1 -1
  2. package/dist/package.json.js +1 -1
  3. package/package.json +1 -1
  4. package/template/antd-custom-cmp-template/package.json +1 -1
  5. package/template/asset-manage-template/package.json +1 -1
  6. package/template/echarts-custom-cmp-template/package.json +1 -1
  7. package/template/empty-custom-cmp-template/package.json +1 -1
  8. package/template/map-custom-cmp-template/package.json +1 -1
  9. package/template/neo-bi-cmps/package.json +1 -1
  10. package/template/neo-bi-cmps/src/components/aiCommitDrawer__c/README.md +52 -0
  11. package/template/neo-bi-cmps/src/components/aiCommitDrawer__c/index.tsx +176 -0
  12. package/template/neo-bi-cmps/src/components/aiCommitDrawer__c/model.ts +49 -0
  13. package/template/neo-bi-cmps/src/components/aiCommitDrawer__c/style.scss +218 -0
  14. package/template/neo-bi-cmps/src/components/filterBar__c/README.md +35 -0
  15. package/template/neo-bi-cmps/src/components/filterBar__c/index.tsx +186 -0
  16. package/template/neo-bi-cmps/src/components/filterBar__c/model.ts +72 -0
  17. package/template/neo-bi-cmps/src/components/filterBar__c/style.scss +212 -0
  18. package/template/neo-bi-cmps/src/components/forecastChart__c/README.md +31 -0
  19. package/template/neo-bi-cmps/src/components/forecastChart__c/index.tsx +161 -0
  20. package/template/neo-bi-cmps/src/components/forecastChart__c/model.ts +39 -0
  21. package/template/neo-bi-cmps/src/components/forecastChart__c/style.scss +154 -0
  22. package/template/neo-bi-cmps/src/components/forecastGrid__c/README.md +36 -0
  23. package/template/neo-bi-cmps/src/components/forecastGrid__c/index.tsx +86 -0
  24. package/template/neo-bi-cmps/src/components/forecastGrid__c/model.ts +34 -0
  25. package/template/neo-bi-cmps/src/components/forecastGrid__c/style.scss +48 -0
  26. package/template/neo-bi-cmps/src/components/gapCloser__c/README.md +24 -0
  27. package/template/neo-bi-cmps/src/components/gapCloser__c/index.tsx +95 -0
  28. package/template/neo-bi-cmps/src/components/gapCloser__c/model.ts +43 -0
  29. package/template/neo-bi-cmps/src/components/gapCloser__c/style.scss +60 -0
  30. package/template/neo-bi-cmps/src/components/kpiCards__c/README.md +35 -0
  31. package/template/neo-bi-cmps/src/components/kpiCards__c/index.tsx +70 -0
  32. package/template/neo-bi-cmps/src/components/kpiCards__c/model.ts +35 -0
  33. package/template/neo-bi-cmps/src/components/kpiCards__c/style.scss +33 -0
  34. package/template/neo-bi-cmps/src/components/oppList__c/README.md +52 -0
  35. package/template/neo-bi-cmps/src/components/oppList__c/index.tsx +228 -0
  36. package/template/neo-bi-cmps/src/components/oppList__c/model.ts +40 -0
  37. package/template/neo-bi-cmps/src/components/oppList__c/style.scss +133 -0
  38. package/template/neo-bi-cmps/src/components/pipelineFunnel__c/README.md +39 -0
  39. package/template/neo-bi-cmps/src/components/pipelineFunnel__c/index.tsx +128 -0
  40. package/template/neo-bi-cmps/src/components/pipelineFunnel__c/model.ts +42 -0
  41. package/template/neo-bi-cmps/src/components/pipelineFunnel__c/style.scss +133 -0
  42. package/template/neo-bi-cmps/src/components/stageSwitch__c/README.md +36 -0
  43. package/template/neo-bi-cmps/src/components/stageSwitch__c/index.tsx +103 -0
  44. package/template/neo-bi-cmps/src/components/stageSwitch__c/model.ts +37 -0
  45. package/template/neo-bi-cmps/src/components/stageSwitch__c/style.scss +89 -0
  46. package/template/neo-bi-cmps/src/components/stageTimeChart__c/README.md +37 -0
  47. package/template/neo-bi-cmps/src/components/stageTimeChart__c/index.tsx +126 -0
  48. package/template/neo-bi-cmps/src/components/stageTimeChart__c/model.ts +35 -0
  49. package/template/neo-bi-cmps/src/components/stageTimeChart__c/style.scss +140 -0
  50. package/template/neo-bi-cmps/src/components/tabSwitch__c/README.md +37 -0
  51. package/template/neo-bi-cmps/src/components/tabSwitch__c/index.tsx +80 -0
  52. package/template/neo-bi-cmps/src/components/tabSwitch__c/model.ts +45 -0
  53. package/template/neo-bi-cmps/src/components/tabSwitch__c/style.scss +37 -0
  54. package/template/neo-custom-cmp-template/package.json +1 -1
  55. package/template/neo-h5-cmps/package.json +1 -1
  56. package/template/neo-order-cmps/package.json +1 -1
  57. package/template/neo-web-entity-grid/package.json +1 -1
  58. package/template/neo-web-form/package.json +1 -1
  59. package/template/react-custom-cmp-template/package.json +1 -1
  60. package/template/react-ts-custom-cmp-template/package.json +1 -1
  61. package/template/vue2-custom-cmp-template/package.json +1 -1
@@ -0,0 +1,35 @@
1
+ export class KpiCardsModel {
2
+ label: string = 'KPI指标卡片';
3
+ description: string = '展示Quota、Closed、Forecast、AI Forecast等核心KPI指标';
4
+ iconUrl: string = 'https://custom-widgets.bj.bcebos.com/kpiCards.svg';
5
+ targetPage: string[] = ['all'];
6
+ targetDevice: string = 'all';
7
+
8
+ defaultComProps = {
9
+ columns: 2,
10
+ items: [
11
+ { label: 'Quota', value: '$10,000,000' },
12
+ { label: 'Closed', value: '$3,200,000', subLabel: 'Gap: $6,800,000', subDirection: 'negative' },
13
+ { label: 'Forecast', value: '$7,500,000', subLabel: 'Gap: $2,500,000', subDirection: 'negative' },
14
+ { label: '✨ AI Forecast', value: '$6,800,000', subLabel: 'Gap: $3,200,000', subDirection: 'negative' },
15
+ ],
16
+ };
17
+
18
+ functions = [
19
+ {
20
+ apiKey: 'refreshData',
21
+ label: '刷新数据',
22
+ helpTextKey: '刷新KPI指标数据',
23
+ },
24
+ ];
25
+
26
+ propsSchema = [
27
+ {
28
+ type: 'number',
29
+ name: 'columns',
30
+ label: '列数',
31
+ },
32
+ ];
33
+ }
34
+
35
+ export default KpiCardsModel;
@@ -0,0 +1,33 @@
1
+ .kpi-cards__c {
2
+ .kpi-card {
3
+ background: #fff;
4
+ border-radius: 8px;
5
+ padding: 16px 20px;
6
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
7
+
8
+ .kpi-label {
9
+ font-size: 12px;
10
+ color: #888;
11
+ margin-bottom: 4px;
12
+ }
13
+
14
+ .kpi-value {
15
+ font-size: 22px;
16
+ font-weight: 700;
17
+ }
18
+
19
+ .kpi-sub {
20
+ font-size: 12px;
21
+ color: #999;
22
+ margin-top: 4px;
23
+
24
+ &.negative {
25
+ color: #ef4444;
26
+ }
27
+
28
+ &.positive {
29
+ color: #22c55e;
30
+ }
31
+ }
32
+ }
33
+ }
@@ -0,0 +1,52 @@
1
+ # OppList 组件
2
+
3
+ 商机列表组件,展示商机数据表格,支持排序、筛选、Tab切换等功能。
4
+
5
+ ## 使用方式
6
+
7
+ ```tsx
8
+ import OppList from './components/oppList__c';
9
+
10
+ <OppList
11
+ title="Opportunity List"
12
+ tabs={[
13
+ { key: 'watch', label: '✨ Prioritized Deals' },
14
+ { key: 'all', label: 'All Opportunities' },
15
+ ]}
16
+ filterChips={['All (3)', 'Rescue (1)', 'Upgrade (2)']}
17
+ columns={['Name', 'Amount', 'Close Date', 'Stage']}
18
+ rows={[...]}
19
+ onRowClick={(opp) => console.log('Clicked:', opp)}
20
+ />
21
+ ```
22
+
23
+ ## Props
24
+
25
+ | 属性 | 说明 | 类型 | 默认值 |
26
+ |------|------|------|--------|
27
+ | title | 标题 | string | 'Opportunity List' |
28
+ | summaryText | 摘要文本 | string | - |
29
+ | tabs | Tab配置 | OppTab[] | [] |
30
+ | activeTab | 当前Tab | string | 第一个Tab |
31
+ | filterChips | 筛选 Chips | string[] | [] |
32
+ | columns | 表头列 | string[] | [] |
33
+ | rows | 商机数据 | Opportunity[] | [] |
34
+ | onTabChange | Tab切换回调 | (tab: string) => void | - |
35
+ | onChipChange | Chip切换回调 | (chip: string) => void | - |
36
+ | onSort | 排序回调 | (column: string) => void | - |
37
+ | onRowClick | 行点击回调 | (opp: Opportunity) => void | - |
38
+
39
+ ## Opportunity
40
+
41
+ | 属性 | 说明 | 类型 |
42
+ |------|------|------|
43
+ | name | 商机名称 | string |
44
+ | account | 账户 | string |
45
+ | forecastType | 预测类型 | string |
46
+ | amount | 金额 | string |
47
+ | closeDate | 关闭日期 | string |
48
+ | stage | 阶段 | string |
49
+ | aiScore | AI评分 | string |
50
+ | aiWinRate | AI赢率 | string |
51
+ | lastActivity | 最近活动 | string |
52
+ | source | 来源 | 'rescue' \| 'upgrade' \| 'all' |
@@ -0,0 +1,228 @@
1
+ /**
2
+ * @file 商机列表组件
3
+ * @description 展示商机数据表格,支持排序、筛选、Tab切换等功能
4
+ */
5
+ import * as React from 'react';
6
+ // @ts-ignore
7
+ import { BaseCmp, StatusHoc, NeoEvent } from 'neo-ui-common';
8
+
9
+ import './style.scss';
10
+
11
+ interface OppTab {
12
+ key: string;
13
+ label: string;
14
+ icon?: string;
15
+ }
16
+
17
+ interface Opportunity {
18
+ name: string;
19
+ account?: string;
20
+ forecastType?: string;
21
+ amount: string;
22
+ closeDate: string;
23
+ stage: string;
24
+ aiScore?: string;
25
+ aiScoreColor?: string;
26
+ aiWinRate?: string;
27
+ lastActivity?: string;
28
+ source?: 'rescue' | 'upgrade' | 'all';
29
+ }
30
+
31
+ interface OppListProps {
32
+ title?: string;
33
+ summaryText?: string;
34
+ tabs?: OppTab[];
35
+ activeTab?: string;
36
+ filterChips?: string[];
37
+ activeChip?: string;
38
+ columns?: string[];
39
+ rows?: Opportunity[];
40
+ onTabChange?: (tab: string) => void;
41
+ onChipChange?: (chip: string) => void;
42
+ onSort?: (column: string) => void;
43
+ onRowClick?: (opp: Opportunity) => void;
44
+ className?: string;
45
+ style?: React.CSSProperties;
46
+ }
47
+
48
+ interface OppListState {
49
+ activeTab: string;
50
+ activeChip: string;
51
+ sortColumn?: string;
52
+ sortDirection?: 'asc' | 'desc';
53
+ }
54
+
55
+ class OppList extends BaseCmp<OppListProps, OppListState> {
56
+ constructor(props: OppListProps) {
57
+ super(props);
58
+ this.state = {
59
+ activeTab: props.activeTab || (props.tabs?.[0]?.key ?? 'watch'),
60
+ activeChip: props.activeChip || (props.filterChips?.[0] ?? 'all'),
61
+ sortColumn: undefined,
62
+ sortDirection: undefined,
63
+ };
64
+ }
65
+
66
+ componentDidMount() {
67
+ console.log('OppList 组件挂载');
68
+ }
69
+
70
+ handleTabChange = (tab: string) => {
71
+ this.setState({ activeTab: tab });
72
+ const { onTabChange } = this.props;
73
+ if (onTabChange) {
74
+ onTabChange(tab);
75
+ }
76
+ };
77
+
78
+ handleChipChange = (chip: string) => {
79
+ this.setState({ activeChip: chip });
80
+ const { onChipChange } = this.props;
81
+ if (onChipChange) {
82
+ onChipChange(chip);
83
+ }
84
+ };
85
+
86
+ handleSort = (column: string) => {
87
+ const { sortColumn, sortDirection } = this.state;
88
+ let newDirection: 'asc' | 'desc' = 'asc';
89
+
90
+ if (sortColumn === column) {
91
+ newDirection = sortDirection === 'asc' ? 'desc' : 'asc';
92
+ }
93
+
94
+ this.setState({ sortColumn: column, sortDirection: newDirection });
95
+ const { onSort } = this.props;
96
+ if (onSort) {
97
+ onSort(column);
98
+ }
99
+ };
100
+
101
+ getScoreBadgeStyle = (score: string, color?: string) => {
102
+ const bgColor = color || '#22c55e';
103
+ return {
104
+ backgroundColor: bgColor,
105
+ color: '#fff',
106
+ padding: '2px 8px',
107
+ borderRadius: '10px',
108
+ fontSize: '11px',
109
+ };
110
+ };
111
+
112
+ filterRows = () => {
113
+ const { rows = [] } = this.props;
114
+ const { activeChip } = this.state;
115
+
116
+ if (activeChip === 'all') {
117
+ return rows;
118
+ }
119
+
120
+ return rows.filter((row) => row.source === activeChip);
121
+ };
122
+
123
+ render() {
124
+ const {
125
+ title = 'Opportunity List',
126
+ summaryText,
127
+ tabs = [],
128
+ filterChips = [],
129
+ columns = [],
130
+ className,
131
+ style,
132
+ } = this.props;
133
+
134
+ const { activeTab, activeChip, sortColumn, sortDirection } = this.state;
135
+ const filteredRows = this.filterRows();
136
+
137
+ return (
138
+ <div className={`opp-list__c ${className || ''}`} style={style}>
139
+ {/* Tab切换 */}
140
+ {tabs.length > 0 && (
141
+ <div className="opp-tabs">
142
+ {tabs.map((tab) => (
143
+ <button
144
+ key={tab.key}
145
+ className={`opp-tab-btn ${activeTab === tab.key ? 'active' : ''}`}
146
+ onClick={() => this.handleTabChange(tab.key)}
147
+ >
148
+ {tab.icon} {tab.label}
149
+ </button>
150
+ ))}
151
+ </div>
152
+ )}
153
+
154
+ {/* 筛选Chips */}
155
+ {filterChips.length > 0 && (
156
+ <div className="filter-chips">
157
+ {filterChips.map((chip, index) => (
158
+ <button
159
+ key={index}
160
+ className={`watch-sub-tab ${activeChip === chip.split('(')[0].trim().toLowerCase() ? 'active' : ''}`}
161
+ onClick={() => this.handleChipChange(chip.split('(')[0].trim().toLowerCase())}
162
+ >
163
+ {chip}
164
+ </button>
165
+ ))}
166
+ </div>
167
+ )}
168
+
169
+ {/* 标题和摘要 */}
170
+ <div className="list-header">
171
+ <h3 className="list-title">{title}</h3>
172
+ {summaryText && <div className="list-summary">{summaryText}</div>}
173
+ </div>
174
+
175
+ {/* 表格 */}
176
+ <div className="table-wrapper">
177
+ <table className="opp-table">
178
+ <thead>
179
+ <tr>
180
+ {columns.map((col, index) => (
181
+ <th key={index} style={{ textAlign: col === 'Amount' ? 'right' : col === 'Close Date' || col === 'Stage' || col === 'Last Activity' || col === 'AI Score' || col === 'AI Win Rate' ? 'center' : 'left' }}>
182
+ {col}
183
+ <span
184
+ className="sort-icon"
185
+ onClick={() => this.handleSort(col)}
186
+ style={{ color: sortColumn === col ? '#6366f1' : '#ccc', marginLeft: '4px' }}
187
+ >
188
+ {sortColumn === col ? (sortDirection === 'asc' ? '↑' : '↓') : '⇅'}
189
+ </span>
190
+ </th>
191
+ ))}
192
+ </tr>
193
+ </thead>
194
+ <tbody>
195
+ {filteredRows.map((opp, index) => (
196
+ <tr key={index} className={opp.source ? `opp-row source-${opp.source}` : ''}>
197
+ <td style={{ textAlign: 'left', color: '#3b82f6', cursor: 'pointer' }}>{opp.name}</td>
198
+ {opp.account && <td style={{ textAlign: 'left' }}>{opp.account}</td>}
199
+ {opp.forecastType && <td style={{ textAlign: 'left' }}>{opp.forecastType}</td>}
200
+ <td style={{ textAlign: 'right' }}>{opp.amount}</td>
201
+ <td style={{ textAlign: 'center' }}>{opp.closeDate}</td>
202
+ <td style={{ textAlign: 'center' }}>{opp.stage}</td>
203
+ {opp.aiScore && (
204
+ <td style={{ textAlign: 'center' }}>
205
+ <span style={this.getScoreBadgeStyle(opp.aiScore, opp.aiScoreColor)}>
206
+ {opp.aiScore}
207
+ </span>
208
+ </td>
209
+ )}
210
+ {opp.aiWinRate && (
211
+ <td style={{ textAlign: 'center', color: '#6366f1', fontWeight: 600 }}>
212
+ {opp.aiWinRate}
213
+ </td>
214
+ )}
215
+ {opp.lastActivity && (
216
+ <td style={{ textAlign: 'center', color: '#999' }}>{opp.lastActivity}</td>
217
+ )}
218
+ </tr>
219
+ ))}
220
+ </tbody>
221
+ </table>
222
+ </div>
223
+ </div>
224
+ );
225
+ }
226
+ }
227
+
228
+ export default StatusHoc(OppList);
@@ -0,0 +1,40 @@
1
+ export class OppListModel {
2
+ label: string = '商机列表';
3
+ description: string = '展示商机数据表格,支持排序、筛选、Tab切换等功能';
4
+ iconUrl: string = 'https://custom-widgets.bj.bcebos.com/oppList.svg';
5
+ targetPage: string[] = ['all'];
6
+ targetDevice: string = 'all';
7
+
8
+ defaultComProps = {
9
+ title: 'Opportunity List',
10
+ tabs: [
11
+ { key: 'watch', label: '✨ Prioritized Deals', icon: '' },
12
+ { key: 'all', label: 'All Opportunities', icon: '' },
13
+ ],
14
+ filterChips: ['All (3)', 'Rescue (1)', 'Upgrade (2)'],
15
+ columns: ['Opportunity Name', 'Account', 'Forecast Type', 'Amount', 'Close Date', 'Stage', '✨ AI Score', '✨ AI Win Rate', 'Last Activity'],
16
+ rows: [
17
+ { name: 'Apollo Project', account: 'Huawei Tech', forecastType: 'Commit', amount: '$1,000,000', closeDate: '2026-04-15', stage: 'Negotiation', aiScore: 'Medium', aiScoreColor: '#f59e0b', aiWinRate: '42%', lastActivity: '2026-03-28', source: 'rescue' },
18
+ { name: 'Aurora Solution', account: 'ByteDance', forecastType: 'Best Case', amount: '$800,000', closeDate: '2026-04-30', stage: 'Proposal', aiScore: 'High', aiScoreColor: '#22c55e', aiWinRate: '91%', lastActivity: '2026-03-29', source: 'upgrade' },
19
+ { name: 'Cloud Migration', account: 'Tencent', forecastType: 'Pipeline', amount: '$1,700,000', closeDate: '2026-05-20', stage: 'Discovery', aiScore: 'Medium', aiScoreColor: '#f59e0b', aiWinRate: '78%', lastActivity: '2026-03-27', source: 'upgrade' },
20
+ ],
21
+ };
22
+
23
+ functions = [
24
+ {
25
+ apiKey: 'refreshData',
26
+ label: '刷新数据',
27
+ helpTextKey: '刷新商机列表数据',
28
+ },
29
+ ];
30
+
31
+ propsSchema = [
32
+ {
33
+ type: 'string',
34
+ name: 'title',
35
+ label: '标题',
36
+ },
37
+ ];
38
+ }
39
+
40
+ export default OppListModel;
@@ -0,0 +1,133 @@
1
+ .opp-list__c {
2
+ background: #fff;
3
+ border-radius: 8px;
4
+ padding: 20px;
5
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
6
+ overflow-x: auto;
7
+
8
+ .opp-tabs {
9
+ display: flex;
10
+ gap: 0;
11
+ margin-bottom: 16px;
12
+ border-bottom: 2px solid #e5e7eb;
13
+ }
14
+
15
+ .opp-tab-btn {
16
+ padding: 8px 20px;
17
+ font-size: 13px;
18
+ font-weight: 600;
19
+ cursor: pointer;
20
+ border: none;
21
+ background: none;
22
+ color: #999;
23
+ border-bottom: 2px solid transparent;
24
+ margin-bottom: -2px;
25
+ transition: all 0.2s;
26
+
27
+ &:hover {
28
+ color: #6366f1;
29
+ }
30
+
31
+ &.active {
32
+ color: #6366f1;
33
+ border-bottom-color: #6366f1;
34
+ }
35
+ }
36
+
37
+ .filter-chips {
38
+ display: flex;
39
+ gap: 8px;
40
+ margin-bottom: 16px;
41
+ }
42
+
43
+ .watch-sub-tab {
44
+ padding: 6px 14px;
45
+ font-size: 12px;
46
+ font-weight: 600;
47
+ cursor: pointer;
48
+ border: 1px solid #ddd;
49
+ background: #fff;
50
+ color: #666;
51
+ border-radius: 20px;
52
+ transition: all 0.2s;
53
+
54
+ &:hover {
55
+ border-color: #6366f1;
56
+ }
57
+
58
+ &.active {
59
+ border: 1px solid #6366f1;
60
+ background: #f0f0ff;
61
+ color: #6366f1;
62
+ }
63
+ }
64
+
65
+ .list-header {
66
+ display: flex;
67
+ justify-content: space-between;
68
+ align-items: center;
69
+ margin-bottom: 12px;
70
+ }
71
+
72
+ .list-title {
73
+ font-size: 14px;
74
+ font-weight: 600;
75
+ margin: 0;
76
+ }
77
+
78
+ .list-summary {
79
+ font-size: 13px;
80
+ color: #666;
81
+
82
+ span {
83
+ font-weight: 700;
84
+ color: #333;
85
+ }
86
+ }
87
+
88
+ .table-wrapper {
89
+ overflow-x: auto;
90
+ }
91
+
92
+ .opp-table {
93
+ width: 100%;
94
+ border-collapse: collapse;
95
+ font-size: 13px;
96
+
97
+ th,
98
+ td {
99
+ padding: 10px 12px;
100
+ border-bottom: 1px solid #f0f0f0;
101
+ }
102
+
103
+ th {
104
+ background: #f8f9fa;
105
+ font-weight: 600;
106
+ color: #333;
107
+ border-bottom: 2px solid #e5e7eb;
108
+ user-select: none;
109
+ }
110
+
111
+ td {
112
+ color: #333;
113
+ }
114
+
115
+ tbody tr {
116
+ transition: background 0.15s;
117
+ border-bottom: 1px solid #f0f0f0;
118
+
119
+ &:hover {
120
+ background: #fafafa;
121
+ }
122
+ }
123
+
124
+ .sort-icon {
125
+ cursor: pointer;
126
+ font-size: 10px;
127
+
128
+ &:hover {
129
+ color: #6366f1 !important;
130
+ }
131
+ }
132
+ }
133
+ }
@@ -0,0 +1,39 @@
1
+ # PipelineFunnel 组件
2
+
3
+ Pipeline漏斗图组件,展示销售管道的漏斗转化情况。
4
+
5
+ ## 使用方式
6
+
7
+ ```tsx
8
+ import PipelineFunnel from './components/pipelineFunnel__c';
9
+
10
+ <PipelineFunnel
11
+ title="Pipeline Funnel"
12
+ totalAmount="$11.1M"
13
+ stages={[
14
+ { name: 'Prospecting', amount: '$3.2M', count: 15, color: '#3b82f6' },
15
+ { name: 'Needs Analysis', amount: '$1.8M', count: 7, conversionRate: '56.3%', color: '#22c55e' },
16
+ ]}
17
+ onStageClick={(name) => console.log('Clicked:', name)}
18
+ />
19
+ ```
20
+
21
+ ## Props
22
+
23
+ | 属性 | 说明 | 类型 | 默认值 |
24
+ |------|------|------|--------|
25
+ | title | 标题 | string | 'Pipeline Funnel' |
26
+ | totalAmount | 总金额 | string | '$11.1M' |
27
+ | stages | 漏斗阶段数据 | FunnelStage[] | [] |
28
+ | showAiButton | 显示AI按钮 | boolean | true |
29
+ | onStageClick | 阶段点击回调 | (stageName: string) => void | - |
30
+
31
+ ## FunnelStage
32
+
33
+ | 属性 | 说明 | 类型 |
34
+ |------|------|------|
35
+ | name | 阶段名称 | string |
36
+ | amount | 金额 | string |
37
+ | count | 数量 | number |
38
+ | conversionRate | 转化率 | string |
39
+ | color | 颜色 | string |
@@ -0,0 +1,128 @@
1
+ /**
2
+ * @file Pipeline漏斗图组件
3
+ * @description 展示销售管道的漏斗转化情况
4
+ */
5
+ import * as React from 'react';
6
+ // @ts-ignore
7
+ import { BaseCmp, StatusHoc, NeoEvent } from 'neo-ui-common';
8
+
9
+ import './style.scss';
10
+
11
+ interface FunnelStage {
12
+ name: string;
13
+ amount: string;
14
+ count: number;
15
+ conversionRate?: string;
16
+ color: string;
17
+ }
18
+
19
+ interface PipelineFunnelProps {
20
+ title?: string;
21
+ totalAmount?: string;
22
+ stages?: FunnelStage[];
23
+ showAiButton?: boolean;
24
+ onStageClick?: (stageName: string) => void;
25
+ className?: string;
26
+ style?: React.CSSProperties;
27
+ }
28
+
29
+ interface PipelineFunnelState {
30
+ loading: boolean;
31
+ }
32
+
33
+ class PipelineFunnel extends BaseCmp<PipelineFunnelProps, PipelineFunnelState> {
34
+ constructor(props: PipelineFunnelProps) {
35
+ super(props);
36
+ this.state = {
37
+ loading: false,
38
+ };
39
+ }
40
+
41
+ componentDidMount() {
42
+ console.log('PipelineFunnel 组件挂载');
43
+ }
44
+
45
+ getFunnelClipPath = (index: number, total: number): string => {
46
+ const offset = index * 10;
47
+ return `polygon(${offset}% 0%, ${100 - offset}% 0%, ${100 - offset - 10}% 100%, ${offset + 10}% 100%)`;
48
+ };
49
+
50
+ render() {
51
+ const {
52
+ title = 'Pipeline Funnel',
53
+ totalAmount = '$11.1M',
54
+ stages = [],
55
+ showAiButton = true,
56
+ onStageClick,
57
+ className,
58
+ style,
59
+ } = this.props;
60
+
61
+ return (
62
+ <div className={`pipeline-funnel__c ${className || ''}`} style={style}>
63
+ <div className="funnel-header">
64
+ <h3 className="funnel-title">{title}</h3>
65
+ {showAiButton && (
66
+ <span
67
+ className="ai-btn"
68
+ onClick={() => console.log('AI Analysis clicked')}
69
+ title="AI Analysis"
70
+ >
71
+
72
+ </span>
73
+ )}
74
+ </div>
75
+
76
+ <div className="funnel-total">
77
+ <div className="funnel-total-label">Sales Amount</div>
78
+ <div className="funnel-total-value">{totalAmount}</div>
79
+ </div>
80
+
81
+ <div className="funnel-body">
82
+ <div className="funnel-left-rates">
83
+ {stages.slice(0, -1).map((stage, index) => (
84
+ <div key={index} className="funnel-rate">
85
+ {stage.conversionRate || '—'}
86
+ </div>
87
+ ))}
88
+ </div>
89
+
90
+ <div className="funnel-main">
91
+ {stages.map((stage, index) => (
92
+ <div
93
+ key={index}
94
+ className="funnel-segment"
95
+ style={{ backgroundColor: stage.color }}
96
+ onClick={() => onStageClick?.(stage.name)}
97
+ >
98
+ {stage.name}
99
+ </div>
100
+ ))}
101
+ </div>
102
+
103
+ <div className="funnel-right-data">
104
+ {stages.map((stage, index) => (
105
+ <div key={index} className="funnel-data">
106
+ {stage.amount} / {stage.count}
107
+ </div>
108
+ ))}
109
+ </div>
110
+ </div>
111
+
112
+ <div className="funnel-legend">
113
+ {stages.map((stage, index) => (
114
+ <span key={index} className="legend-item">
115
+ <span
116
+ className="legend-dot"
117
+ style={{ backgroundColor: stage.color }}
118
+ />
119
+ {stage.name}
120
+ </span>
121
+ ))}
122
+ </div>
123
+ </div>
124
+ );
125
+ }
126
+ }
127
+
128
+ export default StatusHoc(PipelineFunnel);