neo-cmp-cli 1.13.13 → 1.13.15
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.
- package/README.md +1 -1
- package/dist/neo/neoEnvManager.js +1 -1
- package/dist/neo/neoLogin.js +1 -1
- package/dist/neo/neoRequire.js +1 -1
- package/dist/package.json.js +1 -1
- package/package.json +1 -1
- package/template/antd-custom-cmp-template/package.json +1 -1
- package/template/asset-manage-template/package.json +1 -1
- package/template/echarts-custom-cmp-template/package.json +1 -1
- package/template/empty-custom-cmp-template/package.json +1 -1
- package/template/map-custom-cmp-template/package.json +1 -1
- package/template/neo-bi-cmps/docs/gartner-pipeline-apis.md +45 -73
- package/template/neo-bi-cmps/package.json +1 -1
- package/template/neo-bi-cmps/src/components/aiCommitDrawer__c/index.tsx +17 -10
- package/template/neo-bi-cmps/src/components/aiCommitDrawer__c/model.ts +47 -6
- package/template/neo-bi-cmps/src/components/filterBar__c/index.tsx +21 -7
- package/template/neo-bi-cmps/src/components/forecastChart__c/index.tsx +6 -9
- package/template/neo-bi-cmps/src/components/forecastChart__c/model.ts +2 -1
- package/template/neo-bi-cmps/src/components/forecastGrid__c/model.ts +32 -4
- package/template/neo-bi-cmps/src/components/gapCloser__c/index.tsx +7 -2
- package/template/neo-bi-cmps/src/components/gapCloser__c/model.ts +6 -3
- package/template/neo-bi-cmps/src/components/kpiCards__c/model.ts +18 -3
- package/template/neo-bi-cmps/src/components/oppList__c/index.tsx +70 -13
- package/template/neo-bi-cmps/src/components/oppList__c/model.ts +50 -4
- package/template/neo-bi-cmps/src/components/pipelineFunnel__c/index.tsx +3 -1
- package/template/neo-bi-cmps/src/components/pipelineFunnel__c/model.ts +28 -4
- package/template/neo-bi-cmps/src/components/stageSwitch__c/index.tsx +21 -6
- package/template/neo-bi-cmps/src/components/stageSwitch__c/model.ts +60 -5
- package/template/neo-bi-cmps/src/components/stageTimeChart__c/model.ts +26 -4
- package/template/neo-custom-cmp-template/package.json +1 -1
- package/template/neo-h5-cmps/package.json +1 -1
- package/template/neo-order-cmps/package.json +1 -1
- package/template/neo-web-entity-grid/package.json +1 -1
- package/template/neo-web-form/package.json +1 -1
- package/template/neo-web-form/src/components/batchAddTable__c/index.tsx +17 -17
- package/template/react-custom-cmp-template/package.json +1 -1
- package/template/react-ts-custom-cmp-template/package.json +1 -1
- package/template/vue2-custom-cmp-template/package.json +1 -1
package/README.md
CHANGED
|
@@ -32,7 +32,7 @@ neo-cmp-cli 是 Neo 自定义组件开发工具,基于 [AKFun](https://github.
|
|
|
32
32
|
| `neo-web-entity-grid` | Web 端列表组件模板:含基础大列表、Picker 列表等示例组件 | (随 CLI 内置) |
|
|
33
33
|
| `neo-h5-cmps` | H5 端业务组件模板:含全局搜索、数据列表、数据 Tabs、打开 AI 对话页等示例组件 | [neo-h5-cmps](https://github.com/wibetter/neo-h5-cmps) |
|
|
34
34
|
| `neo` | 自定义业务组件模板:含实体表单、实体数据详情、实体数据表格等示例组件 | [neo-custom-cmp-template](https://github.com/wibetter/neo-custom-cmp-template) |
|
|
35
|
-
| `neo-bi-cmps` | 数值指标组件模板:
|
|
35
|
+
| `neo-bi-cmps` | 数值指标组件模板: 可配置展示实体数据源中指标数据 | [neo-bi-cmps](https://github.com/xsy-neoui/neo-bi-cmps) |
|
|
36
36
|
| `echarts` | ECharts 组件模板:含基于 ECharts 的图表示例组件(地图场景请使用 `amap` 模板) | [echarts-custom-cmp-template](https://github.com/wibetter/echarts-custom-cmp-template) |
|
|
37
37
|
| `antd` | Ant Design 组件模板:含数据仪表板、搜索组件等示例 | [antd-custom-cmp-template](https://github.com/wibetter/antd-custom-cmp-template) |
|
|
38
38
|
| `amap` | 地图组件模板:含基于高德地图 API 的示例组件 | [map-custom-cmp-template](https://github.com/wibetter/map-custom-cmp-template) |
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("node:fs"),t=require("node:path"),n=require("../utils/common.js"),i=require("../config/index.js"),r=require("../config/auth.config.js"),s=require("./env.js");var o,u;exports.__require=function(){if(u)return o;u=1;const c=e,g=t,{errorLog:a}=n.__require(),f=i.__require(),h=r.__require(),{DefaultNeoCrmAPI:l}=s.__require();return o=class{static getEnvFilePath(){return g.join(process.cwd(),".neo-cli","env.json")}static ensureEnvDir(){const e=g.dirname(this.getEnvFilePath());c.existsSync(e)||c.mkdirSync(e,{recursive:!0})}static getAuthConfig(){return{...l,...f.neoConfig,...h}}static getEnvConfig(){let e={};const t=this.getEnvFilePath(),n=this.getAuthConfig();if(!c.existsSync(t))return n;try{const n=c.readFileSync(t,"utf-8");e=JSON.parse(n)}catch(e){a(`读取环境配置文件失败: ${e.message||e.msg}`)}return{...n,...e}}static getLinKDebugURL(e){const t=this.getEnvConfig()
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("node:fs"),t=require("node:path"),n=require("../utils/common.js"),i=require("../config/index.js"),r=require("../config/auth.config.js"),s=require("./env.js");var o,u;exports.__require=function(){if(u)return o;u=1;const c=e,g=t,{errorLog:a}=n.__require(),f=i.__require(),h=r.__require(),{DefaultNeoCrmAPI:l}=s.__require();return o=class{static getEnvFilePath(){return g.join(process.cwd(),".neo-cli","env.json")}static ensureEnvDir(){const e=g.dirname(this.getEnvFilePath());c.existsSync(e)||c.mkdirSync(e,{recursive:!0})}static getAuthConfig(){return{...l,...f.neoConfig,...h}}static getEnvConfig(){let e={};const t=this.getEnvFilePath(),n=this.getAuthConfig();if(!c.existsSync(t))return n;try{const n=c.readFileSync(t,"utf-8");e=JSON.parse(n)}catch(e){a(`读取环境配置文件失败: ${e.message||e.msg}`)}return{...n,...e}}static getLinKDebugURL(e){const t=this.getEnvConfig();let n=t.neoBaseURL;return n.endsWith("/")&&(n=n.substring(0,n.length-1)),"web"===e?`${n}${t.pcLinkDebugUrl}`:`${n}${t.h5LinkDebugUrl}`}static setEnvConfig(e){if(!e||"object"!=typeof e)throw new Error("环境配置必须是一个对象");this.ensureEnvDir();const t={...this.getEnvConfig(),...e,updatedAt:Date.now()},n=this.getEnvFilePath();return c.writeFileSync(n,JSON.stringify(t,null,2),"utf-8"),t}static updateEnvConfig(e){return this.setEnvConfig(e)}static clearEnvConfig(){const e=this.getEnvFilePath();c.existsSync(e)&&c.unlinkSync(e)}}};
|
package/dist/neo/neoLogin.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("axios"),t=require("node:fs"),n=require("node:path"),s=require("ora"),r=require("node:http"),o=require("node:url"),i=require("open"),a=require("node:net"),c=require("lodash"),l=require("../utils/common.js"),h=require("./neoEnvManager.js");var p,u;exports.__require=function(){if(u)return p;u=1;const d=e,g=t,m=n,y=s,k=r,_=o,f=i,T=a,w=c,{errorLog:U,successLog:R}=l.__require(),$=h.__require();return p=class{constructor(e){if(this.tokenDir=m.join(process.cwd(),".neo-cli"),this.tokenFile=m.join(this.tokenDir,"token.json"),this.pageDir=m.join(__dirname,"../../template/pageHtml"),this.authErrorTemplate=m.join(this.pageDir,"auth-error.html"),this.authSuccessTemplate=m.join(this.pageDir,"auth-success.html"),this.tokenErrorTemplate=m.join(this.pageDir,"token-error.html"),this.authFailedTemplate=m.join(this.pageDir,"auth-failed.html"),!e)return;const{neoBaseURL:t,loginURL:n,tokenURL:s}=e,r=$.getAuthConfig(),{redirectUri:o,authType:i,auth:a}=r;if(!n||!s)throw new Error("neo.config.js 配置不完整,需要包含 loginURL、tokenURL");this.neoBaseURL=t,this.loginURL=n,this.tokenURL=s,this.NeoCrmAPI=r,this.redirectUri=o,this.authType=i||"oauth2",this.response_type=a.response_type||"code",this.client_id=a.client_id,this.client_secret=a.client_secret,this.scope=a.scope||"all",this.oauthType=a.oauthType||"standard",this.access_type=a.access_type||"offline",this.grant_type=a.grant_type||"authorization_code";try{$.setEnvConfig(w.pick(e,["neoBaseURL","loginURL","tokenURL"]))}catch(e){U(`保存环境配置失败: ${e.message||e.msg}`)}}ensureTokenDir(){g.existsSync(this.tokenDir)||g.mkdirSync(this.tokenDir,{recursive:!0})}saveToken(e){this.ensureTokenDir();const t={...e,savedAt:Date.now(),expiresAt:Date.now()+1e3*(e.expires_in||7200)};g.writeFileSync(this.tokenFile,JSON.stringify(t,null,2),"utf-8")}readToken(){if(!g.existsSync(this.tokenFile))return null;try{return JSON.parse(g.readFileSync(this.tokenFile,"utf-8"))}catch(e){return U(`读取 token 文件失败: ${e.message||e.msg}`),null}}isTokenExpired(e){return!e||!e.expiresAt||Date.now()>=e.expiresAt-3e5}clearToken(){g.existsSync(this.tokenFile)&&g.unlinkSync(this.tokenFile)}getRedirectURI(e){return`http://localhost:${e}`}buildAuthUrl(e){const t=new URLSearchParams({response_type:this.response_type,client_id:this.client_id,redirect_uri:e,scope:this.scope,oauthType:this.oauthType,access_type:this.access_type});return`${this.loginURL}?${t.toString()}`}async openBrowser(e){try{await f(e)}catch(t){U(`无法自动打开浏览器: ${t.message||t.msg}`),console.log(`\n请手动访问以下 URL 进行授权:\n${e}\n`)}}getHtmlTemplate(e,t={}){try{let n=g.readFileSync(e,"utf-8");return Object.keys(t).forEach(e=>{const s=`{{${e}}}`;n=n.replace(new RegExp(s,"g"),t[e])}),n}catch(e){return U(`读取 HTML 模板失败: ${e.message||e.msg}`),"<html><body><h1>页面加载失败</h1></body></html>"}}async isPortInUse(e){return new Promise(t=>{const n=T.createServer();n.once("error",e=>{"EADDRINUSE"===e.code?t(!0):t(!1)}),n.once("listening",()=>{n.once("close",()=>{t(!1)}),n.close()}),n.listen(e)})}async startCallbackServer(){let e=this.redirectUri,t=new URL(e),n=parseInt(t.port,10);await this.isPortInUse(n)&&(U(`\n警告: 端口 ${n} 已被占用,请调整 redirectUri 配置项,使其指向一个未被占用的端口。`),process.exit(1));const s=new Promise((s,r)=>{const o=k.createServer(async(n,i)=>{const a=_.parse(n.url,!0);if(a.pathname===t.pathname||"/"===a.pathname){const t=a.query.code,n=a.query.error;if(n){i.writeHead(400,{"Content-Type":"text/html; charset=utf-8"});const e=this.getHtmlTemplate(this.authErrorTemplate,{ERROR:n});return i.end(e),o.close(),void r(new Error(`授权失败: ${n}`))}if(t)try{const n=await this.getTokenByCode(t,e);this.saveToken(n);const r=n.userInfo.tenantName?`${n.userInfo.tenantName}(${n.tenant_id})`:n.tenant_id||"未返回",a=n.instance_uri||"未返回";i.writeHead(200,{"Content-Type":"text/html; charset=utf-8"});const c=this.getHtmlTemplate(this.authSuccessTemplate,{TENANT_ID:r,INSTANCE_URI:a});return i.end(c),o.close(),void s({code:t,tokenData:n})}catch(e){i.writeHead(500,{"Content-Type":"text/html; charset=utf-8"});const t=this.getHtmlTemplate(this.tokenErrorTemplate,{ERROR:e});return i.end(t),o.close(),void r(e)}i.writeHead(400,{"Content-Type":"text/html; charset=utf-8"});const c=this.getHtmlTemplate(this.authFailedTemplate);i.end(c),o.close(),r(new Error("未获取到授权码"))}else i.writeHead(404,{"Content-Type":"text/plain"}),i.end("Not Found")});o.on("error",e=>{"EADDRINUSE"===e.code?r(new Error(`端口 ${n} 已被占用,无法启动回调服务器`)):r(e)}),o.listen(n,()=>{console.log(`\n本地回调服务器已启动,监听端口: ${n}`),console.log(`回调地址: ${e}`)}),setTimeout(()=>{o.close(),r(new Error("授权超时,请重试"))},3e5)});return{redirectUri:e,getCodeAndTokenPromise:s}}async getTokenByCode(e,t){const n=y("正在获取 access token...").start();try{const s=new URLSearchParams;s.append("grant_type",this.grant_type),s.append("client_id",this.client_id),s.append("client_secret",this.client_secret),s.append("code",e),s.append("redirect_uri",t);const r=(await d.post(this.tokenURL,s.toString(),{headers:{"Content-Type":"application/x-www-form-urlencoded"}})).data;if(r&&r.access_token){r&&r.instance_uri&&r.instance_uri!==this.neoBaseURL&&(this.neoBaseURL=r.instance_uri,$.setEnvConfig({neoBaseURL:r.instance_uri}));const e=await this.getUserInfo(r.access_token);e&&e.tenantName&&e.name?r.userInfo=e:U("获取用户信息失败:",e)}else U("获取 token 失败:响应中未包含 access_token",n),U(`响应数据: ${JSON.stringify(r)}`),process.exit(1);return R("成功获取 access token",n),r}catch(e){U("获取 token 失败",n),U(`\n获取 token 失败: ${e.message||e.msg}`),e.response&&U(`响应数据: ${JSON.stringify(e.response.data)}`),process.exit(1)}}async refreshToken(e){const t=y("正在刷新授权信息(token)...").start();try{const n=new URLSearchParams;n.append("grant_type","refresh_token"),n.append("client_id",this.client_id),n.append("client_secret",this.client_secret),n.append("refresh_token",e);const s=(await d.post(this.tokenURL,n.toString(),{headers:{"Content-Type":"application/x-www-form-urlencoded"}})).data;if(!s||!s.access_token)return U("刷新授权信息失败:响应中未包含 access_token",t),U(`响应数据: ${JSON.stringify(s)}`),null;{const e=await this.getUserInfo(s.access_token);s.userInfo=e}return R("刷新授权信息成功(token)。",t),s}catch(e){return U("刷新授权信息失败",t),U(`\n刷新授权信息失败: ${e.message||e.msg}`),e.response&&U(`响应数据: ${JSON.stringify(e.response.data)}`),null}}async getUserInfo(e){let t={};try{let n=this.buildFullUrl(this.NeoCrmAPI.getUserInfoAPI);const s=await d.get(n,{headers:{Authorization:`Bearer ${e}`,"xsy-inner-source":"bff","Content-Type":"application/json"}}),{code:r,message:o,msg:i}=s.data||{};r&&"200"!==r&&(U(`获取用户信息失败: ${o||i||"未知错误"}`),process.exit(1)),t=s.data.result||s.data.data||{}}catch(e){const t=e.message||e.msg;U(t?`获取用户信息失败: ${t}`:`响应数据: ${JSON.stringify(e)}`),process.exit(1)}return t}async login(){console.log("\n========== NeoCRM 登录授权 ==========\n");try{const{redirectUri:e,getCodeAndTokenPromise:t}=await this.startCallbackServer(),n=this.buildAuthUrl(e);console.log("授权 URL:",n),console.log("\n正在打开浏览器进行授权..."),await this.openBrowser(n);const{code:s,tokenData:r}=await t;return R("\n✓ 已获取授权码"),console.log("\n========== 登录成功 ==========\n"),console.log(`实例地址: ${r.instance_uri||"未返回"}`),console.log(`当前租户信息: ${r.userInfo.tenantName||"未返回租户名称"}(${r.tenant_id||"未返回租户 ID"})`),console.log(`当前登录用户: ${r.userInfo.name||"未返回用户名"}`),r}catch(e){U(`\n登录失败: ${e.message||e.msg}`),process.exit(1)}}async logout(){if(console.log("\n========== NeoCRM 登出 ==========\n"),g.existsSync(this.tokenFile))try{$.clearEnvConfig(),this.clearToken(),R("已清除授权信息,下次登录需要重新授权。"),console.log("\n登出成功!\n")}catch(e){U(`登出失败: ${e.message||e.msg}`),process.exit(1)}else console.log("当前未登录,无需登出。")}async getValidToken(){const e=this.readToken();if(e||(U("未找到授权信息,请先执行 neo login 进行登录。"),process.exit(1)),this.isTokenExpired(e)){console.log("授权信息已过期,正在尝试刷新..."),e.refresh_token||(U("自动刷新授权信息失败,请重新登录(neo login)。"),process.exit(1));const t=await this.refreshToken(e.refresh_token);return t||(U("刷新授权信息失败,请重新登录 (neo login)"),process.exit(1)),this.saveToken(t),t}return e}async getAccessToken(){return await this.getValidToken()}buildFullUrl(e){
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("axios"),t=require("node:fs"),n=require("node:path"),s=require("ora"),r=require("node:http"),o=require("node:url"),i=require("open"),a=require("node:net"),c=require("lodash"),l=require("../utils/common.js"),h=require("./neoEnvManager.js");var p,u;exports.__require=function(){if(u)return p;u=1;const d=e,g=t,m=n,y=s,k=r,_=o,f=i,T=a,w=c,{errorLog:U,successLog:R}=l.__require(),$=h.__require();return p=class{constructor(e){if(this.tokenDir=m.join(process.cwd(),".neo-cli"),this.tokenFile=m.join(this.tokenDir,"token.json"),this.pageDir=m.join(__dirname,"../../template/pageHtml"),this.authErrorTemplate=m.join(this.pageDir,"auth-error.html"),this.authSuccessTemplate=m.join(this.pageDir,"auth-success.html"),this.tokenErrorTemplate=m.join(this.pageDir,"token-error.html"),this.authFailedTemplate=m.join(this.pageDir,"auth-failed.html"),!e)return;const{neoBaseURL:t,loginURL:n,tokenURL:s}=e,r=$.getAuthConfig(),{redirectUri:o,authType:i,auth:a}=r;if(!n||!s)throw new Error("neo.config.js 配置不完整,需要包含 loginURL、tokenURL");this.neoBaseURL=t,this.loginURL=n,this.tokenURL=s,this.NeoCrmAPI=r,this.redirectUri=o,this.authType=i||"oauth2",this.response_type=a.response_type||"code",this.client_id=a.client_id,this.client_secret=a.client_secret,this.scope=a.scope||"all",this.oauthType=a.oauthType||"standard",this.access_type=a.access_type||"offline",this.grant_type=a.grant_type||"authorization_code";try{$.setEnvConfig(w.pick(e,["neoBaseURL","loginURL","tokenURL"]))}catch(e){U(`保存环境配置失败: ${e.message||e.msg}`)}}ensureTokenDir(){g.existsSync(this.tokenDir)||g.mkdirSync(this.tokenDir,{recursive:!0})}saveToken(e){this.ensureTokenDir();const t={...e,savedAt:Date.now(),expiresAt:Date.now()+1e3*(e.expires_in||7200)};g.writeFileSync(this.tokenFile,JSON.stringify(t,null,2),"utf-8")}readToken(){if(!g.existsSync(this.tokenFile))return null;try{return JSON.parse(g.readFileSync(this.tokenFile,"utf-8"))}catch(e){return U(`读取 token 文件失败: ${e.message||e.msg}`),null}}isTokenExpired(e){return!e||!e.expiresAt||Date.now()>=e.expiresAt-3e5}clearToken(){g.existsSync(this.tokenFile)&&g.unlinkSync(this.tokenFile)}getRedirectURI(e){return`http://localhost:${e}`}buildAuthUrl(e){const t=new URLSearchParams({response_type:this.response_type,client_id:this.client_id,redirect_uri:e,scope:this.scope,oauthType:this.oauthType,access_type:this.access_type});return`${this.loginURL}?${t.toString()}`}async openBrowser(e){try{await f(e)}catch(t){U(`无法自动打开浏览器: ${t.message||t.msg}`),console.log(`\n请手动访问以下 URL 进行授权:\n${e}\n`)}}getHtmlTemplate(e,t={}){try{let n=g.readFileSync(e,"utf-8");return Object.keys(t).forEach(e=>{const s=`{{${e}}}`;n=n.replace(new RegExp(s,"g"),t[e])}),n}catch(e){return U(`读取 HTML 模板失败: ${e.message||e.msg}`),"<html><body><h1>页面加载失败</h1></body></html>"}}async isPortInUse(e){return new Promise(t=>{const n=T.createServer();n.once("error",e=>{"EADDRINUSE"===e.code?t(!0):t(!1)}),n.once("listening",()=>{n.once("close",()=>{t(!1)}),n.close()}),n.listen(e)})}async startCallbackServer(){let e=this.redirectUri,t=new URL(e),n=parseInt(t.port,10);await this.isPortInUse(n)&&(U(`\n警告: 端口 ${n} 已被占用,请调整 redirectUri 配置项,使其指向一个未被占用的端口。`),process.exit(1));const s=new Promise((s,r)=>{const o=k.createServer(async(n,i)=>{const a=_.parse(n.url,!0);if(a.pathname===t.pathname||"/"===a.pathname){const t=a.query.code,n=a.query.error;if(n){i.writeHead(400,{"Content-Type":"text/html; charset=utf-8"});const e=this.getHtmlTemplate(this.authErrorTemplate,{ERROR:n});return i.end(e),o.close(),void r(new Error(`授权失败: ${n}`))}if(t)try{const n=await this.getTokenByCode(t,e);this.saveToken(n);const r=n.userInfo.tenantName?`${n.userInfo.tenantName}(${n.tenant_id})`:n.tenant_id||"未返回",a=n.instance_uri||"未返回";i.writeHead(200,{"Content-Type":"text/html; charset=utf-8"});const c=this.getHtmlTemplate(this.authSuccessTemplate,{TENANT_ID:r,INSTANCE_URI:a});return i.end(c),o.close(),void s({code:t,tokenData:n})}catch(e){i.writeHead(500,{"Content-Type":"text/html; charset=utf-8"});const t=this.getHtmlTemplate(this.tokenErrorTemplate,{ERROR:e});return i.end(t),o.close(),void r(e)}i.writeHead(400,{"Content-Type":"text/html; charset=utf-8"});const c=this.getHtmlTemplate(this.authFailedTemplate);i.end(c),o.close(),r(new Error("未获取到授权码"))}else i.writeHead(404,{"Content-Type":"text/plain"}),i.end("Not Found")});o.on("error",e=>{"EADDRINUSE"===e.code?r(new Error(`端口 ${n} 已被占用,无法启动回调服务器`)):r(e)}),o.listen(n,()=>{console.log(`\n本地回调服务器已启动,监听端口: ${n}`),console.log(`回调地址: ${e}`)}),setTimeout(()=>{o.close(),r(new Error("授权超时,请重试"))},3e5)});return{redirectUri:e,getCodeAndTokenPromise:s}}async getTokenByCode(e,t){const n=y("正在获取 access token...").start();try{const s=new URLSearchParams;s.append("grant_type",this.grant_type),s.append("client_id",this.client_id),s.append("client_secret",this.client_secret),s.append("code",e),s.append("redirect_uri",t);const r=(await d.post(this.tokenURL,s.toString(),{headers:{"Content-Type":"application/x-www-form-urlencoded"}})).data;if(r&&r.access_token){r&&r.instance_uri&&r.instance_uri!==this.neoBaseURL&&(this.neoBaseURL=r.instance_uri,$.setEnvConfig({neoBaseURL:r.instance_uri}));const e=await this.getUserInfo(r.access_token);e&&e.tenantName&&e.name?r.userInfo=e:U("获取用户信息失败:",e)}else U("获取 token 失败:响应中未包含 access_token",n),U(`响应数据: ${JSON.stringify(r)}`),process.exit(1);return R("成功获取 access token",n),r}catch(e){U("获取 token 失败",n),U(`\n获取 token 失败: ${e.message||e.msg}`),e.response&&U(`响应数据: ${JSON.stringify(e.response.data)}`),process.exit(1)}}async refreshToken(e){const t=y("正在刷新授权信息(token)...").start();try{const n=new URLSearchParams;n.append("grant_type","refresh_token"),n.append("client_id",this.client_id),n.append("client_secret",this.client_secret),n.append("refresh_token",e);const s=(await d.post(this.tokenURL,n.toString(),{headers:{"Content-Type":"application/x-www-form-urlencoded"}})).data;if(!s||!s.access_token)return U("刷新授权信息失败:响应中未包含 access_token",t),U(`响应数据: ${JSON.stringify(s)}`),null;{const e=await this.getUserInfo(s.access_token);s.userInfo=e}return R("刷新授权信息成功(token)。",t),s}catch(e){return U("刷新授权信息失败",t),U(`\n刷新授权信息失败: ${e.message||e.msg}`),e.response&&U(`响应数据: ${JSON.stringify(e.response.data)}`),null}}async getUserInfo(e){let t={};try{let n=this.buildFullUrl(this.NeoCrmAPI.getUserInfoAPI);const s=await d.get(n,{headers:{Authorization:`Bearer ${e}`,"xsy-inner-source":"bff","Content-Type":"application/json"}}),{code:r,message:o,msg:i}=s.data||{};r&&"200"!==r&&(U(`获取用户信息失败: ${o||i||"未知错误"}`),process.exit(1)),t=s.data.result||s.data.data||{}}catch(e){const t=e.message||e.msg;U(t?`获取用户信息失败: ${t}`:`响应数据: ${JSON.stringify(e)}`),process.exit(1)}return t}async login(){console.log("\n========== NeoCRM 登录授权 ==========\n");try{const{redirectUri:e,getCodeAndTokenPromise:t}=await this.startCallbackServer(),n=this.buildAuthUrl(e);console.log("授权 URL:",n),console.log("\n正在打开浏览器进行授权..."),await this.openBrowser(n);const{code:s,tokenData:r}=await t;return R("\n✓ 已获取授权码"),console.log("\n========== 登录成功 ==========\n"),console.log(`实例地址: ${r.instance_uri||"未返回"}`),console.log(`当前租户信息: ${r.userInfo.tenantName||"未返回租户名称"}(${r.tenant_id||"未返回租户 ID"})`),console.log(`当前登录用户: ${r.userInfo.name||"未返回用户名"}`),r}catch(e){U(`\n登录失败: ${e.message||e.msg}`),process.exit(1)}}async logout(){if(console.log("\n========== NeoCRM 登出 ==========\n"),g.existsSync(this.tokenFile))try{$.clearEnvConfig(),this.clearToken(),R("已清除授权信息,下次登录需要重新授权。"),console.log("\n登出成功!\n")}catch(e){U(`登出失败: ${e.message||e.msg}`),process.exit(1)}else console.log("当前未登录,无需登出。")}async getValidToken(){const e=this.readToken();if(e||(U("未找到授权信息,请先执行 neo login 进行登录。"),process.exit(1)),this.isTokenExpired(e)){console.log("授权信息已过期,正在尝试刷新..."),e.refresh_token||(U("自动刷新授权信息失败,请重新登录(neo login)。"),process.exit(1));const t=await this.refreshToken(e.refresh_token);return t||(U("刷新授权信息失败,请重新登录 (neo login)"),process.exit(1)),this.saveToken(t),t}return e}async getAccessToken(){return await this.getValidToken()}buildFullUrl(e){if(e.startsWith("http://")||e.startsWith("https://"))return e;let t=this.neoBaseURL;return t.endsWith("/")&&(t=t.substring(0,t.length-1)),`${t}${e}`}}};
|
package/dist/neo/neoRequire.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const o=require("lodash");var e,n;exports.__require=function(){if(n)return e;n=1;const s=o,m={react:"^16.13.1","react-dom":"^16.13.1",mobx:"^6.3.0","mobx-react":"^7.0.0","mobx-state-tree":"^5.4.0",axios:"^1.7.0",classnames:"^2.3.2",qs:"^6.11.0",lodash:"^4.17.23",
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const o=require("lodash");var e,n;exports.__require=function(){if(n)return e;n=1;const s=o,m={react:"^16.13.1","react-dom":"^16.13.1",mobx:"^6.3.0","mobx-react":"^7.0.0","mobx-state-tree":"^5.4.0",axios:"^1.7.0",classnames:"^2.3.2",qs:"^6.11.0",lodash:"^4.17.23",amis:"^1.1.5","neo-ui-component-web":"^1.0.0","neo-ui-component-h5":"^1.0.0","neo-ui-common":"^1.0.0"};return e={initNeoRequire:()=>{window.neoRequire||(window.neoRequire=o=>window.__NeoCommonModules[o]||window[o])},addNeoCommonModules:o=>{if(window.__NeoCommonModules||(window.__NeoCommonModules={}),isPlainObject(o)){Object.keys(o).forEach(e=>{const n=o[e],s=window.__NeoCommonModules[e];if(!Object.isFrozen(s)&&s&&Object.keys(s).length<3)try{window.__NeoCommonModules[e]=Object.assign(s,n),void 0!==n.__esModule&&(window.__NeoCommonModules[e].__esModule=n.__esModule),void 0!==n.default&&(window.__NeoCommonModules[e].default=n.default)}catch(o){console.warn(`window.__NeoCommonModules[${e}] Object.assign error:`,o),window.__NeoCommonModules[e]=n}else window.__NeoCommonModules[e]=n})}},addNeoRemoteDeps:o=>{window.__NeoCommonModules||(window.__NeoCommonModules={}),window.__NeoCommonModules.__neoRemoteDeps||(window.__NeoCommonModules.__neoRemoteDeps={}),s.isPlainObject(o)&&(window.__NeoCommonModules.__neoRemoteDeps=Object.assign(window.__NeoCommonModules.__neoRemoteDeps,o))},getExternalsByNeoCommonModules:o=>{const e={};return Object.keys(m).forEach(o=>{e[o]=`commonjs ${o}`}),o&&o.length>0&&o.forEach(o=>{e[o]=`commonjs ${o}`}),e}}};
|
package/dist/package.json.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});var e="1.13.
|
|
1
|
+
"use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});var e="1.13.15";const o={version:e};exports.default=o,exports.version=e;
|
package/package.json
CHANGED
|
@@ -19,12 +19,6 @@ Open API:根据商机业务类型查询商机阶段
|
|
|
19
19
|
|
|
20
20
|
接口URL:`/rest/neobi/v2.0/query/queryByCustomSQL`
|
|
21
21
|
|
|
22
|
-
| 字段 | 类型 | 说明 |
|
|
23
|
-
| --------- | ------ | ------------------------ |
|
|
24
|
-
| id | String | 商机阶段ID |
|
|
25
|
-
| stageName | String | 商机阶段名称 |
|
|
26
|
-
| order | Number | 序号 |
|
|
27
|
-
| status | String | 状态(进行中/输单/赢单) |
|
|
28
22
|
|
|
29
23
|
## 1.2 商机信息
|
|
30
24
|
|
|
@@ -32,24 +26,6 @@ Open API:获取销售机会信息
|
|
|
32
26
|
|
|
33
27
|
接口URL:`/rest/data/v2.0/xobjects/opportunity/{id}`
|
|
34
28
|
|
|
35
|
-
| 字段 | 类型 | 说明 |
|
|
36
|
-
| ---------------- | --------------- | ----------------------------------------------------- |
|
|
37
|
-
| opportunityId | String | 商机ID |
|
|
38
|
-
| opportunityName | String | 商机名称 |
|
|
39
|
-
| accountName | String | 客户名称 |
|
|
40
|
-
| amount | Number | 商机金额 |
|
|
41
|
-
| closeDate | Date | 预计结单日期 |
|
|
42
|
-
| stageId | String | 当前阶段ID |
|
|
43
|
-
| stageName | String | 当前阶段名称 |
|
|
44
|
-
| ownerUserId | String | 商机负责人用户ID |
|
|
45
|
-
| ownerDeptId | String | 商机负责人所属部门ID |
|
|
46
|
-
| businessType | String | 业务类型 |
|
|
47
|
-
| lastActivityDate | DateTime | 最近活动时间 |
|
|
48
|
-
| createdTime | DateTime | 商机创建时间 |
|
|
49
|
-
| winRate | Number | (自定义字段)最终AI赢率(0-100) |
|
|
50
|
-
| baseline | Number | (自定义字段)基线概率(0-100),基于历史同类商机统计 |
|
|
51
|
-
| positiveFactors | Array\<Object\> | (自定义字段)正向因素列表,每项含description和impact |
|
|
52
|
-
| negativeFactors | Array\<Object\> | (自定义字段)负向因素列表,每项含description和impact |
|
|
53
29
|
|
|
54
30
|
## 1.3 自定义实体数据查询
|
|
55
31
|
|
|
@@ -62,29 +38,18 @@ Open API:获取销售机会信息
|
|
|
62
38
|
|
|
63
39
|
> 接口URL:`/rest/neobi/v2.0/query/queryByCustomSQL`
|
|
64
40
|
|
|
65
|
-
|
|
41
|
+
> 接口示例:
|
|
66
42
|
|
|
67
43
|
select opportunity_1_opportunityName,
|
|
68
44
|
opportunity_1_money
|
|
69
45
|
from biCustomModel_397169_20260401104916618 limit 100
|
|
70
46
|
|
|
71
|
-
|
|
72
47
|
## 1.5 商机健康度评估信息
|
|
73
48
|
|
|
74
49
|
> Open API:没有Open API,内部可调用业务接口,获取商机健康度结果接口
|
|
75
50
|
|
|
76
51
|
> 接口URL:/rest/ai/v2.0/agent/apps/health_assessment/get_health_assessment_result
|
|
77
52
|
|
|
78
|
-
| 字段 | 类型 | 说明 |
|
|
79
|
-
| ---------------------------- | ------ | ------------------------------------------ |
|
|
80
|
-
| opportunityId | String | 商机ID |
|
|
81
|
-
| level | String | 评分等级:`High` / `Medium` / `Low` |
|
|
82
|
-
| summary | String | AI生成的自然语言摘要,概述商机当前健康状态 |
|
|
83
|
-
| dimensions | Object | 四维评分明细 |
|
|
84
|
-
| dimensions.demand | String | 需求维度评级 |
|
|
85
|
-
| dimensions.decisionChain | String | 决策链维度评级 |
|
|
86
|
-
| dimensions.budgetProcurement | String | 预算和采购流程维度评级 |
|
|
87
|
-
| dimensions.competition | String | 竞争维度评级 |
|
|
88
53
|
|
|
89
54
|
## 1.6 部门人员信息
|
|
90
55
|
|
|
@@ -98,12 +63,6 @@ Open API:获取当前用户权限内的商机业务类型列表及系统默认
|
|
|
98
63
|
|
|
99
64
|
接口URL:待确认
|
|
100
65
|
|
|
101
|
-
| 字段 | 类型 | 说明 |
|
|
102
|
-
| -------------------- | --------------- | ------------------------------------------- |
|
|
103
|
-
| businessTypes | Array\<Object\> | 业务类型列表 |
|
|
104
|
-
| businessTypes[].id | String | 业务类型ID |
|
|
105
|
-
| businessTypes[].name | String | 业务类型名称 |
|
|
106
|
-
| defaultBusiType | String | 系统默认业务类型ID(固定为defaultBusiType) |
|
|
107
66
|
|
|
108
67
|
## 1.8 提示词模板调用接口
|
|
109
68
|
|
|
@@ -136,23 +95,17 @@ Open API:按照筛选条件查询视图数据
|
|
|
136
95
|
|
|
137
96
|
页面初始化时调用一次,获取筛选栏所需的选项数据和默认值。
|
|
138
97
|
|
|
139
|
-
|
|
98
|
+
**处理流程**:
|
|
99
|
+
1. 调用1.7部门人员信息接口,获取当前用户可见的部门架构及人员列表;默认选择当前用户
|
|
100
|
+
2. 调用1.8权限内业务类型接口,获取业务类型列表;默认选择defaultBusiType(API Key)
|
|
140
101
|
|
|
141
|
-
**返回**:
|
|
142
102
|
|
|
143
|
-
| 字段
|
|
144
|
-
|
|
|
145
|
-
|
|
|
146
|
-
|
|
|
147
|
-
|
|
|
148
|
-
| defaultBusiType | String | 系统默认业务类型ID,用于筛选栏Business Type的默认选中值 |
|
|
103
|
+
| 字段 | Opp API Key
|
|
104
|
+
| ---------------- | --------------- |
|
|
105
|
+
| closeDate | closeDate |
|
|
106
|
+
| Owner | ownerId |
|
|
107
|
+
| Entity Type | entityType |
|
|
149
108
|
|
|
150
|
-
**处理流程**:
|
|
151
|
-
|
|
152
|
-
1. 调用1.7部门人员信息接口,获取当前用户可见的部门架构及人员列表
|
|
153
|
-
2. 调用1.8权限内业务类型接口,获取业务类型列表及系统默认值(defaultBusiType)
|
|
154
|
-
3. 获取当前登录用户信息(userId, name)
|
|
155
|
-
4. 组装返回
|
|
156
109
|
|
|
157
110
|
## 2.2 AGG-2:获取BI视图与AI洞察
|
|
158
111
|
|
|
@@ -175,7 +128,7 @@ Open API:按照筛选条件查询视图数据
|
|
|
175
128
|
1. **商机漏斗**:前端按当前筛选条件组装请求参数,调用1.9接口(viewId=4264464770007375),将返回的数据用于漏斗图渲染(各阶段金额、数量、转化率等)
|
|
176
129
|
2. **平均停留时间**:
|
|
177
130
|
- 调用1.9接口(viewId=4264466340118875),获取各阶段的实际平均停留时间
|
|
178
|
-
- 调用1.3自定义实体数据查询接口,查询自定义对象**Opportunity Stage Plan**(APIKey:customEntity105__c),根据当前商机业务类型opp type
|
|
131
|
+
- 调用1.3自定义实体数据查询接口,查询自定义对象**Opportunity Stage Plan**(APIKey:customEntity105__c),根据当前商机业务类型opp type(APIKey:customItem1__c)+阶段Stage Name(APIKey:customItem2__c),获取各阶段的Target(基准值Reference Advancing Duration(APIKey:customItem3__c))和Limit(保底值Longest Advancing Duration(APIKey:customItem4__c))
|
|
179
132
|
- 前端将实际停留时间与Target/Limit合并渲染条形图
|
|
180
133
|
|
|
181
134
|
|
|
@@ -189,8 +142,8 @@ Open API:按照筛选条件查询视图数据
|
|
|
189
142
|
|
|
190
143
|
| 参数 | 类型 | 说明 |
|
|
191
144
|
|------|------|------|
|
|
192
|
-
| Raw Data | String | BI看板数据(2.2.1返回的columns+rows)+ 基准值/保底值(仅平均停留时间看板需要附加) |
|
|
193
|
-
| business_context
|
|
145
|
+
| Raw Data(APIKey:raw_data) | String | BI看板数据(2.2.1返回的columns+rows)+ 基准值/保底值(仅平均停留时间看板需要附加) |
|
|
146
|
+
| business_context(APIKey:business_context)| String | 背景说明,根据当前BI看板类型传入固定文案(见下方) |
|
|
194
147
|
|
|
195
148
|
**business_context固定文案**:
|
|
196
149
|
|
|
@@ -215,6 +168,12 @@ Open API:按照筛选条件查询视图数据
|
|
|
215
168
|
|
|
216
169
|
1. 调用1.1商机阶段信息接口,获取当前业务类型的阶段列表(含排序)
|
|
217
170
|
2. 调用1.2商机信息接口,按筛选条件(Close Date、Owner、Business Type)批量查询商机,按阶段分组汇总当前金额和数量
|
|
171
|
+
|
|
172
|
+
| 字段 | Opp API Key |
|
|
173
|
+
| ---------------- | --------------- |
|
|
174
|
+
| Amount | money |
|
|
175
|
+
| Sales Stage | saleStageId |
|
|
176
|
+
|
|
218
177
|
3. 调用1.4商机历史快照接口,查询changesSince基准日期的快照数据,按阶段分组汇总基准金额和基准数量
|
|
219
178
|
|
|
220
179
|
**历史快照字段参数**:
|
|
@@ -225,25 +184,17 @@ Open API:按照筛选条件查询视图数据
|
|
|
225
184
|
|-------|---------|
|
|
226
185
|
| ID | id |
|
|
227
186
|
| Amount | opportunity_1_money |
|
|
228
|
-
| Owner department | opportunity_1_dimDepartEntity |
|
|
229
|
-
| Type | opportunity_1_entityType |
|
|
230
|
-
| Forecast Category | opportunity_1_forecastCategory |
|
|
231
|
-
| Status | opportunity_1_status |
|
|
232
|
-
| version | version |
|
|
233
187
|
| Close Date | opportunity_1_closeDate |
|
|
234
|
-
| Recent Activity Record Time | opportunity_1_recentActivityRecordTime |
|
|
235
188
|
| ID | opportunity_1__id |
|
|
236
|
-
| Opportunity | opportunity_1_id |
|
|
237
|
-
| Account Name | opportunity_1_accountId |
|
|
238
189
|
| Sales Stage | opportunity_1_saleStageId |
|
|
239
190
|
| Opportunity Name | opportunity_1_opportunityName |
|
|
240
|
-
|
|
241
|
-
| Opportunity Owner | opportunity_1_ownerId |
|
|
191
|
+
|
|
242
192
|
4. 金额变动 = 当前阶段金额 − 基准快照阶段金额;数量变动 = 当前阶段数量 − 基准快照阶段数量
|
|
243
193
|
5. 判断变动方向(up/down/none)
|
|
244
194
|
6. 组装stageTabs数据返回
|
|
245
195
|
|
|
246
196
|
|
|
197
|
+
|
|
247
198
|
## 2.4 AGG-4:获取商机列表数据
|
|
248
199
|
|
|
249
200
|
阶段切换或筛选变更时调用,分两个阶段加载数据:列表首屏加载和Hover按需加载。
|
|
@@ -255,6 +206,16 @@ Open API:按照筛选条件查询视图数据
|
|
|
255
206
|
1. 调用1.2商机信息接口,按筛选条件+阶段(默认选择第一个阶段)+排序(默认按照销售金额倒叙)+分页查询商机列表,获取所有列表展示字段
|
|
256
207
|
2. AI Score等级标签(level字段)可从商机对象直接获取,无需调用1.6,随列表一并返回
|
|
257
208
|
|
|
209
|
+
|
|
210
|
+
| Field | Opp API Key |
|
|
211
|
+
|-------|---------|
|
|
212
|
+
| Opportunity Name | opportunityName |
|
|
213
|
+
| Amount | money |
|
|
214
|
+
| Close Date |closeDate |
|
|
215
|
+
| Stage | saleStageId|
|
|
216
|
+
| AI Score | oppHealthAssessmentScore |
|
|
217
|
+
| Last Activitvy | recentActivityRecordTime |
|
|
218
|
+
|
|
258
219
|
### 2.4.2 Hover按需加载
|
|
259
220
|
|
|
260
221
|
以下数据仅在用户鼠标hover时触发,按需调用,避免首屏加载过重。
|
|
@@ -265,10 +226,7 @@ Open API:按照筛选条件查询视图数据
|
|
|
265
226
|
- 调用接口:1.5商机历史快照接口
|
|
266
227
|
- 入参:opportunityId + changesSince基准日期
|
|
267
228
|
- 处理:查询该商机在基准日期的快照,与当前实时数据逐字段(Amount/CloseDate/Stage)比对,用当前值-基准日期的值,计算变动方向、变动前后值
|
|
268
|
-
|
|
269
|
-
- 变动时间:调用1.2接口,取商机上的3个时间字段,AmoutChanged、ClosedDateChanged、StageChanged
|
|
270
|
-
|
|
271
|
-
|
|
229
|
+
- 变动时间:调用1.2接口,取商机上的3个时间字段,AmoutChanged(API Key:customItem246__c)、ClosedDateChanged(API Key:customItem247__c)、StageChanged(API Key:customItem245__c)
|
|
272
230
|
|
|
273
231
|
|
|
274
232
|
**AI Score详情**:
|
|
@@ -277,3 +235,17 @@ Open API:按照筛选条件查询视图数据
|
|
|
277
235
|
- 调用接口:1.6商机健康度评估信息接口
|
|
278
236
|
- 入参:opportunityId
|
|
279
237
|
- 返回:四维评分及AI摘要summary
|
|
238
|
+
|
|
239
|
+
**WinRate详情**:
|
|
240
|
+
|
|
241
|
+
- 触发时机:用户hover某条商机的AI Win Rate标签
|
|
242
|
+
- 调用接口:1.2查询商机
|
|
243
|
+
- 入参:opportunityId
|
|
244
|
+
- 返回:如下
|
|
245
|
+
|
|
246
|
+
| Field | Opp API Key |
|
|
247
|
+
|-------|---------|
|
|
248
|
+
| baseline | customItem244__c |
|
|
249
|
+
| positiveFactors | customItem241__c |
|
|
250
|
+
| negativeFactors |customItem242__c |
|
|
251
|
+
| winRate | customItem239__c|
|
|
@@ -75,7 +75,9 @@ class AiCommitDrawer extends BaseCmp<AiCommitDrawerProps, AiCommitDrawerState> {
|
|
|
75
75
|
<div className="deal-card-header">
|
|
76
76
|
<div className="deal-info">
|
|
77
77
|
<div className="deal-name">{deal.name}</div>
|
|
78
|
-
<div className="deal-account">
|
|
78
|
+
<div className="deal-account">
|
|
79
|
+
{deal.account} · {deal.amount}
|
|
80
|
+
</div>
|
|
79
81
|
<div className="deal-meta">
|
|
80
82
|
<span className="deal-stage">{deal.stage}</span>
|
|
81
83
|
<span> · Close: {deal.closeDate}</span>
|
|
@@ -93,10 +95,7 @@ class AiCommitDrawer extends BaseCmp<AiCommitDrawerProps, AiCommitDrawerState> {
|
|
|
93
95
|
>
|
|
94
96
|
Prioritize
|
|
95
97
|
</button>
|
|
96
|
-
<button
|
|
97
|
-
className="btn-dismiss"
|
|
98
|
-
onClick={() => onDismiss?.(deal)}
|
|
99
|
-
>
|
|
98
|
+
<button className="btn-dismiss" onClick={() => onDismiss?.(deal)}>
|
|
100
99
|
Dismiss
|
|
101
100
|
</button>
|
|
102
101
|
</div>
|
|
@@ -132,12 +131,16 @@ class AiCommitDrawer extends BaseCmp<AiCommitDrawerProps, AiCommitDrawerState> {
|
|
|
132
131
|
>
|
|
133
132
|
<div className="drawer-header">
|
|
134
133
|
<h3>✨ AI Forecast</h3>
|
|
135
|
-
<button className="close-btn" onClick={this.handleClose}
|
|
134
|
+
<button className="close-btn" onClick={this.handleClose}>
|
|
135
|
+
✕
|
|
136
|
+
</button>
|
|
136
137
|
</div>
|
|
137
138
|
|
|
138
139
|
<div className="drawer-summary">
|
|
139
140
|
<span className="summary-amount">{totalAmount}</span>
|
|
140
|
-
<span className="summary-desc">
|
|
141
|
+
<span className="summary-desc">
|
|
142
|
+
Based on deal health and engagement signals
|
|
143
|
+
</span>
|
|
141
144
|
</div>
|
|
142
145
|
|
|
143
146
|
{/* Closed Won 汇总 */}
|
|
@@ -154,7 +157,9 @@ class AiCommitDrawer extends BaseCmp<AiCommitDrawerProps, AiCommitDrawerState> {
|
|
|
154
157
|
{tabs.map((tab) => (
|
|
155
158
|
<button
|
|
156
159
|
key={tab.key}
|
|
157
|
-
className={`ai-commit-tab ${
|
|
160
|
+
className={`ai-commit-tab ${
|
|
161
|
+
activeTab === tab.key ? 'active' : ''
|
|
162
|
+
}`}
|
|
158
163
|
onClick={() => this.handleTabChange(tab.key)}
|
|
159
164
|
>
|
|
160
165
|
{tab.label} · {tab.amount} · {tab.count} deals
|
|
@@ -164,8 +169,10 @@ class AiCommitDrawer extends BaseCmp<AiCommitDrawerProps, AiCommitDrawerState> {
|
|
|
164
169
|
|
|
165
170
|
{/* Tab内容 */}
|
|
166
171
|
<div className="drawer-content">
|
|
167
|
-
{activeTab === 'pipeline' &&
|
|
168
|
-
|
|
172
|
+
{activeTab === 'pipeline' &&
|
|
173
|
+
pipelineDeals.map((deal) => this.renderDealCard(deal))}
|
|
174
|
+
{activeTab === 'commit' &&
|
|
175
|
+
commitDeals.map((deal) => this.renderDealCard(deal))}
|
|
169
176
|
</div>
|
|
170
177
|
</div>
|
|
171
178
|
</div>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export class AiCommitDrawerModel {
|
|
2
2
|
label: string = 'AI推荐抽屉';
|
|
3
|
-
description: string =
|
|
3
|
+
description: string =
|
|
4
|
+
'展示AI推荐的Commit商机,支持From Pipeline和From Commit两个Tab';
|
|
4
5
|
iconUrl: string = 'https://custom-widgets.bj.bcebos.com/aiCommitDrawer.svg';
|
|
5
6
|
targetPage: string[] = ['all'];
|
|
6
7
|
targetDevice: string = 'all';
|
|
@@ -11,16 +12,56 @@ export class AiCommitDrawerModel {
|
|
|
11
12
|
closedAmount: '$3,200,000',
|
|
12
13
|
closedCount: 5,
|
|
13
14
|
tabs: [
|
|
14
|
-
{
|
|
15
|
+
{
|
|
16
|
+
key: 'pipeline',
|
|
17
|
+
label: 'From Open Pipeline',
|
|
18
|
+
amount: '$2,500,000',
|
|
19
|
+
count: 2,
|
|
20
|
+
},
|
|
15
21
|
{ key: 'commit', label: 'From Commit', amount: '$1,800,000', count: 2 },
|
|
16
22
|
],
|
|
17
23
|
pipelineDeals: [
|
|
18
|
-
{
|
|
19
|
-
|
|
24
|
+
{
|
|
25
|
+
id: '1',
|
|
26
|
+
name: 'Starlight Initiative',
|
|
27
|
+
account: 'Tencent',
|
|
28
|
+
amount: '$500,000',
|
|
29
|
+
stage: 'Pipeline',
|
|
30
|
+
closeDate: '2026-05-20',
|
|
31
|
+
aiScore: '52%',
|
|
32
|
+
description:
|
|
33
|
+
'Strong engagement signals and past wins with this account',
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: '2',
|
|
37
|
+
name: 'Galaxy Partnership',
|
|
38
|
+
account: 'Alibaba',
|
|
39
|
+
amount: '$2,000,000',
|
|
40
|
+
stage: 'Best Case',
|
|
41
|
+
closeDate: '2026-05-10',
|
|
42
|
+
aiScore: '45%',
|
|
43
|
+
description: 'High product usage and active engagement in last 7 days',
|
|
44
|
+
},
|
|
20
45
|
],
|
|
21
46
|
commitDeals: [
|
|
22
|
-
{
|
|
23
|
-
|
|
47
|
+
{
|
|
48
|
+
id: '3',
|
|
49
|
+
name: 'Aurora Solution',
|
|
50
|
+
account: 'ByteDance',
|
|
51
|
+
amount: '$800,000',
|
|
52
|
+
stage: 'Commit',
|
|
53
|
+
closeDate: '2026-04-30',
|
|
54
|
+
aiScore: '91%',
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
id: '4',
|
|
58
|
+
name: 'Apollo Project',
|
|
59
|
+
account: 'Huawei Tech',
|
|
60
|
+
amount: '$1,000,000',
|
|
61
|
+
stage: 'Commit',
|
|
62
|
+
closeDate: '2026-04-15',
|
|
63
|
+
aiScore: '86%',
|
|
64
|
+
},
|
|
24
65
|
],
|
|
25
66
|
};
|
|
26
67
|
|
|
@@ -96,7 +96,9 @@ class FilterBar extends BaseCmp<FilterBarProps, FilterBarState> {
|
|
|
96
96
|
<label>{filter.label}</label>
|
|
97
97
|
<select
|
|
98
98
|
value={values[filter.name] || ''}
|
|
99
|
-
onChange={(e) =>
|
|
99
|
+
onChange={(e) =>
|
|
100
|
+
this.handleFilterChange(filter.name, e.target.value)
|
|
101
|
+
}
|
|
100
102
|
>
|
|
101
103
|
{filter.options?.map((opt) => (
|
|
102
104
|
<option key={opt.value} value={opt.value}>
|
|
@@ -131,16 +133,22 @@ class FilterBar extends BaseCmp<FilterBarProps, FilterBarState> {
|
|
|
131
133
|
<div className="owner-section-label">People</div>
|
|
132
134
|
{['Current User', 'Alice', 'Steve', 'Chloe']
|
|
133
135
|
.filter((name) =>
|
|
134
|
-
name
|
|
136
|
+
name
|
|
137
|
+
.toLowerCase()
|
|
138
|
+
.includes(this.state.ownerSearchText.toLowerCase()),
|
|
135
139
|
)
|
|
136
140
|
.map((name) => (
|
|
137
141
|
<div
|
|
138
142
|
key={name}
|
|
139
|
-
className={`owner-item ${
|
|
143
|
+
className={`owner-item ${
|
|
144
|
+
values[filter.name] === name ? 'selected' : ''
|
|
145
|
+
}`}
|
|
140
146
|
data-name={name}
|
|
141
147
|
onClick={() => this.handleOwnerSelect(name)}
|
|
142
148
|
>
|
|
143
|
-
<span className="owner-check">
|
|
149
|
+
<span className="owner-check">
|
|
150
|
+
{values[filter.name] === name ? '✓' : ''}
|
|
151
|
+
</span>
|
|
144
152
|
<span className="owner-icon">👤</span>
|
|
145
153
|
{name}
|
|
146
154
|
</div>
|
|
@@ -148,16 +156,22 @@ class FilterBar extends BaseCmp<FilterBarProps, FilterBarState> {
|
|
|
148
156
|
<div className="owner-section-label">Departments</div>
|
|
149
157
|
{['Sales Dept', 'Enterprise Team']
|
|
150
158
|
.filter((name) =>
|
|
151
|
-
name
|
|
159
|
+
name
|
|
160
|
+
.toLowerCase()
|
|
161
|
+
.includes(this.state.ownerSearchText.toLowerCase()),
|
|
152
162
|
)
|
|
153
163
|
.map((name) => (
|
|
154
164
|
<div
|
|
155
165
|
key={name}
|
|
156
|
-
className={`owner-item ${
|
|
166
|
+
className={`owner-item ${
|
|
167
|
+
values[filter.name] === name ? 'selected' : ''
|
|
168
|
+
}`}
|
|
157
169
|
data-name={name}
|
|
158
170
|
onClick={() => this.handleOwnerSelect(name)}
|
|
159
171
|
>
|
|
160
|
-
<span className="owner-check">
|
|
172
|
+
<span className="owner-check">
|
|
173
|
+
{values[filter.name] === name ? '✓' : ''}
|
|
174
|
+
</span>
|
|
161
175
|
<span className="owner-icon">🏢</span>
|
|
162
176
|
{name}
|
|
163
177
|
</div>
|
|
@@ -120,23 +120,20 @@ class ForecastChart extends BaseCmp<ForecastChartProps, ForecastChartState> {
|
|
|
120
120
|
</div>
|
|
121
121
|
|
|
122
122
|
{/* AI基准线 */}
|
|
123
|
-
<div
|
|
124
|
-
className="chart-ai-line"
|
|
125
|
-
style={{ top: `${aiPos}%` }}
|
|
126
|
-
>
|
|
123
|
+
<div className="chart-ai-line" style={{ top: `${aiPos}%` }}>
|
|
127
124
|
<span className="ai-label">✨ AI {aiValue}</span>
|
|
128
125
|
</div>
|
|
129
126
|
|
|
130
127
|
{/* 柱状图 */}
|
|
131
128
|
<div className="chart-bars">
|
|
132
129
|
{columns.map((col, index) => {
|
|
133
|
-
const heightPercent = this.getScaleHeight(
|
|
130
|
+
const heightPercent = this.getScaleHeight(
|
|
131
|
+
col.value,
|
|
132
|
+
quotaValue,
|
|
133
|
+
);
|
|
134
134
|
return (
|
|
135
135
|
<div key={index} className="chart-bar">
|
|
136
|
-
<span
|
|
137
|
-
className="bar-value"
|
|
138
|
-
style={{ color: col.color }}
|
|
139
|
-
>
|
|
136
|
+
<span className="bar-value" style={{ color: col.color }}>
|
|
140
137
|
{col.value}
|
|
141
138
|
</span>
|
|
142
139
|
<div
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export class ForecastChartModel {
|
|
2
2
|
label: string = '预测看板图表';
|
|
3
|
-
description: string =
|
|
3
|
+
description: string =
|
|
4
|
+
'展示预测数据的堆叠柱状图,包含Closed、Commit、Best Case、Pipeline等';
|
|
4
5
|
iconUrl: string = 'https://custom-widgets.bj.bcebos.com/forecastChart.svg';
|
|
5
6
|
targetPage: string[] = ['all'];
|
|
6
7
|
targetDevice: string = 'all';
|